目标
我们会寻求下列问题的答案:
l 如何单独扫过图像的每个像素?
l 如何存储 OpenCV 矩阵值?
l 如何衡量我们的算法的性能?
l 什么是查找表和为什么使用查找表?
我们测试用例
让我们先从一种简单的色彩减弱方法开始考虑。使用C 和 C++的无符号的字符类型矩阵项存储像素的通道最多可能有256个不同值。对于三个通道的图像,这可以在允许的方式下形成非常多颜色(确切地说有1600万)。处理使用那么多不同色调的颜色可能会严重影响我们的算法性能。但是,有时处理与他们相较少了很多的图象就足以获得相同的最终结果。
在此情况下,我们使颜色空间减少是常见的。这意味着我们以一个新的输入值划分当前的颜色空间,最终得到较少的颜色。例如每个值介于零和九之间设为0,在10到19之间就设置为10,以此类推。
当您在使用int值划分 uchar 时(无符号的字符-也可以视为介于 0 和 255 之间的值)结果的值也将 char。这些值可能只是 char 值。因此,任何分数将向下舍入。利用这一事实向上运算中的 uchar 域可表示为:
一种简单的颜色空间减少算法将由只是遍历图像矩阵的每个像素和在每个像素上应用此公式组成。我们做的除法和乘法运算毫无价值,而且这些操作是代价极其高的操作。如果可能的话,通过使用代价更低的操作,如几个减法和加法来避免他们是值得的,或在最好的情况下通过简单的赋值。此外,请注意我们只有有限的上操作的输入值。Uchar 体系的情况下,这值的确切值是256。
因此,对于较大的图像明智之举是在计算之前和期间分配的所有可能值只是通过使用查找表进行分配。 查找表是一个简单的数组(有一个或多个维度)为给定的输入的值保存最终输出值。其优点在于我们不需要计算,我们只需要读取结果。
我们的测试用例程序 (和这里介绍的示例) 将执行以下操作: 控制台命令行参数图像 (即也可以任意一种彩色或灰度-控制台行参数) 中的读取并应用给定的控制台命令行参数整数值来减少。在OpenCV中,现在逐像素遍历图像的像素的他们是三种主要方法。若要使事情更有趣,使每个图像扫描使用所有这些方法,和打印出花了多长时间。
您可以在这里下载完整的源代码或在OpenCV 的cpp 教程代码核心的部分的示例目录中查到。其基本的用法是:
how_to_scan_images imageName.jpg intValueToReduce [G]
最后一个参数是可选的。 如果加载给定图像中的灰度格式,否则使用RGB颜色的方式。 首要的事是计算查找表。
int divideWith; // 将输入的字符串转换成数字 - C++ style
stringstream s;
s << argv[2];
s >> divideWith;
if (!s)
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256];
for (int i = 0; i < 256; ++i)
table[i] = divideWith*(i/divideWith);
这里,我们首先使用c++的stringstream类将第三个命令行参数从文本转换为一个整数格式。然后然后我们用一个简单的查找(look)和上面的公式计算查找表。 这里没有OpenCV具体的东西。
另一个问题是我们如何衡量时间?那么OpenCV提供两个简单的函数来实现它:
getTickCount() 和 getTickFrequency()。前者返回系统CPU完成某些事件发出信号的次数(比如来自你启动你的系统这个事件)。后者返回每一秒你的系统CPU发出多少次信号。以此来计算两个操作之间使用的秒数就简单了,如:
double t = (double)getTickCount();
// 发生的事件 ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
如何在内存中存储图像矩阵?
正如你可能已经在我的Mat-基本图像容器教程中读到的那样,矩阵的大小取决于所使用的颜色系统。更准确的说,这取决于使用的通道数。灰度图像的情况下,我们有类似:
多通道图像的列包含通道的数量一样多的子列,例如在RGB色彩系统下:
请注意,通道的顺序是反的:是BGR而不是RGB。由于在许多情况下,内存很大可能足以以连续的方式在另一个后一行跟着一行存储数据行,创建一个长行。因为所有的东西都是一个空间接着下一个空间,这将有助于加快扫描过程。如果是这种情况,我们可以使用isContinuous() 函数来查看矩阵。请继续阅读下一部分的例子。
有效的方式
在性能方面,你敌不过经典C风格运算符[](指针)访问。 因此,我们推荐的最有效的完成任务的方法是:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// 只接受char类型矩阵
CV_Assert(I.depth() != sizeof(uchar));
int channels = I.channels();
int nRows = I.rows*channels;
int nCols = I.cols;
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;
}
这里,我们基本上只是获取一个指针,指向每一行的开始,然后遍历它直到它结束。 在特殊情况下, 矩阵是以一种持续的方式存储的我们只需要请求一次指针单和一路下来直到结束。 我们需要查看彩色图像:我们有三个通道,所以在每行中我们要遍历3倍多的项。
迭代(安全)方法
有效的方式,确保您遍历适量的 uchar 字段和跳过行之间可能产生的空白是你的责任。迭代器方法被认为是更安全的方式,因为它从用户接管这些任务。您需要做的就是访问图像矩阵的开头和结尾,然后只是增加迭代器的begin,直到到达end。若要获取指出通过迭代器使用的值 * 运算符 (添加在它之前)。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// 只接受char类型矩阵
CV_Assert(I.depth() != sizeof(uchar));
const int channels = I.channels();
switch(channels)
{
case 1:
{
Mat Iterator_<uchar> it, end;
for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)
*it = table[*it];
break;
}
case 3:
{
Mat Iterator_<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 简单迭代器您将只能够访问蓝色通道的值。
用返回的引用进行快速地址计算
最后这种方法并不被建议进行扫描。它已获得或以某种方式修改图像中的随机元素。其基本的用法是项目的指定您要访问的行和列数。你可能已经注意到我们之前的描方法中我们正在看图像是通过何种类型的是非常重要的。这里不同它没有作为在自动查找使用何种类型,您需要手动指定。您可以在下面的源代码 (+at() 函数的使用) 的灰度图像的情况下观察到这一点:
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// 只接受char类型矩阵
CV_Assert(I.depth() != sizeof(uchar));
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模式中使用该方法唯一不同的是,你将为每个图像元素所使用的 C 运算符 []获取新行的指针,然后通过该指针获取获取列的元素。
如果您需要使用这种方法查找多个图像将可能是麻烦的,耗时输入的类型和在访问的每个关键字;OpenCV 的 Mat_数据类型可以解决这个问题。与Mat相同,它需要额外地在定义是您需要根据你在数据矩阵查看什么来指定的数据类型,但是相应地,您可以使用operator()对项进行快速访问。若要使事情更好,很容易就可通过和常用的Mat数据类型相互转换。此示例使用,您可以看到的彩色图像上述函数的运行情况如下。不过,必须注意的是相同的操作 (具有相同的运行时速度) at() 函数也可以做到。这样写只是一种那些懒程序员为了少写几行代码的戏法。
Core模块中的函数
这是实现查找表修改图像中的额外的方法。因为图像处理时您想要替换所有给定的图像中值为其他值是很常见的, OpenCV 有一个函数,不需要修改你写的扫描图像过程。我们使用Core模块的LUT () 函数。首先我们生成查Mat的类型的找表:
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data;
for( int i = 0; i < 256; ++i)
p[i] = table[i];
最后调用这个函数(I是我们的输入图象而J是输出图像):
LUT(I, lookUpTable, J);
性能差异
为达到最佳效果编译该程序并运行依据您自己的CPU速度。为了更好地展示的时间上的差别使用了一个相当大的 (2560 X 1600) 图像。此处提供的演示图像是彩色的图像。更准确的值我已经平均了数百该函数的调用次接到的值。
Efficient Way Iterator On-The-Fly RA LUT function | 79.4717milliseconds 83.7201 milliseconds 93.7878 milliseconds 32.5759 milliseconds |
我们可以得出以下几个结论。可能的话,使用已的 OpenCV (而重新构建这些) 的功能,最快的方法是LUT()函数。这是因为 OpenCV 库是启用通过英特尔线程构建块的多线程。但是,如果您需要编写扫描简单的图像推荐使用指针的方法。迭代器肯定是一个更安全的方法,但是很慢。完整图像扫描使用快速引用的访问方法是在debug模式中代价最高的。在release模式下它有可能胜过迭代器方法,但是它一定会此为牺牲迭代器所具有的安全特质。
最后,你可能在YouTube频道的video posted上看到一个示例程序运行。