leechard / scripts /preview_gemini_face_crop.py
nenae18's picture
Deploy LeeChard
5d3c2a9 verified
Raw
History Blame Contribute Delete
4.21 kB
"""Offline face-crop preview — NO Gemini call, NO API key required.
Loads an image, normalizes EXIF orientation, resolves the face/crop box (auto
detection or a manual override), validates it, and writes:
- crops/preview-face-crop-<tag>.png (the crop that would be sent)
- comparison/preview-face-crop-box-<tag>.png (original w/ face+crop boxes | crop)
so a human can confirm the crop is correct BEFORE spending a real Gemini call.
Logs box coordinates only (no image data / no personal data, no API key).
Exit codes: 0 valid crop, 2 invalid/refused (e.g. forehead-only or no face box).
"""
from __future__ import annotations
import argparse
import sys
from io import BytesIO
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from PIL import Image, ImageDraw # noqa: E402
from app.services.gemini_client import ( # noqa: E402
crop_to_box,
normalize_image_orientation_bytes,
resolve_face_and_crop,
)
from app.services.photo_qc import get_photo_qc # noqa: E402
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Offline face-crop preview (no API call)")
parser.add_argument("--input-image", required=True)
parser.add_argument("--evidence-root", default="runtime/gemini-smoke-evidence")
parser.add_argument("--tag", default="crop-preview-01")
args = parser.parse_args(argv)
input_path = Path(args.input_image)
if not input_path.exists():
print(f"REFUSED: input image not found: {input_path}")
return 2
smoke = Path(args.evidence_root) / "gemini-smoke"
crop_png = smoke / "crops" / f"preview-face-crop-{args.tag}.png"
overlay_png = smoke / "comparison" / f"preview-face-crop-box-{args.tag}.png"
for p in (crop_png, overlay_png):
p.parent.mkdir(parents=True, exist_ok=True)
image_bytes = normalize_image_orientation_bytes(input_path.read_bytes())
orig = Image.open(BytesIO(image_bytes)).convert("RGB")
qc = get_photo_qc().check_input(image_bytes, "image/png")
fallback = qc.face_boxes[0] if qc.passed and qc.face_boxes else None
resolved = resolve_face_and_crop(image_bytes, orig.size, fallback)
face_box = resolved["face_box"]
crop_box = resolved["crop_box"]
validation = resolved["validation"]
# Overlay: original with face_box (green) and crop_box (red).
overlay = orig.copy()
draw = ImageDraw.Draw(overlay)
if face_box:
fx, fy, fw, fh = face_box
draw.rectangle([fx, fy, fx + fw, fy + fh], outline=(0, 220, 0), width=4)
draw.text((fx + 4, fy + 4), "face", fill=(0, 220, 0))
if crop_box:
draw.rectangle(list(crop_box), outline=(255, 40, 40), width=4)
draw.text((crop_box[0] + 4, crop_box[1] + 4), "crop", fill=(255, 40, 40))
panels = [("orig + boxes", overlay)]
if crop_box:
crop_img = Image.open(BytesIO(crop_to_box(image_bytes, crop_box))).convert("RGB")
crop_img.save(crop_png)
panels.append(("crop", crop_img))
h = 420
thumbs = [(lbl, im.resize((max(1, int(im.width * h / im.height)), h))) for lbl, im in panels]
pad, label_h = 12, 26
total_w = sum(t.width for _, t in thumbs) + pad * (len(thumbs) + 1)
canvas = Image.new("RGB", (total_w, h + label_h + pad * 2), (245, 245, 245))
cd = ImageDraw.Draw(canvas)
x = pad
for lbl, t in thumbs:
canvas.paste(t, (x, pad + label_h))
cd.text((x, pad + 6), lbl, fill=(20, 20, 20))
x += t.width + pad
canvas.save(overlay_png)
print(f"image_size={orig.size}")
print(f"face_box={face_box} (source={resolved['face_source']})")
print(f"crop_box={crop_box} (source={resolved['crop_source']})")
print(f"preview_crop={crop_png.name}")
print(f"preview_overlay={overlay_png.name}")
if validation.ok:
print("crop_valid=YES")
for w in validation.warnings:
print(f"WARN: {w}")
return 0
print(f"crop_valid=NO ({validation.hard_fail})")
print('Set a manual crop box (normalized 0..1) and re-run, e.g.:')
print(' $env:GEMINI_FACE_CROP_BOX_NORM = "0.25,0.30,0.75,0.85"')
return 2
if __name__ == "__main__":
sys.exit(main())