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