diff --git a/README.md b/README.md index 10208de4..006b8f8f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,37 @@ [![Build Status](https://github.com/trackmate-sc/TrackMate-Cellpose/actions/workflows/build.yml/badge.svg)](https://github.com/trackmate-sc/TrackMate-Cellpose/actions/workflows/build.yml) -# TrackMate-Cellpose +# Omnipose integration in TrackMate. -Tentative Cellpose integration in TrackMate. +The Omnipose integration in TrackMate works roughly as the Cellpose integration one. +It requires Omnipose to be installed on your system and working independently. This page gives installation details and advices at how to use the omnipose integration in TrackMate. + +## Omnipose +Omnipose is a segmentation algorithm based on Deep-Learning techniques, and inspired from the Cellpose architecture. Omnipose is well suited for bacterial cell segmentation, and achieves remarkable performances on mixed bacterial cultures, antibiotic-treated cells and cells of elongated or branched morphology. + +If you use the Omnipose TrackMate module for your research, please also cite the Omnipose paper: +[Cutler, K.J., Stringer, C., Lo, T.W. et al. Omnipose: a high-precision morphology-independent solution for bacterial cell segmentation. Nat Methods 19, 1438–1448 (2022)](https://www.nature.com/articles/s41592-022-01639-4). + + + +## Example +https://github.com/marieanselmet/TrackMate-Omnipose/assets/32811540/3c2365c9-8d1b-4057-b4d1-2939e4e2b818 + +*E. Coli, Marie Anselmet and Rodrigo Arias Cartin, Barras lab, Institut Pasteur* + + +## Omnipose installation + +This code works with the Omnipose version 0.3.6. It doesn't work with the last version of Omnipose. + +To install Omnipose, you can refer directly to the installation guide provided on the [Omnipose repository](https://github.com/kevinjohncutler/omnipose#how-to-install-omnipose). + +An example Windows installation working on GPU: +``` +conda create -n omnipose +conda activate omnipose +conda install pytorch==2.0.0 torchvision==0.15.0 torchaudio==2.0.0 pytorch-cuda=11.8 -c pytorch -c nvidia +pip install omnipose==0.3.6 +pip install cellpose-omni==0.7.3 +``` + +The default models *bact_phase_omni* and *bact_fluor_omni* are stored in the cellpose pretrained models folder. diff --git a/pom.xml b/pom.xml index 80217659..933a1cb7 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ 0.1.3-SNAPSHOT TrackMate-Cellpose - TrackMate detector based on Cellpose. + TrackMate detector based on cellpose and omnipose. https://github.com/trackmate-sc/TrackMate-Cellpose 2021 @@ -51,6 +51,11 @@ https://imagej.net/people/ctrueden ctrueden + + Marie Anselmet + https://research.pasteur.fr/fr/member/marie-anselmet/ + manselmet + @@ -59,7 +64,7 @@ https://forum.image.sc/tag/trackmate - + scm:git:https://github.com/trackmate-sc/TrackMate-Cellpose scm:git:git@github.com:trackmate-sc/TrackMate-Cellpose @@ -84,7 +89,7 @@ sign,deploy-to-scijava - 7.10.2 + 7.11.1 diff --git a/src/main/java/fiji/plugin/trackmate/cellpose/AbstractCellposeSettings.java b/src/main/java/fiji/plugin/trackmate/cellpose/AbstractCellposeSettings.java new file mode 100644 index 00000000..a17885bf --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/cellpose/AbstractCellposeSettings.java @@ -0,0 +1,144 @@ +package fiji.plugin.trackmate.cellpose; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class AbstractCellposeSettings +{ + + /** + * Interface for enums that represent a pretrained or a custom model in + * cellpose or omnipose. + */ + public interface PretrainedModel + { + + /** + * Returns true if this model is a custom model. + * + * @return true if this model is a custom model. + */ + boolean isCustom(); + + /** + * Returns the name of this pretrained model, or its path if it is a + * custom model. + * + * @return the model name or path. + */ + String getPath(); + + } + + public final String executablePath; + + public final int chan; + + public final int chan2; + + public final String customModelPath; + + public final double diameter; + + public final boolean useGPU; + + public final boolean simplifyContours; + + private final PretrainedModel model; + + protected AbstractCellposeSettings( + final String executablePath, + final PretrainedModel model, + final String customModelPath, + final int chan, + final int chan2, + final double diameter, + final boolean useGPU, + final boolean simplifyContours ) + { + this.executablePath = executablePath; + this.model = model; + this.chan = chan; + this.chan2 = chan2; + this.customModelPath = customModelPath; + this.diameter = diameter; + this.useGPU = useGPU; + this.simplifyContours = simplifyContours; + } + + /** + * Returns the executable name of the cellpose or omnipose command. For + * cellpose, it's simply 'cellpose'. + * + * @return the executable name. + */ + public abstract String getExecutableName(); + + public List< String > toCmdLine( final String imagesDir ) + { + final List< String > cmd = new ArrayList<>(); + + /* + * First decide whether we are calling Cellpose from python, or directly + * the Cellpose executable. We check the last part of the path to check + * whether this is python or cellpose. + */ + final String[] split = executablePath.replace( "\\", "/" ).split( "/" ); + final String lastItem = split[ split.length - 1 ]; + if ( lastItem.toLowerCase().startsWith( "python" ) ) + { + // Calling Cellpose from python. + cmd.add( executablePath ); + cmd.add( "-m" ); + cmd.add( getExecutableName() ); + } + else + { + // Calling Cellpose executable. + cmd.add( executablePath ); + } + + /* + * Cellpose command line arguments. + */ + + // Target dir. + cmd.add( "--dir" ); + cmd.add( imagesDir ); + + // First channel. + cmd.add( "--chan" ); + cmd.add( "" + chan ); + + // Second channel. + if ( chan2 >= 0 ) + { + cmd.add( "--chan2" ); + cmd.add( "" + chan2 ); + } + + // GPU. + if ( useGPU ) + cmd.add( "--use_gpu" ); + + // Diameter. + cmd.add( "--diameter" ); + cmd.add( ( diameter > 0 ) ? "" + diameter : "0" ); + + // Model. + cmd.add( "--pretrained_model" ); + if ( model.isCustom() ) + cmd.add( customModelPath ); + else + cmd.add( model.getPath() ); + + // Export results as PNG. + cmd.add( "--save_png" ); + + // Do not save Numpy files. + cmd.add( "--no_npy" ); + + return Collections.unmodifiableList( cmd ); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetector.java b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetector.java index 3af5bdaf..be5d51df 100644 --- a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetector.java +++ b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetector.java @@ -1,24 +1,3 @@ -/*- - * #%L - * TrackMate: your buddy for everyday tracking. - * %% - * Copyright (C) 2021 - 2023 TrackMate developers. - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public - * License along with this program. If not, see - * . - * #L% - */ package fiji.plugin.trackmate.cellpose; import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_TARGET_CHANNEL; @@ -79,27 +58,22 @@ public class CellposeDetector< T extends RealType< T > & NativeType< T > > implements SpotGlobalDetector< T >, Cancelable, MultiThreaded { - private final static String BASE_ERROR_MESSAGE = "CellposeDetector: "; - private final static File CELLPOSE_LOG_FILE = new File( new File( System.getProperty( "user.home" ), ".cellpose" ), "run.log" ); + private static final Function< Long, String > nameGen = ( frame ) -> String.format( "%d", frame ); - private final static Function< Long, String > nameGen = ( frame ) -> String.format( "%d", frame ); + private final ImgPlus< T > img; - protected final ImgPlus< T > img; - - protected final Interval interval; - - private final CellposeSettings cellposeSettings; + private final Interval interval; private final Logger logger; - protected String baseErrorMessage; + private final String baseErrorMessage; - protected String errorMessage; + private String errorMessage; - protected long processingTime; + private long processingTime; - protected SpotCollection spots; + private SpotCollection spots; private String cancelReason; @@ -109,19 +83,25 @@ public class CellposeDetector< T extends RealType< T > & NativeType< T > > imple private int numThreads; + private final AbstractCellposeSettings cellposeSettings; + + private final File cellposeLogFile; + public CellposeDetector( final ImgPlus< T > img, final Interval interval, - final CellposeSettings cellposeSettings, + final AbstractCellposeSettings cellposeSettings, final Logger logger ) { this.img = img; this.interval = interval; this.cellposeSettings = cellposeSettings; this.logger = ( logger == null ) ? Logger.VOID_LOGGER : logger; - this.baseErrorMessage = BASE_ERROR_MESSAGE; + this.cellposeLogFile = new File( new File( System.getProperty( "user.home" ), "." + cellposeSettings.getExecutableName() ), "run.log" ); + this.baseErrorMessage = "[" + cellposeSettings.getExecutableName() + "Detector] "; } + @Override public boolean process() { @@ -191,7 +171,7 @@ public boolean process() */ // Redirect log to logger. - final Tailer tailer = Tailer.create( CELLPOSE_LOG_FILE, new LoggerTailerListener( logger ), 200, true ); + final Tailer tailer = Tailer.create( cellposeLogFile, new LoggerTailerListener( logger ), 200, true ); final ExecutorService executors = Executors.newFixedThreadPool( nConcurrentTasks ); final List< String > resultDirs = new ArrayList<>( nConcurrentTasks ); @@ -204,7 +184,9 @@ public boolean process() } catch ( final InterruptedException | ExecutionException e ) { - errorMessage = BASE_ERROR_MESSAGE + "Problem running Cellpose:\n" + e.getMessage() + '\n'; + errorMessage = baseErrorMessage + "Problem running " + + cellposeSettings.getExecutableName() + + ":\n" + e.getMessage() + '\n'; e.printStackTrace(); return false; } @@ -229,7 +211,7 @@ public boolean process() * Get the result masks back. */ - logger.log( "Reading Cellpose masks.\n" ); + logger.log( "Reading " + cellposeSettings.getExecutableName() + " masks.\n" ); final List< ImagePlus > masks = new ArrayList<>( imps.size() ); for ( int t = 0; t < imps.size(); t++ ) { @@ -276,7 +258,7 @@ public boolean process() final Concatenator concatenator = new Concatenator(); final ImagePlus output = concatenator.concatenateHyperstacks( masks.toArray( new ImagePlus[] {} ), - img.getName() + "_CellposeOutput", false ); + img.getName() + "_" + cellposeSettings.getExecutableName() + "Output", false ); // Copy calibration. final double[] calibration = TMUtils.getSpatialCalibration( img ); @@ -303,7 +285,7 @@ public boolean process() labelImgTrackMate.setNumThreads( numThreads ); if ( !labelImgTrackMate.execDetection() ) { - errorMessage = BASE_ERROR_MESSAGE + labelImgTrackMate.getErrorMessage(); + errorMessage = baseErrorMessage + labelImgTrackMate.getErrorMessage(); return false; } final SpotCollection tmpSpots = labelImgTrackMate.getModel().getSpots(); @@ -337,103 +319,13 @@ public boolean process() return true; } - private static final < T extends RealType< T > & NativeType< T > > List< ImagePlus > crop( final ImgPlus< T > img, final Interval interval, final Function< Long, String > nameGen ) - { - final int zIndex = img.dimensionIndex( Axes.Z ); - final int cIndex = img.dimensionIndex( Axes.CHANNEL ); - final Interval cropInterval; - if ( zIndex < 0 ) - { - // 2D - if ( cIndex < 0 ) - cropInterval = Intervals.createMinMax( - interval.min( 0 ), interval.min( 1 ), - interval.max( 0 ), interval.max( 1 ) ); - else - // Include all channels - cropInterval = Intervals.createMinMax( - interval.min( 0 ), interval.min( 1 ), img.min( cIndex ), - interval.max( 0 ), interval.max( 1 ), img.max( cIndex ) ); - } - else - { - if ( cIndex < 0 ) - cropInterval = Intervals.createMinMax( - interval.min( 0 ), interval.min( 1 ), interval.min( 2 ), - interval.max( 0 ), interval.max( 1 ), interval.max( 2 ) ); - else - cropInterval = Intervals.createMinMax( - interval.min( 0 ), interval.min( 1 ), interval.min( 2 ), img.min( cIndex ), - interval.max( 0 ), interval.max( 1 ), interval.max( 2 ), img.max( cIndex ) ); - } - - final List< ImagePlus > imps = new ArrayList<>(); - final int timeIndex = img.dimensionIndex( Axes.TIME ); - if ( timeIndex < 0 ) - { - // No time. - final IntervalView< T > crop = Views.interval( img, cropInterval ); - final String name = nameGen.apply( 0l ) + ".tif"; - imps.add( ImageJFunctions.wrap( crop, name ) ); - } - else - { - // In the interval, time is always the last. - final long minT = interval.min( interval.numDimensions() - 1 ); - final long maxT = interval.max( interval.numDimensions() - 1 ); - for ( long t = minT; t <= maxT; t++ ) - { - final ImgPlus< T > tp = ImgPlusViews.hyperSlice( img, timeIndex, t ); - // possibly 2D or 3D with or without channel. - final IntervalView< T > crop = Views.interval( tp, cropInterval ); - final String name = nameGen.apply( t ) + ".tif"; - imps.add( ImageJFunctions.wrap( crop, name ) ); - } - } - return imps; - } - - @Override - public SpotCollection getResult() - { - return spots; - } - - @Override - public boolean checkInput() - { - if ( null == img ) - { - errorMessage = baseErrorMessage + "Image is null."; - return false; - } - if ( img.dimensionIndex( Axes.Z ) >= 0 ) - { - errorMessage = baseErrorMessage + "Image must be 2D over time, got an image with multiple Z."; - return false; - } - return true; - } - - @Override - public String getErrorMessage() - { - return errorMessage; - } - - @Override - public long getProcessingTime() - { - return processingTime; - } - /** * Add a hook to delete the content of given path when Fiji quits. Taken * from https://stackoverflow.com/a/20280989/201698 * * @param path */ - private static void recursiveDeleteOnShutdownHook( final Path path ) + protected static void recursiveDeleteOnShutdownHook( final Path path ) { Runtime.getRuntime().addShutdownHook( new Thread( new Runnable() { @@ -471,6 +363,40 @@ public FileVisitResult postVisitDirectory( final Path dir, final IOException e ) } ) ); } + @Override + public SpotCollection getResult() + { + return spots; + } + + @Override + public boolean checkInput() + { + if ( null == img ) + { + errorMessage = baseErrorMessage + "Image is null."; + return false; + } + if ( img.dimensionIndex( Axes.Z ) >= 0 ) + { + errorMessage = baseErrorMessage + "Image must be 2D over time, got an image with multiple Z."; + return false; + } + return true; + } + + @Override + public String getErrorMessage() + { + return errorMessage; + } + + @Override + public long getProcessingTime() + { + return processingTime; + } + private static class LoggerTailerListener extends TailerListenerAdapter { private final Logger logger; @@ -541,7 +467,7 @@ public int getNumThreads() // --- private classes --- - private final class CellposeTask implements Callable< String > + final class CellposeTask implements Callable< String > { private Process process; @@ -561,7 +487,7 @@ public boolean isOk() return ok.get(); } - private void cancel() + void cancel() { if ( process != null ) process.destroy(); @@ -577,12 +503,12 @@ public String call() throws Exception Path tmpDir = null; try { - tmpDir = Files.createTempDirectory( "TrackMate-Cellpose_" ); + tmpDir = Files.createTempDirectory( "TrackMate-" + cellposeSettings.getExecutableName() + "_" ); recursiveDeleteOnShutdownHook( tmpDir ); } catch ( final IOException e1 ) { - errorMessage = BASE_ERROR_MESSAGE + "Could not create tmp dir to save and load images:\n" + e1.getMessage(); + errorMessage = baseErrorMessage + "Could not create tmp dir to save and load images:\n" + e1.getMessage(); ok.set( false ); return null; } @@ -607,8 +533,8 @@ public String call() throws Exception try { final List< String > cmd = cellposeSettings.toCmdLine( tmpDir.toString() ); - logger.setStatus( "Running Cellpose" ); - logger.log( "Running Cellpose with args:\n" ); + logger.setStatus( "Running " + cellposeSettings.getExecutableName() ); + logger.log( "Running " + cellposeSettings.getExecutableName() + " with args:\n" ); logger.log( String.join( " ", cmd ) ); logger.log( "\n" ); final ProcessBuilder pb = new ProcessBuilder( cmd ); @@ -623,13 +549,13 @@ public String call() throws Exception final String msg = e.getMessage(); if ( msg.matches( ".+error=13.+" ) ) { - errorMessage = BASE_ERROR_MESSAGE + "Problem running Cellpose:\n" + errorMessage = baseErrorMessage + "Problem running " + cellposeSettings.getExecutableName() + ":\n" + "The executable does not have the file permission to run.\n" + "Please see https://github.com/MouseLand/cellpose#run-cellpose-without-local-python-installation for more information.\n"; } else { - errorMessage = BASE_ERROR_MESSAGE + "Problem running Cellpose:\n" + e.getMessage(); + errorMessage = baseErrorMessage + "Problem running " + cellposeSettings.getExecutableName() + ":\n" + e.getMessage(); } e.printStackTrace(); ok.set( false ); @@ -637,7 +563,7 @@ public String call() throws Exception } catch ( final Exception e ) { - errorMessage = BASE_ERROR_MESSAGE + "Problem running Cellpose:\n" + e.getMessage(); + errorMessage = baseErrorMessage + "Problem running " + cellposeSettings.getExecutableName() + ":\n" + e.getMessage(); e.printStackTrace(); ok.set( false ); return null; @@ -649,4 +575,60 @@ public String call() throws Exception return tmpDir.toString(); } } + + private static final < T extends RealType< T > & NativeType< T > > List< ImagePlus > crop( final ImgPlus< T > img, final Interval interval, final Function< Long, String > nameGen ) + { + final int zIndex = img.dimensionIndex( Axes.Z ); + final int cIndex = img.dimensionIndex( Axes.CHANNEL ); + final Interval cropInterval; + if ( zIndex < 0 ) + { + // 2D + if ( cIndex < 0 ) + cropInterval = Intervals.createMinMax( + interval.min( 0 ), interval.min( 1 ), + interval.max( 0 ), interval.max( 1 ) ); + else + // Include all channels + cropInterval = Intervals.createMinMax( + interval.min( 0 ), interval.min( 1 ), img.min( cIndex ), + interval.max( 0 ), interval.max( 1 ), img.max( cIndex ) ); + } + else + { + if ( cIndex < 0 ) + cropInterval = Intervals.createMinMax( + interval.min( 0 ), interval.min( 1 ), interval.min( 2 ), + interval.max( 0 ), interval.max( 1 ), interval.max( 2 ) ); + else + cropInterval = Intervals.createMinMax( + interval.min( 0 ), interval.min( 1 ), interval.min( 2 ), img.min( cIndex ), + interval.max( 0 ), interval.max( 1 ), interval.max( 2 ), img.max( cIndex ) ); + } + + final List< ImagePlus > imps = new ArrayList<>(); + final int timeIndex = img.dimensionIndex( Axes.TIME ); + if ( timeIndex < 0 ) + { + // No time. + final IntervalView< T > crop = Views.interval( img, cropInterval ); + final String name = nameGen.apply( 0l ) + ".tif"; + imps.add( ImageJFunctions.wrap( crop, name ) ); + } + else + { + // In the interval, time is always the last. + final long minT = interval.min( interval.numDimensions() - 1 ); + final long maxT = interval.max( interval.numDimensions() - 1 ); + for ( long t = minT; t <= maxT; t++ ) + { + final ImgPlus< T > tp = ImgPlusViews.hyperSlice( img, timeIndex, t ); + // possibly 2D or 3D with or without channel. + final IntervalView< T > crop = Views.interval( tp, cropInterval ); + final String name = nameGen.apply( t ) + ".tif"; + imps.add( ImageJFunctions.wrap( crop, name ) ); + } + } + return imps; + } } diff --git a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorConfigurationPanel.java index a6a24ea1..c61ee3da 100644 --- a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorConfigurationPanel.java +++ b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorConfigurationPanel.java @@ -55,6 +55,7 @@ import java.util.Map; import java.util.Vector; +import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -67,7 +68,9 @@ import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.cellpose.CellposeSettings.PretrainedModel; +import fiji.plugin.trackmate.cellpose.AbstractCellposeSettings.PretrainedModel; +import fiji.plugin.trackmate.cellpose.CellposeSettings.PretrainedModelCellpose; +import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; import fiji.plugin.trackmate.gui.components.ConfigurationPanel; import fiji.plugin.trackmate.util.DetectionPreview; import fiji.plugin.trackmate.util.FileChooser; @@ -81,7 +84,7 @@ public class CellposeDetectorConfigurationPanel extends ConfigurationPanel private static final String TITLE = CellposeDetectorFactory.NAME; - private static final ImageIcon ICON = CellposeUtils.logo64(); + private static final ImageIcon ICON = CellposeUtils.cellposeLogo64(); private static final NumberFormat DIAMETER_FORMAT = new DecimalFormat( "#.#" ); @@ -89,28 +92,45 @@ public class CellposeDetectorConfigurationPanel extends ConfigurationPanel private final JButton btnBrowseCellposePath; - private final JTextField tfCellposeExecutable; + protected final JTextField tfCellposeExecutable; - private final JComboBox< PretrainedModel > cmbboxPretrainedModel; + protected final JComboBox< PretrainedModel > cmbboxPretrainedModel; - private final JComboBox< String > cmbboxCh1; + protected final JComboBox< String > cmbboxCh1; - private final JComboBox< String > cmbboxCh2; + protected final JComboBox< String > cmbboxCh2; - private final JFormattedTextField ftfDiameter; + protected final JFormattedTextField ftfDiameter; - private final JCheckBox chckbxSimplify; + protected final JCheckBox chckbxSimplify; - private final Logger logger; + protected final Logger logger; - private final JCheckBox chckbxUseGPU; + protected final JCheckBox chckbxUseGPU; - private final JTextField tfCustomPath; + protected final JTextField tfCustomPath; private final JButton btnBrowseCustomModel; - public CellposeDetectorConfigurationPanel( final Settings settings, final Model model ) + private final String executableName; + + public CellposeDetectorConfigurationPanel( + final Settings settings, + final Model model ) + { + this( settings, model, TITLE, ICON, DOC1_URL, "cellpose", PretrainedModelCellpose.values() ); + } + + protected CellposeDetectorConfigurationPanel( + final Settings settings, + final Model model, + final String title, + final Icon icon, + final String docURL, + final String executableName, + final PretrainedModel[] pretrainedModels ) { + this.executableName = executableName; this.logger = model.getLogger(); final GridBagLayout gridBagLayout = new GridBagLayout(); @@ -119,7 +139,7 @@ public CellposeDetectorConfigurationPanel( final Settings settings, final Model gridBagLayout.columnWeights = new double[] { 1.0, 1.0, 0.0 }; setLayout( gridBagLayout ); - final JLabel lblDetector = new JLabel( TITLE, ICON, JLabel.RIGHT ); + final JLabel lblDetector = new JLabel( title, icon, JLabel.RIGHT ); lblDetector.setFont( BIG_FONT ); lblDetector.setHorizontalAlignment( SwingConstants.CENTER ); final GridBagConstraints gbcLblDetector = new GridBagConstraints(); @@ -143,7 +163,7 @@ public void mouseClicked( final java.awt.event.MouseEvent e ) { try { - Desktop.getDesktop().browse( new URI( DOC1_URL ) ); + Desktop.getDesktop().browse( new URI( docURL ) ); } catch ( URISyntaxException | IOException ex ) { @@ -160,7 +180,7 @@ public void mouseExited( final java.awt.event.MouseEvent e ) @Override public void mouseEntered( final java.awt.event.MouseEvent e ) { - lblUrl.setText( "" + DOC1_URL + "" ); + lblUrl.setText( "" + docURL + "" ); } } ); final GridBagConstraints gbcLblUrl = new GridBagConstraints(); @@ -175,7 +195,7 @@ public void mouseEntered( final java.awt.event.MouseEvent e ) * Path to Python or Cellpose. */ - final JLabel lblCusstomModelFile = new JLabel( "Path to cellpose / python executable:" ); + final JLabel lblCusstomModelFile = new JLabel( "Path to " + executableName + " / python executable:" ); lblCusstomModelFile.setFont( FONT ); final GridBagConstraints gbcLblCusstomModelFile = new GridBagConstraints(); gbcLblCusstomModelFile.gridwidth = 2; @@ -252,7 +272,7 @@ public void mouseEntered( final java.awt.event.MouseEvent e ) gbcLblPretrainedModel.gridy = 6; add( lblPretrainedModel, gbcLblPretrainedModel ); - cmbboxPretrainedModel = new JComboBox<>( new Vector<>( Arrays.asList( PretrainedModel.values() ) ) ); + cmbboxPretrainedModel = new JComboBox<>( new Vector<>( Arrays.asList( pretrainedModels ) ) ); cmbboxPretrainedModel.setFont( SMALL_FONT ); final GridBagConstraints gbcCmbboxPretrainedModel = new GridBagConstraints(); gbcCmbboxPretrainedModel.gridwidth = 2; @@ -373,7 +393,7 @@ public void mouseEntered( final java.awt.event.MouseEvent e ) final DetectionPreview detectionPreview = DetectionPreview.create() .model( model ) .settings( settings ) - .detectorFactory( new CellposeDetectorFactory<>() ) + .detectorFactory( getDetectorFactory() ) .detectionSettingsSupplier( () -> getSettings() ) .axisLabel( "Area histogram" ) .get(); @@ -395,7 +415,7 @@ public void mouseEntered( final java.awt.event.MouseEvent e ) */ final ItemListener l3 = e -> { - final boolean isCustom = cmbboxPretrainedModel.getSelectedItem() == PretrainedModel.CUSTOM; + final boolean isCustom = ( ( PretrainedModel ) cmbboxPretrainedModel.getSelectedItem() ).isCustom(); tfCustomPath.setVisible( isCustom ); lblPathToCustomModel.setVisible( isCustom ); btnBrowseCustomModel.setVisible( isCustom ); @@ -407,13 +427,18 @@ public void mouseEntered( final java.awt.event.MouseEvent e ) btnBrowseCustomModel.addActionListener( l -> browseCustomModelPath() ); } - protected void browseCustomModelPath() + protected SpotDetectorFactoryBase< ? > getDetectorFactory() + { + return new CellposeDetectorFactory<>(); + } + + private void browseCustomModelPath() { btnBrowseCustomModel.setEnabled( false ); try { final File file = FileChooser.chooseFile( this, tfCustomPath.getText(), null, - "Browse to a Cellpose custom model", DialogType.LOAD, SelectionMode.FILES_ONLY ); + "Browse to a " + executableName + " custom model", DialogType.LOAD, SelectionMode.FILES_ONLY ); if ( file != null ) tfCustomPath.setText( file.getAbsolutePath() ); } @@ -423,13 +448,13 @@ protected void browseCustomModelPath() } } - protected void browseCellposePath() + private void browseCellposePath() { btnBrowseCellposePath.setEnabled( false ); try { final File file = FileChooser.chooseFile( this, tfCellposeExecutable.getText(), null, - "Browse to the Cellpose Python executable", DialogType.LOAD, SelectionMode.FILES_ONLY ); + "Browse to the " + executableName + " Python executable", DialogType.LOAD, SelectionMode.FILES_ONLY ); if ( file != null ) tfCellposeExecutable.setText( file.getAbsolutePath() ); } diff --git a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorFactory.java index 769c5fa8..92f5b071 100644 --- a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorFactory.java +++ b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeDetectorFactory.java @@ -41,12 +41,13 @@ import javax.swing.ImageIcon; import org.jdom2.Element; +import org.scijava.Priority; import org.scijava.plugin.Plugin; import fiji.plugin.trackmate.Logger; import fiji.plugin.trackmate.Model; import fiji.plugin.trackmate.Settings; -import fiji.plugin.trackmate.cellpose.CellposeSettings.PretrainedModel; +import fiji.plugin.trackmate.cellpose.CellposeSettings.PretrainedModelCellpose; import fiji.plugin.trackmate.detection.SpotDetectorFactory; import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; import fiji.plugin.trackmate.detection.SpotGlobalDetector; @@ -59,7 +60,7 @@ import net.imglib2.type.NativeType; import net.imglib2.type.numeric.RealType; -@Plugin( type = SpotDetectorFactory.class ) +@Plugin( type = SpotDetectorFactory.class, priority = Priority.LOW ) public class CellposeDetectorFactory< T extends RealType< T > & NativeType< T > > implements SpotGlobalDetectorFactory< T > { @@ -69,11 +70,11 @@ public class CellposeDetectorFactory< T extends RealType< T > & NativeType< T > /** * The key to the parameter that stores the path the cellpose model to use. - * Value can be {@link CellposeSettings.PretrainedModel}. + * Value can be {@link CellposeSettings.PretrainedModelCellpose}. */ public static final String KEY_CELLPOSE_MODEL = "CELLPOSE_MODEL"; - public static final PretrainedModel DEFAULT_CELLPOSE_MODEL = PretrainedModel.CYTO; + public static final PretrainedModelCellpose DEFAULT_CELLPOSE_MODEL = PretrainedModelCellpose.CYTO; /** * The key to the parameter that stores the path to the Python instance that @@ -181,7 +182,7 @@ public class CellposeDetectorFactory< T extends RealType< T > & NativeType< T > public SpotGlobalDetector< T > getDetector( final Interval interval ) { final String cellposePythonPath = ( String ) settings.get( KEY_CELLPOSE_PYTHON_FILEPATH ); - final PretrainedModel model = ( PretrainedModel ) settings.get( KEY_CELLPOSE_MODEL ); + final PretrainedModelCellpose model = ( PretrainedModelCellpose ) settings.get( KEY_CELLPOSE_MODEL ); final String customModelPath = ( String ) settings.get( KEY_CELLPOSE_CUSTOM_MODEL_FILEPATH ); final boolean simplifyContours = ( boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); final boolean useGPU = ( boolean ) settings.get( KEY_USE_GPU ); @@ -252,7 +253,7 @@ public boolean marshall( final Map< String, Object > settings, final Element ele ok = ok && writeAttribute( settings, element, KEY_USE_GPU, Boolean.class, errorHolder ); ok = ok && writeAttribute( settings, element, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); - final PretrainedModel model = ( PretrainedModel ) settings.get( KEY_CELLPOSE_MODEL ); + final PretrainedModelCellpose model = ( PretrainedModelCellpose ) settings.get( KEY_CELLPOSE_MODEL ); element.setAttribute( KEY_CELLPOSE_MODEL, model.name() ); if ( !ok ) @@ -282,7 +283,7 @@ public boolean unmarshall( final Element element, final Map< String, Object > se errorHolder.append( "Attribute " + KEY_CELLPOSE_MODEL + " could not be found in XML element.\n" ); ok = false; } - settings.put( KEY_CELLPOSE_MODEL, PretrainedModel.valueOf( str ) ); + settings.put( KEY_CELLPOSE_MODEL, PretrainedModelCellpose.valueOf( str ) ); return checkSettings( settings ); } @@ -316,7 +317,7 @@ public boolean checkSettings( final Map< String, Object > settings ) final StringBuilder errorHolder = new StringBuilder(); ok = ok & checkParameter( settings, KEY_CELLPOSE_PYTHON_FILEPATH, String.class, errorHolder ); ok = ok & checkParameter( settings, KEY_CELLPOSE_CUSTOM_MODEL_FILEPATH, String.class, errorHolder ); - ok = ok & checkParameter( settings, KEY_CELLPOSE_MODEL, PretrainedModel.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_CELLPOSE_MODEL, PretrainedModelCellpose.class, errorHolder ); ok = ok & checkParameter( settings, KEY_TARGET_CHANNEL, Integer.class, errorHolder ); ok = ok & checkParameter( settings, KEY_OPTIONAL_CHANNEL_2, Integer.class, errorHolder ); ok = ok & checkParameter( settings, KEY_CELL_DIAMETER, Double.class, errorHolder ); diff --git a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeSettings.java b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeSettings.java index 187c63cd..d1b117cb 100644 --- a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeSettings.java +++ b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeSettings.java @@ -21,33 +21,12 @@ */ package fiji.plugin.trackmate.cellpose; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class CellposeSettings +public class CellposeSettings extends AbstractCellposeSettings { - public final String cellposePythonPath; - - public final int chan; - - public final int chan2; - - public final PretrainedModel model; - - public final String customModelPath; - - public final double diameter; - - public final boolean useGPU; - - public final boolean simplifyContours; - - public CellposeSettings( final String cellposePythonPath, - final PretrainedModel model, + final PretrainedModelCellpose model, final String customModelPath, final int chan, final int chan2, @@ -55,81 +34,13 @@ public CellposeSettings( final boolean useGPU, final boolean simplifyContours ) { - this.cellposePythonPath = cellposePythonPath; - this.model = model; - this.customModelPath = customModelPath; - this.chan = chan; - this.chan2 = chan2; - this.diameter = diameter; - this.useGPU = useGPU; - this.simplifyContours = simplifyContours; + super( cellposePythonPath, model, customModelPath, chan, chan2, diameter, useGPU, simplifyContours ); } - public List< String > toCmdLine( final String imagesDir ) + @Override + public String getExecutableName() { - final List< String > cmd = new ArrayList<>(); - - /* - * First decide whether we are calling Cellpose from python, or directly - * the Cellpose executable. We check the last part of the path to check - * whether this is python or cellpose. - */ - final String[] split = cellposePythonPath.replace( "\\", "/" ).split( "/" ); - final String lastItem = split[ split.length - 1 ]; - if ( lastItem.toLowerCase().startsWith( "python" ) ) - { - // Calling Cellpose from python. - cmd.add( cellposePythonPath ); - cmd.add( "-m" ); - cmd.add( "cellpose" ); - } - else - { - // Calling Cellpose executable. - cmd.add( cellposePythonPath ); - } - - /* - * Cellpose command line arguments. - */ - - // Target dir. - cmd.add( "--dir" ); - cmd.add( imagesDir ); - - // First channel. - cmd.add( "--chan" ); - cmd.add( "" + chan ); - - // Second channel. - if ( chan2 >= 0 ) - { - cmd.add( "--chan2" ); - cmd.add( "" + chan2 ); - } - - // GPU. - if ( useGPU ) - cmd.add( "--use_gpu" ); - - // Diameter. - cmd.add( "--diameter" ); - cmd.add( ( diameter > 0 ) ? "" + diameter : "0" ); - - // Model. - cmd.add( "--pretrained_model" ); - if ( model == PretrainedModel.CUSTOM ) - cmd.add( customModelPath ); - else - cmd.add( model.path ); - - // Export results as PNG. - cmd.add( "--save_png" ); - - // Do not save Numpy files. - cmd.add( "--no_npy" ); - - return Collections.unmodifiableList( cmd ); + return "cellpose"; } public static Builder create() @@ -146,7 +57,7 @@ public static final class Builder private int chan2 = -1; - private PretrainedModel model = PretrainedModel.CYTO; + private PretrainedModelCellpose model = PretrainedModelCellpose.CYTO; private double diameter = 30.; @@ -174,7 +85,7 @@ public Builder cellposePythonPath( final String cellposePythonPath ) return this; } - public Builder model( final PretrainedModel model ) + public Builder model( final PretrainedModelCellpose model ) { this.model = model; return this; @@ -219,21 +130,24 @@ public CellposeSettings get() } - public enum PretrainedModel + public enum PretrainedModelCellpose implements PretrainedModel { - CYTO( "Cytoplasm", "cyto" ), - NUCLEI( "Nucleus", "nuclei" ), - CYTO2( "Cytoplasm 2.0", "cyto2" ), - CUSTOM( "Custom", "" ); + CYTO( "Cytoplasm", "cyto", false ), + NUCLEI( "Nucleus", "nuclei", false ), + CYTO2( "Cytoplasm 2.0", "cyto2", false ), + CUSTOM( "Custom", "", true ); private final String name; - private final String path; + final String path; + + private final boolean isCustom; - PretrainedModel( final String name, final String path ) + PretrainedModelCellpose( final String name, final String path, final boolean isCustom ) { this.name = name; this.path = path; + this.isCustom = isCustom; } @Override @@ -242,12 +156,18 @@ public String toString() return name; } - public String cellposeName() + @Override + public boolean isCustom() + { + return isCustom; + } + + @Override + public String getPath() { return path; } } public static final CellposeSettings DEFAULT = new Builder().get(); - } diff --git a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeUtils.java b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeUtils.java index 781f1d3b..76c38db7 100644 --- a/src/main/java/fiji/plugin/trackmate/cellpose/CellposeUtils.java +++ b/src/main/java/fiji/plugin/trackmate/cellpose/CellposeUtils.java @@ -60,14 +60,24 @@ public static URL getResource( final String name ) return CellposeDetectorFactory.class.getClassLoader().getResource( name ); } - public static final ImageIcon logo() + public static final ImageIcon cellposeLogo() { return new ImageIcon( getResource( "images/cellposelogo.png" ) ); } - public static final ImageIcon logo64() + public static final ImageIcon cellposeLogo64() { - return scaleImage( logo(), 64, 64 ); + return scaleImage( cellposeLogo(), 64, 64 ); + } + + public static final ImageIcon omniposeLogo() + { + return new ImageIcon( getResource( "images/omniposelogo.png" ) ); + } + + public static final ImageIcon omniposeLogo64() + { + return scaleImage( omniposeLogo(), 64, 64 ); } public static final ImageIcon scaleImage( final ImageIcon icon, final int w, final int h ) diff --git a/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeDetectorConfigurationPanel.java b/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeDetectorConfigurationPanel.java new file mode 100644 index 00000000..be74aadf --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeDetectorConfigurationPanel.java @@ -0,0 +1,102 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2021 - 2023 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package fiji.plugin.trackmate.omnipose; + +import static fiji.plugin.trackmate.cellpose.CellposeDetectorFactory.KEY_CELL_DIAMETER; +import static fiji.plugin.trackmate.cellpose.CellposeDetectorFactory.KEY_LOGGER; +import static fiji.plugin.trackmate.cellpose.CellposeDetectorFactory.KEY_OPTIONAL_CHANNEL_2; +import static fiji.plugin.trackmate.cellpose.CellposeDetectorFactory.KEY_USE_GPU; +import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.omnipose.OmniposeDetectorFactory.KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH; +import static fiji.plugin.trackmate.omnipose.OmniposeDetectorFactory.KEY_OMNIPOSE_MODEL; +import static fiji.plugin.trackmate.omnipose.OmniposeDetectorFactory.KEY_OMNIPOSE_PYTHON_FILEPATH; + +import java.util.HashMap; +import java.util.Map; + +import javax.swing.ImageIcon; + +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.cellpose.CellposeDetectorConfigurationPanel; +import fiji.plugin.trackmate.cellpose.CellposeUtils; +import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; +import fiji.plugin.trackmate.omnipose.OmniposeSettings.PretrainedModelOmnipose; + +public class OmniposeDetectorConfigurationPanel extends CellposeDetectorConfigurationPanel +{ + + private static final long serialVersionUID = 1L; + + private static final String TITLE = OmniposeDetectorFactory.NAME; + + private static final ImageIcon ICON = CellposeUtils.omniposeLogo64(); + + protected static final String DOC1_URL = ""; + + public OmniposeDetectorConfigurationPanel( final Settings settings, final Model model ) + { + super( settings, model, TITLE, ICON, DOC1_URL, "omnipose", PretrainedModelOmnipose.values() ); + } + + @Override + protected SpotDetectorFactoryBase< ? > getDetectorFactory() + { + return new OmniposeDetectorFactory<>(); + } + + @Override + public void setSettings( final Map< String, Object > settings ) + { + tfCellposeExecutable.setText( ( String ) settings.get( KEY_OMNIPOSE_PYTHON_FILEPATH ) ); + tfCustomPath.setText( ( String ) settings.get( KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH ) ); + cmbboxPretrainedModel.setSelectedItem( settings.get( KEY_OMNIPOSE_MODEL ) ); + cmbboxCh1.setSelectedIndex( ( int ) settings.get( KEY_TARGET_CHANNEL ) ); + cmbboxCh2.setSelectedIndex( ( int ) settings.get( KEY_OPTIONAL_CHANNEL_2 ) ); + ftfDiameter.setValue( settings.get( KEY_CELL_DIAMETER ) ); + chckbxUseGPU.setSelected( ( boolean ) settings.get( KEY_USE_GPU ) ); + chckbxSimplify.setSelected( ( boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ) ); + } + + @Override + public Map< String, Object > getSettings() + { + final HashMap< String, Object > settings = new HashMap<>( 9 ); + + settings.put( KEY_OMNIPOSE_PYTHON_FILEPATH, tfCellposeExecutable.getText() ); + settings.put( KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH, tfCustomPath.getText() ); + settings.put( KEY_OMNIPOSE_MODEL, cmbboxPretrainedModel.getSelectedItem() ); + + settings.put( KEY_TARGET_CHANNEL, cmbboxCh1.getSelectedIndex() ); + settings.put( KEY_OPTIONAL_CHANNEL_2, cmbboxCh2.getSelectedIndex() ); + + final double diameter = ( ( Number ) ftfDiameter.getValue() ).doubleValue(); + settings.put( KEY_CELL_DIAMETER, diameter ); + settings.put( KEY_SIMPLIFY_CONTOURS, chckbxSimplify.isSelected() ); + settings.put( KEY_USE_GPU, chckbxUseGPU.isSelected() ); + + settings.put( KEY_LOGGER, logger ); + + return settings; + } +} diff --git a/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeDetectorFactory.java b/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeDetectorFactory.java new file mode 100644 index 00000000..1c00ce65 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeDetectorFactory.java @@ -0,0 +1,318 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2021 - 2023 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package fiji.plugin.trackmate.omnipose; + +import static fiji.plugin.trackmate.detection.DetectorKeys.DEFAULT_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.DetectorKeys.KEY_TARGET_CHANNEL; +import static fiji.plugin.trackmate.detection.ThresholdDetectorFactory.KEY_SIMPLIFY_CONTOURS; +import static fiji.plugin.trackmate.io.IOUtils.readBooleanAttribute; +import static fiji.plugin.trackmate.io.IOUtils.readDoubleAttribute; +import static fiji.plugin.trackmate.io.IOUtils.readIntegerAttribute; +import static fiji.plugin.trackmate.io.IOUtils.readStringAttribute; +import static fiji.plugin.trackmate.io.IOUtils.writeAttribute; +import static fiji.plugin.trackmate.io.IOUtils.writeTargetChannel; +import static fiji.plugin.trackmate.util.TMUtils.checkMapKeys; +import static fiji.plugin.trackmate.util.TMUtils.checkParameter; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jdom2.Element; +import org.scijava.Priority; +import org.scijava.plugin.Plugin; + +import fiji.plugin.trackmate.Logger; +import fiji.plugin.trackmate.Model; +import fiji.plugin.trackmate.Settings; +import fiji.plugin.trackmate.cellpose.CellposeDetector; +import fiji.plugin.trackmate.cellpose.CellposeDetectorFactory; +import fiji.plugin.trackmate.detection.SpotDetectorFactory; +import fiji.plugin.trackmate.detection.SpotDetectorFactoryBase; +import fiji.plugin.trackmate.detection.SpotGlobalDetector; +import fiji.plugin.trackmate.gui.components.ConfigurationPanel; +import fiji.plugin.trackmate.io.IOUtils; +import fiji.plugin.trackmate.omnipose.OmniposeSettings.PretrainedModelOmnipose; +import fiji.plugin.trackmate.util.TMUtils; +import net.imglib2.Interval; +import net.imglib2.type.NativeType; +import net.imglib2.type.numeric.RealType; + +@Plugin( type = SpotDetectorFactory.class, priority = Priority.LOW ) +public class OmniposeDetectorFactory< T extends RealType< T > & NativeType< T > > extends CellposeDetectorFactory< T > +{ + + /* + * CONSTANTS + */ + + /** + * The key to the parameter that stores the path the omnipose model to use. + * Value can be {@link OmniposeSettings.PretrainedModelOmnipose}. + */ + public static final String KEY_OMNIPOSE_MODEL = "OMNIPOSE_MODEL"; + + public static final PretrainedModelOmnipose DEFAULT_OMNIPOSE_MODEL = PretrainedModelOmnipose.BACT_PHASE; + + /** + * The key to the parameter that stores the path to the Python instance that + * can run omnipose if you installed it via Conda or the omnipose executable + * if you have installed the standalone version. Something like + * '/opt/anaconda3/envs/omnipose/bin/python' or + * 'C:\Users\tinevez\Applications\omnipose.exe'. + */ + public static final String KEY_OMNIPOSE_PYTHON_FILEPATH = "OMNIPOSE_PYTHON_FILEPATH"; + + public static final String DEFAULT_OMNIPOSE_PYTHON_FILEPATH = "/opt/anaconda3/envs/omnipose/bin/python"; + + /** + * The key to the parameter that stores the path to the custom model file to + * use with Omnipose. It must be an absolute file path. + */ + public static final String KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH = "OMNIPOSE_MODEL_FILEPATH"; + + public static final String DEFAULT_OMNIPOSE_CUSTOM_MODEL_FILEPATH = ""; + + /** A string key identifying this factory. */ + public static final String DETECTOR_KEY = "OMNIPOSE_DETECTOR"; + + /** The pretty name of the target detector. */ + public static final String NAME = "Omnipose detector"; + + /** An html information text. */ + public static final String INFO_TEXT = "" + + "This detector relies on omnipose to detect objects." + + "

" + + "The detector simply calls an external omnipose installation. So for this " + + "to work, you must have a omnipose installation running on your computer. " + + "Please follow the instructions from the omnipose website: " + + "https://github.com/kevinjohncutler/omnipose" + + "

" + + "You will also need to specify the path to the Python executable that can run omnipose " + + "or the omnipose executable directly. " + + "For instance if you used anaconda to install omnipose, and that you have a " + + "Conda environment called 'omnipose', this path will be something along the line of " + + "'/opt/anaconda3/envs/omnipose/bin/python' or 'C:\\\\Users\\\\tinevez\\\\anaconda3\\\\envs\\\\omnipose_biop_gpu\\\\python.exe' " + + "If you installed the standalone version, the path to it would something like " + + "this on Windows: 'C:\\Users\\tinevez\\Applications\\omnipose.exe'. " + + "

" + + "If you use this detector for your work, please be so kind as to " + + "also cite the omnipose paper: Cutler, Kevin J., et al., " + + "'Omnipose: A High-Precision Morphology-Independent Solution for Bacterial Cell Segmentation.' " + + "Nature Methods 19, no. 11 (November 2022): 1438–48." + + ""; + + /* + * METHODS + */ + + @Override + public SpotGlobalDetector< T > getDetector( final Interval interval ) + { + final String omniposePythonPath = ( String ) settings.get( KEY_OMNIPOSE_PYTHON_FILEPATH ); + final PretrainedModelOmnipose model = ( PretrainedModelOmnipose ) settings.get( KEY_OMNIPOSE_MODEL ); + final String customModelPath = ( String ) settings.get( KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH ); + final boolean simplifyContours = ( boolean ) settings.get( KEY_SIMPLIFY_CONTOURS ); + final boolean useGPU = ( boolean ) settings.get( KEY_USE_GPU ); + + // Channels are 0-based (0: grayscale, then R & G & B). + final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ); + final int channel2 = ( Integer ) settings.get( KEY_OPTIONAL_CHANNEL_2 ); + + // Convert to diameter in pixels. + final double[] calibration = TMUtils.getSpatialCalibration( img ); + final double diameter = ( double ) settings.get( KEY_CELL_DIAMETER ) / calibration[ 0 ]; + + final OmniposeSettings omniposeSettings = OmniposeSettings.create() + .omniposePythonPath( omniposePythonPath ) + .customModel( customModelPath ) + .model( model ) + .channel1( channel ) + .channel2( channel2 ) + .diameter( diameter ) + .useGPU( useGPU ) + .simplifyContours( simplifyContours ) + .get(); + + // Logger. + final Logger logger = ( Logger ) settings.get( KEY_LOGGER ); + final CellposeDetector< T > detector = new CellposeDetector<>( + img, + interval, + omniposeSettings, + logger ); + return detector; + } + + @Override + public boolean marshall( final Map< String, Object > settings, final Element element ) + { + final StringBuilder errorHolder = new StringBuilder(); + boolean ok = writeTargetChannel( settings, element, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_OMNIPOSE_PYTHON_FILEPATH, String.class, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH, String.class, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_TARGET_CHANNEL, Integer.class, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_OPTIONAL_CHANNEL_2, Integer.class, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_CELL_DIAMETER, Double.class, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_USE_GPU, Boolean.class, errorHolder ); + ok = ok && writeAttribute( settings, element, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + + final PretrainedModelOmnipose model = ( PretrainedModelOmnipose ) settings.get( KEY_OMNIPOSE_MODEL ); + element.setAttribute( KEY_OMNIPOSE_MODEL, model.name() ); + + if ( !ok ) + errorMessage = errorHolder.toString(); + + return ok; + } + + @Override + public boolean unmarshall( final Element element, final Map< String, Object > settings ) + { + settings.clear(); + final StringBuilder errorHolder = new StringBuilder(); + boolean ok = true; + ok = ok && readStringAttribute( element, settings, KEY_OMNIPOSE_PYTHON_FILEPATH, errorHolder ); + ok = ok && readStringAttribute( element, settings, KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH, errorHolder ); + ok = ok && readIntegerAttribute( element, settings, KEY_TARGET_CHANNEL, errorHolder ); + ok = ok && readIntegerAttribute( element, settings, KEY_OPTIONAL_CHANNEL_2, errorHolder ); + ok = ok && readDoubleAttribute( element, settings, KEY_CELL_DIAMETER, errorHolder ); + ok = ok && readBooleanAttribute( element, settings, KEY_USE_GPU, errorHolder ); + ok = ok && readBooleanAttribute( element, settings, KEY_SIMPLIFY_CONTOURS, errorHolder ); + + // Read model. + final String str = element.getAttributeValue( KEY_OMNIPOSE_MODEL ); + if ( null == str ) + { + errorHolder.append( "Attribute " + KEY_OMNIPOSE_MODEL + " could not be found in XML element.\n" ); + ok = false; + } + settings.put( KEY_OMNIPOSE_MODEL, PretrainedModelOmnipose.valueOf( str ) ); + + return checkSettings( settings ); + } + + @Override + public ConfigurationPanel getDetectorConfigurationPanel( final Settings settings, final Model model ) + { + return new OmniposeDetectorConfigurationPanel( settings, model ); + } + + @Override + public Map< String, Object > getDefaultSettings() + { + final Map< String, Object > settings = new HashMap<>(); + settings.put( KEY_OMNIPOSE_PYTHON_FILEPATH, DEFAULT_OMNIPOSE_PYTHON_FILEPATH ); + settings.put( KEY_OMNIPOSE_MODEL, DEFAULT_OMNIPOSE_MODEL ); + settings.put( KEY_TARGET_CHANNEL, DEFAULT_TARGET_CHANNEL ); + settings.put( KEY_OPTIONAL_CHANNEL_2, DEFAULT_OPTIONAL_CHANNEL_2 ); + settings.put( KEY_CELL_DIAMETER, DEFAULT_CELL_DIAMETER ); + settings.put( KEY_USE_GPU, DEFAULT_USE_GPU ); + settings.put( KEY_SIMPLIFY_CONTOURS, true ); + settings.put( KEY_LOGGER, Logger.DEFAULT_LOGGER ); + settings.put( KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH, DEFAULT_OMNIPOSE_CUSTOM_MODEL_FILEPATH ); + return settings; + } + + @Override + public boolean checkSettings( final Map< String, Object > settings ) + { + boolean ok = true; + final StringBuilder errorHolder = new StringBuilder(); + ok = ok & checkParameter( settings, KEY_OMNIPOSE_PYTHON_FILEPATH, String.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH, String.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_OMNIPOSE_MODEL, PretrainedModelOmnipose.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_TARGET_CHANNEL, Integer.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_OPTIONAL_CHANNEL_2, Integer.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_CELL_DIAMETER, Double.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_USE_GPU, Boolean.class, errorHolder ); + ok = ok & checkParameter( settings, KEY_SIMPLIFY_CONTOURS, Boolean.class, errorHolder ); + + // If we have a logger, test it is of the right class. + final Object loggerObj = settings.get( KEY_LOGGER ); + if ( loggerObj != null && !Logger.class.isInstance( loggerObj ) ) + { + errorHolder.append( "Value for parameter " + KEY_LOGGER + " is not of the right class. " + + "Expected " + Logger.class.getName() + ", got " + loggerObj.getClass().getName() + ".\n" ); + ok = false; + } + + final List< String > mandatoryKeys = Arrays.asList( + KEY_OMNIPOSE_PYTHON_FILEPATH, + KEY_OMNIPOSE_MODEL, + KEY_TARGET_CHANNEL, + KEY_OPTIONAL_CHANNEL_2, + KEY_CELL_DIAMETER, + KEY_USE_GPU, + KEY_SIMPLIFY_CONTOURS ); + final List< String > optionalKeys = Arrays.asList( + KEY_OMNIPOSE_CUSTOM_MODEL_FILEPATH, + KEY_LOGGER ); + ok = ok & checkMapKeys( settings, mandatoryKeys, optionalKeys, errorHolder ); + if ( !ok ) + errorMessage = errorHolder.toString(); + + // Extra test to make sure we can read the classifier file. + if ( ok ) + { + final Object obj = settings.get( KEY_OMNIPOSE_PYTHON_FILEPATH ); + if ( obj == null ) + { + errorMessage = "The path to the Omnipose python executable is not set."; + return false; + } + + if ( !IOUtils.canReadFile( ( String ) obj, errorHolder ) ) + { + errorMessage = "Problem with Omnipose python executable: " + errorHolder.toString(); + return false; + } + } + + return ok; + } + + @Override + public String getInfoText() + { + return INFO_TEXT; + } + + @Override + public String getKey() + { + return DETECTOR_KEY; + } + + @Override + public String getName() + { + return NAME; + } + + @Override + public SpotDetectorFactoryBase< T > copy() + { + return new OmniposeDetectorFactory<>(); + } +} diff --git a/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeSettings.java b/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeSettings.java new file mode 100644 index 00000000..068e3185 --- /dev/null +++ b/src/main/java/fiji/plugin/trackmate/omnipose/OmniposeSettings.java @@ -0,0 +1,175 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2021 - 2023 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package fiji.plugin.trackmate.omnipose; + +import fiji.plugin.trackmate.cellpose.AbstractCellposeSettings; + +public class OmniposeSettings extends AbstractCellposeSettings +{ + + public OmniposeSettings( + final String omniposePythonPath, + final PretrainedModelOmnipose model, + final String customModelPath, + final int chan, + final int chan2, + final double diameter, + final boolean useGPU, + final boolean simplifyContours ) + { + super( omniposePythonPath, model, customModelPath, chan, chan2, diameter, useGPU, simplifyContours ); + } + + @Override + public String getExecutableName() + { + return "omnipose"; + } + + public static Builder create() + { + return new Builder(); + } + + public static final class Builder + { + + private String omniposePythonPath = "/opt/anaconda3/envs/omnipose/bin/python"; + + private int chan = 0; + + private int chan2 = -1; + + private PretrainedModelOmnipose model = PretrainedModelOmnipose.BACT_PHASE; + + private double diameter = 30.; + + private boolean useGPU = true; + + private boolean simplifyContours = true; + + private String customModelPath = ""; + + public Builder channel1( final int ch ) + { + this.chan = ch; + return this; + } + + public Builder channel2( final int ch ) + { + this.chan2 = ch; + return this; + } + + public Builder omniposePythonPath( final String omniposePythonPath ) + { + this.omniposePythonPath = omniposePythonPath; + return this; + } + + public Builder model( final PretrainedModelOmnipose model ) + { + this.model = model; + return this; + } + + public Builder diameter( final double diameter ) + { + this.diameter = diameter; + return this; + } + + public Builder useGPU( final boolean useGPU ) + { + this.useGPU = useGPU; + return this; + } + + public Builder simplifyContours( final boolean simplifyContours ) + { + this.simplifyContours = simplifyContours; + return this; + } + + public Builder customModel( final String customModelPath ) + { + this.customModelPath = customModelPath; + return this; + } + + public OmniposeSettings get() + { + return new OmniposeSettings( + omniposePythonPath, + model, + customModelPath, + chan, + chan2, + diameter, + useGPU, + simplifyContours ); + } + + } + + public enum PretrainedModelOmnipose implements PretrainedModel + { + BACT_PHASE( "Bacteria phase contrast", "bact_phase_omni", false ), + BACT_FLUO( "Bacteria fluorescence", "bact_fluor_omni", false ), + CUSTOM( "Custom", "", true ); + + private final String name; + + private final String path; + + private final boolean isCustom; + + PretrainedModelOmnipose( final String name, final String path, final boolean isCustom ) + { + this.name = name; + this.path = path; + this.isCustom = isCustom; + } + + @Override + public String toString() + { + return name; + } + + @Override + public boolean isCustom() + { + return isCustom; + } + + @Override + public String getPath() + { + return path; + } + + } + + public static final OmniposeSettings DEFAULT = new Builder().get(); +} diff --git a/src/main/resources/images/omniposelogo.png b/src/main/resources/images/omniposelogo.png new file mode 100644 index 00000000..231cb06e Binary files /dev/null and b/src/main/resources/images/omniposelogo.png differ diff --git a/src/test/java/fiji/plugin/trackmate/CellPoseAttempt.java b/src/test/java/fiji/plugin/trackmate/CellPoseAttempt.java index c4a82798..916e6745 100644 --- a/src/test/java/fiji/plugin/trackmate/CellPoseAttempt.java +++ b/src/test/java/fiji/plugin/trackmate/CellPoseAttempt.java @@ -21,9 +21,6 @@ */ package fiji.plugin.trackmate; -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; - import ij.ImageJ; /** @@ -32,12 +29,10 @@ public class CellPoseAttempt { - public static void main( final String[] args ) throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException + public static void main( final String[] args ) { - UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); ImageJ.main( args ); - -// new TrackMatePlugIn().run( "samples/R2_multiC.tif" ); - new TrackMatePlugIn().run( "D:/Projects/JYTinevez/TrackMate-StarDist/CTCMetrics/Brightfield/01.tif" ); + new TrackMatePlugIn().run( "samples/R2_multiC.tif" ); +// new TrackMatePlugIn().run( "D:/Projects/JYTinevez/TrackMate-StarDist/CTCMetrics/Brightfield/01.tif" ); } } diff --git a/src/test/java/fiji/plugin/trackmate/CellPoseAttempt2.java b/src/test/java/fiji/plugin/trackmate/CellPoseAttempt2.java index e17ca3f0..773caec8 100644 --- a/src/test/java/fiji/plugin/trackmate/CellPoseAttempt2.java +++ b/src/test/java/fiji/plugin/trackmate/CellPoseAttempt2.java @@ -23,6 +23,7 @@ import java.io.IOException; +import fiji.plugin.trackmate.cellpose.AbstractCellposeSettings; import fiji.plugin.trackmate.cellpose.CellposeDetector; import fiji.plugin.trackmate.cellpose.CellposeSettings; import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; @@ -48,7 +49,7 @@ public static void main( final String[] args ) throws IOException, InterruptedEx imp.show(); // Cellpose command line options. - final CellposeSettings cp = CellposeSettings.DEFAULT; + final AbstractCellposeSettings cp = CellposeSettings.DEFAULT; final ImgPlus img = TMUtils.rawWraps( imp ); final CellposeDetector detector = new CellposeDetector( img, img, cp, Logger.DEFAULT_LOGGER ); diff --git a/src/test/java/fiji/plugin/trackmate/LoadCellposeResultsDemo.java b/src/test/java/fiji/plugin/trackmate/LoadCellposeResultsDemo.java new file mode 100644 index 00000000..6fd0af03 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/LoadCellposeResultsDemo.java @@ -0,0 +1,13 @@ +package fiji.plugin.trackmate; + +import ij.ImageJ; + +public class LoadCellposeResultsDemo +{ + + public static void main( final String[] args ) + { + ImageJ.main( args ); + new LoadTrackMatePlugIn().run( "" ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/OmniPoseAttempt.java b/src/test/java/fiji/plugin/trackmate/OmniPoseAttempt.java new file mode 100644 index 00000000..ec5ace33 --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/OmniPoseAttempt.java @@ -0,0 +1,37 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2021 - 2023 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package fiji.plugin.trackmate; + +import ij.ImageJ; + +/** + * Inspired by the BIOP approach. + */ +public class OmniPoseAttempt +{ + + public static void main( final String[] args ) + { + ImageJ.main( args ); + new TrackMatePlugIn().run( "samples/20230331_washed_XY1.ome-1_stabilized_cropped.tif" ); + } +} diff --git a/src/test/java/fiji/plugin/trackmate/OmniPoseAttempt2.java b/src/test/java/fiji/plugin/trackmate/OmniPoseAttempt2.java new file mode 100644 index 00000000..ba0c789a --- /dev/null +++ b/src/test/java/fiji/plugin/trackmate/OmniPoseAttempt2.java @@ -0,0 +1,79 @@ +/*- + * #%L + * TrackMate: your buddy for everyday tracking. + * %% + * Copyright (C) 2021 - 2023 TrackMate developers. + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ +package fiji.plugin.trackmate; + +import java.io.IOException; + +import fiji.plugin.trackmate.cellpose.CellposeDetector; +import fiji.plugin.trackmate.gui.displaysettings.DisplaySettingsIO; +import fiji.plugin.trackmate.omnipose.OmniposeSettings; +import fiji.plugin.trackmate.util.TMUtils; +import fiji.plugin.trackmate.visualization.hyperstack.HyperStackDisplayer; +import ij.IJ; +import ij.ImageJ; +import ij.ImagePlus; +import net.imagej.ImgPlus; + +/** + * Inspired by the BIOP approach. + */ +public class OmniPoseAttempt2 +{ + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + public static void main( final String[] args ) throws IOException, InterruptedException + { + ImageJ.main( args ); + + final ImagePlus imp = IJ.openImage( "samples/P31-crop-2.tif" ); + imp.show(); + + // Omnipose command line options. + final OmniposeSettings cp = OmniposeSettings.DEFAULT; + + final ImgPlus img = TMUtils.rawWraps( imp ); + final CellposeDetector detector = new CellposeDetector( img, img, cp, Logger.DEFAULT_LOGGER ); + if ( !detector.checkInput() ) + { + System.err.println( detector.getErrorMessage() ); + return; + } + + if ( !detector.process() ) + { + System.err.println( detector.getErrorMessage() ); + return; + } + + System.out.println( String.format( "Done in %.1f s.", detector.getProcessingTime() / 1000. ) ); + final SpotCollection spots = detector.getResult(); + spots.setVisible( true ); + System.out.println( spots ); + + final Model model = new Model(); + model.setSpots( spots, false ); + final SelectionModel selectionModel = new SelectionModel( model ); + + final HyperStackDisplayer displayer = new HyperStackDisplayer( model, selectionModel, imp, DisplaySettingsIO.readUserDefault() ); + displayer.render(); + } +}