diff --git a/pom.xml b/pom.xml index 6698a4a4..e777f2dd 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ sc.fiji bigdataviewer-core - 10.4.6-SNAPSHOT + 10.4.7-SNAPSHOT BigDataViewer Core BigDataViewer core classes with minimal dependencies. diff --git a/src/main/java/bdv/BigDataViewer.java b/src/main/java/bdv/BigDataViewer.java index 7e05d08e..f291f2b4 100644 --- a/src/main/java/bdv/BigDataViewer.java +++ b/src/main/java/bdv/BigDataViewer.java @@ -29,6 +29,7 @@ package bdv; import bdv.tools.PreferencesDialog; +import bdv.tools.movie.ProduceMovieDialog; import bdv.ui.UIUtils; import bdv.ui.keymap.Keymap; import bdv.ui.keymap.KeymapManager; @@ -127,6 +128,8 @@ public class BigDataViewer protected final RecordMovieDialog movieDialog; + protected final ProduceMovieDialog produceMovieDialog; + protected final RecordMaxProjectionDialog movieMaxProjectDialog; protected final VisibilityAndGroupingDialog activeSourcesDialog; @@ -398,6 +401,7 @@ public BigDataViewer( cropDialog = ( spimData == null ) ? null : new CropDialog( viewerFrame, viewer, spimData.getSequenceDescription() ); movieDialog = new RecordMovieDialog( viewerFrame, viewer, progressWriter ); + produceMovieDialog = new ProduceMovieDialog(viewerFrame,viewer,progressWriter); // this is just to get updates of window size: viewer.getDisplay().overlays().add( movieDialog ); @@ -502,6 +506,10 @@ public boolean accept( final File f ) miMovie.setText( "Record Movie" ); menu.add( miMovie ); + final JMenuItem miMovieProducer = new JMenuItem(actionMap.get(BigDataViewerActions.PRODUCE_MOVIE)); + miMovieProducer.setText("Produce Movie"); + menu.add(miMovieProducer); + final JMenuItem miMaxProjectMovie = new JMenuItem( actionMap.get( BigDataViewerActions.RECORD_MAX_PROJECTION_MOVIE ) ); miMaxProjectMovie.setText( "Record Max-Projection Movie" ); menu.add( miMaxProjectMovie ); @@ -775,7 +783,7 @@ public void collapseCardPanel() public static void main( final String[] args ) { - final String fn = "/Users/pietzsch/workspace/data/111010_weber_resave.xml"; + final String fn = "/Users/zouinkhim/Desktop/unfinishedProjects/grid-3d-stitched-h5/dataset.xml"; try { System.setProperty( "apple.laf.useScreenMenuBar", "true" ); diff --git a/src/main/java/bdv/BigDataViewerActions.java b/src/main/java/bdv/BigDataViewerActions.java index 9760aa21..20bc92bf 100644 --- a/src/main/java/bdv/BigDataViewerActions.java +++ b/src/main/java/bdv/BigDataViewerActions.java @@ -61,6 +61,7 @@ public class BigDataViewerActions extends Actions public static final String LOAD_SETTINGS = "load settings"; public static final String EXPAND_CARDS = "expand and focus cards panel"; public static final String COLLAPSE_CARDS = "collapse cards panel"; + public static final String PRODUCE_MOVIE = "produce movie"; 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"; @@ -78,6 +79,7 @@ public class BigDataViewerActions extends Actions public static final String[] EXPAND_CARDS_KEYS = new String[] { "P" }; public static final String[] COLLAPSE_CARDS_KEYS = new String[] { "shift P", "shift ESCAPE" }; public static final String[] RECORD_MOVIE_KEYS = new String[] { "F10" }; + public static final String[] PRODUCE_MOVIE_KEYS = new String[] { "F7" }; public static final String[] RECORD_MAX_PROJECTION_MOVIE_KEYS = new String[] { "F8" }; public static final String[] SET_BOOKMARK_KEYS = new String[] { "shift B" }; public static final String[] GO_TO_BOOKMARK_KEYS = new String[] { "B" }; @@ -108,6 +110,7 @@ public void getCommandDescriptions( final CommandDescriptions descriptions ) descriptions.add( EXPAND_CARDS, EXPAND_CARDS_KEYS, "Expand and focus the BigDataViewer card panel" ); descriptions.add( COLLAPSE_CARDS, COLLAPSE_CARDS_KEYS, "Collapse the BigDataViewer card panel" ); descriptions.add( RECORD_MOVIE, RECORD_MOVIE_KEYS, "Show the Record Movie dialog." ); + descriptions.add( PRODUCE_MOVIE, PRODUCE_MOVIE_KEYS, "Show the Produce Movie dialog." ); descriptions.add( RECORD_MAX_PROJECTION_MOVIE, RECORD_MAX_PROJECTION_MOVIE_KEYS, "Show the Record Max Projection Movie dialog." ); descriptions.add( SET_BOOKMARK, SET_BOOKMARK_KEYS, "Set a labeled bookmark at the current location." ); descriptions.add( GO_TO_BOOKMARK, GO_TO_BOOKMARK_KEYS, "Retrieve a labeled bookmark location." ); @@ -132,6 +135,7 @@ public static void install( final Actions actions, final BigDataViewer bdv ) toggleDialogAction( actions, bdv.helpDialog, SHOW_HELP, SHOW_HELP_KEYS ); toggleDialogAction( actions, bdv.cropDialog, CROP, CROP_KEYS ); toggleDialogAction( actions, bdv.movieDialog, RECORD_MOVIE, RECORD_MOVIE_KEYS ); + toggleDialogAction( actions, bdv.produceMovieDialog, PRODUCE_MOVIE, PRODUCE_MOVIE_KEYS ); toggleDialogAction( actions, bdv.movieMaxProjectDialog, RECORD_MAX_PROJECTION_MOVIE, RECORD_MAX_PROJECTION_MOVIE_KEYS ); toggleDialogAction( actions, bdv.preferencesDialog, PREFERENCES_DIALOG, PREFERENCES_DIALOG_KEYS ); bookmarks( actions, bdv.bookmarkEditor ); diff --git a/src/main/java/bdv/tools/movie/MovieProducer.java b/src/main/java/bdv/tools/movie/MovieProducer.java new file mode 100644 index 00000000..f0f233c7 --- /dev/null +++ b/src/main/java/bdv/tools/movie/MovieProducer.java @@ -0,0 +1,207 @@ + +/** + * License: GPL + *

+ * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License 2 + * as published by the Free Software Foundation. + *

+ * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package bdv.tools.movie; + +import bdv.cache.CacheControl; +import bdv.export.ProgressWriter; +import bdv.viewer.ViewerPanel; +import bdv.viewer.ViewerState; +import bdv.viewer.animate.SimilarityTransformAnimator; +import bdv.viewer.overlay.MultiBoxOverlayRenderer; +import bdv.viewer.overlay.ScaleBarOverlayRenderer; +import bdv.viewer.render.MultiResolutionRenderer; +import bdv.viewer.render.PainterThread; +import bdv.viewer.render.RenderTarget; +import bdv.viewer.render.awt.BufferedImageRenderResult; +import net.imglib2.realtransform.AffineTransform3D; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +/** + * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> + * Code was copied from org.janelia.saalfeldlab.hotknife.VNCMovie; + * Modified and adapted by Marwan Zouinkhi + */ + +public class MovieProducer { + + public static class Target implements RenderTarget { + + public BufferedImageRenderResult renderResult = new BufferedImageRenderResult(); + + private final int width; + private final int height; + + public Target(final int width, final int height) { + this.width = width; + this.height = height; + } + + @Override + public BufferedImageRenderResult getReusableRenderResult() { + return renderResult; + } + + @Override + public BufferedImageRenderResult createRenderResult() { + return new BufferedImageRenderResult(); + } + + @Override + public void setRenderResult(final BufferedImageRenderResult renderResult) { + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + } + + /** + * Cosine shape of linear [0,1] + */ + protected static double cos(final double x) { + + return 0.5 - 0.5 * Math.cos(Math.PI * x); + } + + /** + * Acceleration function for a t in [0,1]: + *

+ * types + * 0 symmetric + * 1 slow start + * 2 slow end + * 3 soft symmetric + * 4 soft slow start + * 5 soft slow end + * t = current frame / nb frames + */ + public static double accel(final double t, final int type) { + + switch (type) { + case 1: // slow start + return cos(t * t); + case 2: // slow end + return 1.0 - cos(Math.pow(1.0 - t, 2)); + case 3: // soft symmetric + return cos(cos(t)); + case 4: // soft slow start + return cos(cos(t * t)); + case 5: // soft slow end + return 1.0 - cos(cos(Math.pow(1.0 - t, 2))); + default: // symmetric + return cos(t); + } + } + + public static void recordMovie( + final ViewerPanel viewer, + final AffineTransform3D[] transforms, + final int[] frames, + final int[] accel, + int width, + int height, + final String dir, + ProgressWriter progressWriter) throws IOException { + + final ViewerState renderState = viewer.state(); + final ScaleBarOverlayRenderer scalebar = new ScaleBarOverlayRenderer(); + + int screenWidth = viewer.getDisplayComponent().getWidth(); + int screenHeight = viewer.getDisplayComponent().getHeight(); + double ratio = Math.min(width * 1.0 / screenWidth, height * 1.0 / screenHeight); + + final AffineTransform3D viewerScale = new AffineTransform3D(); + + viewerScale.set( + ratio, 0, 0, 0, + 0, ratio, 0, 0, + 0, 0, 1.0, 0); + + final MultiBoxOverlayRenderer box = new MultiBoxOverlayRenderer(width, height); + + final Target target = new Target(width, height); + + final MultiResolutionRenderer renderer = new MultiResolutionRenderer( + target, + new PainterThread(null), + new double[]{1.0}, + 0l, + 12, + null, + false, + viewer.getOptionValues().getAccumulateProjectorFactory(), + new CacheControl.Dummy()); + + int i = 0; + + for (int k = 1; k < transforms.length; ++k) { + progressWriter.setProgress((k*1.0/transforms.length)); + final SimilarityTransformAnimator animator = new SimilarityTransformAnimator( + transforms[k - 1], + transforms[k], + 0, + 0, + 0); + + for (int d = 0; d < frames[k]; ++d) { + final AffineTransform3D tkd = animator.get(accel((double) d / (double) frames[k], accel[k])); + tkd.preConcatenate(viewerScale); + viewer.state().setViewerTransform(tkd); + renderState.setViewerTransform(tkd); + renderer.requestRepaint(); + try { + renderer.paint(renderState); + } catch (final Exception e) { + e.printStackTrace(); + return; + } + + final BufferedImage bi = target.renderResult.getBufferedImage(); + + final Graphics2D g2 = bi.createGraphics(); + g2.drawImage(bi, 0, 0, null); + + /* scalebar */ + g2.setClip(0, 0, width, height); + scalebar.setViewerState(renderState); + scalebar.paint(g2); + box.setViewerState(renderState); + box.paint(g2); + + /* save image */ + ImageIO.write(bi, "png", new File(String.format("%s/img-%04d.png", dir, i++))); + + System.out.println(String.format("%s/img-%04d.png", dir, i)); + } + } + } + + + +} \ No newline at end of file diff --git a/src/main/java/bdv/tools/movie/ProduceMovieDialog.java b/src/main/java/bdv/tools/movie/ProduceMovieDialog.java new file mode 100644 index 00000000..ee960b9d --- /dev/null +++ b/src/main/java/bdv/tools/movie/ProduceMovieDialog.java @@ -0,0 +1,351 @@ +/* + * #%L + * BigDataViewer core classes with minimal dependencies. + * %% + * Copyright (C) 2012 - 2022 BigDataViewer developers. + * %% + * 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 bdv.tools.movie; + +import bdv.export.ProgressWriter; +import bdv.tools.movie.MovieProducer; +import bdv.tools.movie.panels.ImagePanel; +import bdv.tools.movie.panels.MovieFramePanel; +import bdv.tools.movie.panels.MovieSaveDialog; +import bdv.tools.movie.preview.MovieFrameInst; +import bdv.tools.movie.preview.PreviewRender; +import bdv.tools.movie.preview.PreviewThread; +import bdv.tools.movie.serilizers.MovieFramesSerializer; +import bdv.util.DelayedPackDialog; +import bdv.viewer.ViewerPanel; +import net.imglib2.realtransform.AffineTransform3D; + +import javax.swing.*; +import javax.swing.border.TitledBorder; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.filechooser.FileSystemView; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ProduceMovieDialog extends DelayedPackDialog { + + private final List framesPanels; + private final ViewerPanel viewer; + + private final ProgressWriter progressWriter; + private final JPanel mainPanel; + private final JButton removeButton; + private final MovieSaveDialog saveDialog; + private final JButton exportJsonButton; + private final JButton exportPNGsButton; + private final static int FrameWidth = 920; + private final static int DEFAULT_SLEEP = 100; + private final static int DEFAULT_DOWN = 10; + private PreviewThread previewThread; + private final JTextField downsampleField; + private final JTextField sleepField; + + public ProduceMovieDialog(final Frame owner, final ViewerPanel viewer, final ProgressWriter progressWriter) { + super(owner, "produce movie", false); + setLayout(new FlowLayout()); + setSize(new Dimension(FrameWidth, 340)); + JPanel playerPanel = new JPanel(); + + downsampleField = new JTextField(String.valueOf(DEFAULT_DOWN)); + downsampleField.setPreferredSize(new Dimension(50, 20)); + sleepField = new JTextField(String.valueOf(DEFAULT_SLEEP)); + sleepField.setPreferredSize(new Dimension(50, 20)); + playerPanel.add(new JLabel("PREVIEW: Downsampling: 1/")); + playerPanel.add(downsampleField); + playerPanel.add(new JLabel(" Sleep:")); + playerPanel.add(sleepField); + playerPanel.add(new JLabel("ms ")); + + JButton playButton = new JButton("▶"); + playButton.addActionListener(e -> startPreview()); + playerPanel.add(playButton); + + JButton pauseButton = new JButton("Stop"); + pauseButton.addActionListener(e -> pausePreview()); + + playerPanel.add(pauseButton); + + playerPanel.setPreferredSize(new Dimension(FrameWidth, 40)); + + JPanel makerPanel = new JPanel(new FlowLayout()); + makerPanel.setPreferredSize(new Dimension(FrameWidth, 280)); + + this.saveDialog = new MovieSaveDialog(owner, viewer, progressWriter, this); + this.viewer = viewer; + this.progressWriter = progressWriter; + framesPanels = new ArrayList<>(); + final JPanel controlPanel = new JPanel(new FlowLayout()); + controlPanel.setPreferredSize(new Dimension(50, 200)); + + JButton addButton = new JButton("+"); + addButton.setPreferredSize(new Dimension(50, 30)); + addButton.addActionListener(e -> addFrame()); + controlPanel.add(addButton); + + removeButton = new JButton("-"); + removeButton.setPreferredSize(new Dimension(50, 30)); + removeButton.addActionListener(e -> removeFrame()); + controlPanel.add(removeButton); + + makerPanel.add(controlPanel); + + this.mainPanel = new JPanel(); + + JScrollPane scrollMain = new JScrollPane(mainPanel); + scrollMain.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + scrollMain.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER); + TitledBorder title = BorderFactory.createTitledBorder("Frames: "); + scrollMain.setBorder(title); + scrollMain.setPreferredSize(new Dimension(750, 240)); + makerPanel.add(scrollMain); + + final JPanel exportPanel = new JPanel(new FlowLayout()); + exportPanel.setPreferredSize(new Dimension(100, 200)); + + this.exportJsonButton = new JButton("Export Json"); + exportJsonButton.addActionListener(e -> exportJson()); + exportPanel.add(exportJsonButton); + + this.exportPNGsButton = new JButton("Generate PNGs"); + exportPNGsButton.addActionListener(e -> showSavePNGsDialog()); + exportPanel.add(exportPNGsButton); + + JButton importButton = new JButton("Import"); + importButton.addActionListener(e -> importSequence()); + exportPanel.add(importButton); + + makerPanel.add(exportPanel); + + add(playerPanel); + add(makerPanel); + + 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); + validateButtons(); + } + + private void pausePreview() { + if (previewThread != null) + previewThread.suspend(); + } + + private void restartPreview() { + int sleep = Integer.valueOf(sleepField.getText()); + int down = Integer.valueOf(downsampleField.getText()); + int size = framesPanels.size(); + final AffineTransform3D[] transforms = new AffineTransform3D[size]; + final int[] frames = new int[size]; + final int[] accel = new int[size]; + + for (int i = 0; i < size; i++) { + MovieFrameInst currentFrame = framesPanels.get(i).updateFields().getMovieFrame(); + transforms[i] = currentFrame.getTransform(); + frames[i] = currentFrame.getFrames(); + accel[i] = currentFrame.getAccel(); + } + +// previewThread = new PreviewRender(viewer, +// transforms, +// frames, +// accel, +// sleep, down); + + previewThread = new PreviewThread(viewer, + transforms, + frames, + accel, + sleep, down); + } + + private void startPreview() { + if (previewThread != null) { + previewThread.suspend(); + } + restartPreview(); + previewThread.start(); + } + + + private void importSequence() { + JFileChooser jfc = new JFileChooser(FileSystemView.getFileSystemView().getHomeDirectory()); + jfc.setDialogTitle("Select a Json"); + jfc.setAcceptAllFileFilterUsed(false); + FileNameExtensionFilter filter = new FileNameExtensionFilter("JSON File", "json"); + jfc.addChoosableFileFilter(filter); + + int returnValue = jfc.showOpenDialog(null); + if (returnValue == JFileChooser.APPROVE_OPTION) { + System.out.println(jfc.getSelectedFile().getPath()); + String path = jfc.getSelectedFile().getPath(); + new Thread(() -> { + try { + removeAllFrames(); + List list = MovieFramesSerializer.getFrom(new File(path)); + for (MovieFrameInst frame : list) { + AffineTransform3D currentTransform = frame.getTransform().copy(); + viewer.state().setViewerTransform(currentTransform); + Thread.sleep(50); + ImagePanel imagePanel = ImagePanel.snapshotOf(viewer); + addFrame(frame, imagePanel); + } + + + } catch (FileNotFoundException | InterruptedException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, e.getMessage(), "ERROR", JOptionPane.ERROR_MESSAGE); + + } + }).run(); + } + } + + private void removeAllFrames() { + while (!framesPanels.isEmpty()) + removeFrame(); + } + + private void exportJson() { + JFileChooser fileChooser = new JFileChooser(); + int returnVal = fileChooser.showSaveDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + String path = fileChooser.getSelectedFile().getAbsolutePath(); + if (!path.endsWith(".json")) + path = path + ".json"; + List list = new ArrayList<>(); + for (MovieFramePanel panels : framesPanels) + list.add(panels.getMovieFrame()); + MovieFramesSerializer.save(list, new File(path)); + + JOptionPane.showMessageDialog(this, "File saved successfully!", "File saved", JOptionPane.PLAIN_MESSAGE); + } + } + + private void showSavePNGsDialog() { + saveDialog.setVisible(true); + } + + private void removeFrame() { + mainPanel.remove(framesPanels.remove(framesPanels.size() - 1)); + validateButtons(); + revalidate(); + repaint(); + } + + private void addFrame(MovieFrameInst movieFrame, ImagePanel imagePanel) { + MovieFramePanel movieFramePanel = new MovieFramePanel(movieFrame, imagePanel); + framesPanels.add(movieFramePanel); + mainPanel.add(movieFramePanel); + validateButtons(); + revalidate(); + repaint(); + } + + private void addFrame() { + AffineTransform3D currentTransform = viewer.state().getViewerTransform(); + MovieFramePanel movieFramePanel = new MovieFramePanel(currentTransform, ImagePanel.snapshotOf(viewer), framesPanels.size()); + framesPanels.add(movieFramePanel); + mainPanel.add(movieFramePanel); + validateButtons(); + revalidate(); + repaint(); + } + + public void exportPNGs(File dir, int width, int height, ProgressWriter progressWriter) { + int size = framesPanels.size(); + new Thread(() -> { + final AffineTransform3D[] transforms = new AffineTransform3D[size]; + final int[] frames = new int[size]; + final int[] accel = new int[size]; + + for (int i = 0; i < size; i++) { + MovieFrameInst currentFrame = framesPanels.get(i).updateFields().getMovieFrame(); + transforms[i] = currentFrame.getTransform(); + frames[i] = currentFrame.getFrames(); + accel[i] = currentFrame.getAccel(); + } + + try { + MovieProducer.recordMovie( + viewer, + transforms, + frames, + accel, + width, + height, + dir.getAbsolutePath(), + progressWriter); + JOptionPane.showMessageDialog(mainPanel, "All Files saved successfully!", "Files saved", JOptionPane.PLAIN_MESSAGE); + } catch (IOException e) { + e.printStackTrace(); + } + }).run(); + } + + private void validateButtons() { + if (framesPanels.size() == 2 && !exportPNGsButton.isEnabled()) { + exportJsonButton.setEnabled(true); + exportPNGsButton.setEnabled(true); + exportJsonButton.revalidate(); + exportPNGsButton.revalidate(); + } + if (framesPanels.size() < 2 && exportPNGsButton.isEnabled()) { + exportJsonButton.setEnabled(false); + exportPNGsButton.setEnabled(false); + exportJsonButton.revalidate(); + exportPNGsButton.revalidate(); + } + if (framesPanels.size() > 0) { + if (!removeButton.isEnabled()) + removeButton.setEnabled(true); + removeButton.revalidate(); + removeButton.repaint(); + } else if (removeButton.isEnabled()) { + removeButton.setEnabled(false); + removeButton.revalidate(); + removeButton.repaint(); + } + } +} diff --git a/src/main/java/bdv/tools/movie/panels/ImageFrame.java b/src/main/java/bdv/tools/movie/panels/ImageFrame.java new file mode 100644 index 00000000..c65f64f8 --- /dev/null +++ b/src/main/java/bdv/tools/movie/panels/ImageFrame.java @@ -0,0 +1,44 @@ +package bdv.tools.movie.panels; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class ImageFrame extends JFrame { + private BufferedImage image; + + public ImageFrame() throws HeadlessException { + super(); + setSize(new Dimension(420, 420)); + } + + @Override + public void paint(Graphics g) { + if (image != null) + g.drawImage(image, 10, 10, this); + else + super.paint(g); + } + + public static BufferedImage resize(BufferedImage img, int newW, int newH) { + Image tmp = img.getScaledInstance(newW, newH, Image.SCALE_SMOOTH); + BufferedImage dimg = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2d = dimg.createGraphics(); + g2d.drawImage(tmp, 0, 0, null); + g2d.dispose(); + + return dimg; + } + + public void setImage(BufferedImage image) { + this.image = resize(image, 400, 400); + if (!isVisible()) + setVisible(true); + SwingUtilities.updateComponentTreeUI(this); + } + + public static void main(String[] args) { + new ImageFrame(); + } +} diff --git a/src/main/java/bdv/tools/movie/panels/ImagePanel.java b/src/main/java/bdv/tools/movie/panels/ImagePanel.java new file mode 100644 index 00000000..3580b0ac --- /dev/null +++ b/src/main/java/bdv/tools/movie/panels/ImagePanel.java @@ -0,0 +1,49 @@ +package bdv.tools.movie.panels; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class ImagePanel extends JPanel { + private final BufferedImage image; + + public ImagePanel(BufferedImage image) { + super(); + setPreferredSize(new Dimension(90, 90)); + this.image = scale(image, 90, 90); + } + + public static ImagePanel snapshotOf(JPanel panel) { + return new ImagePanel(takeSnapShot(panel)); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + g.drawImage(image, 0, 0, this); + } + + public static BufferedImage scale(BufferedImage src, int w, int h) { + BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + int x, y; + int ww = src.getWidth(); + int hh = src.getHeight(); + int[] ys = new int[h]; + for (y = 0; y < h; y++) + ys[y] = y * hh / h; + for (x = 0; x < w; x++) { + int newX = x * ww / w; + for (y = 0; y < h; y++) { + int col = src.getRGB(newX, ys[y]); + img.setRGB(x, y, col); + } + } + return img; + } + + public static BufferedImage takeSnapShot(JPanel panel){ + BufferedImage bufImage = new BufferedImage(panel.getSize().width, panel.getSize().height,BufferedImage.TYPE_INT_RGB); + panel.paint(bufImage.createGraphics()); + return bufImage; + } +} diff --git a/src/main/java/bdv/tools/movie/panels/MovieFramePanel.java b/src/main/java/bdv/tools/movie/panels/MovieFramePanel.java new file mode 100644 index 00000000..ca3441b1 --- /dev/null +++ b/src/main/java/bdv/tools/movie/panels/MovieFramePanel.java @@ -0,0 +1,92 @@ +package bdv.tools.movie.panels; + +import bdv.tools.movie.preview.MovieFrameInst; +import net.imglib2.realtransform.AffineTransform3D; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.TitledBorder; +import java.awt.*; + +public class MovieFramePanel extends JPanel { + final private MovieFrameInst movieFrame; + final private ImagePanel image; + private JTextField framesField; + private JComboBox accelField; + private final String[] ACCELS = new String[]{"symmetric", "slow start", "slow end", "soft symmetric", "soft slow start", "soft slow end"}; + + public MovieFramePanel(AffineTransform3D transform, ImagePanel image, int position) { + super(); + this.movieFrame = new MovieFrameInst(position, transform); + this.image = image; + initView(); + } + + public MovieFramePanel(MovieFrameInst movieFrame, ImagePanel image) { + super(); + this.movieFrame = movieFrame; + this.image = image; + initView(); + } + + private void initView() { + Border blackline = BorderFactory.createLineBorder(Color.lightGray); + TitledBorder title = BorderFactory.createTitledBorder(blackline, String.valueOf(movieFrame.getPosition())); + title.setTitleJustification(TitledBorder.CENTER); + setBorder(title); + setPreferredSize(new Dimension(140, 180)); + if (image != null) + add(image); + JPanel fieldsPanel = new JPanel(new GridLayout(2, 1)); + framesField = new JTextField(String.valueOf(movieFrame.getFrames())); + framesField.setFont(new Font("Serif", Font.PLAIN, 9)); + accelField = new JComboBox<>(ACCELS); + accelField.setSelectedIndex(movieFrame.getAccel()); + accelField.setFont(new Font("Serif", Font.PLAIN, 9)); + JLabel framesLabel = new JLabel("Frames: "); + framesLabel.setFont(new Font("Serif", Font.PLAIN, 8)); + JPanel framePanel = new JPanel(new GridLayout(1, 2)); + framePanel.add(framesLabel); + framePanel.add(framesField); + fieldsPanel.add(framePanel); + fieldsPanel.add(accelField); + add(fieldsPanel); + } + + public ImagePanel getImage() { + return image; + } + + public MovieFramePanel updateFields() { + try { + int accel = accelField.getSelectedIndex(); + movieFrame.setAccel(accel); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Invalid value :" + accelField.getSelectedItem()); + } + try { + int frames = Integer.valueOf(framesField.getText()); + movieFrame.setFrames(frames); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Invalid value :" + framesField.getText()); + } + return this; + } + + public int getAccel() { + updateFields(); + return movieFrame.getAccel(); + } + + public int getFrames() { + updateFields(); + return movieFrame.getFrames(); + } + + public MovieFrameInst getMovieFrame() { + updateFields(); + return movieFrame; + } +} diff --git a/src/main/java/bdv/tools/movie/panels/MovieSaveDialog.java b/src/main/java/bdv/tools/movie/panels/MovieSaveDialog.java new file mode 100644 index 00000000..6e2998ca --- /dev/null +++ b/src/main/java/bdv/tools/movie/panels/MovieSaveDialog.java @@ -0,0 +1,134 @@ +package bdv.tools.movie.panels; + +import bdv.export.ProgressWriter; +import bdv.tools.movie.ProduceMovieDialog; +import bdv.util.DelayedPackDialog; +import bdv.viewer.OverlayRenderer; +import bdv.viewer.ViewerPanel; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.File; + +public class MovieSaveDialog extends DelayedPackDialog implements OverlayRenderer { + private static final long serialVersionUID = 1L; + + private final ViewerPanel viewer; + + private final ProgressWriter progressWriter; + + private final JTextField pathTextField; + + private final JSpinner spinnerWidth; + + private final JSpinner spinnerHeight; + + public MovieSaveDialog(final Frame owner, final ViewerPanel viewer, final ProgressWriter progressWriter, ProduceMovieDialog produceMovieDialog) { + super(owner, "record movie", false); + this.viewer = viewer; + 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 typePanel = new JPanel(); + boxes.add(typePanel); + + typePanel.add(new JLabel("Type:")); + + 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 buttonsPanel = new JPanel(); + boxes.add(buttonsPanel); + buttonsPanel.setLayout(new BorderLayout(0, 0)); + + final JButton recordButton = new JButton("Record"); + buttonsPanel.add(recordButton, BorderLayout.EAST); + + 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(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; + } + setVisible(false); + final int width = (Integer) spinnerWidth.getValue(); + final int height = (Integer) spinnerHeight.getValue(); + produceMovieDialog.exportPNGs(dir,width,height,progressWriter); + }); + + 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(); + } + + @Override + public void drawOverlays(final Graphics g) { + } + + @Override + public void setCanvasSize(final int width, final int height) { + spinnerWidth.setValue(width); + spinnerHeight.setValue(height); + } +} \ No newline at end of file diff --git a/src/main/java/bdv/tools/movie/preview/MovieFrameInst.java b/src/main/java/bdv/tools/movie/preview/MovieFrameInst.java new file mode 100644 index 00000000..f7b2f071 --- /dev/null +++ b/src/main/java/bdv/tools/movie/preview/MovieFrameInst.java @@ -0,0 +1,73 @@ +package bdv.tools.movie.preview; + +import net.imglib2.realtransform.AffineTransform3D; + +import java.io.Serializable; + +public class MovieFrameInst implements Serializable { + private volatile transient AffineTransform3D transform; + private double[] transformParams; + private int position; + private int frames; + private int accel; + private final static int DEFAULT_FRAMES = 120; + private final static int DEFAULT_ACCEL = 0; + + + public MovieFrameInst(int position, AffineTransform3D transform) { + this(position, transform, ((position == 0) ? 0 : DEFAULT_FRAMES), ((position == 0) ? 0 : DEFAULT_ACCEL)); + } + + public MovieFrameInst(int position, AffineTransform3D transform, int frames, int accel) { + this.position = position; + this.transform = transform; + this.transformParams = transform.getRowPackedCopy(); + this.frames = frames; + this.accel = accel; + } + + public AffineTransform3D getTransform() { + if (transform == null) + { + transform = new AffineTransform3D(); + transform.set(transformParams); + } + return transform; + } + + public void setTransform(AffineTransform3D transform) { + this.transform = transform; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public int getFrames() { + return frames; + } + + public void setFrames(int frames) { + this.frames = frames; + } + + public int getAccel() { + return accel; + } + + public void setAccel(int accel) { + this.accel = accel; + } + + public static int getDefaultFrames() { + return DEFAULT_FRAMES; + } + + public static int getDefaultAccel() { + return DEFAULT_ACCEL; + } +} diff --git a/src/main/java/bdv/tools/movie/preview/PreviewRender.java b/src/main/java/bdv/tools/movie/preview/PreviewRender.java new file mode 100644 index 00000000..e0ae21cd --- /dev/null +++ b/src/main/java/bdv/tools/movie/preview/PreviewRender.java @@ -0,0 +1,169 @@ +package bdv.tools.movie.preview; + + +import bdv.cache.CacheControl; +import bdv.tools.movie.MovieProducer; +import bdv.tools.movie.panels.ImageFrame; +import bdv.viewer.ViewerPanel; +import bdv.viewer.ViewerState; +import bdv.viewer.animate.SimilarityTransformAnimator; +import bdv.viewer.overlay.MultiBoxOverlayRenderer; +import bdv.viewer.overlay.ScaleBarOverlayRenderer; +import bdv.viewer.render.MultiResolutionRenderer; +import bdv.viewer.render.PainterThread; +import net.imglib2.realtransform.AffineTransform3D; + +import java.awt.*; +import java.awt.image.BufferedImage; + +public class PreviewRender implements Runnable { + + private final ViewerPanel viewer; + private final AffineTransform3D[] transforms; + private final int[] frames; + private final int[] accel; + private final int sleep; + private final int down; + public Thread t; + + boolean suspended = false; + + public PreviewRender( + final ViewerPanel viewer, + final AffineTransform3D[] transforms, + final int[] frames, + final int[] accel, + final int sleep, + final int down) { + System.out.println("got "+transforms.length+" transforms"); + this.viewer = viewer; + this.transforms = transforms; + this.frames = frames; + this.accel = accel; + this.sleep = sleep; + this.down = down; + } + + public void run() { + ImageFrame preview = new ImageFrame(); + + int width = 600; + int height = 600; + + int screenWidth = viewer.getDisplayComponent().getWidth(); + int screenHeight = viewer.getDisplayComponent().getHeight(); + + double ratio = Math.min(width * 1.0 / screenWidth, height * 1.0 / screenHeight); + final AffineTransform3D viewerScale = new AffineTransform3D(); + final AffineTransform3D viewerTranslation = new AffineTransform3D(); + + viewerScale.set( + ratio, 0, 0, 0, + 0, ratio, 0, 0, + 0, 0, 1.0, 0); +// viewerTranslation.set( +// 1, 0, 0, 0.5 * screenWidth, +// 0, 1, 0, 0.5 * screenHeight, +// 0, 0, 1, 0); + + + final ViewerState renderState = viewer.state(); + final ScaleBarOverlayRenderer scalebar = new ScaleBarOverlayRenderer(); + final MultiBoxOverlayRenderer box = new MultiBoxOverlayRenderer(width, height); + + final MovieProducer.Target target = new MovieProducer.Target(width, height); + + final MultiResolutionRenderer renderer = new MultiResolutionRenderer( + target, + new PainterThread(null), + new double[]{1.0}, + 0l, + 12, + null, + false, + viewer.getOptionValues().getAccumulateProjectorFactory(), + new CacheControl.Dummy()); + + + for (int k = 1; k < transforms.length; ++k) { + final SimilarityTransformAnimator animator = new SimilarityTransformAnimator( + transforms[k - 1], + transforms[k], + 0, + 0, + 0); + int downsampledFrames = frames[k]/down; + + for (int d = 0; d < downsampledFrames; ++d) { + final AffineTransform3D tkd = animator.get(MovieProducer.accel((double) d / (double) downsampledFrames, accel[k])); + tkd.preConcatenate(viewerTranslation.inverse()); + tkd.preConcatenate(viewerScale); + tkd.preConcatenate(viewerTranslation); + viewer.state().setViewerTransform(tkd); + renderState.setViewerTransform(tkd); + renderer.requestRepaint(); + try { + renderer.paint(renderState); + } catch (final Exception e) { + e.printStackTrace(); + return; + } + + /* clahe */ + final BufferedImage bi = target.renderResult.getBufferedImage(); + + final Graphics2D g2 = bi.createGraphics(); + + g2.drawImage(bi, 0, 0, null); + + /* scalebar */ + g2.setClip(0, 0, width, height); + scalebar.setViewerState(renderState); + scalebar.paint(g2); + box.setViewerState(renderState); + box.paint(g2); + + preview.setImage(bi); + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + } + } + } + + public boolean getStatus() { + return t == null; + } + + public boolean isDone() { + return (t.getState() == Thread.State.TERMINATED); + } + + public void start() { + if (getStatus()) { + t = new Thread(this); + t.start(); + } + } + + public void suspend() { + t.stop(); + suspended = true; + } + + public boolean isSuspended() { + return suspended; + } + + synchronized void resume() { + suspended = false; + t.resume(); + this.notify(); + } + + +} + diff --git a/src/main/java/bdv/tools/movie/preview/PreviewThread.java b/src/main/java/bdv/tools/movie/preview/PreviewThread.java new file mode 100644 index 00000000..2778175b --- /dev/null +++ b/src/main/java/bdv/tools/movie/preview/PreviewThread.java @@ -0,0 +1,101 @@ +package bdv.tools.movie.preview; + +import bdv.tools.movie.MovieProducer; +import bdv.viewer.Interpolation; +import bdv.viewer.ViewerPanel; +import bdv.viewer.animate.SimilarityTransformAnimator; +import net.imglib2.realtransform.AffineTransform3D; + +import javax.swing.*; + +public class PreviewThread implements Runnable { + + private final ViewerPanel viewer; + private final AffineTransform3D[] transforms; + private final int[] frames; + private final int[] accel; + private final int sleep; + private final int down; + public Thread t; + + boolean suspended = false; + + public PreviewThread( + final ViewerPanel viewer, + final AffineTransform3D[] transforms, + final int[] frames, + final int[] accel, + final int sleep, + final int down) { + this.viewer = viewer; + this.transforms = transforms; + this.frames = frames; + this.accel = accel; + this.sleep = sleep; + this.down = down; + } + + public void run() { + try { + viewer.setInterpolation(Interpolation.NLINEAR); + for (int k = 1; k < transforms.length; ++k) { + synchronized (this) { + if (suspended) { + wait(); + } + } + final SimilarityTransformAnimator animator = new SimilarityTransformAnimator( + transforms[k - 1], + transforms[k], + 0, + 0, + 0); + int downFrames = frames[k] / down; + for (int d = 0; d < downFrames; ++d) { + final AffineTransform3D tkd = animator.get(MovieProducer.accel((double) d / (double) downFrames, accel[k])); +// System.out.println(tkd.toString()); + SwingUtilities.invokeLater(() -> { + viewer.state().setViewerTransform(tkd); + viewer.repaint(); + }); + Thread.sleep(sleep); + } + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + this.start(); + } + + public boolean getStatus() { + return t == null; + } + + public boolean isDone(){ + return (t.getState()==Thread.State.TERMINATED); + } + + public void start() { + if (getStatus()) { + t = new Thread(this); + t.start(); + } + } + + public void suspend() { + t.stop(); + suspended = true; + } + + public boolean isSuspended() { + return suspended; + } + + synchronized void resume() { + suspended = false; + t.resume(); + this.notify(); + } + +} + diff --git a/src/main/java/bdv/tools/movie/serilizers/AffineTransform3DJsonSerializer.java b/src/main/java/bdv/tools/movie/serilizers/AffineTransform3DJsonSerializer.java new file mode 100644 index 00000000..c57b7ad3 --- /dev/null +++ b/src/main/java/bdv/tools/movie/serilizers/AffineTransform3DJsonSerializer.java @@ -0,0 +1,26 @@ +package bdv.tools.movie.serilizers; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import net.imglib2.realtransform.AffineTransform3D; + +import java.lang.reflect.Type; + +public class AffineTransform3DJsonSerializer implements JsonSerializerDeserializer { + + public AffineTransform3D deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + double[] data = context.deserialize(json, double[].class); + AffineTransform3D t = new AffineTransform3D(); + t.set(data); + return t; + } + + public JsonElement serialize(AffineTransform3D src, Type typeOfSrc, JsonSerializationContext context) { + double[] data = new double[12]; + src.toArray(data); + return context.serialize(data); + } + +} \ No newline at end of file diff --git a/src/main/java/bdv/tools/movie/serilizers/JsonSerializerDeserializer.java b/src/main/java/bdv/tools/movie/serilizers/JsonSerializerDeserializer.java new file mode 100644 index 00000000..aaa78465 --- /dev/null +++ b/src/main/java/bdv/tools/movie/serilizers/JsonSerializerDeserializer.java @@ -0,0 +1,7 @@ +package bdv.tools.movie.serilizers; + +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonSerializer; + +public interface JsonSerializerDeserializer extends JsonSerializer, JsonDeserializer { +} \ No newline at end of file diff --git a/src/main/java/bdv/tools/movie/serilizers/MovieFramesSerializer.java b/src/main/java/bdv/tools/movie/serilizers/MovieFramesSerializer.java new file mode 100644 index 00000000..510d6327 --- /dev/null +++ b/src/main/java/bdv/tools/movie/serilizers/MovieFramesSerializer.java @@ -0,0 +1,40 @@ +package bdv.tools.movie.serilizers; + +import bdv.tools.movie.preview.MovieFrameInst; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import net.imglib2.realtransform.AffineTransform3D; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +public class MovieFramesSerializer { + + private static Gson getGson() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.serializeNulls().serializeSpecialFloatingPointValues(); + gsonBuilder.registerTypeAdapter(AffineTransform3D.class, new AffineTransform3DJsonSerializer()); + return gsonBuilder.create(); + } + + public static boolean save(List list, File file){ + try (Writer writer = new FileWriter(file)) { + getGson().toJson(list, writer); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + public static ArrayList getFrom(File file) throws FileNotFoundException { + return getGson().fromJson(new FileReader(file.getAbsolutePath()),new TypeToken>(){}.getType()); + } +}