最近在系統地學習OpenCV,將學習的過程在此做一個記錄,主要以代碼+注釋的方式
記錄學習過程。
1.訪問像素值
要訪問矩陣中的每個獨立元素,只需要指定它的行號和列號。返回的對應元素可以是單個數值,也可
以是多通道圖像的數值向量。給圖像加入椒鹽噪聲(salt-and-pepper noise),來說明如何直接訪問像素值。顧名思義,椒鹽噪聲是一個專門的噪聲類型,它隨機選擇一些像素,把它們的顏色替換成白色或黑色。
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void salt(cv::Mat image, int n){ int i, j; for (int k = 0; k < n; k++) { // rand()是隨機數生成器 //利用cv::Mat中公共成員變量cols和rows得到圖像的列數和行數 i = std::rand() % image.cols; j = std::rand() % image.rows; //使用type方法來區分灰度圖像和彩色圖像。 if (image.type() == CV_8UC1) // 灰度圖像 { //利用cv::Mat的at(int y,int x)方法可以訪問元素 //at方法被實現成一個模板方法,在調用時必須指定圖像元素的類型 image.at<uchar>(j, i) = 255; } else if (image.type() == CV_8UC3) // 彩色圖像 { /*彩色圖像的每個像素對應三個部分:紅色、綠色和藍色通道。因此包 含彩色圖像的cv::Mat類會返回一個向量,向量中包含三個8位的數值。 OpenCV為這樣的短向量定義了一種類型,即cv::Vec3b。這個向量包含 三個無符號字符(unsigned character)類型的數據。因此,訪問彩色 像素中元素的方法如下:*/ image.at<cv::Vec3b>(j, i)[0] = 255; image.at<cv::Vec3b>(j, i)[1] = 255; image.at<cv::Vec3b>(j, i)[2] = 255; } }}/*修改圖像的函數在使用圖像作為參數時,都采用了值傳遞的方式。之所以這樣做,是因為它們在復制圖像時仍共享了同一塊圖像數據。因此在需要修改圖像內容時,圖像參數沒必要采用引用傳遞的方式*/int main(){ // 打開圖像 cv::Mat image = cv::imread("boldt.jpg"); // 調用函數以添加噪聲 salt(image, 3000); // 顯示圖像 cv::namedWindow("Image"); cv::imshow("Image", image); cv::waitKey(0); return 0;}運行結果:

2.用指針遍歷圖像 以減少圖像中顏色的數量這個任務為例,來說明遍歷圖像的過程。
彩色圖像由三個通道組成,每個通道對應三原色(紅、綠、藍)之一的強度。由于每個強度值
都是用一個8位的unsigned char表示,所以全部可能的顏色數目為256 × 256 × 256,
大于1600萬個。理所當然,為了降低分析的復雜度,降低圖像中的顏色數目有時是有用的。
基本的減色算法很簡單。假設N是減色因子,將圖像中每個像素的每個通道的值除以N
(使用整數除法,不保留余數)。然后將結果乘以N,得到N的倍數,并且剛好不超過原始像素值。
只需加上N/2,就得到相鄰的N倍數之間的中間值。對所有8位通道值重復這個過程,就會得到(256/N)× (256/N)×(256/N)種可能的顏色值。
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void colorReduce(cv::Mat image, int div = 64){ int nl = image.rows; // 行數 // 每行的元素數量 int nc = image.cols * image.channels(); for (int j = 0; j < nl; j++) { // 取得行j的地址 /*為了簡化指針運算的計算過程,cv::Mat類提供ptr函數,可以 直接訪問圖像中任一行的地址。ptr函數是一個模板函數, 返回第j行的地址:*/ uchar* data = image.ptr<uchar>(j); for (int i = 0; i < nc; i++) { // 處理每個像素 --------------------- data[i] = data[i] / div*div + div / 2; // 像素處理結束 ----------- /*注意在處理語句中, 我們也可以采用另一種等價的做法, 即利用指針 運算從一列移到下一列。 因此可以使用下面的代碼:*/ //*data = *data / div*div + div2; data++; } // 一行結束 }}int main(){ // 讀取圖像 cv::Mat image = cv::imread("boldt.jpg"); // 處理圖像 colorReduce(image, 64); // 顯示圖像 cv::namedWindow("Image"); cv::imshow("Image", image); cv::waitKey(0); return 0;}運行結果:
3.用迭代器遍歷圖像 在面向對象編程時,我們通常用迭代器對數據集合進行循環遍歷。迭代器是一種類,
專門用于遍歷集合的每個元素,隱藏了遍歷過程的具體細節。標準模板庫(STL)對容器類型
都定義了對應的迭代器,OpenCV也提供了cv::Mat的迭代器,并且與C++ STL中的標準迭代器兼容。
依然以減色程序為例。
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void colorReduce(cv::Mat &image, int div = 64) { // 在初始位置獲得迭代器 /*要得到cv::Mat實例的迭代器,首先要創建一個cv::MatIterator_對象。 跟cv::Mat_類似,這個下劃線表示它是一個模板子類。 因為圖像迭代器是用來 訪問圖像元素的,所以必須在編譯時就明確返回值的類型。 可以這樣定義迭代器:*/ cv::Mat_<cv::Vec3b>::iterator it; /*然后就可以使用常規的迭代器方法begin和end對像素進行循環遍歷了。 不同之處在于它們仍然是模板方法。*/ it = image.begin<cv::Vec3b>(); // 獲得結束位置 cv::Mat_<cv::Vec3b>::iterator itend = image.end<cv::Vec3b>(); // 循環遍歷所有像素 for (; it != itend; ++it) { // 處理每個像素 --------------------- /*注意這里處理的是一個彩色圖像, 因此迭代器返回cv::Vec3b實 例。 你可以用取值運算符[]訪問每個顏色通道的元素。*/ (*it)[0] = (*it)[0] / div*div + div / 2; (*it)[1] = (*it)[1] / div*div + div / 2; (*it)[2] = (*it)[2] / div*div + div / 2; // 像素處理結束 ---------------- }}int main(){ // 讀取圖像 cv::Mat image = cv::imread("boldt.jpg"); // 處理圖像 colorReduce(image, 64); // 顯示圖像 cv::namedWindow("Image"); cv::imshow("Image", image); cv::waitKey(0); return 0;}不管掃描的是哪種類型的集合,使用迭代器時總是遵循同樣的模式。
首先你要使用合適的專用類創建迭代器對象,在本例中是cv::Mat_<cv::Vec3b>:: iterator,
然后可以用begin方法,在開始位置(本例中為圖像的左上角)初始化迭代器。對于cv::Mat實例,
可以使用image.begin<cv::Vec3b>()。
還可以在迭代器上使用數學計算,例如若要從圖像的第二行開始,可以用
image.begin<cv::Vec3b>()+image.cols初始化cv::Mat迭代器。
獲得集合結束位置的方法也類似,只是改用end方法。但是,用end方法得到的迭代器已經超出了
集合范圍,因此必須在結束位置停止迭代過程。結束的迭代器也能使用數學計算,例如,如果你想在最后一行前就結束迭代, 可使用image.end<cv::Vec3b>()-image.cols。初始化迭代器后,建立一個循環遍歷所有元素,直到與結束迭代器相等。
典型的while循環就像這樣:
while (it!= itend) {// 處理每個像素 ---------------------// 像素處理結束 ---------------------++it;}你可以用運算符++來移動到下一個元素,也可以指定更大的步幅。例如用it+=10,
對每10個像素處理一次。最后,在循環內部使用取值運算符*來訪問當前元素,你可以用它來讀(例如element= *it;)或寫(例如*it= element;)。
運行結果(同2中指針遍歷的效果):

4.檢查代碼運行效率為了衡量函數或代碼段的運行時間,OpenCV有一個非常實用的函數,即cv::getTickCount(),
該函數返回從最近一次電腦開機到當前的時鐘周期數。因為我們希望得到以秒為單位的代碼運行時間,所以要使用另一個方法,即cv::getTickFrequency(),這個方法返回每秒的時鐘周期數。
為了獲得某個函數(或代碼段)的運行時間,通常需使用這樣的程序模板:
const int64 start = cv::getTickCount();colorReduce(image); // 調用函數// 經過的時間( 單位: 秒)double duration = (cv::getTickCount()-start)/cv::getTickFrequency();5.遍歷圖像和鄰域操作在圖像處理中計算像素值時,經常需要用它的相鄰像素的值。
以對圖像進行銳化為例,在圖像處理領域有一個眾所周知的結論:如果從圖像中減去拉普拉斯算子部分,圖像的邊緣就會放大,因而圖像會變得更加尖銳。
用以下方法計算銳化的數值:
sharpened_pixel= 5*current-left-right-up-down;
這里不能使用就地處理,使用者必須提供一個輸出圖像。圖像掃描中使用了三個指針,一個表示當前行, 一個表示上面的行,另外一個表示下面的行。另外,在計算每一個像素時都需要訪問與它相鄰的像素,因此有些像素的值是無法計算的,包括第一行、最后一行、第一列、最后一列的像素。這個循環可以這樣寫:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void sharpen(const cv::Mat &image, cv::Mat &result) { // 判斷是否需要分配圖像數據。 如果需要, 就分配 result.create(image.size(), image.type()); int nchannels = image.channels(); // 獲得通道數 // 處理所有行( 除了第一行和最后一行) 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); // 當前行 const uchar* next = image.ptr<const uchar>(j + 1); // 下一行 uchar* output = result.ptr<uchar>(j); // 輸出行 for (int i = nchannels; i < (image.cols - 1)*nchannels; i++) { *output++ = cv::saturate_cast<uchar>( 5 * current[i] - current[i - nchannels] - current[i + nchannels] - previous[i] - next[i]); } } // 把未處理的像素設為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));}int main(){ // 讀取圖像 cv::Mat image = cv::imread("boldt.jpg"); cv::Mat result; // 處理圖像 sharpen(image, result); // 顯示圖像 cv::namedWindow("Image"); cv::imshow("Image", result); cv::waitKey(0); return 0;}5+.卷積操作在對像素鄰域進行計算時, 通常用一個核心矩陣來表示。 這個核心矩陣展現了為得到預期結果, 如何將計算相關的像素組合起來。 針對本節使用的銳化濾波器, 核心矩陣可以是這樣的:

鑒于濾波是圖像處理中常見的操作,OpenCV專門為此定義了一個函數, 即cv::filter2D。 要使用這個函數, 只需要定義一個內核( 以矩陣的形式) , 調用函數并傳入圖像和內核, 即可返回濾波后的圖像。 因此, 使用這個函數可以很容易地重新定義銳化函數:
#include <opencv2/core/core.hpp>#include <opencv2/highgui/highgui.hpp>void sharpen2D(const cv::Mat &image, cv::Mat &result) { // 構造內核( 所有入口都初始化為0) cv::Mat kernel(3, 3, CV_32F, cv::Scalar(0)); // 對內核賦值 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, 0) = -1.0; kernel.at<float>(1, 2) = -1.0; // 對圖像濾波 cv::filter2D(image, result, image.depth(), kernel);}int main(){ // 讀取圖像 cv::Mat image = cv::imread("boldt.jpg"); cv::Mat result; // 處理圖像 sharpen2D(image, result); // 顯示圖像 cv::namedWindow("Image"); cv::imshow("Image", result); cv::waitKey(0); return 0;}但是這段代碼報錯:“filter2D”: 不是“cv”的成員。不知為何。6.實現簡單的圖像運算
圖像就是普通的矩陣,可以進行加、減、乘、除運算,我們使用算法運算符,將第二個圖像與輸入圖像進行組合。下面就是第二個圖像:
這里我們把兩個圖像相加,用于創建特效圖或覆蓋圖像中的信息。 我們可以使用cv::add函數
來實現相加功能。現在我們想得到加權和,因此使用更精確的cv::addWeighted函數:
cv::addWeighted(image1,0.7,image2,0.9,0.,result);操作的結果是一個新圖像,如下圖所示:
新聞熱點
疑難解答