Skip to content

Commit 85b0f6a

Browse files
authored
Merge pull request #169 from kostrykin/dev/filter
Add `ridge_filter_skimage` tool for ridge filtering
2 parents a6fd77b + 4dcbf29 commit 85b0f6a

18 files changed

+419
-0
lines changed

tools/ridge_filter/.shed.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
categories:
2+
- Imaging
3+
description: Ridge filter for images
4+
long_description: Apply a ridge filter to an image
5+
name: ridge_filter
6+
owner: imgteam
7+
homepage_url: https://scikit-image.org/docs/0.25.x/auto_examples/edges/plot_ridge_filter.html
8+
remote_repository_url: https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/ridge_filter/

tools/ridge_filter/creators.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../macros/creators.xml
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import argparse
2+
import json
3+
from typing import (
4+
Any,
5+
Callable,
6+
)
7+
8+
import giatools
9+
import numpy as np
10+
import skimage.filters
11+
12+
13+
filters = {
14+
'frangi': lambda img, **kwargs: (
15+
apply_nd_filter(skimage.filters.frangi, img, **kwargs)
16+
),
17+
'hessian': lambda img, **kwargs: (
18+
apply_nd_filter(skimage.filters.hessian, img, **kwargs)
19+
),
20+
'laplace': lambda img, **kwargs: (
21+
apply_nd_filter(skimage.filters.laplace, img, **kwargs)
22+
),
23+
'meijering': lambda img, **kwargs: (
24+
apply_nd_filter(skimage.filters.meijering, img, **kwargs)
25+
),
26+
'sato': lambda img, **kwargs: (
27+
apply_nd_filter(skimage.filters.sato, img, **kwargs)
28+
),
29+
}
30+
31+
32+
def apply_nd_filter(
33+
filter_impl: Callable[[np.ndarray, Any, ...], np.ndarray],
34+
img: giatools.Image,
35+
dtype: str,
36+
**kwargs: Any,
37+
) -> giatools.Image:
38+
"""
39+
Apply the filter to the 2-D/3-D, potentially multi-frame and multi-channel image.
40+
"""
41+
result_data = np.empty(img.data.shape, dtype=dtype)
42+
for qtc in np.ndindex(
43+
img.data.shape[ 0], # Q axis
44+
img.data.shape[ 1], # T axis
45+
img.data.shape[-1], # C axis
46+
):
47+
sl = np.s_[*qtc[:2], ..., qtc[2]] # noqa: E999
48+
arr = img.data[sl]
49+
assert arr.ndim == 3 # sanity check, should always be True
50+
51+
# Perform 2-D or 3-D filtering
52+
if arr.shape[0] == 1:
53+
info = 'Performing 2-D filtering'
54+
result_data[sl][0] = filter_impl(arr[0], **kwargs).astype(dtype)
55+
else:
56+
info = 'Performing 3-D filtering'
57+
result_data[sl] = filter_impl(arr, **kwargs).astype(dtype)
58+
59+
# Print status info
60+
print(info)
61+
62+
# Return results as 16bit, 32bit, or 64bit floating point
63+
return giatools.Image(result_data.astype(dtype), img.axes)
64+
65+
66+
def apply_filter(
67+
input_filepath: str,
68+
output_filepath: str,
69+
filter_type: str,
70+
**kwargs: Any,
71+
):
72+
# Validate and transform input parameters
73+
params = dict(kwargs)
74+
if (sigma_min := params.pop('sigma_min', None)) is not None and (sigma_max := params.pop('sigma_max', None)) is not None:
75+
num_sigma = params.pop('num_sigma')
76+
if sigma_min < sigma_max:
77+
params['sigmas'] = np.linspace(sigma_min, sigma_max, num_sigma)
78+
elif sigma_min == sigma_max:
79+
params['sigmas'] = [sigma_min]
80+
else:
81+
raise ValueError(f'Minimum sigma ({sigma_min:g}) must not be greater than Maximum sigma ({sigma_max:g})')
82+
83+
# Read the input image
84+
img = giatools.Image.read(input_filepath)
85+
86+
# Perform filtering
87+
print(f'Applying filter: "{filter_type}"')
88+
filter_impl = filters[filter_type]
89+
res = filter_impl(img, **params).normalize_axes_like(img.original_axes)
90+
91+
# Adopt metadata and write the result
92+
res.metadata = img.metadata
93+
res.write(output_filepath, backend='tifffile')
94+
95+
96+
if __name__ == "__main__":
97+
parser = argparse.ArgumentParser()
98+
parser.add_argument('input', type=str)
99+
parser.add_argument('output', type=str)
100+
parser.add_argument('params', type=str)
101+
args = parser.parse_args()
102+
103+
# Read the config file
104+
with open(args.params) as cfgf:
105+
cfg = json.load(cfgf)
106+
107+
apply_filter(
108+
args.input,
109+
args.output,
110+
**cfg,
111+
)

tools/ridge_filter/skimage.xml

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<tool id="ridge_filter_skimage" name="Apply ridge filter" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.05">
2+
<description>with scikit-image</description>
3+
<macros>
4+
<import>creators.xml</import>
5+
<import>tests.xml</import>
6+
<token name="@TOOL_VERSION@">0.22.0</token>
7+
<token name="@VERSION_SUFFIX@">1</token>
8+
<xml name="base_params">
9+
<param name="black_ridges" type="select" label="Mode of operation">
10+
<option value="true" selected="true">Enhance dark ridges (low image intensities)</option>
11+
<option value="false">Enhance bright ridges (high image intensities)</option>
12+
</param>
13+
<param name="sigma_min" type="float" value="1" min="0.1" label="Minimum sigma" help="Lower bound of the scale for multi-scale analysis."/>
14+
<param name="sigma_max" type="float" value="3" min="0.1" label="Maximum sigma" help="Upper bound of the scale for multi-scale analysis."/>
15+
<param name="num_sigma" type="integer" value="10" min="2" label="Number of sigma steps for multi-scale analysis"/>
16+
</xml>
17+
<xml name="frangi_params">
18+
<expand macro="base_params"/>
19+
<param name="alpha" type="float" min="0" value="0.5" label="Alpha"
20+
help="Frangi correction constant that adjusts the filter's sensitivity to deviation from a plate-like structure."/>
21+
<param name="beta" type="float" min="0" value="0.5" label="Beta"
22+
help="Frangi correction constant that adjusts the filter's sensitivity to deviation from a blob-like structure."/>
23+
</xml>
24+
</macros>
25+
<creator>
26+
<expand macro="creators/bmcv"/>
27+
<expand macro="creators/kostrykin"/>
28+
</creator>
29+
<edam_operations>
30+
<edam_operation>operation_3443</edam_operation>
31+
</edam_operations>
32+
<xrefs>
33+
<xref type="bio.tools">galaxy_image_analysis</xref>
34+
<xref type="bio.tools">scikit-image</xref>
35+
<xref type="biii">scikit-image</xref>
36+
</xrefs>
37+
<requirements>
38+
<requirement type="package" version="@TOOL_VERSION@">scikit-image</requirement>
39+
<requirement type="package" version="0.5.2">giatools</requirement>
40+
</requirements>
41+
<command detect_errors="aggressive"><![CDATA[
42+
43+
python '$__tool_directory__/filter_skimage.py'
44+
45+
'$input'
46+
'$output'
47+
'$params'
48+
49+
]]></command>
50+
<configfiles>
51+
<configfile name="params"><![CDATA[
52+
{
53+
54+
#if $filter.filter_type == "frangi" or $filter.filter_type == "hessian" or $filter.filter_type == "meijering" or $filter.filter_type == "sato"
55+
"black_ridges": $filter.black_ridges,
56+
"sigma_min": $filter.sigma_min,
57+
"sigma_max": $filter.sigma_max,
58+
"num_sigma": $filter.num_sigma,
59+
#end if
60+
61+
#if $filter.filter_type == "frangi" or $filter.filter_type == "hessian"
62+
"alpha": $filter.alpha,
63+
"beta": $filter.beta,
64+
#end if
65+
66+
#if $filter.filter_type == "laplace"
67+
"ksize": $filter.ksize,
68+
#end if
69+
70+
"dtype": "$dtype",
71+
"filter_type": "$filter.filter_type"
72+
73+
}
74+
]]></configfile>
75+
</configfiles>
76+
<inputs>
77+
<param name="input" type="data" format="tiff,png" label="Input image"/>
78+
<param name="dtype" type="select" label="Output pixel type"
79+
help="Data type used to store the pixel values in the output image.">
80+
<option value="float64" selected="True">64-bit floating point</option>
81+
<option value="float32">32-bit floating point</option>
82+
<option value="float16">16-bit floating point</option>
83+
</param>
84+
<conditional name="filter">
85+
<param name="filter_type" type="select" label="Filter">
86+
<option value="frangi" selected="True">Frangi vesselness filter</option>
87+
<option value="hessian">Hybrid Hessian filter</option>
88+
<option value="laplace">Laplace filter</option>
89+
<option value="meijering">Meijering neuriteness filter</option>
90+
<option value="sato">Sato tubeness filter</option>
91+
</param>
92+
<when value="frangi">
93+
<expand macro="frangi_params"/>
94+
</when>
95+
<when value="hessian">
96+
<expand macro="frangi_params"/>
97+
</when>
98+
<when value="laplace">
99+
<param name="ksize" type="integer" value="3" label="Kernel size"
100+
help="Define the size of the discrete Laplacian operator such that it will have the size of a hypercube with this edge length (in pixels)."/>
101+
</when>
102+
<when value="meijering">
103+
<expand macro="base_params"/>
104+
</when>
105+
<when value="sato">
106+
<expand macro="base_params"/>
107+
</when>
108+
</conditional>
109+
</inputs>
110+
<outputs>
111+
<data format="tiff" name="output"/>
112+
</outputs>
113+
<tests>
114+
<!-- Single-image (Q), Single-frame (T), Single-slice (Z), multi-channel (C), Frangi (dark) -->
115+
<test>
116+
<param name="input" value="scikit-image/retina.png"/>
117+
<param name="dtype" value="float16"/>
118+
<conditional name="filter">
119+
<param name="filter_type" value="frangi"/>
120+
<param name="black_ridges" value="true"/>
121+
<param name="sigma_min" value="0.5"/>
122+
<param name="sigma_max" value="20"/>
123+
<param name="num_sigma" value="10"/>
124+
<param name="alpha" value="0.5"/>
125+
<param name="beta" value="0.5"/>
126+
</conditional>
127+
<expand macro="tests/intensity_image_diff" name="output" value="retina_frangi.tiff" ftype="tiff"/>
128+
<assert_stdout>
129+
<has_line line='Applying filter: "frangi"'/>
130+
<has_line line="Performing 2-D filtering"/>
131+
</assert_stdout>
132+
</test>
133+
<!-- Single-image (Q), Single-frame (T), Single-slice (Z), single-channel (C), Hessian (dark) -->
134+
<test>
135+
<param name="input" value="retina_0c_0z_0t_0q.tiff"/>
136+
<param name="dtype" value="float16"/>
137+
<conditional name="filter">
138+
<param name="filter_type" value="hessian"/>
139+
<param name="black_ridges" value="true"/>
140+
<param name="sigma_min" value="0.5"/>
141+
<param name="sigma_max" value="20"/>
142+
<param name="num_sigma" value="10"/>
143+
<param name="alpha" value="0.5"/>
144+
<param name="beta" value="0.5"/>
145+
</conditional>
146+
<expand macro="tests/intensity_image_diff" name="output" value="retina_0c_0z_0t_0q_hessian.tiff" ftype="tiff"/>
147+
<assert_stdout>
148+
<has_line line='Applying filter: "hessian"'/>
149+
<has_line line="Performing 2-D filtering"/>
150+
</assert_stdout>
151+
</test>
152+
<!-- Single-image (Q), Multi-frame (T), Single-slice (Z), single-channel (C), Meijering (dark) -->
153+
<test>
154+
<param name="input" value="retina_0c_0z_3t_0q.tiff"/>
155+
<param name="dtype" value="float16"/>
156+
<conditional name="filter">
157+
<param name="filter_type" value="meijering"/>
158+
<param name="black_ridges" value="true"/>
159+
<param name="sigma_min" value="0.5"/>
160+
<param name="sigma_max" value="20"/>
161+
<param name="num_sigma" value="10"/>
162+
</conditional>
163+
<expand macro="tests/intensity_image_diff" name="output" value="retina_0c_0z_3t_0q_meijering.tiff" ftype="tiff"/>
164+
<assert_stdout>
165+
<has_line line='Applying filter: "meijering"'/>
166+
<has_line line="Performing 2-D filtering"/>
167+
</assert_stdout>
168+
</test>
169+
<!-- Multi-image (Q), Single-frame (T), Single-slice (Z), single-channel (C), Sato (dark) -->
170+
<test>
171+
<param name="input" value="retina_0c_0z_0t_2q.tiff"/>
172+
<param name="dtype" value="float16"/>
173+
<conditional name="filter">
174+
<param name="filter_type" value="sato"/>
175+
<param name="black_ridges" value="true"/>
176+
<param name="sigma_min" value="0.5"/>
177+
<param name="sigma_max" value="20"/>
178+
<param name="num_sigma" value="10"/>
179+
</conditional>
180+
<expand macro="tests/intensity_image_diff" name="output" value="retina_0c_0z_0t_2q_sato.tiff" ftype="tiff"/>
181+
<assert_stdout>
182+
<has_line line='Applying filter: "sato"'/>
183+
<has_line line="Performing 2-D filtering"/>
184+
</assert_stdout>
185+
</test>
186+
<!-- Single-image (Q), Single-frame (T), Multi-slice (Z), single-channel (C), Laplace -->
187+
<test>
188+
<param name="input" value="retina_0c_5z_0t_0q.tiff"/>
189+
<param name="dtype" value="float16"/>
190+
<conditional name="filter">
191+
<param name="filter_type" value="laplace"/>
192+
<param name="ksize" value="5"/>
193+
</conditional>
194+
<expand macro="tests/intensity_image_diff" name="output" value="retina_0c_5z_0t_0q_laplace.tiff" ftype="tiff"/>
195+
<assert_stdout>
196+
<has_line line='Applying filter: "laplace"'/>
197+
<has_line line="Performing 3-D filtering"/>
198+
</assert_stdout>
199+
</test>
200+
<!-- Bright ridges, Single-image (Q), Single-frame (T), Multi-slice (Z), single-channel (C), Frangi (bright) -->
201+
<test>
202+
<param name="input" value="retina_inv_0c_0z_0t_0q.tiff"/>
203+
<param name="dtype" value="float16"/>
204+
<conditional name="filter">
205+
<param name="filter_type" value="frangi"/>
206+
<param name="black_ridges" value="false"/>
207+
<param name="sigma_min" value="0.5"/>
208+
<param name="sigma_max" value="20"/>
209+
<param name="num_sigma" value="10"/>
210+
<param name="alpha" value="0.5"/>
211+
<param name="beta" value="0.5"/>
212+
</conditional>
213+
<expand macro="tests/intensity_image_diff" name="output" value="retina_inv_0c_0z_0t_0q_frangi.tiff" ftype="tiff"/>
214+
<assert_stdout>
215+
<has_line line='Applying filter: "frangi"'/>
216+
<has_line line="Performing 2-D filtering"/>
217+
</assert_stdout>
218+
</test>
219+
</tests>
220+
<help>
221+
222+
**Applies a ridge filter to an image.**
223+
224+
Ridge filters can be used to detect ridge-like structures, such as neurites, tubes, vessels, wrinkles, or rivers. Different
225+
ridge filters may be suited for detecting different structures, e.g., depending on contrast or noise level. 2-D and 3-D
226+
images are supported. For multi-channel images, the filter is applied to all channels of the image. For time-series images,
227+
the filter is also applied for all time steps.
228+
229+
Frangi vesselness filter
230+
========================
231+
232+
Filter an image with the Frangi vesselness filter. This filter can be used to detect continuous ridges, e.g. vessels,
233+
wrinkles, rivers. It can be used to calculate the fraction of the whole image containing such objects. Calculates the
234+
eigenvalues of the Hessian to compute the similarity of an image region to vessels, according to the method described in
235+
Frangi et al. (1998).
236+
237+
Hybrid Hessian filter
238+
=====================
239+
240+
Filter an image with the Hybrid Hessian filter (Schrijver 2001, Kroon 2009). This filter can be used to detect continuous
241+
edges, e.g. vessels, wrinkles, rivers. It can be used to calculate the fraction of the whole image containing such objects.
242+
Almost equal to Frangi filter, but uses alternative method of smoothing.
243+
244+
Laplace filter
245+
==============
246+
247+
Find the edges of an image using the Laplace operator.
248+
249+
Meijering neuriteness filter
250+
============================
251+
252+
Filter an image with the Meijering neuriteness filter. This filter can be used to detect continuous ridges, e.g. neurites,
253+
wrinkles, rivers. It can be used to calculate the fraction of the whole image containing such objects. Calculates the
254+
eigenvalues of the Hessian to compute the similarity of an image region to neurites, according to the method described in
255+
Meijering et al. (2004).
256+
257+
Sato tubeness filter
258+
====================
259+
260+
Filter an image with the Sato tubeness filter. This filter can be used to detect continuous ridges, e.g. tubes, wrinkles,
261+
rivers. It can be used to calculate the fraction of the whole image containing such objects. Calculates the eigenvalues of
262+
the Hessian to compute the similarity of an image region to tubes, according to the method described in Sato et al. (1998).
263+
264+
</help>
265+
<citations>
266+
<citation type="doi">10.1007/BFb0056195</citation>
267+
<citation type="doi">10.1002/cyto.a.20022</citation>
268+
<citation type="doi">10.1016/S1361-8415(98)80009-1</citation>
269+
</citations>
270+
</tool>
457 KB
Binary file not shown.
229 KB
Binary file not shown.
557 KB
Binary file not shown.
301 KB
Binary file not shown.
768 KB
Binary file not shown.
384 KB
Binary file not shown.

0 commit comments

Comments
 (0)