5. OpenCV 4.2.0 使用 OpenCV 扫描图像、查找表和时间测量(OpenCV 官方文档翻译)

官方文档链接:https://docs.opencv.org/4.2.0/db/da5/tutorial_how_to_scan_images.html


目标 (Goal)

  • 如何遍历图像的每个像素?
  • OpenCV 矩阵值是如何存储的?
  • 如何衡量算法的性能?
  • 什么是查找表?为什么要使用它们?

测试用例 (Our test case)

为了实现简单的颜色还原方法,可以通过使用 uchar 的 C/C++ 类型来存储矩阵项,像素的通道可以具有多达 256 个不同的值。对于一个三通道的图像,可以产生多种颜色(确切地说是 1600 万)。使用如此多的色阶可能会严重影响算法性能。然而,有时只处理其中的一部分值就可以达到相同的结果。

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

当用一个 uchar 值除以一个 int 值时,结果仍是 char 类型的值。因此,任何分数都将四舍五入。利用这一事实,uchar 域中的上层操作可以表示为:

I n e w = ( I o l d 10 ∗ 10 ) I_new = (\frac{I_{old}}{10} * 10) Inew=(10Iold10)

一个简单的颜色空间压缩算法即如上所示,对图像矩阵的每个像素应用上述公式。值得注意的是,上述公式做了除法和乘法运算。对于一个系统来说,这些操作是非常昂贵的。如果可能的话,通过使用一些更简单的操作(如一些减法、加法或在最好的情况下是简单的赋值)来避免执行乘除法操作。此外,需要注意的是,我们只有有限的上限操作的输入值,对于 uchar 类型,是 256。

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

如下所示测试程序将执行如下操作:读入作为命令行参数传递的图像(可以是彩色或灰度),并使用给定的命令行参数整数值进行像素值缩减。在 OpenCV 中,目前有三种主要的逐像素浏览图像的方法。下述示例中展示了三种扫描图像的方法以及每种方法花费的时间。

先计算 查找表

int valueDivideWith = 10;

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

OpenCV 提供了两种简单的函数来实现程序耗时计算:cv::getTickCount()cv::getTickFrequency()。第一个函数返回系统 CPU 从某个事件开始(如自启动系统以来)的计时次数。第二个函数返回 CPU 在一秒钟内发出的滴答声的次数。因此,测量两个操作之间经过的时间量非常简单:

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

图像矩阵在内存中的存储 (How is the image matrix stored in memory?)

图像矩阵的大小取决于使用的颜色系统。更确切地说,取决于使用的通道数量。在灰度图像中,图像矩阵形式如下:

5. OpenCV 4.2.0 使用 OpenCV 扫描图像、查找表和时间测量(OpenCV 官方文档翻译)_第1张图片

对于多通道图像,列包含的子列数与通道数相同。例如,对于 BGR 颜色系统:
在这里插入图片描述

注意:通道的顺序是相反的:BGR 而不是 RGB。因为在许多情况下,内存足够大,可以连续地存储行,行可以一个接一个地跟随,从而创建一个较长的行。因为所有的东西都在一个地方,一个接一个,有助于加快扫描过程。可以使用 cv::Mat::isContinuous() 函数来询问矩阵是否是这种情况。


方法一 :有效的方式 (The efficient way)

在性能方面,无法超过经典的 C 类型的运算符 [] (指针)访问。因此,推荐最有效的分配方式是:

cv::Mat& ScanImageAndReduceC(cv::Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    
    int channels = I.channels();
    
    int nRows = I.rows;
    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 对象的数据成员返回指向第一行第一列的指针。如果此指针为空,则该对象中没有有效的输入。可检查图像加载是否成功。如果存储是连续的,则可以使用它遍历整个数据指针。如果是灰度图像,则如下所示:

uchar* p = I.data;

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

可以得到同样的结果。


方法二 :迭代器(安全的方法) (The iterator (safe) method)

在这种情况下,可以跳过指定数量的 uchar 字段并跳过行之间可能出现的间隙。迭代器方法被认为是一种更安全的方法,因为它可以帮助用户完成上述任务。你所需要做的就是访问图像矩阵的开始和结束,然后增加开始迭代器直到到达结束。要获取迭代器所指向的值,可使用 * 运算符。

cv::Mat& ScanImageAndReduceIterator(cv::Mat& I, const uchar* const table)
{
    // accept only char type matrices
    CV_Assert(I.depth() == CV_8U);
    
    const int channels = I.channels();
    switch(channels)
    {
        case 1:
        {
            cv::MatIterator_<uchar> it, end;
            for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
                *it = table[*it];
            break;
        }
        
        case 3:
        {
            cv::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 迭代器,则只能访问蓝色通道值。


方法三 :带引用返回的动态地址计算 (On-the-fly address calculation with reference returning)

最后一种方法不推荐用于扫描。它是为了获取或修改图像中的随机元素。它的基本用法是指定要访问的项的行号和列号。之前的扫描方法中,图像的类型很重要。这里却没有什么不同,因为需要手动指定在自动查找中使用的类型。需要使用 cv::Mat::at() 函数:

cv::Mat& ScanImageAndReduceRandomAccess(cv::Mat& I, const uchar* const table)
{
    // accept only char type matrices
    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:
        {
            cv::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;
}

函数接收输入类型和坐标,并计算查询项的地址。然后返回对它的引用。当获得值时,可能是一个常量,而当用户设置值时,可能是非常量。作为调试模式下的一个安全步骤,程序会检查输入坐标是否有效且是否存在。如果不存在,则用户将在标准错误输出流上得到输出消息。与 release 模式中的有效方法相比,使用此方法的唯一区别是,对于图像的每个元素,将获得一个新的行指针,用于用户使用 C 运算符 [] 获取元素。

如果需要使用此方法对图像执行多个查找,则对于每个访问,都要输入类型和 at 关键字,这可能会很麻烦,而且很耗时。为了解决这个问题,OpenCV 有一个 cv::Mat_ 数据类型。这与 Mat 相同,在定义时需要通过查看数据矩阵来指定数据类型,但是相应的,可以使用运算符 () 快速访问项。为了更方便,可以很容易地从普通的 cv::Mat 数据类型转换为普通的 cv::Mat 数据类型。在上面函数的彩色图像的情况下可以看到这个示例用法。不过,需要注意的是,对于 cv::Mat::at 函数可以执行相同的操作(运行时速度相同)。


方法四 :核心功能 (The Core Function)

这是在图像中实现查找表修改的额外方法。在图像处理中,通常需要将所有给定的图像值修改为其他值。OpenCV 提供了一个修改图像值的功能,无需编写图像的扫描逻辑。用户可以使用核心模块的 cv::LUT() 函数。首先,需要构建查找表的 Mat 类型:

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

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

cv::LUT(I, lookUpTable, J);

性能差异 (Performance Difference)

为了得到最好的结果,编译程序并运行。为了使区别更清楚,使用了一个相当大(1600 * 900 ) 的图像。这里介绍的性能是针对彩色图像的。为了得到更精确的值,我们将函数调用中得到的值平均了 100 次。

5. OpenCV 4.2.0 使用 OpenCV 扫描图像、查找表和时间测量(OpenCV 官方文档翻译)_第2张图片

可以得出结论:尽量使用 OpenCV 中已有的函数。对于遍历图像矩阵的每个元素,最快的方法是 LUT 函数。这是因为 OpenCV 库是通过 Intel 线程构建块启用多线程的。不过,如果需要写一个简单的图像扫描则首选指针法。迭代器方法虽然更安全,但相应的速度也会更慢一些。在调试模式下,使用引用访问方法对全图像扫描成本最高。

完整代码

#include 

#include 
#include 
#include 
#include 

#define exampleImage "example_fig.jpg"

void ScanImageAndReduceC(cv::Mat& I, const uchar* const lookUpTable);
void ScanImageAndReduceIterator(cv::Mat& I, const uchar* const lookUpTable);
void ScanImageAndReduceRandomAccess(cv::Mat& I, const uchar* const lookUpTable);
void ScanImageAndReduceLUT(cv::Mat& I, const uchar* const table, cv::Mat& J);

int main(int argc, char** argv)
{
	cv::Mat src = cv::imread(cv::samples::findFile(exampleImage), cv::IMREAD_COLOR);

	int valueDivideWith = 10;
	uchar lookUpTable[256];
	for (int i = 0; i < 256; i++)
		lookUpTable[i] = (uchar)(valueDivideWith * (i / valueDivideWith));

	std::cout << std::endl;

	std::cout << "\tEfficient Way : " << std::endl;
	double t = (double)cv::getTickCount();
	for (int i = 1; i <= 100; i++)
		ScanImageAndReduceC(src, lookUpTable);
	t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();
	std::cout << "\tTimes passed in seconds : " << t/100 << std::endl << std::endl;

	std::cout << "\tIterator : " << std::endl;
	t = (double)cv::getTickCount();
	for (int i = 1; i <= 100; i++)
		ScanImageAndReduceIterator(src, lookUpTable);
	t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();
	std::cout << "\tTimes passed in seconds : " << t/100 << std::endl << std::endl;

	std::cout << "\tOn-The-Fly RA : " << std::endl;
	t = (double)cv::getTickCount();
	for (int i = 1; i <= 100; i++)
		ScanImageAndReduceRandomAccess(src, lookUpTable);
	t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();
	std::cout << "\tTimes passed in seconds : " << t/100 << std::endl << std::endl;

	std::cout << "\tLUT function : " << std::endl;
	cv::Mat dst;
	t = (double)cv::getTickCount();
	for (int i = 1; i <= 100; i++)
		ScanImageAndReduceLUT(src, lookUpTable, dst);
	t = ((double)cv::getTickCount() - t) / cv::getTickFrequency();
	std::cout << "\tTimes passed in seconds : " << t/100 << std::endl << std::endl;


	return 0;
}

void ScanImageAndReduceC(cv::Mat& I, const uchar* const lookUpTable)
{
	CV_Assert(I.depth() == CV_8U);

	int channels = I.channels();

	int nRows = I.rows;
	int nCols = I.cols * channels;

	if (I.isContinuous())
	{
		nCols *= nRows;
		nRows = 1;
	}

	uchar* p;
	for (int iRow = 0; iRow < nRows; ++iRow)
	{
		p = I.ptr<uchar>(iRow);
		for (int iCol = 0; iCol < nCols; ++iCol)
			p[iCol] = lookUpTable[p[iCol]];
	}

	return;
}

void ScanImageAndReduceIterator(cv::Mat& I, const uchar* const lookUpTable)
{
	CV_Assert(I.depth() == CV_8U);

	const int channels = I.channels();
	switch (channels)
	{
	case 1:
	{
		cv::MatIterator_<uchar> it, end;
		for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
			*it = lookUpTable[*it];
		break;
	}

	case 3:
	{
		cv::MatIterator_<cv::Vec3b> it, end;
		for (it = I.begin<cv::Vec3b>(), end = I.end<cv::Vec3b>(); it != end; ++it)
		{
			(*it)[0] = lookUpTable[(*it)[0]];
			(*it)[1] = lookUpTable[(*it)[1]];
			(*it)[2] = lookUpTable[(*it)[2]];
		}
	}

	default:
		break;
	}

	return;
}

void ScanImageAndReduceRandomAccess(cv::Mat& I, const uchar* const lookUpTable)
{
	CV_Assert(I.depth() == CV_8U);

	const int channels = I.channels();
	switch (channels)
	{
	case 1:
	{
		for (int iRow = 0; iRow < I.rows; ++iRow)
			for (int iCol = 0; iCol < I.cols; ++iCol)
				I.at<uchar>(iRow, iCol) = lookUpTable[I.at<uchar>(iRow, iCol)];
		break;
	}

	case 3:
	{
		cv::Mat_ < cv::Vec3b> _I = I;

		for(int iRow = 0; iRow < I.rows; ++iRow)
			for (int iCol = 0; iCol < I.cols; ++iCol)
			{
				_I(iRow, iCol)[0] = lookUpTable[_I(iRow, iCol)[0]];
				_I(iRow, iCol)[1] = lookUpTable[_I(iRow, iCol)[1]];
				_I(iRow, iCol)[2] = lookUpTable[_I(iRow, iCol)[2]];
			}

		I = _I;
		break;
	}
	}

	return;
}

void ScanImageAndReduceLUT(cv::Mat& I, const uchar* const table, cv::Mat& J)
{
	cv::Mat  lookUpTable(1, 256, CV_8U);
	uchar* p = lookUpTable.ptr();
	for (int i = 0; i < 256; ++i)
		p[i] = table[i];

	cv::LUT(I, lookUpTable, J);

	return;
}

你可能感兴趣的:(OpenCV)