3. 图像处理基础(下):图像缩放、旋转、仿射变换。图像滤波与去噪(均值滤波、高斯滤波、中值滤波)。边缘检测原理(Sobel, Canny算子)
好,咱们接着聊。上一节我们把图像的像素、色彩空间和直方图这些基础概念过了一遍。这一节,我们要动真格的了——开始对图像“动手”。
说白了,车道线检测的整个流程,就是一系列图像操作的组合。你想想看,摄像头拍到的原始画面,直接扔给算法,那肯定不行。得先把它处理成算法喜欢的样子。
我个人习惯把这一节的内容分成三大块:几何变换(让图像动起来)、滤波去噪(让图像干净点)、边缘检测(找到图像里的线条)。这三板斧,是车道线检测的基石。
3.1 几何变换:让图像动起来
先说说几何变换。为什么要做这个?因为摄像头装车上,角度是固定的,但路是弯的、车是晃的。我们需要把图像“掰正”,或者把感兴趣的区域提取出来。
3.1.1 图像缩放
缩放是最简单的操作。说白了就是改变图像的尺寸。OpenCV里用 cv2.resize() 搞定。
这里有个坑,我刚开始做的时候踩过。缩放的时候,插值方法选不对,图像质量会打折扣。
常用插值方法对比:
| 方法 | 适用场景 | 特点 |
|---|---|---|
| cv2.INTER_NEAREST | 缩小时快速预览 | 速度快,但锯齿明显 |
| cv2.INTER_LINEAR | 放大/缩小通用 | 默认方法,效果均衡 |
| cv2.INTER_CUBIC | 高质量放大 | 计算量大,但平滑 |
| cv2.INTER_AREA | 缩小图像 | 避免摩尔纹,效果好 |
import cv2
img = cv2.imread('road.jpg')
# 缩小到原来的一半
small = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
# 放大到2倍
big = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
我的经验:做车道线检测时,我习惯先把图像缩小到640x480左右。分辨率太高,计算慢;太低,车道线细节丢失。640x480是个不错的平衡点。
3.1.2 图像旋转
旋转其实也是仿射变换的一种特例。OpenCV里需要先构建一个旋转矩阵,然后用 cv2.warpAffine() 执行。
import cv2
import numpy as np
img = cv2.imread('road.jpg')
h, w = img.shape[:2]
# 获取旋转矩阵:中心点、角度、缩放
M = cv2.getRotationMatrix2D((w//2, h//2), 45, 1.0)
rotated = cv2.warpAffine(img, M, (w, h))
嗯,这里要注意。旋转后图像边缘会被裁剪掉。如果你不想丢失信息,可以调整输出尺寸参数。
3.1.3 仿射变换
仿射变换,说白了就是“拉伸+旋转+平移”的组合。在车道线检测里,最常用的场景是鸟瞰图变换(也叫逆透视变换)。
为什么要做鸟瞰图?因为摄像头拍到的车道线是透视的——近处宽、远处窄。算法不好处理。变成鸟瞰图后,车道线就平行了,检测起来简单很多。
# 定义源点和目标点
src_pts = np.float32([[200, 720], [600, 720], [450, 400], [300, 400]])
dst_pts = np.float32([[300, 720], [500, 720], [500, 0], [300, 0]])
# 计算变换矩阵
M = cv2.getPerspectiveTransform(src_pts, dst_pts)
# 执行变换
warped = cv2.warpPerspective(img, M, (800, 720))
我曾经踩过的坑:源点的选取非常关键。选不好,变换后的车道线还是歪的。我的建议是,先画个图,把车道线的四个角点标出来,再对应到目标矩形上。多试几次,找到最适合你摄像头安装位置的参数。
3.2 图像滤波与去噪:让图像干净点
摄像头拍到的原始图像,或多或少都有噪声。可能是传感器噪点,可能是光照不均,也可能是路面纹理干扰。滤波的目的,就是把这些噪声去掉,同时尽量保留车道线的边缘信息。
3.2.1 均值滤波
均值滤波是最简单的。它用一个窗口在图像上滑动,把窗口内所有像素的平均值作为中心像素的新值。
blurred = cv2.blur(img, (5, 5)) # 5x5的窗口
效果嘛,噪声是去掉了,但图像也变模糊了。边缘信息损失比较严重。我个人不太常用均值滤波做车道线检测,因为车道线本身就是边缘,模糊了反而不好检测。
3.2.2 高斯滤波
高斯滤波比均值滤波聪明一点。它给窗口内的像素分配不同的权重——离中心越近,权重越大。这样既能去噪,又能更好地保留边缘。
gaussian = cv2.GaussianBlur(img, (5, 5), 1.5) # 1.5是标准差
标准差这个参数,我一般设1.0到2.0之间。太大,图像太模糊;太小,去噪效果不够。
为什么高斯滤波更适合车道线检测?
因为车道线的边缘是渐变的,不是突变的。高斯滤波的权重分布更符合自然图像的统计特性。说白了,它去噪的同时,不会把车道线的边缘“抹平”。
3.2.3 中值滤波
中值滤波,顾名思义,取窗口内所有像素的中位数作为新值。它对椒盐噪声(黑白点噪声)效果特别好。
median = cv2.medianBlur(img, 5) # 5是窗口大小
我记得有一次做夜间车道线检测,图像上全是传感器噪点,用高斯滤波效果一般,换成中值滤波后,效果立竿见影。
我的选择策略:
- 白天、光照均匀:高斯滤波,窗口5x5
- 夜间、噪点多:中值滤波,窗口3x3或5x5
- 快速预览:均值滤波,但别用在正式流程里
3.3 边缘检测原理:找到图像里的线条
车道线检测,说白了就是找边缘。图像里亮度变化剧烈的地方,就是边缘。边缘检测算子就是用来捕捉这种变化的。
3.3.1 Sobel算子
Sobel算子通过计算图像在x方向和y方向的梯度,来检测边缘。它用两个3x3的卷积核,一个检测水平边缘,一个检测垂直边缘。
# 计算x方向的梯度
sobelx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
# 计算y方向的梯度
sobely = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
# 合并梯度
sobel = cv2.magnitude(sobelx, sobely)
嗯,这里要注意。Sobel算子的输出是浮点数,需要转成0-255的整数才能显示。我一般用 cv2.convertScaleAbs() 来做转换。
我曾经犯过的错:直接用Sobel检测车道线,结果把路面上的裂缝、阴影都检测出来了。后来我意识到,Sobel对噪声敏感,必须先做高斯滤波。先滤波,再检测,顺序不能乱。
3.3.2 Canny算子
Canny算子是边缘检测的“天花板”。它比Sobel复杂,但效果也好得多。Canny的流程是:
- 高斯滤波去噪
- 计算梯度幅值和方向
- 非极大值抑制(只保留梯度方向上的最大值)
- 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)
edges = cv2.Canny(img, 50, 150) # 低阈值50,高阈值150
双阈值是Canny的精髓。高阈值决定了哪些是“肯定”的边缘,低阈值决定了哪些是“可能”的边缘。只有与强边缘相连的弱边缘才会被保留。
参数调优经验:
- 高阈值:一般是低阈值的2-3倍
- 低阈值:设得太低,噪声多;设得太高,边缘断裂
- 我常用的组合:低阈值30-50,高阈值100-150
为什么Canny在车道线检测里这么受欢迎?因为它能输出单像素宽的边缘。Sobel输出的边缘是“粗”的,而Canny输出的边缘是“细”的。细边缘更容易做后续的直线拟合。
好了,这一节的内容就到这。图像缩放、旋转、仿射变换是预处理的基础;滤波去噪是保证后续算法稳定的关键;边缘检测则是从图像中提取车道线特征的核心手段。这三块内容,在后面的实战章节里会反复用到。
一句话总结:先仿射变换把图像掰正,再滤波去噪把图像弄干净,最后边缘检测把车道线找出来。这个流程,就是车道线检测的“三板斧”。
可可精品资料库