import os os.environ.setdefault("SPCONV_ALGO", "native") os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") import spaces # noqa: E402 (must precede torch) import random # noqa: E402 import tempfile # noqa: E402 import time # noqa: E402 import gradio as gr # noqa: E402 import numpy as np # noqa: E402 import torch # noqa: E402 import trimesh # noqa: E402 from PIL import Image # noqa: E402 from sparc3d import Sparc3DPipeline # noqa: E402 MODEL_ID = "hi3d-official/Hi3Dv1.0" COARSE_REPO = "tencent/Hunyuan3D-2mini" COARSE_SUBFOLDER = "hunyuan3d-dit-v2-mini-turbo" COARSE_STEPS = 30 # matches demo/coarse_stage.py COARSE_OCTREE_RESOLUTION = 256 # matches demo/coarse_stage.py MAX_SEED = 2**31 - 1 # ---------------------------------------------------------------- models print("Loading Hi3D v1.0 refinement pipeline...") pipe = Sparc3DPipeline.from_pretrained(MODEL_ID) _ = pipe.transformer_1024 # materialize the deferred high-res transformer now pipe.to("cuda") print("Loading Hunyuan3D-2mini (turbo) coarse generator...") from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline # noqa: E402 from hy3dgen.shapegen.models import conditioner as _hy_cond # noqa: E402 def _convert_dinov2_keys(state_dict): """Map transformers-4 Dinov2 checkpoint keys to the transformers-5 layout. Mirrors transformers' own Dinov2Model conversion mapping (needed because hy3dgen loads the conditioner with a raw ``load_state_dict``). """ if not any("attention.attention.query" in k or "mlp.weights_in" in k for k in state_dict): return state_dict out = {} for k, v in state_dict.items(): if "mlp.weights_in." in k: gate, up = v.chunk(2, dim=0) out[k.replace("mlp.weights_in.", "mlp.gate_proj.")] = gate out[k.replace("mlp.weights_in.", "mlp.up_proj.")] = up continue nk = ( k.replace("attention.attention.query", "attention.q_proj") .replace("attention.attention.key", "attention.k_proj") .replace("attention.attention.value", "attention.v_proj") .replace("attention.output.dense", "attention.o_proj") .replace("mlp.weights_out", "mlp.down_proj") ) out[nk] = v return out for _cls_name in ("SingleImageEncoder", "DualImageEncoder"): _cls = getattr(_hy_cond, _cls_name, None) if _cls is not None: _orig_load = _cls.load_state_dict def _patched_load(self, state_dict, *a, _orig=_orig_load, **kw): return _orig(self, _convert_dinov2_keys(state_dict), *a, **kw) _cls.load_state_dict = _patched_load coarse_pipe = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained( COARSE_REPO, subfolder=COARSE_SUBFOLDER, use_safetensors=True, device="cuda", ) print("Loading background remover...") from rembg import new_session, remove # noqa: E402 rembg_session = new_session() print("All models loaded.") # ---------------------------------------------------------------- helpers def _has_transparency(image: Image.Image) -> bool: if image.mode not in ("RGBA", "LA") and "transparency" not in image.info: return False alpha = np.asarray(image.convert("RGBA").getchannel("A")) return bool((alpha < 250).any()) def _prepare_input(image: Image.Image, remove_background: bool) -> Image.Image: if remove_background and not _has_transparency(image): image = remove(image.convert("RGB"), session=rembg_session, bgcolor=[255, 255, 255, 0]) return image.convert("RGBA") def _export(mesh: trimesh.Trimesh, suffix: str = ".glb") -> str: path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False).name mesh.export(path) return path def _duration(image, coarse_mesh=None, resolution=1024, num_inference_steps=25, *args, **kwargs): # Measured on ZeroGPU (large, 25 steps): max 46s @512, 212s @1024, 397s @1536 (+~15%). base = {512: 40, 1024: 225, 1536: 440}.get(int(resolution), 225) scale = max(int(num_inference_steps), 10) / 25 return int(min(900, 20 + base * scale)) # ---------------------------------------------------------------- inference @spaces.GPU(duration=_duration) def refine( image: Image.Image, coarse_mesh: str | None = None, resolution: int = 1024, num_inference_steps: int = 25, seed: int = 0, randomize_seed: bool = False, remove_background: bool = True, progress=gr.Progress(), ): """Generate a high-resolution 3D mesh from a single image with Hi3D v1.0. A coarse mesh is generated with Hunyuan3D-2mini-Turbo (or taken from the optional upload), then refined by the Hi3D v1.0 sparse flow model conditioned on the reference image. Args: image: Reference image of a single object (a cut-out with alpha works best). coarse_mesh: Optional path to your own coarse mesh (.glb/.obj/.ply). When omitted, Hunyuan3D-2mini-Turbo produces it from the image. resolution: Target sparse extraction grid: 512 (1 pass), 1024 (2 passes) or 1536 (3 passes). num_inference_steps: Scheduler updates per refinement pass. seed: Random seed. randomize_seed: Pick a random seed instead of `seed`. remove_background: Remove the background with rembg when the image has no transparency. Returns: Paths to the coarse mesh GLB, the refined mesh GLB, the refined GLB as a downloadable file, the coarse GLB as a downloadable file, the seed used and a status string. """ if image is None: raise gr.Error("Please upload an image.") if randomize_seed: seed = random.randint(0, MAX_SEED) seed = int(seed) resolution = int(resolution) num_inference_steps = int(num_inference_steps) t0 = time.perf_counter() progress(0.0, desc="Preparing image") image = _prepare_input(image, remove_background) if coarse_mesh: progress(0.02, desc="Loading coarse mesh") coarse = trimesh.load(coarse_mesh, force="mesh", process=False) else: progress(0.02, desc="Generating coarse mesh (Hunyuan3D-2mini-Turbo)") coarse = coarse_pipe( image=image, num_inference_steps=COARSE_STEPS, octree_resolution=COARSE_OCTREE_RESOLUTION, generator=torch.manual_seed(seed), output_type="trimesh", )[0] t_coarse = time.perf_counter() - t0 coarse_path = _export(coarse) def _report(event): frac = (event.pass_index + event.step / event.num_inference_steps) / event.num_passes progress( 0.1 + 0.8 * frac, desc=f"Hi3D pass {event.pass_index + 1}/{event.num_passes} " f"(grid {event.structure_resolution}³) step {event.step}/{event.num_inference_steps}", ) pipe.progress_callback = _report try: result = pipe( image, coarse, resolution=resolution, num_inference_steps=num_inference_steps, seed=seed, ) finally: pipe.progress_callback = None progress(0.95, desc="Exporting mesh") refined_path = _export(result.mesh) total = time.perf_counter() - t0 status = ( f"Done in {total:.1f}s (coarse {t_coarse:.1f}s) — resolution {result.resolution}, " f"{len(result.mesh.faces):,} faces, seed {seed}" ) print(status) return coarse_path, refined_path, refined_path, coarse_path, seed, status # ---------------------------------------------------------------- UI CSS = """ #col-container { max-width: 1200px; margin: 0 auto; } .dark .gradio-container { color: var(--body-text-color); } """ EXAMPLES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "examples") EXAMPLES = [ [os.path.join(EXAMPLES_DIR, name)] for name in ( "typical_creature_robot_dinosour.png", "typical_humanoid_mech.png", "typical_building_castle.png", "typical_misc_phonograph.png", "typical_vehicle_excavator.png", ) ] with gr.Blocks(title="Hi3Dv1") as demo: with gr.Column(elem_id="col-container"): gr.Markdown( """ # Hi3Dv1 — high-resolution 3D geometry refinement Upload an image of an object. A coarse mesh is generated with [Hunyuan3D-2mini-Turbo](https://huggingface.co/tencent/Hunyuan3D-2mini) (or upload your own), then [**Hi3D v1.0**](https://huggingface.co/hi3d-official/Hi3Dv1.0) — built on Sparc3D — refines it into high-resolution geometry conditioned on the image. Geometry only (no textures). [Model](https://huggingface.co/hi3d-official/Hi3Dv1.0) · [Code](https://github.com/hi3d-official/Hi3Dv1.0) · [Website](http://hi3d.ai/) — *Powered by Hi3D 1.0* """ ) with gr.Row(): with gr.Column(scale=1): image_in = gr.Image(label="Reference image", type="pil", image_mode="RGBA", height=360) coarse_in = gr.File( label="Optional: your own coarse mesh (.glb / .obj / .ply) — leave empty to auto-generate", file_types=[".glb", ".gltf", ".obj", ".ply", ".stl", ".off"], type="filepath", ) resolution = gr.Radio( [512, 1024, 1536], value=1024, label="Resolution (refinement passes: 1 / 2 / 3)", ) run_btn = gr.Button("Generate", variant="primary") with gr.Accordion("Advanced settings", open=False): steps = gr.Slider(5, 50, value=25, step=1, label="Inference steps per pass") seed = gr.Slider(0, MAX_SEED, value=0, step=1, label="Seed") randomize_seed = gr.Checkbox(value=False, label="Randomize seed") remove_bg = gr.Checkbox( value=True, label="Remove background (only applied if the image has no transparency)", ) show_coarse = gr.Checkbox( value=False, label="Also show the coarse (intermediate) mesh — view & download it alongside the refined result", ) with gr.Column(scale=2): with gr.Row(): coarse_out = gr.Model3D( label="Coarse mesh (intermediate)", height=420, clear_color=[1, 1, 1, 1], visible=False ) refined_out = gr.Model3D(label="Hi3D refined mesh", height=420, clear_color=[1, 1, 1, 1]) refined_file = gr.File(label="Download refined GLB") coarse_file = gr.File(label="Download coarse GLB (intermediate)", visible=False) status = gr.Markdown() inputs = [image_in, coarse_in, resolution, steps, seed, randomize_seed, remove_bg] outputs = [coarse_out, refined_out, refined_file, coarse_file, seed, status] run_btn.click(refine, inputs=inputs, outputs=outputs, api_name="refine") show_coarse.change( lambda v: (gr.update(visible=v), gr.update(visible=v)), inputs=show_coarse, outputs=[coarse_out, coarse_file], api_visibility="private", ) gr.Examples( examples=EXAMPLES, inputs=[image_in], outputs=outputs, fn=refine, cache_examples=True, cache_mode="lazy", label="Examples (images from the MIT-licensed TRELLIS repo)", ) if __name__ == "__main__": demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)