Skip to content

Commit 5934cc8

Browse files
authored
Merge pull request #6052 from hjmjohnson/backport-voronoi-fix
BUG: Backport Voronoi infinite loop fix to release-5.4
2 parents f8b7abf + 6b157cf commit 5934cc8

3 files changed

Lines changed: 153 additions & 7 deletions

File tree

Modules/Segmentation/Voronoi/include/itkVoronoiDiagram2DGenerator.hxx

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,6 @@ VoronoiDiagram2DGenerator<TCoordRepType>::ConstructDiagram()
272272
EdgeInfo curr1;
273273
EdgeInfo curr2;
274274

275-
unsigned char frontbnd;
276-
unsigned char backbnd;
277275
std::vector<IdentifierType> cellPoints;
278276
for (unsigned int i = 0; i < m_NumberOfSeeds; ++i)
279277
{
@@ -283,12 +281,56 @@ VoronoiDiagram2DGenerator<TCoordRepType>::ConstructDiagram()
283281
buildEdges.push_back(curr);
284282
EdgeInfo front = curr;
285283
EdgeInfo back = curr;
286-
while (!(rawEdges[i].empty()))
284+
// Assemble raw edges into a connected chain for this Voronoi cell.
285+
// Each iteration pops an edge from the deque and attempts to attach
286+
// it to the front or back of the growing chain. Edges that cannot
287+
// attach are pushed back for retry, because later attachments change
288+
// the chain endpoints and may make previously unattachable edges
289+
// attachable.
290+
//
291+
// A stall counter tracks progress: it resets whenever an edge
292+
// attaches, and terminates the loop when a full pass through the
293+
// deque makes no progress. Without this, certain degenerate seed
294+
// configurations (near-collinear seeds, ITK issue #4386) cause an
295+
// infinite loop because Fortune's algorithm produces near-zero-length
296+
// edges whose endpoints differ by less than floating-point tolerance
297+
// but have different vertex IDs. These degenerate edges cannot
298+
// attach because:
299+
// 1. Their endpoints don't match any chain vertex by ID.
300+
// 2. The chain may already be closed (front[0] == back[1]).
301+
// 3. The boundary-bridging logic doesn't apply when the chain
302+
// endpoints are interior (not on the domain boundary).
303+
// Such edges are safely dropped — they represent floating-point
304+
// artifacts where two boundary intersection points should be
305+
// identical in exact arithmetic.
306+
auto remainingBeforeStall = rawEdges[i].size();
307+
while (!(rawEdges[i].empty()) && (remainingBeforeStall != 0))
287308
{
309+
--remainingBeforeStall;
288310
curr = rawEdges[i].front();
289311
rawEdges[i].pop_front();
290-
frontbnd = Pointonbnd(front[0]);
291-
backbnd = Pointonbnd(back[1]);
312+
313+
// Check if this edge is a degenerate near-zero-length artifact.
314+
// Fortune's algorithm can produce edges whose two endpoints map
315+
// to the same geometric point (within DIFF_TOLERENCE) but have
316+
// different vertex IDs. These carry no geometric information
317+
// and can be safely discarded.
318+
const PointType & edgeStart = m_OutputVD->GetVertex(curr[0]);
319+
const PointType & edgeEnd = m_OutputVD->GetVertex(curr[1]);
320+
if (!differentPoint(edgeStart, edgeEnd))
321+
{
322+
itkDebugMacro("Dropping degenerate near-zero-length edge ["
323+
<< curr[0] << " (" << edgeStart[0] << "," << edgeStart[1] << ") -> " << curr[1] << " ("
324+
<< edgeEnd[0] << "," << edgeEnd[1] << ")]"
325+
<< " for cell " << i << ": endpoints within DIFF_TOLERENCE=" << DIFF_TOLERENCE);
326+
// Count as progress — this edge is resolved (discarded).
327+
remainingBeforeStall = rawEdges[i].size();
328+
continue;
329+
}
330+
331+
unsigned char frontbnd = Pointonbnd(front[0]);
332+
unsigned char backbnd = Pointonbnd(back[1]);
333+
bool edgeAttached = true;
292334
if (curr[0] == back[1])
293335
{
294336
buildEdges.push_back(curr);
@@ -357,20 +399,38 @@ VoronoiDiagram2DGenerator<TCoordRepType>::ConstructDiagram()
357399
else
358400
{
359401
rawEdges[i].push_back(curr);
402+
edgeAttached = false;
360403
}
361404
}
362405
else
363406
{
364407
rawEdges[i].push_back(curr);
408+
edgeAttached = false;
365409
}
410+
if (edgeAttached)
411+
{
412+
// Progress was made — chain endpoints changed, so previously
413+
// unattachable edges may now be attachable.
414+
remainingBeforeStall = rawEdges[i].size();
415+
}
416+
}
417+
// After assembly, all edges for this cell should have been either
418+
// attached to the chain or identified as degenerate artifacts.
419+
// Any remaining edges indicate an unexpected algorithmic failure.
420+
if (!rawEdges[i].empty())
421+
{
422+
itkExceptionMacro("VoronoiDiagram2DGenerator::ConstructDiagram: "
423+
<< rawEdges[i].size() << " non-degenerate edge(s) could not be "
424+
<< "assembled into cell " << i << " boundary chain. "
425+
<< "This indicates an unexpected geometric configuration.");
366426
}
367427

368428
curr = buildEdges.front();
369429
curr1 = buildEdges.back();
370430
if (curr[0] != curr1[1])
371431
{
372-
frontbnd = Pointonbnd(curr[0]);
373-
backbnd = Pointonbnd(curr1[1]);
432+
unsigned char frontbnd = Pointonbnd(curr[0]);
433+
unsigned char backbnd = Pointonbnd(curr1[1]);
374434
if ((frontbnd != 0) && (backbnd != 0))
375435
{
376436
if (frontbnd == backbnd)

Modules/Segmentation/Voronoi/test/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ set(ITKVoronoiTests
33
itkVoronoiSegmentationImageFilterTest.cxx
44
itkVoronoiSegmentationRGBImageFilterTest.cxx
55
itkVoronoiDiagram2DTest.cxx
6+
itkVoronoiDiagram2DInfiniteLoopTest.cxx
67
itkVoronoiPartitioningImageFilterTest.cxx)
78

89
createtestdriver(ITKVoronoi "${ITKVoronoi-Test_LIBRARIES}" "${ITKVoronoiTests}")
910

11+
itk_add_test(
12+
NAME
13+
itkVoronoiDiagram2DInfiniteLoopTest
14+
COMMAND
15+
ITKVoronoiTestDriver
16+
itkVoronoiDiagram2DInfiniteLoopTest)
17+
1018
itk_add_test(
1119
NAME
1220
itkVoronoiSegmentationImageFilterTest
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)