TrackMate finding multiple objects per nuclei

Hi all and @tinevez,

I have a pipeline using TrackMate to find nuclei and 3D seeds (way faster than 3D Maxima Finder…) and wanted to use it on a new project.

However, TrackMate is finding multiple objects per nuclei as you can see here.

Together with @sebherbert we tried multiple things like :

  • removing calibratoin
  • adding black slices above and below the current one
  • with and without thresholding

but we always get the same results.

Would you have an idea of what is going on ?

The image is available here.

Thanks for your help !

Hello Laurent,

Great question I am happy to dive into it.

The reason is that the LoG detector of TrackMate does not respond well to B&W images. It is made to deal with grayscale image, where the blobs it tries to detect smoothly change values.
If you try to have TrackMate detect B&W objects you will always have issues in 2D and 3D.

There is no good solution for that. I don’t think you can make the LoG detector work with B&W images.

What I do is that I programmatically create the spots from the individual objects. Then track them with TrackMate. For instance check what we did with Philippe @philgi :

Hi Jean-Yves !

I also tried on the raw data before thresholding and the results were the same, it was giving the same number of objects somehow so I’m not entirely sure it’s B&W fault here in this case :confused:

Aha!!

Can I get the data? The link you put requires a login / passwd.

Oops sorry, I’m really bad at sharing things with SwitchDrive…

I’ve now updated the link and put both the raw data and the B&W version !

1 Like

True, all spots are duplicated on the BW and grayscale image. I did not investigate exactly why. I suspect it is linked to the Z-range: the nuclei are chopped. But you told me adding black Z-slices above does not change much.

My recommandations if you want to stick to TrackMate is:

  • work on the MIP
  • use the downsampled LoG detector with a factor of 6
1 Like

Thanks for looking into it. I was also thinking of just taking the X and Y and putting Z as the middle slice of the stack but as the script is supposed to be run as batch, I’m a bit against it…

I tried using the downsample and it seems to work thanks !

Is there a lot of change to do script wise to go from the classical LoG detector to the downsampled one ?

Here is my function to return the marker image:

def cellDetection3D(implus, rad, thresh, subpix, med):
    """Function to detect the cells in 3D using TrackMate
    
    Parameters
    ----------
    implus : imagePlus
        ImagePlus of the image to use for detection
    rad : int
        Radius of the cell to detect, half the diameter
    thresh : int
        Quality threshold for the detection
    subpix : bool
        Option for subpixel detection
    med : bool
        Option for median filter before detection
    
    Returns
    -------
    imagePlus
        New imagePlus of the same size with only the 
        centroids of the detected objects. Good for 
        using as seeds for watershed.
    """

    dim = implus.getDimensions()

    implus2 = implus.duplicate()

    # Set the parameters for LogDetector
    img           = ImageJFunctions.wrap(implus2)
    interval      = img
    cal           = implus.getCalibration()
    # calibration = [round(cal.pixelWidth,3), round(cal.pixelHeight,3), round(cal.pixelDepth,3)]
    calibration   = [cal.pixelWidth, cal.pixelHeight, cal.pixelDepth]

    radius     = rad  # the radius is half the diameter
    threshold  = thresh
    doSubpixel = subpix
    doMedian   = med

    # print cal.pixelDepth

    # Setup spot detector (see http://javadoc.imagej.net/Fiji/fiji/plugin/trackmate/detection/LogDetector.html)
    #
    # public LogDetector(RandomAccessible<T> img,
    #            Interval interval,
    #            double[] calibration,
    #            double radius,
    #            double threshold,
    #            boolean doSubPixelLocalization,
    #            boolean doMedianFilter)

    detector = LogDetector(img, interval, calibration, radius,
                           threshold, doSubpixel, doMedian)

    # Start processing and display the results
    if detector.process():
        # Get the list of peaks found
        peaks = detector.getResult()
        # print str(len(peaks)), "peaks were found."

        # Add points to ROI manager
        rm = RoiManager.getInstance()
        if not rm:
            rm = RoiManager()
        rm.reset()

        # Loop through all the peak that were found
        for peak in peaks:
            # Print the current coordinates
            # print peak.getDoublePosition(0), peak.getDoublePosition(1), peak.getDoublePosition(2)
            # Add the current peak to the Roi manager
            roi = PointRoi(peak.getDoublePosition(0) / cal.pixelWidth,
                           peak.getDoublePosition(1) / cal.pixelHeight)
            # print peak.getDoublePosition(2)/cal.pixelDepth
            roi.setPosition(
                int(round(peak.getDoublePosition(2) / cal.pixelDepth)) + 1)
            rm.addRoi(roi)
        # Show all ROIs on the image
        # rm.runCommand(imp2, "Show All")
        cellCount = rm.getCount()
        # Close the duplicate
        implus2.changes = False
        implus2.close()
    else:
        print "The detector could not process the data."

    # create an empty black (all intensities are 0) hyperstack with the same dimensions as the original
    markerimage = IJ.createImage(
        "Markerimage", "8-bit black", dim[0], dim[1], dim[2], dim[3], dim[4])

    # set the forground color to 255 for the fill command
    # resulting image will be binary
    IJ.setForegroundColor(255, 255, 255)
    for roi in range(rm.getCount()):
        # TODO: requires A (any) image window to be shown. not clean...
        rm.select(roi)
        rm.runCommand(markerimage, "Fill")
    # markerimage.show()
    IJ.run(markerimage, "Select None", "")
    IJ.run(implus, "Select None", "")
    return markerimage

Thanks again !

No.

But the post me be at least 20 chars.

To elaborate a bit on @tinevez’ sparse reply:

- detector = LogDetector(img, interval, calibration, radius, threshold, doSubpixel, doMedian)
+ downsamplingFactor = 6
+ detector = DownsampleLogDetector(img, interval, calibration, radius, threshold, downsamplingFactor)

(see the javadoc for DownsampleLogDetector)

1 Like

Hi @imagejan,

Thanks for the reply ! I found it in the javadoc indeed :wink:

However, it still finds 8 objects while the GUI is finding 4 so there must be something weird going on with my function somehow…

I’ll still investigate what’s going on but here’s the full code in case you have some time to lose :wink:

Thanks again !

# ─── SCRIPT PARAMETERS ──────────────────────────────────────────────────────────

#@ File(label="Select the directory with your images", style="directory") src_dir
#@ String(label="Extension for the images to look for", value="czi") filename_filter

# ─── IMPORTS ────────────────────────────────────────────────────────────────────

import os
import csv
import glob
import sys
from itertools import izip

# from java.awt import Color

from ij import IJ, ImagePlus, ImageStack, WindowManager as wm
from ij.measure import Measurements as MEAS
from ij.plugin import Duplicator
from ij.plugin.frame import RoiManager
from ij.plugin.filter import ParticleAnalyzer as PA
from ij.gui import WaitForUserDialog, Roi, PointRoi
from ij.measure import ResultsTable

# Bioformats imports
from loci.plugins import BF
from loci.plugins.in import ImporterOptions

# 3DSuite imports
from mcib3d.geom import Objects3DPopulation
from mcib3d.image3d import ImageInt, ImageHandler
from mcib3d.image3d.regionGrowing import Watershed3D

# TrackMate imports
from fiji.plugin.trackmate.detection import DownsampleLogDetector
from net.imglib2.img.display.imagej import ImageJFunctions


# ─── VARIABLES ──────────────────────────────────────────────────────────────────

# TrackMate detection variables

# Radius for the spots
radius     = 17/2
# Threshold
threshold  = 10
# Do Subpixel detection
doSubpixel = True
# Apply median before detection
doMedian   = False
# Downsample factor for LogDetector
downsample_factor = 6

# Channel for the intensity
cell_channel   = 1
golgi_channel  = 2
nuclei_channel = 3

# Min size for the Golgi volume to discard the smallest ones
min_volsize_golgi = 10

# ─── FUNCTIONS ──────────────────────────────────────────────────────────────────

def checkForFiles(filepath):
    """Check if files are there no matter the extension.
    
    Parameters
    ----------
    filepath : str
        Path and name to check if exists
    
    Returns
    -------
    bool
        True if exists, false otherwise
    """ 
    for filepath_object in glob.glob(filepath):
        if os.path.isfile(filepath_object):
            return True

    return False

def getFileList(directory, filteringString):
    """Get a list of files with specific extension.

    List includes subdirectories
    
    Parameters
    ----------
    directory : str
        Parent folder where to look for files
    filteringString : str
        Extension to look for
    
    Returns
    -------
    str[]
        List of paths for files with the extension
    """

    files = []
    for (dirpath, dirnames, filenames) in os.walk(directory):
        # if out_dir in dirnames: # Ignore destination directory
            # dirnames.remove(OUT_SUBDIR)
        for f in filenames:
            if f.endswith(filteringString):
            # if filteringString in f:
                files.append(os.path.join(dirpath, f))
    return files


def BFImport(indivFile):
    """Import the file using BioFormats
    
    Parameters
    ----------
    indivFile : str
        Path of the file to open
    
    Returns
    -------
    imagePlus[]
        Array of ImagePlus read from the file
    """

    options = ImporterOptions()
    options.setId(str(indivFile))
    options.setColorMode(ImporterOptions.COLOR_MODE_COMPOSITE)
    imps = BF.openImagePlus(options)
    return imps


def cellDetection3D(implus, rad, thresh, downsample_factor):
    """Function to detect the cells in 3D using TrackMate
    
    Parameters
    ----------
    implus : imagePlus
        ImagePlus of the image to use for detection
    rad : int
        Radius of the cell to detect, half the diameter
    thresh : int
        Quality threshold for the detection
    subpix : bool
        Option for subpixel detection
    med : bool
        Option for median filter before detection
    
    Returns
    -------
    imagePlus
        New imagePlus of the same size with only the 
        centroids of the detected objects. Good for 
        using as seeds for watershed.
    """

    dim = implus.getDimensions()

    implus2 = implus.duplicate()

    # Set the parameters for LogDetector
    img           = ImageJFunctions.wrap(implus2)
    interval      = img
    cal           = implus.getCalibration()
    # calibration = [round(cal.pixelWidth,3), round(cal.pixelHeight,3), round(cal.pixelDepth,3)]
    calibration   = [cal.pixelWidth, cal.pixelHeight, cal.pixelDepth]

    radius     = rad  # the radius is half the diameter
    threshold  = thresh
    # doSubpixel = subpix
    # doMedian   = med

    # print cal.pixelDepth

    # Setup spot detector (see http://javadoc.imagej.net/Fiji/fiji/plugin/trackmate/detection/LogDetector.html)
    #
    # public LogDetector(RandomAccessible<T> img,
    #            Interval interval,
    #            double[] calibration,
    #            double radius,
    #            double threshold,
    #            boolean doSubPixelLocalization,
    #            boolean doMedianFilter)

    detector = DownsampleLogDetector(img, interval, calibration, radius,
                           threshold, downsample_factor)

    # Start processing and display the results
    if detector.process():
        # Get the list of peaks found
        peaks = detector.getResult()
        # print str(len(peaks)), "peaks were found."

        # Add points to ROI manager
        rm = RoiManager.getInstance()
        if not rm:
            rm = RoiManager()
        rm.reset()

        # Loop through all the peak that were found
        for peak in peaks:
            # Print the current coordinates
            # print peak.getDoublePosition(0), peak.getDoublePosition(1), peak.getDoublePosition(2)
            # Add the current peak to the Roi manager
            roi = PointRoi(peak.getDoublePosition(0) / cal.pixelWidth,
                           peak.getDoublePosition(1) / cal.pixelHeight)
            # print peak.getDoublePosition(2)/cal.pixelDepth
            roi.setPosition(
                int(round(peak.getDoublePosition(2) / cal.pixelDepth)) + 1)
            rm.addRoi(roi)
        # Show all ROIs on the image
        # rm.runCommand(imp2, "Show All")
        cellCount = rm.getCount()
        # Close the duplicate
        implus2.changes = False
        implus2.close()
    else:
        print "The detector could not process the data."

    # create an empty black (all intensities are 0) hyperstack with the same dimensions as the original
    markerimage = IJ.createImage(
        "Markerimage", "8-bit black", dim[0], dim[1], dim[2], dim[3], dim[4])

    # set the forground color to 255 for the fill command
    # resulting image will be binary
    IJ.setForegroundColor(255, 255, 255)
    for roi in range(rm.getCount()):
        # TODO: requires A (any) image window to be shown. not clean...
        rm.select(roi)
        rm.runCommand(markerimage, "Fill")
    # markerimage.show()
    IJ.run(markerimage, "Select None", "")
    IJ.run(implus, "Select None", "")
    return markerimage

def cleanUp():
    """Cleans the log and prints that it's starting
    """
    IJ.log("\\Clear")
    IJ.log("STARTING")


# ─── MAIN CODE ──────────────────────────────────────────────────────────────────


# Take channel 2
#     Median 10
#     Li thresholding stack
# Take channel 3
#     Trackmate for finding the nuclei : 17Β΅, 10 quality, false, true
# Use nuclei to watershed the channel 2

# Take channel 2
#     Median 2
#     Yen threshold
# Measure Red intensity in Golgi
# Dilate around golgi measure red intensity




cleanUp()

# Retrieve list of files
src_dir = str(src_dir)
files   = getFileList(src_dir, filename_filter)

# If the list of files is not empty
if files:

    filenames              = []
    golgi_intensity        = []
    around_golgi_intensity = []
    cell_intensity         = []

    # For each file finishing with the filtered string
    for file in files:
        # Get info for the files
        folder   = os.path.dirname(file)
        basename = os.path.basename(file)
        basename = os.path.splitext(basename)[0]

        # Import the file with BioFormats
        IJ.log("Currently opening " + basename + "...")
        imps = BFImport(str(file))


        for imp in imps:

            # Get info about the image
            input_dir = imp.getOriginalFileInfo().directory
            filename  = os.path.splitext(imp.getOriginalFileInfo().fileName)[0]
            filename  = filename.replace(" ", "_")

            # Check for existing file_name
            outfile_name       = filename
            test_name          = outfile_name + ".*"
            out_full_path_test = os.path.join(input_dir, test_name)
            count              = 1

            # Save a new file for each ROI
            while checkForFiles(out_full_path_test):
                # print(out_full_path_test)
                outfile_name       = filename + "_" + str(count)
                test_name          = outfile_name + ".*"
                out_full_path_test = os.path.join(input_dir, test_name)
                count              = count + 1

            out_full_path = os.path.join(input_dir, outfile_name + ".csv")

            imp_golgi  = Duplicator().run(imp, golgi_channel, golgi_channel, 1, imp.getNSlices(), 1, 1)
            imp_nuclei = Duplicator().run(imp, nuclei_channel, nuclei_channel, 1, imp.getNSlices(), 1, 1)

            imp_golgi2  = imp_golgi.duplicate()
            imp_cell    = imp_golgi.duplicate()
            imp_nuclei2 = imp_nuclei.duplicate()

            IJ.run(imp_cell, "Median...", "radius=10")

            middle_slice = int(imp_cell.getNSlices() / 2)
            imp_cell.setSlice(middle_slice)

            IJ.setAutoThreshold(imp_cell, "Li dark stack")
            IJ.run(imp_cell, "Convert to Mask", "method=Li background=Dark black")

            marker_image = cellDetection3D(imp_nuclei2, radius, threshold, downsample_factor)

            water = Watershed3D(imp_cell.getImageStack(), marker_image.getImageStack(), 0, 0)
            imp_watershed = water.getWatershedImage3D().getImagePlus()
            imp_watershed.show()

            sys.exit(0)

Sorry it was actually working, I just had a rounding error and had to put the radius to 17.0/2 instead of 17/2.

Thanks both ! :slight_smile:

1 Like