47 lines
2.0 KiB
Python
47 lines
2.0 KiB
Python
"""Optional image metadata and computer-vision hints for image observations."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from typing import Any
|
|
|
|
|
|
def image_metadata(data: bytes) -> dict[str, Any]:
|
|
"""Read safe image metadata without changing the original upload."""
|
|
try:
|
|
from PIL import Image, ImageOps
|
|
with Image.open(io.BytesIO(data)) as image:
|
|
normalized = ImageOps.exif_transpose(image)
|
|
return {
|
|
"width": int(normalized.width),
|
|
"height": int(normalized.height),
|
|
"format": str(image.format or "").lower(),
|
|
"orientation": "landscape" if normalized.width >= normalized.height else "portrait",
|
|
"has_alpha": "A" in normalized.getbands(),
|
|
}
|
|
except Exception as error:
|
|
return {"error": f"image metadata unavailable: {type(error).__name__}"}
|
|
|
|
|
|
def cv_hints(data: bytes) -> dict[str, Any]:
|
|
"""Return conservative CV hints; OpenCV is intentionally optional."""
|
|
try:
|
|
import cv2 # type: ignore
|
|
import numpy as np # type: ignore
|
|
except Exception:
|
|
return {"available": False, "hints": []}
|
|
try:
|
|
image = cv2.imdecode(np.frombuffer(data, dtype=np.uint8), cv2.IMREAD_GRAYSCALE)
|
|
if image is None:
|
|
return {"available": True, "hints": [], "error": "image decode failed"}
|
|
edges = cv2.Canny(image, 50, 150)
|
|
lines = cv2.HoughLinesP(edges, 1, 3.141592653589793 / 180, threshold=50, minLineLength=30, maxLineGap=8)
|
|
line_hints = []
|
|
for line in (lines[:32] if lines is not None else []):
|
|
x1, y1, x2, y2 = [int(value) for value in line[0]]
|
|
line_hints.append({"type": "line", "start_px": [x1, y1], "end_px": [x2, y2]})
|
|
return {"available": True, "hints": line_hints, "edge_pixels": int((edges > 0).sum())}
|
|
except Exception as error:
|
|
return {"available": True, "hints": [], "error": f"cv failed: {type(error).__name__}"}
|
|
|