Spaces:
Running
Running
| import cv2 | |
| import numpy as np | |
| def compute_quality_score( | |
| swapped: np.ndarray, | |
| target: np.ndarray, | |
| src_landmarks, # type: np.ndarray | None | |
| tgt_landmarks, # type: np.ndarray | None | |
| ) -> dict: | |
| """ | |
| Compute quality metrics for the swap result. | |
| Returns dict: alignment, blend, delta_e, naturalness (all 0-100 scale). | |
| """ | |
| alignment = _alignment_score(src_landmarks, tgt_landmarks) | |
| blend = _blend_quality(swapped, target) | |
| delta_e = _compute_delta_e(swapped, target) | |
| naturalness = _naturalness_score(swapped) | |
| return { | |
| "alignment": alignment, | |
| "blend": blend, | |
| "delta_e": delta_e, | |
| "naturalness": naturalness, | |
| } | |
| def _alignment_score(src_lm, tgt_lm) -> float: | |
| """Mean pixel deviation between landmark sets, converted to 0-100 score.""" | |
| if src_lm is None or tgt_lm is None: | |
| return 50.0 | |
| n = min(len(src_lm), len(tgt_lm)) | |
| if n == 0: | |
| return 50.0 | |
| diff = np.linalg.norm(src_lm[:n] - tgt_lm[:n], axis=1) | |
| mean_err = float(diff.mean()) | |
| if not np.isfinite(mean_err): | |
| return 50.0 | |
| score = max(0.0, 100.0 - mean_err * 10.0) | |
| return round(score, 1) | |
| def _blend_quality(swapped: np.ndarray, target: np.ndarray) -> float: | |
| """ | |
| Estimate blend quality by measuring gradient discontinuities at the face region boundary. | |
| Higher = smoother boundary. | |
| """ | |
| if swapped.shape != target.shape: | |
| return 75.0 | |
| diff = cv2.absdiff(swapped, target).astype(np.float32) | |
| edges = cv2.Laplacian(diff, cv2.CV_32F) | |
| discontinuity = np.abs(edges).mean() | |
| # Map: 0 -> 100, 30 -> 0 | |
| score = max(0.0, 100.0 - discontinuity * (100.0 / 30.0)) | |
| return round(score, 1) | |
| def _compute_delta_e(swapped: np.ndarray, target: np.ndarray) -> float: | |
| """ | |
| Compute mean CIE ΔE (L*a*b* Euclidean) between swapped and target images. | |
| Lower is better (target < 10). | |
| """ | |
| if swapped.shape != target.shape: | |
| return 15.0 | |
| lab1 = cv2.cvtColor(swapped, cv2.COLOR_BGR2LAB).astype(np.float32) | |
| lab2 = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype(np.float32) | |
| # Rescale L* from 0-255 to 0-100, a* b* from 0-255 to -128..127 | |
| lab1[:, :, 0] *= 100.0 / 255.0 | |
| lab2[:, :, 0] *= 100.0 / 255.0 | |
| lab1[:, :, 1:] -= 128 | |
| lab2[:, :, 1:] -= 128 | |
| de = np.sqrt(((lab1 - lab2) ** 2).sum(axis=2)) | |
| return round(float(de.mean()), 2) | |
| def _naturalness_score(swapped: np.ndarray) -> float: | |
| """ | |
| Heuristic naturalness score: checks for colour clipping, excessive noise, | |
| and unnatural saturation levels. | |
| """ | |
| clipped = ( | |
| (swapped == 0).sum() + (swapped == 255).sum() | |
| ) / swapped.size | |
| clip_penalty = min(clipped * 500, 30.0) | |
| hsv = cv2.cvtColor(swapped, cv2.COLOR_BGR2HSV) | |
| mean_sat = hsv[:, :, 1].mean() | |
| # Penalise if saturation is very low (<20) or very high (>200) | |
| sat_penalty = max(0, 20 - mean_sat) + max(0, mean_sat - 200) * 0.5 | |
| noise = cv2.Laplacian(cv2.cvtColor(swapped, cv2.COLOR_BGR2GRAY), | |
| cv2.CV_64F).var() | |
| noise_penalty = min(noise / 1000.0, 20.0) | |
| score = max(0.0, 100.0 - clip_penalty - sat_penalty - noise_penalty) | |
| return round(score, 1) | |