本课学习数字图像基础的核心原理与实现。我们将从数学基础出发,通过代码实践真正理解每一个算法的运作机制。
现实世界的光是连续的,但计算机只能处理离散数据。数字图像就是对连续光信号进行采样(Sampling)和量化(Quantization)的结果。
采样率过低会导致混叠(Aliasing)——高频信号被误认为低频信号。奈奎斯特采样定理告诉我们:
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")
像素是数字图像的最小单位。每个像素有一个坐标(x,y)和一个或多个数值。
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)
在NumPy中,灰度图像是2D数组(shape=(H,W)),彩色图像是3D数组(shape=(H,W,C))。
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)
不同的色彩模型适用于不同场景:
| 模型 | 分量 | 适用场景 |
|---|---|---|
| RGB | R(红) G(绿) B(蓝) | 显示器、相机原始数据 |
| HSV | H(色调) S(饱和度) V(明度) | 颜色分割、色相筛选 |
| LAB | L(亮度) A(红绿轴) B(蓝黄轴) | 颜色差异感知、白平衡 |
| YCrCb | Y(亮度) Cr Cb(色度) | 视频压缩、肤色检测 |
| 灰度 | 单通道 | 边缘检测、特征提取 |
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)
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}")
图像在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)
直方图显示每个灰度级的像素数量:
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直方图分析完成")
以上代码已在服务器实机运行,验证结果:
你已经掌握了数字图像基础的核心知识,继续前进!