| """Study plots and report helpers.""" |
|
|
| from __future__ import annotations |
|
|
| import csv |
| from collections.abc import Sequence |
| from pathlib import Path |
|
|
| import numpy as np |
| from PIL import Image, ImageDraw |
|
|
|
|
| def write_metrics_csv(path: Path, rows: Sequence[dict[str, object]]) -> None: |
| """raw rowsからmetrics.csvを生成する。""" |
| path.parent.mkdir(parents=True, exist_ok=True) |
| keys = sorted({key for row in rows for key in row}) |
| with path.open("w", newline="", encoding="utf-8") as stream: |
| writer = csv.DictWriter(stream, fieldnames=keys, extrasaction="ignore") |
| writer.writeheader() |
| writer.writerows(rows) |
|
|
|
|
| def write_layer_metrics_csv(path: Path, rows: Sequence[dict[str, object]]) -> None: |
| """rawのlayers配列を正規化してlayer_metrics.csvへ保存する。""" |
| flattened: list[dict[str, object]] = [] |
| for row in rows: |
| for layer in row.get("layers", []): |
| flattened.append({"condition": row.get("condition"), **layer}) |
| write_metrics_csv(path, flattened) |
|
|
|
|
| def make_layer_plot(rows: Sequence[dict[str, object]], path: Path) -> None: |
| """layer-wise L2/MSE/cosine/changed plotを生成する。""" |
| import matplotlib.pyplot as plt |
| values = [(str(row.get("condition")), layer) for row in rows for layer in row.get("layers", [])] |
| if not values: |
| return |
| figure, axes = plt.subplots(2, 2, figsize=(12, 8), dpi=300) |
| for axis, key in zip(axes.flat, ("l2", "mse", "cosine", "changed_percent")): |
| for condition in sorted({name for name, _ in values}): |
| points = [float(layer[key]) for name, layer in values if name == condition and layer.get(key) is not None and np.isfinite(float(layer[key]))] |
| if points: |
| layer_names = [str(layer.get("layer")) for name, layer in values if name == condition and layer.get(key) is not None and np.isfinite(float(layer[key]))] |
| axis.plot(points, marker=".", label=condition) |
| axis.set_xticks(range(len(layer_names)), layer_names, rotation=60, ha="right", fontsize=5) |
| axis.set_title(key) |
| axis.grid(alpha=0.2) |
| axes[0, 0].legend(fontsize=6) |
| figure.tight_layout() |
| path.parent.mkdir(parents=True, exist_ok=True) |
| figure.savefig(path, dpi=300) |
| plt.close(figure) |
|
|
|
|
| def make_grid(images: Sequence[tuple[Image.Image, str]], path: Path, columns: int = 4) -> None: |
| """画像とラベルから比較 grid を作成する。""" |
| if not images: |
| return |
| cell = max(max(image.width, image.height) for image, _ in images) |
| label_height = 28 |
| rows = (len(images) + columns - 1) // columns |
| canvas = Image.new("RGB", (columns * cell, rows * (cell + label_height)), "white") |
| draw = ImageDraw.Draw(canvas) |
| for index, (image, label) in enumerate(images): |
| row, column = divmod(index, columns) |
| resized = image.convert("RGB").resize((cell, cell)) |
| x, y = column * cell, row * (cell + label_height) |
| canvas.paste(resized, (x, y)) |
| draw.text((x + 3, y + cell + 4), label[:36], fill="black") |
| canvas.save(path, dpi=(300, 300)) |
|
|
|
|
| def make_diff_maps(reference: np.ndarray, candidate: np.ndarray, stem: Path) -> None: |
| """signed/absolute pixel difference heatmapを保存する。""" |
| import matplotlib.pyplot as plt |
| diff = candidate.astype(float) - reference.astype(float) |
| for suffix, data, cmap in (("signed", diff.mean(axis=2), "coolwarm"), ("absolute", np.abs(diff).mean(axis=2), "magma")): |
| figure, axis = plt.subplots(figsize=(5, 5), dpi=300) |
| axis.imshow(data, cmap=cmap) |
| axis.set_axis_off() |
| figure.tight_layout(pad=0) |
| figure.savefig(stem.with_name(f"{stem.name}_{suffix}.png"), dpi=300, bbox_inches="tight", pad_inches=0) |
| plt.close(figure) |
|
|
|
|
| def make_quality_plot(rows: Sequence[dict[str, object]], path: Path) -> None: |
| """PSNR/storageのpublication-quality plotを保存する。""" |
| import matplotlib.pyplot as plt |
| valid = [row for row in rows if row.get("success") and row.get("psnr") is not None] |
| if not valid: |
| return |
| figure, axis = plt.subplots(figsize=(8, 5), dpi=300) |
| axis.scatter([float(row["compression_ratio"]) for row in valid], [float(row["psnr"]) for row in valid], s=24) |
| axis.set_xlabel("Compression ratio (PNG bytes / candidate bytes)") |
| axis.set_ylabel("Image PSNR (dB)") |
| axis.grid(alpha=0.25) |
| figure.tight_layout() |
| figure.savefig(path, dpi=300) |
| plt.close(figure) |
|
|