"""Weight and image quality metrics.""" from __future__ import annotations import math from collections.abc import Mapping, Sequence import numpy as np def finite_metrics(values: np.ndarray) -> dict[str, int | float]: """NaN/Inf/subnormalを修復せずに数える。""" array = np.asarray(values) finite = np.isfinite(array) subnormal = np.isfinite(array) & (array != 0) & (np.abs(array) < np.finfo(array.dtype).tiny) if np.issubdtype(array.dtype, np.floating) else np.zeros(array.shape, dtype=bool) total = int(array.size) return { "total_count": total, "finite_only_count": int(finite.sum()), "finite_rate": float(finite.mean()) if total else 1.0, "nan_count": int(np.isnan(array).sum()), "posinf_count": int(np.isposinf(array).sum()), "neginf_count": int(np.isneginf(array).sum()), "subnormal_count": int(subnormal.sum()), "subnormal_rate": float(subnormal.mean()) if total else 0.0, } def channel_change_metrics(reference: np.ndarray, candidate: np.ndarray) -> dict[str, int | float]: """RGBのbyte/bit/pixel変更数とrateを計算する。""" left = np.asarray(reference, dtype=np.uint8) right = np.asarray(candidate, dtype=np.uint8) if left.shape != right.shape or left.ndim != 3 or left.shape[-1] != 3: raise ValueError("reference and candidate must be equal-shaped RGB arrays") changed = left != right bits = np.unpackbits(np.bitwise_xor(left, right), axis=-1).reshape(*left.shape[:2], 24) result: dict[str, int | float] = { "changed_pixel_count": int(changed.any(axis=2).sum()), "changed_pixel_rate": float(changed.any(axis=2).mean()), "changed_bit_count": int(bits.sum()), "changed_bit_rate": float(bits.mean()), } for index, name in enumerate(("red", "green", "blue")): result[f"{name}_changed_byte_count"] = int(changed[..., index].sum()) result[f"{name}_changed_byte_rate"] = float(changed[..., index].mean()) result[f"{name}_changed_bit_count"] = int(bits[..., index * 8:(index + 1) * 8].sum()) result[f"{name}_changed_bit_rate"] = float(bits[..., index * 8:(index + 1) * 8].mean()) return result def _finite_pair(reference: np.ndarray, candidate: np.ndarray) -> tuple[np.ndarray, np.ndarray]: mask = np.isfinite(reference) & np.isfinite(candidate) return reference[mask].astype(np.float64), candidate[mask].astype(np.float64) def _metrics(reference: np.ndarray, candidate: np.ndarray) -> dict[str, float]: ref, cand = _finite_pair(reference, candidate) if not len(ref): return {"l2": float("nan"), "mse": float("nan"), "cosine": float("nan"), "max_abs": float("nan"), "changed_percent": 100.0} diff = cand - ref denom = np.linalg.norm(ref) * np.linalg.norm(cand) return {"l2": float(np.linalg.norm(diff)), "mse": float(np.mean(diff * diff)), "cosine": float(np.dot(ref, cand) / denom) if denom else 1.0, "max_abs": float(np.max(np.abs(diff))), "changed_percent": float(np.mean(diff != 0) * 100)} def normalized_rmse(reference: np.ndarray, candidate: np.ndarray, eps: float = 1e-12) -> float: """有限要素のRMSEをreferenceのRMSで正規化する。""" ref, cand = _finite_pair(np.asarray(reference), np.asarray(candidate)) if not len(ref): return float("nan") return float(np.sqrt(np.mean((cand - ref) ** 2)) / (np.sqrt(np.mean(ref ** 2)) + eps)) def ulp_distance(reference: np.ndarray, candidate: np.ndarray) -> np.ndarray: """有限float32のbit patternを全順序へ写像してULP距離を返す。""" ref = np.asarray(reference, dtype=np.float32) cand = np.asarray(candidate, dtype=np.float32) mask = np.isfinite(ref) & np.isfinite(cand) ref_bits = ref.view(np.int32).astype(np.int64) cand_bits = cand.view(np.int32).astype(np.int64) ref_order = np.where(ref_bits < 0, 0x80000000 - ref_bits, ref_bits) cand_order = np.where(cand_bits < 0, 0x80000000 - cand_bits, cand_bits) return np.abs(ref_order[mask] - cand_order[mask]) def weight_metrics(reference: np.ndarray, candidate: np.ndarray, layers: Mapping[str, tuple[int, int]]) -> dict[str, object]: """NaN/Infを補完せずにglobal/layer weight metricsを計算する。""" ref = np.asarray(reference).reshape(-1) cand = np.asarray(candidate).reshape(-1) base = _metrics(ref, cand) finite = finite_metrics(np.asarray(candidate)) ulp = ulp_distance(reference, candidate) nonfinite_count = int((~np.isfinite(cand)).sum()) result: dict[str, object] = { "finite": bool(np.isfinite(cand).all()), "nonfinite_count": int((~np.isfinite(ref)).sum() + nonfinite_count), "reference_nonfinite_count": int((~np.isfinite(ref)).sum()), "candidate_nonfinite_count": nonfinite_count, "global_l2": base["l2"], "global_mse": base["mse"], "global_cosine": base["cosine"], "global_max_abs": base["max_abs"], "global_changed_percent": float(np.mean(ref != cand) * 100), "normalized_rmse": normalized_rmse(ref, cand), "ulp_p50": float(np.percentile(ulp, 50)) if len(ulp) else float("nan"), "ulp_p95": float(np.percentile(ulp, 95)) if len(ulp) else float("nan"), **finite, "layers": [], } result["layers"] = [] for name, (start, end) in layers.items(): raw_ref = np.asarray(reference).reshape(-1)[start:end] raw_cand = np.asarray(candidate).reshape(-1)[start:end] layer_ref, layer_cand = _finite_pair(raw_ref, raw_cand) result["layers"].append({"layer": name, **_metrics(layer_ref, layer_cand), "finite": bool(np.isfinite(raw_cand).all()), "nonfinite_count": int((~np.isfinite(raw_cand)).sum()), "normalized_rmse": normalized_rmse(raw_ref, raw_cand)}) return result def _ssim(reference: np.ndarray, candidate: np.ndarray) -> float: from skimage.metrics import structural_similarity size = min(reference.shape[:2]) win_size = min(7, size if size % 2 else size - 1) return float(structural_similarity(reference, candidate, channel_axis=2, data_range=255, win_size=win_size)) def image_metrics(reference: np.ndarray, candidate: np.ndarray, reference_size: int, file_size: int) -> dict[str, float | int]: """RGB画像のPSNR/SSIMとstorage metricsを返す。""" ref = np.asarray(reference, dtype=np.float64) cand = np.asarray(candidate, dtype=np.float64) mse = float(np.mean((ref - cand) ** 2)) psnr = float("inf") if mse == 0 else float(20 * math.log10(255.0 / math.sqrt(mse))) return {"image_mse": mse, "psnr": psnr, "ssim": max(-1.0, min(1.0, _ssim(ref, cand))), "file_size_bytes": int(file_size), "compression_ratio": float(reference_size / file_size) if file_size else float("inf")} def bootstrap_ci(values: Sequence[float], seed: int = 0, samples: int = 2000) -> tuple[float, float]: """paired値のpercentile bootstrap 95% CIを計算する。""" array = np.asarray(values, dtype=float) if array.size == 0: return float("nan"), float("nan") rng = np.random.default_rng(seed) means = np.array([rng.choice(array, array.size, replace=True).mean() for _ in range(samples)]) return float(np.percentile(means, 2.5)), float(np.percentile(means, 97.5))