Files
Hunyuan3D_2.1_Low_VRAM/batch_generate.py
Akasei e150058012 feat(batch): use steps=50, resolution=512, max_views=9 for RTX 3080
768 resolution causes OOM (14.6GB model activation) on RTX 3080 20GB.
512 is the practical maximum: texture model uses 6.59GB, leaving
sufficient headroom. Increased max_views 6→9 for better texture coverage.

Result: 9/9 images → textured GLB in 12.3 min total.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 20:53:12 +08:00

258 lines
8.5 KiB
Python

"""
Batch 3D model generation script for Hunyuan3D-2.1.
Optimized for RTX 3080 (20GB VRAM) by sequential model loading:
Phase 1: Load shape model → generate all meshes → unload
Phase 2: Load texture model → texture all meshes → unload
"""
import sys
sys.path.insert(0, './hy3dshape')
sys.path.insert(0, './hy3dpaint')
try:
from torchvision_fix import apply_fix
apply_fix()
except Exception as e:
print(f"Warning: torchvision fix: {e}")
import os
import gc
import time
import glob
import torch
import trimesh
import numpy as np
from PIL import Image
from pathlib import Path
INPUT_DIR = "test/images"
OUTPUT_DIR = "test/models"
MODEL_PATH = "tencent/Hunyuan3D-2.1"
SUBFOLDER = "hunyuan3d-dit-v2-1"
SHAPE_STEPS = 50
GUIDANCE_SCALE = 7.5
SEED = 1234
OCTREE_RESOLUTION = 256
NUM_CHUNKS = 200000
TEXGEN_MAX_VIEWS = 9
TEXGEN_RESOLUTION = 512
def clear_gpu():
"""Aggressively free GPU memory."""
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
def get_image_files(input_dir):
"""Get all supported image files from input directory."""
extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp', '*.webp']
files = []
for ext in extensions:
files.extend(glob.glob(os.path.join(input_dir, ext)))
files.extend(glob.glob(os.path.join(input_dir, ext.upper())))
return sorted(set(files))
def phase1_shape_generation(image_files, output_dir):
"""Phase 1: Load shape model, generate all meshes, unload."""
print("\n" + "=" * 60)
print("PHASE 1: Shape Generation")
print("=" * 60)
from hy3dshape.rembg import BackgroundRemover
from hy3dshape.pipelines import Hunyuan3DDiTFlowMatchingPipeline
from hy3dshape.pipelines import export_to_trimesh
from hy3dshape import FaceReducer
print("Loading shape model...")
t0 = time.time()
rmbg = BackgroundRemover()
pipeline = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(
MODEL_PATH, subfolder=SUBFOLDER, use_safetensors=False, device='cuda'
)
face_reducer = FaceReducer()
print(f"Shape model loaded in {time.time()-t0:.1f}s")
print(f"GPU memory: {torch.cuda.memory_allocated()/1024**3:.2f} GB")
results = {}
for i, img_path in enumerate(image_files):
name = Path(img_path).stem
item_dir = os.path.join(output_dir, name)
os.makedirs(item_dir, exist_ok=True)
mesh_path = os.path.join(item_dir, "white_mesh.obj")
if os.path.exists(mesh_path):
print(f"[{i+1}/{len(image_files)}] {name}: shape exists, skipping")
results[name] = {"image": img_path, "mesh": mesh_path, "dir": item_dir}
continue
print(f"\n[{i+1}/{len(image_files)}] Generating shape for: {name}")
t1 = time.time()
try:
image = Image.open(img_path).convert("RGBA")
if image.mode == "RGB" or image.getchannel("A").getextrema()[0] > 250:
image = rmbg(image.convert("RGB"))
generator = torch.Generator().manual_seed(SEED)
outputs = pipeline(
image=image,
num_inference_steps=SHAPE_STEPS,
guidance_scale=GUIDANCE_SCALE,
generator=generator,
octree_resolution=OCTREE_RESOLUTION,
num_chunks=NUM_CHUNKS,
output_type='mesh',
)
mesh = export_to_trimesh(outputs)[0]
# Face reduction for texture gen compatibility
mesh = face_reducer(mesh)
mesh.export(mesh_path, include_normals=False)
# Save input image alongside mesh for texture gen
input_copy = os.path.join(item_dir, "input.png")
image.save(input_copy)
results[name] = {"image": img_path, "mesh": mesh_path, "dir": item_dir}
print(f" Done in {time.time()-t1:.1f}s | faces: {mesh.faces.shape[0]}")
except Exception as e:
print(f" ERROR: {e}")
import traceback; traceback.print_exc()
results[name] = {"image": img_path, "mesh": None, "dir": item_dir, "error": str(e)}
clear_gpu()
# Unload shape model
print("\nUnloading shape model...")
del pipeline, rmbg, face_reducer
clear_gpu()
print(f"GPU memory after unload: {torch.cuda.memory_allocated()/1024**3:.2f} GB")
return results
def phase2_texture_generation(results, output_dir):
"""Phase 2: Load texture model, texture all meshes, unload."""
print("\n" + "=" * 60)
print("PHASE 2: Texture Generation")
print("=" * 60)
meshes_to_texture = {k: v for k, v in results.items() if v.get("mesh")}
if not meshes_to_texture:
print("No meshes to texture!")
return results
from hy3dpaint.textureGenPipeline import Hunyuan3DPaintPipeline, Hunyuan3DPaintConfig
from hy3dpaint.convert_utils import create_glb_with_pbr_materials
print("Loading texture model...")
t0 = time.time()
conf = Hunyuan3DPaintConfig(TEXGEN_MAX_VIEWS, TEXGEN_RESOLUTION)
conf.realesrgan_ckpt_path = "hy3dpaint/ckpt/RealESRGAN_x4plus.pth"
conf.multiview_cfg_path = "hy3dpaint/cfgs/hunyuan-paint-pbr.yaml"
conf.custom_pipeline = "hy3dpaint/hunyuanpaintpbr"
tex_pipeline = Hunyuan3DPaintPipeline(conf)
print(f"Texture model loaded in {time.time()-t0:.1f}s")
print(f"GPU memory: {torch.cuda.memory_allocated()/1024**3:.2f} GB")
for i, (name, info) in enumerate(meshes_to_texture.items()):
item_dir = info["dir"]
mesh_path = info["mesh"]
img_path = info["image"]
textured_obj = os.path.join(item_dir, "textured_mesh.obj")
textured_glb = os.path.join(item_dir, "textured_mesh.glb")
if os.path.exists(textured_glb):
print(f"[{i+1}/{len(meshes_to_texture)}] {name}: textured mesh exists, skipping")
results[name]["textured_glb"] = textured_glb
continue
print(f"\n[{i+1}/{len(meshes_to_texture)}] Texturing: {name}")
t1 = time.time()
try:
output_path = tex_pipeline(
mesh_path=mesh_path,
image_path=img_path,
output_mesh_path=textured_obj,
save_glb=False,
)
# Convert OBJ to GLB with PBR materials
textures = {
'albedo': output_path.replace('.obj', '.jpg'),
'metallic': output_path.replace('.obj', '_metallic.jpg'),
'roughness': output_path.replace('.obj', '_roughness.jpg'),
}
create_glb_with_pbr_materials(output_path, textures, textured_glb)
results[name]["textured_obj"] = output_path
results[name]["textured_glb"] = textured_glb
print(f" Done in {time.time()-t1:.1f}s")
print(f" Output: {textured_glb}")
except Exception as e:
print(f" ERROR: {e}")
import traceback; traceback.print_exc()
results[name]["tex_error"] = str(e)
clear_gpu()
# Unload texture model
print("\nUnloading texture model...")
del tex_pipeline
clear_gpu()
print(f"GPU memory after unload: {torch.cuda.memory_allocated()/1024**3:.2f} GB")
return results
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
image_files = get_image_files(INPUT_DIR)
if not image_files:
print(f"No images found in {INPUT_DIR}")
return
print(f"Found {len(image_files)} images:")
for f in image_files:
print(f" - {os.path.basename(f)}")
total_start = time.time()
# Phase 1: Shape generation (shape model only in VRAM)
results = phase1_shape_generation(image_files, OUTPUT_DIR)
# Phase 2: Texture generation (texture model only in VRAM)
results = phase2_texture_generation(results, OUTPUT_DIR)
# Summary
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
total_time = time.time() - total_start
success = sum(1 for v in results.values() if v.get("textured_glb"))
shape_only = sum(1 for v in results.values() if v.get("mesh") and not v.get("textured_glb"))
failed = sum(1 for v in results.values() if not v.get("mesh"))
for name, info in results.items():
status = "✓ textured" if info.get("textured_glb") else (
"△ shape only" if info.get("mesh") else "✗ failed"
)
print(f" {name}: {status}")
print(f"\nTotal: {len(results)} | Success: {success} | Shape only: {shape_only} | Failed: {failed}")
print(f"Total time: {total_time:.1f}s ({total_time/60:.1f}m)")
print(f"Output directory: {os.path.abspath(OUTPUT_DIR)}")
if __name__ == "__main__":
main()