diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java index 9811dd32dd2cfe2bbb4f8a15dbf0c4a8ca3d2205..521dd7a22893285a6474b01b0d54e3d0edeb2be0 100644 --- a/src/main/java/bdv/BigDataViewer.java +++ b/src/main/java/bdv/BigDataViewer.java @@ -42,6 +42,7 @@ import bdv.spimdata.WrapBasicImgLoader; import bdv.spimdata.XmlIoSpimDataMinimal; import bdv.tools.HelpDialog; import bdv.tools.InitializeViewerState; +import bdv.tools.RecordMaxProjectionDialog; import bdv.tools.RecordMovieDialog; import bdv.tools.VisibilityAndGroupingDialog; import bdv.tools.bookmarks.Bookmarks; @@ -79,6 +80,8 @@ public class BigDataViewer protected final RecordMovieDialog movieDialog; + protected final RecordMaxProjectionDialog movieMaxProjectDialog; + protected final VisibilityAndGroupingDialog activeSourcesDialog; protected final HelpDialog helpDialog; @@ -339,6 +342,10 @@ public class BigDataViewer // this is just to get updates of window size: viewer.getDisplay().addOverlayRenderer( movieDialog ); + movieMaxProjectDialog = new RecordMaxProjectionDialog( viewerFrame, viewer, progressWriter ); + // this is just to get updates of window size: + viewer.getDisplay().addOverlayRenderer( movieMaxProjectDialog ); + activeSourcesDialog = new VisibilityAndGroupingDialog( viewerFrame, viewer.getVisibilityAndGrouping() ); helpDialog = new HelpDialog( viewerFrame ); @@ -413,6 +420,10 @@ public class BigDataViewer miMovie.setText( "Record Movie" ); menu.add( miMovie ); + final JMenuItem miMaxProjectMovie = new JMenuItem( actionMap.get( BigDataViewerActions.RECORD_MAX_PROJECTION_MOVIE ) ); + miMaxProjectMovie.setText( "Record Max-Projection Movie" ); + menu.add( miMaxProjectMovie ); + final JMenuItem miManualTransform = new JMenuItem( actionMap.get( BigDataViewerActions.MANUAL_TRANSFORM ) ); miManualTransform.setText( "Manual Transform" ); menu.add( miManualTransform ); diff --git a/src/main/java/bdv/BigDataViewerActions.java b/src/main/java/bdv/BigDataViewerActions.java index f96de3513faf1000b4b0abb040c947296cb7f71f..d7e27011406b40749771ea5c4c2f98ea58e0c3b8 100644 --- a/src/main/java/bdv/BigDataViewerActions.java +++ b/src/main/java/bdv/BigDataViewerActions.java @@ -22,6 +22,7 @@ public class BigDataViewerActions public static final String SAVE_SETTINGS = "save settings"; public static final String LOAD_SETTINGS = "load settings"; public static final String RECORD_MOVIE = "record movie"; + public static final String RECORD_MAX_PROJECTION_MOVIE = "record max projection movie"; public static final String SET_BOOKMARK = "set bookmark"; public static final String GO_TO_BOOKMARK = "go to bookmark"; public static final String GO_TO_BOOKMARK_ROTATION = "go to bookmark rotation"; @@ -55,6 +56,7 @@ public class BigDataViewerActions map.put( VISIBILITY_AND_GROUPING, "F6" ); map.put( MANUAL_TRANSFORM, "T" ); map.put( SHOW_HELP, "F1", "H" ); + map.put( RECORD_MAX_PROJECTION_MOVIE, "F8" ); map.put( CROP, "F9" ); map.put( RECORD_MOVIE, "F10" ); map.put( SAVE_SETTINGS, "F11" ); @@ -74,6 +76,7 @@ public class BigDataViewerActions map.put( new ToggleDialogAction( BRIGHTNESS_SETTINGS, bdv.brightnessDialog ) ); map.put( new ToggleDialogAction( VISIBILITY_AND_GROUPING, bdv.activeSourcesDialog ) ); map.put( new ToggleDialogAction( CROP, bdv.cropDialog ) ); + map.put( new ToggleDialogAction( RECORD_MAX_PROJECTION_MOVIE, bdv.movieMaxProjectDialog ) ); map.put( new ToggleDialogAction( RECORD_MOVIE, bdv.movieDialog ) ); map.put( new ToggleDialogAction( SHOW_HELP, bdv.helpDialog ) ); map.put( new ManualTransformAction( bdv ) ); diff --git a/src/main/java/bdv/tools/RecordMaxProjectionDialog.java b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..77aa574f4f69ed375887ec5792b6f639a9fedcf2 --- /dev/null +++ b/src/main/java/bdv/tools/RecordMaxProjectionDialog.java @@ -0,0 +1,378 @@ +package bdv.tools; + +import java.awt.BorderLayout; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ActionMap; +import javax.swing.BoxLayout; +import javax.swing.InputMap; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SpinnerNumberModel; +import javax.swing.WindowConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.imglib2.Cursor; +import net.imglib2.display.screenimage.awt.ARGBScreenImage; +import net.imglib2.img.Img; +import net.imglib2.img.array.ArrayImgs; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.ARGBType; +import net.imglib2.ui.OverlayRenderer; +import net.imglib2.ui.PainterThread; +import net.imglib2.ui.RenderTarget; +import net.imglib2.util.LinAlgHelpers; +import bdv.export.ProgressWriter; +import bdv.img.cache.Cache; +import bdv.util.Prefs; +import bdv.viewer.ViewerPanel; +import bdv.viewer.overlay.ScaleBarOverlayRenderer; +import bdv.viewer.render.MultiResolutionRenderer; +import bdv.viewer.state.ViewerState; + +public class RecordMaxProjectionDialog extends JDialog implements OverlayRenderer +{ + private static final long serialVersionUID = 1L; + + private final ViewerPanel viewer; + + private final int maxTimepoint; + + private final ProgressWriter progressWriter; + + private final JTextField pathTextField; + + private final JSpinner spinnerMinTimepoint; + + private final JSpinner spinnerMaxTimepoint; + + private final JSpinner spinnerWidth; + + private final JSpinner spinnerHeight; + + private final JSpinner spinnerStepSize; + + private final JSpinner spinnerNumSteps; + + public RecordMaxProjectionDialog( final Frame owner, final ViewerPanel viewer, final ProgressWriter progressWriter ) + { + super( owner, "record max projection movie", false ); + this.viewer = viewer; + maxTimepoint = viewer.getState().getNumTimePoints() - 1; + this.progressWriter = progressWriter; + + final JPanel boxes = new JPanel(); + getContentPane().add( boxes, BorderLayout.NORTH ); + boxes.setLayout( new BoxLayout( boxes, BoxLayout.PAGE_AXIS ) ); + + final JPanel saveAsPanel = new JPanel(); + saveAsPanel.setLayout( new BorderLayout( 0, 0 ) ); + boxes.add( saveAsPanel ); + + saveAsPanel.add( new JLabel( "save to" ), BorderLayout.WEST ); + + pathTextField = new JTextField( "./record/" ); + saveAsPanel.add( pathTextField, BorderLayout.CENTER ); + pathTextField.setColumns( 20 ); + + final JButton browseButton = new JButton( "Browse" ); + saveAsPanel.add( browseButton, BorderLayout.EAST ); + + final JPanel timepointsPanel = new JPanel(); + boxes.add( timepointsPanel ); + + timepointsPanel.add( new JLabel( "timepoints from" ) ); + + spinnerMinTimepoint = new JSpinner(); + spinnerMinTimepoint.setModel( new SpinnerNumberModel( 0, 0, maxTimepoint, 1 ) ); + timepointsPanel.add( spinnerMinTimepoint ); + + timepointsPanel.add( new JLabel( "to" ) ); + + spinnerMaxTimepoint = new JSpinner(); + spinnerMaxTimepoint.setModel( new SpinnerNumberModel( maxTimepoint, 0, maxTimepoint, 1 ) ); + timepointsPanel.add( spinnerMaxTimepoint ); + + final JPanel widthPanel = new JPanel(); + boxes.add( widthPanel ); + widthPanel.add( new JLabel( "width" ) ); + spinnerWidth = new JSpinner(); + spinnerWidth.setModel( new SpinnerNumberModel( 800, 10, 5000, 1 ) ); + widthPanel.add( spinnerWidth ); + + final JPanel heightPanel = new JPanel(); + boxes.add( heightPanel ); + heightPanel.add( new JLabel( "height" ) ); + spinnerHeight = new JSpinner(); + spinnerHeight.setModel( new SpinnerNumberModel( 600, 10, 5000, 1 ) ); + heightPanel.add( spinnerHeight ); + + final JPanel stepSizePanel = new JPanel(); + boxes.add( stepSizePanel ); + stepSizePanel.add( new JLabel( "slice step size" ) ); + spinnerStepSize = new JSpinner(); + spinnerStepSize.setModel( new SpinnerNumberModel( 1, 0.001, 20, 0.1 ) ); + stepSizePanel.add( spinnerStepSize ); + + final JPanel numStepsPanel = new JPanel(); + boxes.add( numStepsPanel ); + numStepsPanel.add( new JLabel( "number of slices" ) ); + spinnerNumSteps = new JSpinner(); + spinnerNumSteps.setModel( new SpinnerNumberModel( 10, 1, 10000, 1 ) ); + numStepsPanel.add( spinnerNumSteps ); + + final JPanel buttonsPanel = new JPanel(); + boxes.add( buttonsPanel ); + buttonsPanel.setLayout(new BorderLayout(0, 0)); + + final JButton recordButton = new JButton( "Record" ); + buttonsPanel.add( recordButton, BorderLayout.EAST ); + + spinnerMinTimepoint.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( final ChangeEvent e ) + { + final int min = ( Integer ) spinnerMinTimepoint.getValue(); + final int max = ( Integer ) spinnerMaxTimepoint.getValue(); + if ( max < min ) + spinnerMaxTimepoint.setValue( min ); + } + } ); + + spinnerMaxTimepoint.addChangeListener( new ChangeListener() + { + @Override + public void stateChanged( final ChangeEvent e ) + { + final int min = ( Integer ) spinnerMinTimepoint.getValue(); + final int max = ( Integer ) spinnerMaxTimepoint.getValue(); + if (min > max) + spinnerMinTimepoint.setValue( max ); + } + } ); + + final JFileChooser fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled( false ); + fileChooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY ); + + browseButton.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( final ActionEvent e ) + { + fileChooser.setSelectedFile( new File( pathTextField.getText() ) ); + final int returnVal = fileChooser.showSaveDialog( null ); + if ( returnVal == JFileChooser.APPROVE_OPTION ) + { + final File file = fileChooser.getSelectedFile(); + pathTextField.setText( file.getAbsolutePath() ); + } + } + } ); + + recordButton.addActionListener( new ActionListener() + { + @Override + public void actionPerformed( final ActionEvent e ) + { + final String dirname = pathTextField.getText(); + final File dir = new File( dirname ); + if ( !dir.exists() ) + dir.mkdirs(); + if ( !dir.exists() || !dir.isDirectory() ) + { + System.err.println( "Invalid export directory " + dirname ); + return; + } + final int minTimepointIndex = ( Integer ) spinnerMinTimepoint.getValue(); + final int maxTimepointIndex = ( Integer ) spinnerMaxTimepoint.getValue(); + final int width = ( Integer ) spinnerWidth.getValue(); + final int height = ( Integer ) spinnerHeight.getValue(); + final double stepSize = ( Double ) spinnerStepSize.getValue(); + final int numSteps = ( Integer ) spinnerNumSteps.getValue(); + new Thread() + { + @Override + public void run() + { + try + { + recordButton.setEnabled( false ); + recordMovie( width, height, minTimepointIndex, maxTimepointIndex, stepSize, numSteps, dir ); + recordButton.setEnabled( true ); + } + catch ( final Exception ex ) + { + ex.printStackTrace(); + } + } + }.start(); + } + } ); + + final ActionMap am = getRootPane().getActionMap(); + final InputMap im = getRootPane().getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + final Object hideKey = new Object(); + final Action hideAction = new AbstractAction() + { + @Override + public void actionPerformed( final ActionEvent e ) + { + setVisible( false ); + } + + private static final long serialVersionUID = 1L; + }; + im.put( KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 ), hideKey ); + am.put( hideKey, hideAction ); + + pack(); + setDefaultCloseOperation( WindowConstants.HIDE_ON_CLOSE ); + } + + /** + * @param stepSize in multiples of width of a source voxel. + */ + public void recordMovie( final int width, final int height, final int minTimepointIndex, final int maxTimepointIndex, final double stepSize, final int numSteps, final File dir ) throws IOException + { + final ViewerState renderState = viewer.getState(); + final int canvasW = viewer.getDisplay().getWidth(); + final int canvasH = viewer.getDisplay().getHeight(); + + final AffineTransform3D tGV = new AffineTransform3D(); + renderState.getViewerTransform( tGV ); + tGV.set( tGV.get( 0, 3 ) - canvasW / 2, 0, 3 ); + tGV.set( tGV.get( 1, 3 ) - canvasH / 2, 1, 3 ); + tGV.scale( ( double ) width / canvasW ); + tGV.set( tGV.get( 0, 3 ) + width / 2, 0, 3 ); + tGV.set( tGV.get( 1, 3 ) + height / 2, 1, 3 ); + + final AffineTransform3D affine = new AffineTransform3D(); + + // get voxel width transformed to current viewer coordinates + final AffineTransform3D tSV = new AffineTransform3D(); + renderState.getSources().get( 0 ).getSpimSource().getSourceTransform( 0, 0, tSV ); + tSV.preConcatenate( tGV ); + final double[] sO = new double[] { 0, 0, 0 }; + final double[] sX = new double[] { 1, 0, 0 }; + final double[] vO = new double[ 3 ]; + final double[] vX = new double[ 3 ]; + tSV.apply( sO, vO ); + tSV.apply( sX, vX ); + LinAlgHelpers.subtract( vO, vX, vO ); + final double dd = LinAlgHelpers.length( vO ); + + final ScaleBarOverlayRenderer scalebar = Prefs.showScaleBarInMovie() ? new ScaleBarOverlayRenderer() : null; + + class MyTarget implements RenderTarget + { + final ARGBScreenImage accumulated; + + public MyTarget() + { + accumulated = new ARGBScreenImage( width, height ); + } + + public void clear() + { + for ( final ARGBType acc : accumulated ) + acc.setZero(); + } + + @Override + public BufferedImage setBufferedImage( final BufferedImage bufferedImage ) + { + final Img< ARGBType > argbs = ArrayImgs.argbs( ( ( DataBufferInt ) bufferedImage.getData().getDataBuffer() ).getData(), width, height ); + final Cursor< ARGBType > c = argbs.cursor(); + for ( final ARGBType acc : accumulated ) + { + final int current = acc.get(); + final int in = c.next().get(); + acc.set( ARGBType.rgba( + Math.max( ARGBType.red( in ), ARGBType.red( current ) ), + Math.max( ARGBType.green( in ), ARGBType.green( current ) ), + Math.max( ARGBType.blue( in ), ARGBType.blue( current ) ), + Math.max( ARGBType.alpha( in ), ARGBType.alpha( current ) ) ) ); + } + return null; + } + + @Override + public final int getWidth() + { + return width; + } + + @Override + public int getHeight() + { + return height; + } + } + final MyTarget target = new MyTarget(); + final MultiResolutionRenderer renderer = new MultiResolutionRenderer( target, new PainterThread( null ), new double[] { 1 }, 0, false, 1, null, false, new Cache.Dummy() ); + progressWriter.setProgress( 0 ); + for ( int timepoint = minTimepointIndex; timepoint <= maxTimepointIndex; ++timepoint ) + { + target.clear(); + renderState.setCurrentTimepoint( timepoint ); + + for ( int step = 0; step < numSteps; ++step ) + { + affine.set( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, -dd * stepSize * step ); + affine.concatenate( tGV ); + renderState.setViewerTransform( affine ); + renderer.requestRepaint(); + renderer.paint( renderState ); + } + + final BufferedImage bi = target.accumulated.image(); + + if ( Prefs.showScaleBarInMovie() ) + { + final Graphics2D g2 = bi.createGraphics(); + g2.setClip( 0, 0, width, height ); + scalebar.setViewerState( renderState ); + scalebar.paint( g2 ); + } + + ImageIO.write( bi, "png", new File( String.format( "%s/img-%03d.png", dir, timepoint ) ) ); + progressWriter.setProgress( ( double ) (timepoint - minTimepointIndex + 1) / (maxTimepointIndex - minTimepointIndex + 1) ); + } + } + + @Override + public void drawOverlays( final Graphics g ) + {} + + @Override + public void setCanvasSize( final int width, final int height ) + { + spinnerWidth.setValue( width ); + spinnerHeight.setValue( height ); + } +}