Spaces:
Sleeping
Sleeping
| """ | |
| One-time asset prep script β NOT part of the runtime app. Run locally | |
| (or once, manually) whenever a new garment PNG is added to | |
| assets/garments_raw/. Produces a canonical, tight-cropped PNG plus a | |
| sidecar .json anchor file that the runtime outfit engine reads. | |
| Canonical format per garment: | |
| - Tight-cropped to alpha bounding box (no wasted transparent margin) | |
| - anchor.json stores: | |
| shoulder_width_px: width of the garment at the collar-band row | |
| collar_y_px: y-coordinate (in the cropped image) of the collar band | |
| collar_cx_px: x-coordinate of the collar center (garment's own | |
| horizontal center, since these assets are symmetric) | |
| This lets runtime code do a single scale+translate per request instead | |
| of re-measuring pixels on every generate click. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import sys | |
| import numpy as np | |
| from PIL import Image | |
| COLLAR_BAND_FRACTION = 0.45 # measure garment width at 45% down from the | |
| # top of its content bbox. Original value of 0.15 was measuring the | |
| # narrow collar/lapel gap near the neck opening, not the actual | |
| # shoulder-to-shoulder width β confirmed by a live runtime failure where | |
| # a garment scaled to 121% of the destination canvas width because the | |
| # scale factor was computed against that too-narrow neck measurement. | |
| # These garment PNGs are torso silhouettes that flare outward from the | |
| # collar down to the arms/shoulders; a live measurement across the | |
| # sample garments showed width increasing from ~119px at 15% down to | |
| # ~279px at 50% down. 45% approximates where real shoulder points (as | |
| # MoveNet's pose model detects them on an actual photo) sit relative to | |
| # the garment's own collar-to-hem span. | |
| COLLAR_TOP_FRACTION = 0.08 # separate, much shallower measurement for the | |
| # actual collar/neckline anchor point β this is where the garment aligns | |
| # to the subject's neck at runtime, and must stay near the top of the | |
| # garment rather than at the (much lower) shoulder-width measurement row. | |
| # Previously these two were incorrectly the same value, which positioned | |
| # the garment's collar-alignment point at the shoulder line instead of | |
| # the neck β confirmed by a runtime paste landing far too low in-frame. | |
| def normalize_garment(src_path: str, out_dir: str, garment_id: str) -> dict: | |
| img = Image.open(src_path).convert("RGBA") | |
| bbox = img.getbbox() | |
| if bbox is None: | |
| raise ValueError(f"{src_path}: fully transparent, no content found") | |
| cropped = img.crop(bbox) | |
| w, h = cropped.size | |
| arr = np.array(cropped) | |
| alpha = arr[:, :, 3] | |
| def _measure_width_at(frac: float) -> tuple[int, int, int]: | |
| """Returns (row_y, x_min, x_max) of non-transparent content at the | |
| given fractional height, scanning nearby rows if the exact row is | |
| empty (can happen right at a garment's top point/collar peak).""" | |
| y = int(h * frac) | |
| row = alpha[y] | |
| nz = np.where(row > 10)[0] | |
| if len(nz) == 0: | |
| for dy in range(1, 20): | |
| y2 = min(h - 1, y + dy) | |
| row = alpha[y2] | |
| nz = np.where(row > 10)[0] | |
| if len(nz): | |
| y = y2 | |
| break | |
| if len(nz) == 0: | |
| raise ValueError(f"{src_path}: no content found near frac={frac}") | |
| return y, int(nz.min()), int(nz.max()) | |
| # Shoulder width: measured further down the garment (COLLAR_BAND_FRACTION, | |
| # despite its name, is really "shoulder band" β kept the name for | |
| # backward compat with any already-generated JSON) where the garment | |
| # silhouette has flared out to its actual shoulder-seam width. | |
| shoulder_y, shoulder_x0, shoulder_x1 = _measure_width_at(COLLAR_BAND_FRACTION) | |
| shoulder_width_px = shoulder_x1 - shoulder_x0 | |
| shoulder_cx_px = int((shoulder_x0 + shoulder_x1) / 2) | |
| # Collar point: measured separately, near the TOP of the garment where | |
| # the actual collar/neckline sits β this is the point that gets | |
| # aligned to the subject's neck/collar line at runtime, and must NOT | |
| # be the same row as the (much lower, much wider) shoulder measurement | |
| # above. COLLAR_TOP_FRACTION intentionally stays close to the top. | |
| collar_y, collar_x0, collar_x1 = _measure_width_at(COLLAR_TOP_FRACTION) | |
| collar_cx_px = int((collar_x0 + collar_x1) / 2) | |
| out_png = os.path.join(out_dir, f"{garment_id}.png") | |
| cropped.save(out_png) | |
| anchor = { | |
| "garment_id": garment_id, | |
| "width": w, | |
| "height": h, | |
| "shoulder_width_px": shoulder_width_px, | |
| "shoulder_y_px": shoulder_y, | |
| "shoulder_cx_px": shoulder_cx_px, | |
| "collar_y_px": collar_y, | |
| "collar_cx_px": collar_cx_px, | |
| } | |
| out_json = os.path.join(out_dir, f"{garment_id}.json") | |
| with open(out_json, "w") as f: | |
| json.dump(anchor, f, indent=2) | |
| return anchor | |
| if __name__ == "__main__": | |
| # args: <src_png> <garment_id> <out_dir> | |
| src, gid, out_dir = sys.argv[1], sys.argv[2], sys.argv[3] | |
| os.makedirs(out_dir, exist_ok=True) | |
| result = normalize_garment(src, out_dir, gid) | |
| print(json.dumps(result, indent=2)) |