This commit is contained in:
Andras Schmelczer 2026-05-03 10:39:31 +01:00
parent 9a009f0b4c
commit eed1567f7f
12 changed files with 463 additions and 243 deletions

View file

@ -2,6 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "7fb27b941602401d91542211134fc71a",
"metadata": {},
"source": [
"# Dithering algorithm comparison\n",
@ -27,26 +28,40 @@
{
"cell_type": "code",
"execution_count": 1,
"id": "acae54e37e7d407bbb7b55eff062a284",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '.')\n",
"from _helpers import bootstrap, immich_client, fetch_pool, download_image, silenced, show_grid\n",
"\n",
"sys.path.insert(0, \".\")\n",
"from _helpers import bootstrap, download_image, fetch_pool, immich_client, show_grid, silenced\n",
"\n",
"bootstrap()\n",
"\n",
"import time\n",
"\n",
"import numpy as np\n",
"from _dither import DITHER_ALGORITHMS, PALETTE_NAMES, PALETTE_RGB, apply_dithering\n",
"from PIL import Image\n",
"from waveshare_epd.epd7in3e import EPD_WIDTH, EPD_HEIGHT, _crop_center\n",
"from _dither import DITHER_ALGORITHMS, apply_dithering, PALETTE_RGB, PALETTE_NAMES\n",
"from waveshare_epd.epd7in3e import EPD_HEIGHT, EPD_WIDTH, _crop_center\n",
"\n",
"# Pure-Python algorithms run at ~30s per 800x480 image; keep a curated subset by default.\n",
"# Toggle SHOW_ALL = True to run everything (will take several minutes).\n",
"DEFAULT_ALGOS = ['atkinson_fast', 'atkinson', 'atkinson_weighted',\n",
" 'floyd_steinberg', 'floyd_steinberg_weighted',\n",
" 'jarvis', 'sierra_lite', 'burkes',\n",
" 'bayer4', 'bayer8', 'pil_fs', 'none']\n",
"DEFAULT_ALGOS = [\n",
" \"atkinson_fast\",\n",
" \"atkinson\",\n",
" \"atkinson_weighted\",\n",
" \"floyd_steinberg\",\n",
" \"floyd_steinberg_weighted\",\n",
" \"jarvis\",\n",
" \"sierra_lite\",\n",
" \"burkes\",\n",
" \"bayer4\",\n",
" \"bayer8\",\n",
" \"pil_fs\",\n",
" \"none\",\n",
"]\n",
"SHOW_ALL = False\n",
"ALGOS = list(DITHER_ALGORITHMS.keys()) if SHOW_ALL else DEFAULT_ALGOS\n",
"\n",
@ -57,6 +72,7 @@
{
"cell_type": "code",
"execution_count": 2,
"id": "9a63283cbaf04dbcab1f6479b197f3a8",
"metadata": {},
"outputs": [
{
@ -91,20 +107,21 @@
" sources.append((asset, _crop_center(img, EPD_WIDTH, EPD_HEIGHT)))\n",
"\n",
"for asset, _ in sources:\n",
" print(asset.get('originalFileName') or asset['id'])\n",
" print(asset.get(\"originalFileName\") or asset[\"id\"])\n",
"\n",
"# Render the 6-colour palette as a tiny banner so the colour budget is visible.\n",
"swatch_h = 60\n",
"swatch_w = 60\n",
"palette_strip = np.zeros((swatch_h, swatch_w * len(PALETTE_RGB), 3), dtype=np.uint8)\n",
"for i, rgb in enumerate(PALETTE_RGB):\n",
" palette_strip[:, i * swatch_w:(i + 1) * swatch_w] = rgb\n",
" palette_strip[:, i * swatch_w : (i + 1) * swatch_w] = rgb\n",
"Image.fromarray(palette_strip)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "8dd0d8092fe74a7c96281538738b07e2",
"metadata": {},
"outputs": [
{
@ -143,22 +160,23 @@
"source": [
"results = [] # list of dict per (photo, algo)\n",
"for asset, source in sources:\n",
" name = asset.get('originalFileName') or asset['id']\n",
" print(f'[{name}]')\n",
" name = asset.get(\"originalFileName\") or asset[\"id\"]\n",
" print(f\"[{name}]\")\n",
" photo_results = []\n",
" for algo in ALGOS:\n",
" info = DITHER_ALGORITHMS[algo]\n",
" t0 = time.perf_counter()\n",
" out = apply_dithering(source, algo)\n",
" dt = time.perf_counter() - t0\n",
" photo_results.append({'algo': algo, 'name': info['name'], 'image': out, 'duration': dt})\n",
" print(f' {info[\"name\"]:32s} {dt:6.2f}s')\n",
" results.append({'asset': asset, 'source': source, 'algos': photo_results})"
" photo_results.append({\"algo\": algo, \"name\": info[\"name\"], \"image\": out, \"duration\": dt})\n",
" print(f\" {info['name']:32s} {dt:6.2f}s\")\n",
" results.append({\"asset\": asset, \"source\": source, \"algos\": photo_results})"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "72eea5119410473aa328ad9291626812",
"metadata": {},
"outputs": [
{
@ -185,21 +203,22 @@
"source": [
"# One grid per photo: original + every algorithm.\n",
"import matplotlib.pyplot as plt\n",
"\n",
"for entry in results:\n",
" panels = [entry['source']] + [r['image'] for r in entry['algos']]\n",
" titles = ['original (cropped)'] + [f\"{r['name']}\\n{r['duration']:.2f}s\" for r in entry['algos']]\n",
" panels = [entry[\"source\"]] + [r[\"image\"] for r in entry[\"algos\"]]\n",
" titles = [\"original (cropped)\"] + [f\"{r['name']}\\n{r['duration']:.2f}s\" for r in entry[\"algos\"]]\n",
" cols = 4\n",
" rows = (len(panels) + cols - 1) // cols\n",
" fig, axes = plt.subplots(rows, cols, figsize=(5.0 * cols, 3.2 * rows))\n",
" axes = np.atleast_2d(axes)\n",
" name = entry['asset'].get('originalFileName') or entry['asset']['id']\n",
" name = entry[\"asset\"].get(\"originalFileName\") or entry[\"asset\"][\"id\"]\n",
" fig.suptitle(name, fontsize=12)\n",
" for k in range(rows * cols):\n",
" ax = axes[k // cols][k % cols]\n",
" if k < len(panels):\n",
" ax.imshow(panels[k])\n",
" ax.set_title(titles[k], fontsize=10)\n",
" ax.axis('off')\n",
" ax.axis(\"off\")\n",
" plt.tight_layout()\n",
" plt.show()"
]
@ -207,6 +226,7 @@
{
"cell_type": "code",
"execution_count": 5,
"id": "8edb47106e1a46a883d545849b8ab81b",
"metadata": {},
"outputs": [
{
@ -232,12 +252,13 @@
"source": [
"# Per-algorithm summary across both photos: mean runtime + a single representative panel.\n",
"from collections import defaultdict\n",
"\n",
"import matplotlib.pyplot as plt\n",
"\n",
"agg = defaultdict(list)\n",
"for entry in results:\n",
" for r in entry['algos']:\n",
" agg[r['algo']].append(r['duration'])\n",
" for r in entry[\"algos\"]:\n",
" agg[r[\"algo\"]].append(r[\"duration\"])\n",
"\n",
"print(f\"{'algorithm':32s} {'avg time':>9s} description\")\n",
"for algo in ALGOS:\n",
@ -248,6 +269,7 @@
},
{
"cell_type": "markdown",
"id": "10185d26023b46108eb7d9f57d49d2b3",
"metadata": {},
"source": [
"## Picking an algorithm\n",