transform() of multiscale labels uses linear interpolation and invents label ids
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 394
- Forks
- 95
- Avg merge
- 4d 3h
- Merged PRs (30d)
- 7
Description
[!NOTE]
This whole message is AI-generated. The issue was automatically discovered and reported by an AI agent (Claude) during an autonomous bug hunt on thespatialdatacode base. It has not been verified or triaged by a human yet; theneeds: triagelabel is set so that a maintainer can confirm it. The reproduction script below was executed by the agent in an isolated environment (see Environment) and its output is pasted verbatim.
Summary
Rotating or scaling a multiscale labels element (DataTree) with transform() resamples it with dask_image.ndinterp.affine_transform's default order=1 (linear interpolation). New label ids that never existed in the input appear (blends of neighbouring ids) and match no table row. Single-scale labels are handled correctly (order=0). The same applies to transform_to_coordinate_system() and transform(sdata, ...). Side inconsistency: single-scale images are resampled with nearest neighbour while multiscale images are linearly interpolated.
Severity (agent's assessment): high — silent data corruption of segmentation masks
Where: src/spatialdata/_core/operations/transform.py, DataTree overload (kwargs = {"prefilter": False} for labels, no order=0, ~line 330); the DataArray overload correctly uses order=0
Expected behaviour
The set of label ids after the transformation is a subset of the input ids (nearest-neighbour resampling), for both single-scale and multiscale labels.
Reproduction
Save as repro.py and run uv run repro.py (the PEP 723 header pins spatialdata to the commit the bug was found on; replace the URL fragment with @main to test the current main branch).
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "spatialdata @ git+https://github.com/scverse/spatialdata.git@ccf1ea048d054b6624214bf618008a9f9ae223e0",
# ]
# ///
"""transform() of multiscale labels uses linear interpolation and invents label ids (single-scale is fine)."""
import warnings
import numpy as np
from spatialdata import transform
from spatialdata.models import Labels2DModel
from spatialdata.transformations import Affine, Scale, set_transformation
warnings.simplefilter("ignore")
arr = np.zeros((64, 64), dtype=np.uint16)
arr[:32, :] = 1
arr[32:, :] = 50
theta = np.deg2rad(30)
rotation = Affine(
np.array([[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1]]),
input_axes=("x", "y"),
output_axes=("x", "y"),
)
bug = False
cases = [
("single-scale labels, rotation", Labels2DModel.parse(arr.copy()), rotation),
("multiscale labels, rotation", Labels2DModel.parse(arr.copy(), scale_factors=[2]), rotation),
("multiscale labels, Scale(1.5)", Labels2DModel.parse(arr.copy(), scale_factors=[2]), Scale([1.5, 1.5], axes=("x", "y"))),
]
for name, labels, t in cases:
set_transformation(labels, t, "global")
out = transform(labels, to_coordinate_system="global")
data = out.data if not hasattr(out, "scale0") else out["scale0"]["image"].data
uniques = np.unique(np.asarray(data.compute())).tolist()
ok = set(uniques) <= {0, 1, 50}
print(f"{name:32s} -> unique ids: {uniques[:10]}{' ...' if len(uniques) > 10 else ''} ({'OK' if ok else 'INVENTED IDS'})")
bug |= not ok
print("VERDICT:", "BUG REPRODUCED" if bug else "NOT REPRODUCED")
Observed output
single-scale labels, rotation -> unique ids: [0, 1, 50] (OK)
multiscale labels, rotation -> unique ids: [0, 1, 3, 4, 5, 6, 7, 8, 9, 10] ... (INVENTED IDS)
multiscale labels, Scale(1.5) -> unique ids: [0, 1, 17, 50] (INVENTED IDS)
VERDICT: BUG REPRODUCED
uv run --no-project --python 3.13 26.84s user 10.31s system 46% cpu 1:19.27 total
Possible fix direction (unverified)
Pass order=0 for labels in the DataTree branch (and decide one interpolation policy for images in both branches, ideally exposing order, see #725). Add a test asserting set(np.unique(out)) <= set(np.unique(in)) for labels after rotation/scaling.
Environment
uv run repro.py with the PEP 723 metadata in the script (fresh, isolated environment; spatialdata built from main @ ccf1ea0 (2026-08-28); Python 3.13, latest releases of the dependencies at run time: pandas 3.0, anndata 0.13, zarr 3.3, dask 2026.8, numpy 2.5, geopandas 1.1, shapely 2.1). macOS (arm64). Also reproduced in a second environment with pandas 2.3.3 / anndata 0.12.11 / numpy 2.4.4 / zarr 3.2.1.
Possibly related issues
#725, #1070
Automatically generated; discovered by an AI agent (Claude) and not yet reviewed by a human.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/spatialdata/_core/operations/transform.py and inspect the DataTree labels branch, then run the provided repro.py with uv to confirm the invented ids. Compare its behavior with the DataArray labels overload and verify that transformed multiscale labels retain only input ids for rotation and scaling, including through transform_to_coordinate_system().
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- data
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100