spacr.mask_io

Cellpose mask I/O — write + read masks as EITHER TIFF or NumPy.

Introduces two helpers to replace scattered np.save/np.load calls across spacr.object and spacr.io:

  • save_mask() — writes a single 2-D uint16 mask. Emits .tif by default (smaller on disk + openable in Fiji / napari) but .npy is still supported via fmt="npy" for users who prefer numpy round-trips.

  • load_mask() — reads a mask regardless of on-disk format. Given a path with no suffix, or a stem, it probes .tif, .tiff and .npy in that order and returns the first hit.

Both helpers preserve uint16 dtype (spaCR’s mask convention) and return arrays with the same shape (H, W). TIFF is written with LZW compression via tifffile.

Migration path for existing scripts:

# BEFORE np.save(“foo_mask.npy”, mask.astype(np.uint16)) m = np.load(“foo_mask.npy”)

# AFTER save_mask(“foo_mask”, mask) # writes foo_mask.tif m = load_mask(“foo_mask”) # finds foo_mask.tif or foo_mask.npy

The env var SPACR_MASK_FORMAT overrides the default across the whole process: set to npy to keep the old behaviour.

Masks as ROIs: QuPath GeoJSON, ImageJ RoiSet and COCO

The second half of this module moves label masks to and from the formats other tools curate and train on: masks_to_geojson() and geojson_to_masks() for QuPath, masks_to_roiset() and roiset_to_masks() for Fiji’s ROI Manager, masks_to_coco() and coco_to_masks() for COCO-style training sets, and export_rois() / import_rois() choosing among them by file name.

Every object is one feature, ROI or annotation, carrying its object type (cell, nucleus …), its object id and its class; the class defaults to the object type. Each object is traced on its own, so objects that touch stay separate. Outlines follow PIXEL EDGES, not pixel centres: a pixel (row, col) is the square from x = col to col + 1 and y = row to row + 1, the convention QuPath and ImageJ both use. A pixel is inside a polygon when its centre is, so a traced outline rasterised back gives the object exactly: no tolerance, holes and several-part objects included. A hole is an interior ring (GeoJSON), a path of a composite ROI (ImageJ) or, since a COCO polygon cannot have one, an RLE segmentation (COCO).

roifile is needed only for ImageJ files and is imported when one is read or written. COCO RLE is encoded and decoded here, so pycocotools is never required; the files it writes are the ones pycocotools reads.

Functions

coco_image_names(→ List[str])

The file names of the images a COCO dataset annotates.

coco_to_masks(data, *[, file_name, shape, ...])

Rasterise the annotations of one image of a COCO dataset.

export_rois(→ pathlib.Path)

Write label masks as QuPath GeoJSON, an ImageJ RoiSet or COCO JSON.

geojson_to_masks(data, shape, *[, object_type, ...])

Rasterise a (QuPath) GeoJSON document into label masks.

import_rois(path[, shape, fmt, file_name, ...])

Read QuPath GeoJSON, an ImageJ RoiSet or COCO JSON into label masks.

load_mask(→ numpy.ndarray)

Read a Cellpose mask regardless of on-disk format.

masks_to_coco(→ Dict[str, Any])

Label masks as a COCO instance-segmentation dataset.

masks_to_geojson(→ Dict[str, Any])

Label masks as a QuPath GeoJSON FeatureCollection.

masks_to_roiset(→ pathlib.Path)

Write label masks as an ImageJ/Fiji RoiSet.zip.

object_polygons(→ List[List[numpy.ndarray]])

The polygons that outline one object of a label mask, exactly.

rle_decode(→ numpy.ndarray)

Decode a COCO RLE, compressed (text) or uncompressed (list of runs).

rle_encode(→ Dict[str, Any])

COCO run-length encoding of a binary mask, compressed as COCO writes it.

roi_format(→ str)

Which ROI format a file is: "geojson", "imagej" or "coco".

roi_suffix(→ str)

The file suffix a ROI format is written with.

roiset_to_masks(path, shape, *[, object_type, ...])

Rasterise an ImageJ RoiSet.zip (or one .roi) into label masks.

save_mask(→ pathlib.Path)

Write a Cellpose mask to disk.

Module Contents

spacr.mask_io.coco_image_names(data: Any) → List[str][source]

The file names of the images a COCO dataset annotates.

Parameters:

data – the parsed dataset, or a path to its .json file.

Returns:

images[].file_name, in file order.

spacr.mask_io.coco_to_masks(data: Any, *, file_name: str | None = None, shape: Tuple[int, int] | None = None, object_type: str | None = None, with_classes: bool = False)[source]

Rasterise the annotations of one image of a COCO dataset.

Polygons are filled by pixel centre and the parts of one annotation united, as pycocotools unites them; RLE (compressed or not) is decoded exactly. The object type is object_type, the annotation’s object_type, or its category name; the id is object_id or the next free one; the class is the category name.

Parameters:
  • data – the parsed dataset, or a path to its .json file.

  • file_name – which image, for a dataset of several.

  • shape – (rows, columns); default the image entry’s height and width.

  • object_type – put every annotation in this one object type.

  • with_classes – also return each object’s class.

Returns:

{object type: uint16 labels}, or (masks, {object type: {id: class}}) with with_classes.

Raises:

ValueError – the image is missing or an RLE does not fit it.

spacr.mask_io.export_rois(masks: Any, path: PathLike, fmt: str | None = None, *, classes: Any = None, object_type: str | None = None, file_name: str | None = None, rle: bool = False) → pathlib.Path[source]

Write label masks as QuPath GeoJSON, an ImageJ RoiSet or COCO JSON.

Parameters:
  • masks – a 2-D label mask, or {object type: mask}.

  • path – the file to write; its suffix picks the format unless fmt does (.geojson, .zip, .json for COCO).

  • fmt – "geojson", "imagej" or "coco".

  • classes – {id: class} or {object type: {id: class}}.

  • object_type – the type of a single mask (default "object").

  • file_name – the image’s name, recorded in GeoJSON ids and COCO images; default the output file’s stem.

  • rle – COCO only: every segmentation as RLE.

Returns:

the path written.

Raises:
  • ImportError – ImageJ output without roifile installed.

  • ValueError – an unknown format or an invalid mask.

spacr.mask_io.geojson_to_masks(data: Any, shape: Tuple[int, int], *, object_type: str | None = None, with_classes: bool = False)[source]

Rasterise a (QuPath) GeoJSON document into label masks.

A pixel belongs to an object when its centre lies inside the polygon and outside its holes; an outline written by masks_to_geojson() therefore comes back pixel for pixel. The object type is, in order, object_type, the feature’s object_type property, the type in a "<type> <id>" name, or its classification – so a QuPath project that was never in spaCR gives one mask per class. The id is the object_id property or measurement, or the id in the name; objects without one are numbered after the rest.

Parameters:
  • data – the parsed document, or a path to a .geojson file.

  • shape – the image’s (rows, columns).

  • object_type – put every feature in this one object type.

  • with_classes – also return each object’s class.

Returns:

{object type: uint16 labels}, or (masks, {object type: {id: class}}) with with_classes.

Raises:

ValueError – the document is not GeoJSON.

spacr.mask_io.import_rois(path: PathLike, shape: Tuple[int, int] | None = None, fmt: str | None = None, *, file_name: str | None = None, object_type: str | None = None, with_classes: bool = False)[source]

Read QuPath GeoJSON, an ImageJ RoiSet or COCO JSON into label masks.

Parameters:
  • path – the file.

  • shape – the image’s (rows, columns); required for GeoJSON and ImageJ, optional for COCO, which records it.

  • fmt – "geojson", "imagej" or "coco"; default from the file (roi_format()).

  • file_name – COCO only: which image of a dataset of several.

  • object_type – put every object in this one object type.

  • with_classes – also return each object’s class.

Returns:

{object type: uint16 labels}, or (masks, {object type: {id: class}}) with with_classes.

Raises:
  • ValueError – no shape where one is needed, or an unreadable file.

  • ImportError – an ImageJ file without roifile installed.

spacr.mask_io.load_mask(path: PathLike) → numpy.ndarray[source]

Read a Cellpose mask regardless of on-disk format.

Parameters:

path – mask file or extensionless stem to resolve and load.

Accepts a full path (foo.tif / foo.npy) OR a stem (foo) — in the stem case, tif → tiff → npy is tried and the first extant file is loaded.

Returns:

uint16 2-D array. Shape unchanged.

Raises:
spacr.mask_io.masks_to_coco(masks: Any, *, file_name: str = 'image', classes: Any = None, object_type: str | None = None, rle: bool = False, dataset: Dict[str, Any] | None = None) → Dict[str, Any][source]

Label masks as a COCO instance-segmentation dataset.

Each object is one annotation with segmentation, area (pixels), bbox ([x, y, width, height] in pixel corners), iscrowd 0 and spaCR’s object_type and object_id. Categories are the (object type, class) pairs: name is the class and supercategory the object type. Segmentations are polygons – one flat [x1, y1, x2, y2, ...] list per part – except for an object with a hole, which a COCO polygon cannot express and which is written as RLE; rle=True writes every object as compressed RLE, which is exact whoever reads it.

Parameters:
  • masks – a 2-D label mask, or {object type: mask}.

  • file_name – the image’s file name, as images[].file_name.

  • classes – {id: class} or {object type: {id: class}}; an object without one is classified as its object type.

  • object_type – the type of a single mask (default "object").

  • rle – write every segmentation as RLE.

  • dataset – an existing COCO dict to add this image to, as a queue export does; None starts a new one.

Returns:

the dataset (dataset itself when given).

Raises:

ValueError – an invalid mask.

spacr.mask_io.masks_to_geojson(masks: Any, *, classes: Any = None, object_type: str | None = None, object_kind: str = 'annotation', image_name: str = '') → Dict[str, Any][source]

Label masks as a QuPath GeoJSON FeatureCollection.

One feature per object: a Polygon, or a MultiPolygon for an object in several parts, with holes as interior rings and coordinates in pixels (pixel corners, x right and y down). The properties are QuPath’s own – objectType, classification (name and color), name and isLocked – plus object_type and object_id, and measurements.object_id so the id survives a round trip through QuPath, which keeps measurements and names. The feature id is a UUID derived from the image name, the object type and the id, so exporting the same mask twice writes the same file.

Parameters:
  • masks – a 2-D label mask, or {object type: mask}.

  • classes – the class of each object: {id: class} for a single mask or {object type: {id: class}}; an object without one is classified as its object type.

  • object_type – the type of a single mask (default "object").

  • object_kind – "annotation" or "detection", the QuPath object each feature becomes.

  • image_name – the image these masks belong to, used to make the feature ids unique across images.

Returns:

the FeatureCollection as a JSON-ready dict.

Raises:

ValueError – an unknown object_kind or an invalid mask.

spacr.mask_io.masks_to_roiset(masks: Any, path: PathLike, *, classes: Any = None, object_type: str | None = None) → pathlib.Path[source]

Write label masks as an ImageJ/Fiji RoiSet.zip.

An object in one piece without holes is a traced polygon ROI, the kind Fiji’s wand makes; any other object is a composite ROI whose paths are all of its rings. Each ROI is named "<type>-<id>" (unique, and what the ROI Manager lists) and carries object_type, object_id and class as ROI properties.

Parameters:
  • masks – a 2-D label mask, or {object type: mask}.

  • path – the .zip to write; replaced if it exists.

  • classes – {id: class} or {object type: {id: class}}; an object without one is classified as its object type.

  • object_type – the type of a single mask (default "object").

Returns:

the path written.

Raises:
spacr.mask_io.object_polygons(mask: numpy.ndarray, label: int) → List[List[numpy.ndarray]][source]

The polygons that outline one object of a label mask, exactly.

Parameters:
  • mask – 2-D label mask.

  • label – the object id to outline.

Returns:

one [exterior, hole, hole, ...] list per 4-connected part of the object; each ring is a closed (n, 2) integer array of x, y pixel-corner coordinates. Empty when the id is absent.

spacr.mask_io.rle_decode(rle: Mapping[str, Any]) → numpy.ndarray[source]

Decode a COCO RLE, compressed (text) or uncompressed (list of runs).

Parameters:

rle – {"size": [rows, columns], "counts": ...}.

Returns:

the 2-D boolean mask.

Raises:

ValueError – the runs do not add up to the size.

spacr.mask_io.rle_encode(binary: numpy.ndarray) → Dict[str, Any][source]

COCO run-length encoding of a binary mask, compressed as COCO writes it.

Parameters:

binary – 2-D boolean mask.

Returns:

{"size": [rows, columns], "counts": str}, what pycocotools.mask.encode returns with counts as text.

spacr.mask_io.roi_format(path: PathLike, fmt: str | None = None) → str[source]

Which ROI format a file is: "geojson", "imagej" or "coco".

Parameters:
  • path – the file; .geojson is GeoJSON, .zip and .roi ImageJ, and an existing .json is read to tell COCO from GeoJSON (a new one is COCO).

  • fmt – the answer, when the caller already knows it; "qupath" and "roiset" are accepted too.

Returns:

the format.

Raises:

ValueError – an unknown fmt or a file of no known format.

spacr.mask_io.roi_suffix(fmt: str) → str[source]

The file suffix a ROI format is written with.

Parameters:

fmt – "geojson", "imagej" or "coco".

Returns:

".geojson", ".zip" or ".json".

spacr.mask_io.roiset_to_masks(path: PathLike, shape: Tuple[int, int], *, object_type: str | None = None, with_classes: bool = False)[source]

Rasterise an ImageJ RoiSet.zip (or one .roi) into label masks.

Polygon, freehand, traced, rectangle and oval ROIs are read, and a composite ROI by the even-odd rule over its paths, as ImageJ fills it; lines and points have no area and are skipped. A pixel belongs to a ROI when its centre is inside, ImageJ’s own rule, so a set written by masks_to_roiset() comes back pixel for pixel. Type, id and class come from the ROI properties, else from a "<type>-<id>" name, else the ROI is an "object" numbered after the rest.

Parameters:
  • path – the .zip or .roi file.

  • shape – the image’s (rows, columns).

  • object_type – put every ROI in this one object type.

  • with_classes – also return each object’s class.

Returns:

{object type: uint16 labels}, or (masks, {object type: {id: class}}) with with_classes.

Raises:

ImportError – roifile is not installed.

spacr.mask_io.save_mask(path: PathLike, mask: numpy.ndarray, fmt: str = None) → pathlib.Path[source]

Write a Cellpose mask to disk.

Parameters:
  • path – destination — extension is optional. foo, foo.tif, foo.npy all work.

  • mask – 2-D integer mask; will be cast to uint16.

  • fmt – force a format ("tif", "tiff", or "npy"). Defaults to DEFAULT_FORMAT (env-overridable).

Returns:

the resolved on-disk path.

Raises:

ValueError – labels are nonfinite, negative, fractional or will not fit in uint16, or fmt names a format this module cannot write.

An id above 65535 is refused rather than cast. The cast wraps, and the first value it wraps to is 0 — background — so object 65536 would come back from disk fused with everything that was never segmented at all, and nothing downstream could tell that from a mask with one fewer object.