diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 02c9c4e6c..851b4521a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,6 +28,7 @@ jobs:
- name: Execute the build
run: .github/build.sh
env:
+ MAVEN_OPTS: -Djava.awt.headless=true
GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
MAVEN_USER: ${{ secrets.MAVEN_USER }}
diff --git a/pom.xml b/pom.xml
index 41da46dd9..19d045462 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,17 +1,19 @@
-
- * The {@link TrackMate} object is loaded and properly configured. This
+ * The {@link guiModel} object is loaded and properly configured. This
* method is called just before the controller and GUI are launched.
*
* @param trackmate
- * the {@link TrackMate} instance that was fledged after loading.
+ * the {@link guiModel} instance that was fledged after loading.
*/
- protected void postRead( final TrackMate trackmate )
+ protected void postRead( final GuiModel guiModel )
{}
/**
@@ -297,13 +292,20 @@ protected TmXmlReader createReader( final File lFile )
public static void main( final String[] args )
{
- GuiUtils.setSystemLookAndFeel();
- ImageJ.main( args );
- final LoadTrackMatePlugIn plugIn = new LoadTrackMatePlugIn();
+ try
+ {
+ GuiUtils.setSystemLookAndFeel();
+ ImageJ.main( args );
+ final LoadTrackMatePlugIn plugIn = new LoadTrackMatePlugIn();
// 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" );
+ }
+ catch ( final Throwable t )
+ {
+ t.printStackTrace();
+ }
}
}
diff --git a/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java b/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java
index 354f4d7cb..5d90e0210 100644
--- a/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java
+++ b/src/main/java/fiji/plugin/trackmate/ManualTrackingPlugIn.java
@@ -22,7 +22,7 @@
package fiji.plugin.trackmate;
import fiji.plugin.trackmate.detection.ManualDetectorFactory;
-import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings;
+import fiji.plugin.trackmate.gui.GuiModel;
import fiji.plugin.trackmate.gui.wizard.WizardSequence;
import fiji.plugin.trackmate.gui.wizard.descriptors.ConfigureViewsDescriptor;
import fiji.plugin.trackmate.tracking.manual.ManualTrackerFactory;
@@ -33,9 +33,17 @@ public class ManualTrackingPlugIn extends TrackMatePlugIn
{
@Override
- protected WizardSequence createSequence( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings )
+ protected WizardSequence createSequence( final GuiModel guiModel )
{
- final WizardSequence sequence = super.createSequence( trackmate, selectionModel, displaySettings );
+ // Trigger computation of features so that they analyzers are declared
+ // in the model.
+ final TrackMate trackmate = guiModel.getTrackMate();
+ trackmate.computeSpotFeatures( false );
+ trackmate.computeEdgeFeatures( false );
+ trackmate.computeTrackFeatures( false );
+
+ // Create sequence and position it on the ConfigureViewsDescriptor.
+ final WizardSequence sequence = super.createSequence( guiModel );
sequence.setCurrent( ConfigureViewsDescriptor.KEY );
return sequence;
}
@@ -54,18 +62,6 @@ protected Settings createSettings( final ImagePlus imp )
return lSettings;
}
- @Override
- protected TrackMate createTrackMate( final Model model, final Settings settings )
- {
- final TrackMate trackmate = super.createTrackMate( model, settings );
- // Trigger computation of features so that they analyzers are declared
- // in the model.
- trackmate.computeSpotFeatures( false );
- trackmate.computeEdgeFeatures( false );
- trackmate.computeTrackFeatures( false );
- return trackmate;
- }
-
public static void main( final String[] args )
{
ImageJ.main( args );
diff --git a/src/main/java/fiji/plugin/trackmate/Model.java b/src/main/java/fiji/plugin/trackmate/Model.java
index 3c4289686..b4e963ef8 100644
--- a/src/main/java/fiji/plugin/trackmate/Model.java
+++ b/src/main/java/fiji/plugin/trackmate/Model.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
*
* Subclassers can override this method to have the model work with their * own subclass of {@link FeatureModel}. - * + * * @return a new instance of {@link FeatureModel}. */ protected FeatureModel createFeatureModel() @@ -321,7 +331,7 @@ public void clearTracks( final boolean doNotify ) /** * Returns the {@link TrackModel} that manages the tracks for this model. - * + * * @return the track model. */ public TrackModel getTrackModel() @@ -447,7 +457,7 @@ public void notifyFeaturesComputed() /** * Set the logger that will receive the messages from the processes * occurring within this trackmate. - * + * * @param logger * the {@link Logger} to use. */ @@ -458,7 +468,7 @@ public void setLogger( final Logger logger ) /** * Return the logger currently set for this model. - * + * * @return the {@link Logger} used. */ public Logger getLogger() @@ -544,7 +554,7 @@ public synchronized Spot moveSpotFrom( final Spot spotToMove, final Integer from * model.endUpdate(); * } * - * + * * @param spotToAdd * the spot to add. * @param toFrame @@ -593,8 +603,11 @@ public synchronized Spot removeSpot( final Spot spotToRemove ) if ( DEBUG ) System.out.println( "[TrackMateModel] Removing spot " + spotToRemove + " from frame " + fromFrame ); - trackModel.removeSpot( spotToRemove ); - // changes to edges will be caught automatically by the TrackGraphModel + // Flag all tracks for undo before removing spot (may split tracks) + undoRedoStack.flagAllTracksForUndo(); + trackModel.removeSpot( spotToRemove ); + // changes to edges will be caught automatically by the + // TrackGraphModel return spotToRemove; } if ( DEBUG ) @@ -603,37 +616,6 @@ public synchronized Spot removeSpot( final Spot spotToRemove ) return null; } - /** - * Mark the specified spot for update. At the end of the model transaction, - * its features will be recomputed, and other edge and track features that - * depends on it will be as well. - *
- * For the model update to happen correctly and listeners to be notified - * properly, a call to this method must happen within a transaction, as in: - * - *
- * model.beginUpdate();
- * try {
- * ... // model modifications here
- * } finally {
- * model.endUpdate();
- * }
- *
- *
- * @param spotToUpdate
- * the spot to mark for update
- */
- public synchronized void updateFeatures( final Spot spotToUpdate )
- {
- spotsUpdated.add( spotToUpdate ); // Enlist for feature update when
- // transaction is marked as finished
- final Set< DefaultWeightedEdge > touchingEdges = trackModel.edgesOf( spotToUpdate );
- if ( null != touchingEdges )
- {
- trackModel.edgesModified.addAll( touchingEdges );
- }
- }
-
/**
* Creates a new edge between two spots, with the specified weight.
* @@ -659,6 +641,15 @@ public synchronized void updateFeatures( final Spot spotToUpdate ) */ public synchronized DefaultWeightedEdge addEdge( final Spot source, final Spot target, final double weight ) { + // Check if this edge will merge two tracks + final Integer sourceTrackId = trackModel.trackIDOf( source ); + final Integer targetTrackId = trackModel.trackIDOf( target ); + if ( sourceTrackId != null && targetTrackId != null && !sourceTrackId.equals( targetTrackId ) ) + { + // Flag both tracks for undo before merging + undoRedoStack.flagTrackForUndo( sourceTrackId ); + undoRedoStack.flagTrackForUndo( targetTrackId ); + } return trackModel.addEdge( source, target, weight ); } @@ -674,6 +665,8 @@ public synchronized DefaultWeightedEdge addEdge( final Spot source, final Spot t */ public synchronized DefaultWeightedEdge removeEdge( final Spot source, final Spot target ) { + // Flag all tracks for undo before removing edge (may split tracks) + undoRedoStack.flagAllTracksForUndo(); return trackModel.removeEdge( source, target ); } @@ -699,6 +692,8 @@ public synchronized DefaultWeightedEdge removeEdge( final Spot source, final Spo */ public synchronized boolean removeEdge( final DefaultWeightedEdge edge ) { + // Flag all tracks for undo before removing edge (may split tracks) + undoRedoStack.flagAllTracksForUndo(); return trackModel.removeEdge( edge ); } @@ -760,13 +755,44 @@ public synchronized boolean setTrackVisibility( final Integer trackID, final boo return oldvis; } + /** + * Sets the name of the track with the specified ID. + *
+ * This method captures the current track state for undo support before + * changing the name. For the model update to happen correctly and listeners + * to be notified properly, a call to this method must happen within a + * transaction, as in: + * + *
+ * model.beginUpdate();
+ * try {
+ * model.setTrackName( trackId, "MyTrackName" );
+ * } finally {
+ * model.endUpdate();
+ * }
+ *
+ *
+ * @param trackID
+ * the track ID.
+ * @param name
+ * the name for the track.
+ */
+ public synchronized void setTrackName( final Integer trackID, final String name )
+ {
+ // Capture current track state for undo
+ undoRedoStack.flagTrackForUndo( trackID );
+ trackModel.setName( trackID, name );
+ // Mark track as having its name modified for the event system
+ tracksNamedModified.add( trackID );
+ }
+
/**
* Returns a copy of this model.
* * The copy is made of the same spot objects but on a different graph, that * can be safely edited. The copy does not include the feature values for * edges and tracks, but the features are declared. - * + * * @return a new model. */ public Model copy() @@ -810,7 +836,7 @@ public Model copy() featureModel.getTrackFeatureShortNames(), featureModel.getTrackFeatureDimensions(), featureModel.getTrackFeatureIsInt() ); - + // Feature values are not copied. return copy; } @@ -824,12 +850,11 @@ public Model copy() */ private void flushUpdate() { - if ( DEBUG ) { System.out.println( "[TrackMateModel] #flushUpdate()." ); System.out.println( "[TrackMateModel] #flushUpdate(): Event cache is :" + eventCache ); - System.out.println( "[TrackMateModel] #flushUpdate(): Track content is:\n" + trackModel.echo() ); +// System.out.println( "[TrackMateModel] #flushUpdate(): Track content is:\n" + trackModel.echo() ); } /* @@ -841,7 +866,7 @@ private void flushUpdate() final int nEdgesToSignal = trackModel.edgesAdded.size() + trackModel.edgesRemoved.size() + trackModel.edgesModified.size(); // Do we have tracks to update? - final HashSet< Integer > tracksToUpdate = new HashSet< >( trackModel.tracksUpdated ); + final HashSet< Integer > tracksToUpdate = new HashSet<>( trackModel.tracksUpdated ); // We also want to update the tracks that have edges that were modified for ( final DefaultWeightedEdge modifiedEdge : trackModel.edgesModified ) @@ -849,11 +874,14 @@ private void flushUpdate() tracksToUpdate.add( trackModel.trackIDOf( modifiedEdge ) ); } + // Add tracks whose names were modified + tracksToUpdate.addAll( tracksNamedModified ); + // Deal with new or moved spots: we need to update their features. final int nSpotsToUpdate = spotsAdded.size() + spotsMoved.size() + spotsUpdated.size(); if ( nSpotsToUpdate > 0 ) { - final HashSet< Spot > spotsToUpdate = new HashSet< >( nSpotsToUpdate ); + final HashSet< Spot > spotsToUpdate = new HashSet<>( nSpotsToUpdate ); spotsToUpdate.addAll( spotsAdded ); spotsToUpdate.addAll( spotsMoved ); spotsToUpdate.addAll( spotsUpdated ); @@ -877,7 +905,10 @@ private void flushUpdate() } for ( final Spot spot : spotsRemoved ) { - event.putSpotFlag( spot, ModelChangeEvent.FLAG_SPOT_REMOVED ); + // Skip spots that were both added and removed in the same transaction + // (they are transient and should not be flagged as removed) + if ( !spotsAdded.contains( spot ) ) + event.putSpotFlag( spot, ModelChangeEvent.FLAG_SPOT_REMOVED ); } for ( final Spot spot : spotsMoved ) { @@ -913,9 +944,12 @@ private void flushUpdate() // Configure it with the tracks we found need updating event.setTracksUpdated( tracksToUpdate ); + // Fire the event if there are any changes to signal + final boolean hasChanges = nEdgesToSignal + nSpotsToSignal > 0 || !tracksNamedModified.isEmpty(); + try { - if ( nEdgesToSignal + nSpotsToSignal > 0 ) + if ( hasChanges ) { if ( DEBUG ) { @@ -950,6 +984,7 @@ private void flushUpdate() spotsRemoved.clear(); spotsMoved.clear(); spotsUpdated.clear(); + tracksNamedModified.clear(); trackModel.edgesAdded.clear(); trackModel.edgesRemoved.clear(); trackModel.edgesModified.clear(); @@ -958,4 +993,88 @@ private void flushUpdate() } } + private static class SpotMeshSliceCacheInvalidator implements ModelChangeListener + { + + @Override + public void modelChanged( final ModelChangeEvent event ) + { + if ( event.getEventID() != ModelChangeEvent.MODEL_MODIFIED ) + return; + + event.getSpots() + .stream() + .filter( s -> event.getSpotFlag( s ) == ModelChangeEvent.FLAG_SPOT_MODIFIED ) + .filter( s -> ( s instanceof SpotMesh ) ) + .forEach( s -> ( ( SpotMesh ) s ).resetZSliceCache() ); + } + } + + /** + * Pauses the undo recording. + *
+ * This is useful when a process is going to make a lot of changes to the + * model that we don't want to be recorded in the undo stack. + */ + public void pauseUndo() + { + undoRedoStack.pauseUndo(); + } + + /** + * Resumes the undo recording. + */ + public void resumeUndo() + { + undoRedoStack.resumeUndo(); + } + + /** + * Undo the last action. + */ + public void undo() + { + undoRedoStack.undo(); + } + + /** + * Redo the last undone action. + */ + public void redo() + { + undoRedoStack.redo(); + } + + /** + * Starts the edition of a spot. + *
+ * This method must be called before a spot is modified (moving it, + * changing a feature value, editing its name, ...), so that the undo stack + * can record its current state. For the model update to happen correctly + * and listeners to be notified properly, a call to this method must happen + * within a transaction, as in: + * + *
+ * model.beginUpdate();
+ * try {
+ * model.beforeEdit( spot );
+ * ... // model modifications here
+ * } finally {
+ * model.endUpdate();
+ * }
+ *
+ *
+ * @param spot
+ * the spot to mark for update
+ */
+ public void beforeEdit( final Spot spot )
+ {
+ // Capture current state of the spot for undo
+ undoRedoStack.flagForUndo( spot );
+ // Enlist for feature update when transaction is marked as finished
+ spotsUpdated.add( spot );
+ final Set< DefaultWeightedEdge > touchingEdges = trackModel.edgesOf( spot );
+ if ( null != touchingEdges )
+ trackModel.edgesModified.addAll( touchingEdges );
+ }
}
diff --git a/src/main/java/fiji/plugin/trackmate/SelectionModel.java b/src/main/java/fiji/plugin/trackmate/SelectionModel.java
index 6c5fea292..7ac5a4951 100644
--- a/src/main/java/fiji/plugin/trackmate/SelectionModel.java
+++ b/src/main/java/fiji/plugin/trackmate/SelectionModel.java
@@ -28,7 +28,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.Stack;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.traverse.GraphIterator;
@@ -314,67 +313,26 @@ public void selectTrack( final Collection< Spot > spots, final Collection< Defau
final HashSet< Spot > lSpotSelection = new HashSet<>();
final HashSet< DefaultWeightedEdge > lEdgeSelection = new HashSet<>();
- if ( direction == 0 )
- { // Unconditionally
- for ( final Spot spot : inspectionSpots )
- {
- lSpotSelection.add( spot );
- final GraphIterator< Spot, DefaultWeightedEdge > walker = model.getTrackModel().getDepthFirstIterator( spot, false );
- while ( walker.hasNext() )
- {
- final Spot target = walker.next();
- lSpotSelection.add( target );
- // Deal with edges
- final Set< DefaultWeightedEdge > targetEdges = model.getTrackModel().edgesOf( target );
- for ( final DefaultWeightedEdge targetEdge : targetEdges )
- {
- lEdgeSelection.add( targetEdge );
- }
- }
- }
+ for ( final Spot spot : inspectionSpots )
+ {
+ lSpotSelection.add( spot );
- }
- else
- { // Only upward or backward in time
- for ( final Spot spot : inspectionSpots )
+ final GraphIterator< Spot, DefaultWeightedEdge > walker;
+ if ( direction == 0 )
+ walker = model.getTrackModel().getDepthFirstIterator( spot );
+ else if ( direction > 0 )
+ walker = model.getTrackModel().getDirectedDepthFirstIterator( spot, true );
+ else
+ walker = model.getTrackModel().getDirectedDepthFirstIterator( spot, false );
+
+ while ( walker.hasNext() )
{
- lSpotSelection.add( spot );
-
- /*
- * A bit more complicated: we want to walk in only one
- * direction, when branching is occurring, we do not want to get
- * back in time.
- */
- final Stack< Spot > stack = new Stack<>();
- stack.add( spot );
- while ( !stack.isEmpty() )
- {
- final Spot inspected = stack.pop();
- final Set< DefaultWeightedEdge > targetEdges = model.getTrackModel().edgesOf( inspected );
- for ( final DefaultWeightedEdge targetEdge : targetEdges )
- {
- Spot other;
- if ( direction > 0 )
- {
- /*
- * Upward in time: we just have to search through
- * edges using their source spots.
- */
- other = model.getTrackModel().getEdgeSource( targetEdge );
- }
- else
- {
- other = model.getTrackModel().getEdgeTarget( targetEdge );
- }
-
- if ( other != inspected )
- {
- lSpotSelection.add( other );
- lEdgeSelection.add( targetEdge );
- stack.add( other );
- }
- }
- }
+ final Spot target = walker.next();
+ lSpotSelection.add( target );
+ // Deal with edges
+ final Set< DefaultWeightedEdge > targetEdges = model.getTrackModel().edgesOf( target );
+ for ( final DefaultWeightedEdge targetEdge : targetEdges )
+ lEdgeSelection.add( targetEdge );
}
}
@@ -388,9 +346,7 @@ public void selectTrack( final Collection< Spot > spots, final Collection< Defau
final Spot source = model.getTrackModel().getEdgeSource( edge );
final Spot target = model.getTrackModel().getEdgeTarget( edge );
if ( !( lSpotSelection.contains( source ) && lSpotSelection.contains( target ) ) )
- {
edgesToRemove.add( edge );
- }
}
lEdgeSelection.removeAll( edgesToRemove );
@@ -398,5 +354,4 @@ public void selectTrack( final Collection< Spot > spots, final Collection< Defau
addSpotToSelection( lSpotSelection );
addEdgeToSelection( lEdgeSelection );
}
-
}
diff --git a/src/main/java/fiji/plugin/trackmate/Settings.java b/src/main/java/fiji/plugin/trackmate/Settings.java
index 79e925cd7..10bcc4fa3 100644
--- a/src/main/java/fiji/plugin/trackmate/Settings.java
+++ b/src/main/java/fiji/plugin/trackmate/Settings.java
@@ -35,8 +35,9 @@
import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase;
import fiji.plugin.trackmate.features.track.TrackAnalyzer;
import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider;
+import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider;
+import fiji.plugin.trackmate.providers.Spot3DMorphologyAnalyzerProvider;
import fiji.plugin.trackmate.providers.SpotAnalyzerProvider;
-import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider;
import fiji.plugin.trackmate.providers.TrackAnalyzerProvider;
import fiji.plugin.trackmate.tracking.SpotTrackerFactory;
import ij.ImagePlus;
@@ -532,24 +533,37 @@ public String getErrorMessage()
*/
public void addAllAnalyzers()
{
+ // Base spot analyzers.
final SpotAnalyzerProvider spotAnalyzerProvider = new SpotAnalyzerProvider( imp == null ? 1 : imp.getNChannels() );
final List< String > spotAnalyzerKeys = spotAnalyzerProvider.getKeys();
for ( final String key : spotAnalyzerKeys )
addSpotAnalyzerFactory( spotAnalyzerProvider.getFactory( key ) );
+ // Shall we add 2D morphology analyzers?
if ( imp != null && DetectionUtils.is2D( imp ) && detectorFactory != null && detectorFactory.has2Dsegmentation() )
{
- final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new SpotMorphologyAnalyzerProvider( imp.getNChannels() );
+ final Spot2DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot2DMorphologyAnalyzerProvider( imp.getNChannels() );
final List< String > spotMorphologyAnaylyzerKeys = spotMorphologyAnalyzerProvider.getKeys();
for ( final String key : spotMorphologyAnaylyzerKeys )
addSpotAnalyzerFactory( spotMorphologyAnalyzerProvider.getFactory( key ) );
}
+ // Shall we add 3D morphology analyzers?
+ if ( imp != null && !DetectionUtils.is2D( imp ) && detectorFactory != null && detectorFactory.has3Dsegmentation() )
+ {
+ final Spot3DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot3DMorphologyAnalyzerProvider( imp.getNChannels() );
+ final List< String > spotMorphologyAnaylyzerKeys = spotMorphologyAnalyzerProvider.getKeys();
+ for ( final String key : spotMorphologyAnaylyzerKeys )
+ addSpotAnalyzerFactory( spotMorphologyAnalyzerProvider.getFactory( key ) );
+ }
+
+ // Edge analyzers.
final EdgeAnalyzerProvider edgeAnalyzerProvider = new EdgeAnalyzerProvider();
final List< String > edgeAnalyzerKeys = edgeAnalyzerProvider.getKeys();
for ( final String key : edgeAnalyzerKeys )
addEdgeAnalyzer( edgeAnalyzerProvider.getFactory( key ) );
+ // Track analyzers.
final TrackAnalyzerProvider trackAnalyzerProvider = new TrackAnalyzerProvider();
final List< String > trackAnalyzerKeys = trackAnalyzerProvider.getKeys();
for ( final String key : trackAnalyzerKeys )
diff --git a/src/main/java/fiji/plugin/trackmate/Spot.java b/src/main/java/fiji/plugin/trackmate/Spot.java
index e0e1ffb37..70ae40247 100644
--- a/src/main/java/fiji/plugin/trackmate/Spot.java
+++ b/src/main/java/fiji/plugin/trackmate/Spot.java
@@ -23,39 +23,44 @@
import static fiji.plugin.trackmate.SpotCollection.VISIBILITY;
-import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
+import com.google.common.collect.ImmutableMap;
+
import fiji.plugin.trackmate.util.AlphanumComparator;
-import net.imglib2.AbstractEuclideanSpace;
+import fiji.plugin.trackmate.util.TMUtils;
+import net.imagej.ImgPlus;
+import net.imglib2.EuclideanSpace;
+import net.imglib2.IterableInterval;
+import net.imglib2.Localizable;
+import net.imglib2.RandomAccessible;
+import net.imglib2.RealInterval;
import net.imglib2.RealLocalizable;
+import net.imglib2.RealPositionable;
+import net.imglib2.type.numeric.RealType;
import net.imglib2.util.Util;
+import net.imglib2.view.Views;
/**
- * A {@link RealLocalizable} implementation, used in TrackMate to represent a
- * detection.
+ * Interface for spots, used in TrackMate to represent a detection, or an object
+ * to be tracked.
* - * On top of being a {@link RealLocalizable}, it can store additional numerical - * named features, with a {@link Map}-like syntax. Constructors enforce the - * specification of the spot location in 3D space (if Z is unused, put 0), the - * spot radius, and the spot quality. This somewhat cumbersome syntax is made to - * avoid any bad surprise with missing features in a subsequent use. The spot - * temporal features ({@link #FRAME} and {@link #POSITION_T}) are set upon - * adding to a {@link SpotCollection}. + * This interface privileges a map of String->Double organization of + * numerical feature, with the X, Y and Z coordinates stored in this map. This + * allows for default implementations for many of the {@link RealLocalizable} + * and {@link RealPositionable} methods of this interface. *
- * Each spot received at creation a unique ID (as an
+ * On top of being a {@link RealLocalizable}, it can store additional numerical
+ * named features, with a {@link Map}-like syntax. Constructors enforce the
+ * specification of the spot location in 3D space (if Z is unused, put 0), the
+ * spot radius, and the spot quality. This somewhat cumbersome syntax is made to
+ * avoid any bad surprise with missing features in a subsequent use. The spot
+ * temporal features ({@link #FRAME} and {@link #POSITION_T}) are set upon
+ * adding to a {@link SpotCollection}.
+ *
+ * Each spot received at creation a unique ID (as an
+ * The coordinates of the vertices are relative to the spot center. That is:
+ * the coordinates are centered on (0,0,0).
+ *
+ * @return the mesh.
+ */
+ public Mesh getMesh()
+ {
+ return ReadOnlyMesh.readOnly( mesh );
+ }
+
+ @Override
+ public double realMax( final int d )
+ {
+ return getDoublePosition( d ) + boundingBox.realMax( d );
+ }
+
+ @Override
+ public double realMin( final int d )
+ {
+ return getDoublePosition( d ) + boundingBox.realMin( d );
+ }
+
+ @Override
+ public < T extends RealType< T > > IterableInterval< T > iterable( final RandomAccessible< T > ra, final double[] calibration )
+ {
+ return new SpotMeshIterable<>( ra, this, calibration );
+ }
+
+ /**
+ * Gets the slice resulting from the intersection of the mesh with the XY
+ * plane with the specified z position, in pixel coordinates, 0-based.
+ *
+ * Relies on a sort of Z-slice cache. To regenerate it if needed, we need
+ * the specification of a scale in XY and Z specified here.
+ *
+ * @param zSlice
+ * the Z position of the slice, in pixel coordinates, 0-based.
+ * @param xyScale
+ * a measure of the mesh scale along XY, for instance the pixel
+ * size in XY that it was generated from. Used to correct and
+ * simplify the slice contours.
+ * @param zScale
+ * the pixel size in Z, used to generate the Z planes spacing.
+ * @return the slice, or
+ * The set maintains the order of the track IDs, so that iterating over it
+ * will return the track IDs in the order of their names. It is possible to
+ * add it to a {@link java.util.List} to facilitate navigating the ids:
+ *
+ * The iterator will iterate forward in time only. If the boolean flag
+ *
* See http://celltrackingchallenge.net/
- *
+ *
* @author Jean-Yves Tinevez
*
*/
@@ -173,7 +174,7 @@ public static String exportAll( final String exportRootFolder, final TrackMate t
/**
* Saves the settings part as XML for reference.
- *
+ *
* @param exportRootFolder
* the root folder for exporting.
* @param saveId
@@ -184,7 +185,7 @@ public static String exportAll( final String exportRootFolder, final TrackMate t
* @param logger
* a logger to report progress.
* @throws IOException
- * if a problem happens during writing.
+ * if there's any problem writing.
*/
public static void exportSettingsFile( final String exportRootFolder, final int saveId, final TrackMate trackmate, final Logger logger ) throws IOException
{
@@ -204,7 +205,7 @@ public static void exportSettingsFile( final String exportRootFolder, final int
* For instance the first id return will be '1', which means that the
* original image data will be saved under the folder '01'. If '01' already
* exists, then this method will return 2, etc.
- *
+ *
* @param exportRootFolder
* the root folder in which to export the data.
* @return an integer id that can be passed in the other method of this
@@ -271,7 +272,7 @@ public static void exportOriginalImageData( final String exportRootFolder, final
*
* Only exports the spots that have a ROI, and write only the frames that
* have at least one spot with a ROI.
- *
+ *
* @param exportRootFolder
* the root of the export folder.
* @param saveId
@@ -316,15 +317,16 @@ public static void exportSegmentationData( final String exportRootFolder, final
for ( int frame = 0; frame < dims[ 3 ]; frame++ )
{
final ImgPlus< UnsignedShortType > imgCT = TMUtils.hyperSlice( labelImg, 0, frame );
- final SpotRoiWriter< UnsignedShortType > spotWriter = new SpotRoiWriter<>( imgCT );
+ final SpotShapeWriter< UnsignedShortType > spotWriter = new SpotShapeWriter<>( imgCT );
for ( final Spot spot : model.getSpots().iterable( frame, true ) )
{
- if ( spot.getRoi() == null )
- continue;
- final int id = idGen.getAndIncrement();
- spotWriter.write( spot, id );
- framesToWrite.add( Integer.valueOf( frame ) );
+ if ( spot instanceof SpotRoi )
+ {
+ final int id = idGen.getAndIncrement();
+ spotWriter.write( spot, id );
+ framesToWrite.add( Integer.valueOf( frame ) );
+ }
}
}
@@ -340,7 +342,7 @@ public static void exportSegmentationData( final String exportRootFolder, final
final Function< Long, String > tifNameGen = nFrames > 999
? i -> String.format( "man_seg%04d.tif", i )
: i -> String.format( "man_seg%03d.tif", i );
-
+
// Only save frames with spots in.
for ( final int frame : framesToWrite )
{
@@ -411,8 +413,8 @@ public static String exportTrackingData( final String exportRootFolder, final in
Files.createDirectories( path.getParent() );
logger.log( "Exporting tracking text file to " + path.toString() );
- try (FileOutputStream fos = new FileOutputStream( path.toFile() );
- BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( fos ) ))
+ try (final FileOutputStream fos = new FileOutputStream( path.toFile() );
+ final BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( fos ) ))
{
for ( final Integer trackID : trackModel.trackIDs( true ) )
@@ -448,7 +450,7 @@ public static String exportTrackingData( final String exportRootFolder, final in
{
final long frame = spot.getFeature( Spot.FRAME ).longValue();
final ImgPlus< UnsignedShortType > imgCT = TMUtils.hyperSlice( labelImg, 0, frame );
- final SpotRoiWriter< UnsignedShortType > spotRoiWriter = new SpotRoiWriter<>( imgCT );
+ final SpotShapeWriter< UnsignedShortType > spotRoiWriter = new SpotShapeWriter<>( imgCT );
spotRoiWriter.write( spot, currentID );
}
@@ -510,7 +512,7 @@ public static String exportTrackingData( final String exportRootFolder, final in
/**
* Returns the folder in which the tracking data will be exported.
- *
+ *
* @param exportRootFolder
* the root folder to export in.
* @param saveId
@@ -561,7 +563,7 @@ private static final Model sanitizeAndCopy( final Model model )
final Spot s2 = spots.get( j );
final double r2 = s2.getFeature( Spot.RADIUS ).doubleValue();
final double d = Math.sqrt( s1.squareDistanceTo( s2 ) );
-
+
if ( fudgeFactor * r1 > ( d + r2 ) || fudgeFactor * r2 > ( d + r1 ) )
{
// They overlap too much. We must fix this.
@@ -611,7 +613,7 @@ private static final Model sanitizeAndCopy( final Model model )
else
sources.add( trackModel.getEdgeSource( edge ) );
}
-
+
model.beginUpdate();
try
{
diff --git a/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java b/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java
index 56b634c41..b58f4b07c 100644
--- a/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java
+++ b/src/main/java/fiji/plugin/trackmate/action/CTCExporterAction.java
@@ -32,10 +32,8 @@
import org.scijava.plugin.Plugin;
-import fiji.plugin.trackmate.SelectionModel;
-import fiji.plugin.trackmate.TrackMate;
import fiji.plugin.trackmate.action.CTCExporter.ExportType;
-import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings;
+import fiji.plugin.trackmate.gui.GuiModel;
import ij.ImagePlus;
import ij.gui.GenericDialog;
@@ -53,11 +51,11 @@ public class CTCExporterAction extends AbstractTMAction
"int), used
- * later for saving, retrieving and loading. Interfering with this value will
- * predictively cause undesired behavior.
- *
- * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> 2010, 2013
+ * They are mainly a 3D {@link RealLocalizable}, that store the object position
+ * in physical coordinates (um, mm, etc). 2D detections are treated by setting
+ * the Z coordinate to 0. Time is treated separately, as a feature.
*
+ * @author Jean-Yves Tinevez
*/
-public class Spot extends AbstractEuclideanSpace implements RealLocalizable, Comparable< Spot >
+public interface Spot extends RealLocalizable, RealPositionable, RealInterval, Comparable< Spot >, EuclideanSpace
{
/*
@@ -64,196 +69,70 @@ public class Spot extends AbstractEuclideanSpace implements RealLocalizable, Com
public static AtomicInteger IDcounter = new AtomicInteger( -1 );
- /** Store the individual features, and their values. */
- private final ConcurrentHashMap< String, Double > features = new ConcurrentHashMap<>();
-
- /** A user-supplied name for this spot. */
- private String name;
-
- /** This spot ID. */
- private final int ID;
-
- /**
- * The polygon that represents the 2D roi around the spot. Can be
- * null if the detector that created this spot does not support
- * ROIs or for 3D images.
- */
- private SpotRoi roi;
-
/*
- * CONSTRUCTORS
+ * PUBLIC METHODS
*/
- /**
- * Creates a new spot.
- *
- * @param x
- * the spot X coordinates, in image units.
- * @param y
- * the spot Y coordinates, in image units.
- * @param z
- * the spot Z coordinates, in image units.
- * @param radius
- * the spot radius, in image units.
- * @param quality
- * the spot quality.
- * @param name
- * the spot name.
- */
- public Spot( final double x, final double y, final double z, final double radius, final double quality, final String name )
- {
- super( 3 );
- this.ID = IDcounter.incrementAndGet();
- putFeature( POSITION_X, Double.valueOf( x ) );
- putFeature( POSITION_Y, Double.valueOf( y ) );
- putFeature( POSITION_Z, Double.valueOf( z ) );
- putFeature( RADIUS, Double.valueOf( radius ) );
- putFeature( QUALITY, Double.valueOf( quality ) );
- if ( null == name )
- {
- this.name = "ID" + ID;
- }
- else
- {
- this.name = name;
- }
- }
+ public void accept( SpotVisitor v );
- /**
- * Creates a new spot, and gives it a default name.
- *
- * @param x
- * the spot X coordinates, in image units.
- * @param y
- * the spot Y coordinates, in image units.
- * @param z
- * the spot Z coordinates, in image units.
- * @param radius
- * the spot radius, in image units.
- * @param quality
- * the spot quality.
- */
- public Spot( final double x, final double y, final double z, final double radius, final double quality )
+ @Override
+ public default int compareTo( final Spot o )
{
- this( x, y, z, radius, quality, null );
+ return ID() - o.ID();
}
/**
- * Creates a new spot, taking its 3D coordinates from a
- * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3
- * dimensions, and must return coordinates in image units.
- *
- * @param location
- * the {@link RealLocalizable} that contains the spot locatiob.
- * @param radius
- * the spot radius, in image units.
- * @param quality
- * the spot quality.
- * @param name
- * the spot name.
+ * Returns a copy of this spot. The class and all fields will be identical,
+ * except for the {@link #ID()}.
+ *
+ * @return a new spot.
*/
- public Spot( final RealLocalizable location, final double radius, final double quality, final String name )
- {
- this( location.getDoublePosition( 0 ), location.getDoublePosition( 1 ), location.getDoublePosition( 2 ), radius, quality, name );
- }
+ public Spot copy();
/**
- * Creates a new spot, taking its 3D coordinates from a
- * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3
- * dimensions, and must return coordinates in image units. The spot will get
- * a default name.
- *
- * @param location
- * the {@link RealLocalizable} that contains the spot locatiob.
- * @param radius
- * the spot radius, in image units.
- * @param quality
- * the spot quality.
+ * Scales the size of this spot by the specified ratio.
+ *
+ * @param alpha
+ * the scale.
*/
- public Spot( final RealLocalizable location, final double radius, final double quality )
- {
- this( location, radius, quality, null );
- }
+ public void scale( double alpha );
/**
- * Creates a new spot, taking its location, its radius, its quality value
- * and its name from the specified spot.
- *
- * @param spot
- * the spot to read from.
+ * Returns an iterable that will iterate over all the pixels contained in
+ * this spot.
+ *
+ * @param ra
+ * the {@link RandomAccessible} to iterate over. It's the caller
+ * responsibility to ensure that the {@link RandomAccessible} can
+ * return values over all the pixels in this spot.
+ * @param calibration
+ * the pixel size array, use to map pixel integer coordinates to
+ * the spot physical coordinates.
+ * @param null
* if it has not been set.
*/
- public Double getFeature( final String feature )
- {
- return features.get( feature );
- }
+ public Double getFeature( final String feature );
/**
* Stores the specified feature value for this spot.
@@ -363,28 +225,36 @@ public Double getFeature( final String feature )
* the value to store, as a {@link Double}. Using
* null will have unpredicted outcomes.
*/
- public void putFeature( final String feature, final Double value )
- {
- features.put( feature, value );
- }
+ public void putFeature( final String feature, final Double value );
/**
- * Copy the listed features of the source spot to this spot.
- *
+ * Copy some of the features values of the specified spot to this spot.
+ *
* @param src
- * the source spot.
+ * the spot to copy feature values from.
* @param features
- * the features to copy.
+ * the collection of feature keys to copy.
*/
- public void copyFeatures( final Spot src, final Map< String, Double > features )
+ public default void copyFeaturesFrom( final Spot src, final Collection< String > features )
{
if ( null == features || features.isEmpty() )
return;
- for ( final String feat : features.keySet() )
+ for ( final String feat : features )
putFeature( feat, src.getFeature( feat ) );
}
+ /**
+ * Copy all the features value from the specified spot to this spot.
+ *
+ * @param src
+ * the spot to copy feature values from.
+ */
+ public default void copyFeaturesFrom( final Spot src )
+ {
+ copyFeaturesFrom( src, src.getFeatures().keySet() );
+ }
+
/**
* Returns the difference of the feature value for this spot with the one of
* the specified spot. By construction, this operation is anti-symmetric (
@@ -399,9 +269,9 @@ public void copyFeatures( final Spot src, final Map< String, Double > features )
* the name of the feature to use for calculation.
* @return the difference in feature value.
*/
- public double diffTo( final Spot s, final String feature )
+ public default double diffTo( final Spot s, final String feature )
{
- final double f1 = features.get( feature ).doubleValue();
+ final double f1 = getFeature( feature ).doubleValue();
final double f2 = s.getFeature( feature ).doubleValue();
return f1 - f2;
}
@@ -427,9 +297,9 @@ public double diffTo( final Spot s, final String feature )
* the name of the feature to use for calculation.
* @return the absolute normalized difference feature value.
*/
- public double normalizeDiffTo( final Spot s, final String feature )
+ public default double normalizeDiffTo( final Spot s, final String feature )
{
- final double a = features.get( feature ).doubleValue();
+ final double a = getFeature( feature ).doubleValue();
final double b = s.getFeature( feature ).doubleValue();
if ( a == -b )
return 0d;
@@ -444,7 +314,7 @@ public double normalizeDiffTo( final Spot s, final String feature )
* the spot to compute the square distance to.
* @return the square distance as a double.
*/
- public double squareDistanceTo( final RealLocalizable s )
+ public default double squareDistanceTo( final RealLocalizable s )
{
double sumSquared = 0d;
for ( int d = 0; d < 3; d++ )
@@ -488,97 +358,232 @@ public double squareDistanceTo( final RealLocalizable s )
public final static String[] POSITION_FEATURES = new String[] { POSITION_X, POSITION_Y, POSITION_Z };
/**
- * The 7 privileged spot features that must be set by a spot detector:
+ * The 8 privileged spot features that must be set by a spot detector:
* {@link #QUALITY}, {@link #POSITION_X}, {@link #POSITION_Y},
- * {@link #POSITION_Z}, {@link #POSITION_Z}, {@link #RADIUS}, {@link #FRAME}
- * .
+ * {@link #POSITION_Z}, {@link #POSITION_Z}, {@link #RADIUS},
+ * {@link #FRAME}, {@link SpotCollection#VISIBILITY}.
*/
- public final static Collection< String > FEATURES = new ArrayList<>( 7 );
-
- /** The 7 privileged spot feature names. */
- public final static Map< String, String > FEATURE_NAMES = new HashMap<>( 7 );
-
- /** The 7 privileged spot feature short names. */
- public final static Map< String, String > FEATURE_SHORT_NAMES = new HashMap<>( 7 );
-
- /** The 7 privileged spot feature dimensions. */
- public final static Map< String, Dimension > FEATURE_DIMENSIONS = new HashMap<>( 7 );
-
- /** The 7 privileged spot feature isInt flags. */
- public final static Map< String, Boolean > IS_INT = new HashMap<>( 7 );
-
- static
- {
- FEATURES.add( QUALITY );
- FEATURES.add( POSITION_X );
- FEATURES.add( POSITION_Y );
- FEATURES.add( POSITION_Z );
- FEATURES.add( POSITION_T );
- FEATURES.add( FRAME );
- FEATURES.add( RADIUS );
- FEATURES.add( SpotCollection.VISIBILITY );
-
- FEATURE_NAMES.put( POSITION_X, "X" );
- FEATURE_NAMES.put( POSITION_Y, "Y" );
- FEATURE_NAMES.put( POSITION_Z, "Z" );
- FEATURE_NAMES.put( POSITION_T, "T" );
- FEATURE_NAMES.put( FRAME, "Frame" );
- FEATURE_NAMES.put( RADIUS, "Radius" );
- FEATURE_NAMES.put( QUALITY, "Quality" );
- FEATURE_NAMES.put( VISIBILITY, "Visibility" );
-
- FEATURE_SHORT_NAMES.put( POSITION_X, "X" );
- FEATURE_SHORT_NAMES.put( POSITION_Y, "Y" );
- FEATURE_SHORT_NAMES.put( POSITION_Z, "Z" );
- FEATURE_SHORT_NAMES.put( POSITION_T, "T" );
- FEATURE_SHORT_NAMES.put( FRAME, "Frame" );
- FEATURE_SHORT_NAMES.put( RADIUS, "R" );
- FEATURE_SHORT_NAMES.put( QUALITY, "Quality" );
- FEATURE_SHORT_NAMES.put( VISIBILITY, "Visibility" );
-
- FEATURE_DIMENSIONS.put( POSITION_X, Dimension.POSITION );
- FEATURE_DIMENSIONS.put( POSITION_Y, Dimension.POSITION );
- FEATURE_DIMENSIONS.put( POSITION_Z, Dimension.POSITION );
- FEATURE_DIMENSIONS.put( POSITION_T, Dimension.TIME );
- FEATURE_DIMENSIONS.put( FRAME, Dimension.NONE );
- FEATURE_DIMENSIONS.put( RADIUS, Dimension.LENGTH );
- FEATURE_DIMENSIONS.put( QUALITY, Dimension.QUALITY );
- FEATURE_DIMENSIONS.put( VISIBILITY, Dimension.NONE );
-
- IS_INT.put( POSITION_X, Boolean.FALSE );
- IS_INT.put( POSITION_Y, Boolean.FALSE );
- IS_INT.put( POSITION_Z, Boolean.FALSE );
- IS_INT.put( POSITION_T, Boolean.FALSE );
- IS_INT.put( FRAME, Boolean.TRUE );
- IS_INT.put( RADIUS, Boolean.FALSE );
- IS_INT.put( QUALITY, Boolean.FALSE );
- IS_INT.put( VISIBILITY, Boolean.TRUE );
+ public final static Collection< String > FEATURES = Arrays.asList( QUALITY,
+ POSITION_X, POSITION_Y, POSITION_Z, POSITION_T, FRAME, RADIUS, SpotCollection.VISIBILITY );
+
+ /** The 8 privileged spot feature names. */
+ public final static Map< String, String > FEATURE_NAMES = ImmutableMap.of(
+ POSITION_X, "X",
+ POSITION_Y, "Y",
+ POSITION_Z, "Z",
+ POSITION_T, "T",
+ FRAME, "Frame",
+ RADIUS, "Radius",
+ QUALITY, "Quality",
+ VISIBILITY, "Visibility" );
+
+ /** The 8 privileged spot feature short names. */
+ public final static Map< String, String > FEATURE_SHORT_NAMES = ImmutableMap.of(
+ POSITION_X, "X",
+ POSITION_Y, "Y",
+ POSITION_Z, "Z",
+ POSITION_T, "T",
+ FRAME, "Frame",
+ RADIUS, "R",
+ QUALITY, "Quality",
+ VISIBILITY, "Visibility" );
+
+ /** The 8 privileged spot feature dimensions. */
+ public final static Map< String, Dimension > FEATURE_DIMENSIONS = ImmutableMap.of(
+ POSITION_X, Dimension.POSITION,
+ POSITION_Y, Dimension.POSITION,
+ POSITION_Z, Dimension.POSITION,
+ POSITION_T, Dimension.TIME,
+ FRAME, Dimension.NONE,
+ RADIUS, Dimension.LENGTH,
+ QUALITY, Dimension.QUALITY,
+ VISIBILITY, Dimension.NONE );
+
+ /** The 8 privileged spot feature isInt flags. */
+ public final static Map< String, Boolean > IS_INT = ImmutableMap.of(
+ POSITION_X, Boolean.FALSE,
+ POSITION_Y, Boolean.FALSE,
+ POSITION_Z, Boolean.FALSE,
+ POSITION_T, Boolean.FALSE,
+ FRAME, Boolean.TRUE,
+ RADIUS, Boolean.FALSE,
+ QUALITY, Boolean.FALSE,
+ VISIBILITY, Boolean.TRUE );
+
+ /*
+ * REALPOSITIONABLE, REAlLOCALIZABLE
+ */
+
+ @Override
+ default int numDimensions()
+ {
+ return 3;
+ }
+
+ @Override
+ public default void move( final float distance, final int d )
+ {
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance );
+ }
+
+ @Override
+ public default void move( final double distance, final int d )
+ {
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance );
}
@Override
- public void localize( final float[] position )
+ public default void move( final RealLocalizable distance )
{
- assert ( position.length >= n );
- for ( int d = 0; d < n; ++d )
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance.getDoublePosition( d ) );
+ }
+
+ @Override
+ public default void move( final float[] distance )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance[ d ] );
+ }
+
+ @Override
+ public default void move( final double[] distance )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] ) + distance[ d ] );
+ }
+
+ @Override
+ public default void setPosition( final RealLocalizable position )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], position.getDoublePosition( d ) );
+ }
+
+ @Override
+ public default void setPosition( final float[] position )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], ( double ) position[ d ] );
+ }
+
+ @Override
+ public default void setPosition( final double[] position )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], position[ d ] );
+ }
+
+ @Override
+ public default void setPosition( final float position, final int d )
+ {
+ putFeature( POSITION_FEATURES[ d ], ( double ) position );
+ }
+
+ @Override
+ public default void setPosition( final double position, final int d )
+ {
+ putFeature( POSITION_FEATURES[ d ], position );
+ }
+
+ @Override
+ public default void fwd( final int d )
+ {
+ move( 1., d );
+ }
+
+ @Override
+ public default void bck( final int d )
+ {
+ move( -1., d );
+ }
+
+ @Override
+ public default void move( final int distance, final int d )
+ {
+ move( ( double ) distance, d );
+ }
+
+ @Override
+ public default void move( final long distance, final int d )
+ {
+ move( ( double ) distance, d );
+ }
+
+ @Override
+ public default void move( final Localizable distance )
+ {
+ move( ( RealLocalizable ) distance );
+ }
+
+ @Override
+ public default void move( final int[] distance )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] + distance[ d ] ) );
+ }
+
+ @Override
+ public default void move( final long[] distance )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], getFeature( POSITION_FEATURES[ d ] + distance[ d ] ) );
+ }
+
+ @Override
+ public default void setPosition( final Localizable position )
+ {
+ setPosition( ( RealLocalizable ) position );
+ }
+
+ @Override
+ public default void setPosition( final int[] position )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], ( double ) position[ d ] );
+ }
+
+ @Override
+ public default void setPosition( final long[] position )
+ {
+ for ( int d = 0; d < 3; d++ )
+ putFeature( POSITION_FEATURES[ d ], ( double ) position[ d ] );
+ }
+
+ @Override
+ public default void setPosition( final int position, final int d )
+ {
+ putFeature( POSITION_FEATURES[ d ], ( double ) position );
+ }
+
+ @Override
+ public default void setPosition( final long position, final int d )
+ {
+ putFeature( POSITION_FEATURES[ d ], ( double ) position );
+ }
+
+ @Override
+ public default void localize( final float[] position )
+ {
+ for ( int d = 0; d < 3; ++d )
position[ d ] = getFloatPosition( d );
}
@Override
- public void localize( final double[] position )
+ public default void localize( final double[] position )
{
- assert ( position.length >= n );
- for ( int d = 0; d < n; ++d )
+ for ( int d = 0; d < 3; ++d )
position[ d ] = getDoublePosition( d );
}
@Override
- public float getFloatPosition( final int d )
+ public default float getFloatPosition( final int d )
{
return ( float ) getDoublePosition( d );
}
@Override
- public double getDoublePosition( final int d )
+ public default double getDoublePosition( final int d )
{
return getFeature( POSITION_FEATURES[ d ] );
}
@@ -596,7 +601,7 @@ public double getDoublePosition( final int d )
* feature.
* @return a new {@link Comparator}.
*/
- public final static Comparator< Spot > featureComparator( final String feature )
+ public static Comparator< Spot > featureComparator( final String feature )
{
final Comparator< Spot > comparator = new Comparator< Spot >()
{
@@ -635,4 +640,23 @@ public int compare( final Spot o1, final Spot o2 )
return comparator.compare( o1.getName(), o2.getName() );
}
};
+
+ public static interface SpotVisitor
+ {
+
+ default void visit( final SpotBase spot )
+ {
+ throw new UnsupportedOperationException( "SpotBase not supported." );
+ }
+
+ default void visit( final SpotRoi spot )
+ {
+ throw new UnsupportedOperationException( "SpotRoi not supported." );
+ }
+
+ default void visit( final SpotMesh spot )
+ {
+ throw new UnsupportedOperationException( "SpotMesh not supported." );
+ }
+ }
}
diff --git a/src/main/java/fiji/plugin/trackmate/SpotBase.java b/src/main/java/fiji/plugin/trackmate/SpotBase.java
new file mode 100644
index 000000000..c92adceaa
--- /dev/null
+++ b/src/main/java/fiji/plugin/trackmate/SpotBase.java
@@ -0,0 +1,336 @@
+/*-
+ * #%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
+ * int), used
+ * later for saving, retrieving and loading. Interfering with this value will
+ * predictively cause undesired behavior.
+ *
+ * @author Jean-Yves Tinevez
+ *
+ */
+public class SpotBase extends AbstractEuclideanSpace implements Spot
+{
+
+ /*
+ * FIELDS
+ */
+
+ public static AtomicInteger IDcounter = new AtomicInteger( -1 );
+
+ /** Store the individual features, and their values. */
+ private final ConcurrentHashMap< String, Double > features = new ConcurrentHashMap<>();
+
+ /** A user-supplied name for this spot. */
+ private String name;
+
+ /** This spot ID. */
+ private final int ID;
+
+ /*
+ * CONSTRUCTORS
+ */
+
+ /**
+ * Creates a new spot.
+ *
+ * @param x
+ * the spot X coordinates, in image units.
+ * @param y
+ * the spot Y coordinates, in image units.
+ * @param z
+ * the spot Z coordinates, in image units.
+ * @param radius
+ * the spot radius, in image units.
+ * @param quality
+ * the spot quality.
+ * @param name
+ * the spot name.
+ */
+ public SpotBase( final double x, final double y, final double z, final double radius, final double quality, final String name )
+ {
+ super( 3 );
+ this.ID = IDcounter.incrementAndGet();
+ putFeature( POSITION_X, Double.valueOf( x ) );
+ putFeature( POSITION_Y, Double.valueOf( y ) );
+ putFeature( POSITION_Z, Double.valueOf( z ) );
+ putFeature( RADIUS, Double.valueOf( radius ) );
+ putFeature( QUALITY, Double.valueOf( quality ) );
+ if ( null == name )
+ {
+ this.name = "ID" + ID;
+ }
+ else
+ {
+ this.name = name;
+ }
+ }
+
+ /**
+ * Creates a new spot, and gives it a default name.
+ *
+ * @param x
+ * the spot X coordinates, in image units.
+ * @param y
+ * the spot Y coordinates, in image units.
+ * @param z
+ * the spot Z coordinates, in image units.
+ * @param radius
+ * the spot radius, in image units.
+ * @param quality
+ * the spot quality.
+ */
+ public SpotBase( final double x, final double y, final double z, final double radius, final double quality )
+ {
+ this( x, y, z, radius, quality, null );
+ }
+
+ /**
+ * Creates a new spot, taking its 3D coordinates from a
+ * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3
+ * dimensions, and must return coordinates in image units.
+ *
+ * @param location
+ * the {@link RealLocalizable} that contains the spot locatiob.
+ * @param radius
+ * the spot radius, in image units.
+ * @param quality
+ * the spot quality.
+ * @param name
+ * the spot name.
+ */
+ public SpotBase( final RealLocalizable location, final double radius, final double quality, final String name )
+ {
+ this( location.getDoublePosition( 0 ), location.getDoublePosition( 1 ), location.getDoublePosition( 2 ), radius, quality, name );
+ }
+
+ /**
+ * Creates a new spot, taking its 3D coordinates from a
+ * {@link RealLocalizable}. The {@link RealLocalizable} must have at least 3
+ * dimensions, and must return coordinates in image units. The spot will get
+ * a default name.
+ *
+ * @param location
+ * the {@link RealLocalizable} that contains the spot locatiob.
+ * @param radius
+ * the spot radius, in image units.
+ * @param quality
+ * the spot quality.
+ */
+ public SpotBase( final RealLocalizable location, final double radius, final double quality )
+ {
+ this( location, radius, quality, null );
+ }
+
+ /**
+ * Creates a new spot, taking its location, its radius, its quality value
+ * and its name from the specified spot.
+ *
+ * @param oldSpot
+ * the spot to read from.
+ */
+ public SpotBase( final Spot oldSpot )
+ {
+ this( oldSpot, oldSpot.getFeature( RADIUS ), oldSpot.getFeature( QUALITY ), oldSpot.getName() );
+ }
+
+ /**
+ * Blank constructor meant to be used when loading a spot collection from a
+ * file. Will mess with the {@link #IDcounter} field, so this
+ * constructor should not be used for normal spot creation.
+ *
+ * @param ID
+ * the spot ID to set
+ */
+ public SpotBase( final int ID )
+ {
+ super( 3 );
+ this.ID = ID;
+ synchronized ( IDcounter )
+ {
+ if ( IDcounter.get() < ID )
+ {
+ IDcounter.set( ID );
+ }
+ }
+ }
+
+ /*
+ * PUBLIC METHODS
+ */
+
+ @Override
+ public void accept( final SpotVisitor v )
+ {
+ v.visit( this );
+ }
+
+ @Override
+ public SpotBase copy()
+ {
+ final SpotBase o = new SpotBase( this );
+ o.copyFeaturesFrom( this );
+ return o;
+ }
+
+ @Override
+ public void scale( final double alpha )
+ {
+ final double radius = getFeature( Spot.RADIUS );
+ final double newRadius = radius * alpha;
+ putFeature( Spot.RADIUS, newRadius );
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return ID;
+ }
+
+ @Override
+ public boolean equals( final Object other )
+ {
+ if ( other == null )
+ return false;
+ if ( other == this )
+ return true;
+ if ( !( other instanceof SpotBase ) )
+ return false;
+ final SpotBase os = ( SpotBase ) other;
+ return os.ID == this.ID;
+ }
+
+ @Override
+ public String getName()
+ {
+ return this.name;
+ }
+
+ @Override
+ public void setName( final String name )
+ {
+ this.name = name;
+ }
+
+ @Override
+ public int ID()
+ {
+ return ID;
+ }
+
+ @Override
+ public String toString()
+ {
+ String str;
+ if ( null == name || name.equals( "" ) )
+ str = "ID" + ID;
+ else
+ str = name;
+ return str;
+ }
+
+ /*
+ * FEATURE RELATED METHODS
+ */
+
+ @Override
+ public Map< String, Double > getFeatures()
+ {
+ return features;
+ }
+
+ @Override
+ public Double getFeature( final String feature )
+ {
+ return features.get( feature );
+ }
+
+ @Override
+ public void putFeature( final String feature, final Double value )
+ {
+ features.put( feature, value );
+ }
+
+ @Override
+ public double realMin( final int d )
+ {
+ return getDoublePosition( d ) - getFeature( SpotBase.RADIUS );
+ }
+
+ @Override
+ public double realMax( final int d )
+ {
+ return getDoublePosition( d ) + getFeature( SpotBase.RADIUS );
+ }
+
+ @Override
+ public < T extends RealType< T > > IterableInterval< T > iterable( final RandomAccessible< T > ra, final double[] calibration )
+ {
+ final double r = features.get( Spot.RADIUS ).doubleValue();
+ if ( r / calibration[ 0 ] <= 1. && r / calibration[ 2 ] <= 1. )
+ return makeSinglePixelIterable( this, ra, calibration );
+
+ return new SpotNeighborhood<>( this, ra, calibration );
+ }
+
+ private static < T > IterableInterval< T > makeSinglePixelIterable( final RealLocalizable center, final RandomAccessible< T > img, final double[] calibration )
+ {
+ final long[] min = new long[ img.numDimensions() ];
+ final long[] max = new long[ img.numDimensions() ];
+ for ( int d = 0; d < min.length; d++ )
+ {
+ final long cx = Math.round( center.getDoublePosition( d ) / calibration[ d ] );
+ min[ d ] = cx;
+ max[ d ] = cx + 1;
+ }
+
+ final Interval interval = new FinalInterval( min, max );
+ return Views.interval( img, interval );
+ }
+}
diff --git a/src/main/java/fiji/plugin/trackmate/SpotCollection.java b/src/main/java/fiji/plugin/trackmate/SpotCollection.java
index 98d9c64f2..c94d872cc 100644
--- a/src/main/java/fiji/plugin/trackmate/SpotCollection.java
+++ b/src/main/java/fiji/plugin/trackmate/SpotCollection.java
@@ -35,6 +35,7 @@
import fiji.plugin.trackmate.features.FeatureFilter;
import fiji.plugin.trackmate.util.Threads;
+import net.imglib2.RealLocalizable;
import net.imglib2.algorithm.MultiThreaded;
/**
@@ -379,7 +380,7 @@ public final Spot getClosestSpot( final Spot location, final int frame, final bo
}
/**
- * Returns the {@link Spot} at the given location (encoded as a Spot),
+ * Returns the {@link Spot} at the given location (in world coordinates),
* contained in the frame frame. A spot is returned only
* if there exists a spot such that the given location is within the spot
* radius. Otherwise null is returned.
@@ -395,7 +396,7 @@ public final Spot getClosestSpot( final Spot location, final int frame, final bo
* radius, member of this collection, or null is such a
* spots cannot be found.
*/
- public final Spot getSpotAt( final Spot location, final int frame, final boolean visibleSpotsOnly )
+ public final Spot getSpotAt( final RealLocalizable location, final int frame, final boolean visibleSpotsOnly )
{
final Set< Spot > spots = content.get( frame );
if ( null == spots || spots.isEmpty() )
diff --git a/src/main/java/fiji/plugin/trackmate/SpotMesh.java b/src/main/java/fiji/plugin/trackmate/SpotMesh.java
new file mode 100644
index 000000000..b2f2de0fd
--- /dev/null
+++ b/src/main/java/fiji/plugin/trackmate/SpotMesh.java
@@ -0,0 +1,385 @@
+/*-
+ * #%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
+ * null if the mesh does not intersect
+ * with the specified XY plane. The slice XY coordinates are
+ * centered so (0,0) corresponds to the mesh center.
+ */
+ public Slice getZSlice( final int zSlice, final double xyScale, final double zScale )
+ {
+ if ( sliceMap == null )
+ sliceMap = buildSliceMap( mesh, boundingBox, this, xyScale, zScale );
+
+ return sliceMap.get( Integer.valueOf( zSlice ) );
+ }
+
+ /**
+ * Invalidates the Z-slices cache. This will force its recomputation. To be
+ * called after the spot has changed size or Z position.
+ */
+ public void resetZSliceCache()
+ {
+ sliceMap = null;
+ }
+
+ /**
+ * Returns the radius of the equivalent sphere with the same volume that of
+ * the specified mesh.
+ *
+ * @param mesh
+ * the mesh.
+ * @return the radius in physical units.
+ */
+ public static final double radius( final Mesh mesh )
+ {
+ return Math.pow( 3. * MeshStats.volume( mesh ) / ( 4 * Math.PI ), 1. / 3. );
+ }
+
+ public double radius()
+ {
+ return radius( mesh );
+ }
+
+ /**
+ * Returns the volume of this mesh.
+ *
+ * @return the volume in physical units.
+ */
+ public double volume()
+ {
+ return MeshStats.volume( mesh );
+ }
+
+ @Override
+ public void scale( final double alpha )
+ {
+ super.scale( alpha );
+ final net.imglib2.mesh.Vertices vertices = mesh.vertices();
+ final long nVertices = vertices.size();
+ final float fAlpha = ( float ) alpha;
+ for ( int v = 0; v < nVertices; v++ )
+ {
+ // Exploit the fact that the mesh is centered on (0,0,0).
+ final float xa = fAlpha * vertices.xf( v );
+ final float ya = fAlpha * vertices.yf( v );
+ final float za = fAlpha * vertices.zf( v );
+ vertices.setPositionf( v, xa, ya, za );
+ }
+ this.boundingBox = Meshes.boundingBox( mesh );
+ resetZSliceCache();
+ }
+
+ @Override
+ public SpotMesh copy()
+ {
+ final BufferMesh meshCopy = new BufferMesh( mesh.vertices().size(), mesh.triangles().size() );
+ Meshes.copy( this.mesh, meshCopy );
+ return new SpotMesh( meshCopy, getFeature( Spot.QUALITY ), getName() );
+ }
+
+ @Override
+ public String toString()
+ {
+ final StringBuilder str = new StringBuilder( super.toString() );
+
+ str.append( "\nBounding-box" );
+ str.append( String.format( "\n%5s: %7.2f -> %7.2f", "X", boundingBox.realMin( 0 ), boundingBox.realMax( 0 ) ) );
+ str.append( String.format( "\n%5s: %7.2f -> %7.2f", "Y", boundingBox.realMin( 1 ), boundingBox.realMax( 1 ) ) );
+ str.append( String.format( "\n%5s: %7.2f -> %7.2f", "Z", boundingBox.realMin( 2 ), boundingBox.realMax( 2 ) ) );
+
+ final net.imglib2.mesh.Vertices vertices = mesh.vertices();
+ final long nVertices = vertices.size();
+ str.append( "\nV (" + nVertices + "):" );
+ for ( long i = 0; i < nVertices; i++ )
+ str.append( String.format( "\n%5d: %7.2f %7.2f %7.2f",
+ i, vertices.x( i ), vertices.y( i ), vertices.z( i ) ) );
+
+ final net.imglib2.mesh.Triangles triangles = mesh.triangles();
+ final long nTriangles = triangles.size();
+ str.append( "\nF (" + nTriangles + "):" );
+ for ( long i = 0; i < nTriangles; i++ )
+ str.append( String.format( "\n%5d: %5d %5d %5d",
+ i, triangles.vertex0( i ), triangles.vertex1( i ), triangles.vertex2( i ) ) );
+
+ return str.toString();
+ }
+
+ /**
+ * Computes the intersections of the specified mesh with the multiple
+ * Z-slice at integer coordinates corresponding to 1-pixel spacing in
+ * Z. This is why we need to have the calibration array. The
+ * slices are centered on (0,0) the mesh center.
+ *
+ * @param mesh
+ * the mesh to reslice, centered on (0,0,0).
+ * @param boundingBox
+ * its bounding box, also centered on (0,0,0).
+ * @param center
+ * the mesh center true position. Needed to reposition it in Z.
+ * @param calibration
+ * the pixel size array, needed to compute the 1-pixel spacing.
+ * @return a map from slice position (integer, pixel coordinates) to slices.
+ */
+ private static final Map< Integer, Slice > buildSliceMap(
+ final Mesh mesh,
+ final RealInterval boundingBox,
+ final RealLocalizable center,
+ final double xyScale,
+ final double zScale )
+ {
+ /*
+ * Let's try to have everything relative to (0,0,0), so that we do not
+ * have to recompute the Z slices when the mesh is moved in X and Y.
+ */
+
+ /*
+ * Compute the Z integers, in pixel coordinates, of the mesh
+ * intersection. These coordinates are absolute value (relative to mesh
+ * center).
+ */
+ final double zc = center.getDoublePosition( 2 );
+ final int minZ = ( int ) Math.ceil( ( boundingBox.realMin( 2 ) + zc ) / zScale );
+ final int maxZ = ( int ) Math.floor( ( boundingBox.realMax( 2 ) + zc ) / zScale );
+ final int[] zSlices = new int[ maxZ - minZ + 1 ];
+ for ( int i = 0; i < zSlices.length; i++ )
+ zSlices[ i ] = ( minZ + i );// pixel coords, absolute value
+
+ /*
+ * Compute equivalent Z positions in physical units, relative to
+ * (0,0,0), of these intersections.
+ */
+ final double[] zPos = new double[ zSlices.length ];
+ for ( int i = 0; i < zPos.length; i++ )
+ zPos[ i ] = zSlices[ i ] * zScale - zc;
+
+ // Compute the slices. They will be centered on (0,0) in XY.
+ final List< Slice > slices = ZSlicer.slices(
+ mesh,
+ zPos,
+ zScale );
+
+ // Simplify below 1/4th of a pixel.
+ final double epsilon = xyScale * 0.25;
+ final List< Slice > simplifiedSlices = slices.stream()
+ .map( s -> RamerDouglasPeucker.simplify( s, epsilon ) )
+ .collect( Collectors.toList() );
+
+ // Store in a map of Z slice -> slice.
+ final Map< Integer, Slice > sliceMap = new HashMap<>();
+ for ( int i = 0; i < zSlices.length; i++ )
+ sliceMap.put( Integer.valueOf( zSlices[ i ] ), simplifiedSlices.get( i ) );
+
+ return sliceMap;
+ }
+}
diff --git a/src/main/java/fiji/plugin/trackmate/SpotRoi.java b/src/main/java/fiji/plugin/trackmate/SpotRoi.java
index 3f081e8c1..ecfff1d45 100644
--- a/src/main/java/fiji/plugin/trackmate/SpotRoi.java
+++ b/src/main/java/fiji/plugin/trackmate/SpotRoi.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
* int array containing the X pixel coordinates
- * to which to paint this polygon.
+ * Sets the Y coordinates of the ith vertex of the polygon, relative to
+ * the spot center, in physical coordinates. This method is meant to be
+ * used for undo / redo operations, and should not be used otherwise.
*
- * @param calibration
- * the pixel size in X, to convert physical coordinates to pixel
- * coordinates.
- * @param xcorner
- * the top-left X corner of the view in the image to paint.
- * @param spotXCenter
- * the X coordinate of the spot center.
- * @param magnification
- * the magnification of the view.
- * @return a new int array.
+ * @param i
+ * the index of the vertex.
+ * @param y
+ * the vertex Y position.
*/
- public double[] toPolygonX( final double calibration, final double xcorner, final double spotXCenter, final double magnification )
+ public void setYr( final int i, final double y )
{
- final double[] xp = new double[ x.length ];
- for ( int i = 0; i < xp.length; i++ )
+ this.y[ i ] = y;
+ }
+
+ public int nPoints()
+ {
+ return x.length;
+ }
+
+ @Override
+ public double realMin( final int d )
+ {
+ if ( d > 1 )
+ return 0;
+ final double[] arr = ( d == 0 ) ? x : y;
+ return getDoublePosition( d ) + Util.min( arr );
+ }
+
+ @Override
+ public double realMax( final int d )
+ {
+ if ( d > 1 )
+ return 0;
+ final double[] arr = ( d == 0 ) ? x : y;
+ return getDoublePosition( d ) + Util.max( arr );
+ }
+
+ /**
+ * Convenience method that returns the X and Y coordinates of the polygon on
+ * this spot, possibly shifted and scale by a specified amount. Such that:
+ *
+ *
+ * xout = x * sx + cx
+ * yout = y * sy + cy
+ *
+ *
+ * @param cx
+ * the shift in X to apply after scaling coordinates.
+ * @param cy
+ * the shift in Y to apply after scaling coordinates.
+ * @param sx
+ * the scale to apply in X.
+ * @param sy
+ * the scale to apply in Y.
+ * @param xout
+ * a list in which to write resulting X coordinates. Reset by
+ * this call.
+ * @param yout
+ * a list in which to write resulting Y coordinates. Reset by
+ * this call.
+ */
+ public void toArray( final double cx, final double cy, final double sx, final double sy, final TDoubleArrayList xout, final TDoubleArrayList yout )
+ {
+ xout.resetQuick();
+ yout.resetQuick();
+ for ( int i = 0; i < x.length; i++ )
{
- final double xc = ( spotXCenter + x[ i ] ) / calibration;
- xp[ i ] = ( xc - xcorner ) * magnification;
+ xout.add( x( i ) + sx + cx );
+ yout.add( y( i ) + sx + cy );
}
- return xp;
}
/**
- * Returns a new int array containing the Y pixel coordinates
- * to which to paint this polygon.
+ * Convenience method that returns the X and Y coordinates of the polygon on
+ * this spot, possibly shifted and scale by a specified amount. Such that:
+ *
+ *
+ * xout = x * sx + cx
+ * yout = y * sy + cy
+ *
*
- * @param calibration
- * the pixel size in Y, to convert physical coordinates to pixel
- * coordinates.
- * @param ycorner
- * the top-left Y corner of the view in the image to paint.
- * @param spotYCenter
- * the spot Y center in physical units.
- * @param magnification
- * the magnification of the view.
- * @return a new int array.
+ * @param cx
+ * the shift in X to apply after scaling coordinates.
+ * @param cy
+ * the shift in Y to apply after scaling coordinates.
+ * @param sx
+ * the scale to apply in X.
+ * @param sy
+ * the scale to apply in Y.
+ * @return a new 2D double array, with the array of X values as the first
+ * element, and the array of Y values as a second element.
*/
- public double[] toPolygonY( final double calibration, final double ycorner, final double spotYCenter, final double magnification )
+ public double[][] toArray( final double cx, final double cy, final double sx, final double sy )
{
- final double[] yp = new double[ y.length ];
- for ( int i = 0; i < yp.length; i++ )
+ final double[] xout = new double[ x.length ];
+ final double[] yout = new double[ x.length ];
+ for ( int i = 0; i < x.length; i++ )
{
- final double yc = ( spotYCenter + y[ i ] ) / calibration;
- yp[ i ] = ( yc - ycorner ) * magnification;
+ xout[ i ] = x( i ) * sx + cx;
+ yout[ i ] = y( i ) * sy + cy;
}
- return yp;
+ return new double[][] { xout, yout };
}
- public < T > IterableInterval< T > sample( final Spot spot, final ImgPlus< T > img )
+ @Override
+ public < T extends RealType< T > > IterableInterval< T > iterable( final RandomAccessible< T > ra, final double[] calibration )
{
- return sample( spot.getDoublePosition( 0 ), spot.getDoublePosition( 1 ), img, img.averageScale( 0 ), img.averageScale( 1 ) );
+ return new SpotRoiIterable<>( this, ra, calibration );
}
- public < T > IterableInterval< T > sample( final double spotXCenter, final double spotYCenter, final RandomAccessibleInterval< T > img, final double xScale, final double yScale )
+ private static double radius( final double[] x, final double[] y )
{
- final double[] xp = toPolygonX( xScale, 0, spotXCenter, 1. );
- final double[] yp = toPolygonY( yScale, 0, spotYCenter, 1. );
- final WritablePolygon2D polygon = GeomMasks.closedPolygon2D( xp, yp );
- final IterableRegion< BoolType > region = Masks.toIterableRegion( polygon );
- return Regions.sample( region, Views.extendMirrorDouble( Views.dropSingletonDimensions( img ) ) );
+ return Math.sqrt( area( x, y ) / Math.PI );
}
- public double radius()
+ private static double area( final double[] x, final double[] y )
{
- return Math.sqrt( area() / Math.PI );
+ return Math.abs( signedArea( x, y ) );
}
public double area()
{
- return Math.abs( signedArea( x, y ) );
+ return area( x, y );
}
+ @Override
public void scale( final double alpha )
{
+ super.scale( alpha );
for ( int i = 0; i < x.length; i++ )
{
final double x = this.x[ i ];
@@ -148,7 +309,7 @@ public void scale( final double alpha )
}
}
- public static Spot createSpot( final double[] x, final double[] y, final double quality )
+ public static SpotRoi createSpot( final double[] x, final double[] y, final double quality )
{
// Put polygon coordinates with respect to centroid.
final double[] centroid = centroid( x, y );
@@ -157,15 +318,10 @@ public static Spot createSpot( final double[] x, final double[] y, final double
final double[] xr = Arrays.stream( x ).map( x0 -> x0 - xc ).toArray();
final double[] yr = Arrays.stream( y ).map( y0 -> y0 - yc ).toArray();
- // Create roi.
- final SpotRoi roi = new SpotRoi( xr, yr );
-
// Create spot.
final double z = 0.;
- final double r = roi.radius();
- final Spot spot = new Spot( xc, yc, z, r, quality );
- spot.setRoi( roi );
- return spot;
+ final double r = radius( xr, yr );
+ return new SpotRoi( xc, yc, z, r, quality, null, xr, yr );
}
/*
@@ -178,16 +334,14 @@ private static final double[] centroid( final double[] x, final double[] y )
double ax = 0.0;
double ay = 0.0;
final int n = x.length;
- for ( int i = 0; i < n - 1; i++ )
+ int i;
+ int j;
+ for ( i = 0, j = n - 1; i < n; j = i++ )
{
- final double w = x[ i ] * y[ i + 1 ] - x[ i + 1 ] * y[ i ];
- ax += ( x[ i ] + x[ i + 1 ] ) * w;
- ay += ( y[ i ] + y[ i + 1 ] ) * w;
+ final double w = x[ j ] * y[ i ] - x[ i ] * y[ j ];
+ ax += ( x[ j ] + x[ i ] ) * w;
+ ay += ( y[ j ] + y[ i ] ) * w;
}
-
- final double w0 = x[ n - 1 ] * y[ 0 ] - x[ 0 ] * y[ n - 1 ];
- ax += ( x[ n - 1 ] + x[ 0 ] ) * w0;
- ay += ( y[ n - 1 ] + y[ 0 ] ) * w0;
return new double[] { ax / 6. / area, ay / 6. / area };
}
@@ -195,9 +349,261 @@ private static final double signedArea( final double[] x, final double[] y )
{
final int n = x.length;
double a = 0.0;
- for ( int i = 0; i < n - 1; i++ )
- a += x[ i ] * y[ i + 1 ] - x[ i + 1 ] * y[ i ];
+ int i;
+ int j;
+ for ( i = 0, j = n - 1; i < n; j = i++ )
+ a += x[ j ] * y[ i ] - x[ i ] * y[ j ];
+
+ return a / 2.;
+ }
+
+ /*
+ * ITERABLE and ITERATOR.
+ */
+
+ private static final class SpotRoiIterable< T extends RealType< T > > implements IterableInterval< T >
+ {
+
+ private final FinalInterval interval;
+
+ /** Polygon X coords in pixel units. */
+ private final double[] x;
+
+ /** Polygon Y coords in pixel units. */
+ private final double[] y;
+
+ private final RandomAccessible< T > ra;
+
+ public SpotRoiIterable( final SpotRoi roi, final RandomAccessible< T > ra, final double[] calibration )
+ {
+ this.ra = ra;
+ final double[][] xy = roi.toArray( 0., 0., 1 / calibration[ 0 ], 1 / calibration[ 1 ] );
+ this.x = xy[ 0 ];
+ this.y = xy[ 1 ];
+ final long minX = ( long ) Math.floor( Util.min( x ) );
+ final long maxX = ( long ) Math.ceil( Util.max( x ) );
+ final long minY = ( long ) Math.floor( Util.min( y ) );
+ final long maxY = ( long ) Math.ceil( Util.max( y ) );
+ interval = Intervals.createMinMax( minX, minY, maxX, maxY );
+ }
+
+ @Override
+ public long size()
+ {
+ int n = 0;
+ final Cursor< T > cursor = cursor();
+ while ( cursor.hasNext() )
+ {
+ cursor.fwd();
+ n++;
+ }
+ return n;
+ }
+
+ @Override
+ public T firstElement()
+ {
+ return cursor().next();
+ }
+
+ @Override
+ public Object iterationOrder()
+ {
+ return this;
+ }
+
+ @Override
+ public double realMin( final int d )
+ {
+ return interval.realMin( d );
+ }
+
+ @Override
+ public double realMax( final int d )
+ {
+ return interval.realMax( d );
+ }
+
+ @Override
+ public int numDimensions()
+ {
+ return 2;
+ }
+
+ @Override
+ public long min( final int d )
+ {
+ return interval.min( d );
+ }
+
+ @Override
+ public long max( final int d )
+ {
+ return interval.max( d );
+ }
+
+ @Override
+ public Cursor< T > cursor()
+ {
+ return new MyCursor< T >( x, y, ra );
+ }
- return ( a + x[ n - 1 ] * y[ 0 ] - x[ 0 ] * y[ n - 1 ] ) / 2.0;
+ @Override
+ public Cursor< T > localizingCursor()
+ {
+ return cursor();
+ }
+
+ @Override
+ public Iterator< T > iterator()
+ {
+ return cursor();
+ }
+ }
+
+ /**
+ * Iterates inside a close polygon given by X & Y in pixel coordinates.
+ *
+ * @param
* Will create and position the sequence that will be played by the wizard
* launched by this plugin.
+ *
+ * @param guiModel
+ * the {@link GuiModel} that will be used to store the data of
+ * the wizard.
*
- * @param trackmate
- * the {@link TrackMate} instance to use.
- * @param selectionModel
- * the {@link SelectionModel} to use.
- * @param displaySettings
- * the {@link DisplaySettings} to use.
* @return a new sequence.
*/
- protected WizardSequence createSequence( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings )
+ protected WizardSequence createSequence( final GuiModel guiModel )
{
- return new TrackMateWizardSequence( trackmate, selectionModel, displaySettings );
+ return new TrackMateWizardSequence( guiModel );
}
/**
@@ -146,7 +139,7 @@ protected WizardSequence createSequence( final TrackMate trackmate, final Select
* {@link TrackMate} instance.
*
* @param imp
- * the {@link ImagePlus} to operate on.
+ * the image the tracking data will be created on.
* @return a new {@link Model} instance.
*/
protected Model createModel( final ImagePlus imp )
@@ -171,41 +164,11 @@ protected Model createModel( final ImagePlus imp )
protected Settings createSettings( final ImagePlus imp )
{
// Persistence.
- final Settings ls = SettingsPersistence.readLastUsedSettings( imp, Logger.DEFAULT_LOGGER );
- // Force adding analyzers found at runtime
- ls.addAllAnalyzers();
- return ls;
- }
-
- /**
- * Hook for subclassers:
- * Creates the TrackMate instance that will be controlled in the GUI.
- *
- * @param model
- * the model to use.
- * @param settings
- * the settings to use.
- * @return a new {@link TrackMate} instance.
- */
- protected TrackMate createTrackMate( final Model model, final Settings settings )
- {
- /*
- * Since we are now sure that we will be working on this model with this
- * settings, we need to pass to the model the units from the settings.
- */
- final String spaceUnits = settings.imp.getCalibration().getXUnit();
- final String timeUnits = settings.imp.getCalibration().getTimeUnit();
- model.setPhysicalUnits( spaceUnits, timeUnits );
-
- final TrackMate trackmate = new TrackMate( model, settings );
- final ObjectService objectService = TMUtils.getContext().service( ObjectService.class );
- if ( objectService != null )
- objectService.addObject( trackmate );
-
- // Set the num of threads from IJ prefs.
- trackmate.setNumThreads( Prefs.getThreads() );
-
- return trackmate;
+ final Settings settings = SettingsPersistence.readLastUsedSettings( imp, Logger.DEFAULT_LOGGER );
+ // Add the analyzers configured by the user.
+ final AnalyzerSelection analyzerSelection = AnalyzerSelectionIO.readUserDefault();
+ analyzerSelection.configure( settings );
+ return settings;
}
protected DisplaySettings createDisplaySettings()
diff --git a/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java b/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java
index 55f65daeb..365f2f0f6 100644
--- a/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java
+++ b/src/main/java/fiji/plugin/trackmate/TrackMateRunner.java
@@ -40,6 +40,7 @@
import fiji.plugin.trackmate.detection.DetectorKeys;
import fiji.plugin.trackmate.features.FeatureFilter;
import fiji.plugin.trackmate.features.track.TrackBranchingAnalyzer;
+import fiji.plugin.trackmate.gui.GuiModel;
import fiji.plugin.trackmate.gui.GuiUtils;
import fiji.plugin.trackmate.gui.components.LogPanel;
import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings;
@@ -53,6 +54,7 @@
import fiji.plugin.trackmate.visualization.TrackMateModelView;
import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer;
import fiji.util.SplitString;
+import ij.IJ;
import ij.ImageJ;
import ij.ImagePlus;
import ij.Macro;
@@ -296,15 +298,14 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) )
}
/*
- * Instantiate TrackMate.
+ * Instantiate GUImodel.
*/
final Settings settings = createSettings( imp );
final Model model = createModel( imp );
- final SelectionModel selectionModel = new SelectionModel( model );
model.setLogger( logger );
- final TrackMate trackmate = createTrackMate( model, settings );
final DisplaySettings displaySettings = createDisplaySettings();
+ final GuiModel guiModel = new GuiModel( model, settings, displaySettings );
/*
* Configure default settings.
@@ -402,12 +403,12 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) )
imp.show();
}
GuiUtils.userCheckImpDimensions( imp );
+
// Main view.
- final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, imp, displaySettings );
- displayer.render();
+ guiModel.getWindowManager().createHyperStackDisplayer();
// Wizard.
- final WizardSequence sequence = createSequence( trackmate, selectionModel, displaySettings );
+ final WizardSequence sequence = createSequence( guiModel );
final JFrame frame = sequence.run( "TrackMate on " + imp.getShortTitle() );
frame.setIconImage( TRACKMATE_ICON.getImage() );
GuiUtils.positionWindow( frame, imp.getWindow() );
@@ -415,6 +416,7 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) )
return;
}
+ final TrackMate trackmate = guiModel.getTrackMate();
final String welcomeMessage = TrackMate.PLUGIN_NAME_STR + " v" + TrackMate.PLUGIN_NAME_VERSION + " started on:\n" + TMUtils.getCurrentTimeString() + '\n';
logger.log( welcomeMessage );
if ( !trackmate.checkInput() || !trackmate.process() )
@@ -434,8 +436,8 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) )
final TmXmlWriter writer = new TmXmlWriter( save_path, logger );
writer.appendLog( logger.toString() );
- writer.appendModel( trackmate.getModel() );
- writer.appendSettings( trackmate.getSettings() );
+ writer.appendModel( model );
+ writer.appendSettings( settings );
try
{
@@ -501,11 +503,11 @@ else if ( macroOptions.containsKey( ARG_INPUT_IMAGE_PATH ) )
*/
// Main view.
- final TrackMateModelView displayer = new HyperStackDisplayer( model, selectionModel, imp, displaySettings );
+ final TrackMateModelView displayer = new HyperStackDisplayer( guiModel );
displayer.render();
// Wizard.
- final WizardSequence sequence = createSequence( trackmate, selectionModel, displaySettings );
+ final WizardSequence sequence = createSequence( guiModel );
sequence.setCurrent( ConfigureViewsDescriptor.KEY );
final JFrame frame = sequence.run( "TrackMate on " + imp.getShortTitle() );
frame.setIconImage( TRACKMATE_ICON.getImage() );
diff --git a/src/main/java/fiji/plugin/trackmate/TrackModel.java b/src/main/java/fiji/plugin/trackmate/TrackModel.java
index bd7b8cb4c..8b104b854 100644
--- a/src/main/java/fiji/plugin/trackmate/TrackModel.java
+++ b/src/main/java/fiji/plugin/trackmate/TrackModel.java
@@ -312,7 +312,7 @@ void setEdgeWeight( final DefaultWeightedEdge edge, final double weight )
edgesModified.add( edge );
}
- Boolean setVisibility( final Integer trackID, final boolean visible )
+ public boolean setVisibility( final Integer trackID, final boolean visible )
{
return visibility.put( trackID, Boolean.valueOf( visible ) );
}
@@ -518,6 +518,14 @@ public boolean isVisible( final Integer ID )
/**
* Returns the set of track IDs managed by this model, ordered by track
* names (alpha-numerically sorted).
+ *
+ * List< String > trackIDList = new ArrayList<>( trackIDs );
+ * ListIterator< String > iterator = trackIDList.listIterator();
+ *
*
* @param visibleOnly
* if true, only visible track IDs will be returned.
@@ -733,27 +741,43 @@ public String echo()
*/
/**
- * Returns a new depth first iterator over the spots connected by links in
- * this model. A boolean flag allow to set whether the returned iterator
- * does take into account the edge direction. If true, the iterator will not
- * be able to iterate backward in time.
+ * Returns a new, undirected depth first iterator over the spots connected
+ * by links in this model. The iterator will iterate backward and forward in
+ * time.
*
* @param start
* the spot to start iteration with. Can be null,
* then the start will be taken randomly and will traverse all
* the links.
- * @param directed
- * if true returns a directed iterator, undirected if false.
* @return a new depth-first iterator.
*/
- public GraphIterator< Spot, DefaultWeightedEdge > getDepthFirstIterator( final Spot start, final boolean directed )
+ public GraphIterator< Spot, DefaultWeightedEdge > getDepthFirstIterator( final Spot start )
{
- if ( directed )
- return new TimeDirectedDepthFirstIterator( graph, start );
-
return new DepthFirstIterator<>( graph, start );
}
+ /**
+ * Returns a new, directed depth first iterator over the spots connected by
+ * links in this model.
+ * reversed is set to true, the iterator will
+ * iterate backward in time only.
+ *
+ * @param start
+ * the spot to start iteration with. Can be null,
+ * then the start will be taken randomly and will traverse all
+ * the links.
+ * @param reversed
+ * if true, the iterator will iterate backward in
+ * time only, otherwise it will iterate forward in time only.
+ * @return a new depth-first iterator.
+ */
+ public GraphIterator< Spot, DefaultWeightedEdge > getDirectedDepthFirstIterator( final Spot start, final boolean reversed )
+ {
+ return new TimeDirectedDepthFirstIterator( graph, start, reversed );
+ }
+
/**
* Returns a new depth first iterator over the spots connected by links in
* this model. This iterator is sorted: when branching, it chooses the next
@@ -1288,7 +1312,7 @@ public void edgeRemoved( final GraphEdgeChangeEvent< Spot, DefaultWeightedEdge >
for ( final Spot v : targetVCS )
vertexToID.put( v, newid );
- final Boolean targetVisibility = visibility.get( id );
+ final boolean targetVisibility = visibility.get( id );
visibility.put( newid, targetVisibility );
names.put( newid, nameGenerator.next() );
// Transaction: both children tracks are marked for
diff --git a/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java b/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java
index 14a285638..e532311a2 100644
--- a/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java
+++ b/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java
@@ -2,7 +2,7 @@
* #%L
* TrackMate: your buddy for everyday tracking.
* %%
- * Copyright (C) 2010 - 2026 TrackMate developers.
+ * 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
@@ -50,9 +50,10 @@
import fiji.plugin.trackmate.Settings;
import fiji.plugin.trackmate.Spot;
import fiji.plugin.trackmate.SpotCollection;
+import fiji.plugin.trackmate.SpotRoi;
import fiji.plugin.trackmate.TrackMate;
import fiji.plugin.trackmate.TrackModel;
-import fiji.plugin.trackmate.action.LabelImgExporter.SpotRoiWriter;
+import fiji.plugin.trackmate.action.LabelImgExporter.SpotShapeWriter;
import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition;
import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition.TrackBranchDecomposition;
import fiji.plugin.trackmate.graph.GraphUtils;
@@ -77,7 +78,7 @@
* Cell-Tracking-Challenge convention.
*