C++ OpenCV學習筆記(完結)


 

1、圖像的加載、修改與保存

涉及API:

cv::imread();    //讀取
cv::imshow();    //顯示
cv::cvtColor();  //修改
cv::imwrite();   //保存

 

擴展圖像窗口創建API:cv::namedWindow();
cv::namedWindow需要兩個參數,第一個參數是窗口名稱,第二個參數是關於窗口操作的關鍵字(包含:WINDOW_AUTOSIZE會根據圖像大小自動設置窗口大小並且生成的窗口大小不能修改;WINDOW_NORMAL此關鍵字一般使用在跟QT集成以后的程序中,表示允許修改窗口大小)

cv::imread:
參數兩個,第一個參數是圖像存儲的絕對路徑,第二個參數讀取圖像類型(包含:IMREAD_UNCANGED表示加載原圖;IMREAD_GRAYSCALE表示將圖像作為灰度圖像加載進來;IMREAD_COLOR表示原圖作為RGB圖像加載進來)

cv::imshow:
兩個參數,第一個參數是圖像窗口名稱(可以自動創建),第二個參數是Mat對象名

cv::cvtColor:
三個參數,第一個是需更改的Mat對象名,第二個是用於保存更改后的Mat對象名,第三個參數是修改使用的源和目標色彩空間(如:COLOR_BGR2GRAY表示修改成灰度圖像)

cv::imwrite:
使用時包含兩個參數,第一個參數是保存圖像的絕對路徑,第二個參數是需要保存的Mat對象名


2、矩陣的掩膜操作

獲取圖像像素指針:

CV_Assert(image.depth() == CV_8U)

Mat.ptr(int i = 0)獲取像素矩陣的指針,其中索引 i 表示第幾行,從0開始計行數
獲取當前行指針語句:

const uchar* current = image.ptr<uchar>(row);

獲取當前像素點P(row,col)的像素值語句:

p(row,col) = current[col];

像素范圍處理saturate_cast<uchar>(重要函數)

saturate_cast<uchar>(-100) //返回0
saturate_cast<uchar>(299)  //返回255
saturate_cast<uchar>(100)  //返回100

 

說明:此函數的功能是確保RGB值的范圍在0~255之間

什么是圖像掩膜操作,掩膜操作實現的是圖像對比度調整

opencv提供的掩膜操作(對比度提高)API:filter2D
定義掩膜矩陣:

Mat kernel = (Mat_<char>(3,3)<< 0,-1,0,-1,5,-1,0,-1,0);

API調用舉例:filter2D(img, dst, img.depth(), kernel);
說明:第一個參數是操作對象名,第二個參數保存操作后對象名,第三個參數圖像深度(使用depth()函數獲取了原圖像深度),第三個參數是掩膜方法(對應掩膜矩陣)

如何初始化一個零時Mat對象用於存儲原圖像?
代碼:

Mat dst = Mat::zeros(img.size(), img.type()); //zeros方法代表創建RGB為0的純黑圖像,大小和類型與原圖像相同

拓展:執行時間的顯示
代碼:

double t = getTickCount();

/* 代碼部分 */
double time = (getTickCount() - t)/getTickFrequency();
// cout << "執行時間: " << time << endl;

 


3、Mat對象(一種比Iplimage更加安全的存儲對象,Mat對象的內存空間被自動分配)

Mat對象構造函數:

Mat()
Mat(int rows, int cols, int type)
Mat(Size size, int type)

Mat(int rows, int cols, int type, const Scaler &s)    // 說明:前兩個參數分別表示行和列,第三個參數是類型參數(比如CV_8UC3中8表示每個通道占8位,U表示無符號,C表示Char類型,3表示三個通道數),第四個參數是向量表示初始化每個像素值為多少,向量長度對應通道數目一致。

Mat(Size size, int type, const Scaler &s)    //Scaler()用來給像素賦值
Mat(int ndims, const int *sizes, int type)
Mat(int ndims, const int *sizes, int type, const Scaler &s)

 

說明:拷貝構造函數只會賦值對象頭部,使用API–>demo = mat.clone() or mat.copyTo(demo) 才能進行完全復制(包括數據部分)

常用方法:

void copyTo(Mat mat);
void convertTo(Mat dst, int type);
Mat clone();
int channels();
int depth();
bool empty();
uchar* ptr(i = 0);    //備注:查資料詳學(讀取像素值)
cv::Mat::create(size, type) //create方法創建對象(可指定對象尺寸大小)

 

兩種用法:

M.create(img.size(), img.type()); 
M.create(4,3,CV_8UC2); M = Scaler(123,123);

 

定義小數組:(掩膜運用)

Mat kernel = (Mat_<char>(3,3)<< 0,-1,0,-1,5,-1,0,-1,0);

 

初始化全0圖像有多種方法,其中比較特殊的是Mat::zero(size, type);
用法:

Mat m = Mat::zero(img.size(), img.type());
Mat m = Mat::zero(2, 2, CV_8UC1);

 

拓展:Mat::eye(……)方法,初始化對角線為一的圖像矩陣。


4、圖像操作

讀取像素:
· 讀取一個gray像素點的像素值(CV_8UC1)
Scalar intensity = img.at<uchar>(row, col); or Scalar intensity = img.at<uchar>(Point(row, col));

· 讀取一個RGB像素點的像素值

Vec3f intensity = img.at<Vec3f>(row, col);
float blue  = intensity.val[0];
float green = intensity.val[1];
float red   = intensity.val[2];

for(int row = o; row < img.rows; row++)
{
    for(int col = 0; col < img.cols; col++)
    {
        int b = img.at<Vec3b>(row, col)[0];
        int g = img.at<Vec3b>(row, col)[1];
        int r = img.at<Vec3b>(row, col)[2];
    }
}

說明:Vec3b是一種數據結構,放置BGR像素點,3b表示3bit讀取,也可以用Vec3f,3f表示以float類型讀取,如第一種讀取方法。

修改像素:
· 灰度圖像

img.at<uchar>(row, col) = 123;

 

· RGB圖像

img.at<Vec3b>(row, col)[0] = 123;    //修改參數B
img.at<Vec3b>(row, col)[1] = 123;    //修改參數G
img.at<Vec3b>(row, col)[2] = 123;    //修改參數R

 

· 空白圖

img = Scalar(100);    //將每個像素點賦值為100

 

· ROI選擇

Rect r(10, 10, 100, 100);
Mat smallimg = img(r);

 

Vec3b與Vec3f
· Vec3b對應三通道順序blue、green、red的uchar類型數據
· Vec3f對應三通道float類型數據
· 把CV_8UC1轉換到CV32F1實現如下:

img.convertTo(dst, CV_32F1);    //使用API-->convertTo(dst, type);

 


5、圖像混合

· 理論-線性混合操作
g(x) = (1-a)f0(x)+af1(x)
說明:f0(x)表示一個圖像,f1(x)表示另一個圖像,其中a的取值范圍為0~1之間,g(x)表示混合后得到的圖像(注意:對圖像每個像素的操作)

相關API

cv::addWeighted(inputArray     src1,          //參數1:輸入圖像Mat - src1
                double         alpha,         //參數2:輸入圖像src1的alpha值(alpha表示表達式中的a)
           inputArray     src2,          //參數3:輸入圖像Mat - src2
             double         beta,          //參數4:輸入圖像src2的alpha值
             double         gamma,         //參數5:gamma值(校驗值,使其得到正常圖像)
             OutputArray    dst,           //參數6:輸出混合圖像
             int         dtpye = -1     //dtpye默認不用帶入
             )

 

注意:兩張圖像大小和類型必須一致才可以使用此API混合

dst(I) = saturate(src1(I) * alpha +src2(I) * beta + gamma);

 

拓展API:

add(img, dst, dst1);    //直接疊加兩個圖像像素
multiply(img, dst, dst1, 1.0);    //兩個圖像像素相乘

 


6、調整圖像亮度和對比度

圖像變換可以看作像素變換(點操作)和領域操作(區域操作),調整圖像亮度和對比度屬於像素變換
g(i,j) = af(i,j) + β (其中a>0,β是增益變量)

再次回顧重要API:
//像素范圍處理函數
saturate_cast<uchar>(-100),返回0
saturate_cast<uchar>(299),返回255
saturate_cast<uchar>(100),返回100

//圖像數據類型轉換
img.convertTo(dst, CV_32F);
說明:如果圖像默認bit類型,我們把數據轉換成浮點型,提高圖像精度可以使處理效果提高。


7、繪制形狀和文字

· 使用cv::Pointcv::Scalar
Point表示2D平面上一個點,坐標(x,y)
如下:

Point p;
p.x = 10;
p.y = 8;
Point p1 = Point(x, y );

Scalar表示四個元素的向量
Scalar(a, b, c); //a = Blue, b = green, c = Red表示BGR三個通道

· 繪制線、矩形、圓、橢圓等基本幾何形狀
API使用:
線–> cv::line(LINE_4\LINE_8\LINE_AA) //參數表示繪制線的類型,LINE_AA表示反鋸齒
橢圓–> cv::ellipse
橢圓API說明:使用語句示例–>

ellipse(img, Point(img.rows/2, img.cols/2), Size(img.rows/5, img.cols/6), 90, 0, 360, color, 2, 8);
/* Point表示橢圓中心坐標,size表示橢圓尺寸,其中兩個參數表示長短軸,angle = 90表示順時針方向旋轉角,startAngle = 0表示繪制的起始角度,endAngle = 360表示繪制的終止角度。*/

矩形 –> cv::rectangle  // 五個參數,第一個參數是Mat對象,第二個參數矩形類型,第三個參數顏色,第四個參數線寬(默認1),第五個參數線的類型(默認LINE_8)
圓 –> cv::circle
填充 –> cv::fillPoly
fillPoly各參數說明:用法 –>

Point pts[1][5] = { Point(100,100), Point(100,200), Point(200,200), Point(200,100), Point(100,100) };
const Point* ppts[] = { pts[0] };
int npt[] = { 5 };
fillPoly(img, ppts, npt, 1, Scalar(255,0,255), 8);        
//ppts表示多邊形各頂點集合(靜態點對象指針數組),npt表示多邊形頂點個數,                                
//ncontours = 1表示填充個數

對象:
Rect rect = Rect(x, y, w_len, h_len);  // 后兩個參數分別是寬高

· 隨機生成與繪制文本
繪制文本API:cv::putText(Mat&, string, Point, int_fontFace, double_fontScale, Scalar, thickness, lineType, bottomLeftOrigin = false);
代碼示例:

putText(img, "Hello OpenCV", Point(100,100), CV_FONT_HERSHEY_COMPLEX, 1.0, Scalar(0,0,255), 1, 8);

說明:int_fontFace表示字體類型,double_fontScale表示字體縮放比例,bottomLeftOrigin默認為FALSE不用管。

隨機生成圖像(以畫線為例)
代碼如下:

void RandomLineDemo(Mat& img)
{
    RNG rng(12345);
    Point pt1, pt2;
    Mat dst = Mat::zeros(img.size(), img.type());
    namedWindow("test7", CV_WINDOW_AUTOSIZE);
    for (int i = 0; i < 10000; i++)
    {
        pt1.x = rng.uniform(0, img.cols);
        pt2.x = rng.uniform(0, img.cols);
        pt1.y = rng.uniform(0, img.rows);
        pt2.y = rng.uniform(0, img.rows);
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
        if (waitKey(50) > 0)
            break;
        imshow("test7", dst);
        line(dst, pt1, pt2, color);
    }
}

說明:RNG是opencv中的隨機數類,構造函數指明隨機數范圍或種子個數,使用uniform(正態分布隨機數)方法指定隨機數范圍,同樣的也可以使用gaussian(double sigma)方法生成高斯隨機數


8、模糊圖像(一)

· 圖像的模糊原理

  • Smooth/Blur是圖像處理中最簡單和常用的操作之一
  • 使用該操作的原因之一就是為了給圖像預處理時候降低噪聲
  • 使用Smooth/Blur操作其背后是數學的卷積計算:g(i, j) = Σ f( i+k, j+l)h(k, l)
  • 通常這些卷積算子計算都是線性操作,所以又叫線性濾波
  • 歸一化盒子濾波(均值濾波)
  • 高斯濾波

· 相關API

  • 均值模糊:
    blur(Mat src, Mat dst, Size(xradius, yradius), Point(-1, -1)); //Point表示中心像素在哪里,(-1, -1)表示默認中心像素

  • 高斯濾波:
    GaussianBlur(Mat src, Mat dst, Size(11, 11), sigmax, sigmay); //sigmax, sigmay是用於調節正態分布圖像的參數
    注意:其Size(x, y)中x和y必須是正數而且是奇數,size表示窗口大小


9、模糊圖像(二)

· 中值濾波

  • 統計排序濾波器
  • 中值對椒鹽噪聲有很好的抑制作用
    比如有一個5×5圖像:
    123 125 126 130 140
    122 124 126 127 135
    118 120 150 125 134
    119 115 119 123 133
    111 116 110 120 130
    3×3領域像素(坐標為22到44)排序如下:115,119,120,123,124,125,126,127,150
    中值等於:124
    均值等於:125.33

API:medianBlur (Mat src, Mat dst, ksize)
注意:中值模糊的ksize大小必須是大於1而且為奇數 --> ksize表示卷積核大小

· 雙邊濾波

  • 均值濾波無法克服邊緣像素信息丟失缺陷,原因是均值濾波是基於平均權重
  • 高斯模糊部分克服該缺陷,但無法完全避免,原因是沒有考慮像素值的不同
  • 高斯雙邊模糊是邊緣保留的濾波方法,避免了邊緣信息的丟失,保留了圖像輪廓不變

API:bilateralFilter (src, dst, d=15, 150, 3)
說明:d=15為計算半徑,半徑之內的像素都會被納入計算,如果該參數提供-1則會根據sigma space參數取計算半徑
150表示sigma color,決定多少差值之內像素會被計算
3表示sigma space如果d值大於0則聲明無效


10、膨脹與腐蝕

· 膨脹

  • 圖像形態學操作:基於形狀的一系列圖像操作集合,主要是基於集合論基礎上的形態學數學
  • 形態學有四個基本操作:腐蝕、膨脹、開、閉
  • 腐蝕和膨脹是圖像處理中最基本的形態學操作

說明:膨脹操作跟卷積操作類似,假設有圖像A和機構元素B,結構元素B在A上移動,其中B定義其中心為錨點,計算B覆蓋下A的最大像素值用來替換錨點像素其中B作為結構體可以是任意形狀。而腐蝕跟膨脹操作類似,唯一不同的是以最小值替換錨點重疊下圖像的像素值。

相關API:
· getStructuringElement(int shape, Size ksize, Point anchor) //獲取結構形狀
說明:三個參數分別代表形狀(MORPH_RECT \ MORPH_CROSS \ MORPH_ELLIPSE)、大小(要求奇數)、錨點(默認是Point(-1,-1)意思就是中心像素)
· dilate(src, dst, kernel) //膨脹
· erode(src, dst, kernel) //腐蝕

拓展:
動態調整結構元素大小 (GUI函數)

 TrackBar -> createTrackbar(constString & trackbarname,
                const String winName,
                int* value,
                int count,
                Trackbarcallback func,
                void* userdata = 0)
//其中最重要的是callback函數功能,如果設置為NULL就是說只有值update,但是不會調用callback的函數。

 


11、形態學操作(多用於二值圖像處理)

· 開操作 - open
說明:圖像先腐蝕后膨脹的操作即稱為圖像的開操作,圖像開操作可以去掉小的對象。

· 閉操作 - close
說明: 先膨脹后腐蝕稱之為閉操作,可以講大面積圖像中小的缺口給填充。

· 形態學梯度 - Morphological Gradient
說明:圖像膨脹后減去原圖的腐蝕圖像,此種方法又稱基本梯度,得到的圖像具有梯度效果。

· 頂帽 - top hat
說明:頂帽是原圖像與開操作之間的差值圖像

· 黑帽 - black hat
說明:黑帽是閉操作圖像與源圖像的差值圖像

API:morphologyEx(src, dst, CV_MOP_BLACKHAT, kernel);
參數說明:
- int OP --> CV_MOP_OPEN/ CV_MOP_CLOSE/ CV_MOP_GRADIENT/ CV_MOP_TOPHAT/ CV_MOP_BLACKHAT (表示形態學操作類型)
- Mat kernel --> 結構元素(選取大小取決於去掉的對象大小)
- int Iteration=1 --> 迭代次數,默認為1

使用實例(開操作):

Mat kernal = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
morphologyEx(src, dst, CV_MOP_OPEN, kernal);
imshow("dst", dst);
waitKey(0);

 


12、提取水平與垂直線(形態學應用)

· 原理方法
圖像形態學操作的時候,可以通過自定義的結構元素實現結構元素對輸入圖像一些對象敏感、另外一些對象不敏感,這樣就會讓敏感的對象改變而不敏感的對象保留輸出。
通過使用兩個最基本的形態學操作:膨脹與腐蝕。使用不同的結構元素實現對輸入圖像的操作並得到想要的結果。

知識回顧:

  • 膨脹,輸出的像素值是結構元素覆蓋下輸入圖像的最大像素值
  • 腐蝕,輸出的像素值是結構元素覆蓋下輸入圖像的最小像素值

· 結構元素
膨脹與腐蝕過程是可以使用任意的結構元素,常見的形狀:矩形、圓、直線、磁盤形狀、磚石形狀等各種自定義形狀。

· 提取步驟
1 - 輸入圖像
2 - 灰度變換
3 - 二值化 --> adaptiveThreshold
4 - 定義結構元素
5 - 開操作提取水平與垂直線

相關API - adaptiveThreshold(src, dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C);
參數說明:
double maxValue - 輸入圖像最大灰度值
int adaptiveMethod - 自適應閾值方法(ADAPTIVE_THRESH_MEAN_C / ADAPTIVE_THRESH_GAUSSIAN_C)
int thresholdType - 閾值類型(常用 THRESH_BINARY)
int blockSize - 子塊大小
double C - 常量,可為正數、負數、0

結構元素定義實例代碼:

Mat hline = getStructuringElement(MORPH_RECT, Size(src.cols / 16, 1), Point(-1, -1));
Mat wline = getStructruingElement(MORPH_RECT, Size(1, src.row . 16), Point(-1, -1));

拓展API:bitwist_not(src, src); --> 圖像二進制數據“非”操作


13、圖像上采樣和降采樣

· 圖像金字塔概念說明
我們在圖像處理中常常會調整圖像大下,最常見的就是放大和縮小,盡管幾何變換也可以實現圖像的放大和縮小,但是這里所說的
大小調整是指圖像金字塔從下到上分辨率的縮小。一個圖像金字塔是一系列圖像組成,最底下一張是圖像尺寸最大,最上方圖像尺
寸最小,從空間上由上向下看就像古埃及金字塔。

圖像金字塔分為高斯金字塔和拉普拉斯金字塔,高斯金字塔用來對圖像進行降采樣,拉普拉斯金字塔用來重建一張圖片根據它的上
層降采樣圖片

高斯金字塔:

  • 高斯金字塔是從底向上,逐層采樣得到的

  • 降采樣之后圖像大小是原圖像 M × N 的 M/2 × N/2,就是對原圖像刪除偶數行與列,即得到采樣后的上層圖像

  • 高斯金字塔的生成過程分為兩步:
    1 - 對當前層進行高斯模糊
    2 - 刪除當前層的偶數行與列
    即可得到上一層圖像,這樣上一層和下一層相比,都只有它大小的1/4

          	{1, 4, 6,4,1;	
           	 4,16,24,16,4;
     1/16 × 6,24,36,24,6;
           	 4,16,24,16,4;
           	 1, 4, 6, 4,1;}
    

高斯不同(DOG):

  • 定義:把同一張圖像在不同的參數下做高斯模糊之后的結果相減,得到的輸出圖像稱之為高斯不同。

  • 高斯不同是圖像的內在特征,在灰度圖像增強、角點檢測中經常使用
    代碼示例:

    cvtColor(src, gray_src, CV_BGR2GRAY);
    GaussianBlur(gray_src, g1_dst, Size(3, 3), 0, 0);
    GaussianBlur(g1_dst, g2_dst, Size(3, 3), 0, 0);
    subtract(g1_dst, g2_dst, DOGimage, Mat());
    normalize(DOGimage, DOGimage, 255, 0, NORM_MINMAX);    // 還原灰度范圍
    imshow("DOG image", DOGimage);
    waitKey(0);

相關API:
· 上采用 --> cv::pyrUp(Mat src, Mat dst, Size(src.cols*2, src.rows*2))
效果:生成的圖像是原圖在寬和高各放大兩倍

· 降采樣 --> cv::pyrDown(Mat src, Mat dst, Size(src.col/2, src.rows/2))
效果:生成的圖像是原圖在寬與高各縮小1/2


14、閾值操作

閾值操作:二值化、反二值化、截斷、閾值取零、閾值反取零
API–>cv::threshold(img, dst, thresh, max_value, type);
說明:type參數表示閾值操作類型,可填寫cv::THRESH_BINARY, cv::THRESH_BINARY_INV, cv::THRESH_TRUNC, cv::THRESH_TOZERO, cv::THRESH_TOZERO_TNV等。


15、邊緣填充

·OpenCV中常用的邊緣填充函數為copyMakeBorder();
函數原型:void copyMakeBorder(const Mat &src, Mat& dst, int top, int bottom, int left, int right, int borderType, const Scalar &value=Scalar());
功能:擴充src的邊緣,將圖像變大,然后以各種外插方式自動填充圖像邊界,這個函數實際上調用了函數cv::borderInterpolate,這個函數最重要的功能就是為了處理邊界,比如均值濾波或者中值濾波中,使用copyMakeBorder將原圖稍微放大,然后我們就可以處理邊界的情況。
參數說明:
src,dst:原圖與目標圖像
top,bottom,left,right分別表示在原圖四周擴充邊緣的大小
borderType:擴充邊緣的類型,就是外插的類型,OpenCV中給出以下幾種方式

  • BORDER_REPLICATE
  • BORDER_REFLECT
  • BORDER_REFLECT_101
  • BORDER_WRAP
  • BORDER_CONSTANT

BORDER_REPLICATE:邊緣像素復制法
BORDER_REFLECT_101:對稱法,以最邊緣像素為軸,對稱
BORDER_CONSTANT:常量法


16、sobel算子銳化和Laplace算子銳化

API:

cv::Sobel(img, dst, depth, dx, dy); // dx和dy分別表示x方向和y方向上的差分階數
cv::Laplacian(img, dst, depth);


17、Canny邊緣檢測算法

· Canny算法五步走:

1、高斯模糊 - GaussianBlur

2、灰度轉換 - cvtColor

3、計算梯度 - Sobel / Scharr

4、非最大信號抑制

5、高低閾值連接輸出二值圖像

什么是非最大信號抑制:

圖像梯度幅值矩陣中的元素值越大,說明圖像中該點的梯度值越大,但這不不能說明該點就是邊緣(這僅僅是屬於圖像增強的過程)。在Canny算法中,非極大值抑制是進行邊緣檢測的重要步驟,通俗意義上是指尋找像素點局部最大值,將非極大值點所對應的灰度值置為0,這樣可以剔除掉一大部分非邊緣的點。

img

根據圖可知,要進行非極大值抑制,就首先要確定像素點C的灰度值在其8值鄰域內是否為最大。圖中藍色的線條方向為C點的梯度方向,這樣就可以確定其局部的最大值肯定分布在這條線上,也即出了C點外,梯度方向的交點dTmp1和dTmp2這兩個點的值也可能會是局部最大值。因此,判斷C點灰度與這兩個點灰度大小即可判斷C點是否為其鄰域內的局部最大灰度點。如果經過判斷,C點灰度值小於這兩個點中的任一個,那就說明C點不是局部極大值,那么則可以排除C點為邊緣。這就是非極大值抑制的工作原理。

注意以下兩點:

​ 1)中非最大抑制是回答這樣一個問題:“當前的梯度值在梯度方向上是一個局部最大值嗎?” 所以,要把當前位置的梯度值與梯度方向上兩側的梯度值進行比較;

​ 2)梯度方向垂直於邊緣方向。

​ 但實際上,我們只能得到C點鄰域的8個點的值,而dTmp1和dTmp2並不在其中,要得到這兩個值就需要對該兩個點兩端的已知灰度進行線性插值,也即根據圖中的g1和g2對dTmp1進行插值,根據g3和g4對dTmp2進行插值,這要用到其梯度方向,這是Canny算法中要求解梯度方向矩陣Thita的原因。

​ 完成非極大值抑制后,會得到一個二值圖像,非邊緣的點灰度值均為0,可能為邊緣的局部灰度極大值點可設置其灰度為128。根據下文的具體測試圖像可以看出,這樣一個檢測結果還是包含了很多由噪聲及其他原因造成的假邊緣。因此還需要進一步的處理。

在這里插入圖片描述

高低閾值連接:

img

· API介紹

cv::Canny(InputArray src,      // 8bit輸入圖像
         OutputArray edges,    // 輸出邊緣圖像,一般都是二值圖像,背景為黑色
         double threshold1,    // 低閾值,常取高閾值的1/2或者1/3
         double threshold2,    // 高閾值
         int aptertureSize,    // Sobel算子size,通常為3×3,取值3
         bool L2gradient       // 選擇true表示是L2歸一化方法,否則使用L1方法
         )

 

· 相關代碼

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using std::cout;
using std::endl;

const int t1_value = 50, max_value = 255;
Mat src, dst, gray_src;
void Canny_Demo(int, void*)
{
    Mat edge_output;
    blur(gray_src, gray_src, Size(3, 3), Point(-1, -1), BORDER_DEFAULT);
    Canny(gray_src, edge_output, t1_value, t1_value*2, 3, false);
    dst.create(src.size(), src.type());
    src.copyTo(dst, edge_output);
    // imshow("output", dst);
    imshow("out", edge_output);
}
int main(int argc, char** argv)
{
    src = imread("Path");
    if(!src.data)
    {
        cout << "could not load image" << endl;
        return -1;
    }
    imshow("input", src);
    cvtColor(src, gray_src, CV_BGR2GRAY);
    createTrackbar("Threshold Value", "Result", &t1_value, max_value, Canny_Demo);
    Canny_Demo(0, 0);
    
    waitKey(0);
    return 0;
}

 

18、霍夫變換 - 直線檢測

  • 霍夫變換算法簡介:霍夫變換運用兩個坐標空間之間的變換,將在一個空間中具有相同形狀的曲線或直線映射到另一個坐標空間的一個點上形成峰值,從而把檢測任意形狀的問題轉化為統計峰值問題,在第二空間出現的峰值點通過反算方法再映射回原有空間中就得到需要檢測圖像上的像素點。

  • 算法實現前提:邊緣檢測已經完成

  • 霍夫直線檢測使用的第二空間是極坐標空間,映射圖示如下:

    1、圖像坐標系到極坐標系參數空間轉化過程

img

    說明:從上面可以看到,參數空間的每個點(ρ,θ)都對應了圖像空間的一條直線,或者說圖像空間的一個點在參 數空間中就對應為一條曲線。參數空間采用極坐標系,這樣就可以在參數空間表示原始空間中的所有直線了。     此時圖像空間(直角坐標系x-y)上的一個點對應到參數空間(極坐標系ρ-θ)上是一條曲線,確切的說是一條 正弦曲線。

 

    2、圖像空間到極坐標空間的轉換過程:

img

圖6b 圖像空間到極坐標參數空間的轉換

    這樣就把在圖像空間中檢測直線的問題轉化為在極坐標參數空間中找通過點(r,θ)的最多正弦曲線數的問題。霍 夫空間中,曲線的交點次數越多,所代表的參數越確定,畫出的圖形越飽滿。

  • 相關API介紹

    · 標准的霍夫變換cv::HoughLines從平面坐標轉換到霍夫空間,最終輸出是(θ, r)表示極坐標空間

    · 霍夫變換直線概率cv::HoughLinesP最終輸出是直線的兩個點(x0, y0, x1, y1)

    cv::HoughLines(InputArray src,      // 輸入圖像,必須是8-bit灰度圖
                   OutputArray lines,   // 輸出的極坐標來表示直線
                   double rho,          // 生成極坐標時候的像素掃描步長
                   double theta,        // 生成極坐標時候角度步長,一般取值CV_PI/180
                   int threshold,       // 閾值,只有獲得足夠交點的極坐標點才被看成是直線
                   double srn=0,        // 是否應用多尺度霍夫變換,默認值為0表示經典霍夫變換
                   double stn=0,        // 同上
                   double min_theta=0,  // 表示多角度掃描范圍0~180之間,默認即可
                   double max_theta=CV_PI
                  ) // 一般情況是有經驗的開發者使用,需要自己反變換到平面空間

     

    cv::HoughLinesP(InputArray src,
                    OutputArray lines,
                    double rho,
                    double theta,
                    int threshold,
                    double minLineLength=0, // 最小直線長度
                    double maxLineGap=0     // 最大間隔
                   ) // 常用API

     

  • 代碼演示 

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using std::cout;
using std::endl;

Mat src, src_gray, dst;
int main(int argc, char** argv)
{
    src = imread("Path");
    if(!src.data)
    {
        cout << "could not load image" << endl;
        return -1;
    }
    imshow("input", src);
    // 邊緣檢測及灰度轉換
    Canny(src, src_gray, 150, 200);
    cvtColor(src_gray, dst, CV_GRAY2BGR);
    imshow("edge", src_gray);
    // 檢測並標注直線
    vector<Vec4f> pLines;
    HoughLinesP(src_gray, pLines, 1, CV_PI/180.0, 10, 0, 10);
    Scalar color = Scalar(0, 0, 255);
    for(size_t i = 0; i < pLines.size(); i++)
    {
        Vec4f hline = pLines[i];
        line(dst, Point(hline[0], hline[1]), Point(hline[2], hline[3]), color, 3, LINE_AA);
    }
    imshow("output", dst);
    
    waitKey(0);
    return 0;
}

 


19、霍夫圓檢測

· 霍夫圓檢測原理

首先霍夫圓檢測的基本思路是認為每個非零像素點都有可能是圓上的點,與霍夫直線檢測一樣,霍夫圓檢測算法也是通過統計峰值生成積累坐標平面,設置一個累積權重來定位圓。我們知道平面坐標系中圓的方程為 (x - a)^2 + (y - b)^2 = r^2,如圖:

img

其中(a, b)是圓心,r是半徑,其函數也可以表述為:x = a + rcosθ,y = b + rsinθ,即 a = x - rcosθ,b = y - rsinθ。在xy坐標系中經過某點的圓映射到abr坐標系中,就是一條三維曲線(機器學習中的支持向量機使用過類似的思想),經過xy坐標系中所有的非零像素點的所有圓就構成了abr坐標系中很多條三維的曲線。在xy坐標系中同一個圓上的所有點的圓方程是一樣的,它們映射到abr坐標系中的是同一個點,所以在abr坐標系中該點就應該有圓的總像素N0個曲線相交。通過判斷abr中每一點的相交(累積)數量,大於一定閾值的點就認為是圓。如圖:

img

· 實現霍夫圓檢測的現實考量

1、因為霍夫圓檢測對比噪聲比較敏感,所以首先要對圖像做中值濾波。

2、基於效率考慮,OpenCV中霍夫圓檢測是基於圖像梯度實現的,分為兩步:第一步檢測邊緣,發現可能的圓心;第二步在第一步的基礎上從候選圓心開始計算最佳半徑大小。

注:霍夫梯度法的檢測思路是去遍歷累加所有非零點對應的圓心,對圓心進行考量。定位圓心的思想是“圓心一定是在圓上的每個點的模向量上,即在垂直於該點並且經過該點的切線的垂直線上,這些圓上的模向量的交點就是圓心”,霍夫梯度法就是要去查找這些圓心,根據該“圓心”上模向量相交數量的多少,根據閾值進行最終的判斷。

img

· 相關API介紹:cv::HoughCircles

cv::HoughCircles(InputArray image,  // 輸入圖像必須是8位單通道灰度圖
                OutputArray circles,
                int method,         // 方法 - HOUGH_GRADIENT
                double dp,          // dp = 1 表示在原圖中尋找
                double mindist,     // 最短距離,可以分辨是兩個圓,否則認為是同心圓
                double param1,      // canny edge detection low threshold
                double param2,      // 中心點累加器閾值 - 候選圓心
                int minradius,      // 最小半徑
                int maxradius       // 最大半徑
                )

 

· 代碼演示

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using std::cout;
using std::endl;
​
int main(int argc, char** argv)
{
    Mat src, dst;
    Mat src_gray;
    src = imread("path");
    if(!src.data)
    {
        cout << "couldn't load image ... " << endl;
        return -1;
    }
    imshow("input_image", src);
    
    medianBlur(src, src_gray, 3);
    cvtColor(src_gray, src_gray, CV_BGR2GRAY);
    // 霍夫圓檢測
    vector<Vec3f> pCircle;
    HoughCircle(src_gray, pCircle, CV_HOUGH_GRADIENT, 1, 10, 100, 30, 5, 50);
    src.copyTo(dst);
    for(size_t i = 0; i < pCircle.size(), i++)
    {
        Vec3f cc = pCircle[i];
        circle(dst, Point(cc[0], cc[1]), cc[2], Scalar(0, 0, 255), 2, LINE_AA);
        circle(dst, Point(cc[0], cc[1]), 2, Scalar(0, 255, 0), 2, LINE_AA);
    }
    imshow("output_image", dst);
    
    waitKey(0);
    return 0;
}

 


20、像素重映射

· 什么是像素重映射:簡單的說就是把輸入圖像中各個像素按照一定規律映射到另一張圖像的對應位置上去,像素重映射可以用 g(x, y) = f(h(x, y)) 表示,其中 h 表示關系函數。

· API介紹 cv::remap

cv::remap(InputArray src,
         OutputArray dst,
         InputArray map1,           // x映射表 CV_32FC1 / CV_32FC2
         InputArray map2,           // y映射表
         int interpolation,         // 選擇插值算法,常使用線性插值,也可選擇立方插值等
         int borderMode,            // BORDER_CONSTANT
         const Scalar borderValue   // color
         )

 

· 代碼演示

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using std::cout;
using std::endl;
​
int index = 0;
void update_map()
{
    for(int row = 0; row < src.rows; row++)
    {
        for(int col = 0; col < src.cols; col++)
        {
            switch(index)
            {
                case 0:
                    if(col > src.cols*0.25 && col <= src.cols*0.75 && row > src.rows*0.25 && row <= src.rows*0.75)
                    {
                        map_x.at<float>(row, col) = (col - src.cols*0.25) * 2;
                        map_y.at<float>(row, col) = (row - src.rows*0.25) * 2;
                    }
                    else
                    {
                        map_x.at<float>(row, col) = 0;
                        map_y.at<float>(row, col) = 0;
                    }
                    break;
                case 1:
                    map_x.at<float>(row, col) = src.cols - col - 1;
                    map_y.at<float>(row, col) = row;
                    break;
                case 2:
                    map_x.at<float>(row, col) = col;
                    map_y.at<float>(row, col) = src.rows - row - 1;
                    break;
                case 3:
                    map_x.at<float>(row, col) = src.cols - col - 1;
                    map_y.at<float>(row, col) = src.rows - row - 1;
                    break;
            }
        }
    }
}
int main(int argc, char** argv)
{
    Mat src, dst, map_x, map_y;
    src = imread("path");
    if(!src.data)
    {
        cout << "couldn't load image .. " << endl;
        return -1;
    }
    imshow("input_img", src);
    
    // 建立映射表
    map_x.create(src.size(), CV_32FC1);
    map_y.create(src.size(), CV_32FC1);
    int c = 0;
    while(true)
    {
        c = watKey(500);
        if(char(c) == 27)
            break;
        index = c % 4;
        update_map();
        // 像素映射
        remap(src, dst, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 255, 255));
        imshow("output_img", dst);
    }
    
    return 0;
}

 


21、直方圖均衡化

· 什么是圖像直方圖與直方圖歸一化:是指對整個圖像在灰度范圍內的像素值統計出現次數,據此統計次數生成的直方圖稱之為圖像直方圖(這個概念想必學習過圖像處理課程的朋友們應該是了如指掌)。至於直方圖歸一化,也是非常好理解的一個概念,即將單個灰度值出現的次數除以像素總數將直方圖縱坐標數值轉換到 0 - 1 之間,以此來減小數據量。由上面兩個概念我們應該注意到,我們只會對灰度圖像計算其直方圖。

· 直方圖均衡化:是一種提高圖像對比度的方法,拉伸圖像灰度值范圍,即將隨機分布的圖像直方圖修改成均勻分布的直方圖。基本思想是對原始圖像的像素灰度做某種映射變換, 使變換后圖像灰度的概率密度呈均勻分布。這就意味着圖像灰度的動態范圍得到了增加, 提高了圖像的對比度。

clip_image001

· API說明 cv::equalizeHist

cv::equalizeHist(InputArray, src,   // 輸入圖像必須是8bit灰度圖像
            OutputArray dst
            )

 

· 代碼演示

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using std::endl;
using std::cout;
​
int main()
{
    Mat src, src_gray, dst;
    src = imread("path");
    if(!src.data)
    {
        cout << "couldn't load image." << endl;
        return -1
    }
    imshow("input_img", src);
    cvtColor(src, src_gray, CV_BGR2GRAY);
    equalizeHist(src_gray, dst);
    imshow("output_img", dst);
    
    waitKey(0);
    return 0;
}

 


 

22、直方圖繪制

· API說明:cv::calcHist

void cv::calcHist(const Mat *images,     // 任意數量通道圖像組
                 int nimages,              // 圖像數量
                 const int *channels,      // 用於計算直方圖的dims通道列表
                 InputArray mask,          // 可選掩碼,如果矩陣不為空,它必須是images[]相同大小的8為數組
                 OutputArray hist,         // 輸出直方圖
                 int dims,                 // 直方圖維度,必須為正數且不能大於CV_MAX_DIMS
                 const int *histSize,      // 每個維度中的直方圖大小數組
                 const float **ranges,     // 每個維度中直方圖邊界的dims數組的數組
                 bool uniform=true,        // 表示直方圖是否均勻的標志
                 bool accumulate=false     // 累積標志,若設置為true,分配的直方圖不會被清除,用於及時更新直方圖
                 )        

 

· 代碼示例

單通道圖像直方圖繪制:

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
 
int main()
{
    Mat srcImage = imread("dog.bmp", 0);    // 直接將圖像讀取成灰度圖像
    imshow("原圖",srcImage);
    if(!srcImage.data) 
    {
        cout << "fail to load image" << endl;   
        return 0;
    }
 
    //定義變量
    Mat dstHist;       
    int dims = 1;
    float hranges[] = {0, 256};
    const float *ranges[] = { hranges };   // 這里需要為const類型
    int size = 256;
    int channels = 0;
 
    //計算圖像的直方圖
    calcHist(&srcImage, 1, &channels, Mat(), dstHist, dims, &size, ranges);   
 
    Mat dstImage(size, size, CV_8U, Scalar(0));
    //獲取最大值和最小值
    double minValue = 0;
    double maxValue = 0;
    minMaxLoc(dstHist,&minValue, &maxValue, 0, 0);  //  在cv中用的是cvGetMinMaxHistValue
 
    //繪制出直方圖
    int hpt = saturate_cast<int>(0.9 * size);
    for(int i = 0; i < 256; i++)
    {
        float binValue = dstHist.at<float>(i);           //   注意hist中是float類型   
        //拉伸到0-max
        int realValue = saturate_cast<int>(binValue * hpt/maxValue);
        line(dstImage, Point(i, size - 1), Point(i, size - realValue), Scalar(255));
    }
    imshow("單通道直方圖", dstImage);
    waitKey(0);
    return 0;
}

 

三通道圖像直方圖繪制:

#include <opencv2/opencv.hpp>  
#include <iostream>
using namespace cv;  
using namespace std;
 
int main(  )
{
    Mat srcImage;
    srcImage = imread("dog.bmp");
    imshow( "原圖", srcImage );
 
    int bins = 256;
    int hist_size[] = {bins};
    float range[] = { 0, 256 };
    const float* ranges[] = { range};
    MatND redHist,grayHist,blueHist;
 
    int channels_r[] = {0};
    //進行直方圖的計算(紅色分量部分)
    calcHist( &srcImage, 1, channels_r, Mat(), //不使用掩膜
        redHist, 1, hist_size, ranges,
        true, false );
 
    //進行直方圖的計算(綠色分量部分)
    int channels_g[] = {1};
    calcHist( &srcImage, 1, channels_g, Mat(), // do not use mask
        grayHist, 1, hist_size, ranges,
        true, // the histogram is uniform
        false );
 
    //進行直方圖的計算(藍色分量部分)
    int channels_b[] = {2};
    calcHist( &srcImage, 1, channels_b, Mat(), // do not use mask
        blueHist, 1, hist_size, ranges,
        true, // the histogram is uniform
        false );
 
    //參數准備
    double maxValue_red,maxValue_green,maxValue_blue;
    minMaxLoc(redHist, 0, &maxValue_red, 0, 0);
    minMaxLoc(grayHist, 0, &maxValue_green, 0, 0);
    minMaxLoc(blueHist, 0, &maxValue_blue, 0, 0);
    int scale = 1;
    int histHeight=256;
    Mat histImage = Mat::zeros(histHeight, bins*3, CV_8UC3);
 
    //正式開始繪制
    for(int i=0; i < bins; i++)
    {
        //參數准備
        float binValue_red    = redHist.at<float>(i); 
        float binValue_green  = grayHist.at<float>(i);
        float binValue_blue   = blueHist.at<float>(i);
        int   intensity_red   = cvRound(binValue_red*histHeight/maxValue_red);        //要繪制的高度
        int   intensity_green = cvRound(binValue_green*histHeight/maxValue_green);  //要繪制的高度
        int   intensity_blue  = cvRound(binValue_blue*histHeight/maxValue_blue);     //要繪制的高度
 
        //繪制紅色分量的直方圖
        line(histImage,Point(i,histHeight-1),Point(i, histHeight - intensity_red),CV_RGB(255,0,0));
        //繪制綠色分量的直方圖
        line(histImage,Point(i+bins,histHeight-1),Point(i+bins, histHeight - intensity_green),CV_RGB(0,255,0));
        //繪制藍色分量的直方圖
        line(histImage,Point(i+bins*2,histHeight-1),Point(i+bins*2, histHeight - intensity_blue),CV_RGB(0,0,255));
    }
    imshow( "圖像的BGR直方圖", histImage );
    waitKey(0);
return 0; }

 


 

23、直方圖比較

· 概述

對輸入的兩張圖像計算得到直方圖H1和H2,歸一化到相同的尺度空間然后可以通過計算H1與H2的之間的距離得到兩個直方圖的相似程度進而比較圖像本身的相似程度,opencv提供的比較方法有四種:相關性比較(Correlation)、卡方比較(Chi-Square)、十字交叉性(Intersection)、巴氏距離(Bhattacharyya distance)。

· 相關性計算(CV_COMP_CORREL)

img

其中

img

其中N試直方圖的BIN個數。最終計算的到的數值越接近1表示兩個圖像直方圖相關性強,相關系數的值若為正值,稱為正相關;相關系數的值若為負值,稱為負相關。如圖:

img

· 卡方計算(CV_COMP_CHISQR)

img

卡方比較和相關性比較恰恰相反,相關性比較的值為0,相似度最低,越趨近於1,相似度越低;卡方比較則是,值為0時說明H1= H2,這個時候相似度最高。

· 十字交叉計算(CV_COMP_INTERSECT)

img

· 巴氏距離計算(CV_COMP_BHATTACHARYYA)

img

巴氏距離的計算結果,其值完全匹配為1,完全不匹配則為0。一般來說,在直方圖相似度計算時,巴氏距離獲得的效果最好,但計算是最為復雜的。

· 直方圖比較流程

1、首先把圖像從BGR色彩空間轉換到HSV色彩空間:cv::cvtColor

2、計算圖像直方圖並歸一化:cv::calcHistcv::normalize

3、選擇使用上述四種方法進行比較:cv::compareHist

· API介紹

double cv::compareHist(InputArray h1,   // 直方圖數據
                      InputArray h2,
                      int method        // 比較方法
                      )

 

· 代碼演示

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
​
​
string convertToString(double d) 
{
    ostringstream os;
    if (os << d)
        return os.str();
    return "invalid conversion";
}
int main(int argc, char** argv) 
{
    Mat base, test1, test2;
    Mat hsvbase, hsvtest1, hsvtest2;
    base = imread(image_path_1);
    if (!base.data) 
    {
        printf("could not load image...\n");
        return -1;
    }
    test1 = imread(image_path_2);
    test2 = imread(iamge_path_3);
​
    //從RGB空間轉換到HSV空間
    cvtColor(base, hsvbase, CV_BGR2HSV);
    cvtColor(test1, hsvtest1, CV_BGR2HSV);
    cvtColor(test2, hsvtest2, CV_BGR2HSV);
​
    //計算直方圖並歸一化
    int h_bins = 50; 
    int s_bins = 60;
    int histSize[] = { h_bins, s_bins };
    // hue varies from 0 to 179, saturation from 0 to 255     
    float h_ranges[] = { 0, 180 };
    float s_ranges[] = { 0, 256 };
    const float* ranges[] = { h_ranges, s_ranges };
    // Use the o-th and 1-st channels     
    int channels[] = { 0, 1 };
    MatND hist_base;
    MatND hist_test1;
    MatND hist_test2;
​
    calcHist(&hsvbase, 1, channels, Mat(), hist_base, 2, histSize, ranges, true, false);
    normalize(hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat());
​
    calcHist(&hsvtest1, 1, channels, Mat(), hist_test1, 2, histSize, ranges, true, false);
    normalize(hist_test1, hist_test1, 0, 1, NORM_MINMAX, -1, Mat());
​
    calcHist(&hsvtest2, 1, channels, Mat(), hist_test2, 2, histSize, ranges, true, false);
    normalize(hist_test2, hist_test2, 0, 1, NORM_MINMAX, -1, Mat());
​
    //比較直方圖,並返回值
    double basebase = compareHist(hist_base, hist_base, CV_COMP_INTERSECT);
    double basetest1 = compareHist(hist_base, hist_test1, CV_COMP_INTERSECT);
    double basetest2 = compareHist(hist_base, hist_test2, CV_COMP_INTERSECT);
    double tes1test2 = compareHist(hist_test1, hist_test2, CV_COMP_INTERSECT);
​
    Mat test12;
    test2.copyTo(test12);
    putText(base, convertToString(basebase), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
    putText(test1, convertToString(basetest1), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
    putText(test2, convertToString(basetest2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
    putText(test12, convertToString(tes1test2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
​
    namedWindow("base", CV_WINDOW_AUTOSIZE);
    namedWindow("test1", CV_WINDOW_AUTOSIZE);
    namedWindow("test2", CV_WINDOW_AUTOSIZE);
​
    imshow("base", base);
    imshow("test1", test1);
    imshow("test2", test2);
    imshow("test12", test12);
​
    waitKey(0);
    return 0;
}
​

 


 

24、直方圖反向投影

· 概念介紹

反向投影是反應直方圖模型在目標圖像中的分布情況,簡單點來說就是用直方圖模型去目標圖像中尋找是否有相似的對象,通常使用HSV色彩空間的HS兩個通道直方圖模型。一般情況下,我們可以通過反向投影來實現圖像分割、背景與對象分離、對已知對象位置進行定位。反向投影在模式識別、對象識別、視頻跟蹤中均有應用。

· 反向投影實現步驟

1、建立直方圖模型

2、計算待檢測圖像直方圖並映射到模型中

3、從模型反向計算生成圖像

· 相關API說明

1、數據結構補充說明:cv::MatND,該數據結構與cv::Mat的差異在於cv::MatND表示三維或多維數據,而在下面將要展示的代碼中,cv::MatND都可以用cv::Mat代替

2、計算反向投影圖像:cv::calcBackProject

// cv::calcBackProject()為重載方法,共有三種形式,根據傳入參數不同會選擇不同的調用
void cv::calcBackProject(const Mat *images,         // 輸入圖像,圖像深度必須為CV_8U、CV_16U或CV_32F中的一種,通道數不限
                        int nimages,                // 輸入圖像數量
                        const int *channels,        // 用於計算反向投影的通道列表,通道數必須與直方圖維度相匹配
                        InputArray hist,            // 輸入直方圖,直方圖的bin可以是密集或者稀疏
                        OutputArray backProject,    // 目標反向投影輸出圖像,單通道圖像,尺寸和深度與原圖像相同
                        const float **ranges,       // 直方圖中每個維度bin的取值范圍
                        double scale=1,             // 可選輸出反向投影的比例因子
                        bool uniform=true           // 直方圖是否均勻分布的標識符
                        )

· 代碼展示

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
​
Mat src, hsv, hue;
int bins = 12;
void hist_and_backProjecttion(int, void*)
{
    int range[] = { 0, 180 };
    const float *histRanges = { range };
    Mat h_hist;
    calcHist(&hue, 1, 0, Mat(), h_hist, 1, &bins, &histRanges, true, false);
    normalize(h_hist, h_hist, 0, 255, NORM_MINMAX, -1, Mat());
    Mat backPrjImage;
    calcBackProject(&hue, 1, 0, h_hist, backPrjImage, &histRanges);
    int hist_h = 400;
    int hist_w = 400;
    int bin_w  = hist_w / bins;
    Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));
    for(int i = 1; i < bins; i++)
    {
        rectangle(histImage, 
                  Point((i - 1) * bin_w, hist_h - cvRound(h_hist.at<float>(i) * (400 / 255))), 
                  Point(i * bin_w, hist_h), 
                  Scalar(0, 0, 255), -1);
    }
    imshow("BackProjection", backPrjImage);
    imshow("Histogram", histImage);
}
int main()
{
    src = imread("path");
    if(src.empty())
    {
        cout << "done" << endl;
        return -1;
    }
    cvtColor(src, hsv, CV_BGR2HSV);
    hue.create(hsv.size(), hsv.depth());
    int nchannels[] = { 0, 0 };
    mixChannels(&hsv, 1, &hue, 1, nchannels, 1);
    
    createTrackbar("Histogram bins:", "原圖", &bins, 180, hist_and_backProjection);
    hist_and_backProjection(0, 0);
    
    imshow("原圖", src);
    waitKey(0);
    return 0;
}

 


 

25、模板匹配

· 模板匹配概念

簡單來說就是使用一個模板對一幅圖像做“偽卷積”,這里我所使用的“偽卷積”概念是在使用模板對圖像進行遍歷的過程中,不進行卷積運算,而只是去匹配(計算)模板與重疊的子圖像是否滿足一種相似關系。模板匹配是在一幅圖像中找尋一個特定目標的方法之一,在匹配過程中,當圖像某塊子圖像與模板相似度高,則我們認為找到了目標。

· 幾種常見的相似度計算方法

1、差值平方和匹配(CV_TM_SQDIFF)

這類方法利用圖像與模板各個像素差值的平方和來進行匹配,最好匹配的計算結果為0,匹配越差,計算結果越大。

2、相關匹配_1(CV_TM_CCORR)

這類方法采用模板和圖像的相關計算作為相似度的度量方法,所以較大的數表示匹配程度較高,0表示最壞匹配效果。

3、標准化差值平方和匹配(CV_TM_SQDIFF_NORMED)

該方法類似於CV_TM_SQDIFF,只不過對其結果進行了標准化操作,這種標准化操作可以保證當模板和圖像各個像素的亮度都乘上了同一個系數時,相關度不發生變化,也就是說當 I(x,y)和T(x,y) 變為kI(x,y)和kT(x,y)時,R(x,y)不發生變化。

4、標准相關匹配_1(CV_TM_CCORR_NORMED)

與上述類似,增加標准化計算都是去除亮度線性變化對相似度計算的影響,保證圖像和模板同時變亮或者變暗相同倍數時結果不變。

5、相關匹配_2(CV_TM_CCOEFF)

 

這種方法也叫做相關匹配,但是和上面的 CV_TM_CCORR 匹配方法還是有不同的。簡單的說,這里是把圖像和模板都減去了各自的平均值,使得這兩幅圖像都沒有直流分量。

6、標准相關匹配_2(CV_TM_CCOEFF_NORMED)

這是 OpenCV 支持的最復雜的一種相似度算法。這里的相關運算就是數理統計學科的相關系數計算方法。具體的說,就是在減去了各自的平均值之外,還要各自除以各自的方差。經過減去平均值和除以方差這么兩步操作之后,無論是我們的待檢圖像還是模板都被標准化了,這樣可以保證圖像和模板分別改變光照亮不影響計算結果。計算出的相關系數被限制在了 -1 到 1 之間,1 表示完全相同,-1 表示兩幅圖像的亮度正好相反,0 表示兩幅圖像之間沒有線性關系。

· API介紹

void cv::matchTemplate(InputArray image,        // 輸入圖像,必須是8位或者32位浮點類型
                      InputArray templ,         // 模板,不可大於image,並具有相同類型
                      OutputArray result,       // 比較結果的映射,必須是單通道32位浮點
                      int method,               // 相似度計算方法
                      InputArray mask=noArray() // 搜索模板的掩碼,必須與templ有相同的數據類型和尺寸,默認情況下不設置
                      )

· 代碼演示

#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
 
Mat src, templ, result;
char* src_window = "原圖";
char* result_window = "結果";
int match_method;
int max_Trackbar = 5;
​
void MatchingMethod(int, void*)
{
    // 將被顯示的原圖像
    Mat src_display;
    src.copyTo(src_display);
 
    // 創建輸出結果的矩陣
    int result_cols = src.cols - templ.cols + 1;
    int result_rows = src.rows - templ.rows + 1;
    result.create(result_cols, result_rows, CV_32FC1);
    // 進行匹配和標准化
    matchTemplate(src, templ, result, match_method);
    normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());
    // 通過函數 minMaxLoc 定位最匹配的位置
    double minVal; double maxVal; Point minLoc; Point maxLoc;
    Point matchLoc;
    minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());
 
    // 對於方法 SQDIFF 和 SQDIFF_NORMED, 越小的數值代表更高的匹配結果. 而對於其他方法, 數值越大匹配越好
    if (match_method == CV_TM_SQDIFF || match_method == CV_TM_SQDIFF_NORMED)
        matchLoc = minLoc;
    else
        matchLoc = maxLoc;
 
    rectangle(src_display, matchLoc, Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), Scalar(0,0,255), 2, 8, 0);
    rectangle(result, matchLoc, Point(matchLoc.x + templ.cols, matchLoc.y + templ.rows), Scalar(0, 0, 255), 2, 8, 0);
    
    imshow(src_window, src_display);
    imshow(result_window, result);
}
 
int main()
{   
    src = imread("path");
    templ = imread("path");
    if (!src.data || !templ.data)
    {
        cout << "could not load image !";
        return -1;
    }
    namedWindow(src_window, CV_WINDOW_AUTOSIZE);
    namedWindow(result_window, CV_WINDOW_AUTOSIZE);
    imshow(src_window, src);
 
    // 創建滑動條
    char* trackbar_label = "Method";
    createTrackbar(trackbar_label, src_window, &match_method, max_Trackbar, MatchingMethod);
    MatchingMethod(0, 0);
 
    waitKey(0);
    return 0;
}

 


 

26、輪廓發現

· 概念介紹

輪廓發現是基於圖像邊緣提取的基礎尋找對象輪廓的方法,所以邊緣提取的閾值選定會影響最終輪廓發現結果。其中我們所說的輪廓是一系列相連的點組成的曲線,代表了物體的基本外形,相對於邊緣,輪廓是連續的,邊緣並不全部連續。如圖:

· API介紹

1、發現輪廓:cv::findContours

void cv::findContours(InputOutputArray binImg,       // 輸入圖像,非0像素被看成1,0像素保持不變,8-bit
                     OutputArrayOfArrays contours,   // 所發現的全部輪廓對象
                     OutputArray hierachy,           // 該圖的拓撲結構,可選,該輪廓發現算法正是基於圖像拓撲結構實現
                     int mode,                       // 輪廓返回的模式
                     int method,                     // 發現方法
                     Point offset=Point()            // 輪廓像素的位移默認(0,0)
                     )
/*
mode參數:輪廓檢索模式,可以通過cv::RetrievalModes()查看詳細信息
1、RETR_EXTERNAL:表示只檢測最外層輪廓,對所有輪廓設置hierarchy[i][2]=hierarchy[i][3]=-1
2、RETR_LIST:提取所有輪廓,並放置在list中,檢測的輪廓不建立等級關系
3、RETR_CCOMP:提取所有輪廓,並將輪廓組織成雙層結構(two-level hierarchy),頂層為連通域的外圍邊界,次層位內層邊界
4、RETR_TREE:提取所有輪廓並重新建立網狀輪廓結構
5、RETR_FLOODFILL:官網沒有介紹,應該是洪水填充法
​
method參數:輪廓近似方法可以通過cv::ContourApproximationModes()查看詳細信息
1、CHAIN_APPROX_NONE:獲取每個輪廓的每個像素,相鄰的兩個點的像素位置差不超過1
2、CHAIN_APPROX_SIMPLE:壓縮水平方向,垂直方向,對角線方向的元素,值保留該方向的重點坐標,如果一個矩形輪廓只需4個點來保存輪廓信息
3、CHAIN_APPROX_TC89_L1和CHAIN_APPROX_TC89_KCOS使用Teh-Chinl鏈逼近算法中的一種
*/

 

2、輪廓繪制:cv::drawContours

// 繪制輪廓,一次只能根據輪廓索引號繪制一次,需要循環所有發現的輪廓
void cv::drawContours(InputOutputArray binImg,        // 輸出圖像
                    OutputArrayOfArrays contours,     // 全部發現的輪廓對象,包含points的vectors的vector
                    int contourIdx,                   // 輪廓索引號
                    const Scalar &color,              // 繪制時候顏色
                    int thickness,                    // 繪制線寬,如果傳-1表示填充輪廓
                    int lineType,                     // 線的類型LINE_8
                    InputArray hierarchy,             // 拓撲結構圖
                    int maxlevel,                     // 最大層數,0只繪制當前的,1表示繪制繪制當前及其內嵌的輪廓
                    Point offset=Point()              // 輪廓位移,可選
                    )

 

· 實現步驟

1、輸入圖像轉化為灰度圖像

2、使用Canny進行邊緣提取,得到二值圖像

3、使用findContours尋找輪廓

4、使用drawContours繪制輪廓

· 代碼展示

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
 
Mat src, src_gray;
int thresh = 100;
int max_thresh = 255;
RNG rng(12345);
​
void thresh_callback(int, void*)
{
    Mat canny_output;
    vector<vector<Point>> contours;     // 每個輪廓由一系列點組成
    vector<Vec4i> hierarchy;
 
    // 用Canny算子檢測邊緣
    Canny(src_gray, canny_output, thresh, thresh*2, 3);
    // 尋找輪廓
    findContours(canny_output, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));
 
    // 發現輪廓
    // 因為要用不同顏色區分輪廓,所以這里用的CV_8UC3 3通道彩色圖,如果是CV_8UC1 繪制的就是灰度圖,感官上不好區分
    Mat drawing = Mat::zeros(canny_output.size(), CV_8UC3);
    for(int i = 0; i< contours.size(); i++)
    {
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255));     // 隨機顏色
        drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());     // 顏色一致的就屬同一發現的輪廓
    }
 
    // 在窗體中顯示結果
    namedWindow("Contours", CV_WINDOW_AUTOSIZE);
    imshow("Contours", drawing);
}
 
int main(int argc, char** argv)
{
    src = imread("path");
    // 轉成灰度並模糊化降噪
    cvtColor(src, src_gray, CV_BGR2GRAY);
    blur(src_gray, src_gray, Size(3,3));
    // 創建窗體
    char* source_window = "Source";
    namedWindow(source_window, CV_WINDOW_AUTOSIZE);
    imshow(source_window, src);
    createTrackbar("Canny thresh:", "Source", &thresh, max_thresh, thresh_callback);
    thresh_callback(0, 0);
    
    waitKey(0);
    return 0;
}
​

 


 

27、凸包

· 概念介紹

凸包(Convex Hull)是一個計算幾何(圖形學)中的概念,它的嚴格的數學定義為:在一個向量空間V中,對於給定集合X,所有包含X的凸集的交集S被稱為X的凸包。簡單來講包含點集合S中所有點的最小凸多邊形稱為凸包。如圖:

· Graham掃描算法

Graham掃描法通過不斷在凸殼中加入新的點和去除影響凸性的點,最后形成凸包。算法的主體由兩部分組成,先是排序,然后掃描。

1、首先選擇Y方向最低點作為起始點p0

2、從p0開始極坐標掃描,依次添加p1...pn(排列順序是根據極坐標的角度大小,逆時針方向排列)

3、對每個點pi來說,如果添加pi點到凸包中導致一個左轉向(逆時針方法)則添加該點到凸包集合中,反之如果導致右轉向(順時針方向)則從凸包中刪除該點

如圖:

· API介紹

void cv::convexHull(InputArray points,      // 輸入的二維點集,Mat類型數據即可
                   OutputArray hull,        // 輸出參數,用於輸出函數調用后找到的凸包
                   bool clockwise=false,    // 操作方向,當標識符為真時,輸出凸包為順時針方向,否則為逆時針方向
                   bool returnPoints=true   // 操作標識符,默認為true,此時返回各凸包的點,否則返回凸包各點的指數,當輸出數組為std::vector時,此標識被忽略
                   )

· 實現步驟

1、把圖像從RGB圖像轉到灰度圖,再由灰度圖轉換到二值圖

2、通過輪廓發現方法找到候選點集合

3、使用cv::convexHull尋找凸包

4、繪制顯示

· 代碼演示

#include <iostream>
#include <opencv2/opencv.hpp>using namespace std;
using namespace cv;
​
Mat srcImage, grayImage;
int thresh = 100;
const int threshMaxValue = 255;
RNG rng(12345);
​
//定義回調函數
void thresh_callback(int, void*)
{
    Mat src_copy = srcImage.clone();
    Mat threshold_output;
    vector<vector<Point>>contours;
    vector<Vec4i>hierarchy;
    //使用Threshold檢測圖像邊緣
    threshold(grayImage, threshold_output, thresh, 255, THRESH_BINARY);
    //尋找圖像輪廓
    findContours(threshold_output, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
​
    //尋找圖像凸包
    vector<vector<Point>>hull(contours.size());
    for (int i = 0; i < contours.size(); i++)
        convexHull(Mat(contours[i]), hull[i], false);
​
    //繪制輪廓和凸包
    Mat drawing = Mat::zeros(threshold_output.size(), CV_8UC3);
    for (int i = 0; i < contours.size(); i++)
    {
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
        drawContours(drawing, contours, i, color, 1, 8, vector<Vec4i>(), 0, Point());
        drawContours(drawing, hull, i, color, 1, 8, vector<Vec4i>(), 0, Point());
    }
​
    namedWindow("凸包", WINDOW_AUTOSIZE);
    imshow("凸包", drawing);
}
​
​
int main()
{
    srcImage = imread("path");
    if (srcImage.empty())
    {
        cout << "圖像加載失敗" << endl;
        return -1;
    }
    //圖像灰度圖轉化並平滑濾波
    cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);
    blur(grayImage, grayImage, Size(3, 3));
    namedWindow("原圖像", WINDOW_AUTOSIZE);
    imshow("原圖像", grayImage);
​
    //創建軌跡條
    createTrackbar("Threshold:", "原圖像", &thresh, threshMaxValue, thresh_callback);
    thresh_callback(thresh, 0);
    
    waitKey(0);
    return 0;
}
 

28、輪廓周圍繪制矩形框和圓形框

· 相關API介紹

// cv::approxPolyDP作用是光滑曲線折線化,基於RDP算法
void cv::approxPolyDP(InputArray curve,         // 輸入輪廓曲線,數據類型vector<Point>或者Mat
                     OutputArray approxCurve,   // 輸出的折線,數據類型同上
                     double epsilon,            // 判斷點到相對應的line segment的距離的閾值,距離大於閾值則舍棄,小於則保留
                     bool closed                // 封閉曲線標識位
                     )
// 得到輪廓周圍最小矩形左上角和右下角點的坐標,繪制一個矩形
Rect cv::boundingRect(InputArray points)
    
// 得到旋轉的矩形,返回旋轉的矩形
RotatedRect cv::minAreaRect(InputArray points)
    
// 得到包含二維點集的最小圓
void cv::minEnclosingCircle(InputArray points,
                           Point2f &center,     // 圓心位置
                           float &radius,       // 半徑
                           )
​
// 繪制最小橢圓
RotatedRect cv::fitEllipse(InputArray points)

· 實現步驟

1、圖像二值化

2、發現輪廓

3、使用相關API在輪廓點上找到最小包含矩形和圓、旋轉矩形和橢圓

4、繪制

· 代碼展示

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace std;
using namespace cv;
​
Mat src, gray_src, drawImg;
int threshold_v   = 170;
int threshold_max = 255;
const char* output_win = "rectangle-demo";
RNG rng(12345);
// 回調函數
void Contours_Callback(int, void*);
​
int main(int argc, char** argv)
{
    src = imread("path");
    if (!src.data)
    {
        printf("could not load image...\n");
        return -1;
    }
    cvtColor(src, gray_src, CV_BGR2GRAY);
    blur(gray_src, gray_src, Size(3, 3), Point(-1, -1));
    
    const char* source_win = "input image";
    namedWindow(source_win, CV_WINDOW_AUTOSIZE);
    namedWindow(output_win, CV_WINDOW_AUTOSIZE);
    imshow(source_win, src);
​
    createTrackbar("Threshold Value:", output_win, &threshold_v, threshold_max, Contours_Callback);
    Contours_Callback(0, 0);
​
    waitKey(0);
    return 0;
}
​
void Contours_Callback(int, void*) 
{
    Mat binary_output;
    vector<vector<Point>> contours;
    vector<Vec4i> hierachy;
    threshold(gray_src, binary_output, threshold_v, threshold_max, THRESH_BINARY);
    findContours(binary_output, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(-1, -1));
​
    vector<vector<Point>> contours_ploy(contours.size());
    vector<Rect> ploy_rects(contours.size());
    vector<Point2f> ccs(contours.size());
    vector<float> radius(contours.size());
​
    vector<RotatedRect> minRects(contours.size());
    vector<RotatedRect> myellipse(contours.size());
​
    for (size_t i = 0; i < contours.size(); i++) 
    {
        approxPolyDP(Mat(contours[i]), contours_ploy[i], 3, true);
        ploy_rects[i] = boundingRect(contours_ploy[i]);
        minEnclosingCircle(contours_ploy[i], ccs[i], radius[i]);
        if (contours_ploy[i].size() > 5) 
        {
            myellipse[i] = fitEllipse(contours_ploy[i]);
            minRects[i] = minAreaRect(contours_ploy[i]);
        }
    }
    // draw it
    drawImg = Mat::zeros(src.size(), src.type());
    Point2f pts[4];
    for (size_t t = 0; t < contours.size(); t++) 
    {
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
        if (contours_ploy[t].size() > 5)
        {
            ellipse(drawImg, myellipse[t], color, 1, 8);
            minRects[t].points(pts);
            for (int r = 0; r < 4; r++)
                line(drawImg, pts[r], pts[(r + 1) % 4], color, 1, 8);
        }
    }
    imshow(output_win, drawImg);
}

 29、圖形矩

· 矩概念介紹

1、幾何矩

· 空間矩: M j i = ∑ x , y ( P ( x , y ) ⋅ x j ⋅ y i ) ,其中(i + j)和等於幾就叫做幾階距

· 中心矩:m u j i = ∑ x , y ( P ( x , y ) ⋅ ( x − x ˉ ) j ⋅ ( y − y ˉ ) i ) ,其中x ˉ , y ˉ 表示它的中心質點

· 中心歸一化矩:

2、圖像中心Center(x0, y0)

在OpenCV中,還可以很方便的得到Hu不變距,Hu不變矩在圖像旋轉、縮放、平移等操作后,仍能保持矩的不變性,所以有時候用Hu不變距更能識別圖像的特征。Hu不變矩參考:[http://www.cnblogs.com/skyseraph/archive/2011/07/19/2110183.html]:

Hu不變矩主要是利用歸一化中心矩構造七個不變特征矩:

image

· API介紹

// cv::moments計算圖像連通域的幾何矩和中心距以及歸一化的幾何矩
Moments cv::moments(InputArray array,       //輸入數據
                   bool binaryImage=false   // 二值標識
                   )
/*
 class Moments
 { 
 public: ...... // 空間矩 double m00, m10, m01, m20, m11, m02, m30, m21, m12, m03; 
                // 中心矩 double mu20, mu11, mu02, mu30, mu21, mu12, mu03;
                // 中心歸一化矩 double nu20, nu11, nu02, nu30, nu21, nu12, nu03;  
 }
 */

moments計算生成數據:

img

// cv::contourArea使用格林公式計算輪廓面積
double cv::contourArea(InputArray contour,  // 輸入向量,二維點,可以是vector或者Mat類型
                      bool oriented=false   // 面向區域標識符,如果為true則函數返回一個帶符號的面積
                      )
/*官方文檔調用示例
vector<Point> contour;
contour.push_back(Point2f(0, 0));
contour.push_back(Point2f(10, 0));
contour.push_back(Point2f(10, 10));
contour.push_back(Point2f(5, 4));
double area0 = contourArea(contour);
vector<Point> approx;
approxPolyDP(contour, approx, 5, true);
double area1 = contourArea(approx);
cout << "area0 =" << area0 << endl <<
        "area1 =" << area1 << endl <<
        "approx poly vertices" << approx.size() << endl;
*/
// cv::arcLength用於計算封閉輪廓的周長和曲線的長度
double cv::arcLength(InputArray curve,  // 輸入二維點集,vector或者Mat對象
                    bool closed         // 封閉曲線標志位,true表示封閉
                    )

· 實現步驟

1、提取圖像邊緣

2、發現輪廓

3、計算每個輪廓對象的矩

4、計算每個對象的中心、弧長、面積

· 代碼展示

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
​
//定義全局變量
Mat srcImage, grayImage;
int thresh = 100;
const int threshMaxValue = 255;
RNG rng(12345);
​
//聲明回調函數
void thresh_callback(int, void*);
int main()
{
    srcImage = imread("image_path");
    if (!srcImage.data)
    {
        cout << "圖像加載失敗...";
        return -1;
    }
​
    namedWindow("原圖像", WINDOW_AUTOSIZE);
    imshow("原圖像", srcImage);
​
    //圖像轉化為灰度圖並平滑
    cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);
    blur(grayImage, grayImage, Size(3, 3));
​
    namedWindow("灰度圖", WINDOW_AUTOSIZE);
    imshow("灰度圖", grayImage);
​
    //創建軌跡條
    createTrackbar("Thresh:", "灰度圖", &thresh, threshMaxValue, thresh_callback);
    thresh_callback(thresh, 0);
    waitKey(0);
​
    return 0;
}
​
void thresh_callback(int, void*)
{
    Mat canny_output;
    vector<vector<Point>>contours;
    vector<Vec4i>hierarchy;
​
    //canny邊緣檢測
    Canny(grayImage, canny_output, thresh, thresh * 2, 3);
    //輪廓提取
    findContours(canny_output, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
​
    //計算圖像矩
    vector<Moments> mu(contours.size());
    for (int i = 0; i < contours.size(); i++)
        mu[i] = moments(contours[i], false);
​
    //計算圖像的質心
    vector<Point2f>mc(contours.size());
    for (int i = 0; i < contours.size(); i++)
        mc[i] = Point2f(mu[i].m10 / mu[i].m00, mu[i].m01 / mu[i].m00);
​
    //繪制輪廓
    Mat drawing = Mat::zeros(canny_output.size(), CV_8UC3);
    for (int i = 0; i < contours.size(); i++)
    {
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
        drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());
        circle(drawing, mc[i], 4, color, -1, 8, 0);
    }
​
    namedWindow("輪廓圖", WINDOW_AUTOSIZE);
    imshow("輪廓圖", drawing);
​
    //用moments矩集計算輪廓面積並與opencv函數計算結果進行比較
    printf("\t Info: Area and Contour Length \n");
    for (int i = 0; i < contours.size(); i++)
    {
        printf("* Contour[%d] - Area(M_00)=%.2f-Area OpenCV:%.2f - Length:%.2f\n", i, mu[i].m00, contourArea(contours[i]), arcLength(contours[i], true));
        Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
        drawContours(drawing, contours, i, color, 2, 8, hierarchy, 0, Point());
        circle(drawing, mc[i], 4, color, -1, 8, 0);
    }
}
​

 


 

30、點多邊形測試

· 概念介紹

點多邊形測試指的是測試一個點是否在給定的多變形內部、邊緣或者外部(多邊形外部標記為-1,多邊形邊緣標記為0,多邊形內部標記為1)。根據相應計算還可以得到各點與多邊形中心的距離。

· API介紹

1、cv::pointPolygonTest

double cv::pointPolygonTest(InputArray contour,     // 輸入的輪廓
                           Point2f pt,              // 測試點
                           bool measureDist         // 是否返回距離值,如果是false則返回標識,即內部為1、外部-1、邊緣0;如果為true,則返回實際距離
                           )

注意:判斷點是否在多邊形內部的算法詳解可參考[https://zhuanlan.zhihu.com/p/150202146]:

2、尋找圖像最大最小坐標值:cv::minMaxLoc

在這里插入圖片描述

· 實現步驟

1、發現輪廓

2、對圖像中所有像素點做點多邊形測試,得到距離后歸一化顯示

· 代碼展示

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;

int main()
{
    const int r = 100;
    Mat src = Mat::zeros(r * 4, r * 4, CV_8UC1);
    // 創建一個數組存儲六邊形的角點
    vector<Point2f> vert(6); 
    vert[0] = Point(3 * r / 2, static_cast<int>(1.34 * r));  // static_cast: 強制類型轉換
    vert[1] = Point(1 * r, 2 * r);
    vert[2] = Point(3 * r / 2, static_cast<int>(2.866 * r));
    vert[3] = Point(5 * r / 2, static_cast<int>(2.866 * r));
    vert[4] = Point(3 * r , 2 * r);
    vert[5] = Point(5 * r / 2, static_cast<int>(1.34 * r));
    // 繪制多邊形  注意畫閉合曲線的方法:最后一個點與第一個點連接起來的方法
    // 單通道圖像:Scalar()函數只能傳入一個參數
    for (size_t i = 0; i < 6; i++)
        line(src, vert[i], vert[(i + 1) % 6], Scalar(255), 2, LINE_8);
    const char* output_win = "point polygon test demo";
    const char* input_win = "input image";

    namedWindow(input_win, WINDOW_AUTOSIZE);
    namedWindow(output_win, WINDOW_AUTOSIZE);
    imshow(input_win, src);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;  // hierarchy: 層次
    Mat csrc;
    src.copyTo(csrc);
    // 尋找輪廓
    findContours(csrc, contours, RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));  // 返回樹形結構,采用...方法

    Mat raw_dist = Mat::zeros(csrc.size(), CV_32FC1);
    for (int row = 0; row < csrc.rows; row++)
    {
        for (int col = 0; col < csrc.cols; col++)
        {
            // 只有一個輪廓,無須重復計算  傳入參數必須類型匹配
            double dist = pointPolygonTest(contours[0],Point2f(static_cast<float>(col), static_cast<float>(row)), true);    // 返回計算距離
            raw_dist.at<float>(row, col) = static_cast<float>(dist);     // 距離值有正有負 值可能大於255(double類型)
        }
    }
    double minValue, maxValue;
    // 尋找圖像上最大最小值像素   
    // 注意: 最大值為正值(多邊形內部),最小值為負值(多邊形外部)   
    minMaxLoc(raw_dist, &minValue, &maxValue, 0, 0, Mat());
    Mat drawImg = Mat::zeros(src.size(),CV_8UC3);
    // 根據計算距離,歸一化處理數據再顯示
    for (int row = 0; row < drawImg.rows; row++)
    {
        for (int col = 0; col < drawImg.cols; col++)
        {
            float dist = raw_dist.at<float>(row, col);
            if (dist > 0)   // 多邊形內部
            {
                // 多邊形內部顯示 blue
                // 這樣越靠近多邊形中心越藍
                // drawImg.at<Vec3b>(row, col)[0] = (uchar)(abs(dist / maxValue) * 255);
                // 像素值轉換到0-255之間;blue
                drawImg.at<Vec3b>(row, col)[0] = (uchar)(abs(1.0 - dist / maxValue) * 255);
            }
            else if(dist < 0)
            {
                // 多邊形外部顯示 red  minValue 為負值
                // 這樣越遠離邊界越紅
                // drawImg.at<Vec3b>(row, col)[2] = (uchar)(abs(dist / minValue) * 255);
                // 像素值轉換到0-255之間;red
                drawImg.at<Vec3b>(row, col)[2] = (uchar)(abs(1.0 - dist / minValue) * 255);
            }
            else
            {
                drawImg.at<Vec3b>(row, col)[0] = (uchar)(abs(255 - dist));  // 白色
                drawImg.at<Vec3b>(row, col)[1] = (uchar)(abs(255 - dist));  // 白色
                drawImg.at<Vec3b>(row, col)[2] = (uchar)(abs(255-dist));      // 白色
            }
        }
    }
    imshow(output_win, drawImg);

    waitKey(0);
    return 0;
}

 


 

31、基於距離變換和分水嶺的圖像分割

· 圖像分割概念介紹

圖像分割是圖像處理最重要的處理手段之一,圖像分割的目的是將圖像中像素根據一定的規則分成若干個Cluster集合,每個集合包含一類像素。根據算法分為監督學習方法和無監督學習方法,圖像分割的算法大多是無監督學習方法,如聚類。

· 距離變換概念介紹

1、什么是距離變換:計算圖像中像素點到最近零像素點的距離,也就是零像素點的最短距離。首先,對圖像進行二值化處理,然后將每個像素賦值為離它最近的背景像素點的距離(這里計算的距離是曼哈頓距離或者歐氏距離),得到距離矩陣,那么離邊界越遠的點就越亮。如圖

img

2、距離變換的常用應用:細化輪廓、尋找質心。

· 分水嶺算法

分水嶺算法基本思想:分水嶺算法基於浸泡理論實現,假設圖像每個位置的像素為不同的地貌,則勢必會形成山峰和山谷,在山底不停加水,直到各個山頭之間形成明顯的分水線,達到分割圖像的目的。傳統分水嶺算法示意圖如下

img

分水嶺算法詳細解釋參考:[https://blog.csdn.net/iracer/article/details/49225823]:

· API介紹

// 計算圖像中每一個非零點距離離自己最近的零點的距離
void cv::distanceTransform(InputArray src,      // 輸入圖像,一般為二值圖像
                          OutputArray dst,      // 輸出圖像
                          int distanceType,     // 求解距離類型
                          int maskSize,         // 距離變換掩膜大小可以是3或5。對CV_DIST_L1或CV_DIST_C的情況,參數值被強制設定為3,因為3×3 mask給出5×5 mask一樣的結果,而且速度還更快
                          int labelType         // 標簽類型
                          )

求解距離類型參數可選項:

img

標簽類型可選項:

img

void cv::watershed(InputArray image,        // 輸入圖像,8位三通道彩色圖像
                  InputOutputArray markers  // 函數返回的結果,輸入\輸出32位單通道圖像的標記結果,即這個參數用於存放函數調用后的輸出結果,需和源圖像有一樣的尺寸
                  )

· 實現步驟

  1. 將白色背景變成黑色 - 目的是為后面的變換做准備

  2. 使用filter2D與拉普拉斯算子實現圖像對比度提高

  3. 轉為二值圖像 - threshold

  4. 距離變換

  5. 對距離變換結果進行歸一化到 [0~1] 之間

  6. 使用閾值,再次二值化,得到標記

  7. 腐蝕得到每個Peak - erode

  8. 發現輪廓 – findContours

  9. 繪制輪廓 - drawContours

  10. 分水嶺變換 - watershed

  11. 對每個分割區域着色輸出結果

· 代碼演示

#include<opencv2/opencv.hpp>
#include<iostream>
#include<math.h>
using namespace std;
using namespace cv;
 
int main()
{
    Mat src;
    src = imread("image_path");
    if (!src.data)
    {
        printf("could not load image...\n");
        return -1;
    }
    namedWindow("input", CV_WINDOW_AUTOSIZE);
    imshow("input", src);
 
    //將白色背景變成黑色
    for (int row = 0; row < src.rows; row++)
    {
        for (int col = 0; col < src.cols; col++)
        {
            if (src.at<Vec3b>(row, col)[0] > 200 && src.at<Vec3b>(row, col)[1] > 200 && src.at<Vec3b>(row, col)[2] > 200)
            {
                src.at<Vec3b>(row, col)[0] = 0;
                src.at<Vec3b>(row, col)[1] = 0;
                src.at<Vec3b>(row, col)[2] = 0;
            }
        }
    }
    const char *input = "change backgroud image";
    namedWindow(input, CV_WINDOW_AUTOSIZE);
    imshow(input, src);
 
    //使用filter2D與拉普拉斯算子實現圖像對比度提高
    Mat imgLaplance;
    Mat sharp  = src;
    Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);
    filter2D(sharp, imgLaplance, CV_32F, kernel, Point(-1, -1), 0, BORDER_DEFAULT);
    src.convertTo(sharp, CV_32F);
    Mat imgResult = sharp - imgLaplance;
 
    imgResult.convertTo(imgResult, CV_8UC3);
    imgLaplance.convertTo(imgLaplance, CV_8UC3);
    namedWindow("Sharp image", CV_WINDOW_AUTOSIZE);
    imshow("Sharp image", imgResult);
 
    //binary image轉為二值圖像
    Mat binaryImag;
    cvtColor(src, binaryImag, CV_BGR2GRAY);
    threshold(binaryImag, binaryImag, 40, 255, THRESH_OTSU | THRESH_BINARY);
    namedWindow("binary image", CV_WINDOW_AUTOSIZE);
    imshow("binary image", binaryImag);
 
    //距離變換(distance transform)
    Mat distImg;
    distanceTransform(binaryImag, distImg, DIST_L1, 3, 5);
 
    //對距離變換結果進行歸一化到[0~1]之間
    normalize(distImg, distImg, 0, 1, NORM_MINMAX);
    imshow("distance image", distImg);
 
    //使用閾值,再次二值化,得到標記(binary again)
    threshold(distImg, distImg, .2, 1, THRESH_BINARY);
 
    //腐蝕得到每個Peak - erode
    Mat kernel1 = Mat::ones(13, 13, CV_8UC1);
    erode(distImg, distImg, kernel1, Point(-1, -1));
    imshow("distance binary image", distImg);
 
    // markers (發現輪廓 – findContours)
    Mat dist_8u;
    distImg.convertTo(dist_8u, CV_8U);
    vector<vector<Point>> contours;
    findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
 
    // create makers(繪制輪廓 - drawContours)
    Mat markers = Mat::zeros(src.size(), CV_32SC1);
    for (size_t i = 0; i < contours.size(); i++)
        drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 1), -1);
    circle(markers, Point(5, 5), 3, Scalar(255, 255, 255), -1);
    imshow("my markers", markers * 1000);
 
    // perform watershed
    watershed(src, markers);
    Mat mark = Mat::zeros(markers.size(), CV_8UC1);
    markers.convertTo(mark, CV_8UC1);
    bitwise_not(mark, mark, Mat());
    imshow("watershed image", mark);
 
    // generate random color
    vector<Vec3b> colors;
    for (size_t i = 0; i < contours.size(); i++) 
    {
        int r = theRNG().uniform(0, 255);
        int g = theRNG().uniform(0, 255);
        int b = theRNG().uniform(0, 255);
        colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
    }
 
    // fill with color and display final result(對每個分割區域着色輸出結果)
    Mat dst = Mat::zeros(markers.size(), CV_8UC3);
    for (int row = 0; row < markers.rows; row++) 
    {
        for (int col = 0; col < markers.cols; col++) 
        {
            int index = markers.at<int>(row, col);
            if (index > 0 && index <= static_cast<int>(contours.size()))
                dst.at<Vec3b>(row, col) = colors[index - 1];
            else
                dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
        }
    }
    imshow("Final Result", dst);
 
    waitKey(0);
    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM