93 lines
3.5 KiB
Python
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()
|