簡體   English   中英

如何在JavaFX中以大頻率顯示圖像?

[英]How to show images in a large frequency in JavaFX?

我的應用程序生成熱圖圖像的速度與 CPU 一樣快(大約每秒 30-60 張),我想將它們顯示在一個“實時熱圖”中。 在 AWT/Swing 中,我可以將它們繪制成一個 JPanel,它就像一個魅力。 最近,我切換到JavaFX,並希望在這里實現相同的功能; 起初,我嘗試使用Canvas ,它很慢但還可以,但存在嚴重的內存泄漏問題,導致應用程序崩潰。 現在,我嘗試了ImageView組件 - 這顯然太慢了,因為圖像變得非常滯后(在每次新迭代中使用ImageView.setImage )。 據我了解,setImage 不保證在函數完成時實際顯示圖像。

我得到的印象是我走錯了路,以一種他們不適合的方式使用這些組件。 如何每秒顯示 30-60 張圖像?

編輯:一個非常簡單的測試應用程序。 您將需要JHeatChart庫。 請注意,在台式機上,我獲得了大約 70-80 FPS,並且可視化效果還不錯且流暢,但是在較小的樹莓派(我的目標機器)上,我獲得了大約 30 FPS,但是可視化效果非常卡。

package sample;

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.tc33.jheatchart.HeatChart;

import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;

public class Main extends Application {
ImageView imageView = new ImageView();
final int scale = 15;

@Override
public void start(Stage primaryStage) {
    Thread generator = new Thread(() -> {
        int col = 0;
        LinkedList<Long> fps = new LinkedList<>();
        while (true) {
            fps.add(System.currentTimeMillis());
            double[][] matrix = new double[48][128];
            for (int i = 0; i < 48; i++) {
                for (int j = 0; j < 128; j++) {
                    matrix[i][j] = col == j ? Math.random() : 0;
                }
            }
            col = (col + 1) % 128;

            HeatChart heatChart = new HeatChart(matrix, 0, 1);
            heatChart.setShowXAxisValues(false);
            heatChart.setShowYAxisValues(false);
            heatChart.setLowValueColour(java.awt.Color.black);
            heatChart.setHighValueColour(java.awt.Color.white);
            heatChart.setAxisThickness(0);
            heatChart.setChartMargin(0);
            heatChart.setCellSize(new Dimension(1, 1));

            long currentTime = System.currentTimeMillis();
            fps.removeIf(elem -> currentTime - elem > 1000);
            System.out.println(fps.size());

            imageView.setImage(SwingFXUtils.toFXImage((BufferedImage) scale(heatChart.getChartImage(), scale), null));
        }
    });

    VBox box = new VBox();
    box.getChildren().add(imageView);

    Scene scene = new Scene(box, 1920, 720);
    primaryStage.setScene(scene);
    primaryStage.show();

    generator.start();
}


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

private static Image scale(Image image, int scale) {
    BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
            BufferedImage.TYPE_INT_ARGB);
    AffineTransform at = new AffineTransform();
    at.scale(scale, scale);
    AffineTransformOp scaleOp =
            new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);

    return scaleOp.filter((BufferedImage) image, res);
}

}

您的代碼從后台線程更新 UI,這絕對是不允許的。 您需要確保從 FX 應用程序線程更新。 您還想嘗試“限制”實際的 UI 更新,使每個 JavaFX 幀渲染不超過一次。 最簡單的方法是使用AnimationTimer ,每次渲染一幀時都會調用其handle()方法。

這是您執行此操作的代碼版本:

import java.awt.Dimension;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicReference;

import org.tc33.jheatchart.HeatChart;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {
    ImageView imageView = new ImageView();
    final int scale = 15;

    @Override
    public void start(Stage primaryStage) {

        AtomicReference<BufferedImage> image = new AtomicReference<>();

        Thread generator = new Thread(() -> {
            int col = 0;
            LinkedList<Long> fps = new LinkedList<>();
            while (true) {
                fps.add(System.currentTimeMillis());
                double[][] matrix = new double[48][128];
                for (int i = 0; i < 48; i++) {
                    for (int j = 0; j < 128; j++) {
                        matrix[i][j] = col == j ? Math.random() : 0;
                    }
                }
                col = (col + 1) % 128;

                HeatChart heatChart = new HeatChart(matrix, 0, 1);
                heatChart.setShowXAxisValues(false);
                heatChart.setShowYAxisValues(false);
                heatChart.setLowValueColour(java.awt.Color.black);
                heatChart.setHighValueColour(java.awt.Color.white);
                heatChart.setAxisThickness(0);
                heatChart.setChartMargin(0);
                heatChart.setCellSize(new Dimension(1, 1));

                long currentTime = System.currentTimeMillis();
                fps.removeIf(elem -> currentTime - elem > 1000);
                System.out.println(fps.size());

                image.set((BufferedImage) scale(heatChart.getChartImage(), scale));

            }
        });

        VBox box = new VBox();
        box.getChildren().add(imageView);

        Scene scene = new Scene(box, 1920, 720);
        primaryStage.setScene(scene);
        primaryStage.show();

        generator.setDaemon(true);
        generator.start();

        AnimationTimer animation = new AnimationTimer() {

            @Override
            public void handle(long now) {
                BufferedImage img = image.getAndSet(null);
                if (img != null) {
                    imageView.setImage(SwingFXUtils.toFXImage(img, null));
                }
            }

        };

        animation.start();
    }

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

    private static Image scale(Image image, int scale) {
        BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
                BufferedImage.TYPE_INT_ARGB);
        AffineTransform at = new AffineTransform();
        at.scale(scale, scale);
        AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);

        return scaleOp.filter((BufferedImage) image, res);
    }
}

使用AtomicReference包裝緩沖圖像可確保它在兩個線程之間安全共享。

在我的機器上,這每秒生成大約 130 個圖像; 請注意,並非所有都顯示,因為每次 JavaFX 圖形框架顯示一幀(通常限制為 60fps)時,只會顯示最新的一個。

如果要確保顯示生成的所有圖像,即通過 JavaFX 幀率限制圖像生成,則可以使用BlockingQueue來存儲圖像:

    // AtomicReference<BufferedImage> image = new AtomicReference<>();

    // Size of the queue is a trade-off between memory consumption
    // and smoothness (essentially works as a buffer size)
    BlockingQueue<BufferedImage> image = new ArrayBlockingQueue<>(5);

    // ...

    // image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
    try {
        image.put((BufferedImage) scale(heatChart.getChartImage(), scale));
    } catch (InterruptedException exc) {
        Thread.currentThread.interrupt();
    }

        @Override
        public void handle(long now) {
            BufferedImage img = image.poll();
            if (img != null) {
                imageView.setImage(SwingFXUtils.toFXImage(img, null));
            }
        }

代碼效率很低,因為每次迭代都會生成一個新矩陣、新HeatChart等。 這會導致在堆上創建許多對象並迅速丟棄,這會導致 GC 運行過於頻繁,尤其是在小內存機器上。 也就是說,我在最大堆大小設置為 64MB ( -Xmx64m ) 的情況下運行它,它仍然表現良好。 您或許可以優化代碼,但使用如上所示的AnimationTimer ,更快地生成圖像不會對 JavaFX 框架造成任何額外壓力。 我建議調查使用HeatChart的可變性(即setZValues() )以避免創建太多對象,和/或使用PixelBuffer將數據直接寫入圖像視圖(這需要在 FX 應用程序線程上完成)。

這是一個不同的例子,它(幾乎)完全減少了對象的創建,使用一個屏幕外的int[]數組來計算數據,並使用一個屏幕上的int[]數組來顯示它。 有一些低級線程細節,以確保屏幕上的數組只能在一致的狀態下看到。 屏幕上的數組用於作為PixelBuffer基礎,后者又用於WritableImage

這個類生成圖像數據:

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;

public class ImageGenerator {

    private final int width;
    private final int height;


    // Keep two copies of the data: one which is not exposed
    // that we modify on the fly during computation;
    // another which we expose publicly. 
    // The publicly exposed one can be viewed only in a complete 
    // state if operations on it are synchronized on this object.
    private final int[] privateData ;
    private final int[] publicData ;

    private final long[] frameTimes ;
    private int currentFrameIndex ;
    private final AtomicLong averageGenerationTime ;

    private final ReentrantLock lock ;


    private static final double TWO_PI = 2 * Math.PI;
    private static final double PI_BY_TWELVE = Math.PI / 12; // 15 degrees

    public ImageGenerator(int width, int height) {
        super();
        this.width = width;
        this.height = height;
        privateData = new int[width * height];
        publicData = new int[width * height];

        lock = new ReentrantLock();

        this.frameTimes = new long[100];
        this.averageGenerationTime = new AtomicLong();
    }

    public void generateImage(double angle) {

        // compute in private data copy:

        int minDim = Math.min(width, height);
        int minR2 = minDim * minDim / 4;
        for (int x = 0; x < width; x++) {
            int xOff = x - width / 2;
            int xOff2 = xOff * xOff;
            for (int y = 0; y < height; y++) {

                int index = x + y * width;

                int yOff = y - height / 2;
                int yOff2 = yOff * yOff;
                int r2 = xOff2 + yOff2;
                if (r2 > minR2) {
                    privateData[index] = 0xffffffff; // white
                } else {
                    double theta = Math.atan2(yOff, xOff);
                    double delta = Math.abs(theta - angle);
                    if (delta > TWO_PI - PI_BY_TWELVE) {
                        delta = TWO_PI - delta;
                    }
                    if (delta < PI_BY_TWELVE) {
                        int green = (int) (255 * (1 - delta / PI_BY_TWELVE));
                        privateData[index] = (0xff << 24) | (green << 8); // green, fading away from center
                    } else {
                        privateData[index] = 0xff << 24; // black
                    }
                }
            }
        }

        // copy computed data to public data copy:
        lock.lock(); 
        try {
            System.arraycopy(privateData, 0, publicData, 0, privateData.length);
        } finally {
            lock.unlock();
        }

        frameTimes[currentFrameIndex] = System.nanoTime() ;
        int nextIndex = (currentFrameIndex + 1) % frameTimes.length ;
        if (frameTimes[nextIndex] > 0) {
            averageGenerationTime.set((frameTimes[currentFrameIndex] - frameTimes[nextIndex]) / frameTimes.length);
        }
        currentFrameIndex = nextIndex ;
    }


    public void consumeData(Consumer<int[]> consumer) {
        lock.lock();
        try {
            consumer.accept(publicData);
        } finally {
            lock.unlock();
        }
    }

    public long getAverageGenerationTime() {
        return averageGenerationTime.get() ;
    }

}

這是用戶界面:

import java.nio.IntBuffer;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class AnimationApp extends Application {


    private final int size = 400 ;
    private IntBuffer buffer ;

    @Override
    public void start(Stage primaryStage) throws Exception {

        // background image data generation:

        ImageGenerator generator = new ImageGenerator(size, size);

        // Generate new image data as fast as possible:
        Thread thread = new Thread(() ->  {
            while( true ) {
                long now = System.currentTimeMillis() ;
                double angle = 2 * Math.PI * (now % 10000) / 10000  - Math.PI;
                generator.generateImage(angle);
            }
        });
        thread.setDaemon(true);
        thread.start();


        generator.consumeData(data -> buffer = IntBuffer.wrap(data));
        PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
        PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
        WritableImage image = new WritableImage(pixelBuffer);

        BorderPane root = new BorderPane(new ImageView(image));

        Label fps = new Label("FPS: ");
        root.setTop(fps);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.setTitle("Give me a ping, Vasili. ");
        primaryStage.show();

        AnimationTimer animation = new AnimationTimer() {

            @Override
            public void handle(long now) { 
                // Update image, ensuring we only see the underlying
                // data in a consistent state:
                generator.consumeData(data ->  {
                    pixelBuffer.updateBuffer(pb -> null); 
                });
                long aveGenTime = generator.getAverageGenerationTime() ;
                if (aveGenTime > 0) {
                    double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
                    fps.setText(String.format("FPS: %.2f", aveFPS));
                }
            }

        };

        animation.start();

    }



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

對於不依賴於 JavaFX 13 PixelBuffer ,您可以修改此類以使用PixelWriter (AIUI 這不會那么有效,但在此示例中工作得同樣順利):

//      generator.consumeData(data -> buffer = IntBuffer.wrap(data));
        PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
//      PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
//      WritableImage image = new WritableImage(pixelBuffer);

        WritableImage image = new WritableImage(size, size);
        PixelWriter pixelWriter = image.getPixelWriter() ;

        AnimationTimer animation = new AnimationTimer() {

            @Override
            public void handle(long now) { 
                // Update image, ensuring we only see the underlying
                // data in a consistent state:
                generator.consumeData(data ->  {
//                  pixelBuffer.updateBuffer(pb -> null); 
                    pixelWriter.setPixels(0, 0, size, size, format, data, 0, size);
                });
                long aveGenTime = generator.getAverageGenerationTime() ;
                if (aveGenTime > 0) {
                    double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
                    fps.setText(String.format("FPS: %.2f", aveFPS));
                }
            }

        };

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM