日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

OpenCV之core 模块. 核心功能(1)Mat - 基本图像容器 OpenCV如何扫描图像、利用查找表和计时 矩阵的掩码操作 使用OpenCV对两幅图像求和(求混合(blending))

發布時間:2025/3/21 编程问答 55 豆豆

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?信息頭所指向的矩陣。總結一下,你需要記住的是

  • 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列?。

    代碼

    • 下列代碼執行運算??:
    #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream>using namespace std; using namespace cv;double alpha; /**< 控制對比度 */ int beta; /**< 控制亮度 */int main( int argc, char** argv ) {/// 讀入用戶提供的圖像Mat image = imread( argv[1] );Mat new_image = Mat::zeros( image.size(), image.type() );/// 初始化cout << " Basic Linear Transforms " << endl;cout << "-------------------------" << endl;cout << "* Enter the alpha value [1.0-3.0]: ";cin >> alpha;cout << "* Enter the beta value [0-100]: ";cin >> beta;/// 執行運算 new_image(i,j) = alpha*image(i,j) + betafor( 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 );}}}/// 創建窗口namedWindow("Original Image", 1);namedWindow("New Image", 1);/// 顯示圖像imshow("Original Image", image);imshow("New Image", new_image);/// 等待用戶按鍵waitKey();return 0; }

    說明

  • 一上來,我們要建立兩個變量,以存儲用戶輸入的??和??:

    double alpha; int beta;
  • 然后,用?imread?載入圖像,并將其存入一個Mat對象:

    Mat image = imread( argv[1] );
  • 此時,因為要對圖像進行一些變換,所以我們需要一個新的Mat對象,以存儲變換后的圖像。我們希望這個Mat對象擁有下面的性質:

    • 像素值初始化為0
    • 與原圖像有相同的大小和類型
    Mat new_image = Mat::zeros( image.size(), image.type() );

    注意到,?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))的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。