1. 程式人生 > >LSD(Line Segment Detector)演算法的研究與實現

LSD(Line Segment Detector)演算法的研究與實現

最近又看了一篇直線檢測的論文LSD(Line-Segment-Detector)
http://www.ipol.im/pub/art/2012/gjmr-lsd/
聽說這個演算法檢測直線簡直是大殺特殺,之前這篇文章 https://blog.csdn.net/u010712012/article/details/84780943 是專門檢測車道直線的,考慮到高速公路車道線的虛實,那就找找不用Hough變換也能檢測直線的演算法,還真有!

LSD主要是在遙感影象中幾何形狀明顯的目標進行檢測時用到。利用LSD,可以快速的檢測影象中的直線段,然後根據目標的幾何特徵設計快速演算法,以快速確定疑似目標區域。

LSD的核心是畫素合併於誤差控制。利用合併畫素來檢測直線段並不是什麼新鮮的方法,但是合併畫素的方法通常運算量較大。LSD號稱是能線上性時間(linear-time)內得到亞畫素級準確度的直線段檢測演算法。LSD雖然號稱不需人工設定任何引數,但是實際使用時,可以設定取樣率和判斷倆畫素是否合併的方向差。我們知道,檢測影象中的直線其實就是尋找影象中梯度變化較大的畫素。因此,梯度和影象的level-line是LSD提及的兩個基本概念。LSD首先計算每一個畫素與level-line的夾角以構成一個level-line場。然後,合併這個場裡方向近似相同的畫素,這樣可以得到一系列regions,這些 regions被稱為 line support regions。如下圖所示。
在這裡插入圖片描述


每一個line support region其實就是一組畫素,它也是直線段(line segment)的候選。同時,對於這個line support region,我們可以觀察它的最小外接矩形。直觀上來講,當一組畫素構成的區域,特別細長時,那麼這組畫素更加可能是直線段。基於此,作者還統計了line support region的最小外接矩形的主方向。line support region中的一個畫素的level-line 角度與最小外接矩形的主方向的角度差在容忍度(tolerance)2τ內的話,那麼這個點被稱作"aligned point"。作者統計最小外接矩形內的所有畫素數和其內的aligned points數,用來判定這個line support region是否是一個直線段。判定的準則使用的是“a contrario approach”和“Helmholtz principle”方法。在這裡,aligned points的數量是我們感興趣的資訊。因此作者考慮如下假設:aligned points越多,那麼region越可能是直線段。對於一副影象i和一個矩形r,記k(i,r)為aligned points的數量,n(r)為矩形r內的總畫素數。那麼,我們希望能夠看到:
在這裡插入圖片描述

其中,Ntest是所有要考慮的矩形的數量。PH0是針對 contrario model H0的一個概率。I是在H0模型下的隨機影象。在這篇文章中,作者用H0的模型,主要有以下兩個屬性:
(1){LLA(j)},其中j是畫素,是一由一組隨機變數組成;(2)LLA(j)在[0,2π]上均勻分佈。因此,判斷一個畫素是不是aligned point可以記作概率:p = τ/π。這樣,再通過誤差控制,最終的直線段檢測演算法如下:
在這裡插入圖片描述
LSD演算法是基於梯度的,但是為了減少梯度的依賴性,LSD演算法也做了一些必要的優化措施,所以它的流程如下。
在這裡插入圖片描述

1. 對圖片進行高斯降取樣,縮小圖片

1.1. 降取樣要解決的問題


通過降取樣可以減緩或解決影象中出現的混疊與量化偽像問題(特別是階梯效應) ,混疊問題是使不同的訊號成為不可區分的效果。它也指失真或偽影,其導致當從樣品中重建訊號時和原始連續訊號差異很大。

而另一個問題,階梯效應實際上就是鋸齒問題,影象在邊緣處常常顯示成鋸齒狀,所以需要通過降取樣解決這個問題。
在這裡插入圖片描述
可以看到在降取樣前,這兩種邊緣處提取的線段,第一張提取的是分段的線段,而本應是完整的一條線,而第二張則是連線段都沒有提取出來。

在這裡插入圖片描述
而在降取樣之後,兩種線段均提取的較為正常了。

1.2. LSD降取樣的方法

該LSD演算法中預設的降取樣比例是0.8,那意味著,X軸Y軸各降取樣0.8,而總畫素降取樣0.64。所以如果我們在計算NFA值的時候,使用的是NM的影象的話,那麼輸入影象的解析度應該是1.25N1.25M。並且在LSD演算法的降取樣中,使用的是高斯降取樣,通過使用高斯核心過濾影象以避免混疊然後進行次取樣。公式求得的是高斯核心標準偏差
在這裡插入圖片描述
S是縮放因子。

2. 計算梯度

LSD梯度的計算利用每畫素點的右邊下方的四個畫素進行計算。這樣做,主要是儘可能少的使用其他畫素,可以減少對梯度的依賴性,這樣對有噪聲的影象更具有魯棒性。計算梯度是為了記錄明暗變化,從而找到可能有線段邊緣的地方。
在這裡插入圖片描述
影象中由明轉暗和由暗轉明處的線段方向是不同的,呈180度差距,那就意味著如果一張圖片倒置其明暗,使用LSD算出的線段依舊是那些線段,但是頭與尾是顛倒過來的。 並且因為梯度計算只用到了右下方的畫素,所以計算出來的梯度並不是(x,y)點的梯度,而是(x+0.5,y+0.5)的梯度。

3. 梯度偽排序

排序演算法一般最快的也是O(nlogn)時間複雜度的,但是如果我們使用偽排序就可以將時間縮短到O(n)線性時間內,而偽排序並不是真正的進行了梯度的排序,只是對梯度值按照他們的分佈進行一定程度的排序。

LSD演算法的排序演算法基於貪心演算法,先從0到最大梯度之間分成1024個等份,再從這1024個分段中每個分段取一個種子畫素,以用來進行排序,因為1024已經可以將0~255分成很細的段了,所以偽排序雖然不是真正的排序,但是效率很高。

4.梯度的閾值

計算完梯度之後,會發現,如果一張影象中有些小梯度區域表現非常均勻,由於值的量化,那裡的畫素就會表現出更高的誤差,那麼設定如果梯度小於某一個閾值ρ就被拋棄並將不再用於線段區域的構建。 假設存在理想影象i和量化噪聲n,我們可以觀察到在這裡插入圖片描述
當角度誤差小於容忍值的時候,我們就接受這個畫素,在這裡插入圖片描述
等式的右邊是容忍度,q是 |▽n| 的邊界,容忍度我們用τ來表示,所以可以得到在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

5. 區域更新 RegionGrow

我們當前畫素計算完梯度之後,會得到一個畫素的方向,而由畫素構成的line區域也會有一個方向,我們便可以通過這兩個方向之間的差距,判斷該畫素是否可以被納入到直線區域中。

從排序列表中選擇一個NOT USED畫素作為種子點,檢查在當前畫素周圍的領域畫素內那些未使用的畫素點中,LLA(Level-line-angle)和區域角度之間的差值在容忍值τ 內話,將會被加入到該line support region區域中。而區域初始角度就是種子點的LLA,每次加入一個新畫素到區域中的時候,區域角度就通過下面的公式對整個直線區域更新一次(其中j是遍歷時的畫素下標): 在這裡插入圖片描述
誤差容忍值預設被設為22.5,那就意味著對於整個區域矩形來說,誤差容忍度是45度,22.5這個值是通過實驗得出的經驗值。

區域更新的流程:
在這裡插入圖片描述

6. 矩形近似計算

上面在規劃區域後,我們要將該區域進行矩形近似計算,以求得一個較為規整的直線區域。
一個直線段對應一個矩形,在評估line support region之前,直線對應的矩形應該被找出來,並可以用公式計算矩形的中心:
在這裡插入圖片描述
G(j)是畫素j的梯度值,j代表區域內的每一個畫素點
矩形的角度,被設為特徵向量的角度,而這個特徵向量與下面M矩陣的最小特徵值有關 在這裡插入圖片描述

7. NFA值計算

字面上解釋,Number of False Alarms指的是誤報數,也就是原本並不是直線但是被當做直線的地方。

因為矩形是有方向的,所以他們的起始點和終止點的排序實際上是有很多種可能的,這樣看的話,從A點到B點,和從B點到A點實際上是不同的兩個線段。所以我們要考慮到所有起始點終止點的可能性,在一張降取樣後,N*M的影象上,我們的點有NM個所以就有可能有NM x NM種搭配的矩形,並且矩形線的線寬有 N M \sqrt{NM} 種。
在這裡插入圖片描述
所以我們的矩形線段就會有 ( N M ) 5 2 (NM)^{\frac{5}{2}} 種可能性。

我們用二項分佈來表示測試資料。我們將p的初值設為τ/π,並用γ來表示p的不同值的可能數量。那麼最終的測試數量是在這裡插入圖片描述
我們用一個閾值ε來對NFA值進行過濾,並且如果一個矩形滿足 NFA(r,i)<=ε 那麼這個矩形被稱作ε-meaningful 矩形。

在這裡存在一個定理: 在這裡插入圖片描述
在上式中,E是期望函式,1是指示函式,R是矩形集,I是隨機影象。“ε有意義”的矩形的平均數小於ε。
因此,噪聲檢測的數量由ε控制,並且可以根據需要進行較小的控制。

說實話要看懂這演算法需要非常好的數學功夫,原始碼我看有3000多行,但是opencv3.0後直接就有了這個函式

程式碼:(C++)
1.從命令列引數中載入影象,並以灰度模式

std::string in;
    if (argc != 2)
    {
        std::cout << "Usage: lsd_lines [input image]. Now loading ../data/building.jpg" << std::endl;
        in = "img path";
    }
    else
    {
        in = argv[1];
    }

    Mat image = imread(in, IMREAD_GRAYSCALE);

2.宣告LineSegmentDetector物件,用於lsd直線檢測

#if 1
    Ptr<LineSegmentDetector> ls = createLineSegmentDetector(LSD_REFINE_STD);
#else
    Ptr<LineSegmentDetector> ls = createLineSegmentDetector(LSD_REFINE_NONE);
#endif

注意:
(1)顯然寫LSD的人和以前OpenCV程式碼的維護人員風格差異很大。

3.開始計時

double start = double(getTickCount());

4.宣告直線檢測儲存物件

vector<Vec4f> lines_std;

5.lsd直線檢測

ls->detect(image, lines_std);

6.結束計時

double duration_ms = (double(getTickCount()) - start) * 1000 / getTickFrequency();

7.宣告繪製直線檢測的影象

Mat drawnLines(image);

注意:
(1)採用這種方式宣告的Mat是深拷貝,也就是說drawnLines和image由各自的記憶體空間

8.繪製直線檢測結果

ls->drawSegments(drawnLines, lines_std);

9.顯示直線檢測結果

imshow("Standard refinement", drawnLines);

#include <iostream>
#include <string>
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
    std::string in;
    if (argc != 2)
    {
        std::cout << "Usage: lsd_lines [input image]. Now loading ../data/building.jpg" << std::endl;
        in = "../data/building.jpg";
    }
    else
    {
        in = argv[1];
    }
    Mat image = imread(in, IMREAD_GRAYSCALE);//讀入原圖,需為灰度影象
#if 0
    Canny(image, image, 50, 200, 3); // Apply canny edge//可選canny運算元
#endif
    // Create and LSD detector with standard or no refinement.
#if 1
    Ptr<LineSegmentDetector> ls = createLineSegmentDetector(LSD_REFINE_STD);//或者兩種LSD演算法,這邊用的是standard的
#else
    Ptr<LineSegmentDetector> ls = createLineSegmentDetector(LSD_REFINE_NONE);
#endif
    double start = double(getTickCount());
    vector<Vec4f> lines_std;
    // Detect the lines
    ls->detect(image, lines_std);//這裡把檢測到的直線線段都存入了lines_std中,4個float的值,分別為起止點的座標
    double duration_ms = (double(getTickCount()) - start) * 1000 / getTickFrequency();
    std::cout << "It took " << duration_ms << " ms." << std::endl;
    // Show found lines
    Mat drawnLines(image);
    ls->drawSegments(drawnLines, lines_std);
    imshow("Standard refinement", drawnLines);
    waitKey();
    return 0;
    }

程式碼(python)

import numpy as np
import cv2
from matplotlib import pyplot as plt

#Read gray image
img = cv2.imread("C:/Users/zdq/Desktop/video process/DJI.jpg",0)

#Create default parametrization LSD
lsd = cv2.createLineSegmentDetector(0)

#Detect lines in the image
lines = lsd.detect(img)[0] #Position 0 of the returned tuple are the detected lines

#Draw detected lines in the image
drawn_img = lsd.drawSegments(img,lines)

#Show image
cv2.imshow("LSD",drawn_img)
cv2.waitKey(0)

簡直不能再簡單了,但是隻有原始碼才能調參,所以,各取所取。
看看結果:
在這裡插入圖片描述

在這裡插入圖片描述
有直線的部分都能檢測出來,真厲害。但是缺點可能就是對於不同的場景不同的目標我們需要的可能沒有那麼多的直線。
在這裡插入圖片描述
在這裡插入圖片描述
DJI Mavic Pro拍出來的照片拿來LSD,感覺確實比hough來的猛,但是那些我不需要的花花草草會帶來一定的影響。例如從上圖可以看出,在陽光非常刺眼的情況下,從樹葉縫隙當中穿透的陽光灑在地上的形狀也被檢測成直線了,這樣嚴重影響判別。而且可以看到,車窗上的樹葉的倒影都能被檢測出直線。。。如果我能把車道線的直線提取出來,再把車輛的輪廓檢測出來,這樣便於後續的判斷壓線模型的研究。

參考:https://blog.csdn.net/chishuideyu/article/details/78081643
https://blog.csdn.net/polly_yang/article/details/10085401
https://blog.csdn.net/carson2005/article/details/9326847
https://blog.csdn.net/u012566751/article/details/54602958
https://blog.csdn.net/MollyLee1011/article/details/47292783