leechard / scripts /recompose_face_a2_spike.py
nenae18's picture
Deploy LeeChard
5d3c2a9 verified
Raw
History Blame Contribute Delete
7.54 kB
"""A2 real-landmark + segmentation recompose spike (no Gemini call).
Detects landmarks on the ORIGINAL and the already-generated raw face crop using
an OPTIONAL backend (MediaPipe / InsightFace). If no backend is installed it
falls back to synthetic landmarks (clearly recorded as landmark_detected=NO) so
the structure still runs for inspection. Aligns via eyes/nose/mouth, protects
everything outside the face-contour landmark polygon, color-matches, and blends
onto the original. No API key, no model weights committed.
Outputs under runtime/ (gitignored). EXPERIMENTAL / NOT_PRODUCTION_READY.
Human QA required. 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, ImageDraw # noqa: E402
from app.services.face_pipeline.geometry_align import recompose_a2 # noqa: E402
from app.services.face_pipeline.landmark_detector import ( # noqa: E402
detect_face_landmarks,
landmark_backend,
synthetic_landmarks,
)
from app.services.face_pipeline.segmentation_mask import ( # noqa: E402
segmentation_backend_available,
)
from app.services.gemini_client import normalize_image_orientation_bytes # noqa: E402
def _parse_norm(value: str) -> tuple[float, float, float, float]:
parts = [float(p) for p in value.split(",")]
if len(parts) != 4:
raise ValueError("crop-box-norm must be 'x1,y1,x2,y2'")
return parts[0], parts[1], parts[2], parts[3]
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="A2 real-landmark recompose spike (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-08-a2-real-landmark")
parser.add_argument("--warp-mode", default="affine", choices=["affine", "piecewise"])
parser.add_argument("--blend-mode", default="safe", choices=["safe", "medium", "strong"])
parser.add_argument("--color-match-strength", type=float, default=None)
parser.add_argument("--feature-strength", type=float, default=None)
parser.add_argument("--inner-core-alpha", type=int, default=None)
parser.add_argument("--boundary-feather", type=int, default=None)
parser.add_argument("--visual-mode", default=None, choices=["natural", "balanced", "strong"])
parser.add_argument("--eye-protect-strength", type=float, default=None)
parser.add_argument("--max-eye-alpha", type=int, default=None)
parser.add_argument("--max-forehead-alpha", type=int, default=None)
parser.add_argument("--max-boundary-alpha", type=int, default=None)
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"
out_png = smoke / "output" / f"gemini-output-{args.tag}.png"
mask_png = smoke / "masks" / f"gemini-mask-{args.tag}.png"
compare_png = smoke / "comparison" / f"gemini-compare-{args.tag}.png"
qa_md = smoke / "qa" / f"gemini-a2-qa-{args.tag}.md"
for p in (out_png, mask_png, compare_png, qa_md):
p.parent.mkdir(parents=True, exist_ok=True)
original_bytes = normalize_image_orientation_bytes(original_path.read_bytes())
crop_bytes = crop_path.read_bytes()
base = Image.open(BytesIO(original_bytes)).convert("RGB")
width_img, height_img = base.size
nx1, ny1, nx2, ny2 = _parse_norm(args.crop_box_norm)
crop_box = (int(nx1 * width_img), int(ny1 * height_img),
int(nx2 * width_img), int(ny2 * height_img))
backend = landmark_backend()
crop_img = Image.open(BytesIO(crop_bytes)).convert("RGB")
target_landmarks = detect_face_landmarks(original_bytes)
source_landmarks = detect_face_landmarks(crop_bytes)
if target_landmarks is not None and source_landmarks is not None:
landmark_source = backend or "unknown"
else:
# Graceful fallback: synthetic landmarks so the structure still runs.
print(f"NOTE: landmark backend unavailable ({backend or 'none'}); "
"using synthetic landmarks (landmark_detected=NO).")
face_xywh = (crop_box[0], crop_box[1],
crop_box[2] - crop_box[0], crop_box[3] - crop_box[1])
target_landmarks = synthetic_landmarks(base.size, face_xywh)
source_landmarks = synthetic_landmarks(crop_img.size, (0, 0, *crop_img.size))
landmark_source = "synthetic"
final_bytes, qa, editable, _warped = recompose_a2(
original_bytes, crop_bytes,
target_landmarks=target_landmarks, source_landmarks=source_landmarks,
landmark_source=landmark_source,
segmentation_used=segmentation_backend_available(),
warp_mode=args.warp_mode,
blend_mode=args.blend_mode,
color_match_strength=args.color_match_strength,
feature_strength=args.feature_strength,
inner_core_alpha=args.inner_core_alpha,
boundary_feather=args.boundary_feather,
visual_mode=args.visual_mode,
eye_protect_strength=args.eye_protect_strength,
max_eye_alpha=args.max_eye_alpha,
max_forehead_alpha=args.max_forehead_alpha,
max_boundary_alpha=args.max_boundary_alpha,
return_debug=True,
)
out_png.write_bytes(final_bytes)
editable.save(mask_png)
raw_crop = crop_img
final_img = Image.open(BytesIO(final_bytes)).convert("RGB")
tint = Image.new("RGB", base.size, (255, 40, 40))
seg_panel = Image.composite(Image.blend(base, tint, 0.45), base, editable)
panels = [("original", base), ("raw crop", raw_crop),
("segmentation mask", seg_panel), ("final composite", final_img)]
h = 360
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(compare_png)
qa_lines = "\n".join(f"- {k}: {v}" for k, v in qa.items())
qa_md.write_text(
f"# A2 real-landmark recompose QA - {args.tag}\n\n"
f"- landmark_backend: {backend or 'none (synthetic fallback)'}\n"
f"- crop_box: {crop_box}\n"
f"{qa_lines}\n\n"
"> EXPERIMENTAL / NOT_PRODUCTION_READY. Human QA required.\n"
"> Pilot Ready: NOT CONFIRMED.\n",
encoding="utf-8",
)
print("recompose_a2_spike: DONE (no API call, no key used)")
print(f"landmark_backend={backend or 'none'} landmark_source={landmark_source}")
for k, v in qa.items():
print(f"{k}={v}")
print(f"output={out_png.name} mask={mask_png.name} compare={compare_png.name}")
print("Human QA required. Pilot Ready: NOT CONFIRMED.")
return 0
if __name__ == "__main__":
sys.exit(main())