Buckets:
| #!/usr/bin/env python3 | |
| """CLI wrapper around microsoft/TRELLIS.2's image-to-3D pipeline. | |
| Turns a single RGBA (or RGB) image crop into a textured GLB mesh. Meant to be | |
| invoked as a subprocess -- from the `fpgm` env or anywhere else -- using the | |
| `trellis2` conda env's interpreter, since that env holds TRELLIS.2's own | |
| torch build (2.6.0+cu124) which must stay isolated from the rest of this | |
| project's torch (2.10.0+cu128 in `fpgm`). | |
| Usage: | |
| source scripts/nvidia_lib_shim.sh # required: fixes NVML driver mismatch | |
| /home/quang/miniconda3/envs/trellis2/bin/python scripts/trellis_generate.py \\ | |
| --image crop.png --out mesh.glb [--resolution 512] [--seed 0] [--simplify 1000000] | |
| On success prints the absolute output path to stdout and exits 0. A sibling | |
| JSON file (<out>.json) is always written on success, recording what the | |
| calling process needs to know about what it got. On failure, prints a clear | |
| message to stderr and exits non-zero. | |
| """ | |
| import os | |
| import sys | |
| # Must happen before torch (or anything that transitively imports it) is | |
| # imported -- per the TRELLIS.2 model card. | |
| os.environ.setdefault("OPENCV_IO_ENABLE_OPENEXR", "1") | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| _TRELLIS_DIR = os.path.join(_REPO_ROOT, "third_party", "TRELLIS.2") | |
| _CONDA_ROOT = "/home/quang/miniconda3" | |
| # TRELLIS.2's repo root has no pyproject.toml -- `trellis2`/`o_voxel`'s | |
| # runtime companion modules are meant to be run with the repo on the path | |
| # (exactly how example.py in the upstream repo works), not pip-installed. | |
| if _TRELLIS_DIR not in sys.path: | |
| sys.path.insert(0, _TRELLIS_DIR) | |
| # flex_gemm (a TRELLIS.2 CUDA extension) JIT-compiles Triton kernels the | |
| # first time it's used, and Triton's build path (triton/runtime/build.py) | |
| # looks for a C compiler via $CC or `gcc`/`clang` on $PATH -- it does not | |
| # reuse whatever compiled flex_gemm itself at install time. Point it at the | |
| # same buildtools gcc used by scripts/setup_trellis_env.sh. | |
| os.environ.setdefault("CC", os.path.join(_CONDA_ROOT, "envs", "buildtools", "bin", "gcc")) | |
| os.environ.setdefault("CXX", os.path.join(_CONDA_ROOT, "envs", "buildtools", "bin", "g++")) | |
| os.environ["PATH"] = os.path.join(_CONDA_ROOT, "envs", "buildtools", "bin") + os.pathsep + os.environ.get("PATH", "") | |
| os.environ.setdefault("CUDA_HOME", os.path.join(_CONDA_ROOT, "envs", "cuda128")) | |
| # Attention backend: whichever one scripts/setup_trellis_env.sh managed to | |
| # install is recorded in .trellis_attn_backend; respect an explicit | |
| # SPARSE_ATTN_BACKEND/ATTN_BACKEND from the caller's environment first. | |
| _attn_backend_file = os.path.join(_REPO_ROOT, ".trellis_attn_backend") | |
| if "SPARSE_ATTN_BACKEND" not in os.environ and "ATTN_BACKEND" not in os.environ and os.path.exists(_attn_backend_file): | |
| with open(_attn_backend_file) as _f: | |
| _backend = _f.read().strip() | |
| if _backend: | |
| os.environ["SPARSE_ATTN_BACKEND"] = _backend | |
| os.environ["ATTN_BACKEND"] = _backend | |
| import argparse | |
| import json | |
| import subprocess | |
| import time | |
| import traceback | |
| from pathlib import Path | |
| PIPELINE_TYPES = ("512", "1024", "1024_cascade", "1536_cascade") | |
| def pick_gpu() -> int: | |
| """Return the index of the GPU with the most free memory right now. | |
| The 4x H200s on this host are shared and free VRAM fluctuates 11-44GB, | |
| so the choice is made fresh on every invocation rather than hardcoded. | |
| If the caller already pinned CUDA_VISIBLE_DEVICES, that choice is | |
| respected instead. | |
| """ | |
| if "CUDA_VISIBLE_DEVICES" in os.environ: | |
| return 0 | |
| try: | |
| out = subprocess.check_output( | |
| ["nvidia-smi", "--query-gpu=memory.free", "--format=csv,noheader,nounits"], | |
| text=True, | |
| timeout=15, | |
| ) | |
| free = [int(x) for x in out.strip().splitlines() if x.strip()] | |
| if not free: | |
| raise ValueError("nvidia-smi returned no GPUs") | |
| return max(range(len(free)), key=lambda i: free[i]) | |
| except Exception as e: | |
| print( | |
| f"WARNING: could not query nvidia-smi to pick a GPU ({e}); " | |
| "defaulting to device 0. If this fails with an NVML error, " | |
| "did you `source scripts/nvidia_lib_shim.sh`?", | |
| file=sys.stderr, | |
| ) | |
| return 0 | |
| def parse_args() -> argparse.Namespace: | |
| p = argparse.ArgumentParser( | |
| description="Turn an RGBA image crop into a textured GLB mesh with TRELLIS.2-4B.", | |
| ) | |
| p.add_argument("--image", required=True, type=Path, help="Path to an RGBA/RGB PNG image.") | |
| p.add_argument("--out", required=True, type=Path, help="Output .glb path.") | |
| p.add_argument( | |
| "--resolution", | |
| default="512", | |
| choices=PIPELINE_TYPES, | |
| help="TRELLIS.2 pipeline_type (shape/texture SLat resolution). " | |
| "Default 512, the lowest-VRAM option, since GPUs here are shared/contended.", | |
| ) | |
| p.add_argument("--seed", type=int, default=0, help="Random seed.") | |
| p.add_argument( | |
| "--simplify", | |
| type=int, | |
| default=50_000, | |
| help="Target vertex count for final mesh decimation (o_voxel.postprocess.to_glb's decimation_target). " | |
| "The model card example uses 1_000_000, but xatlas UV-chart computation over that many vertices " | |
| "was observed to exhaust host RAM (silent SIGKILL, no traceback) on this shared machine for " | |
| "irregular/noisy silhouettes -- 50_000 is a much safer default; raise it explicitly if you have " | |
| "headroom and need finer geometry.", | |
| ) | |
| p.add_argument( | |
| "--texture-size", | |
| type=int, | |
| default=1024, | |
| help="Baked texture resolution (px). Model card default is 4096; lowered further here (from an " | |
| "earlier 2048 default) alongside --simplify to avoid the same OOM risk during UV baking.", | |
| ) | |
| return p.parse_args() | |
| def main() -> None: | |
| args = parse_args() | |
| if not args.image.exists(): | |
| print(f"ERROR: input image not found: {args.image}", file=sys.stderr) | |
| sys.exit(1) | |
| gpu_idx = pick_gpu() | |
| os.environ.setdefault("CUDA_VISIBLE_DEVICES", str(gpu_idx)) | |
| t0 = time.time() | |
| try: | |
| import torch | |
| from PIL import Image | |
| import o_voxel | |
| from trellis2.pipelines import Trellis2ImageTo3DPipeline | |
| import trellis2.pipelines.rembg as _rembg_module | |
| except Exception as e: | |
| print(f"ERROR: failed to import the TRELLIS.2 stack: {e}", file=sys.stderr) | |
| traceback.print_exc() | |
| sys.exit(1) | |
| # Trellis2ImageTo3DPipeline.from_pretrained() unconditionally instantiates | |
| # the background-removal model (BiRefNet, backed by the gated | |
| # briaai/RMBG-2.0 checkpoint on HF) even when it will never be called -- | |
| # it downloads it at pipeline-construction time, before any image is even | |
| # looked at. This CLI's contract is that --image is already a proper RGBA | |
| # crop (see preprocess_image: it only invokes rembg when the input has no | |
| # real alpha channel), so that model is genuinely dead weight here -- and | |
| # on this machine's HF account it isn't even gate-approved, which would | |
| # otherwise hard-fail every run. Swap in a no-op before construction | |
| # rather than patching TRELLIS.2's own source. | |
| class _NoRembg: | |
| def __init__(self, *_a, **_kw): | |
| pass | |
| def to(self, _device): | |
| pass | |
| def cuda(self): | |
| pass | |
| def cpu(self): | |
| pass | |
| def __call__(self, _image): | |
| raise RuntimeError( | |
| "This input image has no real alpha channel (preprocess_image " | |
| "wants to run background removal), but this wrapper disables " | |
| "TRELLIS.2's rembg model (gated briaai/RMBG-2.0 checkpoint, " | |
| "not needed for a pre-masked crop). Pass an --image that " | |
| "already carries a real (non-uniform) alpha channel." | |
| ) | |
| _rembg_module.BiRefNet = _NoRembg | |
| # TRELLIS.2's DINOv3 image-conditioning feature extractor | |
| # (trellis2/modules/image_feature_extractor.py: | |
| # DinoV3FeatureExtractor.extract_features) reaches into | |
| # `self.model.layer`, which matched the `transformers` internals when | |
| # TRELLIS.2 was written. In the `transformers` version installed here | |
| # (5.14.1, current at install time -- no version was pinned upstream), | |
| # `DINOv3ViTModel` wraps its encoder in a `.model` submodule | |
| # (`DINOv3ViTEncoder`), so the real path is `self.model.model.layer`; | |
| # `self.model.layer` raises AttributeError. Rebind the method with the | |
| # corrected attribute path rather than patching TRELLIS.2's source or | |
| # pinning an old, unmaintained transformers release. | |
| import trellis2.modules.image_feature_extractor as _ife_module | |
| def _dinov3_extract_features_fixed(self, image: "torch.Tensor") -> "torch.Tensor": | |
| import torch.nn.functional as F | |
| image = image.to(self.model.embeddings.patch_embeddings.weight.dtype) | |
| hidden_states = self.model.embeddings(image, bool_masked_pos=None) | |
| position_embeddings = self.model.rope_embeddings(image) | |
| for layer_module in self.model.model.layer: # was self.model.layer | |
| hidden_states = layer_module(hidden_states, position_embeddings=position_embeddings) | |
| return F.layer_norm(hidden_states, hidden_states.shape[-1:]) | |
| _ife_module.DinoV3FeatureExtractor.extract_features = _dinov3_extract_features_fixed | |
| if not torch.cuda.is_available(): | |
| print( | |
| "ERROR: torch.cuda.is_available() is False. Did you " | |
| "`source scripts/nvidia_lib_shim.sh` before running this script?", | |
| file=sys.stderr, | |
| ) | |
| sys.exit(1) | |
| torch.cuda.reset_peak_memory_stats() | |
| try: | |
| image = Image.open(args.image) | |
| except Exception as e: | |
| print(f"ERROR: could not open image {args.image}: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| try: | |
| pipeline = Trellis2ImageTo3DPipeline.from_pretrained("microsoft/TRELLIS.2-4B") | |
| if not pipeline.low_vram: | |
| print("WARNING: pipeline.low_vram is False (expected True by default).", file=sys.stderr) | |
| pipeline.cuda() | |
| meshes = pipeline.run(image, seed=args.seed, pipeline_type=args.resolution) | |
| mesh = meshes[0] | |
| mesh.simplify(16_777_216) # nvdiffrast's index limit, per the model card example | |
| # NOTE: render_utils.render_video is intentionally never called here -- | |
| # it's a preview convenience the calling pipeline doesn't need and is | |
| # pure extra GPU/time cost. | |
| glb = o_voxel.postprocess.to_glb( | |
| vertices=mesh.vertices, | |
| faces=mesh.faces, | |
| attr_volume=mesh.attrs, | |
| coords=mesh.coords, | |
| attr_layout=mesh.layout, | |
| voxel_size=mesh.voxel_size, | |
| aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]], | |
| decimation_target=args.simplify, | |
| texture_size=args.texture_size, | |
| remesh=True, | |
| remesh_band=1, | |
| remesh_project=0, | |
| verbose=True, | |
| ) | |
| args.out.parent.mkdir(parents=True, exist_ok=True) | |
| glb.export(str(args.out), extension_webp=True) | |
| except Exception as e: | |
| print(f"ERROR: TRELLIS.2 generation failed: {e}", file=sys.stderr) | |
| traceback.print_exc() | |
| sys.exit(1) | |
| elapsed = time.time() - t0 | |
| peak_vram_bytes = torch.cuda.max_memory_allocated() | |
| info = { | |
| "input_image": str(args.image.resolve()), | |
| "output_glb": str(args.out.resolve()), | |
| "resolution": args.resolution, | |
| "seed": args.seed, | |
| "num_vertices": int(glb.vertices.shape[0]), | |
| "num_faces": int(glb.faces.shape[0]), | |
| "elapsed_seconds": round(elapsed, 2), | |
| "gpu_index": gpu_idx, | |
| "gpu_name": torch.cuda.get_device_name(0), | |
| "peak_vram_bytes": int(peak_vram_bytes), | |
| "peak_vram_gb": round(peak_vram_bytes / (1024**3), 2), | |
| } | |
| json_path = args.out.with_suffix(".json") | |
| json_path.write_text(json.dumps(info, indent=2)) | |
| print(str(args.out.resolve())) | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 12.2 kB
- Xet hash:
- 44067fa77d14c5c5d993d6580ba233a5c85cf5bef502d2b8ecc78bfbac92d252
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.