1. 程式人生 > >OpenCV程式碼指南

OpenCV程式碼指南

目錄


  1. 檔案命名
  2. 檔案結構
  3. 函式命名約定
  4. 函式介面設計
  5. 函式實現
  6. 程式碼佈局
  7. 移植性
  8. 函式測試與實現
  9. 附錄

PS:本文是對OpenCV中程式碼風格的簡短說明,因為OpenCV的核心庫是用C和C++編寫的,所以本文僅對用C和C++編寫的程式有效。


1.檔案命名

所有cv和cvaux庫檔案的命名必須服從於以下規則:

  • 所有的CV庫檔名字首為cv
  • 混合的C/C++介面標頭檔案副檔名為 .h
  • 純C++介面標頭檔案副檔名為 .hpp
  • 實現副檔名為 .cpp
  • 為了與POSIX相容,檔名都以小寫字元組成

2.檔案結構

每個檔案以BSD相容的許可宣告開頭;其它標頭檔案和實現檔案的規則包括:

  • 一行最多90個字元,不包括行結束符;
  • 不使用製表符;
  • 縮排為4個空格符,所以製表符應該用1-4個空格替換(依開始列確定);
  • 標頭檔案必須使用保護巨集,防止檔案被重複包含。

混合C/C++介面標頭檔案用extern “C” { } 包含C語言定義。為了使預編譯頭機制在Visual C++中工作正常,原始檔必須在其它標頭檔案前包含precomp.h標頭檔案。同時,請參見標頭檔案和實現檔案的示例。

3.命名約定

OpenCV中使用大小寫混合樣式來標識外部函式、資料型別和類方法。巨集全部使用大寫字元,詞間用下劃線分隔。

所有的外部或內部名稱,若在多個檔案中可見,則必須含有字首:

  • 外部函式使用字首cv
  • 內部函式使用字首Icv
  • 資料結構(C結構體、列舉、聯合體、類)使用字首Cv
  • 外部或某些內部巨集使用字首CV_
  • 內部巨集使用字首ICV_

4.函式介面設計

為了保持庫的一致性,以如下方式設計介面非常重要。函式介面元素包括:

  1. 功能
  2. 名稱
  3. 返回值
  4. 引數型別
  5. 引數順序
  6. 引數預設值

**函式功能必須定義良好並保持精簡。**函式應該容易鑲入到使用其它OpenCV和IPL函式的不同處理過程。函式名稱應該簡單並能體現函式的功能。
以下是OpenCV中的一些基本命名模式:

  1. 大多數函式名形式:cv[],如:cvCalibrateCamera, cvCalcOpticalFlowPyrLK。特殊預定義情況下物件建立、消除、清理不包括消除分別用cvCreate, cvRelease和cvClear。
  2. 有時候函式以它實現的演算法名或它產生的物件的名稱命名。如:cvSobel, cvCanny, cvRodrigues, cvSqrt, cvGoodFeaturesToTrack.
  3. 在對容器元素操作時,函式名以容器型別開頭,緊跟著是動作名;在這種情況下,函式名可以當作方法名。例如:SeqPush, GraphAddEdgeByIdx。
  4. 返回值應該選擇能簡化功能的用法。通常一個函式建立一個物件並返回該物件。對於函式,處理動態資料結構或標量,這是一個好的方法。然而在圖片處理函式中會經常分配和回收大記憶體塊,所以圖片處理函式不能建立和返回影象結果而是修改輸出一個作為引數傳入的影象。
  5. 函式不應該用關於嚴重錯誤(例如空指標,0除數,錯誤引數範圍,不支援的影象格式等)的訊號作為返回值。在這種情況下,可以用一種類似於IPL中特殊的錯誤處理機制。相反,使用期望的執行時情況訊號作為返回值比較好。(例如,跟蹤影象移動到螢幕外)。

引數型別選擇已經存在於OpenCV中的型別更適宜:IplImage用於光柵影象,CvMat用於矩陣,CvSeq用於輪廓線等。建議不使用簡單指標和計數,因為有許多函式引數,它降低了庫介面並使程式更難讀。

一個一致的引數順序很重要,因為它使引數易於記住順序並且幫助程式設計師避免錯誤和使用錯誤的引數順序聯接函式。

對於簡單過程函式(在命名模式列表中的第一種和第二種型別)
典型的順序是: 輸入引數,輸出引數,標記或可選引數

對於容器元素方法,順序是:容器,元素位置,標記或可選引數。
  
  輸入引數經常用const修飾符。可選引數經常簡化函式用法。因為C++允許在引數列表後跟隨可選引數,它也可能影響決定以引數順序,最重要的是標記位於最前,次重要的隨後。在函式宣告中用CV_DEFAULT巨集指定可選引數的預設值.它使宣告與C相相容。

示例函式宣告請參見cvexample.h和cv.h、cvaux.h.

5. 函式實現

本節主要關注以下幾點:

  1. 引數型別檢查
  2. 錯誤產生和處理
  3. 記憶體管理和資源回收
  4. 呼叫低階函式
      如前面所說,OpenCV函式廣泛使用高階資料型別傳送和返回引數。它簡化了函式的使用,但是增加了使用錯誤的引數組合呼叫函式的可能性(例如浮點影象代替點陣圖,或兩個不同大小的影象)。檢查標準型別引數存在標準的方法。
      IplImage影象能通過CV_CHECK_IMAGE巨集被檢查。該巨集檢查傳給IplImage的指標和潛在的影象資料指標不為空,影象有畫素順序,沒有ROI掩碼或冗餘資訊。
      CV_CHECK_MASK_IMAGE用於檢查掩碼影象,二值圖和灰度圖。除了CV_CHECK_IMAGE檢查的條件外,它還能確保影象有8位深度和單通道。並且,所有的輸入和輸出影象在進行深度\通道數和尺寸組合前應該被檢查。隨後,應該在呼叫cvGetImageRawData函式返回的影象ROI尺寸後應該被檢查。輸入等高線和其它動態資料結構能夠用CV_IS_CONTOUR和相關的巨集進行檢查。

任何時候,當傳入一個錯誤的引數或在函式執行時發生其它嚴重錯誤時,應該通過cvError函式丟擲一個錯誤. OpenCV中與幾乎所有標準的低階C庫的IPL類似,有一個錯誤處理機制.那就是存在一個全域性錯誤狀態代替返回錯誤碼:
  可以通過以下實現:

使用cvError函式設定

  1. 使用cvClearErrStatus清除
  2. 使用cvGetErrorStatus讀取
      除了設定錯誤狀態和指定值外,cvError還能進行額外的操作,依據錯誤處理模式而不同,錯誤處理模式可以通過cvSetErrorMode調整.在silent模式或parent模式下cvError立即返回.在子模式下,它打印出錯誤訊息並終止應用程式。

為了更方便使用. 可以通過使用如下巨集來代替以上函式:

CV_ERROR和 OPENCV_ERROR代替cvError
CV_CALL和OPENCV_CALL代替呼叫函式和檢查狀態
CV_*巨集需要在函式中定義”FuncName”字串變數和”exit”標籤,OPENCV_*巨集則不需要。

在OpenCV中臨時快取用cvAlloc和cvFree函式分配和回收.函式應注意適當對齊,對未釋放的記憶體保持跟蹤,檢查溢位。
  當程式執行出記憶體泛圍時,cvAlloc丟擲一個錯誤.函式能夠呼叫能由使用者賦予完全控制記憶體分配的低階函式.因此強烈建議使用這些函式.以上描述僅對簡單快取有效.臨時影象,記憶體儲存和其它結構使用cvCreate和cvRelease的方式分配和回收.

如果錯誤發生,並且CV_ERROR或CV_CALL巨集被呼叫,控制轉到exit標籤處.同在在程式流中可以通過EXIT巨集跳轉控制.標籤可以通過手動或__BEGIN__和巨集被定義.此標籤引入是為了資源回收.儘管執行分支,當程式流進行時,還是經常發生記憶體洩漏.這種情況通常發生在分支語句中使用返回語句.

使用庫中的技術,可以幫助程式設計師避免大多數記憶體洩漏.在函式開始所有的指標被清除(通常在初始化中).在”exit”標籤後,對每個指標呼叫cvFree函式.cvFree函式可以安全處理空指標.在函式內部,返回語句用EXIT巨集代替.這樣,我們可以確保記憶體的回收.當然,我們可能忘記對某些塊呼叫cvFree函式,函式執行時,僅僅只是記憶體洩漏發發生,並且易於捕捉.

OpenCV中的低階函式象IPP中那樣主要是是C語言實現原始操作的.它們不同於前面討論的介面級高階函式(他們使用簡單的指標和數值,幾乎不用結構體)和錯誤處理方法(它們返回錯誤程式碼而不是全域性錯誤狀態).方便並安全的呼叫這些函式的方法是使用IPPI_CALL巨集.

函式實現示例請參見cvexample.cpp檔案。

6. 程式碼佈局

在OpenCV中有一個單獨的字串規則:每個檔案必須使用一致格式樣式。
當前使用在OpenCV中,並推薦使用的樣式如下:

if( a > 5 )
{
   int b = a*a;
   c = c > b ? c : b + 1;
}
else if( abs(a) < 5 )
{
    c--;
}
else
{
    printf( "a=%d is far to negative\n", a );
}

在符合以上樣式的前提下其它樣式也可能接受。也就是說,如果一個人修改別人的程式碼,他應該使用相同的程式碼樣式。

7. 移植性

所要程式碼必須符合以下標準:
ANSI C 第一個語言標準ISO/IEC 9899:1990
C9X (1999年修訂的新標準) – ISO/IEC 9899.
C++ 標準 – ISO/IEC 14882-1998.

你應該去除編譯器依賴或平臺依賴和系統呼叫,例如:
編譯器: pragma’s
特定關鍵字: __stdcall, __inline, __int64(or long long).使用CV_INLINE, CV_STDCALL, int64分別代替。
編譯器擴充套件,例如?<和<?巨集表示最小和最大,過載巨集等。
內聯彙編
Unix或Win32呼叫,如:bcopy, readdir, CreateFile, WaitForSingleObject 等。
用sizeof代替具體的資料大小(如sizeof(int)而不是4),位元組順序((int)"\x1\x2\x3\x4"是0x01020304或0x04030201或什麼?),用簡單的字元有符號字元或無符號字元處理資料(不是字串)。使用短形式,uchar表示unsigned char和schar表示signed char。使用預處理指令包含非可移植性程式碼片段。不要試圖使用標準元素,主要編譯器製造商幾乎不支援這些。

8. 函式測試實現

每個測試實現為從文字檔案輸入和輸出結果到另外一個文字檔案的C/C++函式。這樣,函式就有如下介面:
bool ( const char* inputfile, const char* output file );
  輸入輸出檔案的格式沒有定義。然而,如果測試系統函式用於從檔案中讀取或寫入高階資料(矩陣,檔名,輪廓等),那麼檔案的格式應與函式相容。
  外部或主測試函式執行所有或選擇的測試並且與標準結果相比較,它可以由其它程式或以前執行的相同測試建立。
使用這種設計可以實現檢查在一個函式上檢查幾個特殊的資料集和測式比較函式在武斷的資料上的輸出和標準輸出。在這種情況下輸出檔案能以不同的兩個輸出和從前面段的標準結果將全部是零。

測試系統API使測試更容易,它包括:

  1. 系統測試核心(測試註冊,檔案管理,用異常處理能力控制測試)
  2. 從文字檔案取得矩陣、檔名和其它資料和寫入資料到文字檔案的函式。
  3. 檢查陣列中的特殊值。
  4. 記憶體管理函式幫助捕捉記憶體洩漏和緩衝區越界。
  5. 隨機資料生成。
  6. 簡單演算法函式(矩陣操作)
  7. 視覺化函式

很多功能實現在OpenCV和HighGUI API上的瘦層.
  以下是一步一步描述怎樣實現測試的示例:

//建立一個測試體檔案:
//
// skeleton_test.cpp
//
#include "opencv_tst.h"
// 測試函式
bool  test_skeleton( const char* input, const char* output )
{
// 從文字檔案中載入一個圖片.
IplImage* img = tstLoadImage( input );	
// 執行函式(參見cvexample.cpp)
cvRasterSkeleton( img, CV_SKEL_PAVLIDIS );
//儲存結果
tstSaveImage( output );
}
// 註冊測試
OPENCV_REGISTER_TEST( test_skeleton, "cvRasterSkeleton" )	
// 以上巨集擴充套件瞭如下程式碼:
//
//     static  CvTstReg( test_skeleton, "cvRasterSkeleton",
//                       "test_skeleton", "skeleton_test.cpp",
//                       20 /* code line number */,
//                       "test_skeleton.in" /* input data file name */
//                       "test_skeleton.out"/* output data file name */ 
//                       "test_skeleton.0"  /* etalon data file name */
//                      );
//
// The line calls constructor of CvTstReg class that links the test to
// the master test list.

將測試檔案加入到測試系統專案中.有main()函式的主測試已經包含在專案中,所以不需要再編寫執行程式碼.
建立輸入和標準資料.首先你可能需要手工建立.標準資料能夠通過用-c(–create-etalon)傳輸測命令列選項標識生成。並且,這種情況可以在測試函式中用tstIsEtalonMode()函式(在這種情況下,給定可靠值給另外一個演算法的變數)處理。

  1. 輸入輸出資料存放在opencv_tst/testdata資料夾中.
  2. 測試資料檔案的名字由測試體檔案和特定的副檔名構成.(輸入資料檔案的副檔名為”.in”,輸出資料檔案的副檔名為”.out”,標準資料檔案的副檔名為”.0”).

隨後,測試系統將在不同的模式下執行.

提示
  某些OpenCV函式使用小結構體作為輸入.使結構體的名稱型別為CvSomething.然後cvSomething通常是內聯的,從引數列表構成物件.使用這些內聯的構造器,使程式碼更容易寫和讀.

使用cvRound,cvFloor和cvCeil快速轉換浮點數為相近的整形數或到負無窮在或正無窮大.在x86架構上,這些函式比簡單轉換操作執行更快.在C9X標準中有幾個標準的函式做相同的事情,但是它們現在很少被支援.當情況發生時,以上函式將轉換為內聯巨集.

附錄A: 規則簡述列表

  1. 檔名以小寫的cv字首開始.標頭檔案副檔名為.h或.hpp
  2. 實現副檔名為.cpp
  3. 每一個檔案在開頭包含BSD相容許可
  4. 標頭檔案有保護巨集和extern “C” { } 保護C語言部分介面
  5. 原始檔包含”precomp.h”作為第一個標頭檔案
  6. 外部函式名和資料型別名寫成大小寫混合型別.外部巨集寫作大寫字元
  7. 外部函式以cv字首開始
  8. 內部函式字首為icv
  9. 資料型別字首為Cv
  10. 外部巨集字首為CV

函式宣告形式為:

OPENCVAPI <returnType>	<functionName>( arguments );

函式實現形式為:

CV_IMPL <returnType>
	<functionName>( arguments )
	{
	CV_FUNCNAME("<functionName>");
	...	
	__BEGIN__;	
	...
	__END__;
	
	// cleanup ...
	return ...;
	}
  1. 使用CV_ERROR/OPENCV_ERROR丟擲錯誤,
  2. 使用CV_CALL/OPENCV_CALL呼叫高階函式,
  3. 使用IPPI_CALL呼叫低階函式
  4. 使用cvAlloc和cvFree分配和回收臨時緩衝區
  5. 在每一個原始檔中保持一致的格式,與C/C++標準相容,避免編譯器依賴性,系統依賴性和平臺依賴性。