Mercurial > repos > imgteam > crop_image
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>
--- /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
--- /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
