Spaces:
Running
Running
| from dataclasses import dataclass | |
| import cv2 | |
| import numpy as np | |
| from models import FingerQualityResult, QualityFeedback | |
| 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 | |
| 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) | |
| 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 (<threshold), too close (>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 | |