Skip to content

Commit 5ad8df9

Browse files
committed
Dithering
1 parent a09919a commit 5ad8df9

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = 'com.labelzoom.api'
8-
version = '1.0.12'
8+
version = '1.0.13'
99

1010
java {
1111
sourceCompatibility = JavaVersion.VERSION_1_8
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.labelzoom.api.util;
2+
3+
import java.awt.*;
4+
import java.awt.image.BufferedImage;
5+
import java.awt.image.ColorModel;
6+
import java.awt.image.WritableRaster;
7+
8+
public class ImageUtils
9+
{
10+
private static final float ERROR_ONE_SIXTEENTH = 1f/16f;
11+
private static final float ERROR_THREE_SIXTEENTHS = 3f/16f;
12+
private static final float ERROR_FIVE_SIXTEENTHS = 5f/16f;
13+
private static final float ERROR_SEVEN_SIXTEENTHS = 7f/16f;
14+
private static final int BLACK = Color.BLACK.getRGB();
15+
private static final int WHITE = Color.WHITE.getRGB();
16+
17+
/**
18+
* Thanks, ChatGPT!
19+
* @param image the source image
20+
* @param luminanceThreshold luminance threshold
21+
* @return the dithered 2-color image
22+
*/
23+
public static BufferedImage desaturateWithDithering(final BufferedImage image, int luminanceThreshold)
24+
{
25+
// Clone original image
26+
final ColorModel cm = image.getColorModel();
27+
final boolean isAlphaPreMultiplied = cm.isAlphaPremultiplied();
28+
final WritableRaster raster = image.copyData(null);
29+
final BufferedImage out = new BufferedImage(cm, raster, isAlphaPreMultiplied, null);
30+
31+
// Apply dithering
32+
final int width = out.getWidth();
33+
final int height = out.getHeight();
34+
35+
// Two colors for dithering, typically black and white
36+
37+
for (int y = 0; y < height; y++) {
38+
for (int x = 0; x < width; x++) {
39+
final int pixel = out.getRGB(x, y);
40+
final int alpha = (pixel >> 24) & 0xff;
41+
final int red = (pixel >> 16) & 0xff;
42+
final int green = (pixel >> 8) & 0xff;
43+
final int blue = pixel & 0xff;
44+
45+
// Convert to grayscale (or use other method to choose between color1 and color2)
46+
final int gray = (int)(0.299 * red + 0.587 * green + 0.114 * blue);
47+
final int newPixel = gray < luminanceThreshold ? BLACK : WHITE;
48+
49+
final int error = gray - ((newPixel == BLACK) ? 0 : 255);
50+
51+
// Distribute the error to neighboring pixels
52+
if (x < width - 1) out.setRGB(x + 1, y, applyError(out.getRGB(x + 1, y), Math.round(error * ERROR_SEVEN_SIXTEENTHS)));
53+
if (x > 0 && y < height - 1) out.setRGB(x - 1, y + 1, applyError(out.getRGB(x - 1, y + 1), Math.round(error * ERROR_THREE_SIXTEENTHS)));
54+
if (y < height - 1) out.setRGB(x, y + 1, applyError(out.getRGB(x, y + 1), Math.round(error * ERROR_FIVE_SIXTEENTHS)));
55+
if (x < width - 1 && y < height - 1) out.setRGB(x + 1, y + 1, applyError(out.getRGB(x + 1, y + 1), Math.round(error * ERROR_ONE_SIXTEENTH)));
56+
57+
out.setRGB(x, y, newPixel | (alpha << 24));
58+
}
59+
}
60+
return out;
61+
}
62+
63+
private static int applyError(int pixel, int error) {
64+
int red = Math.min(Math.max(((pixel >> 16) & 0xff) + error, 0), 255);
65+
int green = Math.min(Math.max(((pixel >> 8) & 0xff) + error, 0), 255);
66+
int blue = Math.min(Math.max((pixel & 0xff) + error, 0), 255);
67+
return (pixel & 0xff000000) | (red << 16) | (green << 8) | blue;
68+
}
69+
70+
public static BufferedImage desaturateWithHardCut(final BufferedImage image, final int luminanceThreshold, final int alphaThreshold)
71+
{
72+
final BufferedImage out = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_BINARY);
73+
for (int y = 0; y < image.getHeight(); y++)
74+
{
75+
for (int x = 0; x < image.getWidth(); x++)
76+
{
77+
final Color pixelColor = new Color(image.getRGB(x, y), true);
78+
final float luminance = HSLColor.fromRGB(pixelColor)[2];
79+
if (pixelColor.getAlpha() >= alphaThreshold && luminance <= luminanceThreshold)
80+
{
81+
out.setRGB(x, y, BLACK);
82+
}
83+
else
84+
{
85+
out.setRGB(x, y, WHITE);
86+
}
87+
}
88+
}
89+
return out;
90+
}
91+
}

0 commit comments

Comments
 (0)