上篇教程:Mat - 基本图像容器
目标
在本篇教程中,我们将寻求以下问题的答案:
- 如何遍历图像的每个像素?
- OpenCV 如何存储矩阵的值?
- 如何衡量我们算法的性能?
- 什么是查询表,为什么要使用它?
测试用例
让我们来考虑一个简单的颜色减少算法。通过使用 C 或 C++ 的 unsigned char 类型来存储图像矩阵的像素值,每个颜色通道可以具有多达 256 个不同的值。对于一个三通道图像,这种方法会形成海量的颜色(确切地说多达 1600 多万种)。使用这么多的颜色可能会严重影响我们的算法性能。然而,有时只需处理少量的颜色即可获得相同的最终结果。
通常,我们会缩小颜色空间。这意味着我们以一个新的输入值划分当前的颜色空间,最终得到较少的颜色。例如,0 到 9 之间的每个值都取新值 0,对 10 到 19 之间的值,取新值 10,依此类推。
当你使用 int 值来划分 uchar(unsigned char - 也就是 0 到 255 之间的值)时,结果也将是char。这些值可能只是 char 值。因此,任何分数都要向下舍入。利用这一事实,uchar 域中的上层操作可以表示为:
一个简单的颜色空间缩简算法只需要遍历一个图像矩阵的每个像素并应用这个公式。值得注意的是我们做的是除法和乘法运算。这些操作对于一个系统来说是非常昂贵的。如果可能的话,使用一些更便宜的操作来避免它们是值得的,比如一些减法、加法,或者最好是一个简单的赋值。此外,请注意,对于上面的操作,我们只有有限数量的输入值,对于uchar来说,它是256。
因此,对于较大的图像,明智的做法是事先计算所有可能的值,并在赋值期间使用查找表进行赋值。查询表是一个简单的数组(具有一个或多个维度),对于给定的输入值变体,它持有最终的输出值。它的优点在于我们不需要做计算,我们只需要读出结果。
我们的测试用例程序(以及这里给出的示例)将执行以下操作:读取控制台命令行参数指定的图像(也可以用命令行参数设置彩色或灰度),并使用给定的命令行参数值进行缩减。在OpenCV中,目前主要有三种逐像素遍历图像的方法。为了让事情变得更加有趣,我们将使用所有这些方法对每张图像进行扫描,并打印出所需的时间。
你可以在下载完整的源代码,然后在OpenCV的样例目录核心部分的cpp教程代码中找到它。它的基本用法是:
how_to_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();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
图像矩阵是如何存储在内存中的?
正如你已经在我的《Mat - 基本图像容器》教程中读到的,矩阵的大小取决于使用的颜色系统。更准确地说,这取决于所使用的通道的数量。对于灰度图像,我们有:
对于多通道图像,每一列包含与通道数一样多的子列。例如,在BGR颜色系统的情况下:
注意,通道的顺序是相反的:BGR而不是RGB。因为在许多情况下,内存足够大,可以以连续的方式存储行,即行可以一个接一个地存储,从而创建一个长行。因为所有的东西都在一个地方,一个接着一个,这可能有助于加快扫描过程。我们可以使用cv::Mat::isContinuous()函数来询问矩阵是否存在这种情况。继续下一部分以查找示例。
高效的方式
在性能方面,用经典的C风格操作符[](指针)来访问具有无可匹敌的优势。因此,我们推荐的最有效的操作方法是:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// 只接受 uchar 类型的矩阵
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)
{
// 只接受 uchar 类型的矩阵
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迭代器,那您将只能访问蓝色通道值。
实时引用访问方式
最后一种方法不推荐用来做扫描。它通常用于需要随机访问或修改图像中像素值的情景。它的基本用法是指定要访问的项的行号和列号。在我们早期的扫描方法中,你已经可以通过我们所使用的图像类型观察到这一点。这里没有什么不同,只是这里您需要手动指定在自动查找中使用的类型。您可以在以下源代码的灰度图像中体会到这一点(cv::Mat::at()函数的用法):
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// 只接受 uchar 类型的矩阵
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;
}
这些函数获取您的输入类型和坐标,并动态计算查询项的地址。然后返回对它的引用。当你得到这个值时,它可能是一个常数当你设置这个值时,它可能是非常数。作为调试模式下的一个安全步骤,只执行检查输入坐标是否有效且是否存在。如果不是这种情况,您将在标准错误输出流上得到一个很好的输出消息。与发布模式中的有效方式相比,使用它的唯一区别是对于图像的每个元素,您将获得一个新的行指针,用于我们使用C运算符 [] 来获取列元素。
如果您需要使用此方法对图像进行多次查找,那么为每次访问输入type和at关键字可能会很麻烦,而且很耗时。为了解决这个问题,OpenCV有一个cv::Mat_ 数据类型。它与Mat相同,只是需要在定义时指定数据类型,即通过什么类型来查看数据矩阵,但是作为回报,可以使用运算符()快速访问项。为了使事情变得更好,它可以很容易地从通常的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);
性能差异
为了得到最好的结果,编译程序并在你的机器上运行它。为了使区别更清楚,我使用了一个相当大的(2560 X 1600)图像。这里展示的性能是针对彩色图像的。为了得到更精确的值,我将函数调用得到的值取了100次平均值。
我们可以得出几个结论。如果可能的话,使用OpenCV的内建函数(而不是重新创建这些函数)。最快的方法是LUT函数。这是因为OpenCV库是通过Intel线程构建块启用多线程的。不过,如果需要编写一个简单的图像扫描,最好使用指针方法。迭代器是一种更安全的选择,但是相当慢。在调试模式下,使用实时引用访问方法进行全图像扫描是最昂贵的。在发布模式中,它可能会超过迭代器方法,也可能不会,但是它肯定会牺牲迭代器的安全性。
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#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;
}