本篇文章对OpenCV文档中如何使用OpenCV扫描图像,查找表格和时间测量给出的源代码进行详细的注释。本篇文章将会涉及到以下内容。
- 头文件的作用
- 颜色空间缩减原理
- C操作符[ ](指针)、迭代器、即时项目地址计算三种方法的使用及代码注解
- Visual Studio 2017 命令参数配置方法等
想自己着手写代码,一定得知道各个头文件的用处。在不知道该用什么库的时候,可以直接用
#include <opencv2\opencv.hpp>
缺点是引用OpenCV库之外的其他代码或者不同opencv版本之间的引用可能会报错。这个时候探究每个头文件的作用就是必要的了。下面从头文件开始,为源代码做出注释。
#include <opencv2/core.hpp> //核心模块,定义了基本的数据结构和算术函数
#include <opencv2/core/utility.hpp> //包含getTickFrequency()等函数
#include "opencv2/imgcodecs.hpp" //图片编解码、加载保存之类的函数
#include <opencv2/highgui.hpp> //视频捕捉、图像和视频的编码解码、图形交互界面的接口
#include <iostream> //C++中用于数据的流式输入与输出的头文件
#include <sstream> //使用stringsteam类型需要用到的头文件
using namespace std;
using namespace cv;
这里使用namespace命名空间,省去了之后代码中反复使用std::或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
<< "./how_to_scan_images <imageNameToUse> <divideWith> [G]" << endl
<< "if you add a G parameter the image is processed in gray scale" << endl
<< "--------------------------------------------------------------------------" << endl
<< endl; //ednl结束本行,相当于c语言中的"\n"。注意endl最后一个字符是字母L,不是数字1。
}
下面是C操作符[ ](指针)、迭代器、即时项目地址计算三种方法函数声明,其中的&读作引用,相当于给函数或变量名起了第二个名字,引用初始化某个变量后,可以使用该引用名称或原变量名称指向该变量,和指针有一定的区别,具体请参考C++引用
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[]) // argc是命令行总的参数个数,argv[]是argc个参数,其中第0个参数是程序的全名,后面跟的用户输入的参数
{
help();
if (argc < 3) //判断输入参数是否合法,如果个数小于3个,则输出参数不够
{
cout << "Not enough parameters" << endl;
return -1;
}
Mat I, J; //I为输入矩阵,J为输出矩阵
if (argc == 4 && !strcmp(argv[3], "G"))
I = imread(argv[1], IMREAD_GRAYSCALE); //如果输入了参数G,图像将以灰度级进行处理
else
I = imread(argv[1], IMREAD_COLOR); //否则,图像将以彩色级进行处理
if (I.empty()) //判断矩阵I是否载入成功
{
cout << "The image" << argv[1] << " could not be loaded." << endl;
return -1;
}
//! [dividewith]
int divideWith = 0; // 将我们输入的字符串转换为数字 -C++风格
stringstream s; //这里用到了stringstream
s << argv[2]; //将第三个参数复制给字符串s
s >> divideWith; //将字符串转化为数字
if (!s || !divideWith) //判断输入是否合法,是否已经输入了字符串或输入的字符串是否为零
{
cout << "Invalid number entered for dividing. " << endl;
return -1;
}
颜色空间缩减原理: 假设dividewith = 10。定义
0~9范围的像素值为0,
10~19范围的像素值为10,
20~29范围的像素值为20,
以此类推,则原单个通道的0~255的256个值,变为了0~25的26个值。
继续主函数,下面是C风格运算符[](指针)、迭代器、即时项目地址计算和LUT四种方法的比较。
uchar table[256]; //建立一张颜色空间缩减的表格,其实就是数组,方便后边查找赋值
for (int i = 0; i < 256; ++i)
table[i] = (uchar)(divideWith * (i / divideWith));
//! [dividewith]
const int times = 100; //定义常量,即值不会改变的量。常量的值为100,目的是计算执行100次的平均时间
double t; //平均执行时间
继续主函数, 第一种方法:C风格运算符[](指针)
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();
J = 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;
继续主函数,第四种方法LUT
//! [table-init]
Mat lookUpTable(1, 256, CV_8U); //Mat的前两个参数代表矩阵的大小,即1行256列,地址为0~256。CV_8U表示使用8位无符号整数型每一个像素由单个像素组成的单通道
uchar* p = lookUpTable.ptr(); //获取lookUpTable表的指针
for (int i = 0; i < 256; ++i)
p[i] = table[i];
//! [table-init]
t = (double)getTickCount();
for (int i = 0; i < times; ++i)
//! [table-use]
LUT(I, lookUpTable, J); //第一个参数,原始图像地址;第二个参数,查找表的地址;第三个参数输出图像地址
//! [table-use]
t = 1000 * ((double)getTickCount() - t) / getTickFrequency();
t /= times;
cout << "Time of reducing with the LUT function (averaged for "
<< times << " runs): " << t << " milliseconds." << endl;
return 0;
}
主函数终于看完了,喘口气,接着看剩下的三个函数。第一个函数,C操作符[ ](指针)方法
//! [scan-c] C操作符[ ](指针)
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table)
{
// accept only char type matrices
CV_Assert(I.depth() == CV_8U); //CV_Assert()作用:CV_Assert()若括号中的表达式值为false,则返回一个错误信息。
int channels = I.channels();
int nRows = I.rows;
int nCols = I.cols * channels; //定义临时变量
//判断矩阵是否是连续的,如果是连续的,就将这个矩阵当作 1 x nCols*nRows ,即 1 x I.rows*I.cols*I.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]
参考OpenCV中矩阵类详解之一:Mat ,可以对OpenCV中的Mat有更深入细致的了解。这里只挑出本函数用到各项进行介绍
depth:深度,即每一个像素的位数(bits),在opencv的Mat.depth()中得到的是一个 0 – 6 的数字,分别代表不同的位数:enum { CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6 }; 可见 0和1都代表8位, 2和3都代表16位,4和5代表32位,6代表64位;
channels:通道,矩阵中的每一个矩阵元素拥有的值的个数。比如 3 * 4 矩阵中一共 12 个元素,如果每个元素有三个值,那么就说这个矩阵是 3 通道的,即 channels = 3。常见的是一张彩色图片有蓝、绿、红(BGR)三个通道。
rows: Mat矩阵的行数。
cols: Mat矩阵的列数。
isContinuous: 矩阵是否连续。矩阵连续是指如果在每一行的结尾无间隙连续存储矩阵的元素,该方法返回 true。否则,它将返回 false。很明显,1 x 1 或 1xN 矩阵始终是连续的。使用 Mat::create() 创建矩阵是始终是连续的。
第二个函数,迭代器方法。
//! [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; //MatIterator_是Mat的迭代器
//在下面的循环中,使用了Mat的begin和end函数,使迭代器分别指向Mat I数据部分的开头和结尾。
for (it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it) //遍历象素,进行替换
*it = table[*it];
break;
}
//图像存储没有双通道的,这里自然就没有 case 2 了
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]
不熟悉面向对象编程中迭代器的概念的读者,可以阅读与STL中迭代器相关的入门书籍和文字。用关键字“STL 迭代器”进行搜索可以找到各种相关的博文和资料
第三个函数,即时项目地址计算方法
//! [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)]; //at返回对指定数组元素的引用。
break;
}
case 3:
{
Mat_<Vec3b> _I = I;
//大多数情况下Mat就足够用了,但如果使用大量元素访问操作并且在编译时知道矩阵类型,则Mat_会更方便。
//比如'Mat :: at(int y,int x)'和'Mat _ :: operator()(int y,int x)'在相同时条件下,后者所花费的时间更短
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]
至此,整篇代码就解读完毕了。让我们运行一下看看结果吧。
这段程序主函数中没有输入函数,所以不能直接在运行后输入所需参数。这里提供一种解决方案:Visual Studio 2017运行此段程序参数配置过程:视图–>其他窗口–>属性管理器–>在右侧出现的属性管理器窗口双击解决方案(或右键解决方案)–>调试–>命令参数。在这里添加
< 图片的路径 > < dividewith的值 > < 是否以灰色图像形式载入 >
- 三个参数以空格隔开,所以图片的路径中不能含有空格。
- 不需要输入尖括号<>。
- 以上添加的三个参数分别是help()函数中提到的主函数的第二三四个参数,第一个参数不需要添加,vs2017会默认为当前工程的名字。如果希望将图片以灰度图形式载入,则添加G,否则不填
如果希望看到图片效果或保存图片,可以自己添加imshow(),imwrite()函数。具体参考这里。