changeset 0:e0beec1d4bf1 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/image_math commit cddd14dcfe71bb7a7e77aaf09bd2c5535e1dd643
author imgteam
date Fri, 06 Mar 2026 17:31:48 +0000
parents
children
files creators.xml image_math.py image_math.xml test-data/half_of_input1_plus_one.tiff test-data/input1.tiff test-data/input1_abs.tiff test-data/input1_times_2.tiff test-data/input1_times_2_uint8.tiff test-data/minus_input1.tiff test-data/ones.tiff tests.xml
diffstat 11 files changed, 456 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/creators.xml	Fri Mar 06 17:31:48 2026 +0000
@@ -0,0 +1,43 @@
+<macros>
+
+    <xml name="creators/bmcv">
+        <organization name="Biomedical Computer Vision Group, Heidelberg Universtiy" alternateName="BMCV" url="http://www.bioquant.uni-heidelberg.de/research/groups/biomedical_computer_vision.html" />
+        <yield />
+    </xml>
+
+    <xml name="creators/kostrykin">
+        <person givenName="Leonid" familyName="Kostrykin"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/rmassei">
+        <person givenName="Riccardo" familyName="Massei"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/alliecreason">
+        <person givenName="Allison" familyName="Creason"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/bugraoezdemir">
+        <person givenName="Bugra" familyName="Oezdemir"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/thawn">
+        <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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image_math.py	Fri Mar 06 17:31:48 2026 +0000
@@ -0,0 +1,103 @@
+import argparse
+import ast
+import operator
+
+import giatools
+import numpy as np
+
+
+supported_operators = {
+    ast.Add: operator.add,
+    ast.Sub: operator.sub,
+    ast.Mult: operator.mul,
+    ast.Div: operator.truediv,
+    ast.FloorDiv: operator.floordiv,
+    ast.Pow: operator.pow,
+    ast.USub: operator.neg,
+}
+
+
+supported_functions = {
+    'sqrt': np.sqrt,
+    'abs': abs,
+}
+
+
+def eval_ast_node(node, inputs):
+    """
+    Evaluates a node of the syntax tree.
+    """
+
+    # Numeric constants evaluate to numeric values.
+    if isinstance(node, ast.Constant):
+        assert type(node.value) in (int, float)
+        return node.value
+
+    # Variables are looked up from the inputs and resolved.
+    if isinstance(node, ast.Name):
+        assert node.id in inputs.keys()
+        return inputs[node.id]
+
+    # Binary operators are evaluated based on the `supported_operators` dictionary.
+    if isinstance(node, ast.BinOp):
+        assert type(node.op) in supported_operators.keys(), node.op
+        op = supported_operators[type(node.op)]
+        return op(eval_ast_node(node.left, inputs), eval_ast_node(node.right, inputs))
+
+    # Unary operators are evaluated based on the `supported_operators` dictionary.
+    if isinstance(node, ast.UnaryOp):
+        assert type(node.op) in supported_operators.keys(), node.op
+        op = supported_operators[type(node.op)]
+        return op(eval_ast_node(node.operand, inputs))
+
+    # Function calls are evaluated based on the `supported_functions` dictionary.
+    if isinstance(node, ast.Call):
+        assert len(node.args) == 1 and len(node.keywords) == 0
+        assert node.func.id in supported_functions.keys(), node.func.id
+        func = supported_functions[node.func.id]
+        return func(eval_ast_node(node.args[0], inputs))
+
+    # The node is unsupported and could not be evaluated.
+    raise TypeError(f'Unsupported node type: "{node}"')
+
+
+def eval_expression(expr, inputs):
+    return eval_ast_node(ast.parse(expr, mode='eval').body, inputs)
+
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('--expression', type=str, required=True)
+    parser.add_argument('--dtype', type=str, default=None)
+    parser.add_argument('--output', type=str, required=True)
+    parser.add_argument('--input', default=list(), action='append', required=True)
+    args = parser.parse_args()
+
+    inputs = dict()
+    im_shape = None
+    for input in args.input:
+        name, filepath = input.split(':')
+        im = giatools.Image.read(filepath)
+        assert name not in inputs, 'Input name "{name}" is ambiguous.'
+        inputs[name] = im.data
+        if im_shape is None:
+            im_shape = im.shape
+        else:
+            assert im.shape == im_shape, 'Input images differ in size and/or number of channels.'
+
+    result = eval_expression(args.expression, inputs)
+
+    # Perform explicit `dtype` conversion
+    if args.dtype:
+        if args.dtype.startswith('uint'):
+            result = result.clip(0, np.inf)
+        result = result.astype(args.dtype)
+
+    # Write result image (preserve metadata from last input image)
+    im.data = result
+    im.normalize_axes_like(
+        im.original_axes,
+    ).write(
+        args.output,
+    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image_math.xml	Fri Mar 06 17:31:48 2026 +0000
@@ -0,0 +1,215 @@
+<tool id="image_math" name="Process images using arithmetic expressions" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="23.0">
+    <description>with NumPy</description>
+    <macros>
+        <import>creators.xml</import>
+        <import>tests.xml</import>
+        <token name="@TOOL_VERSION@">2.3.5</token>
+        <token name="@VERSION_SUFFIX@">0</token>
+    </macros>
+    <creator>
+        <expand macro="creators/bmcv"/>
+        <expand macro="creators/kostrykin"/>
+    </creator>
+    <edam_operations>
+        <edam_operation>operation_3443</edam_operation>
+    </edam_operations>
+    <requirements>
+        <requirement type="package" version="@TOOL_VERSION@">numpy</requirement>
+        <requirement type="package" version="0.7.3">giatools</requirement>
+    </requirements>
+    <required_files>
+        <include type="glob" path="*.py"/>
+    </required_files>
+    <command><![CDATA[
+    
+    ## Inputs
+    
+    python '$__tool_directory__/image_math.py'
+    --expression='$expression'
+    #if '$dtype' != '':
+        --dtype='$dtype'
+    #end if
+    #for $item in $inputs:
+        --input='$item.name:$item.image'
+    #end for
+    
+    ## Outputs
+    
+    --output='./result.tiff'
+    
+    ]]>
+    </command>
+    <inputs>
+        <param argument="--expression" type="text" label="Expression" optional="false">
+            <validator type="regex">^[a-zA-Z0-9-_\*\+ \(\)/\.]+$</validator>
+        </param>
+        <param name="dtype" type="select" label="Data type of the produced TIFF image">
+            <option value="" selected="true">Determine automatically</option>
+            <option value="float16">16-bit floating point</option>
+            <option value="float32">32-bit floating point</option>
+            <option value="float64">64-bit floating point</option>
+            <option value="int8">8-bit signed integer</option>
+            <option value="int16">16-bit signed integer</option>
+            <option value="int32">32-bit signed integer</option>
+            <option value="uint8">8-bit unsigned integer</option>
+            <option value="uint16">16-bit unsigned integer</option>
+            <option value="uint32">32-bit unsigned integer</option>
+        </param>
+        <repeat name="inputs" title="Input images" min="1">
+            <param name="image" type="data" format="png,tiff" label="Image" />
+            <param name="name" type="text" label="Variable for representation of the image within the expression" optional="false">
+                <validator type="regex">^[a-zA-Z_][a-zA-Z0-9_]*$</validator>
+            </param>
+        </repeat>
+    </inputs>
+    <outputs>
+       <data format="tiff" name="result" from_work_dir="result.tiff" />
+    </outputs>
+    <tests>
+        <!-- Multiplication with a scalar -->
+        <test>
+            <!--
+                GIA204: :relax_indent:
+                python '$__tool_directory__/image_math.py'
+                    ––expression='input1 * 2'
+                    ––dtype=''
+                    ––input='input1:input1.tiff'
+                    ––output='./result.tiff'
+            -->
+            <param name="expression" value="input1 * 2" />
+            <repeat name="inputs">
+                <param name="image" value="input1.tiff" />
+                <param name="name" value="input1" />
+            </repeat>
+            <expand macro="tests/intensity_image_diff" name="result" value="input1_times_2.tiff" ftype="tiff"/>
+        </test>
+        <!-- Unary negation operator -->
+        <test>
+            <!--
+                GIA204: :relax_indent:
+                python '$__tool_directory__/image_math.py'
+                    ––expression='-input1'
+                    ––dtype=''
+                    ––input='input1:input1.tiff'
+                    ––output='./result.tiff'
+            -->
+            <param name="expression" value="-input1" />
+            <repeat name="inputs">
+                <param name="image" value="input1.tiff" />
+                <param name="name" value="input1" />
+            </repeat>
+            <expand macro="tests/intensity_image_diff" name="result" value="minus_input1.tiff" ftype="tiff"/>
+        </test>
+        <!-- Binary addition, neutral element, addition with scalar -->
+        <test>
+            <!--
+                GIA204: :relax_indent:
+                python '$__tool_directory__/image_math.py'
+                    ––expression='input1 + input2 + 1'
+                    ––dtype=''
+                    ––input='input1:input1.tiff'
+                    ––input='input2:minus_input1.tiff'
+                    ––output='./result.tiff'
+            -->
+            <param name="expression" value="input1 + input2 + 1" />
+            <repeat name="inputs">
+                <param name="image" value="input1.tiff" />
+                <param name="name" value="input1" />
+            </repeat>
+            <repeat name="inputs">
+                <param name="image" value="minus_input1.tiff" />
+                <param name="name" value="input2" />
+            </repeat>
+            <expand macro="tests/intensity_image_diff" name="result" value="ones.tiff" ftype="tiff"/>
+        </test>
+        <!-- Parentheses -->
+        <test>
+            <!--
+                GIA204: :relax_indent:
+                python '$__tool_directory__/image_math.py'
+                    ––expression='(input1 + input2) / 2'
+                    ––dtype=''
+                    ––input='input1:input1.tiff'
+                    ––input='input2:ones.tiff'
+                    ––output='./result.tiff'
+            -->
+            <param name="expression" value="(input1 + input2) / 2" />
+            <repeat name="inputs">
+                <param name="image" value="input1.tiff" />
+                <param name="name" value="input1" />
+            </repeat>
+            <repeat name="inputs">
+                <param name="image" value="ones.tiff" />
+                <param name="name" value="input2" />
+            </repeat>
+            <expand macro="tests/intensity_image_diff" name="result" value="half_of_input1_plus_one.tiff" ftype="tiff"/>
+        </test>
+        <!-- Abs -->
+        <test>
+            <!--
+                GIA204: :relax_indent:
+                python '$__tool_directory__/image_math.py'
+                    ––expression='abs(input)'
+                    ––dtype=''
+                    ––input='input:input1.tiff'
+                    ––output='./result.tiff'
+            -->
+            <param name="expression" value="abs(input)" />
+            <repeat name="inputs">
+                <param name="image" value="input1.tiff" />
+                <param name="name" value="input" />
+            </repeat>
+            <expand macro="tests/intensity_image_diff" name="result" value="input1_abs.tiff" ftype="tiff"/>
+        </test>
+        <!-- Decimal, conversion to explicit `dtype` -->
+        <test>
+            <!--
+                GIA204: :relax_indent:
+                python '$__tool_directory__/image_math.py'
+                    ––expression='input1 / 0.5'
+                    ––dtype='uint8'
+                    ––input='input1:input1.tiff'
+                    ––output='./result.tiff'
+            -->
+            <param name="expression" value="input1 / 0.5" />
+            <param name="dtype" value="uint8" />
+            <repeat name="inputs">
+                <param name="image" value="input1.tiff" />
+                <param name="name" value="input1" />
+            </repeat>
+            <expand macro="tests/intensity_image_diff" name="result" value="input1_times_2_uint8.tiff" ftype="tiff"/>
+        </test>
+    </tests>
+    <help>
+
+        **Processes images according to pixel-wise arithmetic expressions.**
+
+        The supported pixel-wise expressions are:
+
+        - Addition, subtraction, multiplication, and division (``+``, ``-``, ``*``, ``/``)
+        - Integer division (e.g., ``input // 2``)
+        - Power (e.g., ``input ** 2``)
+        - Negation (e.g., ``-input``)
+        - Absolute values (e.g., ``abs(input)``)
+        - Square root (e.g., ``sqrt(input)``)
+        - Combinations of the above (also using parentheses)
+
+        Examples:
+
+        - **Negate an image.**
+          Expression: ``-image``
+          where ``image`` is an arbitrary input image.
+
+        - **Mean of two images.**
+          Expression: ``(image1 + image2) / 2``
+          where ``image1`` and `image2` are two arbitrary input images.
+
+        - **Perform division avoiding division-by-zero.**
+          Expression: ``image1 / (abs(image2) + 1e-8)``
+          where ``image1`` and `image2` are two arbitrary input images.
+
+    </help>
+    <citations>
+        <citation type="doi">10.1038/s41586-020-2649-2</citation>
+    </citations>
+</tool>
Binary file test-data/half_of_input1_plus_one.tiff has changed
Binary file test-data/input1.tiff has changed
Binary file test-data/input1_abs.tiff has changed
Binary file test-data/input1_times_2.tiff has changed
Binary file test-data/input1_times_2_uint8.tiff has changed
Binary file test-data/minus_input1.tiff has changed
Binary file test-data/ones.tiff has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests.xml	Fri Mar 06 17:31:48 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>