5、OpenCV遍历图像像素、查找表和时间效率

OpenCV遍历图像像素、查找表和时间效率

    • 一、本节学习目标
    • 二、测试案例
    • 三、准备工作
    • 四、对比三种遍历算法的性能
    • 五、使用查找表的方法
    • 六、性能分析
    • 七、完整代码
    • 八、致谢

一、本节学习目标

  • 学会如何遍历图像的每一个像素
  • 弄懂OpenCV矩阵值是如何存储的
  • 学会评估和测量算法的性能(使用计时函数)
  • 弄清楚什么是查找表并学会使用他们

二、测试案例

让我们考虑一个简单的颜色量化(减少一个颜色分量如R通道的取值数)方法。通过使用unsigned char C和c++类型来存储矩阵元素,一个像素通道可以有多达256个不同的值。对于一个三通道的图像,这会形成太多的颜色(确切地说是1600万)。处理如此多的色度可能会给我们的算法性能带来严重的影响。然而,有时候你只需要使用相对较少的颜色取值便能够获得相同的最终结果。

在这种情况下,我们通常会对颜色空间进行缩减。这意味着我们用一个新的输入值分割颜色空间的当前值,最终得到更少的颜色取值。例如,0到9之间的每一个值都取新值0,10到19之间的每一个值都取值10,以此类推。

当你将uchar类型的(无符号char - 其值在0到255之间)值与int值相除时,结果也将是char类型的值。这些值只能是char类型的值。因此,任何分数都会向下舍入(C/C++整除的特性,如3/2=1)。利用这一事实,uchar域上操作可以表示为:
在这里插入图片描述
一个简单的颜色空间缩减算法需要遍历图像矩阵的每个像素,在遍历的过程中应用这个公式。值得注意的是,我们做了除法和乘法运算,这些操作对于一个系统来说是很耗时的。如果可能的话,通过使用一些更高效的操作来避免它们是值得的,比如一些减法、加法,或者最好的情况是一个简单的赋值操作。此外,注意我们只有有限数量的输入值用于上操作。在uchar系统中,准确地说是256。

因此,对于较大的图像,明智的做法是预先计算所有可能输入值对应的输出值,并在赋值过程中使用查找表进行赋值。查找表是简单的数组(具有一个或多个维度),对于给定的输入值,它保存最终的输出值。它的优点是我们不需要做计算,我们只需要读取结果。

我们的测试用例程序(以及下面的代码示例)将执行以下操作:首先读取命令行参数传递的图像(它可能是彩色图像也可能是灰度图),并使用给定的命令行参数的整数值应用缩减。在OpenCV中,目前有三种主要的逐像素浏览图像的方法。为了让事情变得更有趣一点,我们将使用这些方法中的每一种来处理图像,并打印出处理所需的时间。

三、准备工作

1、构建查询表的内容

    #define divideWith 10
    uchar table[256];
    for (int i = 0; i < 256; ++i)
       table[i] = (uchar)(divideWith * (i/divideWith));

2、计时函数统计算法性能
OpenCV提供了两个简单的函数来实现计时,cv::getTickCount()cv::getTickFrequency()。第一个函数返回执行该函数时系统CPU的计数(比如自启动系统以来)。第二个函数返回的是CPU在一秒钟内发出多少次计数(频率)。因此,测量两个操作之间所花费的时间非常简单:

    double t = (double)getTickCount();
    // do something ...
    t = ((double)getTickCount() - t)/getTickFrequency();
    cout << "Times passed in seconds: " << t << endl;

3、图像矩阵如何存储在内存中?
你已经在上一节的笔记中中了解了Mat对象,矩阵的大小取决于所使用的颜色空间和图像的尺寸(这儿只讨论颜色系统)。颜色空间即所使用的通道数量。在灰度图像的情况下,图像矩阵为:
5、OpenCV遍历图像像素、查找表和时间效率_第1张图片
上图中,最上一行标识列号,最左一列标识行号。中间的取值分别代表行列索引。

对于多通道图像,列包含与通道数量相同的子列。例如,BGR颜色空间的图像矩阵为:

在这里插入图片描述
注意通道的顺序是BGR而不是RGB。通常情况下,内存足够大,可以以连续的方式存储行,所以行可能一个接一个地出现,从而创建一个长行。因为所有的东西都在一个地方,一个接着一个,这可能有助于加快扫描过程。我们可以使用 cv::Mat::isContinuous() 函数来判断图像矩阵的存储是否连续。

四、对比三种遍历算法的性能

1、最有效的方式:C特点的指针操作

在性能方面,经典的C风格操作符访问是最有效的。

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
    CV_Assert(I.depth() == CV_8U);   // 只接受char类型的矩阵
    int channels = I.channels();     // 获取图像的通道数
    int nRows = I.rows;              // 获取图像的行数
    // 这个列不是图像的列数,其值与steps相等,表示图像矩阵中一行的列数。因为图像都是按行存储的,
    // 即一行的元素个数等于 图像的列数 * 通道数
    int nCols = I.cols * channels;  
    // 判断图像矩阵的存储是否连续,如果连续,则可以当作一行处理,那就只需要知道总元素个数即可
    if (I.isContinuous())
    {
        nCols *= nRows;
        nRows = 1;
    }
    int i,j;
    uchar* p;
    // 外层循环遍历行
    for( i = 0; i < nRows; ++i)
    {
        // 指针获取一行的首地址
        p = I.ptr<uchar>(i);
        // 内层循环控制遍历每一个元素
        for ( j = 0; j < nCols; ++j)
        {
            // 利用数组的随机读写的特性快速赋值
            p[j] = table[p[j]];
        }
    }
    return I;
}

这里我们基本上只获取一个指向每一行开始的指针,然后遍历它直到它结束。在矩阵以连续方式存储的特殊情况下,我们只需要请求一次指针,并一直走到最后。我们需要处理彩色图像:彩色图像有三个通道,所以我们需要在每一行中遍历三倍多的元素。

还有另一种方法。Mat对象的data数据成员返回指向第一行第一列的指针。如果该指针为空,则该对象中没有有效的输入。检查这是检查图像加载是否成功的最简单的方法。如果存储是连续的,我们可以使用这个来遍历整个数据指针。在灰度图像的情况下,如:

    uchar* p = I.data;
    for( unsigned int i = 0; i < ncol*nrows; ++i)
        *p++ = table[*p];

你会得到相同的结果。然而,这段代码在以后阅读起来会困难得多。如果你在循环中做了更复杂的操作,阅读就更困难了。而且,在实践中,您会得到相同的性能结果(因为大多数现代编译器可能会自动为您实现这个小的优化技巧)。

2、安全的方式:迭代器
如果采用指针的方法,则需要确保传递正确数量的uchar字段,并跳过行之间可能出现的间隙。迭代器方法被认为是一种更安全的方法,因为它代替用户进行了判断。您所需要做的就是找到图像矩阵的开始和结束迭代器,然后递增开始迭代器,直到结束。要获取迭代器所指向的值,请使用*操作符(在它前面添加该值)。

Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
    CV_Assert(I.depth() == CV_8U);
    const int channels = I.channels();
    // 灰度图和彩色图的迭代器类型不一样,需要不同处理
    switch(channels)
    {
    case 1:
        {
            MatIterator_<uchar> it, end;
            // 迭代器的遍历可读性更强
            for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
                *it = table[*it];
            break;
        }
    case 3:
        {
            MatIterator_<Vec3b> it, end;
            for( it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
            {
                (*it)[0] = table[(*it)[0]];
                (*it)[1] = table[(*it)[1]];
                (*it)[2] = table[(*it)[2]];
            }
        }
    }
    return I;
}

对于彩色图像,每个列有三个uchar元素。这可以被认为是uchar元素组成的一个短向量,在OpenCV中它被命名为Vec3b。为了访问第n个子列,我们使用简单的操作符[]访问。重要的是要记住,OpenCV迭代器遍历列并自动跳到下一行。因此,对于彩色图像,如果你使用简单的uchar迭代器,你就只能访问蓝色通道的值。

3、动态地址计算:返回引用
最后一种方法不推荐用于遍历图像像素。它是用来获取或修改图像中的随机元素的。它的基本用途是指定要访问的项的行号和列号。在前面两种遍历方法中,你可能已经注意到:我们正在处理的图像的类型是很重要的,这里则不需要区分,因为您需要手动指定在自动查找中使用的类型。你可以在以下处理灰度图像的源代码的观察到这一点(使用 cv::Mat::at() 函数):

Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
    CV_Assert(I.depth() == CV_8U);
    const int channels = I.channels();
    switch(channels)
    {
    case 1:
        {
            for( int i = 0; i < I.rows; ++i)
                for( int j = 0; j < I.cols; ++j )
                    I.at<uchar>(i,j) = table[I.at<uchar>(i,j)];
            break;
        }
    case 3:
        {
         Mat_<Vec3b> _I = I;
         for( int i = 0; i < I.rows; ++i)
            for( int j = 0; j < I.cols; ++j )
               {
                   _I(i,j)[0] = table[_I(i,j)[0]];
                   _I(i,j)[1] = table[_I(i,j)[1]];
                   _I(i,j)[2] = table[_I(i,j)[2]];
            }
         I = _I;
         break;
        }
    }
    return I;
}

该函数获取输入数据类型和坐标,并计算查询项的地址,然后返回对它的引用。在获取值时,这可能是一个常量,而在设置值时,这可能不是常量。作为仅在debug模式中的安全步骤,将执行一个检查,以确认您的输入坐标是有效的,并且确实存在。如果不是这种情况,您将在标准错误输出流上得到一个很好的输出消息。与release模式下的有效方法相比,使用这个方法的唯一区别是,对于图像的每个元素,都将获得一个新的行指针。

如果您需要使用这种方法对一个图像进行多次查找,那么为每次访问输入数据类型和at关键字可能会很麻烦,而且很耗时。为了解决这个问题,OpenCV使用cv::Mat_ 数据类型,它与Mat相同,只是需要在定义时通过绑定数据矩阵来指定数据类型,但是作为回报,可以使用operator() 来快速访问元素。为了更便捷,这很容易转换为cv::Mat数据类型。您可以在上述函数的对彩色图像的处理中看到它的示例用法。然而要注意,同样的操作(具有相同的运行速度)也可以用cv::Mat::at函数来完成。

五、使用查找表的方法

这是在图像中实现查找表修改的附加方法。在图像处理中,将所有给定的图像值修改为其他值是很常见的。OpenCV提供了一个修改图像值的函数,不需要写图像的遍历逻辑。我们使用了core模块的 cv::LUT() 函数。首先,我们构建一个Mat类型的查找表:

    Mat lookUpTable(1, 256, CV_8U);
    uchar* p = lookUpTable.ptr();
    for( int i = 0; i < 256; ++i)
        p[i] = table[i];

最后调用函数(I是我们的输入图像,J是输出图像):

    LUT(I, lookUpTable, J);

六、性能分析

为了获得最好的结果,请编译程序并自己运行它。为了使不同算法的性能差异扩大,我使用了一个相当大的(3840 X 2160)图像。这里展示的是彩色图像。为了获得更精确的值,我在函数调用中调用100次取平均值。

算法 算法耗时(ms)
C指针 3.27054
MatIterator_ 5.09794
Random Access 5.4999
LUT 2.68983

5、OpenCV遍历图像像素、查找表和时间效率_第2张图片

作者使用的PC环境为WIN10 64位操作系统,CPU为2块Intel® Xeon® Silver 4114,OpenCV版本为OpenCV4.5。以上实验数据仅供参考,每次运行可能略微不同。

七、完整代码

#include
using namespace std;
using namespace cv;
#define DIVIDEWIDTH  10

Mat scanImageAndReduceC(Mat& src, const uchar* const table);
Mat scanImageAndReduceIterator(Mat src, const uchar* const table);
Mat scanAndReduceImageRandomAccess(Mat& src, const uchar* const table);
Mat scanAndReduceImageLUT(Mat& src, Mat& lookUpTable);

int main(int argc, char** argv)
{
	cout << "----->\t\t\tStart scan and reduce image\t\t\t<-----" << endl << endl;

	string fileName = "O:\\CSDN\\2.jpeg";
	Mat src = imread(fileName, IMREAD_COLOR);
	if (src.empty())
	{
		cout << "failed to load image[" << fileName << "],Please check out the path!" << endl;
		system("pause");
		return EXIT_FAILURE;
	}

	uchar table[256];
	for (int i = 0; i < 256; ++i)
		table[i] = (uchar)(DIVIDEWIDTH * (i / DIVIDEWIDTH));

	Mat dst1, dst2, dst3, dst4;

	cout << "Method No1: C point.\tDealing, please wait..." << endl;
	double t1 = (double)getTickCount();
	for (int i = 0; i < 100; ++i)
	{
		 dst1= scanImageAndReduceC(src, table);
	}
	double t1Average = ((double)getTickCount() - t1) / getTickFrequency();
	cout << "The average cost time of Method No1 is: " << t1Average << "(ms)" << endl << endl;

	cout << "Method No2: C++ MatIterator_.\tDealing, please wait..." << endl;
	double t2 = (double)getTickCount();
	for (int i = 0; i < 100; ++i)
	{
		dst2 = scanImageAndReduceIterator(src, table);
	}
	double t2Average = ((double)getTickCount() - t2) / getTickFrequency();
	cout << "The average cost time of Method No2 is: " << t2Average << "(ms)" << endl << endl;

	cout << "Method No3: Random Access.\tDealing, please wait..." << endl;
	double t3 = (double)getTickCount();
	for (int i = 0; i < 100; ++i)
	{
		dst3 = scanAndReduceImageRandomAccess(src, table);
	}
	double t3Average = ((double)getTickCount() - t3) / getTickFrequency();
	cout << "The average cost time of Method No3 is: " << t3Average << "(ms)" << endl << endl;
	
	Mat lookUpTable(1, 256, CV_8U);
	uchar* p = lookUpTable.ptr();
	for (int i = 0; i < 256; ++i)
		p[i] = table[i];

	cout << "Method No4: LUT.\tDealing, please wait..." << endl;
	double t4 = (double)getTickCount();
	for (int i = 0; i < 100; ++i)
	{
		dst4 = scanAndReduceImageLUT(src, lookUpTable);
	}
	double t4Average = ((double)getTickCount() - t4) / getTickFrequency();
	cout << "The average cost time of Method No4 is: " << t4Average << "(ms)" << endl << endl;

	namedWindow("Original", WINDOW_NORMAL);
	resizeWindow("Original", src.cols / 5, src.rows / 5);
	imshow("Original", src);
	namedWindow("C point", WINDOW_NORMAL);
	resizeWindow("C point", src.cols / 5, src.rows / 5);
	imshow("C point", dst1);
	namedWindow("MatIterator_", WINDOW_NORMAL);
	resizeWindow("MatIterator_", src.cols / 5, src.rows / 5);
	imshow("MatIterator_", dst2);
	namedWindow("Random access", WINDOW_NORMAL);
	resizeWindow("Random access", src.cols / 5, src.rows / 5);
	imshow("Random access", dst3);
	namedWindow("LUT", WINDOW_NORMAL);
	resizeWindow("LUT", src.cols / 5, src.rows / 5);
	imshow("LUT", dst4);
	waitKey(0);
	destroyAllWindows();
	
	system("pause");
	return EXIT_SUCCESS;
}

Mat scanImageAndReduceC(Mat& src, const uchar* const table)
{
	Mat I = src.clone();
	CV_Assert(I.depth() == CV_8U);
	int channels = I.channels();
	int rows = I.rows;
	int cols = I.cols * channels;
	if (I.isContinuous())
	{
		cols *= rows;
		rows = 1;
	}
	uchar* p;
	for (int row = 0; row < rows; ++row)
	{
		p = I.ptr<uchar>(row);
		for (int col = 0; col < cols; ++col)
		{
			p[col] = table[p[col]];
		}
	}
	return I;
}

Mat scanImageAndReduceIterator(Mat src, const uchar* const table)
{
	Mat I = src.clone();
	CV_Assert(I.depth() == CV_8U);
	const int channels = I.channels();
	switch (channels)
	{
	case 1:
	{
		MatIterator_<uchar>it, end;
		for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
		{
			*it = table[*it];
		}
		break;
	}
	case 3:
	{
		MatIterator_<Vec3b>it, end;
		for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it)
		{
			(*it)[0] = table[(*it)[0]];
			(*it)[1] = table[(*it)[1]];
			(*it)[2] = table[(*it)[2]];
		}
		break;
	}
	}
	return I;
}

Mat scanAndReduceImageRandomAccess(Mat& src, const uchar* const table)
{
	Mat I = src.clone();
	CV_Assert(I.depth() == CV_8U);
	const int channels = I.channels();
	switch (channels)
	{
	case 1:
	{
		for (int row = 0; row < I.rows; ++row)
			for (int col = 0; col < I.cols; ++col)
				I.at<uchar>(row, col) = table[I.at<uchar>(row, col)];
		break;
	}
	case 3:
	{
		Mat_<Vec3b> _I = I;
		for(int row=0;row<I.rows;++row)
			for (int col = 0; col < I.cols; ++col)
			{
				_I(row, col)[0] = table[_I(row, col)[0]];
				_I(row, col)[1] = table[_I(row, col)[1]];
				_I(row, col)[2] = table[_I(row, col)[2]];
			}
		break;
	}
	}
	return I;
}

Mat scanAndReduceImageLUT(Mat& src,Mat &lookUpTable)
{
	Mat I = src.clone();
	CV_Assert(I.depth() == CV_8U);
	LUT(I, lookUpTable, I);
	return I;
}

八、致谢

1、感谢自己坚持不懈地做好每件事
2、感谢OpenCV官方团队作出地贡献
3、感兴趣的小伙伴欢迎入群讨论学习。入群飞机票

你可能感兴趣的:(Opencv学习笔记,OpenCV教程,OpenCV笔记,OpenCV遍历像素,OpenCV像素操作,OpenCV矩阵操作)