4. OpenCV 图像处理基础:图像读取与显示、色彩空间转换、图像滤波与边缘检测、形态学操作

图像处理是嵌入式视觉的起点。说白了,摄像头采集到的原始数据,不能直接扔给神经网络。你得先把它“收拾”干净,转换成模型能理解的样子。

我个人习惯把 OpenCV 比作一把瑞士军刀。功能多,但真正常用的就那么几招。今天咱们就把这几招练熟。

本章核心脉络: 从硬盘/摄像头读图 → 显示确认 → 色彩空间转换 → 滤波去噪 → 边缘检测 → 形态学后处理。每一步都是下一步的输入。
OpenCV 图像处理基础流程 图像读取 图像显示 色彩空间转换 图像滤波 边缘检测 形态学操作 💡 嵌入式优化要点:优先使用灰度图处理,减少计算量;滤波核大小不超过 5×5 形态学操作常用于边缘检测后的断点连接与噪点清除

4.1 图像读取与显示:第一行代码

先来个最基础的。读取一张图片,显示出来,然后等待按键退出。这段代码我几乎每天都会敲一遍。

#include <opencv2/opencv.hpp>
using namespace cv;

int main() {
    // 读取图像,默认是彩色模式
    Mat img = imread("test.jpg");
    if (img.empty()) {
        printf("图片没找到,检查路径\n");
        return -1;
    }

    // 显示
    imshow("Display Window", img);
    waitKey(0);          // 等待按键
    destroyAllWindows(); // 关闭所有窗口
    return 0;
}
我的习惯: 在嵌入式设备上,我一般用 namedWindow("win", WINDOW_NORMAL) 创建窗口,这样窗口可以缩放,适应小屏幕。

imread() 的第二个参数可以控制读取模式。常用的有:

标志值说明
IMREAD_COLOR1彩色,默认。3 通道 BGR
IMREAD_GRAYSCALE0灰度图,1 通道。省内存,处理快
IMREAD_UNCHANGED-1包含 Alpha 通道,原样读取

你想想看,在边缘设备上,内存和算力都有限。如果后续只做边缘检测,直接读成灰度图,能省掉一次色彩转换的开销。

4.2 色彩空间转换:BGR、灰度、HSV

OpenCV 默认是 BGR 顺序,不是 RGB。这个坑我刚开始也踩过。显示出来颜色不对,排查了半天才发现是通道顺序搞反了。

常用的转换函数是 cvtColor()。

Mat gray, hsv;
cvtColor(img, gray, COLOR_BGR2GRAY);   // 转灰度
cvtColor(img, hsv,  COLOR_BGR2HSV);    // 转 HSV

为什么用 HSV?举个例子。你要在画面中找出红色物体。在 BGR 空间里,红色受光照影响很大,阈值很难调。但在 HSV 空间,H(色调)通道基本不受光照影响,你只需要设定 H 的范围就行。

HSV 各通道范围(OpenCV 中):
H: 0–179  |  S: 0–255  |  V: 0–255

我在做交通灯识别项目时,就是靠 HSV 空间来分割红绿灯区域的。效果比 BGR 好太多。

4.3 图像滤波:去噪是预处理的关键

摄像头采集的图像,多少都有噪声。椒盐噪声、高斯噪声……不处理的话,边缘检测会出来一堆假边缘。

三种常用滤波:

滤波类型函数特点适用场景
均值滤波blur()简单平均,速度快噪声较轻,对实时性要求高
高斯滤波GaussianBlur()加权平均,保留边缘更好高斯噪声,通用场景
中值滤波medianBlur()取中位数,抗椒盐噪声强椒盐噪声明显时
Mat blurred;
blur(img, blurred, Size(3, 3));              // 均值
GaussianBlur(img, blurred, Size(3, 3), 0);   // 高斯
medianBlur(img, blurred, 3);                 // 中值
注意: 滤波核越大,图像越模糊,计算量也越大。在嵌入式平台上,我一般用 3×3 或 5×5,再大就扛不住了。

4.4 边缘检测:Canny 算子

边缘检测里,Canny 是公认的“黄金标准”。它分四步走:去噪 → 计算梯度 → 非极大值抑制 → 双阈值筛选。

Mat edges;
Canny(blurred, edges, 50, 150);  // 低阈值50,高阈值150

双阈值怎么调?我有个经验:高阈值设成低阈值的 2–3 倍。低阈值控制边缘的敏感度,太高会漏掉边缘,太低会引入噪声。

举个例子。我在做车道线检测时,低阈值设 40,高阈值设 120。效果不错。但换了一个光照环境,就得重新调。所以后来我写了个滑动条,动态调参,方便多了。

调试技巧: 用 createTrackbar() 创建滑动条,实时调整 Canny 阈值。这样不用每次改代码重新编译。

4.5 形态学操作:膨胀、腐蚀、开闭运算

边缘检测完,得到的二值图往往有断点、有毛刺。形态学操作就是用来“修图”的。

操作函数效果
腐蚀erode()消除小白点,边缘向内收缩
膨胀dilate()连接断点,边缘向外扩张
开运算morphologyEx(..., MORPH_OPEN)先腐蚀后膨胀,去噪点
闭运算morphologyEx(..., MORPH_CLOSE)先膨胀后腐蚀,填小孔
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
Mat result;

erode(edges, result, kernel);        // 腐蚀
dilate(edges, result, kernel);       // 膨胀
morphologyEx(edges, result, MORPH_OPEN, kernel);  // 开运算
morphologyEx(edges, result, MORPH_CLOSE, kernel); // 闭运算

我曾经做过一个零件尺寸测量项目。边缘检测后,零件轮廓有断点,测量结果偏差很大。后来用了一次闭运算,轮廓连上了,测量精度直接提升到 0.1 毫米以内。

核的形状选择:
MORPH_RECT(矩形)—— 通用
MORPH_ELLIPSE(椭圆)—— 更平滑
MORPH_CROSS(十字)—— 保留角点

嗯,到这里,OpenCV 图像处理的基础链条就串起来了。从读图到显示,从色彩转换到滤波,再到边缘检测和形态学修图。每一步都是为下一步服务的。在嵌入式 Linux 上跑这些代码,只要注意内存和计算量,基本不会出大问题。