How to implement 1-Bit Dithering using Java?

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.

java
image
dithering
asked on Stack Overflow Jan 29, 2021 by PugsAreCute • edited Jan 29, 2021 by PugsAreCute

2 Answers

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!

answered on Stack Overflow Jan 29, 2021 by PugsAreCute
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.

answered on Stack Overflow Jan 29, 2021 by Nobody • edited Jan 29, 2021 by Nobody

User contributions licensed under CC BY-SA 3.0