verifile-x-api / backend /utils /image_quality.py
abinazebinoy's picture
fix: add PIL MAX_IMAGE_PIXELS=50M and explicit >10000px rejection (audit #6)
35363b1
Raw
History Blame Contribute Delete
4 kB
"""
Image Quality Gate for VeriFile-X.
Checks image quality before running expensive forensic analysis.
Rejects or flags images that are too small, corrupt, or unreadable.
Quality tiers:
good - Full analysis, all signals active
degraded - Analysis runs but some signals unreliable
low - Analysis runs with reduced confidence
unsuitable - Reject (too small, corrupt)
"""
import logging
from typing import Dict, Any
from io import BytesIO
logger = logging.getLogger(__name__)
MIN_WIDTH = 64
MIN_HEIGHT = 64
RECOMMENDED_MIN_WIDTH = 256
RECOMMENDED_MIN_HEIGHT = 256
def assess_image_quality(image_bytes: bytes, filename: str = "unknown") -> Dict[str, Any]:
from PIL import Image
# Guard against decompression bombs (e.g. 1KB PNG β†’ 4GB bitmap).
# We set a generous but finite limit; forensic analysis does not
# need images larger than ~50 MP.
Image.MAX_IMAGE_PIXELS = 50_000_000
warnings = []
try:
img = Image.open(BytesIO(image_bytes))
width, height = img.size
# Explicit upper-bound check (belt + suspenders alongside MAX_IMAGE_PIXELS)
if width > 10_000 or height > 10_000:
return {
"tier": "unsuitable", "suitable": False,
"width": width, "height": height, "pixel_count": width * height,
"format": img.format or "unknown", "mode": img.mode,
"warnings": [f"Image too large ({width}x{height}). Maximum: 10000x10000"],
"confidence_cap": 0.0,
"reason": f"Image dimensions {width}x{height} exceed the 10000Γ—10000 analysis limit.",
}
fmt = img.format or "unknown"
mode = img.mode
pixels = width * height
except Exception:
logger.warning("Image quality check failed for %s", filename)
return {
"tier": "unsuitable", "suitable": False,
"width": 0, "height": 0, "pixel_count": 0,
"format": "unknown", "mode": "unknown",
"warnings": ["Image could not be decoded"],
"confidence_cap": 0.0,
"reason": "Image is corrupt or unreadable",
}
if width < MIN_WIDTH or height < MIN_HEIGHT:
return {
"tier": "unsuitable", "suitable": False,
"width": width, "height": height, "pixel_count": pixels,
"format": fmt, "mode": mode,
"warnings": [f"Image too small ({width}x{height}). Minimum: {MIN_WIDTH}x{MIN_HEIGHT}"],
"confidence_cap": 0.0,
"reason": f"Image {width}x{height} is below minimum {MIN_WIDTH}x{MIN_HEIGHT} for forensic analysis.",
}
if width < RECOMMENDED_MIN_WIDTH or height < RECOMMENDED_MIN_HEIGHT:
warnings.append(f"Image is small ({width}x{height}). Less reliable below {RECOMMENDED_MIN_WIDTH}x{RECOMMENDED_MIN_HEIGHT}.")
if mode in ("1", "P"):
warnings.append(f"Color mode {mode} has limited forensic information.")
if mode == "L":
warnings.append("Grayscale image β€” color-based signals will be inactive.")
if mode == "RGBA":
warnings.append("RGBA image β€” alpha channel ignored.")
bytes_per_pixel = len(image_bytes) / max(pixels, 1)
if fmt == "JPEG" and bytes_per_pixel < 0.3:
warnings.append(f"Heavily compressed JPEG ({bytes_per_pixel:.2f} bytes/pixel) may interfere with some signals.")
if width < RECOMMENDED_MIN_WIDTH or height < RECOMMENDED_MIN_HEIGHT:
tier, confidence_cap = "low", 0.70
elif warnings:
tier, confidence_cap = "degraded", 0.85
else:
tier, confidence_cap = "good", 1.0
logger.info("Image quality %s: tier=%s (%dx%d %s %s)", filename, tier, width, height, fmt, mode)
return {
"tier": tier, "suitable": True,
"width": width, "height": height, "pixel_count": pixels,
"format": fmt, "mode": mode,
"warnings": warnings, "confidence_cap": confidence_cap, "reason": None,
}