PixelReader/PixelWriter

This entry is the translation of 4 Dec. entry of JavaFX Advent Calendar.

Recently, I had several times to give presentations about JaaFX 2.2.
I talked some features in the presentations, and showed demonstrations.
Today, I introduce one of the the features: PixelReader/PixelWriter,
and show a demonstation that was made for these presentation.

JavaFX 2.2 began support of bitmap images. The main classes that deal with
bitmap images are PixelReader class and PixelWriter class.

PixelReader reads pixels from a Image object, and PixelWriter writes pixels to a
WritableImage object. WritableImage class is a subclass of Image class. It's similar
to BufferedImage class of Java 2D.

PixelReader/PixelWriter defined some API, however I start reading 1 pixel and
writing 1 pixel.

For example, copying a image.

    Image src = new Image(...);
    PixelReader reader = src.getPixelReader();

    int width = (int)src.getWidth();
    int height = (int)src.getHeight();

    WritableImage dest = new WritableImage(width, height);
    PixelWriter writer = dest.getPixelWriter();

    for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++) {
            // reading a pixel from src image,
            // then writing a pixel to dest image
            Color color = reader.getColor(x, y);
            writer.setColor(x, y, color);

            // this way is also OK
//            int argb = reader.getArgb(x, y);
//            writer.setArgb(x, y, argb);
        }
    }

getPixelReader method of Image class is used for acquiring PixelReader object.

WritableImage is generated with width and height. After that, to acquire
PixelWriter call get PixelWriter method.

PixelReader#getColor(x, y) reads (x, y) pixel, and the return value type of
getColor method is Color class. In a similar way, PixelWriter#setColor(x, y, color)
writes (x, y) pixel to the color.

Similar method getArgb is reading a pixel, and the return type is int.
Top 8 bit of return value indicates alpha, next 8 bit indicates red, next 8 bit green,
and then blue.

This sample is the simplest use of PixelReader/PixelWriter, then I try little bit
complicated sample. That is image blur.

In the sample, I use box blur. It's the simplest blur method.

How to blur is reading both neighbor pixels around target pixel, averaging these
pixels color, and then write the target pixel with the average color.

If the neighbor is square, then it is box blur.

    private void blur() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double red = 0;
                double green = 0;
                double blue = 0;
                double alpha = 0;
                int count = 0;

                // reading neighbor pixels
                for (int i = -kernelSize; i <= kernelSize; i++) {
                    for (int j = -kernelSize; j <= kernelSize; j++) {
                        if (x + i < 0 || x + i >= width 
                           || y + j < 0 || y + j >= height) {
                            continue;
                        }
                        Color color = reader.getColor(x + i, y + j);
                        red += color.getRed();
                        green += color.getGreen();
                        blue += color.getBlue();
                        alpha += color.getOpacity();
                        count++;
                    }
                }
                Color blurColor = Color.color(red / count, 
                                              green / count, 
                                              blue / count, 
                                              alpha / count);
                writer.setColor(x, y, blurColor);
            }
        }
    }

Reading pixels around edge is little bit hard, because some pinxels of neighbor don't exist.
In the case, the system reads all pixels that is able to read, and then average them.

Moreover, I use Slider class to make the quantity of blur.

        slider.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                DoubleProperty value = (DoubleProperty)o;
                int intValue = (int) value.get();
                if (intValue != kernelSize) {
                    kernelSize = intValue;
                    blur();
                }
            }
        });

A the type of value property of Slider class si double, so the system cast it to int.

The result of execution is the following image:

But, this way is BAD parformance! Especially, when kernelSize become big, the performance
goes down prominently.

The reason is, you know, that the times of reading pixel become huge. Therefore, I should
reduce reading pixels.

How? It's bulk reading.

Bulk pixel reading

Decreasing the number of reading pixels, we can use bulk reading methods. There are three overloading getPixels method. The difference is how to store pixel data: byte, int and Buffer.

I used int in this entry.

One of arguments of PixelReader#getPixels is WritablePixelFormat. Because WritablePixelFormat is abstract class, we can't generate WritablePixelFormat object directly. Therefore, static methods defined by PixelFormat that is superclass of WritablePixelFormat are used for gettting WritablePixelFormat object.

getIntArgbInstance method and getIntArgbPreInstancecorrespond to int and I used getIntArgbInstance in this entry.

getPixes argumets are x, y, width, height, writablePixelFormat, buffer, offset, and scalingStride. Type of buffer is int[], and offset means offset of buffer. The scalingStride shows size of a line to next line.

I rewrote blur method using getPixels and blur2 method is showed blow:

    private void blur2() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int centerX = x - kernelSize;
                int centerY = y - kernelSize;
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                if (centerX < 0) {
                    centerX = 0;
                    kernelWidth = x + kernelSize;
                } else if (x + kernelSize >= width) {
                    kernelWidth = width - centerX;
                }

                if (centerY < 0) {
                    centerY = 0;
                    kernelHeight = y + kernelSize;
                } else if (y + kernelSize >= height) {
                    kernelHeight = height - centerY;
                }

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(centerX, centerY, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }

                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                writer.setArgb(x, y, blurColor);
            }
        }
    }


Because region edges blur process is complicated, lenth of code became longer. However, the performance was improved.

For comparing performance, I used radio buttons to select 1 pixel reader or bulk reading.

Bulk pixel writing

PixelWriter also defines bulk writing methods, in the same way as bulk reading of PixelReader.

So, let's try tessellate image.

The application caliculates an average of neighbor pixels in the same way as blur, but set the average color to all neighbor pixels.

There are four overloaded PixelWriter#setPixels methods: byte, int, ByteBuffer and PixelReader for pixel data. And I used int[] version of setPixels here.

    private void mosaic() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = kernelSize; 
             x < width - kernelSize * 2; 
             x += kernelSize * 2 + 1) {
            for (int y = kernelSize; 
                 y < height - kernelSize * 2; 
                 y += kernelSize * 2 + 1) {

                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                Arrays.fill(buffer, blurColor);
                writer.setPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);
            }
        }
    }

The result is shown below:

All sourcecode is shown below:

import java.nio.IntBuffer;
import java.util.Arrays;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class WritableImageDemo extends Application {

    private Image src;
    private WritableImage dest;
    private int kernelSize = 1;
    private int width;
    private int height;
    
    private RadioButton blurButton;
    private RadioButton blur2Button;
    private RadioButton mosaicButton;

    @Override
    public void start(Stage stage) {

        AnchorPane root = new AnchorPane();

        initImage(root);

        Scene scene = new Scene(root);

        stage.setTitle("WritableImage Demo");
        stage.setResizable(false);
        stage.setScene(scene);
        stage.show();
    }

    private void initImage(AnchorPane root) {
        src = new Image("macaron.jpg");
        ImageView srcView = new ImageView(src);
        root.getChildren().add(srcView);
        AnchorPane.setTopAnchor(srcView, 0.0);
        AnchorPane.setLeftAnchor(srcView, 0.0);

        width = (int) src.getWidth();
        height = (int) src.getHeight();
        root.setPrefSize(width * 2.0, height + 50);

        dest = new WritableImage(width, height);
        ImageView destView = new ImageView(dest);
        destView.setTranslateX(width);
        root.getChildren().add(destView);
        AnchorPane.setTopAnchor(destView, 0.0);
        AnchorPane.setRightAnchor(destView, (double) width);

        Slider slider = new Slider(0, 10, kernelSize);
        slider.setPrefSize(width, 50);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setSnapToTicks(true);
        slider.setMajorTickUnit(1.0);
        slider.setMinorTickCount(0);

        slider.valueProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable o) {
                DoubleProperty value = (DoubleProperty) o;
                int intValue = (int) value.get();
                if (intValue != kernelSize) {
                    kernelSize = intValue;
                    if (blurButton.isSelected()) {
                        blur();
                    } else if (blur2Button.isSelected()) {
                        blur2();
                    } else {
                        mosaic();
                    }
                }
            }
        });

        root.getChildren().add(slider);
        AnchorPane.setBottomAnchor(slider, 0.0);
        AnchorPane.setRightAnchor(slider, 10.0);

        HBox hbox = new HBox(10);
        hbox.setAlignment(Pos.CENTER);
        hbox.setPrefWidth(width);
        hbox.setPrefHeight(50);
        root.getChildren().add(hbox);
        AnchorPane.setBottomAnchor(hbox, 0.0);
        AnchorPane.setLeftAnchor(hbox, 10.0);

        ToggleGroup group = new ToggleGroup();
        blurButton = new RadioButton("Blur");
        blurButton.setToggleGroup(group);
        blurButton.setSelected(true);
        hbox.getChildren().add(blurButton);
        blur2Button = new RadioButton("Blur2");
        blur2Button.setToggleGroup(group);
        hbox.getChildren().add(blur2Button);
        mosaicButton = new RadioButton("Mosaic");
        mosaicButton.setToggleGroup(group);
        hbox.getChildren().add(mosaicButton);

        blur();
    }

    private void blur() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                double red = 0;
                double green = 0;
                double blue = 0;
                double alpha = 0;
                int count = 0;
                for (int i = -kernelSize; i <= kernelSize; i++) {
                    for (int j = -kernelSize; j <= kernelSize; j++) {
                        if (x + i < 0 || x + i >= width 
                           || y + j < 0 || y + j >= height) {
                            continue;
                        }
                        Color color = reader.getColor(x + i, y + j);
                        red += color.getRed();
                        green += color.getGreen();
                        blue += color.getBlue();
                        alpha += color.getOpacity();
                        count++;
                    }
                }
                Color blurColor = Color.color(red / count, 
                                              green / count, 
                                              blue / count, 
                                              alpha / count);
                writer.setColor(x, y, blurColor);
            }
        }
    }

    private void blur2() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int centerX = x - kernelSize;
                int centerY = y - kernelSize;
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                if (centerX < 0) {
                    centerX = 0;
                    kernelWidth = x + kernelSize;
                } else if (x + kernelSize >= width) {
                    kernelWidth = width - centerX;
                }

                if (centerY < 0) {
                    centerY = 0;
                    kernelHeight = y + kernelSize;
                } else if (y + kernelSize >= height) {
                    kernelHeight = height - centerY;
                }

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(centerX, centerY, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                writer.setArgb(x, y, blurColor);
            }
        }
    }

    private void mosaic() {
        PixelReader reader = src.getPixelReader();
        PixelWriter writer = dest.getPixelWriter();
        WritablePixelFormat<IntBuffer> format 
            = WritablePixelFormat.getIntArgbInstance();

        for (int x = kernelSize; x < width - kernelSize * 2; x += kernelSize * 2 + 1) {
            for (int y = kernelSize; y < height - kernelSize * 2; y += kernelSize * 2 + 1) {
                int kernelWidth = kernelSize * 2 + 1;
                int kernelHeight = kernelSize * 2 + 1;

                int[] buffer = new int[kernelWidth * kernelHeight];
                reader.getPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);

                int alpha = 0;
                int red = 0;
                int green = 0;
                int blue = 0;

                for (int color : buffer) {
                    alpha += (color >>> 24);
                    red += (color >>> 16 & 0xFF);
                    green += (color >>> 8 & 0xFF);
                    blue += (color & 0xFF);
                }
                alpha = alpha / kernelWidth / kernelHeight;
                red = red / kernelWidth / kernelHeight;
                green = green / kernelWidth / kernelHeight;
                blue = blue / kernelWidth / kernelHeight;

                int blurColor = (alpha << 24) 
                              + (red << 16) 
                              + (green << 8) 
                              + blue;
                Arrays.fill(buffer, blurColor);
                writer.setPixels(x, y, 
                                 kernelWidth, kernelHeight, 
                                 format, buffer, 0, kernelWidth);
            }
        }
    }

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