第二章 计算机视觉基础回顾:图像表示、滤波、边缘检测与霍夫变换

各位同学,欢迎来到第二章。在正式开始写车道线检测代码之前,我们得先把地基打牢。说白了,车道线检测本质上就是在一堆像素里找到特定的线条。如果你连图像怎么存、怎么去噪、怎么找边缘都不清楚,后面肯定会踩坑。

我个人习惯,每次开始一个新项目前,都会花半小时把这几块基础过一遍。别嫌烦,这就像开车前系安全带——省不掉。好,我们开始。

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 之间。

注意:高斯滤波会模糊边缘。如果你后续要做边缘检测,核不能太大。我曾经在一条弯道密集的路段用了 9x9 的核,结果车道线边缘全糊了,检测效果一塌糊涂。

2.2.2 中值滤波

中值滤波是一种非线性滤波。它把窗口内的像素排序,取中间值作为输出。这玩意儿对椒盐噪声(就是那种黑白点噪声)特别有效。

我做过一个对比实验:在加了大量椒盐噪声的图像上,高斯滤波会留下很多残留噪点,而中值滤波几乎能完美去除。

# 中值滤波
img_median = cv2.medianBlur(img_gray, 5)
# 5 是核大小,必须是奇数

中值滤波的缺点是计算量大一点,因为要排序。不过对于车道线这种实时性要求高的场景,用 3x3 或 5x5 的核,速度完全够用。

滤波类型 适用场景 缺点
高斯滤波 高斯噪声、一般平滑 模糊边缘
中值滤波 椒盐噪声、脉冲噪声 计算稍慢

2.3 边缘检测:Canny 算法

边缘,就是图像中亮度变化剧烈的地方。车道线相对于路面,有明显的亮度跳变,所以边缘检测是找车道线的关键一步。

Canny 边缘检测是目前最经典的方法。它不是一个单一操作,而是一套流程:

  1. 高斯滤波:先降噪,防止噪声被误判为边缘。
  2. 计算梯度:用 Sobel 算子算出每个像素的梯度强度和方向。
  3. 非极大值抑制:只保留梯度方向上的局部最大值,把粗边缘细化成单像素宽。
  4. 双阈值检测:设定高阈值和低阈值。高于高阈值的,确定为强边缘;低于低阈值的,直接丢弃;介于两者之间的,只有与强边缘相连才保留。

嗯,这里要注意:双阈值是 Canny 的精髓。高阈值决定了哪些边缘是「确信无疑」的,低阈值决定了哪些边缘是「可能」的。我一般设高阈值 150,低阈值 50,然后根据实际效果微调。

# Canny 边缘检测
edges = cv2.Canny(img_gaussian, 50, 150)
# 参数:低阈值 50,高阈值 150
避坑指南:我曾经在一个阴天场景下,直接用默认阈值,结果车道线边缘断断续续。后来我把低阈值降到 30,高阈值降到 120,效果就好了很多。记住:阈值不是固定的,要根据光照和对比度动态调整。

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: 最大间隔
我的习惯:在车道线场景中,我会把 minLineLength 设大一点(比如 100 像素),过滤掉那些短小的噪声线段。maxLineGap 设 50 左右,允许车道线因为磨损或阴影出现小断裂。

2.5 本章知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个流程图,也可以当作一个检查清单。

车道线检测基础:图像处理流水线 输入图像 灰度转换 滤波(高斯/中值) Canny 边缘检测 霍夫变换检测直线 图2-1:车道线检测基础处理流水线

你看,整个流程是串行的:输入图像 → 灰度化 → 滤波去噪 → 边缘检测 → 霍夫变换找直线。每一步的输出,都是下一步的输入。我在实际项目中,经常需要回到某一步去调参数,比如滤波核大小、Canny 阈值、霍夫投票阈值。这很正常,调参本身就是工程的一部分。

好了,这一章的基础知识就到这里。记住:图像是矩阵,滤波是卷积,边缘是梯度突变,霍夫是投票。把这四个概念刻在脑子里,后面写代码你会觉得特别顺手。

可可精品资料库