OpenCV之core 模块. 核心功能(2)基本绘图 随机数发生器绘制文字 离散傅立叶变换 输入输出XML和YAML文件 与 OpenCV 1 同时使用
基本繪圖
目的
本節你將學到:
- 如何用?Point?在圖像中定義 2D 點
- 如何以及為何使用?Scalar
- 用OpenCV的函數?line?繪?直線
- 用OpenCV的函數?ellipse?繪?橢圓
- 用OpenCV的函數?rectangle?繪?矩形
- 用OpenCV的函數?circle?繪?圓
- 用OpenCV的函數?fillPoly?繪?填充的多邊形
OpenCV 原理
本節中,我門將大量使用?Point?和?Scalar?這兩個結構:
Point
次數據結構表示了由其圖像坐標??和??指定的2D點??啥x為: Point pt; pt.x = 10; pt.y = 8;或者
Point pt = Point(10, 8);Scalar
-
表示了具有4個元素的數組。次類型在OpenCV中被大量用于傳遞像素值。
-
本節中,我們將進一步用它來表示RGB顏色值(三個參數)。如果用不到第四個參數,則無需定義。
-
我們來看個例子,如果給出以下顏色參數表達式:
Scalar( a, b, c )那么定義的RGB顏色值為:?Red = c,?Green = b?and?Blue = a
代碼
- 這些代碼都來自OpenCV代碼的sample文件夾?;蛘呖?點擊此處?獲取。
代碼分析
我們打算畫兩個例子(原子和賭棍), 所以必須創建兩個圖像和對應的窗口以顯示。
/// 窗口名字 char atom_window[] = "Drawing 1: Atom"; char rook_window[] = "Drawing 2: Rook";/// 創建空全黑像素的空圖像 Mat atom_image = Mat::zeros( w, w, CV_8UC3 ); Mat rook_image = Mat::zeros( w, w, CV_8UC3 );創建用來畫不同幾何形狀的函數。比如用?MyEllipse?和?MyFilledCircle?來畫原子。
/// 1. 畫一個簡單的原子。/// 1.a. 創建橢圓 MyEllipse( atom_image, 90 ); MyEllipse( atom_image, 0 ); MyEllipse( atom_image, 45 ); MyEllipse( atom_image, -45 );/// 1.b. 創建圓 MyFilledCircle( atom_image, Point( w/2.0, w/2.0) );接下來用?MyLine*,*rectangle?和 a?MyPolygon?來畫賭棍:
/// 2. 畫一個賭棍/// 2.a. 創建一個凸多邊形 MyPolygon( rook_image );/// 2.b. 創建矩形 rectangle( rook_image,Point( 0, 7*w/8.0 ),Point( w, w),Scalar( 0, 255, 255 ),-1,8 );/// 2.c. 畫幾條直線 MyLine( rook_image, Point( 0, 15*w/16 ), Point( w, 15*w/16 ) ); MyLine( rook_image, Point( w/4, 7*w/8 ), Point( w/4, w ) ); MyLine( rook_image, Point( w/2, 7*w/8 ), Point( w/2, w ) ); MyLine( rook_image, Point( 3*w/4, 7*w/8 ), Point( 3*w/4, w ) );現在來看看每個函數內部如何定義:
-
MyLine
void MyLine( Mat img, Point start, Point end ) {int thickness = 2;int lineType = 8;line( img,start,end,Scalar( 0, 0, 0 ),thickness,lineType ); }正如我們所見,?MyLine?調用函數?line?來實現以下操作:
- 畫一條從點?start?到點?end?的直線段
- 此線段將被畫到圖像?img?上
- 線的顏色由?Scalar( 0, 0, 0)?來定義,在此其相應RGB值為?黑色
- 線的粗細由?thickness?設定(此處設為 2)
- 此線為8聯通 (lineType?= 8)
-
MyEllipse
void MyEllipse( Mat img, double angle ) {int thickness = 2;int lineType = 8;ellipse( img,Point( w/2.0, w/2.0 ),Size( w/4.0, w/16.0 ),angle,0,360,Scalar( 255, 0, 0 ),thickness,lineType ); }根據以上代碼,我們可看到函數?ellipse?按照以下規則繪制橢圓:
- 橢圓將被畫到圖像?img?上
- 橢圓中心為點?(w/2.0, w/2.0)?并且大小位于矩形?(w/4.0, w/16.0)?內
- 橢圓旋轉角度為?angle
- 橢圓擴展的弧度從?0?度到?360?度
- 圖形顏色為?Scalar( 255, 255, 0)?,既藍色
- 繪橢圓的線粗為?thickness?,此處是2
-
MyFilledCircle
void MyFilledCircle( Mat img, Point center ) {int thickness = -1;int lineType = 8;circle( img,center,w/32.0,Scalar( 0, 0, 255 ),thickness,lineType ); }類似于橢圓函數,我們可以看到?circle?函數的參數意義如下:
- 圓將被畫到圖像 (?img?)上
- 圓心由點?center?定義
- 圓的半徑為:?w/32.0
- 圓的顏色為:?Scalar(0, 0, 255)?,按BGR的格式為?紅色
- 線粗定義為?thickness?= -1, 因此次圓將被填充
-
MyPolygon
void MyPolygon( Mat img ) {int lineType = 8;/** 創建一些點 */Point rook_points[1][20];rook_points[0][0] = Point( w/4.0, 7*w/8.0 );rook_points[0][1] = Point( 3*w/4.0, 7*w/8.0 );rook_points[0][2] = Point( 3*w/4.0, 13*w/16.0 );rook_points[0][3] = Point( 11*w/16.0, 13*w/16.0 );rook_points[0][4] = Point( 19*w/32.0, 3*w/8.0 );rook_points[0][5] = Point( 3*w/4.0, 3*w/8.0 );rook_points[0][6] = Point( 3*w/4.0, w/8.0 );rook_points[0][7] = Point( 26*w/40.0, w/8.0 );rook_points[0][8] = Point( 26*w/40.0, w/4.0 );rook_points[0][9] = Point( 22*w/40.0, w/4.0 );rook_points[0][10] = Point( 22*w/40.0, w/8.0 );rook_points[0][11] = Point( 18*w/40.0, w/8.0 );rook_points[0][12] = Point( 18*w/40.0, w/4.0 );rook_points[0][13] = Point( 14*w/40.0, w/4.0 );rook_points[0][14] = Point( 14*w/40.0, w/8.0 );rook_points[0][15] = Point( w/4.0, w/8.0 );rook_points[0][16] = Point( w/4.0, 3*w/8.0 );rook_points[0][17] = Point( 13*w/32.0, 3*w/8.0 );rook_points[0][18] = Point( 5*w/16.0, 13*w/16.0 );rook_points[0][19] = Point( w/4.0, 13*w/16.0) ;const Point* ppt[1] = { rook_points[0] };int npt[] = { 20 };fillPoly( img,ppt,npt,1,Scalar( 255, 255, 255 ),lineType );}我們用函數 :fill_poly:`fillPoly <>` 來繪制填充的多邊形。請注意:- 多邊形將被畫到圖像?img?上
- 多邊形的頂點集為?ppt
- 要繪制的多邊形頂點數目為?npt
- 要繪制的多邊形數量僅為?1
- 多邊形的顏色定義為?Scalar( 255, 255, 255), 既BGR值為?白色
-
rectangle
rectangle( rook_image,Point( 0, 7*w/8.0 ),Point( w, w),Scalar( 0, 255, 255 ),-1,8 );最后是函數:rectangle:rectangle <>?(我們并沒有為這家伙創建特定函數)。請注意:
- 矩形將被畫到圖像?rook_image?上
- 矩形兩個對角頂點為?Point( 0, 7*w/8.0 )?和?Point( w, w)
- 矩形的顏色為?Scalar(0, 255, 255)?,既BGR格式下的?黃色
- 由于線粗為?-1, 此矩形將被填充
結果
編譯并運行例程,你將看到如下結果:
隨機數發生器&繪制文字
目的
本節你將學到:
- 使用?隨機數發生器類?(RNG) 并得到均勻分布的隨機數。
- 通過使用函數?putText?顯示文字。
代碼
- 在之前的章節中 (基本繪圖) 我們繪制過不同的幾何圖形, 我提供了一些繪制參數,比如 coordinates(坐標) (在繪制點Points?的時候 ), color(顏色), thickness(線條-粗細,點-大小), 等等... ,你會發現我們給出了這些參數明確的數值。
- 在本章中, 我們會試著賦予這些參數?random隨機?的數值。 并且, 我們會試圖在圖像上繪制大量的幾何圖形. 因為我們將用隨機的方式初始化這些圖形, 這個過程將很自然的用到?loops循環?.
- 本代碼在OpenCV的sample文件夾下,如果招不到,你可以從這里?here 得到它:?.
說明
讓我們檢視?main?函數。我們發現第一步是實例化一個?Random Number Generator(隨機數發生器對象)?(RNG):
RNG rng( 0xFFFFFFFF );RNG的實現了一個隨機數發生器。 在上面的例子中,?rng?是用數值?0xFFFFFFFF?來實例化的一個RNG對象。
然后我們初始化一個?0?矩陣(代表一個全黑的圖像), 并且指定它的寬度,高度,和像素格式:
/// 初始化一個0矩陣 Mat image = Mat::zeros( window_height, window_width, CV_8UC3 );/// 把它會知道一個窗口中 imshow( window_name, image );然后我們開始瘋狂的繪制。看過代碼時候你會發現它主要分八個部分,正如函數定義的一樣:
/// 現在我們先畫線 c = Drawing_Random_Lines(image, window_name, rng); if( c != 0 ) return 0;/// 繼續,這次是一些矩形 c = Drawing_Random_Rectangles(image, window_name, rng); if( c != 0 ) return 0;/// 畫一些弧線 c = Drawing_Random_Ellipses( image, window_name, rng ); if( c != 0 ) return 0;/// 畫一些折線 c = Drawing_Random_Polylines( image, window_name, rng ); if( c != 0 ) return 0;/// 畫被填充的多邊形 c = Drawing_Random_Filled_Polygons( image, window_name, rng ); if( c != 0 ) return 0;/// 畫圓 c = Drawing_Random_Circles( image, window_name, rng ); if( c != 0 ) return 0;/// 在隨機的地方繪制文字 c = Displaying_Random_Text( image, window_name, rng ); if( c != 0 ) return 0;/// Displaying the big end! c = Displaying_Big_End( image, window_name, rng );所有這些范數都遵循相同的模式,所以我們只分析其中的一組,因為這適用于所有。
查看函數?Drawing_Random_Lines:
int Drawing_Random_Lines( Mat image, char* window_name, RNG rng ) {int lineType = 8;Point pt1, pt2;for( int i = 0; i < NUMBER; i++ ){pt1.x = rng.uniform( x_1, x_2 );pt1.y = rng.uniform( y_1, y_2 );pt2.x = rng.uniform( x_1, x_2 );pt2.y = rng.uniform( y_1, y_2 );line( image, pt1, pt2, randomColor(rng), rng.uniform(1, 10), 8 );imshow( window_name, image );if( waitKey( DELAY ) >= 0 ){ return -1; }}return 0; }我們可以看到:
-
for?循環將重復?NUMBER?次。 并且函數?line?在循環中, 這意味著要生成?NUMBER?條線段。
-
線段的兩個端點分別是?pt1?和?pt2. 對于?pt1?我們看到:
pt1.x = rng.uniform( x_1, x_2 ); pt1.y = rng.uniform( y_1, y_2 );-
我們知道?rng?是一個?隨機數生成器?對象。在上面的代碼中我們調用了?rng.uniform(a,b)?。這指定了一個在?a和?b?之間的均勻分布(包含?a, 但不含?b)。
-
由上面的說明,我們可以推斷出?pt1?和?pt2?將會是隨機的數值,因此產生的線段是變幻不定的,這會產生一個很好的視覺效果(從下面繪制的圖片可以看出)。
-
我們還可以發現, 在?line?的參數設置中,對于?color?的設置我們用了:
randomColor(rng)讓我們來看看函數的實現:
static Scalar randomColor( RNG& rng ){int icolor = (unsigned) rng;return Scalar( icolor&255, (icolor>>8)&255, (icolor>>16)&255 );}正如我們看到的,函數的返回值是一個用三個隨機數初始化的?Scalar?對象,這三個隨機數代表了顏色的?R,?G,?B分量。所以,線段的顏色也是隨機的!
-
上面的解釋同樣適用于其它的幾何圖形,比如說參數?center(圓心)?和?vertices(頂點)?也是隨機的。
在結束之前,我們還應該看看函數?Display_Random_Text?和?Displaying_Big_End, 因為它們有一些有趣的特征:
Display_Random_Text:
int Displaying_Random_Text( Mat image, char* window_name, RNG rng ) {int lineType = 8;for ( int i = 1; i < NUMBER; i++ ){Point org;org.x = rng.uniform(x_1, x_2);org.y = rng.uniform(y_1, y_2);putText( image, "Testing text rendering", org, rng.uniform(0,8),rng.uniform(0,100)*0.05+0.1, randomColor(rng), rng.uniform(1, 10), lineType);imshow( window_name, image );if( waitKey(DELAY) >= 0 ){ return -1; }}return 0; }這些看起來都很熟悉,但是這一句:
putText( image, "Testing text rendering", org, rng.uniform(0,8),rng.uniform(0,100)*0.05+0.1, randomColor(rng), rng.uniform(1, 10), lineType);函數?putText?都做了些什么?在我們的例子中:
- 在?image?上繪制文字?“Testing text rendering”?。
- 文字的左下角將用點?org?指定。
- 字體參數是用一個在??之間的整數來定義。
- 字體的縮放比例是用表達式?rng.uniform(0, 100)x0.05 + 0.1?指定(表示它的范圍是?)。
- 字體的顏色是隨機的 (記為?randomColor(rng))。
- 字體的粗細范圍是從 1 到 10, 表示為?rng.uniform(1,10)?。
因此, 我們將繪制 (與其余函數類似)?NUMBER?個文字到我們的圖片上,以位置隨機的方式。
Displaying_Big_End
int Displaying_Big_End( Mat image, char* window_name, RNG rng ) {Size textsize = getTextSize("OpenCV forever!", CV_FONT_HERSHEY_COMPLEX, 3, 5, 0);Point org((window_width - textsize.width)/2, (window_height - textsize.height)/2);int lineType = 8;Mat image2;for( int i = 0; i < 255; i += 2 ){image2 = image - Scalar::all(i);putText( image2, "OpenCV forever!", org, CV_FONT_HERSHEY_COMPLEX, 3,Scalar(i, i, 255), 5, lineType );imshow( window_name, image2 );if( waitKey(DELAY) >= 0 ){ return -1; }}return 0; }除了?getTextSize?(用于獲取文字的大小參數), 我們可以發現在?for?循環里的新操作:
image2 = image - Scalar::all(i)**image2** 是 **image** 和 **Scalar::all(i)** 的差。事實上,**image2** 的每個像素都是 **image** 的每個像素減去 **i** (對于每個像素,都是由R,G,B三個分量組成,每個分量都會獨立做差)的差。結果
正如你在代碼部分看到的, 程序將依次執行不同的繪圖函數,這將:
首先?NUMBER?條線段將出現在屏幕上,正如截圖所示:
然后,一個新的圖形,這次是一些矩形:
現在,一些弧線會出現,每一個弧線都有隨機的位置,大小,邊緣的粗細和弧長:
現在,帶有三個參數的?polylines(折線)?將會出現在屏幕上,同樣以隨機的方式:
填充的多邊形 (這里是三角形) 會出現.
最后出現的圖形:圓
在結尾處,文字?“Testing Text Rendering”?將會以不同的字體,大小,顏色和位置出現在屏幕上。
最后 (這也順便表達了OpenCV的宗旨):
離散傅立葉變換
目標
本文檔嘗試解答如下問題:
- 什么是傅立葉變換及其應用?
- 如何使用OpenCV提供的傅立葉變換?
- 相關函數的使用,如:?copyMakeBorder(),?merge(),?dft(),?getOptimalDFTSize(),?log()?和?normalize()?.
源碼
你可以?從此處下載源碼?或者通過OpenCV源碼庫文件samples/cpp/tutorial_code/core/discrete_fourier_transform/discrete_fourier_transform.cpp?查看.
以下為函數?dft()?使用范例:
| 123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #include "opencv2/core/core.hpp" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> int main(int argc, char ** argv) {const char* filename = argc >=2 ? argv[1] : "lena.jpg";Mat I = imread(filename, CV_LOAD_IMAGE_GRAYSCALE);if( I.empty())return -1;Mat padded; //expand input image to optimal sizeint m = getOptimalDFTSize( I.rows );int n = getOptimalDFTSize( I.cols ); // on the border add zero valuescopyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)};Mat complexI;merge(planes, 2, complexI); // Add to the expanded another plane with zerosdft(complexI, complexI); // this way the result may fit in the source matrix// compute the magnitude and switch to logarithmic scale// => log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude Mat magI = planes[0];magI += Scalar::all(1); // switch to logarithmic scalelog(magI, magI);// crop the spectrum, if it has an odd number of rows or columnsmagI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2));// rearrange the quadrants of Fourier image so that the origin is at the image center int cx = magI.cols/2;int cy = magI.rows/2;Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - Create a ROI per quadrant Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-RightMat q2(magI, Rect(0, cy, cx, cy)); // Bottom-LeftMat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-RightMat tmp; // swap quadrants (Top-Left with Bottom-Right)q0.copyTo(tmp);q3.copyTo(q0);tmp.copyTo(q3);q1.copyTo(tmp); // swap quadrant (Top-Right with Bottom-Left)q2.copyTo(q1);tmp.copyTo(q2);normalize(magI, magI, 0, 1, CV_MINMAX); // Transform the matrix with float values into a // viewable image form (float between values 0 and 1).imshow("Input Image" , I ); // Show the resultimshow("spectrum magnitude", magI); waitKey();return 0; } |
原理
對一張圖像使用傅立葉變換就是將它分解成正弦和余弦兩部分。也就是將圖像從空間域(spatial domain)轉換到頻域(frequency domain)。 這一轉換的理論基礎來自于以下事實:任一函數都可以表示成無數個正弦和余弦函數的和的形式。傅立葉變換就是一個用來將函數分解的工具。 2維圖像的傅立葉變換可以用以下數學公式表達:
式中 f 是空間域(spatial domain)值, F 則是頻域(frequency domain)值。 轉換之后的頻域值是復數, 因此,顯示傅立葉變換之后的結果需要使用實數圖像(real image) 加虛數圖像(complex image), 或者幅度圖像(magitude image)加相位圖像(phase image)。 在實際的圖像處理過程中,僅僅使用了幅度圖像,因為幅度圖像包含了原圖像的幾乎所有我們需要的幾何信息。 然而,如果你想通過修改幅度圖像或者相位圖像的方法來間接修改原空間圖像,你需要使用逆傅立葉變換得到修改后的空間圖像,這樣你就必須同時保留幅度圖像和相位圖像了。
在此示例中,我將展示如何計算以及顯示傅立葉變換后的幅度圖像。由于數字圖像的離散性,像素值的取值范圍也是有限的。比如在一張灰度圖像中,像素灰度值一般在0到255之間。 因此,我們這里討論的也僅僅是離散傅立葉變換(DFT)。 如果你需要得到圖像中的幾何結構信息,那你就要用到它了。請參考以下步驟(假設輸入圖像為單通道的灰度圖像?I):
將圖像延擴到最佳尺寸. 離散傅立葉變換的運行速度與圖片的尺寸息息相關。當圖像的尺寸是2, 3,5的整數倍時,計算速度最快。 因此,為了達到快速計算的目的,經常通過添湊新的邊緣像素的方法獲取最佳圖像尺寸。函數?getOptimalDFTSize()返回最佳尺寸,而函數?copyMakeBorder()?填充邊緣像素:
Mat padded; //將輸入圖像延擴到最佳的尺寸 int m = getOptimalDFTSize( I.rows ); int n = getOptimalDFTSize( I.cols ); // 在邊緣添加0 copyMakeBorder(I, padded, 0, m - I.rows, 0, n - I.cols, BORDER_CONSTANT, Scalar::all(0));添加的像素初始化為0.
為傅立葉變換的結果(實部和虛部)分配存儲空間. 傅立葉變換的結果是復數,這就是說對于每個原圖像值,結果是兩個圖像值。 此外,頻域值范圍遠遠超過空間值范圍, 因此至少要將頻域儲存在?float?格式中。 結果我們將輸入圖像轉換成浮點類型,并多加一個額外通道來儲存復數部分:
Mat planes[] = {Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F)}; Mat complexI; merge(planes, 2, complexI); // 為延擴后的圖像增添一個初始化為0的通道進行離散傅立葉變換. 支持圖像原地計算 (輸入輸出為同一圖像):
dft(complexI, complexI); // 變換結果很好的保存在原始矩陣中將復數轉換為幅度.復數包含實數部分(Re)和復數部分 (imaginary -?Im)。 離散傅立葉變換的結果是復數,對應的幅度可以表示為:
轉化為OpenCV代碼:
split(complexI, planes); // planes[0] = Re(DFT(I), planes[1] = Im(DFT(I)) magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude Mat magI = planes[0];對數尺度(logarithmic scale)縮放. 傅立葉變換的幅度值范圍大到不適合在屏幕上顯示。高值在屏幕上顯示為白點,而低值為黑點,高低值的變化無法有效分辨。為了在屏幕上凸顯出高低變化的連續性,我們可以用對數尺度來替換線性尺度:
轉化為OpenCV代碼:
magI += Scalar::all(1); // 轉換到對數尺度 log(magI, magI);剪切和重分布幅度圖象限. 還記得我們在第一步時延擴了圖像嗎? 那現在是時候將新添加的像素剔除了。為了方便顯示,我們也可以重新分布幅度圖象限位置(注:將第五步得到的幅度圖從中間劃開得到四張1/4子圖像,將每張子圖像看成幅度圖的一個象限,重新分布即將四個角點重疊到圖片中心)。 這樣的話原點(0,0)就位移到圖像中心。
magI = magI(Rect(0, 0, magI.cols & -2, magI.rows & -2)); int cx = magI.cols/2; int cy = magI.rows/2;Mat q0(magI, Rect(0, 0, cx, cy)); // Top-Left - 為每一個象限創建ROI Mat q1(magI, Rect(cx, 0, cx, cy)); // Top-Right Mat q2(magI, Rect(0, cy, cx, cy)); // Bottom-Left Mat q3(magI, Rect(cx, cy, cx, cy)); // Bottom-RightMat tmp; // 交換象限 (Top-Left with Bottom-Right) q0.copyTo(tmp); q3.copyTo(q0); tmp.copyTo(q3);q1.copyTo(tmp); // 交換象限 (Top-Right with Bottom-Left) q2.copyTo(q1); tmp.copyTo(q2);歸一化. 這一步的目的仍然是為了顯示。 現在我們有了重分布后的幅度圖,但是幅度值仍然超過可顯示范圍[0,1] 。我們使用normalize()?函數將幅度歸一化到可顯示范圍。
結果
離散傅立葉變換的一個應用是決定圖片中物體的幾何方向.比如,在文字識別中首先要搞清楚文字是不是水平排列的? 看一些文字,你就會注意到文本行一般是水平的而字母則有些垂直分布。文本段的這兩個主要方向也是可以從傅立葉變換之后的圖像看出來。我們使用這個?水平文本圖像?以及?旋轉文本圖像?來展示離散傅立葉變換的結果 。
水平文本圖像:
旋轉文本圖像:
觀察這兩張幅度圖你會發現頻域的主要內容(幅度圖中的亮點)是和空間圖像中物體的幾何方向相關的。 通過這點我們可以計算旋轉角度并修正偏差。
輸入輸出XML和YAML文件
目的
你將得到以下幾個問題的答案:
- 如何將文本寫入YAML或XML文件,及如何從從OpenCV中讀取YAML或XML文件中的文本
- 如何利用YAML或XML文件存取OpenCV數據結構
- 如何利用YAML或XML文件存取自定義數據結構?
- OpenCV中相關數據結構的使用方法,如 :xmlymlpers:FileStorage <filestorage>,?FileNode?或?FileNodeIterator.
代碼
你可以?點擊此處下載?或直接從OpenCV代碼庫中找到源文件。samples/cpp/tutorial_code/core/file_input_output/file_input_output.cpp?。
以下用簡單的示例代碼演示如何逐一實現所有目的.
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | #include <opencv2/core/core.hpp> #include <iostream> #include <string>using namespace cv; using namespace std;class MyData { public:MyData() : A(0), X(0), id(){}explicit MyData(int) : A(97), X(CV_PI), id("mydata1234") // explicit to avoid implicit conversion{}void write(FileStorage& fs) const //Write serialization for this class{fs << "{" << "A" << A << "X" << X << "id" << id << "}";}void read(const FileNode& node) //Read serialization for this class{A = (int)node["A"];X = (double)node["X"];id = (string)node["id"];} public: // Data Membersint A;double X;string id; };//These write and read functions must be defined for the serialization in FileStorage to work void write(FileStorage& fs, const std::string&, const MyData& x) {x.write(fs); } void read(const FileNode& node, MyData& x, const MyData& default_value = MyData()){if(node.empty())x = default_value;elsex.read(node); }// This function will print our custom class to the console ostream& operator<<(ostream& out, const MyData& m) { out << "{ id = " << m.id << ", ";out << "X = " << m.X << ", ";out << "A = " << m.A << "}";return out; }int main(int ac, char** av) {if (ac != 2){help(av);return 1;}string filename = av[1];{ //writeMat R = Mat_<uchar>::eye(3, 3),T = Mat_<double>::zeros(3, 1);MyData m(1);FileStorage fs(filename, FileStorage::WRITE);fs << "iterationNr" << 100;fs << "strings" << "["; // text - string sequencefs << "image1.jpg" << "Awesomeness" << "baboon.jpg";fs << "]"; // close sequencefs << "Mapping"; // text - mappingfs << "{" << "One" << 1;fs << "Two" << 2 << "}"; fs << "R" << R; // cv::Matfs << "T" << T;fs << "MyData" << m; // your own data structuresfs.release(); // explicit closecout << "Write Done." << endl;}{//readcout << endl << "Reading: " << endl;FileStorage fs; fs.open(filename, FileStorage::READ);int itNr; //fs["iterationNr"] >> itNr;itNr = (int) fs["iterationNr"];cout << itNr;if (!fs.isOpened()){cerr << "Failed to open " << filename << endl;help(av);return 1;}FileNode n = fs["strings"]; // Read string sequence - Get nodeif (n.type() != FileNode::SEQ){cerr << "strings is not a sequence! FAIL" << endl;return 1;}FileNodeIterator it = n.begin(), it_end = n.end(); // Go through the nodefor (; it != it_end; ++it)cout << (string)*it << endl;n = fs["Mapping"]; // Read mappings from a sequencecout << "Two " << (int)(n["Two"]) << "; "; cout << "One " << (int)(n["One"]) << endl << endl; MyData m;Mat R, T;fs["R"] >> R; // Read cv::Matfs["T"] >> T;fs["MyData"] >> m; // Read your own structure_cout << endl << "R = " << R << endl;cout << "T = " << T << endl << endl;cout << "MyData = " << endl << m << endl << endl;//Show default behavior for non existing nodescout << "Attempt to read NonExisting (should initialize the data structure with its default)."; fs["NonExisting"] >> m;cout << endl << "NonExisting = " << endl << m << endl;}cout << endl << "Tip: Open up " << filename << " with a text editor to see the serialized data." << endl;return 0; } |
代碼分析
這里我們僅討論XML和YAML文件輸入。你的輸出(和相應的輸入)文件可能僅具有其中一個擴展名以及對應的文件結構。XML和YAML的串行化分別采用兩種不同的數據結構:?mappings?(就像STL map) 和?element sequence?(比如 STL vector>。二者之間的區別在map中每個元素都有一個唯一的標識名供用戶訪問;而在sequences中你必須遍歷所有的元素才能找到指定元素。
XML\YAML 文件的打開和關閉。?在你寫入內容到此類文件中前,你必須先打開它,并在結束時關閉它。在OpenCV中標識XML和YAML的數據結構是?FileStorage?。要將此結構和硬盤上的文件綁定時,可使用其構造函數或者?open()?函數:
string filename = "I.xml"; FileStorage fs(filename, FileStorage::WRITE); \\... fs.open(filename, FileStorage::READ);無論以哪種方式綁定,函數中的第二個參數都以常量形式指定你要對文件進行操作的類型,包括:WRITE, READ 或 APPEND。文件擴展名決定了你將采用的輸出格式。如果你指定擴展名如?.xml.gz?,輸出甚至可以是壓縮文件。
當?FileStorage?對象被銷毀時,文件將自動關閉。當然你也可以顯示調用?release?函數:
fs.release(); // 顯示關閉輸入\輸出文本和數字。?數據結構使用與STL相同的 << 輸出操作符。輸出任何類型的數據結構時,首先都必須指定其標識符,這通過簡單級聯輸出標識符即可實現。基本類型數據輸出必須遵循此規則:
fs << "iterationNr" << 100;讀入則通過簡單的尋址(通過 [] 操作符)操作和強制轉換或 >> 操作符實現:
int itNr; fs["iterationNr"] >> itNr; itNr = (int) fs["iterationNr"];輸入\輸出OpenCV數據結構。?其實和對基本類型的操作方法是相同的:
Mat R = Mat_<uchar >::eye (3, 3),T = Mat_<double>::zeros(3, 1);fs << "R" << R; // 寫 cv::Mat fs << "T" << T;fs["R"] >> R; // 讀 cv::Mat fs["T"] >> T;輸入\輸出 vectors(數組)和相應的maps.?之前提到我們也可以輸出maps和序列(數組, vector)。同樣,首先輸出變量的標識符,接下來必須指定輸出的是序列還是map。
對于序列,在第一個元素前輸出”[“字符,并在最后一個元素后輸出”]“字符:
fs << "strings" << "["; // 文本 - 字符串序列 fs << "image1.jpg" << "Awesomeness" << "baboon.jpg"; fs << "]"; // 序列結束對于maps使用相同的方法,但采用”{“和”}“作為分隔符。
fs << "Mapping"; // 文本 - mapping fs << "{" << "One" << 1; fs << "Two" << 2 << "}";對于數據讀取,可使用?FileNode?和?FileNodeIterator?數據結構。?FileStorage?的[] 操作符將返回一個?FileNode?數據類型。如果這個節點是序列化的,我們可以使用?FileNodeIterator?來迭代遍歷所有元素。
FileNode n = fs["strings"]; // 讀取字符串序列 - 獲取節點 if (n.type() != FileNode::SEQ) {cerr << "strings is not a sequence! FAIL" << endl;return 1; }FileNodeIterator it = n.begin(), it_end = n.end(); // 遍歷節點 for (; it != it_end; ++it)cout << (string)*it << endl;對于maps類型,可以用 [] 操作符訪問指定的元素(或者 >> 操作符):
n = fs["Mapping"]; // 從序列中讀取map cout << "Two " << (int)(n["Two"]) << "; "; cout << "One " << (int)(n["One"]) << endl << endl;讀寫自定義數據類型。?假設你定義了如下數據類型:
class MyData { public:MyData() : A(0), X(0), id() {} public: // 數據成員int A;double X;string id; };添加內部和外部的讀寫函數,就可以使用OpenCV I/O XML/YAML接口對其進行序列化(就像對OpenCV數據結構進行序列化一樣)。內部函數定義如下:
void write(FileStorage& fs) const //對自定義類進行寫序列化 {fs << "{" << "A" << A << "X" << X << "id" << id << "}"; }void read(const FileNode& node) //從序列讀取自定義類 {A = (int)node["A"];X = (double)node["X"];id = (string)node["id"]; }接下來在類的外部定義以下函數:
void write(FileStorage& fs, const std::string&, const MyData& x) { x.write(fs); }void read(const FileNode& node, MyData& x, const MyData& default_value = MyData()) { if(node.empty())x = default_value; elsex.read(node); }這兒可以看到,如果讀取的節點不存在,我們返回默認值。更復雜一些的解決方案是返回一個對象ID為負值的實例。
一旦添加了這四個函數,就可以用 >> 操作符和 << 操作符分別進行讀,寫操作:
MyData m(1); fs << "MyData" << m; // 寫自定義數據結構 fs["MyData"] >> m; // 讀自定義數據結構或試著讀取不存在的值:
fs["NonExisting"] >> m; // 請注意不是 fs << "NonExisting" << m cout << endl << "NonExisting = " << endl << m << endl;結果
好的,大多情況下我們只輸出定義過的成員。在控制臺程序的屏幕上,你將看到:
Write Done.Reading: 100image1.jpg Awesomeness baboon.jpg Two 2; One 1R = [1, 0, 0;0, 1, 0;0, 0, 1] T = [0; 0; 0]MyData = { id = mydata1234, X = 3.14159, A = 97}Attempt to read NonExisting (should initialize the data structure with its default). NonExisting = { id = , X = 0, A = 0}Tip: Open up output.xml with a text editor to see the serialized data.然而, 在輸出的xml文件中看到的結果將更加有趣:
<?xml version="1.0"?> <opencv_storage> <iterationNr>100</iterationNr> <strings>image1.jpg Awesomeness baboon.jpg</strings> <Mapping><One>1</One><Two>2</Two></Mapping> <R type_id="opencv-matrix"><rows>3</rows><cols>3</cols><dt>u</dt><data>1 0 0 0 1 0 0 0 1</data></R> <T type_id="opencv-matrix"><rows>3</rows><cols>1</cols><dt>d</dt><data>0. 0. 0.</data></T> <MyData><A>97</A><X>3.1415926535897931e+000</X><id>mydata1234</id></MyData> </opencv_storage>或YAML文件:
%YAML:1.0 iterationNr: 100 strings:- "image1.jpg"- Awesomeness- "baboon.jpg" Mapping:One: 1Two: 2 R: !!opencv-matrixrows: 3cols: 3dt: udata: [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ] T: !!opencv-matrixrows: 3cols: 1dt: ddata: [ 0., 0., 0. ] MyData:A: 97X: 3.1415926535897931e+000id: mydata1234你也可以看到動態實例:?YouTube here?.
與 OpenCV 1 同時使用
目的
對于OpenCV的開發團隊來說,持續穩定地提高代碼庫非常重要。我們一直在思考如何在使其易用的同時保持靈活性。新的C++接口即為此而來。盡管如此,向下兼容仍然十分重要。我們并不想打斷你基于早期OpenCV庫的開發。因此,我們添加了一些函數來處理這種情況。在以下內容中你將學到:
- 相比第一個版本,第二版的OpenCV在用法上有何改變
- 如何在一幅圖像中加入高斯噪聲
- 什么事查找表及如何使用
概述
在用新版本之前,你首先需要學習一些新的圖像數據結構:?Mat - 基本圖像容器?,它取代了舊的?CvMat?和?IplImage?。轉換到新函數非常容易,你僅需記住幾條新的原則。
OpenCV 2 接受按需定制。所有函數不再裝入一個單一的庫中。我們會提供許多模塊,每個模塊都包含了與其功能相關的數據結構和函數。這樣一來,如果你僅僅需要使用OpenCV的一部分功能,你就不需要把整個巨大的OpenCV庫都裝入你的程序中。使用時,你僅需要包含用到的頭文件,比如:
#include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp>所有OpenCV用到的東西都被放入名字空間?cv?中以避免與其他庫的數據結構和函數名稱的命名沖突。因此,在使用OpenCV庫中的任何定義和函數時,你必須在名稱之前冠以?cv::?,或者在包含頭文件后,加上以下指令:
using namespace cv; // 新的C++接口API都在此名字空間中,需要導入。因為所有庫中函數都已在此名字空間中,所以無需加?cv?作為前綴。據此所有新的C++兼容函數都無此前綴,并且遵循駝峰命名準則。也就是第一個字母為小寫(除非是單個單詞作為函數名,如 Canny)并且后續單詞首字母大寫(如?copyMakeBorder?).
接下來,請記住你需要將所有用到的模塊鏈接到你的程序中。如果你在Windows下開發且用到了?動態鏈接庫(DLL)?,你還需要將OpenCV對應動態鏈接庫的路徑加入程序執行路徑中。關于Windows下開發的更多信息請閱讀?How to build applications with OpenCV inside the Microsoft Visual Studio?;對于Linux用戶,可參考?Using OpenCV with Eclipse (plugin CDT)?中的實例及說明。
你可以使用?IplImage?或?CvMat?操作符來轉換?Mat?對象。在C接口中,你習慣于使用指針,但此處將不再需要。在C++接口中,我們大多數情況下都是用?Mat?對象。此對象可通過簡單的賦值操作轉換為?IplImage?和?CvMat?。示例如下:
Mat I; IplImage pI = I; CvMat mI = I;現在,如果你想獲取指針,轉換就變得麻煩一點。編譯器將不能自動識別你的意圖,所以你需要明確指出你的目的。可以通過調用IplImage?和?CvMat?操作符來獲取他們的指針。我們可以用 & 符號獲取其指針如下:
Mat I; IplImage* pI = &I.operator IplImage(); CvMat* mI = &I.operator CvMat();來自C接口最大的抱怨是它將所有內存管理工作交給你來做。你需要知道何時可以安全釋放不再使用的對象,并且確定在程序結束之前釋放它,否則就會造成討厭的內存泄露。為了繞開這一問題,OpenCV引進了一種智能指針。它將自動釋放不再使用的對象。使用時,指針將被聲明為?Ptr?模板的特化:
Ptr<IplImage> piI = &I.operator IplImage();將C接口的數據結構轉換為?Mat?時,可將其作為構造函數的參數傳入,例如:
Mat K(piL), L; L = Mat(pI);實例學習
現在,你已經學習了最基本的知識。?這里?你將會看到一個混合使用C接口和C++接口的例子。你也可以在可以再OpenCV的代碼庫中的sample目錄中找到此文件samples/cpp/tutorial_code/core/interoperability_with_OpenCV_1/interoperability_with_OpenCV_1.cpp?。為了進一步幫助你認清其中區別,程序支持兩種模式:C和C++混合,以及純C++。如果你宏定義了?DEMO_MIXED_API_USE?,程序將按第一種模式編譯。程序的功能是劃分顏色平面,對其進行改動并最終將其重新合并。
| 123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <stdio.h> #include <iostream>#include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/highgui/highgui.hpp>using namespace cv; // The new C++ interface API is inside this namespace. Import it. using namespace std; #define DEMO_MIXED_API_USE int main( int argc, char** argv ) {const char* imagename = argc > 1 ? argv[1] : "lena.jpg";#ifdef DEMO_MIXED_API_USEPtr<IplImage> IplI = cvLoadImage(imagename); // Ptr<T> is safe ref-counting pointer classif(IplI.empty()){cerr << "Can not load image " << imagename << endl;return -1;}Mat I(IplI); // Convert to the new style container. Only header created. Image not copied. #elseMat I = imread(imagename); // the newer cvLoadImage alternative, MATLAB-style functionif( I.empty() ) // same as if( !I.data ){cerr << "Can not load image " << imagename << endl;return -1;} #endif |
在此,你可一看到新的結構再無指針問題,哪怕使用舊的函數,并在最后結束時將結果轉換為?Mat?對象。
| 1 2 3 4 5 6 | // convert image to YUV color space. The output image will be created automatically. Mat I_YUV;cvtColor(I, I_YUV, CV_BGR2YCrCb); vector<Mat> planes; // Use the STL's vector structure to store multiple Mat objects split(I_YUV, planes); // split the image into separate color planes (Y U V) |
因為我們打算搞亂圖像的亮度通道,所以首先將圖像由默認的RGB顏色空間轉為YUV顏色空間,然后將其劃分為獨立顏色平面(Y,U,V)。第一個例子中,我們對每一個平面用OpenCV中三個主要圖像掃描算法(C []操作符,迭代,單獨元素訪問)中的一個進行處理。在第二個例子中,我們給圖像添加一些高斯噪聲,然后依據一些準則融合所有通道。
運用掃描算法的代碼如下:
| 123456789 10 11 12 13 14 15 16 17 18 19 20 21 | // Method 1. process Y plane using an iteratorMatIterator_<uchar> it = planes[0].begin<uchar>(), it_end = planes[0].end<uchar>();for(; it != it_end; ++it){double v = *it * 1.7 + rand()%21 - 10;*it = saturate_cast<uchar>(v*v/255);}for( int y = 0; y < I_YUV.rows; y++ ){// Method 2. process the first chroma plane using pre-stored row pointer.uchar* Uptr = planes[1].ptr<uchar>(y);for( int x = 0; x < I_YUV.cols; x++ ){Uptr[x] = saturate_cast<uchar>((Uptr[x]-128)/2 + 128);// Method 3. process the second chroma plane using individual element accessuchar& Vxy = planes[2].at<uchar>(y, x);Vxy = saturate_cast<uchar>((Vxy-128)/2 + 128);}} |
此處可看到,我們可以以三種方式遍歷圖像的所有像素:迭代器,C指針和單獨元素訪問方式你可在?OpenCV如何掃描圖像、利用查找表和計時?中獲得更深入的了解。從舊的函數名轉換新版本非常容易,僅需要刪除 cv 前綴,并且使用?Mat?數據結構。下面的例子中使用了加權加法:
| 123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | Mat noisyI(I.size(), CV_8U); // Create a matrix of the specified size and type// Fills the matrix with normally distributed random values (around number with deviation off).// There is also randu() for uniformly distributed random number generationrandn(noisyI, Scalar::all(128), Scalar::all(20)); // blur the noisyI a bit, kernel size is 3x3 and both sigma's are set to 0.5GaussianBlur(noisyI, noisyI, Size(3, 3), 0.5, 0.5); const double brightness_gain = 0;const double contrast_gain = 1.7;#ifdef DEMO_MIXED_API_USE// To pass the new matrices to the functions that only work with IplImage or CvMat do:// step 1) Convert the headers (tip: data will not be copied).// step 2) call the function (tip: to pass a pointer do not forget unary "&" to form pointers)IplImage cv_planes_0 = planes[0], cv_noise = noisyI; cvAddWeighted(&cv_planes_0, contrast_gain, &cv_noise, 1, -128 + brightness_gain, &cv_planes_0); #elseaddWeighted(planes[0], contrast_gain, noisyI, 1, -128 + brightness_gain, planes[0]); #endifconst double color_scale = 0.5;// Mat::convertTo() replaces cvConvertScale. // One must explicitly specify the output matrix type (we keep it intact - planes[1].type())planes[1].convertTo(planes[1], planes[1].type(), color_scale, 128*(1-color_scale));// alternative form of cv::convertScale if we know the datatype at compile time ("uchar" here).// This expression will not create any temporary arrays ( so should be almost as fast as above)planes[2] = Mat_<uchar>(planes[2]*color_scale + 128*(1-color_scale));// Mat::mul replaces cvMul(). Again, no temporary arrays are created in case of simple expressions.planes[0] = planes[0].mul(planes[0], 1./255); |
正如你所見,變量?planes?也是?Mat?類型的。無論如何,將?Mat?轉換為?IplImage?都可通過簡單的賦值操作符自動實現。
| 123456789 10 11 12 13 | merge(planes, I_YUV); // now merge the results backcvtColor(I_YUV, I, CV_YCrCb2BGR); // and produce the output RGB imagenamedWindow("image with grain", CV_WINDOW_AUTOSIZE); // use this to create images#ifdef DEMO_MIXED_API_USE// this is to demonstrate that I and IplI really share the data - the result of the above// processing is stored in I and thus in IplI too.cvShowImage("image with grain", IplI); #elseimshow("image with grain", I); // the new MATLAB style function show |
新的?imshow?highgui函數可接受?Mat?和?IplImage?數據結構。 編譯并運行例程,如果輸入以下第一幅圖像,程序將輸出以下第二幅或者第三幅圖像。
你可以在點擊此處看到動態示例:?YouTube here?,并可以?點擊此處?下載源文件,或者在OpenCV源代碼庫中找到源文件:samples/cpp/tutorial_code/core/interoperability_with_OpenCV_1/interoperability_with_OpenCV_1.cpp?。
from: http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/core/table_of_content_core/table_of_content_core.html#table-of-content-core
總結
以上是生活随笔為你收集整理的OpenCV之core 模块. 核心功能(2)基本绘图 随机数发生器绘制文字 离散傅立叶变换 输入输出XML和YAML文件 与 OpenCV 1 同时使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OpenCV之core 模块. 核心功能
- 下一篇: OpenCV之imgproc 模块. 图