Java - Convert Image to black and white - fails with bright colors
Asked Answered
C

2

8

I'm attempting to convert an image to black and white only (not grey scale).

I've used this:

BufferedImage blackAndWhiteImage = new BufferedImage(
        dWidth.intValue(),
        dHeight.intValue(),
        BufferedImage.TYPE_BYTE_BINARY);
Graphics2D graphics = blackAndWhiteImage.createGraphics();
graphics.drawImage(colourImage, 0, 0, null);

return blackAndWhiteImage;

Everything fine, until I decided to try out brighter colors, like the Google logo for example:

googleLogo

and it came out with this:

goolgeLogoBroken1

Then I tried first to pass trough grey scale first using:

BufferedImage blackAndWhiteImage2 = new BufferedImage(
        dWidth.intValue(),
        dHeight.intValue(),
        BufferedImage.TYPE_USHORT_GRAY);

And it seemed to have saved the Blue color, but not the brightest (in this case yellow), and as you may see it decreased in quality:

goolgeLogoBroken1

Any suggestions are much appreciated; I believe what I am seeking for is to convert every colour to Black except White (which would be the background color), this is already done when applying TYPE_BYTE_BINARY removing the alpha channel.


EDIT: Maybe I have not explained very clear:

  • the final image has to have White background **1
  • every other color has to be converted to Black

**1 - there are some cases where the image is actually White on Black..which is annoying (whiteOnBlackExample) as it complicates a lot this process, and I will leave this later on, as priority now is to convert "normal" images.

What I did was, first strip out the alpha channel if it exists -> therefore convert the alpha channel to White; then convert every other color to Black

Coly answered 11/5, 2017 at 14:33 Comment(9)
the problem is that you're using only black and white, and not grey, so bright colors are displayed as white.Jacksnipe
See #23981054 you can do the same just be replacing non white pixels with blackEstuary
@Estuary Traversing and manipulating pixels seems to be slower than built-in methods. See #34688682Gymnasium
Are you limited to Swing/AWT or is JavaFX an option?Gymnasium
@Gymnasium I am actually using JavaFX Image and then convert to AWT Image back and forth. No limitations.Coly
@Estuary seems interesting, I'll try and recreate that structure and see how it behaves, I'll post back if there are resultsColy
You definitely need an algorithm with adjustable threshold. In general, algorithm should be as follows: pixel(x, y) < threshold ? white : black. Then just pick suitable threshold value. Since your original pixels are RGB you also might need to convert this value to an applicable one for compare with threshold. There are several ways reach this: Lightness method: (max(R, G, B) + min(R, G, B)) / 2; Average method: (R + G + B) / 3; Luminosity: 0.21 * R + 0.72 * G + 0.07 * BMona
@YevhenDanchenko seems interesting, could you elaborate with an example?Coly
This isn't the same question and doesn't use the superior calculation functions for the threshold as in Yevhen's comment, but the answer does demonstrate creating a writable image with pixel setting based upon a threshold: threshold based fill function. So, not really the example you are looking for, but perhaps helpful, and just ignore if not.Stace
G
5

If you use JavaFX you can use the ColorAdjust effect with brightness of -1 (minimum), which makes all the (non-white) colors black:

public class Main extends Application {

    Image image = new Image("https://i.sstatic.net/UPmqE.png");

    @Override
    public void start(Stage primaryStage) {
        ImageView colorView = new ImageView(image);
        ImageView bhView = new ImageView(image);

        ColorAdjust colorAdjust = new ColorAdjust();
        colorAdjust.setBrightness(-1);
        bhView.setEffect(colorAdjust);

        primaryStage.setScene(new Scene(new VBox(colorView, bhView)));
        primaryStage.show();
    }

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

enter image description here

These Effects are optimized so they are probably faster than what you would achieve by applying them manually.

Edit

Since your requirements are that

  1. any pixel which is not opaque should be transformed to white, and
  2. any pixel which is not white should be transformed to black,

the predesigned effects won't suit you as far as I can tell - they are too specific. You can do pixel by pixel manipulation:

WritableImage writableImage = new WritableImage(image.getPixelReader(), (int) image.getWidth(), (int) image.getHeight());
PixelWriter pixelWriter = writableImage.getPixelWriter();
PixelReader pixelReader = writableImage.getPixelReader();
for (int i = 0; i < writableImage.getHeight(); i++) {
    for (int j = 0; j < writableImage.getWidth(); j++) {
        Color c = pixelReader.getColor(j, i);
        if (c.getOpacity() < 1) {
            pixelWriter.setColor(j, i, Color.WHITE);
        }
        if (c.getRed() > 0 || c.getGreen() > 0 || c.getBlue() > 0) {
            pixelWriter.setColor(j, i, Color.BLACK);
        }
    }
}
ImageView imageView = new ImageView(writableImage);

enter image description here

Note that the order in which you apply the rules matter. A transparent non-white pixel will turn white if you apply 1 and then 2, but if you apply 2 and then 1 it will end up black. This is because the predefined WHITE and BLACK colors are opaque. You can manually set the red, green and blue values while not changing the alpha value instead. It all depends on your exact requirements.

Remember that due to lossy compression of some file formats you might not find true white in them at all, but a value which is close to true white and your eye won't be able to tell the difference.

Gymnasium answered 11/5, 2017 at 15:35 Comment(10)
I have tried this approach before, but it worked partially; try this link for the image, see what happens: i.imgur.com/FxNnW2M.jpgColy
@Coly The problem with that image is that it doesn't have a lot of true white, but many of the seemingly white pixels are close to white, like 0.07450980693101883, 0.07450980693101883, 0.07450980693101883 (RGB). So these pixels are set to black as well. Remember that different image format have different color spectrums and some are lossy. It comes down to what you would call white while it is not really white.Gymnasium
@Coly There's also the question of how you want to handle transparency.Gymnasium
you are correct, my mistake, I have assumed it's zero, that's why everything goes Black. Transparency/alpha has to be transformed to WhiteColy
@Coly So any pixel which is not completely opaque (alpha = 1) should be white, and any pixel which is not exactly white should be black? These are the conditions?Gymnasium
Yep, that should do the trick, I'm attempting at the moment to iterate though each pixel, but I am not that experienced with image processing.Coly
appreciate the effort; you are right, I am facing that issue now, with the colors close to White -> they turn to Black; I guess, some kind of tolerance is needed and round those bright (close to White) colors down to WhiteColy
@Coly You can do statistical analysis of the pixels and find the tolerance level you need on a per-image basis.Gymnasium
The JavaFX example is great because it maintains the alpha channel on the edges of the image, but modern JDKs don't ship with JavaFX any longer. Is there a way to do this with pure Java?Silkworm
@Silkworm By "pure Java" I assume you mean "only JDK". You can have a look at the AWT image package, there are tools for modifying images there. If you can't get something to work search the site and if there's no answer then ask a new question.Gymnasium
M
3

Here is the example from my comment. At first open the input image and create a new one for output.

BufferedImage myColorImage = ImageIO.read(fileInput);
BufferedImage myBWImage = new BufferedImage(myColorImage.getWidth(), myColorImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY);

Then iterate through all the pixels and compare rgb values with a threshold:

for (int x = 0; x < myColorImage.getWidth(); x++)
    for (int y = 0; y < myColorImage.getHeight(); y++)
        if (rgbToGray(myColorImage.getRGB(x, y), MODE.AVERAGE) > threshold)
            myBWImage.setRGB(x, y, 0);
        else
            myBWImage.setRGB(x, y, 0xffffff); 

Here is rgbToGray method implementation to compare with threshold:

private static int rgbToGray(int rgb, MODE mode) {
    // split rgb integer into R, G and B components
    int r = (rgb >> 16) & 0xff;
    int g = (rgb >> 8) & 0xff;
    int b = rgb & 0xff;
    int gray;
    // Select mode
    switch (mode) {
        case LIGHTNESS:
            gray = Math.round((Math.max(r, Math.max(g, b)) + Math.min(r, Math.min(g, b))) / 2);
            break;
        case LUMINOSITY:
            gray = Math.round(0.21f * r + 0.72f * g + 0.07f * b);
            break;
        case AVERAGE:
        default:
            gray = Math.round((r + g + b) / 3);
            break;
    }
    return gray;
}

An utility enum:

private enum MODE {
    LIGHTNESS, AVERAGE, LUMINOSITY
}

I got the following result:

Black and White converted

Note: for your google image even threshold = 1 is suitable, for other images you should pick another values from range [0..255]. For photos, most likely, more appropriate values are about 100-150. MODE also will affect the final result.

Mona answered 11/5, 2017 at 21:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.