直线段检测算法---LSD:a Line Segment Detector
LSD的核心是像素合并于误差控制。利用合并像素来检测直线段并不是什么新鲜的方法,但是合并像素的方法通常运算量较大。LSD号称是能在线性时间(linear-time)内得到亚像素级准确度的直线段检测算法。LSD虽然号称不需人工设置任何参数,但是实际使用时,可以设置采样率和判断俩像素是否合并的方向差。我们知道,检测图像中的直线其实就是寻找图像中梯度变化较大的像素。LSD的目标在于检测图像中局部的直的轮廓,这也是我们称之为直线分割的原因。轮廓是图像中的某些特殊区域,在这些区域,图像的灰度从黑到白或者从白到黑的剧烈变化。因此,梯度和level-line是两个重要的概念,如下图所示:
算法首先计算每个像素的level-line angle(此文章下面的[2])以构成一个level-line 场。该场被分割为连通的若干个部分,它们方向近似相同并且在容忍度τ内,这样可以得到一系列regions,这些 regions被称为 line support regions(支持域)。如下图所示:
每一个line support region其实就是一组像素,它也是直线段(line segment)的候选。同时,对于这个line support region,我们可以观察它的最小外接矩形。直观上来讲,当一组像素构成的区域,特别细长时,那么这组像素更加可能是直线段。line support region的一个主惯性轴作为矩形的主方向,矩形的大小选择为覆盖整个区域。
矩形中的像素的level-line angle与最小外接矩形的主方向的角度差在容忍(tolerance)τ内的话,那么这个点被称作"aligned point"(同性点)。通过统计最小外接矩形内的所有像素数n和其内的alinedg points个数k,用来判定这个line support region是否是一个直线段。判定的准则使用的是a contrario approach and the Helmholtz principle。
LSD算法的具体解释:
输入:灰度图
输出:一系列的直线分割结果。
1.以 s=0.8的尺度对输入图像进行高斯核采样。
2.计算每一个点的梯度值以及梯度方向(level-line orientation),其中gx和gy分别为水平和垂直方向梯度。
3.根据梯度值对所有点进行伪排序(pseudo-ordered),建立状态列表,所有点设置为UNUSED。
[设置图像梯度强度范围到[0, 1023](大于1023的梯度强度,强制设置为1023),创建1024个链表,遍历整个梯度图,根据梯度强度,相同梯度值像素坐标放入同一张链表中,将1024个链表,按从大到小顺序,合成一张大链表(首部为1023链表,尾部为0)]
4.将梯度值小于ρ的点状态表中相应位置设置为USED。
5.取出链表头部存储图像坐标位置,作为种子像素,
do:
a.以seed为起点,根据梯度角方向相似(搜索周围UNUSED并且方向在阈值[ -τ, τ]范围内的点),进行区域扩散。(每扩散一个像素,将该像素坐标从链表中删除,并且做标记(状态改为USED),之后新的区域扩散,无法再扩散到在像素)。
b.将扩散区域进行矩形拟合,R。
c.判断同性点(aligned pt)密度是否满足阈值D,若不满足,截断(cut)R变为多个矩形框,直至满足。
d.计算NFA(拟合矩形精度误差)。
e.改变R使NFA的值更小直至NFA <= ε ,R加入输出列表;如果改变了还不满足或者矩形区域太小,舍去。
6.继续回到第5步,从链表中,找到下一个种子点,从剩下图像进行区域扩散,至到遍历完全图,得到所有检测到的直线。
【1】将输入的图像缩小为原来大小的80%,即缩放尺度为80%(S=0.8),目的在于减弱甚至消除很多图像中出现的锯齿效应。注意:80%的缩放因子意味着x和y方向都缩放为原来的80%,即总的像素为原来的64%。
缩小是通过高斯采样来进行的:图像首先采用高斯核进行滤波从而避免锯齿效应,再进行降采样。高斯核的标准差是由 Σ / S来决定的,此处的S是缩放因子,参数Σ设置为0.6,以此来保持锯齿效应和图像模糊之间的平衡。
【2】梯度计算
【3】梯度排序
梯度值越大,越是显著的边缘点,更适合作为种子点。但是对梯度值进行完全排序是一个时耗性很高的工作。因此简单的将梯度值划分为1024个等级(bins),这1024个等级涵盖了梯度由0~255的变化范围,这种排序是一个线性的时耗。LSD首先将最大梯度的像素作为种子点,种子点从梯度值最高的bin开始搜索,依次往下,直至所有点标记为UNUSED。
【4】梯度阈值(小梯度值抑制)
小梯度值点往往出现在平滑区域,或者仅仅是噪声。不在关注的范围内,但是他们的存在往往会严重影响直线角度的计算。在LSD计算过程中,梯度幅值小于ρ的像素点将被拒绝参与line support region或者矩形的构建过程。
【5】区域增长
line support region是通过合并方向近似相同的像素得到。其实在这里,这个合并的过程更多的是依赖于区域生长算法。
输入:level-line 场LLA,种子像素P,和角度容忍度 τ以及对应每个像素的状态变量
输出:一组像素区域
区域生长算法用于生成一个line support region。最初的区域角θregion是种子点(P)的level-line angle,递归的,将已经在区域的像素(P)的未使用的邻居用来测试,
and the ones whose level-line angle is equal to the region angle θregion up to a tolerance τ are added to the region.
每次添加一个新的像素到该区域,区域的角度就被更新为:
下标j:用于遍历区域中的所有像素,如此持续进行,直到没有任何像素添加到矩形当中。
角度容忍度 τ设置为22.5度,或者说π/8弧度,so it was set to obtain p = 1/8。在该误差容忍度范围内的像素点都将被选择到矩形中,这是因为他们在很大程度上跟矩形的方向保持一致。
如下图所示,实验发现,22.5是一个较好的参数。
【6】矩形估计
上一步形成line-support region一系列相邻的离散点,需要将他们包含在一个矩形框内R,这个可以看做宽度为R的宽,长度为R的长的候选直线。R选择能包含line-support region的最小矩形,所有点的梯度规范化值平均计算重心,R长轴的方向设置为R的方向。
矩形的中心(c x , c y ):
G(j)是像素j的梯度幅值,下标j用于遍历矩形区域内的所有像素。
矩形的主方向被设置为矩阵的最小特征值对应的特征向量的角度:
【7】NFA计算
自己的理解:其实这个NFA,就是通过你的观察图中矩形中的aligned points所占比例的大小与引入的一个新的模型 (a contrario model)的aligned points作比较的概率,来约束这个矩形是不是可以作为一条“线段”。(就如同作者所希望的,这个aligned points越多,越有可能是一条“线段”)
矩形验证:
用来验证矩形是否可以作为检测线段的方法是基于a contrario approach and the Helmholtz principle .
the Helmholtz principle:在完美噪声图像图像中不应该检测到目标。
a contrario approach:一个不会检测到目标的噪声图像。
对于本课题,contrario model H0是一个像素值为level-line angle的图像,其level-line angle随机分解为独立且服从平均分布于[0,2π]。
(即主要有以下两个属性: (1){LLA(j)},是 level-line场,其中j是像素,由独立随机变量组成;(2)LLA(j)在[0,2π]上均匀分布。 )
这里用NFA(Number of False Alarms)来评判observe img中某个候选R小于contrario model中相同位置R里同性点(aligned points)的数量的概率,如果NFA的值很大,认为在观测图像中aligned points比contrario model中aligned points小的概率很大,将其认为是common,平常的,背景中的一部分。如果NFA的值很小,认为目标是相对突出(rare)的,是一个合适的“直线”。
其中,Ntest为当前大小(n*m)图像中直线(矩形框)的数量。在n*m的图像中直线的起点和终点分别有m*n种选择,所以一共有(n*m)*(n*m)种起点和终点搭配。线段的宽度为(n*m)^0.5,因此在m*n大小的图像中有(n*m)^2.5 种不同直线。The precision p is initially set to the value τ /π.We will note γ the number different p values potentially tried. Each rectangle with each p value is a different test.Thus, the final number of tests is
PH0是对应 contrario model H0 的一个概率,I是在模型H0下的随机图像。(H0是图像梯度方向的噪声模型而不是一个图像的噪声模型。)
k(r,I) 为contrario model ,I 中 r 矩形里aligned pt的数量。
k(r,i) 为observe img,i 中 r 矩形里aligned pt的数量。
一个关键概念是p-aligned points,是指矩形中的level-line angle与最小外接矩形的主方向的角度差在容忍(tolerance)pπ内的像素点。 precision p最初设置的值为τ /π(角度正负容忍误差为τ ,总容忍误差为2τ 。那么在contrario model中,某个点为aligned point的概率为 p=2*τ / 2*π =τ / π),
k(r, I) 服从二项分布,所以:
所以,NFA的数量与矩形r有关:
简化r和i的公式:
其中,N和M是采样过后图像的列和行,n:矩形的像素的总数,k:p-aligned点数。
二项分布:
解释:设k(r,i)=k。那么,在 I 中的 r 矩形里,总像素个数为 n,I 中的 r 矩形里aligned pt个数k(r,I)大于等于k的话,可选择的值为k(r,i)、k(r,i)+1、k(r,i)+2,......n。
NFA计算:
若NFA(r) ≤ ε,那么可以认为结果有效。As stated before,we set ε = 1 .
其中,E是期望算子,1是指示符函数,r是考虑的矩形集合,而I是H0上的随机图像。
该定理指出在 contrario model H0上的ε-meaningful 矩形的平均数量小于ε。因此,噪声检测的次数通过ε控制的并且可以根据需要往小的调整。
换句话说,这表明LSD满足亥姆霍兹原理(具体证明可以看原文)。
【8】Aligned Points Density
在某些情况下,这τ角度容忍度的方法产生一个错误的解释。这个问题可能会出现两直线边缘存在于图像形成一个夹角小于容忍度τ。如下图显示了一个线支持区域(灰色)和与其对应的矩形的例子:
在LSD中,这个问题是通过检测有问题的线支持区域并将其切割成两个较小的区域来处理的,希望在适当的位置将该区域分割出来以解决问题。这个“角度问题”的检测是基于矩形中同性点的密度。
当这个问题不存在时,矩形非常适合于线支持区域,并且同性点的密度很高。另一方面,当“角问题”出现时,正如前面的图所看到的,同性点的密度很低。同样,当一个稍微弯曲的边被一系列直边近似地逼近时,近似的程度(多少线段被用来覆盖曲线的一部分)与同性点的密度有关;因此,对齐点密度也与线段近似曲线的精度有关。
同性点密度的计算公式:
规定一个阈值D ,在这里设置D=0.7,文章认为这个数字既能保证同一个R中的同性点属性相近,也能保证R不会被过分的分割为小的矩形。
若d > D(同性点密度阈值),accepted。否则,需要将R截断。
R截断的方法有两种:“缩小角度误差阈值”与“缩小区域半径”的方法。在这两种方法中,区域中的一部分像素被保留,而另一些则重新标记为NOT USED,因此它们可以在将来的线支持区域中再次使用。
缩小角度容忍阈值:简单的将τ值缩小,再次从当前R的seed开始搜寻,看是否满足要求。
缩小区域半径:逐渐去除远离种子的像素,直到满足标准或区域太小而被拒绝为止。当线支持区域对应于一条曲线时,该方法效果最好,并且在满足密度准则之前需要减少区域,通常意味着对曲线有一定程度的近似。
【9】矩形的改进
如果当前的R仍旧不能满足NFA(要求:NFA(r) ≤ ε),以下的方法将对其进行改进。考虑到在有些情况下,删除line-support region中的一个点会减少R的 length-1个点(想象为对角线)。对不满足NFA的R,采取以下策略:
1.减小p=p/2
2.短边减少一行
3.长边减少一行
4.长边减少另一行
5.减小p=p/2
直至满足NFA(NFA(r) ≤ ε)或 R过小被拒 或 p为原来的1/32。
备注:
[1]二项分布的计算:
[2] 梯度的数学基本概念 https://wenku.baidu.com/view/d3dbe40903d8ce2f00662358.html
导数:场中沿不同方向的变化率
梯度:确定下降最快的方向
转载自:直线段检测算法---LSD:a Line Segment Detector
OpenCV中的demo实现
#include
#include
#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
#endif
// Create and LSD detector with standard or no refinement.
#if 1
Ptr ls = createLineSegmentDetector(LSD_REFINE_STD);
#else
Ptr ls = createLineSegmentDetector(LSD_REFINE_NONE);
#endif
double start = double(getTickCount());
vector lines_std;
// Detect the lines
ls->detect(image, lines_std);
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;
}