LSD(Line Segment Detector)算法的研究与实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010712012/article/details/85038614

最近又看了一篇直线检测的论文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

猜你喜欢

转载自blog.csdn.net/u010712012/article/details/85038614