1. 程式人生 > >QT+opencv學習筆記——邊緣檢測、輪廓提取及輪廓跟蹤

QT+opencv學習筆記——邊緣檢測、輪廓提取及輪廓跟蹤

開發環境為:win10+QT5.8+opencv3.2

  數字影象的邊緣檢測是影象分割、目標區域的識別、區域形狀提取等影象分析領域十分重要的基礎,影象分析和理解的第一步往往就是邊緣檢測。輪廓跟蹤是獲取影象的外部輪廓特徵,為影象的形狀分析做準備。本文主要實現影象邊緣檢測、輪廓提取、輪廓跟蹤。

一、讀取影象

     讀取影象見QT+opencv學習筆記(1)——影象點運算,這裡不再贅述。

     讀取結果如下圖:

二、邊緣檢測

   邊緣是指影象區域性強度變化最顯著的部分。邊緣主要存在與目標與目標、目標與背景、區域與區域之間。影象強度的不連續性可分為:階躍不連續,即影象強度在不連續處的兩邊的畫素灰度值有顯著的差異;線條不連續,即影象強度從一個值變化到另一個值,保持一較小行程後又回到原來的值。

邊緣檢測運算元檢查每個畫素的鄰域並對灰度變換率進行量化,也包括方向的確定。大多數使用基於方向倒數掩模求卷積的方法。

下面介紹幾種常用的邊緣檢測運算元。

Canny運算元

   Canny運算元運用比較廣泛。是在Sobel運算元的基礎上改進的。

   Canny運算元的步驟是:

          1.先進行濾波降噪。

          2.計算梯度幅值和方向(進行Sobel運算元計算)。

          3.非極大值抑制。

          4.滯後閾值。

    Canny邊緣檢測可通過Canny()函式來實現。Canny()函式的定義如下:

//推薦高低閾值比例介於2:1與3:1之間
void Canny(InputArray image, //8位單通道輸入影象
OutputArray edges, //輸出影象,和輸入影象的尺寸型別一致
double threshold1, //滯後閾值低閾值(用於邊緣連線)
double threshold2, //滯後閾值高閾值(控制邊緣初始段)
int apertureSize=3, //表示Sobel運算元孔徑大小,預設為3
bool L2gradient=false //計算影象梯度幅值的標識
);
Canny邊緣檢測主要程式碼如下:

//Canny邊緣檢測
Canny(grayImg, edgeImg, 30, 80);
Canny邊緣檢測處理結果如下:

Sobel運算元

   Sobel運算元是一個主要用於邊緣檢測的離散微分運算元,它結合了高斯平滑和微分求導,用於計算影象灰度函式的近似梯度。

   Sobel運算元檢測方法對灰度漸變和噪聲較多的影象處理效果較好,對邊緣定位不是很準確,影象的邊緣不止一個畫素。

   Sobel邊緣檢測可通過Sobel()函式來實現。Sobel()函式的定義如下:

void Sobel(InputArray src, //輸入影象
OutputArray dst, //輸出影象,和輸入影象的尺寸型別一致
int ddepth, //輸出影象的深度
int xorder, //x方向上的差分階數
int yorder, //y方向上的差分階數
int ksize=3, //Sobel核的大小,取值1,3,5,7
double scale=1, //計算倒數時的縮放因子
double delta=0, //可選delta值
int borderType=BORDERDEFAULT //邊界模式,一般預設即可
)
Sobel邊緣檢測主要程式碼如下:

        //Sobel邊緣檢測
        Mat x_edgeImg, y_edgeImg;
        Mat abs_x_edgeImg, abs_y_edgeImg;

        /*先對x方向進行邊緣檢測**/
        //因為Sobel求出來的結果有正負,8位無符號表示不全,故用16位有符號表示
        Sobel(grayImg,x_edgeImg, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);
        convertScaleAbs(x_edgeImg, abs_x_edgeImg);//將16位有符號轉化為8位無符號

        /*再對y方向進行邊緣檢測**/
        Sobel(grayImg, y_edgeImg, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);
        convertScaleAbs(y_edgeImg, abs_y_edgeImg);

        addWeighted(abs_x_edgeImg, 0.5, abs_y_edgeImg, 0.5, 0, edgeImg);
   Sobel邊緣檢測處理結果如下:

Laplacian運算元

    Laplacian運算元邊緣檢測是通過二階倒數,二階倒數比一階倒數的好處是在與受到周圍的干擾小,其不具有方向性,操作容易,且對於很多方向的影象處理好。

    Laplacian運算元對噪聲比較敏感,所以很少用該運算元檢測邊緣,而是用來判斷邊緣畫素視為與影象的明區還是暗區。

     Laplacian邊緣檢測可通過Laplacian()函式來實現。Laplacian()函式的定義如下:

void Laplacian(InputArray src, //輸入影象
OutputArray dst, //輸出影象,和輸入影象的尺寸型別一致
int ddepth, //輸出影象的深度
int ksize=1, //用於計算二階導數的濾波器孔徑大小,須為正奇數,預設值為1
double scale=1, //可選比例因子,預設值為1
double delta=0, //可選delta值
int borderType=BORDER_DEFAULT //邊界模式,一般預設即可
)
Laplacian邊緣檢測主要程式碼如下:

        //Laplacian邊緣檢測
        Mat lapImg;

        Laplacian(grayImg, lapImg, CV_16S, 5, 1, 0, BORDER_DEFAULT);
        convertScaleAbs(lapImg, edgeImg);
    Laplacian邊緣檢測處理結果如下:

三、輪廓提取

     輪廓的提取,邊緣檢測就可以做到,不過得到的輪廓比較粗糙。

     影象輪廓的提取先對影象二值化,再通過findContours()函式提取輪廓,最後通過drawContours()函式將輪廓繪製出來。在將輪廓提取的結果使用imwrite函式儲存到本地時,總是寫不了,查了半天沒找出問題,剛開始檔名為con.bmp,最後把檔名改成cont.bmp就好了,玄學。。。

   findContours()函式定義如下:

void cv::findContours(
cv::InputOutputArray image, // 輸入的8位單通道“二值”影象
cv::OutputArrayOfArrays contours, // 包含points的vectors的vector
cv::OutputArray hierarchy, // (可選) 拓撲資訊
int mode, // 輪廓檢索模式
int method, // 近似方法
cv::Point offset = cv::Point() // (可選) 所有點的偏移
);
drawContours()函式定義如下:

void cv::drawContours(
cv::InputOutputArray image, // 用於繪製的輸入影象
cv::InputArrayOfArrays contours, // 點的vectors的vector
int contourIdx, // 需要繪製的輪廓的指數 (-1 表示 “all”)
const cv::Scalar& color, // 輪廓的顏色
int thickness = 1, // 輪廓線的寬度
int lineType = 8, // 輪廓線的鄰域模式('4’鄰域 或 '8’鄰域)
cv::InputArray hierarchy = noArray(), // 可選 (從 findContours得到)
int maxLevel = INT_MAX, // 輪廓中的最大下降
cv::Point offset = cv::Point() // (可選) 所有點的偏移
)
輪廓提取主要程式碼如下:

Mat contImg = Mat ::zeros(grayImg.size(),CV_8UC3);//定義三通道輪廓提取影象

Mat binImg;
threshold(grayImg, binImg, 127, 255, THRESH_OTSU);//大津法進行影象二值化

vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
//查詢輪廓
findContours(binImg, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_NONE);
//繪製查詢到的輪廓
drawContours(contImg, contours, -1, Scalar(0,255,0));
    輪廓提取處理結果如下:

四、輪廓跟蹤

    通常在進行邊緣檢測之後,需要通過邊緣跟蹤來將離散的邊緣串接起來,常使用的方法邊緣跟蹤法。邊緣跟蹤又分為八鄰域和四鄰域兩種。

    實現步驟:

    1、灰度化並進行Canny邊緣檢測;

    2、按照預先設定的跟蹤方向(順時針)進行邊緣跟蹤;

    3、每次跟蹤的終止條件為:8鄰域都不存在輪廓。

    主要程式碼如下:

Mat edgeImg,trackImg;

// Canny邊緣檢測
Canny(grayImg, edgeImg, 50, 100);
vector<Point> edge_t;
vector<vector<Point>> edges;
//邊緣跟蹤
EdgeTracking(edgeImg,edge_t,edges,trackImg);

void Dialog::EdgeTracking(Mat& Edge,vector& edge_t,vector<vector>& edges,Mat& trace_edge_color)
{
// 8 neighbors
const Point directions[8] = { { 0, 1 }, {1,1}, { 1, 0 }, { 1, -1 }, { 0, -1 }, { -1, -1 }, { -1, 0 },{ -1, 1 } };
int i, j, counts = 0, curr_d = 0;
for (i = 1; i < Edge.rows - 1; i++)
for (j = 1; j < Edge.cols - 1; j++)
{
// 起始點及當前點
//Point s_pt = Point(i, j);
Point b_pt = Point(i, j);
Point c_pt = Point(i, j);
// 如果當前點為前景點
if (255 == Edge.at(c_pt.x, c_pt.y))
{
edge_t.clear();
bool tra_flag = false;
// 存入
edge_t.push_back(c_pt);
Edge.at(c_pt.x, c_pt.y) = 0; // 用過的點直接給設定為0

            // 進行跟蹤
            while (!tra_flag)
            {
                // 迴圈八次
                for (counts = 0; counts < 8; counts++)
                {
                    // 防止索引出界
                    if (curr_d >= 8)
                    {
                        curr_d -= 8;
                    }
                    if (curr_d < 0)
                    {
                        curr_d += 8;
                    }
                    // 當前點座標
                    // 跟蹤的過程,應該是個連續的過程,需要不停的更新搜尋的root點
                    c_pt = Point(b_pt.x + directions[curr_d].x, b_pt.y + directions[curr_d].y);
                    // 邊界判斷
                    if ((c_pt.x > 0) && (c_pt.x < Edge.cols - 1) &&
                        (c_pt.y > 0) && (c_pt.y < Edge.rows - 1))
                    {
                        // 如果存在邊緣
                        if (255 == Edge.at<uchar>(c_pt.x, c_pt.y))
                        {
                            curr_d -= 2;   // 更新當前方向
                            edge_t.push_back(c_pt);
                            Edge.at<uchar>(c_pt.x, c_pt.y) = 0;

                            // 更新b_pt:跟蹤的root點
                            b_pt.x = c_pt.x;
                            b_pt.y = c_pt.y;

                            //cout << c_pt.x << " " << c_pt.y << endl;
                            break;   // 跳出for迴圈
                        }
                    }
                    curr_d++;
                }   // end for
                // 跟蹤的終止條件:如果8鄰域都不存在邊緣
                if (8 == counts )
                {
                    // 清零
                    curr_d = 0;
                    tra_flag = true;
                    edges.push_back(edge_t);
                    break;
                }
            }  // end if
        }  // end while
    }
// 顯示一下
Mat trace_edge = Mat::zeros(Edge.rows, Edge.cols, CV_8UC1);
//Mat trace_edge_color;
cvtColor(trace_edge, trace_edge_color, CV_GRAY2BGR);
for (i = 0; i < edges.size(); i++)
{
    Scalar color = Scalar(rand()%255, rand()%255, rand()%255);
    // 過濾掉較小的邊緣
    if (edges[i].size() > 5)
    {
        for (j = 0; j < edges[i].size(); j++)
        {
            trace_edge_color.at<Vec3b>(edges[i][j].x, edges[i][j].y)[0] = color[0];
            trace_edge_color.at<Vec3b>(edges[i][j].x, edges[i][j].y)[1] = color[1];
            trace_edge_color.at<Vec3b>(edges[i][j].x, edges[i][j].y)[2] = color[2];
        }
    }

}

}
輪廓跟蹤處理結果如下:

    整體工程程式碼見<a href="https://download.csdn.net/download/minghui/10445804" rel="nofollow" target="_blank">QT+opencv邊緣檢測,輪廓提取及輪廓跟蹤

參考:

(1)邊緣檢測概念理解

(2)【OpenCV學習筆記】十九、影象邊緣檢測

(3)OPENCV輪廓提取findContours和drawContours

(4)【OpenCV3】影象輪廓查詢與繪製——cv::findContours()與cv::drawContours()詳解

(5)影象處理(五):八鄰域邊緣跟蹤與區域生長演算法