3. 图像采集与预处理
好,咱们正式开始第三章。前面两章我们把视觉定位的总体架构和坐标系捋了一遍,现在该碰真东西了——图像数据是怎么来的,又该怎么把它“收拾”成算法能用的样子。
图像采集与预处理,说白了就是视觉系统的“前端”。这一步做不好,后面再牛的算法也白搭。我在产线上见过太多案例,折腾半天定位不准,最后发现是图像噪声没滤干净。嗯,咱们一步步来。
3.1 相机SDK调用——别小看这一步
很多人觉得SDK调用就是装个驱动、调个API,没什么技术含量。我一开始也这么想,直到有一次在产线上被坑惨了。
那次用的是某品牌工业相机,SDK文档写得稀里糊涂。我按示例代码初始化,结果相机死活连不上。折腾了两小时,最后发现是相机IP和主机不在同一网段。你说冤不冤?
所以,调用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(注意:中值滤波核必须是奇数)
那什么时候用高斯,什么时候用中值?我个人的经验是:
- 产线光照稳定、传感器质量好 → 高斯滤波就够了,速度快。
- 环境有电磁干扰、或者相机有坏点 → 中值滤波更靠谱。
- 实在拿不准 → 两个都试一下,看效果选。
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(分块大小)
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;如果图像本身很干净,中值滤波那步可以省掉,省点时间。