File size: 5,493 Bytes
347d1a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""
Image quality assessment utilities.

This module handles:
- Blur detection using Laplacian variance
- Exposure/contrast analysis
- Overall quality scoring
"""

import cv2
import numpy as np
from typing import Dict, Any, Tuple


# Quality thresholds
BLUR_THRESHOLD = 20.0  # Laplacian variance below this is considered blurry
MIN_BRIGHTNESS = 40  # Mean brightness below this is underexposed
MAX_BRIGHTNESS = 220  # Mean brightness above this is overexposed
MIN_CONTRAST = 30  # Std dev below this indicates low contrast


def detect_blur(image: np.ndarray) -> Tuple[float, bool]:
    """
    Detect image blur using Laplacian variance method.

    The Laplacian operator highlights regions of rapid intensity change,
    so a well-focused image will have high variance in Laplacian response.

    Args:
        image: Input BGR image

    Returns:
        Tuple of (blur_score, is_sharp)
        - blur_score: Laplacian variance (higher = sharper)
        - is_sharp: True if image passes sharpness threshold
    """
    # Convert to grayscale if needed
    if len(image.shape) == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    else:
        gray = image

    # Compute Laplacian
    laplacian = cv2.Laplacian(gray, cv2.CV_64F)

    # Variance of Laplacian indicates focus quality
    blur_score = laplacian.var()

    is_sharp = blur_score >= BLUR_THRESHOLD

    return blur_score, is_sharp


def check_exposure(image: np.ndarray) -> Dict[str, Any]:
    """
    Check image exposure and contrast using histogram analysis.

    Args:
        image: Input BGR image

    Returns:
        Dictionary containing:
        - brightness: Mean brightness (0-255)
        - contrast: Standard deviation of brightness
        - is_underexposed: True if image is too dark
        - is_overexposed: True if image is too bright
        - has_good_contrast: True if contrast is sufficient
    """
    # Convert to grayscale if needed
    if len(image.shape) == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    else:
        gray = image

    # Calculate statistics
    brightness = float(np.mean(gray))
    contrast = float(np.std(gray))

    # Check exposure conditions
    is_underexposed = brightness < MIN_BRIGHTNESS
    is_overexposed = brightness > MAX_BRIGHTNESS
    has_good_contrast = contrast >= MIN_CONTRAST

    return {
        "brightness": brightness,
        "contrast": contrast,
        "is_underexposed": is_underexposed,
        "is_overexposed": is_overexposed,
        "has_good_contrast": has_good_contrast,
    }


def check_resolution(image: np.ndarray, min_dimension: int = 720) -> Dict[str, Any]:
    """
    Check if image resolution is sufficient.

    Args:
        image: Input BGR image
        min_dimension: Minimum acceptable dimension (default 720 for 720p)

    Returns:
        Dictionary containing:
        - width: Image width in pixels
        - height: Image height in pixels
        - is_sufficient: True if resolution meets minimum
    """
    height, width = image.shape[:2]
    min_dim = min(width, height)

    return {
        "width": width,
        "height": height,
        "is_sufficient": min_dim >= min_dimension,
    }


def assess_image_quality(image: np.ndarray) -> Dict[str, Any]:
    """
    Comprehensive image quality assessment.

    Combines blur detection, exposure check, and resolution check
    to determine if image is suitable for processing.

    Args:
        image: Input BGR image

    Returns:
        Dictionary containing:
        - passed: True if image passes all quality checks
        - blur_score: Laplacian variance score
        - brightness: Mean brightness
        - contrast: Standard deviation
        - resolution: (width, height)
        - issues: List of quality issues found
        - fail_reason: Primary failure reason if failed, else None
    """
    issues = []
    fail_reason = None

    # Check blur
    blur_score, is_sharp = detect_blur(image)
    if not is_sharp:
        issues.append(f"Image is blurry (score: {blur_score:.1f}, threshold: {BLUR_THRESHOLD})")
        if fail_reason is None:
            fail_reason = "image_too_blurry"

    # Check exposure
    exposure = check_exposure(image)
    if exposure["is_underexposed"]:
        issues.append(f"Image is underexposed (brightness: {exposure['brightness']:.1f})")
        if fail_reason is None:
            fail_reason = "image_underexposed"
    if exposure["is_overexposed"]:
        issues.append(f"Image is overexposed (brightness: {exposure['brightness']:.1f})")
        if fail_reason is None:
            fail_reason = "image_overexposed"
    if not exposure["has_good_contrast"]:
        issues.append(f"Image has low contrast (std: {exposure['contrast']:.1f})")
        if fail_reason is None:
            fail_reason = "image_low_contrast"

    # Check resolution
    resolution = check_resolution(image)
    if not resolution["is_sufficient"]:
        issues.append(
            f"Resolution too low ({resolution['width']}x{resolution['height']})"
        )
        if fail_reason is None:
            fail_reason = "image_resolution_too_low"

    passed = len(issues) == 0

    return {
        "passed": passed,
        "blur_score": round(blur_score, 2),
        "brightness": round(exposure["brightness"], 2),
        "contrast": round(exposure["contrast"], 2),
        "resolution": (resolution["width"], resolution["height"]),
        "issues": issues,
        "fail_reason": fail_reason,
    }