OpenCV 图像处理实战指南

前言

OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和图像处理库,拥有2500+优化算法,支持Python、C++、Java等多种编程语言。本文将带你从基础入门到实战,掌握 OpenCV 的核心图像处理技术。

1. 环境搭建

1
pip install opencv-python opencv-contrib-python numpy matplotlib

提示opencv-python 包含主要模块,opencv-contrib-python 包含额外的高级算法(如SIFT、SURF等)。

2. 图像基础原理

数字图像表示

数字图像本质是一个像素矩阵:

  • 灰度图像:单通道矩阵,每个元素表示像素亮度(0-255)
  • 彩色图像:多通道矩阵,通常为BGR三通道,每个通道0-255
  • RGBA图像:四通道,包含透明度Alpha通道
1
2
3
4
5
6
7
8
9
10
11
12
图像坐标系统:
┌─────────────────────────────→ X (列)
│ (0,0) (1,0) (2,0)
│ ●────────●────────●
│ │ pixel │ pixel │
│ │ │ │
│ (0,1) (1,1) (2,1)
│ ●────────●────────●
│ │ pixel │ pixel │
│ ↓ ↓ ↓
└─────────────────────────────
Y (行)

色彩空间原理

色彩空间 组成 应用场景
RGB 红、绿、蓝加法混色 屏幕显示、图像存储
BGR OpenCV默认格式 OpenCV内部处理
GRAY 亮度单通道 灰度处理、特征提取
HSV 色调、饱和度、明度 颜色分割、目标追踪
LAB 明度、a通道、b通道 色彩均衡、跨设备一致

HSV 色彩空间优势:H(色调)不受光线亮度影响,更适合颜色识别任务。

3. 基础图像操作

读取、显示和保存图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 读取图像
img = cv2.imread('image.jpg')

# 转换色彩空间(BGR转RGB)
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# 显示图像
cv2.imshow('Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

# 保存图像
cv2.imwrite('output.jpg', img)

原理:OpenCV 默认使用 BGR 格式读取图像,而 Matplotlib 使用 RGB 格式,因此显示时需要转换。

获取图像基本信息

1
2
3
print(f"图像形状: {img.shape}")  # (高度, 宽度, 通道数)
print(f"图像数据类型: {img.dtype}")
print(f"图像尺寸: {img.size} 像素")

4. 图像几何变换

调整大小与裁剪

1
2
3
4
5
6
7
8
9
10
11
# 调整图像大小
resized = cv2.resize(img, (800, 600))

# 按比例缩放
scale = 0.5
width = int(img.shape[1] * scale)
height = int(img.shape[0] * scale)
resized = cv2.resize(img, (width, height))

# 裁剪图像
cropped = img[100:400, 200:500]

旋转与翻转原理

旋转矩阵数学表达

1
2
R = [cosθ  -sinθ]
[sinθ cosθ]
1
2
3
4
5
6
7
8
9
10
11
12
13
# 获取旋转矩阵
center = (img.shape[1] // 2, img.shape[0] // 2)
angle = 45
scale = 1.0
rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)

# 应用旋转
rotated = cv2.warpAffine(img, rotation_matrix, (img.shape[1], img.shape[0]))

# 翻转图像
flipped_horizontal = cv2.flip(img, 1) # 水平翻转
flipped_vertical = cv2.flip(img, 0) # 垂直翻转
flipped_both = cv2.flip(img, -1) # 同时翻转

仿射变换与透视变换

仿射变换(Affine Transform):保持平行线的平行性,包括旋转、平移、缩放、剪切。

1
2
3
4
5
6
7
原始图像                          仿射变换后
┌────────┐ ╱╲
│ ●───● │ ─────→ ╱────╲
│ │ │ │ ╱ ╲
│ ●───● │ ╱________╲
└────────┘
平行四边形 保持平行四边形

透视变换(Perspective Transform):可处理任意四边形到四边形的映射,用于矫正倾斜文档。

1
2
3
4
5
6
7
8
9
10
11
# 定义变换前的四个点
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])

# 定义变换后的四个点
pts2 = np.float32([[0, 0], [300, 0], [0, 300], [300, 300]])

# 获取透视变换矩阵 (3x3)
M = cv2.getPerspectiveTransform(pts1, pts2)

# 应用透视变换
warped = cv2.warpPerspective(img, M, (300, 300))

5. 图像色彩空间处理

1
2
3
4
5
6
7
8
9
10
11
12
13
# BGR 转灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# BGR 转 HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# HSV 通道分离
h, s, v = cv2.split(hsv)

# 颜色阈值分割 - 提取特定颜色
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([130, 255, 255])
mask = cv2.inRange(hsv, lower_blue, upper_blue)

灰度转换原理

加权求和法(Luminosity Method):

1
Gray = 0.299 × R + 0.587 × G + 0.114 × B

人眼对绿色最敏感,所以绿色权重最高。

颜色阈值分割原理

在 HSV 空间进行颜色分割更稳定,因为:

  • H 通道代表颜色类型(0-180°)
  • S 通道代表颜色纯度
  • V 通道代表亮度

这样可以将”是什么颜色”和”有多亮”分开处理。

6. 图像平滑与降噪

模糊算法原理对比

算法 原理 特点 适用场景
均值模糊 邻域像素求平均 简单、计算快 快速去噪
高斯模糊 加权平均(高斯核) 权重随距离递减 去除高斯噪声
中值模糊 邻域像素中值 对椒盐噪声效果好 脉冲噪声
双边滤波 空间+像素差加权 保留边缘 美颜、HDR
1
2
3
4
5
6
7
8
9
10
11
# 均值模糊
blur = cv2.blur(img, (5, 5))

# 高斯模糊
gaussian_blur = cv2.GaussianBlur(img, (5, 5), 0)

# 中值模糊(对椒盐噪声效果好)
median_blur = cv2.medianBlur(img, 5)

# 双边滤波(保留边缘)
bilateral = cv2.bilateralFilter(img, 9, 75, 75)

高斯模糊数学原理

二维高斯函数:

1
G(x,y) = (1/2πσ²) × e^(-(x²+y²)/2σ²)

其中 σ 控制模糊程度,值越大越模糊。

双边滤波原理

双边滤波同时考虑空间距离和像素值差异:

1
BF(I) = (1/Wp) × Σ Gs(||p-q||) × Gr(|Ip-Iq|) × Iq
  • Gs:空间高斯函数
  • Gr:像素值高斯函数

这样边缘处像素差大,权重小,被保留;平坦区域权重均匀,被平滑。

锐化处理

1
2
3
4
kernel = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
sharpened = cv2.filter2D(img, -1, kernel)

锐化原理:增强图像细节,公式:
$$\text{Sharpened} = \text{Original} + \alpha \times (\text{Original} - \text{Blurred})$$

拉普拉斯算子可提取边缘,将边缘加回原图即可锐化。

7. 边缘检测与轮廓

边缘检测算子对比

算子 原理 优缺点
Sobel 梯度近似(一阶导数) 对噪声敏感,可检测方向
Laplacian 二阶导数 边缘更细,对噪声更敏感
Canny 多阶段算法 边缘细、定位准确、抗噪好

Canny 算法原理(多阶段)

1
2
3
4
5
6
7
8
9
10
11
12
13
原始图像

高斯模糊(去噪)

梯度计算(Sobel)

非极大值抑制(NMS)- 细化边缘

双阈值检测 - 区分强边缘/弱边缘

边缘连接(滞后阈值处理)

最终边缘

双阈值机制

  • 高阈值:确定边缘
  • 低阈值:连接断开的边缘(只连接有强边缘相邻的弱边缘)
1
2
3
4
5
6
7
8
9
# 转换为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# Canny 边缘检测
edges = cv2.Canny(gray, 50, 150)

# 显示结果
cv2.imshow('Edges', edges)
cv2.waitKey(0)

Sobel 和 Laplacian 算子

1
2
3
4
5
6
7
8
9
# Sobel 边缘检测(计算梯度)
sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) # X方向梯度
sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) # Y方向梯度

# 合并梯度
sobel = cv2.bitwise_or(sobelx, sobely)

# Laplacian 边缘检测(二阶导数)
laplacian = cv2.Laplacian(gray, cv2.CV_64F)

Sobel 梯度原理

1
2
3
4
5
Gx = [-1  0  +1]     Gy = [-1  -2  -1]
[-2 0 +2] [ 0 0 0]
[-1 0 +1] [+1 +2 +1]

|G| = √(Gx² + Gy²)

轮廓查找与绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查找轮廓
contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

# 绘制轮廓
contour_img = img.copy()
cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 2)

# 计算轮廓特征
for cnt in contours:
area = cv2.contourArea(cnt)
perimeter = cv2.arcLength(cnt, True)
if area > 100:
print(f"面积: {area}, 周长: {perimeter}")

轮廓检索模式

  • RETR_EXTERNAL:只检索外轮廓
  • RETR_LIST:检索所有轮廓,不建立层级
  • RETR_TREE:检索所有轮廓,建立完整层级树

8. 图像阈值处理

全局阈值 vs 自适应阈值

方法 原理 适用场景
全局阈值 固定像素值作为阈值 光照均匀图像
自适应阈值 根据邻域计算局部阈值 光照不均图像
Otsu自动阈值 最大化类间方差 双峰图像自动寻优
1
2
3
4
5
6
7
8
9
# 全局阈值
_, thresh_binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 自适应阈值
adaptive_thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)

# Otsu's 二值化
_, otsu_thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Otsu 阈值法原理

自动寻找最佳分割阈值,使前景和背景的类间方差最大:

1
σ² = w₁ × w₂ × (μ₁ - μ₂)²

其中 w₁, w₂ 是两类权重,μ₁, μ₂ 是两类均值。

阈值类型

1
2
3
4
THRESH_BINARY:  if src > thresh: dst = max_val else: dst = 0
THRESH_BINARY_INV: if src > thresh: dst = 0 else: dst = max_val
THRESH_TRUNC: if src > thresh: dst = thresh else: dst = src
THRESH_TOZERO: if src > thresh: dst = src else: dst = 0

9. 形态学操作

腐蚀与膨胀原理

操作 原理 效果
腐蚀 (Erosion) 邻域最小值替换中心 去除白色噪点、断开连接
膨胀 (Dilation) 邻域最大值替换中心 扩大白色区域、填充空洞
1
2
3
4
原始          腐蚀后         膨胀后
██████ ██ ██ ████████
██ ██ → ██ ██ → ████████
██████ ██ ██ ████████
1
2
3
4
5
6
7
kernel = np.ones((5, 5), np.uint8)

# 腐蚀
eroded = cv2.erode(img, kernel, iterations=1)

# 膨胀
dilated = cv2.dilate(img, kernel, iterations=1)

开运算与闭运算

操作 组成 作用
开运算 先腐蚀后膨胀 去除小噪点、断开细小连接
闭运算 先膨胀后腐蚀 填充小孔洞、连接邻近物体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开运算(去噪)
opened = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

# 闭运算(填充)
closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)

# 梯度运算(提取轮廓)
gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)

# 顶帽(提取小物体)
tophat = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel)

# 黑帽(提取小孔洞)
blackhat = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel)

10. 图像分割

分水岭算法原理

模拟地形水淹过程:

  1. 从低洼处(局部最小值)开始注水
  2. 水位上升,相邻盆地相遇时筑坝
  3. 最终水坝即为分割边界
1
2
3
4
5
6
7
# 距离变换
dist_transform = cv2.distanceTransform(gray, cv2.DIST_L2, 5)

# 分水岭分割
markers = np.zeros(gray.shape, dtype=np.int32)
# 标记前景和背景...
watershed = cv2.watershed(img, markers)

GrabCut 算法原理

基于图割(Graph Cut)的迭代前景提取:

  1. 用户定义初始矩形(前景种子)
  2. 建立像素级图模型
  3. 迭代优化:最小割 → 更新分割 → 重复
  4. 最终得到前景/背景概率
1
2
3
4
5
6
7
8
9
10
# 定义初始矩形 (x, y, w, h)
rect = (50, 50, 300, 300)
mask = np.zeros(gray.shape, np.uint8)
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)

cv2.grabCut(img, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)

# 获取前景掩码
final_mask = np.where((mask == 2) | (mask == 0), 0, 1).astype('uint8')

11. 霍夫变换

霍夫直线检测原理

将笛卡尔坐标系的直线转换到霍夫参数空间:
$$y = mx + b \Rightarrow b = -mx + y$$

一条直线上的点,在参数空间中汇聚成一条线:

1
2
3
4
5
6
笛卡尔坐标系              霍夫参数空间 (m, b)
│ │
│ ● P1 │ ● (m1,b1)
│ ● P2 │ ●
│● P3 │● 交点=(m,b)对应一直线
└─────────→ └─────────→
1
2
3
4
5
6
# 概率霍夫直线检测(更快)
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100,
minLineLength=50, maxLineGap=10)
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(img, (x1, y1), (x2, y2), (0, 255, 0), 2)

霍夫圆检测原理

圆的一般方程:

1
(x-a)² + (y-b)² = r²

需要三个参数 (a, b, r),因此是三维霍夫空间。

1
2
投票累加器:统计 (a, b, r) 组合的票数
峰值点 → 检测到的圆
1
2
3
4
5
6
7
# 霍夫圆检测
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 20,
param1=50, param2=30)
if circles is not None:
circles = np.uint16(np.around(circles))
for circle in circles[0, :]:
cv2.circle(img, (circle[0], circle[1]), circle[2], (0, 255, 0), 2)

12. 图像拼接与混合

图像融合原理

金字塔融合

  1. 构建高斯金字塔
  2. 构建拉普拉斯金字塔
  3. 在金字塔层级上融合
  4. 重建图像
1
2
# 简单混合
blended = cv2.addWeighted(img1, 0.5, img2, 0.5, 0)

拼接方法

1
2
3
4
5
# 横向拼接
stitched = np.hstack((img1, img2))

# 纵向拼接
stitched = np.vstack((img1, img2))

13. 实战项目:文档扫描仪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def scan_document(image):
"""
文档扫描完整流程:
1. 去噪
2. 边缘检测
3. 轮廓查找
4. 透视变换矫正
"""
# 转换为灰度
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# 高斯模糊去噪
blurred = cv2.GaussianBlur(gray, (5, 5), 0)

# Canny 边缘检测
edged = cv2.Canny(blurred, 75, 200)

# 查找轮廓
contours, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]

# 多边形拟合找四边形
for cnt in contours:
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
if len(approx) == 4:
# 透视变换矫正
pts = approx.reshape(4, 2)
# ... 透视变换代码
return approx

return None

14. 性能优化技巧

技巧 说明
使用 NumPy 向量化 避免 Python 循环,用数组操作
适当降采样 大图先缩小处理,再放大
选择合适的数据类型 uint8 vs float32,按需转换
并行处理 cv2.setUseOptimized(True) 启用优化
GPU 加速 使用 cv2.cuda 模块(需安装 cuda 版本)
1
2
3
4
5
6
# 性能优化
cv2.setUseOptimized(True)
cv2.setNumThreads(8)

# 快速降采样
small = img[::2, ::2] # 2倍降采样

总结

本文深入讲解了 OpenCV 图像处理的核心原理与实战技巧:

类别 内容
基础 图像读取、显示、色彩空间转换
变换 旋转、翻转、仿射变换、透视变换
滤波 高斯模糊、中值滤波、双边滤波、锐化
边缘 Sobel、Laplacian、Canny 算子
形态学 腐蚀、膨胀、开闭运算
分割 阈值分割、分水岭、GrabCut
变换检测 霍夫直线、霍夫圆检测

进阶学习方向

  • 视频处理:摄像头读取、帧间处理
  • 特征检测:SIFT、ORB、FAST 特征点
  • 目标追踪:KCF、MOSSE、卡尔曼滤波
  • 深度学习集成:DNN 模块加载 TensorFlow/ONNX 模型
  • 3D 重建:立体匹配、点云处理

参考资源


关注公众号,获取更多技术干货!