简体   繁体   English

在 JavaFX 中逐像素生成图像,进度可见

[英]Generating images pixel by pixel in JavaFX with progress visible

I'm writing an JavaFX application which generates abstract pattern images pixel by pixel.我正在编写一个 JavaFX 应用程序,它逐像素生成抽象图案图像。 The result should look somewhat like this .结果应该看起来有点像这样 Here is my main class:这是我的主要课程:

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

The PixelGenerator class is generating new pixels one by one, filling the canvas using the PixelWriter.setColor() method. PixelGenerator 类正在一个一个地生成新像素,使用 PixelWriter.setColor() 方法填充画布。 It calculates the color for the new pixels based on some random numbers and on previously generated colors.它根据一些随机数和先前生成的颜色计算新像素的颜色。

If I run the PixelGenerator on the application thread, the GUI is blocked until the whole available space is filled and only then I see the complete picture.如果我在应用程序线程上运行 PixelGenerator,GUI 会被阻塞,直到整个可用空间被填满,然后我才能看到完整的画面。

To avoid this I made my PixelGenerator class extend the javafx.concurrent.Task and its call() method generates all the pixels at once.为了避免这种情况,我让我的 PixelGenerator 类扩展了 javafx.concurrent.Task 并且它的 call() 方法一次生成所有像素。 Sometimes it works as expected and I can see how the image gets generated step by step, but sometimes the picture remains unfinished , as if the task would not run to the end.有时它按预期工作,我可以看到图像是如何一步一步生成的,但有时图片仍未完成,好像任务不会运行到最后。 Debugging shows that it always runs to the end, but the later PixelWriter.setColor() calls have no effect.调试显示它总是运行到最后,但后面的 PixelWriter.setColor() 调用没有效果。

I tried different approaches to fix it.我尝试了不同的方法来修复它。 Eg I added an onSucceeded event handler, and tried to cause the Canvas to "refresh" since I thought that it just somehow 'skips' its last refresh iteration.例如,我添加了一个 onSucceeded 事件处理程序,并试图使画布“刷新”,因为我认为它只是以某种方式“跳过”了它的最后一次刷新迭代。 No success.没有成功。 Strange, even coloring further pixels has no effect inside of the listener.奇怪的是,即使着色更多的像素在听者内部也没有影响。

I also tried using an AnimationTimer instead of utilizing the Task.我还尝试使用 AnimationTimer 而不是使用 Task。 It works, but my problem is that I can't predict how much pixels I am able to generate between its handle() calls.它有效,但我的问题是我无法预测在其 handle() 调用之间能够生成多少像素。 The generation algorithm complexity and though the CPU time needed for the pixel generation will change as the algorithm will evolve.生成算法的复杂性以及生成像素所需的 CPU 时间会随着算法的发展而变化。

My ideal target would be to spend all available CPU time generating pixels, but at the same time to be able to see the generation progress in detail (60 or even 30 FPS would be OK).我的理想目标是使用所有可用的 CPU 时间来生成像素,但同时能够详细查看生成进度(60 甚至 30 FPS 就可以了)。

Please help me out, what I'm doing wrong and what direction should I go given my target?请帮帮我,我做错了什么,我应该朝着什么方向前进?

It's true that you can't make use of PixelWriter in a thread other than the JavaFX application thread.确实,您不能在 JavaFX 应用程序线程以外的线程中使用 PixelWriter。 However, your Task can place the pixel data itself in a non-JavaFX value object like an IntBuffer , which the application thread can then pass to setPixels .但是,您的任务可以将像素数据本身放置在非 JavaFX 值对象中,例如IntBuffer ,然后应用程序线程可以将其传递给setPixels An example program:一个示例程序:

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

If you expect your image to be very large, and thus impractical to keep in memory all at once, you can write the Task to accept constructor arguments that allow it to generate only part of the image, then create multiple tasks to handle the pixel generation piece by piece.如果您希望您的图像非常大,因此一次保存在内存中是不切实际的,您可以编写 Task 以接受构造函数参数,使其仅生成图像的一部分,然后创建多个任务来处理像素生成一块一块。 Another option is having a single Task that repeatedly calls updateValue , but you would then have to create a custom value class that contains both the buffer and the rectangular area in the image to which that buffer should be applied.另一种选择是让单个 Task 重复调用updateValue ,但您必须创建一个自定义值类,其中包含缓冲区和图像中应应用该缓冲区的矩形区域。

Update:更新:

You've clarified you want progressive image rendering.您已经阐明您想要渐进式图像渲染。 A Task won't work for rapid updates, since changes to a Task value may be coalesced by JavaFX.任务不适用于快速更新,因为对任务值的更改可能会被 JavaFX 合并。 So it's back to fundamentals: make a Runnable that invokes setPixels in a Platform.runLater call, to ensure correct thread usage:所以它回到了基本原理:在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();
    }
}

You could invoke Platform.runLater for each individual pixel, but I suspect that would overwhelm the JavaFX application thread.可以为每个单独的像素调用 Platform.runLater,但我怀疑这会淹没 JavaFX 应用程序线程。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM