第三章 计算机视觉基础(上):图像表示与色彩空间、滤波与边缘检测、形态学操作
各位同学,欢迎来到实战课程的基础篇。说实话,很多初学者一上来就追着 YOLO、Transformer 跑,结果连图像在计算机眼里长什么样都没搞明白。我个人觉得,这部分基础打不牢,后面调参就像在摸黑走路。今天我们就花点时间,把图像表示、色彩空间、滤波和形态学这些基本功捋一遍。
3.1 图像表示:计算机眼中的世界
先问个问题:一张彩色图片,在计算机里到底是什么?
说白了,就是一堆数字。一张 640×480 的彩色图,在计算机内存里就是一个 480 行、640 列、3 个通道的矩阵。每个像素点的值范围是 0 到 255,代表亮度。
我记得刚入行时,有个同事 debug 了半天,发现图像读出来全是黑的。最后发现是 imread 默认以灰度图方式读取,彩色通道全丢了。嗯,这种坑踩过一次就记住了。
import cv2
import numpy as np
# 读取彩色图像
img = cv2.imread('road.jpg')
print(img.shape) # 输出:(480, 640, 3)
# 第一个维度是高度(行数),第二个是宽度(列数),第三个是通道数
3.2 色彩空间:RGB 与 HSV
RGB 是我们最熟悉的色彩模型。红绿蓝三个通道叠加,就能表示几乎所有颜色。但在做图像处理时,RGB 其实不太好用。为什么?因为三个通道都跟亮度强相关,光照一变,三个值一起跳,很难做颜色分割。
这时候 HSV 就派上用场了。H 是色调,S 是饱和度,V 是明度。色调用角度表示,0° 是红色,120° 是绿色,240° 是蓝色。你想想看,不管光照怎么变,红色物体的 H 值基本稳定在 0° 附近。这就好办了。
我在做交通信号灯检测时,就靠 HSV 空间做颜色分割。红灯的 H 值范围大概在 0°~10° 和 170°~180°,绿灯在 40°~80°。用 inRange 函数一筛,候选区域就出来了。
# BGR 转 HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 定义红色的 HSV 范围
lower_red1 = np.array([0, 50, 50])
upper_red1 = np.array([10, 255, 255])
lower_red2 = np.array([170, 50, 50])
upper_red2 = np.array([180, 255, 255])
# 提取红色区域
mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask = cv2.bitwise_or(mask1, mask2)
3.3 图像滤波:去噪与平滑
实际项目中,摄像头采集的图像几乎没有干净的。传感器噪声、光照不均、运动模糊,各种干扰。滤波就是用来处理这些问题的。
最基础的是均值滤波。用一个 3×3 的窗口滑过图像,每个像素取周围 9 个点的平均值。简单,但会模糊边缘。高斯滤波稍微好点,给中心像素更高的权重,边缘保留得更好。
我个人在行人检测的预处理中,常用高斯滤波。核大小选 5×5,sigma 设 1.0 左右。既能去掉大部分噪点,又不会把行人的轮廓给抹没了。
# 高斯滤波
blurred = cv2.GaussianBlur(img, (5, 5), 1.0)
# 均值滤波
blurred_mean = cv2.blur(img, (5, 5))
# 中值滤波——对椒盐噪声特别有效
blurred_median = cv2.medianBlur(img, 5)
3.4 边缘检测:Sobel 与 Canny
边缘是图像中灰度变化剧烈的地方。行人、车辆、车道线,这些目标的边界都是边缘。检测边缘,说白了就是求图像的梯度。
Sobel 算子是最经典的梯度算子。它用两个 3×3 的核分别计算水平和垂直方向的梯度。我习惯把两个方向的梯度合并,得到边缘的幅值图。
但 Sobel 有个问题:它对噪声敏感,而且检测出的边缘比较粗。Canny 算法就聪明多了。它先做高斯平滑,然后计算梯度,再做非极大值抑制——只保留梯度方向上的局部最大值,最后用双阈值连接边缘。
Canny 的两个阈值怎么设?我一般设低阈值为 50,高阈值为 150。如果边缘断断续续的,就降低低阈值;如果出现太多假边缘,就提高高阈值。这个需要根据实际图像微调。
# Sobel 边缘检测
sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
sobel_combined = cv2.magnitude(sobel_x, sobel_y)
# Canny 边缘检测
edges = cv2.Canny(gray, 50, 150)
3.5 图像形态学操作:膨胀、腐蚀与开闭运算
形态学操作,说白了就是处理二值图像的形状。膨胀让白色区域变大,腐蚀让白色区域变小。这两个操作组合起来,能解决很多实际问题。
开运算是先腐蚀后膨胀。它能去掉小的白色噪点,同时保持大物体的形状不变。闭运算是先膨胀后腐蚀,能填补物体内部的小空洞。
我在做车辆检测时,经常用闭运算来连接断裂的车身轮廓。一辆车在 Canny 边缘图上可能断成好几段,闭运算一处理,轮廓就连起来了,后续的轮廓检测就准确多了。
# 定义结构元素
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 腐蚀
eroded = cv2.erode(binary, kernel, iterations=1)
# 膨胀
dilated = cv2.dilate(binary, kernel, iterations=1)
# 开运算
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
3.6 综合实战:预处理流水线
好了,我们把今天学的东西串起来,搭一个简单的图像预处理流水线。这个流程在我做的行人检测项目中实际用过,效果还不错。
def preprocess_pipeline(img):
# 1. 高斯滤波去噪
blurred = cv2.GaussianBlur(img, (5, 5), 1.0)
# 2. BGR 转灰度
gray = cv2.cvtColor(blurred, cv2.COLOR_BGR2GRAY)
# 3. Canny 边缘检测
edges = cv2.Canny(gray, 50, 150)
# 4. 闭运算连接断裂边缘
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
return closed
这个流水线处理后的图像,边缘清晰、噪声少、轮廓完整。后续不管是做轮廓检测还是特征提取,都会省心很多。
嗯,今天的内容就到这。下一章我们会讲特征点检测和光流法,这些都是跟踪算法的基础。回去把今天的代码跑一遍,有问题随时问我。