计算机视觉实战: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 | t² |
慢入快出 |
| 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后端 → 计算机视觉