2、OpenCV核心数据结构:Mat类详解、图像读取与显示、像素操作、ROI区域提取、颜色空间转换
好,咱们正式开始动手写代码了。这一章要啃的是OpenCV里最核心的东西——Mat类。说白了,你在OpenCV里处理的所有图像,底层都是Mat对象。我刚开始学的时候,觉得这玩意儿就是个存像素的容器,后来踩了不少坑才明白,它的内存管理、数据类型、通道数这些细节,直接决定了你的程序会不会崩、跑得快不快。
2.1 Mat类:图像在内存里的“身份证”
Mat全称是Matrix,矩阵。一张彩色图片,在计算机眼里就是一个三维矩阵:高、宽、通道数(BGR)。灰度图就是二维矩阵。
我个人习惯把Mat理解成一个“智能指针+头信息”的组合体。它不直接存像素数据,而是存一个指针指向数据区,外加记录了宽、高、类型、步长等元信息。这样做的好处是——浅拷贝极快。
核心要点:Mat的赋值运算符和拷贝构造函数默认只复制头信息和指针,不复制像素数据。这叫“引用计数机制”。只有当你显式调用 clone() 或 copyTo() 时,才会深拷贝。
来看一个典型的Mat创建方式:
// 创建一个3通道、8位无符号整型、300x400的黑色图像
cv::Mat img(300, 400, CV_8UC3, cv::Scalar(0, 0, 0));
// 或者从已有数据创建
cv::Mat img2 = cv::imread("test.jpg");
这里 CV_8UC3 是什么意思?我拆解一下:
| 部分 | 含义 | 常见取值 |
|---|---|---|
| 8U | 8位无符号整型(0~255) | 8U, 16U, 32F, 64F |
| C3 | 3个通道 | C1, C3, C4 |
嗯,这里要注意:类型不匹配是OpenCV最常见的崩溃原因之一。我曾经在项目里把一个 CV_32F 的矩阵直接传给一个期望 CV_8U 的函数,结果程序秒崩,查了半天才发现是类型没转。
2.2 图像读取与显示:你的第一行OpenCV代码
读取图像用 imread,显示用 imshow,配合 waitKey 才能看到窗口。这个流程我建议你形成肌肉记忆。
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 读取图像,默认彩色模式
cv::Mat src = cv::imread("lena.jpg", cv::IMREAD_COLOR);
if (src.empty()) {
std::cerr << "图片加载失败,检查路径" << std::endl;
return -1;
}
// 显示原图
cv::imshow("Original", src);
// 等待按键,参数0表示无限等待
int key = cv::waitKey(0);
if (key == 27) { // ESC键
cv::destroyAllWindows();
}
return 0;
}
imread 的第二个参数有几个常用选项:
IMREAD_COLOR:默认,转成BGR三通道,忽略透明度。IMREAD_GRAYSCALE:转成单通道灰度图。IMREAD_UNCHANGED:保留原格式,包括Alpha通道。
我的经验:如果你不确定图片有没有加载成功,一定检查 src.empty()。很多新手直接拿空Mat往下处理,程序不崩才怪。
2.3 像素操作:直接读写图像里的每个点
操作像素,最直接的方式是用 at<T>(row, col)。但要注意,这个函数有边界检查,调试时安全,但循环里用多了会慢。
// 遍历每个像素,把BGR三个通道的值都取出来
for (int r = 0; r < src.rows; ++r) {
for (int c = 0; c < src.cols; ++c) {
cv::Vec3b pixel = src.at<cv::Vec3b>(r, c);
uchar blue = pixel[0];
uchar green = pixel[1];
uchar red = pixel[2];
// 做个反色效果
src.at<cv::Vec3b>(r, c) = cv::Vec3b(255 - blue, 255 - green, 255 - red);
}
}
如果你追求性能,用指针方式 ptr<T>(row) 会快很多。我做过测试,对一张1920x1080的图,指针方式比 at 快3~5倍。
// 指针方式遍历,效率更高
for (int r = 0; r < src.rows; ++r) {
uchar* row_ptr = src.ptr<uchar>(r);
for (int c = 0; c < src.cols * 3; c += 3) {
row_ptr[c] = 255 - row_ptr[c]; // B
row_ptr[c + 1] = 255 - row_ptr[c + 1]; // G
row_ptr[c + 2] = 255 - row_ptr[c + 2]; // R
}
}
避坑指南:用指针方式时,一定要确认图像是连续的(isContinuous() 返回true)。如果图像是从ROI截取的,可能不连续,这时候用单层循环遍历整行会越界。我曾经在这个坑里摔过一次,排查了整整一下午。
2.4 ROI区域提取:只处理你关心的部分
ROI(Region of Interest)就是感兴趣区域。比如你做人脸检测,检测到人脸框后,只需要提取人脸区域做后续识别,没必要处理整张图。
提取ROI有两种方式:
- 矩形区域截取:用
cv::Rect指定区域。 - 行/列范围截取:用
cv::Range指定范围。
// 方式一:Rect截取
cv::Rect roi_rect(100, 50, 200, 150); // x, y, width, height
cv::Mat face_region = src(roi_rect);
// 方式二:Range截取
cv::Mat face_region2 = src(cv::Range(50, 200), cv::Range(100, 300));
注意:ROI操作默认是浅拷贝。也就是说,face_region 和 src 共享同一块内存。你改了 face_region 的像素,原图也会变。如果你想要独立的数据,用 face_region.clone()。
实用技巧:在做目标跟踪时,我经常先用ROI把目标区域裁出来,再在这个小图上做检测或特征提取。这样能省掉大量不必要的计算,推理速度能提升30%以上。
2.5 颜色空间转换:BGR、灰度、HSV、YUV
OpenCV默认读图是BGR顺序,不是RGB。这一点太容易搞混了。我见过不少同事用 imshow 显示一张RGB图片,结果颜色完全不对,就是因为没转通道顺序。
颜色空间转换用 cvtColor:
cv::Mat gray, hsv, yuv;
// BGR -> 灰度
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
// BGR -> HSV
cv::cvtColor(src, hsv, cv::COLOR_BGR2HSV);
// BGR -> YUV
cv::cvtColor(src, yuv, cv::COLOR_BGR2YUV);
为什么需要HSV?因为HSV把色调(H)、饱和度(S)、明度(V)分开,做颜色分割时特别好用。比如你要提取红色物体,在BGR空间里红色是(0,0,255),但受光照影响,实际像素可能是(50,30,200),很难用固定阈值。而在HSV空间里,红色的H值集中在0~10和170~180两个区间,用 inRange 就能稳定提取。
// 提取红色区域(HSV空间)
cv::Mat hsv_img, mask;
cv::cvtColor(src, hsv_img, cv::COLOR_BGR2HSV);
// 红色在HSV中的范围
cv::Scalar lower_red1(0, 50, 50);
cv::Scalar upper_red1(10, 255, 255);
cv::Scalar lower_red2(170, 50, 50);
cv::Scalar upper_red2(180, 255, 255);
cv::Mat mask1, mask2;
cv::inRange(hsv_img, lower_red1, upper_red1, mask1);
cv::inRange(hsv_img, lower_red2, upper_red2, mask2);
cv::bitwise_or(mask1, mask2, mask);
我的习惯:做颜色相关的任务,我第一件事就是把图转到HSV。BGR对光照太敏感,HSV的H通道相对稳定。比如做交通信号灯检测,用HSV提取红/绿/黄灯,比BGR直接阈值靠谱得多。
2.6 本章知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着复习:
这一章的内容,说白了就是OpenCV图像处理的“内功心法”。Mat类你玩明白了,后面做图像预处理、特征提取、目标检测,都会顺手很多。我建议你把这几个API都亲手敲一遍,尤其是像素遍历和ROI操作,这两个在后续的YOLO推理加速里会反复用到。