diff --git a/src/imcflibs/imagej/processing.py b/src/imcflibs/imagej/processing.py index 41a6b9a..ddcae63 100644 --- a/src/imcflibs/imagej/processing.py +++ b/src/imcflibs/imagej/processing.py @@ -9,13 +9,77 @@ from ..log import LOG as log +def filter_options(filter_method, filter_radius, do_3d=False): + """Build the ImageJ filter command and options strings. + + Parameters + ---------- + filter_method : str + Name of the filter method to use. Must be one of: + - Median + - Mean + - Gaussian Blur + - Minimum + - Maximum + filter_radius : int + Radius of the filter to use. + do_3d : bool, optional + If set to True, will do a 3D filtering, by default False. + + Returns + ------- + tuple[str, str] + The filter name and options strings. + """ + + if do_3d: + filter_name = filter_method + " 3D..." + else: + filter_name = filter_method + "..." + + if filter_method == "Gaussian Blur": + options = "sigma=" + str(filter_radius) + " stack" + else: + options = "radius=" + str(filter_radius) + " stack" + + return filter_name, options + + +def threshold_options(threshold_method, do_3d=True): + """Build the ImageJ threshold option strings. + + Parameters + ---------- + threshold_method : str + Name of the threshold method to use. + do_3d : bool, optional + If set to True, the automatic threshold will be done on a 3D stack, + by default True. + + Returns + ------- + tuple[str, str] + The auto threshold options and the convert to binary options strings. + """ + + auto_threshold_options = ( + threshold_method + " " + "dark" + " " + "stack" if do_3d else "" + ) + + convert_to_binary_options = ( + "method=" + threshold_method + " " + "background=Dark" + " " + "black" + ) + + return auto_threshold_options, convert_to_binary_options + + def apply_filter(imp, filter_method, filter_radius, do_3d=False): """Make a specific filter followed by a threshold method of choice. Parameters ---------- imp : ImagePlus - Input ImagePlus to filter and threshold + Input ImagePlus to filter and threshold. filter_method : str Name of the filter method to use. Must be one of: - Median @@ -24,16 +88,17 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): - Minimum - Maximum filter_radius : int - Radius of the filter filter to use + Radius of the filter filter to use. do_3d : bool, optional - If set to True, will do a 3D filtering, by default False + If set to True, will do a 3D filtering, by default False. Returns ------- ij.ImagePlus - Filtered ImagePlus + Filtered ImagePlus. """ + log.info("Applying filter %s with radius %d" % (filter_method, filter_radius)) if filter_method not in [ @@ -47,16 +112,7 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): "filter_method must be one of: Median, Mean, Gaussian Blur, Minimum, Maximum" ) - if do_3d: - filter = filter_method + " 3D..." - else: - filter = filter_method + "..." - - options = ( - "sigma=" - if filter_method == "Gaussian Blur" - else "radius=" + str(filter_radius) + " stack" - ) + filter, options = filter_options(filter_method, filter_radius, do_3d=do_3d) log.debug("Filter: <%s> with options <%s>" % (filter, options)) @@ -66,69 +122,125 @@ def apply_filter(imp, filter_method, filter_radius, do_3d=False): return imageplus -def apply_rollingball_bg_subtraction(imp, rolling_ball_radius, do_3d=False): +def apply_rollingball_bg_subtraction( + imp, + rolling_ball_radius, + light_background=False, + sliding=False, + disable_smoothing=False, + do_3d=False, +): """Perform background subtraction using a rolling ball method. Parameters ---------- imp : ij.ImagePlus - Input ImagePlus to filter and threshold + Input ImagePlus to filter and threshold. rolling_ball_radius : int - Radius of the rolling ball filter to use + Radius of the rolling ball filter to use. + light_background : bool, optional + If set to True, will treat the background as light, by default False. + sliding : bool, optional + If set to True, will do a sliding window approach, by default False. + disable_smoothing : bool, optional + If set to True, will disable the smoothing, by default False. do_3d : bool, optional - If set to True, will do a 3D filtering, by default False + If set to True, will do a 3D filtering, by default False. Returns ------- ij.ImagePlus - Filtered ImagePlus + Filtered ImagePlus. """ log.info("Applying rolling ball with radius %d" % rolling_ball_radius) - options = "rolling=" + str(rolling_ball_radius) + " stack" if do_3d else "" + options = rolling_ball_options( + rolling_ball_radius, + light_background=light_background, + sliding=sliding, + disable_smoothing=disable_smoothing, + do_3d=do_3d, + ) log.debug("Background subtraction options: %s" % options) imageplus = imp.duplicate() - IJ.run(imageplus, "Substract Background...", options) + IJ.run(imageplus, "Subtract Background...", options) return imageplus +def rolling_ball_options( + rolling_ball_radius, + light_background=False, + sliding=False, + disable_smoothing=False, + do_3d=False, +): + """Generate the options for the "Subtract Background..." macro command. + + Parameters + ---------- + rolling_ball_radius : int + Radius of the rolling ball filter to use. + light_background : bool, optional + If set to True, will treat the background as light, by default False. + sliding : bool, optional + If set to True, will do a sliding window approach, by default False. + disable_smoothing : bool, optional + If set to True, will disable the smoothing, by default False. + do_3d : bool, optional + If set to True, will do a 3D filtering, by default False. + + Returns + ------- + str + The options string for the "Subtract Background..." macro command. + """ + + parts = ["rolling=" + str(rolling_ball_radius)] + if light_background: + parts.append("light") + if sliding: + parts.append("sliding") + if disable_smoothing: + parts.append("disable") + if do_3d: + parts.append("stack") + return " ".join(parts) + + def apply_threshold(imp, threshold_method, do_3d=True): """Apply a threshold method to the input ImagePlus. Parameters ---------- imp : ij.ImagePlus - Input ImagePlus to filter and threshold + Input ImagePlus to filter and threshold. threshold_method : str - Name of the threshold method to use + Name of the threshold method to use. do_3d : bool, optional - If set to True, the automatic threshold will be done on a 3D stack, by default True + If set to True, the automatic threshold will be done on a 3D stack, + by default True. Returns ------- ij.ImagePlus - Thresholded ImagePlus + Thresholded ImagePlus. """ log.info("Applying threshold method %s" % threshold_method) imageplus = imp.duplicate() - auto_threshold_options = ( - threshold_method + " " + "dark" + " " + "stack" if do_3d else "" + auto_threshold_options, convert_to_binary_options = threshold_options( + threshold_method, do_3d=do_3d ) log.debug("Auto threshold options: %s" % auto_threshold_options) IJ.setAutoThreshold(imageplus, auto_threshold_options) - convert_to_binary_options = ( - "method=" + threshold_method + " " + "background=Dark" + " " + "black" - ) - log.debug("Convert to binary options: %s" % convert_to_binary_options) IJ.run(imageplus, "Convert to Mask", convert_to_binary_options) diff --git a/tests/test_processing.py b/tests/test_processing.py new file mode 100644 index 0000000..0a4ee2b --- /dev/null +++ b/tests/test_processing.py @@ -0,0 +1,59 @@ +"""Tests for the imcflibs.imagej.processing module.""" + +from imcflibs.imagej.processing import ( + filter_options, + rolling_ball_options, + threshold_options, +) + + +def test_rolling_ball_options(): + """Test the rolling_ball_options function.""" + + options = rolling_ball_options(42.23) + assert options == "rolling=42.23" + + +def test_rolling_ball_options_with_flags(): + """Test `rolling_ball_options()` string concatenation with all flags.""" + + options = rolling_ball_options( + 12, + light_background=True, + sliding=True, + disable_smoothing=True, + do_3d=True, + ) + assert options == "rolling=12 light sliding disable stack" + + +def test_filter_options(): + """Test `filter_options()` string concatenation.""" + + command, options = filter_options("Mean", 5, do_3d=True) + assert command == "Mean 3D..." + assert options == "radius=5 stack" + + +def test_filter_options_gaussian_blur(): + """Test `filter_options()` with the Gaussian Blur branch.""" + + command, options = filter_options("Gaussian Blur", 5) + assert command == "Gaussian Blur..." + assert options == "sigma=5 stack" + + +def test_threshold_options(): + """Test `threshold_options()` string concatenation.""" + + auto_threshold, convert_to_binary = threshold_options("Otsu", do_3d=True) + assert auto_threshold == "Otsu dark stack" + assert convert_to_binary == "method=Otsu background=Dark black" + + +def test_threshold_options_without_stack(): + """Test `threshold_options()` when 3D stacking is disabled.""" + + auto_threshold, convert_to_binary = threshold_options("Otsu", do_3d=False) + assert auto_threshold == "" + assert convert_to_binary == "method=Otsu background=Dark black"