OpenCV實現霍夫變換直線檢測

霍夫變換(Hough Transform)是圖像處理中檢測是否存在直線的重要算法,該算法是由Paul Hough在1962年首次提出,最開始隻能檢測圖像中的直線,但是霍夫變換經過不斷的擴展和完善已經可以檢測多種規則形狀,例如圓形、橢圓等。霍夫變換通過將圖像中的像素在一個空間坐標系中變換到另一個坐標空間坐標系中,使得在原空間中具有形同特性的曲線或者直線映射到另一個空間中形成峰值,從而把檢測任意形狀的問題轉化為統計峰值的問題。

霍夫變換通過構建檢測形狀的數學解析式將圖像中像素點映射到參數空間中,例如我們想檢測兩個像素點所在的直線,需要構建直線的數學解析式。在圖像空間x-y直角坐標系中,對於直線可以用式(7.1)所示的解析式來表示。

其中k是直線的斜率,b是直線的截距。假設圖像中存在一像素點A(x0,y0),所有經過這個像素點直線可以用式表示。

在圖像空間x-y直角坐標系中,由於變量是x和y,因此式表示的是經過點像素點A(x0,y0)的直線,但是經過一點的直線有無數條,因此式中的 和 具有無數個可以選擇的值,如果將x0和y0看作是變量, k和 b表示定值,那麼式可以表示在k-b空間的一條直線,映射過程示意圖如圖所示。用式的形式表示映射的結果如式所示,即霍夫變換將x-y直角坐標系中經過一點的所有直線映射成瞭k-b空間中的一條直線,直線上的每個點都對應著x-y直角坐標系中的一條直線。

當圖像中存在另一個像素點B(x1,y1)時,在圖像空間x-y直角坐標系中所有經過像素點B(x1,y1)的直線也會在參數空間中映射出一條直線。由於參數空間中每一個點都表示圖像空間x-y直角坐標系中直線的斜率和截距,因此如果有一條直線經過像素點A(x0,y0)和像素點B(x1,y1)時,這條直線所映射在參數空間中的坐標點應該既在像素點A(x0,y0)映射的直線上又在像素點B(x1,y1)映射的直線上。在平面內一個點同時在兩條直線上,那麼這個點一定是兩條直線的交點,因此這條同時經過A(x0,y0)和B(x1,y1)的直線所對應的斜率和截距就是參數空間中兩條直線的交點。

根據前面的分析可以得到霍夫變換中存在兩個重要的結論:(1)圖像空間中的每條直線在參數空間中都對應著單獨一個點來表示;(2)圖像空間中的直線上任何像素點在參數空間對應的直線相交於同一個點。圖給出瞭第二條結論的示意圖。因此通過霍夫變換尋找圖像中的直線就是尋找參數空間中大量直線相交的一點。

利用式形式進行霍夫變換可以尋找到圖像中絕大多數直線,但是當圖像中存在垂直直線時,即所有的像素點的x坐標相同時,直線上的像素點利用上述霍夫變換方法得到的參數空間中多條直線互相平行,無法相交於一點。例如在圖像上存在3個像素點(2,1)、(2,2)和(2,3) ,利用式可以求得參數空間中3條直線解析式如式中所示,這些直線具有相同的斜率,因此無法交於一點,具體形式如圖所示。

為瞭解決垂直直線在參數空間沒有交點的問題,一般采用極坐標方式表示圖像空間x-y直角坐標系中的直線,具體形式如式(7.5)所示。

其中 r為坐標原點到直線的距離, Θ為坐標原點到直線的垂線與x軸的夾角,這兩個參數的含義如圖所示。

根據霍夫變換原理,利用極坐標形式表示直線時,在圖像空間中經過某一點的所有直線映射到參數空間中是一個正弦曲線。圖像空間中直線上的兩個點在參數空間中映射的兩條正弦曲線相交於一點,圖中給出瞭用極坐標形式表示直線的霍夫變換的示意圖。

通過上述的變換過程,將圖像中的直線檢測轉換成瞭在參數空間中尋找某個點 通過的正線曲線最多的問題。由於在參數空間內的曲線是連續的,而在實際情況中圖像的像素是離散的,因此我們需要將參數空間的r軸和Θ軸進行離散化,用離散後的方格表示每一條正弦曲線。首先尋找符合條件的網格,之後尋找該網格對應的圖像空間中所有的點,這些點共同組成瞭原圖像中的直線。

總結上面所有的原理和步驟,霍夫變換算法檢測圖像中的直線主要分為4個步驟:

霍夫檢測具有抗幹擾能力強,對圖像中直線的殘缺部分、噪聲以及其它共存的非直線結構不敏感,能容忍特征邊界描述中的間隙,並且相對不受圖像噪聲影響等優點,但是霍夫變換的時間復雜度和空間復雜度都很高,並且檢測精度受參數離散間隔制約。離散間隔較大時會降低檢測精度,離散間隔較小時雖然能提高精度,但是會增加計算負擔,導致計算時間邊長。

標準霍夫變換

void HoughLines( InputArray image, OutputArray lines,
                              double rho, double theta, int threshold,
                              double srn = 0, double stn = 0,
                              double min_theta = 0, double max_theta = CV_PI );
  • image:待檢測直線的原圖像,必須是CV_8U的單通道二值圖像。
  • lines:霍夫變換檢測到的直線輸出量,每一條直線都由兩個參數表示,分別表示直線距離坐標原點的距離 和坐標原點到直線的垂線與x軸的夾角 。
  • rho:以像素為單位的距離分辨率,即距離 離散化時的單位長度。
  • theta:以弧度為單位的角度分辨率,即夾角 離散化時的單位角度。
  • threshold:累加器的閾值,即參數空間中離散化後每個方格被通過的累計次數大於該閾值時將被識別為直線,否則不被識別為直線。
  • srn:對於多尺度霍夫變換算法中,該參數表示距離分辨率的除數,粗略的累加器距離分辨率是第三個參數rho,精確的累加器分辨率是rho/srn。這個參數必須是非負數,默認參數為0。
  • stn:對於多尺度霍夫變換算法中,該參數表示角度分辨率的除數,粗略的累加器距離分辨率是第四個參數rho,精確的累加器分辨率是rho/stn。這個參數必須是非負數,默認參數為0。當這個參數與第六個參數srn同時為0時,此函數表示的是標準霍夫變換。
  • min_theta:檢測直線的最小角度,默認參數為0。
  • max_theta:檢測直線的最大角度,默認參數為CV_PI,是OpenCV

4中的默認數值具體為3.1415926535897932384626433832795。

該函數用於尋找圖像中的直線,並以極坐標的形式將圖像中直線的極坐標參數輸出。該函數的第一個參數為輸入圖像,必須是CV_8U的單通道二值圖像,如果需要檢測彩色圖像或者灰度圖像中是否存在直線,可以通過Canny()函數計算圖像的邊緣,並將邊緣檢測結果二值化後的圖像作為輸入圖像賦值給該參數。函數的第二個參數是霍夫變換檢測到的圖像中直線極坐標描述的系數,是一個N×2的vector矩陣,每一行中的第一個元素是直線距離坐標原點的距離,第二個元素是該直線過坐標原點的垂線與x軸的夾角,這裡需要註意的是圖像中的坐標原點在圖像的左上角。函數第三個和第四個參數是霍夫變換中對參數空間坐標軸進行離散化後單位長度,這兩個參數的大小直接影響到檢測圖像中直線的精度,數值越小精度越高。第三個參數表示參數空間 軸的單位長度,單位為像素,該參數常設置為1;第四個參數表示參數空間 軸的單位長度,單位為弧度,該函數常設置為CV_PI/180。函數第五個參數是累加器的閾值,表示參數空間中某個方格是否被認定為直線的判定標準,這個數值越大,對應在原圖像中構成直線的像素點越多,反之則越少。第六個和第七個參數起到選擇標準霍夫變換和多尺度霍夫變換的作用,當兩個參數全為0時,該函數使用標準霍夫變換算法,否則該函數使用多尺度霍夫變換算法,當函數使用多尺度霍夫變換算法時,這兩個函數分別表示第三個參數單位距離長度的除數和第四個參數角度單位角度的除數。函數最後兩個參數是檢測直線的最小角度和最大角度,兩個參數必須大於等於0小於等於CV_PI(3.1415926535897932384626433832795),並且最小角度的數值要小於最大角度的數值。

該函數隻能輸出直線的極坐標表示形式的參數。

該函數隻能判斷圖像中是否有直線,而不能判斷直線的起始位置

漸進概率式霍夫變換

 漸進概率式霍夫變換函數HoughLinesP()可以得到圖像中滿足條件的直線或者線段兩個端點的坐標,進而確定直線或者線段的位置

void HoughLinesP( InputArray image, OutputArray lines,
                               double rho, double theta, int threshold,
                               double minLineLength = 0, double maxLineGap = 0 );
  • image:待檢測直線的原圖像,必須是CV_8C的單通道二值圖像。
  • lines:霍夫變換檢測到的直線輸出量,每一條直線都由4個參數進行描述,分別是直線兩個端點的坐標
  • rho:以像素為單位的距離分辨率,即距離 離散化時的單位長度。
  • theta:以弧度為單位的角度分辨率,即夾角 離散化時的單位角度。
  • threshold:累加器的閾值,即參數空間中離散化後每個方格被通過的累計次數大於閾值時則被識別為直線,否則不被識別為直線。
  • minLineLength:直線的最小長度,當檢測直線的長度小於該數值時將會被剔除。
  • maxLineGap:允許將同一行兩個點連接起來的最大距離。

該函數用於尋找圖像中滿足條件的直線或者線段兩個端點的坐標。該函數的第一個參數為輸入圖像,必須是CV_8U的單通道二值圖像,如果需要檢測彩色圖像或者灰度圖像中是否存在直線,可以通過Canny()函數計算圖像的邊緣,並將邊緣檢測結果二值化後的圖像作為輸入圖像賦值給該參數。函數的第二個參數是圖像中直線或者線段兩個端點的坐標,是一個N×4的vector矩陣。Vec4i中前兩個元素分別是直線或者線段一個端點的x坐標和y坐標,後兩個元素分別是直線或者線段另一個端點的x坐標和y坐標。函數第三個和第四個參數含義與HoughLines()函數的參數含義相同,都是霍夫變換中對參數空間坐標軸進行離散化後的單位長度,這兩個參數的大小直接影響到檢測圖像中直線的精度,數值越小精度越高。第三個參數表示參數空間 軸的單位長度,單位為像素,該參數常設置為1;第四個參數表示參數空間 軸的單位角度,單位為弧度,該函數常設置為CV_PI/180。函數第五個參數是累加器的閾值,表示參數空間中某個方格是否被認定為直線的判定標準,這個數值越大,對應在原圖像中的直線越長,反之則越短。第六個參數是檢測直線或者線段的長度,如果圖像中直線的長度小於這個閾值,即使是直線也不會作為最終結果輸出。函數最後一個參數是鄰近兩個點連接的最大距離,這個參數主要能夠控制傾斜直線的檢測長度,當提取較長的傾斜直線時該參數應該具有較大取值。

該函數的最大特點是能夠直接給出圖像中直線或者線段兩個端點的像素坐標,因此可較精確的定位到圖像中直線的位置。

在含有坐標點集合中尋找是否存在直線

void HoughLinesPointSet( InputArray _point, OutputArray _lines, int lines_max, int threshold,
                                      double min_rho, double max_rho, double rho_step,
                                      double min_theta, double max_theta, double theta_step );
  • _point:輸入點的集合,必須是平面內的2D坐標,數據類型必須是CV_32FC2或CV_32SC2。
  • _lines:在輸入點集合中可能存在的直線,每一條直線都具有三個參數,分別是權重、直線距離坐標原點的距離 和坐標原點到直線的垂線與x軸的夾角 。
  • lines_max:檢測直線的最大數目。
  • threshold:累加器的閾值,即參數空間中離散化後每個方格被通過的累計次數大於閾值時則被識別為直線,否則不被識別為直線。
  • min_rho:檢測直線長度的最小距離,以像素為單位。
  • max_rho:檢測直線長度的最大距離,以像素為單位。
  • rho_step::以像素為單位的距離分辨率,即距離 離散化時的單位長度。
  • min_theta:檢測直線的最小角度值,以弧度為單位。
  • max_theta:檢測直線的最大角度值,以弧度為單位。
  • theta_step:以弧度為單位的角度分辨率,即夾角 離散化時的單位角度。

該函數用於在含有坐標的2D點的集合中尋找直線,函數檢測直線使用的方法是標準霍夫變換法。函數第一個參數是2D點集合中每個點的坐標,由於坐標必須是CV_32F或者CV_32S類型,因此可以將點集定義成vector< Point2f>或者vector< Point2f>類型。函數的第二個參數是檢測到的輸入點集合中可能存在的直線,是一個1×N的矩陣,數據類型為CV_64FC3,其中第1個數據表示該直線的權重,權重越大表示是直線的可靠性越高,第2個數據和第3個數據分別表示直線距離坐標原點的距離 和坐標原點到直線的垂線與x軸的夾角 ,矩陣中數據的順序是按照權重由大到小依次存放。函數第三個參數是檢測直線的數目,如果數目過大,檢測到的直線可能存在權重較小的情況。函數第四個參數是累加器的閾值,表示參數空間中某個方格是否被認定為直線的判定標準,這個數值越大,表示檢測的直線需要通過的點的數目越多。函數第五個、第六個參數是檢測直線長度的取值范圍,單位為像素。函數第七個參數是霍夫變換算法中離散化時距離分辨率的大小,單位為像素。函數第八個、第九個參數是檢測直線經過坐標原點的垂線與x軸夾角的范圍,單位為弧度。函數第七個參數是霍夫變換算法中離散化時角度分辨率的大小,單位為弧度。

簡單示例

//
// Created by smallflyfly on 2021/6/21.
//
 
#include "opencv2/opencv.hpp"
#include <iostream>
 
using namespace std;
using namespace cv;
 
 
void drawLine(const Mat &im, const vector<Vec2f> &lines) {
    Point p1, p2;
    cout << lines.size() << endl;
    for (int i = 0; i < lines.size(); ++i) {
        float rho = lines[i][0];
        float theta = lines[i][1];
        double a = cos(theta);
        double b = sin(theta);
        double x0 = a * rho, y0 = b * rho;
        double length = max(im.rows, im.cols);
 
        p1.x = cvRound(x0 + length * (-b));
        p1.y = cvRound(y0 + length * a);
 
        p2.x = cvRound(x0 - length * (-b));
        p2.y = cvRound(y0 - length * a);
 
        line(im, p1, p2, Scalar(255), 1);
    }
}
 
int  main() {
    Mat im = imread("road2.jfif");
    resize(im, im, Size(0, 0), 0.5, 0.5);
    cvtColor(im, im, CV_BGR2GRAY);
    imshow("im", im);
 
 
    Mat edge;
    Canny(im, edge, 200, 220, 3, false);
    threshold(edge, edge, 125, 255, THRESH_BINARY);
 
    imshow("edge", edge);
 
    // 標準霍夫變換
    vector<Vec2f> lines1, lines2;
    HoughLines(edge, lines1, 1, CV_PI/180, 150);
    HoughLines(edge, lines2, 1, CV_PI/180, 100);
 
    Mat im1, im2;
    im1 = im.clone();
    im2 = im.clone();
 
    drawLine(im1, lines1);
    drawLine(im2, lines2);
 
    imshow("im1", im1);
    imshow("im2", im2);
 
    // 漸進概率式霍夫變換
    Mat im3, im4;
    im3 = im.clone();
    im4 = im.clone();
    vector<Vec4f> lines3, lines4;
    HoughLinesP(edge, lines3, 1, CV_PI/180, 150, 10, 300);
    HoughLinesP(edge, lines4, 1, CV_PI/180, 100, 10, 300);
 
    for (Vec4f & i : lines3) {
        line(im3, Point(i[0], i[1]), Point(i[2], i[3]),
             Scalar(255, 255, 255), 1);
    }
 
    for (Vec4f & i : lines4) {
        line(im4, Point(i[0], i[1]), Point(i[2], i[3]),
             Scalar(255, 255, 255), 1);
    }
 
    imshow("im3", im3);
    imshow("im4", im4);
 
 
    // 通過霍夫變換獲取點集合中的直線  采用標準霍夫變換
    const static float points[20][2] = {
            { 0.0f,   369.0f },{ 10.0f,  364.0f },{ 20.0f,  358.0f },{ 30.0f,  352.0f },
            { 40.0f,  346.0f },{ 50.0f,  341.0f },{ 60.0f,  335.0f },{ 70.0f,  329.0f },
            { 80.0f,  323.0f },{ 90.0f,  318.0f },{ 100.0f, 312.0f },{ 110.0f, 306.0f },
            { 120.0f, 300.0f },{ 130.0f, 295.0f },{ 140.0f, 289.0f },{ 150.0f, 284.0f },
            { 160.0f, 277.0f },{ 170.0f, 271.0f },{ 180.0f, 266.0f },{ 190.0f, 260.0f }
    };
    vector<Point2f> pts;
    for (auto point : points) {
        pts.emplace_back(point[0], point[1]);
    }
    vector<Vec3d> lines;
    HoughLinesPointSet(pts, lines, 20, 1, 0, 360, 1, 0,
                       CV_PI / 2, CV_PI / 180);
 
    for (int i = 0; i < lines.size(); ++i) {
        cout << "votes: " << lines[i][0] << ", "
        << "rho: " << lines[i][1] << ", "
        << "theta :" << lines[i][2] << endl;
    }
 
    waitKey(0);
    destroyAllWindows();
 
    return 0;
}

以上就是本文的全部內容,希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: