import numpy as np import cv2 from dataclasses import dataclass from typing import Optional from src.geometry import FaceGeometry @dataclass class QualityReport: passed: bool score: float warnings: list[str] blocking: Optional[str] = None blocking_code: Optional[str] = None def _blocking(code: str, message: str) -> QualityReport: return QualityReport( passed=False, score=0.0, warnings=[], blocking=message, blocking_code=code, ) def assess_quality(landmarks, img) -> QualityReport: h, w = img.shape[:2] geo = FaceGeometry(landmarks) warnings = [] gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) brightness = np.mean(gray) if brightness < 40: warnings.append("Image is too dark — try better lighting") elif brightness > 230: warnings.append("Image is overexposed — try softer lighting") face_width_px = geo.face_width() min_face_px = min(w, h) * 0.15 if face_width_px < min_face_px: return _blocking( "FACE_TOO_SMALL", "Face too small — move closer to the camera", ) nose_x = geo.nose()[0] left_x = geo.left_eye()[0] right_x = geo.right_eye()[0] eye_mid_x = (left_x + right_x) / 2 eye_dist = abs(left_x - right_x) yaw_offset = abs(nose_x - eye_mid_x) / eye_dist if eye_dist > 0 else 0 if yaw_offset > 0.35: return _blocking( "FACE_ROTATED", "Please face the camera directly", ) elif yaw_offset > 0.25: warnings.append("Head is turned — results may be less accurate") chin_y = geo.chin()[1] forehead_y = geo.forehead_top()[1] face_h = abs(chin_y - forehead_y) nose_y = geo.nose()[1] expected_y = (chin_y + forehead_y) / 2 pitch_offset = abs(nose_y - expected_y) / face_h if face_h > 0 else 0 if pitch_offset > 0.25: return _blocking( "FACE_TILTED", "Head is tilted too much — try a straight-on photo", ) elif pitch_offset > 0.15: warnings.append("Head is tilted up or down - try a straight-on photo") left_cheek_x = geo.left_cheek()[0] right_cheek_x = geo.right_cheek()[0] mid_x = (left_cheek_x + right_cheek_x) / 2 nose_offset = abs(geo.nose()[0] - mid_x) / face_width_px if nose_offset > 0.20: return _blocking( "POOR_ALIGNMENT", "Could not align landmarks — try a clearer, well-lit photo", ) if nose_offset > 0.12: warnings.append("Unusual landmark alignment - lighting or angle may be off") score = max(0.1, 1.0 - len(warnings) * 0.2) return QualityReport(passed=True, score=round(score, 2), warnings=warnings)