-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathinference.py
More file actions
executable file
·359 lines (315 loc) · 15.2 KB
/
Copy pathinference.py
File metadata and controls
executable file
·359 lines (315 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
import os
import argparse
import math
import time
import torch
import numpy as np
import cv2
from PIL import Image
os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '1'
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
os.environ.setdefault("ATTN_BACKEND", "flash_attn")
os.environ["FLEX_GEMM_AUTOTUNE_CACHE_PATH"] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'autotune_cache.json')
os.environ["FLEX_GEMM_AUTOTUNER_VERBOSE"] = '1'
from pixal3d.pipelines import Pixal3DImageTo3DPipeline
import o_voxel
# ============================================================================
# Constants & Defaults
# ============================================================================
MOGE_MODEL_NAME = "Ruicheng/moge-2-vitl"
# Base Pixal3D weights. Prefer the copy bundled next to this file so the tree is
# self-contained (no HuggingFace cache required) — this is also the SAME directory
# reconstruct_object.py reads its LoRA base checkpoints from, so the base
# weights are loaded from one place instead of two. Falls back to the HF repo id
# when the local copy is absent; init_pipeline prints which one is used.
_LOCAL_MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'pretrained', 'Pixal3D')
MODEL_PATH = (_LOCAL_MODEL_PATH
if os.path.exists(os.path.join(_LOCAL_MODEL_PATH, 'pipeline.json'))
else "TencentARC/Pixal3D")
IMAGE_COND_CONFIGS = {
"ss": {
"model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
"image_size": 512,
"grid_resolution": 16,
},
"shape_512": {
"model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
"image_size": 512,
"grid_resolution": 32,
"use_naf_upsample": True,
"naf_target_size": 512,
},
"shape_1024": {
"model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
"image_size": 1024,
"grid_resolution": 64,
"use_naf_upsample": True,
"naf_target_size": 512,
},
"tex_1024": {
"model_name": "camenduru/dinov3-vitl16-pretrain-lvd1689m",
"image_size": 1024,
"grid_resolution": 64,
"use_naf_upsample": True,
"naf_target_size": 1024,
},
}
# ============================================================================
# Model Loading
# ============================================================================
def build_image_cond_model(config: dict):
from pixal3d.trainers.flow_matching.mixins.image_conditioned_proj import DinoV3ProjFeatureExtractor
model = DinoV3ProjFeatureExtractor(**config)
model.eval()
return model
def load_moge_model(device="cuda", model_name=MOGE_MODEL_NAME):
from moge.model.v2 import MoGeModel
moge_model = MoGeModel.from_pretrained(model_name)
moge_model = moge_model.to(device)
moge_model.eval()
return moge_model
def _load_state_dict_any(path: str):
"""Load a state_dict from .pt or .safetensors."""
if path.endswith('.safetensors'):
from safetensors.torch import load_file
return load_file(path)
return torch.load(path, map_location='cpu', weights_only=True)
def override_pipeline_ckpt(pipeline, model_name: str, ckpt_path: str):
"""Swap one of the pipeline's model weights with a local checkpoint.
Example:
override_pipeline_ckpt(pipeline, 'tex_slat_flow_model_1024',
'results/tex_ft1024_smoke_100iter/ckpts/denoiser_step0000100.pt')
"""
if model_name not in pipeline.models:
raise KeyError(f"Pipeline has no model '{model_name}'. Available: {list(pipeline.models.keys())}")
print(f"[Override] Loading {ckpt_path} into pipeline.models['{model_name}']...")
state_dict = _load_state_dict_any(ckpt_path)
missing, unexpected = pipeline.models[model_name].load_state_dict(state_dict, strict=False)
if missing:
print(f" Warning: {len(missing)} missing key(s), e.g. {missing[:3]}")
if unexpected:
print(f" Warning: {len(unexpected)} unexpected key(s), e.g. {unexpected[:3]}")
print(f"[Override] Done. ({len(state_dict)} params loaded)")
def init_pipeline(model_path=MODEL_PATH, device="cuda", low_vram=False,
tex_flow_ckpt: str = None):
print(f"[Pipeline] Loading from {model_path}...")
pipeline = Pixal3DImageTo3DPipeline.from_pretrained(model_path)
# Swap texture flow weights with a finetuned checkpoint, if provided.
if tex_flow_ckpt:
override_pipeline_ckpt(pipeline, 'tex_slat_flow_model_1024', tex_flow_ckpt)
print("[ImageCond] Building DinoV3ProjFeatureExtractor models...")
pipeline.image_cond_model_ss = build_image_cond_model(IMAGE_COND_CONFIGS["ss"])
pipeline.image_cond_model_shape_512 = build_image_cond_model(IMAGE_COND_CONFIGS["shape_512"])
pipeline.image_cond_model_shape_1024 = build_image_cond_model(IMAGE_COND_CONFIGS["shape_1024"])
pipeline.image_cond_model_tex_1024 = build_image_cond_model(IMAGE_COND_CONFIGS["tex_1024"])
if low_vram:
# Low-VRAM mode: models stay on CPU, loaded to GPU on-demand per stage.
# Peak VRAM = one flow model + one DinoV3, not all ~18 GB at once.
print("[NAF] Pre-downloading NAF upsampler weights (CPU only)...")
for attr in ['image_cond_model_ss', 'image_cond_model_shape_512',
'image_cond_model_shape_1024', 'image_cond_model_tex_1024']:
m = getattr(pipeline, attr, None)
if m is not None and getattr(m, 'use_naf_upsample', False):
m._load_naf()
pipeline._device = torch.device(device)
pipeline.low_vram = True
print("[Pipeline] Low-VRAM mode enabled.")
else:
# Standard mode: all models loaded to GPU at once (faster, needs more VRAM).
pipeline.low_vram = False
pipeline.cuda()
pipeline.image_cond_model_ss.cuda()
pipeline.image_cond_model_shape_512.cuda()
pipeline.image_cond_model_shape_1024.cuda()
pipeline.image_cond_model_tex_1024.cuda()
print("[NAF] Pre-loading NAF upsampler model...")
for attr in ['image_cond_model_ss', 'image_cond_model_shape_512',
'image_cond_model_shape_1024', 'image_cond_model_tex_1024']:
m = getattr(pipeline, attr, None)
if m is not None and getattr(m, 'use_naf_upsample', False):
m._load_naf()
print("[Pipeline] Standard mode (all models on GPU).")
return pipeline
# ============================================================================
# Camera Estimation
# ============================================================================
def compute_f_pixels(camera_angle_x: float, resolution: int) -> float:
focal_length = 16.0 / torch.tan(torch.tensor(camera_angle_x / 2.0))
f_pixels = focal_length * resolution / 32.0
return float(f_pixels.item())
def distance_from_fov(camera_angle_x, grid_point, target_point, mesh_scale, image_resolution):
rotation_matrix = torch.tensor([[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]])
gp = grid_point.to(torch.float32) @ rotation_matrix.T
gp = gp / mesh_scale / 2
xw, yw, zw = gp[0].item(), gp[1].item(), gp[2].item()
xt, yt = float(target_point[0].item()), float(target_point[1].item())
f_pixels = compute_f_pixels(camera_angle_x, image_resolution)
x_ndc = xt - image_resolution / 2.0
y_ndc = -(yt - image_resolution / 2.0)
distance_x = f_pixels * xw / x_ndc - yw
return {"distance_from_x": float(distance_x), "f_pixels": float(f_pixels)}
def get_camera_params_wild_moge(image_path, moge_model, device="cuda", mesh_scale=1.0, extend_pixel=0, image_resolution=512):
pil_image = Image.open(image_path).convert("RGB")
width, height = pil_image.size
image_np = np.array(pil_image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_np).permute(2, 0, 1).to(device)
with torch.no_grad():
output = moge_model.infer(image_tensor)
intrinsics = output["intrinsics"].squeeze().cpu().numpy()
fx_normalized = intrinsics[0, 0]
fx = fx_normalized * width
camera_angle_x = 2 * math.atan(width / (2 * fx))
grid_point = torch.tensor([-1.0, 0.0, 0.0])
distance = distance_from_fov(
camera_angle_x, grid_point,
torch.tensor([0 - extend_pixel, image_resolution - 1 + extend_pixel]),
mesh_scale, image_resolution
)["distance_from_x"]
return {'camera_angle_x': camera_angle_x, 'distance': distance, 'mesh_scale': mesh_scale}
# ============================================================================
# Main Inference
# ============================================================================
def run_inference(
image_path: str,
output_path: str,
seed: int = 42,
ss_guidance_strength: float = 7.5,
ss_guidance_rescale: float = 0.7,
ss_sampling_steps: int = 12,
ss_rescale_t: float = 5.0,
shape_slat_guidance_strength: float = 7.5,
shape_slat_guidance_rescale: float = 0.5,
shape_slat_sampling_steps: int = 12,
shape_slat_rescale_t: float = 3.0,
tex_slat_guidance_strength: float = 1.0,
tex_slat_guidance_rescale: float = 0.0,
tex_slat_sampling_steps: int = 12,
tex_slat_rescale_t: float = 3.0,
mesh_scale: float = 1.0,
extend_pixel: int = 0,
image_resolution: int = 512,
max_num_tokens: int = 49152,
model_path: str = MODEL_PATH,
manual_fov: float = -1.0,
low_vram: bool = False,
resolution: int = -1,
tex_flow_ckpt: str = None,
):
# Load models
pipeline = init_pipeline(model_path, low_vram=low_vram, tex_flow_ckpt=tex_flow_ckpt)
# Preprocess image first — rembg loads to GPU for this call, then offloads.
# MoGe is loaded afterwards so both never occupy VRAM at the same time.
print(f"[Inference] Processing image: {image_path}")
img = Image.open(image_path)
image_preprocessed = pipeline.preprocess_image(img)
# Save preprocessed image for MoGe
tmp_path = os.path.join(os.path.dirname(os.path.abspath(output_path)), f"_tmp_preprocessed_{int(time.time()*1000)}.png")
image_preprocessed.save(tmp_path)
# Camera estimation
if manual_fov > 0:
# Use manually specified FOV (in radians)
camera_angle_x = float(manual_fov)
grid_point = torch.tensor([-1.0, 0.0, 0.0])
distance = distance_from_fov(
camera_angle_x, grid_point,
torch.tensor([0 - extend_pixel, image_resolution - 1 + extend_pixel]),
mesh_scale, image_resolution
)["distance_from_x"]
camera_params = {'camera_angle_x': camera_angle_x, 'distance': distance, 'mesh_scale': mesh_scale}
print(f"[Inference] Using manual FOV: {math.degrees(manual_fov):.2f}° ({manual_fov:.4f} rad), distance={distance:.4f}")
else:
print("[MoGe-2] Loading model for camera estimation...")
moge_model = load_moge_model(device="cuda")
print("[Inference] Estimating camera parameters...")
camera_params = get_camera_params_wild_moge(
tmp_path, moge_model, device="cuda",
mesh_scale=mesh_scale, extend_pixel=extend_pixel,
image_resolution=image_resolution,
)
print(f" camera_angle_x={camera_params['camera_angle_x']:.4f}, distance={camera_params['distance']:.4f}")
# MoGe is only needed for camera estimation; free its VRAM for inference.
moge_model.cpu()
del moge_model
torch.cuda.empty_cache()
os.remove(tmp_path)
# Run pipeline
print("[Inference] Running 3D generation pipeline...")
torch.manual_seed(seed)
ss_sampler_override = {
"steps": ss_sampling_steps, "guidance_strength": ss_guidance_strength,
"guidance_rescale": ss_guidance_rescale, "rescale_t": ss_rescale_t,
}
shape_sampler_override = {
"steps": shape_slat_sampling_steps, "guidance_strength": shape_slat_guidance_strength,
"guidance_rescale": shape_slat_guidance_rescale, "rescale_t": shape_slat_rescale_t,
}
tex_sampler_override = {
"steps": tex_slat_sampling_steps, "guidance_strength": tex_slat_guidance_strength,
"guidance_rescale": tex_slat_guidance_rescale, "rescale_t": tex_slat_rescale_t,
}
pipeline_type = f"{resolution if resolution > 0 else (1024 if low_vram else 1536)}_cascade"
print(f"[Inference] Using pipeline_type={pipeline_type}")
mesh_list, (shape_slat, tex_slat, res) = pipeline.run(
image_preprocessed,
camera_params=camera_params,
seed=seed,
sparse_structure_sampler_params=ss_sampler_override,
shape_slat_sampler_params=shape_sampler_override,
tex_slat_sampler_params=tex_sampler_override,
preprocess_image=False,
return_latent=True,
pipeline_type=pipeline_type,
max_num_tokens=max_num_tokens,
)
mesh = mesh_list[0]
# Extract GLB
print("[Inference] Extracting GLB...")
glb = o_voxel.postprocess.to_glb(
vertices=mesh.vertices, faces=mesh.faces, attr_volume=mesh.attrs,
coords=mesh.coords, attr_layout=pipeline.pbr_attr_layout,
grid_size=res, aabb=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]],
decimation_target=1000000, texture_size=4096,
remesh=True, remesh_band=1, remesh_project=0, use_tqdm=True,
)
# Apply rotation
rot = np.array([
[-1, 0, 0, 0],
[ 0, 0, -1, 0],
[ 0, -1, 0, 0],
[ 0, 0, 0, 1],
], dtype=np.float64)
glb.apply_transform(rot)
# Export
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
glb.export(output_path, extension_webp=False)
print(f"[Done] GLB saved to: {output_path}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pixal3D Inference: Image to GLB")
parser.add_argument("--image", type=str, required=True, help="Path to input image")
parser.add_argument("--output", type=str, default="./output.glb", help="Output GLB file path")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("--fov", type=float, default=-1.0,
help="Manual camera FOV in radians (e.g. 0.2). "
"If not set, FOV is auto-estimated via MoGe-2. "
"Try 0.2 rad if you notice distortion.")
parser.add_argument("--model_path", type=str, default=MODEL_PATH, help="Model path or HuggingFace repo")
parser.add_argument("--low_vram", action="store_true",
help="Enable low-VRAM mode: models stay on CPU and are loaded to GPU on-demand per stage. "
"Reduces peak VRAM from ~18GB to ~10-12GB at the cost of slower inference.")
parser.add_argument("--resolution", type=int, default=-1,
help="Pipeline resolution (1024 or 1536). Default: 1024 if --low_vram, else 1536.")
parser.add_argument("--tex_flow_ckpt", type=str, default=None,
help="Override texture flow weights with a finetuned ckpt "
"(.pt or .safetensors). Loaded into pipeline.models['tex_slat_flow_model_1024'].")
args = parser.parse_args()
run_inference(
image_path=args.image,
output_path=args.output,
seed=args.seed,
manual_fov=args.fov,
model_path=args.model_path,
low_vram=args.low_vram,
resolution=args.resolution,
tex_flow_ckpt=args.tex_flow_ckpt,
)