官方文档链接:https://docs.opencv.org/4.2.0/db/da5/tutorial_how_to_scan_images.html
为了实现简单的颜色还原方法,可以通过使用 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=(10Iold∗10)
一个简单的颜色空间压缩算法即如上所示,对图像矩阵的每个像素应用上述公式。值得注意的是,上述公式做了除法和乘法运算。对于一个系统来说,这些操作是非常昂贵的。如果可能的话,通过使用一些更简单的操作(如一些减法、加法或在最好的情况下是简单的赋值)来避免执行乘除法操作。此外,需要注意的是,我们只有有限的上限操作的输入值,对于 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;
图像矩阵的大小取决于使用的颜色系统。更确切地说,取决于使用的通道数量。在灰度图像中,图像矩阵形式如下:
对于多通道图像,列包含的子列数与通道数相同。例如,对于 BGR 颜色系统:
注意:通道的顺序是相反的:BGR 而不是 RGB。因为在许多情况下,内存足够大,可以连续地存储行,行可以一个接一个地跟随,从而创建一个较长的行。因为所有的东西都在一个地方,一个接一个,有助于加快扫描过程。可以使用 cv::Mat::isContinuous() 函数来询问矩阵是否是这种情况。
在性能方面,无法超过经典的 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];
可以得到同样的结果。
在这种情况下,可以跳过指定数量的 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 迭代器,则只能访问蓝色通道值。
最后一种方法不推荐用于扫描。它是为了获取或修改图像中的随机元素。它的基本用法是指定要访问的项的行号和列号。之前的扫描方法中,图像的类型很重要。这里却没有什么不同,因为需要手动指定在自动查找中使用的类型。需要使用 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 函数可以执行相同的操作(运行时速度相同)。
这是在图像中实现查找表修改的额外方法。在图像处理中,通常需要将所有给定的图像值修改为其他值。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);
为了得到最好的结果,编译程序并运行。为了使区别更清楚,使用了一个相当大(1600 * 900 ) 的图像。这里介绍的性能是针对彩色图像的。为了得到更精确的值,我们将函数调用中得到的值平均了 100 次。
可以得出结论:尽量使用 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;
}