diff --git a/TESTING.md b/TESTING.md index 4aa7a25..3a663c0 100644 --- a/TESTING.md +++ b/TESTING.md @@ -22,10 +22,10 @@ test -d "venv" || python3 -m venv venv source venv/bin/activate # install dependencies / requirements: -MOCKS_REL="0.1.1" +MOCKS_REL="0.2.0" URL_PFX="https://github.com/imcf/imcf-fiji-mocks/releases/download/v$MOCKS_REL" pip install --upgrade \ - $URL_PFX/imcf_fiji_mocks-0.1.1-py2.py3-none-any.whl \ + $URL_PFX/imcf_fiji_mocks-${MOCKS_REL}-py2.py3-none-any.whl \ $URL_PFX/micrometa-15.2.2-py2.py3-none-any.whl \ $URL_PFX/sjlogging-0.5.2-py2.py3-none-any.whl \ olefile \ diff --git a/pyproject.toml b/pyproject.toml index 79bee63..d37ac7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" version = "1.5.0.a0" [tool.poetry.dependencies] -imcf-fiji-mocks = "^0.1.1" +imcf-fiji-mocks = "^0.2.0" micrometa = "^15.2.2" python = ">=2.7" sjlogging = ">=0.5.2" diff --git a/src/imcflibs/imagej/bdv.py b/src/imcflibs/imagej/bdv.py index 2154b5f..3ddd892 100644 --- a/src/imcflibs/imagej/bdv.py +++ b/src/imcflibs/imagej/bdv.py @@ -9,9 +9,9 @@ # The attribute count is not really our choice: # pylint: disable-msg=too-many-instance-attributes -import sys import os import shutil +import sys from ij import IJ @@ -19,7 +19,11 @@ from ..log import LOG as log SINGLE = "[Single %s (Select from List)]" -"""@private template string""" +"""Option to use to select only one value for the current dimension""" +MULTIPLE = "[Multiple %ss (Select from List)]" +"""Option to use to select specified multiple values for the current dimension""" +RANGE = "[Range of %ss (Specify by Name)]" +"""Option to use to select a range of values for the current dimension""" class ProcessingOptions(object): @@ -87,8 +91,8 @@ def __init__(self): self._treat_angles = "[treat individually]" self._treat_channels = "group" self._treat_illuminations = "group" - self._treat_tiles = "group" - self._treat_timepoints = "group" + self._treat_tiles = "compare" + self._treat_timepoints = "[treat individually]" ### reference-X methods @@ -177,56 +181,111 @@ def reference_timepoint(self, value): ### process-X methods - def process_angle(self, value): # def angle_select(self, value): + def process_angle(self, value, range_end=None): # def angle_select(self, value): """Select a single angle to use for processing. Parameters ---------- - value : int or int-like + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int, optional + Contains the end of the range, by default None + """ - self._angle_processing_option = SINGLE % "angle" - self._angle_select = "processing_angle=[angle %s]" % value - def process_channel(self, value): # def channel_select(self, value): - """Select a single channel to use for processing. + selection = check_processing_input(value, range_end) + processing_option, dimension_select = get_processing_settings( + "angle", selection, value, range_end + ) + + self._angle_processing_option = processing_option + self._angle_select = dimension_select + + def process_channel( + self, value, range_end=None + ): # def channel_select(self, value): + """Select the channel(s) to use for processing. Parameters ---------- - value : int or int-like + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int, optional + Contains the end of the range, by default None + """ - self._channel_processing_option = SINGLE % "channel" - # channel = int(value) - 1 - self._channel_select = "processing_channel=[channel %s]" % int(value) - def process_illumination(self, value): # def illumination_select(self, value): + selection = check_processing_input(value, range_end) + processing_option, dimension_select = get_processing_settings( + "channel", selection, value, range_end + ) + + self._channel_processing_option = processing_option + self._channel_select = dimension_select + + def process_illumination( + self, value, range_end=None + ): # def illumination_select(self, value): """Select a single illumination to use for processing. Parameters ---------- - value : int or int-like + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int, optional + Contains the end of the range, by default None + """ - self._illumination_processing_option = SINGLE % "illumination" - self._illumination_select = "processing_illumination=[illumination %s]" % value - def process_tile(self, value): # def tile_select(self, value): + selection = check_processing_input(value, range_end) + processing_option, dimension_select = get_processing_settings( + "illumination", selection, value, range_end + ) + + self._illumination_processing_option = processing_option + self._illumination_select = dimension_select + + def process_tile(self, value, range_end=None): # def tile_select(self, value): """Select a single tile to use for processing. Parameters ---------- - value : int or int-like + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int, optional + Contains the end of the range, by default None + """ - self._tile_processing_option = SINGLE % "tile" - self._tile_select = "processing_tile=[tile %s]" % value - def process_timepoint(self, value): # def timepoint_select(self, value): + selection = check_processing_input(value, range_end) + processing_option, dimension_select = get_processing_settings( + "tile", selection, value, range_end + ) + + self._tile_processing_option = processing_option + self._tile_select = dimension_select + + def process_timepoint( + self, value, range_end=None + ): # def timepoint_select(self, value): """Select a single timepoint to use for processing. Parameters ---------- - value : int or int-like + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int, optional + Contains the end of the range, by default None + """ - self._timepoint_processing_option = SINGLE % "timepoint" - self._timepoint_select = "processing_timepoint=[timepoint %s]" % value + + selection = check_processing_input(value, range_end) + processing_option, dimension_select = get_processing_settings( + "timepoint", selection, value, range_end + ) + + self._timepoint_processing_option = processing_option + self._timepoint_select = dimension_select ### treat-X methods @@ -278,7 +337,7 @@ def treat_illuminations(self, value): def treat_tiles(self, value): """Set the value for the `how_to_treat_tiles` option. - The default setting is `group`. + The default setting is `compare`. Parameters ---------- @@ -291,7 +350,7 @@ def treat_tiles(self, value): def treat_timepoints(self, value): """Set the value for the `how_to_treat_timepoints` option. - The default setting is `group`. + The default setting is `[treat individually]`. Parameters ---------- @@ -394,8 +453,13 @@ def fmt_use_acitt(self): SINGLE_FILE = "[NO (one %s)]" +"""Option to use if the current dimension is singular (like only one angle).""" MULTI_SINGLE_FILE = "[YES (all %ss in one file)]" +"""Option to use if the current dimension is plural (like multiple angles) +contained in a single file.""" MULTI_MULTI_FILE = "[YES (one file per %s)]" +"""Option to use if the current dimension is plural (like multiple angles) +contained in multiple files.""" class DefinitionOptions(object): @@ -421,7 +485,7 @@ class DefinitionOptions(object): def __init__(self): self._angle_definition = SINGLE_FILE % "angle" self._channel_definition = MULTI_SINGLE_FILE % "channel" - self._illumination_definition = SINGLE_FILE % "illumination" + self._illumination_definition = SINGLE_FILE % "illumination direction" self._tile_definition = MULTI_MULTI_FILE % "tile" self._timepoint_definition = SINGLE_FILE % "time-point" @@ -535,6 +599,86 @@ def fmt_acitt_options(self): return parameter_string + " " +def check_processing_input(value, range_end): + """Sanitize and clarifies the acitt input selection + Parameters + ---------- + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int or None + Contains the end of the range if need be + Returns + ------- + str + Returns the type of selection: single, multiple or range + """ + if type(value) is not list: + value = [value] + # Check if all the elements of the value list are of the same type + if not all(isinstance(x, type(value[0])) for x in value): + raise TypeError("Invalid input type. All the values should be of the same type") + if type(range_end) is int: + if type(value[0]) is not int: + raise TypeError("Invalid input type. Expected an int for the range start") + elif len(value) != 1: + raise ValueError( + "Invalid input type. Expected a single number for the range start" + ) + else: + return "range" + elif len(value) == 1: + return "single" + else: + return "multiple" + + +def get_processing_settings(dimension, selection, value, range_end): + """Get the processing option and dimension selection string that corresponds + to the selected processing mode + + Parameters + ---------- + dimension : str + "angle", "channel", "illumination", "tile" or "timepoint" + selection : str + "single", "multiple", or "range" + value : str, int, list of int or list of str + Contains the list of input dimensions, the first input dimension of a range or a single channel + range_end : int or None + Contains the end of the range if need be + + Returns + ------- + list of str + processing options string, dimension selection string + """ + + if selection == "single": + processing_option = SINGLE % dimension + dimension_select = "processing_" + dimension + "=[" + dimension + " %s]" % value + + if selection == "multiple": + processing_option = MULTIPLE % dimension + dimension_list = "" + for dimension_name in value: + dimension_list += dimension + "_%s " % dimension_name + dimension_select = dimension_list.rstrip() + + if selection == "range": + processing_option = RANGE % dimension + dimension_select = ( + "process_following_" + + dimension + + "s=%s-%s" + % ( + value, + range_end, + ) + ) + + return processing_option, dimension_select + + def backup_xml_files(source_directory, subfolder_name): """Create a backup of BDV-XML files inside a subfolder of `xml-backup`. @@ -613,30 +757,15 @@ def define_dataset_auto( if not dataset_save_path: dataset_save_path = pathtools.join2(result_folder, project_filename) if subsampling_factors: - subsampling_factors = "subsampling_factors=" + subsampling_factors + " " - else: subsampling_factors = ( - "manual_mipmap_setup " - "subsampling_factors=[{ " - "{1,1,1}, " - "{2,2,1}, " - "{4,4,1}, " - "{8,8,2}, " - "{16,16,4} " - "}] " + "manual_mipmap_setup subsampling_factors=" + subsampling_factors + " " ) + else: + subsampling_factors = "" if hdf5_chunk_sizes: hdf5_chunk_sizes = "hdf5_chunk_sizes=" + hdf5_chunk_sizes + " " else: - hdf5_chunk_sizes = ( - "hdf5_chunk_sizes=[{ " - "{32,32,4}, " - "{32,16,8}, " - "{16,16,16}, " - "{32,16,8}, " - "{32,32,4} " - "}] " - ) + hdf5_chunk_sizes = "" if bf_series_type == "Angles": angle_rotation = "apply_angle_rotation " @@ -661,7 +790,7 @@ def define_dataset_auto( + resave + "] " + "dataset_save_path=[" - + result_folder + + dataset_save_path + "] " + "check_stack_sizes " + angle_rotation diff --git a/src/imcflibs/imagej/bioformats.py b/src/imcflibs/imagej/bioformats.py index ca9f5f2..fcee745 100644 --- a/src/imcflibs/imagej/bioformats.py +++ b/src/imcflibs/imagej/bioformats.py @@ -14,8 +14,7 @@ from ij import IJ -from ._loci import ImporterOptions -from ._loci import BF, ImageReader, Memoizer +from ._loci import BF, ImageReader, ImporterOptions, DynamicMetadataOptions, Memoizer, ZeissCZIReader from ..pathtools import gen_name_from_orig from ..log import LOG as log diff --git a/src/imcflibs/imagej/labelimage.py b/src/imcflibs/imagej/labelimage.py index 5326f29..7735b38 100644 --- a/src/imcflibs/imagej/labelimage.py +++ b/src/imcflibs/imagej/labelimage.py @@ -70,7 +70,9 @@ def relate_label_images(label_image_ref, label_image_to_relate): ❗ NOTE: Won't work with touching labels ❗ - FIXME: explain with an example what the function is doing! + Given two label images, this function will create a new label image + with the same labels as the reference image, but with the objects + of the second image. Parameters ---------- @@ -90,7 +92,7 @@ def relate_label_images(label_image_ref, label_image_to_relate): Prefs.blackBackground = True IJ.run(imp_dup, "Convert to Mask", "") IJ.run(imp_dup, "Divide...", "value=255") - return ImageCalculator.run(label_image_ref, imp_dup, "Multimage_processorly create") + return ImageCalculator.run(label_image_ref, imp_dup, "Multiply create") def filter_objects(label_image, table, string, min_val, max_val): diff --git a/tests/bdv/test_define_dataset_auto.py b/tests/bdv/test_define_dataset_auto.py new file mode 100644 index 0000000..f1c2dd6 --- /dev/null +++ b/tests/bdv/test_define_dataset_auto.py @@ -0,0 +1,121 @@ +import logging + +from imcflibs import pathtools +from imcflibs.imagej import bdv + +def set_default_values(project_filename, file_path): + """Set the default values for dataset definitions. + + Parameters + ---------- + project_filename : str + Name of the project + file_path : pathlib.Path + Path to a temporary folder + + Returns + ---------- + str + Start of the options for dataset definitions. + """ + # Additional settings + file_info = pathtools.parse_path(file_path) + + options = ( + "define_dataset=[Automatic Loader (Bioformats based)] " + + "project_filename=[" + + project_filename + + ".xml" + + "] " + + "path=[" + + file_info["path"] + + "] " + + "exclude=10 " + + "move_tiles_to_grid_(per_angle)?=[Do not move Tiles to Grid (use Metadata if available)] " + ) + + return options + +def test_define_dataset_auto_tile(tmp_path, caplog): + """Test method for tiles.""" + + caplog.set_level(logging.WARNING) + caplog.clear() + + project_filename = "proj_name" + file_path = tmp_path + file_info = pathtools.parse_path(file_path) + + result_folder = pathtools.join2(file_info["path"], project_filename) + dataset_save_path = pathtools.join2(result_folder, project_filename) + + # Default settings + + bf_series_type = "Tiles" + + cmd = "Define dataset ..." + + options = set_default_values(project_filename, file_path) + + options = ( + options + + "how_to_load_images=[" + + "Re-save as multiresolution HDF5" + + "] " + + "dataset_save_path=[" + + dataset_save_path + + "] " + + "check_stack_sizes " + + "split_hdf5 " + + "timepoints_per_partition=1 " + + "setups_per_partition=0 " + + "use_deflate_compression " + ) + + final_call = "IJ.run(cmd=[%s], params=[%s])" % (cmd, options) + + bdv.define_dataset_auto(project_filename, file_path, bf_series_type) + assert final_call == caplog.messages[0] + + +def test_define_dataset_auto_angle(tmp_path, caplog): + """Test method for tiles.""" + + caplog.set_level(logging.WARNING) + caplog.clear() + + project_filename = "proj_name" + file_path = tmp_path + file_info = pathtools.parse_path(file_path) + + result_folder = pathtools.join2(file_info["path"], project_filename) + dataset_save_path = pathtools.join2(result_folder, project_filename) + + # Default settings + + bf_series_type = "Angles" + cmd = "Define Multi-View Dataset" + + options = set_default_values(project_filename, file_path) + + + options = ( + options + + "how_to_load_images=[" + + "Re-save as multiresolution HDF5" + + "] " + + "dataset_save_path=[" + + dataset_save_path + + "] " + + "check_stack_sizes " + + "apply_angle_rotation " + + "split_hdf5 " + + "timepoints_per_partition=1 " + + "setups_per_partition=0 " + + "use_deflate_compression " + ) + + final_call = "IJ.run(cmd=[%s], params=[%s])" % (cmd, options) + + bdv.define_dataset_auto(project_filename, file_path, bf_series_type) + assert final_call == caplog.messages[0] \ No newline at end of file diff --git a/tests/bdv/test_definitionoptions.py b/tests/bdv/test_definitionoptions.py new file mode 100644 index 0000000..d3674f6 --- /dev/null +++ b/tests/bdv/test_definitionoptions.py @@ -0,0 +1,78 @@ +import pytest + +from imcflibs.imagej.bdv import DefinitionOptions + +def test_defaults(): + """Test the default options by calling all formatters on a "raw" objects.""" + acitt_options = ( + "multiple_angles=[NO (one angle)] " + "multiple_channels=[YES (all channels in one file)] " + "multiple_illuminations=[NO (one illumination direction)] " + "multiple_tiles=[YES (one file per tile)] " + "multiple_timepoints=[NO (one time-point)] " + ) + + def_opts = DefinitionOptions() + + assert def_opts.fmt_acitt_options() == acitt_options + +def test__definition_option(): + """Test an example with wrong setting for definition option.""" + + test_value = "Multiple" + + def_opts = DefinitionOptions() + with pytest.raises(ValueError) as excinfo: + def_opts.set_angle_definition(test_value) + assert str(excinfo.value) == "Value must be one of single, multi_multi or multi_single" + +def test__multiple_timepoints_files(): + """Test an example setting how to treat multiple time-points.""" + + acitt_options = ( + "multiple_angles=[NO (one angle)] " + "multiple_channels=[YES (all channels in one file)] " + "multiple_illuminations=[NO (one illumination direction)] " + "multiple_tiles=[YES (one file per tile)] " + "multiple_timepoints=[YES (one file per time-point)] " + ) + + def_opts = DefinitionOptions() + def_opts.set_timepoint_definition("multi_multi") + + assert def_opts.fmt_acitt_options() == acitt_options + +def test__multiple_channels_files_multiple_timepoints(): + """Test an example setting how to treat multiple channels and multiple time-points.""" + + acitt_options = ( + "multiple_angles=[NO (one angle)] " + "multiple_channels=[YES (one file per channel)] " + "multiple_illuminations=[NO (one illumination direction)] " + "multiple_tiles=[YES (one file per tile)] " + "multiple_timepoints=[YES (all time-points in one file)] " + ) + + def_opts = DefinitionOptions() + def_opts.set_channel_definition("multi_multi") + def_opts.set_timepoint_definition("multi_single") + + assert def_opts.fmt_acitt_options() == acitt_options + +def test_single_tile_multiple_angles_files(): + """Test an example setting how to treat single tile and multiple angle + files""" + + acitt_options = ( + "multiple_angles=[YES (one file per angle)] " + "multiple_channels=[YES (all channels in one file)] " + "multiple_illuminations=[NO (one illumination direction)] " + "multiple_tiles=[NO (one tile)] " + "multiple_timepoints=[NO (one time-point)] " + ) + + def_opts = DefinitionOptions() + def_opts.set_angle_definition("multi_multi") + def_opts.set_tile_definition("single") + + assert def_opts.fmt_acitt_options() == acitt_options diff --git a/tests/bdv/test_processingoptions.py b/tests/bdv/test_processingoptions.py index 0e97529..6c87dac 100644 --- a/tests/bdv/test_processingoptions.py +++ b/tests/bdv/test_processingoptions.py @@ -1,10 +1,8 @@ -import pytest - from imcflibs.imagej.bdv import ProcessingOptions def test_defaults(): - """Test the default options by calling all fomatters on a "raw" object.""" + """Test the default options by calling all formatters on a "raw" object.""" acitt_options = ( "process_angle=[All angles] " "process_channel=[All channels] " @@ -17,15 +15,10 @@ def test_defaults(): "how_to_treat_angles=[treat individually] " "how_to_treat_channels=group " "how_to_treat_illuminations=group " - "how_to_treat_tiles=group " - "how_to_treat_timepoints=group " - ) - use_acitt = ( - "channels=[Average Channels] " - "illuminations=[Average Illuminations] " - "tiles=[Average Tiles] " - "timepoints=[Average Timepoints] " + "how_to_treat_tiles=compare " + "how_to_treat_timepoints=[treat individually] " ) + use_acitt = "channels=[Average Channels] " "illuminations=[Average Illuminations] " proc_opts = ProcessingOptions() @@ -54,10 +47,7 @@ def test__treat_tc_ti__ref_c1(): "how_to_treat_tiles=compare " "how_to_treat_timepoints=[treat individually] " ) - use_acitt = ( - "channels=[use Channel 1] " - "illuminations=[Average Illuminations] " - ) + use_acitt = "channels=[use Channel 1] " "illuminations=[Average Illuminations] " proc_opts = ProcessingOptions() proc_opts.treat_tiles("compare") @@ -69,6 +59,37 @@ def test__treat_tc_ti__ref_c1(): assert proc_opts.fmt_use_acitt() == use_acitt assert proc_opts.fmt_how_to_treat() == how_to_treat + +def test__process_c1_treat_tg_ti_use_t3(): + + acitt_options = ( + "process_angle=[All angles] " + "process_channel=[Single channel (Select from List)] " + "process_illumination=[All illuminations] " + "process_tile=[All tiles] " + "process_timepoint=[All Timepoints] " + ) + + acitt_selectors = "processing_channel=[channel 1] " + how_to_treat = ( + "how_to_treat_angles=[treat individually] " + "how_to_treat_channels=group " + "how_to_treat_illuminations=group " + "how_to_treat_tiles=group " + "how_to_treat_timepoints=[treat individually] " + ) + use_acitt = ( + "channels=[Average Channels] " + "illuminations=[Average Illuminations] " + "tiles=[use Tile 3] " + ) + + proc_opts = ProcessingOptions() + proc_opts.process_channel(1) + # proc_opts.treat_timepoints("[treat individually]") + proc_opts.treat_tiles("group") + proc_opts.reference_tile(3) + assert proc_opts.fmt_acitt_options() == acitt_options assert proc_opts.fmt_acitt_selectors() == acitt_selectors assert proc_opts.fmt_use_acitt() == use_acitt diff --git a/tests/bdv/test_processingoptions_example3.py b/tests/bdv/test_processingoptions_example3.py new file mode 100644 index 0000000..2b5c28b --- /dev/null +++ b/tests/bdv/test_processingoptions_example3.py @@ -0,0 +1,31 @@ + +from imcflibs.imagej.bdv import ProcessingOptions + + +def test__process_c1c2_treat_tc_ti(): + + acitt_options = ( + "process_angle=[All angles] " + "process_channel=[Multiple channels (Select from List)] " + "process_illumination=[All illuminations] " + "process_tile=[All tiles] " + "process_timepoint=[All Timepoints] " + ) + + acitt_selectors = "channel_1 channel_2 " + how_to_treat = ( + "how_to_treat_angles=[treat individually] " + "how_to_treat_channels=group " + "how_to_treat_illuminations=group " + "how_to_treat_tiles=compare " + "how_to_treat_timepoints=[treat individually] " + ) + use_acitt = "channels=[Average Channels] illuminations=[Average Illuminations] " + + proc_opts = ProcessingOptions() + proc_opts.process_channel([1, 2]) + + assert proc_opts.fmt_acitt_options() == acitt_options + assert proc_opts.fmt_acitt_selectors() == acitt_selectors + assert proc_opts.fmt_use_acitt() == use_acitt + assert proc_opts.fmt_how_to_treat() == how_to_treat diff --git a/tests/bdv/test_processingoptions_example4.py b/tests/bdv/test_processingoptions_example4.py new file mode 100644 index 0000000..9219461 --- /dev/null +++ b/tests/bdv/test_processingoptions_example4.py @@ -0,0 +1,31 @@ +import pytest + +from imcflibs.imagej.bdv import ProcessingOptions + + +def test__process_c1c3(): + + acitt_options = ( + "process_angle=[All angles] " + "process_channel=[Range of channels (Specify by Name)] " + "process_illumination=[All illuminations] " + "process_tile=[All tiles] " + "process_timepoint=[All Timepoints] " + ) + acitt_selectors = "process_following_channels=1-3 " + how_to_treat = ( + "how_to_treat_angles=[treat individually] " + "how_to_treat_channels=group " + "how_to_treat_illuminations=group " + "how_to_treat_tiles=compare " + "how_to_treat_timepoints=[treat individually] " + ) + use_acitt = "channels=[Average Channels] illuminations=[Average Illuminations] " + + proc_opts = ProcessingOptions() + proc_opts.process_channel(1, 3) + + assert proc_opts.fmt_acitt_options() == acitt_options + assert proc_opts.fmt_acitt_selectors() == acitt_selectors + assert proc_opts.fmt_use_acitt() == use_acitt + assert proc_opts.fmt_how_to_treat() == how_to_treat