首页 > 解决方案 > 在 JavaFX 中逐像素生成图像,进度可见

问题描述

我正在编写一个 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,GUI 会被阻塞,直到整个可用空间都被填满,然后我才能看到完整的画面。

为了避免这种情况,我让我的 PixelGenerator 类扩展了 javafx.concurrent.Task 并且它的 call() 方法一次生成所有像素。有时它按预期工作,我可以看到图像是如何逐步生成的,但有时图片仍然未完成,好像任务不会运行到最后。调试显示一直运行到最后,但是后面的 PixelWriter.setColor() 调用没有效果。

我尝试了不同的方法来修复它。例如,我添加了一个 onSucceeded 事件处理程序,并试图使 Canvas “刷新”,因为我认为它只是以某种方式“跳过”了它的最后一次刷新迭代。没有成功。奇怪的是,即使为更多像素着色,在听者内部也没有效果。

我还尝试使用 AnimationTimer 而不是使用任务。它有效,但我的问题是我无法预测在它的 handle() 调用之间我能够生成多少像素。生成算法的复杂性以及像素生成所需的 CPU 时间将随着算法的发展而变化。

我的理想目标是花费所有可用的 CPU 时间来生成像素,但同时能够详细查看生成进度(60 甚至 30 FPS 都可以)。

请帮帮我,我做错了什么,给定我的目标我应该走什么方向?

标签: javaperformanceimage-processingjavafxoptimization

解决方案


确实,您不能在 JavaFX 应用程序线程以外的线程中使用 PixelWriter。但是,您的 Task 可以将像素数据本身放在非 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();
    }
}

如果您希望您的图像非常大,因此一次保存在内存中是不切实际的,您可以编写 Task 以接受构造函数参数,使其仅生成图像的一部分,然后创建多个任务来处理像素生成一块一块的。另一种选择是拥有一个重复调用updateValue的任务,但您必须创建一个自定义值类,其中包含缓冲区和图像中应应用该缓冲区的矩形区域。

更新:

您已经澄清您需要渐进式图像渲染。Task 不适用于快速更新,因为对 Task 值的更改可能会被 JavaFX 合并。所以它回到了基础:创建一个在Platform.runLater调用中调用 setPixels 的 Runnable,以确保正确的线程使用:

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

可以为每个单独的像素调用 Platform.runLater,但我怀疑这会使 JavaFX 应用程序线程不堪重负。


推荐阅读