赞
踩
在上一篇文章《基本图像容器——Mat》中,已经详细描述了OpenCV储存图像数据的形式(图像的每个像素储存为一个矩阵中的数据项,矩阵的每个数据项包括各个颜色通道的值,如RGB3通道包含红、绿、蓝共3个通道的颜色值)。所以,矩阵的值的列数为矩阵的列数乘以颜色通道数,如下图所示,OpenCV默认的BGR格式的数据有3个颜色通道,所以实际有m*3列数值;行数则不变:
如果我们使用uchar(8bits)类型去储存每个像素的值,那么像素的每个颜色通道可以有256个可能的值( 2 8 = 256 2^8=256 28=256),这样的话如果是3通道的数据,那每个数据项就有( 25 6 3 = 16 , 777 , 216 256^3=16,777,216 2563=16,777,216)种可能的颜色值了。如果矩阵很大,那就会给算法的执行带来很大压力。
为了减轻扫描图像数据的算法压力,可以将现有的颜色值除以某个值,从而缩小颜色值的值域。例如,将所有0-9的颜色值都用0替代,将所有10-19的颜色值都用10替代,依此类推。用数学公式来表示:
I
n
e
w
=
(
I
o
l
d
10
)
∗
10
I_{new} = ( \frac{I_{old}} {10})*10
Inew=(10Iold)∗10
注意:uchar类型被整数除了之后,得出的结果依然是uchar类型
但是对每个颜色值都执行上述的除法和乘法运算,仍然会消耗很多算力。然而,由于每个颜色值的值域是有限的,比如uchar类型是[0, 256],所以如果能直接计算出所有可能的缩减结果,并进行赋值运算,会节省很多算力。所以,就产生了“查询表”
查询表就是储存与原始值一一对应的缩减值的数组(一维或多维)。一旦数据类型确定,查询表的大小就确定不变了。比如,uchar类型的查询表就只有256个缩减值,因为原始值总共就只有256种可能。然后运用这个查询表将每个原始值都替换成查询表中对应的值,就不必对每一个原始值都进行缩减计算了,而只是简单的查询和赋值,这样就能节省算力。而且,原始值越多,节省效果越好。这就是查询表的优势。
该实例将3种算法放在一个main函数中,main函数接收一个参数数组argv[],其中有可以有3个或4个参数:
static void help()
{
std::cout
<< "\n--------------------------------------------------------------------------" << endl
<< "这个程序展示如何在OpenCV中扫描图片(cv::Mat)"
<< "根据输入图片路径以及缩减因子(大于0的整数)" << endl
<< "这个程序分别使用了C风格方法、迭代器方法、坐标方法和LUT方法进行扫描" << endl
<< "参数输入:" << endl
<< "程序名 <图片路径> <缩减因子> [G]" << endl
<< "如果加了参数G,则使用灰度格式读入图片" << endl
<< "--------------------------------------------------------------------------" << endl
<< endl;
}
在main函数中,首先对输入的参数进行分析和处理:
if (argc < 3) {//参数数量小于3个,则退出函数并输出提示 cout << "Not enough parameters" << endl; return -1; } Mat I, J; if (argc == 4 && !strcmp(argv[3], "G")) //当参数数量为4且,最后一个参数是"G"时 I = imread(argv[1], IMREAD_GRAYSCALE); //以灰度格式读取图片,并储存在Mat对象I中 else I = imread(argv[1], IMREAD_COLOR); //否则以BGR格式读取图片,并储存在Mat对象I中 if (I.empty()) {//如果读取的数据为空,则退出函数并输出提示 cout << "The image" << argv[1] << " could not be loaded." << endl; return -1; }
接下来,根据传入的参数数组中的第3个参数,即argv[2],计算查询表:
int divideWith { 0 }; //1-4行:将字符串转换成数字,并储存在变量divdeWith中作为缩减因子
stringstream s;
s << argv[2];
s >> divideWith;
if (!s || !divideWith)
{//如果无法接收第3个参数,或者参数为0,则退出函数,并输出提示
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
uchar table[256]; //用一个长度为256的一维数组来储存查询表
for (int i { 0 }; i < 256; ++i) //计算查询表,计算结果为uchar类型
table[i] = static_cast<uchar>(divideWith * (i / divideWith));
在将字符串转换成数字的过程中使用了C++中的字符串流stringstream
,字符串流对象s
,接收参数字符串argv[2]
,然后将其传给整数变量divideWith
,作为后面进行缩减运算的缩减因子。
计算查询表的for循环块,进行了256次循环,将[0, 255]中的所有整数依次进行了缩减运算,得出256个uchar类型的缩减值,并依次储存在一个uchar类型的数组中。
这个程序为了比较不同扫描方法的速度,使用了使用了OpenCV中的cv::getTickCount()
和cv::getTickFrequency()
函数进行计时。前者返回某个运行节点的CPU的tick数(一个tick为CPU频率的倒数);后者返回CPU每秒的tick数。如果获取事件起始点和结束点的CPU的tick数,然后用它们的差除以CPU每秒的tick数就能得到事件起始点和结束点之间的时间差,单位为秒。具体代码如下:
double t { static_cast<double>getTickCount() }; //将返回值类型从原本的int型转换为double型
//扫描函数
t = (static_cast<double>getTickCount() - t)/getTickFrequency(); //转换为double型后,除法运算结果中的小数就会被保留
cout << "用时 " << t << " 秒" << endl;
这几行代码放在每个扫描方法的前后,从而能为每个扫描方法计算运行时间,便可比较它们的运行速度
虽然储存图像数据的Mat对象可能是个二维甚至多维矩阵,但是在内存中矩阵是被按行分成若干个一维数组储存的。这些一维数组可能被放在一起,形成一个连续的内存空间,也可能被分开储存。OpenCV中的cv::Mat::isContinuous()
可以判断矩阵在内存中是否是连续的。被储存在一个连续内存中的矩阵扫描起来会更快。
如果没耐心对比每种方法的思路,可以直接结论部分看每种方法的优缺点,然后找到相应方法的章节进行进一步阅读。
但是,除了LUT方法,前三种方法都需要自己编写将替换原始值的语句,该语句中对查询表的索引,思路比较绕,为了避免重复,本文只在第一种方法,即C风格的扫描方法中对此进行了详细解释,如果看不懂可以到该章节进行参考。
C风格的扫描方法少不了要进行C风格的二维数组的遍历操作,包括行指针、列指针等。
//! [scan-c] Mat& ScanImageAndReduceC(Mat& I, const uchar* const table) { //只接收uchar类型的矩阵 CV_Assert(I.depth() == CV_8U); //depth函数返回每个通道的数值的类型,如CV_8U为8比特无符号字符串类型 int channels { I.channels() }; //channels函数返回矩阵中的颜色通道数 int nRows { I.rows }; int nCols { I.cols * channels }; //实际列数为矩阵列数*颜色通道数 if (I.isContinuous()) {//如果是储存在连续空间,则按一维数组来处理 nCols *= nRows; //一维数组的实际列数为原列数*行数 nRows = 1; //一维数组行数为1 } int i, j; uchar* p; //uchar类型的p指针用来储存扫描结果 for (i = 0; i < nRows; ++i) { p = I.ptr<uchar>(i); //ptr模板函数根据行数i返回矩阵第i行的行指针,并在尖括号中指定返回的指针类型 for (j = 0; j < nCols; ++j) {//原来的p[j]是个列指针,实际为矩阵i行j列的值,即颜色的原始值 //因为查询表示按0-255的顺序排列的, //所以,以p[j]为下标访问查询表,正好能访问到原始值对应的缩减值 p[j] = table[p[j]]; //将查询表中的缩减值赋值给p指针中对应的位置 //由于p是指向Mat对象I中的矩阵的,所以I实际上也被更改了 } } return I; //返回被更改的I } //! [scan-c]
注意,该函数传入的第2个参数为uchar类型的常量指针常量,即这个指针指向的对象不能被修改,指针的地址也不能被修改,这样才能保证查询表在函数运行结束之后仍然没变,可以被再次利用。
该函数最核心的部分就是对数组进行遍历操作的for循环语句。这里巧妙地使用列指针,即原始值,作为访问查询表数组的下标,从而找到对应的缩减值。具体思路写在注释里了,读者可以参阅。
当矩阵是被储存在连续的内存空间中的时候,实际上是对一个一维数组进行遍历,i为1,只循环1次。
如果确定矩阵是储存在连续内存空间中的,那么还有另外一种方法可以完成对它的遍历:
uchar* p { I.data }; //data是Mat类的public数据成员,它储存了Mat对象首行的行指针
for(unsinged int i { 0 }; i < ncol*nrows; ++i)
//行指针自增之后就变成了列指针,即一维数组第i列的地址
//再进行解引用操作,就得到了第i列的值
*p++ = table[*p];
使用迭代器比用数组指针更加方便,因为不用考虑行指针、列指针的问题,没有嵌套的for循环,只需要一层for循环。
但是,迭代器只能代替矩阵中的数据项。也就是说,如果是单通道的灰度格式的图片数据,迭代器正好代替矩阵中的每个颜色值;但是,如果是3通道的BGR格式的图片数据,迭代器实际上代替的是一个长度为3的数组,其中包含了3个颜色值(分别为蓝、绿、红值),在这种情况下还必须对迭代器进行进一步的操作。
//! [scan-iterator] Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table) { CV_Assert(I.depth() == CV_8U); const int channels{ I.channels() }; switch (channels) {//switch语句实现对两种情况的分别处理 case 1: //单通道的灰度图片数据 { MatIterator_<uchar> it, end; //声明起始和终止迭代器 for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it) //begin和end函数分别返回指向第一个和最后一个数据项的迭代器 *it = table[*it]; //对迭代器进行解引用操作可获得对应的数据项 break; } case 3: //3通道的BGR图片数据 { MatIterator_<Vec3b> it, end; for (it = I.begin<Vec3b>(), end = I.end<Vec3b>(); it != end; ++it) {//还需要对迭代器中的3个值分别进行赋值操作 //*it[0]为数据项在第一个颜色通道的值 (*it)[0] = table[(*it)[0]]; (*it)[1] = table[(*it)[1]]; (*it)[2] = table[(*it)[2]]; } } } return I; } //! [scan-iterator]
如果在3通道的情况,没有对迭代器中的数组进行进一步的操作的话,那改变的只是每个像素的蓝色值。因为OpenCV将RGB转换成了BGR,第一个值是蓝色值。
这种方法其实一般是用来确定需要某个数据项的行数和列数的,也称为随机获取,所以并不建议用来扫描图像数据。
这种方法也需要对单通道和3通道数据进行分情况的处理。
在单通道数据中,该方法运用了数据项的动态地址,并返回数据项的引用,这由函数cv::Mat::at()
来实现;
在3通道数据中,该方法运用了Mat_
类型达到了同样的目的。/Mat_类型为储存了每个数据项的类型信息的Mat类型;Mat_类型可以用(row, col)方式来访问;这种访问等同于在Mat类型中用at(row, col)来进行访问
具体解释见代码注释:
//! [scan-random] Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table) { 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) //at函数获取矩阵在i行j列的数据项的地址,并返回它的引用 I.at<uchar>(i, j) = table[I.at<uchar>(i, j)]; break; } case 3: { //Vec3b类型为OpenCV中定义的3字节数据类型,用来储存长度为3的uchar类型数组正好 Mat_<Vec3b> _I = I; //这里不能用C++20的初始化语法,会报错:无法转化参数类型 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; } //! [scan-random]
在OpenCV的core模块,有一个专门用来修改图片数据矩阵中的值的方法,cv::LUT()
。其具体用法如下:
//! [table-init]
Mat lookUpTable(1, 256, CV_8U); //用Mat构造函数创建uchar类型的1维矩阵
uchar* p = lookUpTable.ptr(); //获取矩阵的首地址
for (int i = 0; i < 256; ++i)
p[i] = table[i]; //将之前计算的查询表中的数值复制到矩阵中
//! [table-init]
//! [table-use]
LUT(I, lookUpTable, J);
//! [table-use]
cv::LUT()
函数使用了3个参数
将3种方法整合在一个cpp文件中,并统计每个方法的运算时长。整合后的代码如下:
#include <opencv2/core.hpp> #include <opencv2/core/utility.hpp> #include <opencv2/imgcodecs.hpp> #include <opencv2/highgui.hpp> import <iostream>; import <sstream>; using namespace cv; using namespace std; static void help() { std::cout << "\n--------------------------------------------------------------------------" << endl << "这个程序展示如何在OpenCV中扫描图片(cv::Mat)" << "根据输入图片路径以及缩减因子(大于0的整数)" << endl << "这个程序分别使用了C风格方法、迭代器方法、坐标方法和LUT方法进行扫描" << endl << "参数输入:" << endl << "程序名 <图片路径> <缩减因子> [G]" << endl << "如果加了参数G,则使用灰度格式读入图片" << 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) { std::cout << "缺少参数" << endl; return -1; } Mat I, J; if (argc == 4 && !strcmp(argv[3], "G")) I = imread(argv[1], IMREAD_GRAYSCALE); else I = imread(argv[1], IMREAD_COLOR); if (I.empty()) { std::cout << "图片" << argv[1] << "打不开。" << endl; return -1; } //! [dividewith] int divideWith{ 0 }; stringstream s; s << argv[2]; s >> divideWith; if (!s || !divideWith) { std::cout << "无效缩减因子。 " << endl; return -1; } uchar table[256]; for (int i{ 0 }; i < 256; ++i) table[i] = static_cast<uchar>(divideWith * (i / divideWith)); //! [dividewith] const int times{ 100 }; double t; t = static_cast<double>(getTickCount()); for (int i{ 0 }; i < times; ++i) { Mat clone_i{ I.clone() }; J = ScanImageAndReduceC(clone_i, table); } t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency(); t /= times; std::cout << "C风格方法每运行" << times << "次平均耗时: " << t << "毫秒。" << endl; t = static_cast<double>(getTickCount()); for (int i{ 0 }; i < times; ++i) { Mat clone_i{ I.clone() }; J = ScanImageAndReduceIterator(clone_i, table); } t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency(); t /= times; std::cout << "迭代器方法每运行" << times << "次平均耗时:" << t << "毫秒。" << endl; t = static_cast<double>(getTickCount()); for (int i{ 0 }; i < times; ++i) { Mat clone_i{ I.clone() }; ScanImageAndReduceRandomAccess(clone_i, table); } t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency(); t /= times; std::cout << "坐标方法每运行" << times << "次平均耗时:" << t << "毫秒。" << endl; //! [table-init] Mat lookUpTable(1, 256, CV_8U); uchar* p{ lookUpTable.ptr() }; for (int i{ 0 }; i < 256; ++i) p[i] = table[i]; //! [table-init] t = static_cast<double>(getTickCount()); for (int i{ 0 }; i < times; ++i) //! [table-use] LUT(I, lookUpTable, J); //! [table-use] t = 1000 * (static_cast<double>(getTickCount()) - t) / getTickFrequency(); t /= times; std::cout << "LUT方法每运行 " << times << "次平均耗时:" << t << "毫秒。" << endl; return 0; } //! [scan-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; } //! [scan-c] //! [scan-iterator] 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; } //! [scan-iterator] //! [scan-random] 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; } //! [scan-random]
要调试程序,需要输入参数的main函数。在VS中,可以在项目属性中提前输入参数,如下图中黑体的th.jpg 10,就是用空格隔开的两个参数。注意第一个参数可以不用输入,默认为项目程序名。
我这里使用的th.jpg,是一个1920*1200的图片。这是为了测试处理比较大的图片的时候各种方法的性能。
调试运行结果为:
从运行结果可以看到用时最短的是LUT方法,这是因为OpenCV库使用了多线程方法加快了运行速度。但这并不代表LUT方法永远是最好的,其他方法的优缺点如下:
** 文章较长,不免有遗漏或笔误,欢迎大家指正!**
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。