diff --git a/corpus/donsol for pico-8 v1 8 4 _2.png b/corpus/donsol for pico-8 v1 8 4 _2.png new file mode 100644 index 0000000..b01a3ff Binary files /dev/null and b/corpus/donsol for pico-8 v1 8 4 _2.png differ diff --git a/corpus/quantized/age of ants_2.png b/corpus/quantized/age of ants_2.png new file mode 100644 index 0000000..5f2e20c Binary files /dev/null and b/corpus/quantized/age of ants_2.png differ diff --git a/corpus/quantized/age of ants_5.png b/corpus/quantized/age of ants_5.png new file mode 100644 index 0000000..6cfc3db Binary files /dev/null and b/corpus/quantized/age of ants_5.png differ diff --git a/corpus/quantized/age_of_ants-title.png b/corpus/quantized/age_of_ants-title.png new file mode 100644 index 0000000..be2e213 Binary files /dev/null and b/corpus/quantized/age_of_ants-title.png differ diff --git a/corpus/quantized/build a jetpack_0.png b/corpus/quantized/build a jetpack_0.png new file mode 100644 index 0000000..378c7ab Binary files /dev/null and b/corpus/quantized/build a jetpack_0.png differ diff --git a/corpus/quantized/build a jetpack_5.png b/corpus/quantized/build a jetpack_5.png new file mode 100644 index 0000000..7cae473 Binary files /dev/null and b/corpus/quantized/build a jetpack_5.png differ diff --git a/corpus/quantized/build a jetpack_9.png b/corpus/quantized/build a jetpack_9.png new file mode 100644 index 0000000..e4b22c6 Binary files /dev/null and b/corpus/quantized/build a jetpack_9.png differ diff --git a/corpus/quantized/donsol for pico-8 v1 8 4 _2.png b/corpus/quantized/donsol for pico-8 v1 8 4 _2.png new file mode 100644 index 0000000..e6a5cc9 Binary files /dev/null and b/corpus/quantized/donsol for pico-8 v1 8 4 _2.png differ diff --git a/corpus/quantized/floodedcaves_0.png b/corpus/quantized/floodedcaves_0.png new file mode 100644 index 0000000..eef9ce4 Binary files /dev/null and b/corpus/quantized/floodedcaves_0.png differ diff --git a/corpus/quantized/hersheys_train_line_0.png b/corpus/quantized/hersheys_train_line_0.png new file mode 100644 index 0000000..df693c2 Binary files /dev/null and b/corpus/quantized/hersheys_train_line_0.png differ diff --git a/corpus/quantized/iso_ray-09_000.png b/corpus/quantized/iso_ray-09_000.png new file mode 100644 index 0000000..c52958f Binary files /dev/null and b/corpus/quantized/iso_ray-09_000.png differ diff --git a/corpus/quantized/pico20068.png b/corpus/quantized/pico20068.png new file mode 100644 index 0000000..402c82c Binary files /dev/null and b/corpus/quantized/pico20068.png differ diff --git a/corpus/quantized/pico8_build_a_jetpack-1.png b/corpus/quantized/pico8_build_a_jetpack-1.png new file mode 100644 index 0000000..e6bc3e4 Binary files /dev/null and b/corpus/quantized/pico8_build_a_jetpack-1.png differ diff --git a/corpus/quantized/pico8_bunnysurvivor-9.png b/corpus/quantized/pico8_bunnysurvivor-9.png new file mode 100644 index 0000000..6ebdb28 Binary files /dev/null and b/corpus/quantized/pico8_bunnysurvivor-9.png differ diff --git a/corpus/quantized/pico8_donsol8_v1-14.png b/corpus/quantized/pico8_donsol8_v1-14.png new file mode 100644 index 0000000..9999341 Binary files /dev/null and b/corpus/quantized/pico8_donsol8_v1-14.png differ diff --git a/corpus/quantized/pico8_mot_raymarch3-0.png b/corpus/quantized/pico8_mot_raymarch3-0.png new file mode 100644 index 0000000..e09e838 Binary files /dev/null and b/corpus/quantized/pico8_mot_raymarch3-0.png differ diff --git a/corpus/quantized/pico8_ppwr-5.png b/corpus/quantized/pico8_ppwr-5.png new file mode 100644 index 0000000..4effa39 Binary files /dev/null and b/corpus/quantized/pico8_ppwr-5.png differ diff --git a/corpus/quantized/pico8_px9-9.png b/corpus/quantized/pico8_px9-9.png new file mode 100644 index 0000000..3699ed2 Binary files /dev/null and b/corpus/quantized/pico8_px9-9.png differ diff --git a/corpus/quantized/pico8_rotslimepires_1_1-0.png b/corpus/quantized/pico8_rotslimepires_1_1-0.png new file mode 100644 index 0000000..62ed0fe Binary files /dev/null and b/corpus/quantized/pico8_rotslimepires_1_1-0.png differ diff --git a/corpus/quantized/pico8_witchcrafttd-6.png b/corpus/quantized/pico8_witchcrafttd-6.png new file mode 100644 index 0000000..0fa2e4c Binary files /dev/null and b/corpus/quantized/pico8_witchcrafttd-6.png differ diff --git a/corpus/quantized/shinkansen 1 3_1.png b/corpus/quantized/shinkansen 1 3_1.png new file mode 100644 index 0000000..6ae4e7a Binary files /dev/null and b/corpus/quantized/shinkansen 1 3_1.png differ diff --git a/corpus/quantized/storming the grandmothership_1.png b/corpus/quantized/storming the grandmothership_1.png new file mode 100644 index 0000000..d316e50 Binary files /dev/null and b/corpus/quantized/storming the grandmothership_1.png differ diff --git a/python/quantize.ipynb b/python/quantize.ipynb new file mode 100644 index 0000000..4ff5c9a --- /dev/null +++ b/python/quantize.ipynb @@ -0,0 +1,61 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "from util import load_image_128, quantize_image" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processing ../corpus/iso_ray-09_000.png...\n" + ] + } + ], + "source": [ + "corpus_dir = Path('../corpus/')\n", + "quantized_dir = corpus_dir / 'quantized'\n", + "\n", + "for src_path in Path('../corpus/').glob('*.*'):\n", + " dest_path = quantized_dir / src_path.name\n", + " if not dest_path.exists():\n", + " print(f\"Processing {src_path}...\")\n", + " img = load_image_128(src_path)\n", + " img = quantize_image(img)\n", + " img.save(dest_path)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/python/util.py b/python/util.py new file mode 100644 index 0000000..e31c2da --- /dev/null +++ b/python/util.py @@ -0,0 +1,76 @@ +from PIL import Image +from pathlib import Path +import numpy as np +import math + +def load_palette(path: Path) -> np.ndarray: + palette = [] + with open(path, 'r') as f: + for line in f: + hex_color = line.strip() + # Convert hex to RGB tuple + rgb_color = [int(hex_color[i:i+2], 16) for i in (0, 2, 4)] + palette.append(rgb_color) + return np.array(palette) + +pico8_palette = load_palette('../pico-8-secret-palette.hex') + +def load_image_128(path: Path) -> Image.Image: + img = Image.open(path).convert('RGB') + w, h = img.size + if w > 128 or h > 128: + if w % 128 == 0 and h % 128 == 0: + resample = Image.Resampling.NEAREST + else: + resample = Image.Resampling.BICUBIC + if w > h: + w, h = 128, int(round(h * (128 / w))) + else: + w, h = int(round(w * (128 / h))), 128 + img = img.resize((w, h), resample=resample) + return img + + +def quantize_image(img: Image.Image) -> Image.Image: + pal_idxs = choose_subpalette_indexes(img, 16, pico8_palette) + pal = np.array([pico8_palette[i] for i in pal_idxs]).astype(np.float32) + img_np = np.array(img) + pixels = img_np.reshape(-1, 3).astype(np.float32) + distances = np.sum((pixels[:, None, :] - pal[None, :, :]) ** 2, axis=2) + closest_idxs = np.argmin(distances, axis=1).astype(np.uint8).reshape(img_np.shape[1], -1) + result = Image.fromarray(closest_idxs, mode="P") + result.putpalette(pal.astype(np.uint8).reshape(-1).tolist()) + return result + + +def choose_subpalette_indexes(img: Image.Image, n: int, palette: np.ndarray): + assert len(palette.shape) == 2; + assert palette.shape[1] == 3 + img_np = np.array(img) + result = list(range(n)) + num_steps = 0 + pal_idx = 0 + while num_steps < n: + num_steps += 1 + old_val = result[pal_idx] + result[pal_idx] = None + best_err = math.inf + best_val = None + for val in [i for i in range(len(palette)) if i not in result]: + result[pal_idx] = val + err = _squared_quantization_error(img_np, [palette[i] for i in result]) + if err < best_err: + best_err = err + best_val = val + result[pal_idx] = best_val + if best_val != old_val: + num_steps = 0 + pal_idx = (pal_idx + 1) % len(result) + return result + +def _squared_quantization_error(img_np: np.ndarray, palette: np.ndarray): + pixels = img_np.reshape(-1, 3).astype(np.float32) + pal = np.array(palette, dtype=np.float32) + sq_distances = np.sum((pixels[:, None, :] - pal[None, :, :]) ** 2, axis=2) + min_errs = np.min(sq_distances, axis=1) + return np.sum(min_errs)