FINGERQUALITYAPI / quality_analyzer.py
sol9x-sagar's picture
initial setup
e735bf3
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