3. 图像采集与预处理

好,咱们正式开始第三章。前面两章我们把视觉定位的总体架构和坐标系捋了一遍,现在该碰真东西了——图像数据是怎么来的,又该怎么把它“收拾”成算法能用的样子。

图像采集与预处理,说白了就是视觉系统的“前端”。这一步做不好,后面再牛的算法也白搭。我在产线上见过太多案例,折腾半天定位不准,最后发现是图像噪声没滤干净。嗯,咱们一步步来。

本章核心脉络: 相机SDK调用 → 图像格式转换 → 滤波去噪 → 直方图均衡化 → 输出可用图像
图像采集与预处理流程 相机SDK调用 图像格式转换 滤波去噪 直方图均衡化 初始化 → 打开设备 → 开始采集 设置参数(曝光/增益/帧率) BGR ↔ RGB ↔ 灰度 数据类型转换(uint8/float) 高斯滤波(去高斯噪声) 中值滤波(去椒盐噪声) 全局直方图均衡化 CLAHE(自适应) 输出:预处理后的可用图像

3.1 相机SDK调用——别小看这一步

很多人觉得SDK调用就是装个驱动、调个API,没什么技术含量。我一开始也这么想,直到有一次在产线上被坑惨了。

那次用的是某品牌工业相机,SDK文档写得稀里糊涂。我按示例代码初始化,结果相机死活连不上。折腾了两小时,最后发现是相机IP和主机不在同一网段。你说冤不冤?

所以,调用SDK时我建议你重点关注这几个点:

  • 初始化流程:枚举设备 → 选择设备 → 打开设备 → 开始采集。顺序别搞反。
  • 参数设置:曝光时间、增益、帧率、触发模式。这些参数直接影响图像质量。
  • 回调机制:用回调函数接收图像帧,别在主循环里轮询,效率差太多。
  • 资源释放:采集完一定要关闭设备、释放内存。我见过有人忘了这步,内存泄漏到系统崩溃。
我的习惯: 写一个相机封装类,把SDK调用包一层。这样换相机品牌时,只需要改底层实现,上层算法代码不用动。省心。

3.2 图像格式转换——BGR/RGB/灰度

相机采集到的原始数据,通常是BGR格式(OpenCV默认)。但很多算法库用的是RGB,比如深度学习框架。你想想看,如果不做转换,颜色通道对不上,结果能对吗?

格式转换其实就几行代码,但有个细节要注意——数据类型。相机输出一般是uint8(0-255),但有些算法需要float(0.0-1.0)。转换时别忘了归一化。

// C++示例:BGR转灰度 + 数据类型转换
cv::Mat frame_bgr;  // 原始BGR图像,uint8
cv::Mat frame_gray; // 灰度图

// BGR转灰度
cv::cvtColor(frame_bgr, frame_gray, cv::COLOR_BGR2GRAY);

// 如果需要float类型,做归一化
cv::Mat frame_float;
frame_gray.convertTo(frame_float, CV_32F, 1.0/255.0);
// 现在frame_float的范围是0.0-1.0

我曾经在一个项目中,直接用uint8数据喂给深度学习模型,结果模型输出全是NaN。查了半天才发现是数据类型不匹配。嗯,这种坑踩过一次就记住了。

转换类型 OpenCV标志 典型应用场景
BGR → RGB COLOR_BGR2RGB 深度学习模型输入(PyTorch/TensorFlow)
BGR → 灰度 COLOR_BGR2GRAY 特征提取、模板匹配、边缘检测
BGR → HSV COLOR_BGR2HSV 颜色分割、目标跟踪
uint8 → float convertTo() 归一化后输入算法

3.3 滤波去噪——高斯 vs 中值

图像噪声是视觉系统的天敌。产线上常见的噪声有两种:高斯噪声(传感器热噪声、光照波动)和椒盐噪声(传输干扰、像素坏点)。对付它们,得用不同的滤波器。

高斯滤波

高斯滤波用高斯核做卷积,对高斯噪声效果很好。核心参数是核大小和标准差σ。核越大,图像越模糊,但去噪也越彻底。我一般从5×5开始试,效果不够就加大。

// 高斯滤波
cv::Mat denoised_gaussian;
cv::GaussianBlur(frame_gray, denoised_gaussian, cv::Size(5, 5), 1.0);
// 核大小5×5,标准差1.0

中值滤波

中值滤波用邻域像素的中值代替中心像素,对椒盐噪声有奇效。你想想看,一个孤立的噪点,在中值计算时会被周围正常像素“淹没”。

// 中值滤波
cv::Mat denoised_median;
cv::medianBlur(frame_gray, denoised_median, 5);
// 核大小5×5(注意:中值滤波核必须是奇数)
避坑指南: 中值滤波的核大小不能是偶数!我见过有人写medianBlur(img, dst, 4),程序直接崩了。另外,中值滤波计算量大,实时性要求高的场景慎用。

那什么时候用高斯,什么时候用中值?我个人的经验是:

  • 产线光照稳定、传感器质量好 → 高斯滤波就够了,速度快。
  • 环境有电磁干扰、或者相机有坏点 → 中值滤波更靠谱。
  • 实在拿不准 → 两个都试一下,看效果选。

3.4 直方图均衡化——让图像“显形”

有时候图像整体偏暗或偏亮,细节都藏在阴影里。这时候直方图均衡化就派上用场了。它通过拉伸像素值的分布,让图像对比度更高、细节更清晰。

OpenCV提供了两种方式:

  • 全局直方图均衡化(equalizeHist):简单粗暴,但容易放大噪声。
  • CLAHE(自适应直方图均衡化):分块处理,效果更自然,噪声控制更好。
// 全局直方图均衡化
cv::Mat equalized_global;
cv::equalizeHist(frame_gray, equalized_global);

// CLAHE(自适应)
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8, 8));
cv::Mat equalized_clahe;
clahe->apply(frame_gray, equalized_clahe);
// 参数:clipLimit=2.0(对比度限制),tileGridSize=8×8(分块大小)
我的经验: 在产线视觉定位中,我几乎只用CLAHE。全局均衡化容易把背景噪声也放大,导致特征点误匹配。CLAHE的clipLimit参数我一般设在2.0-3.0之间,太大反而会引入伪影。

3.5 完整预处理流水线

好了,把上面这些串起来,就是一个完整的预处理流程。我一般这样写:

// 完整预处理流水线
cv::Mat preprocessImage(const cv::Mat& src) {
    cv::Mat dst;
    
    // 1. 格式转换:BGR → 灰度
    cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY);
    
    // 2. 滤波去噪:先用中值去椒盐噪声,再用高斯平滑
    cv::medianBlur(dst, dst, 5);
    cv::GaussianBlur(dst, dst, cv::Size(3, 3), 0.8);
    
    // 3. 直方图均衡化:CLAHE
    cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.5, cv::Size(8, 8));
    clahe->apply(dst, dst);
    
    return dst;
}

这个流水线我用了好几年,在多个产线项目上验证过。当然,具体参数要根据你的相机和场景微调。比如光照特别差的场景,我会把CLAHE的clipLimit调到3.5;如果图像本身很干净,中值滤波那步可以省掉,省点时间。