changeset 5:1b7e67168732 draft

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/2d_simple_filter/ commit a6fd77be465068f709a71d377900da99becf94d8
author imgteam
date Fri, 12 Dec 2025 21:18:21 +0000
parents d51310ab328a
children 6fecb8056a80
files creators.xml filter.py filter.xml filter_image.py test-data/input1_gaussian.tif test-data/input1_gaussian.tiff test-data/input1_median.tif test-data/input1_median.tiff test-data/input1_prewitt_h.tif test-data/input1_prewitt_h.tiff test-data/input1_prewitt_v.tif test-data/input1_prewitt_v.tiff test-data/input1_sobel_h.tif test-data/input1_sobel_h.tiff test-data/input1_sobel_v.tif test-data/input1_sobel_v.tiff test-data/input1_uint8.tif test-data/input1_uint8.tiff test-data/input2_float.tif test-data/input2_float.tiff test-data/input2_gaussian.tif test-data/input2_gaussian.tiff test-data/input2_uniform.tiff test-data/retina_gaussian_order0.tiff test-data/retina_gaussian_order2_axis0.tiff test-data/scikit-image/LICENSE.txt test-data/scikit-image/retina.png
diffstat 27 files changed, 326 insertions(+), 84 deletions(-) [+]
line wrap: on
line diff
--- a/creators.xml	Wed Apr 24 08:12:03 2024 +0000
+++ b/creators.xml	Fri Dec 12 21:18:21 2025 +0000
@@ -5,6 +5,16 @@
         <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/>
@@ -19,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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/filter.py	Fri Dec 12 21:18:21 2025 +0000
@@ -0,0 +1,110 @@
+import argparse
+import json
+from typing import (
+    Any,
+    Callable,
+)
+
+import giatools
+import numpy as np
+import scipy.ndimage as ndi
+from skimage.morphology import disk
+
+
+def image_astype(img: giatools.Image, dtype: np.dtype) -> giatools.Image:
+    return giatools.Image(
+        data=img.data.astype(dtype),
+        axes=img.axes,
+        original_axes=img.original_axes,
+        metadata=img.metadata,
+    )
+
+
+filters = {
+    'gaussian': lambda img, sigma, order=0, axis=None: (
+        apply_2d_filter(
+            ndi.gaussian_filter,
+            img if order == 0 else image_astype(img, float),
+            sigma=sigma,
+            order=order,
+            axes=axis,
+        )
+    ),
+    'uniform': lambda img, size: (
+        apply_2d_filter(ndi.uniform_filter, img, size=size)
+    ),
+    'median': lambda img, radius: (
+        apply_2d_filter(ndi.median_filter, img, footprint=disk(radius))
+    ),
+    'prewitt': lambda img, axis: (
+        apply_2d_filter(ndi.prewitt, img, axis=axis)
+    ),
+    'sobel': lambda img, axis: (
+        apply_2d_filter(ndi.sobel, img, axis=axis)
+    ),
+}
+
+
+def apply_2d_filter(
+    filter_impl: Callable[[np.ndarray, Any, ...], np.ndarray],
+    img: giatools.Image,
+    **kwargs: Any,
+) -> giatools.Image:
+    """
+    Apply the 2-D filter to the 2-D/3-D, potentially multi-frame and multi-channel image.
+    """
+    result_data = None
+    for qtzc in np.ndindex(
+        img.data.shape[ 0],  # Q axis
+        img.data.shape[ 1],  # T axis
+        img.data.shape[ 2],  # Z axis
+        img.data.shape[-1],  # C axis
+    ):
+        sl = np.s_[*qtzc[:3], ..., qtzc[3]]  # noqa: E999
+        arr = img.data[sl]
+        assert arr.ndim == 2  # sanity check, should always be True
+
+        # Perform 2-D filtering
+        res = filter_impl(arr, **kwargs)
+        if result_data is None:
+            result_data = np.empty(img.data.shape, res.dtype)
+        result_data[sl] = res
+
+    # Return results
+    return giatools.Image(result_data, img.axes)
+
+
+def apply_filter(
+    input_filepath: str,
+    output_filepath: str,
+    filter_type: str,
+    **kwargs: Any,
+):
+    # Read the input image
+    img = giatools.Image.read(input_filepath)
+
+    # Perform filtering
+    filter_impl = filters[filter_type]
+    res = filter_impl(img, **kwargs).normalize_axes_like(img.original_axes)
+
+    # Adopt metadata and write the result
+    res.metadata = img.metadata
+    res.write(output_filepath, backend='tifffile')
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('input', type=str, help='Input image filepath')
+    parser.add_argument('output', type=str, help='Output image filepath (TIFF)')
+    parser.add_argument('params', type=str)
+    args = parser.parse_args()
+
+    # Read the config file
+    with open(args.params) as cfgf:
+        cfg = json.load(cfgf)
+
+    apply_filter(
+        args.input,
+        args.output,
+        **cfg,
+    )
--- a/filter.xml	Wed Apr 24 08:12:03 2024 +0000
+++ b/filter.xml	Fri Dec 12 21:18:21 2025 +0000
@@ -1,81 +1,136 @@
-<tool id="ip_filter_standard" name="Filter 2-D image" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.05">
+<tool id="ip_filter_standard" name="Apply 2-D image filter" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.05">
     <description>with scipy</description>
     <macros>
         <import>creators.xml</import>
         <import>tests.xml</import>
-        <token name="@TOOL_VERSION@">1.12.0</token>
-        <token name="@VERSION_SUFFIX@">1</token>
+        <token name="@TOOL_VERSION@">1.16.3</token>
+        <token name="@VERSION_SUFFIX@">0</token>
+        <xml name="select_axis">
+            <param name="axis" type="select" label="Direction">
+                <option value="1" selected="true">Horizontal</option>
+                <option value="0">Vertical</option>
+            </param>
+        </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="biii">scipy</xref>
     </xrefs>
     <requirements>
         <requirement type="package" version="@TOOL_VERSION@">scipy</requirement>
-        <requirement type="package" version="1.26.4">numpy</requirement>
-        <requirement type="package" version="0.22.0">scikit-image</requirement>
-        <requirement type="package" version="2024.2.12">tifffile</requirement>
-        <requirement type="package" version="0.1">giatools</requirement>
+        <requirement type="package" version="2.3.5">numpy</requirement>
+        <requirement type="package" version="0.25.2">scikit-image</requirement>
+        <requirement type="package" version="2025.10.16">tifffile</requirement>
+        <requirement type="package" version="0.5.2">giatools</requirement>
     </requirements>
     <command detect_errors="aggressive"><![CDATA[
 
-        python '$__tool_directory__/filter_image.py'
+        python '$__tool_directory__/filter.py'
 
         '$input'
         '$output'
-
-        $filter_type
-        $size
+        '$params'
 
     ]]></command>
+    <configfiles>
+        <configfile name="params"><![CDATA[
+            {
+
+            ## =====================================================================
+            #if $filter.filter_type == "gaussian"
+                "sigma": $filter.sigma,
+                "order": $filter.derivative.order,
+
+                #if $filter.derivative.order != "0"
+                    "axis": $filter.derivative.axis,
+                #end if
+
+            ## =====================================================================
+            #elif $filter.filter_type == "uniform"
+                "size": $filter.size,
+
+            ## =====================================================================
+            #elif $filter.filter_type == "median"
+                "radius": $filter.radius,
+
+            ## =====================================================================
+            #elif $filter.filter_type == "prewitt" or $filter.filter_type == "sobel"
+                "axis": $filter.axis,
+
+            ## =====================================================================
+            #end if
+
+                "filter_type": "$filter.filter_type"
+
+            }
+        ]]></configfile>
+    </configfiles>
     <inputs>
-        <param name="input" type="data" format="tiff,png" label="Input image" />
+        <param name="input" type="data" format="tiff,png" label="Input image"/>
         <conditional name="filter">
             <param name="filter_type" type="select" label="Filter type">
                 <option value="gaussian" selected="True">Gaussian</option>
+                <option value="uniform">Box filter (uniform filter)</option>
                 <option value="median">Median</option>
-                <option value="prewitt_h">Prewitt horizontal</option>
-                <option value="prewitt_v">Prewitt vertical</option>
-                <option value="sobel_h">Sobel horizontal</option>
-                <option value="sobel_v">Sobel vertical</option>
+                <option value="prewitt">Prewitt</option>
+                <option value="sobel">Sobel</option>
             </param>
             <when value="gaussian">
-                <param name="size" type="float" value="3" min="0.1" label="Sigma" help="The half width of the Gaussian bell (in pixels)." />
+                <param name="sigma" type="float" value="3" min="0.1" label="Sigma"
+                       help="The half width of the Gaussian bell (in pixels)."/>
+                <conditional name="derivative">
+                    <param name="order" type="select" label="Use a derivative?">
+                        <option value="0">No derivative (mean filter)</option>
+                        <option value="1">1st-order derivative</option>
+                        <option value="2">2nd-order derivative</option>
+                    </param>
+                    <when value="0">
+                    </when>
+                    <when value="1">
+                        <expand macro="select_axis"/>
+                    </when>
+                    <when value="2">
+                        <expand macro="select_axis"/>
+                    </when>
+                </conditional>
+            </when>
+            <when value="uniform">
+                <param name="size" type="integer" min="2" value="3" label="Size"
+                       help="Edge length of the neighborhood (square, in pixels)."/>
             </when>
             <when value="median">
-                <param name="size" type="integer" value="3" label="Radius" help="Radius of the neighborhood (in pixels)." />
-            </when>
-            <when value="prewitt_h">
-                <param name="size" type="hidden" value="0" />
+                <param name="radius" type="integer" min="2" value="3" label="Radius"
+                       help="Radius of the neighborhood (circle, in pixels)." />
             </when>
-            <when value="prewitt_v">
-                <param name="size" type="hidden" value="0" />
+            <when value="prewitt">
+                <expand macro="select_axis"/>
             </when>
-            <when value="sobel_h">
-                <param name="size" type="hidden" value="0" />
-            </when>
-            <when value="sobel_v">
-                <param name="size" type="hidden" value="0" />
+            <when value="sobel">
+                <expand macro="select_axis"/>
             </when>
         </conditional>
     </inputs>
     <outputs>
        <data format="tiff" name="output" />
-    </outputs> 
+    </outputs>
     <tests>
         <!-- Tests with uint8 TIFF input image -->
         <test>
-            <param name="input" value="input1_uint8.tif" />
-            <param name="filter_type" value="gaussian" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input1_gaussian.tif" ftype="tiff">
+            <param name="input" value="input1_uint8.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="gaussian"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input1_gaussian.tiff" ftype="tiff">
                 <!--
 
-                The input file `input1_uint8.tif` has values ranging between 23 and 254, with a mean value of 63.67.
+                The input file `input1_uint8.tiff` has values ranging between 23 and 254, with a mean value of 63.67.
 
                 Below, we use an assertion in addition to the `image_diff` comparison, to ensure that the range of
                 values is preserved. The motiviation behind this is that the expectation images are usually checked
@@ -88,48 +143,106 @@
             </expand>
         </test>
         <test>
-            <param name="input" value="input1_uint8.tif" />
-            <param name="filter_type" value="median" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input1_median.tif" ftype="tiff">
+            <param name="input" value="input1_uint8.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="median"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input1_median.tiff" ftype="tiff">
                 <!-- See note for Gaussian filter above. -->
                 <has_image_mean_intensity mean_intensity="63.67" eps="10"/>
             </expand>
         </test>
         <test>
-            <param name="input" value="input1_uint8.tif" />
-            <param name="filter_type" value="prewitt_h" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input1_prewitt_h.tif" ftype="tiff"/>
+            <param name="input" value="input1_uint8.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="prewitt"/>
+                <param name="axis" value="1"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input1_prewitt_h.tiff" ftype="tiff"/>
         </test>
         <test>
-            <param name="input" value="input1_uint8.tif" />
-            <param name="filter_type" value="prewitt_v" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input1_prewitt_v.tif" ftype="tiff"/>
+            <param name="input" value="input1_uint8.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="prewitt"/>
+                <param name="axis" value="0"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input1_prewitt_v.tiff" ftype="tiff"/>
         </test>
         <test>
-            <param name="input" value="input1_uint8.tif" />
-            <param name="filter_type" value="sobel_h" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input1_sobel_h.tif" ftype="tiff"/>
+            <param name="input" value="input1_uint8.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="sobel"/>
+                <param name="axis" value="1"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input1_sobel_h.tiff" ftype="tiff"/>
         </test>
         <test>
-            <param name="input" value="input1_uint8.tif" />
-            <param name="filter_type" value="sobel_v" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input1_sobel_v.tif" ftype="tiff"/>
+            <param name="input" value="input1_uint8.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="sobel"/>
+                <param name="axis" value="0"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input1_sobel_v.tiff" ftype="tiff"/>
         </test>
         <!-- Tests with float TIFF input image -->
         <test>
-            <param name="input" value="input2_float.tif" />
-            <param name="filter_type" value="gaussian" />
-            <expand macro="tests/intensity_image_diff" name="output" value="input2_gaussian.tif" ftype="tiff">
+            <param name="input" value="input2_float.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="gaussian"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input2_gaussian.tiff" ftype="tiff">
                 <!-- See note for Gaussian filter above. -->
                 <has_image_mean_intensity mean_intensity="0.25" eps="0.01"/>
             </expand>
         </test>
+        <test>
+            <param name="input" value="input2_float.tiff"/>
+            <conditional name="filter">
+                <param name="filter_type" value="uniform"/>
+                <param name="size" value="4"/>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="input2_uniform.tiff" ftype="tiff">
+                <!-- See note for Gaussian filter above. -->
+                <has_image_mean_intensity mean_intensity="0.25" eps="0.01"/>
+            </expand>
+        </test>
+        <!-- Tests with multi-channel image (RGB) -->
+        <test>
+            <param name="input" value="scikit-image/retina.png"/>
+            <conditional name="filter">
+                <param name="filter_type" value="gaussian"/>
+                <param name="sigma" value="5"/>
+                <conditional name="derivative">
+                    <param name="order" value="0"/>
+                </conditional>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="retina_gaussian_order0.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <param name="input" value="scikit-image/retina.png"/>
+            <conditional name="filter">
+                <param name="filter_type" value="gaussian"/>
+                <param name="sigma" value="3"/>
+                <conditional name="derivative">
+                    <param name="order" value="2"/>
+                    <param name="axis" value="0"/>
+                </conditional>
+            </conditional>
+            <expand macro="tests/intensity_image_diff" name="output" value="retina_gaussian_order2_axis0.tiff" ftype="tiff"/>
+        </test>
     </tests>
     <help>
 
-        **Applies a standard filter to a single-channel 2-D image.**
+        **Applies a standard, general-purpose 2-D filter to an image.**
+
+        Support for different image types:
 
-        Mean filters like the Gaussian filter or the median filter preserve both the brightness of the image, and the range of values.
+        - For 3-D images, the filter is applied to all z-slices of the image.
+        - For multi-channel images, the filter is applied to all channels of the image.
+        - For time-series images, the filter is also applied for all time steps.
+
+        Mean filters like the Gaussian filter, the box filter, or the median filter preserve both the brightness of the image, and
+        the range of values. This does not hold for the derivative variants of the Gaussian filter, which may produce negative values.
 
     </help>
     <citations>
--- a/filter_image.py	Wed Apr 24 08:12:03 2024 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-import argparse
-
-import giatools.io
-import scipy.ndimage as ndi
-import skimage.io
-import skimage.util
-from skimage.morphology import disk
-
-
-filters = {
-    'gaussian': lambda im, sigma: ndi.gaussian_filter(im, sigma),
-    'median': lambda im, radius: ndi.median_filter(im, footprint=disk(radius)),
-    'prewitt_h': lambda im, *args: ndi.prewitt(im, axis=1),
-    'prewitt_v': lambda im, *args: ndi.prewitt(im, axis=0),
-    'sobel_h': lambda im, *args: ndi.sobel(im, axis=1),
-    'sobel_v': lambda im, *args: ndi.sobel(im, axis=0),
-}
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser()
-    parser.add_argument('input', type=argparse.FileType('r'), help='Input file')
-    parser.add_argument('output', type=argparse.FileType('w'), help='Output file (TIFF)')
-    parser.add_argument('filter', choices=filters.keys(), help='Filter to be used')
-    parser.add_argument('size', type=float, help='Size of the filter (e.g., radius, sigma)')
-    args = parser.parse_args()
-
-    im = giatools.io.imread(args.input.name)
-    res = filters[args.filter](im, args.size)
-    skimage.io.imsave(args.output.name, res, plugin='tifffile')
Binary file test-data/input1_gaussian.tif has changed
Binary file test-data/input1_gaussian.tiff has changed
Binary file test-data/input1_median.tif has changed
Binary file test-data/input1_median.tiff has changed
Binary file test-data/input1_prewitt_h.tif has changed
Binary file test-data/input1_prewitt_h.tiff has changed
Binary file test-data/input1_prewitt_v.tif has changed
Binary file test-data/input1_prewitt_v.tiff has changed
Binary file test-data/input1_sobel_h.tif has changed
Binary file test-data/input1_sobel_h.tiff has changed
Binary file test-data/input1_sobel_v.tif has changed
Binary file test-data/input1_sobel_v.tiff has changed
Binary file test-data/input1_uint8.tif has changed
Binary file test-data/input1_uint8.tiff has changed
Binary file test-data/input2_float.tif has changed
Binary file test-data/input2_float.tiff has changed
Binary file test-data/input2_gaussian.tif has changed
Binary file test-data/input2_gaussian.tiff has changed
Binary file test-data/input2_uniform.tiff has changed
Binary file test-data/retina_gaussian_order0.tiff has changed
Binary file test-data/retina_gaussian_order2_axis0.tiff has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/scikit-image/LICENSE.txt	Fri Dec 12 21:18:21 2025 +0000
@@ -0,0 +1,28 @@
+Files: *
+Copyright: 2009-2022 the scikit-image team
+License: BSD-3-Clause
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+3. Neither the name of the University nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Binary file test-data/scikit-image/retina.png has changed