本文系统梳理 OpenCV C++ 课程的核心知识点,涵盖色彩空间转换、Mat 对象、像素操作、归一化、图像缩放与插值、翻转、视频读写、卷积、高斯模糊及人脸检测。所有代码均来自实际项目
QuickDemo类,已在 Visual Studio + OpenCV 4.4+ 环境下验证通过,并附常见编译与运行错误解决方案。
一、课程内容筛选(按机器人视觉需求)
根据机器人 + YOLO 的目标,将 30 讲分为三类:
| 类型 | 章节 | 说明 |
|---|---|---|
| 精学 | 图像读写、色彩空间转换、Mat 对象、归一化、图像缩放、摄像头读取、卷积 | 高频操作,必须熟练掌握 |
| 过一遍 | 像素遍历、通道分离、像素统计、翻转、旋转、视频保存、高斯模糊、人脸检测 | 理解流程,用时能查 |
| 跳过 | 滚动条、键盘鼠标交互、绘图、直方图等 | 与机器人感知关系不大 |
二、图像窗口与显示基础
namedWindow("win", WINDOW_FREERATIO)允许自由调整窗口比例。waitKey(0)无限等待按键;waitKey(30)等待 30 毫秒,常用于视频循环。- 图像深度:24 位(3 通道 × 8 位/通道),像素值范围 0~255。
cpp
// 对应 QuickDemo 中 colorSpace_Demo 等函数的开头
Mat src = imread("D:/test.jpg", IMREAD_COLOR);
if (src.empty()) return -1;
namedWindow("输入", WINDOW_FREERATIO);
imshow("输入", src);
waitKey(0);
destroyAllWindows();
三、Mat 对象创建与赋值
Mat包含头部(尺寸、类型、通道)、数据指针、引用计数。- 深拷贝:
clone()或copyTo(),独立内存。 - 浅拷贝:直接赋值,共享数据。
cpp
// 来自 QuickDemo::mat_creation_demo
Mat m3 = Mat::zeros(Size(8, 8), CV_8UC3);
m3 = Scalar(0, 0, 255); // 红色
cout << "width: " << m3.cols << " height: " << m3.rows << " channels: " << m3.channels() << endl;
Mat m4;
m3.copyTo(m4);
m4 = Scalar(0, 255, 255); // 黄色,不影响 m3
imshow("图像", m3);
imshow("图像4", m4);
四、像素读写操作
- 灰度图:
image.at<uchar>(row, col) - 彩色图:
image.at<Vec3b>(row, col)[0](0=B,1=G,2=R) - 指针遍历效率更高(参考
pixel_visit_demo):
cpp
// 来自 QuickDemo::pixel_visit_demo(指针版本)
int w = image.cols, h = image.rows, dims = image.channels();
for (int row = 0; row < h; row++) {
uchar* current_row = image.ptr<uchar>(row);
for (int col = 0; col < w; col++) {
if (dims == 1) { // 灰度图
int pv = *current_row;
*current_row++ = 255 - pv;
}
if (dims == 3) { // 彩色图
*current_row++ = 255 - *current_row;
*current_row++ = 255 - *current_row;
*current_row++ = 255 - *current_row;
}
}
}
imshow("像素读写演示", image);
五、通道分离与合并
cpp
// 来自 QuickDemo::channels_demo
vector<Mat> mv;
split(image, mv);
imshow("蓝色", mv[0]);
imshow("绿色", mv[1]);
imshow("红色", mv[2]);
Mat dst;
mv[0] = 0;
// mv[1] = 0;
merge(mv, dst);
imshow("蓝色通道置零", dst);
int from_to[] = { 0, 2, 1, 1, 2, 0 };
mixChannels(&image, 1, &dst, 1, from_to, 3);
imshow("通道重映射", dst);
六、色彩空间转换(重点,位于归一化之前)
转换函数 cvtColor(src, dst, code),常用常量值:
| 常量 | 数值 | 说明 |
|---|---|---|
COLOR_BGR2GRAY |
6 | 彩色 → 灰度 |
COLOR_GRAY2BGR |
8 | 灰度 → 伪彩色(无法恢复真彩色) |
COLOR_BGR2HSV |
40 | BGR → HSV |
COLOR_HSV2BGR |
54 | HSV → BGR |
COLOR_BGR2RGB |
4 | 通道交换 |
HSV 范围(OpenCV 特有)
- H: 0 ~ 180(色调)
- S: 0 ~ 255(饱和度)
- V: 0 ~ 255(明度)
HSV 空间在颜色分割时更稳定,不受光照强度影响。
cpp
// 来自 QuickDemo::colorSpace_Demo
Mat gray, hsv;
cvtColor(image, hsv, COLOR_BGR2HSV);
cvtColor(image, gray, COLOR_BGR2GRAY);
imshow("HSV", hsv);
imshow("灰度", gray);
imwrite("D:/hsv.png", hsv);
imwrite("D:/gray.png", gray);
常见错误 :混淆 BGR 与 RGB 导致颜色异常,可用 cvtColor(src, dst, COLOR_BGR2RGB) 修正;HSV 中 H 值误用 0~360 导致分割失败。
6.1 提取指定色彩范围(inRange)与背景替换
cpp
// 来自 QuickDemo::inrange_demo
Mat hsv;
cvtColor(image, hsv, COLOR_BGR2HSV);
Mat mask;
inRange(hsv, Scalar(35, 43, 46), Scalar(77, 255, 255), mask);
Mat redback = Mat::zeros(image.size(), image.type());
redback = Scalar(40, 40, 200);
bitwise_not(mask, mask);
imshow("mask", mask);
image.copyTo(redback, mask);
imshow("roi区域提取", redback);
七、图像归一化(深度学习前处理关键)
重要:归一化必须在色彩空间转换之后进行,因为神经网络通常要求输入为 0,1 或 -1,1 范围的浮点数。
目的:将像素值从 0,255 映射到 0,1 或 -1,1。
cpp
// 来自 QuickDemo::norm_demo
Mat dst;
cout << image.type() << endl; // 16 (CV_8UC3)
image.convertTo(image, CV_32F); // 改为浮点
cout << image.type() << endl; // 21 (CV_32FC3)
normalize(image, dst, 1.0, 0, NORM_MINMAX);
cout << dst.type() << endl; // 21
imshow("图像数据归一化", dst);
注意 :convertTo 只改变数据类型,不改变数值范围,必须再除以 255 或使用 normalize。
八、图像放缩与插值
cpp
// 来自 QuickDemo::resize_demo
Mat zoomin, zoomout;
int h = image.rows, w = image.cols;
resize(image, zoomin, Size(w/2, h/2), 0, 0, INTER_LINEAR);
imshow("zoomin", zoomin);
resize(image, zoomout, Size(w*1.5, h*1.5), 0, 0, INTER_LINEAR);
imshow("zoomout", zoomout);
插值算法(由快到慢、由低到高质量):
INTER_NEAREST:最近邻,速度快,有锯齿INTER_LINEAR:双线性(默认),质量与速度平衡INTER_CUBIC:双三次,较慢,质量更高INTER_LANCZOS4:Lanczos 插值,质量最高但最慢
缩小图像时推荐 INTER_AREA。
九、图像翻转与旋转
翻转:
cpp
// 来自 QuickDemo::flip_demo
Mat dst;
// flip(image, dst, 0); // 上下翻转
// flip(image, dst, 1); // 左右翻转
flip(image, dst, -1); // 180°旋转
imshow("图像翻转", dst);
旋转(仿射变换):
cpp
// 来自 QuickDemo::rotate_demo
Mat dst, M;
int w = image.cols, h = image.rows;
M = getRotationMatrix2D(Point2f(w/2, h/2), 45, 1.0);
double cos = abs(M.at<double>(0,0));
double sin = abs(M.at<double>(0,1));
int nw = cos*w + sin*h;
int nh = sin*w + cos*h;
M.at<double>(0,2) += (nw/2 - w/2);
M.at<double>(1,2) += (nh/2 - h/2);
warpAffine(image, dst, M, Size(nw, nh), INTER_LINEAR, 0, Scalar(255,255,0));
imshow("旋转演示", dst);
十、摄像头与视频读取
cpp
// 来自 QuickDemo::video_demo(读取部分)
VideoCapture capture("D:/images/video/example_dsh.mp4"); // 也可用 0 打开摄像头
int frame_width = capture.get(CAP_PROP_FRAME_WIDTH);
int frame_height = capture.get(CAP_PROP_FRAME_HEIGHT);
double fps = capture.get(CAP_PROP_FPS);
Mat frame;
while (true) {
capture.read(frame);
if (frame.empty()) break;
imshow("frame", frame);
if (waitKey(1) == 27) break; // ESC 退出
}
capture.release();
十一、视频保存
cpp
// 来自 QuickDemo::video_demo(保存部分)
VideoWriter writer("D:/test.mp4", capture.get(CAP_PROP_FOURCC), fps,
Size(frame_width, frame_height), true);
while (capture.read(frame)) {
writer.write(frame);
}
writer.release();
常用 fourcc:'M','J','P','G'(MJPG)、'X','V','I','D'(XVID)。
十二、图像卷积(均值模糊)
卷积核在图像上滑动,每个位置做加权和。均值模糊核所有系数相等。
cpp
// 来自 QuickDemo::blur_demo
Mat dst;
blur(image, dst, Size(15, 15), Point(-1, -1));
imshow("图像模糊", dst);
- 核大小必须为奇数(1,3,5,...)
Point(-1,-1)表示锚点位于核中心
十三、高斯模糊
高斯核权重符合正态分布,中心最大,离中心越远权重越小,能更好地保留边缘。
cpp
// 来自 QuickDemo::gaussian_blur_demo
Mat dst;
GaussianBlur(image, dst, Size(0, 0), 15); // sigma=15,核大小自动计算
imshow("高斯模糊", dst);
高斯函数公式(近似):
G(x,y) = \\frac{1}{2\\pi\\sigma\^2} e^{-\\frac{x^2+y^2}{2\\sigma^2}}
sigma越大,图像越模糊- 核大小可设为
Size(0,0)由 sigma 自动推导
十四、人脸检测(DNN 模块)
使用 OpenCV DNN 加载 TensorFlow 模型进行人脸检测。
cpp
// 来自 QuickDemo::face_detection_demo
string root_dir = "D:/opencv-4.4.0/opencv/sources/samples/dnn/face_detector/";
dnn::Net net = dnn::readNetFromTensorflow(root_dir + "opencv_face_detector_uint8.pb",
root_dir + "opencv_face_detector.pbtxt");
VideoCapture capture("D:/images/video/example_dsh.mp4");
Mat frame;
while (true) {
capture.read(frame);
if (frame.empty()) break;
Mat blob = dnn::blobFromImage(frame, 1.0, Size(300,300),
Scalar(104,177,123), false, false);
net.setInput(blob);
Mat probs = net.forward(); // 输出维度: 1x1xNx7
Mat detectionMat(probs.size[2], probs.size[3], CV_32F, probs.ptr<float>());
for (int i = 0; i < detectionMat.rows; i++) {
float confidence = detectionMat.at<float>(i, 2);
if (confidence > 0.5) {
int x1 = static_cast<int>(detectionMat.at<float>(i, 3) * frame.cols);
int y1 = static_cast<int>(detectionMat.at<float>(i, 4) * frame.rows);
int x2 = static_cast<int>(detectionMat.at<float>(i, 5) * frame.cols);
int y2 = static_cast<int>(detectionMat.at<float>(i, 6) * frame.rows);
Rect box(x1, y1, x2 - x1, y2 - y1);
rectangle(frame, box, Scalar(0,0,255), 2, 8, 0);
}
}
imshow("人脸检测演示", frame);
if (waitKey(1) == 27) break;
}
输出解析:
- 矩阵形状
1×1×N×7,N 为检测框数量 - 每个框的 7 个值:
[batch_id, class_id, confidence, x1, y1, x2, y2] - 坐标是归一化的,需乘以原图宽高
十五、常见错误与解决方案
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
| 窗口一闪而过 | 缺少 waitKey(0) |
添加 waitKey(0) |
imread 失败 |
路径单反斜杠 \ 被转义 |
使用 D:/ 或 D:\\ |
| LNK1104 无法打开 .exe | 上次程序未关闭,进程占用 | 关闭图像窗口,任务管理器结束进程 |
| LNK2005 main 重复定义 | 多个 .cpp 文件包含 main | 从项目中排除多余文件 |
| C4244 警告 | float 转 int 可能丢数据 | 显式强制转换 (int)x1 |
| C4819 编码警告 | 文件含有中文且非 UTF-8 | 删除中文注释或另存为 UTF-8 with BOM |
| 人脸检测置信度低 | 阈值 >0.5 太高 | 调试时降低到 0.3 |
| 卷积核大小为偶数 | OpenCV 要求奇数 | 使用 3,5,7 等奇数 |
| 归一化后显示全黑 | 浮点图像 0,1 直接保存 | imshow 可显示,保存前转回 CV_8U |
| VS 重装后环境丢失 | 未使用属性表 | 创建 .props 属性表,新项目直接添加 |
| inRange 提取颜色不准确 | 光照影响 | 动态调整阈值或先做直方图均衡化 |
十六、心得体会
- 学以致用:OpenCV 只是工具,重点是与 YOLO、ROS2 结合。不需要精通全部 API,但必须熟练掌握图像获取 → 预处理(色彩空间转换 + 归一化)→ 推理 → 后处理这一链条。
- 笔记习惯 :手写笔记记录关键函数和参数,比光看视频记忆更深刻。例如整理
cvtColor常量和对应数值,随时查阅。 - HSV 比 RGB 更适合作颜色分割:对光照不敏感,机器人视觉中提取目标颜色非常实用。
- 调试技巧 :
cout输出尺寸、类型,imshow分步观察中间结果(特别是 mask),遇到链接错误先检查进程占用。 - 属性表 :创建
.props属性表后,新建项目配置 OpenCV 只需 5 秒,强烈推荐。 - 色彩空间转换必须在归一化之前:因为归一化通常是在最终输入网络前对像素值范围进行压缩,而颜色转换是改变数据语义,必须先完成。
十七、总结
通过本次学习,我掌握了:
- 图像读取、显示、保存及窗口操作
- 色彩空间转换(BGR↔GRAY/HSV)以及利用
inRange进行颜色提取和背景替换 Mat对象的深/浅拷贝、创建空白图像- 像素级读写(灰度与彩色)与通道分离合并
- 图像归一化(
convertTo+/255或normalize) - 几何变换(缩放、翻转、旋转)
- 视频摄像头读取与保存
- 图像卷积(均值模糊)与高斯模糊
- 基于 DNN 模块的人脸检测(加载模型、解析输出、绘制结果)
- 常见编译/运行错误及解决方案
接下来,我将把上述能力迁移到 ROS2 环境中,结合 cv_bridge 实现从 ROS 话题读取图像,并接入 YOLO 进行目标检测,最终完成实物机器人的视觉导航。