diff --git a/pom.xml b/pom.xml index 03b803051..dec20d441 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ sc.fiji TrackMate - 7.14.1-SNAPSHOT + 8.0.0-SNAPSHOT TrackMate TrackMate plugin for Fiji. @@ -147,14 +147,6 @@ 2.5.2 0.11.1 - - 7.1.0 - 4.0.3 - 0.15.0 - 1.0.0-beta-18 - 0.15.3 - 2.0.2 - 10.6.0 @@ -163,6 +155,10 @@ sc.fiji fiji-lib + + sc.fiji + labkit-ui + @@ -215,10 +211,10 @@ com.github.vlsi.mxgraph jgraphx - + com.itextpdf itextpdf diff --git a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java index 840076e19..78aee0a46 100644 --- a/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java +++ b/src/main/java/fiji/plugin/trackmate/LoadTrackMatePlugIn.java @@ -24,6 +24,7 @@ import static fiji.plugin.trackmate.gui.Icons.TRACKMATE_ICON; import java.awt.Color; +import java.awt.Dimension; import java.io.File; import javax.swing.JFrame; @@ -65,8 +66,6 @@ public class LoadTrackMatePlugIn extends TrackMatePlugIn @Override public void run( final String filePath ) { -// GuiUtils.setSystemLookAndFeel(); - final Logger logger = Logger.IJ_LOGGER; File file; if ( null == filePath || filePath.length() == 0 ) @@ -215,6 +214,8 @@ public void run( final String filePath ) frame.setIconImage( TRACKMATE_ICON.getImage() ); GuiUtils.positionWindow( frame, settings.imp.getWindow() ); frame.setVisible( true ); + final Dimension size = frame.getSize(); + frame.setSize( size.width, size.height + 1 ); // Text final LogPanelDescriptor2 logDescriptor = ( LogPanelDescriptor2 ) sequence.logDescriptor(); @@ -230,7 +231,7 @@ public void run( final String filePath ) final String warning = reader.getErrorMessage(); if ( !warning.isEmpty() ) { - logger2.log( "Warnings occured during reading the file:\n" + logger2.log( "Warnings occurred during reading the file:\n" + "--------------------\n" + warning + "--------------------\n", @@ -292,11 +293,12 @@ protected TmXmlReader createReader( final File lFile ) public static void main( final String[] args ) { + GuiUtils.setSystemLookAndFeel(); ImageJ.main( args ); final LoadTrackMatePlugIn plugIn = new LoadTrackMatePlugIn(); - plugIn.run( null ); +// plugIn.run( null ); // plugIn.run( "samples/FakeTracks.xml" ); -// plugIn.run( "samples/MAX_Merged.xml" ); + plugIn.run( "samples/MAX_Merged.xml" ); // plugIn.run( "c:/Users/tinevez/Development/TrackMateWS/TrackMate-Cellpose/samples/R2_multiC.xml" ); // plugIn.run( "/Users/tinevez/Desktop/230901_DeltaRcsB-ZipA-mCh_timestep5min_Stage9_reg/230901_DeltaRcsB-ZipA-mCh_timestep5min_Stage9_reg_merge65.xml" ); } diff --git a/src/main/java/fiji/plugin/trackmate/TrackMate.java b/src/main/java/fiji/plugin/trackmate/TrackMate.java index f463dd7d8..1e5caf68a 100644 --- a/src/main/java/fiji/plugin/trackmate/TrackMate.java +++ b/src/main/java/fiji/plugin/trackmate/TrackMate.java @@ -107,6 +107,12 @@ public class TrackMate implements Benchmark, MultiThreaded, Algorithm, Named, Ca public TrackMate( final Settings settings ) { this( new Model(), settings ); + if ( settings.imp != null && settings.imp.getCalibration() != null ) + { + final String spaceUnits = settings.imp.getCalibration().getXUnit(); + final String timeUnits = settings.imp.getCalibration().getTimeUnit(); + model.setPhysicalUnits( spaceUnits, timeUnits ); + } } public TrackMate( final Model model, final Settings settings ) diff --git a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java index b01746bf7..ebb380e6b 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DetectionUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -39,8 +39,8 @@ import fiji.plugin.trackmate.SpotCollection; import fiji.plugin.trackmate.TrackMate; import fiji.plugin.trackmate.detection.util.MedianFilter2D; -import fiji.plugin.trackmate.util.Threads; import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.util.Threads; import ij.ImagePlus; import net.imagej.ImgPlus; import net.imagej.axis.Axes; @@ -69,7 +69,6 @@ import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.real.FloatType; import net.imglib2.util.Intervals; -import net.imglib2.util.Util; import net.imglib2.view.IntervalView; import net.imglib2.view.Views; @@ -82,7 +81,7 @@ public class DetectionUtils * This method returns immediately and execute the detection in a separate * thread. It executes the detection in one frame only and writes the * results in the specified model object. - * + * * @param model * the model to write detection results in. * @param settings @@ -167,7 +166,7 @@ public static final void preview( * Returns true if the specified image is 2D. It can have * multiple channels and multiple time-points; this method only looks at * whether several Z-slices can be found. - * + * * @param img * the image. * @return true if the image is 2D, regardless of time and @@ -189,7 +188,7 @@ public static final boolean is2D( final ImagePlus imp ) * radius specified using calibrated units. The specified calibration * is used to determine the dimensionality of the kernel and to map it on a * pixel grid. - * + * * @param radius * the blob radius (in image unit). * @param nDims @@ -277,7 +276,7 @@ public static final < T extends RealType< T > > Img< FloatType > copyToFloatImg( final RandomAccess< T > in = Views.zeroMin( Views.interval( img, interval ) ).randomAccess(); final Cursor< FloatType > out = output.cursor(); final RealFloatConverter< T > c = new RealFloatConverter<>(); - + while ( out.hasNext() ) { out.fwd(); @@ -344,7 +343,7 @@ public static final < T extends RealType< T > > List< Spot > findLocalMaxima( * Find maxima. */ - final T val = Util.getTypeFromInterval( source ).createVariable(); + final T val = source.getType().createVariable(); val.setReal( threshold ); final LocalNeighborhoodCheck< Point, T > localNeighborhoodCheck = new LocalExtrema.MaximumCheck<>( val ); final IntervalView< T > dogWithBorder = Views.interval( Views.extendMirrorSingle( source ), Intervals.expand( source, 1 ) ); @@ -492,7 +491,7 @@ else if ( source.numDimensions() > 1 ) /** * Return a view of the specified input image, at the specified channel * (0-based) and the specified frame (0-based too). - * + * * @param * the type of the input image. * @param img @@ -524,7 +523,7 @@ public static final < T extends Type< T > > RandomAccessibleInterval< T > prepar /** * Normalize the pixel value of an image between 0 and 1. - * + * * @param * the type of pixels in the image. Must extend {@link RealType}. * @param input diff --git a/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java b/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java index eec3adca7..6362ccaa4 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/DogDetector.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -23,7 +23,6 @@ import net.imglib2.Cursor; import net.imglib2.Interval; -import net.imglib2.IterableInterval; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; import net.imglib2.algorithm.dog.DifferenceOfGaussian; @@ -117,10 +116,8 @@ public boolean process() e.printStackTrace(); } - final IterableInterval< FloatType > dogIterable = Views.iterable( dog ); - final IterableInterval< FloatType > tmpIterable = Views.iterable( dog2 ); - final Cursor< FloatType > dogCursor = dogIterable.cursor(); - final Cursor< FloatType > tmpCursor = tmpIterable.cursor(); + final Cursor< FloatType > dogCursor = dog.cursor(); + final Cursor< FloatType > tmpCursor = dog2.cursor(); while ( dogCursor.hasNext() ) dogCursor.next().sub( tmpCursor.next() ); diff --git a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java index 39ec68d35..3c54dceef 100644 --- a/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java +++ b/src/main/java/fiji/plugin/trackmate/detection/LabelImageDetector.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -101,9 +101,7 @@ public boolean process() { final long start = System.currentTimeMillis(); final RandomAccessibleInterval< T > rai = Views.interval( input, interval ); - final T type = Util.getTypeFromInterval( rai ); - - if ( type instanceof IntegerType ) + if ( rai.getType() instanceof IntegerType ) { processIntegerImg( ( RandomAccessibleInterval ) Views.zeroMin( rai ) ); } @@ -126,7 +124,7 @@ private < R extends IntegerType< R > > void processIntegerImg( final RandomAcces { // Get all labels. final AtomicInteger max = new AtomicInteger( 0 ); - Views.iterable( rai ).forEach( p -> { + rai.forEach( p -> { final int val = p.getInteger(); if ( val != 0 && val > max.get() ) max.set( val ); diff --git a/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java b/src/main/java/fiji/plugin/trackmate/detection/MaskUtils.java index 2786afec3..213e9e424 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 * . @@ -23,18 +23,24 @@ 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.util.Threads; -import ij.ImagePlus; import ij.gui.PolygonRoi; -import ij.measure.Measurements; import ij.process.FloatPolygon; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; import net.imglib2.Cursor; import net.imglib2.Interval; +import net.imglib2.IterableInterval; import net.imglib2.RandomAccess; import net.imglib2.RandomAccessible; import net.imglib2.RandomAccessibleInterval; @@ -46,15 +52,13 @@ import net.imglib2.histogram.Real1dBinMapper; import net.imglib2.img.Img; import net.imglib2.img.ImgFactory; -import net.imglib2.img.display.imagej.ImageJFunctions; import net.imglib2.roi.labeling.ImgLabeling; import net.imglib2.roi.labeling.LabelRegion; -import net.imglib2.roi.labeling.LabelRegionCursor; import net.imglib2.roi.labeling.LabelRegions; import net.imglib2.type.BooleanType; import net.imglib2.type.logic.BitType; +import net.imglib2.type.logic.BoolType; import net.imglib2.type.numeric.IntegerType; -import net.imglib2.type.numeric.NumericType; import net.imglib2.type.numeric.RealType; import net.imglib2.type.numeric.integer.IntType; import net.imglib2.util.Util; @@ -77,13 +81,13 @@ public class MaskUtils public static final < T extends RealType< T > > double otsuThreshold( final RandomAccessibleInterval< T > img ) { // Min & max - final T t = Util.getTypeFromInterval( img ); + final T t = img.getType(); final T max = t.createVariable(); max.setReal( Double.NEGATIVE_INFINITY ); final T min = t.createVariable(); min.setReal( Double.POSITIVE_INFINITY ); - for ( final T pixel : Views.iterable( img ) ) + for ( final T pixel : img ) { if ( pixel.compareTo( min ) < 0 ) min.set( pixel ); @@ -94,7 +98,7 @@ public static final < T extends RealType< T > > double otsuThreshold( final Rand // Histogram. final Real1dBinMapper< T > mapper = new Real1dBinMapper<>( min.getRealDouble(), max.getRealDouble(), 256, false ); - final Histogram1d< T > histogram = new Histogram1d<>( Views.iterable( img ), mapper ); + final Histogram1d< T > histogram = new Histogram1d<>( img, mapper ); // Threshold. final long k = getThreshold( histogram ); @@ -173,7 +177,7 @@ public static final long getThreshold( final Histogram1d< ? > hist ) /** * Creates a zero-min label image from a thresholded input image. - * + * * @param * the type of the input image. Must be real, scalar. * @param input @@ -225,7 +229,7 @@ public static final < T extends RealType< T > > ImgLabeling< Integer, IntType > * Creates spots from a grayscale image, thresholded to create a mask. A * spot is created for each connected-component of the mask, with a size * that matches the mask size. - * + * * @param * the type of the input image. Must be real, scalar. * @param input @@ -257,7 +261,7 @@ public static < T extends RealType< T > > List< Spot > fromThreshold( /** * Creates spots from a label image. - * + * * @param * the type that backs-up the labeling. * @param labeling @@ -281,7 +285,7 @@ public static < R extends IntegerType< R > > List< Spot > fromLabeling( while ( iterator.hasNext() ) { final LabelRegion< Integer > region = iterator.next(); - final Cursor< Void > cursor = region.inside().localizingCursor(); + final Cursor< BoolType > cursor = region.localizingCursor(); final int[] cursorPos = new int[ labeling.numDimensions() ]; final long[] sum = new long[ 3 ]; while ( cursor.hasNext() ) @@ -319,7 +323,7 @@ public static < R extends IntegerType< R > > List< Spot > fromLabeling( * spot is created for each connected-component of the mask, with a size * that matches the mask size. The quality of the spots is read from another * image, by taking the max pixel value of this image with the ROI. - * + * * @param * the type of the input image. Must be real, scalar. * @param input @@ -359,7 +363,7 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > while ( iterator.hasNext() ) { final LabelRegion< Integer > region = iterator.next(); - final Cursor< Void > cursor = region.inside().localizingCursor(); + final Cursor< BoolType > cursor = region.localizingCursor(); final int[] cursorPos = new int[ labeling.numDimensions() ]; final long[] sum = new long[ 3 ]; double quality = Double.NEGATIVE_INFINITY; @@ -407,7 +411,7 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > * connected-component of the mask, with a size that matches the mask size. * The quality of the spots is read from another image, by taking the max * pixel value of this image with the ROI. - * + * * @param * the type of the input image. Must be real, scalar. * @param @@ -429,7 +433,7 @@ public static < T extends RealType< T >, R extends RealType< R > > List< Spot > * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static final < T extends RealType< T >, S extends NumericType< S > > List< Spot > fromThresholdWithROI( + public static final < T extends RealType< T >, S extends RealType< S > > List< Spot > fromThresholdWithROI( final RandomAccessible< T > input, final Interval interval, final double[] calibration, @@ -440,7 +444,7 @@ public static final < T extends RealType< T >, S extends NumericType< S > > List { 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 ); @@ -448,9 +452,9 @@ public static final < T extends RealType< T >, S extends NumericType< S > > List /** * Creates spots with ROIs from a 2D label image. The quality - * value is read from a secondary image, byt taking the max value in each + * value is read from a secondary image, by taking the max value in each * ROI. - * + * * @param * the type that backs-up the labeling. * @param @@ -469,7 +473,51 @@ public static final < T extends RealType< T >, S extends NumericType< S > > List * the image in which to read the quality value. * @return a list of spots, with ROI. */ - public static < R extends IntegerType< R >, S extends NumericType< S > > List< Spot > fromLabelingWithROI( + public static < R extends IntegerType< R >, S extends RealType< S > > List< Spot > fromLabelingWithROI( + final ImgLabeling< Integer, R > labeling, + final Interval interval, + final double[] calibration, + final boolean simplify, + final RandomAccessibleInterval< S > qualityImage ) + { + final Map< Integer, List< Spot > > map = fromLabelingWithROIMap( labeling, interval, calibration, simplify, qualityImage ); + final List spots = new ArrayList<>(); + for ( final List< Spot > s : map.values() ) + spots.addAll( s ); + + return spots; + } + + /** + * Creates spots with ROIs from a 2D label image. The quality + * value is read from a secondary image, by taking the max value in each + * ROI. + *

+ * The spots are returned in a map, where the key is the integer value of + * the label they correspond to in the label image. Because one spot + * corresponds to one connected component in the label image, there might be + * several spots for a label, hence the values of the map are list of spots. + * + * @param + * the type that backs-up the labeling. + * @param + * the type of the quality image. Must be real, scalar. + * @param labeling + * the labeling, must be zero-min and 2D.. + * @param interval + * the interval, used to reposition the spots from the zero-min + * labeling to the proper coordinates. + * @param calibration + * the physical calibration. + * @param simplify + * if true the polygon will be post-processed to be + * smoother and contain less points. + * @param qualityImage + * the image in which to read the quality value. + * @return a map linking the label integer value to the list of spots, with + * ROI, it corresponds to. + */ + public static < R extends IntegerType< R >, S extends RealType< S > > Map< Integer, List< Spot > > fromLabelingWithROIMap( final ImgLabeling< Integer, R > labeling, final Interval interval, final double[] calibration, @@ -481,9 +529,14 @@ public static < R extends IntegerType< R >, S extends NumericType< S > > List< S final LabelRegions< Integer > regions = new LabelRegions< Integer >( labeling ); - // Parse regions to create polygons on boundaries. - final List< Polygon > polygons = new ArrayList<>( regions.getExistingLabels().size() ); + /* + * Map of label in the label image to a collection of polygons around + * this label. Because 1 polygon correspond to 1 connected component, + * there might be several polygons for a label. + */ + final Map< Integer, List< Polygon > > polygonsMap = new HashMap<>( regions.getExistingLabels().size() ); final Iterator< LabelRegion< Integer > > iterator = regions.iterator(); + // Parse regions to create polygons on boundaries. while ( iterator.hasNext() ) { final LabelRegion< Integer > region = iterator.next(); @@ -493,55 +546,75 @@ public static < R extends IntegerType< R >, S extends NumericType< S > > List< S for ( final Polygon polygon : pp ) polygon.translate( ( int ) region.min( 0 ), ( int ) region.min( 1 ) ); - polygons.addAll( pp ); + final Integer label = region.getLabel(); + polygonsMap.put( label, pp ); } - // Quality image. - final List< Spot > spots = new ArrayList<>( polygons.size() ); - final ImagePlus qualityImp = ( null == qualityImage ) - ? null - : ImageJFunctions.wrap( qualityImage, "QualityImage" ); + + // Storage for results. + final Map< Integer, List< Spot > > output = new HashMap<>( polygonsMap.size() ); // Simplify them and compute a quality. - for ( final Polygon polygon : polygons ) + for ( final Integer label : polygonsMap.keySet() ) { - final PolygonRoi roi = new PolygonRoi( polygon, PolygonRoi.POLYGON ); + final List< Spot > spots = new ArrayList<>( polygonsMap.size() ); + output.put( label, spots ); - // Create Spot ROI. - final PolygonRoi fRoi; - if ( simplify ) - fRoi = simplify( roi, SMOOTH_INTERVAL, DOUGLAS_PEUCKER_MAX_DISTANCE ); - else - fRoi = roi; + final List< Polygon > polygons = polygonsMap.get( label ); + for ( final Polygon polygon : polygons ) + { + final PolygonRoi roi = new PolygonRoi( polygon, PolygonRoi.POLYGON ); - // Don't include ROIs that have been shrunk to < 1 pixel. - if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) - continue; + // Create Spot ROI. + final PolygonRoi fRoi; + if ( simplify ) + fRoi = simplify( roi, SMOOTH_INTERVAL, DOUGLAS_PEUCKER_MAX_DISTANCE ); + else + fRoi = roi; - // Measure quality. - final double quality; - if ( null == qualityImp ) - { - quality = fRoi.getStatistics().area; - } - else - { - qualityImp.setRoi( fRoi ); - quality = qualityImp.getStatistics( Measurements.MIN_MAX ).max; - } + // Don't include ROIs that have been shrunk to < 1 pixel. + if ( fRoi.getNCoordinates() < 3 || fRoi.getStatistics().area <= 0. ) + continue; - final Polygon fPolygon = fRoi.getPolygon(); - final double[] xpoly = new double[ fPolygon.npoints ]; - final double[] ypoly = new double[ fPolygon.npoints ]; - for ( int i = 0; i < fPolygon.npoints; i++ ) - { - xpoly[ i ] = calibration[ 0 ] * ( interval.min( 0 ) + fPolygon.xpoints[ i ] - 0.5 ); - ypoly[ i ] = calibration[ 1 ] * ( interval.min( 1 ) + fPolygon.ypoints[ i ] - 0.5 ); - } + final Polygon fPolygon = fRoi.getPolygon(); + final double[] xpoly = new double[ fPolygon.npoints ]; + final double[] ypoly = new double[ fPolygon.npoints ]; + for ( int i = 0; i < fPolygon.npoints; i++ ) + { + xpoly[ i ] = calibration[ 0 ] * ( interval.min( 0 ) + fPolygon.xpoints[ i ] - 0.5 ); + ypoly[ i ] = calibration[ 1 ] * ( interval.min( 1 ) + fPolygon.ypoints[ i ] - 0.5 ); + } + + final Spot spot = SpotRoi.createSpot( xpoly, ypoly, -1. ); - spots.add( SpotRoi.createSpot( xpoly, ypoly, quality ) ); + // Measure quality. + final double quality; + if ( null == qualityImage ) + { + quality = fRoi.getStatistics().area; + } + else + { + final String name = "QualityImage"; + final AxisType[] axes = new AxisType[] { Axes.X, Axes.Y }; + final double[] cal = new double[] { calibration[ 0 ], calibration[ 1 ] }; + final String[] units = new String[] { "unitX", "unitY" }; + final ImgPlus< S > qualityImgPlus = new ImgPlus<>( ImgPlus.wrapToImg( qualityImage ), name, axes, cal, units ); + final IterableInterval< S > iterable = SpotUtil.iterable( spot, qualityImgPlus ); + double max = Double.NEGATIVE_INFINITY; + for ( final S s : iterable ) + { + final double val = s.getRealDouble(); + if ( val > max ) + max = val; + } + quality = max; + } + spot.putFeature( Spot.QUALITY, quality ); + spots.add( spot ); + } } - return spots; + return output; } private static final double distanceSquaredBetweenPoints( final double vx, final double vy, final double wx, final double wy ) @@ -624,7 +697,7 @@ private static final void douglasPeucker( final List< double[] > list, final int * the number of points in a curve that is approximated by a series of * points. *

- * + * * @see Ramer–Douglas–Peucker * Algorithm (Wikipedia) @@ -637,7 +710,7 @@ private static final void douglasPeucker( final List< double[] > list, final int */ public static final List< double[] > douglasPeucker( final List< double[] > list, final double epsilon ) { - final List< double[] > resultList = new ArrayList< >(); + final List< double[] > resultList = new ArrayList<>(); douglasPeucker( list, 0, list.size(), epsilon, resultList ); return resultList; } @@ -665,7 +738,7 @@ public static final PolygonRoi simplify( final PolygonRoi roi, final double smoo /** * Start at 1. - * + * * @return a new iterator that goes like 1, 2, 3, ... */ public static final Iterator< Integer > labelGenerator() @@ -697,7 +770,7 @@ public boolean hasNext() * Warning: cannot deal with holes, they are simply ignored. *

* Copied and adapted from ImageJ1 code by Wayne Rasband. - * + * * @param * the type of the mask. * @param mask diff --git a/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java b/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java index 6d2ca0142..ee0d0b599 100644 --- a/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java +++ b/src/main/java/fiji/plugin/trackmate/features/FeatureFilter.java @@ -21,6 +21,9 @@ */ package fiji.plugin.trackmate.features; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + /** * A helper class to store a feature filter. It is just made of 3 public fields. *

@@ -55,4 +58,31 @@ public String toString() return str; } + @Override + public boolean equals( final Object obj ) + { + if ( obj == null ) + return false; + if ( obj == this ) + return true; + if ( obj.getClass() != getClass() ) + return false; + final FeatureFilter other = ( FeatureFilter ) obj; + return new EqualsBuilder() + .append( feature, other.feature ) + .append( value, other.value ) + .append( isAbove, other.isAbove ) + .isEquals(); + } + + @Override + public int hashCode() + { + return new HashCodeBuilder( 17, 37 ) + .append( feature ) + .append( value ) + .append( isAbove ) + .toHashCode(); + } + } diff --git a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java index 2bfbee9f9..a9055ee1c 100644 --- a/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java +++ b/src/main/java/fiji/plugin/trackmate/graph/GraphUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -35,6 +35,7 @@ import org.jgrapht.alg.util.NeighborCache; import org.jgrapht.graph.DefaultWeightedEdge; import org.jgrapht.graph.SimpleDirectedWeightedGraph; +import org.jgrapht.graph.SimpleWeightedGraph; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.TrackModel; @@ -42,6 +43,47 @@ public class GraphUtils { + /** + * Converts a {@link SimpleDirectedWeightedGraph} to a + * {@link SimpleWeightedGraph}. + * + * @param directedGraph + * the {@link SimpleDirectedWeightedGraph} to be converted + * @return a {@link SimpleWeightedGraph} with the same vertices and edges as + * the input graph, but with undirected edges and the maximum weight + * between any two vertices + */ + public static < V, E > SimpleWeightedGraph< V, E > convertToSimpleWeightedGraph( final SimpleDirectedWeightedGraph< V, E > directedGraph ) + { + @SuppressWarnings( "unchecked" ) + final Class< E > edgeClass = ( Class< E > ) directedGraph.getEdgeSupplier().get().getClass(); + final SimpleWeightedGraph< V, E > undirectedGraph = new SimpleWeightedGraph< V, E >( edgeClass ); + + // Add vertices from the directed graph + for ( final V vertex : directedGraph.vertexSet() ) + undirectedGraph.addVertex( vertex ); + + // Add edges from the directed graph + for ( final E edge : directedGraph.edgeSet() ) + { + final V source = directedGraph.getEdgeSource( edge ); + final V target = directedGraph.getEdgeTarget( edge ); + final double weight = directedGraph.getEdgeWeight( edge ); + + if ( undirectedGraph.containsEdge( source, target ) ) + { + final double existingWeight = undirectedGraph.getEdgeWeight( undirectedGraph.getEdge( source, target ) ); + undirectedGraph.setEdgeWeight( undirectedGraph.getEdge( source, target ), Math.max( existingWeight, weight ) ); + } + else + { + final E newEdge = undirectedGraph.addEdge( source, target ); + undirectedGraph.setEdgeWeight( newEdge, weight ); + } + } + return undirectedGraph; + } + /** * @return a pretty-print string representation of a {@link TrackModel}, as * long it is a tree (each spot must not have more than one @@ -312,7 +354,7 @@ public void compute( final Spot input, final int[] output ) * Find root spots & first spots Roots are spots without any ancestors. * There might be more than one per track. First spots are the first * root found in a track. There is only one per track. - * + * * By the way we compute the largest spot name */ diff --git a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java index a2d82584d..d2c38ee2d 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java +++ b/src/main/java/fiji/plugin/trackmate/gui/GuiUtils.java @@ -87,6 +87,12 @@ public static final void selectAllOnFocus( final JTextField tf ) tf.addFocusListener( selectAllFocusListener ); } + public static final void setFont( final JComponent panel, final Font font ) + { + for ( final Component c : panel.getComponents() ) + c.setFont( font ); + } + /** * Returns the black color or white color depending on the specified * background color, to ensure proper readability of the text on said @@ -394,7 +400,7 @@ public static final ImageIcon scaleImage( final ImageIcon icon, final int w, fin nw = ( icon.getIconWidth() * nh ) / icon.getIconHeight(); } - return new ImageIcon( icon.getImage().getScaledInstance( nw, nh, Image.SCALE_DEFAULT ) ); + return new ImageIcon( icon.getImage().getScaledInstance( nw, nh, Image.SCALE_SMOOTH ) ); } public static URL getResource( final String name, final Class< ? > clazz ) diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java index 57a48197d..85ddbf7c2 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/ConfigureViewsPanel.java @@ -50,10 +50,13 @@ import javax.swing.border.LineBorder; import fiji.plugin.trackmate.gui.GuiUtils; +import fiji.plugin.trackmate.gui.Icons; import fiji.plugin.trackmate.gui.displaysettings.ConfigTrackMateDisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.TrackDisplayMode; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings.UpdateListener; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.util.WrapLayout; /** * A configuration panel used to tune the aspect of spots and tracks in multiple @@ -79,7 +82,8 @@ public ConfigureViewsPanel( final String spaceUnits, final Action launchTrackSchemeAction, final Action showTrackTablesAction, - final Action showSpotTableAction ) + final Action showSpotTableAction, + final Action launchLabKitAction ) { this.setPreferredSize( new Dimension( 300, 521 ) ); this.setSize( 300, 500 ); @@ -326,7 +330,7 @@ public ConfigureViewsPanel( final GridBagConstraints gbcPanelDrawingZDepth = new GridBagConstraints(); gbcPanelDrawingZDepth.gridwidth = 2; gbcPanelDrawingZDepth.insets = new Insets( 0, 5, 5, 5 ); - gbcPanelDrawingZDepth.fill = GridBagConstraints.BOTH; + gbcPanelDrawingZDepth.fill = GridBagConstraints.HORIZONTAL; gbcPanelDrawingZDepth.gridx = 0; gbcPanelDrawingZDepth.gridy = 5; add( panelDrawingZDepth, gbcPanelDrawingZDepth ); @@ -349,6 +353,7 @@ public ConfigureViewsPanel( */ final JPanel panelButtons = new JPanel(); + panelButtons.setLayout( new WrapLayout() ); // TrackScheme button. final JButton btnShowTrackScheme = new JButton( launchTrackSchemeAction ); @@ -363,7 +368,26 @@ public ConfigureViewsPanel( final JButton btnShowSpotTable = new JButton( showSpotTableAction ); panelButtons.add( btnShowSpotTable ); btnShowSpotTable.setFont( FONT ); + + // Labkit button. + // Is labkit available? + if ( TMUtils.isClassPresent( "sc.fiji.labkit.ui.LabkitFrame" ) ) + { + final JButton btnLabKit = new JButton( launchLabKitAction ); + btnLabKit.setFont( FONT ); + btnLabKit.setText( "Launch spot editor" ); + btnLabKit.setIcon( Icons.PENCIL_ICON ); + btnLabKit.setToolTipText( "" + + "Launch the Labkit editor to edit spot segmentation
" + + "on the time-point currently displayed in the main
" + + "view." + + "

" + + "Shift + click will launch the editor on all the
" + + "time-points in the movie." ); + panelButtons.add( btnLabKit ); + } + panelButtons.setSize( new Dimension( 300, 1 ) ); final GridBagConstraints gbcPanelButtons = new GridBagConstraints(); gbcPanelButtons.gridwidth = 2; gbcPanelButtons.anchor = GridBagConstraints.SOUTH; @@ -371,6 +395,7 @@ public ConfigureViewsPanel( gbcPanelButtons.gridx = 0; gbcPanelButtons.gridy = 7; add( panelButtons, gbcPanelButtons ); + setSize( new Dimension( 300, 1 ) ); /* * Listeners & co. diff --git a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java index 379accbbb..9f9447723 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java +++ b/src/main/java/fiji/plugin/trackmate/gui/components/tracker/NearestNeighborTrackerSettingsPanel.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -23,15 +23,18 @@ import static fiji.plugin.trackmate.gui.Fonts.BIG_FONT; import static fiji.plugin.trackmate.gui.Fonts.FONT; -import static fiji.plugin.trackmate.gui.Fonts.TEXTFIELD_DIMENSION; import static fiji.plugin.trackmate.tracking.TrackerKeys.KEY_LINKING_MAX_DISTANCE; -import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; import java.util.HashMap; import java.util.Map; +import javax.swing.BorderFactory; import javax.swing.JFormattedTextField; import javax.swing.JLabel; +import javax.swing.JTextField; import javax.swing.SwingConstants; import fiji.plugin.trackmate.gui.GuiUtils; @@ -44,12 +47,6 @@ public class NearestNeighborTrackerSettingsPanel extends ConfigurationPanel private JFormattedTextField maxDistField; - private JLabel labelTrackerDescription; - - private JLabel labelUnits; - - private JLabel labelTracker; - private final String infoText; private final String trackerName; @@ -80,44 +77,75 @@ public void setSettings( final Map< String, Object > settings ) private void initGUI() { + setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) ); - setLayout( null ); + final GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[] { 164, 40, 54, 0 }; + gridBagLayout.rowHeights = new int[] { 30, 40, 225, 30, 60 }; + gridBagLayout.columnWeights = new double[] { 1.0, 0.0, 0.0, Double.MIN_VALUE }; + gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 1.0, 0.0, Double.MIN_VALUE }; + setLayout( gridBagLayout ); final JLabel lblSettingsForTracker = new JLabel( "Settings for tracker:" ); - lblSettingsForTracker.setBounds( 10, 11, 280, 20 ); lblSettingsForTracker.setFont( FONT ); - add( lblSettingsForTracker ); - - labelTracker = new JLabel( trackerName ); + final GridBagConstraints gbcLblSettingsForTracker = new GridBagConstraints(); + gbcLblSettingsForTracker.fill = GridBagConstraints.BOTH; + gbcLblSettingsForTracker.insets = new Insets( 0, 0, 5, 0 ); + gbcLblSettingsForTracker.gridwidth = 3; + gbcLblSettingsForTracker.gridx = 0; + gbcLblSettingsForTracker.gridy = 0; + add( lblSettingsForTracker, gbcLblSettingsForTracker ); + + final JLabel labelTracker = new JLabel( trackerName ); labelTracker.setFont( BIG_FONT ); labelTracker.setHorizontalAlignment( SwingConstants.CENTER ); - labelTracker.setBounds( 10, 42, 280, 20 ); - add( labelTracker ); - - labelTrackerDescription = new JLabel( "" ); - labelTrackerDescription.setFont( FONT.deriveFont( Font.ITALIC ) ); - labelTrackerDescription.setBounds( 10, 67, 280, 225 ); - labelTrackerDescription.setText( infoText.replace( "
", "" ).replace( "

", "

" ).replace( "", "

" ) ); - add( labelTrackerDescription ); + final GridBagConstraints gbcLabelTracker = new GridBagConstraints(); + gbcLabelTracker.fill = GridBagConstraints.BOTH; + gbcLabelTracker.insets = new Insets( 0, 0, 5, 0 ); + gbcLabelTracker.gridwidth = 3; + gbcLabelTracker.gridx = 0; + gbcLabelTracker.gridy = 1; + add( labelTracker, gbcLabelTracker ); + + final GridBagConstraints gbcLabelTrackerDescription = new GridBagConstraints(); + gbcLabelTrackerDescription.anchor = GridBagConstraints.NORTH; + gbcLabelTrackerDescription.fill = GridBagConstraints.BOTH; + gbcLabelTrackerDescription.insets = new Insets( 0, 0, 5, 0 ); + gbcLabelTrackerDescription.gridwidth = 3; + gbcLabelTrackerDescription.gridx = 0; + gbcLabelTrackerDescription.gridy = 2; + add( GuiUtils.textInScrollPanel( GuiUtils.infoDisplay( infoText ) ), gbcLabelTrackerDescription ); final JLabel lblMaximalLinkingDistance = new JLabel( "Maximal linking distance: " ); lblMaximalLinkingDistance.setFont( FONT ); - lblMaximalLinkingDistance.setBounds( 10, 314, 164, 20 ); - add( lblMaximalLinkingDistance ); + final GridBagConstraints gbcLblMaximalLinkingDistance = new GridBagConstraints(); + gbcLblMaximalLinkingDistance.fill = GridBagConstraints.BOTH; + gbcLblMaximalLinkingDistance.insets = new Insets( 0, 0, 0, 5 ); + gbcLblMaximalLinkingDistance.gridx = 0; + gbcLblMaximalLinkingDistance.gridy = 3; + add( lblMaximalLinkingDistance, gbcLblMaximalLinkingDistance ); maxDistField = new JFormattedTextField( 15. ); + maxDistField.setHorizontalAlignment( JTextField.CENTER ); maxDistField.setFont( FONT ); - maxDistField.setBounds( 184, 316, 62, 16 ); - maxDistField.setSize( TEXTFIELD_DIMENSION ); - add( maxDistField ); - - labelUnits = new JLabel( spaceUnits ); - labelUnits.setFont( FONT ); - labelUnits.setBounds( 236, 314, 34, 20 ); - add( labelUnits ); + final GridBagConstraints gbcMaxDistField = new GridBagConstraints(); + gbcMaxDistField.fill = GridBagConstraints.BOTH; + gbcMaxDistField.insets = new Insets( 0, 0, 0, 5 ); + gbcMaxDistField.gridx = 1; + gbcMaxDistField.gridy = 3; + add( maxDistField, gbcMaxDistField ); // Select text-fields content on focus. GuiUtils.selectAllOnFocus( maxDistField ); + + final JLabel labelUnits = new JLabel( spaceUnits ); + labelUnits.setFont( FONT ); + final GridBagConstraints gbcLabelUnits = new GridBagConstraints(); + gbcLabelUnits.anchor = GridBagConstraints.WEST; + gbcLabelUnits.fill = GridBagConstraints.VERTICAL; + gbcLabelUnits.gridx = 2; + gbcLabelUnits.gridy = 3; + add( labelUnits, gbcLabelUnits ); } @Override diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java new file mode 100644 index 000000000..6d2e6fde6 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/ImpBdvShowable.java @@ -0,0 +1,257 @@ +package fiji.plugin.trackmate.gui.editor; + +import java.awt.Color; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import bdv.tools.brightness.ConverterSetup; +import bdv.util.AxisOrder; +import bdv.util.BdvFunctions; +import bdv.util.BdvOptions; +import bdv.util.BdvStackSource; +import bdv.viewer.DisplayMode; +import bdv.viewer.SourceAndConverter; +import bdv.viewer.SynchronizedViewerState; +import bdv.viewer.ViewerState; +import fiji.plugin.trackmate.util.TMUtils; +import ij.CompositeImage; +import ij.IJ; +import ij.ImagePlus; +import ij.gui.Roi; +import ij.process.LUT; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.FinalInterval; +import net.imglib2.Interval; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.NumericType; +import net.imglib2.view.Views; +import sc.fiji.labkit.ui.bdv.BdvShowable; +import sc.fiji.labkit.ui.inputimage.ImgPlusViewsOld; + +/** + * A {@link BdvShowable} from a {@link ImgPlus}, but with channel colors, min + * and max, channel visibility and display mode taken from a specified + * {@link ImagePlus}. + *

+ * Adapted from Matthias Arzt' ImgPlusBdvShowable, reusing code I made for + * Mastodon support of IJ ImagePlus. + * + * @author Jean-Yves Tinevez + */ +public class ImpBdvShowable implements BdvShowable +{ + + /** + * Returns a new {@link BdvShowable} that wraps the specified + * {@link ImagePlus}. The LUT and display settings are read from the + * {@link ImagePlus}. + * + * @param + * the pixel type. + * @param imp + * the {@link ImagePlus} to wrap and read LUT and display + * settings from. + * @return a new {@link BdvShowable} + */ + public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImagePlus imp ) + { + @SuppressWarnings( "unchecked" ) + final ImgPlus< T > src = TMUtils.rawWraps( imp ); + if ( src.dimensionIndex( Axes.CHANNEL ) < 0 ) + Views.addDimension( src ); + return fromImp( src, imp ); + } + + /** + * Returns a new {@link BdvShowable} for the specified image, but using the + * LUT and display settings of the specified {@link ImagePlus}. + * + * @param + * the pixel type. + * @param frame + * the image to wrap in a {@link BdvShowable}. + * @param imp + * the {@link ImagePlus} to read LUT and display settings from. + * @return a new {@link BdvShowable} + */ + public static < T extends NumericType< T > > ImpBdvShowable fromImp( final ImgPlus< T > frame, final ImagePlus imp ) + { + return new ImpBdvShowable( prepareImage( frame ), imp ); + } + + private final ImagePlus imp; + + private final ImgPlus< ? extends NumericType< ? > > image; + + private Interval interval; + + ImpBdvShowable( final ImgPlus< ? extends NumericType< ? > > image, final ImagePlus imp ) + { + this.image = image; + this.imp = imp; + final Roi roi = imp.getRoi(); + if ( roi == null ) + { + this.interval = image; + } + else + { + final long[] min = image.minAsLongArray(); + final long[] max = image.maxAsLongArray(); + min[ 0 ] = roi.getBounds().x; + min[ 1 ] = roi.getBounds().y; + max[ 0 ] = roi.getBounds().x + roi.getBounds().width; + max[ 1 ] = roi.getBounds().y + roi.getBounds().height; + this.interval = new FinalInterval( min, max ); + } + } + + @Override + public Interval interval() + { + return interval; + } + + @Override + public AffineTransform3D transformation() + { + final AffineTransform3D transform = new AffineTransform3D(); + transform.set( + getCalibration( Axes.X ), 0, 0, 0, + 0, getCalibration( Axes.Y ), 0, 0, + 0, 0, getCalibration( Axes.Z ), 0 ); + return transform; + } + + @Override + public BdvStackSource< ? > show( final String title, final BdvOptions options ) + { + final String name = image.getName(); + final BdvOptions options1 = options.axisOrder( getAxisOrder() ).sourceTransform( transformation() ); + final BdvStackSource< ? extends NumericType< ? > > stackSource = BdvFunctions.show( image, name == null + ? title : name, options1 ); + + final List< ConverterSetup > converterSetups = stackSource.getConverterSetups(); + final SynchronizedViewerState state = stackSource.getBdvHandle().getViewerPanel().state(); + + transferChannelVisibility( state ); + transferChannelSettings( converterSetups ); + state.setDisplayMode( DisplayMode.FUSED ); + return stackSource; + } + + private int transferChannelVisibility( final ViewerState state ) + { + final int nChannels = imp.getNChannels(); + final CompositeImage ci = imp.isComposite() ? ( CompositeImage ) imp : null; + final List< SourceAndConverter< ? > > sources = state.getSources(); + if ( ci != null && ci.getCompositeMode() == IJ.COMPOSITE ) + { + final boolean[] activeChannels = ci.getActiveChannels(); + int numActiveChannels = 0; + for ( int i = 0; i < Math.min( activeChannels.length, nChannels ); ++i ) + { + final SourceAndConverter< ? > source = sources.get( i ); + state.setSourceActive( source, activeChannels[ i ] ); + state.setCurrentSource( source ); + numActiveChannels += activeChannels[ i ] ? 1 : 0; + } + return numActiveChannels; + } + else + { + final int activeChannel = imp.getChannel() - 1; + for ( int i = 0; i < nChannels; ++i ) + state.setSourceActive( sources.get( i ), i == activeChannel ); + state.setCurrentSource( sources.get( activeChannel ) ); + return 1; + } + } + + private void transferChannelSettings( final List< ConverterSetup > converterSetups ) + { + final int nChannels = imp.getNChannels(); + final CompositeImage ci = imp.isComposite() ? ( CompositeImage ) imp : null; + if ( ci != null ) + { + final int mode = ci.getCompositeMode(); + final boolean transferColor = mode == IJ.COMPOSITE || mode == IJ.COLOR; + for ( int c = 0; c < nChannels; ++c ) + { + final LUT lut = ci.getChannelLut( c + 1 ); + final ConverterSetup setup = converterSetups.get( c ); + if ( transferColor ) + setup.setColor( new ARGBType( lut.getRGB( 255 ) ) ); + else + setup.setColor( new ARGBType( Color.WHITE.getRGB() ) ); + setup.setDisplayRange( lut.min, lut.max ); + } + } + else + { + final double displayRangeMin = imp.getDisplayRangeMin(); + final double displayRangeMax = imp.getDisplayRangeMax(); + for ( int c = 0; c < nChannels; ++c ) + { + final ConverterSetup setup = converterSetups.get( c ); + final LUT[] luts = imp.getLuts(); + if ( luts.length != 0 ) + setup.setColor( new ARGBType( luts[ 0 ].getRGB( 255 ) ) ); + setup.setDisplayRange( displayRangeMin, displayRangeMax ); + } + } + } + + private static ImgPlus< ? extends NumericType< ? > > prepareImage( + final ImgPlus< ? extends NumericType< ? > > image ) + { + final List< AxisType > order = Arrays.asList( Axes.X, Axes.Y, Axes.Z, Axes.CHANNEL, + Axes.TIME ); + return ImgPlusViewsOld.sortAxes( labelAxes( image ), order ); + } + + private static ImgPlus< ? extends NumericType< ? > > labelAxes( + final ImgPlus< ? extends NumericType< ? > > image ) + { + if ( image.firstElement() instanceof ARGBType ) + return ImgPlusViewsOld + .fixAxes( image, Arrays.asList( Axes.X, Axes.Y, Axes.Z, Axes.TIME ) ); + if ( image.numDimensions() == 4 ) + return ImgPlusViewsOld.fixAxes( image, Arrays + .asList( Axes.X, Axes.Y, Axes.Z, Axes.TIME, Axes.CHANNEL ) ); + return ImgPlusViewsOld.fixAxes( image, Arrays.asList( Axes.X, Axes.Y, Axes.Z, + Axes.CHANNEL, Axes.TIME ) ); + } + + private double getCalibration( final AxisType axisType ) + { + final int d = image.dimensionIndex( axisType ); + if ( d == -1 ) + return 1; + return image.axis( d ).averageScale( image.min( d ), image.max( d ) ); + } + + private AxisOrder getAxisOrder() + { + final String code = IntStream + .range( 0, image.numDimensions() ) + .mapToObj( i -> image + .axis( i ) + .type() + .getLabel().substring( 0, 1 ) ) + .collect( Collectors.joining() ); + try + { + return AxisOrder.valueOf( code ); + } + catch ( final IllegalArgumentException e ) + { + return AxisOrder.DEFAULT; + } + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java new file mode 100644 index 000000000..51697fed7 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitImporter.java @@ -0,0 +1,292 @@ +package fiji.plugin.trackmate.gui.editor; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.jgrapht.graph.DefaultWeightedEdge; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Spot; +import fiji.plugin.trackmate.detection.MaskUtils; +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 + * started from. + */ +public class LabkitImporter< T extends IntegerType< T > & NativeType< T > > +{ + + private static final boolean DEBUG = false; + + private final Model model; + + private final double[] calibration; + + private final double dt; + + private final boolean simplify; + + /** + * Creates a new re-importer. + * + * @param model + * the model to add, remove or edit spots in. + * @param calibration + * the spatial calibration array: [dx, dy dz]. + * @param dt + * the frame interval. + * @param simplifyContours + * if true the contours of the spots imported and + * modified will be simplified. If false their + * contour will follow pixel edges. + */ + public LabkitImporter( + final Model model, + final double[] calibration, + final double dt, + final boolean simplifyContours ) + { + this.model = model; + this.calibration = calibration; + this.dt = dt; + this.simplify = simplifyContours; + } + + /** + * Re-import the specified label image (specified by its index image) into + * the TrackMate model. The label images must corresponds to one time-point. + *

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

+ * To properly detect modifications, the indices in the label images must + * correspond to the spot ID + 1 (index = id + 1). + * + * @param novelIndexImg + * the new index image of the labeling model, that represents the + * TrackMate model in the specified time-point after + * modification. + * @param previousIndexImg + * the previous index image, that represents the TrackMate model + * before modification. + * @param currentTimePoint + * the time-point in the TrackMate model that corresponds to the + * index image. + * @param previousSpotLabels + * the map of spots (vs the label value in the previous labeling) + * that were written in the previous index image. + */ + public void reimport( + final RandomAccessibleInterval< T > novelIndexImg, + final RandomAccessibleInterval< T > previousIndexImg, + final int currentTimePoint, + final Map< Integer, Spot > previousSpotLabels ) + { + // Collect labels corresponding to spots that have been modified. + final Set< Integer > modifiedLabels = getModifiedLabels( novelIndexImg, previousIndexImg ); + final int nModified = modifiedLabels.size(); + if ( nModified == 0 ) + return; + + model.beginUpdate(); + try + { + /* + * Get all the spots present in the new image, as a map against the + * label in the novel index image. + */ + final Map< Integer, List< Spot > > novelSpots = getSpots( novelIndexImg ); + + // Update model for those spots. + for ( final int labelValue : modifiedLabels ) + { + final Spot previousSpot = previousSpotLabels.get( labelValue ); + final List< Spot > novelSpotList = novelSpots.get( labelValue ); + if ( previousSpot == null ) + { + /* + * A new one (possible several) I cannot find in the + * previous list -> add as a new spot. + */ + if ( novelSpotList != null ) + addNewSpot( novelSpotList, currentTimePoint ); + } + else if ( novelSpotList == null || novelSpotList.isEmpty() ) + { + /* + * One I had in the previous spot list, but that has + * disappeared. Remove it. + */ + IJ.log( " - Removed spot " + str( previousSpot ) ); + model.removeSpot( previousSpot ); + } + else + { + /* + * I know of them both. Treat the case as if the previous + * spot was modified. + */ + modifySpot( novelSpotList, previousSpot, currentTimePoint ); + } + } + } + finally + { + model.endUpdate(); + } + } + + private void modifySpot( final List< Spot > novelSpotList, final Spot previousSpot, final int currentTimePoint ) + { + /* + * Hopefully there is only one spot in the novel spot list. If not we + * privilege the one closest to the previous spots. + */ + final Spot mainNovelSpot; + if ( novelSpotList.size() == 1 ) + { + mainNovelSpot = novelSpotList.get( 0 ); + } + else + { + Spot closest = null; + double minD2 = Double.POSITIVE_INFINITY; + for ( final Spot s : novelSpotList ) + { + final double d2 = s.squareDistanceTo( previousSpot ); + if ( d2 < minD2 ) + { + minD2 = d2; + closest = s; + } + } + mainNovelSpot = closest; + } + + // Add it properly. + mainNovelSpot.setName( previousSpot.getName() ); + mainNovelSpot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + mainNovelSpot.putFeature( Spot.QUALITY, -1. ); + model.addSpotTo( mainNovelSpot, Integer.valueOf( currentTimePoint ) ); + // Recreate links. + final Set< DefaultWeightedEdge > edges = model.getTrackModel().edgesOf( previousSpot ); + for ( final DefaultWeightedEdge e : edges ) + { + final double weight = model.getTrackModel().getEdgeWeight( e ); + final Spot source = model.getTrackModel().getEdgeSource( e ); + final Spot target = model.getTrackModel().getEdgeTarget( e ); + if ( source == previousSpot ) + model.addEdge( mainNovelSpot, target, weight ); + else if ( target == previousSpot ) + model.addEdge( source, mainNovelSpot, weight ); + else + throw new IllegalArgumentException( "The edge of a spot does not have the spot as source or target?!?" ); + } + if ( DEBUG ) + IJ.log( " - Modified spot " + str( previousSpot ) + " -> " + str( mainNovelSpot ) ); + + model.removeSpot( previousSpot ); + + // Deal with the other ones. + final HashSet< Spot > extraSpots = new HashSet<>( novelSpotList ); + extraSpots.remove( mainNovelSpot ); + + int i = 1; + for ( final Spot s : extraSpots ) + { + s.setName( previousSpot.getName() + "_" + i++ ); + s.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + s.putFeature( Spot.QUALITY, -1. ); + model.addSpotTo( s, Integer.valueOf( currentTimePoint ) ); + + if ( DEBUG ) + IJ.log( " - Added spot " + str( s ) ); + } + } + + private void addNewSpot( final Iterable< Spot > novelSpotList, final int currentTimePoint ) + { + for ( final Spot spot : novelSpotList ) + { + spot.putFeature( Spot.POSITION_T, currentTimePoint * dt ); + spot.putFeature( Spot.QUALITY, -1. ); + model.addSpotTo( spot, Integer.valueOf( currentTimePoint ) ); + + if ( DEBUG ) + IJ.log( " - Added spot " + str( spot ) ); + } + } + + private final Set< Integer > getModifiedLabels( + final RandomAccessibleInterval< T > novelIndexImg, + final RandomAccessibleInterval< T > previousIndexImg ) + { + final ConcurrentSkipListSet< Integer > modifiedIDs = new ConcurrentSkipListSet<>(); + LoopBuilder.setImages( novelIndexImg, previousIndexImg ) + .multiThreaded( false ) + .forEachPixel( ( c, p ) -> { + final int ci = c.getInteger(); + final int pi = p.getInteger(); + if ( ci == 0 && pi == 0 ) + return; + if ( ci != pi ) + { + modifiedIDs.add( Integer.valueOf( pi ) ); + modifiedIDs.add( Integer.valueOf( ci ) ); + } + } ); + modifiedIDs.remove( Integer.valueOf( -1 ) ); + return modifiedIDs; + } + + private Map< Integer, List< Spot > > getSpots( final RandomAccessibleInterval< T > rai ) + { + // Get all labels. + final AtomicInteger max = new AtomicInteger( 0 ); + rai.forEach( p -> { + final int val = p.getInteger(); + if ( val != 0 && val > max.get() ) + max.set( val ); + } ); + final List< Integer > indices = new ArrayList<>( max.get() ); + for ( int i = 0; i < max.get(); i++ ) + indices.add( Integer.valueOf( i + 1 ) ); + + final ImgLabeling< Integer, ? > labeling = ImgLabeling.fromImageAndLabels( rai, indices ); + final Map< Integer, List< Spot > > spots = MaskUtils.fromLabelingWithROIMap( + labeling, + Views.zeroMin( labeling ), + calibration, + simplify, + rai ); + return spots; + } + + private static final String str( final Spot spot ) + { + return spot.ID() + " (" + + roundToN( spot.getDoublePosition( 0 ), 1 ) + ", " + + roundToN( spot.getDoublePosition( 1 ), 1 ) + ", " + + roundToN( spot.getDoublePosition( 2 ), 1 ) + ") " + + "@ t=" + spot.getFeature( Spot.FRAME ).intValue(); + } + + private static double roundToN( final double num, final int n ) + { + final double scale = Math.pow( 10, n ); + return Math.round( num * scale ) / scale; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java new file mode 100644 index 000000000..c1654465b --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/LabkitLauncher.java @@ -0,0 +1,532 @@ +package fiji.plugin.trackmate.gui.editor; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JRootPane; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; + +import org.scijava.Context; +import org.scijava.ui.behaviour.util.AbstractNamedAction; + +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; +import ij.gui.Roi; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.FinalInterval; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.ImgFactory; +import net.imglib2.img.ImgView; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.img.display.imagej.ImgPlusViews; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.type.numeric.IntegerType; +import net.imglib2.type.numeric.integer.UnsignedIntType; +import net.imglib2.util.Intervals; +import net.imglib2.util.Pair; +import net.imglib2.util.Util; +import net.imglib2.util.ValuePair; +import net.imglib2.view.Views; +import sc.fiji.labkit.ui.inputimage.DatasetInputImage; +import sc.fiji.labkit.ui.labeling.Label; +import sc.fiji.labkit.ui.labeling.Labeling; +import sc.fiji.labkit.ui.models.DefaultSegmentationModel; + +public class LabkitLauncher< T extends IntegerType< T > & NativeType< T > > +{ + + private final double[] calibration; + + private final TrackMate trackmate; + + private final EverythingDisablerAndReenabler disabler; + + private int currentTimePoint; + + private final DisplaySettings ds; + + private final boolean is3D; + + private final boolean isSingleTimePoint; + + private static boolean simplify = true; + + public LabkitLauncher( final TrackMate trackmate, final DisplaySettings ds, final EverythingDisablerAndReenabler disabler ) + { + this.trackmate = trackmate; + this.ds = ds; + this.disabler = disabler; + final ImagePlus imp = trackmate.getSettings().imp; + this.calibration = TMUtils.getSpatialCalibration( imp ); + this.is3D = !DetectionUtils.is2D( imp ); + this.isSingleTimePoint = imp.getNFrames() <= 1; + } + + /** + * Launches the Labkit editor. + * + * @param singleTimePoint + * if true, will launch the editor using only the + * time-point currently displayed in the main view. Otherwise, + * will edit all time-points. + */ + protected void launch( final boolean singleTimePoint ) + { + final ImagePlus imp = trackmate.getSettings().imp; + final DatasetInputImage input = makeInput( imp, singleTimePoint ); + final Pair< Labeling, Map< Integer, Spot > > pair = makeLabeling( imp, trackmate.getModel().getSpots(), singleTimePoint ); + final Labeling labeling = pair.getA(); + final Map< Integer, Spot > spotLabels = pair.getB(); + + // Make a labeling model from it. + final Context context = TMUtils.getContext(); + final DefaultSegmentationModel model = new DefaultSegmentationModel( context, input ); + model.imageLabelingModel().labeling().set( labeling ); + + // Store a copy. + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< T > previousIndexImg = copy( ( RandomAccessibleInterval< T > ) labeling.getIndexImg() ); + + // Show LabKit. + String title = "Editing TrackMate data for " + imp.getShortTitle(); + if ( singleTimePoint ) + title += "at frame " + ( currentTimePoint + 1 ); + final TrackMateLabkitFrame labkit = TrackMateLabkitFrame.show( model, title ); + + // Prepare re-importer. + final double dt = imp.getCalibration().frameInterval; + labkit.onCloseListeners().addListener( () -> { + @SuppressWarnings( "unchecked" ) + final RandomAccessibleInterval< T > indexImg = ( RandomAccessibleInterval< T > ) model.imageLabelingModel().labeling().get().getIndexImg(); + reimport( indexImg, previousIndexImg, spotLabels, dt ); + } ); + } + + private void reimport( + final RandomAccessibleInterval< T > indexImg, + final RandomAccessibleInterval< T > previousIndexImg, + final Map< Integer, Spot > spotLabels, + final double dt ) + { + new Thread( "TrackMate-LabKit-Importer-thread" ) + { + @Override + public void run() + { + try + { + // Do we have something to reimport? + final AtomicBoolean modified = new AtomicBoolean( false ); + LoopBuilder.setImages( previousIndexImg, indexImg ) + .multiThreaded() + .forEachChunk( chunk -> { + if ( modified.get() ) + return null; + chunk.forEachPixel( ( p1, p2 ) -> { + if ( p1.getInteger() != p2.getInteger() ) + { + modified.set( true ); + return; + } + } ); + return null; + } ); + if ( !modified.get() ) + return; + + // Message the user. + final String msg = ( isSingleTimePoint ) + ? "Commit the changes made to the\n" + + "segmentation in the image?" + : ( currentTimePoint < 0 ) + ? "Commit the changes made to the\n" + + "segmentation in whole movie?" + : "Commit the changes made to the\n" + + "segmentation in frame " + ( currentTimePoint + 1 ) + "?"; + final String title = "Commit edits to TrackMate"; + final JCheckBox chkbox = new JCheckBox( "Simplify the contours of modified spots" ); + chkbox.setSelected( simplify ); + final Object[] objs = new Object[] { msg, new JSeparator(), chkbox }; + final int returnedValue = JOptionPane.showConfirmDialog( + null, + objs, + title, + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, + Icons.TRACKMATE_ICON ); + if ( returnedValue != JOptionPane.YES_OPTION ) + return; + + simplify = chkbox.isSelected(); + final LabkitImporter< T > reimporter = new LabkitImporter<>( trackmate.getModel(), calibration, dt, simplify ); + + // Possibly determine the number of time-points to parse. + final int timeDim = ( isSingleTimePoint ) + ? -1 + : ( is3D ) ? 3 : 2; + final long nTimepoints = ( timeDim < 0 ) + ? 0 + : indexImg.numDimensions() > timeDim ? indexImg.dimension( timeDim ) : 0; + + if ( currentTimePoint < 0 && nTimepoints > 1 ) + { + // All time-points. + final Logger log = Logger.IJ_LOGGER; + log.setStatus( "Re-importing from Labkit..." ); + for ( int t = 0; t < nTimepoints; t++ ) + { + // The spots of this time-point: + final Map< Integer, Spot > spotLabelsThisFrame = new HashMap<>(); + for ( final Integer label : spotLabels.keySet() ) + { + final Spot spot = spotLabels.get( label ); + if ( spot.getFeature( Spot.FRAME ).intValue() == t ) + spotLabelsThisFrame.put( label, spot ); + } + + final RandomAccessibleInterval< T > novelIndexImgThisFrame = Views.hyperSlice( indexImg, timeDim, t ); + final RandomAccessibleInterval< T > previousIndexImgThisFrame = Views.hyperSlice( previousIndexImg, timeDim, t ); + reimporter.reimport( novelIndexImgThisFrame, previousIndexImgThisFrame, t, spotLabelsThisFrame ); + log.setProgress( t / ( double ) nTimepoints ); + } + log.setStatus( "" ); + log.setProgress( 0. ); + } + else + { + // Only one. + final int localT = Math.max( 0, currentTimePoint ); + reimporter.reimport( indexImg, previousIndexImg, localT, spotLabels ); + } + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + finally + { + disabler.reenable(); + } + } + }.start(); + } + + private Img< T > copy( final RandomAccessibleInterval< T > in ) + { + final ImgFactory< T > factory = Util.getArrayOrCellImgFactory( in, in.getType() ); + final Img< T > out = factory.create( in ); + LoopBuilder.setImages( in, out ) + .multiThreaded() + .forEachPixel( ( i, o ) -> o.setInteger( i.getInteger() ) ); + return out; + } + + /** + * Creates a new {@link DatasetInputImage} from the specified + * {@link ImagePlus}. The embedded label image is empty. + * + * @param imp + * the input {@link ImagePlus}. + * @param singleTimePoint + * if true, then the dataset will be created only + * for the time-point currently displayed in the + * {@link ImagePlus}. This time-point is then stored in the + * {@link #currentTimePoint} field. If false, the + * dataset is created for the whole movie and the + * {@link #currentTimePoint} takes the value -1. + * @return a new {@link DatasetInputImage}. + */ + @SuppressWarnings( { "rawtypes", "unchecked" } ) + private final DatasetInputImage makeInput( final ImagePlus imp, final boolean singleTimePoint ) + { + final ImgPlus src = TMUtils.rawWraps( imp ); + // Crop if we have a ROI. + final Roi roi = imp.getRoi(); + final RandomAccessibleInterval crop; + if ( roi != null ) + { + final long[] min = src.minAsLongArray(); + final long[] max = src.maxAsLongArray(); + min[ 0 ] = roi.getBounds().x; + min[ 1 ] = roi.getBounds().y; +// max[ 0 ] = roi.getBounds().x + roi.getBounds().width; +// max[ 1 ] = roi.getBounds().y + roi.getBounds().height; + max[ 0 ] = roi.getBounds().x + roi.getBounds().width - 1; + max[ 1 ] = roi.getBounds().y + roi.getBounds().height - 1; + crop = Views.interval( src, min, max ); + } + else + { + crop = src; + } + final ImgPlus srcCropped = new ImgPlus<>( ImgView.wrap( crop ), src ); + + // Possibly reslice for current time-point. + final ImpBdvShowable showable; + final ImgPlus inputImg; + final int timeAxis = src.dimensionIndex( Axes.TIME ); + if ( singleTimePoint && timeAxis >= 0 ) + { + this.currentTimePoint = imp.getFrame() - 1; + inputImg = ImgPlusViews.hyperSlice( srcCropped, timeAxis, currentTimePoint ); + showable = ImpBdvShowable.fromImp( inputImg, imp ); + } + else + { + this.currentTimePoint = -1; + inputImg = srcCropped; + showable = ImpBdvShowable.fromImp( inputImg, imp ); + } + return new DatasetInputImage( inputImg, showable ); + } + + /** + * Prepare the label image for annotation. The labeling is created and each + * of its labels receive the name and the color from the spot it is created + * from. Only the spots fully included in the bounding box of the ROI of the + * source image are written in the labeling. + * + * @param imp + * the source image plus. + * @param spots + * the spot collection. + * @param singleTimePoint + * if true we only annotate one time-point. + * @return the pair of: A. a new {@link Labeling}, B. the map of spots that + * were written in the labeling. The keys are the label value in the + * labeling. + */ + private Pair< Labeling, Map< Integer, Spot > > makeLabeling( final ImagePlus imp, final SpotCollection spots, final boolean singleTimePoint ) + { + // Axes. + final AxisType[] axes = ( is3D ) + ? ( singleTimePoint ) + ? new AxisType[] { Axes.X, Axes.Y, Axes.Z } + : new AxisType[] { Axes.X, Axes.Y, Axes.Z, Axes.TIME } + : ( singleTimePoint ) + ? new AxisType[] { Axes.X, Axes.Y } + : new AxisType[] { Axes.X, Axes.Y, Axes.TIME }; + + // N dimensions. + final int nDims = is3D + ? singleTimePoint ? 3 : 4 + : singleTimePoint ? 2 : 3; + + // Dimensions. + final long[] dims = new long[ nDims ]; + int dim = 0; + dims[ dim++ ] = imp.getWidth(); + dims[ dim++ ] = imp.getHeight(); + if ( is3D ) + dims[ dim++ ] = imp.getNSlices(); + if ( !singleTimePoint ) + dims[ dim++ ] = imp.getNFrames(); + + // Possibly crop. + final Roi roi = imp.getRoi(); + final long[] origin; + if ( roi != null ) + { + dims[ 0 ] = roi.getBounds().width + 1; + dims[ 1 ] = roi.getBounds().height + 1; + origin = new long[ dims.length ]; + origin[ 0 ] = roi.getBounds().x; + origin[ 1 ] = roi.getBounds().y; + } + else + { + origin = null; + } + + // Raw image. + Img< UnsignedIntType > lblImg = ArrayImgs.unsignedInts( dims ); + if ( origin != null ) + { + final RandomAccessibleInterval< UnsignedIntType > translated = Views.translate( lblImg, origin ); + lblImg = ImgView.wrap( translated ); + } + + // Calibration. + final double[] c = TMUtils.getSpatialCalibration( imp ); + final double[] calibration = new double[ nDims ]; + dim = 0; + calibration[ dim++ ] = c[ 0 ]; + calibration[ dim++ ] = c[ 1 ]; + if ( is3D ) + calibration[ dim++ ] = c[ 2 ]; + if ( !singleTimePoint ) + calibration[ dim++ ] = 1.; + + // Label image holder. + final ImgPlus< UnsignedIntType > lblImgPlus = new ImgPlus<>( lblImg, "LblImg", axes, calibration ); + + // Write spots in it with index = id + 1 and build a map index -> spot. + final Map< Integer, Spot > spotLabels = new HashMap<>(); + if ( singleTimePoint ) + { + processFrame( lblImgPlus, spots, currentTimePoint, spotLabels, origin ); + } + else + { + final int timeDim = lblImgPlus.dimensionIndex( Axes.TIME ); + for ( int t = 0; t < imp.getNFrames(); t++ ) + { + final ImgPlus< UnsignedIntType > lblImgPlusThisFrame = ImgPlusViews.hyperSlice( lblImgPlus, timeDim, t ); + processFrame( lblImgPlusThisFrame, spots, t, spotLabels, origin ); + } + } + final Labeling labeling = Labeling.fromImg( lblImgPlus ); + + // Fine tune labels name and color. + final FeatureColorGenerator< Spot > colorGen = FeatureUtils.createSpotColorGenerator( trackmate.getModel(), ds ); + for ( final Label label : labeling.getLabels() ) + { + final String name = label.name(); + final int labelVal = Integer.parseInt( name ); + final Spot spot = spotLabels.get( labelVal ); + if ( spot == null ) + { + System.out.println( "Spot is null for label " + labelVal + "!!" ); // DEBUG + continue; + } + label.setName( spot.getName() ); + label.setColor( new ARGBType( colorGen.color( spot ).getRGB() ) ); + } + + return new ValuePair<>( labeling, spotLabels ); + } + + private final void processFrame( + final ImgPlus< UnsignedIntType > lblImgPlus, + final SpotCollection spots, + final int t, + final Map< Integer, Spot > spotLabels, + final long[] origin ) + { + // If we have a single timepoint, don't use -1 to retrieve spots. + final int lt = t < 0 ? 0 : t; + final Iterable< Spot > spotsThisFrame = spots.iterable( lt, true ); + if ( null == origin ) + { + for ( final Spot spot : spotsThisFrame ) + { + final int index = spot.ID() + 1; + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spotLabels.put( index, spot ); + } + } + else + { + final long[] min = new long[ 2 ]; + final long[] max = new long[ 2 ]; + final FinalInterval spotBB = FinalInterval.wrap( min, max ); + final FinalInterval imgBB = Intervals.createMinSize( origin[ 0 ], origin[ 1 ], lblImgPlus.dimension( 0 ), lblImgPlus.dimension( 1 ) ); + for ( final Spot spot : spotsThisFrame ) + { + boundingBox( spot, min, max ); + // Inside? We skip if we touch the border. + final boolean isInside = Intervals.contains( imgBB, spotBB ); + if ( !isInside ) + continue; + + final int index = spot.ID() + 1; + SpotUtil.iterable( spot, lblImgPlus ).forEach( p -> p.set( index ) ); + spotLabels.put( index, spot ); + } + } + } + + private void boundingBox( final Spot spot, final long[] min, final long[] max ) + { + final SpotRoi roi = spot.getRoi(); + if ( roi == null ) + { + final double cx = spot.getDoublePosition( 0 ); + final double cy = spot.getDoublePosition( 1 ); + final double r = spot.getFeature( Spot.RADIUS ).doubleValue(); + min[ 0 ] = ( long ) Math.floor( ( cx - r ) / calibration[ 0 ] ); + min[ 1 ] = ( long ) Math.floor( ( cy - r ) / calibration[ 1 ] ); + max[ 0 ] = ( long ) Math.ceil( ( cx + r ) / calibration[ 0 ] ); + max[ 1 ] = ( long ) Math.ceil( ( cy + r ) / calibration[ 1 ] ); + } + else + { + final 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[ 0 ] = Math.max( 0, min[ 0 ] ); + min[ 1 ] = Math.max( 0, min[ 1 ] ); + final ImagePlus imp = trackmate.getSettings().imp; + max[ 0 ] = Math.min( imp.getWidth(), max[ 0 ] ); + max[ 1 ] = Math.min( imp.getHeight(), max[ 1 ] ); + } + + public static final AbstractNamedAction getLaunchAction( final TrackMate trackmate, final DisplaySettings ds ) + { + return new AbstractNamedAction( "launch labkit editor" ) + { + + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed( final ActionEvent ae ) + { + new Thread( "TrackMate editor thread" ) + { + @Override + public void run() + { + // Is shift pressed? + final int mod = ae.getModifiers(); + final boolean shiftPressed = ( mod & ActionEvent.SHIFT_MASK ) > 0; + final boolean singleTimepoint = !shiftPressed; + + final JRootPane parent = SwingUtilities.getRootPane( ( Component ) ae.getSource() ); + final EverythingDisablerAndReenabler disabler = new EverythingDisablerAndReenabler( parent, new Class[] { JLabel.class } ); + disabler.disable(); + try + { + @SuppressWarnings( "rawtypes" ) + final LabkitLauncher launcher = new LabkitLauncher( trackmate, ds, disabler ); + launcher.launch( singleTimepoint ); + } + catch ( final Exception e ) + { + disabler.reenable(); + e.printStackTrace(); + } + }; + }.start(); + } + }; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java new file mode 100644 index 000000000..c4d407694 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabKitSegmentationComponent.java @@ -0,0 +1,174 @@ +/*- + * #%L + * The Labkit image segmentation tool for Fiji. + * %% + * Copyright (C) 2017 - 2024 Matthias Arzt + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package fiji.plugin.trackmate.gui.editor; + +import java.awt.BorderLayout; + +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JPanel; +import javax.swing.JSplitPane; + +import net.miginfocom.swing.MigLayout; +import sc.fiji.labkit.ui.BasicLabelingComponent; +import sc.fiji.labkit.ui.DefaultExtensible; +import sc.fiji.labkit.ui.MenuBar; +import sc.fiji.labkit.ui.SegmentationComponent; +import sc.fiji.labkit.ui.actions.AddLabelingIoAction; +import sc.fiji.labkit.ui.actions.LabelEditAction; +import sc.fiji.labkit.ui.actions.LabelingIoAction; +import sc.fiji.labkit.ui.actions.ResetViewAction; +import sc.fiji.labkit.ui.actions.ShowHelpAction; +import sc.fiji.labkit.ui.menu.MenuKey; +import sc.fiji.labkit.ui.models.ColoredLabelsModel; +import sc.fiji.labkit.ui.models.ImageLabelingModel; +import sc.fiji.labkit.ui.models.SegmentationItem; +import sc.fiji.labkit.ui.models.SegmentationModel; +import sc.fiji.labkit.ui.panel.ImageInfoPanel; +import sc.fiji.labkit.ui.panel.LabelPanel; +import sc.fiji.labkit.ui.segmentation.PredictionLayer; + +/** + * This class is copy/pasted and adapted from {@link SegmentationComponent} to + * control what is shown in the TrackMate UI. This could be revised to avoid + * class duplication. + * + * {@link SegmentationComponent} is the central Labkit UI component. Provides UI + * to display and modify a {@link SegmentationModel}. + *

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

+ * A main menu that contains many actions for open and saving data, can be + * accessed by using {@link #getMenuBar()}. + */ +public class TrackMateLabKitSegmentationComponent extends JPanel implements AutoCloseable +{ + + private static final long serialVersionUID = 1L; + + private final boolean unmodifiableLabels; + + private final DefaultExtensible extensible; + + private final BasicLabelingComponent labelingComponent; + + private final SegmentationModel segmentationModel; + + public TrackMateLabKitSegmentationComponent( final JFrame dialogBoxOwner, + final SegmentationModel segmentationModel, final boolean unmodifiableLabels ) + { + this.extensible = new DefaultExtensible( segmentationModel.context(), dialogBoxOwner ); + this.unmodifiableLabels = unmodifiableLabels; + this.segmentationModel = segmentationModel; + final ImageLabelingModel imageLabelingModel = segmentationModel.imageLabelingModel(); + labelingComponent = new BasicLabelingComponent( dialogBoxOwner, imageLabelingModel ); + labelingComponent.addBdvLayer( PredictionLayer.createPredictionLayer( segmentationModel ) ); + initActions(); + setLayout( new BorderLayout() ); + add( initGui() ); + } + + private void initActions() + { +// final Holder< SegmentationItem > selectedSegmenter = segmentationModel.segmenterList().selectedSegmenter(); + final ImageLabelingModel labelingModel = segmentationModel.imageLabelingModel(); +// new TrainClassifier( extensible, segmentationModel.segmenterList() ); +// new ClassifierSettingsAction( extensible, segmentationModel.segmenterList() ); +// new ClassifierIoAction( extensible, segmentationModel.segmenterList() ); + new LabelingIoAction( extensible, labelingModel ); + new AddLabelingIoAction( extensible, labelingModel.labeling() ); +// new SegmentationExportAction( extensible, labelingModel ); + new ResetViewAction( extensible, labelingModel ); +// new BatchSegmentAction( extensible, selectedSegmenter ); +// new SegmentationAsLabelAction( extensible, segmentationModel ); +// new BitmapImportExportAction( extensible, labelingModel ); + new LabelEditAction( extensible, unmodifiableLabels, new ColoredLabelsModel( labelingModel ) ); +// MeasureConnectedComponents.addAction( extensible, labelingModel ); + new ShowHelpAction( extensible ); + labelingComponent.addShortcuts( extensible.getShortCuts() ); + } + + private JPanel initLeftPanel() + { + final JPanel panel = new JPanel(); + panel.setLayout( new MigLayout( "", "[grow]", "[][grow]" ) ); + panel.add( ImageInfoPanel.newFramedImageInfoPanel( segmentationModel.imageLabelingModel(), labelingComponent ), "grow, wrap" ); + panel.add( LabelPanel.newFramedLabelPanel( segmentationModel.imageLabelingModel(), extensible, unmodifiableLabels ), "grow, wrap, height 0:5000" ); +// panel.add( SegmenterPanel.newFramedSegmeterPanel( segmentationModel.segmenterList(), extensible ), "grow, height 0:50" ); + panel.invalidate(); + panel.repaint(); + return panel; + } + + private JSplitPane initGui() + { + final JSplitPane panel = new JSplitPane(); + panel.setOneTouchExpandable( true ); + panel.setLeftComponent( initLeftPanel() ); + panel.setRightComponent( labelingComponent ); + panel.setBorder( BorderFactory.createEmptyBorder() ); + return panel; + } + + @Deprecated + public JComponent getComponent() + { + return this; + } + + public JMenu createMenu( final MenuKey< Void > key ) + { + if ( key == MenuBar.SEGMENTER_MENU ) + return extensible.createMenu( + SegmentationItem.SEGMENTER_MENU, segmentationModel + .segmenterList().selectedSegmenter()::get ); + return extensible.createMenu( key, () -> null ); + } + + @Override + public void close() + { + labelingComponent.close(); + } + + public JMenuBar getMenuBar() + { + return new MenuBar( this::createMenu ); + } + + public void autoContrast() + { + labelingComponent.autoContrast(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java new file mode 100644 index 000000000..f76cf3c9c --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/gui/editor/TrackMateLabkitFrame.java @@ -0,0 +1,152 @@ +/*- + * #%L + * The Labkit image segmentation tool for Fiji. + * %% + * Copyright (C) 2017 - 2024 Matthias Arzt + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ + +package fiji.plugin.trackmate.gui.editor; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; + +import javax.swing.JFrame; + +import org.scijava.Context; + +import fiji.plugin.trackmate.gui.Icons; +import io.scif.services.DatasetIOService; +import net.imagej.Dataset; +import sc.fiji.labkit.pixel_classification.utils.SingletonContext; +import sc.fiji.labkit.ui.InitialLabeling; +import sc.fiji.labkit.ui.LabkitFrame; +import sc.fiji.labkit.ui.inputimage.DatasetInputImage; +import sc.fiji.labkit.ui.inputimage.InputImage; +import sc.fiji.labkit.ui.models.DefaultSegmentationModel; +import sc.fiji.labkit.ui.models.SegmentationModel; +import sc.fiji.labkit.ui.utils.Notifier; + +/** + * This class is copied and adapted from {@link LabkitFrame}. + *

+ * The main Labkit window. (This window allows to segment a single image. It has + * to be distinguished from the LabkitProjectFrame, which allows to operation on + * multiple images.) The window only contains a + * {@link TrackMateLabKitSegmentationComponent} and shows the associated main + * menu. + * + * @author Matthias Arzt + */ +public class TrackMateLabkitFrame +{ + + private final JFrame frame = initFrame(); + + private final Notifier onCloseListeners = new Notifier(); + + public static TrackMateLabkitFrame showForFile( Context context, + final String filename ) + { + if ( context == null ) + context = SingletonContext.getInstance(); + final Dataset dataset = openDataset( context, filename ); + return showForImage( context, new DatasetInputImage( dataset ) ); + } + + private static Dataset openDataset( final Context context, final String filename ) + { + try + { + return context.service( DatasetIOService.class ).open( filename ); + } + catch ( final IOException e ) + { + throw new RuntimeException( e ); + } + } + + public static TrackMateLabkitFrame showForImage( Context context, final InputImage inputImage ) + { + if ( context == null ) + context = SingletonContext.getInstance(); + final SegmentationModel model = new DefaultSegmentationModel( context, inputImage ); + model.imageLabelingModel().labeling().set( InitialLabeling.initialLabeling( context, inputImage ) ); + return show( model, inputImage.imageForSegmentation().getName() ); + } + + public static TrackMateLabkitFrame show( final SegmentationModel model, final String title ) + { + return new TrackMateLabkitFrame( model, title ); + } + + private TrackMateLabkitFrame( final SegmentationModel model, final String title ) + { + @SuppressWarnings( "unused" ) + final TrackMateLabKitSegmentationComponent segmentationComponent = initSegmentationComponent( model ); + setTitle( title ); + frame.setIconImage( Icons.TRACKMATE_ICON.getImage() ); +// frame.setJMenuBar( new MenuBar( segmentationComponent::createMenu ) ); + frame.setVisible( true ); + } + + private TrackMateLabKitSegmentationComponent initSegmentationComponent( final SegmentationModel segmentationModel ) + { + final TrackMateLabKitSegmentationComponent segmentationComponent = new TrackMateLabKitSegmentationComponent( frame, segmentationModel, false ); + frame.add( segmentationComponent ); + frame.addWindowListener( new WindowAdapter() + { + + @Override + public void windowClosed( final WindowEvent e ) + { + segmentationComponent.close(); + onCloseListeners.notifyListeners(); + } + } ); + return segmentationComponent; + } + + private JFrame initFrame() + { + final JFrame frame = new JFrame(); + frame.setBounds( 50, 50, 1200, 900 ); + frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + return frame; + } + + private void setTitle( final String name ) + { + if ( name == null || name.isEmpty() ) + frame.setTitle( "Labkit" ); + else + frame.setTitle( "Labkit - " + name ); + } + + public Notifier onCloseListeners() + { + return onCloseListeners; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java index 63103ea39..9ab663389 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/TrackMateWizardSequence.java @@ -50,6 +50,7 @@ import fiji.plugin.trackmate.gui.components.FeatureDisplaySelector; import fiji.plugin.trackmate.gui.components.LogPanel; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettings; +import fiji.plugin.trackmate.gui.editor.LabkitLauncher; import fiji.plugin.trackmate.gui.wizard.descriptors.ActionChooserDescriptor; import fiji.plugin.trackmate.gui.wizard.descriptors.ChooseDetectorDescriptor; import fiji.plugin.trackmate.gui.wizard.descriptors.ChooseTrackerDescriptor; @@ -146,7 +147,14 @@ public TrackMateWizardSequence( final TrackMate trackmate, final SelectionModel chooseTrackerDescriptor = new ChooseTrackerDescriptor( new TrackerProvider(), trackmate ); executeTrackingDescriptor = new ExecuteTrackingDescriptor( trackmate, logPanel ); trackFilterDescriptor = new TrackFilterDescriptor( trackmate, trackFilters, featureSelector ); - configureViewsDescriptor = new ConfigureViewsDescriptor( displaySettings, featureSelector, new LaunchTrackSchemeAction(), new ShowTrackTablesAction(), new ShowSpotTableAction(), model.getSpaceUnits() ); + configureViewsDescriptor = new ConfigureViewsDescriptor( + displaySettings, + featureSelector, + new LaunchTrackSchemeAction(), + new ShowTrackTablesAction(), + new ShowSpotTableAction(), + LabkitLauncher.getLaunchAction( trackmate, displaySettings ), + model.getSpaceUnits() ); grapherDescriptor = new GrapherDescriptor( trackmate, selectionModel, displaySettings ); actionChooserDescriptor = new ActionChooserDescriptor( new ActionProvider(), trackmate, selectionModel, displaySettings ); saveDescriptor = new SaveDescriptor( trackmate, displaySettings, this ); @@ -175,7 +183,6 @@ public WizardPanelDescriptor next() return current; } - @Override public WizardPanelDescriptor previous() { @@ -201,7 +208,6 @@ public WizardPanelDescriptor current() return current; } - @Override public WizardPanelDescriptor logDescriptor() { diff --git a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java index 1d849386c..7856dd874 100644 --- a/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java +++ b/src/main/java/fiji/plugin/trackmate/gui/wizard/descriptors/ConfigureViewsDescriptor.java @@ -39,6 +39,7 @@ public ConfigureViewsDescriptor( final Action launchTrackSchemeAction, final Action showTrackTablesAction, final Action showSpotTableAction, + final Action launchLabkitAction, final String spaceUnits ) { super( KEY ); @@ -48,6 +49,7 @@ public ConfigureViewsDescriptor( spaceUnits, launchTrackSchemeAction, showTrackTablesAction, - showSpotTableAction ); + showSpotTableAction, + launchLabkitAction ); } } diff --git a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java index 3a5b6cab7..9a7c82a8c 100644 --- a/src/main/java/fiji/plugin/trackmate/io/IOUtils.java +++ b/src/main/java/fiji/plugin/trackmate/io/IOUtils.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -37,6 +37,7 @@ import java.io.FilenameFilter; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import javax.swing.JDialog; import javax.swing.JFileChooser; @@ -59,6 +60,20 @@ public class IOUtils { + private static final Pattern ILLEGAL_XML_CHARS = Pattern.compile( + "[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F-\\x84\\x86-\\x9F\\uD800-\\uDFFF\\uFDD0-\\uFDEF\\uFFFE\\uFFFF\\u2FFFE\\u2FFFF\\u3FFFE\\u3FFFF\\u4FFFE\\u4FFFF\\u5FFFE\\u5FFFF\\u6FFFE\\u6FFFF\\u7FFFE\\u7FFFF\\u8FFFE\\u8FFFF\\u9FFFE\\u9FFFF\\uAFFFE\\uAFFFF\\uBFFFE\\uBFFFF\\uCFFFE\\uCFFFF\\uDFFFE\\uDFFFF\\uEFFFE\\uEFFFF\\uFFFFE\\uFFFFF\\u10FFFE\\u10FFFF]" ); + + private static final Pattern ILLEGAL_SURROGATE_PAIRS = Pattern.compile( + "[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF])[\uDC00-\uDFFF]" ); + + public static String cleanInvalidXmlChars( final String input ) + { + if ( input == null ) + return null; + final String cleaned = ILLEGAL_XML_CHARS.matcher( input ).replaceAll( "" ); + return ILLEGAL_SURROGATE_PAIRS.matcher( cleaned ).replaceAll( "" ); + } + public static final boolean canReadFile( final String path, final StringBuilder errorHolder ) { if ( path.isEmpty() ) diff --git a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java index 79d0bf9ef..6b74242f9 100644 --- a/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java +++ b/src/main/java/fiji/plugin/trackmate/io/TmXmlWriter.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -286,7 +286,7 @@ public void appendLog( final String log ) if ( null != log ) { final Element logElement = new Element( LOG_ELEMENT_KEY ); - logElement.addContent( log ); + logElement.addContent( IOUtils.cleanInvalidXmlChars( log ) ); root.addContent( logElement ); logger.log( " Added log.\n" ); } diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java deleted file mode 100644 index 7694cff9f..000000000 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/FlagNode.java +++ /dev/null @@ -1,55 +0,0 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ -package fiji.plugin.trackmate.tracking.kdtree; - -public class FlagNode< K > -{ - - private K value; - - private boolean visited = false; - - public FlagNode( final K value ) - { - this.setValue( value ); - } - - public boolean isVisited() - { - return visited; - } - - public void setVisited( final boolean visited ) - { - this.visited = visited; - } - - public K getValue() - { - return value; - } - - public void setValue( final K value ) - { - this.value = value; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java deleted file mode 100644 index 98a5e00dd..000000000 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborFlagSearchOnKDTree.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2010 - 2024 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ - -package fiji.plugin.trackmate.tracking.kdtree; - -import net.imglib2.KDTree; -import net.imglib2.KDTreeNode; -import net.imglib2.RealLocalizable; -import net.imglib2.Sampler; -import net.imglib2.neighborsearch.NearestNeighborSearch; - - -// TODO: revise for new KDTree implementation, where KDTreeNode are reusable proxies. -public class NearestNeighborFlagSearchOnKDTree< T > implements NearestNeighborSearch< FlagNode< T > > -{ - - protected KDTree< FlagNode< T > > tree; - - protected final int n; - - protected final double[] pos; - - protected KDTreeNode< FlagNode< T > > bestPoint; - - protected double bestSquDistance; - - public NearestNeighborFlagSearchOnKDTree( final KDTree< FlagNode< T > > tree ) - { - n = tree.numDimensions(); - pos = new double[ n ]; - this.tree = tree; - } - - @Override - public int numDimensions() - { - return n; - } - - @Override - public void search( final RealLocalizable p ) - { - p.localize( pos ); - bestSquDistance = Double.MAX_VALUE; - searchNode( tree.getRoot() ); - } - - protected void searchNode( final KDTreeNode< FlagNode< T > > current ) - { - // consider the current node - final double distance = current.squDistanceTo( pos ); - final boolean visited = current.get().isVisited(); - if ( distance < bestSquDistance && !visited ) - { - bestSquDistance = distance; - bestPoint = current; - } - - final double axisDiff = pos[ current.getSplitDimension() ] - current.getSplitCoordinate(); - final double axisSquDistance = axisDiff * axisDiff; - final boolean leftIsNearBranch = axisDiff < 0; - - // search the near branch - final KDTreeNode< FlagNode< T > > nearChild = leftIsNearBranch ? current.left() : current.right(); - final KDTreeNode< FlagNode< T > > awayChild = leftIsNearBranch ? current.right() : current.left(); - if ( nearChild != null ) - searchNode( nearChild ); - - // search the away branch - maybe - if ( ( axisSquDistance <= bestSquDistance ) && ( awayChild != null ) ) - searchNode( awayChild ); - } - - @Override - public Sampler< fiji.plugin.trackmate.tracking.kdtree.FlagNode< T > > getSampler() - { - return bestPoint; - } - - @Override - public RealLocalizable getPosition() - { - return bestPoint; - } - - @Override - public double getSquareDistance() - { - return bestSquDistance; - } - - @Override - public double getDistance() - { - return Math.sqrt( bestSquDistance ); - } - - @Override - public NearestNeighborFlagSearchOnKDTree< T > copy() - { - final NearestNeighborFlagSearchOnKDTree< T > copy = new NearestNeighborFlagSearchOnKDTree<>( tree ); - System.arraycopy( pos, 0, copy.pos, 0, pos.length ); - copy.bestPoint = bestPoint; - copy.bestSquDistance = bestSquDistance; - return copy; - } -} diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java index acacd1226..81319db64 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTracker.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -37,17 +37,19 @@ import java.util.concurrent.atomic.AtomicInteger; import org.jgrapht.graph.DefaultWeightedEdge; +import org.jgrapht.graph.SimpleDirectedWeightedGraph; import org.jgrapht.graph.SimpleWeightedGraph; import org.scijava.Cancelable; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Spot; import fiji.plugin.trackmate.SpotCollection; +import fiji.plugin.trackmate.graph.GraphUtils; import fiji.plugin.trackmate.tracking.SpotTracker; import fiji.plugin.trackmate.util.Threads; import net.imglib2.KDTree; -import net.imglib2.RealPoint; import net.imglib2.algorithm.MultiThreadedBenchmarkAlgorithm; +import net.imglib2.neighborsearch.KNearestNeighborSearchOnKDTree; public class NearestNeighborTracker extends MultiThreadedBenchmarkAlgorithm implements SpotTracker, Cancelable { @@ -62,7 +64,7 @@ public class NearestNeighborTracker extends MultiThreadedBenchmarkAlgorithm impl protected Logger logger = Logger.VOID_LOGGER; - protected SimpleWeightedGraph< Spot, DefaultWeightedEdge > graph; + protected SimpleDirectedWeightedGraph< Spot, DefaultWeightedEdge > graph; private boolean isCanceled; @@ -134,55 +136,62 @@ public Void call() throws Exception return null; } - final List< RealPoint > targetCoords = new ArrayList<>( nTargetSpots ); - final List< FlagNode< Spot > > targetNodes = new ArrayList<>( nTargetSpots ); - final Iterator< Spot > targetIt = spots.iterator( targetFrame, true ); - while ( targetIt.hasNext() ) - { - final double[] coords = new double[ 3 ]; - final Spot spot = targetIt.next(); - spot.localize( coords ); - targetCoords.add( new RealPoint( coords ) ); - targetNodes.add( new FlagNode<>( spot ) ); - } - - final KDTree< FlagNode< Spot > > tree = new KDTree<>( targetNodes, targetCoords ); - final NearestNeighborFlagSearchOnKDTree< Spot > search = new NearestNeighborFlagSearchOnKDTree<>( tree ); + /* + * Create kD-Tree and NN search. + */ + final Iterable< Spot > targetSpots = spots.iterable( targetFrame, true ); + final KDTree< Spot > tree = new KDTree< Spot >( nTargetSpots, targetSpots, targetSpots ); + final KNearestNeighborSearchOnKDTree< Spot > search = new KNearestNeighborSearchOnKDTree<>( tree, nTargetSpots ); /* * For each spot in the source frame, find its nearest * neighbor in the target frame. */ final Iterator< Spot > sourceIt = spots.iterator( sourceFrame, true ); - while ( sourceIt.hasNext() ) + SOURCE: while ( sourceIt.hasNext() ) { final Spot source = sourceIt.next(); - final double[] coords = new double[ 3 ]; - source.localize( coords ); - final RealPoint sourceCoords = new RealPoint( coords ); - search.search( sourceCoords ); - - final double squareDist = search.getSquareDistance(); - final FlagNode< Spot > targetNode = search.getSampler().get(); + search.search( source ); /* - * The closest we could find is too far. We skip this - * source spot and do not create a link + * Loop over target spots in nearest neighbor order. */ - if ( squareDist > maxDistSquare ) - continue; - - /* - * Everything is ok. This node is free and below max - * dist. We create a link and mark this node as - * assigned. - */ - - targetNode.setVisited( true ); - synchronized ( graph ) + int iNeighbor = -1; + TARGET: while ( ++iNeighbor < nTargetSpots ) { - final DefaultWeightedEdge edge = graph.addEdge( source, targetNode.getValue() ); - graph.setEdgeWeight( edge, squareDist ); + /* + * Is the closest we could find too far? If yes, we + * skip this source spot and do not create a link. + */ + final double squareDist = search.getSquareDistance( iNeighbor ); + if ( squareDist > maxDistSquare ) + continue SOURCE; + + /* + * Is the closest one already taken? Has it already + * an incoming edge? + */ + final Spot target = search.getSampler( iNeighbor ).get(); + if ( graph.inDegreeOf( target ) > 0 ) + { + /* + * In that case we need to test the next nearest + * neighbor (next target spot). + */ + continue TARGET; + } + + /* + * Everything is ok. This node is free and below max + * dist. We create a link and loop to the next + * source spot. + */ + synchronized ( graph ) + { + final DefaultWeightedEdge edge = graph.addEdge( source, target ); + graph.setEdgeWeight( edge, squareDist ); + } + break TARGET; } } logger.setProgress( progress.incrementAndGet() / ( double ) frames.size() ); @@ -222,12 +231,12 @@ public Void call() throws Exception @Override public SimpleWeightedGraph< Spot, DefaultWeightedEdge > getResult() { - return graph; + return GraphUtils.convertToSimpleWeightedGraph( graph ); } public void reset() { - graph = new SimpleWeightedGraph<>( DefaultWeightedEdge.class ); + graph = new SimpleDirectedWeightedGraph<>( DefaultWeightedEdge.class ); final Iterator< Spot > it = spots.iterator( true ); while ( it.hasNext() ) graph.addVertex( it.next() ); diff --git a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java index 655afb5be..356847080 100644 --- a/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java +++ b/src/main/java/fiji/plugin/trackmate/tracking/kdtree/NearestNeighborTrackerFactory.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -50,19 +50,23 @@ public class NearestNeighborTrackerFactory implements SpotTrackerFactory public static final String NAME = "Nearest-neighbor tracker"; public static final String INFO_TEXT = "" - + "This tracker is the most simple one, and is based on nearest neighbor
" - + "search. The spots in the target frame are searched for the nearest neighbor
" - + "of each spot in the source frame. If the spots found are closer than the
" - + "maximal allowed distance, a link between the two is created.
" + + "This tracker is the most simple one, and is based on nearest neighbor " + + "search. " + "

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

" + + "The nearest neighbor search relies upon the KD-tree technique implemented " + + "in imglib2. This ensure a very efficient " + + "tracking and makes this tracker suitable for situation where a huge number " + + "of particles are to be tracked over a very large number of frames. However, " + + "because of the naiveness of its principles, it can result in pathological " + + "tracks. It can only do frame-to-frame linking; there cannot be any track " + "merging or splitting, and gaps will not be closed. Also, the end results are non-" - + "deterministic." + + "deterministic, as the links created depend in the order in which the source spots " + + "are iterated." + " "; private String errorMessage; diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java index d0edab222..43298c3e3 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreview.java @@ -48,6 +48,8 @@ public class DetectionPreview private final DetectionPreviewPanel panel; + private TrackMate trackmate; + protected DetectionPreview( final Model model, final Settings settings, @@ -66,6 +68,7 @@ protected DetectionPreview( detectionSettingsSupplier.get(), currentFrameSupplier.get(), thresholdKey ) ); + panel.btnCancel.addActionListener( l -> cancel() ); } public DetectionPreviewPanel getPanel() @@ -86,7 +89,9 @@ protected void preview( final int frame, final String thresholdKey ) { - panel.btnPreview.setEnabled( false ); + panel.btnPreview.setVisible( false ); + panel.btnCancel.setVisible( true ); + panel.btnCancel.setEnabled( true ); Threads.run( "TrackMate preview detection thread", () -> { try @@ -115,7 +120,8 @@ protected void preview( } finally { - panel.btnPreview.setEnabled( true ); + panel.btnPreview.setVisible( true ); + panel.btnCancel.setVisible( false ); } } ); } @@ -161,38 +167,54 @@ protected Pair< Model, Double > runPreviewDetection( lSettings.detectorFactory = detectorFactory; lSettings.detectorSettings = new HashMap<>( detectorSettings ); - // Does this detector have a THRESHOLD parameter? - final boolean hasThreshold = ( thresholdKey != null ) && ( detectorSettings.containsKey( thresholdKey ) ); - final double threshold; - if ( hasThreshold ) - { - threshold = ( ( Double ) detectorSettings.get( thresholdKey ) ).doubleValue(); - lSettings.detectorSettings.put( thresholdKey, Double.valueOf( Double.NEGATIVE_INFINITY ) ); - } - else + this.trackmate = new TrackMate( lSettings ); + + try { - threshold = Double.NaN; - } + // Does this detector have a THRESHOLD parameter? + final boolean hasThreshold = ( thresholdKey != null ) && ( detectorSettings.containsKey( thresholdKey ) ); + final double threshold; + if ( hasThreshold ) + { + threshold = ( ( Double ) detectorSettings.get( thresholdKey ) ).doubleValue(); +// lSettings.detectorSettings.put( thresholdKey, Double.valueOf( Double.NEGATIVE_INFINITY ) ); + lSettings.detectorSettings.put( thresholdKey, Double.valueOf( 0. ) ); + } + else + { + threshold = Double.NaN; + } - // Execute preview. - final TrackMate trackmate = new TrackMate( lSettings ); - trackmate.getModel().setLogger( panel.logger ); + // Execute preview. + trackmate.getModel().setLogger( panel.logger ); - final boolean detectionOk = trackmate.execDetection(); - if ( !detectionOk ) + final boolean detectionOk = trackmate.execDetection(); + if ( !detectionOk ) + { + panel.logger.error( trackmate.getErrorMessage() ); + return null; + } + + if ( hasThreshold ) + // Filter by the initial threshold value. + trackmate.getModel().getSpots().filter( new FeatureFilter( Spot.QUALITY, threshold, true ) ); + else + // Make them all visible. + trackmate.getModel().getSpots().setVisible( true ); + + return new ValuePair< Model, Double >( trackmate.getModel(), Double.valueOf( threshold ) ); + } + finally { - panel.logger.error( trackmate.getErrorMessage() ); - return null; + this.trackmate = null; } + } - if ( hasThreshold ) - // Filter by the initial threshold value. - trackmate.getModel().getSpots().filter( new FeatureFilter( Spot.QUALITY, threshold, true ) ); - else - // Make them all visible. - trackmate.getModel().getSpots().setVisible( true ); - - return new ValuePair< Model, Double >( trackmate.getModel(), Double.valueOf( threshold ) ); + private void cancel() + { + panel.btnCancel.setEnabled( false ); + if ( null != trackmate ) + trackmate.cancel( "User canceled preview" ); } protected void updateModelAndHistogram( final Model targetModel, final Model sourceModel, final int frame, final double threshold ) diff --git a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java index f3adad062..e2b36983b 100644 --- a/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java +++ b/src/main/java/fiji/plugin/trackmate/util/DetectionPreviewPanel.java @@ -22,14 +22,17 @@ package fiji.plugin.trackmate.util; import static fiji.plugin.trackmate.gui.Fonts.SMALL_FONT; +import static fiji.plugin.trackmate.gui.Icons.CANCEL_ICON; import static fiji.plugin.trackmate.gui.Icons.PREVIEW_ICON; +import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.util.function.DoubleConsumer; import javax.swing.JButton; +import javax.swing.JLabel; import javax.swing.JPanel; import fiji.plugin.trackmate.Logger; @@ -48,28 +51,37 @@ public class DetectionPreviewPanel extends JPanel + "get rid of them later." + ""; + private static final String TOOLTIP_CANCEL = "" + + "Cancel the current preview." + + ""; + final Logger logger; final JButton btnPreview; + final JButton btnCancel; + final QualityHistogramChart chart; + public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final String axisLabel ) { final GridBagLayout gridBagLayout = new GridBagLayout(); gridBagLayout.columnWeights = new double[] { 1.0, 0.0 }; - gridBagLayout.rowWeights = new double[] { 0., 0. }; - gridBagLayout.rowHeights = new int[] { 120, 20 }; + gridBagLayout.rowWeights = new double[] { 1., 0., 0. }; + gridBagLayout.rowHeights = new int[] { 0, 120, 20 }; setLayout( gridBagLayout ); + add( new JLabel() ); + this.chart = new QualityHistogramChart( thresholdUpdater, axisLabel ); final GridBagConstraints gbcHistogram = new GridBagConstraints(); gbcHistogram.gridwidth = 2; gbcHistogram.insets = new Insets( 0, 0, 5, 0 ); gbcHistogram.fill = GridBagConstraints.BOTH; gbcHistogram.gridx = 0; - gbcHistogram.gridy = 0; + gbcHistogram.gridy = 1; add( chart, gbcHistogram ); final JLabelLogger labelLogger = new JLabelLogger(); @@ -78,18 +90,29 @@ public DetectionPreviewPanel( final DoubleConsumer thresholdUpdater, final Strin gbcLabelLogger.insets = new Insets( 5, 5, 0, 5 ); gbcLabelLogger.fill = GridBagConstraints.BOTH; gbcLabelLogger.gridx = 0; - gbcLabelLogger.gridy = 1; + gbcLabelLogger.gridy = 2; add( labelLogger, gbcLabelLogger ); this.logger = labelLogger.getLogger(); this.btnPreview = new JButton( "Preview", PREVIEW_ICON ); btnPreview.setToolTipText( TOOLTIP_PREVIEW ); + btnPreview.setFont( SMALL_FONT ); + this.btnCancel = new JButton( "Cancel", CANCEL_ICON ); + btnCancel.setToolTipText( TOOLTIP_CANCEL ); + btnCancel.setVisible( false ); + btnCancel.setFont( SMALL_FONT ); + + final JPanel btnPanel = new JPanel(); + btnPanel.add( btnPreview ); + btnPanel.add( btnCancel ); + final GridBagConstraints gbcBtnPreview = new GridBagConstraints(); gbcBtnPreview.anchor = GridBagConstraints.NORTHEAST; gbcBtnPreview.insets = new Insets( 5, 5, 0, 0 ); gbcBtnPreview.gridx = 1; - gbcBtnPreview.gridy = 1; - this.add( btnPreview, gbcBtnPreview ); - btnPreview.setFont( SMALL_FONT ); + gbcBtnPreview.gridy = 2; + this.add( btnPanel, gbcBtnPreview ); + + setPreferredSize( new Dimension( 240, 100 ) ); } } diff --git a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java index cd4c2ee05..4acca565c 100644 --- a/src/main/java/fiji/plugin/trackmate/util/TMUtils.java +++ b/src/main/java/fiji/plugin/trackmate/util/TMUtils.java @@ -865,6 +865,30 @@ public static String getImagePathWithoutExtension( final Settings settings ) } } + /** + * Returns true if the class with the fully qualified name is + * present at runtime. This is useful to detect whether a certain update + * site has been activated in Fiji and to enable or disable component based + * on this. + * + * @param className + * the fully qualified class name, e.g. + * "sc.fiji.labkit.ui.LabkitFrame" + * @return true if the the class with the specified name is + * present, false otherwise. + */ + public static boolean isClassPresent( final String className ) + { + try + { + Class.forName( className, false, TMUtils.class.getClassLoader() ); + return true; + } + catch ( final ClassNotFoundException e1 ) + {} + return false; + } + private TMUtils() {} } diff --git a/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java b/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java new file mode 100644 index 000000000..ccd1bac73 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/WrapLayout.java @@ -0,0 +1,117 @@ +package fiji.plugin.trackmate.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +public class WrapLayout extends FlowLayout +{ + private static final long serialVersionUID = 1L; + + public WrapLayout() + { + super(); + } + + public WrapLayout( final int align ) + { + super( align ); + } + + public WrapLayout( final int align, final int hgap, final int vgap ) + { + super( align, hgap, vgap ); + } + + @Override + public Dimension preferredLayoutSize( final Container target ) + { + return layoutSize( target, true ); + } + + @Override + public Dimension minimumLayoutSize( final Container target ) + { + final Dimension minimum = layoutSize( target, false ); + minimum.width -= ( getHgap() + 1 ); + return minimum; + } + + private Dimension layoutSize( final Container target, final boolean preferred ) + { + synchronized ( target.getTreeLock() ) + { + int targetWidth = target.getSize().width; + + if ( targetWidth == 0 ) + targetWidth = Integer.MAX_VALUE; + + final int hgap = getHgap(); + final int vgap = getVgap(); + final Insets insets = target.getInsets(); + final int horizontalInsetsAndGap = insets.left + insets.right + ( hgap * 2 ); + final int maxWidth = targetWidth - horizontalInsetsAndGap; + + final Dimension dim = new Dimension( 0, 0 ); + int rowWidth = 0; + int rowHeight = 0; + + final int nmembers = target.getComponentCount(); + + for ( int i = 0; i < nmembers; i++ ) + { + final Component m = target.getComponent( i ); + + if ( m.isVisible() ) + { + final Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + if ( rowWidth + d.width > maxWidth ) + { + addRow( dim, rowWidth, rowHeight ); + rowWidth = 0; + rowHeight = 0; + } + + if ( rowWidth != 0 ) + { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max( rowHeight, d.height ); + } + } + + addRow( dim, rowWidth, rowHeight ); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + final Container scrollPane = SwingUtilities.getAncestorOfClass( JScrollPane.class, target ); + if ( scrollPane != null ) + { + dim.width -= ( hgap + 1 ); + } + + return dim; + } + } + + private void addRow( final Dimension dim, final int rowWidth, final int rowHeight ) + { + dim.width = Math.max( dim.width, rowWidth ); + + if ( dim.height > 0 ) + { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java index db9b4a9fa..169f15326 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CLIConfigurator.java @@ -8,12 +8,12 @@ * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public * License along with this program. If not, see * . @@ -36,6 +36,14 @@ import fiji.plugin.trackmate.util.cli.CommandCLIConfigurator.ExecutablePath; import fiji.plugin.trackmate.util.cli.CondaCLIConfigurator.CondaEnvironmentCommand; +/** + * Base class for CLI configurator tools. The implementation of a CLI + * configurator is made by subclassing this class (or one of its specialization) + * and specifying arguments for the CLI component with the addXYX() + * builders. + * + * @author Jean-Yves Tinevez + */ public abstract class CLIConfigurator { @@ -181,7 +189,7 @@ public void select( final String key ) { for ( int i = 0; i < args.size(); i++ ) { - if ( args.get( i ).getKey().equals( key ) ) + if ( key.equals( args.get( i ).getKey() ) ) { this.selected = i; return; @@ -282,60 +290,133 @@ abstract class Adder< A extends Argument< A, O >, T extends Adder< A, T, O >, O protected boolean inCLI = true; // by default + /** + * Specifies the argument to use in the CLI. + * + * @param argument + * the command line argument. + * @return this adder. + */ public T argument( final String argument ) { this.argument = argument; return ( T ) this; } + /** + * Specifies whether this argument will be visible in user interfaces + * generated from the configurator. + * + * @param visible + * UI visibility. + * @return this adder. + */ public T visible( final boolean visible ) { this.visible = visible; return ( T ) this; } + /** + * Specifies a user-friendly name for the argument. + * + * @param name + * the argument name. + * @return this adder. + */ public T name( final String name ) { this.name = name; return ( T ) this; } + /** + * Specifies a help text for the argument. + * + * @param help + * the help text. + * @return this adder. + */ public T help( final String help ) { this.help = help; return ( T ) this; } + /** + * Specifies the key to use to serialize this argument in TrackMate XML + * file. If null, the argument will not be serialized. + * + * @param key + * the argument key. + * @return this adder. + */ public T key( final String key ) { this.key = key; return ( T ) this; } + /** + * Specifies whether this argument is required in the CLI. If + * true, if not set and if there are no default value, an + * error will be thrown. + * + * @param required + * whether this argument is required. + * @return this adder. + */ public T required( final boolean required ) { this.required = required; return ( T ) this; } + /** + * Specifies units for values accepted by this argument. + * + * @param units + * argument value units. + * @return this adder. + */ public T units( final String units ) { this.units = units; return ( T ) this; } + /** + * Specifies a default value for this argument. If the argument is not + * set, it will appear in the command line with this default value. + * + * @param defaultValue + * the argument default value. + * @return this adder. + */ public T defaultValue( final O defaultValue ) { this.defaultValue = defaultValue; return ( T ) this; } + /** + * Specifies whether this argument will appear in the command line. + * + * @param inCLI + * appears in the command line. + * @return this adder. + */ public T inCLI( final boolean inCLI ) { this.inCLI = inCLI; return ( T ) this; } + /** + * Returns the argument created by this builder. + * + * @return the argument. + */ public abstract A get(); } @@ -346,12 +427,28 @@ private abstract class BoundedAdder< A extends BoundedValueArgument< A, O >, T e protected O max; + /** + * Specifies a min for the values accepted by this argument. If the user + * sets a value below this min value, an error is thrown. + * + * @param min + * the min value. + * @return this adder. + */ public T min( final O min ) { this.min = min; return ( T ) this; } + /** + * Specifies a max for the values accepted by this argument. If the user + * sets a value above this max value, an error is thrown. + * + * @param max + * the max value. + * @return this adder. + */ public T max( final O max ) { this.max = max; @@ -487,6 +584,14 @@ private ChoiceAdder() private final List< String > choices = new ArrayList<>(); + /** + * Adds a selectable item for this choice argument. The user will be + * able to select from the list of choices added by this method. + * + * @param choice + * the choice to add. + * @return this adder. + */ public ChoiceAdder addChoice( final String choice ) { if ( !choices.contains( choice ) ) @@ -494,6 +599,14 @@ public ChoiceAdder addChoice( final String choice ) return this; } + /** + * Adds the specified items for this choice argument. The user will be + * able to select from the list of choices added by this method. + * + * @param c + * the choices to add. + * @return this adder. + */ public Adder< ChoiceArgument, ChoiceAdder, String > addChoiceAll( final Collection< String > c ) { for ( final String in : c ) @@ -501,6 +614,17 @@ public Adder< ChoiceArgument, ChoiceAdder, String > addChoiceAll( final Collecti return this; } + /** + * Specifies a default value for this argument. If the argument is not + * set, it will appear in the command line with this default value. + *

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

+ * In the CLI tools we have been trying to implement, they exist in two + * flavors, that are both supported. + * + * If the argument starts with a double-dash --, as in Python + * argparse syntax, then it is understood that setting this flag to true + * makes it appear in the CLI. For instance: --use-gpu. + * + * If the arguments ends with a '=' sign (e.g. "save_txt="), + * then it expects to receive a 'true' or 'false' value. + * + * @return a new flag argument builder. + */ protected FlagAdder addFlag() { return new FlagAdder(); } + /** + * Adds a string argument to the CLI, via a builder. + * + * @return new string argument builder. + */ protected StringAdder addStringArgument() { return new StringAdder(); } + /** + * Adds a path argument to the CLI, via a builder. + * + * @return new path argument builder. + */ protected PathAdder addPathArgument() { return new PathAdder(); } + /** + * Adds a integer argument to the CLI, via a builder. + * + * @return new integer argument builder. + */ protected IntAdder addIntArgument() { return new IntAdder(); } + /** + * Adds a double argument to the CLI, via a builder. + * + * @return new double argument builder. + */ protected DoubleAdder addDoubleArgument() { return new DoubleAdder(); } + /** + * Adds a choice argument to the CLI, via a builder. Such arguments can + * accept a series of discrete values (specified by addChoice() method in + * the builder). + * + * @return new choice argument builder. + */ protected ChoiceAdder addChoiceArgument() { return new ChoiceAdder(); @@ -596,6 +772,9 @@ public static class Flag extends Argument< Flag, Boolean > Flag() {} + /** + * Sets this flag argument to true. + */ public void set() { set( true ); @@ -708,7 +887,7 @@ public static class ChoiceArgument extends AbstractStringArgument< ChoiceArgumen private ChoiceArgument() {} - ChoiceArgument addChoice( final String choice ) + private ChoiceArgument addChoice( final String choice ) { if ( !choices.contains( choice ) ) choices.add( choice ); @@ -1040,7 +1219,7 @@ public String getHelp() public String getKey() { - return ( key == null ) ? getName() : key; + return key; } public abstract void accept( final ArgumentVisitor visitor ); diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java index 41af6992c..eca1de965 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CliGuiBuilder.java @@ -393,6 +393,8 @@ private void addPathToLayout( final String help, final JLabel lbl, final JTextFi final BoxLayout bl = new BoxLayout( p, BoxLayout.LINE_AXIS ); p.setLayout( bl ); + tf.setColumns( 10 ); // Avoid long paths deforming new panels. + lbl.setText( lbl.getText() + " " ); lbl.setFont( Fonts.SMALL_FONT ); tf.setFont( Fonts.SMALL_FONT ); diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java b/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java index ebe01db0e..6aec3e860 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CommandBuilder.java @@ -91,12 +91,22 @@ public void visit( final Flag flag ) val = flag.getValue(); else val = flag.getDefaultValue(); - if ( val ) + + // Deal with flag that have a '=' vs switches. + final String a = flag.getArgument(); + final List< String > vals = translators.getOrDefault( flag, v -> Collections.singletonList( "" + v ) ).apply( val ); + if ( a.endsWith( "=" ) ) { - tokens.add( flag.getArgument() ); - tokens.addAll( translators.getOrDefault( flag, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + tokens.add( a + String.join( ",", vals ) ); + } + else + { + if ( val ) + { + tokens.add( a ); + tokens.addAll( vals ); + } } - } @Override @@ -122,8 +132,19 @@ public void visit( final IntArgument arg ) if ( arg.getMax() != Integer.MAX_VALUE && ( val > arg.getMax() ) ) throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() + "' is larger than the max: " + arg.getMax() ); - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + final String a = arg.getArgument(); + final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } @Override @@ -151,8 +172,19 @@ public void visit( final DoubleArgument arg ) throw new IllegalArgumentException( "Value " + val + " for argument '" + arg.getName() + "' is larger than the max: " + arg.getMax() ); - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + final String a = arg.getArgument(); + final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } private void visitString( final AbstractStringArgument< ? > arg ) @@ -171,8 +203,19 @@ private void visitString( final AbstractStringArgument< ? > arg ) ? arg.getDefaultValue() : arg.getValue(); - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ) ); + final String a = arg.getArgument(); + final List< String > vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( val ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } @Override @@ -198,9 +241,20 @@ public void visit( final ChoiceArgument arg ) // Is not set? -> skip if ( !arg.isSet() ) return; - - tokens.add( arg.getArgument() ); - tokens.addAll( translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( arg.getValue() ) ); + + final String a = arg.getArgument(); + final List vals = translators.getOrDefault( arg, v -> Collections.singletonList( "" + v ) ).apply( arg.getValue() ); + // Does the switch ends in '='? + if ( a.endsWith( "=" ) ) + { + // Concatenante with no space. + tokens.add( a + String.join( ",", vals ) ); + } + else + { + tokens.add( a ); + tokens.addAll( vals ); + } } public static List< String > build( final CLIConfigurator cli ) diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java index 371c7728c..fd465925e 100644 --- a/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CondaCLIConfigurator.java @@ -33,7 +33,7 @@ public abstract class CondaCLIConfigurator extends CLIConfigurator public static final String KEY_CONDA_ENV = "CONDA_ENV"; - private final CondaEnvironmentCommand condaEnv; + protected final CondaEnvironmentCommand condaEnv; public static class CondaEnvironmentCommand extends AbstractStringArgument< CondaEnvironmentCommand > { diff --git a/src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java b/src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java new file mode 100644 index 000000000..167141573 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/util/cli/CondaExecutableCLIConfigurator.java @@ -0,0 +1,71 @@ +package fiji.plugin.trackmate.util.cli; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import ij.IJ; + +/** + * Mother class for CLI configurator that relies on an executable + * installed in a conda environment. This happens when the Python code we want + * to call does not have a Python module (that we could call with 'python -m') + * or is simply not a Python program. In that case: + *

    + *
  • on Mac we resolve the env path, and append the executable name to build + * an absolute path to the executable. + *
  • on Windows we simply activate the conda environment and call the + * executable assuming it is on the path. + *
+ */ +public abstract class CondaExecutableCLIConfigurator extends CondaCLIConfigurator +{ + + public CondaExecutableCLIConfigurator() + { + super(); + + // Add the translator to make a proper cmd line calling conda first. + setTranslator( condaEnv, s -> { + final List< String > cmd = new ArrayList<>(); + final String condaPath = CLIUtils.getCondaPath(); + // Conda and executable stuff. + final String envname = ( String ) s; + if ( IJ.isWindows() ) + { + cmd.addAll( Arrays.asList( "cmd.exe", "/c" ) ); + cmd.addAll( Arrays.asList( condaPath, "activate", envname ) ); + cmd.add( "&" ); + // Add command name + final String executableCommand = getCommand(); + // Split by spaces + final String[] split = executableCommand.split( " " ); + cmd.addAll( Arrays.asList( split ) ); + return cmd; + + } + else + { + try + { + final String pythonPath = CLIUtils.getEnvMap().get( envname ); + final int i = pythonPath.lastIndexOf( "python" ); + final String binPath = pythonPath.substring( 0, i ); + final String executablePath = binPath + getCommand(); + final String[] split = executablePath.split( " " ); + cmd.addAll( Arrays.asList( split ) ); + return cmd; + } + catch ( final IOException e ) + { + System.err.println( "Could not find the conda executable or change the conda environment.\n" + + "Please configure the path to your conda executable in Edit > Options > Configure TrackMate Conda path..." ); + e.printStackTrace(); + } + } + return null; + } ); + } + +} diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java index 1fc20c149..d091862d2 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/SpotEditTool.java @@ -355,7 +355,6 @@ public void keyPressed( final KeyEvent e ) case KeyEvent.VK_SPACE: { actions.startMoveSpot(); - e.consume(); break; } diff --git a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java index 077456d15..ed6611395 100644 --- a/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java +++ b/src/main/java/fiji/plugin/trackmate/visualization/hyperstack/TrackOverlay.java @@ -211,7 +211,8 @@ public final synchronized void drawOverlay( final Graphics g ) final Set< DefaultWeightedEdge > track; synchronized ( model ) { - track = new HashSet<>( model.getTrackModel().trackEdges( trackID ) ); + final Set< DefaultWeightedEdge > edges = model.getTrackModel().trackEdges( trackID ); + track = ( edges == null ) ? new HashSet<>() : new HashSet<>( edges ); } for ( final DefaultWeightedEdge edge : track ) {