在 OpenCV 的 C++ 开发中,Mat
是承载图像数据的核心数据结构 ------ 它不仅解决了传统IplImage
手动内存管理的痛点,还支持多通道、多数据类型的灵活配置,是所有图像处理操作的 "基石"。本章将围绕Mat
的创建、像素访问、算术 / 位运算及通道操作,通过实操代码与原理剖析,帮助开发者夯实底层基础。
一、Mat 创建方法与基础操作
Mat
的创建需结合业务场景选择合适方式,不同创建方法对应不同内存分配逻辑;基础操作则聚焦Mat
的核心属性(尺寸、类型、通道数)获取与修改,是后续操作的前提。
1.1 常用创建方法(附代码示例)
Mat
的创建本质是 "定义图像尺寸、数据类型与通道数",以下为 6 种高频创建方式,覆盖绝大多数开发场景:
(1)默认构造(空矩阵,需后续初始化)
适用于先声明Mat
对象,后续通过其他操作(如imread
、resize
)赋值的场景:
#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_and
、bitwise_or
、bitwise_not
、bitwise_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),可实现 "抠出前景并叠加到新背景" 的经典需求,步骤如下:
-
加载前景图(带 Alpha 通道或手动创建掩膜);
-
创建前景掩膜(二值图,前景为白,背景为黑);
-
用位与提取前景,位与非清除背景对应区域;
-
前景与背景叠加。
// 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
的核心操作展开,关键要点总结如下:
- Mat 创建 :区分浅拷贝(默认)与深拷贝(
clone()
),明确CV_8UC3
等类型的含义,避免尺寸参数顺序错误; - 像素遍历 :大图像用指针,安全场景用迭代器,调试用
at
方法,需匹配Mat
数据类型(如Vec3b
对应 3 通道 8U); - 算术运算 :基础运算用
add/subtract
,叠加用addWeighted
,注意 8U 图像溢出问题,建议先转 32F; - 位运算 :核心用于掩膜与区域提取,
bitwise_and
取交集,bitwise_or
取并集,实战中常结合 ROI; - 通道与类型 :
split/merge
处理通道,convertTo
转换类型,BGR 与 RGB 的重排需用mixChannels
。
掌握这些操作后,可轻松应对后续的图像滤波、边缘检测、目标识别等进阶场景 ------ 所有复杂算法的底层,都是对Mat
像素的精准操作。建议结合实际需求编写代码,重点关注 "效率" 与 "数据类型匹配",避免常见坑点(如浅拷贝修改原图像、像素值溢出)。