Java – Generate images pixel by pixel in JavaFX with visible progress

Generate images pixel by pixel in JavaFX with visible progress… here is a solution to the problem.

Generate images pixel by pixel in JavaFX with visible progress

I’m writing a JavaFX application that generates abstract pattern images pixel by pixel. The result should look like somewhat like this .
This 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 generates new pixels one after the other, populating the Canvas with the PixelWriter.setColor() method. It calculates the color of a new pixel based on some random number and a previously generated color.

If I run PixelGenerator on an application thread, the GUI will be blocked until the entire free space is filled and then I won’t see the full picture.

To avoid this, I had my PixelGenerator class extend javafx.concurrent.Task and its call() method generates all pixels at once. Sometimes it works as expected and I can see how the image is generated step by step, but sometimes the image remains unfinished as if the task doesn’t run to the end. The debug display runs until the end, but the subsequent PixelWriter.setColor() call has no effect.

I tried different methods to fix it. For example. I added an onSucceeded event handler and tried to get Canvas to “refresh” because I thought it just somehow “skipped” its last refresh iteration. It didn’t work out. Oddly enough, even coloring more pixels does not have any effect inside the listener.

I also tried using AnimationTimer instead of Task. It works, but my problem is that I can’t predict how many pixels I’ll be able to generate between its handle() calls. The complexity of the generation algorithm and the CPU time required to generate pixels vary as the algorithm evolves.

My ideal goal is to spend all available CPU time spawning pixels, but at the same time being able to see the build progress in detail (60 or even 30 FPS will do).

Please help me, what am I doing wrong and where should I go with my goals?

Solution

It is true that you cannot use PixelWriter in threads other than JavaFX application threads. However, your task can place the pixel data itself in a non-JavaFX value object, such as IntBuffer。 , which the application thread can then pass to 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();
    }
}

If you want your image to be so large that it is impractical to keep it in memory at once, you can write a task to accept constructor arguments that allow it to generate only a portion of the image, and then create multiple tasks to handle generating pixels one by one. Another option is to have a task repeatedly call updateValue But then you have to create a custom value class that contains the buffer and the rectangular area in the image to which the buffer should be applied.

Update:

You’ve clarified that you need progressive image rendering. Task is not suitable for quick updates because changes to Task values may be merged by JavaFX. So back to basics: make a Runnable, it’s in Platform.runLater Call the setPixels call to ensure proper thread usage:

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 can call Platform.runLater for each individual pixel, but I suspect this will overwhelm the JavaFX application thread.

Related Problems and Solutions