File size: 5,128 Bytes
5bf61fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | """Debug face-landmark detection (no Gemini call, no key).
Runs the OPTIONAL landmark backend on three inputs and records, separately, why
detection succeeds/fails:
1. original full image
2. original cropped to the face crop box
3. generated raw face crop
For each it writes a landmark overlay (bbox + eye/nose/mouth + contour on success;
size/crop/backend/error annotation on failure) and a QA markdown. No model
weights are committed. EXPERIMENTAL. Pilot Ready: NOT CONFIRMED.
"""
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 # noqa: E402
from app.services.face_pipeline.landmark_detector import ( # noqa: E402
detect_landmarks_result,
draw_landmark_overlay,
landmark_backend,
)
from app.services.gemini_client import normalize_image_orientation_bytes # noqa: E402
def _parse_norm(value: str):
parts = [float(p) for p in value.split(",")]
if len(parts) != 4:
raise ValueError("crop-box-norm must be 'x1,y1,x2,y2'")
return tuple(parts)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Debug face landmark detection (no API)")
parser.add_argument("--original", required=True)
parser.add_argument("--generated-crop", required=True)
parser.add_argument("--crop-box-norm", default="0.33,0.39,0.69,0.78")
parser.add_argument("--evidence-root", default="runtime/gemini-smoke-evidence")
parser.add_argument("--tag", default="salon-crop-09-landmark-debug")
parser.add_argument("--min-confidence", type=float, default=0.5)
args = parser.parse_args(argv)
original_path = Path(args.original)
crop_path = Path(args.generated_crop)
if not original_path.exists():
print(f"REFUSED: original not found: {original_path}")
return 2
if not crop_path.exists():
print(f"REFUSED: generated crop not found: {crop_path}")
return 2
smoke = Path(args.evidence_root) / "gemini-smoke"
lm_dir = smoke / "landmarks"
qa_md = smoke / "qa" / f"landmark-debug-{args.tag}.md"
for p in (lm_dir / "x", qa_md):
p.parent.mkdir(parents=True, exist_ok=True)
backend = landmark_backend()
original_bytes = normalize_image_orientation_bytes(original_path.read_bytes())
crop_bytes = crop_path.read_bytes()
base = Image.open(BytesIO(original_bytes)).convert("RGB")
w, h = base.size
nx1, ny1, nx2, ny2 = _parse_norm(args.crop_box_norm)
crop_box = (int(nx1 * w), int(ny1 * h), int(nx2 * w), int(ny2 * h))
# original cropped to the face crop box
orig_crop = base.crop(crop_box)
orig_crop_buf = BytesIO()
orig_crop.save(orig_crop_buf, format="PNG")
orig_crop_bytes = orig_crop_buf.getvalue()
targets = [
("original", original_bytes, crop_box),
("original-crop", orig_crop_bytes, None),
("generated-crop", crop_bytes, None),
]
rows = []
any_success = False
for name, img_bytes, box in targets:
res = detect_landmarks_result(img_bytes, min_detection_confidence=args.min_confidence)
any_success = any_success or res.success
overlay = draw_landmark_overlay(img_bytes, res, crop_box=box, label=name)
out_path = lm_dir / f"landmark-debug-{args.tag}-{name}.png"
out_path.write_bytes(overlay)
rows.append((name, res, out_path.name))
print(f"[{name}] success={res.success} count={res.detection_count} "
f"size={res.image_size} backend={res.backend} "
f"error={res.error_type or '-'} hint={res.hint()}")
real_eval = "YES" if any_success else "NO"
lines = [f"# Landmark detection debug - {args.tag}\n",
f"- landmark_backend: {backend or 'none'}",
f"- crop_box: {crop_box}",
f"- real_landmark_eval: {real_eval}",
f"- synthetic_fallback_used: {'NO' if any_success else 'YES'}",
f"- business_quality_eval_valid: {'PENDING (human QA)' if any_success else 'NO'}",
f"- reason: {'' if any_success else 'real landmarks were not detected on any input'}",
"- pilot_ready: NOT CONFIRMED\n",
"## Per-image detection\n"]
for name, res, fname in rows:
lines += [
f"### {name}",
f"- overlay: {fname}",
f"- success: {'YES' if res.success else 'NO'}",
f"- detection_count: {res.detection_count}",
f"- image_size: {res.image_size}",
f"- error_type: {res.error_type or '-'}",
f"- error_message: {(res.error_message or '-')[:200]}",
f"- attempts: {res.attempts}",
f"- hint: {res.hint()}\n",
]
qa_md.write_text("\n".join(lines), encoding="utf-8")
print(f"landmark_debug: DONE backend={backend or 'none'} real_landmark_eval={real_eval}")
print(f"overlays in {lm_dir} ; qa={qa_md.name}")
print("EXPERIMENTAL. Human QA required. Pilot Ready: NOT CONFIRMED.")
return 0
if __name__ == "__main__":
sys.exit(main())
|