在 OpenCV 中,Mat
对象是图像的核心数据结构,包含了图像的元数据和像素数据。了解 Mat
对象的数据组织形式以及像素数据的存储方式对于实现高效的像素级别图像操作非常重要。
元数据头部:
像素数据块部分:
连续存储:
行优先存储(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指针方式访问 -- 对图像取反
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指针的方式遍历图像的方式是最快的: