I was inspired by my answer from another popularity contest question which used iterative depth-first search to generate a maze. I used a variation of the algorithm used in a couple other answers that determines the best color to place at a certain pixel given the colors occupying the neighboring pixels. The algorithm starts at the center pixel and works its way visiting every pixel in the image in a random path.
The code is set up so that one can change the fields to give different results, such as adjusting the width and height of the image (and all relevant colors are used depending on the dimensions), random seed, and whether to shuffle or sort the colors before placing them from the list onto the image. Sorting is done using the HSV color model.
The resulting image will appear in a window once the image generation has finished. Scroll panes may be added for convenience in larger images. Assuming searchLimit = 8
, on my machine it takes seconds to produce 256x128 and 512x512 images, around ten minutes for 2048x1024 images, and half a day or longer for 4096x4096 images.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.function.BinaryOperator;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
public class RGBImage {
/**
* Width and height of the image. Choose either of:
* 128x256 256x128 512x512 1024x2048 2048x1024 4096x4096
**/
private final int width = 512, height = 512;
/**
* Number of colors from the front of the list to compare when searching
* for the best color. Note: the higher this value, the longer it will take
* to produce the image.
**/
private final int searchLimit = 8;
/**
* Seed for the color list shuffling and depth-first search algorithm.
* Using the same seed will yield the same image provided no other
* algorithm settings were altered. Add 'L' or 'l' after the value; e.g. 1L
* Using 'null' will create a random image every time the program is run.
**/
private final Long seed = null;
private final Random rand = new Random(seed == null ? System.nanoTime() : seed);
/**
* List of all colors.
**/
private List<Integer> colors;
/**
* Comparator to sort the colors. Use any of
* null byHue() bySaturation() byValue() byRed() byGreen() byBlue()
*
* Using 'null' causes the colors to be sorted in ascending order by their
* numerical value.
* You can sort by one method first and then by another; e.g.
* byHue().thenComparing(bySaturation());
**/
private final Comparator<Integer> comparator = null;
/**
* Flags for shuffling and sorting the values in the list.
* If both flags are 'false', the colors in the list will be sorted in
* ascending order by their numerical value.
* If both flags are 'true', shuffling will occur first, then sorting. This
* may give interesting results!
**/
private final boolean shuffle = false, sort = false;
/**
* The name to save the image as. The extension is "png" and should not be
* included.
**/
private final String fileName = "output";
public RGBImage() {
init();
int[] pixels = new int[width * height];
Arrays.fill(pixels, -1);
constructArray(pixels, new short[]{(short) (width/2), (short) (height/2)});
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
img.setRGB(0, 0, width, height, pixels, 0, width);
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame();
JPanel panel = new JPanel() {
@Override
public Dimension getPreferredSize() {
return new Dimension(width, height);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(img, 0, 0, this);
}
};
frame.add(new JScrollPane(panel), BorderLayout.CENTER);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
if (width <= 512)
frame.pack();
else
frame.setSize(750, 750);
frame.setVisible(true);
});
try {
ImageIO.write(img, "png", new File(fileName + ".png"));
} catch (IOException ex) {
ex.printStackTrace();
}
}
private void init() {
colors = new ArrayList<>(width * height);
int bitsPerComp = (int) (Math.log(width * height) / Math.log(2)) / 3;
int pow2 = 1 << bitsPerComp, fact256 = 256 / pow2;
float adj = 255f / (256 - fact256);
for (int k = 0; k < width * height; k++) {
int r = (int) (((k >> (bitsPerComp * 2)) % pow2) * fact256 * adj) << 16,
g = (int) (((k >> bitsPerComp) % pow2) * fact256 * adj) << 8,
b = (int) ((k % pow2) * fact256 * adj);
colors.add(r | g | b);
}
if (shuffle)
Collections.shuffle(colors);
if (sort)
Collections.sort(colors, comparator);
}
private void constructArray(int[] pixels, short[] coord) {
Deque<short[]> path = new LinkedList<>();
path.add(coord);
boolean up, right, down, left;
while (true) {
coord = path.peekFirst();
int col = bestColor(pixels, coord);
pixels[coord[1] * width + coord[0]] = col;
colors.remove(Integer.valueOf(col));
if (isSurrounded(pixels, coord)) {
do {
coord = path.removeFirst();
} while (isSurrounded(pixels, coord) && !path.isEmpty());
}
if (path.isEmpty())
break;
up = coord[1] != 0 && pixels[(coord[1] - 1) * width + coord[0]] == -1;
right = coord[0] != width - 1 && pixels[coord[1] * width + coord[0] + 1] == -1;
down = coord[1] != height - 1 && pixels[(coord[1] + 1) * width + coord[0]] == -1;
left = coord[0] != 0 && pixels[coord[1] * width + coord[0] - 1] == -1;
byte size = 0;
if (up) size++;
if (right) size++;
if (down) size++;
if (left) size++;
byte[] possible = new byte[size];
if (up) possible[--size] = 0;
if (right) possible[--size] = 1;
if (down) possible[--size] = 2;
if (left) possible[--size] = 3;
switch (possible[rand.nextInt(possible.length)]) {
case 0: path.addFirst(new short[] {coord[0], (short) (coord[1] - 1)}); break;
case 1: path.addFirst(new short[] {(short) (coord[0] + 1), coord[1]}); break;
case 2: path.addFirst(new short[] {coord[0], (short) (coord[1] + 1)}); break;
case 3: path.addFirst(new short[] {(short) (coord[0] - 1), coord[1]}); break;
}
}
}
private boolean isSurrounded(int[] pixels, short[] c) {
return (c[1] == 0 ? true : pixels[(c[1] - 1) * width + c[0]] != -1) &&
(c[0] == width - 1 ? true : pixels[c[1] * width + c[0] + 1] != -1) &&
(c[1] == height - 1 ? true : pixels[(c[1] + 1) * width + c[0]] != -1) &&
(c[0] == 0 ? true : pixels[c[1] * width + c[0] - 1] != -1);
}
private int bestColor(int[] pixels, short[] coord) {
return colors
.subList(0, Math.min(colors.size(), searchLimit))
.parallelStream()
.reduce(BinaryOperator.minBy(
Comparator.comparingDouble(col ->
Arrays.stream(getNeighbors(coord))
.filter(s -> s != null)
.mapToInt(s -> pixels[s[1] * width + s[0]])
.filter(i -> i != -1)
.mapToDouble(i -> HSVdiff(col, i))
.sum()
)
))
.orElseThrow(() -> new RuntimeException("No best color found"));
}
private short[][] getNeighbors(short[] arr) {
short[][] res = new short[8][2];
byte index = 0;
for (; index < 8; index++)
res[index] = null;
index = 0;
for (short cx = (short) (arr[0] - 1); cx <= arr[0] + 1; cx++) {
if (cx < 0 || cx == width) continue;
for (short cy = (short) (arr[1] - 1); cy <= arr[1] + 1; cy++) {
if (cy < 0 || cy == height) continue;
if (cx == arr[0] && cy == arr[1]) continue;
res[index++] = new short[] {cx, cy};
}
}
return res;
}
private double HSVdiff(int c1, int c2) {
float[] hsv1 = Color.RGBtoHSB(c1 >> 16, (c1 >> 8) & 0xff, c1 * 0xff, null),
hsv2 = Color.RGBtoHSB(c2 >> 16, (c2 >> 8) & 0xff, c2 * 0xff, null);
return Math.pow(hsv2[0] - hsv1[0], 2) + Math.pow(hsv2[1] - hsv1[1], 2) + Math.pow(hsv2[2] - hsv1[2], 2);
}
private Comparator<Integer> byHue() {
return Comparator.comparingDouble(col -> Color.RGBtoHSB(col >> 16, (col >> 8) & 0xff, col & 0xff, null)[0]);
}
private Comparator<Integer> bySaturation() {
return Comparator.comparingDouble(col -> Color.RGBtoHSB(col >> 16, (col >> 8) & 0xff, col & 0xff, null)[1]);
}
private Comparator<Integer> byValue() {
return Comparator.comparingDouble(col -> Color.RGBtoHSB(col >> 16, (col >> 8) & 0xff, col & 0xff, null)[2]);
}
private Comparator<Integer> byRed() {
return Comparator.comparingInt(col -> col >> 16);
}
private Comparator<Integer> byGreen() {
return Comparator.comparingInt(col -> (col >> 8) & 0xff);
}
private Comparator<Integer> byBlue() {
return Comparator.comparingInt(col -> col & 0xff);
}
public static void main(String[] args) {
new RGBImage();
}
}
Here's a random 512x512 image, unsorted (colors are stored in the list in ascending numerical order), searchLimit = 8
:
The next three are 512x512, use seed = 24680L
, and shuffled the colors in the list without sorting them afterward. The searchLimit
was the only thing that changed between them, producing interesting results.
searchLimit = 8
:
searchLimit = 128
:
searchLimit = 2048
:
Some 256x128 images, seed = 24680
, searchLimit = 64
, the first sorted by hue, the second sorted by saturation, the third sorted by value, and the fourth shuffled and then sorted by value:
Linked are 2048x1024 images, with seed = 24680
and the colors sorted by hue. Some interesting differences arise when modifying the searchLimit
again. The ones used here are searchLimit = 8
and searchLimit = 512
.
And last, but certainly not least, linked is a random 4096x4096 image with searchLimit = 8
and sorted by hue.