Files
Vase-Matcher/pipeline/utils.py
2026-08-04 17:09:29 +05:30

96 lines
3.1 KiB
Python

import io
import logging
import os
import cv2
from PIL import Image, ImageOps
import config
logger = logging.getLogger(__name__)
def list_template_files():
if not os.path.isdir(config.TEMPLATE_IMAGES_DIR):
raise FileNotFoundError(f"Template folder not found: {config.TEMPLATE_IMAGES_DIR}")
return sorted(
f for f in os.listdir(config.TEMPLATE_IMAGES_DIR)
if f.lower().endswith(config.VALID_EXTS)
)
def template_name(fname):
return os.path.splitext(fname)[0]
def annotate_score(bgr_img, score, confidence_pct, label=None):
img = bgr_img.copy()
text = f"Score: {score} Conf: {confidence_pct:.1f}%"
if label:
text = f"{label} | {text}"
font = cv2.FONT_HERSHEY_SIMPLEX
scale = max(0.55, img.shape[1] / 900)
thickness = max(1, int(scale * 2))
(tw, th), baseline = cv2.getTextSize(text, font, scale, thickness)
cv2.rectangle(img, (5, 5), (15 + tw, 20 + th + baseline), (20, 20, 20), -1)
cv2.putText(img, text, (10, 15 + th), font, scale, (110, 231, 183), thickness,
cv2.LINE_AA)
return img
def encode_png_bytes(bgr_or_bgra_img):
ok, buf = cv2.imencode(".png", bgr_or_bgra_img)
if not ok:
raise RuntimeError("Failed to encode image to PNG")
return buf.tobytes()
def compress_image_bytes(image_bytes: bytes, max_bytes: int, max_dim: int) -> tuple:
"""
Downscale + re-encode as JPEG only if image_bytes exceeds max_bytes;
otherwise returns it untouched -- images already under the limit are
never re-compressed, so nothing is lost for the common case.
Downscaling to max_dim costs no *usable* detail here: the matching
pipeline (bg removal, SIFT/ORB/SuperPoint/LoFTR) already caps every
image to this same size before processing it, and the external AI
verification endpoint's vision model downsamples internally to its own
fixed input resolution regardless. This just stops storing/transmitting
pixels nothing in the system ever actually looks at.
Returns (bytes, was_compressed).
"""
if len(image_bytes) <= max_bytes:
return image_bytes, False
original_size = len(image_bytes)
pil_img = Image.open(io.BytesIO(image_bytes))
pil_img = ImageOps.exif_transpose(pil_img) # bake in camera rotation before resizing
pil_img = pil_img.convert("RGB")
w, h = pil_img.size
scale = max_dim / max(w, h)
if scale < 1.0:
pil_img = pil_img.resize((max(1, int(w * scale)), max(1, int(h * scale))),
Image.LANCZOS)
quality = config.COMPRESS_JPEG_QUALITY_START
data = None
while True:
buf = io.BytesIO()
pil_img.save(buf, format="JPEG", quality=quality, optimize=True)
data = buf.getvalue()
if len(data) <= max_bytes or quality <= config.COMPRESS_JPEG_QUALITY_MIN:
break
quality -= config.COMPRESS_JPEG_QUALITY_STEP
logger.info(
"Compressed upload: %.1f MB -> %.1f MB (%dx%d, JPEG q%d)",
original_size / (1024 * 1024), len(data) / (1024 * 1024),
pil_img.width, pil_img.height, quality,
)
return data, True