Core moudle(基础模块)
1.How to scan images, lookup tables and time measurement with OpenCV
1.1 测试用实例描述
OpenCV官方文档中此部分的应用背景:一种简答的色彩还原方法。
问题描述:由于元素在矩阵中的存储最少也是256个不同值,如果直接采用一对一的映射将十分的需要资源,为了解决这个问题,首先对图像进行一个简单的色彩空间缩减算法,原理很简单,通过C++中的取商后乘十来实现,具体公式如下;
I
n
e
w
=
(
I
o
l
d
10
)
∗
10
I_{new}=(\frac{I_{old}}{10})*10
Inew=(10Iold)∗10
这样以个256的像素值就会被压缩很多,如此一来就基本上解决了数据过大的问题,然而,就算使用这个公式进行计算,由于图像像素多,对于每个图像都应用这个公式进行操作也十分消耗时间,为此使用查找表的方式,先计算斌存好映射关系和结果,到时候直接根据像素值进行查找就可以了,省去实际计算时间。
首先是计算查找表
int divideWith = 0; //划分的宽度,由输入而来
stringstream s;
s << argv[2];
s >> divideWith; //将输入的字符转换成int型
if (!s || !divideWith)
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256]; //查找表计算,256个像素值对应26个结果,多对一的映射
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i/divideWith));
如何衡量时间
-
cv::getTickCount()
,返回来自某个事件的系统 CPU 的滴答数,一直在变化。 -
cv::getTickFrequency()
,返回您的 CPU 在一秒内发出滴答声的次数。
double t = (double) getTickCount ();//起始时间
// 做点什么 ...
t = ((double) getTickCount () - t)/ getTickFrequency (); //时间差除以CPU每秒滴答数=运行时间s
cout << "时间以秒为单位:" << t << endl;
1.2 图像矩阵在内存中如何存储。
一个图像矩阵的存储方式取决于:颜色系统(通道数),分辨位数。
对于灰度图像只有一个通道,以及像素值。
对于多通道图像,每一列中包含多个子列。
注意:通道的存储方式是BGR不是RGB
在内存足够的情况下,每一个元素的存储时连续的,从而创建一个超级长的一行数据,可以通过cv::Mat::isContinuous()
来查看是否图像存储是否连续。
1.3 有效的访问方式
在性能方面,OpenCV自己表示C风格的指针访问或者[]
访问效率高,所以依然要使用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); //p指向每一行的首地址
for ( j = 0; j < nCols; ++j)
{
p[j] = table[p[j]]; //根据每行元素的值进行查找
}
}
return I; //经过处理后,矩阵的数据完全变化了,而其他信息没有变化
}
另一种方法是使用Mat
对象的data
数据成员生成指针,因为data
数据成员的指针指向第一行第一列的元素的地址。
uchar* p = I.data;
for( unsigned int i = 0; i < ncol*nrows; ++i)
*p++ = table[*p];
这种方式访问是十分不直观的,无法知道是操作的是哪一行那一列数据。
1.4 迭代器方法----安全
迭代器访问不需要我们自己去计算从哪访问,要访问多少个,迭代器的方式更加模块化。
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;
}
如果用<char>
型的迭代器去访问三个通道的图像,这样只能访问蓝色通道所有数据。可以看出,迭代器得到的类似于“每个像素的首地址”,并且迭代器访问完一行行后自动换行
1.5 On-the-fly address calculation with reference returning(这种方法不建议用)
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::Mat_
这种数据类型需要在定义的时候额外添加数据类型Mat_<double> M(20,20)
,如此一来以后访问就不用指定访问的数据类型了,可以直接使用()
和cv::Mat::at
.
1.6 使用核心函数
在Core Function中提供了一种直接根据查找表操作进行的函数cv::LUT()
.
//创建查找表
Mat lookUpTable(1, 256, CV_8U);
uchar* p = lookUpTable.ptr();
for( int i = 0; i < 256; ++i)
p[i] = table[i];
//使用函数直接完成查找,I根据lookUpTable进行查找并得到J,J是输出
LUT(I, lookUpTable, J);
1.7几种方法的速度
咳咳,事实证明,能用函数就不要自己写,费力不讨好。。。解决开始提出的问题的源码地址