diff --git a/2d_spots_in_fibers.py b/2d_spots_in_fibers.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e0a81636e3d9dec4e242499d9a2948488d06244
--- /dev/null
+++ b/2d_spots_in_fibers.py
@@ -0,0 +1,479 @@
+#@ OpService ops
+#@ CommandService command
+#@ RoiManager rm
+#@ File (label="select image", description="select your input image", style=file) path_to_image
+#@ Integer (label="select image file series", description="leave 1 if not needed", value=1, min=1, max=20, stepSize=1, persist=false, style=slider) series_number
+#@ File (label="Fiber Segmentation", description="select the Fiber Segmentation RoiSet .zip file", style=file) fiber_segmentation_roiset
+#@ Integer (label="DAPI channel", description="select the DAPI channel", min=1, max=20, stepSize=1, persist=true, style=slider) dapi_channel_number
+#@ Integer (label="DAPI threshold", description="0 = Auto", min=0) threshold
+#@ String (label="processing channels", description="comma separated list of channels, e.g. 2,3,6,7", value="1,2,3,4") processing_channels_string
+#@ String (label="Spot detection quality threshold", description="comma separated list of values, one per channel", value="80,80,80,80" ) quality_thresholds_string
+#@ Double (label="Minimum spot diameter", description="smaller spots will be discarded", value=0.3, min=1, max=10, stepSize=0.1) spot_diameter
+
+# trackmate imports
+from fiji.plugin.trackmate import Settings
+from fiji.plugin.trackmate import TrackMate
+from fiji.plugin.trackmate.detection import DogDetectorFactory
+from fiji.plugin.trackmate.action import LabelImgExporter
+# ij imports
+from ij import IJ
+from ij.plugin import ImageCalculator
+from ij.measure import ResultsTable
+from ij.measure import Measurements as M
+from ij.plugin.filter import Analyzer
+# Bio-Formats imports
+from loci.plugins import BF
+from loci.common import Region
+from loci.plugins.in import ImporterOptions
+from loci.formats import ImageReader, MetadataTools, Memoizer
+# MorpholibJ imports
+from inra.ijpb.binary import BinaryImages
+# BIOP imports
+from ch.epfl.biop.ij2command import Labels2Rois
+# python imports
+import os
+
+
+def get_parentdir_filename_ext_from_path(path):
+    """ returns parent directory, filename, and file extension from a given path
+
+    Parameters
+    ----------
+    path : String
+        the full path to a file
+
+    Returns
+    -------
+    String
+        the parent directory, the filename without extension and the extension split after the dot
+    """
+    parent_dir = os.path.dirname(path)
+    base = os.path.basename(path)
+    (filename, ext) = os.path.splitext(base)
+    
+    return (parent_dir, filename, ext)
+
+
+def write_bf_memoryfile(path_to_file):
+    """Write a BF memo-file so subsequent access to the same file is faster.
+
+    The Bio-Formats memo-file is written next to the image file (i.e. in the
+    same folder as the given file).
+
+    Parameters
+    ----------
+    string
+        The full path to the image file.
+    """
+    reader = Memoizer(ImageReader())
+    reader.setId(path_to_file)
+    reader.close()
+
+
+def get_ome_metadata(path_to_image, series_number):
+    """Get some metadata fields using BF from an image on disk from a given series.
+    Gets number of channels, the image width, image height and bit depth.
+
+    Parameters
+    ----------
+    path_to_image : str
+        full path to the image
+    series_number : int
+        the Bio-Formats series number
+
+    Returns
+    -------
+    int
+        n_channels : the number of channels
+    int
+        image_width : the image width in px
+    int 
+        image_height : the image height in px
+    int
+        bit_depth : the image bit depth
+    """
+    series_number -= 1 # python starts counting from 0
+    reader = ImageReader()
+    ome_meta = MetadataTools.createOMEXMLMetadata()
+    reader.setMetadataStore(ome_meta)
+    reader.setId(path_to_image)
+    reader.setSeries(series_number)
+    n_channels = reader.getSizeC()
+    image_width = reader.getSizeX()
+    image_height = reader.getSizeY()
+    bit_depth = reader.getBitsPerPixel()
+    
+    reader.close()
+
+    return n_channels, image_width, image_height, bit_depth
+
+
+def BFopen_image(path_to_image, channel_number, series_number, region=None, z_slice_number=None):
+    """Use Bio-Formats to open a given image or subset.
+
+    Parameters
+    ----------
+    path_to_image : str
+        full path to the image file on disk
+    channel_number : int
+        the number of the channel to open
+    series_number : int
+        the Bio-Formats series number to open
+    region : list, optional
+        Bio-Formats crop region, by default None. format = [start_x, start_y, width in px, height in px]
+    z_slice_number : int, optional
+        the number of the z-slice to open, by default None
+
+    Returns
+    -------
+    ImagePlus
+        the requested image
+    """
+    series_number -= 1 # python starts counting from 0
+    channel_number -= 1 # python starts counting from 0
+    options = ImporterOptions()
+    options.setId(path_to_image)
+    options.setColorMode(ImporterOptions.COLOR_MODE_GRAYSCALE)
+    options.setSplitChannels(False)
+    options.setSplitFocalPlanes(False)
+    options.setSplitTimepoints(False)
+    options.setSeriesOn(series_number, True)
+
+    if region is not None:
+        options.setCrop(True)
+        options.setCropRegion(series_number, Region(region[0], region[1], region[2], region[3])) # x,y,w,h
+
+    options.setSpecifyRanges(True)
+    options.setCBegin(series_number, channel_number)
+    options.setCEnd(series_number, channel_number)
+    options.setCStep(series_number, 1)
+    
+    if z_slice_number is not None:
+        z_slice_number -= 1 # python starts counting from 0
+        options.setZBegin(series_number, z_slice_number)
+        options.setZEnd(series_number, z_slice_number)
+        options.setZStep(series_number, 1)
+
+    imps = BF.openImagePlus(options)
+
+    return imps[0]
+
+
+def run_trackmate_dog_spot_detector(imp, spot_diameter, quality_threshold):
+    """Run TrackMates DoG detector with a given quality threshold
+    on a target image
+
+    Parameters
+    ----------
+    imp : ImagePlus
+        the input image
+    quality_threshold : float or double
+        the spot detection quality threshold
+
+    Returns
+    -------
+    ImagePlus
+        a label image of the detected spots.
+    """
+
+    cal = imp.getCalibration()
+    settings = Settings(imp)
+
+    settings.detectorFactory = DogDetectorFactory()
+    settings.detectorSettings['DO_SUBPIXEL_LOCALIZATION'] = True
+    settings.detectorSettings['RADIUS'] = spot_diameter / 2 # type = double
+    settings.detectorSettings['TARGET_CHANNEL'] = 0
+    settings.detectorSettings['THRESHOLD'] = quality_threshold # type = double
+    settings.detectorSettings['DO_MEDIAN_FILTERING'] = False
+
+    trackmate = TrackMate(settings)
+    trackmate.computeSpotFeatures(False)
+    trackmate.computeTrackFeatures(False)
+
+    if not trackmate.checkInput():
+        print( str( trackmate.getErrorMessage() ) )
+
+    if not trackmate.process():
+        print( str( trackmate.getErrorMessage() ) )
+
+    exportSpotsAsDots = True
+    exportTracksOnly = False
+    labelIdPainting = LabelImgExporter.LabelIdPainting.LABEL_IS_INDEX_MOVIE_UNIQUE
+    label_imp = LabelImgExporter.createLabelImagePlus(trackmate, exportSpotsAsDots, exportTracksOnly, labelIdPainting)
+    label_imp.setCalibration(cal)
+    imp.close() 
+
+    return label_imp
+
+
+def get_threshold_from_method(imp, method):
+    """returns the threshold value of chosen IJ AutoThreshold method from a stack (hyperstack?)
+
+    Parameters
+    ----------
+    imp : ImagePlus (maybe also accepts ij2 dataset, img?)
+        the image from which to get the threshold value
+    method : string
+        the AutoThreshold method to use, e.g. "otsu", "yen", ...
+
+    Returns
+    -------
+    int
+    """
+    histogram = ops.run("image.histogram", imp)
+    threshold_value = ops.run("threshold.%s" % method, histogram)
+    threshold_value = int(round(threshold_value.get()))
+    
+    return threshold_value
+
+
+def convert_to_binary(imp, threshold=1, scale_binary=True):
+    """Convert grayscale image to a mask / binary image
+
+    Parameters
+    ----------
+    imp : ImagePlus
+        the input image
+    threshold : int, optional
+        the threshold above which pixels are considered foreground, by default 1
+    scale_binary : bool, optional
+        scale the mask image from 0/255 to 0/1, by default True
+
+    Returns
+    -------
+    ImagePlus
+        the mask / binary image
+    """
+    mask_imp = imp.duplicate()
+    bitdepth = mask_imp.getBitDepth()
+    if bitdepth > 16.0:
+        IJ.run("Conversions...", " ")
+        IJ.run(mask_imp, "16-bit", "")
+        IJ.run("Conversions...", "scale")
+
+    IJ.setRawThreshold(mask_imp, threshold, (2**bitdepth)-1, None)
+    IJ.run(mask_imp, "Convert to Mask", "")
+    if scale_binary == True:
+        IJ.run(mask_imp, "Divide...", "value=255")
+        IJ.run(mask_imp, "Enhance Contrast", "saturated=0.35")
+    mask_imp.changes = False
+    
+    return mask_imp
+
+
+def save_image_as_IJtif(imp, filename, suffix, target):
+    """Save the image as ImageJ-Tiff
+
+    Parameters
+    ----------
+    imp : ImagePlus
+        the input Image to be saved
+    filename : str
+        the image filename without extension
+    suffix : int, float or str
+        a suffix that will be added to the filename as a string
+    target : str
+        the target directory in with to save the image
+    """
+
+    savename = filename + "_" + str(suffix) + ".tif"
+    savepath = os.path.join(target, savename)
+    IJ.log("now saving: " + str(savepath))
+    IJ.saveAs(imp, "Tiff", savepath)
+
+
+def convert_labelimage_to_binary(label_imp, scale_binary=True):
+    """Convert a label image to a mask / binary image
+
+    Parameters
+    ----------
+    label_imp : ImagePlus
+        the input label image
+    scale_binary : bool, optional
+        scale the mask image from 0/255 to 0/1, by default True
+
+    Returns
+    -------
+    ImagePlus
+        the mask / binary image
+    """
+    binary_imp = BinaryImages.binarize(label_imp)
+    if scale_binary == True:
+        IJ.run(binary_imp, "Divide...", "value=255")
+
+    return binary_imp
+
+
+def measure_intensity_sum(imp, rm):
+    """
+    Measure theraw integrated intensity for all rois in target imp.
+
+    Parameters
+    ----------
+    imp : ImagePlus
+        The image from which the intensity will be measured.
+    rm : RoiManager
+        The ROI Manager containing the regions of interest to be analyzed.
+
+    Returns
+    -------
+    label_id : list of str
+        A list of labels corresponding to each ROI.
+    sum_intensity : list of int
+        A list of summed integrated intensities for each ROI.
+
+    Notes
+    -----
+    The results are stored in a `ij.ResultsTable`,
+    from which the raw integrated density values are extracted.
+
+    Example
+    -------
+    >>> labels, intensities = measure_intensity_sum(imp, roi_manager)
+    """
+
+    rt_ = ResultsTable()
+    options = M.INTEGRATED_DENSITY
+    an = Analyzer (imp, options, rt_)
+    an.setPrecision(0)
+    
+    label_id = []
+    sum_intensity = []
+
+    for index, roi in enumerate(rm.getRoisAsArray()):
+        imp.setRoi(roi)
+        an.measure()
+        label_id.append(roi.getName())
+        sum_intensity.append(int(rt_.getColumn("RawIntDen")[index]))
+    
+    return label_id, sum_intensity
+
+
+def add_results_to_resultstable(results_table, column, values):
+    """Add values to the ResultsTable starting from row 0 of a given column.
+
+    Parameters
+    ----------
+    results_table : ij.measure.ResultsTable
+        a reference of the IJ-ResultsTable
+    column : string
+        the column in which to add the values
+    values : list(int, double or float)
+        array with values to be added
+    """
+    for index, value in enumerate(values):
+        results_table.setValue(column, index, value)
+
+
+def save_results_table(results_table, filename, suffix, target):
+    """Save an IJ-ResultsTable as txt to a given location
+
+    Parameters
+    ----------
+    results_table : IJ-ResultsTable
+        an IJ-ResultsTable
+    filename : str
+        filename to include in the tables name
+    suffix : str, int or float
+        a suffix to be included in the tables name
+    target : str
+        the target directory in which to save the table
+    """
+
+    savename = filename + "_" + str(suffix) + ".txt"
+    savepath = os.path.join(target, savename)
+    results_table.save(savepath)
+
+
+def save_labelimage_as_ijroiset(label_imp, rm, filename, suffix, target):
+    """Save all labels i a label Images as IJ-ROIset to target location.
+      Only 2D, requires PTBIOP update site.
+
+    Parameters
+    ----------
+    label_imp : ImagePlus
+        the input label image
+    filename : str
+        the filename to include in the ROIset name
+    suffix : str, int or float
+        a suffix to be included in the ROIset name
+    target : str
+        the target directory in which to save the table
+    """
+    command.run( Labels2Rois , False , 'imp' , label_imp , 'rm', rm).get()
+    savename = filename + "_" + str(suffix) + ".zip"
+    savepath = os.path.join(target, savename)
+    rm.runCommand("Save", savepath)
+    rm.reset()
+
+
+def load_rois_from_zip(path, rm):
+    """Load ROIs from the given zip file and add them to the RoiManager.
+
+    Parameters
+    ----------
+    path : string
+        Path to the ROI zip file.
+    rm : RoiManager
+        The ROI Manager.
+    """
+    rm.runCommand("Open", path)
+
+
+def close_images(list_of_imps):
+    """Close given ImagePlus images
+
+    Parameters
+    ----------
+    list_of_imps : list of ImagePlus
+        list of ImagePlus images to be closed
+    """
+    for imp in list_of_imps:
+        imp.changes = False
+        imp.close()
+
+
+path_to_image = str(path_to_image).replace("\\","/")
+fiber_segmentation_roiset = str(fiber_segmentation_roiset).replace("\\","/")
+parent_dir, filename, ext = get_parentdir_filename_ext_from_path(path_to_image)
+write_bf_memoryfile(path_to_image)
+n_channels, image_width, image_height, bit_depth = get_ome_metadata(path_to_image, series_number)
+processing_channels_string = processing_channels_string.replace(" ", "")
+processing_channels = processing_channels_string.split(",")
+quality_thresholds_string = quality_thresholds_string.replace(" ", "")
+quality_thresholds = quality_thresholds_string.split(",")
+
+# threshold DAPI channel and convert to binary
+dapi_channel = BFopen_image(path_to_image, dapi_channel_number, series_number)
+if threshold <= 0:
+    threshold = get_threshold_from_method(dapi_channel, "otsu")
+dapi_binary = convert_to_binary(dapi_channel, threshold)
+
+# detect spots and count them per fiber
+results_table = ResultsTable()
+for index, channel in enumerate(processing_channels):
+    channel = int(channel)
+    quality_threshold = float(quality_thresholds[index])
+    spots_channel = BFopen_image(path_to_image, channel, series_number)
+    spots_label_imp = run_trackmate_dog_spot_detector(spots_channel, spot_diameter, quality_threshold)
+    save_labelimage_as_ijroiset(spots_label_imp, rm, filename, "spots_ch" + str(channel), parent_dir)
+    spots_binary_imp = convert_labelimage_to_binary(spots_label_imp)
+    dapi_positive_spots_binary = ImageCalculator.run(spots_binary_imp, dapi_binary, "Multiply create")
+    dapi_negative_spots_binary = ImageCalculator.run(spots_binary_imp, dapi_positive_spots_binary, "Subtract create")
+    
+    load_rois_from_zip(fiber_segmentation_roiset, rm)
+    fiber_label_IDs, n_spots_per_fiber = measure_intensity_sum(spots_binary_imp, rm)
+    _, n_dapi_positive_spots_per_fiber = measure_intensity_sum(dapi_positive_spots_binary, rm)
+    _, n_dapi_negative_spots_per_fiber = measure_intensity_sum(dapi_negative_spots_binary, rm)
+
+    # TODO: the column fiber label ID needs to be added only one time. Maybe check before if column exists.
+    add_results_to_resultstable(results_table, "fiber label ID", fiber_label_IDs)
+    add_results_to_resultstable(results_table, "n spots channel " + str(channel), n_spots_per_fiber)
+    add_results_to_resultstable(results_table, "n spots dapi positive channel " + str(channel), n_dapi_positive_spots_per_fiber)
+    add_results_to_resultstable(results_table, "n spots dapi negative channel " + str(channel), n_dapi_negative_spots_per_fiber)
+    close_images([spots_channel, spots_label_imp, spots_binary_imp, spots_binary_imp, dapi_binary, dapi_positive_spots_binary, dapi_negative_spots_binary])
+
+results_table.show("Spots")
+save_results_table(results_table, filename, "Spots", parent_dir)
+IJ.run("Collect Garbage", "")
+IJ.log("DONE")
\ No newline at end of file