BUG: Fix infinite loop in VoronoiDiagram2DGenerator#6017
Conversation
dzenanz
left a comment
There was a problem hiding this comment.
Looks good on a glance. It would be good if somebody else reviewed too.
Investigation Report: Unattachable Edges in VoronoiDiagram2DGeneratorExperiment SetupAdded diagnostic instrumentation to
Ran all 8 Voronoi tests with instrumentation. Key Finding: Only 1 Edge Dropped, Only in the Degenerate CaseAll existing Voronoi tests (normal seed configurations) produce zero dropped edges. Only the degenerate infinite-loop test (issue #4386) drops a single edge in cell 2. The Dropped Edge — Detailed AnalysisWhat happened: The chain for cell 2 assembled into a closed loop (front == back == vertex 0) before this edge was processed. The edge connects two points on the left boundary wall that are extremely close together (Δx = 0.00003, Δy = 0.00002). Neither endpoint matches the chain's single vertex, and the boundary-matching logic doesn't apply because the chain endpoints are not on a boundary. Why This Edge is UnattachableThe edge
Is the Edge Redundant?Looking at the final cell boundaries: Vertex 2 is already in cell 2's boundary (attached via a different path). Vertex 5 is in cell 5's boundary. The dropped edge Is the Result Geometrically Correct?The 6 seeds near-collinear along x ≈ -1.2 to -1.4 create Voronoi cells that are extremely elongated. The two vertices Conclusion: The dropped edge is a floating-point artifact. Dropping it produces the correct Voronoi diagram. The stall-detection fix is the right approach — it terminates when no progress can be made, and the only edges dropped are these degenerate near-zero-length artifacts. Verification
|
Fix an infinite loop in ConstructDiagram() that occurs with certain degenerate seed configurations (ITK issue InsightSoftwareConsortium#4386, supersedes PR InsightSoftwareConsortium#5400). Root cause: Fortune's algorithm produces near-zero-length edges when boundary intersection points coincide within floating-point tolerance but receive different vertex IDs. These edges cannot attach to the growing cell boundary chain because: 1. Their vertex IDs don't match any chain endpoint. 2. The chain may already be closed (front == back). 3. Boundary-bridging logic doesn't apply when chain endpoints are interior (not on the domain boundary). The fix has three parts: 1. Explicit degenerate edge detection: before attempting attachment, each edge is checked with the existing differentPoint() tolerance (DIFF_TOLERENCE = 0.001). Edges whose endpoints are geometrically identical are discarded immediately — they carry no geometric information and are floating-point artifacts. 2. Stall detection: a counter tracks assembly progress and resets whenever an edge attaches (since new attachments change the chain endpoints and may make previously unattachable edges attachable). When a full pass through the deque makes no progress, the loop terminates. This improves on PR InsightSoftwareConsortium#5400's simple countdown which terminated too early, breaking itkVoronoiPartitioningImageFilterTest2. 3. Exception on unexpected residuals: after assembly, if any non-degenerate edges remain, itkExceptionMacro fires to ensure unexpected geometric configurations are reported rather than silently producing incomplete diagrams. A regression test using the seed configuration from issue InsightSoftwareConsortium#4386 verifies termination and validates all cell boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0148c2e to
3b3f3c2
Compare
|
| Filename | Overview |
|---|---|
| Modules/Segmentation/Voronoi/test/CMakeLists.txt | Two P1 build-breaking bugs: test source listed twice in ITKVoronoiTests, and itk_add_test registered twice with the same test name |
| Modules/Segmentation/Voronoi/include/itkVoronoiDiagram2DGenerator.hxx | Stall-detection fix and degenerate-edge drop are algorithmically correct; post-loop itkExceptionMacro is a minor concern for unusual valid configurations |
| Modules/Segmentation/Voronoi/test/itkVoronoiDiagram2DInfiniteLoopTest.cxx | Well-written regression test with clear root-cause comments and a concrete seed reproducer for issue #4386 |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Start cell i: remainingBeforeStall = deque.size()"] --> B{"deque empty OR\nremainingBeforeStall == 0?"}
B -- Yes --> G["Post-loop: deque empty?"]
B -- No --> C["--remainingBeforeStall\npop front edge"]
C --> D{"differentPoint check:\nedge is degenerate?"}
D -- "Yes: drop" --> E["remainingBeforeStall = deque.size()\ncontinue"]
E --> B
D -- No --> F{"Try to attach edge\nto front or back of chain"}
F -- Attached --> H["remainingBeforeStall = deque.size()"]
F -- "Not attached" --> I["push_back edge to deque"]
H --> B
I --> B
G -- "Not empty" --> J["itkExceptionMacro:\nunexpected failure"]
G -- Empty --> K["Finalize cell boundary"]
Comments Outside Diff (1)
-
Modules/Segmentation/Voronoi/test/CMakeLists.txt, line 74-79 (link)Duplicate
itk_add_testregistrationitkVoronoiDiagram2DInfiniteLoopTestis already registered at lines 14–19. CMake will fail at configure time with "Cannot add test with already existing name," preventing the project from configuring on any platform. Remove this second block entirely; the first registration is sufficient.
Reviews (1): Last reviewed commit: "BUG: Fix infinite loop in VoronoiDiagram..." | Re-trigger Greptile
Summary
Fix an infinite loop in
VoronoiDiagram2DGenerator::ConstructDiagram()thatoccurs with certain degenerate seed configurations (ITK issue #4386).
Supersedes PR #5400 by @ILoveGoulash with an improved termination strategy
that preserves correctness for all existing tests.
Root Cause
ConstructDiagram()builds Voronoi cell boundaries by assembling raw edgesinto a connected chain. It pops an edge from the deque, tries to attach it
to the front or back of the growing chain, and pushes it back if it cannot
attach. With certain seed configurations, some edges can never be attached
(due to the geometric structure of the Voronoi diagram near boundary
corners), causing the loop to cycle endlessly.
The loop exit condition (
maxStop) was originally present but was neverwired into the
whilecondition. Commit cd97879 (2023) removed it as an"unused variable."
Why PR #5400's fix was insufficient
PR #5400 restored
maxStopas a simple countdown (maxStop = rawEdges[i].size(),decrement each iteration). This limits the loop to one pass through the deque,
but is too aggressive: when an edge successfully attaches, it changes the
chain's front/back endpoints, potentially making previously unattachable edges
now attachable. The simple countdown drops these edges prematurely.
Evidence: With PR #5400's approach,
itkVoronoiPartitioningImageFilterTest2fails (17 pixels differ, mean error 29.1) because edges that would have been
attachable in a subsequent pass were dropped.
This fix
This PR uses a stall-detection strategy instead of a simple countdown:
The counter resets whenever an edge attaches. The loop only terminates when
a full pass through the remaining edges makes zero progress — proving
that no further edges can ever attach. This is both correct (preserves the
algorithm's ability to attach edges in multiple passes) and safe (guaranteed
O(n^2) termination in the worst case).
Test Results
itkVoronoiDiagram2DInfiniteLoopTestitkVoronoiSegmentationImageFilterTestitkVoronoiSegmentationRGBImageFilterTestitkVoronoiDiagram2DTestitkVoronoiPartitioningImageFilterTest1itkVoronoiPartitioningImageFilterTest28/8 Voronoi tests pass with this fix.
AI Assistance
Claude Code (Opus) was used to:
All analysis and changes were reviewed and validated by the commit author.
Testing
Closes #4386.