codex/raster-pixels-grayscale-quantization.py

raster-pixels-grayscale-quantization.py
"""Synthetic quantization/sRGB illustration; no measured data.

Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Run from _media with
MPLCONFIGDIR=/tmp/codex-wiki-matplotlib; writes matching PNG basename to CWD.
"""
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np


def decode(s):
    s = np.asarray(s)
    return np.where(s <= .04045, s / 12.92, ((s + .055) / 1.055) ** 2.4)


def encode(light):
    light = np.asarray(light)
    return np.where(light <= .0031308, 12.92 * light, 1.055 * light ** (1 / 2.4) - .055)


def main():
    plt.rcParams.update({"font.size": 10, "axes.titlesize": 11})
    x = np.linspace(0, 1, 1024)
    q2 = np.floor(3 * x + .5) / 3
    q8 = np.floor(255 * x + .5) / 255
    assert np.unique(q2).size == 4 and np.unique(q8).size == 256
    assert np.max(abs(q2 - x)) <= 1 / 6 + 1e-14
    assert np.max(abs(q8 - x)) <= 1 / 510 + 1e-14
    fig, axes = plt.subplots(2, 2, figsize=(10, 6.7), facecolor="white", layout="constrained")
    ax = axes[0, 0]
    ax.imshow(np.vstack([q8, q2]), cmap="gray", vmin=0, vmax=1,
              origin="lower", extent=(0, 1, 0, 2), aspect="auto", interpolation="nearest")
    ax.set(yticks=[.5, 1.5], yticklabels=["8 bits: 256 codes", "2 bits: 4 codes"],
           xlabel="Input x (uniform in encoded shade)", title="A. Same spatial grid, different quantization")
    ax.tick_params(axis="y", length=0)
    ax = axes[0, 1]
    ax.plot(x, x, "--", color="#7b8088", label="Unquantized x")
    ax.plot(x, q2, color="#146caa", lw=2, label="2-bit reconstructed value")
    ax.set(xlim=(0, 1), ylim=(-.03, 1.03), xlabel="Input x", ylabel="Reconstructed value",
           title="B. Uniform step = 1/3; error ≤ 1/6")
    ax.set_yticks([0, 1/3, 2/3, 1], ["0", "⅓", "⅔", "1"])
    ax.legend(fontsize=9, loc="upper left")
    ax.grid(alpha=.2)
    ax = axes[1, 0]
    ax.plot(x, decode(x), lw=2.4, color="#bd2876", label="sRGB decoding D(s)")
    ax.plot(x, x, "--", color="#7b8088", label="Linear encoding for comparison")
    ax.scatter([.5, float(encode(.5))], [float(decode(.5)), .5], color="#146caa", zorder=3)
    ax.annotate("D(0.5) ≈ 0.214", (.5, float(decode(.5))), xytext=(.08, .37),
                arrowprops={"arrowstyle": "->", "color": "#146caa"}, fontsize=10)
    ax.annotate("E(0.5) ≈ 0.735", (float(encode(.5)), .5), xytext=(.39, .72),
                arrowprops={"arrowstyle": "->", "color": "#146caa"}, fontsize=10)
    ax.set(xlim=(0, 1), ylim=(0, 1), xlabel="Encoded neutral sRGB value s",
           ylabel="Relative luminance Y", title="C. Code spacing is not light spacing")
    ax.legend(fontsize=9, loc="upper left")
    ax.grid(alpha=.2)
    ax = axes[1, 1]
    ax.set(xlim=(0, 1), ylim=(0, 1), title="D. Equal weights on black and white")
    codes = [128, int(np.floor(255 * encode(.5) + .5))]
    assert codes == [128, 188]
    for y, q, label in zip([.62, .15], codes, ["Average encoded values", "Average linear light, then encode"]):
        ax.add_patch(Rectangle((.04, y), .28, .22, facecolor=(q/255,) * 3, edgecolor="#666"))
        ax.text(.36, y + .18, label, va="center", fontsize=10)
        ax.text(.36, y + .07, f"8-bit code {q}; Y ≈ {float(decode(q/255)):.3f}", va="center", fontsize=10)
    ax.text(.04, .02, "Swatches use rounded codes; luminance follows the stated model.", fontsize=8.7)
    ax.axis("off")
    fig.suptitle("Quantization and light encoding — calculated toy examples", fontsize=15)
    fig.savefig(Path(__file__).stem + ".png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()