Skip to content

LabelImgExporter.java created negative indexes #338

@ABCarvalho

Description

@ABCarvalho
Script writen in Jython

#@ String(label="File Path", required=False) input_path
#@ ImageJ ij

import os
import sys
from ij import IJ
from fiji.plugin.trackmate import Logger, Settings, Model, TrackMate
from fiji.plugin.trackmate.detection import LabelImageDetectorFactory
from fiji.plugin.trackmate.tracking.overlap import OverlapTrackerFactory
from fiji.plugin.trackmate.action import LabelImgExporter
from fiji.plugin.trackmate.features import FeatureFilter
from fiji.plugin.trackmate.features.track import TrackDurationAnalyzer, TrackBranchingAnalyzer

# Import the Enum class for label painting
from fiji.plugin.trackmate.action.LabelImgExporter import LabelIdPainting

# --- SETTINGS ---
IOU_THRESHOLD = 0.2
SCALE_FACTOR = 1.0
MIN_TRACK_DURATION = 5
MIN_SPLIT_EVENTS = 1

# Handle potential UTF-8 encoding issues in Fiji's Jython
reload(sys)
sys.setdefaultencoding('utf-8')

def process_image(imp):
    if imp is None:
        return

    # --- Dimensions Fix ---
    stack_size = imp.getStackSize()
    if imp.getNSlices() > 1 and imp.getNFrames() == 1:
        IJ.log("  [Fix] Converting Stack Z-slices to Time-frames...")
        imp.setDimensions(1, 1, stack_size)
        imp.setOpenAsHyperStack(True)

    # 1. Setup
    model = Model()
    model.setLogger(Logger.IJ_LOGGER)
    settings = Settings(imp)

    # 2. Detector (Label Image)
    settings.detectorFactory = LabelImageDetectorFactory()
    settings.detectorSettings = {
        'TARGET_CHANNEL': 1,
        'SIMPLIFY_CONTOURS': False
    }

    # 3. Tracker (Overlap)
    settings.trackerFactory = OverlapTrackerFactory()
    settings.trackerSettings = settings.trackerFactory.getDefaultSettings()
    settings.trackerSettings['MIN_IOU'] = float(IOU_THRESHOLD)
    settings.trackerSettings['SCALE_FACTOR'] = float(SCALE_FACTOR)

    # 4. Analyzers
    settings.addAllAnalyzers()

    # 5. Run Initial Processing
    trackmate = TrackMate(model, settings)
    if not trackmate.checkInput() or not trackmate.process():
        IJ.log("[Error] TrackMate failed: " + str(trackmate.getErrorMessage()))
        return

    # =========================================================
    # EXPORT 1: DURATION FILTER
    # =========================================================
    settings.getTrackFilters().clear()
    
    filter_duration = FeatureFilter('TRACK_DURATION', float(MIN_TRACK_DURATION), True)
    settings.addTrackFilter(filter_duration)
    trackmate.execTrackFiltering(True) 
    
    IJ.log("  [Filter 1] Duration >= " + str(MIN_TRACK_DURATION))
    IJ.log("  Tracks kept: " + str(model.getTrackModel().nTracks(True)))
    export_labels(imp, trackmate, suffix="_Tracked")

# =========================================================

def export_labels(imp, trackmate, suffix):
    try:
        # Use LABEL_IS_TRACK_ID via class reference for Lineage Color consistency
        paint_strategy = LabelIdPainting.LABEL_IS_TRACK_ID
        exportSpotsAsDots = False
        exportTracksOnly = True
        
        # Calling static method directly
        tracked_imp = LabelImgExporter.createLabelImagePlus(trackmate, exportSpotsAsDots, exportTracksOnly, paint_strategy)

   except Exception as e:
        IJ.log("  [Error] Export API failed: " + str(e))
        return

    if tracked_imp:
        file_info = imp.getOriginalFileInfo()
        folder = file_info.directory if file_info else IJ.getDirectory("current")
        name = file_info.fileName if file_info else imp.getTitle()
        
        base_name = os.path.splitext(name)[0].replace("_CleanSeg", "")
        output_name = base_name + suffix + ".tif"
        save_path = os.path.join(folder, output_name)
        
        tracked_imp.setTitle(output_name)
        IJ.saveAs(tracked_imp, "Tiff", save_path)
        IJ.log("  [Success] Saved: " + output_name)
        
    else:
        IJ.log("  [Warn] No tracks passed filters for " + suffix)
                

So when automating a first round of tracking with Trackmate I encountered this issue. Possibly common knowledge, but it was not obvious to me so I decided to report it.

If a spot is filtered out by a Track filter, in this example a simple Track_duration filter = 5 (mins). Combining LabelIdPainting.LABEL_IS_TRACK_ID with exportTracksOnly = True resulted in some labels with a negative index.

This was fixed by turning exportTracksOnly = False, which made spots filtered out by a track filter behave as described:

"The spot label is the ID of the track it belongs to, plus one (+1). "
+ "Spots that do not belong to tracks are painted with a unique integer "
+ "larger than the last trackID in the dataset." ),

However, I did not find any documentation pertaining to this funky interaction, so I am leaving here the report.

Metadata

Metadata

Assignees

Labels

investigateAssignee should verify the issue and try to reproduce it.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions