OpenCV C++ 核心:Mat 与像素操作全解析

在 OpenCV 的 C++ 开发中,Mat是承载图像数据的核心数据结构 ------ 它不仅解决了传统IplImage手动内存管理的痛点,还支持多通道、多数据类型的灵活配置,是所有图像处理操作的 "基石"。本章将围绕Mat的创建、像素访问、算术 / 位运算及通道操作,通过实操代码与原理剖析,帮助开发者夯实底层基础。

一、Mat 创建方法与基础操作

Mat的创建需结合业务场景选择合适方式,不同创建方法对应不同内存分配逻辑;基础操作则聚焦Mat的核心属性(尺寸、类型、通道数)获取与修改,是后续操作的前提。

1.1 常用创建方法(附代码示例)

Mat的创建本质是 "定义图像尺寸、数据类型与通道数",以下为 6 种高频创建方式,覆盖绝大多数开发场景:

(1)默认构造(空矩阵,需后续初始化)

适用于先声明Mat对象,后续通过其他操作(如imreadresize)赋值的场景:

复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main() {
    // 默认构造:创建空Mat,无实际数据
    Mat img;
    // 后续通过imread赋值(此时才分配内存)
    img = imread("test.jpg");
    if (img.empty()) {
        cout << "图像加载失败!" << endl;
        return -1;
    }
    imshow("默认构造+imread", img);
    waitKey(0);
    destroyAllWindows();
    return 0;
}
(2)指定尺寸、类型与初始值

最常用的创建方式,需明确rows(行数 / 高度)、cols(列数 / 宽度)、type(数据类型 + 通道数),可选初始值:

复制代码
// 关键:理解Mat类型定义(以CV_8UC3为例)
// CV_[位深][数据类型][通道数]:8U=8位无符号uchar,C3=3通道(如RGB)
Mat img1(480, 640, CV_8UC3, Scalar(255, 0, 0));  // 480x640蓝色图像(BGR顺序!)
Mat img2(Size(640, 480), CV_8UC1, Scalar(128));  // 640x480灰度图(像素值128,灰色)

// 显示结果
imshow("3通道蓝色图像", img1);
imshow("单通道灰度图", img2);
waitKey(0);
destroyAllWindows();

坑点提醒:Mat(rows, cols)Mat(Size(cols, rows))参数顺序不同,前者是 "高度 × 宽度",后者是 "宽度 × 高度",需严格匹配,否则图像会拉伸变形。

(3)从已有数据创建(数组 / 指针)

适用于将外部数组(如 C 数组)转换为Mat,需确保数据类型与Mat类型一致:

复制代码
// 示例:将3x3的uchar数组转为单通道Mat
uchar data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
Mat img(3, 3, CV_8UC1, data);  // 3行3列,单通道,数据指向data数组

// 打印Mat内容(验证是否正确加载)
cout << "从数组创建的Mat:" << endl << img << endl;
(4)拷贝构造与克隆(浅拷贝 vs 深拷贝)

Mat的拷贝默认是浅拷贝 (仅复制头部,数据区共享),修改拷贝后的对象会影响原对象;若需完全独立的副本,需用clone()copyTo()实现深拷贝

复制代码
Mat img = imread("test.jpg");
Mat img_shallow = img;  // 浅拷贝:共享数据区
Mat img_deep = img.clone();  // 深拷贝:独立数据区

// 修改浅拷贝对象的像素(原对象也会变)
img_shallow.at<Vec3b>(0, 0) = Vec3b(0, 0, 255);  // 左上角像素设为红色

// 显示对比:原图像左上角已变红,深拷贝图像不变
imshow("原图像(受浅拷贝影响)", img);
imshow("深拷贝图像(独立)", img_deep);
waitKey(0);
destroyAllWindows();

1.2 Mat 基础操作(属性获取与修改)

通过Mat的成员函数或属性,可快速获取图像核心信息,常用于预处理判断:

复制代码
Mat img = imread("test.jpg");
// 1. 获取尺寸与通道
int rows = img.rows;          // 高度(行数)
int cols = img.cols;          // 宽度(列数)
Size size = img.size();       // 尺寸(Size(cols, rows))
int channels = img.channels();// 通道数(1=灰度,3=彩色,4=带Alpha通道)

// 2. 获取数据类型与深度
int type = img.type();        // 完整类型(如CV_8UC3)
int depth = img.depth();      // 数据深度(如CV_8U对应0,CV_32F对应5)

// 3. 修改尺寸(resize)
Mat img_resized;
resize(img, img_resized, Size(320, 240));  // 缩放到320x240

// 4. 打印信息
cout << "图像尺寸:" << rows << "x" << cols << endl;
cout << "通道数:" << channels << endl;
cout << "数据类型:" << type << "(CV_8UC3对应16)" << endl;

二、像素访问遍历(3 种常用方式)

像素遍历是图像处理的核心操作(如灰度化、阈值分割),不同遍历方式的效率差异较大,需根据场景选择。以下按 "效率从高到低" 排序讲解,均以CV_8UC3(8 位 3 通道彩色图) 为例。

2.1 指针遍历(效率最高,推荐大图像)

通过ptr<>()获取每行的像素指针,逐行遍历,避免重复计算行偏移,效率最优,适合实时处理或大尺寸图像:

复制代码
Mat img = imread("test.jpg");
if (img.empty() || img.type() != CV_8UC3) {
    cout << "图像类型需为8位3通道!" << endl;
    return -1;
}

// 指针遍历:逐行逐列访问像素
for (int i = 0; i < img.rows; ++i) {
    // 获取第i行的像素指针(Vec3b=3个uchar组成的向量,对应BGR)
    Vec3b* row_ptr = img.ptr<Vec3b>(i);
    for (int j = 0; j < img.cols; ++j) {
        // 访问第i行第j列的像素(BGR顺序)
        Vec3b& pixel = row_ptr[j];
        // 示例:反色处理(255-原像素值)
        pixel[0] = 255 - pixel[0];  // B通道
        pixel[1] = 255 - pixel[1];  // G通道
        pixel[2] = 255 - pixel[2];  // R通道
    }
}

imshow("指针遍历-反色图像", img);
waitKey(0);
destroyAllWindows();

2.2 迭代器遍历(最安全,无越界风险)

通过MatIterator_迭代器遍历,无需手动计算索引,自动处理边界,适合对安全性要求高的场景(如不确定图像尺寸时),效率略低于指针:

复制代码
Mat img = imread("test.jpg");
MatIterator_<Vec3b> it = img.begin<Vec3b>();  // 起始迭代器
MatIterator_<Vec3b> it_end = img.end<Vec3b>();// 结束迭代器

// 迭代器遍历:逐像素访问
for (; it != it_end; ++it) {
    // 示例:降低亮度(每个通道值减50,避免溢出用saturate_cast)
    (*it)[0] = saturate_cast<uchar>((*it)[0] - 50);  // B通道
    (*it)[1] = saturate_cast<uchar>((*it)[1] - 50);  // G通道
    (*it)[2] = saturate_cast<uchar>((*it)[2] - 50);  // R通道
}

imshow("迭代器遍历-低亮度图像", img);
waitKey(0);
destroyAllWindows();

关键:saturate_cast<uchar>()用于处理像素值溢出,若直接减 50 导致值为负,会自动截断为 0;若超过 255,截断为 255,避免出现异常颜色。

2.3 at 方法遍历(最简单,效率最低)

通过at<>()方法直接按坐标访问像素,语法简洁,适合小图像或调试场景,效率最低(每次调用需检查边界):

复制代码
Mat img = imread("test.jpg");
// at方法遍历:i=行(y轴),j=列(x轴)
for (int i = 0; i < img.rows; ++i) {
    for (int j = 0; j < img.cols; ++j) {
        // 示例:转为灰度(加权平均公式:Y=0.299R+0.587G+0.114B)
        Vec3b pixel = img.at<Vec3b>(i, j);
        uchar gray = saturate_cast<uchar>(
            0.299 * pixel[2] + 0.587 * pixel[1] + 0.114 * pixel[0]
        );
        // 将彩色像素设为灰度值(B=G=R=gray)
        img.at<Vec3b>(i, j) = Vec3b(gray, gray, gray);
    }
}

imshow("at方法遍历-灰度图像", img);
waitKey(0);
destroyAllWindows();

坑点提醒:at<Type>(i,j)Type必须与Mat类型匹配,如 CV_8UC3 对应Vec3b,CV_32FC1 对应float,若类型不匹配会直接崩溃。

三、图像算术运算(加减乘除与加权融合)

图像算术运算基于像素逐元素操作,常用于图像叠加、亮度调整、曝光融合等场景,OpenCV 提供封装函数,自动处理溢出与边界。

3.1 基础算术运算(add/subtract/multiply/divide)

核心规则:逐像素对应通道运算,默认采用 "饱和运算"(溢出时截断到 0-255),支持单通道与多通道图像(需尺寸、类型一致)。

复制代码
// 加载两张尺寸、类型相同的图像(示例:前景图与背景图)
Mat img1 = imread("foreground.jpg");
Mat img2 = imread("background.jpg");
Mat dst_add, dst_sub, dst_mul, dst_div;

// 1. 加法(img1 + img2,像素值叠加,溢出取255)
add(img1, img2, dst_add);  // 等价于 dst_add = img1 + img2(OpenCV重载运算符)

// 2. 减法(img1 - img2,像素值相减,不足取0)
subtract(img1, img2, dst_sub);  // 等价于 dst_sub = img1 - img2

// 3. 乘法(像素值相乘,结果需归一化,否则易溢出)
// 注意:CV_8U图像相乘后值可能远超255,需先转CV_32F再归一化
Mat img1_f, img2_f;
img1.convertTo(img1_f, CV_32F);
img2.convertTo(img2_f, CV_32F);
multiply(img1_f, img2_f, dst_mul);  // 32F类型,值范围0~255²
dst_mul = dst_mul / 255;  // 归一化到0~255
dst_mul.convertTo(dst_mul, CV_8U);  // 转回8U类型

// 4. 除法(像素值相除,需避免除零,同样建议先转32F)
divide(img1_f, img2_f + 1e-5, dst_div);  // +1e-5防止除零
dst_div = dst_div * 255;  // 缩放回0~255
dst_div.convertTo(dst_div, CV_8U);

// 显示结果
imshow("加法", dst_add);
imshow("减法", dst_sub);
imshow("乘法(归一化)", dst_mul);
imshow("除法(防除零)", dst_div);
waitKey(0);
destroyAllWindows();

3.2 加权融合(addWeighted,常用图像叠加)

addWeighted实现 "加权平均" 运算:dst = src1*alpha + src2*beta + gamma,常用于图像叠加(如加水印、前景背景融合),支持调整透明度。

复制代码
Mat img = imread("background.jpg");
Mat logo = imread("logo.png");  // 假设logo尺寸小于img,且带Alpha通道(可选)

// 1. 提取logo感兴趣区域(ROI:Region of Interest)
Rect roi(100, 100, logo.cols, logo.rows);  // 左上角(100,100),尺寸与logo一致
Mat img_roi = img(roi);  // img的ROI区域(浅拷贝,修改会影响原图像)

// 2. 加权融合:logo透明度0.5,背景透明度0.5,gamma=0(亮度偏移)
double alpha = 0.5;  // logo权重
double beta = 1 - alpha;  // 背景权重
addWeighted(logo, alpha, img_roi, beta, 0, img_roi);

// 显示结果:logo叠加在img的(100,100)位置,半透明
imshow("加权融合-加水印", img);
waitKey(0);
destroyAllWindows();

四、图像位运算(与 / 或 / 非 / 异或,核心用于遮罩)

位运算基于二进制位操作,在图像分割、遮罩生成、区域提取中应用广泛,尤其适合 "非黑即白" 的二值图像操作。OpenCV 提供bitwise_andbitwise_orbitwise_notbitwise_xor四个函数。

4.1 位运算基础(以二值图像为例)

复制代码
// 创建两张二值图像(0=黑,255=白)
Mat img1 = Mat::zeros(400, 400, CV_8UC1);
Mat img2 = Mat::zeros(400, 400, CV_8UC1);
rectangle(img1, Rect(50, 50, 150, 150), Scalar(255), -1);  // img1画白色矩形
circle(img2, Point(200, 200), 100, Scalar(255), -1);       // img2画白色圆形

Mat dst_and, dst_or, dst_not, dst_xor;

// 1. 位与(bitwise_and):两图均为白(255)时结果为白,否则黑(提取交集)
bitwise_and(img1, img2, dst_and);

// 2. 位或(bitwise_or):任一图为白时结果为白(提取并集)
bitwise_or(img1, img2, dst_or);

// 3. 位非(bitwise_not):反转像素值(黑变白,白变黑,取反)
bitwise_not(img1, dst_not);

// 4. 位异或(bitwise_xor):两图像素值不同时为白,相同为黑(提取异集)
bitwise_xor(img1, img2, dst_xor);

// 显示结果
imshow("img1(矩形)", img1);
imshow("img2(圆形)", img2);
imshow("位与(交集)", dst_and);
imshow("位或(并集)", dst_or);
imshow("位非(img1反转)", dst_not);
imshow("位异或(异集)", dst_xor);
waitKey(0);
destroyAllWindows();

4.2 位运算实战:图像抠图(提取特定区域)

结合位运算与掩膜(mask),可实现 "抠出前景并叠加到新背景" 的经典需求,步骤如下:

  1. 加载前景图(带 Alpha 通道或手动创建掩膜);

  2. 创建前景掩膜(二值图,前景为白,背景为黑);

  3. 用位与提取前景,位与非清除背景对应区域;

  4. 前景与背景叠加。

    // 1. 加载图像(前景图带Alpha通道,背景图任意)
    Mat foreground = imread("foreground_alpha.png", IMREAD_UNCHANGED); // 读取Alpha通道
    Mat background = imread("background.jpg");
    resize(background, background, foreground.size()); // 背景尺寸与前景一致

    // 2. 拆分前景图的RGB通道与Alpha通道(Alpha通道作为掩膜)
    vector<Mat> fg_channels;
    split(foreground, fg_channels); // fg_channels[0]=B, 1=G, 2=R, 3=Alpha
    Mat fg_rgb = Mat(foreground.size(), CV_8UC3);
    vector<Mat> fg_rgb_channels = {fg_channels[0], fg_channels[1], fg_channels[2]};
    merge(fg_rgb_channels, fg_rgb); // 前景RGB图像
    Mat mask = fg_channels[3]; // Alpha通道作为掩膜(255=前景,0=背景)

    // 3. 位运算抠图
    Mat background_roi;
    bitwise_not(mask, mask); // 掩膜反转(0=前景,255=背景)
    bitwise_and(background, background, background_roi, mask); // 清除背景的前景区域
    bitwise_not(mask, mask); // 恢复原掩膜
    Mat dst = background_roi + fg_rgb; // 前景与背景叠加

    // 显示结果:前景抠出并叠加到新背景
    imshow("抠图结果", dst);
    waitKey(0);
    destroyAllWindows();

五、通道与数据类型操作(拆分 / 合并 / 类型转换)

通道与数据类型是Mat的核心属性,不同场景需灵活调整(如单通道灰度图用于边缘检测,3 通道彩色图用于显示,32 位浮点图用于算法计算)。

5.1 通道操作(split/merge/mixChannels)

(1)通道拆分(split)与合并(merge)

将多通道图像拆分为单通道图像,修改后再合并,常用于调整特定通道(如增强 R 通道亮度):

复制代码
Mat img = imread("test.jpg");  // CV_8UC3(BGR)
vector<Mat> channels;

// 1. 拆分通道
split(img, channels);  // channels[0]=B, 1=G, 2=R

// 2. 修改单个通道(示例:增强R通道亮度)
channels[2] = channels[2] + 50;  // R通道值加50
channels[2] = saturate_cast<uchar>(channels[2]);  // 处理溢出

// 3. 合并通道
Mat img_enhanced;
merge(channels, img_enhanced);

// 显示对比:增强R通道后图像更红
imshow("原图像", img);
imshow("R通道增强", img_enhanced);
waitKey(0);
destroyAllWindows();
(2)通道重排(mixChannels)

更灵活的通道操作,可实现 "跨图像通道复制",如将 BGR 图像转为 RGB(OpenCV 默认 BGR,其他库如 OpenCV-Python 默认 RGB):

复制代码
Mat img_bgr = imread("test.jpg");
Mat img_rgb(img_bgr.size(), CV_8UC3);

// 通道重排规则:BGR→RGB
// fromTo数组:{原通道索引, 目标通道索引, ...}
int fromTo[] = {0, 2, 1, 1, 2, 0};  // B→R, G→G, R→B
mixChannels(&img_bgr, 1, &img_rgb, 1, fromTo, 3);  // 1个输入,1个输出,3组通道映射

imshow("BGR(OpenCV默认)", img_bgr);
imshow("RGB(重排后)", img_rgb);
waitKey(0);
destroyAllWindows();

5.2 数据类型转换(convertTo)

convertTo用于实现Mat数据类型的转换,常需指定 "缩放因子" 与 "偏移量",核心场景:

  • 8 位无符号(CV_8U)→32 位浮点(CV_32F):用于后续算法计算(如滤波、特征检测,避免整数截断误差);

  • 32 位浮点→8 位无符号:计算完成后转回显示格式。

    Mat img_8u = imread("test.jpg"); // CV_8U(0~255)
    Mat img_32f, img_8u_back;

    // 1. CV_8U → CV_32F(归一化到0~1,避免后续计算溢出)
    img_8u.convertTo(img_32f, CV_32F, 1.0/255.0); // 缩放因子=1/255,偏移量=0
    cout << "CV_32F图像像素范围:" << img_32f.at<float>(0,0) << "~" << img_32f.at<float>(img_32f.rows-1, img_32f.cols-1) << endl;

    // 2. CV_32F → CV_8U(缩放回0~255)
    img_32f.convertTo(img_8u_back, CV_8U, 255.0); // 缩放因子=255,偏移量=0

    // 显示:转换后图像无明显差异(因仅改变数据类型,未修改像素值)
    imshow("原CV_8U图像", img_8u);
    imshow("转换回CV_8U的图像", img_8u_back);
    waitKey(0);
    destroyAllWindows();

关键:转换时需合理设置缩放因子,如 CV_8U→CV_32F 除以 255,确保浮点值在 0~1 之间,避免后续算术运算(如矩阵乘法)出现数值溢出。

六、总结

Mat是 OpenCV C++ 开发的 "基石",本章围绕Mat的核心操作展开,关键要点总结如下:

  1. Mat 创建 :区分浅拷贝(默认)与深拷贝(clone()),明确CV_8UC3等类型的含义,避免尺寸参数顺序错误;
  2. 像素遍历 :大图像用指针,安全场景用迭代器,调试用at方法,需匹配Mat数据类型(如Vec3b对应 3 通道 8U);
  3. 算术运算 :基础运算用add/subtract,叠加用addWeighted,注意 8U 图像溢出问题,建议先转 32F;
  4. 位运算 :核心用于掩膜与区域提取,bitwise_and取交集,bitwise_or取并集,实战中常结合 ROI;
  5. 通道与类型split/merge处理通道,convertTo转换类型,BGR 与 RGB 的重排需用mixChannels

掌握这些操作后,可轻松应对后续的图像滤波、边缘检测、目标识别等进阶场景 ------ 所有复杂算法的底层,都是对Mat像素的精准操作。建议结合实际需求编写代码,重点关注 "效率" 与 "数据类型匹配",避免常见坑点(如浅拷贝修改原图像、像素值溢出)。

相关推荐
劳尔的狙击镜3 小时前
CT影像寻找皮肤轮廓预处理
python·opencv·findcontours·ct·皮肤轮廓·皮肤表皮建模·医学影像处理
极客智造3 小时前
OpenCV C++ 色彩空间详解:转换、应用与 LUT 技术
c++·人工智能·opencv
湫兮之风3 小时前
OpenCV: cv::warpAffine()逆仿射变换详解
人工智能·opencv·计算机视觉
非优秀程序员3 小时前
开发人员如何使用在自己的系统中对接 Nano Banana 【完整教程】
人工智能
阿三08123 小时前
钉钉 AI 深度赋能制造业 LTC 全流程:以钉钉宜搭、Teambition 为例
人工智能·低代码·钉钉·teambition
摩羯座-185690305943 小时前
京东商品评论接口技术实现:从接口分析到数据挖掘全方案
人工智能·数据挖掘
格调UI成品3 小时前
智能制造新视角:工业4.0中,数字孪生如何优化产品全生命周期?
人工智能·工业4.0
小江村儿的文杰3 小时前
理解UE4中C++17的...符号及enable_if_t的用法及SFINAE思想
数据结构·c++·ue4
机器学习之心3 小时前
PINN物理信息神经网络用于求解二阶常微分方程(ODE)的边值问题,Matlab实现
人工智能·神经网络·matlab·物理信息神经网络·二阶常微分方程