Chapter_02 如何扫描查看图像,查询表以及Opencv中的时间度量

文章目录

      • 一. 本章目标
      • 二. 我们的测试用例
      • 三. 我们在内存中是如何保存图像矩阵的
      • 四. 扫描图像的有效方法
      • 五. 迭代器(安全)方法
      • 六. 使用引用返回的动态地址计算
      • 七. 核心方法
      • 八. 性能差异

一. 本章目标

我们将回答以下的问题:

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

二. 我们的测试用例

让我们考虑一个简单的颜色还原方法.通过使用无符号char 类型的C/C++来存储矩阵,一个像素通道可以有256个不同的值.对于一个三通道的图像,这可以形成太多的颜色(确切的来说是1600万).处理如此多的色度可能胡给我们的算法性能带来沉重的打击.然而,有时使用更少的数量就足以获得相同的最终结果.

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

当你用一个int类型的数值除以一个uchar(unsigned char - aka的值在0到255之间)值的时候,结果也是char类型.这些值只能是char值.因此任何分数都将向下四舍五入.利用这一事实,uchar域的上层操作可以表示为:

一个简单的颜色空间缩减算法包括通过图像矩阵的每个像素并应用这个公式.值得注意的是,我们做了除法和乘法运算.这些操作对系统来说是非常费事的.如果可能的话,值得使用一些更省事的操作,比如一些减法,加法,或者在最好的情况下使用简单的赋值操作来避免它们.此外,请注意,对于上面的操作,只有有限数量的输入值.在uchar系统中,准确来说是256.

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

我们的测试用例程序(以及下面的代码示例)将执行以下操作:读取作为命令函参数传递的图像(可以是颜色或灰度图像),并使用给定的命令行参数整数值应用缩减.在Opencv中,目前有三种主要的逐像素处理图像的方法.为了让事情变得更有趣,我们将使用每种方法来处理图像,并带你出所需要的时间.
这里面加一个知识点,如何在visul_sutio 中配置main()函数的参数
点击项目,试图->属性页->调试->命令参数
Chapter_02 如何扫描查看图像,查询表以及Opencv中的时间度量_第1张图片

下面是程序示例:

#include 
#include 
#include "opencv2/imgcodecs.hpp"
#include 
#include 
#include 

using namespace std;
using namespace cv;

static void help()
{
    cout
        << "\n--------------------------------------------------------------------------" << endl
        << "This program shows how to scan image objects in OpenCV (cv::Mat). As use case"
        << " we take an input image and divide the native color palette (255) with the " << endl
        << "input. Shows C operator[] method, iterators and at function for on-the-fly item address calculation."
         << endl
        << "Usage:" << endl
        << "./how_to_scan_images   [G]" << endl
        << "if you add a G parameter the image is processed in gray scale" << endl
        << "--------------------------------------------------------------------------" << endl
        << endl;
}

Mat &ScanImageAndReduceC(Mat &I, const uchar *table);
Mat &ScanImageAndReduceIterator(Mat &I, const uchar *table);
Mat &ScanImageAndReduceRandomAccess(Mat &I, const uchar *table);


//! [scan-c]
Mat &ScanImageAndReduceC(Mat &I, const uchar *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 (int i = 0; i < nRows; i++)
    {
        p = I.ptr<uchar>(i);
        for (j = 0; j < nCols; j++)
        {
            p[j] = table[p[j]];
        }
    }
    return I;
}
//! [scan-c]


//! [scan-iterator]
Mat &ScanImageAndReduceIterator(Mat &I, const uchar * table)
{
    // accept only char type matrices
    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;

}
//! [scan-iterator]

//! [scan-random]
Mat &ScanImageAndReduceRandomAccess(Mat &I, const uchar *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;
}
//! [scan-random]

int main(int argc,char* argv[])
{
    help();
    if (argc < 3)
    {
        cout << "Not enough parameters" << endl;
        return -1;
    }

    Mat I, J;
    if (argc == 4 && !strcmp(argv[3], "G"))
    {
        I = imread(argv[1], IMREAD_GRAYSCALE);
    }
    else
    {
        I = imread(argv[1], IMREAD_COLOR);
    }

    if (I.empty())
    {
        cout << "The image" << argv[1] << "could not be loaded." << endl;
        return -1;
    }

    //! [dividewith]
    int divideWith = 0; // convert our input string to number
    stringstream s;
    s << argv[2];
    s >> divideWith;
    if (!s || !divideWith)
    {
        cout << "Invalid number entered for dividing." << endl;
        return -1;
    }

    uchar table[256];
    for (int i = 0; i < 256; i++)
    {
        table[i] = (uchar)(divideWith * (i / divideWith));
    }
    // ![dividewith]
    const int times = 100;
    double t;

    t = (double)getTickCount();
    for (int i = 0; i < times; i++)
    {
        cv::Mat clone_i = I.clone();
        J = ScanImageAndReduceC(clone_i, table);
    }
    // getTickCount()函数,该函数返回的值为自某一时刻(比如计算机启动)开始,计算机总共经过的tick的次数.
    // 其需要结合getTickFrequency()函数使用,getTickFrequency()返回的是CPU在一秒中内会发出的tick的次数.
    // 所以这里两个函数的结果相除得到的就是计数开始到此刻位置经过的秒数,再乘以1000就是毫秒数,最后又除以100
    //,表示的是单次的时间 最终的单位也是ms.
    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;

    cout << "Time of reducing with the C operator [](averaged for" << times << " runs): " << 
        t << " milliseconds." << endl;

    t = (double)getTickCount();

    for (int i = 0; i < times; i++)
    {
        cv::Mat clone_i = I.clone();
        J = ScanImageAndReduceIterator(clone_i, table);
    }
    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;
    cout << "Time of reducing with the iterator (averated for" << times << " runs): " <<
        t << "milliseconds." << endl;

    t = (double)getCPUTickCount();
    for (int i = 0; i < times; i++)
    {
        cv::Mat clone_i = I.clone();
        ScanImageAndReduceRandomAccess(clone_i, table);
    }

    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;
    cout << "Time of reducing with the on-the-fly address generation - at function 
    (averaged for" << times << " runs): " << t << "milliseconds." << endl;


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

    t = (double)getTickCount();
    for (int i = 0; i < times; i++)
    {
        //! [table-use]
        LUT(I, lookUpTable, J);
    }

    t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
    t /= times;
    cout << "Time of reducing with the LUT function (averaged for" << times << " runs): " 
        << t << "milliseconds." << endl;
    return 0;
}

结果:
Chapter_02 如何扫描查看图像,查询表以及Opencv中的时间度量_第2张图片

你可以通过如下的方式运行程序:

how_to_scan_images imageName.jpg intValueToReduce G

最后一个参数是可选的.如果给定图像以灰度格式加载,否在使用BGR颜色空间.首先要计算查找表

int divideWith = 0; // convert our input string to number - C++ type
stringstream s;
s << argv[2];
s >> dividewith;
if(!s || !dividewith)
{
	cout << "Invalid number entered for dividing. " << endl;
	return -1;
}
uchar table[256];
for(int i = 0; i < 256;i++)
{
	table[i] = (uchar)(dividewith * (i / dividewith));
}

这里,我们首先使用C++ stringstream类将第三个命令行参数从文本转换为整数格式.然后我们使用一个简单的表格和上面的公式来计算查找表.这里没有Opencv特定的东西.

另外一个问题是我们如何测量时间?Opencv提供了两个简单的函数来实现,cv::getTickCount()cv::getTickFrequency().第一个函数返回系统CPU在某个事件(比如系统启动之后)中的Tick.第二个函数返回CPU在一秒钟的发出的tick数.因此测量两个操作之间的时间间隔很容易:

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

三. 我们在内存中是如何保存图像矩阵的

我们可以在Mat-基本图像的容器教程中读到,矩阵的大小取决于所使用的颜色系统.更准确的来说,它取决于所使用的通道数据量.在灰度图像的情况下,我们有这样的东西:

Chapter_02 如何扫描查看图像,查询表以及Opencv中的时间度量_第3张图片
对于多通道的图像,列包含与通道数相同的子列.以BGR颜色系统为例:

注意,通道的顺序是相反的:BGR而不是RGB.因为在多数的情况下,内存大到足以连续的方式存储行,所以行可能是一个接一个的跟着,创建一个长行.因为所有的东西都在同一个地方,一个接一个,这可能有助于加快扫描过程.我们使用cv::Mat::isContinuous()函数来判断矩阵是否存在这种情况.我们将在下一节用例子说明:

四. 扫描图像的有效方法

在性能方面,你无法击败经典的C风格操作符[](指针)访问,因此,我们可以推荐的最有效的赋值方法是:

Mat& ScanImageAndReduceC(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;
	if(I.isContinuous())
	{
		nCols *= nRows;
		nRows = 1;
	}
	int i,j;
	uchar* p;
	for(int i = 0; i < nRows;i++)
	{
		p = I.ptr<uchar>(i);
		for(j = 0;j < nCols;j++)
		{
			p[j] = table[[pj]]
		}
	}
}

这里我们只需要获取一个指向每行开始的指针,然后遍历它,直到它结束.在矩阵以连续方式存储的特殊情况下,我们只需要请求一次指针,然后一直到结束.我们需要注意彩色图像:我们有三个通道,所以我们需要没行中通过三倍多的项.

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

uchar* p = I.data;
for(unsigned int i = 0;i < ncols * nrows; i++)
{
	*p++ = talbe[*p];
}

你会得到相同的结果.但是,这段代码以后阅读起来会困难的多.如果在这里你有一些更高级的技术,它将会变得很困难.此外,在实践中,你会发现将获取相同的性能结果(因为大多数的现代编译器可能会自动为你实现这个小的优化技巧)

五. 迭代器(安全)方法

相比于确保传递正确数量的uchar字段并跳过行之间可能发生的间隙的有效方法由您负责.itrator方法被认为是一种更安全的方法,因为它从用户手中接管了这些任务.你所需要做的就是询问图像矩阵的开始和结束,然后增加开始迭代器,直到到达结束.要获得迭代器所指向的值,使用*操作符(将其添加到迭代器的前面)

Mat& ScanImageAndReduceInterator(Mat& I,const uchar* const table)
{
	// accept only char type matrices
	CV_Assert(I.depth() == CV_8U);
	const int channels = I.chanels();
	switch(channels)
	{
		case 1:
			{
				MatItrator_<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] = talbe[(*it)[1]];
					(*it)[2] = talbe[(*it)[2]];
				}
			}
	}
	return I;
}

对于彩色图像,每列有三个uchar项.这可以被认为是uchar项的小向量,在Opencv中被命名为Vec3b.要访问第n个子列,我们使用简单的operator[]访问.重要的是要记住,Opencv迭代器编列列并自动跳到写一行.因此,对于彩色图像,如果使用简单的uchar迭代器,它只能访问蓝色通道的值.

六. 使用引用返回的动态地址计算

最后一种方法不推荐用于遍历.它被用来获取或者修改图像中的随机元素.它基本的用法是指定要访问的项目的行号和列号.在我们早期的遍历方法中,你可能已经注意到,我们正在操作的图像是什么类型的是非常重要的.这里没有什么不同,因为你需要再自动查找手动指定要使用的类型.线面的源代码(cv::Mat::at()函数的用法)的灰度图像中可以观察到这一点:

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

该函数接收你输入的类型和坐标,并计算查询项的地址.然后返回对该对象的引用.当你获取值的时候,它可能是一个常数,而你设置值的时候,它可能是非常数.作为在调试模式下的一个安全的步骤,*号操作符会执行一个检查,以确保你输入的坐标是有效的,并且确实存在.如果不是这种情况,你将在标准错误输出流上得到一个很好的输出消息.与release模式下的有效方法相比,使用这个方法的唯一区别是,对于图像中的每个元素,你都将获取一个新的行指针,二我们使用C操作符[]来获取列元素.

如果你需要使用这种方法对一个图像进行多次查找,那么为每次访问输入typeat关键字可能是麻烦和耗时的.为了解决这个问题,Opencv有一个cv::Mat_数据类型.它与Mat相同,只是在定义的时候需要通过查看数据矩阵来指定数据类型,但作为回报,你可以使用操作符()来快速访问项.更棒的是,它可以很容易地转换为cv::Mat数据类型.你可以在上面的彩色图像中看到这个函数的示例用法.然而,需要注意的是,同样的操作(以相同的运行速度)也可以用cv::Mat::at函数完成.这只是一个为懒惰的程序员编写的小技巧.

七. 核心方法

这是在图像中实现查找表修改的一种额外的方法.在图像处理中,将所有给定的图像值修改为其他值是很常见的.Opencv提供了修改图像值的功能,二不需要边学图像的遍历逻辑.我们使用核心模块的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);

八. 性能差异

为了得到最好的结果,请自己编译并运行程序.为了使得差异更明显,我们使用了一个相当大的(2560*1600)图像.这里介绍的性能是用于彩色图像的.为了得到一个更精确的值,我们获取100次函数调用的平均值.
Chapter_02 如何扫描查看图像,查询表以及Opencv中的时间度量_第4张图片
我们可以得出几点结论.如果可能的话,使用Opencv已有的函数(而不是重新发明这些函数).最快的方法是LUT函数.这是因为Opencv库是通过英特尔线程构建模块实现多线程的.然而,如果你需要写一个简单的图像扫描首选指针方法.迭代器是一个更安全的选择,但是相当的慢.在调试模式下,全图像扫描使用动态引用访问方法是最费时的.在release模式下,它可能会击败迭代器方法,也可能不会,但是它肯定会牺牲迭代器的安全特性.

你可能感兴趣的:(opencv,python,c++)