Download modules/octree_decode.py from MayMeHappy/mul: direct link, hf CLI and curl.
- Browser
- Download file 26.5 kB
-
https://huggingface.co/MayMeHappy/mul/resolve/main/modules/octree_decode.py
- Command line
-
hf download hf://MayMeHappy/mul/modules/octree_decode.py
-
curl -L -o octree_decode.py https://huggingface.co/MayMeHappy/mul/resolve/main/modules/octree_decode.py
26.5 kB
| import gc | |
| import glob | |
| import math | |
| import os | |
| import random | |
| import re | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import trimesh | |
| from accelerate import Accelerator, DistributedDataParallelKwargs | |
| from tqdm import tqdm | |
| import importlib.util | |
| from utils.config import load_config | |
| from dataset.mesh_dataset import mul_encode_mesh, discrete_undiscrete_and_clean | |
| from models.generator import FactorizedDiffusionTransformer | |
| from modules.octree_codec import MeshCodec | |
| from utils.postprocessing import _fill_holes | |
| from utils.data_utils import normalize | |
| # fix3 编码格式的数据通路(与训练同一份实现, 已抽成独立模块) | |
| from modules.token_utils import split_fix3_token_list, subcodes_to_flat_token | |
| def set_seed(seed): | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| if torch.cuda.is_available(): | |
| torch.cuda.manual_seed(seed) | |
| torch.cuda.manual_seed_all(seed) | |
| torch.backends.cudnn.deterministic = True | |
| torch.backends.cudnn.benchmark = False | |
| os.environ["PYTHONHASHSEED"] = str(seed) | |
| def _depth_from_resolution(resolution): | |
| if resolution <= 0: | |
| raise ValueError(f"resolution must be positive, got {resolution}") | |
| depth = int(round(math.log2(resolution))) | |
| if (1 << depth) != resolution: | |
| raise ValueError(f"resolution must be power of 2, got {resolution}") | |
| return depth | |
| def read_dataset_paths(txt_path): | |
| mesh_paths = [] | |
| with open(txt_path, "r") as f: | |
| for line in f: | |
| path = line.strip() | |
| if path: | |
| mesh_paths.append(path) | |
| return mesh_paths | |
| def raw_stem_to_pair_key(raw_path): | |
| stem = Path(raw_path).stem | |
| # raw name example: 1(42)_y_f_4244 -> pair key: 1(42) | |
| return re.sub(r"_y_f_\d+$", "", stem) | |
| def gt_stem_to_pair_key(gt_path): | |
| return Path(gt_path).stem | |
| def get_face_count(file_path): | |
| """ | |
| 使用正则表达式从文件名中提取面片数量 | |
| 支持格式: *_f{num}_v*.obj 或 *_facenum_{num}_*.glb | |
| """ | |
| filename = Path(file_path).name | |
| match = re.search(r'_f(\d+)_v|_facenum_(\d+)', filename) | |
| if match: | |
| # group(1) 对应 _f(\d+)_v 里的数字 | |
| # group(2) 对应 _facenum_(\d+) 里的数字 | |
| return int(match.group(1) or match.group(2)) | |
| return None | |
| # def build_raw_gt_pairs(raw_txt_path, gt_txt_path): | |
| # raw_paths = read_dataset_paths(raw_txt_path) | |
| # gt_paths = read_dataset_paths(gt_txt_path) | |
| # # If two txt paths are the same source, pair each mesh with itself directly. | |
| # if Path(raw_txt_path).resolve() == Path(gt_txt_path).resolve(): | |
| # return [(p, p) for p in raw_paths], [] | |
| # gt_by_key = {} | |
| # for gt_path in gt_paths: | |
| # gt_by_key[gt_stem_to_pair_key(gt_path)] = gt_path | |
| # pairs = [] | |
| # missing_gt = [] | |
| # for raw_path in raw_paths: | |
| # key = raw_stem_to_pair_key(raw_path) | |
| # gt_path = gt_by_key.get(key) | |
| # if gt_path is None: | |
| # missing_gt.append(raw_path) | |
| # continue | |
| # pairs.append((raw_path, gt_path)) | |
| # return pairs, missing_gt | |
| def build_raw_gt_pairs(raw_txt_path, gt_txt_path, min_faces=None, max_faces=None): | |
| raw_paths = read_dataset_paths(raw_txt_path) | |
| gt_paths = read_dataset_paths(gt_txt_path) | |
| # ===== 新增过滤逻辑 ===== | |
| filtered_raw_paths = [] | |
| for p in raw_paths: | |
| if min_faces is None and max_faces is None: | |
| filtered_raw_paths.append(p) | |
| continue | |
| faces = get_face_count(p) | |
| if faces is not None: | |
| # 检查是否在指定范围内 | |
| if min_faces is not None and faces < min_faces: | |
| continue | |
| if max_faces is not None and faces > max_faces: | |
| continue | |
| filtered_raw_paths.append(p) | |
| else: | |
| # 如果文件名中没找到面片数特征,默认保留(你也可以改为 continue 丢弃它) | |
| filtered_raw_paths.append(p) | |
| raw_paths = filtered_raw_paths | |
| # ======================== | |
| # If two txt paths are the same source, pair each mesh with itself directly. | |
| if Path(raw_txt_path).resolve() == Path(gt_txt_path).resolve(): | |
| return [(p, p) for p in raw_paths], [] | |
| gt_by_key = {} | |
| for gt_path in gt_paths: | |
| gt_by_key[gt_stem_to_pair_key(gt_path)] = gt_path | |
| pairs = [] | |
| missing_gt = [] | |
| for raw_path in raw_paths: | |
| key = raw_stem_to_pair_key(raw_path) | |
| gt_path = gt_by_key.get(key) | |
| if gt_path is None: | |
| missing_gt.append(raw_path) | |
| continue | |
| pairs.append((raw_path, gt_path)) | |
| return pairs, missing_gt | |
| def save_gt_mesh(mesh, out_dir, name): | |
| gt_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False) | |
| gt_mesh.vertices = gt_mesh.vertices[:, [1, 2, 0]] | |
| gt_mesh.export(os.path.join(out_dir, f"{name}_gt.obj")) | |
| def save_raw_gt_mesh(mesh, out_dir, name): | |
| gt_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False) | |
| gt_mesh.vertices = gt_mesh.vertices[:, [1, 2, 0]] | |
| gt_mesh.export(os.path.join(out_dir, f"{name}_raw_gt.obj")) | |
| def save_stage_input_mesh(mesh, out_dir, name, stage_in_res, stage_out_res, stage_in_depth): | |
| coarse_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False) | |
| coarse_mesh.vertices = coarse_mesh.vertices[:, [1, 2, 0]] | |
| suffix = f"_stage_{stage_in_res}to{stage_out_res}_input_coarse" | |
| coarse_mesh.export(os.path.join(out_dir, f"{name}{suffix}_depth{stage_in_depth}.obj")) | |
| def save_stage_prediction(codec, coord_seq, gen_subcodes, codec_cfg, out_dir, name, target_depth, suffix, fix_holes, raw_dis_mesh=None, gt_dis_mesh=None, remove_degenerate=True): | |
| # fix3 数据通路:coord(粗层条件, 含 offset) + 采样得到的 refine 子码(local[0,256)) -> | |
| # subcodes_to_flat_token 还原成扁平 60/face token(含 offset) -> codec.decode_tokens 解成 mesh。 | |
| # 与 acc_v2_fix3.fix3_generate 完全一致;不再做旧 18/face 的 coord/refine 交错。 | |
| flat = subcodes_to_flat_token(coord_seq, gen_subcodes, codec_cfg) | |
| if not flat: | |
| return None | |
| mesh = codec.decode_tokens(flat, target_depth=target_depth - 1) | |
| if mesh is None: | |
| return None | |
| # 先剔除退化面(三顶点存在重合的零面积面),再做 normals/holes 修复,避免退化面干扰后续几何修复。 | |
| if remove_degenerate: | |
| mesh = remove_degenerate_faces(mesh) | |
| trimesh.repair.fix_normals(mesh) | |
| if fix_holes: | |
| verts = torch.tensor(mesh.vertices, device="cuda", dtype=torch.float32) | |
| faces = torch.tensor(mesh.faces, device="cuda", dtype=torch.int32) | |
| new_verts, new_faces = _fill_holes(verts, faces, debug=False, verbose=False, only_fill_holes=True) | |
| mesh = trimesh.Trimesh(vertices=new_verts.cpu().numpy(), faces=new_faces.cpu().numpy()) | |
| return_mesh = mesh.copy() | |
| mesh.vertices = mesh.vertices[:, [1, 2, 0]] | |
| mesh.export(os.path.join(out_dir, f"{name}{suffix}_depth{target_depth}.obj")) | |
| if raw_dis_mesh is not None: | |
| raw_dis_mesh.vertices = raw_dis_mesh.vertices[:, [1, 2, 0]] | |
| raw_dis_mesh.export(os.path.join(out_dir, f"{name}{suffix}_raw_dis_depth{target_depth-1}.obj")) | |
| if gt_dis_mesh is not None: | |
| gt_dis_mesh.vertices = gt_dis_mesh.vertices[:, [1, 2, 0]] | |
| gt_dis_mesh.export(os.path.join(out_dir, f"{name}{suffix}_gt_dis_depth{target_depth}.obj")) | |
| return return_mesh | |
| def _decode_subcodes_to_mesh(codec, coord_seq, gen_subcodes, codec_cfg, target_depth): | |
| """与 save_stage_prediction 同款 decode(不 fix_normals/fix_holes/不导出),用于 best-of-N 打分。""" | |
| flat = subcodes_to_flat_token(coord_seq, gen_subcodes, codec_cfg) | |
| if not flat: | |
| return None | |
| return codec.decode_tokens(flat, target_depth=target_depth - 1) | |
| def _pc_chamfer_score(mesh, pc_xyz, n_sample=4096): | |
| """候选 mesh 表面采样点 与 条件点云 pc_xyz 的对称 chamfer(越小越贴合目标点云)。 | |
| 两者同处归一化 frame(mul_encode_mesh 内 normalize + decode 反离散化),可直接算。 | |
| 退化面(零面积)不会被 sample_surface 采到 -> 天然只评有效曲面;无有效面返回 inf。""" | |
| if mesh is None or len(getattr(mesh, "faces", [])) == 0 or len(mesh.vertices) == 0: | |
| return float("inf") | |
| try: | |
| pts, _ = trimesh.sample.sample_surface(mesh, n_sample) | |
| pts = np.asarray(pts) | |
| except Exception: | |
| pts = np.asarray(mesh.vertices) | |
| if pts.shape[0] == 0: | |
| return float("inf") | |
| pts = torch.as_tensor(pts, dtype=torch.float32, device=pc_xyz.device) | |
| ref = pc_xyz | |
| if ref.shape[0] > n_sample: | |
| idx = torch.randperm(ref.shape[0], device=ref.device)[:n_sample] | |
| ref = ref[idx] | |
| d = torch.cdist(pts, ref) # [Np, Nr] | |
| return float((d.min(1).values.mean() + d.min(0).values.mean()).item()) | |
| def remove_degenerate_faces(mesh): | |
| """删除退化面:三角形三个顶点中存在重合顶点(任意两顶点坐标相同 -> 零面积/退化)。 | |
| 按顶点坐标判定(而非仅 index),因为 octree 解码后不同 index 可能落到同一离散坐标。 | |
| 返回过滤后的新 mesh;若没有退化面则原样返回;若全部退化则返回原 mesh(避免产出空 mesh)。""" | |
| if mesh is None or len(getattr(mesh, "faces", [])) == 0 or len(getattr(mesh, "vertices", [])) == 0: | |
| return mesh | |
| faces = np.asarray(mesh.faces) | |
| verts = np.asarray(mesh.vertices) | |
| v0, v1, v2 = verts[faces[:, 0]], verts[faces[:, 1]], verts[faces[:, 2]] | |
| # 三条边都非零长度(三顶点两两不重合)才保留 | |
| keep = ( | |
| np.any(v0 != v1, axis=1) | |
| & np.any(v1 != v2, axis=1) | |
| & np.any(v0 != v2, axis=1) | |
| ) | |
| if keep.all(): | |
| return mesh | |
| if not keep.any(): | |
| return mesh | |
| return trimesh.Trimesh(vertices=verts.copy(), faces=faces[keep].copy(), process=False) | |
| def process_one_mesh( | |
| raw_mesh_path, | |
| gt_mesh_path, | |
| model, | |
| codec, | |
| codec_cfg, | |
| device, | |
| pad_id, | |
| config, | |
| output_folder, | |
| stage_resolutions, | |
| fix_holes=True, | |
| use_gt_pc=True, | |
| remove_degenerate=True, | |
| ): | |
| import time | |
| t_start = time.time() | |
| import time | |
| t_total_start = time.time() | |
| name = Path(raw_mesh_path).stem | |
| t_start = time.time() | |
| mesh = trimesh.load(raw_mesh_path, force="mesh") | |
| mesh.vertices = mesh.vertices[:, [2, 0, 1]] | |
| gt_mesh = trimesh.load(gt_mesh_path, force="mesh") | |
| gt_mesh.vertices = gt_mesh.vertices[:, [2, 0, 1]] | |
| normalize(gt_mesh) | |
| normalize(mesh) | |
| save_raw_gt_mesh(mesh, output_folder, name) | |
| save_gt_mesh(gt_mesh, output_folder, name) | |
| t_load = time.time() | |
| print(f"[{name}] Load & normalize time: {t_load - t_start:.4f}s") | |
| stage_depths = [_depth_from_resolution(x) for x in stage_resolutions] | |
| # First-stage input uses codec encode-decode result at the minimum resolution. | |
| min_stage_depth = stage_depths[0] | |
| t_start = time.time() | |
| init_raw_token, _, init_quantized_mesh = codec.encode_mesh(mesh, target_depth=min_stage_depth) | |
| init_mesh = codec.decode_raw_tokens(init_raw_token, target_depth=min_stage_depth) | |
| t_init = time.time() | |
| print(f"[{name}] Init encode & decode (depth={min_stage_depth}): {t_init - t_start:.4f}s") | |
| if init_mesh is not None: | |
| current_mesh = init_mesh | |
| elif init_quantized_mesh is not None: | |
| current_mesh = init_quantized_mesh | |
| else: | |
| current_mesh = mesh.copy() | |
| for stage_idx in range(len(stage_depths) - 1): | |
| stage_start_time = time.time() | |
| stage_in_depth = stage_depths[stage_idx] | |
| stage_out_depth = stage_depths[stage_idx + 1] | |
| stage_in_res = stage_resolutions[stage_idx] | |
| stage_out_res = stage_resolutions[stage_idx + 1] | |
| save_stage_input_mesh( | |
| mesh=current_mesh, | |
| out_dir=output_folder, | |
| name=name, | |
| stage_in_res=stage_in_res, | |
| stage_out_res=stage_out_res, | |
| stage_in_depth=stage_in_depth, | |
| ) | |
| t_start = time.time() | |
| stage_data = mul_encode_mesh( | |
| raw_mesh=current_mesh, | |
| gt_mesh=gt_mesh, | |
| codec=codec, | |
| tokenizer_target_depth=stage_in_depth, | |
| pc_num=config.data_processing.pc_num, | |
| total_pc_num=config.data_processing.total_pc_num, | |
| use_gt_pc=use_gt_pc, | |
| clean=False | |
| ) | |
| t_mul_encode = time.time() | |
| print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | mul_encode_mesh time: {t_mul_encode - t_start:.4f}s") | |
| pc = stage_data["pc"].unsqueeze(0).to(device) | |
| # fix3 数据通路(与 acc_v2_fix3.fix3_generate 一致): | |
| # 扁平 60/face token -> 取 coord(粗层条件) -> 因子分解采样 refine 子码。 | |
| # 这里只用 coord 作为条件,refine 子码全部由模型 sample 生成(GT refine 不参与推理)。 | |
| coord_list, _ = split_fix3_token_list([stage_data["token"]], pad_id, device, codec_cfg) | |
| depth_t = torch.tensor([stage_in_depth], device=device, dtype=torch.long) | |
| cfg_scale = float(getattr(config.evaluation, "cfg_scale", 1.0)) | |
| # CFG ramp:linear 时每级采样的最初几步用低 cfg(cfg_start),按步线性升到 cfg_scale, | |
| # 避免高 cfg 在早期步把没把握的位置置信度抬高、过早 commit。constant=旧行为。 | |
| cfg_schedule = str(getattr(config.evaluation, "cfg_schedule", "constant")) | |
| cfg_start = float(getattr(config.evaluation, "cfg_start", 1.0)) | |
| best_of_n = int(getattr(config.evaluation, "best_of_n", 1)) | |
| # best-of-N 对抗级联累积误差:每级采 N 个候选 -> 各自 decode -> 选 chamfer-to-pc 最小者(最贴合 | |
| # 目标点云)作为本级输出喂下一级。点云 pc 是跨级不漂移的全局锚,用它选优能降低单级错误率、抑制误差累积。 | |
| # N=1 退化为单次贪心采样(确定性, 行为同前);N>1 需 temperature>0 才能采出多样候选。 | |
| sample_temp = float(getattr(config.evaluation, "sample_temperature", 1.0)) if best_of_n > 1 else 0.0 | |
| pc_xyz = stage_data["pc"][:, :3].to(device).float() | |
| t_start = time.time() | |
| gen_subcodes, best_score = None, float("inf") | |
| for cand in range(max(1, best_of_n)): | |
| gs = model.sample( | |
| coord_list, | |
| depth_t, | |
| pc=pc, | |
| T_infer=config.evaluation.diffusion_sample_steps, | |
| cfg_scale=cfg_scale, | |
| temperature=sample_temp, | |
| cfg_schedule=cfg_schedule, | |
| cfg_start=cfg_start, | |
| ) | |
| if best_of_n <= 1: | |
| gen_subcodes = gs | |
| break | |
| cand_mesh = _decode_subcodes_to_mesh(codec, coord_list[0], gs[0], codec_cfg, stage_out_depth) | |
| score = _pc_chamfer_score(cand_mesh, pc_xyz) | |
| if score < best_score: | |
| best_score, gen_subcodes = score, gs | |
| torch.cuda.synchronize() | |
| t_sample = time.time() | |
| if best_of_n > 1: | |
| print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | best-of-{best_of_n} (T={sample_temp}, cfg={cfg_scale}) " | |
| f"chamfer={best_score:.6f} | sample time: {t_sample - t_start:.4f}s") | |
| else: | |
| print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | factorized sample time: {t_sample - t_start:.4f}s") | |
| t_start = time.time() | |
| stage_suffix = f"_stage_{stage_in_res}to{stage_out_res}" | |
| stage_mesh = save_stage_prediction( | |
| codec=codec, | |
| coord_seq=coord_list[0], | |
| gen_subcodes=gen_subcodes[0], | |
| codec_cfg=codec_cfg, | |
| out_dir=output_folder, | |
| name=name, | |
| target_depth=stage_out_depth, | |
| suffix=stage_suffix, | |
| fix_holes=fix_holes, | |
| raw_dis_mesh=stage_data.get("raw_dis_mesh"), | |
| gt_dis_mesh=stage_data.get("gt_dis_mesh"), | |
| remove_degenerate=remove_degenerate, | |
| ) | |
| t_save_pred = time.time() | |
| print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | save_stage_prediction (decode + fix) time: {t_save_pred - t_start:.4f}s") | |
| print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | total stage time: {time.time() - stage_start_time:.4f}s\n") | |
| if stage_mesh is None: | |
| break | |
| current_mesh = stage_mesh | |
| print(f"[{name}] Total process_one_mesh time: {time.time() - t_total_start:.4f}s") | |
| class EMA: | |
| def __init__(self, model, decay=0.999): | |
| self.decay = decay | |
| self.shadow = {} | |
| self.register(model) | |
| def register(self, model): | |
| self.shadow = {} | |
| for name, param in model.named_parameters(): | |
| if param.requires_grad: | |
| self.shadow[name] = param.data.clone() | |
| def load_state_dict(self, state): | |
| self.decay = state.get("decay", self.decay) | |
| self.shadow = {k: v.clone() for k, v in state["shadow"].items()} | |
| def copy_to(self, model): | |
| """Swap shadow weights into the model in-place (for inference).""" | |
| copied, missing = 0, 0 | |
| for name, param in model.named_parameters(): | |
| if name in self.shadow: | |
| param.data.copy_(self.shadow[name].to(device=param.device, dtype=param.dtype)) | |
| copied += 1 | |
| else: | |
| missing += 1 | |
| print(f"[EMA.copy_to] copied {copied} params into model ({missing} not present in shadow)") | |
| def run_inference_on_all_checkpoints(model, accelerator, config, device, pad_id, codec, codec_cfg, ema=None): | |
| ckpt_dir = "ckpt_fix" | |
| cfg_scale = float(getattr(config.evaluation, "cfg_scale", 1.0)) # 采样 CFG 强度(与细分循环 model.sample 一致) | |
| output_folder = f"./pretrain_model_mul_more_data_1_to_64_eval_fix_easy2_cfg_{cfg_scale}_step_20" | |
| # raw_data_txt = "dataset/bpt_res_list.txt" /root/junkai_lin/mul_res_diffusion/demo/bpt_retrain_gt/data.txt | |
| # gt_data_txt = "dataset/trellis_clean_mesh.txt" /root/junkai_lin/mul_res_diffusion/demo/bpt_gt/data.txt | |
| # raw_data_txt = "/root/dataset/unique_files_glb_under20000face_compress_glb_with_info_val.txt" | |
| # gt_data_txt = "/root/dataset/unique_files_glb_under20000face_compress_glb_with_info_val.txt" | |
| raw_data_txt = "/root/dataset/unique_files_glb_under6000face_2degree_30ratio_0.01_with_info_val.txt" | |
| gt_data_txt = "/root/dataset/unique_files_glb_under6000face_2degree_30ratio_0.01_with_info_val.txt" | |
| # raw_data_txt = "/root/junkai_lin/mul_res_diffusion/demo/hun_yuan/data.txt" | |
| # gt_data_txt = "/root/junkai_lin/mul_res_diffusion/demo/hun_yuan/data.txt" | |
| stage_resolutions = [1,2,4,8,16,32,64] | |
| # raw_data_txt = "demo/tripo 1.0/data.txt" | |
| # gt_data_txt = "demo/tripo 1.0/data.txt" | |
| use_gt_pc=True | |
| fix_holes=False | |
| # 退化面删除开关:True=导出前剔除三顶点存在重合的零面积面。可在 config.evaluation.remove_degenerate_faces 覆盖。 | |
| remove_degenerate = bool(getattr(config.evaluation, "remove_degenerate_faces", True)) | |
| if accelerator.is_main_process: | |
| print(f"Using raw data txt: {raw_data_txt}") | |
| print(f"Using gt data txt: {gt_data_txt}") | |
| print(f"Searching checkpoints in: {ckpt_dir}") | |
| # mesh_pairs, missing_gt = build_raw_gt_pairs(raw_data_txt, gt_data_txt, max_faces=config.data_processing.max_face_num, min_faces=config.data_processing.min_face_num) | |
| mesh_pairs, missing_gt = build_raw_gt_pairs(raw_data_txt, gt_data_txt, max_faces=20000, min_faces=500) | |
| if accelerator.is_main_process: | |
| print(f"Matched raw/gt pairs: {len(mesh_pairs)}") | |
| if missing_gt: | |
| print(f"Raw without matched gt: {len(missing_gt)}") | |
| search_pattern = os.path.join(ckpt_dir, "checkpoint_*_epoch_*_loss.pth") | |
| ckpt_files = glob.glob(search_pattern) | |
| if not ckpt_files: | |
| if accelerator.is_main_process: | |
| print("No checkpoints found!") | |
| return | |
| ckpt_data = [] | |
| for f in ckpt_files: | |
| match = re.search(r"checkpoint_(\d+)_epoch_", f) | |
| if match: | |
| epoch = int(match.group(1)) | |
| ckpt_data.append((epoch, f)) | |
| ckpt_data.sort(key=lambda x: x[0]) | |
| # stage_resolutions = [32, 64, 128, 256] | |
| # stage_resolutions = [128, 256] | |
| for epoch, ckpt_path in ckpt_data: | |
| if accelerator.is_main_process: | |
| print(f"\n[{'='*40}]") | |
| print(f"Starting inference for epoch {epoch}") | |
| print(f"Loading weights from: {ckpt_path}") | |
| output_folder_ply = os.path.join(output_folder, f"{epoch}_epoch_val_generate_mul") | |
| if accelerator.is_main_process: | |
| os.makedirs(output_folder_ply, exist_ok=True) | |
| checkpoint = torch.load(ckpt_path, map_location=device) | |
| model_to_load = accelerator.unwrap_model(model) | |
| # 加载的是用「新模型代码(AR 连接头 + factorized)」训练出的 ckpt,必须 strict=True: | |
| # key/shape 一旦不匹配立即报错,避免 strict=False 把 AR 连接头静默留成随机初始化 -> 输出垃圾还不报警。 | |
| # (只有训练脚本 acc_v2_fix3 从旧 Linear 头 ckpt 暖启动时才用 load_strict=false,推理这里不允许。) | |
| model_to_load.load_state_dict(checkpoint["model_state_dict"], strict=True) | |
| # Auto-detect EMA: if the ckpt has an `ema_state_dict`, swap those | |
| # shadow weights into the model for inference. Otherwise fall back to | |
| # the raw `model_state_dict` we already loaded above. No reliance on | |
| # config `use_ema` / a separate EMA object — just check the ckpt. | |
| ema_state = checkpoint.get("ema_state_dict") | |
| shadow = ema_state.get("shadow") if isinstance(ema_state, dict) else None | |
| if shadow: | |
| with torch.no_grad(): | |
| copied, skipped = 0, 0 | |
| for name, param in model_to_load.named_parameters(): | |
| if name in shadow: | |
| param.data.copy_(shadow[name].to(device=param.device, dtype=param.dtype)) | |
| copied += 1 | |
| else: | |
| skipped += 1 | |
| if accelerator.is_main_process: | |
| msg = f"[Epoch {epoch}] EMA shadow detected in ckpt -> applied to model ({copied} params" | |
| if skipped: | |
| msg += f", {skipped} model params not in shadow" | |
| print(msg + ")") | |
| else: | |
| if accelerator.is_main_process: | |
| print(f"[Epoch {epoch}] no EMA in ckpt -> running with raw model_state_dict weights.") | |
| with torch.no_grad(): | |
| # 用 unwrap 后的网络做推理:FactorizedDiffusionTransformer.sample 定义在裸模型上, | |
| # DDP 包装层没有 .sample 方法。 | |
| model_to_load.eval() | |
| pbar = tqdm( | |
| enumerate(mesh_pairs), | |
| total=len(mesh_pairs), | |
| desc=f"Infer Epoch {epoch}", | |
| disable=not accelerator.is_local_main_process, | |
| dynamic_ncols=True, | |
| ) | |
| for line_idx, (raw_mesh_path, gt_mesh_path) in pbar: | |
| if line_idx % accelerator.num_processes != accelerator.process_index: | |
| continue | |
| try: | |
| process_one_mesh( | |
| raw_mesh_path=raw_mesh_path, | |
| gt_mesh_path=gt_mesh_path, | |
| model=model_to_load, | |
| codec=codec, | |
| codec_cfg=codec_cfg, | |
| device=device, | |
| pad_id=pad_id, | |
| config=config, | |
| output_folder=output_folder_ply, | |
| stage_resolutions=stage_resolutions, | |
| fix_holes=fix_holes, | |
| use_gt_pc=use_gt_pc, | |
| remove_degenerate=remove_degenerate, | |
| ) | |
| except Exception as e: | |
| print(f"[infer error] raw={raw_mesh_path}, gt={gt_mesh_path}: {str(e)}") | |
| torch.cuda.empty_cache() | |
| gc.collect() | |
| if accelerator.is_main_process: | |
| print("\nBatch inference complete for all checkpoints.") | |
| def main(config): | |
| print("start init Accelerator...") | |
| ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) | |
| accelerator = Accelerator( | |
| gradient_accumulation_steps=config.accelerator.gradient_accumulation_steps, | |
| mixed_precision=config.accelerator.mixed_precision, | |
| kwargs_handlers=[ddp_kwargs], | |
| ) | |
| device = accelerator.device | |
| codec = MeshCodec(args=config.tokenizer) | |
| codec_cfg = codec.get_model_config() | |
| input_num_categories = codec_cfg["input_num_categories"] # coord 词表 = resolution + 1 (含 offset) | |
| pad_id = codec_cfg["pad_token_id"] | |
| # fix3 因子分解模型(与 acc_v2_fix3 训练时一致的构造方式)。 | |
| # rotary 只需覆盖「模型位置」(9/face),用 config.model.max_seq_len(回退到 max_len)。 | |
| model_max_seq_len = int(getattr(config.model, "max_seq_len", config.data_processing.max_len)) | |
| model = FactorizedDiffusionTransformer( | |
| coord_vocab_size=input_num_categories, | |
| max_seq_len=model_max_seq_len, | |
| d_model=config.model.feature_dim, | |
| nhead=config.model.num_heads, | |
| depth=config.model.hourglass_depth, | |
| dropout=config.model.dropout, | |
| encoder_freeze=getattr(config.model, "encoder_freeze", True), | |
| use_pc=True, | |
| time_embed_scale=config.training.diffusion_train_steps, | |
| nonempty_row_weight=float(getattr(config.training, "nonempty_row_weight", 1.0)), # 与 acc_v2_fix3 构造一致(推理不算 loss, 仅对齐) | |
| ) | |
| use_ema = getattr(config.training, "use_ema", False) | |
| ema_decay = getattr(config.training, "ema_decay", 0.999) | |
| ema = EMA(model, decay=ema_decay) if use_ema else None | |
| model = accelerator.prepare(model) | |
| run_inference_on_all_checkpoints( | |
| model=model, | |
| accelerator=accelerator, | |
| config=config, | |
| device=device, | |
| pad_id=pad_id, | |
| codec=codec, | |
| codec_cfg=codec_cfg, | |
| ema=ema, | |
| ) | |
| if __name__ == "__main__": | |
| set_seed(42) | |
| config = load_config() | |
| main(config) | |