1. 程式人生 > >OpenCV 學習(直線擬合)

OpenCV 學習(直線擬合)

Hough 變換可以提取影象中的直線。但是提取的直線的精度不高。而很多場合下,我們需要精確的估計直線的引數,這時就需要進行直線擬合。

直線擬合的方法很多,比如一元線性迴歸就是一種最簡單的直線擬合方法。但是這種方法不適合用於提取影象中的直線。因為這種演算法假設每個資料點的X 座標是準確的,Y 座標是帶有高斯噪聲的。可實際上,影象中的每個資料點的XY 座標都是帶有噪聲的。

下面就來講講適用於提取影象中直線的直線擬合算法。

一個點 (xi,yi)(xi,yi) 到直線的距離用 riri 來表示。

所謂直線擬合,就是找到一條直線,使得:

ρ(ri)∑ρ(ri)

最小。

ρ(r)

ρ(r) 是距離函式。ρ(r)ρ(r) 函式取不同的形式,對應不同的直線擬合方法。OpenCV 中支援 6 種不同的ρ(r)ρ(r) 函式形式。分別是:

CV_DIST_L2 

ρ(r)=r22ρ(r)=r22

這種方法是以距離平方和為擬合判據。也就是常見的最小二乘擬合算法,執行速度也最快。但是這個演算法也有個很大的問題,就是當干擾點離直線較遠時,一個干擾點就可能將整條擬合直線拉偏了。簡單的說就是對干擾點的魯棒性不夠。所以後來又提出了其他的函式。

CV_DIST_L1

ρ(r)=rρ(r)=r

CV_DIST_L12

ρ(r)=2(1+r221)ρ(r)=2(1+r22−1)

CV_DIST_FAIR

ρ(r)=C2(rClog(1+rC))ρ(r)=C2(rC−log⁡(1+rC))
其中 C = 1.3998

CV_DIST_WELSCH

ρ(r)=C22(1exp((rC)2))ρ(r)=C22(1−exp⁡(−(rC)2))
其中 C = 2.9846

CV_DIST_HUBER

ρ(r)={r22C(rC2)ifr<C,otherwise.ρ(r)={r22if  r<C,C(r−C2)otherwise.

其中 C = 1.345

後面這 5 種函式我知道第一種,其他的不知道是怎麼來的。OpenCV 的幫助文件給出了一個連結:

M-estimator

但是這個頁面也被牆了。

下面來說說 OpenCV 提供的直線擬合函式。函式原型如下:

void fitLine( InputArray points, 
    OutputArray line, 
    int distType,
    double param, 
    double reps, 
    double aeps );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

distType 指定擬合函式的型別,可以取 CV_DIST_L2、CV_DIST_L1、CV_DIST_L12、CV_DIST_FAIR、CV_DIST_WELSCH、CV_DIST_HUBER。

param 就是 CV_DIST_FAIR、CV_DIST_WELSCH、CV_DIST_HUBER 公式中的C。如果取 0,則程式自動選取合適的值。

reps 表示直線到原點距離的精度,建議取 0.01。 
aeps 表示直線角度的精度,建議取 0.01。

計算出的直線資訊存放在 line 中,為 cv::Vec4f 型別。line[0]、line[1] 存放的是直線的方向向量。line[2]、line[3] 存放的是直線上一個點的座標。

如果直線用 y=kx+by=kx+b 來表示,那麼 k = line[1]/line[0],b = line[3] - k * line[2]。

如果直線用 ρ=xcosθ+ysinθρ=xcos⁡θ+ysin⁡θ 來表示, 那麼 θ=arctank+π2θ=arctan⁡k+π2

下面是個測試影象: 
這裡寫圖片描述

影象中有一條直線和一些干擾圖案。

下面的程式碼可以從影象中提取出需要的座標點。

std::vector<cv::Point> getPoints(cv::Mat &image, int value)
{
    int nl = image.rows; // number of lines
    int nc = image.cols * image.channels();
    std::vector<cv::Point> points;
    for (int j = 0; j < nl; j++)
    {
        uchar* data = image.ptr<uchar>(j);
        for (int i = 0; i < nc; i++)
        {
            if(data[i] == value)
            {
                points.push_back(cv::Point(i, j));
            }
        }
    }
    return points;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

下面的程式碼可以在圖中畫一條直線。

void drawLine(cv::Mat &image, double theta, double rho, cv::Scalar color)
{
    if (theta < PI/4. || theta > 3.*PI/4.)// ~vertical line
    {
        cv::Point pt1(rho/cos(theta), 0);
        cv::Point pt2((rho - image.rows * sin(theta))/cos(theta), image.rows);
        cv::line( image, pt1, pt2, cv::Scalar(255), 1);
    }
    else
    {
        cv::Point pt1(0, rho/sin(theta));
        cv::Point pt2(image.cols, (rho - image.cols * cos(theta))/sin(theta));
        cv::line(image, pt1, pt2, color, 1);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

下面的程式碼是程式的主體。

int main(int argc, char *argv[])
{
    QCoreApplica