Skip to content

Instantly share code, notes, and snippets.

@tomwhoiscontrary
Created January 5, 2021 15:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tomwhoiscontrary/337cb8aaef013327a8909967970adf48 to your computer and use it in GitHub Desktop.
Save tomwhoiscontrary/337cb8aaef013327a8909967970adf48 to your computer and use it in GitHub Desktop.
Crappy threshold maps for dithering, following https://news.ycombinator.com/item?id=25645286
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ThresholdMap {
public static void main(String[] args) throws IOException {
int width = 400;
int height = 256;
String mode = "hp";
int diameter = 5;
long seed = new Random().nextLong();
for (String arg : args) {
int split = arg.indexOf("=");
String name = arg.substring(0, split);
String value = arg.substring(split + 1);
switch (name) {
case "width":
width = Integer.parseInt(value);
break;
case "height":
height = Integer.parseInt(value);
break;
case "mode":
mode = value;
break;
case "diameter":
diameter = Integer.parseInt(value);
break;
case "seed":
seed = Long.parseLong(value);
break;
default:
throw new IllegalArgumentException(name);
}
}
main(width, height, mode, diameter, seed);
}
private static void main(int width,
int height,
String mode,
int diameter,
long seed) throws IOException {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
WritableRaster raster = image.getRaster();
Random random = new Random(seed);
List<Object> label = new ArrayList<>();
label.add(mode);
switch (mode) {
case "w": {
whiteNoise(random, raster);
}
break;
case "sb": {
label.add(diameter);
shuffledBlockBlueNoise(diameter, random, raster);
}
break;
case "hp": {
label.add(diameter);
highPassFilterBlueNoise(diameter, random, raster);
}
break;
default: {
throw new IllegalArgumentException(mode);
}
}
label.add(String.format("%016x", seed));
write(label, image);
}
// whiteNoise
private static void whiteNoise(Random random, WritableRaster raster) {
for (int x = 0; x < raster.getWidth(); x++) {
for (int y = 0; y < raster.getHeight(); y++) {
raster.setSample(x, y, 0, random.nextInt(256));
}
}
}
// shuffledBlockBlueNoise
private static void shuffledBlockBlueNoise(int blockDiameter, Random random, WritableRaster raster) {
int blockArea = square(blockDiameter);
int step = 259 / blockArea; // oversize numerator to mitigate bias from rounding down
int[] ramp = IntStream.range(0, blockArea).map(v -> v * step).toArray();
for (int blockX = 0; blockX < raster.getWidth(); blockX += blockDiameter) {
for (int blockY = 0; blockY < raster.getHeight(); blockY += blockDiameter) {
shuffle(ramp, random);
for (int offsetX = 0; offsetX < blockDiameter; offsetX++) {
for (int offsetY = 0; offsetY < blockDiameter; offsetY++) {
int x = blockX + offsetX;
int y = blockY + offsetY;
if (!raster.getBounds().contains(x, y)) continue;
int i = index(offsetX, offsetY, blockDiameter, blockDiameter);
raster.setSample(x, y, 0, ramp[i]);
}
}
}
}
}
private static void shuffle(int[] array, Random random) {
for (int i = 0; i < array.length - 1; i++) {
int j = i + random.nextInt(array.length - i);
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}
// highPassFilterBlueNoise
private static void highPassFilterBlueNoise(int kernelDiameter, Random random, WritableRaster raster) {
int width = raster.getWidth();
int height = raster.getHeight();
int size = width * height;
double[] noise = random.doubles().limit(size).toArray();
double[] kernel = gaussianKernel(1, kernelDiameter); // sigma is always 1 despite diameter, which is a bit useless!
double[] filtered = IntStream.range(0, noise.length).mapToDouble(i -> noise[i] - convolve(width, height, noise, kernelDiameter, kernel, i)).toArray();
double[] cdf = Arrays.stream(filtered).sorted().toArray();
double[] thresholds = IntStream.range(0, 256).mapToDouble(i -> cdf[i * size / 256]).toArray();
int[] equalized = Arrays.stream(filtered).mapToInt(v -> indexOfFloor(thresholds, v)).toArray();
raster.setSamples(0, 0, width, height, 0, equalized);
}
static double[] gaussianKernel(double sigma, int diameter) {
// thanks https://www.geeksforgeeks.org/gaussian-filter-generation-c/
double[] kernel = new double[square(diameter)];
double s = 2.0 * sigma * sigma;
double sum = 0;
for (int x = 0; x < diameter; x++) {
for (int y = 0; y < diameter; y++) {
int r = square(x - diameter / 2) + square(y - diameter / 2);
double v = Math.exp(-(double) r / s) / Math.PI / s;
kernel[index(x, y, diameter, diameter)] = v;
sum += v;
}
}
for (int x = 0; x < diameter; ++x) {
for (int y = 0; y < diameter; ++y) {
kernel[index(x, y, diameter, diameter)] /= sum;
}
}
return kernel;
}
private static double convolve(int width, int height, double[] a, int kernelSize, double[] kernel, int i) {
int x = i % width;
int y = i / width;
double sum = 0;
for (int kx = 0; kx < kernelSize; kx++) {
for (int ky = 0; ky < kernelSize; ky++) {
int dx = kx - kernelSize / 2;
int dy = ky - kernelSize / 2;
int sx = (x + dx + width) % width;
int sy = (y + dy + height) % height;
sum += a[index(sx, sy, width, height)] * kernel[index(kx, ky, kernelSize, kernelSize)];
}
}
return sum;
}
private static int indexOfFloor(double[] a, double key) {
int i = Arrays.binarySearch(a, key);
return i >= 0 ? i : -(i + 1) - 1;
}
private static int index(int x, int y, int width, int height) {
Objects.checkIndex(x, width);
Objects.checkIndex(y, height);
return x + y * width;
}
// common
private static int square(int a) {
return a * a;
}
// write
private static void write(List<Object> label, BufferedImage image) throws IOException {
String name = Stream.of(Stream.of(ThresholdMap.class.getSimpleName()),
label.stream().map(String::valueOf),
Stream.of("png"))
.reduce(Stream.empty(), Stream::concat)
.collect(Collectors.joining("."));
Path path = findTempDir().resolve(name);
System.out.println(path);
ImageIO.write(image, "PNG", path.toFile());
}
private static Path findTempDir() throws IOException {
Path tempFile = Files.createTempFile("", "");
Path tempDir = tempFile.getParent();
Files.delete(tempFile);
return tempDir;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment