codexraster-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()