[FEAT] scripts: cream-ground generation and BiRefNet cutout pipeline
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AvianVisitors - cut the cream ground off the generated illustrations.
|
||||
|
||||
Step 2 of the illustration pipeline (after pregen.py, before build_masks.py).
|
||||
|
||||
pregen.py renders each bird on a flat cream ground because the image model
|
||||
can't cut a clean transparent background on its own (it leaves holes and
|
||||
fringes). A flat known ground, by contrast, removes cleanly. This runs each
|
||||
illustration through the BiRefNet matting model (via rembg), then crops to
|
||||
the bird's bounding box with a small even margin, and saves an RGBA cutout
|
||||
back in place.
|
||||
|
||||
Idempotent: an illustration that already has a transparent background is
|
||||
skipped unless you pass --force.
|
||||
|
||||
Requires rembg + onnxruntime (see requirements.txt). The first run downloads
|
||||
the BiRefNet model (~1 GB) to ~/.u2net/.
|
||||
|
||||
Usage:
|
||||
python3 cutout.py # process every illustration
|
||||
python3 cutout.py calypte-anna # one slug (both poses)
|
||||
python3 cutout.py calypte-anna-2 --force
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
here = Path(__file__).resolve().parents[1]
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("slugs", nargs="*",
|
||||
help="Slugs to process (e.g. calypte-anna). Default: all.")
|
||||
ap.add_argument("--dir", type=Path, default=here / "assets" / "illustrations",
|
||||
help="Illustration directory (default: avian/assets/illustrations/)")
|
||||
ap.add_argument("--model", default="birefnet-general",
|
||||
help="rembg model name (default: birefnet-general)")
|
||||
ap.add_argument("--margin", type=float, default=0.02,
|
||||
help="Even margin around the bird, as a fraction of its "
|
||||
"long side (default: 0.02)")
|
||||
ap.add_argument("--force", action="store_true",
|
||||
help="Re-cut illustrations that already have transparency")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from rembg import new_session, remove
|
||||
except ImportError:
|
||||
print("error: needs Pillow + rembg (pip install -r requirements.txt)",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.slugs:
|
||||
paths = [args.dir / f"{s}.png" for s in args.slugs]
|
||||
missing = [p for p in paths if not p.exists()]
|
||||
if missing:
|
||||
print("error: not found: " + ", ".join(p.name for p in missing), file=sys.stderr)
|
||||
return 2
|
||||
else:
|
||||
paths = sorted(args.dir.glob("*.png"))
|
||||
if not paths:
|
||||
print("error: no illustrations found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
session = new_session(args.model)
|
||||
done = skipped = 0
|
||||
for p in paths:
|
||||
im = Image.open(p)
|
||||
if not args.force and im.mode == "RGBA" and im.getchannel("A").getextrema()[0] == 0:
|
||||
skipped += 1
|
||||
continue
|
||||
cut = remove(im.convert("RGB"), session=session) # RGBA, ground -> transparent
|
||||
bbox = cut.getchannel("A").getbbox()
|
||||
if bbox:
|
||||
pad = round(args.margin * max(bbox[2] - bbox[0], bbox[3] - bbox[1]))
|
||||
x0, y0 = max(0, bbox[0] - pad), max(0, bbox[1] - pad)
|
||||
x1, y1 = min(cut.width, bbox[2] + pad), min(cut.height, bbox[3] + pad)
|
||||
cut = cut.crop((x0, y0, x1, y1))
|
||||
cut.save(p)
|
||||
done += 1
|
||||
print(f" [cut] {p.name} -> {cut.width}x{cut.height}")
|
||||
|
||||
print(f"\ncut {done} · skipped {skipped} (already transparent)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user