第三章 计算机视觉基础(上):图像表示与色彩空间、滤波与边缘检测、形态学操作

各位同学,欢迎来到实战课程的基础篇。说实话,很多初学者一上来就追着 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)
# 第一个维度是高度(行数),第二个是宽度(列数),第三个是通道数
小提示:OpenCV 读取图像的通道顺序是 BGR,不是 RGB。很多新手在这里翻车,显示出来的图像颜色怪怪的。我习惯在读图后立即用 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 转换一下。

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)
注意:OpenCV 中 HSV 的 H 范围是 0~180,不是 0~360。因为用 uint8 存储,360 除以 2 就是 180。我曾经在项目里直接用 0~360 的范围,结果 mask 全黑,排查了半天才发现这个细节。

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)
避坑指南:我曾经在夜间行车视频上用 Canny,结果全是噪点,根本看不到车道线。后来发现是低阈值设得太低,把路面纹理也当成了边缘。把低阈值从 30 提到 80,效果立竿见影。

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×3 开始试,不行再加大。迭代次数也同理,1 次不够就 2 次,但别超过 3 次,否则目标会严重变形。

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

这个流水线处理后的图像,边缘清晰、噪声少、轮廓完整。后续不管是做轮廓检测还是特征提取,都会省心很多。

嗯,今天的内容就到这。下一章我们会讲特征点检测和光流法,这些都是跟踪算法的基础。回去把今天的代码跑一遍,有问题随时问我。