Генерация изображений попиксельно в JavaFX с видимым прогрессом - PullRequest
0 голосов
/ 01 марта 2019

Я пишу приложение JavaFX, которое генерирует изображения абстрактных шаблонов попиксельно.Результат должен выглядеть примерно так: .Вот мой основной класс:

package application;

public class Main extends Application {

  private static final int WIDTH = 800; 
  private static final int HEIGHT = 600; 

    @Override
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        Scene scene = new Scene(root, WIDTH, HEIGHT);
        scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();

    final Canvas canvas = new Canvas(WIDTH, HEIGHT);

        root.getChildren().add(canvas);

        final GraphicsContext gc = canvas.getGraphicsContext2D();
        final PixelWriter pw = gc.getPixelWriter();

        final PixelGenerator generator = new PixelGenerator(WIDTH, HEIGHT, pw);
        final Thread th = new Thread(generator);
        th.setDaemon(true);
        th.start();
    }

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

Класс PixelGenerator генерирует новые пиксели один за другим, заполняя холст с помощью метода PixelWriter.setColor ().Он вычисляет цвет для новых пикселей на основе некоторых случайных чисел и ранее созданных цветов.

Если я запускаю PixelGenerator в потоке приложения, графический интерфейс пользователя блокируется, пока не будет заполнено все доступное пространство, и только тогда яувидеть полную картину.

Чтобы избежать этого, я заставил свой класс PixelGenerator расширить javafx.concurrent.Task, а его метод call () генерирует все пиксели одновременно.Иногда это работает, как ожидалось, и я могу видеть, как изображение генерируется шаг за шагом, но иногда картинка остается незаконченной , как будто задача не будет выполняться до конца.Отладка показывает, что она всегда выполняется до конца, но более поздние вызовы PixelWriter.setColor () не имеют никакого эффекта.

Я пробовал разные подходы, чтобы исправить это.Например, я добавил обработчик события onSucceeded и попытался заставить Canvas «обновиться», так как я думал, что он просто каким-то образом «пропускает» свою последнюю итерацию обновления.Нет успехаСтранно, даже раскраска дальнейших пикселей никак не влияет на слушателя.

Я также попытался использовать AnimationTimer вместо использования Задачи.Это работает, но моя проблема в том, что я не могу предсказать, сколько пикселей я смогу сгенерировать между вызовами handle ().Сложность алгоритма генерации и, хотя время процессора, необходимое для генерации пикселя, будет изменяться по мере развития алгоритма.

Моя идеальная цель - потратить все доступное время процессора на генерацию пикселей, но в то же времячтобы быть в состоянии увидеть прогресс генерации в деталях (60 или даже 30 FPS будет в порядке).

Пожалуйста, помогите мне, что я делаю неправильно и в каком направлении я должен идти, учитывая мою цель

1 Ответ

0 голосов
/ 01 марта 2019

Это правда, что вы не можете использовать PixelWriter в потоке, отличном от потока приложения JavaFX.Тем не менее, ваша задача может поместить данные пикселя в объект значения, отличный от JavaFX, например IntBuffer , который поток приложения затем может передать setPixels .Пример программы:

import java.nio.IntBuffer;

import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;

public class PixelGeneratorTest
extends Application {
    public static final int WIDTH = Integer.getInteger("width", 500);
    public static final int HEIGHT = Integer.getInteger("height", 500);

    public class PixelGenerator
    extends Task<IntBuffer> {

        private final int width;
        private final int height;

        public PixelGenerator(int width,
                              int height) {
            this.width = width;
            this.height = height;
        }

        @Override
        public IntBuffer call() {
            IntBuffer buffer = IntBuffer.allocate(width * height);

            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    Color pixel = Color.hsb(
                        y * 360.0 / HEIGHT, 1, (double) x / WIDTH);

                    int argb = 0xff000000 |
                        ((int) (pixel.getRed() * 255) << 16) |
                        ((int) (pixel.getGreen() * 255) << 8) |
                         (int) (pixel.getBlue() * 255);

                    buffer.put(argb);
                }
            }

            buffer.flip();

            return buffer;
        }
    }

    @Override
    public void start(Stage stage) {
        Canvas canvas = new Canvas(WIDTH, HEIGHT);

        stage.setTitle("PixelGenerator Test");
        stage.setScene(new Scene(new BorderPane(canvas)));
        stage.show();

        GraphicsContext gc = canvas.getGraphicsContext2D();
        PixelWriter pw = gc.getPixelWriter();

        PixelGenerator generator = new PixelGenerator(WIDTH, HEIGHT);

        generator.valueProperty().addListener((o, oldValue, pixels) ->
            pw.setPixels(0, 0, WIDTH, HEIGHT,
                PixelFormat.getIntArgbInstance(), pixels, WIDTH));

        Thread th = new Thread(generator);
        th.setDaemon(true);
        th.start();
    }
}

Если вы ожидаете, что ваше изображение будет очень большим и, следовательно, нецелесообразно хранить его в памяти сразу, вы можете написать задачу, чтобы принимать аргументы конструктора, которые позволяют ему генерировать только частьизображения, а затем создать несколько задач для обработки генерации пикселей по частям.Другим вариантом является наличие одной Задачи, которая многократно вызывает updateValue , но затем вам нужно будет создать класс пользовательских значений, который будет содержать как буфер, так и прямоугольную область изображения, к которой этот буфер должен быть применен.

Обновление:

Вы уточнили, что хотите прогрессивную визуализацию изображения.Задача не будет работать для быстрых обновлений, поскольку JavaFX может объединять изменения в значении Задачи.Итак, вернемся к основам: создайте Runnable, который вызывает setPixels в вызове Platform.runLater , чтобы обеспечить правильное использование потока:

import java.nio.IntBuffer;
import java.util.Objects;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;

public class PixelGeneratorTest2
extends Application {
    public static final int WIDTH = Integer.getInteger("width", 500);
    public static final int HEIGHT = Integer.getInteger("height", 500);

    private static final PixelFormat<IntBuffer> pixelFormat =
        PixelFormat.getIntArgbInstance();

    public class PixelGenerator
    implements Runnable {

        private final int width;
        private final int height;
        private final PixelWriter writer;

        public PixelGenerator(int width,
                              int height,
                              PixelWriter pw) {
            this.width = width;
            this.height = height;
            this.writer = Objects.requireNonNull(pw, "Writer cannot be null");
        }

        @Override
        public void run() {
            int blockHeight = 4;
            IntBuffer buffer = IntBuffer.allocate(width * blockHeight);

            try {
                for (int y = 0; y < height; y++) {
                    for (int x = 0; x < width; x++) {
                        Color pixel = Color.hsb(
                            y * 360.0 / HEIGHT, 1, (double) x / WIDTH);

                        int argb = 0xff000000 |
                            ((int) (pixel.getRed() * 255) << 16) |
                            ((int) (pixel.getGreen() * 255) << 8) |
                             (int) (pixel.getBlue() * 255);

                        buffer.put(argb);
                    }

                    if (y % blockHeight == blockHeight - 1 || y == height - 1) {
                        buffer.flip();

                        int regionY = y - y % blockHeight;
                        int regionHeight =
                            Math.min(blockHeight, height - regionY);

                        Platform.runLater(() -> 
                            writer.setPixels(0, regionY, width, regionHeight,
                                pixelFormat, buffer, width));

                        buffer.clear();
                    }

                    // Pretend pixel calculation was CPU-intensive.
                    Thread.sleep(25);

                }
            } catch (InterruptedException e) {
                System.err.println("Interrupted, exiting.");
            }
        }
    }

    @Override
    public void start(Stage stage) {
        Canvas canvas = new Canvas(WIDTH, HEIGHT);

        stage.setTitle("PixelGenerator Test");
        stage.setScene(new Scene(new BorderPane(canvas)));
        stage.show();

        GraphicsContext gc = canvas.getGraphicsContext2D();
        PixelWriter pw = gc.getPixelWriter();

        PixelGenerator generator = new PixelGenerator(WIDTH, HEIGHT, pw);

        Thread th = new Thread(generator);
        th.setDaemon(true);
        th.start();
    }
}

Вы могли бы вызывать платформу.runLater для каждого отдельного пикселя, но я подозреваю, что это приведет к перегрузке потока приложения JavaFX.

...