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 提取,最终得到可以送入推理引擎的数据。

图像采集与预处理流程 图像采集与预处理流程 相机原始数据 格式转换 Bayer → BGR / 灰度 去噪滤波 高斯 / 中值 直方图均衡化 全局 / CLAHE ROI提取 裁剪感兴趣区域 关键原则 • 能用灰度不用彩色 • 去噪后再做增强 • ROI 留余量防偏移 常见陷阱 • 中值滤波核过大 • 全局均衡放大噪声

4.6 预处理流水线的工程建议

在实际项目中,预处理不是孤立操作的。它们需要组合成一个流水线。我一般这样组织:

  1. 采集:从相机获取 Bayer 或 BGR 数据。
  2. 格式转换:如果需要灰度,尽早转。减少后续计算量。
  3. 去噪:先做中值或高斯滤波。噪声类型决定选哪个。
  4. 增强:用 CLAHE 做对比度拉伸。注意 clipLimit 不要太大。
  5. ROI 提取:最后裁剪。这样前面几步只处理全图,但 ROI 提取后,后续算法只处理小区域。

性能警告: 每一步都有计算开销。在实时系统中,能用 OpenCV 的 C++ 接口就别用 Python 循环。我见过有人用 Python for 循环做中值滤波,1080p 图像一帧要 2 秒——这显然不行。