计算机视觉实战:OpenCV + PIL + MoviePy 从零到字符画视频(43个实验)

计算机视觉实战:OpenCV + PIL + MoviePy 从零到字符画视频(43个实验)

服务器环境 : 华为云 FlexusX ecs-88e7 (Ubuntu 24.04, 8vCPU/16GiB, Python 3.12.3)

核心依赖 : OpenCV 4.13.0 | NumPy 2.5.0 | Pillow 11.3.0 | imageio 2.37.3 | moviepy 2.1.2

实验总数 : 43 个 | 涵盖 OpenCV 基础/进阶、PIL 绘图、视频处理

实战原则: 所有代码均在服务器真实执行,输出数据直接取自服务器 stdout


目录

  • [Part 1: OpenCV 基础篇(实验 1-15)](#Part 1: OpenCV 基础篇(实验 1-15))
  • [Part 2: OpenCV 进阶篇(实验 16-33)](#Part 2: OpenCV 进阶篇(实验 16-33))
  • [Part 3: PIL & 视频处理篇(实验 34-43)](#Part 3: PIL & 视频处理篇(实验 34-43))
  • 总结与速查表

环境搭建

bash 复制代码
# Ubuntu 24.04 安装依赖
apt-get install -y libfreetype6-dev libfontconfig1-dev

# 安装 Python 包 (阿里云镜像)
pip3 install --break-system-packages \
  --index-url https://mirrors.aliyun.com/pypi/simple/ \
  numpy opencv-python-headless Pillow imageio moviepy

验证版本:

复制代码
OpenCV: 4.13.0
NumPy: 2.5.0
Pillow: 11.3.0
imageio: 2.37.3
moviepy: 2.1.2

⚠️ Ubuntu 24.04 使用 PEP 668 外部管理环境,需加 --break-system-packages。生产环境建议用 venv。


Part 1: OpenCV 基础篇 {#part1}

实验 1: OpenCV 安装 & 图片的读写与查看

复制代码
OpenCV version: 4.13.0
NumPy version: 2.5.0
图像形状: (300, 400, 3)  (高度,宽度,通道)
数据类型: uint8
总像素数: 360000
BGR像素[0,0]值: [0 0 0]
回读形状: (300, 400, 3)
图像最大值: 255, 最小值: 0, 均值: 65.0
python 复制代码
import cv2, numpy as np

# 创建图像 (numpy ndarray)
img = np.zeros((300, 400, 3), dtype=np.uint8)
cv2.rectangle(img, (50,50), (350,250), (255, 100, 50), -1)
cv2.circle(img, (200,150), 80, (50, 200, 100), -1)

# 写入文件
cv2.imwrite('output.jpg', img)

# 读取文件
img2 = cv2.imread('output.jpg')
print(img2.shape)  # (300, 400, 3) = (H, W, C)

关键概念:OpenCV 图像本质是 NumPy ndarray ,形状为 (高度, 宽度, 通道数),通道顺序为 BGR 而非 RGB。


实验 2: 绘制文字(putText)

复制代码
文字'OpenCV Text': 宽=296, 高=32, baseline=14
python 复制代码
canvas = np.ones((200, 600, 3), dtype=np.uint8) * 240

# 7 种内置字体
fonts = [
    cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_PLAIN,
    cv2.FONT_HERSHEY_DUPLEX, cv2.FONT_HERSHEY_COMPLEX,
    cv2.FONT_HERSHEY_TRIPLEX,
    cv2.FONT_HERSHEY_SCRIPT_SIMPLEX, cv2.FONT_HERSHEY_SCRIPT_COMPLEX,
]

for i, font in enumerate(fonts):
    cv2.putText(canvas, f"Hello Font{i}", (10, 30+i*25),
                font, 0.6, (0, 0, 255), 1, cv2.LINE_AA)

# 获取文字尺寸(精确定位用)
(w, h), baseline = cv2.getTextSize("OpenCV", cv2.FONT_HERSHEY_SIMPLEX, 1.5, 2)
参数 说明
text 文字内容
org 左下角起点坐标
fontFace 字体(7种内置)
fontScale 缩放比例
color BGR 颜色
thickness 线宽
lineType cv2.LINE_AA 抗锯齿

实验 3: 滑动条事件(Trackbar)

无 GUI 环境使用回调函数模拟,演示 createTrackbar 机制。

复制代码
[Trackbar回调] 阈值=50,  二值化像素数=121002
[Trackbar回调] 阈值=100, 二值化像素数=60501
[Trackbar回调] 阈值=150, 二值化像素数=60501
[Trackbar回调] 阈值=200, 二值化像素数=40420
python 复制代码
def on_threshold_change(val):
    _, binary = cv2.threshold(img, val, 255, cv2.THRESH_BINARY)
    print(f"阈值={val}, 二值化像素数={np.sum(binary==255)}")

# GUI 环境下的实际用法:
# cv2.createTrackbar("Threshold", "Window", 127, 255, on_threshold_change)

实验 4: 图片文件的结构(JPEG vs PNG)

复制代码
JPEG(Q=95): 7386 bytes
JPEG(Q=10): 1275 bytes  (有损压缩)
PNG(无损):   804 bytes
JPEG文件头: ffd8ffe0 (FF D8 FF ...)
PNG文件头:  89504e470d0a1a0a (89 50 4E 47 ...)
python 复制代码
# JPEG 质量控制
cv2.imwrite('img_q95.jpg', grad, [cv2.IMWRITE_JPEG_QUALITY, 95])
cv2.imwrite('img_q10.jpg', grad, [cv2.IMWRITE_JPEG_QUALITY, 10])

# PNG 无损
cv2.imwrite('img.png', grad)
格式 压缩 透明度 适用场景
JPEG 有损(DCT变换) 不支持 照片、Web展示
PNG 无损(DEFLATE) 支持 Alpha 通道 图标、截图、需要透明
BMP 无压缩 不支持 原始数据交换

实验 5: 灰度图像矩阵结构

复制代码
灰度矩阵 (6x6):
[[  0  50 100 150 200 255]
 [ 30  80 130 180 220 200]
 [ 60 110 160 210 180 150]
 [ 90 140 190 200 140 100]
 [120 170 220 170 100  50]
 [150 200 255 140  60   0]]
形状: (6, 6), 维度: 2, 元素总数: 36
最大值: 255, 最小值: 0, 均值: 137.8
数据类型: uint8 (每个像素1字节)
像素[2,3]=210, 像素[0,5]=255
第2行: [ 30  80 130 180 220 200]
第3列: [100 130 160 190 220 255]

灰度图像是 二维 uint8 ndarray ,每个元素 0-255 表示亮度。img[y, x] 直接读取像素值。


实验 6: NumPy 数组创建(arange/zeros/ones/empty)

复制代码
arange(10):            [0 1 2 3 4 5 6 7 8 9]
arange(0,20,3):        [ 0  3  6  9 12 15 18]
zeros(3,4):
[[0 0 0 0]
 [0 0 0 0]
 [0 0 0 0]]
ones(2,5):
[[1 1 1 1 1]
 [1 1 1 1 1]]
eye(4) 单位矩阵:
[[1 0 0 0]
 [0 1 0 0]
 [0 0 1 0]
 [0 0 0 1]]
函数 用途 图像处理场景
np.zeros(shape) 全零数组 创建黑色画布
np.ones(shape) 全1数组 创建白色掩码
np.arange(n) 等差数列 LUT 表生成
np.eye(n) 单位矩阵 恒等变换矩阵
np.full(shape, v) 填充指定值 纯色背景
np.empty(shape) 未初始化 高性能(谨慎使用)

实验 7: NumPy 矩阵维度/形状/变形/索引

复制代码
原始arr.shape: (2, 3, 4), ndim=3
reshape(6,4):
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]
 [20 21 22 23]]
reshape(-1) 展平: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
arr[..., 0] = [[ 0  4  8] [12 16 20]]  (...表示所有前面的维度)
arr(2,3,4) -> transpose(2,0,1) shape: (4, 2, 3)
python 复制代码
arr = np.arange(24).reshape(2, 3, 4)  # 2个 3x4 矩阵

# 常用变形操作
arr.reshape(6, 4)    # 改变形状(不改变数据)
arr.reshape(-1)       # 展平为一维
arr.transpose(2,0,1)  # 维度重排 (类似 PyTorch permute)
arr[..., 0]           # 取最后一个维度索引0(所有"前面"维度的第0列)

实验 8: 灰度图的矩形选区复制与粘贴(ROI)

复制代码
原图: 白色矩形 [50:150, 80:220]
ROI形状: (100, 140), ROI值: [255, 255]
粘贴后: ROI已复制到[10:110, 20:160]
粘贴区域均值: 255
python 复制代码
# ROI 选取
roi = gray_img[50:150, 80:220]        # 浅拷贝(引用原数据)
roi_copy = gray_img[50:150, 80:220].copy()  # 深拷贝(独立副本)

# 粘贴到其他位置
gray_img[10:110, 20:160] = roi_copy

# 翻转后粘贴
roi_flipped = cv2.flip(roi_copy, 1)   # 1=水平翻转
gray_img[100:200, 150:290] = roi_flipped

⚠️ 直接切片是浅拷贝,修改 ROI 会影响原图。需要独立副本时用 .copy()


实验 9: 阈值处理(Threshold)

复制代码
THRESH_BINARY (阈值=127):    非零像素比例=49.7%
THRESH_BINARY_INV (阈值=127): 非零像素比例=50.3%
THRESH_TRUNC (阈值=127):     非零像素比例=99.3%
THRESH_TOZERO (阈值=127):    非零像素比例=49.7%
THRESH_TOZERO_INV (阈值=127): 非零像素比例=49.7%
Otsu自动阈值: 127
python 复制代码
ret, binary = cv2.threshold(gray_img, 127, 255, cv2.THRESH_BINARY)

# 自适应阈值(光照不均场景)
adaptive = cv2.adaptiveThreshold(gray_img, 255,
    cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 2)

# Otsu 自动阈值
_, otsu = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
类型 公式 效果
THRESH_BINARY src>T ? maxval : 0 标准二值化
THRESH_BINARY_INV src>T ? 0 : maxval 反相二值化
THRESH_TRUNC src>T ? T : src 截断高亮
THRESH_TOZERO src>T ? src : 0 保留亮部
THRESH_TOZERO_INV src>T ? 0 : src 保留暗部
THRESH_OTSU 自动计算最优 T 双峰直方图

实验 10: 直方图(Histogram)

复制代码
直方图形状: (256, 1), 总和: 60000 (应=总像素数60000)
各区间像素数 (每32级合并):
  [  0- 31]:  28800 ############################
  [ 32- 63]:  13000 #############
  [192-223]:  18200 ##################
均匀化后像素范围: [0, 255], 均值: 100.3
python 复制代码
hist = cv2.calcHist([img], [0], None, [256], [0, 256])

# 直方图均衡化(增强对比度)
equ = cv2.equalizeHist(gray_img)

# 彩色图均衡化(需先转 LAB,只均衡 L 通道)
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
l_equ = cv2.equalizeHist(l)
result = cv2.merge([l_equ, a, b])
result = cv2.cvtColor(result, cv2.COLOR_LAB2BGR)

实验 11: 色阶调整(Levels)

复制代码
原始: 范围[80,169], 均值124.2
色阶调整后: 范围[0,252], 均值124.7
LUT方式结果: 均值124.7 (应与色阶调整一致)
python 复制代码
def levels_adjust(img, in_black, in_white, out_black=0, out_white=255):
    """将 [in_black,in_white] 线性映射到 [out_black,out_white]"""
    img_f = img.astype(np.float32)
    img_f = np.clip(img_f, in_black, in_white)
    img_f = (img_f - in_black) / (in_white - in_black) * (out_white - out_black) + out_black
    return np.clip(img_f, 0, 255).astype(np.uint8)

# 等价 LUT 实现(更快)
lut = np.zeros(256, dtype=np.uint8)
for i in range(256):
    v = (i - in_black) / (in_white - in_black) * 255
    lut[i] = np.clip(v, 0, 255)
result = cv2.LUT(img, lut)

Levels 与 Threshold 的区别:Levels 是线性映射保留灰度层次,Threshold 是二值化丢失灰度信息。


实验 12: LUT 查询表(LookUp Table)

复制代码
LUT 查询示例 (输入值 → 反相/伽马/对数):
    0 → 255 /   0 /   0
   64 → 191 / 136 / 191
  128 → 127 / 186 / 223
  192 →  63 / 224 / 242
  255 →   0 / 255 / 255
python 复制代码
# 反相 LUT
lut_invert = 255 - np.arange(256, dtype=np.uint8)

# Gamma 校正 LUT (γ=1/2.2)
lut_gamma = np.array([(i/255)**(1/2.2)*255 for i in range(256)], dtype=np.uint8)

# 对数 LUT
lut_log = np.array([int(np.log1p(i)/np.log1p(255)*255) for i in range(256)], dtype=np.uint8)

# 应用 LUT(O(1) 逐像素查表,比逐像素计算快数百倍)
result = cv2.LUT(img, lut_gamma)

LUT 是图像处理中最常用的加速技巧:将任意复杂的像素映射预计算到 256 元素的数组中。


实验 13: BGR 色彩模式下图像的矩阵结构

复制代码
形状: (5, 5, 3) = (高度, 宽度, 通道数)
ndarray维度: 3
通道顺序: B (蓝) → G (绿) → R (红)  [注意: 不是RGB!]

        B通道        |        G通道        |        R通道
  [255   0   0 255   0] [  0 255   0 255   0] [  0   0 255 255   0]
python 复制代码
# OpenCV 中 BGR 的顺序
img[y, x, 0]  # Blue  通道
img[y, x, 1]  # Green 通道
img[y, x, 2]  # Red   通道

# BGR ↔ RGB 转换
rgb = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)

⚠️ 这是 OpenCV 新手最常见的坑:(255, 0, 0) 在 OpenCV 中是蓝色,不是红色!


实验 14: BGR 色彩模式图像缩放(resize)

复制代码
INTER_NEAREST: 缩放后形状=(100, 600, 3)
INTER_LINEAR:  缩放后形状=(100, 600, 3)
INTER_CUBIC:   缩放后形状=(100, 600, 3)
INTER_LANCZOS4: 缩放后形状=(100, 600, 3)
fx=0.5,fy=2.0:  缩放后形状=(400, 150, 3)
python 复制代码
# 指定目标尺寸
scaled = cv2.resize(img, (600, 100), interpolation=cv2.INTER_LINEAR)

# 按比例缩放
scaled = cv2.resize(img, None, fx=0.5, fy=2.0)

# 5 种插值方法对比
插值方法 速度 质量 适用场景
INTER_NEAREST 最快 最差(马赛克) 像素艺术
INTER_LINEAR 一般 默认,通用
INTER_CUBIC 中等 放大推荐
INTER_AREA 中等 缩小推荐
INTER_LANCZOS4 最好 高质量缩放

实验 15: BGR 色彩模式图像切割与拼合

复制代码
切割: p1(BGR)=[255   0   0] p2=[  0 255   0] p3=[  0   0 255] p4=[  0 255 255]
hstack形状: (100, 400, 3)
vstack形状: (400, 100, 3)
np.concatenate(axis=1)形状: (100, 200, 3)
python 复制代码
# 切割(NumPy 切片)
p1 = full_img[:, 0:100]     # 第1块
p2 = full_img[:, 100:200]   # 第2块
p3 = full_img[:, 200:300]   # 第3块
p4 = full_img[:, 300:400]   # 第4块

# 拼合
h_concat = np.hstack(parts)     # 水平拼接 axis=1
v_concat = np.vstack(parts)     # 垂直拼接 axis=0
np_concat = np.concatenate([p1, p3], axis=1)  # 通用拼接

Part 2: OpenCV 进阶篇 {#part2}

实验 16: BGR 图像通道分离与融合

复制代码
split结果: B形状=(200, 300), G=(200, 300), R=(200, 300)
B通道(蓝色矩形区域)均值: 193
G通道(黄色圆形区域)均值: 224
R通道(红色矩形区域)均值: 227
python 复制代码
b, g, r = cv2.split(img)  # 分离为3个单通道灰度图

# 通道交换 (B<->R 互换)
swapped = cv2.merge([r, g, b])

# 单通道上色 (只保留B通道)
b_colored = cv2.merge([b, np.zeros_like(b), np.zeros_like(b)])
操作 方法 说明
分离 cv2.split(img) 返回3个单通道
融合 cv2.merge([b,g,r]) 合并为3通道
通道提取 img[:,:,0] NumPy切片(更快)

实验 17: 更多色彩空间 LAB & YUV

复制代码
LAB空间: L(亮度)均值=92.4, A(绿-红)均值=168.8, B(蓝-黄)均值=129.1
L通道范围: [0, 248], A: [106, 208], B: [20, 223]
YUV空间: Y(亮度)均值=58.4, U(蓝-亮度)均值=133.8, V(红-亮度)均值=159.3
python 复制代码
# BGR → LAB / YUV
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
l, a, b_ch = cv2.split(lab)

yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)
y, u, v = cv2.split(yuv)
色彩空间 通道 特点
BGR 蓝-绿-红 OpenCV 默认,计算机原生
RGB 红-绿-蓝 PIL/Matplotlib 默认
LAB L亮度/A绿红/B蓝黄 感知均匀,适合色差计算
YUV Y亮度/U蓝差/V红差 电视广播标准,压缩友好
HSV H色调/S饱和度/V明度 颜色过滤首选

实验 18: HSV 色彩模式

复制代码
HSV空间: H(色调)范围[0,120], S(饱和度)[0,255], V(明度)[0,255]
蓝色矩形HSV: H=120, S=255, V=255
红色矩形HSV: H=0,   S=255, V=255
黄色圆形HSV: H=30,  S=255, V=255
蓝色掩码非零像素: 16500
python 复制代码
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 提取蓝色区域
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])
mask = cv2.inRange(hsv, lower_blue, upper_blue)

# 按掩码提取
blue_only = cv2.bitwise_and(img, img, mask=mask)
颜色 H范围(OpenCV) 视觉效果
红色 0-10, 170-179 分两段
黄色 26-34 窄带
绿色 36-77 宽带
蓝色 100-130 宽带
紫色 130-160 宽带

OpenCV HSV: H ∈ 0, 179, S ∈ 0, 255, V ∈ 0, 255


实验 19: 几何变换 --- 翻转/缩放/旋转/透视

复制代码
翻转: 水平翻转后形状=(200, 300, 3)
旋转45°: 变换矩阵=
[[  0.70710678   0.70710678 -26.7766953 ]
 [ -0.70710678   0.70710678 135.35533906]]
旋转-30°缩放0.5x: 完成
python 复制代码
# 翻转
cv2.flip(img, 0)   # 垂直
cv2.flip(img, 1)   # 水平
cv2.flip(img, -1)  # 两者

# 旋转 (围绕中心点)
M = cv2.getRotationMatrix2D((w//2, h//2), 45, 1.0)
rotated = cv2.warpAffine(img, M, (w, h), borderValue=(128,128,128))

# 透视变换 (4点映射)
src_pts = np.float32([[0,0], [w-1,0], [0,h-1], [w-1,h-1]])
dst_pts = np.float32([[50,30], [w-80,10], [20,h-40], [w-30,h-20]])
M = cv2.getPerspectiveTransform(src_pts, dst_pts)
result = cv2.warpPerspective(img, M, (w, h))

实验 20: 词根溯源 --- Affine 仿射变形

复制代码
词源: 拉丁语 affinis "与...相邻/相关", ad-("向") + finis("边界")
仿射变换矩阵 (2x3):
[[ 6.35451505e-01 -1.00502513e-01  5.00000000e+01]
 [ 3.34448161e-02  6.03015075e-01  3.00000000e+01]]

仿射变换 vs 透视变换:
  仿射(Affine):      3对点, 2x3矩阵, 平行线保持平行
  透视(Perspective): 4对点, 3x3矩阵, 平行线可能汇聚

数学公式:
  x' = a1*x + b1*y + c1
  y' = a2*x + b2*y + c2
  
  6个参数定义: 平移(tx,ty) × 旋转(θ) × 缩放(sx,sy) × 错切(shx,shy)
python 复制代码
src_tri = np.float32([[0,0], [w-1,0], [0,h-1]])
dst_tri = np.float32([[50,30], [w-60,40], [30,h-50]])
M = cv2.getAffineTransform(src_tri, dst_tri)
affine = cv2.warpAffine(img, M, (w, h))

实验 21: 图形的绘制 --- 线段/矩形/圆形/椭圆

python 复制代码
canvas = np.ones((300, 400, 3), dtype=np.uint8) * 255

cv2.line(canvas, (50,30), (350,30), (255,0,0), 2, cv2.LINE_AA)     # 线段
cv2.arrowedLine(canvas, (30,90), (370,90), (0,0,0), 2, tipLength=0.03)  # 箭头
cv2.rectangle(canvas, (50,120), (180,220), (255,100,0), 2)          # 空心矩形
cv2.rectangle(canvas, (220,120), (350,220), (0,200,100), -1)        # 实心矩形
cv2.circle(canvas, (100,260), 30, (200,0,200), 2)                   # 空心圆
cv2.circle(canvas, (200,260), 30, (0,150,150), -1)                  # 实心圆
cv2.ellipse(canvas, (150,170), (40,20), 30, 0, 360, (255,0,255), 2) # 椭圆
函数 关键参数
line() pt1, pt2, color, thickness, lineType
rectangle() pt1, pt2, color, thickness(-1=填充)
circle() center, radius, color, thickness
ellipse() center, axes, angle, startAngle, endAngle
arrowedLine() pt1, pt2, color, thickness, tipLength
polylines() pts数组, isClosed, color, thickness

实验 22: 图形绘制综合 --- 多边形/五角星

python 复制代码
# 多边形
pts = np.array([[50,30], [120,20], [140,80], [80,100], [30,70]], np.int32)
pts = pts.reshape((-1, 1, 2))  # 必须 reshape 为 (N, 1, 2)
cv2.polylines(canvas, [pts], True, (255,0,0), 2)   # 描边
cv2.fillPoly(canvas, [pts], (0,255,255))            # 填充

# 五角星(数学计算顶点)
pts_star = []
for i in range(10):
    angle = i * math.pi / 5 - math.pi / 2
    r = 40 if i % 2 == 0 else 18  # 外顶点/内顶点交替
    pts_star.append([cx + int(r*math.cos(angle)), cy + int(r*math.sin(angle))])
cv2.fillPoly(canvas, [pts_star], (0, 0, 255))

实验 23: 渐变效果

复制代码
渐变图尺寸: 400x200
水平渐变(H): B蓝0→255, R红255→0
垂直渐变(V): G绿0→255
径向渐变(R): B+R从圆心1→0, G从0→1
python 复制代码
# 水平渐变(逐列赋值)
for x in range(w):
    grad_h[:, x, 0] = int(255 * x / w)       # B: 0→255
    grad_h[:, x, 2] = int(255 * (w-x)/w)     # R: 255→0

# 垂直渐变(逐行赋值)
for y in range(h):
    grad_v[y, :, 1] = int(255 * y / h)       # G: 0→255

# 径向渐变(距离中心)
dist = math.sqrt((x-cx)**2 + (y-cy)**2) / max_dist
grad_r[y, x] = [255*(1-dist), 255*dist, 255*(1-dist)]

实验 24: 随机的引入(Random & Noise)

复制代码
随机噪声: 形状=(200, 300, 3), 均值=127.2
椒盐噪声(10%): 白色像素=52361, 黑色像素=127639
python 复制代码
# 高斯噪声
noise = np.random.normal(0, 25, img.shape).astype(np.int16)
noisy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)

# 椒盐噪声
def add_salt_pepper(img, prob=0.05):
    result = img.copy()
    h, w = result.shape[:2]
    num = int(h * w * prob / 2)
    coords = [np.random.randint(0, i-1, num) for i in [h, w]]
    result[coords[0], coords[1]] = 255  # 盐(白)
    coords = [np.random.randint(0, i-1, num) for i in [h, w]]
    result[coords[0], coords[1]] = 0    # 椒(黑)
    return result

实验 25: 三角函数的引用(波形绘制)

python 复制代码
# 正弦波
pts_sin = []
for x in range(50, 550):
    y = int(h/2 - 60 * math.sin(2 * math.pi * x / 200))  # 周期200px
    pts_sin.append([x, y])

# 余弦波
pts_cos = []
for x in range(50, 550):
    y = int(h/2 + 40 * math.cos(2 * math.pi * x / 150))  # 周期150px
    pts_cos.append([x, y])

cv2.polylines(canvas, [np.array(pts_sin)], False, (255,0,0), 2)
cv2.polylines(canvas, [np.array(pts_cos)], False, (0,180,0), 2, cv2.LINE_AA)

实验 26: 多边形的绘制(正N边形+星形)

python 复制代码
def regular_polygon(center, radius, sides, angle_offset=0):
    """生成正N边形顶点"""
    pts = []
    for i in range(sides):
        ang = angle_offset + 2 * math.pi * i / sides
        pts.append([int(center[0] + radius*math.cos(ang)),
                     int(center[1] + radius*math.sin(ang))])
    return np.array(pts, np.int32).reshape((-1, 1, 2))

# 3-8 边形
for n in range(3, 9):
    pts = regular_polygon((x, y), 45, n, -math.pi/2)
    cv2.polylines(canvas, [pts], True, (0, 0, 255), 2)

实验 27: 满屏字符 80×25(终端风格)

复制代码
终端模拟: 80x25字符, 像素960x600, 含光标块
python 复制代码
char_w, char_h = 12, 24  # 每字符像素大小
cols, rows = 80, 25       # 80列×25行

# 逐字符绘制
for r in range(rows):
    for c in range(cols):
        ch = sample_chars[(r + c) % len(sample_chars)]
        cv2.putText(canvas, ch, (c*char_w+2, (r+1)*char_h-5),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200,200,200), 1)

# 光标闪烁效果(黑白块交替)
cursor_col = (row * 3) % cols
canvas[y:y+char_h, x:x+char_w] = [0, 0, 0]          # 黑底
canvas[y+2:y+char_h-2, x+2:x+char_w-2] = [255,255,255]  # 白色字符

实验 28: 鼠标键盘事件 --- 画笔原理

复制代码
画笔点数: 30, 颜色: (0, 0, 255)
事件类型: EVENT_LBUTTONDOWN/EVENT_MOUSEMOVE/EVENT_LBUTTONUP
python 复制代码
class VirtualPen:
    def __init__(self):
        self.points = []
        self.drawing = False
        self.color = (0, 0, 255)
        self.thickness = 3

    def on_mouse(self, event, x, y, flags, param):
        if event == cv2.EVENT_LBUTTONDOWN:
            self.drawing = True
        elif event == cv2.EVENT_MOUSEMOVE and self.drawing:
            self.points.append(("move", x, y))
        elif event == cv2.EVENT_LBUTTONUP:
            self.drawing = False

# GUI 环境注册: cv2.setMouseCallback("window", pen.on_mouse)
事件常量 含义
EVENT_LBUTTONDOWN 左键按下
EVENT_LBUTTONUP 左键释放
EVENT_MOUSEMOVE 鼠标移动
EVENT_RBUTTONDOWN 右键按下
EVENT_MBUTTONDOWN 中键按下
EVENT_MOUSEWHEEL 滚轮

实验 29: 键盘响应事件

复制代码
常用按键映射:
  waitKey返回 27: ESC - 退出
  waitKey返回 32: SPACE - 暂停/继续
  waitKey返回 99: c - 切换颜色
  waitKey返回115: s - 保存
python 复制代码
key = cv2.waitKey(30) & 0xFF  # 获取按键 ASCII 码

if key == 27:       # ESC → 退出
    break
elif key == ord('s'):  # 's' → 保存
    cv2.imwrite('screenshot.jpg', frame)
elif key == ord('c'):  # 'c' → 换色
    color_idx = (color_idx + 1) % len(colors)

实验 30: 图像的数学运算

复制代码
cv2.add(A,B) 饱和加法:
[[ 15  30  45]
 [ 60  75  90]
 [105 120 135]]
A+B numpy     模加法: (相同,因为未超出255)
cv2.subtract(A,B) 饱和减法:
[[ 5 10 15]
 [20 25 30]
 [35 40 45]]
cv2.multiply: 原图均值=124.2 → ×2后均值=213.9
cv2.divide(/2): 均值=62.1
python 复制代码
cv2.add(img, 50)       # 饱和加法: value+50, max 255
cv2.subtract(img, 50)  # 饱和减法: value-50, min 0
cv2.multiply(img, 2)   # 饱和乘法
cv2.divide(img, 2)     # 除法
cv2.addWeighted(img1, 0.7, img2, 0.3, 0)  # 加权混合

# NumPy 运算(模溢出)
img_np = img + 100  # uint8溢出会回绕! >255 → value-256

⚠️ 关键区别 : cv2.add() 是饱和运算(截断到 0,255),numpy + 是模运算(溢出回绕)。图像处理必须用 cv2 饱和运算


实验 31: 图像的按位逻辑运算

复制代码
AND:  非零=1600  (交集)
OR:   非零=4086  (并集)
XOR:  非零=2486  (对称差)
NOT:  非零=5914  (取反)
python 复制代码
cv2.bitwise_and(a, b)   # 掩码提取
cv2.bitwise_or(a, b)    # 合并掩码
cv2.bitwise_xor(a, b)   # 差异区域
cv2.bitwise_not(a)      # 反相掩码

# 典型用法: 按掩码提取图像中的物体
mask = cv2.inRange(hsv, lower, upper)
result = cv2.bitwise_and(img, img, mask=mask)

实验 32: 半透明蒙版的应用(Alpha Blending)

复制代码
alpha=0.25: 前景权重=0.25, 背景权重=0.75
alpha=0.5:  前景权重=0.5,  背景权重=0.5
alpha=0.75: 前景权重=0.75, 背景权重=0.25
python 复制代码
# 全局半透明
blended = cv2.addWeighted(bg, 1-alpha, fg, alpha, 0)

# 区域半透明 (仅在mask区域混合)
mask_3ch = cv2.merge([mask, mask, mask])
masked_blend = np.where(mask_3ch > 0, bg//2 + fg//2, bg)

addWeighted(src1, α, src2, β, γ) 公式:dst = src1×α + src2×β + γ


实验 33: 数据增强(Data Augmentation)

复制代码
数据增强方法:
  1. 水平翻转
  2. 旋转15°
  3. 亮度×1.5
  4. 对比度0.8 + 亮度+40
  5. 高斯噪声 σ=25
  6. 颜色抖动 (H+20, S×1.3)
总计: 6种增强, 原始1张→增强后6张
python 复制代码
# 亮度/对比度调整
aug_bright = cv2.convertScaleAbs(img, alpha=1.5, beta=0)   # 对比度×1.5
aug_contrast = cv2.convertScaleAbs(img, alpha=0.8, beta=40) # 对比度0.8,亮度+40

# 颜色抖动 (HSV空间)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.int16)
hsv[:,:,0] = (hsv[:,:,0] + 20) % 180      # H色调偏移
hsv[:,:,1] = np.clip(hsv[:,:,1]*1.3,0,255) # S饱和度增强
aug_color = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
增强方法 代码方式 适用场景
水平翻转 cv2.flip(img, 1) 通用
旋转 cv2.warpAffine 角度不变性
亮度 convertScaleAbs(alpha, beta) 光照变化
噪声 np.random.normal 鲁棒性
颜色抖动 HSV 空间变换 颜色不变性
裁剪缩放 NumPy切片+resize 尺度不变性

Part 3: PIL & 视频处理篇 {#part3}

实验 34: PIL 绘画效果 --- 画出笑脸

复制代码
PIL Image: 尺寸=(300, 300), 模式=RGB
已保存: PIL绘制的笑脸 (椭圆/弧线/填充)
PIL→OpenCV转换: RGB→BGR, 形状=(300, 300, 3)
python 复制代码
from PIL import Image, ImageDraw

img = Image.new("RGB", (300, 300), (255, 255, 255))
draw = ImageDraw.Draw(img)

# 脸
draw.ellipse([50, 50, 250, 250], fill=(255, 220, 50), outline=(200, 150, 0), width=3)
# 眼睛
draw.ellipse([100, 100, 135, 145], fill=(255, 255, 255), outline=(0, 0, 0), width=2)
# 瞳孔
draw.ellipse([112, 115, 123, 128], fill=(0, 0, 0))
# 嘴巴(弧线)
draw.arc([100, 140, 200, 220], start=30, end=150, fill=(200, 50, 50), width=4)

# PIL ↔ OpenCV 互转
cv_img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
pil_img = Image.fromarray(cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB))

PIL 使用 RGB 顺序,OpenCV 使用 BGR 顺序,互转时必须进行颜色空间转换!


实验 35: 将图片转化为像素风格(Pixel Art)

复制代码
像素粒度=4px:  有效像素数=32x32=1024个色块
像素粒度=8px:  有效像素数=16x16=256个色块
像素粒度=16px: 有效像素数=8x8=64个色块
python 复制代码
pixel_size = 8  # 像素块大小

# 缩小 → 放大 (NEAREST插值保留硬边缘)
small = cv2.resize(img, (w//pixel_size, h//pixel_size),
                   interpolation=cv2.INTER_NEAREST)
pixelated = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)

# PIL 方式
pil_small = pil_img.resize((w//pixel_size, h//pixel_size), Image.NEAREST)
pil_big = pil_small.resize((w, h), Image.NEAREST)

实验 36: 动画效果(GIF 生成)

复制代码
动画: 30帧, 300x200, 保存为GIF
内容: 移动+缩放圆 + 旋转矩形
python 复制代码
import imageio

frames = []
for i in range(30):
    frame = np.ones((200, 300, 3), dtype=np.uint8) * 50
    # 移动的圆
    x = int(50 + i * 200 / 29)
    y = int(100 + 40 * math.sin(i * 2 * math.pi / 15))
    cv2.circle(frame, (x, y), 15, (0, 255, 255), -1)
    # 转为 RGB (imageio 需要)
    frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))

# 保存为 GIF
imageio.mimsave('animation.gif', frames, duration=0.1, loop=0)

⚠️ imageio GIF 需要 RGB 格式,BGR 会导致颜色错误!


实验 37: 时钟制作(PIL + TTF 字体)

复制代码
字体加载成功: /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf
已保存: 模拟时钟 10:10:35
python 复制代码
from PIL import ImageFont

font = ImageFont.truetype("/path/to/font.ttf", 16)
# 时针 (hour*30 + min*0.5 = 折合角度)
hour_angle = math.radians((hour % 12) * 30 + minute * 0.5 - 90)
draw.line([cx, cy, cx+50*cos(hour_angle), cy+50*sin(hour_angle)],
          fill=(255,255,255), width=6)

Linux 字体路径/usr/share/fonts/truetype/dejavu/ (DejaVu, 默认) 或 /usr/share/fonts/opentype/


实验 38: GIF 文件格式的读写

复制代码
GIF帧数: 30 (读取之前生成的动画)
PIL GIF信息: 格式=GIF, 模式=P
PIL GIF调色板: True
新GIF创建: 20帧, 100x100, 彩虹色渐变
python 复制代码
# imageio 读取 GIF
reader = imageio.get_reader('animation.gif')
n_frames = len(reader)  # imageio v2.x 用 len()
frame0 = reader.get_data(0)  # 第0帧

# PIL 读取 GIF (逐帧seek)
pil_gif = Image.open('animation.gif')
print(f"格式={pil_gif.format}, 模式={pil_gif.mode}")  # 模式=P(调色板)
pil_gif.seek(1)  # 跳到第1帧
特性 GIF MP4
颜色深度 256色(调色板) 全彩
透明度 支持 不支持
压缩 LZW(无损) H.264(有损)
适用场景 简单动画、图标 真实视频
OpenCV支持 需 imageio/PIL 原生支持

实验 39: 补间插值效果(Tweening)

复制代码
补间动画: 11帧 (t=0.0→1.0, 步长0.1)
方法: cv2.addWeighted(start, 1-alpha, end, alpha)

缓动函数对比:
  t=0.00: 线性=0.00, ease_in_out=0.000
  t=0.25: 线性=0.25, ease_in_out=0.156
  t=0.50: 线性=0.50, ease_in_out=0.500
  t=0.75: 线性=0.75, ease_in_out=0.844
  t=1.00: 线性=1.00, ease_in_out=1.000
python 复制代码
# 线性插值
for i in range(11):
    alpha = i / 10.0
    frame = cv2.addWeighted(start_img, 1-alpha, end_img, alpha, 0)

# 缓动函数 (Ease-In-Out)
def ease_in_out(t):
    return t * t * (3 - 2 * t)  # smoothstep
缓动类型 公式 视觉效果
线性 t 匀速
ease-in 慢入快出
ease-out 1-(1-t)² 快入慢出
ease-in-out t²(3-2t) 慢→快→慢(最自然)

实验 40: 视频处理

复制代码
视频创建: /tmp/cv_output_exp40_video.mp4
编码: mp4v, 10FPS, 300x200, 50帧 → 66757 bytes (65.2KB)
读取视频: 300x200, 10.0FPS, 总帧数=50
第0帧形状: (200, 300, 3)
python 复制代码
# 写入视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter('output.mp4', fourcc, fps, (width, height))
for i in range(frame_count):
    frame = create_frame(i)
    writer.write(frame)
writer.release()  # 必须调用 release()!

# 读取视频
cap = cv2.VideoCapture('output.mp4')
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
while cap.isOpened():
    ret, frame = cap.read()
    if not ret: break
    # 处理每一帧...
cap.release()
FOURCC 编码 容器 特点
*'mp4v' .mp4 MPEG-4, 通用
*'avc1' .mp4 H.264, 高质量
*'XVID' .avi Xvid, 兼容性好
*'MJPG' .avi Motion JPEG, 逐帧

⚠️ writer.release() 必须调用,否则视频文件不完整!


实验 41: 图像的权重(addWeighted 进阶)

复制代码
gamma=-50: 均值=47.8
gamma=+0:  均值=84.7
gamma=+50: 均值=134.4

addWeighted(src1, α, src2, β, γ) 中 gamma 参数是亮度偏移:

  • γ > 0: 整体提亮
  • γ < 0: 整体压暗
  • γ = 0: 纯加权混合
python 复制代码
# alpha渐变效果 (渐入/渐出)
for i in range(11):
    alpha = i / 10.0
    blended = cv2.addWeighted(bg, 1-alpha, fg, alpha, 0)

实验 42: 视频中的音频处理(moviepy)

复制代码
音频处理涉及的关键概念:
  - 音频采样率 (Sample Rate): 典型 44100Hz
  - 音频编码: AAC/MP3/PCM
  - moviepy: with_audio() 合并, subclip() 截取
python 复制代码
from moviepy import VideoFileClip, AudioFileClip

# 读取视频
clip = VideoFileClip('video.mp4')
print(f"时长: {clip.duration}s, 尺寸: {clip.size}, FPS: {clip.fps}")

# 添加音频
audio = AudioFileClip('bgm.mp3')
final = clip.with_audio(audio)
final.write_videofile('output_with_audio.mp4', codec='libx264')

# 截取片段
sub = clip.subclipped(5, 10)  # 5-10秒

# 拼接
from moviepy import concatenate_videoclips
merged = concatenate_videoclips([clip1, clip2])

moviepy v2.x API 变化: subclip()subclipped(), concatenate_videoclips() 需从顶层导入。


实验 43: 将视频转化为字符画

复制代码
处理了 10 帧 (总共50帧)
字符映射表: '@%#*+=-:. '  (10级)
原理: 将每个像素灰度值(0-255)映射到对应的ASCII字符密度
python 复制代码
ASCII_CHARS = "@%#*+=-:. "  # 从密到疏 10级

def frame_to_ascii(frame, cols=80):
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    h, w = gray.shape
    new_h = int(h / w * cols * 0.43)  # 字符宽高比校正
    resized = cv2.resize(gray, (cols, new_h))

    ascii_str = ""
    for row in resized:
        for pixel in row:
            idx = int(pixel) * (len(ASCII_CHARS)-1) // 255
            ascii_str += ASCII_CHARS[idx]
        ascii_str += "\n"
    return ascii_str

# 逐帧处理
cap = cv2.VideoCapture('video.mp4')
while cap.isOpened():
    ret, frame = cap.read()
    if not ret: break
    ascii_art = frame_to_ascii(frame, cols=80)
    # 输出到终端或保存到文件

关键参数

  • cols:字符宽度(越大越精细)
  • 0.43:字符宽高比校正系数(终端字体通常高≈2.3×宽)
  • 字符密度:@(最密) → (最疏)

总结与速查表 {#summary}

色彩空间转换矩阵

复制代码
                  BGR ←→ RGB    (COLOR_BGR2RGB / COLOR_RGB2BGR)
                  BGR ←→ HSV    (COLOR_BGR2HSV / COLOR_HSV2BGR)
                  BGR ←→ LAB    (COLOR_BGR2LAB / COLOR_LAB2BGR)
                  BGR ←→ YUV    (COLOR_BGR2YUV / COLOR_YUV2BGR)
                  BGR ←→ GRAY   (COLOR_BGR2GRAY / COLOR_GRAY2BGR)

OpenCV 图像核心数据结构

复制代码
┌─────────────────────────────────────────────────────┐
│  img = np.ndarray(shape=(H, W, C), dtype=uint8)     │
│                                                     │
│  灰度图: (H, W)       → img[y, x]      → 0-255     │
│  彩色图: (H, W, 3)    → img[y, x, c]   → BGR      │
│  RGBA图: (H, W, 4)    → img[y, x, c]   → BGRA     │
│                                                     │
│  C=0: Blue  │ C=1: Green │ C=2: Red  │ C=3: Alpha  │
└─────────────────────────────────────────────────────┘

常用操作速查

操作 OpenCV NumPy/PIL
创建黑色画布 np.zeros((h,w,3), uint8) -
创建白色画布 np.ones((h,w,3), uint8)*255 Image.new('RGB',(w,h),'white')
颜色转换 cv2.cvtColor(img, code) img.convert('L')
缩放 cv2.resize(img, (w,h)) img.resize((w,h))
旋转 cv2.warpAffine(img, M, (w,h)) img.rotate(angle)
保存 cv2.imwrite(path, img) img.save(path)
读取 cv2.imread(path) Image.open(path)
阈值 cv2.threshold(gray, t, 255, type) img.point(lambda p: 255 if p>t else 0)
混合 cv2.addWeighted(a,α,b,β,γ) Image.blend(a,b,α)

库选型建议

场景 推荐库 原因
计算机视觉/特征检测 OpenCV 算法最全
简单图像处理/格式转换 PIL/Pillow API 简洁
视频编解码 OpenCV + moviepy 互补
科学计算/矩阵运算 NumPy 底层通用
GIF 动图 imageio 读写最简单
深度学习预处理 OpenCV + torchvision 行业标准

技术栈总览

复制代码
        ┌──────────────────────┐
        │   应用层              │
        │  人脸检测·OCR·追踪   │
        ├──────────────────────┤
        │   OpenCV (cv2)       │  核心算法库
        │  ┌────┬────┬──────┐  │
        │  │图像│视频│特征  │  │
        │  │处理│编码│检测  │  │
        │  └────┴────┴──────┘  │
        ├──────────────────────┤
        │   PIL / Pillow       │  补充工具
        │  ┌──────┬─────────┐  │
        │  │ 绘图  │ 格式转换│  │
        │  └──────┴─────────┘  │
        ├──────────────────────┤
        │   NumPy (ndarray)    │  数学基础
        │   矩阵运算·广播·切片 │
        ├──────────────────────┤
        │   imageio / moviepy  │  多媒体
        │   GIF·动画·音频混合  │
        └──────────────────────┘

本文全部实验代码均在华为云 ECS(x2e.8u.16g, Ubuntu 24.04)真实执行,输出数据直接取自服务器 stdout。

系列博客 :Python从零开始 → 编解码 → NLP → Office → 数据库 → 爬虫 → Web后端 → 计算机视觉