diff --git a/pom.xml b/pom.xml index b2c82cef9..4137de1cc 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ sc.fiji TrackMate - 8.0.0-SNAPSHOT + 9.0.0-SNAPSHOT TrackMate TrackMate plugin for Fiji. @@ -134,6 +134,7 @@ + true fiji.plugin.trackmate gpl_v3 TrackMate developers. @@ -158,6 +159,12 @@ sc.fiji labkit-ui + + + net.imagej + imagej-legacy + + @@ -169,6 +176,37 @@ net.imagej imagej-common + + net.imglib2 + imglib2-mesh + + + net.imagej + imagej-ops + + + sc.fiji + bigvolumeviewer + + + org.jogamp.jogl + jogl-all + + + org.jogamp.gluegen + gluegen-rt + + + org.jogamp.gluegen + gluegen-rt + ${scijava.natives.classifier.gluegen} + + + org.jogamp.jogl + jogl-all + ${scijava.natives.classifier.jogl} + + @@ -205,8 +243,16 @@ org.scijava scijava-listeners + + org.scijava + ui-behaviour + + + com.google.guava + guava + com.github.vlsi.mxgraph jgraphx diff --git a/src/main/java/fiji/plugin/trackmate/Dimension.java b/src/main/java/fiji/plugin/trackmate/Dimension.java index ece2b7d00..103caa203 100644 --- a/src/main/java/fiji/plugin/trackmate/Dimension.java +++ b/src/main/java/fiji/plugin/trackmate/Dimension.java @@ -31,8 +31,13 @@ public enum Dimension POSITION, VELOCITY, LENGTH, - AREA, TIME, ANGLE, RATE, // count per frames - ANGLE_RATE, STRING; // for non-numeric features + AREA, + VOLUME, + TIME, + ANGLE, + RATE, // count per frames + ANGLE_RATE, + STRING; // for non-numeric features /* * We separated length and position so that x,y,z are plotted on a different diff --git a/src/main/java/fiji/plugin/trackmate/Model.java b/src/main/java/fiji/plugin/trackmate/Model.java index d17b965ff..7f9d1534b 100644 --- a/src/main/java/fiji/plugin/trackmate/Model.java +++ b/src/main/java/fiji/plugin/trackmate/Model.java @@ -81,13 +81,13 @@ public class Model */ private int updateLevel = 0; - private final HashSet< Spot > spotsAdded = new HashSet< >(); + private final HashSet< Spot > spotsAdded = new HashSet<>(); - private final HashSet< Spot > spotsRemoved = new HashSet< >(); + private final HashSet< Spot > spotsRemoved = new HashSet<>(); - private final HashSet< Spot > spotsMoved = new HashSet< >(); + private final HashSet< Spot > spotsMoved = new HashSet<>(); - private final HashSet< Spot > spotsUpdated = new HashSet< >(); + private final HashSet< Spot > spotsUpdated = new HashSet<>(); /** * The event cache. During a transaction, some modifications might trigger @@ -96,15 +96,15 @@ public class Model * the event ID in this cache in the meantime. The event cache contains only * the int IDs of the events listed in {@link ModelChangeEvent}, namely * * The {@link ModelChangeEvent#MODEL_MODIFIED} cannot be cached this way, * for it needs to be configured with modification spot and edge targets, so * it uses a different system (see {@link #flushUpdate()}). */ - private final HashSet< Integer > eventCache = new HashSet< >(); + private final HashSet< Integer > eventCache = new HashSet<>(); // OTHERS @@ -120,7 +120,7 @@ public class Model /** * The list of listeners listening to model content change. */ - Set< ModelChangeListener > modelChangeListeners = new LinkedHashSet< >(); + Set< ModelChangeListener > modelChangeListeners = new LinkedHashSet<>(); /* * CONSTRUCTOR @@ -130,6 +130,7 @@ public Model() { featureModel = createFeatureModel(); trackModel = createTrackModel(); + addModelChangeListener( new SpotMeshSliceCacheInvalidator() ); } /* @@ -154,7 +155,7 @@ protected TrackModel createTrackModel() *

* 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 +322,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 +448,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 +459,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 +545,7 @@ public synchronized Spot moveSpotFrom( final Spot spotToMove, final Integer from * model.endUpdate(); * } * - * + * * @param spotToAdd * the spot to add. * @param toFrame @@ -593,8 +594,9 @@ 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 + trackModel.removeSpot( spotToRemove ); + // changes to edges will be caught automatically by the + // TrackGraphModel return spotToRemove; } if ( DEBUG ) @@ -626,7 +628,7 @@ public synchronized Spot removeSpot( final Spot spotToRemove ) public synchronized void updateFeatures( final Spot spotToUpdate ) { spotsUpdated.add( spotToUpdate ); // Enlist for feature update when - // transaction is marked as finished + // transaction is marked as finished final Set< DefaultWeightedEdge > touchingEdges = trackModel.edgesOf( spotToUpdate ); if ( null != touchingEdges ) { @@ -766,7 +768,7 @@ public synchronized boolean setTrackVisibility( final Integer trackID, final boo * 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 +812,7 @@ public Model copy() featureModel.getTrackFeatureShortNames(), featureModel.getTrackFeatureDimensions(), featureModel.getTrackFeatureIsInt() ); - + // Feature values are not copied. return copy; } @@ -841,7 +843,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 ) @@ -853,7 +855,7 @@ private void flushUpdate() 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 ); @@ -958,4 +960,20 @@ 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() ); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/Settings.java b/src/main/java/fiji/plugin/trackmate/Settings.java index 22cad10fa..1034eef99 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; @@ -270,7 +271,7 @@ public int getYend() * copied, as well as filters, etc. The exception are analyzers: all the * analyzers that are found at runtime are added, regardless of the content * of the instance to copy. - * + * * @param newImp * the image to copy the settings for. * @return a new settings object. @@ -531,24 +532,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 00bb2f5bd..b0a678f2b 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 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,233 +69,94 @@ 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 - */ - - /** - * 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 METHODS */ - 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; - } - } - /** - * 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 + * the type of pixels in the {@link RandomAccessible}. + * @return an iterable. */ - public Spot( final Spot spot ) - { - this( spot, spot.getFeature( RADIUS ), spot.getFeature( QUALITY ), spot.getName() ); - } + public < T extends RealType< T > > IterableInterval< T > iterable( RandomAccessible< T > ra, double calibration[] ); /** - * 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 + * Returns an iterable that will iterate over all the pixels contained in + * this spot. + * + * @param img + * the ImgPlus to iterate over. + * @param + * the type of pixels in the {@link RandomAccessible}. + * @return an iterable. */ - public Spot( final int ID ) + public default < T extends RealType< T > > IterableInterval< T > iterable( final ImgPlus< T > img ) { - super( 3 ); - this.ID = ID; - synchronized ( IDcounter ) - { - if ( IDcounter.get() < ID ) - { - IDcounter.set( ID ); - } - } - } - - /* - * PUBLIC METHODS - */ - - @Override - public int hashCode() - { - return ID; - } - - @Override - public int compareTo( final Spot o ) - { - return ID - o.ID; - } - - @Override - public boolean equals( final Object other ) - { - if ( other == null ) - return false; - if ( other == this ) - return true; - if ( !( other instanceof Spot ) ) - return false; - final Spot os = ( Spot ) other; - return os.ID == this.ID; - } - - public void setRoi( final SpotRoi roi ) - { - this.roi = roi; - } - - public SpotRoi getRoi() - { - return roi; + return iterable( Views.extendMirrorSingle( img ), TMUtils.getSpatialCalibration( img ) ); } /** * @return the name for this Spot. */ - public String getName() - { - return this.name; - } + public String getName(); /** * Set the name of this Spot. - * + * * @param name * the name to use. */ - public void setName( final String name ) - { - this.name = name; - } - - public int ID() - { - return ID; - } + public void setName( final String name ); - @Override - public String toString() - { - String str; - if ( null == name || name.equals( "" ) ) - str = "ID" + ID; - else - str = name; - return str; - } + /** + * Returns the unique ID of this spot. The ID is unique within a session. + * + * @return the spot ID. + */ + public int ID(); /** * Return a string representation of this spot, with calculated features. - * + * * @return a string representation of the spot. */ - public String echo() + public default String echo() { final StringBuilder s = new StringBuilder(); - + final String name = getName(); // Name if ( null == name ) s.append( "Spot: \n" ); @@ -306,6 +172,7 @@ public String echo() s.append( "Position: " + Util.printCoordinates( coordinates ) + "\n" ); // Feature list + final Map< String, Double > features = getFeatures(); if ( null == features || features.size() < 1 ) s.append( "No features calculated\n" ); else @@ -336,10 +203,7 @@ public String echo() * * @return a map of {@link String}s to {@link Double}s. */ - public Map< String, Double > getFeatures() - { - return features; - } + public Map< String, Double > getFeatures(); /** * Returns the value corresponding to the specified spot feature. @@ -349,10 +213,7 @@ public Map< String, Double > getFeatures() * @return the feature value, as a {@link Double}. Will be 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,24 +224,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 spot src to the current spot + * Copy some of the features values of the specified spot to this spot. * + * @param src + * the spot to copy feature values from. + * @param features + * the collection of feature keys to copy. */ - public void copyFeatures( 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 ( @@ -395,9 +268,9 @@ public void copyFeatures( 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; } @@ -423,9 +296,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; @@ -440,7 +313,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++ ) @@ -484,97 +357,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 = 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 */ - 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 ); + + @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 default void move( final RealLocalizable distance ) + { + 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 void localize( final float[] position ) + public default void localize( final float[] position ) { - assert ( position.length >= n ); - for ( int d = 0; d < n; ++d ) + 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 ] ); } @@ -592,7 +600,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 >() { 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..b97961d74 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/SpotBase.java @@ -0,0 +1,330 @@ +/*- + * #%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; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import fiji.plugin.trackmate.util.SpotNeighborhood; +import net.imglib2.AbstractEuclideanSpace; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessible; +import net.imglib2.RealLocalizable; +import net.imglib2.type.numeric.RealType; +import net.imglib2.view.Views; + +/** + * A {@link RealLocalizable} implementation of {@link Spot}, used in TrackMate + * to represent a detection. This concrete implementation has the simplest + * shape: a spot is a sphere of fixed radius. + *

+ * 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 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 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/SpotMesh.java b/src/main/java/fiji/plugin/trackmate/SpotMesh.java new file mode 100644 index 000000000..2da0821b2 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/SpotMesh.java @@ -0,0 +1,391 @@ +/*- + * #%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; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import fiji.plugin.trackmate.util.mesh.SpotMeshIterable; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessible; +import net.imglib2.RealInterval; +import net.imglib2.RealLocalizable; +import net.imglib2.RealPoint; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.MeshStats; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.Vertices; +import net.imglib2.mesh.alg.zslicer.RamerDouglasPeucker; +import net.imglib2.mesh.alg.zslicer.Slice; +import net.imglib2.mesh.alg.zslicer.ZSlicer; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.type.numeric.RealType; + +public class SpotMesh extends SpotBase +{ + + /** + * The mesh representing the 3D contour of the spot. The mesh is centered on + * (0, 0, 0) and the true position of its vertices is obtained by adding the + * spot center. + */ + private BufferMesh mesh; + + private Map< Integer, Slice > sliceMap; + + /** The bounding-box, centered on (0,0,0) of this object. */ + private RealInterval boundingBox; + + public SpotMesh( + final Mesh mesh, + final double quality ) + { + this( mesh, quality, null ); + } + + /** + * Creates a new spot from the specified mesh. Its position and radius are + * calculated from the mesh. + * + * @param quality + * the spot quality. + * @param name + * the spot name. + * @param m + * the mesh to create the spot from. + */ + public SpotMesh( + final Mesh m, + final double quality, + final String name ) + { + // Dummy coordinates and radius. + super( 0., 0., 0., 0., quality, name ); + setMesh( m ); + } + + public void setMesh( final Mesh m ) + { + // Store a copy in a Buffer mesh. + final BufferMesh mesh = new BufferMesh( m.vertices().size(), m.triangles().size() ); + Meshes.calculateNormals( m, mesh ); + this.mesh = mesh; + + // Compute new true center. + final RealPoint center = Meshes.center( mesh ); + + // Reposition the spot. + setPosition( center ); + + // Shift mesh to (0, 0, 0). + final net.imglib2.mesh.Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + for ( long i = 0; i < nVertices; i++ ) + vertices.setPositionf( i, + vertices.xf( i ) - center.getFloatPosition( 0 ), + vertices.yf( i ) - center.getFloatPosition( 1 ), + vertices.zf( i ) - center.getFloatPosition( 2 ) ); + + // Compute radius. + final double r = radius( mesh ); + putFeature( Spot.RADIUS, r ); + + // Bounding box, also centered on (0,0,0) + this.boundingBox = Meshes.boundingBox( mesh ); + + // Slice cache. + resetZSliceCache(); + } + + public RealInterval getBoundingBox() + { + return boundingBox; + } + + /** + * This constructor is only used for deserializing a model from a TrackMate + * file. It messes with the ID of the spots and should be not used + * otherwise. + * + * @param ID + * the ID to create the spot with. + * @param mesh + * the mesh used to create the spot. + */ + public SpotMesh( final int ID, final BufferMesh mesh ) + { + super( ID ); + this.mesh = mesh; + final RealPoint center = Meshes.center( mesh ); + + // Reposition the spot. + setPosition( center ); + + // Shift mesh to (0, 0, 0). + final Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + for ( long i = 0; i < nVertices; i++ ) + vertices.setPositionf( i, + vertices.xf( i ) - center.getFloatPosition( 0 ), + vertices.yf( i ) - center.getFloatPosition( 1 ), + vertices.zf( i ) - center.getFloatPosition( 2 ) ); + + // Compute radius. + final double r = radius( mesh ); + putFeature( Spot.RADIUS, r ); + + // Bounding box, also centered on (0,0,0) + this.boundingBox = Meshes.boundingBox( mesh ); + } + + /** + * Exposes the mesh object stores in this spot. 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 BufferMesh getMesh() + { + return 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 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 ) + { + final net.imglib2.mesh.Vertices vertices = mesh.vertices(); + final long nVertices = vertices.size(); + for ( int v = 0; v < nVertices; v++ ) + { + final float x = vertices.xf( v ); + final float y = vertices.yf( v ); + final float z = vertices.zf( v ); + + // Spherical coords. + if ( x == 0. && y == 0. ) + { + if ( z == 0 ) + continue; + + vertices.setPositionf( v, 0f, 0f, ( float ) ( z * alpha ) ); + continue; + } + final double r = Math.sqrt( x * x + y * y + z * z ); + final double theta = Math.acos( z / r ); + final double phi = Math.signum( y ) * Math.acos( x / Math.sqrt( x * x + y * y ) ); + + final double ra = r * alpha; + final float xa = ( float ) ( ra * Math.sin( theta ) * Math.cos( phi ) ); + final float ya = ( float ) ( ra * Math.sin( theta ) * Math.sin( phi ) ); + final float za = ( float ) ( ra * Math.cos( theta ) ); + vertices.setPositionf( v, xa, ya, za ); + } + this.boundingBox = Meshes.boundingBox( mesh ); + } + + @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 00d2556e6..00396adb2 100644 --- a/src/main/java/fiji/plugin/trackmate/SpotRoi.java +++ b/src/main/java/fiji/plugin/trackmate/SpotRoi.java @@ -22,114 +22,238 @@ package fiji.plugin.trackmate; import java.util.Arrays; +import java.util.Iterator; -import net.imagej.ImgPlus; +import gnu.trove.list.array.TDoubleArrayList; +import net.imglib2.Cursor; +import net.imglib2.FinalInterval; import net.imglib2.IterableInterval; -import net.imglib2.RandomAccessibleInterval; -import net.imglib2.roi.IterableRegion; -import net.imglib2.roi.Masks; -import net.imglib2.roi.Regions; -import net.imglib2.roi.geom.GeomMasks; -import net.imglib2.roi.geom.real.WritablePolygon2D; -import net.imglib2.type.logic.BoolType; +import net.imglib2.Localizable; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessible; +import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Util; +import net.imglib2.view.IntervalView; import net.imglib2.view.Views; -public class SpotRoi +public class SpotRoi extends SpotBase { - /** - * Polygon points X coordinates, in physical units. - */ - public final double[] x; + /** Polygon points X coordinates, in physical units, centered (0,0). */ + private final double[] x; + + /** Polygon points Y coordinates, in physical units, centered (0,0). */ + private final double[] y; + + public SpotRoi( + final double xc, + final double yc, + final double zc, + final double r, + final double quality, + final String name, + final double[] x, + final double[] y ) + { + super( xc, yc, zc, r, quality, name ); + this.x = x; + this.y = y; + } /** - * Polygon points Y coordinates, in physical units. + * This constructor is only used for deserializing a model from a TrackMate + * file. It messes with the ID of the spots and should be not used + * otherwise. + * + * @param ID + * the ID to use when creating the spot. + * @param x + * the spot contour X coordinates. + * @param y + * the spot contour Y coordinates. */ - public final double[] y; - - public SpotRoi( final double[] x, final double[] y ) + public SpotRoi( + final int ID, + final double[] x, + final double[] y ) { + super( ID ); this.x = x; this.y = y; } + @Override public SpotRoi copy() { - return new SpotRoi( x.clone(), y.clone() ); + final double xc = getDoublePosition( 0 ); + final double yc = getDoublePosition( 1 ); + final double zc = getDoublePosition( 2 ); + final double r = getFeature( Spot.RADIUS ); + final double quality = getFeature( Spot.QUALITY ); + return new SpotRoi( xc, yc, zc, r, quality, getName(), x.clone(), y.clone() ); + } + + /** + * Returns the X coordinates of the ith vertex of the polygon, in physical + * coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex X position. + */ + public double x( final int i ) + { + return x[ i ] + getDoublePosition( 0 ); + } + + /** + * Returns the Y coordinates of the ith vertex of the polygon, in physical + * coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex Y position. + */ + public double y( final int i ) + { + return y[ i ] + getDoublePosition( 1 ); + } + + /** + * Returns the X coordinates of the ith vertex of the polygon, relative + * to the spot center, in physical coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex X position. + */ + public double xr( final int i ) + { + return x[ i ]; + } + + /** + * Returns the Y coordinates of the ith vertex of the polygon, relative + * to the spot center, in physical coordinates. + * + * @param i + * the index of the vertex. + * @return the vertex Y position. + */ + public double yr( final int i ) + { + return y[ i ]; + } + + public int nPoints() + { + return x.length; + } + + @Override + public double realMin( final int d ) + { + final double[] arr = ( d == 0 ) ? x : y; + return getDoublePosition( d ) + Util.min( arr ); + } + + @Override + public double realMax( final int d ) + { + final double[] arr = ( d == 0 ) ? x : y; + return getDoublePosition( d ) + Util.max( arr ); } /** - * Returns a new int array containing the X 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 X, to convert physical coordinates to pixel - * coordinates. - * @param xcorner - * the top-left X corner of the view in the image to paint. - * @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. + * @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 double[] toPolygonX( final double calibration, final double xcorner, final double spotXCenter, final double magnification ) + public void toArray( final double cx, final double cy, final double sx, final double sy, final TDoubleArrayList xout, final TDoubleArrayList yout ) { - final double[] xp = new double[ x.length ]; - for ( int i = 0; i < xp.length; i++ ) + 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 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 ) { for ( int i = 0; i < x.length; i++ ) @@ -144,7 +268,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 ); @@ -153,15 +277,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 ); } /* @@ -174,16 +293,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 }; } @@ -191,9 +308,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; - return ( a + x[ n - 1 ] * y[ 0 ] - x[ 0 ] * y[ n - 1 ] ) / 2.0; + /** 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 ); + } + + @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 + * the type of pixel in the image. + */ + private static final class MyCursor< T extends RealType< T > > implements Cursor< T > + { + + private final FinalInterval interval; + + private Cursor< T > cursor; + + private final double[] x; + + private final double[] y; + + private boolean hasNext; + + private final RandomAccessible< T > rae; + + private RandomAccess< T > ra; + + public MyCursor( final double[] x, final double[] y, final RandomAccessible< T > rae ) + { + this.x = x; + this.y = y; + this.rae = rae; + 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 ); + reset(); + } + + @Override + public T get() + { + return ra.get(); + } + + @Override + public void fwd() + { + ra.setPosition( cursor ); + fetch(); + } + + private void fetch() + { + while ( cursor.hasNext() ) + { + cursor.fwd(); + if ( isInside( cursor, x, y ) ) + { + hasNext = cursor.hasNext(); + return; + } + } + hasNext = false; + } + + private static final boolean isInside( final Localizable localizable, final double[] x, final double[] y ) + { + // Taken from Imglib2-roi GeomMaths. No edge case. + final double xl = localizable.getDoublePosition( 0 ); + final double yl = localizable.getDoublePosition( 1 ); + + int i; + int j; + boolean inside = false; + for ( i = 0, j = x.length - 1; i < x.length; j = i++ ) + { + final double xj = x[ j ]; + final double yj = y[ j ]; + + final double xi = x[ i ]; + final double yi = y[ i ]; + + if ( ( yi > yl ) != ( yj > yl ) && ( xl < ( xj - xi ) * ( yl - yi ) / ( yj - yi ) + xi ) ) + inside = !inside; + } + return inside; + } + + @Override + public void reset() + { + final IntervalView< T > view = Views.interval( rae, interval ); + cursor = view.localizingCursor(); + ra = rae.randomAccess( interval ); + fetch(); + } + + @Override + public double getDoublePosition( final int d ) + { + return ra.getDoublePosition( d ); + } + + @Override + public int numDimensions() + { + return 2; + } + + @Override + public void jumpFwd( final long steps ) + { + for ( int i = 0; i < steps; i++ ) + fwd(); + } + + @Override + public boolean hasNext() + { + return hasNext; + } + + @Override + public T next() + { + fwd(); + return get(); + } + + @Override + public long getLongPosition( final int d ) + { + return ra.getLongPosition( d ); + } + + @Override + public Cursor< T > copy() + { + return new MyCursor<>( x, y, rae ); + } + + @Override + public Cursor< T > copyCursor() + { + return copy(); + } } } diff --git a/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java b/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java index 868319c14..803c5fb81 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/TrackMatePlugIn.java @@ -30,6 +30,8 @@ import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelection; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelectionIO; import fiji.plugin.trackmate.gui.wizard.TrackMateWizardSequence; import fiji.plugin.trackmate.gui.wizard.WizardSequence; import fiji.plugin.trackmate.io.SettingsPersistence; @@ -109,8 +111,11 @@ else if ( imp.getType() == ImagePlus.COLOR_RGB ) * launched by this plugin. * * @param trackmate + * the TrackMate instance. * @param selectionModel + * the selection model. * @param displaySettings + * the display settings. * @return a new sequence. */ protected WizardSequence createSequence( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings ) @@ -124,7 +129,7 @@ protected WizardSequence createSequence( final TrackMate trackmate, final Select * {@link TrackMate} instance. * * @param imp - * + * the image the tracking data will be created on. * @return a new {@link Model} instance. */ protected Model createModel( final ImagePlus imp ) @@ -149,16 +154,21 @@ 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; + 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; } /** * Hook for subclassers:
* Creates the TrackMate instance that will be controlled in the GUI. - * + * + * @param model + * the model to create the TrackMate instance with. + * @param settings + * the settings to create the TrackMate instance with. * @return a new {@link TrackMate} instance. */ protected TrackMate createTrackMate( final Model model, final Settings settings ) @@ -172,7 +182,9 @@ protected TrackMate createTrackMate( final Model model, final Settings settings model.setPhysicalUnits( spaceUnits, timeUnits ); final TrackMate trackmate = new TrackMate( model, settings ); - ObjectService objectService = TMUtils.getContext().service( ObjectService.class ); + + // Register it to the object service. + final ObjectService objectService = TMUtils.getContext().service( ObjectService.class ); if ( objectService != null ) objectService.addObject( trackmate ); diff --git a/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java b/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java index 8ddb63ac9..e532311a2 100644 --- a/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/CTCExporter.java @@ -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; @@ -183,6 +184,8 @@ public static String exportAll( final String exportRootFolder, final TrackMate t * the trackmate to export. * @param logger * a logger to report progress. + * @throws IOException + * if there's any problem writing. */ public static void exportSettingsFile( final String exportRootFolder, final int saveId, final TrackMate trackmate, final Logger logger ) throws IOException { @@ -314,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 ) ); + } } } @@ -409,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 ) ) @@ -446,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 ); } diff --git a/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java b/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java index b6560ded4..87e939a33 100644 --- a/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/CaptureOverlayAction.java @@ -120,7 +120,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo TMUtils.getSpatialCalibration( imp ) ); if ( whiteBackground ) { - ImageProcessor ip = imp2.getProcessor(); + final ImageProcessor ip = imp2.getProcessor(); ip.invertLut(); if ( imp2.getStackSize() > 1 ) imp2.getStack().setColorModel( ip.getColorModel() ); @@ -153,6 +153,8 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo * the first frame, inclusive, to capture. * @param last * the last frame, inclusive, to capture. + * @param logger + * a logger instance to echo capture progress. * @return a new ImagePlus. */ public static ImagePlus capture( final TrackMate trackmate, final int first, final int last, final Logger logger ) diff --git a/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java b/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java index d1c18e4ea..9d2925711 100644 --- a/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/IJRoiExporter.java @@ -86,14 +86,13 @@ public void export( final Iterable< Spot > spots ) public void export( final Spot spot ) { - final SpotRoi sroi = spot.getRoi(); final Roi roi; - if ( sroi != null ) + if ( spot instanceof SpotRoi ) { - final double[] xs = sroi.toPolygonX( dx, 0., spot.getDoublePosition( 0 ), 1. ); - final double[] ys = sroi.toPolygonY( dy, 0., spot.getDoublePosition( 1 ), 1. ); - final float[] xp = toFloat( xs ); - final float[] yp = toFloat( ys ); + final SpotRoi sroi = ( SpotRoi ) spot; + final double[][] out = sroi.toArray( 0., 0., 1 / dx, 1 / dy ); + final float[] xp = toFloat( out[ 0 ] ); + final float[] yp = toFloat( out[ 1 ] ); roi = new PolygonRoi( xp, yp, PolygonRoi.POLYGON ); } else diff --git a/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java b/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java index 8b286be51..38add12ec 100644 --- a/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java +++ b/src/main/java/fiji/plugin/trackmate/action/LabelImgExporter.java @@ -41,7 +41,6 @@ import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; -import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; import fiji.plugin.trackmate.visualization.GlasbeyLut; import ij.ImagePlus; @@ -166,8 +165,8 @@ public static final ImagePlus createLabelImagePlus( * of this input image, except for the number of channels, which * will be 1. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -203,8 +202,8 @@ public static final ImagePlus createLabelImagePlus( * source image, except for the number of channels, which will be * 1. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -237,8 +236,8 @@ public static final ImagePlus createLabelImagePlus( * source image, except for the number of channels, which will be * 1. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -285,9 +284,12 @@ public static final ImagePlus createLabelImagePlus( * the desired dimensions of the output image (width, height, * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. + * @param calibration + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -319,9 +321,12 @@ public static final ImagePlus createLabelImagePlus( * the desired dimensions of the output image (width, height, * nZSlices, nFrames) as a 4 element int array. Spots outside * these dimensions are ignored. + * @param calibration + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -367,9 +372,12 @@ public static final ImagePlus createLabelImagePlus( * the desired dimensions of the output image (width, height, * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. + * @param calibration + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -401,9 +409,12 @@ public static final Img< FloatType > createLabelImg( * the desired dimensions of the output image (width, height, * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. + * @param calibration + * the pixel size to map physical spot coordinates to pixel + * coordinates. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param exportTracksOnly * if true, only the spots belonging to visible * tracks will be painted. If false, spots not @@ -454,8 +465,7 @@ public static final Img< FloatType > createLabelImg( final ImgPlus< FloatType > imgCT = TMUtils.hyperSlice( imgPlus, 0, frame ); final SpotWriter spotWriter = exportSpotsAsDots ? new SpotAsDotWriter<>( imgCT ) - : new SpotRoiWriter<>( imgCT ); - idGenerator.nextFrame(); + : new SpotShapeWriter<>( imgCT ); for ( final Spot spot : model.getSpots().iterable( frame, true ) ) { @@ -482,8 +492,8 @@ public static final Img< FloatType > createLabelImg( * nZSlices, nFrames) as a 4 element long array. Spots outside * these dimensions are ignored. * @param exportSpotsAsDots - * if true, spots will be painted as single dots - * instead of their shape. + * if true, spots will be painted as single dots. If + * false they will be painted with their shape. * @param labelIdPainting * specifies how to paint the label ID of spots. The * {@link LabelIdPainting#LABEL_IS_TRACK_ID} is not supported and @@ -546,7 +556,7 @@ public static < T extends RealType< T > & NativeType< T > > ImgPlus< T > createL final ImgPlus< T > imgCT = TMUtils.hyperSlice( imgPlus, 0, frame ); final SpotWriter spotWriter = exportSpotsAsDots ? new SpotAsDotWriter<>( imgCT ) - : new SpotRoiWriter<>( imgCT ); + : new SpotShapeWriter<>( imgCT ); idGenerator.nextFrame(); for ( final Spot spot : spots.iterable( frame, true ) ) @@ -604,12 +614,12 @@ public static interface SpotWriter public void write( Spot spot, int id ); } - public static final class SpotRoiWriter< T extends RealType< T > > implements SpotWriter + public static final class SpotShapeWriter< T extends RealType< T > > implements SpotWriter { private final ImgPlus< T > img; - public SpotRoiWriter( final ImgPlus< T > img ) + public SpotShapeWriter( final ImgPlus< T > img ) { this.img = img; } @@ -617,7 +627,7 @@ public SpotRoiWriter( final ImgPlus< T > img ) @Override public void write( final Spot spot, final int id ) { - for ( final T pixel : SpotUtil.iterable( spot, img ) ) + for ( final T pixel : spot.iterable( img ) ) pixel.setReal( id ); } } diff --git a/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java b/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java index af0027b9f..924bfc7df 100644 --- a/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/MergeFileAction.java @@ -39,6 +39,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.io.IOUtils; @@ -117,7 +118,7 @@ public void execute( final TrackMate trackmate, final SelectionModel selectionMo * An awkward way to avoid spot ID conflicts after loading * two files */ - newSpot = new Spot( oldSpot ); + newSpot = new SpotBase( oldSpot ); for ( final String feature : oldSpot.getFeatures().keySet() ) newSpot.putFeature( feature, oldSpot.getFeature( feature ) ); diff --git a/src/main/java/fiji/plugin/trackmate/action/MeshSeriesExporter.java b/src/main/java/fiji/plugin/trackmate/action/MeshSeriesExporter.java new file mode 100644 index 000000000..21027b385 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/MeshSeriesExporter.java @@ -0,0 +1,176 @@ +/*- + * #%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.action; + +import static fiji.plugin.trackmate.gui.Icons.ORANGE_ASTERISK_ICON; + +import java.awt.Frame; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.NavigableSet; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.io.IOUtils; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; +import net.imglib2.mesh.view.TranslateMesh; + +public class MeshSeriesExporter extends AbstractTMAction +{ + + public static final String NAME = "Export spot 3D meshes to a file series"; + + public static final String KEY = "MESH_SERIES_EXPORTER"; + + public static final String INFO_TEXT = "" + + "Export the 3D meshes in the spot of the current model " + + "to a PLY file series. " + + "

" + + "A folder is created with the file name, in which " + + "there will be one PLY file per time-point. " + + "The series can be easily imported in mesh visualization " + + "softwares, such as ParaView. " + + "

" + + "Only the visible spots containing 3D meshes are exported. " + + "If there are no such spots, no file is created. " + + ""; + + @Override + public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + { + logger.log( "Exporting spot 3D meshes to a file series.\n" ); + final Model model = trackmate.getModel(); + File file; + final File folder = new File( System.getProperty( "user.dir" ) ).getParentFile().getParentFile(); + try + { + String filename = trackmate.getSettings().imageFileName; + int i = filename.indexOf( "." ); + if ( i < 0 ) + i = filename.length(); + filename = filename.substring( 0, i ); + file = new File( folder.getPath() + File.separator + filename + "-meshes.ply" ); + } + catch ( final NullPointerException npe ) + { + file = new File( folder.getPath() + File.separator + "TrackMateMeshes.ply" ); + } + file = IOUtils.askForFileForSaving( file, parent ); + if ( null == file ) + { + logger.log( "Aborted.\n" ); + return; + } + + exportMeshesToFileSeries( model.getSpots(), file, logger ); + } + + public static void exportMeshesToFileSeries( final SpotCollection spots, final File file, final Logger logger ) + { + String folderName = file.getAbsolutePath(); + folderName = folderName.substring( 0, folderName.indexOf( "." ) ); + final File folder = new File( folderName ); + folder.mkdirs(); + + final NavigableSet< Integer > frames = spots.keySet(); + for ( final Integer frame : frames ) + { + final String fileName = folder.getName() + '_' + frame + ".ply"; + final File targetFile = new File( folder, fileName ); + final List< Mesh > meshes = new ArrayList<>(); + for ( final Spot spot : spots.iterable( frame, true ) ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + meshes.add( TranslateMesh.translate( sm.getMesh(), spot ) ); + } + } + logger.log( " - Found " + meshes.size() + " meshes in frame " + frame + "." ); + final Mesh merged = Meshes.merge( meshes ); + final BufferMesh mesh = new BufferMesh( merged.vertices().size(), merged.triangles().size() ); + Meshes.calculateNormals( merged, mesh ); + try + { + PLYMeshIO.save( mesh, targetFile.getAbsolutePath() ); + } + catch ( final IOException e ) + { + logger.error( "\nProblem writing to " + targetFile + '\n' + e.getMessage() + '\n' ); + e.printStackTrace(); + continue; + } + logger.log( " Saved.\n" ); + } + logger.log( "Done. Meshes saved to folder " + folder + '\n' ); + } + + @Plugin( type = TrackMateActionFactory.class, visible = true ) + public static class Factory implements TrackMateActionFactory + { + + @Override + public String getInfoText() + { + return INFO_TEXT; + } + + @Override + public String getName() + { + return NAME; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public ImageIcon getIcon() + { + return ORANGE_ASTERISK_ICON; + } + + @Override + public TrackMateAction create() + { + return new MeshSeriesExporter(); + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java b/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java index c7838ff14..d9c6dc335 100644 --- a/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java +++ b/src/main/java/fiji/plugin/trackmate/action/TrackMateAction.java @@ -54,6 +54,9 @@ public interface TrackMateAction /** * Sets the logger that will receive logs when this action is executed. + * + * @param logger + * the logger. */ public void setLogger( Logger logger ); } diff --git a/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java b/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java index 17b8d2b15..90e73da00 100644 --- a/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java +++ b/src/main/java/fiji/plugin/trackmate/action/closegaps/GapClosingMethod.java @@ -33,6 +33,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.detection.DetectionUtils; @@ -87,7 +88,9 @@ public default List< GapClosingParameter > getParameters() * Performs the gap closing. * * @param trackmate + * the trackmate instance to operate on. * @param logger + * a logger instance to echoes the gap-closing process. */ public void execute( TrackMate trackmate, Logger logger ); @@ -173,7 +176,7 @@ public static List< Spot > interpolate( final Model model, final DefaultWeighted position[ d ] = weight * sPos[ d ] + ( 1.0 - weight ) * tPos[ d ]; final RealPoint rp = new RealPoint( position ); - final Spot newSpot = new Spot( rp, 0, 0 ); + final Spot newSpot = new SpotBase( rp, 0, 0 ); newSpot.putFeature( Spot.FRAME, Double.valueOf( f ) ); // Set some properties of the new spot diff --git a/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java b/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java index d329725e3..35588e86c 100644 --- a/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java +++ b/src/main/java/fiji/plugin/trackmate/action/fit/AbstractSpotFitter.java @@ -69,7 +69,6 @@ public abstract class AbstractSpotFitter implements SpotFitter private long processingTime = -1; - @SuppressWarnings( "unchecked" ) public AbstractSpotFitter( final ImagePlus imp, final int channel ) { this.channel = channel; diff --git a/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java b/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java index c432dc5ce..38c20c166 100644 --- a/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/action/fit/SpotFitterPanel.java @@ -200,9 +200,9 @@ public SpotFitterPanel( final List< String > fits, final List< String > docs, fi } /** - * 1-based. + * Returns the selected channel. 1-based. * - * @return + * @return the selected channel. */ public int getSelectedChannel() { diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmoother.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmoother.java new file mode 100644 index 000000000..dcfe73f39 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmoother.java @@ -0,0 +1,203 @@ +/*- + * #%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.action.meshtools; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.util.Threads; +import net.imglib2.algorithm.MultiThreaded; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.alg.TaubinSmoothing; +import net.imglib2.mesh.alg.TaubinSmoothing.TaubinWeightType; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.util.ValuePair; + +public class MeshSmoother implements MultiThreaded +{ + + private static final long TIME_OUT_DELAY = 2; + + private static final TimeUnit TIME_OUT_UNITS = TimeUnit.HOURS; + + /** Stores initial position and mesh of the spot. */ + + private final ConcurrentHashMap< SpotMesh, ValuePair< BufferMesh, double[] > > undoMap; + + private final Logger logger; + + private int numThreads; + + + public MeshSmoother( final Logger logger ) + { + this.logger = logger; + this.undoMap = new ConcurrentHashMap<>(); + setNumThreads(); + } + + + public List< Spot > undo() + { + logger.setStatus( "Undoing mesh smoothing" ); + final Set< SpotMesh > keys = undoMap.keySet(); + final int nSpots = keys.size(); + int i = 0; + logger.log( "Undoing mesh smoothing for " + nSpots + " spots.\n" ); + final List< Spot > modifiedSpots = new ArrayList<>(); + for ( final SpotMesh sm : keys ) + { + final ValuePair< BufferMesh, double[] > old = undoMap.get( sm ); + sm.setMesh( old.getA() ); + sm.setPosition( old.getB() ); + modifiedSpots.add( sm ); + logger.setProgress( ( double ) ( ++i ) / nSpots ); + } + logger.setStatus( "" ); + logger.log( "Done.\n" ); + return modifiedSpots; + } + + public List< Spot > smooth( final MeshSmootherModel smootherModel, final Iterable< Spot > spots ) + { + final double mu = smootherModel.getMu(); + final double lambda = smootherModel.getLambda(); + final int nIters = smootherModel.getNIters(); + final TaubinWeightType weightType = smootherModel.getWeightType(); + + final int nSpots = count( spots ); + logger.setStatus( "Taubin smoothing" ); + logger.log( "Started Taubin smoothing over " + nSpots + " spots with parameters:\n" ); + logger.log( String.format( " - %s: %.2f\n", "µ", mu ) ); + logger.log( String.format( " - %s: %.2f\n", "λ", lambda ) ); + logger.log( String.format( " - %s: %d\n", "N iterations", nIters ) ); + logger.log( String.format( " - %s: %s\n", "weights", weightType ) ); + + final AtomicInteger ai = new AtomicInteger( 0 ); + final ExecutorService executors = Threads.newFixedThreadPool( numThreads ); + final List< Spot > modifiedSpots = new ArrayList<>(); + for ( final Spot spot : spots ) + { + if ( SpotMesh.class.isInstance( spot ) ) + { + final SpotMesh sm = ( SpotMesh ) spot; + executors.execute( process( sm, nIters, mu, lambda, weightType, ai, nSpots ) ); + modifiedSpots.add( sm ); + } + } + + executors.shutdown(); + try + { + final boolean ok = executors.awaitTermination( TIME_OUT_DELAY, TIME_OUT_UNITS ); + if ( !ok ) + logger.error( "Timeout of " + TIME_OUT_DELAY + " " + TIME_OUT_UNITS + " reached while smoothing.\n" ); + + logger.log( "Done.\n" ); + } + catch ( final InterruptedException e ) + { + logger.error( e.getMessage() ); + e.printStackTrace(); + } + finally + { + logger.setProgress( 1 ); + logger.setStatus( "" ); + } + return modifiedSpots; + } + + private static final int count( final Iterable< Spot > spots ) + { + if ( Collection.class.isInstance( spots ) ) + return ( ( Collection< ? > ) spots ).size(); + + int n = 0; + for ( @SuppressWarnings( "unused" ) + final Spot spot : spots ) + n++; + return n; + } + + private Runnable process( + final SpotMesh sm, + final int nIters, + final double mu, + final double lambda, + final TaubinWeightType weightType, + final AtomicInteger ai, + final int nSpots ) + { + return new Runnable() + { + @Override + public void run() + { + final BufferMesh mesh = sm.getMesh(); + final double[] center = new double[ 3 ]; + sm.localize( center ); + + // Store for undo. + if ( !undoMap.containsKey( sm ) ) + { + final ValuePair< BufferMesh, double[] > pair = new ValuePair<>( mesh, center ); + undoMap.put( sm, pair ); + } + + // Process. + Meshes.translate( mesh, center ); + final BufferMesh smoothedMesh = TaubinSmoothing.smooth( mesh, nIters, lambda, mu, weightType ); + sm.setMesh( smoothedMesh ); + + logger.setProgress( ( double ) ai.incrementAndGet() / nSpots ); + } + }; + } + + @Override + public void setNumThreads() + { + this.numThreads = Math.max( 1, Runtime.getRuntime().availableProcessors() / 2 ); + } + + @Override + public void setNumThreads( final int numThreads ) + { + this.numThreads = numThreads; + } + + @Override + public int getNumThreads() + { + return numThreads; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherAction.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherAction.java new file mode 100644 index 000000000..4fdc0965a --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherAction.java @@ -0,0 +1,92 @@ +/*- + * #%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.action.meshtools; + +import java.awt.Frame; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.action.AbstractTMAction; +import fiji.plugin.trackmate.action.TrackMateAction; +import fiji.plugin.trackmate.action.TrackMateActionFactory; +import fiji.plugin.trackmate.gui.Icons; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; + +public class MeshSmootherAction extends AbstractTMAction +{ + + @Override + public void execute( final TrackMate trackmate, final SelectionModel selectionModel, final DisplaySettings displaySettings, final Frame parent ) + { + final MeshSmootherController controller = new MeshSmootherController( trackmate.getModel(), selectionModel, logger ); + controller.setNumThreads( trackmate.getNumThreads() ); + controller.show( parent ); + } + + @Plugin( type = TrackMateActionFactory.class ) + public static class Factory implements TrackMateActionFactory + { + + public static final String NAME = "Smooth 3D meshes"; + + public static final String KEY = "MESH_SMOOTHER"; + + public static final String INFO_TEXT = "" + + "Displays a tool to smooth the 3D mesh present in " + + "the data, using the Taubin smoothing algorithm."; + + @Override + public String getInfoText() + { + return INFO_TEXT; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public TrackMateAction create() + { + return new MeshSmootherAction(); + } + + @Override + public ImageIcon getIcon() + { + return Icons.VECTOR_ICON; + } + + @Override + public String getName() + { + return NAME; + } + } + +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherController.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherController.java new file mode 100644 index 000000000..25da09913 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherController.java @@ -0,0 +1,146 @@ +/*- + * #%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.action.meshtools; + +import java.awt.Component; +import java.util.Collection; + +import javax.swing.JFrame; +import javax.swing.JLabel; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.gui.GuiUtils; +import fiji.plugin.trackmate.gui.Icons; +import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; +import net.imglib2.algorithm.MultiThreaded; + +public class MeshSmootherController implements MultiThreaded +{ + + private final Model model; + + private final SelectionModel selectionModel; + + private final MeshSmootherPanel gui; + + private final MeshSmoother smoother; + + private final Logger logger; + + public MeshSmootherController( final Model model, final SelectionModel selectionModel, final Logger logger ) + { + this.model = model; + this.selectionModel = selectionModel; + this.logger = logger; + this.gui = new MeshSmootherPanel(); + this.smoother = new MeshSmoother( logger ); + + gui.btnRun.addActionListener( e -> run( gui.getModel() ) ); + gui.btnUndo.addActionListener( e -> undo() ); + } + + public void show( final Component parent ) + { + final JFrame frame = new JFrame( "Smoothing params" ); + frame.getContentPane().add( gui ); + frame.setSize( 400, 300 ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); + GuiUtils.positionWindow( frame, parent ); + frame.setVisible( true ); + } + + private void run( final MeshSmootherModel smootherModel ) + { + final Iterable< Spot > spots; + if ( gui.rdbtnAll.isSelected() ) + spots = model.getSpots().iterable( true ); + else + spots = selectionModel.getSpotSelection(); + + new Thread( () -> { + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( gui, new Class[] { JLabel.class } ); + try + { + enabler.disable(); + final Collection< Spot > modifiedSpots = smoother.smooth( smootherModel, spots ); + fireEvent( modifiedSpots ); + } + catch ( final Exception err ) + { + err.printStackTrace(); + } + finally + { + enabler.reenable(); + } + }, "TrackMate mesh smoother thread" ).start(); + } + + private void undo() + { + new Thread( () -> { + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( gui, new Class[] { JLabel.class } ); + try + { + enabler.disable(); + final Collection< Spot > modifiedSpots = smoother.undo(); + fireEvent( modifiedSpots ); + } + finally + { + enabler.reenable(); + } + }, "TrackMate mesh smoothing undoer thread" ).start(); + } + + private void fireEvent( final Collection< Spot > modifiedSpots ) + { + logger.log( "Updating spot features and meshes.\n" ); + final ModelChangeEvent event = new ModelChangeEvent( this, ModelChangeEvent.MODEL_MODIFIED ); + event.addAllSpots( modifiedSpots ); + modifiedSpots.forEach( s -> event.putSpotFlag( s, ModelChangeEvent.FLAG_SPOT_MODIFIED ) ); + model.getModelChangeListener().forEach( l -> l.modelChanged( event ) ); + logger.log( "Done.\n" ); + } + + @Override + public void setNumThreads() + { + smoother.setNumThreads(); + } + + @Override + public void setNumThreads( final int numThreads ) + { + smoother.setNumThreads( numThreads ); + } + + @Override + public int getNumThreads() + { + return smoother.getNumThreads(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherModel.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherModel.java new file mode 100644 index 000000000..abc4ac371 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherModel.java @@ -0,0 +1,100 @@ +/*- + * #%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.action.meshtools; + +import net.imglib2.mesh.alg.TaubinSmoothing.TaubinWeightType; + +public class MeshSmootherModel +{ + + private int nIters = 10; + + private double mu = 0.5; + + private double lambda = -0.53; + + private TaubinWeightType weightType = TaubinWeightType.NAIVE; + + public void setWeightType( final TaubinWeightType weightType ) + { + this.weightType = weightType; + } + + public void setMu( final double mu ) + { + this.mu = Math.min( 1., Math.max( 0, mu ) ); + } + + public void setLambda( final double lambda ) + { + this.lambda = Math.min( 0., Math.max( -1., lambda ) ); + } + + public void setNIters( final int nIters ) + { + this.nIters = Math.max( 0, nIters ); + } + + public double getMu() + { + return mu; + } + + public double getLambda() + { + return lambda; + } + + public int getNIters() + { + return nIters; + } + + public TaubinWeightType getWeightType() + { + return weightType; + } + + /** + * Ad-hoc method setting parameters for little smoothing (close to 0) or a + * lot of smoothing (close to 1). + * + * @param smoothing + * the smoothing parameter. + */ + public void setSmoothing( final double smoothing ) + { + setMu( Math.max( 0, Math.min( 0.97, smoothing ) ) ); + setLambda( -mu - 0.03 ); + } + + @Override + public String toString() + { + final StringBuilder str = new StringBuilder( super.toString() + '\n' ); + str.append( String.format( " - %s: %.2f\n", "µ", mu ) ); + str.append( String.format( " - %s: %.2f\n", "λ", lambda ) ); + str.append( String.format( " - %s: %d\n", "N iterations", nIters ) ); + str.append( String.format( " - %s: %s\n", "weights", weightType ) ); + return str.toString(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherPanel.java b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherPanel.java new file mode 100644 index 000000000..4987a6f3b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/action/meshtools/MeshSmootherPanel.java @@ -0,0 +1,255 @@ +/*- + * #%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.action.meshtools; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.Arrays; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTabbedPane; + +import fiji.plugin.trackmate.gui.displaysettings.SliderPanel; +import fiji.plugin.trackmate.gui.displaysettings.SliderPanelDouble; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.EnumElement; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.IntElement; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StyleElement; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.StyleElementVisitor; +import net.imglib2.mesh.alg.TaubinSmoothing.TaubinWeightType; + +public class MeshSmootherPanel extends JPanel +{ + + private static final long serialVersionUID = 1L; + + final JButton btnRun; + + final JButton btnUndo; + + final JRadioButton rdbtnSelection; + + final JRadioButton rdbtnAll; + + private final MeshSmootherModel modelBasic; + + private final MeshSmootherModel modelSimple; + + private final MeshSmootherModel modelAdvanced; + + private final JTabbedPane mainPanel; + + public MeshSmootherPanel() + { + this.modelBasic = new MeshSmootherModel(); + modelBasic.setMu( 1. ); + modelBasic.setLambda( 0. ); + modelBasic.setWeightType( TaubinWeightType.NAIVE ); + final IntElement nItersBasic = StyleElements.intElement( "N iterations", 1, 50, modelBasic::getNIters, modelBasic::setNIters ); + final List< StyleElement > superSimpleElements = Arrays.asList( nItersBasic ); + + this.modelSimple = new MeshSmootherModel(); + final BoundedDoubleElement smoothing = StyleElements.boundedDoubleElement( "Smoothing (%)", 0., 100., () -> modelSimple.getMu() * 100., v -> modelSimple.setSmoothing( v / 100. ) ); + final IntElement nItersSimple = StyleElements.intElement( "N iterations", 1, 50, modelSimple::getNIters, modelSimple::setNIters ); + final List< StyleElement > simpleElements = Arrays.asList( smoothing, nItersSimple ); + + this.modelAdvanced = new MeshSmootherModel(); + final BoundedDoubleElement mu = StyleElements.boundedDoubleElement( "µ", 0., 1., modelAdvanced::getMu, modelAdvanced::setMu ); + final BoundedDoubleElement lambda = StyleElements.boundedDoubleElement( "-λ", 0., 1., () -> -modelAdvanced.getLambda(), l -> modelAdvanced.setLambda( -l ) ); + final EnumElement< TaubinWeightType > weightType = StyleElements.enumElement( "weight type", TaubinWeightType.values(), modelAdvanced::getWeightType, modelAdvanced::setWeightType ); + final IntElement nItersAdvanced = StyleElements.intElement( "N iterations", 1, 50, modelAdvanced::getNIters, modelAdvanced::setNIters ); + final List< StyleElement > advancedElements = Arrays.asList( mu, lambda, nItersAdvanced, weightType ); + + setLayout( new BorderLayout( 0, 0 ) ); + + final JPanel bottomPanel = new JPanel(); + add( bottomPanel, BorderLayout.SOUTH ); + bottomPanel.setLayout( new BoxLayout( bottomPanel, BoxLayout.Y_AXIS ) ); + + final JPanel selectionPanel = new JPanel(); + selectionPanel.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + bottomPanel.add( selectionPanel ); + selectionPanel.setLayout( new BoxLayout( selectionPanel, BoxLayout.X_AXIS ) ); + + final JLabel lblRunOn = new JLabel( "Run on:" ); + selectionPanel.add( lblRunOn ); + + selectionPanel.add( Box.createHorizontalGlue() ); + + rdbtnSelection = new JRadioButton( "selection only" ); + selectionPanel.add( rdbtnSelection ); + + rdbtnAll = new JRadioButton( "all visible spots" ); + selectionPanel.add( rdbtnAll ); + + final JPanel buttonPanel = new JPanel(); + bottomPanel.add( buttonPanel ); + buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.X_AXIS ) ); + + this.btnUndo = new JButton( "Undo" ); + buttonPanel.add( btnUndo ); + + buttonPanel.add( Box.createHorizontalGlue() ); + + this.btnRun = new JButton( "Run" ); + buttonPanel.add( btnRun ); + + this.mainPanel = new JTabbedPane( JTabbedPane.TOP ); + add( mainPanel, BorderLayout.CENTER ); + + final JPanel panelSuperSimple = new JPanel(); + panelSuperSimple.setOpaque( false ); + panelSuperSimple.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + final MyStyleElementVisitors superSimplePanelVisitor = new MyStyleElementVisitors( panelSuperSimple ); + superSimpleElements.forEach( el -> el.accept( superSimplePanelVisitor ) ); + mainPanel.addTab( "Basic", null, panelSuperSimple, null ); + + final JPanel panelSimple = new JPanel(); + panelSimple.setOpaque( false ); + panelSimple.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + final MyStyleElementVisitors simplePanelVisitor = new MyStyleElementVisitors( panelSimple ); + simpleElements.forEach( el -> el.accept( simplePanelVisitor ) ); + mainPanel.addTab( "Simple", null, panelSimple, null ); + + final JPanel panelAdvanced = new JPanel(); + panelAdvanced.setOpaque( false ); + panelAdvanced.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); + final MyStyleElementVisitors advancedPanelVisitor = new MyStyleElementVisitors( panelAdvanced ); + advancedElements.forEach( el -> el.accept( advancedPanelVisitor ) ); + mainPanel.addTab( "Advanced", null, panelAdvanced, null ); + + final ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add( rdbtnAll ); + buttonGroup.add( rdbtnSelection ); + rdbtnSelection.setSelected( true ); + } + + public MeshSmootherModel getModel() + { + switch ( mainPanel.getSelectedIndex() ) + { + case 0: + return modelBasic; + case 1: + return modelSimple; + case 2: + return modelAdvanced; + } + throw new IllegalStateException( "Cannot handle mesh smoothing settings type number " + ( mainPanel.getSelectedIndex() ) ); + } + + private static class MyStyleElementVisitors implements StyleElementVisitor + { + + private final JPanel panel; + + private final GridBagConstraints gbcs; + + public MyStyleElementVisitors( final JPanel panel ) + { + this.panel = panel; + final GridBagLayout layout = new GridBagLayout(); + layout.columnWidths = new int[] { 0, 0, 0 }; + layout.rowHeights = new int[] { 40, 40, 40, 40 }; + layout.columnWeights = new double[] { 0., 1., Double.MIN_VALUE }; + layout.rowWeights = new double[] { 0., 0., 0., 0., 1. }; + panel.setLayout( layout ); + + this.gbcs = new GridBagConstraints(); + gbcs.fill = GridBagConstraints.HORIZONTAL; + gbcs.gridx = 0; + gbcs.gridy = 0; + } + + @Override + public < E > void visit( final EnumElement< E > el ) + { + gbcs.gridx = 0; + final JLabel lbl = new JLabel( el.getLabel() ); + lbl.setHorizontalAlignment( JLabel.RIGHT ); + lbl.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + panel.add( lbl, gbcs ); + gbcs.gridx++; + panel.add( StyleElements.linkedComboBoxEnumSelector( el ), gbcs ); + gbcs.gridy++; + } + + @Override + public void visit( final BoundedDoubleElement el ) + { + gbcs.gridx = 0; + final JLabel lbl = new JLabel( el.getLabel() ); + lbl.setHorizontalAlignment( JLabel.RIGHT ); + lbl.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + panel.add( lbl, gbcs ); + gbcs.gridx++; + final SliderPanelDouble sliderPanel = StyleElements.linkedSliderPanel( el, 3 ); + sliderPanel.setOpaque( false ); + panel.add( sliderPanel, gbcs ); + gbcs.gridy++; + } + + @Override + public void visit( final IntElement el ) + { + gbcs.gridx = 0; + final JLabel lbl = new JLabel( el.getLabel() ); + lbl.setHorizontalAlignment( JLabel.RIGHT ); + lbl.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + panel.add( lbl, gbcs ); + gbcs.gridx++; + final SliderPanel sliderPanel = StyleElements.linkedSliderPanel( el, 3 ); + sliderPanel.setOpaque( false ); + panel.add( sliderPanel, gbcs ); + gbcs.gridy++; + } + + private Font getFont() + { + return panel.getFont(); + } + } + + public static void main( final String[] args ) + { + final MeshSmootherPanel panel = new MeshSmootherPanel(); + + final JFrame frame = new JFrame( "Smoothing params" ); + frame.getContentPane().add( panel ); + frame.setSize( 400, 300 ); + frame.setLocationRelativeTo( null ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + frame.setVisible( true ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java index c73f70e5b..9dcb2630a 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java @@ -37,6 +37,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.util.MedianFilter2D; @@ -272,6 +273,8 @@ public static final Img< FloatType > createLoGKernel( final double radius, final * @return a new float Img. Careful: even if the specified interval does not * start at (0, 0), the new image will have its first pixel at * coordinates (0, 0). + * @param + * the pixel type of the input image. */ public static final < T extends RealType< T > > Img< FloatType > copyToFloatImg( final RandomAccessible< T > img, final Interval interval, final ImgFactory< FloatType > factory ) { @@ -324,7 +327,14 @@ public static final Interval squeeze( final Interval interval ) } /** - * Apply a simple 3x3 median filter to the target image. + * Applies a simple 3x3 median filter to the target image. + * + * @param + * the pixel type in the image. + * @param image + * the image to filter. + * @return the filtered image, as a new image, or null if there + * was a problem during processing. */ public static final < R extends RealType< R > & NativeType< R > > Img< R > applyMedianFilter( final RandomAccessibleInterval< R > image ) { @@ -410,7 +420,7 @@ public static final < T extends RealType< T > > List< Spot > findLocalMaxima( final double x = refinedPeak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = refinedPeak.getDoublePosition( 1 ) * calibration[ 1 ]; final double z = refinedPeak.getDoublePosition( 2 ) * calibration[ 2 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -423,7 +433,7 @@ else if ( source.numDimensions() > 1 ) final double quality = ra.get().getRealDouble(); final double x = refinedPeak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = refinedPeak.getDoublePosition( 1 ) * calibration[ 1 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -436,7 +446,7 @@ else if ( source.numDimensions() > 1 ) ra.setPosition( refinedPeak.getOriginalPeak() ); final double quality = ra.get().getRealDouble(); final double x = refinedPeak.getDoublePosition( 0 ) * calibration[ 0 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } @@ -455,7 +465,7 @@ else if ( source.numDimensions() > 1 ) final double x = peak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = peak.getDoublePosition( 1 ) * calibration[ 1 ]; final double z = peak.getDoublePosition( 2 ) * calibration[ 2 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -468,7 +478,7 @@ else if ( source.numDimensions() > 1 ) final double quality = ra.get().getRealDouble(); final double x = peak.getDoublePosition( 0 ) * calibration[ 0 ]; final double y = peak.getDoublePosition( 1 ) * calibration[ 1 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } } @@ -481,10 +491,9 @@ else if ( source.numDimensions() > 1 ) ra.setPosition( peak ); final double quality = ra.get().getRealDouble(); final double x = peak.getDoublePosition( 0 ) * calibration[ 0 ]; - final Spot spot = new Spot( x, y, z, radius, quality ); + final Spot spot = new SpotBase( x, y, z, radius, quality ); spots.add( spot ); } - } } diff --git a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java index 3c54dceef..e341bd667 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 * . @@ -73,6 +73,8 @@ public class LabelImageDetector< T extends RealType< T > & NativeType< T > > imp */ protected final boolean simplify; + private final double smoothingScale; + /* * CONSTRUCTORS */ @@ -81,12 +83,14 @@ public LabelImageDetector( final RandomAccessible< T > input, final Interval interval, final double[] calibration, - final boolean simplify ) + final boolean simplify, + final double smoothingScale ) { this.input = input; this.interval = DetectionUtils.squeeze( interval ); this.calibration = calibration; this.simplify = simplify; + this.smoothingScale = smoothingScale; } @Override @@ -110,9 +114,9 @@ public boolean process() final ImgFactory< IntType > factory = Util.getArrayOrCellImgFactory( interval, new IntType() ); final Img< IntType > img = factory.create( interval ); LoopBuilder - .setImages( Views.zeroMin( rai ), img ) - .multiThreaded( false ) - .forEachPixel( ( i, o ) -> o.setReal( i.getRealDouble() ) ); + .setImages( Views.zeroMin( rai ), img ) + .multiThreaded( false ) + .forEachPixel( ( i, o ) -> o.setReal( i.getRealDouble() ) ); processIntegerImg( img ); } final long end = System.currentTimeMillis(); @@ -135,9 +139,31 @@ private < R extends IntegerType< R > > void processIntegerImg( final RandomAcces final ImgLabeling< Integer, R > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); if ( input.numDimensions() == 2 ) - spots = MaskUtils.fromLabelingWithROI( labeling, interval, calibration, simplify, null ); + { + spots = SpotRoiUtils.from2DLabelingWithROI( + labeling, + interval.minAsDoubleArray(), + calibration, + simplify, + smoothingScale, + null ); + } + else if ( input.numDimensions() == 3 ) + { + spots = SpotMeshUtils.from3DLabelingWithROI( + labeling, + interval.minAsDoubleArray(), + calibration, + simplify, + smoothingScale, + null ); + } else - spots = MaskUtils.fromLabeling( labeling, interval, calibration ); + { + throw new IllegalArgumentException( BASE_ERROR_MESSAGE + "Can only process 2D or 3D images. Got a " + + input.numDimensions() + "D image over: " + + Util.printInterval( interval ) ); + } } @Override diff --git a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java index 13738452a..e9d79ee37 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetectorFactory.java @@ -24,11 +24,13 @@ import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SMOOTHING_SCALE; import static fiji.plugin.trackmate.io.IOUtils.readBooleanAttribute; import static fiji.plugin.trackmate.io.IOUtils.readIntegerAttribute; import static fiji.plugin.trackmate.io.IOUtils.writeAttribute; import static fiji.plugin.trackmate.io.IOUtils.writeTargetChannel; import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; +import static fiji.plugin.trackmate.util.TMUtils.checkOptionalParameter; import static fiji.plugin.trackmate.util.TMUtils.checkParameter; import java.util.ArrayList; @@ -75,8 +77,8 @@ public class LabelImageDetectorFactory< T extends RealType< T > & NativeType< T + "that is unique to the object." + "

" + "This detector reads such an image and create spots from each object. " - + "In 2D the contour of a label is imported. In 3D, spherical spots " - + "of the same volume that the label are created." + + "In 2D the contour of a label is imported. In 3D, a mesh around the " + + "label is imported." + "

" + "The spot quality stores the object area or volume in pixels." + ""; @@ -103,11 +105,12 @@ public boolean setTarget( final ImgPlus< T > img, final Map< String, Object > se this.settings = settings; return checkSettings( settings ); } - + @Override public SpotDetector< T > getDetector( final Interval interval, final int frame ) { final boolean simplifyContours = ( Boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final double smoothingScale = ( Double ) settings.get( KEY_SMOOTHING_SCALE ); final double[] calibration = TMUtils.getSpatialCalibration( img ); final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1; final RandomAccessible< T > imFrame = DetectionUtils.prepareFrameImg( img, channel, frame ); @@ -116,7 +119,8 @@ public SpotDetector< T > getDetector( final Interval interval, final int frame ) imFrame, interval, calibration, - simplifyContours ); + simplifyContours, + smoothingScale ); return detector; } @@ -126,6 +130,12 @@ public boolean has2Dsegmentation() return true; } + @Override + public boolean has3Dsegmentation() + { + return true; + } + @Override public String getKey() { @@ -145,14 +155,16 @@ public boolean checkSettings( final Map< String, Object > lSettings ) final StringBuilder errorHolder = new StringBuilder(); ok = ok & checkParameter( lSettings, KEY_TARGET_CHANNEL, Integer.class, errorHolder ); ok = ok & checkParameter( lSettings, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + ok = ok & checkOptionalParameter( lSettings, KEY_SMOOTHING_SCALE, Double.class, errorHolder ); final List< String > mandatoryKeys = new ArrayList<>(); mandatoryKeys.add( KEY_TARGET_CHANNEL ); mandatoryKeys.add( KEY_SIMPLIFY_CONTOURS ); - ok = ok & checkMapKeys( lSettings, mandatoryKeys, null, errorHolder ); + final List< String > optionalKeys = new ArrayList<>(); + optionalKeys.add( KEY_SMOOTHING_SCALE ); + ok = ok & checkMapKeys( lSettings, mandatoryKeys, optionalKeys, errorHolder ); if ( !ok ) - { errorMessage = errorHolder.toString(); - } + return ok; } @@ -209,6 +221,7 @@ public Map< String, Object > getDefaultSettings() final Map< String, Object > lSettings = new HashMap<>(); lSettings.put( KEY_TARGET_CHANNEL, DEFAULT_TARGET_CHANNEL ); lSettings.put( KEY_SIMPLIFY_CONTOURS, true ); + lSettings.put( KEY_SMOOTHING_SCALE, -1. ); return lSettings; } diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskDetector.java b/src/main/java/fiji/plugin/trackmate/detection/MaskDetector.java new file mode 100644 index 000000000..43141e3ef --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskDetector.java @@ -0,0 +1,66 @@ +/*- + * #%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.detection; + +import net.imglib2.Interval; +import net.imglib2.RandomAccessible; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +public class MaskDetector< T extends RealType< T > & NativeType< T > > extends ThresholdDetector< T > +{ + + private final static String BASE_ERROR_MESSAGE = "MaskDetector: "; + + /* + * CONSTRUCTORS + */ + + public MaskDetector( + final RandomAccessible< T > input, + final Interval interval, + final double[] calibration, + final boolean simplify, + final double smoothingScale ) + { + super( input, interval, calibration, Double.NaN, simplify, smoothingScale ); + baseErrorMessage = BASE_ERROR_MESSAGE; + } + + + @Override + public boolean process() + { + final long start = System.currentTimeMillis(); + spots = MaskUtils.fromMaskWithROI( + input, + interval, + calibration, + simplify, + smoothingScale, + numThreads, + null ); + final long end = System.currentTimeMillis(); + this.processingTime = end - start; + return true; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java index 54675018a..eaed9e990 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/MaskDetectorFactory.java @@ -24,10 +24,12 @@ import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_TARGET_CHANNEL; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; import static fiji.plugin.trackmate.io.IOUtils.readBooleanAttribute; +import static fiji.plugin.trackmate.io.IOUtils.readDoubleAttribute; import static fiji.plugin.trackmate.io.IOUtils.readIntegerAttribute; import static fiji.plugin.trackmate.io.IOUtils.writeAttribute; import static fiji.plugin.trackmate.io.IOUtils.writeTargetChannel; import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; +import static fiji.plugin.trackmate.util.TMUtils.checkOptionalParameter; import static fiji.plugin.trackmate.util.TMUtils.checkParameter; import java.util.ArrayList; @@ -47,6 +49,8 @@ import fiji.plugin.trackmate.util.TMUtils; import net.imglib2.Interval; import net.imglib2.RandomAccessible; +import net.imglib2.converter.Converter; +import net.imglib2.converter.Converters; import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; @@ -72,9 +76,8 @@ public class MaskDetectorFactory< T extends RealType< T > & NativeType< T > > ex + "a value strictly larger than 0 are " + "considered as part of the foreground, " + "and used to build connected regions. In 2D, spots are created with " - + "the (possibly simplified) contour of the region. In 3D, a spherical " - + "spot is created for each region in its center, with a volume equal to the " - + "region volume." + + "the (possibly simplified) contour of the region. In 3D, a mesh is " + + "created for each region." + "

" + "The spot quality stores the object area or volume in pixels." + ""; @@ -85,25 +88,54 @@ public boolean has2Dsegmentation() return true; } + @Override + public boolean has3Dsegmentation() + { + return true; + } + @Override public SpotDetector< T > getDetector( final Interval interval, final int frame ) { - final double intensityThreshold = 0.; final boolean simplifyContours = ( Boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final double smoothingScale = ( Double ) settings.get( KEY_SMOOTHING_SCALE ); final double[] calibration = TMUtils.getSpatialCalibration( img ); final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1; final RandomAccessible< T > imFrame = DetectionUtils.prepareFrameImg( img, channel, frame ); + final RandomAccessible< T > mask = mask( imFrame ); - final ThresholdDetector< T > detector = new ThresholdDetector<>( - imFrame, + final MaskDetector< T > detector = new MaskDetector<>( + mask, interval, calibration, - intensityThreshold, - simplifyContours ); + simplifyContours, + smoothingScale ); + detector.setNumThreads( 1 ); return detector; } + /** + * Return a view of the input image where all pixels with values strictly + * larger than 0 are set to 1, and set to 0 otherwise. + * + * @param input + * the image to wrap. + * @return a view of the image. + */ + protected RandomAccessible< T > mask( final RandomAccessible< T > input ) + { + final Converter< T, T > c = new Converter< T, T >() + { + @Override + public void convert( final T input, final T output ) + { + output.setReal( input.getRealDouble() > 0. ? 1. : 0. ); + } + }; + return Converters.convert( input, c, img.firstElement().createVariable() ); + } + @Override public String getKey() { @@ -117,10 +149,13 @@ public boolean checkSettings( final Map< String, Object > lSettings ) final StringBuilder errorHolder = new StringBuilder(); ok = ok & checkParameter( lSettings, KEY_TARGET_CHANNEL, Integer.class, errorHolder ); ok = ok & checkParameter( lSettings, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + ok = ok & checkOptionalParameter( lSettings, KEY_SMOOTHING_SCALE, Double.class, errorHolder ); final List< String > mandatoryKeys = new ArrayList<>(); mandatoryKeys.add( KEY_TARGET_CHANNEL ); mandatoryKeys.add( KEY_SIMPLIFY_CONTOURS ); - ok = ok & checkMapKeys( lSettings, mandatoryKeys, null, errorHolder ); + final List< String > optionalKeys = new ArrayList<>(); + optionalKeys.add( KEY_SMOOTHING_SCALE ); + ok = ok & checkMapKeys( lSettings, mandatoryKeys, optionalKeys, errorHolder ); if ( !ok ) { errorMessage = errorHolder.toString(); @@ -133,7 +168,8 @@ public boolean marshall( final Map< String, Object > lSettings, final Element el { final StringBuilder errorHolder = new StringBuilder(); final boolean ok = writeTargetChannel( lSettings, element, errorHolder ) - && writeAttribute( lSettings, element, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + && writeAttribute( lSettings, element, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ) + && writeAttribute( lSettings, element, KEY_SMOOTHING_SCALE, Double.class, errorHolder ); if ( !ok ) errorMessage = errorHolder.toString(); @@ -149,6 +185,7 @@ public boolean unmarshall( final Element element, final Map< String, Object > lS boolean ok = true; ok = ok & readIntegerAttribute( element, lSettings, KEY_TARGET_CHANNEL, errorHolder ); ok = ok & readBooleanAttribute( element, lSettings, KEY_SIMPLIFY_CONTOURS, errorHolder ); + ok = ok & readDoubleAttribute( element, lSettings, KEY_SMOOTHING_SCALE, errorHolder ); if ( !ok ) { errorMessage = errorHolder.toString(); @@ -181,6 +218,7 @@ public Map< String, Object > getDefaultSettings() final Map< String, Object > lSettings = new HashMap<>(); lSettings.put( KEY_TARGET_CHANNEL, DEFAULT_TARGET_CHANNEL ); lSettings.put( KEY_SIMPLIFY_CONTOURS, true ); + lSettings.put( KEY_SMOOTHING_SCALE, -1. ); return lSettings; } diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 213e9e424..1a467b580 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 * . @@ -21,29 +21,20 @@ */ package fiji.plugin.trackmate.detection; -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; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.util.SpotUtil; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.util.Threads; -import ij.gui.PolygonRoi; -import ij.process.FloatPolygon; -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; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; +import net.imglib2.algorithm.gauss3.Gauss3; import net.imglib2.algorithm.labeling.ConnectedComponents; import net.imglib2.algorithm.labeling.ConnectedComponents.StructuringElement; import net.imglib2.converter.Converter; @@ -52,15 +43,16 @@ import net.imglib2.histogram.Real1dBinMapper; import net.imglib2.img.Img; import net.imglib2.img.ImgFactory; +import net.imglib2.parallel.Parallelization; import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.roi.labeling.LabelRegion; import net.imglib2.roi.labeling.LabelRegions; -import net.imglib2.type.BooleanType; +import net.imglib2.type.NativeType; 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; +import net.imglib2.type.numeric.real.FloatType; import net.imglib2.util.Util; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; @@ -68,16 +60,6 @@ public class MaskUtils { - /** - * Smoothing interval for ROIs. - */ - private static final double SMOOTH_INTERVAL = 2.; - - /** - * Douglas-Peucker polygon simplification max distance. - */ - private static final double DOUGLAS_PEUCKER_MAX_DISTANCE = 0.5; - public static final < T extends RealType< T > > double otsuThreshold( final RandomAccessibleInterval< T > img ) { // Min & max @@ -116,11 +98,11 @@ public static final long getThreshold( final Histogram1d< ? > hist ) int k, kStar; // k = the current threshold; kStar = optimal threshold final int L = histogram.length; // The total intensity of the image long N1, N; // N1 = # points with intensity <=k; N = total number of - // points + // points long Sk; // The total intensity for all histogram points <=k long S; double BCV, BCVmax; // The current Between Class Variance and maximum - // BCV + // BCV double num, denom; // temporary bookkeeping // Initialize values: @@ -149,15 +131,15 @@ public static final long getThreshold( final Histogram1d< ? > hist ) // precision and // will prevent overflow in the case of large saturated images denom = ( double ) ( N1 ) * ( N - N1 ); // Maximum value of denom is - // (N^2)/4 = - // approx. 3E10 + // (N^2)/4 = + // approx. 3E10 if ( denom != 0 ) { // Float here is to avoid loss of precision when dividing num = ( ( double ) N1 / N ) * S - Sk; // Maximum value of num = - // 255*N = - // approx 8E7 + // 255*N = + // approx 8E7 BCV = ( num * num ) / denom; } else @@ -182,8 +164,6 @@ public static final long getThreshold( final Histogram1d< ? > hist ) * the type of the input image. Must be real, scalar. * @param input * the input image. - * @param interval - * the interval in the input image to analyze. * @param threshold * the threshold to apply to the input image. * @param numThreads @@ -191,20 +171,17 @@ public static final long getThreshold( final Histogram1d< ? > hist ) * @return a new label image. */ public static final < T extends RealType< T > > ImgLabeling< Integer, IntType > toLabeling( - final RandomAccessible< T > input, - final Interval interval, + final RandomAccessibleInterval< T > input, final double threshold, final int numThreads ) { - // Crop. - final IntervalView< T > crop = Views.interval( input, interval ); - final IntervalView< T > in = Views.zeroMin( crop ); + // To mask. final Converter< T, BitType > converter = ( a, b ) -> b.set( a.getRealDouble() > threshold ); - final RandomAccessible< BitType > bitMask = Converters.convertRAI( in, converter, new BitType() ); + final RandomAccessible< BitType > bitMask = Converters.convertRAI( input, converter, new BitType() ); // Prepare output. - final ImgFactory< IntType > factory = Util.getArrayOrCellImgFactory( in, new IntType() ); - final Img< IntType > out = factory.create( in ); + final ImgFactory< IntType > factory = Util.getArrayOrCellImgFactory( input, new IntType() ); + final Img< IntType > out = factory.create( input ); final ImgLabeling< Integer, IntType > labeling = new ImgLabeling<>( out ); // Structuring element. @@ -226,106 +203,15 @@ 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 - * the input image. - * @param interval - * the interval in the input image to analyze. - * @param calibration - * the physical calibration. - * @param threshold - * the threshold to apply to the input image. - * @param numThreads - * how many threads to use for multithreaded computation. - * @return a list of spots, without ROI. - */ - public static < T extends RealType< T > > List< Spot > fromThreshold( - final RandomAccessible< T > input, - final Interval interval, - final double[] calibration, - final double threshold, - final int numThreads ) - { - // Get labeling from mask. - final ImgLabeling< Integer, IntType > labeling = toLabeling( input, interval, threshold, numThreads ); - return fromLabeling( - labeling, - interval, - calibration ); - } - - /** - * Creates spots from a label image. - * - * @param - * the type that backs-up the labeling. - * @param labeling - * the labeling, must be zero-min. - * @param interval - * the interval, used to reposition the spots from the zero-min - * labeling to the proper coordinates. - * @param calibration - * the physical calibration. - * @return a list of spots, without ROI. - */ - public static < R extends IntegerType< R > > List< Spot > fromLabeling( - final ImgLabeling< Integer, R > labeling, - final Interval interval, - final double[] calibration ) - { - // Parse each component. - final LabelRegions< Integer > regions = new LabelRegions<>( labeling ); - final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); - final List< Spot > spots = new ArrayList<>( regions.getExistingLabels().size() ); - while ( iterator.hasNext() ) - { - final LabelRegion< Integer > region = iterator.next(); - final Cursor< BoolType > cursor = region.localizingCursor(); - final int[] cursorPos = new int[ labeling.numDimensions() ]; - final long[] sum = new long[ 3 ]; - while ( cursor.hasNext() ) - { - cursor.fwd(); - cursor.localize( cursorPos ); - for ( int d = 0; d < sum.length; d++ ) - sum[ d ] += cursorPos[ d ]; - } - - final double[] pos = new double[ 3 ]; - for ( int d = 0; d < pos.length; d++ ) - pos[ d ] = sum[ d ] / ( double ) region.size(); - - final double x = calibration[ 0 ] * ( interval.min( 0 ) + pos[ 0 ] ); - final double y = calibration[ 1 ] * ( interval.min( 1 ) + pos[ 1 ] ); - final double z = calibration[ 2 ] * ( interval.min( 2 ) + pos[ 2 ] ); - - double volume = region.size(); - for ( int d = 0; d < calibration.length; d++ ) - if ( calibration[ d ] > 0 ) - volume *= calibration[ d ]; - final double radius = ( labeling.numDimensions() == 2 ) - ? Math.sqrt( volume / Math.PI ) - : Math.pow( 3. * volume / ( 4. * Math.PI ), 1. / 3. ); - final double quality = region.size(); - spots.add( new Spot( x, y, z, radius, quality ) ); - } - - return spots; - } - - /** - * 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 + * Creates spots by thresholding a grayscale image. A spot is created for + * each connected-component object in the thresholded input, 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. + * the pixel type of the input image. Must be real, scalar. + * @param + * the pixel type of the quality image. Must be real, scalar. * @param input * the input image. * @param interval @@ -348,8 +234,15 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > final int numThreads, final RandomAccessibleInterval< R > qualityImage ) { + // Crop. + final IntervalView< T > crop = Views.interval( input, interval ); + final IntervalView< T > in = Views.zeroMin( crop ); + // Get labeling from mask. - final ImgLabeling< Integer, IntType > labeling = toLabeling( input, interval, threshold, numThreads ); + final ImgLabeling< Integer, IntType > labeling = toLabeling( + in, + threshold, + numThreads ); // Crop of the quality image. final IntervalView< R > cropQuality = Views.interval( qualityImage, interval ); @@ -399,343 +292,170 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > final double radius = ( labeling.numDimensions() == 2 ) ? Math.sqrt( volume / Math.PI ) : Math.pow( 3. * volume / ( 4. * Math.PI ), 1. / 3. ); - spots.add( new Spot( x, y, z, radius, quality ) ); + spots.add( new SpotBase( x, y, z, radius, quality ) ); } return spots; } /** - * Creates spots with their ROIs from a 2D 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. - * The quality of the spots is read from another image, by taking the max - * pixel value of this image with the ROI. + * Creates spots with their ROIs or meshes from a 2D or 3D + * mask. A 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 * the type of the quality image. Must be real, scalar. * @param input - * the input image. Must be 2D. + * the input mask image. Can be 2D or 3D. It does not have to be + * of boolean type: every pixel with a real value strictly larger + * than 0.5 will be considered true and + * false otherwise. * @param interval * the interval in the input image to analyze. * @param calibration * the physical calibration. - * @param threshold - * the threshold to apply to the input image. * @param simplify * if true the polygon will be post-processed to be * smoother and contain less points. * @param numThreads * how many threads to use for multithreaded computation. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. * @param qualityImage * 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 RealType< S > > List< Spot > fromThresholdWithROI( - final RandomAccessible< T > input, - final Interval interval, - final double[] calibration, - final double threshold, - final boolean simplify, - final int numThreads, + public static < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > fromMaskWithROI( + final RandomAccessible< T > input, + final Interval interval, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final int numThreads, final RandomAccessibleInterval< S > qualityImage ) { - 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 ); + final double threshold = 0.5; + return fromThresholdWithROI( + input, + interval, + calibration, + threshold, + simplify, + smoothingScale, + numThreads, + qualityImage ); } /** - * 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. + * Creates spots with their ROIs or meshes from a 2D or 3D by + * thresholding a grayscale image. A spot is created for each object in the + * thresholded image. 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 that backs-up the labeling. + * @param + * the type of the input image. Must be real, scalar. * @param * the type of the quality image. Must be real, scalar. - * @param labeling - * the labeling, must be zero-min and 2D.. + * @param input + * the input image. Can be 2D or 3D. * @param interval - * the interval, used to reposition the spots from the zero-min - * labeling to the proper coordinates. + * the interval in the input image to analyze. * @param calibration * the physical calibration. + * @param threshold + * the threshold to apply to the input image. * @param simplify * if true the polygon will be post-processed to be * smoother and contain less points. + * @param smoothingScale + * if strictly larger than 0, the input will be smoothed before + * creating the contour, resulting in smoother contours. The + * scale value sets the (Gaussian) filter radius and is specified + * in physical units. If 0 or lower than 0, no smoothing is + * applied. + * @param numThreads + * how many threads to use for multithreaded computation. * @param qualityImage * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > fromLabelingWithROI( - final ImgLabeling< Integer, R > labeling, - final Interval interval, - 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, + @SuppressWarnings( { "unchecked", "rawtypes" } ) + public static final < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > fromThresholdWithROI( + final RandomAccessible< T > input, final Interval interval, final double[] calibration, + final double threshold, final boolean simplify, + final double smoothingScale, + final int numThreads, 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 ); + /* + * Crop. + */ + final IntervalView< T > crop = Views.interval( input, interval ); + final IntervalView< T > in = Views.zeroMin( crop ); /* - * 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. + * Possibly filter. */ - 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 RandomAccessibleInterval< T > filtered; + if ( smoothingScale > 0. ) { - final LabelRegion< Integer > region = iterator.next(); - // Analyze in zero-min region. - final List< Polygon > pp = maskToPolygons( Views.zeroMin( region ) ); - // Translate back to interval coords. - for ( final Polygon polygon : pp ) - polygon.translate( ( int ) region.min( 0 ), ( int ) region.min( 1 ) ); + final double[] sigmas = new double[ in.numDimensions() ]; + for ( int d = 0; d < sigmas.length; d++ ) + sigmas[ d ] = smoothingScale / Math.sqrt( in.numDimensions() ) / calibration[ d ]; - final Integer label = region.getLabel(); - polygonsMap.put( label, pp ); + filtered = ( RandomAccessibleInterval ) Util.getArrayOrCellImgFactory( in, new FloatType() ).create( in ); + Parallelization.runWithNumThreads( numThreads, + () -> Gauss3.gauss( sigmas, Views.extendMirrorDouble( in ), filtered ) ); } - - - // Storage for results. - final Map< Integer, List< Spot > > output = new HashMap<>( polygonsMap.size() ); - - // Simplify them and compute a quality. - for ( final Integer label : polygonsMap.keySet() ) + else { - 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; - - // 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 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 ); - } + filtered = in; } - return output; - } - - private static final double distanceSquaredBetweenPoints( final double vx, final double vy, final double wx, final double wy ) - { - final double deltax = ( vx - wx ); - final double deltay = ( vy - wy ); - return deltax * deltax + deltay * deltay; - } - private static final double distanceToSegmentSquared( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) - { - final double l2 = distanceSquaredBetweenPoints( vx, vy, wx, wy ); - if ( l2 == 0 ) - return distanceSquaredBetweenPoints( px, py, vx, vy ); - final double t = ( ( px - vx ) * ( wx - vx ) + ( py - vy ) * ( wy - vy ) ) / l2; - if ( t < 0 ) - return distanceSquaredBetweenPoints( px, py, vx, vy ); - if ( t > 1 ) - return distanceSquaredBetweenPoints( px, py, wx, wy ); - return distanceSquaredBetweenPoints( px, py, ( vx + t * ( wx - vx ) ), ( vy + t * ( wy - vy ) ) ); - } - - private static final double perpendicularDistance( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) - { - return Math.sqrt( distanceToSegmentSquared( px, py, vx, vy, wx, wy ) ); - } - - private static final void douglasPeucker( final List< double[] > list, final int s, final int e, final double epsilon, final List< double[] > resultList ) - { - // Find the point with the maximum distance - double dmax = 0; - int index = 0; - - final int start = s; - final int end = e - 1; - for ( int i = start + 1; i < end; i++ ) + if ( input.numDimensions() == 2 ) { - // Point - final double px = list.get( i )[ 0 ]; - final double py = list.get( i )[ 1 ]; - // Start - final double vx = list.get( start )[ 0 ]; - final double vy = list.get( start )[ 1 ]; - // End - final double wx = list.get( end )[ 0 ]; - final double wy = list.get( end )[ 1 ]; - final double d = perpendicularDistance( px, py, vx, vy, wx, wy ); - if ( d > dmax ) - { - index = i; - dmax = d; - } + /* + * In 2D: Threshold, make a labeling, then create contours. + */ + return SpotRoiUtils.from2DThresholdWithROI( + filtered, + interval.minAsDoubleArray(), + calibration, + threshold, + simplify, + qualityImage ); } - // If max distance is greater than epsilon, recursively simplify - if ( dmax > epsilon ) + else if ( input.numDimensions() == 3 ) { - // Recursive call - douglasPeucker( list, s, index, epsilon, resultList ); - douglasPeucker( list, index, e, epsilon, resultList ); + /* + * In 3D: Directly operate on grayscale to create a big mesh, + * separate it in connected components, remerge them based on + * bounding-box before creating spots. We want to use the grayscale + * version of marching-cubes to have nice, smooth meshes. + */ + return SpotMeshUtils.from3DThresholdWithROI( + filtered, + interval.minAsDoubleArray(), + calibration, + threshold, + simplify, + qualityImage ); } else { - if ( ( end - start ) > 0 ) - { - resultList.add( list.get( start ) ); - resultList.add( list.get( end ) ); - } - else - { - resultList.add( list.get( start ) ); - } + throw new IllegalArgumentException( "Can only process 2D or 3D images with this method, but got " + input.numDimensions() + "D." ); } } - /** - * Given a curve composed of line segments find a similar curve with fewer - * points. - *

- * The Ramer–Douglas–Peucker algorithm (RDP) is an algorithm for reducing - * the number of points in a curve that is approximated by a series of - * points. - *

- * - * @see Ramer–Douglas–Peucker - * Algorithm (Wikipedia) - * @author Justin Wetherell - * @param list - * List of Double[] points (x,y) - * @param epsilon - * Distance dimension - * @return Similar curve with fewer points - */ - public static final List< double[] > douglasPeucker( final List< double[] > list, final double epsilon ) - { - final List< double[] > resultList = new ArrayList<>(); - douglasPeucker( list, 0, list.size(), epsilon, resultList ); - return resultList; - } - - public static final PolygonRoi simplify( final PolygonRoi roi, final double smoothInterval, final double epsilon ) - { - final FloatPolygon fPoly = roi.getInterpolatedPolygon( smoothInterval, true ); - - final List< double[] > points = new ArrayList<>( fPoly.npoints ); - for ( int i = 0; i < fPoly.npoints; i++ ) - points.add( new double[] { fPoly.xpoints[ i ], fPoly.ypoints[ i ] } ); - - final List< double[] > simplifiedPoints = douglasPeucker( points, epsilon ); - final float[] sX = new float[ simplifiedPoints.size() ]; - final float[] sY = new float[ simplifiedPoints.size() ]; - for ( int i = 0; i < sX.length; i++ ) - { - sX[ i ] = ( float ) simplifiedPoints.get( i )[ 0 ]; - sY[ i ] = ( float ) simplifiedPoints.get( i )[ 1 ]; - } - final FloatPolygon simplifiedPolygon = new FloatPolygon( sX, sY ); - final PolygonRoi fRoi = new PolygonRoi( simplifiedPolygon, PolygonRoi.POLYGON ); - return fRoi; - } - /** * Start at 1. * @@ -762,455 +482,4 @@ public boolean hasNext() } }; } - - /** - * Parse a 2D mask and return a list of polygons for the external contours - * of white objects. - *

- * 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 - * the mask image. - * @return a new list of polygons. - */ - private static final < T extends BooleanType< T > > List< Polygon > maskToPolygons( final RandomAccessibleInterval< T > mask ) - { - final int w = ( int ) mask.dimension( 0 ); - final int h = ( int ) mask.dimension( 1 ); - final RandomAccess< T > ra = mask.randomAccess( mask ); - - final List< Polygon > polygons = new ArrayList<>(); - boolean[] prevRow = new boolean[ w + 2 ]; - boolean[] thisRow = new boolean[ w + 2 ]; - final Outline[] outline = new Outline[ w + 1 ]; - - for ( int y = 0; y <= h; y++ ) - { - ra.setPosition( y, 1 ); - - final boolean[] b = prevRow; - prevRow = thisRow; - thisRow = b; - int xAfterLowerRightCorner = -1; - Outline oAfterLowerRightCorner = null; - - ra.setPosition( 0, 0 ); - thisRow[ 1 ] = y < h ? ra.get().get() : false; - - for ( int x = 0; x <= w; x++ ) - { - // we need to read one pixel ahead - ra.setPosition( x + 1, 0 ); - if ( y < h && x < w - 1 ) - thisRow[ x + 2 ] = ra.get().get(); - else if ( x < w - 1 ) - thisRow[ x + 2 ] = false; - - if ( thisRow[ x + 1 ] ) - { // i.e., pixel (x,y) is selected - if ( !prevRow[ x + 1 ] ) - { - // Upper edge of selected area: - // - left and right outlines are null: new outline - // - left null: append (line to left) - // - right null: prepend (line to right), or - // prepend&append (after lower right corner, two borders - // from one corner) - // - left == right: close (end of hole above) unless we - // can continue at the right - // - left != right: merge (prepend) unless we can - // continue at the right - if ( outline[ x ] == null ) - { - if ( outline[ x + 1 ] == null ) - { - outline[ x + 1 ] = outline[ x ] = new Outline(); - outline[ x ].append( x + 1, y ); - outline[ x ].append( x, y ); - } - else - { - outline[ x ] = outline[ x + 1 ]; - outline[ x + 1 ] = null; - outline[ x ].append( x, y ); - } - } - else if ( outline[ x + 1 ] == null ) - { - if ( x == xAfterLowerRightCorner ) - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = oAfterLowerRightCorner; - outline[ x ].append( x, y ); - outline[ x + 1 ].prepend( x + 1, y ); - } - else - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = null; - outline[ x + 1 ].prepend( x + 1, y ); - } - } - else if ( outline[ x + 1 ] == outline[ x ] ) - { - if ( x < w - 1 && y < h && x != xAfterLowerRightCorner - && !thisRow[ x + 2 ] && prevRow[ x + 2 ] ) - { // at lower right corner & next pxl deselected - outline[ x ] = null; - // outline[x+1] unchanged - outline[ x + 1 ].prepend( x + 1, y ); - xAfterLowerRightCorner = x + 1; - oAfterLowerRightCorner = outline[ x + 1 ]; - } - else - { - // MINUS (add inner hole) - // We cannot handle holes in TrackMate. -// polygons.add( outline[ x ].getPolygon() ); - outline[ x + 1 ] = null; - outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; - } - } - else - { - outline[ x ].prepend( outline[ x + 1 ] ); - for ( int x1 = 0; x1 <= w; x1++ ) - if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) - { - outline[ x1 ] = outline[ x ]; - outline[ x + 1 ] = null; - outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; - break; - } - if ( outline[ x + 1 ] != null ) - throw new RuntimeException( "assertion failed" ); - } - } - if ( !thisRow[ x ] ) - { - // left edge - if ( outline[ x ] == null ) - throw new RuntimeException( "assertion failed" ); - outline[ x ].append( x, y + 1 ); - } - } - else - { // !thisRow[x + 1], i.e., pixel (x,y) is deselected - if ( prevRow[ x + 1 ] ) - { - // Lower edge of selected area: - // - left and right outlines are null: new outline - // - left == null: prepend - // - right == null: append, or append&prepend (after - // lower right corner, two borders from one corner) - // - right == left: close unless we can continue at the - // right - // - right != left: merge (append) unless we can - // continue at the right - if ( outline[ x ] == null ) - { - if ( outline[ x + 1 ] == null ) - { - outline[ x ] = outline[ x + 1 ] = new Outline(); - outline[ x ].append( x, y ); - outline[ x ].append( x + 1, y ); - } - else - { - outline[ x ] = outline[ x + 1 ]; - outline[ x + 1 ] = null; - outline[ x ].prepend( x, y ); - } - } - else if ( outline[ x + 1 ] == null ) - { - if ( x == xAfterLowerRightCorner ) - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = oAfterLowerRightCorner; - outline[ x ].prepend( x, y ); - outline[ x + 1 ].append( x + 1, y ); - } - else - { - outline[ x + 1 ] = outline[ x ]; - outline[ x ] = null; - outline[ x + 1 ].append( x + 1, y ); - } - } - else if ( outline[ x + 1 ] == outline[ x ] ) - { - // System.err.println("add " + outline[x]); - if ( x < w - 1 && y < h && x != xAfterLowerRightCorner - && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) - { // at lower right corner & next pxl selected - outline[ x ] = null; - // outline[x+1] unchanged - outline[ x + 1 ].append( x + 1, y ); - xAfterLowerRightCorner = x + 1; - oAfterLowerRightCorner = outline[ x + 1 ]; - } - else - { - polygons.add( outline[ x ].getPolygon() ); - outline[ x + 1 ] = null; - outline[ x ] = x == xAfterLowerRightCorner ? oAfterLowerRightCorner : null; - } - } - else - { - if ( x < w - 1 && y < h && x != xAfterLowerRightCorner - && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) - { // at lower right corner && next pxl selected - outline[ x ].append( x + 1, y ); - outline[ x + 1 ].prepend( x + 1, y ); - xAfterLowerRightCorner = x + 1; - oAfterLowerRightCorner = outline[ x ]; - // outline[x + 1] unchanged (the one at the - // right-hand side of (x, y-1) to the top) - outline[ x ] = null; - } - else - { - outline[ x ].append( outline[ x + 1 ] ); // merge - for ( int x1 = 0; x1 <= w; x1++ ) - if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) - { - outline[ x1 ] = outline[ x ]; - outline[ x + 1 ] = null; - outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; - break; - } - if ( outline[ x + 1 ] != null ) - throw new RuntimeException( "assertion failed" ); - } - } - } - if ( thisRow[ x ] ) - { - // right edge - if ( outline[ x ] == null ) - throw new RuntimeException( "assertion failed" ); - outline[ x ].prepend( x, y + 1 ); - } - } - } - } - return polygons; - } - - /** - * This class implements a Cartesian polygon in progress. The edges are - * supposed to be parallel to the x or y axis. It is implemented as a deque - * to be able to add points to both sides. - */ - private static class Outline - { - - private int[] x, y; - - private int first, last, reserved; - - /** - * Default extra (spare) space when enlarging arrays (similar - * performance with 6-20) - */ - private final int GROW = 10; - - public Outline() - { - reserved = GROW; - x = new int[ reserved ]; - y = new int[ reserved ]; - first = last = GROW / 2; - } - - /** - * Makes sure that enough free space is available at the beginning and - * end of the list, by enlarging the arrays if required - */ - private void needs( final int neededAtBegin, final int neededAtEnd ) - { - if ( neededAtBegin > first || neededAtEnd > reserved - last ) - { - final int extraSpace = Math.max( GROW, Math.abs( x[ last - 1 ] - x[ first ] ) ); - final int newSize = reserved + neededAtBegin + neededAtEnd + extraSpace; - final int newFirst = neededAtBegin + extraSpace / 2; - final int[] newX = new int[ newSize ]; - final int[] newY = new int[ newSize ]; - System.arraycopy( x, first, newX, newFirst, last - first ); - System.arraycopy( y, first, newY, newFirst, last - first ); - x = newX; - y = newY; - last += newFirst - first; - first = newFirst; - reserved = newSize; - } - } - - /** Adds point x, y at the end of the list */ - public void append( final int x, final int y ) - { - if ( last - first >= 2 && collinear( this.x[ last - 2 ], this.y[ last - 2 ], this.x[ last - 1 ], this.y[ last - 1 ], x, y ) ) - { - this.x[ last - 1 ] = x; // replace previous point - this.y[ last - 1 ] = y; - } - else - { - needs( 0, 1 ); // new point - this.x[ last ] = x; - this.y[ last ] = y; - last++; - } - } - - /** Adds point x, y at the beginning of the list */ - public void prepend( final int x, final int y ) - { - if ( last - first >= 2 && collinear( this.x[ first + 1 ], this.y[ first + 1 ], this.x[ first ], this.y[ first ], x, y ) ) - { - this.x[ first ] = x; // replace previous point - this.y[ first ] = y; - } - else - { - needs( 1, 0 ); // new point - first--; - this.x[ first ] = x; - this.y[ first ] = y; - } - } - - /** - * Merge with another Outline by adding it at the end. Thereafter, the - * other outline must not be used any more. - */ - public void append( final Outline o ) - { - final int size = last - first; - final int oSize = o.last - o.first; - if ( size <= o.first && oSize > reserved - last ) - { // we don't have enough space in our own array but in that of 'o' - System.arraycopy( x, first, o.x, o.first - size, size ); - System.arraycopy( y, first, o.y, o.first - size, size ); - x = o.x; - y = o.y; - first = o.first - size; - last = o.last; - reserved = o.reserved; - } - else - { // append to our own array - needs( 0, oSize ); - System.arraycopy( o.x, o.first, x, last, oSize ); - System.arraycopy( o.y, o.first, y, last, oSize ); - last += oSize; - } - } - - /** - * Merge with another Outline by adding it at the beginning. Thereafter, - * the other outline must not be used any more. - */ - public void prepend( final Outline o ) - { - final int size = last - first; - final int oSize = o.last - o.first; - if ( size <= o.reserved - o.last && oSize > first ) - { /* - * We don't have enough space in our own array but in that of - * 'o' so append our own data to that of 'o' - */ - System.arraycopy( x, first, o.x, o.last, size ); - System.arraycopy( y, first, o.y, o.last, size ); - x = o.x; - y = o.y; - first = o.first; - last = o.last + size; - reserved = o.reserved; - } - else - { // prepend to our own array - needs( oSize, 0 ); - first -= oSize; - System.arraycopy( o.x, o.first, x, first, oSize ); - System.arraycopy( o.y, o.first, y, first, oSize ); - } - } - - public Polygon getPolygon() - { - /* - * optimize out intermediate points of straight lines (created, - * e.g., by merging outlines) - */ - int i, j = first + 1; - for ( i = first + 1; i + 1 < last; j++ ) - { - if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ j + 1 ], y[ j + 1 ] ) ) - { - // merge i + 1 into i - last--; - continue; - } - if ( i != j ) - { - x[ i ] = x[ j ]; - y[ i ] = y[ j ]; - } - i++; - } - // wraparound - if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ first ], y[ first ] ) ) - last--; - else - { - x[ i ] = x[ j ]; - y[ i ] = y[ j ]; - } - if ( last - first > 2 && collinear( x[ last - 1 ], y[ last - 1 ], x[ first ], y[ first ], x[ first + 1 ], y[ first + 1 ] ) ) - first++; - - final int count = last - first; - final int[] xNew = new int[ count ]; - final int[] yNew = new int[ count ]; - System.arraycopy( x, first, xNew, 0, count ); - System.arraycopy( y, first, yNew, 0, count ); - return new Polygon( xNew, yNew, count ); - } - - /** Returns whether three points are on one straight line */ - public boolean collinear( final int x1, final int y1, final int x2, final int y2, final int x3, final int y3 ) - { - return ( x2 - x1 ) * ( y3 - y2 ) == ( y2 - y1 ) * ( x3 - x2 ); - } - - @Override - public String toString() - { - String res = "[first:" + first + ",last:" + last + - ",reserved:" + reserved + ":"; - if ( last > x.length ) - System.err.println( "ERROR!" ); - int nmax = 10; // don't print more coordinates than this - for ( int i = first; i < last && i < x.length; i++ ) - { - if ( last - first > nmax && i - first > nmax / 2 ) - { - i = last - nmax / 2; - res += "..."; - nmax = last - first; // dont check again - } - else - res += "(" + x[ i ] + "," + y[ i ] + ")"; - } - return res + "]"; - } - } - } diff --git a/src/main/java/fiji/plugin/trackmate/detection/Process2DZ.java b/src/main/java/fiji/plugin/trackmate/detection/Process2DZ.java new file mode 100644 index 000000000..8bb62d906 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/Process2DZ.java @@ -0,0 +1,315 @@ +/*- + * #%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.detection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.scijava.Cancelable; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.TrackModel; +import fiji.plugin.trackmate.action.LabelImgExporter; +import fiji.plugin.trackmate.action.LabelImgExporter.LabelIdPainting; +import fiji.plugin.trackmate.util.TMUtils; +import ij.ImagePlus; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.Interval; +import net.imglib2.RandomAccess; +import net.imglib2.algorithm.MultiThreadedBenchmarkAlgorithm; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.mesh.alg.TaubinSmoothing; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.view.TranslateMesh; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.view.IntervalView; +import net.imglib2.view.Views; + +/** + * A {@link SpotDetector} for 3D images that work by running a spot segmentation + * algorithm on 2D slices, and merging results using a tracker. This yield a + * label image that is then converted to 3D meshes using the + * {@link LabelImageDetector}. + *

+ * This is a convenience class, made to be used in specialized + * {@link SpotDetectorFactory} with specific choices of detector and merging + * strategy. + * + * @author Jean-Yves Tinevez, 2023 + * + * @param + * the pixel type in the image processed. + */ +public class Process2DZ< T extends RealType< T > & NativeType< T > > + extends MultiThreadedBenchmarkAlgorithm + implements SpotDetector< T >, Cancelable +{ + + private static final String BASE_ERROR_MESSAGE = "[Process2DZ] "; + + private final ImgPlus< T > img; + + private final Interval interval; + + private final double[] calibration; + + private final Settings settings; + + private final boolean simplify; + + private List< Spot > spots; + + private final double smoothingScale; + + private boolean isCanceled; + + private String cancelReason; + + private TrackMate trackmate; + + /** + * Creates a new {@link Process2DZ} detector. + * + * @param img + * the input data. Must be 3D or 4D (3D plus possibly channels) + * and the 3 spatial dimensions must be X, Y and Z. + * @param interval + * the interval in the input data to process. Must have the same + * number of dimensions that the input data. + * @param calibration + * the pixel size array. + * @param settings + * a TrackMate settings object, configured to operate on the + * (cropped) input data as if it was a 2D(+C)+T image. + * @param simplifyMeshes + * whether or not to smooth and simplify meshes resulting from + * merging the 2D contours. + * @param smoothingScale + * if positive, will smooth the 3D mask by a gaussian of + * specified sigma to yield smooth meshes. + */ + public Process2DZ( + final ImgPlus< T > img, + final Interval interval, + final double[] calibration, + final Settings settings, + final boolean simplifyMeshes, + final double smoothingScale ) + { + this.img = img; + this.interval = interval; + this.calibration = calibration; + this.settings = settings; + this.simplify = simplifyMeshes; + this.smoothingScale = smoothingScale; + } + + @Override + public boolean checkInput() + { + if ( !( img.numDimensions() == 3 || img.numDimensions() == 4 ) ) + { + errorMessage = BASE_ERROR_MESSAGE + "Source image is not 3D or 4D, but " + img.numDimensions() + "D.\n"; + return false; + } + if ( img.dimensionIndex( Axes.TIME ) >= 0 ) + { + errorMessage = BASE_ERROR_MESSAGE + "Source image has a time dimension, but should not.\n"; + return false; + } + if ( img.dimensionIndex( Axes.Z ) < 0 ) + { + errorMessage = BASE_ERROR_MESSAGE + "Source image does not have a Z dimension.\n"; + return false; + } + if ( interval.numDimensions() != img.numDimensions() ) + { + errorMessage = BASE_ERROR_MESSAGE + "Provided interval does not have the same dimensionality that of the source image. " + + "Interval is " + interval.numDimensions() + "D and the image is " + img.numDimensions() + "D.\n"; + return false; + } + return true; + } + + @Override + public boolean process() + { + isCanceled = false; + cancelReason = null; + spots = null; + + /* + * Segment and track as a 2D+T image with the specified detector and + * settings. + */ + + // Make the final single T 3D image, a 2D + T image final by making Z->T + final IntervalView< T > cropped = Views.interval( img, interval ); + final ImagePlus imp = ImageJFunctions.wrap( cropped, null ); + final int nFrames = ( int ) interval.dimension( img.dimensionIndex( Axes.Z ) ); + final int cDim = img.dimensionIndex( Axes.CHANNEL ); + final int nChannels = cDim < 0 ? 1 : ( int ) interval.dimension( cDim ); + imp.setDimensions( nChannels, 1, nFrames ); + imp.getCalibration().pixelWidth = calibration[ 0 ]; + imp.getCalibration().pixelHeight = calibration[ 1 ]; + imp.getCalibration().pixelDepth = calibration[ 2 ]; + + // Execute segmentation and tracking. + final Settings settingsFrame = settings.copyOn( imp ); + this.trackmate = new TrackMate( settingsFrame ); + trackmate.setNumThreads( numThreads ); + trackmate.getModel().setLogger( Logger.VOID_LOGGER ); + if ( !trackmate.checkInput() || !trackmate.process() ) + { + errorMessage = BASE_ERROR_MESSAGE + trackmate.getErrorMessage(); + return false; + } + + // Get 2D+T masks + final ImagePlus lblImp = LabelImgExporter.createLabelImagePlus( trackmate, false, true, LabelIdPainting.LABEL_IS_TRACK_ID ); + + /* + * Exposes tracked labels as a 3D image and segment them again with + * label image detector. + */ + + // Back to a 3D single time-point image. + lblImp.setDimensions( lblImp.getNChannels(), lblImp.getNFrames(), lblImp.getNSlices() ); + + // Convert labels to 3D meshes. + final ImgPlus< T > lblImg = TMUtils.rawWraps( lblImp ); + final LabelImageDetector< T > detector = new LabelImageDetector<>( + lblImg, + lblImg, + calibration, + simplify, + smoothingScale ); + if ( !detector.checkInput() || !detector.process() ) + { + errorMessage = BASE_ERROR_MESSAGE + detector.getErrorMessage(); + return false; + } + + final List< Spot > results = detector.getResult(); + spots = new ArrayList<>( results.size() ); + + // To read the label value (=trackID) later. + final RandomAccess< T > ra = lblImg.randomAccess(); + final TrackModel tm = trackmate.getModel().getTrackModel(); + + for ( final Spot spot : results ) + { + + /* + * Smooth spot? + */ + + final Spot newSpot; + if ( !simplify || !spot.getClass().isAssignableFrom( SpotMesh.class ) ) + { + newSpot = spot; + } + else + { + final SpotMesh sm = ( SpotMesh ) spot; + final BufferMesh out = TaubinSmoothing.smooth( TranslateMesh.translate( sm.getMesh(), sm ) ); + newSpot = SpotMeshUtils.meshToSpotMesh( out, simplify, new double[] { 1., 1., 1. }, null, new double[] { 0., 0., 0. } ); + if ( newSpot == null ) + continue; + } + + /* + * Try to get quality from the tracks resulting from the 2D+T image. + */ + + // Position RA where the spot is. + for ( int d = 0; d < 3; d++ ) + ra.setPosition( Math.round( spot.getDoublePosition( d ) / calibration[ d ] ), d ); + + // Read track ID from label value. + final int trackID = ( int ) ra.get().getRealDouble() - 1; + + // Average quality from the corresponding track. + final Set< Spot > trackSpots = tm.trackSpots( trackID ); + final double avgQuality; + if ( trackSpots != null ) + { + avgQuality = trackSpots.stream() + .mapToDouble( s -> s.getFeature( Spot.QUALITY ).doubleValue() ) + .average() + .getAsDouble(); + } + else + { + // default if something goes wrong. + avgQuality = spot.getFeature( Spot.QUALITY ); + } + + // Pass quality to new spot. + newSpot.putFeature( Spot.QUALITY, Double.valueOf( avgQuality ) ); + + // Shift them by interval min. + newSpot.move( interval.min( img.dimensionIndex( Axes.X ) ) * calibration[ 0 ], 0 ); + newSpot.move( interval.min( img.dimensionIndex( Axes.Y ) ) * calibration[ 1 ], 1 ); + newSpot.move( interval.min( img.dimensionIndex( Axes.Z ) ) * calibration[ 2 ], 2 ); + + spots.add( newSpot ); + } + return true; + } + + @Override + public List< Spot > getResult() + { + return spots; + } + + // --- org.scijava.Cancelable methods --- + + @Override + public boolean isCanceled() + { + return isCanceled; + } + + @Override + public void cancel( final String reason ) + { + isCanceled = true; + cancelReason = reason; + if ( trackmate != null ) + trackmate.cancel( reason ); + } + + @Override + public String getCancelReason() + { + return cancelReason; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java index 8578ecc08..e62e562df 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetector.java @@ -30,15 +30,13 @@ import net.imglib2.type.numeric.RealType; /** - * Interface for Spot detector classes, that are able to segment spots of a - * given estimated radius within a 2D or 3D image. + * Interface for Spot detector classes, that are able to detect or segment spots + * in a single time-point 2D or 3D image. *

- * Normally, concrete implementation are not expected to be multi-threaded. - * Indeed, the {@link fiji.plugin.trackmate.TrackMate} trackmate generates one - * instance of the concrete implementation per thread, to process multiple - * frames simultaneously. + * Concrete implementation can be multithreaded. In that case TrackMate will + * possible allocate some threads to each instance of this class. * - * @author Jean-Yves Tinevez <jeanyves.tinevez@gmail.com> 2010 - 2012 + * @author Jean-Yves Tinevez, 2010 - 2012 * */ public interface SpotDetector< T extends RealType< T > & NativeType< T > > extends OutputAlgorithm< List< Spot > >, Benchmark diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java index 0800908dd..b57714cc7 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactory.java @@ -27,7 +27,7 @@ /** * For detectors that process one time-point at a time, independently, and for - * which we can therefore propose multithreading. * + * which we can therefore propose multithreading. *

* These classes are able to configure a {@link SpotDetector} to operate on a * single time-point of the target ImgPlus. @@ -35,6 +35,7 @@ * @author Jean-Yves Tinevez * * @param + * the pixel type in the image processed by the detector. */ public interface SpotDetectorFactory< T extends RealType< T > & NativeType< T > > extends SpotDetectorFactoryBase< T > { @@ -53,6 +54,7 @@ public interface SpotDetectorFactory< T extends RealType< T > & NativeType< T > * then the interval must be 3D). * @param frame * the frame index in the source image to operate on + * @return a new detector. */ public SpotDetector< T > getDetector( final Interval interval, int frame ); } diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java index d4a9bf41f..30331f8b9 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotDetectorFactoryBase.java @@ -65,11 +65,12 @@ public interface SpotDetectorFactoryBase< T extends RealType< T > & NativeType< * @see #setTarget(ImgPlus, Map) * @see #marshall(Map, Element) * @see #unmarshall(Element, Map) + * @return an error message. */ public String getErrorMessage(); /** - * Marshalls a settings map to a JDom element, ready for saving to XML. The + * Marshals a settings map to a JDom element, ready for saving to XML. The * element is updated with new attributes. *

* Only parameters specific to the specific detector factory are marshalled. @@ -77,13 +78,18 @@ public interface SpotDetectorFactoryBase< T extends RealType< T > & NativeType< * {@value DetectorKeys#XML_ATTRIBUTE_DETECTOR_NAME} that saves the target * {@link SpotDetectorFactory} key. * + * @param settings + * the settings map to marshal. + * @param element + * the element to marshal to. * @return true if marshalling was successful. If not, check * {@link #getErrorMessage()} + * */ public boolean marshall( final Map< String, Object > settings, final Element element ); /** - * Un-marshalls a JDom element to update a settings map. + * Un-marshals a JDom element to update a settings map. * * @param element * the JDom element to read from. @@ -106,19 +112,20 @@ public interface SpotDetectorFactoryBase< T extends RealType< T > & NativeType< * @param model * the current model, used to get info to display on the GUI * panel. + * @return a new configuration panel. */ public ConfigurationPanel getDetectorConfigurationPanel( final Settings settings, final Model model ); /** * Returns a new default settings map suitable for the target detector. * Settings are instantiated with default values. - * + * * @return a new map. */ public Map< String, Object > getDefaultSettings(); /** - * Check that the given settings map is suitable for target detector. + * Checks that the given settings map is suitable for target detector. * * @param settings * the map to test. @@ -127,15 +134,15 @@ public interface SpotDetectorFactoryBase< T extends RealType< T > & NativeType< public boolean checkSettings( final Map< String, Object > settings ); /** - * Return true for the detectors that can provide a spot with a - * 2D SpotRoi when they operate on 2D images. + * Returns true for the detectors that can provide a spot with + * a 2D SpotRoi when they operate on 2D images. *

* This flag may be used by clients to exploit the fact that the spots * created with this detector will have a contour that can be used - * e.g. to compute morphological features. The default is + * e.g. to compute 2D morphological features. The default is * false, indicating that this detector provides spots as a X, * Y, Z, radius tuple. - * + * * @return true if the spots created by this detector have a 2D * contour. */ @@ -144,9 +151,27 @@ public default boolean has2Dsegmentation() return false; } + /** + * Returns true for the detectors that can provide a spot with + * a 3D SpotMesh when they operate on 3D images. + *

+ * This flag may be used by clients to exploit the fact that the spots + * created with this detector will have a 3D mesh that can be used + * e.g. to compute 3D morphological features. The default is + * false, indicating that this detector provides spots as a X, + * Y, Z, radius tuple. + * + * @return true if the spots created by this detector have a 2D + * contour. + */ + public default boolean has3Dsegmentation() + { + return false; + } + /** * Returns a copy the current instance. - * + * * @return a new instance of this detector factory. */ public SpotDetectorFactoryBase< T > copy(); diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java index 2793d46e9..51ab70cd4 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotGlobalDetectorFactory.java @@ -33,6 +33,7 @@ * @author Jean-Yves Tinevez * * @param + * the pixel type in the image processed by the detector. */ public interface SpotGlobalDetectorFactory< T extends RealType< T > & NativeType< T > > extends SpotDetectorFactoryBase< T > { @@ -45,10 +46,13 @@ public interface SpotGlobalDetectorFactory< T extends RealType< T > & NativeType * * @param interval * the interval that determines the region in the source image to - * operate on. This must not have a dimension for time - * (e.g. if the source image is 2D+T (3D), then the - * interval must be 2D; if the source image is 3D without time, - * then the interval must be 3D). + * operate on. This must have a dimension for time, but + * not for channels (e.g. if the source image is 2D+T + * (3D), then the interval must be 3D; if the source image is 3D + * without time, then the interval must be 4D). + * @return a new detector. + * @see TMUtils#getIntervalWithTime(net.imagej.ImgPlus, + * fiji.plugin.trackmate.Settings) */ public SpotGlobalDetector< T > getDetector( final Interval interval ); diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotMeshUtils.java b/src/main/java/fiji/plugin/trackmate/detection/SpotMeshUtils.java new file mode 100644 index 000000000..341376d58 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotMeshUtils.java @@ -0,0 +1,466 @@ +/*- + * #%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.detection; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.Interval; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.RealInterval; +import net.imglib2.algorithm.gauss3.Gauss3; +import net.imglib2.img.Img; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.MeshStats; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.alg.MeshConnectedComponents; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.roi.labeling.LabelRegion; +import net.imglib2.roi.labeling.LabelRegions; +import net.imglib2.type.NativeType; +import net.imglib2.type.logic.BoolType; +import net.imglib2.type.numeric.IntegerType; +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.Views; + +/** + * Utility classes to create 3D {@link fiji.plugin.trackmate.SpotMesh}es from + * single time-point, single channel images. + * + * @author Jean-Yves Tinevez, 2023 + */ +public class SpotMeshUtils +{ + + /** Number of triangles below which not to simplify a mesh. */ + private static final int MIN_N_TRIANGLES = 100; + + /** Quadratic mesh decimation aggressiveness for simplification. */ + private static final float SIMPLIFY_AGGRESSIVENESS = 10f; + + /** Minimal volume, in pixels, below which we discard meshes. */ + private static final double MIN_MESH_PIXEL_VOLUME = 15.; + + /** + * Precision for the vertex duplicate removal step. A value of 2 means that + * the vertices with coordinates (in pixel units) equal up to the second + * decimal will be considered duplicates and merged. + */ + private static final int VERTEX_DUPLICATE_REMOVAL_PRECISION = 2; + + /** + * Creates spots with meshes from a 3D grayscale image. The + * quality value is read from a secondary image, by taking the max value in + * each object, or the volume if the quality image is null. + *

+ * The grayscale marching-cube algorithm is used to create one big mesh from + * the source image. It is then split in connected-components to create + * single spot objects. However, to deal with possible holes in objects, + * meshes are possibly re-merged based on full inclusion of their bounding + * box. For instance, a hollow sphere would be represented by two + * connected-components, yielding two meshes. But because the small one is + * included in the big one, they are merged in this method. + * + * @param + * the type of the source image. Must be real, scalar. + * @param + * the type of the quality image. Must be real, scalar. + * @param input + * the source image, must be zero-min and 3D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param threshold + * the threshold to apply to the input image. + * @param simplify + * if true the meshes will be post-processed to be + * smoother and contain less points. + * @param qualityImage + * the image in which to read the quality value. + * @return a list of spots, with meshes. + */ + public static < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > from3DThresholdWithROI( + final RandomAccessibleInterval< T > input, + final double[] origin, + final double[] calibration, + final double threshold, + final boolean simplify, + final RandomAccessibleInterval< S > qualityImage ) + { + if ( input.numDimensions() != 3 ) + throw new IllegalArgumentException( "Can only process 3D images with this method, but got " + input.numDimensions() + "D." ); + + // Get big mesh. + final Mesh mc = Meshes.marchingCubes( input, threshold ); + final Mesh bigMesh = Meshes.removeDuplicateVertices( mc, VERTEX_DUPLICATE_REMOVAL_PRECISION ); + + // Split into connected components. + final List< Mesh > meshes = new ArrayList<>(); + final List< RealInterval > boundingBoxes = new ArrayList<>(); + for ( final BufferMesh m : MeshConnectedComponents.iterable( bigMesh ) ) + { + meshes.add( m ); + boundingBoxes.add( Meshes.boundingBox( m ) ); + } + + // Merge if bb is included in one another. + final List< Mesh > out = new ArrayList<>(); + MESH_I: for ( int i = 0; i < meshes.size(); i++ ) + { + final RealInterval bbi = boundingBoxes.get( i ); + final Mesh meshi = meshes.get( i ); + + /* + * FIXME revise this. Improper and incorrect. + */ + + // Can we put it inside another? + for ( int j = i + 1; j < meshes.size(); j++ ) + { + final RealInterval bbj = boundingBoxes.get( j ); + if ( Intervals.contains( bbj, bbi ) ) + { + final Mesh meshj = meshes.get( j ); + + // Merge the ith into the jth. + final Mesh merged = Meshes.merge( Arrays.asList( meshi, meshj ) ); + meshes.set( j, merged ); + continue MESH_I; + } + } + + // We could not, retain it for later. + out.add( meshi ); + } + + // Create spot from merged meshes. + final List< Spot > spots = new ArrayList<>( out.size() ); + for ( final Mesh mesh : out ) + { + final SpotMesh spot = meshToSpotMesh( + mesh, + simplify, + calibration, + qualityImage, + origin ); + if ( spot != null ) + spots.add( spot ); + } + return spots; + } + + /** + * Creates spots with meshes from a 3D label image. The labels + * are possibly smoothed before creating the mesh. The quality value is read + * from a secondary image, by taking the max value inside the mesh. + * + * @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 3D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the meshes will be post-processed to + * contain less verrtices. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @param qualityImage + * the image in which to read the quality value. + * @return a list of spots, with meshes. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > from3DLabelingWithROI( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + final Map< Integer, List< Spot > > map = from3DLabelingWithROIMap( labeling, origin, calibration, simplify, smoothingScale, qualityImage ); + final List< Spot > spots = new ArrayList<>(); + for ( final List< Spot > s : map.values() ) + spots.addAll( s ); + + return spots; + } + + /** + * Creates spots with meshes from a 3D label image. The labels + * are possibly smoothed before creating the mesh. The quality value is read + * from a secondary image, by taking the max value inside the mesh. + *

+ * The spots are returned in a map, where the key is the integer value of + * the label they correspond to in the label image. In 3D, there is one spot + * per label, even for disconnected components, so the lists are made of one + * element for now. + * + * @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 3D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the meshes will be post-processed to + * contain less verrtices. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @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 + * meshes, it corresponds to. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integer, List< Spot > > from3DLabelingWithROIMap( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + if ( labeling.numDimensions() != 3 ) + throw new IllegalArgumentException( "Can only process 3D images with this method, but got " + labeling.numDimensions() + "D." ); + + // Parse regions to create meshes on label. + final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); + final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + final Map< Integer, List< Spot > > spots = new HashMap<>( regions.getExistingLabels().size() ); + while ( iterator.hasNext() ) + { + final LabelRegion< Integer > region = iterator.next(); + final Spot spot = regionToSpotMesh( + region, + simplify, + calibration, + smoothingScale, + origin, + qualityImage ); + if ( spot == null ) + continue; + + spots.put( region.getLabel(), Collections.singletonList( spot ) ); + } + return spots; + } + + /** + * Returns a new {@link Spot} with a {@link SpotMesh} as shape, built from + * the specified bit-mask. + * + * @param + * the type of pixels in the quality image. + * @param region + * the bit-mask to build the mesh from. + * @param simplify + * if true the mesh will be simplified. + * @param calibration + * the pixel size array, used to scale the mesh to physical + * coordinates. + * @param qualityImage + * an image from which to read the quality value. If not + * null, the quality of the spot will be the max + * value of this image inside the mesh. If null, the + * quality will be the mesh volume. + * @param minInterval + * the origin in image coordinates of the ROI used for detection. + * @param smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * + * @return a new spot. + */ + private static < S extends RealType< S > > Spot regionToSpotMesh( + final RandomAccessibleInterval< BoolType > region, + final boolean simplify, + final double[] calibration, + final double smoothingScale, + final double[] minInterval, + final RandomAccessibleInterval< S > qualityImage ) + { + final RandomAccessibleInterval< BoolType > box = Views.zeroMin( region ); + final Mesh mesh; + + // Possibly filter. + final long[] borders; + if ( smoothingScale > 0 ) + { + final double[] sigmas = new double[ 3 ]; + for ( int d = 0; d < 3; d++ ) + sigmas[ d ] = smoothingScale / Math.sqrt( 3. ) / calibration[ d ]; + + // Increase the output size. + final int[] halfkernelsizes = Gauss3.halfkernelsizes( sigmas );; + borders = Arrays.stream( halfkernelsizes ).asLongStream().toArray(); + final Interval outputSize = Intervals.expand( box, borders ); + final Img< FloatType > img = Util.getArrayOrCellImgFactory( outputSize, new FloatType() ).create( outputSize ); + final RandomAccessibleInterval< FloatType > filtered = Views.translateInverse( img, borders ); + Gauss3.gauss( sigmas, Views.extendZero( box ), filtered ); + mesh = Meshes.marchingCubes( img, 0.5 ); + } + else + { + mesh = Meshes.marchingCubes( box ); + borders = new long[] { 0, 0, 0 }; + } + + // To mesh. + final Mesh cleaned = Meshes.removeDuplicateVertices( mesh, VERTEX_DUPLICATE_REMOVAL_PRECISION ); + // Shift coords. + final double[] origin = region.minAsDoubleArray(); + for ( int d = 0; d < 3; d++ ) + origin[ d ] += minInterval[ d ] - borders[ d ]; + // To spot. + return meshToSpotMesh( + cleaned, + simplify, + calibration, + qualityImage, + origin ); + } + + /** + * Creates a {@link SpotMesh} from a {@link Mesh}. + * + * @param + * the type of the quality image. + * @param mesh + * the mesh to create a spot from. + * @param simplify + * whether the simplify the mesh. + * @param calibration + * the pixel size array, to map pixel coords to physical coords. + * @param qualityImage + * the quality image. If not null the quality he + * quality of the spot will be the max value of this image inside + * the mesh. If null, the quality will be the mesh + * volume. + * @param origin + * the origin of the interval the mesh was created on. This is + * used to put back the mesh coordinates with respect to the + * initial source image (same referential that for the quality + * image). + * @return a new spot. + */ + public static < S extends RealType< S > > SpotMesh meshToSpotMesh( + final Mesh mesh, + final boolean simplify, + final double[] calibration, + final RandomAccessibleInterval< S > qualityImage, + final double[] origin ) + { + final Mesh simplified; + if ( simplify ) + { + // Dont't go below a certain number of triangles. + final int nTriangles = mesh.triangles().size(); + if ( nTriangles < MIN_N_TRIANGLES ) + { + simplified = mesh; + } + else + { + // Crude heuristics. + final float targetRatio; + if ( nTriangles < 2 * MIN_N_TRIANGLES ) + targetRatio = 0.5f; + else if ( nTriangles < 10_000 ) + targetRatio = 0.2f; + else if ( nTriangles < 1_000_000 ) + targetRatio = 0.1f; + else + targetRatio = 0.05f; + simplified = Meshes.simplify( mesh, targetRatio, SIMPLIFY_AGGRESSIVENESS ); + } + } + else + { + simplified = mesh; + } + // Remove meshes that are too small + final double volumeThreshold = MIN_MESH_PIXEL_VOLUME * calibration[ 0 ] * calibration[ 1 ] * calibration[ 2 ]; + if ( MeshStats.volume( simplified ) < volumeThreshold ) + return null; + + // Translate back to interval coords & scale to physical coords. + Meshes.translateScale( simplified, origin, calibration ); + + // Make spot with default quality. + final SpotMesh spot = new SpotMesh( simplified, 0. ); + + // Measure quality. + final double quality; + if ( null == qualityImage ) + { + quality = MeshStats.volume( simplified ); + } + else + { + final IterableInterval< S > iterable = spot.iterable( qualityImage, calibration ); + 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, Double.valueOf( quality ) ); + return spot; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/SpotRoiUtils.java b/src/main/java/fiji/plugin/trackmate/detection/SpotRoiUtils.java new file mode 100644 index 000000000..5e3078861 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/detection/SpotRoiUtils.java @@ -0,0 +1,841 @@ +/*- + * #%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.detection; + +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 fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotRoi; +import ij.gui.PolygonRoi; +import ij.process.FloatPolygon; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.IterableInterval; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.roi.labeling.LabelRegion; +import net.imglib2.roi.labeling.LabelRegions; +import net.imglib2.type.BooleanType; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.view.Views; + +/** + * Utility classes to create 2D {@link fiji.plugin.trackmate.SpotRoi}s from + * single time-point, single channel images. + * + * @author Jean-Yves Tinevez, 2023 + */ +public class SpotRoiUtils +{ + + /** Smoothing interval for ROIs. */ + private static final double SMOOTH_INTERVAL = 2.; + + /** Douglas-Peucker polygon simplification max distance. */ + private static final double DOUGLAS_PEUCKER_MAX_DISTANCE = 0.5; + + public static < T extends RealType< T > & NativeType< T >, S extends RealType< S > > List< Spot > from2DThresholdWithROI( + final RandomAccessibleInterval< T > input, + final double[] origin, + final double[] calibration, + final double threshold, + final boolean simplify, + final RandomAccessibleInterval< S > qualityImage ) + { + final ImgLabeling< Integer, IntType > labeling = MaskUtils.toLabeling( + input, + threshold, + 1 ); + return from2DLabelingWithROI( + labeling, + origin, + calibration, + simplify, + -1., + qualityImage ); + } + + /** + * 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 + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 2D. + * @param origin + * the origin (min pos) of the interval the labeling was + * generated from, 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 smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @param qualityImage + * the image in which to read the quality value. + * @return a list of spots, with ROI. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > from2DLabelingWithROI( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + final RandomAccessibleInterval< S > qualityImage ) + { + final Map< Integer, List< Spot > > map = from2DLabelingWithROIMap( + labeling, + origin, + calibration, + simplify, + smoothingScale, + qualityImage ); + final List< Spot > 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 origin + * the origin (min pos) of the interval the labeling was + * generated from, 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 smoothingScale + * if strictly larger than 0, the mask will be smoothed before + * creating the mesh, resulting in smoother meshes. The scale + * value sets the (Gaussian) filter radius and is specified in + * physical units. If 0 or lower than 0, no smoothing is applied. + * @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 > > from2DLabelingWithROIMap( + final ImgLabeling< Integer, R > labeling, + final double[] origin, + final double[] calibration, + final boolean simplify, + final double smoothingScale, + 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 ); + + /* + * 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(); + // Analyze in zero-min region. + final List< Polygon > pp = maskToPolygons( Views.zeroMin( region ) ); + // Translate back to interval coords. + for ( final Polygon polygon : pp ) + polygon.translate( ( int ) region.min( 0 ), ( int ) region.min( 1 ) ); + + final Integer label = region.getLabel(); + polygonsMap.put( label, pp ); + } + + // Storage for results. + final Map< Integer, List< Spot > > output = new HashMap<>( polygonsMap.size() ); + + // Simplify them and compute a quality. + for ( final Integer label : polygonsMap.keySet() ) + { + 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; + + // 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 ] * ( origin[ 0 ] + fPolygon.xpoints[ i ] - 0.5 ); + ypoly[ i ] = calibration[ 1 ] * ( origin[ 1 ] + fPolygon.ypoints[ i ] - 0.5 ); + } + + 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 = spot.iterable( 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 output; + } + + private static final double distanceSquaredBetweenPoints( final double vx, final double vy, final double wx, final double wy ) + { + final double deltax = ( vx - wx ); + final double deltay = ( vy - wy ); + return deltax * deltax + deltay * deltay; + } + + private static final double distanceToSegmentSquared( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) + { + final double l2 = distanceSquaredBetweenPoints( vx, vy, wx, wy ); + if ( l2 == 0 ) + return distanceSquaredBetweenPoints( px, py, vx, vy ); + final double t = ( ( px - vx ) * ( wx - vx ) + ( py - vy ) * ( wy - vy ) ) / l2; + if ( t < 0 ) + return distanceSquaredBetweenPoints( px, py, vx, vy ); + if ( t > 1 ) + return distanceSquaredBetweenPoints( px, py, wx, wy ); + return distanceSquaredBetweenPoints( px, py, ( vx + t * ( wx - vx ) ), ( vy + t * ( wy - vy ) ) ); + } + + private static final double perpendicularDistance( final double px, final double py, final double vx, final double vy, final double wx, final double wy ) + { + return Math.sqrt( distanceToSegmentSquared( px, py, vx, vy, wx, wy ) ); + } + + private static final void douglasPeucker( final List< double[] > list, final int s, final int e, final double epsilon, final List< double[] > resultList ) + { + // Find the point with the maximum distance + double dmax = 0; + int index = 0; + + final int start = s; + final int end = e - 1; + for ( int i = start + 1; i < end; i++ ) + { + // Point + final double px = list.get( i )[ 0 ]; + final double py = list.get( i )[ 1 ]; + // Start + final double vx = list.get( start )[ 0 ]; + final double vy = list.get( start )[ 1 ]; + // End + final double wx = list.get( end )[ 0 ]; + final double wy = list.get( end )[ 1 ]; + final double d = perpendicularDistance( px, py, vx, vy, wx, wy ); + if ( d > dmax ) + { + index = i; + dmax = d; + } + } + // If max distance is greater than epsilon, recursively simplify + if ( dmax > epsilon ) + { + // Recursive call + douglasPeucker( list, s, index, epsilon, resultList ); + douglasPeucker( list, index, e, epsilon, resultList ); + } + else + { + if ( ( end - start ) > 0 ) + { + resultList.add( list.get( start ) ); + resultList.add( list.get( end ) ); + } + else + { + resultList.add( list.get( start ) ); + } + } + } + + /** + * Given a curve composed of line segments find a similar curve with fewer + * points. + *

+ * The Ramer–Douglas–Peucker algorithm (RDP) is an algorithm for reducing + * the number of points in a curve that is approximated by a series of + * points. + *

+ * + * @see Ramer–Douglas–Peucker + * Algorithm (Wikipedia) + * @author Justin Wetherell + * @param list + * List of double[] points (x,y) + * @param epsilon + * Distance dimension + * @return Similar curve with fewer points + */ + public static final List< double[] > douglasPeucker( final List< double[] > list, final double epsilon ) + { + final List< double[] > resultList = new ArrayList<>(); + douglasPeucker( list, 0, list.size(), epsilon, resultList ); + return resultList; + } + + public static final PolygonRoi simplify( final PolygonRoi roi, final double smoothInterval, final double epsilon ) + { + final FloatPolygon fPoly = roi.getInterpolatedPolygon( smoothInterval, true ); + + final List< double[] > points = new ArrayList<>( fPoly.npoints ); + for ( int i = 0; i < fPoly.npoints; i++ ) + points.add( new double[] { fPoly.xpoints[ i ], fPoly.ypoints[ i ] } ); + + final List< double[] > simplifiedPoints = douglasPeucker( points, epsilon ); + final float[] sX = new float[ simplifiedPoints.size() ]; + final float[] sY = new float[ simplifiedPoints.size() ]; + for ( int i = 0; i < sX.length; i++ ) + { + sX[ i ] = ( float ) simplifiedPoints.get( i )[ 0 ]; + sY[ i ] = ( float ) simplifiedPoints.get( i )[ 1 ]; + } + final FloatPolygon simplifiedPolygon = new FloatPolygon( sX, sY ); + final PolygonRoi fRoi = new PolygonRoi( simplifiedPolygon, PolygonRoi.POLYGON ); + return fRoi; + } + + /** + * Parse a 2D mask and return a list of polygons for the external contours + * of white objects. + *

+ * 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 + * the mask image. + * @return a new list of polygons. + */ + private static final < T extends BooleanType< T > > List< Polygon > maskToPolygons( final RandomAccessibleInterval< T > mask ) + { + final int w = ( int ) mask.dimension( 0 ); + final int h = ( int ) mask.dimension( 1 ); + final RandomAccess< T > ra = mask.randomAccess( mask ); + + final List< Polygon > polygons = new ArrayList<>(); + boolean[] prevRow = new boolean[ w + 2 ]; + boolean[] thisRow = new boolean[ w + 2 ]; + final Outline[] outline = new Outline[ w + 1 ]; + + for ( int y = 0; y <= h; y++ ) + { + ra.setPosition( y, 1 ); + + final boolean[] b = prevRow; + prevRow = thisRow; + thisRow = b; + int xAfterLowerRightCorner = -1; + Outline oAfterLowerRightCorner = null; + + ra.setPosition( 0, 0 ); + thisRow[ 1 ] = y < h ? ra.get().get() : false; + + for ( int x = 0; x <= w; x++ ) + { + // we need to read one pixel ahead + ra.setPosition( x + 1, 0 ); + if ( y < h && x < w - 1 ) + thisRow[ x + 2 ] = ra.get().get(); + else if ( x < w - 1 ) + thisRow[ x + 2 ] = false; + + if ( thisRow[ x + 1 ] ) + { // i.e., pixel (x,y) is selected + if ( !prevRow[ x + 1 ] ) + { + // Upper edge of selected area: + // - left and right outlines are null: new outline + // - left null: append (line to left) + // - right null: prepend (line to right), or + // prepend&append (after lower right corner, two borders + // from one corner) + // - left == right: close (end of hole above) unless we + // can continue at the right + // - left != right: merge (prepend) unless we can + // continue at the right + if ( outline[ x ] == null ) + { + if ( outline[ x + 1 ] == null ) + { + outline[ x + 1 ] = outline[ x ] = new Outline(); + outline[ x ].append( x + 1, y ); + outline[ x ].append( x, y ); + } + else + { + outline[ x ] = outline[ x + 1 ]; + outline[ x + 1 ] = null; + outline[ x ].append( x, y ); + } + } + else if ( outline[ x + 1 ] == null ) + { + if ( x == xAfterLowerRightCorner ) + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = oAfterLowerRightCorner; + outline[ x ].append( x, y ); + outline[ x + 1 ].prepend( x + 1, y ); + } + else + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = null; + outline[ x + 1 ].prepend( x + 1, y ); + } + } + else if ( outline[ x + 1 ] == outline[ x ] ) + { + if ( x < w - 1 && y < h && x != xAfterLowerRightCorner + && !thisRow[ x + 2 ] && prevRow[ x + 2 ] ) + { // at lower right corner & next pxl deselected + outline[ x ] = null; + // outline[x+1] unchanged + outline[ x + 1 ].prepend( x + 1, y ); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[ x + 1 ]; + } + else + { + // MINUS (add inner hole) + // We cannot handle holes in TrackMate. +// polygons.add( outline[ x ].getPolygon() ); + outline[ x + 1 ] = null; + outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; + } + } + else + { + outline[ x ].prepend( outline[ x + 1 ] ); + for ( int x1 = 0; x1 <= w; x1++ ) + if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) + { + outline[ x1 ] = outline[ x ]; + outline[ x + 1 ] = null; + outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; + break; + } + if ( outline[ x + 1 ] != null ) + throw new RuntimeException( "assertion failed" ); + } + } + if ( !thisRow[ x ] ) + { + // left edge + if ( outline[ x ] == null ) + throw new RuntimeException( "assertion failed" ); + outline[ x ].append( x, y + 1 ); + } + } + else + { // !thisRow[x + 1], i.e., pixel (x,y) is deselected + if ( prevRow[ x + 1 ] ) + { + // Lower edge of selected area: + // - left and right outlines are null: new outline + // - left == null: prepend + // - right == null: append, or append&prepend (after + // lower right corner, two borders from one corner) + // - right == left: close unless we can continue at the + // right + // - right != left: merge (append) unless we can + // continue at the right + if ( outline[ x ] == null ) + { + if ( outline[ x + 1 ] == null ) + { + outline[ x ] = outline[ x + 1 ] = new Outline(); + outline[ x ].append( x, y ); + outline[ x ].append( x + 1, y ); + } + else + { + outline[ x ] = outline[ x + 1 ]; + outline[ x + 1 ] = null; + outline[ x ].prepend( x, y ); + } + } + else if ( outline[ x + 1 ] == null ) + { + if ( x == xAfterLowerRightCorner ) + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = oAfterLowerRightCorner; + outline[ x ].prepend( x, y ); + outline[ x + 1 ].append( x + 1, y ); + } + else + { + outline[ x + 1 ] = outline[ x ]; + outline[ x ] = null; + outline[ x + 1 ].append( x + 1, y ); + } + } + else if ( outline[ x + 1 ] == outline[ x ] ) + { + // System.err.println("add " + outline[x]); + if ( x < w - 1 && y < h && x != xAfterLowerRightCorner + && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) + { // at lower right corner & next pxl selected + outline[ x ] = null; + // outline[x+1] unchanged + outline[ x + 1 ].append( x + 1, y ); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[ x + 1 ]; + } + else + { + polygons.add( outline[ x ].getPolygon() ); + outline[ x + 1 ] = null; + outline[ x ] = x == xAfterLowerRightCorner ? oAfterLowerRightCorner : null; + } + } + else + { + if ( x < w - 1 && y < h && x != xAfterLowerRightCorner + && thisRow[ x + 2 ] && !prevRow[ x + 2 ] ) + { // at lower right corner && next pxl selected + outline[ x ].append( x + 1, y ); + outline[ x + 1 ].prepend( x + 1, y ); + xAfterLowerRightCorner = x + 1; + oAfterLowerRightCorner = outline[ x ]; + // outline[x + 1] unchanged (the one at the + // right-hand side of (x, y-1) to the top) + outline[ x ] = null; + } + else + { + outline[ x ].append( outline[ x + 1 ] ); // merge + for ( int x1 = 0; x1 <= w; x1++ ) + if ( x1 != x + 1 && outline[ x1 ] == outline[ x + 1 ] ) + { + outline[ x1 ] = outline[ x ]; + outline[ x + 1 ] = null; + outline[ x ] = ( x == xAfterLowerRightCorner ) ? oAfterLowerRightCorner : null; + break; + } + if ( outline[ x + 1 ] != null ) + throw new RuntimeException( "assertion failed" ); + } + } + } + if ( thisRow[ x ] ) + { + // right edge + if ( outline[ x ] == null ) + throw new RuntimeException( "assertion failed" ); + outline[ x ].prepend( x, y + 1 ); + } + } + } + } + return polygons; + } + + /** + * This class implements a Cartesian polygon in progress. The edges are + * supposed to be parallel to the x or y axis. It is implemented as a deque + * to be able to add points to both sides. + */ + private static class Outline + { + + private int[] x, y; + + private int first, last, reserved; + + /** + * Default extra (spare) space when enlarging arrays (similar + * performance with 6-20) + */ + private final int GROW = 10; + + public Outline() + { + reserved = GROW; + x = new int[ reserved ]; + y = new int[ reserved ]; + first = last = GROW / 2; + } + + /** + * Makes sure that enough free space is available at the beginning and + * end of the list, by enlarging the arrays if required + */ + private void needs( final int neededAtBegin, final int neededAtEnd ) + { + if ( neededAtBegin > first || neededAtEnd > reserved - last ) + { + final int extraSpace = Math.max( GROW, Math.abs( x[ last - 1 ] - x[ first ] ) ); + final int newSize = reserved + neededAtBegin + neededAtEnd + extraSpace; + final int newFirst = neededAtBegin + extraSpace / 2; + final int[] newX = new int[ newSize ]; + final int[] newY = new int[ newSize ]; + System.arraycopy( x, first, newX, newFirst, last - first ); + System.arraycopy( y, first, newY, newFirst, last - first ); + x = newX; + y = newY; + last += newFirst - first; + first = newFirst; + reserved = newSize; + } + } + + /** Adds point x, y at the end of the list */ + public void append( final int x, final int y ) + { + if ( last - first >= 2 && collinear( this.x[ last - 2 ], this.y[ last - 2 ], this.x[ last - 1 ], this.y[ last - 1 ], x, y ) ) + { + this.x[ last - 1 ] = x; // replace previous point + this.y[ last - 1 ] = y; + } + else + { + needs( 0, 1 ); // new point + this.x[ last ] = x; + this.y[ last ] = y; + last++; + } + } + + /** Adds point x, y at the beginning of the list */ + public void prepend( final int x, final int y ) + { + if ( last - first >= 2 && collinear( this.x[ first + 1 ], this.y[ first + 1 ], this.x[ first ], this.y[ first ], x, y ) ) + { + this.x[ first ] = x; // replace previous point + this.y[ first ] = y; + } + else + { + needs( 1, 0 ); // new point + first--; + this.x[ first ] = x; + this.y[ first ] = y; + } + } + + /** + * Merge with another Outline by adding it at the end. Thereafter, the + * other outline must not be used any more. + */ + public void append( final Outline o ) + { + final int size = last - first; + final int oSize = o.last - o.first; + if ( size <= o.first && oSize > reserved - last ) + { // we don't have enough space in our own array but in that of 'o' + System.arraycopy( x, first, o.x, o.first - size, size ); + System.arraycopy( y, first, o.y, o.first - size, size ); + x = o.x; + y = o.y; + first = o.first - size; + last = o.last; + reserved = o.reserved; + } + else + { // append to our own array + needs( 0, oSize ); + System.arraycopy( o.x, o.first, x, last, oSize ); + System.arraycopy( o.y, o.first, y, last, oSize ); + last += oSize; + } + } + + /** + * Merge with another Outline by adding it at the beginning. Thereafter, + * the other outline must not be used any more. + */ + public void prepend( final Outline o ) + { + final int size = last - first; + final int oSize = o.last - o.first; + if ( size <= o.reserved - o.last && oSize > first ) + { /* + * We don't have enough space in our own array but in that of + * 'o' so append our own data to that of 'o' + */ + System.arraycopy( x, first, o.x, o.last, size ); + System.arraycopy( y, first, o.y, o.last, size ); + x = o.x; + y = o.y; + first = o.first; + last = o.last + size; + reserved = o.reserved; + } + else + { // prepend to our own array + needs( oSize, 0 ); + first -= oSize; + System.arraycopy( o.x, o.first, x, first, oSize ); + System.arraycopy( o.y, o.first, y, first, oSize ); + } + } + + public Polygon getPolygon() + { + /* + * optimize out intermediate points of straight lines (created, + * e.g., by merging outlines) + */ + int i, j = first + 1; + for ( i = first + 1; i + 1 < last; j++ ) + { + if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ j + 1 ], y[ j + 1 ] ) ) + { + // merge i + 1 into i + last--; + continue; + } + if ( i != j ) + { + x[ i ] = x[ j ]; + y[ i ] = y[ j ]; + } + i++; + } + // wraparound + if ( collinear( x[ j - 1 ], y[ j - 1 ], x[ j ], y[ j ], x[ first ], y[ first ] ) ) + last--; + else + { + x[ i ] = x[ j ]; + y[ i ] = y[ j ]; + } + if ( last - first > 2 && collinear( x[ last - 1 ], y[ last - 1 ], x[ first ], y[ first ], x[ first + 1 ], y[ first + 1 ] ) ) + first++; + + final int count = last - first; + final int[] xNew = new int[ count ]; + final int[] yNew = new int[ count ]; + System.arraycopy( x, first, xNew, 0, count ); + System.arraycopy( y, first, yNew, 0, count ); + return new Polygon( xNew, yNew, count ); + } + + /** Returns whether three points are on one straight line */ + public boolean collinear( final int x1, final int y1, final int x2, final int y2, final int x3, final int y3 ) + { + return ( x2 - x1 ) * ( y3 - y2 ) == ( y2 - y1 ) * ( x3 - x2 ); + } + + @Override + public String toString() + { + String res = "[first:" + first + ",last:" + last + + ",reserved:" + reserved + ":"; + if ( last > x.length ) + System.err.println( "ERROR!" ); + int nmax = 10; // don't print more coordinates than this + for ( int i = first; i < last && i < x.length; i++ ) + { + if ( last - first > nmax && i - first > nmax / 2 ) + { + i = last - nmax / 2; + res += "..."; + nmax = last - first; // dont check again + } + else + res += "(" + x[ i ] + "," + y[ i ] + ")"; + } + return res + "]"; + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java index 1fea44b66..2e011934d 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetector.java @@ -61,10 +61,16 @@ public class ThresholdDetector< T extends RealType< T > & NativeType< T > > impl protected final double threshold; + /** If true, the contours will be simplified. */ + protected final boolean simplify; + /** - * If true, the contours will be smoothed and simplified. + * If strictly larger than 0, the mask will be smoothed before creating the + * mesh, resulting in smoother meshes. The scale value sets the (Gaussian) + * filter radius and is specified in physical units. If 0 or lower than 0, + * no smoothing is applied. */ - protected final boolean simplify; + protected final double smoothingScale; /* * CONSTRUCTORS @@ -75,9 +81,11 @@ public ThresholdDetector( final Interval interval, final double[] calibration, final double threshold, - final boolean simplify ) + final boolean simplify, + final double smoothingScale ) { this.input = input; + this.smoothingScale = smoothingScale; this.interval = DetectionUtils.squeeze( interval ); this.calibration = calibration; this.threshold = threshold; @@ -110,27 +118,15 @@ public boolean checkInput() public boolean process() { final long start = System.currentTimeMillis(); - if ( input.numDimensions() == 2 ) - { - /* - * 2D: we compute and store the contour. - */ - spots = MaskUtils.fromThresholdWithROI( input, interval, calibration, threshold, simplify, numThreads, null ); - - } - else if ( input.numDimensions() == 3 ) - { - /* - * 3D: We create spots of the same volume that of the region. - */ - spots = MaskUtils.fromThreshold( input, interval, calibration, threshold, numThreads ); - } - else - { - errorMessage = baseErrorMessage + "Required a 2D or 3D input, got " + input.numDimensions() + "D."; - return false; - } - + spots = MaskUtils.fromThresholdWithROI( + input, + interval, + calibration, + threshold, + simplify, + smoothingScale, + numThreads, + null ); final long end = System.currentTimeMillis(); this.processingTime = end - start; diff --git a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java index 289043950..d14586080 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/detection/ThresholdDetectorFactory.java @@ -29,6 +29,7 @@ import static fiji.plugin.trackmate.io.IOUtils.writeAttribute; import static fiji.plugin.trackmate.io.IOUtils.writeTargetChannel; import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; +import static fiji.plugin.trackmate.util.TMUtils.checkOptionalParameter; import static fiji.plugin.trackmate.util.TMUtils.checkParameter; import java.util.ArrayList; @@ -73,15 +74,22 @@ public class ThresholdDetectorFactory< T extends RealType< T > & NativeType< T > + "Pixels in the designated channel that have " + "a value larger than the threshold are considered as part of the foreground, " + "and used to build connected regions. In 2D, spots are created with " - + "the (possibly simplified) contour of the region. In 3D, a spherical " - + "spot is created for each region in its center, with a volume equal to the " - + "region volume." + + "the (possibly simplified) contour of the region. In 3D, a mesh is " + + "created for each region." + "

" + "The spot quality stores the object area or volume in pixels." + ""; public static final String KEY_SIMPLIFY_CONTOURS = "SIMPLIFY_CONTOURS"; + /** + * If strictly larger than 0, the mask will be smoothed before creating the + * mesh, resulting in smoother meshes. The scale value sets the (Gaussian) + * filter radius and is specified in physical units. If 0 or lower than 0, + * no smoothing is applied. + */ + public static final String KEY_SMOOTHING_SCALE = "SMOOTHING_SCALE"; + public static final String KEY_INTENSITY_THRESHOLD = "INTENSITY_THRESHOLD"; /* @@ -106,12 +114,13 @@ public boolean setTarget( final ImgPlus< T > img, final Map< String, Object > se this.settings = settings; return checkSettings( settings ); } - + @Override public SpotDetector< T > getDetector( final Interval interval, final int frame ) { final double intensityThreshold = ( Double ) settings.get( KEY_INTENSITY_THRESHOLD ); final boolean simplifyContours = ( Boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final double smoothingScale = ( Double ) settings.getOrDefault( KEY_SMOOTHING_SCALE, -1. ); final double[] calibration = TMUtils.getSpatialCalibration( img ); final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1; final RandomAccessible< T > imFrame = DetectionUtils.prepareFrameImg( img, channel, frame ); @@ -121,7 +130,8 @@ public SpotDetector< T > getDetector( final Interval interval, final int frame ) interval, calibration, intensityThreshold, - simplifyContours ); + simplifyContours, + smoothingScale ); detector.setNumThreads( 1 ); return detector; } @@ -132,6 +142,12 @@ public boolean has2Dsegmentation() return true; } + @Override + public boolean has3Dsegmentation() + { + return true; + } + @Override public String getKey() { @@ -152,15 +168,17 @@ public boolean checkSettings( final Map< String, Object > lSettings ) ok = ok & checkParameter( lSettings, KEY_TARGET_CHANNEL, Integer.class, errorHolder ); ok = ok & checkParameter( lSettings, KEY_INTENSITY_THRESHOLD, Double.class, errorHolder ); ok = ok & checkParameter( lSettings, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + ok = ok & checkOptionalParameter( lSettings, KEY_SMOOTHING_SCALE, Double.class, errorHolder ); final List< String > mandatoryKeys = new ArrayList<>(); mandatoryKeys.add( KEY_TARGET_CHANNEL ); mandatoryKeys.add( KEY_INTENSITY_THRESHOLD ); mandatoryKeys.add( KEY_SIMPLIFY_CONTOURS ); - ok = ok & checkMapKeys( lSettings, mandatoryKeys, null, errorHolder ); + final List< String > optionalKeys = new ArrayList<>(); + optionalKeys.add( KEY_SMOOTHING_SCALE ); + ok = ok & checkMapKeys( lSettings, mandatoryKeys, optionalKeys, errorHolder ); if ( !ok ) - { errorMessage = errorHolder.toString(); - } + return ok; } @@ -170,7 +188,8 @@ public boolean marshall( final Map< String, Object > lSettings, final Element el final StringBuilder errorHolder = new StringBuilder(); final boolean ok = writeTargetChannel( lSettings, element, errorHolder ) && writeAttribute( lSettings, element, KEY_INTENSITY_THRESHOLD, Double.class, errorHolder ) - && writeAttribute( lSettings, element, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + && writeAttribute( lSettings, element, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ) + && writeAttribute( lSettings, element, KEY_SMOOTHING_SCALE, Double.class, errorHolder ); if ( !ok ) errorMessage = errorHolder.toString(); @@ -187,6 +206,7 @@ public boolean unmarshall( final Element element, final Map< String, Object > lS ok = ok & readIntegerAttribute( element, lSettings, KEY_TARGET_CHANNEL, errorHolder ); ok = ok & readDoubleAttribute( element, lSettings, KEY_INTENSITY_THRESHOLD, errorHolder ); ok = ok & readBooleanAttribute( element, lSettings, KEY_SIMPLIFY_CONTOURS, errorHolder ); + ok = ok & readDoubleAttribute( element, lSettings, KEY_SMOOTHING_SCALE, errorHolder ); if ( !ok ) { errorMessage = errorHolder.toString(); @@ -220,6 +240,7 @@ public Map< String, Object > getDefaultSettings() lSettings.put( KEY_TARGET_CHANNEL, DEFAULT_TARGET_CHANNEL ); lSettings.put( KEY_INTENSITY_THRESHOLD, 0. ); lSettings.put( KEY_SIMPLIFY_CONTOURS, true ); + lSettings.put( KEY_SMOOTHING_SCALE, -1. ); return lSettings; } diff --git a/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java b/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java index db871d4ae..10ed6b6b4 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java +++ b/src/main/java/fiji/plugin/trackmate/detection/semiauto/SemiAutoTracker.java @@ -45,7 +45,6 @@ public class SemiAutoTracker< T extends RealType< T > & NativeType< T > > extend private final ImagePlus imp; - @SuppressWarnings( "unchecked" ) public SemiAutoTracker( final Model model, final SelectionModel selectionModel, final ImagePlus imp, final Logger logger ) { super( model, selectionModel, logger ); diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java index a84cf174b..8f22b8513 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureAnalyzer.java @@ -32,21 +32,29 @@ public interface FeatureAnalyzer extends TrackMateModule /** * Returns the list of features this analyzer can compute. + * + * @return the list of features. */ public List< String > getFeatures(); /** * Returns the map of short names for any feature the analyzer can compute. + * + * @return the map of feature short names. */ public Map< String, String > getFeatureShortNames(); /** * Returns the map of names for any feature this analyzer can compute. + * + * @return the map of feature names. */ public Map< String, String > getFeatureNames(); /** * Returns the map of feature dimension this analyzer can compute. + * + * @return the map of feature dimension. */ public Map< String, Dimension > getFeatureDimensions(); @@ -54,6 +62,8 @@ public interface FeatureAnalyzer extends TrackMateModule * Returns the map that states whether the key feature is a feature that * returns integers. If true, then special treatment is applied * when saving/loading, etc. for clarity and precision. + * + * @return the map. */ public Map< String, Boolean > getIsIntFeature(); diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java b/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java index 733fbd0d5..639802788 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureUtils.java @@ -36,6 +36,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; import fiji.plugin.trackmate.features.manual.ManualEdgeColorAnalyzer; import fiji.plugin.trackmate.features.manual.ManualSpotColorAnalyzerFactory; @@ -157,12 +158,18 @@ public static final Map< String, String > collectFeatureKeys( final TrackMateObj } /** - * Missing or undefined values are not included. + * Collect feature values from the specified model. Missing or undefined + * values are not included. * * @param featureKey + * the key of the feature to collect values from. * @param target + * the type of object the feature is defined for. * @param model + * the model to read from. * @param visibleOnly + * if true feature values will be collected only + * from the objects marked as visible. * @return a new double[] array containing the numerical * feature values. */ @@ -383,7 +390,7 @@ public static final FeatureColorGenerator< Integer > createWholeTrackColorGenera final double z = ran.nextDouble(); final double r = ran.nextDouble(); final double q = ran.nextDouble(); - final Spot spot = new Spot( x, y, z, r, q ); + final Spot spot = new SpotBase( x, y, z, r, q ); DUMMY_MODEL.addSpotTo( spot, t ); if ( previous != null ) DUMMY_MODEL.addEdge( previous, spot, ran.nextDouble() ); diff --git a/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java b/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java index 18e109500..74977f052 100644 --- a/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java +++ b/src/main/java/fiji/plugin/trackmate/features/SpotFeatureCalculator.java @@ -40,8 +40,8 @@ import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.features.spot.SpotAnalyzer; import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; -import fiji.plugin.trackmate.util.Threads; import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.util.Threads; import net.imagej.ImgPlus; import net.imglib2.algorithm.MultiThreaded; import net.imglib2.algorithm.MultiThreadedBenchmarkAlgorithm; @@ -127,6 +127,11 @@ public boolean process() * Calculates all the spot features configured in the {@link Settings} * object, but only for the spots in the specified collection. Features are * calculated for each spot, using their location, and the raw image. + * + * @param toCompute + * the spot collection. + * @param doLogIt + * if true the computation will be logged. */ public void computeSpotFeatures( final SpotCollection toCompute, final boolean doLogIt ) { diff --git a/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java b/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java index 0b3136040..a103b427d 100644 --- a/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java +++ b/src/main/java/fiji/plugin/trackmate/features/TrackFeatureCalculator.java @@ -114,6 +114,11 @@ public boolean process() /** * Calculates all the track features configured in the {@link Settings} * object for the specified tracks. + * + * @param trackIDs + * the IDs of the track to compute the features of. + * @param doLogIt + * if true the computation will be logged. */ public void computeTrackFeatures( final Collection< Integer > trackIDs, final boolean doLogIt ) { diff --git a/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java index 5c33d038f..a1a062ecc 100644 --- a/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/edges/EdgeAnalyzer.java @@ -66,6 +66,8 @@ public interface EdgeAnalyzer extends Benchmark, FeatureAnalyzer, MultiThreaded *

* Example of non-local edge feature: the local curvature of the trajectory, * which depends on the neighbor edges. + * + * @return whether this analyzer is a local analyzer. */ public boolean isLocal(); diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull.java b/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull2D.java similarity index 86% rename from src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull.java rename to src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull2D.java index b1387fed5..92bdef088 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/ConvexHull2D.java @@ -25,13 +25,14 @@ import java.util.Collections; import java.util.List; +import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotRoi; /** * Adapted from a code by Kirill Artemov, * https://github.com/DoctorGester/cia-stats. */ -public final class ConvexHull +public final class ConvexHull2D { private static List< Point > makeHull( final List< Point > points ) @@ -121,9 +122,9 @@ public int compareTo( final Point other ) public static SpotRoi convexHull( final SpotRoi roi ) { - final List< Point > points = new ArrayList<>( roi.x.length ); - for ( int i = 0; i < roi.x.length; i++ ) - points.add( new Point( roi.x[ i ], roi.y[ i ] ) ); + final List< Point > points = new ArrayList<>( roi.nPoints() ); + for ( int i = 0; i < roi.nPoints(); i++ ) + points.add( new Point( roi.xr( i ), roi.yr( i ) ) ); final List< Point > hull = makeHull( points ); final double[] xhull = new double[ hull.size() ]; @@ -133,6 +134,11 @@ public static SpotRoi convexHull( final SpotRoi roi ) xhull[ i ] = hull.get( i ).x; yhull[ i ] = hull.get( i ).y; } - return new SpotRoi( xhull, yhull ); + final double xc = roi.getDoublePosition( 0 ); + final double yc = roi.getDoublePosition( 1 ); + final double zc = roi.getDoublePosition( 2 ); + final double r = roi.getFeature( Spot.RADIUS ); + final double quality = roi.getFeature( Spot.QUALITY ); + return new SpotRoi( xc, yc, zc, r, quality, roi.getName(), xhull, yhull ); } } diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzer.java similarity index 84% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzer.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzer.java index 992856912..dfbb4fdc4 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzer.java @@ -27,14 +27,13 @@ import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotRoi; import net.imglib2.type.numeric.RealType; -import net.imglib2.util.Util; -public class SpotFitEllipseAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +public class Spot2DFitEllipseAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > { private final boolean is2D; - public SpotFitEllipseAnalyzer( final boolean is2D ) + public Spot2DFitEllipseAnalyzer( final boolean is2D ) { this.is2D = is2D; } @@ -51,13 +50,13 @@ public void process( final Spot spot ) if ( is2D ) { - final SpotRoi roi = spot.getRoi(); - if ( roi != null ) + if ( spot instanceof SpotRoi ) { - final double[] Q = fitEllipse( roi.x, roi.y ); + final SpotRoi roi = ( SpotRoi ) spot; + final double[] Q = fitEllipse( roi ); final double[] A = quadraticToCartesian( Q ); - x0 = A[ 0 ]; - y0 = A[ 1 ]; + x0 = A[ 0 ] - roi.getDoublePosition( 0 ); + y0 = A[ 1 ] - roi.getDoublePosition( 1 ); major = A[ 2 ]; minor = A[ 3 ]; theta = A[ 4 ]; @@ -83,12 +82,12 @@ public void process( final Spot spot ) theta = Double.NaN; aspectRatio = Double.NaN; } - spot.putFeature( SpotFitEllipseAnalyzerFactory.X0, x0 ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.Y0, y0 ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.MAJOR, major ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.MINOR, minor ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.THETA, theta ); - spot.putFeature( SpotFitEllipseAnalyzerFactory.ASPECTRATIO, aspectRatio ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.X0, x0 ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.Y0, y0 ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.MAJOR, major ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.MINOR, minor ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.THETA, theta ); + spot.putFeature( Spot2DFitEllipseAnalyzerFactory.ASPECTRATIO, aspectRatio ); } /** @@ -112,17 +111,17 @@ public void process( final Spot spot ) * script * @author Michael Doube */ - private static double[] fitEllipse( final double[] x, final double[] y ) + private static double[] fitEllipse( final SpotRoi roi ) { - final int nPoints = x.length; - final double[] centroid = getCentroid( x, y ); - final double xC = centroid[ 0 ]; - final double yC = centroid[ 1 ]; + final double xC = roi.getDoublePosition( 0 ); + final double yC = roi.getDoublePosition( 1 ); + + final int nPoints = roi.nPoints(); final double[][] d1 = new double[ nPoints ][ 3 ]; for ( int i = 0; i < nPoints; i++ ) { - final double xixC = x[ i ] - xC; - final double yiyC = y[ i ] - yC; + final double xixC = roi.xr( i ); + final double yiyC = roi.yr( i ); d1[ i ][ 0 ] = xixC * xixC; d1[ i ][ 1 ] = xixC * yiyC; d1[ i ][ 2 ] = yiyC * yiyC; @@ -131,8 +130,8 @@ private static double[] fitEllipse( final double[] x, final double[] y ) final double[][] d2 = new double[ nPoints ][ 3 ]; for ( int i = 0; i < nPoints; i++ ) { - d2[ i ][ 0 ] = x[ i ] - xC; - d2[ i ][ 1 ] = y[ i ] - yC; + d2[ i ][ 0 ] = roi.xr( i ); + d2[ i ][ 1 ] = roi.yr( i ); d2[ i ][ 2 ] = 1; } final Matrix D2 = new Matrix( d2 ); @@ -182,17 +181,12 @@ private static double[] fitEllipse( final double[] x, final double[] y ) return A.getColumnPackedCopy(); } - private static double[] getCentroid( final double[] x, final double[] y ) - { - return new double[] { Util.average( x ), Util.average( y ) }; - } - /** * Convert to cartesian coordnates for the ellipse. Return [ x0 y0 a b theta * ]. We always have a > b. theta in radians measure the angle of the * ellipse long axis with the x axis, in radians, and positive means * counter-clockwise. - * + * * Formulas from * https://en.wikipedia.org/wiki/Ellipse#In_Cartesian_coordinates */ @@ -241,9 +235,12 @@ else if ( A < 0 ) } /** - * Computes the Moore–Penrose pseudoinverse using the SVD method. - * - * Modified version of the original implementation by Kim van der Linde. + * Computes the Moore–Penrose pseudoinverse using the SVD method. Modified + * version of the original implementation by Kim van der Linde. + * + * @param x + * the matrix. + * @return the pseudo-inverse as a new matrix. */ public static Matrix pinv( final Matrix x ) { diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzerFactory.java similarity index 94% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzerFactory.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzerFactory.java index 23f8f1462..bcff9cab4 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotFitEllipseAnalyzerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DFitEllipseAnalyzerFactory.java @@ -36,8 +36,8 @@ import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; -@Plugin( type = SpotMorphologyAnalyzerFactory.class ) -public class SpotFitEllipseAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements SpotMorphologyAnalyzerFactory< T > +@Plugin( type = Spot2DMorphologyAnalyzerFactory.class ) +public class Spot2DFitEllipseAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot2DMorphologyAnalyzerFactory< T > { public static final String KEY = "Spot fit 2D ellipse"; @@ -94,7 +94,7 @@ public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, f if ( channel != 0 ) return SpotAnalyzer.dummyAnalyzer(); - return new SpotFitEllipseAnalyzer<>( DetectionUtils.is2D( img ) ); + return new Spot2DFitEllipseAnalyzer<>( DetectionUtils.is2D( img ) ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotMorphologyAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DMorphologyAnalyzerFactory.java similarity index 86% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotMorphologyAnalyzerFactory.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DMorphologyAnalyzerFactory.java index 5becf6b1d..3af620dd8 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotMorphologyAnalyzerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DMorphologyAnalyzerFactory.java @@ -26,9 +26,9 @@ /** * Special interface for spot analyzers that can compute feature values based on - * the contour of spots. + * the 2D contour of spots. * * @author Jean-Yves Tinevez - 2020 */ -public interface SpotMorphologyAnalyzerFactory< T extends RealType< T > & NativeType< T > > extends SpotAnalyzerFactoryBase< T > +public interface Spot2DMorphologyAnalyzerFactory< T extends RealType< T > & NativeType< T > > extends SpotAnalyzerFactoryBase< T > {} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzer.java similarity index 67% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzer.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzer.java index 3e7004e56..7d66eeb6c 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzer.java @@ -25,12 +25,12 @@ import fiji.plugin.trackmate.SpotRoi; import net.imglib2.type.numeric.RealType; -public class SpotShapeAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +public class Spot2DShapeAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > { private final boolean is2D; - public SpotShapeAnalyzer( final boolean is2D ) + public Spot2DShapeAnalyzer( final boolean is2D ) { this.is2D = is2D; } @@ -44,12 +44,12 @@ public void process( final Spot spot ) if ( is2D ) { - final SpotRoi roi = spot.getRoi(); - if ( roi != null ) + if ( spot instanceof SpotRoi ) { + final SpotRoi roi = ( SpotRoi ) spot; area = roi.area(); perimeter = getLength( roi ); - final SpotRoi convexHull = ConvexHull.convexHull( roi ); + final SpotRoi convexHull = ConvexHull2D.convexHull( roi ); convexArea = convexHull.area(); } else @@ -71,33 +71,28 @@ public void process( final Spot spot ) final double solidity = area / convexArea; final double shapeIndex = ( area <= 0. ) ? Double.NaN : perimeter / Math.sqrt( area ); - spot.putFeature( SpotShapeAnalyzerFactory.AREA, area ); - spot.putFeature( SpotShapeAnalyzerFactory.PERIMETER, perimeter ); - spot.putFeature( SpotShapeAnalyzerFactory.CIRCULARITY, circularity ); - spot.putFeature( SpotShapeAnalyzerFactory.SOLIDITY, solidity ); - spot.putFeature( SpotShapeAnalyzerFactory.SHAPE_INDEX, shapeIndex ); + spot.putFeature( Spot2DShapeAnalyzerFactory.AREA, area ); + spot.putFeature( Spot2DShapeAnalyzerFactory.PERIMETER, perimeter ); + spot.putFeature( Spot2DShapeAnalyzerFactory.CIRCULARITY, circularity ); + spot.putFeature( Spot2DShapeAnalyzerFactory.SOLIDITY, solidity ); + spot.putFeature( Spot2DShapeAnalyzerFactory.SHAPE_INDEX, shapeIndex ); } private static final double getLength( final SpotRoi roi ) { - final double[] x = roi.x; - final double[] y = roi.y; - final int npoints = x.length; - if ( npoints < 2 ) + final int nPoints = roi.nPoints(); + if ( nPoints < 2 ) return 0; double length = 0; - for ( int i = 0; i < npoints - 1; i++ ) + int i; + int j; + for ( i = 0, j = nPoints - 1; i < nPoints; j = i++ ) { - final double dx = x[ i + 1 ] - x[ i ]; - final double dy = y[ i + 1 ] - y[ i ]; + final double dx = roi.x( i ) - roi.x( j ); + final double dy = roi.y( i ) - roi.y( j ); length += Math.sqrt( dx * dx + dy * dy ); } - - final double dx0 = x[ 0 ] - x[ npoints - 1 ]; - final double dy0 = y[ 0 ] - y[ npoints - 1 ]; - length += Math.sqrt( dx0 * dx0 + dy0 * dy0 ); - return length; } } diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzerFactory.java similarity index 94% rename from src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzerFactory.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzerFactory.java index 25c2d3bf2..62a653b39 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotShapeAnalyzerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot2DShapeAnalyzerFactory.java @@ -36,8 +36,8 @@ import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; -@Plugin( type = SpotMorphologyAnalyzerFactory.class ) -public class SpotShapeAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements SpotMorphologyAnalyzerFactory< T > +@Plugin( type = Spot2DMorphologyAnalyzerFactory.class ) +public class Spot2DShapeAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot2DMorphologyAnalyzerFactory< T > { public static final String KEY = "Spot 2D shape descriptors"; @@ -88,7 +88,7 @@ public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, f if ( channel != 0 ) return SpotAnalyzer.dummyAnalyzer(); - return new SpotShapeAnalyzer<>( DetectionUtils.is2D( img ) ); + return new Spot2DShapeAnalyzer<>( DetectionUtils.is2D( img ) ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzer.java new file mode 100644 index 000000000..648af607a --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzer.java @@ -0,0 +1,179 @@ +/*- + * #%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.features.spot; + +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.ASPECTRATIO; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.ELLIPSOID_SHAPE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MAJOR; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MAJOR_PHI; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MAJOR_THETA; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MEDIAN; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MEDIAN_PHI; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MEDIAN_THETA; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MINOR; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MINOR_PHI; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.MINOR_THETA; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_CLASS_TOLERANCE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_ELLIPSOID; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_OBLATE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_PROLATE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.SHAPE_SPHERE; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.X0; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.Y0; +import static fiji.plugin.trackmate.features.spot.Spot3DFitEllipsoidAnalyzerFactory.Z0; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.RealLocalizable; +import net.imglib2.mesh.alg.EllipsoidFitter; +import net.imglib2.mesh.alg.EllipsoidFitter.Ellipsoid; +import net.imglib2.type.numeric.RealType; + +public class Spot3DFitEllipsoidAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +{ + + private final boolean is3D; + + public Spot3DFitEllipsoidAnalyzer( final boolean is3D ) + { + this.is3D = is3D; + } + + @Override + public void process( final Spot spot ) + { + final double x0; + final double y0; + final double z0; + final double rA; + final double rB; + final double rC; + final double phiA; + final double thetaA; + final double phiB; + final double thetaB; + final double phiC; + final double thetaC; + final double aspectRatio; + final int shapeIndex; + + if ( is3D ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + final Ellipsoid fit = EllipsoidFitter.fit( sm.getMesh() ); + x0 = fit.center.getDoublePosition( 0 ); + y0 = fit.center.getDoublePosition( 1 ); + z0 = fit.center.getDoublePosition( 2 ); + rA = Math.abs( fit.r1 ); + rB = Math.abs( fit.r2 ); + rC = Math.abs( fit.r3 ); + aspectRatio = rA / rC; + final double drAB = ( rB - rA ) / rB; + final double drBC = ( rC - rB ) / rC; + if ( drAB < SHAPE_CLASS_TOLERANCE && drBC < SHAPE_CLASS_TOLERANCE ) + shapeIndex = SHAPE_SPHERE; + else if ( drBC < SHAPE_CLASS_TOLERANCE ) + shapeIndex = SHAPE_OBLATE; + else if ( drAB < SHAPE_CLASS_TOLERANCE ) + shapeIndex = SHAPE_PROLATE; + else + shapeIndex = SHAPE_ELLIPSOID; + + phiA = phi( fit.ev1 ); + phiB = phi( fit.ev2 ); + phiC = phi( fit.ev3 ); + thetaA = theta( fit.ev1 ); + thetaB = theta( fit.ev2 ); + thetaC = theta( fit.ev3 ); + + } + else + { + // Assume plain sphere. + x0 = 0.; + y0 = 0.; + z0 = 0.; + final double radius = spot.getFeature( Spot.RADIUS ); + rA = radius; + rB = radius; + rC = radius; + aspectRatio = 1.; + shapeIndex = SHAPE_ELLIPSOID; + + phiA = 0.; + phiB = 0.; + phiC = 0.; + thetaA = 0.; + thetaB = 0.; + thetaC = 0.; + } + } + else + { + // Undefined for 2D: default to NaN. + x0 = Double.NaN; + y0 = Double.NaN; + z0 = Double.NaN; + rA = Double.NaN; + rB = Double.NaN; + rC = Double.NaN; + aspectRatio = Double.NaN; + shapeIndex = SHAPE_ELLIPSOID; + + phiA = Double.NaN; + phiB = Double.NaN; + phiC = Double.NaN; + thetaA = Double.NaN; + thetaB = Double.NaN; + thetaC = Double.NaN; + } + spot.putFeature( X0, x0 ); + spot.putFeature( Y0, y0 ); + spot.putFeature( Z0, z0 ); + spot.putFeature( MINOR, rA ); + spot.putFeature( MEDIAN, rB ); + spot.putFeature( MAJOR, rC ); + spot.putFeature( MINOR_PHI, phiA ); + spot.putFeature( MEDIAN_PHI, phiB ); + spot.putFeature( MAJOR_PHI, phiC ); + spot.putFeature( MINOR_THETA, thetaA ); + spot.putFeature( MEDIAN_THETA, thetaB ); + spot.putFeature( MAJOR_THETA, thetaC ); + spot.putFeature( ASPECTRATIO, aspectRatio ); + spot.putFeature( ELLIPSOID_SHAPE, ( double ) shapeIndex ); + } + + private double theta( final RealLocalizable v ) + { + final double z = v.getDoublePosition( 2 ); + return Math.acos( z ); + } + + private static final double phi( final RealLocalizable v ) + { + final double x = v.getDoublePosition( 0 ); + final double y = v.getDoublePosition( 1 ); + return Math.atan2( y, x ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzerFactory.java new file mode 100644 index 000000000..064c74b02 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DFitEllipsoidAnalyzerFactory.java @@ -0,0 +1,237 @@ +/*- + * #%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.features.spot; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.detection.DetectionUtils; +import net.imagej.ImgPlus; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +@Plugin( type = Spot3DMorphologyAnalyzerFactory.class ) +public class Spot3DFitEllipsoidAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot3DMorphologyAnalyzerFactory< T > +{ + + public static final String KEY = "Spot fit 3D ellipsoid"; + + public static final String X0 = "ELLIPSOID_X0"; + public static final String Y0 = "ELLIPSOID_Y0"; + public static final String Z0 = "ELLIPSOID_Z0"; + public static final String MAJOR = "ELLIPSOID_MAJOR_LENGTH"; + public static final String MEDIAN = "ELLIPSOID_MEDIAN_LENGTH"; + public static final String MINOR = "ELLIPSOID_MINOR_LENGTH"; + public static final String MAJOR_PHI = "ELLIPSOID_MAJOR_PHI"; + public static final String MAJOR_THETA = "ELLIPSOID_MAJOR_THETA"; + public static final String MEDIAN_PHI = "ELLIPSOID_MEDIAN_PHI"; + public static final String MEDIAN_THETA = "ELLIPSOID_MEDIAN_THETA"; + public static final String MINOR_PHI = "ELLIPSOID_MINOR_PHI"; + public static final String MINOR_THETA = "ELLIPSOID_MINOR_THETA"; + public static final String ASPECTRATIO = "ELLIPSOID_ASPECTRATIO"; + public static final String ELLIPSOID_SHAPE = "ELLIPSOID_SHAPE"; + + /** Denotes an ellipsoid with no particular shape. */ + public static final int SHAPE_ELLIPSOID = 0; + + /** + * Denotes an ellipsoid with the oblate shape. The two largest radii are + * roughly equal. Resembles a lentil. + */ + public static final int SHAPE_OBLATE = 1; + + /** + * Denotes an ellipsoid with the prolate shape. The two smallest radii are + * roughly equal. Resembles a rugby balloon. + */ + public static final int SHAPE_PROLATE = 2; + + /** + * Denotes an ellipsoid with the spherical shape. The three radii are + * roughly equal. Resembles a sphere + */ + public static final int SHAPE_SPHERE = 3; + + /** + * Tolerance in percentage on the radii values to be considered roughly + * equals. + * + * @see #SHAPE_ELLIPSOID + * @see #SHAPE_OBLATE + * @see #SHAPE_PROLATE + * @see #SHAPE_SPHERE + */ + public static final double SHAPE_CLASS_TOLERANCE = 0.1; + + private static final List< String > FEATURES = Arrays.asList( new String[] { + X0, Y0, Z0, + MAJOR, MEDIAN, MINOR, + MAJOR_PHI, MAJOR_THETA, + MEDIAN_PHI, MEDIAN_THETA, + MINOR_PHI, MINOR_THETA, + ASPECTRATIO, + ELLIPSOID_SHAPE } ); + private static final Map< String, String > FEATURE_SHORTNAMES = new HashMap< >(); + private static final Map< String, String > FEATURE_NAMES = new HashMap< >(); + private static final Map< String, Dimension > FEATURE_DIMENSIONS = new HashMap< >(); + private static final Map< String, Boolean > FEATURE_ISINTS = new HashMap< >(); + static + { + FEATURE_SHORTNAMES.put( X0, "El. x0" ); + FEATURE_SHORTNAMES.put( Y0, "El. y0" ); + FEATURE_SHORTNAMES.put( Z0, "El. z0" ); + FEATURE_SHORTNAMES.put( MAJOR, "El. long axis" ); + FEATURE_SHORTNAMES.put( MEDIAN, "El. med. axis" ); + FEATURE_SHORTNAMES.put( MINOR, "El. sh. axis" ); + FEATURE_SHORTNAMES.put( MAJOR_PHI, "El. l.a. phi" ); + FEATURE_SHORTNAMES.put( MEDIAN_PHI, "El. m.a. phi" ); + FEATURE_SHORTNAMES.put( MINOR_PHI, "El. s.a. phi" ); + FEATURE_SHORTNAMES.put( MAJOR_THETA, "El. l.a. theta" ); + FEATURE_SHORTNAMES.put( MEDIAN_THETA, "El. m.a. theta" ); + FEATURE_SHORTNAMES.put( MINOR_THETA, "El. s.a. theta" ); + FEATURE_SHORTNAMES.put( ASPECTRATIO, "El. a.r." ); + FEATURE_SHORTNAMES.put( ELLIPSOID_SHAPE, "El. shape" ); + + FEATURE_NAMES.put( X0, "Ellipsoid center x0" ); + FEATURE_NAMES.put( Y0, "Ellipsoid center y0" ); + FEATURE_NAMES.put( Z0, "Ellipsoid center z0" ); + FEATURE_NAMES.put( MAJOR, "Ellipsoid long axis" ); + FEATURE_NAMES.put( MEDIAN, "Ellipsoid long axis" ); + FEATURE_NAMES.put( MINOR, "Ellipsoid short axis" ); + FEATURE_NAMES.put( MAJOR_PHI, "Ellipsoid long axis phi" ); + FEATURE_NAMES.put( MEDIAN_PHI, "Ellipsoid long axis. phi" ); + FEATURE_NAMES.put( MINOR_PHI, "Ellipsoid short axis phi" ); + FEATURE_NAMES.put( MAJOR_THETA, "Ellipsoid long axis theta" ); + FEATURE_NAMES.put( MEDIAN_THETA, "Ellipsoid long axis theta" ); + FEATURE_NAMES.put( MINOR_THETA, "Ellipsoid short axis theta" ); + FEATURE_NAMES.put( ASPECTRATIO, "Ellipsoid aspect ratio" ); + FEATURE_NAMES.put( ELLIPSOID_SHAPE, "Ellipsoid shape class" ); + + FEATURE_DIMENSIONS.put( X0, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( Y0, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( Z0, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MAJOR, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MEDIAN, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MINOR, Dimension.LENGTH ); + FEATURE_DIMENSIONS.put( MAJOR_PHI, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MAJOR_THETA, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MEDIAN_PHI, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MEDIAN_THETA, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MINOR_PHI, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( MINOR_THETA, Dimension.ANGLE ); + FEATURE_DIMENSIONS.put( ASPECTRATIO, Dimension.NONE ); + FEATURE_DIMENSIONS.put( ELLIPSOID_SHAPE, Dimension.NONE ); + + FEATURE_ISINTS.put( X0, Boolean.FALSE ); + FEATURE_ISINTS.put( Y0, Boolean.FALSE ); + FEATURE_ISINTS.put( Z0, Boolean.FALSE ); + FEATURE_ISINTS.put( MAJOR, Boolean.FALSE ); + FEATURE_ISINTS.put( MEDIAN, Boolean.FALSE ); + FEATURE_ISINTS.put( MINOR, Boolean.FALSE ); + FEATURE_ISINTS.put( MAJOR_PHI, Boolean.FALSE ); + FEATURE_ISINTS.put( MAJOR_THETA, Boolean.FALSE ); + FEATURE_ISINTS.put( MEDIAN_PHI, Boolean.FALSE ); + FEATURE_ISINTS.put( MEDIAN_THETA, Boolean.FALSE ); + FEATURE_ISINTS.put( MINOR_PHI, Boolean.FALSE ); + FEATURE_ISINTS.put( MINOR_THETA, Boolean.FALSE ); + FEATURE_ISINTS.put( ASPECTRATIO, Boolean.FALSE ); + FEATURE_ISINTS.put( ELLIPSOID_SHAPE, Boolean.TRUE ); + } + + + @Override + public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, final int channel ) + { + // Don't run more than once. + if ( channel != 0 ) + return SpotAnalyzer.dummyAnalyzer(); + + return new Spot3DFitEllipsoidAnalyzer<>( !DetectionUtils.is2D( img ) ); + } + + @Override + public List< String > getFeatures() + { + return FEATURES; + } + + @Override + public Map< String, String > getFeatureShortNames() + { + return FEATURE_SHORTNAMES; + } + + @Override + public Map< String, String > getFeatureNames() + { + return FEATURE_NAMES; + } + + @Override + public Map< String, Dimension > getFeatureDimensions() + { + return FEATURE_DIMENSIONS; + } + + @Override + public Map< String, Boolean > getIsIntFeature() + { + return FEATURE_ISINTS; + } + + @Override + public boolean isManualFeature() + { + return false; + } + + @Override + public String getInfoText() + { + return null; + } + + @Override + public ImageIcon getIcon() + { + return null; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public String getName() + { + return KEY; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/graph/StringFormater.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DMorphologyAnalyzerFactory.java similarity index 65% rename from src/main/java/fiji/plugin/trackmate/graph/StringFormater.java rename to src/main/java/fiji/plugin/trackmate/features/spot/Spot3DMorphologyAnalyzerFactory.java index cef4b94bf..e6fdfe20f 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/StringFormater.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DMorphologyAnalyzerFactory.java @@ -19,21 +19,16 @@ * . * #L% */ -package fiji.plugin.trackmate.graph; +package fiji.plugin.trackmate.features.spot; + +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; /** - * Interface for function that can build a human-readable string representation - * of an object - * - * @author JeanYves + * Special interface for spot analyzers that can compute feature values based on + * the 3D mesh building the 3D shape of spots. * + * @author Jean-Yves Tinevez - 2023 */ -public interface StringFormater< V > -{ - - /** - * Convert the given instance to a string representation. - */ - public String toString( V instance ); - -} +public interface Spot3DMorphologyAnalyzerFactory< T extends RealType< T > & NativeType< T > > extends SpotAnalyzerFactoryBase< T > +{} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzer.java new file mode 100644 index 000000000..890bbb2ae --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzer.java @@ -0,0 +1,91 @@ +/*- + * #%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.features.spot; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.mesh.MeshStats; +import net.imglib2.mesh.alg.hull.ConvexHull; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.type.numeric.RealType; + +public class Spot3DShapeAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > +{ + + private final boolean is3D; + + public Spot3DShapeAnalyzer( final boolean is3D ) + { + this.is3D = is3D; + } + + @Override + public void process( final Spot spot ) + { + double volume; + double sa; + double solidity; + double convexity; + double sphericity; + if ( is3D ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + final NaiveDoubleMesh ch = ConvexHull.calculate( sm.getMesh() ); + volume = sm.volume(); + final double volumeCH = MeshStats.volume( ch ); + solidity = volume / volumeCH; + + sa = MeshStats.surfaceArea( sm.getMesh() ); + final double saCH = MeshStats.surfaceArea( ch ); + convexity = sa / saCH; + + final double sphereArea = Math.pow( Math.PI, 1. / 3. ) + * Math.pow( 6. * volume, 2. / 3. ); + sphericity = sphereArea / sa; + } + else + { + final double radius = spot.getFeature( Spot.RADIUS ); + volume = 4. / 3. * Math.PI * radius * radius * radius; + sa = 4. * Math.PI * radius * radius; + solidity = 1.; + convexity = 1.; + sphericity = 1.; + } + } + else + { + volume = Double.NaN; + sa = Double.NaN; + solidity = Double.NaN; + convexity = Double.NaN; + sphericity = Double.NaN; + } + spot.putFeature( Spot3DShapeAnalyzerFactory.VOLUME, volume ); + spot.putFeature( Spot3DShapeAnalyzerFactory.SURFACE_AREA, sa ); + spot.putFeature( Spot3DShapeAnalyzerFactory.SPHERICITY, sphericity ); + spot.putFeature( Spot3DShapeAnalyzerFactory.SOLIDITY, solidity ); + spot.putFeature( Spot3DShapeAnalyzerFactory.CONVEXITY, convexity ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzerFactory.java b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzerFactory.java new file mode 100644 index 000000000..228261fed --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/features/spot/Spot3DShapeAnalyzerFactory.java @@ -0,0 +1,153 @@ +/*- + * #%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.features.spot; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.ImageIcon; + +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.detection.DetectionUtils; +import net.imagej.ImgPlus; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +@Plugin( type = Spot3DMorphologyAnalyzerFactory.class ) +public class Spot3DShapeAnalyzerFactory< T extends RealType< T > & NativeType< T > > implements Spot3DMorphologyAnalyzerFactory< T > +{ + + public static final String KEY = "Spot 3D shape descriptors"; + + public static final String VOLUME = "VOLUME"; + public static final String SURFACE_AREA = "SURFACE_AREA"; + public static final String SPHERICITY = "SPHERICITY"; + public static final String SOLIDITY = "SOLIDITY"; + public static final String CONVEXITY = "CONVEXITY"; + + private static final List< String > FEATURES = Arrays.asList( new String[] { + VOLUME, SURFACE_AREA, SPHERICITY, SOLIDITY, CONVEXITY } ); + private static final Map< String, String > FEATURE_SHORTNAMES = new HashMap< >(); + private static final Map< String, String > FEATURE_NAMES = new HashMap< >(); + private static final Map< String, Dimension > FEATURE_DIMENSIONS = new HashMap< >(); + private static final Map< String, Boolean > FEATURE_ISINTS = new HashMap< >(); + static + { + FEATURE_SHORTNAMES.put( VOLUME, "Volume" ); + FEATURE_SHORTNAMES.put( SURFACE_AREA, "Surf. area" ); + FEATURE_SHORTNAMES.put( SPHERICITY, "Sphericity" ); + FEATURE_SHORTNAMES.put( CONVEXITY, "Conv." ); + FEATURE_SHORTNAMES.put( SOLIDITY, "Solidity" ); + + FEATURE_NAMES.put( VOLUME, "Volume" ); + FEATURE_NAMES.put( SURFACE_AREA, "Surface area" ); + FEATURE_NAMES.put( SPHERICITY, "Sphericity" ); + FEATURE_NAMES.put( CONVEXITY, "Convexity" ); + FEATURE_NAMES.put( SOLIDITY, "Solidity" ); + + FEATURE_DIMENSIONS.put( SURFACE_AREA, Dimension.AREA ); + FEATURE_DIMENSIONS.put( VOLUME, Dimension.VOLUME ); + FEATURE_DIMENSIONS.put( SPHERICITY, Dimension.NONE ); + FEATURE_DIMENSIONS.put( CONVEXITY, Dimension.NONE ); + FEATURE_DIMENSIONS.put( SOLIDITY, Dimension.NONE ); + + FEATURE_ISINTS.put( VOLUME, Boolean.FALSE ); + FEATURE_ISINTS.put( SURFACE_AREA, Boolean.FALSE ); + FEATURE_ISINTS.put( SPHERICITY, Boolean.FALSE ); + FEATURE_ISINTS.put( CONVEXITY, Boolean.FALSE ); + FEATURE_ISINTS.put( SOLIDITY, Boolean.FALSE ); + } + + @Override + public SpotAnalyzer< T > getAnalyzer( final ImgPlus< T > img, final int frame, final int channel ) + { + // Don't run more than once. + if ( channel != 0 ) + return SpotAnalyzer.dummyAnalyzer(); + + return new Spot3DShapeAnalyzer<>( !DetectionUtils.is2D( img ) ); + } + + @Override + public List< String > getFeatures() + { + return FEATURES; + } + + @Override + public Map< String, String > getFeatureShortNames() + { + return FEATURE_SHORTNAMES; + } + + @Override + public Map< String, String > getFeatureNames() + { + return FEATURE_NAMES; + } + + @Override + public Map< String, Dimension > getFeatureDimensions() + { + return FEATURE_DIMENSIONS; + } + + @Override + public Map< String, Boolean > getIsIntFeature() + { + return FEATURE_ISINTS; + } + + @Override + public boolean isManualFeature() + { + return false; + } + + @Override + public String getInfoText() + { + return null; + } + + @Override + public ImageIcon getIcon() + { + return null; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public String getName() + { + return KEY; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java b/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java index 4b36ee8fa..80004d10e 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/SpotAnalyzerFactoryBase.java @@ -53,6 +53,7 @@ public interface SpotAnalyzerFactoryBase< T extends RealType< T > & NativeType< * the target frame to operate on. * @param channel * the target channel to operate on. + * @return a new spot analyzer. */ public SpotAnalyzer< T > getAnalyzer( ImgPlus< T > img, int frame, int channel ); diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java index 2bd128663..6ae6e2f0a 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/SpotContrastAndSNRAnalyzer.java @@ -29,11 +29,6 @@ import static fiji.plugin.trackmate.features.spot.SpotIntensityMultiCAnalyzerFactory.makeFeatureKey; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.detection.DetectionUtils; -import fiji.plugin.trackmate.util.SpotNeighborhood; -import fiji.plugin.trackmate.util.SpotNeighborhoodCursor; -import fiji.plugin.trackmate.util.SpotUtil; import net.imagej.ImgPlus; import net.imglib2.IterableInterval; import net.imglib2.type.numeric.RealType; @@ -54,7 +49,7 @@ * Important: this analyzer relies on some results provided by the * {@link SpotIntensityMultiCAnalyzer} analyzer. Thus, it must be run * after it. - * + * * @author Jean-Yves Tinevez, 2011 - 2012. Revised December 2020. */ public class SpotContrastAndSNRAnalyzer< T extends RealType< T > > extends AbstractSpotFeatureAnalyzer< T > @@ -72,7 +67,7 @@ public class SpotContrastAndSNRAnalyzer< T extends RealType< T > > extends Abstr /** * Instantiates an analyzer for contrast and SNR. - * + * * @param img * the 2D or 3D image of the desired time-point and channel to * operate on, @@ -99,76 +94,33 @@ public final void process( final Spot spot ) final double radius = spot.getFeature( Spot.RADIUS ); final double outterRadius = 2. * radius; - // Operate on ROI only if we have one and the image is 2D. - final double meanOut; - final SpotRoi roi = spot.getRoi(); - if ( null != roi && DetectionUtils.is2D( img ) ) - { - final double alpha = outterRadius / radius; - final SpotRoi outterRoi = roi.copy(); - outterRoi.scale( alpha ); - final IterableInterval< T > neighborhood = SpotUtil.iterable( outterRoi, spot, img ); - double totalSum = 0.; - int nTotal = 0; // Total number of non-NaN pixels - - // Iterate over the big ROI. - for ( final T t : neighborhood ) - { - final double val = t.getRealDouble(); - if ( Double.isNaN( val ) ) - continue; - nTotal++; - totalSum += val; - } - - // Sum intensity inside (over non-NaN pixels). - final String sumFeature = makeFeatureKey( TOTAL_INTENSITY, channel ); - final double innerSum = spot.getFeature( sumFeature ); - - // Compute number of non-NaN pixels in the inner roi. - final int nInner = ( int ) ( innerSum / meanIn ); - - // Total number of non-NaN pixels in the outer roi. - final int nOut = nTotal - nInner; - - final double outterSum = totalSum - innerSum; - meanOut = outterSum / nOut; - } - else + final double alpha = outterRadius / radius; + final Spot outterRoi = spot.copy(); + outterRoi.scale( alpha ); + final IterableInterval< T > neighborhood = outterRoi.iterable( img ); + double totalSum = 0.; + int nTotal = 0; + for ( final T t : neighborhood ) { - // Otherwise default to circle / sphere. - final Spot largeSpot = new Spot( spot ); - largeSpot.putFeature( Spot.RADIUS, outterRadius ); - final SpotNeighborhood< T > neighborhood = new SpotNeighborhood<>( largeSpot, img ); - if ( neighborhood.size() <= 1 ) - { - spot.putFeature( makeFeatureKey( CONTRAST, channel ), Double.NaN ); - spot.putFeature( makeFeatureKey( SNR, channel ), Double.NaN ); - return; - } - - final double radius2 = radius * radius; - int nOut = 0; // Outer number of non-NaN pixels. - double sumOut = 0; - - // Compute mean in the outer ring - final SpotNeighborhoodCursor< T > cursor = neighborhood.cursor(); - while ( cursor.hasNext() ) - { - cursor.fwd(); - final double dist2 = cursor.getDistanceSquared(); - if ( dist2 > radius2 ) - { - final double val = cursor.get().getRealDouble(); - if ( Double.isNaN( val ) ) - continue; - nOut++; - sumOut += val; - } - } - meanOut = sumOut / nOut; + final double val = t.getRealDouble(); + if ( Double.isNaN( val ) ) + continue; + nTotal++; + totalSum += val; } + final String sumFeature = makeFeatureKey( TOTAL_INTENSITY, channel ); + final double innerSum = spot.getFeature( sumFeature ); + + // Compute number of non-NaN pixels in the inner roi. + final int nInner = ( int ) ( innerSum / meanIn ); + + // Total number of non-NaN pixels in the outer roi. + final int nOut = nTotal - nInner; + + final double outterSum = totalSum - innerSum; + final double meanOut = outterSum / nOut; + // Compute contrast final double contrast = ( meanIn - meanOut ) / ( meanIn + meanOut ); @@ -179,4 +131,3 @@ public final void process( final Spot spot ) spot.putFeature( makeFeatureKey( SNR, channel ), snr ); } } - diff --git a/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java index a09b38a51..dd6c510e2 100644 --- a/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/spot/SpotIntensityMultiCAnalyzer.java @@ -31,7 +31,6 @@ import org.scijava.util.DoubleArray; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.util.SpotUtil; import fiji.plugin.trackmate.util.TMUtils; import net.imagej.ImgPlus; import net.imglib2.IterableInterval; @@ -54,7 +53,7 @@ public SpotIntensityMultiCAnalyzer( final ImgPlus< T > imgCT, final int channel @Override public void process( final Spot spot ) { - final IterableInterval< T > neighborhood = SpotUtil.iterable( spot, imgCT ); + final IterableInterval< T > neighborhood = spot.iterable( imgCT ); final DoubleArray intensities = new DoubleArray(); for ( final T pixel : neighborhood ) diff --git a/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java b/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java index 74cd391cc..6e207a605 100644 --- a/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java +++ b/src/main/java/fiji/plugin/trackmate/features/track/TrackAnalyzer.java @@ -23,10 +23,10 @@ import java.util.Collection; -import net.imglib2.algorithm.Benchmark; -import net.imglib2.algorithm.MultiThreaded; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.features.FeatureAnalyzer; +import net.imglib2.algorithm.Benchmark; +import net.imglib2.algorithm.MultiThreaded; /** * Mother interface for the classes that can compute the feature of tracks. @@ -80,6 +80,8 @@ public interface TrackAnalyzer extends Benchmark, FeatureAnalyzer, MultiThreaded *

* Example of a non-local track feature: the rank of the track sorted by its * number of spots, compared to other tracks. + * + * @return whether this analyzer is a local analyzer. */ public boolean isLocal(); diff --git a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java index a9055ee1c..5bdd82da1 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java +++ b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java @@ -85,6 +85,10 @@ public static < V, E > SimpleWeightedGraph< V, E > convertToSimpleWeightedGraph( } /** + * Pretty-prints a model. + * + * @param model + * the model. * @return a pretty-print string representation of a {@link TrackModel}, as * long it is a tree (each spot must not have more than one * predecessor). @@ -421,8 +425,14 @@ private static char[] makeChars( final int width, final char c ) } /** - * @return true only if the given model is a tree; that is: every spot has - * one or less predecessors. + * Returns the siblings of a spot. That is: all the spots that have the same + * predecessor. + * + * @param cache + * a neighbor cache. + * @param spot + * the spot to inspect. + * @return a new set made of the spot siblings. Includes the spot. */ public static final Set< Spot > getSibblings( final NeighborCache< Spot, DefaultWeightedEdge > cache, final Spot spot ) { diff --git a/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java b/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java index c176c36f6..c97cb348d 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java +++ b/src/main/java/fiji/plugin/trackmate/graph/SortedDepthFirstIterator.java @@ -132,7 +132,9 @@ private static enum VisitColor * the graph to be iterated. * @param startVertex * the vertex iteration to be started. - * + * @param comparator + * used to compare the several children of a vertex, and + * specifies in what order they are iterated. * @throws IllegalArgumentException * if g==null or does not contain * startVertex @@ -276,7 +278,7 @@ private static < V, E > Specifics< V, E > createGraphSpecifics( final Graph< V, return new UndirectedSpecifics<>( g ); } - /** + /* * This is where we add the multiple children in proper sorted order. */ protected void addUnseenChildrenOf( final V vertex ) @@ -352,18 +354,12 @@ private boolean isConnectedComponentExhausted() } } - /** - * @param edge - */ protected void encounterVertex( final V vertex, final E edge ) { seen.put( vertex, VisitColor.WHITE ); stack.addLast( vertex ); } - /** - * @param edge - */ protected void encounterVertexAgain( final V vertex, final E edge ) { final VisitColor color = seen.get( vertex ); diff --git a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java index d2c38ee2d..123c06ccc 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java +++ b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java @@ -121,8 +121,10 @@ public static Color textColorForBackground( final Color backgroundColor ) * https://stackoverflow.com/questions/4593469/java-how-to-convert-rgb-color-to-cie-lab * * @param a + * the first color. * @param b - * @return + * the second color. + * @return the distance between the two colors. */ public static double colorDistance( final Color a, final Color b ) { @@ -179,7 +181,12 @@ public static final Color invert( final Color color ) } /** - * Positions a JFrame more or less cleverly next a {@link Component}. + * Positions a window more or less cleverly next a {@link Component}. + * + * @param gui + * the window to position. + * @param component + * the component to position the window with respect to. */ public static void positionWindow( final Window gui, final Component component ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/Icons.java b/src/main/java/fiji/plugin/trackmate/gui/Icons.java index 04211d7ed..fcfb52147 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/Icons.java +++ b/src/main/java/fiji/plugin/trackmate/gui/Icons.java @@ -203,4 +203,9 @@ public class Icons public static final ImageIcon VECTOR_ICON = new ImageIcon( Icons.class.getResource( "images/vector.png" ) ); + public static final ImageIcon BULLET_GREEN_ICON = new ImageIcon( Icons.class.getResource( "images/bullet_green.png" ) ); + + public static final ImageIcon QUESTION_ICON = new ImageIcon( Icons.class.getResource( "images/help.png" ) ); + + public static final ImageIcon BVV_ICON = new ImageIcon( Icons.class.getResource( "images/TrackMateBVV-logo-16x16.png" ) ); } diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/CategoryJComboBox.java b/src/main/java/fiji/plugin/trackmate/gui/components/CategoryJComboBox.java index 5dbe8031c..dcf03a345 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/CategoryJComboBox.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/CategoryJComboBox.java @@ -270,9 +270,6 @@ public void actionPerformed( final ActionEvent e ) im.put( KeyStroke.getKeyStroke( KeyEvent.VK_KP_DOWN, 0 ), "selectNext" ); } - /** - * Demo - */ public static void main( final String[] args ) { // diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java index 694daa0bf..583206e2a 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigurationPanel.java @@ -45,12 +45,18 @@ public abstract class ConfigurationPanel extends JPanel public final ActionEvent PREVIEW_BUTTON_PUSHED = new ActionEvent( this, 0, "PreviewButtonPushed" ); /** - * Echo the parameters of the given settings on this panel. + * Echoes the parameters of the given settings on this panel. + * + * @param settings + * the settings as a map. */ public abstract void setSettings( final Map< String, Object > settings ); /** - * @return a new settings map object with its values set by this panel. + * Returns a new settings map of string-object with its values set by this + * panel. + * + * @return a new map. */ public abstract Map< String, Object > getSettings(); 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 346eb1f26..a525a8f91 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -80,6 +80,7 @@ public ConfigureViewsPanel( final DisplaySettings ds, final FeatureDisplaySelector featureSelector, final String spaceUnits, + final Action launchBVVAction, final Action launchTrackSchemeAction, final Action showTrackTablesAction, final Action showSpotTableAction, @@ -358,6 +359,11 @@ public ConfigureViewsPanel( final JPanel panelButtons = new JPanel(); panelButtons.setLayout( new WrapLayout() ); + // BVV button. + final JButton btnShowBVV = new JButton( launchBVVAction ); + panelButtons.add( btnShowBVV ); + btnShowBVV.setFont( FONT ); + // TrackScheme button. final JButton btnShowTrackScheme = new JButton( launchTrackSchemeAction ); panelButtons.add( btnShowTrackScheme ); @@ -381,11 +387,15 @@ public ConfigureViewsPanel( 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
" + + "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
" + + "If a ROI is present in the image, only the spots and the
" + + "image in the ROI will be opened for edition in LabKit
" + + "(this can speed up editing large images)." + + "

" + + "Shift + click will launch the editor on all the
" + "time-points in the movie." ); panelButtons.add( btnLabKit ); } diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java b/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java index 519168ad1..019204688 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FeatureDisplaySelector.java @@ -138,9 +138,15 @@ private double[] autoMinMax( final TrackMateObject target ) } /** - * Return a {@link CategoryJComboBox} that lets a user select among all - * available features in TrackMate. + * Returns a {@link CategoryJComboBox} that lets a user select among all + * available features in TrackMate. The features are read from the model and + * settings, and the model is listened to so that the combo-box is updated + * when new features are added to the model. * + * @param model + * the model to read existing features from. + * @param settings + * the settings to read configured features from. * @return a new {@link CategoryJComboBox}. */ public static final CategoryJComboBox< TrackMateObject, String > createComboBoxSelector( final Model model, final Settings settings ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java index b473be9d5..89b6f7410 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FilterGuiPanel.java @@ -238,7 +238,9 @@ public void stateChanged( final ChangeEvent e ) } /** - * Returns the thresholds currently set by this GUI. + * Returns the filters currently set by this GUI. + * + * @return the list of filters. */ public List< FeatureFilter > getFeatureFilters() { @@ -246,10 +248,13 @@ public List< FeatureFilter > getFeatureFilters() } /** - * Add an {@link ChangeListener} to this panel. The {@link ChangeListener} + * Adds a {@link ChangeListener} to this panel. The {@link ChangeListener} * will be notified when a change happens to the thresholds displayed by * this panel, whether due to the slider being move, the auto-threshold * button being pressed, or the combo-box selection being changed. + * + * @param listener + * the listener to add. */ public void addChangeListener( final ChangeListener listener ) { @@ -257,9 +262,12 @@ public void addChangeListener( final ChangeListener listener ) } /** - * Remove a ChangeListener from this panel. + * Removes a ChangeListener from this panel. * - * @return true if the listener was in listener collection of this instance. + * @param listener + * the listener to remove. + * @return true if the listener was in listener collection of + * this instance. */ public boolean removeChangeListener( final ChangeListener listener ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java index 0d61c975c..9b5d23047 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/FilterPanel.java @@ -70,8 +70,8 @@ import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.gui.GuiUtils; -import fiji.plugin.trackmate.util.Threads; import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.util.Threads; import fiji.util.NumberParser; /** @@ -307,10 +307,13 @@ public FeatureFilter getFilter() } /** - * Add an {@link ChangeListener} to this panel. The {@link ChangeListener} + * Adds a {@link ChangeListener} to this panel. The {@link ChangeListener} * will be notified when a change happens to the threshold displayed by this * panel, whether due to the slider being move, the auto-threshold button * being pressed, or the combo-box selection being changed. + * + * @param listener + * the listener to add. */ public void addChangeListener( final ChangeListener listener ) { @@ -318,8 +321,10 @@ public void addChangeListener( final ChangeListener listener ) } /** - * Remove an ChangeListener. + * Removes a ChangeListener. * + * @param listener + * the listener to remove. * @return true if the listener was in listener collection of this instance. */ public boolean removeChangeListener( final ChangeListener listener ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java index 7c8e0da9b..247cc96bb 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/InitFilterPanel.java @@ -62,7 +62,10 @@ public class InitFilterPanel extends JPanel * Default constructor, initialize component. * * @param filter + * the filter to initialize the panel with. * @param valueCollector + * a function that can return the value collection of a specified + * feature. */ public InitFilterPanel( final FeatureFilter filter, final Function< String, double[] > valueCollector ) { @@ -135,7 +138,9 @@ public void refresh() } /** - * Return the feature threshold on quality set by this panel. + * Returns the feature threshold on quality set by this panel. + * + * @return the feature threshold. */ public FeatureFilter getFeatureThreshold() { diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/PanelProbaThreshold.java b/src/main/java/fiji/plugin/trackmate/gui/components/PanelProbaThreshold.java new file mode 100644 index 000000000..bf0212b17 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/components/PanelProbaThreshold.java @@ -0,0 +1,88 @@ +/*- + * #%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.gui.components; + +import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import fiji.plugin.trackmate.gui.displaysettings.SliderPanelDouble; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; + +/** + * A utility widget that lets a user specify a threshold on a probability value, + * from 0 to 1. + */ +public class PanelProbaThreshold extends JPanel +{ + + private static final long serialVersionUID = 1L; + + private double threshold; + + private final SliderPanelDouble sliderPanel; + + private final BoundedDoubleElement thresholdEl; + + public PanelProbaThreshold( final double threshold ) + { + this.threshold = threshold; + setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) ); + + final JLabel chckbxSmooth = new JLabel( "Proba threshold" ); + chckbxSmooth.setFont( SMALL_FONT ); + add( chckbxSmooth ); + add( Box.createHorizontalGlue() ); + + final DoubleSupplier getter = () -> getThreshold(); + final Consumer< Double > setter = v -> setThresholdPrivate( v ); + thresholdEl = StyleElements.boundedDoubleElement( "threshold", 0., 1., getter, setter ); + sliderPanel = StyleElements.linkedSliderPanel( thresholdEl, 3, 0.1 ); + sliderPanel.setFont( SMALL_FONT ); + + add( sliderPanel ); + } + + private void setThresholdPrivate( final double threshold ) + { + this.threshold = threshold; + } + + public void setThreshold( final double threshold ) + { + setThresholdPrivate( threshold ); + thresholdEl.getValue().setCurrentValue( threshold ); + sliderPanel.update(); + } + + public double getThreshold() + { + return threshold; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/PanelSmoothContour.java b/src/main/java/fiji/plugin/trackmate/gui/components/PanelSmoothContour.java new file mode 100644 index 000000000..466ae4c5f --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/components/PanelSmoothContour.java @@ -0,0 +1,105 @@ +/*- + * #%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.gui.components; + +import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import fiji.plugin.trackmate.gui.displaysettings.SliderPanelDouble; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements; +import fiji.plugin.trackmate.gui.displaysettings.StyleElements.BoundedDoubleElement; + +public class PanelSmoothContour extends JPanel +{ + + private static final long serialVersionUID = 1L; + + private double scale; + + private final SliderPanelDouble sliderPanel; + + private final JCheckBox chckbxSmooth; + + private final BoundedDoubleElement scaleEl; + + public PanelSmoothContour( final double scale, final String units ) + { + this.scale = scale; + setLayout( new BoxLayout( this, BoxLayout.X_AXIS ) ); + + chckbxSmooth = new JCheckBox( "Smooth" ); + chckbxSmooth.setFont( SMALL_FONT ); + add( chckbxSmooth ); + add( Box.createHorizontalGlue() ); + + final DoubleSupplier getter = () -> getScale(); + final Consumer< Double > setter = v -> setScalePrivate( v ); + scaleEl = StyleElements.boundedDoubleElement( "scale", 0., 20., getter, setter ); + sliderPanel = StyleElements.linkedSliderPanel( scaleEl, 2 ); + sliderPanel.setFont( SMALL_FONT ); + + add( sliderPanel ); + add( Box.createHorizontalStrut( 5 ) ); + final JLabel lblUnits = new JLabel( units ); + lblUnits.setFont( SMALL_FONT ); + add( lblUnits ); + + chckbxSmooth.addActionListener( e -> sliderPanel.setEnabled( chckbxSmooth.isSelected() ) ); + setOnOff(); + if ( scale > 0. ) + scaleEl.set( scale ); + } + + private void setOnOff() + { + chckbxSmooth.setSelected( scale > 0. ); + sliderPanel.setEnabled( scale > 0. ); + } + + private void setScalePrivate( final double scale ) + { + this.scale = scale; + } + + public void setScale( final double scale ) + { + setScalePrivate( scale ); + setOnOff(); + scaleEl.getValue().setCurrentValue( scale ); + sliderPanel.update(); + } + + public double getScale() + { + if ( chckbxSmooth.isSelected() ) + return scale; + return -1.; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java index 7fff2eff7..93b8a2b21 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/detector/LabelImageDetectorConfigurationPanel.java @@ -21,7 +21,7 @@ */ package fiji.plugin.trackmate.gui.components.detector; -import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD; import java.util.Map; @@ -29,7 +29,6 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.detection.LabelImageDetectorFactory; import fiji.plugin.trackmate.detection.SpotDetectorFactory; -import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; /** * Configuration panel for spot detectors based on label images. @@ -59,15 +58,14 @@ public LabelImageDetectorConfigurationPanel( public Map< String, Object > getSettings() { final Map< String, Object > lSettings = super.getSettings(); - lSettings.remove( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD ); + lSettings.remove( KEY_INTENSITY_THRESHOLD ); return lSettings; } @Override public void setSettings( final Map< String, Object > settings ) { - sliderChannel.setValue( ( Integer ) settings.get( KEY_TARGET_CHANNEL ) ); - chkboxSimplify.setSelected( ( Boolean ) settings.get( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS ) ); + setSettingsNonIntensity( settings ); } /** diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java index 83090bb8b..5bb008a1d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/detector/MaskDetectorConfigurationPanel.java @@ -21,8 +21,6 @@ */ package fiji.plugin.trackmate.gui.components.detector; -import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; - import java.util.Map; import fiji.plugin.trackmate.Model; @@ -66,8 +64,7 @@ public Map< String, Object > getSettings() @Override public void setSettings( final Map< String, Object > settings ) { - sliderChannel.setValue( ( Integer ) settings.get( KEY_TARGET_CHANNEL ) ); - chkboxSimplify.setSelected( ( Boolean ) settings.get( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS ) ); + setSettingsNonIntensity( settings ); } /** diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java index c77183971..23375850f 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/detector/ThresholdDetectorConfigurationPanel.java @@ -22,6 +22,9 @@ package fiji.plugin.trackmate.gui.components.detector; import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SMOOTHING_SCALE; import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; import static fiji.plugin.trackmate.gui.Fonts.FONT; import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; @@ -51,9 +54,10 @@ import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; +import fiji.plugin.trackmate.gui.components.PanelSmoothContour; import fiji.plugin.trackmate.util.DetectionPreview; -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.imglib2.Interval; @@ -87,6 +91,8 @@ public class ThresholdDetectorConfigurationPanel extends ConfigurationPanel protected final JLabel lblIntensityThreshold; + protected final PanelSmoothContour smoothingPanel; + /* * CONSTRUCTOR */ @@ -126,10 +132,10 @@ protected ThresholdDetectorConfigurationPanel( setPreferredSize( new Dimension( 300, 511 ) ); final GridBagLayout gridBagLayout = new GridBagLayout(); - gridBagLayout.rowHeights = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 47 }; + gridBagLayout.rowHeights = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 47 }; gridBagLayout.columnWidths = new int[] { 0, 0, 20 }; gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0 }; - gridBagLayout.rowWeights = new double[] { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, }; + gridBagLayout.rowWeights = new double[] { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; setLayout( gridBagLayout ); final JLabel jLabelDetectorName = new JLabel( detectorName, JLabel.CENTER ); @@ -220,6 +226,16 @@ protected ThresholdDetectorConfigurationPanel( chkboxSimplify.setText( "Simplify contours." ); chkboxSimplify.setFont( FONT ); + smoothingPanel = new PanelSmoothContour( -1., model.getSpaceUnits() ); + final GridBagConstraints gbSmoothPanel = new GridBagConstraints(); + gbSmoothPanel.anchor = GridBagConstraints.NORTHWEST; + gbSmoothPanel.insets = new Insets( 5, 5, 5, 5 ); + gbSmoothPanel.gridwidth = 3; + gbSmoothPanel.gridx = 0; + gbSmoothPanel.gridy = 6; + gbSmoothPanel.fill = GridBagConstraints.HORIZONTAL; + this.add( smoothingPanel, gbSmoothPanel ); + final DetectionPreview detectionPreview = DetectionPreview.create() .model( model ) .settings( settings ) @@ -234,7 +250,7 @@ protected ThresholdDetectorConfigurationPanel( gbcBtnPreview.insets = new Insets( 5, 5, 5, 5 ); gbcBtnPreview.gridwidth = 3; gbcBtnPreview.gridx = 0; - gbcBtnPreview.gridy = 7; + gbcBtnPreview.gridy = 8; this.add( detectionPreview.getPanel(), gbcBtnPreview ); /* @@ -267,11 +283,9 @@ protected ThresholdDetectorConfigurationPanel( private < T extends RealType< T > & NativeType< T > > void autoThreshold() { btnAutoThreshold.setEnabled( false ); - Threads.run( "TrackMate compute threshold thread", () -> - { + Threads.run( "TrackMate compute threshold thread", () -> { try { - @SuppressWarnings( "unchecked" ) final ImgPlus< T > img = TMUtils.rawWraps( settings.imp ); final int channel = ( ( Number ) sliderChannel.getValue() ).intValue() - 1; final int frame = settings.imp.getT() - 1; @@ -294,21 +308,30 @@ public Map< String, Object > getSettings() final int targetChannel = sliderChannel.getValue(); final boolean simplify = chkboxSimplify.isSelected(); final double intensityThreshold = ( ( Number ) ftfIntensityThreshold.getValue() ).doubleValue(); + final double scale = smoothingPanel.getScale(); final HashMap< String, Object > lSettings = new HashMap<>( 3 ); lSettings.put( KEY_TARGET_CHANNEL, targetChannel ); - lSettings.put( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD, intensityThreshold ); - lSettings.put( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, simplify ); + lSettings.put( KEY_INTENSITY_THRESHOLD, intensityThreshold ); + lSettings.put( KEY_SIMPLIFY_CONTOURS, simplify ); + lSettings.put( KEY_SMOOTHING_SCALE, scale ); return lSettings; } - @Override - public void setSettings( final Map< String, Object > settings ) + protected void setSettingsNonIntensity( final Map< String, Object > settings ) { sliderChannel.setValue( ( Integer ) settings.get( KEY_TARGET_CHANNEL ) ); chkboxSimplify.setSelected( ( Boolean ) settings.get( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS ) ); + final Object scaleObj = settings.get( KEY_SMOOTHING_SCALE ); + final double scale = scaleObj == null ? -1. : ( ( Number ) scaleObj ).doubleValue(); + smoothingPanel.setScale( scale ); + } - final Double intensityThreshold = Double.valueOf( ( Double ) settings.get( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD ) ); + @Override + public void setSettings( final Map< String, Object > settings ) + { + setSettingsNonIntensity( settings ); + final Double intensityThreshold = Double.valueOf( ( Double ) settings.get( KEY_INTENSITY_THRESHOLD ) ); if ( intensityThreshold == null || intensityThreshold == 0. ) autoThreshold(); else diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java index fab2155f4..e8697378d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/JPanelFeatureSelectionGui.java @@ -86,6 +86,11 @@ public JPanelFeatureSelectionGui() /** * Set the features and their names that should be presented by this GUI. * The user will be allowed to choose amongst the given features. + * + * @param features + * the features to add in the GUI. + * @param featureNames + * the feature names that will be displayed. */ public void setDisplayFeatures( final Collection< String > features, final Map< String, String > featureNames ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java index 1e4862069..7fd52abd9 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/Colormap.java @@ -160,6 +160,15 @@ public static List< Colormap > getAvailableLUTs() /** * Create a paint scale with given lower and upper bound, and a specified * default color. + * + * @param name + * the name of the colormap. + * @param lowerBound + * the lower bound. + * @param upperBound + * the upper bound. + * @param defaultColor + * a default color. */ public Colormap( final String name, final double lowerBound, final double upperBound, final Color defaultColor ) { @@ -172,6 +181,13 @@ public Colormap( final String name, final double lowerBound, final double upperB /** * Create a paint scale with a given lower and upper bound and a default * black color. + * + * @param name + * the name of the colormap. + * @param lowerBound + * the lower bound. + * @param upperBound + * the upper bound. */ public Colormap( final String name, final double lowerBound, final double upperBound ) { @@ -181,6 +197,9 @@ public Colormap( final String name, final double lowerBound, final double upperB /** * Create a paint scale with a lower bound of 0, an upper bound of 1 and a * default black color. + * + * @param name + * the colormap name. */ public Colormap( final String name ) { @@ -201,6 +220,11 @@ public String getName() * by value. If value is greater than the upper * bound or lower than the lower bound set at construction, this call will * be ignored. + * + * @param value + * the value at which to add the color. + * @param color + * the color. */ public void add( final double value, final Color color ) { diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java index 3af77b6e7..d5ec5c58f 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanel.java @@ -37,6 +37,8 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import fiji.plugin.trackmate.gui.GuiUtils; + /** * A {@link JSlider} with a {@link JSpinner} next to it, both modifying the same * {@link BoundedValue value}. @@ -61,6 +63,8 @@ public class SliderPanel extends JPanel implements BoundedValue.UpdateListener * label to show next to the slider. * @param model * the value that is modified. + * @param spinnerStepSize + * the step size in the spinner to create. */ public SliderPanel( final String name, final BoundedValue model, final int spinnerStepSize ) { @@ -68,13 +72,21 @@ public SliderPanel( final String name, final BoundedValue model, final int spinn setLayout( new BorderLayout( 10, 10 ) ); setPreferredSize( PANEL_SIZE ); - slider = new JSlider( SwingConstants.HORIZONTAL, model.getRangeMin(), model.getRangeMax(), model.getCurrentValue() ); + final int imin = model.getRangeMin(); + final int imax = model.getRangeMax(); + int ivalue = model.getCurrentValue(); + ivalue = Math.max( imin, ivalue ); + ivalue = Math.min( imax, ivalue ); + slider = new JSlider( SwingConstants.HORIZONTAL, imin, imax, ivalue ); + spinner = new JSpinner(); final double min = model.getRangeMin(); final double max = model.getRangeMax(); - final double val = Math.max( Math.min( model.getCurrentValue(), max ), min ); - spinner.setModel( new SpinnerNumberModel( val, min, max, spinnerStepSize ) ); + double value = model.getCurrentValue(); + value = Math.min( max, value ); + value = Math.max( min, value ); + spinner.setModel( new SpinnerNumberModel( value, min, max, spinnerStepSize ) ); slider.addChangeListener( new ChangeListener() { @@ -125,6 +137,22 @@ public void stateChanged( final ChangeEvent e ) add( slider, BorderLayout.CENTER ); add( spinner, BorderLayout.EAST ); + final MouseWheelListener mouseWheelListener = new MouseWheelListener() + { + + @Override + public void mouseWheelMoved( final MouseWheelEvent e ) + { + if ( !slider.isEnabled() ) + return; + final int notches = e.getWheelRotation(); + final int step = notches < 0 ? 1 : -1; + slider.setValue( slider.getValue() + step ); + } + }; + slider.addMouseWheelListener( mouseWheelListener ); + spinner.addMouseWheelListener( mouseWheelListener ); + this.model = model; model.setUpdateListener( this ); } @@ -134,16 +162,6 @@ public void setNumColummns( final int cols ) ( ( JSpinner.NumberEditor ) spinner.getEditor() ).getTextField().setColumns( cols ); } - @Override - public void setFont( final Font font ) - { - super.setFont( font ); - if ( spinner != null ) - spinner.setFont( font ); - if ( slider != null ) - slider.setFont( font ); - } - @Override public void setToolTipText( final String text ) { @@ -154,6 +172,20 @@ public void setToolTipText( final String text ) slider.setToolTipText( text ); } + @Override + public void setEnabled( final boolean enabled ) + { + spinner.setEnabled( enabled ); + slider.setEnabled( enabled ); + super.setEnabled( enabled ); + } + + @Override + public void setFont( final Font font ) + { + GuiUtils.setFont( this, font ); + } + @Override public void update() { diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java index e91d751c5..f707d9d96 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/SliderPanelDouble.java @@ -39,6 +39,8 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import fiji.plugin.trackmate.gui.GuiUtils; + /** * A {@link JSlider} with a {@link JSpinner} next to it, both modifying the same * {@link BoundedValue value}. @@ -69,13 +71,15 @@ public interface RangeListener } /** - * Create a {@link SliderPanelDouble} to modify a given {@link BoundedValueDouble value}. + * Create a {@link SliderPanelDouble} to modify a given + * {@link BoundedValueDouble value}. * * @param name * label to show next to the slider. * @param model * the value that is modified. * @param spinnerStepSize + * the steps size for the spinner created. */ public SliderPanelDouble( final String name, @@ -86,13 +90,21 @@ public SliderPanelDouble( setLayout( new BorderLayout( 10, 10 ) ); setPreferredSize( SliderPanel.PANEL_SIZE ); + final int imin = 0; + final int imax = sliderLength; + int ivalue = toSlider( model.getCurrentValue() ); + ivalue = Math.max( imin, ivalue ); + ivalue = Math.min( imax, ivalue ); + slider = new JSlider( SwingConstants.HORIZONTAL, imin, imax, ivalue ); + + spinner = new JSpinner(); dmin = model.getRangeMin(); dmax = model.getRangeMax(); - final double val = Math.min( Math.max( model.getCurrentValue(), dmin ), dmax ); - slider = new JSlider( SwingConstants.HORIZONTAL, 0, sliderLength, toSlider( val ) ); - spinner = new JSpinner(); - spinner.setModel( new SpinnerNumberModel( val, dmin, dmax, spinnerStepSize ) ); + double value = model.getCurrentValue(); + value = Math.min( dmax, value ); + value = Math.max( dmin, value ); + spinner.setModel( new SpinnerNumberModel( value, dmin, dmax, spinnerStepSize ) ); slider.addChangeListener( new ChangeListener() { @@ -152,6 +164,22 @@ public void stateChanged( final ChangeEvent e ) add( slider, BorderLayout.CENTER ); add( spinner, BorderLayout.EAST ); + final MouseWheelListener mouseWheelListener = new MouseWheelListener() + { + + @Override + public void mouseWheelMoved( final MouseWheelEvent e ) + { + if ( !slider.isEnabled() ) + return; + final int notches = e.getWheelRotation(); + final int step = notches < 0 ? 1 : -1; + slider.setValue( slider.getValue() + step ); + } + }; + slider.addMouseWheelListener( mouseWheelListener ); + spinner.addMouseWheelListener( mouseWheelListener ); + this.model = model; model.setUpdateListener( this ); } @@ -175,16 +203,6 @@ public void setNumColummns( final int cols ) ( ( JSpinner.NumberEditor ) spinner.getEditor() ).getTextField().setColumns( cols ); } - @Override - public void setFont( final Font font ) - { - super.setFont( font ); - if ( spinner != null ) - spinner.setFont( font ); - if ( slider != null ) - slider.setFont( font ); - } - @Override public void setToolTipText( final String text ) { @@ -195,6 +213,20 @@ public void setToolTipText( final String text ) slider.setToolTipText( text ); } + @Override + public void setEnabled( final boolean enabled ) + { + spinner.setEnabled( enabled ); + slider.setEnabled( enabled ); + super.setEnabled( enabled ); + } + + @Override + public void setFont( final Font font ) + { + GuiUtils.setFont( this, font ); + } + @Override public void update() { diff --git a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java index e003344f6..aa02573f0 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java +++ b/src/main/java/fiji/plugin/trackmate/gui/displaysettings/StyleElements.java @@ -1112,6 +1112,7 @@ public static < E > JComboBox< E > linkedComboBoxEnumSelector( final EnumElement if ( e != model.getSelectedItem() ) model.setSelectedItem( e ); } ); + cb.setSelectedItem( element.getValue() ); return cb; } 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 6d2e6fde6..40fb780e0 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -1,3 +1,24 @@ +/*- + * #%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.gui.editor; import java.awt.Color; @@ -60,7 +81,6 @@ 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 ); 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 51697fed7..e1cddc6cb 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -1,3 +1,24 @@ +/*- + * #%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.gui.editor; import java.util.ArrayList; @@ -12,14 +33,14 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.detection.MaskUtils; +import fiji.plugin.trackmate.detection.SpotMeshUtils; +import fiji.plugin.trackmate.detection.SpotRoiUtils; import ij.IJ; import net.imglib2.RandomAccessibleInterval; import net.imglib2.loops.LoopBuilder; import net.imglib2.roi.labeling.ImgLabeling; 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 @@ -266,13 +287,25 @@ private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T indices.add( Integer.valueOf( i + 1 ) ); final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); - final Map< Integer, List< Spot > > spots = MaskUtils.fromLabelingWithROIMap( - labeling, - Views.zeroMin( labeling ), - calibration, - simplify, - rai ); - return spots; + + final boolean is3D = rai.numDimensions() > 2; + final double smoothingScale = -1.; // TODO + if ( is3D ) + return SpotMeshUtils.from3DLabelingWithROIMap( + labeling, + new double[] { 0., 0., 0. }, + calibration, + simplify, + smoothingScale, + rai ); + else + return SpotRoiUtils.from2DLabelingWithROIMap( + labeling, + new double[] { 0., 0. }, + calibration, + simplify, + smoothingScale, + rai ); } private static final String str( final Spot spot ) 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 c1654465b..60f4605cf 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -1,3 +1,24 @@ +/*- + * #%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.gui.editor; import java.awt.Component; @@ -19,14 +40,12 @@ 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; 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; @@ -114,7 +133,7 @@ protected void launch( final boolean singleTimePoint ) // Show LabKit. String title = "Editing TrackMate data for " + imp.getShortTitle(); if ( singleTimePoint ) - title += "at frame " + ( currentTimePoint + 1 ); + title += " at frame " + ( currentTimePoint + 1 ); final TrackMateLabkitFrame labkit = TrackMateLabkitFrame.show( model, title ); // Prepare re-importer. @@ -435,7 +454,7 @@ private final void processFrame( for ( final Spot spot : spotsThisFrame ) { final int index = spot.ID() + 1; - SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spot.iterable( lblImgPlus ).forEach( p -> p.set( index ) ); spotLabels.put( index, spot ); } } @@ -454,7 +473,7 @@ private final void processFrame( continue; final int index = spot.ID() + 1; - SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spot.iterable( lblImgPlus ).forEach( p -> p.set( index ) ); spotLabels.put( index, spot ); } } @@ -462,32 +481,23 @@ private final void processFrame( private void boundingBox( final Spot spot, final long[] min, final long[] max ) { - final SpotRoi roi = spot.getRoi(); - if ( roi == null ) + for ( int d = 0; d < min.length; d++ ) { - 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 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 ) ); + min[ d ] = ( long ) Math.floor( spot.realMin( d ) / calibration[ d ] ); + max[ d ] = ( long ) Math.ceil( spot.realMax( d ) / calibration[ d ] ); } 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 ] ); + final long[] maxImp = new long[] { imp.getWidth(), imp.getHeight(), imp.getNSlices() }; + + for ( int d = 0; d < min.length; d++ ) + { + min[ d ] = Math.max( 0, ( long ) Math.floor( spot.realMin( d ) / calibration[ d ] ) ); + max[ d ] = Math.min( maxImp[ d ], ( long ) Math.ceil( spot.realMax( d ) / calibration[ d ] ) ); + } } public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate, final DisplaySettings ds ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelection.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelection.java new file mode 100644 index 000000000..3afc0cbd3 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelection.java @@ -0,0 +1,269 @@ +/*- + * #%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.gui.featureselector; + +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.EDGES; +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.SPOTS; +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.TRACKS; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.detection.DetectionUtils; +import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; +import fiji.plugin.trackmate.features.spot.Spot2DMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.Spot3DMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.SpotContrastAndSNRAnalyzerFactory; +import fiji.plugin.trackmate.features.track.TrackAnalyzer; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; +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.TrackAnalyzerProvider; + +public class AnalyzerSelection +{ + + static final List< TrackMateObject > objs = Arrays.asList( new TrackMateObject[] { SPOTS, EDGES, TRACKS } ); + + private final Map< TrackMateObject, Map< String, Boolean > > allAnalyzers = new LinkedHashMap<>(); + + private AnalyzerSelection() + { + allAnalyzers.put( SPOTS, new TreeMap<>() ); + allAnalyzers.put( EDGES, new TreeMap<>() ); + allAnalyzers.put( TRACKS, new TreeMap<>() ); + } + + public boolean isSelected( final TrackMateObject obj, final String key ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return false; + return map.getOrDefault( key, false ); + } + + public void setSelected( final TrackMateObject obj, final String key, final boolean selected ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return; + + map.put( key, selected ); + } + + public List< String > getKeys( final TrackMateObject obj ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return Collections.emptyList(); + + return new ArrayList<>( map.keySet() ); + } + + public List< String > getSelectedAnalyzers( final TrackMateObject obj ) + { + final Map< String, Boolean > map = allAnalyzers.get( obj ); + if ( map == null ) + return Collections.emptyList(); + + return map.entrySet() + .stream() + .filter( e -> e.getValue() ) + .map( e -> e.getKey() ) + .collect( Collectors.toList() ); + } + + /** + * Configure the specified settings object so that it includes only all the + * analyzers in this selection. + * + * @param settings + * the settings to configure. + */ + public void configure( final Settings settings ) + { + settings.clearSpotAnalyzerFactories(); + settings.clearEdgeAnalyzers(); + settings.clearTrackAnalyzers(); + + final List< String > selectionSpotAnalyzers = getSelectedAnalyzers( SPOTS ); + + // Base spot analyzers, in priority order. + final SpotAnalyzerProvider spotAnalyzerProvider = new SpotAnalyzerProvider( settings.imp == null + ? 1 : settings.imp.getNChannels() ); + for ( final String key : spotAnalyzerProvider.getVisibleKeys() ) + { + if ( selectionSpotAnalyzers.contains( key ) ) + { + final SpotAnalyzerFactory< ? > factory = spotAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addSpotAnalyzerFactory( factory ); + } + } + + // Shall we add 2D morphology analyzers? + if ( settings.imp != null + && DetectionUtils.is2D( settings.imp ) + && settings.detectorFactory != null + && settings.detectorFactory.has2Dsegmentation() ) + { + final Spot2DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot2DMorphologyAnalyzerProvider( settings.imp.getNChannels() ); + for ( final String key : spotMorphologyAnalyzerProvider.getVisibleKeys() ) + { + if ( selectionSpotAnalyzers.contains( key ) ) + { + final Spot2DMorphologyAnalyzerFactory< ? > factory = spotMorphologyAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addSpotAnalyzerFactory( factory ); + } + } + } + + // Shall we add 3D morphology analyzers? + if ( settings.imp != null + && !DetectionUtils.is2D( settings.imp ) + && settings.detectorFactory != null + && settings.detectorFactory.has3Dsegmentation() ) + { + final Spot3DMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new Spot3DMorphologyAnalyzerProvider( settings.imp.getNChannels() ); + for ( final String key : spotMorphologyAnalyzerProvider.getVisibleKeys() ) + { + if ( selectionSpotAnalyzers.contains( key ) ) + { + final Spot3DMorphologyAnalyzerFactory< ? > factory = spotMorphologyAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addSpotAnalyzerFactory( factory ); + } + } + } + + // Edge analyzers. + final List< String > selectedEdgeAnalyzers = getSelectedAnalyzers( EDGES ); + final EdgeAnalyzerProvider edgeAnalyzerProvider = new EdgeAnalyzerProvider(); + for ( final String key : edgeAnalyzerProvider.getVisibleKeys() ) + { + if ( selectedEdgeAnalyzers.contains( key ) ) + { + final EdgeAnalyzer factory = edgeAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addEdgeAnalyzer( factory ); + } + } + + // Track analyzers. + final List< String > selectedTrackAnalyzers = getSelectedAnalyzers( TRACKS ); + final TrackAnalyzerProvider trackAnalyzerProvider = new TrackAnalyzerProvider(); + for ( final String key : trackAnalyzerProvider.getVisibleKeys() ) + { + if ( selectedTrackAnalyzers.contains( key ) ) + { + final TrackAnalyzer factory = trackAnalyzerProvider.getFactory( key ); + if ( factory != null ) + settings.addTrackAnalyzer( factory ); + } + } + } + + /** + * Possibly adds the analyzers that are discovered at runtime, but not + * present in the analyzer selection, with the 'selected' flag. + */ + public void mergeWithDefault() + { + final AnalyzerSelection df = defaultSelection(); + for ( final TrackMateObject obj : objs ) + { + final Map< String, Boolean > source = df.allAnalyzers.get( obj ); + final Map< String, Boolean > target = allAnalyzers.get( obj ); + for ( final String key : source.keySet() ) + target.putIfAbsent( key, true ); + } + } + + @Override + public String toString() + { + final StringBuilder str = new StringBuilder( super.toString() ); + for ( final TrackMateObject obj : objs ) + { + str.append( "\n" + toName( obj ) + " analyzers:" ); + + final Map< String, Boolean > map = allAnalyzers.get( obj ); + for ( final String key : map.keySet() ) + str.append( String.format( "\n\t%25s \t-> %s", key, ( map.get( key ).booleanValue() ? "selected" : "deselected" ) ) ); + } + return str.toString(); + } + + public static AnalyzerSelection defaultSelection() + { + final AnalyzerSelection fs = new AnalyzerSelection(); + + for ( final String key : new SpotAnalyzerProvider( 1 ).getVisibleKeys() ) + fs.setSelected( SPOTS, key, true ); + + for ( final String key : new Spot2DMorphologyAnalyzerProvider( 1 ).getVisibleKeys() ) + fs.setSelected( SPOTS, key, true ); + + for ( final String key : new Spot3DMorphologyAnalyzerProvider( 1 ).getVisibleKeys() ) + fs.setSelected( SPOTS, key, true ); + + for ( final String key : new EdgeAnalyzerProvider().getVisibleKeys() ) + fs.setSelected( EDGES, key, true ); + + for ( final String key : new TrackAnalyzerProvider().getVisibleKeys() ) + fs.setSelected( TRACKS, key, true ); + + // Fine tune. + fs.setSelected( SPOTS, SpotContrastAndSNRAnalyzerFactory.KEY, false ); + + return fs; + } + + public void set( final AnalyzerSelection o ) + { + allAnalyzers.clear(); + for ( final TrackMateObject obj : objs ) + allAnalyzers.put( obj, new TreeMap<>( o.allAnalyzers.get( obj ) ) ); + + mergeWithDefault(); + } + + public static final String toName( final TrackMateObject obj ) + { + final String str = obj.toString(); + return StringUtils.capitalize( str ).substring( 0, str.length() - 1 ); + } + +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectionIO.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectionIO.java new file mode 100644 index 000000000..d899e729d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectionIO.java @@ -0,0 +1,112 @@ +/*- + * #%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.gui.featureselector; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.stream.Collectors; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class AnalyzerSelectionIO +{ + + private static File userSelectionFile = new File( new File( System.getProperty( "user.home" ), ".trackmate" ), "featureselection.json" ); + + public static AnalyzerSelection readUserDefault() + { + if ( !userSelectionFile.exists() ) + { + final AnalyzerSelection fs = AnalyzerSelection.defaultSelection(); + saveToUserDefault( fs ); + return fs; + } + + try (FileReader reader = new FileReader( userSelectionFile )) + { + final String str = Files.lines( Paths.get( userSelectionFile.getAbsolutePath() ) ) + .collect( Collectors.joining( System.lineSeparator() ) ); + + return fromJson( str ); + } + catch ( final FileNotFoundException e ) + { + System.err.println( "Could not find the user feature selection file: " + userSelectionFile + + ". Using built-in default setting." ); + e.printStackTrace(); + } + catch ( final IOException e ) + { + System.err.println( "Could not read the user feature selection file: " + userSelectionFile + + ". Using built-in default setting." ); + e.printStackTrace(); + } + return AnalyzerSelection.defaultSelection(); + } + + public static AnalyzerSelection fromJson( final String str ) + { + final AnalyzerSelection fs = ( str == null || str.isEmpty() ) ? readUserDefault() : getGson().fromJson( str, AnalyzerSelection.class ); + fs.mergeWithDefault(); + return fs; + } + + public static void saveToUserDefault( final AnalyzerSelection fs ) + { + final String str = toJson( fs ); + + if ( !userSelectionFile.exists() ) + userSelectionFile.getParentFile().mkdirs(); + + try (FileWriter writer = new FileWriter( userSelectionFile )) + { + writer.append( str ); + } + catch ( final IOException e ) + { + System.err.println( "Could not write the user default settings to " + userSelectionFile ); + e.printStackTrace(); + } + } + + public static String toJson( final AnalyzerSelection fs ) + { + return getGson().toJson( fs ); + } + + private static Gson getGson() + { + final GsonBuilder builder = new GsonBuilder(); + return builder.setPrettyPrinting().create(); + } + + public static void main( final String[] args ) + { + System.out.println( readUserDefault() ); // DEBUG + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelector.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelector.java new file mode 100644 index 000000000..ac70fe09a --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelector.java @@ -0,0 +1,72 @@ +/*- + * #%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.gui.featureselector; + +import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; + +import javax.swing.JDialog; +import javax.swing.JFrame; + +import org.scijava.command.Command; +import org.scijava.plugin.Plugin; + +@Plugin( type = Command.class, + label = "Configure TrackMate feature analyzers...", + iconPath = "/icons/commands/information.png", + menuPath = "Edit > Options > Configure TrackMate feature analyzers...", + description = "Shows a dialog that allows configuring what feature analyzers will be used " + + "in the next TrackMate session." ) + +public class AnalyzerSelector implements Command +{ + + private final JDialog dialog; + + private final AnalyzerSelectorPanel gui; + + public AnalyzerSelector() + { + dialog = new JDialog( ( JFrame ) null, "TrackMate feature analyzers selection" ); + dialog.setLocationByPlatform( true ); + dialog.setLocationRelativeTo( null ); + gui = new AnalyzerSelectorPanel( AnalyzerSelectionIO.readUserDefault() ); + dialog.getContentPane().add( gui ); + dialog.setIconImage( TRACKMATE_ICON.getImage() ); + dialog.pack(); + } + + public JDialog getDialog() + { + return dialog; + } + + @Override + public void run() + { + dialog.setVisible( true ); + } + + public static void main( final String[] args ) + { + new AnalyzerSelector().run(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectorPanel.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectorPanel.java new file mode 100644 index 000000000..ec7bca318 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/AnalyzerSelectorPanel.java @@ -0,0 +1,303 @@ +/*- + * #%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.gui.featureselector; + +import static fiji.plugin.trackmate.gui.Icons.APPLY_ICON; +import static fiji.plugin.trackmate.gui.Icons.RESET_ICON; +import static fiji.plugin.trackmate.gui.Icons.REVERT_ICON; +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.EDGES; +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.SPOTS; +import static fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject.TRACKS; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.ScrollPaneConstants; + +import fiji.plugin.trackmate.features.FeatureAnalyzer; +import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; +import fiji.plugin.trackmate.providers.AbstractProvider; +import fiji.plugin.trackmate.providers.EdgeAnalyzerProvider; +import fiji.plugin.trackmate.providers.TrackAnalyzerProvider; + +public class AnalyzerSelectorPanel extends JPanel +{ + private static final long serialVersionUID = 1L; + + private static final String APPLY_TOOLTIP = "Save the current analyzer selection to the user default settings. " + + "The selection be used in all the following TrackMate sessions."; + + private static final String REVERT_TOOLTIP = "Revert the current analyzer selection to the ones saved in the " + + "user default settings file."; + + private static final String RESET_TOOLTIP = "Reset the current analyzer selection to the built-in defaults."; + + final JPanel panelConfig; + + public AnalyzerSelectorPanel( final AnalyzerSelection selection ) + { + setLayout( new BorderLayout( 0, 0 ) ); + + final JPanel panelTable = new JPanel(); + add( panelTable, BorderLayout.SOUTH ); + + final GridBagLayout gblPanelTable = new GridBagLayout(); + gblPanelTable.columnWeights = new double[] { 0.0, 1.0 }; + gblPanelTable.rowWeights = new double[] { 1.0 }; + panelTable.setLayout( gblPanelTable ); + + final JPanel panelButton = new JPanel(); + final GridBagConstraints gbcPanelButton = new GridBagConstraints(); + gbcPanelButton.gridwidth = 2; + gbcPanelButton.insets = new Insets( 10, 10, 10, 10 ); + gbcPanelButton.fill = GridBagConstraints.BOTH; + gbcPanelButton.gridx = 0; + gbcPanelButton.gridy = 0; + panelTable.add( panelButton, gbcPanelButton ); + panelButton.setLayout( new BoxLayout( panelButton, BoxLayout.X_AXIS ) ); + + final BoxLayout panelButtonLayout = new BoxLayout( panelButton, BoxLayout.LINE_AXIS ); + panelButton.setLayout( panelButtonLayout ); + final JButton btnReset = new JButton( "Reset", RESET_ICON ); + btnReset.setToolTipText( RESET_TOOLTIP ); + final JButton btnRevert = new JButton( "Revert", REVERT_ICON ); + btnRevert.setToolTipText( REVERT_TOOLTIP ); + final JButton btnApply = new JButton( "Save to user defaults", APPLY_ICON ); + btnApply.setToolTipText( APPLY_TOOLTIP ); + panelButton.add( btnReset ); + panelButton.add( Box.createHorizontalStrut( 5 ) ); + panelButton.add( btnRevert ); + panelButton.add( Box.createHorizontalGlue() ); + panelButton.add( btnApply ); + panelButton.setBorder( BorderFactory.createEmptyBorder( 10, 5, 10, 5 ) ); + + final JPanel panelTitle = new JPanel( new FlowLayout( FlowLayout.LEADING ) ); + add( panelTitle, BorderLayout.NORTH ); + + final JLabel title = new JLabel( "Configure TrackMate feature analyzers:" ); + title.setFont( getFont().deriveFont( Font.BOLD ) ); + panelTitle.add( title ); + + final JSplitPane splitPane = new JSplitPane(); + splitPane.setBorder( null ); + splitPane.setResizeWeight( 0.5 ); + add( splitPane, BorderLayout.CENTER ); + + final JPanel panelLeft = new JPanel(); + splitPane.setLeftComponent( panelLeft ); + panelLeft.setLayout( new BorderLayout( 0, 0 ) ); + + final JScrollPane scrollPaneFeatures = new JScrollPane(); + panelLeft.add( scrollPaneFeatures ); + scrollPaneFeatures.setViewportBorder( null ); + scrollPaneFeatures.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); + scrollPaneFeatures.getVerticalScrollBar().setUnitIncrement( 20 ); + + final JPanel panelFeatures = new JPanel(); + panelFeatures.setBorder( BorderFactory.createEmptyBorder( 10, 10, 10, 10 ) ); + scrollPaneFeatures.setViewportView( panelFeatures ); + final BoxLayout boxLayout = new BoxLayout( panelFeatures, BoxLayout.PAGE_AXIS ); + panelFeatures.setLayout( boxLayout ); + + final JPanel panelRight = new JPanel(); + panelRight.setPreferredSize( new Dimension( 300, 300 ) ); + splitPane.setRightComponent( panelRight ); + panelRight.setLayout( new BorderLayout( 0, 0 ) ); + + this.panelConfig = new JPanel(); + panelConfig.setLayout( new BorderLayout() ); + final JScrollPane scrollPane = new JScrollPane( panelConfig ); + scrollPane.getVerticalScrollBar().setUnitIncrement( 16 ); + panelRight.add( scrollPane, BorderLayout.CENTER ); + + // Feed the feature panel. + final FeatureTable.Tables aggregator = new FeatureTable.Tables(); + + // Providers to test presence of an analyzer and get info. + final Map< TrackMateObject, AbstractProvider< ? > > allProviders = new LinkedHashMap<>( 3 ); + allProviders.put( SPOTS, new MySpotAnalyzerProvider() ); + allProviders.put( EDGES, new EdgeAnalyzerProvider() ); + allProviders.put( TRACKS, new TrackAnalyzerProvider() ); + + for ( final TrackMateObject target : AnalyzerSelection.objs ) + { + @SuppressWarnings( "unchecked" ) + final AbstractProvider< FeatureAnalyzer > provider = ( AbstractProvider< FeatureAnalyzer > ) allProviders.get( target ); + + final JPanel headerPanel = new JPanel(); + final BoxLayout hpLayout = new BoxLayout( headerPanel, BoxLayout.LINE_AXIS ); + headerPanel.setLayout( hpLayout ); + + final JLabel lbl = new JLabel( AnalyzerSelection.toName( target ) + " analyzers:" ); + lbl.setFont( panelFeatures.getFont().deriveFont( Font.BOLD ) ); + lbl.setAlignmentX( Component.LEFT_ALIGNMENT ); + + headerPanel.add( lbl ); + + panelFeatures.add( headerPanel ); + headerPanel.setAlignmentX( Component.LEFT_ALIGNMENT ); + panelFeatures.add( Box.createVerticalStrut( 5 ) ); + + final List< String > analyzerKeys = selection.getKeys( target ); + final Function< String, String > getName = k -> provider.getFactory( k ).getName(); + final Predicate< String > isSelected = k -> selection.isSelected( target, k ); + final BiConsumer< String, Boolean > setSelected = ( k, b ) -> selection.setSelected( target, k, b ); + final Predicate< String > isAnalyzerPresent = k -> provider.getKeys().contains( k ); + + final FeatureTable< List< String >, String > featureTable = + new FeatureTable<>( + analyzerKeys, + List::size, + List::get, + getName, + isSelected, + setSelected, + isAnalyzerPresent ); + + featureTable.getComponent().setAlignmentX( Component.LEFT_ALIGNMENT ); + featureTable.getComponent().setBackground( panelFeatures.getBackground() ); + panelFeatures.add( featureTable.getComponent() ); + panelFeatures.add( Box.createVerticalStrut( 10 ) ); + + aggregator.add( featureTable ); + + final FeatureTable.SelectionListener< String > sl = key -> displayConfigPanel( provider.getFactory( key ) ); + featureTable.selectionListeners().add( sl ); + } + scrollPaneFeatures.setPreferredSize( new Dimension( 300, 300 ) ); + + /* + * Listeners. + */ + + btnReset.addActionListener( e -> { + selection.set( AnalyzerSelection.defaultSelection() ); + title.setText( "Reset the current settings to the built-in defaults." ); + repaint(); + } ); + btnRevert.addActionListener( e -> { + selection.set( AnalyzerSelectionIO.readUserDefault() ); + title.setText( "Reverted the current settings to the user defaults." ); + repaint(); + } ); + btnApply.addActionListener( e -> { + AnalyzerSelectionIO.saveToUserDefault( selection ); + title.setText( "Saved the current settings to the user defaults file." ); + } ); + } + + private void displayConfigPanel( final FeatureAnalyzer factory ) + { + panelConfig.removeAll(); + if ( null == factory ) + return; + + final JPanel infoPanel = new JPanel(); + infoPanel.setLayout( new GridBagLayout() ); + final GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets( 5, 5, 5, 5 ); + c.anchor = GridBagConstraints.LINE_START; + c.fill = GridBagConstraints.BOTH; + c.weightx = 1.; + c.weighty = 1.; + + final JLabel title = new JLabel( factory.getName(), factory.getIcon(), JLabel.CENTER ); + title.setFont( getFont().deriveFont( Font.BOLD ) ); + c.gridy = 0; + infoPanel.add( title, c ); + + final JLabel infoLbl = new JLabel(); + infoLbl.setFont( getFont().deriveFont( Font.ITALIC ) ); + final String infoText = factory.getInfoText(); + infoLbl.setText( "" + ( ( infoText != null ) ? infoText : "No documentation." + "" ) ); + c.gridy++; + infoPanel.add( infoLbl, c ); + + final StringBuilder infoStr = new StringBuilder( "" ); + + infoStr.append( "Features included:

    " ); + for ( final String featureKey : factory.getFeatures() ) + { + infoStr.append( "
  • " + factory.getFeatureNames().get( featureKey ) ); + infoStr.append( "
    - Short name: " + factory.getFeatureShortNames().get( featureKey ) ); + infoStr.append( "
    - Is integer valued: " + factory.getIsIntFeature().get( featureKey ) ); + infoStr.append( "
    - Dimension: " + factory.getFeatureDimensions().get( featureKey ) ); + infoStr.append( "
    - Key: " + featureKey ); + infoStr.append( "
  • " ); + infoStr.append( "
    " ); + } + infoStr.append( "

" ); + + infoStr.append( "Details:

    " ); + infoStr.append( String.format( "
  • %25s: %s
  • ", "Key", + factory.getKey() ) ); + infoStr.append( String.format( "
  • %25s: %s
  • ", "Can use multithreading", + !factory.forbidMultithreading() ) ); + infoStr.append( String.format( "
  • %25s: %s
  • ", "Is manual", + factory.isManualFeature() ) ); + infoStr.append( "
" ); + c.gridy++; + final JLabel infoLabel = new JLabel( infoStr.toString() ); + infoLabel.setFont( getFont().deriveFont( Font.PLAIN ) ); + infoPanel.add( infoLabel, c ); + + panelConfig.add( infoPanel, BorderLayout.NORTH ); + panelConfig.revalidate(); + panelConfig.repaint(); + } + + /** + * A private provider, that return all spot providers, regardless of whether + * they act on 2D shape, 3D shape or dont use shape information. + */ + @SuppressWarnings( "rawtypes" ) + private static class MySpotAnalyzerProvider extends AbstractProvider< SpotAnalyzerFactoryBase > + { + + public MySpotAnalyzerProvider() + { + super( SpotAnalyzerFactoryBase.class ); + } + + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/featureselector/FeatureTable.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/FeatureTable.java new file mode 100644 index 000000000..b7e6698a6 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/FeatureTable.java @@ -0,0 +1,402 @@ +/*- + * #%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.gui.featureselector; + +import static javax.swing.JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; + +import javax.swing.Action; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableCellRenderer; + +import org.scijava.listeners.Listeners; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; + +import fiji.plugin.trackmate.gui.Icons; + +/** + * + * @param collection-of-elements type + * @param element type + */ +public class FeatureTable< C, T > +{ + public static class Tables implements ListSelectionListener + { + private final List< FeatureTable< ?, ? > > tables = new ArrayList<>(); + + public void add( final FeatureTable< ?, ? > table ) + { + tables.add( table ); + table.tables = this; + table.table.getSelectionModel().addListSelectionListener( this ); + } + + @Override + public void valueChanged( final ListSelectionEvent event ) + { + final ListSelectionModel source = ( ListSelectionModel ) event.getSource(); + for ( final FeatureTable< ?, ? > table : tables ) + { + final ListSelectionModel lsm = table.table.getSelectionModel(); + if ( lsm.equals( source ) ) + continue; + + lsm.removeListSelectionListener( this ); + lsm.clearSelection(); + lsm.addListSelectionListener( this ); + } + } + + boolean selectNextTable( final FeatureTable< ?, ? > table ) + { + final int i = tables.indexOf( table ); + if ( i < 0 || i >= tables.size() - 1 ) + return false; + + final JTable next = tables.get( i + 1 ).table; + if ( next.getRowCount() > 0 ) + { + table.clearSelectionQuiet(); + next.setRowSelectionInterval( 0, 0 ); + next.requestFocusInWindow(); + } + return true; + } + + boolean selectPreviousTable( final FeatureTable< ?, ? > table ) + { + final int i = tables.indexOf( table ); + if ( i <= 0 ) + return false; + + final JTable previous = tables.get( i - 1 ).table; + final int rows = previous.getRowCount(); + if ( rows > 0 ) + { + table.clearSelectionQuiet(); + previous.setRowSelectionInterval( rows - 1, rows - 1 ); + previous.requestFocusInWindow(); + } + return true; + } + } + + private static final ImageIcon UP_TO_DATE_ICON = Icons.BULLET_GREEN_ICON; + private static final ImageIcon NOT_UP_TO_DATE_ICON = Icons.QUESTION_ICON; + + private C elements; + private final ToIntFunction< C > size; + private final BiFunction< C, Integer, T > get; + private final Function< T, String > getName; + private final Predicate< T > isSelected; + private final BiConsumer< T, Boolean > setSelected; + private final Predicate< T > isUptodate; + + private final Listeners.List< SelectionListener< T > > selectionListeners; + + private final ListSelectionListener listSelectionListener; + + private final MyTableModel tableModel; + + private final JTable table; + + private Tables tables; + + /** + * Creates a new feature table. + * + * @param elements + * collection of elements. + * @param size + * given collection returns number of elements. + * @param get + * given collection and index returns element at index. + * @param getName + * given element returns name. + * @param isSelected + * given element returns whether it is selected. + * @param setSelected + * given element and boolean sets selected state of element. + * @param isUptodate + * given element returns whether it is up-to-date. + */ + public FeatureTable( + final C elements, + final ToIntFunction< C > size, + final BiFunction< C, Integer, T > get, + final Function< T, String > getName, + final Predicate< T > isSelected, + final BiConsumer< T, Boolean > setSelected, + final Predicate< T > isUptodate ) + { + this.elements = elements; + this.size = size; + this.get = get; + this.getName = getName; + this.isSelected = isSelected; + this.setSelected = setSelected; + this.isUptodate = isUptodate; + + selectionListeners = new Listeners.SynchronizedList<>(); + + tableModel = new MyTableModel(); + table = new JTable( tableModel ); + table.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); + table.setTableHeader( null ); + table.setFillsViewportHeight( true ); + table.setAutoResizeMode( JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS ); + table.setRowHeight( 30 ); + listSelectionListener = e -> { + if ( e.getValueIsAdjusting() ) + return; + final int row = table.getSelectedRow(); + final T selected = ( this.elements != null && row >= 0 && row < this.size.applyAsInt( this.elements ) ) + ? this.get.apply( this.elements, row ) + : null; + selectionListeners.list.forEach( l -> l.selectionChanged( selected ) ); + }; + table.getSelectionModel().addListSelectionListener( listSelectionListener ); + table.setIntercellSpacing( new Dimension( 0, 0 ) ); + table.setSurrendersFocusOnKeystroke( true ); + table.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null ); + table.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null ); + table.getColumnModel().getColumn( 0 ).setMaxWidth( 30 ); + table.getColumnModel().getColumn( 2 ).setMaxWidth( 64 ); + table.getColumnModel().getColumn( 2 ).setCellRenderer( new UpdatedCellRenderer() ); + table.setShowGrid( false ); + + final Actions actions = new Actions( table.getInputMap( WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ), table.getActionMap(), new InputTriggerConfig() ); + actions.runnableAction( this::toggleSelectedRow, "toggle selected row", "SPACE", "ENTER" ); + actions.runnableAction( this::nextRowOrTable, "select next row or table", "DOWN" ); + actions.runnableAction( this::previousRowOrTable, "select previous row or table", "UP" ); + + setElements( elements ); + } + + private void toggleSelectedRow() + { + final int row = table.getSelectedRow(); + if ( row >= 0 ) + { + final T feature = get.apply( elements, row ); + setSelected.accept( feature, !isSelected.test( feature ) ); + tableModel.fireTableCellUpdated( row, 0 ); + } + } + + private void nextRowOrTable() + { + final int row = table.getSelectedRow(); + if ( elements == null || tables == null || row != table.getRowCount() - 1 || !tables.selectNextTable( this ) ) + { + final Action action = table.getActionMap().get( "selectNextRow" ); + if ( action != null ) + action.actionPerformed( new ActionEvent( table, 0, null ) ); + } + } + + private void previousRowOrTable() + { + final int row = table.getSelectedRow(); + if ( elements == null || tables == null || row != 0 || !tables.selectPreviousTable( this ) ) + { + final Action action = table.getActionMap().get( "selectPreviousRow" ); + if ( action != null ) + action.actionPerformed( new ActionEvent( table, 0, null ) ); + } + } + + private void clearSelectionQuiet() + { + table.getSelectionModel().removeListSelectionListener( listSelectionListener ); + table.clearSelection(); + table.getSelectionModel().addListSelectionListener( listSelectionListener ); + + } + + /** + * Exposes the component in which the elements are displayed. + * + * @return the component. + */ + public JComponent getComponent() + { + return table; + } + + /** + * Sets the collection of elements to show. + * + * @param elements + * the collection of elements to show. + */ + public void setElements( final C elements ) + { + this.elements = elements; + if ( elements == null ) + selectionListeners.list.forEach( l -> l.selectionChanged( null ) ); + else + tableModel.fireTableDataChanged(); + } + + public void selectFirstRow() + { + if ( table.getRowCount() > 0 ) + table.setRowSelectionInterval( 0, 0 ); + } + + public interface SelectionListener< T > + { + void selectionChanged( T selected ); + } + + public Listeners< SelectionListener< T > > selectionListeners() + { + return selectionListeners; + } + + private class MyTableModel extends DefaultTableModel + { + + private static final long serialVersionUID = 1L; + + @Override + public int getColumnCount() + { + return 3; + } + + @Override + public int getRowCount() + { + return ( null == elements ) ? 0 : size.applyAsInt( elements ); + } + + public T get( final int index ) + { + return get.apply( elements, Integer.valueOf( index ) ); + } + + @Override + public Object getValueAt( final int rowIndex, final int columnIndex ) + { + switch ( columnIndex ) + { + case 0: + return isSelected.test( get( rowIndex ) ); + case 1: + return getName.apply( get( rowIndex ) ); + case 2: + return isUptodate.test( get( rowIndex ) ); + } + throw new IllegalArgumentException( "Cannot return value for colum index larger than " + getColumnCount() ); + } + + @Override + public Class< ? > getColumnClass( final int columnIndex ) + { + switch ( columnIndex ) + { + case 0: + return Boolean.class; + case 1: + return String.class; + case 2: + return Boolean.class; + } + throw new IllegalArgumentException( "Cannot return value for colum index larger than " + getColumnCount() ); + } + + @Override + public boolean isCellEditable( final int rowIndex, final int columnIndex ) + { + return columnIndex == 0; + } + + @Override + public void setValueAt( final Object aValue, final int rowIndex, final int columnIndex ) + { + final boolean selected = (columnIndex == 0) + ? ( boolean ) aValue + : !isSelected.test( get( rowIndex ) ); + if ( selected != isSelected.test( get( rowIndex ) ) ) + { + setSelected.accept( get( rowIndex ), ( Boolean ) aValue ); + fireTableRowsUpdated( rowIndex, rowIndex ); + for ( final SelectionListener< T > listener : selectionListeners.list ) + listener.selectionChanged( get( rowIndex ) ); + } + } + } + + private class UpdatedCellRenderer implements TableCellRenderer + { + + private final DefaultTableCellRenderer renderer; + + public UpdatedCellRenderer() + { + this.renderer = new DefaultTableCellRenderer(); + final JLabel label = ( JLabel ) renderer.getTableCellRendererComponent( null, null, false, false, 0, 0 ); + label.setHorizontalAlignment( SwingConstants.CENTER ); + } + + @Override + public Component getTableCellRendererComponent( + final JTable table, + final Object value, + final boolean isSelected, + final boolean hasFocus, + final int row, + final int column ) + { + final JLabel label = ( JLabel ) renderer.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); + label.setIcon( isUptodate.test( get.apply( elements, Integer.valueOf( row ) ) ) + ? UP_TO_DATE_ICON + : NOT_UP_TO_DATE_ICON ); + label.setText( "" ); + return label; + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/graph/OutputFunction.java b/src/main/java/fiji/plugin/trackmate/gui/featureselector/package-info.java similarity index 75% rename from src/main/java/fiji/plugin/trackmate/graph/OutputFunction.java rename to src/main/java/fiji/plugin/trackmate/gui/featureselector/package-info.java index e6f89c6c6..f18999b5a 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/OutputFunction.java +++ b/src/main/java/fiji/plugin/trackmate/gui/featureselector/package-info.java @@ -19,17 +19,4 @@ * . * #L% */ -package fiji.plugin.trackmate.graph; - -/** - * Interface for functions that return a new object, computed from two input - * arguments. - * - * @author Jean-Yves Tinevez - */ -public interface OutputFunction< E > -{ - - public E compute( E input1, E input2 ); - -} +package fiji.plugin.trackmate.gui.featureselector; 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 9ab663389..320f237c9 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java @@ -21,10 +21,12 @@ */ package fiji.plugin.trackmate.gui.wizard; +import static fiji.plugin.trackmate.gui.Icons.BVV_ICON; import static fiji.plugin.trackmate.gui.Icons.SPOT_TABLE_ICON; import static fiji.plugin.trackmate.gui.Icons.TRACK_SCHEME_ICON_16x16; import static fiji.plugin.trackmate.gui.Icons.TRACK_TABLES_ICON; +import java.awt.Component; import java.awt.event.ActionEvent; import java.util.Arrays; import java.util.HashMap; @@ -32,7 +34,11 @@ import java.util.Map; import javax.swing.AbstractAction; +import javax.swing.JLabel; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; +import bvv.vistools.BvvHandle; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; @@ -42,10 +48,12 @@ import fiji.plugin.trackmate.action.AbstractTMAction; import fiji.plugin.trackmate.action.ExportAllSpotsStatsAction; import fiji.plugin.trackmate.action.ExportStatsTablesAction; +import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.detection.ManualDetectorFactory; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.ModelFeatureUpdater; +import fiji.plugin.trackmate.gui.GuiUtils; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.LogPanel; @@ -72,9 +80,12 @@ import fiji.plugin.trackmate.tracking.SpotImageTrackerFactory; import fiji.plugin.trackmate.tracking.SpotTrackerFactory; import fiji.plugin.trackmate.tracking.manual.ManualTrackerFactory; +import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; import fiji.plugin.trackmate.util.Threads; +import fiji.plugin.trackmate.visualization.bvv.TrackMateBVV; import fiji.plugin.trackmate.visualization.trackscheme.SpotImageUpdater; import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; +import ij.ImagePlus; public class TrackMateWizardSequence implements WizardSequence { @@ -144,12 +155,13 @@ public TrackMateWizardSequence( final TrackMate trackmate, final SelectionModel executeDetectionDescriptor = new ExecuteDetectionDescriptor( trackmate, logPanel ); initFilterDescriptor = new InitFilterDescriptor( trackmate, initialFilter ); spotFilterDescriptor = new SpotFilterDescriptor( trackmate, spotFilters, featureSelector ); - chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), trackmate ); - executeTrackingDescriptor = new ExecuteTrackingDescriptor( trackmate, logPanel ); + chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), trackmate, displaySettings ); + executeTrackingDescriptor = new ExecuteTrackingDescriptor( trackmate, logPanel, displaySettings ); trackFilterDescriptor = new TrackFilterDescriptor( trackmate, trackFilters, featureSelector ); configureViewsDescriptor = new ConfigureViewsDescriptor( displaySettings, featureSelector, + new LaunchBVVAction(), new LaunchTrackSchemeAction(), new ShowTrackTablesAction(), new ShowSpotTableAction(), @@ -384,7 +396,7 @@ private SpotTrackerDescriptor getTrackerConfigDescriptor() * Special case: are we dealing with the manual tracker? If yes, no * config, no detection. */ - if ( trackerFactory.getKey().equals( ManualTrackerFactory.TRACKER_KEY ) ) + if ( trackerFactory == null || trackerFactory.getKey().equals( ManualTrackerFactory.TRACKER_KEY ) ) { // Position sequence next and previous. next.put( chooseTrackerDescriptor, trackFilterDescriptor ); @@ -439,7 +451,7 @@ private SpotTrackerDescriptor getTrackerConfigDescriptor() previous.put( configDescriptor, chooseTrackerDescriptor ); previous.put( executeTrackingDescriptor, configDescriptor ); previous.put( trackFilterDescriptor, configDescriptor ); - + return configDescriptor; } @@ -452,6 +464,58 @@ private SpotTrackerDescriptor getTrackerConfigDescriptor() private static final String TRACKSCHEME_BUTTON_TOOLTIP = "Launch a new instance of TrackScheme."; + private static final String BVV_BUTTON_TOOLTIP = "Launch a new 3D viewer."; + + private class LaunchBVVAction extends AbstractAction + { + private static final long serialVersionUID = 1L; + + private LaunchBVVAction() + { + super( "3D view", BVV_ICON ); + putValue( SHORT_DESCRIPTION, BVV_BUTTON_TOOLTIP ); + final ImagePlus imp = trackmate.getSettings().imp; + final boolean enabled = ( imp != null ) && !DetectionUtils.is2D( imp ); + setEnabled( enabled ); + } + + @Override + public void actionPerformed( final ActionEvent e ) + { + new Thread( "Launching BVV thread" ) + { + @Override + public void run() + { + final Component c = ( Component ) e.getSource(); + final JRootPane parent = SwingUtilities.getRootPane( c ); + final EverythingDisablerAndReenabler enabler = new EverythingDisablerAndReenabler( parent, new Class[] { JLabel.class } ); + enabler.disable(); + try + { + final Model model = trackmate.getModel(); + final ImagePlus imp = trackmate.getSettings().imp; + if ( imp != null ) + { + final TrackMateBVV< ? > tbvv = new TrackMateBVV<>( model, selectionModel, imp, displaySettings ); + tbvv.render(); + final BvvHandle bvvHandle = tbvv.getBvvHandle(); + GuiUtils.positionWindow( SwingUtilities.getWindowAncestor( bvvHandle.getViewerPanel() ), c ); + } + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + finally + { + enabler.reenable(); + } + } + }.start(); + } + } + private class LaunchTrackSchemeAction extends AbstractAction { private static final long serialVersionUID = 1L; diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java index 7fd221ed1..2393ba443 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardController.java @@ -204,7 +204,13 @@ private void exec( final Runnable runnable ) public void init() { - final WizardPanelDescriptor descriptor = sequence.current(); + WizardPanelDescriptor descriptor = sequence.current(); + if ( descriptor == null ) + { + sequence.setCurrent( sequence.configDescriptor().panelIdentifier ); + descriptor = sequence.configDescriptor(); + } + wizardPanel.btnPrevious.setEnabled( sequence.hasPrevious() ); wizardPanel.btnNext.setEnabled( sequence.hasNext() ); descriptor.aboutToDisplayPanel(); diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java index ba8b9fb23..51660bf66 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/WizardSequence.java @@ -37,7 +37,9 @@ public interface WizardSequence /** * Launches the wizard to play this sequence. - * + * + * @param title + * the title of the frame in which the wizard is displayed. * @return the {@link JFrame} in which the wizard is displayed. */ public default JFrame run( final String title ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java index 7e01dbe10..345a34eb4 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ChooseTrackerDescriptor.java @@ -24,7 +24,10 @@ import java.util.Map; import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.gui.components.ModuleChooserPanel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.io.SettingsPersistence; import fiji.plugin.trackmate.providers.TrackerProvider; @@ -40,11 +43,17 @@ public class ChooseTrackerDescriptor extends WizardPanelDescriptor private final TrackerProvider trackerProvider; - public ChooseTrackerDescriptor( final TrackerProvider trackerProvider, final TrackMate trackmate ) + private final DisplaySettings displaySettings; + + public ChooseTrackerDescriptor( + final TrackerProvider trackerProvider, + final TrackMate trackmate, + final DisplaySettings displaySettings ) { super( KEY ); this.trackmate = trackmate; this.trackerProvider = trackerProvider; + this.displaySettings = displaySettings; String selectedTracker = SimpleSparseLAPTrackerFactory.THIS2_TRACKER_KEY; // default if ( null != trackmate.getSettings().trackerFactory ) @@ -106,7 +115,12 @@ public void aboutToHidePanel() @Override public Runnable getBackwardRunnable() { - // Delete tracks. - return () -> trackmate.getModel().clearTracks( true ); + // Delete tracks and put back default coloring if needed. + return () -> { + if ( displaySettings.getSpotColorByType() == TrackMateObject.TRACKS + || displaySettings.getSpotColorByType() == TrackMateObject.EDGES ) + displaySettings.setSpotColorBy( TrackMateObject.DEFAULT, FeatureUtils.USE_UNIFORM_COLOR_KEY ); + trackmate.getModel().clearTracks( true ); + }; } } 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 7856dd874..3dfa1a517 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 @@ -36,6 +36,7 @@ public class ConfigureViewsDescriptor extends WizardPanelDescriptor public ConfigureViewsDescriptor( final DisplaySettings ds, final FeatureDisplaySelector featureSelector, + final Action launchBVVAction, final Action launchTrackSchemeAction, final Action showTrackTablesAction, final Action showSpotTableAction, @@ -44,9 +45,10 @@ public ConfigureViewsDescriptor( { super( KEY ); this.targetPanel = new ConfigureViewsPanel( - ds, - featureSelector, + ds, + featureSelector, spaceUnits, + launchBVVAction, launchTrackSchemeAction, showTrackTablesAction, showSpotTableAction, diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java index 8d74b3481..f40d12c7e 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ExecuteTrackingDescriptor.java @@ -28,7 +28,11 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; +import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.components.LogPanel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; public class ExecuteTrackingDescriptor extends WizardPanelDescriptor @@ -38,11 +42,14 @@ public class ExecuteTrackingDescriptor extends WizardPanelDescriptor private final TrackMate trackmate; - public ExecuteTrackingDescriptor( final TrackMate trackmate, final LogPanel logPanel ) + private final DisplaySettings displaySettings; + + public ExecuteTrackingDescriptor( final TrackMate trackmate, final LogPanel logPanel, final DisplaySettings displaySettings ) { super( KEY ); this.trackmate = trackmate; this.targetPanel = logPanel; + this.displaySettings = displaySettings; } @Override @@ -66,6 +73,11 @@ public Runnable getForwardRunnable() logger.log( String.format( " - avg size: %.1f spots.\n", stats.getAverage() ) ); logger.log( String.format( " - min size: %d spots.\n", stats.getMin() ) ); logger.log( String.format( " - max size: %d spots.\n", stats.getMax() ) ); + + // Possibly tweak display settings: color spots by track id. + if ( displaySettings.getSpotColorByType() == TrackMateObject.DEFAULT ) + if ( displaySettings.getSpotColorByFeature().equals( FeatureUtils.USE_UNIFORM_COLOR_KEY ) ) + displaySettings.setSpotColorBy( TrackMateObject.TRACKS, TrackIndexAnalyzer.TRACK_INDEX ); }; } diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java index 4d628b893..e484a6637 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/SpotFilterDescriptor.java @@ -23,7 +23,6 @@ import java.awt.Container; import java.util.List; -import java.util.stream.Collectors; import javax.swing.JLabel; @@ -34,15 +33,14 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackMate; -import fiji.plugin.trackmate.detection.DetectionUtils; import fiji.plugin.trackmate.features.FeatureFilter; -import fiji.plugin.trackmate.features.spot.SpotMorphologyAnalyzerFactory; import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.FilterGuiPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackMateObject; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelection; +import fiji.plugin.trackmate.gui.featureselector.AnalyzerSelectionIO; import fiji.plugin.trackmate.gui.wizard.WizardPanelDescriptor; import fiji.plugin.trackmate.io.SettingsPersistence; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; import fiji.plugin.trackmate.util.EverythingDisablerAndReenabler; public class SpotFilterDescriptor extends WizardPanelDescriptor @@ -103,27 +101,17 @@ public void run() logger.log( String.format( "Retained %d spots out of %d.\n", nselected, ntotal ) ); /* - * Should we add morphology feature analyzers? + * Add analyzers in the user selection and possible the + * morphology ones in 2D or 3D. */ - if ( trackmate.getSettings().detectorFactory != null - && trackmate.getSettings().detectorFactory.has2Dsegmentation() - && DetectionUtils.is2D( trackmate.getSettings().imp ) ) - { - logger.log( "\nAdding morphology analyzers...\n", Logger.BLUE_COLOR ); - final Settings settings = trackmate.getSettings(); - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider = new SpotMorphologyAnalyzerProvider( settings.imp.getNChannels() ); - @SuppressWarnings( "rawtypes" ) - final List< SpotMorphologyAnalyzerFactory > factories = spotMorphologyAnalyzerProvider - .getKeys() - .stream() - .map( key -> spotMorphologyAnalyzerProvider.getFactory( key ) ) - .collect( Collectors.toList() ); - factories.forEach( settings::addSpotAnalyzerFactory ); - final StringBuilder strb = new StringBuilder(); - Settings.prettyPrintFeatureAnalyzer( factories, strb ); - logger.log( strb.toString() ); - } + final AnalyzerSelection analyzerSelection = AnalyzerSelectionIO.readUserDefault(); + final Settings settings = trackmate.getSettings(); + analyzerSelection.configure( settings ); + logger.log( "\nAdding the following spot feature analyzers...\n", Logger.BLUE_COLOR ); + final StringBuilder strb = new StringBuilder(); + Settings.prettyPrintFeatureAnalyzer( settings.getSpotAnalyzerFactories(), strb ); + logger.log( strb.toString() ); /* * Show and log to progress bar in the filter GUI panel. diff --git a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java index 9a7c82a8c..40ac3e8b5 100644 --- a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java +++ b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java @@ -309,6 +309,14 @@ protected JDialog createDialog( final Component lParent ) throws HeadlessExcepti * Read and return an integer attribute from a JDom {@link Element}, and * substitute a default value of 0 if the attribute is not found or of the * wrong type. + * + * @param element + * the element to read from. + * @param name + * the name of the integer attribute. + * @param logger + * error messages will be logged via this logger. + * @return the int value. */ public static final int readIntAttribute( final Element element, final String name, final Logger logger ) { @@ -489,6 +497,12 @@ public static final boolean readStringAttribute( final Element element, final Ma * are added to the specified map. If a value is found not to be a * double, an error is returned. * + * @param element + * the element to unmarshall. + * @param map + * the map the unmarshalled info will be added to. + * @param errorHolder + * error messages will be appended to this buffer. * @return true if all values were found and mapped as doubles, * false otherwise and the error holder is updated. */ @@ -560,6 +574,8 @@ public static final boolean writeDownsamplingFactor( final Map< String, Object > * the key to the parameter value in the map * @param expectedClass * the expected class for the value + * @param errorHolder + * a buffer to append possible errors to. * @return true if the parameter was found, of the right class, * and was successfully added to the element, false if * not, and updated the specified error holder. @@ -587,10 +603,29 @@ public static final boolean writeAttribute( final Map< String, Object > settings /** * Stores the given mapping in a given JDom element, using attributes in a * KEY="VALUE" fashion. + * + * @param map + * the map. + * @param element + * the element to write the map into. */ public static void marshallMap( final Map< String, Double > map, final Element element ) { for ( final String key : map.keySet() ) element.setAttribute( key, map.get( key ).toString() ); } + + /** + * Possibly creates the whole directories needed to save a file with the + * specified path. + * + * @param path + * the path. + * @return true if folders have actually been created. + */ + public static boolean mkdirs( final String path ) + { + final File dir = new File( path ).getParentFile(); + return dir == null ? false : dir.mkdirs(); + } } diff --git a/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java b/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java index bb02c1803..bd00281ca 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java +++ b/src/main/java/fiji/plugin/trackmate/io/TGMMImporter.java @@ -32,12 +32,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import net.imglib2.algorithm.Benchmark; -import net.imglib2.algorithm.OutputAlgorithm; -import net.imglib2.realtransform.AffineTransform3D; -import net.imglib2.util.LinAlgHelpers; -import net.imglib2.util.Util; - import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; @@ -50,7 +44,13 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; +import net.imglib2.algorithm.Benchmark; +import net.imglib2.algorithm.OutputAlgorithm; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.util.LinAlgHelpers; +import net.imglib2.util.Util; public class TGMMImporter implements OutputAlgorithm< Model >, Benchmark { @@ -377,7 +377,7 @@ public boolean process() * Make a spot and add it to this frame collection. */ - final Spot spot = new Spot( mx, my, mz, radius, score, lineage + " (" + id + ")" ); + final Spot spot = new SpotBase( mx, my, mz, radius, score, lineage + " (" + id + ")" ); spots.add( spot ); currentSpotID.put( Integer.valueOf( id ), spot ); diff --git a/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java b/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java index 267ce5d41..32f5bb8d2 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java +++ b/src/main/java/fiji/plugin/trackmate/io/TmXmlReader.java @@ -91,6 +91,7 @@ import static fiji.plugin.trackmate.io.TmXmlKeys.TRACK_FILTER_COLLECTION_ELEMENT_KEY; import static fiji.plugin.trackmate.io.TmXmlKeys.TRACK_ID_ELEMENT_KEY; import static fiji.plugin.trackmate.io.TmXmlKeys.TRACK_NAME_ATTRIBUTE_NAME; +import static fiji.plugin.trackmate.io.TmXmlWriter.MESH_FILE_EXTENSION; import static fiji.plugin.trackmate.tracking.TrackerKeys.XML_ATTRIBUTE_TRACKER_NAME; import java.io.File; @@ -104,6 +105,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; import org.jdom2.Attribute; import org.jdom2.DataConversionException; @@ -122,7 +127,9 @@ import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; import fiji.plugin.trackmate.features.FeatureFilter; @@ -136,8 +143,9 @@ import fiji.plugin.trackmate.gui.wizard.descriptors.ConfigureViewsDescriptor; import fiji.plugin.trackmate.providers.DetectorProvider; 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.providers.TrackerProvider; import fiji.plugin.trackmate.providers.ViewProvider; @@ -147,6 +155,10 @@ import fiji.plugin.trackmate.visualization.trackscheme.TrackScheme; import ij.IJ; import ij.ImagePlus; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; public class TmXmlReader { @@ -184,7 +196,10 @@ public class TmXmlReader */ /** - * Initialize this reader to read the file given in argument. + * Initializes this reader to read the file given in argument. + * + * @param file + * the file to read. */ public TmXmlReader( final File file ) { @@ -218,6 +233,8 @@ public TmXmlReader( final File file ) /** * Returns the log text saved in the file, or null if log text * was not saved. + * + * @return the log. */ public String getLog() { @@ -371,7 +388,6 @@ public Model getModel() ok = false; // Track features - try { final Map< Integer, Map< String, Double > > savedFeatureMap = readTrackFeatures( modelElement ); @@ -423,7 +439,8 @@ public Settings readSettings( final ImagePlus imp ) new SpotAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ), new EdgeAnalyzerProvider(), new TrackAnalyzerProvider(), - new SpotMorphologyAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ) ); + new Spot2DMorphologyAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ), + new Spot3DMorphologyAnalyzerProvider( ( imp == null ) ? 1 : imp.getNChannels() ) ); } /** @@ -433,7 +450,7 @@ public Settings readSettings( final ImagePlus imp ) * file. * * @param imp - * + * the image to store in the new Settings object. * @param detectorProvider * the detector provider, required to configure the settings with * a correct SpotDetectorFactory. If @@ -454,6 +471,11 @@ public Settings readSettings( final ImagePlus imp ) * the track analyzer provider, required to instantiates the * saved {@link TrackAnalyzer}s. If null, will skip * reading track analyzers. + * @param spot2DMorphologyAnalyzerProvider + * the spot 2D morphology provider. + * @param spot3DMorphologyAnalyzerProvider + * the spot 3D morphology provider. + * @return a new Settings object. */ public Settings readSettings( final ImagePlus imp, @@ -462,7 +484,8 @@ public Settings readSettings( final SpotAnalyzerProvider spotAnalyzerProvider, final EdgeAnalyzerProvider edgeAnalyzerProvider, final TrackAnalyzerProvider trackAnalyzerProvider, - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider ) + final Spot2DMorphologyAnalyzerProvider spot2DMorphologyAnalyzerProvider, + final Spot3DMorphologyAnalyzerProvider spot3DMorphologyAnalyzerProvider ) { final Element settingsElement = root.getChild( SETTINGS_ELEMENT_KEY ); if ( null == settingsElement ) @@ -509,13 +532,16 @@ public Settings readSettings( spotAnalyzerProvider, edgeAnalyzerProvider, trackAnalyzerProvider, - spotMorphologyAnalyzerProvider ); + spot2DMorphologyAnalyzerProvider, + spot3DMorphologyAnalyzerProvider ); return settings; } /** * Returns the version string stored in the file. + * + * @return the version string stored in the file. */ public String getVersion() { @@ -911,7 +937,6 @@ private SpotCollection getSpots( final Element modelElement ) final Map< Integer, Set< Spot > > content = new HashMap<>( frameContent.size() ); for ( final Element currentFrameContent : frameContent ) { - currentFrame = readIntAttribute( currentFrameContent, FRAME_ATTRIBUTE_NAME, logger ); final List< Element > spotContent = currentFrameContent.getChildren( SPOT_ELEMENT_KEY ); final Set< Spot > spotSet = new HashSet<>( spotContent.size() ); @@ -923,6 +948,69 @@ private SpotCollection getSpots( final Element modelElement ) } content.put( currentFrame, spotSet ); } + + // Do we have a mesh file? + final File meshFile = new File( file.getAbsolutePath() + MESH_FILE_EXTENSION ); + if ( meshFile.exists() ) + { + // Matcher for zipped file name. + final String regex = "(\\d+)\\.ply"; + final Pattern pattern = Pattern.compile( regex ); + // Iterate through entries. + try (final ZipFile zipFile = new ZipFile( meshFile )) + { + zipFile.stream().forEach( entry -> { + final String name = entry.getName(); + final Matcher matcher = pattern.matcher( name ); + if ( matcher.matches() ) + { + // Get corresponding spot. + final int id = Integer.parseInt( matcher.group( 1 ) ); + final Spot spot = cache.get( id ); + // Deserialize mesh. + try + { + final Mesh m = PLYMeshIO.open( zipFile.getInputStream( entry ) ); + final BufferMesh mesh = new BufferMesh( m.vertices().size(), m.triangles().size() ); + Meshes.calculateNormals( m, mesh ); + + // Create new spot in the mesh and replace it in the + // cache. + final SpotMesh spotMesh = new SpotMesh( id, mesh ); + spotMesh.copyFeaturesFrom( spot ); + spotMesh.setName( spot.getName() ); + cache.put( id, spotMesh ); + + // And in the content. + final Set< Spot > spots = content.get( spot.getFeature( Spot.FRAME ).intValue() ); + spots.remove( spot ); + spots.add( spotMesh ); + } + catch ( final Exception e ) + { + ok = false; + logger.error( "Problem reading mesh for spot " + id + ":\n" + + e.getMessage() + '\n' ); + e.printStackTrace(); + } + } + + } ); + } + catch ( final ZipException e ) + { + ok = false; + logger.error( "Issues reading the mesh file:\n" + e.getMessage() + '\n' ); + e.printStackTrace(); + } + catch ( final IOException e ) + { + ok = false; + logger.error( "Issues reading the mesh file:\n" + e.getMessage() + '\n' ); + e.printStackTrace(); + } + } + final SpotCollection allSpots = SpotCollection.fromMap( content ); return allSpots; } @@ -932,7 +1020,12 @@ private SpotCollection getSpots( final Element modelElement ) * into the model specified. The track collection element is expected to be * found as a child of the specified element. * - * @return true if reading tracks was successful, false otherwise. + * @param modelElement + * the element to read from. + * @param model + * the model to add to. + * @return true if reading tracks was successful, + * false otherwise. */ protected boolean readTracks( final Element modelElement, final Model model ) { @@ -1133,23 +1226,15 @@ private Spot createSpotFrom( final Element spotEl ) { // Read id. final int ID = readIntAttribute( spotEl, SPOT_ID_ATTRIBUTE_NAME, logger ); - final Spot spot = new Spot( ID ); - +// final List< Attribute > atts = spotEl.getAttributes(); removeAttributeFromName( atts, SPOT_ID_ATTRIBUTE_NAME ); - // Read name. - String name = spotEl.getAttributeValue( SPOT_NAME_ATTRIBUTE_NAME ); - if ( null == name || name.equals( "" ) ) - name = "ID" + ID; - - spot.setName( name ); - removeAttributeFromName( atts, SPOT_NAME_ATTRIBUTE_NAME ); - /* * Try to read ROI if any. */ final int roiNPoints = readIntAttribute( spotEl, ROI_N_POINTS_ATTRIBUTE_NAME, Logger.VOID_LOGGER ); + final Spot spot; if ( roiNPoints > 2 ) { final double[] xrois = new double[ roiNPoints ]; @@ -1164,10 +1249,22 @@ private Spot createSpotFrom( final Element spotEl ) final double y = Double.parseDouble( vals[ index++ ] ); yrois[ i ] = y; } - spot.setRoi( new SpotRoi( xrois, yrois ) ); + spot = new SpotRoi( ID, xrois, yrois ); + } + else + { + spot = new SpotBase( ID ); } removeAttributeFromName( atts, ROI_N_POINTS_ATTRIBUTE_NAME ); + // Read name. + String name = spotEl.getAttributeValue( SPOT_NAME_ATTRIBUTE_NAME ); + if ( null == name || name.equals( "" ) ) + name = "ID" + ID; + + spot.setName( name ); + removeAttributeFromName( atts, SPOT_NAME_ATTRIBUTE_NAME ); + /* * Read all other attributes -> features. */ @@ -1297,7 +1394,8 @@ private void readAnalyzers( final SpotAnalyzerProvider spotAnalyzerProvider, final EdgeAnalyzerProvider edgeAnalyzerProvider, final TrackAnalyzerProvider trackAnalyzerProvider, - final SpotMorphologyAnalyzerProvider spotMorphologyAnalyzerProvider ) + final Spot2DMorphologyAnalyzerProvider spot2DMorphologyAnalyzerProvider, + final Spot3DMorphologyAnalyzerProvider spot3DMorphologyAnalyzerProvider ) { final Element analyzersEl = settingsElement.getChild( ANALYZER_COLLECTION_ELEMENT_KEY ); @@ -1348,11 +1446,15 @@ private void readAnalyzers( /* * Special case: if we cannot find a matching * analyzer for a declared factory, then we will try - * to see whether it is a morphology spot analyzer, - * that are treated separately. If it is not, we - * give up. + * to see whether it is a morphology spot analyzer + * in 2D then in 3D, that are treated separately. If + * it is not, we give up. */ - spotAnalyzer = spotMorphologyAnalyzerProvider.getFactory( key ); + spotAnalyzer = spot2DMorphologyAnalyzerProvider.getFactory( key ); + if ( spotAnalyzer == null ) + { + spotAnalyzer = spot3DMorphologyAnalyzerProvider.getFactory( key ); + } } if ( null == spotAnalyzer ) diff --git a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java index 6b74242f9..745c4a45d 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 * . @@ -104,6 +104,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.jdom2.Attribute; import org.jdom2.Document; @@ -119,6 +121,7 @@ import fiji.plugin.trackmate.Settings; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.features.FeatureFilter; import fiji.plugin.trackmate.features.edges.EdgeAnalyzer; @@ -128,10 +131,20 @@ import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import gnu.trove.map.hash.TIntIntHashMap; +import gnu.trove.procedure.TIntIntProcedure; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; +import net.imglib2.mesh.view.TranslateMesh; public class TmXmlWriter { + static final String MESH_FILE_EXTENSION = ".meshes"; + + /** Zip compression level (0-9) */ + private static final int COMPRESSION_LEVEL = 5; + /* * FIELD */ @@ -162,6 +175,8 @@ public TmXmlWriter( final File file ) * * @param file * the xml file to write to, will be overwritten. + * @param logger + * a logger instance to log writing progress and errors. */ public TmXmlWriter( final File file, final Logger logger ) { @@ -178,6 +193,12 @@ public TmXmlWriter( final File file, final Logger logger ) /** * Writes the document to the file. Content must be appended first. * + * @throws FileNotFoundException + * if the file exists but is a directory rather than a regular + * file, does not exist but cannot be created, or cannot be + * opened for any other reason. + * @throws IOException + * if there's any problem writing. * @see #appendLog(String) * @see #appendModel(Model) * @see #appendSettings(Settings) @@ -236,6 +257,8 @@ public void appendModel( final Model model ) modelElement.addContent( filteredTrackElement ); root.addContent( modelElement ); + + writeSpotMeshes( model.getSpots().iterable( false ) ); } /** @@ -695,11 +718,7 @@ protected Element echoAnalyzers( final Settings settings ) return analyzersElement; } - /* - * STATIC METHODS - */ - - private static final Element marshalSpot( final Spot spot, final FeatureModel fm ) + private final Element marshalSpot( final Spot spot, final FeatureModel fm ) { final Collection< Attribute > attributes = new ArrayList<>(); final Attribute IDattribute = new Attribute( SPOT_ID_ATTRIBUTE_NAME, "" + spot.ID() ); @@ -723,23 +742,92 @@ private static final Element marshalSpot( final Spot spot, final FeatureModel fm } final Element spotElement = new Element( SPOT_ELEMENT_KEY ); - final SpotRoi roi = spot.getRoi(); - if ( roi != null ) + if ( spot instanceof SpotRoi ) { - final int nPoints = roi.x.length; + final SpotRoi roi = ( SpotRoi ) spot; + final int nPoints = roi.nPoints(); attributes.add( new Attribute( ROI_N_POINTS_ATTRIBUTE_NAME, Integer.toString( nPoints ) ) ); final StringBuilder str = new StringBuilder(); for ( int i = 0; i < nPoints; i++ ) { - str.append( Double.toString( roi.x[ i ] ) ); + str.append( Double.toString( roi.xr( i ) ) ); str.append( ' ' ); - str.append( Double.toString( roi.y[ i ] ) ); + str.append( Double.toString( roi.yr( i ) ) ); str.append( ' ' ); } spotElement.setText( str.toString() ); } - spotElement.setAttributes( attributes ); return spotElement; } + + protected void writeSpotMeshes( final Iterable< Spot > spots ) + { + // Only create the meshes file if at least one spot has a mesh. + boolean hasMesh = false; + for ( final Spot spot : spots ) + { + if ( spot instanceof SpotMesh ) + { + hasMesh = true; + break; + } + } + if ( !hasMesh ) + return; + + // Holder for map spot -> frame + final TIntIntHashMap frameMap = new TIntIntHashMap(); + + // Create zip output stream and write to it. + final File meshFile = new File( file.getAbsolutePath() + MESH_FILE_EXTENSION ); + logger.log( " Writing spot meshes to " + meshFile.getName() + "\n" ); + + try (final ZipOutputStream zos = new ZipOutputStream( new FileOutputStream( meshFile ) )) + { + zos.setMethod( ZipOutputStream.DEFLATED ); + zos.setLevel( COMPRESSION_LEVEL ); + + // Write spot meshes. + for ( final Spot spot : spots ) + { + if ( spot instanceof SpotMesh ) + { + // Save mesh in true coordinates. + final SpotMesh sm = ( SpotMesh ) spot; + final Mesh mesh = sm.getMesh(); + final Mesh translated = TranslateMesh.translate( mesh, spot ); + final byte[] bs = PLYMeshIO.writeBinary( translated ); + + final String entryName = spot.ID() + ".ply"; + zos.putNextEntry( new ZipEntry( entryName ) ); + zos.write( bs ); + zos.closeEntry(); + + frameMap.put( spot.ID(), spot.getFeature( Spot.FRAME ).intValue() ); + } + } + + // Write dict text file. + final StringBuilder str = new StringBuilder(); + str.append( "frame,ID\n" ); + frameMap.forEachEntry( new TIntIntProcedure() + { + + @Override + public boolean execute( final int ID, final int t ) + { + str.append( String.format( "%d,%d\n", t, ID ) ); + return true; + } + } ); + zos.putNextEntry( new ZipEntry( "mesh-info.txt" ) ); + zos.write( str.toString().getBytes() ); + } + catch ( final IOException e ) + { + logger.error( "Problem writing the mesh file:\n" + e.getMessage() ); + e.printStackTrace(); + } + } } diff --git a/src/main/java/fiji/plugin/trackmate/providers/SpotMorphologyAnalyzerProvider.java b/src/main/java/fiji/plugin/trackmate/providers/Spot2DMorphologyAnalyzerProvider.java similarity index 68% rename from src/main/java/fiji/plugin/trackmate/providers/SpotMorphologyAnalyzerProvider.java rename to src/main/java/fiji/plugin/trackmate/providers/Spot2DMorphologyAnalyzerProvider.java index 6f36f99c8..9738c4271 100644 --- a/src/main/java/fiji/plugin/trackmate/providers/SpotMorphologyAnalyzerProvider.java +++ b/src/main/java/fiji/plugin/trackmate/providers/Spot2DMorphologyAnalyzerProvider.java @@ -21,24 +21,24 @@ */ package fiji.plugin.trackmate.providers; -import fiji.plugin.trackmate.features.spot.SpotMorphologyAnalyzerFactory; +import fiji.plugin.trackmate.features.spot.Spot2DMorphologyAnalyzerFactory; @SuppressWarnings( "rawtypes" ) -public class SpotMorphologyAnalyzerProvider extends AbstractProvider< SpotMorphologyAnalyzerFactory > +public class Spot2DMorphologyAnalyzerProvider extends AbstractProvider< Spot2DMorphologyAnalyzerFactory > { private final int nChannels; - public SpotMorphologyAnalyzerProvider( final int nChannels ) + public Spot2DMorphologyAnalyzerProvider( final int nChannels ) { - super( SpotMorphologyAnalyzerFactory.class ); + super( Spot2DMorphologyAnalyzerFactory.class ); this.nChannels = nChannels; } @Override - public SpotMorphologyAnalyzerFactory getFactory( final String key ) + public Spot2DMorphologyAnalyzerFactory getFactory( final String key ) { - final SpotMorphologyAnalyzerFactory factory = super.getFactory( key ); + final Spot2DMorphologyAnalyzerFactory factory = super.getFactory( key ); if ( factory == null ) return null; @@ -48,7 +48,7 @@ public SpotMorphologyAnalyzerFactory getFactory( final String key ) public static void main( final String[] args ) { - final SpotMorphologyAnalyzerProvider provider = new SpotMorphologyAnalyzerProvider( 2 ); + final Spot2DMorphologyAnalyzerProvider provider = new Spot2DMorphologyAnalyzerProvider( 2 ); System.out.println( provider.echo() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/providers/Spot3DMorphologyAnalyzerProvider.java b/src/main/java/fiji/plugin/trackmate/providers/Spot3DMorphologyAnalyzerProvider.java new file mode 100644 index 000000000..b8d605b45 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/providers/Spot3DMorphologyAnalyzerProvider.java @@ -0,0 +1,57 @@ +/*- + * #%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.providers; + +import fiji.plugin.trackmate.features.spot.Spot3DMorphologyAnalyzerFactory; + +/** + * Provider for 3D morphology analyzers, working on SpotMesh. + */ +@SuppressWarnings( "rawtypes" ) +public class Spot3DMorphologyAnalyzerProvider extends AbstractProvider< Spot3DMorphologyAnalyzerFactory > +{ + + private final int nChannels; + + public Spot3DMorphologyAnalyzerProvider( final int nChannels ) + { + super( Spot3DMorphologyAnalyzerFactory.class ); + this.nChannels = nChannels; + } + + @Override + public Spot3DMorphologyAnalyzerFactory getFactory( final String key ) + { + final Spot3DMorphologyAnalyzerFactory factory = super.getFactory( key ); + if ( factory == null ) + return null; + + factory.setNChannels( nChannels ); + return factory; + } + + public static void main( final String[] args ) + { + final Spot3DMorphologyAnalyzerProvider provider = new Spot3DMorphologyAnalyzerProvider( 2 ); + System.out.println( provider.echo() ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java index c867b045a..e06a8e5af 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/SpotTrackerFactory.java @@ -56,14 +56,18 @@ public interface SpotTrackerFactory extends TrackMateModule public ConfigurationPanel getTrackerConfigurationPanel( final Model model ); /** - * Marshalls a settings map to a JDom element, ready for saving to XML. The + * Marshals a settings map to a JDom element, ready for saving to XML. The * element is updated with new attributes. *

* Only parameters specific to the concrete tracker factory are marshalled. * The element also always receive an attribute named * {@value TrackerKeys#XML_ATTRIBUTE_TRACKER_NAME} that saves the target * {@link SpotTracker} key. - * + * + * @param settings + * the settings map to marshal. + * @param element + * the element to marshal to. * @return true if marshalling was successful. If not, check * {@link #getErrorMessage()} */ @@ -109,7 +113,9 @@ public interface SpotTrackerFactory extends TrackMateModule * validity check is strict: we check that all needed parameters are here * and are of the right class, and that there is no extra unwanted * parameters. - * + * + * @param settings + * the settings map. * @return true if the settings map can be used with the target factory. If * not, check {@link #getErrorMessage()} */ diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java index 195498918..09a714ed3 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/LAPUtils.java @@ -43,6 +43,7 @@ import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_GAP_CLOSING_FEATURE_PENALTIES; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_GAP_CLOSING_MAX_DISTANCE; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_GAP_CLOSING_MAX_FRAME_GAP; +import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_FEATURE_PENALTIES; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_MERGING_FEATURE_PENALTIES; @@ -74,7 +75,6 @@ import javax.swing.table.TableModel; import fiji.plugin.trackmate.Spot; -import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_KALMAN_SEARCH_RADIUS; public class LAPUtils { @@ -183,8 +183,7 @@ public static String echoFeaturePenalties( final Map< String, Double > featurePe } /** - * Compute the cost to link two spots, in the default way for the TrackMate - * trackmate. + * Computes the cost to link two spots, in the default way for TrackMate. *

* This cost is calculated as follow: *

    @@ -209,6 +208,19 @@ public static String echoFeaturePenalties( final Map< String, Double > featurePe * For instance: if 2 spots differ by twice the value in a feature which is * in the penalty map with a factor of 1, they will look as if they * were twice as far. + * + * @param s0 + * the source spot. + * @param s1 + * the target spot. + * @param distanceCutOff + * the distance cutoff. + * @param blockingValue + * the blocking value. + * @param featurePenalties + * the feature penalties, as a map of feature keys to penalty + * weight. + * @return the linking cost. */ public static final double computeLinkingCostFor( final Spot s0, final Spot s1, final double distanceCutOff, final double blockingValue, final Map< String, Double > featurePenalties ) { @@ -233,18 +245,25 @@ public static final double computeLinkingCostFor( final Spot s0, final Spot s1, } /** - * @return true if the settings map can be used with the LAP trackers. We do - * not check that all the spot features used in penalties are indeed - * found in all spots, because if such a feature is absent from one - * spot, the LAP trackers simply ignores the penalty and does not - * generate an error. + * Returns true if the settings map can be used with the LAP + * trackers. We do not check that all the spot features used in penalties + * are indeed found in all spots, because if such a feature is absent from + * one spot, the LAP trackers simply ignores the penalty and does not + * generate an error. + * * @param settings * the map to test. + * @param linking + * if true will also test for the presence of the + * frame-to-frame linking keys. If false will only + * test for the segment linking keys. * @param errorHolder * a {@link StringBuilder} that will contain an error message if * the check is not successful. + * @return true if the settings map can be used with the LAP + * trackers. */ - public static final boolean checkSettingsValidity( final Map< String, Object > settings, final StringBuilder errorHolder, boolean linking ) + public static final boolean checkSettingsValidity( final Map< String, Object > settings, final StringBuilder errorHolder, final boolean linking ) { if ( null == settings ) { diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java index 13099bee9..7ff0bea19 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/DefaultCostMatrixCreator.java @@ -35,6 +35,9 @@ * @author Jean-Yves Tinevez - 2014 * * @param + * the type of sources. + * @param + * the type of targets. */ public class DefaultCostMatrixCreator< K extends Comparable< K >, J extends Comparable< J > > implements CostMatrixCreator< K, J > { diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java index e9586b238..4292d750e 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanLinkingCostMatrixCreator.java @@ -35,7 +35,9 @@ * @author Jean-Yves Tinevez - 2014 * * @param + * the type of sources. * @param + * the type of targets. */ public class JaqamanLinkingCostMatrixCreator< K extends Comparable< K >, J extends Comparable< J > > implements CostMatrixCreator< K, J > { diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java index 8a03bcdb3..cd1ebbb82 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/JaqamanSegmentCostMatrixCreator.java @@ -37,12 +37,6 @@ import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; import static fiji.plugin.trackmate.util.TMUtils.checkParameter; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.tracking.jaqaman.costfunction.CostFunction; -import fiji.plugin.trackmate.tracking.jaqaman.costfunction.FeaturePenaltyCostFunction; -import fiji.plugin.trackmate.tracking.jaqaman.costfunction.SquareDistCostFunction; -import fiji.plugin.trackmate.util.Threads; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -50,10 +44,16 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; -import net.imglib2.algorithm.MultiThreaded; import org.jgrapht.Graph; import org.jgrapht.graph.DefaultWeightedEdge; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.tracking.jaqaman.costfunction.CostFunction; +import fiji.plugin.trackmate.tracking.jaqaman.costfunction.FeaturePenaltyCostFunction; +import fiji.plugin.trackmate.tracking.jaqaman.costfunction.SquareDistCostFunction; +import fiji.plugin.trackmate.util.Threads; +import net.imglib2.algorithm.MultiThreaded; + /** * This class generates the top-left quadrant of the LAP segment linking cost * matrix, following Jaqaman et al., 2008 Nature Methods. It can @@ -98,7 +98,13 @@ public class JaqamanSegmentCostMatrixCreator implements CostMatrixCreator< Spot, /** * Instantiates a cost matrix creator for the top-left quadrant of the * segment linking cost matrix. - * + * + * @param graph + * the graph from which connected components (segments) will be + * extracted. + * @param settings + * the settings for the cost matrix, as map containing the + * Jaqaman LAP keys. */ public JaqamanSegmentCostMatrixCreator( final Graph< Spot, DefaultWeightedEdge > graph, final Map< String, Object > settings ) { diff --git a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java index e214eabe2..97782786d 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/jaqaman/costmatrix/SparseCostMatrix.java @@ -101,7 +101,7 @@ public class SparseCostMatrix * These two arrays must be arranged row by row, starting with the first * one. And in each row, the columns must be sorted in increasing order (to * facilitate index search). Also, each row must have at least one - * non-infinte cost. If not, an {@link IllegalArgumentException} is thrown. + * non-infinite cost. If not, an {@link IllegalArgumentException} is thrown. *
      *
    1. number an int[] array, with one element per * row, that contains the number of non infinite cost for a row. @@ -113,6 +113,8 @@ public class SparseCostMatrix * the column index of each cost. * @param number * the number of element for each row. + * @param nCols + * the number of columns in the cost matrix. * @throws IllegalArgumentException * if the cost and column arrays are not of the same size, if * the column array is not sorted row by row, of if one row has diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java index 7477b9609..1cb6ce27c 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kalman/KalmanTracker.java @@ -36,6 +36,7 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.tracking.SpotTracker; import fiji.plugin.trackmate.tracking.jaqaman.JaqamanLinker; @@ -85,11 +86,20 @@ public class KalmanTracker implements SpotTracker, Benchmark, Cancelable */ /** + * Creates a new Kalman tracker. + * * @param spots * the spots to track. * @param maxSearchRadius + * the maximal search radius to continue a track, in physical + * units. * @param maxFrameGap + * the max frame gap when detections are missing, after which a + * track will be stopped. * @param initialSearchRadius + * the initial search radius to nucleate new tracks. + * @param featurePenalties + * the feature penalties. */ public KalmanTracker( final SpotCollection spots, final double maxSearchRadius, final int maxFrameGap, final double initialSearchRadius, final Map< String, Double > featurePenalties ) { @@ -236,17 +246,17 @@ public boolean process() { final double[] X = kf.predict(); final Spot s = kalmanFiltersMap.get( kf ); - final Spot predSpot = new Spot( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); + final Spot predSpot = new SpotBase( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); // copy the necessary features of original spot to the predicted // spot if ( null != featurePenalties ) - predSpot.copyFeatures( s, featurePenalties ); + predSpot.copyFeaturesFrom( s, featurePenalties.keySet() ); predictionMap.put( predSpot, kf ); if ( savePredictions ) { - final Spot pred = new Spot( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); + final Spot pred = new SpotBase( X[ 0 ], X[ 1 ], X[ 2 ], s.getFeature( Spot.RADIUS ), s.getFeature( Spot.QUALITY ) ); pred.setName( "Pred_" + s.getName() ); pred.putFeature( Spot.RADIUS, s.getFeature( Spot.RADIUS ) ); predictionsCollection.add( predSpot, frame ); diff --git a/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java index 628560a96..6feca9b02 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTracker.java @@ -24,7 +24,6 @@ import static fiji.plugin.trackmate.tracking.overlap.OverlapTrackerFactory.BASE_ERROR_MESSAGE; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -204,7 +203,11 @@ public boolean process() final Map< Spot, Polygon2D > targetGeometries = createGeometry( spots.iterable( targetFrame, true ), method, enlargeFactor ); if ( sourceGeometries.isEmpty() || targetGeometries.isEmpty() ) + { + sourceGeometries = targetGeometries; + logger.setProgress( ( double ) progress++ / spots.keySet().size() ); continue; + } final ExecutorService executors = Threads.newFixedThreadPool( numThreads ); final List< Future< IoULink > > futures = new ArrayList<>(); @@ -298,18 +301,17 @@ private static SimplePolygon2D toPolygon( final Spot spot, final double scale ) { final double xc = spot.getDoublePosition( 0 ); final double yc = spot.getDoublePosition( 1 ); - final SpotRoi roi = spot.getRoi(); final SimplePolygon2D poly; - if ( roi == null ) + if ( spot instanceof SpotRoi ) { - final double radius = spot.getFeature( Spot.RADIUS ).doubleValue(); - poly = new SimplePolygon2D( new Circle2D( xc, yc, radius ).asPolyline( 32 ) ); + final SpotRoi roi = ( SpotRoi ) spot; + final double[][] out = roi.toArray( 0., 0., 1., 1. ); + poly = new SimplePolygon2D( out[ 0 ], out[ 1 ] ); } else { - final double[] xcoords = roi.toPolygonX( 1., 0., xc, 1. ); - final double[] ycoords = roi.toPolygonY( 1., 0., yc, 1. ); - poly = new SimplePolygon2D( xcoords, ycoords ); + final double radius = spot.getFeature( Spot.RADIUS ).doubleValue(); + poly = new SimplePolygon2D( new Circle2D( xc, yc, radius ).asPolyline( 32 ) ); } return poly.transform( AffineTransform2D.createScaling( new Point2D( xc, yc ), scale, scale ) ); } @@ -318,19 +320,19 @@ private static Rectangle2D toBoundingBox( final Spot spot, final double scale ) { final double xc = spot.getDoublePosition( 0 ); final double yc = spot.getDoublePosition( 1 ); - final SpotRoi roi = spot.getRoi(); - if ( roi == null ) + if ( spot instanceof SpotRoi ) { - final double radius = spot.getFeature( Spot.RADIUS ).doubleValue() * scale; - return new Rectangle2D( xc - radius, yc - radius, 2 * radius, 2 * radius ); + final SpotRoi roi = ( SpotRoi ) spot; + final double minX = roi.realMin( 0 ) * scale; + final double maxX = roi.realMax( 0 ) * scale; + final double minY = roi.realMin( 1 ) * scale; + final double maxY = roi.realMax( 1 ) * scale; + return new Rectangle2D( xc + minX, yc + minY, maxX - minX, maxY - minY ); } else { - final double minX = Arrays.stream( roi.x ).min().getAsDouble() * scale; - final double maxX = Arrays.stream( roi.x ).max().getAsDouble() * scale; - final double minY = Arrays.stream( roi.y ).min().getAsDouble() * scale; - final double maxY = Arrays.stream( roi.y ).max().getAsDouble() * scale; - return new Rectangle2D( xc + minX, yc + minY, maxX - minX, maxY - minY ); + final double radius = spot.getFeature( Spot.RADIUS ).doubleValue() * scale; + return new Rectangle2D( xc - radius, yc - radius, 2 * radius, 2 * radius ); } } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTrackerFactory.java index 5bc05f791..5741b343b 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/overlap/OverlapTrackerFactory.java @@ -175,6 +175,8 @@ public boolean unmarshall( final Element element, final Map< String, Object > se ok = ok & readDoubleAttribute( element, settings, KEY_SCALE_FACTOR, errorHolder ); ok = ok & readDoubleAttribute( element, settings, KEY_MIN_IOU, errorHolder ); ok = ok & readStringAttribute( element, settings, KEY_IOU_CALCULATION, errorHolder ); + if ( !ok ) + errorMessage = "[" + getKey() + "] " + errorHolder.toString(); return ok; } diff --git a/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java b/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java index 97f58371d..3da46c7be 100644 --- a/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java +++ b/src/main/java/fiji/plugin/trackmate/util/ChartExporter.java @@ -61,7 +61,7 @@ public class ChartExporter * @param height * the height of the panel the chart is painted in. * @throws UnsupportedEncodingException - * @throws IOException + * If the UTF-8 encoding is not supported. */ public static void exportChartAsSVG( final File svgFile, final JFreeChart chart, final int width, final int height ) throws UnsupportedEncodingException, IOException { diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java index 43298c3e3..2a42efdce 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java @@ -162,6 +162,8 @@ protected Pair< Model, Double > runPreviewDetection( final Settings lSettings = new Settings( settings.imp ); lSettings.tstart = frame; lSettings.tend = frame; + lSettings.zstart = settings.zstart; + lSettings.zend = settings.zend; settings.setRoi( settings.imp.getRoi() ); lSettings.detectorFactory = detectorFactory; diff --git a/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java b/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java index cc98c862c..01cf189a9 100644 --- a/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java +++ b/src/main/java/fiji/plugin/trackmate/util/OnRequestUpdater.java @@ -81,6 +81,9 @@ public class OnRequestUpdater extends Thread /** * Constructor autostarts thread + * + * @param refreshable + * the refreshable to update. */ public OnRequestUpdater( final Refreshable refreshable ) { diff --git a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java index 5efdc98dd..7b17c0346 100644 --- a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java +++ b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhood.java @@ -22,10 +22,10 @@ package fiji.plugin.trackmate.util; import fiji.plugin.trackmate.Spot; -import net.imagej.ImgPlus; import net.imglib2.FinalInterval; import net.imglib2.Interval; import net.imglib2.Positionable; +import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.RealPositionable; import net.imglib2.algorithm.neighborhood.Neighborhood; @@ -35,6 +35,7 @@ import net.imglib2.algorithm.region.localneighborhood.RectangleNeighborhoodGPL; import net.imglib2.outofbounds.OutOfBoundsMirrorExpWindowingFactory; import net.imglib2.type.numeric.RealType; +import net.imglib2.view.Views; public class SpotNeighborhood< T extends RealType< T > > implements Neighborhood< T > { @@ -53,18 +54,22 @@ public class SpotNeighborhood< T extends RealType< T > > implements Neighborhood * CONSTRUCTOR */ - public SpotNeighborhood( final Spot spot, final ImgPlus< T > img ) + public SpotNeighborhood( final Spot spot, final RandomAccessible< T > ra, final double[] calibration ) { - this.calibration = TMUtils.getSpatialCalibration( img ); - // Center - this.center = new long[ img.numDimensions() ]; + this.calibration = calibration; + // Center, span and interval. + this.center = new long[ ra.numDimensions() ]; + final long[] span = new long[ ra.numDimensions() ]; + final long[] min = new long[ra.numDimensions()]; + final long[] max = new long[ ra.numDimensions() ]; for ( int d = 0; d < center.length; d++ ) - center[ d ] = Math.round( spot.getFeature( Spot.POSITION_FEATURES[ d ] ).doubleValue() / calibration[ d ] ); - - // Span - final long[] span = new long[ img.numDimensions() ]; - for ( int d = 0; d < span.length; d++ ) + { + center[ d ] = Math.round( spot.getDoublePosition( d ) / calibration[ d ] ); span[ d ] = Math.round( spot.getFeature( Spot.RADIUS ) / calibration[ d ] ); + min[d] = center[d] - span[d]; + max[d] = center[d] + span[d]; + } + final FinalInterval interval = new FinalInterval( min, max ); // Neighborhood @@ -74,28 +79,26 @@ public SpotNeighborhood( final Spot spot, final ImgPlus< T > img ) * have to test pedantically. */ + final RandomAccessibleInterval< T > rai = Views.interval( ra, interval ); final OutOfBoundsMirrorExpWindowingFactory< T, RandomAccessibleInterval< T > > oob = new OutOfBoundsMirrorExpWindowingFactory<>(); - if ( img.numDimensions() == 2 && img.dimension( 0 ) < 2 || img.dimension( 1 ) < 2 ) + if ( ra.numDimensions() == 1 ) { - if ( img.dimension( 0 ) < 2 ) - span[ 0 ] = 0; - else - span[ 1 ] = 0; - this.neighborhood = new RectangleNeighborhoodGPL<>( img, oob ); + span[ 0 ] = 0; + this.neighborhood = new RectangleNeighborhoodGPL<>( rai, oob ); neighborhood.setPosition( center ); neighborhood.setSpan( span ); } - else if ( img.numDimensions() == 2 ) + else if ( ra.numDimensions() == 2 ) { - this.neighborhood = new EllipseNeighborhood<>( img, center, span, oob ); + this.neighborhood = new EllipseNeighborhood<>( rai, center, span, oob ); } - else if ( img.numDimensions() == 3 ) + else if ( ra.numDimensions() == 3 ) { - this.neighborhood = new EllipsoidNeighborhood<>( img, center, span, oob ); + this.neighborhood = new EllipsoidNeighborhood<>( rai, center, span, oob ); } else { - throw new IllegalArgumentException( "Source input must be 1D, 2D or 3D, got nDims = " + img.numDimensions() ); + throw new IllegalArgumentException( "Source input must be 1D, 2D or 3D, got nDims = " + ra.numDimensions() ); } } diff --git a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java index 168e6da30..89185f867 100644 --- a/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java +++ b/src/main/java/fiji/plugin/trackmate/util/SpotNeighborhoodCursor.java @@ -44,7 +44,7 @@ public class SpotNeighborhoodCursor< T extends RealType< T > > implements Cursor * CONSTRUCTOR */ - public SpotNeighborhoodCursor( SpotNeighborhood< T > sn ) + public SpotNeighborhoodCursor( final SpotNeighborhood< T > sn ) { this.cursor = sn.neighborhood.cursor(); this.calibration = sn.calibration; @@ -59,21 +59,24 @@ public SpotNeighborhoodCursor( SpotNeighborhood< T > sn ) */ /** - * Store the relative calibrated position with respect to the + * Stores the relative calibrated position with respect to the * neighborhood center. + * + * @param position + * an array to store to relative position in. */ - public void getRelativePosition( double[] position ) + public void getRelativePosition( final double[] position ) { cursor.localize( pos ); for ( int d = 0; d < center.length; d++ ) - { position[ d ] = calibration[ d ] * ( pos[ d ] - center[ d ] ); - } } /** - * Return the square distance measured from the center of the domain to the + * Returns the square distance measured from the center of the domain to the * current cursor position, in calibrated units. + * + * @return the square distance. */ public double getDistanceSquared() { @@ -89,32 +92,36 @@ public double getDistanceSquared() } /** - * Return the current inclination with respect to this spot center. Will be + * Returns the current inclination with respect to this spot center. Will be * in the range [0, π]. *

      * In spherical coordinates, the inclination is the angle between the Z axis * and the line OM where O is the sphere center and M is the point location. + * + * @return the inclination. */ public double getTheta() { if ( numDimensions() < 2 ) return 0; - double dx = calibration[ 2 ] * ( cursor.getDoublePosition( 2 ) - center[ 2 ] ); + final double dx = calibration[ 2 ] * ( cursor.getDoublePosition( 2 ) - center[ 2 ] ); return Math.acos( dx / Math.sqrt( getDistanceSquared() ) ); } /** - * Return the azimuth of the spherical coordinates of this cursor, with + * Returns the azimuth of the spherical coordinates of this cursor, with * respect to its center. Will be in the range ]-π, π]. *

      * In spherical coordinates, the azimuth is the angle measured in the plane * XY between the X axis and the line OH where O is the sphere center and H * is the orthogonal projection of the point M on the XY plane. + * + * @return the azimuth. */ public double getPhi() { - double dx = calibration[ 0 ] * ( cursor.getDoublePosition( 0 ) - center[ 0 ] ); - double dy = calibration[ 1 ] * ( cursor.getDoublePosition( 1 ) - center[ 1 ] ); + final double dx = calibration[ 0 ] * ( cursor.getDoublePosition( 0 ) - center[ 0 ] ); + final double dy = calibration[ 1 ] * ( cursor.getDoublePosition( 1 ) - center[ 1 ] ); return Math.atan2( dy, dx ); } @@ -123,25 +130,25 @@ public double getPhi() */ @Override - public void localize( float[] position ) + public void localize( final float[] position ) { cursor.localize( position ); } @Override - public void localize( double[] position ) + public void localize( final double[] position ) { cursor.localize( position ); } @Override - public float getFloatPosition( int d ) + public float getFloatPosition( final int d ) { return cursor.getFloatPosition( d ); } @Override - public double getDoublePosition( int d ) + public double getDoublePosition( final int d ) { return cursor.getDoublePosition( d ); } @@ -165,7 +172,7 @@ public Cursor< T > copy() } @Override - public void jumpFwd( long steps ) + public void jumpFwd( final long steps ) { cursor.jumpFwd( steps ); } @@ -201,25 +208,25 @@ public void remove() } @Override - public void localize( int[] position ) + public void localize( final int[] position ) { cursor.localize( position ); } @Override - public void localize( long[] position ) + public void localize( final long[] position ) { cursor.localize( position ); } @Override - public int getIntPosition( int d ) + public int getIntPosition( final int d ) { return cursor.getIntPosition( d ); } @Override - public long getLongPosition( int d ) + public long getLongPosition( final int d ) { return cursor.getLongPosition( d ); } diff --git a/src/main/java/fiji/plugin/trackmate/util/SpotUtil.java b/src/main/java/fiji/plugin/trackmate/util/SpotUtil.java deleted file mode 100644 index 1fc72fc4c..000000000 --- a/src/main/java/fiji/plugin/trackmate/util/SpotUtil.java +++ /dev/null @@ -1,331 +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.util; - -import java.util.Iterator; - -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; -import fiji.plugin.trackmate.detection.DetectionUtils; -import net.imagej.ImgPlus; -import net.imglib2.Cursor; -import net.imglib2.FinalInterval; -import net.imglib2.Interval; -import net.imglib2.IterableInterval; -import net.imglib2.Localizable; -import net.imglib2.RandomAccess; -import net.imglib2.RealLocalizable; -import net.imglib2.type.numeric.RealType; -import net.imglib2.util.Intervals; -import net.imglib2.util.Util; -import net.imglib2.view.IntervalView; -import net.imglib2.view.Views; - -public class SpotUtil -{ - - public static final < T extends RealType< T > > IterableInterval< T > iterable( final SpotRoi roi, final RealLocalizable center, final ImgPlus< T > img ) - { - final SpotRoiIterable< T > neighborhood = new SpotRoiIterable<>( roi, center, img ); - if ( neighborhood.dimension( 0 ) <= 1 && neighborhood.dimension( 1 ) <= 1 ) - return makeSinglePixelIterable( center, img ); - else - return neighborhood; - } - - public static final < T extends RealType< T > > IterableInterval< T > iterable( final Spot spot, final ImgPlus< T > img ) - { - // Prepare neighborhood - final SpotRoi roi = spot.getRoi(); - if ( null != roi && DetectionUtils.is2D( img ) ) - { - // Operate on ROI only if we have one and the image is 2D. - return iterable( roi, spot, img ); - } - else - { - // Otherwise default to circle / sphere. - final SpotNeighborhood< T > neighborhood = new SpotNeighborhood<>( spot, img ); - - final int npixels = ( int ) neighborhood.size(); - if ( npixels <= 1 ) - return makeSinglePixelIterable( spot, img ); - else - return neighborhood; - } - } - - private static < T > IterableInterval< T > makeSinglePixelIterable( final RealLocalizable center, final ImgPlus< T > img ) - { - final double[] calibration = TMUtils.getSpatialCalibration( img ); - 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 ); - } - - private static final class SpotRoiIterable< T extends RealType< T > > implements IterableInterval< T > - { - - private final SpotRoi roi; - - private final RealLocalizable center; - - private final ImgPlus< T > img; - - private final FinalInterval interval; - - public SpotRoiIterable( final SpotRoi roi, final RealLocalizable center, final ImgPlus< T > img ) - { - this.roi = roi; - this.center = center; - this.img = img; - final double[] x = roi.toPolygonX( img.averageScale( 0 ), 0, center.getDoublePosition( 0 ), 1. ); - final double[] y = roi.toPolygonX( img.averageScale( 1 ), 0, center.getDoublePosition( 1 ), 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 >( roi, center, img ); - } - - @Override - public Cursor< T > localizingCursor() - { - return cursor(); - } - - @Override - public Iterator< T > iterator() - { - return cursor(); - } - } - - private static final class MyCursor< T extends RealType< T > > implements Cursor< T > - { - - private final SpotRoi roi; - - private final RealLocalizable center; - - private final ImgPlus< T > img; - - private final FinalInterval interval; - - private Cursor< T > cursor; - - private final double[] x; - - private final double[] y; - - private boolean hasNext; - - private RandomAccess< T > ra; - - public MyCursor( final SpotRoi roi, final RealLocalizable center, final ImgPlus< T > img ) - { - this.roi = roi; - this.center = center; - this.img = img; - x = roi.toPolygonX( img.averageScale( 0 ), 0, center.getDoublePosition( 0 ), 1. ); - y = roi.toPolygonY( img.averageScale( 1 ), 0, center.getDoublePosition( 1 ), 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 ); - reset(); - } - - @Override - public T get() - { - return ra.get(); - } - - @Override - public void fwd() - { - ra.setPosition( cursor ); - fetch(); - } - - private void fetch() - { - while ( cursor.hasNext() ) - { - cursor.fwd(); - if ( isInside( cursor, x, y ) ) - { - hasNext = cursor.hasNext(); - return; - } - } - hasNext = false; - } - - private static final boolean isInside( final Localizable localizable, final double[] x, final double[] y ) - { - // Taken from Imglib2-roi GeomMaths. No edge case. - final double xl = localizable.getDoublePosition( 0 ); - final double yl = localizable.getDoublePosition( 1 ); - - int i; - int j; - boolean inside = false; - for ( i = 0, j = x.length - 1; i < x.length; j = i++ ) - { - final double xj = x[ j ]; - final double yj = y[ j ]; - - final double xi = x[ i ]; - final double yi = y[ i ]; - - if ( ( yi > yl ) != ( yj > yl ) && ( xl < ( xj - xi ) * ( yl - yi ) / ( yj - yi ) + xi ) ) - inside = !inside; - } - return inside; - } - - @Override - public void reset() - { - final IntervalView< T > view = Views.interval( img, interval ); - cursor = view.localizingCursor(); - ra = Views.extendMirrorSingle( img ).randomAccess(); - fetch(); - } - - @Override - public double getDoublePosition( final int d ) - { - return ra.getDoublePosition( d ); - } - - @Override - public int numDimensions() - { - return 2; - } - - @Override - public void jumpFwd( final long steps ) - { - for ( int i = 0; i < steps; i++ ) - fwd(); - } - - @Override - public boolean hasNext() - { - return hasNext; - } - - @Override - public T next() - { - fwd(); - return get(); - } - - @Override - public long getLongPosition( final int d ) - { - return ra.getLongPosition( d ); - } - - @Override - public Cursor< T > copy() - { - return new MyCursor<>( roi, center, img ); - } - } -} diff --git a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java index 4acca565c..19bacbfbe 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java +++ b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java @@ -45,6 +45,7 @@ import fiji.plugin.trackmate.Dimension; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; import ij.IJ; import ij.ImagePlus; import net.imagej.ImgPlus; @@ -55,7 +56,6 @@ import net.imglib2.img.ImagePlusAdapter; import net.imglib2.img.display.imagej.ImgPlusViews; import net.imglib2.type.Type; -import net.imglib2.type.numeric.real.DoubleType; import net.imglib2.util.Util; /** @@ -73,7 +73,17 @@ public class TMUtils */ /** - * Return a new map sorted by its values. + * Returns a new map sorted by its values. + * + * @param + * the type of keys in the map. + * @param + * the type of values in the map. + * @param map + * the map. + * @param comparator + * a comparator to sort based on values. + * @return a new map, with entries sorted by values. */ public static < K, V extends Comparable< ? super V > > Map< K, V > sortByValue( final Map< K, V > map, final Comparator< V > comparator ) { @@ -97,7 +107,13 @@ public int compare( final Entry< K, V > o1, final Entry< K, V > o2 ) } /** - * Generate a string representation of a map, typically a settings map. + * Generates a string representation of a map, typically a settings map. + * + * @param map + * the map. + * @param indent + * the indent size to use. + * @return a representation of the map. */ public static final String echoMap( final Map< String, Object > map, final int indent ) { @@ -134,16 +150,19 @@ else if ( obj instanceof Logger ) } /** - * Wraps an IJ {@link ImagePlus} in an imglib2 {@link ImgPlus}, without - * parameterized types. The only way I have found to beat javac constraints - * on bounded multiple wildcard. + * Wraps an IJ {@link ImagePlus} in an imglib2 {@link ImgPlus}, abiding to a + * returned type. + * + * @param + * the pixel type in the returned image. + * @param imp + * the {@link ImagePlus} to wrap. + * @return a wrapped {@link ImgPlus}. */ - @SuppressWarnings( "rawtypes" ) - public static final ImgPlus rawWraps( final ImagePlus imp ) + @SuppressWarnings( "unchecked" ) + public static final < T > ImgPlus< T > rawWraps( final ImagePlus imp ) { - final ImgPlus< DoubleType > img = ImagePlusAdapter.wrapImgPlus( imp ); - final ImgPlus raw = img; - return raw; + return ( ImgPlus< T > ) ImagePlusAdapter.wrapImgPlus( imp ); } /** @@ -162,6 +181,8 @@ public static final ImgPlus rawWraps( final ImagePlus imp ) * will be appended with an error message. * @return if all mandatory keys are found in the map, and possibly some * optional ones, but no others. + * @param + * the type of keys. */ public static final < T > boolean checkMapKeys( final Map< T, ? > map, Collection< T > mandatoryKeys, Collection< T > optionalKeys, final StringBuilder errorHolder ) { @@ -195,8 +216,39 @@ public static final < T > boolean checkMapKeys( final Map< T, ? > map, Collectio } /** - * Check the presence and the validity of a key in a map, and test it is of - * the desired class. + * Check the optional presence and the validity of a key in a map, and test + * it is of the desired class. If the key is not present, this method + * returns true. If it is present, it tests the value is of the + * right class. + * + * @param map + * the map to inspect. + * @param key + * the key to find. + * @param expectedClass + * the expected class of the target value . + * @param errorHolder + * will be appended with an error message. + * @return true if the key is not found in the map, or if it is found, and + * map a value of the desired class. + */ + public static final boolean checkOptionalParameter( final Map< String, Object > map, final String key, final Class< ? > expectedClass, final StringBuilder errorHolder ) + { + final Object obj = map.get( key ); + if ( null == obj ) + return true; + + if ( !expectedClass.isInstance( obj ) ) + { + errorHolder.append( "Value for parameter " + key + " is not of the right class. Expected " + expectedClass.getName() + ", got " + obj.getClass().getName() + ".\n" ); + return false; + } + return true; + } + + /** + * Check the mandatory presence and the validity of a key in a map, and test + * its value is of the desired class. * * @param map * the map to inspect. @@ -217,17 +269,22 @@ public static final boolean checkParameter( final Map< String, Object > map, fin errorHolder.append( "Parameter " + key + " could not be found in settings map, or is null.\n" ); return false; } - if ( !expectedClass.isInstance( obj ) ) - { - errorHolder.append( "Value for parameter " + key + " is not of the right class. Expected " + expectedClass.getName() + ", got " + obj.getClass().getName() + ".\n" ); - return false; - } - return true; + return checkOptionalParameter( map, key, expectedClass, errorHolder ); } /** * Returns the mapping in a map that is targeted by a list of keys, in the * order given in the list. + * + * @param + * the type of keys in the collection and the map. + * @param + * the type of values in the map. + * @param keys + * the collection of keys. + * @param mapping + * the mapping. + * @return a new list of values. */ public static final < J, K > List< K > getArrayFromMaping( final Collection< J > keys, final Map< J, K > mapping ) { @@ -242,9 +299,13 @@ public static final < J, K > List< K > getArrayFromMaping( final Collection< J > */ /** - * Return the xyz calibration stored in an {@link ImgPlusMetadata} in a + * Returns the xyz calibration stored in an {@link ImgPlusMetadata} in a * 3-elements double array. Calibration is ordered as X, Y, Z. If one axis * is not found, then the calibration for this axis takes the value of 1. + * + * @param img + * the image metadata object. + * @return a new double array. */ public static final double[] getSpatialCalibration( final ImgPlusMetadata img ) { @@ -276,10 +337,15 @@ public static double[] getSpatialCalibration( final ImagePlus imp ) /** * Returns an estimate of the pth percentile of the values in * the values array. Taken from commons-math. + * + * @param values + * the values. + * @param p + * the percentile. + * @return the percentile of the values. */ public static final double getPercentile( final double[] values, final double p ) { - final int size = values.length; if ( ( p > 1 ) || ( p <= 0 ) ) throw new IllegalArgumentException( "invalid quantile value: " + p ); @@ -322,6 +388,22 @@ private static final double[] getRange( final double[] data ) return new double[] { ( max - min ), min, max }; } + /** + * Stores the x, y, z coordinates of the specified spot in the first 3 + * elements of the specified double array. + * + * @param spot + * the spot. + * @param coords + * the array to write coordinates to. + */ + public static final void localize( final Spot spot, final double[] coords ) + { + coords[ 0 ] = spot.getFeature( Spot.POSITION_X ).doubleValue(); + coords[ 1 ] = spot.getFeature( Spot.POSITION_Y ).doubleValue(); + coords[ 2 ] = spot.getFeature( Spot.POSITION_Z ).doubleValue(); + } + /** * Returns the optimal bin number for a histogram of the data given in * array, using the Freedman and Diaconis rule (bin_space = 2*IQR/n^(1/3)). @@ -387,8 +469,12 @@ private static final int[] histogram( final double data[], final int nBins ) } /** - * Return a threshold for the given data, using an Otsu histogram + * Returns a threshold for the given data, using an Otsu histogram * thresholding method. + * + * @param data + * the data. + * @return the Otsu threshold. */ public static final double otsuThreshold( final double[] data ) { @@ -396,8 +482,14 @@ public static final double otsuThreshold( final double[] data ) } /** - * Return a threshold for the given data, using an Otsu histogram + * Returns a threshold for the given data, using an Otsu histogram * thresholding method with a given bin number. + * + * @param data + * the data. + * @param the + * desired number of bins in the histogram. + * @return the Otsu thresold. */ private static final double otsuThreshold( final double[] data, final int nBins ) { @@ -464,9 +556,17 @@ private static final int otsuThresholdIndex( final int[] hist, final int nPoints } /** - * Return a String unit for the given dimension. When suitable, the unit is + * Returns a String unit for the given dimension. When suitable, the unit is * taken from the settings field, which contains the spatial and time units. * Otherwise, default units are used. + * + * @param dimension + * the dimension. + * @param spaceUnits + * the space units. + * @param timeUnits + * the time units. + * @return the units for the specified dimension. */ public static final String getUnitsFor( final Dimension dimension, final String spaceUnits, final String timeUnits ) { @@ -485,6 +585,8 @@ public static final String getUnitsFor( final Dimension dimension, final String return spaceUnits; case AREA: return spaceUnits + "^2"; + case VOLUME: + return spaceUnits + "^3"; case QUALITY: return "quality"; case COST: @@ -706,7 +808,11 @@ public static final Interval getInterval( final ImgPlus< ? > img, final Settings return interval; } - /** Obtains the SciJava {@link Context} in use by ImageJ. */ + /** + * Obtains the SciJava {@link Context} in use by ImageJ. + * + * @return the context. + */ public static Context getContext() { final Context localContext = context; @@ -843,6 +949,8 @@ public static double standardDeviation( final DoubleArray data ) * Returns a string of the name of the image without the extension, with the * full path * + * @param settings + * A {@link Settings} object referencing the image * @return full name of the image without the extension */ public static String getImagePathWithoutExtension( final Settings settings ) diff --git a/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java b/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java index ccd1bac73..aa1af51c0 100644 --- a/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java +++ b/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java @@ -1,3 +1,24 @@ +/*- + * #%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.util; import java.awt.Component; diff --git a/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshCursor.java b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshCursor.java new file mode 100644 index 000000000..aebe3496b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshCursor.java @@ -0,0 +1,244 @@ +/*- + * #%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.util.mesh; + +import fiji.plugin.trackmate.SpotMesh; +import gnu.trove.list.array.TDoubleArrayList; +import net.imglib2.Cursor; +import net.imglib2.RandomAccess; +import net.imglib2.RealInterval; +import net.imglib2.mesh.alg.zslicer.Slice; + +/** + * A {@link Cursor} that iterates over the pixels inside a mesh. + *

      + * It is based on an implementation of the ray casting algorithm, with some + * optimization to avoid querying the mesh for every single pixel. It does its + * best to ensure that the pixels iterated inside a mesh created from a mask are + * exactly the pixels of the original mask, but does not succeed fully (yet). + * + * @author Jean-Yves Tinevez + * + * @param + * the types of the pixels iterated. + */ +public class SpotMeshCursor< T > implements Cursor< T > +{ + + private final double[] cal; + + private final int minX; + + private final int maxX; + + private final int minY; + + private final int maxY; + + private final int minZ; + + private final int maxZ; + + private final RandomAccess< T > ra; + + private final SpotMesh sm; + + private boolean hasNext; + + private int iy; + + private int iz; + + private int ix; + + /** + * List of resolved X positions where we enter / exit the mesh. Set by the + * ray casting algorithm. + */ + private final TDoubleArrayList intersectionXs = new TDoubleArrayList(); + + private Slice slice; + + public SpotMeshCursor( final RandomAccess< T > ra, final SpotMesh sm, final double[] cal ) + { + this.ra = ra; + this.sm = sm; + this.cal = cal; + final RealInterval bb = sm.getBoundingBox(); + this.minX = ( int ) Math.floor( ( bb.realMin( 0 ) + sm.getDoublePosition( 0 ) ) / cal[ 0 ] ); + this.maxX = ( int ) Math.ceil( ( bb.realMax( 0 ) + sm.getDoublePosition( 0 ) ) / cal[ 0 ] ); + this.minY = ( int ) Math.floor( ( bb.realMin( 1 ) + sm.getDoublePosition( 1 ) ) / cal[ 1 ] ); + this.maxY = ( int ) Math.ceil( ( bb.realMax( 1 ) + sm.getDoublePosition( 1 ) ) / cal[ 1 ] ); + this.minZ = ( int ) Math.floor( ( bb.realMin( 2 ) + sm.getDoublePosition( 2 ) ) / cal[ 2 ] ); + this.maxZ = ( int ) Math.ceil( ( bb.realMax( 2 ) + sm.getDoublePosition( 2 ) ) / cal[ 2 ] ); + reset(); + } + + @Override + public void reset() + { + this.ix = maxX; // To force a new ray cast when we call fwd() + this.iy = minY - 1; // Then we will move to minY. + this.iz = minZ; + this.slice = sm.getZSlice( iz, cal[ 0 ], cal[ 2 ] ); + this.hasNext = true; + preFetch(); + } + + @Override + public void fwd() + { + ra.setPosition( ix, 0 ); + ra.setPosition( iy, 1 ); + ra.setPosition( iz, 2 ); + preFetch(); + } + + private void preFetch() + { + hasNext = false; + while ( true ) + { + // Find next position. + ix++; + if ( ix > maxX ) + { + ix = minX; + while ( true ) + { + // Next Y line, we will need to ray cast again. + ix = minX; + iy++; + if ( iy > maxY ) + { + iy = minY; + iz++; + if ( iz > maxZ ) + return; // Finished! + slice = sm.getZSlice( iz, cal[ 0 ], cal[ 2 ] ); + } + if ( slice == null ) + continue; + + // New ray cast, relative to slice center + final double y = iy * cal[ 1 ] - sm.getDoublePosition( 1 ); + slice.xRayCast( y, intersectionXs, cal[ 1 ] ); + + // No intersection? + if ( !intersectionXs.isEmpty() ) + break; + + // No intersection on this line, move to the next. + } + } + // We have found the next position. + + // Is it inside? + final double x = ix * cal[ 0 ] - sm.getDoublePosition( 0 ); + + // Special case: only one intersection. + if ( intersectionXs.size() == 1 ) + { + if ( x == intersectionXs.getQuick( 0 ) ) + { + hasNext = true; + return; + } + else + { + continue; + } + } + + final int i = intersectionXs.binarySearch( x ); + if ( i >= 0 ) + { + // Fall on an intersection exactly. + hasNext = true; + return; + } + final int ip = -( i + 1 ); + // Odd or even? + if ( ip % 2 != 0 ) + { + // Odd. We are inside. + hasNext = true; + return; + } + + // Not inside, move to the next point. + } + } + + @Override + public boolean hasNext() + { + return hasNext; + } + + @Override + public void jumpFwd( final long steps ) + { + for ( int i = 0; i < steps; i++ ) + fwd(); + } + + @Override + public T next() + { + fwd(); + return get(); + } + + @Override + public long getLongPosition( final int d ) + { + return ra.getLongPosition( d ); + } + + @Override + public Cursor< T > copyCursor() + { + return new SpotMeshCursor<>( + ra.copy(), + sm.copy(), + cal.clone() ); + } + + @Override + public Cursor< T > copy() + { + return copyCursor(); + } + + @Override + public int numDimensions() + { + return 3; + } + + @Override + public T get() + { + return ra.get(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshIterable.java b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshIterable.java new file mode 100644 index 000000000..58bd432c2 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/mesh/SpotMeshIterable.java @@ -0,0 +1,116 @@ +/*- + * #%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.util.mesh; + +import java.util.Iterator; + +import fiji.plugin.trackmate.SpotMesh; +import net.imglib2.Cursor; +import net.imglib2.IterableInterval; +import net.imglib2.Localizable; +import net.imglib2.RandomAccessible; + +public class SpotMeshIterable< T > implements IterableInterval< T >, Localizable +{ + + private final double[] calibration; + + private final RandomAccessible< T > img; + + private final SpotMesh sm; + + public SpotMeshIterable( + final RandomAccessible< T > img, + final SpotMesh sm, + final double[] calibration ) + { + this.img = img; + this.sm = sm; + this.calibration = calibration; + } + + @Override + public int numDimensions() + { + return 3; + } + + @Override + public long getLongPosition( final int d ) + { + return Math.round( sm.getDoublePosition( d ) / calibration[ d ] ); + } + + @Override + public long size() + { + // Costly! + long size = 0; + for ( @SuppressWarnings( "unused" ) + final T t : this ) + size++; + + return size; + } + + @Override + public T firstElement() + { + return cursor().next(); + } + + @Override + public Object iterationOrder() + { + return this; + } + + @Override + public Iterator< T > iterator() + { + return cursor(); + } + + @Override + public long min( final int d ) + { + return Math.round( ( sm.getBoundingBox().realMin( d ) + sm.getFloatPosition( d ) ) / calibration[ d ] ); + } + + @Override + public long max( final int d ) + { + return Math.round( ( sm.getBoundingBox().realMax( d ) + sm.getFloatPosition( d ) ) / calibration[ d ] ); + } + + @Override + public Cursor< T > cursor() + { + return new SpotMeshCursor<>( img.randomAccess(), sm, calibration ); + } + + @Override + public Cursor< T > localizingCursor() + { + return cursor(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java b/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java index 488e329a6..591afb6c3 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/TrackMateModelView.java @@ -51,11 +51,16 @@ public interface TrackMateModelView /** * Centers the view on the given spot. + * + * @param spot + * the spot to center the view on. */ public void centerViewOn( final Spot spot ); /** * Returns the model displayed in this view. + * + * @return the model. */ public Model getModel(); diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVUtils.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVUtils.java new file mode 100644 index 000000000..cea8ef42b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/BVVUtils.java @@ -0,0 +1,144 @@ +/*- + * #%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.visualization.bvv; + +import bvv.vistools.Bvv; +import bvv.vistools.BvvFunctions; +import bvv.vistools.BvvHandle; +import bvv.vistools.BvvSource; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.util.TMUtils; +import ij.CompositeImage; +import ij.ImagePlus; +import ij.process.ImageProcessor; +import ij.process.LUT; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.mesh.util.Icosahedron; +import net.imglib2.mesh.view.TranslateMesh; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.ARGBType; + +public class BVVUtils +{ + + public static final StupidMesh createMesh( final Spot spot ) + { + if ( spot instanceof SpotMesh ) + { + final SpotMesh sm = ( SpotMesh ) spot; + final Mesh mesh = TranslateMesh.translate( sm.getMesh(), spot ); + final BufferMesh bm = new BufferMesh( mesh.vertices().size(), mesh.triangles().size() ); + Meshes.copy( mesh, bm ); + return new StupidMesh( bm ); + } + return new StupidMesh( Icosahedron.sphere( spot, spot.getFeature( Spot.RADIUS ).doubleValue() ) ); + } + + public static final < T extends Type< T > > BvvHandle createViewer( final ImagePlus imp ) + { + final double[] cal = TMUtils.getSpatialCalibration( imp ); + + // Convert and split by channels. + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final int cAxis = img.dimensionIndex( Axes.CHANNEL ); + final BvvHandle bvvHandle; + if ( cAxis < 0 ) + { + final BvvSource source = BvvFunctions.show( img, imp.getShortTitle(), + Bvv.options() + .maxAllowedStepInVoxels( 0 ) + .renderWidth( 1024 ) + .renderHeight( 1024 ) + .preferredSize( 512, 512 ) + .frameTitle( "3D view " + imp.getShortTitle() ) + .sourceTransform( cal ) ); + source.setDisplayRange( imp.getDisplayRangeMin(), imp.getDisplayRangeMax() ); + if ( imp.getLuts().length > 0 ) + { + final LUT lut = imp.getLuts()[ 0 ]; + final int rgb = lut.getColorModel().getRGB( ( int ) imp.getDisplayRangeMax() ); + source.setColor( new ARGBType( rgb ) ); + } + bvvHandle = source.getBvvHandle(); + } + else + { + BvvHandle h = null; + final long nChannels = img.dimension( cAxis ); + final String st = imp.getShortTitle(); + for ( int c = 0; c < nChannels; c++ ) + { + final ImgPlus< T > channel = ImgPlusViews.hyperSlice( img, cAxis, c ); + final BvvSource source; + if ( h == null ) + { + source = BvvFunctions.show( channel, st + "_c" + ( c + 1 ), + Bvv.options() + .maxAllowedStepInVoxels( 0 ) + .renderWidth( 1024 ) + .renderHeight( 1024 ) + .preferredSize( 512, 512 ) + .frameTitle( "3D view " + imp.getShortTitle() ) + .sourceTransform( cal ) ); + h = source.getBvvHandle(); + } + else + { + source = BvvFunctions.show( channel, st + "_c" + ( c + 1 ), + Bvv.options() + .maxAllowedStepInVoxels( 0 ) + .renderWidth( 1024 ) + .renderHeight( 1024 ) + .preferredSize( 512, 512 ) + .sourceTransform( cal ) + .addTo( h ) ); + + } + final int i = imp.getStackIndex( c + 1, 1, 1 ); + if ( imp instanceof CompositeImage ) + { + final CompositeImage cp = ( CompositeImage ) imp; + source.setDisplayRange( cp.getChannelLut( c + 1 ).min, cp.getChannelLut( c + 1 ).max ); + } + else + { + final ImageProcessor ip = imp.getStack().getProcessor( i ); + source.setDisplayRange( ip.getMin(), ip.getMax() ); + } + if ( imp.getLuts().length > 0 ) + { + final LUT lut = imp.getLuts()[ c ]; + final int rgb = lut.getColorModel().getRGB( ( int ) imp.getDisplayRangeMax() ); + source.setColor( new ARGBType( rgb ) ); + } + } + bvvHandle = h; + } + return bvvHandle; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/Icosahedron.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/Icosahedron.java new file mode 100644 index 000000000..1b19c3a4b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/Icosahedron.java @@ -0,0 +1,154 @@ +package fiji.plugin.trackmate.visualization.bvv; + +import fiji.plugin.trackmate.Spot; +import net.imglib2.RealLocalizable; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.Triangle; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.mesh.impl.nio.BufferMesh; + +/** + * Icosahedron spheres. + *

      + * Based on https://github.com/caosdoar/spheres + * + * @author Jean-Yves Tinevez + */ +public class Icosahedron +{ + + public static final Mesh core() + { + final NaiveDoubleMesh mesh = new NaiveDoubleMesh(); + + // Vertices + final double t = ( 1. + Math.sqrt( 5. ) ) / 2.; + final double[][] vs = new double[][] { + { -1.0, t, 0.0 }, + { 1.0, t, 0.0 }, + { -1.0, -t, 0.0 }, + { 1.0, -t, 0.0 }, + { 0.0, -1.0, t }, + { 0.0, 1.0, t }, + { 0.0, -1.0, -t }, + { 0.0, 1.0, -t }, + { t, 0.0, -1.0 }, + { t, 0.0, 1.0 }, + { -t, 0.0, -1.0 }, + { -t, 0.0, 1.0 } + }; + final double[] tmp = new double[ 3 ]; + for ( final double[] v : vs ) + { + normalize( v, tmp ); + mesh.vertices().add( tmp[ 0 ], tmp[ 1 ], tmp[ 2 ] ); + } + + // Faces + mesh.triangles().add( 0, 11, 5 ); + mesh.triangles().add( 0, 5, 1 ); + mesh.triangles().add( 0, 1, 7 ); + mesh.triangles().add( 0, 7, 10 ); + mesh.triangles().add( 0, 10, 11 ); + mesh.triangles().add( 1, 5, 9 ); + mesh.triangles().add( 5, 11, 4 ); + mesh.triangles().add( 11, 10, 2 ); + mesh.triangles().add( 10, 7, 6 ); + mesh.triangles().add( 7, 1, 8 ); + mesh.triangles().add( 3, 9, 4 ); + mesh.triangles().add( 3, 4, 2 ); + mesh.triangles().add( 3, 2, 6 ); + mesh.triangles().add( 3, 6, 8 ); + mesh.triangles().add( 3, 8, 9 ); + mesh.triangles().add( 4, 9, 5 ); + mesh.triangles().add( 2, 4, 11 ); + mesh.triangles().add( 6, 2, 10 ); + mesh.triangles().add( 8, 6, 7 ); + mesh.triangles().add( 9, 8, 1 ); + return mesh; + } + + public static final BufferMesh refine( final Mesh core ) + { + final int nVerticesOut = 6 * core.triangles().size(); + final int nTrianglesOut = 4 * core.triangles().size(); + final BufferMesh out = new BufferMesh( nVerticesOut, nTrianglesOut ); + + final double[] tmpIn = new double[ 3 ]; + final double[] tmpOut = new double[ 3 ]; + for ( final Triangle t : core.triangles() ) + { + final long v0 = out.vertices().add( t.v0x(), t.v0y(), t.v0z() ); + final long v1 = out.vertices().add( t.v1x(), t.v1y(), t.v1z() ); + final long v2 = out.vertices().add( t.v2x(), t.v2y(), t.v2z() ); + + tmpIn[ 0 ] = 0.5 * ( t.v0xf() + t.v1xf() ); + tmpIn[ 1 ] = 0.5 * ( t.v0yf() + t.v1yf() ); + tmpIn[ 2 ] = 0.5 * ( t.v0zf() + t.v1zf() ); + normalize( tmpIn, tmpOut ); + final long v3 = out.vertices().add( tmpOut[ 0 ], tmpOut[ 1 ], tmpOut[ 2 ] ); + + tmpIn[ 0 ] = 0.5 * ( t.v2xf() + t.v1xf() ); + tmpIn[ 1 ] = 0.5 * ( t.v2yf() + t.v1yf() ); + tmpIn[ 2 ] = 0.5 * ( t.v2zf() + t.v1zf() ); + normalize( tmpIn, tmpOut ); + final long v4 = out.vertices().add( tmpOut[ 0 ], tmpOut[ 1 ], tmpOut[ 2 ] ); + + tmpIn[ 0 ] = 0.5 * ( t.v0xf() + t.v2xf() ); + tmpIn[ 1 ] = 0.5 * ( t.v0yf() + t.v2yf() ); + tmpIn[ 2 ] = 0.5 * ( t.v0zf() + t.v2zf() ); + normalize( tmpIn, tmpOut ); + final long v5 = out.vertices().add( tmpOut[ 0 ], tmpOut[ 1 ], tmpOut[ 2 ] ); + + out.triangles().add( v0, v3, v5 ); + out.triangles().add( v3, v1, v4 ); + out.triangles().add( v5, v4, v2 ); + out.triangles().add( v3, v4, v5 ); + } + + return out; + } + + public static BufferMesh sphere( final Spot spot ) + { + return sphere( spot, spot.getFeature( Spot.RADIUS ) ); + } + + public static BufferMesh sphere( final RealLocalizable center, final double radius ) + { + return sphere( center, radius, 3 ); + } + + public static BufferMesh sphere( final RealLocalizable center, final double radius, final int nSubdivisions ) + { + Mesh mesh = core(); + for ( int i = 0; i < nSubdivisions; i++ ) + mesh = refine( mesh ); + + scale( mesh, center, radius ); + final BufferMesh out = new BufferMesh( mesh.vertices().size(), mesh.triangles().size() ); + Meshes.calculateNormals( mesh, out ); + return out; + } + + private static void scale( final Mesh mesh, final RealLocalizable center, final double radius ) + { + final long nV = mesh.vertices().size(); + for ( int i = 0; i < nV; i++ ) + { + final double x = mesh.vertices().x( i ) * radius + center.getDoublePosition( 0 ); + final double y = mesh.vertices().y( i ) * radius + center.getDoublePosition( 1 ); + final double z = mesh.vertices().z( i ) * radius + center.getDoublePosition( 2 ); + mesh.vertices().set( i, x, y, z ); + } + } + + private static void normalize( final double[] v, final double[] tmp ) + { + final double l = Math.sqrt( v[ 0 ] * v[ 0 ] + v[ 1 ] * v[ 1 ] + v[ 2 ] * v[ 2 ] ); + tmp[ 0 ] = v[ 0 ] / l; + tmp[ 1 ] = v[ 1 ] / l; + tmp[ 2 ] = v[ 2 ] / l; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/StupidMesh.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/StupidMesh.java new file mode 100644 index 000000000..1661ca793 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/StupidMesh.java @@ -0,0 +1,149 @@ +/*- + * #%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.visualization.bvv; + +import static com.jogamp.opengl.GL.GL_FLOAT; +import static com.jogamp.opengl.GL.GL_TRIANGLES; +import static com.jogamp.opengl.GL.GL_UNSIGNED_INT; + +import java.awt.Color; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Matrix4fc; + +import com.jogamp.opengl.GL; +import com.jogamp.opengl.GL3; + +import bvv.core.backend.jogl.JoglGpuContext; +import bvv.core.shadergen.DefaultShader; +import bvv.core.shadergen.Shader; +import bvv.core.shadergen.generate.Segment; +import bvv.core.shadergen.generate.SegmentTemplate; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import net.imglib2.mesh.impl.nio.BufferMesh; + +public class StupidMesh +{ + private final Shader prog; + + private final BufferMesh mesh; + + private boolean initialized; + + private int vao; + + private final float[] carr = new float[ 4 ]; + + private final float[] scarr = new float[ 4 ]; + + public StupidMesh( final BufferMesh mesh ) + { + this.mesh = mesh; + final Segment meshVp = new SegmentTemplate( StupidMesh.class, "mesh.vp" ).instantiate(); + final Segment meshFp = new SegmentTemplate( StupidMesh.class, "mesh.fp" ).instantiate(); + prog = new DefaultShader( meshVp.getCode(), meshFp.getCode() ); + DisplaySettings.defaultStyle().getSpotUniformColor().getColorComponents( carr ); + DisplaySettings.defaultStyle().getHighlightColor().getColorComponents( scarr ); + } + + private void init( final GL3 gl ) + { + initialized = true; + + final int[] tmp = new int[ 3 ]; + gl.glGenBuffers( 3, tmp, 0 ); + final int meshPosVbo = tmp[ 0 ]; + final int meshNormalVbo = tmp[ 1 ]; + final int meshEbo = tmp[ 2 ]; + + final FloatBuffer vertices = mesh.vertices().verts(); + vertices.rewind(); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshPosVbo ); + gl.glBufferData( GL.GL_ARRAY_BUFFER, vertices.limit() * Float.BYTES, vertices, GL.GL_STATIC_DRAW ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, 0 ); + + final FloatBuffer normals = mesh.vertices().normals(); + normals.rewind(); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshNormalVbo ); + gl.glBufferData( GL.GL_ARRAY_BUFFER, normals.limit() * Float.BYTES, normals, GL.GL_STATIC_DRAW ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, 0 ); + + final IntBuffer indices = mesh.triangles().indices(); + indices.rewind(); + gl.glBindBuffer( GL.GL_ELEMENT_ARRAY_BUFFER, meshEbo ); + gl.glBufferData( GL.GL_ELEMENT_ARRAY_BUFFER, indices.limit() * Integer.BYTES, indices, GL.GL_STATIC_DRAW ); + gl.glBindBuffer( GL.GL_ELEMENT_ARRAY_BUFFER, 0 ); + + gl.glGenVertexArrays( 1, tmp, 0 ); + vao = tmp[ 0 ]; + gl.glBindVertexArray( vao ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshPosVbo ); + gl.glVertexAttribPointer( 0, 3, GL_FLOAT, false, 3 * Float.BYTES, 0 ); + gl.glEnableVertexAttribArray( 0 ); + gl.glBindBuffer( GL.GL_ARRAY_BUFFER, meshNormalVbo ); + gl.glVertexAttribPointer( 1, 3, GL_FLOAT, false, 3 * Float.BYTES, 0 ); + gl.glEnableVertexAttribArray( 1 ); + gl.glBindBuffer( GL.GL_ELEMENT_ARRAY_BUFFER, meshEbo ); + gl.glBindVertexArray( 0 ); + } + + public void setColor( final Color color, final float alpha ) + { + color.getComponents( carr ); + carr[ 3 ] = alpha; + } + + public void setSelectionColor( final Color selectionColor, final float alpha ) + { + selectionColor.getComponents( scarr ); + scarr[ 3 ] = alpha; + } + + public void draw( final GL3 gl, final Matrix4fc pvm, final Matrix4fc vm, final boolean isSelected ) + { + if ( !initialized ) + init( gl ); + + final JoglGpuContext context = JoglGpuContext.get( gl ); + final Matrix4f itvm = vm.invert( new Matrix4f() ).transpose(); + + prog.getUniformMatrix4f( "pvm" ).set( pvm ); + prog.getUniformMatrix4f( "vm" ).set( vm ); + prog.getUniformMatrix3f( "itvm" ).set( itvm.get3x3( new Matrix3f() ) ); + prog.getUniform4f( "ObjectColor" ).set( carr[ 0 ], carr[ 1 ], carr[ 2 ], carr[ 3 ] ); + prog.getUniform1f( "IsSelected" ).set( isSelected ? 1f : 0f ); + prog.getUniform4f( "SelectionColor" ).set( scarr[ 0 ], scarr[ 1 ], scarr[ 2 ], scarr[ 3 ] ); + prog.setUniforms( context ); + prog.use( context ); + + gl.glBindVertexArray( vao ); + gl.glEnable( GL.GL_CULL_FACE ); + gl.glCullFace( GL.GL_BACK ); + gl.glFrontFace( GL.GL_CCW ); + gl.glDrawElements( GL_TRIANGLES, mesh.triangles().size() * 3, GL_UNSIGNED_INT, 0 ); + gl.glBindVertexArray( 0 ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/bvv/TrackMateBVV.java b/src/main/java/fiji/plugin/trackmate/visualization/bvv/TrackMateBVV.java new file mode 100644 index 000000000..27140275c --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/bvv/TrackMateBVV.java @@ -0,0 +1,210 @@ +/*- + * #%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.visualization.bvv; + +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.joml.Matrix4f; + +import bdv.viewer.animate.TranslationAnimator; +import bvv.core.VolumeViewerPanel; +import bvv.core.util.MatrixMath; +import bvv.vistools.BvvHandle; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.features.FeatureUtils; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.visualization.AbstractTrackMateModelView; +import fiji.plugin.trackmate.visualization.FeatureColorGenerator; +import ij.ImagePlus; +import net.imglib2.RealLocalizable; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.Type; + +public class TrackMateBVV< T extends Type< T > > extends AbstractTrackMateModelView +{ + + private static final String KEY = "BIGVOLUMEVIEWER"; + + private final ImagePlus imp; + + private BvvHandle handle; + + private final Map< Spot, StupidMesh > meshMap; + + public TrackMateBVV( final Model model, final SelectionModel selectionModel, final ImagePlus imp, final DisplaySettings displaySettings ) + { + super( model, selectionModel, displaySettings ); + this.imp = imp; + this.meshMap = new HashMap<>(); + final Iterable< Spot > it = model.getSpots().iterable( true ); + it.forEach( s -> meshMap.computeIfAbsent( s, BVVUtils::createMesh ) ); + updateColor(); + displaySettings.listeners().add( this::updateColor ); + selectionModel.addSelectionChangeListener( e -> refresh() ); + } + + /** + * Returns the {@link BvvHandle} that contains this view. Returns + * null if this view has not been rendered yet. + * + * @return the BVV handle, or null. + */ + public BvvHandle getBvvHandle() + { + return handle; + } + + @Override + public void render() + { + this.handle = BVVUtils.createViewer( imp ); + final VolumeViewerPanel viewer = handle.getViewerPanel(); + viewer.setRenderScene( ( gl, data ) -> { + if ( displaySettings.isSpotVisible() ) + { + final Matrix4f pvm = new Matrix4f( data.getPv() ); + final Matrix4f view = MatrixMath.affine( data.getRenderTransformWorldToScreen(), new Matrix4f() ); + final Matrix4f vm = MatrixMath.screen( data.getDCam(), data.getScreenWidth(), data.getScreenHeight(), new Matrix4f() ).mul( view ); + + final int t = data.getTimepoint(); + final Iterable< Spot > it = model.getSpots().iterable( t, true ); + it.forEach( s -> meshMap.computeIfAbsent( s, BVVUtils::createMesh ).draw( gl, pvm, vm, selectionModel.getSpotSelection().contains( s ) ) ); + } + } ); + } + + @Override + public void refresh() + { + if ( handle != null ) + handle.getViewerPanel().requestRepaint(); + } + + @Override + public void clear() + { + // TODO Auto-generated method stub + + } + + @Override + public void centerViewOn( final Spot spot ) + { + if ( handle == null ) + return; + + final VolumeViewerPanel panel = handle.getViewerPanel(); + panel.setTimepoint( spot.getFeature( Spot.FRAME ).intValue() ); + + final AffineTransform3D c = panel.state().getViewerTransform(); + final double[] translation = getTranslation( c, spot, panel.getWidth(), panel.getHeight() ); + if ( translation != null ) + { + final TranslationAnimator animator = new TranslationAnimator( c, translation, 300 ); + animator.setTime( System.currentTimeMillis() ); + panel.setTransformAnimator( animator ); + } + } + + /** + * Returns a translation vector that will put the specified position at the + * center of the panel when used with a TranslationAnimator. + * + * @param t + * the viewer panel current view transform. + * @param target + * the position to focus on. + * @param width + * the width of the panel. + * @param height + * the height of the panel. + * @return a new double[] array with 3 elements containing the + * translation to use. + */ + private static final double[] getTranslation( final AffineTransform3D t, final RealLocalizable target, final int width, final int height ) + { + final double[] pos = new double[ 3 ]; + final double[] vPos = new double[ 3 ]; + target.localize( pos ); + t.apply( pos, vPos ); + + final double dx = width / 2 - vPos[ 0 ] + t.get( 0, 3 ); + final double dy = height / 2 - vPos[ 1 ] + t.get( 1, 3 ); + final double dz = -vPos[ 2 ] + t.get( 2, 3 ); + + return new double[] { dx, dy, dz }; + } + + @Override + public String getKey() + { + return KEY; + } + + @Override + public void modelChanged( final ModelChangeEvent event ) + { + switch ( event.getEventID() ) + { + case ModelChangeEvent.SPOTS_FILTERED: + case ModelChangeEvent.SPOTS_COMPUTED: + case ModelChangeEvent.TRACKS_VISIBILITY_CHANGED: + case ModelChangeEvent.TRACKS_COMPUTED: + refresh(); + break; + case ModelChangeEvent.MODEL_MODIFIED: + { + for ( final Spot spot : event.getSpots() ) + { + final StupidMesh mesh = BVVUtils.createMesh( spot ); + meshMap.put( spot, mesh ); + } + updateColor(); + refresh(); + break; + } + } + } + + private void updateColor() + { + final FeatureColorGenerator< Spot > spotColorGenerator = FeatureUtils.createSpotColorGenerator( model, displaySettings ); + for ( final Entry< Spot, StupidMesh > entry : meshMap.entrySet() ) + { + final StupidMesh sm = entry.getValue(); + if ( sm == null ) + continue; + + final Color color = spotColorGenerator.color( entry.getKey() ); + final float alpha = ( float ) displaySettings.getSpotTransparencyAlpha(); + sm.setColor( color, alpha ); + sm.setSelectionColor( displaySettings.getHighlightColor(), alpha ); + } + refresh(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java index 0400ace50..ed5c9bb48 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/HyperStackDisplayer.java @@ -77,7 +77,7 @@ public HyperStackDisplayer( final Model model, final SelectionModel selectionMod * the spots. * * @param displaySettings - * + * the display settings. * @return the spot overlay */ protected SpotOverlay createSpotOverlay( final DisplaySettings displaySettings ) @@ -90,7 +90,7 @@ protected SpotOverlay createSpotOverlay( final DisplaySettings displaySettings ) * the spots. * * @param displaySettings - * + * the display settings. * @return the track overlay */ protected TrackOverlay createTrackOverlay( final DisplaySettings displaySettings ) diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.java index 671cd839a..2c49fa8f3 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/ModelEditActions.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,7 +38,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.detection.semiauto.SemiAutoTracker; import fiji.plugin.trackmate.util.ModelTools; import fiji.plugin.trackmate.util.TMUtils; @@ -97,7 +97,7 @@ private Spot makeSpot( Point mouseLocation ) SwingUtilities.convertPointFromScreen( mouseLocation, canvas ); } final double[] calibration = TMUtils.getSpatialCalibration( imp ); - return new Spot( + return new SpotBase( ( -0.5 + canvas.offScreenXD( mouseLocation.x ) ) * calibration[ 0 ], ( -0.5 + canvas.offScreenYD( mouseLocation.y ) ) * calibration[ 1 ], ( imp.getSlice() - 1 ) * calibration[ 2 ], @@ -279,7 +279,7 @@ public void changeSpotRadius( final boolean increase, final boolean fast ) return; final double radius = target.getFeature( Spot.RADIUS ); - final int factor = ( increase ) ? 1 : -1; + final int factor = ( increase ) ? -1 : 1; final double dx = imp.getCalibration().pixelWidth; final double newRadius = ( fast ) @@ -292,17 +292,11 @@ public void changeSpotRadius( final boolean increase, final boolean fast ) // Store new value of radius for next spot creation. previousRadius = newRadius; - final SpotRoi roi = target.getRoi(); - if ( null == roi ) - { - target.putFeature( Spot.RADIUS, newRadius ); - } - else - { - final double alpha = newRadius / radius; - roi.scale( alpha ); - target.putFeature( Spot.RADIUS, roi.radius() ); - } + // Actually scale the spot. + target.scale( radius / newRadius ); + + // Scale spot + target.putFeature( Spot.RADIUS, newRadius ); model.beginUpdate(); try diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotMesh.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotMesh.java new file mode 100644 index 000000000..37d6d446d --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotMesh.java @@ -0,0 +1,159 @@ +/*- + * #%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.visualization.hyperstack; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Area; +import java.awt.geom.Path2D; +import java.util.function.DoubleUnaryOperator; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import ij.ImagePlus; +import net.imglib2.RealInterval; +import net.imglib2.RealLocalizable; +import net.imglib2.mesh.alg.zslicer.Contour; +import net.imglib2.mesh.alg.zslicer.Slice; + +/** + * Utility class to paint the {@link SpotMesh} component of spots. + * + * @author Jean-Yves Tinevez + * + */ +public class PaintSpotMesh extends TrackMatePainter< SpotMesh > +{ + + private final Path2D.Double polygon; + + private final Area shape; + + public PaintSpotMesh( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + super( imp, calibration, displaySettings ); + this.polygon = new Path2D.Double(); + this.shape = new Area(); + + } + + @Override + public int paint( final Graphics2D g2d, final SpotMesh spot ) + { + final RealInterval bb = spot.getBoundingBox(); + if ( !intersect( bb, spot ) ) + return -1; + + // Z plane does not cross bounding box. + final double x = spot.getFeature( Spot.POSITION_X ); + final double y = spot.getFeature( Spot.POSITION_Y ); + final double xs = toScreenX( x ); + final double ys = toScreenY( y ); + final double z = spot.getFeature( Spot.POSITION_Z ); + final int zSlice = imp.getSlice() - 1; + final double dz = zSlice * calibration[ 2 ]; + if ( bb.realMin( 2 ) + z > dz || bb.realMax( 2 ) + z < dz ) + { + paintOutOfFocus( g2d, xs, ys ); + return -1; + } + + // Convert to AWT shape. Only work in non-pathological cases, and + // because contours are sorted by decreasing area. + final Slice slice = spot.getZSlice( zSlice, calibration[ 0 ], calibration[ 2 ] ); + if ( slice == null ) + { + paintOutOfFocus( g2d, xs, ys ); + return -1; + } + + + if ( displaySettings.isSpotFilled() ) + { + // Should not be null. + shape.reset(); + for ( final Contour c : slice ) + { + toPolygon( spot, c, polygon, this::toScreenX, this::toScreenY ); + if ( c.isInterior() ) + shape.add( new Area( polygon ) ); + else + shape.subtract( new Area( polygon ) ); + } + g2d.fill( shape ); + g2d.setColor( Color.BLACK ); + g2d.draw( shape ); + } + else + { + for ( final Contour c : slice ) + { + toPolygon( spot, c, polygon, this::toScreenX, this::toScreenY ); + g2d.draw( polygon ); + } + } + final Rectangle bounds = shape.getBounds(); + final int maxTextPos = bounds.x + bounds.width; + return ( int ) ( maxTextPos - xs ); + } + + /** + * Maps the coordinates of this contour to a Path2D polygon, and return the + * max X coordinate of the produced shape. + * + * @param contour + * the contour to convert. + * @param polygon + * the polygon to write. Reset by this call. + * @param toScreenX + * a function to convert the X coordinate of this contour to + * screen coordinates. + * @param toScreenY + * a function to convert the Y coordinate of this contour to + * screen coordinates. + * @return the max X position in screen units of this shape. + */ + private static final double toPolygon( final RealLocalizable center, final Contour contour, final Path2D polygon, final DoubleUnaryOperator toScreenX, final DoubleUnaryOperator toScreenY ) + { + double maxTextPos = Double.NEGATIVE_INFINITY; + polygon.reset(); + final double x0 = toScreenX.applyAsDouble( contour.x( 0 ) + center.getDoublePosition( 0 ) ); + final double y0 = toScreenY.applyAsDouble( contour.y( 0 ) + center.getDoublePosition( 1 ) ); + polygon.moveTo( x0, y0 ); + if ( x0 > maxTextPos ) + maxTextPos = x0; + + for ( int i = 1; i < contour.size(); i++ ) + { + final double xi = toScreenX.applyAsDouble( contour.x( i ) + center.getDoublePosition( 0 ) ); + final double yi = toScreenY.applyAsDouble( contour.y( i ) + center.getDoublePosition( 1 ) ); + polygon.lineTo( xi, yi ); + + if ( xi > maxTextPos ) + maxTextPos = xi; + } + polygon.closePath(); + return maxTextPos; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotRoi.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotRoi.java new file mode 100644 index 000000000..263540812 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotRoi.java @@ -0,0 +1,135 @@ +/*- + * #%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.visualization.hyperstack; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Path2D; +import java.util.function.DoubleUnaryOperator; + +import fiji.plugin.trackmate.SpotRoi; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import gnu.trove.list.TDoubleList; +import ij.ImagePlus; + +/** + * Utility class to paint the {@link SpotRoi} component of spots. + * + * @author Jean-Yves Tinevez + * + */ +public class PaintSpotRoi extends TrackMatePainter< SpotRoi > +{ + + private final java.awt.geom.Path2D.Double polygon; + + public PaintSpotRoi( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + super( imp, calibration, displaySettings ); + this.polygon = new Path2D.Double(); + } + + /** + * Paint the specified spot using its {@link SpotRoi} field. The latter must + * not be null. + * + * @param g2d + * the graphics object, configured to paint the spot with. + * @param spot + * the spot to paint. + * @return the text position X indent in pixels to use to paint a string + * next to the painted contour. + */ + @Override + public int paint( final Graphics2D g2d, final SpotRoi spot ) + { + if ( !intersect( spot ) ) + return -1; + + final double maxTextPos = toPolygon( spot, polygon, this::toScreenX, this::toScreenY ); + if ( displaySettings.isSpotFilled() ) + { + g2d.fill( polygon ); + g2d.setColor( Color.BLACK ); + g2d.draw( polygon ); + } + else + { + g2d.draw( polygon ); + } + + final double xs = toScreenX( spot.getDoublePosition( 0 ) ); + final int textPos = ( int ) ( maxTextPos - xs ); + return textPos; + } + + static final double max( final TDoubleList l ) + { + double max = Double.NEGATIVE_INFINITY; + for ( int i = 0; i < l.size(); i++ ) + { + final double v = l.get( i ); + if ( v > max ) + max = v; + } + return max; + } + + /** + * Maps the coordinates of this contour to a Path2D polygon, and return the + * max X coordinate of the produced shape. + * + * @param contour + * the contour to convert. + * @param polygon + * the polygon to write. Reset by this call. + * @param toScreenX + * a function to convert the X coordinate of this contour to + * screen coordinates. + * @param toScreenY + * a function to convert the Y coordinate of this contour to + * screen coordinates. + * @return the max X position in screen units of this shape. + */ + private static final double toPolygon( final SpotRoi roi, final Path2D polygon, final DoubleUnaryOperator toScreenX, final DoubleUnaryOperator toScreenY ) + { + double maxTextPos = Double.NEGATIVE_INFINITY; + polygon.reset(); + final double x0 = toScreenX.applyAsDouble( roi.x( 0 ) ); + final double y0 = toScreenY.applyAsDouble( roi.y( 0 ) ); + polygon.moveTo( x0, y0 ); + if ( x0 > maxTextPos ) + maxTextPos = x0; + + for ( int i = 1; i < roi.nPoints(); i++ ) + { + final double xi = toScreenX.applyAsDouble( roi.x( i ) ); + final double yi = toScreenY.applyAsDouble( roi.y( i ) ); + polygon.lineTo( xi, yi ); + + if ( xi > maxTextPos ) + maxTextPos = xi; + } + polygon.closePath(); + return maxTextPos; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotSphere.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotSphere.java new file mode 100644 index 000000000..cc62851c0 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/PaintSpotSphere.java @@ -0,0 +1,95 @@ +/*- + * #%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.visualization.hyperstack; + +import java.awt.Graphics2D; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import ij.ImagePlus; +import net.imglib2.RealInterval; +import net.imglib2.util.Intervals; + +/** + * Utility class to paint the spots as little spheres. + * + * @author Jean-Yves Tinevez + * + */ +public class PaintSpotSphere extends TrackMatePainter< SpotBase > +{ + + public PaintSpotSphere( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + super( imp, calibration, displaySettings ); + } + + @Override + public int paint( final Graphics2D g2d, final SpotBase spot ) + { + if ( !intersect( boundingBox( spot ), spot ) ) + return -1; + + final double x = spot.getFeature( Spot.POSITION_X ); + final double y = spot.getFeature( Spot.POSITION_Y ); + final double z = spot.getFeature( Spot.POSITION_Z ); + final double zslice = ( imp.getSlice() - 1 ) * calibration[ 2 ]; + final double dz = zslice - z; + final double dz2 = dz * dz; + final double radiusRatio = displaySettings.getSpotDisplayRadius(); + final double radius = spot.getFeature( Spot.RADIUS ) * radiusRatio; + + final double xs = toScreenX( x ); + final double ys = toScreenY( y ); + final double magnification = getMagnification(); + + if ( dz2 >= radius * radius ) + { + paintOutOfFocus( g2d, xs, ys ); + return -1; // Do not paint spot name. + } + + final double apparentRadius = Math.sqrt( radius * radius - dz2 ) / calibration[ 0 ] * magnification; + if ( displaySettings.isSpotFilled() ) + g2d.fillOval( + ( int ) Math.round( xs - apparentRadius ), + ( int ) Math.round( ys - apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ) ); + else + g2d.drawOval( + ( int ) Math.round( xs - apparentRadius ), + ( int ) Math.round( ys - apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ), + ( int ) Math.round( 2 * apparentRadius ) ); + + final int textPos = ( int ) apparentRadius; + return textPos; + } + + private static final RealInterval boundingBox( final Spot spot ) + { + final double r = spot.getFeature( Spot.RADIUS ).doubleValue(); + return Intervals.createMinMaxReal( -r, -r, r, r ); + } +} 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 d091862d2..4ee2de83d 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java @@ -116,8 +116,10 @@ public void imageClosed( final ImagePlus imp ) } /** - * Return the singleton instance for this tool. If it was not previously + * Returns the singleton instance for this tool. If it was not previously * instantiated, this calls instantiates it. + * + * @return the instance. */ public static SpotEditTool getInstance() { @@ -128,7 +130,11 @@ public static SpotEditTool getInstance() } /** - * Return true if the tool is currently present in ImageJ toolbar. + * Returns true if the tool is currently present in ImageJ + * toolbar. + * + * @return true if the tool is currently present in ImageJ + * toolbar. */ public static boolean isLaunched() { @@ -159,17 +165,17 @@ protected void registerTool( final ImageCanvas canvas ) { /* * Double check! Since TrackMate v7 there the following bug: - * + * * Sometimes the listeners of this tool get added to the target image * canvas TWICE. This causes an unspeakable mess where all events are * triggered twice for e.g. a single click. For instance you cannot * shift-click on a spot to add it to the selection, because the event * is fired TWICE, which results in the spot being de-selected * immediately after being selected. - * + * * But the double registration seems to happen randomly. Sometimes the * listeners are added only once, *sometimes* (more often) twice. - * + * * To work around this mess, we overload the registerTool(ImageCanvas) * method and skip the registration if we find that the mouse listener * has already been added to the canvas. It fixes the issue, regardless @@ -187,8 +193,11 @@ protected void registerTool( final ImageCanvas canvas ) } /** - * Register the given {@link HyperStackDisplayer}. If this method id not + * Registers the given {@link HyperStackDisplayer}. If this method is not * called, the tool will not respond. + * + * @param displayer + * the displayer to register. */ public void register( final HyperStackDisplayer displayer ) { @@ -402,7 +411,6 @@ public void keyPressed( final KeyEvent e ) break; } } - } @Override diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java index 041b12bcd..9b20ef711 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotOverlay.java @@ -32,15 +32,14 @@ import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.AffineTransform; -import java.awt.geom.Path2D; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; import fiji.plugin.trackmate.SpotRoi; import fiji.plugin.trackmate.features.FeatureUtils; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -73,6 +72,12 @@ public class SpotOverlay extends Roi protected final Model model; + private final PaintSpotRoi paintSpotRoi; + + private final PaintSpotSphere paintSpotSphere; + + private final PaintSpotMesh paintSpotMesh; + /* * CONSTRUCTOR */ @@ -84,6 +89,9 @@ public SpotOverlay( final Model model, final ImagePlus imp, final DisplaySetting this.imp = imp; this.calibration = TMUtils.getSpatialCalibration( imp ); this.displaySettings = displaySettings; + this.paintSpotSphere = new PaintSpotSphere( imp, calibration, displaySettings ); + this.paintSpotRoi = new PaintSpotRoi( imp, calibration, displaySettings ); + this.paintSpotMesh = new PaintSpotMesh( imp, calibration, displaySettings ); } /* @@ -132,7 +140,7 @@ public void drawOverlay( final Graphics g ) g2d.setStroke( new BasicStroke( ( float ) displaySettings.getLineThickness() ) ); - if ( selectionOnly && null != spotSelection) + if ( selectionOnly && null != spotSelection ) { // Track display mode only displays selection. for ( final Spot spot : spotSelection ) @@ -229,10 +237,6 @@ public void drawOverlay( final Graphics g ) g2d.setFont( originalFont ); } - /** - * @param g2d - * @param frame - */ protected void drawExtraLayer( final Graphics2D g2d, final int frame ) {} @@ -243,86 +247,60 @@ public void setSpotSelection( final Collection< Spot > spots ) protected void drawSpot( final Graphics2D g2d, final Spot spot, final double zslice, final int xcorner, final int ycorner, final double magnification, final boolean filled ) { + // Spot center in pixel coords. final double x = spot.getFeature( Spot.POSITION_X ); final double y = spot.getFeature( Spot.POSITION_Y ); - final double z = spot.getFeature( Spot.POSITION_Z ); - final double dz2 = ( z - zslice ) * ( z - zslice ); - final double radiusRatio = displaySettings.getSpotDisplayRadius(); - final double radius = spot.getFeature( Spot.RADIUS ) * radiusRatio; - // In pixel units + // Pixel coords. final double xp = x / calibration[ 0 ] + 0.5f; final double yp = y / calibration[ 1 ] + 0.5f; - // so that spot centers are displayed on the pixel centers. - - // Scale to image zoom + // 0.5, so that spot centers are displayed on the pixel centers. + // Display window coordinates. final double xs = ( xp - xcorner ) * magnification; final double ys = ( yp - ycorner ) * magnification; - if ( dz2 >= radius * radius ) - { - g2d.fillOval( ( int ) Math.round( xs - 2 * magnification ), ( int ) Math.round( ys - 2 * magnification ), ( int ) Math.round( 4 * magnification ), ( int ) Math.round( 4 * magnification ) ); - return; - } + // Get a painter adequate for the spot and config we have. + @SuppressWarnings( "rawtypes" ) + final TrackMatePainter painter = getPainter( spot ); + @SuppressWarnings( "unchecked" ) + final int textPos = painter.paint( g2d, spot ); - final SpotRoi roi = spot.getRoi(); - if ( !displaySettings.isSpotDisplayedAsRoi() || roi == null || roi.x.length < 2 ) + if ( textPos >= 0 && displaySettings.isSpotShowName() ) { - final double apparentRadius = Math.sqrt( radius * radius - dz2 ) / calibration[ 0 ] * magnification; - final int textPos = ( int ) apparentRadius; - if ( displaySettings.isSpotShowName() ) - drawSpotName( g2d, spot, xs, ys, textPos ); - if ( filled ) - g2d.fillOval( - ( int ) Math.round( xs - apparentRadius ), - ( int ) Math.round( ys - apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ) ); - else - g2d.drawOval( - ( int ) Math.round( xs - apparentRadius ), - ( int ) Math.round( ys - apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ), - ( int ) Math.round( 2 * apparentRadius ) ); - } - else - { - final double[] polygonX = roi.toPolygonX( calibration[ 0 ], xcorner - 0.5, x, magnification ); - final double[] polygonY = roi.toPolygonY( calibration[ 1 ], ycorner - 0.5, y, magnification ); - // The 0.5 is here so that we plot vertices at pixel centers. - final Path2D polygon = new Path2D.Double(); - polygon.moveTo( polygonX[ 0 ], polygonY[ 0 ] ); - for ( int i = 1; i < polygonX.length; ++i ) - polygon.lineTo( polygonX[ i ], polygonY[ i ] ); - polygon.closePath(); - final int textPos = ( int ) ( Arrays.stream( polygonX ).max().getAsDouble() - xs ); - - if ( filled ) - { - if ( displaySettings.isSpotShowName() ) - drawSpotName( g2d, spot, xs, ys, textPos ); - g2d.fill( polygon ); - g2d.setColor( Color.BLACK ); - g2d.draw( polygon ); - } - else - { - if ( displaySettings.isSpotShowName() ) - drawSpotName( g2d, spot, xs, ys, textPos ); - g2d.draw( polygon ); - } + final int windowWidth = imp.getWindow().getWidth(); + drawString( g2d, fm, windowWidth, spot.toString(), xs, ys, textPos ); } } - private final void drawSpotName( final Graphics2D g2d, final Spot spot, final double xs, final double ys, final int textPos ) + private TrackMatePainter< ? extends Spot > getPainter( final Spot spot ) + { + if ( !displaySettings.isSpotDisplayedAsRoi() ) + return paintSpotSphere; + + if ( spot instanceof SpotRoi ) + return paintSpotRoi; + + if ( spot instanceof SpotMesh ) + return paintSpotMesh; + + return paintSpotSphere; + } + + private static final void drawString( + final Graphics2D g2d, + final FontMetrics fm, + final int windowWidth, + final String str, + final double xs, + final double ys, + final int textPos ) { - final String str = spot.toString(); final int xindent = fm.stringWidth( str ); int xtext = ( int ) ( xs + textPos + 5 ); - if ( xtext + xindent > imp.getWindow().getWidth() ) + if ( xtext + xindent > windowWidth ) xtext = ( int ) ( xs - textPos - 5 - xindent ); final int yindent = fm.getAscent() / 2; final int ytext = ( int ) ys + yindent; - g2d.drawString( spot.toString(), xtext, ytext ); + g2d.drawString( str, xtext, ytext ); } } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackMatePainter.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackMatePainter.java new file mode 100644 index 000000000..95be2bdf8 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackMatePainter.java @@ -0,0 +1,156 @@ +/*- + * #%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.visualization.hyperstack; + +import java.awt.Graphics2D; + +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import ij.ImagePlus; +import ij.gui.ImageCanvas; +import net.imglib2.RealInterval; +import net.imglib2.RealLocalizable; + +public abstract class TrackMatePainter< T extends Spot > +{ + + protected final double[] calibration; + + protected final DisplaySettings displaySettings; + + protected final ImagePlus imp; + + public TrackMatePainter( final ImagePlus imp, final double[] calibration, final DisplaySettings displaySettings ) + { + this.imp = imp; + this.calibration = calibration; + this.displaySettings = displaySettings; + } + + public abstract int paint( final Graphics2D g2d, final T spot ); + + /** + * Returns true if the specified bounding-box, shifted by the + * Specified amount, intersects with the display window. + * + * @param boundingBox + * the bounding box, centered at (0,0), in physical coordinates. + * @param center + * the center of the bounding-box, in physical coordinates. + * @return if the specified bounding-box intersects with the display window. + */ + protected boolean intersect( final RealInterval boundingBox, final RealLocalizable center ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return false; + + if ( toScreenX( boundingBox.realMin( 0 ) + center.getDoublePosition( 0 ) ) > canvas.getWidth() ) + return false; + if ( toScreenX( boundingBox.realMax( 0 ) + center.getDoublePosition( 0 ) ) < 0 ) + return false; + if ( toScreenY( boundingBox.realMin( 1 ) + center.getDoublePosition( 1 ) ) > canvas.getHeight() ) + return false; + if ( toScreenY( boundingBox.realMax( 1 ) + center.getDoublePosition( 1 ) ) < 0 ) + return false; + return true; + } + + /** + * Returns true if the specified bounding-box intersects with + * the display window. + * + * @param boundingBox + * the bounding box, in physical coordinates. + * @return true if the specified bounding-box intersects with + * the display window. + */ + protected boolean intersect( final RealInterval boundingBox ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return false; + + if ( toScreenX( boundingBox.realMin( 0 ) ) > canvas.getWidth() ) + return false; + if ( toScreenX( boundingBox.realMax( 0 ) ) < 0 ) + return false; + if ( toScreenY( boundingBox.realMin( 1 ) ) > canvas.getHeight() ) + return false; + if ( toScreenY( boundingBox.realMax( 1 ) ) < 0 ) + return false; + return true; + } + + /** + * Converts a X position in physical units (possible um) to screen + * coordinates to be used with the graphics object. + * + * @param x + * the X position to convert. + * @return the screen X coordinate. + */ + protected double toScreenX( final double x ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return Double.NaN; + + final double xp = x / calibration[ 0 ] + 0.5; // pixel coords + return canvas.screenXD( xp ); + } + + /** + * Converts a Y position in physical units (possible um) to screen + * coordinates to be used with the graphics object. + * + * @param y + * the Y position to convert. + * @return the screen Y coordinate. + */ + protected double toScreenY( final double y ) + { + final ImageCanvas canvas = imp.getCanvas(); + if ( canvas == null ) + return Double.NaN; + + final double yp = y / calibration[ 0 ] + 0.5; // pixel coords + return canvas.screenYD( yp ); + } + + protected void paintOutOfFocus( final Graphics2D g2d, final double xs, final double ys ) + { + final double magnification = getMagnification(); + g2d.fillOval( + ( int ) Math.round( xs - 2 * magnification ), + ( int ) Math.round( ys - 2 * magnification ), + ( int ) Math.round( 4 * magnification ), + ( int ) Math.round( 4 * magnification ) ); + } + + protected double getMagnification() + { + if ( imp.getCanvas() == null ) + return 1.; + return imp.getCanvas().getMagnification(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java index 8c737051d..c15e1729c 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SaveAction.java @@ -81,6 +81,13 @@ public SaveAction( final TrackScheme trackScheme ) * Saves XML+PNG format. * * @param frame + * the TrackScheme frame to capture. + * @param filename + * the file to save to. + * @param bg + * the color of the background in the exported image. + * @throws IOException + * if an error happens while writing. */ protected void saveXmlPng( final TrackSchemeFrame frame, final String filename, final Color bg ) throws IOException { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java index a9115544b..7c928259a 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotIconGrabber.java @@ -74,6 +74,7 @@ public SpotIconGrabber( final ImgPlus< T > img ) * a factor that determines the size of the thumbnail. The * thumbnail will have a size equal to the spot diameter times * this radius. + * @return a Base64 representation of the spot image. */ public String getImageString( final Spot spot, final double radiusFactor ) { diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java index 8f18b1893..f092cbb12 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/SpotImageUpdater.java @@ -57,6 +57,8 @@ public SpotImageUpdater( final Settings settings ) * is stored for subsequent calls of this method. So it is a good idea to * group calls to this method for spots that belong to the same frame. * + * @param spot + * the spot. * @param radiusFactor * a factor that determines the size of the thumbnail. The * thumbnail will have a size equal to the spot diameter times diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java index 4f75c9391..7ac53f325 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackScheme.java @@ -198,8 +198,10 @@ public SelectionModel getSelectionModel() } /** - * @return the column index that is the first one after all the track - * columns. + * Returns the column index that is the first one after all the track + * columns. + * + * @return the column index. */ public int getUnlaidSpotColumn() { @@ -207,6 +209,10 @@ public int getUnlaidSpotColumn() } /** + * Returns the first free column for the target row. + * + * @param frame + * the row. * @return the first free column for the target row. */ public int getNextFreeColumn( final int frame ) @@ -221,6 +227,8 @@ public int getNextFreeColumn( final int frame ) /** * Returns the GUI frame controlled by this class. + * + * @return the GUI. */ public TrackSchemeFrame getGUI() { @@ -230,6 +238,8 @@ public TrackSchemeFrame getGUI() /** * Returns the {@link JGraphXAdapter} that serves as a model for the graph * displayed in this frame. + * + * @return the adapter. */ public JGraphXAdapter getGraph() { @@ -238,6 +248,8 @@ public JGraphXAdapter getGraph() /** * Returns the graph layout in charge of arranging the cells on the graph. + * + * @return the graph layout. */ public TrackSchemeGraphLayout getGraphLayout() { @@ -1281,10 +1293,10 @@ public void removeSelectedCells() public void removeSelectedLinkCells() { - List< Object > edgeCells = new ArrayList<>(); - for ( Object obj : graph.getSelectionCells() ) + final List< Object > edgeCells = new ArrayList<>(); + for ( final Object obj : graph.getSelectionCells() ) { - DefaultWeightedEdge e = graph.getEdgeFor( ( mxICell ) obj ); + final DefaultWeightedEdge e = graph.getEdgeFor( ( mxICell ) obj ); if ( e == null ) continue; diff --git a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java index d927df640..7d11d151f 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/trackscheme/TrackSchemeKeyboardHandler.java @@ -99,7 +99,9 @@ protected InputMap getInputMap( final int condition ) } /** - * Return the mapping between JTree's input map and JGraph's actions. + * Returns the mapping between JTree's input map and JGraph's actions. + * + * @return the action map. */ protected ActionMap createActionMap() { diff --git a/src/main/resources/fiji/plugin/trackmate/gui/images/TrackMateBVV-logo-16x16.png b/src/main/resources/fiji/plugin/trackmate/gui/images/TrackMateBVV-logo-16x16.png new file mode 100644 index 000000000..2e5b7d5fa Binary files /dev/null and b/src/main/resources/fiji/plugin/trackmate/gui/images/TrackMateBVV-logo-16x16.png differ diff --git a/src/main/resources/fiji/plugin/trackmate/gui/images/bullet_green.png b/src/main/resources/fiji/plugin/trackmate/gui/images/bullet_green.png new file mode 100644 index 000000000..058ad261f Binary files /dev/null and b/src/main/resources/fiji/plugin/trackmate/gui/images/bullet_green.png differ diff --git a/src/main/resources/fiji/plugin/trackmate/gui/images/help.png b/src/main/resources/fiji/plugin/trackmate/gui/images/help.png new file mode 100644 index 000000000..5c870176d Binary files /dev/null and b/src/main/resources/fiji/plugin/trackmate/gui/images/help.png differ diff --git a/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.fp b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.fp new file mode 100644 index 000000000..fb732ddd4 --- /dev/null +++ b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.fp @@ -0,0 +1,48 @@ +out vec4 fragColor; + +in vec3 Normal; +in vec3 FragPos; + +uniform vec4 ObjectColor; +uniform float IsSelected; +uniform vec4 SelectionColor; + +const vec3 lightColor1 = 0.5 * vec3(0.9, 0.9, 1); +const vec3 lightDir1 = normalize(vec3(0, -0.2, -1)); + +const vec3 lightColor2 = 0.5 * vec3(0.1, 0.1, 1); +const vec3 lightDir2 = normalize(vec3(1, 1, 0.5)); + +const vec3 ambient = vec3(0.7, 0.7, 0.7); + +const float specularStrength = 5; + +vec3 phong(vec3 norm, vec3 viewDir, vec3 lightDir, vec3 lightColor, float shininess, float specularStrength) +{ + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess); + vec3 specular = specularStrength * spec * lightColor; + + return diffuse + specular; +} + +void main() +{ +// fragColor = vec4(ObjectColor, 1); + vec3 norm = normalize(Normal); + vec3 viewDir = normalize(-FragPos); + + vec3 l1 = phong( norm, viewDir, lightDir1, lightColor1, 32, 0.1 ); + vec3 l2 = phong( norm, viewDir, lightDir2, lightColor2, 32, 0.5 ); + + if (IsSelected > 0.5) { + fragColor = vec4( (ambient + l1 + l2), SelectionColor[3]) * SelectionColor; + } else { + float it = dot(norm, viewDir); + fragColor = vec4( it * (ambient + l1 + l2), 1) * ObjectColor + (1-it) * vec4(1,1,1,ObjectColor[3]); + } + +} diff --git a/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.vp b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.vp new file mode 100644 index 000000000..e86810409 --- /dev/null +++ b/src/main/resources/fiji/plugin/trackmate/visualization/bvv/mesh.vp @@ -0,0 +1,16 @@ +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; + +out vec3 FragPos; +out vec3 Normal; + +uniform mat4 pvm; +uniform mat4 vm; +uniform mat3 itvm; + +void main() +{ + gl_Position = pvm * vec4( aPos, 1.0 ); + FragPos = vec3(vm * vec4(aPos, 1.0)); + Normal = itvm * aNormal; +} diff --git a/src/test/java/fiji/plugin/trackmate/ModelTest.java b/src/test/java/fiji/plugin/trackmate/ModelTest.java index b7ee8b6bd..dd2a85f4a 100644 --- a/src/test/java/fiji/plugin/trackmate/ModelTest.java +++ b/src/test/java/fiji/plugin/trackmate/ModelTest.java @@ -42,17 +42,17 @@ public class ModelTest { public void testTrackVisibility() { final Model model = new Model(); // Build track 1 with 5 spots - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); // Build track 2 with 2 spots - final Spot s6 = new Spot( 0d, 0d, 0d, 1d, -1d, "S6" ); - final Spot s7 = new Spot( 0d, 0d, 0d, 1d, -1d, "S7" ); + final Spot s6 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S6" ); + final Spot s7 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S7" ); // Build track 3 with 2 spots - final Spot s8 = new Spot( 0d, 0d, 0d, 1d, -1d, "S8" ); - final Spot s9 = new Spot( 0d, 0d, 0d, 1d, -1d, "S9" ); + final Spot s8 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S8" ); + final Spot s9 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S9" ); model.beginUpdate(); try { @@ -161,11 +161,11 @@ public void testTrackNumber() { assertEquals(0, model.getTrackModel().nTracks(false)); // Build track with 5 spots - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); model.beginUpdate(); try { model.addSpotTo(s1, 0); @@ -242,11 +242,11 @@ public void modelChanged(final ModelChangeEvent event) { model.addModelChangeListener(eventLogger); - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); // System.out.println("Create the graph in one update:"); model.beginUpdate(); @@ -381,7 +381,7 @@ public void testRemovingWholeTracksAtOnce() { Spot previous = null; Spot spot = null; for (int j = 0; j < DEPTH; j++) { - spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo(spot, j); if (i == 0) { trackSpots.add(spot); @@ -444,11 +444,11 @@ public void exampleManipulation() { // Add an event listener now model.addModelChangeListener(new EventLogger()); - final Spot s1 = new Spot( 0d, 0d, 0d, 1d, -1d, "S1" ); - final Spot s2 = new Spot( 0d, 0d, 0d, 1d, -1d, "S2" ); - final Spot s3 = new Spot( 0d, 0d, 0d, 1d, -1d, "S3" ); - final Spot s4 = new Spot( 0d, 0d, 0d, 1d, -1d, "S4" ); - final Spot s5 = new Spot( 0d, 0d, 0d, 1d, -1d, "S5" ); + final Spot s1 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S1" ); + final Spot s2 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S2" ); + final Spot s3 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S3" ); + final Spot s4 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S4" ); + final Spot s5 = new SpotBase( 0d, 0d, 0d, 1d, -1d, "S5" ); System.out.println("Create the graph in one update:"); model.beginUpdate(); diff --git a/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java b/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java index 0e7fce526..4acdb0aff 100644 --- a/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java +++ b/src/test/java/fiji/plugin/trackmate/SpotCollectionTest.java @@ -70,7 +70,7 @@ public void setUp() throws Exception final HashSet< Spot > spots = new HashSet<>( 100 ); for ( int j = 0; j < N_SPOTS; j++ ) { - final Spot spot = new Spot( j, j, j, 1d, -1d ); + final Spot spot = new SpotBase( j, j, j, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( i ) ); spot.putFeature( Spot.QUALITY, Double.valueOf( j ) ); spot.putFeature( Spot.RADIUS, Double.valueOf( j / 2 ) ); @@ -101,7 +101,7 @@ public void testAdd() } // Add a spot to target frame final int targetFrame = 1 + 2 * new Random().nextInt( 50 ); - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); sc.add( spot, targetFrame ); // Test for ( final Integer frame : frames ) @@ -221,7 +221,7 @@ public void testGetClosestSpot() final FeatureFilter filter = new FeatureFilter( Spot.QUALITY, 20d, false ); sc.filter( filter ); - final Spot location = new Spot( 50.1, 50.1, 50.1, 1d, -1d ); + final Spot location = new SpotBase( 50.1, 50.1, 50.1, 1d, -1d ); for ( final Integer frame : frames ) { // Closest non-visible spot should be the one with QUALITY = 50 @@ -240,8 +240,8 @@ public void testGetSpotAt() final FeatureFilter filter = new FeatureFilter( Spot.QUALITY, 20d, false ); sc.filter( filter ); - final Spot location1 = new Spot( 50.1, 50.1, 50.1, 1d, -1d ); - final Spot location2 = new Spot( 10.1, 10.1, 10.1, 1d, -1d ); + final Spot location1 = new SpotBase( 50.1, 50.1, 50.1, 1d, -1d ); + final Spot location2 = new SpotBase( 10.1, 10.1, 10.1, 1d, -1d ); for ( final Integer frame : frames ) { // The closest non-visible spot should be the one with QUALITY = 50 @@ -391,7 +391,7 @@ public void testPut() final HashSet< Spot > spots = new HashSet<>( N_SPOTS_TO_ADD ); for ( int i = 0; i < N_SPOTS_TO_ADD; i++ ) { - spots.add( new Spot( -1d, -1d, -1d, 1d, -1d ) ); + spots.add( new SpotBase( -1d, -1d, -1d, 1d, -1d ) ); } // Add it to a new frame int targetFrame = 1000; @@ -435,7 +435,7 @@ public void testFirstKey() final HashSet< Spot > spots = new HashSet<>( N_SPOTS_TO_ADD ); for ( int i = 0; i < N_SPOTS_TO_ADD; i++ ) { - spots.add( new Spot( -1d, -1d, -1d, 1d, -1d ) ); + spots.add( new SpotBase( -1d, -1d, -1d, 1d, -1d ) ); } // Add it to a new frame final int targetFrame = -1; @@ -456,7 +456,7 @@ public void testLastKey() final HashSet< Spot > spots = new HashSet<>( N_SPOTS_TO_ADD ); for ( int i = 0; i < N_SPOTS_TO_ADD; i++ ) { - spots.add( new Spot( -1d, -1d, -1d, 1d, -1d ) ); + spots.add( new SpotBase( -1d, -1d, -1d, 1d, -1d ) ); } // Add it to a new frame final int targetFrame = 1000; diff --git a/src/test/java/fiji/plugin/trackmate/TestTrackMatePlugin.java b/src/test/java/fiji/plugin/trackmate/TestTrackMatePlugin.java new file mode 100644 index 000000000..5cde8c717 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/TestTrackMatePlugin.java @@ -0,0 +1,43 @@ +/*- + * #%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; + +import org.scijava.Context; + +import fiji.plugin.trackmate.util.TMUtils; +import ij.IJ; +import ij.ImagePlus; + +class TestTrackMatePlugin extends TrackMatePlugIn { + + @SuppressWarnings("unused") + public void setUp() { + final ImagePlus imp = IJ.createImage("Test Image", 256, 256, 10, 8); + final Settings settings = createSettings(imp); + final Model model = createModel(imp); + final TrackMate trackMate = createTrackMate(model, settings); + } + + public Context getLocalContext() { + return TMUtils.getContext(); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java b/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java index 68e56be4b..132c27dfe 100644 --- a/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java +++ b/src/test/java/fiji/plugin/trackmate/TrackMatePluginTest.java @@ -26,39 +26,18 @@ import java.util.List; import org.junit.Test; -import org.scijava.Context; import org.scijava.object.ObjectService; -import fiji.plugin.trackmate.util.TMUtils; -import ij.IJ; -import ij.ImagePlus; - public class TrackMatePluginTest { @Test public void testTrackMateRegistration() { - TestTrackMatePlugin testPlugin = new TestTrackMatePlugin(); + final TestTrackMatePlugin testPlugin = new TestTrackMatePlugin(); testPlugin.setUp(); - ObjectService objectService = testPlugin.getLocalContext().service(ObjectService.class); + final ObjectService objectService = testPlugin.getLocalContext().service(ObjectService.class); - List trackMateInstances = objectService.getObjects(TrackMate.class); + final List trackMateInstances = objectService.getObjects(TrackMate.class); assertTrue(trackMateInstances.size() == 1); assertTrue(trackMateInstances.get(0) instanceof TrackMate); } - - private class TestTrackMatePlugin extends TrackMatePlugIn { - - @SuppressWarnings("unused") - public void setUp() { - ImagePlus imp = IJ.createImage("Test Image", 256, 256, 10, 8); - Settings settings = createSettings(imp); - Model model = createModel(imp); - TrackMate trackMate = createTrackMate(model, settings); - } - - public Context getLocalContext() { - return TMUtils.getContext(); - } - - } } diff --git a/src/test/java/fiji/plugin/trackmate/TrackModelTest.java b/src/test/java/fiji/plugin/trackmate/TrackModelTest.java index 5731fb74b..52a1e1740 100644 --- a/src/test/java/fiji/plugin/trackmate/TrackModelTest.java +++ b/src/test/java/fiji/plugin/trackmate/TrackModelTest.java @@ -49,7 +49,7 @@ public void testBuildingTracks() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -83,7 +83,7 @@ public void testConnectingTracks() Spot spot = null; for ( int j = 0; j < DEPTH; j++ ) { - spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -125,7 +125,7 @@ public void testBreakingTracksBySpots() { for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -158,14 +158,14 @@ public void testBreakingTracksByEdges() // Build 1 long track final TrackModel model = new TrackModel(); final List< DefaultWeightedEdge > trackBreaks = new ArrayList<>(); - Spot previous = new Spot( 0d, 0d, 0d, 1d, -1d ); + Spot previous = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( previous ); for ( int i = 0; i < N_TRACKS; i++ ) { DefaultWeightedEdge edge = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); edge = model.addEdge( previous, spot, 1 ); previous = spot; @@ -198,7 +198,7 @@ public void testVisibility() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { @@ -245,7 +245,7 @@ public void testVisibilityMerge() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpot( spot ); if ( null != previous ) { diff --git a/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java b/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java index 795e3cd27..cd223f3f8 100644 --- a/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java +++ b/src/test/java/fiji/plugin/trackmate/action/CloseGapsByLinearInterpolationActionTest.java @@ -29,6 +29,7 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.action.closegaps.CloseGapsByLinearInterpolation; @@ -180,8 +181,7 @@ private void checkPositions( final GraphIterator< Spot, DefaultWeightedEdge > sp private Spot createSpot( final double x, final double y, final double z ) { - final Spot newSpot = new Spot( x, y, z, 1.0, 1.0 ); - + final Spot newSpot = new SpotBase( x, y, z, 1.0, 1.0 ); newSpot.getFeatures().put( Spot.POSITION_T, 1.0 ); return newSpot; } diff --git a/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java b/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java index 4a954868c..f546832c1 100644 --- a/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java +++ b/src/test/java/fiji/plugin/trackmate/detection/HessianDetectorTestDrive1.java @@ -55,7 +55,6 @@ public static < T extends RealType< T > & NativeType< T > > void main( final Str final ImagePlus imp = IJ.openImage( "samples/TSabateCell.tif" ); imp.show(); - @SuppressWarnings( "unchecked" ) final ImgPlus< T > input = TMUtils.rawWraps( imp ); final double[] calibration = TMUtils.getSpatialCalibration( imp ); final double radiusXY = 0.6 / 2.; // um; diff --git a/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java b/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java index a89e1b3a4..fecad1adc 100644 --- a/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java +++ b/src/test/java/fiji/plugin/trackmate/features/SpotFeatureComputationBenchmark.java @@ -32,7 +32,7 @@ import fiji.plugin.trackmate.features.spot.SpotAnalyzerFactoryBase; import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.providers.SpotAnalyzerProvider; -import fiji.plugin.trackmate.providers.SpotMorphologyAnalyzerProvider; +import fiji.plugin.trackmate.providers.Spot2DMorphologyAnalyzerProvider; import ij.ImagePlus; import net.imglib2.util.Util; @@ -67,7 +67,7 @@ public static void main( final String[] args ) for ( final String key : provider1.getVisibleKeys() ) factories.add( provider1.getFactory( key ) ); - final SpotMorphologyAnalyzerProvider provider2 = new SpotMorphologyAnalyzerProvider( 1 ); + final Spot2DMorphologyAnalyzerProvider provider2 = new Spot2DMorphologyAnalyzerProvider( 1 ); for ( final String key : provider2.getVisibleKeys() ) factories.add( provider2.getFactory( key ) ); diff --git a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java index b41f66811..3775ae727 100644 --- a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTargetAnalyzerTest.java @@ -23,11 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeTargetAnalyzer; import java.util.Collection; import java.util.HashMap; @@ -37,6 +32,13 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.features.edges.EdgeTargetAnalyzer; + public class EdgeTargetAnalyzerTest { @@ -73,7 +75,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { diff --git a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java index 500ae7464..a1bfe32c3 100644 --- a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeTimeAndLocationAnalyzerTest.java @@ -23,12 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import fiji.plugin.trackmate.Dimension; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeTimeLocationAnalyzer; import java.util.Collection; import java.util.HashMap; @@ -39,6 +33,14 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Dimension; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.features.edges.EdgeTimeLocationAnalyzer; + public class EdgeTimeAndLocationAnalyzerTest { @@ -75,7 +77,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { - final Spot spot = new Spot( i + j, i + j, i + j, 1d, -1d ); + final Spot spot = new SpotBase( i + j, i + j, i + j, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model.addSpotTo( spot, j ); if ( null != previous ) diff --git a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java index 4b70968b2..0d313108a 100644 --- a/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/edge/EdgeVelocityAnalyzerTest.java @@ -23,11 +23,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.features.edges.EdgeSpeedAnalyzer; import java.util.Collection; import java.util.HashMap; @@ -37,6 +32,13 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.features.edges.EdgeSpeedAnalyzer; + public class EdgeVelocityAnalyzerTest { @@ -75,10 +77,9 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); - spot.putFeature( posFeats[ i % 3 ], Double.valueOf( i + j ) ); // rotate - // displacement - // dimension + final Spot spot = new SpotBase( 0., 0., 0., 1., -1. ); + spot.putFeature( posFeats[ i % 3 ], Double.valueOf( i + j ) ); + // rotate displacement dimension spot.putFeature( Spot.POSITION_T, Double.valueOf( 2 * j ) ); model.addSpotTo( spot, j ); if ( null != previous ) diff --git a/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java index 9556f6380..f92194793 100644 --- a/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/spot/SpotIntensityAnalyzerTest.java @@ -27,10 +27,11 @@ import org.junit.Test; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.util.SpotNeighborhood; +import fiji.plugin.trackmate.SpotBase; import net.imagej.ImgPlus; import net.imagej.axis.Axes; import net.imagej.axis.AxisType; +import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; import net.imglib2.img.Img; import net.imglib2.img.array.ArrayImgs; @@ -75,7 +76,7 @@ public void setUp() throws Exception } - spot = new Spot( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d, "1" ); + spot = new SpotBase( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d, "1" ); } @Test @@ -97,16 +98,12 @@ public static void main( final String[] args ) throws Exception final SpotIntensityAnalyzerTest test = new SpotIntensityAnalyzerTest(); test.setUp(); - final Spot tmpSpot = new Spot( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d ); - final SpotNeighborhood< UnsignedShortType > disc = new SpotNeighborhood<>( tmpSpot, test.img2D ); + final Spot tmpSpot = new SpotBase( CENTER[ 0 ], CENTER[ 1 ], CENTER[ 2 ], RADIUS, -1d ); + final IterableInterval< UnsignedShortType > disc = tmpSpot.iterable( test.img2D ); for ( final UnsignedShortType pixel : disc ) - { pixel.set( 1500 ); - } ij.ImageJ.main( args ); net.imglib2.img.display.imagej.ImageJFunctions.show( test.img2D ); - } - } diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java index 86d2f6cea..d55ffa590 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackBranchingAnalyzerTest.java @@ -37,6 +37,7 @@ import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; public class TrackBranchingAnalyzerTest { @@ -74,7 +75,7 @@ public void setUp() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -93,7 +94,7 @@ public void setUp() { continue; } - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -109,7 +110,7 @@ public void setUp() split = null; // Store the spot at the branch split for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); if ( j == DEPTH / 2 ) { split = spot; @@ -129,7 +130,7 @@ public void setUp() previous = split; for ( int j = DEPTH / 2 + 1; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); model.addEdge( previous, spot, 1 ); previous = spot; @@ -143,7 +144,7 @@ public void setUp() Spot merge = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); if ( j == DEPTH / 2 ) { merge = spot; @@ -158,7 +159,7 @@ public void setUp() previous = null; for ( int j = 0; j < DEPTH / 2; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -233,8 +234,8 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); model.addEdge( spot1, spot2, 1 ); } @@ -269,7 +270,7 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - newSpot = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); + newSpot = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); model.addEdge( lFirstSpot, newSpot, 1 ); } finally diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java index 07ed099fe..0669806d0 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackDurationAnalyzerTest.java @@ -24,10 +24,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; import java.util.Collection; import java.util.HashMap; @@ -39,6 +35,12 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; + public class TrackDurationAnalyzerTest { @@ -86,7 +88,7 @@ public void setUp() final HashSet< Spot > track = new HashSet<>(); for ( int j = start; j <= stop; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model.addSpotTo( spot, j ); track.add( spot ); @@ -161,9 +163,9 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); spot1.putFeature( Spot.POSITION_T, 0d ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); spot2.putFeature( Spot.POSITION_T, 1d ); model.addEdge( spot1, spot2, 1 ); @@ -201,7 +203,7 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - newSpot = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); + newSpot = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), firstFrame + 1 ); newSpot.putFeature( Spot.POSITION_T, Double.valueOf( firstFrame + 1 ) ); model.addEdge( firstSpot, newSpot, 1 ); } diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java index 096b887a1..42147997c 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackIndexAnalyzerTest.java @@ -39,6 +39,7 @@ import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; /** * @author Jean-Yves Tinevez @@ -65,7 +66,7 @@ public void setUp() Spot previous = null; for ( int j = 0; j < DEPTH; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d ); model.addSpotTo( spot, j ); if ( null != previous ) { @@ -151,7 +152,7 @@ public void modelChanged( final ModelChangeEvent event ) try { final Spot targetSpot = model.getSpots().iterator( 0, true ).next(); - final Spot newSpot = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot newSpot = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); model.addEdge( targetSpot, newSpot, 1 ); } finally diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java index 4111c4696..a8867811d 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackLocationAnalyzerTest.java @@ -24,10 +24,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.ModelChangeEvent; -import fiji.plugin.trackmate.ModelChangeListener; -import fiji.plugin.trackmate.Spot; import java.util.Collection; import java.util.HashMap; @@ -38,6 +34,12 @@ import org.junit.Before; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.ModelChangeEvent; +import fiji.plugin.trackmate.ModelChangeListener; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; + public class TrackLocationAnalyzerTest { @@ -73,7 +75,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { // We use deterministic locations - final Spot spot = new Spot( j + i, j + i, j + i, 1d, -1d ); + final Spot spot = new SpotBase( j + i, j + i, j + i, 1d, -1d ); model.addSpotTo( spot, j ); track.add( spot ); if ( null != previous ) @@ -144,9 +146,9 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); spot1.putFeature( Spot.POSITION_T, 0d ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); spot2.putFeature( Spot.POSITION_T, 1d ); model.addEdge( spot1, spot2, 1 ); diff --git a/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java b/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java index 10b04a7a3..3d23a5ac6 100644 --- a/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java +++ b/src/test/java/fiji/plugin/trackmate/features/track/TrackSpeedStatisticsAnalyzerTest.java @@ -38,6 +38,7 @@ import fiji.plugin.trackmate.ModelChangeEvent; import fiji.plugin.trackmate.ModelChangeListener; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; public class TrackSpeedStatisticsAnalyzerTest { @@ -71,7 +72,7 @@ public void setUp() for ( int j = 0; j <= DEPTH; j++ ) { // We use deterministic locations - final Spot spot = new Spot( j * i, i, i, 1d, -1d ); + final Spot spot = new SpotBase( j * i, i, i, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model.addSpotTo( spot, j ); track.add( spot ); @@ -127,7 +128,7 @@ public final void testProcess2() for ( int j = 0; j <= DEPTH; j++ ) { // We use deterministic locations - final Spot spot = new Spot( j * j, 0d, 0d, 1d, -1d ); + final Spot spot = new SpotBase( j * j, 0d, 0d, 1d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( j ) ); model2.addSpotTo( spot, j ); track.add( spot ); @@ -197,9 +198,9 @@ public void modelChanged( final ModelChangeEvent event ) model.beginUpdate(); try { - final Spot spot1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 0 ); + final Spot spot1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 0 ); spot1.putFeature( Spot.POSITION_T, 0d ); - final Spot spot2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d ), 1 ); + final Spot spot2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d ), 1 ); spot2.putFeature( Spot.POSITION_T, 1d ); model.addEdge( spot1, spot2, 1 ); diff --git a/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java b/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java index 5f0b8cea0..5e481754e 100644 --- a/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java +++ b/src/test/java/fiji/plugin/trackmate/graph/ConvexBranchDecompositionDebug.java @@ -24,6 +24,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackModel; import fiji.plugin.trackmate.graph.ConvexBranchesDecomposition.TrackBranchDecomposition; @@ -35,17 +36,17 @@ public class ConvexBranchDecompositionDebug public static void main( final String[] args ) { - final Spot sa0 = new Spot( 0, 0, 0, 1, -1, "SA_0" ); - final Spot sa1 = new Spot( 0, 0, 0, 1, -1, "SA_1" ); - final Spot sa3 = new Spot( 0, 0, 0, 1, -1, "SA_3" ); - final Spot sa4 = new Spot( 0, 0, 0, 1, -1, "SA_4" ); + final Spot sa0 = new SpotBase( 0, 0, 0, 1, -1, "SA_0" ); + final Spot sa1 = new SpotBase( 0, 0, 0, 1, -1, "SA_1" ); + final Spot sa3 = new SpotBase( 0, 0, 0, 1, -1, "SA_3" ); + final Spot sa4 = new SpotBase( 0, 0, 0, 1, -1, "SA_4" ); - final Spot sb0 = new Spot( 0, 0, 0, 1, -1, "SB_0" ); - final Spot sb1 = new Spot( 0, 0, 0, 1, -1, "SB_1" ); - final Spot sb3 = new Spot( 0, 0, 0, 1, -1, "SB_3" ); - final Spot sb4 = new Spot( 0, 0, 0, 1, -1, "SB_4" ); + final Spot sb0 = new SpotBase( 0, 0, 0, 1, -1, "SB_0" ); + final Spot sb1 = new SpotBase( 0, 0, 0, 1, -1, "SB_1" ); + final Spot sb3 = new SpotBase( 0, 0, 0, 1, -1, "SB_3" ); + final Spot sb4 = new SpotBase( 0, 0, 0, 1, -1, "SB_4" ); - final Spot nexus = new Spot( 0, 0, 0, 1, -1, "NEXUS" ); + final Spot nexus = new SpotBase( 0, 0, 0, 1, -1, "NEXUS" ); final SpotCollection spots = new SpotCollection(); spots.add( sa0, 0 ); diff --git a/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java b/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java index 86c6a1d33..dead02c46 100644 --- a/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java +++ b/src/test/java/fiji/plugin/trackmate/graph/SortedDepthFirstIteratorTest.java @@ -22,8 +22,6 @@ package fiji.plugin.trackmate.graph; import static org.junit.Assert.assertArrayEquals; -import fiji.plugin.trackmate.Model; -import fiji.plugin.trackmate.Spot; import java.util.Arrays; import java.util.Comparator; @@ -33,6 +31,10 @@ import org.junit.BeforeClass; import org.junit.Test; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; + public class SortedDepthFirstIteratorTest { @@ -78,7 +80,7 @@ public int compare( final Spot o1, final Spot o2 ) { // Root - root = new Spot( 0d, 0d, 0d, 1d, -1d, "Root" ); + root = new SpotBase( 0d, 0d, 0d, 1d, -1d, "Root" ); model.addSpotTo( root, 0 ); // First level @@ -88,7 +90,7 @@ public int compare( final Spot o1, final Spot o2 ) { names[ i ] = "A"; // randomString( 5 ); - final Spot spotChild = new Spot( 0d, 0d, 0d, 1d, -1d, names[ i ] ); + final Spot spotChild = new SpotBase( 0d, 0d, 0d, 1d, -1d, names[ i ] ); model.addSpotTo( spotChild, 1 ); model.addEdge( root, spotChild, -1 ); spots[ 0 ][ i ] = spotChild; @@ -96,7 +98,7 @@ public int compare( final Spot o1, final Spot o2 ) spots[ 0 ][ i ] = spotChild; for ( int j = 1; j < spots.length; j++ ) { - final Spot spot = new Spot( 0d, 0d, 0d, 1d, -1d, " " + j + "_" + randomString( 3 ) ); + final Spot spot = new SpotBase( 0d, 0d, 0d, 1d, -1d, " " + j + "_" + randomString( 3 ) ); spots[ j ][ i ] = spot; model.addSpotTo( spot, j + 1 ); model.addEdge( spots[ j - 1 ][ i ], spots[ j ][ i ], -1 ); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java b/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java index a6c55615f..e309122b3 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/GraphTest.java @@ -30,6 +30,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.graph.GraphUtils; import fiji.plugin.trackmate.graph.TimeDirectedNeighborIndex; @@ -103,22 +104,22 @@ public static final Model getExampleModel() // Create spots - final Spot root = new Spot( 3d, 0d, 0d, 1d, -1d, "Zygote" ); + final Spot root = new SpotBase( 3d, 0d, 0d, 1d, -1d, "Zygote" ); - final Spot AB = new Spot( 0d, 1d, 0d, 1d, -1d, "AB" ); - final Spot P1 = new Spot( 3d, 1d, 0d, 1d, -1d, "P1" ); + final Spot AB = new SpotBase( 0d, 1d, 0d, 1d, -1d, "AB" ); + final Spot P1 = new SpotBase( 3d, 1d, 0d, 1d, -1d, "P1" ); - final Spot P2 = new Spot( 4d, 2d, 0d, 1d, -1d, "P2" ); - final Spot EMS = new Spot( 2d, 2d, 0d, 1d, -1d, "EMS" ); + final Spot P2 = new SpotBase( 4d, 2d, 0d, 1d, -1d, "P2" ); + final Spot EMS = new SpotBase( 2d, 2d, 0d, 1d, -1d, "EMS" ); - final Spot P3 = new Spot( 5d, 3d, 0d, 1d, -1d, "P3" ); - final Spot C = new Spot( 3d, 3d, 0d, 1d, -1d, "C" ); - final Spot E = new Spot( 1d, 3d, 0d, 1d, -1d, "E" ); - final Spot MS = new Spot( 2d, 3d, 0d, 1d, -1d, "MS" ); - final Spot AB3 = new Spot( 0d, 3d, 0d, 1d, -1d, "AB" ); + final Spot P3 = new SpotBase( 5d, 3d, 0d, 1d, -1d, "P3" ); + final Spot C = new SpotBase( 3d, 3d, 0d, 1d, -1d, "C" ); + final Spot E = new SpotBase( 1d, 3d, 0d, 1d, -1d, "E" ); + final Spot MS = new SpotBase( 2d, 3d, 0d, 1d, -1d, "MS" ); + final Spot AB3 = new SpotBase( 0d, 3d, 0d, 1d, -1d, "AB" ); - final Spot D = new Spot( 4d, 4d, 0d, 1d, -1d, "D" ); - final Spot P4 = new Spot( 5d, 4d, 0d, 1d, -1d, "P4" ); + final Spot D = new SpotBase( 4d, 4d, 0d, 1d, -1d, "D" ); + final Spot P4 = new SpotBase( 5d, 4d, 0d, 1d, -1d, "P4" ); // Add them to the graph @@ -194,9 +195,9 @@ public static final Model getComplicatedExample() try { // new spots - final Spot Q1 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d, "Q1" ), 0 ); - final Spot Q2 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d, "Q2" ), 1 ); - final Spot Q3 = model.addSpotTo( new Spot( 0d, 0d, 0d, 1d, -1d, "Q3" ), 2 ); + final Spot Q1 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d, "Q1" ), 0 ); + final Spot Q2 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d, "Q2" ), 1 ); + final Spot Q3 = model.addSpotTo( new SpotBase( 0d, 0d, 0d, 1d, -1d, "Q3" ), 2 ); // new links model.addEdge( Q1, Q2, -1 ); model.addEdge( Q2, Q3, -1 ); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java index 3acc2b682..0de7fe53d 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotFeatureGrapherExample.java @@ -35,6 +35,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.features.SpotFeatureGrapher; @@ -96,7 +97,7 @@ private static Model getSpiralModel() final double x = 100d + 100 * i / 100. * Math.cos( i / 100. * 5 * 2 * Math.PI ); final double y = 100d + 100 * i / 100. * Math.sin( i / 100. * 5 * 2 * Math.PI ); final double z = 0d; - final Spot spot = new Spot( x, y, z, 2d, -1d ); + final Spot spot = new SpotBase( x, y, z, 2d, -1d ); spot.putFeature( Spot.POSITION_T, Double.valueOf( i ) ); spots.add( spot ); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java index c26273c44..4df98de72 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/SpotNeighborhoodTest.java @@ -22,15 +22,17 @@ package fiji.plugin.trackmate.interactivetests; import fiji.plugin.trackmate.Spot; -import fiji.plugin.trackmate.util.SpotNeighborhood; -import fiji.plugin.trackmate.util.SpotNeighborhoodCursor; +import fiji.plugin.trackmate.SpotBase; import ij.ImageJ; import net.imagej.ImgPlus; +import net.imglib2.Cursor; +import net.imglib2.IterableInterval; import net.imglib2.img.array.ArrayImg; import net.imglib2.img.array.ArrayImgs; import net.imglib2.img.basictypeaccess.array.ShortArray; import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.type.numeric.integer.UnsignedShortType; +import net.imglib2.util.Util; public class SpotNeighborhoodTest { @@ -42,12 +44,13 @@ public static void main( final String[] args ) // 3D final ArrayImg< UnsignedShortType, ShortArray > image = ArrayImgs.unsignedShorts( 100, 100, 100 ); final ImgPlus< UnsignedShortType > img = new ImgPlus<>( image ); - final Spot spot = new Spot( 50d, 50d, 50d, 30d, -1d ); - final SpotNeighborhood< UnsignedShortType > neighborhood = new SpotNeighborhood<>( spot, img ); - final SpotNeighborhoodCursor< UnsignedShortType > cursor = neighborhood.cursor(); + final Spot spot = new SpotBase( 50d, 50d, 50d, 30d, -1d ); + final IterableInterval< UnsignedShortType > neighborhood = spot.iterable( img ); + final Cursor< UnsignedShortType > cursor = neighborhood.cursor(); while ( cursor.hasNext() ) { - cursor.next().set( ( int ) cursor.getDistanceSquared() ); + final double d = Util.distance( spot, cursor ); + cursor.next().set( ( int ) ( d * d ) ); } System.out.println( "Finished" ); ImageJFunctions.wrap( img, "3D" ).show(); @@ -55,12 +58,13 @@ public static void main( final String[] args ) // 2D final ArrayImg< UnsignedShortType, ShortArray > image2 = ArrayImgs.unsignedShorts( 100, 100 ); final ImgPlus< UnsignedShortType > img2 = new ImgPlus<>( image2 ); - final Spot spot2 = new Spot( 50d, 50d, 0d, 30d, -1d ); - final SpotNeighborhood< UnsignedShortType > neighborhood2 = new SpotNeighborhood<>( spot2, img2 ); - final SpotNeighborhoodCursor< UnsignedShortType > cursor2 = neighborhood2.cursor(); + final Spot spot2 = new SpotBase( 50d, 50d, 0d, 30d, -1d ); + final IterableInterval< UnsignedShortType > neighborhood2 = spot2.iterable( img2 ); + final Cursor< UnsignedShortType > cursor2 = neighborhood2.cursor(); while ( cursor2.hasNext() ) { - cursor2.next().set( ( int ) cursor2.getDistanceSquared() ); + final double d = Util.distance( spot2, cursor2 ); + cursor2.next().set( ( int ) ( d * d ) ); } System.out.println( "Finished" ); ImageJFunctions.wrap( img2, "3D" ).show(); diff --git a/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java b/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java index 8ad49b2e9..60b115ec3 100644 --- a/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java +++ b/src/test/java/fiji/plugin/trackmate/interactivetests/TmXmlReaderTestDrive.java @@ -31,8 +31,9 @@ import fiji.plugin.trackmate.io.TmXmlReader; import fiji.plugin.trackmate.providers.DetectorProvider; 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.providers.TrackerProvider; import ij.ImagePlus; @@ -57,7 +58,8 @@ public static void main( final String args[] ) new SpotAnalyzerProvider( imp.getNChannels() ), new EdgeAnalyzerProvider(), new TrackAnalyzerProvider(), - new SpotMorphologyAnalyzerProvider( imp.getNChannels() ) ); + new Spot2DMorphologyAnalyzerProvider( imp.getNChannels() ), + new Spot3DMorphologyAnalyzerProvider( imp.getNChannels() ) ); System.out.println( settings ); System.out.println( model ); diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DebugZSlicer.java b/src/test/java/fiji/plugin/trackmate/mesh/DebugZSlicer.java new file mode 100644 index 000000000..7fa4019e5 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DebugZSlicer.java @@ -0,0 +1,85 @@ +/*- + * #%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.mesh; + +import java.io.File; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.io.TmXmlReader; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.CompositeImage; +import ij.ImageJ; +import ij.ImagePlus; +import net.imglib2.mesh.alg.zslicer.Contour; +import net.imglib2.mesh.alg.zslicer.Slice; +import net.imglib2.mesh.alg.zslicer.ZSlicer; + +public class DebugZSlicer +{ + public static void main( final String[] args ) + { + try + { + ImageJ.main( args ); + + final String filePath = "samples/CElegans3D-smoothed-mask-orig-t7.xml"; + final TmXmlReader reader = new TmXmlReader( new File( filePath ) ); + if ( !reader.isReadingOk() ) + { + System.err.println( reader.getErrorMessage() ); + return; + } + + final ImagePlus imp = reader.readImage(); + imp.show(); + final double[] calibration = TMUtils.getSpatialCalibration( imp ); + + final Model model = reader.getModel(); + final SelectionModel selection = new SelectionModel( model ); + final DisplaySettings ds = reader.getDisplaySettings(); + + final HyperStackDisplayer view = new HyperStackDisplayer( model, selection, imp, ds ); + view.render(); + imp.setDisplayMode( CompositeImage.GRAYSCALE ); + + final Spot spot = model.getSpots().iterable( true ).iterator().next(); + final double z = 21.; + + imp.setZ( ( int ) Math.round( z / calibration[ 2 ] ) + 1 ); + + final Slice contours = ZSlicer.slice( ( ( SpotMesh ) spot ).getMesh(), z, calibration[ 2 ] ); + System.out.println( "Found " + contours.size() + " contours." ); + for ( final Contour contour : contours ) + System.out.println( contour ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} + diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DefaultMesh.java b/src/test/java/fiji/plugin/trackmate/mesh/DefaultMesh.java new file mode 100644 index 000000000..74bd896b7 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DefaultMesh.java @@ -0,0 +1,107 @@ +/*- + * #%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.mesh; + +import java.util.List; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.detection.ThresholdDetector; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.NewImage; +import ij.measure.Calibration; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.mesh.Mesh; +import net.imglib2.type.logic.BitType; + +public class DefaultMesh +{ + + public static void main( final String[] args ) + { + ImageJ.main( args ); + final ImgPlus< BitType > img = Demo3DMesh.loadTestMask2(); + final ImagePlus imp = ImageJFunctions.show( img, "box" ); + imp.setDimensions( + img.dimensionIndex( Axes.CHANNEL ), + img.dimensionIndex( Axes.Z ), + img.dimensionIndex( Axes.TIME ) ); + final double[] calibration = new double[] { 1., 1., 1. }; + + final ThresholdDetector< BitType > detector = new ThresholdDetector< BitType >( img, img, calibration, 0, false, -1. ); + detector.process(); + final List< Spot > spots = detector.getResult(); + + final Model model = new Model(); + for ( final Spot spot : spots ) + { + model.getSpots().add( spot, 0 ); + System.out.println( spot ); + } + + final SelectionModel selectionModel = new SelectionModel( model ); + final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); + final HyperStackDisplayer view = new HyperStackDisplayer( model, selectionModel, imp, ds ); + view.render(); + } + + public static void main2( final String[] args ) + { + ImageJ.main( args ); + final ImagePlus imp = NewImage.createByteImage( "dummy", + 64, 64, 64, NewImage.FILL_RAMP ); + final Calibration cal = imp.getCalibration(); + cal.pixelWidth = 0.5; + cal.pixelHeight = 0.5; + cal.pixelDepth = 0.5; + imp.show(); + + final long[] min = new long[] { 2, 2, 2 }; + final long[] max = new long[] { 20, 20, 20 }; + final Mesh mesh = Demo3DMesh.debugMesh( min, max ); + final Spot spot = new SpotMesh( mesh, 1. ); + + final Model model = new Model(); + model.beginUpdate(); + try + { + model.addSpotTo( spot, 0 ); + } + finally + { + model.endUpdate(); + } + + final SelectionModel selectionModel = new SelectionModel( model ); + final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); + final HyperStackDisplayer view = new HyperStackDisplayer( model, selectionModel, imp, ds ); + view.render(); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMesh.java b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMesh.java new file mode 100644 index 000000000..e203d7972 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMesh.java @@ -0,0 +1,274 @@ +/*- + * #%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.mesh; + +import java.awt.Color; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Iterator; + +import fiji.plugin.trackmate.detection.MaskUtils; +import fiji.plugin.trackmate.util.TMUtils; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.Overlay; +import ij.gui.PolygonRoi; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.converter.RealTypeConverters; +import net.imglib2.img.ImgView; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.Vertices; +import net.imglib2.mesh.alg.zslicer.Contour; +import net.imglib2.mesh.alg.zslicer.Slice; +import net.imglib2.mesh.alg.zslicer.ZSlicer; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.mesh.io.ply.PLYMeshIO; +import net.imglib2.mesh.io.stl.STLMeshIO; +import net.imglib2.roi.labeling.ImgLabeling; +import net.imglib2.roi.labeling.LabelRegion; +import net.imglib2.roi.labeling.LabelRegions; +import net.imglib2.type.logic.BitType; +import net.imglib2.type.logic.BoolType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.integer.IntType; +import net.imglib2.view.IntervalView; +import net.imglib2.view.Views; + +public class Demo3DMesh +{ + + public static void main( final String[] args ) + { + try + { + + ImageJ.main( args ); +// final ImgPlus< BitType > mask = loadTestMask2(); + final ImgPlus< BitType > mask = loadTestMask(); + + // Convert it to labeling. + final ImgLabeling< Integer, IntType > labeling = MaskUtils.toLabeling( mask, 0.5, 1 ); + final ImagePlus out = ImageJFunctions.show( labeling.getIndexImg(), "labeling" ); + out.setDimensions( mask.dimensionIndex( Axes.CHANNEL ), mask.dimensionIndex( Axes.Z ), mask.dimensionIndex( Axes.TIME ) ); + + // Iterate through all components. + final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); + final double[] cal = TMUtils.getSpatialCalibration( mask ); + + // Parse regions to create polygons on boundaries. + final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + int j = 0; + while ( iterator.hasNext() ) + { + final LabelRegion< Integer > region = iterator.next(); + + // To mesh. + final IntervalView< BoolType > box = Views.zeroMin( region ); + final Mesh mesh = Meshes.marchingCubes( box ); + System.out.println( "Before cleaning: " + mesh.vertices().size() + " vertices and " + mesh.triangles().size() + " faces." ); + final Mesh cleaned = Meshes.removeDuplicateVertices( mesh, 0 ); + System.out.println( "Before simplification: " + cleaned.vertices().size() + " vertices and " + cleaned.triangles().size() + " faces." ); + final Mesh simplified = Meshes.simplify( cleaned, 0.25f, 10 ); + + // Wrap as mesh with edges. + System.out.println( "After simplification: " + simplified.vertices().size() + " vertices and " + simplified.triangles().size() + " faces." ); + System.out.println(); + + // Scale and offset with physical coordinates. + final double[] origin = region.minAsDoubleArray(); + scale( simplified.vertices(), cal, origin ); + + /* + * IO. + */ + testIO( simplified, ++j ); + + /* + * Display. + */ + + // Intersection with a XY plane at a fixed Z position. + final int zslice = 22; // plan + final double z = ( zslice - 1 ) * cal[ 2 ]; // um + + final Slice contours = ZSlicer.slice( simplified, z, cal[ 2 ] ); + toOverlay( contours, out, cal ); + } + System.out.println( "Done." ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } + + @SuppressWarnings( "unused" ) + static Mesh debugMesh( final long[] min, final long[] max ) + { + final NaiveDoubleMesh mesh = new NaiveDoubleMesh(); + final net.imglib2.mesh.impl.naive.NaiveDoubleMesh.Vertices vertices = mesh.vertices(); + final net.imglib2.mesh.impl.naive.NaiveDoubleMesh.Triangles triangles = mesh.triangles(); + + // Coords as X Y Z + + // Bottom square. + final double[] bnw = new double[] { min[ 0 ], min[ 1 ], min[ 2 ] }; + final double[] bne = new double[] { max[ 0 ], min[ 1 ], min[ 2 ] }; + final double[] bsw = new double[] { min[ 0 ], max[ 1 ], min[ 2 ] }; + final double[] bse = new double[] { max[ 0 ], max[ 1 ], min[ 2 ] }; + + // Top square. + final double[] tnw = new double[] { min[ 0 ], min[ 1 ], max[ 2 ] }; + final double[] tne = new double[] { max[ 0 ], min[ 1 ], max[ 2 ] }; + final double[] tsw = new double[] { min[ 0 ], max[ 1 ], max[ 2 ] }; + final double[] tse = new double[] { max[ 0 ], max[ 1 ], max[ 2 ] }; + + // Add vertices. + final long bnwi = vertices.add( bnw[ 0 ], bnw[ 1 ], bnw[ 2 ] ); + final long bnei = vertices.add( bne[ 0 ], bne[ 1 ], bne[ 2 ] ); + final long bswi = vertices.add( bsw[ 0 ], bsw[ 1 ], bsw[ 2 ] ); + final long bsei = vertices.add( bse[ 0 ], bse[ 1 ], bse[ 2 ] ); + final long tnwi = vertices.add( tnw[ 0 ], tnw[ 1 ], tnw[ 2 ] ); + final long tnei = vertices.add( tne[ 0 ], tne[ 1 ], tne[ 2 ] ); + final long tswi = vertices.add( tsw[ 0 ], tsw[ 1 ], tsw[ 2 ] ); + final long tsei = vertices.add( tse[ 0 ], tse[ 1 ], tse[ 2 ] ); + + // Add triangles for the 6 faces. + + // Bottom. + triangles.add( bnwi, bnei, bswi ); + triangles.add( bnei, bsei, bswi ); + + // Top. + triangles.add( tnwi, tnei, tswi ); + triangles.add( tnei, tsei, tswi ); + + // Front (facing south). + triangles.add( tswi, tsei, bsei ); + triangles.add( tswi, bsei, bswi ); + + // Back (facing north). + triangles.add( tnwi, tnei, bnei ); + triangles.add( tnwi, bnei, bnwi ); + + // Left (facing west). + triangles.add( tnwi, tswi, bswi ); + triangles.add( tnwi, bnwi, bswi ); + + // Right (facing east). + triangles.add( tnei, tsei, bsei ); + triangles.add( tnei, bnei, bsei ); + + return mesh; + } + + private static void toOverlay( final Slice contours, final ImagePlus out, final double[] cal ) + { + Overlay overlay = out.getOverlay(); + if ( overlay == null ) + { + overlay = new Overlay(); + out.setOverlay( overlay ); + } + + for ( final Contour contour : contours ) + { + System.out.println( contour ); // DEBUG + final float[] xRoi = new float[ contour.size() ]; + final float[] yRoi = new float[ contour.size() ]; + for ( int i = 0; i < contour.size(); i++ ) + { + xRoi[ i ] = ( float ) ( contour.x( i ) / cal[ 0 ] + 0.5 ); + yRoi[ i ] = ( float ) ( contour.y( i ) / cal[ 1 ] + 0.5 ); + } + final PolygonRoi roi = new PolygonRoi( xRoi, yRoi, PolygonRoi.POLYGON ); + roi.setStrokeColor( contour.isInterior() ? Color.GREEN : Color.RED ); + overlay.add( roi ); + } + } + + private static void testIO( final Mesh mesh, final int j ) + { + // Serialize to disk. + try + { + STLMeshIO.save( mesh, String.format( "samples/mesh/io/STL_%02d.stl", j ) ); + + PLYMeshIO.save( mesh, String.format( "samples/mesh/io/PLY_%02d.ply", j ) ); + final byte[] bs = PLYMeshIO.writeAscii( mesh ); + final String str = new String( bs ); + try (final FileWriter writer = new FileWriter( + String.format( "samples/mesh/io/PLYTEXT_%02d.txt", j ) )) + { + writer.write( str ); + } + } + catch ( final IOException e ) + { + e.printStackTrace(); + } + } + + private static void scale( final Vertices vertices, final double[] scale, final double[] origin ) + { + final long nv = vertices.size(); + for ( long i = 0; i < nv; i++ ) + { + final double x = ( origin[ 0 ] + vertices.x( i ) ) * scale[ 0 ]; + final double y = ( origin[ 1 ] + vertices.y( i ) ) * scale[ 1 ]; + final double z = ( origin[ 2 ] + vertices.z( i ) ) * scale[ 2 ]; + vertices.set( i, x, y, z ); + } + } + + static < T extends RealType< T > & NumericType< T > > ImgPlus< BitType > loadTestMask() + { + final String filePath = "samples/mesh/CElegansMask3D.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + + // First channel is the mask. + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final ImgPlus< T > c1 = ImgPlusViews.hyperSlice( img, img.dimensionIndex( Axes.CHANNEL ), 0 ); + + // Take the first time-point + final ImgPlus< T > t1 = ImgPlusViews.hyperSlice( c1, c1.dimensionIndex( Axes.TIME ), 0 ); + // Make it to boolean. + final RandomAccessibleInterval< BitType > mask = RealTypeConverters.convert( t1, new BitType() ); + return new ImgPlus< BitType >( ImgView.wrap( mask ), t1 ); + } + + static < T extends RealType< T > & NumericType< T > > ImgPlus< BitType > loadTestMask2() + { + final String filePath = "samples/mesh/Cube.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final RandomAccessibleInterval< BitType > mask = RealTypeConverters.convert( img, new BitType() ); + return new ImgPlus<>( ImgView.wrap( mask ), img ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMeshTrackMate.java b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMeshTrackMate.java new file mode 100644 index 000000000..76ce297d8 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/Demo3DMeshTrackMate.java @@ -0,0 +1,50 @@ +/*- + * #%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.mesh; + +import fiji.plugin.trackmate.TrackMatePlugIn; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; + +public class Demo3DMeshTrackMate +{ + + public static void main( final String[] args ) + { + try + { + + ImageJ.main( args ); +// final String filePath = "samples/CElegans3D-smoothed-mask-orig-t7.tif"; + final String filePath = "samples/Celegans-5pc-17timepoints.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + imp.show(); + + new TrackMatePlugIn().run( null ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DemoContour.java b/src/test/java/fiji/plugin/trackmate/mesh/DemoContour.java new file mode 100644 index 000000000..fe145ac2a --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DemoContour.java @@ -0,0 +1,64 @@ +/*- + * #%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.mesh; + +import java.io.File; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.io.TmXmlReader; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.ImageJ; +import ij.ImagePlus; +import math.geom2d.polygon.Polygon2D; +import math.geom2d.polygon.Polygons2D; +import math.geom2d.polygon.SimplePolygon2D; + +public class DemoContour +{ + + public static void main3( final String[] args ) + { + final SimplePolygon2D a = new SimplePolygon2D( new double[] { 0, 2, 2 }, new double[] { 0, 0, 2 } ); + final SimplePolygon2D b = new SimplePolygon2D( new double[] { 0, 0, 2 }, new double[] { 0, 2, 0 } ); + final Polygon2D c = Polygons2D.union( a, b ); + System.out.println( c.area() ); // DEBUG + + } + + public static void main( final String[] args ) + { + ImageJ.main( args ); + final String filePath = "samples/mesh/Torus-mask.xml"; + final TmXmlReader reader = new TmXmlReader( new File( filePath ) ); + final Model model = reader.getModel(); + final ImagePlus imp = reader.readImage(); + imp.show(); + + final SelectionModel selection = new SelectionModel( model ); + final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); + final HyperStackDisplayer view = new HyperStackDisplayer( model, selection, imp, ds ); + view.render(); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DemoHollowMesh.java b/src/test/java/fiji/plugin/trackmate/mesh/DemoHollowMesh.java new file mode 100644 index 000000000..f51aad467 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DemoHollowMesh.java @@ -0,0 +1,89 @@ +/*- + * #%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.mesh; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.NewImage; +import net.imagej.ImgPlus; +import net.imglib2.RealPoint; +import net.imglib2.type.numeric.integer.UnsignedByteType; + +public class DemoHollowMesh +{ + + public static void main( final String[] args ) + { + ImageJ.main( args ); + final ImagePlus imp = makeImg(); + + final Settings settings = new Settings( imp ); + settings.detectorFactory = new ThresholdDetectorFactory<>(); + settings.detectorSettings = settings.detectorFactory.getDefaultSettings(); + settings.detectorSettings.put( ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD, 120. ); + settings.detectorSettings.put( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, false ); + + final TrackMate trackmate = new TrackMate( settings ); + trackmate.execDetection(); + + final Model model = trackmate.getModel(); + model.getSpots().setVisible( true ); + final SelectionModel selection = new SelectionModel( model ); + final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); + + final HyperStackDisplayer view = new HyperStackDisplayer( model, selection, imp, ds ); + view.render(); + } + + public static ImagePlus makeImg() + { + final ImagePlus imp = NewImage.createByteImage( "Hollow", 256, 256, 256, NewImage.FILL_BLACK ); + final ImgPlus< UnsignedByteType > img = TMUtils.rawWraps( imp ); + + final RealPoint center = RealPoint.wrap( new double[] { + imp.getWidth() / 2., + imp.getHeight() / 2., + imp.getNSlices() / 2. + } ); + final double r1 = imp.getWidth() / 4.; + final double r2 = imp.getWidth() / 8.; + final double r3 = imp.getWidth() / 16.; + final Spot s1 = new SpotBase( center, r1, 1. ); + final Spot s2 = new SpotBase( center, r2, 1. ); + final Spot s3 = new SpotBase( center, r3, 1. ); + s1.iterable( img ).forEach( p -> p.setReal( 250. ) ); + s2.iterable( img ).forEach( p -> p.setZero() ); + s3.iterable( img ).forEach( p -> p.setReal( 250. ) ); + return imp; + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/DemoPixelIteration.java b/src/test/java/fiji/plugin/trackmate/mesh/DemoPixelIteration.java new file mode 100644 index 000000000..e8d067e66 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/DemoPixelIteration.java @@ -0,0 +1,131 @@ +/*- + * #%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.mesh; + +import java.awt.Color; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.SelectionModel; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; +import ij.gui.NewImage; +import ij.process.LUT; +import net.imagej.ImgPlus; +import net.imglib2.Cursor; +import net.imglib2.RandomAccess; +import net.imglib2.type.numeric.RealType; + +public class DemoPixelIteration +{ + + public static < T extends RealType< T > > void main( final String[] args ) + { + try + { + ImageJ.main( args ); + +// final Mesh mesh = Demo3DMesh.debugMesh( new long[] { 4, 4, 4 }, new long[] { 10, 10, 10 } ); +// final Spot s0 = SpotMesh.createSpot( mesh, 1. ); +// final Model model = new Model(); +// model.beginUpdate(); +// try +// { +// model.addSpotTo( s0, 0 ); +// } +// finally +// { +// model.endUpdate(); +// } +// final ImagePlus imp = NewImage.createByteImage( "cube", 16, 16, 16, NewImage.FILL_BLACK ); + + final String imPath = "samples/mesh/CElegansMask3DNoScale-mask-t1.tif"; + final ImagePlus imp = IJ.openImage( imPath ); + + final Settings settings = new Settings( imp ); + settings.detectorFactory = new ThresholdDetectorFactory<>(); + settings.detectorSettings = settings.detectorFactory.getDefaultSettings(); + settings.detectorSettings.put( + ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, false ); + settings.detectorSettings.put( + ThresholdDetectorFactory.KEY_INTENSITY_THRESHOLD, 100. ); + + final TrackMate trackmate = new TrackMate( settings ); + trackmate.setNumThreads( 4 ); + trackmate.execDetection(); + + final Model model = trackmate.getModel(); + final SpotCollection spots = model.getSpots(); + spots.setVisible( true ); + + final ImagePlus out = NewImage.createShortImage( "OUT", imp.getWidth(), imp.getHeight(), imp.getNSlices(), NewImage.FILL_BLACK ); + out.show(); + out.resetDisplayRange(); + + imp.show(); + imp.resetDisplayRange(); + + final double[] cal = TMUtils.getSpatialCalibration( imp ); + int i = 0; + for ( final Spot spot : model.getSpots().iterable( true ) ) + { + System.out.println( spot ); + final ImgPlus< T > img = TMUtils.rawWraps( out ); + final Cursor< T > cursor = spot.iterable( img, cal ).localizingCursor(); + final RandomAccess< T > ra = img.randomAccess(); + while ( cursor.hasNext() ) + { + cursor.fwd(); + cursor.get().setReal( 1 + i++ ); + + ra.setPosition( cursor ); + ra.get().setReal( 100 ); + } + } + + final SelectionModel sm = new SelectionModel( model ); + final DisplaySettings ds = DisplaySettingsIO.readUserDefault(); + final HyperStackDisplayer view = new HyperStackDisplayer( model, sm, imp, ds ); + view.render(); + + imp.setSlice( 19 ); + imp.resetDisplayRange(); + imp.setLut( LUT.createLutFromColor( Color.BLUE ) ); + out.setSlice( 19 ); + out.resetDisplayRange(); + System.out.println( "Done." ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/ExportMeshForDemo.java b/src/test/java/fiji/plugin/trackmate/mesh/ExportMeshForDemo.java new file mode 100644 index 000000000..1d019b057 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/ExportMeshForDemo.java @@ -0,0 +1,84 @@ +/*- + * #%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.mesh; + +import java.io.File; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.SpotMesh; +import fiji.plugin.trackmate.TrackMate; +import fiji.plugin.trackmate.detection.MaskDetectorFactory; +import fiji.plugin.trackmate.detection.ThresholdDetectorFactory; +import ij.IJ; +import ij.ImagePlus; +import net.imglib2.mesh.io.stl.STLMeshIO; + +public class ExportMeshForDemo +{ + + public static void main( final String[] args ) + { + try + { + final String filePath = "samples/mesh/CElegansMask3D.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + + final Settings settings = new Settings( imp ); + settings.detectorFactory = new MaskDetectorFactory<>(); + settings.detectorSettings = settings.detectorFactory.getDefaultSettings(); + settings.detectorSettings.put( ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS, false ); + + final TrackMate trackmate = new TrackMate( settings ); + trackmate.setNumThreads( 4 ); + trackmate.execDetection(); + + final Model model = trackmate.getModel(); + final SpotCollection spots = model.getSpots(); + spots.setVisible( true ); + + final String meshDir = "samples/mesh/io"; + for ( final File file : new File( meshDir ).listFiles() ) + if ( !file.isDirectory() ) + file.delete(); + + for ( final Spot spot : spots.iterable( true ) ) + { + final int t = spot.getFeature( Spot.FRAME ).intValue(); + final int id = spot.ID(); + final String savePath = String.format( "%s/mesh_t%2d_id_%04d.stl", meshDir, t, id ); + if ( spot instanceof SpotMesh ) + { + final SpotMesh mesh = ( SpotMesh ) spot; + STLMeshIO.save( mesh.getMesh(), savePath ); + } + } + System.out.println( "Export done." ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/MeshPlayground.java b/src/test/java/fiji/plugin/trackmate/mesh/MeshPlayground.java new file mode 100644 index 000000000..b038f6d5b --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/MeshPlayground.java @@ -0,0 +1,134 @@ +/*- + * #%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.mesh; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.joml.Matrix4f; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.util.Actions; + +import bvv.core.VolumeViewerPanel; +import bvv.core.util.MatrixMath; +import bvv.vistools.Bvv; +import bvv.vistools.BvvFunctions; +import bvv.vistools.BvvSource; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.bvv.StupidMesh; +import ij.IJ; +import ij.ImagePlus; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.Meshes; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; +import net.imglib2.mesh.impl.nio.BufferMesh; +import net.imglib2.type.Type; +import net.imglib2.type.numeric.ARGBType; + +public class MeshPlayground +{ + public static < T extends Type< T > > void main( final String[] args ) + { + final String filePath = "samples/mesh/CElegansMask3D.tif"; + final ImagePlus imp = IJ.openImage( filePath ); + + final ImgPlus< T > img = TMUtils.rawWraps( imp ); + final ImgPlus< T > c1 = ImgPlusViews.hyperSlice( img, img.dimensionIndex( Axes.CHANNEL ), 1 ); + final ImgPlus< T > t1 = ImgPlusViews.hyperSlice( c1, c1.dimensionIndex( Axes.TIME ), 0 ); + final double[] cal = TMUtils.getSpatialCalibration( t1 ); + + final BvvSource source = BvvFunctions.show( c1, "t1", + + Bvv.options() + .maxAllowedStepInVoxels( 0 ) + .renderWidth( 1024 ) + .renderHeight( 1024 ) + .preferredSize( 512, 512 ) + .sourceTransform( cal ) ); + + source.setDisplayRangeBounds( 0, 1024 ); + source.setColor( new ARGBType( 0xaaffaa ) ); + + final List< StupidMesh > meshes = new ArrayList<>(); + for ( int j = 1; j <= 3; ++j ) + { + final String fn = String.format( "samples/mesh/CElegansMask3D_%02d.stl", j ); + meshes.add( new StupidMesh( load( fn ) ) ); + } + + final VolumeViewerPanel viewer = source.getBvvHandle().getViewerPanel(); + + final AtomicBoolean showMeshes = new AtomicBoolean( true ); + viewer.setRenderScene( ( gl, data ) -> { + if ( showMeshes.get() ) + { + final Matrix4f pvm = new Matrix4f( data.getPv() ); + final Matrix4f view = MatrixMath.affine( data.getRenderTransformWorldToScreen(), new Matrix4f() ); + final Matrix4f vm = MatrixMath.screen( data.getDCam(), data.getScreenWidth(), data.getScreenHeight(), new Matrix4f() ).mul( view ); + meshes.forEach( mesh -> mesh.draw( gl, pvm, vm, false ) ); + } + } ); + + final Actions actions = new Actions( new InputTriggerConfig() ); + actions.install( source.getBvvHandle().getKeybindings(), "my-new-actions" ); + actions.runnableAction( () -> { + showMeshes.set( !showMeshes.get() ); + viewer.requestRepaint(); + }, "toggle meshes", "G" ); + + viewer.requestRepaint(); + } + + private static BufferMesh load( final String fn ) + { + BufferMesh mesh = null; + try + { + final NaiveDoubleMesh nmesh = new NaiveDoubleMesh(); + net.imglib2.mesh.io.stl.STLMeshIO.read( nmesh, new File( fn ) ); + mesh = calculateNormals( + nmesh +// Meshes.removeDuplicateVertices( nmesh, 5 ) + ); + } + catch ( final IOException e ) + { + e.printStackTrace(); + } + return mesh; + } + + private static BufferMesh calculateNormals( final Mesh mesh ) + { + final int nvertices = mesh.vertices().size(); + final int ntriangles = mesh.triangles().size(); + final BufferMesh bufferMesh = new BufferMesh( nvertices, ntriangles, true ); + Meshes.calculateNormals( mesh, bufferMesh ); + return bufferMesh; + } +} diff --git a/src/test/java/fiji/plugin/trackmate/mesh/TestEllipsoidFit.java b/src/test/java/fiji/plugin/trackmate/mesh/TestEllipsoidFit.java new file mode 100644 index 000000000..07e5b823b --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/mesh/TestEllipsoidFit.java @@ -0,0 +1,122 @@ +/*- + * #%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.mesh; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import net.imglib2.mesh.Mesh; +import net.imglib2.mesh.alg.EllipsoidFitter; +import net.imglib2.mesh.alg.EllipsoidFitter.Ellipsoid; +import net.imglib2.mesh.impl.naive.NaiveDoubleMesh; + +public class TestEllipsoidFit +{ + + @Test + public void testSimpleEllipsoids() + { + final double TOLERANCE = 1e-6; + + final double ra = 1.; + final double rb = 2.; + for ( double rc = 3.; rc < 10.; rc++ ) + { + final Mesh mesh = generateEllipsoidMesh( ra, rb, rc, -1, -1 ); + final Ellipsoid fit = EllipsoidFitter.fit( mesh ); + + final double[] arr = new double[ 3 ]; + + // Center on 0. + fit.center.localize( arr ); + assertArrayEquals( "Ellipsoid center should be close to 0.", arr, new double[] { 0., 0., 0. }, TOLERANCE ); + + // Proper radius, ordered by increasing absolute value. + assertEquals( "Smallest radius has unexpected value.", ra, fit.r1, TOLERANCE ); + assertEquals( "Mid radius has unexpected value.", rb, fit.r2, TOLERANCE ); + assertEquals( "Largest radius has unexpected value.", rc, fit.r3, TOLERANCE ); + + // Vectors, aligned with axes. + fit.ev1.localize( arr ); + for ( int d = 0; d < arr.length; d++ ) + arr[ d ] = Math.abs( arr[ d ] ); + assertArrayEquals( "Smallest eigenvector should be aligned with X axis.", arr, new double[] { 1., 0., 0. }, TOLERANCE ); + + fit.ev2.localize( arr ); + for ( int d = 0; d < arr.length; d++ ) + arr[ d ] = Math.abs( arr[ d ] ); + assertArrayEquals( "Mid eigenvector should be aligned with Y axis.", arr, new double[] { 0., 1., 0. }, TOLERANCE ); + + fit.ev3.localize( arr ); + for ( int d = 0; d < arr.length; d++ ) + arr[ d ] = Math.abs( arr[ d ] ); + assertArrayEquals( "Largest eigenvector should be aligned with Z axis.", arr, new double[] { 0., 0., 1. }, TOLERANCE ); + } + } + + private static Mesh generateEllipsoidMesh( final double ra, final double rb, final double rc, int numLongitudes, int numLatitudes ) + { + if ( numLongitudes < 4 ) + numLongitudes = 36; // Number of longitudinal divisions + if ( numLatitudes < 4 ) + numLatitudes = 18; // Number of latitudinal divisions + + final NaiveDoubleMesh mesh = new NaiveDoubleMesh(); + for ( int lat = 0; lat < numLatitudes; lat++ ) + { + final double theta1 = ( double ) lat / numLatitudes * Math.PI; + final double theta2 = ( double ) ( lat + 1 ) / numLatitudes * Math.PI; + + for ( int lon = 0; lon < numLongitudes; lon++ ) + { + final double phi1 = ( double ) lon / numLongitudes * 2 * Math.PI; + final double phi2 = ( double ) ( lon + 1 ) / numLongitudes * 2 * Math.PI; + + // Calculate the vertices of each triangle + final long p1 = addVertex( mesh, ra, rb, rc, theta1, phi1 ); + final long p2 = addVertex( mesh, ra, rb, rc, theta1, phi2 ); + final long p3 = addVertex( mesh, ra, rb, rc, theta2, phi1 ); + final long p4 = addVertex( mesh, ra, rb, rc, theta2, phi2 ); + + // Draw the triangles + addTriangle( mesh, p1, p3, p2 ); + addTriangle( mesh, p2, p3, p4 ); + } + } + return mesh; + } + + private static long addVertex( final Mesh mesh, final double ra, final double rb, final double rc, final double theta, final double phi ) + { + final double x = ra * Math.sin( theta ) * Math.cos( phi ); + final double y = rb * Math.sin( theta ) * Math.sin( phi ); + final double z = rc * Math.cos( theta ); + return mesh.vertices().add( x, y, z ); + } + + private static long addTriangle( final Mesh mesh, final long p1, final long p2, final long p3 ) + { + return mesh.triangles().add( p1, p2, p3 ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java index d07627e35..057f0e950 100644 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest.java @@ -32,6 +32,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -237,7 +238,7 @@ private SpotCollection createParallelLines() { for ( int k = 0; k < y.length; k++ ) { - final Spot spot = new Spot( x[ k ] + ran.nextGaussian() * WIDTH / 100, y[ k ] + ran.nextGaussian() * WIDTH / 100, 0, 2, k, "T_" + k + "_S_" + t ); + final Spot spot = new SpotBase( x[ k ] + ran.nextGaussian() * WIDTH / 100, y[ k ] + ran.nextGaussian() * WIDTH / 100, 0, 2, k, "T_" + k + "_S_" + t ); spots.add( spot, t ); x[ k ] += vx0[ k ]; @@ -274,7 +275,13 @@ private SpotCollection createSpots() { for ( int k = 0; k < y.length; k++ ) { - final Spot spot = new Spot( x[ k ] + ran.nextGaussian() * WIDTH / 200, y[ k ] + ran.nextGaussian() * WIDTH / 200, 0, 2, k, "T_" + k + "_S_" + t ); + final Spot spot = new SpotBase( + x[ k ] + ran.nextGaussian() * WIDTH / 200, + y[ k ] + ran.nextGaussian() * WIDTH / 200, + 0, + 2, + k, + "T_" + k + "_S_" + t ); spots.add( spot, t ); x[ k ] += vx0[ k ]; diff --git a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java index ae55af4bd..48b807e5a 100755 --- a/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java +++ b/src/test/java/fiji/plugin/trackmate/tracking/kalman/KalmanTrackerInteractiveTest3.java @@ -27,6 +27,7 @@ import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.SelectionModel; import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.SpotBase; import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.features.track.TrackIndexAnalyzer; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; @@ -115,7 +116,13 @@ private SpotCollection createSingleLine() double y = y0; for ( int t = 0; t < NFRAMES; t++ ) { - final Spot spot = new Spot( x + ran.nextGaussian() * sigma, y + ran.nextGaussian() * sigma, 0, 2, 1, "S_" + t ); + final Spot spot = new SpotBase( + x + ran.nextGaussian() * sigma, + y + ran.nextGaussian() * sigma, + 0, + 2, + 1, + "S_" + t ); spots.add( spot, t ); x += vx0; diff --git a/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java b/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java index 74b27db3d..1fcd102f3 100644 --- a/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java +++ b/src/test/java/fiji/plugin/trackmate/util/SpotRoiIterableTest.java @@ -61,7 +61,7 @@ public void testIterationPolygon() 84, 85 }; final TIntArrayList vals = new TIntArrayList(); - final IterableInterval< UnsignedByteType > iterable = SpotUtil.iterable( spot, new ImgPlus<>( img ) ); + final IterableInterval< UnsignedByteType > iterable = spot.iterable( new ImgPlus<>( img ) ); final Cursor< UnsignedByteType > cursor = iterable.cursor(); while ( cursor.hasNext() ) { @@ -85,7 +85,7 @@ public static void main( final String[] args ) final double[] yp = new double[] { 1.5, 5, 8.8, 5 }; final Spot spot = SpotRoi.createSpot( xp, yp, 1. ); - final IterableInterval< UnsignedByteType > iterable = SpotUtil.iterable( spot, new ImgPlus<>( img ) ); + final IterableInterval< UnsignedByteType > iterable = spot.iterable( new ImgPlus<>( img ) ); final Cursor< UnsignedByteType > cursor = iterable.cursor(); while ( cursor.hasNext() ) {