圖像的表示
在正式介紹之前,先簡單介紹一下數字圖像的基本概念。如圖 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; }
