from __future__ import annotations from dataclasses import asdict, dataclass from pathlib import Path import numpy as np from PIL import Image @dataclass(frozen=True) class QualityMetrics: path: str width: int height: int alpha_min: int alpha_max: int alpha_mean: float nonzero_alpha_pixels: int edge_pixels: int edge_green_excess_mean: float edge_green_excess_p95: float edge_green_excess_max: float def to_dict(self) -> dict: return asdict(self) def measure_rgba(path: str | Path) -> QualityMetrics: image = Image.open(path).convert("RGBA") arr = np.asarray(image).astype(np.float32) / 255.0 rgb = arr[..., :3] alpha = arr[..., 3] edge = (alpha > 0.02) & (alpha < 0.98) green_excess = np.maximum(rgb[..., 1] - np.maximum(rgb[..., 0], rgb[..., 2]), 0.0) edge_values = green_excess[edge] if edge_values.size: edge_mean = float(edge_values.mean()) edge_p95 = float(np.percentile(edge_values, 95)) edge_max = float(edge_values.max()) else: edge_mean = edge_p95 = edge_max = 0.0 alpha_u8 = (alpha * 255.0).round().astype(np.uint8) return QualityMetrics( path=str(path), width=image.width, height=image.height, alpha_min=int(alpha_u8.min()), alpha_max=int(alpha_u8.max()), alpha_mean=float(alpha_u8.mean()), nonzero_alpha_pixels=int((alpha_u8 > 0).sum()), edge_pixels=int(edge.sum()), edge_green_excess_mean=edge_mean, edge_green_excess_p95=edge_p95, edge_green_excess_max=edge_max, ) def assert_quality( metrics: QualityMetrics, max_edge_green_excess_p95: float | None = None, require_alpha_range: bool = True, ) -> list[str]: failures: list[str] = [] if require_alpha_range: if metrics.alpha_min != 0: failures.append(f"alpha_min is {metrics.alpha_min}, expected 0") if metrics.alpha_max != 255: failures.append(f"alpha_max is {metrics.alpha_max}, expected 255") if metrics.nonzero_alpha_pixels <= 0: failures.append("nonzero alpha pixel count is 0") if ( max_edge_green_excess_p95 is not None and metrics.edge_green_excess_p95 > max_edge_green_excess_p95 ): failures.append( "edge_green_excess_p95 " f"{metrics.edge_green_excess_p95:.4f} exceeds {max_edge_green_excess_p95:.4f}" ) return failures