NumPy: 图像处理

1. 你将学到


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) 图像处理流程

100%
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 个奇异值即可压缩:

当 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)

📖 小节


📝 作业

(1) 创建渐变图像

  1. 创建 100×100×3 的 uint8 彩色图像
  2. R 通道从左到右渐变(0→255),G 通道从上到下渐变(0→255),B 通道固定 128
  3. 转为灰度图,打印灰度图的均值和标准差
  4. np.histogram 统计灰度直方图,打印亮度最高的前 5 个 bin

(2) 翻转旋转

  1. 创建 4×4×3 的彩色图像(元素随意填充)
  2. 分别进行水平翻转、垂直翻转、逆时针旋转 90°、旋转 180°
  3. 验证:连续两次水平翻转是否恢复原图?用 np.array_equal 验证
  4. 验证:旋转 4 次 90° 是否恢复原图?

(3) SVD 压缩对比质量

  1. np.meshgrid 生成 100×100 合成灰度图像(自选公式,至少两个频率分量叠加)
  2. 对灰度图做 SVD,分别用 rank=5、10、20、50 重建
  3. 对每个 rank 计算:压缩率、能量保留比例、相对误差 ||orig-recon|| / ||orig||
  4. 找出能量保留 ≥ 95% 的最小 rank
  5. 打印格式化的对比表格
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏