""" 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, }