鑒于中文語境下久免,學(xué)習(xí) OpenCV 的資料其實(shí)稀少,不是主要講解已經(jīng)過時(shí)de 1.x 版內(nèi)容《學(xué)習(xí) OpenCV》扭弧,就是各路博主碎片化的學(xué)習(xí)心得阎姥,《OpenCV 2 計(jì)算機(jī)視覺編程手冊(cè)》可以說是學(xué)習(xí) OpenCV 的最佳入門途徑了。最近需要將卷積神經(jīng)網(wǎng)絡(luò)的 Matlab 代碼轉(zhuǎn)換成 C++ 的鸽捻,我也向?qū)嶒?yàn)室同房間的一位學(xué)弟借了此書呼巴,大致看了一下,重點(diǎn)看了第 1 章御蒲、第 2 章衣赶、第 6 章和附錄。
因?yàn)檫@書是手冊(cè)性質(zhì)的删咱,都是一些函數(shù)實(shí)例屑埋,所以記錄下來豪筝,以便日后再用痰滋。下文是書中部分內(nèi)容的摘錄,夾雜一些我的理解续崖。全文可能有點(diǎn)長敲街,遂給出目錄如下:
第1章 接觸圖像
第2章 操作像素
第6章 圖像濾波
附錄 OpenCV3 介紹及代碼導(dǎo)讀
<div id="Section1">第1章 接觸圖像</div>
- OpenCV 庫的結(jié)構(gòu)
- 載入、顯示及保存圖像
OpenCV 庫的結(jié)構(gòu)
- sources文件夾下的子文件夾:
- doc 文件夾中包含的是文檔 + include 文件夾中是所有頭文件
- modules 文件夾中包含所有的源程序
- samples 文件夾中則是許多簡短的學(xué)習(xí)用范例
第 2 頁講了下怎么編譯的严望,對(duì)于新版 OpenCV 來說已經(jīng)沒有必要了多艇,解壓后的 build 文件夾就是編譯好的內(nèi)容。
第 3 頁介紹了各模塊的功能像吻,還有推薦的聲明方式峻黍,為什么要用這種聲明方式呢?
第 6 頁提到拨匆,為了遵循 ANSI C++ 標(biāo)準(zhǔn)姆涩,在用 Visual Studio 建立工程時(shí)選擇 Application Settings 時(shí),沒有勾選 Precompiled Header 選項(xiàng)惭每,這是 Visual Studio 的預(yù)編譯頭文件特性骨饿,可以加速編譯過程。
載入台腥、顯示及保存圖像
- 聲明圖像變量
cv::Mat image;
創(chuàng)建寬高都為0的圖像宏赘,返回值是一個(gè)結(jié)構(gòu)體,
- 圖像讀取黎侈、解碼以及內(nèi)存分配
image = cv::imread("img.jpg");
- 檢查圖像是否被正確讀取
if (!image.data) {
// 圖像尚未創(chuàng)建……
}
此處的成員變量data事實(shí)上是指向已分配的內(nèi)存塊的指針察署,包括圖像數(shù)據(jù)。當(dāng)不存在數(shù)據(jù)時(shí)峻汉,它被簡單設(shè)置為0.
- 聲明一個(gè)需要進(jìn)行圖像顯示的窗口贴汪,接著指定需要顯示的圖像:
cv::namedWindow("Original Image"); // 定義窗口
cv::imshow("Original Image", image); // 顯示圖像
顯示圖像的這條語句之所以還要出現(xiàn)窗口名稱储藐,是為了指定究竟把圖像顯示到哪個(gè)窗口去,因?yàn)榭赡艽嬖诙鄠€(gè)窗口嘶是。
- 圖像翻轉(zhuǎn)
cv::Mat result;
cv::flip(image, result, 1); // 1表示水平翻轉(zhuǎn)
// 0表示垂直翻轉(zhuǎn)
// 負(fù)數(shù)表示既有水平也有垂直翻轉(zhuǎn)
- 等待用戶輸入
cv::waitKey(0)钙勃; //括號(hào)中填的數(shù)字是毫秒數(shù),0為一直等待
如果沒有這句話聂喇,顯示的圖像會(huì)一閃而過辖源。
- 將圖像寫到磁盤
cv::imwrite("output.bmp", result);
文件的后綴名決定了圖像保存時(shí)的編碼格式。
- 指定初始尺寸
cv::Mat ima(240, 320, CV_8U, cv::Scalar(100));
CV_8U對(duì)應(yīng)的是單字節(jié)的像素圖象希太,字母U意味著無符號(hào)的(Unsigned)克饶。對(duì)于彩色圖像,需要指定3個(gè)通道(CV_8UC3)誊辉。
當(dāng) cv::Mat 對(duì)象離開作用域后矾湃,分配的內(nèi)存將自動(dòng)釋放,從而避免內(nèi)存泄漏的困擾堕澄。
另外邀跃,cv::Mat 實(shí)現(xiàn)了引用計(jì)數(shù)以及淺拷貝,當(dāng)圖像之間進(jìn)行賦值時(shí)蛙紫,圖像數(shù)據(jù)并沒有發(fā)生復(fù)制拍屑,兩個(gè)對(duì)象都指向同一塊內(nèi)存塊。這也可用于參數(shù)傳值的圖像坑傅,以及返回值傳值的圖像僵驰。引用計(jì)數(shù)的作用是當(dāng)所有引用內(nèi)存數(shù)據(jù)的對(duì)象都被析構(gòu)后,才會(huì)釋放內(nèi)存塊唁毒。如果你希望創(chuàng)建的圖像擁有原始圖像的嶄新拷貝蒜茴,那么可以使用copyTo()方法。
cv::Mat image2, image3;
image2 = result; // 兩幅圖像擁有同一份數(shù)據(jù)
result.copyTo(image3); // 創(chuàng)建新的拷貝
如果翻轉(zhuǎn)output圖像浆西,并顯示image2和image3粉私,可以看到image2頁翻轉(zhuǎn)了,而image3沒有變室谚。
同理毡鉴,函數(shù)返回其實(shí)也是一次淺拷貝過程。
cv::Mat function() {
// 創(chuàng)建圖像
cv::Mat ima(240, 320, CV_8U, cv::Scalar(100));
// 并返回它
return ima;
}
// 得到灰度圖
cv::Mat gray = function();
在函數(shù)function內(nèi)秒赤,ima只是個(gè)局部變量猪瞬,在離開作用域時(shí)應(yīng)當(dāng)被析構(gòu)掉,但由于他所關(guān)聯(lián)的引用計(jì)數(shù)表示內(nèi)部圖像正在被另一個(gè)對(duì)象gray所引用入篮,因此內(nèi)存塊并不會(huì)被釋放陈瘦。
<div id="Section2">第2章 操作像素</div>
- 彩色或灰度圖像存取像素值
void salt(cv::Mat &image, int n) {
for (int k = 0; k < n; k++) {
// rand() 是隨機(jī)數(shù)生成函數(shù)
int i = rand() % image.cols;
int j = rand() % image.rows;
if (image.channels() == 1) { // 灰度圖
image.at<uchar>(j,i) = 255;
} else if (image.channels() == 3) { // 彩色圖
image.at<cv::Vec3b>(j,i)[0] = 255;
image.at<cv::Vec3b>(j,i)[1] = 255;
image.at<cv::Vec3b>(j,i)[2] = 255;
}
}
}
類 cv::Mat 有若干成員函數(shù)可以獲取圖像的屬性。公有成員變量 cols 和 rows 給出了圖像的寬和高潮售。成員函數(shù) at(int y, int x) 可以用來存取圖像元素。 但是必須在編譯期知道圖像的數(shù)據(jù)類型,因?yàn)?cv::Mat 可以存放任意數(shù)據(jù)類型的元素夏醉。這也是這個(gè)函數(shù)用模板函數(shù)來實(shí)現(xiàn)的原因。所以 at 方法要指定數(shù)據(jù)類型皱埠,而且 at 方法本身不會(huì)進(jìn)行任何數(shù)據(jù)類型轉(zhuǎn)換。
cv::Vec3b咖驮,即由三個(gè) unsigned char 組成的向量边器。
image.at<cv::Vec3b>(j,i)[channel] = value;
索引值 channel 標(biāo)明了顏色通道號(hào)。
類似的托修,還有二元素向量類 cv::Vec2b 和四元素向量類 cv::Vec4b忘巧,s 代表 short,i 代表 int睦刃,f 代表 float砚嘴,d 代表 double。所有這些類型都是使用模板類 cv::Vect<T, N> 定義的涩拙,其中 T 代表類型际长,N 代表向量中的元素個(gè)數(shù)。
- 有時(shí)候使用 cv::Mat 的成員函數(shù)會(huì)很麻煩吃环,因?yàn)榉祷刂档念愋捅仨毻ㄟ^在調(diào)用時(shí)通過模板參數(shù)指定也颤。因此,OpenCV 提供了類 cv::Mat_郁轻,它是 cv::Mat 的一個(gè)模板子類。在事先知道矩陣類型的情況下文留,使用 cv::Mat_ 可以帶來一些便利好唯。這個(gè)類額外定義了一些方法,但是沒有任何成員變量燥翅,所以此類的指針或者引用可以直接進(jìn)行相互類型轉(zhuǎn)換。該類重載了操作符 ()森书,允許我們可以通過它直接存取矩陣元素靶端。因此,假設(shè)有一個(gè) uchar 類型的矩陣凛膏,我們可以這樣寫:
cv::Mat_<uchar> im2 = image; // im2 指向 image
im2(50, 100) = 0; // 存取第 50 行杨名,100列
由于 cv::Mat_ 的元素類型在創(chuàng)建實(shí)例的時(shí)候已經(jīng)聲明,操作符 () 在編譯期就知道要返回的數(shù)據(jù)類型猖毫。使用操作符 () 得到返回值和使用 cv::Mat 的 at 方法得到的返回值是完全一致的台谍,而且寫起來更加簡潔。
- 雙重循環(huán)遍歷所有像素值:
void colorReduce(cv::Mat &image, int div = 64) {
int nl = image.rows; // 行數(shù)
int nc = image.cols * image.channels();
for (int j = 0; j < nl; j++) {
// 得到第 j 行的首地址
uchar* data = image.ptr<uchar>(j);
for (int i = 0; i < nc; i++) {
data[i] = data[i] / div * div + div / 2;
}
}
}
OpenCV 默認(rèn)使用 BGR 的通道順序吁断,而且 size 成員函數(shù)返回的先是寬趁蕊,然后是高坞生,成員變量 cols 代表圖像的寬度(列數(shù)),rows 代表圖像的高度掷伙,step 代表以字節(jié)為單位的圖像的有效寬度是己,即使你的圖像元素類型不是 uchar,step 仍然帶代表著行的字節(jié)數(shù)任柜。圖像的通道數(shù)可以由 channels 方法得到赃泡,total 函數(shù)返回矩陣的像素個(gè)數(shù),像素大小可以從 elemSize 函數(shù)得到乘盼,對(duì)于一個(gè)三通道的 short 型矩陣 CV_16SC3升熊, elemSize 返回 6。
為了簡化指針運(yùn)算绸栅,cv::Mat 提供了 ptr 函數(shù)可以得到圖像任意行的首地址级野。 ptr 函數(shù)是一個(gè)模板函數(shù),它返回第 j 行的首地址:
uchar* data = image.ptr<uchar>(j);
等效地使用指針運(yùn)算從一列移到下一列粹胯,所以蓖柔,也可以這么些:
*data++ = *data / div * div + div / 2;
- 有一個(gè)知識(shí),跟能否快速遍歷圖像有關(guān)风纠,需要提前知道况鸣,那就是:
出于效率的考慮,OpenCV 可能會(huì)給矩陣的每行填補(bǔ)一些額外元素竹观。這是因?yàn)楦渑酰绻械拈L度是 4 或 8 的倍數(shù),一些多媒體處理芯片(如 Intel 的 MMX 架構(gòu))可以更高效地處理圖像臭增。這些額外的像素不會(huì)被顯示或者保存懂酱,填補(bǔ)的值將被忽略。OpenCV將填補(bǔ)后一行的長度指定為關(guān)鍵字誊抛。如果圖像沒有對(duì)行進(jìn)行填補(bǔ)列牺,那么圖像的有效寬度就等于圖像的真實(shí)寬度。
當(dāng)不對(duì)行進(jìn)行填補(bǔ)的時(shí)候拗窃,圖像可以被視為一個(gè)長為 W*H 的一維數(shù)組瞎领。我們可以通過 cv::Mat 的一個(gè)成員函數(shù) isContinuous 來判斷這幅圖像是否對(duì)行進(jìn)行了填補(bǔ)。如果 isContinuous 方法返回值為真的話随夸,說明這幅圖像沒有對(duì)行進(jìn)行填補(bǔ)九默。在一些圖像處理算法中,我們可以利用圖像的連續(xù)性逃魄,把整個(gè)處理過程使用一個(gè)循環(huán)完成荤西;
void colorReduce(cv::Mat &image, int div = 64) {
int nl = image.rows; // 行數(shù)
int nc = image.cols * image.channels();
if (image.isContinuous()) {
// 沒有額外的填補(bǔ)像素
nc = nc * nl;
nl = 1; // it is now a 1D array
}
// 對(duì)于連續(xù)圖像,本循環(huán)只執(zhí)行一次
for (int j = 0; j < nl; j++) {
// 得到第 j 行的首地址
uchar* data = image.ptr<uchar>(j);
for (int i = 0; i < nc; i++) {
data[i] = data[i] / div * div + div / 2;
}
}
}
當(dāng)我們通過 isContinuous 函數(shù)得知圖像沒有對(duì)行進(jìn)行填補(bǔ)之后,我們就可以將寬設(shè)置為 1邪锌,高度設(shè)置為 W*H勉躺,從而消除外層循環(huán)。注意觅丰,我們也可以使用 reshape 方法來重寫這段代碼:
if (image.isContinous()) {
// no padded pixels
image.reshape(1, image.cols*image.rows); // 分別是行數(shù)和通道數(shù)
}
int nl = image.rows; // 列數(shù)
int nc = image.cols * image.channels();
reshape 不需要內(nèi)存拷貝或者重新分配就能改變矩陣的維度饵溅。兩個(gè)參數(shù)分別為新的通道數(shù)和新的行數(shù)。矩陣的列數(shù)可以根據(jù)新的通道數(shù)和行數(shù)來自適應(yīng)妇萄。
在這些視線中蜕企,內(nèi)存循環(huán)一次處理圖像的全部像素。這個(gè)方法在同時(shí)處理若干個(gè)小圖像時(shí)會(huì)很有優(yōu)勢(shì)冠句。
底層指針運(yùn)算
在類 cv::Mat 中轻掩,圖像數(shù)據(jù)以 unsigned char 形式保存在一塊內(nèi)存中。這塊內(nèi)存的首地址可以通過 data 成員變量得到懦底。data 是一個(gè) unsigned char 型的指針唇牧,uoyi循環(huán)可以以如下方式開始:
uchar *data = image.data;
從當(dāng)前行到下一行可以通過對(duì)指針加上行寬完成:
data += image.step; // 下一行
step 代表圖像的行寬(包括填補(bǔ)像素)。通常而言聚唐,你可以通過如下方式獲得第 j 行丐重、第 i 列像素的地址:
// (j, i) 處像素的地址為 &image.at(j, i)
data = image.data + j * image.step + i * image.elemSize();
但是,即使這種方式確實(shí)行之有效杆查,我們依然不建議使用這種處理方式扮惦。因?yàn)檫@種方式除了容易出錯(cuò),還不適用于帶有“感興趣區(qū)域”的圖像亲桦。
使用迭代器遍歷圖像
在面向?qū)ο蟮木幊讨醒旅郏闅v數(shù)據(jù)集合通常是通過迭代器來完成的。迭代器是一種特殊的類烙肺,它專門用來遍歷集合中的各個(gè)元素纳猪,同時(shí)隱藏了在給定的集合上元素迭代的具體實(shí)現(xiàn)方式。這種信息隱蔽原則的使用使得遍歷集合更加容易桃笙。另外,不管數(shù)據(jù)類型是什么沙绝,我們都可以使用相似的方式遍歷集合搏明。標(biāo)準(zhǔn)模板庫 STL 為每個(gè)容器類型都提供了迭代器,OpenCV 同樣為 cv::Mat 提供了與 STL 迭代器兼容的迭代器闪檬。
一個(gè) cv::Mat 實(shí)例的迭代器可以通過創(chuàng)建一個(gè) cv::MatIterator_ 的實(shí)例來得到星著。類似于子類 cv::Mat_,下劃線意味著 cv::MatIterator_ 是一個(gè)模板類粗悯。之所以如此是由于通過迭代器來存取圖像的元素虚循,就必須在編譯期知道圖像元素的數(shù)據(jù)類型。一個(gè)圖像迭代器可以用如下方式聲明:
cv::MatIterator_<cv::Vec3b> it;
另外一種方式是使用定義在 Mat_ 內(nèi)部的迭代器類型:
cv::Mat_<cv::Vec3b>::iterator it;
這樣就可以通過常規(guī)的 begin 和 end 這兩個(gè)迭代器方法來遍歷所有像素。值得指出的是横缔,如果使用后一種方式铺遂,那么 begin 和 end 方法也必須要使用對(duì)應(yīng)的模板化的版本。這樣茎刚,顏色縮減函數(shù)就可以重寫為:
void colorReduce(cv::Mat &image, int div = 64) {
// 得到初始位置的迭代器
cv::Mat_<cv::Vec3b>::iterator it = image.begin<cv::Vec3b>();
// 得到終止位置的迭代器
cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>();
// 遍歷所有像素
for (; it != itend; ++it) {
(*it)[0] = (*it)[0] / div * div + div / 2;
(*it)[1] = (*it)[1] / div * div + div / 2;
(*it)[2] = (*it)[2] / div * div + div / 2;
}
}
注意襟锐,因?yàn)槲覀冞@里處理的彩色圖像,所以迭代器返回的是 cv::Vec3b膛锭,每個(gè)顏色分量可以通過操作符 [] 得到粮坞。
使用迭代器遍歷任何形式的集合都遵循同樣的模式。首先初狰,創(chuàng)建一個(gè)迭代器特化版本的實(shí)例莫杈。在我們的示例代碼中,就是 cv::Mat_<cv::Vec3b>::iterator (或者 cv::MatIterator_<cv::Vec3b>).
然后奢入,使用集合初始位置(圖像的左上角)的迭代器對(duì)其進(jìn)行初始化筝闹。初始位置的迭代器通常是通過 begin 方法得到的。對(duì)于一個(gè) cv::Mat 的實(shí)例俊马,你可以通過 image.begin<cv::Vec3b>() 來得到圖像左上角位置的迭代器丁存。你也可以通過對(duì)迭代器進(jìn)行代數(shù)運(yùn)算。例如:如果你想從圖像的第二行開始柴我,那么你可以用 image.begin<cv::Vec3b>() + image.rows 來初始化迭代器解寝。集合終止位置的迭代器可以通過 end 方法得到。但是end 方法得到的迭代器其實(shí)已經(jīng)超出了集合艘儒。這也意味著迭代過程必須在迭代器到達(dá)這個(gè)位置時(shí)結(jié)束聋伦。end 方法得到的迭代器也可以進(jìn)行代數(shù)運(yùn)算。如果界睁,你希望迭代過程在圖像最后一行之前停止觉增,那么迭代器的終止位置應(yīng)該是 image.end<cv::Vec3b>() - image.rows。一旦迭代器初始化完成之后翻斟,你就可以創(chuàng)建一個(gè)循環(huán)遍歷所有的元素知道到達(dá)終止位置逾礁。一個(gè)典型的 while 循環(huán)如下所示:
while (it != itend) {
// do something
...
++it;
}
操作符 ++ 用來將迭代器從當(dāng)前位置移動(dòng)到下一個(gè)位置,你也可以使用更大的補(bǔ)償访惜,比如嘹履,用it+=10將迭代器每次移動(dòng) 10px。
在循環(huán)體內(nèi)部债热,你可以使用解引用操作符 * 來讀寫當(dāng)前元素砾嫉。都操作使用 element = *it,寫操作使用 *it = element窒篱。注意:如果你的操作對(duì)象是 const cv::Mat焕刮,或者你想強(qiáng)調(diào)當(dāng)前循環(huán)不會(huì)對(duì) cv::Mat 的實(shí)例進(jìn)行修改舶沿,那么你就應(yīng)該創(chuàng)建常量迭代器。常量迭代器的聲明如下:
cv::MatConstIterator_<cv::Vec3b> it;
或者
cv::Mat_<cv::Vec3b>::const_iterator it;
在本例中配并,迭代器的開始位置和終止位置是通過模板函數(shù) begin 和 end 得到的括荡。如果我們?cè)诒菊碌谝粍t秘訣中所做的那樣,我們可以通過 cv::Mat_ 的實(shí)例來得到他們荐绝。這樣可以避免在使用 begin 和 end 方法的時(shí)候還要置頂?shù)鞯念愋鸵黄V钥梢赃@樣,是因?yàn)橐粋€(gè) cv::Mat_ 引用在創(chuàng)建的時(shí)候就隱式聲明了迭代器的類型低滩。
cv::Mat_<cv::Vec3b> cimage = image;
cv::Mat_<cv::Vec3b>::iterator it = cimage.begin();
cv::Mat_<cv::Vec3b>::iterator itend = cimage.end();
之所以這個(gè)例子可以而前面那個(gè)例子不可以是因?yàn)檎偌校懊婺莻€(gè)例子的圖像類型是 cv::Mat, 而這個(gè)例子的圖像類型是 cv::Mat_恕沫。
獲取代碼運(yùn)行時(shí)間
OpenCV 有一個(gè)非常實(shí)用的函數(shù) cv::getTickCount() 可以用來測(cè)量一段代碼的運(yùn)行時(shí)間监憎。這個(gè)函數(shù)返回從上次開機(jī)算起的時(shí)鐘周期數(shù)。由于我們需要的是某個(gè)代碼段運(yùn)行的毫秒數(shù)婶溯,因此還需要另外一個(gè) cv::getTickFrequency()鲸阔。此函數(shù)返回沒秒內(nèi)的時(shí)鐘周期數(shù),用于統(tǒng)計(jì)函數(shù)(或一段代碼)耗費(fèi)時(shí)間的方法如下:
double duration;
duration = static_cast<double>(cv::getTickCount());
colorReduce(image); // 被測(cè)試的函數(shù)
duration = static_cast<double>(cv::getTickCount()) - duration;
duration /= cv::getTickFrequency(); // 運(yùn)行時(shí)間迄委,以 ms 為單位
訪問方式 | 時(shí)間 |
---|---|
data[i] = data[i] / div * div + div / 2; | 37ms |
*data++ = *data / div * div + div / 2; | 37ms |
*data++ = v - v % div + div / 2; | 52ms |
*data++ = *data&mask + div / 2; | 35ms |
colorReduce(input, output); | 44ms |
i<image.cols*image.channels()>; | 65ms |
MatIterator | 67ms |
.at(j,i) | 80ms |
3-channel loop | 29ms |
當(dāng)輸出圖像需要被重新分配而不是以原地(in-place)方式處理時(shí)(第5行)褐筛,運(yùn)行時(shí)間為44ms,比 in-place的要慢叙身。額外的時(shí)間消耗來自于內(nèi)存分配渔扎。在循環(huán)體內(nèi)存,對(duì)于可提前計(jì)算的變量應(yīng)避免重復(fù)計(jì)算信轿。
圖像鄰域操作的一個(gè)例子
void sharpen(const cv::Mat &image, cv::Mat &result) {
// 如有必要?jiǎng)t分配內(nèi)存
result.create(image.size(), image.type());
for(int j = 1; j < image.rows-1; j++) { // 處理除了第一行和最后一行之外的所有行
const uchar* previous = image.ptr<const uchar>(j-1); // 上一行
const uchar* current = image.ptr<const uchar>(j); // 當(dāng)前行
const uchar* next = image.ptr<const uchar>(j+1); // 下一行
for(int i = 1; i < image.cols - 1; i++) {
*output++ = cv::saturate_cast<uchar>(5*current[i]-current[i-1]-current[i+1]-previous[i]-next[i]);
}
}
// 將未處理的像素設(shè)置為0
result.row(0).setTo(cv::Scalar(0));
result.row(result.rows-1).setTo(cv::Scalar(0));
result.col(0).setTo(cv::Scalar(0));
result.col(result.cols-1).setTo(cv::Scalar(0));
}
在計(jì)算輸出像素值時(shí)晃痴,模板函數(shù) cv::saturate_cast 被用來對(duì)計(jì)算結(jié)果進(jìn)行階段。
setTo 函數(shù)可以用來設(shè)置矩陣的值财忽,這個(gè)函數(shù)會(huì)將矩陣的所有元素都設(shè)為指定的值倘核。對(duì)于一個(gè)三通道的彩色圖像,需要用 cv::Scalar(a,b,c) 來指定像素三個(gè)通道的目標(biāo)值即彪。
void sharpen2D(const cv::Mat &image, cv::Mat &result) {
// 構(gòu)造核(所有項(xiàng)都初始化為 0)
cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0));
// 對(duì)核元素進(jìn)行賦值
kernel.at<float>(1,1) = 5.0;
kernel.at<float>(0,1) = -1.0;
kernel.at<float>(2,1) = -1.0;
kernel.at<float>(1,1) = -1.0;
kernel.at<float>(1,2) = -1.0;
// 對(duì)圖像進(jìn)行濾波
cv::filter2D(image, result, image.depth(), kernel);
}
- 函數(shù) cv::split 將彩色圖像的三個(gè)通道分別拷貝到三個(gè)獨(dú)立的 cv::Mat 實(shí)例中紧唱,然后在對(duì)這個(gè)通道單獨(dú)處理。
// 創(chuàng)建一個(gè)圖像向量
std::vector<cv::Mat> planes;
// 講一個(gè)三通道圖像分離為三個(gè)單通道圖像
cv::split(image1, planes);
planes[0] += image2;
// 將三個(gè)單通道圖像重新合并為一個(gè)三通道圖像
cv::merge(planes, result);
提取興趣區(qū)域(其實(shí)就是slicing)
imageROI = image(cv::Rect(colId, rowId, logo.cols, logo.rows));
定義ROI的一種方法是使用 cv::Rect隶校,顧名思義琼蚯,cv::Rect 表示一個(gè)矩形區(qū)域。指定矩形的左上角坐標(biāo)(構(gòu)造函數(shù)的前兩個(gè)參數(shù))和矩形的長寬(構(gòu)造函數(shù)的后兩個(gè)參數(shù))就可以定義一個(gè)矩形區(qū)域惠况。
另一種定義ROI的方式是指定感興趣行或列的范圍(Range)。Range是指從起始索引到終止索引(不包含終止索引)的一段連續(xù)序列宁仔。cv::Range 可以用來定義Range稠屠。如果用 cv::Range 來定義 ROI,那么前例中定義 ROI 的代碼可以重寫為:
cv::Mat imageROI = image(cv::Range(270,270+logo.rows), cv::Range(385,385+logo.cols));
cv::Mat 的 () 操作符返回另一個(gè) cv::Mat 實(shí)例,這個(gè)實(shí)例可以用在接下來的函數(shù)調(diào)用中权埠,因?yàn)镽OI和原始圖像共享數(shù)據(jù)緩沖區(qū)榨了,對(duì)ROI的任何變換都會(huì)影響到原始圖像的對(duì)應(yīng)區(qū)域。由于創(chuàng)建ROI時(shí)不會(huì)拷貝數(shù)據(jù)攘蔽,所以不論ROI的大小如何龙屉,創(chuàng)建ROI的運(yùn)行時(shí)間都是常量。
如果想創(chuàng)建包含原始圖像特定行的ROI满俗,可以使用如下代碼:
cv::Mat imageROI = image.rowRange(start, end);
類似地转捕,對(duì)于列:
cv::Mat imageROI = image.colRange(start, end);
在秘訣“遍歷圖像和鄰域操作”中使用到的row方法和col方法其實(shí)是rowRange和colRange方法的特例,即起始索引等于終止索引唆垃,等于是定義了一個(gè)單行或單列的ROI五芝。
<div id="Section6">第6章 圖像濾波</div>
- 均值濾波
cv::blur(image, result, cv::Size(5,5));
- 高斯濾波
cv::GaussianBlur(image, result, cv::Size(5,5), 1.5);
這個(gè) 1.5 就是高斯函數(shù)的$\sigma$,決定高斯函數(shù)平坦與否辕万。
- 生成 1 維高斯核
cv::Mat gauss = cv::getGaussianKernel(9, sigma, CV_32F);
9就是一維高斯核向量的長度枢步。
- 先對(duì)原圖應(yīng)用低通濾波,然后隔行渐尿、隔列取出像素
cv::Mat reducedImage; // 包含縮小后的圖像
cv::pyrDown(image, reducedImage); //將圖像尺寸減半
同理醉途,還存在 cv::pyrUp 函數(shù)將圖像尺寸放大一倍。
- 指定目標(biāo)圖像的尺寸
cv::Mat reducedImage; // 包含改變尺寸后的圖像
cv::resize(image, reducedImage, cv::Size(image.cols/3, image.rows/3)); // 改變?yōu)?1/3 大小
還提到了 cv::boxFilter 和 cv::filter2D 函數(shù)
- 中值濾波
cv::medianBlur(image, result, 5);
- Sobel函數(shù)
cv::Sobel
cv::minMaxLoc
sobel.convertTo
cv::threshold
cv::cartToPolar
cv::Scharr
<div id="Section7">附錄
把附錄的內(nèi)容全部敲下來砖茸,因?yàn)樽屇愀玫乩斫釵penCV的組織架構(gòu)隘擎,以及它是什么,能做到什么渔彰?還有就是samples/cpp/ 文件夾中的范例介紹嵌屎,應(yīng)該有最純正的OpenCV編程風(fēng)格,可以用于學(xué)習(xí)恍涂。
OpenCV3的改動(dòng)在哪宝惰?
C風(fēng)格的API很快將會(huì)消失,完全被C++的API替代再沧,代碼風(fēng)格更加簡潔尼夺,不易出錯(cuò)。讀者如果想借助OpenCV最新的功能炒瘸,記得清理代碼中C風(fēng)格API
C++ API將更加簡潔
所有的算法都將繼承自 cv::Algorithm 接口
大型的模塊拆分為小模塊淤堵,模塊將在后面繼續(xù)講解。
OpenCV 3 的源代碼文件夾:
- 3rdparty/: 包含第三方庫顷扩,如用視頻解碼用的 ffmpeg拐邪、jpg、png隘截、tiff 等圖片的解碼庫扎阶。
- apps/: 包含進(jìn)行 Haar 分類器訓(xùn)練的工具汹胃,OpenCV 進(jìn)行人臉檢測(cè)便是基于 Haar 分類器。如果你想檢測(cè)人臉以外的圖片东臀,千萬不要錯(cuò)過這幾個(gè)工具着饥。
- cmake/: 包含生成工程項(xiàng)目時(shí) cmake 的依賴文件,用于只能搜索第三方庫宰掉,普通開發(fā)者不需要關(guān)心這個(gè)文件夾的內(nèi)容赁濒。
- data/: 包含 OpenCV 庫及范例中用到的資源文件,Haar 物體檢測(cè)的分類器位于 haarcascades 子文件中流部。
- doc/: 包含生成文檔所需的源文件及輔助腳本戚绕。
- include/: 包含入口頭文件舞丛。OpenCV 子文件夾中是 C 語言風(fēng)格的 API,也就是《學(xué)習(xí) OpenCV》中描述的 API 函數(shù)果漾,官方將逐漸淘汰 C 風(fēng)格函數(shù)球切,因此我不推薦大家使用該文件夾中的頭文件吨凑。OpenCV 2 子文件夾中只有一個(gè) opencv.hpp 文件鸵钝,這是 OpenCV 2 及 OpenCV 3 推薦使用的頭文件庐镐。
- modules/: 包含核心代碼,OpenCV 真正的代碼都在這個(gè)文件夾中怠堪。OpenCV 從 2.0 開始以模塊的方式組織各種功能粟矿,近兩年模塊的數(shù)量增長得很快陌粹,后面我會(huì)依次介紹每個(gè)模塊的作用福压。
- platforms/: 包含交叉編譯所需的工具鏈及額外的代碼,交叉編譯指的是在一個(gè)操作系統(tǒng)中編譯供另一個(gè)系統(tǒng)使用的文件。
- samples/: 這是大家最喜歡的范例文件夾杆煞,后面我也會(huì)進(jìn)一步講解决乎。
CPU模塊
- androidcamera/: 僅用于 Android 平臺(tái)派桩,使得可以通過與其他平臺(tái)相同的接口來控制 Android 設(shè)備的相機(jī)铆惑。
- core/: 核心功能模塊员魏,定義了基本的數(shù)據(jù)結(jié)構(gòu),包括最重要的 Mat 類受裹、XML 讀寫棉饶、OpenGL 三維渲染等照藻。
- imgproc/: 全稱為 Image Processing坑律,即圖像處理晃择,包括圖像濾波宫屠、集合圖像變換、直方圖計(jì)算抵栈、形狀描述子等古劲。圖像處理是計(jì)算機(jī)視覺的重要工具产艾。
- highgui/: 高級(jí)圖形界面及多媒體文件讀寫闷堡,包括用戶界面、Qt弯菊、對(duì)圖像及視頻文件的讀寫操作管钳。
- video/: 視頻分析模塊蹋嵌,包括背景提取栽烂、光流跟蹤恋脚、卡爾曼濾波等糟描,做視頻監(jiān)控的開發(fā)者會(huì)經(jīng)常使用這個(gè)模塊船响。
- calib3d/: 相機(jī)標(biāo)定及三維重建见间。相機(jī)標(biāo)定用于取出相機(jī)自身缺陷導(dǎo)致的畫面形變米诉,還原真實(shí)的場(chǎng)景,確保計(jì)算的準(zhǔn)確性拴泌。三維重建通常用在雙目視覺(立體視覺)蚪腐,即兩個(gè)標(biāo)定后的攝像頭觀察同一個(gè)場(chǎng)景削茁,通過計(jì)算兩幅畫面中的相關(guān)性來估計(jì)像素深度茧跋。
- features2d/: 包含 2D 特征值檢測(cè)的框架瘾杭。包含各種特征值檢測(cè)器及描述子哪亿,如 FAST蝇棉、MSER篡殷、OBRB板辽、BRISK 等劲弦。各類特征值擁有統(tǒng)一的算法接口,因此在不影響程序邏輯的情況下可以替換替換次坡。
- objdetect/: 物體檢測(cè)模塊砸琅,包括 Haar 分類器明棍、SVM 檢測(cè)器及文字檢測(cè)摊腋。
- ml/: 全稱為 Machine Learning兴蒸,即機(jī)器學(xué)習(xí)橙凳。包括統(tǒng)計(jì)模型、K 最近鄰钓觉、支持向量機(jī)荡灾、決策樹批幌、神經(jīng)網(wǎng)絡(luò)等經(jīng)典的機(jī)器學(xué)習(xí)算法荧缘。
- flann/: 用于在多維空間內(nèi)聚類及搜索的近似算法截粗,做圖像檢索的開發(fā)者對(duì)它不會(huì)陌生桐愉。
- photo/: 計(jì)算攝影學(xué)从诲,包括圖像修補(bǔ)系洛、去噪略步、HDR 成像趟薄、非真實(shí)感渲染等。如果讀者想實(shí)現(xiàn) Photoshop 的高級(jí)功能卒落,那么這個(gè)模塊必不可少儡毕。
- stitching:/ 圖像拼接腰湾,可用于制作全景圖费坊。
- nonfree/: 受專利保護(hù)的算法葵萎,包括 SIFT 和 SURF。從功能上來說磕昼,這兩個(gè)算法屬于 features2d 模塊票从,但由于它們都是受專利保護(hù)的峰鄙,相擁在項(xiàng)目中可能需要專利方的許可吟榴。
- contrib:/ 包含新添加的實(shí)驗(yàn)性質(zhì)的代碼吩翻。開發(fā)者期待已久的人臉識(shí)別功能便位于這個(gè)模塊內(nèi)狭瞎,名為 FaceRecognizer熊锭。
- legacy/: 英文含義為遺產(chǎn)碗殷,即廢棄已久的代碼亿扁,官方不推薦使用這個(gè)模塊中的功能从祝。
- optim/: 全稱為 Optimization牍陌,這個(gè)模塊包含通用的數(shù)值優(yōu)化毒涧。包含線性規(guī)劃等算法契讲。
- shape/: 形狀匹配算法模塊捡偏,用于描述形狀银伟、比較形狀彤避。
- softcascade/: 另一種物體檢測(cè)算法琉预,Soft Cascade 分類器模孩,包含檢測(cè)模塊和訓(xùn)練模塊榨咐。
- superres/: 全稱為 Super Resolution块茁,用于增強(qiáng)圖像的分辨率。
- videostab/: 全稱為 Video Stabilization崎场,用于解決相機(jī)移動(dòng)拍攝時(shí)視頻不夠穩(wěn)定的問題谭跨。
- viz/: 三維可視化模塊螃宙∽辉可以認(rèn)為這個(gè)模塊實(shí)現(xiàn)了一個(gè)簡單的三維可視化引擎堂湖,有各種 UI 空間和鍵盤无蜂、鼠標(biāo)交互方式。底層實(shí)現(xiàn)基于 CTK 這個(gè)第三方庫。
CUDA模塊
這些模塊的名稱都以 cuda 開始慰照,cuda 是顯卡制造商 NVIDIA 推出的通用計(jì)算語言毒租,在 OpenCV 3 中有大量的模塊已經(jīng)被移植到了 cuda 語言墅垮。讓我們依次看一下算色。
- cuda/: CUDA- 加速的計(jì)算機(jī)視覺算法灾梦,包括數(shù)據(jù)結(jié)構(gòu) cuda::GpuMat若河、基于 cuda 的相機(jī)標(biāo)定及三維重建等萧福。
- cudaarithm/: CUDA- 加速的矩陣運(yùn)算模塊膏燕。
- cudabgsegm/: CUDA- 加速的背景分割模塊煌寇,通常用于視頻監(jiān)控阀溶。
- cudacodec/: CUDA- 加速的視頻編碼與解碼银锻。
- cudafeatures2d/: CUDA- 加速的特征檢測(cè)與描述模塊击纬,與 features2d/ 模塊功能類似。
- cudafilters/: CUDA- 加速的圖像濾波肯腕。
- cudaimgproc/: CUDA- 加速的圖像處理算法实撒,包含直方圖計(jì)算知态、霍夫變換等负敏。
- cudaoptflow/: CUDA- 加速的光流檢測(cè)算法友扰。
- cudastereo/: CUDA- 加速的立體視覺匹配算法村怪。
- cudawarping/: 實(shí)現(xiàn) CUDA- 加速的快速圖像變換甚负,包括透視變換梭域、旋轉(zhuǎn)、改變尺寸等既穆。
samples/ 文件夾
- android/: Android 平臺(tái)的范例幻工。既有完全是 Java 的工程囊颅,也有完全是 C++ 的工程踢代,也有更為常見的 Java 與 C++ 共存的工程。
- c/: 使用 C API 的范例。在 C API 逐漸退出歷史舞臺(tái)后儿惫,這個(gè)文件夾也應(yīng)該會(huì)隨之消失吧肾请。
- cpp/: 由于 OpenCV 是一款 C++ 庫隔显,因此 C++ 的返利是最多的括眠,后面將重點(diǎn)介紹倍权。
- directx/: directx (d3d) 是微軟的私有三維圖像 API掷豺,這個(gè)文件夾中的范例覆蓋了 d3d9、d3d10薄声、d3d11.
- gpu/: 利用 cuda 加速的范例当船。
- java/: OpenCV 3 官方支持 Java 語言綁定,因此這里演示如何使用 Java 版本的 OpenCV默辨。
- python2/: OpenCV 3 官方支持 Python 語言綁定,因此這里演示使用 Python 2 版本的范例缩幸。
- tapi/: tapi 是 OpenCV 3 的一個(gè)新特性壹置,使用 cv::UMat 替代 cv::Mat,實(shí)現(xiàn) CPU 和 GPU 的運(yùn)算使用統(tǒng)一的接口桌粉,不再需要顯式地在 CPU 和 GPU 之間傳遞數(shù)據(jù)蒸绩,方便開發(fā)人員。
- winrt/: Windows RT 平臺(tái)的范例铃肯,開發(fā)語言是微軟的 C++ “方言”.
samples/cpp/ 文件夾中的范例介紹
3calibration.cpp/: 同時(shí)標(biāo)定三臺(tái)水平放置的相機(jī)患亿。
bagofwords_classification.cpp/: 使用圖像檢測(cè)實(shí)現(xiàn)簡易的圖像搜索功能。
bgfg_gmg.cpp/: 演示 GMG 背景檢測(cè)算法的使用方式押逼。
bgfg_segm.cpp/: 演示高斯混合背景檢測(cè)算法的使用方式步藕。
brief_match_test.cpp/: 使用 BRIEF 特征值來匹配兩張圖像。
build3dmodel.cpp/: 演示如何使用基礎(chǔ)矩陣和特征值來創(chuàng)建三維模型挑格。
calibration.cpp/: 完整的多用途標(biāo)定程序咙冗。
calibration_artificial.cpp/: 在程序中生成一個(gè)虛擬的相機(jī),并進(jìn)行標(biāo)定漂彤。
camshiftdemo.cpp/: 讀取實(shí)時(shí)的攝像頭數(shù)據(jù)雾消,并演示基于均值偏移算法的視頻跟蹤。
chamfer.cpp/: 使用 Chamfer 算法匹配兩副邊緣圖像挫望。
cloning_demo.cpp/: 命令行模式的圖像克隆立润。
cloning_gui.cpp/: 圖形界面交互的圖像克隆。
connected_components.cpp/: 查找并繪制圖像中的連通區(qū)域媳板。
contours2.cpp/: 查找并繪制圖像中的輪廓桑腮。
convexhull.cpp/: 查找并繪制由點(diǎn)的集合組成的凸包。
cout_mat.cpp/: 使用 cout 來輸出各種格式化的 Mat 對(duì)象蛉幸。
create_mask.cpp/: 演示如何創(chuàng)建黑白掩碼圖像破讨。
dbt_face_detection.cpp/: 基于檢測(cè)的人臉跟蹤代碼丛晦。
delaunay2.cpp/: 通過鼠標(biāo)交互式地生成 Delaunay 三角形。
demhist.cpp/: 演示直方圖的用法提陶。
descriptor_extractor_matcher.cpp/: 演示 features2d 檢測(cè)框架的用法烫沙。
detection_based_tracker_sample.cpp/: 與 dbt_face_detection.cpp 類似。
detector_descriptor_evaluation.cpp/: 評(píng)估各種特征檢測(cè)器和描述子搁骑。
detector_descriptor_matcher_evaluation.cpp/: 評(píng)估各種特征檢測(cè)器和匹配器斧吐。
dft.cpp/: 演示一幅圖像的離散傅里葉變換。
distrans.cpp/: 顯示邊緣圖像的距離變換值仲器。
drawing.cpp/: 演示繪畫和文字顯示功能煤率。
edge.cpp/: 演示 Canny 邊緣檢測(cè)。
em.cpp/: 對(duì)隨機(jī)生成的數(shù)據(jù)點(diǎn)進(jìn)行 EM 聚類乏冀。
fabmap_sample.cpp/: 演示 FAB-MAP 圖像檢索算法蝶糯。
facerec_demo.cpp/: 人臉識(shí)別。
fback.cpp/: 實(shí)時(shí)的 Farneback 光流跟蹤辆沦。
ffilldemo.cpp/: 演示 floodFill() 像素填充算法昼捍。
filestorage.cpp/: 演示序列化到外部文件,如yml肢扯、xml等妒茬。
fitellipse.cpp/: 將輪廓點(diǎn)匹配到橢圓。
freak_demo.cpp/: 演示 FREAK 特征值的用法蔚晨。
gencolors.cpp/: 演示 generateColors()乍钻。
generic_descriptor_match.cpp/: 基于 SURF 的兩幅圖像間的匹配。
grabcut.cpp/: 演示 GrabCut 分割算法铭腕。
houghcircles.cpp/: 用霍夫算法檢測(cè)圓银择。
houghlines.cpp/: 用霍夫算法檢測(cè)直線。
hybridtrackingsample.cpp/: 混合跟蹤算法(Hybrid Tracker)的演示累舷。
image.cpp/: 來回轉(zhuǎn)換 cv::Mat 和 IplImage浩考。
image_alignment.cpp/: 演示 findTransformECC() 函數(shù)。
image_sequence.cpp/: 使用 VideoCapture 對(duì)象讀取序列幀被盈。
imagelist_creator.cpp/: 創(chuàng)建圖像列表到 xml 文件析孽。
inpaint.cpp/: 使用鼠標(biāo)交互地進(jìn)行圖像修補(bǔ)。
intelperc_capture.cpp/: Intel 感知計(jì)算設(shè)備相關(guān)的函數(shù)只怎。
kalman.cpp/: 使用卡爾曼濾波進(jìn)行二維跟蹤袜瞬。
kmeans.cpp/: Kmeans 聚類算法的演示。
laplace.cpp/: 拉普拉斯邊緣檢測(cè)尝盼。
latentsvm_multidetect.cpp/: latentSVM 檢測(cè)器。
letter_recog.cpp/: 字母識(shí)別佑菩。
linemod.cpp/: 基于 OpenNI 的體感設(shè)備應(yīng)用盾沫。
lkdemo.cpp/: 演示Lukas-Kanade 光流法裁赠。
logpolar_bsm.cpp/: 演示 LogPolar 盲點(diǎn)模型。
lsd_lines.cpp/: LSD 線段檢測(cè)赴精。
matcher_simple.cpp/: SURF 特征檢測(cè)佩捞。
matching_to_many_images.cpp/: 一對(duì)多的特征檢測(cè)筒狠。
meanshift_segmentation.cpp/: 演示基于均值漂移的色彩分割函數(shù)——meanShiftSegmentation()厂财。
minarea.cpp/: 尋找最小包圍盒浓恳、包圍圓扣墩。
morphology2.cpp/: 形態(tài)學(xué)圖像處理阳欲。
npr_demo.cpp/: 演示各種非真實(shí)感渲染效果刊橘。
opencv_version.cpp/: 輸出 OpenCV 庫的版本號(hào)尉姨。
openni_capture.cpp/: 演示 OpenNI 相關(guān)的體感設(shè)備翩活。
pca.cpp/: 基于 PCA 的人臉識(shí)別逐哈。
peopledetect.cpp/: 基于 cascade 或 hog 進(jìn)行物體(人)檢測(cè)芬迄。
phase_corr.cpp/: 演示 phaseCorrelate() 函數(shù)。
points_classifier.cpp/: 演示各種機(jī)器學(xué)習(xí)算法昂秃。
rgbdodometry.cpp/: 對(duì)深度傳感器如 Kinect 的數(shù)據(jù)進(jìn)行處理禀梳。
segment_objects.cpp/: 實(shí)時(shí)地在視頻或相機(jī)畫面中檢測(cè)前景物體。
shape_example.cpp/: 比較并檢索形狀肠骆。
shape_transformation.cpp/: 用 SURF 特征值檢測(cè)形狀并進(jìn)行變換算途。
squares.cpp/: 檢測(cè)圖像中的方塊形狀。
starter_imagelist.cpp/: 一個(gè) “hello worl” 性質(zhì)的入門范例蚀腿。
starter_video.cpp/: 另一個(gè) “hello worl” 性質(zhì)的入門范例嘴瓤。
stereo_calib.cpp/: 雙目視覺的標(biāo)定。
stereo_match.cpp/: 計(jì)算左右視覺的圖像的差異唯咬,生成點(diǎn)云文件纱注。
stitching.cpp/: 演示圖像拼接算法。
stitching_detailed.cpp/: 演示更多參數(shù)的圖像拼接算法胆胰。
textdetection.cpp/: 實(shí)時(shí)場(chǎng)景中的文字定位與識(shí)別狞贱。
train_HOG.cpp/: 訓(xùn)練 HOG 分類器。
ufacedetect.cpp/: 人臉檢測(cè)蜀涨。
video_homography.cpp/: 使用 FAST 特征值來跟蹤平面物體瞎嬉。
videostab.cpp/: 演示 videostab 中各個(gè)參數(shù)的用法。
watershed.cpp/: 演示著名的分水嶺圖像分割算法厚柳。
本書程序代碼及彩圖下載:
http://www.sciencep.com/downloads/
https://github.com/ITpublishing
<div id="Section8">勘誤</div>
- P249 頁氧枣,倒數(shù)第 5-6 行,分別有兩個(gè)“圖像中”多余别垮。
<div id="Section9">我的困惑</div>
- 深拷貝 image.clone() 和 copyTo 有什么區(qū)別便监?不是一樣的嗎
<div id="Section10">下一步計(jì)劃</div>
初寫于 2015-04-05,未完待續(xù)。
首發(fā)于 Yimian Dai's Homepage烧董,轉(zhuǎn)載請(qǐng)注明出處毁靶。