changeset 4:8f0dd9a58ec3 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/binary2labelimage/ commit f5a4de7535e433e3b0e96e0694e481b6643a54f8
author imgteam
date Sat, 03 Jan 2026 14:14:05 +0000
parents a041e4e9d449
children
files 2d_split_binaryimage_by_watershed.py binary2label.py binary2label.xml creators.xml test-data/galaxyIcon_noText.tiff test-data/in.tiff test-data/input/README.md test-data/input/input10.zarr/0/c/0/0/0 test-data/input/input10.zarr/0/c/1/0/0 test-data/input/input10.zarr/0/zarr.json test-data/input/input10.zarr/1/c/0/0/0 test-data/input/input10.zarr/1/c/1/0/0 test-data/input/input10.zarr/1/zarr.json test-data/input/input10.zarr/2/c/0/0/0 test-data/input/input10.zarr/2/c/1/0/0 test-data/input/input10.zarr/2/zarr.json test-data/input/input10.zarr/3/c/0/0/0 test-data/input/input10.zarr/3/c/1/0/0 test-data/input/input10.zarr/3/zarr.json test-data/input/input10.zarr/4/c/1/0/0 test-data/input/input10.zarr/4/zarr.json test-data/input/input10.zarr/zarr.json test-data/input/input11.tiff test-data/input/input9.tiff test-data/input/input9.zarr/0/c/0/0/0 test-data/input/input9.zarr/0/c/1/0/0 test-data/input/input9.zarr/0/zarr.json test-data/input/input9.zarr/1/c/0/0/0 test-data/input/input9.zarr/1/c/1/0/0 test-data/input/input9.zarr/1/zarr.json test-data/input/input9.zarr/2/c/0/0/0 test-data/input/input9.zarr/2/c/1/0/0 test-data/input/input9.zarr/2/zarr.json test-data/input/input9.zarr/3/c/0/0/0 test-data/input/input9.zarr/3/c/1/0/0 test-data/input/input9.zarr/3/zarr.json test-data/input/input9.zarr/4/c/0/0/0 test-data/input/input9.zarr/4/c/1/0/0 test-data/input/input9.zarr/4/zarr.json test-data/input/input9.zarr/zarr.json test-data/input/rgb.png test-data/label.tiff test-data/out.tiff test-data/output/input11-cca.tiff test-data/output/input11-watershed.tiff test-data/output/input9-cca.tiff test-data/uint8_z12_x11_y10-output.tiff test-data/uint8_z12_x11_y10.tiff tests.xml validators.xml
diffstat 50 files changed, 1082 insertions(+), 92 deletions(-) [+]
line wrap: on
line diff
--- a/2d_split_binaryimage_by_watershed.py	Mon May 12 08:15:32 2025 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-import argparse
-import sys
-
-import numpy as np
-import skimage.io
-import skimage.util
-from scipy import ndimage as ndi
-from skimage.feature import peak_local_max
-from skimage.segmentation import watershed
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description='Split binaryimage by watershed')
-    parser.add_argument('input_file', type=argparse.FileType('r'), default=sys.stdin, help='input file')
-    parser.add_argument('out_file', type=argparse.FileType('w'), default=sys.stdin, help='out file (TIFF)')
-    parser.add_argument('min_distance', type=int, default=100, help='Minimum distance to next object')
-    args = parser.parse_args()
-
-    img_in = skimage.io.imread(args.input_file.name)
-    distance = ndi.distance_transform_edt(img_in)
-
-    local_max_indices = peak_local_max(
-        distance,
-        min_distance=args.min_distance,
-        labels=img_in,
-    )
-    local_max_mask = np.zeros(img_in.shape, dtype=bool)
-    local_max_mask[tuple(local_max_indices.T)] = True
-    markers = ndi.label(local_max_mask)[0]
-    res = watershed(-distance, markers, mask=img_in)
-
-    res = skimage.util.img_as_uint(res)
-    skimage.io.imsave(args.out_file.name, res, plugin="tifffile")
--- a/binary2label.py	Mon May 12 08:15:32 2025 +0000
+++ b/binary2label.py	Sat Jan 03 14:14:05 2026 +0000
@@ -1,27 +1,70 @@
-import argparse
+import giatools
+import numpy as np
+import scipy.ndimage as ndi
+
+# Fail early if an optional backend is not available
+giatools.require_backend('omezarr')
+
 
-import giatools
-import scipy.ndimage as ndi
-import tifffile
+def label_watershed(arr: np.ndarray, **kwargs) -> np.ndarray:
+    import skimage.util
+    from skimage.feature import peak_local_max
+    from skimage.segmentation import watershed
+    distance = ndi.distance_transform_edt(arr)
+    local_max_indices = peak_local_max(
+        distance,
+        labels=arr,
+        **kwargs,
+    )
+    local_max_mask = np.zeros(arr.shape, dtype=bool)
+    local_max_mask[tuple(local_max_indices.T)] = True
+    markers = ndi.label(local_max_mask)[0]
+    res = watershed(-distance, markers, mask=arr)
+    return skimage.util.img_as_uint(res)  # converts to uint16
 
 
-# Parse CLI parameters
-parser = argparse.ArgumentParser()
-parser.add_argument('input', type=str, help='input file')
-parser.add_argument('output', type=str, help='output file (TIFF)')
-args = parser.parse_args()
+if __name__ == '__main__':
+
+    tool = giatools.ToolBaseplate()
+    tool.add_input_image('input')
+    tool.add_output_image('output')
+    tool.parse_args()
+
+    # Validate the input image and the selected method
+    try:
+        input_image = tool.args.input_images['input']
+        if (method := tool.args.params.pop('method')) == 'watershed' and input_image.shape[input_image.axes.index('Z')] > 1:
+            raise ValueError(f'Method "{method}" is not applicable to 3-D images.')
+
+        elif input_image.shape[input_image.axes.index('C')] > 1:
+            raise ValueError('Multi-channel images are forbidden to avoid confusion with multi-channel labels (e.g., RGB labels).')
+
+        else:
+
+            # Choose the requested labeling method
+            match method:
 
-# Read the input image with the original axes
-img = giatools.Image.read(args.input)
-img = img.normalize_axes_like(
-    img.original_axes,
-)
+                case 'cca':
+                    joint_axes = 'ZYX'
+                    label = lambda input_section_bin: (  # noqa: E731
+                        ndi.label(input_section_bin, **tool.args.params)[0].astype(np.uint16)
+                    )
+
+                case 'watershed':
+                    joint_axes = 'YX'
+                    label = lambda input_section_bin: (  # noqa: E731
+                        label_watershed(input_section_bin, **tool.args.params)  # already uint16
+                    )
 
-# Make sure the image is truly binary
-img_arr_bin = (img.data > 0)
+                case _:
+                    raise ValueError(f'Unknown method: "{method}"')
 
-# Perform the labeling
-img.data = ndi.label(img_arr_bin)[0]
+        # Perform the labeling
+        for section in tool.run(joint_axes):
+            section['output'] = label(
+                section['input'].data > 0,  # ensure that the input data is truly binary
+            )
 
-# Write the result image (same axes as input image)
-tifffile.imwrite(args.output, img.data, metadata=dict(axes=img.axes))
+    # Exit and print error to stderr
+    except ValueError as err:
+        exit(err.args[0])
--- a/binary2label.xml	Mon May 12 08:15:32 2025 +0000
+++ b/binary2label.xml	Sat Jan 03 14:14:05 2026 +0000
@@ -1,83 +1,209 @@
 <tool id="ip_binary_to_labelimage" name="Convert binary image to label map" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@">
-    <description></description>
+    <description>with giatools</description>
     <macros>
         <import>creators.xml</import>
-        <token name="@TOOL_VERSION@">0.6</token>
+        <import>tests.xml</import>
+        <import>validators.xml</import>
+        <token name="@TOOL_VERSION@">0.7.3</token>
         <token name="@VERSION_SUFFIX@">0</token>
+        <xml name="input">
+            <!-- JPEG is not allowed because it is a lossy compression that has no strictly constant labels -->
+            <param name="input" type="data" format="tiff,zarr,png" label="Binary image">
+                <expand macro="validators/is_binary"/>
+                <yield/>
+            </param>
+        </xml>
     </macros>
     <creator>
         <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>
-        <requirement type="package" version="0.4.0">giatools</requirement>
-        <requirement type="package" version="1.12.0">scipy</requirement>
+        <requirement type="package" version="@TOOL_VERSION@">giatools</requirement>
+        <requirement type="package" version="1.16.3">scipy</requirement>
+        <requirement type="package" version="0.12.2">ome-zarr</requirement>
     </requirements>
-    <command detect_errors="aggressive">
-        <![CDATA[
-        #if str($mode.mode_selector) == 'cca':
-            python '$__tool_directory__/binary2label.py' '$input' '$output'
-        #elif str($mode.mode_selector) == 'watershed':
-            python '$__tool_directory__/2d_split_binaryimage_by_watershed.py' '$input' '$output' $min_distance
+    <required_files>
+        <include type="literal" path="binary2label.py"/>
+    </required_files>
+    <command detect_errors="aggressive"><![CDATA[
+
+        python '$__tool_directory__/binary2label.py'
+
+        #if $setup.input.extension == "zarr"
+            --input '$setup.input.extra_files_path/$setup.input.metadata.store_root'
+        #else
+            --input '$setup.input'
         #end if
-        ]]>
-    </command>
+
+        --output 'output.tiff'
+        --params '$params'
+        --verbose
+
+    ]]></command>
+    <configfiles>
+        <configfile name="params"><![CDATA[
+            {
+
+            #if str($setup.method) == "watershed"
+                "min_distance": $setup.min_distance,
+            #end if
+
+                "method": "$setup.method"
+
+            }
+        ]]></configfile>
+    </configfiles>
     <inputs>
-        <param name="input" type="data" format="tiff,png,jpg,bmp" label="Binary image"/>
-        <conditional name="mode">
-            <param name="mode_selector" type="select" label="Mode">
+        <conditional name="setup">
+            <param name="method" type="select" label="Mode"
+                   help="Connected component analysis assigns unique labels to objects that are separated by 1 pixel or more. Watershed transform can also separate partially overlapping objects, but is only applicable to 2-D image data.">
                 <option value="cca" selected="true">Connected component analysis</option>
                 <option value="watershed">Watershed transform</option>
             </param>
             <when value="cca">
+                <expand macro="input"/>
             </when>
             <when value="watershed">
-                <param name="min_distance" type="integer" min="0" value="5" label="Minimum distance between two objects" />
+                <expand macro="input">
+                    <expand macro="validators/is_2d"/>
+                </expand>
+                <param name="min_distance" type="integer" min="0" value="5" label="Minimum distance between two objects"/>
             </when>
         </conditional>
     </inputs>
     <outputs>
-        <data format="tiff" name="output"/>
+        <data format="tiff" name="output" from_work_dir="output.tiff"/>
     </outputs>
     <tests>
+        <!-- Tests for 2-D -->
         <test>
-            <param name="input" value="galaxyIcon_noText.tiff" />
-            <conditional name="mode">
-                <param name="mode_selector" value="cca" />
+            <conditional name="setup">
+                <param name="method" value="cca"/>
+                <param name="input" value="input/input11.tiff"/>
             </conditional>
-            <output name="output" value="label.tiff" ftype="tiff" compare="image_diff"/>
+            <expand macro="tests/label_image_diff" name="output" value="output/input11-cca.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="[input] Input image axes: YX"/>
+                <has_line line="[input] Input image shape: (265, 329)"/>
+                <has_line line="[input] Input image dtype: uint16"/>
+                <has_line line="[output] Output image axes: YX"/>
+                <has_line line="[output] Output image shape: (265, 329)"/>
+                <has_line line="[output] Output image dtype: uint16"/>
+            </assert_stdout>
         </test>
         <test>
-            <param name="input" value="in.tiff"/>
-            <conditional name="mode">
-                <param name="mode_selector" value="watershed" />
-                <param name="min_distance" value="10" />
+            <conditional name="setup">
+                <param name="method" value="watershed"/>
+                <param name="input" value="input/input11.tiff"/>
+                <param name="min_distance" value="10"/>
             </conditional>
-            <output name="output" value="out.tiff" ftype="tiff" compare="image_diff"/>
+            <expand macro="tests/label_image_diff" name="output" value="output/input11-watershed.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="[input] Input image axes: YX"/>
+                <has_line line="[input] Input image shape: (265, 329)"/>
+                <has_line line="[input] Input image dtype: uint16"/>
+                <has_line line="[output] Output image axes: YX"/>
+                <has_line line="[output] Output image shape: (265, 329)"/>
+                <has_line line="[output] Output image dtype: uint16"/>
+            </assert_stdout>
         </test>
+        <!-- Tests for 3-D -->
         <test>
-            <param name="input" value="uint8_z12_x11_y10.tiff"/>
-            <conditional name="mode">
-                <param name="mode_selector" value="cca" />
+            <conditional name="setup">
+                <param name="method" value="cca"/>
+                <param name="input" value="input/input9.zarr"/>
+            </conditional>
+            <!-- `label_image_diff` currently does not support 3-D images: https://github.com/galaxyproject/galaxy/pull/21455 -->
+            <expand macro="tests/intensity_image_diff" name="output" value="output/input9-cca.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="[input] Input image axes: ZYX"/>
+                <has_line line="[input] Input image shape: (2, 100, 100)"/>
+                <has_line line="[input] Input image dtype: bool"/>
+                <has_line line="[input] Input image resolution=(1.0, 1.0), unit='um', z_spacing=1.0"/>
+                <has_line line="[output] Output image axes: ZYX"/>
+                <has_line line="[output] Output image shape: (2, 100, 100)"/>
+                <has_line line="[output] Output image dtype: uint16"/>
+                <has_line line="[output] Output image resolution=(1.0, 1.0), unit='um', z_spacing=1.0"/>
+            </assert_stdout>
+        </test>
+        <test expect_failure="true">
+            <conditional name="setup">
+                <param name="method" value="watershed"/>
+                <param name="input" value="input/input9.zarr"/>
+            </conditional>
+            <assert_stderr>
+                <!-- Rejected by py-script -->
+                <has_text text='Method "watershed" is not applicable to 3-D images.'/>
+            </assert_stderr>
+            <assert_stdout>
+                <has_line line="[input] Input image axes: ZYX"/>
+                <has_line line="[input] Input image shape: (2, 100, 100)"/>
+                <has_line line="[input] Input image dtype: bool"/>
+                <has_line line="[input] Input image resolution=(1.0, 1.0), unit='um', z_spacing=1.0"/>
+            </assert_stdout>
+        </test>
+        <test expect_failure="true">
+            <conditional name="setup">
+                <param name="method" value="watershed"/>
+                <param name="input" value="input/input9.tiff"/>
             </conditional>
-            <output name="output" value="uint8_z12_x11_y10-output.tiff" ftype="tiff" compare="image_diff">
-                <assert_contents>
-                    <has_image_width width="11"/>
-                    <has_image_height height="10"/>
-                    <has_image_depth depth="12"/>
-                </assert_contents>
-            </output>
+            <assert_stderr>
+                <!-- Rejected by validator -->
+                <has_n_lines n="0"/>
+            </assert_stderr>
+            <assert_stdout>
+                <!-- Rejected by validator -->
+                <has_n_lines n="0"/>
+            </assert_stdout>
+        </test>
+        <!-- Tests for multi-channel images -->
+        <test expect_failure="true">
+            <conditional name="setup">
+                <param name="method" value="cca"/>
+                <param name="input" value="input/input10.zarr"/>
+            </conditional>
+            <assert_stderr>
+                <!-- Rejected by py-script -->
+                <has_text text='Multi-channel images are forbidden to avoid confusion with multi-channel labels (e.g., RGB labels).'/>
+            </assert_stderr>
+            <assert_stdout>
+                <has_line line="[input] Input image axes: CYX"/>
+                <has_line line="[input] Input image shape: (2, 64, 64)"/>
+                <has_line line="[input] Input image dtype: uint8"/>
+                <has_line line="[input] Input image resolution=(1.0, 1.0)"/>
+            </assert_stdout>
+        </test>
+        <test expect_failure="true">
+            <conditional name="setup">
+                <param name="method" value="cca"/>
+                <param name="input" value="input/rgb.png"/>
+            </conditional>
+            <assert_stderr>
+                <!-- Rejected by validator -->
+                <has_n_lines n="0"/>
+            </assert_stderr>
+            <assert_stdout>
+                <!-- Rejected by validator -->
+                <has_n_lines n="0"/>
+            </assert_stdout>
         </test>
     </tests>
     <help>
-    This tool assigns each object a unique label.
+
+        **Converts a binary image to a label map.**
 
-    Individual objects are determined using connected component analysis, or distance transform and watershed.
+        This tool assigns each object a unique label.
+
+        Individual objects are determined using connected component analysis, or distance transform and watershed.
+
     </help>
     <citations>
         <citation type="doi">10.1016/j.jbiotec.2017.07.019</citation>
--- a/creators.xml	Mon May 12 08:15:32 2025 +0000
+++ b/creators.xml	Sat Jan 03 14:14:05 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/>
@@ -24,5 +29,15 @@
         <person givenName="Till" familyName="Korten"/>
         <yield/>
     </xml>
-    
+
+    <xml name="creators/pavanvidem">
+        <person givenName="Pavan" familyName="Videm"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/tuncK">
+        <person givenName="Tunc" familyName="Kayikcioglu"/>
+        <yield/>
+    </xml>
+
 </macros>
Binary file test-data/galaxyIcon_noText.tiff has changed
Binary file test-data/in.tiff has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/README.md	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+# Overview of the test images
+
+## `input9.tiff`:
+
+- axes: `ZYX`
+- resolution: `(2, 100, 100)`
+- dtype: `bool`
+- binary image
+- metadata:
+  - resolution: `(1.0, 1.0)`
+  - z-spacing: `1.0`
+  - unit: `um`
+
+## `input9.zarr`:
+
+- axes: `ZYX`
+- resolution: `(2, 100, 100)`
+- dtype: `bool`
+- binary image
+- metadata:
+  - resolution: `(1.0, 1.0)`
+  - z-spacing: `1.0`
+  - unit: `um`
+
+## `input10.zarr`:
+
+- axes: `CYX`
+- resolution: `(2, 64, 64)`
+- dtype: `uint8`
+- metadata:
+  - resolution: `(1.0, 1.0)`
+  - z-spacing: `1.0`
+  - unit: `um`
+
+## `input11.tiff`
+
+- axes: `YX`
+- resolution: `(265, 329)`
+- dtype: `uint16`
+- binary image
+
+## `rgb.png`:
+
+- axes: `YXC`
+- resolution: `(6, 6, 3)`
+- dtype: `uint8`
Binary file test-data/input/input10.zarr/0/c/0/0/0 has changed
Binary file test-data/input/input10.zarr/0/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input10.zarr/0/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    64,
+    64
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "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
Binary file test-data/input/input10.zarr/1/c/0/0/0 has changed
Binary file test-data/input/input10.zarr/1/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input10.zarr/1/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    32,
+    32
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "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
Binary file test-data/input/input10.zarr/2/c/0/0/0 has changed
Binary file test-data/input/input10.zarr/2/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input10.zarr/2/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    16,
+    16
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "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
Binary file test-data/input/input10.zarr/3/c/0/0/0 has changed
Binary file test-data/input/input10.zarr/3/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input10.zarr/3/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    8,
+    8
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "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
Binary file test-data/input/input10.zarr/4/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input10.zarr/4/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    4,
+    4
+  ],
+  "data_type": "uint8",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "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/input/input10.zarr/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,95 @@
+{
+  "attributes": {
+    "ome": {
+      "version": "0.5",
+      "multiscales": [
+        {
+          "datasets": [
+            {
+              "path": "0",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    1.0,
+                    1.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "1",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    2.0,
+                    2.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "2",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    4.0,
+                    4.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "3",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    8.0,
+                    8.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "4",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    16.0,
+                    16.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/input/input11.tiff has changed
Binary file test-data/input/input9.tiff has changed
Binary file test-data/input/input9.zarr/0/c/0/0/0 has changed
Binary file test-data/input/input9.zarr/0/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input9.zarr/0/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    100,
+    100
+  ],
+  "data_type": "bool",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": false,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "z",
+    "y",
+    "x"
+  ],
+  "zarr_format": 3,
+  "node_type": "array",
+  "storage_transformers": []
+}
\ No newline at end of file
Binary file test-data/input/input9.zarr/1/c/0/0/0 has changed
Binary file test-data/input/input9.zarr/1/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input9.zarr/1/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    50,
+    50
+  ],
+  "data_type": "bool",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": false,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "z",
+    "y",
+    "x"
+  ],
+  "zarr_format": 3,
+  "node_type": "array",
+  "storage_transformers": []
+}
\ No newline at end of file
Binary file test-data/input/input9.zarr/2/c/0/0/0 has changed
Binary file test-data/input/input9.zarr/2/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input9.zarr/2/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    25,
+    25
+  ],
+  "data_type": "bool",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": false,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "z",
+    "y",
+    "x"
+  ],
+  "zarr_format": 3,
+  "node_type": "array",
+  "storage_transformers": []
+}
\ No newline at end of file
Binary file test-data/input/input9.zarr/3/c/0/0/0 has changed
Binary file test-data/input/input9.zarr/3/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input9.zarr/3/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    12,
+    12
+  ],
+  "data_type": "bool",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": false,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "z",
+    "y",
+    "x"
+  ],
+  "zarr_format": 3,
+  "node_type": "array",
+  "storage_transformers": []
+}
\ No newline at end of file
Binary file test-data/input/input9.zarr/4/c/0/0/0 has changed
Binary file test-data/input/input9.zarr/4/c/1/0/0 has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input/input9.zarr/4/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,46 @@
+{
+  "shape": [
+    2,
+    6,
+    6
+  ],
+  "data_type": "bool",
+  "chunk_grid": {
+    "name": "regular",
+    "configuration": {
+      "chunk_shape": [
+        1,
+        100,
+        100
+      ]
+    }
+  },
+  "chunk_key_encoding": {
+    "name": "default",
+    "configuration": {
+      "separator": "/"
+    }
+  },
+  "fill_value": false,
+  "codecs": [
+    {
+      "name": "bytes"
+    },
+    {
+      "name": "zstd",
+      "configuration": {
+        "level": 0,
+        "checksum": false
+      }
+    }
+  ],
+  "attributes": {},
+  "dimension_names": [
+    "z",
+    "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/input/input9.zarr/zarr.json	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,98 @@
+{
+  "attributes": {
+    "ome": {
+      "version": "0.5",
+      "multiscales": [
+        {
+          "datasets": [
+            {
+              "path": "0",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    1.0,
+                    1.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "1",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    2.0,
+                    2.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "2",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    4.0,
+                    4.0
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "3",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    8.333333333333334,
+                    8.333333333333334
+                  ]
+                }
+              ]
+            },
+            {
+              "path": "4",
+              "coordinateTransformations": [
+                {
+                  "type": "scale",
+                  "scale": [
+                    1.0,
+                    16.666666666666668,
+                    16.666666666666668
+                  ]
+                }
+              ]
+            }
+          ],
+          "name": "/",
+          "axes": [
+            {
+              "name": "z",
+              "type": "space",
+              "unit": "micrometer"
+            },
+            {
+              "name": "y",
+              "type": "space",
+              "unit": "micrometer"
+            },
+            {
+              "name": "x",
+              "type": "space",
+              "unit": "micrometer"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "zarr_format": 3,
+  "node_type": "group"
+}
Binary file test-data/input/rgb.png has changed
Binary file test-data/label.tiff has changed
Binary file test-data/out.tiff has changed
Binary file test-data/output/input11-cca.tiff has changed
Binary file test-data/output/input11-watershed.tiff has changed
Binary file test-data/output/input9-cca.tiff has changed
Binary file test-data/uint8_z12_x11_y10-output.tiff has changed
Binary file test-data/uint8_z12_x11_y10.tiff has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests.xml	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,95 @@
+<macros>
+
+    <!-- Macros for verification of image outputs -->
+
+    <xml
+        name="tests/binary_image_diff"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="mae"
+        token_eps="0.01">
+
+        <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="0">
+            <assert_contents>
+                <has_image_n_labels n="2"/>
+                <yield/>
+            </assert_contents>
+        </output>
+
+    </xml>
+
+    <xml
+        name="tests/label_image_diff"
+        tokens="name,value,ftype,metric,eps,pin_labels"
+        token_metric="iou"
+        token_eps="0.01"
+        token_pin_labels="0">
+
+        <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="@PIN_LABELS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </output>
+
+    </xml>
+
+    <xml
+        name="tests/intensity_image_diff"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="rms"
+        token_eps="0.01">
+
+        <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </output>
+
+    </xml>
+
+    <!-- Variants of the above for verification of collection elements -->
+
+    <xml
+        name="tests/binary_image_diff/element"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="mae"
+        token_eps="0.01">
+
+        <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="0">
+            <assert_contents>
+                <has_image_n_labels n="2"/>
+                <yield/>
+            </assert_contents>
+        </element>
+
+    </xml>
+
+    <xml
+        name="tests/label_image_diff/element"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="iou"
+        token_eps="0.01"
+        token_pin_labels="0">
+
+        <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="@PIN_LABELS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </element>
+
+    </xml>
+
+    <xml
+        name="tests/intensity_image_diff/element"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="rms"
+        token_eps="0.01">
+
+        <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </element>
+
+    </xml>
+
+</macros>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/validators.xml	Sat Jan 03 14:14:05 2026 +0000
@@ -0,0 +1,45 @@
+<macros>
+
+    <!-- Macros for validation of inputs -->
+
+    <xml name="validators/is_single_channel">
+        <!--
+        The OME-Zarr datatype in Galaxy is currently not derived from the Image datatype, and it does
+        hence not inherit the metadata fields like `channels`. To cope with that, we allow all datasets
+        except those where we *know* that they are *not* single-channel.
+        -->
+        <validator type="expression" message="Dataset is a multi-channel image"
+            ><![CDATA[getattr(value.metadata, "channels", None) in (None, '') or int(value.metadata.channels) < 2]]></validator>
+    </xml>
+
+    <xml name="validators/is_single_frame">
+        <!--
+        The OME-Zarr datatype in Galaxy is currently not derived from the Image datatype, and it does
+        hence not inherit the metadata fields like `frames`. To cope with that, we allow all datasets
+        except those where we *know* that they are *not* single-frame.
+        -->
+        <validator type="expression" message="Dataset is a multi-frame image"
+            ><![CDATA[getattr(value.metadata, "frames", None) in (None, '') or int(value.metadata.frames) < 2]]></validator>
+    </xml>
+
+    <xml name="validators/is_2d">
+        <!--
+        The OME-Zarr datatype in Galaxy is currently not derived from the Image datatype, and it does
+        hence not inherit the metadata fields like `depth`. To cope with that, we allow all datasets
+        except those where we *know* that they are *not* 2-D.
+        -->
+        <validator type="expression" message="Dataset is a 3-D image"
+            ><![CDATA[getattr(value.metadata, "depth", None) in (None, '') or int(value.metadata.depth) < 2]]></validator>
+    </xml>
+
+    <xml name="validators/is_binary">
+        <!--
+        The OME-Zarr datatype in Galaxy is currently not derived from the Image datatype, and it does
+        hence not inherit the metadata fields like `num_unique_values`. To cope with that, we allow all
+        datasets except those where we *know* that they are *not* binary.
+        -->
+        <validator type="expression" message="Dataset is not a binary image"
+            ><![CDATA[getattr(value.metadata, "num_unique_values", None) in (None, '') or int(value.metadata.num_unique_values) <= 2]]></validator>
+    </xml>
+
+</macros>