javaimagedithering

How to implement 1-Bit Dithering using Java?


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.sstatic.net/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.


Solution

  • 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!