2

Recently, our teacher gave us the task to convert a colorful image to a 1-bit image using Java. After a little experimentation I had the following result:

BufferedImage image = ...
for (int y = 0; y < image.getHeight(); y++) {
  for (int x = 0; x < image.getWidth(); x++) {
    int clr = image.getRGB(x, y);
    int  r   = (clr & 0x00ff0000) >> 16;
    int  g = (clr & 0x0000ff00) >> 8;
    int  b  =  clr & 0x000000ff;
                
    double mono = 0.2126*r + 0.7152*g + 0.0722*b;
                
    int c = mono < 128 ? 1 : 0;
    
    //Adding to image buffer
    buffer.add(c);
  }
}

Well, it works but a lot of details are unfortunately lost. Here is a comparison:

Original:

Original

Output:

Converted Output

What I want: (HQ: https://i.stack.imgur.com/vlEAE.png)

My goal

I was considering adding dithering to my converter, but I haven't found a working way yet, let alone any pseudo code.

Can anyone help me?

Edit:

So I created a DitheringUtils-class:

import java.awt.Color;
import java.awt.image.BufferedImage;

public class DitheringUtils {
    
    public static BufferedImage dithering(BufferedImage image) {
        Color3i[] palette = new Color3i[] {
            new Color3i(0, 0, 0),
            new Color3i(255, 255, 255)
        };
        
        int width = image.getWidth();
        int height = image.getHeight();
        
        Color3i[][] buffer = new Color3i[height][width];
        
        for(int y=0;y<height;y++) {
            for(int x=0;x<width;x++) {
                buffer[y][x] = new Color3i(image.getRGB(x, y));
            }
        }
        
        for(int y=0; y<image.getHeight();y++) {
            for(int x=0; x<image.getWidth();x++) {
                Color3i old = buffer[y][x];
                Color3i nem = findClosestPaletteColor(old, palette);
                image.setRGB(x, y, nem.toColor().getRGB());
                
                Color3i error = old.sub(nem);
                
                if (x+1 < width)         buffer[y  ][x+1] = buffer[y  ][x+1].add(error.mul(7./16));
                if (x-1>=0 && y+1<height) buffer[y+1][x-1] = buffer[y+1][x-1].add(error.mul(3./16));
                if (y+1 < height)         buffer[y+1][x  ] = buffer[y+1][x  ].add(error.mul(5./16));
                if (x+1<width && y+1<height)  buffer[y+1][x+1] = buffer[y+1][x+1].add(error.mul(1./16));
            }
        }
        
        return image;
    }

    private static Color3i findClosestPaletteColor(Color3i match, Color3i[] palette) {
        Color3i closest = palette[0];
        
        for(Color3i color : palette) {
            if(color.diff(match) < closest.diff(match)) {
                closest = color;
            }
        }
        
        return closest;
    }
}

class Color3i {
    
    private int r, g, b;

    public Color3i(int c) {
        Color color = new Color(c);
        this.r = color.getRed();
        this.g = color.getGreen();
        this.b = color.getBlue();
    }
    
    public Color3i(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    public Color3i add(Color3i o) {
        return new Color3i(r + o.r, g + o.g, b + o.b);
    }
    
    public Color3i sub(Color3i o) {
        return new Color3i(r - o.r, g - o.g, b - o.b);
    }
    
    public Color3i mul(double d) {
        return new Color3i((int) (d * r), (int) (d * g), (int) (d * b));
    }
    
    public int diff(Color3i o) {
        return Math.abs(r - o.r) +  Math.abs(g - o.g) +  Math.abs(b - o.b);
    }

    public int toRGB() {
        return toColor().getRGB();
    }
    
    public Color toColor() {
        return new Color(clamp(r), clamp(g), clamp(b));
    }
    
    public int clamp(int c) {
        return Math.max(0, Math.min(255, c));
    }
}

And changed my function to this:

for (int y = 0; y < dithImage.getHeight(); ++y) {
    for (int x = 0; x < dithImage.getWidth(); ++x) {
        final int clr = dithImage.getRGB(x, y);
        final int r = (clr & 0xFF0000) >> 16;
        final int g = (clr & 0xFF00) >> 8;
        final int b = clr & 0xFF;
                
        if(382.5>(r+g+b)) {
            buffer.add(0);
        } else {
            buffer.add(1);
        }
    }
}

But the output ends up looking... strange?

Trippy Output

I really don't get why there are such waves.

  • Accordingly to [Wikipedia about that algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering), you can use it with a 1-bit depth. EDIT : There even is an example to convert 32-bit to 16-bit at the end of the page. – Lutzi Jan 29 '21 at 11:41
  • only with 2 colors, what to expect, for me at quick view looks fine. You could try with a image composed of shaded of gray (from pure white to pure black) and see how it's transformed. Means if conversion is proportional half should be white, half black (other conversion could be useful only if picture is not well "balanced"). – Traian GEICU Jan 29 '21 at 12:52
  • I added a "goal" image so you can see what I want. Sadly due to the compression, you can't see any Dithering patterns, but I hope it can still help. – PugsAreCute Jan 29 '21 at 15:36
  • Does this answer your question? [convert a RGB image to grayscale Image reducing the memory in java](https://stackoverflow.com/questions/9131678/convert-a-rgb-image-to-grayscale-image-reducing-the-memory-in-java) – Nobody Jan 29 '21 at 16:00
  • No, I do not want to convert the image to grayscale. – PugsAreCute Jan 29 '21 at 16:15
  • I think the code in the sample will work fine for you, you just need to change the palette to a two-color palette with only [0,0,0 and 255,255,255] and you are basically done. Why do you think they have a "different goal"? Error diffusion should work with any number of colors in the lookup table. – Harald K Jan 29 '21 at 20:20
  • Oh, I didn't knew that. I'll take a look at that and edit the question (Or perhaps add an answer) when I'm done. Thanks – PugsAreCute Jan 29 '21 at 20:45
  • I have posted an answer by the way. – PugsAreCute Jan 31 '21 at 11:29

2 Answers2

1

I finally got it working! I improved the diff function and changed if(382.5>(r+g+b)) to if(765==(r+g+b)).

My DitheringUtils-class:

import java.awt.Color;
import java.awt.image.BufferedImage;

public class DitheringUtils {
    
    public static BufferedImage dithering(BufferedImage image) {
        Color3i[] palette = new Color3i[] {
            new Color3i(0, 0, 0),
            new Color3i(255, 255, 255)
        };
        
        int width = image.getWidth();
        int height = image.getHeight();
        
        Color3i[][] buffer = new Color3i[height][width];
        
        for(int y=0;y<height;y++) {
            for(int x=0;x<width;x++) {
                buffer[y][x] = new Color3i(image.getRGB(x, y));
            }
        }
        
        for(int y=0; y<image.getHeight();y++) {
            for(int x=0; x<image.getWidth();x++) {
                Color3i old = buffer[y][x];
                Color3i nem = findClosestPaletteColor(old, palette);
                image.setRGB(x, y, nem.toColor().getRGB());
                
                Color3i error = old.sub(nem);
                
                if (x+1 < width)         buffer[y  ][x+1] = buffer[y  ][x+1].add(error.mul(7./16));
                if (x-1>=0 && y+1<height) buffer[y+1][x-1] = buffer[y+1][x-1].add(error.mul(3./16));
                if (y+1 < height)         buffer[y+1][x  ] = buffer[y+1][x  ].add(error.mul(5./16));
                if (x+1<width && y+1<height)  buffer[y+1][x+1] = buffer[y+1][x+1].add(error.mul(1./16));
            }
        }
        
        return image;
    }

    private static Color3i findClosestPaletteColor(Color3i match, Color3i[] palette) {
        Color3i closest = palette[0];
        
        for(Color3i color : palette) {
            if(color.diff(match) < closest.diff(match)) {
                closest = color;
            }
        }
        
        return closest;
    }
}

class Color3i {
    
    private int r, g, b;

    public Color3i(int c) {
        Color color = new Color(c);
        this.r = color.getRed();
        this.g = color.getGreen();
        this.b = color.getBlue();
    }
    
    public Color3i(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    public Color3i add(Color3i o) {
        return new Color3i(r + o.r, g + o.g, b + o.b);
    }
    
    public Color3i sub(Color3i o) {
        return new Color3i(r - o.r, g - o.g, b - o.b);
    }
    
    public Color3i mul(double d) {
        return new Color3i((int) (d * r), (int) (d * g), (int) (d * b));
    }
    
    public int diff(Color3i o) {
        int Rdiff = o.r - r;
        int Gdiff = o.g - g;
        int Bdiff = o.b - b;
        int distanceSquared = Rdiff * Rdiff + Gdiff * Gdiff + Bdiff * Bdiff;
        return distanceSquared;
    }

    public int toRGB() {
        return toColor().getRGB();
    }
    
    public Color toColor() {
        return new Color(clamp(r), clamp(g), clamp(b));
    }
    
    public int clamp(int c) {
        return Math.max(0, Math.min(255, c));
    }
}

The final writing function:

for (int y = 0; y < dithImage.getHeight(); ++y) {
    for (int x = 0; x < dithImage.getWidth(); ++x) {
        final int clr = dithImage.getRGB(x, y);
        final int r = (clr & 0xFF0000) >> 16;
        final int g = (clr & 0xFF00) >> 8;
        final int b = clr & 0xFF;
                
        if(765==(r+g+b)) {
            buffer.add(0);
        } else {
            buffer.add(1);
        }
    }
}

Thanks everyone!

0
BufferedImage image = ...
for (int y = 0; y < image.getHeight(); y++) {
  for (int x = 0; x < image.getWidth(); x++) {
    Color color = new Color(image.getRGB(x, y));
    int red = color.getRed();
    int green = color.getGreen();
    int blue = color.getBlue();
    
    
    int mono = (red+green+blue)/255;
    
    //Adding to image buffer
    int col = (0 << 24) | (mono << 16) | (mono << 8) | mono;
    
    image.setRGB(x,y,col);
  }
}

Try this out

What you were doing wrong was, instead of trying to convert the picture to grayscale you were trying to convert to black and white.

Nobody
  • 99
  • 1
  • 9
  • I'm not sure if it's my fault, by for some reason my image now only consists of zeros. What is happening? – PugsAreCute Jan 29 '21 at 16:14
  • No I made a mistake, sorry, let me fix it – Nobody Jan 29 '21 at 16:25
  • @PugsAreCute Ive edited the question, check it once – Nobody Jan 29 '21 at 16:32
  • @PugsAreCute did you edit the question with noise in the last picture? If that is the case then I would edit it once more, before that once check it this works – Nobody Jan 29 '21 at 16:34
  • Well it produces a gray scale image, but my converter was supposed to produce a monochrome image which consists of only 2 colors (black and white). But thank you anyway. – PugsAreCute Jan 29 '21 at 16:53
  • But according to what you have shown, it is a grayscale picture, not a B/W one, if you want BW youve done it correctly the first time, except adding the noise. Is it noise you want? – Nobody Jan 29 '21 at 17:06
  • I'm really sorry that it looks like it was grayscale. Actually it looks like this: https://i.stack.imgur.com/vlEAE.png – PugsAreCute Jan 29 '21 at 17:09
  • This is actually grayscale though – Nobody Jan 29 '21 at 17:23
  • It's **monochrome**. It only consists of 2 colors. 0 and 1. Black and White. It doesn't have any colors in between so it's not **grayscale**. – PugsAreCute Jan 29 '21 at 17:37
  • It is'nt monochrome, look at it properly, the orange candies have a value between black and white – Nobody Jan 29 '21 at 18:06
  • I think you get what I want. – PugsAreCute Jan 29 '21 at 19:57