图像处理基础 第1课/共35课

第01课:数字图像基础

📖 课程概述

本课学习数字图像基础的核心原理与实现。我们将从数学基础出发,通过代码实践真正理解每一个算法的运作机制。

📑 本课目录

1. 数字图像的本质 2. 像素与分辨率 3. 图像的数值表示 4. 色彩模型 5. OpenCV读写与显示 6. NumPy操作像素 7. 直方图与统计

1. 数字图像的本质

现实世界的光是连续的,但计算机只能处理离散数据。数字图像就是对连续光信号进行采样(Sampling)量化(Quantization)的结果。

🔑 从光到像素

f(x,y) = Q[ S[ I(x,y) ] ]
其中 I(x,y) 是连续光强,S 是采样算子,Q 是量化算子

采样率过低会导致混叠(Aliasing)——高频信号被误认为低频信号。奈奎斯特采样定理告诉我们:

fs ≥ 2 · fmax (奈奎斯特采样定理)
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

# 演示采样与量化对图像的影响
img = np.zeros((256, 256), dtype=np.uint8)
for i in range(256):
    img[i, :] = i  # 垂直渐变 0→255

# 模拟低采样率
def downsample_upsample(img, factor):
    small = img[::factor, ::factor]
    return cv2.resize(small, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_NEAREST)

sample_2 = downsample_upsample(img, 2)
sample_4 = downsample_upsample(img, 4)
sample_8 = downsample_upsample(img, 8)

# 量化:减少位深度
def quantize(img, levels):
    step = 256 // levels
    return (img // step) * step

quant_16 = quantize(img, 16)   # 4-bit
quant_4  = quantize(img, 4)    # 2-bit
quant_2  = quantize(img, 2)    # 1-bit

fig, axes = plt.subplots(2, 3, figsize=(12, 8))
axes[0,0].imshow(sample_2, cmap='gray'); axes[0,0].set_title('Sampling x2')
axes[0,1].imshow(sample_4, cmap='gray'); axes[0,1].set_title('Sampling x4')
axes[0,2].imshow(sample_8, cmap='gray'); axes[0,2].set_title('Sampling x8')
axes[1,0].imshow(quant_16, cmap='gray'); axes[1,0].set_title('Quant 16 levels (4bit)')
axes[1,1].imshow(quant_4, cmap='gray'); axes[1,1].set_title('Quant 4 levels (2bit)')
axes[1,2].imshow(quant_2, cmap='gray'); axes[1,2].set_title('Quant 2 levels (1bit)')
for ax in axes.flat: ax.axis('off')
plt.tight_layout()
plt.savefig('/var/www/ttl/cv/l01_sampling.png', dpi=100)
plt.close()
print(f"Original: {img.shape}, dtype: {img.dtype}")
print(f"Sampling x8 MSE: {np.mean((img.astype(float)-sample_8.astype(float))**2):.2f}")
print(f"Quant 2-level MSE: {np.mean((img.astype(float)-quant_2.astype(float))**2):.2f}")
print("Sampling and quantization demo complete")

2. 像素与分辨率

像素是数字图像的最小单位。每个像素有一个坐标(x,y)和一个或多个数值。

📐 分辨率概念

图像数据量 = 宽 × 高 × 位深度 × 通道数 / 8 (字节)
例: 1920×1080×8bit×3通道 = 6,220,800 字节 ≈ 5.93 MB
import cv2
import numpy as np

# 分辨率与像素操作实践
img_gray = np.zeros((256, 256), dtype=np.uint8)
img_gray[0:64, 0:64] = 50      # 左上 - 深灰
img_gray[0:64, 64:128] = 100   # 中上 - 中灰
img_gray[0:64, 128:192] = 150  # 右上 - 浅灰
img_gray[0:64, 192:256] = 200  # 最右 - 更浅
cv2.circle(img_gray, (128, 128), 50, 255, -1)

print(f"左上角像素值: {img_gray[0, 0]}")    # 50
print(f"圆心像素值: {img_gray[128, 128]}")   # 255
print(f"图像shape: {img_gray.shape}")         # (256, 256)
print(f"数据类型: {img_gray.dtype}")           # uint8
print(f"总像素数: {img_gray.size}")            # 65536
print(f"最小值: {img_gray.min()}, 最大值: {img_gray.max()}")
print(f"均值: {img_gray.mean():.2f}, 标准差: {img_gray.std():.2f}")
cv2.imwrite('/var/www/ttl/cv/l01_resolution.png', img_gray)

3. 图像的数值表示

在NumPy中,灰度图像是2D数组(shape=(H,W)),彩色图像是3D数组(shape=(H,W,C))。

灰度图: I ∈ ℝH×W, 值域 [0, 255] (uint8)
彩色图: I ∈ ℝH×W×3, 3个通道表示R,G,B
import numpy as np
import cv2

# 理解图像的NumPy表示
img_color = np.zeros((200, 300, 3), dtype=np.uint8)
img_color[0:100, 0:100] = [255, 0, 0]   # BGR红色
img_color[0:100, 100:200] = [0, 255, 0]  # BGR绿色
img_color[0:100, 200:300] = [0, 0, 255]  # BGR蓝色
img_color[100:200, 0:100] = [255, 255, 255]  # 白色
img_color[100:200, 100:200] = [0, 255, 255]  # 黄色(BGR: G+R)
img_color[100:200, 200:300] = [255, 0, 255]  # 紫色(BGR: B+R)

print(f"彩色图像shape: {img_color.shape}")
print(f"红色区域像素[50,50]: {img_color[50, 50]}")
print(f"白色区域像素[150,50]: {img_color[150, 50]}")

# 数据类型的影响
img_float = img_color.astype(np.float32) / 255.0
print(f"\nfloat32范围: [{img_float.min():.3f}, {img_float.max():.3f}]")

# NumPy加法 vs cv2.add(溢出问题)
a = np.array([200], dtype=np.uint8)
print(f"\nNumPy加法 200+100 = {(a+100).item()}")  # 溢出! 44
print(f"cv2.add 200+100 = {cv2.add(a, np.array([100], dtype=np.uint8)).item()}")  # 饱和! 255

cv2.imwrite('/var/www/ttl/cv/l01_color_channels.png', img_color)

4. 色彩模型

不同的色彩模型适用于不同场景:

模型分量适用场景
RGBR(红) G(绿) B(蓝)显示器、相机原始数据
HSVH(色调) S(饱和度) V(明度)颜色分割、色相筛选
LABL(亮度) A(红绿轴) B(蓝黄轴)颜色差异感知、白平衡
YCrCbY(亮度) Cr Cb(色度)视频压缩、肤色检测
灰度单通道边缘检测、特征提取
灰度转换公式: Y = 0.299·R + 0.587·G + 0.114·B
(人眼对绿色最敏感,权重最大)
import cv2
import numpy as np

# 色彩空间转换实践
hsv_img = np.zeros((180, 360, 3), dtype=np.uint8)
for h in range(180):
    for s in range(0, 360, 2):
        hsv_img[h, s:s+2] = [h, 255, 255]
bgr_from_hsv = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR)

# 测试RGB图像转换
test_rgb = np.zeros((200, 200, 3), dtype=np.uint8)
test_rgb[:] = [50, 100, 200]  # BGR: B=50, G=100, R=200

hsv = cv2.cvtColor(test_rgb, cv2.COLOR_BGR2HSV)
lab = cv2.cvtColor(test_rgb, cv2.COLOR_BGR2LAB)
ycrcb = cv2.cvtColor(test_rgb, cv2.COLOR_BGR2YCrCb)
gray = cv2.cvtColor(test_rgb, cv2.COLOR_BGR2GRAY)

print(f"原始BGR [50,100,200]:")
print(f"  HSV: H={hsv[0,0,0]}, S={hsv[0,0,1]}, V={hsv[0,0,2]}")
print(f"  LAB: L={lab[0,0,0]}, A={lab[0,0,1]}, B={lab[0,0,2]}")
print(f"  YCrCb: Y={ycrcb[0,0,0]}, Cr={ycrcb[0,0,1]}, Cb={ycrcb[0,0,2]}")
print(f"  灰度: {gray[0,0]}")

# 手动验证灰度转换: Y = 0.299*R + 0.587*G + 0.114*B
# BGR[50,100,200] => B:50, G:100, R:200
manual_gray = 0.299 * 200 + 0.587 * 100 + 0.114 * 50
print(f"\n手动灰度计算: 0.299×200 + 0.587×100 + 0.114×50 = {manual_gray:.1f}")
print(f"OpenCV灰度值: {gray[0,0]}")

cv2.imwrite('/var/www/ttl/cv/l01_hsv_wheel.png', bgr_from_hsv)

5. OpenCV读写与显示

OpenCV(cv2)是计算机视觉最常用的库,图像读写是第一步:

import cv2
import numpy as np

# OpenCV图像读写全流程
img = np.zeros((300, 400, 3), dtype=np.uint8)
cv2.putText(img, 'OpenCV', (80, 160), cv2.FONT_HERSHEY_SIMPLEX, 2, (244, 114, 182), 3)
cv2.rectangle(img, (50, 80), (350, 200), (192, 132, 252), 2)

# 保存图像
cv2.imwrite('/var/www/ttl/cv/l01_opencv_demo.png', img)
print(f"保存图像: {img.shape}")

# 读取图像
loaded = cv2.imread('/var/www/ttl/cv/l01_opencv_demo.png')
if loaded is not None:
    print(f"读取成功: shape={loaded.shape}, dtype={loaded.dtype}")
    print(f"像素一致性: {np.array_equal(img, loaded)}")

# 读取为灰度图
gray = cv2.imread('/var/www/ttl/cv/l01_opencv_demo.png', cv2.IMREAD_GRAYSCALE)
print(f"灰度读取: shape={gray.shape}")

# 图像格式对比
formats = {'PNG': '/var/www/ttl/cv/l01_fmt.png',
           'JPG95': '/var/www/ttl/cv/l01_fmt_q95.jpg',
           'JPG50': '/var/www/ttl/cv/l01_fmt_q50.jpg'}
cv2.imwrite(formats['PNG'], img)
cv2.imwrite(formats['JPG95'], img, [cv2.IMWRITE_JPEG_QUALITY, 95])
cv2.imwrite(formats['JPG50'], img, [cv2.IMWRITE_JPEG_QUALITY, 50])

import os
for name, path in formats.items():
    size = os.path.getsize(path)
    reloaded = cv2.imread(path)
    diff = np.mean(np.abs(img.astype(float) - reloaded.astype(float)))
    print(f"{name}: 文件={size}字节, 像素MSE={diff:.2f}")

6. NumPy操作像素

图像在NumPy中就是数组,所有NumPy操作都适用:

import cv2
import numpy as np

# NumPy像素操作大全
img = np.zeros((256, 256, 3), dtype=np.uint8)
img[50:100, 50:100] = [0, 255, 0]
img[150:200, 150:200] = [0, 0, 255]
roi = img[50:100, 50:100].copy()
img[50:100, 150:200] = roi

# NumPy加法 vs cv2.add
a = np.array([200], dtype=np.uint8)
print(f"NumPy加法 200+100 = {(a+100).item()}")  # 溢出: 44
print(f"cv2.add 200+100 = {cv2.add(a, np.array([100], dtype=np.uint8)).item()}")  # 饱和: 255

# 图像混合
img1 = np.full((100,100,3), 200, dtype=np.uint8)
img2 = np.full((100,100,3), 100, dtype=np.uint8)
blend = cv2.addWeighted(img1, 0.7, img2, 0.3, 0)
print(f"混合结果: {blend[0,0]}")  # [170, 170, 170]

# 位运算 - 圆形mask
mask = np.zeros((256, 256), dtype=np.uint8)
cv2.circle(mask, (128, 128), 80, 255, -1)
masked = cv2.bitwise_and(img, img, mask=mask)

# 通道操作
b, g, r = cv2.split(img)
print(f"分离: B={b.shape}, G={g.shape}, R={r.shape}")
merged = cv2.merge([b, g, r])
print(f"合并一致: {np.array_equal(img, merged)}")

cv2.imwrite('/var/www/ttl/cv/l01_numpy_ops.png', masked)

7. 直方图与统计

直方图显示每个灰度级的像素数量:

H(k) = ∑x,y δ(I(x,y) = k), k ∈ [0, 255]
归一化: p(k) = H(k) / (W × H)
import cv2
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

# 创建测试图像
img = np.zeros((256, 256), dtype=np.uint8)
for i in range(256):
    img[i, :] = i
cv2.circle(img, (128, 128), 40, 220, -1)
noise = np.random.normal(0, 15, img.shape).astype(np.int16)
img_noisy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)

# 计算直方图
hist = cv2.calcHist([img_noisy], [0], None, [256], [0, 256])

# 直方图均衡化
equalized = cv2.equalizeHist(img_noisy)
hist_eq = cv2.calcHist([equalized], [0], None, [256], [0, 256])

# CLAHE
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_img = clahe.apply(img_noisy)

print("原始图像统计:")
print(f"  均值: {img_noisy.mean():.2f}, 标准差: {img_noisy.std():.2f}")
print(f"  中位数: {np.median(img_noisy):.1f}")
print("\n均衡化后统计:")
print(f"  均值: {equalized.mean():.2f}, 标准差: {equalized.std():.2f}")

# 绘制直方图对比
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].plot(hist); axes[0].set_title('Original Histogram')
axes[0].set_xlabel('Pixel Value'); axes[0].set_ylabel('Count')
axes[1].plot(hist_eq); axes[1].set_title('Equalized Histogram')
axes[1].set_xlabel('Pixel Value')
axes[2].imshow(np.hstack([img_noisy, equalized, clahe_img]), cmap='gray')
axes[2].set_title('Original | Equalized | CLAHE')
axes[2].axis('off')
plt.tight_layout()
plt.savefig('/var/www/ttl/cv/l01_histogram.png', dpi=100)
plt.close()
print("\n直方图分析完成")

✅ 实机验证

以上代码已在服务器实机运行,验证结果:

🏆

图像基础达人

你已经掌握了数字图像基础的核心知识,继续前进!