diff --git a/README.rst b/README.rst index a1624b7..38d5fc5 100644 --- a/README.rst +++ b/README.rst @@ -59,10 +59,10 @@ To export images via bioformats2raw we use the ```--bf``` flag:: $ omero zarr --output /home/user/zarr_files export 1 --bf Image exported to /home/user/zarr_files/2chZT.lsm -Masks -^^^^^ +Masks and Polygons +^^^^^^^^^^^^^^^^^^ -To export Masks for an Image or Plate:: +To export Masks or Polygons for an Image or Plate, use the `masks` or `polygons` command:: # Saved under 1.zarr/labels/0 - 1.zarr/ must already exist $ omero zarr masks Image:1 @@ -78,12 +78,16 @@ To export Masks for an Image or Plate:: # e.g. Export to 1.zarr/labels/A $ omero zarr masks Image:1 --label-name=A -The default behaviour is to export all masks on the Image to a single 5D -"labeled" zarr array, with a different value for each mask Shape. -An exception will be thrown if any of the masks overlap. + # Allow overlapping masks or polygons (overlap will be maximum value of the dtype) + $ omero zarr polygons Image:1 --overlaps=dtype_max -To handle overlapping masks, split masks into non-overlapping zarr groups -using a "label-map" which is a csv file of that specifies the name of +The default behaviour is to export all masks or polygons on the Image to a single 5D +"labeled" zarr array, with a different value for each Shape. +An exception will be thrown if any of the masks overlap, unless the `--overlaps` +option is used as above. + +An alternative to handle overlapping masks is to split masks into non-overlapping zarr +groups using a "label-map" which is a csv file that specifies the name of the zarr group for each ROI on the Image. Columns are ID, NAME, ROI_ID. For example, to create a group from the `textValue` of each Shape, diff --git a/src/omero_zarr/cli.py b/src/omero_zarr/cli.py index 5d94f72..a94500f 100644 --- a/src/omero_zarr/cli.py +++ b/src/omero_zarr/cli.py @@ -11,7 +11,12 @@ from zarr.hierarchy import open_group from zarr.storage import FSStore -from .masks import MASK_DTYPE_SIZE, image_shapes_to_zarr, plate_shapes_to_zarr +from .masks import ( + MASK_DTYPE_SIZE, + MaskSaver, + image_shapes_to_zarr, + plate_shapes_to_zarr, +) from .raw_pixels import ( add_omero_metadata, add_toplevel_metadata, @@ -272,6 +277,16 @@ def _configure(self, parser: Parser) -> None: subcommand.add_argument( "--output", type=str, default="", help="The output directory" ) + for subcommand in (polygons, masks): + subcommand.add_argument( + "--overlaps", + type=str, + default=MaskSaver.OVERLAPS[0], + choices=MaskSaver.OVERLAPS, + help="To allow overlapping shapes, use 'dtype_max':" + " All overlapping regions will be set to the" + " max value for the dtype", + ) @gateway_required def masks(self, args: argparse.Namespace) -> None: diff --git a/src/omero_zarr/masks.py b/src/omero_zarr/masks.py index f238679..26020aa 100644 --- a/src/omero_zarr/masks.py +++ b/src/omero_zarr/masks.py @@ -62,7 +62,13 @@ def plate_shapes_to_zarr( dtype = MASK_DTYPE_SIZE[int(args.label_bits)] saver = MaskSaver( - plate, None, dtype, args.label_path, args.style, args.source_image + plate, + None, + dtype, + args.label_path, + args.style, + args.source_image, + args.overlaps, ) count = 0 @@ -154,7 +160,13 @@ def image_shapes_to_zarr( if masks: saver = MaskSaver( - None, image, dtype, args.label_path, args.style, args.source_image + None, + image, + dtype, + args.label_path, + args.style, + args.source_image, + args.overlaps, ) if args.style == "split": @@ -178,6 +190,8 @@ class MaskSaver: masks to zarr groups/arrays. """ + OVERLAPS = ("error", "dtype_max") + def __init__( self, plate: Optional[omero.gateway.PlateWrapper], @@ -186,6 +200,7 @@ def __init__( path: str = "labels", style: str = "labeled", source: str = "..", + overlaps: str = "error", ) -> None: self.dtype = dtype self.path = path @@ -193,6 +208,7 @@ def __init__( self.source_image = source self.plate = plate self.plate_path = Optional[str] + self.overlaps = overlaps if image: self.image = image self.size_t = image.getSizeT() @@ -302,7 +318,6 @@ def save(self, masks: List[omero.model.Shape], name: str) -> None: masks, mask_shape, ignored_dimensions, - check_overlaps=True, ) axes = marshal_axes(self.image) @@ -433,7 +448,7 @@ def masks_to_labels( masks: List[omero.model.Mask], mask_shape: Tuple[int, ...], ignored_dimensions: Set[str] = None, - check_overlaps: bool = True, + check_overlaps: bool = None, ) -> Tuple[np.ndarray, Dict[int, str], Dict[int, Dict]]: """ :param masks [MaskI]: Iterable container of OMERO masks @@ -464,6 +479,11 @@ def masks_to_labels( sorted_roi_ids = list(set(roi_ids)) sorted_roi_ids.sort() + if check_overlaps is None: + # If overlaps isn't 'dtype_max', an exception is thrown + # if any overlaps exist + check_overlaps = self.overlaps != "dtype_max" + # label values are 1...n max_value = len(sorted_roi_ids) # find most suitable dtype... @@ -512,22 +532,27 @@ def masks_to_labels( for i_z in self._get_indices( ignored_dimensions, "Z", z, size_z ): - if check_overlaps and np.any( - np.logical_and( - labels[ - i_t, i_c, i_z, y : (y + h), x : (x + w) - ].astype(np.bool), - binim_yx, - ) - ): - raise Exception( - f"Mask {shape.roi.id.val} overlaps " - "with existing labels" - ) + overlap = np.logical_and( + labels[i_t, i_c, i_z, y : (y + h), x : (x + w)].astype( + np.bool + ), + binim_yx, + ) # ADD to the array, so zeros in our binarray don't # wipe out previous shapes labels[i_t, i_c, i_z, y : (y + h), x : (x + w)] += ( binim_yx * shape_value ) + if np.any(overlap): + if check_overlaps: + raise Exception( + f"Shape {shape.roi.id.val} overlaps " + "with existing labels" + ) + else: + # set overlapping region to max(dtype) + labels[i_t, i_c, i_z, y : (y + h), x : (x + w)][ + overlap + ] = np.iinfo(labels_dtype).max return labels, fillColors, properties