NumPy: 图像处理
1. 你将学到
- ❶ 图像 ndarray 表示
- ❷ 像素操作
- ❸ 灰度转换/直方图
- ❹ 阈值处理
- ❺ SVD 压缩
2. 故事
Bob 以为图像处理必须用 OpenCV。Alice:
"图像就是 ndarray——翻转=切片[::-1],裁剪=切片[100:300,200:400],灰度=加权平均。NumPy 就是入门工具,OpenCV 是高级工具。"
3. 图像数据模型
(1) 图像即 ndarray
数字图像在内存中就是一个 ndarray。灰度图是 2D 数组(H×W),彩色图是 3D 数组(H×W×C):
PYTHON
import numpy as np
gray = np.random.randint(0, 256, size=(100, 80), dtype=np.uint8)
print(f"Gray image shape: {gray.shape}") # (100, 80)
print(f"Gray image dtype: {gray.dtype}") # uint8
print(f"Gray image ndim: {gray.ndim}") # 2
color = np.random.randint(0, 256, size=(100, 80, 3), dtype=np.uint8)
print(f"Color image shape: {color.shape}") # (100, 80, 3)
print(f"Color image ndim: {color.ndim}") # 3
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) H×W×C 含义
| 维度 | 索引 | 含义 | 取值范围 |
|---|---|---|---|
| 0 | H | 行(高度,从上到下) | 0 ~ height-1 |
| 1 | W | 列(宽度,从左到右) | 0 ~ width-1 |
| 2 | C | 通道(RGB) | 0=R, 1=G, 2=B |
PYTHON
img = np.zeros((480, 640, 3), dtype=np.uint8)
img[0, 0] # 左上角像素 [R, G, B]
img[0, 0, 0] # 左上角红色通道值
img[479, 639, 2] # 右下角蓝色通道值
img[:, :, 0] # 整幅图的红色通道 (480, 640)
img[100:200, :, 1] # 第100~199行的绿色通道 (100, 640)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(3) 灰度 vs 彩色 ndarray
| 特性 | 灰度图 | 彩色图 |
|---|---|---|
| shape | (H, W) | (H, W, 3) |
| ndim | 2 | 3 |
| 每像素 | 1 个值(亮度) | 3 个值(RGB) |
| 内存 | H×W 字节 | H×W×3 字节 |
| 访问像素 | img[y, x] |
img[y, x, c] |
| dtype | uint8 (0~255) | uint8 (0~255) |
(4) 图像处理流程
graph TB
A["加载图像<br/>ndarray (H,W,3)"] --> B["裁剪/翻转<br/>切片操作"]
B --> C["灰度转换<br/>RGB→Gray"]
C --> D["直方图分析<br/>np.histogram"]
D --> E["阈值处理<br/>二值化"]
C --> F["SVD 压缩<br/>np.linalg.svd"]
E --> G["输出结果"]
F --> G
style A fill:#4CAF50,color:#fff
style B fill:#2196F3,color:#fff
style C fill:#FF9800,color:#fff
style D fill:#9C27B0,color:#fff
style E fill:#F44336,color:#fff
style F fill:#00BCD4,color:#fff
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
4. 像素操作
(1) 创建纯色与渐变图像
PYTHON
import numpy as np
red_img = np.zeros((100, 100, 3), dtype=np.uint8)
red_img[:, :, 0] = 255 # R 通道全满
green_img = np.zeros((100, 100, 3), dtype=np.uint8)
green_img[:, :, 1] = 255
gradient = np.linspace(0, 255, 100, dtype=np.uint8)
gray_grad = np.tile(gradient, (100, 1))
print(f"Gradient shape: {gray_grad.shape}") # (100, 100)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 区域填充
PYTHON
img = np.zeros((200, 200, 3), dtype=np.uint8)
img[50:150, 50:150] = [255, 0, 0] # 红色方块
img[0:50, :] = [255, 255, 255] # 白色顶条
img[150:200, :, 2] = 255 # 底部蓝色条
white_pixel_count = np.sum(np.all(img == [255, 255, 255], axis=-1))
print(f"White pixels: {white_pixel_count}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:像素操作(难度⭐)
PYTHON
import numpy as np
img = np.zeros((100, 100, 3), dtype=np.uint8)
img[25:75, 25:75] = [0, 0, 255]
center = img[49, 49]
print(f"Center pixel: {center}")
img[40:60, 40:60, 1] = 200
print(f"Pixel dtype: {img.dtype}")
print(f"Pixel range: [{img.min()}, {img.max()}]")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
5. 裁剪/翻转/旋转
(1) 裁剪 = 切片
PYTHON
cropped = img[100:300, 200:400] # 行100~299, 列200~399
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 翻转/旋转方式
| 操作 | 代码 | 效果 |
|---|---|---|
| 水平翻转 | img[:, ::-1] |
左右镜像 |
| 垂直翻转 | img[::-1, :] |
上下镜像 |
| 旋转90° (逆时针) | np.rot90(img) |
逆时针90° |
| 旋转90° (顺时针) | np.rot90(img, k=3) |
等价顺时针90° |
| 旋转180° | np.rot90(img, k=2) |
旋转180° |
| 转置 | img.transpose(1, 0, 2) |
行列互换 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:裁剪/翻转/旋转(难度⭐⭐)
PYTHON
import numpy as np
img = np.arange(48, dtype=np.uint8).reshape(4, 4, 3).copy()
print(f"Original shape: {img.shape}")
cropped = img[1:3, 1:3]
print(f"Cropped shape: {cropped.shape}") # (2, 2, 3)
flip_h = img[:, ::-1]
flip_v = img[::-1, :]
rot90 = np.rot90(img)
rot180 = np.rot90(img, k=2)
print(f"Flip H shape: {flip_h.shape}")
print(f"Rot90 shape: {rot90.shape}")
row0_orig = img[0, 0, :]
row0_flipv = flip_v[-1, 0, :]
print(f"Vertical flip preserves rows: {np.array_equal(row0_orig, row0_flipv)}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
6. 灰度转换
(1) RGB → 灰度
人眼对绿色最敏感,因此灰度转换使用加权平均(ITU-R BT.601 标准):
Gray = 0.299 × R + 0.587 × G + 0.114 × B
PYTHON
import numpy as np
color = np.random.randint(0, 256, size=(100, 80, 3), dtype=np.uint8)
gray = np.dot(color[..., :3].astype(np.float64), [0.299, 0.587, 0.114])
gray = np.clip(gray, 0, 255).astype(np.uint8)
print(f"Color shape: {color.shape}") # (100, 80, 3)
print(f"Gray shape: {gray.shape}") # (100, 80)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 灰度转换权重来源
| 通道 | 权重 | 原因 |
|---|---|---|
| R | 0.299 | 人眼对红色敏感度中等 |
| G | 0.587 | 人眼对绿色最敏感 |
| B | 0.114 | 人眼对蓝色最不敏感 |
权重来自 ITU-R BT.601 标准,基于人眼亮度感知实验。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:灰度转换(难度⭐⭐)
PYTHON
import numpy as np
img = np.zeros((50, 50, 3), dtype=np.uint8)
img[10:40, 10:40, 0] = 255 # 红色方块
img[:, 25:, 1] = 200 # 右半绿色
gray = np.dot(img.astype(np.float64), [0.299, 0.587, 0.114])
gray = np.clip(gray, 0, 255).astype(np.uint8)
print(f"Red-only region gray value: {gray[20, 15]}")
print(f"Red+Green region gray value: {gray[20, 35]}")
print(f"Black region gray value: {gray[5, 5]}")
simple_avg = img.mean(axis=2).astype(np.uint8)
print(f"Weighted vs simple diff (mean): "
f"{np.mean(np.abs(gray.astype(int) - simple_avg.astype(int))):.2f}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
7. 直方图
(1) np.histogram
直方图统计每个亮度值出现的像素数,是图像分析的基础工具:
PYTHON
import numpy as np
gray = np.random.randint(0, 256, size=(100, 100), dtype=np.uint8)
hist, bins = np.histogram(gray, bins=256, range=(0, 256))
print(f"Hist length: {len(hist)}") # 256
print(f"Bins length: {len(bins)}") # 257
print(f"Total pixels: {hist.sum()}") # 10000
bright = np.sum(hist[200:])
dark = np.sum(hist[:50])
print(f"Bright pixels (>200): {bright}")
print(f"Dark pixels (<50): {dark}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 各通道直方图
PYTHON
color = np.random.randint(0, 256, size=(100, 100, 3), dtype=np.uint8)
for i, ch in enumerate(["R", "G", "B"]):
h, _ = np.histogram(color[:, :, i], bins=256, range=(0, 256))
print(f"Channel {ch}: mean={color[:,:,i].mean():.1f}, "
f"peak_bin={np.argmax(h)}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:直方图分析(难度⭐⭐)
PYTHON
import numpy as np
dark_img = np.clip(np.random.normal(50, 20, (100, 100)), 0, 255).astype(np.uint8)
bright_img = np.clip(np.random.normal(200, 20, (100, 100)), 0, 255).astype(np.uint8)
for name, img in [("Dark", dark_img), ("Bright", bright_img)]:
hist, _ = np.histogram(img, bins=256, range=(0, 256))
peak = np.argmax(hist)
print(f"{name}: mean={img.mean():.1f}, peak_bin={peak}, "
f"std={img.std():.1f}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
8. 阈值处理
(1) 二值化
阈值处理将灰度图转为黑白图——亮度高于阈值为白,否则为黑:
PYTHON
import numpy as np
gray = np.random.randint(0, 256, size=(100, 100), dtype=np.uint8)
threshold = 128
binary = (gray > threshold).astype(np.uint8) * 255
white_count = np.sum(binary == 255)
black_count = np.sum(binary == 0)
print(f"White: {white_count}, Black: {black_count}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 阈值策略
| 策略 | 代码 | 适用场景 |
|---|---|---|
| 固定阈值 | gray > T |
亮度均匀的图像 |
| 均值阈值 | gray > gray.mean() |
通用 |
| Otsu 阈值 | 需 scipy/skimage | 双峰分布 |
| 自适应 | 分块计算阈值 | 光照不均 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:阈值处理(难度⭐⭐)
PYTHON
import numpy as np
img = np.zeros((100, 100), dtype=np.uint8)
img[20:80, 20:80] = 180
img[30:70, 30:70] = 220
hist, _ = np.histogram(img, bins=256, range=(0, 256))
nonzero_bins = np.where(hist > 0)[0]
print(f"Non-zero bins: {nonzero_bins}")
mean_t = img.mean()
binary_mean = (img > mean_t).astype(np.uint8) * 255
print(f"Mean threshold: {mean_t:.1f}")
print(f"Foreground pixels: {np.sum(binary_mean == 255)}")
for t in [100, 150, 200]:
b = (img > t).astype(np.uint8) * 255
fg = np.sum(b == 255)
print(f"T={t}: foreground={fg}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
9. SVD 压缩
(1) SVD 压缩原理
对灰度图做 SVD 分解:Image = U @ diag(s) @ Vt。保留前 r 个奇异值即可压缩:
- 原始数据量:H × W
- 压缩后数据量:H × r + r + r × W = r × (H + W + 1)
当 r 远小于 min(H, W) 时,压缩率极高。
(2) 压缩率计算
| rank (r) | 存储量 | 压缩率 |
|---|---|---|
| 5 | 5×(100+100+1) = 1005 | 1005/10000 = 10.05% |
| 10 | 10×(100+100+1) = 2010 | 20.1% |
| 20 | 20×(100+100+1) = 4020 | 40.2% |
| 50 | 50×(100+100+1) = 10050 | 100.5% |
原始 100×100 图像数据量 = 10000。当 r > 50 时压缩反而变大,需选择合适的 rank。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:SVD 压缩(难度⭐⭐⭐)
PYTHON
import numpy as np
x = np.linspace(0, 2 * np.pi, 100)
y = np.linspace(0, 2 * np.pi, 100)
X, Y = np.meshgrid(x, y)
image = ((np.sin(X) + np.cos(Y) + 2) / 4 * 255).astype(np.float64)
U, s, Vt = np.linalg.svd(image, full_matrices=False)
total_energy = np.sum(s ** 2)
cumulative = np.cumsum(s ** 2) / total_energy
print(f"{'Rank':>5} {'Compression':>12} {'Energy':>8} {'Rel Error':>10}")
print("-" * 40)
for rank in [5, 10, 20, 50]:
U_r = U[:, :rank]
s_r = s[:rank]
Vt_r = Vt[:rank, :]
compressed = U_r @ np.diag(s_r) @ Vt_r
original_size = 100 * 100
compressed_size = 100 * rank + rank + rank * 100
ratio = compressed_size / original_size
energy = cumulative[rank - 1]
rel_error = np.linalg.norm(image - compressed) / np.linalg.norm(image)
print(f"{rank:5d} {ratio:11.2%} {energy:7.2%} {rel_error:10.4f}")
min_95 = np.searchsorted(cumulative, 0.95) + 1
print(f"\nMin rank for 95% energy: {min_95}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
10. np.pad — 边界填充
图像卷积、拼接等操作常需在边界填充像素:
PYTHON
import numpy as np
img = np.ones((5, 5), dtype=np.uint8) * 128
padded = np.pad(img, pad_width=2, mode='constant', constant_values=0)
print(f"Original: {img.shape}") # (5, 5)
print(f"Padded: {padded.shape}") # (9, 9)
padded_edge = np.pad(img, pad_width=1, mode='edge')
print(f"Edge pad: {padded_edge.shape}") # (7, 7) 边缘复制填充
padded_reflect = np.pad(img, pad_width=2, mode='reflect')
print(f"Edge pad: {padded_edge.shape}")
print(f"Reflect pad: {padded_reflect.shape}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
| mode | 说明 | 效果 |
|---|---|---|
constant |
填充固定值 | 边界外全为指定值 |
edge |
复制边缘值 | 最近像素延伸 |
reflect |
镜像反射 | 以边缘为轴镜像 |
wrap |
环绕 | 周期性重复 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:np.pad 边界填充(难度⭐)
PYTHON
import numpy as np
img = np.arange(9, dtype=np.uint8).reshape(3, 3)
print("Original:")
print(img)
for mode in ['constant', 'edge', 'reflect', 'wrap']:
if mode == 'constant':
# NumPy 2.x:仅 constant 模式支持 constant_values
padded = np.pad(img, pad_width=1, mode=mode, constant_values=0)
else:
padded = np.pad(img, pad_width=1, mode=mode)
print(f"\n{mode}:")
print(padded)
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
11. NumPy vs OpenCV 定位
| 能力 | NumPy | OpenCV |
|---|---|---|
| 图像表示 | ndarray 基础 | 基于 ndarray |
| 裁剪/翻转 | 切片,零拷贝 | 封装函数 |
| 灰度转换 | np.dot 加权 |
cv2.cvtColor 优化 |
| 直方图 | np.histogram |
cv2.calcHist 优化 |
| 阈值 | 布尔索引 | cv2.threshold 支持 Otsu |
| 滤波/卷积 | 需手写或 scipy | cv2.GaussianBlur 等 |
| 特征检测 | 不支持 | SIFT/ORB/Haar |
| 编解码 | 需 PIL/imageio | cv2.imread/imwrite |
| 定位 | 入门/教学/快速原型 | 工业级图像处理 |
NumPy 是理解图像处理原理的最佳工具,OpenCV 是生产环境的最佳工具。
12. 综合示例:图像处理流水线
从生成合成图像开始,依次完成裁剪 → 灰度 → 直方图 → 阈值 → SVD 压缩对比。
PYTHON
import numpy as np
np.random.seed(42)
# ========================================
# Step 1: 生成合成彩色图像
# ========================================
print("=== Step 1: Generate Synthetic Image ===\n")
H, W = 200, 300
img = np.zeros((H, W, 3), dtype=np.uint8)
x = np.linspace(0, 4 * np.pi, W)
y = np.linspace(0, 4 * np.pi, H)
X, Y = np.meshgrid(x, y)
img[:, :, 0] = ((np.sin(X) + 1) / 2 * 200).astype(np.uint8)
img[:, :, 1] = ((np.cos(Y) + 1) / 2 * 180).astype(np.uint8)
img[:, :, 2] = ((np.sin(X + Y) + 1) / 2 * 160).astype(np.uint8)
img[60:140, 80:220] = [255, 200, 50]
print(f"Image shape: {img.shape}")
print(f"Image dtype: {img.dtype}")
print(f"Pixel range: [{img.min()}, {img.max()}]")
# ========================================
# Step 2: 裁剪中心区域
# ========================================
print("\n=== Step 2: Crop Center Region ===\n")
cropped = img[50:150, 75:225].copy()
print(f"Cropped shape: {cropped.shape}")
# ========================================
# Step 3: 灰度转换
# ========================================
print("\n=== Step 3: Grayscale Conversion ===\n")
gray = np.dot(img.astype(np.float64), [0.299, 0.587, 0.114])
gray = np.clip(gray, 0, 255).astype(np.uint8)
print(f"Gray shape: {gray.shape}")
print(f"Gray mean: {gray.mean():.1f}")
print(f"Gray std: {gray.std():.1f}")
# ========================================
# Step 4: 直方图分析
# ========================================
print("\n=== Step 4: Histogram Analysis ===\n")
hist, bins = np.histogram(gray, bins=256, range=(0, 256))
peak_bin = np.argmax(hist)
print(f"Histogram peak at bin: {peak_bin}")
print(f"Dark pixels (<64): {np.sum(hist[:64])}")
print(f"Mid pixels (64-192): {np.sum(hist[64:192])}")
print(f"Bright pixels (>192):{np.sum(hist[192:])}")
# ========================================
# Step 5: 阈值处理
# ========================================
print("\n=== Step 5: Thresholding ===\n")
mean_t = gray.mean()
binary_mean = (gray > mean_t).astype(np.uint8) * 255
fg = np.sum(binary_mean == 255)
print(f"Mean threshold: {mean_t:.1f}")
print(f"Foreground pixels: {fg} / {gray.size} ({fg/gray.size:.1%})")
for t in [80, 128, 180]:
b = (gray > t).astype(np.uint8) * 255
fg_t = np.sum(b == 255)
print(f" T={t}: foreground {fg_t/gray.size:.1%}")
# ========================================
# Step 6: SVD 压缩对比
# ========================================
print("\n=== Step 6: SVD Compression ===\n")
U, s, Vt = np.linalg.svd(gray.astype(np.float64), full_matrices=False)
total_energy = np.sum(s ** 2)
cumulative = np.cumsum(s ** 2) / total_energy
print(f"Top 5 singular values: {np.round(s[:5], 2)}")
print(f"\n{'Rank':>5} {'Compression':>12} {'Energy':>8} {'Rel Error':>10}")
print("-" * 40)
for rank in [5, 10, 20, 50, 100]:
U_r = U[:, :rank]
s_r = s[:rank]
Vt_r = Vt[:rank, :]
compressed = U_r @ np.diag(s_r) @ Vt_r
compressed = np.clip(compressed, 0, 255)
original_size = H * W
compressed_size = H * rank + rank + rank * W
ratio = compressed_size / original_size
energy = cumulative[rank - 1]
rel_error = (np.linalg.norm(gray.astype(np.float64) - compressed) /
np.linalg.norm(gray.astype(np.float64)))
print(f"{rank:5d} {ratio:11.2%} {energy:7.2%} {rel_error:10.4f}")
min_95 = np.searchsorted(cumulative, 0.95) + 1
min_99 = np.searchsorted(cumulative, 0.99) + 1
print(f"\nMin rank for 95% energy: {min_95}")
print(f"Min rank for 99% energy: {min_99}")
# ========================================
# Step 7: 翻转/旋转演示
# ========================================
print("\n=== Step 7: Flip & Rotate ===\n")
flip_h = img[:, ::-1]
flip_v = img[::-1, :]
rot90 = np.rot90(img)
rot180 = np.rot90(img, k=2)
print(f"Original shape: {img.shape}")
print(f"Flip H shape: {flip_h.shape}")
print(f"Flip V shape: {flip_v.shape}")
print(f"Rot90 shape: {rot90.shape}")
print(f"Rot180 shape: {rot180.shape}")
print(f"Rot90 == Original transposed: "
f"{np.array_equal(rot90, img.transpose(1, 0, 2)[::-1])}")
# ========================================
# Step 8: np.pad 边界填充
# ========================================
print("\n=== Step 8: Padding ===\n")
padded = np.pad(gray, pad_width=10, mode='constant', constant_values=0)
print(f"Original gray: {gray.shape}")
print(f"Padded gray: {padded.shape}")
print(f"Padded border all zero: "
f"{np.all(padded[:10] == 0) and np.all(padded[-10:] == 0)}")
TEXT
📖 仅展示
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
❓ 常见问题
Q NumPy 能替代 OpenCV 吗?
A 不能完全替代。NumPy 适合理解图像处理原理和快速原型,覆盖裁剪/翻转/灰度/直方图/阈值/SVD 压缩等基础操作。OpenCV 提供滤波/特征检测/编解码等工业级功能,生产环境必须用 OpenCV。NumPy 是入门工具,OpenCV 是高级工具。
Q 图像 shape 怎么理解?
A 彩色图 shape=(H, W, C):H=行数(高度从上到下),W=列数(宽度从左到右),C=通道数(RGB=3)。灰度图 shape=(H, W),没有通道维。注意 H 在前 W 在后,与传统"宽×高"写法相反。
Q 灰度转换权重 0.299/0.587/0.114 的来源?
A 来自 ITU-R BT.601 标准,基于人眼对不同波长光的亮度感知实验。人眼对绿色(555nm 附近)最敏感,所以绿色权重最大(0.587);对蓝色最不敏感,权重最小(0.114)。简单平均(1/3, 1/3, 1/3)不符合人眼感知。
Q SVD 压缩原理是什么?
A SVD 将矩阵分解为 U·Σ·Vᵀ,奇异值按从大到小排列。前几个奇异值包含大部分信息,后面的贡献越来越小。只保留前 r 个奇异值重建图像,存储量从 H×W 降为 r×(H+W+1),实现有损压缩。r 越小压缩率越高但失真越大。
Q np.pad 的用途是什么?
A np.pad 在数组边界填充值,常用于图像卷积(需要边界外的"虚拟"像素)、拼接图像时的对齐、以及防止切片越界。支持 constant/edge/reflect/wrap 四种填充模式。
Q uint8 图像运算溢出怎么办?
A uint8 范围 0~255,运算溢出会回绕(如 200+100=44 而非 300)。正确做法:先转为 float64 运算,再用
np.clip(0, 255) 截断,最后转回 uint8。例如 result = np.clip(img.astype(np.float64) + 50, 0, 255).astype(np.uint8)。📖 小节
- 图像即 ndarray:灰度=(H,W),彩色=(H,W,3),dtype=uint8
- 裁剪=切片
img[y1:y2, x1:x2],翻转=切片[::-1],旋转=np.rot90 - 灰度转换:加权平均 0.299R + 0.587G + 0.114B(ITU-R BT.601)
- 直方图用
np.histogram(gray, bins=256, range=(0, 256))统计亮度分布 - 阈值处理:
(gray > T).astype(np.uint8) * 255,均值阈值通用简单 - SVD 压缩:保留前 r 个奇异值,存储量 = r×(H+W+1),r 越小压缩率越高
- np.pad 填充边界:constant/edge/reflect/wrap 四种模式
- NumPy 定位=入门/教学/原型,OpenCV 定位=工业级图像处理
- uint8 运算注意溢出:先转 float64 → clip → 转回 uint8
📝 作业
(1) 创建渐变图像
- 创建 100×100×3 的 uint8 彩色图像
- R 通道从左到右渐变(0→255),G 通道从上到下渐变(0→255),B 通道固定 128
- 转为灰度图,打印灰度图的均值和标准差
- 用
np.histogram统计灰度直方图,打印亮度最高的前 5 个 bin
(2) 翻转旋转
- 创建 4×4×3 的彩色图像(元素随意填充)
- 分别进行水平翻转、垂直翻转、逆时针旋转 90°、旋转 180°
- 验证:连续两次水平翻转是否恢复原图?用
np.array_equal验证 - 验证:旋转 4 次 90° 是否恢复原图?
(3) SVD 压缩对比质量
- 用
np.meshgrid生成 100×100 合成灰度图像(自选公式,至少两个频率分量叠加) - 对灰度图做 SVD,分别用 rank=5、10、20、50 重建
- 对每个 rank 计算:压缩率、能量保留比例、相对误差
||orig-recon|| / ||orig|| - 找出能量保留 ≥ 95% 的最小 rank
- 打印格式化的对比表格