BUG: Make itkBresenhamLine::BuildLine finish at the correct end point#6024
Conversation
There was a problem hiding this comment.
Thank you for contributing a pull request! 🙏
Welcome to the ITK community! 🤗👋☀️
We are glad you are here and appreciate your contribution. Please keep in mind our community participation guidelines. 📜
More support and guidance on the contribution process can be found in our contributing guide. 📖
This is an automatic message. Allow for time for the ITK community to be able to read the pull request and comment
on it.
Test-before-fix verificationConfirmed that the new The Existing tests ( |
Algorithm review: comparison with reference implementations and correctness analysisClassic Bresenham propertiesThe classic Bresenham line algorithm (1965) is integer-only and guarantees hitting both endpoints exactly. It tracks an integer error term, steps one pixel at a time along the dominant axis, and decides whether to step in secondary axes based on accumulated error. The N-dimensional generalization preserves this: identify the dominant axis (largest absolute displacement), step along it, accumulate integer error terms for each of the other N-1 axes. How other libraries handle this
The common pattern: the integer-endpoint-to-endpoint formulation is primary. Direction+length overloads are convenience wrappers, not the other way around. ITK inverts this — Current ITK bug analysisIn
Test results on current
PR #6024 fix assessmentThe fix is mathematically sound for the forward case:
|
Integer-only Bresenham for
|
| Test | What it verifies |
|---|---|
AxisAligned2D |
Horizontal/vertical lines hit exact endpoints, stay on-axis |
SinglePixelLine |
p0 == p1 returns exactly 1 pixel |
Connectivity2D |
All consecutive pixels are 8-connected (Chebyshev ≤ 1) |
Connectivity3D |
All consecutive pixels are 26-connected (Chebyshev ≤ 1) |
PixelCountMatchesChebyshev |
Pixel count = Chebyshev distance + 1 for various cases |
ReverseSymmetry |
Forward/reverse lines have same length, correct endpoints |
SteepDiagonal3D |
The original bug case {0,0,0}→{250,250,1} in both directions |
All 11 tests pass (4 pre-existing + 7 new).
7efbaf5 to
9e1a748
Compare
Performance analysis:
|
| Benchmark | main (ms/call) | PR (ms/call) | Speedup |
|---|---|---|---|
BuildLine(Index,Index) 2D 1000×500 |
0.0071 | 0.0052 | 1.37× |
BuildLine(Index,Index) 2D 10×10 |
0.000167 | 0.000100 | 1.67× |
BuildLine(Index,Index) 3D 500×300×200 |
0.0046 | 0.0030 | 1.56× |
BuildLine(Index,Index) 3D 250×250×1 |
0.0029 | 0.0021 | 1.38× |
BuildLine(Index,Index) 3D reverse |
0.0029 | 0.0021 | 1.38× |
BuildLine(Dir,len) 2D len=1001 |
0.0050 | 0.0049 | ~same |
BuildLine(Dir,len) 3D len=501 |
0.0034 | 0.0034 | ~same |
| 1000 short 3D lines (batch) | 0.1737 | 0.0845 | 2.05× |
Summary
BuildLine(Index, Index)is 1.4–2× faster due to eliminating floatPointallocation, direction vector normalization (sqrt+ division), andfloat→introundingBuildLine(Direction, length)is unchanged (expected — same code path)- Batch of 1000 short lines shows the largest gain (2×) since per-call overhead of float conversion dominates for short lines
- No regressions in any benchmark
9d13a6b to
76e2e15
Compare
Add 16 GTest cases covering endpoint accuracy, connectivity, symmetry,
axis-aligned lines, single-pixel lines, negative coordinates, steep
diagonals, all octants, duplicate detection, and a very-long-line
stress test.
The FinishAtEndIndex test demonstrates the existing bug: BuildLine
with start={0,0,0}, end={250,250,1} overshoots to {251,251,0} because
LastIndex is computed using Chebyshev distance instead of actual
Euclidean line length. This test is expected to fail on unfixed code.
The ReverseSymmetry test verifies that forward and reverse lines have
the same length and hit exact endpoints. Pixel-by-pixel reversal
symmetry is NOT guaranteed by integer Bresenham for all directions
due to error accumulator tie-breaking differences.
Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
Replace the floating-point delegation through BuildLine(Direction,
length) with a direct integer-only N-dimensional Bresenham that
guarantees exact start and end points by construction.
The previous implementation converted integer endpoints to a
floating-point direction vector, computed Chebyshev distance for
the line length, then reconstructed LastIndex via rounding. This
introduced endpoint error for lines where the dominant-axis
projection differed from the Euclidean length (e.g., {0,0,0} to
{250,250,1} would overshoot to {251,251,0}).
The integer-only algorithm:
- Computes per-axis displacements and step directions (+1/-1)
- Identifies the dominant axis (largest absolute displacement)
- Uses integer error accumulators (2*absDelta per step, overflow
at maxAbsDelta) to decide secondary-axis steps
- Produces exactly Chebyshev_distance + 1 pixels
- Guarantees the line starts at p0 and ends at p1
BuildLine(Direction, length) now computes LastIndex from the
direction vector using Euclidean (not Chebyshev) length for
accurate rounding, then delegates to BuildLine(Index, Index).
Performance improvement: 1.4-2x faster for BuildLine(Index, Index)
due to eliminating float Point allocation, direction normalization
(sqrt + division), and float-to-int rounding.
Reference: classic Bresenham extended to N-D as used by scikit-image
(line_nd), OpenCV (LineIterator), and VTK (vtkLine).
Co-Authored-By: Hans J. Johnson <hans-johnson@uiowa.edu>
Branch reorganizationReorganized the 6 commits into 2 clean concept-based commits on
Original 6 commits backed up at Review comment resolutions
Authorship
Test results18/18 BresenhamLine GTest cases pass (0.14 sec total). |
8bdf78e to
59e3c06
Compare
|
@slavust THANK YOU for your attention today. Now that the changes are all passing and meeting your requirements, I have rebased and reorganized to keep the git history as clean as possible ( and improve git bisect performance). |
|
| Filename | Overview |
|---|---|
| Modules/Core/Common/include/itkBresenhamLine.hxx | Rewrites BuildLine(p0,p1) as integer-only N-D Bresenham (correct) and refactors BuildLine(Direction,length) to delegate to it; single-precision float for euclideanLineLen may lose precision for very large lines |
| Modules/Core/Common/test/itkBresenhamLineGTest.cxx | Comprehensive new tests covering 8 octants, connectivity, uniqueness, axis-aligned lines, negative coordinates, steep/shallow slopes, and very long lines — good coverage of the bug fix and edge cases |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["BuildLine(Direction, length)"] --> B["Normalize Direction\nFind maxDistanceDimension"]
B --> C["euclideanLineLen = (length-1) / |Dir[max]|"]
C --> D["LastIndex[i] = RoundHalfIntegerUp(euclideanLineLen * Dir[i])"]
D --> E["BuildLine(StartIndex=0, LastIndex)"]
E --> F
G["BuildLine(p0, p1)"] --> F["Compute absDeltas, step\nFind mainAxis, maxAbsDelta"]
F --> H["numPixels = maxAbsDelta + 1"]
H --> I["Loop s = 0..numPixels-1"]
I --> J["push currentIndex"]
J --> K["Advance mainAxis by step[mainAxis]"]
K --> L["For each secondary axis:\naccumError += 2*absDelta\nif accum >= maxAbsDelta: index++, accum -= 2*maxAbsDelta"]
L --> M{s < numPixels-1?}
M -- yes --> I
M -- no --> N["Return indices\n(first = p0, last = p1)"]
Reviews (1): Last reviewed commit: "BUG: Implement integer-only Bresenham fo..." | Re-trigger Greptile
…putation Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
bf21b39 to
b7248b1
Compare
Overload of the BuildLine(Index, Index) function could end at index which is different from specified one. The issue was that when reconstructing LastIndex from the direction vector we didn't use actual Euclidean line length, but used Chebyshev distance instead. This led to rounding to integers closer to the line start, leading to bigger deviation at the line end.
The alternative approach would be to move the algorithm implementation to the BuildLine(Index, Index) and work fully on integers there, but this would require more code changes.
UPDATE: AI agent actually suggested code with algorithm implementation in BuildLine(Index, Index) overload. Since AI is allowed to do such modification, I'll proceed with it.
Bresenham line algorithm works on integer indices, and we cannot avoid rounding errors while having floating-point direction. But it's still possible to always finish at the user-specified point in the second function overload.
PR Checklist
Refer to the ITK Software Guide for
further development details if necessary.