Skip to content

Commit f7d1ed8

Browse files
authored
Merge pull request InsightSoftwareConsortium#6017 from hjmjohnson/fix-voronoi-infinite-loop
BUG: Fix infinite loop in VoronoiDiagram2DGenerator
2 parents 1e73a2b + 3b3f3c2 commit f7d1ed8

File tree

3 files changed

+149
-1
lines changed

3 files changed

+149
-1
lines changed

Modules/Segmentation/Voronoi/include/itkVoronoiDiagram2DGenerator.hxx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,56 @@ VoronoiDiagram2DGenerator<TCoordinate>::ConstructDiagram()
273273
buildEdges.push_back(curr);
274274
EdgeInfo front = curr;
275275
EdgeInfo back = curr;
276-
while (!(rawEdges[i].empty()))
276+
// Assemble raw edges into a connected chain for this Voronoi cell.
277+
// Each iteration pops an edge from the deque and attempts to attach
278+
// it to the front or back of the growing chain. Edges that cannot
279+
// attach are pushed back for retry, because later attachments change
280+
// the chain endpoints and may make previously unattachable edges
281+
// attachable.
282+
//
283+
// A stall counter tracks progress: it resets whenever an edge
284+
// attaches, and terminates the loop when a full pass through the
285+
// deque makes no progress. Without this, certain degenerate seed
286+
// configurations (near-collinear seeds, ITK issue #4386) cause an
287+
// infinite loop because Fortune's algorithm produces near-zero-length
288+
// edges whose endpoints differ by less than floating-point tolerance
289+
// but have different vertex IDs. These degenerate edges cannot
290+
// attach because:
291+
// 1. Their endpoints don't match any chain vertex by ID.
292+
// 2. The chain may already be closed (front[0] == back[1]).
293+
// 3. The boundary-bridging logic doesn't apply when the chain
294+
// endpoints are interior (not on the domain boundary).
295+
// Such edges are safely dropped — they represent floating-point
296+
// artifacts where two boundary intersection points should be
297+
// identical in exact arithmetic.
298+
auto remainingBeforeStall = rawEdges[i].size();
299+
while (!(rawEdges[i].empty()) && (remainingBeforeStall != 0))
277300
{
301+
--remainingBeforeStall;
278302
curr = rawEdges[i].front();
279303
rawEdges[i].pop_front();
304+
305+
// Check if this edge is a degenerate near-zero-length artifact.
306+
// Fortune's algorithm can produce edges whose two endpoints map
307+
// to the same geometric point (within DIFF_TOLERENCE) but have
308+
// different vertex IDs. These carry no geometric information
309+
// and can be safely discarded.
310+
const PointType & edgeStart = m_OutputVD->GetVertex(curr[0]);
311+
const PointType & edgeEnd = m_OutputVD->GetVertex(curr[1]);
312+
if (!differentPoint(edgeStart, edgeEnd))
313+
{
314+
itkDebugMacro("Dropping degenerate near-zero-length edge ["
315+
<< curr[0] << " (" << edgeStart[0] << "," << edgeStart[1] << ") -> " << curr[1] << " ("
316+
<< edgeEnd[0] << "," << edgeEnd[1] << ")]"
317+
<< " for cell " << i << ": endpoints within DIFF_TOLERENCE=" << DIFF_TOLERENCE);
318+
// Count as progress — this edge is resolved (discarded).
319+
remainingBeforeStall = rawEdges[i].size();
320+
continue;
321+
}
322+
280323
unsigned char frontbnd = Pointonbnd(front[0]);
281324
unsigned char backbnd = Pointonbnd(back[1]);
325+
bool edgeAttached = true;
282326
if (curr[0] == back[1])
283327
{
284328
buildEdges.push_back(curr);
@@ -353,12 +397,30 @@ VoronoiDiagram2DGenerator<TCoordinate>::ConstructDiagram()
353397
else
354398
{
355399
rawEdges[i].push_back(curr);
400+
edgeAttached = false;
356401
}
357402
}
358403
else
359404
{
360405
rawEdges[i].push_back(curr);
406+
edgeAttached = false;
361407
}
408+
if (edgeAttached)
409+
{
410+
// Progress was made — chain endpoints changed, so previously
411+
// unattachable edges may now be attachable.
412+
remainingBeforeStall = rawEdges[i].size();
413+
}
414+
}
415+
// After assembly, all edges for this cell should have been either
416+
// attached to the chain or identified as degenerate artifacts.
417+
// Any remaining edges indicate an unexpected algorithmic failure.
418+
if (!rawEdges[i].empty())
419+
{
420+
itkExceptionMacro("VoronoiDiagram2DGenerator::ConstructDiagram: "
421+
<< rawEdges[i].size() << " non-degenerate edge(s) could not be "
422+
<< "assembled into cell " << i << " boundary chain. "
423+
<< "This indicates an unexpected geometric configuration.");
362424
}
363425

364426
curr = buildEdges.front();

Modules/Segmentation/Voronoi/test/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ set(
44
itkVoronoiSegmentationImageFilterTest.cxx
55
itkVoronoiSegmentationRGBImageFilterTest.cxx
66
itkVoronoiDiagram2DTest.cxx
7+
itkVoronoiDiagram2DInfiniteLoopTest.cxx
78
itkVoronoiPartitioningImageFilterTest.cxx
89
)
910

1011
createtestdriver(ITKVoronoi "${ITKVoronoi-Test_LIBRARIES}" "${ITKVoronoiTests}")
1112

13+
itk_add_test(
14+
NAME itkVoronoiDiagram2DInfiniteLoopTest
15+
COMMAND
16+
ITKVoronoiTestDriver
17+
itkVoronoiDiagram2DInfiniteLoopTest
18+
)
19+
1220
itk_add_test(
1321
NAME itkVoronoiSegmentationImageFilterTest
1422
COMMAND
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*=========================================================================
2+
*
3+
* Copyright NumFOCUS
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0.txt
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*=========================================================================*/
18+
19+
#include "itkVoronoiDiagram2DGenerator.h"
20+
#include "itkTestingMacros.h"
21+
22+
// Regression test for ITK issue #4386: near-collinear seed configurations
23+
// caused an infinite loop in VoronoiDiagram2DGenerator::ConstructDiagram().
24+
//
25+
// Root cause: Fortune's algorithm produces near-zero-length edges when
26+
// boundary intersection points coincide within floating-point tolerance.
27+
// These degenerate edges have different vertex IDs but geometrically
28+
// identical endpoints (within DIFF_TOLERENCE = 0.001). They cannot be
29+
// attached to the growing boundary chain because:
30+
// 1. Their vertex IDs don't match any chain endpoint.
31+
// 2. The chain may already be closed (front == back).
32+
// 3. Boundary-bridging logic doesn't apply when chain endpoints are
33+
// interior (not on the domain boundary).
34+
// The fix explicitly detects and drops these degenerate edges using the
35+
// existing differentPoint() tolerance check.
36+
int
37+
itkVoronoiDiagram2DInfiniteLoopTest(int argc, char * argv[])
38+
{
39+
if (argc != 1)
40+
{
41+
std::cerr << "Usage: " << itkNameOfTestExecutableMacro(argv) << std::endl;
42+
return EXIT_FAILURE;
43+
}
44+
45+
using VoronoiDiagramType = itk::VoronoiDiagram2D<double>;
46+
using VoronoiGeneratorType = itk::VoronoiDiagram2DGenerator<double>;
47+
using PointType = VoronoiDiagramType::PointType;
48+
49+
// Six near-collinear seeds (x in [-1.40, -1.21]) that produce a
50+
// degenerate Voronoi edge on the left domain boundary where two
51+
// intersection points are ~0.00003 apart.
52+
auto vg = VoronoiGeneratorType::New();
53+
vg->SetOrigin(PointType{ { -1.61569, -1.76726 } });
54+
vg->SetBoundary(PointType{ { 1.60174, 1.76345 } });
55+
vg->AddOneSeed(PointType{ { -1.39649, 0.322212 } });
56+
vg->AddOneSeed(PointType{ { -1.30128, 0.231786 } });
57+
vg->AddOneSeed(PointType{ { -1.21509, 0.0515039 } });
58+
vg->AddOneSeed(PointType{ { -1.22364, -0.030281 } });
59+
vg->AddOneSeed(PointType{ { -1.22125, -0.120815 } });
60+
vg->AddOneSeed(PointType{ { -1.25159, -0.23593 } });
61+
62+
// Without the fix, this call loops forever. With the fix, the
63+
// degenerate near-zero-length edge is detected and dropped, and
64+
// the exception guard ensures no non-degenerate edges are lost.
65+
ITK_TRY_EXPECT_NO_EXCEPTION(vg->Update());
66+
67+
// Verify all 6 cells were constructed with valid boundaries
68+
auto vd = vg->GetOutput();
69+
for (unsigned int i = 0; i < 6; ++i)
70+
{
71+
VoronoiDiagramType::CellAutoPointer cellPtr;
72+
vd->GetCellId(i, cellPtr);
73+
ITK_TEST_EXPECT_TRUE(cellPtr->GetNumberOfPoints() >= 2);
74+
}
75+
76+
std::cout << "Test passed." << std::endl;
77+
return EXIT_SUCCESS;
78+
}

0 commit comments

Comments
 (0)