目标

我们将寻求以下问题的答案:

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

我们的测试案例

让我们考虑一种简单的色彩还原方法。通过使用无符号 char C 和 C++ 类型来存储矩阵项,一个像素通道最多可以有 256 个不同的值。对于三通道图像来说,这可能会产生过多的颜色(准确地说,是 1600 万种)。处理如此多的色调可能会严重影响我们的算法性能。不过,有时只需使用较少的色调就能获得相同的最终结果。

在这种情况下,我们通常会缩小色彩空间。这意味着我们将色彩空间的当前值与新的输入值相除,最终得到更少的颜色。例如,0 到 9 之间的每一个值都会变成新值 0,10 到 19 之间的每一个值都会变成新值 10,以此类推。

如果将一个 uchar(无符号字符,又称 0 至 255 之间的值)值除以一个 int 值,结果也将是字符。这些值只能是 char 值。因此,任何分数都将向下舍入。利用这一事实,uchar 域中的上运算可以表示为

opencv测保险 opencv测量_计算机视觉

一个简单的色彩空间缩减算法只需通过图像矩阵的每个像素并应用此公式即可。值得注意的是,我们要进行除法和乘法运算。这些运算对系统来说非常昂贵。在可能的情况下,我们应该使用更便宜的运算来避免这些运算,比如一些减法、加法运算,或者最好是简单的赋值运算。此外,需要注意的是,上运算的输入值数量有限。就 uchar 系统而言,确切地说是 256 个。

因此,对于较大的图像,明智的做法是事先计算所有可能的值,然后在赋值时使用查找表进行赋值。查找表是一个简单的数组(有一个或多个维度),对于给定的输入值变化,它可以保存最终的输出值。它的优势在于我们无需进行计算,只需读取结果即可。

我们的测试用例程序(以及下面的代码示例)将执行以下操作:读取作为命令行参数传递的图像(可以是彩色或灰度图像),并使用给定的命令行参数整数值进行还原。在 OpenCV 中,目前有三种逐像素处理图像的主要方法。为了增加趣味性,我们将使用每种方法对图像进行扫描,并打印出扫描所需的时间。

你可以在这里下载完整的源代码,或者在 OpenCV 的 samples 目录中核心部分的 cpp 教程代码中查找。其基本用法如下

how_too_scan_images imageName.jpg intValueToReduce [G]

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

int divideWith = 0; // 将输入字符串转换为数字--C++ 风格
 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 从某个事件(如启动系统后)开始的滴答次数。第二个函数返回 CPU 在一秒钟内发出滴答声的次数。因此,测量两个操作之间的时间间隔非常简单:

double t = (double)getTickCount();
// 做一些事情...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;

图像矩阵如何存储在内存中?

正如您在 Mat - 基本图像容器

opencv测保险 opencv测量_OpenCV_02

对于多通道图像,列中包含的子列数量与通道数相同。例如 BGR 颜色系统:

opencv测保险 opencv测量_opencv_03

请注意,通道的顺序是相反的: BGR 而不是 RGB。因为在很多情况下,内存容量足以以连续的方式存储行,所以行可能会一个接一个,形成一个长行。由于所有内容都集中在一个地方,一个接一个,这可能有助于加快扫描过程。我们可以使用 cv::Mat::isContinuous()

高效方法

就性能而言,经典的 C 风格运算符[](指针)访问是无法比拟的。因此,我们推荐最有效的赋值方法:

Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
 // 只接受字符类型矩阵
 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 对象的 data 数据成员返回第一行第一列的指针。如果该指针为空,则表示该对象中没有有效输入。检查图像加载是否成功的最简单方法就是检查这个指针。如果存储是连续的,我们可以用它来查看整个数据指针。如果是灰度图像,则如下所示

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

你会得到相同的结果。不过,这段代码以后读起来会困难很多。如果你有一些更高级的技术,那就更难了。此外,在实践中,我观察到你会得到相同的性能结果(因为大多数现代编译器可能会自动为你完成这个小小的优化技巧)。

迭代器(安全)方法

在使用高效方法时,您有责任确保传递正确数量的 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 iterator,则只能访问蓝色通道值。

通过返回引用进行即时地址计算

最后一种方法不推荐用于扫描。该方法用于获取或修改图像中的随机元素。其基本用法是指定要访问项目的行和列编号。在我们前面的扫描方法中,你已经注意到,通过什么类型来查看图像是很重要的。这里也不例外,因为您需要手动指定自动查找时使用的类型。您可以通过以下源代码(使用 + 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.at<uchar>(i,j) = table[I.at<uchar>(i,j]] break
 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] = 表格[_I(i,j)[2]];
 }
 I = _I;
 break;
 }
 }
 return I;
}

该函数接收输入类型和坐标,计算出查询项的地址。然后返回一个引用。在获取值时,它可能是一个常数,而在设置值时,它可能是一个非常数。作为仅在调试模式下的一个安全步骤*,会对输入坐标是否有效和是否存在进行检查。如果不存在,你将在标准错误输出流中看到一条很好的输出信息。与释放模式下的高效方法相比,使用这种方法的唯一区别是,图像的每个元素都会得到一个新的行指针,我们使用 C 运算符[]来获取列元素。

如果您需要使用这种方法对一幅图像进行多次查询,那么为每次访问输入类型和 at 关键字可能会非常麻烦和耗时。为了解决这个问题,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 X 1600)的图像。这里显示的是彩色图像的性能。为了获得更精确的数值,我对调用函数百次后得到的数值进行了平均。

方法

时间

高效方法

79.4717 毫秒

迭代器

83.7201 毫秒

实时 RA

93.7878 毫秒

LUT 函数

32.5759 毫秒

我们可以得出一些结论。如果可能,请使用 OpenCV 已有的函数(而不是重新发明这些函数)。最快的方法是 LUT 函数。这是因为 OpenCV 库通过英特尔线程积木(Intel Threaded Building Blocks)支持多线程。不过,如果需要编写简单的图像扫描,最好还是使用指针方法。迭代器是一种更安全的方法,但速度相当慢。在调试模式下,使用即时引用访问方法进行全图像扫描的成本最高。在发布模式下,它可能会击败迭代器方法,但肯定会牺牲迭代器的安全特性。