Как показывать изображения с большой частотой в JavaFX? - PullRequest
2 голосов
/ 13 марта 2020

Мое приложение генерирует изображения тепловых карт так быстро, как может процессор (около 30-60 в секунду), и я хочу отобразить их в одной "живой тепловой карте". В AWT / Swing я мог просто нарисовать их в JPanel, который работал как шарм. Недавно я перешел на JavaFX и хочу добиться того же; сначала я попробовал с Canvas , который был медленным, но все в порядке - я sh, но имел серьезную утечку памяти, из-за которой приложение взломало sh. Теперь я попробовал компонент ImageView - который, по-видимому, слишком медленный, так как изображение становится довольно медленным (используя ImageView.setImage на каждой новой итерации). Насколько я понимаю, setImage не гарантирует, что изображение действительно будет отображаться после завершения функции.

У меня складывается впечатление, что я нахожусь на неправильном пути, используя эти компоненты таким образом, что они не сделаны к. Как я могу отображать свои 30-60 изображений в секунду?

РЕДАКТИРОВАТЬ: очень простое тестовое приложение. Вам понадобится библиотека JHeatChart . Обратите внимание, что на настольном компьютере я получаю около 70-80 кадров в секунду, и визуализация нормальная и плавная, но на меньшем малиновом пи (моя целевая машина) я получаю около 30 кадров в секунду, но визуализация с экстремальными затратами.

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);
}

}

1 Ответ

3 голосов
/ 13 марта 2020

Ваш код обновляет пользовательский интерфейс из фонового потока, что однозначно запрещено. Вы должны убедиться, что вы обновляете из потока приложений FX. Вы также хотите попытаться «задушить» фактические обновления пользовательского интерфейса, чтобы они происходили не более одного раза за рендеринг фрейма 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 отображает фрейм (который обычно регулируется со скоростью 60 кадров в секунду), отображается только самая последняя.

Если вы хотите, чтобы вы показывали все изображений, которые генерируются, т.е. вы дросселируете генерацию изображений с частотой кадров 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, et c, на каждой итерации. Это приводит к тому, что многие объекты создаются в куче и быстро удаляются, что может привести к слишком частому запуску G C, особенно на машине с небольшим объемом памяти. Тем не менее, я запустил его с максимальным размером кучи, установленным на 64 МБ (-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));
                }
            }

        };
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...