Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """Create deterministic RGBA/preprocessed RGB inputs for Flux2 VAE parity. | |
| With no --input, the script creates a synthetic transparent RGBA object. It then | |
| follows the alpha-present branch of official TripoSplat preprocessing: resize the | |
| short side to 1024, erode alpha, crop a square around the alpha bounds with 1.2x | |
| padding, resize to 1024, and composite on black. It also writes an NPZ containing | |
| ``image_rgb`` and a seeded explicit ``epsilon`` tensor for the ONNX validator. | |
| This helper intentionally does not export or emulate BiRefNet. An opaque input must | |
| first be background-removed elsewhere and supplied with meaningful alpha. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| IMAGE_SIZE = 1024 | |
| IMAGE_SHAPE = (1, 3, IMAGE_SIZE, IMAGE_SIZE) | |
| EPSILON_SHAPE = (1, 32, 128, 128) | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| parser.add_argument( | |
| "--input", | |
| type=Path, | |
| help="Optional transparent RGBA source. Omit to generate a deterministic source.", | |
| ) | |
| parser.add_argument( | |
| "--output-prefix", | |
| type=Path, | |
| default=Path("fixtures/triposplat/flux2_vae"), | |
| help=( | |
| "Output prefix for *_source_rgba.png, *_preprocessed_rgb.png and *_inputs.npz " | |
| "(default: %(default)s)." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--erode-radius", | |
| type=int, | |
| default=1, | |
| help="Alpha MinFilter radius matching official preprocessing (default: %(default)s).", | |
| ) | |
| parser.add_argument( | |
| "--seed", | |
| type=int, | |
| default=20260516, | |
| help="NumPy PCG64 seed for explicit epsilon (default: %(default)s).", | |
| ) | |
| args = parser.parse_args() | |
| if args.erode_radius < 0: | |
| parser.error("--erode-radius must be non-negative") | |
| return args | |
| def deterministic_rgba(np: Any, image_module: Any) -> Any: | |
| """Generate a stable, nontrivial transparent fixture without external assets.""" | |
| width, height = 1280, 960 | |
| yy, xx = np.mgrid[0:height, 0:width].astype(np.float32) | |
| x = (xx - np.float32(width * 0.52)) / np.float32(width * 0.29) | |
| y = (yy - np.float32(height * 0.48)) / np.float32(height * 0.37) | |
| # A soft superellipse plus a smaller asymmetric lobe exercises alpha crop and | |
| # Lanczos resampling without relying on platform font or drawing rasterization. | |
| body_distance = np.power(np.abs(x), 3.2) + np.power(np.abs(y), 3.2) | |
| lobe_distance = ( | |
| ((xx - np.float32(width * 0.70)) / np.float32(width * 0.12)) ** 2 | |
| + ((yy - np.float32(height * 0.34)) / np.float32(height * 0.16)) ** 2 | |
| ) | |
| alpha = np.maximum( | |
| np.clip((np.float32(1.04) - body_distance) * np.float32(12.0), 0.0, 1.0), | |
| np.clip((np.float32(1.03) - lobe_distance) * np.float32(10.0), 0.0, 1.0), | |
| ) | |
| stripe = np.float32(0.5) + np.float32(0.5) * np.sin( | |
| xx * np.float32(0.031) + yy * np.float32(0.017) | |
| ) | |
| red = np.clip(np.float32(0.15) + np.float32(0.75) * xx / width, 0.0, 1.0) | |
| green = np.clip(np.float32(0.20) + np.float32(0.65) * yy / height, 0.0, 1.0) | |
| blue = np.clip(np.float32(0.18) + np.float32(0.70) * stripe, 0.0, 1.0) | |
| rgba = np.stack((red, green, blue, alpha), axis=-1) | |
| rgba_u8 = np.rint(rgba * np.float32(255.0)).astype(np.uint8) | |
| return image_module.fromarray(rgba_u8, mode="RGBA") | |
| def preprocess_alpha_present(image: Any, np: Any, image_module: Any, image_filter: Any, radius: int) -> Any: | |
| """Mirror official preprocess_image after its alpha/background-removal choice.""" | |
| image = image.convert("RGBA") | |
| width, height = image.size | |
| scale = IMAGE_SIZE / min(width, height) | |
| resized_size = ( | |
| max(1, int(round(width * scale))), | |
| max(1, int(round(height * scale))), | |
| ) | |
| image = image.resize(resized_size, image_module.Resampling.LANCZOS) | |
| alpha_array = np.asarray(image.getchannel("A"), dtype=np.uint8) | |
| if int(alpha_array.min()) == 255: | |
| raise ValueError( | |
| "Input alpha is fully opaque. This helper does not run BiRefNet; provide a " | |
| "background-removed RGBA image with transparent pixels." | |
| ) | |
| if radius: | |
| image.putalpha(image.getchannel("A").filter(image_filter.MinFilter(2 * radius + 1))) | |
| alpha_array = np.asarray(image.getchannel("A"), dtype=np.uint8) | |
| ys, xs = np.nonzero(alpha_array) | |
| if xs.size == 0: | |
| raise ValueError("Alpha erosion removed the entire foreground") | |
| bbox = [int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())] | |
| center_x = (bbox[0] + bbox[2]) / 2.0 | |
| center_y = (bbox[1] + bbox[3]) / 2.0 | |
| half = max(bbox[2] - bbox[0], bbox[3] - bbox[1]) / 2.0 * 1.2 | |
| if half <= 0: | |
| raise ValueError("Foreground alpha bounds are degenerate") | |
| image = image.crop( | |
| [ | |
| int(center_x - half), | |
| int(center_y - half), | |
| int(center_x + half), | |
| int(center_y + half), | |
| ] | |
| ) | |
| image = image.resize((IMAGE_SIZE, IMAGE_SIZE), image_module.Resampling.LANCZOS) | |
| background = image_module.new("RGB", (IMAGE_SIZE, IMAGE_SIZE), (0, 0, 0)) | |
| background.paste(image, mask=image.getchannel("A")) | |
| return background | |
| def checksum(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as stream: | |
| while block := stream.read(4 * 1024 * 1024): | |
| digest.update(block) | |
| return digest.hexdigest() | |
| def output_paths(prefix: Path) -> tuple[Path, Path, Path]: | |
| resolved = prefix.expanduser().resolve() | |
| return ( | |
| resolved.parent / f"{resolved.name}_source_rgba.png", | |
| resolved.parent / f"{resolved.name}_preprocessed_rgb.png", | |
| resolved.parent / f"{resolved.name}_inputs.npz", | |
| ) | |
| def main() -> None: | |
| args = parse_args() | |
| try: | |
| import numpy as np | |
| from PIL import Image, ImageFilter | |
| except ImportError as exc: | |
| raise SystemExit( | |
| "Missing fixture dependency. Run `python -m pip install numpy Pillow`. " | |
| f"Original error: {exc}" | |
| ) from exc | |
| if args.input: | |
| input_path = args.input.expanduser().resolve() | |
| if not input_path.is_file(): | |
| raise FileNotFoundError(f"RGBA input does not exist: {input_path}") | |
| with Image.open(input_path) as opened: | |
| source = opened.convert("RGBA") | |
| source_description = str(input_path) | |
| else: | |
| source = deterministic_rgba(np, Image) | |
| source_description = "generated deterministic RGBA" | |
| preprocessed = preprocess_alpha_present( | |
| source, | |
| np=np, | |
| image_module=Image, | |
| image_filter=ImageFilter, | |
| radius=args.erode_radius, | |
| ) | |
| image_rgb = ( | |
| np.asarray(preprocessed, dtype=np.float32).transpose(2, 0, 1)[None, ...] | |
| / np.float32(255.0) | |
| ) | |
| rng = np.random.default_rng(args.seed) | |
| epsilon = rng.standard_normal(EPSILON_SHAPE, dtype=np.float32) | |
| assert tuple(image_rgb.shape) == IMAGE_SHAPE | |
| source_path, rgb_path, fixture_path = output_paths(args.output_prefix) | |
| source_path.parent.mkdir(parents=True, exist_ok=True) | |
| source.save(source_path, format="PNG") | |
| preprocessed.save(rgb_path, format="PNG") | |
| metadata = { | |
| "source": source_description, | |
| "preprocess": "official alpha-present branch", | |
| "canvas_size": IMAGE_SIZE, | |
| "erode_radius": args.erode_radius, | |
| "epsilon_rng": "numpy.random.Generator(PCG64).standard_normal(float32)", | |
| "epsilon_seed": args.seed, | |
| "image_layout": "NCHW RGB float32 [0,1]", | |
| } | |
| np.savez_compressed( | |
| fixture_path, | |
| image_rgb=np.ascontiguousarray(image_rgb, dtype=np.float32), | |
| epsilon=np.ascontiguousarray(epsilon, dtype=np.float32), | |
| metadata=np.asarray(json.dumps(metadata, sort_keys=True)), | |
| ) | |
| for path in (source_path, rgb_path, fixture_path): | |
| print(f"Wrote {path} ({path.stat().st_size:,} bytes, sha256={checksum(path)})") | |
| if __name__ == "__main__": | |
| main() | |