@@ -1002,6 +1002,109 @@ def _douglas_peucker(coords, tolerance):
10021002 return result
10031003
10041004
1005+ @ngjit
1006+ def _visvalingam_whyatt (coords , tolerance ):
1007+ """Visvalingam-Whyatt area-based line simplification.
1008+
1009+ Iteratively removes the vertex that forms the smallest triangle
1010+ area with its neighbors, until no triangle area is below tolerance.
1011+ Endpoints are always preserved.
1012+
1013+ Parameters
1014+ ----------
1015+ coords : np.ndarray, shape (N, 2)
1016+ Input coordinate array.
1017+ tolerance : float
1018+ Minimum triangle area threshold. Vertices forming triangles
1019+ with area below this value are removed.
1020+
1021+ Returns
1022+ -------
1023+ np.ndarray, shape (M, 2)
1024+ Simplified coordinate array.
1025+ """
1026+ n = len (coords )
1027+ if n <= 2 :
1028+ return coords .copy ()
1029+
1030+ # Use a doubly-linked list via prev/next index arrays.
1031+ prev_idx = np .empty (n , dtype = np .int64 )
1032+ next_idx = np .empty (n , dtype = np .int64 )
1033+ for i in range (n ):
1034+ prev_idx [i ] = i - 1
1035+ next_idx [i ] = i + 1
1036+ # Endpoints are never removed (sentinel values).
1037+ prev_idx [0 ] = - 1
1038+ next_idx [n - 1 ] = - 1
1039+
1040+ # Compute triangle areas for interior vertices.
1041+ areas = np .full (n , np .inf , dtype = np .float64 )
1042+ for i in range (1 , n - 1 ):
1043+ ax , ay = coords [prev_idx [i ], 0 ], coords [prev_idx [i ], 1 ]
1044+ bx , by = coords [i , 0 ], coords [i , 1 ]
1045+ cx , cy = coords [next_idx [i ], 0 ], coords [next_idx [i ], 1 ]
1046+ areas [i ] = abs ((ax * (by - cy ) + bx * (cy - ay ) + cx * (ay - by )) / 2.0 )
1047+
1048+ removed = np .zeros (n , dtype = np .bool_ )
1049+ remaining = n
1050+
1051+ while remaining > 2 :
1052+ # Find vertex with minimum area.
1053+ min_area = np .inf
1054+ min_idx = - 1
1055+ for i in range (1 , n - 1 ):
1056+ if not removed [i ] and areas [i ] < min_area :
1057+ min_area = areas [i ]
1058+ min_idx = i
1059+
1060+ if min_idx == - 1 or min_area >= tolerance :
1061+ break
1062+
1063+ # Remove vertex.
1064+ removed [min_idx ] = True
1065+ remaining -= 1
1066+
1067+ # Update linked list.
1068+ p = prev_idx [min_idx ]
1069+ nx_i = next_idx [min_idx ]
1070+ if p >= 0 :
1071+ next_idx [p ] = nx_i
1072+ if nx_i >= 0 and nx_i < n :
1073+ prev_idx [nx_i ] = p
1074+
1075+ # Recompute areas for affected neighbors.
1076+ if p > 0 and prev_idx [p ] >= 0 :
1077+ ax , ay = coords [prev_idx [p ], 0 ], coords [prev_idx [p ], 1 ]
1078+ bx , by = coords [p , 0 ], coords [p , 1 ]
1079+ cx , cy = coords [next_idx [p ], 0 ], coords [next_idx [p ], 1 ]
1080+ new_area = abs ((ax * (by - cy ) + bx * (cy - ay ) + cx * (ay - by )) / 2.0 )
1081+ # Enforce monotonicity: area can only increase.
1082+ areas [p ] = max (new_area , min_area )
1083+
1084+ if nx_i >= 0 and nx_i < n - 1 and next_idx [nx_i ] >= 0 :
1085+ ax , ay = coords [prev_idx [nx_i ], 0 ], coords [prev_idx [nx_i ], 1 ]
1086+ bx , by = coords [nx_i , 0 ], coords [nx_i , 1 ]
1087+ cx , cy = coords [next_idx [nx_i ], 0 ], coords [next_idx [nx_i ], 1 ]
1088+ new_area = abs ((ax * (by - cy ) + bx * (cy - ay ) + cx * (ay - by )) / 2.0 )
1089+ areas [nx_i ] = max (new_area , min_area )
1090+
1091+ # Collect remaining vertices.
1092+ count = 0
1093+ for i in range (n ):
1094+ if not removed [i ]:
1095+ count += 1
1096+
1097+ result = np .empty ((count , 2 ), dtype = np .float64 )
1098+ j = 0
1099+ for i in range (n ):
1100+ if not removed [i ]:
1101+ result [j , 0 ] = coords [i , 0 ]
1102+ result [j , 1 ] = coords [i , 1 ]
1103+ j += 1
1104+
1105+ return result
1106+
1107+
10051108def _find_junctions (all_rings ):
10061109 """Find junction vertices that must be pinned during simplification.
10071110
@@ -1169,20 +1272,23 @@ def _chain_key(chain):
11691272 return (start , end , interior )
11701273
11711274
1172- def _simplify_polygons (polygon_points , tolerance ):
1275+ def _simplify_polygons (polygon_points , tolerance , method = "douglas-peucker" ):
11731276 """Topology-preserving simplification of all polygons.
11741277
11751278 Uses shared-edge decomposition: finds junction vertices, splits
11761279 rings into chains at junctions, simplifies each unique chain once
1177- with Douglas-Peucker , then reassembles rings.
1280+ with the chosen algorithm , then reassembles rings.
11781281
11791282 Parameters
11801283 ----------
11811284 polygon_points : list of list of np.ndarray
11821285 Output of polygonize backend: list of polygons, each polygon
11831286 is [exterior_ring, *hole_rings].
11841287 tolerance : float
1185- Douglas-Peucker tolerance in coordinate units.
1288+ Simplification tolerance in coordinate units.
1289+ method : str, default="douglas-peucker"
1290+ Simplification algorithm: ``"douglas-peucker"`` or
1291+ ``"visvalingam-whyatt"``.
11861292
11871293 Returns
11881294 -------
@@ -1210,7 +1316,10 @@ def _simplify_polygons(polygon_points, tolerance):
12101316 for chain in chains :
12111317 key = _chain_key (chain )
12121318 if key not in simplified_chains :
1213- simplified_chains [key ] = _douglas_peucker (chain , tolerance )
1319+ if method == "douglas-peucker" :
1320+ simplified_chains [key ] = _douglas_peucker (chain , tolerance )
1321+ else :
1322+ simplified_chains [key ] = _visvalingam_whyatt (chain , tolerance )
12141323 # Determine if this chain was reversed relative to canonical.
12151324 start = (chain [0 , 0 ], chain [0 , 1 ])
12161325 canonical_start = (simplified_chains [key ][0 , 0 ],
@@ -1376,6 +1485,8 @@ def polygonize(
13761485 transform : Optional [np .ndarray ] = None , # shape (6,)
13771486 column_name : str = "DN" ,
13781487 return_type : str = "numpy" ,
1488+ simplify_tolerance : Optional [float ] = None ,
1489+ simplify_method : str = "douglas-peucker" ,
13791490):
13801491 """
13811492 Polygonize creates vector polygons for connected regions of pixels in a
@@ -1416,6 +1527,23 @@ def polygonize(
14161527 "geopandas", "awkward" and "geojson". "numpy" and "geojson" are
14171528 always available, the others require optional dependencies.
14181529
1530+ simplify_tolerance: float, optional
1531+ Simplification tolerance in coordinate units. When set, polygon
1532+ boundaries are simplified using shared-edge decomposition to
1533+ preserve topology between adjacent polygons. Default is None
1534+ (no simplification).
1535+
1536+ For ``"douglas-peucker"``, this is the maximum perpendicular
1537+ distance a vertex may deviate from the simplified line.
1538+
1539+ For ``"visvalingam-whyatt"``, this is the minimum triangle area
1540+ threshold; vertices forming triangles smaller than this are removed.
1541+
1542+ simplify_method: str, default="douglas-peucker"
1543+ Simplification algorithm. Options are ``"douglas-peucker"``
1544+ (distance-based, good for general use) and ``"visvalingam-whyatt"``
1545+ (area-based, tends to produce better cartographic results).
1546+
14191547 Returns
14201548 -------
14211549 Polygons and their corresponding values in a format determined by
@@ -1462,6 +1590,16 @@ def polygonize(
14621590 raise ValueError (
14631591 f"Incorrect transform length of { len (transform )} instead of 6" )
14641592
1593+ # Check simplification parameters.
1594+ if simplify_tolerance is not None and simplify_tolerance < 0 :
1595+ raise ValueError (
1596+ "simplify_tolerance must be non-negative, "
1597+ f"got { simplify_tolerance } " )
1598+ if simplify_method not in ("douglas-peucker" , "visvalingam-whyatt" ):
1599+ raise ValueError (
1600+ f"simplify_method must be 'douglas-peucker' or "
1601+ f"'visvalingam-whyatt', got '{ simplify_method } '" )
1602+
14651603 mapper = ArrayTypeFunctionMapping (
14661604 numpy_func = _polygonize_numpy ,
14671605 cupy_func = _polygonize_cupy ,
@@ -1471,6 +1609,11 @@ def polygonize(
14711609 column , polygon_points = mapper (raster )(
14721610 raster .data , mask_data , connectivity_8 , transform )
14731611
1612+ # Apply simplification if requested.
1613+ if simplify_tolerance is not None and simplify_tolerance > 0 :
1614+ polygon_points = _simplify_polygons (
1615+ polygon_points , simplify_tolerance , method = simplify_method )
1616+
14741617 # Convert to requested return_type.
14751618 if return_type == "numpy" :
14761619 return column , polygon_points
0 commit comments