Skip to content

Instantly share code, notes, and snippets.

@visy
Created April 12, 2023 17:51
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 visy/b01ce0c311d0c30b48933b8505c65c14 to your computer and use it in GitHub Desktop.
Save visy/b01ce0c311d0c30b48933b8505c65c14 to your computer and use it in GitHub Desktop.
320x200 image to 40x25 charmode generator
import java.util.*;
import java.util.Comparator;
PImage img;
PImage ditheredImg;
int blockSize = 8;
int numChars = 128;
int ditherSize = 128;
ArrayList<PImage> customCharset;
int[][] pseudographics;
final color[] C64_PALETTE = {
color(0, 0, 0), // Black
color(255, 255, 255), // White
color(136, 0, 0), // Red
color(170, 255, 238), // Cyan
color(204, 68, 204), // Purple
color(0, 204, 85), // Green
color(0, 0, 170), // Blue
color(238, 238, 119), // Yellow
color(221, 136, 85), // Orange
color(102, 68, 0), // Brown
color(255, 119, 119), // Light Red
color(51, 51, 51), // Dark Gray
color(119, 119, 119), // Gray
color(170, 255, 102), // Light Green
color(0, 136, 255), // Light Blue
color(187, 187, 187) // Light Gray
};
byte[][] colorRam;
void setup() {
size(320, 200);
img = loadImage("image.jpg");
colorRam = generateColorRamFromImage(img, 40, 25);
redither();
}
void draw() {
displayPseudographics(pseudographics, customCharset);
// image(ditheredImg,0,0);
}
byte[][] generateColorRamFromImage(PImage img, int width, int height) {
byte[][] colorRam = new byte[height][width];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
PImage cell = img.get(x * blockSize, y * blockSize, blockSize, blockSize);
ArrayList<Integer> dominantColors = findTopTwoColors(cell);
byte[] c64Colors = new byte[2];
for (int i = 0; i < dominantColors.size(); i++) {
c64Colors[i] = (byte)dominantColors.get(i).byteValue();
}
colorRam[y][x] = (byte) ((0<<4) | (c64Colors[0] & 0x0F));
}
}
return colorRam;
}
ArrayList<Integer> findTopTwoColors(PImage img) {
HashMap<Integer, Integer> colorCounts = new HashMap<Integer, Integer>();
for (int y = 0; y < img.height; y++) {
for (int x = 0; x < img.width; x++) {
int c = img.get(x, y);
int c64ColorIndex = findClosestC64ColorIndex(c);
colorCounts.put(c64ColorIndex, colorCounts.getOrDefault(c64ColorIndex, 0) + 1);
}
}
List<Map.Entry<Integer, Integer>> sortedEntries = new ArrayList<Map.Entry<Integer, Integer>>(colorCounts.entrySet());
Collections.sort(sortedEntries, new Comparator<Map.Entry<Integer, Integer>>() {
@Override
public int compare(Map.Entry<Integer, Integer> entry1, Map.Entry<Integer, Integer> entry2) {
return entry2.getValue().compareTo(entry1.getValue());
}
});
ArrayList<Integer> topTwoColors = new ArrayList<Integer>();
topTwoColors.add(sortedEntries.get(0).getKey());
if (sortedEntries.size() == 1) {
} else {
topTwoColors.add(sortedEntries.get(1).getKey());
}
return topTwoColors;
}
float colorDistance(color c1, color c2) {
float rDiff = red(c1) - red(c2);
float gDiff = green(c1) - green(c2);
float bDiff = blue(c1) - blue(c2);
return sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
}
byte findClosestC64ColorIndex(color c) {
float minDistance = 100000;
byte minIndex = -1;
for (int i = 0; i < C64_PALETTE.length; i++) {
float distance = colorDistance(c, C64_PALETTE[i]);
if (distance < minDistance) {
minDistance = distance;
minIndex = (byte) i;
}
}
return minIndex;
}
void redither() {
ditheredImg = ditherImage(img,ditherSize); // Convert to 1-bit black and white with Atkinson dithering
customCharset = generateCustomCharset(ditheredImg, blockSize, numChars);
pseudographics = generatePseudographics(ditheredImg, customCharset);
}
void saveCharsetImage(ArrayList<PImage> customCharset, String filename) {
PImage charsetImage = createImage(128, 128, RGB);
charsetImage.loadPixels();
for (int i = 0; i < customCharset.size(); i++) {
PImage charImg = customCharset.get(i);
int x = (i % 16) * blockSize;
int y = (i / 16) * blockSize;
charImg.loadPixels();
for (int row = 0; row < blockSize; row++) {
for (int col = 0; col < blockSize; col++) {
charsetImage.pixels[(x + col) + (y + row) * 128] = charImg.pixels[col + row * blockSize];
}
}
}
charsetImage.updatePixels();
charsetImage.save(filename);
}
void saveIndexTable(int[][] pseudographics, String filename) {
PrintWriter output = createWriter(filename);
for (int y = 0; y < pseudographics.length; y++) {
String line = "";
for (int x = 0; x < pseudographics[y].length; x++) {
line += pseudographics[y][x] + (x < pseudographics[y].length - 1 ? "," : "");
}
output.println(line);
}
output.flush();
output.close();
}
PImage ditherImage(PImage inputImg, int threshold) {
PImage dithered = inputImg.copy();
dithered.filter(GRAY);
dithered.loadPixels();
float[][] atkinsonMatrix = {
{0, 0, 1 / 8.0, 1 / 8.0},
{1 / 8.0, 1 / 8.0, 1 / 8.0, 0},
{0, 1 / 8.0, 0, 0}
};
for (int y = 0; y < dithered.height; y++) {
for (int x = 0; x < dithered.width; x++) {
int idx = x + y * dithered.width;
float oldPixel = brightness(dithered.pixels[idx]);
float newPixel = oldPixel < threshold ? 0 : 255;
dithered.pixels[idx] = color(newPixel);
float quantError = oldPixel - newPixel;
for (int j = 0; j < atkinsonMatrix.length; j++) {
for (int i = 0; i < atkinsonMatrix[j].length; i++) {
int newY = y + j;
int newX = x + i - 1;
if (newX >= 0 && newY >= 0 && newX < dithered.width && newY < dithered.height) {
int newIdx = newX + newY * dithered.width;
float newValue = brightness(dithered.pixels[newIdx]) + quantError * atkinsonMatrix[j][i];
dithered.pixels[newIdx] = color(constrain(newValue, 0, 255));
}
}
}
}
}
dithered.updatePixels();
return dithered;
}
ArrayList<PImage> generateCustomCharset(PImage ditheredImg, int blockSize, int numChars) {
ArrayList<PImage> blocks = getBlocks(ditheredImg, blockSize);
ArrayList<PImage> centroids = initCentroids(blocks, numChars);
boolean changed = true;
int iterations = 0;
while (changed && iterations < 50) {
ArrayList<ArrayList<PImage>> clusters = createEmptyClusters(numChars);
for (PImage block : blocks) {
int minIndex = findClosestCentroidIndex(block, centroids);
clusters.get(minIndex).add(block);
}
ArrayList<PImage> newCentroids = new ArrayList<PImage>();
for (ArrayList<PImage> cluster : clusters) {
newCentroids.add(calculateCentroid(cluster, blockSize));
}
if (compareCentroids(centroids, newCentroids)) {
changed = false;
} else {
centroids = newCentroids;
}
iterations++;
}
PImage whiteTile = createImage(8, 8, RGB);
whiteTile.loadPixels();
for (int i = 0; i < whiteTile.pixels.length; i++) {
whiteTile.pixels[i] = color(255);
}
whiteTile.updatePixels();
String whiteTileHash = getImageHash(whiteTile);
for (int i = centroids.size() - 1; i >= 0; i--) {
if (getImageHash(centroids.get(i)).equals(whiteTileHash)) {
centroids.remove(i);
}
}
// Sort the charset by black to white fill ratio
centroids.sort(new Comparator<PImage>() {
public int compare(PImage img1, PImage img2) {
float fillRatio1 = getBlackFillRatio(img1);
float fillRatio2 = getBlackFillRatio(img2);
return Float.compare(fillRatio1, fillRatio2);
}
});
return centroids;
}
float getBlackFillRatio(PImage img) {
img.loadPixels();
int blackPixels = 0;
for (int i = 0; i < img.pixels.length; i++) {
if (img.pixels[i] == color(0)) {
blackPixels++;
}
}
return (float) blackPixels / img.pixels.length;
}
ArrayList<PImage> getBlocks(PImage img, int blockSize) {
ArrayList<PImage> blocks = new ArrayList<PImage>();
for (int y = 0; y < img.height; y += blockSize) {
for (int x = 0; x < img.width; x += blockSize) {
PImage block = img.get(x, y, blockSize, blockSize);
blocks.add(block);
}
}
return blocks;
}
ArrayList<PImage> initCentroids(ArrayList<PImage> blocks, int numChars) {
ArrayList<PImage> centroids = new ArrayList<PImage>();
HashSet<String> uniqueCentroids = new HashSet<String>();
while (centroids.size() < numChars) {
int randomIndex = int(random(blocks.size()));
PImage block = blocks.get(randomIndex);
String blockHash = getImageHash(block);
if (!uniqueCentroids.contains(blockHash)) {
centroids.add(block);
uniqueCentroids.add(blockHash);
}
blocks.remove(randomIndex);
}
return centroids;
}
String getImageHash(PImage img) {
img.loadPixels();
StringBuilder hash = new StringBuilder();
for (int i = 0; i < img.pixels.length; i++) {
hash.append(img.pixels[i]);
}
return hash.toString();
}
ArrayList<ArrayList<PImage>> createEmptyClusters(int numChars) {
ArrayList<ArrayList<PImage>> clusters = new ArrayList<ArrayList<PImage>>();
for (int i = 0; i < numChars; i++) {
clusters.add(new ArrayList<PImage>());
}
return clusters;
}
int findClosestCentroidIndex(PImage block, ArrayList<PImage> centroids) {
int minIndex = 0;
float minDist = Float.MAX_VALUE;
for (int i = 0; i < centroids.size(); i++) {
float dist = calculateBlockDistance(block, centroids.get(i));
if (dist < minDist) {
minDist = dist;
minIndex = i;
}
}
return minIndex;
}
float calculateBlockDistance(PImage block1, PImage block2) {
float dist = 0;
for (int y = 0; y < block1.height; y++) {
for (int x = 0; x < block1.width; x++) {
float diff = abs(brightness(block1.get(x, y)) - brightness(block2.get(x, y)));
dist += diff;
}
}
return dist;
}
PImage calculateCentroid(ArrayList<PImage> cluster, int blockSize) {
PImage centroid = createImage(blockSize, blockSize, RGB);
centroid.loadPixels();
for (int y = 0; y < blockSize; y++) {
for (int x = 0; x < blockSize; x++) {
float sum = 0;
for (PImage block : cluster) {
sum += brightness(block.get(x, y));
}
float averageBrightness = sum / cluster.size();
centroid.pixels[x + y * blockSize] = color(averageBrightness < 128 ? 0 : 255);
}
}
centroid.updatePixels();
return centroid;
}
boolean compareCentroids(ArrayList<PImage> centroids1, ArrayList<PImage> centroids2) {
for (int i = 0; i < centroids1.size(); i++) {
if (calculateBlockDistance(centroids1.get(i), centroids2.get(i)) > 0.01) {
return false;
}
}
return true;
}
int[][] generatePseudographics(PImage ditheredImg, ArrayList<PImage> customCharset) {
int[][] pseudographics = new int[ditheredImg.height / blockSize][ditheredImg.width / blockSize];
for (int y = 0; y < ditheredImg.height; y += blockSize) {
for (int x = 0; x < ditheredImg.width; x += blockSize) {
PImage block = ditheredImg.get(x, y, blockSize, blockSize);
int minIndex = findClosestCentroidIndex(block, customCharset);
pseudographics[y / blockSize][x / blockSize] = minIndex;
}
}
return pseudographics;
}
void displayPseudographics(int[][] pseudographics, ArrayList<PImage> customCharset) {
for (int y = 0; y < pseudographics.length; y++) {
for (int x = 0; x < pseudographics[y].length; x++) {
PImage block = customCharset.get(pseudographics[y][x]);
int colorData = colorRam[y][x];
int fgColor = C64_PALETTE[(colorData & 0xF0) >> 4];
int bgColor = C64_PALETTE[colorData & 0x0F];
renderBlockWithColors(block, x * blockSize, y * blockSize, fgColor, bgColor);
}
}
}
void renderBlockWithColors(PImage block, int x, int y, color fgColor, color bgColor) {
block.loadPixels();
for (int j = 0; j < block.height; j++) {
for (int i = 0; i < block.width; i++) {
if (block.pixels[j * block.width + i] == color(0)) {
set(x + i, y + j, fgColor);
} else {
set(x + i, y + j, bgColor);
}
}
}
}
void keyPressed() {
if (key == 'z') {
ditherSize-=16;
customCharset.clear();
pseudographics = null;
redither();
}
if (key == 'x') {
ditherSize+=16;
customCharset.clear();
pseudographics = null;
redither();
}
if (key == 'a') {
numChars-=1;
customCharset.clear();
pseudographics = null;
redither();
}
if (key == 's') {
numChars+=1;
customCharset.clear();
pseudographics = null;
redither();
}
if (key == 'o') {
saveCharsetImage(customCharset, "charset.png");
saveIndexTable(pseudographics, "index_table.txt");
}
if (key == 'q') {
saveFrame("frame.png");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment