Skip to content

Commit 5aa32bf

Browse files
account for the padding shift during vectorization + use multipolygon
1 parent 4021946 commit 5aa32bf

2 files changed

Lines changed: 31 additions & 10 deletions

File tree

src/spatialdata/_core/operations/vectorize.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,30 +227,30 @@ def _vectorize_chunk(chunk: np.ndarray, yoff: int, xoff: int) -> GeoDataFrame:
227227
return ShapesModel.parse(gdf, transformations=transformations.copy())
228228

229229

230-
def _region_props_to_polygons(region_props: RegionProperties) -> list[Polygon]:
230+
def _region_props_to_polygon(region_props: RegionProperties) -> MultiPolygon:
231231
mask = np.pad(region_props.image, 1)
232232
contours = skimage.measure.find_contours(mask, 0.5)
233233

234234
# shapes with <= 3 vertices, i.e. lines, can't be converted into a polygon
235235
polygons = [Polygon(contour[:, [1, 0]]) for contour in contours if contour.shape[0] >= 4]
236+
polygon = MultiPolygon(polygons) if len(polygons) > 1 else polygons[0]
236237

237238
yoff, xoff, *_ = region_props.bbox
238-
return [shapely.affinity.translate(poly, xoff, yoff) for poly in polygons]
239+
return shapely.affinity.translate(polygon, xoff - 1, yoff - 1) # account for the padding
239240

240241

241242
def _vectorize_mask(
242243
mask: np.ndarray, # type: ignore[type-arg]
243244
) -> GeoDataFrame:
244245
if mask.max() == 0:
245-
return GeoDataFrame(geometry=[])
246+
return GeoDataFrame({"label": []}, geometry=[])
246247

247248
regions = skimage.measure.regionprops(mask)
248249

249-
polygons_list = [_region_props_to_polygons(region) for region in regions]
250-
geoms = [poly for polygons in polygons_list for poly in polygons]
251-
labels = [region.label for i, region in enumerate(regions) for _ in range(len(polygons_list[i]))]
252-
253-
return GeoDataFrame({"label": labels}, geometry=geoms)
250+
return GeoDataFrame(
251+
{"label": [region.label for region in regions]},
252+
geometry=[_region_props_to_polygon(region) for region in regions],
253+
)
254254

255255

256256
def _dissolve_on_overlaps(label: int, group: GeoDataFrame) -> GeoDataFrame:

tests/core/operations/test_vectorize.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import numpy as np
44
import pytest
55
from geopandas import GeoDataFrame
6-
from shapely import MultiPoint, Point
6+
from shapely import MultiPoint, Point, Polygon
7+
from shapely.ops import unary_union
8+
from skimage.draw import polygon
79

8-
from spatialdata._core.operations.vectorize import to_circles, to_polygons
10+
from spatialdata._core.operations.vectorize import _vectorize_mask, to_circles, to_polygons
911
from spatialdata.datasets import blobs
1012
from spatialdata.models.models import ShapesModel
1113
from spatialdata.testing import assert_elements_are_identical
@@ -139,3 +141,22 @@ def test_invalid_geodataframe_to_polygons() -> None:
139141
gdf = GeoDataFrame(geometry=[MultiPoint([[0, 0], [1, 1]])])
140142
with pytest.raises(RuntimeError, match="Unsupported geometry type"):
141143
to_polygons(gdf)
144+
145+
146+
def test_vectorize_mask_almost_invertible() -> None:
147+
cell = Polygon([[10, 10], [30, 40], [90, 50], [100, 20]])
148+
image_shape = (70, 120)
149+
150+
rasterized_image = np.zeros(image_shape, dtype=np.int8)
151+
x, y = cell.exterior.coords.xy
152+
rr, cc = polygon(y, x, image_shape)
153+
rasterized_image[rr, cc] = 1
154+
155+
new_cell = _vectorize_mask(rasterized_image)
156+
new_cell = unary_union(new_cell.geometry)
157+
158+
assert new_cell.intersection(cell).area / new_cell.union(cell).area > 0.97
159+
160+
def test_label_column_vectorize_mask() -> None:
161+
assert "label" in _vectorize_mask(np.array([0]))
162+
assert "label" in _vectorize_mask(np.array([[0, 1], [1, 1]]))

0 commit comments

Comments
 (0)