2

I have a pretty specific problem with javaFx's ColorAdjust effect, I'm trying to apply a grayscale filter on an image, I'm using a ColorAdjust effect and setting the saturation Here is a reproducible example of what I'm trying to do

public class App extends Application {
    @Override
    public void start(Stage ps) {
        Pane root = new Pane();
        root.setMinSize(300, 300);
        
        root.setStyle("-fx-background-color: #40444b;");
        
        ImageView view = new ImageView(new Image("https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png"));
        view.setTranslateX(5);
        view.setTranslateY(5);
        view.setEffect(new ColorAdjust(0, -1, 0, 0));
        
        root.getChildren().add(view);
        
        ps.setScene(new Scene(root));

        ps.show();
    }
}

now this piece of code does exactly what it's supposed to do, but I'm not satisfied with the result, I want a grayscale filter that behaves similarly to the web css grayscale filter, which produces much better results for my use case :

<html>

<body style="background-color: #40444b;">
    <img src="https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png" style="filter: grayscale(100);">
</body>

</html>

javafx grayscale web grayscale

[ Left is javafx, Right is Web (firefox) ]

I know the difference isn't a lot but it's crucial for my use case and I would appreciate if anyone has better ideas to get similar results to the web version of the grayscale filter

SDIDSA
  • 894
  • 10
  • 19

3 Answers3

2

IMO, the update you provided with the PixelWriter solution is probably the best you can do, and you should edit the question to remove the update and place the update as an answer.

I realize that the process in the update is slightly more involved than using a ColorAdjust effect.

But I don't think you can do what you want using ColorAdjust.

The grayscale function adjusts colors via RGB multiplication:

double gray = 0.21 * red + 0.71 * green + 0.07 * blue; 
return Color.color(gray, gray, gray, opacity);

ColorAdjust adjusts via HSB.

I could be wrong, but I don't think you can provide the HSB params to ColorAdjust to perform the equivalent RGB multiplication.

Potential higher performance solutions

Only spend time on this if you have a performance issue and must have better performance.

You could create your own GrayscaleEffect using the effect pipeline that uses the hardware acceleration and that might be something to look into if you need high performance. But that process is really complicated, it is not easy to create a hardware accelerated effect, there is no documentation for it and to do so you would need to study the existing effect code in the openjfx repository and adapt that for your purposes.

If the hardware accelerated implementation is too tricky, you could use the fork join pool to do the adjustments in parallel for speed up, operating on the byte buffer directly rather than using color objects. The algorithm you need to apply for each pixel is given by the grayscale function implementation. There is a partial example in the answers to:

  • javafx argb to grayscale conversion

    int pixel = pixelReader.getArgb(x, y);
    
    int alpha = ((pixel >> 24) & 0xff);
    int red = ((pixel >> 16) & 0xff);
    int green = ((pixel >> 8) & 0xff);
    int blue = (pixel & 0xff);
    
    int grayLevel = (int) (0.2162 * red + 0.7152 * green + 0.0722 * blue);
    int gray = (alpha << 24) + (grayLevel << 16) + (grayLevel << 8) + grayLevel;
    
    grayImage.getPixelWriter().setArgb(x, y, gray);
    

But that doesn't use the byte buffer and it may be more efficient to work directly on the bytebuffer from the pixel reader/writer, though that is a little trickier.

jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • since I'm going to be applying the effect on relatively small images of ~72x72px i believe the pixelWriter solution will be sufficient in terms of performance, I'm mostly concerned about the *cleanliness* of the solution, appreciate the explanation about the difference between The grayscale function and grayscale via ColorAdjust. – SDIDSA Jan 24 '22 at 07:37
2

I might have suggested ColorConvertOp, shown here, but it's marginally slower than PixelWriter and not especially more versatile.

As an illustration of @jewelsea's key insight, note the low contrast between the hand and face in the unadjusted image. Manipulation via ColorAdjust can alter the properties of the image as a whole. Unfortunately, it can't alter the relative contrast of image areas, as shown in your preferred result.

Nevertheless, the example will let you adjust the properties empirically in case you find an acceptable result.

image

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/** @see https://stackoverflow.com/q/70821638/230513 */
public class GrayApp extends Application {

    private final Insets insets = new Insets(10);

    private Slider createSlider(DoubleProperty dp) {
        Slider s = new Slider(-1, 1, dp.get());
        s.setBlockIncrement(0.1);
        s.setTooltip(new Tooltip(dp.getName()));
        dp.bind(s.valueProperty());
        VBox.setMargin(s, insets);
        return s;
    }

    @Override
    public void start(Stage stage) {
        ImageView view = new ImageView(new Image(
            "https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png"));
        ColorAdjust adjust = new ColorAdjust(0, -1, 0, 0);
        view.setEffect(adjust);

        Slider hSlider = createSlider(adjust.hueProperty());
        Slider sSlider = createSlider(adjust.saturationProperty());
        Slider bSlider = createSlider(adjust.brightnessProperty());
        Slider cSlider = createSlider(adjust.contrastProperty());

        VBox root = new VBox();
        root.setPadding(insets);
        VBox.setMargin(view, insets);
        root.setAlignment(Pos.CENTER);
        root.setStyle("-fx-background-color: #808080;");
        root.getChildren().addAll(view, hSlider, sSlider, bSlider, cSlider);
        stage.setScene(new Scene(root));
        stage.show();
    }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • Interesting approach, finding a combination of Saturation/Brightness/Contrast values that gives better results than just adjusting the saturation would be a bit cleaner than keeping 2 images – SDIDSA Jan 24 '22 at 06:52
2

manually converting the image to grayscale using a WritableImage and Color.grayscale() gives better results but it would complicate the process of switching between color and grayscale :

public class App extends Application {

    @Override
    public void start(Stage ps) {
        Pane root = new Pane();
        root.setMinSize(300, 300);

        root.setStyle("-fx-background-color: #40444b;");

        Image image = new Image("https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png");
        
        ImageView view = new ImageView(grayScale(image));
        
        view.setTranslateX(5);
        view.setTranslateY(5);

        root.getChildren().add(view);

        ps.setScene(new Scene(root));
        ps.setTitle("javafx grayscale test");
        ps.show();
    }

    private static Image grayScale(Image img) {
        WritableImage res = new WritableImage((int) img.getWidth(), (int) img.getHeight());

        PixelReader pr = img.getPixelReader();
        PixelWriter pw = res.getPixelWriter();
        for (int y = 0; y < img.getHeight(); y++) {
            for (int x = 0; x < img.getWidth(); x++) {
                pw.setColor(x, y, pr.getColor(x, y).grayscale());
            }
        }
        return res;
    }
}

enter image description here

you have the choice of saving the filtered image or generating it every time, depending on whether that trade-off (increased memory usage for increased performance) is worthwhile.

SDIDSA
  • 894
  • 10
  • 19
  • 1
    As warranted, [profile](http://stackoverflow.com/q/2064427/230513; optionally, allow for an [LRU cache](https://stackoverflow.com/q/224868/230513). – trashgod Jan 24 '22 at 19:10