Spaces:
Running on Zero
Running on Zero
Download inference.py from ritianyu/SplatAnything: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/spaces/ritianyu/SplatAnything/resolve/main/inference.py
- Command line
-
hf download hf://spaces/ritianyu/SplatAnything/inference.py
-
curl -L -o inference.py https://huggingface.co/spaces/ritianyu/SplatAnything/resolve/main/inference.py
12.4 kB
| #!/usr/bin/env python3 | |
| """SplatAnything: single-scene feed-forward Gaussian inference.""" | |
| import argparse | |
| import json | |
| import time | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent | |
| def parse_args(argv=None): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument('--config_path', default=str(ROOT / 'configs/inference.yaml')) | |
| inputs = parser.add_mutually_exclusive_group() | |
| inputs.add_argument('--example', help='Bundled scene, e.g. object/1. See --list_examples.') | |
| inputs.add_argument('--input_path', help='Directory of images for one scene.') | |
| parser.add_argument('--list_examples', action='store_true', help='List bundled scenes and exit; no weights needed.') | |
| parser.add_argument('--output_path', help='Output root; default outputs, or outputs/CATEGORY/MODE for examples.') | |
| parser.add_argument('--resolution', type=int, nargs=2, metavar=('W', 'H'), | |
| help='Positive multiples of 14; example preset or 1022 1022.') | |
| parser.add_argument('--max_images', type=int, help='First N sorted images; example preset or 64.') | |
| parser.add_argument('--cameras', '--prior_cam_path', dest='cameras', | |
| help='prior_camera.json: OpenCV 4x4 camera-to-world poses and ' | |
| '3x3 pixel intrinsics at the original image resolution.') | |
| mode = parser.add_mutually_exclusive_group() | |
| mode.add_argument('--mode', choices=['rgb', 'pose'], help='RGB-only or pose-conditioned inference (requires cameras).') | |
| mode.add_argument('--pose_condition', dest='pose_condition', action='store_true', | |
| help=argparse.SUPPRESS) | |
| mode.add_argument('--no_pose_condition', dest='pose_condition', action='store_false', | |
| help=argparse.SUPPRESS) | |
| parser.set_defaults(pose_condition=None) | |
| parser.add_argument('--sky_seg', action='store_true', help='Segment and remove sky before voxel merging.') | |
| parser.add_argument('--sky_onnx_path', default=str(ROOT / 'checkpoints/skyseg.onnx')) | |
| parser.add_argument('--sky_max_fraction', type=float, default=0.95) | |
| parser.add_argument('--ckpt_path', help='Complete Gaussian student checkpoint (.ckpt/.pth/.safetensors).') | |
| parser.add_argument('--geometry_ckpt_path', help='WorldMirror-2 geometry checkpoint.') | |
| parser.add_argument('--opacity_thresh', type=float, help='Override the exported Gaussian opacity threshold.') | |
| parser.add_argument('--no_cam_align', action='store_true', help='Keep supplied camera frame; default anchors to camera 0.') | |
| parser.add_argument('--no_depth_rgb', action='store_true', help='Skip per-view RGB and depth export.') | |
| parser.add_argument('--device', choices=['cuda', 'cpu'], default='cuda') | |
| args = parser.parse_args(argv) | |
| if args.list_examples: | |
| manifest = json.loads((ROOT / 'examples/manifest.json').read_text()) | |
| for name, preset in manifest['examples'].items(): | |
| modes = 'rgb, pose' if preset['cameras'] else 'rgb (pose requires --cameras)' | |
| width, height = preset['resolution'] | |
| print(f'{name:12} {width}x{height}, up to {preset["max_images"]:3} views; {modes}') | |
| parser.exit() | |
| if args.mode is not None: | |
| args.pose_condition = args.mode == 'pose' | |
| preset = {} | |
| if args.example: | |
| manifest = json.loads((ROOT / 'examples/manifest.json').read_text()) | |
| if args.example not in manifest['examples']: | |
| parser.error(f'Unknown example {args.example!r}. Use --list_examples.') | |
| preset = manifest['examples'][args.example] | |
| args.input_path = str(ROOT / 'examples' / preset['images']) | |
| if args.pose_condition is None: | |
| args.pose_condition = False | |
| args.mode = 'pose' if args.pose_condition else 'rgb' | |
| if args.pose_condition and not args.cameras: | |
| if not preset['cameras']: | |
| parser.error(f'{args.example} has no bundled calibration. Use --mode rgb or supply --cameras.') | |
| args.cameras = str(ROOT / 'examples' / preset['cameras']) | |
| if args.output_path is None: | |
| args.output_path = str(ROOT / 'outputs' / args.example.split('/')[0] / args.mode) | |
| if not args.input_path: | |
| parser.error('Provide --example NAME or --input_path IMAGES.') | |
| args.resolution = args.resolution if args.resolution is not None else preset.get('resolution', [1022, 1022]) | |
| args.max_images = args.max_images if args.max_images is not None else preset.get('max_images', 64) | |
| args.output_path = args.output_path or str(ROOT / 'outputs') | |
| if any(n <= 0 or n % 14 for n in args.resolution): | |
| parser.error('--resolution W H must be positive multiples of 14 (e.g. 518 518 or 1022 756).') | |
| if args.max_images <= 0: | |
| parser.error('--max_images must be positive.') | |
| if not 0 <= args.sky_max_fraction <= 1: | |
| parser.error('--sky_max_fraction must be in [0, 1].') | |
| if args.opacity_thresh is not None and not 0 <= args.opacity_thresh <= 1: | |
| parser.error('--opacity_thresh must be in [0, 1].') | |
| return args | |
| def load_config(args): | |
| from omegaconf import OmegaConf | |
| cfg = OmegaConf.load(args.config_path) | |
| if not OmegaConf.is_dict(cfg) or set(cfg) != {'wrapper'}: | |
| raise ValueError('The config must contain only the wrapper mapping.') | |
| fields = { | |
| 'wrapper': {'worldmirror2_pretrained', 'SplatAnything_pretrained', | |
| 'enable_cond', 'infer_opacity_thresh', 'model'}, | |
| 'wrapper.model': {'use_gt_camera', 'filter_flying_points_infer', | |
| 'filter_large_scale_infer', | |
| 'max_scale_abs', 'max_scale_pct', 'gs_voxel_size'}, | |
| } | |
| for path, expected in fields.items(): | |
| section = OmegaConf.select(cfg, path) | |
| if not OmegaConf.is_dict(section): | |
| raise ValueError(f'{path} must be a mapping.') | |
| missing = expected - set(section) | |
| extra = set(section) - expected | |
| if missing or extra: | |
| raise ValueError(f'Invalid config fields in {path}: ' | |
| f'missing={sorted(missing)}, unsupported={sorted(extra)}.') | |
| if args.pose_condition is not None: | |
| cfg.wrapper.enable_cond = args.pose_condition | |
| cfg.wrapper.model.use_gt_camera = args.pose_condition | |
| if bool(cfg.wrapper.enable_cond) != bool(cfg.wrapper.model.use_gt_camera): | |
| raise ValueError('enable_cond and use_gt_camera must both be true (pose-conditioned) ' | |
| 'or both false (RGB-only).') | |
| if cfg.wrapper.enable_cond and not args.cameras: | |
| raise ValueError('Pose conditioning requires --cameras with poses AND intrinsics for every input image.') | |
| if not cfg.wrapper.enable_cond and args.cameras: | |
| raise ValueError('--cameras requires pose conditioning. Add --mode pose or set both config flags true.') | |
| if args.cameras and not Path(args.cameras).is_file(): | |
| raise FileNotFoundError(f'Camera JSON not found: {args.cameras}') | |
| if args.ckpt_path: | |
| cfg.wrapper.SplatAnything_pretrained = args.ckpt_path | |
| if args.geometry_ckpt_path: | |
| cfg.wrapper.worldmirror2_pretrained = args.geometry_ckpt_path | |
| if args.opacity_thresh is not None: | |
| cfg.wrapper.infer_opacity_thresh = args.opacity_thresh | |
| if not 0 <= cfg.wrapper.infer_opacity_thresh <= 1: | |
| raise ValueError('infer_opacity_thresh must be in [0, 1].') | |
| if cfg.wrapper.model.gs_voxel_size <= 0: | |
| raise ValueError('gs_voxel_size must be positive.') | |
| for name in ('filter_flying_points_infer', 'filter_large_scale_infer'): | |
| if not isinstance(cfg.wrapper.model[name], bool): | |
| raise ValueError(f'{name} must be true or false.') | |
| max_scale_abs = cfg.wrapper.model.max_scale_abs | |
| if max_scale_abs is not None and max_scale_abs <= 0: | |
| raise ValueError('max_scale_abs must be null or positive.') | |
| pct = cfg.wrapper.model.max_scale_pct | |
| if pct is not None and not 0 < pct <= 100: | |
| raise ValueError('max_scale_pct must be null or in (0, 100].') | |
| # Paths in YAML are relative to the repository root; explicit CLI paths are relative to cwd. | |
| for key, override in [('SplatAnything_pretrained', args.ckpt_path), | |
| ('worldmirror2_pretrained', args.geometry_ckpt_path)]: | |
| path = Path(cfg.wrapper[key]).expanduser() | |
| if not path.is_absolute(): | |
| path = (Path.cwd() if override else ROOT) / path | |
| if not path.is_file(): | |
| raise FileNotFoundError(f'{key} checkpoint not found: {path}. See README.md for weight setup.') | |
| cfg.wrapper[key] = str(path.resolve()) | |
| return cfg | |
| def main(argv=None): | |
| args = parse_args(argv) | |
| import torch | |
| from omegaconf import OmegaConf | |
| from src.models.models.gaussian_worldmirror import GaussianWorldMirror | |
| from src.models.models.geometry_worldmirror import GeometryWorldMirror | |
| from src.utils.checkpoints import load_model_weights | |
| from src.utils.scene_io import build_views, export_gaussians, export_points, export_depth_rgb | |
| from src.utils.sky_segmentation import SkySegmenter | |
| cfg = load_config(args) | |
| device = torch.device(args.device) | |
| if device.type == 'cuda' and not torch.cuda.is_available(): | |
| raise RuntimeError('CUDA is unavailable. Use a working CUDA/PyTorch environment, or --device cpu for a slow smoke run.') | |
| segmenter = SkySegmenter(args.sky_onnx_path, device=args.device) if args.sky_seg else None | |
| views, scene_name, img_paths = build_views( | |
| args.input_path, args.cameras, tuple(args.resolution), device, | |
| cam_align=not args.no_cam_align, max_images=args.max_images, | |
| sky_segmenter=segmenter, sky_max_fraction=args.sky_max_fraction) | |
| cond_flags = [1, 0, 1] if cfg.wrapper.enable_cond else [0, 0, 0] | |
| # Architecture, positional encoding, spacing normalization, and chunking use code defaults. | |
| model = GaussianWorldMirror(**OmegaConf.to_container(cfg.wrapper.model), | |
| enable_cond=bool(cfg.wrapper.enable_cond), | |
| filter_sky_infer=args.sky_seg) | |
| teacher = GeometryWorldMirror(enable_cond=bool(cfg.wrapper.enable_cond)) | |
| load_model_weights(model, cfg.wrapper.SplatAnything_pretrained) | |
| load_model_weights(teacher, cfg.wrapper.worldmirror2_pretrained) | |
| model.to(device).eval().requires_grad_(False) | |
| teacher.to(device).eval().requires_grad_(False) | |
| print(f'[Input] {scene_name}: {views["img"].shape[1]} images, resolution={args.resolution}, ' | |
| f'enable_cond={cfg.wrapper.enable_cond}, use_gt_camera={cfg.wrapper.model.use_gt_camera}, sky_seg={args.sky_seg}') | |
| started = time.perf_counter() | |
| with torch.inference_mode(): | |
| geometry = teacher(views, cond_flags=cond_flags) | |
| preds = model(views, geometry, cond_flags=cond_flags) | |
| if device.type == 'cuda': | |
| torch.cuda.synchronize() | |
| print(f'[Inference] {time.perf_counter() - started:.2f}s') | |
| outdir = Path(args.output_path) / scene_name | |
| outdir.mkdir(parents=True, exist_ok=True) | |
| # An empty filtered scene must not be mistaken for a successful reconstruction. | |
| if not len(preds['splats']['means'][0]): | |
| raise RuntimeError('All Gaussians were filtered. Check the input, sky masks, and scale threshold.') | |
| if not (preds['splats']['opacities'][0] >= cfg.wrapper.infer_opacity_thresh).any(): | |
| raise RuntimeError('All Gaussians are below opacity_thresh. Lower the threshold and check the checkpoint.') | |
| export_gaussians(preds, outdir, cfg.wrapper.infer_opacity_thresh, None) | |
| export_points(preds, outdir) | |
| if not args.no_depth_rgb: | |
| export_depth_rgb(preds, views, img_paths, outdir) | |
| cameras = {'extrinsics': [], 'intrinsics': []} | |
| for index, image in enumerate(img_paths): | |
| camera_id = Path(image).stem | |
| for name, key in [('extrinsics', 'export_camera_poses'), ('intrinsics', 'export_camera_intrs')]: | |
| cameras[name].append({'camera_id': camera_id, 'matrix': preds[key][0, index].cpu().tolist()}) | |
| (outdir / 'cameras.json').write_text(json.dumps(cameras, indent=2) + '\n') | |
| OmegaConf.save(cfg, outdir / 'config.yaml', resolve=True) | |
| (outdir / 'run.json').write_text(json.dumps({**vars(args), 'images': img_paths, 'cond_flags': cond_flags}, indent=2) + '\n') | |
| print(f'[Done] {outdir}') | |
| if __name__ == '__main__': | |
| main() | |