第二章 计算机视觉基础回顾:图像表示、滤波、边缘检测与霍夫变换
各位同学,欢迎来到第二章。在正式开始写车道线检测代码之前,我们得先把地基打牢。说白了,车道线检测本质上就是在一堆像素里找到特定的线条。如果你连图像怎么存、怎么去噪、怎么找边缘都不清楚,后面肯定会踩坑。
我个人习惯,每次开始一个新项目前,都会花半小时把这几块基础过一遍。别嫌烦,这就像开车前系安全带——省不掉。好,我们开始。
2.1 图像表示:RGB 与灰度
计算机看到的图像,其实就是一个数字矩阵。我刚开始学的时候,总觉得这很抽象。后来自己写了个程序,把一张图片打印成数字,才恍然大悟——原来如此。
2.1.1 RGB 彩色图像
一张彩色图片,由三个通道叠加而成:红(R)、绿(G)、蓝(B)。每个像素点,都有三个数值,范围 0~255。0 代表最暗,255 代表最亮。
举个例子,一个纯红色的像素,它的值是 (255, 0, 0)。纯白色是 (255, 255, 255)。纯黑色是 (0, 0, 0)。
在 OpenCV 里,读取彩色图像默认是 BGR 顺序,不是 RGB。这是个经典坑,我当年调了半天颜色不对,最后发现是通道顺序搞反了。
import cv2
# 读取彩色图像
img_color = cv2.imread('road.jpg') # 注意:这里是 BGR 顺序
print(img_color.shape) # 输出:(高度, 宽度, 3)
2.1.2 灰度图像
灰度图像只有一个通道,每个像素只有一个数值,同样 0~255。0 是黑色,255 是白色。
为什么车道线检测常用灰度图?因为颜色信息对找线条来说,很多时候是冗余的。我们只关心亮度变化,不关心它是红色还是蓝色。灰度图数据量只有彩色图的 1/3,处理起来快得多。
# 转为灰度图
img_gray = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY)
print(img_gray.shape) # 输出:(高度, 宽度)
2.2 图像滤波:高斯滤波与中值滤波
原始图像里往往有很多噪声。比如摄像头传感器噪点、路面上的小石子、阴影边缘的毛刺。滤波的目的,就是把这些干扰去掉,保留我们真正关心的结构。
2.2.1 高斯滤波
高斯滤波是一种线性平滑滤波。它用一个高斯核(说白了就是一个权重矩阵)去和图像做卷积。中心像素的权重最大,越往边缘权重越小。这样处理后的图像,会变得模糊,但噪声也被抹平了。
为什么用高斯核而不是平均核?因为高斯核更符合自然图像的统计特性——离得近的像素相关性高,离得远的像素相关性低。
# 高斯滤波
img_gaussian = cv2.GaussianBlur(img_gray, (5, 5), 1.5)
# 参数解释:(5,5)是核大小,1.5是标准差
核大小选奇数,比如 3x3、5x5、7x7。核越大,图像越模糊。标准差控制模糊程度,一般设 1.0~2.0 之间。
2.2.2 中值滤波
中值滤波是一种非线性滤波。它把窗口内的像素排序,取中间值作为输出。这玩意儿对椒盐噪声(就是那种黑白点噪声)特别有效。
我做过一个对比实验:在加了大量椒盐噪声的图像上,高斯滤波会留下很多残留噪点,而中值滤波几乎能完美去除。
# 中值滤波
img_median = cv2.medianBlur(img_gray, 5)
# 5 是核大小,必须是奇数
中值滤波的缺点是计算量大一点,因为要排序。不过对于车道线这种实时性要求高的场景,用 3x3 或 5x5 的核,速度完全够用。
| 滤波类型 | 适用场景 | 缺点 |
|---|---|---|
| 高斯滤波 | 高斯噪声、一般平滑 | 模糊边缘 |
| 中值滤波 | 椒盐噪声、脉冲噪声 | 计算稍慢 |
2.3 边缘检测:Canny 算法
边缘,就是图像中亮度变化剧烈的地方。车道线相对于路面,有明显的亮度跳变,所以边缘检测是找车道线的关键一步。
Canny 边缘检测是目前最经典的方法。它不是一个单一操作,而是一套流程:
- 高斯滤波:先降噪,防止噪声被误判为边缘。
- 计算梯度:用 Sobel 算子算出每个像素的梯度强度和方向。
- 非极大值抑制:只保留梯度方向上的局部最大值,把粗边缘细化成单像素宽。
- 双阈值检测:设定高阈值和低阈值。高于高阈值的,确定为强边缘;低于低阈值的,直接丢弃;介于两者之间的,只有与强边缘相连才保留。
嗯,这里要注意:双阈值是 Canny 的精髓。高阈值决定了哪些边缘是「确信无疑」的,低阈值决定了哪些边缘是「可能」的。我一般设高阈值 150,低阈值 50,然后根据实际效果微调。
# Canny 边缘检测
edges = cv2.Canny(img_gaussian, 50, 150)
# 参数:低阈值 50,高阈值 150
2.4 霍夫变换原理
边缘检测之后,我们得到了一堆像素点。但这些点怎么变成一条直线?霍夫变换就是干这个的。
它的核心思想很巧妙:把图像空间中的点,映射到参数空间中的曲线。在图像空间中,一条直线上的所有点,在参数空间中对应的曲线会交于一点。找到这个交点,就找到了直线。
具体来说,我们用极坐标表示直线:ρ = x·cosθ + y·sinθ。其中 ρ 是原点到直线的距离,θ 是直线的法线角度。
算法步骤:
- 初始化一个二维累加器数组,横轴是 θ(0~180度),纵轴是 ρ。
- 对于每个边缘点 (x, y),遍历所有 θ,计算对应的 ρ,在累加器中加 1。
- 累加器中值超过阈值的点,就对应一条直线。
说白了,霍夫变换就是「投票」。每个边缘点都投一票,票数高的就是直线。
# 霍夫直线检测
lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=50,
minLineLength=100, maxLineGap=50)
# rho: 距离分辨率,theta: 角度分辨率,threshold: 投票阈值
# minLineLength: 最小线段长度,maxLineGap: 最大间隔
2.5 本章知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个流程图,也可以当作一个检查清单。
你看,整个流程是串行的:输入图像 → 灰度化 → 滤波去噪 → 边缘检测 → 霍夫变换找直线。每一步的输出,都是下一步的输入。我在实际项目中,经常需要回到某一步去调参数,比如滤波核大小、Canny 阈值、霍夫投票阈值。这很正常,调参本身就是工程的一部分。
好了,这一章的基础知识就到这里。记住:图像是矩阵,滤波是卷积,边缘是梯度突变,霍夫是投票。把这四个概念刻在脑子里,后面写代码你会觉得特别顺手。