One sweep, all segments. A one-pass, O(width)-memory line segment detector
with an integer-only streaming core.
Full-HD photo, 2980 segments in ~11 ms (one detector sweep, single thread).
The idea. Most line segment detectors hold the whole image (and several
full-size intermediates) in memory and revisit pixels many times. SweepLSD instead treats the
image as a row stream: every stage — smoothing, gradient, edge thinning, endpoint
detection, labelling, even the final line test — is expressed so that it only ever needs
a few rows of state. The image flows through once, top to bottom, and segments fall out
as their last pixel passes by. That is what makes the design FPGA-friendly (the algorithm was
designed for hardware in a 2014 master's thesis, where it was called OPLSD), and it is
also why it is fast on CPUs: the working set stays in cache.
1. The pipeline
Five stages, each a simple row kernel. The figures below are the actual intermediates
(sweeplsd_dump_stages) on a 960×540 photo.
Gaussian. Separable 5×5 blur with weights {16,64,96,64,16}, all integer, one
final >>10 rescale. The vertical pass fits exactly in uint16, so the compiler
auto-vectorizes it 16 lanes wide.
Gradient. A 2×2 operator on the smoothed image; power = (|dx|+|dy|+1)/2 and a
direction quantised to just horizontal or vertical — that is all the NMS needs.
Edge. Threshold on the power plus non-maximum suppression along the dominant
gradient axis, branch-free.
Endpoint candidates. A 5×5 ring test marks edge pixels where a segment starts,
ends, branches or corners. A candidate is confirmed only where two are consecutive and
the inner one is kept — a deterministic rule that also lands the recovered tips ≈1 px
inside the true endpoint (a correctable extent bias; see the
quality page). These delimit the pixel runs that may become
segments.
Labelling + judgment. A row-streaming connected-component labeller carries, per
label, running scatter moments (Σx, Σy, Σx², Σxy, Σy², count). When a run closes —
its second endpoint-candidate contact — it is judged on the spot: a segment is emitted iff
it has enough pixels and the PCA eigenvalue ratio of its scatter says "thin and straight".
The moments also give sub-pixel endpoints and the line direction for free.
2. The one-pass architecture
The library ships two drivers over the same kernels, tested to produce identical
output:
driver
what it does
memory
speed (Full-HD)
detect()
one full-image pass per stage — easiest to read and debug
O(pixels)
~12 ms
detectOnePass()
single top-to-bottom sweep; each stage keeps only the rows the next stage needs; labelling trails the input by 7 rows
O(width)
~11 ms
The per-pixel core is integer-only (the thesis targeted an FPGA datapath);
floating point appears only in the once-per-segment finalization (PCA, endpoint projection).
There are no SIMD intrinsics anywhere — the kernels are written so GCC/Clang
auto-vectorize them, which keeps the code readable and portable.
The hardware-friendliness is not hypothetical: the detector
runs live on a 2009-era FPGA — HDMI in → detect → overlay out at
1080p30, with no frame buffer.
How the labelling stays O(width). The connected-component labeller is a
single-pass union–find (with path compression) that merges edge runs touching across
adjacent rows. What keeps its memory bounded is label recycling: labels live in a
fixed pool addressed through a ring free-list, split into a hot record (moment
accumulation for active runs) and a cold record (dormant). The instant a component can no longer
be touched — its last edge row has passed above the scan line — an end-of-row retire sweep frees
its slot for reuse. The number of live labels is therefore bounded by the image
width, not its pixel count: the working set is ~15 KB and stays in cache, where a
grow-forever label table would reach tens of thousands of slots (megabytes) on a dense frame. The
pool is sized to width/4 and grows to width/2 only on abnormal inputs; a correct image never
exceeds it (in hardware, overflow drops labels rather than allocating). This is what turns "a few
rows of pixels" into a genuine O(width) memory bound end to end.
3. Measured refinements (all on by default)
Params{} is SweepLSD as published: the refinements below are all enabled, and
each was added only after measuring its effect on the three evaluation axes (synthetic-GT
F-score, real-photo inspection, downstream vanishing points). The right-hand column states
which way each one moved the needle; the measurements behind it are on the
quality and vanishing-point
pages. Each can be disabled individually, and Params::original2014() reproduces
the 2014 thesis implementation's behaviour, so any row can be reproduced by turning it off:
improvement
what
measured effect
strict NMS tie-break
gradient plateaus thin to 1 px instead of 2
cleaner edges, fewer duplicate labels
sub-pixel NMS
parabolic peak interpolation accumulated into the moments
lateral error ↓
streaming hysteresis
low extraction threshold + per-label strong-pixel requirement; the low threshold adapts to the image noise floor (O(1) state)
recall ↑ on clean images without flooding noisy ones
endpoints from projection extremes
bounding-box corners projected on the fitted axis
robust when a label has >2 endpoint contacts
curve rejection
absolute bound on the perpendicular RMS spread
circles/arcs rejected by design (see the isotropy page)
half-pixel lattice shift
the 2×2 gradient lives on pixel corners; output is shifted +0.5 px (canonical LSD applies the same correction)
All refinements preserve the streaming / O(width) property. One optional feature is
off by default: a gap-tolerant collinear linker (link_collinear) that
re-assembles fragments across junction cuts and noise breaks — its measured effect is
reported on the quality and
vanishing-point pages, always explicitly labelled.
Known limitation, stated plainly. SweepLSD's edge model is
contrast-gated: a pixel becomes an edge only if its gradient peak clears a threshold.
Soft, wide luminance ramps (dim wall corners, defocused structure) never form a peak, so
LSD / EDLines / ELSED — which link pixels by orientation coherence instead — recover some
low-contrast structure SweepLSD misses. The quality and vanishing-point pages quantify
exactly when this matters and when it does not.