Skip to content

Commit 4a0be8b

Browse files
committed
Fix column/polygon_points sync on degenerate polygon drop (#1151)
_simplify_polygons now accepts and returns column alongside polygon_points so both stay in sync when degenerate polygons are dropped. Added tests for holes, transform, single-pixel raster, and column/pp length consistency.
1 parent 6f41d92 commit 4a0be8b

File tree

2 files changed

+76
-13
lines changed

2 files changed

+76
-13
lines changed

xrspatial/polygonize.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,8 @@ def _chain_key(chain):
12721272
return (start, end, interior)
12731273

12741274

1275-
def _simplify_polygons(polygon_points, tolerance, method="douglas-peucker"):
1275+
def _simplify_polygons(column, polygon_points, tolerance,
1276+
method="douglas-peucker"):
12761277
"""Topology-preserving simplification of all polygons.
12771278
12781279
Uses shared-edge decomposition: finds junction vertices, splits
@@ -1281,6 +1282,8 @@ def _simplify_polygons(polygon_points, tolerance, method="douglas-peucker"):
12811282
12821283
Parameters
12831284
----------
1285+
column : list
1286+
Pixel values corresponding to each polygon.
12841287
polygon_points : list of list of np.ndarray
12851288
Output of polygonize backend: list of polygons, each polygon
12861289
is [exterior_ring, *hole_rings].
@@ -1292,11 +1295,12 @@ def _simplify_polygons(polygon_points, tolerance, method="douglas-peucker"):
12921295
12931296
Returns
12941297
-------
1295-
list of list of np.ndarray
1296-
Same structure as input, with simplified coordinates.
1298+
(list, list of list of np.ndarray)
1299+
Filtered column and simplified polygon_points. Polygons whose
1300+
exterior ring collapses below 4 vertices are dropped from both.
12971301
"""
12981302
if tolerance <= 0:
1299-
return polygon_points
1303+
return column, polygon_points
13001304

13011305
# Step 1: Find junctions.
13021306
junctions = _find_junctions(polygon_points)
@@ -1330,6 +1334,7 @@ def _simplify_polygons(polygon_points, tolerance, method="douglas-peucker"):
13301334
ring_info.append(poly_info)
13311335

13321336
# Step 4: Reassemble rings.
1337+
result_column = []
13331338
result = []
13341339
for poly_idx, rings in enumerate(polygon_points):
13351340
new_rings = []
@@ -1371,9 +1376,10 @@ def _simplify_polygons(polygon_points, tolerance, method="douglas-peucker"):
13711376
for ring in new_rings[1:]:
13721377
if len(ring) >= 4:
13731378
filtered.append(ring)
1379+
result_column.append(column[poly_idx])
13741380
result.append(filtered)
13751381

1376-
return result
1382+
return result_column, result
13771383

13781384

13791385
def _merge_polygon_rings(polys_list):
@@ -1608,8 +1614,9 @@ def polygonize(
16081614

16091615
# Apply simplification if requested.
16101616
if simplify_tolerance is not None and simplify_tolerance > 0:
1611-
polygon_points = _simplify_polygons(
1612-
polygon_points, simplify_tolerance, method=simplify_method)
1617+
column, polygon_points = _simplify_polygons(
1618+
column, polygon_points, simplify_tolerance,
1619+
method=simplify_method)
16131620

16141621
# Convert to requested return_type.
16151622
if return_type == "numpy":

xrspatial/tests/test_polygonize.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,8 @@ def test_simplify_polygons_reduces_vertices(self):
593593
data = xr.DataArray(raster)
594594
column_orig, pp_orig = polygonize(data, return_type="numpy")
595595

596-
pp_simplified = _simplify_polygons(pp_orig, tolerance=1.5)
596+
_, pp_simplified = _simplify_polygons(
597+
column_orig, pp_orig, tolerance=1.5)
597598

598599
# Vertex count should be reduced for at least one polygon.
599600
orig_total_verts = sum(
@@ -624,7 +625,7 @@ def test_simplify_polygons_topology_preserved(self):
624625
data = xr.DataArray(raster)
625626
column, pp = polygonize(data, return_type="numpy")
626627

627-
pp_simplified = _simplify_polygons(pp, tolerance=0.0)
628+
_, pp_simplified = _simplify_polygons(column, pp, tolerance=0.0)
628629

629630
# With tolerance=0, no vertices should be removed; output
630631
# should match input exactly.
@@ -648,7 +649,7 @@ def test_simplify_polygons_shared_edge_identical(self):
648649
data = xr.DataArray(raster)
649650
column, pp = polygonize(data, return_type="numpy")
650651

651-
pp_simplified = _simplify_polygons(pp, tolerance=1.5)
652+
_, pp_simplified = _simplify_polygons(column, pp, tolerance=1.5)
652653

653654
# Check total area is preserved (which requires no gaps/overlaps).
654655
total_orig = sum(
@@ -708,12 +709,28 @@ def test_simplify_polygons_drops_degenerate(self):
708709

709710
# Find the small polygon (value 1, single pixel).
710711
small_idx = column.index(1)
711-
small_poly = [pp[small_idx]] # wrap in list for _simplify_polygons
712+
small_col = [column[small_idx]]
713+
small_poly = [pp[small_idx]]
712714

713715
# Large tolerance should collapse the single-pixel polygon.
714-
result = _simplify_polygons(small_poly, tolerance=10.0)
716+
result_col, result_pp = _simplify_polygons(
717+
small_col, small_poly, tolerance=10.0)
718+
# Column and polygon_points must stay in sync.
719+
assert len(result_col) == len(result_pp)
715720
# The polygon should be dropped since its exterior collapses.
716-
assert len(result) == 0 or all(len(r[0]) >= 4 for r in result)
721+
assert len(result_pp) == 0 or all(len(r[0]) >= 4 for r in result_pp)
722+
723+
def test_simplify_column_pp_sync_through_public_api(self):
724+
"""Public API must keep column and polygon_points in sync."""
725+
# Raster with a single-pixel region that may collapse.
726+
raster = np.array([
727+
[1, 2, 2],
728+
[2, 2, 2],
729+
[2, 2, 2],
730+
], dtype=np.int64)
731+
data = xr.DataArray(raster)
732+
col, pp = polygonize(data, simplify_tolerance=10.0)
733+
assert len(col) == len(pp)
717734

718735

719736
class TestPolygonizeSimplify:
@@ -879,6 +896,45 @@ def test_simplify_with_awkward(self):
879896
return_type="awkward")
880897
assert result is not None
881898

899+
def test_simplify_with_holes(self):
900+
"""Simplification should handle polygons with interior holes."""
901+
raster = np.zeros((8, 8), dtype=np.int32)
902+
raster[1:7, 1:7] = 1
903+
raster[3:5, 3:5] = 0 # hole in the 1-region
904+
data = xr.DataArray(raster)
905+
906+
col_orig, pp_orig = polygonize(data)
907+
col_simp, pp_simp = polygonize(data, simplify_tolerance=0.5)
908+
909+
assert len(col_simp) == len(pp_simp)
910+
# Area should be preserved.
911+
total_orig = sum(
912+
sum(calc_boundary_area(r) for r in rings) for rings in pp_orig)
913+
total_simp = sum(
914+
sum(calc_boundary_area(r) for r in rings) for rings in pp_simp)
915+
assert_allclose(total_simp, total_orig, atol=1e-10)
916+
917+
def test_simplify_with_transform(self):
918+
"""Simplification should work with affine transform."""
919+
raster = np.array([
920+
[1, 1, 2, 2],
921+
[1, 1, 2, 2],
922+
[1, 1, 2, 2],
923+
], dtype=np.int64)
924+
transform = np.array([10.0, 0.0, 100.0, 0.0, 10.0, 200.0])
925+
data = xr.DataArray(raster)
926+
col, pp = polygonize(data, transform=transform,
927+
simplify_tolerance=5.0)
928+
assert len(col) == len(pp)
929+
assert len(col) == 2
930+
931+
def test_simplify_single_pixel_raster(self):
932+
"""Simplification on a 1x1 raster should not crash."""
933+
raster = np.array([[5]], dtype=np.int64)
934+
data = xr.DataArray(raster)
935+
col, pp = polygonize(data, simplify_tolerance=1.0)
936+
assert len(col) == len(pp)
937+
882938

883939
@pytest.mark.skipif(da is None, reason="dask not installed")
884940
class TestPolygonizeSimplifyDask:

0 commit comments

Comments
 (0)