Files
aetherbound-guild/tools/process_identity_anchor.py
T

93 lines
3.5 KiB
Python

#!/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()