Create an image-overlay mask in javafx

imageimageviewjavafxmaskoverlay

I'm trying to do a simple thing. I have a binary image and all I want is to overlay the binary image on a color image, but the white pixels in the binary image should be red, and the black transparent.
I'm quite used to JavaFx but I'm stuck with this one. I know I could achieve it by iterating through all pixels with a PixelReader, but I'm sure there is an easier way. I tried to use some sort of Blend-effect but no luck so far.
I think it should be similar to this:
How to Blend two Image in javaFX

Example of what I want to achieve

I came up with this:
Image image = new Image("/circle.jpg", false);
ImageView iv = new ImageView(image);

Image mask = new Image("/mask.jpg", false);
ImageView ivMask = new ImageView(mask);

Rectangle r = new Rectangle(mask.getWidth(), mask.getHeight());
r.setFill(Color.RED);

r.setBlendMode(BlendMode.MULTIPLY); // sets the white area red

Group g = new Group(ivMask, r);   // sets the white area red


// this is not working as expected
iv.setBlendMode(BlendMode.DIFFERENCE);

Group g2 = new Group(iv, g);

Thanks for any suggestions!
If you think, processing pixel-wise is faster than just creating an overlay, please let me know.

Solution by pixel-reader would be:

Pane root = new Pane();

// read the underlaying image
root.getChildren().add(new ImageView(new Image("/src.jpg")));

Image mask = new Image("/mask.jpg");
PixelReader pixelReader = mask.getPixelReader();

Canvas resultCanvas = new Canvas();
root.getChildren().add(resultCanvas);

GraphicsContext resultLayer = resultCanvas.getGraphicsContext2D();
for (int y = 0; y < mask.getHeight(); y++) {
  for (int x = 0; x < mask.getWidth(); x++) {
    if( pixelReader.getColor(x, y).equals(Color.WHITE) ){
      resultLayer.fillRect(x, y, 1.0, 1.0);
    }
  }
}   

Cheers!

Best Answer

What you are Doing Wrong

The difference operator isn't a binary difference based on whether a pixel is set instead it is a difference in the RGB components, so instead of a solid red overlay, you will get a multi-colored overlay because the difference in the RGB components of the blended images differs between pixels.

Background

You are trying to do something similar to a masked bit-blit operation with blend modes (basically, an OR then an AND of pixel data based on a white on black mask). It is possible though a little tricky with the built-in blends in JavaFX 8.

You could create a feature request for additional support in the blend API for bit-blt style basics as well as exposing a full porter duff compositing implementation like Swing has so that the underlying blend engine has a bit more power and is possibly a little easier to use.

Alternatives

The preferred thing to do would be to pre-process your mask in an image editor like photoshop to convert the black part to an alpha channel - then you can just layer your mask on top of your original and the default compositing mode will take of it.

To make your alpha enabled mask red, you could just use mask.setBlendMode(BlendMode.RED) (or you could pre-color the mask in an image editor before using it in your program).

Another alternative is the PixelReader solution you have in your question (which I think is fine if you are unable to pre-convert your mask to use alpha).

The blend operations can be hardware accelerated on appropriate hardware. So potentially using a blend could be faster if you are doing it very often (but you would have to have many blends being run very quickly on large images to really notice any kind of performance difference).

Sample Solution Using Blend Operations

Sample Output

blended

Input Images

original.jpg

original

stencil.jpg

stencil

Code

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.effect.BlendMode;
import javafx.scene.image.*;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class Blended extends Application {
    @Override
    public void start(Stage stage) {
        Image original = new Image(
            getClass().getResourceAsStream("original.jpg")
        );

        Image stencil = new Image(
            getClass().getResourceAsStream("stencil.jpg")
        );

        // first invert the stencil so that it is black on white rather than white on black.
        Rectangle whiteRect = new Rectangle(stencil.getWidth(), stencil.getHeight());
        whiteRect.setFill(Color.WHITE);
        whiteRect.setBlendMode(BlendMode.DIFFERENCE);

        Group inverted = new Group(
                new ImageView(stencil),
                whiteRect
        );

        // overlay the black portions of the inverted mask onto the image.
        inverted.setBlendMode(BlendMode.MULTIPLY);
        Group overlaidBlack = new Group(
                new ImageView(original),
                inverted
        );

        // create a new mask with a red tint (red on black).
        Rectangle redRect = new Rectangle(stencil.getWidth(), stencil.getHeight());
        redRect.setFill(Color.RED);
        redRect.setBlendMode(BlendMode.MULTIPLY);

        Group redStencil = new Group(
                new ImageView(stencil),
                redRect
        );

        // overlay the red mask on to the image.
        redStencil.setBlendMode(BlendMode.ADD);
        Group overlaidRed = new Group(
                overlaidBlack,
                redStencil
        );

        // display the original, composite image and stencil.
        HBox layout = new HBox(10);
        layout.getChildren().addAll(
                new ImageView(original),
                overlaidRed,
                new ImageView(stencil)
        );
        layout.setPadding(new Insets(10));
        stage.setScene(new Scene(layout));
        stage.show();
    }

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