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