4. 图像采集与预处理:从传感器到可用数据
图像采集,说白了就是相机把光信号变成数字信号的过程。但相机给到我们的原始数据,往往不能直接喂给算法。为什么?因为传感器有噪声、光照不均匀、格式也不对。我做了这么多年视觉工程,踩过最多的坑,反而是在预处理阶段。
这一章,我们就来拆解几个最常用的预处理操作。你不需要背公式,但要知道每个操作在什么场景下用、用错了会有什么后果。
4.1 图像格式:BGR、灰度与Bayer
先搞清楚相机输出的数据长什么样。不同格式,处理方式完全不同。
| 格式 | 通道数 | 数据量(以1080p为例) | 典型用途 |
|---|---|---|---|
| BGR | 3(蓝、绿、红) | 约6.2 MB | 彩色图像、缺陷检测(颜色相关) |
| 灰度 | 1 | 约2.1 MB | 边缘检测、模板匹配、OCR |
| Bayer | 1(马赛克排列) | 约2.1 MB | 相机原始输出,需去马赛克 |
BGR 是 OpenCV 的默认格式。注意,它不是 RGB。我第一次用 OpenCV 显示图像时,发现颜色不对,查了半天才发现是 BGR 和 RGB 搞反了。嗯,这个坑几乎每个新手都会踩一次。
灰度图 只有亮度信息。很多算法其实不需要颜色,比如边缘检测、二值化。用灰度图可以减少 2/3 的数据量,推理速度能快不少。
Bayer 格式 是 CMOS 传感器的原始输出。每个像素只记录一种颜色(R、G 或 B),排列成马赛克。必须经过 去马赛克(demosaicing) 才能变成彩色图。我建议,如果你对颜色精度要求高,尽量在相机端就完成去马赛克,不要在 CPU 上做,太慢了。
核心原则: 能用灰度,就不用彩色。能提前转换,就不要在推理时转换。
4.2 去噪滤波:高斯 vs 中值
噪声是工业视觉的天敌。光照不足、传感器发热、传输干扰,都会引入噪声。去噪滤波,就是把这些噪声干掉,同时尽量保留边缘。
我常用的两种滤波:高斯滤波 和 中值滤波。它们各有各的脾气。
4.2.1 高斯滤波
高斯滤波是线性滤波。它用邻域像素的加权平均来替代中心像素。权重由高斯分布决定,离中心越近,权重越大。
适合处理 高斯噪声(比如传感器热噪声)。但缺点也很明显——它会模糊边缘。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('defect.jpg', cv2.IMREAD_GRAYSCALE)
# 高斯滤波,核大小 5x5,sigma=1.5
blurred = cv2.GaussianBlur(img, (5, 5), 1.5)
# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Gaussian Blur', blurred)
cv2.waitKey(0)
我的习惯: 核大小选奇数,3x3 或 5x5 就够了。sigma 一般设 1.0~2.0。太大,图像会糊成一团。
4.2.2 中值滤波
中值滤波是非线性的。它取邻域内所有像素的中位数,替代中心像素。对 椒盐噪声(黑白点噪声)效果极好。
而且,它比高斯滤波更能保留边缘。我曾在一次 PCB 焊点检测中,用高斯滤波把焊点边缘给模糊了,导致误判。换成中值滤波,问题就解决了。
# 中值滤波,核大小 5
median = cv2.medianBlur(img, 5)
cv2.imshow('Median Blur', median)
cv2.waitKey(0)
我曾经踩过的坑: 中值滤波的核大小不能太大。超过 7x7,图像细节会丢失严重。而且,它比高斯滤波慢,尤其是在大核下。实时系统里慎用 9x9 以上。
4.3 直方图均衡化:让暗部亮起来
工业现场光照往往不均匀。有的地方过曝,有的地方太暗。直方图均衡化,就是拉伸图像的对比度,让暗部细节显现出来。
原理很简单:把像素值的分布,从集中在某个区间,拉伸到整个 0-255 范围。
# 全局直方图均衡化
equ = cv2.equalizeHist(img)
cv2.imshow('Equalized', equ)
cv2.waitKey(0)
但全局均衡化有个问题——它会放大噪声。尤其是暗部区域的噪声,会被一起拉伸。我建议在均衡化之前,先做一次轻度的去噪。
还有一种改进方法叫 CLAHE(对比度受限自适应直方图均衡化)。它把图像分成小块,每块单独做均衡化,同时限制对比度放大倍数。效果比全局均衡好很多。
# CLAHE
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
clahe_img = clahe.apply(img)
cv2.imshow('CLAHE', clahe_img)
cv2.waitKey(0)
经验之谈: 在金属表面缺陷检测中,我常用 CLAHE。它能同时保留高光区和暗区的细节,比全局均衡化稳定得多。
4.4 ROI提取:只处理感兴趣的区域
整张图像 1920x1080,但缺陷可能只占 100x100 的区域。全图处理,浪费算力,也容易引入干扰。
ROI(Region of Interest)提取,就是只裁剪出你关心的区域。这是实时推理中最有效的加速手段之一。
# 假设我们已知缺陷区域在 (x, y, w, h) = (200, 150, 100, 80)
x, y, w, h = 200, 150, 100, 80
roi = img[y:y+h, x:x+w]
# 在 ROI 上做检测
# 比如边缘检测
edges = cv2.Canny(roi, 50, 150)
cv2.imshow('ROI', roi)
cv2.imshow('Edges in ROI', edges)
cv2.waitKey(0)
ROI 提取的关键是 定位准确。如果 ROI 偏移了,检测结果就全错了。我一般会在产线调试阶段,先用标定板确定 ROI 的精确位置,然后固化到配置文件中。
我的建议: ROI 不要设得太紧。留 10-20 像素的余量,防止因机械振动导致 ROI 偏移。
4.5 本章知识结构图
下面这张图,把图像采集与预处理的流程串起来了。从相机原始数据开始,经过格式转换、去噪、增强、ROI 提取,最终得到可以送入推理引擎的数据。
4.6 预处理流水线的工程建议
在实际项目中,预处理不是孤立操作的。它们需要组合成一个流水线。我一般这样组织:
- 采集:从相机获取 Bayer 或 BGR 数据。
- 格式转换:如果需要灰度,尽早转。减少后续计算量。
- 去噪:先做中值或高斯滤波。噪声类型决定选哪个。
- 增强:用 CLAHE 做对比度拉伸。注意 clipLimit 不要太大。
- ROI 提取:最后裁剪。这样前面几步只处理全图,但 ROI 提取后,后续算法只处理小区域。
性能警告: 每一步都有计算开销。在实时系统中,能用 OpenCV 的 C++ 接口就别用 Python 循环。我见过有人用 Python for 循环做中值滤波,1080p 图像一帧要 2 秒——这显然不行。