changeset 2:387414aa6496 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/color_deconvolution/ commit f546b3cd5cbd3a8613cd517975c7ad1d1f83514e
author imgteam
date Thu, 06 Mar 2025 18:12:13 +0000
parents 29110ca1b63a
children
files color-deconvolution.xml color_deconvolution.py color_deconvolution.xml creators.xml static/images/he.png static/images/he_deconv.png static/images/he_recomposed.png test-data/galaxyIcon_noText.png test-data/galaxyIcon_noText.tiff test-data/hdab1.tiff test-data/hdab1_deconv_hdab.tiff test-data/he1.tiff test-data/he1_axes_cyx.tiff test-data/he1_axes_yxz.tiff test-data/he1_deconv_he.tiff test-data/he1_deconv_hed.tiff test-data/he1_deconv_hed_recomposed.tiff test-data/he1_deconv_hed_recomposed1.tiff test-data/he1_hsv.tiff test-data/im_axes_yx.tif tests.xml
diffstat 21 files changed, 433 insertions(+), 163 deletions(-) [+]
line wrap: on
line diff
--- a/color-deconvolution.xml	Mon Jul 22 04:55:58 2019 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-<tool id="ip_color_deconvolution" name="Color Deconvolution" version="0.8"> 
-    <description>Color deconvolution</description>
-    <requirements>
-        <requirement type="package" version="0.14.2">scikit-image</requirement>
-        <requirement type="package" version="4.0.0">pillow</requirement>
-        <requirement type="package" version="0.18.1">scikit-learn</requirement>
-        <requirement type="package" version="1.12">numpy</requirement>
-        <requirement type="package" version="0.15.1">tifffile</requirement>
-    </requirements>
-    <command detect_errors="aggressive">
-        <![CDATA[ 
-        python '$__tool_directory__/color_deconvolution.py' '$input' '$output' '$convtype'
-        ]]>
-    </command>
-    <inputs>
-        <param name="input" type="data" format="tiff,png,jpg,bmp" label="Image file with 3 channels"/>
-        <param name="convtype" type="select" label="Transformation type">
-            <option value="ica" selected="True">ica</option>
-            <option value="pca">pca</option>
-            <option value="nmf">nmf</option>
-            <option value="fa">fa</option>
-            <option value="xyz2rgb">xyz2rgb</option>
-            <option value="rgb_from_rbd">rgb_from_rbd</option>
-            <option value="rgb_from_hdx">rgb_from_hdx</option>
-            <option value="rgb2hsv">rgb2hsv</option>
-            <option value="rgb_from_bro">rgb_from_bro</option>
-            <option value="bpx_from_rgb">bpx_from_rgb</option>
-            <option value="hed_from_rgb">hed_from_rgb</option>
-            <option value="rgbcie2rgb">rgbcie2rgb</option>
-            <option value="hdx_from_rgb">hdx_from_rgb</option>
-            <option value="xyz2luv">xyz2luv</option>
-            <option value="rgb2lab">rgb2lab</option>
-            <option value="hpx_from_rgb">hpx_from_rgb</option>
-            <option value="rgb_from_fgx">rgb_from_fgx</option>
-            <option value="rgb_from_gdx">rgb_from_gdx</option>
-            <option value="lab2xyz">lab2xyz</option>
-            <option value="rgb_from_hpx">rgb_from_hpx</option>
-            <option value="lab2rgb">lab2rgb</option>
-            <option value="rgb2rgbcie">rgb2rgbcie</option>
-            <option value="bex_from_rgb">bex_from_rgb</option>
-            <option value="xyz2lab">xyz2lab</option>
-            <option value="rgb_from_bex">rgb_from_bex</option>
-            <option value="fgx_from_rgb">fgx_from_rgb</option>
-            <option value="rbd_from_rgb">rbd_from_rgb</option>
-            <option value="rgb2hed">rgb2hed</option>
-            <option value="hed2rgb">hed2rgb</option>
-            <option value="luv2rgb">luv2rgb</option>
-            <option value="luv2xyz">luv2xyz</option>
-            <option value="lch2lab">lch2lab</option>
-            <option value="rgb2luv">rgb2luv</option>
-            <option value="ahx_from_rgb">ahx_from_rgb</option>
-            <option value="rgb_from_hax">rgb_from_hax</option>
-            <option value="hax_from_rgb">hax_from_rgb</option>
-            <option value="rgb_from_bpx">rgb_from_bpx</option>
-            <option value="rgb2xyz">rgb2xyz</option>
-            <option value="gdx_from_rgb">gdx_from_rgb</option>
-            <option value="rgb_from_ahx">rgb_from_ahx</option>
-            <option value="lab2lch">lab2lch</option>
-            <option value="rgb_from_hed">rgb_from_hed</option>
-            <option value="bro_from_rgb">bro_from_rgb</option>
-            <option value="hsv2rgb">hsv2rgb</option>
-        </param>
-    </inputs>
-    <outputs>
-        <data format="tiff" name="output"/>
-    </outputs>
-    <tests>
-        <test>
-            <param name="input" value="galaxyIcon_noText.png" />
-            <param name="convtype" value="rgb2hsv" />
-            <output name="output" ftype="tiff" />
-        </test>
-    </tests>
-    <help>
-    **What it does**
-
-    This tools performs several color deconvolution techniques.</help>
-    <citations>
-      <citation type="doi">10.7717/peerj.453</citation>
-      <citation type="bibtex">@inproceedings{sklearn_api,
-  author    = {Lars Buitinck and Gilles Louppe and Mathieu Blondel and
-               Fabian Pedregosa and Andreas Mueller and Olivier Grisel and
-               Vlad Niculae and Peter Prettenhofer and Alexandre Gramfort
-               and Jaques Grobler and Robert Layton and Jake VanderPlas and
-               Arnaud Joly and Brian Holt and Ga{\"{e}}l Varoquaux},
-  title     = {{API} design for machine learning software: experiences from the scikit-learn
-               project},
-  booktitle = {ECML PKDD Workshop: Languages for Data Mining and Machine Learning},
-  year      = {2013},
-  pages = {108--122},
-}</citation>
-    </citations>
-</tool>
--- a/color_deconvolution.py	Mon Jul 22 04:55:58 2019 -0400
+++ b/color_deconvolution.py	Thu Mar 06 18:12:13 2025 +0000
@@ -1,87 +1,115 @@
 import argparse
 import sys
 import warnings
+
+import giatools.io
 import numpy as np
-import skimage.io
 import skimage.color
+import skimage.io
 import skimage.util
-from sklearn.decomposition import PCA, NMF, FastICA, FactorAnalysis
+import tifffile
+from sklearn.decomposition import FactorAnalysis, FastICA, NMF, PCA
+
+# Stain separation matrix for H&E color deconvolution, extracted from ImageJ/FIJI
+rgb_from_he = np.array([
+    [0.64431860, 0.7166757, 0.26688856],
+    [0.09283128, 0.9545457, 0.28324000],
+    [0.63595444, 0.0010000, 0.77172660],
+])
 
 convOptions = {
-           'hed2rgb' : lambda img_raw: skimage.color.hed2rgb(img_raw),
-           'hsv2rgb' : lambda img_raw: skimage.color.hsv2rgb(img_raw),
-           'lab2lch' : lambda img_raw: skimage.color.lab2lch(img_raw),
-           'lab2rgb' : lambda img_raw: skimage.color.lab2rgb(img_raw),
-           'lab2xyz' : lambda img_raw: skimage.color.lab2xyz(img_raw),
-           'lch2lab' : lambda img_raw: skimage.color.lch2lab(img_raw),
-           'luv2rgb' : lambda img_raw: skimage.color.luv2rgb(img_raw),
-           'luv2xyz' : lambda img_raw: skimage.color.luv2xyz(img_raw),
-           'rgb2hed' : lambda img_raw: skimage.color.rgb2hed(img_raw),
-           'rgb2hsv' : lambda img_raw: skimage.color.rgb2hsv(img_raw),
-           'rgb2lab' : lambda img_raw: skimage.color.rgb2lab(img_raw),
-           'rgb2luv' : lambda img_raw: skimage.color.rgb2luv(img_raw),
-           'rgb2rgbcie' : lambda img_raw: skimage.color.rgb2rgbcie(img_raw),
-           'rgb2xyz' : lambda img_raw: skimage.color.rgb2xyz(img_raw),
-           #'rgb2ycbcr' : lambda img_raw: skimage.color.rgb2ycbcr(img_raw),
-           #'rgb2yiq' : lambda img_raw: skimage.color.rgb2yiq(img_raw),
-           #'rgb2ypbpr' : lambda img_raw: skimage.color.rgb2ypbpr(img_raw),
-           #'rgb2yuv' : lambda img_raw: skimage.color.rgb2yuv(img_raw),
-           #'rgba2rgb' : lambda img_raw: skimage.color.rgba2rgb(img_raw),
-           'rgbcie2rgb' : lambda img_raw: skimage.color.rgbcie2rgb(img_raw),
-           'xyz2lab' : lambda img_raw: skimage.color.xyz2lab(img_raw),
-           'xyz2luv' : lambda img_raw: skimage.color.xyz2luv(img_raw),
-           'xyz2rgb' : lambda img_raw: skimage.color.xyz2rgb(img_raw),
-           #'ycbcr2rgb' : lambda img_raw: skimage.color.ycbcr2rgb(img_raw),
-           #'yiq2rgb' : lambda img_raw: skimage.color.yiq2rgb(img_raw),
-           #'ypbpr2rgb' : lambda img_raw: skimage.color.ypbpr2rgb(img_raw),
-           #'yuv2rgb' : lambda img_raw: skimage.color.yuv2rgb(img_raw),    
-    
-           'rgb_from_hed' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hed),
-           'rgb_from_hdx' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hdx),
-           'rgb_from_fgx' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_fgx),
-           'rgb_from_bex' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_bex),
-           'rgb_from_rbd' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_rbd),
-           'rgb_from_gdx' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_gdx),
-           'rgb_from_hax' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hax),
-           'rgb_from_bro' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_bro),
-           'rgb_from_bpx' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_bpx),
-           'rgb_from_ahx' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_ahx),
-           'rgb_from_hpx' : lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hpx),
-    
-           'hed_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hed_from_rgb),
-           'hdx_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hdx_from_rgb),
-           'fgx_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.fgx_from_rgb),
-           'bex_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.bex_from_rgb),
-           'rbd_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.rbd_from_rgb),
-           'gdx_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.gdx_from_rgb),
-           'hax_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hax_from_rgb),
-           'bro_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.bro_from_rgb),
-           'bpx_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.bpx_from_rgb),
-           'ahx_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.ahx_from_rgb),
-           'hpx_from_rgb' : lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hpx_from_rgb),    
-    
-           'pca' : lambda img_raw: np.reshape(PCA(n_components=3).fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])), 
-                              [img_raw.shape[0],img_raw.shape[1],-1]),    
-           'nmf' : lambda img_raw: np.reshape(NMF(n_components=3, init='nndsvda').fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])), 
-                              [img_raw.shape[0],img_raw.shape[1],-1]),
-           'ica' : lambda img_raw: np.reshape(FastICA(n_components=3).fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])), 
-                              [img_raw.shape[0],img_raw.shape[1],-1]),
-           'fa' : lambda img_raw: np.reshape(FactorAnalysis(n_components=3).fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])), 
-                              [img_raw.shape[0],img_raw.shape[1],-1])
+    # General color space conversion operations
+    'hed2rgb': lambda img_raw: skimage.color.hed2rgb(img_raw),
+    'hsv2rgb': lambda img_raw: skimage.color.hsv2rgb(img_raw),
+    'lab2lch': lambda img_raw: skimage.color.lab2lch(img_raw),
+    'lab2rgb': lambda img_raw: skimage.color.lab2rgb(img_raw),
+    'lab2xyz': lambda img_raw: skimage.color.lab2xyz(img_raw),
+    'lch2lab': lambda img_raw: skimage.color.lch2lab(img_raw),
+    'luv2rgb': lambda img_raw: skimage.color.luv2rgb(img_raw),
+    'luv2xyz': lambda img_raw: skimage.color.luv2xyz(img_raw),
+    'rgb2hed': lambda img_raw: skimage.color.rgb2hed(img_raw),
+    'rgb2hsv': lambda img_raw: skimage.color.rgb2hsv(img_raw),
+    'rgb2lab': lambda img_raw: skimage.color.rgb2lab(img_raw),
+    'rgb2luv': lambda img_raw: skimage.color.rgb2luv(img_raw),
+    'rgb2rgbcie': lambda img_raw: skimage.color.rgb2rgbcie(img_raw),
+    'rgb2xyz': lambda img_raw: skimage.color.rgb2xyz(img_raw),
+    'rgbcie2rgb': lambda img_raw: skimage.color.rgbcie2rgb(img_raw),
+    'xyz2lab': lambda img_raw: skimage.color.xyz2lab(img_raw),
+    'xyz2luv': lambda img_raw: skimage.color.xyz2luv(img_raw),
+    'xyz2rgb': lambda img_raw: skimage.color.xyz2rgb(img_raw),
+
+    # Color deconvolution operations
+    'hed_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hed_from_rgb),
+    'hdx_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hdx_from_rgb),
+    'fgx_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.fgx_from_rgb),
+    'bex_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.bex_from_rgb),
+    'rbd_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.rbd_from_rgb),
+    'gdx_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.gdx_from_rgb),
+    'hax_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hax_from_rgb),
+    'bro_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.bro_from_rgb),
+    'bpx_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.bpx_from_rgb),
+    'ahx_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.ahx_from_rgb),
+    'hpx_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, skimage.color.hpx_from_rgb),
+
+    # Recomposition operations (reverse color deconvolution)
+    'rgb_from_hed': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hed),
+    'rgb_from_hdx': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hdx),
+    'rgb_from_fgx': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_fgx),
+    'rgb_from_bex': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_bex),
+    'rgb_from_rbd': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_rbd),
+    'rgb_from_gdx': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_gdx),
+    'rgb_from_hax': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hax),
+    'rgb_from_bro': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_bro),
+    'rgb_from_bpx': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_bpx),
+    'rgb_from_ahx': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_ahx),
+    'rgb_from_hpx': lambda img_raw: skimage.color.combine_stains(img_raw, skimage.color.rgb_from_hpx),
+
+    # Custom color deconvolution and recomposition operations
+    'rgb_from_he': lambda img_raw: skimage.color.combine_stains(img_raw, rgb_from_he),
+    'he_from_rgb': lambda img_raw: skimage.color.separate_stains(img_raw, np.linalg.inv(rgb_from_he)),
+
+    # Unsupervised machine learning-based operations
+    'pca': lambda img_raw: np.reshape(PCA(n_components=3).fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])),
+                                      [img_raw.shape[0], img_raw.shape[1], -1]),
+    'nmf': lambda img_raw: np.reshape(NMF(n_components=3, init='nndsvda').fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])),
+                                      [img_raw.shape[0], img_raw.shape[1], -1]),
+    'ica': lambda img_raw: np.reshape(FastICA(n_components=3).fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])),
+                                      [img_raw.shape[0], img_raw.shape[1], -1]),
+    'fa': lambda img_raw: np.reshape(FactorAnalysis(n_components=3).fit_transform(np.reshape(img_raw, [-1, img_raw.shape[2]])),
+                                     [img_raw.shape[0], img_raw.shape[1], -1])
 }
 
 parser = argparse.ArgumentParser()
 parser.add_argument('input_file', type=argparse.FileType('r'), default=sys.stdin, help='input file')
 parser.add_argument('out_file', type=argparse.FileType('w'), default=sys.stdin, help='out file (TIFF)')
 parser.add_argument('conv_type', choices=convOptions.keys(), help='conversion type')
-args = parser.parse_args() 
+parser.add_argument('--isolate_channel', type=int, help='set all other channels to zero (1-3)', default=0)
+args = parser.parse_args()
+
+# Read and normalize the input image as TZYXC
+img_in = giatools.io.imread(args.input_file.name)
+
+# Verify input image
+assert img_in.shape[0] == 1, f'Image must have 1 frame (it has {img_in.shape[0]} frames)'
+assert img_in.shape[1] == 1, f'Image must have 1 slice (it has {img_in.shape[1]} slices)'
+assert img_in.shape[4] == 3, f'Image must have 3 channels (it has {img_in.shape[4]} channels)'
 
-img_in = skimage.io.imread(args.input_file.name)[:,:,0:3]
-res = convOptions[args.conv_type](img_in)
-res[res<-1]=-1
-res[res>1]=1
+# Normalize the image from TZYXC to YXC
+img_in = img_in.squeeze()
+assert img_in.ndim == 3
+
+# Apply channel isolation
+if args.isolate_channel:
+    for ch in range(3):
+        if ch + 1 != args.isolate_channel:
+            img_in[:, :, ch] = 0
+
+result = convOptions[args.conv_type](img_in)
+
+# It is sufficient to store 32bit floating point data, the precision loss is tolerable
+if result.dtype == np.float64:
+    result = result.astype(np.float32)
 
 with warnings.catch_warnings():
-	warnings.simplefilter("ignore")
-	res = skimage.util.img_as_uint(res) #Attention: precision loss
-	skimage.io.imsave(args.out_file.name, res, plugin='tifffile')
+    warnings.simplefilter('ignore')
+    tifffile.imwrite(args.out_file.name, result)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/color_deconvolution.xml	Thu Mar 06 18:12:13 2025 +0000
@@ -0,0 +1,212 @@
+<tool id="ip_color_deconvolution" name="Perform color deconvolution or transformation" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@"> 
+    <description></description>
+    <macros>
+        <import>creators.xml</import>
+        <import>tests.xml</import>
+        <token name="@TOOL_VERSION@">0.9</token>
+        <token name="@VERSION_SUFFIX@">0</token>
+    </macros>
+    <creator>
+        <expand macro="creators/bmcv"/>
+    </creator>
+    <edam_operations>
+        <edam_operation>operation_3443</edam_operation>
+    </edam_operations>
+    <xrefs>
+        <xref type="bio.tools">galaxy_image_analysis</xref>
+    </xrefs>
+    <requirements>
+        <requirement type="package" version="0.24.0">scikit-image</requirement>
+        <requirement type="package" version="11.0.0">pillow</requirement>
+        <requirement type="package" version="1.5.2">scikit-learn</requirement>
+        <requirement type="package" version="2.1.2">numpy</requirement>
+        <requirement type="package" version="2024.9.20">tifffile</requirement>
+        <requirement type="package" version="0.3.1">giatools</requirement>
+    </requirements>
+    <command detect_errors="aggressive">
+        <![CDATA[ 
+
+        python '$__tool_directory__/color_deconvolution.py'
+
+            '$input'
+            '$output'
+            '$convtype'
+
+            #if str($isolate_channel) != '':
+                --isolate_channel '${isolate_channel}'
+            #end if
+
+        ]]>
+    </command>
+    <inputs>
+        <param name="input" type="data" format="tiff,png,jpg,bmp" label="Input image" help="The input image must have 3 axes and 3 channels. The channels must correspond to the last axis."/>
+        <param name="convtype" type="select" label="Transformation type">
+
+            <!-- Color deconvolution and recomposition operations -->
+            <option value="he_from_rgb">Deconvolve RGB into H&#38;E (Hematoxylin + Eosin)</option>
+            <option value="rgb_from_he">Recompose RGB from H&#38;E (Hematoxylin + Eosin)</option>
+            <option value="hdx_from_rgb">Deconvolve RGB into Hematoxylin + DAB</option>
+            <option value="rgb_from_hdx">Recompose RGB from Hematoxylin + DAB</option>
+            <option value="rgb2hed">Deconvolve RGB into Hematoxylin + Eosin + DAB</option>
+            <option value="hed2rgb">Recompose RGB from Hematoxylin + Eosin + DAB</option>
+            <option value="hpx_from_rgb">Deconvolve RGB into Hematoxylin + PAS</option>
+            <option value="rgb_from_hpx">Recompose RGB from Hematoxylin + PAS</option>
+            <option value="hax_from_rgb">Deconvolve RGB into Hematoxylin + AEC</option>
+            <option value="rgb_from_hax">Recompose RGB from Hematoxylin + AEC</option>
+            <option value="ahx_from_rgb">Deconvolve RGB into Alcian Blue + Hematoxylin</option>
+            <option value="rgb_from_ahx">Recompose RGB from Alcian Blue + Hematoxylin</option>
+            <option value="bro_from_rgb">Deconvolve RGB to AZAN</option>
+            <option value="rgb_from_bro">Recompose RGB from AZAN</option>
+            <option value="rbd_from_rgb">Deconvolve RGB into FastRed + FastBlue + DAB</option>
+            <option value="rgb_from_rbd">Recompose RGB from FastRed + FastBlue + DAB</option>
+            <option value="fgx_from_rgb">Deconvolve RGB into Feulgen + Light Green</option>
+            <option value="rgb_from_fgx">Recompose RGB from Feulgen + Light Green</option>
+            <option value="bex_from_rgb">Deconvolve RGB into Giemsa stain (Methyl Blue + Eosin)</option>
+            <option value="rgb_from_bex">Recompose RGB from Giemsa stain (Methyl Blue + Eosin)</option>
+            <option value="bpx_from_rgb">Deconvolve RGB into Methyl Blue + Ponceau Fuchsin</option>
+            <option value="rgb_from_bpx">Recompose RGB from Methyl Blue + Ponceau Fuchsin</option>
+            <option value="gdx_from_rgb">Deconvolve RGB into Methyl Green + DAB</option>
+            <option value="rgb_from_gdx">Recompose RGB from Methyl Green + DAB</option>
+
+            <!-- Unsupervised machine learning-based operations -->
+            <option value="ica">Project onto independent components (ICA)</option>
+            <option value="pca">Project onto principal components (PCA)</option>
+            <option value="nmf">Non-negative matrix factorization</option>
+            <option value="fa">Factor analysis</option>
+
+            <!-- General color space conversion operations -->
+            <option value="rgb2hsv" selected="True">Convert RGB to HSV</option>
+            <option value="hsv2rgb">Convert HSV to RGB</option>
+            <option value="rgb2xyz">Convert RGB to XYZ (CIE 1931)</option>
+            <option value="xyz2rgb">Convert XYZ (CIE 1931) to RGB</option>
+            <option value="rgb2luv">Convert RGB to CIE-LUV</option>
+            <option value="luv2rgb">Convert CIE-LUV to RGB</option>
+            <option value="xyz2luv">Convert XYZ (CIE 1931) to CIE-LUV</option>
+            <option value="luv2xyz">Convert CIE-LUV to XYZ (CIE 1931)</option>
+            <option value="rgb2lab">Convert RGB to CIE-LAB (illuminant D65, aperture angle 2&#xb0;)</option>
+            <option value="lab2rgb">Convert CIE-LAB (illuminant D65, aperture angle 2&#xb0;) to RGB</option>
+            <option value="lab2xyz">Convert CIE-LAB (illuminant D65, aperture angle 2&#xb0;) to XYZ (CIE 1931)</option>
+            <option value="xyz2lab">Convert XYZ (CIE 1931) to CIE-LAB (illuminant D65, aperture angle 2&#xb0;)</option>
+            <option value="lch2lab">Convert CIE-LCh to CIE-LAB</option>
+            <option value="lab2lch">Convert CIE-LAB to CIE-LCh</option>
+            <option value="rgb2rgbcie">Convert RGB to RGB-CIE</option>
+            <option value="rgbcie2rgb">Convert RGB-CIE to RGB</option>
+
+        </param>
+        <param name="isolate_channel" type="integer" optional="true" value="" label="Isolate channel" help="Must be empty or the number of a channel (1 to 3). If used, all other channels are set to zero before performing the chosen transformation." />
+    </inputs>
+    <outputs>
+        <data format="tiff" name="output"/>
+    </outputs>
+    <tests>
+
+        <!-- Tests with compatible input files -->
+
+        <test>
+            <!-- Test PNG input and standard color transformation -->
+            <param name="input" value="galaxyIcon_noText.png" />
+            <param name="convtype" value="rgb2hsv" />
+            <expand macro="tests/intensity_image_diff" name="output" value="galaxyIcon_noText.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <!-- Test deconvolution using scikit-image (H&E + DAB) -->
+            <param name="input" value="he1.tiff" />
+            <param name="convtype" value="rgb2hed" />
+            <expand macro="tests/intensity_image_diff" name="output" value="he1_deconv_hed.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <!-- Test full recomposition -->
+            <param name="input" value="he1_deconv_hed.tiff" />
+            <param name="convtype" value="hed2rgb" />
+            <expand macro="tests/intensity_image_diff" name="output" value="he1_deconv_hed_recomposed.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <!-- Test recomposition with isolated channel -->
+            <param name="input" value="he1_deconv_hed.tiff" />
+            <param name="convtype" value="hed2rgb" />
+            <param name="isolate_channel" value="1" />
+            <expand macro="tests/intensity_image_diff" name="output" value="he1_deconv_hed_recomposed1.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <!-- Test deconvolution using custom stain matrix -->
+            <param name="input" value="he1.tiff" />
+            <param name="convtype" value="he_from_rgb" />
+            <expand macro="tests/intensity_image_diff" name="output" value="he1_deconv_he.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <!-- Test deconvolution using scikit-image (Hematoxylin + DAB) -->
+            <param name="input" value="hdab1.tiff" />
+            <param name="convtype" value="hdx_from_rgb" />
+            <expand macro="tests/intensity_image_diff" name="output" value="hdab1_deconv_hdab.tiff" ftype="tiff"/>
+        </test>
+        <test>
+            <!-- Test with image that has 3 axes but in unusual order (and 3 channels) -->
+            <param name="input" value="he1_axes_cyx.tiff" />
+            <param name="convtype" value="rgb2hsv" />
+            <expand macro="tests/intensity_image_diff" name="output" value="he1_hsv.tiff" ftype="tiff"/>
+        </test>
+
+        <!-- Tests with incompatible input files (should fail) -->
+
+        <test expect_failure="true">
+            <!-- Test with image that only has 2 axes instead of required 3 -->
+            <param name="input" value="im_axes_yx.tif" />
+            <param name="convtype" value="rgb2hsv" />
+        </test>
+        <test expect_failure="true">
+            <!-- Test with image that has 3 axes but YXZ instead of YXC (and 3 slices) -->
+            <param name="input" value="he1_axes_yxz.tiff" />
+            <param name="convtype" value="rgb2hsv" />
+        </test>
+
+    </tests>
+    <help><![CDATA[
+
+**This tool converts the color space of an image.**
+
+Several color deconvolution techniques are also supported.
+
+Color Deconvolution Example
+===========================
+
+In this example, we are going to perform color deconvolution of the following RGB image:
+
+.. image:: he.png
+  :width: 434px
+  :scale: 50%
+
+Using the option "Deconvolve RGB into H&E (Hematoxylin + Eosin)" for the **Transformation type** performs color deconvolution and yields a new image with three chnnales:
+
+.. image:: he_deconv.png
+  :width: 1305px
+  :scale: 50%
+
+The channels of the deconvolved image are the Hematoxylin intensities (Channel 1), the Eosin intensities (Channel 2), and the residuals (Channel 3). White image regions correspond to high intensities and black image regions correspond to low intensities.
+
+For visual inspection of the color deconvolution results, it may be useful to recompose separate RGB images for the Hematoxylin, Eosin, and residual channels. To create such images, this tool must be run once for each channel of the deconvolved image (i.e. three times), using the following options:
+
+* **Input image:** The result of the color deconvolution (image shown in the figure above)
+* **Transformation type:** Recompose RGB from H&E (Hematoxylin + Eosin)
+
+The **Isolate channel** field must be set to 1, 2, and 3 during the three runs, respectively. This will yield the following recomposed RGB images for better visualization of the color deconvolution results:
+
+.. image:: he_recomposed.png
+  :width: 1305px
+  :scale: 50%
+
+    ]]></help>
+    <citations>
+      <citation type="doi">10.7717/peerj.453</citation>
+      <citation type="bibtex">@inproceedings{sklearn_api,
+            author = {Lars Buitinck and Gilles Louppe and Mathieu Blondel and
+                      Fabian Pedregosa and Andreas Mueller and Olivier Grisel and
+                      Vlad Niculae and Peter Prettenhofer and Alexandre Gramfort
+                      and Jaques Grobler and Robert Layton and Jake VanderPlas and
+                      Arnaud Joly and Brian Holt and Ga{\"{e}}l Varoquaux},
+            title = {{API} design for machine learning software: experiences from the scikit-learn project},
+            booktitle = {ECML PKDD Workshop: Languages for Data Mining and Machine Learning},
+            year = {2013},
+            pages = {108--122},
+        }</citation>
+    </citations>
+</tool>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/creators.xml	Thu Mar 06 18:12:13 2025 +0000
@@ -0,0 +1,28 @@
+<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/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>
+    
+</macros>
Binary file static/images/he.png has changed
Binary file static/images/he_deconv.png has changed
Binary file static/images/he_recomposed.png has changed
Binary file test-data/galaxyIcon_noText.png has changed
Binary file test-data/galaxyIcon_noText.tiff has changed
Binary file test-data/hdab1.tiff has changed
Binary file test-data/hdab1_deconv_hdab.tiff has changed
Binary file test-data/he1.tiff has changed
Binary file test-data/he1_axes_cyx.tiff has changed
Binary file test-data/he1_axes_yxz.tiff has changed
Binary file test-data/he1_deconv_he.tiff has changed
Binary file test-data/he1_deconv_hed.tiff has changed
Binary file test-data/he1_deconv_hed_recomposed.tiff has changed
Binary file test-data/he1_deconv_hed_recomposed1.tiff has changed
Binary file test-data/he1_hsv.tiff has changed
Binary file test-data/im_axes_yx.tif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests.xml	Thu Mar 06 18:12:13 2025 +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>