diff --git a/.ci/settings.xml b/.ci/settings.xml new file mode 100644 index 000000000..ad6b5b222 --- /dev/null +++ b/.ci/settings.xml @@ -0,0 +1,6 @@ + + + build + + diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index c1e45bbaf..6d8e9f5c1 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -50,7 +50,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) - MAVEN_OPTS: -Xmx4096m run: | export DISPLAY=:99 sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & diff --git a/README.md b/README.md index 93e8d0cfb..9e13a243e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ * [Lineage Tree Classification](#lineage-tree-classification) * [Parameters](#parameters) * [Example](#example) +* [Imports](#imports) + * [Import Spots from Label Image](#import-spots-from-label-image) * [Exports](#exports) * [Label Image Exporter](#label-image-exporter) * [GraphML Exporter](#graphml-exporter) @@ -215,6 +217,85 @@ Tree2 * The resulting tag set used for coloring the track scheme branch view. ![Trackscheme Branch View with tags](doc/deep_lineage/lineage_classification/trackscheme_branch_with_tags.png) +## Imports + +### Import Spots from Label Image + +* You can use the plugin to import spots from a label image representing an instance segmentation into Mastodon. This + may be useful if you have an instance segmentation of cells or other objects, and you want to track them using + Mastodon. +* The label image is expected to contain the spot ids as pixel values. +* The label image is expected to have the same dimensions as the image data in Mastodon. +* Labels are processed frame by frame. +* Multiple blobs with the same id in the same frame are considered to belong to the same spot by this importer. It is + advised to use a unique ids for spots in the same frame. +* The resulting spots are ellipsoids with the semi axes computed from the variance covariance matrix of this pixel + positions of each label. +* The resulting spots may be linked using the linker plugin in Mastodon (`Plugins > Tracking > Linking...`) + or [Elephant](https://elephant-track.github.io/#/?id=linking-workflow). + +#### Parameters + +* Ellipsoid scaling factor: The scaling factor to apply to the ellipsoids. The default is 1.0. The scaling factor is + applied to the semi axes of the ellipsoids. The ellipsoid scaling factor can be used to increase (>1) or decrease ( + <1) the size of the resulting ellipsoid. 1 is equivalent of ellipsoids drawn at 2.2σ. +* Link spots having the same id in consecutive frames: If checked, spots with the same id in consecutive frames are + linked. + +#### Instance segmentation as active image in ImageJ + +* The label image can be opened in ImageJ and the plugin can be called from the + menu: ![Plugin Import Menu](doc/deep_lineage/import/label_image/imagej/plugin_import_imagej_menu.png) +* Please make sure that the label image is the active image in ImageJ. +* Please make sure that the label image has the same dimensions as the image data in Mastodon. + * You can use the `Image > Properties` command ImageJ to check (and) set the dimensions of the label image. + +##### Example + +* Example + dataset: [Fluo-C3DL-MDA231 from Cell Tracking Challenge](http://data.celltrackingchallenge.net/training-datasets/Fluo-C3DL-MDA231.zip) + * Extract the file to a folder named `Fluo-C3DL-MDA231` + * Import the image sequence with the actual image into ImageJ contained in folder `Fluo-C3DL-MDA231/01/` + * `File > Import > Image Sequence...` + * Set the dimensions of the image sequence to 512x512x1x30x12 (XYCTZ) using `Image > Properties` + * ![plugin_import_example_1.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_1.png) + * Open Mastodon from Fiji and create a new project with the image sequence + * `Plugins > Mastodon > new Mastodon project > Use an image opened in ImageJ > Create` + * ![plugin_import_example_2.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_2.png) + * Import the image sequence encoding the label images into ImageJ contained in folder: `Fluo-C3DL-MDA231/01_ERR_SEG/` + * Set the dimensions of the label image to 512x512x1x30x12 (XYCTZ) using `Image > Properties` + * ![plugin_import_example_3.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_3.png) + * Open Import window in Mastodon: `Plugins > Imports > Import spots from label image > Import spots from ImageJ image` + * Click `OK` and the spots are imported into Mastodon. + * ![plugin_import_example_4.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_4.png) + +#### Instance segmentation as BDV channel + +* The plugin can be called from the + menu: ![Plugin Import Menu](doc/deep_lineage/import/label_image/imagej/plugin_import_imagej_menu.png) + +##### Example + +* Example + dataset: [Fluo-C3DL-MDA231 from Cell Tracking Challenge](http://data.celltrackingchallenge.net/training-datasets/Fluo-C3DL-MDA231.zip) + * Extract the file to a folder named `Fluo-C3DL-MDA231` + * Import the image sequence with the actual image into ImageJ contained in folder `Fluo-C3DL-MDA231/01/` + * `File > Import > Image Sequence...` + * Set the dimensions of the image sequence to 512x512x1x30x12 (XYCTZ) using `Image > Properties` + * ![plugin_import_example_1.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_1.png) + * Import the image sequence encoding the label images into ImageJ contained in folder: `Fluo-C3DL-MDA231/01_ERR_SEG/` + * Set the dimensions of the label image to 512x512x1x30x12 (XYCTZ) using `Image > Properties` + * ![plugin_import_example_3.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_3.png) + * Merge the 2 images into a single image using the `Image > Color > Merge Channels...` command + * ![plugin_import_example_5.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_5.png) + * Open Mastodon from Fiji and create a new project with merged image + * `Plugins > Mastodon > new Mastodon project > Use an image opened in ImageJ > Create` + * ![plugin_import_example_6.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_6.png) + * Open Import window: `Plugins > Imports > Import spots from label image > Import spots from BDV channel` + * Select the BDV channel containing the label image + * Click `OK` and the spots are imported into Mastodon. + * ![plugin_import_example_7.png](doc/deep_lineage/import/label_image/imagej/plugin_import_example_7.png) + ## Exports ### Label Image Exporter diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_bdv_menu.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_bdv_menu.png new file mode 100644 index 000000000..b2143c617 Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_bdv_menu.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_1.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_1.png new file mode 100644 index 000000000..137f5f363 Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_1.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_2.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_2.png new file mode 100644 index 000000000..5fa87da1e Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_2.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_3.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_3.png new file mode 100644 index 000000000..7eedf21d2 Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_3.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_4.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_4.png new file mode 100644 index 000000000..1c4acb1ba Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_4.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_5.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_5.png new file mode 100644 index 000000000..10f1f4a0f Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_5.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_6.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_6.png new file mode 100644 index 000000000..c95c742a4 Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_6.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_example_7.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_7.png new file mode 100644 index 000000000..e823cedfd Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_example_7.png differ diff --git a/doc/deep_lineage/import/label_image/imagej/plugin_import_imagej_menu.png b/doc/deep_lineage/import/label_image/imagej/plugin_import_imagej_menu.png new file mode 100644 index 000000000..d3c373c0d Binary files /dev/null and b/doc/deep_lineage/import/label_image/imagej/plugin_import_imagej_menu.png differ diff --git a/pom.xml b/pom.xml index 87525dfd6..d03eafcae 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ Mastodon authors Stefan Hahmann - 1.0.0-beta-29 + 1.0.0-beta-30-SNAPSHOT org.mastodon 2.97.1 @@ -201,15 +201,13 @@ build - - - + org.apache.maven.plugins maven-surefire-plugin 3.1.0 - -Xmx4096m + -Xmx1g @@ -219,6 +217,15 @@ coverage + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.0 + + @{argLine} -Xmx1g + + org.jacoco jacoco-maven-plugin diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/ImportSpotsFromLabelImagePlugin.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/ImportSpotsFromLabelImagePlugin.java new file mode 100644 index 000000000..bd8525190 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/ImportSpotsFromLabelImagePlugin.java @@ -0,0 +1,80 @@ +package org.mastodon.mamut.io.importer.labelimage; + +import org.mastodon.app.ui.ViewMenuBuilder; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.ui.ImportSpotsFromImgPlusView; +import org.mastodon.mamut.plugin.MamutPlugin; +import org.mastodon.mamut.io.importer.labelimage.ui.ImportSpotsFromBdvChannelView; +import org.scijava.command.CommandService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.ui.behaviour.util.AbstractNamedAction; +import org.scijava.ui.behaviour.util.Actions; +import org.scijava.ui.behaviour.util.RunnableAction; + +import java.util.Collections; +import java.util.List; + +import static org.mastodon.app.ui.ViewMenuBuilder.item; +import static org.mastodon.app.ui.ViewMenuBuilder.menu; + +@SuppressWarnings( "unused" ) +@Plugin( type = MamutPlugin.class ) +public class ImportSpotsFromLabelImagePlugin implements MamutPlugin +{ + private static final String IMPORT_SPOTS_FROM_IMAGEJ = "Import spots from ImageJ image"; + + private static final String IMPORT_SPOTS_FROM_BDV_CHANNEL = "Import spots from BDV channel"; + + private static final String[] SHORT_CUTS = { "not mapped" }; + + private final AbstractNamedAction imageJImport; + + private final AbstractNamedAction bdvChannelImport; + + private ProjectModel appModel; + + @SuppressWarnings( "unused" ) + @Parameter + private CommandService commandService; + + @SuppressWarnings( "unused" ) + public ImportSpotsFromLabelImagePlugin() + { + imageJImport = new RunnableAction( IMPORT_SPOTS_FROM_IMAGEJ, this::importSpotsFromImageJ ); + bdvChannelImport = new RunnableAction( IMPORT_SPOTS_FROM_BDV_CHANNEL, this::importSpotsFromBdvChannel ); + } + + @Override + public void setAppPluginModel( final ProjectModel appPluginModel ) + { + this.appModel = appPluginModel; + } + + @Override + public List< ViewMenuBuilder.MenuItem > getMenuItems() + { + return Collections.singletonList( + menu( "Plugins", menu( "Imports", + menu( "Import spots from label image", item( IMPORT_SPOTS_FROM_IMAGEJ ), + item( IMPORT_SPOTS_FROM_BDV_CHANNEL ) ) ) ) ); + } + + @Override + public void installGlobalActions( Actions actions ) + { + actions.namedAction( imageJImport, SHORT_CUTS ); + actions.namedAction( bdvChannelImport, SHORT_CUTS ); + } + + private void importSpotsFromImageJ() + { + commandService.run( ImportSpotsFromImgPlusView.class, true, "projectModel", appModel ); + } + + private void importSpotsFromBdvChannel() + { + commandService.run( ImportSpotsFromBdvChannelView.class, true, "projectModel", appModel ); + } + +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/LabelImageUtils.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/LabelImageUtils.java new file mode 100644 index 000000000..9bd26018c --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/LabelImageUtils.java @@ -0,0 +1,383 @@ +package org.mastodon.mamut.io.importer.labelimage; + +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.sequence.TimePoint; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imagej.ImgPlus; +import net.imglib2.Cursor; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.type.numeric.RealType; +import net.imglib2.util.Cast; +import net.imglib2.util.Pair; +import net.imglib2.util.ValuePair; +import net.imglib2.view.Views; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.math.CovarianceMatrix; +import org.mastodon.mamut.io.importer.labelimage.math.MeansVector; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.util.LineageTreeUtils; +import org.mastodon.views.bdv.SharedBigDataViewerData; +import org.scijava.app.StatusService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.IntFunction; + +/** + * Utility class to create spots from label images. + */ +public class LabelImageUtils +{ + + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private LabelImageUtils() + { + // prevent from instantiation + } + + /** + * Creates spots from the given label image.
+ * The method runs twice through each image (i.e. each frame). Once to determine maximum/minimum values for array initialization, and once to do summation for covariance and mean. + * @param frameProvider a function that provides the image data for each frame. + * @param model the model to add the spots to. + * @param scaleFactor the scale factor to use for the ellipsoid. 1 means 2.2σ and is the default. + * @param linkSpotsWithSameLabels whether to link spots with the same labels. + * @param sequenceDescription the sequence description of the image data. Contains the voxel dimensions and the time points. + * @param statusService the status service to report progress to. + */ + static void createSpotsFromLabelImage( final IntFunction< RandomAccessibleInterval< RealType< ? > > > frameProvider, + final Model model, final double scaleFactor, final boolean linkSpotsWithSameLabels, + final AbstractSequenceDescription< ?, ?, ? > sequenceDescription, + final StatusService statusService ) + { + final ModelGraph graph = model.getGraph(); + final List< TimePoint > frames = sequenceDescription.getTimePoints().getTimePointsOrdered(); + // NB: Use the dimensions of the first source and the first time point only without checking if they are equal in other sources and time points. + final VoxelDimensions voxelDimensions = sequenceDescription.getViewSetups().get( 0 ).getVoxelSize(); + ReentrantReadWriteLock lock = graph.getLock(); + lock.writeLock().lock(); + int count = 0; + try + { + int numTimepoints = frames.size(); + for ( int i = 0; i < numTimepoints; i++ ) + { + TimePoint frame = frames.get( i ); + int frameId = frame.getId(); + final RandomAccessibleInterval< RealType< ? > > rai = frameProvider.apply( frameId ); + count += createSpotsForFrame( graph, rai, frameId, voxelDimensions, scaleFactor ); + if ( statusService != null ) + statusService.showProgress( i + 1, numTimepoints ); + } + if ( linkSpotsWithSameLabels ) + LineageTreeUtils.linkSpotsWithSameLabel( model ); + model.setUndoPoint(); + } + finally + { + lock.writeLock().unlock(); + } + graph.notifyGraphChanged(); + logger.info( "Created {} new spot(s) in {} frame(s).", count, frames.size() ); + } + + /** + * Create spots for the given frame. + * @param graph the graph to add the spots to. + * @param frame the image data to read and process. + * @param frameId the frame id the spots should belong to. + * @param voxelDimensions the dimensions of the voxels in the image. + * @param scaleFactor the scale factor to use for the ellipsoid. 1 means 2.2σ and is the default. + * @return the number of spots created. + */ + private static int createSpotsForFrame( final ModelGraph graph, final RandomAccessibleInterval< RealType< ? > > frame, + final int frameId, final VoxelDimensions voxelDimensions, final double scaleFactor ) + { + logger.debug( "Computing mean, covariance of all labels at frame {}", frameId ); + logger.debug( "Dimensions of frame: {}, {}, {}", frame.dimension( 0 ), frame.dimension( 1 ), frame.dimension( 2 ) ); + + // get the maximum value possible to learn how many objects need to be instantiated + Pair< Integer, Integer > minAndMax = getPixelValueInterval( frame ); + + int minimumLabel = minAndMax.getA(); + int maximumLabel = minAndMax.getB(); + if ( minimumLabel == Integer.MAX_VALUE || maximumLabel == Integer.MIN_VALUE ) + { + logger.debug( "No labels found in frame {}", frameId ); + return 0; + } + int numLabels = maximumLabel - minimumLabel + 1; + if ( numLabels > 10_000 ) // NB: 10_000 is arbitrary, but such a high number of labels is suspicious, thus we log a warning here + logger.warn( "found {} labels, are you sure you used the correct image source?", numLabels ); + + logger.debug( "Found {} label(s) in frame {}. Range: [{}, {}]", numLabels, frameId, minimumLabel, maximumLabel ); + Label[] labels = extractLabelsFromFrame( frame, minimumLabel, numLabels ); + return createSpotsFromFrameLabels( graph, frameId, labels, voxelDimensions, scaleFactor ); + } + + /** + * Read the frame and determine the minimum and maximum pixel values in the frame. + * @param frame an image to read and process + * @return A pair of values (min, max) that represent the minimum and maximum pixel values in the image + * @author Noam Dori + */ + private static Pair< Integer, Integer > getPixelValueInterval( final RandomAccessibleInterval< RealType< ? > > frame ) + { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + Cursor< RealType< ? > > cursor = Views.iterable( frame ).cursor(); + while ( cursor.hasNext() ) + { + int val = ( int ) cursor.next().getRealDouble(); + // we ignore 0 as it is background + if ( val == 0 ) + continue; + if ( min > val ) + min = val; + if ( max < val ) + max = val; + } + return new ValuePair<>( min, max ); + } + + /** + * Create spots from the labels in the given frame. + * @param graph the graph to add the spots to. + * @param frameId the frame id the spots should belong to. + * @param labels the labels in the frame. + * @param voxelDimensions the dimensions of the voxels in the image. + * @param scaleFactor the size factor to use for the ellipsoid. 1 means 2.2σ and is the default. + * @return the number of spots created. + */ + private static int createSpotsFromFrameLabels( final ModelGraph graph, final int frameId, final Label[] labels, + final VoxelDimensions voxelDimensions, final double scaleFactor ) + { + int count = 0; + // combine the sums into mean and covariance matrices, then add the corresponding spot + for ( final Label label : labels ) + { + // skip labels that are not present in the image or have only one pixel + if ( label == null || label.numPixels < 2 ) + continue; + double[] mean = label.covariances.getMeans(); + double[][] cov = label.covariances.get(); + scale( mean, voxelDimensions ); + scale( cov, scaleFactor, voxelDimensions ); + try + { + Spot spot = graph.addVertex().init( frameId, mean, cov ); + spot.setLabel( String.valueOf( label.value ) ); + count++; + } + catch ( Exception e ) + { + logger.trace( "Could not add vertex to graph. Mean: {}, Covariance: {}", Arrays.toString( mean ), + Arrays.deepToString( cov ) ); + } + } + logger.debug( "Added {} spot(s) to frame {}", count, frameId ); + return count; + } + + /** + * Extracts the labels from the given frame by iterating once over all pixels of that frame and computing the means and covariances for all labels. + * @param frame the pointer to the image to read. + * @param minimumLabelValue the minimum value of the pixels in the image. + * @param numLabels the number of labels in the frame. + */ + private static Label[] extractLabelsFromFrame( final RandomAccessibleInterval< RealType< ? > > frame, int minimumLabelValue, + int numLabels ) + { + Label[] labels = new Label[ numLabels ]; + // read all pixels of the picture to sum everything up + Cursor< RealType< ? > > cursor = Views.iterable( frame ).cursor(); + int[] pixel = new int[ cursor.numDimensions() ]; + while ( cursor.hasNext() ) + { + int pixelValue = ( int ) cursor.next().getRealDouble(); + int labelIndex = pixelValue - minimumLabelValue; + if ( pixelValue <= 0 ) + continue; + if ( labels[ labelIndex ] == null ) + labels[ labelIndex ] = new Label( pixelValue, cursor.numDimensions() ); + Label label = labels[ labelIndex ]; + cursor.localize( pixel ); + label.addPixel( pixel ); + } + return labels; + } + + /** + * Scales the mean vector by the voxel dimensions. + * @param mean the mean vector to scale. + * @param voxelDimensions the dimensions of the voxels in the image. + */ + private static void scale( final double[] mean, final VoxelDimensions voxelDimensions ) + { + if ( mean.length != voxelDimensions.numDimensions() ) + throw new IllegalArgumentException( "Mean vector has wrong dimension." ); + for ( int i = 0; i < mean.length; i++ ) + mean[ i ] = mean[ i ] * voxelDimensions.dimension( i ); + } + + /** + * Returns the dimensions of the given image as an array in the order x, y, z, t. + * @param imgPlus the image to get the dimensions from. + * @return the dimensions of the image. + */ + public static long[] getImgPlusDimensions( final ImgPlus< ? > imgPlus ) + { + long[] imgPlusDimensions = imgPlus.getImg().dimensionsAsLongArray(); + int numDimensions = imgPlusDimensions.length; + long imgPlusX = imgPlusDimensions[ 0 ]; + long imgPlusY = imgPlusDimensions[ 1 ]; + long imgPlusZ = numDimensions > 2 ? imgPlusDimensions[ 2 ] : 0; + long imgPlusT = numDimensions > 3 ? imgPlusDimensions[ 3 ] : 0; + return new long[] { imgPlusX, imgPlusY, imgPlusZ, imgPlusT }; + } + + /** + * Returns the dimensions of the given big data viewer image as an array in the order x, y, z, t. + * @param sharedBdvData the big data viewer data to get the dimensions from. + * @return the dimensions of the big data viewer image. + */ + public static long[] getBdvDimensions( final SharedBigDataViewerData sharedBdvData ) + { + AbstractSequenceDescription< ?, ?, ? > sequenceDescription = sharedBdvData.getSpimData().getSequenceDescription(); + int bdvTimepoints = sequenceDescription.getTimePoints().size(); + long[] bdvSpatialDimensions = sequenceDescription.getViewSetups().get( 0 ).getSize().dimensionsAsLongArray(); + return new long[] { bdvSpatialDimensions[ 0 ], bdvSpatialDimensions[ 1 ], bdvSpatialDimensions[ 2 ], bdvTimepoints }; + } + + /** + * Checks if the dimensions of the given big data viewer image match the dimensions of the given image. + * @param sharedBigDataViewerData the big data viewer data to check the dimensions against. + * @param imgPlus the image to check the dimensions against. + * @return true if the dimensions match, false otherwise. + */ + public static boolean dimensionsMatch( final SharedBigDataViewerData sharedBigDataViewerData, final ImgPlus< ? > imgPlus ) + { + long[] bdvDimensions = getBdvDimensions( sharedBigDataViewerData ); + long[] imgPlusDimensions = getImgPlusDimensions( imgPlus ); + return Arrays.equals( bdvDimensions, imgPlusDimensions ); + } + + /** + * Imports spots from the given ImageJ image into the given project model. + * @param projectModel the project model to add the spots to. + * @param imgPlus the image to import the spots from. + * @param scaleFactor the scale factor to use for the ellipsoid. 1 means 2.2σ and is the default. + * @param linkSpotsWithSameLabels whether to link spots with the same labels. + * @throws IllegalArgumentException if the dimensions of the given image do not match the dimensions of the big data viewer image contained in the project model. + */ + public static void importSpotsFromImgPlus( final ProjectModel projectModel, final ImgPlus< ? > imgPlus, final double scaleFactor, + final boolean linkSpotsWithSameLabels ) + { + logger.debug( "ImageJ image: {}", imgPlus.getName() ); + final SharedBigDataViewerData sharedBdvData = projectModel.getSharedBdvData(); + if ( !dimensionsMatch( sharedBdvData, imgPlus ) ) + throw new IllegalArgumentException( "The dimensions of the ImageJ image " + imgPlus.getName() + + " do not match the dimensions of the big data viewer image." ); + IntFunction< RandomAccessibleInterval< RealType< ? > > > frameProvider = + frameId -> Cast.unchecked( Views.hyperSlice( imgPlus.getImg(), 3, frameId ) ); + createSpotsFromLabelImage( frameProvider, projectModel.getModel(), scaleFactor, linkSpotsWithSameLabels, + sharedBdvData.getSpimData().getSequenceDescription(), + projectModel.getContext().getService( StatusService.class ) ); + } + + /** + * Imports spots from the given big data viewer channel into the given project model. + * @param projectModel the project model to add the spots to. + * @param source the source to import the spots from. + * @param scaleFactor the scale factor to use for the ellipsoid. 1 means 2.2σ and is the default. + * @param linkSpotsWithSameLabels whether to link spots with the same labels. + * @throws IllegalArgumentException if the label channel index is out of bounds, i.e. if it is greater than the number of channels in the big data viewer source contained in the project model. + */ + public static void importSpotsFromBdvChannel( final ProjectModel projectModel, final Source< ? > source, final double scaleFactor, + final boolean linkSpotsWithSameLabels ) + { + IntFunction< RandomAccessibleInterval< RealType< ? > > > frameProvider = + frameId -> Cast.unchecked( source.getSource( frameId, 0 ) ); + createSpotsFromLabelImage( frameProvider, projectModel.getModel(), scaleFactor, linkSpotsWithSameLabels, + projectModel.getSharedBdvData().getSpimData().getSequenceDescription(), + projectModel.getContext().getService( StatusService.class ) ); + } + + /** + * Scales the covariance matrix the given sigma and the voxel dimensions. + * @param covariance the covariance matrix to scale. + * @param scaleFactor the factor to scale the covariance matrix with. + * @param voxelDimensions the dimensions of the voxels in the image. + * @throws IllegalArgumentException if the covariance matrix has not the same dimensions as the given voxelDimensions. + */ + public static void scale( final double[][] covariance, final double scaleFactor, final VoxelDimensions voxelDimensions ) + { + if ( covariance.length != voxelDimensions.numDimensions() ) + throw new IllegalArgumentException( "Covariance matrix has wrong dimension." ); + for ( int i = 0; i < covariance.length; i++ ) + { + if ( covariance[ i ].length != voxelDimensions.numDimensions() ) + throw new IllegalArgumentException( "Covariance matrix has wrong dimension." ); + for ( int j = i; j < covariance.length; j++ ) + { + covariance[ i ][ j ] *= Math.pow( scaleFactor, 2 ) * 5 * voxelDimensions.dimension( i ) * voxelDimensions.dimension( j ); + // the covariance matrix is symmetric! + if ( i != j ) + covariance[ j ][ i ] = covariance[ i ][ j ]; + } + } + } + + /** + * Returns the names of the sources contained in the given big data viewer data. + * @param sharedBdvData the big data viewer data to get the source names from. + * @return the names of the sources. + */ + public static List< String > getSourceNames( final SharedBigDataViewerData sharedBdvData ) + { + final List< SourceAndConverter< ? > > sources = sharedBdvData.getSources(); + List< String > choices = new ArrayList<>(); + for ( SourceAndConverter< ? > source : sources ) + choices.add( source.getSpimSource().getName() ); + return choices; + } + + /** + * A class to hold the pixel count, mean and covariances of a label in the image. + */ + private static class Label + { + private final int value; + + private int numPixels; + + private final MeansVector means; + + private final CovarianceMatrix covariances; + + private Label( final int value, final int numDimensions ) + { + this.value = value; + numPixels = 0; + means = new MeansVector( numDimensions ); + covariances = new CovarianceMatrix( numDimensions ); + } + + private void addPixel( int[] pixel ) + { + numPixels++; + means.addValues( pixel ); + covariances.addValues( pixel ); + } + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/Covariance.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/Covariance.java new file mode 100644 index 000000000..a7e6b9815 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/Covariance.java @@ -0,0 +1,60 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +/** + * Computes the covariance for two independent variables.
+ * Uses an online algorithm to compute the covariance, cf.: Online algorithm for covariance + */ +public class Covariance +{ + private double meanX = 0; + + private double meanY = 0; + + private double c = 0; + + private int n = 0; + + /** + * Add a new pair of values to the covariance computation. + * @param x the first value + * @param y the second value + */ + public void addValues( double x, double y ) + { + n++; + double dx = x - meanX; + meanX += dx / n; + meanY += ( y - meanY ) / n; + c += dx * ( y - meanY ); + } + + /** + * Gets the covariance. + * @throws IllegalArgumentException if the number of samples is less than 2 + * @return the covariance + */ + public double get() + { + if ( n < 2 ) + throw new IllegalArgumentException( "Number of samples is less than 2." ); + return c / ( n - 1 ); + } + + /** + * Gets the mean of the first variable. + * @return the mean of the first variable + */ + public double getMeanX() + { + return meanX; + } + + /** + * Gets the mean of the second variable. + * @return the mean of the second variable + */ + public double getMeanY() + { + return meanY; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceMatrix.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceMatrix.java new file mode 100644 index 000000000..a51d9ea36 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceMatrix.java @@ -0,0 +1,71 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +/** + * Computes the covariance matrix for independent values given as value vectors.
+ * Uses an online algorithm to compute the covariance, cf.: Online algorithm for covariance + */ +public class CovarianceMatrix +{ + private final Covariance[][] c; + + /** + * Create a new covariance matrix for the given number of dimensions. + * @param dimensions the number of dimensions + */ + public CovarianceMatrix( int dimensions ) + { + c = new Covariance[ dimensions ][ dimensions ]; + } + + /** + * Add a new value vector to the covariance matrix computation.
+ * The vector must have the same length as the number of dimensions of the covariance matrix. + * @param x the value vector + */ + public void addValues( int[] x ) + { + if ( x.length != c.length ) + throw new IllegalArgumentException( "Input array has wrong length." ); + for ( int i = 0; i < x.length; i++ ) + { + for ( int j = i; j < x.length; j++ ) + { + if ( c[ i ][ j ] == null ) + c[ i ][ j ] = new Covariance(); + c[ i ][ j ].addValues( x[ i ], x[ j ] ); + } + } + } + + /** + * Gets the covariance matrix. + * @throws IllegalArgumentException if the number of samples is less than 2 + * @return the covariance matrix + */ + public double[][] get() + { + double[][] result = new double[ c.length ][ c.length ]; + for ( int i = 0; i < c.length; i++ ) + { + for ( int j = i; j < c.length; j++ ) + { + result[ i ][ j ] = c[ i ][ j ].get(); + if ( i != j ) + result[ j ][ i ] = result[ i ][ j ]; + } + } + return result; + } + + /** + * Gets the means of the variables. + * @return the means of the variables + */ + public double[] getMeans() + { + double[] result = new double[ c.length ]; + for ( int i = 0; i < c.length; i++ ) + result[ i ] = c[ i ][ i ].getMeanX(); + return result; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/Mean.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/Mean.java new file mode 100644 index 000000000..839d16e53 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/Mean.java @@ -0,0 +1,31 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +/** + * Computes the mean of a set of values. + */ +public class Mean +{ + private long sum = 0; + + private int n; + + /** + * Add a new value to the mean computation. + * @param x the value + */ + public void addValue( long x ) + { + n++; + sum += x; + } + + /** + * Gets the mean. + * Returns {@link Double#NaN}, if no samples have been added yet. + * @return the mean + */ + public double get() + { + return ( double ) sum / n; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/MeansVector.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/MeansVector.java new file mode 100644 index 000000000..294a3756e --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/math/MeansVector.java @@ -0,0 +1,48 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +/** + * Computes the mean of a set of values.
+ */ +public class MeansVector +{ + private final Mean[] means; + + /** + * Create a new means vector for the given number of dimensions. + * @param dimensions the number of dimensions + */ + public MeansVector( int dimensions ) + { + means = new Mean[ dimensions ]; + } + + /** + * Add a new value vector to the means computation.
+ * The vector must have the same length as the number of dimensions of the means vector. + * @param x the value vector + */ + public void addValues( int[] x ) + { + if ( x.length != means.length ) + throw new IllegalArgumentException( "Input array has wrong length." ); + for ( int i = 0; i < x.length; i++ ) + { + if ( means[ i ] == null ) + means[ i ] = new Mean(); + means[ i ].addValue( x[ i ] ); + } + } + + /** + * Gets the means vector. + * Returns a vector with {@link Double#NaN}s, if no samples have been added yet. + * @return the means vector + */ + public double[] get() + { + double[] result = new double[ means.length ]; + for ( int i = 0; i < means.length; i++ ) + result[ i ] = means[ i ].get(); + return result; + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromBdvChannelView.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromBdvChannelView.java new file mode 100644 index 000000000..e76d12e0d --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromBdvChannelView.java @@ -0,0 +1,63 @@ +package org.mastodon.mamut.io.importer.labelimage.ui; + +import bdv.viewer.SourceAndConverter; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.LabelImageUtils; +import org.scijava.ItemVisibility; +import org.scijava.command.Command; +import org.scijava.command.DynamicCommand; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; + +import java.util.List; +import java.util.Optional; + +@Plugin( type = Command.class, label = "Import spots from BDV channel" ) +public class ImportSpotsFromBdvChannelView extends DynamicCommand +{ + private static final int WIDTH = 15; + + @SuppressWarnings( "all" ) + @Parameter( visibility = ItemVisibility.MESSAGE, required = false, persist = false ) + private String documentation = "\n" + + "\n" + + "

Import spots from instance segmentation in BDV channel

\n" + + "

This command can import spots from label image data contained in a channel of the Big Data Viewer. The image data in that channel is assumed to represent an instance segmentation (i.e. a label image) that has been processed outside Mastodon. The existing labels in that channel will be used as spot names.

\n" + + "

The BDV channel that contains the labels has to be chosen.

\n" + + "

The ellipsoid scaling factor can be used to increase (>1) or decrease (<1) the size of the resulting ellipsoid. 1 is equivalent of ellipsoids drawn at 2.2σ.

\n" + + "\n" + + "\n"; + + @SuppressWarnings( "unused" ) + @Parameter + private ProjectModel projectModel; + + @Parameter( label = "Instance segmentation source", initializer = "initImgSourceChoices" ) + public String imgSourceChoice = ""; + + @SuppressWarnings( "all" ) + @Parameter( label = "Ellipsoid scaling factor", min = "0", description = "Changes the size of the resulting ellipsoid in all dimensions. 1 means that the ellipsoid is drawn at 2.2σ, which is the default." ) + private double scaleFactor = 1; + + @SuppressWarnings( "all" ) + @Parameter( label = "Link spots having the same labels in consecutive frames", description = "This option assumes that labels from the input images are unique for one tracklet." ) + boolean linkSpotsWithSameLabels = false; + + @SuppressWarnings( "unused" ) + private void initImgSourceChoices() + { + List< String > choices = LabelImageUtils.getSourceNames( projectModel.getSharedBdvData() ); + getInfo().getMutableInput( "imgSourceChoice", String.class ).setChoices( choices ); + } + + @Override + public void run() + { + Optional< SourceAndConverter< ? > > sourceAndConverter = projectModel.getSharedBdvData().getSources().stream() + .filter( source -> source.getSpimSource().getName().equals( imgSourceChoice ) ).findFirst(); + if ( !sourceAndConverter.isPresent() ) + return; + LabelImageUtils.importSpotsFromBdvChannel( projectModel, sourceAndConverter.get().getSpimSource(), scaleFactor, + linkSpotsWithSameLabels ); + } +} diff --git a/src/main/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromImgPlusView.java b/src/main/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromImgPlusView.java new file mode 100644 index 000000000..ae6ed9d25 --- /dev/null +++ b/src/main/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromImgPlusView.java @@ -0,0 +1,88 @@ +package org.mastodon.mamut.io.importer.labelimage.ui; + +import net.imagej.ImgPlus; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.LabelImageUtils; +import org.scijava.ItemIO; +import org.scijava.ItemVisibility; +import org.scijava.command.Command; +import org.scijava.command.ContextCommand; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import java.util.Arrays; + +@Plugin( type = Command.class, label = "Import spots from ImageJ image" ) +public class ImportSpotsFromImgPlusView< T > extends ContextCommand +{ + + private static final int WIDTH = 15; + + private static final String TEMPLATE = "\n" + + "\n" + + "

Import spots from label image in ImageJ.

\n" + + "

This command can import spots from the active image in ImageJ that contains an instance segmentation that has been processed outside Mastodon. The label image is assumed to match the existing big data viewer image in all dimensions (x,y,z and t). The existing labels will be used as spot names.

\n" + + "

The ellipsoid scaling factor can be used to increase (>1) or decrease (<1) the size of the resulting ellipsoid. 1 is equivalent of ellipsoids drawn at 2.2σ.

\n" + + "

The active image in ImageJ is: %s.
\n" + + "

It has the these dimensions: x=%s, y=%s, z=%s, t=%s.

\n" + + "

The big data viewer image has these dimensions: x=%s, y=%s, z=%s, t=%s.

\n" + + "

%s

\n" + + "\n" + + "\n"; + + @Parameter( type = ItemIO.INPUT, validater = "validateImageData" ) + private ImgPlus< T > imgPlus; + + @SuppressWarnings( "unused" ) + @Parameter( visibility = ItemVisibility.MESSAGE, required = false, persist = false, initializer = "initMessage" ) + private String documentation; + + @SuppressWarnings( "unused" ) + @Parameter + private ProjectModel projectModel; + + @SuppressWarnings( "all" ) + @Parameter( label = "Ellipsoid scaling factor", min = "0", description = "Changes the size of the resulting ellipsoid in all dimensions. 1 means that the ellipsoid is drawn at 2.2σ, which is the default." ) + private double scaleFactor = 1; + + @SuppressWarnings( "all" ) + @Parameter( label = "Link spots having the same labels in consecutive frames", description = "This option assumes that labels from the input images are unique for one tracklet." ) + boolean linkSpotsWithSameLabels = false; + + @SuppressWarnings( "unused" ) + private void validateImageData() + { + if ( !LabelImageUtils.dimensionsMatch( projectModel.getSharedBdvData(), imgPlus ) ) + { + String imgPlusDimensions = Arrays.toString( LabelImageUtils.getImgPlusDimensions( imgPlus ) ); + String bdvDimensions = Arrays.toString( LabelImageUtils.getBdvDimensions( projectModel.getSharedBdvData() ) ); + cancel( String.format( + "The dimensions of the ImageJ image (%s) do not match the dimensions of the big data viewer image (%s)." + + "\nThus no spots can be imported." + + "\nPlease make sure the dimensions match.", + imgPlusDimensions, bdvDimensions ) ); + } + } + + @Override + public void run() + { + if ( isCanceled() ) + return; + LabelImageUtils.importSpotsFromImgPlus( projectModel, imgPlus, scaleFactor, linkSpotsWithSameLabels ); + } + + @SuppressWarnings( "unused" ) + private void initMessage() + { + if ( imgPlus == null ) + return; + long[] bdvDimensions = LabelImageUtils.getBdvDimensions( projectModel.getSharedBdvData() ); + long[] imgPlusDimensions = LabelImageUtils.getImgPlusDimensions( imgPlus ); + boolean dimensionsMatch = Arrays.equals( bdvDimensions, imgPlusDimensions ); + String color = dimensionsMatch ? "green" : "red"; + String doNot = dimensionsMatch ? "" : " do not"; + String dimensionMatch = "The dimensions" + doNot + " match."; + documentation = String.format( TEMPLATE, imgPlus.getName(), imgPlusDimensions[ 0 ], imgPlusDimensions[ 1 ], imgPlusDimensions[ 2 ], + imgPlusDimensions[ 3 ], bdvDimensions[ 0 ], bdvDimensions[ 1 ], bdvDimensions[ 2 ], bdvDimensions[ 3 ], dimensionMatch ); + } +} diff --git a/src/main/java/org/mastodon/mamut/util/LineageTreeUtils.java b/src/main/java/org/mastodon/mamut/util/LineageTreeUtils.java index 2eadbd5d3..5e4a60277 100644 --- a/src/main/java/org/mastodon/mamut/util/LineageTreeUtils.java +++ b/src/main/java/org/mastodon/mamut/util/LineageTreeUtils.java @@ -48,9 +48,13 @@ import org.mastodon.mamut.model.branch.BranchLink; import org.mastodon.mamut.model.branch.BranchSpot; import org.mastodon.mamut.model.branch.ModelBranchGraph; +import org.mastodon.spatial.SpatialIndex; import org.mastodon.util.TreeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; +import java.lang.invoke.MethodHandles; import java.util.NoSuchElementException; import java.util.Set; import java.util.function.BooleanSupplier; @@ -59,6 +63,8 @@ public class LineageTreeUtils { + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + private LineageTreeUtils() { // prevent from instantiation @@ -283,4 +289,45 @@ public static < V extends Vertex< E >, E extends Edge< V > > RefSet< E > getAllE } return children; } + + /** + * This method adds edges to the graph of the model between spots that have the same label and are in consecutive time points. + *

+ * E.g. assume this set of spots: + *
+	 * Spot(label=0,X=1,Y=1,tp=0)        Spot(label=1,X=0,Y=1,tp=0 )       Spot(label=2,X=2,Y=1,tp=0 )
+	 *
+	 * Spot(label=0,X=1,Y=2,tp=1)        Spot(label=1,X=0,Y=0,tp=1 )       Spot(label=2,X=2,Y=0,tp=1 )
+	 *
+	 * Spot(label=0,X=1,Y=3,tp=2 )       Spot(label=1,X=0,Y=-1,tp=2 )      Spot(label=3,X=2,Y=-1,tp=2 )
+	 *
+	 * Spot(label=0,X=1,Y=4,tp=3 )       Spot(label=0,X=0,Y=-2,tp=3 )
+	 * 
+ * This method will add edges between the following spots: + *
+	 * Spot(label=0,X=1,Y=1,tp=0)        Spot(label=1,X=0,Y=1,tp=0 )       Spot(label=2,X=2,Y=1,tp=0 )
+	 *              │                                 │                                 │
+	 * Spot(label=0,X=1,Y=2,tp=1)        Spot(label=1,X=0,Y=0,tp=1 )       Spot(label=2,X=2,Y=0,tp=1 )
+	 *              │                                 │
+	 * Spot(label=0,X=1,Y=3,tp=2 )       Spot(label=1,X=0,Y=-1,tp=2 )      Spot(label=3,X=2,Y=-1,tp=2 )
+	 *       ┌──────┴────────────────────────────┐
+	 * Spot(label=0,X=1,Y=4,tp=3 )  Spot(label=0,X=0,Y=-2,tp=3 )
+	 * 
+ * @param model the model to link spots in. + */ + public static void linkSpotsWithSameLabel( final Model model ) + { + Link edgeRef = model.getGraph().edgeRef(); + for ( int timepoint = TreeUtils.getMinTimepoint( model ); timepoint < TreeUtils.getMaxTimepoint( model ); timepoint++ ) + { + SpatialIndex< Spot > currentTimepoint = model.getSpatioTemporalIndex().getSpatialIndex( timepoint ); + SpatialIndex< Spot > nextTimepoint = model.getSpatioTemporalIndex().getSpatialIndex( timepoint + 1 ); + currentTimepoint.forEach( spotA -> nextTimepoint.forEach( spotB -> { + if ( spotA.getLabel().equals( spotB.getLabel() ) ) + model.getGraph().addEdge( spotA, spotB, edgeRef ).init(); + } ) ); + } + model.getGraph().releaseRef( edgeRef ); + logger.debug( "Added {} edge(s) to the graph.", model.getGraph().edges().size() ); + } } diff --git a/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph6.java b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph6.java new file mode 100644 index 000000000..2540c39d0 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph6.java @@ -0,0 +1,59 @@ +package org.mastodon.mamut.feature.branch.exampleGraph; + +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; + +/** + * Represents a {@link AbstractExampleGraph} with the following {@link ModelGraph}: + * + *

Model-Graph (i.e. Graph of Spots)

+ *
+ * Spot( 0, X=1, Y=1, tp=0 )        Spot( 1, X=0, Y=1, tp=0 )       Spot( 2, X=2, Y=1, tp=0 )
+ *
+ * Spot( 0, X=1, Y=2, tp=1 )        Spot( 1, X=0, Y=0, tp=1 )       Spot( 2, X=2, Y=0, tp=1 )
+ *
+ * Spot( 0, X=1, Y=3, tp=2 )        Spot( 1, X=0, Y=-1, tp=2 )      Spot( 3, X=2, Y=-1, tp=2 )
+ *
+ * Spot( 0, X=1, Y=4, tp=3 )        Spot( 0, X=0, Y=-2, tp=3 )
+ * 
+ */ +public class ExampleGraph6 extends AbstractExampleGraph +{ + + public final Spot spot0; + + public final Spot spot1; + + public final Spot spot2; + + public final Spot spot3; + + public final Spot spot4; + + public final Spot spot5; + + public final Spot spot6; + + public final Spot spot7; + + public final Spot spot8; + + public final Spot spot9; + + public final Spot spot10; + + public ExampleGraph6() + { + spot0 = addNode( "0", 0, new double[] { 1d, 1d, 0d } ); + spot1 = addNode( "0", 1, new double[] { 1d, 2d, 0d } ); + spot2 = addNode( "0", 2, new double[] { 1d, 3d, 0d } ); + spot3 = addNode( "1", 0, new double[] { 0d, 1d, 0d } ); + spot4 = addNode( "1", 1, new double[] { 0d, 0d, 0d } ); + spot5 = addNode( "1", 2, new double[] { 0d, -1d, 0d } ); + spot6 = addNode( "2", 0, new double[] { 2d, 1d, 0d } ); + spot7 = addNode( "2", 1, new double[] { 2d, 0d, 0d } ); + spot8 = addNode( "3", 2, new double[] { 2d, -1d, 0d } ); + spot9 = addNode( "0", 3, new double[] { 1d, 4d, 0d } ); + spot10 = addNode( "0", 3, new double[] { 0d, -2d, 0d } ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/exporter/labelimage/ExportLabelImageControllerTest.java b/src/test/java/org/mastodon/mamut/io/exporter/labelimage/ExportLabelImageControllerTest.java index 93d5bbd1e..d9be57cc0 100644 --- a/src/test/java/org/mastodon/mamut/io/exporter/labelimage/ExportLabelImageControllerTest.java +++ b/src/test/java/org/mastodon/mamut/io/exporter/labelimage/ExportLabelImageControllerTest.java @@ -88,41 +88,43 @@ public void setUp() @Test public void testSaveLabelImageToFile() throws IOException { - AbstractSource< IntType > source = createRandomSource(); - Context context = new Context(); - TimePoint timePoint = new TimePoint( timepoint ); - List< TimePoint > timePoints = Collections.singletonList( timePoint ); - VoxelDimensions voxelDimensions = new DefaultVoxelDimensions( 3 ); - voxelDimensions.dimensions( new double[] { 1, 1, 1 } ); - ExportLabelImageController exportLabelImageController = - new ExportLabelImageController( model, timePoints, Cast.unchecked( source ), context, voxelDimensions ); - File outputSpot = getTempFile( "resultSpot" ); - File outputBranchSpot = getTempFile( "resultBranchSpot" ); - File outputTrack = getTempFile( "resultTrack" ); - exportLabelImageController.saveLabelImageToFile( LabelOptions.SPOT_ID, outputSpot, false, 1 ); - exportLabelImageController.saveLabelImageToFile( LabelOptions.BRANCH_SPOT_ID, outputBranchSpot, false, 1 ); - exportLabelImageController.saveLabelImageToFile( LabelOptions.TRACK_ID, outputTrack, false, 1 ); - - ImgOpener imgOpener = new ImgOpener( context ); - SCIFIOImgPlus< IntType > imgSpot = getIntTypeSCIFIOImgPlus( imgOpener, outputSpot ); - SCIFIOImgPlus< IntType > imgBranchSpot = getIntTypeSCIFIOImgPlus( imgOpener, outputBranchSpot ); - SCIFIOImgPlus< IntType > imgTrack = getIntTypeSCIFIOImgPlus( imgOpener, outputTrack ); - - // check that the spot id / branchSpot id / track id is used as value in the center of the spot - assertNotNull( imgSpot ); - assertEquals( 3, imgSpot.dimensionsAsLongArray().length ); - assertEquals( 100, imgSpot.dimension( 0 ) ); - assertEquals( 100, imgSpot.dimension( 1 ) ); - assertEquals( 100, imgSpot.dimension( 2 ) ); - assertEquals( spot.getInternalPoolIndex() + ExportLabelImageController.LABEL_ID_OFFSET, imgSpot.getAt( center ).get() ); - assertEquals( - branchSpot.getInternalPoolIndex() + ExportLabelImageController.LABEL_ID_OFFSET, imgBranchSpot.getAt( center ).get() ); - assertEquals( ExportLabelImageController.LABEL_ID_OFFSET, imgTrack.getAt( center ).get() ); - // check that there is no value set outside the ellipsoid of the spot - long[] corner = new long[] { 0, 0, 0 }; - assertEquals( 0, imgSpot.getAt( corner ).get() ); - assertEquals( 0, imgBranchSpot.getAt( corner ).get() ); - assertEquals( 0, imgTrack.getAt( corner ).get() ); + try (final Context context = new Context()) + { + AbstractSource< IntType > source = createRandomSource(); + TimePoint timePoint = new TimePoint( timepoint ); + List< TimePoint > timePoints = Collections.singletonList( timePoint ); + VoxelDimensions voxelDimensions = new DefaultVoxelDimensions( 3 ); + voxelDimensions.dimensions( new double[] { 1, 1, 1 } ); + ExportLabelImageController exportLabelImageController = + new ExportLabelImageController( model, timePoints, Cast.unchecked( source ), context, voxelDimensions ); + File outputSpot = getTempFile( "resultSpot" ); + File outputBranchSpot = getTempFile( "resultBranchSpot" ); + File outputTrack = getTempFile( "resultTrack" ); + exportLabelImageController.saveLabelImageToFile( LabelOptions.SPOT_ID, outputSpot, false, 1 ); + exportLabelImageController.saveLabelImageToFile( LabelOptions.BRANCH_SPOT_ID, outputBranchSpot, false, 1 ); + exportLabelImageController.saveLabelImageToFile( LabelOptions.TRACK_ID, outputTrack, false, 1 ); + + ImgOpener imgOpener = new ImgOpener( context ); + SCIFIOImgPlus< IntType > imgSpot = getIntTypeSCIFIOImgPlus( imgOpener, outputSpot ); + SCIFIOImgPlus< IntType > imgBranchSpot = getIntTypeSCIFIOImgPlus( imgOpener, outputBranchSpot ); + SCIFIOImgPlus< IntType > imgTrack = getIntTypeSCIFIOImgPlus( imgOpener, outputTrack ); + + // check that the spot id / branchSpot id / track id is used as value in the center of the spot + assertNotNull( imgSpot ); + assertEquals( 3, imgSpot.dimensionsAsLongArray().length ); + assertEquals( 100, imgSpot.dimension( 0 ) ); + assertEquals( 100, imgSpot.dimension( 1 ) ); + assertEquals( 100, imgSpot.dimension( 2 ) ); + assertEquals( spot.getInternalPoolIndex() + ExportLabelImageController.LABEL_ID_OFFSET, imgSpot.getAt( center ).get() ); + assertEquals( + branchSpot.getInternalPoolIndex() + ExportLabelImageController.LABEL_ID_OFFSET, imgBranchSpot.getAt( center ).get() ); + assertEquals( ExportLabelImageController.LABEL_ID_OFFSET, imgTrack.getAt( center ).get() ); + // check that there is no value set outside the ellipsoid of the spot + long[] corner = new long[] { 0, 0, 0 }; + assertEquals( 0, imgSpot.getAt( corner ).get() ); + assertEquals( 0, imgBranchSpot.getAt( corner ).get() ); + assertEquals( 0, imgTrack.getAt( corner ).get() ); + } } private static SCIFIOImgPlus< IntType > getIntTypeSCIFIOImgPlus( ImgOpener imgOpener, File outputSpot ) @@ -142,15 +144,18 @@ private static File getTempFile( final String prefix ) throws IOException @Test public void testExceptions() throws IOException { - ExportLabelImageController controller = - new ExportLabelImageController( model, Collections.emptyList(), null, new Context(), null ); - File file = File.createTempFile( "foo", "foo" ); - file.deleteOnExit(); - assertThrows( - IllegalArgumentException.class, - () -> controller.saveLabelImageToFile( LabelOptions.SPOT_ID, null, false, 1 ) - ); - assertThrows( IllegalArgumentException.class, () -> controller.saveLabelImageToFile( null, file, false, 1 ) ); + try (final Context context = new Context()) + { + ExportLabelImageController controller = + new ExportLabelImageController( model, Collections.emptyList(), null, context, null ); + File file = File.createTempFile( "foo", "foo" ); + file.deleteOnExit(); + assertThrows( + IllegalArgumentException.class, + () -> controller.saveLabelImageToFile( LabelOptions.SPOT_ID, null, false, 1 ) + ); + assertThrows( IllegalArgumentException.class, () -> controller.saveLabelImageToFile( null, file, false, 1 ) ); + } } private static AbstractSource< IntType > createRandomSource() diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/LabelImageUtilsTest.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/LabelImageUtilsTest.java new file mode 100644 index 000000000..372d7576a --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/LabelImageUtilsTest.java @@ -0,0 +1,305 @@ +package org.mastodon.mamut.io.importer.labelimage; + +import bdv.spimdata.SequenceDescriptionMinimal; +import bdv.util.AbstractSource; +import bdv.util.RandomAccessibleIntervalSource; +import mpicbg.spim.data.generic.sequence.AbstractSequenceDescription; +import mpicbg.spim.data.generic.sequence.BasicViewSetup; +import mpicbg.spim.data.sequence.FinalVoxelDimensions; +import mpicbg.spim.data.sequence.TimePoint; +import mpicbg.spim.data.sequence.TimePoints; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.CalibratedAxis; +import net.imagej.axis.DefaultLinearAxis; +import net.imagej.patcher.LegacyInjector; +import net.imglib2.FinalDimensions; +import net.imglib2.RandomAccess; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgFactory; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Cast; +import net.imglib2.view.Views; +import org.junit.Before; +import org.junit.Test; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.feature.EllipsoidIterable; +import org.mastodon.mamut.io.importer.labelimage.util.DemoUtils; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.Spot; +import org.mastodon.views.bdv.overlay.util.JamaEigenvalueDecomposition; +import org.scijava.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntFunction; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class LabelImageUtilsTest +{ + private static final Logger logger = LoggerFactory.getLogger( MethodHandles.lookup().lookupClass() ); + + private Model model; + + private AbstractSequenceDescription< ?, ?, ? > sequenceDescription; + + @Before + public void setUp() + { + model = new Model(); + TimePoints timePoints = new TimePoints( Collections.singletonList( new TimePoint( 0 ) ) ); + VoxelDimensions voxelDimensions = new FinalVoxelDimensions( "um", 1, 1, 1 ); + Map< Integer, ? extends BasicViewSetup > setups = + Collections.singletonMap( 0, new BasicViewSetup( 0, "setup 0", new FinalDimensions( 10, 10, 10 ), voxelDimensions ) ); + sequenceDescription = new SequenceDescriptionMinimal( timePoints, setups, null, null ); + } + + @Test + public void testScaleExceptions() + { + VoxelDimensions voxelDimensions = new FinalVoxelDimensions( "um", 1, 1, 1 ); + assertThrows( IllegalArgumentException.class, () -> LabelImageUtils.scale( new double[ 2 ][ 2 ], 1, voxelDimensions ) ); + assertThrows( IllegalArgumentException.class, () -> LabelImageUtils.scale( new double[ 3 ][ 2 ], 1, voxelDimensions ) ); + } + + @Test + public void testCreateSpotFromLabelImageEmpty() + { + RandomAccessibleIntervalSource< FloatType > img = + new RandomAccessibleIntervalSource<>( createImageCubeCorners( 0 ), new FloatType(), new AffineTransform3D(), + "Segmentation" ); + + IntFunction< RandomAccessibleInterval< RealType< ? > > > imgProvider = frameId -> Cast.unchecked( img.getSource( frameId, 0 ) ); + LabelImageUtils.createSpotsFromLabelImage( imgProvider, model, 1, false, sequenceDescription, null ); + assertEquals( 0, model.getGraph().vertices().size() ); + } + + @Test + public void testCreateSpotFromNonLabelImage() + { + AbstractSource< FloatType > img = createNonLabelImage(); + + IntFunction< RandomAccessibleInterval< RealType< ? > > > imgProvider = frameId -> Cast.unchecked( img.getSource( frameId, 0 ) ); + LabelImageUtils.createSpotsFromLabelImage( imgProvider, model, 1, false, sequenceDescription, null ); + assertEquals( 0, model.getGraph().vertices().size() ); + } + + @Test + public void testCreateSpotFromWrongVoxelDimensions() + { + + RandomAccessibleIntervalSource< FloatType > img = + new RandomAccessibleIntervalSource<>( createImageCubeCorners( 1 ), new FloatType(), new AffineTransform3D(), + "Segmentation" ); + + VoxelDimensions wrongDimensions = new FinalVoxelDimensions( "um", 1, 1 ); + TimePoints timePoints = new TimePoints( Collections.singletonList( new TimePoint( 0 ) ) ); + Map< Integer, ? extends BasicViewSetup > setups = + Collections.singletonMap( 0, new BasicViewSetup( 0, "setup 0", new FinalDimensions( 10, 10, 10 ), wrongDimensions ) ); + AbstractSequenceDescription< ?, ?, ? > faultySequenceDescription = new SequenceDescriptionMinimal( timePoints, setups, null, null ); + IntFunction< RandomAccessibleInterval< RealType< ? > > > imgProvider = frameId -> Cast.unchecked( img.getSource( frameId, 0 ) ); + assertThrows( IllegalArgumentException.class, + () -> LabelImageUtils.createSpotsFromLabelImage( imgProvider, model, 1, false, faultySequenceDescription, null ) ); + + } + + @Test + public void testImportSpotsFromBdvChannel() + { + LegacyInjector.preinit(); + try (Context context = new Context()) + { + int pixelValue = 1; + Img< FloatType > img = createImageCubeCorners( pixelValue ); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( img, model, context ); + LabelImageUtils.importSpotsFromBdvChannel( projectModel, projectModel.getSharedBdvData().getSources().get( 0 ).getSpimSource(), + 1, false ); + + Iterator< Spot > iter = model.getGraph().vertices().iterator(); + Spot spot = iter.next(); + double[][] covarianceMatrix = new double[ 3 ][ 3 ]; + spot.getCovariance( covarianceMatrix ); + final JamaEigenvalueDecomposition eigenvalueDecomposition = new JamaEigenvalueDecomposition( 3 ); + eigenvalueDecomposition.decomposeSymmetric( covarianceMatrix ); + final double[] eigenValues = eigenvalueDecomposition.getRealEigenvalues(); + double axisA = Math.sqrt( eigenValues[ 0 ] ); + double axisB = Math.sqrt( eigenValues[ 1 ] ); + double axisC = Math.sqrt( eigenValues[ 2 ] ); + + assertNotNull( spot ); + assertEquals( 0, spot.getTimepoint() ); + assertEquals( 2, spot.getDoublePosition( 0 ), 0.01 ); + assertEquals( 2, spot.getDoublePosition( 1 ), 0.01 ); + assertEquals( 2, spot.getDoublePosition( 2 ), 0.01 ); + assertEquals( 0, spot.getInternalPoolIndex() ); + assertEquals( String.valueOf( pixelValue ), spot.getLabel() ); + assertEquals( 2.2, axisA, 0.2d ); + assertEquals( 2.2, axisB, 0.2d ); + assertEquals( 2.2, axisC, 0.2d ); + assertEquals( 5d, spot.getBoundingSphereRadiusSquared(), 1d ); + assertFalse( iter.hasNext() ); + } + } + + @Test + public void testImportSpotsFromImgPlus() + { + LegacyInjector.preinit(); + try (Context context = new Context()) + { + double[] center = { 18, 21, 22 }; + double[][] givenCovariance = { + { 33, 14, 0 }, + { 14, 32, 0 }, + { 0, 0, 95 } + }; + Spot spot = model.getGraph().addVertex().init( 0, center, givenCovariance ); + int pixelValue = 1; + Img< FloatType > image = createImageFromSpot( spot, pixelValue ); + ImgPlus< FloatType > imgPlus = createImgPlus( image, new FinalVoxelDimensions( "um", 1, 1, 1 ) ); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( image, model, context ); + LabelImageUtils.importSpotsFromImgPlus( projectModel, imgPlus, 1, false ); + + Iterator< Spot > iterator = model.getGraph().vertices().iterator(); + iterator.next(); + Spot createdSpot = iterator.next(); + double[] mean = createdSpot.positionAsDoubleArray(); + double[][] computedCovariance = new double[ 3 ][ 3 ]; + createdSpot.getCovariance( computedCovariance ); + + logger.debug( "Given center: {}", Arrays.toString( center ) ); + logger.debug( "Computed mean: {}", Arrays.toString( mean ) ); + logger.debug( "Given covariance: {}", Arrays.deepToString( givenCovariance ) ); + logger.debug( "Computed covariance: {}", Arrays.deepToString( computedCovariance ) ); + + assertArrayEquals( center, mean, 0.01d ); + assertArrayEquals( givenCovariance[ 0 ], computedCovariance[ 0 ], 10d ); + assertArrayEquals( givenCovariance[ 1 ], computedCovariance[ 1 ], 10d ); + assertArrayEquals( givenCovariance[ 2 ], computedCovariance[ 2 ], 10d ); + assertEquals( String.valueOf( pixelValue ), createdSpot.getLabel() ); + } + } + + @Test + public void testImportSpotsFromImgPlusAndLinkSameLabels() + { + LegacyInjector.preinit(); + try (Context context = new Context()) + { + Img< FloatType > twoFramesImage = DemoUtils.generateExampleImage(); + ImgPlus< FloatType > imgPlus = createImgPlus( twoFramesImage, new FinalVoxelDimensions( "um", 1, 1, 1 ) ); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( twoFramesImage, model, context ); + LabelImageUtils.importSpotsFromImgPlus( projectModel, imgPlus, 1, true ); + + assertEquals( 2, model.getGraph().vertices().size() ); + assertEquals( 1, model.getGraph().edges().size() ); + assertEquals( 1, model.getSpatioTemporalIndex().getSpatialIndex( 0 ).size() ); + assertEquals( 1, model.getSpatioTemporalIndex().getSpatialIndex( 1 ).size() ); + } + } + + @Test + public void testDimensionsMatch() + { + LegacyInjector.preinit(); + try (final Context context = new Context()) + { + Img< FloatType > image = ArrayImgs.floats( 10, 10, 10, 2 ); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( image, model, context ); + ImgPlus< FloatType > imgPlus = createImgPlus( image, new FinalVoxelDimensions( "um", 1, 1, 1 ) ); + assertTrue( LabelImageUtils.dimensionsMatch( projectModel.getSharedBdvData(), imgPlus ) ); + } + } + + @Test + public void testGetImgPlusDimensions() + { + Img< FloatType > image = ArrayImgs.floats( 100, 100, 100, 2 ); + ImgPlus< FloatType > imgPlus = createImgPlus( image, new FinalVoxelDimensions( "um", 1, 1, 1 ) ); + long[] dimensions = LabelImageUtils.getImgPlusDimensions( imgPlus ); + assertArrayEquals( new long[] { 100, 100, 100, 2 }, dimensions ); + } + + @Test + public void testGetSourceNames() + { + LegacyInjector.preinit(); + try (final Context context = new Context()) + { + Img< FloatType > image = ArrayImgs.floats( 100, 100, 100, 2 ); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( image, model, context ); + List< String > sourceNames = LabelImageUtils.getSourceNames( projectModel.getSharedBdvData() ); + assertEquals( 1, sourceNames.size() ); + assertEquals( "image channel 1", sourceNames.get( 0 ) ); + } + } + + private ImgPlus< FloatType > createImgPlus( final Img< FloatType > img, final VoxelDimensions voxelDimensions ) + { + final CalibratedAxis[] axes = { new DefaultLinearAxis( Axes.X, voxelDimensions.dimension( 0 ) ), + new DefaultLinearAxis( Axes.Y, voxelDimensions.dimension( 1 ) ), + new DefaultLinearAxis( Axes.Z, voxelDimensions.dimension( 2 ) ), new DefaultLinearAxis( Axes.TIME ) }; + return new ImgPlus<>( img, "Result", axes ); + } + + private static Img< FloatType > createImageCubeCorners( int pixelValue ) + { + Img< FloatType > img = new ArrayImgFactory<>( new FloatType() ).create( 4, 4, 4 ); + RandomAccess< FloatType > ra = img.randomAccess(); + // 8 corners of a cube + ra.setPositionAndGet( 1, 1, 1 ).set( pixelValue ); + ra.setPositionAndGet( 1, 3, 1 ).set( pixelValue ); + ra.setPositionAndGet( 3, 1, 1 ).set( pixelValue ); + ra.setPositionAndGet( 3, 3, 1 ).set( pixelValue ); + ra.setPositionAndGet( 1, 1, 3 ).set( pixelValue ); + ra.setPositionAndGet( 1, 3, 3 ).set( pixelValue ); + ra.setPositionAndGet( 3, 1, 3 ).set( pixelValue ); + ra.setPositionAndGet( 3, 3, 3 ).set( pixelValue ); + + return img; + } + + private static AbstractSource< FloatType > createNonLabelImage() + { + Img< FloatType > img = new ArrayImgFactory<>( new FloatType() ).create( 25, 25, 25 ); + AtomicInteger value = new AtomicInteger( 0 ); + LoopBuilder.setImages( img ).forEachPixel( floatType -> floatType.set( value.incrementAndGet() ) ); + + return new RandomAccessibleIntervalSource<>( img, new FloatType(), new AffineTransform3D(), + "Segmentation" ); + } + + private static Img< FloatType > createImageFromSpot( final Spot spot, int pixelValue ) + { + long[] dimensions = { 40, 40, 40, 1 }; + Img< FloatType > image = ArrayImgs.floats( dimensions ); + AffineTransform3D transform = new AffineTransform3D(); + AbstractSource< FloatType > frame = + new RandomAccessibleIntervalSource<>( Views.hyperSlice( image, 3, 0 ), new FloatType(), transform, "Ellipsoids" ); + + final EllipsoidIterable< FloatType > ellipsoidIterable = new EllipsoidIterable<>( frame ); + ellipsoidIterable.reset( spot ); + ellipsoidIterable.forEach( pixel -> pixel.set( pixelValue ) ); + return image; + } +} + diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceMatrixTest.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceMatrixTest.java new file mode 100644 index 000000000..a5dc570e3 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceMatrixTest.java @@ -0,0 +1,40 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; + +public class CovarianceMatrixTest +{ + @Test + public void testGet() + { + int[][] dataInt = { { 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 } }; + CovarianceMatrix matrix = new CovarianceMatrix( 2 ); + for ( int[] values : dataInt ) + matrix.addValues( values ); + double[][] actual = matrix.get(); + + assertArrayEquals( new double[] { 3d, 4d }, matrix.getMeans(), 0.0001d ); + assertArrayEquals( new double[] { 2.5d, 2.5d }, actual[ 0 ], 0.0001d ); + assertArrayEquals( new double[] { 2.5d, 2.5d }, actual[ 1 ], 0.0001d ); + } + + @Test + public void testException1() + { + CovarianceMatrix covarianceMatrix = new CovarianceMatrix( 2 ); + covarianceMatrix.addValues( new int[] { 1, 1 } ); + assertThrows( IllegalArgumentException.class, covarianceMatrix::get ); + } + + @Test + public void testException2() + { + CovarianceMatrix covarianceMatrix = new CovarianceMatrix( 2 ); + assertThrows( IllegalArgumentException.class, () -> covarianceMatrix.addValues( new int[] { 1 } ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceTest.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceTest.java new file mode 100644 index 000000000..dd6e98d37 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/CovarianceTest.java @@ -0,0 +1,33 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class CovarianceTest +{ + @Test + public void testGet() + { + double[] x = { 1, 2, 3, 4, 5 }; + double[] y = { 2, 3, 4, 5, 6 }; + org.mastodon.mamut.io.importer.labelimage.math.Covariance covariance = + new org.mastodon.mamut.io.importer.labelimage.math.Covariance(); + for ( int i = 0; i < x.length; i++ ) + covariance.addValues( x[ i ], y[ i ] ); + double actual = covariance.get(); + assertEquals( 2.5d, actual, 0.0001d ); + assertEquals( 3d, covariance.getMeanX(), 0.0001d ); + assertEquals( 4d, covariance.getMeanY(), 0.0001d ); + } + + @Test + public void testException() + { + org.mastodon.mamut.io.importer.labelimage.math.Covariance covariance = + new org.mastodon.mamut.io.importer.labelimage.math.Covariance(); + covariance.addValues( 1, 1 ); + assertThrows( IllegalArgumentException.class, covariance::get ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/MeansVectorTest.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/MeansVectorTest.java new file mode 100644 index 000000000..f74e9b186 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/math/MeansVectorTest.java @@ -0,0 +1,15 @@ +package org.mastodon.mamut.io.importer.labelimage.math; + +import org.junit.Test; + +import static org.junit.Assert.assertThrows; + +public class MeansVectorTest +{ + @Test + public void testAddValuesException() + { + MeansVector meansVector = new MeansVector( 3 ); + assertThrows( IllegalArgumentException.class, () -> meansVector.addValues( new int[] { 1, 2 } ) ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromBdvChannelViewDemo.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromBdvChannelViewDemo.java new file mode 100644 index 000000000..d436abb71 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromBdvChannelViewDemo.java @@ -0,0 +1,36 @@ +package org.mastodon.mamut.io.importer.labelimage.ui; + +import net.imglib2.img.Img; +import net.imglib2.type.numeric.real.FloatType; +import org.mastodon.mamut.MainWindow; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.util.DemoUtils; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.views.bdv.MamutViewBdv; +import org.scijava.Context; +import org.scijava.command.CommandService; +import org.scijava.ui.UIService; + +public class ImportSpotsFromBdvChannelViewDemo +{ + + public static void main( String[] args ) + { + @SuppressWarnings( "all" ) + Context context = new Context(); + UIService ui = context.service( UIService.class ); + CommandService cmd = context.service( CommandService.class ); + + Img< FloatType > image = DemoUtils.generateExampleImage(); + + // show ImageJ + ui.showUI(); + // open the image in Mastodon + Model model = new Model(); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( image, model, context ); + new MainWindow( projectModel ).setVisible( true ); + projectModel.getWindowManager().createView( MamutViewBdv.class ); + // run import spots command + cmd.run( ImportSpotsFromBdvChannelView.class, true, "projectModel", projectModel ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromImgPlusViewDemo.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromImgPlusViewDemo.java new file mode 100644 index 000000000..9d46816c9 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/ui/ImportSpotsFromImgPlusViewDemo.java @@ -0,0 +1,44 @@ +package org.mastodon.mamut.io.importer.labelimage.ui; + +import ij.ImagePlus; +import net.imglib2.img.Img; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.type.numeric.real.FloatType; + +import org.mastodon.mamut.MainWindow; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.util.DemoUtils; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.views.bdv.MamutViewBdv; +import org.scijava.Context; +import org.scijava.command.CommandService; +import org.scijava.ui.UIService; + +public class ImportSpotsFromImgPlusViewDemo +{ + + public static void main( String[] args ) + { + @SuppressWarnings( "all" ) + Context context = new Context(); + UIService ui = context.service( UIService.class ); + CommandService cmd = context.service( CommandService.class ); + + Img< FloatType > image = DemoUtils.generateExampleImage(); + + // show ImageJ + ui.showUI(); + // show image in ImageJ + ImagePlus imagePlus = ImageJFunctions.wrap( image, "label image" ); + imagePlus.setDimensions( 1, 100, 2 ); + imagePlus.setZ( 50 ); + imagePlus.show(); + // open the image in Mastodon + Model model = new Model(); + ProjectModel projectModel = DemoUtils.wrapAsAppModel( image, model, context ); + new MainWindow( projectModel ).setVisible( true ); + projectModel.getWindowManager().createView( MamutViewBdv.class ); + // run import spots command + cmd.run( ImportSpotsFromImgPlusView.class, true, "projectModel", projectModel ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/ComputeMeanAndVarianceDemo.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/ComputeMeanAndVarianceDemo.java new file mode 100644 index 000000000..7761908e2 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/ComputeMeanAndVarianceDemo.java @@ -0,0 +1,156 @@ +/*- + * #%L + * mastodon-ellipsoid-fitting + * %% + * Copyright (C) 2015 - 2023 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * 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 org.mastodon.mamut.io.importer.labelimage.util; + +import mpicbg.spim.data.sequence.DefaultVoxelDimensions; +import mpicbg.spim.data.sequence.VoxelDimensions; +import net.imagej.patcher.LegacyInjector; +import net.imglib2.Cursor; +import net.imglib2.img.Img; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Cast; +import net.imglib2.util.LinAlgHelpers; +import net.imglib2.util.Pair; +import net.imglib2.util.StopWatch; +import org.mastodon.mamut.io.importer.labelimage.LabelImageUtils; +import org.mastodon.mamut.model.Model; +import org.scijava.Context; + +import java.util.Arrays; + +/** + * Computing the mean position and covariance matrix for a given segmented + * region of an image is an easy way to get good ellipsoid parameters for + * that segment. + *

+ * Here is an example of how to do that. + */ +public class ComputeMeanAndVarianceDemo +{ + + public static void main( String[] args ) + { + LegacyInjector.preinit(); + try (final Context context = new Context()) + { + double[] center = { 40, 50, 60 }; + double[][] givenCovariance = { + { 400, 20, -10 }, + { 20, 200, 30 }, + { -10, 30, 100 } + }; + + long[] dimensions = { 100, 100, 100 }; + int background = 0; + int pixelValue = 1; + Img< FloatType > image = DemoUtils.generateExampleImage( center, givenCovariance, dimensions, background, pixelValue ); + + StopWatch stopWatchOnline = StopWatch.createAndStart(); + Pair< double[], double[][] > results = + DemoUtils.computeMeanCovarianceOnline( Cast.unchecked( image ), pixelValue, 2.1 ); + double[] onlineMean = results.getA(); + double[][] onlineCovariance = results.getB(); + stopWatchOnline.stop(); + + StopWatch stopWatchTwoPass = StopWatch.createAndStart(); + double[] mean = computeMean( image, pixelValue ); + double[][] computedCovariance = computeCovariance( image, mean, pixelValue ); + stopWatchTwoPass.stop(); + + System.out.println( "Given center: " + Arrays.toString( center ) ); + System.out.println( "Computed mean: " + Arrays.toString( mean ) ); + System.out.println( "Computed mean online: " + Arrays.toString( onlineMean ) ); + System.out.println( "Given covariance: " + Arrays.deepToString( givenCovariance ) ); + System.out.println( "Computed covariance: " + Arrays.deepToString( computedCovariance ) ); + System.out.println( "Computed covariance online: " + Arrays.deepToString( onlineCovariance ) ); + System.out.println( "Time to compute mean and covariance: \n" + stopWatchTwoPass.nanoTime() / 1e9 + " nano seconds" ); + System.out.println( "Time to compute mean and covariance online: \n" + stopWatchOnline.nanoTime() / 1e9 + " nano seconds" ); + + Model model = new Model(); + model.getGraph().addVertex().init( 0, onlineMean, onlineCovariance ); + DemoUtils.showBdvWindow( DemoUtils.wrapAsAppModel( image, model, context ) ); + } + } + + /** + * Computes the mean position of the pixels whose value equals the given {@code pixelValue}. + * + * @param image the image + * @param pixelValue the pixel value + * @return the mean position + */ + private static double[] computeMean( final Img< FloatType > image, final int pixelValue ) + { + Cursor< FloatType > cursor = image.cursor(); + double[] sum = new double[ 3 ]; + double[] position = new double[ 3 ]; + long counter = 0; + while ( cursor.hasNext() ) + if ( cursor.next().get() == pixelValue ) + { + cursor.localize( position ); + LinAlgHelpers.add( sum, position, sum ); + counter++; + } + LinAlgHelpers.scale( sum, 1. / counter, sum ); + return sum; + } + + /** + * Computes the covariance matrix of the pixels whose value equals the given {@code pixelValue}. + * Uses a two-pass algorithm to compute the covariance matrix, cf. Two-pass algorithm for covariance + * + * @param image the image + * @param mean the mean position + * @param pixelValue the pixel value + */ + private static double[][] computeCovariance( final Img< FloatType > image, final double[] mean, final int pixelValue ) + { + Cursor< FloatType > cursor = image.cursor(); + long counter = 0; + double[] position = new double[ 3 ]; + double[][] covariance = new double[ 3 ][ 3 ]; + cursor.reset(); + while ( cursor.hasNext() ) + if ( cursor.next().get() == pixelValue ) + { + cursor.localize( position ); + LinAlgHelpers.subtract( position, mean, position ); + for ( int i = 0; i < 3; i++ ) + for ( int j = 0; j < 3; j++ ) + covariance[ i ][ j ] += position[ i ] * position[ j ]; + counter++; + } + VoxelDimensions voxelDimensions = new DefaultVoxelDimensions( 3 ); + LabelImageUtils.scale( covariance, Math.sqrt( 1d / counter ), voxelDimensions ); + // I don't know why the factor 5 is needed. But it works. + LabelImageUtils.scale( covariance, Math.sqrt( 5d ), voxelDimensions ); + return covariance; + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/DemoUtils.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/DemoUtils.java new file mode 100644 index 000000000..bc92f0885 --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/DemoUtils.java @@ -0,0 +1,156 @@ +/*- + * #%L + * mastodon-ellipsoid-fitting + * %% + * Copyright (C) 2015 - 2023 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * 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 org.mastodon.mamut.io.importer.labelimage.util; + +import ij.ImagePlus; +import mpicbg.spim.data.sequence.DefaultVoxelDimensions; +import net.imagej.ImgPlus; +import net.imagej.axis.Axes; +import net.imagej.axis.AxisType; +import net.imglib2.Cursor; +import net.imglib2.img.Img; +import net.imglib2.img.ImgView; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.img.display.imagej.ImgToVirtualStack; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.type.numeric.RealType; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Pair; +import net.imglib2.util.ValuePair; +import net.imglib2.view.Views; +import org.mastodon.mamut.ProjectModel; +import org.mastodon.mamut.io.importer.labelimage.LabelImageUtils; +import org.mastodon.mamut.io.importer.labelimage.math.CovarianceMatrix; +import org.mastodon.mamut.io.importer.labelimage.math.MeansVector; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.views.bdv.MamutViewBdv; +import org.mastodon.views.bdv.SharedBigDataViewerData; +import org.scijava.Context; + +import javax.annotation.Nonnull; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class DemoUtils +{ + private DemoUtils() + { + // prevent from instantiation + } + + public static ProjectModel wrapAsAppModel( final Img< FloatType > image, final Model model, final Context context ) + { + final SharedBigDataViewerData sharedBigDataViewerData = asSharedBdvDataXyz( image ); + return ProjectModel.create( context, model, sharedBigDataViewerData, null ); + } + + public static SharedBigDataViewerData asSharedBdvDataXyz( final Img< FloatType > image1 ) + { + final ImagePlus image = + ImgToVirtualStack.wrap( new ImgPlus<>( image1, "image", new AxisType[] { Axes.X, Axes.Y, Axes.Z, Axes.TIME } ) ); + return Objects.requireNonNull( SharedBigDataViewerData.fromImagePlus( image ) ); + } + + public static void showBdvWindow( @Nonnull final ProjectModel appModel ) + { + appModel.getWindowManager().createView( MamutViewBdv.class ); + } + + /** + * Returns an example image with a single ellipsoid. + * + * @param center center of the ellipsoid + * @param cov covariance matrix of the ellipsoid + * @param dimensions dimensions of the image + * @param background value of the background + * @param pixelValue value of the ellipsoid + */ + public static Img< FloatType > generateExampleImage( + final double[] center, final double[][] cov, final long[] dimensions, final int background, final int pixelValue + ) + { + Img< FloatType > image = ArrayImgs.floats( dimensions ); + MultiVariateNormalDistributionRenderer.renderMultivariateNormalDistribution( center, cov, image ); + LoopBuilder.setImages( image ).forEachPixel( pixel -> { + if ( pixel.get() > 500 ) + pixel.set( pixelValue ); + else + pixel.set( background ); + } ); + return image; + } + + /** + * Returns an example image with a single ellipsoid and black background. + */ + public static Img< FloatType > generateExampleImage() + { + double[] center = { 40, 80, 50 }; + double[][] givenCovariance = { + { 400, 20, -10 }, + { 20, 200, 30 }, + { -10, 30, 100 } + }; + long[] dimensions = { 100, 100, 100 }; + int background = 0; + int pixelValue = 1; + Img< FloatType > frame = generateExampleImage( center, givenCovariance, dimensions, background, pixelValue ); + List< Img< FloatType > > twoIdenticalFrames = Arrays.asList( frame, frame ); + return ImgView.wrap( Views.stack( twoIdenticalFrames ) ); + } + + /** + * Computes the covariance matrix of the pixels whose value equals the given {@code pixelValue}. + * Uses an online algorithm to compute the covariance matrix, cf. Online algorithm for covariance + * + * @param image the image + * @param pixelValue the pixel value + */ + public static Pair< double[], double[][] > computeMeanCovarianceOnline( final Img< RealType< ? > > image, final int pixelValue, + double sigma ) + { + Cursor< RealType< ? > > cursor = image.cursor(); + int[] position = new int[ 3 ]; + cursor.reset(); + MeansVector mean = new MeansVector( 3 ); + CovarianceMatrix cov = new CovarianceMatrix( 3 ); + while ( cursor.hasNext() ) + if ( cursor.next().getRealDouble() == pixelValue ) + { + cursor.localize( position ); + mean.addValues( position ); + cov.addValues( position ); + } + double[] means = mean.get(); + double[][] covariances = cov.get(); + LabelImageUtils.scale( covariances, sigma, new DefaultVoxelDimensions( 3 ) ); + return new ValuePair<>( means, covariances ); + } +} diff --git a/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/MultiVariateNormalDistributionRenderer.java b/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/MultiVariateNormalDistributionRenderer.java new file mode 100644 index 000000000..045eccbbf --- /dev/null +++ b/src/test/java/org/mastodon/mamut/io/importer/labelimage/util/MultiVariateNormalDistributionRenderer.java @@ -0,0 +1,77 @@ +/*- + * #%L + * mastodon-ellipsoid-fitting + * %% + * Copyright (C) 2015 - 2023 Tobias Pietzsch, Jean-Yves Tinevez + * %% + * 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 org.mastodon.mamut.io.importer.labelimage.util; + +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.loops.LoopBuilder; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.real.FloatType; +import net.imglib2.util.Intervals; +import net.imglib2.util.LinAlgHelpers; + +public class MultiVariateNormalDistributionRenderer +{ + + /** + * Renders the density function of a multivariate normal distribution into a given image. + * @see Wikipedia Multivariate normal distribution + * @param center center of the distribution + * @param cov covariance matrix of the distribution (must be symmetric and positive definite) + * @param image the image to render into (image is a cube) + * + */ + public static void renderMultivariateNormalDistribution( double[] center, double[][] cov, + RandomAccessibleInterval< FloatType > image ) + { + AffineTransform3D sigma = new AffineTransform3D(); + sigma.set( + cov[ 0 ][ 0 ], cov[ 0 ][ 1 ], cov[ 0 ][ 2 ], 0, + cov[ 1 ][ 0 ], cov[ 1 ][ 1 ], cov[ 1 ][ 2 ], 0, + cov[ 2 ][ 0 ], cov[ 2 ][ 1 ], cov[ 2 ][ 2 ], 0 + ); + double[] coord = new double[ 3 ]; + double[] out = new double[ 3 ]; + LoopBuilder.setImages( Intervals.positions( image ), image ).forEachPixel( ( position, pixel ) -> { + position.localize( coord ); + LinAlgHelpers.subtract( coord, center, coord ); + sigma.applyInverse( out, coord ); + // leave out the 1 / (sqrt( ( 2 * pi ) ^ 3 * det( cov )) factor to make the image more visible + double value = Math.exp( -0.5 * scalarProduct( coord, out ) ); + pixel.setReal( 1000 * value ); + } ); + } + + /** + * Computes the scalar product of two vectors. + */ + public static double scalarProduct( double[] a, double[] b ) + { + return a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] + a[ 2 ] * b[ 2 ]; + } +} diff --git a/src/test/java/org/mastodon/mamut/util/LineageTreeUtilsTest.java b/src/test/java/org/mastodon/mamut/util/LineageTreeUtilsTest.java index c2394a95e..a373395c4 100644 --- a/src/test/java/org/mastodon/mamut/util/LineageTreeUtilsTest.java +++ b/src/test/java/org/mastodon/mamut/util/LineageTreeUtilsTest.java @@ -34,6 +34,7 @@ import org.mastodon.collection.RefSet; import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph4; +import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph6; import org.mastodon.mamut.model.Link; import org.mastodon.mamut.model.Model; import org.mastodon.mamut.model.Spot; @@ -57,11 +58,14 @@ public class LineageTreeUtilsTest private ExampleGraph4 graph4; + private ExampleGraph6 graph6; + @Before public void setUp() { graph2 = new ExampleGraph2(); graph4 = new ExampleGraph4(); + graph6 = new ExampleGraph6(); } @Test @@ -134,4 +138,26 @@ public void testGetAllEdgeSuccessors() actual = LineageTreeUtils.getAllEdgeSuccessors( graph4.branchSpotD, graph4.getModel().getBranchGraph() ); assertEquals( expected, actual ); } + + @Test + public void testLinkSpotsWithSameLabel() + { + assertEquals( 0, graph6.getModel().getGraph().edges().size() ); + LineageTreeUtils.linkSpotsWithSameLabel( graph6.getModel() ); + assertEquals( 7, graph6.getModel().getGraph().edges().size() ); + assertSpotEquals( graph6.spot0.outgoingEdges().get( 0 ).getTarget(), graph6.spot1 ); + assertSpotEquals( graph6.spot1.outgoingEdges().get( 0 ).getTarget(), graph6.spot2 ); + assertSpotEquals( graph6.spot3.outgoingEdges().get( 0 ).getTarget(), graph6.spot4 ); + assertSpotEquals( graph6.spot4.outgoingEdges().get( 0 ).getTarget(), graph6.spot5 ); + assertSpotEquals( graph6.spot6.outgoingEdges().get( 0 ).getTarget(), graph6.spot7 ); + assertEquals( 2, graph6.spot2.outgoingEdges().size() ); + assertSpotEquals( graph6.spot2.outgoingEdges().get( 0 ).getTarget(), graph6.spot10 ); + assertSpotEquals( graph6.spot2.outgoingEdges().get( 1 ).getTarget(), graph6.spot9 ); + assertEquals( 0, graph6.spot7.outgoingEdges().size() ); + } + + private void assertSpotEquals( final Spot expected, final Spot actual ) + { + assertEquals( expected, actual ); + } }