Experiments with RLE, color palette selection

This commit is contained in:
2024-11-03 01:14:42 -07:00
parent 90e3a213fb
commit fdb2d23d01
5 changed files with 3291 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from PIL import Image\n",
"from pathlib import Path\n",
"import numpy as np\n",
"import math"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"# Function to read hex color codes from a file and convert them to RGB tuples\n",
"def load_palette(file_path):\n",
" palette = []\n",
" with open(file_path, 'r') as f:\n",
" for line in f:\n",
" hex_color = line.strip()\n",
" # Convert hex to RGB tuple\n",
" rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))\n",
" palette.append(rgb_color)\n",
" return palette\n",
"\n",
"pico8_palette = load_palette('../pico-8-secret-palette.hex')"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"def load_image(path: Path) -> Image.Image:\n",
" img = Image.open(path).convert('RGB')\n",
" w, h = img.size\n",
" if w > 128 or h > 128:\n",
" if w > h:\n",
" w, h = 128, int(round(h * (128 / w)))\n",
" else:\n",
" w, h = int(round(w * (128 / h))), 128\n",
" img = img.resize((w, h))\n",
" return img\n",
"\n",
"img = load_image('../peacock.jpg')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"def convert_to_indexed(img_np: np.array, palette: list) -> Image.Image:\n",
" pixels = img_np.reshape(-1, 3).astype(np.float32)\n",
" pal = np.array(palette, dtype=np.float32)\n",
" distances = np.sum((pixels[:, None, :] - pal[None, :, :]) ** 2, axis=2)\n",
" closest_idxs = np.argmin(distances, axis=1).astype(np.uint8).reshape(img_np.shape[1], -1)\n",
" result = Image.fromarray(closest_idxs, mode=\"P\")\n",
" result.putpalette(np.array(palette).reshape(-1).tolist())\n",
" return result"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def squared_quantization_error(img_np: np.array, palette: np.array):\n",
" pixels = img_np.reshape(-1, 3).astype(np.float32)\n",
" pal = np.array(palette, dtype=np.float32)\n",
" sq_distances = np.sum((pixels[:, None, :] - pal[None, :, :]) ** 2, axis=2)\n",
" min_errs = np.min(sq_distances, axis=1)\n",
" return np.sum(min_errs)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<PIL.Image.Image image mode=P size=128x128>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def pick_palette_indexes(img, n, palette):\n",
" img_np = np.array(img)\n",
" result = list(range(n))\n",
" num_steps = 0\n",
" pal_idx = 0\n",
" while num_steps < n:\n",
" num_steps += 1\n",
" old_val = result[pal_idx]\n",
" result[pal_idx] = None\n",
" best_err = math.inf\n",
" best_val = None\n",
" for val in [i for i in range(len(palette)) if i not in result]:\n",
" result[pal_idx] = val\n",
" err = squared_quantization_error(img_np, [palette[i] for i in result])\n",
" if err < best_err:\n",
" best_err = err\n",
" best_val = val\n",
" result[pal_idx] = best_val\n",
" if best_val != old_val:\n",
" num_steps = 0\n",
" pal_idx = (pal_idx + 1) % len(result)\n",
" return result\n",
"\n",
"pal_idxs = pick_palette_indexes(img, 16, pico8_palette)\n",
"\n",
"\n",
"pal = [pico8_palette[i] for i in pal_idxs]\n",
"convert_to_indexed(np.array(img), pal)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<PIL.Image.Image image mode=P size=128x128>"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"convert_to_indexed(np.array(img), pico8_palette[:16])"
]
}
],
"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
}
+2850
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[tool.poetry]
name = "color-cell"
version = "0.1.0"
description = ""
authors = ["Chris Mounce <christophermounce@gmail.com>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
jupyter = "^1.1.1"
matplotlib = "^3.9.2"
pillow = "^11.0.0"
numpy = "^2.1.2"
bitarray = "^3.0.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
+213
View File
File diff suppressed because one or more lines are too long