Spaces:
Running
Running
Commit ·
f31a0c8
1
Parent(s): ff4f595
feat(quality): add image quality gate before forensic analysis
Browse filesbackend/utils/image_quality.py — assess_image_quality() checks images
before expensive processing:
- Rejects images < 64x64 with HTTP 422
- Warns on images < 256x256 (reduced confidence_cap to 0.70)
- Warns on grayscale, RGBA, heavily-compressed JPEG
- Returns confidence_cap to cap signal confidence appropriately
analyze.py: quality gate added as Layer 4 between MIME validation and
cache lookup. Prevents wasted computation and inaccurate analysis on
solid-color, tiny, or corrupt images.
backend/utils/image_quality.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Image Quality Gate for VeriFile-X.
|
| 3 |
+
|
| 4 |
+
Checks image quality before running expensive forensic analysis.
|
| 5 |
+
Rejects or flags images that are too small, corrupt, or unreadable.
|
| 6 |
+
|
| 7 |
+
Quality tiers:
|
| 8 |
+
good - Full analysis, all signals active
|
| 9 |
+
degraded - Analysis runs but some signals unreliable
|
| 10 |
+
low - Analysis runs with reduced confidence
|
| 11 |
+
unsuitable - Reject (too small, corrupt)
|
| 12 |
+
"""
|
| 13 |
+
import logging
|
| 14 |
+
from typing import Dict, Any
|
| 15 |
+
from io import BytesIO
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
MIN_WIDTH = 64
|
| 20 |
+
MIN_HEIGHT = 64
|
| 21 |
+
|
| 22 |
+
RECOMMENDED_MIN_WIDTH = 256
|
| 23 |
+
RECOMMENDED_MIN_HEIGHT = 256
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def assess_image_quality(image_bytes: bytes, filename: str = "unknown") -> Dict[str, Any]:
|
| 27 |
+
from PIL import Image
|
| 28 |
+
warnings = []
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
img = Image.open(BytesIO(image_bytes))
|
| 32 |
+
width, height = img.size
|
| 33 |
+
fmt = img.format or "unknown"
|
| 34 |
+
mode = img.mode
|
| 35 |
+
pixels = width * height
|
| 36 |
+
except Exception:
|
| 37 |
+
logger.warning("Image quality check failed for %s", filename)
|
| 38 |
+
return {
|
| 39 |
+
"tier": "unsuitable", "suitable": False,
|
| 40 |
+
"width": 0, "height": 0, "pixel_count": 0,
|
| 41 |
+
"format": "unknown", "mode": "unknown",
|
| 42 |
+
"warnings": ["Image could not be decoded"],
|
| 43 |
+
"confidence_cap": 0.0,
|
| 44 |
+
"reason": "Image is corrupt or unreadable",
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
if width < MIN_WIDTH or height < MIN_HEIGHT:
|
| 48 |
+
return {
|
| 49 |
+
"tier": "unsuitable", "suitable": False,
|
| 50 |
+
"width": width, "height": height, "pixel_count": pixels,
|
| 51 |
+
"format": fmt, "mode": mode,
|
| 52 |
+
"warnings": [f"Image too small ({width}x{height}). Minimum: {MIN_WIDTH}x{MIN_HEIGHT}"],
|
| 53 |
+
"confidence_cap": 0.0,
|
| 54 |
+
"reason": f"Image {width}x{height} is below minimum {MIN_WIDTH}x{MIN_HEIGHT} for forensic analysis.",
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
if width < RECOMMENDED_MIN_WIDTH or height < RECOMMENDED_MIN_HEIGHT:
|
| 58 |
+
warnings.append(f"Image is small ({width}x{height}). Less reliable below {RECOMMENDED_MIN_WIDTH}x{RECOMMENDED_MIN_HEIGHT}.")
|
| 59 |
+
|
| 60 |
+
if mode in ("1", "P"):
|
| 61 |
+
warnings.append(f"Color mode {mode} has limited forensic information.")
|
| 62 |
+
if mode == "L":
|
| 63 |
+
warnings.append("Grayscale image — color-based signals will be inactive.")
|
| 64 |
+
if mode == "RGBA":
|
| 65 |
+
warnings.append("RGBA image — alpha channel ignored.")
|
| 66 |
+
|
| 67 |
+
bytes_per_pixel = len(image_bytes) / max(pixels, 1)
|
| 68 |
+
if fmt == "JPEG" and bytes_per_pixel < 0.3:
|
| 69 |
+
warnings.append(f"Heavily compressed JPEG ({bytes_per_pixel:.2f} bytes/pixel) may interfere with some signals.")
|
| 70 |
+
|
| 71 |
+
if width < RECOMMENDED_MIN_WIDTH or height < RECOMMENDED_MIN_HEIGHT:
|
| 72 |
+
tier, confidence_cap = "low", 0.70
|
| 73 |
+
elif warnings:
|
| 74 |
+
tier, confidence_cap = "degraded", 0.85
|
| 75 |
+
else:
|
| 76 |
+
tier, confidence_cap = "good", 1.0
|
| 77 |
+
|
| 78 |
+
logger.info("Image quality %s: tier=%s (%dx%d %s %s)", filename, tier, width, height, fmt, mode)
|
| 79 |
+
|
| 80 |
+
return {
|
| 81 |
+
"tier": tier, "suitable": True,
|
| 82 |
+
"width": width, "height": height, "pixel_count": pixels,
|
| 83 |
+
"format": fmt, "mode": mode,
|
| 84 |
+
"warnings": warnings, "confidence_cap": confidence_cap, "reason": None,
|
| 85 |
+
}
|