codex/lossy-block-image-comparison.py

lossy-block-image-comparison.py
"""Rounded block means lose detail; save the PNG to the current directory.

Uses the existing NumPy/Matplotlib dependencies; tested with Python 3.14.4.
This is a teaching format, not a model of PNG or JPEG compression.
"""

from pathlib import Path

import matplotlib

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


def source_image():
    r, c = np.indices((16, 16))
    baseline = 40 + 10 * (r // 2) + 12 * (c // 2)
    return baseline + 20 * (-1) ** (r + c) + ((r % 2 == 0) & (c % 2 == 0))


def encode(image):
    means = image.reshape(8, 2, 8, 2).mean(axis=(1, 3))
    stored = np.floor(means + 0.5).astype(np.uint8)
    header = f"{16:08b}{16:08b}1"
    bits = header + "".join(f"{int(value):08b}" for value in stored.ravel())
    return bits, stored, means


def decode(bits):
    if len(bits) != 529 or any(bit not in "01" for bit in bits):
        raise ValueError("Expected exactly 529 binary bits before padding")
    if (int(bits[:8], 2), int(bits[8:16], 2), bits[16]) != (16, 16, "1"):
        raise ValueError("This decoder accepts only the 16 by 16 block-mean profile")
    values = np.array([int(bits[i:i + 8], 2) for i in range(17, len(bits), 8)])
    return values.reshape(8, 8).repeat(2, axis=0).repeat(2, axis=1)


def main():
    image = source_image()
    bits, stored, means = encode(image)
    reconstructed = decode(bits)
    i, j = np.indices((8, 8))
    np.testing.assert_array_equal(stored, 40 + 10 * i + 12 * j)
    np.testing.assert_array_equal(means, stored.astype(float) + 0.25)
    np.testing.assert_array_equal(image[:2, :2], [[61, 20], [20, 60]])
    assert image.min() == 20 and image.max() == 215
    mse = float(np.mean((image - reconstructed) ** 2))
    unrounded = means.repeat(2, axis=0).repeat(2, axis=1)
    mse_unrounded = float(np.mean((image - unrounded) ** 2))
    assert mse == 410.25 and mse_unrounded == 410.1875
    assert encode(reconstructed)[0] == bits  # Two distinct sources share one encoding.
    assert not np.array_equal(image, reconstructed)
    assert len(bits) - 17 == 512 and len(bits) == 529
    assert len(bits) + (-len(bits) % 8) == 536

    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white"})
    fig, axes = plt.subplots(1, 3, figsize=(12, 6.2), dpi=110)
    fig.subplots_adjust(left=0.035, right=0.965, bottom=0.32, top=0.81, wspace=0.18)
    fig.suptitle("Lossy: block means keep broad shades and discard fine detail",
                 fontsize=16, y=0.97)
    fig.text(0.5, 0.90, "All panels use the same shade scale: 0 = black, 255 = white",
             ha="center", color="#344b60")
    titles = ("Original: 16 × 16 pixels\n2048 payload bits",
              "Stored: 8 × 8 rounded means\n512 payload bits",
              "Reconstruction: 16 × 16 pixels\nCopy each value into a 2 × 2 block")
    for ax, array, title in zip(axes, (image, stored, reconstructed), titles):
        ax.imshow(array, cmap="gray", vmin=0, vmax=255, interpolation="nearest")
        n = array.shape[0]
        step = 1 if n == 8 else 2
        ax.set_xticks(np.arange(-0.5, n, step), minor=True)
        ax.set_yticks(np.arange(-0.5, n, step), minor=True)
        ax.grid(which="minor", color="#628095", linewidth=0.65, alpha=0.8)
        ax.tick_params(which="both", bottom=False, left=False,
                       labelbottom=False, labelleft=False)
        ax.set_title(title, fontsize=12, pad=10)
    for r in range(8):
        for c in range(8):
            value = int(stored[r, c])
            axes[1].text(c, r, str(value), ha="center", va="center", fontsize=8,
                         color="white" if value < 125 else "black")
    texts = ("First block\n61   20\n20   60",
             "Mean = 161 / 4 = 40.25\nRound half upward → 40\nStore 00101000 (8 bits)",
             "First reconstructed block\n40   40\n40   40")
    for ax, label in zip(axes, texts):
        ax.text(0.5, -0.07, label, transform=ax.transAxes, ha="center", va="top",
                fontsize=11, linespacing=1.45)
    fig.text(0.5, 0.125, "MSE = (21² + 20² + 20² + 20²) / 4 = 410.25 shade units²",
             ha="center", fontsize=13, weight="bold")
    fig.text(0.5, 0.065, "75% smaller payload   •   Common header: 17 bits   •   Complete sizes: 2065 → 529 bits",
             ha="center", fontsize=11)
    fig.savefig(Path.cwd() / "lossy-block-image-comparison.png", facecolor="white",
                transparent=False)
    plt.close(fig)
    print({"original_payload_bits": image.size * 8, "stored_payload_bits": stored.size * 8,
           "file_bits": len(bits), "padded_file_bits": 536, "mse": mse,
           "mse_unrounded": mse_unrounded, "identical_encoding_of_distinct_sources": True})


if __name__ == "__main__":
    main()