changeset 1:457514bb6750 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/crop_image/ commit 52a95105291e38f3410e347ed3b60d6acd6d5daa
author imgteam
date Fri, 09 Jan 2026 14:54:49 +0000
parents f8bfa85cac4c
children
files creators.xml crop_image.py crop_image.xml test-data/cyx_uint8.zarr/0/c/0/0/0 test-data/cyx_uint8.zarr/0/c/1/0/0 test-data/cyx_uint8.zarr/0/c/2/0/0 test-data/cyx_uint8.zarr/0/zarr.json test-data/cyx_uint8.zarr/zarr.json test-data/cyx_uint8_uint8.tiff test-data/yx_uint8_mask.tiff test-data/yx_uint8_mask.zarr/0/c/0/0 test-data/yx_uint8_mask.zarr/0/zarr.json test-data/yx_uint8_mask.zarr/zarr.json
diffstat 13 files changed, 300 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/creators.xml	Fri Jun 06 12:46:50 2025 +0000
+++ b/creators.xml	Fri Jan 09 14:54:49 2026 +0000
@@ -5,6 +5,11 @@
         <yield />
     </xml>
 
+    <xml name="creators/kostrykin">
+        <person givenName="Leonid" familyName="Kostrykin"/>
+        <yield/>
+    </xml>
+
     <xml name="creators/rmassei">
         <person givenName="Riccardo" familyName="Massei"/>
         <yield/>
@@ -30,4 +35,9 @@
         <yield/>
     </xml>
 
+    <xml name="creators/tuncK">
+        <person givenName="Tunc" familyName="Kayikcioglu"/>
+        <yield/>
+    </xml>
+
 </macros>
--- a/crop_image.py	Fri Jun 06 12:46:50 2025 +0000
+++ b/crop_image.py	Fri Jan 09 14:54:49 2026 +0000
@@ -1,8 +1,13 @@
 import argparse
 import os
 
+import dask.array as da
+import giatools
+import giatools.image
 import numpy as np
-from giatools.image import Image
+
+# Fail early if an optional backend is not available
+giatools.require_backend('omezarr')
 
 
 def crop_image(
@@ -12,21 +17,39 @@
     output_dir: str,
     skip_labels: frozenset[int],
 ):
-    image = Image.read(image_filepath)
-    labelmap = Image.read(labelmap_filepath)
+    axes = giatools.default_normalized_axes
+    image = giatools.Image.read(image_filepath, normalize_axes=axes)
+    labelmap = giatools.Image.read(labelmap_filepath, normalize_axes=axes)
+
+    # Establish compatibility of multi-channel/frame/etc. images with single-channel/frame/etc. label maps
+    original_labelmap_shape = labelmap.shape
+    for image_s, labelmap_s, (axis_idx, axis) in zip(image.shape, labelmap.shape, enumerate(axes)):
+        if image_s > 1 and labelmap_s == 1 and axis not in 'YX':
+            target_shape = list(labelmap.shape)
+            target_shape[axis_idx] = image_s
 
-    if image.axes != labelmap.axes:
-        raise ValueError(f'Axes mismatch between image ({image.axes}) and label map ({labelmap.axes}).')
+            # Broadcast the labelmap data to the target shape without copying
+            if hasattr(labelmap.data, 'compute'):
+                labelmap.data = da.broadcast_to(labelmap.data, target_shape)  # `data` is Dask array
+            else:
+                labelmap.data = np.broadcast_to(labelmap.data, target_shape, subok=True)  # `data` is NumPy array
 
-    if image.data.shape != labelmap.data.shape:
-        raise ValueError(f'Shape mismatch between image ({image.data.shape}) and label map ({labelmap.data.shape}).')
+    # Validate that the shapes of the images are compatible
+    if image.shape != labelmap.shape:
+        labelmap_shape_str = str(original_labelmap_shape)
+        if labelmap.shape != original_labelmap_shape:
+            labelmap_shape_str = f'{labelmap_shape_str}, broadcasted to {labelmap.shape}'
+        raise ValueError(
+            f'Shape mismatch between image {image.shape} and label map {labelmap_shape_str}, with {axes} axes.',
+        )
 
-    for label in np.unique(labelmap.data):
+    # Extract the image crops
+    for label in giatools.image._unique(labelmap.data):
         if label in skip_labels:
             continue
         roi_mask = (labelmap.data == label)
         roi = crop_image_to_mask(image.data, roi_mask)
-        roi_image = Image(roi, image.axes).normalize_axes_like(image.original_axes)
+        roi_image = giatools.Image(roi, image.axes).normalize_axes_like(image.original_axes)
         roi_image.write(os.path.join(output_dir, f'{label}.{output_ext}'))
 
 
@@ -42,8 +65,18 @@
     for dim in range(data.ndim):
         mask1d = mask.any(axis=tuple(i for i in range(mask.ndim) if i != dim))
         mask1d_indices = np.where(mask1d)[0]
+
+        # Convert `mask1d_indices` to a NumPy array if it is a Dask array
+        if hasattr(mask1d_indices, 'compute'):
+            mask1d_indices = mask1d_indices.compute()
+
         mask1d_indices_cvxhull = np.arange(min(mask1d_indices), max(mask1d_indices) + 1)
-        data = data.take(axis=dim, indices=mask1d_indices_cvxhull)
+
+        # Crop the `data` to the minimal bounding box
+        if hasattr(data, 'compute'):
+            data = da.take(data, axis=dim, indices=mask1d_indices_cvxhull)  # `data` is a Dask array
+        else:
+            data = data.take(axis=dim, indices=mask1d_indices_cvxhull)  # `data` is a NumPy array
 
     return data
 
--- a/crop_image.xml	Fri Jun 06 12:46:50 2025 +0000
+++ b/crop_image.xml	Fri Jan 09 14:54:49 2026 +0000
@@ -3,16 +3,18 @@
     <macros>
         <import>creators.xml</import>
         <import>tests.xml</import>
-        <token name="@TOOL_VERSION@">0.4.1</token>
+        <token name="@TOOL_VERSION@">0.7.3</token>
         <token name="@VERSION_SUFFIX@">0</token>
     </macros>
     <creator>
-        <expand macro="creators/bmcv" />
+        <expand macro="creators/bmcv"/>
+        <expand macro="creators/kostrykin"/>
     </creator>
     <edam_operations>
         <edam_operation>operation_3443</edam_operation>
     </edam_operations>
     <xrefs>
+        <xref type="bio.tools">galaxy_image_analysis</xref>
         <xref type="bio.tools">giatools</xref>
     </xrefs>
     <requirements>
@@ -23,17 +25,34 @@
         mkdir ./output &&
         python '$__tool_directory__/crop_image.py'
 
-        '$image'
-        '$labelmap'
+        #if $image.extension == "zarr"
+            '$image.extra_files_path/$image.metadata.store_root'
+        #else
+            '$image'
+        #end if
+
+        #if $labelmap.extension == "zarr"
+            '$labelmap.extra_files_path/$labelmap.metadata.store_root'
+        #else
+            '$labelmap'
+        #end if
+
         '$skip_labels'
-        '${image.ext}'
+
+        #if str($image.ext).lower() == 'png'
+            'png'
+        #else
+            'tiff'
+        #end if
 
         ./output
 
     ]]></command>
     <inputs>
-        <param name="image" type="data" format="png,tiff" label="Image file" help="The image to be cropped."/>
-        <param name="labelmap" type="data" format="png,tiff" label="Label map" help="Each label identifies an individual region of interest, for which a cropped image is produced."/>
+        <param name="image" type="data" format="png,tiff,zarr" label="Image file"
+               help="The image to be cropped."/>
+        <param name="labelmap" type="data" format="png,tiff,zarr" label="Label map"
+               help="Each label identifies an individual region of interest, for which a cropped image is produced."/>
         <param name="skip_labels" type="text" label="Skip labels" value="0" optional="true" help="Comma-separated list of labels for which no cropped image shall be produced.">
             <validator type="regex">^\d+(,\d+)*$|^$</validator>
         </param>
@@ -46,7 +65,7 @@
     <tests>
         <!-- Test 2D TIFF -->
         <test>
-            <param name="image" value="yx_float32.tiff" ftype="tiff"/>
+            <param name="image" value="yx_float32.tiff"/>
             <param name="labelmap" value="yx_uint8.tiff"/>
             <output_collection name="output" type="list" count="2">
                 <expand macro="tests/intensity_image_diff/element" name="1" value="yx_float32_uint8_1.tiff" ftype="tiff"/>
@@ -55,7 +74,7 @@
         </test>
         <!-- Test with `skip_labels` -->
         <test>
-            <param name="image" value="yx_float32.tiff" ftype="tiff"/>
+            <param name="image" value="yx_float32.tiff"/>
             <param name="labelmap" value="yx_uint8.tiff"/>
             <param name="skip_labels" value="0,1"/>
             <output_collection name="output" type="list" count="1">
@@ -64,7 +83,7 @@
         </test>
         <!-- Test with empty `skip_labels` -->
         <test>
-            <param name="image" value="yx_float32.tiff" ftype="tiff"/>
+            <param name="image" value="yx_float32.tiff"/>
             <param name="labelmap" value="yx_uint8.tiff"/>
             <param name="skip_labels" value=""/>
             <output_collection name="output" type="list" count="3">
@@ -75,18 +94,55 @@
         </test>
         <!-- Test 3D TIFF (multi-frame) -->
         <test>
-            <param name="image" value="zyx_uint16.tiff" ftype="tiff"/>
+            <param name="image" value="zyx_uint16.tiff"/>
             <param name="labelmap" value="yxz_uint8.tiff"/>
             <output_collection name="output" type="list" count="1">
-                <expand macro="tests/intensity_image_diff/element" name="1" value="zyx_uint16_uint8_1.tiff" ftype="tiff"/>
+                <expand macro="tests/intensity_image_diff/element" name="1" value="zyx_uint16_uint8_1.tiff" ftype="tiff">
+                    <has_image_width width="5"/>
+                    <has_image_height height="3"/>
+                    <has_image_depth depth="6"/>
+                </expand>
+            </output_collection>
+        </test>
+        <!-- Test PNG (multi-channel) -->
+        <test>
+            <param name="image" value="yxc_uint8.png"/>
+            <param name="labelmap" value="yxc_uint8_mask.png"/>
+            <output_collection name="output" type="list" count="1">
+                <expand macro="tests/intensity_image_diff/element" name="2" value="yxc_uint8_uint8_2.png" ftype="png">
+                    <has_image_width width="10"/>
+                    <has_image_height height="12"/>
+                    <has_image_channels channels="3"/>
+                </expand>
             </output_collection>
         </test>
-        <!-- Test PNG -->
+        <!-- Test PNG+Zarr (multi-channel `image` PNG with single-channel `labelmap` Zarr) -->
+        <test>
+            <param name="image" value="yxc_uint8.png"/>
+            <param name="labelmap" value="yx_uint8_mask.zarr"/>
+            <output_collection name="output" type="list" count="1">
+                <expand macro="tests/intensity_image_diff/element" name="2" value="yxc_uint8_uint8_2.png" ftype="png">
+                    <has_image_width width="10"/>
+                    <has_image_height height="12"/>
+                    <has_image_channels channels="3"/>
+                </expand>
+            </output_collection>
+        </test>
+        <!-- Test Zarr+TIFF (multi-channel `image` Zarr with single-channel `labelmap` TIFF) -->
         <test>
-            <param name="image" value="yxc_uint8.png" ftype="png"/>
-            <param name="labelmap" value="yxc_uint8_mask.png"/>
+            <param name="image" value="cyx_uint8.zarr"/>
+            <param name="labelmap" value="yx_uint8_mask.tiff"/>
             <output_collection name="output" type="list" count="1">
-                <expand macro="tests/intensity_image_diff/element" name="2" value="yxc_uint8_uint8_2.png" ftype="png"/>
+                <!--
+                OME-Zarr requires a specific axes order. For this reason, it is not possible to generally write "any" image as an OME-Zarr.
+                This would require adapting the axes order, which is do-able, but changes the image. This might be unintended or unexpected
+                in many cases, which is why it is not happening automatically. We might change this behaviour somehow in the future.
+                -->
+                <expand macro="tests/intensity_image_diff/element" name="2" value="cyx_uint8_uint8.tiff" ftype="tiff">
+                    <has_image_width width="10"/>
+                    <has_image_height height="12"/>
+                    <has_image_channels channels="3"/>
+                </expand>
             </output_collection>
         </test>
     </tests>
@@ -94,9 +150,12 @@
 
         **Crops an image using one or more regions of interest.**
 
-        The image is cropped using a label map that identifies individual regions of interest. The image and the label map must be of equal size.
+        The image is cropped using a label map that identifies individual regions of interest. The image and the label map must be of
+        compatible size. The sizes are compatible if they are equal, or, if the label map can be broadcasted to the size of the image
+        (e.g., if the image is a multi-channel image and the label map is single-channel but has identical width and height).
 
-        This operation preserves the file type of the image, the brightness, and the range of values.
+        This operation preserves the brightness and the range of values of the input image. The file format is also preserved, unless
+        the input image is a Zarr, for which the output image file format is TIFF.
 
     </help>
     <citations>
Binary file test-data/cyx_uint8.zarr/0/c/0/0/0 has changed
Binary file test-data/cyx_uint8.zarr/0/c/1/0/0 has changed
Binary file test-data/cyx_uint8.zarr/0/c/2/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/cyx_uint8.zarr/0/zarr.json	Fri Jan 09 14:54:49 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    3,
+    16,
+    20
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        16,
+        20
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": 0,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "C",
+    "Y",
+    "X"
+  ],
+  "zarr_format": 3,
+  "node_type": "array",
+  "storage_transformers": []
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/cyx_uint8.zarr/zarr.json	Fri Jan 09 14:54:49 2026 +0000
@@ -0,0 +1,43 @@
+{
+  "attributes": {
+    "ome": {
+      "version": "0.5",
+      "multiscales": [
+        {
+          "datasets": [
+            {
+              "path": "0",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    1.0,
+                    1.0
+                  ]
+                }
+              ]
+            }
+          ],
+          "name": "/",
+          "axes": [
+            {
+              "name": "C",
+              "type": "channel"
+            },
+            {
+              "name": "Y",
+              "type": "space"
+            },
+            {
+              "name": "X",
+              "type": "space"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "zarr_format": 3,
+  "node_type": "group"
+}
\ No newline at end of file
Binary file test-data/cyx_uint8_uint8.tiff has changed
Binary file test-data/yx_uint8_mask.tiff has changed
Binary file test-data/yx_uint8_mask.zarr/0/c/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/yx_uint8_mask.zarr/0/zarr.json	Fri Jan 09 14:54:49 2026 +0000
@@ -0,0 +1,43 @@
+{
+  "shape": [
+    16,
+    20
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        16,
+        20
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": 0,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "Y",
+    "X"
+  ],
+  "zarr_format": 3,
+  "node_type": "array",
+  "storage_transformers": []
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/yx_uint8_mask.zarr/zarr.json	Fri Jan 09 14:54:49 2026 +0000
@@ -0,0 +1,38 @@
+{
+  "attributes": {
+    "ome": {
+      "version": "0.5",
+      "multiscales": [
+        {
+          "datasets": [
+            {
+              "path": "0",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    1.0
+                  ]
+                }
+              ]
+            }
+          ],
+          "name": "/",
+          "axes": [
+            {
+              "name": "Y",
+              "type": "space"
+            },
+            {
+              "name": "X",
+              "type": "space"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "zarr_format": 3,
+  "node_type": "group"
+}
\ No newline at end of file