"""Image quality metrics — brightness, contrast, sharpness, noise, composite. Consolidates the heuristic quality scoring that was previously duplicated between the image_quality provider and the duplicate_detector provider. """ from __future__ import annotations import cv2 import numpy as np from cores.vision.color import to_gray def brightness(img: np.ndarray) -> float: """Mean pixel intensity (0-255).""" return float(np.mean(to_gray(img))) def contrast(img: np.ndarray) -> float: """Standard deviation of pixel intensity.""" return float(np.std(to_gray(img))) def sharpness(img: np.ndarray) -> float: """Variance of Laplacian — higher = sharper.""" gray = to_gray(img) return float(cv2.Laplacian(gray, cv2.CV_64F).var()) def noise_level(img: np.ndarray) -> float: """Estimate noise via median absolute deviation of the Laplacian. Robust, simple, no model required. """ gray = to_gray(img) lap = cv2.Laplacian(gray, cv2.CV_64F) return float(np.median(np.abs(lap - np.median(lap))) / 0.6745) def quality_score(img: np.ndarray) -> float: """Composite 0-1 quality score (heuristic). Combines brightness, contrast, sharpness, and noise into a single 0-1 score where 1.0 = excellent quality. """ b = brightness(img) c = contrast(img) s = sharpness(img) n = noise_level(img) # Brightness: ideal ~128 b_score = 1.0 - min(1.0, abs(b - 128.0) / 128.0) # Contrast: ideal std ~50-80 c_score = 1.0 - min(1.0, abs(c - 60.0) / 100.0) # Sharpness: log-scale, ~100 = good, ~1000 = excellent s_score = min(1.0, np.log1p(s) / np.log1p(1000.0)) # Noise: lower is better; >20 is bad n_score = max(0.0, 1.0 - n / 30.0) return 0.25 * (b_score + c_score + s_score + n_score)