changeset 6:999c5941a6f0 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/concat_channels/ commit a94f04c109c545a9f892a6ce7a5ffef152253201
author imgteam
date Fri, 12 Dec 2025 21:15:56 +0000
parents 8d50a0a9e4af
children
files concat_channels.py concat_channels.xml test-data/heart_ct_3923.tiff test-data/heart_ct_3953.tiff test-data/heart_ct_3983.tiff test-data/heart_ct_4043.tiff
diffstat 6 files changed, 200 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/concat_channels.py	Sun Dec 07 16:16:03 2025 +0000
+++ b/concat_channels.py	Fri Dec 12 21:15:56 2025 +0000
@@ -1,4 +1,5 @@
 import argparse
+from typing import Any
 
 import giatools
 import numpy as np
@@ -6,20 +7,19 @@
 import skimage.util
 
 
-normalized_axes = 'QTZYXC'
-
-
 def concat_channels(
     input_image_paths: list[str],
     output_image_path: str,
     axis: str,
     preserve_values: bool,
+    sort_by: str | None,
 ):
     # Create list of arrays to be concatenated
-    images = []
+    images = list()
+    metadata = dict()
     for image_path in input_image_paths:
 
-        img = giatools.Image.read(image_path, normalize_axes=normalized_axes)
+        img = giatools.Image.read(image_path, normalize_axes=giatools.default_normalized_axes)
         arr = img.data
 
         # Preserve values: Convert to `float` dtype without changing the values
@@ -30,25 +30,106 @@
         else:
             arr = skimage.util.img_as_float(arr)
 
+        # Record the metadata
+        for metadata_key, metadata_value in img.metadata.items():
+            metadata.setdefault(metadata_key, list())
+            metadata[metadata_key].append(metadata_value)
+
+        # Record the image data
         images.append(arr)
 
+    # Perform sorting, if requested
+    if sort_by is not None:
+
+        # Validate that `sort_by` is available as metadata for all images
+        sort_keys = list(
+            filter(
+                lambda value: value is not None,
+                metadata.get(sort_by, list()),
+            ),
+        )
+        if len(sort_keys) != len(images):
+            raise ValueError(
+                f'Requested to sort by "{sort_by}", '
+                f'but this is not available for all {len(images)} images'
+                f' (available for only {len(sort_keys)} images)'
+            )
+
+        # Sort images by the corresponding `sort_key` metadata value
+        sorted_indices = sorted(range(len(images)), key=lambda i: sort_keys[i])
+        images = [images[i] for i in sorted_indices]
+
+    # Determine consensual metadata
+    # TODO: Convert metadata of images with different units of measurement into a common unit
+    final_metadata = dict()
+    for metadata_key, metadata_values in metadata.items():
+        if (metadata_value := reduce_metadata(metadata_values)) is not None:
+            final_metadata[metadata_key] = metadata_value
+
+    # Update the `z_spacing` metadata, if concatenating along the Z-axis and `z_position` is available for all images
+    if axis == 'Z' and len(images) >= 2 and len(z_positions := metadata.get('z_position', list())) == len(images):
+        z_positions = sorted(z_positions)  # don't mutate the `metadata` dictionary for easier future code maintenance
+        final_metadata['z_spacing'] = abs(np.subtract(z_positions[1:], z_positions[:-1]).mean())
+
     # Do the concatenation
-    axis_pos = normalized_axes.index(axis)
+    axis_pos = giatools.default_normalized_axes.index(axis)
     arr = np.concatenate(images, axis_pos)
-    res = giatools.Image(arr, normalized_axes)
+    res = giatools.Image(
+        data=arr,
+        axes=giatools.default_normalized_axes,
+        metadata=final_metadata,
+    )
 
     # Squeeze singleton axes and save
-    squeezed_axes = ''.join(np.array(list(res.axes))[np.array(arr.shape) > 1])
-    res = res.squeeze_like(squeezed_axes)
+    res = res.squeeze()
+    print('Output TIFF shape:', res.data.shape)
+    print('Output TIFF axes:', res.axes)
+    print('Output TIFF', metadata_to_str(final_metadata))
     res.write(output_image_path, backend='tifffile')
 
 
+def reduce_metadata(values: list[Any]) -> Any | None:
+    non_none_values = list(filter(lambda value: value is not None, values))
+
+    # Reduction is not possible if more than one type is involved (or none)
+    value_types = [type(value) for value in non_none_values]
+    if len(frozenset(value_types)) != 1:
+        return None
+    else:
+        value_type = value_types[0]
+
+    # For floating point types, reduce via arithmetic average
+    if np.issubdtype(value_type, np.floating):
+        return np.mean(non_none_values)
+
+    # For integer types, reduce via the median
+    if np.issubdtype(value_type, np.integer):
+        return int(np.median(non_none_values))
+
+    # For all other types, reduction is only possible if the values are identical
+    if len(frozenset(non_none_values)) == 1:
+        return non_none_values[0]
+    else:
+        return None
+
+
+def metadata_to_str(metadata: dict) -> str:
+    tokens = list()
+    for key in sorted(metadata.keys()):
+        value = metadata[key]
+        if isinstance(value, tuple):
+            value = '(' + ', '.join([f'{val}' for val in value]) + ')'
+        tokens.append(f'{key}: {value}')
+    return ', '.join(tokens)
+
+
 if __name__ == "__main__":
     parser = argparse.ArgumentParser()
     parser.add_argument('input_files', type=str, nargs='+')
     parser.add_argument('out_file', type=str)
     parser.add_argument('axis', type=str)
     parser.add_argument('--preserve_values', default=False, action='store_true')
+    parser.add_argument('--sort_by', type=str, default=None)
     args = parser.parse_args()
 
     concat_channels(
@@ -56,4 +137,5 @@
         args.out_file,
         args.axis,
         args.preserve_values,
+        args.sort_by,
     )
--- a/concat_channels.xml	Sun Dec 07 16:16:03 2025 +0000
+++ b/concat_channels.xml	Fri Dec 12 21:15:56 2025 +0000
@@ -1,4 +1,4 @@
-<tool id="ip_concat_channels" name="Concatenate images or channels" version="0.4" profile="20.05">
+<tool id="ip_concat_channels" name="Concatenate images or channels" version="0.5" profile="20.05">
     <description></description>
     <macros>
         <import>creators.xml</import>
@@ -6,6 +6,7 @@
     </macros>
     <creator>
         <expand macro="creators/bmcv"/>
+        <expand macro="creators/kostrykin"/>
     </creator>
     <edam_operations>
         <edam_operation>operation_3443</edam_operation>
@@ -17,7 +18,7 @@
         <requirement type="package" version="0.25.2">scikit-image</requirement>
         <requirement type="package" version="2.3.5">numpy</requirement>
         <requirement type="package" version="2025.10.16">tifffile</requirement>
-        <requirement type="package" version="0.4.1">giatools</requirement>
+        <requirement type="package" version="0.5.2">giatools</requirement>
     </requirements>
     <command detect_errors="aggressive"><![CDATA[
 
@@ -32,6 +33,10 @@
 
         $mode
 
+        #if $sort_by != ""
+            --sort_by '$sort_by'
+        #end if
+
     ]]></command>
     <inputs>
         <param name="inputs" type="data" multiple="true" format="tiff,png" label="Images to concatenate"/>
@@ -47,6 +52,10 @@
             <option value="" selected="true">Preserve brightness</option>
             <option value="--preserve_values">Preserve range of values</option>
         </param>
+        <param name="sort_by" type="select" label="Sort images before concatenating">
+            <option value="" selected="true">Do not sort (keep the order of the datasets)</option>
+            <option value="z_position">Sort images by their position along the Z-axis</option>
+        </param>
     </inputs>
     <outputs>
         <data format="tiff" name="output"/>
@@ -57,13 +66,20 @@
             <param name="inputs" value="input1_uint8.png,input2_float.tiff"/>
             <param name="axis" value="Y"/>
             <param name="mode" value=""/>
+            <param name="sort_by" value=""/>
             <expand macro="tests/intensity_image_diff" name="output" value="res_preserve_brightness.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (238, 119, 4)"/>
+                <has_line line="Output TIFF axes: YXC"/>
+                <has_line line="Output TIFF resolution: (1.0, 1.0)"/>
+            </assert_stdout>
         </test>
         <!-- Test with "preserve range of values", vertical concatenation -->
         <test>
             <param name="inputs" value="input1_uint8.png,input2_float.tiff"/>
             <param name="axis" value="Y"/>
             <param name="mode" value="--preserve_values"/>
+            <param name="sort_by" value=""/>
             <expand macro="tests/intensity_image_diff" name="output" value="res_preserve_values.tiff" ftype="tiff">
                 <!--
 
@@ -77,11 +93,17 @@
                 -->
                 <has_image_mean_intensity min="0" max="255"/>
             </expand>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (238, 119, 4)"/>
+                <has_line line="Output TIFF axes: YXC"/>
+                <has_line line="Output TIFF resolution: (1.0, 1.0)"/>
+            </assert_stdout>
         </test>
         <!-- Test concatenation of channels (axis *exists* in both images) -->
         <test>
             <param name="inputs" value="input1_uint8.png,input2_float.tiff"/>
             <param name="axis" value="C"/>
+            <param name="sort_by" value=""/>
             <output name="output" ftype="tiff">
                 <assert_contents>
                     <has_image_width width="119"/>
@@ -91,11 +113,17 @@
                     <has_image_frames frames="1"/>
                 </assert_contents>
             </output>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (119, 119, 8)"/>
+                <has_line line="Output TIFF axes: YXC"/>
+                <has_line line="Output TIFF resolution: (1.0, 1.0)"/>
+            </assert_stdout>
         </test>
         <!-- Test concatenation of frames (axis *does not* exist in both images) -->
         <test>
             <param name="inputs" value="input1_uint8.png,input2_float.tiff"/>
             <param name="axis" value="T"/>
+            <param name="sort_by" value=""/>
             <output name="output" ftype="tiff">
                 <assert_contents>
                     <has_image_width width="119"/>
@@ -105,6 +133,85 @@
                     <has_image_frames frames="2"/>
                 </assert_contents>
             </output>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (2, 119, 119, 4)"/>
+                <has_line line="Output TIFF axes: TYXC"/>
+                <has_line line="Output TIFF resolution: (1.0, 1.0)"/>
+            </assert_stdout>
+        </test>
+        <!-- Test concatenation of z-slices with metadata -->
+        <test>
+            <param name="inputs" value="heart_ct_4043.tiff,heart_ct_3983.tiff,heart_ct_3953.tiff,heart_ct_3923.tiff"/>
+            <param name="axis" value="Z"/>
+            <param name="mode" value="--preserve_values"/>
+            <param name="sort_by" value=""/>
+            <output name="output" ftype="tiff">
+                <assert_contents>
+                    <has_image_width width="512"/>
+                    <has_image_height height="512"/>
+                    <has_image_depth depth="4"/>
+                    <has_image_channels channels="1"/>
+                    <has_image_frames frames="1"/>
+                    <has_image_center_of_mass slice="0" center_of_mass="254.83911700, 251.72483820" eps="1e-8"/><!-- 4043 -->
+                    <has_image_center_of_mass slice="1" center_of_mass="254.94356937, 251.88178729" eps="1e-8"/><!-- 3983 -->
+                    <has_image_center_of_mass slice="2" center_of_mass="254.95107235, 252.04425222" eps="1e-8"/><!-- 3953 -->
+                    <has_image_center_of_mass slice="3" center_of_mass="254.91235475, 252.24777978" eps="1e-8"/><!-- 3923 -->
+                </assert_contents>
+            </output>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (4, 512, 512)"/>
+                <has_line line="Output TIFF axes: ZYX"/>
+                <has_line line="Output TIFF resolution: (1.137778101526753, 1.137778101526753), unit: mm, z_position: -219.860001, z_spacing: 2.5"/>
+            </assert_stdout>
+        </test>
+        <!-- Test concatenation of z-slices with metadata + sorting by `z_position` (inputs in wrong order) -->
+        <test>
+            <param name="inputs" value="heart_ct_3953.tiff,heart_ct_3983.tiff,heart_ct_4043.tiff,heart_ct_3923.tiff"/>
+            <param name="axis" value="Z"/>
+            <param name="mode" value="--preserve_values"/>
+            <param name="sort_by" value="z_position"/>
+            <output name="output" ftype="tiff">
+                <assert_contents>
+                    <has_image_width width="512"/>
+                    <has_image_height height="512"/>
+                    <has_image_depth depth="4"/>
+                    <has_image_channels channels="1"/>
+                    <has_image_frames frames="1"/>
+                    <has_image_center_of_mass slice="0" center_of_mass="254.83911700, 251.72483820" eps="1e-8"/><!-- 4043 -->
+                    <has_image_center_of_mass slice="1" center_of_mass="254.94356937, 251.88178729" eps="1e-8"/><!-- 3983 -->
+                    <has_image_center_of_mass slice="2" center_of_mass="254.95107235, 252.04425222" eps="1e-8"/><!-- 3953 -->
+                    <has_image_center_of_mass slice="3" center_of_mass="254.91235475, 252.24777978" eps="1e-8"/><!-- 3923 -->
+                </assert_contents>
+            </output>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (4, 512, 512)"/>
+                <has_line line="Output TIFF axes: ZYX"/>
+                <has_line line="Output TIFF resolution: (1.137778101526753, 1.137778101526753), unit: mm, z_position: -219.860001, z_spacing: 2.5"/>
+            </assert_stdout>
+        </test>
+        <!-- Test concatenation of z-slices with a missing slice in between (`z_position` must increase) -->
+        <test>
+            <param name="inputs" value="heart_ct_4043.tiff,heart_ct_3983.tiff,heart_ct_3923.tiff"/>
+            <param name="axis" value="Z"/>
+            <param name="mode" value="--preserve_values"/>
+            <param name="sort_by" value=""/>
+            <output name="output" ftype="tiff">
+                <assert_contents>
+                    <has_image_width width="512"/>
+                    <has_image_height height="512"/>
+                    <has_image_depth depth="3"/>
+                    <has_image_channels channels="1"/>
+                    <has_image_frames frames="1"/>
+                    <has_image_center_of_mass slice="0" center_of_mass="254.83911700, 251.72483820" eps="1e-8"/><!-- 4043 -->
+                    <has_image_center_of_mass slice="1" center_of_mass="254.94356937, 251.88178729" eps="1e-8"/><!-- 3983 -->
+                    <has_image_center_of_mass slice="2" center_of_mass="254.91235475, 252.24777978" eps="1e-8"/><!-- 3923 -->
+                </assert_contents>
+            </output>
+            <assert_stdout>
+                <has_line line="Output TIFF shape: (3, 512, 512)"/>
+                <has_line line="Output TIFF axes: ZYX"/>
+                <has_line line="Output TIFF resolution: (1.137778101526753, 1.137778101526753), unit: mm, z_position: -220.27666766666667, z_spacing: 3.75"/>
+            </assert_stdout>
         </test>
     </tests>
     <help>
Binary file test-data/heart_ct_3923.tiff has changed
Binary file test-data/heart_ct_3953.tiff has changed
Binary file test-data/heart_ct_3983.tiff has changed
Binary file test-data/heart_ct_4043.tiff has changed