前言
在 OpenCV 中,Mat
对象是图像的核心数据结构,包含了图像的元数据和像素数据。了解 Mat
对象的数据组织形式以及像素数据的存储方式对于实现高效的像素级别图像操作非常重要。
Mat 对象的组成:
-
元数据头部:
- 包含图像的基本信息,如行数(rows)、列数(cols)、通道数(channels)、数据类型(data type)等。
- 存储了图像的一些属性和结构信息。
-
像素数据块部分:
- 存储了图像的像素值。
- 数据存储的方式取决于图像的数据类型和通道数。
像素数据的存储方式:
-
连续存储:
- 对于单通道图像,像素值是按行存储的,每一行的像素连续排列。
- 对于多通道图像,每个像素的所有通道值连续存储,然后下一个像素的所有通道值排列在后面。
-
行优先存储(Row-Major Order):
- 对于多通道图像,行优先存储意味着每个通道的所有像素值排列完后,再处理下一个通道的所有像素值。
-
列优先存储(Column-Major Order):
- 对于多通道图像,列优先存储意味着每个像素的所有通道值排列完后,再处理下一个像素的所有通道值。
高效遍历方法的选择:
在考虑像素遍历方法的效率时,可以利用图像的连续存储性质,以及行优先或列优先存储的方式来提高访问速度。使用 .ptr
或 .data
方法可以直接访问图像数据,而使用 MatIterator_
则提供了一种更抽象的迭代器方式。
选择合适的遍历方法还要考虑到图像的大小、数据类型、通道数等因素。在实践中,经过性能测试后再选择最适合场景的方法是一个不错的做法,以确保在大图像处理中能够获得较好的性能。
遍历图像
- 使用嵌套循环遍历每个像素:
//数组遍历 -- 进行反色处理
void read_pixe_array(cv::Mat srcImg, cv::Mat& dstImg)
{
dstImg = Mat::zeros(srcImg.size(), srcImg.type());
if (srcImg.channels() == 1) //单通道
{
for (int i = 0; i < srcImg.rows; i++)
{
for (int j = 0; j < srcImg.cols; j++)
{
dstImg.at<uchar>(i, j) = 255 - srcImg.at<uchar>(i, j);
}
}
}
else
{
//3通道
for (int i = 0; i < srcImg.rows; i++)
{
for (int j = 0; j < srcImg.cols; j++)
{
dstImg.at<Vec3b>(i, j)[0] = 255 - srcImg.at<Vec3b>(i, j)[0]; //B通道
dstImg.at<Vec3b>(i, j)[1] = 255 - srcImg.at<Vec3b>(i, j)[1]; //G通道
dstImg.at<Vec3b>(i, j)[2] = 255 - srcImg.at<Vec3b>(i, j)[2]; //R通道
}
}
}
}
-
使用指针遍历图像像素:
这种方法使用指针来访问图像像素,相对于嵌套循环,可能会更快一些。
//指针遍历 -- 所有像素加50,图片整体变亮
void read_pixe_ptr(cv::Mat srcImg, cv::Mat& dstImg)
{
dstImg = Mat::zeros(srcImg.size(), srcImg.type());
if (srcImg.channels() == 1) //单通道
{
for (int i = 0; i < srcImg.rows; i++)
{
uchar* src = srcImg.ptr<uchar>(i);
uchar* dst = dstImg.ptr<uchar>(i);
for (int j = 0; j < srcImg.cols; j++)
{
dst[j] = saturate_cast<uchar>(src[j] + 50);
}
}
}
else
{
//3通道
for (int i = 0; i < srcImg.rows; i++)
{
uchar* src = srcImg.ptr<uchar>(i);
uchar* dst = dstImg.ptr<uchar>(i);
for (int j = 0; j < srcImg.cols * 3; j++)
{
dst[j] = saturate_cast<uchar>(src[j] + 50);
}
}
}
}
- 使用C指针遍历图像像素:
//迭C指针方式访问 -- 对图像取反
void read_pixe_c(cv::Mat srcImg, cv::Mat& dstImg)
{
dstImg = srcImg.clone();
if (srcImg.channels() == 1) //单通道
{
for (int rows = 0; rows < dstImg.rows; rows++)
{
uchar* pixel = dstImg.data + rows * dstImg.step;
(*pixel) = 255 - (*pixel);
}
}
else
{
for (int rows = 0; rows < dstImg.rows; rows++)
{
uchar* pixel = dstImg.data + rows * dstImg.step;
for (int cols = 0; cols < dstImg.cols; cols++)
{
pixel[0] = 255 - pixel[0];
pixel[1] = 255 - pixel[1];
pixel[2] = 255 - pixel[2];
pixel += 3;
}
}
}
}
-
使用迭代器遍历图像像素:
C++中的迭代器提供了一种更现代的方式来访问容器元素,包括图像像素。
/迭代器访问 -- 所有像素减50,图片整体变暗
void read_pixe_it(cv::Mat srcImg, cv::Mat& dstImg)
{
dstImg = srcImg.clone();
if (srcImg.channels() == 1) //单通道
{
MatIterator_<uchar>it = dstImg.begin<uchar>();
MatIterator_<uchar>itEnd = dstImg.end<uchar>();
for (; it != itEnd; it++)
{
(*it) = saturate_cast<uchar>((*it) - 50);
}
}
else
{
//3通道
MatIterator_<Vec3b>it = dstImg.begin<Vec3b>();
MatIterator_<Vec3b>itEnd = dstImg.end<Vec3b>();
for (; it != itEnd; it++)
{
(*it)[0] = saturate_cast<uchar>((*it)[0] - 50);
(*it)[1] = saturate_cast<uchar>((*it)[1] - 50);
(*it)[2] = saturate_cast<uchar>((*it)[2] - 50);
}
}
}
测试速度
测试的图像大小是100m左右,下载地址:https://www.nodeseek.com/post-10739-1,测试方法是,调用10次的方式,下载是测试代码:
void main()
{
cv::Mat cv_src = cv::imread("13.png");
for (int i = 0; i < 10; ++i)
{
std::cout << "遍历第: " << i << "次" << std::endl;
cv::Mat cv_dst;
double start_1 = getTickCount();
read_pixe_array(cv_src, cv_dst);
int64 end_1 = cv::getTickCount();
// 计算运行时间(以秒为单位)
double totalTime_1 = (end_1 - start_1) / cv::getTickFrequency();
std::cout << "数组遍历运行时间: " << totalTime_1 << " seconds" << std::endl;
double start_2 = getTickCount();
read_pixe_ptr(cv_src, cv_dst);
int64 end_2 = cv::getTickCount();
// 计算运行时间(以秒为单位)
double totalTime_2 = (end_2 - start_2) / cv::getTickFrequency();
std::cout << "指针遍历运行时间: " << totalTime_2 << " seconds" << std::endl;
double start_3 = getTickCount();
read_pixe_it(cv_src, cv_dst);
int64 end_3 = cv::getTickCount();
// 计算运行时间(以秒为单位)
double totalTime_3 = (end_3 - start_3) / cv::getTickFrequency();
std::cout << "迭代器遍历运行时间: " << totalTime_3 << " seconds" << std::endl;
//cv::waitKey();
double start_4 = getTickCount();
read_pixe_c(cv_src, cv_dst);
int64 end_4 = cv::getTickCount();
// 计算运行时间(以秒为单位)
double totalTime_4 = (end_4 - start_4) / cv::getTickFrequency();
std::cout << "C指针遍历运行时间: " << totalTime_4 << " seconds" << std::endl;
std::cout << std::endl;
}
}
测试结果,可以看出,使用C指针的方式遍历图像的方式是最快的: