如何扫描图像、查询表以及OpenCV的时间测量
一、目标
接下来主要解决下面几个问题:
l 如何访问图像的每一个像素?
l OpenCV矩阵的值是如何储存的?
l 如何衡量我们算法的性能?
l 什么是查询表以及为什么我们要使用它?
二、测试案例
让我们思考一个简单的减少色彩的方法。通过使用unsigned char类型的数据来作为矩阵数据的存储,一个通道的像素可能有最多256种不同的值,对于一幅三通道的图像,这个值就可能更大了。在如此多的的颜色背景下会给我们的算法性能带来很不好的影响。然而,有时候我们可能仅仅用最少的颜色也足够取得同样的效果。
在这种情况下我们通常会减少颜色空间。这就意味着我们将颜色空间现在的值再除以一个新输入的值来得到更少的颜色。
一般的灰度图像有256个灰度级,而有时我们并不需要这么精确的灰度级(严重影响运算时间),比如黑白图像。这意味着我们以一个新的输入值划分当前的颜色空间,比如灰度到黑白色,将0~127灰度值直接赋值0,128~255赋值1,最终得到较少的黑白两色。查找表就扮演着这种降低灰度级而提高运算速度的角色。量化前和量化后的灰度值可以用下面的表达式表示:
其中Q表示量化级别,如取10,表示:灰度值1-10用灰度值1表示,灰度值11-20用灰度值11表示,以此类推!
上面公式代价就是要对图片遍历的每个灰度值都计算一次。因此,本着程序设计中的“以空间换时间”的基本算法策略,引入查找表,查找表就是将0~255个灰度值量化后的结果提前计算好存储在一个表中.
因此,对于很大的图像来说,明智的做法是先尽可能地计算所有可能的值,然后就直接通过查询表进行赋值操作。查询表是一个简单的数组(一维或多维),这个数组对于一个给定输入的值其保存最终的输出值。它的特点就是我们不需要去做计算,我们要做的仅仅是去读取结果就可以了。
下面的示例程序将这样做:从命令行读入图像数据,并且对其应用对于整数值的颜色空间减少。(示例程序可以打开openCV的安装文件中的samples中的Tutorials中的core文件夹里面的How to scan images)。
void CreateLookupTable(Mat& table, uchar quan_val)
{
table.create(1,256,CV_8UC1);
uchar *p = table.data;
for(int i = 0; i < 256; ++i)
{
p[i] = quan_val*(i/quan_val);
}
}
上面的程序就是对其应用公式进行查询表的制作。有了查找表后,要对图像中的像素灰度值进行替换.
那么我们如何来测量时间呢?OpenCV提供了两个简单的函数来达到测量时间的目的,一个是getTickCount(),一个是getTickFrequency()。第一个返回一个你系统CPU相应一个特定事件的脉冲数,第二个返回一秒钟你的系统CPU发出多少个脉冲。所以以秒来测量两个操作之间的时间是很容易的,如下:
double t = (double)getTickCount();
// do something ...
t = ((double)getTickCount() - t)/getTickFrequency();
cout << "Times passed in seconds: " << t << endl;
图像矩阵在内存中怎么储存
正如上一节Mat中讲到的,矩阵的大小取决于所使用的颜色系统。
更准确地说,矩阵的大小取决于使用的通道数。在一个灰度图像中的矩阵数据是这样的:
对于多通道图像,有多少个通道,列就包含多少个子列。例如在BGR颜色空间中,图像数据就是这样的:
因为在很多情况下,内存都足够大以一种连续的方式来储存行,行可能是一行接着一行的,只需要创建一个很长的行就可以了(先这样理解吧)。因为任何事以一种连续的方式可能都将有利于我们加速扫描处理。我们可以使用isContinuous()函数来确定矩阵是否是连续的。
l 一种有效的方式
当考虑到性能的时候我们就不得不想到经典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;
}
在这里我们只是获得了每一行开始的指针,然后依次访问每一行,特殊的情况下,矩阵的所有行都是连续的,这时候我们实际上只循环了一次就可以直接访问完整个矩阵。
访问矩阵数据还有另外一种方法:Mat对象的data数据成员返回第一行第一列的指针。如果这个指针是空则说明你的输入是空。检查data是查看自己的图像是否载入成功的最简单的方式。如果矩阵存储是连续的我们可以使用这个data指针访问完整个数据。例如,灰度图像中就像下面这样:
Uchar* p=I.data;
For(unsigned int i=0;i<ncol*nrows;++i)
*p++=table[*p];
l 一种迭代器方法
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的迭代器中,它一直扫面一列,并且会自动跳到下一行。
l 核心函数
通过查询表去修改一幅图像是一种有额外收获的方法,因为在图像处理过程中,你想去用其他的值去替换所有给定的图像数据的值是很常见的(例如我们就需要用改变了灰度级后的查询表的数据去替换原始图像的每个像素的灰度值)。为此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];
最后我们就是调用这个LUT()函数了:
LUT(I,lookUpTable,J);//其中I是我们的输入图像数据,J是输出图像。
下面是这次学习的两个例程程序:
第一个是解释查询表的,对我们理解查询表很有帮助,如下:
//
//lookuptable.cpp--explain the meaning of the lookup table
//Date:2016/2/27 CST
//Author:York.
//email:y_zhou1991@163.com
// 查询表其本质就是为了减少图像的灰度级,以空间代价换取时间
// 对图像所有像素进行扫描这项工作还是避免不了,在利用那个公式
// 制作查询表时就是对图像的每个像素进行扫描然后将每个像素的灰度值
// 重新升级后保存到查询表中,只是对于比较大的图像,对灰度级别要求不高的图像,
// 我们利用查询表我们可以把灰度级降下来,提高了访问图像的效率
//
#include <cv.h>
#include <highgui.h>
using namespace std;
using namespace cv;
//定义量化级别,注意因为我输入的是三通道图像,所以值比较大
//如果是单通道图像值不能大于256
#define QUAN_VAL1 (10)
#define QUAN_VAL2 (100)
//制作lookup table,即查找表
void CreateLookupTable(Mat& table, uchar quan_val)
{
table.create(1, 256, CV_8UC1);//创建表矩阵,可以看出表示Mat类型
uchar *p = table.data;//获得表数据的第一行第一列的指针
for (int i = 0; i < 256; ++i)
{
p[i] = quan_val*(i / quan_val); //应用公式对图像的每一个像素的灰度重新进行灰度级划分
}
}
int main(int argc, char *argv[])
{
Mat img;
Mat out1;
Mat out2;
img = imread("F:/Photo/OpenCV_Photo/fruits.jpg", 1);
Mat table;//定义查找表
//创建灰度级别是10的查找表
CreateLookupTable(table, QUAN_VAL1);
//使用查找表直接计算出量化灰度级后的图像并将其保存到out1中
LUT(img, table, out1);
//创建灰度级别是100的查找表
CreateLookupTable(table, QUAN_VAL2);
//使用查找表直接计算出量化灰度级后的图像并将其保存到out2中
LUT(img, table, out2); // Call OpenCV function
namedWindow("baboon", CV_WINDOW_NORMAL);
imshow("baboon", img);
namedWindow("QUAN_VAL=10", CV_WINDOW_NORMAL);
imshow("QUAN_VAL=10", out1);
namedWindow("QUAN_VAL=100", CV_WINDOW_NORMAL);
imshow("QUAN_VAL=100", out2);
waitKey(0);
return 0;
}
最终效果如图:
当量级是10的时候效果不是很明显,但当量级是100的时候效果就比较明显了,它把灰度级别0-100之间的划为新的灰度级别0,100-200之间的灰度值在新的灰度值是1,以此类推,所以,灰度值领域在100之间的灰度都被降下来了,这样的最终效果就是(颜色相近的都变成了一个区域)。
接下来的程序就是今天的程序汇总,里面包含了查询表,也包含了通过迭代器来扫描图像,但是这种方法的效率明显没有用查询表这种方法的效率高(在某种情况下,例如图像比较大且对灰度级别要求不高),主要是看其几种方式的运行效率,这里特别注意,OpenCV中有的函数尽量用,而不要自己再去写,因为OpenCV自带的函数都是优化过的,其效率是毋庸置疑的,好了代码如下:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
#include <sstream>
using namespace std;
using namespace cv;
static void help()
{
cout
<< "\n--------------------------------------------------------------------------" << endl
<< "This program shows how to scan image objects in OpenCV (cv::Mat). As use case"
<< " we take an input image and divide the native color palette (255) with the " << endl
<< "input. Shows C operator[] method, iterators and at function for on-the-fly item address calculation." << endl
<< "Usage:" << endl
<< "./howToScanImages imageNameToUse divideWith [G]" << endl
<< "if you add a G parameter the image is processed in gray scale" << endl
<< "--------------------------------------------------------------------------" << endl
<< endl;
}
Mat& ScanImageAndReduceC(Mat& I, const uchar* table);
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* table);
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar * table);
int main(int argc, char* argv[])
{
help();
if (argc < 3)
{
cout << "Not enough parameters" << endl;
system("pause");
return -1;
}
Mat I, J;
if (argc == 4 && !strcmp(argv[3], "G"))
I = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
else
I = imread(argv[1], CV_LOAD_IMAGE_COLOR);
if (!I.data)
{
cout << "The image" << argv[1] << " could not be loaded." << endl;
system("pause");
return -1;
}
int divideWith = 0; // convert our input string to number - C++ style
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));
const int times = 100;
double t;
t = (double)getTickCount();
for (int i = 0; i < times; ++i)
{
cv::Mat clone_i = I.clone();
J = ScanImageAndReduceC(clone_i, table);
}
t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
t /= times;
cout << "Time of reducing with the C operator [] (averaged for "
<< times << " runs): " << t << " milliseconds." << endl;
t = (double)getTickCount();
for (int i = 0; i < times; ++i)
{
cv::Mat clone_i = I.clone();
J = ScanImageAndReduceIterator(clone_i, table);
}
t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
t /= times;
cout << "Time of reducing with the iterator (averaged for "
<< times << " runs): " << t << " milliseconds." << endl;
t = (double)getTickCount();
for (int i = 0; i < times; ++i)
{
cv::Mat clone_i = I.clone();
ScanImageAndReduceRandomAccess(clone_i, table);
}
t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
t /= times;
cout << "Time of reducing with the on-the-fly address generation - at function (averaged for "
<< times << " runs): " << t << " milliseconds." << endl;
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.data;
for (int i = 0; i < 256; ++i)
p[i] = table[i];
t = (double)getTickCount();
for (int i = 0; i < times; ++i)
LUT(I, lookUpTable, J);
t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
t /= times;
cout << "Time of reducing with the LUT function (averaged for "
<< times << " runs): " << t << " milliseconds." << endl;
system("pause");
return 0;
}
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
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& ScanImageAndReduceIterator(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() != sizeof(uchar));
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;
}
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table)
{
// accept only char type matrices
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;
}
效果图:
PS:有很多人在用这段代码的时候出现各种问题,如果出现问题请看下一篇博客(main函数的两个参数argc和argv)