图像的基本操作
图像的表示
在正式介绍之前,先简单介绍一下数字图像的基本概念。如图 3.1 中所示 的图像,我们看到的是 Lena 的头像,但是计算机看来,这副图像只是一堆亮度 各异的点。一副尺寸为 M × N 的图像可以用一个 M × N 的矩阵来表示,矩 阵元素的值表示这个位置上的像素的亮度,一般来说像素值越大表示该点越 亮。如图 3.1 中白色圆圈内的区域,进行放大并仔细查看,将会如图 3.2 所 示。
一般来说,灰度图用 2 维矩阵表示,彩色(多通道)图像用 3 维矩阵(M × N × 3)表示。对于图像显示来说,目前大部分设备都是用无符号 8 位整 数(类型为 CV_8U)表示像素亮度。 图像数据在计算机内存中的存储顺序为以图像最左上点(也可能是最左下 点)开始,存储如表 3-1 所示。
Iij 表示第 i 行 j 列的像素值。如果是多通道图像,比如 RGB 图像,则每个 像素用三个字节表示。在 OpenCV 中,RGB 图像的通道顺序为 BGR ,存储如 表 3-2 所示。
Mat类
早期的 OpenCV 中,使用 IplImage 和 CvMat 数据结构来表示图像。IplImage 和 CvMat 都是 C 语言的结构。使用这两个结构的问题是内存需要手动管理,开 发者必须清楚的知道何时需要申请内存,何时需要释放内存。这个开发者带来了 一定的负担,开发者应该将更多精力用于算法设计,因此在新版本的 OpenCV 中 引入了 Mat 类。
新加入的 Mat 类能够自动管理内存。使用 Mat 类,你不再需要花费大量精 力在内存管理上。而且你的代码会变得很简洁,代码行数会变少。但 C++接口唯 一的不足是当前一些嵌入式开发系统可能只支持 C 语言,如果你的开发平台支持 C++,完全没有必要再用 IplImage 和 CvMat。在新版本的 OpenCV 中,开发者依 然可以使用 IplImage 和 CvMat,但是一些新增加的函数只提供了 Mat 接口。本书 中的例程也都将采用新的 Mat 类,不再介绍 IplImage 和 CvMat。
Mat 类的定义如下所示,关键的属性如下方代码所示:
class CV_EXPORTS Mat { public: //一系列函数 ... /* flag 参数中包含许多关于矩阵的信息,如: -Mat 的标识 -数据是否连续 -深度 -通道数目 */ int flags; //矩阵的维数,取值应该大于或等于 2 int dims; //矩阵的行数和列数,如果矩阵超过 2 维,这两个变量的值都为-1 int rows, cols; //指向数据的指针 uchar* data; //指向引用计数的指针 //如果数据是由用户分配的,则为 NULL int* refcount; //其他成员变量和成员函数 ... };
创建 Mat 对象
Mat 是一个非常优秀的图像类,它同时也是一个通用的矩阵类,可以用来创 建和操作多维矩阵。有多种方法创建一个 Mat 对象。
构造函数方法
Mat 类提供了一系列构造函数,可以方便的根据需要创建 Mat 对象。下面是 一个使用构造函数创建对象的例子。
Mat M(3,2, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl;
第一行代码创建一个行数(高度)为 3,列数(宽度)为 2 的图像,图像元 素是 8 位无符号整数类型,且有三个通道。图像的所有像素值被初始化为(0, 0, 255)。由于 OpenCV 中默认的颜色顺序为 BGR,因此这是一个全红色的图像。
第二行代码是输出Mat类的实例M的所有像素值。Mat重定义了<<操作符, 使用这个操作符,可以方便地输出所有像素值,而不需要使用 for 循环逐个像素 输出。
Mat的常见构造方法
常用的构造函数有:
Mat::Mat() 无参数构造方法;
Mat::Mat(int rows, int cols, int type) 创建行数为 rows,列数为 col,类型为 type 的图像;
Mat::Mat(Size size, int type) 创建大小为 size,类型为 type 的图像;
Mat::Mat(int rows, int cols, int type, const Scalar& s) 创建行数为 rows,列数为 col,类型为 type 的图像,并将所有元素初始 化为值 s;
Mat::Mat(Size size, int type, const Scalar& s) 创建大小为 size,类型为 type 的图像,并将所有元素初始化为值 s;
Mat::Mat(const Mat& m) 将 m 赋值给新创建的对象,此处不会对图像数据进行复制,m 和新对象 共用图像数据;
Mat::Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP) 创建行数为 rows,列数为 col,类型为 type 的图像,此构造函数不创建 图像数据所需内存,而是直接使用 data 所指内存,图像的行步长由 step 指定。
Mat::Mat(Size size, int type, void* data, size_t step=AUTO_STEP) 创建大小为 size,类型为 type 的图像,此构造函数不创建图像数据所需 内存,而是直接使用 data 所指内存,图像的行步长由 step 指定。
Mat::Mat(const Mat& m, const Range& rowRange, const Range& colRange) 创建的新图像为 m 的一部分,具体的范围由 rowRange 和 colRange 指 定,此构造函数也不进行图像数据的复制操作,新图像与 m 共用图像数 据;
Mat::Mat(const Mat& m, const Rect& roi) 创建的新图像为 m 的一部分,具体的范围 roi 指定,此构造函数也不进 行图像数据的复制操作,新图像与 m 共用图像数据。
这些构造函数中,很多都涉及到类型type。type可以是CV_8UC1,CV_16SC1, …, CV_64FC4 等。里面的 8U 表示 8 位无符号整数,16S 表示 16 位有符号整数,64F 表示 64 位浮点数(即 double 类型);C 后面的数表示通道数,例如 C1 表示一个 通道的图像,C4 表示 4 个通道的图像,以此类推。
如果你需要更多的通道数,需要用宏 CV_8UC(n),例如:
Mat M(3,2, CV_8UC(5));//创建行数为 3,列数为 2,通道数为 5 的图像
create()函数创建对象
除了在构造函数中可以创建图像,也可以使用 Mat 类的 create()函数创建图 像。如果 create()函数指定的参数与图像之前的参数相同,则不进行实质的内存 申请操作;如果参数不同,则减少原始数据内存的索引,并重新申请内存。使用 方法如下面例程所示:
Mat M(2,2, CV_8UC3);//构造函数创建图像
M.create(3,2, CV_8UC2);//释放内存重新创建图像
矩阵的基本元素表达
对于单通道图像,其元素类型一般为8U(即8位无符号整数),当然也可以是16S、32F等;这些类型可以直接用uchar、short、float等C/C++语言中的基本数据类型表达。如果多通道图像,如RGB彩色图像,需要用三个通道来表示。在这种情况下,如果依然将图像视作一个二维矩阵,那么矩阵的元素不再是基本的数据类型。27OpenCV中有模板类Vec,可以表示一个向量。OpenCV中使用Vec类预定义了一些小向量,可以将之用于矩阵元素的表达。
typedef Vec<uchar, 2> Vec2b; typedef Vec<uchar, 3> Vec3b; typedef Vec<uchar, 4> Vec4b; typedef Vec<short, 2> Vec2s; typedef Vec<short, 3> Vec3s; typedef Vec<short, 4> Vec4s; typedef Vec<int, 2> Vec2i; typedef Vec<int, 3> Vec3i; typedef Vec<int, 4> Vec4i; typedef Vec<float, 2> Vec2f; typedef Vec<float, 3> Vec3f; typedef Vec<float, 4> Vec4f; typedef Vec<float, 6> Vec6f; typedef Vec<double, 2> Vec2d; typedef Vec<double, 3> Vec3d; typedef Vec<double, 4> Vec4d; typedef Vec<double, 6> Vec6d;
例如8U类型的RGB彩色图像可以使用Vec3b,3通道float类型的矩阵可以使用Vec3f。
对于Vec对象,可以使用[]符号如操作数组般读写其元素,如:
Vec3b color; //变量描述一种RGB颜色 color[0]=255; //B分量 color[1]=0; //G分量 color[2]=0; //R分量
像素值的读写
很多时候,我们需要读取某个像素值,或者设置某个像素值;在更多的时候,我们需要对整个图像里的所有像素进行遍历。OpenCV提供了多种方法来实现图像的遍历。
at()函数
函数at()来实现读去矩阵中的某个像素,或者对某个像素进行赋值操作。下面两行代码演示了at()函数的使用方法。
uchar value = grayim.at<uchar>(i,j);//读出第i行第j列像素值
grayim.at<uchar>(i,j)=128; //将第i行第j列像素值设置为128
如果要对图像进行遍历,可以参考下面的例程。这个例程创建了两个图像,分别是单通道的grayim以及3个通道的colorim,然后对两个图像的所有像素值进行赋值,最后现实结果。
#include<iostream> #include"opencv2/opencv.hpp" using namespace std; using namespace cv; /*需要注意的是: 如果要遍历图像,并不推荐使用at()函数。使用这个函数的优点是代码的可读性比较高但是at的效率不是很高*/ int main(int argc,char* argv[]) { Mat grayim(600,800,CV_8UC1); Mat colorim(600,800,CV_8UC3); //遍历所有元素,并设置像素值 for(int i=0;i<grayim.rows;i++) for(int j=0;j<grayim.cols;j++) grayim.at<uchar>(i,j)=(i+j)%255; //遍历所有像素,并且设置像素值 for(int i=0;i<colorim.rows;i++) for(int j=0;j<colorim.cols;j++) { Vec3b pixel; pixel[0]=i%255; pixel[1]=j%255; pixel[2]=0; colorim.at<Vec3b>(i,j)=pixel; } //显示结果 imshow("grayim",grayim); imshow("colorim",colorim); waitKey(0); return 0; }
如果你熟悉C++的STL库那么你一定了解迭代器的使用。迭代器可以方便的遍历所有的元素。Mat也增加了迭代器的支持,一边矩阵元素的便利。下面的实例功能跟上一节的比较类似,但是由于使用了迭代器,而不是使用行数和列数来便利所有这儿没有了i和j变量,像素的像素值为一个随机数。
#include<iostream> #include"opencv2/opencv.hpp" using namespace std; using namespace cv; /*需要注意的是: 如果要遍历图像,并不推荐使用at()函数。使用这个函数的优点是代码的可读性比较高但是at的效率不是很高*/ int main(int argc,char* argv[]) { Mat grayim(680,800,CV_8UC1); Mat colorim(680,800,CV_8UC3); //遍历所有元素,并设置像素值 MatIterator_<uchar> grayit,grayend; for(grayit=grayim.begin<uchar>(),grayend=grayim.end<uchar>();grayit!=grayend;grayit++) *grayit=rand()%255; MatIterator_<Vec3b> colorit,colorend; for(colorit=colorim.begin<Vec3b>(),colorend=colorim.end<Vec3b>();colorit!=colorend;colorit++) { (*colorit)[0]=rand()%255;//Blue (*colorit)[1]=rand()%255;//Green (*colorit)[2]=rand()%255;//Red } imshow("yuan",grayim); imshow("chong",colorim); // printf("asdddddddddddddd"); waitKey(0); return 0; }
通过数据指针
使用iplimage结构的时候,我们会经常使用数据指针来直接操作像素。通过指针操作来访问像素值非常高效的,但是C/C++中的指针操作是不进行类型以及是否越界检查的,如果指针的访问出错程序运行时有时候可能看上去一切正常但是,有时候会弹出来一个“段错误”(segment fault)。
当程序的规模比较大,并且逻辑十分复杂的时候,查找指针错误就十分困难。对于不熟悉指针的编程者来说指针就如同噩梦。如果你对指针使用没有自信,则不建议使用。如果非常注重程序的运行速度,那么遍历象素的时候,建议使用指针。
0.783
#include<iostream> #include<time.h> #include<stdlib.h> #include"opencv2/opencv.hpp" using namespace std; using namespace cv; /*需要注意的是: 如果要遍历图像,并不推荐使用at()函数。使用这个函数的优点是代码的可读性比较高但是at的效率不是很高*/ int main(int argc,char* argv[]) { clock_t start,finish; //计时之用 double totalTime; //计时之用 start=clock(); //计时之用 Mat grayim(680,800,CV_8UC1); Mat colorim(680,800,CV_8UC3); //遍历所有元素,并设置像素值 for(int i=0;i<grayim.rows;i++) { uchar *p=grayim.ptr<uchar>(i); for(int j=0;j<grayim.cols;j++) { p[j]=(i+j)%255; } } for(int i=0;i<colorim.rows;i++) { Vec3b *p=colorim.ptr<Vec3b>(i); for(int j=0;j<colorim.cols;j++) { p[j][0]=j%255; p[j][1]=j%255; p[j][2]=j; } } imshow("yuan",grayim); imshow("chong",colorim); finish=clock();//计时之用 totalTime=(double)(finish-start)/CLOCKS_PER_SEC;//计时之用 printf("------------%lf------------\n",totalTime); waitKey(0); return 0; }
单行或者单列选择
提取矩阵的一行或者一列可以使用row()或者col()。函数的声明如下:
Mat Mat::row(int i) const Mat Mat::col(int j) const
参数i和j分别是行标和列标。例如取出A矩阵的第i行可以使用如下代码:
Mat line = A.row(i);
例如取出A矩阵的第i行,将这一行的所有元素都乘以2,然后赋值给第j行可以这样写:
A.row(j) = A.row(i)*2;
用range选择多行或多列
Range是OpenCV中新增的类,该类有两个关键变量star和end。Range对象可以用来表示矩阵的多个连续的行或者多个连续的列。其表示的范围为从start到end,包含start但不包含end。Range类的定义如下。
class range { public: ... int start,end; };
Range类还提供了一个静态方法all(),这个方法的作用如同Matlab中的“:”,表示所有的行或者列。
//创建一个单位阵 Mat A = Mat::eye(10,10,CV_32S); //提取第一行到第三行(不包括三) Mat B = A(Range::all(),Range(1,3)); //提取B的第5至第9行(不包括9) //其实等价于C = A(Range(5,9),Range(1,3)) Mat C = B(Range(5,9),Range::all());
感兴趣的区域
从头像中提取感兴趣的区域有两种方法,一种是使用构造函数,如下例所示:
//创建宽度为320,高度为241,的三通道图像 Mat img(Size(320,240),CV_8UC3); //roi是表示img中rect(10,10,100,100)区域的对象 Mat roi(img,rect(10,10,100,100)); //除了使用构造函数,还可以使用括号运算符,如下: Mat roi2=img(Rect(10,10,100,100)); //当然也可以使用Range运算符来定义感兴趣对象,如下: Mat roi3=img(Range(10,100),Range(10,100));
Mat表达式
利用C++中的运算符重载,OpenCV2中引入了Mat运算表达式。这一特点使得用C++进行编程的时候,就如同写Matlab脚本一样,代码变得简洁易懂,也便于维护。
如果矩阵A和B大小相同,则可以使用如下表达式:
C=A+B+1;
其执行结果是矩阵A和B大小相同,则可以使用如下表达式:
C=A+B+1;
其执行结果是A和B的元素相加,然后再加上1,并将生成的矩阵赋值给C变量。
下面是Mat表达式所支持的运算。下面的列表中使用A和B表示Mat类型的对象,使用s表示Scalar对象,alpha表示double值。
*加法,减法,取负:A+B,A-B,A+s,A-s,s+A,s-A,-A
*缩放取值范围:A*alpha
*矩阵对应元素的乘法和除法:A.mul(B),A/B ,alpha/A
*矩阵乘法:A*B(此处是矩阵乘法,而不是矩阵对应元素相乘)
*矩阵转置:A.t()
*矩阵求逆和求伪逆:A.inv()
*矩阵比较运算:A cmpop B,A cmpop alpha , alphga cmpop A。此处的cmpop可以是>,>=,==,!=,<=,<。如果条件成立则结果矩阵?(8U类型矩阵)的对应元素被设置为255,否则设置为0。
*矩阵位逻辑运算:A logicop B,A logicop s , s logicop A,~A,此处logicop可以是&,|和^。
*矩阵对应元素的最大值和最小值:min(A,B), min(A,alpha), max(A,B), max(A,alpha)。
*矩阵中元素的绝对值:abs(A)
*叉积和点积:A.cross(B),A.dot(B)
下面的例程展示了Mat表达式的使用方法,例程的输出结果如下所示。
#include<iostream> #include<stdio.h> #include"opencv2/opencv.hpp" using namespace std; using namespace cv; int main(int argc,char* argv[]) { Mat M(600,800,CV_8UC1); for(int i=0;i<M.rows;i++) { uchar *p=M.ptr<uchar>(i); //声明一个uchar指针,并且将矩阵M的第i行头指针赋给该指针,单位是uchar。 for(int j=0;j<M.cols;j++) { double d1=(double)((i+j%255)); //用at读写像素的时候需要指定类型。 M.at<uchar>(i,j)=d1; //下面的代码错误,应该使用at<uchar>() //但是编译时不会提醒错误 //运行结果不正确,d2不等于d1. double d2 = M.at<double>(i,j); } } //在变量声明时指定矩阵元素类型 Mat_<uchar> M1 = (Mat_<uchar>&)M; for(int i=0;i<M1.rows;i++) { uchar *p=M1.ptr(i); for(int j=0;j<M1.cols;j++) { double d1 = (double)((i+j)%255); M1(i,j)=d1; double d2=M1(i,j); } } return 0; }
Mat类的内存管理
使用Mat类,内存管理变得简单,不再像使用iplimage那样需要自己申请和释放内存了。虽然不了解Mat的内存管理机制,也无妨Mat类的使用,但是如果清楚了解Mat的内存管理机制,会更清楚一些函数到底执行了那些数据。
Mat是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸,储存方法,储存地址等信息)和一个指向储存所有像素值的矩阵的指针,如图所示矩阵头的尺寸是常数值,但是矩阵的尺寸会依图像的不同而不同,通常比矩阵头大数个数量级。复制矩阵数据往往需要花费较多的时间,所以一般情况下不要赋值较大矩阵,能够复用就尽量复用。
为了解决矩阵数据的传递,OpenCV使用了引用计数机制。其思路是让每个Mat对象有自己的矩阵头信息,但是多个Mat对象可以共享一个矩阵数据。让矩阵指针指向同一地址而实现这一目的。很多函数以及很多操作(比如函数参数传递),支付至矩阵头信息,而不复制矩阵数据。
前面提到过,有很多方法创建Mat类。如果Mat类自己申请数据空间,那么会多申请4个字节,多出的四个字节储存数据被引用的次数。引用次数储存于数据空间的后面,refcount指向这个位置,如图所示当计数为0的时候就释放该空间。
关于多个矩阵对象共享同一矩阵数据,我们可以看这个例子:
Mat A(100,100,CV_8UC1); Mat B=A; Mat C = A(Rect(50,50,30,30));
上面的代码有三个Mat对象,分别是A,B和C。这三者共有同一矩阵数据其示意图如:
输出
从前面的例程,可以看到Mat类重载了<<运算符,可以方便的使用溜操作来输出矩阵的内容。默认情况下输出的格式是类似于Matlab中矩阵的输出格式。除了默认格式,Mat也支持其他的输出格式。代码如下:
首先创建一个矩阵,并用随机数填充。填充的范围由randu()函数的第二个和第三个参数去诶的那个,下面代码是介于0到255之间。
1 #include<iostream> 2 #include<fstream> 3 #include<iostream> 4 #include<stdio.h> 5 #include"opencv2/opencv.hpp" 6 #include<stdlib.h> 7 using namespace std; 8 using namespace cv; 9 int main(int argc,char* argv[]) 10 { 11 FILE *f; 12 ofstream fileio("C:\\Users\\xpower\\Desktop\\jack.txt",ios::out|ios::in); 13 14 Mat R = Mat(300,200,CV_8UC3); 15 16 randu(R,Scalar::all(0),Scalar::all(255));//填充的范围由randu()参数和第三个参数确定,下面代码是介于0到255之间。 17 18 //默认格式输出的代码如下: 19 imshow("jack",R); 20 21 //cout<<"R (default) = "<<endl<<R<<endl<<endl; 22 fileio<<R<<endl; 23 fileio.close(); 24 printf("end!\n"); 25 waitKey(0); 26 }
Mat与iplimage和CvMat的转换
在OpenCV2中虽然引入了方便的Mat类,处于兼容性的考虑,OpenCV依然支持C语言接口的iplimage和CvMat结果,如果想要和以前的代码兼容将会涉及到Mat与iplimage和CvMat的转换。
Mat转为IpLimage和CvMat的转换
。。。。。
读写图像文件
将图像文件读入内存,可以使用imread()函数;将Mat对象以图像文件格式写入内存,可以使用imwrite()函数。
读取图像文件
imread函数返回的是Mat对象,如果读取文件失败,则会返回一个空矩阵,即Mat::data的值食NULL.执行imread()之后, 需要检查文件是否读入成功, 你可以使用Mat::empty()函数进行检查。imread()函数的声明如下。
Mat imread(const string &filename,int flag=1)
很明显参数filename就是被读取或者保存的文件名。在imread()函数中flag的取值有三种情况:
flag>0, 该函数返回3通道图像,如果磁盘上的图像文件时单通道的则会被强制转换为三通道;
flag=0,该函数返回单通道图像,如果磁盘的文件是强制转化为单通道图像。
flag<0, 函数不对图像做通道转换。
imread()函数支持多种文件格式,且该函数是根据图像文件的内容来确定文件格式,而不是根据文件的扩展名来确定,文件格式名如下。
Windows位图文件-BMP,DIB;
JPEG文件-JPEG,JPG,JPE;
便携式网络图片-PNG
便携式图像格式-PBM,PGM,PPM;
Sun rasters - SR,RAS;
TIFF文件-TIFF,TIF
OpenEXR HDR图片,EXR
JPEG图片 - jp2
你所安装的OpenCV并不一定能支持上述所有格式,文件格式的支持需要特定的库,只有在编译OpenCV添加了相应的的文件格式库,才可支持其格式,。
写图像文件
将图像写入文件,可以使用imwrite()函数,该函数的声明如下:
bool imwrite(const string &filename,InputArray image,const vector<int> ¶ms=vector<int>())
文件的格式由filename参数指定的文件扩展名确定。推荐使用PNG文件格式。BMP格式是无损格式,但是一般不进行压缩,文件尺寸非常大;JPEG格式的文件娇小,但是JPEG是有损压缩,会丢失一些信息。PNG是无损压缩格式,推荐使用。
imwrite()函数的第三个参数params可以指定文件格式的一些细节信息。这个参数里面的数值是跟文件格式相关的:
*JPEG:表示图像的质量,取值范围从0到100。数值越大表示图像质量越高,当然文件也越大。默认值是95。
*PNG:表示压缩级别,取值范围是从0到9。数值越大表示文件越小,但是压缩花费的时间也越长。默认值是3。
*PPM,PGM或PBM:表示文件是以二进制还是纯文本方式存储,取值为0或1。如果取值为1,则表示以二进制方式存储。默认值是1。
并不是所有的Mat对象都可以存为图像文件,目前支持的格式只有8U类型的单通道和3通道(颜色顺序为BGR)矩阵;如果需要要保存16U格式图像,只能使用PNG、JPEG 2000和TIFF格式。如果希望将其他格式的矩阵保存为图像文件,可以先用Mat::convertTo()函数或者cvtColor()函数将矩阵转为可以保存的格式。
另外需要注意的是,在保存文件时,如果文件已经存在,imwrite()进行提醒,将直接覆盖掉以前的文件。
下面例程展示了如何读入一副图像,然后对图像进行Canny边缘操作,最后将结果保存到图像文件中。
#include<iostream> #include<opencv2/opencv.hpp> using namespace std; using namespace cv; int main(int argc,char *argv[]) { //读入图像并将之转化为单通道图像 Mat im = imread("C:\\Users\\xpower\\Desktop\\niao.jpg",0); //检查是否读取成功 if(im.empty()) { cout<<"读取文件失败"<<endl; return -1; } //进行Canny操作,并将结果储存于result Mat result; Canny(im,result,50,150); //保存结果 imwrite("C:\\Users\\xpower\\Desktop\\niao.jpg",result); imshow("niaoniao",result); waitKey(0); }
读写视频
介绍OpenCV读写视频之前,先介绍一下编解码器(codec)。如果是图像文件,我们可以根据文件扩展名得知图像的格式。但是此经验并不能推广到视频文件中。有些OpenCV用户会碰到奇怪的问题,都是avi视频文件,有的能用OpenCV打开,有的不能。
视频的格式主要由压缩算法决定。压缩算法称之为编码器(coder),解压算法称之为解码器(decoder),编解码算法可以统称为编解码器(codec)。视频文件能读或者写,关键看是否有相应的编解码器。编解码器的种类非常多,常用的有MJPG、XVID、DIVX等,完整的列表请参考FOURCC网站3。因此视频文件的扩展名(如avi等)往往只能表示这是一个视频文件。
OpenCV 2中提供了两个类来实现视频的读写。读视频的类是VideoCapture,写视频的类是VideoWriter。
读视频
VideoCapture既可以从视频文件读取图像,也可以从摄像头读取图像。可以使用该类的构造函数打开视频文件或者摄像头。如果VideoCapture对象已经创建,也可以使用VideoCapture::open()打开,VideoCapture::open()函数会自动调用VideoCapture::release()函数,先释放已经打开的视频,然后再打开新视频。
如果要读一帧,可以使用VideoCapture::read()VideoCapture类重载了操作符,实现了读视频帧的功能。下面的例程演示了使用VideoCapture类读视频。
1 #include<iostream> 2 #include"opencv2/opencv.hpp" 3 using namespace std; 4 using namespace cv; 5 int main(int argc,char** argv) 6 { 7 8 //打开第一个摄像头 9 //VideoCapture cap(0); 10 11 //打开视频文件 12 VideoCapture cap("video.short.raw.avi");// 文件名就是这个。 13 if(!cap.isOpened()) 14 { 15 cerr<<"Can not open a camera or file."<<endl; 16 return -1; 17 } 18 Mat edges; 19 namedWindow("jackchen",1); 20 int i=0; 21 for(;;) 22 { 23 cerr<<i<<endl; 24 i++; 25 Mat frame; 26 //从cap中读一帧,存到frame。 27 cap>>frame; 28 //如果没有读到图像 29 if(frame.empty()) 30 { 31 // cerr<<"没有读取到图像!"<<endl; 32 break; 33 } 34 //将读取到的图像转化为灰度图 35 cvtColor(frame,edges,CV_BGR2GRAY); 36 //进行边缘提取操作 37 Canny(edges,edges,0,30,3); 38 //显示结果 39 imshow("edges",edges); 40 if(waitKey(30)>=0) 41 break; 42 43 } 44 return 0; 45 }
写视频
使用OpenCV创建视频也非常简单,与毒食品不同的是,你需要在创建视频时设置一系列参数,包括:文件名,编解码器,帧率,宽度和高度等。编解码器使用四个字符表示,可以是CV_FOURCC('M','J','P','G'), CV_FOURCC('X','V','I','D')以及CV_FOURCC('D','I','V','X');等。如果使用编码器无法创建视频文件,请尝试用其他的编码器。
将图像写入视频可以用VideoWrite::Write函数,VideoWrite类中也重载了<<运算符,使用起来非常方便。另外需要注意的是待写入图像的尺寸必须和写入图像的尺寸一致。
下面的例程演示了如何写视频文件。本例程将生成一个视频文件,视频的第0帧上是一个红色的“0”,第一帧上是一个红色的“1”,以此类推共100帧。
#include<stdio.h> #include<iostream> #include"opencv2/opencv.hpp" using namespace std; using namespace cv; int main(int argc,char **argv) { //定义视频的高度和宽度 Size s(320,240); //创建Write,并指定Fourcc和FPS等参数 VideoWriter writer = VideoWriter("MyVideo.avi",CV_FOURCC('M','J','P','G'),25,s); //检查是否创建成功 if(!writer.isOpened()) { cerr<<"创建视频文件失败\n"; return -1; } //视频帧 Mat frame(s,CV_8UC3); for(int i=0;i<100;i++) { //将图像设置成黑色 frame=Scalar::all(0); //将整数i转化为字符串类型 char text[128]; snprintf(text,sizeof(text),"%d",i); //将数字绘到图片上 putText(frame,text,Point(s.width/3,s.height/3),FONT_HERSHEY_SCRIPT_SIMPLEX,3,Scalar(0,0,255),3,8); //将图像写入视频 writer<<frame; } //退出程序自动关闭视频文件 return 0; }