繁体   English   中英

有没有办法将 JavaFX animation 导出到图像帧?

[英]Is there any way to export a JavaFX animation to image frames?

我想要完成的事情:
制作 JavaFX animation 最终将在 video.mp4 (或任何其他扩展名)中进行协调

一种方法:
在 animation 播放期间,将 30fps(或 60fps)导出为静止图像(即 png 图像)。 之后使用一些工具处理图像并创建视频。

那么如何创建 JavaFX window 的框架呢?

免责声明

以下解决方案仅作为概念证明提供,没有附带解释,没有评论支持,不保证它会为您工作,不保证它不会出现错误(它可能有一些小错误),并且没有promise 适用于任何用途。

解决方案策略

Capture 使用 James 和 mipa 评论中建议的技术:

创建一个未显示在包含 animation 的 window 中的场景。 不要 play() animation,而是重复调用 animation 上的 jumpTo(...) 来创建每一帧,对结果进行快照,然后写入文件。

JPEG 是一种有损编码方法,此示例的 output 有点模糊(可能是由于可以调整的默认 ImageIO 设置)。

如果需要,可以将每帧 output 转换为无损格式(如 png)的单独文件,而不是 mjpeg,然后通过第三方处理软件运行以创建另一种视频格式,如 mp4。

MjpegCaptureAndPlayApp.java

package com.example.mjpeg;

import javafx.animation.Animation;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.nio.file.Files;

public class MjpegCaptureAndPlayApp extends Application {
    final private double W = 100, H = 100;

    private MjpegPlayer player;

    @Override
    public void start(Stage stage) throws Exception {
        String movieFile = Files.createTempFile("mjpeg-capture", "mjpeg").toString();

        CaptureAnimation captureAnimation = createCaptureAnimation();
        MjpegCapture mjpegCapture = new MjpegCapture(
                movieFile,
                captureAnimation.root(),
                captureAnimation.animation()
        );
        mjpegCapture.capture();

        player = new MjpegPlayer(movieFile);
        StackPane viewer = new StackPane(player.getViewer());
        viewer.setPrefSize(W, H);

        VBox layout = new VBox(20);
        layout.setStyle("-fx-background-color: cornsilk;");
        layout.setPadding(new Insets(10));
        layout.setAlignment(Pos.CENTER);

        layout.getChildren().setAll(
                viewer,
                player.getControls()
        );

        stage.setScene(new Scene(layout));
        stage.show();

        player.getTimeline().playFromStart();
    }

    @Override
    public void stop() throws Exception {
        if (player != null) {
            player.dispose();
        }
    }

    record CaptureAnimation(Parent root, Animation animation) {}

    private CaptureAnimation createCaptureAnimation() {
        Pane root = new Pane();
        root.setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
        root.setPrefSize(W, H);
        root.setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);

        Circle circle = new Circle(W / 10, Color.PURPLE);
        root.getChildren().add(circle);

        TranslateTransition translateTransition = new TranslateTransition(
                Duration.seconds(5),
                circle
        );
        translateTransition.setFromX(0);
        translateTransition.setToX(W);
        translateTransition.setFromY(H/2);
        translateTransition.setToY(H/2);
        translateTransition.setAutoReverse(true);
        translateTransition.setCycleCount(2);

        // move to start pos.
        circle.setTranslateX(0);
        circle.setTranslateY(H/2);

        return new CaptureAnimation(root, translateTransition);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

MjpegCapture.java

package com.example.mjpeg;

import javafx.animation.Animation;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.util.Duration;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class MjpegCapture {

    private final Duration SECS_PER_FRAME = Duration.seconds(1.0 / 24);

    private final String videoFilename;
    private final Parent root;
    private final Animation animation;

    public MjpegCapture(String videoFilename, Parent root, Animation animation) {
        this.videoFilename = videoFilename;
        this.root = root;
        this.animation = animation;
    }

    public void capture() throws IOException {
        VideoStreamOutput videoStreamOutput = new VideoStreamOutput(videoFilename);

        animation.playFromStart();
        Duration curPos = Duration.ZERO;

        SnapshotParameters snapshotParameters = new SnapshotParameters();
        // transparent fill not supported by jpeg I believe so not enabled.
        //snapshotParameters.setFill(Color.TRANSPARENT);

        Scene scene = new Scene(root);

        // force a layout pass so that we can measure the height and width of the root node.
        scene.snapshot(null);
        int w = (int) scene.getWidth();
        int h = (int) scene.getHeight();
        WritableImage fxImage = new WritableImage(w, h);

        boolean complete;
        ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
        do {
            animation.jumpTo(curPos);
            root.snapshot(snapshotParameters, fxImage);

            // Get buffered image:
            // uses ugly, inefficient workaround from:
            //   https://stackoverflow.com/a/19605733/1155209
            BufferedImage image = SwingFXUtils.fromFXImage(fxImage, null);

            // Remove alpha-channel from buffered image:
            BufferedImage imageRGB = new BufferedImage(
                    image.getWidth(),
                    image.getHeight(),
                    BufferedImage.OPAQUE);

            Graphics2D graphics = imageRGB.createGraphics();
            graphics.drawImage(image, 0, 0, null);
            ImageIO.write(imageRGB, "jpg", outputBuffer);

            videoStreamOutput.writeNextFrame(outputBuffer.toByteArray());
            outputBuffer.reset();

            complete = curPos.greaterThanOrEqualTo(animation.getTotalDuration());

            if (curPos.add(SECS_PER_FRAME).greaterThan(animation.getTotalDuration())) {
                curPos = animation.getTotalDuration();
            } else {
                curPos = curPos.add(SECS_PER_FRAME);
            }
        } while(!complete);

        videoStreamOutput.close();
    }
}

MjpegPlayer.java

package com.example.mjpeg;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.util.Duration;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;

public class MjpegPlayer {

    private final String videoFilename;
    private final Timeline timeline;
    private final ImageView viewer = new ImageView();
    private final HBox controls;
    private VideoStream videoStream;

    public MjpegPlayer(String filename) throws FileNotFoundException {
        videoFilename = filename;
        videoStream = new VideoStream(filename);
        timeline = createTimeline(viewer);
        controls = createControls(timeline);
    }

    private Timeline createTimeline(ImageView viewer) {
        final Timeline timeline = new Timeline();
        final byte[] buf = new byte[15000];

        timeline.getKeyFrames().setAll(
                new KeyFrame(Duration.ZERO, event -> {
                    try {
                        int len = videoStream.readNextFrame(buf);
                        if (len == -1) {
                            timeline.stop();
                            return;
                        }
                        viewer.setImage(
                                new Image(
                                        new ByteArrayInputStream(
                                                Arrays.copyOf(buf, len)
                                        )
                                )
                        );
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }),
                new KeyFrame(Duration.seconds(1.0 / 24))
        );
        timeline.setCycleCount(Timeline.INDEFINITE);

        return timeline;
    }

    private HBox createControls(final Timeline timeline) {
        Button play = new Button("Play");
        play.setOnAction(event -> timeline.play());

        Button pause = new Button("Pause");
        pause.setOnAction(event -> timeline.pause());

        Button restart = new Button("Restart");
        restart.setOnAction(event -> {
            try {
                timeline.stop();
                videoStream = new VideoStream(videoFilename);
                timeline.playFromStart();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        HBox controls = new HBox(10);
        controls.setAlignment(Pos.CENTER);
        controls.getChildren().setAll(
                play,
                pause,
                restart
        );

        return controls;
    }

    public void dispose() throws IOException {
        videoStream.close();
    }

    public String getVideoFilename() {
        return videoFilename;
    }

    public Timeline getTimeline() {
        return timeline;
    }

    public ImageView getViewer() {
        return viewer;
    }

    public HBox getControls() {
        return controls;
    }
}

视频流.java

package com.example.mjpeg;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

class VideoStream {

    private final FileInputStream fis; //video file

    VideoStream(String filename) throws FileNotFoundException {
        fis = new FileInputStream(filename);
    }

    int readNextFrame(byte[] frame) throws Exception {
        int length;
        String lengthAsString;
        byte[] lengthAsBytes = new byte[5];

        //read current frame length
        fis.read(lengthAsBytes, 0, 5);

        //transform lengthAsBytes to integer
        lengthAsString = new String(lengthAsBytes);
        try {
            length = Integer.parseInt(lengthAsString);
        } catch (Exception e) {
            return -1;
        }

        return (fis.read(frame, 0, length));
    }

    void close() throws IOException {
        fis.close();
    }
}

视频流输出.java

package com.example.mjpeg;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

class VideoStreamOutput {
    private FileOutputStream fos; //video file
    private int frameNum; //current frame nb

    public VideoStreamOutput(String filename) throws FileNotFoundException {
        fos = new FileOutputStream(filename);
        frameNum = 0;
    }

    public void writeNextFrame(byte[] frame) throws IOException {
        frameNum++;

        String lengthAsString = String.format("%05d", frame.length);
        byte[] lengthAsBytes = lengthAsString.getBytes(StandardCharsets.US_ASCII);

        fos.write(lengthAsBytes);
        fos.write(frame);

        System.out.println(frameNum + ": " + lengthAsString);
    }

    public void close() throws IOException {
        fos.flush();
        fos.close();
    }
}

模块信息.java

module com.example.mjpeg {
    requires javafx.controls;
    requires javafx.swing;
    requires java.desktop;

    exports com.example.mjpeg;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM