Spaces:
Running
Running
File size: 5,219 Bytes
e735bf3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
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 (<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
|