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有两种方式:

  1. 矩形区域截取:用 cv::Rect 指定区域。
  2. 行/列范围截取:用 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 本章知识体系总览

下面这张图把本章的核心知识点串起来了,你可以对照着复习:

第2章:OpenCV核心数据结构知识体系 Mat类(核心容器) 图像读取与显示 像素操作 ROI区域提取 颜色空间转换 imread/imshow waitKey/destroy at<> / ptr Vec3b访问 Rect截取 Range截取 BGR ↔ 灰度 BGR ↔ HSV 💡 核心:Mat是引用计数智能指针,浅拷贝不复制数据 ROI默认浅拷贝,clone()才是深拷贝

这一章的内容,说白了就是OpenCV图像处理的“内功心法”。Mat类你玩明白了,后面做图像预处理、特征提取、目标检测,都会顺手很多。我建议你把这几个API都亲手敲一遍,尤其是像素遍历和ROI操作,这两个在后续的YOLO推理加速里会反复用到。