ring-sizer / src /image_quality.py
feng-x's picture
Upload folder using huggingface_hub
347d1a8 verified
"""
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,
}