Add project files
This commit is contained in:
95
pipeline/utils.py
Normal file
95
pipeline/utils.py
Normal file
@@ -0,0 +1,95 @@
|
||||
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
|
||||
Reference in New Issue
Block a user