From ce7465fd7b0d3a6bad8ed75993afa773292ab13d Mon Sep 17 00:00:00 2001
From: Laurent Guerard <laurent.guerard@unibas.ch>
Date: Tue, 29 Apr 2025 14:49:20 +0200
Subject: [PATCH] Refactor script 1 to use `imcflibs`

---
 1_identify_fibers.py | 774 +++++++++++++------------------------------
 1 file changed, 227 insertions(+), 547 deletions(-)

diff --git a/1_identify_fibers.py b/1_identify_fibers.py
index 5b02125..feb18d6 100755
--- a/1_identify_fibers.py
+++ b/1_identify_fibers.py
@@ -34,110 +34,21 @@
 
 # ─── Imports ──────────────────────────────────────────────────────────────────
 
-# IJ imports
-# TODO: are the imports RoiManager and ResultsTable needed when using the services?
 import os
-import sys
-
-# Bio-formats imports
-# from loci.plugins import BF
-# from loci.plugins.in import ImporterOptions
-# python imports
 import time
 
 from ch.epfl.biop.ij2command import Labels2Rois
-
-# TrackMate imports
-from fiji.plugin.trackmate import Logger, Model, Settings, TrackMate
-from fiji.plugin.trackmate.action import LabelImgExporter
-from fiji.plugin.trackmate.cellpose import CellposeDetectorFactory
-from fiji.plugin.trackmate.cellpose.CellposeSettings import PretrainedModel
-from fiji.plugin.trackmate.features import FeatureFilter
-from fiji.plugin.trackmate.providers import (
-    SpotAnalyzerProvider,
-    SpotMorphologyAnalyzerProvider,
-)
-from fiji.plugin.trackmate.tracking.jaqaman import SparseLAPTrackerFactory
 from ij import IJ
 from ij import WindowManager as wm
 from ij.measure import ResultsTable
 from ij.plugin import Duplicator, ImageCalculator
 from imcflibs import pathtools
 from imcflibs.imagej import bioformats as bf
-from imcflibs.imagej import misc
-from loci.formats import ImageReader, MetadataTools
+from imcflibs.imagej import misc, resultstable, roimanager, trackmate
 
 # ─── Functions ────────────────────────────────────────────────────────────────
 
 
-def fix_ij_options():
-    """Put IJ into a defined state."""
-    # disable inverting LUT
-    IJ.run("Appearance...", " menu=0 16-bit=Automatic")
-    # set foreground color to be white, background black
-    IJ.run("Colors...", "foreground=white background=black selection=red")
-    # black BG for binary images and pad edges when eroding
-    IJ.run("Options...", "black pad")
-    # set saving format to .txt files
-    IJ.run("Input/Output...", "file=.txt save_column save_row")
-    # ============= DON’T MOVE UPWARDS =============
-    # set "Black Background" in "Binary Options"
-    IJ.run("Options...", "black")
-    # scale when converting = checked
-    IJ.run("Conversions...", "scale")
-
-
-def fix_ij_dirs(path):
-    """use forward slashes in directory paths.
-
-    Parameters
-    ----------
-    path : string
-        a directory path obtained from dialogue or script parameter
-
-    Returns
-    -------
-    string
-        a more robust path with forward slashes as separators
-    """
-
-    fixed_path = str(path).replace("\\", "/")
-    # fixed_path = fixed_path + "/"
-
-    return fixed_path
-
-
-def fix_BF_czi_imagetitle(imp):
-    """Fix the title of an image read using the bio-formats importer.
-
-    The title is modified to remove the ".czi" extension and replace
-    spaces with underscores.
-
-    Parameters
-    ----------
-    imp : ij.ImagePlus
-        The image to be processed.
-
-    Returns
-    -------
-    string
-        The modified title of the image.
-    """
-    image_title = os.path.basename(imp.getShortTitle())
-    # remove the ".czi" extension
-    image_title = image_title.replace(".czi", "")
-    # replace spaces with underscores
-    image_title = image_title.replace(" ", "_")
-    # remove any double underscores
-    image_title = image_title.replace("_-_", "")
-    # remove any double underscores
-    image_title = image_title.replace("__", "_")
-    # remove any "#" characters
-    image_title = image_title.replace("#", "Series")
-
-    return image_title
-
-
 def do_background_correction(imp, gaussian_radius=20):
     """Perform background correction on an image.
 
@@ -194,186 +105,186 @@ def get_threshold_from_method(imp, channel, method):
     return lower_thr, upper_thr
 
 
-def run_tm(
-    implus,
-    channel_seg,
-    cellpose_env,
-    seg_model,
-    diam_seg,
-    channel_sec=0,
-    quality_thresh=[0, 0],
-    intensity_thresh=[0, 0],
-    circularity_thresh=[0, 0],
-    perimeter_thresh=[0, 0],
-    area_thresh=[0, 0],
-    crop_roi=None,
-    use_gpu=True,
-):
-    """
-    Function to run TrackMate on open data, applying filters to spots.
-
-    Parameters
-    ----------
-    implus : ij.ImagePlus
-        ImagePlus on which to run the function
-    channel_seg : int
-        Channel of interest
-    cellpose_env : str
-        Path to the cellpose environment
-    seg_model : PretrainedModel
-        Model to use for the segmentation
-    diam_seg : float
-        Diameter to use for segmentation
-    channel_sec : int, optional
-        Secondary channel to use for segmentation, by default 0
-    quality_thresh : float, optional
-        Threshold for quality filtering, by default None
-    intensity_thresh : float, optional
-        Threshold for intensity filtering, by default None
-    circularity_thresh : float, optional
-        Threshold for circularity filtering, by default None
-    perimeter_thresh : float, optional
-        Threshold for perimeter filtering, by default None
-    area_thresh : float, optional
-        Threshold for area filtering, by default None
-    crop_roi : ROI, optional
-        ROI to crop on the image, by default None
-    use_gpu : bool, optional
-        Boolean to use GPU or not, by default True
-
-    Returns
-    -------
-    ij.ImagePlus
-        Label image with the segmented objects
-    """
-
-    # Get image dimensions and calibration
-    dims = implus.getDimensions()
-    cal = implus.getCalibration()
-
-    # If the image has more than one slice, adjust the dimensions
-    if implus.getNSlices() > 1:
-        implus.setDimensions(dims[2], dims[4], dims[3])
-
-    # Set ROI if provided
-    if crop_roi is not None:
-        implus.setRoi(crop_roi)
-
-    # Initialize TrackMate model
-    model = Model()
-    model.setLogger(Logger.IJTOOLBAR_LOGGER)
-
-    # Prepare settings for TrackMate
-    settings = Settings(implus)
-    settings.detectorFactory = CellposeDetectorFactory()
-
-    # Configure detector settings
-    settings.detectorSettings["TARGET_CHANNEL"] = channel_seg
-    settings.detectorSettings["OPTIONAL_CHANNEL_2"] = channel_sec
-    settings.detectorSettings["CELLPOSE_PYTHON_FILEPATH"] = os.path.join(
-        cellpose_env, "python.exe"
-    )
-    settings.detectorSettings["CELLPOSE_MODEL_FILEPATH"] = os.path.join(
-        os.environ["USERPROFILE"], ".cellpose", "models"
-    )
-    settings.detectorSettings["CELLPOSE_MODEL"] = seg_model
-    settings.detectorSettings["CELL_DIAMETER"] = diam_seg
-    settings.detectorSettings["USE_GPU"] = use_gpu
-    settings.detectorSettings["SIMPLIFY_CONTOURS"] = True
-
-    settings.initialSpotFilterValue = -1.0
-
-    # Add spot analyzers
-    spotAnalyzerProvider = SpotAnalyzerProvider(1)
-    spotMorphologyProvider = SpotMorphologyAnalyzerProvider(1)
-
-    for key in spotAnalyzerProvider.getKeys():
-        settings.addSpotAnalyzerFactory(spotAnalyzerProvider.getFactory(key))
-
-    for key in spotMorphologyProvider.getKeys():
-        settings.addSpotAnalyzerFactory(spotMorphologyProvider.getFactory(key))
-
-    # Apply spot filters based on thresholds
-    if any(quality_thresh):
-        settings = set_trackmate_filter(settings, "QUALITY", quality_thresh)
-    if any(intensity_thresh):
-        settings = set_trackmate_filter(
-            settings, "MEAN_INTENSITY_CH" + str(channel_seg), intensity_thresh
-        )
-    if any(circularity_thresh):
-        settings = set_trackmate_filter(settings, "CIRCULARITY", circularity_thresh)
-    if any(area_thresh):
-        settings = set_trackmate_filter(settings, "AREA", area_thresh)
-    if any(perimeter_thresh):
-        settings = set_trackmate_filter(settings, "PERIMETER", perimeter_thresh)
-
-    # print(settings)
-
-    # Configure tracker
-    settings.trackerFactory = SparseLAPTrackerFactory()
-    settings.trackerSettings = settings.trackerFactory.getDefaultSettings()
-    # settings.addTrackAnalyzer(TrackDurationAnalyzer())
-    settings.trackerSettings["LINKING_MAX_DISTANCE"] = 3.0
-    settings.trackerSettings["GAP_CLOSING_MAX_DISTANCE"] = 3.0
-    settings.trackerSettings["MAX_FRAME_GAP"] = 2
-
-    # Initialize TrackMate with model and settings
-    trackmate = TrackMate(model, settings)
-    trackmate.computeSpotFeatures(True)
-    trackmate.computeTrackFeatures(False)
-
-    # Check input validity
-    if not trackmate.checkInput():
-        sys.exit(str(trackmate.getErrorMessage()))
-        return
-
-    # Process the data
-    if not trackmate.process():
-        if "[SparseLAPTracker] The spot collection is empty." in str(
-            trackmate.getErrorMessage()
-        ):
-            return IJ.createImage(
-                "Untitled",
-                "8-bit black",
-                implus.getWidth(),
-                implus.getHeight(),
-                implus.getNFrames(),
-            )
-        else:
-            sys.exit(str(trackmate.getErrorMessage()))
-            return
-
-    # Export the label image
-    # sm = SelectionModel(model)
-    exportSpotsAsDots = False
-    exportTracksOnly = False
-    label_imp = LabelImgExporter.createLabelImagePlus(
-        trackmate, exportSpotsAsDots, exportTracksOnly, False
-    )
-    label_imp.setDimensions(1, dims[3], dims[4])
-    label_imp.setCalibration(cal)
-    implus.setDimensions(dims[2], dims[3], dims[4])
-    return label_imp
-
-
-def set_trackmate_filter(settings, filter_name, filter_value):
-    """Sets a TrackMate spot filter with specified filter name and values.
-
-    Parameters
-    ----------
-    settings : Settings
-        TrackMate settings object to which the filter will be added.
-    filter_name : str
-        The name of the filter to be applied.
-    filter_value : list
-        A list containing two values for the filter. The first value is
-        applied as an above-threshold filter, and the second as a below-threshold filter.
-    """
-    filter = FeatureFilter(filter_name, filter_value[0], True)
-    settings.addSpotFilter(filter)
-    filter = FeatureFilter(filter_name, filter_value[1], False)
-    settings.addSpotFilter(filter)
-    return settings
+# def run_tm(
+#     implus,
+#     channel_seg,
+#     cellpose_env,
+#     seg_model,
+#     diam_seg,
+#     channel_sec=0,
+#     quality_thresh=[0, 0],
+#     intensity_thresh=[0, 0],
+#     circularity_thresh=[0, 0],
+#     perimeter_thresh=[0, 0],
+#     area_thresh=[0, 0],
+#     crop_roi=None,
+#     use_gpu=True,
+# ):
+#     """
+#     Function to run TrackMate on open data, applying filters to spots.
+
+#     Parameters
+#     ----------
+#     implus : ij.ImagePlus
+#         ImagePlus on which to run the function
+#     channel_seg : int
+#         Channel of interest
+#     cellpose_env : str
+#         Path to the cellpose environment
+#     seg_model : PretrainedModel
+#         Model to use for the segmentation
+#     diam_seg : float
+#         Diameter to use for segmentation
+#     channel_sec : int, optional
+#         Secondary channel to use for segmentation, by default 0
+#     quality_thresh : float, optional
+#         Threshold for quality filtering, by default None
+#     intensity_thresh : float, optional
+#         Threshold for intensity filtering, by default None
+#     circularity_thresh : float, optional
+#         Threshold for circularity filtering, by default None
+#     perimeter_thresh : float, optional
+#         Threshold for perimeter filtering, by default None
+#     area_thresh : float, optional
+#         Threshold for area filtering, by default None
+#     crop_roi : ROI, optional
+#         ROI to crop on the image, by default None
+#     use_gpu : bool, optional
+#         Boolean to use GPU or not, by default True
+
+#     Returns
+#     -------
+#     ij.ImagePlus
+#         Label image with the segmented objects
+#     """
+
+#     # Get image dimensions and calibration
+#     dims = implus.getDimensions()
+#     cal = implus.getCalibration()
+
+#     # If the image has more than one slice, adjust the dimensions
+#     if implus.getNSlices() > 1:
+#         implus.setDimensions(dims[2], dims[4], dims[3])
+
+#     # Set ROI if provided
+#     if crop_roi is not None:
+#         implus.setRoi(crop_roi)
+
+#     # Initialize TrackMate model
+#     model = Model()
+#     model.setLogger(Logger.IJTOOLBAR_LOGGER)
+
+#     # Prepare settings for TrackMate
+#     settings = Settings(implus)
+#     settings.detectorFactory = CellposeDetectorFactory()
+
+#     # Configure detector settings
+#     settings.detectorSettings["TARGET_CHANNEL"] = channel_seg
+#     settings.detectorSettings["OPTIONAL_CHANNEL_2"] = channel_sec
+#     settings.detectorSettings["CELLPOSE_PYTHON_FILEPATH"] = os.path.join(
+#         cellpose_env, "python.exe"
+#     )
+#     settings.detectorSettings["CELLPOSE_MODEL_FILEPATH"] = os.path.join(
+#         os.environ["USERPROFILE"], ".cellpose", "models"
+#     )
+#     settings.detectorSettings["CELLPOSE_MODEL"] = seg_model
+#     settings.detectorSettings["CELL_DIAMETER"] = diam_seg
+#     settings.detectorSettings["USE_GPU"] = use_gpu
+#     settings.detectorSettings["SIMPLIFY_CONTOURS"] = True
+
+#     settings.initialSpotFilterValue = -1.0
+
+#     # Add spot analyzers
+#     spotAnalyzerProvider = SpotAnalyzerProvider(1)
+#     spotMorphologyProvider = SpotMorphologyAnalyzerProvider(1)
+
+#     for key in spotAnalyzerProvider.getKeys():
+#         settings.addSpotAnalyzerFactory(spotAnalyzerProvider.getFactory(key))
+
+#     for key in spotMorphologyProvider.getKeys():
+#         settings.addSpotAnalyzerFactory(spotMorphologyProvider.getFactory(key))
+
+#     # Apply spot filters based on thresholds
+#     if any(quality_thresh):
+#         settings = set_trackmate_filter(settings, "QUALITY", quality_thresh)
+#     if any(intensity_thresh):
+#         settings = set_trackmate_filter(
+#             settings, "MEAN_INTENSITY_CH" + str(channel_seg), intensity_thresh
+#         )
+#     if any(circularity_thresh):
+#         settings = set_trackmate_filter(settings, "CIRCULARITY", circularity_thresh)
+#     if any(area_thresh):
+#         settings = set_trackmate_filter(settings, "AREA", area_thresh)
+#     if any(perimeter_thresh):
+#         settings = set_trackmate_filter(settings, "PERIMETER", perimeter_thresh)
+
+#     # print(settings)
+
+#     # Configure tracker
+#     settings.trackerFactory = SparseLAPTrackerFactory()
+#     settings.trackerSettings = settings.trackerFactory.getDefaultSettings()
+#     # settings.addTrackAnalyzer(TrackDurationAnalyzer())
+#     settings.trackerSettings["LINKING_MAX_DISTANCE"] = 3.0
+#     settings.trackerSettings["GAP_CLOSING_MAX_DISTANCE"] = 3.0
+#     settings.trackerSettings["MAX_FRAME_GAP"] = 2
+
+#     # Initialize TrackMate with model and settings
+#     trackmate = TrackMate(model, settings)
+#     trackmate.computeSpotFeatures(True)
+#     trackmate.computeTrackFeatures(False)
+
+#     # Check input validity
+#     if not trackmate.checkInput():
+#         sys.exit(str(trackmate.getErrorMessage()))
+#         return
+
+#     # Process the data
+#     if not trackmate.process():
+#         if "[SparseLAPTracker] The spot collection is empty." in str(
+#             trackmate.getErrorMessage()
+#         ):
+#             return IJ.createImage(
+#                 "Untitled",
+#                 "8-bit black",
+#                 implus.getWidth(),
+#                 implus.getHeight(),
+#                 implus.getNFrames(),
+#             )
+#         else:
+#             sys.exit(str(trackmate.getErrorMessage()))
+#             return
+
+#     # Export the label image
+#     # sm = SelectionModel(model)
+#     exportSpotsAsDots = False
+#     exportTracksOnly = False
+#     label_imp = LabelImgExporter.createLabelImagePlus(
+#         trackmate, exportSpotsAsDots, exportTracksOnly, False
+#     )
+#     label_imp.setDimensions(1, dims[3], dims[4])
+#     label_imp.setCalibration(cal)
+#     implus.setDimensions(dims[2], dims[3], dims[4])
+#     return label_imp
+
+
+# def set_trackmate_filter(settings, filter_name, filter_value):
+#     """Sets a TrackMate spot filter with specified filter name and values.
+
+#     Parameters
+#     ----------
+#     settings : Settings
+#         TrackMate settings object to which the filter will be added.
+#     filter_name : str
+#         The name of the filter to be applied.
+#     filter_value : list
+#         A list containing two values for the filter. The first value is
+#         applied as an above-threshold filter, and the second as a below-threshold filter.
+#     """
+#     filter = FeatureFilter(filter_name, filter_value[0], True)
+#     settings.addSpotFilter(filter)
+#     filter = FeatureFilter(filter_name, filter_value[1], False)
+#     settings.addSpotFilter(filter)
+#     return settings
 
 
 def delete_channel(imp, channel_number):
@@ -390,178 +301,6 @@ def delete_channel(imp, channel_number):
     IJ.run(imp, "Delete Slice", "delete=channel")
 
 
-def measure_in_all_rois(imp, channel, rm):
-    """Gives measurements for all ROIs in ROIManager.
-
-    Measures in all ROIS on a given channel of imp all parameters that are set in IJ "Set Measurements".
-
-    Parameters
-    ----------
-    imp : ij.ImagePlus
-        the imp to measure on
-    channel : integer
-        the channel to measure in. starts at 1.
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    """
-    imp.setC(channel)
-    rm.runCommand(imp, "Deselect")
-    rm.runCommand(imp, "Measure")
-
-
-def change_all_roi_color(rm, color):
-    """Cchange the color of all ROIs in the RoiManager.
-
-    Parameters
-    ----------
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    color : string
-        the desired color. e.g. "green", "red", "yellow", "magenta" ...
-    """
-    number_of_rois = rm.getCount()
-    for roi in range(number_of_rois):
-        rm.select(roi)
-        rm.runCommand("Set Color", color)
-
-
-def change_subset_roi_color(rm, selected_rois, color):
-    """Change the color of selected ROIs in the RoiManager.
-
-    Parameters
-    ----------
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    selected_rois : array
-        ROIs in the RoiManager to change
-    color : string
-        the desired color. e.g. "green", "red", "yellow", "magenta" ...
-    """
-    rm.runCommand("Deselect")
-    rm.setSelectedIndexes(selected_rois)
-    rm.runCommand("Set Color", color)
-    rm.runCommand("Deselect")
-
-
-def show_all_rois_on_image(rm, imp):
-    """Shows all ROIs in the ROiManager on imp.
-
-    Parameters
-    ----------
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    imp : ij.ImagePlus
-        the imp on which to show the ROIs
-    """
-    imp.show()
-    rm.runCommand(imp, "Show All")
-
-
-def save_all_rois(rm, target):
-    """Save all ROIs in the RoiManager as zip to target path.
-
-    Parameters
-    ----------
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    target : string
-        the path in to store the ROIs. e.g. /my-images/resulting_rois.zip
-    """
-    rm.runCommand("Save", target)
-
-
-def save_selected_rois(rm, selected_rois, target):
-    """Save selected ROIs in the RoiManager as zip to target path.
-
-    Parameters
-    ----------
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    selected_rois : array
-        ROIs in the RoiManager to save
-    target : string
-        the path in to store the ROIs. e.g. /my-images/resulting_rois_subset.zip
-    """
-    rm.runCommand("Deselect")
-    rm.setSelectedIndexes(selected_rois)
-    rm.runCommand("save selected", target)
-    rm.runCommand("Deselect")
-
-
-def select_positive_fibers(imp, channel, rm, min_intensity):
-    """Select ROIs in ROIManager based on intensity in specific channel.
-
-    For all ROIs in the RoiManager, select ROIs based on intensity measurement in given channel of imp.
-    See https://imagej.nih.gov/ij/developer/api/ij/process/ImageStatistics.html
-
-    Parameters
-    ----------
-    imp : ij.ImagePlus
-        the imp on which to measure
-    channel : integer
-        the channel on which to measure. starts at 1
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    min_intensity : integer
-        the selection criterion (here: intensity threshold)
-
-    Returns
-    -------
-    array
-        a selection of ROIs which passed the selection criterion (are above the threshold)
-    """
-    imp.setC(channel)
-    all_rois = rm.getRoisAsArray()
-    selected_rois = []
-    for i, roi in enumerate(all_rois):
-        imp.setRoi(roi)
-        stats = imp.getStatistics()
-        if stats.mean > min_intensity:
-            selected_rois.append(i)
-
-    return selected_rois
-
-
-def preset_results_column(rt, column, value):
-    """Pre-set values in selected column from the ResultsTable.
-
-    Pre-set all rows in given column of the IJ-ResultsTable with desired value.
-
-    Parameters
-    ----------
-    rt : ResultsTable
-        a reference of the IJ-ResultsTable
-    column : string
-        the desired column. will be created if it does not yet exist
-    value : string or float or integer
-        the value to be set
-    """
-    for i in range(rt.size()):
-        rt.setValue(column, i, value)
-
-    rt.show("Results")
-
-
-def add_results(rt, column, row, value):
-    """Adds a value in desired rows of a given column.
-
-    Parameters
-    ----------
-    rt : ResultsTable
-        a reference of the IJ-ResultsTable
-    column : string
-        the column in which to add the values
-    row : array
-        the row numbers in which too add the values.
-    value : string or float or integer
-        the value to be set
-    """
-    for i in range(len(row)):
-        rt.setValue(column, row[i], value)
-
-    rt.show("Results")
-
-
 def enhance_contrast(imp):
     """Use "Auto" Contrast & Brightness settings in each channel of imp.
 
@@ -575,19 +314,6 @@ def enhance_contrast(imp):
         IJ.run(imp, "Enhance Contrast", "saturated=0.35")
 
 
-def renumber_rois(rm):
-    """Rename all ROIs in the RoiManager according to their number.
-
-    Parameters
-    ----------
-    rm : RoiManager
-        a reference of the IJ-RoiManager
-    """
-    number_of_rois = rm.getCount()
-    for roi in range(number_of_rois):
-        rm.rename(roi, str(roi + 1))
-
-
 def setup_defined_ij(rm, rt):
     """Set up a clean and defined Fiji user environment.
 
@@ -598,68 +324,12 @@ def setup_defined_ij(rm, rt):
     rt : ResultsTable
         a reference of the IJ-ResultsTable
     """
-    fix_ij_options()
-    rm.runCommand("reset")
+    prefs.set_default_ij_options()
+    roimanager.clear_ij_roi_manager(rm)
     rt.reset()
     IJ.log("\\Clear")
 
 
-def get_series_info_from_ome_metadata(path_to_file, skip_labels=False):
-    """Get the Bio-Formates series count from a file on disk.
-
-    Useful to access a specific image in a container format like .czi, .nd2, .lif...
-
-    Parameters
-    ----------
-    path_to_file : str
-        The full path to the image file.
-
-    Returns
-    -------
-    int
-        The number of Bio-Formats series detected in the image file metadata.
-    """
-
-    if not skip_labels:
-        reader = ImageReader()
-        reader.setFlattenedResolutions(False)
-        ome_meta = MetadataTools.createOMEXMLMetadata()
-        reader.setMetadataStore(ome_meta)
-        reader.setId(path_to_file)
-        series_count = reader.getSeriesCount()
-
-        reader.close()
-        return series_count, range(series_count)
-
-    else:
-        reader = ImageReader()
-        # reader.setFlattenedResolutions(True)
-        ome_meta = MetadataTools.createOMEXMLMetadata()
-        reader.setMetadataStore(ome_meta)
-        reader.setId(path_to_file)
-        series_count = reader.getSeriesCount()
-
-        series_ids = []
-        series_names = []
-        x = 0
-        y = 0
-        for i in range(series_count):
-            reader.setSeries(i)
-
-            if reader.getSizeX() > x and reader.getSizeY() > y:
-                name = ome_meta.getImageName(i)
-
-                if name not in ["label image", "macro image"]:
-                    series_ids.append(i)
-                    series_names.append(name)
-
-            x = reader.getSizeX()
-            y = reader.getSizeY()
-
-        print(series_names)
-        return len(series_ids), series_ids
-
-
 # ─── Main Code ────────────────────────────────────────────────────────────────
 
 if __name__ == "__main__":
@@ -668,6 +338,9 @@ if __name__ == "__main__":
     misc.timed_log("Script starting")
     setup_defined_ij(rm, rt)
 
+    if not output_folder:
+        output_folder = src_dir.getPath()
+
     file_list = pathtools.listdir_matching(
         src_dir.getPath(), filename_filter, fullpath=True
     )
@@ -679,7 +352,7 @@ if __name__ == "__main__":
         file_info = pathtools.parse_path(file)
         misc.progressbar(index + 1, len(file_list), 1, "Opening : ")
 
-        _, series_index = get_series_info_from_ome_metadata(
+        _, series_index = bf.get_series_info_from_ome_metadata(
             file_info["full"], skip_labels=True
         )
         print(series_index)
@@ -689,7 +362,7 @@ if __name__ == "__main__":
 
             # get image info
             raw_image_calibration = raw.getCalibration()
-            raw_image_title = fix_BF_czi_imagetitle(raw)
+            raw_image_title = misc.sanitize_image_title(raw)
             print("raw image title: ", str(raw_image_title))
 
             # take care of paths and directories
@@ -698,8 +371,7 @@ if __name__ == "__main__":
             )
             print("output_dir: ", str(output_dir))
 
-            if not os.path.exists(str(output_dir)):
-                os.makedirs(str(output_dir))
+            pathtools.create_directory(output_dir)
 
             # update the log for the user
             misc.timed_log("Now working on " + str(raw_image_title))
@@ -739,16 +411,21 @@ if __name__ == "__main__":
             IJ.run(imp_bgd_corrected, "16-bit", "")
 
             misc.timed_log("Running the segmentation")
-            imp_result = run_tm(
-                imp_bgd_corrected,
-                1,
-                cellpose_dir.getPath(),
-                PretrainedModel.CYTO2,
-                30.0,
-                area_thresh=[minAr, maxAr],
-                circularity_thresh=[minCir, maxCir],
-                perimeter_thresh=[minPer, maxPer],
-            )
+            trackmate_settings = trackmate.cellpose_detector(imp_bgd_corrected, cellpose_dir.getPath(), "cyto2", 30.0, 1)
+            trackmate_settings = trackmate.spot_filtering(trackmate_settings, area_thresh = minAr, circularity_thresh = minCir)
+            trackmate_settings = trackmate.spot_filtering(trackmate_settings, area_thresh = -maxAr, circularity_thresh = -maxCir)
+            imp_result = trackmate.run_trackmate(imp_bgd_corrected, trackmate_settings)
+
+            # imp_result = run_tm(
+            #     imp_bgd_corrected,
+            #     1,
+            #     cellpose_dir.getPath(),
+            #     PretrainedModel.CYTO2,
+            #     30.0,
+            #     area_thresh=[minAr, maxAr],
+            #     circularity_thresh=[minCir, maxCir],
+            #     perimeter_thresh=[minPer, maxPer],
+            # )
 
             misc.timed_log("Resizing the segmentation results (if needed)")
             imp_result = imp_result.resize(
@@ -770,10 +447,10 @@ if __name__ == "__main__":
             misc.timed_log("Getting the ROIs")
             command.run(Labels2Rois, True, "rm", rm, "imp", imp_result).get()
 
-            renumber_rois(rm)
+            roimanager.rename_rois_by_number(rm)
 
             misc.timed_log("Saving the ROIs")
-            save_all_rois(
+            roimanager.save_rois_to_zip(
                 rm,
                 os.path.join(
                     output_dir,
@@ -791,15 +468,14 @@ if __name__ == "__main__":
                     misc.timed_log("automatic intensity threshold detection: True")
 
                 IJ.log("fiber intensity threshold: " + str(min_fiber_intensity))
-                change_all_roi_color(rm, "blue")
-                positive_fibers = select_positive_fibers(
+                roimanager.change_roi_color(rm, "blue")
+                positive_fibers = roimanager.select_rois_above_min_intensity(
                     raw, fiber_channel, rm, min_fiber_intensity
                 )
-                change_subset_roi_color(rm, positive_fibers, "magenta")
+                roimanager.change_roi_color(rm, "magenta", positive_fibers)
                 misc.timed_log("Saving positive fibers")
-                save_selected_rois(
+                roimanager.save_rois_to_zip(
                     rm,
-                    positive_fibers,
                     os.path.join(
                         output_dir,
                         raw_image_title
@@ -807,6 +483,7 @@ if __name__ == "__main__":
                         + str(serie_index)
                         + "_mhc_positive_fiber_rois.zip",
                     ),
+                    positive_fibers,
                 )
 
             # measure size & shape, save
@@ -816,7 +493,7 @@ if __name__ == "__main__":
                 "area perimeter shape feret's redirect=None decimal=4",
             )
             IJ.run("Clear Results", "")
-            measure_in_all_rois(raw, membrane_channel, rm)
+            roimanager.measure_in_all_rois(raw, membrane_channel, rm)
 
             rt = ResultsTable.getResultsTable("Results")
 
@@ -824,9 +501,13 @@ if __name__ == "__main__":
 
             if fiber_channel > 0:
                 # print(rt.size())
-                preset_results_column(rt, "MHC Positive Fibers (magenta)", "NO")
+                resultstable.preset_results_column(
+                    rt, "MHC Positive Fibers (magenta)", "NO"
+                )
                 # print(rt.size())
-                add_results(rt, "MHC Positive Fibers (magenta)", positive_fibers, "YES")
+                resultstable.add_results_to_resultstable(
+                    rt, "MHC Positive Fibers (magenta)", positive_fibers, "YES"
+                )
                 # print(rt.size())
 
             misc.timed_log("Saving the measurements")
@@ -847,7 +528,7 @@ if __name__ == "__main__":
             )
             rm.show()
             raw.show()
-            show_all_rois_on_image(rm, raw)
+            roimanager.show_all_rois_on_image(rm, raw)
             raw.setDisplayMode(IJ.COMPOSITE)
             enhance_contrast(raw)
             IJ.run(
@@ -857,18 +538,15 @@ if __name__ == "__main__":
             IJ.saveAs(
                 qc_duplicate,
                 "PNG",
-                output_dir
-                + "/"
-                + raw_image_title
-                + "_"
-                + str(serie_index)
-                + "_all_fibers",
+                pathtools.join2(
+                    output_dir, raw_image_title + "_" + str(serie_index) + "_all_fibers"
+                ),
             )
             qc_duplicate.close()
             wm.toFront(raw.getWindow())
             IJ.run("Remove Overlay", "")
             raw.setDisplayMode(IJ.GRAYSCALE)
-            show_all_rois_on_image(rm, raw)
+            roimanager.show_all_rois_on_image(rm, raw)
 
             misc.timed_log("Save log")
             IJ.selectWindow("Log")
@@ -890,6 +568,8 @@ if __name__ == "__main__":
             if close_raw == True:
                 raw.close()
 
-    total_execution_time_min = (time.time() - execution_start_time) / 60.0
-    IJ.log("total time in minutes: " + str(total_execution_time_min))
+    IJ.log(
+        "Script finished. Total runtime [HH:MM:SS.ss]: "
+        + misc.elapsed_time_since(execution_start_time)
+    )
     IJ.log("~~ all done ~~")
-- 
GitLab