From dc27c902c3a607ef91f19ff4d7c528870d5c8966 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 1 Sep 2023 18:01:15 +0200 Subject: [PATCH 01/65] Add LbKit as a dependency. And remove Gson from now. Apparently there are incompatibilities from the (same) declaration in LabKit pom, which triggers the errors I complained about today on the forum. --- pom.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b282df815..97689a66c 100644 --- a/pom.xml +++ b/pom.xml @@ -156,6 +156,11 @@ sc.fiji fiji-lib + + sc.fiji + labkit-ui + 0.3.12-SNAPSHOT + @@ -208,10 +213,10 @@ com.github.vlsi.mxgraph jgraphx - + com.itextpdf itextpdf From 4bbf6652724bdbaded10f73862321044ec8656ce Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 1 Sep 2023 18:07:14 +0200 Subject: [PATCH 02/65] A LabKit launcher made to edit spots in the current frame. This launcher catches what time-point is currently diplayed, and wrap all information needed for LabKit to edit the spot it contains, as labels. While the user edits the labels in LabKit, the TrackMate UI is disabled. Once the user closes the LabKit window, the modifications they made are inspected. This class compares the new labels with the previous ones, and can determine whether a spot has been added, removed or modified. In the last case it updates the model with the modified spots, and reintroduces it in the tracks as it should. All features of modified spots and their edges are recomputed. If a label has several connected components, they are added as separate spots. The one closest to the original spot is reintroduced in the tracks. The label names are important: they are used to retrieve the original spot id and the original spot shape for comparison. If the user modifies a label, it will be perceived as a new spot instead of a modified one. Tested in 2D so far. --- .../trackmate/gui/editor/LabkitLauncher.java | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java new file mode 100644 index 000000000..f5491e1ec --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -0,0 +1,276 @@ +package fiji.plugin.trackmate.gui.editor; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.List; +import java.util.Set; + +import javax.swing.JLabel; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; + +import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.Context; +import org.scijava.ui.behaviour.util.AbstractNamedAction; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.detection.MaskUtils; +import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; +import fiji.plugin.trackmate.util.TMUtils; +import ij.ImagePlus; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.test.ImgLib2Assert; +import net.imglib2.type.logic.BitType; +import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import sc.fiji.labkit.ui.LabkitFrame; +import sc.fiji.labkit.ui.inputimage.DatasetInputImage; +import sc.fiji.labkit.ui.labeling.Label; +import sc.fiji.labkit.ui.labeling.Labeling; +import sc.fiji.labkit.ui.models.DefaultSegmentationModel; +import sc.fiji.labkit.ui.models.ImageLabelingModel; + +public class LabkitLauncher +{ + + private final double[] calibration; + + private final TrackMate trackmate; + + private final EverythingDisablerAndReenabler disabler; + + private Labeling previousLabels; + + private int currentTimePoint; + + private final boolean is2D; + + private final double dt; + + public LabkitLauncher( final TrackMate trackmate, final EverythingDisablerAndReenabler disabler ) + { + this.trackmate = trackmate; + this.disabler = disabler; + final ImagePlus imp = trackmate.getSettings().imp; + this.calibration = TMUtils.getSpatialCalibration( imp ); + this.is2D = DetectionUtils.is2D( imp ); + this.dt = imp.getCalibration().frameInterval; + + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + protected void launch() + { + final Context context = TMUtils.getContext(); + final ImagePlus imp = trackmate.getSettings().imp; + final ImgPlus src = TMUtils.rawWraps( imp ); + final int timeAxis = src.dimensionIndex( Axes.TIME ); + + // Reslice for current time-point. + this.currentTimePoint = imp.getFrame() - 1; + final ImgPlus frame = ImgPlusViews.hyperSlice( src, timeAxis, currentTimePoint ); + final DatasetInputImage input = new DatasetInputImage( frame ); + + // Prepare label image. + final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y, Axes.Z, }; + final long[] dims = new long[ is2D ? 2 : 3 ]; + for ( int d = 0; d < dims.length; d++ ) + dims[ d ] = src.dimension( src.dimensionIndex( axes[ d ] ) ); + final Img< UnsignedShortType > lblImg = ArrayImgs.unsignedShorts( dims ); + final double[] calibration = TMUtils.getSpatialCalibration( imp ); + final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); + + // Write spots in it. + final Iterable< Spot > spotsThisFrame = trackmate.getModel().getSpots().iterable( currentTimePoint, true ); + for ( final Spot spot : spotsThisFrame ) + spot.iterable( lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); + + // Labeling model. + final DefaultSegmentationModel model = new DefaultSegmentationModel( context, input ); + model.imageLabelingModel().labeling().set( Labeling.fromImg( lblImgPlus ) ); + + // Store a copy. + final ImgPlus< UnsignedShortType > copy = lblImgPlus.copy(); + this.previousLabels = Labeling.fromImg( copy ); + + // Show LabKit. + final LabkitFrame labkit = LabkitFrame.show( model, "Edit TrackMate data frame " + ( currentTimePoint + 1 ) ); + labkit.onCloseListeners().addWeakListener( () -> reimportData( model.imageLabelingModel(), currentTimePoint ) ); + } + + private void reimportData( final ImageLabelingModel lm, final int currentTimePoint ) + { + final Model model = trackmate.getModel(); + final SpotCollection spots = model.getSpots(); + final Labeling labeling = lm.labeling().get(); + final List< Label > labels = labeling.getLabels(); + model.beginUpdate(); + try + { + for ( final Label label : labels ) + { + + try + { + final int id = Integer.parseInt( label.name() ) - 1; + final Spot spot = spots.search( id ); + if ( spot == null ) + addNewSpot( label, labeling ); + else + modifySpot( label, labeling, spot ); + + } + catch ( final NumberFormatException nfe ) + { + addNewSpot( label, labeling ); + } + } + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + finally + { + model.endUpdate(); + disabler.reenable(); + } + } + + private void modifySpot( final Label label, final Labeling labeling, final Spot spot ) + { + final Label previousLabel = previousLabels.getLabel( label.name() ); + final RandomAccessibleInterval< BitType > previousRegion = previousLabels.getRegion( previousLabel ); + final RandomAccessibleInterval< BitType > region = labeling.getRegion( label ); + if ( !hasChanged( region, previousRegion ) ) + return; + + final boolean simplify = true; + final int numThreads = 1; + final RandomAccessibleInterval< UnsignedByteType > qualityImage = null; + final List< Spot > spots = MaskUtils.fromMaskWithROI( region, region, calibration, simplify, numThreads, qualityImage ); + + // Time position. + for ( final Spot s : spots ) + s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + + // If there is no spot, it's because we removed it. + final Model model = trackmate.getModel(); + if ( spots.isEmpty() ) + { + model.removeSpot( spot ); + return; + } + // Hopefully there is only one, if not we pick the closest one. + Spot closest = null; + double minD2 = Double.POSITIVE_INFINITY; + for ( final Spot s : spots ) + { + final double d2 = s.squareDistanceTo( spot ); + if ( d2 < minD2 ) + { + minD2 = d2; + closest = s; + } + } + closest.setName( spot.getName() ); + model.addSpotTo( closest, Integer.valueOf( currentTimePoint ) ); + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( spot ); + for ( final DefaultWeightedEdge e : edges ) + { + final double weight = model.getTrackModel().getEdgeWeight( e ); + final Spot source = model.getTrackModel().getEdgeSource( e ); + final Spot target = model.getTrackModel().getEdgeTarget( e ); + if ( source == spot ) + model.addEdge( closest, target, weight ); + else if ( target == spot ) + model.addEdge( source, closest, weight ); + else + throw new IllegalArgumentException( "The edge of a spot does not have the spot as source or target?!?" ); + } + model.removeSpot( spot ); + + // If not, add the other ones as new ones. + for ( int i = 1; i < spots.size(); i++ ) + { + final Spot s = spots.get( i ); + s.setName( spot.getName() + "_" + i ); + model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); + } + } + + private static final boolean hasChanged( final RandomAccessibleInterval< BitType > region, final RandomAccessibleInterval< BitType > previousRegion ) + { + try + { + ImgLib2Assert.assertImageEquals( region, previousRegion ); + } + catch ( final AssertionError e ) + { + return true; + } + return false; + } + + + private void addNewSpot( final Label label, final Labeling labeling ) + { + final boolean simplify = true; + final int numThreads = 1; + final RandomAccessibleInterval< UnsignedByteType > qualityImage = null; + + // Slice by time. + final RandomAccessibleInterval< BitType > region = labeling.getRegion( label ); + final List< Spot > spots = MaskUtils.fromMaskWithROI( region, region, calibration, simplify, numThreads, qualityImage ); + for ( final Spot spot : spots ) + { + spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + spot.setName( label.name() ); + trackmate.getModel().addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); + } + + } + + public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate ) + { + return new AbstractNamedAction( "launch labkit editor" ) + { + + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed( final ActionEvent ae ) + { + new Thread( "TrackMate editor thread" ) + { + @Override + public void run() + { + final JRootPane parent = SwingUtilities.getRootPane( ( Component ) ae.getSource() ); + final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( parent, new Class[] { JLabel.class } ); + disabler.disable(); + try + { + final LabkitLauncher launcher = new LabkitLauncher( trackmate, disabler ); + launcher.launch(); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + }; + }.start(); + } + }; + } +} From 43f8687dfae9ab03f71c1067d3e7f07283acaab7 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 1 Sep 2023 18:08:14 +0200 Subject: [PATCH 03/65] Add a button to launch LabKit in the configure views panel of the wizard. But because the other table, trackscheme and bvv buttons take too much place, we don't see it without resizing the window. --- .../trackmate/gui/components/ConfigureViewsPanel.java | 8 +++++++- .../trackmate/gui/wizard/TrackMateWizardSequence.java | 5 ++--- .../gui/wizard/descriptors/ConfigureViewsDescriptor.java | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java index 57a48197d..f1358762a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -79,7 +79,8 @@ public ConfigureViewsPanel( final String spaceUnits, final Action launchTrackSchemeAction, final Action showTrackTablesAction, - final Action showSpotTableAction ) + final Action showSpotTableAction, + final Action launchLabKitAction ) { this.setPreferredSize( new Dimension( 300, 521 ) ); this.setSize( 300, 500 ); @@ -363,6 +364,11 @@ public ConfigureViewsPanel( final JButton btnShowSpotTable = new JButton( showSpotTableAction ); panelButtons.add( btnShowSpotTable ); btnShowSpotTable.setFont( FONT ); + + // Labkit button. + final JButton btnLabKit = new JButton( launchLabKitAction ); + panelButtons.add( btnLabKit ); + btnLabKit.setFont( FONT ); final GridBagConstraints gbcPanelButtons = new GridBagConstraints(); gbcPanelButtons.gridwidth = 2; diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java index 63103ea39..04856c18f 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java @@ -50,6 +50,7 @@ import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.LogPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.editor.LabkitLauncher; import fiji.plugin.trackmate.gui.wizard.descriptors.ActionChooserDescriptor; import fiji.plugin.trackmate.gui.wizard.descriptors.ChooseDetectorDescriptor; import fiji.plugin.trackmate.gui.wizard.descriptors.ChooseTrackerDescriptor; @@ -146,7 +147,7 @@ public TrackMateWizardSequence( final TrackMate trackmate, final SelectionModel chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), trackmate ); executeTrackingDescriptor = new ExecuteTrackingDescriptor( trackmate, logPanel ); trackFilterDescriptor = new TrackFilterDescriptor( trackmate, trackFilters, featureSelector ); - configureViewsDescriptor = new ConfigureViewsDescriptor( displaySettings, featureSelector, new LaunchTrackSchemeAction(), new ShowTrackTablesAction(), new ShowSpotTableAction(), model.getSpaceUnits() ); + configureViewsDescriptor = new ConfigureViewsDescriptor( displaySettings, featureSelector, new LaunchTrackSchemeAction(), new ShowTrackTablesAction(), new ShowSpotTableAction(), LabkitLauncher.getLaunchAction( trackmate ), model.getSpaceUnits() ); grapherDescriptor = new GrapherDescriptor( trackmate, selectionModel, displaySettings ); actionChooserDescriptor = new ActionChooserDescriptor( new ActionProvider(), trackmate, selectionModel, displaySettings ); saveDescriptor = new SaveDescriptor( trackmate, displaySettings, this ); @@ -175,7 +176,6 @@ public WizardPanelDescriptor next() return current; } - @Override public WizardPanelDescriptor previous() { @@ -201,7 +201,6 @@ public WizardPanelDescriptor current() return current; } - @Override public WizardPanelDescriptor logDescriptor() { diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java index 1d849386c..7856dd874 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java @@ -39,6 +39,7 @@ public ConfigureViewsDescriptor( final Action launchTrackSchemeAction, final Action showTrackTablesAction, final Action showSpotTableAction, + final Action launchLabkitAction, final String spaceUnits ) { super( KEY ); @@ -48,6 +49,7 @@ public ConfigureViewsDescriptor( spaceUnits, launchTrackSchemeAction, showTrackTablesAction, - showSpotTableAction ); + showSpotTableAction, + launchLabkitAction ); } } From c1e6c09b3be33125d0521724246a8140daff174e Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 7 Jul 2024 12:47:44 +0200 Subject: [PATCH 04/65] Do not use a yet non-existing method in MaskUtils. --- .../trackmate/gui/editor/LabkitLauncher.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index f5491e1ec..d36f7a2e3 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -20,6 +20,7 @@ import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.detection.MaskUtils; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; +import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; import ij.ImagePlus; import net.imagej.ImgPlus; @@ -93,7 +94,7 @@ protected void launch() // Write spots in it. final Iterable< Spot > spotsThisFrame = trackmate.getModel().getSpots().iterable( currentTimePoint, true ); for ( final Spot spot : spotsThisFrame ) - spot.iterable( lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); // Labeling model. final DefaultSegmentationModel model = new DefaultSegmentationModel( context, input ); @@ -158,7 +159,15 @@ private void modifySpot( final Label label, final Labeling labeling, final Spot final boolean simplify = true; final int numThreads = 1; final RandomAccessibleInterval< UnsignedByteType > qualityImage = null; - final List< Spot > spots = MaskUtils.fromMaskWithROI( region, region, calibration, simplify, numThreads, qualityImage ); + final double threshold = 0.5; + final List< Spot > spots = MaskUtils.fromThresholdWithROI( + region, + region, + calibration, + threshold, + simplify, + numThreads, + qualityImage ); // Time position. for ( final Spot s : spots ) @@ -231,7 +240,15 @@ private void addNewSpot( final Label label, final Labeling labeling ) // Slice by time. final RandomAccessibleInterval< BitType > region = labeling.getRegion( label ); - final List< Spot > spots = MaskUtils.fromMaskWithROI( region, region, calibration, simplify, numThreads, qualityImage ); + final double threshold = 0.5; + final List< Spot > spots = MaskUtils.fromThresholdWithROI( + region, + region, + calibration, + threshold, + simplify, + numThreads, + qualityImage ); for ( final Spot spot : spots ) { spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); From bb139c2ceb477ea946c6a4e4aec2db3dbed92bab Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sat, 2 Sep 2023 18:34:50 +0200 Subject: [PATCH 05/65] Wrap the button line of the configure views panel. Otherwise the button for the editor is not visible without resizing the window. I also had to programmatically resize the main TrackMate frame after display so that components are properly aligned. Such a hack for something simple... Also give the editor button a proper name and a temporary icon. --- .../plugin/trackmate/LoadTrackMatePlugIn.java | 12 +- .../gui/components/ConfigureViewsPanel.java | 11 +- .../plugin/trackmate/util/WrapLayout.java | 117 ++++++++++++++++++ 3 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 src/main/java/fiji/plugin/trackmate/util/WrapLayout.java diff --git a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java index 840076e19..78aee0a46 100644 --- a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java @@ -24,6 +24,7 @@ import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; import java.awt.Color; +import java.awt.Dimension; import java.io.File; import javax.swing.JFrame; @@ -65,8 +66,6 @@ public class LoadTrackMatePlugIn extends TrackMatePlugIn @Override public void run( final String filePath ) { -// GuiUtils.setSystemLookAndFeel(); - final Logger logger = Logger.IJ_LOGGER; File file; if ( null == filePath || filePath.length() == 0 ) @@ -215,6 +214,8 @@ public void run( final String filePath ) frame.setIconImage( TRACKMATE_ICON.getImage() ); GuiUtils.positionWindow( frame, settings.imp.getWindow() ); frame.setVisible( true ); + final Dimension size = frame.getSize(); + frame.setSize( size.width, size.height + 1 ); // Text final LogPanelDescriptor2 logDescriptor = ( LogPanelDescriptor2 ) sequence.logDescriptor(); @@ -230,7 +231,7 @@ public void run( final String filePath ) final String warning = reader.getErrorMessage(); if ( !warning.isEmpty() ) { - logger2.log( "Warnings occured during reading the file:\n" + logger2.log( "Warnings occurred during reading the file:\n" + "--------------------\n" + warning + "--------------------\n", @@ -292,11 +293,12 @@ protected TmXmlReader createReader( final File lFile ) public static void main( final String[] args ) { + GuiUtils.setSystemLookAndFeel(); ImageJ.main( args ); final LoadTrackMatePlugIn plugIn = new LoadTrackMatePlugIn(); - plugIn.run( null ); +// plugIn.run( null ); // plugIn.run( "samples/FakeTracks.xml" ); -// plugIn.run( "samples/MAX_Merged.xml" ); + plugIn.run( "samples/MAX_Merged.xml" ); // plugIn.run( "c:/Users/tinevez/Development/TrackMateWS/TrackMate-Cellpose/samples/R2_multiC.xml" ); // plugIn.run( "/Users/tinevez/Desktop/230901_DeltaRcsB-ZipA-mCh_timestep5min_Stage9_reg/230901_DeltaRcsB-ZipA-mCh_timestep5min_Stage9_reg_merge65.xml" ); } diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java index f1358762a..fd82589d0 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -50,10 +50,12 @@ import javax.swing.border.LineBorder; import fiji.plugin.trackmate.gui.GuiUtils; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.ConfigTrackMateDisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; +import fiji.plugin.trackmate.util.WrapLayout; /** * A configuration panel used to tune the aspect of spots and tracks in multiple @@ -327,7 +329,7 @@ public ConfigureViewsPanel( final GridBagConstraints gbcPanelDrawingZDepth = new GridBagConstraints(); gbcPanelDrawingZDepth.gridwidth = 2; gbcPanelDrawingZDepth.insets = new Insets( 0, 5, 5, 5 ); - gbcPanelDrawingZDepth.fill = GridBagConstraints.BOTH; + gbcPanelDrawingZDepth.fill = GridBagConstraints.HORIZONTAL; gbcPanelDrawingZDepth.gridx = 0; gbcPanelDrawingZDepth.gridy = 5; add( panelDrawingZDepth, gbcPanelDrawingZDepth ); @@ -350,6 +352,7 @@ public ConfigureViewsPanel( */ final JPanel panelButtons = new JPanel(); + panelButtons.setLayout( new WrapLayout() ); // TrackScheme button. final JButton btnShowTrackScheme = new JButton( launchTrackSchemeAction ); @@ -367,9 +370,12 @@ public ConfigureViewsPanel( // Labkit button. final JButton btnLabKit = new JButton( launchLabKitAction ); - panelButtons.add( btnLabKit ); btnLabKit.setFont( FONT ); + btnLabKit.setText( "Launch spot editor" ); + btnLabKit.setIcon( Icons.PENCIL_ICON ); + panelButtons.add( btnLabKit ); + panelButtons.setSize( new Dimension( 300, 1 ) ); final GridBagConstraints gbcPanelButtons = new GridBagConstraints(); gbcPanelButtons.gridwidth = 2; gbcPanelButtons.anchor = GridBagConstraints.SOUTH; @@ -377,6 +383,7 @@ public ConfigureViewsPanel( gbcPanelButtons.gridx = 0; gbcPanelButtons.gridy = 7; add( panelButtons, gbcPanelButtons ); + setSize( new Dimension( 300, 1 ) ); /* * Listeners & co. diff --git a/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java b/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java new file mode 100644 index 000000000..ccd1bac73 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java @@ -0,0 +1,117 @@ +package fiji.plugin.trackmate.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +public class WrapLayout extends FlowLayout +{ + private static final long serialVersionUID = 1L; + + public WrapLayout() + { + super(); + } + + public WrapLayout( final int align ) + { + super( align ); + } + + public WrapLayout( final int align, final int hgap, final int vgap ) + { + super( align, hgap, vgap ); + } + + @Override + public Dimension preferredLayoutSize( final Container target ) + { + return layoutSize( target, true ); + } + + @Override + public Dimension minimumLayoutSize( final Container target ) + { + final Dimension minimum = layoutSize( target, false ); + minimum.width -= ( getHgap() + 1 ); + return minimum; + } + + private Dimension layoutSize( final Container target, final boolean preferred ) + { + synchronized ( target.getTreeLock() ) + { + int targetWidth = target.getSize().width; + + if ( targetWidth == 0 ) + targetWidth = Integer.MAX_VALUE; + + final int hgap = getHgap(); + final int vgap = getVgap(); + final Insets insets = target.getInsets(); + final int horizontalInsetsAndGap = insets.left + insets.right + ( hgap * 2 ); + final int maxWidth = targetWidth - horizontalInsetsAndGap; + + final Dimension dim = new Dimension( 0, 0 ); + int rowWidth = 0; + int rowHeight = 0; + + final int nmembers = target.getComponentCount(); + + for ( int i = 0; i < nmembers; i++ ) + { + final Component m = target.getComponent( i ); + + if ( m.isVisible() ) + { + final Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + if ( rowWidth + d.width > maxWidth ) + { + addRow( dim, rowWidth, rowHeight ); + rowWidth = 0; + rowHeight = 0; + } + + if ( rowWidth != 0 ) + { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max( rowHeight, d.height ); + } + } + + addRow( dim, rowWidth, rowHeight ); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + final Container scrollPane = SwingUtilities.getAncestorOfClass( JScrollPane.class, target ); + if ( scrollPane != null ) + { + dim.width -= ( hgap + 1 ); + } + + return dim; + } + } + + private void addRow( final Dimension dim, final int rowWidth, final int rowHeight ) + { + dim.width = Math.max( dim.width, rowWidth ); + + if ( dim.height > 0 ) + { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } +} From 2815d6324d62b69fb6da80041b062a9605580c5d Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sat, 2 Sep 2023 18:44:00 +0200 Subject: [PATCH 06/65] Rework and improve the import method of the spot editor. - We don't depend on labels anymore, but directly operate and compare the index images (before modification and after). Because the index is directly related to the spot ID, we can get a match from previous spot to novel spot in an easy manner. - The spots from the edited version are created directly from the novel index image, using something adapted from the label image detector code, so again, just one pass. We use the fact that we can provide it with a 'quality' image, and read the index of the label image 'under' the spot and write it into its quality value. This way we can retrieve the id of the matching previous spot in an easy manner. - The price to pay for not working with labels anymore is that we don't have access to the label name, but that's life. - We make only one pass over the image to collect the ids of the spots that have been modified, instead of one pass per spot. Also, this pass is multithreaded (thanks LoopBuilder). - I have also learned that I should not use weakListeners() if I am doing something with threads inside the listener. Using listeners() instead works, but I do not know why the other one does not. Probably something arcane with Java WeakReferences being collected. - As a result of all this the performance is much better than before and the 'return to TrackMate' should happen without the user noticing the process. --- .../trackmate/gui/editor/LabkitLauncher.java | 259 +++++++++++------- 1 file changed, 159 insertions(+), 100 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index d36f7a2e3..7a46f7974 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -2,8 +2,14 @@ import java.awt.Component; import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JLabel; import javax.swing.JRootPane; @@ -30,13 +36,12 @@ import net.imglib2.img.Img; import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImgPlusViews; -import net.imglib2.test.ImgLib2Assert; -import net.imglib2.type.logic.BitType; -import net.imglib2.type.numeric.integer.UnsignedByteType; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.view.Views; import sc.fiji.labkit.ui.LabkitFrame; import sc.fiji.labkit.ui.inputimage.DatasetInputImage; -import sc.fiji.labkit.ui.labeling.Label; import sc.fiji.labkit.ui.labeling.Labeling; import sc.fiji.labkit.ui.models.DefaultSegmentationModel; import sc.fiji.labkit.ui.models.ImageLabelingModel; @@ -66,13 +71,11 @@ public LabkitLauncher( final TrackMate trackmate, final EverythingDisablerAndRee this.calibration = TMUtils.getSpatialCalibration( imp ); this.is2D = DetectionUtils.is2D( imp ); this.dt = imp.getCalibration().frameInterval; - } @SuppressWarnings( { "rawtypes", "unchecked" } ) protected void launch() { - final Context context = TMUtils.getContext(); final ImagePlus imp = trackmate.getSettings().imp; final ImgPlus src = TMUtils.rawWraps( imp ); final int timeAxis = src.dimensionIndex( Axes.TIME ); @@ -91,12 +94,13 @@ protected void launch() final double[] calibration = TMUtils.getSpatialCalibration( imp ); final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); - // Write spots in it. + // Write spots in it with index = id + 1 final Iterable< Spot > spotsThisFrame = trackmate.getModel().getSpots().iterable( currentTimePoint, true ); for ( final Spot spot : spotsThisFrame ) SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); - // Labeling model. + // Make a labeling model from it. + final Context context = TMUtils.getContext(); final DefaultSegmentationModel model = new DefaultSegmentationModel( context, input ); model.imageLabelingModel().labeling().set( Labeling.fromImg( lblImgPlus ) ); @@ -106,34 +110,86 @@ protected void launch() // Show LabKit. final LabkitFrame labkit = LabkitFrame.show( model, "Edit TrackMate data frame " + ( currentTimePoint + 1 ) ); - labkit.onCloseListeners().addWeakListener( () -> reimportData( model.imageLabelingModel(), currentTimePoint ) ); + labkit.onCloseListeners().addListener( () -> reimportData( model.imageLabelingModel(), currentTimePoint ) ); } + @SuppressWarnings( "unchecked" ) private void reimportData( final ImageLabelingModel lm, final int currentTimePoint ) { final Model model = trackmate.getModel(); - final SpotCollection spots = model.getSpots(); - final Labeling labeling = lm.labeling().get(); - final List< Label > labels = labeling.getLabels(); model.beginUpdate(); try { - for ( final Label label : labels ) + /* + * We will update the spots using a comparison based on only the + * index images. + */ + final Labeling labeling = lm.labeling().get(); + final RandomAccessibleInterval< UnsignedShortType > novelIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) labeling.getIndexImg(); + final RandomAccessibleInterval< UnsignedShortType > previousIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) previousLabels.getIndexImg(); + + // Map of previous spots against their ID: + final SpotCollection spots = model.getSpots(); + final Map< Integer, Spot > previousSpotIDs = new HashMap<>(); + spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); + + /* + * Get all the spots present in the new image. Because we specified + * the novel label image as 'quality' image, they have a quality + * value equal to the index in the label image (id+1). + */ + final List< Spot > novelSpots = getSpots( novelIndexImg ); + + /* + * Map of novel spots against the ID taken from the index of the + * novel label image. Normally, this index, and hence the novel id, + * corresponds to the id of previous spots. If one of the novel spot + * has an id we cannot find in the previous spot list, it means that + * it is a new one. + * + * Careful! The user might have created several connected components + * with the same label in LabKit, which will result in having + * several spots with the same quality value. We don't want to loose + * them, so the map is that of a id to a list of spots. + */ + final Map< Integer, List< Spot > > novelSpotIDs = new HashMap<>(); + novelSpots.forEach( s -> { + final int id = Integer.valueOf( s.getFeature( Spot.QUALITY ).intValue() - 1 ); + final List< Spot > list = novelSpotIDs.computeIfAbsent( Integer.valueOf( id ), ( i ) -> new ArrayList<>() ); + list.add( s ); + } ); + + // Collect ids of spots that have been modified. id = index - 1 + final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); + + // Update model for those spots. + for ( final int id : modifiedIDs ) { - - try + final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); + final List< Spot > novelSpotList = novelSpotIDs.get( Integer.valueOf( id ) ); + if ( previousSpot == null ) { - final int id = Integer.parseInt( label.name() ) - 1; - final Spot spot = spots.search( id ); - if ( spot == null ) - addNewSpot( label, labeling ); - else - modifySpot( label, labeling, spot ); - + /* + * A new one (possible several) I cannot find in the + * previous list -> add as a new spot. + */ + addNewSpot( novelSpotList ); + } + else if ( novelSpotList == null || novelSpotList.isEmpty() ) + { + /* + * One I add in the previous spot list, but that has + * disappeared. Remove it. + */ + model.removeSpot( previousSpot ); } - catch ( final NumberFormatException nfe ) + else { - addNewSpot( label, labeling ); + /* + * I know of them both. Treat the case as if the previous + * spot was modified. + */ + modifySpot( novelSpotList, previousSpot ); } } } @@ -148,114 +204,117 @@ private void reimportData( final ImageLabelingModel lm, final int currentTimePoi } } - private void modifySpot( final Label label, final Labeling labeling, final Spot spot ) + private void modifySpot( final List< Spot > novelSpotList, final Spot previousSpot ) { - final Label previousLabel = previousLabels.getLabel( label.name() ); - final RandomAccessibleInterval< BitType > previousRegion = previousLabels.getRegion( previousLabel ); - final RandomAccessibleInterval< BitType > region = labeling.getRegion( label ); - if ( !hasChanged( region, previousRegion ) ) - return; - - final boolean simplify = true; - final int numThreads = 1; - final RandomAccessibleInterval< UnsignedByteType > qualityImage = null; - final double threshold = 0.5; - final List< Spot > spots = MaskUtils.fromThresholdWithROI( - region, - region, - calibration, - threshold, - simplify, - numThreads, - qualityImage ); - - // Time position. - for ( final Spot s : spots ) - s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); - - // If there is no spot, it's because we removed it. final Model model = trackmate.getModel(); - if ( spots.isEmpty() ) + + /* + * Hopefully there is only one spot in the novel spot list. If not we + * privilege the one closest to the previous spots. + */ + final Spot mainNovelSpot; + if ( novelSpotList.size() == 1 ) { - model.removeSpot( spot ); - return; + mainNovelSpot = novelSpotList.get( 0 ); } - // Hopefully there is only one, if not we pick the closest one. - Spot closest = null; - double minD2 = Double.POSITIVE_INFINITY; - for ( final Spot s : spots ) + else { - final double d2 = s.squareDistanceTo( spot ); - if ( d2 < minD2 ) + Spot closest = null; + double minD2 = Double.POSITIVE_INFINITY; + for ( final Spot s : novelSpotList ) { - minD2 = d2; - closest = s; + final double d2 = s.squareDistanceTo( previousSpot ); + if ( d2 < minD2 ) + { + minD2 = d2; + closest = s; + } } + mainNovelSpot = closest; } - closest.setName( spot.getName() ); - model.addSpotTo( closest, Integer.valueOf( currentTimePoint ) ); - final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( spot ); + + // Add it properly. + mainNovelSpot.setName( previousSpot.getName() ); + mainNovelSpot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + model.addSpotTo( mainNovelSpot, Integer.valueOf( currentTimePoint ) ); + // Recreate links. + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( previousSpot ); for ( final DefaultWeightedEdge e : edges ) { final double weight = model.getTrackModel().getEdgeWeight( e ); final Spot source = model.getTrackModel().getEdgeSource( e ); final Spot target = model.getTrackModel().getEdgeTarget( e ); - if ( source == spot ) - model.addEdge( closest, target, weight ); - else if ( target == spot ) - model.addEdge( source, closest, weight ); + if ( source == previousSpot ) + model.addEdge( mainNovelSpot, target, weight ); + else if ( target == previousSpot ) + model.addEdge( source, mainNovelSpot, weight ); else throw new IllegalArgumentException( "The edge of a spot does not have the spot as source or target?!?" ); } - model.removeSpot( spot ); + model.removeSpot( previousSpot ); - // If not, add the other ones as new ones. - for ( int i = 1; i < spots.size(); i++ ) + // Deal with the other ones. + final HashSet< Spot > extraSpots = new HashSet<>( novelSpotList ); + extraSpots.remove( mainNovelSpot ); + int i = 1; + for ( final Spot s: extraSpots ) { - final Spot s = spots.get( i ); - s.setName( spot.getName() + "_" + i ); + s.setName( previousSpot.getName() + "_" + i++ ); + s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); } } - private static final boolean hasChanged( final RandomAccessibleInterval< BitType > region, final RandomAccessibleInterval< BitType > previousRegion ) + private void addNewSpot( final List< Spot > novelSpotList ) { - try + for ( final Spot spot : novelSpotList ) { - ImgLib2Assert.assertImageEquals( region, previousRegion ); - } - catch ( final AssertionError e ) - { - return true; + spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + trackmate.getModel().addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); } - return false; } + private static final Set< Integer > getModifiedIDs( final RandomAccessibleInterval< UnsignedShortType > novelIndexImg, final RandomAccessibleInterval< UnsignedShortType > previousIndexImg ) + { + final ConcurrentSkipListSet< Integer > modifiedIDs = new ConcurrentSkipListSet<>(); + LoopBuilder.setImages( novelIndexImg, previousIndexImg ) + .multiThreaded( false ) + .forEachPixel( ( c, p ) -> { + final int ci = c.get(); + final int pi = p.get(); + if ( ci == 0 && pi == 0 ) + return; + if ( ci != pi ) + { + modifiedIDs.add( Integer.valueOf( pi - 1 ) ); + modifiedIDs.add( Integer.valueOf( ci - 1 ) ); + } + } ); + modifiedIDs.remove( Integer.valueOf( -1 ) ); + return modifiedIDs; + } - private void addNewSpot( final Label label, final Labeling labeling ) + private List< Spot > getSpots( final RandomAccessibleInterval< UnsignedShortType > rai ) { + // Get all labels. + final AtomicInteger max = new AtomicInteger( 0 ); + Views.iterable( rai ).forEach( p -> { + final int val = p.getInteger(); + if ( val != 0 && val > max.get() ) + max.set( val ); + } ); + final List< Integer > indices = new ArrayList<>( max.get() ); + for ( int i = 0; i < max.get(); i++ ) + indices.add( Integer.valueOf( i + 1 ) ); + + final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); final boolean simplify = true; - final int numThreads = 1; - final RandomAccessibleInterval< UnsignedByteType > qualityImage = null; - - // Slice by time. - final RandomAccessibleInterval< BitType > region = labeling.getRegion( label ); - final double threshold = 0.5; - final List< Spot > spots = MaskUtils.fromThresholdWithROI( - region, - region, + return MaskUtils.fromLabelingWithROI( + labeling, + labeling, calibration, - threshold, simplify, - numThreads, - qualityImage ); - for ( final Spot spot : spots ) - { - spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); - spot.setName( label.name() ); - trackmate.getModel().addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); - } - + rai ); } public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate ) From 0fd41934fb68d46d54f20b0fe26c36585f9d7c51 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sat, 2 Sep 2023 19:12:57 +0200 Subject: [PATCH 07/65] Utility to know whether a class is present at runtime. I wanted to use it to check whether the user has activated the 'LabKit' update site, but apparently the labkit jars are included with vanilla Fiji. --- .../fiji/plugin/trackmate/util/TMUtils.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java index 0550f0adb..2896511f5 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java +++ b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java @@ -865,6 +865,30 @@ public static String getImagePathWithoutExtension( final Settings settings ) } } + /** + * Returns true if the class with the fully qualified name is + * present at runtime. This is useful to detect whether a certain update + * site has been activated in Fiji and to enable or disable component based + * on this. + * + * @param className + * the fully qualified class name, e.g. + * "sc.fiji.labkit.ui.LabkitFrame" + * @return true if the the class with the specified name is + * present, false otherwise. + */ + public static boolean isClassPresent( final String className ) + { + try + { + Class.forName( className, false, TMUtils.class.getClassLoader() ); + return true; + } + catch ( final ClassNotFoundException e1 ) + {} + return false; + } + private TMUtils() {} } From a73f8327d5e18d3f4350d2d7c956ee950855199e Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sat, 2 Sep 2023 19:13:19 +0200 Subject: [PATCH 08/65] Make the editor button visible only if LabKit is available. --- .../gui/components/ConfigureViewsPanel.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java index fd82589d0..e537d96e3 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -55,6 +55,7 @@ import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; +import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.util.WrapLayout; /** @@ -369,11 +370,20 @@ public ConfigureViewsPanel( btnShowSpotTable.setFont( FONT ); // Labkit button. - final JButton btnLabKit = new JButton( launchLabKitAction ); - btnLabKit.setFont( FONT ); - btnLabKit.setText( "Launch spot editor" ); - btnLabKit.setIcon( Icons.PENCIL_ICON ); - panelButtons.add( btnLabKit ); + // Is labkit available? + if ( TMUtils.isClassPresent( "sc.fiji.labkit.ui.LabkitFrame" ) ) + { + System.out.println( "LabKit found." ); // DEBUG + final JButton btnLabKit = new JButton( launchLabKitAction ); + btnLabKit.setFont( FONT ); + btnLabKit.setText( "Launch spot editor" ); + btnLabKit.setIcon( Icons.PENCIL_ICON ); + panelButtons.add( btnLabKit ); + } + else + { + System.out.println( "LabKit not found." ); // DEBUG + } panelButtons.setSize( new Dimension( 300, 1 ) ); final GridBagConstraints gbcPanelButtons = new GridBagConstraints(); From d7b25d6b7c0e14fde06c66a5d7ae57d3cdbbebaf Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Sun, 3 Sep 2023 17:38:08 +0200 Subject: [PATCH 09/65] After editing in LbKit, message the user Let them choose to discard or commit the changes. --- .../trackmate/gui/editor/LabkitLauncher.java | 152 +++++++++++------- 1 file changed, 90 insertions(+), 62 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 7a46f7974..06ac1bdfe 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -12,6 +12,7 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JRootPane; import javax.swing.SwingUtilities; @@ -25,6 +26,7 @@ import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.detection.MaskUtils; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; @@ -116,8 +118,6 @@ protected void launch() @SuppressWarnings( "unchecked" ) private void reimportData( final ImageLabelingModel lm, final int currentTimePoint ) { - final Model model = trackmate.getModel(); - model.beginUpdate(); try { /* @@ -128,70 +128,99 @@ private void reimportData( final ImageLabelingModel lm, final int currentTimePoi final RandomAccessibleInterval< UnsignedShortType > novelIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) labeling.getIndexImg(); final RandomAccessibleInterval< UnsignedShortType > previousIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) previousLabels.getIndexImg(); - // Map of previous spots against their ID: - final SpotCollection spots = model.getSpots(); - final Map< Integer, Spot > previousSpotIDs = new HashMap<>(); - spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); - - /* - * Get all the spots present in the new image. Because we specified - * the novel label image as 'quality' image, they have a quality - * value equal to the index in the label image (id+1). - */ - final List< Spot > novelSpots = getSpots( novelIndexImg ); - - /* - * Map of novel spots against the ID taken from the index of the - * novel label image. Normally, this index, and hence the novel id, - * corresponds to the id of previous spots. If one of the novel spot - * has an id we cannot find in the previous spot list, it means that - * it is a new one. - * - * Careful! The user might have created several connected components - * with the same label in LabKit, which will result in having - * several spots with the same quality value. We don't want to loose - * them, so the map is that of a id to a list of spots. - */ - final Map< Integer, List< Spot > > novelSpotIDs = new HashMap<>(); - novelSpots.forEach( s -> { - final int id = Integer.valueOf( s.getFeature( Spot.QUALITY ).intValue() - 1 ); - final List< Spot > list = novelSpotIDs.computeIfAbsent( Integer.valueOf( id ), ( i ) -> new ArrayList<>() ); - list.add( s ); - } ); - // Collect ids of spots that have been modified. id = index - 1 final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); - - // Update model for those spots. - for ( final int id : modifiedIDs ) + final int nModified = modifiedIDs.size(); + + if ( nModified == 0 ) + return; + + // Message the user. + final String msg = + "Commit the changes made to the\n" + + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; + final String title = "Commit edits to TrackMate"; + final int returnedValue = JOptionPane.showConfirmDialog( + null, + msg, + title, + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + Icons.TRACKMATE_ICON ); + if ( returnedValue != JOptionPane.YES_OPTION ) + return; + + final Model model = trackmate.getModel(); + model.beginUpdate(); + try { - final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); - final List< Spot > novelSpotList = novelSpotIDs.get( Integer.valueOf( id ) ); - if ( previousSpot == null ) - { - /* - * A new one (possible several) I cannot find in the - * previous list -> add as a new spot. - */ - addNewSpot( novelSpotList ); - } - else if ( novelSpotList == null || novelSpotList.isEmpty() ) - { - /* - * One I add in the previous spot list, but that has - * disappeared. Remove it. - */ - model.removeSpot( previousSpot ); - } - else + // Map of previous spots against their ID: + final SpotCollection spots = model.getSpots(); + final Map< Integer, Spot > previousSpotIDs = new HashMap<>(); + spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); + + /* + * Get all the spots present in the new image. Because we + * specified the novel label image as 'quality' image, they have + * a quality value equal to the index in the label image (id+1). + */ + final List< Spot > novelSpots = getSpots( novelIndexImg ); + + /* + * Map of novel spots against the ID taken from the index of the + * novel label image. Normally, this index, and hence the novel + * id, corresponds to the id of previous spots. If one of the + * novel spot has an id we cannot find in the previous spot + * list, it means that it is a new one. + * + * Careful! The user might have created several connected + * components with the same label in LabKit, which will result + * in having several spots with the same quality value. We don't + * want to loose them, so the map is that of a id to a list of + * spots. + */ + final Map< Integer, List< Spot > > novelSpotIDs = new HashMap<>(); + novelSpots.forEach( s -> { + final int id = Integer.valueOf( s.getFeature( Spot.QUALITY ).intValue() - 1 ); + final List< Spot > list = novelSpotIDs.computeIfAbsent( Integer.valueOf( id ), ( i ) -> new ArrayList<>() ); + list.add( s ); + } ); + + // Update model for those spots. + for ( final int id : modifiedIDs ) { - /* - * I know of them both. Treat the case as if the previous - * spot was modified. - */ - modifySpot( novelSpotList, previousSpot ); + final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); + final List< Spot > novelSpotList = novelSpotIDs.get( Integer.valueOf( id ) ); + if ( previousSpot == null ) + { + /* + * A new one (possible several) I cannot find in the + * previous list -> add as a new spot. + */ + addNewSpot( novelSpotList ); + } + else if ( novelSpotList == null || novelSpotList.isEmpty() ) + { + /* + * One I add in the previous spot list, but that has + * disappeared. Remove it. + */ + model.removeSpot( previousSpot ); + } + else + { + /* + * I know of them both. Treat the case as if the + * previous spot was modified. + */ + modifySpot( novelSpotList, previousSpot ); + } } } + finally + { + model.endUpdate(); + } } catch ( final Exception e ) { @@ -199,7 +228,6 @@ else if ( novelSpotList == null || novelSpotList.isEmpty() ) } finally { - model.endUpdate(); disabler.reenable(); } } @@ -257,7 +285,7 @@ else if ( target == previousSpot ) final HashSet< Spot > extraSpots = new HashSet<>( novelSpotList ); extraSpots.remove( mainNovelSpot ); int i = 1; - for ( final Spot s: extraSpots ) + for ( final Spot s : extraSpots ) { s.setName( previousSpot.getName() + "_" + i++ ); s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); From 6c23066a04fe558a273dda457b080072aad310f9 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Sun, 3 Sep 2023 18:35:50 +0200 Subject: [PATCH 10/65] Copy the channel color, min & max, etc to the editor BDV window. So that the image in the LabKit window opens with the same display settings than in the ImagePlus main view. --- .../trackmate/gui/editor/ImpBdvShowable.java | 177 ++++++++++++++++++ .../trackmate/gui/editor/LabkitLauncher.java | 3 +- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java new file mode 100644 index 000000000..b37bf9d66 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -0,0 +1,177 @@ +package fiji.plugin.trackmate.gui.editor; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import bdv.tools.brightness.ConverterSetup; +import bdv.util.AxisOrder; +import bdv.util.BdvFunctions; +import bdv.util.BdvOptions; +import bdv.util.BdvStackSource; +import bdv.viewer.DisplayMode; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SynchronizedViewerState; +import bdv.viewer.ViewerState; +import ij.CompositeImage; +import ij.IJ; +import ij.ImagePlus; +import ij.process.LUT; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.Interval; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.NumericType; +import sc.fiji.labkit.ui.bdv.BdvShowable; + +/** + * A {@link BdvShowable} from a {@link ImgPlus}, but with channel colors, min & + * max, channel visibility and display mode taken from a specified + * {@link ImagePlus}. + *

+ * Adapted from Matthias Arzt' ImgPlusBdvShowable, reusing code I made for + * Mastodon support of IJ ImagePlus. + * + * @author Jean-Yves Tinevez + */ +public class ImpBdvShowable implements BdvShowable +{ + + public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPlus< T > frame, final ImagePlus imp ) + { + return new ImpBdvShowable( frame, imp ); + } + + private final ImgPlus< ? extends NumericType< ? > > image; + + private final ImagePlus imp; + + ImpBdvShowable( final ImgPlus< ? extends NumericType< ? > > image, final ImagePlus imp ) + { + this.image = image; + this.imp = imp; + } + + @Override + public Interval interval() + { + return image; + } + + @Override + public AffineTransform3D transformation() + { + final AffineTransform3D transform = new AffineTransform3D(); + transform.set( + getCalibration( Axes.X ), 0, 0, 0, + 0, getCalibration( Axes.Y ), 0, 0, + 0, 0, getCalibration( Axes.Z ), 0 ); + return transform; + } + + @Override + public BdvStackSource< ? > show( final String title, final BdvOptions options ) + { + final String name = image.getName(); + final BdvOptions options1 = options.axisOrder( getAxisOrder() ).sourceTransform( transformation() ); + final BdvStackSource< ? extends NumericType< ? > > stackSource = BdvFunctions.show( image, name == null + ? title : name, options1 ); + + final List< ConverterSetup > converterSetups = stackSource.getConverterSetups(); + final SynchronizedViewerState state = stackSource.getBdvHandle().getViewerPanel().state(); + + final int numActiveChannels = transferChannelVisibility( state ); + transferChannelSettings( converterSetups ); + state.setDisplayMode( numActiveChannels > 1 ? DisplayMode.FUSED : DisplayMode.SINGLE ); + return stackSource; + } + + private AxisOrder getAxisOrder() + { + final String code = IntStream + .range( 0, image.numDimensions() ) + .mapToObj( i -> image + .axis( i ) + .type() + .getLabel().substring( 0, 1 ) ) + .collect( Collectors.joining() ); + try + { + return AxisOrder.valueOf( code ); + } + catch ( final IllegalArgumentException e ) + { + return AxisOrder.DEFAULT; + } + } + + private double getCalibration( final AxisType axisType ) + { + final int d = image.dimensionIndex( axisType ); + if ( d == -1 ) + return 1; + return image.axis( d ).averageScale( image.min( d ), image.max( d ) ); + } + + private int transferChannelVisibility( final ViewerState state ) + { + final int nChannels = imp.getNChannels(); + final CompositeImage ci = imp.isComposite() ? ( CompositeImage ) imp : null; + final List< SourceAndConverter< ? > > sources = state.getSources(); + if ( ci != null && ci.getCompositeMode() == IJ.COMPOSITE ) + { + final boolean[] activeChannels = ci.getActiveChannels(); + int numActiveChannels = 0; + for ( int i = 0; i < Math.min( activeChannels.length, nChannels ); ++i ) + { + final SourceAndConverter< ? > source = sources.get( i ); + state.setSourceActive( source, activeChannels[ i ] ); + state.setCurrentSource( source ); + numActiveChannels += activeChannels[ i ] ? 1 : 0; + } + return numActiveChannels; + } + else + { + final int activeChannel = imp.getChannel() - 1; + for ( int i = 0; i < nChannels; ++i ) + state.setSourceActive( sources.get( i ), i == activeChannel ); + state.setCurrentSource( sources.get( activeChannel ) ); + return 1; + } + } + + private void transferChannelSettings( final List< ConverterSetup > converterSetups ) + { + final int nChannels = imp.getNChannels(); + final CompositeImage ci = imp.isComposite() ? ( CompositeImage ) imp : null; + if ( ci != null ) + { + final int mode = ci.getCompositeMode(); + final boolean transferColor = mode == IJ.COMPOSITE || mode == IJ.COLOR; + for ( int c = 0; c < nChannels; ++c ) + { + final LUT lut = ci.getChannelLut( c + 1 ); + final ConverterSetup setup = converterSetups.get( c ); + if ( transferColor ) + setup.setColor( new ARGBType( lut.getRGB( 255 ) ) ); + setup.setDisplayRange( lut.min, lut.max ); + } + } + else + { + final double displayRangeMin = imp.getDisplayRangeMin(); + final double displayRangeMax = imp.getDisplayRangeMax(); + for ( int c = 0; c < nChannels; ++c ) + { + final ConverterSetup setup = converterSetups.get( c ); + final LUT[] luts = imp.getLuts(); + if ( luts.length != 0 ) + setup.setColor( new ARGBType( luts[ 0 ].getRGB( 255 ) ) ); + setup.setDisplayRange( displayRangeMin, displayRangeMax ); + } + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 06ac1bdfe..b739f374a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -85,7 +85,8 @@ protected void launch() // Reslice for current time-point. this.currentTimePoint = imp.getFrame() - 1; final ImgPlus frame = ImgPlusViews.hyperSlice( src, timeAxis, currentTimePoint ); - final DatasetInputImage input = new DatasetInputImage( frame ); + final ImpBdvShowable showable = ImpBdvShowable.fromImp( frame, imp ); + final DatasetInputImage input = new DatasetInputImage( frame, showable ); // Prepare label image. final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y, Axes.Z, }; From cf486c431b253f8eaf7560b5fac3bffb10773b4e Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Sun, 3 Sep 2023 18:41:14 +0200 Subject: [PATCH 11/65] Directly store the index image as previous labels. We don't need the Labeling wrapper. --- .../plugin/trackmate/gui/editor/LabkitLauncher.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index b739f374a..23b685ca5 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -57,7 +57,7 @@ public class LabkitLauncher private final EverythingDisablerAndReenabler disabler; - private Labeling previousLabels; + private ImgPlus< UnsignedShortType > previousIndexImg; private int currentTimePoint; @@ -108,8 +108,7 @@ protected void launch() model.imageLabelingModel().labeling().set( Labeling.fromImg( lblImgPlus ) ); // Store a copy. - final ImgPlus< UnsignedShortType > copy = lblImgPlus.copy(); - this.previousLabels = Labeling.fromImg( copy ); + this.previousIndexImg = lblImgPlus.copy(); // Show LabKit. final LabkitFrame labkit = LabkitFrame.show( model, "Edit TrackMate data frame " + ( currentTimePoint + 1 ) ); @@ -127,10 +126,9 @@ private void reimportData( final ImageLabelingModel lm, final int currentTimePoi */ final Labeling labeling = lm.labeling().get(); final RandomAccessibleInterval< UnsignedShortType > novelIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) labeling.getIndexImg(); - final RandomAccessibleInterval< UnsignedShortType > previousIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) previousLabels.getIndexImg(); // Collect ids of spots that have been modified. id = index - 1 - final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); + final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg ); final int nModified = modifiedIDs.size(); if ( nModified == 0 ) @@ -303,7 +301,7 @@ private void addNewSpot( final List< Spot > novelSpotList ) } } - private static final Set< Integer > getModifiedIDs( final RandomAccessibleInterval< UnsignedShortType > novelIndexImg, final RandomAccessibleInterval< UnsignedShortType > previousIndexImg ) + private final Set< Integer > getModifiedIDs( final RandomAccessibleInterval< UnsignedShortType > novelIndexImg ) { final ConcurrentSkipListSet< Integer > modifiedIDs = new ConcurrentSkipListSet<>(); LoopBuilder.setImages( novelIndexImg, previousIndexImg ) From 73d096a01b3694014bba54713e597949f0a6551d Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Mon, 4 Sep 2023 13:16:31 +0200 Subject: [PATCH 12/65] Modifed and new spots get a QUALITY value of -1. --- .../java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 23b685ca5..59f3aa58a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -263,6 +263,7 @@ private void modifySpot( final List< Spot > novelSpotList, final Spot previousSp // Add it properly. mainNovelSpot.setName( previousSpot.getName() ); mainNovelSpot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + mainNovelSpot.putFeature( Spot.QUALITY, -1. ); model.addSpotTo( mainNovelSpot, Integer.valueOf( currentTimePoint ) ); // Recreate links. final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( previousSpot ); @@ -288,6 +289,7 @@ else if ( target == previousSpot ) { s.setName( previousSpot.getName() + "_" + i++ ); s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + s.putFeature( Spot.QUALITY, -1. ); model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); } } @@ -297,6 +299,7 @@ private void addNewSpot( final List< Spot > novelSpotList ) for ( final Spot spot : novelSpotList ) { spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + spot.putFeature( Spot.QUALITY, -1. ); trackmate.getModel().addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); } } From 9de374b40839c54f6adfbf770757474d3d3ad87b Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Tue, 12 Sep 2023 14:11:36 +0200 Subject: [PATCH 13/65] Fix editing spots in 3D. The LabKit editor worked fine foe us but only in 2D. In 3D the labels imported from the spots into LabKit were off along the Z axis, as if the Z caibration was not handled properly. This was caused by the custom BDV showable I made missing some important preparation steps. I simply copied these steps from the working version of BDVShowabble in the core LabKit code. --- .../trackmate/gui/editor/ImpBdvShowable.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index b37bf9d66..287417135 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -1,5 +1,6 @@ package fiji.plugin.trackmate.gui.editor; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -25,6 +26,7 @@ import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.NumericType; import sc.fiji.labkit.ui.bdv.BdvShowable; +import sc.fiji.labkit.ui.inputimage.ImgPlusViewsOld; /** * A {@link BdvShowable} from a {@link ImgPlus}, but with channel colors, min & @@ -41,7 +43,7 @@ public class ImpBdvShowable implements BdvShowable public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPlus< T > frame, final ImagePlus imp ) { - return new ImpBdvShowable( frame, imp ); + return new ImpBdvShowable( prepareImage( frame ), imp ); } private final ImgPlus< ? extends NumericType< ? > > image; @@ -174,4 +176,25 @@ private void transferChannelSettings( final List< ConverterSetup > converterSetu } } } + + private static ImgPlus< ? extends NumericType< ? > > prepareImage( + final ImgPlus< ? extends NumericType< ? > > image ) + { + final List< AxisType > order = Arrays.asList( Axes.X, Axes.Y, Axes.Z, Axes.CHANNEL, + Axes.TIME ); + return ImgPlusViewsOld.sortAxes( labelAxes( image ), order ); + } + + private static ImgPlus< ? extends NumericType< ? > > labelAxes( + final ImgPlus< ? extends NumericType< ? > > image ) + { + if ( image.firstElement() instanceof ARGBType ) + return ImgPlusViewsOld + .fixAxes( image, Arrays.asList( Axes.X, Axes.Y, Axes.Z, Axes.TIME ) ); + if ( image.numDimensions() == 4 ) + return ImgPlusViewsOld.fixAxes( image, Arrays + .asList( Axes.X, Axes.Y, Axes.Z, Axes.TIME, Axes.CHANNEL ) ); + return ImgPlusViewsOld.fixAxes( image, Arrays.asList( Axes.X, Axes.Y, Axes.Z, + Axes.CHANNEL, Axes.TIME ) ); + } } From 3f7dc783926bd80025dbd3f328f19e62103bdaec Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 17 Sep 2023 11:53:08 +0200 Subject: [PATCH 14/65] ImpBdvShowable wraps around the ImgPlusBdvShowable. As suggested by Matthias. --- .../trackmate/gui/editor/ImpBdvShowable.java | 63 +++---------------- 1 file changed, 10 insertions(+), 53 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index 287417135..8a5ffe1b1 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -2,12 +2,8 @@ import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; import bdv.tools.brightness.ConverterSetup; -import bdv.util.AxisOrder; -import bdv.util.BdvFunctions; import bdv.util.BdvOptions; import bdv.util.BdvStackSource; import bdv.viewer.DisplayMode; @@ -43,44 +39,32 @@ public class ImpBdvShowable implements BdvShowable public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPlus< T > frame, final ImagePlus imp ) { - return new ImpBdvShowable( prepareImage( frame ), imp ); + return new ImpBdvShowable( BdvShowable.wrap( prepareImage( frame ) ), imp ); } - private final ImgPlus< ? extends NumericType< ? > > image; + private final BdvShowable showable; private final ImagePlus imp; - ImpBdvShowable( final ImgPlus< ? extends NumericType< ? > > image, final ImagePlus imp ) + ImpBdvShowable( final BdvShowable showable, final ImagePlus imp ) { - this.image = image; + this.showable = showable; this.imp = imp; } @Override - public Interval interval() - { - return image; + public Interval interval() { + return showable.interval(); } @Override - public AffineTransform3D transformation() - { - final AffineTransform3D transform = new AffineTransform3D(); - transform.set( - getCalibration( Axes.X ), 0, 0, 0, - 0, getCalibration( Axes.Y ), 0, 0, - 0, 0, getCalibration( Axes.Z ), 0 ); - return transform; + public AffineTransform3D transformation() { + return showable.transformation(); } @Override - public BdvStackSource< ? > show( final String title, final BdvOptions options ) - { - final String name = image.getName(); - final BdvOptions options1 = options.axisOrder( getAxisOrder() ).sourceTransform( transformation() ); - final BdvStackSource< ? extends NumericType< ? > > stackSource = BdvFunctions.show( image, name == null - ? title : name, options1 ); - + public BdvStackSource< ? > show( final String title, final BdvOptions options ) { + final BdvStackSource stackSource = showable.show(title, options); final List< ConverterSetup > converterSetups = stackSource.getConverterSetups(); final SynchronizedViewerState state = stackSource.getBdvHandle().getViewerPanel().state(); @@ -90,33 +74,6 @@ public AffineTransform3D transformation() return stackSource; } - private AxisOrder getAxisOrder() - { - final String code = IntStream - .range( 0, image.numDimensions() ) - .mapToObj( i -> image - .axis( i ) - .type() - .getLabel().substring( 0, 1 ) ) - .collect( Collectors.joining() ); - try - { - return AxisOrder.valueOf( code ); - } - catch ( final IllegalArgumentException e ) - { - return AxisOrder.DEFAULT; - } - } - - private double getCalibration( final AxisType axisType ) - { - final int d = image.dimensionIndex( axisType ); - if ( d == -1 ) - return 1; - return image.axis( d ).averageScale( image.min( d ), image.max( d ) ); - } - private int transferChannelVisibility( final ViewerState state ) { final int nChannels = imp.getNChannels(); From e263f83863e3a50172acba12b50b43137b2af46b Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 17 Sep 2023 13:22:30 +0200 Subject: [PATCH 15/65] The Labkit editor can be launched on all time-points at once. Shift-click on the button. --- .../trackmate/gui/editor/ImpBdvShowable.java | 31 ++ .../trackmate/gui/editor/LabkitImporter.java | 281 ++++++++++++ .../trackmate/gui/editor/LabkitLauncher.java | 428 +++++++----------- 3 files changed, 487 insertions(+), 253 deletions(-) create mode 100644 src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index 8a5ffe1b1..223a37c9e 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -10,6 +10,7 @@ import bdv.viewer.SourceAndConverter; import bdv.viewer.SynchronizedViewerState; import bdv.viewer.ViewerState; +import fiji.plugin.trackmate.util.TMUtils; import ij.CompositeImage; import ij.IJ; import ij.ImagePlus; @@ -37,6 +38,36 @@ public class ImpBdvShowable implements BdvShowable { + /** + * Returns a new {@link BdvShowable} that wraps the specified + * {@link ImagePlus}. The LUT and display settings are read from the + * {@link ImagePlus}. + * + * @param + * the pixel type. + * @param imp + * the {@link ImagePlus} to wrap and read LUT and display + * settings from. + * @return a new {@link BdvShowable} + */ + public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImagePlus imp ) + { + final ImgPlus< T > src = TMUtils.rawWraps( imp ); + return fromImp( src, imp ); + } + + /** + * Returns a new {@link BdvShowable} for the specified image, but using the + * LUT and display settings of the specified {@link ImagePlus}. + * + * @param + * the pixel type. + * @param frame + * the image to wrap in a {@link BdvShowable}. + * @param imp + * the {@link ImagePlus} to read LUT and display settings from. + * @return a new {@link BdvShowable} + */ public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPlus< T > frame, final ImagePlus imp ) { return new ImpBdvShowable( BdvShowable.wrap( prepareImage( frame ) ), imp ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java new file mode 100644 index 000000000..420828128 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -0,0 +1,281 @@ +package fiji.plugin.trackmate.gui.editor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jgrapht.graph.DefaultWeightedEdge; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.detection.MaskUtils; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.view.Views; + +/** + * Re-import the edited segmentation made in Labkit into the TrackMate model it + * started from. + */ +public class LabkitImporter +{ + + private final Model model; + + private final double[] calibration; + + private final double dt; + + /** + * Creates a new re-importer. + * + * @param model + * the model to add, remove or edit spots in. + * @param currentTimePoint + * the time-point used for editing. If negative, then all the + * time-points in the input movie will be processed. + * @param calibration + * the spatial calibration array: [dx, dy dz]. + * @param dt + * the frame interval. + */ + public LabkitImporter( + final Model model, + final double[] calibration, + final double dt ) + { + this.model = model; + this.calibration = calibration; + this.dt = dt; + } + + /** + * Re-import the specified label image (specified by its index image) into + * the TrackMate model. The label images must corresponds to one time-point. + *

+ * The index image after modification is compared with the original one, and + * modifications are detected. Spots corresponding to modifications are + * added, removed or edited in the TrackMate model. + *

+ * To properly detect modifications, the indices in the label images must + * correspond to the spot ID + 1 (index = id + 1). + * + * @param novelIndexImg + * the new index image of the labeling model, that represents the + * TrackMate model in the specified time-point after + * modification. + * @param previousIndexImg + * the previous index image, that represents the TrackMate model + * before modification. + * @param currentTimePoint + * the time-point in the TrackMate model that corresponds to the + * index image. + */ + public void reimport( + final RandomAccessibleInterval< UnsignedShortType > novelIndexImg, + final RandomAccessibleInterval< UnsignedShortType > previousIndexImg, + final int currentTimePoint ) + { + + // Collect ids of spots that have been modified. id = index - 1 + final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); + final int nModified = modifiedIDs.size(); + + if ( nModified == 0 ) + return; + + model.beginUpdate(); + try + { + // Map of previous spots against their ID: + final SpotCollection spots = model.getSpots(); + final Map< Integer, Spot > previousSpotIDs = new HashMap<>(); + spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); + + /* + * Get all the spots present in the new image. Because we specified + * the novel label image as 'quality' image, they have a quality + * value equal to the index in the label image (id+1). + */ + final List< Spot > novelSpots = getSpots( novelIndexImg ); + + /* + * Map of novel spots against the ID taken from the index of the + * novel label image. Normally, this index, and hence the novel id, + * corresponds to the id of previous spots. If one of the novel spot + * has an id we cannot find in the previous spot list, it means that + * it is a new one. + * + * Careful! The user might have created several connected components + * with the same label in LabKit, which will result in having + * several spots with the same quality value. We don't want to loose + * them, so the map is that of a id to a list of spots. + */ + final Map< Integer, List< Spot > > novelSpotIDs = new HashMap<>(); + novelSpots.forEach( s -> { + final int id = Integer.valueOf( s.getFeature( Spot.QUALITY ).intValue() - 1 ); + final List< Spot > list = novelSpotIDs.computeIfAbsent( Integer.valueOf( id ), ( i ) -> new ArrayList<>() ); + list.add( s ); + } ); + + // Update model for those spots. + for ( final int id : modifiedIDs ) + { + final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); + final List< Spot > novelSpotList = novelSpotIDs.get( Integer.valueOf( id ) ); + if ( previousSpot == null ) + { + /* + * A new one (possible several) I cannot find in the + * previous list -> add as a new spot. + */ + addNewSpot( novelSpotList, currentTimePoint ); + } + else if ( novelSpotList == null || novelSpotList.isEmpty() ) + { + /* + * One I add in the previous spot list, but that has + * disappeared. Remove it. + */ + model.removeSpot( previousSpot ); + } + else + { + /* + * I know of them both. Treat the case as if the previous + * spot was modified. + */ + modifySpot( novelSpotList, previousSpot, currentTimePoint ); + } + } + } + finally + { + model.endUpdate(); + } + } + + private void modifySpot( final List< Spot > novelSpotList, final Spot previousSpot, final int currentTimePoint ) + { + /* + * Hopefully there is only one spot in the novel spot list. If not we + * privilege the one closest to the previous spots. + */ + final Spot mainNovelSpot; + if ( novelSpotList.size() == 1 ) + { + mainNovelSpot = novelSpotList.get( 0 ); + } + else + { + Spot closest = null; + double minD2 = Double.POSITIVE_INFINITY; + for ( final Spot s : novelSpotList ) + { + final double d2 = s.squareDistanceTo( previousSpot ); + if ( d2 < minD2 ) + { + minD2 = d2; + closest = s; + } + } + mainNovelSpot = closest; + } + + // Add it properly. + mainNovelSpot.setName( previousSpot.getName() ); + mainNovelSpot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + mainNovelSpot.putFeature( Spot.QUALITY, -1. ); + model.addSpotTo( mainNovelSpot, Integer.valueOf( currentTimePoint ) ); + // Recreate links. + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( previousSpot ); + for ( final DefaultWeightedEdge e : edges ) + { + final double weight = model.getTrackModel().getEdgeWeight( e ); + final Spot source = model.getTrackModel().getEdgeSource( e ); + final Spot target = model.getTrackModel().getEdgeTarget( e ); + if ( source == previousSpot ) + model.addEdge( mainNovelSpot, target, weight ); + else if ( target == previousSpot ) + model.addEdge( source, mainNovelSpot, weight ); + else + throw new IllegalArgumentException( "The edge of a spot does not have the spot as source or target?!?" ); + } + model.removeSpot( previousSpot ); + + // Deal with the other ones. + final HashSet< Spot > extraSpots = new HashSet<>( novelSpotList ); + extraSpots.remove( mainNovelSpot ); + int i = 1; + for ( final Spot s : extraSpots ) + { + s.setName( previousSpot.getName() + "_" + i++ ); + s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + s.putFeature( Spot.QUALITY, -1. ); + model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); + } + } + + private void addNewSpot( final List< Spot > novelSpotList, final int currentTimePoint ) + { + for ( final Spot spot : novelSpotList ) + { + spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + spot.putFeature( Spot.QUALITY, -1. ); + model.addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); + } + } + + private final Set< Integer > getModifiedIDs( + final RandomAccessibleInterval< UnsignedShortType > novelIndexImg, + final RandomAccessibleInterval< UnsignedShortType > previousIndexImg ) + { + final ConcurrentSkipListSet< Integer > modifiedIDs = new ConcurrentSkipListSet<>(); + LoopBuilder.setImages( novelIndexImg, previousIndexImg ) + .multiThreaded( false ) + .forEachPixel( ( c, p ) -> { + final int ci = c.get(); + final int pi = p.get(); + if ( ci == 0 && pi == 0 ) + return; + if ( ci != pi ) + { + modifiedIDs.add( Integer.valueOf( pi - 1 ) ); + modifiedIDs.add( Integer.valueOf( ci - 1 ) ); + } + } ); + modifiedIDs.remove( Integer.valueOf( -1 ) ); + return modifiedIDs; + } + + private List< Spot > getSpots( final RandomAccessibleInterval< UnsignedShortType > rai ) + { + // Get all labels. + final AtomicInteger max = new AtomicInteger( 0 ); + Views.iterable( rai ).forEach( p -> { + final int val = p.getInteger(); + if ( val != 0 && val > max.get() ) + max.set( val ); + } ); + final List< Integer > indices = new ArrayList<>( max.get() ); + for ( int i = 0; i < max.get(); i++ ) + indices.add( Integer.valueOf( i + 1 ) ); + + final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); + final boolean simplify = true; + return MaskUtils.fromLabelingWithROI( + labeling, + labeling, + calibration, + simplify, + rai ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 59f3aa58a..552a71b93 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -2,30 +2,20 @@ import java.awt.Component; import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JRootPane; import javax.swing.SwingUtilities; -import org.jgrapht.graph.DefaultWeightedEdge; import org.scijava.Context; import org.scijava.ui.behaviour.util.AbstractNamedAction; -import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.detection.DetectionUtils; -import fiji.plugin.trackmate.detection.MaskUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; import fiji.plugin.trackmate.util.SpotUtil; @@ -39,14 +29,12 @@ import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImgPlusViews; import net.imglib2.loops.LoopBuilder; -import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.view.Views; import sc.fiji.labkit.ui.LabkitFrame; import sc.fiji.labkit.ui.inputimage.DatasetInputImage; import sc.fiji.labkit.ui.labeling.Labeling; import sc.fiji.labkit.ui.models.DefaultSegmentationModel; -import sc.fiji.labkit.ui.models.ImageLabelingModel; public class LabkitLauncher { @@ -57,50 +45,29 @@ public class LabkitLauncher private final EverythingDisablerAndReenabler disabler; - private ImgPlus< UnsignedShortType > previousIndexImg; - private int currentTimePoint; - private final boolean is2D; - - private final double dt; - public LabkitLauncher( final TrackMate trackmate, final EverythingDisablerAndReenabler disabler ) { this.trackmate = trackmate; this.disabler = disabler; final ImagePlus imp = trackmate.getSettings().imp; this.calibration = TMUtils.getSpatialCalibration( imp ); - this.is2D = DetectionUtils.is2D( imp ); - this.dt = imp.getCalibration().frameInterval; } - @SuppressWarnings( { "rawtypes", "unchecked" } ) - protected void launch() + /** + * Launches the Labkit editor. + * + * @param singleTimePoint + * if true, will launch the editor using only the + * time-point currently displayed in the main view. Otherwise, + * will edit all time-points. + */ + protected void launch( final boolean singleTimePoint ) { final ImagePlus imp = trackmate.getSettings().imp; - final ImgPlus src = TMUtils.rawWraps( imp ); - final int timeAxis = src.dimensionIndex( Axes.TIME ); - - // Reslice for current time-point. - this.currentTimePoint = imp.getFrame() - 1; - final ImgPlus frame = ImgPlusViews.hyperSlice( src, timeAxis, currentTimePoint ); - final ImpBdvShowable showable = ImpBdvShowable.fromImp( frame, imp ); - final DatasetInputImage input = new DatasetInputImage( frame, showable ); - - // Prepare label image. - final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y, Axes.Z, }; - final long[] dims = new long[ is2D ? 2 : 3 ]; - for ( int d = 0; d < dims.length; d++ ) - dims[ d ] = src.dimension( src.dimensionIndex( axes[ d ] ) ); - final Img< UnsignedShortType > lblImg = ArrayImgs.unsignedShorts( dims ); - final double[] calibration = TMUtils.getSpatialCalibration( imp ); - final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); - - // Write spots in it with index = id + 1 - final Iterable< Spot > spotsThisFrame = trackmate.getModel().getSpots().iterable( currentTimePoint, true ); - for ( final Spot spot : spotsThisFrame ) - SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); + final DatasetInputImage input = makeInput( imp, singleTimePoint ); + final ImgPlus< UnsignedShortType > lblImgPlus = makeLblImage( imp, trackmate.getModel().getSpots(), singleTimePoint ); // Make a labeling model from it. final Context context = TMUtils.getContext(); @@ -108,243 +75,193 @@ protected void launch() model.imageLabelingModel().labeling().set( Labeling.fromImg( lblImgPlus ) ); // Store a copy. - this.previousIndexImg = lblImgPlus.copy(); + final ImgPlus< UnsignedShortType > previousIndexImg = lblImgPlus.copy(); // Show LabKit. final LabkitFrame labkit = LabkitFrame.show( model, "Edit TrackMate data frame " + ( currentTimePoint + 1 ) ); - labkit.onCloseListeners().addListener( () -> reimportData( model.imageLabelingModel(), currentTimePoint ) ); - } - @SuppressWarnings( "unchecked" ) - private void reimportData( final ImageLabelingModel lm, final int currentTimePoint ) - { - try - { - /* - * We will update the spots using a comparison based on only the - * index images. - */ - final Labeling labeling = lm.labeling().get(); - final RandomAccessibleInterval< UnsignedShortType > novelIndexImg = ( RandomAccessibleInterval< UnsignedShortType > ) labeling.getIndexImg(); - - // Collect ids of spots that have been modified. id = index - 1 - final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg ); - final int nModified = modifiedIDs.size(); - - if ( nModified == 0 ) - return; - - // Message the user. - final String msg = - "Commit the changes made to the\n" - + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; - final String title = "Commit edits to TrackMate"; - final int returnedValue = JOptionPane.showConfirmDialog( - null, - msg, - title, - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - Icons.TRACKMATE_ICON ); - if ( returnedValue != JOptionPane.YES_OPTION ) - return; - - final Model model = trackmate.getModel(); - model.beginUpdate(); + // Prepare re-importer. + final double dt = imp.getCalibration().frameInterval; + final LabkitImporter reimporter = new LabkitImporter( trackmate.getModel(), calibration, dt ); + labkit.onCloseListeners().addListener( () -> { try { - // Map of previous spots against their ID: - final SpotCollection spots = model.getSpots(); - final Map< Integer, Spot > previousSpotIDs = new HashMap<>(); - spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); - - /* - * Get all the spots present in the new image. Because we - * specified the novel label image as 'quality' image, they have - * a quality value equal to the index in the label image (id+1). - */ - final List< Spot > novelSpots = getSpots( novelIndexImg ); - - /* - * Map of novel spots against the ID taken from the index of the - * novel label image. Normally, this index, and hence the novel - * id, corresponds to the id of previous spots. If one of the - * novel spot has an id we cannot find in the previous spot - * list, it means that it is a new one. - * - * Careful! The user might have created several connected - * components with the same label in LabKit, which will result - * in having several spots with the same quality value. We don't - * want to loose them, so the map is that of a id to a list of - * spots. - */ - final Map< Integer, List< Spot > > novelSpotIDs = new HashMap<>(); - novelSpots.forEach( s -> { - final int id = Integer.valueOf( s.getFeature( Spot.QUALITY ).intValue() - 1 ); - final List< Spot > list = novelSpotIDs.computeIfAbsent( Integer.valueOf( id ), ( i ) -> new ArrayList<>() ); - list.add( s ); - } ); - - // Update model for those spots. - for ( final int id : modifiedIDs ) + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< UnsignedShortType > indexImg = ( RandomAccessibleInterval< UnsignedShortType > ) model.imageLabelingModel().labeling().get().getIndexImg(); + + // Do we have something to reimport? + final AtomicBoolean modified = new AtomicBoolean( false ); + LoopBuilder.setImages( previousIndexImg, indexImg ) + .multiThreaded() + .forEachChunk( chunk -> { + if ( modified.get() ) + return null; + chunk.forEachPixel( ( p1, p2 ) -> { + if ( p1.get() != p2.get() ) + { + modified.set( true ); + return; + } + } ); + return null; + } ); + if ( !modified.get() ) + return; + + // Message the user. + final String msg = ( currentTimePoint < 0 ) + ? "Commit the changes made to the\n" + + "segmentation in whole movie?" + : "Commit the changes made to the\n" + + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; + final String title = "Commit edits to TrackMate"; + final int returnedValue = JOptionPane.showConfirmDialog( + null, + msg, + title, + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + Icons.TRACKMATE_ICON ); + if ( returnedValue != JOptionPane.YES_OPTION ) + return; + + final int timeDim = 2; + if ( currentTimePoint < 0 ) { - final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); - final List< Spot > novelSpotList = novelSpotIDs.get( Integer.valueOf( id ) ); - if ( previousSpot == null ) - { - /* - * A new one (possible several) I cannot find in the - * previous list -> add as a new spot. - */ - addNewSpot( novelSpotList ); - } - else if ( novelSpotList == null || novelSpotList.isEmpty() ) + // All time-points. + final Logger log = Logger.IJ_LOGGER; + log.setStatus( "Re-importing from Labkit..." ); + for ( int t = 0; t < imp.getNFrames(); t++ ) { - /* - * One I add in the previous spot list, but that has - * disappeared. Remove it. - */ - model.removeSpot( previousSpot ); - } - else - { - /* - * I know of them both. Treat the case as if the - * previous spot was modified. - */ - modifySpot( novelSpotList, previousSpot ); + final RandomAccessibleInterval< UnsignedShortType > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); + final RandomAccessibleInterval< UnsignedShortType > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); + log.setProgress( ++t / ( double ) imp.getNFrames() ); } + log.setStatus( "" ); + log.setProgress( 0. ); + } + else + { + // Only one. + reimporter.reimport( indexImg, previousIndexImg, currentTimePoint ); } } + catch ( final Exception e ) + { + e.printStackTrace(); + } finally { - model.endUpdate(); + disabler.reenable(); } - } - catch ( final Exception e ) - { - e.printStackTrace(); - } - finally - { - disabler.reenable(); - } + } ); } - private void modifySpot( final List< Spot > novelSpotList, final Spot previousSpot ) + /** + * Creates a new {@link DatasetInputImage} from the specified + * {@link ImagePlus}. The embedded label image is empty. + * + * @param imp + * the input {@link ImagePlus}. + * @param singleTimePoint + * if true, then the dataset will be created only + * for the time-point currently displayed in the + * {@link ImagePlus}. This time-point is then stored in the + * {@link #currentTimePoint} field. If false, the + * dataset is created for the whole movie and the + * {@link #currentTimePoint} takes the value -1. + * @return a new {@link DatasetInputImage}. + */ + @SuppressWarnings( { "rawtypes", "unchecked" } ) + private final DatasetInputImage makeInput( final ImagePlus imp, final boolean singleTimePoint ) { - final Model model = trackmate.getModel(); - - /* - * Hopefully there is only one spot in the novel spot list. If not we - * privilege the one closest to the previous spots. - */ - final Spot mainNovelSpot; - if ( novelSpotList.size() == 1 ) + final ImgPlus src = TMUtils.rawWraps( imp ); + // Possibly reslice for current time-point. + final ImpBdvShowable showable; + final ImgPlus inputImg; + if ( singleTimePoint ) { - mainNovelSpot = novelSpotList.get( 0 ); + this.currentTimePoint = imp.getFrame() - 1; + final int timeAxis = src.dimensionIndex( Axes.TIME ); + inputImg = ImgPlusViews.hyperSlice( src, timeAxis, currentTimePoint ); + showable = ImpBdvShowable.fromImp( inputImg, imp ); } else { - Spot closest = null; - double minD2 = Double.POSITIVE_INFINITY; - for ( final Spot s : novelSpotList ) - { - final double d2 = s.squareDistanceTo( previousSpot ); - if ( d2 < minD2 ) - { - minD2 = d2; - closest = s; - } - } - mainNovelSpot = closest; + this.currentTimePoint = -1; + showable = ImpBdvShowable.fromImp( imp ); + inputImg = src; } + return new DatasetInputImage( inputImg, showable ); + } - // Add it properly. - mainNovelSpot.setName( previousSpot.getName() ); - mainNovelSpot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); - mainNovelSpot.putFeature( Spot.QUALITY, -1. ); - model.addSpotTo( mainNovelSpot, Integer.valueOf( currentTimePoint ) ); - // Recreate links. - final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( previousSpot ); - for ( final DefaultWeightedEdge e : edges ) - { - final double weight = model.getTrackModel().getEdgeWeight( e ); - final Spot source = model.getTrackModel().getEdgeSource( e ); - final Spot target = model.getTrackModel().getEdgeTarget( e ); - if ( source == previousSpot ) - model.addEdge( mainNovelSpot, target, weight ); - else if ( target == previousSpot ) - model.addEdge( source, mainNovelSpot, weight ); - else - throw new IllegalArgumentException( "The edge of a spot does not have the spot as source or target?!?" ); - } - model.removeSpot( previousSpot ); + /** + * Prepare the label image for annotation. + * + * @param imp + * the source image plus. + * @param spots + * the spot collection. + * @param singleTimePoint + * if true we only annotate one time-point. + * @return a new {@link ImgPlus}. + */ + private ImgPlus< UnsignedShortType > makeLblImage( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) + { + // Careful: Only works for 2D images! FIXME + + // Axes. + final AxisType[] axes = ( singleTimePoint ) + ? new AxisType[] { Axes.X, Axes.Y } + : new AxisType[] { Axes.X, Axes.Y, Axes.TIME }; - // Deal with the other ones. - final HashSet< Spot > extraSpots = new HashSet<>( novelSpotList ); - extraSpots.remove( mainNovelSpot ); - int i = 1; - for ( final Spot s : extraSpots ) + // N dimensions. + final int timeDim = 2; + final int nDims = singleTimePoint ? 2 : 3; + + // Dimensions. + final long[] dims = new long[ nDims ]; + dims[ 0 ] = imp.getWidth(); + dims[ 1 ] = imp.getHeight(); + if ( !singleTimePoint ) + dims[ timeDim ] = imp.getNFrames(); + + // Raw image. + final Img< UnsignedShortType > lblImg = ArrayImgs.unsignedShorts( dims ); + + // Calibration. + final double[] c = TMUtils.getSpatialCalibration( imp ); + final double[] calibration = new double[ nDims ]; + for ( int i = 0; i < 2; i++ ) + calibration[ i ] = c[ i ]; + if ( !singleTimePoint ) + calibration[ timeDim ] = 1.; + + // Label image holder. + final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); + + // Write spots in it with index = id + 1 + if ( singleTimePoint ) { - s.setName( previousSpot.getName() + "_" + i++ ); - s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); - s.putFeature( Spot.QUALITY, -1. ); - model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); + processFrame( lblImgPlus, spots, currentTimePoint ); } - } - - private void addNewSpot( final List< Spot > novelSpotList ) - { - for ( final Spot spot : novelSpotList ) + else { - spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); - spot.putFeature( Spot.QUALITY, -1. ); - trackmate.getModel().addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); + for ( int t = 0; t < imp.getNFrames(); t++ ) + { + final ImgPlus< UnsignedShortType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); + processFrame( lblImgPlusThisFrame, spots, t ); + } } + return lblImgPlus; } - private final Set< Integer > getModifiedIDs( final RandomAccessibleInterval< UnsignedShortType > novelIndexImg ) + private static final void processFrame( final ImgPlus< UnsignedShortType > lblImgPlus, final SpotCollection spots, final int t ) { - final ConcurrentSkipListSet< Integer > modifiedIDs = new ConcurrentSkipListSet<>(); - LoopBuilder.setImages( novelIndexImg, previousIndexImg ) - .multiThreaded( false ) - .forEachPixel( ( c, p ) -> { - final int ci = c.get(); - final int pi = p.get(); - if ( ci == 0 && pi == 0 ) - return; - if ( ci != pi ) - { - modifiedIDs.add( Integer.valueOf( pi - 1 ) ); - modifiedIDs.add( Integer.valueOf( ci - 1 ) ); - } - } ); - modifiedIDs.remove( Integer.valueOf( -1 ) ); - return modifiedIDs; - } - - private List< Spot > getSpots( final RandomAccessibleInterval< UnsignedShortType > rai ) - { - // Get all labels. - final AtomicInteger max = new AtomicInteger( 0 ); - Views.iterable( rai ).forEach( p -> { - final int val = p.getInteger(); - if ( val != 0 && val > max.get() ) - max.set( val ); - } ); - final List< Integer > indices = new ArrayList<>( max.get() ); - for ( int i = 0; i < max.get(); i++ ) - indices.add( Integer.valueOf( i + 1 ) ); - - final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); - final boolean simplify = true; - return MaskUtils.fromLabelingWithROI( - labeling, - labeling, - calibration, - simplify, - rai ); + final Iterable< Spot > spotsThisFrame = spots.iterable( t, true ); + for ( final Spot spot : spotsThisFrame ) + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); } public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate ) @@ -362,13 +279,18 @@ public void actionPerformed( final ActionEvent ae ) @Override public void run() { + // Is shift pressed? + final int mod = ae.getModifiers(); + final boolean shiftPressed = ( mod & ActionEvent.SHIFT_MASK ) > 0; + final boolean singleTimepoint = !shiftPressed; + final JRootPane parent = SwingUtilities.getRootPane( ( Component ) ae.getSource() ); final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( parent, new Class[] { JLabel.class } ); disabler.disable(); try { final LabkitLauncher launcher = new LabkitLauncher( trackmate, disabler ); - launcher.launch(); + launcher.launch( singleTimepoint ); } catch ( final Exception e ) { From 3757a83158bc0a4880580c6e7bf2faf6a43fc044 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 17 Sep 2023 13:22:47 +0200 Subject: [PATCH 16/65] Add tooltip for the editor button. And remove debug code. --- .../gui/components/ConfigureViewsPanel.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java index e537d96e3..85ddbf7c2 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -373,17 +373,19 @@ public ConfigureViewsPanel( // Is labkit available? if ( TMUtils.isClassPresent( "sc.fiji.labkit.ui.LabkitFrame" ) ) { - System.out.println( "LabKit found." ); // DEBUG final JButton btnLabKit = new JButton( launchLabKitAction ); btnLabKit.setFont( FONT ); btnLabKit.setText( "Launch spot editor" ); btnLabKit.setIcon( Icons.PENCIL_ICON ); + btnLabKit.setToolTipText( "" + + "Launch the Labkit editor to edit spot segmentation
" + + "on the time-point currently displayed in the main
" + + "view." + + "

" + + "Shift + click will launch the editor on all the
" + + "time-points in the movie." ); panelButtons.add( btnLabKit ); } - else - { - System.out.println( "LabKit not found." ); // DEBUG - } panelButtons.setSize( new Dimension( 300, 1 ) ); final GridBagConstraints gbcPanelButtons = new GridBagConstraints(); From f9c0b73c57585acb53a6b63939c63fc1dd5f4c04 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 17 Sep 2023 17:40:20 +0200 Subject: [PATCH 17/65] In Labkit editor, labels receive name and color from spots. I am not so sure it is a good idea for the color... --- .../trackmate/gui/editor/LabkitLauncher.java | 208 +++++++++++------- .../gui/wizard/TrackMateWizardSequence.java | 9 +- 2 files changed, 136 insertions(+), 81 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 552a71b93..34a53fd0c 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -2,6 +2,8 @@ import java.awt.Component; import java.awt.event.ActionEvent; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JLabel; @@ -16,23 +18,31 @@ import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.gui.Icons; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.FeatureColorGenerator; import ij.ImagePlus; import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.Img; +import net.imglib2.img.ImgFactory; import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImgPlusViews; import net.imglib2.loops.LoopBuilder; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.util.Util; import net.imglib2.view.Views; import sc.fiji.labkit.ui.LabkitFrame; import sc.fiji.labkit.ui.inputimage.DatasetInputImage; +import sc.fiji.labkit.ui.labeling.Label; import sc.fiji.labkit.ui.labeling.Labeling; import sc.fiji.labkit.ui.models.DefaultSegmentationModel; @@ -47,9 +57,12 @@ public class LabkitLauncher private int currentTimePoint; - public LabkitLauncher( final TrackMate trackmate, final EverythingDisablerAndReenabler disabler ) + private final DisplaySettings ds; + + public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, final EverythingDisablerAndReenabler disabler ) { this.trackmate = trackmate; + this.ds = ds; this.disabler = disabler; final ImagePlus imp = trackmate.getSettings().imp; this.calibration = TMUtils.getSpatialCalibration( imp ); @@ -67,95 +80,110 @@ protected void launch( final boolean singleTimePoint ) { final ImagePlus imp = trackmate.getSettings().imp; final DatasetInputImage input = makeInput( imp, singleTimePoint ); - final ImgPlus< UnsignedShortType > lblImgPlus = makeLblImage( imp, trackmate.getModel().getSpots(), singleTimePoint ); + final Labeling labeling = makeLabeling( imp, trackmate.getModel().getSpots(), singleTimePoint ); // Make a labeling model from it. final Context context = TMUtils.getContext(); final DefaultSegmentationModel model = new DefaultSegmentationModel( context, input ); - model.imageLabelingModel().labeling().set( Labeling.fromImg( lblImgPlus ) ); + model.imageLabelingModel().labeling().set( labeling ); // Store a copy. - final ImgPlus< UnsignedShortType > previousIndexImg = lblImgPlus.copy(); + final RandomAccessibleInterval< UnsignedShortType > previousIndexImg = copy( labeling.getIndexImg() ); // Show LabKit. final LabkitFrame labkit = LabkitFrame.show( model, "Edit TrackMate data frame " + ( currentTimePoint + 1 ) ); // Prepare re-importer. final double dt = imp.getCalibration().frameInterval; - final LabkitImporter reimporter = new LabkitImporter( trackmate.getModel(), calibration, dt ); labkit.onCloseListeners().addListener( () -> { - try - { - @SuppressWarnings( "unchecked" ) - final RandomAccessibleInterval< UnsignedShortType > indexImg = ( RandomAccessibleInterval< UnsignedShortType > ) model.imageLabelingModel().labeling().get().getIndexImg(); - - // Do we have something to reimport? - final AtomicBoolean modified = new AtomicBoolean( false ); - LoopBuilder.setImages( previousIndexImg, indexImg ) - .multiThreaded() - .forEachChunk( chunk -> { - if ( modified.get() ) - return null; - chunk.forEachPixel( ( p1, p2 ) -> { - if ( p1.get() != p2.get() ) - { - modified.set( true ); - return; - } - } ); + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< UnsignedShortType > indexImg = ( RandomAccessibleInterval< UnsignedShortType > ) model.imageLabelingModel().labeling().get().getIndexImg(); + reimport( indexImg, previousIndexImg, dt ); + } ); + } + + private void reimport( final RandomAccessibleInterval< UnsignedShortType > indexImg, final RandomAccessibleInterval< UnsignedShortType > previousIndexImg, final double dt ) + { + try + { + // Do we have something to reimport? + final AtomicBoolean modified = new AtomicBoolean( false ); + LoopBuilder.setImages( previousIndexImg, indexImg ) + .multiThreaded() + .forEachChunk( chunk -> { + if ( modified.get() ) return null; + chunk.forEachPixel( ( p1, p2 ) -> { + if ( p1.get() != p2.get() ) + { + modified.set( true ); + return; + } } ); - if ( !modified.get() ) - return; - - // Message the user. - final String msg = ( currentTimePoint < 0 ) - ? "Commit the changes made to the\n" - + "segmentation in whole movie?" - : "Commit the changes made to the\n" - + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; - final String title = "Commit edits to TrackMate"; - final int returnedValue = JOptionPane.showConfirmDialog( - null, - msg, - title, - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - Icons.TRACKMATE_ICON ); - if ( returnedValue != JOptionPane.YES_OPTION ) - return; - + return null; + } ); + if ( !modified.get() ) + return; + + // Message the user. + final String msg = ( currentTimePoint < 0 ) + ? "Commit the changes made to the\n" + + "segmentation in whole movie?" + : "Commit the changes made to the\n" + + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; + final String title = "Commit edits to TrackMate"; + final int returnedValue = JOptionPane.showConfirmDialog( + null, + msg, + title, + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + Icons.TRACKMATE_ICON ); + if ( returnedValue != JOptionPane.YES_OPTION ) + return; + + final LabkitImporter reimporter = new LabkitImporter( trackmate.getModel(), calibration, dt ); + if ( currentTimePoint < 0 ) + { + // All time-points. final int timeDim = 2; - if ( currentTimePoint < 0 ) - { - // All time-points. - final Logger log = Logger.IJ_LOGGER; - log.setStatus( "Re-importing from Labkit..." ); - for ( int t = 0; t < imp.getNFrames(); t++ ) - { - final RandomAccessibleInterval< UnsignedShortType > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); - final RandomAccessibleInterval< UnsignedShortType > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); - reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); - log.setProgress( ++t / ( double ) imp.getNFrames() ); - } - log.setStatus( "" ); - log.setProgress( 0. ); - } - else + final long nTimepoints = indexImg.dimension( timeDim ); + final Logger log = Logger.IJ_LOGGER; + log.setStatus( "Re-importing from Labkit..." ); + for ( int t = 0; t < nTimepoints; t++ ) { - // Only one. - reimporter.reimport( indexImg, previousIndexImg, currentTimePoint ); + final RandomAccessibleInterval< UnsignedShortType > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); + final RandomAccessibleInterval< UnsignedShortType > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); + log.setProgress( ++t / ( double ) nTimepoints ); } + log.setStatus( "" ); + log.setProgress( 0. ); } - catch ( final Exception e ) + else { - e.printStackTrace(); + // Only one. + reimporter.reimport( indexImg, previousIndexImg, currentTimePoint ); } - finally - { - disabler.reenable(); - } - } ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + finally + { + disabler.reenable(); + } + } + + private static Img< UnsignedShortType > copy( final RandomAccessibleInterval< ? extends IntegerType< ? > > in ) + { + final ImgFactory< UnsignedShortType > factory = Util.getArrayOrCellImgFactory( in, new UnsignedShortType() ); + final Img< UnsignedShortType > out = factory.create( in ); + LoopBuilder.setImages( in, out ) + .multiThreaded() + .forEachPixel( ( i, o ) -> o.set( i.getInteger() ) ); + return out; } /** @@ -197,7 +225,9 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si } /** - * Prepare the label image for annotation. + * Prepare the label image for annotation. The labeling is created and each + * of its labels receive the name and the color from the spot it is created + * from. * * @param imp * the source image plus. @@ -205,9 +235,9 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si * the spot collection. * @param singleTimePoint * if true we only annotate one time-point. - * @return a new {@link ImgPlus}. + * @return a new {@link Labeling}. */ - private ImgPlus< UnsignedShortType > makeLblImage( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) + private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) { // Careful: Only works for 2D images! FIXME @@ -241,30 +271,48 @@ private ImgPlus< UnsignedShortType > makeLblImage( final ImagePlus imp, final Sp // Label image holder. final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); - // Write spots in it with index = id + 1 + // Write spots in it with index = id + 1 and build a map index -> spot. + final Map< Integer, Spot > spotIDs = new HashMap<>(); if ( singleTimePoint ) { - processFrame( lblImgPlus, spots, currentTimePoint ); + processFrame( lblImgPlus, spots, currentTimePoint, spotIDs ); } else { for ( int t = 0; t < imp.getNFrames(); t++ ) { final ImgPlus< UnsignedShortType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); - processFrame( lblImgPlusThisFrame, spots, t ); + processFrame( lblImgPlusThisFrame, spots, t, spotIDs ); } } - return lblImgPlus; + final Labeling labeling = Labeling.fromImg( lblImgPlus ); + + // Fine tune labels name and color. + final FeatureColorGenerator< Spot > colorGen = FeatureUtils.createSpotColorGenerator( trackmate.getModel(), ds ); + for ( final Label label : labeling.getLabels() ) + { + final String name = label.name(); + final int index = Integer.parseInt( name ); + final Spot spot = spotIDs.get( index ); + label.setName( spot.getName() ); + label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); + } + + return labeling; } - private static final void processFrame( final ImgPlus< UnsignedShortType > lblImgPlus, final SpotCollection spots, final int t ) + private static final void processFrame( final ImgPlus< UnsignedShortType > lblImgPlus, final SpotCollection spots, final int t, final Map< Integer, Spot > spotIDs ) { final Iterable< Spot > spotsThisFrame = spots.iterable( t, true ); for ( final Spot spot : spotsThisFrame ) - SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( spot.ID() + 1 ) ); + { + final int index = spot.ID() + 1; + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spotIDs.put( index, spot ); + } } - public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate ) + public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate, final DisplaySettings ds ) { return new AbstractNamedAction( "launch labkit editor" ) { @@ -289,7 +337,7 @@ public void run() disabler.disable(); try { - final LabkitLauncher launcher = new LabkitLauncher( trackmate, disabler ); + final LabkitLauncher launcher = new LabkitLauncher( trackmate, ds, disabler ); launcher.launch( singleTimepoint ); } catch ( final Exception e ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java index 04856c18f..9ab663389 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java @@ -147,7 +147,14 @@ public TrackMateWizardSequence( final TrackMate trackmate, final SelectionModel chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), trackmate ); executeTrackingDescriptor = new ExecuteTrackingDescriptor( trackmate, logPanel ); trackFilterDescriptor = new TrackFilterDescriptor( trackmate, trackFilters, featureSelector ); - configureViewsDescriptor = new ConfigureViewsDescriptor( displaySettings, featureSelector, new LaunchTrackSchemeAction(), new ShowTrackTablesAction(), new ShowSpotTableAction(), LabkitLauncher.getLaunchAction( trackmate ), model.getSpaceUnits() ); + configureViewsDescriptor = new ConfigureViewsDescriptor( + displaySettings, + featureSelector, + new LaunchTrackSchemeAction(), + new ShowTrackTablesAction(), + new ShowSpotTableAction(), + LabkitLauncher.getLaunchAction( trackmate, displaySettings ), + model.getSpaceUnits() ); grapherDescriptor = new GrapherDescriptor( trackmate, selectionModel, displaySettings ); actionChooserDescriptor = new ActionChooserDescriptor( new ActionProvider(), trackmate, selectionModel, displaySettings ); saveDescriptor = new SaveDescriptor( trackmate, displaySettings, this ); From ee3b86add17c879d3f2649d8dd698ae91fc7d000 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 17 Sep 2023 18:06:18 +0200 Subject: [PATCH 18/65] Better editor frame title. --- .../fiji/plugin/trackmate/gui/editor/LabkitLauncher.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 34a53fd0c..c76b27ff5 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -91,7 +91,10 @@ protected void launch( final boolean singleTimePoint ) final RandomAccessibleInterval< UnsignedShortType > previousIndexImg = copy( labeling.getIndexImg() ); // Show LabKit. - final LabkitFrame labkit = LabkitFrame.show( model, "Edit TrackMate data frame " + ( currentTimePoint + 1 ) ); + String title = "Editing TrackMate data for " + imp.getShortTitle(); + if ( singleTimePoint ) + title += "at frame " + ( currentTimePoint + 1 ); + final LabkitFrame labkit = LabkitFrame.show( model, title ); // Prepare re-importer. final double dt = imp.getCalibration().frameInterval; From 8aaf763e8cb14389ae5f822a1475237c957bba1d Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Tue, 19 Sep 2023 15:40:07 +0200 Subject: [PATCH 19/65] Properly handle 2D and 3D with or without time in the editor. --- .../trackmate/gui/editor/LabkitLauncher.java | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index c76b27ff5..0b4cd461f 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -18,6 +18,7 @@ import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -59,6 +60,8 @@ public class LabkitLauncher private final DisplaySettings ds; + private final boolean is3D; + public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, final EverythingDisablerAndReenabler disabler ) { this.trackmate = trackmate; @@ -66,6 +69,7 @@ public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, fina this.disabler = disabler; final ImagePlus imp = trackmate.getSettings().imp; this.calibration = TMUtils.getSpatialCalibration( imp ); + this.is3D = !DetectionUtils.is2D( imp ); } /** @@ -149,14 +153,13 @@ private void reimport( final RandomAccessibleInterval< UnsignedShortType > index if ( currentTimePoint < 0 ) { // All time-points. - final int timeDim = 2; - final long nTimepoints = indexImg.dimension( timeDim ); + final long nTimepoints = indexImg.dimension( 3 ); final Logger log = Logger.IJ_LOGGER; log.setStatus( "Re-importing from Labkit..." ); for ( int t = 0; t < nTimepoints; t++ ) { - final RandomAccessibleInterval< UnsignedShortType > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); - final RandomAccessibleInterval< UnsignedShortType > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); + final RandomAccessibleInterval< UnsignedShortType > novelIndexImgThisFrame = Views.hyperSlice( indexImg, 3, t ); + final RandomAccessibleInterval< UnsignedShortType > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, 3, t ); reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); log.setProgress( ++t / ( double ) nTimepoints ); } @@ -242,23 +245,29 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si */ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) { - // Careful: Only works for 2D images! FIXME - // Axes. - final AxisType[] axes = ( singleTimePoint ) - ? new AxisType[] { Axes.X, Axes.Y } - : new AxisType[] { Axes.X, Axes.Y, Axes.TIME }; + final AxisType[] axes = ( is3D ) + ? ( singleTimePoint ) + ? new AxisType[] { Axes.X, Axes.Y, Axes.Z } + : new AxisType[] { Axes.X, Axes.Y, Axes.Z, Axes.TIME } + : ( singleTimePoint ) + ? new AxisType[] { Axes.X, Axes.Y } + : new AxisType[] { Axes.X, Axes.Y, Axes.TIME }; // N dimensions. - final int timeDim = 2; - final int nDims = singleTimePoint ? 2 : 3; + final int nDims = is3D + ? singleTimePoint ? 3 : 4 + : singleTimePoint ? 2 : 3; // Dimensions. final long[] dims = new long[ nDims ]; - dims[ 0 ] = imp.getWidth(); - dims[ 1 ] = imp.getHeight(); + int dim = 0; + dims[ dim++ ] = imp.getWidth(); + dims[ dim++ ] = imp.getHeight(); + if ( is3D ) + dims[ dim++ ] = imp.getNSlices(); if ( !singleTimePoint ) - dims[ timeDim ] = imp.getNFrames(); + dims[ dim++ ] = imp.getNFrames(); // Raw image. final Img< UnsignedShortType > lblImg = ArrayImgs.unsignedShorts( dims ); @@ -266,10 +275,13 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, // Calibration. final double[] c = TMUtils.getSpatialCalibration( imp ); final double[] calibration = new double[ nDims ]; - for ( int i = 0; i < 2; i++ ) - calibration[ i ] = c[ i ]; + dim = 0; + calibration[ dim++ ] = c[ 0 ]; + calibration[ dim++ ] = c[ 1 ]; + if ( is3D ) + calibration[ dim++ ] = c[ 2 ]; if ( !singleTimePoint ) - calibration[ timeDim ] = 1.; + calibration[ dim++ ] = 1.; // Label image holder. final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); @@ -282,6 +294,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, } else { + final int timeDim = lblImgPlus.dimensionIndex( Axes.TIME ); for ( int t = 0; t < imp.getNFrames(); t++ ) { final ImgPlus< UnsignedShortType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); From 544b3dfb5a8d819df36ae2457b76f9ce14c94a17 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Tue, 19 Sep 2023 15:46:14 +0200 Subject: [PATCH 20/65] Always start the editor in FUSED mode, and use the white LUT when the imp is not displayed as a Composite. --- .../fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index 223a37c9e..1293e4616 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -1,5 +1,6 @@ package fiji.plugin.trackmate.gui.editor; +import java.awt.Color; import java.util.Arrays; import java.util.List; @@ -99,9 +100,9 @@ public AffineTransform3D transformation() { final List< ConverterSetup > converterSetups = stackSource.getConverterSetups(); final SynchronizedViewerState state = stackSource.getBdvHandle().getViewerPanel().state(); - final int numActiveChannels = transferChannelVisibility( state ); + transferChannelVisibility( state ); transferChannelSettings( converterSetups ); - state.setDisplayMode( numActiveChannels > 1 ? DisplayMode.FUSED : DisplayMode.SINGLE ); + state.setDisplayMode( DisplayMode.FUSED ); return stackSource; } @@ -147,6 +148,8 @@ private void transferChannelSettings( final List< ConverterSetup > converterSetu final ConverterSetup setup = converterSetups.get( c ); if ( transferColor ) setup.setColor( new ARGBType( lut.getRGB( 255 ) ) ); + else + setup.setColor( new ARGBType( Color.WHITE.getRGB() ) ); setup.setDisplayRange( lut.min, lut.max ); } } From 48594c2c2668407481baf8286327e761f1203f9d Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 22 Sep 2023 15:11:01 +0200 Subject: [PATCH 21/65] Don't crash when editing on images with 1 channel. Noticed by @MiniMiette --- .../trackmate/gui/editor/ImpBdvShowable.java | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index 1293e4616..426db661a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -3,8 +3,12 @@ import java.awt.Color; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import bdv.tools.brightness.ConverterSetup; +import bdv.util.AxisOrder; +import bdv.util.BdvFunctions; import bdv.util.BdvOptions; import bdv.util.BdvStackSource; import bdv.viewer.DisplayMode; @@ -23,6 +27,7 @@ import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.NumericType; +import net.imglib2.view.Views; import sc.fiji.labkit.ui.bdv.BdvShowable; import sc.fiji.labkit.ui.inputimage.ImgPlusViewsOld; @@ -54,6 +59,8 @@ public class ImpBdvShowable implements BdvShowable public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImagePlus imp ) { final ImgPlus< T > src = TMUtils.rawWraps( imp ); + if ( src.dimensionIndex( Axes.CHANNEL ) < 0 ) + Views.addDimension( src ); return fromImp( src, imp ); } @@ -71,32 +78,44 @@ public static < T extends NumericType< T > > ImpBdvShowable fromImp( final Image */ public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPlus< T > frame, final ImagePlus imp ) { - return new ImpBdvShowable( BdvShowable.wrap( prepareImage( frame ) ), imp ); + return new ImpBdvShowable( prepareImage( frame ), imp ); } - private final BdvShowable showable; - private final ImagePlus imp; - ImpBdvShowable( final BdvShowable showable, final ImagePlus imp ) + private final ImgPlus< ? extends NumericType< ? > > image; + + ImpBdvShowable( final ImgPlus< ? extends NumericType< ? > > image, final ImagePlus imp ) { - this.showable = showable; + this.image = image; this.imp = imp; } @Override - public Interval interval() { - return showable.interval(); + public Interval interval() + { + return image; } @Override - public AffineTransform3D transformation() { - return showable.transformation(); + public AffineTransform3D transformation() + { + final AffineTransform3D transform = new AffineTransform3D(); + transform.set( + getCalibration( Axes.X ), 0, 0, 0, + 0, getCalibration( Axes.Y ), 0, 0, + 0, 0, getCalibration( Axes.Z ), 0 ); + return transform; } @Override - public BdvStackSource< ? > show( final String title, final BdvOptions options ) { - final BdvStackSource stackSource = showable.show(title, options); + public BdvStackSource< ? > show( final String title, final BdvOptions options ) + { + final String name = image.getName(); + final BdvOptions options1 = options.axisOrder( getAxisOrder() ).sourceTransform( transformation() ); + final BdvStackSource< ? extends NumericType< ? > > stackSource = BdvFunctions.show( image, name == null + ? title : name, options1 ); + final List< ConverterSetup > converterSetups = stackSource.getConverterSetups(); final SynchronizedViewerState state = stackSource.getBdvHandle().getViewerPanel().state(); @@ -188,4 +207,31 @@ private void transferChannelSettings( final List< ConverterSetup > converterSetu return ImgPlusViewsOld.fixAxes( image, Arrays.asList( Axes.X, Axes.Y, Axes.Z, Axes.CHANNEL, Axes.TIME ) ); } + + private double getCalibration( final AxisType axisType ) + { + final int d = image.dimensionIndex( axisType ); + if ( d == -1 ) + return 1; + return image.axis( d ).averageScale( image.min( d ), image.max( d ) ); + } + + private AxisOrder getAxisOrder() + { + final String code = IntStream + .range( 0, image.numDimensions() ) + .mapToObj( i -> image + .axis( i ) + .type() + .getLabel().substring( 0, 1 ) ) + .collect( Collectors.joining() ); + try + { + return AxisOrder.valueOf( code ); + } + catch ( final IllegalArgumentException e ) + { + return AxisOrder.DEFAULT; + } + } } From a788cf128bf768d91f97968d0bad993445574908 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 22 Sep 2023 15:13:36 +0200 Subject: [PATCH 22/65] If we crash when launching the editor, unfreeze TrackMate. --- .../java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 0b4cd461f..06a35a1d3 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -358,6 +358,7 @@ public void run() } catch ( final Exception e ) { + disabler.reenable(); e.printStackTrace(); } }; From b503a63a3c7c76ae4e1b792a4f357aa3d26fe8ad Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 22 Sep 2023 15:55:11 +0200 Subject: [PATCH 23/65] Store labels on ints, not on shorts. Otherwise we get crashes when we have more than 4k labels. Which is not what Labkit is optimized for but we will see that in a second time. In case we change our minds on the backing integer type, right now the labkit launcher and importer classes are generic. --- .../trackmate/gui/editor/LabkitImporter.java | 19 +++++---- .../trackmate/gui/editor/LabkitLauncher.java | 42 +++++++++++-------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 420828128..bb01cd41b 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -18,14 +18,15 @@ import net.imglib2.RandomAccessibleInterval; import net.imglib2.loops.LoopBuilder; import net.imglib2.roi.labeling.ImgLabeling; -import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; import net.imglib2.view.Views; /** * Re-import the edited segmentation made in Labkit into the TrackMate model it * started from. */ -public class LabkitImporter +public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > { private final Model model; @@ -80,8 +81,8 @@ public LabkitImporter( * index image. */ public void reimport( - final RandomAccessibleInterval< UnsignedShortType > novelIndexImg, - final RandomAccessibleInterval< UnsignedShortType > previousIndexImg, + final RandomAccessibleInterval< T > novelIndexImg, + final RandomAccessibleInterval< T > previousIndexImg, final int currentTimePoint ) { @@ -235,15 +236,15 @@ private void addNewSpot( final List< Spot > novelSpotList, final int currentTime } private final Set< Integer > getModifiedIDs( - final RandomAccessibleInterval< UnsignedShortType > novelIndexImg, - final RandomAccessibleInterval< UnsignedShortType > previousIndexImg ) + final RandomAccessibleInterval< T > novelIndexImg, + final RandomAccessibleInterval< T > previousIndexImg ) { final ConcurrentSkipListSet< Integer > modifiedIDs = new ConcurrentSkipListSet<>(); LoopBuilder.setImages( novelIndexImg, previousIndexImg ) .multiThreaded( false ) .forEachPixel( ( c, p ) -> { - final int ci = c.get(); - final int pi = p.get(); + final int ci = c.getInteger(); + final int pi = p.getInteger(); if ( ci == 0 && pi == 0 ) return; if ( ci != pi ) @@ -256,7 +257,7 @@ private final Set< Integer > getModifiedIDs( return modifiedIDs; } - private List< Spot > getSpots( final RandomAccessibleInterval< UnsignedShortType > rai ) + private List< Spot > getSpots( final RandomAccessibleInterval< T > rai ) { // Get all labels. final AtomicInteger max = new AtomicInteger( 0 ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 06a35a1d3..2238bc328 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -36,9 +36,10 @@ import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImgPlusViews; import net.imglib2.loops.LoopBuilder; +import net.imglib2.type.NativeType; import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.type.numeric.integer.UnsignedIntType; import net.imglib2.util.Util; import net.imglib2.view.Views; import sc.fiji.labkit.ui.LabkitFrame; @@ -47,7 +48,7 @@ import sc.fiji.labkit.ui.labeling.Labeling; import sc.fiji.labkit.ui.models.DefaultSegmentationModel; -public class LabkitLauncher +public class LabkitLauncher< T extends IntegerType< T > & NativeType< T > > { private final double[] calibration; @@ -92,7 +93,8 @@ protected void launch( final boolean singleTimePoint ) model.imageLabelingModel().labeling().set( labeling ); // Store a copy. - final RandomAccessibleInterval< UnsignedShortType > previousIndexImg = copy( labeling.getIndexImg() ); + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< T > previousIndexImg = copy( ( RandomAccessibleInterval< T > ) labeling.getIndexImg() ); // Show LabKit. String title = "Editing TrackMate data for " + imp.getShortTitle(); @@ -104,12 +106,12 @@ protected void launch( final boolean singleTimePoint ) final double dt = imp.getCalibration().frameInterval; labkit.onCloseListeners().addListener( () -> { @SuppressWarnings( "unchecked" ) - final RandomAccessibleInterval< UnsignedShortType > indexImg = ( RandomAccessibleInterval< UnsignedShortType > ) model.imageLabelingModel().labeling().get().getIndexImg(); + final RandomAccessibleInterval< T > indexImg = ( RandomAccessibleInterval< T > ) model.imageLabelingModel().labeling().get().getIndexImg(); reimport( indexImg, previousIndexImg, dt ); } ); } - private void reimport( final RandomAccessibleInterval< UnsignedShortType > indexImg, final RandomAccessibleInterval< UnsignedShortType > previousIndexImg, final double dt ) + private void reimport( final RandomAccessibleInterval< T > indexImg, final RandomAccessibleInterval< T > previousIndexImg, final double dt ) { try { @@ -121,7 +123,7 @@ private void reimport( final RandomAccessibleInterval< UnsignedShortType > index if ( modified.get() ) return null; chunk.forEachPixel( ( p1, p2 ) -> { - if ( p1.get() != p2.get() ) + if ( p1.getInteger() != p2.getInteger() ) { modified.set( true ); return; @@ -149,7 +151,7 @@ private void reimport( final RandomAccessibleInterval< UnsignedShortType > index if ( returnedValue != JOptionPane.YES_OPTION ) return; - final LabkitImporter reimporter = new LabkitImporter( trackmate.getModel(), calibration, dt ); + final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt ); if ( currentTimePoint < 0 ) { // All time-points. @@ -158,8 +160,8 @@ private void reimport( final RandomAccessibleInterval< UnsignedShortType > index log.setStatus( "Re-importing from Labkit..." ); for ( int t = 0; t < nTimepoints; t++ ) { - final RandomAccessibleInterval< UnsignedShortType > novelIndexImgThisFrame = Views.hyperSlice( indexImg, 3, t ); - final RandomAccessibleInterval< UnsignedShortType > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, 3, t ); + final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, 3, t ); + final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, 3, t ); reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); log.setProgress( ++t / ( double ) nTimepoints ); } @@ -182,13 +184,13 @@ private void reimport( final RandomAccessibleInterval< UnsignedShortType > index } } - private static Img< UnsignedShortType > copy( final RandomAccessibleInterval< ? extends IntegerType< ? > > in ) + private Img< T > copy( final RandomAccessibleInterval< T > in ) { - final ImgFactory< UnsignedShortType > factory = Util.getArrayOrCellImgFactory( in, new UnsignedShortType() ); - final Img< UnsignedShortType > out = factory.create( in ); + final ImgFactory< T > factory = Util.getArrayOrCellImgFactory( in, Util.getTypeFromInterval( in ) ); + final Img< T > out = factory.create( in ); LoopBuilder.setImages( in, out ) .multiThreaded() - .forEachPixel( ( i, o ) -> o.set( i.getInteger() ) ); + .forEachPixel( ( i, o ) -> o.setInteger( i.getInteger() ) ); return out; } @@ -270,7 +272,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, dims[ dim++ ] = imp.getNFrames(); // Raw image. - final Img< UnsignedShortType > lblImg = ArrayImgs.unsignedShorts( dims ); + final Img< UnsignedIntType > lblImg = ArrayImgs.unsignedInts( dims ); // Calibration. final double[] c = TMUtils.getSpatialCalibration( imp ); @@ -284,7 +286,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, calibration[ dim++ ] = 1.; // Label image holder. - final ImgPlus< UnsignedShortType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); + final ImgPlus< UnsignedIntType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); // Write spots in it with index = id + 1 and build a map index -> spot. final Map< Integer, Spot > spotIDs = new HashMap<>(); @@ -297,7 +299,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, final int timeDim = lblImgPlus.dimensionIndex( Axes.TIME ); for ( int t = 0; t < imp.getNFrames(); t++ ) { - final ImgPlus< UnsignedShortType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); + final ImgPlus< UnsignedIntType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); processFrame( lblImgPlusThisFrame, spots, t, spotIDs ); } } @@ -310,6 +312,11 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, final String name = label.name(); final int index = Integer.parseInt( name ); final Spot spot = spotIDs.get( index ); + if ( spot == null ) + { + System.out.println( "Spot is null for index " + index + "!!" ); // DEBUG + continue; + } label.setName( spot.getName() ); label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); } @@ -317,7 +324,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, return labeling; } - private static final void processFrame( final ImgPlus< UnsignedShortType > lblImgPlus, final SpotCollection spots, final int t, final Map< Integer, Spot > spotIDs ) + private static final void processFrame( final ImgPlus< UnsignedIntType > lblImgPlus, final SpotCollection spots, final int t, final Map< Integer, Spot > spotIDs ) { final Iterable< Spot > spotsThisFrame = spots.iterable( t, true ); for ( final Spot spot : spotsThisFrame ) @@ -353,6 +360,7 @@ public void run() disabler.disable(); try { + @SuppressWarnings( "rawtypes" ) final LabkitLauncher launcher = new LabkitLauncher( trackmate, ds, disabler ); launcher.launch( singleTimepoint ); } From 4d3a3267d70ac74e0506d36a5d1d5153a33f26a6 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 7 Jul 2024 20:18:04 +0200 Subject: [PATCH 24/65] Fix reimporting labels with large IDs in 2D. The re-importing of labels from Tabkit to TrackMate could fail for 2D images and labels with a large index. For instance, it failed consistently when trying to re-import labels with an index larger than 65643. This problem roots in the getSpots() method of LabkitImporter. It relies on a trick: We get the new label image, and create spots from this label image. But we want the new spots to keep track of the index in the label image they were generated from. For this, in 2D, we use the MaskUtils.fromLabelingWithRoi() method. These methods accept an image as last argument used to read a value in the label image within the spot, that is normally used for the quality value of the new spot. But the SpotRoiUtils.from2DLabelingWithRoi() method converted the extra image to ImagePlus (because I was lazy). So the label image was effectively cast on ushort for an IntegerType image, hence the problem with the max label being 65453. The solution is to rewrite the from2DLabelingWithRoi() so that it does not rely on converting to ImagePlus, but on good old iteration with imglib2. --- .../plugin/trackmate/detection/MaskUtils.java | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 53cf70fd8..12c01a0c5 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java @@ -26,14 +26,18 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutorService; + import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.Threads; -import ij.ImagePlus; import ij.gui.PolygonRoi; -import ij.measure.Measurements; import ij.process.FloatPolygon; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; import net.imglib2.Interval; +import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; @@ -45,7 +49,6 @@ import net.imglib2.histogram.Real1dBinMapper; import net.imglib2.img.Img; import net.imglib2.img.ImgFactory; -import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.roi.labeling.LabelRegion; import net.imglib2.roi.labeling.LabelRegionCursor; @@ -53,7 +56,6 @@ import net.imglib2.type.BooleanType; import net.imglib2.type.logic.BitType; import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.NumericType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.IntType; import net.imglib2.util.Util; @@ -428,7 +430,7 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static final < T extends RealType< T >, S extends NumericType< S > > List< Spot > fromThresholdWithROI( + public static final < T extends RealType< T >, S extends RealType< S > > List< Spot > fromThresholdWithROI( final RandomAccessible< T > input, final Interval interval, final double[] calibration, @@ -447,7 +449,7 @@ public static final < T extends RealType< T >, S extends NumericType< S > > List /** * Creates spots with ROIs from a 2D label image. The quality - * value is read from a secondary image, byt taking the max value in each + * value is read from a secondary image, by taking the max value in each * ROI. * * @param @@ -468,7 +470,7 @@ public static final < T extends RealType< T >, S extends NumericType< S > > List * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static < R extends IntegerType< R >, S extends NumericType< S > > List< Spot > fromLabelingWithROI( + public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > fromLabelingWithROI( final ImgLabeling< Integer, R > labeling, final Interval interval, final double[] calibration, @@ -497,9 +499,6 @@ public static < R extends IntegerType< R >, S extends NumericType< S > > List< S // Quality image. final List< Spot > spots = new ArrayList<>( polygons.size() ); - final ImagePlus qualityImp = ( null == qualityImage ) - ? null - : ImageJFunctions.wrap( qualityImage, "QualityImage" ); // Simplify them and compute a quality. for ( final Polygon polygon : polygons ) @@ -517,18 +516,6 @@ public static < R extends IntegerType< R >, S extends NumericType< S > > List< S if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) continue; - // Measure quality. - final double quality; - if ( null == qualityImp ) - { - quality = fRoi.getStatistics().area; - } - else - { - qualityImp.setRoi( fRoi ); - quality = qualityImp.getStatistics( Measurements.MIN_MAX ).max; - } - final Polygon fPolygon = fRoi.getPolygon(); final double[] xpoly = new double[ fPolygon.npoints ]; final double[] ypoly = new double[ fPolygon.npoints ]; @@ -538,7 +525,33 @@ public static < R extends IntegerType< R >, S extends NumericType< S > > List< S ypoly[ i ] = calibration[ 1 ] * ( interval.min( 1 ) + fPolygon.ypoints[ i ] - 0.5 ); } - spots.add( SpotRoi.createSpot( xpoly, ypoly, quality ) ); + final Spot spot = SpotRoi.createSpot( xpoly, ypoly, -1. ); + + // Measure quality. + final double quality; + if ( null == qualityImage ) + { + quality = fRoi.getStatistics().area; + } + else + { + final String name = "QualityImage"; + final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y }; + final double[] cal = new double[] { calibration[ 0 ], calibration[ 1 ] }; + final String[] units = new String[] { "unitX", "unitY" }; + final ImgPlus< S > qualityImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( qualityImage ), name, axes, cal, units ); + final IterableInterval< S > iterable = SpotUtil.iterable( spot, qualityImgPlus ); + double max = Double.NEGATIVE_INFINITY; + for ( final S s : iterable ) + { + final double val = s.getRealDouble(); + if ( val > max ) + max = val; + } + quality = max; + } + spot.putFeature( Spot.QUALITY, quality ); + spots.add( spot ); } return spots; } From 39e16fc6b0354509672cdbf2bdc438c89be349c5 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Sun, 8 Oct 2023 18:25:47 +0200 Subject: [PATCH 25/65] Fix javadoc errors. --- .../java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java | 4 ++-- .../java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index 426db661a..27ee12401 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -32,8 +32,8 @@ import sc.fiji.labkit.ui.inputimage.ImgPlusViewsOld; /** - * A {@link BdvShowable} from a {@link ImgPlus}, but with channel colors, min & - * max, channel visibility and display mode taken from a specified + * A {@link BdvShowable} from a {@link ImgPlus}, but with channel colors, min + * and max, channel visibility and display mode taken from a specified * {@link ImagePlus}. *

* Adapted from Matthias Arzt' ImgPlusBdvShowable, reusing code I made for diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index bb01cd41b..608dc5788 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -40,9 +40,6 @@ public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > * * @param model * the model to add, remove or edit spots in. - * @param currentTimePoint - * the time-point used for editing. If negative, then all the - * time-points in the input movie will be processed. * @param calibration * the spatial calibration array: [dx, dy dz]. * @param dt From 2d9edceae02bee3727a77cc798abce9103e37f87 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Thu, 8 Feb 2024 15:14:32 +0100 Subject: [PATCH 26/65] Make the spot editor work with 1-timepoint images. Noticed by @m-albert --- .../trackmate/gui/editor/LabkitLauncher.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 2238bc328..e6099a9ef 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -75,7 +75,7 @@ public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, fina /** * Launches the Labkit editor. - * + * * @param singleTimePoint * if true, will launch the editor using only the * time-point currently displayed in the main view. Otherwise, @@ -135,11 +135,15 @@ private void reimport( final RandomAccessibleInterval< T > indexImg, final Rando return; // Message the user. - final String msg = ( currentTimePoint < 0 ) + final long nTimepoints = indexImg.dimension( 3 ); + final String msg = ( nTimepoints <= 1 ) ? "Commit the changes made to the\n" - + "segmentation in whole movie?" - : "Commit the changes made to the\n" - + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; + + "segmentation in the image?" + : ( currentTimePoint < 0 ) + ? "Commit the changes made to the\n" + + "segmentation in whole movie?" + : "Commit the changes made to the\n" + + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; final String title = "Commit edits to TrackMate"; final int returnedValue = JOptionPane.showConfirmDialog( null, @@ -152,10 +156,9 @@ private void reimport( final RandomAccessibleInterval< T > indexImg, final Rando return; final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt ); - if ( currentTimePoint < 0 ) + if ( currentTimePoint < 0 && nTimepoints > 1 ) { // All time-points. - final long nTimepoints = indexImg.dimension( 3 ); final Logger log = Logger.IJ_LOGGER; log.setStatus( "Re-importing from Labkit..." ); for ( int t = 0; t < nTimepoints; t++ ) @@ -171,7 +174,8 @@ private void reimport( final RandomAccessibleInterval< T > indexImg, final Rando else { // Only one. - reimporter.reimport( indexImg, previousIndexImg, currentTimePoint ); + final int localT = Math.max( 0, currentTimePoint ); + reimporter.reimport( indexImg, previousIndexImg, localT ); } } catch ( final Exception e ) @@ -197,7 +201,7 @@ private Img< T > copy( final RandomAccessibleInterval< T > in ) /** * Creates a new {@link DatasetInputImage} from the specified * {@link ImagePlus}. The embedded label image is empty. - * + * * @param imp * the input {@link ImagePlus}. * @param singleTimePoint @@ -216,10 +220,10 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si // Possibly reslice for current time-point. final ImpBdvShowable showable; final ImgPlus inputImg; - if ( singleTimePoint ) + final int timeAxis = src.dimensionIndex( Axes.TIME ); + if ( singleTimePoint && timeAxis >= 0 ) { this.currentTimePoint = imp.getFrame() - 1; - final int timeAxis = src.dimensionIndex( Axes.TIME ); inputImg = ImgPlusViews.hyperSlice( src, timeAxis, currentTimePoint ); showable = ImpBdvShowable.fromImp( inputImg, imp ); } @@ -236,7 +240,7 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si * Prepare the label image for annotation. The labeling is created and each * of its labels receive the name and the color from the spot it is created * from. - * + * * @param imp * the source image plus. * @param spots From e4eb1bc8fcc23b6327a6114a601911418475ddbd Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Mon, 11 Mar 2024 14:54:13 +0100 Subject: [PATCH 27/65] Fix import of spots in LabKit for 2D single time-point images. --- .../java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index e6099a9ef..fa89eb424 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -330,7 +330,9 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, private static final void processFrame( final ImgPlus< UnsignedIntType > lblImgPlus, final SpotCollection spots, final int t, final Map< Integer, Spot > spotIDs ) { - final Iterable< Spot > spotsThisFrame = spots.iterable( t, true ); + // If we have a single timepoint, don't use -1 to retrieve spots. + final int lt = t < 0 ? 0 : t; + final Iterable< Spot > spotsThisFrame = spots.iterable( lt, true ); for ( final Spot spot : spotsThisFrame ) { final int index = spot.ID() + 1; From 8c9961b3b8093548d04a84c67b876769bd76e8b5 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Mon, 11 Mar 2024 14:54:32 +0100 Subject: [PATCH 28/65] Protect against weird error caused by empty labels. --- .../fiji/plugin/trackmate/gui/editor/LabkitImporter.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 608dc5788..cba71804c 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -37,7 +37,7 @@ public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > /** * Creates a new re-importer. - * + * * @param model * the model to add, remove or edit spots in. * @param calibration @@ -65,7 +65,7 @@ public LabkitImporter( *

* To properly detect modifications, the indices in the label images must * correspond to the spot ID + 1 (index = id + 1). - * + * * @param novelIndexImg * the new index image of the labeling model, that represents the * TrackMate model in the specified time-point after @@ -135,12 +135,13 @@ public void reimport( * A new one (possible several) I cannot find in the * previous list -> add as a new spot. */ - addNewSpot( novelSpotList, currentTimePoint ); + if ( novelSpotList != null ) + addNewSpot( novelSpotList, currentTimePoint ); } else if ( novelSpotList == null || novelSpotList.isEmpty() ) { /* - * One I add in the previous spot list, but that has + * One I had in the previous spot list, but that has * disappeared. Remove it. */ model.removeSpot( previousSpot ); From e65687cee02e1fb230906eec55431221237a9a2b Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Tue, 28 Nov 2023 16:08:48 +0100 Subject: [PATCH 29/65] setFont() utility. --- src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java index a2d82584d..3b59b62d8 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java +++ b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java @@ -87,6 +87,12 @@ public static final void selectAllOnFocus( final JTextField tf ) tf.addFocusListener( selectAllFocusListener ); } + public static final void setFont( final JComponent panel, final Font font ) + { + for ( final Component c : panel.getComponents() ) + c.setFont( font ); + } + /** * Returns the black color or white color depending on the specified * background color, to ensure proper readability of the text on said From 1d95bea5bceba4ccd9f06be534fe9bbdbbf8c8cf Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Thu, 28 Mar 2024 20:18:00 +0100 Subject: [PATCH 30/65] The detection preview is cancelable. Provided that the detector that is called is cancelable. --- .../trackmate/util/DetectionPreview.java | 77 ++++++++++++------- .../trackmate/util/DetectionPreviewPanel.java | 21 ++++- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java index d0edab222..3bf2102d9 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java @@ -48,6 +48,8 @@ public class DetectionPreview private final DetectionPreviewPanel panel; + private TrackMate trackmate; + protected DetectionPreview( final Model model, final Settings settings, @@ -66,6 +68,7 @@ protected DetectionPreview( detectionSettingsSupplier.get(), currentFrameSupplier.get(), thresholdKey ) ); + panel.btnCancel.addActionListener( l -> cancel() ); } public DetectionPreviewPanel getPanel() @@ -86,7 +89,9 @@ protected void preview( final int frame, final String thresholdKey ) { - panel.btnPreview.setEnabled( false ); + panel.btnPreview.setVisible( false ); + panel.btnCancel.setVisible( true ); + panel.btnCancel.setEnabled( true ); Threads.run( "TrackMate preview detection thread", () -> { try @@ -115,7 +120,8 @@ protected void preview( } finally { - panel.btnPreview.setEnabled( true ); + panel.btnPreview.setVisible( true ); + panel.btnCancel.setVisible( false ); } } ); } @@ -161,38 +167,53 @@ protected Pair< Model, Double > runPreviewDetection( lSettings.detectorFactory = detectorFactory; lSettings.detectorSettings = new HashMap<>( detectorSettings ); - // Does this detector have a THRESHOLD parameter? - final boolean hasThreshold = ( thresholdKey != null ) && ( detectorSettings.containsKey( thresholdKey ) ); - final double threshold; - if ( hasThreshold ) - { - threshold = ( ( Double ) detectorSettings.get( thresholdKey ) ).doubleValue(); - lSettings.detectorSettings.put( thresholdKey, Double.valueOf( Double.NEGATIVE_INFINITY ) ); - } - else + this.trackmate = new TrackMate( lSettings ); + + try { - threshold = Double.NaN; - } + // Does this detector have a THRESHOLD parameter? + final boolean hasThreshold = ( thresholdKey != null ) && ( detectorSettings.containsKey( thresholdKey ) ); + final double threshold; + if ( hasThreshold ) + { + threshold = ( ( Double ) detectorSettings.get( thresholdKey ) ).doubleValue(); + lSettings.detectorSettings.put( thresholdKey, Double.valueOf( Double.NEGATIVE_INFINITY ) ); + } + else + { + threshold = Double.NaN; + } - // Execute preview. - final TrackMate trackmate = new TrackMate( lSettings ); - trackmate.getModel().setLogger( panel.logger ); + // Execute preview. + trackmate.getModel().setLogger( panel.logger ); - final boolean detectionOk = trackmate.execDetection(); - if ( !detectionOk ) + final boolean detectionOk = trackmate.execDetection(); + if ( !detectionOk ) + { + panel.logger.error( trackmate.getErrorMessage() ); + return null; + } + + if ( hasThreshold ) + // Filter by the initial threshold value. + trackmate.getModel().getSpots().filter( new FeatureFilter( Spot.QUALITY, threshold, true ) ); + else + // Make them all visible. + trackmate.getModel().getSpots().setVisible( true ); + + return new ValuePair< Model, Double >( trackmate.getModel(), Double.valueOf( threshold ) ); + } + finally { - panel.logger.error( trackmate.getErrorMessage() ); - return null; + this.trackmate = null; } + } - if ( hasThreshold ) - // Filter by the initial threshold value. - trackmate.getModel().getSpots().filter( new FeatureFilter( Spot.QUALITY, threshold, true ) ); - else - // Make them all visible. - trackmate.getModel().getSpots().setVisible( true ); - - return new ValuePair< Model, Double >( trackmate.getModel(), Double.valueOf( threshold ) ); + private void cancel() + { + panel.btnCancel.setEnabled( false ); + if ( null != trackmate ) + trackmate.cancel( "User canceled preview" ); } protected void updateModelAndHistogram( final Model targetModel, final Model sourceModel, final int frame, final double threshold ) diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java index f3adad062..0c6f887be 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java @@ -22,6 +22,7 @@ package fiji.plugin.trackmate.util; import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; +import static fiji.plugin.trackmate.gui.Icons.CANCEL_ICON; import static fiji.plugin.trackmate.gui.Icons.PREVIEW_ICON; import java.awt.GridBagConstraints; @@ -48,12 +49,19 @@ public class DetectionPreviewPanel extends JPanel + "get rid of them later." + ""; + private static final String TOOLTIP_CANCEL = "" + + "Cancel the current preview." + + ""; + final Logger logger; final JButton btnPreview; + final JButton btnCancel; + final QualityHistogramChart chart; + public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final String axisLabel ) { final GridBagLayout gridBagLayout = new GridBagLayout(); @@ -84,12 +92,21 @@ public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final Strin this.btnPreview = new JButton( "Preview", PREVIEW_ICON ); btnPreview.setToolTipText( TOOLTIP_PREVIEW ); + btnPreview.setFont( SMALL_FONT ); + this.btnCancel = new JButton( "Cancel", CANCEL_ICON ); + btnCancel.setToolTipText( TOOLTIP_CANCEL ); + btnCancel.setVisible( false ); + btnCancel.setFont( SMALL_FONT ); + + final JPanel btnPanel = new JPanel(); + btnPanel.add( btnPreview ); + btnPanel.add( btnCancel ); + final GridBagConstraints gbcBtnPreview = new GridBagConstraints(); gbcBtnPreview.anchor = GridBagConstraints.NORTHEAST; gbcBtnPreview.insets = new Insets( 5, 5, 0, 0 ); gbcBtnPreview.gridx = 1; gbcBtnPreview.gridy = 1; - this.add( btnPreview, gbcBtnPreview ); - btnPreview.setFont( SMALL_FONT ); + this.add( btnPanel, gbcBtnPreview ); } } From 0bd86ec35b902489d08495e3e96c6d8c1230652a Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Tue, 21 May 2024 14:23:00 +0200 Subject: [PATCH 31/65] Fix preview panel size in free-size layouts. --- .../java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java index 0c6f887be..604162741 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java @@ -25,6 +25,7 @@ import static fiji.plugin.trackmate.gui.Icons.CANCEL_ICON; import static fiji.plugin.trackmate.gui.Icons.PREVIEW_ICON; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; @@ -108,5 +109,7 @@ public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final Strin gbcBtnPreview.gridx = 1; gbcBtnPreview.gridy = 1; this.add( btnPanel, gbcBtnPreview ); + + setPreferredSize( new Dimension( 240, 100 ) ); } } From d6fdfb5a2a84773eb4ecbee04d854480f452bab1 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Tue, 9 Jul 2024 18:36:30 +0200 Subject: [PATCH 32/65] Detection preview panel tweak. Make sure the detection preview panel is slanted at the bottom of its display. --- .../trackmate/util/DetectionPreviewPanel.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java index 604162741..e2b36983b 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java @@ -32,6 +32,7 @@ import java.util.function.DoubleConsumer; import javax.swing.JButton; +import javax.swing.JLabel; import javax.swing.JPanel; import fiji.plugin.trackmate.Logger; @@ -67,18 +68,20 @@ public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final Strin { final GridBagLayout gridBagLayout = new GridBagLayout(); gridBagLayout.columnWeights = new double[] { 1.0, 0.0 }; - gridBagLayout.rowWeights = new double[] { 0., 0. }; - gridBagLayout.rowHeights = new int[] { 120, 20 }; + gridBagLayout.rowWeights = new double[] { 1., 0., 0. }; + gridBagLayout.rowHeights = new int[] { 0, 120, 20 }; setLayout( gridBagLayout ); + add( new JLabel() ); + this.chart = new QualityHistogramChart( thresholdUpdater, axisLabel ); final GridBagConstraints gbcHistogram = new GridBagConstraints(); gbcHistogram.gridwidth = 2; gbcHistogram.insets = new Insets( 0, 0, 5, 0 ); gbcHistogram.fill = GridBagConstraints.BOTH; gbcHistogram.gridx = 0; - gbcHistogram.gridy = 0; + gbcHistogram.gridy = 1; add( chart, gbcHistogram ); final JLabelLogger labelLogger = new JLabelLogger(); @@ -87,7 +90,7 @@ public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final Strin gbcLabelLogger.insets = new Insets( 5, 5, 0, 5 ); gbcLabelLogger.fill = GridBagConstraints.BOTH; gbcLabelLogger.gridx = 0; - gbcLabelLogger.gridy = 1; + gbcLabelLogger.gridy = 2; add( labelLogger, gbcLabelLogger ); this.logger = labelLogger.getLogger(); @@ -107,7 +110,7 @@ public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final Strin gbcBtnPreview.anchor = GridBagConstraints.NORTHEAST; gbcBtnPreview.insets = new Insets( 5, 5, 0, 0 ); gbcBtnPreview.gridx = 1; - gbcBtnPreview.gridy = 1; + gbcBtnPreview.gridy = 2; this.add( btnPanel, gbcBtnPreview ); setPreferredSize( new Dimension( 240, 100 ) ); From de5de593262af69c71107a81e77a8692b9a0dc68 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Wed, 10 Jul 2024 20:53:25 +0200 Subject: [PATCH 33/65] Fixed a difficult bug with the LabKit editor reimporter. The bug was causing weird issues with unedited spots being deleted, unedited spots being duplicated etc. It took me really long to understand the cause. It was hidden in the step where we go from a label image to a collection of spots. Because spots are polygons, with simplified contours, there might be some pixels on the edges of the object that are not strictly inside the label. In this importer, we read the label value in one go, by storing it in the QUALITY value of the spot, in the MaskUtils class. But since the spots have simplified contours, and since the QUALITY value is the maximal value iterated over, our approach might fail on border cases: - when the contout is approximated and include pixels from another object - and when this object has a label value higher than the lael of the spot. This commit include a temporary fix: we reiterate over the spot but takes the median value iterated over, to make sure we read the correct value for the label. Other attempts will follow, for reference. But a true fix involves making a method that returns a map from label value to spot. --- .../trackmate/gui/editor/LabkitImporter.java | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index cba71804c..98875ff1a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -1,6 +1,7 @@ package fiji.plugin.trackmate.gui.editor; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -10,11 +11,16 @@ import java.util.concurrent.atomic.AtomicInteger; import org.jgrapht.graph.DefaultWeightedEdge; +import org.scijava.util.IntArray; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.detection.MaskUtils; +import fiji.plugin.trackmate.util.SpotUtil; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; import net.imglib2.RandomAccessibleInterval; import net.imglib2.loops.LoopBuilder; import net.imglib2.roi.labeling.ImgLabeling; @@ -82,11 +88,9 @@ public void reimport( final RandomAccessibleInterval< T > previousIndexImg, final int currentTimePoint ) { - // Collect ids of spots that have been modified. id = index - 1 final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); final int nModified = modifiedIDs.size(); - if ( nModified == 0 ) return; @@ -213,6 +217,7 @@ else if ( target == previousSpot ) // Deal with the other ones. final HashSet< Spot > extraSpots = new HashSet<>( novelSpotList ); extraSpots.remove( mainNovelSpot ); + int i = 1; for ( final Spot s : extraSpots ) { @@ -223,7 +228,7 @@ else if ( target == previousSpot ) } } - private void addNewSpot( final List< Spot > novelSpotList, final int currentTimePoint ) + private void addNewSpot( final Iterable< Spot > novelSpotList, final int currentTimePoint ) { for ( final Spot spot : novelSpotList ) { @@ -270,11 +275,36 @@ private List< Spot > getSpots( final RandomAccessibleInterval< T > rai ) final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); final boolean simplify = true; - return MaskUtils.fromLabelingWithROI( + final List< Spot > spots = MaskUtils.fromLabelingWithROI( labeling, labeling, calibration, simplify, - rai ); + null ); + + /* + * Read the label from the index image and store it in the quality + * feature. To ensure we don't have weirdness with the non + * convex-objects we iterate through all pixels and take the median. + */ + + final String name = "LabelImgPlus"; + final boolean is3D = rai.numDimensions() == 3; + final AxisType[] axes = ( is3D ) ? new AxisType[] { Axes.X, Axes.Y, Axes.Z } : new AxisType[] { Axes.X, Axes.Y }; + final double[] cal = ( is3D ) ? new double[] { calibration[ 0 ], calibration[ 1 ], calibration[ 2 ] } : new double[] { calibration[ 0 ], calibration[ 1 ] }; + final String[] units = ( is3D ) ? new String[] { "unitX", "unitY", "unitZ" } : new String[] { "unitX", "unitY" }; + final ImgPlus< T > labelImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( rai ), name, axes, cal, units ); + + final IntArray arr = new IntArray(); + for ( final Spot spot : spots ) + { + arr.clear(); + SpotUtil.iterable( spot, labelImgPlus ).forEach( p -> arr.addValue( p.getInteger() ) ); + Arrays.sort( arr.getArray(), 0, arr.size() ); + final int label = arr.getValue( arr.size() / 2 ); + spot.putFeature( Spot.QUALITY, Double.valueOf( label ) ); + } + + return spots; } } From 2c56943fe8acc1e0c0cddf8603c847934fbef268 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Wed, 10 Jul 2024 20:55:14 +0200 Subject: [PATCH 34/65] Another fix for the LabKit reimporter bug. This time we fix it by creating spots that do not have a simplified contours. In that case we strictly iterate over the pixels inside label and get the correct value. However the created spots have a pixelated aspect (contours are not simplified), which might not be what the user wants. We should let them choose. Still not the perfect solution, as mentionned in the previous commmit. --- .../trackmate/gui/editor/LabkitImporter.java | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 98875ff1a..fbfebe142 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -1,7 +1,6 @@ package fiji.plugin.trackmate.gui.editor; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -11,16 +10,11 @@ import java.util.concurrent.atomic.AtomicInteger; import org.jgrapht.graph.DefaultWeightedEdge; -import org.scijava.util.IntArray; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.detection.MaskUtils; -import fiji.plugin.trackmate.util.SpotUtil; -import net.imagej.ImgPlus; -import net.imagej.axis.Axes; -import net.imagej.axis.AxisType; import net.imglib2.RandomAccessibleInterval; import net.imglib2.loops.LoopBuilder; import net.imglib2.roi.labeling.ImgLabeling; @@ -274,37 +268,13 @@ private List< Spot > getSpots( final RandomAccessibleInterval< T > rai ) indices.add( Integer.valueOf( i + 1 ) ); final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); - final boolean simplify = true; + final boolean simplify = false; // needed to properly read label values. final List< Spot > spots = MaskUtils.fromLabelingWithROI( labeling, labeling, calibration, simplify, - null ); - - /* - * Read the label from the index image and store it in the quality - * feature. To ensure we don't have weirdness with the non - * convex-objects we iterate through all pixels and take the median. - */ - - final String name = "LabelImgPlus"; - final boolean is3D = rai.numDimensions() == 3; - final AxisType[] axes = ( is3D ) ? new AxisType[] { Axes.X, Axes.Y, Axes.Z } : new AxisType[] { Axes.X, Axes.Y }; - final double[] cal = ( is3D ) ? new double[] { calibration[ 0 ], calibration[ 1 ], calibration[ 2 ] } : new double[] { calibration[ 0 ], calibration[ 1 ] }; - final String[] units = ( is3D ) ? new String[] { "unitX", "unitY", "unitZ" } : new String[] { "unitX", "unitY" }; - final ImgPlus< T > labelImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( rai ), name, axes, cal, units ); - - final IntArray arr = new IntArray(); - for ( final Spot spot : spots ) - { - arr.clear(); - SpotUtil.iterable( spot, labelImgPlus ).forEach( p -> arr.addValue( p.getInteger() ) ); - Arrays.sort( arr.getArray(), 0, arr.size() ); - final int label = arr.getValue( arr.size() / 2 ); - spot.putFeature( Spot.QUALITY, Double.valueOf( label ) ); - } - + rai ); return spots; } } From fc19713b9d23580860d4dd50084966676192c91c Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Wed, 10 Jul 2024 21:08:26 +0200 Subject: [PATCH 35/65] Label image to spots: also returns the label corresponding to created spots. --- .../plugin/trackmate/detection/MaskUtils.java | 160 ++++++++++++------ 1 file changed, 110 insertions(+), 50 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 12c01a0c5..5ead9f1e1 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java @@ -23,8 +23,10 @@ import java.awt.Polygon; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import fiji.plugin.trackmate.Spot; @@ -441,7 +443,7 @@ public static final < T extends RealType< T >, S extends RealType< S > > List< S { if ( input.numDimensions() != 2 ) throw new IllegalArgumentException( "Can only process 2D images with this method, but got " + input.numDimensions() + "D." ); - + // Get labeling. final ImgLabeling< Integer, IntType > labeling = toLabeling( input, interval, threshold, numThreads ); return fromLabelingWithROI( labeling, interval, calibration, simplify, qualityImage ); @@ -476,15 +478,64 @@ public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot final double[] calibration, final boolean simplify, final RandomAccessibleInterval< S > qualityImage ) + { + final Map< Integer, List< Spot > > map = fromLabelingWithROIMap( labeling, interval, calibration, simplify, qualityImage ); + final List spots = new ArrayList<>(); + for ( final List< Spot > s : map.values() ) + spots.addAll( s ); + + return spots; + } + + /** + * Creates spots with ROIs from a 2D label image. The quality + * value is read from a secondary image, by taking the max value in each + * ROI. + *

+ * The spots are returned in a map, where the key is the integer value of + * the label they correspond to in the label image. Because one spot + * corresponds to one connected component in the label image, there might be + * several spots for a label, hence the values of the map are list of spots. + * + * @param + * the type that backs-up the labeling. + * @param + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 2D.. + * @param interval + * the interval, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the polygon will be post-processed to be + * smoother and contain less points. + * @param qualityImage + * the image in which to read the quality value. + * @return a map linking the label integer value to the list of spots, with + * ROI, it corresponds to. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integer, List< Spot > > fromLabelingWithROIMap( + final ImgLabeling< Integer, R > labeling, + final Interval interval, + final double[] calibration, + final boolean simplify, + final RandomAccessibleInterval< S > qualityImage ) { if ( labeling.numDimensions() != 2 ) throw new IllegalArgumentException( "Can only process 2D images with this method, but got " + labeling.numDimensions() + "D." ); final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); - // Parse regions to create polygons on boundaries. - final List< Polygon > polygons = new ArrayList<>( regions.getExistingLabels().size() ); + /* + * Map of label in the label image to a collection of polygons around + * this label. Because 1 polygon correspond to 1 connected component, + * there might be several polygons for a label. + */ + final Map< Integer, List< Polygon > > polygonsMap = new HashMap<>( regions.getExistingLabels().size() ); final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + // Parse regions to create polygons on boundaries. while ( iterator.hasNext() ) { final LabelRegion< Integer > region = iterator.next(); @@ -494,66 +545,75 @@ public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot for ( final Polygon polygon : pp ) polygon.translate( ( int ) region.min( 0 ), ( int ) region.min( 1 ) ); - polygons.addAll( pp ); + final Integer label = region.getLabel(); + polygonsMap.put( label, pp ); } - // Quality image. - final List< Spot > spots = new ArrayList<>( polygons.size() ); + + // Storage for results. + final Map< Integer, List< Spot > > output = new HashMap<>( polygonsMap.size() ); // Simplify them and compute a quality. - for ( final Polygon polygon : polygons ) + for ( final Integer label : polygonsMap.keySet() ) { - final PolygonRoi roi = new PolygonRoi( polygon, PolygonRoi.POLYGON ); + final List< Spot > spots = new ArrayList<>( polygonsMap.size() ); + output.put( label, spots ); + + final List< Polygon > polygons = polygonsMap.get( label ); + for ( final Polygon polygon : polygons ) + { + final PolygonRoi roi = new PolygonRoi( polygon, PolygonRoi.POLYGON ); - // Create Spot ROI. - final PolygonRoi fRoi; - if ( simplify ) - fRoi = simplify( roi, SMOOTH_INTERVAL, DOUGLAS_PEUCKER_MAX_DISTANCE ); - else - fRoi = roi; + // Create Spot ROI. + final PolygonRoi fRoi; + if ( simplify ) + fRoi = simplify( roi, SMOOTH_INTERVAL, DOUGLAS_PEUCKER_MAX_DISTANCE ); + else + fRoi = roi; - // Don't include ROIs that have been shrunk to < 1 pixel. - if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) - continue; + // Don't include ROIs that have been shrunk to < 1 pixel. + if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) + continue; - final Polygon fPolygon = fRoi.getPolygon(); - final double[] xpoly = new double[ fPolygon.npoints ]; - final double[] ypoly = new double[ fPolygon.npoints ]; - for ( int i = 0; i < fPolygon.npoints; i++ ) - { - xpoly[ i ] = calibration[ 0 ] * ( interval.min( 0 ) + fPolygon.xpoints[ i ] - 0.5 ); - ypoly[ i ] = calibration[ 1 ] * ( interval.min( 1 ) + fPolygon.ypoints[ i ] - 0.5 ); - } + final Polygon fPolygon = fRoi.getPolygon(); + final double[] xpoly = new double[ fPolygon.npoints ]; + final double[] ypoly = new double[ fPolygon.npoints ]; + for ( int i = 0; i < fPolygon.npoints; i++ ) + { + xpoly[ i ] = calibration[ 0 ] * ( interval.min( 0 ) + fPolygon.xpoints[ i ] - 0.5 ); + ypoly[ i ] = calibration[ 1 ] * ( interval.min( 1 ) + fPolygon.ypoints[ i ] - 0.5 ); + } - final Spot spot = SpotRoi.createSpot( xpoly, ypoly, -1. ); + final Spot spot = SpotRoi.createSpot( xpoly, ypoly, -1. ); - // Measure quality. - final double quality; - if ( null == qualityImage ) - { - quality = fRoi.getStatistics().area; - } - else - { - final String name = "QualityImage"; - final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y }; - final double[] cal = new double[] { calibration[ 0 ], calibration[ 1 ] }; - final String[] units = new String[] { "unitX", "unitY" }; - final ImgPlus< S > qualityImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( qualityImage ), name, axes, cal, units ); - final IterableInterval< S > iterable = SpotUtil.iterable( spot, qualityImgPlus ); - double max = Double.NEGATIVE_INFINITY; - for ( final S s : iterable ) + // Measure quality. + final double quality; + if ( null == qualityImage ) { - final double val = s.getRealDouble(); - if ( val > max ) - max = val; + quality = fRoi.getStatistics().area; } - quality = max; + else + { + final String name = "QualityImage"; + final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y }; + final double[] cal = new double[] { calibration[ 0 ], calibration[ 1 ] }; + final String[] units = new String[] { "unitX", "unitY" }; + final ImgPlus< S > qualityImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( qualityImage ), name, axes, cal, units ); + final IterableInterval< S > iterable = SpotUtil.iterable( spot, qualityImgPlus ); + double max = Double.NEGATIVE_INFINITY; + for ( final S s : iterable ) + { + final double val = s.getRealDouble(); + if ( val > max ) + max = val; + } + quality = max; + } + spot.putFeature( Spot.QUALITY, quality ); + spots.add( spot ); } - spot.putFeature( Spot.QUALITY, quality ); - spots.add( spot ); } - return spots; + return output; } private static final double distanceSquaredBetweenPoints( final double vx, final double vy, final double wx, final double wy ) @@ -649,7 +709,7 @@ private static final void douglasPeucker( final List< double[] > list, final int */ public static final List< double[] > douglasPeucker( final List< double[] > list, final double epsilon ) { - final List< double[] > resultList = new ArrayList< >(); + final List< double[] > resultList = new ArrayList<>(); douglasPeucker( list, 0, list.size(), epsilon, resultList ); return resultList; } From f886b653d836b6a557a72e7c00e5abcef842fbbe Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Wed, 10 Jul 2024 21:08:50 +0200 Subject: [PATCH 36/65] Do not execute the LabKit reimporter in the AWT thread. --- .../trackmate/gui/editor/LabkitLauncher.java | 143 +++++++++--------- 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index fa89eb424..05271c4c5 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -113,79 +113,86 @@ protected void launch( final boolean singleTimePoint ) private void reimport( final RandomAccessibleInterval< T > indexImg, final RandomAccessibleInterval< T > previousIndexImg, final double dt ) { - try + new Thread( "TrackMate-LabKit-Importer-thread" ) { - // Do we have something to reimport? - final AtomicBoolean modified = new AtomicBoolean( false ); - LoopBuilder.setImages( previousIndexImg, indexImg ) - .multiThreaded() - .forEachChunk( chunk -> { - if ( modified.get() ) - return null; - chunk.forEachPixel( ( p1, p2 ) -> { - if ( p1.getInteger() != p2.getInteger() ) - { - modified.set( true ); - return; - } - } ); - return null; - } ); - if ( !modified.get() ) - return; - - // Message the user. - final long nTimepoints = indexImg.dimension( 3 ); - final String msg = ( nTimepoints <= 1 ) - ? "Commit the changes made to the\n" - + "segmentation in the image?" - : ( currentTimePoint < 0 ) - ? "Commit the changes made to the\n" - + "segmentation in whole movie?" - : "Commit the changes made to the\n" - + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; - final String title = "Commit edits to TrackMate"; - final int returnedValue = JOptionPane.showConfirmDialog( - null, - msg, - title, - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, - Icons.TRACKMATE_ICON ); - if ( returnedValue != JOptionPane.YES_OPTION ) - return; - - final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt ); - if ( currentTimePoint < 0 && nTimepoints > 1 ) + @Override + public void run() { - // All time-points. - final Logger log = Logger.IJ_LOGGER; - log.setStatus( "Re-importing from Labkit..." ); - for ( int t = 0; t < nTimepoints; t++ ) + try { - final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, 3, t ); - final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, 3, t ); - reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); - log.setProgress( ++t / ( double ) nTimepoints ); + // Do we have something to reimport? + final AtomicBoolean modified = new AtomicBoolean( false ); + LoopBuilder.setImages( previousIndexImg, indexImg ) + .multiThreaded() + .forEachChunk( chunk -> { + if ( modified.get() ) + return null; + chunk.forEachPixel( ( p1, p2 ) -> { + if ( p1.getInteger() != p2.getInteger() ) + { + modified.set( true ); + return; + } + } ); + return null; + } ); + if ( !modified.get() ) + return; + + // Message the user. + final long nTimepoints = indexImg.dimension( 3 ); + final String msg = ( nTimepoints <= 1 ) + ? "Commit the changes made to the\n" + + "segmentation in the image?" + : ( currentTimePoint < 0 ) + ? "Commit the changes made to the\n" + + "segmentation in whole movie?" + : "Commit the changes made to the\n" + + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; + final String title = "Commit edits to TrackMate"; + final int returnedValue = JOptionPane.showConfirmDialog( + null, + msg, + title, + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + Icons.TRACKMATE_ICON ); + if ( returnedValue != JOptionPane.YES_OPTION ) + return; + + final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt ); + if ( currentTimePoint < 0 && nTimepoints > 1 ) + { + // All time-points. + final Logger log = Logger.IJ_LOGGER; + log.setStatus( "Re-importing from Labkit..." ); + for ( int t = 0; t < nTimepoints; t++ ) + { + final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, 3, t ); + final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, 3, t ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); + log.setProgress( ++t / ( double ) nTimepoints ); + } + log.setStatus( "" ); + log.setProgress( 0. ); + } + else + { + // Only one. + final int localT = Math.max( 0, currentTimePoint ); + reimporter.reimport( indexImg, previousIndexImg, localT ); + } + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + finally + { + disabler.reenable(); } - log.setStatus( "" ); - log.setProgress( 0. ); - } - else - { - // Only one. - final int localT = Math.max( 0, currentTimePoint ); - reimporter.reimport( indexImg, previousIndexImg, localT ); } - } - catch ( final Exception e ) - { - e.printStackTrace(); - } - finally - { - disabler.reenable(); - } + }.start(); } private Img< T > copy( final RandomAccessibleInterval< T > in ) From d1ca968743b4b1e25bd5077e964334b9fbd2298e Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Thu, 11 Jul 2024 14:17:57 +0200 Subject: [PATCH 37/65] Simplify the LabKit importer. Use the new label immg to spot method to get spots AND the label they come from. --- .../trackmate/gui/editor/LabkitImporter.java | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index fbfebe142..7e5c19bf9 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -97,36 +97,17 @@ public void reimport( spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); /* - * Get all the spots present in the new image. Because we specified - * the novel label image as 'quality' image, they have a quality - * value equal to the index in the label image (id+1). + * Get all the spots present in the new image, as a map against the + * label in the novel index image. This label value corresponds to + * the ids of the spots in the previous index image (label = id+1). */ - final List< Spot > novelSpots = getSpots( novelIndexImg ); - - /* - * Map of novel spots against the ID taken from the index of the - * novel label image. Normally, this index, and hence the novel id, - * corresponds to the id of previous spots. If one of the novel spot - * has an id we cannot find in the previous spot list, it means that - * it is a new one. - * - * Careful! The user might have created several connected components - * with the same label in LabKit, which will result in having - * several spots with the same quality value. We don't want to loose - * them, so the map is that of a id to a list of spots. - */ - final Map< Integer, List< Spot > > novelSpotIDs = new HashMap<>(); - novelSpots.forEach( s -> { - final int id = Integer.valueOf( s.getFeature( Spot.QUALITY ).intValue() - 1 ); - final List< Spot > list = novelSpotIDs.computeIfAbsent( Integer.valueOf( id ), ( i ) -> new ArrayList<>() ); - list.add( s ); - } ); + final Map< Integer, List< Spot > > novelSpots = getSpots( novelIndexImg ); // Update model for those spots. for ( final int id : modifiedIDs ) { final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); - final List< Spot > novelSpotList = novelSpotIDs.get( Integer.valueOf( id ) ); + final List< Spot > novelSpotList = novelSpots.get( Integer.valueOf( id ) + 1 ); if ( previousSpot == null ) { /* @@ -254,7 +235,7 @@ private final Set< Integer > getModifiedIDs( return modifiedIDs; } - private List< Spot > getSpots( final RandomAccessibleInterval< T > rai ) + private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T > rai ) { // Get all labels. final AtomicInteger max = new AtomicInteger( 0 ); @@ -269,7 +250,7 @@ private List< Spot > getSpots( final RandomAccessibleInterval< T > rai ) final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); final boolean simplify = false; // needed to properly read label values. - final List< Spot > spots = MaskUtils.fromLabelingWithROI( + final Map< Integer, List< Spot > > spots = MaskUtils.fromLabelingWithROIMap( labeling, labeling, calibration, From aa9dc52df190650e1bfebf8132a9accf8971ef72 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Thu, 11 Jul 2024 15:11:21 +0200 Subject: [PATCH 38/65] In LabKit editor, offer to simplify the contour of modified spots. Also better message when closing the editor. --- .../trackmate/gui/editor/LabkitImporter.java | 11 +++++++-- .../trackmate/gui/editor/LabkitLauncher.java | 23 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 7e5c19bf9..7c2eb390e 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -35,6 +35,8 @@ public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > private final double dt; + private final boolean simplify; + /** * Creates a new re-importer. * @@ -44,15 +46,21 @@ public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > * the spatial calibration array: [dx, dy dz]. * @param dt * the frame interval. + * @param simplifyContours + * if true the contours of the spots imported and + * modified will be simplified. If false their + * contour will follow pixel edges. */ public LabkitImporter( final Model model, final double[] calibration, - final double dt ) + final double dt, + final boolean simplifyContours ) { this.model = model; this.calibration = calibration; this.dt = dt; + this.simplify = simplifyContours; } /** @@ -249,7 +257,6 @@ private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T indices.add( Integer.valueOf( i + 1 ) ); final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); - final boolean simplify = false; // needed to properly read label values. final Map< Integer, List< Spot > > spots = MaskUtils.fromLabelingWithROIMap( labeling, labeling, diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 05271c4c5..ba133c455 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -6,9 +6,11 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JRootPane; +import javax.swing.JSeparator; import javax.swing.SwingUtilities; import org.scijava.Context; @@ -63,6 +65,10 @@ public class LabkitLauncher< T extends IntegerType< T > & NativeType< T > > private final boolean is3D; + private final boolean isSingleTimePoint; + + private static boolean simplify = true; + public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, final EverythingDisablerAndReenabler disabler ) { this.trackmate = trackmate; @@ -71,6 +77,7 @@ public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, fina final ImagePlus imp = trackmate.getSettings().imp; this.calibration = TMUtils.getSpatialCalibration( imp ); this.is3D = !DetectionUtils.is2D( imp ); + this.isSingleTimePoint = imp.getNFrames() <= 1; } /** @@ -140,8 +147,12 @@ public void run() return; // Message the user. - final long nTimepoints = indexImg.dimension( 3 ); - final String msg = ( nTimepoints <= 1 ) + // Axes. + final int timeDim = ( isSingleTimePoint ) + ? -1 + : ( is3D ) ? 3 : 2; + final long nTimepoints = ( timeDim < 0 ) ? 0 : indexImg.dimension( timeDim ); + final String msg = ( isSingleTimePoint ) ? "Commit the changes made to the\n" + "segmentation in the image?" : ( currentTimePoint < 0 ) @@ -150,9 +161,12 @@ public void run() : "Commit the changes made to the\n" + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; final String title = "Commit edits to TrackMate"; + final JCheckBox chkbox = new JCheckBox( "Simplify the contours of modified spots" ); + chkbox.setSelected( simplify ); + final Object[] objs = new Object[] { msg, new JSeparator(), chkbox }; final int returnedValue = JOptionPane.showConfirmDialog( null, - msg, + objs, title, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, @@ -160,7 +174,8 @@ public void run() if ( returnedValue != JOptionPane.YES_OPTION ) return; - final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt ); + simplify = chkbox.isSelected(); + final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt, simplify ); if ( currentTimePoint < 0 && nTimepoints > 1 ) { // All time-points. From 73c85db2fd3f6bbc4109674b4841f29ee070e2e2 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Fri, 12 Jul 2024 16:38:27 +0200 Subject: [PATCH 39/65] Let the user edit a ROI in the image with the LabKit editor. If a ROI exists in the input image when launching the LabKit editor, only the image and the spots present in the ROI are sent to the editor. Spots touching the borders are not imported. --- .../trackmate/gui/editor/ImpBdvShowable.java | 22 ++- .../trackmate/gui/editor/LabkitImporter.java | 2 +- .../trackmate/gui/editor/LabkitLauncher.java | 138 +++++++++++++++--- 3 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java index 27ee12401..6d2e6fde6 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -19,10 +19,12 @@ import ij.CompositeImage; import ij.IJ; import ij.ImagePlus; +import ij.gui.Roi; import ij.process.LUT; import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; +import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.realtransform.AffineTransform3D; import net.imglib2.type.numeric.ARGBType; @@ -58,6 +60,7 @@ public class ImpBdvShowable implements BdvShowable */ public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImagePlus imp ) { + @SuppressWarnings( "unchecked" ) final ImgPlus< T > src = TMUtils.rawWraps( imp ); if ( src.dimensionIndex( Axes.CHANNEL ) < 0 ) Views.addDimension( src ); @@ -85,16 +88,33 @@ public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPl private final ImgPlus< ? extends NumericType< ? > > image; + private Interval interval; + ImpBdvShowable( final ImgPlus< ? extends NumericType< ? > > image, final ImagePlus imp ) { this.image = image; this.imp = imp; + final Roi roi = imp.getRoi(); + if ( roi == null ) + { + this.interval = image; + } + else + { + final long[] min = image.minAsLongArray(); + final long[] max = image.maxAsLongArray(); + min[ 0 ] = roi.getBounds().x; + min[ 1 ] = roi.getBounds().y; + max[ 0 ] = roi.getBounds().x + roi.getBounds().width; + max[ 1 ] = roi.getBounds().y + roi.getBounds().height; + this.interval = new FinalInterval( min, max ); + } } @Override public Interval interval() { - return image; + return interval; } @Override diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 7c2eb390e..2a2929fbe 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -259,7 +259,7 @@ private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); final Map< Integer, List< Spot > > spots = MaskUtils.fromLabelingWithROIMap( labeling, - labeling, + Views.zeroMin( labeling ), calibration, simplify, rai ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index ba133c455..08936f544 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -19,6 +19,7 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.features.FeatureUtils; @@ -29,12 +30,15 @@ import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.FeatureColorGenerator; import ij.ImagePlus; +import ij.gui.Roi; import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; +import net.imglib2.FinalInterval; import net.imglib2.RandomAccessibleInterval; import net.imglib2.img.Img; import net.imglib2.img.ImgFactory; +import net.imglib2.img.ImgView; import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.display.imagej.ImgPlusViews; import net.imglib2.loops.LoopBuilder; @@ -42,6 +46,7 @@ import net.imglib2.type.numeric.ARGBType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.util.Intervals; import net.imglib2.util.Util; import net.imglib2.view.Views; import sc.fiji.labkit.ui.LabkitFrame; @@ -147,11 +152,6 @@ public void run() return; // Message the user. - // Axes. - final int timeDim = ( isSingleTimePoint ) - ? -1 - : ( is3D ) ? 3 : 2; - final long nTimepoints = ( timeDim < 0 ) ? 0 : indexImg.dimension( timeDim ); final String msg = ( isSingleTimePoint ) ? "Commit the changes made to the\n" + "segmentation in the image?" @@ -176,6 +176,15 @@ public void run() simplify = chkbox.isSelected(); final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt, simplify ); + + // Possibly determine the number of time-points to parse. + final int timeDim = ( isSingleTimePoint ) + ? -1 + : ( is3D ) ? 3 : 2; + final long nTimepoints = ( timeDim < 0 ) + ? 0 + : indexImg.numDimensions() > timeDim ? indexImg.dimension( timeDim ) : 0; + if ( currentTimePoint < 0 && nTimepoints > 1 ) { // All time-points. @@ -183,10 +192,10 @@ public void run() log.setStatus( "Re-importing from Labkit..." ); for ( int t = 0; t < nTimepoints; t++ ) { - final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, 3, t ); - final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, 3, t ); + final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); + final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); - log.setProgress( ++t / ( double ) nTimepoints ); + log.setProgress( t / ( double ) nTimepoints ); } log.setStatus( "" ); log.setProgress( 0. ); @@ -239,6 +248,25 @@ private Img< T > copy( final RandomAccessibleInterval< T > in ) private final DatasetInputImage makeInput( final ImagePlus imp, final boolean singleTimePoint ) { final ImgPlus src = TMUtils.rawWraps( imp ); + // Crop if we have a ROI. + final Roi roi = imp.getRoi(); + final RandomAccessibleInterval crop; + if ( roi != null ) + { + final long[] min = src.minAsLongArray(); + final long[] max = src.maxAsLongArray(); + min[ 0 ] = roi.getBounds().x; + min[ 1 ] = roi.getBounds().y; + max[ 0 ] = roi.getBounds().x + roi.getBounds().width - 1; + max[ 1 ] = roi.getBounds().y + roi.getBounds().height - 1; + crop = Views.interval( src, min, max ); + } + else + { + crop = src; + } + final ImgPlus srcCropped = new ImgPlus<>( ImgView.wrap( crop ), src ); + // Possibly reslice for current time-point. final ImpBdvShowable showable; final ImgPlus inputImg; @@ -246,14 +274,14 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si if ( singleTimePoint && timeAxis >= 0 ) { this.currentTimePoint = imp.getFrame() - 1; - inputImg = ImgPlusViews.hyperSlice( src, timeAxis, currentTimePoint ); + inputImg = ImgPlusViews.hyperSlice( srcCropped, timeAxis, currentTimePoint ); showable = ImpBdvShowable.fromImp( inputImg, imp ); } else { this.currentTimePoint = -1; - showable = ImpBdvShowable.fromImp( imp ); - inputImg = src; + inputImg = srcCropped; + showable = ImpBdvShowable.fromImp( inputImg, imp ); } return new DatasetInputImage( inputImg, showable ); } @@ -297,8 +325,29 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, if ( !singleTimePoint ) dims[ dim++ ] = imp.getNFrames(); + // Possibly crop. + final Roi roi = imp.getRoi(); + final long[] origin; + if ( roi != null ) + { + dims[ 0 ] = roi.getBounds().width + 1; + dims[ 1 ] = roi.getBounds().height + 1; + origin = new long[ dims.length ]; + origin[ 0 ] = roi.getBounds().x; + origin[ 1 ] = roi.getBounds().y; + } + else + { + origin = null; + } + // Raw image. - final Img< UnsignedIntType > lblImg = ArrayImgs.unsignedInts( dims ); + Img< UnsignedIntType > lblImg = ArrayImgs.unsignedInts( dims ); + if ( origin != null ) + { + final RandomAccessibleInterval< UnsignedIntType > translated = Views.translate( lblImg, origin ); + lblImg = ImgView.wrap( translated ); + } // Calibration. final double[] c = TMUtils.getSpatialCalibration( imp ); @@ -318,7 +367,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, final Map< Integer, Spot > spotIDs = new HashMap<>(); if ( singleTimePoint ) { - processFrame( lblImgPlus, spots, currentTimePoint, spotIDs ); + processFrame( lblImgPlus, spots, currentTimePoint, spotIDs, origin ); } else { @@ -326,7 +375,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, for ( int t = 0; t < imp.getNFrames(); t++ ) { final ImgPlus< UnsignedIntType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); - processFrame( lblImgPlusThisFrame, spots, t, spotIDs ); + processFrame( lblImgPlusThisFrame, spots, t, spotIDs, origin ); } } final Labeling labeling = Labeling.fromImg( lblImgPlus ); @@ -350,16 +399,67 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, return labeling; } - private static final void processFrame( final ImgPlus< UnsignedIntType > lblImgPlus, final SpotCollection spots, final int t, final Map< Integer, Spot > spotIDs ) + private final void processFrame( + final ImgPlus< UnsignedIntType > lblImgPlus, + final SpotCollection spots, + final int t, + final Map< Integer, Spot > spotIDs, + final long[] origin ) { // If we have a single timepoint, don't use -1 to retrieve spots. final int lt = t < 0 ? 0 : t; final Iterable< Spot > spotsThisFrame = spots.iterable( lt, true ); - for ( final Spot spot : spotsThisFrame ) + if ( null == origin ) + { + for ( final Spot spot : spotsThisFrame ) + { + final int index = spot.ID() + 1; + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spotIDs.put( index, spot ); + } + } + else + { + final long[] min = new long[ 2 ]; + final long[] max = new long[ 2 ]; + final FinalInterval spotBB = FinalInterval.wrap( min, max ); + final FinalInterval imgBB = Intervals.createMinSize( origin[ 0 ], origin[ 1 ], lblImgPlus.dimension( 0 ), lblImgPlus.dimension( 1 ) ); + for ( final Spot spot : spotsThisFrame ) + { + boundingBox( spot, min, max ); + // Inside? We skip if we touch the border. + final boolean isInside = Intervals.contains( imgBB, spotBB ); + if ( !isInside ) + continue; + + final int index = spot.ID() + 1; + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spotIDs.put( index, spot ); + } + } + } + + private void boundingBox( final Spot spot, final long[] min, final long[] max ) + { + final SpotRoi roi = spot.getRoi(); + if ( roi == null ) + { + final double cx = spot.getDoublePosition( 0 ); + final double cy = spot.getDoublePosition( 1 ); + final double r = spot.getFeature( Spot.RADIUS ).doubleValue(); + min[ 0 ] = ( long ) Math.floor( ( cx - r ) / calibration[ 0 ] ); + min[ 1 ] = ( long ) Math.floor( ( cy - r ) / calibration[ 1 ] ); + max[ 0 ] = ( long ) Math.ceil( ( cx + r ) / calibration[ 0 ] ); + max[ 1 ] = ( long ) Math.ceil( ( cy + r ) / calibration[ 1 ] ); + } + else { - final int index = spot.ID() + 1; - SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); - spotIDs.put( index, spot ); + final double[] x = roi.toPolygonX( calibration[ 0 ], 0, spot.getDoublePosition( 0 ), 1. ); + final double[] y = roi.toPolygonY( calibration[ 1 ], 0, spot.getDoublePosition( 1 ), 1. ); + min[ 0 ] = ( long ) Math.floor( Util.min( x ) ); + min[ 1 ] = ( long ) Math.floor( Util.min( y ) ); + max[ 0 ] = ( long ) Math.ceil( Util.max( x ) ); + max[ 1 ] = ( long ) Math.ceil( Util.max( y ) ); } } From 186520e92611b0b783702568c1851ad686292f1b Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Wed, 17 Jul 2024 17:49:29 +0200 Subject: [PATCH 40/65] Fix panning not working when the TrackMate tool is selected. Normally pressing space and dragging should move the view, as for normal ImageJ tools. The removed line was preventing it. --- .../plugin/trackmate/visualization/hyperstack/SpotEditTool.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java index 1fc20c149..d091862d2 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java @@ -355,7 +355,6 @@ public void keyPressed( final KeyEvent e ) case KeyEvent.VK_SPACE: { actions.startMoveSpot(); - e.consume(); break; } From 28666c3379309bfae9d50c4b1420208e0b284401 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Thu, 18 Jul 2024 16:01:56 +0200 Subject: [PATCH 41/65] In LabKit editor, import spots at the border of the image. --- .../plugin/trackmate/gui/editor/LabkitLauncher.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 08936f544..0cbb26d78 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -257,8 +257,10 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si final long[] max = src.maxAsLongArray(); min[ 0 ] = roi.getBounds().x; min[ 1 ] = roi.getBounds().y; - max[ 0 ] = roi.getBounds().x + roi.getBounds().width - 1; - max[ 1 ] = roi.getBounds().y + roi.getBounds().height - 1; + max[ 0 ] = roi.getBounds().x + roi.getBounds().width; + max[ 1 ] = roi.getBounds().y + roi.getBounds().height; +// max[ 0 ] = roi.getBounds().x + roi.getBounds().width - 1; +// max[ 1 ] = roi.getBounds().y + roi.getBounds().height - 1; crop = Views.interval( src, min, max ); } else @@ -461,6 +463,12 @@ private void boundingBox( final Spot spot, final long[] min, final long[] max ) max[ 0 ] = ( long ) Math.ceil( Util.max( x ) ); max[ 1 ] = ( long ) Math.ceil( Util.max( y ) ); } + + min[ 0 ] = Math.max( 0, min[ 0 ] ); + min[ 1 ] = Math.max( 0, min[ 1 ] ); + final ImagePlus imp = trackmate.getSettings().imp; + max[ 0 ] = Math.min( imp.getWidth(), max[ 0 ] ); + max[ 1 ] = Math.min( imp.getHeight(), max[ 1 ] ); } public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate, final DisplaySettings ds ) From faa87b90ccb57b5883a63c40aafcaf74820a6a14 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Thu, 18 Jul 2024 16:20:11 +0200 Subject: [PATCH 42/65] Fix bug in LabKit editor: avoid spot ID and label conflicts. When editing a sub-portion of the image, the spots outside the ROI are not imported in the editor. It is possible that the user creates a new label in LabKit that will have the same id that an existing spot outside the ROI. Before this fix, the spots in that case were replaced by the new ones, creating a mess. The fix consists in reminding what spots are imported in the LabKit editor, then compariing to this list the new ids when reimporting from the editor. --- .../trackmate/gui/editor/LabkitImporter.java | 13 ++++----- .../trackmate/gui/editor/LabkitLauncher.java | 28 +++++++++++++------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 2a2929fbe..0cfe53c5a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -1,7 +1,6 @@ package fiji.plugin.trackmate.gui.editor; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -13,7 +12,6 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.detection.MaskUtils; import net.imglib2.RandomAccessibleInterval; import net.imglib2.loops.LoopBuilder; @@ -84,11 +82,15 @@ public LabkitImporter( * @param currentTimePoint * the time-point in the TrackMate model that corresponds to the * index image. + * @param the + * map of spots (vs their ID) that were written in the previous + * index image. */ public void reimport( final RandomAccessibleInterval< T > novelIndexImg, final RandomAccessibleInterval< T > previousIndexImg, - final int currentTimePoint ) + final int currentTimePoint, + final Map< Integer, Spot > previousSpotIDs ) { // Collect ids of spots that have been modified. id = index - 1 final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); @@ -99,11 +101,6 @@ public void reimport( model.beginUpdate(); try { - // Map of previous spots against their ID: - final SpotCollection spots = model.getSpots(); - final Map< Integer, Spot > previousSpotIDs = new HashMap<>(); - spots.iterable( currentTimePoint, true ).forEach( s -> previousSpotIDs.put( Integer.valueOf( s.ID() ), s ) ); - /* * Get all the spots present in the new image, as a map against the * label in the novel index image. This label value corresponds to diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 0cbb26d78..b01b4912f 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -47,7 +47,9 @@ import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.integer.UnsignedIntType; import net.imglib2.util.Intervals; +import net.imglib2.util.Pair; import net.imglib2.util.Util; +import net.imglib2.util.ValuePair; import net.imglib2.view.Views; import sc.fiji.labkit.ui.LabkitFrame; import sc.fiji.labkit.ui.inputimage.DatasetInputImage; @@ -97,7 +99,9 @@ protected void launch( final boolean singleTimePoint ) { final ImagePlus imp = trackmate.getSettings().imp; final DatasetInputImage input = makeInput( imp, singleTimePoint ); - final Labeling labeling = makeLabeling( imp, trackmate.getModel().getSpots(), singleTimePoint ); + final Pair< Labeling, Map< Integer, Spot > > pair = makeLabeling( imp, trackmate.getModel().getSpots(), singleTimePoint ); + final Labeling labeling = pair.getA(); + final Map< Integer, Spot > spotIDs = pair.getB(); // Make a labeling model from it. final Context context = TMUtils.getContext(); @@ -119,11 +123,15 @@ protected void launch( final boolean singleTimePoint ) labkit.onCloseListeners().addListener( () -> { @SuppressWarnings( "unchecked" ) final RandomAccessibleInterval< T > indexImg = ( RandomAccessibleInterval< T > ) model.imageLabelingModel().labeling().get().getIndexImg(); - reimport( indexImg, previousIndexImg, dt ); + reimport( indexImg, previousIndexImg, spotIDs, dt ); } ); } - private void reimport( final RandomAccessibleInterval< T > indexImg, final RandomAccessibleInterval< T > previousIndexImg, final double dt ) + private void reimport( + final RandomAccessibleInterval< T > indexImg, + final RandomAccessibleInterval< T > previousIndexImg, + final Map< Integer, Spot > spotIDs, + final double dt ) { new Thread( "TrackMate-LabKit-Importer-thread" ) { @@ -194,7 +202,7 @@ public void run() { final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); - reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t, spotIDs ); log.setProgress( t / ( double ) nTimepoints ); } log.setStatus( "" ); @@ -204,7 +212,7 @@ public void run() { // Only one. final int localT = Math.max( 0, currentTimePoint ); - reimporter.reimport( indexImg, previousIndexImg, localT ); + reimporter.reimport( indexImg, previousIndexImg, localT, spotIDs ); } } catch ( final Exception e ) @@ -291,7 +299,8 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si /** * Prepare the label image for annotation. The labeling is created and each * of its labels receive the name and the color from the spot it is created - * from. + * from. Only the spots fully included in the bounding box of the ROI of the + * source image are written in the labeling. * * @param imp * the source image plus. @@ -299,9 +308,10 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si * the spot collection. * @param singleTimePoint * if true we only annotate one time-point. - * @return a new {@link Labeling}. + * @return the pair of: A. a new {@link Labeling}, B. the map of spots that + * were written in the labeling. */ - private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) + private Pair< Labeling, Map< Integer, Spot > > makeLabeling( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) { // Axes. final AxisType[] axes = ( is3D ) @@ -398,7 +408,7 @@ private Labeling makeLabeling( final ImagePlus imp, final SpotCollection spots, label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); } - return labeling; + return new ValuePair<>( labeling, spotIDs ); } private final void processFrame( From 8ccd3ee8b3d340025880d2e4faf7218a595e1f57 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Fri, 19 Jul 2024 09:41:53 +0200 Subject: [PATCH 43/65] Custom LabKit frame and segmentation component. Simplified, removing the menu, the segmentation features and the segmenter features. Somply done by copy-pasting Matthias classes and removing the lines with the features to prune. --- .../trackmate/gui/editor/LabkitLauncher.java | 3 +- .../TrackMateLabKitSegmentationComponent.java | 169 ++++++++++++++++++ .../gui/editor/TrackMateLabkitFrame.java | 148 +++++++++++++++ 3 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java create mode 100644 src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index b01b4912f..73e5d3570 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -51,7 +51,6 @@ import net.imglib2.util.Util; import net.imglib2.util.ValuePair; import net.imglib2.view.Views; -import sc.fiji.labkit.ui.LabkitFrame; import sc.fiji.labkit.ui.inputimage.DatasetInputImage; import sc.fiji.labkit.ui.labeling.Label; import sc.fiji.labkit.ui.labeling.Labeling; @@ -116,7 +115,7 @@ protected void launch( final boolean singleTimePoint ) String title = "Editing TrackMate data for " + imp.getShortTitle(); if ( singleTimePoint ) title += "at frame " + ( currentTimePoint + 1 ); - final LabkitFrame labkit = LabkitFrame.show( model, title ); + final TrackMateLabkitFrame labkit = TrackMateLabkitFrame.show( model, title ); // Prepare re-importer. final double dt = imp.getCalibration().frameInterval; diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java new file mode 100644 index 000000000..10c7bc50e --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java @@ -0,0 +1,169 @@ +/*- + * #%L + * The Labkit image segmentation tool for Fiji. + * %% + * Copyright (C) 2017 - 2024 Matthias Arzt + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package fiji.plugin.trackmate.gui.editor; + +import java.awt.BorderLayout; + +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JPanel; +import javax.swing.JSplitPane; + +import net.miginfocom.swing.MigLayout; +import sc.fiji.labkit.ui.BasicLabelingComponent; +import sc.fiji.labkit.ui.DefaultExtensible; +import sc.fiji.labkit.ui.MenuBar; +import sc.fiji.labkit.ui.actions.AddLabelingIoAction; +import sc.fiji.labkit.ui.actions.LabelEditAction; +import sc.fiji.labkit.ui.actions.LabelingIoAction; +import sc.fiji.labkit.ui.actions.ResetViewAction; +import sc.fiji.labkit.ui.actions.ShowHelpAction; +import sc.fiji.labkit.ui.menu.MenuKey; +import sc.fiji.labkit.ui.models.ColoredLabelsModel; +import sc.fiji.labkit.ui.models.ImageLabelingModel; +import sc.fiji.labkit.ui.models.SegmentationItem; +import sc.fiji.labkit.ui.models.SegmentationModel; +import sc.fiji.labkit.ui.panel.ImageInfoPanel; +import sc.fiji.labkit.ui.panel.LabelPanel; +import sc.fiji.labkit.ui.segmentation.PredictionLayer; + +/** + * {@link SegmentationComponent} is the central Labkit UI component. Provides UI + * to display and modify a {@link SegmentationModel}. + *

+ * The UI consist of a Big Data Viewer panel, with brush tools and a side bar. + * The side bar lists panels and segmentation algorithms. + *

+ * A main menu that contains many actions for open and saving data, can be + * accessed by using {@link #getMenuBar()}. + */ +public class TrackMateLabKitSegmentationComponent extends JPanel implements AutoCloseable +{ + + private static final long serialVersionUID = 1L; + + private final boolean unmodifiableLabels; + + private final DefaultExtensible extensible; + + private final BasicLabelingComponent labelingComponent; + + private final SegmentationModel segmentationModel; + + public TrackMateLabKitSegmentationComponent( final JFrame dialogBoxOwner, + final SegmentationModel segmentationModel, final boolean unmodifiableLabels ) + { + this.extensible = new DefaultExtensible( segmentationModel.context(), dialogBoxOwner ); + this.unmodifiableLabels = unmodifiableLabels; + this.segmentationModel = segmentationModel; + final ImageLabelingModel imageLabelingModel = segmentationModel.imageLabelingModel(); + labelingComponent = new BasicLabelingComponent( dialogBoxOwner, imageLabelingModel ); + labelingComponent.addBdvLayer( PredictionLayer.createPredictionLayer( segmentationModel ) ); + initActions(); + setLayout( new BorderLayout() ); + add( initGui() ); + } + + private void initActions() + { +// final Holder< SegmentationItem > selectedSegmenter = segmentationModel.segmenterList().selectedSegmenter(); + final ImageLabelingModel labelingModel = segmentationModel.imageLabelingModel(); +// new TrainClassifier( extensible, segmentationModel.segmenterList() ); +// new ClassifierSettingsAction( extensible, segmentationModel.segmenterList() ); +// new ClassifierIoAction( extensible, segmentationModel.segmenterList() ); + new LabelingIoAction( extensible, labelingModel ); + new AddLabelingIoAction( extensible, labelingModel.labeling() ); +// new SegmentationExportAction( extensible, labelingModel ); + new ResetViewAction( extensible, labelingModel ); +// new BatchSegmentAction( extensible, selectedSegmenter ); +// new SegmentationAsLabelAction( extensible, segmentationModel ); +// new BitmapImportExportAction( extensible, labelingModel ); + new LabelEditAction( extensible, unmodifiableLabels, new ColoredLabelsModel( labelingModel ) ); +// MeasureConnectedComponents.addAction( extensible, labelingModel ); + new ShowHelpAction( extensible ); + labelingComponent.addShortcuts( extensible.getShortCuts() ); + } + + private JPanel initLeftPanel() + { + final JPanel panel = new JPanel(); + panel.setLayout( new MigLayout( "", "[grow]", "[][grow]" ) ); + panel.add( ImageInfoPanel.newFramedImageInfoPanel( segmentationModel.imageLabelingModel(), labelingComponent ), "grow, wrap" ); + panel.add( LabelPanel.newFramedLabelPanel( segmentationModel.imageLabelingModel(), extensible, unmodifiableLabels ), "grow, wrap, height 0:5000" ); +// panel.add( SegmenterPanel.newFramedSegmeterPanel( segmentationModel.segmenterList(), extensible ), "grow, height 0:50" ); + panel.invalidate(); + panel.repaint(); + return panel; + } + + private JSplitPane initGui() + { + final JSplitPane panel = new JSplitPane(); + panel.setOneTouchExpandable( true ); + panel.setLeftComponent( initLeftPanel() ); + panel.setRightComponent( labelingComponent ); + panel.setBorder( BorderFactory.createEmptyBorder() ); + return panel; + } + + @Deprecated + public JComponent getComponent() + { + return this; + } + + public JMenu createMenu( final MenuKey< Void > key ) + { + if ( key == MenuBar.SEGMENTER_MENU ) + return extensible.createMenu( + SegmentationItem.SEGMENTER_MENU, segmentationModel + .segmenterList().selectedSegmenter()::get ); + return extensible.createMenu( key, () -> null ); + } + + @Override + public void close() + { + labelingComponent.close(); + } + + public JMenuBar getMenuBar() + { + return new MenuBar( this::createMenu ); + } + + public void autoContrast() + { + labelingComponent.autoContrast(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java new file mode 100644 index 000000000..5d3014ba1 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java @@ -0,0 +1,148 @@ +/*- + * #%L + * The Labkit image segmentation tool for Fiji. + * %% + * Copyright (C) 2017 - 2024 Matthias Arzt + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package fiji.plugin.trackmate.gui.editor; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; + +import javax.swing.JFrame; + +import org.scijava.Context; + +import fiji.plugin.trackmate.gui.Icons; +import io.scif.services.DatasetIOService; +import net.imagej.Dataset; +import sc.fiji.labkit.pixel_classification.utils.SingletonContext; +import sc.fiji.labkit.ui.InitialLabeling; +import sc.fiji.labkit.ui.inputimage.DatasetInputImage; +import sc.fiji.labkit.ui.inputimage.InputImage; +import sc.fiji.labkit.ui.models.DefaultSegmentationModel; +import sc.fiji.labkit.ui.models.SegmentationModel; +import sc.fiji.labkit.ui.utils.Notifier; + +/** + * The main Labkit window. (This window allows to segment a single image. It has + * to be distinguished from the LabkitProjectFrame, which allows to operation on + * multiple images.) The window only contains a {@link SegmentationComponent} + * and shows the associated main menu. + * + * @author Matthias Arzt + */ +public class TrackMateLabkitFrame +{ + + private final JFrame frame = initFrame(); + + private final Notifier onCloseListeners = new Notifier(); + + public static TrackMateLabkitFrame showForFile( Context context, + final String filename ) + { + if ( context == null ) + context = SingletonContext.getInstance(); + final Dataset dataset = openDataset( context, filename ); + return showForImage( context, new DatasetInputImage( dataset ) ); + } + + private static Dataset openDataset( final Context context, final String filename ) + { + try + { + return context.service( DatasetIOService.class ).open( filename ); + } + catch ( final IOException e ) + { + throw new RuntimeException( e ); + } + } + + public static TrackMateLabkitFrame showForImage( Context context, final InputImage inputImage ) + { + if ( context == null ) + context = SingletonContext.getInstance(); + final SegmentationModel model = new DefaultSegmentationModel( context, inputImage ); + model.imageLabelingModel().labeling().set( InitialLabeling.initialLabeling( context, inputImage ) ); + return show( model, inputImage.imageForSegmentation().getName() ); + } + + public static TrackMateLabkitFrame show( final SegmentationModel model, final String title ) + { + return new TrackMateLabkitFrame( model, title ); + } + + private TrackMateLabkitFrame( final SegmentationModel model, final String title ) + { + @SuppressWarnings( "unused" ) + final TrackMateLabKitSegmentationComponent segmentationComponent = initSegmentationComponent( model ); + setTitle( title ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); +// frame.setJMenuBar( new MenuBar( segmentationComponent::createMenu ) ); + frame.setVisible( true ); + } + + private TrackMateLabKitSegmentationComponent initSegmentationComponent( final SegmentationModel segmentationModel ) + { + final TrackMateLabKitSegmentationComponent segmentationComponent = new TrackMateLabKitSegmentationComponent( frame, segmentationModel, false ); + frame.add( segmentationComponent ); + frame.addWindowListener( new WindowAdapter() + { + + @Override + public void windowClosed( final WindowEvent e ) + { + segmentationComponent.close(); + onCloseListeners.notifyListeners(); + } + } ); + return segmentationComponent; + } + + private JFrame initFrame() + { + final JFrame frame = new JFrame(); + frame.setBounds( 50, 50, 1200, 900 ); + frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + return frame; + } + + private void setTitle( final String name ) + { + if ( name == null || name.isEmpty() ) + frame.setTitle( "Labkit" ); + else + frame.setTitle( "Labkit - " + name ); + } + + public Notifier onCloseListeners() + { + return onCloseListeners; + } +} From 2f9583e67e339b2fc4f4eb74a68b11bfa95ffbb9 Mon Sep 17 00:00:00 2001 From: Jean-Yves TINEVEZ Date: Fri, 19 Jul 2024 10:00:21 +0200 Subject: [PATCH 44/65] Fix another bug with the LabKit importer. This time it was due to confusion between a map from spot ID to spots, and a map from label value to spot. I fixed it by rewriting everything in terms of label values in the labeling. So we do not need to keep track of the spot ID anymore. The labels could be anything. They are still equal to spot ID + 1, because it is convenient to debug. --- .../trackmate/gui/editor/LabkitImporter.java | 27 ++++++++------- .../trackmate/gui/editor/LabkitLauncher.java | 33 ++++++++++--------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 0cfe53c5a..45153f6e6 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -83,18 +83,18 @@ public LabkitImporter( * the time-point in the TrackMate model that corresponds to the * index image. * @param the - * map of spots (vs their ID) that were written in the previous - * index image. + * map of spots (vs the label value in the previous labeling) + * that were written in the previous index image. */ public void reimport( final RandomAccessibleInterval< T > novelIndexImg, final RandomAccessibleInterval< T > previousIndexImg, final int currentTimePoint, - final Map< Integer, Spot > previousSpotIDs ) + final Map< Integer, Spot > previousSpotLabels ) { - // Collect ids of spots that have been modified. id = index - 1 - final Set< Integer > modifiedIDs = getModifiedIDs( novelIndexImg, previousIndexImg ); - final int nModified = modifiedIDs.size(); + // Collect labels corresponding to spots that have been modified. + final Set< Integer > modifiedLabels = getModifiedLabels( novelIndexImg, previousIndexImg ); + final int nModified = modifiedLabels.size(); if ( nModified == 0 ) return; @@ -103,16 +103,15 @@ public void reimport( { /* * Get all the spots present in the new image, as a map against the - * label in the novel index image. This label value corresponds to - * the ids of the spots in the previous index image (label = id+1). + * label in the novel index image. */ final Map< Integer, List< Spot > > novelSpots = getSpots( novelIndexImg ); // Update model for those spots. - for ( final int id : modifiedIDs ) + for ( final int labelValue : modifiedLabels ) { - final Spot previousSpot = previousSpotIDs.get( Integer.valueOf( id ) ); - final List< Spot > novelSpotList = novelSpots.get( Integer.valueOf( id ) + 1 ); + final Spot previousSpot = previousSpotLabels.get( labelValue ); + final List< Spot > novelSpotList = novelSpots.get( labelValue ); if ( previousSpot == null ) { /* @@ -218,7 +217,7 @@ private void addNewSpot( final Iterable< Spot > novelSpotList, final int current } } - private final Set< Integer > getModifiedIDs( + private final Set< Integer > getModifiedLabels( final RandomAccessibleInterval< T > novelIndexImg, final RandomAccessibleInterval< T > previousIndexImg ) { @@ -232,8 +231,8 @@ private final Set< Integer > getModifiedIDs( return; if ( ci != pi ) { - modifiedIDs.add( Integer.valueOf( pi - 1 ) ); - modifiedIDs.add( Integer.valueOf( ci - 1 ) ); + modifiedIDs.add( Integer.valueOf( pi ) ); + modifiedIDs.add( Integer.valueOf( ci ) ); } } ); modifiedIDs.remove( Integer.valueOf( -1 ) ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 73e5d3570..a4025239c 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -100,7 +100,7 @@ protected void launch( final boolean singleTimePoint ) final DatasetInputImage input = makeInput( imp, singleTimePoint ); final Pair< Labeling, Map< Integer, Spot > > pair = makeLabeling( imp, trackmate.getModel().getSpots(), singleTimePoint ); final Labeling labeling = pair.getA(); - final Map< Integer, Spot > spotIDs = pair.getB(); + final Map< Integer, Spot > spotLabels = pair.getB(); // Make a labeling model from it. final Context context = TMUtils.getContext(); @@ -122,14 +122,14 @@ protected void launch( final boolean singleTimePoint ) labkit.onCloseListeners().addListener( () -> { @SuppressWarnings( "unchecked" ) final RandomAccessibleInterval< T > indexImg = ( RandomAccessibleInterval< T > ) model.imageLabelingModel().labeling().get().getIndexImg(); - reimport( indexImg, previousIndexImg, spotIDs, dt ); + reimport( indexImg, previousIndexImg, spotLabels, dt ); } ); } private void reimport( final RandomAccessibleInterval< T > indexImg, final RandomAccessibleInterval< T > previousIndexImg, - final Map< Integer, Spot > spotIDs, + final Map< Integer, Spot > spotLabels, final double dt ) { new Thread( "TrackMate-LabKit-Importer-thread" ) @@ -201,7 +201,7 @@ public void run() { final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); - reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t, spotIDs ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t, spotLabels ); log.setProgress( t / ( double ) nTimepoints ); } log.setStatus( "" ); @@ -211,7 +211,7 @@ public void run() { // Only one. final int localT = Math.max( 0, currentTimePoint ); - reimporter.reimport( indexImg, previousIndexImg, localT, spotIDs ); + reimporter.reimport( indexImg, previousIndexImg, localT, spotLabels ); } } catch ( final Exception e ) @@ -308,7 +308,8 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si * @param singleTimePoint * if true we only annotate one time-point. * @return the pair of: A. a new {@link Labeling}, B. the map of spots that - * were written in the labeling. + * were written in the labeling. The keys are the label value in the + * labeling. */ private Pair< Labeling, Map< Integer, Spot > > makeLabeling( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) { @@ -375,10 +376,10 @@ private Pair< Labeling, Map< Integer, Spot > > makeLabeling( final ImagePlus imp final ImgPlus< UnsignedIntType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); // Write spots in it with index = id + 1 and build a map index -> spot. - final Map< Integer, Spot > spotIDs = new HashMap<>(); + final Map< Integer, Spot > spotLabels = new HashMap<>(); if ( singleTimePoint ) { - processFrame( lblImgPlus, spots, currentTimePoint, spotIDs, origin ); + processFrame( lblImgPlus, spots, currentTimePoint, spotLabels, origin ); } else { @@ -386,7 +387,7 @@ private Pair< Labeling, Map< Integer, Spot > > makeLabeling( final ImagePlus imp for ( int t = 0; t < imp.getNFrames(); t++ ) { final ImgPlus< UnsignedIntType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); - processFrame( lblImgPlusThisFrame, spots, t, spotIDs, origin ); + processFrame( lblImgPlusThisFrame, spots, t, spotLabels, origin ); } } final Labeling labeling = Labeling.fromImg( lblImgPlus ); @@ -396,25 +397,25 @@ private Pair< Labeling, Map< Integer, Spot > > makeLabeling( final ImagePlus imp for ( final Label label : labeling.getLabels() ) { final String name = label.name(); - final int index = Integer.parseInt( name ); - final Spot spot = spotIDs.get( index ); + final int labelVal = Integer.parseInt( name ); + final Spot spot = spotLabels.get( labelVal ); if ( spot == null ) { - System.out.println( "Spot is null for index " + index + "!!" ); // DEBUG + System.out.println( "Spot is null for label " + labelVal + "!!" ); // DEBUG continue; } label.setName( spot.getName() ); label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); } - return new ValuePair<>( labeling, spotIDs ); + return new ValuePair<>( labeling, spotLabels ); } private final void processFrame( final ImgPlus< UnsignedIntType > lblImgPlus, final SpotCollection spots, final int t, - final Map< Integer, Spot > spotIDs, + final Map< Integer, Spot > spotLabels, final long[] origin ) { // If we have a single timepoint, don't use -1 to retrieve spots. @@ -426,7 +427,7 @@ private final void processFrame( { final int index = spot.ID() + 1; SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); - spotIDs.put( index, spot ); + spotLabels.put( index, spot ); } } else @@ -445,7 +446,7 @@ private final void processFrame( final int index = spot.ID() + 1; SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); - spotIDs.put( index, spot ); + spotLabels.put( index, spot ); } } } From 9d3c58e1fb236f57f6ff1680bbafd8aa00a81be7 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Mon, 22 Jul 2024 16:38:35 +0200 Subject: [PATCH 45/65] Fixed one more bug with the labkit importer. When editing the whole movie, if the label of a new spot was using the label of an existing spot in another time-point, the existing one was removed. Because the new spot was identified as a modification of the existing one. The solution is to pass to the importer only the list of existing spots in the current time-frame. --- .../trackmate/gui/editor/LabkitImporter.java | 28 +++++++++++++++++++ .../trackmate/gui/editor/LabkitLauncher.java | 11 +++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 45153f6e6..4a205973d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -13,6 +13,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.detection.MaskUtils; +import ij.IJ; import net.imglib2.RandomAccessibleInterval; import net.imglib2.loops.LoopBuilder; import net.imglib2.roi.labeling.ImgLabeling; @@ -27,6 +28,8 @@ public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > { + private static final boolean DEBUG = false; + private final Model model; private final double[] calibration; @@ -127,6 +130,7 @@ else if ( novelSpotList == null || novelSpotList.isEmpty() ) * One I had in the previous spot list, but that has * disappeared. Remove it. */ + IJ.log( " - Removed spot " + str( previousSpot ) ); model.removeSpot( previousSpot ); } else @@ -191,6 +195,9 @@ else if ( target == previousSpot ) else throw new IllegalArgumentException( "The edge of a spot does not have the spot as source or target?!?" ); } + if ( DEBUG ) + IJ.log( " - Modified spot " + str( previousSpot ) + " -> " + str( mainNovelSpot ) ); + model.removeSpot( previousSpot ); // Deal with the other ones. @@ -204,6 +211,9 @@ else if ( target == previousSpot ) s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); s.putFeature( Spot.QUALITY, -1. ); model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); + + if ( DEBUG ) + IJ.log( " - Added spot " + str( s ) ); } } @@ -214,6 +224,9 @@ private void addNewSpot( final Iterable< Spot > novelSpotList, final int current spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); spot.putFeature( Spot.QUALITY, -1. ); model.addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); + + if ( DEBUG ) + IJ.log( " - Added spot " + str( spot ) ); } } @@ -261,4 +274,19 @@ private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T rai ); return spots; } + + private static final String str( final Spot spot ) + { + return spot.ID() + " (" + + roundToN( spot.getDoublePosition( 0 ), 1 ) + ", " + + roundToN( spot.getDoublePosition( 1 ), 1 ) + ", " + + roundToN( spot.getDoublePosition( 2 ), 1 ) + ") " + + "@ t=" + spot.getFeature( Spot.FRAME ).intValue(); + } + + private static double roundToN( final double num, final int n ) + { + final double scale = Math.pow( 10, n ); + return Math.round( num * scale ) / scale; + } } diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index a4025239c..50d71fc90 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -199,9 +199,18 @@ public void run() log.setStatus( "Re-importing from Labkit..." ); for ( int t = 0; t < nTimepoints; t++ ) { + // The spots of this time-point: + final Map< Integer, Spot > spotLabelsThisFrame = new HashMap<>(); + for ( final Integer label : spotLabels.keySet() ) + { + final Spot spot = spotLabels.get( label ); + if ( spot.getFeature( Spot.FRAME ).intValue() == t ) + spotLabelsThisFrame.put( label, spot ); + } + final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); - reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t, spotLabels ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t, spotLabelsThisFrame ); log.setProgress( t / ( double ) nTimepoints ); } log.setStatus( "" ); From 5e9643d1587abb32f79ca761b12310b2124774d7 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Mon, 30 Sep 2024 15:37:19 +0200 Subject: [PATCH 46/65] Don't crash when displaying an empty model. This could happen after loading a TrackMate file with specific display settings but no tracks. --- .../trackmate/visualization/hyperstack/TrackOverlay.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java index 077456d15..ed6611395 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java @@ -211,7 +211,8 @@ public final synchronized void drawOverlay( final Graphics g ) final Set< DefaultWeightedEdge > track; synchronized ( model ) { - track = new HashSet<>( model.getTrackModel().trackEdges( trackID ) ); + final Set< DefaultWeightedEdge > edges = model.getTrackModel().trackEdges( trackID ); + track = ( edges == null ) ? new HashSet<>() : new HashSet<>( edges ); } for ( final DefaultWeightedEdge edge : track ) { From cff5746abc1e5a849bf0aa91b4af55ec9f32147b Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Tue, 1 Oct 2024 16:43:11 +0200 Subject: [PATCH 47/65] Compatibility with command lines args that ends in '=' Like the latest YOLO... --- .../trackmate/util/cli/CommandBuilder.java | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java index ebe01db0e..6aec3e860 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java @@ -91,12 +91,22 @@ public void visit( final Flag flag ) val = flag.getValue(); else val = flag.getDefaultValue(); - if ( val ) + + // Deal with flag that have a '=' vs switches. + final String a = flag.getArgument(); + final List< String > vals = translators.getOrDefault( flag, v -> Collections.singletonList( "" + v ) ).apply( val ); + if ( a.endsWith( "=" ) ) { - tokens.add( flag.getArgument() ); - tokens.addAll( translators.getOrDefault( flag, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + tokens.add( a + String.join( ",", vals ) ); + } + else + { + if ( val ) + { + tokens.add( a ); + tokens.addAll( vals ); + } } - } @Override @@ -122,8 +132,19 @@ public void visit( final IntArgument arg ) if ( arg.getMax() != Integer.MAX_VALUE && ( val > arg.getMax() ) ) throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() + "' is larger than the max: " + arg.getMax() ); - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + final String a = arg.getArgument(); + final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } @Override @@ -151,8 +172,19 @@ public void visit( final DoubleArgument arg ) throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() + "' is larger than the max: " + arg.getMax() ); - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + final String a = arg.getArgument(); + final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } private void visitString( final AbstractStringArgument< ? > arg ) @@ -171,8 +203,19 @@ private void visitString( final AbstractStringArgument< ? > arg ) ? arg.getDefaultValue() : arg.getValue(); - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + final String a = arg.getArgument(); + final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } @Override @@ -198,9 +241,20 @@ public void visit( final ChoiceArgument arg ) // Is not set? -> skip if ( !arg.isSet() ) return; - - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( arg.getValue() ) ); + + final String a = arg.getArgument(); + final List vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( arg.getValue() ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } public static List< String > build( final CLIConfigurator cli ) From a030b716dd4e7769bbe1f08462b4c8dd4be2d911 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Tue, 1 Oct 2024 21:04:43 +0200 Subject: [PATCH 48/65] A new CLI configurator for executables that live in a conda env. Mother class for CLI configurator that relies on an executable installed in a conda environment. This happens when the Python code we want to call does not have a Python module (that we could call with 'python -m') or is simply not a Python program. In that case: on Mac we resolve the env path, and append the executable name to build an absolute path to the executable. on Windows we simply activate the conda environment and call the executable assuming it is on the path. --- .../util/cli/CondaCLIConfigurator.java | 2 +- .../cli/CondaExecutableCLIConfigurator.java | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java index e8a20114d..6fe52431d 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java @@ -33,7 +33,7 @@ public abstract class CondaCLIConfigurator extends CLIConfigurator public static final String KEY_CONDA_ENV = "CONDA_ENV"; - private final CondaEnvironmentCommand condaEnv; + protected final CondaEnvironmentCommand condaEnv; public static class CondaEnvironmentCommand extends AbstractStringArgument< CondaEnvironmentCommand > { diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java new file mode 100644 index 000000000..167141573 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java @@ -0,0 +1,71 @@ +package fiji.plugin.trackmate.util.cli; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import ij.IJ; + +/** + * Mother class for CLI configurator that relies on an executable + * installed in a conda environment. This happens when the Python code we want + * to call does not have a Python module (that we could call with 'python -m') + * or is simply not a Python program. In that case: + *

    + *
  • on Mac we resolve the env path, and append the executable name to build + * an absolute path to the executable. + *
  • on Windows we simply activate the conda environment and call the + * executable assuming it is on the path. + *
+ */ +public abstract class CondaExecutableCLIConfigurator extends CondaCLIConfigurator +{ + + public CondaExecutableCLIConfigurator() + { + super(); + + // Add the translator to make a proper cmd line calling conda first. + setTranslator( condaEnv, s -> { + final List< String > cmd = new ArrayList<>(); + final String condaPath = CLIUtils.getCondaPath(); + // Conda and executable stuff. + final String envname = ( String ) s; + if ( IJ.isWindows() ) + { + cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); + cmd.addAll( Arrays.asList( condaPath, "activate", envname ) ); + cmd.add( "&" ); + // Add command name + final String executableCommand = getCommand(); + // Split by spaces + final String[] split = executableCommand.split( " " ); + cmd.addAll( Arrays.asList( split ) ); + return cmd; + + } + else + { + try + { + final String pythonPath = CLIUtils.getEnvMap().get( envname ); + final int i = pythonPath.lastIndexOf( "python" ); + final String binPath = pythonPath.substring( 0, i ); + final String executablePath = binPath + getCommand(); + final String[] split = executablePath.split( " " ); + cmd.addAll( Arrays.asList( split ) ); + return cmd; + } + catch ( final IOException e ) + { + System.err.println( "Could not find the conda executable or change the conda environment.\n" + + "Please configure the path to your conda executable in Edit > Options > Configure TrackMate Conda path..." ); + e.printStackTrace(); + } + } + return null; + } ); + } + +} From 6cd0cc329f6cd707e2b48e1f99019589cd4e40c2 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 2 Oct 2024 14:55:11 +0200 Subject: [PATCH 49/65] Smooth icon when resizing. --- src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java index 3b59b62d8..d2c38ee2d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java +++ b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java @@ -400,7 +400,7 @@ public static final ImageIcon scaleImage( final ImageIcon icon, final int w, fin nw = ( icon.getIconWidth() * nh ) / icon.getIconHeight(); } - return new ImageIcon( icon.getImage().getScaledInstance( nw, nh, Image.SCALE_DEFAULT ) ); + return new ImageIcon( icon.getImage().getScaledInstance( nw, nh, Image.SCALE_SMOOTH ) ); } public static URL getResource( final String name, final Class< ? > clazz ) From a33a1c378c6b827554972c1529145d400a2a2fdd Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 2 Oct 2024 14:56:39 +0200 Subject: [PATCH 50/65] In the CLI configurator, when a key is null, let it be null. A null key means that the argument should not appear in the settings map when the CLI will be deserialized to the map. This is useful e.g. for arguments of the CLI that must not be configured by the user, nor should not be serialized to disk. Such as the path where to save temp images. --- .../java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java index db9b4a9fa..771061383 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java @@ -181,7 +181,7 @@ public void select( final String key ) { for ( int i = 0; i < args.size(); i++ ) { - if ( args.get( i ).getKey().equals( key ) ) + if ( key.equals( args.get( i ).getKey() ) ) { this.selected = i; return; @@ -1040,7 +1040,7 @@ public String getHelp() public String getKey() { - return ( key == null ) ? getName() : key; + return key; } public abstract void accept( final ArgumentVisitor visitor ); From a23c3075dd34625fd62edbef16e9c1bd5d8fe870 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 4 Oct 2024 17:36:09 +0200 Subject: [PATCH 51/65] Don't set quality value for preview to - infinity. 0 is good enough as we require the quality to be a positive value. --- src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java index 3bf2102d9..43298c3e3 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java @@ -177,7 +177,8 @@ protected Pair< Model, Double > runPreviewDetection( if ( hasThreshold ) { threshold = ( ( Double ) detectorSettings.get( thresholdKey ) ).doubleValue(); - lSettings.detectorSettings.put( thresholdKey, Double.valueOf( Double.NEGATIVE_INFINITY ) ); +// lSettings.detectorSettings.put( thresholdKey, Double.valueOf( Double.NEGATIVE_INFINITY ) ); + lSettings.detectorSettings.put( thresholdKey, Double.valueOf( 0. ) ); } else { From b3f4d870335f0e719cc8e544e01c31d988b32139 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 4 Oct 2024 17:36:32 +0200 Subject: [PATCH 52/65] In the CLI GUI builder, avoid long paths deforming new panels. --- src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java index 41af6992c..eca1de965 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java @@ -393,6 +393,8 @@ private void addPathToLayout( final String help, final JLabel lbl, final JTextFi final BoxLayout bl = new BoxLayout( p, BoxLayout.LINE_AXIS ); p.setLayout( bl ); + tf.setColumns( 10 ); // Avoid long paths deforming new panels. + lbl.setText( lbl.getText() + " " ); lbl.setFont( Fonts.SMALL_FONT ); tf.setFont( Fonts.SMALL_FONT ); From 4802efa569fdbb428904ba13550bb9b09290c696 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 10:03:08 +0100 Subject: [PATCH 53/65] Update parent to pom-scijava 39.0.0 Then removes the specific imglib2 and labkit version specs. Also named this future version the true v8. The existing v8 branch, that focuses on 3D, shall be renamed v9. --- pom.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 97689a66c..dec20d441 100644 --- a/pom.xml +++ b/pom.xml @@ -5,13 +5,13 @@ org.scijava pom-scijava - 37.0.0 + 39.0.0 sc.fiji TrackMate - 7.13.3-SNAPSHOT + 8.0.0-SNAPSHOT TrackMate TrackMate plugin for Fiji. @@ -147,7 +147,6 @@ 2.5.2 0.11.1 - 6.1.0 @@ -159,7 +158,6 @@ sc.fiji labkit-ui - 0.3.12-SNAPSHOT From 6eb47f6956dc1c02034378fbe3f8f61fe547de58 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 10:04:41 +0100 Subject: [PATCH 54/65] Fix compile error with new version of imglib2. Focuses on the rewrite of KDtree. The left and right fields of KDTreeNode are not visible anymore. The implementation changed to be proxy-based, so the corresponding methods, used in this commit to fix the compile errors, are deprectaed. We therefore shall also full rewrite the KDtree NN tracker using the proxy approach. --- .../kdtree/NearestNeighborFlagSearchOnKDTree.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java index b5e639341..437fdb49b 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -78,8 +78,9 @@ protected void searchNode( final KDTreeNode< FlagNode< T > > current ) final boolean leftIsNearBranch = axisDiff < 0; // search the near branch - final KDTreeNode< FlagNode< T > > nearChild = leftIsNearBranch ? current.left : current.right; - final KDTreeNode< FlagNode< T > > awayChild = leftIsNearBranch ? current.right : current.left; + // TODO Rewrite to remove deprecated methods, proxy object based. + final KDTreeNode< FlagNode< T > > nearChild = leftIsNearBranch ? current.left() : current.right(); + final KDTreeNode< FlagNode< T > > awayChild = leftIsNearBranch ? current.right() : current.left(); if ( nearChild != null ) searchNode( nearChild ); From 349460f2857369a53153ecda4a5e8d8e45de72a1 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 10:11:31 +0100 Subject: [PATCH 55/65] Fix compile errors linked to new LabelRegion code. --- .../plugin/trackmate/detection/MaskUtils.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 5ead9f1e1..547693b77 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -38,6 +38,7 @@ import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; +import net.imglib2.Cursor; import net.imglib2.Interval; import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; @@ -53,10 +54,10 @@ import net.imglib2.img.ImgFactory; import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.roi.labeling.LabelRegion; -import net.imglib2.roi.labeling.LabelRegionCursor; import net.imglib2.roi.labeling.LabelRegions; import net.imglib2.type.BooleanType; import net.imglib2.type.logic.BitType; +import net.imglib2.type.logic.BoolType; import net.imglib2.type.numeric.IntegerType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.IntType; @@ -176,7 +177,7 @@ public static final long getThreshold( final Histogram1d< ? > hist ) /** * Creates a zero-min label image from a thresholded input image. - * + * * @param * the type of the input image. Must be real, scalar. * @param input @@ -228,7 +229,7 @@ public static final < T extends RealType< T > > ImgLabeling< Integer, IntType > * Creates spots from a grayscale image, thresholded to create a mask. A * spot is created for each connected-component of the mask, with a size * that matches the mask size. - * + * * @param * the type of the input image. Must be real, scalar. * @param input @@ -260,7 +261,7 @@ public static < T extends RealType< T > > List< Spot > fromThreshold( /** * Creates spots from a label image. - * + * * @param * the type that backs-up the labeling. * @param labeling @@ -284,7 +285,7 @@ public static < R extends IntegerType< R > > List< Spot > fromLabeling( while ( iterator.hasNext() ) { final LabelRegion< Integer > region = iterator.next(); - final LabelRegionCursor cursor = region.localizingCursor(); + final Cursor< BoolType > cursor = region.localizingCursor(); final int[] cursorPos = new int[ labeling.numDimensions() ]; final long[] sum = new long[ 3 ]; while ( cursor.hasNext() ) @@ -322,7 +323,7 @@ public static < R extends IntegerType< R > > List< Spot > fromLabeling( * spot is created for each connected-component of the mask, with a size * that matches the mask size. The quality of the spots is read from another * image, by taking the max pixel value of this image with the ROI. - * + * * @param * the type of the input image. Must be real, scalar. * @param input @@ -362,7 +363,7 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > while ( iterator.hasNext() ) { final LabelRegion< Integer > region = iterator.next(); - final LabelRegionCursor cursor = region.localizingCursor(); + final Cursor< BoolType > cursor = region.localizingCursor(); final int[] cursorPos = new int[ labeling.numDimensions() ]; final long[] sum = new long[ 3 ]; double quality = Double.NEGATIVE_INFINITY; @@ -410,7 +411,7 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > * connected-component of the mask, with a size that matches the mask size. * The quality of the spots is read from another image, by taking the max * pixel value of this image with the ROI. - * + * * @param * the type of the input image. Must be real, scalar. * @param @@ -453,7 +454,7 @@ public static final < T extends RealType< T >, S extends RealType< S > > List< S * Creates spots with ROIs from a 2D label image. The quality * value is read from a secondary image, by taking the max value in each * ROI. - * + * * @param * the type that backs-up the labeling. * @param @@ -483,7 +484,7 @@ public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot final List spots = new ArrayList<>(); for ( final List< Spot > s : map.values() ) spots.addAll( s ); - + return spots; } @@ -496,7 +497,7 @@ public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot * the label they correspond to in the label image. Because one spot * corresponds to one connected component in the label image, there might be * several spots for a label, hence the values of the map are list of spots. - * + * * @param * the type that backs-up the labeling. * @param @@ -558,7 +559,7 @@ public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integ { final List< Spot > spots = new ArrayList<>( polygonsMap.size() ); output.put( label, spots ); - + final List< Polygon > polygons = polygonsMap.get( label ); for ( final Polygon polygon : polygons ) { @@ -696,7 +697,7 @@ private static final void douglasPeucker( final List< double[] > list, final int * the number of points in a curve that is approximated by a series of * points. *

- * + * * @see Ramer–Douglas–Peucker * Algorithm (Wikipedia) @@ -737,7 +738,7 @@ public static final PolygonRoi simplify( final PolygonRoi roi, final double smoo /** * Start at 1. - * + * * @return a new iterator that goes like 1, 2, 3, ... */ public static final Iterator< Integer > labelGenerator() @@ -769,7 +770,7 @@ public boolean hasNext() * Warning: cannot deal with holes, they are simply ignored. *

* Copied and adapted from ImageJ1 code by Wayne Rasband. - * + * * @param * the type of the mask. * @param mask From 319ef4438c0116ad68094b4c487c06865c56e96e Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 10:19:20 +0100 Subject: [PATCH 56/65] Fix some deprecation warnings with new imglib2. - Now RAIs are iterable. - You can get its type with getType() method. --- .../trackmate/detection/DetectionUtils.java | 21 +++++++++---------- .../trackmate/detection/DogDetector.java | 11 ++++------ .../detection/LabelImageDetector.java | 10 ++++----- .../plugin/trackmate/detection/MaskUtils.java | 6 +++--- .../trackmate/gui/editor/LabkitImporter.java | 2 +- .../trackmate/gui/editor/LabkitLauncher.java | 2 +- 6 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java index b01746bf7..ebb380e6b 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -39,8 +39,8 @@ import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.util.MedianFilter2D; -import fiji.plugin.trackmate.util.Threads; import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.util.Threads; import ij.ImagePlus; import net.imagej.ImgPlus; import net.imagej.axis.Axes; @@ -69,7 +69,6 @@ import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.util.Intervals; -import net.imglib2.util.Util; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; @@ -82,7 +81,7 @@ public class DetectionUtils * This method returns immediately and execute the detection in a separate * thread. It executes the detection in one frame only and writes the * results in the specified model object. - * + * * @param model * the model to write detection results in. * @param settings @@ -167,7 +166,7 @@ public static final void preview( * Returns true if the specified image is 2D. It can have * multiple channels and multiple time-points; this method only looks at * whether several Z-slices can be found. - * + * * @param img * the image. * @return true if the image is 2D, regardless of time and @@ -189,7 +188,7 @@ public static final boolean is2D( final ImagePlus imp ) * radius specified using calibrated units. The specified calibration * is used to determine the dimensionality of the kernel and to map it on a * pixel grid. - * + * * @param radius * the blob radius (in image unit). * @param nDims @@ -277,7 +276,7 @@ public static final < T extends RealType< T > > Img< FloatType > copyToFloatImg( final RandomAccess< T > in = Views.zeroMin( Views.interval( img, interval ) ).randomAccess(); final Cursor< FloatType > out = output.cursor(); final RealFloatConverter< T > c = new RealFloatConverter<>(); - + while ( out.hasNext() ) { out.fwd(); @@ -344,7 +343,7 @@ public static final < T extends RealType< T > > List< Spot > findLocalMaxima( * Find maxima. */ - final T val = Util.getTypeFromInterval( source ).createVariable(); + final T val = source.getType().createVariable(); val.setReal( threshold ); final LocalNeighborhoodCheck< Point, T > localNeighborhoodCheck = new LocalExtrema.MaximumCheck<>( val ); final IntervalView< T > dogWithBorder = Views.interval( Views.extendMirrorSingle( source ), Intervals.expand( source, 1 ) ); @@ -492,7 +491,7 @@ else if ( source.numDimensions() > 1 ) /** * Return a view of the specified input image, at the specified channel * (0-based) and the specified frame (0-based too). - * + * * @param * the type of the input image. * @param img @@ -524,7 +523,7 @@ public static final < T extends Type< T > > RandomAccessibleInterval< T > prepar /** * Normalize the pixel value of an image between 0 and 1. - * + * * @param * the type of pixels in the image. Must extend {@link RealType}. * @param input diff --git a/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java b/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java index eec3adca7..6362ccaa4 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -23,7 +23,6 @@ import net.imglib2.Cursor; import net.imglib2.Interval; -import net.imglib2.IterableInterval; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.algorithm.dog.DifferenceOfGaussian; @@ -117,10 +116,8 @@ public boolean process() e.printStackTrace(); } - final IterableInterval< FloatType > dogIterable = Views.iterable( dog ); - final IterableInterval< FloatType > tmpIterable = Views.iterable( dog2 ); - final Cursor< FloatType > dogCursor = dogIterable.cursor(); - final Cursor< FloatType > tmpCursor = tmpIterable.cursor(); + final Cursor< FloatType > dogCursor = dog.cursor(); + final Cursor< FloatType > tmpCursor = dog2.cursor(); while ( dogCursor.hasNext() ) dogCursor.next().sub( tmpCursor.next() ); diff --git a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java index 39ec68d35..3c54dceef 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -101,9 +101,7 @@ public boolean process() { final long start = System.currentTimeMillis(); final RandomAccessibleInterval< T > rai = Views.interval( input, interval ); - final T type = Util.getTypeFromInterval( rai ); - - if ( type instanceof IntegerType ) + if ( rai.getType() instanceof IntegerType ) { processIntegerImg( ( RandomAccessibleInterval ) Views.zeroMin( rai ) ); } @@ -126,7 +124,7 @@ private < R extends IntegerType< R > > void processIntegerImg( final RandomAcces { // Get all labels. final AtomicInteger max = new AtomicInteger( 0 ); - Views.iterable( rai ).forEach( p -> { + rai.forEach( p -> { final int val = p.getInteger(); if ( val != 0 && val > max.get() ) max.set( val ); diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 547693b77..213e9e424 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java @@ -81,13 +81,13 @@ public class MaskUtils public static final < T extends RealType< T > > double otsuThreshold( final RandomAccessibleInterval< T > img ) { // Min & max - final T t = Util.getTypeFromInterval( img ); + final T t = img.getType(); final T max = t.createVariable(); max.setReal( Double.NEGATIVE_INFINITY ); final T min = t.createVariable(); min.setReal( Double.POSITIVE_INFINITY ); - for ( final T pixel : Views.iterable( img ) ) + for ( final T pixel : img ) { if ( pixel.compareTo( min ) < 0 ) min.set( pixel ); @@ -98,7 +98,7 @@ public static final < T extends RealType< T > > double otsuThreshold( final Rand // Histogram. final Real1dBinMapper< T > mapper = new Real1dBinMapper<>( min.getRealDouble(), max.getRealDouble(), 256, false ); - final Histogram1d< T > histogram = new Histogram1d<>( Views.iterable( img ), mapper ); + final Histogram1d< T > histogram = new Histogram1d<>( img, mapper ); // Threshold. final long k = getThreshold( histogram ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index 4a205973d..aa1c28d94 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -256,7 +256,7 @@ private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T { // Get all labels. final AtomicInteger max = new AtomicInteger( 0 ); - Views.iterable( rai ).forEach( p -> { + rai.forEach( p -> { final int val = p.getInteger(); if ( val != 0 && val > max.get() ) max.set( val ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index 50d71fc90..a4ef250d5 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -237,7 +237,7 @@ public void run() private Img< T > copy( final RandomAccessibleInterval< T > in ) { - final ImgFactory< T > factory = Util.getArrayOrCellImgFactory( in, Util.getTypeFromInterval( in ) ); + final ImgFactory< T > factory = Util.getArrayOrCellImgFactory( in, in.getType() ); final Img< T > out = factory.create( in ); LoopBuilder.setImages( in, out ) .multiThreaded() From 37cc27c56e7ecd36f581acbaefe17f1543fee813 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 14:21:04 +0100 Subject: [PATCH 57/65] Utility method to copy a directed graph to an undirected one. TrackMate model requires the graph to be undirected (weird choice be hey, it enforces the time-directivity 'by hand'). But sometimes it is advantageous for trackers to manipulate a directed graph. This method bridges the two, because there are no method for that in the JGraphT lib. --- .../plugin/trackmate/graph/GraphUtils.java | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java index 2bfbee9f9..a9055ee1c 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java +++ b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -35,6 +35,7 @@ import org.jgrapht.alg.util.NeighborCache; import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.jgrapht.graph.SimpleWeightedGraph; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackModel; @@ -42,6 +43,47 @@ public class GraphUtils { + /** + * Converts a {@link SimpleDirectedWeightedGraph} to a + * {@link SimpleWeightedGraph}. + * + * @param directedGraph + * the {@link SimpleDirectedWeightedGraph} to be converted + * @return a {@link SimpleWeightedGraph} with the same vertices and edges as + * the input graph, but with undirected edges and the maximum weight + * between any two vertices + */ + public static < V, E > SimpleWeightedGraph< V, E > convertToSimpleWeightedGraph( final SimpleDirectedWeightedGraph< V, E > directedGraph ) + { + @SuppressWarnings( "unchecked" ) + final Class< E > edgeClass = ( Class< E > ) directedGraph.getEdgeSupplier().get().getClass(); + final SimpleWeightedGraph< V, E > undirectedGraph = new SimpleWeightedGraph< V, E >( edgeClass ); + + // Add vertices from the directed graph + for ( final V vertex : directedGraph.vertexSet() ) + undirectedGraph.addVertex( vertex ); + + // Add edges from the directed graph + for ( final E edge : directedGraph.edgeSet() ) + { + final V source = directedGraph.getEdgeSource( edge ); + final V target = directedGraph.getEdgeTarget( edge ); + final double weight = directedGraph.getEdgeWeight( edge ); + + if ( undirectedGraph.containsEdge( source, target ) ) + { + final double existingWeight = undirectedGraph.getEdgeWeight( undirectedGraph.getEdge( source, target ) ); + undirectedGraph.setEdgeWeight( undirectedGraph.getEdge( source, target ), Math.max( existingWeight, weight ) ); + } + else + { + final E newEdge = undirectedGraph.addEdge( source, target ); + undirectedGraph.setEdgeWeight( newEdge, weight ); + } + } + return undirectedGraph; + } + /** * @return a pretty-print string representation of a {@link TrackModel}, as * long it is a tree (each spot must not have more than one @@ -312,7 +354,7 @@ public void compute( final Spot input, final int[] output ) * Find root spots & first spots Roots are spots without any ancestors. * There might be more than one per track. First spots are the first * root found in a track. There is only one per track. - * + * * By the way we compute the largest spot name */ From a0e1398b7b61eaa844e82f7bc9646e8bdbf977bc Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 14:22:53 +0100 Subject: [PATCH 58/65] Refactor the nearest neighbor tracker. Get rid of the custom classes, simply rely on the imglib2 implementation and on a directed graph to check whether a target node is free or not. Much simplier, shorter. --- .../trackmate/tracking/kdtree/FlagNode.java | 55 -------- .../NearestNeighborFlagSearchOnKDTree.java | 125 ------------------ .../kdtree/NearestNeighborTracker.java | 95 +++++++------ 3 files changed, 52 insertions(+), 223 deletions(-) delete mode 100644 src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java delete mode 100644 src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java deleted file mode 100644 index 7694cff9f..000000000 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java +++ /dev/null @@ -1,55 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ -package fiji.plugin.trackmate.tracking.kdtree; - -public class FlagNode< K > -{ - - private K value; - - private boolean visited = false; - - public FlagNode( final K value ) - { - this.setValue( value ); - } - - public boolean isVisited() - { - return visited; - } - - public void setVisited( final boolean visited ) - { - this.visited = visited; - } - - public K getValue() - { - return value; - } - - public void setValue( final K value ) - { - this.value = value; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java deleted file mode 100644 index 437fdb49b..000000000 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -package fiji.plugin.trackmate.tracking.kdtree; - -import net.imglib2.KDTree; -import net.imglib2.KDTreeNode; -import net.imglib2.RealLocalizable; -import net.imglib2.Sampler; -import net.imglib2.neighborsearch.NearestNeighborSearch; - -public class NearestNeighborFlagSearchOnKDTree< T > implements NearestNeighborSearch< FlagNode< T > > -{ - - protected KDTree< FlagNode< T > > tree; - - protected final int n; - - protected final double[] pos; - - protected KDTreeNode< FlagNode< T > > bestPoint; - - protected double bestSquDistance; - - public NearestNeighborFlagSearchOnKDTree( final KDTree< FlagNode< T > > tree ) - { - n = tree.numDimensions(); - pos = new double[ n ]; - this.tree = tree; - } - - @Override - public int numDimensions() - { - return n; - } - - @Override - public void search( final RealLocalizable p ) - { - p.localize( pos ); - bestSquDistance = Double.MAX_VALUE; - searchNode( tree.getRoot() ); - } - - protected void searchNode( final KDTreeNode< FlagNode< T > > current ) - { - // consider the current node - final double distance = current.squDistanceTo( pos ); - final boolean visited = current.get().isVisited(); - if ( distance < bestSquDistance && !visited ) - { - bestSquDistance = distance; - bestPoint = current; - } - - final double axisDiff = pos[ current.getSplitDimension() ] - current.getSplitCoordinate(); - final double axisSquDistance = axisDiff * axisDiff; - final boolean leftIsNearBranch = axisDiff < 0; - - // search the near branch - // TODO Rewrite to remove deprecated methods, proxy object based. - final KDTreeNode< FlagNode< T > > nearChild = leftIsNearBranch ? current.left() : current.right(); - final KDTreeNode< FlagNode< T > > awayChild = leftIsNearBranch ? current.right() : current.left(); - if ( nearChild != null ) - searchNode( nearChild ); - - // search the away branch - maybe - if ( ( axisSquDistance <= bestSquDistance ) && ( awayChild != null ) ) - searchNode( awayChild ); - } - - @Override - public Sampler< fiji.plugin.trackmate.tracking.kdtree.FlagNode< T > > getSampler() - { - return bestPoint; - } - - @Override - public RealLocalizable getPosition() - { - return bestPoint; - } - - @Override - public double getSquareDistance() - { - return bestSquDistance; - } - - @Override - public double getDistance() - { - return Math.sqrt( bestSquDistance ); - } - - @Override - public NearestNeighborFlagSearchOnKDTree< T > copy() - { - final NearestNeighborFlagSearchOnKDTree< T > copy = new NearestNeighborFlagSearchOnKDTree<>( tree ); - System.arraycopy( pos, 0, copy.pos, 0, pos.length ); - copy.bestPoint = bestPoint; - copy.bestSquDistance = bestSquDistance; - return copy; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java index acacd1226..81319db64 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -37,17 +37,19 @@ import java.util.concurrent.atomic.AtomicInteger; import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; import org.jgrapht.graph.SimpleWeightedGraph; import org.scijava.Cancelable; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.graph.GraphUtils; import fiji.plugin.trackmate.tracking.SpotTracker; import fiji.plugin.trackmate.util.Threads; import net.imglib2.KDTree; -import net.imglib2.RealPoint; import net.imglib2.algorithm.MultiThreadedBenchmarkAlgorithm; +import net.imglib2.neighborsearch.KNearestNeighborSearchOnKDTree; public class NearestNeighborTracker extends MultiThreadedBenchmarkAlgorithm implements SpotTracker, Cancelable { @@ -62,7 +64,7 @@ public class NearestNeighborTracker extends MultiThreadedBenchmarkAlgorithm impl protected Logger logger = Logger.VOID_LOGGER; - protected SimpleWeightedGraph< Spot, DefaultWeightedEdge > graph; + protected SimpleDirectedWeightedGraph< Spot, DefaultWeightedEdge > graph; private boolean isCanceled; @@ -134,55 +136,62 @@ public Void call() throws Exception return null; } - final List< RealPoint > targetCoords = new ArrayList<>( nTargetSpots ); - final List< FlagNode< Spot > > targetNodes = new ArrayList<>( nTargetSpots ); - final Iterator< Spot > targetIt = spots.iterator( targetFrame, true ); - while ( targetIt.hasNext() ) - { - final double[] coords = new double[ 3 ]; - final Spot spot = targetIt.next(); - spot.localize( coords ); - targetCoords.add( new RealPoint( coords ) ); - targetNodes.add( new FlagNode<>( spot ) ); - } - - final KDTree< FlagNode< Spot > > tree = new KDTree<>( targetNodes, targetCoords ); - final NearestNeighborFlagSearchOnKDTree< Spot > search = new NearestNeighborFlagSearchOnKDTree<>( tree ); + /* + * Create kD-Tree and NN search. + */ + final Iterable< Spot > targetSpots = spots.iterable( targetFrame, true ); + final KDTree< Spot > tree = new KDTree< Spot >( nTargetSpots, targetSpots, targetSpots ); + final KNearestNeighborSearchOnKDTree< Spot > search = new KNearestNeighborSearchOnKDTree<>( tree, nTargetSpots ); /* * For each spot in the source frame, find its nearest * neighbor in the target frame. */ final Iterator< Spot > sourceIt = spots.iterator( sourceFrame, true ); - while ( sourceIt.hasNext() ) + SOURCE: while ( sourceIt.hasNext() ) { final Spot source = sourceIt.next(); - final double[] coords = new double[ 3 ]; - source.localize( coords ); - final RealPoint sourceCoords = new RealPoint( coords ); - search.search( sourceCoords ); - - final double squareDist = search.getSquareDistance(); - final FlagNode< Spot > targetNode = search.getSampler().get(); + search.search( source ); /* - * The closest we could find is too far. We skip this - * source spot and do not create a link + * Loop over target spots in nearest neighbor order. */ - if ( squareDist > maxDistSquare ) - continue; - - /* - * Everything is ok. This node is free and below max - * dist. We create a link and mark this node as - * assigned. - */ - - targetNode.setVisited( true ); - synchronized ( graph ) + int iNeighbor = -1; + TARGET: while ( ++iNeighbor < nTargetSpots ) { - final DefaultWeightedEdge edge = graph.addEdge( source, targetNode.getValue() ); - graph.setEdgeWeight( edge, squareDist ); + /* + * Is the closest we could find too far? If yes, we + * skip this source spot and do not create a link. + */ + final double squareDist = search.getSquareDistance( iNeighbor ); + if ( squareDist > maxDistSquare ) + continue SOURCE; + + /* + * Is the closest one already taken? Has it already + * an incoming edge? + */ + final Spot target = search.getSampler( iNeighbor ).get(); + if ( graph.inDegreeOf( target ) > 0 ) + { + /* + * In that case we need to test the next nearest + * neighbor (next target spot). + */ + continue TARGET; + } + + /* + * Everything is ok. This node is free and below max + * dist. We create a link and loop to the next + * source spot. + */ + synchronized ( graph ) + { + final DefaultWeightedEdge edge = graph.addEdge( source, target ); + graph.setEdgeWeight( edge, squareDist ); + } + break TARGET; } } logger.setProgress( progress.incrementAndGet() / ( double ) frames.size() ); @@ -222,12 +231,12 @@ public Void call() throws Exception @Override public SimpleWeightedGraph< Spot, DefaultWeightedEdge > getResult() { - return graph; + return GraphUtils.convertToSimpleWeightedGraph( graph ); } public void reset() { - graph = new SimpleWeightedGraph<>( DefaultWeightedEdge.class ); + graph = new SimpleDirectedWeightedGraph<>( DefaultWeightedEdge.class ); final Iterator< Spot > it = spots.iterator( true ); while ( it.hasNext() ) graph.addVertex( it.next() ); From 42acf1cf2d9f4e7d9ccdd7028358aa1700812d2b Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 14:48:37 +0100 Subject: [PATCH 59/65] Rework the NN tracker UI and description. --- .../NearestNeighborTrackerSettingsPanel.java | 94 ++++++++++++------- .../kdtree/NearestNeighborTrackerFactory.java | 30 +++--- 2 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java index 379accbbb..9f9447723 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -23,15 +23,18 @@ import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; import static fiji.plugin.trackmate.gui.Fonts.FONT; -import static fiji.plugin.trackmate.gui.Fonts.TEXTFIELD_DIMENSION; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; -import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; import java.util.HashMap; import java.util.Map; +import javax.swing.BorderFactory; import javax.swing.JFormattedTextField; import javax.swing.JLabel; +import javax.swing.JTextField; import javax.swing.SwingConstants; import fiji.plugin.trackmate.gui.GuiUtils; @@ -44,12 +47,6 @@ public class NearestNeighborTrackerSettingsPanel extends ConfigurationPanel private JFormattedTextField maxDistField; - private JLabel labelTrackerDescription; - - private JLabel labelUnits; - - private JLabel labelTracker; - private final String infoText; private final String trackerName; @@ -80,44 +77,75 @@ public void setSettings( final Map< String, Object > settings ) private void initGUI() { + setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); - setLayout( null ); + final GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[] { 164, 40, 54, 0 }; + gridBagLayout.rowHeights = new int[] { 30, 40, 225, 30, 60 }; + gridBagLayout.columnWeights = new double[] { 1.0, 0.0, 0.0, Double.MIN_VALUE }; + gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 1.0, 0.0, Double.MIN_VALUE }; + setLayout( gridBagLayout ); final JLabel lblSettingsForTracker = new JLabel( "Settings for tracker:" ); - lblSettingsForTracker.setBounds( 10, 11, 280, 20 ); lblSettingsForTracker.setFont( FONT ); - add( lblSettingsForTracker ); - - labelTracker = new JLabel( trackerName ); + final GridBagConstraints gbcLblSettingsForTracker = new GridBagConstraints(); + gbcLblSettingsForTracker.fill = GridBagConstraints.BOTH; + gbcLblSettingsForTracker.insets = new Insets( 0, 0, 5, 0 ); + gbcLblSettingsForTracker.gridwidth = 3; + gbcLblSettingsForTracker.gridx = 0; + gbcLblSettingsForTracker.gridy = 0; + add( lblSettingsForTracker, gbcLblSettingsForTracker ); + + final JLabel labelTracker = new JLabel( trackerName ); labelTracker.setFont( BIG_FONT ); labelTracker.setHorizontalAlignment( SwingConstants.CENTER ); - labelTracker.setBounds( 10, 42, 280, 20 ); - add( labelTracker ); - - labelTrackerDescription = new JLabel( "" ); - labelTrackerDescription.setFont( FONT.deriveFont( Font.ITALIC ) ); - labelTrackerDescription.setBounds( 10, 67, 280, 225 ); - labelTrackerDescription.setText( infoText.replace( "
", "" ).replace( "

", "

" ).replace( "", "

" ) ); - add( labelTrackerDescription ); + final GridBagConstraints gbcLabelTracker = new GridBagConstraints(); + gbcLabelTracker.fill = GridBagConstraints.BOTH; + gbcLabelTracker.insets = new Insets( 0, 0, 5, 0 ); + gbcLabelTracker.gridwidth = 3; + gbcLabelTracker.gridx = 0; + gbcLabelTracker.gridy = 1; + add( labelTracker, gbcLabelTracker ); + + final GridBagConstraints gbcLabelTrackerDescription = new GridBagConstraints(); + gbcLabelTrackerDescription.anchor = GridBagConstraints.NORTH; + gbcLabelTrackerDescription.fill = GridBagConstraints.BOTH; + gbcLabelTrackerDescription.insets = new Insets( 0, 0, 5, 0 ); + gbcLabelTrackerDescription.gridwidth = 3; + gbcLabelTrackerDescription.gridx = 0; + gbcLabelTrackerDescription.gridy = 2; + add( GuiUtils.textInScrollPanel( GuiUtils.infoDisplay( infoText ) ), gbcLabelTrackerDescription ); final JLabel lblMaximalLinkingDistance = new JLabel( "Maximal linking distance: " ); lblMaximalLinkingDistance.setFont( FONT ); - lblMaximalLinkingDistance.setBounds( 10, 314, 164, 20 ); - add( lblMaximalLinkingDistance ); + final GridBagConstraints gbcLblMaximalLinkingDistance = new GridBagConstraints(); + gbcLblMaximalLinkingDistance.fill = GridBagConstraints.BOTH; + gbcLblMaximalLinkingDistance.insets = new Insets( 0, 0, 0, 5 ); + gbcLblMaximalLinkingDistance.gridx = 0; + gbcLblMaximalLinkingDistance.gridy = 3; + add( lblMaximalLinkingDistance, gbcLblMaximalLinkingDistance ); maxDistField = new JFormattedTextField( 15. ); + maxDistField.setHorizontalAlignment( JTextField.CENTER ); maxDistField.setFont( FONT ); - maxDistField.setBounds( 184, 316, 62, 16 ); - maxDistField.setSize( TEXTFIELD_DIMENSION ); - add( maxDistField ); - - labelUnits = new JLabel( spaceUnits ); - labelUnits.setFont( FONT ); - labelUnits.setBounds( 236, 314, 34, 20 ); - add( labelUnits ); + final GridBagConstraints gbcMaxDistField = new GridBagConstraints(); + gbcMaxDistField.fill = GridBagConstraints.BOTH; + gbcMaxDistField.insets = new Insets( 0, 0, 0, 5 ); + gbcMaxDistField.gridx = 1; + gbcMaxDistField.gridy = 3; + add( maxDistField, gbcMaxDistField ); // Select text-fields content on focus. GuiUtils.selectAllOnFocus( maxDistField ); + + final JLabel labelUnits = new JLabel( spaceUnits ); + labelUnits.setFont( FONT ); + final GridBagConstraints gbcLabelUnits = new GridBagConstraints(); + gbcLabelUnits.anchor = GridBagConstraints.WEST; + gbcLabelUnits.fill = GridBagConstraints.VERTICAL; + gbcLabelUnits.gridx = 2; + gbcLabelUnits.gridy = 3; + add( labelUnits, gbcLabelUnits ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java index 655afb5be..356847080 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -50,19 +50,23 @@ public class NearestNeighborTrackerFactory implements SpotTrackerFactory public static final String NAME = "Nearest-neighbor tracker"; public static final String INFO_TEXT = "" - + "This tracker is the most simple one, and is based on nearest neighbor
" - + "search. The spots in the target frame are searched for the nearest neighbor
" - + "of each spot in the source frame. If the spots found are closer than the
" - + "maximal allowed distance, a link between the two is created.
" + + "This tracker is the most simple one, and is based on nearest neighbor " + + "search. " + "

" - + "The nearest neighbor search relies upon the KD-tree technique implemented
" - + "in imglib by Johannes Schindelin and friends. This ensure a very efficient " - + "tracking and makes this tracker suitable for situation where a huge number
" - + "of particles are to be tracked over a very large number of frames. However,
" - + "because of the naiveness of its principles, it can result in pathological
" - + "tracks. It can only do frame-to-frame linking; there cannot be any track
" + + "For each pair of consecutive frames t1 and t2, it iterates through all " + + "spots in frame t1. For each source spot in t1, it searches for the nearest target spot " + + "in frame t2. If it is not already connected to a spot in frame t1, and is " + + "within the maximal linking distance, a link between the two spots is created.
" + + "

" + + "The nearest neighbor search relies upon the KD-tree technique implemented " + + "in imglib2. This ensure a very efficient " + + "tracking and makes this tracker suitable for situation where a huge number " + + "of particles are to be tracked over a very large number of frames. However, " + + "because of the naiveness of its principles, it can result in pathological " + + "tracks. It can only do frame-to-frame linking; there cannot be any track " + "merging or splitting, and gaps will not be closed. Also, the end results are non-" - + "deterministic." + + "deterministic, as the links created depend in the order in which the source spots " + + "are iterated." + " "; private String errorMessage; From 58f869628cad32b1d8262bf02242095b1dff01b1 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Wed, 6 Nov 2024 15:13:52 +0100 Subject: [PATCH 60/65] Fix labkit launcher with ROIs that touch the border. Before this commit, the labkit import "did not work". The image appeared black, the UI was super slow etc. Because I do not know what is really going on, and because I already have been changed these two lines, I am keeping them in comments, to see if I need to revert this later. --- .../fiji/plugin/trackmate/gui/editor/LabkitLauncher.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java index a4ef250d5..c1654465b 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -273,10 +273,10 @@ private final DatasetInputImage makeInput( final ImagePlus imp, final boolean si final long[] max = src.maxAsLongArray(); min[ 0 ] = roi.getBounds().x; min[ 1 ] = roi.getBounds().y; - max[ 0 ] = roi.getBounds().x + roi.getBounds().width; - max[ 1 ] = roi.getBounds().y + roi.getBounds().height; -// max[ 0 ] = roi.getBounds().x + roi.getBounds().width - 1; -// max[ 1 ] = roi.getBounds().y + roi.getBounds().height - 1; +// max[ 0 ] = roi.getBounds().x + roi.getBounds().width; +// max[ 1 ] = roi.getBounds().y + roi.getBounds().height; + max[ 0 ] = roi.getBounds().x + roi.getBounds().width - 1; + max[ 1 ] = roi.getBounds().y + roi.getBounds().height - 1; crop = Views.interval( src, min, max ); } else From be5c22289e5a2eb80f2a213c9abe9a03fd13fcac Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Thu, 7 Nov 2024 11:59:43 +0100 Subject: [PATCH 61/65] Fix javadoc errors. --- .../plugin/trackmate/gui/editor/LabkitImporter.java | 4 ++-- .../editor/TrackMateLabKitSegmentationComponent.java | 9 +++++++-- .../trackmate/gui/editor/TrackMateLabkitFrame.java | 12 ++++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java index aa1c28d94..51697fed7 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -85,8 +85,8 @@ public LabkitImporter( * @param currentTimePoint * the time-point in the TrackMate model that corresponds to the * index image. - * @param the - * map of spots (vs the label value in the previous labeling) + * @param previousSpotLabels + * the map of spots (vs the label value in the previous labeling) * that were written in the previous index image. */ public void reimport( diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java index 10c7bc50e..c4d407694 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -43,6 +43,7 @@ import sc.fiji.labkit.ui.BasicLabelingComponent; import sc.fiji.labkit.ui.DefaultExtensible; import sc.fiji.labkit.ui.MenuBar; +import sc.fiji.labkit.ui.SegmentationComponent; import sc.fiji.labkit.ui.actions.AddLabelingIoAction; import sc.fiji.labkit.ui.actions.LabelEditAction; import sc.fiji.labkit.ui.actions.LabelingIoAction; @@ -58,6 +59,10 @@ import sc.fiji.labkit.ui.segmentation.PredictionLayer; /** + * This class is copy/pasted and adapted from {@link SegmentationComponent} to + * control what is shown in the TrackMate UI. This could be revised to avoid + * class duplication. + * * {@link SegmentationComponent} is the central Labkit UI component. Provides UI * to display and modify a {@link SegmentationModel}. *

diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java index 5d3014ba1..f76cf3c9c 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -42,6 +42,7 @@ import net.imagej.Dataset; import sc.fiji.labkit.pixel_classification.utils.SingletonContext; import sc.fiji.labkit.ui.InitialLabeling; +import sc.fiji.labkit.ui.LabkitFrame; import sc.fiji.labkit.ui.inputimage.DatasetInputImage; import sc.fiji.labkit.ui.inputimage.InputImage; import sc.fiji.labkit.ui.models.DefaultSegmentationModel; @@ -49,10 +50,13 @@ import sc.fiji.labkit.ui.utils.Notifier; /** + * This class is copied and adapted from {@link LabkitFrame}. + *

* The main Labkit window. (This window allows to segment a single image. It has * to be distinguished from the LabkitProjectFrame, which allows to operation on - * multiple images.) The window only contains a {@link SegmentationComponent} - * and shows the associated main menu. + * multiple images.) The window only contains a + * {@link TrackMateLabKitSegmentationComponent} and shows the associated main + * menu. * * @author Matthias Arzt */ From 9507e6db3718997906593bd72b7456464bca4fe8 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Thu, 7 Nov 2024 15:52:14 +0100 Subject: [PATCH 62/65] Add javadoc. --- .../trackmate/util/cli/CLIConfigurator.java | 185 +++++++++++++++++- 1 file changed, 182 insertions(+), 3 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java index 771061383..169f15326 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -36,6 +36,14 @@ import fiji.plugin.trackmate.util.cli.CommandCLIConfigurator.ExecutablePath; import fiji.plugin.trackmate.util.cli.CondaCLIConfigurator.CondaEnvironmentCommand; +/** + * Base class for CLI configurator tools. The implementation of a CLI + * configurator is made by subclassing this class (or one of its specialization) + * and specifying arguments for the CLI component with the addXYX() + * builders. + * + * @author Jean-Yves Tinevez + */ public abstract class CLIConfigurator { @@ -282,60 +290,133 @@ abstract class Adder< A extends Argument< A, O >, T extends Adder< A, T, O >, O protected boolean inCLI = true; // by default + /** + * Specifies the argument to use in the CLI. + * + * @param argument + * the command line argument. + * @return this adder. + */ public T argument( final String argument ) { this.argument = argument; return ( T ) this; } + /** + * Specifies whether this argument will be visible in user interfaces + * generated from the configurator. + * + * @param visible + * UI visibility. + * @return this adder. + */ public T visible( final boolean visible ) { this.visible = visible; return ( T ) this; } + /** + * Specifies a user-friendly name for the argument. + * + * @param name + * the argument name. + * @return this adder. + */ public T name( final String name ) { this.name = name; return ( T ) this; } + /** + * Specifies a help text for the argument. + * + * @param help + * the help text. + * @return this adder. + */ public T help( final String help ) { this.help = help; return ( T ) this; } + /** + * Specifies the key to use to serialize this argument in TrackMate XML + * file. If null, the argument will not be serialized. + * + * @param key + * the argument key. + * @return this adder. + */ public T key( final String key ) { this.key = key; return ( T ) this; } + /** + * Specifies whether this argument is required in the CLI. If + * true, if not set and if there are no default value, an + * error will be thrown. + * + * @param required + * whether this argument is required. + * @return this adder. + */ public T required( final boolean required ) { this.required = required; return ( T ) this; } + /** + * Specifies units for values accepted by this argument. + * + * @param units + * argument value units. + * @return this adder. + */ public T units( final String units ) { this.units = units; return ( T ) this; } + /** + * Specifies a default value for this argument. If the argument is not + * set, it will appear in the command line with this default value. + * + * @param defaultValue + * the argument default value. + * @return this adder. + */ public T defaultValue( final O defaultValue ) { this.defaultValue = defaultValue; return ( T ) this; } + /** + * Specifies whether this argument will appear in the command line. + * + * @param inCLI + * appears in the command line. + * @return this adder. + */ public T inCLI( final boolean inCLI ) { this.inCLI = inCLI; return ( T ) this; } + /** + * Returns the argument created by this builder. + * + * @return the argument. + */ public abstract A get(); } @@ -346,12 +427,28 @@ private abstract class BoundedAdder< A extends BoundedValueArgument< A, O >, T e protected O max; + /** + * Specifies a min for the values accepted by this argument. If the user + * sets a value below this min value, an error is thrown. + * + * @param min + * the min value. + * @return this adder. + */ public T min( final O min ) { this.min = min; return ( T ) this; } + /** + * Specifies a max for the values accepted by this argument. If the user + * sets a value above this max value, an error is thrown. + * + * @param max + * the max value. + * @return this adder. + */ public T max( final O max ) { this.max = max; @@ -487,6 +584,14 @@ private ChoiceAdder() private final List< String > choices = new ArrayList<>(); + /** + * Adds a selectable item for this choice argument. The user will be + * able to select from the list of choices added by this method. + * + * @param choice + * the choice to add. + * @return this adder. + */ public ChoiceAdder addChoice( final String choice ) { if ( !choices.contains( choice ) ) @@ -494,6 +599,14 @@ public ChoiceAdder addChoice( final String choice ) return this; } + /** + * Adds the specified items for this choice argument. The user will be + * able to select from the list of choices added by this method. + * + * @param c + * the choices to add. + * @return this adder. + */ public Adder< ChoiceArgument, ChoiceAdder, String > addChoiceAll( final Collection< String > c ) { for ( final String in : c ) @@ -501,6 +614,17 @@ public Adder< ChoiceArgument, ChoiceAdder, String > addChoiceAll( final Collecti return this; } + /** + * Specifies a default value for this argument. If the argument is not + * set, it will appear in the command line with this default value. + *

+ * The value specified must belong to the list of choices set with + * {@link #addChoice(String)} or {@link #addChoiceAll(Collection)}. + * + * @param defaultChoice + * the argument default value. + * @return this adder. + */ @Override public ChoiceAdder defaultValue( final String defaultChoice ) { @@ -511,6 +635,16 @@ public ChoiceAdder defaultValue( final String defaultChoice ) return super.defaultValue( defaultChoice ); } + /** + * Specifies a default value for this argument, by selecting the + * possible value in order or addition. If the argument is not set, it + * will appear in the command line with this default value. + * + * @param selected + * the index of the default value in the list of possible + * choices. + * @return this adder. + */ public ChoiceAdder defaultValue( final int selected ) { if ( selected < 0 || selected >= choices.size() ) @@ -544,31 +678,73 @@ public ChoiceArgument get() * ADDER METHODS. */ + /** + * Adds a boolean flag argument to the CLI, via a builder. + *

+ * In the CLI tools we have been trying to implement, they exist in two + * flavors, that are both supported. + * + * If the argument starts with a double-dash --, as in Python + * argparse syntax, then it is understood that setting this flag to true + * makes it appear in the CLI. For instance: --use-gpu. + * + * If the arguments ends with a '=' sign (e.g. "save_txt="), + * then it expects to receive a 'true' or 'false' value. + * + * @return a new flag argument builder. + */ protected FlagAdder addFlag() { return new FlagAdder(); } + /** + * Adds a string argument to the CLI, via a builder. + * + * @return new string argument builder. + */ protected StringAdder addStringArgument() { return new StringAdder(); } + /** + * Adds a path argument to the CLI, via a builder. + * + * @return new path argument builder. + */ protected PathAdder addPathArgument() { return new PathAdder(); } + /** + * Adds a integer argument to the CLI, via a builder. + * + * @return new integer argument builder. + */ protected IntAdder addIntArgument() { return new IntAdder(); } + /** + * Adds a double argument to the CLI, via a builder. + * + * @return new double argument builder. + */ protected DoubleAdder addDoubleArgument() { return new DoubleAdder(); } + /** + * Adds a choice argument to the CLI, via a builder. Such arguments can + * accept a series of discrete values (specified by addChoice() method in + * the builder). + * + * @return new choice argument builder. + */ protected ChoiceAdder addChoiceArgument() { return new ChoiceAdder(); @@ -596,6 +772,9 @@ public static class Flag extends Argument< Flag, Boolean > Flag() {} + /** + * Sets this flag argument to true. + */ public void set() { set( true ); @@ -708,7 +887,7 @@ public static class ChoiceArgument extends AbstractStringArgument< ChoiceArgumen private ChoiceArgument() {} - ChoiceArgument addChoice( final String choice ) + private ChoiceArgument addChoice( final String choice ) { if ( !choices.contains( choice ) ) choices.add( choice ); From 89d3b7a64b9a2c8bb33febc01794989dfc8411c9 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Fri, 8 Nov 2024 10:42:10 +0100 Subject: [PATCH 63/65] Make sure we don't save a log with invalid XML chars. --- .../fiji/plugin/trackmate/io/IOUtils.java | 19 +++++++++++++++++-- .../fiji/plugin/trackmate/io/TmXmlWriter.java | 6 +++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java index 3a5b6cab7..9a7c82a8c 100644 --- a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java +++ b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -37,6 +37,7 @@ import java.io.FilenameFilter; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import javax.swing.JDialog; import javax.swing.JFileChooser; @@ -59,6 +60,20 @@ public class IOUtils { + private static final Pattern ILLEGAL_XML_CHARS = Pattern.compile( + "[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F-\\x84\\x86-\\x9F\\uD800-\\uDFFF\\uFDD0-\\uFDEF\\uFFFE\\uFFFF\\u2FFFE\\u2FFFF\\u3FFFE\\u3FFFF\\u4FFFE\\u4FFFF\\u5FFFE\\u5FFFF\\u6FFFE\\u6FFFF\\u7FFFE\\u7FFFF\\u8FFFE\\u8FFFF\\u9FFFE\\u9FFFF\\uAFFFE\\uAFFFF\\uBFFFE\\uBFFFF\\uCFFFE\\uCFFFF\\uDFFFE\\uDFFFF\\uEFFFE\\uEFFFF\\uFFFFE\\uFFFFF\\u10FFFE\\u10FFFF]" ); + + private static final Pattern ILLEGAL_SURROGATE_PAIRS = Pattern.compile( + "[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF])[\uDC00-\uDFFF]" ); + + public static String cleanInvalidXmlChars( final String input ) + { + if ( input == null ) + return null; + final String cleaned = ILLEGAL_XML_CHARS.matcher( input ).replaceAll( "" ); + return ILLEGAL_SURROGATE_PAIRS.matcher( cleaned ).replaceAll( "" ); + } + public static final boolean canReadFile( final String path, final StringBuilder errorHolder ) { if ( path.isEmpty() ) diff --git a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java index 79d0bf9ef..6b74242f9 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java +++ b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -286,7 +286,7 @@ public void appendLog( final String log ) if ( null != log ) { final Element logElement = new Element( LOG_ELEMENT_KEY ); - logElement.addContent( log ); + logElement.addContent( IOUtils.cleanInvalidXmlChars( log ) ); root.addContent( logElement ); logger.log( " Added log.\n" ); } From 11147cd89ad89c6bd4f00ec2fac909bb22db3848 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Thu, 12 Dec 2024 15:54:50 +0100 Subject: [PATCH 64/65] Sets the units of a model from the settings when creating an empty TrackMate. --- src/main/java/fiji/plugin/trackmate/TrackMate.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/TrackMate.java b/src/main/java/fiji/plugin/trackmate/TrackMate.java index f463dd7d8..1e5caf68a 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackMate.java +++ b/src/main/java/fiji/plugin/trackmate/TrackMate.java @@ -107,6 +107,12 @@ public class TrackMate implements Benchmark, MultiThreaded, Algorithm, Named, Ca public TrackMate( final Settings settings ) { this( new Model(), settings ); + if ( settings.imp != null && settings.imp.getCalibration() != null ) + { + final String spaceUnits = settings.imp.getCalibration().getXUnit(); + final String timeUnits = settings.imp.getCalibration().getTimeUnit(); + model.setPhysicalUnits( spaceUnits, timeUnits ); + } } public TrackMate( final Model model, final Settings settings ) From fdf59bc21ade5e8721ea83ddce06990025981f52 Mon Sep 17 00:00:00 2001 From: Jean-Yves Tinevez Date: Thu, 12 Dec 2024 15:55:22 +0100 Subject: [PATCH 65/65] Implement equals() for FeatureFilter. Important if we want to decide whether a filter is present in a collection or not. --- .../trackmate/features/FeatureFilter.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java b/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java index 6d2ca0142..ee0d0b599 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java @@ -21,6 +21,9 @@ */ package fiji.plugin.trackmate.features; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + /** * A helper class to store a feature filter. It is just made of 3 public fields. *

@@ -55,4 +58,31 @@ public String toString() return str; } + @Override + public boolean equals( final Object obj ) + { + if ( obj == null ) + return false; + if ( obj == this ) + return true; + if ( obj.getClass() != getClass() ) + return false; + final FeatureFilter other = ( FeatureFilter ) obj; + return new EqualsBuilder() + .append( feature, other.feature ) + .append( value, other.value ) + .append( isAbove, other.isAbove ) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder( 17, 37 ) + .append( feature ) + .append( value ) + .append( isAbove ) + .toHashCode(); + } + }