from dataclasses import dataclass import cv2 import numpy as np from models import FingerQualityResult, QualityFeedback @dataclass class QualityConfig: target_width: int = 640 blur_min: float = 60.0 illum_min: float = 50.0 illum_max: float = 200.0 coverage_min: float = 0.10 orientation_max_deviation: float = 45.0 vertical_expected: bool = True overall_quality_threshold: float = 0.70 class QualityAnalyzer: def __init__(self, config: QualityConfig): self.config = config @staticmethod def resize_keep_aspect(img: np.ndarray, target_width: int) -> np.ndarray: h, w = img.shape[:2] if w == target_width: return img scale = target_width / float(w) new_size = (target_width, int(round(h * scale))) return cv2.resize(img, new_size, interpolation=cv2.INTER_AREA) @staticmethod def blur_score_laplacian(gray: np.ndarray) -> float: # Variance-of-Laplacian focus measure. [page:1] return float(cv2.Laplacian(gray, cv2.CV_64F).var()) def orientation_pass(self, angle_deg: float) -> bool: if self.config.vertical_expected: dev = min(abs(abs(angle_deg) - 90.0), abs(angle_deg)) else: dev = abs(angle_deg) return dev <= self.config.orientation_max_deviation def compute_quality_score( self, blur_score: float, illumination_score: float, coverage_ratio: float, orientation_ok: bool ) -> float: blur_norm = np.clip(blur_score / (self.config.blur_min * 2.0), 0.0, 1.0) illum_center = (self.config.illum_min + self.config.illum_max) / 2.0 illum_range = (self.config.illum_max - self.config.illum_min) / 2.0 illum_norm = 1.0 - np.clip(abs(illumination_score - illum_center) / (illum_range + 1e-6), 0.0, 1.0) coverage_norm = np.clip(coverage_ratio / (self.config.coverage_min * 2.0), 0.0, 1.0) orient_norm = 1.0 if orientation_ok else 0.0 w_blur, w_illum, w_cov, w_orient = 0.35, 0.25, 0.25, 0.15 return float(w_blur * blur_norm + w_illum * illum_norm + w_cov * coverage_norm + w_orient * orient_norm) def generate_feedback(self, result: FingerQualityResult) -> QualityFeedback: """ User-friendly feedback with ranges consistent with your thresholds: - blur_min (default 60): below ~0.6x is "very blurry", below threshold is "slightly blurry". - illum_min/illum_max: too dark, too bright, or OK. - coverage_min: very small (<0.5x), small (0.70), OK. - orientation_max_deviation: error if >1.5x deviation, warning if >threshold. """ fb = QualityFeedback() # Blur if result.blur_score < 0.6 * self.config.blur_min: fb.add("blur", "Image is very blurry. Hold phone steady and tap to focus.", "error") elif result.blur_score < self.config.blur_min: fb.add("blur", "Image is slightly blurry. Try holding your phone steadier.", "warning") else: fb.add("blur", "Image sharpness is good.", "success") # Illumination if result.illumination_score < 0.8 * self.config.illum_min: fb.add("light", "Image is too dark. Move to a well-lit area.", "error") elif result.illumination_score < self.config.illum_min: fb.add("light", "Lighting is dim. Move closer to a light source.", "warning") elif result.illumination_score > self.config.illum_max: fb.add("light", "Image is overexposed. Avoid direct bright light.", "warning") else: fb.add("light", "Lighting conditions are good.", "success") # Coverage if result.coverage_ratio < 0.5 * self.config.coverage_min: fb.add("position", "Finger too small. Move phone closer to your finger.", "error") elif result.coverage_ratio < self.config.coverage_min: fb.add("position", "Finger appears small. Move the camera closer.", "warning") elif result.coverage_ratio > 0.70: fb.add("position", "Finger too close. Move phone slightly away.", "warning") else: fb.add("position", "Finger positioning is good.", "success") # Orientation # compute deviation using same logic as pass/fail angle_deg = result.orientation_angle_deg if self.config.vertical_expected: dev = min(abs(abs(angle_deg) - 90.0), abs(angle_deg)) else: dev = abs(angle_deg) if dev > 1.5 * self.config.orientation_max_deviation: fb.add("orientation", "Finger is tilted too much. Align finger straight.", "error") elif dev > self.config.orientation_max_deviation: fb.add("orientation", "Finger is slightly tilted. Try to keep it straighter.", "warning") else: fb.add("orientation", "Finger orientation is correct.", "success") # Overall if result.overall_pass: fb.add("overall", "Capture is acceptable.", "success") else: fb.add("overall", "Capture is not acceptable. Fix the issues above and retake.", "error") return fb