| """Image enhancement for varying conditions (low light, rain, noise). Geometry is |
| preserved (no resize/crop) so detection boxes stay valid on the original image, which is |
| what we annotate and hash for evidence. |
| |
| Prefers OpenCV (CLAHE + denoise + gamma); falls back to a Pillow-only path so the code |
| runs without cv2 installed. ``enhance`` writes to a temp file and returns its path. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import tempfile |
|
|
|
|
| def _enhance_cv2(src: str, dst: str) -> None: |
| import cv2 |
| import numpy as np |
|
|
| img = cv2.imread(src) |
| if img is None: |
| raise ValueError("cv2 could not read image") |
| lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) |
| lo, a, b = cv2.split(lab) |
| clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) |
| lo = clahe.apply(lo) |
| img = cv2.cvtColor(cv2.merge((lo, a, b)), cv2.COLOR_LAB2BGR) |
| img = cv2.fastNlMeansDenoisingColored(img, None, 5, 5, 7, 21) |
| if img.mean() < 70: |
| inv = 1.0 / 0.6 |
| table = ((np.arange(256) / 255.0) ** inv * 255).astype("uint8") |
| img = cv2.LUT(img, table) |
| cv2.imwrite(dst, img) |
|
|
|
|
| def _enhance_pil(src: str, dst: str) -> None: |
| from PIL import Image, ImageFilter, ImageOps |
|
|
| img = Image.open(src).convert("RGB") |
| img = ImageOps.autocontrast(img, cutoff=1) |
| img = img.filter(ImageFilter.MedianFilter(size=3)) |
| img.save(dst, format="JPEG") |
|
|
|
|
| def enhance(src: str) -> str: |
| fd, dst = tempfile.mkstemp(suffix=".jpg") |
| os.close(fd) |
| try: |
| _enhance_cv2(src, dst) |
| except Exception: |
| _enhance_pil(src, dst) |
| return dst |
|
|
|
|
| def cleanup(path: str, keep: str) -> None: |
| if path != keep and os.path.exists(path): |
| try: |
| os.remove(path) |
| except OSError: |
| pass |
|
|