Repository navigation
fix: label masks silently truncate past 65,535 cells - #43
Merged
Merged
Conversation
parquet_to_tiff.py hardcoded dtype=np.uint16 when rasterising segmentation geometries. Label IDs are stored as pixel values, so that caps a mask at 65535 distinct cells -- and rasterio.features.rasterize does not raise on overflow, it silently saturates at the dtype maximum and drops every geometry past it. The failure is invisible on the crops used in testing and only surfaces on whole-slide images. On a 370 mm^2 section (~370k cells at 0.4964 um/px) it truncated to exactly 65535 labels, with the discarded cells collapsing onto low label IDs. That leaves single labels whose parts are scattered across the whole slide, whose bounding boxes span the image, and whose geometries are correspondingly expensive downstream. Select the narrowest dtype that fits the geometry count instead, and raise rather than truncate if even uint32 is too narrow. Masks under 65535 cells are unchanged, so existing outputs and snapshots stay byte-identical. Diagnostics go to stderr because stdout carries the TIFF. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
test_parquet_to_tiff.py exercises label dtype selection, which lives in bin/parquet_to_tiff.py. That module imports rasterio, shapely, pyarrow and typer at import time, so the tests cannot reach the function without them and collection failed with ModuleNotFoundError. Skipping the module on missing imports would have made CI green while leaving the truncation bug unguarded, which is the opposite of the point. These are plain wheels with no GPU or gated weights, so the job stays within its stated dependency-light intent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Widening the label dtype doubles these masks: on a 36583x40986 slide the PARQUETTOTIFF output goes from 3.0 GB to 6.0 GB, and SMOOTHMASKS then reads that and writes another copy. All of it lands on shared scratch, where a recent run spent six hours on a single 2.87 GB mask write. Label masks are long runs of a repeated ID, so deflate more than cancels the widening -- a dense whole-slide mask goes from 2.87 GB to 31 MB, and the compressed write is faster than the uncompressed one even on local disk. Verified end to end through the stdout redirect the module uses, since TIFF writing needs a seekable stream: 70,000 cells round-trip as uint32 with every label intact, 12x smaller on disk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mcmero
approved these changes
Aug 11, 2026
mcmero
left a comment
Contributor
There was a problem hiding this comment.
Looks good, some minor comments.
Replace the uint16/uint32 selection (select_label_dtype + LABEL_DTYPES) with a single fixed uint32 dtype. uint32 indexes 4.29e9 cells, orders of magnitude past any real slide, so there is no case where narrowing to uint16 is worth the extra code -- and it removes the sorted-tuple assumption raised in review. A fail-fast guard still rejects the implausible >uint32 case rather than truncating silently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The smoothed mask is the viewer-facing output and some viewers choke on compressed-strip TIFFs, so drop compression here entirely and write a plain TIFF. No option is exposed -- this output is meant to be opened, not squeezed onto scratch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CellSAM segmenter wrote its label mask uncompressed, unlike the Cellpose path (parquet_to_tiff). Both are intermediates that get smoothed downstream, so compress this one the same way -- zlib level 1, ~90x on label masks. Only the final smoothed mask is left uncompressed for viewers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Collaborator
Author
|
Have set uint32 as default, have set intermediate masks as compressed and final output mask as uncompressed. |
0.2.2 carries WEHI-SODA-Hub/cellmeasurement-py#7, the STRtree broad phase for the GeoJSON export overlap pass — the companion to the mask changes here. It also carries #6, which drops the geometric descriptors from the erosion and expansion bins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
parquet_to_tiff.pyrasterised label masks asuint16, which caps them at 65,535 cells. Past that limitrasterio.features.rasterizedoes not raise — it silently caps at the dtype maximum and drops every geometry beyond it.A 370 mm² section (~370k cells) came back reporting exactly
Matched 65535 cells; another section reported 65534. The discarded cells collapse onto low label IDs, producing single labels with parts scattered across the whole slide — which is also why the downstream overlap pass went from 36s to 16 hours.Only affects the SOPA/Cellpose route (
PARQUETTOTIFF). CellSAM writesuint32. Present since the file was created in10a29f4(2025-07-14).Changes
uint32is too narrow. Masks under 65,535 cells still come outuint16, so existing outputs and snapshots are byte-identical.parquet_to_tiff.pyand both write sites insmooth_masks.py.Verification
End to end through the
> file.tiffredirect the module uses:12 tests in
tests/python/test_parquet_to_tiff.py, including one pinning the silent-truncation behaviour so a fixed dtype fails loudly in future.The final pipeline output mask is compressed separately in WEHI-SODA-Hub/cellmeasurement-py#7.
🤖 Generated with Claude Code