3. 图像处理基础(二):颜色空间转换、滤波与边缘检测

好,咱们接着聊。上一节我们把图像当成了一堆数字矩阵,这一节要动真格的了——让这些数字“说话”。

在自动驾驶里,摄像头拍到的原始画面,说白了就是一堆RGB值。但你想让车看懂车道线,直接拿RGB去算,效果往往很差。为什么?因为RGB受光照影响太大了。同一个白色车道线,晴天和阴天拍出来数值差好几倍。

所以,我们需要先做颜色空间转换,再做滤波去噪,最后用边缘检测把车道线的轮廓抓出来。这三步,是我做车道线项目时每天都要打交道的流程。

3.1 颜色空间转换:BGR转灰度、HSV

OpenCV默认读图是BGR顺序,不是我们熟悉的RGB。这个坑我刚开始踩过好几次,调试时发现颜色不对,查了半天才发现是通道顺序搞反了。

3.1.1 BGR转灰度图

灰度图就是去掉颜色信息,只保留亮度。每个像素用一个0-255的值表示,0是纯黑,255是纯白。

转换公式其实很简单:

Gray = 0.299 * R + 0.587 * G + 0.114 * B

为什么绿色权重最高?因为人眼对绿色最敏感。OpenCV里一行代码搞定:

import cv2

img = cv2.imread('road.jpg')        # BGR格式
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

我个人习惯,在做边缘检测前,一定会先转灰度。因为Canny算子只认单通道,你给它三通道它反而会懵。

小技巧: 如果你发现车道线在灰度图里对比度不够,可以试试先做直方图均衡化,能有效提升对比度。

3.1.2 BGR转HSV

HSV比RGB更符合人类对颜色的感知。H是色调,S是饱和度,V是明度。

举个例子:你要提取黄色车道线。在RGB空间里,黄色可能是(255,255,0),但光照一变,数值就飘了。而在HSV空间里,黄色的H值稳定在30-60之间,你只要设定这个范围,就能稳定提取。

我在项目中遇到过这样的情况:白天和黄昏的黄色车道线,在RGB下差异很大,但转到HSV后,H通道几乎不变。这就是HSV的威力。

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 提取黄色区域
lower_yellow = np.array([20, 100, 100])
upper_yellow = np.array([30, 255, 255])
mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
注意: OpenCV中HSV的H范围是0-180,不是0-360。因为OpenCV用uint8存,0-360放不下,所以折半了。这个细节我当年调试了半小时才反应过来。

3.2 图像滤波:高斯滤波与中值滤波

摄像头拍到的图像,多少都有噪声。椒盐噪声、高斯噪声……这些噪声如果不处理,边缘检测会检测出一堆假边缘,车道线反而看不清。

滤波,说白了就是“模糊”一下,把噪声抹平。

3.2.1 高斯滤波

高斯滤波用的是一个高斯核,中心权重最大,越往边缘权重越小。这样模糊出来的图像,既去除了噪声,又保留了边缘的轮廓。

为什么用高斯核?因为图像中的噪声往往服从高斯分布,用高斯核去卷积,正好“门当户对”。

blurred = cv2.GaussianBlur(gray, (5, 5), 1.5)

这里(5,5)是核大小,必须是奇数。1.5是标准差σ。σ越大,图像越模糊。

我建议你从(5,5)开始试,σ设1.0-1.5。核太大,车道线边缘也会被模糊掉,得不偿失。

核心经验: 高斯滤波是边缘检测前的“标准动作”。我几乎没见过哪个车道线项目跳过这一步。

3.2.2 中值滤波

中值滤波对付椒盐噪声特别有效。它的原理很简单:把核内所有像素排序,取中间值替代中心像素。

举个例子,一个3x3区域里有几个白点(椒盐噪声),排序后中间值大概率是正常像素值,噪声就被干掉了。

median = cv2.medianBlur(gray, 5)

中值滤波的核大小也必须是奇数。5x5是比较常用的。

我曾经在一个夜间项目中,图像里全是摄像头传感器产生的椒盐噪声。高斯滤波效果一般,换成中值滤波后,车道线瞬间清晰了。嗯,这里要注意:中值滤波计算量比高斯滤波大,实时性要求高的场景要权衡。

滤波方法 适用场景 优点 缺点
高斯滤波 高斯噪声、通用去噪 平滑效果好,保留边缘 对椒盐噪声效果一般
中值滤波 椒盐噪声、脉冲噪声 去噪彻底,边缘保持好 计算量大,实时性略差

3.3 边缘检测:Canny算子

终于到重头戏了。Canny边缘检测,可以说是计算机视觉里最经典的算法之一。它为什么经典?因为它把边缘检测这件事,做得非常“讲究”。

Canny算法分四步走:

  1. 高斯滤波去噪——先模糊,再找边
  2. 计算梯度幅值和方向——用Sobel算子算x和y方向的梯度
  3. 非极大值抑制——只保留梯度方向上的局部最大值,把“胖”边缘变“瘦”
  4. 双阈值检测——高阈值确定强边缘,低阈值连接弱边缘

第四步是关键。高阈值设高了,边缘会断;设低了,噪声会混进来。我一般设高阈值100-150,低阈值50-75。具体数值要看你的图像对比度。

edges = cv2.Canny(blurred, 50, 150)

你想想看,为什么Canny要搞两个阈值?因为真实世界的边缘,有的很清晰(高梯度),有的很模糊(低梯度)。双阈值让算法既能抓住强边缘,又能通过连通性把弱边缘也捡回来。

避坑指南: 我曾经在一个项目中,Canny检测出的车道线总是断断续续。后来发现是高斯滤波的σ设太大了,把车道线边缘也模糊掉了。把σ从2.0降到1.2,效果立竿见影。

最后,给你一个完整的处理流程示例:

import cv2
import numpy as np

def preprocess_lane_image(img):
    # 1. 转灰度
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 2. 高斯滤波去噪
    blurred = cv2.GaussianBlur(gray, (5, 5), 1.2)
    
    # 3. Canny边缘检测
    edges = cv2.Canny(blurred, 50, 150)
    
    return edges

这个流程,我用了不下上百次。它简单、稳定、高效。当然,实际项目中你还需要做透视变换、车道线拟合等后续处理,但图像预处理这一步,打好基础比什么都重要。

下一节,我们会聊透视变换和ROI提取,把车道线从图像中“抠”出来。到时候你会发现,前面这些滤波和边缘检测的功夫,一点都没白费。