Mercurial > repos > imgteam > concat_channels
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>
