OpenCV之core 模块. 核心功能(1)Mat - 基本图像容器 OpenCV如何扫描图像、利用查找表和计时 矩阵的掩码操作 使用OpenCV对两幅图像求和(求混合(blending))
Mat - 基本圖像容器
目的
從真實世界中獲取數字圖像有很多方法,比如數碼相機、掃描儀、CT或者磁共振成像。無論哪種方法,我們(人類)看到的是圖像,而讓數字設備來“看“的時候,則是在記錄圖像中的每一個點的數值。
比如上面的圖像,在標出的鏡子區域中你見到的只是一個矩陣,該矩陣包含了所有像素點的強度值。如何獲取并存儲這些像素值由我們的需求而定,最終在計算機世界里所有圖像都可以簡化為數值矩以及矩陣信息。作為一個計算機視覺庫,?OpenCV?其主要目的就是通過處理和操作這些信息,來獲取更高級的信息。因此,OpenCV如何存儲并操作圖像是你首先要學習的。
Mat
在2001年剛剛出現的時候,OpenCV基于?C?語言接口而建。為了在內存(memory)中存放圖像,當時采用名為?IplImage?的C語言結構體,時至今日這仍出現在大多數的舊版教程和教學材料。但這種方法必須接受C語言所有的不足,這其中最大的不足要數手動內存管理,其依據是用戶要為開辟和銷毀內存負責。雖然對于小型的程序來說手動管理內存不是問題,但一旦代碼開始變得越來越龐大,你需要越來越多地糾纏于這個問題,而不是著力解決你的開發目標。
幸運的是,C++出現了,并且帶來類的概念,這給用戶帶來另外一個選擇:自動的內存管理(不嚴謹地說)。這是一個好消息,如果C++完全兼容C的話,這個變化不會帶來兼容性問題。為此,OpenCV在2.0版本中引入了一個新的C++接口,利用自動內存管理給出了解決問題的新方法。使用這個方法,你不需要糾結在管理內存上,而且你的代碼會變得簡潔(少寫多得)。但C++接口唯一的不足是當前許多嵌入式開發系統只支持C語言。所以,當目標不是這種開發平臺時,沒有必要使用?舊?方法(除非你是自找麻煩的受虐狂碼農)。
關于?Mat?,首先要知道的是你不必再手動地(1)為其開辟空間(2)在不需要時立即將空間釋放。但手動地做還是可以的:大多數OpenCV函數仍會手動地為輸出數據開辟空間。當傳遞一個已經存在的?Mat?對象時,開辟好的矩陣空間會被重用。也就是說,我們每次都使用大小正好的內存來完成任務。
基本上講?Mat?是一個類,由兩個數據部分組成:矩陣頭(包含矩陣尺寸,存儲方法,存儲地址等信息)和一個指向存儲所有像素值的矩陣(根據所選存儲方法的不同矩陣可以是不同的維數)的指針。矩陣頭的尺寸是常數值,但矩陣本身的尺寸會依圖像的不同而不同,通常比矩陣頭的尺寸大數個數量級。因此,當在程序中傳遞圖像并創建拷貝時,大的開銷是由矩陣造成的,而不是信息頭。OpenCV是一個圖像處理庫,囊括了大量的圖像處理函數,為了解決問題通常要使用庫中的多個函數,因此在函數中傳遞圖像是家常便飯。同時不要忘了我們正在討論的是計算量很大的圖像處理算法,因此,除非萬不得已,我們不應該拷貝?大?的圖像,因為這會降低程序速度。
為了搞定這個問題,OpenCV使用引用計數機制。其思路是讓每個?Mat?對象有自己的信息頭,但共享同一個矩陣。這通過讓矩陣指針指向同一地址而實現。而拷貝構造函數則?只拷貝信息頭和矩陣指針?,而不拷貝矩陣。
| 1 2 3 4 5 6 | Mat A, C; // 只創建信息頭部分 A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 這里為矩陣開辟內存Mat B(A); // 使用拷貝構造函數C = A; // 賦值運算符 |
以上代碼中的所有Mat對象最終都指向同一個也是唯一一個數據矩陣。雖然它們的信息頭不同,但通過任何一個對象所做的改變也會影響其它對象。實際上,不同的對象只是訪問相同數據的不同途徑而已。這里還要提及一個比較棒的功能:你可以創建只引用部分數據的信息頭。比如想要創建一個感興趣區域(?ROI?),你只需要創建包含邊界信息的信息頭:
| 1 2 | Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle Mat E = A(Range:all(), Range(1,3)); // using row and column boundaries |
現在你也許會問,如果矩陣屬于多個?Mat?對象,那么當不再需要它時誰來負責清理?簡單的回答是:最后一個使用它的對象。通過引用計數機制來實現。無論什么時候有人拷貝了一個?Mat?對象的信息頭,都會增加矩陣的引用次數;反之當一個頭被釋放之后,這個計數被減一;當計數值為零,矩陣會被清理。但某些時候你仍會想拷貝矩陣本身(不只是信息頭和矩陣指針),這時可以使用函數clone()?或者?copyTo()?。
| 1 2 3 | Mat F = A.clone(); Mat G; A.copyTo(G); |
現在改變?F?或者?G?就不會影響?Mat?信息頭所指向的矩陣??偨Y一下,你需要記住的是
- OpenCV函數中輸出圖像的內存分配是自動完成的(如果不特別指定的話)。
- 使用OpenCV的C++接口時不需要考慮內存釋放問題。
- 賦值運算符和拷貝構造函數(?ctor?)只拷貝信息頭。
- 使用函數?clone()?或者?copyTo()?來拷貝一副圖像的矩陣。
存儲?方法
這里講述如何存儲像素值。需要指定顏色空間和數據類型。顏色空間是指對一個給定的顏色,如何組合顏色元素以對其編碼。最簡單的顏色空間要屬灰度級空間,只處理黑色和白色,對它們進行組合可以產生不同程度的灰色。
對于?彩色?方式則有更多種類的顏色空間,但不論哪種方式都是把顏色分成三個或者四個基元素,通過組合基元素可以產生所有的顏色。RGB顏色空間是最常用的一種顏色空間,這歸功于它也是人眼內部構成顏色的方式。它的基色是紅色、綠色和藍色,有時為了表示透明顏色也會加入第四個元素 alpha (A)。
有很多的顏色系統,各有自身優勢:
- RGB是最常見的,這是因為人眼采用相似的工作機制,它也被顯示設備所采用。
- HSV和HLS把顏色分解成色調、飽和度和亮度/明度。這是描述顏色更自然的方式,比如可以通過拋棄最后一個元素,使算法對輸入圖像的光照條件不敏感。
- YCrCb在JPEG圖像格式中廣泛使用。
- CIE L*a*b*是一種在感知上均勻的顏色空間,它適合用來度量兩個顏色之間的?距離?。
每個組成元素都有其自己的定義域,取決于其數據類型。如何存儲一個元素決定了我們在其定義域上能夠控制的精度。最小的數據類型是?char?,占一個字節或者8位,可以是有符號型(0到255之間)或無符號型(-127到+127之間)。盡管使用三個?char?型元素已經可以表示1600萬種可能的顏色(使用RGB顏色空間),但若使用float(4字節,32位)或double(8字節,64位)則能給出更加精細的顏色分辨能力。但同時也要切記增加元素的尺寸也會增加了圖像所占的內存空間。
顯式地創建一個?Mat?對象
教程?讀取、修改、保存圖像?已經講解了如何使用函數?imwrite()?將一個矩陣寫入圖像文件中。但是為了debug,更加方便的方式是看實際值。為此,你可以通過?Mat?的運算符 << 來實現,但要記住這只對二維矩陣有效。
Mat?不但是一個很贊的圖像容器類,它同時也是一個通用的矩陣類,所以可以用來創建和操作多維矩陣。創建一個Mat對象有多種方法:-
Mat()?構造函數
Mat M(2,2, CV_8UC3, Scalar(0,0,255)); cout << "M = " << endl << " " << M << endl << endl;
對于二維多通道圖像,首先要定義其尺寸,即行數和列數。
然后,需要指定存儲元素的數據類型以及每個矩陣點的通道數。為此,依據下面的規則有多種定義
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]比如?CV_8UC3?表示使用8位的 unsigned char 型,每個像素由三個元素組成三通道。預先定義的通道數可以多達四個。Scalar?是個short型vector。指定這個能夠使用指定的定制化值來初始化矩陣。當然,如果你需要更多通道數,你可以使用大寫的宏并把通道數放在小括號中,如下所示
-
在 C\C++ 中通過構造函數進行初始化
int sz[3] = {2,2,2}; Mat L(3,sz, CV_8UC(1), Scalar::all(0));上面的例子演示了如何創建一個超過兩維的矩陣:指定維數,然后傳遞一個指向一個數組的指針,這個數組包含每個維度的尺寸;其余的相同
-
為已存在IplImage指針創建信息頭:
IplImage* img = cvLoadImage("greatwave.png", 1); Mat mtx(img); // convert IplImage* -> Mat -
Create()?function: 函數
M.create(4,4, CV_8UC(2));cout << "M = "<< endl << " " << M << endl << endl;
這個創建方法不能為矩陣設初值,它只是在改變尺寸時重新為矩陣數據開辟內存。
-
MATLAB形式的初始化方式:?zeros(),?ones(), :eyes()?。使用以下方式指定尺寸和數據類型:
Mat E = Mat::eye(4, 4, CV_64F); cout << "E = " << endl << " " << E << endl << endl;Mat O = Mat::ones(2, 2, CV_32F); cout << "O = " << endl << " " << O << endl << endl;Mat Z = Mat::zeros(3,3, CV_8UC1);cout << "Z = " << endl << " " << Z << endl << endl;
-
對于小矩陣你可以用逗號分隔的初始化函數:
Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0); cout << "C = " << endl << " " << C << endl << endl;
-
使用?clone()?或者?copyTo()?為一個存在的?Mat?對象創建一個新的信息頭。
Mat RowClone = C.row(1).clone();cout << "RowClone = " << endl << " " << RowClone << endl << endl;
格式化打印
Note
?調用函數?randu()?來對一個矩陣使用隨機數填充,需要指定隨機數的上界和下界:
Mat R = Mat(3, 2, CV_8UC3);randu(R, Scalar::all(0), Scalar::all(255));從上面的例子中可以看到默認格式,除此之外,OpenCV還支持以下的輸出習慣
-
默認方式
cout << "R (default) = " << endl << R << endl << endl; -
Python
cout << "R (python) = " << endl << format(R,"python") << endl << endl; -
以逗號分隔的數值 (CSV)
cout << "R (csv) = " << endl << format(R,"csv" ) << endl << endl; -
Numpy
cout << "R (numpy) = " << endl << format(R,"numpy" ) << endl << endl; -
C語言
cout << "R (c) = " << endl << format(R,"C" ) << endl << endl;
打印其它常用項目
OpenCV支持使用運算符<<來打印其它常用OpenCV數據結構。
-
2維點
Point2f P(5, 1);cout << "Point (2D) = " << P << endl << endl; -
3維點
Point3f P3f(2, 6, 7);cout << "Point (3D) = " << P3f << endl << endl; -
基于cv::Mat的std::vector
vector<float> v;v.push_back( (float)CV_PI); v.push_back(2); v.push_back(3.01f);cout << "Vector of floats via Mat = " << Mat(v) << endl << endl; -
std::vector點
vector<Point2f> vPoints(20);for (size_t E = 0; E < vPoints.size(); ++E)vPoints[E] = Point2f((float)(E * 5), (float)(E % 7));cout << "A vector of 2D Points = " << vPoints << endl << endl;
這里的例子大多數出現在一個短小的控制臺應用程序中,你可以在?here?下載到,或者在c++示例部分中找到。
可以在?YouTube?找到簡短的視頻演示。
OpenCV如何掃描圖像、利用查找表和計時
目的
我們將探索以下問題的答案:
- 如何遍歷圖像中的每一個像素?
- OpenCV的矩陣值是如何存儲的?
- 如何測試我們所實現算法的性能?
- 查找表是什么?為什么要用它?
測試用例
這里我們測試的,是一種簡單的顏色縮減方法。如果矩陣元素存儲的是單通道像素,使用C或C++的無符號字符類型,那么像素可有256個不同值。但若是三通道圖像,這種存儲格式的顏色數就太多了(確切地說,有一千六百多萬種)。用如此之多的顏色可能會對我們的算法性能造成嚴重影響。其實有時候,僅用這些顏色的一小部分,就足以達到同樣效果。
這種情況下,常用的一種方法是?顏色空間縮減?。其做法是:將現有顏色空間值除以某個輸入值,以獲得較少的顏色數。例如,顏色值0到9可取為新值0,10到19可取為10,以此類推。
uchar?(無符號字符,即0到255之間取值的數)類型的值除以?int?值,結果仍是?char?。因為結果是char類型的,所以求出來小數也要向下取整。利用這一點,剛才提到在?uchar?定義域中進行的顏色縮減運算就可以表達為下列形式:
這樣的話,簡單的顏色空間縮減算法就可由下面兩步組成:一、遍歷圖像矩陣的每一個像素;二、對像素應用上述公式。值得注意的是,我們這里用到了除法和乘法運算,而這兩種運算又特別費時,所以,我們應盡可能用代價較低的加、減、賦值等運算替換它們。此外,還應注意到,上述運算的輸入僅能在某個有限范圍內取值,如?uchar?類型可取256個值。
由此可知,對于較大的圖像,有效的方法是預先計算所有可能的值,然后需要這些值的時候,利用查找表直接賦值即可。查找表是一維或多維數組,存儲了不同輸入值所對應的輸出值,其優勢在于只需讀取、無需計算。
我們的測試用例程序(以及這里給出的示例代碼)做了以下幾件事:以命令行參數形式讀入圖像(可以是彩色圖像,也可以是灰度圖像,由命令行參數決定),然后用命令行參數給出的整數進行顏色縮減。目前,OpenCV主要有三種逐像素遍歷圖像的方法。我們將分別用這三種方法掃描圖像,并將它們所用時間輸出到屏幕上。我想這樣的對比應該很有意思。
你可以從?這里?下載源代碼,也可以找到OpenCV的samples目錄,進入cpp的tutorial_code的core目錄,查閱該程序的代碼。程序的基本用法是:
how_to_scan_images imageName.jpg intValueToReduce [G]最后那個參數是可選的。如果提供該參數,則圖像以灰度格式載入,否則使用彩色格式。在該程序中,我們首先要計算查找表。
int divideWith; // convert our input string to number - C++ stylestringstream s;s << argv[2];s >> divideWith;if (!s){cout << "Invalid number entered for dividing. " << endl; return -1;}uchar table[256]; for (int i = 0; i < 256; ++i)table[i] = divideWith* (i/divideWith);這里我們先使用C++的?stringstream?類,把第三個命令行參數由字符串轉換為整數。然后,我們用數組和前面給出的公式計算查找表。這里并未涉及有關OpenCV的內容。
另外有個問題是如何計時。沒錯,OpenCV提供了兩個簡便的可用于計時的函數?getTickCount()?和?getTickFrequency()?。第一個函數返回你的CPU自某個事件(如啟動電腦)以來走過的時鐘周期數,第二個函數返回你的CPU一秒鐘所走的時鐘周期數。這樣,我們就能輕松地以秒為單位對某運算計時:
double t = (double)getTickCount(); // 做點什么 ... t = ((double)getTickCount() - t)/getTickFrequency(); cout << "Times passed in seconds: " << t << endl;圖像矩陣是如何存儲在內存之中的?
在我的教程?Mat - 基本圖像容器?中,你或許已了解到,圖像矩陣的大小取決于我們所用的顏色模型,確切地說,取決于所用通道數。如果是灰度圖像,矩陣就會像這樣:
而對多通道圖像來說,矩陣中的列會包含多個子列,其子列個數與通道數相等。例如,RGB顏色模型的矩陣:
注意到,子列的通道順序是反過來的:BGR而不是RGB。很多情況下,因為內存足夠大,可實現連續存儲,因此,圖像中的各行就能一行一行地連接起來,形成一個長行。連續存儲有助于提升圖像掃描速度,我們可以使用?isContinuous()?來去判斷矩陣是否是連續存儲的. 相關示例會在接下來的內容中提供。
1.高效的方法 Efficient Way
說到性能,經典的C風格運算符[](指針)訪問要更勝一籌. 因此,我們推薦的效率最高的查找表賦值方法,還是下面的這種:
Mat& ScanImageAndReduceC(Mat& I, const uchar* const table) {// accept only char type matricesCV_Assert(I.depth() != sizeof(uchar)); int channels = I.channels();int nRows = I.rows * channels; int nCols = I.cols;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; }這里,我們獲取了每一行開始處的指針,然后遍歷至該行末尾。如果矩陣是以連續方式存儲的,我們只需請求一次指針、然后一路遍歷下去就行。彩色圖像的情況有必要加以注意:因為三個通道的原因,我們需要遍歷的元素數目也是3倍。
這里有另外一種方法來實現遍歷功能,就是使用?data?, data會從?Mat?中返回指向矩陣第一行第一列的指針。注意如果該指針為NULL則表明對象里面無輸入,所以這是一種簡單的檢查圖像是否被成功讀入的方法。當矩陣是連續存儲時,我們就可以通過遍歷data?來掃描整個圖像。例如,一個灰度圖像,其操作如下:
uchar* p = I.data;for( unsigned int i =0; i < ncol*nrows; ++i)*p++ = table[*p];這回得出和前面相同的結果。但是這種方法編寫的代碼可讀性方面差,并且進一步操作困難。同時,我發現在實際應用中,該方法的性能表現上并不明顯優于前一種(因為現在大多數編譯器都會對這類操作做出優化)。
2.迭代法 The iterator (safe) method
在高性能法(the efficient way)中,我們可以通過遍歷正確的?uchar?域并跳過行與行之間可能的空缺-你必須自己來確認是否有空缺,來實現圖像掃描,迭代法則被認為是一種以更安全的方式來實現這一功能。在迭代法中,你所需要做的僅僅是獲得圖像矩陣的begin和end,然后增加迭代直至從begin到end。將*操作符添加在迭代指針前,即可訪問當前指向的內容。
Mat& ScanImageAndReduceIterator(Mat& I, const uchar* const table) {// accept only char type matricesCV_Assert(I.depth() != sizeof(uchar)); const int channels = I.channels();switch(channels){case 1: {MatIterator_<uchar> it, end; for( it = I.begin<uchar>(), end = I.end<uchar>(); it != end; ++it)*it = table[*it];break;}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; }對于彩色圖像中的一行,每列中有3個uchar元素,這可以被認為是一個小的包含uchar元素的vector,在OpenCV中用?Vec3b?來命名。如果要訪問第n個子列,我們只需要簡單的利用[]來操作就可以。需要指出的是,OpenCV的迭代在掃描過一行中所有列后會自動跳至下一行,所以說如果在彩色圖像中如果只使用一個簡單的?uchar?而不是?Vec3b?迭代的話就只能獲得藍色通道(B)里的值。
3. 通過相關返回值的On-the-fly地址計算
事實上這個方法并不推薦被用來進行圖像掃描,它本來是被用于獲取或更改圖像中的隨機元素。它的基本用途是要確定你試圖訪問的元素的所在行數與列數。在前面的掃描方法中,我們觀察到知道所查詢的圖像數據類型是很重要的。這里同樣的你得手動指定好你要查找的數據類型。下面的代碼中是一個關于灰度圖像的示例(運用 +?at()?函數):
Mat& ScanImageAndReduceRandomAccess(Mat& I, const uchar* const table) {// accept only char type matricesCV_Assert(I.depth() != sizeof(uchar)); 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)];break;}case 3: {Mat_<Vec3b> _I = I;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; }該函數輸入為數據類型及需求元素的坐標,返回的是一個對應的值-如果用?get?則是constant,如果是用?set?、則為non-constant. 處于程序安全,當且僅當在?debug 模式下?它會檢查你的輸入坐標是否有效或者超出范圍. 如果坐標有誤,則會輸出一個標準的錯誤信息. 和高性能法(the efficient way)相比, 在 release模式下,它們之間的區別僅僅是On-the-fly方法對于圖像矩陣的每個元素,都會獲取一個新的行指針,通過該指針和[]操作來獲取列元素.
當你對一張圖片進行多次查詢操作時,為避免反復輸入數據類型和at帶來的麻煩和浪費的時間,OpenCV 提供了:basicstructures:Mat_ <id3>?data type. 它同樣可以被用于獲知矩陣的數據類型,你可以簡單利用()操作返回值來快速獲取查詢結果. 值得注意的是你可以利用?at()?函數來用同樣速度完成相同操作. 它僅僅是為了讓懶惰的程序員少寫點 >_< .
4. 核心函數LUT(The Core Function)
這是最被推薦的用于實現批量圖像元素查找和更該操作圖像方法。在圖像處理中,對于一個給定的值,將其替換成其他的值是一個很常見的操作,OpenCV 提供里一個函數直接實現該操作,并不需要你自己掃描圖像,就是:operationsOnArrays:LUT() <lut>?,一個包含于core module的函數. 首先我們建立一個mat型用于查表:
Mat lookUpTable(1, 256, CV_8U);uchar* p = lookUpTable.data; for( int i = 0; i < 256; ++i)p[i] = table[i];然后我們調用函數 (I 是輸入 J 是輸出):
LUT(I, lookUpTable, J);性能表現
為了得到最優的結果,你最好自己編譯并運行這些程序. 為了更好的表現性能差異,我用了一個相當大的圖片(2560 X 1600). 性能測試這里用的是彩色圖片,結果是數百次測試的平均值.
| Efficient Way | 79.4717 milliseconds |
| Iterator | 83.7201 milliseconds |
| On-The-Fly RA | 93.7878 milliseconds |
| LUT function | 32.5759 milliseconds |
我們得出一些結論: 盡量使用 OpenCV 內置函數. 調用LUT 函數可以獲得最快的速度. 這是因為OpenCV庫可以通過英特爾線程架構啟用多線程. 當然,如果你喜歡使用指針的方法來掃描圖像,迭代法是一個不錯的選擇,不過速度上較慢。在debug模式下使用on-the-fly方法掃描全圖是一個最浪費資源的方法,在release模式下它的表現和迭代法相差無幾,但是從安全性角度來考慮,迭代法是更佳的選擇
最后,你可以在我們的YouTube頻道上觀看范例視頻?<https://www.youtube.com/watch?v=fB3AN5fjgwc>.
矩陣的掩碼操作
矩陣的掩碼操作很簡單。其思想是:根據掩碼矩陣(也稱作核)重新計算圖像中每個像素的值。掩碼矩陣中的值表示近鄰像素值(包括該像素自身的值)對新像素值有多大影響。從數學觀點看,我們用自己設置的權值,對像素鄰域內的值做了個加權平均。
測試用例
思考一下圖像對比度增強的問題。我們可以對圖像的每個像素應用下面的公式:
上面那種表達法是公式的形式,而下面那種是以掩碼矩陣表示的緊湊形式。使用掩碼矩陣的時候,我們先把矩陣中心的元素(上面的例子中是(0,0)位置的元素,也就是5)對齊到要計算的目標像素上,再把鄰域像素值和相應的矩陣元素值的乘積加起來。雖然這兩種形式是完全等價的,但在大矩陣情況下,下面的形式看起來會清楚得多。
現在,我們來看看實現掩碼操作的兩種方法。一種方法是用基本的像素訪問方法,另一種方法是用?filter2D?函數。
基本方法
下面是實現了上述功能的函數:
void Sharpen(const Mat& myImage,Mat& Result) {CV_Assert(myImage.depth() == CV_8U); // 僅接受uchar圖像Result.create(myImage.size(),myImage.type());const int nChannels = myImage.channels();for(int j = 1 ; j < myImage.rows-1; ++j){const uchar* previous = myImage.ptr<uchar>(j - 1);const uchar* current = myImage.ptr<uchar>(j );const uchar* next = myImage.ptr<uchar>(j + 1);uchar* output = Result.ptr<uchar>(j);for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i){*output++ = saturate_cast<uchar>(5*current[i]-current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]);}}Result.row(0).setTo(Scalar(0));Result.row(Result.rows-1).setTo(Scalar(0));Result.col(0).setTo(Scalar(0));Result.col(Result.cols-1).setTo(Scalar(0)); }剛進入函數的時候,我們要確保輸入圖像是無符號字符類型的。為了做到這點,我們使用了?CV_Assert?函數。若該函數括號內的表達式為false,則會拋出一個錯誤。
CV_Assert(myImage.depth() == CV_8U); // 僅接受uchar圖像然后,我們創建了一個與輸入有著相同大小和類型的輸出圖像。在?圖像矩陣是如何存儲在內存之中的??一節可以看到,根據圖像的通道數,我們有一個或多個子列。我們用指針在每一個通道上迭代,因此通道數就決定了需計算的元素總數。
Result.create(myImage.size(),myImage.type()); const int nChannels = myImage.channels();利用C語言的[]操作符,我們能簡單明了地訪問像素。因為要同時訪問多行像素,所以我們獲取了其中每一行像素的指針(分別是前一行、當前行和下一行)。此外,我們還需要一個指向計算結果存儲位置的指針。有了這些指針后,我們使用[]操作符,就能輕松訪問到目標元素。為了讓輸出指針向前移動,我們在每一次操作之后對輸出指針進行了遞增(移動一個字節):
for(int j = 1 ; j < myImage.rows-1; ++j) {const uchar* previous = myImage.ptr<uchar>(j - 1);const uchar* current = myImage.ptr<uchar>(j );const uchar* next = myImage.ptr<uchar>(j + 1);uchar* output = Result.ptr<uchar>(j);for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i){*output++ = saturate_cast<uchar>(5*current[i]-current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]);} }在圖像的邊界上,上面給出的公式會訪問不存在的像素位置(比如(0,-1))。因此我們的公式對邊界點來說是未定義的。一種簡單的解決方法,是不對這些邊界點使用掩碼,而直接把它們設為0:
Result.row(0).setTo(Scalar(0)); // 上邊界 Result.row(Result.rows-1).setTo(Scalar(0)); // 下邊界 Result.col(0).setTo(Scalar(0)); // 左邊界 Result.col(Result.cols-1).setTo(Scalar(0)); // 右邊界filter2D函數
濾波器在圖像處理中的應用太廣泛了,因此OpenCV也有個用到了濾波器掩碼(某些場合也稱作核)的函數。不過想使用這個函數,你必須先定義一個表示掩碼的?Mat?對象:
Mat kern = (Mat_<char>(3,3) << 0, -1, 0,-1, 5, -1,0, -1, 0);然后調用?filter2D?函數,參數包括輸入、輸出圖像以及用到的核:
filter2D(I, K, I.depth(), kern );它還帶有第五個可選參數——指定核的中心,和第六個可選參數——指定函數在未定義區域(邊界)的行為。使用該函數有一些優點,如代碼更加清晰簡潔、通常比?自己實現的方法?速度更快(因為有一些專門針對它實現的優化技術)等等。例如,我測試的濾波器方法僅花了13毫秒,而前面那樣自己實現迭代方法花了約31毫秒,二者有著不小差距。
示例:
你可以從?here?下載這個示例的源代碼,也可瀏覽OpenCV源代碼庫的示例目錄samples/cpp/tutorial_code/core/mat_mask_operations/mat_mask_operations.cpp?。
我們的?YouTube頻道?可觀看該程序的運行示例。
使用OpenCV對兩幅圖像求和(求混合(blending))
目的
在這節教程中您將學到
- 線性混合?(linear blending) 是什么以及有什么用處.
- 如何使用?addWeighted?進行兩幅圖像求和
原理
Note
?以下解釋基于Richard Szeliski所著?Computer Vision: Algorithms and Applications
在前面的教程中,我們已經了解一點?像素操作?的知識。?線性混合操作?也是一種典型的二元(兩個輸入)的?像素操作?:
通過在范圍??內改變??,這個操可以用來對兩幅圖像或兩段視頻產生時間上的?畫面疊化?(cross-dissolve)效果,就像在幻燈片放映和電影制作中那樣(很酷吧?)(譯者注:在幻燈片翻頁時可以設置為前后頁緩慢過渡以產生疊加效果,電影中經常在情節過渡時出現畫面疊加效果)。
代碼
在簡短的說明后我們來看代碼:
#include <cv.h> #include <highgui.h> #include <iostream>using namespace cv;int main( int argc, char** argv ) {double alpha = 0.5; double beta; double input;Mat src1, src2, dst;/// Ask the user enter alphastd::cout<<" Simple Linear Blender "<<std::endl;std::cout<<"-----------------------"<<std::endl;std::cout<<"* Enter alpha [0-1]: ";std::cin>>input;/// We use the alpha provided by the user iff it is between 0 and 1if( alpha >= 0 && alpha <= 1 ){ alpha = input; }/// Read image ( same size, same type )src1 = imread("../../images/LinuxLogo.jpg");src2 = imread("../../images/WindowsLogo.jpg");if( !src1.data ) { printf("Error loading src1 \n"); return -1; }if( !src2.data ) { printf("Error loading src2 \n"); return -1; }/// Create WindowsnamedWindow("Linear Blend", 1);beta = ( 1.0 - alpha );addWeighted( src1, alpha, src2, beta, 0.0, dst);imshow( "Linear Blend", dst );waitKey(0);return 0; }說明
既然我們要執行
我們需要兩幅輸入圖像 (?和?)。相應地,我們使用常用的方法加載圖像
src1 = imread("../../images/LinuxLogo.jpg"); src2 = imread("../../images/WindowsLogo.jpg");Warning
?因為我們對?src1?和?src2?求?和?,它們必須要有相同的尺寸(寬度和高度)和類型。
現在我們生成圖像??.為此目的,使用函數?addWeighted?可以很方便地實現:
beta = ( 1.0 - alpha ); addWeighted( src1, alpha, src2, beta, 0.0, dst);這是因為?addWeighted?進行如下計算
這里??對應于上面代碼中被設為??的參數。
創建顯示窗口,顯示圖像并等待用戶結束程序。
結果
改變圖像的對比度和亮度
目的
本篇教程中,你將學到:
- 訪問像素值
- 用0初始化矩陣
- saturate_cast?是做什么用的,以及它為什么有用
- 一些有關像素變換的精彩內容
原理
Note
?以下解釋節選自Richard Szeliski所著?Computer Vision: Algorithms and Applications
圖像處理
- 一般來說,圖像處理算子是帶有一幅或多幅輸入圖像、產生一幅輸出圖像的函數。
- 圖像變換可分為以下兩種:
- 點算子(像素變換)
- 鄰域(基于區域的)算子
像素變換
- 在這一類圖像處理變換中,僅僅根據輸入像素值(有時可加上某些全局信息或參數)計算相應的輸出像素值。
- 這類算子包括?亮度和對比度調整?,以及顏色校正和變換。
亮度和對比度調整
-
兩種常用的點過程(即點算子),是用常數對點進行?乘法?和?加法?運算:
-
兩個參數??和??一般稱作?增益?和?偏置?參數。我們往往用這兩個參數來分別控制?對比度?和?亮度?。
-
你可以把??看成源圖像像素,把??看成輸出圖像像素。這樣一來,上面的式子就能寫得更清楚些:
其中,??和??表示像素位于?第i行?和?第j列?。
代碼
- 下列代碼執行運算??:
說明
一上來,我們要建立兩個變量,以存儲用戶輸入的??和??:
double alpha; int beta;然后,用?imread?載入圖像,并將其存入一個Mat對象:
Mat image = imread( argv[1] );此時,因為要對圖像進行一些變換,所以我們需要一個新的Mat對象,以存儲變換后的圖像。我們希望這個Mat對象擁有下面的性質:
- 像素值初始化為0
- 與原圖像有相同的大小和類型
注意到,?Mat::zeros?采用Matlab風格的初始化方式,用?image.size()?和?image.type()?來對Mat對象進行0初始化。
現在,為了執行運算??,我們要訪問圖像的每一個像素。因為是對RGB圖像進行運算,每個像素有三個值(R、G、B),所以我們要分別訪問它們。下面是訪問像素的代碼片段:
for( int y = 0; y < image.rows; y++ ) {for( int x = 0; x < image.cols; x++ ){for( int c = 0; c < 3; c++ ){new_image.at<Vec3b>(y,x)[c] = saturate_cast<uchar>( alpha*( image.at<Vec3b>(y,x)[c] ) + beta );}} }注意以下兩點:
- 為了訪問圖像的每一個像素,我們使用這一語法:?image.at<Vec3b>(y,x)[c]?其中,?y?是像素所在的行,?x?是像素所在的列,?c?是R、G、B(0、1、2)之一。
- 因為??的運算結果可能超出像素取值范圍,還可能是非整數(如果??是浮點數的話),所以我們要用saturate_cast?對結果進行轉換,以確保它為有效值。
最后,用傳統方法創建窗口并顯示圖像。
namedWindow("Original Image", 1); namedWindow("New Image", 1);imshow("Original Image", image); imshow("New Image", new_image);waitKey(0);Note
?我們可以不用?for?循環來訪問每個像素,而是直接采用下面這個命令:
image.convertTo(new_image, -1, alpha, beta);這里的?convertTo?將執行我們想做的?new_image = a*image + beta?。然而,我們想展現訪問每一個像素的過程,所以選用了for循環的方式。實際上,這兩種方式都能返回同樣的結果。
結果
-
運行代碼,取參數??和?
$ ./BasicLinearTransforms lena.jpg Basic Linear Transforms ------------------------- * Enter the alpha value [1.0-3.0]: 2.2 * Enter the beta value [0-100]: 50 -
我們將得到下面的結果:
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 模块. 核心功能(1)Mat - 基本图像容器 OpenCV如何扫描图像、利用查找表和计时 矩阵的掩码操作 使用OpenCV对两幅图像求和(求混合(blending))的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows服务程序时钟调用
- 下一篇: OpenCV之core 模块. 核心功能