简介:面向图像处理与机器视觉方向的研究者、开发者及高校学生,这份 MATLAB 直线检测工具箱围绕 LSD(Line Segment Detector)算法展开,用于在图像中快速、鲁棒地提取直线段,可服务于自动驾驶、无人机导航、工业检测等依赖线段特征的场景。压缩包共 186 个文件,约 19.28MB,以 38 个 .m 脚本、41 个 .c 与 22 个 .h 源码、59 张 png 示例图为主,另有 cpp、html 文档及 txt 配置说明,兼顾算法实现、参数配置与结果可视化演示。已有 1011 人学习下载。使用时可从 lsd.m 主函数入手,配合 reco_toolbox 目录下的 readme.txt 调整线段长度、角度阈值等参数,示例图便于对照不同设置下的检测效果;底层 C 源码的保留也为理解算法细节与二次开发提供了入口,适合具备一定 MATLAB 基础、希望快速集成直线检测能力的读者。
1. 一条斜边被拆成五段之后,我才回头去读 LSD 的原理
在一套金属件的视觉检测流程里,我把边缘拟合从霍夫换成了 LSD 直线检测,用的是编译好的 matlab 工具箱接口。同一张 1200×900 的灰度图上,霍夫给了四十多条线,一半是表面纹理;LSD 只吐了九条,全部贴着真实棱边。正当我准备收工,问题来了:一段 120 像素的斜倒角被拆成了五段,坐标还整体偏了一个像素,后续算角度时误差直接翻倍。LSD 的卖点就在“不用手动设阈值、一条直线尽量出一条线段”,拆断和偏移说明我根本没用对。这篇笔记按我当时的顺序来:LSD 在像素上到底算哪几步、怎么把它接进 MATLAB、七个参数里哪几个真的天天动、翻车现场怎么排查。适合手上已经有 C 版实现、想批量跑图的人,也适合还在霍夫和 LSD 之间犹豫的人。
2. 梯度排序、线支持区域与 NFA:LSD 在图像上其实只做三件事
2.1 线支持区域生长:LSD 先找“方向一致的像素团”,再把它拟合成矩形
LSD 的核心假设只有一条:如果一段像素的梯度方向高度一致,并且它们能被一个细长矩形包住、矩形内像素密度足够高,那么这段像素大概率是人为画出来的一条边。注意这里说的是梯度方向,不是边缘方向,两者相差 90 度,写代码时搞反,区域会朝着垂直于棱边的方向长,长出来全是一团一团的斑点。
具体流程分六步。第一步按scale把图缩小,这一步不是可选项,原实现的默认值就小于 1,目的是让噪声和细纹理在重采样时被抹掉一部分。第二步用相邻差分算梯度,注意是G(:,2:end)-G(:,1:end-1)这种一阶差分,不是gradient的中心差分,后者会把边缘抹宽两个像素,拟合出来的线段两端会飘。第三步把梯度幅值做伪排序,原实现用 1024 个 bin 分箱,避免了对全图做完整排序的开销。第四步从当前幅值最大且未使用的像素出发,做 8 邻域生长,邻居的梯度方向和种子方向夹角小于ang_th就并入区域,同时标记为已用。第五步对区域内的像素做惯性矩分析,求出主轴方向、长度、宽度,把像素投影到主轴和法向上,得到外接矩形。第六步做 Helmholtz 检验,用 NFA 判断这个矩形“像不像人为画出来的”,不通过就丢弃。
这套流程和霍夫的区别很实在。霍夫在参数空间投票,你需要给峰值阈值、给最小投票数、还要处理同一条线在参数空间分裂成多个峰;LSD 把判断交给统计检验,没有“投多少票算线”这种人为门槛。代价是 LSD 对噪声纹理同样敏感,只是它用 NFA 把纹理段筛掉了,而不是用阈值。
伪排序这一步经常被忽略。它的作用不是加速,而是决定生长顺序:幅值大的像素先长,长出来的区域更贴近强边缘;如果反过来从小幅值开始长,纹理和噪声会先占住大片区域,强边缘反而长不起来。所以quant这个参数虽然看起来无关紧要,实际是在控制“多大的幅值差会被当成同一档”。
2.2 用 MATLAB 写一个能跑的 lsdLite:从区域生长到矩形拟合
真正的 C 实现有上千行,但骨架可以压到八十行。下面这段是我用来验证理解的教学版,不做性能优化,只保证每一步和原算法对得上,你可以拿它对照着读 C 源码,也可以拿它当没有编译器时的降级方案。
function [segs, lognfa] = lsdLite(I, opts) % LSDFLITE 教学版 LSD:梯度伪排序 + 线支持区域生长 + 矩形拟合 + NFA % I : 灰度图(double 0~1 或 uint8 0~255) % opts : 可选结构体,缺省值见下 if nargin < 2, opts = struct(); end if ~isfield(opts,'scale'), opts.scale = 0.8; end if ~isfield(opts,'sigma_scale'), opts.sigma_scale = 0.6; end if ~isfield(opts,'ang_th'), opts.ang_th = 22.5; end % 度 if ~isfield(opts,'density_th'), opts.density_th = 0.7; end if ~isfield(opts,'min_len'), opts.min_len = 10; end if ~isfield(opts,'log_eps'), opts.log_eps = 0; end I = double(I); if max(I(:)) > 1, I = I / 255; end s = opts.scale; if s ~= 1 I = imresize(I, s, 'bilinear'); end G = imgaussfilt(I, opts.sigma_scale / s); [h, w] = size(G); gx = zeros(h, w); gy = zeros(h, w); gx(:, 1:w-1) = G(:, 2:w) - G(:, 1:w-1); % 一阶差分,别用 gradient gy(1:h-1, :) = G(2:h, :) - G(1:h-1, :); mag = hypot(gx, gy); theta = mod(atan2(gy, gx), pi); % 梯度方向折叠到 [0,pi) [~, order] = sort(mag(:), 'descend'); % 真排序,原实现是分箱伪排序 used = false(h, w); segs = zeros(0, 4); lognfa = zeros(0, 1); nb = [-1 0; 1 0; 0 -1; 0 1; -1 -1; -1 1; 1 -1; 1 1]; angTol = opts.ang_th * pi / 180; for k = 1:numel(order) p0 = order(k); if used(p0) || mag(p0) < 1e-8, continue; end ref = theta(p0); queue = p0; head = 1; used(p0) = true; xs = []; ys = []; while head <= numel(queue) p = queue(head); head = head + 1; [py, px] = ind2sub([h w], p); xs(end+1) = px; ys(end+1) = py; %#ok<AGROW> for t = 1:8 yy = py + nb(t,1); xx = px + nb(t,2); if yy < 1 || yy > h || xx < 1 || xx > w, continue; end q = sub2ind([h w], yy, xx); if used(q), continue; end d = abs(theta(q) - ref); d = min(d, pi - d); % 角度差按圆周算 if d < angTol used(q) = true; queue(end+1) = q; %#ok<AGROW> end end end n = numel(xs); cx = mean(xs); cy = mean(ys); mxx = mean((xs-cx).^2); myy = mean((ys-cy).^2); mxy = mean((xs-cx).*(ys-cy)); phi = 0.5 * atan2(2*mxy, mxx - myy); % 主惯性轴方向 u = (xs-cx)*cos(phi) + (ys-cy)*sin(phi); v = -(xs-cx)*sin(phi) + (ys-cy)*cos(phi); len = max(u) - min(u) + 1; wid = max(v) - min(v) + 1; if len < opts.min_len || len < 2*wid, continue; end if n / (len*wid) < opts.density_th, continue; end lnfa = logNfaBinom(n, len*wid, angTol/pi, numel(I)); if lnfa > opts.log_eps, continue; end um = (max(u)+min(u))/2; vm = (max(v)+min(v))/2; bx = cx + um*cos(phi) - vm*sin(phi); by = cy + um*sin(phi) + vm*cos(phi); dx = cos(phi)*len/2; dy = sin(phi)*len/2; segs(end+1, :) = ([bx-dx, by-dy, bx+dx, by+dy] - 1) / s + 1; %#ok<AGROW> lognfa(end+1, 1) = lnfa; end end function v = logNfaBinom(n, npix, p, N) % 二项分布尾概率的 log10;n 为对齐像素数,npix 为矩形面积 k = min(max(round(npix), round(n)), 1e5); s = -inf; for j = n:k t = gammaln(k+1) - gammaln(j+1) - gammaln(k-j+1) + ... j* <p> <a href="https://download.csdn.net/download/subin0403/9368390" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>