3. 图像处理基础(下):图像缩放、旋转、仿射变换。图像滤波与去噪(均值滤波、高斯滤波、中值滤波)。边缘检测原理(Sobel, Canny算子)

好,咱们接着聊。上一节我们把图像的像素、色彩空间和直方图这些基础概念过了一遍。这一节,我们要动真格的了——开始对图像“动手”。

说白了,车道线检测的整个流程,就是一系列图像操作的组合。你想想看,摄像头拍到的原始画面,直接扔给算法,那肯定不行。得先把它处理成算法喜欢的样子。

我个人习惯把这一节的内容分成三大块:几何变换(让图像动起来)、滤波去噪(让图像干净点)、边缘检测(找到图像里的线条)。这三板斧,是车道线检测的基石。

图像处理基础(下) 几何变换 缩放 · 旋转 · 仿射变换 滤波与去噪 均值 · 高斯 · 中值滤波 边缘检测 Sobel · Canny算子 图像缩放 图像旋转 仿射变换 均值滤波 高斯滤波 中值滤波 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的流程是:

  1. 高斯滤波去噪
  2. 计算梯度幅值和方向
  3. 非极大值抑制(只保留梯度方向上的最大值)
  4. 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)
edges = cv2.Canny(img, 50, 150)  # 低阈值50,高阈值150

双阈值是Canny的精髓。高阈值决定了哪些是“肯定”的边缘,低阈值决定了哪些是“可能”的边缘。只有与强边缘相连的弱边缘才会被保留。

参数调优经验:

  • 高阈值:一般是低阈值的2-3倍
  • 低阈值:设得太低,噪声多;设得太高,边缘断裂
  • 我常用的组合:低阈值30-50,高阈值100-150

为什么Canny在车道线检测里这么受欢迎?因为它能输出单像素宽的边缘。Sobel输出的边缘是“粗”的,而Canny输出的边缘是“细”的。细边缘更容易做后续的直线拟合。

好了,这一节的内容就到这。图像缩放、旋转、仿射变换是预处理的基础;滤波去噪是保证后续算法稳定的关键;边缘检测则是从图像中提取车道线特征的核心手段。这三块内容,在后面的实战章节里会反复用到。

一句话总结:先仿射变换把图像掰正,再滤波去噪把图像弄干净,最后边缘检测把车道线找出来。这个流程,就是车道线检测的“三板斧”。

可可精品资料库