brulee-1's picture
Add files using upload-large-folder tool
6debdcc verified
Raw
History Blame Contribute Delete
7.27 kB
"""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))