- 操作系统:Windows 7 SP1
- 编译器:mingw64 g++ 8.1.0
- 编辑器:Emacs 30.1
- 第三方库:OpenCV 4.1.2.0
- 对 Emacs 有基本了解
目标
- 浏览图像的每个像素
- OpenCV 矩阵值存储
- 查找表
测试用例
- 减色方法。有时用最少的颜色可以得到相同的结果
- 例如:0-9 之间取 0,10-19 之间取 10,以此类推
- 当将 uchar 除以一个整数,任何小数向下舍入,结果也将是 char,因此:
I n e w = ( I o l d 10 ) ∗ 10 I_{new} = (\frac{I_{old}} {10}) * 10 Inew=(10Iold)∗10 - 简单的颜色空间降维算法应用此公式
- 对于大图像,事先做好所有可能的值,并在任务期间查找表(查找表是简单的数组,具有一个或多个维度),优点是不需要计算
- 下面的示例将:
- 读取命令行参数传递的图像(可以是彩色或灰度图像)
- 根据命令行参数的整数值应用图像缩减
- 在 OpenCV 中,目前有三种主要的方法逐像素遍历图像
- 使用每种方法对图像进行扫描并打印出所需的时间
- 源码在这里下载
how_to_scan_images imageName.jpg intValueToReduce [G]
- 最后的参数是可选的,如果提供,以灰度格式加载,否则将使用 BGR 颜色空间
int divideWith = 0; stringstream s; s << argv[2]; s >> divideWith; if (!s || ! divideWith) { cout << "输入的除数无效!" << endl; return -1; } uchar table[256]; for (int i = 0; i < 256; ++i) table[i] = (uchar) (i / divideWith * (i / divideWith ));
- 使用 C++ stringstream 将命令行的第三个参数从文本转换为整数格式,这没有 OpenCV 特定代码
- OpenCV 提供了两简单的函数实现时间(性能)的测量
double t = (double) getTickCount(); // do something ... t = ((double)getTickCount() - t) / getTickFrequency(); cout << "Times passed in seconds: " << t << endl;
- cv::getTickCount() 从某个事件返回 CPU 的脉冲数
- cv::getTickFrequency() 返回 CPU 在一秒内发出多少脉冲
图像矩阵如何存储在内存中?
在Mat-图像容器中说过,矩阵的大小取决于使用的颜色系统。准确地说取决于使用的通道数
-
灰度
-
多通道
这是 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 * 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;
}
- 在这里,只是获取每行的起始指针,并逐行遍历直到结束。在矩阵以连续方式存储的情况下,只需请求一次指针并一
直遍历到结束。彩色图像有三个通道,是普通情况的三倍 - 另一种方法,先判断图像是否加载完成。如果是灰度图像:
uchar* p = I.data; for( unsigned int i = 0; i < ncol*nrows; ++i) ,*p++ = table[*p];
(安全)迭代方法
只需要询问图像矩阵的开始和结束,使迭代器指向值,用*运算符
Mat& ScanImageAndReduceIterator(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:
{
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;
}
需要记住,OpenCV 的迭代器会遍历列,并自动跳到下一行
动态地址计算与引用返回
Mat& ScanImageAndReduceRandomAccess(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:
{
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;
}
如果对图像进行多次查找,使用这种方法可能会很麻烦
- 核心功能
在实际中,可能希望将给定图像的所有值修改为其他值。OpenCV 提供了一个函数来修改图像值,无需编写图像扫描逻辑
使用核心模块的 cv::LUT() 函数
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
#+END_SRC
#+BEGIN_SRC C
LUT(I, lookUpTable, J); // I 为输入图像,J 为输出图像
性能差异
使用的图像大小为(2560 x 1600 ),函数调用一百次得到的结果取平均值
Method | Time |
---|---|
Efficient Way | 79.4717 milliseconds |
Iterator | 83.7201 milliseconds |
On-The-Fly RA | 93.7878 milliseconds |
LUT function | 32.5759 milliseconds |
结论
- 尽可能的用 OpenCV 提供的方法
- 最快的方法是 LUT 函数
- OpenCV 库通过 Intel 的线程构建块(Threaded Building Blocks)启用多线程
- 在实际中,真要手写请自己斟酌:指针方法、迭代器安全法、带引用返回的即时地址计算