Skip to content

Preserve contour holes before dissolving chunked label polygons - #1299

Open
sophiamaedler wants to merge 2 commits into
scverse:mainfrom
sophiamaedler:fix/vectorize-hole-topology-before-dissolve
Open

sophiamaedler wants to merge 2 commits into
scverse:mainfrom
sophiamaedler:fix/vectorize-hole-topology-before-dissolve

Conversation

@sophiamaedler

Copy link
Copy Markdown

fixes #1235

_region_props_to_polygon currently converts each contour into a separate polygon, so an interior contour becomes a filled shell rather than a hole. This produces invalid MultiPolygons with nested shells.

When a label spans multiple chunks, the subsequent dissolve can fill that incorrectly represented hole and return a valid but incorrect polygon. If another label occupies the hole, the resulting shapes overlap despite the raster labels being mutually exclusive. These overlaps can cause downstream area calculations to double-count regions.

This change applies make_valid to invalid geometries with multiple contour rings before chunk fragments are dissolved. Single-ring geometries skip the additional validity check. Regression tests cover occupied holes with different chunk sizes and preservation of disconnected components.

Benchmark results

The benchmark uses the complete cell_labels layer from the public 10x Xenium human lung cancer SpatialData object, distributed as xenium_2.0.0_io_spatialdata_0.7.1.zip on the SpatialData dataset page.

The raster contains 17,098 × 51,187 pixels and 162,254 cell labels. It was loaded into memory once and divided into 1024 × 1024 chunks. Each implementation ran once using Dask's single-threaded scheduler. Data loading and geometry validation were excluded from the timings.

Metric Before After
Vectorization time 164.84 s 165.90 s
Cell shapes 162,254 162,254
Invalid shapes 266 0
Positive-area overlap pairs among valid shapes 1 0

Cell IDs were unchanged. The measured runtime difference was +0.64%. These are single-run wall times. Invalid baseline geometries were counted separately and excluded from intersection calculations.

All 22 vectorization tests passed, along with Ruff and mypy checks.

Download the dataset and run the benchmark

From this PR's checkout, using Python 3.12 or later:

python -m pip install -e . fsspec aiohttp

mkdir -p temp/xenium-vectorization

python - <<'PYTHON'
from pathlib import Path
import zipfile
import fsspec

url = (
    "https://s3.embl.de/spatialdata/spatialdata-sandbox/"
    "xenium_2.0.0_io_spatialdata_0.7.1.zip"
)
destination = Path("temp/xenium-vectorization/xenium-lung-0.7.1")

# Download only the label layers and root metadata using HTTP range requests.
with fsspec.open(
    url, "rb", block_size=8 * 1024**2, cache_type="blockcache"
) as remote:
    with zipfile.ZipFile(remote) as archive:
        for name in archive.namelist():
            if name == "data.zarr/zarr.json" or name.startswith("data.zarr/labels/"):
                archive.extract(name, destination)
PYTHON

Save the script benchmark_xenium_vectorization.py as temp/xenium-vectorization/benchmark_xenium_vectorization.py, then run:

set -o pipefail

python -u temp/xenium-vectorization/benchmark_xenium_vectorization.py \
  2>&1 | tee temp/xenium-vectorization/benchmark.log

Images, transcripts and tables are not needed for this benchmark.

@codecov

codecov Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.45%. Comparing base (af431ef) to head (b959b89).

Files with missing lines Patch % Lines
src/spatialdata/_core/operations/vectorize.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1299      +/-   ##
==========================================
+ Coverage   91.43%   91.45%   +0.01%     
==========================================
  Files          53       53              
  Lines        8395     8400       +5     
==========================================
+ Hits         7676     7682       +6     
+ Misses        719      718       -1     
Files with missing lines Coverage Δ
src/spatialdata/_core/operations/vectorize.py 92.73% <80.00%> (+0.78%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

to_polygons(labels) turns a label containing a hole into an invalid MultiPolygon with the hole counted as area

1 participant