#!/usr/bin/env python3 """Remove green chroma and normalize one full-body identity anchor.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path from PIL import Image from process_green_grid_assets import remove_green def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("source", type=Path) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--report", type=Path, required=True) parser.add_argument("--pixels", type=int, default=512) parser.add_argument("--height-fraction", type=float, default=0.88) parser.add_argument("--width-fraction", type=float, default=0.84) return parser.parse_args() def digest(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def main() -> None: args = parse_args() if args.pixels < 64 or not 0 < args.height_fraction <= 1 or not 0 < args.width_fraction <= 1: raise SystemExit("invalid output normalization") source = Image.open(args.source).convert("RGBA") keyed = remove_green(source) bound = keyed.getchannel("A").point(lambda value: 255 if value >= 16 else 0).getbbox() if not bound: raise SystemExit("empty identity after chroma removal") content = keyed.crop(bound) max_width = round(args.pixels * args.width_fraction) max_height = round(args.pixels * args.height_fraction) scale = min(max_width / content.width, max_height / content.height) size = (max(1, round(content.width * scale)), max(1, round(content.height * scale))) resized = content.convert("RGBa").resize(size, Image.Resampling.LANCZOS).convert("RGBA") cleaned: list[tuple[int, int, int, int]] = [] green_spill = 0 for red, green, blue, alpha in resized.getdata(): if alpha < 4 or (green - red > 50 and green - blue > 50): if alpha >= 4: green_spill += 1 cleaned.append((0, 0, 0, 0)) else: cleaned.append((red, green, blue, alpha)) resized.putdata(cleaned) canvas = Image.new("RGBA", (args.pixels, args.pixels), (0, 0, 0, 0)) left = (args.pixels - resized.width) // 2 bottom_margin = round(args.pixels * (1 - args.height_fraction) / 2) top = args.pixels - bottom_margin - resized.height canvas.alpha_composite(resized, (left, top)) args.output.parent.mkdir(parents=True, exist_ok=True) args.report.parent.mkdir(parents=True, exist_ok=True) canvas.save(args.output) output_bound = canvas.getchannel("A").point(lambda value: 255 if value >= 16 else 0).getbbox() report = { "ok": True, "source": args.source.as_posix(), "source_sha256": digest(args.source), "source_dimensions": list(source.size), "source_content_bound": list(bound), "output": args.output.as_posix(), "output_sha256": digest(args.output), "output_dimensions": list(canvas.size), "output_content_bound": list(output_bound) if output_bound else None, "removed_visible_green_pixels_after_resize": green_spill, "normalization": { "height_fraction": args.height_fraction, "width_fraction": args.width_fraction, "feet_baseline_margin": bottom_margin, }, } args.report.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") print(f"IDENTITY_ANCHOR_PROCESS_OK output={args.output} sha256={report['output_sha256']}") if __name__ == "__main__": main()