changeset 4:5122286b700e draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/scale_image/ commit cd908933bd7bd8756c213af57ea6343a90effc12
author imgteam
date Sat, 13 Dec 2025 22:11:13 +0000
parents ba2b1a6f1b84
children
files creators.xml scale_image.py scale_image.xml test-data/anisotropic.png test-data/input1_binary_rgb.png test-data/input2_normalized.tiff test-data/input3_not_normalized.tiff test-data/inputs/binary_c0.tiff test-data/inputs/binary_c2.tiff test-data/inputs/binary_c2_z10.tiff test-data/inputs/binary_rgba.png test-data/inputs/rgb.png test-data/normalized.tiff test-data/not_normalized.tiff test-data/outputs/binary_c0_explicit_cubic_aa.tiff test-data/outputs/binary_c2_isotropy_linear.tiff test-data/outputs/binary_c2_z10_isotropy_linear.tiff test-data/outputs/binary_rgba_explicit_cubic_aa.png test-data/outputs/binary_rgba_uniform_down_nn.png test-data/outputs/binary_rgba_uniform_up_nn.png test-data/outputs/rgb_uniform_down_linear_aa.png test-data/outputs/rgb_uniform_up_linear_aa.png test-data/uniform.png test-data/uniform_binary.png
diffstat 24 files changed, 573 insertions(+), 77 deletions(-) [+]
line wrap: on
line diff
--- a/creators.xml	Thu Oct 17 10:47:14 2024 +0000
+++ b/creators.xml	Sat Dec 13 22:11:13 2025 +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>
--- a/scale_image.py	Thu Oct 17 10:47:14 2024 +0000
+++ b/scale_image.py	Sat Dec 13 22:11:13 2025 +0000
@@ -1,51 +1,272 @@
 import argparse
+import json
 import sys
+from typing import (
+    Any,
+    Literal,
+)
 
 import giatools.io
 import numpy as np
 import skimage.io
 import skimage.transform
 import skimage.util
-from PIL import Image
+
+
+def get_uniform_scale(
+    img: giatools.Image,
+    axes: Literal['all', 'spatial'],
+    factor: float,
+) -> tuple[float, ...]:
+    """
+    Determine a tuple of `scale` factors for uniform or spatially uniform scaling.
+
+    Axes, that are not present in the original image data, are ignored.
+    """
+    ignored_axes = [
+        axis for axis_idx, axis in enumerate(img.axes)
+        if axis not in img.original_axes or (
+            factor < 1 and img.data.shape[axis_idx] == 1
+        )
+    ]
+    match axes:
+
+        case 'all':
+            return tuple(
+                [
+                    (factor if axis not in ignored_axes else 1)
+                    for axis in img.axes if axis != 'C'
+                ]
+            )
+
+        case 'spatial':
+            return tuple(
+                [
+                    (factor if axis in 'YXZ' and axis not in ignored_axes else 1)
+                    for axis in img.axes if axis != 'C'
+                ]
+            )
+
+        case _:
+            raise ValueError(f'Unknown axes for uniform scaling: "{axes}"')
+
+
+def get_scale_for_isotropy(
+    img: giatools.Image,
+    sample: Literal['up', 'down'],
+) -> tuple[float, ...]:
+    """
+    Determine a tuple of `scale` factors to establish spatial isotropy.
+
+    The `sample` parameter governs whether to up-sample or down-sample the image data.
+    """
+    scale = [1] * (len(img.axes) - 1)  # omit the channel axis
+    z_axis, y_axis, x_axis = [
+        img.axes.index(axis) for axis in 'ZYX'
+    ]
+
+    # Determine the pixel size of the image
+    if 'resolution' in img.metadata:
+        pixel_size = np.divide(1, img.metadata['resolution'])
+    else:
+        sys.exit('Resolution information missing in image metadata')
+
+    # Define unified transformation of pixel/voxel sizes to scale factors
+    def voxel_size_to_scale(voxel_size: np.ndarray) -> list:
+        match sample:
+            case 'up':
+                return (voxel_size / voxel_size.min()).tolist()
+            case 'down':
+                return (voxel_size / voxel_size.max()).tolist()
+            case _:
+                raise ValueError(f'Unknown value for sample: "{sample}"')
+
+    # Handle the 3-D case
+    if img.data.shape[z_axis] > 1:
+
+        # Determine the voxel depth of the image
+        if (voxel_depth := img.metadata.get('z_spacing', None)) is None:
+            sys.exit('Voxel depth information missing in image metadata')
+
+        # Determine the XYZ scale factors
+        scale[x_axis], scale[y_axis], scale[z_axis] = (
+            voxel_size_to_scale(
+                np.array([*pixel_size, voxel_depth]),
+            )
+        )
+
+    # Handle the 2-D case
+    else:
+
+        # Determine the XY scale factors
+        scale[x_axis], scale[y_axis] = (
+            voxel_size_to_scale(
+                np.array(pixel_size),
+            )
+        )
+
+    return tuple(scale)
+
+
+def get_aa_sigma_by_scale(scale: float) -> float:
+    """
+    Determine the optimal size of the Gaussian filter for anti-aliasing.
+
+    See for details: https://scikit-image.org/docs/0.25.x/api/skimage.transform.html#skimage.transform.rescale
+    """
+    return (1 / scale - 1) / 2 if scale < 1 else 0
 
 
-def scale_image(input_file, output_file, scale, order, antialias):
-    Image.MAX_IMAGE_PIXELS = 50000 * 50000
-    im = giatools.io.imread(input_file)
+def get_new_metadata(
+    old: giatools.Image,
+    scale: float | tuple[float, ...],
+    arr: np.ndarray,
+) -> dict[str, Any]:
+    """
+    Determine the result metadata (copy and adapt).
+    """
+    metadata = dict(old.metadata)
+    scales = (
+        [scale] * (len(old.axes) - 1)  # omit the channel axis
+        if isinstance(scale, float) else scale
+    )
+
+    # Determine the original pixel size
+    old_pixel_size = (
+        np.divide(1, old.metadata['resolution'])
+        if 'resolution' in old.metadata else (1, 1)
+    )
 
-    # Parse `--scale` argument
-    if ',' in scale:
-        scale = [float(s.strip()) for s in scale.split(',')]
-        assert len(scale) <= im.ndim, f'Image has {im.ndim} axes, but scale factors were given for {len(scale)} axes.'
-        scale = scale + [1] * (im.ndim - len(scale))
+    # Determine the new pixel size and update metadata
+    new_pixel_size = np.divide(
+        old_pixel_size,
+        (
+            scales[old.axes.index('X')],
+            scales[old.axes.index('Y')],
+        ),
+    )
+    metadata['resolution'] = tuple(1 / new_pixel_size)
+
+    # Update the metadata for the new voxel depth
+    old_voxel_depth = old.metadata.get('z_spacing', 1)
+    metadata['z_spacing'] = old_voxel_depth / scales[old.axes.index('Z')]
+
+    return metadata
+
+
+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}')
+    if len(metadata_str := ', '.join(tokens)) > 0:
+        return metadata_str
+    else:
+        return 'has no metadata'
+
+
+def write_output(filepath: str, img: giatools.Image):
+    """
+    Validate that the output file format is suitable for the image data, then write it.
+    """
+    print('Output shape:', img.data.shape)
+    print('Output axes:', img.axes)
+    print('Output', metadata_to_str(img.metadata))
 
-    else:
-        scale = float(scale)
+    # Validate that the output file format is suitable for the image data
+    if filepath.lower().endswith('.png'):
+        if not frozenset(img.axes) <= frozenset('YXC'):
+            sys.exit(f'Cannot write PNG file with axes "{img.axes}"')
+
+    # Write image data to the output file
+    img.write(filepath)
+
+
+def scale_image(
+    input_filepath: str,
+    output_filepath: str,
+    mode: Literal['uniform', 'explicit', 'isotropy'],
+    order: int,
+    anti_alias: bool,
+    **cfg,
+):
+    img = giatools.Image.read(input_filepath)
+    print('Input axes:', img.original_axes)
+    print('Input', metadata_to_str(img.metadata))
+
+    # Determine `scale` for scaling
+    match mode:
+
+        case 'uniform':
+            scale = get_uniform_scale(img, cfg['axes'], cfg['factor'])
 
-        # For images with 3 or more axes, the last axis is assumed to correspond to channels
-        if im.ndim >= 3:
-            scale = [scale] * (im.ndim - 1) + [1]
+        case 'explicit':
+            scale = tuple(
+                [cfg.get(f'factor_{axis.lower()}', 1) for axis in img.axes if axis != 'C']
+            )
+
+        case 'isotropy':
+            scale = get_scale_for_isotropy(img, cfg['sample'])
+
+        case _:
+            raise ValueError(f'Unknown mode: "{mode}"')
 
-    # Do the scaling
-    res = skimage.transform.rescale(im, scale, order, anti_aliasing=antialias, preserve_range=True)
+    # Assemble remaining `rescale` parameters
+    rescale_kwargs = dict(
+        scale=scale,
+        order=order,
+        preserve_range=True,
+        channel_axis=img.axes.index('C'),
+    )
+    if (anti_alias := anti_alias and (np.array(scale) < 1).any()):
+        rescale_kwargs['anti_aliasing'] = anti_alias
+        rescale_kwargs['anti_aliasing_sigma'] = tuple(
+            [
+                get_aa_sigma_by_scale(s) for s in scale
+            ] + [0]  # `skimage.transform.rescale` also expects a value for the channel axis
+        )
+    else:
+        rescale_kwargs['anti_aliasing'] = False
+
+    # Re-sample the image data to perform the scaling
+    for key, value in rescale_kwargs.items():
+        print(f'{key}: {value}')
+    arr = skimage.transform.rescale(img.data, **rescale_kwargs)
 
     # Preserve the `dtype` so that both brightness and range of values is preserved
-    if res.dtype != im.dtype:
-        if np.issubdtype(im.dtype, np.integer):
-            res = res.round()
-        res = res.astype(im.dtype)
+    if arr.dtype != img.data.dtype:
+        if np.issubdtype(img.data.dtype, np.integer):
+            arr = arr.round()
+        arr = arr.astype(img.data.dtype)
 
-    # Save result
-    skimage.io.imsave(output_file, res)
+    # Determine the result metadata and save result
+    metadata = get_new_metadata(img, scale, arr)
+    write_output(
+        output_filepath,
+        giatools.Image(
+            data=arr,
+            axes=img.axes,
+            metadata=metadata,
+        ).squeeze()
+    )
 
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser()
-    parser.add_argument('input_file', type=argparse.FileType('r'), default=sys.stdin)
-    parser.add_argument('out_file', type=argparse.FileType('w'), default=sys.stdin)
-    parser.add_argument('--scale', type=str, required=True)
-    parser.add_argument('--order', type=int, required=True)
-    parser.add_argument('--antialias', default=False, action='store_true')
+    parser.add_argument('input', type=str)
+    parser.add_argument('output', type=str)
+    parser.add_argument('params', type=str)
     args = parser.parse_args()
 
-    scale_image(args.input_file.name, args.out_file.name, args.scale, args.order, args.antialias)
+    # Read the config file
+    with open(args.params) as cfgf:
+        cfg = json.load(cfgf)
+
+    # Perform scaling
+    scale_image(
+        args.input,
+        args.output,
+        **cfg,
+    )
--- a/scale_image.xml	Thu Oct 17 10:47:14 2024 +0000
+++ b/scale_image.xml	Sat Dec 13 22:11:13 2025 +0000
@@ -3,96 +3,356 @@
     <macros>
         <import>creators.xml</import>
         <import>tests.xml</import>
-        <token name="@TOOL_VERSION@">0.18.3</token>
-        <token name="@VERSION_SUFFIX@">2</token>
+        <token name="@TOOL_VERSION@">0.25.2</token>
+        <token name="@VERSION_SUFFIX@">0</token>
+        <xml name="factor_input" tokens="name,label">
+            <param name="@NAME@" type="float" min="0.01" max="10" value="1" label="@LABEL@ scaling factor"
+                   help="Values less than 1 will down-sample the image data. Values greater than 1 will up-sample the image data."/>
+        </xml>
+        <xml name="anti_alias_input">
+            <param name="anti_alias" type="boolean" truevalue="true" falsevalue="false" checked="true" label="Enable anti-aliasing"
+                   help="Disable this when processing binary images or label maps."/>
+        </xml>
     </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">scikit-image</xref>
         <xref type="biii">scikit-image</xref>
     </xrefs>
     <requirements>
         <requirement type="package" version="@TOOL_VERSION@">scikit-image</requirement>
-        <requirement type="package" version="10.0.1">pillow</requirement>
-        <requirement type="package" version="1.24.4">numpy</requirement>
-        <requirement type="package" version="2021.7.2">tifffile</requirement>
-        <requirement type="package" version="0.1">giatools</requirement>
+        <requirement type="package" version="2.3.5">numpy</requirement>
+        <requirement type="package" version="0.5.2">giatools</requirement>
+        <requirement type="package" version="2025.10.16">tifffile</requirement>
     </requirements> 
     <command detect_errors="aggressive"><![CDATA[
 
-        python '$__tool_directory__/scale_image.py' '$input'
-
-        ./output.${input.ext}
+        python '$__tool_directory__/scale_image.py'
 
-        --scale '$scale'
-        --order  $order
-        $antialias
+        '$input'
+        ./output.${input.ext}
+        '$params'
 
         && mv ./output.${input.ext} ./output
 
     ]]></command>
+    <configfiles>
+        <configfile name="params"><![CDATA[
+            {
+
+            #if $scale.mode == "uniform"
+                "axes": "$scale.axes",
+                "factor": $scale.factor,
+
+            #elif $scale.mode == "explicit"
+                "factor_x": $scale.factor_x,
+                "factor_y": $scale.factor_y,
+                "factor_z": $scale.factor_z,
+                "factor_t": $scale.factor_t,
+                "factor_q": $scale.factor_q,
+
+            #elif $scale.mode == "isotropy"
+                "sample": "$scale.sample",
+
+            #end if
+                "mode": "$scale.mode",
+                "order": $interpolation.order,
+                "anti_alias": $interpolation.anti_alias
+
+            }
+        ]]></configfile>
+    </configfiles>
     <inputs>
-        <param name="input" type="data" format="png,tiff" label="Image file"/>
-        <param argument="--scale" type="text" value="1" label="Scaling factor" help="Use either a single scaling factor (uniform scaling), or a comma-separated list of scaling factors (anistropic scaling). For a 2-D single-channel or RGB image, the first scaling factor corresponds to the image width and the second corresponds to the image height. For images with 3 or more axes, the last axis is assumed to correspond to the image channels if uniform scaling is used (a single value)."/>
-        <param argument="--order" type="select" label="Interpolation method">
-            <option value="0">Nearest-neighbor</option>
-            <option value="1" selected="true">Bi-linear</option>
-            <option value="2">Bi-cubic</option>
-        </param>
-        <param name="antialias" type="boolean" truevalue="--antialias" falsevalue="" checked="true" label="Enable anti-aliasing" help="This should only be used for down-scaling."/>
+        <param name="input" type="data" format="png,tiff" label="Input image"/>
+        <conditional name="scale">
+            <param name="mode" type="select" label="How to scale?"
+                   help='Using the "Scale to spatially isotropic pixels/voxels" option requires that the real-world resolution is available from the metadata of the input image.'>
+                <option value="uniform" selected="true">Uniform scaling factor</option>
+                <option value="explicit">Explicit scaling factors</option>
+                <option value="isotropy">Scale to spatially isotropic pixels/voxels</option>
+            </param>
+            <when value="uniform">
+                <param name="axes" type="select" label="Axes to be scaled">
+                    <option value="spatial" selected="true">All spatial axes (X, Y, Z)</option>
+                    <option value="all">All axes except channels (X, Y, Z, T, Q)</option>
+                </param>
+                <expand macro="factor_input" name="factor" label="Uniform"/>
+            </when>
+            <when value="explicit">
+                <expand macro="factor_input" name="factor_x" label="Horizontal (X)"/>
+                <expand macro="factor_input" name="factor_y" label="Vertical (Y)"/>
+                <expand macro="factor_input" name="factor_z" label="Depth axis (Z)"/>
+                <expand macro="factor_input" name="factor_t" label="Temporal (T)"/>
+                <expand macro="factor_input" name="factor_q" label="Other axis (Q)"/>
+            </when>
+            <when value="isotropy">
+                <param name="sample" type="select" label="Method">
+                    <option value="up">Up-sample (enlarges the image data)</option>
+                    <option value="down" selected="true">Down-sample (might lose information)</option>
+                </param>
+            </when>
+        </conditional>
+        <conditional name="interpolation">
+            <param name="order" type="select" label="Interpolation method">
+                <option value="0">Nearest-neighbor (good for binary images and label maps)</option>
+                <option value="1" selected="true">Linear (better preserves the intensity values)</option>
+                <option value="2">Cubic (yields visually superior results)</option>
+            </param>
+            <when value="0">
+                <param name="anti_alias" type="hidden" value="false"/>
+            </when>
+            <when value="1">
+                <expand macro="anti_alias_input"/>
+            </when>
+            <when value="2">
+                <expand macro="anti_alias_input"/>
+            </when>
+        </conditional>
     </inputs>
     <outputs>
         <data name="output" from_work_dir="output" format_source="input" metadata_source="input"/>
     </outputs>
     <tests>
-        <!-- Test PNG, without antialias -->
+        <!-- Test PNG, uniform scaling, nearest neighbor interpolation (without anti-alias) -->
         <test>
-            <param name="input" value="input1_binary_rgb.png"/>
-            <param name="scale" value="0.5"/>
-            <param name="antialias" value="false"/>
-            <param name="order" value="0"/>
-            <expand macro="tests/binary_image_diff" name="output" value="uniform_binary.png" ftype="png"/>
+            <param name="input" value="inputs/binary_rgba.png"/>
+            <conditional name="scale">
+                <param name="mode" value="uniform"/>
+                <param name="factor" value="0.7"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="0"/>
+            </conditional>
+            <expand macro="tests/binary_image_diff" name="output" value="outputs/binary_rgba_uniform_down_nn.png" ftype="png"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line line="Input has no metadata"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line line="Output resolution: (0.7, 0.7), z_spacing: 1.0"/>
+                <has_line line="scale: (1, 1, 1, 0.7, 0.7)"/>
+                <has_line line="order: 0"/>
+                <has_line line="anti_aliasing: False"/>
+            </assert_stdout>
         </test>
-        <!-- Test PNG, uniform scaling -->
+        <test>
+            <param name="input" value="inputs/binary_rgba.png"/>
+            <conditional name="scale">
+                <param name="mode" value="uniform"/>
+                <param name="factor" value="1.5"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="0"/>
+            </conditional>
+            <expand macro="tests/binary_image_diff" name="output" value="outputs/binary_rgba_uniform_up_nn.png" ftype="png"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line line="Input has no metadata"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line line="Output resolution: (1.5, 1.5), z_spacing: 1.0"/>
+                <has_line line="scale: (1, 1, 1, 1.5, 1.5)"/>
+                <has_line line="order: 0"/>
+                <has_line line="anti_aliasing: False"/>
+            </assert_stdout>
+        </test>
+        <!-- Test PNG, uniform scaling, linear interpolation with anti-alias -->
         <test>
-            <param name="input" value="input1_binary_rgb.png"/>
-            <param name="scale" value="0.5"/>
-            <expand macro="tests/intensity_image_diff" name="output" value="uniform.png" ftype="png"/>
+            <param name="input" value="inputs/rgb.png"/>
+            <conditional name="scale">
+                <param name="mode" value="uniform"/>
+                <param name="factor" value="0.7"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="1"/>
+                <param name="anti_alias" value="true"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/rgb_uniform_down_linear_aa.png" ftype="png"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line line="Input has no metadata"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line line="Output resolution: (0.7, 0.7), z_spacing: 1.0"/>
+                <has_line line="scale: (1, 1, 1, 0.7, 0.7)"/>
+                <has_line line="order: 1"/>
+                <has_line line="anti_aliasing: True"/>
+                <has_line_matching expression="anti_aliasing_sigma: \(0, 0, 0, 0.214[0-9]+, 0.214[0-9]+, 0\)"/>
+            </assert_stdout>
         </test>
-        <!-- Test PNG, anistropic scaling -->
+        <test>
+            <param name="input" value="inputs/rgb.png"/>
+            <conditional name="scale">
+                <param name="mode" value="uniform"/>
+                <param name="factor" value="1.5"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="1"/>
+                <param name="anti_alias" value="true"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/rgb_uniform_up_linear_aa.png" ftype="png"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line line="Input has no metadata"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line line="Output resolution: (1.5, 1.5), z_spacing: 1.0"/>
+                <has_line line="scale: (1, 1, 1, 1.5, 1.5)"/>
+                <has_line line="order: 1"/>
+                <has_line line="anti_aliasing: False"/>
+            </assert_stdout>
+        </test>
+        <!-- Test PNG, explicit scaling, cubic interpolation with anti-alias -->
         <test>
-            <param name="input" value="input1_binary_rgb.png"/>
-            <param name="scale" value="0.5, 0.8"/>
-            <expand macro="tests/intensity_image_diff" name="output" value="anisotropic.png" ftype="png"/>
+            <param name="input" value="inputs/binary_rgba.png"/>
+            <conditional name="scale">
+                <param name="mode" value="explicit"/>
+                <param name="factor_x" value="1.5"/>
+                <param name="factor_y" value="0.7"/>
+                <param name="factor_z" value="1.0"/>
+                <param name="factor_t" value="1.0"/>
+                <param name="factor_q" value="1.0"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="2"/>
+                <param name="anti_alias" value="true"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/binary_rgba_explicit_cubic_aa.png" ftype="png"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line line="Input has no metadata"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line line="Output resolution: (1.5, 0.7), z_spacing: 1.0"/>
+                <has_line line="scale: (1.0, 1.0, 1.0, 0.7, 1.5)"/>
+                <has_line line="order: 2"/>
+                <has_line line="anti_aliasing: True"/>
+                <has_line_matching expression="anti_aliasing_sigma: \(0, 0, 0, 0.214[0-9]+, 0, 0\)"/>
+            </assert_stdout>
         </test>
         <test>
-            <param name="input" value="input1_binary_rgb.png"/>
-            <param name="scale" value="0.5, 0.8, 1"/>
-            <expand macro="tests/intensity_image_diff" name="output" value="anisotropic.png" ftype="png"/>
+            <param name="input" value="inputs/binary_rgba.png"/>
+            <conditional name="scale">
+                <param name="mode" value="explicit"/>
+                <param name="factor_x" value="1.5"/>
+                <param name="factor_y" value="0.7"/>
+                <param name="factor_z" value="1.0"/>
+                <param name="factor_t" value="0.1"/>
+                <param name="factor_q" value="1.0"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="2"/>
+                <param name="anti_alias" value="true"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/binary_rgba_explicit_cubic_aa.png" ftype="png"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line line="Input has no metadata"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line line="Output resolution: (1.5, 0.7), z_spacing: 1.0"/>
+                <has_line line="scale: (1.0, 0.1, 1.0, 0.7, 1.5)"/>
+                <has_line line="order: 2"/>
+                <has_line line="anti_aliasing: True"/>
+                <has_line_matching expression="anti_aliasing_sigma: \(0, 4.5, 0, 0.214[0-9]+, 0, 0\)"/>
+            </assert_stdout>
         </test>
-        <!-- Test TIFF, normalized -->
+        <test expect_failure="true">
+            <param name="input" value="inputs/binary_rgba.png"/>
+            <conditional name="scale">
+                <param name="mode" value="explicit"/>
+                <param name="factor_x" value="1.5"/>
+                <param name="factor_y" value="0.7"/>
+                <param name="factor_z" value="2.0"/>
+                <param name="factor_t" value="0.1"/>
+                <param name="factor_q" value="1.0"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="2"/>
+                <param name="anti_alias" value="true"/>
+            </conditional>
+            <assert_stderr>
+                <has_line line='Cannot write PNG file with axes "ZYXC"'/>
+            </assert_stderr>
+        </test>
+        <!-- Test TIFF, explicit scaling, cubic interpolation with anti-alias -->
         <test>
-            <param name="input" value="input2_normalized.tiff"/>
-            <param name="scale" value="0.5"/>
-            <expand macro="tests/intensity_image_diff" name="output" value="normalized.tiff" ftype="tiff"/>
+            <param name="input" value="inputs/binary_c0.tiff"/>
+            <conditional name="scale">
+                <param name="mode" value="explicit"/>
+                <param name="factor_x" value="1.5"/>
+                <param name="factor_y" value="0.7"/>
+                <param name="factor_z" value="2.0"/>
+                <param name="factor_t" value="0.1"/>
+                <param name="factor_q" value="1.0"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="2"/>
+                <param name="anti_alias" value="true"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/binary_c0_explicit_cubic_aa.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="Input axes: YX"/>
+                <has_line line="Input resolution: (1.0, 1.0)"/>
+                <has_line line="Output axes: ZYX"/>
+                <has_line line="Output resolution: (1.5, 0.7), z_spacing: 0.5"/>
+                <has_line line="scale: (1.0, 0.1, 2.0, 0.7, 1.5)"/>
+                <has_line line="order: 2"/>
+                <has_line line="anti_aliasing: True"/>
+                <has_line_matching expression="anti_aliasing_sigma: \(0, 4.5, 0, 0.214[0-9]+, 0, 0\)"/>
+            </assert_stdout>
         </test>
-        <!-- Test TIFF, not normalized -->
+        <!-- Test TIFF, isotropy scaling, linear interpolation (without anti-alias) -->
         <test>
-            <param name="input" value="input3_not_normalized.tiff"/>
-            <param name="scale" value="0.5"/>
-            <expand macro="tests/intensity_image_diff" name="output" value="not_normalized.tiff" ftype="tiff"/>
+            <param name="input" value="inputs/binary_c2_z10.tiff"/>
+            <conditional name="scale">
+                <param name="mode" value="isotropy"/>
+                <param name="sample" value="up"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="1"/>
+                <param name="anti_alias" value="false"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/binary_c2_z10_isotropy_linear.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="Input axes: ZYXC"/>
+                <has_line_matching expression="Input resolution: \(0.1[0-9]*, 0.05[0-9]*\), unit: um, z_spacing: 40.0"/>
+                <has_line line="Output axes: ZYXC"/>
+                <has_line_matching expression="Output resolution: \(0.1[0-9]*, 0.1[0-9]*\), unit: um, z_spacing: (?:9.999[0-9]+|10.0[0-9]*)"/>
+                <has_line_matching expression="scale: \(1, 1, 4.0[0-9]*, 2.0, 1.0\)"/>
+                <has_line line="order: 1"/>
+                <has_line line="anti_aliasing: False"/>
+            </assert_stdout>
+        </test>
+        <test>
+            <param name="input" value="inputs/binary_c2.tiff"/>
+            <conditional name="scale">
+                <param name="mode" value="isotropy"/>
+                <param name="sample" value="up"/>
+            </conditional>
+            <conditional name="interpolation">
+                <param name="order" value="1"/>
+                <param name="anti_alias" value="false"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="outputs/binary_c2_isotropy_linear.tiff" ftype="tiff"/>
+            <assert_stdout>
+                <has_line line="Input axes: YXC"/>
+                <has_line_matching expression="Input resolution: \(0.1[0-9]*, 0.05[0-9]*\), unit: um, z_spacing: 40.0"/>
+                <has_line line="Output axes: YXC"/>
+                <has_line_matching expression="Output resolution: \(0.1[0-9]*, 0.1[0-9]*\), unit: um, z_spacing: 40.0"/>
+                <has_line line="scale: (1, 1, 1, 2.0, 1.0)"/>
+                <has_line line="order: 1"/>
+                <has_line line="anti_aliasing: False"/>
+            </assert_stdout>
         </test>
     </tests>
     <help>
 
-        **Scales an image using one or more scaling factors.**
+        **Scales an image by re-sampling the image data.**
 
-        The image is rescaled uniformly along all axes, or anistropically if multiple scale factors are given.
+        The image is rescaled uniformly along all axes, or anisotropically if multiple scale factors are given.
+        In addition, the metadata of an image can be used to automatically rescale the image to obtain isotropic pixels or voxels.
 
         This operation preserves both the brightness of the image, and the range of values.
 
Binary file test-data/anisotropic.png has changed
Binary file test-data/input1_binary_rgb.png has changed
Binary file test-data/input2_normalized.tiff has changed
Binary file test-data/input3_not_normalized.tiff has changed
Binary file test-data/inputs/binary_c0.tiff has changed
Binary file test-data/inputs/binary_c2.tiff has changed
Binary file test-data/inputs/binary_c2_z10.tiff has changed
Binary file test-data/inputs/binary_rgba.png has changed
Binary file test-data/inputs/rgb.png has changed
Binary file test-data/normalized.tiff has changed
Binary file test-data/not_normalized.tiff has changed
Binary file test-data/outputs/binary_c0_explicit_cubic_aa.tiff has changed
Binary file test-data/outputs/binary_c2_isotropy_linear.tiff has changed
Binary file test-data/outputs/binary_c2_z10_isotropy_linear.tiff has changed
Binary file test-data/outputs/binary_rgba_explicit_cubic_aa.png has changed
Binary file test-data/outputs/binary_rgba_uniform_down_nn.png has changed
Binary file test-data/outputs/binary_rgba_uniform_up_nn.png has changed
Binary file test-data/outputs/rgb_uniform_down_linear_aa.png has changed
Binary file test-data/outputs/rgb_uniform_up_linear_aa.png has changed
Binary file test-data/uniform.png has changed
Binary file test-data/uniform_binary.png has changed