簡體   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