MayMeHappy commited on
Commit
e1e8545
·
verified ·
1 Parent(s): 90ca518

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. mulres_infer/.gitignore +5 -0
  2. mulres_infer/ckpt/generator.pth +3 -0
  3. mulres_infer/ckpt/readme.md +28 -0
  4. mulres_infer/ckpt/subdivider.pth +3 -0
  5. mulres_infer/configs/generator.yaml +151 -0
  6. mulres_infer/configs/subdivider.yaml +126 -0
  7. mulres_infer/dataset/__init__.py +0 -0
  8. mulres_infer/dataset/mesh_dataset.py +940 -0
  9. mulres_infer/dataset/mesh_dataset_altpc.py +94 -0
  10. mulres_infer/models/__init__.py +0 -0
  11. mulres_infer/models/generator.py +1507 -0
  12. mulres_infer/models/point_conditioner.py +200 -0
  13. mulres_infer/models/subdivider.py +1188 -0
  14. mulres_infer/modules/__init__.py +0 -0
  15. mulres_infer/modules/attention.py +376 -0
  16. mulres_infer/modules/fast_octree_level.py +52 -0
  17. mulres_infer/modules/miche/LICENSE +674 -0
  18. mulres_infer/modules/miche/__init__.py +0 -0
  19. mulres_infer/modules/miche/download_ckpt.py +25 -0
  20. mulres_infer/modules/miche/encode.py +84 -0
  21. mulres_infer/modules/miche/michelangelo/__init__.py +1 -0
  22. mulres_infer/modules/miche/michelangelo/graphics/__init__.py +1 -0
  23. mulres_infer/modules/miche/michelangelo/graphics/primitives/__init__.py +4 -0
  24. mulres_infer/modules/miche/michelangelo/graphics/primitives/volume.py +21 -0
  25. mulres_infer/modules/miche/michelangelo/models/__init__.py +1 -0
  26. mulres_infer/modules/miche/michelangelo/models/modules/__init__.py +3 -0
  27. mulres_infer/modules/miche/michelangelo/models/modules/checkpoint.py +64 -0
  28. mulres_infer/modules/miche/michelangelo/models/modules/distributions.py +83 -0
  29. mulres_infer/modules/miche/michelangelo/models/modules/embedder.py +213 -0
  30. mulres_infer/modules/miche/michelangelo/models/modules/transformer_blocks.py +309 -0
  31. mulres_infer/modules/miche/michelangelo/models/tsal/__init__.py +1 -0
  32. mulres_infer/modules/miche/michelangelo/models/tsal/asl_pl_module.py +384 -0
  33. mulres_infer/modules/miche/michelangelo/models/tsal/clip_asl_module.py +118 -0
  34. mulres_infer/modules/miche/michelangelo/models/tsal/inference_utils.py +76 -0
  35. mulres_infer/modules/miche/michelangelo/models/tsal/loss.py +130 -0
  36. mulres_infer/modules/miche/michelangelo/models/tsal/sal_perceiver.py +459 -0
  37. mulres_infer/modules/miche/michelangelo/models/tsal/tsal_base.py +125 -0
  38. mulres_infer/modules/miche/michelangelo/utils/__init__.py +3 -0
  39. mulres_infer/modules/miche/michelangelo/utils/misc.py +83 -0
  40. mulres_infer/modules/miche/shapevae-256.yaml +46 -0
  41. mulres_infer/modules/octree_codec.py +981 -0
  42. mulres_infer/modules/octree_codec_acc.py +857 -0
  43. mulres_infer/modules/octree_decode.py +625 -0
  44. mulres_infer/modules/pipeline.py +964 -0
  45. mulres_infer/modules/subdivider_runtime.py +575 -0
  46. mulres_infer/modules/token_utils.py +52 -0
  47. mulres_infer/readme.md +122 -0
  48. mulres_infer/requirements.txt +22 -0
  49. mulres_infer/scripts/__init__.py +0 -0
  50. mulres_infer/scripts/eval.py +429 -0
mulres_infer/.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ ckpt/*.pth
4
+ ckpt/*.ckpt
5
+ outputs/
mulres_infer/ckpt/generator.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:edf562427ae7c583d5185890769e24266455346650a4292ab41d00a29eddf6a5
3
+ size 3284020143
mulres_infer/ckpt/readme.md ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 权重
2
+
3
+ 推理需要两个文件,放在本目录下:
4
+
5
+ | 文件 | 说明 | 大小 |
6
+ |---|---|---|
7
+ | `generator.pth` | 低分辨率生成器(TSSR 三前向离散扩散),逐级生成到格 64 | 3.1 GB |
8
+ | `subdivider.pth` | 高分辨率细分器(acc_uni),64 → 128 → 256 → 512 | 1.2 GB |
9
+
10
+ 两者都**只含 `model_state_dict`**,已剥离优化器/调度器状态,且 EMA 影子权重已合并进权重本体
11
+ (推理的原始行为就是「加载 model_state_dict 后用 EMA 覆盖」,合并后逐位等价)。
12
+
13
+ 想自己从训练产出的 ckpt 转换:
14
+
15
+ ```bash
16
+ python scripts/strip_ckpt.py <训练ckpt.pth> ckpt/generator.pth
17
+ # 想要未经 EMA 平滑的原始权重: 加 --no-ema
18
+ ```
19
+
20
+ ## 可选
21
+
22
+ `shapevae-256.ckpt`(Michelangelo 点云编码器)**推理不需要**——点云条件编码器的权重已包含在
23
+ `generator.pth` 内,缺失时 `miche_conditioner` 会先构造未初始化的编码器再被权重覆盖,结果不受影响。
24
+ 只有从头训练才需要它。
25
+
26
+ ## 路径覆盖
27
+
28
+ 环境变量 `RAIL_CKPT` / `SUPERRES_CKPT` / `SHAPEVAE_CKPT`,或 `infer.py --ckpt / --superres-ckpt`。
mulres_infer/ckpt/subdivider.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55feaa1feaf4254f2ba9d94cc2c3def528bbb5408068a8191f636dbcd55e2bd2
3
+ size 1258077735
mulres_infer/configs/generator.yaml ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 几何 RoPE vcat 线(2026-08-05): geo 配方不变, 只把粗尺度(3/face,1/face)锚点从"面质心"
2
+ # 换成"三顶点各占1/3通道拼接"(rope_mode="geo_vcat", 用户提议) —— 共质心三角形可分、
3
+ # 顶点信息在粗尺度保留。9/face 尺度与 geo 逐位一致。从 geo ckpt_47 剥离 init 热启动。
4
+ # (token 位置 = 3D 锚点[顶点/边中点/面质心] + 面内类型, 序列顺序不参与注意力, 面片置换等变;
5
+ # 设计动机: 跨面"同顶点=同相位"取代 z-major 排序邻近这个有损协调代理, 见 model_acc_fix3_altpc.__init__)。
6
+ # [vcat_1024] 放大版(2026-08-08 用户定): 1024宽/16头/[2,4,12]/ffn2816, 骨干624M+编码器~193M≈818M。
7
+ # init = vcat ckpt_18 的 Net2Net 扩展(script/expand_vcat_768_to_1024.py: 旧权重按组拷入/新维小随机/
8
+ # 新谷底块6-11零门控恒等), strict 加载。
9
+ # [首启后] 本目录出现自己的 ckpt 后, 把 init_ckpt_path 置 null 切回 resume_latest(否则重启永远回 init)。
10
+
11
+ project_name: "gobjaverse_fix3_octant_factorized_1_to_1024_geo_vcat_1024_pc1024_sfocal_tssr"
12
+ output_folder_base: "./pretrained_model"
13
+
14
+ data:
15
+ # 2026-09-10 上集群: 与 sfocal config 同步做前缀迁移(内容同源, /root/dataset -> 仓库内 dataset_lists/,
16
+ # mesh 实体在 /data/home/from-baidu-h100-jfs/)。仍是 08-21 用户定的 focal 时代旧数据, 未换数据集。
17
+ train_dataset_path: "./dataset_lists/under_20k_concat.txt"
18
+ eval_dataset_path: "./dataset_lists/unique_files_glb_under20000face_compress_glb_with_info_val.txt"
19
+ num_workers: 12
20
+
21
+ data_processing:
22
+ overfit: false
23
+ y_up: true
24
+ n_discrete_size: 128
25
+ vertex_order: "zyx"
26
+ max_len: 600000 # focal 时代口径(60/face, 1万面上限)
27
+ min_len: 0
28
+ max_face_num: 10000 # focal 时代口径
29
+ min_face_num: 200
30
+ pc_num: 40960
31
+ total_pc_num: 50000
32
+ windowing:
33
+ window_size: -1
34
+ window_stride: 9000
35
+ split_method: "sliding_window"
36
+ train_augmentation:
37
+ shift_augment: false
38
+ scale_augment: true
39
+ dec_augment: false
40
+ rot_augment: false # focal 时代口径
41
+ sample_augment: true
42
+ val_augmentation:
43
+ shift_augment: false
44
+ scale_augment: false
45
+ dec_augment: false
46
+ rot_augment: false
47
+ sample_augment: false
48
+
49
+ tokenizer:
50
+ max_depth: 10
51
+ max_resolution: 1024
52
+ use_hourglass: true
53
+ max_children: 8
54
+ target_depth: [0,1,2,3,4,5,6] # 用户定(08-19): 砍掉深度7(格256)提速, 训到格128
55
+
56
+ model:
57
+ feature_dim: 1024
58
+ num_heads: 16 # head_dim 保持 64(flash+rope 槽位不变)
59
+ hourglass_depth: [2, 4, 12] # 谷底 6->12; 新块 6-11 由扩展 init 零门控(起步恒等)
60
+ dropout: 0.0
61
+ encoder_freeze: false
62
+ conditioned_on_pc: true
63
+ max_seq_len: 150000
64
+ # ---- 几何 RoPE ----
65
+ rope_mode: "geo_vcat" # 顶点显式锚(2026-08-05 用户提议): 粗尺度=三顶点各1/3通道拼接;
66
+ # 9/face=双端点布局(F/M_ii=(v,v)复制, M_ij=(vi,vj)拆分, 治对角线中点混叠)
67
+ rope_geo_scale: 1.0 # 体素单位(不按 depth 归一, 理由见模型注释)
68
+ rope_theta_xyz: 100.0 # 邻格 1 rad 可分辨; depth7 全程 128 格最低频 2.0 rad < π
69
+ rope_theta_type: 10.0
70
+ rope_type_slots: 2 # 32 槽 = xyz 30 + type 2; vcat 粗尺度切 3顶点x9槽+type2+pad3
71
+ rope_theta_vcat: 300.0 # 3槽/轴的频率梯子(邻格1rad可分辨, 128格最低频2.85rad<π)
72
+ ffn_hidden: 2816 # 显式覆盖(8/3*1024=2730 非 256 倍数, 取 2816 对齐 Tensor Core)
73
+ pc_num_latents: 1024 # 点云编码器 latent 1+1024(query 平铺扩容, 注入方式不变: 每层 cross-attn 双云交替)
74
+ miche_sdpa: true # miche 注意力 SDPA 化(省 ~20GB 瞬时峰, fpsq 线验证过, 数值等价)
75
+
76
+ training:
77
+ skip_resume: false
78
+ # 冷启动: 从 hg246 ckpt_39 的剥离 init(只留 model+ema)起步, epoch 从 0 计。
79
+ # 首启跑出本目录 ckpt 后改: init_ckpt_path: null
80
+ init_ckpt_path: null # ckpt_1(0.1894)已落盘(08-23), 切回 resume 语义; 冷启曾用 sfocal/tssr_init_from_ep24.pth
81
+ resume_latest: true
82
+ load_strict: false # TSSR 新头 mode_emb/to_acc fresh(零初始化尾层, warm-start 友好)
83
+ reset_epoch: false # 已切 resume 语义
84
+
85
+ epoches_num: 40
86
+ base_lr: 3.0e-5 # warm-start 大模型 + 新头 + 新目标(uniform/分类器), 小 lr 防骨干漂移; 稳定后可提
87
+ min_lr: 1.0e-6
88
+ warmup_ratio: 0.02
89
+
90
+ cond_drop_prob: 0.1
91
+ pc_target_drop_prob: 1.0 # 恒丢云B => 每层恒定注入同一朵全分辨率云A(用户最终定案: 经典focal式单云)
92
+ single_target_pc: false # 已否决(08-21二次修正): 用户要的是全分辨率云A, 不是云B
93
+
94
+ nonempty_row_weight: 1.0 # 原始 focal 的正确配置(focal γ 即类平衡, 别叠 w2; 2.0是后来的w2密度fork)
95
+ focal_gamma: 2.0 # focal loss(同 sfocal)
96
+ # ----- TSSR 三前向超参(与旧 tssr 线一致) -----
97
+ tssr_p_mask: 0.5 # 每 step 走 mask 步的概率(逐 step 跨 rank 同步)
98
+ tssr_p_selfgen: 0.1 # no_mask 步用自生成脏输入的概率(治暴露偏差)
99
+ tssr_acc_loss_weight: 1.0 # 逐 token 正确性分类器 BCE 权重
100
+ tssr_acc_pos_weight: 1.0 # BCE 中「错」类上权
101
+ tssr_mask_logits_lo: 0.2 # sample_tssr 分类器 re-mask 阈值下限
102
+ tssr_mask_logits_hi: 0.9 # 上限
103
+
104
+ use_ema: true
105
+ ema_decay: 0.999
106
+ ema_start_step: 0
107
+ ema_update_every: 1
108
+
109
+ grad_checkpoint: true # 1024x24块@90k闸 激活~2x于768线的69GB悬崖, 必开(激活省~10x/步时+30%)
110
+ save_ckpt_every: 1
111
+ save_ckpt_step_num: 3000 # 9h/epoch(9400步)且宿主机不稳: ~每1.8h一个中途档, 防整epoch丢失
112
+ batch_size: 6
113
+ diffusion_train_steps: 500
114
+ optimizer:
115
+ type: "AdamW"
116
+ weight_decay: 0.01
117
+ betas: [0.9, 0.95]
118
+ debug_fixed_mask: false
119
+ debug_fixed_t: None
120
+ debug_train_on_all_valid_tokens: false
121
+ dynamic_token_batch:
122
+ enabled: true
123
+ tokens_per_face: 9
124
+ budget_mode: "varlen"
125
+ # 2026-07-30 批点重定(零丢样档, 用户选定): 校准 ratio 后 est≈1.31x 实际, 预算 95k -> 批实际
126
+ # 位置中位 ~58k / P90 ~76k; 闸 72k 只兜长尾(全量模拟: 触闸批 2%, 丢 0.3% 样本/0.6% 位置)。
127
+ # 旧点 60k+旧ratio+55k 闸实际在静默丢 12% 样本/22% 位置(偏丢深 depth 大 mesh);
128
+ # tssr 式 340k 会丢 64% 位置 —— 预算永远要和 hard_max_positions 对齐着调, 别只动一个。
129
+ # 2026-07-30 三调(用户定 90k): 闸 72k 实测触发 ~14% 批(全在 72-82k 区间, 丢 ~1.4% 样本,
130
+ # 实际长度绕估计 ±20% 噪声推批过阈)。90k 下预算 95k(est≈1.31x实际→实际中位 ~72k)几乎不可能触闸,
131
+ # 只剩单样本巨 mesh 长尾。实测 bs14+闸72k 峰值 64.9GB, +18k 位置 ≈ +4-5GB -> ~69-70GB, 安全。
132
+ max_tokens_per_batch: 200000 # 用户定(2026-08-12): 容纳2万面样本@depth7(est~164k), 大mesh获得全深度覆盖
133
+ max_batch_size: 20
134
+ hard_max_positions: 250000 # 用户定: 预算-闸余量25%, 兜深样本组合的估计低估长尾
135
+ bucket_size: 2048
136
+ length_safety_factor: 1.05
137
+
138
+ accelerator:
139
+ gradient_accumulation_steps: 1
140
+ mixed_precision: "bf16"
141
+
142
+ evaluation:
143
+ generate_only_after_training: false
144
+ generate_val_mesh_every: 3 # 大 val mesh 逐个采样很慢, 降频防 epoch 末长尾(NCCL 超时已提1h兜底)
145
+ diffusion_sample_steps: 20
146
+ pc_num: 1024
147
+ cfg_scale: 1.5
148
+ cfg_schedule: "linear"
149
+ cfg_start: 1.0
150
+ best_of_n: 0
151
+ sample_temperature: 0.0
mulres_infer/configs/subdivider.yaml ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ project_name: "gobjaverse_5k_diffusion_pc_fix_tokenizer_1_to_1024_hourglass_fix_pc_more_data_10k_acc_uni_fix_tokenizer"
2
+ output_folder_base: "./pretrained_model"
3
+
4
+ data:
5
+ train_dataset_path: "/root/dataset/under_20k_concat.txt"
6
+ eval_dataset_path: "/root/dataset/unique_files_glb_under20000face_compress_glb_with_info_val.txt"
7
+ num_workers: 12
8
+
9
+ data_processing:
10
+ overfit: false
11
+ y_up: true
12
+ n_discrete_size: 128
13
+ vertex_order: "zyx"
14
+ max_len: 270000
15
+ min_len: 0
16
+ max_face_num: 13000
17
+ min_face_num: 200
18
+ pc_num: 40960
19
+ total_pc_num: 50000
20
+ windowing:
21
+ window_size: -1
22
+ window_stride: 9000
23
+ split_method: "sliding_window"
24
+ train_augmentation:
25
+ shift_augment: false
26
+ scale_augment: true
27
+ dec_augment: false
28
+ rot_augment: false
29
+ sample_augment: true
30
+ val_augmentation:
31
+ shift_augment: false
32
+ scale_augment: false
33
+ dec_augment: false
34
+ rot_augment: false
35
+ sample_augment: false
36
+
37
+ tokenizer:
38
+ max_depth: 10
39
+ max_resolution: 1024
40
+ use_hourglass: true
41
+ max_children: 3
42
+ target_depth: [0,1,2,3,4,5,6,7,8,9,10]
43
+
44
+ model:
45
+ codebook_path: "/root/mul-res/checkpoints/octree_codebook_meshgpt_shapenet_d5_6.npz"
46
+ use_hourglass: true
47
+ depth: 12
48
+ model_version: "v1"
49
+ feature_dim: 768
50
+ num_heads: 12
51
+ root_pred_depth: 3
52
+ context_embedding_dim: 256
53
+ num_classify: 100
54
+ hourglass_depth: [2, 2, 4]
55
+ input_embe_pos: "no"
56
+ root_pred_method: "index"
57
+ conditioned_on_pc: true
58
+ encoder_freeze: false
59
+ dropout: 0.0
60
+
61
+ training:
62
+ # ---- fine-tune knobs ----
63
+ # checkpoint_394_..._loss.pth 是 pre-CFG (496 参数, 无 null_cond / EMA, 用它做
64
+ # init_ckpt_path 会触发 optimizer param-group size mismatch).
65
+ # 真正的 refine ckpt 是下面这个 (497 参数, 有 null_cond + ema_state_dict).
66
+ init_ckpt_path: "/root/junkai_lin/mul_res_diffusion/pretrained_model/gobjaverse_5k_diffusion_pc_fix_tokenizer_1_to_1024_hourglass_fix_pc_more_data_10k_acc_uni/checkpoint_19_epoch_0.1954_loss.pth"
67
+ load_strict: true # refine ckpt 已含 null_cond, 严格加载更安全
68
+ reset_epoch: true # epoch 从 0 重新计数
69
+ skip_resume: false
70
+
71
+ # ---- LR / 时长 ----
72
+ epoches_num: 20 # 由于 trainer 循环 off-by-one (range(1, epoches_num)), 实际只跑 5 epoch
73
+ base_lr: 5.0e-5 # 比原训练 (1e-4) 低 10x, 不毁掉已收敛的权重
74
+ min_lr: 1.0e-6
75
+ warmup_ratio: 0.02
76
+
77
+ # ---- CFG cond-drop ----
78
+ cond_drop_prob: 0.1 # 10% batch 把 PC 置空, 学习 null_cond
79
+
80
+ # ---- EMA (开启 + 快 decay) ----
81
+ use_ema: true
82
+ ema_decay: 0.99 # fine-tune 步数少, 必须用快 decay
83
+ ema_start_step: 0 # 从第一步就开始累积
84
+ ema_update_every: 1
85
+
86
+ # ---- 保留的训练参数 ----
87
+ save_ckpt_every: 1
88
+ save_ckpt_step_num: 10000
89
+ batch_size: 3
90
+ diffusion_train_steps: 500
91
+ optimizer:
92
+ type: "AdamW"
93
+ weight_decay: 0.01
94
+ betas: [0.9, 0.95]
95
+ loss_weights:
96
+ root_pred_weight: 0.2
97
+ root_method: "root_move_index"
98
+ root_embed: false
99
+ debug_fixed_mask: false
100
+ debug_fixed_batch: false
101
+ debug_fixed_t: None
102
+ debug_train_on_all_valid_tokens: false
103
+ dynamic_token_batch:
104
+ enabled: true
105
+ tokens_per_face: 9
106
+ budget_mode: "varlen"
107
+ max_tokens_per_batch: 117000
108
+ max_batch_size: 6
109
+ bucket_size: 2048
110
+ length_safety_factor: 1.05
111
+
112
+ accelerator:
113
+ gradient_accumulation_steps: 4
114
+ mixed_precision: "bf16"
115
+
116
+ evaluation:
117
+ generate_only_after_training: false
118
+ debug_root: false
119
+ generate_val_mesh_every: 1
120
+ completion_input_len: 18
121
+ pc_num: 1024
122
+ use_toppk: true
123
+ top_k: 8
124
+ top_p: 0.8
125
+ temperature: 1
126
+ diffusion_sample_steps: 40
mulres_infer/dataset/__init__.py ADDED
File without changes
mulres_infer/dataset/mesh_dataset.py ADDED
@@ -0,0 +1,940 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import trimesh
2
+ import networkx as nx
3
+ import numpy as np
4
+ import os, pickle
5
+ import json
6
+ import math
7
+ import threading
8
+ import torch
9
+ from torch.utils.data import Dataset
10
+ import glob
11
+ from torch.utils.data import Sampler
12
+ from tqdm import tqdm
13
+ import numpy as np
14
+ from utils.data_utils import undiscretize, discretize, normalize, clean_mesh
15
+ import logging
16
+ from concurrent.futures import ProcessPoolExecutor, as_completed
17
+ import random
18
+ import multiprocessing as mp
19
+ from pathlib import Path
20
+ import open3d
21
+ from scipy.spatial import cKDTree
22
+ import concurrent.futures
23
+ from PIL import Image
24
+
25
+
26
+ # ----------------------------------------------------------------------------
27
+ # nvdiffrast on-the-fly normal-map renderer.
28
+ #
29
+ # Vendored from /root/junkai_lin/TRELLIS/dataset_toolkits/render_normals_gpu.py
30
+ # so the dataset stays self-contained. When `image_cond.render_on_the_fly` is
31
+ # enabled, we render the AUGMENTED mesh directly in the dataset's world frame
32
+ # (Z-up, normalized to [-1, 1]) so the projection math in
33
+ # `diffusion/img_cond_conditioner.py` lines up without needing the
34
+ # `undo_dataset_y_up` swap or the 0.5 pc_render_scale that the TRELLIS
35
+ # pre-renders required. Set those config flags accordingly when switching to
36
+ # render-on-the-fly mode (see img_cond yaml).
37
+ #
38
+ # Render failures (e.g. degenerate mesh, CUDA OOM, zero-face mesh) raise; the
39
+ # `except Exception` loop in __getitem__ then resamples a different mesh.
40
+ # ----------------------------------------------------------------------------
41
+
42
+ _NVDR_CTX = None
43
+ _NVDR_LOCK = threading.Lock()
44
+
45
+
46
+ def _get_nvdr_ctx():
47
+ """Lazily create one nvdiffrast CUDA context per process.
48
+
49
+ DataLoader workers run as separate processes; each one initializes its own
50
+ context the first time __getitem__ touches the renderer. The default
51
+ `fork` start method may conflict with CUDA already initialized in the
52
+ parent — if you hit `cuda: initialization error` here, switch the
53
+ DataLoader to `multiprocessing_context='spawn'`.
54
+ """
55
+ global _NVDR_CTX
56
+ if _NVDR_CTX is not None:
57
+ return _NVDR_CTX
58
+ with _NVDR_LOCK:
59
+ if _NVDR_CTX is None:
60
+ import nvdiffrast.torch as dr
61
+ if not torch.cuda.is_available():
62
+ raise RuntimeError(
63
+ "image_cond.render_on_the_fly=true requires CUDA; no GPU visible to this worker"
64
+ )
65
+ device = torch.device("cuda", 0)
66
+ _NVDR_CTX = {"glctx": dr.RasterizeCudaContext(device=device), "device": device}
67
+ return _NVDR_CTX
68
+
69
+
70
+ def _build_camera_for_render(yaw, pitch, radius, fov, aspect=1.0, near=0.01, far=100.0):
71
+ """Z-up world, origin-tracking camera. yaw/pitch/fov in radians.
72
+
73
+ Returns (view[4,4], proj[4,4], cam_pos[3], R_c2w[3,3]) as float64 numpy.
74
+ Matches `render_normals_gpu.build_camera` exactly so the pose convention
75
+ is consistent with the TRELLIS pre-render path.
76
+ """
77
+ cx = radius * math.cos(yaw) * math.cos(pitch)
78
+ cy = radius * math.sin(yaw) * math.cos(pitch)
79
+ cz = radius * math.sin(pitch)
80
+ cam_pos = np.array([cx, cy, cz], dtype=np.float64)
81
+
82
+ forward = -cam_pos / np.linalg.norm(cam_pos)
83
+ world_up = np.array([0.0, 0.0, 1.0])
84
+ right = np.cross(forward, world_up)
85
+ rn = np.linalg.norm(right)
86
+ if rn < 1e-9:
87
+ world_up = np.array([0.0, 1.0, 0.0])
88
+ right = np.cross(forward, world_up)
89
+ rn = np.linalg.norm(right)
90
+ right /= rn
91
+ up = np.cross(right, forward)
92
+ R_c2w = np.stack([right, up, -forward], axis=1)
93
+ R_w2c = R_c2w.T
94
+ t_w2c = -R_w2c @ cam_pos
95
+
96
+ view = np.eye(4, dtype=np.float64)
97
+ view[:3, :3] = R_w2c
98
+ view[:3, 3] = t_w2c
99
+
100
+ f = 1.0 / math.tan(fov / 2.0)
101
+ proj = np.zeros((4, 4), dtype=np.float64)
102
+ proj[0, 0] = f / aspect
103
+ # nvdiffrast image-space y is flipped vs OpenGL; negate so row 0 = top.
104
+ proj[1, 1] = -f
105
+ proj[2, 2] = -(far + near) / (far - near)
106
+ proj[2, 3] = -2.0 * far * near / (far - near)
107
+ proj[3, 2] = -1.0
108
+ return view, proj, cam_pos, R_c2w
109
+
110
+
111
+ def _explode_to_flat_normals(verts, faces):
112
+ """Per-face geometric normals via mesh explosion (flat shading)."""
113
+ F = faces.shape[0]
114
+ v0 = verts[faces[:, 0]]
115
+ v1 = verts[faces[:, 1]]
116
+ v2 = verts[faces[:, 2]]
117
+ fn = np.cross(v1 - v0, v2 - v0)
118
+ fn_len = np.linalg.norm(fn, axis=1, keepdims=True)
119
+ fn = fn / np.where(fn_len < 1e-12, 1.0, fn_len)
120
+ new_verts = np.empty((F * 3, 3), dtype=verts.dtype)
121
+ new_verts[0::3] = v0
122
+ new_verts[1::3] = v1
123
+ new_verts[2::3] = v2
124
+ new_normals = np.repeat(fn, 3, axis=0).astype(verts.dtype)
125
+ new_faces = np.arange(F * 3, dtype=np.int32).reshape(F, 3)
126
+ return new_verts, new_faces, new_normals
127
+
128
+
129
+ def _fit_radius_to_silhouette(verts_t, yaw, pitch, fov, init_radius, target_fill=0.9, iters=3):
130
+ """Shrink/grow camera radius so the mesh's max |NDC| equals target_fill."""
131
+ radius = float(init_radius)
132
+ ones = torch.ones((verts_t.shape[0], 1), device=verts_t.device, dtype=verts_t.dtype)
133
+ v_h = torch.cat([verts_t, ones], dim=1)
134
+ for _ in range(iters):
135
+ view, proj, _, _ = _build_camera_for_render(yaw, pitch, radius, fov)
136
+ mvp = (proj @ view).astype(np.float32)
137
+ mvp_t = torch.from_numpy(mvp).to(verts_t.device)
138
+ v_clip = v_h @ mvp_t.T
139
+ front = v_clip[:, 3] > 1e-3
140
+ if not front.any():
141
+ break
142
+ c = v_clip[front]
143
+ ndc_xy = c[:, :2] / c[:, 3:4].clamp_min(1e-6)
144
+ max_ndc = float(ndc_xy.abs().max().item())
145
+ if max_ndc < 1e-6:
146
+ break
147
+ radius *= max_ndc / target_fill
148
+ radius = max(0.5, min(radius, 50.0))
149
+ return radius
150
+
151
+
152
+ def _render_normals_image(
153
+ verts_np,
154
+ faces_np,
155
+ yaw_deg=315.0,
156
+ pitch_deg=30.0,
157
+ fov_deg=40.0,
158
+ init_radius=4.0,
159
+ resolution=518,
160
+ auto_radius=True,
161
+ target_fill=0.9,
162
+ ):
163
+ """Render a normal-visualization image of `(verts, faces)` in the mesh's
164
+ own world frame (assumed Z-up).
165
+
166
+ Returns
167
+ -------
168
+ image_tensor : torch.float16, [3, H, W], values in [0, 1] (white background)
169
+ transform_matrix : torch.float32, [4, 4] (camera-to-world, NeRF convention)
170
+ camera_angle_x : torch.float32, scalar (horizontal FOV in radians)
171
+ """
172
+ import nvdiffrast.torch as dr
173
+
174
+ if faces_np is None or faces_np.shape[0] == 0:
175
+ raise ValueError("render: mesh has zero faces")
176
+
177
+ ctx = _get_nvdr_ctx()
178
+ device = ctx["device"]
179
+ glctx = ctx["glctx"]
180
+
181
+ yaw = math.radians(yaw_deg)
182
+ pitch = math.radians(pitch_deg)
183
+ fov = math.radians(fov_deg)
184
+
185
+ verts_np = np.ascontiguousarray(verts_np, dtype=np.float32)
186
+ faces_np = np.ascontiguousarray(faces_np, dtype=np.int32)
187
+ verts_np, faces_np, normals_np = _explode_to_flat_normals(verts_np, faces_np)
188
+
189
+ verts_t = torch.from_numpy(verts_np).to(device)
190
+ faces_t = torch.from_numpy(faces_np).to(device).to(torch.int32)
191
+ normals_t = torch.from_numpy(normals_np).to(device)
192
+ ones = torch.ones((verts_t.shape[0], 1), device=device, dtype=verts_t.dtype)
193
+ verts_h = torch.cat([verts_t, ones], dim=1)
194
+
195
+ radius = (
196
+ _fit_radius_to_silhouette(verts_t, yaw, pitch, fov, init_radius, target_fill)
197
+ if auto_radius
198
+ else float(init_radius)
199
+ )
200
+
201
+ view_M, proj_M, cam_pos, R_c2w = _build_camera_for_render(yaw, pitch, radius, fov)
202
+ mvp = (proj_M @ view_M).astype(np.float32)
203
+ mvp_t = torch.from_numpy(mvp).to(device)
204
+
205
+ clip = (verts_h @ mvp_t.T).unsqueeze(0).contiguous() # (1, V, 4)
206
+ rast, _ = dr.rasterize(glctx, clip, faces_t, resolution=(resolution, resolution))
207
+ alpha = (rast[0, ..., 3] > 0).float() # (H, W)
208
+ if float(alpha.sum().item()) < 4.0:
209
+ raise ValueError("render: empty silhouette (mesh out of frustum?)")
210
+
211
+ normal_img, _ = dr.interpolate(normals_t.unsqueeze(0), rast, faces_t)
212
+ normal_img = normal_img[0] # (H, W, 3)
213
+ nlen = torch.linalg.norm(normal_img, dim=-1, keepdim=True).clamp_min(1e-8)
214
+ normal_img = normal_img / nlen
215
+ normal_img = normal_img * alpha.unsqueeze(-1)
216
+
217
+ # Visualize normals as RGB: (N * 0.5 + 0.5), white background.
218
+ vis = (normal_img * 0.5 + 0.5).clamp(0.0, 1.0)
219
+ a = alpha.unsqueeze(-1)
220
+ vis = vis * a + (1.0 - a) # background = white
221
+ image_tensor = vis.permute(2, 0, 1).contiguous().to(dtype=torch.float16).cpu()
222
+
223
+ transform_matrix = torch.eye(4, dtype=torch.float32)
224
+ transform_matrix[:3, :3] = torch.from_numpy(R_c2w).float()
225
+ transform_matrix[:3, 3] = torch.from_numpy(cam_pos).float()
226
+ camera_angle_x = torch.tensor(float(fov), dtype=torch.float32)
227
+ return image_tensor, transform_matrix, camera_angle_x
228
+
229
+
230
+ def _read_image_cond_cfg(opt):
231
+ """Return (enabled, render_root, image_resolution, render_on_the_fly,
232
+ render_cfg, render_manifest_path).
233
+
234
+ `render_on_the_fly=True` enables the nvdiffrast path. In that mode
235
+ `render_root` is unused (no disk-render filter), but it may still be set
236
+ in the yaml for backward compatibility.
237
+
238
+ `render_manifest_path` is the path to a precomputed list of valid render
239
+ subdir stems (one per line). When set and the file exists, the dataset
240
+ skips the slow per-line `os.path.exists` and uses O(1) set membership.
241
+ Build it with `script/build_render_manifest.py`.
242
+ """
243
+ cfg = getattr(opt, "image_cond", None)
244
+ if cfg is None or not getattr(cfg, "enabled", False):
245
+ return False, None, None, False, None, None
246
+ image_resolution = int(getattr(cfg, "image_resolution", 512))
247
+ render_on_the_fly = bool(getattr(cfg, "render_on_the_fly", False))
248
+ render_root = getattr(cfg, "render_root", None)
249
+ if not render_on_the_fly and render_root is None:
250
+ raise ValueError(
251
+ "image_cond.enabled=true but image_cond.render_root is not set "
252
+ "(and render_on_the_fly is false)"
253
+ )
254
+ render_cfg = getattr(cfg, "render", None)
255
+ render_manifest_path = getattr(cfg, "render_manifest_path", None)
256
+ return True, render_root, image_resolution, render_on_the_fly, render_cfg, render_manifest_path
257
+
258
+
259
+ def _render_dir_for(file_path: str, render_root: str) -> str:
260
+ return os.path.join(render_root, Path(file_path).stem)
261
+
262
+
263
+ def _has_render(file_path: str, render_root: str) -> bool:
264
+ d = _render_dir_for(file_path, render_root)
265
+ return os.path.exists(os.path.join(d, "transforms.json")) and os.path.exists(
266
+ os.path.join(d, "000.png")
267
+ )
268
+
269
+
270
+ # Per-path LRU cache so the manifest is read once per process.
271
+ _RENDER_MANIFEST_CACHE: dict = {}
272
+
273
+
274
+ def _load_render_manifest(manifest_path):
275
+ """Load a manifest file into a set of valid render stems.
276
+
277
+ Returns None when `manifest_path` is None/empty or the file doesn't
278
+ exist, so the caller falls back to per-line `os.path.exists`. Build the
279
+ manifest with `script/build_render_manifest.py` whenever the render
280
+ contents change — it drops startup time from O(N stat) to O(N lookup).
281
+ """
282
+ if not manifest_path:
283
+ return None
284
+ if manifest_path in _RENDER_MANIFEST_CACHE:
285
+ return _RENDER_MANIFEST_CACHE[manifest_path]
286
+ if not os.path.isfile(manifest_path):
287
+ _RENDER_MANIFEST_CACHE[manifest_path] = None
288
+ return None
289
+ with open(manifest_path, "r") as f:
290
+ stems = {line.strip() for line in f if line.strip()}
291
+ _RENDER_MANIFEST_CACHE[manifest_path] = stems
292
+ return stems
293
+
294
+
295
+ # TRELLIS Blender depth encoding (see dataset_toolkits/blender_script/render.py):
296
+ # pixel_val (uint16) = ((depth_world - depth.min) / (depth.max - depth.min)) * 65535
297
+ # Background ends up clamped to 65535 (== depth.max output). We treat 65535 as
298
+ # "no surface" so points landing on BG pixels are considered occluded (not
299
+ # visible) in that view.
300
+ _DEPTH_BG_VALUE = 65535
301
+
302
+
303
+ def _load_depth_map(depth_png_path: str, depth_min: float, depth_max: float,
304
+ target_resolution: int):
305
+ """Decode TRELLIS depth PNG to world-space depth.
306
+
307
+ Returns (depth[H,W] float32, valid_mask[H,W] bool).
308
+ `valid_mask` is False on background pixels (no surface hit).
309
+ """
310
+ arr = np.array(Image.open(depth_png_path))
311
+ if arr.dtype != np.uint16:
312
+ # 8-bit fallback — still treat the brightest value as background.
313
+ max_val = np.iinfo(arr.dtype).max
314
+ bg_val = max_val
315
+ norm = arr.astype(np.float32) / float(max_val)
316
+ else:
317
+ bg_val = _DEPTH_BG_VALUE
318
+ norm = arr.astype(np.float32) / 65535.0
319
+ valid = arr != bg_val
320
+ depth = depth_min + norm * (depth_max - depth_min)
321
+ if (arr.shape[0], arr.shape[1]) != (target_resolution, target_resolution):
322
+ # Resize via PIL (nearest) so depth values aren't blended across the
323
+ # silhouette — bilinear would corrupt the foreground/background edge.
324
+ depth_img = Image.fromarray(depth.astype(np.float32), mode="F").resize(
325
+ (target_resolution, target_resolution), Image.NEAREST
326
+ )
327
+ valid_img = Image.fromarray(valid.astype(np.uint8) * 255, mode="L").resize(
328
+ (target_resolution, target_resolution), Image.NEAREST
329
+ )
330
+ depth = np.asarray(depth_img, dtype=np.float32)
331
+ valid = np.asarray(valid_img, dtype=np.uint8) > 0
332
+ return depth, valid
333
+
334
+
335
+ def _load_image_and_camera(file_path: str, render_root: str, image_resolution: int):
336
+ """Load BOTH rendered views + depth maps for back-projection.
337
+
338
+ Returns:
339
+ image_tensor : [V, 3, H, W] float16 in [0, 1] (white bg)
340
+ transform_matrix : [V, 4, 4] float32 (camera-to-world)
341
+ camera_angle_x : [V] float32 (horizontal FOV, rad)
342
+ depth_map : [V, H, W] float32 (world-space depth)
343
+ depth_valid : [V, H, W] bool (False on background)
344
+
345
+ Iterates every frame listed in transforms.json (the dataset stores two
346
+ views: 000 and 001). Missing depth PNGs raise — the caller will catch and
347
+ resample a different mesh.
348
+ """
349
+ d = _render_dir_for(file_path, render_root)
350
+ with open(os.path.join(d, "transforms.json"), "r") as f:
351
+ meta = json.load(f)
352
+
353
+ images = []
354
+ transforms = []
355
+ fovs = []
356
+ depths = []
357
+ valids = []
358
+ for frame in meta["frames"]:
359
+ rgb_path = os.path.join(d, frame["file_path"])
360
+ img = Image.open(rgb_path)
361
+ if img.mode == "RGBA":
362
+ bg = Image.new("RGB", img.size, (255, 255, 255))
363
+ bg.paste(img, mask=img.split()[-1])
364
+ img = bg
365
+ else:
366
+ img = img.convert("RGB")
367
+ if img.size != (image_resolution, image_resolution):
368
+ img = img.resize((image_resolution, image_resolution), Image.LANCZOS)
369
+ arr = np.asarray(img, dtype=np.float32) / 255.0
370
+ images.append(torch.from_numpy(arr).permute(2, 0, 1).to(dtype=torch.float16))
371
+
372
+ transforms.append(
373
+ torch.tensor(frame["transform_matrix"], dtype=torch.float32)
374
+ )
375
+ fovs.append(torch.tensor(float(frame["camera_angle_x"]), dtype=torch.float32))
376
+
377
+ depth_info = frame.get("depth")
378
+ if depth_info is None:
379
+ raise ValueError(
380
+ f"transforms.json frame {frame.get('file_path')} has no 'depth' entry; "
381
+ "back-projection requires depth bounds."
382
+ )
383
+ stem = os.path.splitext(frame["file_path"])[0]
384
+ depth_path = os.path.join(d, f"{stem}_depth.png")
385
+ depth_np, valid_np = _load_depth_map(
386
+ depth_path,
387
+ float(depth_info["min"]),
388
+ float(depth_info["max"]),
389
+ image_resolution,
390
+ )
391
+ depths.append(torch.from_numpy(depth_np))
392
+ valids.append(torch.from_numpy(valid_np))
393
+
394
+ image_tensor = torch.stack(images, dim=0) # [V, 3, H, W]
395
+ transform_matrix = torch.stack(transforms, dim=0) # [V, 4, 4]
396
+ camera_angle_x = torch.stack(fovs, dim=0) # [V]
397
+ depth_map = torch.stack(depths, dim=0) # [V, H, W]
398
+ depth_valid = torch.stack(valids, dim=0) # [V, H, W]
399
+ return image_tensor, transform_matrix, camera_angle_x, depth_map, depth_valid
400
+ def shift_vertices(vertices, x_lims=(-0.1, 0.1), y_lims=(-0.1, 0.1), z_lims=(-0.075, 0.075)):
401
+ # shift x, y, z
402
+ x = np.random.uniform(low=x_lims[0], high=x_lims[1], size=(1,))
403
+ y = np.random.uniform(low=y_lims[0], high=y_lims[1], size=(1,))
404
+ z = np.random.uniform(low=z_lims[0], high=z_lims[1], size=(1,))
405
+ x = max(min(x, 0.5 - vertices[:, 0].max()), -0.5 - vertices[:, 0].min())
406
+ y = max(min(y, 0.5 - vertices[:, 1].max()), -0.5 - vertices[:, 1].min())
407
+ z = max(min(z, 0.5 - vertices[:, 2].max()), -0.5 - vertices[:, 2].min())
408
+ vertices = np.stack([vertices[:, 0] + x, vertices[:, 1] + y, vertices[:, 2] + z], axis=-1)
409
+ return vertices
410
+
411
+ def scale_vertices(vertices, x_lims=(0.75, 1.25), y_lims=(0.75, 1.25), z_lims=(0.75, 1.25)):
412
+ # scale x, y, z
413
+ x = np.random.uniform(low=x_lims[0], high=x_lims[1], size=(1,))
414
+ y = np.random.uniform(low=y_lims[0], high=y_lims[1], size=(1,))
415
+ z = np.random.uniform(low=z_lims[0], high=z_lims[1], size=(1,))
416
+ vertices = np.stack([vertices[:, 0] * x, vertices[:, 1] * y, vertices[:, 2] * z], axis=-1)
417
+ return vertices
418
+
419
+ def normalize_vertices(vertices, eps: float = 1e-8):
420
+ bounds = np.array([vertices.min(axis=0), vertices.max(axis=0)]) # type: ignore
421
+ vertices = vertices - (bounds[0] + bounds[1])[None, :] / 2
422
+ # 数据中存在退化 mesh (bounds 范围为 0), 分母加 eps 避免除零产生 inf/NaN
423
+ scale = (bounds[1] - bounds[0]).max() + eps
424
+ vertices = vertices / scale
425
+ return vertices
426
+
427
+ def agu_mesh(vertices: np.ndarray, shift_augment, scale_augment):
428
+ # 随机轴缩放
429
+ if scale_augment:
430
+ # lower, upper = scale_factor
431
+ # scale_axis = lower + np.random.rand(vertices.shape[-1]) * (upper - lower)
432
+ # vertices = vertices * scale_axis
433
+ vertices = scale_vertices(vertices)
434
+ vertices = normalize_vertices(vertices)
435
+ if shift_augment:
436
+ vertices = shift_vertices(vertices)
437
+ return vertices
438
+
439
+
440
+
441
+ def dec(mesh, target_face):
442
+ open3d_mesh = open3d.geometry.TriangleMesh(
443
+ vertices=open3d.utility.Vector3dVector(mesh.vertices),
444
+ triangles=open3d.utility.Vector3iVector(mesh.faces))
445
+ simple = open3d_mesh.simplify_quadric_decimation(target_face)
446
+ return trimesh.Trimesh(vertices=simple.vertices, faces=simple.triangles)
447
+
448
+ def normalize(mesh, eps: float = 1e-8):
449
+ bounds = np.array([mesh.vertices.min(axis=0), mesh.vertices.max(axis=0)])
450
+ mesh.vertices = mesh.vertices - (bounds[0] + bounds[1])[None, :] / 2
451
+ # 数据中存在退化 mesh (bounds 范围为 0), 分母加 eps 避免除零产生 inf/NaN
452
+ scale = (bounds[1] - bounds[0]).max() + eps
453
+ mesh.vertices = 2 * (mesh.vertices / scale)
454
+ mesh.vertices = mesh.vertices.clip(-1.0, 1.0) # 限制顶点坐标在[-1.0, 1.0]
455
+
456
+ def merge_duplicate_vertices(mesh, tolerance=0):
457
+ """更可靠的顶点合并方法"""
458
+ vertices = mesh.vertices
459
+ faces = mesh.faces
460
+
461
+ # 使用KD树查找相近的顶点
462
+ tree = cKDTree(vertices)
463
+
464
+ # 查找在容差范围内的顶点对
465
+ pairs = tree.query_pairs(tolerance, output_type='ndarray')
466
+
467
+ if len(pairs) == 0:
468
+ return mesh # 没有需要合并的顶点
469
+
470
+ # 创建顶点映射:将重复顶点映射到第一个顶点
471
+ vertex_map = {i: i for i in range(len(vertices))}
472
+
473
+ for i, j in pairs:
474
+ # 总是映射到较小的索引
475
+ if i < j:
476
+ vertex_map[j] = i
477
+ else:
478
+ vertex_map[i] = j
479
+
480
+ # 应用顶点映射
481
+ new_faces = []
482
+ for face in faces:
483
+ new_face = [vertex_map[v] for v in face]
484
+ new_faces.append(new_face)
485
+
486
+ # 创建新的网格
487
+ new_mesh = trimesh.Trimesh(vertices=vertices, faces=new_faces)
488
+
489
+ # 移除未引用的顶点
490
+ new_mesh.remove_unreferenced_vertices()
491
+
492
+ return new_mesh
493
+
494
+ def discrete_undiscrete_and_clean(mesh, n_discrete, clean=True):
495
+ vertices = discretize(mesh.vertices, num_discrete=n_discrete)
496
+ vertices = undiscretize(vertices, num_discrete=n_discrete)
497
+ dis_mesh = trimesh.Trimesh(vertices=vertices, faces=mesh.faces)
498
+ new_mesh = merge_duplicate_vertices(dis_mesh)
499
+ new_mesh.merge_vertices()
500
+ new_mesh.update_faces(new_mesh.nondegenerate_faces())
501
+ new_mesh.update_faces(new_mesh.unique_faces())
502
+ new_mesh.remove_unreferenced_vertices()
503
+ new_mesh.fix_normals()
504
+ return new_mesh
505
+
506
+ # 点云加噪开关(环境变量,默认关=旧加噪;DataLoader worker 会继承 env)。
507
+ # PAPER_PC_NOISE=1 开启论文式加噪: 位置 σ_pos "up to" PC_NOISE_POS(逐样本采强度) + 法向 σ_normal=PC_NOISE_NORMAL
508
+ # (加噪后重归一化)。默认关 -> 走旧加噪(仅位置 +N(0,0.01)、50% 概率, 法向不加噪),其它训练零影响。
509
+ # 幅度也可用 env PC_NOISE_POS / PC_NOISE_NORMAL 覆盖(默认 0.1 / 0.2)。只在 aug=True(训练)时生效,推理不加噪。
510
+ def _paper_pc_noise_cfg():
511
+ return (os.environ.get("PAPER_PC_NOISE", "0") == "1",
512
+ float(os.environ.get("PC_NOISE_POS", "0.1")),
513
+ float(os.environ.get("PC_NOISE_NORMAL", "0.2")))
514
+
515
+
516
+ def sample_pc(mesh, pc_num, total_pc_num = 50000, with_normal=True, aug=True, overfit=False):
517
+ if overfit:
518
+ np.random.seed(42)
519
+ if not with_normal:
520
+ points, _ = mesh.sample(pc_num, return_index=True)
521
+ return points
522
+
523
+ points, face_idx = mesh.sample(total_pc_num, return_index=True)
524
+ normals = mesh.face_normals[face_idx]
525
+ paper_noise, sigma_pos_max, sigma_normal = _paper_pc_noise_cfg()
526
+ if paper_noise:
527
+ # 论文式加噪: 位置 σ_pos<=sigma_pos_max + 法向 σ_normal(重归一化为单位向量)
528
+ if aug:
529
+ points = points.astype(np.float32)
530
+ normals = normals.astype(np.float32)
531
+ sigma_pos = np.random.uniform(0.0, sigma_pos_max) # "up to" -> 逐样本采噪声强度
532
+ points = points + np.random.randn(*points.shape).astype(np.float32) * sigma_pos
533
+ normals = normals + np.random.randn(*normals.shape).astype(np.float32) * sigma_normal
534
+ nlen = np.linalg.norm(normals, axis=-1, keepdims=True)
535
+ normals = normals / np.clip(nlen, 1e-8, None) # 加噪后法向重归一化为单位向量
536
+ else:
537
+ # 旧加噪(默认): 仅位置 +N(0,0.01)、50% 概率, 法向不加噪(保持既有训练行为不变)
538
+ if aug and random.random() < 0.5:
539
+ points = points + np.random.randn(*points.shape) * 0.01
540
+ pc_normal = np.concatenate([points, normals], axis=-1, dtype=np.float16)
541
+
542
+ # random sample point cloud
543
+ ind = np.random.choice(pc_normal.shape[0], pc_num, replace=False)
544
+ pc_normal = pc_normal[ind]
545
+
546
+ return pc_normal
547
+
548
+ def mul_encode_mesh(raw_mesh, gt_mesh, codec, tokenizer_target_depth, pc_num, total_pc_num, use_gt_pc, clean=True,
549
+ normalize_input=True):
550
+ mesh = raw_mesh
551
+
552
+ # 归一化。normalize_input=False: 跳过对 raw_mesh 的归一化 —— 级联推理用(2026-08-22 修复):
553
+ # 级间 mesh 是上一级 tokenizer 解码产物, 天生在规范 [-1,1] 格坐标系; 按它自身 bbox 再归一化会在
554
+ # 预测带极值杂散面时把脚手架帧拉离 GT/点云帧(z 轴撕裂 bug, 见 infer_multilevel CASCADE_RENORM)。
555
+ # 训练路径 raw_mesh==gt_mesh(磁盘任意坐标系), 保持默认 True 不变。
556
+ if normalize_input:
557
+ normalize(mesh)
558
+ normalize(gt_mesh)
559
+
560
+ if isinstance(tokenizer_target_depth, list):
561
+ selected_depth = random.choice(tokenizer_target_depth)
562
+ else:
563
+ selected_depth = tokenizer_target_depth
564
+ raw_dis_mesh = discrete_undiscrete_and_clean(gt_mesh, n_discrete=2**selected_depth, clean=clean)
565
+ gt_dis_mesh = discrete_undiscrete_and_clean(gt_mesh, n_discrete=2**(selected_depth+1), clean=clean)
566
+ # 采样点云
567
+ if use_gt_pc:
568
+ pc = sample_pc(gt_mesh, pc_num=pc_num, total_pc_num=total_pc_num,
569
+ with_normal=True, aug=False, overfit=False)
570
+ else:
571
+ pc = sample_pc(mesh, pc_num=pc_num, total_pc_num=total_pc_num,
572
+ with_normal=True, aug=False, overfit=False)
573
+ # raw_token, token = self.codec.encode_mesh(mesh, target_depth=self.tokenizer_target_depth)
574
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
575
+
576
+ future = executor.submit(codec.encode_mesh, mesh, target_depth=selected_depth)
577
+ # 如果超过 2.0 秒没算完,这里会直接抛出 concurrent.futures.TimeoutError
578
+ raw_token, token, _ = future.result(timeout=20.0)
579
+
580
+ n = len(token)
581
+
582
+ data_dict = {}
583
+ data_dict["token"] = torch.tensor(token, dtype=torch.int64)
584
+ data_dict["raw_token"] = torch.tensor(raw_token, dtype=torch.int64)
585
+ data_dict["raw_vertices"] = mesh.vertices
586
+ data_dict["raw_faces"] = mesh.faces
587
+ data_dict["raw_dis_mesh"] = raw_dis_mesh
588
+ data_dict["gt_dis_mesh"] = gt_dis_mesh
589
+
590
+ data_dict["start"] = 0
591
+ data_dict["end"] = len(data_dict["token"])
592
+ data_dict["pc"] = torch.from_numpy(pc)
593
+ data_dict["token_len"] = torch.tensor([len(data_dict["token"])], dtype=torch.int64)
594
+
595
+ data_dict["selected_depth"] = selected_depth
596
+ return data_dict
597
+
598
+ class MeshDataset(Dataset):
599
+ """
600
+ 重构后的版本,初始化参数通过 opt 对象传入
601
+ """
602
+ def __init__(self, opt, data, codec, all_token_len, is_train):
603
+ # 将配置对象和核心数据存储到实例中
604
+ self.opt = opt
605
+ self.data = data
606
+ self.codec = codec
607
+ self.all_token_len = all_token_len
608
+ self.is_train = is_train
609
+ # 从 opt 对象中解包参数,保持原有代码逻辑不变
610
+ # 这样做的好处是 __getitem__ 等其他方法不需要修改
611
+ if is_train:
612
+ self.shift_augment = self.opt.data_processing.train_augmentation.shift_augment
613
+ self.scale_augment = self.opt.data_processing.train_augmentation.scale_augment
614
+ self.dec_augment = self.opt.data_processing.train_augmentation.dec_augment
615
+ self.rot_augment = self.opt.data_processing.train_augmentation.rot_augment
616
+ self.sample_augment = self.opt.data_processing.train_augmentation.sample_augment
617
+ else:
618
+ self.shift_augment = self.opt.data_processing.val_augmentation.shift_augment
619
+ self.scale_augment = self.opt.data_processing.val_augmentation.scale_augment
620
+ self.dec_augment = self.opt.data_processing.val_augmentation.dec_augment
621
+ self.rot_augment = self.opt.data_processing.val_augmentation.rot_augment
622
+ self.sample_augment = self.opt.data_processing.val_augmentation.sample_augment
623
+ self.n_discrete_size = self.opt.data_processing.n_discrete_size
624
+ self.vertex_order = self.opt.data_processing.vertex_order
625
+ self.window_size = self.opt.data_processing.windowing.window_size
626
+ self.window_stride = self.opt.data_processing.windowing.window_stride
627
+ self.split_method = self.opt.data_processing.windowing.split_method
628
+ self.max_length = self.opt.data_processing.max_len-20 # 假设max_length也在opt中
629
+ self.min_length = self.opt.data_processing.min_len # 假设max_length也在opt中
630
+ self.pc_num = self.opt.data_processing.pc_num
631
+ self.total_pc_num = self.opt.data_processing.total_pc_num
632
+ self.y_up = self.opt.data_processing.y_up
633
+ self.overfit = self.opt.data_processing.overfit if hasattr(self.opt.data_processing, 'overfit') else False
634
+ self.tokenizer_target_depth = self.opt.tokenizer.target_depth
635
+ # 2a 暴露偏差训练腐蚀(仅训练): 粗输入加伪面(过连接)+节点坐标抖动(位移),目标查 GT 不变。
636
+ # 仅 FastMeshCodec.encode_mesh 实现; 推理/eval 传 None -> 与原版逐位一致。
637
+ self.corrupt_cfg = None
638
+ _ct = getattr(getattr(self.opt, "training", None), "corrupt", None)
639
+ if is_train and _ct is not None and bool(getattr(_ct, "enabled", False)):
640
+ self.corrupt_cfg = {
641
+ "spurious_rate": float(getattr(_ct, "spurious_rate", 0.0)),
642
+ "spatial_radius": int(getattr(_ct, "spatial_radius", 2)),
643
+ "jitter_prob": float(getattr(_ct, "jitter_prob", 0.0)),
644
+ "jitter_max": int(getattr(_ct, "jitter_max", 1)),
645
+ }
646
+ print(f" [2a Corrupt] ENABLED (is_train): {self.corrupt_cfg}")
647
+ (
648
+ self.image_cond_enabled,
649
+ self.image_cond_render_root,
650
+ self.image_cond_resolution,
651
+ self.image_cond_render_on_the_fly,
652
+ self.image_cond_render_cfg,
653
+ self.image_cond_render_manifest_path,
654
+ ) = _read_image_cond_cfg(opt)
655
+ # Default render parameters; can be overridden by `image_cond.render` in yaml.
656
+ # Defaults match render_normals_gpu.sh (yaw=315, pitch=30, FOV=40, auto_radius)
657
+ # but with init_radius=4.0 because our mesh frame is [-1, 1] (extent ~2) vs.
658
+ # the TRELLIS pre-render frame [-0.5, 0.5] (extent ~1).
659
+ rcfg = self.image_cond_render_cfg
660
+ self.render_yaw_deg = float(getattr(rcfg, "yaw_deg", 315.0)) if rcfg is not None else 315.0
661
+ self.render_pitch_deg = float(getattr(rcfg, "pitch_deg", 30.0)) if rcfg is not None else 30.0
662
+ self.render_fov_deg = float(getattr(rcfg, "fov_deg", 40.0)) if rcfg is not None else 40.0
663
+ self.render_init_radius = float(getattr(rcfg, "init_radius", 4.0)) if rcfg is not None else 4.0
664
+ self.render_auto_radius = bool(getattr(rcfg, "auto_radius", True)) if rcfg is not None else True
665
+ self.render_target_fill = float(getattr(rcfg, "target_fill", 0.9)) if rcfg is not None else 0.9
666
+ # If true (and is_train), randomly sample yaw in [0, 360) per call.
667
+ self.render_randomize_yaw = bool(getattr(rcfg, "randomize_yaw", False)) if rcfg is not None else False
668
+ print("\n" + "="*70)
669
+ print("="*70)
670
+ # 使用 f-string 格式化输出,将相关的配置项打印在同一行
671
+ print(f" [Augmentation] Shift: {self.shift_augment}, Scale: {self.scale_augment}, Decimation: {self.dec_augment}, Rotation: {self.rot_augment}, Sample: {self.sample_augment}")
672
+ print(f" [Data Specs] Discretization Size: {self.n_discrete_size}, Vertex Order: '{self.vertex_order}'")
673
+ print(f" [Windowing] Size: {self.window_size}, Stride: {self.window_stride}, Method: '{self.split_method}'")
674
+ print(f" [Sequence Len] Min: {self.min_length}, Max: {self.max_length}")
675
+ print(f" [Point Cloud] PC Num: {self.pc_num}, Total PC Num: {self.total_pc_num}")
676
+ print(f" [Tokenizer] Target Depth: {self.tokenizer_target_depth}")
677
+ if self.image_cond_enabled:
678
+ if self.image_cond_render_on_the_fly:
679
+ print(
680
+ f" [Image Cond] Enabled | mode=render_on_the_fly | res={self.image_cond_resolution} "
681
+ f"| yaw={self.render_yaw_deg} pitch={self.render_pitch_deg} fov={self.render_fov_deg} "
682
+ f"radius_init={self.render_init_radius} auto_radius={self.render_auto_radius} "
683
+ f"randomize_yaw={self.render_randomize_yaw}"
684
+ )
685
+ else:
686
+ print(f" [Image Cond] Enabled | mode=disk | render_root={self.image_cond_render_root} | res={self.image_cond_resolution}")
687
+ print("="*70 + "\n")
688
+ print(f"[MeshDataset] Created from {len(self.data)} entries")
689
+
690
+ def __len__(self):
691
+ return len(self.data)
692
+
693
+ def get_all_token_lenth(self):
694
+ return self.all_token_len
695
+
696
+ # __getitem__ 方法保持不变
697
+ def __getitem__(self, item):
698
+ iter_cnt = 0
699
+ if isinstance(item, tuple):
700
+ idx, start, end = item
701
+ else:
702
+ idx = item
703
+ start = 0
704
+ end = -1
705
+ while True:
706
+ file_path = self.data[idx]
707
+ iter_cnt += 1
708
+ if iter_cnt >= 4:
709
+ print(f'iter_cnt>=4: {iter_cnt}')
710
+ try:
711
+ mesh = trimesh.load(file_path, force="mesh")
712
+ # y轴朝上->z轴朝上
713
+ if self.y_up:
714
+ mesh.vertices = mesh.vertices[:, [2, 0, 1]]
715
+
716
+ # 随机塌缩
717
+ if self.dec_augment:
718
+ if mesh.faces.shape[0] >= self.min_length and random.random() < 0.5:
719
+ target = np.random.randint(max(self.min_length, mesh.faces.shape[0] // 4), mesh.faces.shape[0])
720
+ mesh = dec(mesh=mesh, target_face=target)
721
+
722
+ # 随机拉伸和平移
723
+ if self.scale_augment or self.shift_augment:
724
+ mesh.vertices = agu_mesh(mesh.vertices, self.shift_augment, self.scale_augment)
725
+
726
+ # 随机旋转
727
+ if self.rot_augment:
728
+ # azimuth = np.random.uniform(0, 360)
729
+ azimuth = np.random.choice([0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330], 1)[0]
730
+ theta = np.radians(-azimuth)
731
+ rotz = np.array([
732
+ [np.cos(theta), -np.sin(theta), 0],
733
+ [np.sin(theta), np.cos(theta), 0],
734
+ [0, 0, 1]
735
+ ])
736
+ mesh.vertices = mesh.vertices @ rotz.T # 绕z轴旋转
737
+
738
+ # 归一化
739
+ normalize(mesh)
740
+
741
+ if isinstance(self.tokenizer_target_depth, list):
742
+ selected_depth = random.choice(self.tokenizer_target_depth)
743
+ else:
744
+ selected_depth = self.tokenizer_target_depth
745
+
746
+ # discrete_mesh = discrete_undiscrete_and_clean(mesh, n_discrete=2**selected_depth)
747
+
748
+ # # 采样点云
749
+ # pc = sample_pc(discrete_mesh, pc_num=self.pc_num, total_pc_num=self.total_pc_num,
750
+ # with_normal=True, aug=self.sample_augment, overfit=self.overfit)
751
+ # 采样点云
752
+ pc = sample_pc(mesh, pc_num=self.pc_num, total_pc_num=self.total_pc_num,
753
+ with_normal=True, aug=self.sample_augment, overfit=self.overfit)
754
+ if not np.isfinite(pc).all():
755
+ raise ValueError("NaN/Inf in sampled point cloud")
756
+ # raw_token, token = self.codec.encode_mesh(mesh, target_depth=self.tokenizer_target_depth)
757
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
758
+
759
+ future = executor.submit(self.codec.encode_mesh, mesh, target_depth=selected_depth,
760
+ corrupt=self.corrupt_cfg)
761
+ # 超时会抛 concurrent.futures.TimeoutError(str() 为空)-> 换成带 depth/面数的明确消息
762
+ try:
763
+ raw_token, token, dis_mesh = future.result(timeout=10.0)
764
+ except concurrent.futures.TimeoutError:
765
+ raise TimeoutError(f"encode_mesh 超时(>10s, depth={selected_depth} res{2**(selected_depth+1)}, 输入面={len(mesh.faces)}) —— 多半粗分辨率过连接")
766
+
767
+ n = len(token)
768
+ if n < self.min_length:
769
+ raise ValueError(f"Token length {n} is less than min_length {self.min_length}")
770
+
771
+ if n > self.max_length:
772
+ raise ValueError(f"Token length {n} exceeds max_length {self.max_length}")
773
+
774
+ # Render the (augmented) mesh on-the-fly. Done inside the try
775
+ # block so any render failure resamples a different mesh.
776
+ rendered = None
777
+ if self.image_cond_enabled:
778
+ if self.image_cond_render_on_the_fly:
779
+ yaw_deg = (
780
+ float(np.random.uniform(0.0, 360.0))
781
+ if (self.render_randomize_yaw and self.is_train)
782
+ else self.render_yaw_deg
783
+ )
784
+ rendered = _render_normals_image(
785
+ np.asarray(mesh.vertices, dtype=np.float32),
786
+ np.asarray(mesh.faces, dtype=np.int32),
787
+ yaw_deg=yaw_deg,
788
+ pitch_deg=self.render_pitch_deg,
789
+ fov_deg=self.render_fov_deg,
790
+ init_radius=self.render_init_radius,
791
+ resolution=self.image_cond_resolution,
792
+ auto_radius=self.render_auto_radius,
793
+ target_fill=self.render_target_fill,
794
+ )
795
+ else:
796
+ rendered = _load_image_and_camera(
797
+ file_path,
798
+ self.image_cond_render_root,
799
+ self.image_cond_resolution,
800
+ )
801
+
802
+ data_dict = {}
803
+ data_dict["token"] = torch.tensor(token, dtype=torch.int64)
804
+ data_dict["raw_token"] = torch.tensor(raw_token, dtype=torch.int64)
805
+ data_dict["raw_vertices"] = mesh.vertices
806
+ data_dict["raw_faces"] = mesh.faces
807
+ data_dict["dis_mesh"] = dis_mesh
808
+ del mesh
809
+ break
810
+ except Exception as e:
811
+ _reason = str(e) if str(e) else f"{type(e).__name__}(空消息)"
812
+ print(f'[dataiter wrong]: {file_path}, {_reason}')
813
+ idx = np.random.randint(0, len(self.data))
814
+ # start = 0
815
+ # end = -1
816
+
817
+ if len(data_dict["token"]) < self.window_size:
818
+ data_dict["start"] = 0
819
+ data_dict["end"] = len(data_dict["token"])
820
+ else:
821
+ if start == 0:
822
+ data_dict["start"] = 0
823
+ data_dict["end"] = self.window_size
824
+ # 随机截断
825
+ elif start == -1 and end == -1:
826
+ data_dict["start"] = -1
827
+ data_dict["end"] = -1
828
+ else:
829
+ data_dict["start"] = len(data_dict["token"]) - self.window_size
830
+ data_dict["end"] = len(data_dict["token"])
831
+ data_dict["pc"] = torch.from_numpy(pc)
832
+ data_dict["token_len"] = torch.tensor([len(data_dict["token"])], dtype=torch.int64)
833
+ data_dict['name'] = Path(file_path).stem
834
+ data_dict["file_path"] = file_path
835
+ data_dict["selected_depth"] = selected_depth
836
+ if self.image_cond_enabled:
837
+ if self.image_cond_render_on_the_fly:
838
+ # Render-on-the-fly path still returns the legacy single-view tuple
839
+ # (no depth). Add a leading view-dim so downstream code is uniform.
840
+ image_tensor, transform_matrix, camera_angle_x = rendered
841
+ data_dict["image"] = image_tensor.unsqueeze(0)
842
+ data_dict["transform_matrix"] = transform_matrix.unsqueeze(0)
843
+ data_dict["camera_angle_x"] = camera_angle_x.unsqueeze(0)
844
+ # No depth available — emit dummies; the conditioner will skip
845
+ # occlusion testing when depth_valid is all False.
846
+ H = image_tensor.shape[-1]
847
+ data_dict["depth_map"] = torch.zeros(1, H, H, dtype=torch.float32)
848
+ data_dict["depth_valid"] = torch.zeros(1, H, H, dtype=torch.bool)
849
+ else:
850
+ (image_tensor, transform_matrix, camera_angle_x,
851
+ depth_map, depth_valid) = rendered
852
+ data_dict["image"] = image_tensor
853
+ data_dict["transform_matrix"] = transform_matrix
854
+ data_dict["camera_angle_x"] = camera_angle_x
855
+ data_dict["depth_map"] = depth_map
856
+ data_dict["depth_valid"] = depth_valid
857
+ return data_dict
858
+
859
+ @classmethod
860
+ def load(cls, opt, path, codec, accelerator, is_train):
861
+ """
862
+ 读取数据并使用 opt 对象来初始化 Dataset 实例。
863
+ """
864
+ all_token_len = []
865
+ data = []
866
+ max_length = opt.data_processing.max_face_num
867
+ min_len = opt.data_processing.min_face_num
868
+ (image_cond_enabled, render_root, _, render_on_the_fly,
869
+ _, render_manifest_path) = _read_image_cond_cfg(opt)
870
+ # Disk-render availability filter only applies when we read PNGs from
871
+ # disk; in render_on_the_fly mode the renderer makes its own image.
872
+ apply_disk_render_filter = image_cond_enabled and not render_on_the_fly
873
+ # Prefer the manifest when the disk-render filter is on. Path comes
874
+ # from `image_cond.render_manifest_path` in yaml; if absent or the
875
+ # file doesn't exist, fall back to per-line os.path.exists (slow).
876
+ render_manifest = None
877
+ if apply_disk_render_filter:
878
+ render_manifest = _load_render_manifest(render_manifest_path)
879
+ if accelerator.is_main_process:
880
+ if render_manifest is not None:
881
+ print(f"[MeshDataset] Using render manifest ({len(render_manifest)} stems) "
882
+ f"from {render_manifest_path}")
883
+ else:
884
+ if render_manifest_path:
885
+ print(f"[MeshDataset] WARNING: image_cond.render_manifest_path={render_manifest_path} "
886
+ f"not found — falling back to per-line os.path.exists (slow). "
887
+ f"Run script/build_render_manifest.py to fix.")
888
+ else:
889
+ print(f"[MeshDataset] WARNING: image_cond.render_manifest_path not set — "
890
+ f"falling back to per-line os.path.exists (slow). "
891
+ f"Run script/build_render_manifest.py and set the yaml key.")
892
+ dropped_no_render = 0
893
+ with open(path, 'r') as f:
894
+ for line in tqdm(f):
895
+ # 这段逻辑可能需要根据你的文件名格式调整
896
+ try:
897
+ token_len = int(line.split("_f")[-1].split("_")[0])
898
+ except (IndexError, ValueError):
899
+ try:
900
+ token_len = int(line.split("_facenum_")[-1].split("_")[0])
901
+ except (ValueError, IndexError):
902
+ try:
903
+ token_len = int(line.split("_facenum_")[-1].split(".")[0])
904
+ except (ValueError, IndexError):
905
+ # 如果文件名不包含token长度
906
+ mesh = trimesh.load(line.strip(), force="mesh")
907
+ token_len = mesh.faces.shape[0]
908
+ if token_len <= max_length and token_len>= min_len:
909
+ file_path = line.strip()
910
+ if apply_disk_render_filter:
911
+ if render_manifest is not None:
912
+ if Path(file_path).stem not in render_manifest:
913
+ dropped_no_render += 1
914
+ continue
915
+ elif not _has_render(file_path, render_root):
916
+ dropped_no_render += 1
917
+ continue
918
+ data.append(file_path)
919
+ all_token_len.append(token_len)
920
+
921
+ if accelerator.is_main_process:
922
+ print(f"[MeshDataset] Loaded {len(data)} entries from {path}")
923
+ if apply_disk_render_filter:
924
+ print(
925
+ f"[MeshDataset] Image-cond filter dropped {dropped_no_render} entries with no render under {render_root}"
926
+ )
927
+ if len(data) == 0:
928
+ print(
929
+ f"[MeshDataset] WARNING: no usable entries after image-cond filter — render root may be wrong."
930
+ )
931
+ elif image_cond_enabled and render_on_the_fly:
932
+ print(f"[MeshDataset] Image-cond mode=render_on_the_fly — no disk-render filter applied.")
933
+
934
+ # 使用 opt 对象来创建类的实例
935
+ return cls(opt,
936
+ data=data,
937
+ codec=codec,
938
+ all_token_len=all_token_len,
939
+ is_train=is_train)
940
+
mulres_infer/dataset/mesh_dataset_altpc.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """altpc 变体:pc_target 改用「编码→解码的 mesh」(octree token 解码的 GT@目标分辨率),
2
+ 而非 discrete_undiscrete 塌缩。理由:解码 mesh 与模型实际输出同一表示(含退化面结构),条件更一致。
3
+ PCT_DEGEN=1 时, 额外在退化面(零面积, 即栏杆/薄结构边)上采点并注入 pc_target(法向置0)。
4
+ 直接 decode 父类已存的 d["token"](无需重编码)。
5
+ """
6
+ import os
7
+ import numpy as np
8
+ import trimesh
9
+ import torch
10
+
11
+ from dataset.mesh_dataset import MeshDataset, sample_pc
12
+
13
+ PCT_DEGEN = os.environ.get("PCT_DEGEN", "0") == "1" # 是否在退化面上也采样注入
14
+ _EPS = 1e-7
15
+
16
+
17
+ def rail_edges(mesh):
18
+ """退化面(零面积)的有效栏杆边:取每个退化面最长边(对 (A,B,B),B-B 长 0,最长边即真实 A-B)。
19
+ 返回 (A[D,3], B[D,3], 边长[D])。无退化面则 D=0。"""
20
+ af = np.asarray(mesh.area_faces)
21
+ deg = np.where(af < _EPS)[0]
22
+ if len(deg) == 0:
23
+ return np.zeros((0, 3)), np.zeros((0, 3)), np.zeros((0,))
24
+ v = np.asarray(mesh.vertices); f = np.asarray(mesh.faces)[deg]
25
+ ev = v[f]
26
+ pairs = [(0, 1), (1, 2), (0, 2)]
27
+ lens = np.stack([np.linalg.norm(ev[:, i] - ev[:, j], axis=1) for i, j in pairs], axis=1) # [D,3]
28
+ pick = lens.argmax(1)
29
+ A = ev[np.arange(len(deg)), [pairs[p][0] for p in pick]]
30
+ B = ev[np.arange(len(deg)), [pairs[p][1] for p in pick]]
31
+ return A, B, lens[np.arange(len(deg)), pick]
32
+
33
+
34
+ def sample_rail(A, B, lengths, n):
35
+ """沿栏杆边按边长加权采 n 个点(长边多采)。返回 [n,3]。"""
36
+ if n <= 0 or len(lengths) == 0 or lengths.sum() <= 0:
37
+ return np.zeros((0, 3))
38
+ counts = np.random.multinomial(n, lengths / lengths.sum()) # 每条边分到的点数, 和=n
39
+ idx = np.repeat(np.arange(len(lengths)), counts) # [n] 每点所属边
40
+ t = np.random.rand(len(idx), 1).astype(np.float32)
41
+ return A[idx] + t * (B[idx] - A[idx])
42
+
43
+
44
+ def degen_point_budget(mesh, pc_num, resolution):
45
+ """按「一致面密度」分配表面/栏杆点数, 不固定比例。
46
+ 退化边当 1 体素宽薄条: 等效面积 = 边长 × 体素宽(mesh尺度/分辨率)。
47
+ ρ = pc_num/(表面积+栏杆等效面积); n_rail = round(ρ·栏杆等效面积)。
48
+ 高分辨率体素小→栏杆相对少; 栏杆多的 mesh→栏杆点自然多。返回 (n_surf, n_rail, A, B, lengths)。"""
49
+ af = np.asarray(mesh.area_faces)
50
+ surf_area = float(af[af >= _EPS].sum())
51
+ A, B, lengths = rail_edges(mesh)
52
+ rail_len = float(lengths.sum())
53
+ if rail_len <= 0 or surf_area <= 0:
54
+ return pc_num, 0, A, B, lengths
55
+ voxel = float(np.max(mesh.extents)) / max(1, resolution) # 1 体素的物理尺度
56
+ rail_eff_area = rail_len * voxel
57
+ rho = pc_num / (surf_area + rail_eff_area)
58
+ n_rail = int(round(rho * rail_eff_area))
59
+ n_rail = max(0, min(n_rail, pc_num))
60
+ return pc_num - n_rail, n_rail, A, B, lengths
61
+
62
+
63
+ def sample_pc_with_degen(mesh, pc_num, total_pc_num, aug, overfit, resolution=64):
64
+ """表面采样 + 退化面边上采样, 按一致密度自适应分配(不固定比例)。栏杆点法向置 0。总数 = pc_num。"""
65
+ n_surf, n_rail, A, B, lengths = degen_point_budget(mesh, pc_num, resolution)
66
+ surf = sample_pc(mesh, pc_num=pc_num, total_pc_num=total_pc_num, with_normal=True, aug=aug, overfit=overfit)
67
+ if n_rail <= 0:
68
+ return surf
69
+ rail_xyz = sample_rail(A, B, lengths, n_rail)
70
+ if len(rail_xyz) == 0:
71
+ return surf
72
+ rail = np.concatenate([rail_xyz, np.zeros_like(rail_xyz)], axis=1).astype(surf.dtype) # 法向 0
73
+ return np.concatenate([surf[:pc_num - len(rail)], rail], axis=0)
74
+
75
+
76
+ class MeshDatasetAltPCDecoded(MeshDataset):
77
+ def __getitem__(self, item):
78
+ d = super().__getitem__(item)
79
+ sd = int(d["selected_depth"])
80
+ # 直接 decode 父类已存的 token -> octree 解码 GT@目标分辨率(与模型输出同表示, 含退化面)
81
+ try:
82
+ decoded = self.codec.decode_tokens(d["token"].tolist(), target_depth=sd)
83
+ except Exception:
84
+ decoded = None
85
+ if decoded is None or len(getattr(decoded, "faces", [])) < 4:
86
+ decoded = trimesh.Trimesh(vertices=d["raw_vertices"], faces=d["raw_faces"], process=False)
87
+ if PCT_DEGEN:
88
+ pc_t = sample_pc_with_degen(decoded, self.pc_num, self.total_pc_num, self.sample_augment,
89
+ self.overfit, resolution=2 ** (sd + 1))
90
+ else:
91
+ pc_t = sample_pc(decoded, pc_num=self.pc_num, total_pc_num=self.total_pc_num,
92
+ with_normal=True, aug=self.sample_augment, overfit=self.overfit)
93
+ d["pc_target"] = torch.from_numpy(pc_t)
94
+ return d
mulres_infer/models/__init__.py ADDED
File without changes
mulres_infer/models/generator.py ADDED
@@ -0,0 +1,1507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fix3 生成器模型(已融合: 基类 + alt-PC 双云注入 + TSSR 三前向)。
2
+
3
+ 类继承链(融合后仍保留, 因各层职责清晰):
4
+ FactorizedDiffusionTransformer 骨干: 因子分解 token / AdaLN cross-attn / 自回归行头
5
+ └ FactorizedDiffusionTransformerAltPC 逐层交替双点云注入 + 几何 RoPE + sample()
6
+ └ FactorizedTSSRTransformer mode flag + 逐 token 正确性分类器 + sample_tssr()
7
+
8
+ 原先的 FactorizedDiffusionTransformerFocal 一层只重写训练用的 factorized_loss, 推理路径上是空壳,
9
+ 已移除并让 AltPC 直接继承骨干。各类的纯训练方法(factorized_loss / factorized_accuracy /
10
+ sample_log / forward_mask / forward_nomask / zero_touch_conditional_params)也已剥离。
11
+ """
12
+
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+ import math
17
+ from models.subdivider import (
18
+ SinusoidalPosEmb,
19
+ ModernCrossAttnTransformerBlock,
20
+ ModernAttention,
21
+ RMSNorm,
22
+ SwiGLU,
23
+ RotaryEmbedding,
24
+ LinearDownsample,
25
+ LinearUpsample,
26
+ apply_rotary_pos_emb,
27
+ apply_rotary_pos_emb_varlen,
28
+ lengths_to_position_ids,
29
+ cosine_schedule,
30
+ )
31
+ from models.point_conditioner import PointConditioner
32
+ import os
33
+ import numpy as np
34
+ from models.subdivider import (
35
+ apply_rotary_pos_emb_varlen, lengths_to_position_ids, cosine_schedule,
36
+ )
37
+
38
+
39
+ # ==========================================================================
40
+ # 原 model_acc_fix3.py
41
+ # ==========================================================================
42
+
43
+ def modulate(x, shift, scale):
44
+ """adaLN 调制: x * (1 + scale) + shift。shift/scale 与 x 同形状(逐 token 广播)。"""
45
+ return x * (1 + scale) + shift
46
+ class AdaLNCrossAttnBlock(nn.Module):
47
+ """fix3 专用 block:在 self-attn 与 FFN 上加 adaLN 调制(条件 c = time + depth)。
48
+ cross-attn 仍走点云条件(不调制)。结构与 ModernCrossAttnTransformerBlock 同名(self_attn/
49
+ cross_attn/norm1-3/ffn)以便迁移 fix2 权重;adaLN_modulation 为**新增**且做「恒等初始化」
50
+ (shift=0, scale=0, gate=1),使加载迁移权重时 block == 旧 block,backbone 不被破坏。"""
51
+
52
+ def __init__(self, d_model, nhead, dropout=0., qk_norm=False, ffn_hidden=None):
53
+ super().__init__()
54
+ # QK-norm 只加在 self-attn(instability 主源);cross-attn 的 K 是点云条件,保持默认不动
55
+ self.self_attn = ModernAttention(d_model, nhead, dropout=dropout, qk_norm=qk_norm)
56
+ self.cross_attn = ModernAttention(d_model, nhead, dropout=dropout)
57
+ self.norm1 = RMSNorm(d_model)
58
+ self.norm2 = RMSNorm(d_model)
59
+ self.norm3 = RMSNorm(d_model)
60
+ self.ffn = SwiGLU(d_model, ffn_hidden)
61
+ self.dropout = nn.Dropout(dropout)
62
+ # 从条件 c 产生 self-attn / FFN 各自的 (shift, scale, gate),共 6*d_model
63
+ self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_model, 6 * d_model))
64
+ # 恒等初始化:weight=0;bias 使 shift=0, scale=0, gate=1 -> block 退化为旧 block
65
+ nn.init.zeros_(self.adaLN_modulation[1].weight)
66
+ with torch.no_grad():
67
+ b = torch.zeros(6 * d_model)
68
+ b[2 * d_model:3 * d_model] = 1.0 # gate_sa = 1
69
+ b[5 * d_model:6 * d_model] = 1.0 # gate_ff = 1
70
+ self.adaLN_modulation[1].bias.copy_(b)
71
+
72
+ def forward_varlen(self, x, c, lengths, cond=None, cond_lengths=None, rope_apply_fn=None,
73
+ max_len=None, cond_max_len=None):
74
+ """c: [total, d_model] 逐 token 条件(time+depth)。
75
+ max_len / cond_max_len: 透传给 attention 的 max_seqlen(python int), 省掉 GPU->CPU 同步;
76
+ 不传则由 attention 内部回退到 `.item()`(见 ModernAttention.forward_varlen 的说明)。
77
+
78
+ 注: 曾试过「c 只传 [b,d]、在块内 adaLN 后再 repeat_interleave」以省掉 768->6*768 Linear
79
+ 在同一行上重复算 total 次的冗余(每块 424 GFLOP@60k token) —— 实测输出逐位相同但**零加速**
80
+ (hourglass 已把 token 降到 1/3、1/9, 那 3.1 TFLOP 不是瓶颈), 且会让 torch.compile 报
81
+ dtype 不匹配。故回退, 别再试。
82
+ """
83
+ # c 来自 time_mlp + depth_embedding, 是 fp32(nn.Embedding 不在 autocast 转型名单里)。
84
+ # 对齐到 x 的 dtype: eager+autocast 下等价(Linear 本来就会转), 但 torch.compile 编译这个方法时
85
+ # 会丢掉外层 autocast 上下文, 不显式对齐就会在后面的 matmul 上报 "Float and BFloat16"。
86
+ c = c.to(x.dtype)
87
+ shift_sa, scale_sa, gate_sa, shift_ff, scale_ff, gate_ff = self.adaLN_modulation(c).chunk(6, dim=-1)
88
+ h = modulate(self.norm1(x), shift_sa, scale_sa)
89
+ sa = self.self_attn.forward_varlen(h, h, h, lengths, rope_apply_fn=rope_apply_fn,
90
+ max_len_q=max_len)
91
+ x = x + gate_sa * self.dropout(sa)
92
+ if cond is not None:
93
+ h = self.norm2(x)
94
+ # CFG drop 的步里 cond 来自 fp32 的 null_cond; 同 c 一样显式对齐(见上), 否则 compile 会崩
95
+ ca = self.cross_attn.forward_varlen(h, cond.to(x.dtype), cond.to(x.dtype), lengths,
96
+ lengths_k=cond_lengths,
97
+ max_len_q=max_len, max_len_k=cond_max_len)
98
+ x = x + self.dropout(ca)
99
+ h = modulate(self.norm3(x), shift_ff, scale_ff)
100
+ ff = self.ffn(h)
101
+ x = x + gate_ff * self.dropout(ff)
102
+ return x
103
+ SUB_VOCAB = 256 # 每个子码(8bit)的取值数
104
+ MASK_ID = SUB_VOCAB # 子码 MASK id = 256(embedding 表大小 = 257)
105
+ NUM_OCTANTS = 8
106
+ TYPE_F, TYPE_INTRA, TYPE_INTER = 0, 1, 2
107
+ REFINE_POS_PER_FACE = 9 # 3 F + 3 intra + 3 inter
108
+ class AutoregressiveRowHead(nn.Module):
109
+ """连接矩阵(intra/inter)的 8 行**自回归** MLP 头(参考 TreeMeshGPT 的 head_coord1/2/3)。
110
+
111
+ 取代原先「8 行并行独立预测」:行 r 的 256-way logits 由
112
+ cat([hidden, emb(行0), ..., emb(行r-1)]) -> 3 层 MLP
113
+ 给出,从而**显式建模行间依赖**,让模型只产出能共现的合法矩阵(对称/可成 clique),
114
+ 减少独立预测导致的「悬边→退化面/碎块」。
115
+ * 训练: teacher forcing(回喂 GT 行 embedding),一次 forward 出全 8 行 logits。
116
+ * 采样: 逐行 argmax rollout(回喂已采行);支持 CFG —— cond/uncond 在每行 logits 级合并,
117
+ 合并后采样得到的同一行回喂两路,保持自回归状态一致。
118
+ 行 embedding 维度默认 = d_model//8(与输入端 row_embed_dim 一致,控参数量);最后一行不作输入故只需 7 张表。
119
+ """
120
+
121
+ def __init__(self, d_model, num_rows=NUM_OCTANTS, vocab=SUB_VOCAB, row_emb_dim=None):
122
+ super().__init__()
123
+ self.num_rows = num_rows
124
+ self.vocab = vocab
125
+ self.row_emb_dim = row_emb_dim or (d_model // num_rows)
126
+ self.mlps = nn.ModuleList([
127
+ nn.Sequential(
128
+ nn.Linear(d_model + r * self.row_emb_dim, d_model), nn.ReLU(),
129
+ nn.Linear(d_model, d_model), nn.ReLU(),
130
+ nn.Linear(d_model, vocab),
131
+ ) for r in range(num_rows)
132
+ ])
133
+ self.row_emb = nn.ModuleList([nn.Embedding(vocab, self.row_emb_dim) for _ in range(num_rows - 1)])
134
+
135
+ def forward_teacher(self, h, gt_rows):
136
+ """h [n,d];gt_rows [n,num_rows] long(取值[0,vocab)) -> logits [n,num_rows,vocab]。teacher forcing。"""
137
+ feats = [h]
138
+ logits = []
139
+ for r in range(self.num_rows):
140
+ logits.append(self.mlps[r](torch.cat(feats, dim=-1)))
141
+ if r < self.num_rows - 1:
142
+ feats.append(self.row_emb[r](gt_rows[:, r]).to(h.dtype))
143
+ return torch.stack(logits, dim=1)
144
+
145
+ def rollout(self, h, h_u=None, cfg_scale=1.0, temperature=0.0):
146
+ """逐行采样。h [n,d];h_u 提供则做 CFG(logits=uncond+w*(cond-uncond))。
147
+ temperature<=0: argmax 贪心(确定性);>0: 按 (logit/temperature) 多项式采样(用于 best-of-N 产生多样候选)。
148
+ 返回 (pred [n,num_rows] long, conf [n,num_rows] 每行所选类的概率)。"""
149
+ feats = [h]
150
+ feats_u = [h_u] if h_u is not None else None
151
+ preds, confs = [], []
152
+ for r in range(self.num_rows):
153
+ logit = self.mlps[r](torch.cat(feats, dim=-1))
154
+ if feats_u is not None:
155
+ lu = self.mlps[r](torch.cat(feats_u, dim=-1))
156
+ logit = lu + cfg_scale * (logit - lu)
157
+ if temperature and temperature > 0:
158
+ prob = (logit / temperature).softmax(-1)
159
+ am = torch.multinomial(prob.float(), 1).squeeze(-1) # [n]
160
+ pm = prob.gather(-1, am.unsqueeze(-1)).squeeze(-1) # 所选类概率
161
+ else:
162
+ prob = logit.softmax(-1)
163
+ pm, am = prob.max(-1)
164
+ preds.append(am); confs.append(pm)
165
+ if r < self.num_rows - 1:
166
+ e = self.row_emb[r](am).to(h.dtype)
167
+ feats.append(e)
168
+ if feats_u is not None:
169
+ feats_u.append(e)
170
+ return torch.stack(preds, dim=1), torch.stack(confs, dim=1)
171
+ class FactorizedDiffusionTransformer(nn.Module):
172
+ def __init__(self,
173
+ coord_vocab_size, # raw_embedding 大小 = resolution+1 (coord 含 offset, 0=pad)
174
+ max_seq_len,
175
+ d_model=768,
176
+ nhead=12,
177
+ depth=(4, 4, 8),
178
+ dropout=0.,
179
+ encoder_freeze=True,
180
+ encoder_name='miche-256-feature',
181
+ use_pc=True,
182
+ time_embed_scale=100.0,
183
+ num_depths=16, # depth(层级) embedding 大小,覆盖 target_depth 0..max
184
+ nonempty_row_weight=1.0, # 连接矩阵「非空行」(目标!=0, 即真实连接) 的 loss 上权
185
+ qk_norm=False, # self-attn 的 Q/K 加 per-head RMSNorm(��定 attention);默认关
186
+ ffn_hidden=None, # FFN 隐层维度显式覆盖(None=8/3*d_model; 1024宽用2816对齐TensorCore)
187
+ pc_num_latents=None, # 点云编码器 latent 数(None=预训练默认256; 1024=条件带宽x4, query平铺扩容)
188
+ time_embed_dim=None): # 正弦基底/time_mlp 隐层维度(None=d_model, 默认即可)。备注:
189
+ # SinusoidalPosEmb 的"列-频率/sin-cos"布局随 dim 变化, 宽度扩展
190
+ # 热启动时旧 time_mlp 权重不可拷贝(语义错位会爆), 见扩展脚本。
191
+ super().__init__()
192
+ self.d_model = d_model
193
+ self.mask_token_id = MASK_ID
194
+ self.num_depths = num_depths
195
+ # 连接(intra/inter)行子码中「非空行」(target!=0) 的 loss 权重。connectivity 矩阵 ~92% 是空行,
196
+ # 等权 CE 会让模型坍缩到「全预测空行」(逐子码 acc 虚高、生成 mesh 欠连接碎裂)。>1 上权真实连接行。
197
+ self.nonempty_row_weight = float(nonempty_row_weight)
198
+
199
+ # 1. Embeddings ----------------------------------------------------
200
+ self.raw_embedding = nn.Embedding(coord_vocab_size, d_model) # coord(条件)
201
+ self.f_sub_embedding = nn.Embedding(SUB_VOCAB + 1, d_model) # F: 单子码
202
+ # intra/inter: 8 个行子码各查表(+1 行给 MASK),再 cat 后 Linear 投影到 d_model。
203
+ # 用 cat 而非 sum:保留 8 行的「行序/位置语义」(行 r = octant r),避免 sum 的置换不变与叠加丢失;
204
+ # 行 embedding 维度 = d_model // 8,cat 后恰好 = d_model,再过一层 Linear 学行间组合(含 intra 对称)。
205
+ self.row_embed_dim = d_model // NUM_OCTANTS
206
+ self.intra_sub_embedding = nn.Embedding(SUB_VOCAB + 1, self.row_embed_dim)
207
+ self.inter_sub_embedding = nn.Embedding(SUB_VOCAB + 1, self.row_embed_dim)
208
+ self.intra_in_proj = nn.Linear(NUM_OCTANTS * self.row_embed_dim, d_model)
209
+ self.inter_in_proj = nn.Linear(NUM_OCTANTS * self.row_embed_dim, d_model)
210
+
211
+ # 2. 条件: 时间步 + 层级(depth) —— 一起作为 adaLN 的条件 c -------------
212
+ te = int(time_embed_dim) if time_embed_dim else d_model
213
+ self.time_mlp = nn.Sequential(
214
+ SinusoidalPosEmb(te, scale=time_embed_scale),
215
+ nn.Linear(te, te * 4),
216
+ nn.GELU(),
217
+ nn.Linear(te * 4, d_model),
218
+ )
219
+ self.depth_embedding = nn.Embedding(num_depths, d_model) # 层级 embedding(随机初始化)
220
+
221
+ # 3. 主干: 带 adaLN 的 block(条件 c = time + depth);blocks 内 self_attn/cross_attn/
222
+ # norm/ffn 与 ModernCrossAttnTransformerBlock 同名可迁移,adaLN_modulation 为新增(恒等初始化)
223
+ self.depth = list(depth)
224
+ _blk = lambda: AdaLNCrossAttnBlock(d_model, nhead, dropout=dropout, qk_norm=qk_norm, ffn_hidden=ffn_hidden)
225
+ self.enc_blocks_9 = nn.ModuleList([_blk() for _ in range(depth[0])])
226
+ self.enc_blocks_3 = nn.ModuleList([_blk() for _ in range(depth[1])])
227
+ self.bottleneck_blocks = nn.ModuleList([_blk() for _ in range(depth[2])])
228
+ self.dec_blocks_3 = nn.ModuleList([_blk() for _ in range(depth[1])])
229
+ self.dec_blocks_9 = nn.ModuleList([_blk() for _ in range(depth[0])])
230
+ self.down_9_to_3 = LinearDownsample(d_model, shorten_factor=3)
231
+ self.down_3_to_1 = LinearDownsample(d_model, shorten_factor=3)
232
+ self.up_1_to_3 = LinearUpsample(d_model, shorten_factor=3)
233
+ self.up_3_to_9 = LinearUpsample(d_model, shorten_factor=3)
234
+ self.rotary_emb = RotaryEmbedding(dim=d_model // nhead, max_position_embeddings=max_seq_len)
235
+
236
+ # 4. 因子分解输出头 ------------------------------------------------
237
+ self.f_head = nn.Linear(d_model, SUB_VOCAB) # F: 1 个子码(单头)
238
+ # intra/inter: 8 行**自回归** MLP 头(行间依赖, 取代旧的 8 行并行独立 Linear)
239
+ self.intra_head = AutoregressiveRowHead(d_model, num_rows=NUM_OCTANTS, vocab=SUB_VOCAB)
240
+ self.inter_head = AutoregressiveRowHead(d_model, num_rows=NUM_OCTANTS, vocab=SUB_VOCAB)
241
+
242
+ # 5. 点云条件 ------------------------------------------------------
243
+ self.use_pc = use_pc
244
+ if use_pc:
245
+ print(f'[fix3] Point cloud encoder: {encoder_name} | freeze: {encoder_freeze}')
246
+ self.conditioner = PointConditioner(model_name=encoder_name, freeze=encoder_freeze, feature_dim=d_model,
247
+ num_latents=pc_num_latents)
248
+ self.null_cond_len = 257
249
+ self.null_cond = nn.Parameter(torch.randn(self.null_cond_len, d_model) * 0.02)
250
+
251
+ # ------------------------------------------------------------------ #
252
+ # 工具
253
+ # ------------------------------------------------------------------ #
254
+ def types_from_lengths(self, lengths):
255
+ """由每个样本长度(=9*nface)推出 packed 的位置类型 [total],0/1/2 = F/intra/inter。"""
256
+ parts = []
257
+ for L in lengths.tolist():
258
+ idx = torch.arange(L, device=lengths.device)
259
+ fp = idx % REFINE_POS_PER_FACE
260
+ t = torch.where(fp < 3, TYPE_F,
261
+ torch.where(fp < 6, TYPE_INTRA, TYPE_INTER))
262
+ parts.append(t)
263
+ return torch.cat(parts).long() if parts else torch.empty(0, dtype=torch.long, device=lengths.device)
264
+
265
+ def embed_refine(self, subcodes, types):
266
+ """subcodes [N,8] long, types [N] long -> [N,d]。
267
+ F 位置: 单子码查表。intra/inter 位置: 8 行各查表 -> cat -> Linear(保留行序,避免 sum 丢失)。"""
268
+ N = subcodes.size(0)
269
+ out = self.f_sub_embedding.weight.new_zeros((N, self.d_model))
270
+ is_f = types == TYPE_F
271
+ is_in = types == TYPE_INTRA
272
+ is_ix = types == TYPE_INTER
273
+ # autocast(bf16) 下 Linear 输出是 bf16,而 out 是 fp32;原地 index_put 要求 dtype 一致,
274
+ # 故统一 .to(out.dtype)(保持 refine_emb 与 raw_emb 一致的 fp32,backbone matmul 再由 autocast 转 bf16)。
275
+ if is_f.any():
276
+ out[is_f] = self.f_sub_embedding(subcodes[is_f, 0]).to(out.dtype)
277
+ if is_in.any():
278
+ e = self.intra_sub_embedding(subcodes[is_in]) # [n, 8, row_embed_dim]
279
+ out[is_in] = self.intra_in_proj(e.reshape(e.size(0), -1)).to(out.dtype) # cat -> Linear -> [n, d]
280
+ if is_ix.any():
281
+ e = self.inter_sub_embedding(subcodes[is_ix])
282
+ out[is_ix] = self.inter_in_proj(e.reshape(e.size(0), -1)).to(out.dtype)
283
+ return out
284
+
285
+ # ------------------------------------------------------------------ #
286
+ # 前向(varlen):返回主干 hidden h [total, d] 与 packed types [total]
287
+ # ------------------------------------------------------------------ #
288
+ def forward_varlen(self, coord_list, subcode_list, t, depth, pc=None):
289
+ device = t.device
290
+ coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
291
+ subcode_list = [s.to(device=device, dtype=torch.long) for s in subcode_list]
292
+ lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
293
+ if int(lengths.sum().item()) == 0:
294
+ raise ValueError("forward_varlen received an empty batch.")
295
+ if torch.any(lengths % REFINE_POS_PER_FACE != 0):
296
+ raise ValueError("每个序列长度必须是 9 的倍数(每 face 9 个 refine 位置)。")
297
+
298
+ coord_packed = torch.cat(coord_list, dim=0)
299
+ subcode_packed = torch.cat(subcode_list, dim=0)
300
+ types = self.types_from_lengths(lengths)
301
+
302
+ raw_emb = self.raw_embedding(coord_packed)
303
+ refine_emb = self.embed_refine(subcode_packed, types)
304
+ x = raw_emb + refine_emb # 不再加法注入 time;time/depth 走 adaLN
305
+
306
+ # adaLN 条件 c = time_emb + depth_emb(逐样本 [b,d]);各 hourglass 阶段按其长度展开到逐 token
307
+ depth = depth.to(device=device, dtype=torch.long)
308
+ c_sample = self.time_mlp(t) + self.depth_embedding(depth) # [b, d_model]
309
+
310
+ def c_for(cur_lengths):
311
+ return c_sample.repeat_interleave(cur_lengths, dim=0)
312
+
313
+ if self.use_pc:
314
+ if pc is None:
315
+ bsz = len(coord_list)
316
+ null = self.null_cond
317
+ cond = null.unsqueeze(0).expand(bsz, -1, -1).reshape(-1, null.size(-1))
318
+ cond_lengths = torch.full((bsz,), self.null_cond_len, device=device, dtype=torch.long)
319
+ else:
320
+ pc = pc.to(device=device)
321
+ cond = self.conditioner(pc)
322
+ cond_lengths = torch.full((cond.size(0),), cond.size(1), device=device, dtype=torch.long)
323
+ cond = cond.reshape(-1, cond.size(-1))
324
+ else:
325
+ cond = None
326
+ cond_lengths = None
327
+
328
+ def rope_fn_for(cur_lengths):
329
+ position_ids = lengths_to_position_ids(cur_lengths)
330
+ cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
331
+ return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)
332
+
333
+ rope_fn_9 = rope_fn_for(lengths)
334
+ c9 = c_for(lengths)
335
+ for block in self.enc_blocks_9:
336
+ x = block.forward_varlen(x, c9, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
337
+ skip_9 = x
338
+ lengths_9 = lengths
339
+
340
+ x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
341
+ rope_fn_3 = rope_fn_for(lengths)
342
+ c3 = c_for(lengths)
343
+ for block in self.enc_blocks_3:
344
+ x = block.forward_varlen(x, c3, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
345
+ skip_3 = x
346
+ lengths_3 = lengths
347
+
348
+ x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
349
+ rope_fn_1 = rope_fn_for(lengths)
350
+ c1 = c_for(lengths)
351
+ for block in self.bottleneck_blocks:
352
+ x = block.forward_varlen(x, c1, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_1)
353
+
354
+ x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
355
+ x = x + skip_3
356
+ for block in self.dec_blocks_3:
357
+ x = block.forward_varlen(x, c3, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
358
+
359
+ x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
360
+ x = x + skip_9
361
+ for block in self.dec_blocks_9:
362
+ x = block.forward_varlen(x, c9, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
363
+
364
+ return x, types # hidden [total,d], types [total]
365
+
366
+ def forward(self, coord_list, noisy_subcode_list, t, depth,
367
+ target_subcode_list=None, loss_mask=None, pc=None, return_extra_acc=False):
368
+ """训练入口:把因子分解头放进 forward 内部(经 factorized_loss),保证 DDP 正确同步。
369
+ depth: [b] long,每个样本的层级(target_depth),与 time 一起作为 adaLN 条件。
370
+ - 传 target/loss_mask -> 返回 (loss, acc);return_extra_acc=True 时返回 (loss, acc, acc_nonempty)
371
+ (acc_nonempty=连接非空行准确率,见 factorized_accuracy)。默认 False 与旧接口兼容。
372
+ - 不传 -> 返回 (hidden, types)(推理/调试)
373
+ """
374
+ h, types = self.forward_varlen(coord_list, noisy_subcode_list, t, depth, pc)
375
+ if target_subcode_list is None:
376
+ return h, types
377
+ target = torch.cat([s.to(device=h.device, dtype=torch.long) for s in target_subcode_list], dim=0)
378
+ loss_mask = loss_mask.to(device=h.device)
379
+ loss = self.factorized_loss(h, target, types, loss_mask)
380
+ with torch.no_grad():
381
+ acc, acc_ne = self.factorized_accuracy(h, target, types, loss_mask)
382
+ if return_extra_acc:
383
+ return loss, acc, acc_ne
384
+ return loss, acc
385
+
386
+ # ------------------------------------------------------------------ #
387
+ # 因子分解 loss / accuracy
388
+ # ------------------------------------------------------------------ #
389
+
390
+
391
+ # ------------------------------------------------------------------ #
392
+ # 采样(MaskGIT 风格,因子分解、varlen、逐位置置信度解 mask)
393
+ # ------------------------------------------------------------------ #
394
+ @torch.no_grad()
395
+ def sample(self, coord_list, depth, pc=None, T_infer=10, cfg_scale=1.0, temperature=0.0,
396
+ cfg_schedule="constant", cfg_start=1.0):
397
+ """给定 coord(粗层条件)与 depth(层级),从全 MASK 迭代生成 refine 子码。
398
+ depth: [b] long(每样本层级)。返回 subcode_list(list[ [L,8] ])。
399
+ cfg_scale: classifier-free guidance 强度,约定 logits = uncond + cfg_scale*(cond - uncond)。
400
+ =1.0 即纯条件(无引导,跳过无条件前向);>1 放大点云条件(锐化连接预测、对抗欠连接)。
401
+ 仅在 use_pc 且 pc 提供时生效(无条件分支用 pc=None 走训练过的 null_cond)。
402
+ temperature: <=0 argmax 贪心(确定性,默认,与旧行为一致);>0 多项式随机采样。
403
+ best-of-N 时需 >0(如 1.0) 才能采出多样候选,否则贪心 N 次结果完全相同。
404
+ cfg_schedule: "constant"=全程恒定 cfg_scale(默认,旧行为);"linear"=按步从 cfg_start
405
+ 线性升到 cfg_scale(MUSE 式 ramp)。最初几步决定全局结构,CFG 锐化会把条件
406
+ 敏感(=分歧大、没把握)位置的置信度人为抬高、过早 commit 且不可回滚;低 cfg
407
+ 起步可避开这种早期过冲,引导力留给后期细化。
408
+ cfg_start: ramp 起点(仅 linear 生效)。默认 1.0 = 第一步纯条件(并跳过该步无条件前向)。"""
409
+ self.eval()
410
+ device = next(self.parameters()).device
411
+ coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
412
+ depth = depth.to(device=device, dtype=torch.long)
413
+ lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
414
+ types = self.types_from_lengths(lengths)
415
+ want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and self.use_pc and (pc is not None)
416
+
417
+ total = int(lengths.sum().item())
418
+ subcodes = torch.full((total, NUM_OCTANTS), MASK_ID, dtype=torch.long, device=device)
419
+ decided = torch.zeros(total, dtype=torch.bool, device=device)
420
+
421
+ for step in range(T_infer):
422
+ # 当步实际引导强度:linear 时 step0 = cfg_start,末步 = cfg_scale
423
+ if cfg_schedule == "linear":
424
+ cfg_t = float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(T_infer - 1, 1)
425
+ else:
426
+ cfg_t = float(cfg_scale)
427
+ use_cfg = want_cfg and (cfg_t != 1.0)
428
+
429
+ ratio = step / T_infer
430
+ t = torch.full((len(coord_list),), ratio, device=device, dtype=torch.float)
431
+ sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
432
+ h, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc)
433
+ h_u = self.forward_varlen(coord_list, sub_list, t, depth, None)[0] if use_cfg else None
434
+
435
+ # 逐类型预测子码 + 置信度(该位置有效子码的平均最大概率)
436
+ pred = subcodes.clone()
437
+ conf = torch.zeros(total, device=device)
438
+ # F: 单头(可 CFG)
439
+ mf = types == TYPE_F
440
+ if mf.any():
441
+ logits = self.f_head(h[mf]) # [nm,256]
442
+ if use_cfg:
443
+ lu = self.f_head(h_u[mf])
444
+ logits = lu + cfg_t * (logits - lu)
445
+ if temperature and temperature > 0:
446
+ prob = (logits / temperature).softmax(-1)
447
+ am = torch.multinomial(prob.float(), 1).squeeze(-1) # [nm]
448
+ pm = prob.gather(-1, am.unsqueeze(-1)).squeeze(-1)
449
+ else:
450
+ prob = logits.softmax(-1)
451
+ pm, am = prob.max(-1) # [nm],[nm]
452
+ pred[mf, :] = am.unsqueeze(1).expand(-1, NUM_OCTANTS) # F 复制到 8 槽
453
+ conf[mf] = pm.to(conf.dtype)
454
+ # intra/inter: 8 行自回归 rollout(可 CFG, cond/uncond 在每行 logits 级合并)
455
+ for ty, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
456
+ m = types == ty
457
+ if not m.any():
458
+ continue
459
+ p, pmaxs = head.rollout(h[m], h_u[m] if use_cfg else None, cfg_t, temperature) # [nm,8],[nm,8]
460
+ pred[m] = p
461
+ conf[m] = pmaxs.mean(dim=1).to(conf.dtype) # autocast 下 pmax 可能是 bf16
462
+
463
+ # 已决定的位置保持不变
464
+ keep_ratio = cosine_schedule(torch.tensor(float(step + 1)), float(T_infer)).item()
465
+ n_keep_masked = int(keep_ratio * total) # 仍保持 mask 的目标数(随步数减少)
466
+ conf_for_select = conf.clone()
467
+ conf_for_select[decided] = float('inf') # 已定的不再参与"新解mask"
468
+ # 选出本步要确定的位置:未定中置信度最高的若干个
469
+ n_to_decide = max(0, (total - n_keep_masked) - int(decided.sum().item()))
470
+ if n_to_decide > 0:
471
+ undecided_idx = (~decided).nonzero(as_tuple=True)[0]
472
+ order = conf[undecided_idx].argsort(descending=True)
473
+ chosen = undecided_idx[order[:n_to_decide]]
474
+ subcodes[chosen] = pred[chosen]
475
+ decided[chosen] = True
476
+ # 未决定位置回到 MASK,已决定保持
477
+ subcodes[~decided] = MASK_ID
478
+
479
+ # 收尾:还没定的全部按当前预测填上
480
+ subcodes[~decided] = pred[~decided]
481
+ return list(torch.split(subcodes, lengths.tolist(), dim=0))
482
+
483
+ # ==========================================================================
484
+ # 原 model_acc_fix3_altpc.py
485
+ # ==========================================================================
486
+
487
+ _BF16_RESIDUAL = os.environ.get("BF16_RESIDUAL", "1") == "1"
488
+ _STATIC_PAD_BUCKETS = tuple(sorted(int(x) for x in os.environ.get("STATIC_PAD_BUCKETS", "").split(",") if x.strip()))
489
+ _STATIC_PAD_SLOTS = int(os.environ.get("STATIC_PAD_SLOTS", "16"))
490
+ assert all(b % 9 == 0 for b in _STATIC_PAD_BUCKETS), "STATIC_PAD_BUCKETS 必须都是 9 的倍数"
491
+ class GeoRoPE:
492
+ """几何 RoPE(rope_mode="geo"/"geo_vcat")的相位计算器。纯计算、零参数(不进 state_dict)。
493
+ token 的"位置" = 3D 锚点 + 面内类型, 序列顺序完全不参与注意力(模型对面片块置换严格等变)。
494
+
495
+ mode="geo": 锚点用聚合点(旧线 ckpt 在用, 勿删):
496
+ 9/face: F_i/M_ii 锚在父顶点 v_i, M_ij 锚在边中点 (v_i+v_j)/2; 类型 0/1/2=F/intra/inter
497
+ 3/face: 三个组 token 同锚面质心, 类型 = 组 id 0/1/2(与 9/face 同一条语义轴)
498
+ 1/face: 面质心, 类型 = 3
499
+ head_dim/2 个频率槽切 [n,n,n,type_slots](n=(d/2-type_slots)//3, 余数补恒等相位),
500
+ 依序旋转 x/y/z/type(LATO RotaryPositionPhasesEmbedder 的 rotate_half 版)。
501
+ mode="geo_vcat"(2026-08-05, 用户提议): 锚点显式编码顶点, **不用质心/中点等聚合点**。
502
+ 3/face 与 1/face 粗尺度: 「三顶点各占 1/3 通道拼接」(共质心三角形可分):
503
+ xyz 30 槽切 3 组 x 每顶点 9 槽(x/y/z 各 3, 顶点序=面内 zyx 字典序), type 2 槽, 余 3 pad。
504
+ 9/face: 「双端点布局」(治中点混叠: 平行四边形两对角线同中点会被 geo 判成同一条边):
505
+ 每 token 两锚点各占一半 xyz 槽(x/y/z 各 5), F_i/M_ii=(v_i,v_i) 复制, M_ij=(v_i,v_j)
506
+ (zyx 排序 => 端点对全局规范, 共享边跨面同相位); F 与其所属边的 inter 前半通道对齐。
507
+ 频率梯子统一 theta_vcat=300(3 槽/轴最低频 2.85 rad、5 槽/轴 1.33 rad, 均 <π 无歧义,
508
+ 邻格 1 rad 可分辨)。与 geo ckpt 参数完全同构, 但两个尺度的注意力几何都变了 ->
509
+ 热启动 loss 先跳后收(同 flat->geo 的迁移形态)。
510
+
511
+ ���标约定(两种模式共同):
512
+ 锚点用**原始体素坐标**(乘 geo_scale, 默认 1.0), 刻意不按 depth 归一:
513
+ 注意力只见同样本内的相对相位(每样本单一 depth), 而连接决策的几何是体素邻接 ——
514
+ 体素单位让"相邻一格"在所有 depth 下相位签名一致(θ_xyz=100: 邻格 1 rad 可分辨,
515
+ depth7 全程 128 格最低频 2.0 rad < π 无歧义)。
516
+ coord 不减 offset: 常数平移让 q/k 同转一个角, 相对相位(注意力唯一可见量)不变。
517
+ 纯位置编码零新增参数 -> 任意旧 ckpt strict 热启动(注意几何变了, loss 先跳后收)。
518
+ """
519
+
520
+ def __init__(self, mode="flat", geo_scale=1.0, theta_xyz=100.0,
521
+ theta_type=10.0, type_slots=2, theta_vcat=300.0):
522
+ self.mode = mode
523
+ self.geo_scale = float(geo_scale)
524
+ self.theta_xyz = float(theta_xyz)
525
+ self.theta_type = float(theta_type)
526
+ self.type_slots = int(type_slots)
527
+ self.theta_vcat = float(theta_vcat)
528
+
529
+ @property
530
+ def is_geo(self):
531
+ return str(self.mode).startswith("geo")
532
+
533
+ @staticmethod
534
+ def _ladder(theta, n, device):
535
+ """几何频率梯子: freq_i = 1/theta^(i/n), i=0..n-1。"""
536
+ return 1.0 / (theta ** (torch.arange(n, device=device, dtype=torch.float32) / max(n, 1)))
537
+
538
+ @staticmethod
539
+ def _cos_sin(rot, dtype):
540
+ """[N, d_half] 相位 -> ([N,1,head_dim] cos, sin): 频率复制两半配 rotate_half,
541
+ 布局与 RotaryEmbedding.forward_positions 一致, apply_rotary_pos_emb_varlen 原样可用。"""
542
+ emb = torch.cat((rot, rot), dim=-1)
543
+ return emb.cos().to(dtype).unsqueeze(1), emb.sin().to(dtype).unsqueeze(1)
544
+
545
+ def scales(self, coord_packed, d_half, dtype):
546
+ """由 coord 流现算三个 hourglass 尺度的 (cos, sin), 返回 {9:(cos,sin), 3:..., 1:...}。
547
+ 每面 9 个 coord 标量 = 3 父顶点 x 3 分量(面内坐标字典序); 样本长度都是 9 的倍数且
548
+ face 块不跨样本, 所以可以全局 view。"""
549
+ device = coord_packed.device
550
+ V = coord_packed.view(-1, 3, 3).to(torch.float32) * self.geo_scale # [nF, 3顶点, xyz]
551
+ nF = V.shape[0]
552
+ freqs_type = self._ladder(self.theta_type, self.type_slots, device)
553
+ # 面内类型分量(与 token 排布一致): 9/face = F F F | intra x3 | inter x3;
554
+ # 3/face = 组 id 0/1/2(与 9/face 同一条语义轴); 1/face = 3
555
+ type9 = torch.tensor([0., 0., 0., 1., 1., 1., 2., 2., 2.], device=device).repeat(nF)
556
+ type3 = torch.tensor([0., 1., 2.], device=device).repeat(nF)
557
+ type1 = torch.full((nF,), 3.0, device=device)
558
+ if getattr(self, "mode", "geo") == "geo_vcat":
559
+ return self._scales_vcat(V, type9, type3, type1, freqs_type, d_half, dtype, device)
560
+ return self._scales_geo(V, type9, type3, type1, freqs_type, d_half, dtype, device)
561
+
562
+ def _scales_geo(self, V, type9, type3, type1, freqs_type, d_half, dtype, device):
563
+ """geo 布局: [x*n_ax | y*n_ax | z*n_ax | type*type_slots | pad]。"""
564
+ n_type = self.type_slots
565
+ n_ax = (d_half - n_type) // 3
566
+ n_pad = d_half - n_type - 3 * n_ax
567
+ freqs_ax = self._ladder(self.theta_xyz, n_ax, device)
568
+
569
+ nF = V.shape[0]
570
+ mid01 = (V[:, 0] + V[:, 1]) * 0.5
571
+ mid02 = (V[:, 0] + V[:, 2]) * 0.5
572
+ mid12 = (V[:, 1] + V[:, 2]) * 0.5
573
+ centroid = V.mean(dim=1) # [nF,3]
574
+ # 槽位序 = [F0 F1 F2 M00 M11 M22 M01 M02 M12](与 token 排布一致)
575
+ anchors9 = torch.stack([V[:, 0], V[:, 1], V[:, 2],
576
+ V[:, 0], V[:, 1], V[:, 2],
577
+ mid01, mid02, mid12], dim=1) # [nF,9,3]
578
+ anchors3 = centroid[:, None, :].expand(nF, 3, 3) # down_9_to_3 按连续三元折叠成组
579
+ anchors1 = centroid[:, None, :]
580
+
581
+ def build(anchors, type_comp):
582
+ a = anchors.reshape(-1, 3)
583
+ parts = [a[:, 0:1] * freqs_ax, a[:, 1:2] * freqs_ax, a[:, 2:3] * freqs_ax,
584
+ type_comp[:, None] * freqs_type]
585
+ if n_pad:
586
+ parts.append(torch.zeros(a.shape[0], n_pad, device=device, dtype=torch.float32))
587
+ return self._cos_sin(torch.cat(parts, dim=-1), dtype)
588
+
589
+ return {9: build(anchors9, type9), 3: build(anchors3, type3), 1: build(anchors1, type1)}
590
+
591
+ def _scales_vcat(self, V, type9, type3, type1, freqs_type, d_half, dtype, device):
592
+ """geo_vcat: 锚点全部为显式顶点(不算质心/中点)。"""
593
+ n_type = self.type_slots
594
+
595
+ # ---- 粗尺度(3/face 与 1/face) = 三顶点拼接: 每顶点每轴 n_per_vert 槽,
596
+ # 布局 [v0: x,y,z | v1: x,y,z | v2: x,y,z | type | pad]
597
+ n_per_vert = (d_half - n_type) // 9
598
+ n_pad_v = d_half - n_type - 9 * n_per_vert
599
+ freqs_vcat = self._ladder(self.theta_vcat, n_per_vert, device)
600
+
601
+ def build_vcat(type_comp, reps):
602
+ parts_xyz = [V[:, vtx, ax:ax + 1] * freqs_vcat for vtx in range(3) for ax in range(3)]
603
+ rot_face = torch.cat(parts_xyz, dim=-1) # [nF, 9*n_per_vert]
604
+ rot = rot_face.repeat_interleave(reps, dim=0) if reps > 1 else rot_face
605
+ parts = [rot, type_comp[:, None] * freqs_type]
606
+ if n_pad_v:
607
+ parts.append(torch.zeros(rot.shape[0], n_pad_v, device=device, dtype=torch.float32))
608
+ return self._cos_sin(torch.cat(parts, dim=-1), dtype)
609
+
610
+ # ---- 9/face = 双端点布局: 每 token 两个锚点各占一半 xyz 槽
611
+ # [P1: x/y/z 各 n_ax9 | P2: 同 | type n_type], 30=2x15 无 pad。
612
+ # F_i/M_ii -> (v_i, v_i) 复制; M_ij -> (v_i, v_j), i<j(面内 zyx 排序 => 端点对全局规范,
613
+ # 共享边跨面同相位)。免费性质: F 锚 v_a 与 inter 锚 (v_a,v_b) 前半通道完全对齐 ——
614
+ # "顶点是边的端点"在相位空间直接可见。
615
+ n_ax9 = (d_half - n_type) // 6
616
+ n_pad9 = d_half - n_type - 6 * n_ax9
617
+ freqs_9 = self._ladder(self.theta_vcat, n_ax9, device)
618
+ P1 = torch.stack([V[:, 0], V[:, 1], V[:, 2], V[:, 0], V[:, 1], V[:, 2],
619
+ V[:, 0], V[:, 0], V[:, 1]], dim=1).reshape(-1, 3) # [nF*9,3]
620
+ P2 = torch.stack([V[:, 0], V[:, 1], V[:, 2], V[:, 0], V[:, 1], V[:, 2],
621
+ V[:, 1], V[:, 2], V[:, 2]], dim=1).reshape(-1, 3)
622
+ parts9 = [P1[:, 0:1] * freqs_9, P1[:, 1:2] * freqs_9, P1[:, 2:3] * freqs_9,
623
+ P2[:, 0:1] * freqs_9, P2[:, 1:2] * freqs_9, P2[:, 2:3] * freqs_9,
624
+ type9[:, None] * freqs_type]
625
+ if n_pad9:
626
+ parts9.append(torch.zeros(P1.shape[0], n_pad9, device=device, dtype=torch.float32))
627
+ scales9 = self._cos_sin(torch.cat(parts9, dim=-1), dtype)
628
+ return {9: scales9, 3: build_vcat(type3, 3), 1: build_vcat(type1, 1)}
629
+ _COORD_PACK_BASE = 4096 # 坐标打包基数(> 最大体素坐标), xyz -> 单一整数 code
630
+ def _pack_coord(p3):
631
+ """[.,3] 整数 xyz -> 单一整数 code(供 torch.unique 分组用)。"""
632
+ return p3[:, 0] * _COORD_PACK_BASE * _COORD_PACK_BASE + p3[:, 1] * _COORD_PACK_BASE + p3[:, 2]
633
+ class TokenGroup:
634
+ """一个跨面 token 组划分 + 组内表决。
635
+ 字段: pos=该类型 token 的全局位置; group=每 token 的组 id(0..n_group-1);
636
+ face=每 token 所在面; code=父顶点打包坐标(仅 by_parent, 顶点原子揭示要用)。"""
637
+
638
+ def __init__(self, pos, group, n_group, face, code=None):
639
+ self.pos, self.group, self.n_group, self.face, self.code = pos, group, n_group, face, code
640
+
641
+ @classmethod
642
+ def by_parent(cls, types, face_coords, token_type, parent_offset):
643
+ """F/intra 按父顶点分组(同一顶点在不同面的 token 同组)。
644
+ parent_offset: F=0(面内位置 0/1/2 -> 父 v0/v1/v2), intra=3(位置 3/4/5)。"""
645
+ pos = (types == token_type).nonzero(as_tuple=True)[0]
646
+ face = pos // REFINE_POS_PER_FACE
647
+ code = _pack_coord(face_coords[face, (pos % REFINE_POS_PER_FACE) - parent_offset])
648
+ uniq, group = torch.unique(code, return_inverse=True)
649
+ return cls(pos, group, int(uniq.numel()), face, code)
650
+
651
+ @classmethod
652
+ def by_edge(cls, types, face_coords):
653
+ """inter 按「无序父对(=共享边)」分组: 面内位置 6/7/8 -> 父对 (v0,v1)/(v0,v2)/(v1,v2)。
654
+ 父对取 (小,大) => 同一条边跨面同组, 朝向天然规范(无 M vs M^T 问题)。"""
655
+ device = face_coords.device
656
+ pos = (types == TYPE_INTER).nonzero(as_tuple=True)[0]
657
+ face = pos // REFINE_POS_PER_FACE
658
+ local = (pos % REFINE_POS_PER_FACE) - 6 # 0/1/2 -> M01/M02/M12
659
+ pa = torch.tensor([0, 0, 1], device=device)
660
+ pb = torch.tensor([1, 2, 2], device=device)
661
+ code_a = _pack_coord(face_coords[face, pa[local]])
662
+ code_b = _pack_coord(face_coords[face, pb[local]])
663
+ pair = torch.stack([torch.minimum(code_a, code_b), torch.maximum(code_a, code_b)], dim=1)
664
+ uniq, group = torch.unique(pair, dim=0, return_inverse=True)
665
+ return cls(pos, group, int(uniq.shape[0]), face)
666
+
667
+ @staticmethod
668
+ def _mode_of(group, vid, n_group):
669
+ """每组内 vid 的众数(sort+segment, 免 python 循环), 广播回每个成员。"""
670
+ n_vid = int(vid.max().item()) + 1 if vid.numel() else 1
671
+ key, cnt = torch.unique(group.long() * n_vid + vid.long(), return_counts=True)
672
+ g, v = key // n_vid, key % n_vid
673
+ order = torch.argsort(g * (int(cnt.max().item()) + 1) - cnt) # 组内按计数降序
674
+ g, v = g[order], v[order]
675
+ first = torch.ones_like(g, dtype=torch.bool)
676
+ if g.numel() > 1:
677
+ first[1:] = g[1:] != g[:-1]
678
+ mode = torch.zeros(n_group, dtype=torch.long, device=vid.device)
679
+ mode[g[first]] = v[first]
680
+ return mode[group]
681
+
682
+ def vote_bytes(self, vals):
683
+ """F 通道: 每组对占用字节(0..255)计数表决, 返回广播回每 token 的众数字节。"""
684
+ cnt = torch.bincount(self.group * 256 + vals.clamp(0, 255),
685
+ minlength=self.n_group * 256).view(self.n_group, 256)
686
+ return cnt.argmax(1)[self.group]
687
+
688
+ def vote_vec(self, vecs):
689
+ """intra/inter 通道: 8-subcode 向量整体表决。返回 (该 token 是否≠众数, 广播回的众数向量)。"""
690
+ uniq, vid = torch.unique(vecs, dim=0, return_inverse=True)
691
+ mode_vid = self._mode_of(self.group, vid, self.n_group)
692
+ return vid != mode_vid, uniq[mode_vid]
693
+ class _DualCloudCond:
694
+ """一次前向内的点云条件: A=全GT云, B=目标分辨率塌缩云, 按全局 block 序号交替提供 cross-attn KV
695
+ (奇数层->A, 偶数层->B; B 未提供时两套相同 = 原单点云行为; 云为 None 走 null_cond = CFG 无条件分支)。
696
+ 每套 cond = (packed KV [sum(len),d], 每样本 KV 长度, KV max_len)。
697
+ fake_lens 桶 pad 时在 conditioner **之后**补零行(不浪费 Michelangelo 前向)。
698
+ cache: 推理跨步复用编码(采样各步点云不变, miche 逐步重算纯浪费; 数值严格等价), 训练传 None。"""
699
+
700
+ def __init__(self, model, coord_list, fake_lens, device, pc, pc_target, cache=None):
701
+ self._model = model
702
+ self._n_seq = len(coord_list) # 含 fake 序列(null_cond 也要按批展开)
703
+ self._fake_lens = fake_lens
704
+ self._device = device
705
+ key = ("null" if pc is None else "cond", pc_target is None)
706
+ if cache is not None and key in cache:
707
+ self.cond_a, self.cond_b = cache[key]
708
+ return
709
+ self.cond_a = self._encode(pc) # 奇数层(全GT)
710
+ if model.use_pc and (pc_target is not None):
711
+ self.cond_b = self._encode(pc_target) # 偶数层(目标分辨率塌缩)
712
+ else:
713
+ self.cond_b = self.cond_a # 未提供 -> 两套相同(原行为)
714
+ if cache is not None:
715
+ cache[key] = (self.cond_a, self.cond_b)
716
+
717
+ def _encode(self, cloud):
718
+ model, device = self._model, self._device
719
+ if not model.use_pc:
720
+ return None, None, None
721
+ if cloud is None: # CFG 无条件分支
722
+ null = model.null_cond
723
+ kv = null.unsqueeze(0).expand(self._n_seq, -1, -1).reshape(-1, null.size(-1))
724
+ kv_len = torch.full((self._n_seq,), model.null_cond_len, device=device, dtype=torch.long)
725
+ return kv, kv_len, int(model.null_cond_len)
726
+ emb = model.conditioner(cloud.to(device=device))
727
+ if self._fake_lens: # 假序列的 cross-attn KV = 零行
728
+ emb = torch.cat([emb, emb.new_zeros(len(self._fake_lens), emb.size(1), emb.size(2))], dim=0)
729
+ kv_len = torch.full((emb.size(0),), emb.size(1), device=device, dtype=torch.long)
730
+ return emb.reshape(-1, emb.size(-1)), kv_len, int(emb.size(1))
731
+
732
+ def for_layer(self, gi):
733
+ """gi: 0-based 全局 block 序号; 层号=gi+1 -> 奇数层(gi 偶)用 A, 偶数层(gi 奇)用 B。"""
734
+ return self.cond_a if gi % 2 == 0 else self.cond_b
735
+ class _ReMDM:
736
+ """ReMDM(arXiv 2503.00307, 训练免费): 步首以 σ_t 重掩已定 token, 本步前向重新预测;
737
+ 揭示额度按 n_keep_masked 自动补足 -> 掩码数轨迹严格贴原 cosine 调度(边际保持的计数空间等价)。
738
+ σ_max=min(1, m_s/|C|); cap: σ=min(η,σ_max); rescale: σ=η·σ_max; conf: cap 的量+定向重掩
739
+ 提交置信度最低者。env 门控(sample 调用时读取), REMDM 为空 = 与原行为逐位一致。"""
740
+
741
+ def __init__(self):
742
+ self.mode = os.environ.get("REMDM", "").strip().lower()
743
+ self.eta = float(os.environ.get("REMDM_ETA", "0.05"))
744
+ self.t_start = float(os.environ.get("REMDM_TSTART", "0.0"))
745
+ self.t_end = float(os.environ.get("REMDM_TEND", "1.0"))
746
+
747
+ def maybe_remask(self, step, T_infer, total, decided, subcodes, commit_conf, device):
748
+ """就地重掩(decided/subcodes 原位改写)。"""
749
+ if not (self.mode and step > 0 and self.t_start <= step / T_infer <= self.t_end):
750
+ return
751
+ n_committed = int(decided.sum().item())
752
+ if n_committed == 0:
753
+ return
754
+ n_mask_target = int(cosine_schedule(torch.tensor(float(step + 1)), float(T_infer)).item() * total)
755
+ sigma_max = min(1.0, n_mask_target / n_committed)
756
+ sigma = self.eta * sigma_max if self.mode == "rescale" else min(self.eta, sigma_max)
757
+ n_remask = int(sigma * n_committed)
758
+ if n_remask <= 0:
759
+ return
760
+ committed_idx = decided.nonzero(as_tuple=True)[0]
761
+ if self.mode == "conf":
762
+ remask_idx = committed_idx[commit_conf[committed_idx].argsort()[:n_remask]] # 最低提交置信优先
763
+ else:
764
+ remask_idx = committed_idx[torch.randperm(n_committed, device=device)[:n_remask]]
765
+ decided[remask_idx] = False
766
+ subcodes[remask_idx] = MASK_ID
767
+ class FactorizedDiffusionTransformerAltPC(FactorizedDiffusionTransformer):
768
+ def __init__(self, *args, rope_mode="flat", rope_geo_scale=1.0,
769
+ rope_theta_xyz=100.0, rope_theta_type=10.0, rope_type_slots=2,
770
+ rope_theta_vcat=300.0, **kwargs):
771
+ super().__init__(*args, **kwargs)
772
+ # 几何 RoPE 封装在 GeoRoPE(纯计算无参数, 不影响 state_dict)。
773
+ # rope_mode="flat"(默认): 原行为, 逐 token 序列位置 0..L-1, 与所有旧 ckpt 完全兼容;
774
+ # "geo"/"geo_vcat" 的设计与 theta 约束见 GeoRoPE docstring。
775
+ self.geo_rope = GeoRoPE(mode=rope_mode, geo_scale=rope_geo_scale,
776
+ theta_xyz=rope_theta_xyz, theta_type=rope_theta_type,
777
+ type_slots=rope_type_slots, theta_vcat=rope_theta_vcat)
778
+
779
+ # rope_* 属性代理到 GeoRoPE(单一事实源): 外部读写 self.rope_mode 等保持兼容
780
+ # (含子类/脚本事后改 rope_mode 的用法, 如 tssr 的 self.rope_mode = "hier")。
781
+ rope_mode = property(lambda self: self.geo_rope.mode,
782
+ lambda self, v: setattr(self.geo_rope, "mode", v))
783
+ rope_geo_scale = property(lambda self: self.geo_rope.geo_scale,
784
+ lambda self, v: setattr(self.geo_rope, "geo_scale", float(v)))
785
+ rope_theta_xyz = property(lambda self: self.geo_rope.theta_xyz,
786
+ lambda self, v: setattr(self.geo_rope, "theta_xyz", float(v)))
787
+ rope_theta_type = property(lambda self: self.geo_rope.theta_type,
788
+ lambda self, v: setattr(self.geo_rope, "theta_type", float(v)))
789
+ rope_type_slots = property(lambda self: self.geo_rope.type_slots,
790
+ lambda self, v: setattr(self.geo_rope, "type_slots", int(v)))
791
+ rope_theta_vcat = property(lambda self: self.geo_rope.theta_vcat,
792
+ lambda self, v: setattr(self.geo_rope, "theta_vcat", float(v)))
793
+
794
+ def _geo_rope_scales(self, coord_packed, dtype):
795
+ """geo/geo_vcat: 三个 hourglass 尺度的 (cos,sin)。方法名/签名保留(fpsq 子类在调用)。"""
796
+ return self.geo_rope.scales(coord_packed, self.rotary_emb.inv_freq.numel(), dtype)
797
+
798
+ @staticmethod
799
+ def _static_pad_plan(total, b_real):
800
+ """返回假序列长度列表(都是 9 的倍数且 >=9): padded 总数落到桶位、批槽位补满 _STATIC_PAD_SLOTS。
801
+ 超过最大桶位时返回 None(不 pad, 触发一次形状特化编译; 配 hard_max_positions 应当罕见)。"""
802
+ n_fake = max(_STATIC_PAD_SLOTS - b_real, 1)
803
+ min_pad = 9 * n_fake
804
+ target = next((B for B in _STATIC_PAD_BUCKETS if B >= total + min_pad), None)
805
+ if target is None:
806
+ return None
807
+ fake = [9] * n_fake
808
+ fake[0] += target - total - min_pad # 桶位/总数都是 9 的倍数 -> 余量也是
809
+ return fake
810
+
811
+ def forward_varlen(self, coord_list, subcode_list, t, depth, pc=None, pc_target=None,
812
+ cond_cache=None):
813
+ device = t.device
814
+ coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
815
+ subcode_list = [s.to(device=device, dtype=torch.long) for s in subcode_list]
816
+ # 长度在 python 端本来就知道 -> 一并算出各尺度 max_seqlen 传给 attention,
817
+ # 省掉每个 flash 调用的 int(lengths.max().item()) GPU->CPU 同步(见 model_acc_fix3_tssr)。
818
+ lengths_py = [int(c.numel()) for c in coord_list]
819
+ if sum(lengths_py) == 0:
820
+ raise ValueError("forward_varlen received an empty batch.")
821
+ if any(n % REFINE_POS_PER_FACE != 0 for n in lengths_py):
822
+ raise ValueError("每个序列长度必须是 9 的倍数(每 face 9 个 refine 位置)。")
823
+
824
+ # ---- 静态桶化 padding(见文件顶部说明; STATIC_PAD_BUCKETS 为空 = 原行为) ----
825
+ real_total, real_b = sum(lengths_py), len(lengths_py)
826
+ fake_lens = self._static_pad_plan(real_total, real_b) if _STATIC_PAD_BUCKETS else None
827
+ if fake_lens:
828
+ sub_tail = tuple(subcode_list[0].shape[1:])
829
+ for L in fake_lens:
830
+ coord_list.append(torch.zeros(L, dtype=torch.long, device=device))
831
+ subcode_list.append(torch.zeros((L,) + sub_tail, dtype=torch.long, device=device))
832
+ lengths_py = lengths_py + fake_lens
833
+ lengths = torch.tensor(lengths_py, device=device, dtype=torch.long)
834
+ if fake_lens:
835
+ # pad 模式: max_len 传桶位常数(= padded 总数, 合法上界), 保证编译形状/guard 全静态
836
+ max_len_9 = sum(lengths_py)
837
+ else:
838
+ max_len_9 = max(lengths_py)
839
+ max_len_3 = max_len_9 // 3 # hourglass 整除, 断言在上面
840
+ max_len_1 = max_len_9 // 9
841
+
842
+ coord_packed = torch.cat(coord_list, dim=0)
843
+ subcode_packed = torch.cat(subcode_list, dim=0)
844
+ types = self.types_from_lengths(lengths)
845
+
846
+ raw_emb = self.raw_embedding(coord_packed)
847
+ refine_emb = self.embed_refine(subcode_packed, types)
848
+ x = raw_emb + refine_emb # 不再加法注入 time;time/depth 走 adaLN
849
+ # 统一残差流 dtype(理由见文件顶部 _BF16_RESIDUAL 注释); c_sample 刻意保持 fp32(adaLN 惯例)
850
+ if _BF16_RESIDUAL and torch.is_autocast_enabled():
851
+ x = x.to(torch.get_autocast_dtype("cuda"))
852
+
853
+ # adaLN 条件 c = time_emb + depth_emb(逐样本 [b,d]); 各 hourglass 阶段按其长度展开到逐 token
854
+ depth = depth.to(device=device, dtype=torch.long)
855
+ c_sample = self.time_mlp(t) + self.depth_embedding(depth) # [b, d_model]
856
+ if fake_lens: # 假序列的 adaLN 条件 = 零行
857
+ c_sample = torch.cat([c_sample, c_sample.new_zeros(len(fake_lens), c_sample.size(-1))], dim=0)
858
+
859
+ def c_per_token(cur_lengths):
860
+ return c_sample.repeat_interleave(cur_lengths, dim=0)
861
+
862
+ # 点云条件: 双云编码 + 逐层交替选择(见 _DualCloudCond)
863
+ cond = _DualCloudCond(self, coord_list, fake_lens, device, pc, pc_target, cond_cache)
864
+
865
+ # geo 模式: 三个尺度的 cos/sin 一次算好(锚点只依赖 coord/depth, 与序列位置无关)
866
+ geo_scales = (self._geo_rope_scales(coord_packed, x.dtype)
867
+ if str(getattr(self, "rope_mode", "flat")).startswith("geo") else None)
868
+
869
+ def rope_fn_for(cur_lengths, tokens_per_face=9):
870
+ if geo_scales is not None:
871
+ cos, sin = geo_scales[tokens_per_face]
872
+ else:
873
+ position_ids = lengths_to_position_ids(cur_lengths)
874
+ cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
875
+ return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)
876
+
877
+ # ---- hourglass 五段: enc9 -> enc3 -> bottleneck -> dec3 -> dec9 ----
878
+ gi = 0 # 全局 block 序号(横跨五段, 决定该层注入云A还是云B)
879
+
880
+ def run_stage(x, blocks, c_stage, stage_lengths, rope_fn, stage_max_len):
881
+ nonlocal gi
882
+ for block in blocks:
883
+ cond_kv, cond_kv_len, cond_kv_max = cond.for_layer(gi)
884
+ gi += 1
885
+ x = block.forward_varlen(x, c_stage, stage_lengths, cond_kv, cond_kv_len,
886
+ rope_apply_fn=rope_fn, max_len=stage_max_len,
887
+ cond_max_len=cond_kv_max)
888
+ return x
889
+
890
+ rope_fn_9, c9 = rope_fn_for(lengths, 9), c_per_token(lengths)
891
+ x = run_stage(x, self.enc_blocks_9, c9, lengths, rope_fn_9, max_len_9)
892
+ skip_9 = x
893
+
894
+ x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
895
+ rope_fn_3, c3 = rope_fn_for(lengths, 3), c_per_token(lengths)
896
+ x = run_stage(x, self.enc_blocks_3, c3, lengths, rope_fn_3, max_len_3)
897
+ skip_3 = x
898
+
899
+ x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
900
+ rope_fn_1, c1 = rope_fn_for(lengths, 1), c_per_token(lengths)
901
+ x = run_stage(x, self.bottleneck_blocks, c1, lengths, rope_fn_1, max_len_1)
902
+
903
+ x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
904
+ x = run_stage(x + skip_3, self.dec_blocks_3, c3, lengths, rope_fn_3, max_len_3)
905
+
906
+ x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
907
+ x = run_stage(x + skip_9, self.dec_blocks_9, c9, lengths, rope_fn_9, max_len_9)
908
+
909
+ if fake_lens: # 切掉假位置(不进头/loss)
910
+ x, types = x[:real_total], types[:real_total]
911
+ return x, types # hidden [total,d], types [total]
912
+
913
+ def forward(self, coord_list, noisy_subcode_list, t, depth,
914
+ target_subcode_list=None, loss_mask=None, pc=None, pc_target=None, return_extra_acc=False):
915
+ """训练入口, 与基类同, 只多传 pc_target(逐层交替的第二套点云)。"""
916
+ h, types = self.forward_varlen(coord_list, noisy_subcode_list, t, depth, pc, pc_target)
917
+ if target_subcode_list is None:
918
+ return h, types
919
+ target = torch.cat([s.to(device=h.device, dtype=torch.long) for s in target_subcode_list], dim=0)
920
+ loss_mask = loss_mask.to(device=h.device)
921
+ loss = self.factorized_loss(h, target, types, loss_mask) # focal(继承)
922
+ with torch.no_grad():
923
+ acc, acc_ne = self.factorized_accuracy(h, target, types, loss_mask)
924
+ if return_extra_acc:
925
+ return loss, acc, acc_ne
926
+ return loss, acc
927
+
928
+ def _predict_heads(self, h, h_u, types, subcodes, cfg_t, temperature):
929
+ """三个头对全部位置出预测 + 置信度: F=256 类单分类(整块 9 位广播同值),
930
+ intra/inter=8 行自回归 rollout。h_u 非 None 时做 logits 级 CFG。返回 (pred, conf)。"""
931
+ pred = subcodes.clone()
932
+ conf = torch.zeros(types.numel(), device=h.device)
933
+ is_f = types == TYPE_F
934
+ if is_f.any():
935
+ logits = self.f_head(h[is_f])
936
+ if h_u is not None:
937
+ logits_u = self.f_head(h_u[is_f])
938
+ logits = logits_u + cfg_t * (logits - logits_u)
939
+ if temperature and temperature > 0:
940
+ prob = (logits / temperature).softmax(-1)
941
+ choice = torch.multinomial(prob.float(), 1).squeeze(-1)
942
+ choice_prob = prob.gather(-1, choice.unsqueeze(-1)).squeeze(-1)
943
+ else:
944
+ prob = logits.softmax(-1)
945
+ choice_prob, choice = prob.max(-1)
946
+ pred[is_f, :] = choice.unsqueeze(1).expand(-1, NUM_OCTANTS)
947
+ conf[is_f] = choice_prob.to(conf.dtype)
948
+ for token_type, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
949
+ sel = types == token_type
950
+ if not sel.any():
951
+ continue
952
+ row_pred, row_conf = head.rollout(h[sel], h_u[sel] if h_u is not None else None, cfg_t, temperature)
953
+ pred[sel] = row_pred
954
+ conf[sel] = row_conf.mean(dim=1).to(conf.dtype)
955
+ return pred, conf
956
+
957
+ @torch.no_grad()
958
+ def sample(self, coord_list, depth, pc=None, pc_target=None, T_infer=10, cfg_scale=1.0,
959
+ temperature=0.0, cfg_schedule="constant", cfg_start=1.0,
960
+ align_fm=False, align_inter=False, align_per_step=False, align_debug=True,
961
+ diag_vertex=False):
962
+ """前向多传 pc_target(逐层交替)。CFG 无条件分支两套点云都走 null_cond。
963
+
964
+ 跨面一致性对齐(GT 侧这些量对共享结构 100% 一致, 见 check_tokenizer_consistency):
965
+ align_fm=True : 对齐 F(占用字节, 按"共享父"分组) + intra(M_inner 8-subcode 矩阵, 按"共享父")。
966
+ align_inter=True : 对齐 inter(M_outer 8-subcode 矩阵, 按"共享父对"=共享边分组)。
967
+ 如面 (v0,v1,v2) 与 (v0,v1,v3) 共享边 (v0,v1) -> 其 (v0,v1) 的 inter 应一致。
968
+ align_per_step : False(默认, 推荐)= 只在收尾把各启用通道投影到表决值(不扰动去噪, 不造洞);
969
+ True = 每步额外把"不一致的面"整块 9 token 重掩重预测(实测打乱揭示节奏 ->
970
+ 反而多洞, 仅供对照)。
971
+ 全 False -> 与原行为逐位一致。
972
+
973
+ env 门控(为空/未设 = 与原行为逐位一致):
974
+ REMDM=cap|rescale|conf (+REMDM_ETA/TSTART/TEND): 步首重掩已定 token(见 _ReMDM)。
975
+ WINDOW_REVEAL=1(仅 batch=1): BFS 前缀窗内 token 才可提交(块因果 变体A), 注意力/预测不变。
976
+ """
977
+ self.eval()
978
+ device = next(self.parameters()).device
979
+ coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
980
+ depth = depth.to(device=device, dtype=torch.long)
981
+ lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
982
+ types = self.types_from_lengths(lengths)
983
+ want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and self.use_pc and (pc is not None)
984
+
985
+ total = int(lengths.sum().item())
986
+ nface = total // REFINE_POS_PER_FACE
987
+
988
+ align_any = align_fm or align_inter
989
+ if align_any or diag_vertex:
990
+ face_coords = torch.cat(coord_list).view(nface, 3, 3) # [nface, 3父, xyz]
991
+
992
+ # ---- 顶点组诊断: 同一父(顶点)在不同面的 F/intra token, 记录 commit step, 末尾统计一致性 ----
993
+ if diag_vertex:
994
+ diag_f = TokenGroup.by_parent(types, face_coords, TYPE_F, 0)
995
+ diag_i = TokenGroup.by_parent(types, face_coords, TYPE_INTRA, 3)
996
+ commit_step = torch.full((total,), int(T_infer), dtype=torch.long, device=device)
997
+
998
+ # ---- 跨面对齐分组: F/intra 按"共享父", inter 按"共享父对"(=共享边) ----
999
+ if align_fm:
1000
+ grp_f = TokenGroup.by_parent(types, face_coords, TYPE_F, 0)
1001
+ grp_i = TokenGroup.by_parent(types, face_coords, TYPE_INTRA, 3)
1002
+ if align_inter:
1003
+ grp_e = TokenGroup.by_edge(types, face_coords)
1004
+
1005
+ subcodes = torch.full((total, NUM_OCTANTS), MASK_ID, dtype=torch.long, device=device)
1006
+ decided = torch.zeros(total, dtype=torch.bool, device=device)
1007
+ pred = subcodes.clone()
1008
+
1009
+ remdm = _ReMDM() # env 门控, 未设=无操作
1010
+ window_reveal = os.environ.get("WINDOW_REVEAL", "0") == "1" and len(lengths) == 1
1011
+ commit_conf = torch.zeros(total, device=device) # 每 token 提交时的置信度(ReMDM-conf 用)
1012
+ cond_cache = {} # 点云条件跨步缓存(严格等价, 见 _DualCloudCond)
1013
+
1014
+ for step in range(T_infer):
1015
+ remdm.maybe_remask(step, T_infer, total, decided, subcodes, commit_conf, device)
1016
+
1017
+ if cfg_schedule == "linear":
1018
+ cfg_t = float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(T_infer - 1, 1)
1019
+ else:
1020
+ cfg_t = float(cfg_scale)
1021
+ use_cfg = want_cfg and (cfg_t != 1.0)
1022
+
1023
+ # ---- 前向(有条件 + 可选无条件 CFG 分支) -> 三头全量预测 ----
1024
+ t = torch.full((len(coord_list),), step / T_infer, device=device, dtype=torch.float)
1025
+ sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
1026
+ h, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, cond_cache=cond_cache)
1027
+ h_u = self.forward_varlen(coord_list, sub_list, t, depth, None, None, cond_cache=cond_cache)[0] if use_cfg else None
1028
+ h = h.float() # 头部权重 fp32; bf16 残差模式下需 cast(fp32 下 no-op)
1029
+ if h_u is not None:
1030
+ h_u = h_u.float()
1031
+ pred, conf = self._predict_heads(h, h_u, types, subcodes, cfg_t, temperature)
1032
+
1033
+ # ---- 每步重掩(align_per_step): 跨面不一致的面整块 9 token 重掩(本步不揭示+撤销已定)。
1034
+ # 检测 F/intra(按共享父)与 inter(按共享父对)的不一致; 任一通道不一致的面都重掩。实测会造洞, 默认关。
1035
+ if align_per_step and align_any:
1036
+ force_face = torch.zeros(nface, dtype=torch.bool, device=device)
1037
+ if align_fm:
1038
+ if grp_f.pos.numel():
1039
+ bad = pred[grp_f.pos, 0] != grp_f.vote_bytes(pred[grp_f.pos, 0])
1040
+ force_face[grp_f.face[bad]] = True
1041
+ if grp_i.pos.numel():
1042
+ dis_intra, _ = grp_i.vote_vec(pred[grp_i.pos])
1043
+ force_face[grp_i.face[dis_intra]] = True
1044
+ if align_inter and grp_e.pos.numel():
1045
+ dis_inter, _ = grp_e.vote_vec(pred[grp_e.pos])
1046
+ force_face[grp_e.face[dis_inter]] = True
1047
+ remask = force_face.repeat_interleave(REFINE_POS_PER_FACE)
1048
+ conf[remask] = -float('inf')
1049
+ decided = decided & (~remask)
1050
+
1051
+ # ---- 按 cosine 调度揭示 top-k 置信 token ----
1052
+ keep_ratio = cosine_schedule(torch.tensor(float(step + 1)), float(T_infer)).item()
1053
+ n_keep_masked = int(keep_ratio * total)
1054
+ n_to_decide = max(0, (total - n_keep_masked) - int(decided.sum().item()))
1055
+ if n_to_decide > 0:
1056
+ undecided_idx = (~decided).nonzero(as_tuple=True)[0]
1057
+ if window_reveal:
1058
+ # 块序前沿窗口(推理期块因果 变体A): 仅允许 BFS 前缀窗内的 token 本步被提交。
1059
+ # 注意力/预测不变(coord 脚手架全局可见), 只限制提交资格; 窗内不够本步预算时自适应扩窗。
1060
+ n_blocks = total // 9
1061
+ win_blocks = max(1, int(np.ceil(n_blocks * (step + 1) / float(T_infer))))
1062
+ while True:
1063
+ in_window = undecided_idx[undecided_idx < win_blocks * 9]
1064
+ if len(in_window) >= n_to_decide or win_blocks >= n_blocks:
1065
+ break
1066
+ win_blocks += max(1, (n_to_decide - len(in_window) + 8) // 9)
1067
+ undecided_idx = undecided_idx[undecided_idx < win_blocks * 9]
1068
+ order = conf[undecided_idx].argsort(descending=True)
1069
+ chosen = undecided_idx[order[:n_to_decide]]
1070
+ subcodes[chosen] = pred[chosen]
1071
+ decided[chosen] = True
1072
+ commit_conf[chosen] = conf[chosen]
1073
+ if diag_vertex:
1074
+ commit_step[chosen] = step
1075
+ subcodes[~decided] = MASK_ID
1076
+
1077
+ subcodes[~decided] = pred[~decided]
1078
+ if diag_vertex:
1079
+ self._diag_vertex_report(subcodes, commit_step, diag_f.pos, diag_f.group,
1080
+ diag_i.pos, diag_i.group, int(T_infer))
1081
+
1082
+ # ---- 末步: 把各启用通道强制对齐到表决值(不扰动去噪, 推荐的对齐方式)。
1083
+ # align_debug=True: 打印每通道"与表决值不一致(即被改动)的 token 数 / 涉及面数"便于 debug。
1084
+ def report(tag, disagree, face_of):
1085
+ n_tok = int(disagree.sum().item())
1086
+ n_all = disagree.numel()
1087
+ n_face = int(torch.unique(face_of[disagree]).numel()) if n_tok else 0
1088
+ print(f"[align_debug] {tag:5s}: 不一致 {n_tok}/{n_all} token ({100 * n_tok / max(n_all, 1):.1f}%), 涉及 {n_face} 面")
1089
+
1090
+ if align_fm:
1091
+ if grp_f.pos.numel():
1092
+ voted_f = grp_f.vote_bytes(subcodes[grp_f.pos, 0])
1093
+ if align_debug:
1094
+ report("F", subcodes[grp_f.pos, 0] != voted_f, grp_f.face)
1095
+ subcodes[grp_f.pos, :] = voted_f.unsqueeze(1).expand(-1, NUM_OCTANTS)
1096
+ if grp_i.pos.numel():
1097
+ dis_intra, voted_intra = grp_i.vote_vec(subcodes[grp_i.pos])
1098
+ if align_debug:
1099
+ report("intra", dis_intra, grp_i.face)
1100
+ subcodes[grp_i.pos, :] = voted_intra
1101
+ if align_inter and grp_e.pos.numel():
1102
+ dis_inter, voted_inter = grp_e.vote_vec(subcodes[grp_e.pos])
1103
+ if align_debug:
1104
+ report("inter", dis_inter, grp_e.face)
1105
+ subcodes[grp_e.pos, :] = voted_inter
1106
+ return list(torch.split(subcodes, lengths.tolist(), dim=0))
1107
+
1108
+
1109
+ @torch.no_grad()
1110
+ def _diag_vertex_report(self, subcodes, commit_step, f_pos, f_grp, i_pos, i_grp, T):
1111
+ """诊断: 同一父(顶点)在不同面的 F/intra token 是否一致, 以及不一致的时序来源。
1112
+ 关键问题——不一致是(a)同步揭示时就分裂, 还是(b)已有同组 token commit(可见上下文)后仍预测错?"""
1113
+ def analyze(pos, grp, tag, is_vec):
1114
+ if pos.numel() == 0:
1115
+ print(f"[diag {tag}] 无 token")
1116
+ return
1117
+ if is_vec: # intra: 8-subcode 向量 -> 唯一 id
1118
+ _, vid = torch.unique(subcodes[pos], dim=0, return_inverse=True)
1119
+ vals = vid.cpu().numpy()
1120
+ else: # F: 占用字节
1121
+ vals = subcodes[pos, 0].clamp(0, 255).cpu().numpy()
1122
+ groups = grp.cpu().numpy()
1123
+ steps = commit_step[pos].cpu().numpy()
1124
+ order = np.argsort(groups, kind="stable")
1125
+ groups, vals, steps = groups[order], vals[order], steps[order]
1126
+ segs = np.split(np.arange(len(groups)), np.flatnonzero(np.diff(groups)) + 1)
1127
+ n_multi = n_incons = 0
1128
+ post_total = post_dis = 0 # 后揭示事件(同组已有更早 commit): 与已定值 一致/不一致
1129
+ coreveal = postcommit = both = 0 # 不一致组来源分类
1130
+ spreads = []
1131
+ for seg in segs:
1132
+ if len(seg) < 2:
1133
+ continue
1134
+ n_multi += 1
1135
+ v, s = vals[seg], steps[seg]
1136
+ inconsistent = len(set(v.tolist())) > 1
1137
+ first_step = int(s.min())
1138
+ early_vals = v[s == first_step]
1139
+ ref = np.bincount(early_vals).argmax() # 最早 step 的众数值作参照
1140
+ coreveal_split = len(set(early_vals.tolist())) > 1
1141
+ later = v[s > first_step]
1142
+ post_total += len(later)
1143
+ post_dis += int((later != ref).sum())
1144
+ has_post = bool((later != ref).any())
1145
+ if inconsistent:
1146
+ n_incons += 1
1147
+ spreads.append(int(s.max() - s.min()))
1148
+ if coreveal_split and has_post:
1149
+ both += 1
1150
+ elif coreveal_split:
1151
+ coreveal += 1
1152
+ else:
1153
+ postcommit += 1
1154
+ pct_dis = 100 * post_dis / max(post_total, 1)
1155
+ med = int(np.median(spreads)) if spreads else 0
1156
+ print(f"[diag {tag}] 多面顶点组 {n_multi} | 最终不一致 {n_incons} ({100*n_incons/max(n_multi,1):.1f}%)")
1157
+ print(f" 后揭示token(同组已有commit) {post_total}: 不一致 {post_dis} ({pct_dis:.1f}%) <- 看到已定值仍预测错的比例")
1158
+ print(f" 不一致组来源: 同步揭示分裂 {coreveal} / 后揭示才分裂 {postcommit} / 两者 {both} | commit step 跨度中位 {med}/{T}")
1159
+
1160
+ analyze(f_pos, f_grp, "F", False)
1161
+ analyze(i_pos, i_grp, "intra", True)
1162
+
1163
+ # ==========================================================================
1164
+ # 原 model_acc_fix3_tssr.py
1165
+ # ==========================================================================
1166
+
1167
+ _BF16_RESIDUAL = os.environ.get("BF16_RESIDUAL", "1") == "1"
1168
+ class FactorizedTSSRTransformer(FactorizedDiffusionTransformerAltPC):
1169
+ def __init__(self, *args, mode_embed_scale=1000.0, rope_mode="flat",
1170
+ rope_theta_face=10000.0, rope_theta_group=10000.0, rope_theta_member=10000.0, **kwargs):
1171
+ super().__init__(*args, **kwargs)
1172
+ # ---- 层级 RoPE 开关(移植自 Tencent-TSSR MultiLevelRoPE, 适配本仓库 rotate_half 约定) ----
1173
+ # rope_mode="flat"(默认): 原行为, 逐 token 平位置 0..L-1, 与所有旧 ckpt 完全兼容。
1174
+ # rope_mode="hier" : head 维的 d/2 个频率槽切三段, 分别用三个结构分量旋转:
1175
+ # face 段: p // tokens_per_face (第几个面; 长程)
1176
+ # group 段: 组内三元组编号 (9/face 时 (p%9)//3: 0=F,1=intra,2=inter)
1177
+ # member 段: 三元组内成员 (9/face 时 p%3: 顶点/边槽位, F0↔M00 同相位)
1178
+ # hourglass 三个尺度(9/3/1 token per face)各有对应分解(对齐 TSSR 的 token/vertex/face 三级)。
1179
+ # 纯位置编码无参数 -> 任意旧 ckpt 可直接热启动, 但注意几何变了, loss 会先跳再收。
1180
+ self.rope_mode = rope_mode
1181
+ self.rope_thetas = (float(rope_theta_face), float(rope_theta_group), float(rope_theta_member))
1182
+ d = self.d_model
1183
+ # 模式 flag 嵌入:标量 mode∈{0,1} -> 正弦 -> MLP -> [b,d](仿 TSSR DiscreteValueEmbeding)。
1184
+ # scale 大让 0 与 1 的正弦特征清晰可分;末层零初始化��初始 mode token≈0(warm-start 不破坏骨干)。
1185
+ self.mode_emb = nn.Sequential(
1186
+ SinusoidalPosEmb(d, scale=mode_embed_scale),
1187
+ nn.Linear(d, d), nn.SiLU(),
1188
+ nn.Linear(d, d),
1189
+ )
1190
+ nn.init.zeros_(self.mode_emb[-1].weight)
1191
+ nn.init.zeros_(self.mode_emb[-1].bias)
1192
+ # 逐 token 正确性分类器(detach 骨干)
1193
+ self.to_acc = nn.Sequential(
1194
+ nn.Linear(d, d // 2), nn.SiLU(),
1195
+ nn.Linear(d // 2, d // 4), nn.SiLU(),
1196
+ nn.Linear(d // 4, 1),
1197
+ )
1198
+
1199
+ def _hier_cos_sin(self, position_ids, tokens_per_face, dtype):
1200
+ """层级 RoPE 的 cos/sin, 形状/布局与 RotaryEmbedding.forward_positions 一致([total, head_dim],
1201
+ 频率复制两半以配 rotate_half), 因此 apply_rotary_pos_emb_varlen 原样可用。
1202
+ d/2 个频率槽按 TSSR 方式切 [n, n, d/2-2n](n=d//6), 依序分给 face/group/member 分量。"""
1203
+ device = position_ids.device
1204
+ d_half = self.rotary_emb.inv_freq.numel() # = head_dim // 2
1205
+ n = d_half // 3
1206
+ chunk_sizes = (n, n, d_half - 2 * n)
1207
+ p = position_ids
1208
+ if tokens_per_face == 9:
1209
+ comps = (p // 9, (p % 9) // 3, p % 3)
1210
+ elif tokens_per_face == 3:
1211
+ comps = (p // 3, p % 3, torch.zeros_like(p))
1212
+ elif tokens_per_face == 1:
1213
+ comps = (p, torch.zeros_like(p), torch.zeros_like(p))
1214
+ else:
1215
+ raise ValueError(f"tokens_per_face 只支持 9/3/1, 收到 {tokens_per_face}")
1216
+ rotations = []
1217
+ for size, theta, comp in zip(chunk_sizes, self.rope_thetas, comps):
1218
+ freqs = 1.0 / (theta ** (torch.arange(size, device=device, dtype=torch.float32) / max(size, 1)))
1219
+ rotations.append(comp.to(torch.float32)[:, None] * freqs[None, :])
1220
+ rot = torch.cat(rotations, dim=-1) # [total, d_half]
1221
+ emb = torch.cat((rot, rot), dim=-1) # [total, head_dim], 同 rotate_half 布局
1222
+ # [total, 1, head_dim]: 中间加 head 广播维, 与 forward_positions 的返回形状一致
1223
+ # (q/k 是 [total, nhead, d_head], 少了这一维会把 total 广播到 nhead 上直接报形状错)
1224
+ return emb.cos().to(dtype).unsqueeze(1), emb.sin().to(dtype).unsqueeze(1)
1225
+
1226
+ # ------------------------------------------------------------------ #
1227
+ # forward_varlen:复制 AltPC 版,改动 = ①mode token 拼点云 context ②rope_mode=hier 时层级 RoPE
1228
+ # ------------------------------------------------------------------ #
1229
+ def forward_varlen(self, coord_list, subcode_list, t, depth, pc=None, pc_target=None, mode=None):
1230
+ device = t.device
1231
+ coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
1232
+ subcode_list = [s.to(device=device, dtype=torch.long) for s in subcode_list]
1233
+ # 长度在 python 端本来就知道 -> 一并算出各尺度的 max_seqlen, 传给 attention 省掉
1234
+ # 每次 flash 调用的 int(lengths.max().item()) 同步(一次前向 72 次, 见 ModernAttention 说明)。
1235
+ lengths_py = [int(c.numel()) for c in coord_list]
1236
+ if sum(lengths_py) == 0:
1237
+ raise ValueError("forward_varlen received an empty batch.")
1238
+ if any(n % REFINE_POS_PER_FACE != 0 for n in lengths_py):
1239
+ raise ValueError("每个序列长度必须是 9 的倍数(每 face 9 个 refine 位置)。")
1240
+ lengths = torch.tensor(lengths_py, device=device, dtype=torch.long)
1241
+ max_len_9 = max(lengths_py)
1242
+ max_len_3 = max_len_9 // 3 # hourglass: lengths // 3, 整除有断言保证
1243
+ max_len_1 = max_len_9 // 9
1244
+
1245
+ coord_packed = torch.cat(coord_list, dim=0)
1246
+ subcode_packed = torch.cat(subcode_list, dim=0)
1247
+ types = self.types_from_lengths(lengths)
1248
+
1249
+ raw_emb = self.raw_embedding(coord_packed)
1250
+ refine_emb = self.embed_refine(subcode_packed, types)
1251
+ x = raw_emb + refine_emb
1252
+
1253
+ # nn.Embedding 不在 autocast 的转型名单里 -> 上面的 x 是 fp32。不显式降下来的话:
1254
+ # ① enc_blocks_9 / dec_blocks_9(经 skip_9 带回)这 4 个块的残差流全程 fp32 —— 而它们
1255
+ # 正是 token 数最多的一档(hourglass 把其余降到 1/3、1/9), RMSNorm/modulate/残差加
1256
+ # 在最大的激活上按 fp32 跑, 内存流量白翻一倍;
1257
+ # ② rope_fn_9 是按 `dtype=x.dtype` 建 cos/sin 的, x 是 fp32 就把 bf16 的 Q/K 提升回 fp32,
1258
+ # 导致 can_use_flash_attn 判否、掉进 SDPA 逐序列 python 循环(见 apply_rotary_pos_emb_varlen)。
1259
+ # 统一降到 autocast dtype, 让整条残差流和 RoPE 都是 bf16。
1260
+ if _BF16_RESIDUAL and torch.is_autocast_enabled():
1261
+ x = x.to(torch.get_autocast_dtype("cuda"))
1262
+
1263
+ depth = depth.to(device=device, dtype=torch.long)
1264
+ # c_sample **刻意保持 fp32**(按混精惯例): 它只有 [b, d_model], 保高精度的成本可忽略, 而它经
1265
+ # adaLN 变成 shift/scale/gate 逐元素乘加到所有激活上 —— 误差是系统性的、不会被平均掉。
1266
+ # DiT / SD3 / Flux 都把时间嵌入与调制留在 fp32。对比: 点云 latent 是 cross-attn 的 K/V,
1267
+ # 紧接着就进 k_proj/v_proj 的 matmul(autocast 必然转 bf16), 且 flash-attn 不接受 fp32, 故 bf16。
1268
+ c_sample = self.time_mlp(t) + self.depth_embedding(depth) # [b, d_model], fp32
1269
+
1270
+ def c_for(cur_lengths):
1271
+ return c_sample.repeat_interleave(cur_lengths, dim=0)
1272
+
1273
+ bsz = len(coord_list)
1274
+ # 模式 token(每样本一个),拼到点云 context 尾部(TSSR 式 flag 注入)
1275
+ mode_tok = None
1276
+ if mode is not None:
1277
+ if torch.is_tensor(mode):
1278
+ mode_vec = mode.to(device=device, dtype=torch.float).reshape(-1)
1279
+ else:
1280
+ mode_vec = torch.full((bsz,), float(mode), device=device)
1281
+ mode_tok = self.mode_emb(mode_vec).unsqueeze(1) # [b,1,d]
1282
+
1283
+ # ---- 点云条件:两套(全GT / 目标分辨率塌缩),逐层交替; 各拼上 mode token ----
1284
+ def _encode_cond(_pc):
1285
+ if not self.use_pc:
1286
+ return None, None, None
1287
+ if _pc is None:
1288
+ emb = self.null_cond.unsqueeze(0).expand(bsz, -1, -1) # [b,257,d]
1289
+ else:
1290
+ _pc = _pc.to(device=device)
1291
+ emb = self.conditioner(_pc) # [b,257,d]
1292
+ if mode_tok is not None:
1293
+ emb = torch.cat([emb, mode_tok.to(emb.dtype)], dim=1) # [b,258,d]
1294
+ ctx_len = int(emb.size(1)) # 每样本 context 长度恒为 257(+1 = mode token)
1295
+ cl = torch.full((emb.size(0),), ctx_len, device=device, dtype=torch.long)
1296
+ return emb.reshape(-1, emb.size(-1)), cl, ctx_len
1297
+
1298
+ cond_a, cond_a_len, cond_max_len = _encode_cond(pc) # 奇数层(全GT)
1299
+ if self.use_pc and (pc_target is not None):
1300
+ cond_b, cond_b_len, _ = _encode_cond(pc_target) # 偶数层(目标分辨率塌缩)
1301
+ else:
1302
+ cond_b, cond_b_len = cond_a, cond_a_len # 未提供 -> 两套相同
1303
+
1304
+ def _cond_for(gi):
1305
+ return (cond_a, cond_a_len) if (gi % 2 == 0) else (cond_b, cond_b_len)
1306
+
1307
+ # geo/geo_vcat(2026-08-23, 接 sfocal 大模型 warm-start): 三尺度 cos/sin 由 coord 现算,
1308
+ # 复用父类 GeoRoPE(锚点/频率梯子与主线逐位一致); hier/flat 走原路径不变。
1309
+ geo_scales = (self._geo_rope_scales(coord_packed, x.dtype)
1310
+ if str(getattr(self, "rope_mode", "flat")).startswith("geo") else None)
1311
+
1312
+ def rope_fn_for(cur_lengths, tokens_per_face=9):
1313
+ if geo_scales is not None:
1314
+ cos, sin = geo_scales[tokens_per_face]
1315
+ elif self.rope_mode == "hier":
1316
+ position_ids = lengths_to_position_ids(cur_lengths)
1317
+ cos, sin = self._hier_cos_sin(position_ids, tokens_per_face, x.dtype)
1318
+ else:
1319
+ position_ids = lengths_to_position_ids(cur_lengths)
1320
+ cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
1321
+ return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)
1322
+
1323
+ gi = 0
1324
+ rope_fn_9 = rope_fn_for(lengths, 9)
1325
+ c9 = c_for(lengths)
1326
+ for block in self.enc_blocks_9:
1327
+ cnd, cnl = _cond_for(gi); gi += 1
1328
+ x = block.forward_varlen(x, c9, lengths, cnd, cnl, rope_apply_fn=rope_fn_9,
1329
+ max_len=max_len_9, cond_max_len=cond_max_len)
1330
+ skip_9 = x
1331
+
1332
+ x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
1333
+ rope_fn_3 = rope_fn_for(lengths, 3)
1334
+ c3 = c_for(lengths)
1335
+ for block in self.enc_blocks_3:
1336
+ cnd, cnl = _cond_for(gi); gi += 1
1337
+ x = block.forward_varlen(x, c3, lengths, cnd, cnl, rope_apply_fn=rope_fn_3,
1338
+ max_len=max_len_3, cond_max_len=cond_max_len)
1339
+ skip_3 = x
1340
+
1341
+ x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
1342
+ rope_fn_1 = rope_fn_for(lengths, 1)
1343
+ c1 = c_for(lengths)
1344
+ for block in self.bottleneck_blocks:
1345
+ cnd, cnl = _cond_for(gi); gi += 1
1346
+ x = block.forward_varlen(x, c1, lengths, cnd, cnl, rope_apply_fn=rope_fn_1,
1347
+ max_len=max_len_1, cond_max_len=cond_max_len)
1348
+
1349
+ x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
1350
+ x = x + skip_3
1351
+ for block in self.dec_blocks_3:
1352
+ cnd, cnl = _cond_for(gi); gi += 1
1353
+ x = block.forward_varlen(x, c3, lengths, cnd, cnl, rope_apply_fn=rope_fn_3,
1354
+ max_len=max_len_3, cond_max_len=cond_max_len)
1355
+
1356
+ x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
1357
+ x = x + skip_9
1358
+ for block in self.dec_blocks_9:
1359
+ cnd, cnl = _cond_for(gi); gi += 1
1360
+ x = block.forward_varlen(x, c9, lengths, cnd, cnl, rope_apply_fn=rope_fn_9,
1361
+ max_len=max_len_9, cond_max_len=cond_max_len)
1362
+
1363
+ return x, types
1364
+
1365
+ # ------------------------------------------------------------------ #
1366
+ # 分类器工具
1367
+ # ------------------------------------------------------------------ #
1368
+ def to_acc_logit(self, h):
1369
+ """逐 token 正确性 logit [total];detach 骨干,分类器梯度不回流。"""
1370
+ return self.to_acc(h.detach()).squeeze(-1)
1371
+
1372
+ @torch.no_grad()
1373
+ def _pos_correct(self, h, target, types):
1374
+ """逐位置正确性 label [total] bool:该 refine 位置的头 argmax(teacher-forced)是否==GT。
1375
+ F 比 slot0;intra/inter 比 8 行全等。"""
1376
+ total = h.size(0)
1377
+ correct = torch.zeros(total, dtype=torch.bool, device=h.device)
1378
+ mf = types == TYPE_F
1379
+ if mf.any():
1380
+ pred = self.f_head(h[mf]).argmax(-1)
1381
+ correct[mf] = pred == target[mf, 0]
1382
+ for ty, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
1383
+ m = types == ty
1384
+ if not m.any():
1385
+ continue
1386
+ tgt = target[m]
1387
+ pred = head.forward_teacher(h[m], tgt).argmax(-1) # [n,8] teacher-forced
1388
+ correct[m] = (pred == tgt).all(dim=1)
1389
+ return correct
1390
+
1391
+ # ------------------------------------------------------------------ #
1392
+ # 训练入口
1393
+ # ------------------------------------------------------------------ #
1394
+ def forward(self, coord_list, noisy_subcode_list, t, depth, target_subcode_list=None,
1395
+ loss_mask=None, pc=None, pc_target=None, task="mask", acc_wrong_weight=1.0):
1396
+ """DDP 单一入口(必须走 model(...) 才能触发梯度同步;自定义方法会绕过 DDP)。
1397
+ task="mask" -> (loss, acc, acc_ne) 吸收腐蚀 CE(仅 masked 位置)
1398
+ task="nomask" -> (loss_nomask, loss_acc, frac_correct, cls_acc) uniform 去噪 CE(全位置)+ 分类器 BCE
1399
+ """
1400
+ if task == "mask":
1401
+ return self.forward_mask(coord_list, noisy_subcode_list, t, depth,
1402
+ target_subcode_list, loss_mask, pc, pc_target)
1403
+ elif task == "nomask":
1404
+ return self.forward_nomask(coord_list, noisy_subcode_list, t, depth,
1405
+ target_subcode_list, pc, pc_target, acc_wrong_weight)
1406
+ raise ValueError(f"unknown task {task!r}")
1407
+
1408
+
1409
+
1410
+
1411
+ # ------------------------------------------------------------------ #
1412
+ # 采样工具
1413
+ # ------------------------------------------------------------------ #
1414
+ def _predict_fill(self, h, h_u, types, cfg_t, use_cfg, temperature):
1415
+ """由 hidden 出各类型头 argmax(可 CFG/温度),填满 subcodes [total,8]。"""
1416
+ total = h.size(0)
1417
+ pred = torch.zeros((total, NUM_OCTANTS), dtype=torch.long, device=h.device)
1418
+ mf = types == TYPE_F
1419
+ if mf.any():
1420
+ logits = self.f_head(h[mf])
1421
+ if use_cfg:
1422
+ lu = self.f_head(h_u[mf]); logits = lu + cfg_t * (logits - lu)
1423
+ if temperature and temperature > 0:
1424
+ prob = (logits / temperature).softmax(-1)
1425
+ am = torch.multinomial(prob.float(), 1).squeeze(-1)
1426
+ else:
1427
+ am = logits.argmax(-1)
1428
+ pred[mf, :] = am.unsqueeze(1).expand(-1, NUM_OCTANTS)
1429
+ for ty, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
1430
+ m = types == ty
1431
+ if not m.any():
1432
+ continue
1433
+ p, _ = head.rollout(h[m], h_u[m] if use_cfg else None, cfg_t, temperature)
1434
+ pred[m] = p
1435
+ return pred
1436
+
1437
+ @torch.no_grad()
1438
+ def predict_subcodes(self, coord_list, sub_list, t, depth, pc=None, pc_target=None, mode=1,
1439
+ cfg_scale=1.0, temperature=0.0):
1440
+ """一次前向 + 头 argmax 得填满的 subcodes [total,8](供自生成脏输入 / 单步预测)。"""
1441
+ lengths = torch.tensor([c.numel() for c in coord_list], device=t.device, dtype=torch.long)
1442
+ types = self.types_from_lengths(lengths)
1443
+ use_cfg = (float(cfg_scale) != 1.0) and self.use_pc and (pc is not None)
1444
+ h, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, mode=mode)
1445
+ h_u = self.forward_varlen(coord_list, sub_list, t, depth, None, None, mode=mode)[0] if use_cfg else None
1446
+ return self._predict_fill(h, h_u, types, float(cfg_scale), use_cfg, temperature)
1447
+
1448
+ @torch.no_grad()
1449
+ def sample_tssr(self, coord_list, depth, pc=None, pc_target=None, T_infer=10,
1450
+ cfg_scale=1.0, cfg_schedule="constant", cfg_start=1.0,
1451
+ mask_logits_lo=0.2, mask_logits_hi=0.9, temperature=0.0,
1452
+ generator=None, return_stats=False):
1453
+ """TSSR 三前向解码。返回 list[[L,8]] local 子码(drop-in 父类 decode 路径)。
1454
+ 每步:① mask 预测(mode=1)填满 -> ② no-mask 精修(mode=0)+ 同前向出置信 -> ③ re-mask 判错位置���
1455
+ 末步不再 re-mask。"""
1456
+ self.eval()
1457
+ device = next(self.parameters()).device
1458
+ coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
1459
+ depth = depth.to(device=device, dtype=torch.long)
1460
+ lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
1461
+ types = self.types_from_lengths(lengths)
1462
+ want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and self.use_pc and (pc is not None)
1463
+ total = int(lengths.sum().item())
1464
+
1465
+ subcodes = torch.full((total, NUM_OCTANTS), MASK_ID, dtype=torch.long, device=device)
1466
+ sched = torch.linspace(float(mask_logits_lo), float(mask_logits_hi), max(T_infer, 1), device=device)
1467
+ n_remask_hist = []
1468
+
1469
+ for step in range(T_infer):
1470
+ if cfg_schedule == "linear":
1471
+ cfg_t = float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(T_infer - 1, 1)
1472
+ else:
1473
+ cfg_t = float(cfg_scale)
1474
+ use_cfg = want_cfg and (cfg_t != 1.0)
1475
+ t = torch.full((len(coord_list),), step / T_infer, device=device, dtype=torch.float)
1476
+
1477
+ # ① mask 预测(mode=1)
1478
+ sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
1479
+ h1, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, mode=1)
1480
+ h1u = self.forward_varlen(coord_list, sub_list, t, depth, None, None, mode=1)[0] if use_cfg else None
1481
+ x_t = self._predict_fill(h1, h1u, types, cfg_t, use_cfg, temperature)
1482
+
1483
+ # ② no-mask 精修(mode=0)+ 复用该前向出逐 token 置信
1484
+ sub_list2 = list(torch.split(x_t, lengths.tolist(), dim=0))
1485
+ h2, _ = self.forward_varlen(coord_list, sub_list2, t, depth, pc, pc_target, mode=0)
1486
+ h2u = self.forward_varlen(coord_list, sub_list2, t, depth, None, None, mode=0)[0] if use_cfg else None
1487
+ x_t = self._predict_fill(h2, h2u, types, cfg_t, use_cfg, temperature)
1488
+ acc = torch.sigmoid(self.to_acc(h2).squeeze(-1)).float() # [total] ∈[0,1]
1489
+
1490
+ # ③ re-mask 判错位置(末步不做)
1491
+ if step < T_infer - 1:
1492
+ mask_prob = ((1.0 - acc) * sched[step]).clamp(1e-6, 1.0 - 1e-6)
1493
+ if generator is not None:
1494
+ r = torch.rand(total, device=device, generator=generator)
1495
+ else:
1496
+ r = torch.rand(total, device=device)
1497
+ remask = r < mask_prob
1498
+ subcodes = x_t.clone()
1499
+ subcodes[remask] = MASK_ID
1500
+ n_remask_hist.append(int(remask.sum().item()))
1501
+ else:
1502
+ subcodes = x_t
1503
+
1504
+ out = list(torch.split(subcodes, lengths.tolist(), dim=0))
1505
+ if return_stats:
1506
+ return out, {"n_remask_per_step": n_remask_hist, "total_pos": total}
1507
+ return out
mulres_infer/models/point_conditioner.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from beartype import beartype
4
+ from modules.miche.encode import load_model
5
+ import os
6
+
7
+ _PKG_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8
+ # helper functions
9
+
10
+ def exists(val):
11
+ return val is not None
12
+
13
+ def default(*values):
14
+ for value in values:
15
+ if exists(value):
16
+ return value
17
+ return None
18
+
19
+
20
+ # point-cloud encoder from Michelangelo
21
+ @beartype
22
+ class PointConditioner(torch.nn.Module):
23
+ def __init__(
24
+ self,
25
+ *,
26
+ dim_latent = None,
27
+ model_name = 'miche-256-feature',
28
+ cond_dim = 768,
29
+ feature_dim = 768,
30
+ freeze = True,
31
+ img_feat_dim: int = 0,
32
+ img_feat_extractor=None,
33
+ num_latents = None, # None=预训练默认(256); >256 时把可学习 query 平铺+微噪扩容(预训练权重原样热启动)
34
+ ):
35
+ super().__init__()
36
+
37
+ # open-source version of miche
38
+ if model_name == 'miche-256-feature':
39
+ ckpt_path = os.environ.get("SHAPEVAE_CKPT",
40
+ os.path.join(_PKG_ROOT, "ckpt", "shapevae-256.ckpt"))
41
+ if not os.path.exists(ckpt_path):
42
+ ckpt_path=None
43
+ print('[WARNING] Michelangelo ckpt not exist, please check if you are training')
44
+ config_path = os.path.join(_PKG_ROOT, 'modules', 'miche', 'shapevae-256.yaml')
45
+
46
+ self.feature_dim = feature_dim # embedding dimension
47
+ self.cond_length = 257 # length of embedding
48
+ self.point_encoder = load_model(ckpt_path=ckpt_path, config_path=config_path)
49
+ if num_latents:
50
+ self._expand_latents(int(num_latents))
51
+
52
+ # additional layers to connect miche and GPT
53
+ self.cond_head_proj = nn.Linear(cond_dim, self.feature_dim)
54
+ self.cond_proj = nn.Linear(cond_dim, self.feature_dim)
55
+
56
+ else:
57
+ raise NotImplementedError
58
+
59
+ # whether to finetuen point-cloud encoder
60
+ if freeze:
61
+ for parameter in self.point_encoder.parameters():
62
+ parameter.requires_grad = False
63
+
64
+ self.freeze = freeze
65
+ self.model_name = model_name
66
+ self.dim_latent = self.feature_dim
67
+
68
+ # ---------- image-conditioning extension ----------
69
+ self.img_feat_dim = int(img_feat_dim or 0)
70
+ self.img_feat_extractor = img_feat_extractor
71
+ if self.img_feat_dim > 0:
72
+ self._widen_input_proj(self.img_feat_dim)
73
+
74
+ self.register_buffer('_device_param', torch.tensor(0.), persistent = False)
75
+
76
+
77
+ @property
78
+ def device(self):
79
+ return next(self.buffers()).device
80
+
81
+ def _expand_latents(self, num_latents: int):
82
+ """把 perceiver 可学习 query 扩到 [1头 + num_latents body]: 预训练 query 是 257=1+256(行0=头token),
83
+ 头保持 1 个, body 256 平铺复制 + 1% std 微噪打破对称(首份无噪)。attention/n_ctx 均动态读 shape,
84
+ 无其他结构依赖(SDPA 补丁同样形状无关)。训练热启动时上层 ckpt(若已含扩容 query)会原样覆盖。"""
85
+ enc = self.point_encoder.model.shape_model.encoder
86
+ old_q = enc.query.data # [1+B, w]
87
+ body = old_q[1:]
88
+ if body.shape[0] == num_latents:
89
+ self.cond_length = 1 + num_latents
90
+ return
91
+ assert num_latents % body.shape[0] == 0, f"num_latents={num_latents} 须为 body={body.shape[0]} 的整数倍"
92
+ reps = num_latents // body.shape[0]
93
+ new_body = body.repeat(reps, 1)
94
+ noise = torch.randn_like(new_body) * body.std() * 0.01
95
+ noise[: body.shape[0]] = 0 # 首份 body 保持原 query 精确不动
96
+ enc.query = nn.Parameter(torch.cat([old_q[:1], new_body + noise], dim=0))
97
+ enc.num_latents = 1 + num_latents
98
+ self.cond_length = 1 + num_latents
99
+ print(f"[PointConditioner] query 扩容 1+{body.shape[0]} -> 1+{num_latents} (body tile+1%噪, 首份无噪)")
100
+
101
+ def _widen_input_proj(self, img_feat_dim: int):
102
+ """Replace Michelangelo's input_proj Linear with a wider one and zero-init
103
+ the new input columns so loading the pretrained ckpt doesn't change init
104
+ behavior (image features get multiplied by zero on construction)."""
105
+ enc = self.point_encoder.model.shape_model.encoder
106
+ if getattr(enc, "img_feat_dim", 0) == img_feat_dim and enc.input_proj.in_features == (
107
+ enc.fourier_embedder.out_dim + enc.point_feats + img_feat_dim
108
+ ):
109
+ return # already widened (e.g. resume from a widened ckpt)
110
+ old = enc.input_proj
111
+ new_in = enc.fourier_embedder.out_dim + enc.point_feats + img_feat_dim
112
+ if new_in == old.in_features:
113
+ enc.img_feat_dim = img_feat_dim
114
+ return
115
+ new_proj = nn.Linear(
116
+ new_in,
117
+ old.out_features,
118
+ bias=old.bias is not None,
119
+ device=old.weight.device,
120
+ dtype=old.weight.dtype,
121
+ )
122
+ with torch.no_grad():
123
+ new_proj.weight.zero_()
124
+ new_proj.weight[:, : old.in_features].copy_(old.weight)
125
+ if old.bias is not None:
126
+ new_proj.bias.copy_(old.bias)
127
+ enc.input_proj = new_proj
128
+ enc.img_feat_dim = img_feat_dim
129
+ if self.freeze:
130
+ # input_proj is part of the shape encoder, which we keep frozen by
131
+ # default; only the new image-feature columns ever produce nonzero
132
+ # gradients on top of frozen weights. If callers want to fine-tune
133
+ # the whole input_proj they can flip freeze=False.
134
+ for p in new_proj.parameters():
135
+ p.requires_grad = False
136
+
137
+
138
+ def embed_pc(
139
+ self,
140
+ pc_normal,
141
+ image=None,
142
+ transform_matrix=None,
143
+ camera_angle_x=None,
144
+ depth_map=None,
145
+ depth_valid=None,
146
+ img_feats=None,
147
+ ):
148
+ # encode point cloud to embeddings
149
+ if self.model_name == 'miche-256-feature':
150
+ if self.img_feat_dim > 0:
151
+ if img_feats is None:
152
+ if self.img_feat_extractor is None:
153
+ raise RuntimeError(
154
+ "img_feat_dim>0 but no img_feats provided and no extractor configured."
155
+ )
156
+ if image is None or transform_matrix is None or camera_angle_x is None:
157
+ raise ValueError(
158
+ "image, transform_matrix, camera_angle_x are required when img_feat_extractor is set."
159
+ )
160
+ img_feats = self.img_feat_extractor(
161
+ image, pc_normal[..., :3], transform_matrix, camera_angle_x,
162
+ depth_map=depth_map, depth_valid=depth_valid,
163
+ )
164
+ img_feats = img_feats.to(dtype=pc_normal.dtype)
165
+ point_feature = self.point_encoder.encode_latents(pc_normal, img_feats=img_feats)
166
+ else:
167
+ point_feature = self.point_encoder.encode_latents(pc_normal)
168
+ pc_embed_head = self.cond_head_proj(point_feature[:, 0:1])
169
+ pc_embed = self.cond_proj(point_feature[:, 1:])
170
+ pc_embed = torch.cat([pc_embed_head, pc_embed], dim=1)
171
+
172
+ return pc_embed
173
+
174
+
175
+ def forward(
176
+ self,
177
+ pc = None,
178
+ pc_embeds = None,
179
+ image=None,
180
+ transform_matrix=None,
181
+ camera_angle_x=None,
182
+ depth_map=None,
183
+ depth_valid=None,
184
+ img_feats=None,
185
+ ):
186
+ if pc_embeds is None:
187
+ target_dtype = next(self.buffers()).dtype
188
+ pc_embeds = self.embed_pc(
189
+ pc.to(target_dtype),
190
+ image=image,
191
+ transform_matrix=transform_matrix,
192
+ camera_angle_x=camera_angle_x,
193
+ depth_map=depth_map,
194
+ depth_valid=depth_valid,
195
+ img_feats=img_feats,
196
+ )
197
+
198
+ assert not torch.any(torch.isnan(pc_embeds)), 'NAN values in pc embedings'
199
+
200
+ return pc_embeds
mulres_infer/models/subdivider.py ADDED
@@ -0,0 +1,1188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+ from einops import rearrange
6
+ from modules.attention import MLA
7
+ from .point_conditioner import PointConditioner
8
+
9
+ try:
10
+ from flash_attn import flash_attn_varlen_func
11
+ except Exception:
12
+ try:
13
+ from flash_attn.flash_attn_interface import flash_attn_varlen_func
14
+ except Exception:
15
+ flash_attn_varlen_func = None
16
+
17
+ class SinusoidalPosEmb(nn.Module):
18
+ def __init__(self, dim, scale=100.0):
19
+ super().__init__()
20
+ self.dim = dim
21
+ self.scale = scale
22
+
23
+ def forward(self, x):
24
+ device = x.device
25
+ dtype = x.dtype
26
+ x = x.float() * self.scale
27
+ half_dim = self.dim // 2
28
+ emb = math.log(10000) / (half_dim - 1)
29
+ emb = torch.exp(torch.arange(half_dim, device=device) * -emb)
30
+ emb = x[:, None] * emb[None, :]
31
+ emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
32
+ return emb.to(dtype=dtype)
33
+
34
+ class RMSNorm(nn.Module):
35
+ """现代 LLM 标配:去均值的 RMSNorm"""
36
+ def __init__(self, dim, eps=1e-6):
37
+ super().__init__()
38
+ self.eps = eps
39
+ # 只需要学习一个缩放参数,去掉 bias
40
+ self.weight = nn.Parameter(torch.ones(dim))
41
+
42
+ def forward(self, x):
43
+ # 计算均方根 (Root Mean Square)
44
+ norm_x = torch.mean(x.pow(2), dim=-1, keepdim=True)
45
+ x_normed = x * torch.rsqrt(norm_x + self.eps)
46
+ return self.weight * x_normed
47
+
48
+ class SwiGLU(nn.Module):
49
+ """现代 LLM 标配:SwiGLU 激活的门控 FFN"""
50
+ def __init__(self, d_model, hidden_dim=None):
51
+ super().__init__()
52
+ # 将原先 4 * d_model 的维度缩小到 8/3,保持参数量对等; hidden_dim 可显式覆盖
53
+ # (如 1024 宽度时 8/3 给 2730 非 256 倍数, 显式取 2816 对齐 Tensor Core)
54
+ hidden_dim = int((8 / 3) * d_model) if hidden_dim is None else int(hidden_dim)
55
+ # 提示:在实际工程中,通常会把 hidden_dim 向上取整到 128 或 256 的倍数,以最大化 Tensor Core 效率
56
+
57
+ self.w1 = nn.Linear(d_model, hidden_dim, bias=False)
58
+ self.w2 = nn.Linear(d_model, hidden_dim, bias=False)
59
+ self.w3 = nn.Linear(hidden_dim, d_model, bias=False)
60
+
61
+ def forward(self, x):
62
+ # 公式: Swish(xW1) ⊗ (xW2) -> W3
63
+ # PyTorch 中 F.silu 就是 Swish
64
+ return self.w3(F.silu(self.w1(x)) * self.w2(x))
65
+
66
+ class ModernAttention(nn.Module):
67
+ def __init__(self, d_model, nhead, dropout=0., qk_norm=False):
68
+ super().__init__()
69
+ assert d_model % nhead == 0, "d_model 必须能被 nhead 整除"
70
+ self.nhead = nhead
71
+ self.d_head = d_model // nhead
72
+
73
+ # 现代架构通常在 Attention 的投影层中也丢弃 bias
74
+ self.q_proj = nn.Linear(d_model, d_model, bias=False)
75
+ self.k_proj = nn.Linear(d_model, d_model, bias=False)
76
+ self.v_proj = nn.Linear(d_model, d_model, bias=False)
77
+ self.out_proj = nn.Linear(d_model, d_model, bias=False)
78
+ self.dropout = dropout
79
+ # QK-norm: 逐 head 对 Q/K 在 head_dim 上做 RMSNorm(在 RoPE 之前),稳定 attention logits、
80
+ # 防幅度爆炸。默认关(不改变既有模型);flow 等新模型可开。新增参数 -> 旧 ckpt load_strict=False。
81
+ self.qk_norm = qk_norm
82
+ if qk_norm:
83
+ self.q_norm = RMSNorm(self.d_head)
84
+ self.k_norm = RMSNorm(self.d_head)
85
+
86
+ def forward(self, q, k, v, mask=None, is_causal=False, rope_apply_fn=None):
87
+ B, L_q, _ = q.size()
88
+ _, L_k, _ = k.size()
89
+
90
+ # 投影并调整形状为 [Batch, Head, SeqLen, HeadDim]
91
+ Q = self.q_proj(q).view(B, L_q, self.nhead, self.d_head).transpose(1, 2)
92
+ K = self.k_proj(k).view(B, L_k, self.nhead, self.d_head).transpose(1, 2)
93
+ V = self.v_proj(v).view(B, L_k, self.nhead, self.d_head).transpose(1, 2)
94
+
95
+ if self.qk_norm: # QK-norm(RoPE 前),保持 dtype 供 SDPA/flash
96
+ Q = self.q_norm(Q).to(Q.dtype); K = self.k_norm(K).to(K.dtype)
97
+
98
+ # 注入旋转位置编码 (RoPE)
99
+ if rope_apply_fn is not None:
100
+ Q, K = rope_apply_fn(Q, K)
101
+
102
+ # 使用 PyTorch 2.0+ 内置的 SDPA,它会自动调用 Flash Attention 节省巨量显存
103
+ out = F.scaled_dot_product_attention(
104
+ Q, K, V,
105
+ attn_mask=mask,
106
+ dropout_p=self.dropout if self.training else 0.0,
107
+ is_causal=is_causal
108
+ )
109
+
110
+ # 恢复形状并输出
111
+ out = out.transpose(1, 2).contiguous().view(B, L_q, -1)
112
+ return self.out_proj(out)
113
+
114
+ def forward_varlen(self, q, k, v, lengths_q, lengths_k=None, is_causal=False, rope_apply_fn=None,
115
+ max_len_q=None, max_len_k=None):
116
+ """max_len_q / max_len_k: flash 需要的 max_seqlen。**传 python int 可避免 GPU->CPU 同步**。
117
+
118
+ 不传时回退到 `int(lengths.max().item())` —— 那是一次 cudaStreamSynchronize: CPU 必须等
119
+ GPU 把已排队的活干完才能读到值, 于是无法提前排队 kernel。本模型一次前向有 36 次 attention
120
+ 调用(18 self + 18 cross), 即 72 次同步, 是「kernel 又多又小、GPU 有空隙」的直接成因,
121
+ 也让 torch.compile 在这里 graph break(数据依赖的 unbacked symint)。
122
+ 调用方本来就知道这些长度(lengths 是从 [c.numel() for c in coord_list] 建的), 应当传进来。
123
+ """
124
+ if lengths_k is None:
125
+ lengths_k = lengths_q
126
+ if max_len_k is None:
127
+ max_len_k = max_len_q
128
+
129
+ total_q = q.size(0)
130
+ total_k = k.size(0)
131
+ Q = self.q_proj(q).view(total_q, self.nhead, self.d_head)
132
+ K = self.k_proj(k).view(total_k, self.nhead, self.d_head)
133
+ V = self.v_proj(v).view(total_k, self.nhead, self.d_head)
134
+
135
+ if self.qk_norm: # QK-norm(RoPE 前),.to 保持 dtype 供 flash-attn
136
+ Q = self.q_norm(Q).to(Q.dtype); K = self.k_norm(K).to(K.dtype)
137
+
138
+ if rope_apply_fn is not None:
139
+ Q, K = rope_apply_fn(Q, K)
140
+
141
+ dropout_p = self.dropout if self.training else 0.0
142
+ if can_use_flash_attn(Q):
143
+ out = flash_attn_varlen_func(
144
+ Q.contiguous(),
145
+ K.contiguous(),
146
+ V.contiguous(),
147
+ lengths_to_cu_seqlens(lengths_q),
148
+ lengths_to_cu_seqlens(lengths_k),
149
+ int(lengths_q.max().item()) if max_len_q is None else int(max_len_q),
150
+ int(lengths_k.max().item()) if max_len_k is None else int(max_len_k),
151
+ dropout_p=dropout_p,
152
+ causal=is_causal,
153
+ )
154
+ else:
155
+ out_chunks = []
156
+ q_start = 0
157
+ k_start = 0
158
+ for q_len, k_len in zip(lengths_q.tolist(), lengths_k.tolist()):
159
+ q_end = q_start + q_len
160
+ k_end = k_start + k_len
161
+ q_i = Q[q_start:q_end].transpose(0, 1).unsqueeze(0)
162
+ k_i = K[k_start:k_end].transpose(0, 1).unsqueeze(0)
163
+ v_i = V[k_start:k_end].transpose(0, 1).unsqueeze(0)
164
+ out_i = F.scaled_dot_product_attention(
165
+ q_i,
166
+ k_i,
167
+ v_i,
168
+ dropout_p=dropout_p,
169
+ is_causal=is_causal,
170
+ )
171
+ out_chunks.append(out_i.squeeze(0).transpose(0, 1))
172
+ q_start = q_end
173
+ k_start = k_end
174
+ out = torch.cat(out_chunks, dim=0)
175
+
176
+ return self.out_proj(out.contiguous().view(total_q, -1))
177
+
178
+ # class CrossAttnTransformerBlock(nn.Module):
179
+ # def __init__(self, d_model, nhead, dropout=0.):
180
+ # super().__init__()
181
+ # self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
182
+ # self.cross_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
183
+
184
+ # self.norm1 = nn.LayerNorm(d_model)
185
+ # self.norm2 = nn.LayerNorm(d_model)
186
+ # self.norm3 = nn.LayerNorm(d_model)
187
+
188
+ # self.ffn = nn.Sequential(
189
+ # nn.Linear(d_model, d_model * 4),
190
+ # nn.GELU(),
191
+ # nn.Dropout(dropout),
192
+ # nn.Linear(d_model * 4, d_model),
193
+ # )
194
+ # self.dropout = nn.Dropout(dropout)
195
+
196
+ # def forward(self, x, cond):
197
+ # # 1) token self-attention
198
+ # h = self.norm1(x)
199
+ # sa_out, _ = self.self_attn(h, h, h, need_weights=False)
200
+ # x = x + self.dropout(sa_out)
201
+
202
+ # # 2) cross-attention: query from current tokens, key/value from condition tokens
203
+ # h = self.norm2(x)
204
+ # ca_out, _ = self.cross_attn(h, cond, cond, need_weights=False)
205
+ # x = x + self.dropout(ca_out)
206
+
207
+ # # 3) feed-forward
208
+ # h = self.norm3(x)
209
+ # ff_out = self.ffn(h)
210
+ # x = x + self.dropout(ff_out)
211
+ # return x
212
+ class ModernCrossAttnTransformerBlock(nn.Module):
213
+ def __init__(self, d_model, nhead, dropout=0.):
214
+ super().__init__()
215
+
216
+ # 替换为支持 Flash Attention 的现代 Attention
217
+ self.self_attn = ModernAttention(d_model, nhead, dropout=dropout)
218
+ self.cross_attn = ModernAttention(d_model, nhead, dropout=dropout)
219
+
220
+ # 替换 LayerNorm 为 RMSNorm
221
+ self.norm1 = RMSNorm(d_model)
222
+ self.norm2 = RMSNorm(d_model)
223
+ self.norm3 = RMSNorm(d_model)
224
+
225
+ # 替换传统 FFN 为 SwiGLU
226
+ self.ffn = SwiGLU(d_model)
227
+
228
+ self.dropout = nn.Dropout(dropout)
229
+
230
+ def forward(self, x, cond, rope_apply_fn=None, causal_mask=False):
231
+ # 1) token self-attention
232
+ # 你的 Pre-Norm 逻辑保持不变,这是正确的
233
+ h = self.norm1(x)
234
+ # 注意:在这里把 RoPE 传进去处理 Query 和 Key
235
+ sa_out = self.self_attn(h, h, h, is_causal=causal_mask, rope_apply_fn=rope_apply_fn)
236
+ x = x + self.dropout(sa_out)
237
+
238
+ # 2) cross-attention: query from current tokens, key/value from condition tokens
239
+ if cond is not None:
240
+ h = self.norm2(x)
241
+ # Cross Attention 一般不使用 RoPE,因为输入源不同
242
+ ca_out = self.cross_attn(h, cond, cond)
243
+ x = x + self.dropout(ca_out)
244
+
245
+ # 3) feed-forward (SwiGLU)
246
+ h = self.norm3(x)
247
+ ff_out = self.ffn(h)
248
+ x = x + self.dropout(ff_out)
249
+
250
+ return x
251
+
252
+ def forward_varlen(self, x, lengths, cond=None, cond_lengths=None, rope_apply_fn=None, causal_mask=False):
253
+ h = self.norm1(x)
254
+ sa_out = self.self_attn.forward_varlen(
255
+ h,
256
+ h,
257
+ h,
258
+ lengths,
259
+ is_causal=causal_mask,
260
+ rope_apply_fn=rope_apply_fn,
261
+ )
262
+ x = x + self.dropout(sa_out)
263
+
264
+ if cond is not None:
265
+ h = self.norm2(x)
266
+ ca_out = self.cross_attn.forward_varlen(
267
+ h,
268
+ cond,
269
+ cond,
270
+ lengths,
271
+ lengths_k=cond_lengths,
272
+ )
273
+ x = x + self.dropout(ca_out)
274
+
275
+ h = self.norm3(x)
276
+ ff_out = self.ffn(h)
277
+ x = x + self.dropout(ff_out)
278
+ return x
279
+
280
+ def rotate_half(x):
281
+ """
282
+ 旋转特征的一半维度。
283
+ 对于向量 [x1, x2, ..., x_d/2, x_d/2+1, ..., x_d],
284
+ 将其转换为 [-x_d/2+1, ..., -x_d, x1, x2, ..., x_d/2]。
285
+ 这在数学上等价于复数平面上的乘以 i。
286
+ """
287
+ x1 = x[..., : x.shape[-1] // 2]
288
+ x2 = x[..., x.shape[-1] // 2 :]
289
+ return torch.cat((-x2, x1), dim=-1)
290
+
291
+ def apply_rotary_pos_emb(q, k, cos, sin):
292
+ """
293
+ 将预计算好的 cos 和 sin 注入到 Query 和 Key 中。
294
+ q, k 形状期望为: (Batch, Num_Heads, Seq_Len, Head_Dim)
295
+ cos, sin 形状期望为: (1, 1, Seq_Len, Head_Dim)
296
+ """
297
+ q_embed = (q * cos) + (rotate_half(q) * sin)
298
+ k_embed = (k * cos) + (rotate_half(k) * sin)
299
+ return q_embed, k_embed
300
+
301
+ def apply_rotary_pos_emb_varlen(q, k, cos, sin):
302
+ """RoPE。**cos/sin 先转成 q/k 的 dtype**,保证输出 dtype 不被 cos/sin 带跑。
303
+
304
+ 为什么必须这么写:cos/sin 由 `rotary_emb.forward_positions(..., dtype=x.dtype)` 生成,而
305
+ 9 尺度的 rope_fn 是在 `x = raw_emb + refine_emb` 之后立刻构造的 —— `nn.Embedding` 不在 autocast
306
+ 的转型名单里,那时 x 还是 **fp32**,于是 cos/sin 是 fp32。bf16 的 Q/K 乘上 fp32 的 cos/sin 会
307
+ 类型提升成 fp32,`can_use_flash_attn` 判否,直接掉进 `ModernAttention.forward_varlen` 里
308
+ **SDPA 的逐序列 python 循环**。命中的恰好是 enc_blocks_9 / dec_blocks_9 这 4 层,
309
+ 也就是跑在满 token 长度(其余层已被 hourglass 降到 1/3、1/9)上最贵的 4 个注意力。
310
+ 3 尺度与 bottleneck 的 rope_fn 因为构造时 x 已是 bf16,本来就走 flash —— 即同一网络里
311
+ 4 层 fp32 慢路径 + 14 层 bf16 快路径,不一致。统一到 bf16。
312
+ """
313
+ cos_q, sin_q = cos.to(q.dtype), sin.to(q.dtype)
314
+ q_embed = (q * cos_q) + (rotate_half(q) * sin_q)
315
+ cos_k, sin_k = (cos_q, sin_q) if k.dtype == q.dtype else (cos.to(k.dtype), sin.to(k.dtype))
316
+ k_embed = (k * cos_k) + (rotate_half(k) * sin_k)
317
+ return q_embed, k_embed
318
+
319
+ def lengths_to_cu_seqlens(lengths):
320
+ lengths = lengths.to(dtype=torch.int32)
321
+ prefix = torch.empty(lengths.numel() + 1, device=lengths.device, dtype=torch.int32)
322
+ prefix[0] = 0
323
+ prefix[1:] = torch.cumsum(lengths, dim=0, dtype=torch.int32)
324
+ return prefix.contiguous()
325
+
326
+ def lengths_to_position_ids(lengths):
327
+ total = int(lengths.sum().item())
328
+ if total == 0:
329
+ return torch.empty(0, device=lengths.device, dtype=torch.long)
330
+ starts = torch.cumsum(lengths, dim=0) - lengths
331
+ return torch.arange(total, device=lengths.device, dtype=torch.long) - torch.repeat_interleave(starts, lengths)
332
+
333
+ def can_use_flash_attn(x):
334
+ return (
335
+ flash_attn_varlen_func is not None
336
+ and x.is_cuda
337
+ and x.dtype in (torch.float16, torch.bfloat16)
338
+ )
339
+
340
+ class RotaryEmbedding(nn.Module):
341
+ """
342
+ 预计算和缓存 RoPE 的 cos 和 sin 矩阵。
343
+ """
344
+ def __init__(self, dim, max_position_embeddings=90000, base=10000.0, device=None):
345
+ super().__init__()
346
+ self.dim = dim
347
+ self.max_position_embeddings = max_position_embeddings
348
+ self.base = base
349
+
350
+ # 计算旋转频率: theta_i = 10000^(-2(i-1)/d)
351
+ inv_freq = 1.0 / (self.base ** (torch.arange(0, self.dim, 2, dtype=torch.float32, device=device) / self.dim))
352
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
353
+
354
+ # 预先构建缓存
355
+ self._set_cos_sin_cache(seq_len=max_position_embeddings, device=self.inv_freq.device, dtype=torch.get_default_dtype())
356
+
357
+ def _set_cos_sin_cache(self, seq_len, device, dtype):
358
+ self.max_seq_len_cached = seq_len
359
+ t = torch.arange(self.max_seq_len_cached, device=device, dtype=torch.float32)
360
+
361
+ # freqs 形状: (seq_len, dim/2)
362
+ freqs = torch.outer(t, self.inv_freq)
363
+
364
+ # 将频率在特征维度上复制一份,形状变为: (seq_len, dim)
365
+ # 前半部分和后半部分的频率一致,为了和 rotate_half 的逻辑对齐
366
+ emb = torch.cat((freqs, freqs), dim=-1)
367
+
368
+ # 增加 Batch 和 Head 的广播维度: (1, 1, Seq_Len, Head_Dim)
369
+ self.register_buffer("cos_cached", emb.cos().to(dtype).unsqueeze(0).unsqueeze(0), persistent=False)
370
+ self.register_buffer("sin_cached", emb.sin().to(dtype).unsqueeze(0).unsqueeze(0), persistent=False)
371
+
372
+ def forward(self, x, seq_len=None):
373
+ """
374
+ x 形状: (Batch, Num_Heads, Seq_Len, Head_Dim)
375
+ """
376
+ # 如果当前序列长度超过了预计算的缓存,则动态扩展缓存
377
+ if seq_len > self.max_seq_len_cached:
378
+ self._set_cos_sin_cache(seq_len=seq_len, device=x.device, dtype=x.dtype)
379
+
380
+ # 截取当前需要的序列长度返回
381
+ return (
382
+ self.cos_cached[:, :, :seq_len, ...].to(dtype=x.dtype),
383
+ self.sin_cached[:, :, :seq_len, ...].to(dtype=x.dtype),
384
+ )
385
+
386
+ def forward_positions(self, position_ids, dtype=None):
387
+ if dtype is None:
388
+ dtype = self.cos_cached.dtype
389
+ if position_ids.numel() == 0:
390
+ empty = torch.empty(0, 1, self.dim, device=position_ids.device, dtype=dtype)
391
+ return empty, empty
392
+ seq_len = int(position_ids.max().item()) + 1
393
+ if seq_len > self.max_seq_len_cached:
394
+ self._set_cos_sin_cache(seq_len=seq_len, device=position_ids.device, dtype=dtype)
395
+ cos = self.cos_cached[0, 0, position_ids, :].to(device=position_ids.device, dtype=dtype)
396
+ sin = self.sin_cached[0, 0, position_ids, :].to(device=position_ids.device, dtype=dtype)
397
+ return cos.unsqueeze(1), sin.unsqueeze(1)
398
+
399
+ class LinearDownsample(nn.Module):
400
+ def __init__(self, dim, shorten_factor):
401
+ super().__init__()
402
+ self.proj = nn.Linear(dim * shorten_factor, dim)
403
+ self.shorten_factor = shorten_factor
404
+
405
+ def forward(self, x):
406
+ x = rearrange(x, 'b (n s) d -> b n (s d)', s=self.shorten_factor)
407
+ return self.proj(x)
408
+
409
+ def forward_varlen(self, x, lengths):
410
+ if torch.any(lengths % self.shorten_factor != 0):
411
+ raise ValueError(f"All sequence lengths must be divisible by {self.shorten_factor} for hourglass downsample.")
412
+ x = rearrange(x, '(n s) d -> n (s d)', s=self.shorten_factor)
413
+ return self.proj(x), lengths // self.shorten_factor
414
+
415
+ class LinearUpsample(nn.Module):
416
+ def __init__(self, dim, shorten_factor):
417
+ super().__init__()
418
+ self.proj = nn.Linear(dim, dim * shorten_factor)
419
+ self.shorten_factor = shorten_factor
420
+
421
+ def forward(self, x):
422
+ x = self.proj(x)
423
+ return rearrange(x, 'b n (s d) -> b (n s) d', s=self.shorten_factor)
424
+
425
+ def forward_varlen(self, x, lengths):
426
+ x = self.proj(x)
427
+ x = rearrange(x, 'n (s d) -> (n s) d', s=self.shorten_factor)
428
+ return x, lengths * self.shorten_factor
429
+
430
+ class DiscreteDiffusionTransformer(nn.Module):
431
+ def __init__(self,
432
+ vocab_A,
433
+ vocab_B,
434
+ max_seq_len,
435
+ d_model=768,
436
+ nhead=12,
437
+ depth = [4,4,8],
438
+ dropout=0.,
439
+ encoder_freeze=True,
440
+ encoder_name='miche-256-feature',
441
+ use_pc=True,
442
+ time_embed_scale=100.0):
443
+ super().__init__()
444
+ self.vocab_A = vocab_A
445
+ self.vocab_B = vocab_B
446
+ self.d_model = d_model
447
+
448
+ # MASK Token 的 ID,我们将其设为 B(因为真实的 refine_token 是 0 到 B-1)
449
+ self.mask_token_id = vocab_B
450
+
451
+ # 1. 词嵌入 (Embeddings)
452
+ self.raw_embedding = nn.Embedding(vocab_A, d_model)
453
+ # refine_token 的词表大小是 B + 1 (包含了 MASK token)
454
+ self.refine_embedding = nn.Embedding(vocab_B + 1, d_model)
455
+
456
+ # 2. 位置编码 (Positional Encoding)
457
+ # self.pos_embedding = nn.Embedding(max_seq_len, d_model)
458
+
459
+ # 3. 时间步编码 (Timestep Embedding)
460
+ self.time_mlp = nn.Sequential(
461
+ SinusoidalPosEmb(d_model, scale=time_embed_scale),
462
+ nn.Linear(d_model, d_model * 4),
463
+ nn.GELU(),
464
+ nn.Linear(d_model * 4, d_model)
465
+ )
466
+
467
+ # 4. 主干网络 (Denoising Backbone)
468
+ # U-Net/Hourglass 主干:
469
+ # enc(9) -> down -> enc(3) -> down -> bottleneck(1) -> up+skip -> dec(3) -> up+skip -> dec(9)
470
+ self.depth = depth
471
+
472
+ self.enc_blocks_9 = nn.ModuleList(
473
+ [ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[0])]
474
+ )
475
+ self.enc_blocks_3 = nn.ModuleList(
476
+ [ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[1])]
477
+ )
478
+ self.bottleneck_blocks = nn.ModuleList(
479
+ [ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[2])]
480
+ )
481
+ self.dec_blocks_3 = nn.ModuleList(
482
+ [ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[1])]
483
+ )
484
+ self.dec_blocks_9 = nn.ModuleList(
485
+ [ModernCrossAttnTransformerBlock(d_model, nhead, dropout=dropout) for _ in range(depth[0])]
486
+ )
487
+
488
+ self.down_9_to_3 = LinearDownsample(d_model, shorten_factor=3)
489
+ self.down_3_to_1 = LinearDownsample(d_model, shorten_factor=3)
490
+ self.up_1_to_3 = LinearUpsample(d_model, shorten_factor=3)
491
+ self.up_3_to_9 = LinearUpsample(d_model, shorten_factor=3)
492
+
493
+ self.rotary_emb = RotaryEmbedding(dim=d_model//nhead, max_position_embeddings=max_seq_len)
494
+ # 5. 输出预测头 (只预测真实的 B 个 Token)
495
+ self.lm_head = nn.Linear(d_model, vocab_B)
496
+ self.use_pc = use_pc
497
+ if use_pc:
498
+ print(f'Point cloud encoder: {encoder_name} | freeze: {encoder_freeze}')
499
+ self.conditioner = PointConditioner(model_name=encoder_name, freeze=encoder_freeze, feature_dim=d_model)
500
+ # Learnable null condition for classifier-free guidance.
501
+ # Replaces self.conditioner(pc) when pc is None, so the model can learn
502
+ # an "unconditional" representation during cond-drop fine-tuning.
503
+ self.null_cond_len = 257 # matches PointConditioner.cond_length
504
+ self.null_cond = nn.Parameter(torch.randn(self.null_cond_len, d_model) * 0.02)
505
+
506
+ def forward(self, raw_tokens, noisy_refine_tokens, t, pc=None):
507
+ """
508
+ raw_tokens: [b, n], 取值 0~A-1
509
+ noisy_refine_tokens: [b, n], 取值 0~B (B是MASK)
510
+ t: [b], 当前的时间步
511
+ pc: [b, n, 6], 点云数据
512
+ """
513
+ if isinstance(raw_tokens, (list, tuple)):
514
+ return self.forward_varlen(raw_tokens, noisy_refine_tokens, t, pc)
515
+
516
+ b, n = raw_tokens.shape
517
+ device = raw_tokens.device
518
+
519
+ # 获取 Embedding
520
+ raw_emb = self.raw_embedding(raw_tokens) # [b, n, d_model]
521
+ refine_emb = self.refine_embedding(noisy_refine_tokens) # [b, n, d_model]
522
+
523
+ # 获取位置和时间 Embedding
524
+ time_emb = self.time_mlp(t).unsqueeze(1) # [b, 1, d_model]
525
+
526
+ # x 作为被去噪序列,cond 作为每层 cross-attn 的条件序列
527
+ x = raw_emb + refine_emb + time_emb
528
+
529
+ if self.use_pc:
530
+ if pc is None:
531
+ # Classifier-free guidance / cond-drop: use learnable null condition.
532
+ cond = self.null_cond.unsqueeze(0).expand(b, -1, -1)
533
+ else:
534
+ cond = self.conditioner(pc) # [b, 257, d_model]
535
+ else:
536
+ cond = None
537
+
538
+ if n % 9 != 0:
539
+ raise ValueError(f"Sequence length {n} is not compatible with 9->3->1 hourglass compression.")
540
+
541
+ dummy_tensor = torch.empty(1, device=x.device, dtype=torch.float32)
542
+
543
+ def rope_fn_for(seq_len):
544
+ cos, sin = self.rotary_emb(dummy_tensor, seq_len=seq_len)
545
+ return lambda q, k: apply_rotary_pos_emb(q, k, cos, sin)
546
+
547
+ # Encoder @ length 9
548
+ rope_fn_9 = rope_fn_for(x.shape[1])
549
+ for block in self.enc_blocks_9:
550
+ x = block(x, cond, rope_apply_fn=rope_fn_9)
551
+ skip_9 = x
552
+
553
+ # Downsample 9 -> 3
554
+ x = self.down_9_to_3(x)
555
+
556
+ # Encoder @ length 3
557
+ rope_fn_3 = rope_fn_for(x.shape[1])
558
+ for block in self.enc_blocks_3:
559
+ x = block(x, cond, rope_apply_fn=rope_fn_3)
560
+ skip_3 = x
561
+
562
+ # Downsample 3 -> 1 (or in general: n/3 -> n/9)
563
+ x = self.down_3_to_1(x)
564
+
565
+ # Bottleneck
566
+ rope_fn_1 = rope_fn_for(x.shape[1])
567
+ for block in self.bottleneck_blocks:
568
+ x = block(x, cond, rope_apply_fn=rope_fn_1)
569
+
570
+ # Upsample 1 -> 3 and long skip connection
571
+ x = self.up_1_to_3(x)
572
+ x = x + skip_3
573
+
574
+ # Decoder @ length 3
575
+ for block in self.dec_blocks_3:
576
+ x = block(x, cond, rope_apply_fn=rope_fn_3)
577
+
578
+ # Upsample 3 -> 9 and long skip connection
579
+ x = self.up_3_to_9(x)
580
+ x = x + skip_9
581
+
582
+ # Decoder @ length 9
583
+ for block in self.dec_blocks_9:
584
+ x = block(x, cond, rope_apply_fn=rope_fn_9)
585
+
586
+ # 预测 Logits
587
+ logits = self.lm_head(x) # [b, n, vocab_B]
588
+ return logits
589
+
590
+ def forward_varlen(self, raw_tokens, noisy_refine_tokens, t, pc=None):
591
+ device = t.device
592
+ raw_tokens = [seq.to(device=device, dtype=torch.long) for seq in raw_tokens]
593
+ noisy_refine_tokens = [seq.to(device=device, dtype=torch.long) for seq in noisy_refine_tokens]
594
+ lengths = torch.tensor([seq.numel() for seq in raw_tokens], device=device, dtype=torch.long)
595
+ if len(raw_tokens) == 0 or int(lengths.sum().item()) == 0:
596
+ raise ValueError("forward_varlen received an empty batch.")
597
+ if any(a.numel() != b.numel() for a, b in zip(raw_tokens, noisy_refine_tokens)):
598
+ raise ValueError("raw token lengths and refine token lengths must match per sample.")
599
+ if torch.any(lengths % 9 != 0):
600
+ bad = lengths[lengths % 9 != 0].detach().cpu().tolist()
601
+ raise ValueError(f"Varlen hourglass requires each sequence length to be divisible by 9, got {bad}.")
602
+
603
+ raw_packed = torch.cat(raw_tokens, dim=0)
604
+ refine_packed = torch.cat(noisy_refine_tokens, dim=0)
605
+ raw_emb = self.raw_embedding(raw_packed)
606
+ refine_emb = self.refine_embedding(refine_packed)
607
+ time_emb = self.time_mlp(t).repeat_interleave(lengths, dim=0)
608
+ x = raw_emb + refine_emb + time_emb
609
+
610
+ if self.use_pc:
611
+ if pc is None:
612
+ # Null condition path for CFG / cond-drop. Replicate the learned
613
+ # null_cond per sample and flatten to match the varlen layout.
614
+ bsz = len(raw_tokens)
615
+ null = self.null_cond # [257, d_model]
616
+ cond = null.unsqueeze(0).expand(bsz, -1, -1).reshape(-1, null.size(-1))
617
+ cond_lengths = torch.full((bsz,), self.null_cond_len, device=device, dtype=torch.long)
618
+ else:
619
+ pc = pc.to(device=device)
620
+ cond = self.conditioner(pc)
621
+ cond_lengths = torch.full((cond.size(0),), cond.size(1), device=device, dtype=torch.long)
622
+ cond = cond.reshape(-1, cond.size(-1))
623
+ else:
624
+ cond = None
625
+ cond_lengths = None
626
+
627
+ def rope_fn_for(cur_lengths):
628
+ position_ids = lengths_to_position_ids(cur_lengths)
629
+ cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
630
+ return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)
631
+
632
+ rope_fn_9 = rope_fn_for(lengths)
633
+ for block in self.enc_blocks_9:
634
+ x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
635
+ skip_9 = x
636
+ lengths_9 = lengths
637
+
638
+ x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
639
+ rope_fn_3 = rope_fn_for(lengths)
640
+ for block in self.enc_blocks_3:
641
+ x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
642
+ skip_3 = x
643
+ lengths_3 = lengths
644
+
645
+ x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
646
+ rope_fn_1 = rope_fn_for(lengths)
647
+ for block in self.bottleneck_blocks:
648
+ x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_1)
649
+
650
+ x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
651
+ if not torch.equal(lengths, lengths_3):
652
+ raise RuntimeError("Internal length mismatch after 1->3 upsample.")
653
+ x = x + skip_3
654
+ for block in self.dec_blocks_3:
655
+ x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
656
+
657
+ x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
658
+ if not torch.equal(lengths, lengths_9):
659
+ raise RuntimeError("Internal length mismatch after 3->9 upsample.")
660
+ x = x + skip_9
661
+ for block in self.dec_blocks_9:
662
+ x = block.forward_varlen(x, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
663
+
664
+ return self.lm_head(x)
665
+
666
+ def cosine_schedule(t, T):
667
+ """
668
+ 余弦调度函数。
669
+ t: 当前进度步数 (0 表示起始/全噪,T 表示结束/真实数据)
670
+ 返回: 需要 mask 的比例 (t=0时返回1.0,t=T时返回0.0)
671
+ """
672
+ # 确保 t 是浮点数以进行安全除法
673
+ return torch.cos((t / T) * (math.pi / 2))
674
+
675
+ return torch.cos((t / T) * (math.pi / 2))
676
+
677
+
678
+ def precompute_topology_tensors(raw_faces, tokens_per_face=9):
679
+ """
680
+ 预处理:完全向量化的拓扑索引构建(在去噪循环外执行一次即可)
681
+ 返回的是一维的张量,方便在 GPU 上做极速的 Scatter/Gather 计算
682
+ """
683
+ B, num_faces, _, _ = raw_faces.shape
684
+ device = raw_faces.device
685
+
686
+ # 转为 Python list 加速哈希构建
687
+ raw_faces_list = raw_faces.cpu().tolist()
688
+
689
+ token_indices_list = []
690
+ v_indices_list = []
691
+ v_shared_count = 0
692
+
693
+ for b in range(B):
694
+ vertex_dict = {}
695
+ for f in range(num_faces):
696
+ for v_idx in range(3):
697
+ coord = tuple(raw_faces_list[b][f][v_idx])
698
+
699
+ # 计算展平后的【全局绝对 Token 索引】
700
+ global_token_idx = b * (num_faces * tokens_per_face) + f * tokens_per_face + v_idx
701
+
702
+ if coord not in vertex_dict:
703
+ vertex_dict[coord] = []
704
+ vertex_dict[coord].append(global_token_idx)
705
+
706
+ # 仅保留被共享的顶点
707
+ for indices in vertex_dict.values():
708
+ if len(indices) > 1:
709
+ token_indices_list.extend(indices)
710
+ v_indices_list.extend([v_shared_count] * len(indices)) # 记录属于哪个全局共享顶点 ID
711
+ v_shared_count += 1
712
+
713
+ # ���果没有任何共享顶点,直接返回空
714
+ if len(token_indices_list) == 0:
715
+ return None, None, None, 0
716
+
717
+ # [M] 个共享 Token 的绝对索引
718
+ token_idx = torch.tensor(token_indices_list, dtype=torch.long, device=device)
719
+ # [M] 每个 Token 对应的全局顶点 ID
720
+ v_idx = torch.tensor(v_indices_list, dtype=torch.long, device=device)
721
+ # [M] 每个 Token 对应的绝对面片(Face)索引
722
+ face_idx = token_idx // tokens_per_face
723
+
724
+ return token_idx, v_idx, face_idx, v_shared_count
725
+
726
+ def sample_refine_tokens_with_topology(
727
+ model,
728
+ mask_token_id,
729
+ raw_tokens,
730
+ pc,
731
+ T_infer=10,
732
+ T_train=100,
733
+ tokens_per_face=9,
734
+ use_gumbel=False,
735
+ gumbel_temp_init=1.0,
736
+ use_soft_consensus=False,
737
+ consensus_start_step_ratio=0.0,
738
+ cfg_scale=0.0,
739
+ null_pc=None,
740
+ diag_stats=None,
741
+ forbidden_token_ids=(0,),
742
+ forbid_mask_token=True,
743
+ empty_token_ids=(),
744
+ empty_token_bias=0.0,
745
+ ):
746
+ """
747
+ 极致优化的拓扑一致性采样代码(零 Python Loop)
748
+
749
+ empty_token_ids / empty_token_bias: 对"空/背景"占用 token(实测 258,770 各~33%)的 logits
750
+ 减去 empty_token_bias(负偏置)。>0 -> 空 token 更不易被选 -> 更密的细分(更多面)。默认 no-op。
751
+
752
+ 优化 flag:
753
+ use_gumbel : 重 mask 时给 confidence 加温度退火的 Gumbel 噪声 (MaskGIT 标配)
754
+ gumbel_temp_init : 初始温度,随 step 线性退火到 0
755
+ use_soft_consensus : 共享顶点用概率分布聚合代替硬多数表决
756
+ consensus_start_step_ratio: 在 step / T_infer 达到该值前不做 topology consensus
757
+ cfg_scale : Classifier-free guidance 强度. 0 = 不开启 (单 forward).
758
+ >0 时每步多一次 forward (null_pc 作 uncond).
759
+ logits = logits_c + cfg_scale * (logits_c - logits_u)
760
+ null_pc : (B, N, C) 作为 uncond 输入. 若为 None, 用 zeros_like(pc).
761
+ 注: 当前模型未训练 cond-drop, 用 null pc 是 OOD 行为, 效果不保证.
762
+ diag_stats : 可选 dict, 把每步的诊断信息追加进去 (供测试用)
763
+ forbidden_token_ids : 禁止模型"决定"采样到的 token id 集合. 这些 id 的 logits
764
+ 在 softmax 前被设为 -inf, 后续 argmax / consensus 全部
765
+ 自动避开. 默认 (0,) 即 pad/EOS——堵住"模型中段吐出 0
766
+ -> save_stage_prediction 截断 -> mesh 只剩前半段"这条 bug.
767
+ forbid_mask_token : 同时也禁止预测 mask_token_id (默认 True). mask 是占位符,
768
+ 模型不应把它当最终答案.
769
+ """
770
+ model.eval()
771
+ b, n = raw_tokens.shape
772
+ device = raw_tokens.device
773
+ num_faces = n // tokens_per_face
774
+
775
+ # 收集要屏蔽的 token id; 真正裁剪并搬到 GPU 留到拿到 logits 后再做
776
+ # (mask_token_id 通常等于 vocab_B, 不在输出 logits 的维度内, 直接索引会越界)
777
+ _forbidden_raw = list(forbidden_token_ids) if forbidden_token_ids is not None else []
778
+ if forbid_mask_token:
779
+ _forbidden_raw.append(int(mask_token_id))
780
+ _forbidden_raw = sorted(set(int(x) for x in _forbidden_raw))
781
+ forbidden_t = None # 在首次 forward 后用 vocab_size 过滤构建
782
+
783
+ # --- 1. 预处理 (仅执行一次,耗时极短) ---
784
+ raw_faces = raw_tokens.view(b, num_faces, 3, 3)
785
+ token_idx, v_idx, face_idx, v_shared_count = precompute_topology_tensors(raw_faces, tokens_per_face)
786
+
787
+ # 初始化
788
+ current_tokens = torch.full((b, n), mask_token_id, dtype=torch.long, device=device)
789
+ unmasked_flags = torch.zeros((b, n), dtype=torch.bool, device=device)
790
+ vocab_size = None # 将在第一次 forward 后获取
791
+
792
+ def cosine_schedule(t, max_val):
793
+ return torch.cos(t * torch.pi * 0.5) * max_val
794
+
795
+ # 预备 null_pc (CFG)
796
+ # 三种模式:
797
+ # - null_pc 是 tensor : 用该 tensor 作为 uncond 输入(老模型可用 zeros_like(pc))
798
+ # - null_pc == "learned" : 调用 model(..., pc=None), 走模型的 self.null_cond 路径
799
+ # (只有 cond-drop fine-tune 过的模型才会有意义)
800
+ # - null_pc is None (默认): 用 zeros_like(pc), 行为同老版
801
+ use_learned_null = (isinstance(null_pc, str) and null_pc.lower() == "learned")
802
+ if cfg_scale > 0.0 and not use_learned_null:
803
+ if null_pc is None:
804
+ null_pc = torch.zeros_like(pc)
805
+ null_pc = null_pc.to(device=device, dtype=pc.dtype)
806
+
807
+ for step in range(T_infer):
808
+ current_progress_ratio = step / T_infer
809
+ t_model = torch.full((b,), current_progress_ratio, device=device, dtype=torch.float)
810
+
811
+ # [1] 前向预测 (可选 CFG: 双 forward 线性外推)
812
+ logits_c = model(raw_tokens, current_tokens, t_model, pc) # [b, n, vocab_size]
813
+ if cfg_scale > 0.0:
814
+ if use_learned_null:
815
+ logits_u = model(raw_tokens, current_tokens, t_model, None)
816
+ else:
817
+ logits_u = model(raw_tokens, current_tokens, t_model, null_pc)
818
+ logits = logits_c + cfg_scale * (logits_c - logits_u)
819
+ else:
820
+ logits = logits_c
821
+ if vocab_size is None:
822
+ vocab_size = logits.shape[-1]
823
+ # 现在知道真实输出 vocab 大小, 把越界的 id (例如 mask_token_id=vocab_B)
824
+ # 剔除掉后再构建 forbidden_t. 否则 logits[..., id] 会触发 CUDA assert.
825
+ _valid = [x for x in _forbidden_raw if 0 <= x < vocab_size]
826
+ if len(_valid) > 0:
827
+ forbidden_t = torch.tensor(_valid, dtype=torch.long, device=device)
828
+
829
+ # 禁止采样到 pad / EOS. softmax 前置 -inf, 后续 argmax 与
830
+ # consensus 聚合 (软/硬) 都因 probs[forbidden]=0 自动避开.
831
+ if forbidden_t is not None:
832
+ logits[..., forbidden_t] = float('-inf')
833
+
834
+ # 空行/空占用 token 负偏置: 减小其 logits -> 更不易被选 -> 更密的细分
835
+ if empty_token_bias != 0.0 and len(empty_token_ids) > 0:
836
+ _et = torch.tensor([int(i) for i in empty_token_ids if 0 <= int(i) < vocab_size],
837
+ dtype=torch.long, device=device)
838
+ if _et.numel() > 0:
839
+ logits[..., _et] = logits[..., _et] - float(empty_token_bias)
840
+
841
+ probs = F.softmax(logits, dim=-1)
842
+ max_probs, pred_tokens = probs.max(dim=-1) # [b, n]
843
+
844
+ force_mask = torch.zeros((b, n), dtype=torch.bool, device=device)
845
+ target_vals = None # 最后一步若开启 consensus 会用到
846
+
847
+ # ---------------------------------------------------------
848
+ # [2] 拓扑共识 (硬多数表决 / 软概率聚合)
849
+ # 通过 consensus_start_step_ratio 控制何时开启
850
+ # ---------------------------------------------------------
851
+ apply_consensus = (v_shared_count > 0) and (current_progress_ratio >= consensus_start_step_ratio)
852
+ if apply_consensus:
853
+ if use_soft_consensus:
854
+ # 软共识: 直接 scatter_add 共享 token 的概率分布
855
+ shared_probs = probs.view(-1, vocab_size)[token_idx] # [M, V]
856
+ agg_probs = torch.zeros((v_shared_count, vocab_size), dtype=shared_probs.dtype, device=device)
857
+ v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
858
+ agg_probs.scatter_add_(0, v_idx_expanded, shared_probs)
859
+
860
+ mode_vals = agg_probs.argmax(dim=-1)
861
+ target_vals = mode_vals[v_idx]
862
+
863
+ preds_flat = pred_tokens.view(-1)
864
+ shared_vals = preds_flat[token_idx]
865
+ is_disagree = (shared_vals != target_vals)
866
+ else:
867
+ # 硬多数表决
868
+ preds_flat = pred_tokens.view(-1)
869
+ shared_vals = preds_flat[token_idx]
870
+
871
+ one_hot_vals = F.one_hot(shared_vals, num_classes=vocab_size) # [M, V]
872
+ votes = torch.zeros((v_shared_count, vocab_size), dtype=torch.long, device=device)
873
+ v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
874
+ votes.scatter_add_(0, v_idx_expanded, one_hot_vals)
875
+
876
+ mode_vals = votes.argmax(dim=-1)
877
+ target_vals = mode_vals[v_idx]
878
+ is_disagree = (shared_vals != target_vals)
879
+
880
+ if is_disagree.any():
881
+ disagree_faces = face_idx[is_disagree]
882
+ force_mask_faces = torch.zeros(b * num_faces, dtype=torch.bool, device=device)
883
+ force_mask_faces[disagree_faces] = True
884
+ force_mask_flat = force_mask_faces.repeat_interleave(tokens_per_face)
885
+ force_mask = force_mask_flat.view(b, n)
886
+
887
+ # ---------------------------------------------------------
888
+ # [3] 置信度干预与 Mask 更新
889
+ # ---------------------------------------------------------
890
+ # 注意: 这里先克隆,避免 Gumbel 噪声破坏掉之后我们对 unmasked/force 的 ±inf 标记
891
+ confidence = max_probs.clone()
892
+ if use_gumbel:
893
+ # 温度随进度线性退火到 0
894
+ temperature = gumbel_temp_init * (1.0 - current_progress_ratio)
895
+ if temperature > 0:
896
+ uniform = torch.rand_like(confidence).clamp_(1e-9, 1.0 - 1e-9)
897
+ gumbel = -torch.log(-torch.log(uniform))
898
+ confidence = confidence + temperature * gumbel
899
+
900
+ # 已确认的位置不应被再 mask
901
+ confidence[unmasked_flags] = float('inf')
902
+
903
+ if apply_consensus:
904
+ confidence[force_mask] = -float('inf')
905
+ unmasked_flags = unmasked_flags & (~force_mask)
906
+
907
+ # 诊断信息
908
+ if diag_stats is not None:
909
+ diag_stats.setdefault('force_count', []).append(int(force_mask.sum().item()))
910
+ diag_stats.setdefault('avg_conf', []).append(float(max_probs.mean().item()))
911
+
912
+ if step == T_infer - 1:
913
+ # 最后一步:用共识算出的最终值硬覆盖所有可能残余的不一致
914
+ if apply_consensus and target_vals is not None:
915
+ preds_flat = pred_tokens.view(-1)
916
+ preds_flat[token_idx] = target_vals
917
+ pred_tokens = preds_flat.view(b, n)
918
+
919
+ current_tokens = torch.where(unmasked_flags, current_tokens, pred_tokens)
920
+
921
+ else:
922
+ next_progress_ratio = (step + 1) / T_infer
923
+ ratio = cosine_schedule(torch.tensor(next_progress_ratio), 1.0).item()
924
+ num_to_mask = int(ratio * n)
925
+
926
+ max_forced_in_batch = int(force_mask.sum(dim=-1).max().item()) if apply_consensus else 0
927
+ actual_k = min(max(num_to_mask, max_forced_in_batch), n)
928
+
929
+ _, mask_indices = torch.topk(-confidence, k=actual_k, dim=-1)
930
+
931
+ new_mask = torch.zeros_like(unmasked_flags)
932
+ new_mask.scatter_(1, mask_indices, True)
933
+
934
+ # 兜底: force_mask 必入 new_mask (Gumbel 噪声不会让 -inf 漏选, 但保留以防边界条件)
935
+ new_mask = new_mask | force_mask
936
+
937
+ current_tokens = torch.where(new_mask, mask_token_id, pred_tokens)
938
+ unmasked_flags = ~new_mask
939
+
940
+ return current_tokens
941
+
942
+
943
+ def precompute_topology_tensors_fm(raw_faces, tokens_per_face=9):
944
+ """precompute_topology_tensors 的扩展版: 除了 F(refine 位置 0/1/2 = 3 个顶点/父的占用,
945
+ 按顶点 coord 分组), 额外把 intra(M_inner, refine 位置 3/4/5, 分别对应父 0/1/2 的内部连接
946
+ 矩阵)也按其父 coord 分组。F 与 intra 用不同"通道标签"分开归组, 绝不互相表决。
947
+
948
+ token 布局(与 fix3 同构, 见 tokenizer decode 的 chunk[9..17]):
949
+ refine[0,1,2] = F0,F1,F2(父 0/1/2 占用) -> 按父 coord 分组(每父冗余, 应一致)
950
+ refine[3,4,5] = M00,M11,M22(父 0/1/2 intra)-> 按父 coord 分组(每父冗余, 应一致)
951
+ refine[6,7,8] = M01,M02,M12(inter, 每面本地)-> 不分组、不表决
952
+
953
+ 返回 (token_idx, v_idx, face_idx, v_shared_count), 结构与原版完全一致, 可直接喂给
954
+ 采样循环(F 组的众数落在 F 值域, intra 组的众数落在 intra 值域, 天然不串)。
955
+ """
956
+ B, num_faces, _, _ = raw_faces.shape
957
+ device = raw_faces.device
958
+ raw_faces_list = raw_faces.cpu().tolist()
959
+
960
+ # 每个"冗余通道" = (refine 位置, 该位置对应的父编号, 通道标签)。
961
+ # F: 位置 p -> 父 p; intra: 位置 3+p -> 父 p。inter(6/7/8)不在其中。
962
+ channels = [(0, 0, "F"), (1, 1, "F"), (2, 2, "F"),
963
+ (3, 0, "I"), (4, 1, "I"), (5, 2, "I")]
964
+
965
+ token_indices_list = []
966
+ v_indices_list = []
967
+ v_shared_count = 0
968
+ for b in range(B):
969
+ group_dict = {}
970
+ for f in range(num_faces):
971
+ for pos, parent, tag in channels:
972
+ coord = tuple(raw_faces_list[b][f][parent]) # 该父的坐标
973
+ key = (tag, coord) # F / intra 分开归组
974
+ global_token_idx = b * (num_faces * tokens_per_face) + f * tokens_per_face + pos
975
+ group_dict.setdefault(key, []).append(global_token_idx)
976
+ for indices in group_dict.values():
977
+ if len(indices) > 1: # 仅保留被共享(>1 面引用)的
978
+ token_indices_list.extend(indices)
979
+ v_indices_list.extend([v_shared_count] * len(indices))
980
+ v_shared_count += 1
981
+
982
+ if len(token_indices_list) == 0:
983
+ return None, None, None, 0
984
+ token_idx = torch.tensor(token_indices_list, dtype=torch.long, device=device)
985
+ v_idx = torch.tensor(v_indices_list, dtype=torch.long, device=device)
986
+ face_idx = token_idx // tokens_per_face
987
+ return token_idx, v_idx, face_idx, v_shared_count
988
+
989
+
990
+ def sample_refine_tokens_with_topology_fm(
991
+ model,
992
+ mask_token_id,
993
+ raw_tokens,
994
+ pc,
995
+ T_infer=10,
996
+ T_train=100,
997
+ tokens_per_face=9,
998
+ use_gumbel=False,
999
+ gumbel_temp_init=1.0,
1000
+ use_soft_consensus=False,
1001
+ consensus_start_step_ratio=0.0,
1002
+ cfg_scale=0.0,
1003
+ null_pc=None,
1004
+ diag_stats=None,
1005
+ forbidden_token_ids=(0,),
1006
+ forbid_mask_token=True,
1007
+ ):
1008
+ """sample_refine_tokens_with_topology 的"F + M_inner(intra)双对齐"版。
1009
+
1010
+ 与原版唯一区别: 拓扑分组用 precompute_topology_tensors_fm ——
1011
+ 原版只把 refine 位置 0/1/2(F, 顶点占用)按共享顶点分组表决;
1012
+ 本版额外把 3/4/5(intra, 每父内部连接矩阵)也按共享父分组表决。
1013
+ 因此每步的共识检测/末步硬覆盖会同时作用在 F 与 intra 上, 任一不一致的面整块 9 token
1014
+ 重掩重预测。inter(6/7/8)每面本地、无冗余, 仍不表决(但会随所在面被连坐重掩)。
1015
+
1016
+ 其余采样逻辑(CFG 双前向、forbidden、Gumbel、cosine 揭示)与原版逐行一致。
1017
+ 参数含义见 sample_refine_tokens_with_topology。
1018
+ """
1019
+ model.eval()
1020
+ b, n = raw_tokens.shape
1021
+ device = raw_tokens.device
1022
+ num_faces = n // tokens_per_face
1023
+
1024
+ _forbidden_raw = list(forbidden_token_ids) if forbidden_token_ids is not None else []
1025
+ if forbid_mask_token:
1026
+ _forbidden_raw.append(int(mask_token_id))
1027
+ _forbidden_raw = sorted(set(int(x) for x in _forbidden_raw))
1028
+ forbidden_t = None
1029
+
1030
+ # --- 1. 预处理: F + intra 双通道拓扑分组(唯一改动) ---
1031
+ raw_faces = raw_tokens.view(b, num_faces, 3, 3)
1032
+ token_idx, v_idx, face_idx, v_shared_count = precompute_topology_tensors_fm(raw_faces, tokens_per_face)
1033
+
1034
+ current_tokens = torch.full((b, n), mask_token_id, dtype=torch.long, device=device)
1035
+ unmasked_flags = torch.zeros((b, n), dtype=torch.bool, device=device)
1036
+ vocab_size = None
1037
+
1038
+ def cosine_schedule(t, max_val):
1039
+ return torch.cos(t * torch.pi * 0.5) * max_val
1040
+
1041
+ use_learned_null = (isinstance(null_pc, str) and null_pc.lower() == "learned")
1042
+ if cfg_scale > 0.0 and not use_learned_null:
1043
+ if null_pc is None:
1044
+ null_pc = torch.zeros_like(pc)
1045
+ null_pc = null_pc.to(device=device, dtype=pc.dtype)
1046
+
1047
+ for step in range(T_infer):
1048
+ current_progress_ratio = step / T_infer
1049
+ t_model = torch.full((b,), current_progress_ratio, device=device, dtype=torch.float)
1050
+
1051
+ # [1] 前向预测 (可选 CFG)
1052
+ logits_c = model(raw_tokens, current_tokens, t_model, pc)
1053
+ if cfg_scale > 0.0:
1054
+ if use_learned_null:
1055
+ logits_u = model(raw_tokens, current_tokens, t_model, None)
1056
+ else:
1057
+ logits_u = model(raw_tokens, current_tokens, t_model, null_pc)
1058
+ logits = logits_c + cfg_scale * (logits_c - logits_u)
1059
+ else:
1060
+ logits = logits_c
1061
+ if vocab_size is None:
1062
+ vocab_size = logits.shape[-1]
1063
+ _valid = [x for x in _forbidden_raw if 0 <= x < vocab_size]
1064
+ if len(_valid) > 0:
1065
+ forbidden_t = torch.tensor(_valid, dtype=torch.long, device=device)
1066
+
1067
+ if forbidden_t is not None:
1068
+ logits[..., forbidden_t] = float('-inf')
1069
+
1070
+ probs = F.softmax(logits, dim=-1)
1071
+ max_probs, pred_tokens = probs.max(dim=-1)
1072
+
1073
+ force_mask = torch.zeros((b, n), dtype=torch.bool, device=device)
1074
+ target_vals = None
1075
+
1076
+ # [2] 拓扑共识 (F + intra 同时, 因两者在 v_idx 里是不同组, 自动分别表决)
1077
+ apply_consensus = (v_shared_count > 0) and (current_progress_ratio >= consensus_start_step_ratio)
1078
+ if apply_consensus:
1079
+ if use_soft_consensus:
1080
+ shared_probs = probs.view(-1, vocab_size)[token_idx]
1081
+ agg_probs = torch.zeros((v_shared_count, vocab_size), dtype=shared_probs.dtype, device=device)
1082
+ v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
1083
+ agg_probs.scatter_add_(0, v_idx_expanded, shared_probs)
1084
+ mode_vals = agg_probs.argmax(dim=-1)
1085
+ target_vals = mode_vals[v_idx]
1086
+ preds_flat = pred_tokens.view(-1)
1087
+ shared_vals = preds_flat[token_idx]
1088
+ is_disagree = (shared_vals != target_vals)
1089
+ else:
1090
+ preds_flat = pred_tokens.view(-1)
1091
+ shared_vals = preds_flat[token_idx]
1092
+ one_hot_vals = F.one_hot(shared_vals, num_classes=vocab_size)
1093
+ votes = torch.zeros((v_shared_count, vocab_size), dtype=torch.long, device=device)
1094
+ v_idx_expanded = v_idx.unsqueeze(1).expand(-1, vocab_size)
1095
+ votes.scatter_add_(0, v_idx_expanded, one_hot_vals)
1096
+ mode_vals = votes.argmax(dim=-1)
1097
+ target_vals = mode_vals[v_idx]
1098
+ is_disagree = (shared_vals != target_vals)
1099
+
1100
+ if is_disagree.any():
1101
+ disagree_faces = face_idx[is_disagree]
1102
+ force_mask_faces = torch.zeros(b * num_faces, dtype=torch.bool, device=device)
1103
+ force_mask_faces[disagree_faces] = True
1104
+ force_mask_flat = force_mask_faces.repeat_interleave(tokens_per_face)
1105
+ force_mask = force_mask_flat.view(b, n)
1106
+
1107
+ # [3] 置信度干预与 Mask 更新
1108
+ confidence = max_probs.clone()
1109
+ if use_gumbel:
1110
+ temperature = gumbel_temp_init * (1.0 - current_progress_ratio)
1111
+ if temperature > 0:
1112
+ uniform = torch.rand_like(confidence).clamp_(1e-9, 1.0 - 1e-9)
1113
+ gumbel = -torch.log(-torch.log(uniform))
1114
+ confidence = confidence + temperature * gumbel
1115
+
1116
+ confidence[unmasked_flags] = float('inf')
1117
+ if apply_consensus:
1118
+ confidence[force_mask] = -float('inf')
1119
+ unmasked_flags = unmasked_flags & (~force_mask)
1120
+
1121
+ if diag_stats is not None:
1122
+ diag_stats.setdefault('force_count', []).append(int(force_mask.sum().item()))
1123
+ diag_stats.setdefault('avg_conf', []).append(float(max_probs.mean().item()))
1124
+
1125
+ if step == T_infer - 1:
1126
+ if apply_consensus and target_vals is not None:
1127
+ preds_flat = pred_tokens.view(-1)
1128
+ preds_flat[token_idx] = target_vals
1129
+ pred_tokens = preds_flat.view(b, n)
1130
+ current_tokens = torch.where(unmasked_flags, current_tokens, pred_tokens)
1131
+ else:
1132
+ next_progress_ratio = (step + 1) / T_infer
1133
+ ratio = cosine_schedule(torch.tensor(next_progress_ratio), 1.0).item()
1134
+ num_to_mask = int(ratio * n)
1135
+ max_forced_in_batch = int(force_mask.sum(dim=-1).max().item()) if apply_consensus else 0
1136
+ actual_k = min(max(num_to_mask, max_forced_in_batch), n)
1137
+ _, mask_indices = torch.topk(-confidence, k=actual_k, dim=-1)
1138
+ new_mask = torch.zeros_like(unmasked_flags)
1139
+ new_mask.scatter_(1, mask_indices, True)
1140
+ new_mask = new_mask | force_mask
1141
+ current_tokens = torch.where(new_mask, mask_token_id, pred_tokens)
1142
+ unmasked_flags = ~new_mask
1143
+
1144
+ return current_tokens
1145
+
1146
+
1147
+
1148
+
1149
+ def precompute_topology_tensors_fmi(raw_faces, tokens_per_face=9):
1150
+ """F + intra + inter 三通道拓扑分组(逐步共识用)。
1151
+ F (refine 0/1/2): 按单父 coord 分组(每父冗余, 引用同父的面应一致)
1152
+ intra(refine 3/4/5): 按单父 coord 分组(每父冗余)
1153
+ inter(refine 6/7/8): 按"无序父对 coord"分组 —— 两个面若恰好共享 2 顶点(=共享一条粗边)
1154
+ 就共享同一父对, 其 inter 矩阵应一致; tokenizer 每面 3 父排序+按
1155
+ (小,大)取 inter, 同一父对在任何面里 token 值相同(朝向天然规范)。
1156
+ 三通道用不同 key 前缀("F"/"I"/"O")互不表决。返回结构与 precompute_topology_tensors 完全一致,
1157
+ 可直接喂采样循环(各通道众数落各自值域, 不串)。
1158
+ """
1159
+ B, num_faces, _, _ = raw_faces.shape
1160
+ device = raw_faces.device
1161
+ raw_faces_list = raw_faces.cpu().tolist()
1162
+ single_ch = [(0, 0, "F"), (1, 1, "F"), (2, 2, "F"), (3, 0, "I"), (4, 1, "I"), (5, 2, "I")]
1163
+ pair_ch = [(6, 0, 1), (7, 0, 2), (8, 1, 2)] # inter 位置 -> 父对
1164
+ token_indices_list, v_indices_list, v_shared_count = [], [], 0
1165
+ for b in range(B):
1166
+ gd = {}
1167
+ for f in range(num_faces):
1168
+ fc = raw_faces_list[b][f]
1169
+ base = b * (num_faces * tokens_per_face) + f * tokens_per_face
1170
+ for pos, parent, tag in single_ch:
1171
+ gd.setdefault((tag, tuple(fc[parent])), []).append(base + pos)
1172
+ for pos, a, bp in pair_ch:
1173
+ ca, cb = tuple(fc[a]), tuple(fc[bp])
1174
+ pair = (ca, cb) if ca <= cb else (cb, ca)
1175
+ gd.setdefault(("O", pair), []).append(base + pos)
1176
+ for indices in gd.values():
1177
+ if len(indices) > 1:
1178
+ token_indices_list.extend(indices)
1179
+ v_indices_list.extend([v_shared_count] * len(indices))
1180
+ v_shared_count += 1
1181
+ if len(token_indices_list) == 0:
1182
+ return None, None, None, 0
1183
+ token_idx = torch.tensor(token_indices_list, dtype=torch.long, device=device)
1184
+ v_idx = torch.tensor(v_indices_list, dtype=torch.long, device=device)
1185
+ face_idx = token_idx // tokens_per_face
1186
+ return token_idx, v_idx, face_idx, v_shared_count
1187
+
1188
+
mulres_infer/modules/__init__.py ADDED
File without changes
mulres_infer/modules/attention.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """注意力与 Transformer 原语(从 pure_iflame_with_condition 提取, 只保留 MLA 及其依赖)。
2
+
3
+ MLA 是细分器 subdivider.DiscreteDiffusionTransformer 使用的多头潜在注意力块。
4
+ 原目录里的 MeshRes / MLCA / LinearDownsample / LinearUpsample / MultiheadCrossFlashrope
5
+ 都不在推理路径上, 已删除。
6
+ """
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import math
11
+ import torch.nn.functional as F
12
+ from torch import nn
13
+ import time #cal_time new at 10.21
14
+ import numpy as np
15
+ from typing import List, Optional, Tuple
16
+ from tqdm import tqdm
17
+ from einops import rearrange, repeat, pack
18
+
19
+
20
+ # ---- 原 rms_norm.py ----
21
+
22
+ class RMSNorm(nn.Module):
23
+ def __init__(self, dim: int, eps: float = 1e-6, elementwise_affine=True, memory_efficient=False):
24
+ super().__init__()
25
+ self.dim = dim
26
+ self.eps = eps
27
+ self.elementwise_affine = elementwise_affine
28
+ if self.elementwise_affine:
29
+ self.weight = nn.Parameter(torch.ones(dim))
30
+ else:
31
+ self.register_parameter('weight', None)
32
+
33
+ def _norm(self, x):
34
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
35
+
36
+ def forward(self, x):
37
+ output = self._norm(x.float()).type_as(x)
38
+ if self.weight is not None:
39
+ output = output * self.weight
40
+ return output
41
+
42
+ def extra_repr(self) -> str:
43
+ return f'dim={self.dim}, eps={self.eps}, elementwise_affine={self.elementwise_affine}'
44
+
45
+ # ---- 原 multihead_flashdiff_2.py ----
46
+
47
+ class Rotary(nn.Module):
48
+ def __init__(self, dim: int, max_seq_len=9*50000, dtype=torch.float16):
49
+ super().__init__()
50
+ # half-truncate RoPE by @YouJiacheng (w/ base freq tuning)
51
+ angular_freq = (1 / 1024) ** torch.linspace(0, 1, steps=dim//4, dtype=dtype)
52
+ angular_freq = torch.cat([angular_freq, angular_freq.new_zeros(dim//4)])
53
+ t = torch.arange(max_seq_len, dtype=dtype)
54
+ theta = torch.einsum("i,j -> ij", t, angular_freq)
55
+ self.register_buffer('cos', theta.cos(), persistent=False)
56
+ self.register_buffer('sin', theta.sin(), persistent=False)
57
+ self.max_seq_len = max_seq_len
58
+ self.dtype = dtype
59
+
60
+ def forward(self, x_BTHD, offset=0):
61
+ """
62
+ Args:
63
+ x_BTHD: Input tensor of shape [batch, seq_len, num_heads, dim]
64
+ offset: Position offset for the sequence
65
+ """
66
+ batch_size, seq_len = x_BTHD.size(0), x_BTHD.size(-3)
67
+ assert torch.max(offset + seq_len) <= self.max_seq_len, f"Sequence length {offset + seq_len} exceeds maximum length {self.max_seq_len}"
68
+
69
+ # Get the appropriate slice of cos/sin based on offset
70
+ positions = torch.arange(seq_len, device=x_BTHD.device).expand(batch_size, -1) + offset.unsqueeze(-1)
71
+ cos = self.cos[positions].unsqueeze(-2)
72
+ sin = self.sin[positions].unsqueeze(-2)
73
+
74
+ x1, x2 = x_BTHD.to(dtype=self.dtype).chunk(2, dim=-1)
75
+ y1 = x1 * cos - x2 * sin
76
+ y2 = x1 * sin + x2 * cos
77
+ return torch.cat((y1, y2), dim=-1)
78
+
79
+ class MultiheadFlashrope(nn.Module):
80
+ def __init__(self, args, embed_dim, num_heads):
81
+ super().__init__()
82
+ self.args = args
83
+ kvcache_method, kv_cache_window_size, window_stride = args
84
+ self.embed_dim = embed_dim
85
+ self.num_heads = num_heads
86
+ self.num_kv_heads = num_heads
87
+ self.head_dim = embed_dim // num_heads
88
+ self.scaling = self.head_dim ** -0.5
89
+ self.kv_cache_window_size = kv_cache_window_size
90
+ self.window_stride = window_stride
91
+ self.kvcache_method = kvcache_method
92
+ # KV cache related attributes
93
+ self.kv_cache_enabled = False
94
+ self.k_cache = None
95
+ self.v_cache = None
96
+ self.cache_pos = 0 # Track position in cache
97
+
98
+ self.q_proj = nn.Linear(embed_dim, embed_dim, bias=False)
99
+ self.k_proj = nn.Linear(embed_dim, embed_dim, bias=False)
100
+ self.v_proj = nn.Linear(embed_dim, embed_dim, bias=False)
101
+ self.out_proj = nn.Linear(embed_dim, embed_dim, bias=False)
102
+ self.rotary = Rotary(self.head_dim)
103
+ self.sliding_window_cur_len = 0
104
+ # self.attention_scores = None
105
+
106
+ def forward(self, x, start_pos, context=None, attention_mask=None, causal=False):
107
+ """
108
+ Args:
109
+ x: [batch_size, tgt_len, embed_dim]
110
+ start_pos: [batch_size]
111
+ context: Optional context for cross-attention [batch_size, src_len, embed_dim]
112
+ """
113
+ bsz, tgt_len, embed_dim = x.size()
114
+ # Project inputs to q, k, v
115
+ q = self.q_proj(x)
116
+ k = self.k_proj(context if context is not None else x)
117
+ v = self.v_proj(context if context is not None else x)
118
+
119
+ # Reshape to [batch_size, seq_len, num_heads, head_dim]
120
+ q = q.view(bsz, -1, self.num_heads, self.head_dim)
121
+ k = k.view(bsz, -1, self.num_heads, self.head_dim)
122
+ v = v.view(bsz, -1, self.num_heads, self.head_dim)
123
+ q = self.rotary(q, offset=start_pos)
124
+ # Normal operation without cache
125
+ k = self.rotary(k, offset=start_pos)
126
+ if attention_mask is not None:
127
+ attn_output = self.attention_pytorch_optimized(q, k, v, mask=attention_mask)
128
+ else:
129
+ attn_output = self.attention_pytorch_optimized(q, k, v, causal=causal)
130
+
131
+ # Reshape output
132
+ attn_output = attn_output.contiguous().view(bsz, tgt_len, embed_dim)
133
+ attn_output = self.out_proj(attn_output)
134
+
135
+ return attn_output
136
+
137
+ def attention_pytorch_optimized(self, q, k, v, mask=None, causal=False):
138
+ b, seq_q, head_num, c = q.shape
139
+ seq_k = k.shape[1]
140
+ q = q.permute(0, 2, 1, 3)
141
+ k = k.permute(0, 2, 1, 3)
142
+ v = v.permute(0, 2, 1, 3)
143
+ if mask is not None:
144
+ atten_mask_npu = mask.unsqueeze(1).expand(b, head_num, seq_q, seq_k)
145
+ atten_mask_npu = torch.logical_not(atten_mask_npu)
146
+ attn_output = F.scaled_dot_product_attention(q, k, v, attn_mask=atten_mask_npu)
147
+ else:
148
+ if causal:
149
+ attn_output = F.scaled_dot_product_attention(q, k, v, is_causal=True)
150
+ else:
151
+ attn_output = F.scaled_dot_product_attention(q, k, v)
152
+
153
+ return attn_output.permute(0, 2, 1, 3)
154
+
155
+ def attention_pytorch(self, q, k, v, causal=False):
156
+ # [batch_size, num_heads, seq_len, head_dim]
157
+ q = q.permute(0, 2, 1, 3)
158
+ k = k.permute(0, 2, 1, 3)
159
+ v = v.permute(0, 2, 1, 3)
160
+
161
+ attn_weights = torch.matmul(q, k.transpose(-2, -1)) * self.scaling
162
+
163
+ if causal:
164
+ seq_len = q.size(-2)
165
+ causal_mask = torch.triu(
166
+ torch.ones(seq_len, seq_len, dtype=torch.bool, device=q.device),
167
+ diagonal=1
168
+ )
169
+ attn_weights.masked_fill_(causal_mask, float('-inf'))
170
+
171
+ attn_weights = F.softmax(attn_weights, dim=-1)
172
+
173
+
174
+ attn_output = torch.matmul(attn_weights, v)
175
+
176
+
177
+ return attn_output.permute(0, 2, 1, 3)
178
+
179
+ def empty_kv_cache(self, batch_size: int, dtype: torch.dtype):
180
+ """Initialize empty KV cache"""
181
+ self.kv_cache_enabled=True
182
+
183
+ device = next(self.parameters()).device
184
+ kv_cache_maxlen = self.kv_cache_window_size
185
+ # Initialize empty caches
186
+ k_cache = torch.zeros(
187
+ batch_size,
188
+ kv_cache_maxlen,
189
+ self.num_heads,
190
+ self.head_dim,
191
+ dtype=dtype,
192
+ device=device
193
+ )
194
+ self.k_cache=k_cache
195
+
196
+ v_cache = torch.zeros(
197
+ batch_size,
198
+ kv_cache_maxlen,
199
+ self.num_heads,
200
+ self.head_dim,
201
+ dtype=dtype,
202
+ device=device
203
+ )
204
+ self.v_cache=v_cache
205
+ self.cache_pos = 0 # Reset cache position
206
+
207
+ element_size = {
208
+ torch.float32: 4,
209
+ torch.float16: 2,
210
+ torch.bfloat16: 2,
211
+ }[dtype]
212
+ k_cache_size = (batch_size *
213
+ kv_cache_maxlen *
214
+ self.num_heads *
215
+ self.head_dim *
216
+ element_size)
217
+ v_cache_size = k_cache_size
218
+ total_cache_size = k_cache_size + v_cache_size
219
+ cache_size_gb = total_cache_size / (1024**3)
220
+ return cache_size_gb
221
+
222
+
223
+ def reset_cache(self):
224
+ """Reset KV cache state"""
225
+ self.k_cache = None
226
+ self.v_cache = None
227
+ self.cache_pos = 0
228
+
229
+ class CrossAttention(nn.Module):
230
+ def __init__(self, dim, context_dim, n_heads, dropout=0.0):
231
+ super().__init__()
232
+ self.n_heads = n_heads
233
+ self.scale = (dim // n_heads) ** -0.5
234
+
235
+ self.q_proj = nn.Linear(dim, dim, bias=False)
236
+ self.kv_proj = nn.Linear(context_dim, 2 * dim, bias=False)
237
+ self.out_proj = nn.Linear(dim, dim, bias=False)
238
+ self.dropout = nn.Dropout(dropout)
239
+
240
+ def forward(self, x, context):
241
+ # x: [batch, seq_len, dim], context: [batch, context_len, context_dim]
242
+ B, N, C = x.shape
243
+ _, M, _ = context.shape
244
+ H = self.n_heads
245
+
246
+ # Linear projections
247
+ q = self.q_proj(x).view(B, N, H, C // H) # [B, H, seq_len, dim//H]
248
+ k, v = self.kv_proj(context).chunk(2, dim=-1)
249
+ k = k.view(B, M, H, C // H) # [B, context_len, H, dim//H]
250
+ v = v.view(B, M, H, C // H) # [B, context_len, H, dim//H]
251
+
252
+ attn_output = self.attention_pytorch_optimized(q,k,v)
253
+ attn_output = attn_output.contiguous().view(B, N, C)
254
+
255
+ # q = self.q_proj(x).view(B, N, H, C // H).transpose(1, 2) # [B, H, seq_len, dim//H]
256
+ # k, v = self.kv_proj(context).chunk(2, dim=-1)
257
+ # k = k.view(B, M, H, C // H).transpose(1, 2) # [B, H, context_len, dim//H]
258
+ # v = v.view(B, M, H, C // H).transpose(1, 2) # [B, H, context_len, dim//H]
259
+ # # Scaled dot-product attention
260
+ # attn_weights = (q @ k.transpose(-2, -1)) * self.scale # [B, H, seq_len, context_len]
261
+ # attn_weights = attn_weights.softmax(dim=-1)
262
+ # attn_output = (attn_weights @ v).transpose(1, 2).contiguous().view(B, N, C) # [B, seq_len, dim]
263
+ out = self.out_proj(attn_output)
264
+ out = self.dropout(out)
265
+
266
+ return out
267
+
268
+ def attention_pytorch_optimized(self, q, k, v, mask=None, causal=False):
269
+ b, seq_q, head_num, c = q.shape
270
+ seq_k = k.shape[1]
271
+ q = q.permute(0, 2, 1, 3)
272
+ k = k.permute(0, 2, 1, 3)
273
+ v = v.permute(0, 2, 1, 3)
274
+ if mask is not None:
275
+ atten_mask_npu = mask.unsqueeze(1).expand(b, head_num, seq_q, seq_k)
276
+ atten_mask_npu = torch.logical_not(atten_mask_npu)
277
+ attn_output = F.scaled_dot_product_attention(q, k, v, attn_mask=atten_mask_npu)
278
+ else:
279
+ if causal:
280
+ attn_output = F.scaled_dot_product_attention(q, k, v, is_causal=True)
281
+ else:
282
+ attn_output = F.scaled_dot_product_attention(q, k, v)
283
+
284
+ return attn_output.permute(0, 2, 1, 3)
285
+
286
+ # ---- 原 transformer.py ----
287
+
288
+ class SwiGLU(nn.Module):
289
+ def __init__(self, embed_dim):
290
+ super(SwiGLU, self).__init__()
291
+ self.proj1 = nn.Linear(embed_dim, int(8 / 3 * embed_dim)) # XWG
292
+ self.proj2 = nn.Linear(embed_dim, int(8 / 3 * embed_dim)) # XW1
293
+ self.proj3 = nn.Linear(int(8 / 3 * embed_dim), embed_dim) # W2
294
+
295
+ def forward(self, x):
296
+ # Apply Swish (SiLU in PyTorch) to the first projection
297
+ x_proj1 = F.silu(self.proj1(x)) # Swish(XWG)
298
+ # Apply the second projection
299
+ x_proj2 = self.proj2(x) # XW1
300
+ # Element-wise multiplication
301
+ x_glu = x_proj1 * x_proj2 # (swish(XWG) ⊙ XW1)
302
+ # Final projection back to the original dimension
303
+ output = self.proj3(x_glu) # (swish(XWG) ⊙ XW1)W2
304
+
305
+ return output
306
+
307
+ class MLA(nn.Module):
308
+ def __init__(self, embed_dim, num_heads, args, cross_embed_dim, causal=True, dropout=0.):
309
+ """
310
+ Differential Transformer Block with optional causal self-attention or cross-attention
311
+ and automatic application of ROPE.
312
+
313
+ Args:
314
+ embed_dim (int): Embedding dimension.
315
+ num_heads (int): Number of attention heads.
316
+ args (Namespace): Arguments for attention settings.
317
+ causal (bool): Whether to use causal masking by default for self-attention.
318
+ """
319
+ super(MLA, self).__init__()
320
+ # Multihead differential attention module
321
+ self.attn = MultiheadFlashrope(args, embed_dim, num_heads)
322
+ self.causal = causal
323
+
324
+ self.feed_forward = SwiGLU(embed_dim)
325
+ self.norm1 = RMSNorm(embed_dim, eps=1e-5)
326
+ self.norm2 = RMSNorm(embed_dim, eps=1e-5)
327
+ self.attn_dropout = nn.Dropout(dropout)
328
+ self.ff_dropout = nn.Dropout(dropout)
329
+ self.residual_dropout = nn.Dropout(dropout)
330
+ if cross_embed_dim != -1:
331
+ self.cross_norm = RMSNorm(embed_dim, eps=1e-5)
332
+ self.cross_attn = CrossAttention(embed_dim, cross_embed_dim, num_heads)
333
+
334
+ def forward(
335
+ self, x, start, conds=None, attention_mask=None, use_cache=False
336
+ ):
337
+ """
338
+ Args:
339
+ x: Input tensor
340
+ use_cache: Whether to use KV cache
341
+ """
342
+ # Enable/disable KV cache in attention module
343
+ self.attn.kv_cache_enabled = use_cache
344
+
345
+ # 第一个残差连接:注意力 + dropout
346
+ residual = x
347
+ x = self.norm1(x)
348
+ attn_out = self.attn(
349
+ x,
350
+ start_pos=start,
351
+ attention_mask=attention_mask,
352
+ causal=self.causal,
353
+ )
354
+ attn_out = self.attn_dropout(attn_out)
355
+ x = residual + attn_out
356
+ if conds is not None:
357
+ x_skip = x
358
+ x = self.cross_norm(x)
359
+ x = self.cross_attn(x, conds) + x_skip
360
+
361
+ # 第二个残差连接:前馈网络 + dropout
362
+ residual = x
363
+ ff_out = self.ff_dropout((self.feed_forward(self.norm2(x))))
364
+ x = residual + ff_out
365
+
366
+ return x
367
+
368
+ def init_kv_cache(self, batch_size, dtype=torch.bfloat16):
369
+ """Initialize KV cache for this transformer block"""
370
+ return self.attn.empty_kv_cache(
371
+ batch_size=batch_size, dtype=dtype
372
+ )
373
+
374
+ def reset_kv_cache(self):
375
+ """Reset KV cache for this transformer block"""
376
+ self.attn.reset_cache()
mulres_infer/modules/fast_octree_level.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """数组化 + Numba 的「单层」八叉树编码,等价替换
2
+ MatrixOctree(max_depth=target_depth+1).build + OctreeFaceTokenBuilder(mc=8).encode_level_tokens(target_depth)
3
+
4
+ 前提(per-row 栈已满足):max_depth = target_depth+1 -> 只有一次「叶子(td+1)->节点(td)」聚合,
5
+ 不需要多层 neighbor 传播;且 max_children=8 时 reduce_node_children_to_max 是 no-op(活跃 octant ≤ 8)。
6
+
7
+ 去掉 OctreeNode 对象 / dict-of-tuple / set-of-tuple:
8
+ * 叶子坐标量化 -> 边表(numpy) -> 节点坐标(leaf>>1)+ octant 局部索引(位运算, 向量化)
9
+ * 按节点聚合 intra 8×8 / 活跃掩码 F / 节点-节点邻接(向量化 scatter / np.unique)
10
+ * 三角形(3-clique)枚举走 Numba njit(CSR 两指针求交), 这是原 get_level_info 的热点
11
+ * inter 8×8 按 (节点,邻居) 分组填充
12
+ 输出 (raw_t, out_t) 与原实现「逐 slot 解码后网格一致」(顺序可不同;decode 与顺序无关)。
13
+ """
14
+ import numpy as np
15
+ from numba import njit
16
+
17
+ from utils.data_utils import discretize
18
+
19
+ _OCT_W = np.array([1, 2, 4], dtype=np.int64) # octant 局部索引 = x + 2y + 4z
20
+
21
+
22
+ @njit(cache=False)
23
+ def _triangles_csr(indptr, indices):
24
+ """前向邻接 CSR(indices[indptr[u]:indptr[u+1]] = 与 u 相连且 >u 的节点, 升序)。
25
+ 枚举 u<v<w 三角形, 返回 [[u,v,w],...]。两指针求 adj[u]∩adj[v]。"""
26
+ out_u = []
27
+ out_v = []
28
+ out_w = []
29
+ n = indptr.shape[0] - 1
30
+ for u in range(n):
31
+ su, eu = indptr[u], indptr[u + 1]
32
+ for pa in range(su, eu):
33
+ v = indices[pa]
34
+ sv, ev = indptr[v], indptr[v + 1]
35
+ # 交集: adj[u] 中 >v 的部分(即 pa+1..eu, 因为升序且后面都 >v) 与 adj[v](sv..ev)
36
+ i = pa + 1
37
+ j = sv
38
+ while i < eu and j < ev:
39
+ wi = indices[i]
40
+ wj = indices[j]
41
+ if wi == wj:
42
+ out_u.append(u); out_v.append(v); out_w.append(wi)
43
+ i += 1; j += 1
44
+ elif wi < wj:
45
+ i += 1
46
+ else:
47
+ j += 1
48
+ return (np.array(out_u, dtype=np.int64),
49
+ np.array(out_v, dtype=np.int64),
50
+ np.array(out_w, dtype=np.int64))
51
+
52
+
mulres_infer/modules/miche/LICENSE ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ When we speak of free software, we are referring to freedom, not
23
+ price. Our General Public Licenses are designed to make sure that you
24
+ have the freedom to distribute copies of free software (and charge for
25
+ them if you wish), that you receive source code or can get it if you
26
+ want it, that you can change the software or use pieces of it in new
27
+ free programs, and that you know you can do these things.
28
+
29
+ To protect your rights, we need to prevent others from denying you
30
+ these rights or asking you to surrender the rights. Therefore, you have
31
+ certain responsibilities if you distribute copies of the software, or if
32
+ you modify it: responsibilities to respect the freedom of others.
33
+
34
+ For example, if you distribute copies of such a program, whether
35
+ gratis or for a fee, you must pass on to the recipients the same
36
+ freedoms that you received. You must make sure that they, too, receive
37
+ or can get the source code. And you must show them these terms so they
38
+ know their rights.
39
+
40
+ Developers that use the GNU GPL protect your rights with two steps:
41
+ (1) assert copyright on the software, and (2) offer you this License
42
+ giving you legal permission to copy, distribute and/or modify it.
43
+
44
+ For the developers' and authors' protection, the GPL clearly explains
45
+ that there is no warranty for this free software. For both users' and
46
+ authors' sake, the GPL requires that modified versions be marked as
47
+ changed, so that their problems will not be attributed erroneously to
48
+ authors of previous versions.
49
+
50
+ Some devices are designed to deny users access to install or run
51
+ modified versions of the software inside them, although the manufacturer
52
+ can do so. This is fundamentally incompatible with the aim of
53
+ protecting users' freedom to change the software. The systematic
54
+ pattern of such abuse occurs in the area of products for individuals to
55
+ use, which is precisely where it is most unacceptable. Therefore, we
56
+ have designed this version of the GPL to prohibit the practice for those
57
+ products. If such problems arise substantially in other domains, we
58
+ stand ready to extend this provision to those domains in future versions
59
+ of the GPL, as needed to protect the freedom of users.
60
+
61
+ Finally, every program is threatened constantly by software patents.
62
+ States should not allow patents to restrict development and use of
63
+ software on general-purpose computers, but in those that do, we wish to
64
+ avoid the special danger that patents applied to a free program could
65
+ make it effectively proprietary. To prevent this, the GPL assures that
66
+ patents cannot be used to render the program non-free.
67
+
68
+ The precise terms and conditions for copying, distribution and
69
+ modification follow.
70
+
71
+ TERMS AND CONDITIONS
72
+
73
+ 0. Definitions.
74
+
75
+ "This License" refers to version 3 of the GNU General Public License.
76
+
77
+ "Copyright" also means copyright-like laws that apply to other kinds of
78
+ works, such as semiconductor masks.
79
+
80
+ "The Program" refers to any copyrightable work licensed under this
81
+ License. Each licensee is addressed as "you". "Licensees" and
82
+ "recipients" may be individuals or organizations.
83
+
84
+ To "modify" a work means to copy from or adapt all or part of the work
85
+ in a fashion requiring copyright permission, other than the making of an
86
+ exact copy. The resulting work is called a "modified version" of the
87
+ earlier work or a work "based on" the earlier work.
88
+
89
+ A "covered work" means either the unmodified Program or a work based
90
+ on the Program.
91
+
92
+ To "propagate" a work means to do anything with it that, without
93
+ permission, would make you directly or secondarily liable for
94
+ infringement under applicable copyright law, except executing it on a
95
+ computer or modifying a private copy. Propagation includes copying,
96
+ distribution (with or without modification), making available to the
97
+ public, and in some countries other activities as well.
98
+
99
+ To "convey" a work means any kind of propagation that enables other
100
+ parties to make or receive copies. Mere interaction with a user through
101
+ a computer network, with no transfer of a copy, is not conveying.
102
+
103
+ An interactive user interface displays "Appropriate Legal Notices"
104
+ to the extent that it includes a convenient and prominently visible
105
+ feature that (1) displays an appropriate copyright notice, and (2)
106
+ tells the user that there is no warranty for the work (except to the
107
+ extent that warranties are provided), that licensees may convey the
108
+ work under this License, and how to view a copy of this License. If
109
+ the interface presents a list of user commands or options, such as a
110
+ menu, a prominent item in the list meets this criterion.
111
+
112
+ 1. Source Code.
113
+
114
+ The "source code" for a work means the preferred form of the work
115
+ for making modifications to it. "Object code" means any non-source
116
+ form of a work.
117
+
118
+ A "Standard Interface" means an interface that either is an official
119
+ standard defined by a recognized standards body, or, in the case of
120
+ interfaces specified for a particular programming language, one that
121
+ is widely used among developers working in that language.
122
+
123
+ The "System Libraries" of an executable work include anything, other
124
+ than the work as a whole, that (a) is included in the normal form of
125
+ packaging a Major Component, but which is not part of that Major
126
+ Component, and (b) serves only to enable use of the work with that
127
+ Major Component, or to implement a Standard Interface for which an
128
+ implementation is available to the public in source code form. A
129
+ "Major Component", in this context, means a major essential component
130
+ (kernel, window system, and so on) of the specific operating system
131
+ (if any) on which the executable work runs, or a compiler used to
132
+ produce the work, or an object code interpreter used to run it.
133
+
134
+ The "Corresponding Source" for a work in object code form means all
135
+ the source code needed to generate, install, and (for an executable
136
+ work) run the object code and to modify the work, including scripts to
137
+ control those activities. However, it does not include the work's
138
+ System Libraries, or general-purpose tools or generally available free
139
+ programs which are used unmodified in performing those activities but
140
+ which are not part of the work. For example, Corresponding Source
141
+ includes interface definition files associated with source files for
142
+ the work, and the source code for shared libraries and dynamically
143
+ linked subprograms that the work is specifically designed to require,
144
+ such as by intimate data communication or control flow between those
145
+ subprograms and other parts of the work.
146
+
147
+ The Corresponding Source need not include anything that users
148
+ can regenerate automatically from other parts of the Corresponding
149
+ Source.
150
+
151
+ The Corresponding Source for a work in source code form is that
152
+ same work.
153
+
154
+ 2. Basic Permissions.
155
+
156
+ All rights granted under this License are granted for the term of
157
+ copyright on the Program, and are irrevocable provided the stated
158
+ conditions are met. This License explicitly affirms your unlimited
159
+ permission to run the unmodified Program. The output from running a
160
+ covered work is covered by this License only if the output, given its
161
+ content, constitutes a covered work. This License acknowledges your
162
+ rights of fair use or other equivalent, as provided by copyright law.
163
+
164
+ You may make, run and propagate covered works that you do not
165
+ convey, without conditions so long as your license otherwise remains
166
+ in force. You may convey covered works to others for the sole purpose
167
+ of having them make modifications exclusively for you, or provide you
168
+ with facilities for running those works, provided that you comply with
169
+ the terms of this License in conveying all material for which you do
170
+ not control copyright. Those thus making or running the covered works
171
+ for you must do so exclusively on your behalf, under your direction
172
+ and control, on terms that prohibit them from making any copies of
173
+ your copyrighted material outside their relationship with you.
174
+
175
+ Conveying under any other circumstances is permitted solely under
176
+ the conditions stated below. Sublicensing is not allowed; section 10
177
+ makes it unnecessary.
178
+
179
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180
+
181
+ No covered work shall be deemed part of an effective technological
182
+ measure under any applicable law fulfilling obligations under article
183
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184
+ similar laws prohibiting or restricting circumvention of such
185
+ measures.
186
+
187
+ When you convey a covered work, you waive any legal power to forbid
188
+ circumvention of technological measures to the extent such circumvention
189
+ is effected by exercising rights under this License with respect to
190
+ the covered work, and you disclaim any intention to limit operation or
191
+ modification of the work as a means of enforcing, against the work's
192
+ users, your or third parties' legal rights to forbid circumvention of
193
+ technological measures.
194
+
195
+ 4. Conveying Verbatim Copies.
196
+
197
+ You may convey verbatim copies of the Program's source code as you
198
+ receive it, in any medium, provided that you conspicuously and
199
+ appropriately publish on each copy an appropriate copyright notice;
200
+ keep intact all notices stating that this License and any
201
+ non-permissive terms added in accord with section 7 apply to the code;
202
+ keep intact all notices of the absence of any warranty; and give all
203
+ recipients a copy of this License along with the Program.
204
+
205
+ You may charge any price or no price for each copy that you convey,
206
+ and you may offer support or warranty protection for a fee.
207
+
208
+ 5. Conveying Modified Source Versions.
209
+
210
+ You may convey a work based on the Program, or the modifications to
211
+ produce it from the Program, in the form of source code under the
212
+ terms of section 4, provided that you also meet all of these conditions:
213
+
214
+ a) The work must carry prominent notices stating that you modified
215
+ it, and giving a relevant date.
216
+
217
+ b) The work must carry prominent notices stating that it is
218
+ released under this License and any conditions added under section
219
+ 7. This requirement modifies the requirement in section 4 to
220
+ "keep intact all notices".
221
+
222
+ c) You must license the entire work, as a whole, under this
223
+ License to anyone who comes into possession of a copy. This
224
+ License will therefore apply, along with any applicable section 7
225
+ additional terms, to the whole of the work, and all its parts,
226
+ regardless of how they are packaged. This License gives no
227
+ permission to license the work in any other way, but it does not
228
+ invalidate such permission if you have separately received it.
229
+
230
+ d) If the work has interactive user interfaces, each must display
231
+ Appropriate Legal Notices; however, if the Program has interactive
232
+ interfaces that do not display Appropriate Legal Notices, your
233
+ work need not make them do so.
234
+
235
+ A compilation of a covered work with other separate and independent
236
+ works, which are not by their nature extensions of the covered work,
237
+ and which are not combined with it such as to form a larger program,
238
+ in or on a volume of a storage or distribution medium, is called an
239
+ "aggregate" if the compilation and its resulting copyright are not
240
+ used to limit the access or legal rights of the compilation's users
241
+ beyond what the individual works permit. Inclusion of a covered work
242
+ in an aggregate does not cause this License to apply to the other
243
+ parts of the aggregate.
244
+
245
+ 6. Conveying Non-Source Forms.
246
+
247
+ You may convey a covered work in object code form under the terms
248
+ of sections 4 and 5, provided that you also convey the
249
+ machine-readable Corresponding Source under the terms of this License,
250
+ in one of these ways:
251
+
252
+ a) Convey the object code in, or embodied in, a physical product
253
+ (including a physical distribution medium), accompanied by the
254
+ Corresponding Source fixed on a durable physical medium
255
+ customarily used for software interchange.
256
+
257
+ b) Convey the object code in, or embodied in, a physical product
258
+ (including a physical distribution medium), accompanied by a
259
+ written offer, valid for at least three years and valid for as
260
+ long as you offer spare parts or customer support for that product
261
+ model, to give anyone who possesses the object code either (1) a
262
+ copy of the Corresponding Source for all the software in the
263
+ product that is covered by this License, on a durable physical
264
+ medium customarily used for software interchange, for a price no
265
+ more than your reasonable cost of physically performing this
266
+ conveying of source, or (2) access to copy the
267
+ Corresponding Source from a network server at no charge.
268
+
269
+ c) Convey individual copies of the object code with a copy of the
270
+ written offer to provide the Corresponding Source. This
271
+ alternative is allowed only occasionally and noncommercially, and
272
+ only if you received the object code with such an offer, in accord
273
+ with subsection 6b.
274
+
275
+ d) Convey the object code by offering access from a designated
276
+ place (gratis or for a charge), and offer equivalent access to the
277
+ Corresponding Source in the same way through the same place at no
278
+ further charge. You need not require recipients to copy the
279
+ Corresponding Source along with the object code. If the place to
280
+ copy the object code is a network server, the Corresponding Source
281
+ may be on a different server (operated by you or a third party)
282
+ that supports equivalent copying facilities, provided you maintain
283
+ clear directions next to the object code saying where to find the
284
+ Corresponding Source. Regardless of what server hosts the
285
+ Corresponding Source, you remain obligated to ensure that it is
286
+ available for as long as needed to satisfy these requirements.
287
+
288
+ e) Convey the object code using peer-to-peer transmission, provided
289
+ you inform other peers where the object code and Corresponding
290
+ Source of the work are being offered to the general public at no
291
+ charge under subsection 6d.
292
+
293
+ A separable portion of the object code, whose source code is excluded
294
+ from the Corresponding Source as a System Library, need not be
295
+ included in conveying the object code work.
296
+
297
+ A "User Product" is either (1) a "consumer product", which means any
298
+ tangible personal property which is normally used for personal, family,
299
+ or household purposes, or (2) anything designed or sold for incorporation
300
+ into a dwelling. In determining whether a product is a consumer product,
301
+ doubtful cases shall be resolved in favor of coverage. For a particular
302
+ product received by a particular user, "normally used" refers to a
303
+ typical or common use of that class of product, regardless of the status
304
+ of the particular user or of the way in which the particular user
305
+ actually uses, or expects or is expected to use, the product. A product
306
+ is a consumer product regardless of whether the product has substantial
307
+ commercial, industrial or non-consumer uses, unless such uses represent
308
+ the only significant mode of use of the product.
309
+
310
+ "Installation Information" for a User Product means any methods,
311
+ procedures, authorization keys, or other information required to install
312
+ and execute modified versions of a covered work in that User Product from
313
+ a modified version of its Corresponding Source. The information must
314
+ suffice to ensure that the continued functioning of the modified object
315
+ code is in no case prevented or interfered with solely because
316
+ modification has been made.
317
+
318
+ If you convey an object code work under this section in, or with, or
319
+ specifically for use in, a User Product, and the conveying occurs as
320
+ part of a transaction in which the right of possession and use of the
321
+ User Product is transferred to the recipient in perpetuity or for a
322
+ fixed term (regardless of how the transaction is characterized), the
323
+ Corresponding Source conveyed under this section must be accompanied
324
+ by the Installation Information. But this requirement does not apply
325
+ if neither you nor any third party retains the ability to install
326
+ modified object code on the User Product (for example, the work has
327
+ been installed in ROM).
328
+
329
+ The requirement to provide Installation Information does not include a
330
+ requirement to continue to provide support service, warranty, or updates
331
+ for a work that has been modified or installed by the recipient, or for
332
+ the User Product in which it has been modified or installed. Access to a
333
+ network may be denied when the modification itself materially and
334
+ adversely affects the operation of the network or violates the rules and
335
+ protocols for communication across the network.
336
+
337
+ Corresponding Source conveyed, and Installation Information provided,
338
+ in accord with this section must be in a format that is publicly
339
+ documented (and with an implementation available to the public in
340
+ source code form), and must require no special password or key for
341
+ unpacking, reading or copying.
342
+
343
+ 7. Additional Terms.
344
+
345
+ "Additional permissions" are terms that supplement the terms of this
346
+ License by making exceptions from one or more of its conditions.
347
+ Additional permissions that are applicable to the entire Program shall
348
+ be treated as though they were included in this License, to the extent
349
+ that they are valid under applicable law. If additional permissions
350
+ apply only to part of the Program, that part may be used separately
351
+ under those permissions, but the entire Program remains governed by
352
+ this License without regard to the additional permissions.
353
+
354
+ When you convey a copy of a covered work, you may at your option
355
+ remove any additional permissions from that copy, or from any part of
356
+ it. (Additional permissions may be written to require their own
357
+ removal in certain cases when you modify the work.) You may place
358
+ additional permissions on material, added by you to a covered work,
359
+ for which you have or can give appropriate copyright permission.
360
+
361
+ Notwithstanding any other provision of this License, for material you
362
+ add to a covered work, you may (if authorized by the copyright holders of
363
+ that material) supplement the terms of this License with terms:
364
+
365
+ a) Disclaiming warranty or limiting liability differently from the
366
+ terms of sections 15 and 16 of this License; or
367
+
368
+ b) Requiring preservation of specified reasonable legal notices or
369
+ author attributions in that material or in the Appropriate Legal
370
+ Notices displayed by works containing it; or
371
+
372
+ c) Prohibiting misrepresentation of the origin of that material, or
373
+ requiring that modified versions of such material be marked in
374
+ reasonable ways as different from the original version; or
375
+
376
+ d) Limiting the use for publicity purposes of names of licensors or
377
+ authors of the material; or
378
+
379
+ e) Declining to grant rights under trademark law for use of some
380
+ trade names, trademarks, or service marks; or
381
+
382
+ f) Requiring indemnification of licensors and authors of that
383
+ material by anyone who conveys the material (or modified versions of
384
+ it) with contractual assumptions of liability to the recipient, for
385
+ any liability that these contractual assumptions directly impose on
386
+ those licensors and authors.
387
+
388
+ All other non-permissive additional terms are considered "further
389
+ restrictions" within the meaning of section 10. If the Program as you
390
+ received it, or any part of it, contains a notice stating that it is
391
+ governed by this License along with a term that is a further
392
+ restriction, you may remove that term. If a license document contains
393
+ a further restriction but permits relicensing or conveying under this
394
+ License, you may add to a covered work material governed by the terms
395
+ of that license document, provided that the further restriction does
396
+ not survive such relicensing or conveying.
397
+
398
+ If you add terms to a covered work in accord with this section, you
399
+ must place, in the relevant source files, a statement of the
400
+ additional terms that apply to those files, or a notice indicating
401
+ where to find the applicable terms.
402
+
403
+ Additional terms, permissive or non-permissive, may be stated in the
404
+ form of a separately written license, or stated as exceptions;
405
+ the above requirements apply either way.
406
+
407
+ 8. Termination.
408
+
409
+ You may not propagate or modify a covered work except as expressly
410
+ provided under this License. Any attempt otherwise to propagate or
411
+ modify it is void, and will automatically terminate your rights under
412
+ this License (including any patent licenses granted under the third
413
+ paragraph of section 11).
414
+
415
+ However, if you cease all violation of this License, then your
416
+ license from a particular copyright holder is reinstated (a)
417
+ provisionally, unless and until the copyright holder explicitly and
418
+ finally terminates your license, and (b) permanently, if the copyright
419
+ holder fails to notify you of the violation by some reasonable means
420
+ prior to 60 days after the cessation.
421
+
422
+ Moreover, your license from a particular copyright holder is
423
+ reinstated permanently if the copyright holder notifies you of the
424
+ violation by some reasonable means, this is the first time you have
425
+ received notice of violation of this License (for any work) from that
426
+ copyright holder, and you cure the violation prior to 30 days after
427
+ your receipt of the notice.
428
+
429
+ Termination of your rights under this section does not terminate the
430
+ licenses of parties who have received copies or rights from you under
431
+ this License. If your rights have been terminated and not permanently
432
+ reinstated, you do not qualify to receive new licenses for the same
433
+ material under section 10.
434
+
435
+ 9. Acceptance Not Required for Having Copies.
436
+
437
+ You are not required to accept this License in order to receive or
438
+ run a copy of the Program. Ancillary propagation of a covered work
439
+ occurring solely as a consequence of using peer-to-peer transmission
440
+ to receive a copy likewise does not require acceptance. However,
441
+ nothing other than this License grants you permission to propagate or
442
+ modify any covered work. These actions infringe copyright if you do
443
+ not accept this License. Therefore, by modifying or propagating a
444
+ covered work, you indicate your acceptance of this License to do so.
445
+
446
+ 10. Automatic Licensing of Downstream Recipients.
447
+
448
+ Each time you convey a covered work, the recipient automatically
449
+ receives a license from the original licensors, to run, modify and
450
+ propagate that work, subject to this License. You are not responsible
451
+ for enforcing compliance by third parties with this License.
452
+
453
+ An "entity transaction" is a transaction transferring control of an
454
+ organization, or substantially all assets of one, or subdividing an
455
+ organization, or merging organizations. If propagation of a covered
456
+ work results from an entity transaction, each party to that
457
+ transaction who receives a copy of the work also receives whatever
458
+ licenses to the work the party's predecessor in interest had or could
459
+ give under the previous paragraph, plus a right to possession of the
460
+ Corresponding Source of the work from the predecessor in interest, if
461
+ the predecessor has it or can get it with reasonable efforts.
462
+
463
+ You may not impose any further restrictions on the exercise of the
464
+ rights granted or affirmed under this License. For example, you may
465
+ not impose a license fee, royalty, or other charge for exercise of
466
+ rights granted under this License, and you may not initiate litigation
467
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
468
+ any patent claim is infringed by making, using, selling, offering for
469
+ sale, or importing the Program or any portion of it.
470
+
471
+ 11. Patents.
472
+
473
+ A "contributor" is a copyright holder who authorizes use under this
474
+ License of the Program or a work on which the Program is based. The
475
+ work thus licensed is called the contributor's "contributor version".
476
+
477
+ A contributor's "essential patent claims" are all patent claims
478
+ owned or controlled by the contributor, whether already acquired or
479
+ hereafter acquired, that would be infringed by some manner, permitted
480
+ by this License, of making, using, or selling its contributor version,
481
+ but do not include claims that would be infringed only as a
482
+ consequence of further modification of the contributor version. For
483
+ purposes of this definition, "control" includes the right to grant
484
+ patent sublicenses in a manner consistent with the requirements of
485
+ this License.
486
+
487
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
488
+ patent license under the contributor's essential patent claims, to
489
+ make, use, sell, offer for sale, import and otherwise run, modify and
490
+ propagate the contents of its contributor version.
491
+
492
+ In the following three paragraphs, a "patent license" is any express
493
+ agreement or commitment, however denominated, not to enforce a patent
494
+ (such as an express permission to practice a patent or covenant not to
495
+ sue for patent infringement). To "grant" such a patent license to a
496
+ party means to make such an agreement or commitment not to enforce a
497
+ patent against the party.
498
+
499
+ If you convey a covered work, knowingly relying on a patent license,
500
+ and the Corresponding Source of the work is not available for anyone
501
+ to copy, free of charge and under the terms of this License, through a
502
+ publicly available network server or other readily accessible means,
503
+ then you must either (1) cause the Corresponding Source to be so
504
+ available, or (2) arrange to deprive yourself of the benefit of the
505
+ patent license for this particular work, or (3) arrange, in a manner
506
+ consistent with the requirements of this License, to extend the patent
507
+ license to downstream recipients. "Knowingly relying" means you have
508
+ actual knowledge that, but for the patent license, your conveying the
509
+ covered work in a country, or your recipient's use of the covered work
510
+ in a country, would infringe one or more identifiable patents in that
511
+ country that you have reason to believe are valid.
512
+
513
+ If, pursuant to or in connection with a single transaction or
514
+ arrangement, you convey, or propagate by procuring conveyance of, a
515
+ covered work, and grant a patent license to some of the parties
516
+ receiving the covered work authorizing them to use, propagate, modify
517
+ or convey a specific copy of the covered work, then the patent license
518
+ you grant is automatically extended to all recipients of the covered
519
+ work and works based on it.
520
+
521
+ A patent license is "discriminatory" if it does not include within
522
+ the scope of its coverage, prohibits the exercise of, or is
523
+ conditioned on the non-exercise of one or more of the rights that are
524
+ specifically granted under this License. You may not convey a covered
525
+ work if you are a party to an arrangement with a third party that is
526
+ in the business of distributing software, under which you make payment
527
+ to the third party based on the extent of your activity of conveying
528
+ the work, and under which the third party grants, to any of the
529
+ parties who would receive the covered work from you, a discriminatory
530
+ patent license (a) in connection with copies of the covered work
531
+ conveyed by you (or copies made from those copies), or (b) primarily
532
+ for and in connection with specific products or compilations that
533
+ contain the covered work, unless you entered into that arrangement,
534
+ or that patent license was granted, prior to 28 March 2007.
535
+
536
+ Nothing in this License shall be construed as excluding or limiting
537
+ any implied license or other defenses to infringement that may
538
+ otherwise be available to you under applicable patent law.
539
+
540
+ 12. No Surrender of Others' Freedom.
541
+
542
+ If conditions are imposed on you (whether by court order, agreement or
543
+ otherwise) that contradict the conditions of this License, they do not
544
+ excuse you from the conditions of this License. If you cannot convey a
545
+ covered work so as to satisfy simultaneously your obligations under this
546
+ License and any other pertinent obligations, then as a consequence you may
547
+ not convey it at all. For example, if you agree to terms that obligate you
548
+ to collect a royalty for further conveying from those to whom you convey
549
+ the Program, the only way you could satisfy both those terms and this
550
+ License would be to refrain entirely from conveying the Program.
551
+
552
+ 13. Use with the GNU Affero General Public License.
553
+
554
+ Notwithstanding any other provision of this License, you have
555
+ permission to link or combine any covered work with a work licensed
556
+ under version 3 of the GNU Affero General Public License into a single
557
+ combined work, and to convey the resulting work. The terms of this
558
+ License will continue to apply to the part which is the covered work,
559
+ but the special requirements of the GNU Affero General Public License,
560
+ section 13, concerning interaction through a network will apply to the
561
+ combination as such.
562
+
563
+ 14. Revised Versions of this License.
564
+
565
+ The Free Software Foundation may publish revised and/or new versions of
566
+ the GNU General Public License from time to time. Such new versions will
567
+ be similar in spirit to the present version, but may differ in detail to
568
+ address new problems or concerns.
569
+
570
+ Each version is given a distinguishing version number. If the
571
+ Program specifies that a certain numbered version of the GNU General
572
+ Public License "or any later version" applies to it, you have the
573
+ option of following the terms and conditions either of that numbered
574
+ version or of any later version published by the Free Software
575
+ Foundation. If the Program does not specify a version number of the
576
+ GNU General Public License, you may choose any version ever published
577
+ by the Free Software Foundation.
578
+
579
+ If the Program specifies that a proxy can decide which future
580
+ versions of the GNU General Public License can be used, that proxy's
581
+ public statement of acceptance of a version permanently authorizes you
582
+ to choose that version for the Program.
583
+
584
+ Later license versions may give you additional or different
585
+ permissions. However, no additional obligations are imposed on any
586
+ author or copyright holder as a result of your choosing to follow a
587
+ later version.
588
+
589
+ 15. Disclaimer of Warranty.
590
+
591
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
+
600
+ 16. Limitation of Liability.
601
+
602
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610
+ SUCH DAMAGES.
611
+
612
+ 17. Interpretation of Sections 15 and 16.
613
+
614
+ If the disclaimer of warranty and limitation of liability provided
615
+ above cannot be given local legal effect according to their terms,
616
+ reviewing courts shall apply local law that most closely approximates
617
+ an absolute waiver of all civil liability in connection with the
618
+ Program, unless a warranty or assumption of liability accompanies a
619
+ copy of the Program in return for a fee.
620
+
621
+ END OF TERMS AND CONDITIONS
622
+
623
+ How to Apply These Terms to Your New Programs
624
+
625
+ If you develop a new program, and you want it to be of the greatest
626
+ possible use to the public, the best way to achieve this is to make it
627
+ free software which everyone can redistribute and change under these terms.
628
+
629
+ To do so, attach the following notices to the program. It is safest
630
+ to attach them to the start of each source file to most effectively
631
+ state the exclusion of warranty; and each file should have at least
632
+ the "copyright" line and a pointer to where the full notice is found.
633
+
634
+ <one line to give the program's name and a brief idea of what it does.>
635
+ Copyright (C) <year> <name of author>
636
+
637
+ This program is free software: you can redistribute it and/or modify
638
+ it under the terms of the GNU General Public License as published by
639
+ the Free Software Foundation, either version 3 of the License, or
640
+ (at your option) any later version.
641
+
642
+ This program is distributed in the hope that it will be useful,
643
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
644
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645
+ GNU General Public License for more details.
646
+
647
+ You should have received a copy of the GNU General Public License
648
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
649
+
650
+ Also add information on how to contact you by electronic and paper mail.
651
+
652
+ If the program does terminal interaction, make it output a short
653
+ notice like this when it starts in an interactive mode:
654
+
655
+ <program> Copyright (C) <year> <name of author>
656
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657
+ This is free software, and you are welcome to redistribute it
658
+ under certain conditions; type `show c' for details.
659
+
660
+ The hypothetical commands `show w' and `show c' should show the appropriate
661
+ parts of the General Public License. Of course, your program's commands
662
+ might be different; for a GUI interface, you would use an "about box".
663
+
664
+ You should also get your employer (if you work as a programmer) or school,
665
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
666
+ For more information on this, and how to apply and follow the GNU GPL, see
667
+ <https://www.gnu.org/licenses/>.
668
+
669
+ The GNU General Public License does not permit incorporating your program
670
+ into proprietary programs. If your program is a subroutine library, you
671
+ may consider it more useful to permit linking proprietary applications with
672
+ the library. If this is what you want to do, use the GNU Lesser General
673
+ Public License instead of this License. But first, please read
674
+ <https://www.gnu.org/licenses/why-not-lgpl.html>.
mulres_infer/modules/miche/__init__.py ADDED
File without changes
mulres_infer/modules/miche/download_ckpt.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import hf_hub_download
2
+ import os
3
+
4
+ # 设置镜像(国内加速)
5
+ # os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
6
+
7
+ def download_michelangelo_checkpoint():
8
+ """下载Michelangelo模型的checkpoint文件"""
9
+ try:
10
+ # 下载文件
11
+ local_path = hf_hub_download(
12
+ repo_id="Maikou/Michelangelo",
13
+ filename="checkpoints/aligned_shape_latents/shapevae-256.ckpt",
14
+ local_dir="./downloads", # 本地保存目录
15
+ local_dir_use_symlinks=False, # 不使用符号链接
16
+ resume_download=True, # 支持断点续传
17
+ )
18
+ print(f"文件已下载到: {local_path}")
19
+ return local_path
20
+ except Exception as e:
21
+ print(f"下载失败: {e}")
22
+ return None
23
+
24
+ if __name__ == "__main__":
25
+ download_michelangelo_checkpoint()
mulres_infer/modules/miche/encode.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ # -*- coding: utf-8 -*-
3
+ import argparse
4
+ from omegaconf import OmegaConf
5
+ import numpy as np
6
+ import torch
7
+ from .michelangelo.utils.misc import instantiate_from_config
8
+
9
+ def load_surface(fp):
10
+
11
+ with np.load(fp) as input_pc:
12
+ surface = input_pc['points']
13
+ normal = input_pc['normals']
14
+
15
+ rng = np.random.default_rng()
16
+ ind = rng.choice(surface.shape[0], 4096, replace=False)
17
+ surface = torch.FloatTensor(surface[ind])
18
+ normal = torch.FloatTensor(normal[ind])
19
+
20
+ surface = torch.cat([surface, normal], dim=-1).unsqueeze(0).cuda()
21
+
22
+ return surface
23
+
24
+ def reconstruction(args, model, bounds=(-1.25, -1.25, -1.25, 1.25, 1.25, 1.25), octree_depth=7, num_chunks=10000):
25
+
26
+ surface = load_surface(args.pointcloud_path)
27
+ # old_surface = surface.clone()
28
+
29
+ # surface[0,:,0]*=-1
30
+ # surface[0,:,1]*=-1
31
+ surface[0,:,2]*=-1
32
+
33
+ # encoding
34
+ shape_embed, shape_latents = model.model.encode_shape_embed(surface, return_latents=True)
35
+ shape_zq, posterior = model.model.shape_model.encode_kl_embed(shape_latents)
36
+
37
+ # decoding
38
+ latents = model.model.shape_model.decode(shape_zq)
39
+ # geometric_func = partial(model.model.shape_model.query_geometry, latents=latents)
40
+
41
+ return 0
42
+
43
+ def load_model(ckpt_path=None, config_path=None):
44
+ _HERE = os.path.dirname(os.path.abspath(__file__))
45
+ config_path = config_path or os.path.join(_HERE, "shapevae-256.yaml")
46
+
47
+ model_config = OmegaConf.load(config_path)
48
+ # print(model_config)
49
+ if hasattr(model_config, "model"):
50
+ model_config = model_config.model
51
+
52
+ original_load = torch.load
53
+ torch.load = lambda path, *args, **kwargs: original_load(path, map_location="cpu", weights_only=False)
54
+ try:
55
+ model = instantiate_from_config(model_config, ckpt_path=ckpt_path)
56
+ model = model.eval()
57
+ finally:
58
+ # 恢复原始的 torch.load
59
+ torch.load = original_load
60
+
61
+ return model
62
+ if __name__ == "__main__":
63
+ '''
64
+ 1. Reconstruct point cloud
65
+ 2. Image-conditioned generation
66
+ 3. Text-conditioned generation
67
+ '''
68
+ parser = argparse.ArgumentParser()
69
+ parser.add_argument("--config_path", type=str, required=True)
70
+ parser.add_argument("--ckpt_path", type=str, required=True)
71
+ parser.add_argument("--pointcloud_path", type=str, default='./example_data/surface.npz',
72
+ help='Path to the input point cloud')
73
+ parser.add_argument("--image_path", type=str, help='Path to the input image')
74
+ parser.add_argument("--text", type=str,
75
+ help='Input text within a format: A 3D model of motorcar; Porsche 911.')
76
+ parser.add_argument("--output_dir", type=str, default='./output')
77
+ parser.add_argument("-s", "--seed", type=int, default=0)
78
+ args = parser.parse_args()
79
+
80
+ print(f'-----------------------------------------------------------------------------')
81
+ print(f'>>> Output directory: {args.output_dir}')
82
+ print(f'-----------------------------------------------------------------------------')
83
+
84
+ reconstruction(args, load_model(args))
mulres_infer/modules/miche/michelangelo/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # -*- coding: utf-8 -*-
mulres_infer/modules/miche/michelangelo/graphics/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # -*- coding: utf-8 -*-
mulres_infer/modules/miche/michelangelo/graphics/primitives/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .volume import generate_dense_grid_points
4
+
mulres_infer/modules/miche/michelangelo/graphics/primitives/volume.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import numpy as np
4
+
5
+ # produce dense points
6
+ def generate_dense_grid_points(bbox_min: np.ndarray,
7
+ bbox_max: np.ndarray,
8
+ octree_depth: int,
9
+ indexing: str = "ij"):
10
+ length = bbox_max - bbox_min
11
+ num_cells = np.exp2(octree_depth)
12
+ x = np.linspace(bbox_min[0], bbox_max[0], int(num_cells) + 1, dtype=np.float32)
13
+ y = np.linspace(bbox_min[1], bbox_max[1], int(num_cells) + 1, dtype=np.float32)
14
+ z = np.linspace(bbox_min[2], bbox_max[2], int(num_cells) + 1, dtype=np.float32)
15
+ [xs, ys, zs] = np.meshgrid(x, y, z, indexing=indexing)
16
+ xyz = np.stack((xs, ys, zs), axis=-1)
17
+ xyz = xyz.reshape(-1, 3)
18
+ grid_size = [int(num_cells) + 1, int(num_cells) + 1, int(num_cells) + 1]
19
+
20
+ return xyz, grid_size, length
21
+
mulres_infer/modules/miche/michelangelo/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # -*- coding: utf-8 -*-
mulres_infer/modules/miche/michelangelo/models/modules/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .checkpoint import checkpoint
mulres_infer/modules/miche/michelangelo/models/modules/checkpoint.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import torch
4
+ from typing import Callable, Iterable, Sequence, Union
5
+
6
+
7
+ def checkpoint(
8
+ func: Callable[..., Union[torch.Tensor, Sequence[torch.Tensor]]],
9
+ inputs: Sequence[torch.Tensor],
10
+ params: Iterable[torch.Tensor],
11
+ flag: bool,
12
+ use_deepspeed: bool = False
13
+ ):
14
+ # Evaluate a function without caching intermediate activations, allowing for
15
+ # reduced memory at the expense of extra compute in the backward pass.
16
+ # :param func: the function to evaluate.
17
+ # :param inputs: the argument sequence to pass to `func`.
18
+ # :param params: a sequence of parameters `func` depends on but does not
19
+ # explicitly take as arguments.
20
+ # :param flag: if False, disable gradient checkpointing.
21
+ # :param use_deepspeed: if True, use deepspeed
22
+ if flag:
23
+ if use_deepspeed:
24
+ import deepspeed
25
+ return deepspeed.checkpointing.checkpoint(func, *inputs)
26
+
27
+ args = tuple(inputs) + tuple(params)
28
+ return CheckpointFunction.apply(func, len(inputs), *args)
29
+ else:
30
+ return func(*inputs)
31
+
32
+
33
+ class CheckpointFunction(torch.autograd.Function):
34
+ @staticmethod
35
+ @torch.cuda.amp.custom_fwd
36
+ def forward(ctx, run_function, length, *args):
37
+ ctx.run_function = run_function
38
+ ctx.input_tensors = list(args[:length])
39
+ ctx.input_params = list(args[length:])
40
+
41
+ with torch.no_grad():
42
+ output_tensors = ctx.run_function(*ctx.input_tensors)
43
+ return output_tensors
44
+
45
+ @staticmethod
46
+ @torch.cuda.amp.custom_bwd
47
+ def backward(ctx, *output_grads):
48
+ ctx.input_tensors = [x.detach().requires_grad_(True) for x in ctx.input_tensors]
49
+ with torch.enable_grad():
50
+ # Fixes a bug where the first op in run_function modifies the
51
+ # Tensor storage in place, which is not allowed for detach()'d
52
+ # Tensors.
53
+ shallow_copies = [x.view_as(x) for x in ctx.input_tensors]
54
+ output_tensors = ctx.run_function(*shallow_copies)
55
+ input_grads = torch.autograd.grad(
56
+ output_tensors,
57
+ ctx.input_tensors + ctx.input_params,
58
+ output_grads,
59
+ allow_unused=True,
60
+ )
61
+ del ctx.input_tensors
62
+ del ctx.input_params
63
+ del output_tensors
64
+ return (None, None) + input_grads
mulres_infer/modules/miche/michelangelo/models/modules/distributions.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import torch
4
+ import numpy as np
5
+ from typing import Union, List
6
+
7
+
8
+ class DiagonalGaussianDistribution(object):
9
+ # Gaussian distribution
10
+ def __init__(self, parameters: Union[torch.Tensor, List[torch.Tensor]], deterministic=False, feat_dim=1):
11
+ self.feat_dim = feat_dim
12
+ self.parameters = parameters
13
+
14
+ if isinstance(parameters, list):
15
+ self.mean = parameters[0]
16
+ self.logvar = parameters[1]
17
+ else:
18
+ self.mean, self.logvar = torch.chunk(parameters, 2, dim=feat_dim)
19
+
20
+ self.logvar = torch.clamp(self.logvar, -30.0, 20.0)
21
+ self.deterministic = deterministic
22
+ self.std = torch.exp(0.5 * self.logvar)
23
+ self.var = torch.exp(self.logvar)
24
+ if self.deterministic:
25
+ self.var = self.std = torch.zeros_like(self.mean)
26
+
27
+ # sample from the guassian distribution
28
+ def sample(self):
29
+ x = self.mean + self.std * torch.randn_like(self.mean)
30
+ return x
31
+
32
+ def kl(self, other=None, dims=(1, 2, 3)):
33
+ if self.deterministic:
34
+ return torch.Tensor([0.])
35
+ else:
36
+ if other is None:
37
+ return 0.5 * torch.mean(torch.pow(self.mean, 2)
38
+ + self.var - 1.0 - self.logvar,
39
+ dim=dims)
40
+ else:
41
+ return 0.5 * torch.mean(
42
+ torch.pow(self.mean - other.mean, 2) / other.var
43
+ + self.var / other.var - 1.0 - self.logvar + other.logvar,
44
+ dim=dims)
45
+
46
+ def nll(self, sample, dims=(1, 2, 3)):
47
+ if self.deterministic:
48
+ return torch.Tensor([0.])
49
+ logtwopi = np.log(2.0 * np.pi)
50
+ return 0.5 * torch.sum(
51
+ logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var,
52
+ dim=dims)
53
+
54
+ def mode(self):
55
+ return self.mean
56
+
57
+
58
+ def normal_kl(mean1, logvar1, mean2, logvar2):
59
+ # Compute the KL divergence between two gaussians.
60
+ # Shapes are automatically broadcasted, so batches can be compared to
61
+ # scalars, among other use cases.
62
+
63
+ tensor = None
64
+ for obj in (mean1, logvar1, mean2, logvar2):
65
+ if isinstance(obj, torch.Tensor):
66
+ tensor = obj
67
+ break
68
+ assert tensor is not None, "at least one argument must be a Tensor"
69
+
70
+ # Force variances to be Tensors. Broadcasting helps convert scalars to
71
+ # Tensors, but it does not work for torch.exp().
72
+ logvar1, logvar2 = [
73
+ x if isinstance(x, torch.Tensor) else torch.tensor(x).to(tensor)
74
+ for x in (logvar1, logvar2)
75
+ ]
76
+
77
+ return 0.5 * (
78
+ -1.0
79
+ + logvar2
80
+ - logvar1
81
+ + torch.exp(logvar1 - logvar2)
82
+ + ((mean1 - mean2) ** 2) * torch.exp(-logvar2)
83
+ )
mulres_infer/modules/miche/michelangelo/models/modules/embedder.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn as nn
6
+ import math
7
+
8
+ VALID_EMBED_TYPES = ["identity", "fourier", "hashgrid", "sphere_harmonic", "triplane_fourier"]
9
+
10
+
11
+ class FourierEmbedder(nn.Module):
12
+ """The sin/cosine positional embedding. Given an input tensor `x` of shape [n_batch, ..., c_dim], it converts
13
+ each feature dimension of `x[..., i]` into:
14
+ [
15
+ sin(x[..., i]),
16
+ sin(f_1*x[..., i]),
17
+ sin(f_2*x[..., i]),
18
+ ...
19
+ sin(f_N * x[..., i]),
20
+ cos(x[..., i]),
21
+ cos(f_1*x[..., i]),
22
+ cos(f_2*x[..., i]),
23
+ ...
24
+ cos(f_N * x[..., i]),
25
+ x[..., i] # only present if include_input is True.
26
+ ], here f_i is the frequency.
27
+
28
+ Denote the space is [0 / num_freqs, 1 / num_freqs, 2 / num_freqs, 3 / num_freqs, ..., (num_freqs - 1) / num_freqs].
29
+ If logspace is True, then the frequency f_i is [2^(0 / num_freqs), ..., 2^(i / num_freqs), ...];
30
+ Otherwise, the frequencies are linearly spaced between [1.0, 2^(num_freqs - 1)].
31
+
32
+ Args:
33
+ num_freqs (int): the number of frequencies, default is 6;
34
+ logspace (bool): If logspace is True, then the frequency f_i is [..., 2^(i / num_freqs), ...],
35
+ otherwise, the frequencies are linearly spaced between [1.0, 2^(num_freqs - 1)];
36
+ input_dim (int): the input dimension, default is 3;
37
+ include_input (bool): include the input tensor or not, default is True.
38
+
39
+ Attributes:
40
+ frequencies (torch.Tensor): If logspace is True, then the frequency f_i is [..., 2^(i / num_freqs), ...],
41
+ otherwise, the frequencies are linearly spaced between [1.0, 2^(num_freqs - 1);
42
+
43
+ out_dim (int): the embedding size, if include_input is True, it is input_dim * (num_freqs * 2 + 1),
44
+ otherwise, it is input_dim * num_freqs * 2.
45
+
46
+ """
47
+
48
+ def __init__(self,
49
+ num_freqs: int = 6,
50
+ logspace: bool = True,
51
+ input_dim: int = 3,
52
+ include_input: bool = True,
53
+ include_pi: bool = True) -> None:
54
+
55
+ """The initialization"""
56
+
57
+ super().__init__()
58
+
59
+ if logspace:
60
+ frequencies = 2.0 ** torch.arange(
61
+ num_freqs,
62
+ dtype=torch.float32
63
+ )
64
+ else:
65
+ frequencies = torch.linspace(
66
+ 1.0,
67
+ 2.0 ** (num_freqs - 1),
68
+ num_freqs,
69
+ dtype=torch.float32
70
+ )
71
+
72
+ if include_pi:
73
+ frequencies *= torch.pi
74
+
75
+ self.register_buffer("frequencies", frequencies, persistent=False)
76
+ self.include_input = include_input
77
+ self.num_freqs = num_freqs
78
+
79
+ self.out_dim = self.get_dims(input_dim)
80
+
81
+ def get_dims(self, input_dim):
82
+ temp = 1 if self.include_input or self.num_freqs == 0 else 0
83
+ out_dim = input_dim * (self.num_freqs * 2 + temp)
84
+
85
+ return out_dim
86
+
87
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
88
+ """ Forward process.
89
+
90
+ Args:
91
+ x: tensor of shape [..., dim]
92
+
93
+ Returns:
94
+ embedding: an embedding of `x` of shape [..., dim * (num_freqs * 2 + temp)]
95
+ where temp is 1 if include_input is True and 0 otherwise.
96
+ """
97
+
98
+ if self.num_freqs > 0:
99
+ embed = (x[..., None].contiguous() * self.frequencies).view(*x.shape[:-1], -1)
100
+ if self.include_input:
101
+ return torch.cat((x, embed.sin(), embed.cos()), dim=-1)
102
+ else:
103
+ return torch.cat((embed.sin(), embed.cos()), dim=-1)
104
+ else:
105
+ return x
106
+
107
+
108
+ class LearnedFourierEmbedder(nn.Module):
109
+ """ following @crowsonkb "s lead with learned sinusoidal pos emb """
110
+ """ https://github.com/crowsonkb/v-diffusion-jax/blob/master/diffusion/models/danbooru_128.py#L8 """
111
+
112
+ def __init__(self, in_channels, dim):
113
+ super().__init__()
114
+ assert (dim % 2) == 0
115
+ half_dim = dim // 2
116
+ per_channel_dim = half_dim // in_channels
117
+ self.weights = nn.Parameter(torch.randn(per_channel_dim))
118
+
119
+ def forward(self, x):
120
+ """
121
+
122
+ Args:
123
+ x (torch.FloatTensor): [..., c]
124
+
125
+ Returns:
126
+ x (torch.FloatTensor): [..., d]
127
+ """
128
+
129
+ # [b, t, c, 1] * [1, d] = [b, t, c, d] -> [b, t, c * d]
130
+ freqs = (x[..., None] * self.weights[None] * 2 * np.pi).view(*x.shape[:-1], -1)
131
+ fouriered = torch.cat((x, freqs.sin(), freqs.cos()), dim=-1)
132
+ return fouriered
133
+
134
+
135
+ class TriplaneLearnedFourierEmbedder(nn.Module):
136
+ def __init__(self, in_channels, dim):
137
+ super().__init__()
138
+
139
+ self.yz_plane_embedder = LearnedFourierEmbedder(in_channels, dim)
140
+ self.xz_plane_embedder = LearnedFourierEmbedder(in_channels, dim)
141
+ self.xy_plane_embedder = LearnedFourierEmbedder(in_channels, dim)
142
+
143
+ self.out_dim = in_channels + dim
144
+
145
+ def forward(self, x):
146
+
147
+ yz_embed = self.yz_plane_embedder(x)
148
+ xz_embed = self.xz_plane_embedder(x)
149
+ xy_embed = self.xy_plane_embedder(x)
150
+
151
+ embed = yz_embed + xz_embed + xy_embed
152
+
153
+ return embed
154
+
155
+
156
+ def sequential_pos_embed(num_len, embed_dim):
157
+ assert embed_dim % 2 == 0
158
+
159
+ pos = torch.arange(num_len, dtype=torch.float32)
160
+ omega = torch.arange(embed_dim // 2, dtype=torch.float32)
161
+ omega /= embed_dim / 2.
162
+ omega = 1. / 10000 ** omega # (D/2,)
163
+
164
+ pos = pos.reshape(-1) # (M,)
165
+ out = torch.einsum("m,d->md", pos, omega) # (M, D/2), outer product
166
+
167
+ emb_sin = torch.sin(out) # (M, D/2)
168
+ emb_cos = torch.cos(out) # (M, D/2)
169
+
170
+ embeddings = torch.cat([emb_sin, emb_cos], dim=1) # (M, D)
171
+
172
+ return embeddings
173
+
174
+
175
+ def timestep_embedding(timesteps, dim, max_period=10000):
176
+ """
177
+ Create sinusoidal timestep embeddings.
178
+ :param timesteps: a 1-D Tensor of N indices, one per batch element.
179
+ These may be fractional.
180
+ :param dim: the dimension of the output.
181
+ :param max_period: controls the minimum frequency of the embeddings.
182
+ :return: an [N x dim] Tensor of positional embeddings.
183
+ """
184
+ half = dim // 2
185
+ freqs = torch.exp(
186
+ -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
187
+ ).to(device=timesteps.device)
188
+ args = timesteps[:, None].to(timesteps.dtype) * freqs[None]
189
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
190
+ if dim % 2:
191
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
192
+ return embedding
193
+
194
+
195
+ def get_embedder(embed_type="fourier", num_freqs=-1, input_dim=3, degree=4,
196
+ num_levels=16, level_dim=2, per_level_scale=2, base_resolution=16,
197
+ log2_hashmap_size=19, desired_resolution=None):
198
+ if embed_type == "identity" or (embed_type == "fourier" and num_freqs == -1):
199
+ return nn.Identity(), input_dim
200
+
201
+ elif embed_type == "fourier":
202
+ embedder_obj = FourierEmbedder(num_freqs=num_freqs, input_dim=input_dim,
203
+ logspace=True, include_input=True)
204
+ return embedder_obj, embedder_obj.out_dim
205
+
206
+ elif embed_type == "hashgrid":
207
+ raise NotImplementedError
208
+
209
+ elif embed_type == "sphere_harmonic":
210
+ raise NotImplementedError
211
+
212
+ else:
213
+ raise ValueError(f"{embed_type} is not valid. Currently only supprts {VALID_EMBED_TYPES}")
mulres_infer/modules/miche/michelangelo/models/modules/transformer_blocks.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import math
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from typing import Optional
8
+ from modules.miche.michelangelo.models.modules.checkpoint import checkpoint
9
+
10
+ # Initialize linear layers with normal distribution weights and zero biases
11
+ def init_linear(l, stddev):
12
+ nn.init.normal_(l.weight, std=stddev)
13
+ if l.bias is not None:
14
+ nn.init.constant_(l.bias, 0.0)
15
+
16
+ # Multihead attention module
17
+ class MultiheadAttention(nn.Module):
18
+ def __init__(
19
+ self,
20
+ *,
21
+ device: torch.device,
22
+ dtype: torch.dtype,
23
+ n_ctx: int, # Context size
24
+ width: int, # Width of the input tensor
25
+ heads: int, # Number of attention heads
26
+ init_scale: float, # Initialization scale for weights
27
+ qkv_bias: bool, # Whether to use bias in QKV layers
28
+ flash: bool = False # Whether to use flash attention
29
+ ):
30
+ super().__init__()
31
+ self.n_ctx = n_ctx
32
+ self.width = width
33
+ self.heads = heads
34
+ self.c_qkv = nn.Linear(width, width * 3, bias=qkv_bias, device=device, dtype=dtype)
35
+ self.c_proj = nn.Linear(width, width, device=device, dtype=dtype)
36
+ self.attention = QKVMultiheadAttention(device=device, dtype=dtype, heads=heads, n_ctx=n_ctx, flash=flash)
37
+ init_linear(self.c_qkv, init_scale)
38
+ init_linear(self.c_proj, init_scale)
39
+
40
+ def forward(self, x):
41
+ x = self.c_qkv(x)
42
+ x = checkpoint(self.attention, (x,), (), True)
43
+ x = self.c_proj(x)
44
+ return x
45
+
46
+ # QKV multihead attention module
47
+ class QKVMultiheadAttention(nn.Module):
48
+ def __init__(self, *, device: torch.device, dtype: torch.dtype, heads: int, n_ctx: int, flash: bool = False):
49
+ super().__init__()
50
+ self.device = device
51
+ self.dtype = dtype
52
+ self.heads = heads
53
+ self.n_ctx = n_ctx
54
+ self.flash = flash
55
+
56
+ def forward(self, qkv):
57
+ bs, n_ctx, width = qkv.shape
58
+ attn_ch = width // self.heads // 3
59
+ scale = 1 / math.sqrt(math.sqrt(attn_ch))
60
+ qkv = qkv.view(bs, n_ctx, self.heads, -1)
61
+ q, k, v = torch.split(qkv, attn_ch, dim=-1)
62
+
63
+ if self.flash:
64
+ # print(1)
65
+ # # out = F.scaled_dot_product_attention(q, k, v)
66
+ # q = q.permute(0, 2, 1, 3) # -> (b, h, t, c)
67
+ # k = k.permute(0, 2, 1, 3) # -> (b, h, t, c)
68
+ # v = v.permute(0, 2, 1, 3) # -> (b, h, t, c)
69
+
70
+ # # F.scaled_dot_product_attention 直接返回计算结果,无需手动 reshape
71
+ # out = F.scaled_dot_product_attention(q, k, v) # -> (b, h, t, c)
72
+
73
+ # # 关键:将输出变回 (b, t, h, c) 并 reshape,以匹配 else 分支的输出
74
+ # out = out.permute(0, 2, 1, 3).reshape(bs, n_ctx, -1) # -> (b, t, h*c)
75
+ out = torch_npu.npu_fusion_attention(
76
+ q, k, v, self.heads, "BSND", keep_prob=1.0, scale=scale)[0].reshape(bs, n_ctx, -1)
77
+ else:
78
+ weight = torch.einsum(
79
+ "bthc,bshc->bhts", q * scale, k * scale
80
+ ) # More stable with f16 than dividing afterwards
81
+ wdtype = weight.dtype
82
+ weight = torch.softmax(weight.float(), dim=-1).type(wdtype)
83
+ out = torch.einsum("bhts,bshc->bthc", weight, v).reshape(bs, n_ctx, -1)
84
+
85
+ return out
86
+
87
+ # Residual attention block module
88
+ class ResidualAttentionBlock(nn.Module):
89
+ def __init__(
90
+ self,
91
+ *,
92
+ device: torch.device,
93
+ dtype: torch.dtype,
94
+ use_checkpoint: bool = False,
95
+ n_ctx: int, # Context size
96
+ width: int, # Width of the input tensor
97
+ heads: int, # Number of attention heads
98
+ init_scale: float, # Initialization scale for weights
99
+ qkv_bias: bool, # Whether to use bias in QKV layers
100
+ flash: bool = False # Whether to use flash attention
101
+ ):
102
+ super().__init__()
103
+
104
+ self.use_checkpoint = use_checkpoint
105
+
106
+ self.attn = MultiheadAttention(
107
+ device=device,
108
+ dtype=dtype,
109
+ n_ctx=n_ctx,
110
+ width=width,
111
+ heads=heads,
112
+ init_scale=init_scale,
113
+ qkv_bias=qkv_bias,
114
+ flash=flash
115
+ )
116
+ self.ln_1 = nn.LayerNorm(width, device=device, dtype=dtype)
117
+ self.mlp = MLP(device=device, dtype=dtype, width=width, init_scale=init_scale)
118
+ self.ln_2 = nn.LayerNorm(width, device=device, dtype=dtype)
119
+
120
+ def _forward(self, x: torch.Tensor):
121
+ x = x + self.attn(self.ln_1(x))
122
+ x = x + self.mlp(self.ln_2(x))
123
+ return x
124
+
125
+ def forward(self, x: torch.Tensor):
126
+ return checkpoint(self._forward, (x,), self.parameters(), self.use_checkpoint)
127
+
128
+ # Multihead cross attention module
129
+ class MultiheadCrossAttention(nn.Module):
130
+ def __init__(
131
+ self,
132
+ *,
133
+ device: torch.device,
134
+ dtype: torch.dtype,
135
+ n_data: Optional[int] = None,
136
+ data_width: Optional[int] = None,
137
+ width: int, # Width of the input tensor
138
+ heads: int, # Number of attention heads
139
+ init_scale: float, # Initialization scale for weights
140
+ qkv_bias: bool, # Whether to use bias in QKV layers
141
+ flash: bool = False # Whether to use flash attention
142
+ ):
143
+ super().__init__()
144
+ self.n_data = n_data
145
+ self.width = width
146
+ self.heads = heads
147
+ self.data_width = width if data_width is None else data_width
148
+ self.c_q = nn.Linear(width, width, bias=qkv_bias, device=device, dtype=dtype)
149
+ self.c_kv = nn.Linear(self.data_width, width * 2, bias=qkv_bias, device=device, dtype=dtype)
150
+ self.c_proj = nn.Linear(width, width, device=device, dtype=dtype)
151
+ self.attention = QKVMultiheadCrossAttention(
152
+ device=device, dtype=dtype, heads=heads, n_data=n_data, flash=flash
153
+ )
154
+ init_linear(self.c_q, init_scale)
155
+ init_linear(self.c_kv, init_scale)
156
+ init_linear(self.c_proj, init_scale)
157
+
158
+ def forward(self, x, data):
159
+ x = self.c_q(x)
160
+ data = self.c_kv(data)
161
+ x = checkpoint(self.attention, (x, data), (), True)
162
+ x = self.c_proj(x)
163
+ return x
164
+
165
+ # QKV multihead cross attention module
166
+ class QKVMultiheadCrossAttention(nn.Module):
167
+ def __init__(self, *, device: torch.device, dtype: torch.dtype, heads: int,
168
+ flash: bool = False, n_data: Optional[int] = None):
169
+
170
+ super().__init__()
171
+ self.device = device
172
+ self.dtype = dtype
173
+ self.heads = heads
174
+ self.n_data = n_data
175
+ self.flash = flash
176
+
177
+ def forward(self, q, kv):
178
+ _, n_ctx, _ = q.shape
179
+ bs, n_data, width = kv.shape
180
+ attn_ch = width // self.heads // 2
181
+ scale = 1 / math.sqrt(math.sqrt(attn_ch))
182
+ q = q.view(bs, n_ctx, self.heads, -1)
183
+ kv = kv.view(bs, n_data, self.heads, -1)
184
+ k, v = torch.split(kv, attn_ch, dim=-1)
185
+
186
+ if self.flash:
187
+ # print(2)
188
+ # # out = F.scaled_dot_product_attention(q, k, v)
189
+ # q = q.permute(0, 2, 1, 3) # -> (b, h, t, c)
190
+ # k = k.permute(0, 2, 1, 3) # -> (b, h, t, c)
191
+ # v = v.permute(0, 2, 1, 3) # -> (b, h, t, c)
192
+
193
+ # # F.scaled_dot_product_attention 直接返回计算结果,无需手动 reshape
194
+ # out = F.scaled_dot_product_attention(q, k, v) # -> (b, h, t, c)
195
+
196
+ # # 关键:将输出变回 (b, t, h, c) 并 reshape,以匹配 else 分支的输出
197
+ # out = out.permute(0, 2, 1, 3).reshape(bs, n_ctx, -1) # -> (b, t, h*c)
198
+ out = torch_npu.npu_fusion_attention(
199
+ q, k, v, self.heads, "BSND", keep_prob=1.0, scale=scale)[0].reshape(bs, n_ctx, -1)
200
+ else:
201
+ weight = torch.einsum(
202
+ "bthc,bshc->bhts", q * scale, k * scale
203
+ ) # More stable with f16 than dividing afterwards
204
+ wdtype = weight.dtype
205
+ weight = torch.softmax(weight.float(), dim=-1).type(wdtype)
206
+ out = torch.einsum("bhts,bshc->bthc", weight, v).reshape(bs, n_ctx, -1)
207
+
208
+ return out
209
+
210
+ # Residual cross attention block module
211
+ class ResidualCrossAttentionBlock(nn.Module):
212
+ def __init__(
213
+ self,
214
+ *,
215
+ device: Optional[torch.device],
216
+ dtype: Optional[torch.dtype],
217
+ n_data: Optional[int] = None,
218
+ data_width: Optional[int] = None,
219
+ width: int, # Width of the input tensor
220
+ heads: int, # Number of attention heads
221
+ init_scale: float, # Initialization scale for weights
222
+ qkv_bias: bool, # Whether to use bias in QKV layers
223
+ flash: bool = False # Whether to use flash attention
224
+ ):
225
+ super().__init__()
226
+
227
+ if data_width is None:
228
+ data_width = width
229
+
230
+ self.attn = MultiheadCrossAttention(
231
+ device=device,
232
+ dtype=dtype,
233
+ n_data=n_data,
234
+ width=width,
235
+ heads=heads,
236
+ data_width=data_width,
237
+ init_scale=init_scale,
238
+ qkv_bias=qkv_bias,
239
+ flash=flash,
240
+ )
241
+ self.ln_1 = nn.LayerNorm(width, device=device, dtype=dtype)
242
+ self.ln_2 = nn.LayerNorm(data_width, device=device, dtype=dtype)
243
+ self.mlp = MLP(device=device, dtype=dtype, width=width, init_scale=init_scale)
244
+ self.ln_3 = nn.LayerNorm(width, device=device, dtype=dtype)
245
+
246
+ def forward(self, x: torch.Tensor, data: torch.Tensor):
247
+ x = x + self.attn(self.ln_1(x), self.ln_2(data))
248
+ x = x + self.mlp(self.ln_3(x))
249
+ return x
250
+
251
+ # MLP Module
252
+ class MLP(nn.Module):
253
+ def __init__(self, *,
254
+ device: Optional[torch.device],
255
+ dtype: Optional[torch.dtype],
256
+ width: int,
257
+ init_scale: float):
258
+ super().__init__()
259
+ self.width = width
260
+ self.c_fc = nn.Linear(width, width * 4, device=device, dtype=dtype)
261
+ self.c_proj = nn.Linear(width * 4, width, device=device, dtype=dtype)
262
+ self.gelu = nn.GELU()
263
+ init_linear(self.c_fc, init_scale)
264
+ init_linear(self.c_proj, init_scale)
265
+
266
+ def forward(self, x):
267
+ return self.c_proj(self.gelu(self.c_fc(x)))
268
+
269
+ # Transformer Module
270
+ class Transformer(nn.Module):
271
+ def __init__(
272
+ self,
273
+ *,
274
+ device: Optional[torch.device],
275
+ dtype: Optional[torch.dtype],
276
+ layers: int,
277
+ use_checkpoint: bool = False,
278
+ n_ctx: int, # Context size
279
+ width: int, # Width of the input tensor
280
+ heads: int, # Number of attention heads
281
+ init_scale: float, # Initialization scale for weights
282
+ qkv_bias: bool, # Whether to use bias in QKV layers
283
+ flash: bool = False # Whether to use flash attention
284
+ ):
285
+ super().__init__()
286
+ self.n_ctx = n_ctx
287
+ self.width = width
288
+ self.layers = layers
289
+ self.resblocks = nn.ModuleList(
290
+ [
291
+ ResidualAttentionBlock(
292
+ device=device,
293
+ dtype=dtype,
294
+ n_ctx=n_ctx,
295
+ width=width,
296
+ heads=heads,
297
+ init_scale=init_scale,
298
+ qkv_bias=qkv_bias,
299
+ flash=flash,
300
+ use_checkpoint=use_checkpoint
301
+ )
302
+ for _ in range(layers)
303
+ ]
304
+ )
305
+
306
+ def forward(self, x: torch.Tensor):
307
+ for block in self.resblocks:
308
+ x = block(x)
309
+ return x
mulres_infer/modules/miche/michelangelo/models/tsal/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # -*- coding: utf-8 -*-
mulres_infer/modules/miche/michelangelo/models/tsal/asl_pl_module.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from typing import List, Tuple, Dict, Optional
4
+ from omegaconf import DictConfig
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from torch import nn
9
+ from torch.optim import lr_scheduler
10
+ from typing import Union
11
+ from functools import partial
12
+
13
+ from modules.miche.michelangelo.utils import instantiate_from_config
14
+
15
+ from .tsal_base import (
16
+ AlignedShapeAsLatentModule,
17
+ ShapeAsLatentModule,
18
+ Latent2MeshOutput,
19
+ AlignedMeshOutput
20
+ )
21
+ from modules.miche.michelangelo.models.tsal.inference_utils import extract_geometry
22
+ import trimesh
23
+
24
+ class AlignedShapeAsLatentPLModule(nn.Module):
25
+ def __init__(self, *,
26
+ shape_module_cfg,
27
+ aligned_module_cfg,
28
+ loss_cfg,
29
+ optimizer_cfg: Optional[DictConfig] = None,
30
+ ckpt_path: Optional[str] = None,
31
+ ignore_keys: Union[Tuple[str], List[str]] = ()):
32
+
33
+ super().__init__()
34
+
35
+ shape_model: ShapeAsLatentModule = instantiate_from_config(
36
+ shape_module_cfg, device=None, dtype=None
37
+ )
38
+ self.model: AlignedShapeAsLatentModule = instantiate_from_config(
39
+ aligned_module_cfg, shape_model=shape_model
40
+ )
41
+
42
+ self.loss = instantiate_from_config(loss_cfg)
43
+
44
+ self.optimizer_cfg = optimizer_cfg
45
+
46
+ if ckpt_path is not None:
47
+ self.init_from_ckpt(ckpt_path, ignore_keys=ignore_keys)
48
+
49
+ def set_shape_model_only(self):
50
+ self.model.set_shape_model_only()
51
+
52
+ @property
53
+ def latent_shape(self):
54
+ return self.model.shape_model.latent_shape
55
+
56
+ @property
57
+ def zero_rank(self):
58
+ if self._trainer:
59
+ zero_rank = self.trainer.local_rank == 0
60
+ else:
61
+ zero_rank = True
62
+
63
+ return zero_rank
64
+
65
+ def init_from_ckpt(self, path, ignore_keys=()):
66
+ state_dict = torch.load(path, map_location="cpu")["state_dict"]
67
+
68
+ keys = list(state_dict.keys())
69
+ for k in keys:
70
+ for ik in ignore_keys:
71
+ if k.startswith(ik):
72
+ print("Deleting key {} from state_dict.".format(k))
73
+ del state_dict[k]
74
+
75
+ missing, unexpected = self.load_state_dict(state_dict, strict=False)
76
+ print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys")
77
+ if len(missing) > 0:
78
+ print(f"Missing Keys: {missing}")
79
+ print(f"Unexpected Keys: {unexpected}")
80
+
81
+ def configure_optimizers(self) -> Tuple[List, List]:
82
+ lr = self.learning_rate
83
+
84
+ trainable_parameters = list(self.model.parameters())
85
+
86
+ if self.optimizer_cfg is None:
87
+ optimizers = [torch.optim.AdamW(trainable_parameters, lr=lr, betas=(0.9, 0.99), weight_decay=1e-3)]
88
+ schedulers = []
89
+ else:
90
+ optimizer = instantiate_from_config(self.optimizer_cfg.optimizer, params=trainable_parameters)
91
+ scheduler_func = instantiate_from_config(
92
+ self.optimizer_cfg.scheduler,
93
+ max_decay_steps=self.trainer.max_steps,
94
+ lr_max=lr
95
+ )
96
+ scheduler = {
97
+ "scheduler": lr_scheduler.LambdaLR(optimizer, lr_lambda=scheduler_func.schedule),
98
+ "interval": "step",
99
+ "frequency": 1
100
+ }
101
+ optimizers = [optimizer]
102
+ schedulers = [scheduler]
103
+
104
+ return optimizers, schedulers
105
+
106
+ def forward(self,
107
+ surface: torch.FloatTensor,
108
+ image: torch.FloatTensor,
109
+ text: torch.FloatTensor,
110
+ volume_queries: torch.FloatTensor):
111
+ # Args:
112
+ # surface (torch.FloatTensor):
113
+ # image (torch.FloatTensor):
114
+ # text (torch.FloatTensor):
115
+ # volume_queries (torch.FloatTensor):
116
+ #
117
+ # Returns:
118
+
119
+ embed_outputs, shape_z = self.model(surface, image, text)
120
+
121
+ shape_zq, posterior = self.model.shape_model.encode_kl_embed(shape_z)
122
+ latents = self.model.shape_model.decode(shape_zq)
123
+ logits = self.model.shape_model.query_geometry(volume_queries, latents)
124
+
125
+ return embed_outputs, logits, posterior
126
+
127
+ def encode(self, surface: torch.FloatTensor, sample_posterior=True):
128
+
129
+ pc = surface[..., 0:3]
130
+ feats = surface[..., 3:6]
131
+
132
+ shape_embed, shape_zq, posterior = self.model.shape_model.encode(
133
+ pc=pc, feats=feats, sample_posterior=sample_posterior
134
+ )
135
+
136
+ return shape_zq
137
+
138
+ def encode_latents(self, surface: torch.FloatTensor, img_feats: Optional[torch.FloatTensor] = None):
139
+
140
+ pc = surface[..., 0:3]
141
+ feats = surface[..., 3:6]
142
+
143
+ shape_embed, shape_latents = self.model.shape_model.encode_latents(
144
+ pc=pc, feats=feats, img_feats=img_feats,
145
+ )
146
+ shape_embed = shape_embed.unsqueeze(1)
147
+ # 长度无关: latent 数可被 PointConditioner._expand_latents 扩容(如 1024), 只校验头 token
148
+ assert shape_embed.shape[1] == 1
149
+ cat_latents = torch.cat([shape_embed, shape_latents], dim=1)
150
+
151
+ return cat_latents
152
+
153
+ def recon(self, surface):
154
+ cat_latents = self.encode_latents(surface)
155
+ shape_latents = cat_latents[:, 1:]
156
+ shape_zq, posterior = self.model.shape_model.encode_kl_embed(shape_latents)
157
+
158
+ # decoding
159
+ latents = self.model.shape_model.decode(shape_zq)
160
+ geometric_func = partial(self.model.shape_model.query_geometry, latents=latents)
161
+
162
+ # reconstruction
163
+ mesh_v_f, has_surface = extract_geometry(
164
+ geometric_func=geometric_func,
165
+ device=surface.device,
166
+ batch_size=surface.shape[0],
167
+ bounds=(-1.25, -1.25, -1.25, 1.25, 1.25, 1.25),
168
+ octree_depth=7,
169
+ num_chunks=10000,
170
+ )
171
+ recon_mesh = trimesh.Trimesh(mesh_v_f[0][0], mesh_v_f[0][1])
172
+
173
+ return recon_mesh
174
+
175
+
176
+ def to_shape_latents(self, latents):
177
+
178
+ shape_zq, posterior = self.model.shape_model.encode_kl_embed(latents, sample_posterior = False)
179
+ return self.model.shape_model.decode(shape_zq)
180
+
181
+ def decode(self,
182
+ z_q,
183
+ bounds: Union[Tuple[float], List[float], float] = 1.1,
184
+ octree_depth: int = 7,
185
+ num_chunks: int = 10000) -> List[Latent2MeshOutput]:
186
+
187
+ latents = self.model.shape_model.decode(z_q) # latents: [bs, num_latents, dim]
188
+ outputs = self.latent2mesh(latents, bounds=bounds, octree_depth=octree_depth, num_chunks=num_chunks)
189
+
190
+ return outputs
191
+
192
+ def training_step(self, batch: Dict[str, torch.FloatTensor],
193
+ batch_idx: int, optimizer_idx: int = 0) -> torch.FloatTensor:
194
+ #Args:
195
+ # batch (dict): the batch sample, and it contains:
196
+ # - surface (torch.FloatTensor): [bs, n_surface, (3 + input_dim)]
197
+ # - image (torch.FloatTensor): [bs, 3, 224, 224]
198
+ # - text (torch.FloatTensor): [bs, num_templates, 77]
199
+ # - geo_points (torch.FloatTensor): [bs, n_pts, (3 + 1)]
200
+ #
201
+ # batch_idx (int):
202
+ #
203
+ # optimizer_idx (int):
204
+ #
205
+ # Returns:
206
+ # loss (torch.FloatTensor):
207
+
208
+ surface = batch["surface"]
209
+ image = batch["image"]
210
+ text = batch["text"]
211
+
212
+ volume_queries = batch["geo_points"][..., 0:3]
213
+ shape_labels = batch["geo_points"][..., -1]
214
+
215
+ embed_outputs, shape_logits, posteriors = self(surface, image, text, volume_queries)
216
+
217
+ aeloss, log_dict_ae = self.loss(
218
+ **embed_outputs,
219
+ posteriors=posteriors,
220
+ shape_logits=shape_logits,
221
+ shape_labels=shape_labels,
222
+ split="train"
223
+ )
224
+
225
+ self.log_dict(log_dict_ae, prog_bar=True, logger=True, batch_size=shape_logits.shape[0],
226
+ sync_dist=False, rank_zero_only=True)
227
+
228
+ return aeloss
229
+
230
+ def validation_step(self, batch: Dict[str, torch.FloatTensor], batch_idx: int) -> torch.FloatTensor:
231
+
232
+ surface = batch["surface"]
233
+ image = batch["image"]
234
+ text = batch["text"]
235
+
236
+ volume_queries = batch["geo_points"][..., 0:3]
237
+ shape_labels = batch["geo_points"][..., -1]
238
+
239
+ embed_outputs, shape_logits, posteriors = self(surface, image, text, volume_queries)
240
+
241
+ aeloss, log_dict_ae = self.loss(
242
+ **embed_outputs,
243
+ posteriors=posteriors,
244
+ shape_logits=shape_logits,
245
+ shape_labels=shape_labels,
246
+ split="val"
247
+ )
248
+ self.log_dict(log_dict_ae, prog_bar=True, logger=True, batch_size=shape_logits.shape[0],
249
+ sync_dist=False, rank_zero_only=True)
250
+
251
+ return aeloss
252
+
253
+ def visual_alignment(self,
254
+ surface: torch.FloatTensor,
255
+ image: torch.FloatTensor,
256
+ text: torch.FloatTensor,
257
+ description: Optional[List[str]] = None,
258
+ bounds: Union[Tuple[float], List[float]] = (-1.25, -1.25, -1.25, 1.25, 1.25, 1.25),
259
+ octree_depth: int = 7,
260
+ num_chunks: int = 10000) -> List[AlignedMeshOutput]:
261
+
262
+ """
263
+
264
+ Args:
265
+ surface:
266
+ image:
267
+ text:
268
+ description:
269
+ bounds:
270
+ octree_depth:
271
+ num_chunks:
272
+
273
+ Returns:
274
+ mesh_outputs (List[AlignedMeshOutput]): the mesh outputs list.
275
+
276
+ """
277
+
278
+ outputs = []
279
+
280
+ device = surface.device
281
+ bs = surface.shape[0]
282
+
283
+ embed_outputs, shape_z = self.model(surface, image, text)
284
+
285
+ # calculate the similarity
286
+ image_embed = embed_outputs["image_embed"]
287
+ text_embed = embed_outputs["text_embed"]
288
+ shape_embed = embed_outputs["shape_embed"]
289
+
290
+ # normalized features
291
+ shape_embed = F.normalize(shape_embed, dim=-1, p=2)
292
+ text_embed = F.normalize(text_embed, dim=-1, p=2)
293
+ image_embed = F.normalize(image_embed, dim=-1, p=2)
294
+
295
+ # B x B
296
+ shape_text_similarity = (100.0 * shape_embed @ text_embed.T).softmax(dim=-1)
297
+
298
+ # B x B
299
+ shape_image_similarity = (100.0 * shape_embed @ image_embed.T).softmax(dim=-1)
300
+
301
+ # shape reconstruction
302
+ shape_zq, posterior = self.model.shape_model.encode_kl_embed(shape_z)
303
+ latents = self.model.shape_model.decode(shape_zq)
304
+ geometric_func = partial(self.model.shape_model.query_geometry, latents=latents)
305
+
306
+ # 2. decode geometry
307
+ mesh_v_f, has_surface = extract_geometry(
308
+ geometric_func=geometric_func,
309
+ device=device,
310
+ batch_size=bs,
311
+ bounds=bounds,
312
+ octree_depth=octree_depth,
313
+ num_chunks=num_chunks,
314
+ disable=not self.zero_rank
315
+ )
316
+
317
+ # 3. decode texture
318
+ for i, ((mesh_v, mesh_f), is_surface) in enumerate(zip(mesh_v_f, has_surface)):
319
+ if not is_surface:
320
+ outputs.append(None)
321
+ continue
322
+
323
+ out = AlignedMeshOutput()
324
+ out.mesh_v = mesh_v
325
+ out.mesh_f = mesh_f
326
+ out.surface = surface[i].cpu().numpy()
327
+ out.image = image[i].cpu().numpy()
328
+ if description is not None:
329
+ out.text = description[i]
330
+ out.shape_text_similarity = shape_text_similarity[i, i]
331
+ out.shape_image_similarity = shape_image_similarity[i, i]
332
+
333
+ outputs.append(out)
334
+
335
+ return outputs
336
+
337
+ def latent2mesh(self,
338
+ latents: torch.FloatTensor,
339
+ bounds: Union[Tuple[float], List[float], float] = 1.1,
340
+ octree_depth: int = 7,
341
+ num_chunks: int = 10000) -> List[Latent2MeshOutput]:
342
+
343
+ """
344
+
345
+ Args:
346
+ latents: [bs, num_latents, dim]
347
+ bounds:
348
+ octree_depth:
349
+ num_chunks:
350
+
351
+ Returns:
352
+ mesh_outputs (List[MeshOutput]): the mesh outputs list.
353
+
354
+ """
355
+
356
+ outputs = []
357
+
358
+ geometric_func = partial(self.model.shape_model.query_geometry, latents=latents)
359
+
360
+ # 2. decode geometry
361
+ device = latents.device
362
+ mesh_v_f, has_surface = extract_geometry(
363
+ geometric_func=geometric_func,
364
+ device=device,
365
+ batch_size=len(latents),
366
+ bounds=bounds,
367
+ octree_depth=octree_depth,
368
+ num_chunks=num_chunks,
369
+ disable=not self.zero_rank
370
+ )
371
+
372
+ # 3. decode texture
373
+ for i, ((mesh_v, mesh_f), is_surface) in enumerate(zip(mesh_v_f, has_surface)):
374
+ if not is_surface:
375
+ outputs.append(None)
376
+ continue
377
+
378
+ out = Latent2MeshOutput()
379
+ out.mesh_v = mesh_v
380
+ out.mesh_f = mesh_f
381
+
382
+ outputs.append(out)
383
+
384
+ return outputs
mulres_infer/modules/miche/michelangelo/models/tsal/clip_asl_module.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import torch
4
+ from torch import nn
5
+ from einops import rearrange
6
+ from transformers import CLIPModel
7
+
8
+ from modules.miche.michelangelo.models.tsal.tsal_base import AlignedShapeAsLatentModule
9
+
10
+
11
+ class CLIPAlignedShapeAsLatentModule(AlignedShapeAsLatentModule):
12
+
13
+ def __init__(self, *,
14
+ shape_model,
15
+ clip_model_version: str = "openai/clip-vit-large-patch14"):
16
+
17
+ super().__init__()
18
+
19
+ # self.clip_model: CLIPModel = CLIPModel.from_pretrained(clip_model_version)
20
+ # for params in self.clip_model.parameters():
21
+ # params.requires_grad = False
22
+ self.clip_model = None
23
+ self.shape_model = shape_model
24
+ self.shape_projection = nn.Parameter(torch.empty(self.shape_model.width, self.shape_model.width))
25
+ # nn.init.normal_(self.shape_projection, std=self.shape_model.width ** -0.5)
26
+
27
+ def set_shape_model_only(self):
28
+ self.clip_model = None
29
+
30
+ def encode_shape_embed(self, surface, return_latents: bool = False):
31
+ """
32
+
33
+ Args:
34
+ surface (torch.FloatTensor): [bs, n, 3 + c]
35
+ return_latents (bool):
36
+
37
+ Returns:
38
+ x (torch.FloatTensor): [bs, projection_dim]
39
+ shape_latents (torch.FloatTensor): [bs, m, d]
40
+ """
41
+
42
+ pc = surface[..., 0:3]
43
+ feats = surface[..., 3:]
44
+
45
+ shape_embed, shape_latents = self.shape_model.encode_latents(pc, feats)
46
+ x = shape_embed @ self.shape_projection
47
+
48
+ if return_latents:
49
+ return x, shape_latents
50
+ else:
51
+ return x
52
+
53
+ def encode_image_embed(self, image):
54
+ """
55
+
56
+ Args:
57
+ image (torch.FloatTensor): [bs, 3, h, w]
58
+
59
+ Returns:
60
+ x (torch.FloatTensor): [bs, projection_dim]
61
+ """
62
+
63
+ x = self.clip_model.get_image_features(image)
64
+
65
+ return x
66
+
67
+ def encode_text_embed(self, text):
68
+ x = self.clip_model.get_text_features(text)
69
+ return x
70
+
71
+ def forward(self, surface, image, text):
72
+ """
73
+
74
+ Args:
75
+ surface (torch.FloatTensor):
76
+ image (torch.FloatTensor): [bs, 3, 224, 224]
77
+ text (torch.LongTensor): [bs, num_templates, 77]
78
+
79
+ Returns:
80
+ embed_outputs (dict): the embedding outputs, and it contains:
81
+ - image_embed (torch.FloatTensor):
82
+ - text_embed (torch.FloatTensor):
83
+ - shape_embed (torch.FloatTensor):
84
+ - logit_scale (float):
85
+ """
86
+
87
+ # # text embedding
88
+ # text_embed_all = []
89
+ # for i in range(text.shape[0]):
90
+ # text_for_one_sample = text[i]
91
+ # text_embed = self.encode_text_embed(text_for_one_sample)
92
+ # text_embed = text_embed / text_embed.norm(dim=-1, keepdim=True)
93
+ # text_embed = text_embed.mean(dim=0)
94
+ # text_embed = text_embed / text_embed.norm(dim=-1, keepdim=True)
95
+ # text_embed_all.append(text_embed)
96
+ # text_embed_all = torch.stack(text_embed_all)
97
+
98
+ b = text.shape[0]
99
+ text_tokens = rearrange(text, "b t l -> (b t) l")
100
+ text_embed = self.encode_text_embed(text_tokens)
101
+ text_embed = rearrange(text_embed, "(b t) d -> b t d", b=b)
102
+ text_embed = text_embed.mean(dim=1)
103
+ text_embed = text_embed / text_embed.norm(dim=-1, keepdim=True)
104
+
105
+ # image embedding
106
+ image_embed = self.encode_image_embed(image)
107
+
108
+ # shape embedding
109
+ shape_embed, shape_latents = self.encode_shape_embed(surface, return_latents=True)
110
+
111
+ embed_outputs = {
112
+ "image_embed": image_embed,
113
+ "text_embed": text_embed,
114
+ "shape_embed": shape_embed,
115
+ # "logit_scale": self.clip_model.logit_scale.exp()
116
+ }
117
+
118
+ return embed_outputs, shape_latents
mulres_infer/modules/miche/michelangelo/models/tsal/inference_utils.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import torch
4
+ from tqdm import tqdm
5
+ from einops import repeat
6
+ import numpy as np
7
+ from typing import Callable, Tuple, List, Union, Optional
8
+ from skimage import measure
9
+
10
+ from modules.miche.michelangelo.graphics.primitives import generate_dense_grid_points
11
+
12
+
13
+ @torch.no_grad()
14
+ def extract_geometry(geometric_func: Callable,
15
+ device: torch.device,
16
+ batch_size: int = 1,
17
+ bounds: Union[Tuple[float], List[float], float] = (-1.25, -1.25, -1.25, 1.25, 1.25, 1.25),
18
+ octree_depth: int = 7,
19
+ num_chunks: int = 10000,
20
+ disable: bool = True):
21
+
22
+ # Args:
23
+ # geometric_func:
24
+ # device:
25
+ # bounds:
26
+ # octree_depth:
27
+ # batch_size:
28
+ # num_chunks:
29
+ # disable:
30
+ # Returns:
31
+
32
+ if isinstance(bounds, float):
33
+ bounds = [-bounds, -bounds, -bounds, bounds, bounds, bounds]
34
+
35
+ bbox_min = np.array(bounds[0:3])
36
+ bbox_max = np.array(bounds[3:6])
37
+ bbox_size = bbox_max - bbox_min
38
+
39
+ xyz_samples, grid_size, length = generate_dense_grid_points(
40
+ bbox_min=bbox_min,
41
+ bbox_max=bbox_max,
42
+ octree_depth=octree_depth,
43
+ indexing="ij"
44
+ )
45
+ xyz_samples = torch.FloatTensor(xyz_samples)
46
+
47
+ batch_logits = []
48
+ for start in tqdm(range(0, xyz_samples.shape[0], num_chunks),
49
+ desc="Implicit Function:", disable=disable, leave=False):
50
+ queries = xyz_samples[start: start + num_chunks, :].to(device)
51
+ batch_queries = repeat(queries, "p c -> b p c", b=batch_size)
52
+
53
+ logits = geometric_func(batch_queries)
54
+ batch_logits.append(logits.cpu())
55
+
56
+ grid_logits = torch.cat(batch_logits, dim=1).view((batch_size, grid_size[0], grid_size[1], grid_size[2])).numpy()
57
+
58
+ mesh_v_f = []
59
+ has_surface = np.zeros((batch_size,), dtype=np.bool_)
60
+ for i in range(batch_size):
61
+ try:
62
+ vertices, faces, normals, _ = measure.marching_cubes(grid_logits[i], 0, method="lewiner")
63
+ vertices = vertices / grid_size * bbox_size + bbox_min
64
+ # vertices[:, [0, 1]] = vertices[:, [1, 0]]
65
+ mesh_v_f.append((vertices.astype(np.float32), np.ascontiguousarray(faces)))
66
+ has_surface[i] = True
67
+
68
+ except ValueError:
69
+ mesh_v_f.append((None, None))
70
+ has_surface[i] = False
71
+
72
+ except RuntimeError:
73
+ mesh_v_f.append((None, None))
74
+ has_surface[i] = False
75
+
76
+ return mesh_v_f, has_surface
mulres_infer/modules/miche/michelangelo/models/tsal/loss.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+
6
+ from typing import Optional
7
+
8
+ from modules.miche.michelangelo.models.modules.distributions import DiagonalGaussianDistribution
9
+ from modules.miche.michelangelo.utils import misc
10
+
11
+
12
+ class ContrastKLNearFar(nn.Module):
13
+ def __init__(self,
14
+ contrast_weight: float = 1.0,
15
+ near_weight: float = 0.1,
16
+ kl_weight: float = 1.0,
17
+ num_near_samples: Optional[int] = None):
18
+
19
+ super().__init__()
20
+
21
+ self.labels = None
22
+ self.last_local_batch_size = None
23
+
24
+ self.contrast_weight = contrast_weight
25
+ self.near_weight = near_weight
26
+ self.kl_weight = kl_weight
27
+ self.num_near_samples = num_near_samples
28
+ self.geo_criterion = nn.BCEWithLogitsLoss()
29
+
30
+ def forward(self,
31
+ shape_embed: torch.FloatTensor,
32
+ text_embed: torch.FloatTensor,
33
+ image_embed: torch.FloatTensor,
34
+ logit_scale: torch.FloatTensor,
35
+ posteriors: Optional[DiagonalGaussianDistribution],
36
+ shape_logits: torch.FloatTensor,
37
+ shape_labels: torch.FloatTensor,
38
+ split: Optional[str] = "train", **kwargs):
39
+
40
+ # shape_embed: torch.FloatTensor
41
+ # text_embed: torch.FloatTensor
42
+ # image_embed: torch.FloatTensor
43
+ # logit_scale: torch.FloatTensor
44
+ # posteriors: Optional[DiagonalGaussianDistribution]
45
+ # shape_logits: torch.FloatTensor
46
+ # shape_labels: torch.FloatTensor
47
+
48
+ local_batch_size = shape_embed.size(0)
49
+
50
+ if local_batch_size != self.last_local_batch_size:
51
+ self.labels = local_batch_size * misc.get_rank() + torch.arange(
52
+ local_batch_size, device=shape_embed.device
53
+ ).long()
54
+ self.last_local_batch_size = local_batch_size
55
+
56
+ # normalized features
57
+ shape_embed = F.normalize(shape_embed, dim=-1, p=2)
58
+ text_embed = F.normalize(text_embed, dim=-1, p=2)
59
+ image_embed = F.normalize(image_embed, dim=-1, p=2)
60
+
61
+ # gather features from all GPUs
62
+ shape_embed_all, text_embed_all, image_embed_all = misc.all_gather_batch(
63
+ [shape_embed, text_embed, image_embed]
64
+ )
65
+
66
+ # cosine similarity as logits
67
+ logits_per_shape_text = logit_scale * shape_embed @ text_embed_all.t()
68
+ logits_per_text_shape = logit_scale * text_embed @ shape_embed_all.t()
69
+ logits_per_shape_image = logit_scale * shape_embed @ image_embed_all.t()
70
+ logits_per_image_shape = logit_scale * image_embed @ shape_embed_all.t()
71
+ contrast_loss = (F.cross_entropy(logits_per_shape_text, self.labels) +
72
+ F.cross_entropy(logits_per_text_shape, self.labels)) / 2 + \
73
+ (F.cross_entropy(logits_per_shape_image, self.labels) +
74
+ F.cross_entropy(logits_per_image_shape, self.labels)) / 2
75
+
76
+ # shape reconstruction
77
+ if self.num_near_samples is None:
78
+ num_vol = shape_logits.shape[1] // 2
79
+ else:
80
+ num_vol = shape_logits.shape[1] - self.num_near_samples
81
+
82
+ vol_logits = shape_logits[:, 0:num_vol]
83
+ vol_labels = shape_labels[:, 0:num_vol]
84
+
85
+ near_logits = shape_logits[:, num_vol:]
86
+ near_labels = shape_labels[:, num_vol:]
87
+
88
+ # occupancy loss
89
+ vol_bce = self.geo_criterion(vol_logits.float(), vol_labels.float())
90
+ near_bce = self.geo_criterion(near_logits.float(), near_labels.float())
91
+
92
+ if posteriors is None:
93
+ kl_loss = torch.tensor(0.0, dtype=vol_logits.dtype, device=vol_logits.device)
94
+ else:
95
+ kl_loss = posteriors.kl(dims=(1, 2))
96
+ kl_loss = torch.mean(kl_loss)
97
+
98
+ loss = vol_bce + near_bce * self.near_weight + kl_loss * self.kl_weight + contrast_loss * self.contrast_weight
99
+
100
+ # compute accuracy
101
+ with torch.no_grad():
102
+ pred = torch.argmax(logits_per_shape_text, dim=-1)
103
+ correct = pred.eq(self.labels).sum()
104
+ shape_text_acc = 100 * correct / local_batch_size
105
+
106
+ pred = torch.argmax(logits_per_shape_image, dim=-1)
107
+ correct = pred.eq(self.labels).sum()
108
+ shape_image_acc = 100 * correct / local_batch_size
109
+
110
+ preds = shape_logits >= 0
111
+ accuracy = (preds == shape_labels).float()
112
+ accuracy = accuracy.mean()
113
+
114
+ log = {
115
+ "{}/contrast".format(split): contrast_loss.clone().detach(),
116
+ "{}/near".format(split): near_bce.detach(),
117
+ "{}/far".format(split): vol_bce.detach(),
118
+ "{}/kl".format(split): kl_loss.detach(),
119
+ "{}/shape_text_acc".format(split): shape_text_acc,
120
+ "{}/shape_image_acc".format(split): shape_image_acc,
121
+ "{}/total_loss".format(split): loss.clone().detach(),
122
+ "{}/accuracy".format(split): accuracy,
123
+ }
124
+
125
+ if posteriors is not None:
126
+ log[f"{split}/mean"] = posteriors.mean.mean().detach()
127
+ log[f"{split}/std_mean"] = posteriors.std.mean().detach()
128
+ log[f"{split}/std_max"] = posteriors.std.max().detach()
129
+
130
+ return loss, log
mulres_infer/modules/miche/michelangelo/models/tsal/sal_perceiver.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ from typing import Optional
6
+ from einops import repeat
7
+ import math
8
+
9
+ from modules.miche.michelangelo.models.modules import checkpoint
10
+ from modules.miche.michelangelo.models.modules.embedder import FourierEmbedder
11
+ from modules.miche.michelangelo.models.modules.distributions import DiagonalGaussianDistribution
12
+ from modules.miche.michelangelo.models.modules.transformer_blocks import (
13
+ ResidualCrossAttentionBlock,
14
+ Transformer
15
+ )
16
+
17
+ from .tsal_base import ShapeAsLatentModule
18
+
19
+
20
+ class CrossAttentionEncoder(nn.Module):
21
+
22
+ def __init__(self, *,
23
+ device: Optional[torch.device],
24
+ dtype: Optional[torch.dtype],
25
+ num_latents: int,
26
+ fourier_embedder: FourierEmbedder,
27
+ point_feats: int,
28
+ width: int,
29
+ heads: int,
30
+ layers: int,
31
+ init_scale: float = 0.25,
32
+ qkv_bias: bool = True,
33
+ flash: bool = False,
34
+ use_ln_post: bool = False,
35
+ use_checkpoint: bool = False,
36
+ img_feat_dim: int = 0):
37
+
38
+ super().__init__()
39
+
40
+ self.use_checkpoint = use_checkpoint
41
+ self.num_latents = num_latents
42
+
43
+ self.query = nn.Parameter(torch.randn((num_latents, width), device=device, dtype=dtype) * 0.02)
44
+
45
+ self.fourier_embedder = fourier_embedder
46
+ self.point_feats = point_feats
47
+ self.img_feat_dim = img_feat_dim
48
+ self.input_proj = nn.Linear(
49
+ self.fourier_embedder.out_dim + point_feats + img_feat_dim,
50
+ width,
51
+ device=device,
52
+ dtype=dtype,
53
+ )
54
+ self.cross_attn = ResidualCrossAttentionBlock(
55
+ device=device,
56
+ dtype=dtype,
57
+ width=width,
58
+ heads=heads,
59
+ init_scale=init_scale,
60
+ qkv_bias=qkv_bias,
61
+ flash=flash,
62
+ )
63
+
64
+ self.self_attn = Transformer(
65
+ device=device,
66
+ dtype=dtype,
67
+ n_ctx=num_latents,
68
+ width=width,
69
+ layers=layers,
70
+ heads=heads,
71
+ init_scale=init_scale,
72
+ qkv_bias=qkv_bias,
73
+ flash=flash,
74
+ use_checkpoint=False
75
+ )
76
+
77
+ if use_ln_post:
78
+ self.ln_post = nn.LayerNorm(width, dtype=dtype, device=device)
79
+ else:
80
+ self.ln_post = None
81
+
82
+ def _forward(self, pc, feats, img_feats=None):
83
+
84
+ # Args:
85
+ # pc (torch.FloatTensor): [B, N, 3]
86
+ # feats (torch.FloatTensor or None): [B, N, C]
87
+ # img_feats (torch.FloatTensor or None): [B, N, img_feat_dim]
88
+
89
+ bs = pc.shape[0]
90
+
91
+ parts = [self.fourier_embedder(pc)]
92
+ if feats is not None:
93
+ parts.append(feats)
94
+ if self.img_feat_dim > 0:
95
+ if img_feats is None:
96
+ raise ValueError("img_feats is required when img_feat_dim > 0")
97
+ parts.append(img_feats)
98
+ data = self.input_proj(torch.cat(parts, dim=-1))
99
+
100
+ query = repeat(self.query, "m c -> b m c", b=bs)
101
+ latents = self.cross_attn(query, data)
102
+ latents = self.self_attn(latents)
103
+
104
+ if self.ln_post is not None:
105
+ latents = self.ln_post(latents)
106
+
107
+ return latents, pc
108
+
109
+ def forward(
110
+ self,
111
+ pc: torch.FloatTensor,
112
+ feats: Optional[torch.FloatTensor] = None,
113
+ img_feats: Optional[torch.FloatTensor] = None,
114
+ ):
115
+
116
+ # Args:
117
+ # pc (torch.FloatTensor): [B, N, 3]
118
+ # feats (torch.FloatTensor or None): [B, N, C]
119
+ # img_feats (torch.FloatTensor or None): [B, N, img_feat_dim]
120
+
121
+ # NOTE: Michelangelo's custom CheckpointFunction.backward does
122
+ # `x.detach()` on every entry of ctx.input_tensors and cannot tolerate
123
+ # None values. So we MUST NOT pass `img_feats=None` into the checkpoint
124
+ # inputs tuple. Build the inputs based on which optional args are
125
+ # actually tensors.
126
+ inputs = [pc]
127
+ if feats is not None:
128
+ inputs.append(feats)
129
+ if img_feats is not None:
130
+ inputs.append(img_feats)
131
+
132
+ # _forward expects (pc, feats, img_feats). When some are None we cannot
133
+ # rely on positional defaults inside checkpoint, so wrap to restore them.
134
+ feats_is_none = feats is None
135
+ img_feats_is_none = img_feats is None
136
+
137
+ def _forward_wrapper(*args):
138
+ it = iter(args)
139
+ pc_arg = next(it)
140
+ feats_arg = None if feats_is_none else next(it)
141
+ img_feats_arg = None if img_feats_is_none else next(it)
142
+ return self._forward(pc_arg, feats_arg, img_feats_arg)
143
+
144
+ return checkpoint(_forward_wrapper, tuple(inputs), self.parameters(), self.use_checkpoint)
145
+
146
+
147
+ class CrossAttentionDecoder(nn.Module):
148
+
149
+ def __init__(self, *,
150
+ device: Optional[torch.device],
151
+ dtype: Optional[torch.dtype],
152
+ num_latents: int,
153
+ out_channels: int,
154
+ fourier_embedder: FourierEmbedder,
155
+ width: int,
156
+ heads: int,
157
+ init_scale: float = 0.25,
158
+ qkv_bias: bool = True,
159
+ flash: bool = False,
160
+ use_checkpoint: bool = False):
161
+
162
+ super().__init__()
163
+
164
+ self.use_checkpoint = use_checkpoint
165
+ self.fourier_embedder = fourier_embedder
166
+
167
+ self.query_proj = nn.Linear(self.fourier_embedder.out_dim, width, device=device, dtype=dtype)
168
+
169
+ self.cross_attn_decoder = ResidualCrossAttentionBlock(
170
+ device=device,
171
+ dtype=dtype,
172
+ n_data=num_latents,
173
+ width=width,
174
+ heads=heads,
175
+ init_scale=init_scale,
176
+ qkv_bias=qkv_bias,
177
+ flash=flash
178
+ )
179
+
180
+ self.ln_post = nn.LayerNorm(width, device=device, dtype=dtype)
181
+ self.output_proj = nn.Linear(width, out_channels, device=device, dtype=dtype)
182
+
183
+ def _forward(self, queries: torch.FloatTensor, latents: torch.FloatTensor):
184
+ queries = self.query_proj(self.fourier_embedder(queries))
185
+ x = self.cross_attn_decoder(queries, latents)
186
+ x = self.ln_post(x)
187
+ x = self.output_proj(x)
188
+ return x
189
+
190
+ def forward(self, queries: torch.FloatTensor, latents: torch.FloatTensor):
191
+ return checkpoint(self._forward, (queries, latents), self.parameters(), self.use_checkpoint)
192
+
193
+
194
+ class ShapeAsLatentPerceiver(ShapeAsLatentModule):
195
+ def __init__(self, *,
196
+ device: Optional[torch.device],
197
+ dtype: Optional[torch.dtype],
198
+ num_latents: int,
199
+ point_feats: int = 0,
200
+ embed_dim: int = 0,
201
+ num_freqs: int = 8,
202
+ include_pi: bool = True,
203
+ width: int,
204
+ heads: int,
205
+ num_encoder_layers: int,
206
+ num_decoder_layers: int,
207
+ init_scale: float = 0.25,
208
+ qkv_bias: bool = True,
209
+ flash: bool = False,
210
+ use_ln_post: bool = False,
211
+ use_checkpoint: bool = False,
212
+ img_feat_dim: int = 0):
213
+
214
+ super().__init__()
215
+
216
+ self.use_checkpoint = use_checkpoint
217
+
218
+ self.num_latents = num_latents
219
+ self.fourier_embedder = FourierEmbedder(num_freqs=num_freqs, include_pi=include_pi)
220
+ self.img_feat_dim = img_feat_dim
221
+
222
+ init_scale = init_scale * math.sqrt(1.0 / width)
223
+ self.encoder = CrossAttentionEncoder(
224
+ device=device,
225
+ dtype=dtype,
226
+ fourier_embedder=self.fourier_embedder,
227
+ num_latents=num_latents,
228
+ point_feats=point_feats,
229
+ width=width,
230
+ heads=heads,
231
+ layers=num_encoder_layers,
232
+ init_scale=init_scale,
233
+ qkv_bias=qkv_bias,
234
+ flash=flash,
235
+ use_ln_post=use_ln_post,
236
+ use_checkpoint=use_checkpoint,
237
+ img_feat_dim=img_feat_dim,
238
+ )
239
+
240
+ self.embed_dim = embed_dim
241
+ if embed_dim > 0:
242
+ # VAE embed
243
+ self.pre_kl = nn.Linear(width, embed_dim * 2, device=device, dtype=dtype)
244
+ self.post_kl = nn.Linear(embed_dim, width, device=device, dtype=dtype)
245
+ self.latent_shape = (num_latents, embed_dim)
246
+ else:
247
+ self.latent_shape = (num_latents, width)
248
+
249
+ self.transformer = Transformer(
250
+ device=device,
251
+ dtype=dtype,
252
+ n_ctx=num_latents,
253
+ width=width,
254
+ layers=num_decoder_layers,
255
+ heads=heads,
256
+ init_scale=init_scale,
257
+ qkv_bias=qkv_bias,
258
+ flash=flash,
259
+ use_checkpoint=use_checkpoint
260
+ )
261
+
262
+ # geometry decoder
263
+ self.geo_decoder = CrossAttentionDecoder(
264
+ device=device,
265
+ dtype=dtype,
266
+ fourier_embedder=self.fourier_embedder,
267
+ out_channels=1,
268
+ num_latents=num_latents,
269
+ width=width,
270
+ heads=heads,
271
+ init_scale=init_scale,
272
+ qkv_bias=qkv_bias,
273
+ flash=flash,
274
+ use_checkpoint=use_checkpoint
275
+ )
276
+
277
+ def encode(self,
278
+ pc: torch.FloatTensor,
279
+ feats: Optional[torch.FloatTensor] = None,
280
+ sample_posterior: bool = True,
281
+ img_feats: Optional[torch.FloatTensor] = None):
282
+
283
+
284
+ # Args:
285
+ # pc (torch.FloatTensor): [B, N, 3]
286
+ # feats (torch.FloatTensor or None): [B, N, C]
287
+ # sample_posterior (bool):
288
+
289
+ # Returns:
290
+ # latents (torch.FloatTensor)
291
+ # center_pos (torch.FloatTensor or None):
292
+ # posterior (DiagonalGaussianDistribution or None):
293
+
294
+
295
+ latents, center_pos = self.encoder(pc, feats, img_feats=img_feats)
296
+
297
+ posterior = None
298
+ if self.embed_dim > 0:
299
+ moments = self.pre_kl(latents)
300
+ posterior = DiagonalGaussianDistribution(moments, feat_dim=-1)
301
+
302
+ if sample_posterior:
303
+ latents = posterior.sample()
304
+ else:
305
+ latents = posterior.mode()
306
+
307
+ return latents, center_pos, posterior
308
+
309
+ def decode(self, latents: torch.FloatTensor):
310
+ latents = self.post_kl(latents)
311
+ return self.transformer(latents)
312
+
313
+ def query_geometry(self, queries: torch.FloatTensor, latents: torch.FloatTensor):
314
+ logits = self.geo_decoder(queries, latents).squeeze(-1)
315
+ return logits
316
+
317
+ def forward(self,
318
+ pc: torch.FloatTensor,
319
+ feats: torch.FloatTensor,
320
+ volume_queries: torch.FloatTensor,
321
+ sample_posterior: bool = True):
322
+
323
+ # Args:
324
+ # pc (torch.FloatTensor): [B, N, 3]
325
+ # feats (torch.FloatTensor or None): [B, N, C]
326
+ # volume_queries (torch.FloatTensor): [B, P, 3]
327
+ # sample_posterior (bool):
328
+
329
+ # Returns:
330
+ # logits (torch.FloatTensor): [B, P]
331
+ # center_pos (torch.FloatTensor): [B, M, 3]
332
+ # posterior (DiagonalGaussianDistribution or None).
333
+
334
+
335
+
336
+ latents, center_pos, posterior = self.encode(pc, feats, sample_posterior=sample_posterior)
337
+
338
+ latents = self.decode(latents)
339
+ logits = self.query_geometry(volume_queries, latents)
340
+
341
+ return logits, center_pos, posterior
342
+
343
+
344
+ class AlignedShapeLatentPerceiver(ShapeAsLatentPerceiver):
345
+
346
+ def __init__(self, *,
347
+ device: Optional[torch.device],
348
+ dtype: Optional[torch.dtype],
349
+ num_latents: int,
350
+ point_feats: int = 0,
351
+ embed_dim: int = 0,
352
+ num_freqs: int = 8,
353
+ include_pi: bool = True,
354
+ width: int,
355
+ heads: int,
356
+ num_encoder_layers: int,
357
+ num_decoder_layers: int,
358
+ init_scale: float = 0.25,
359
+ qkv_bias: bool = True,
360
+ flash: bool = False,
361
+ use_ln_post: bool = False,
362
+ use_checkpoint: bool = False,
363
+ img_feat_dim: int = 0):
364
+
365
+ super().__init__(
366
+ device=device,
367
+ dtype=dtype,
368
+ num_latents=1 + num_latents,
369
+ point_feats=point_feats,
370
+ embed_dim=embed_dim,
371
+ num_freqs=num_freqs,
372
+ include_pi=include_pi,
373
+ width=width,
374
+ heads=heads,
375
+ num_encoder_layers=num_encoder_layers,
376
+ num_decoder_layers=num_decoder_layers,
377
+ init_scale=init_scale,
378
+ qkv_bias=qkv_bias,
379
+ flash=flash,
380
+ use_ln_post=use_ln_post,
381
+ use_checkpoint=use_checkpoint,
382
+ img_feat_dim=img_feat_dim,
383
+ )
384
+
385
+ self.width = width
386
+
387
+ def encode(self,
388
+ pc: torch.FloatTensor,
389
+ feats: Optional[torch.FloatTensor] = None,
390
+ sample_posterior: bool = True,
391
+ img_feats: Optional[torch.FloatTensor] = None):
392
+
393
+ # Args:
394
+ # pc (torch.FloatTensor): [B, N, 3]
395
+ # feats (torch.FloatTensor or None): [B, N, c]
396
+ # sample_posterior (bool):
397
+
398
+ # Returns:
399
+ # shape_embed (torch.FloatTensor)
400
+ # kl_embed (torch.FloatTensor):
401
+ # posterior (DiagonalGaussianDistribution or None):
402
+
403
+
404
+ shape_embed, latents = self.encode_latents(pc, feats, img_feats=img_feats)
405
+ kl_embed, posterior = self.encode_kl_embed(latents, sample_posterior)
406
+
407
+ return shape_embed, kl_embed, posterior
408
+
409
+ def encode_latents(self,
410
+ pc: torch.FloatTensor,
411
+ feats: Optional[torch.FloatTensor] = None,
412
+ img_feats: Optional[torch.FloatTensor] = None):
413
+
414
+ x, _ = self.encoder(pc, feats, img_feats=img_feats)
415
+
416
+ shape_embed = x[:, 0]
417
+ latents = x[:, 1:]
418
+
419
+ return shape_embed, latents
420
+
421
+ def encode_kl_embed(self, latents: torch.FloatTensor, sample_posterior: bool = True):
422
+ posterior = None
423
+ if self.embed_dim > 0:
424
+ moments = self.pre_kl(latents)
425
+ posterior = DiagonalGaussianDistribution(moments, feat_dim=-1)
426
+
427
+ if sample_posterior:
428
+ kl_embed = posterior.sample()
429
+ else:
430
+ kl_embed = posterior.mode()
431
+ else:
432
+ kl_embed = latents
433
+
434
+ return kl_embed, posterior
435
+
436
+ def forward(self,
437
+ pc: torch.FloatTensor,
438
+ feats: torch.FloatTensor,
439
+ volume_queries: torch.FloatTensor,
440
+ sample_posterior: bool = True):
441
+
442
+ # Args:
443
+ # pc (torch.FloatTensor): [B, N, 3]
444
+ # feats (torch.FloatTensor or None): [B, N, C]
445
+ # volume_queries (torch.FloatTensor): [B, P, 3]
446
+ # sample_posterior (bool):
447
+
448
+ # Returns:
449
+ # shape_embed (torch.FloatTensor): [B, projection_dim]
450
+ # logits (torch.FloatTensor): [B, M]
451
+ # posterior (DiagonalGaussianDistribution or None).
452
+
453
+
454
+ shape_embed, kl_embed, posterior = self.encode(pc, feats, sample_posterior=sample_posterior)
455
+
456
+ latents = self.decode(kl_embed)
457
+ logits = self.query_geometry(volume_queries, latents)
458
+
459
+ return shape_embed, logits, posterior
mulres_infer/modules/miche/michelangelo/models/tsal/tsal_base.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import torch.nn as nn
4
+ from typing import Tuple, List, Optional
5
+
6
+ # Base class for output of Point to Mesh transformation
7
+ class Point2MeshOutput(object):
8
+ def __init__(self):
9
+ self.mesh_v = None # Vertices of the mesh
10
+ self.mesh_f = None # Faces of the mesh
11
+ self.center = None # Center of the mesh
12
+ self.pc = None # Point cloud data
13
+
14
+
15
+ # Base class for output of Latent to Mesh transformation
16
+ class Latent2MeshOutput(object):
17
+ def __init__(self):
18
+ self.mesh_v = None # Vertices of the mesh
19
+ self.mesh_f = None # Faces of the mesh
20
+
21
+
22
+ # Base class for output of Aligned Mesh transformation
23
+ class AlignedMeshOutput(object):
24
+ def __init__(self):
25
+ self.mesh_v = None # Vertices of the mesh
26
+ self.mesh_f = None # Faces of the mesh
27
+ self.surface = None # Surface data
28
+ self.image = None # Aligned image data
29
+ self.text: Optional[str] = None # Aligned text data
30
+ self.shape_text_similarity: Optional[float] = None # Similarity between shape and text
31
+ self.shape_image_similarity: Optional[float] = None # Similarity between shape and image
32
+
33
+
34
+ # Base class for Shape as Latent with Point to Mesh transformation module
35
+ class ShapeAsLatentPLModule(nn.Module):
36
+ latent_shape: Tuple[int] # Shape of the latent space
37
+
38
+ def encode(self, surface, *args, **kwargs):
39
+ raise NotImplementedError
40
+
41
+ def decode(self, z_q, *args, **kwargs):
42
+ raise NotImplementedError
43
+
44
+ def latent2mesh(self, latents, *args, **kwargs) -> List[Latent2MeshOutput]:
45
+ raise NotImplementedError
46
+
47
+ def point2mesh(self, *args, **kwargs) -> List[Point2MeshOutput]:
48
+ raise NotImplementedError
49
+
50
+
51
+ # Base class for Shape as Latent module
52
+ class ShapeAsLatentModule(nn.Module):
53
+ latent_shape: Tuple[int, int] # Shape of the latent space
54
+
55
+ def __init__(self, *args, **kwargs):
56
+ super().__init__()
57
+
58
+ def encode(self, *args, **kwargs):
59
+ raise NotImplementedError
60
+
61
+ def decode(self, *args, **kwargs):
62
+ raise NotImplementedError
63
+
64
+ def query_geometry(self, *args, **kwargs):
65
+ raise NotImplementedError
66
+
67
+
68
+ # Base class for Aligned Shape as Latent with Point to Mesh transformation module
69
+ class AlignedShapeAsLatentPLModule(nn.Module):
70
+ latent_shape: Tuple[int] # Shape of the latent space
71
+
72
+ def set_shape_model_only(self):
73
+ raise NotImplementedError
74
+
75
+ def encode(self, surface, *args, **kwargs):
76
+ raise NotImplementedError
77
+
78
+ def decode(self, z_q, *args, **kwargs):
79
+ raise NotImplementedError
80
+
81
+ def latent2mesh(self, latents, *args, **kwargs) -> List[Latent2MeshOutput]:
82
+ raise NotImplementedError
83
+
84
+ def point2mesh(self, *args, **kwargs) -> List[Point2MeshOutput]:
85
+ raise NotImplementedError
86
+
87
+
88
+ # Base class for Aligned Shape as Latent module
89
+ class AlignedShapeAsLatentModule(nn.Module):
90
+ shape_model: ShapeAsLatentModule # Shape model module
91
+ latent_shape: Tuple[int, int] # Shape of the latent space
92
+
93
+
94
+ def __init__(self, *args, **kwargs):
95
+ super().__init__()
96
+
97
+ def set_shape_model_only(self):
98
+ raise NotImplementedError
99
+
100
+ def encode_image_embed(self, *args, **kwargs):
101
+ raise NotImplementedError
102
+
103
+ def encode_text_embed(self, *args, **kwargs):
104
+ raise NotImplementedError
105
+
106
+ def encode_shape_embed(self, *args, **kwargs):
107
+ raise NotImplementedError
108
+
109
+ # Base class for Textured Shape as Latent module
110
+ class TexturedShapeAsLatentModule(nn.Module):
111
+
112
+ def __init__(self, *args, **kwargs):
113
+ super().__init__()
114
+
115
+ def encode(self, *args, **kwargs):
116
+ raise NotImplementedError
117
+
118
+ def decode(self, *args, **kwargs):
119
+ raise NotImplementedError
120
+
121
+ def query_geometry(self, *args, **kwargs):
122
+ raise NotImplementedError
123
+
124
+ def query_color(self, *args, **kwargs):
125
+ raise NotImplementedError
mulres_infer/modules/miche/michelangelo/utils/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from .misc import instantiate_from_config
mulres_infer/modules/miche/michelangelo/utils/misc.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import importlib
4
+
5
+ import torch
6
+ import torch.distributed as dist
7
+
8
+
9
+
10
+ def get_obj_from_str(string, reload=False):
11
+ module, cls = string.rsplit(".", 1)
12
+ if reload:
13
+ module_imp = importlib.import_module(module)
14
+ importlib.reload(module_imp)
15
+ return getattr(importlib.import_module(module, package=None), cls)
16
+
17
+
18
+ def get_obj_from_config(config):
19
+ if "target" not in config:
20
+ raise KeyError("Expected key `target` to instantiate.")
21
+
22
+ return get_obj_from_str(config["target"])
23
+
24
+
25
+ def instantiate_from_config(config, **kwargs):
26
+ if "target" not in config:
27
+ raise KeyError("Expected key `target` to instantiate.")
28
+
29
+ cls = get_obj_from_str(config["target"])
30
+
31
+ params = config.get("params", dict())
32
+ # params.update(kwargs)
33
+ # instance = cls(**params)
34
+ kwargs.update(params)
35
+ instance = cls(**kwargs)
36
+
37
+ return instance
38
+
39
+
40
+ def is_dist_avail_and_initialized():
41
+ if not dist.is_available():
42
+ return False
43
+ if not dist.is_initialized():
44
+ return False
45
+ return True
46
+
47
+
48
+ def get_rank():
49
+ if not is_dist_avail_and_initialized():
50
+ return 0
51
+ return dist.get_rank()
52
+
53
+
54
+ def get_world_size():
55
+ if not is_dist_avail_and_initialized():
56
+ return 1
57
+ return dist.get_world_size()
58
+
59
+
60
+ def all_gather_batch(tensors):
61
+ """
62
+ Performs all_gather operation on the provided tensors.
63
+ """
64
+ # Queue the gathered tensors
65
+ world_size = get_world_size()
66
+ # There is no need for reduction in the single-proc case
67
+ if world_size == 1:
68
+ return tensors
69
+ tensor_list = []
70
+ output_tensor = []
71
+ for tensor in tensors:
72
+ tensor_all = [torch.ones_like(tensor) for _ in range(world_size)]
73
+ dist.all_gather(
74
+ tensor_all,
75
+ tensor,
76
+ async_op=False # performance opt
77
+ )
78
+
79
+ tensor_list.append(tensor_all)
80
+
81
+ for tensor_all in tensor_list:
82
+ output_tensor.append(torch.cat(tensor_all, dim=0))
83
+ return output_tensor
mulres_infer/modules/miche/shapevae-256.yaml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model:
2
+ target: modules.miche.michelangelo.models.tsal.asl_pl_module.AlignedShapeAsLatentPLModule
3
+ params:
4
+ shape_module_cfg:
5
+ target: modules.miche.michelangelo.models.tsal.sal_perceiver.AlignedShapeLatentPerceiver
6
+ params:
7
+ num_latents: 256
8
+ embed_dim: 64
9
+ point_feats: 3 # normal
10
+ num_freqs: 8
11
+ include_pi: false
12
+ heads: 12
13
+ width: 768
14
+ num_encoder_layers: 8
15
+ num_decoder_layers: 16
16
+ use_ln_post: true
17
+ init_scale: 0.25
18
+ qkv_bias: false
19
+ use_checkpoint: true
20
+ aligned_module_cfg:
21
+ target: modules.miche.michelangelo.models.tsal.clip_asl_module.CLIPAlignedShapeAsLatentModule
22
+ params:
23
+ clip_model_version: "./checkpoints/clip/clip-vit-large-patch14"
24
+
25
+ loss_cfg:
26
+ target: modules.miche.michelangelo.models.tsal.loss.ContrastKLNearFar
27
+ params:
28
+ contrast_weight: 0.1
29
+ near_weight: 0.1
30
+ kl_weight: 0.001
31
+
32
+ optimizer_cfg:
33
+ optimizer:
34
+ target: torch.optim.AdamW
35
+ params:
36
+ betas: [0.9, 0.99]
37
+ eps: 1.e-6
38
+ weight_decay: 1.e-2
39
+
40
+ scheduler:
41
+ target: modules.miche.michelangelo.utils.trainings.lr_scheduler.LambdaWarmUpCosineFactorScheduler
42
+ params:
43
+ warm_up_steps: 5000
44
+ f_start: 1.e-6
45
+ f_min: 1.e-3
46
+ f_max: 1.0
mulres_infer/modules/octree_codec.py ADDED
@@ -0,0 +1,981 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fix3 tokenizer(已融合 numba 加速实现, 原 *_fast.py 并入本文件)。
2
+
3
+ encode_mesh / decode_tokens 走 numpy 向量化 + numba 枚举, 比纯 python 版快约 5x。
4
+ quantized_mesh 与 decode 输出和纯 python 版完全一致; token 的 face 块集合一致,
5
+ 仅少数平局块顺序不同(本实现补了 (f2.y,f2.x) 决定性平局键, 比原版更可复现)。
6
+
7
+ 原文件头:
8
+
9
+ fix3: 连接矩阵改为「octant 绝对原位 8x8 + 因子分解(按行)」编码,保留 F。
10
+
11
+ 相对 fix2 的核心变化
12
+ --------------------
13
+ 1. 不再按 F 排序、不再受 max_children 限制。每个连接矩阵(intra / inter)统一是
14
+ 8x8 的「octant 绝对原位」矩阵:行/列 = octant 0..7 的固定语义。
15
+ → 天然 mc=8 无损,"超标 / 合并 / 退化分摊" 这套概念全部消失。
16
+ 2. 8x8 矩阵不再编码成单个 token(那是 2^64 词表),而是按行拆成 8 个「行子码」,
17
+ 每个行子码 = 该行 8 个 bit ∈ [0, 256)。intra / inter 各 8 个子码。
18
+ 3. **逻辑序列长度不变**:每个 face 仍是 18 个「位置」(9 coord + 3 F + 3 intra + 3 inter),
19
+ 只是 intra / inter 这 6 个位置每个携带 8 个子码(给因子分解头:同一位置 8 个并行 256-way 头)。
20
+ coord / F 仍是单子码位置。tokenizer.encode 返回 List[List[int]]:每个内层 list = 一个位置的子码。
21
+ 4. 保留 F(标识 active octant)。重建时用 F mask 掉非 active 行列(M &= F⊗F),消除歧义。
22
+
23
+ 词表(极小):pad(1) + coord(resolution) + F(256) + intra_row(256) + inter_row(256)。
24
+ """
25
+ import numpy as np
26
+ import trimesh
27
+ from tqdm import tqdm
28
+ import os
29
+ import sys
30
+ import glob
31
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
32
+ from pathlib import Path
33
+ from utils.data_utils import undiscretize, discretize, normalize, clean_mesh
34
+ from utils.mesh_metric import compute_mesh_metrics
35
+ from modules.fast_octree_level import _triangles_csr
36
+ import itertools
37
+ from dataclasses import dataclass
38
+
39
+ NUM_OCTANTS = 8
40
+
41
+ # 预计算 24 个节点(3 槽位 x 8 octant)中选 3 个的所有组合,用于向量化找三角形
42
+ _TRI_INDICES = np.array(list(itertools.combinations(range(24), 3)))
43
+ _IDX_I = _TRI_INDICES[:, 0]
44
+ _IDX_J = _TRI_INDICES[:, 1]
45
+ _IDX_K = _TRI_INDICES[:, 2]
46
+
47
+
48
+ # ==========================================
49
+ # Tokenizer:octant 原位 + 按行子码
50
+ # ==========================================
51
+ class DynamicMeshTokenizer:
52
+ """
53
+ encode 输出格式(fine):List[List[int]],每个内层 list 是一个「逻辑位置」的子码。
54
+ 每 face 18 个位置: [v0x][v0y][v0z][v1x][v1y][v1z][v2x][v2y][v2z] (9 个 coord, 各 1 子码)
55
+ [F0][F1][F2] (3 个 F, 各 1 子码)
56
+ [M00 的 8 行][M11..][M22..] (3 个 intra, 各 8 子码)
57
+ [M01 的 8 行][M02..][M12..] (3 个 inter, 各 8 子码)
58
+ coord / F 位置长度 1;intra / inter 位置长度 8(行子码)。逻辑序列长度 = 18/face,与 fix2 相同。
59
+ """
60
+ def __init__(self, resolution, max_children=NUM_OCTANTS):
61
+ self.resolution = resolution
62
+ self.max_children = NUM_OCTANTS # octant 原位下固定为 8;保留字段仅为兼容 config
63
+
64
+ self.pad_id = 0
65
+ self.coord_offset = 1
66
+ self.f_offset = self.coord_offset + self.resolution
67
+ self.row_vocab = 1 << NUM_OCTANTS # 2^8 = 256
68
+ self.f_vocab = 1 << NUM_OCTANTS # F 也是 8bit
69
+ self.intra_row_offset = self.f_offset + self.f_vocab
70
+ self.inter_row_offset = self.intra_row_offset + self.row_vocab
71
+ self.vocab_size = self.inter_row_offset + self.row_vocab
72
+
73
+ # ---- bit <-> int ----
74
+ def _row_to_int(self, bits):
75
+ v = 0
76
+ for i, b in enumerate(bits):
77
+ if b:
78
+ v |= (1 << i)
79
+ return v
80
+
81
+ def _int_to_row(self, val):
82
+ return [(val >> i) & 1 for i in range(NUM_OCTANTS)]
83
+
84
+ def _mat_to_rows(self, M, offset):
85
+ return [self._row_to_int(M[r]) + offset for r in range(NUM_OCTANTS)]
86
+
87
+ def _rows_to_mat(self, rows, offset):
88
+ return [self._int_to_row(rows[r] - offset) for r in range(NUM_OCTANTS)]
89
+
90
+ # ------------------------------------
91
+ # 细分网格 (Fine Mesh) —— 扁平 1D int 布局,每 face 60 个 token:
92
+ # 9 coord + 3 F + 6 矩阵 x 8 行子码
93
+ # 顺序: [coord(9)] [F0 F1 F2] [M00 M11 M22 各 8 行] [M01 M02 M12 各 8 行]
94
+ # F: 每个 1 个 token;intra/inter: 每个矩阵按行拆成 8 个行子码(每行 8bit)
95
+ # 所有真实 token 都带 offset(>=1),0 仅作 trailing pad,所以 F 不需要复制避坑。
96
+ # 与现有 dataset/collate(1D int, pad_id=0) 兼容;模型侧整理成 9 个 refine 位置/face
97
+ # (F 位置 1 子码、M 位置 8 行子码 sum 成一个 embedding token),逻辑序列 18/face 不变。
98
+ # ------------------------------------
99
+ TOKENS_PER_FACE = 9 + 3 + 6 * NUM_OCTANTS # 60
100
+ REFINE_POS_PER_FACE = 9 # 模型逻辑: 3 F + 3 intra + 3 inter
101
+
102
+ def encode(self, faces_data, use_hourglass=True):
103
+ seq = []
104
+ for face in faces_data:
105
+ v0, v1, v2, F0, F1, F2, M00, M11, M22, M01, M02, M12 = face
106
+ for v in (v0, v1, v2):
107
+ seq.extend([v[0] + self.coord_offset, v[1] + self.coord_offset, v[2] + self.coord_offset])
108
+ for F in (F0, F1, F2):
109
+ seq.append(self._row_to_int(F) + self.f_offset) # F: 1 个 token
110
+ for M in (M00, M11, M22):
111
+ seq.extend(self._mat_to_rows(M, self.intra_row_offset)) # 各 8 行
112
+ for M in (M01, M02, M12):
113
+ seq.extend(self._mat_to_rows(M, self.inter_row_offset)) # 各 8 行
114
+ return seq
115
+
116
+ def decode(self, seq):
117
+ faces_data = []
118
+ B = self.TOKENS_PER_FACE # 60
119
+ clean_seq = [t for t in seq if t != self.pad_id]
120
+ co = self.coord_offset
121
+ for i in range(0, len(clean_seq) - B + 1, B):
122
+ ch = clean_seq[i:i + B]
123
+ cv = [ch[k] - co for k in range(9)]
124
+ v0 = (cv[0], cv[1], cv[2]); v1 = (cv[3], cv[4], cv[5]); v2 = (cv[6], cv[7], cv[8])
125
+ F0 = self._int_to_row(ch[9] - self.f_offset)
126
+ F1 = self._int_to_row(ch[10] - self.f_offset)
127
+ F2 = self._int_to_row(ch[11] - self.f_offset)
128
+ M00 = self._rows_to_mat(ch[12:20], self.intra_row_offset)
129
+ M11 = self._rows_to_mat(ch[20:28], self.intra_row_offset)
130
+ M22 = self._rows_to_mat(ch[28:36], self.intra_row_offset)
131
+ M01 = self._rows_to_mat(ch[36:44], self.inter_row_offset)
132
+ M02 = self._rows_to_mat(ch[44:52], self.inter_row_offset)
133
+ M12 = self._rows_to_mat(ch[52:60], self.inter_row_offset)
134
+ faces_data.append((v0, v1, v2, F0, F1, F2, M00, M11, M22, M01, M02, M12))
135
+ return faces_data
136
+
137
+ # ------------------------------------
138
+ # 粗糙网格 (Raw Mesh):仅顶点坐标,每 face 9 个单子码位置
139
+ # ------------------------------------
140
+ def encode_raw(self, raw_faces_data, use_hourglass=True):
141
+ seq = []
142
+ for face in raw_faces_data:
143
+ v0, v1, v2 = face
144
+ for v in (v0, v1, v2):
145
+ seq.append([v[0] + self.coord_offset])
146
+ seq.append([v[1] + self.coord_offset])
147
+ seq.append([v[2] + self.coord_offset])
148
+ return seq
149
+
150
+ def decode_raw(self, seq):
151
+ faces_data = []
152
+ clean_seq = [p for p in seq if not (len(p) == 1 and p[0] == self.pad_id)]
153
+ for i in range(0, len(clean_seq) - 9 + 1, 9):
154
+ chunk = clean_seq[i:i + 9]
155
+ if len(chunk) < 9:
156
+ break
157
+ cv = [chunk[k][0] - self.coord_offset for k in range(9)]
158
+ faces_data.append(((cv[0], cv[1], cv[2]), (cv[3], cv[4], cv[5]), (cv[6], cv[7], cv[8])))
159
+ return faces_data
160
+
161
+
162
+ # ==========================================
163
+ # Builder:直接构建 octant 原位 8x8 矩阵
164
+ # ==========================================
165
+ class OctreeFaceTokenBuilder:
166
+ def __init__(self, tree, max_children=NUM_OCTANTS):
167
+ self.tree = tree
168
+ self.max_children = NUM_OCTANTS # octant 原位固定 8
169
+
170
+ def get_level_info(self, depth):
171
+ nodes_at_depth = {}
172
+
173
+ def collect_nodes(current, target_depth):
174
+ if current.depth == target_depth:
175
+ nodes_at_depth[current.coord] = current
176
+ return
177
+ for child in current.children:
178
+ if child:
179
+ collect_nodes(child, target_depth)
180
+
181
+ collect_nodes(self.tree.root, depth)
182
+
183
+ coords = sorted(nodes_at_depth.keys(), key=lambda x: (x[2], x[1], x[0]))
184
+ coord_to_idx = {c: i for i, c in enumerate(coords)}
185
+
186
+ adj = {i: set() for i in range(len(coords))}
187
+ for i, c in enumerate(coords):
188
+ node = nodes_at_depth[c]
189
+ all_neighbors = node.intra_neighbors | node.inter_neighbors
190
+ for neighbor_coord in all_neighbors:
191
+ if neighbor_coord in coord_to_idx:
192
+ j = coord_to_idx[neighbor_coord]
193
+ if i < j:
194
+ adj[i].add(j)
195
+
196
+ raw_faces_set = set()
197
+ covered_edges = set()
198
+
199
+ # 1. 3-clique (三角形)
200
+ for u in range(len(coords)):
201
+ for v in adj[u]:
202
+ common_neighbors = adj[u] & adj[v]
203
+ for w in common_neighbors:
204
+ face_coords = tuple(sorted([coords[u], coords[v], coords[w]]))
205
+ raw_faces_set.add(face_coords)
206
+ covered_edges.add(tuple(sorted([face_coords[0], face_coords[1]])))
207
+ covered_edges.add(tuple(sorted([face_coords[1], face_coords[2]])))
208
+ covered_edges.add(tuple(sorted([face_coords[0], face_coords[2]])))
209
+
210
+ # 2. 未被三角形覆盖的独立边 -> 退化面片 (A, B, B)
211
+ for u in range(len(coords)):
212
+ for v in adj[u]:
213
+ edge = tuple(sorted([coords[u], coords[v]]))
214
+ if edge not in covered_edges:
215
+ raw_faces_set.add((edge[0], edge[1], edge[1]))
216
+
217
+ # 3. 孤立顶点 -> (A, A, A)
218
+ for u in range(len(coords)):
219
+ has_edges = len(adj[u]) > 0 or any(u in adj[i] for i in range(u))
220
+ if not has_edges:
221
+ raw_faces_set.add((coords[u], coords[u], coords[u]))
222
+
223
+ raw_faces_at_depth = list(raw_faces_set)
224
+ raw_faces_at_depth.sort(key=lambda f: (f[0][2], f[0][1], f[0][0],
225
+ f[1][2], f[1][1], f[1][0],
226
+ f[2][2]))
227
+ return raw_faces_at_depth, nodes_at_depth
228
+
229
+ def _active_octants(self, coord, n, nodes_map):
230
+ """该节点哪些 octant 子节点存在(occupancy)。
231
+ 用 children 占据而非"有连接"标记 —— 因为「有连接 ⟹ 必有子节点」,occupancy 是
232
+ "有连接"的超集,且正好等于 raw 的 get_level_info 收集的 depth+1 节点集;
233
+ 这样「有几何但无边」的孤立子节点也会被 F 标记,fine 重构能补回孤立点,与 raw 完全一致。
234
+ M(连接矩阵)不变,故三角形/边重构不受影响。"""
235
+ return set(i for i in range(NUM_OCTANTS) if n.children[i] is not None)
236
+
237
+ def encode_level_tokens(self, depth):
238
+ raw_faces, nodes_map = self.get_level_info(depth)
239
+ empty = np.zeros((NUM_OCTANTS, NUM_OCTANTS), dtype=np.int8)
240
+
241
+ # F:每个节点的 active octant 掩码 (8bit)
242
+ node_F = {}
243
+ for coord, n in nodes_map.items():
244
+ active = self._active_octants(coord, n, nodes_map)
245
+ node_F[coord] = [1 if i in active else 0 for i in range(NUM_OCTANTS)]
246
+
247
+ intra_cache = {}
248
+
249
+ def intra8(coord):
250
+ if coord not in intra_cache:
251
+ M = (nodes_map[coord].intra_matrix != 0).astype(np.int8) # 8x8 octant 原位、对称
252
+ intra_cache[coord] = M.tolist()
253
+ return intra_cache[coord]
254
+
255
+ def inter8(a, b):
256
+ if a == b:
257
+ return intra8(a)
258
+ na = nodes_map[a]
259
+ M = na.inter_matrices.get(b)
260
+ if M is None:
261
+ nb = nodes_map[b]
262
+ Mt = nb.inter_matrices.get(a)
263
+ M = Mt.T if Mt is not None else empty
264
+ return (np.asarray(M) != 0).astype(np.int8).tolist()
265
+
266
+ raw_tokens = []
267
+ output_tokens = []
268
+ for face in raw_faces:
269
+ v0_c, v1_c, v2_c = face
270
+ raw_tokens.append([v0_c, v1_c, v2_c])
271
+
272
+ F0, F1, F2 = node_F[v0_c], node_F[v1_c], node_F[v2_c]
273
+ M00, M11, M22 = intra8(v0_c), intra8(v1_c), intra8(v2_c)
274
+ M01 = inter8(v0_c, v1_c)
275
+ M02 = inter8(v0_c, v2_c)
276
+ M12 = inter8(v1_c, v2_c)
277
+
278
+ output_tokens.append((v0_c, v1_c, v2_c, F0, F1, F2, M00, M11, M22, M01, M02, M12))
279
+ return raw_tokens, output_tokens
280
+
281
+
282
+ class OctreeNode:
283
+ def __init__(self, x, y, z, depth):
284
+ self.coord = (x, y, z)
285
+ self.depth = depth
286
+ self.children = [None] * 8
287
+ self.intra_neighbors = set()
288
+ self.inter_neighbors = set()
289
+ self.intra_matrix = np.zeros((8, 8), dtype=np.int8)
290
+ self.inter_matrices = {}
291
+
292
+
293
+ class MatrixOctree:
294
+ def __init__(self, max_depth=7):
295
+ self.max_depth = max_depth
296
+ self.root = None
297
+
298
+ def _get_parent_coord(self, coord):
299
+ return (coord[0] >> 1, coord[1] >> 1, coord[2] >> 1)
300
+
301
+ def _get_local_index(self, coord):
302
+ return ((coord[2] & 1) << 2) | ((coord[1] & 1) << 1) | (coord[0] & 1)
303
+
304
+ def build(self, vertices, faces):
305
+ resolution = 2 ** self.max_depth
306
+ quantized = discretize(vertices, num_discrete=resolution)
307
+
308
+ leaf_nodes_map = {}
309
+ for i in range(len(vertices)):
310
+ coord = tuple(quantized[i])
311
+ if coord not in leaf_nodes_map:
312
+ leaf_nodes_map[coord] = OctreeNode(coord[0], coord[1], coord[2], self.max_depth)
313
+
314
+ for face in faces:
315
+ coords = [tuple(quantized[i]) for i in face]
316
+ for i in range(3):
317
+ u, v = coords[i], coords[(i + 1) % 3]
318
+ if u == v:
319
+ continue
320
+ parent_u = self._get_parent_coord(u)
321
+ parent_v = self._get_parent_coord(v)
322
+ node_u = leaf_nodes_map[u]
323
+ node_v = leaf_nodes_map[v]
324
+ if parent_u == parent_v:
325
+ node_u.intra_neighbors.add(v)
326
+ node_v.intra_neighbors.add(u)
327
+ else:
328
+ node_u.inter_neighbors.add(v)
329
+ node_v.inter_neighbors.add(u)
330
+
331
+ current_layer_nodes = list(leaf_nodes_map.values())
332
+
333
+ for d in range(self.max_depth, 0, -1):
334
+ parent_layer_map = {}
335
+ for child in current_layer_nodes:
336
+ parent_coord = self._get_parent_coord(child.coord)
337
+ if parent_coord not in parent_layer_map:
338
+ parent_layer_map[parent_coord] = OctreeNode(*parent_coord, d - 1)
339
+ parent_node = parent_layer_map[parent_coord]
340
+ idx = self._get_local_index(child.coord)
341
+ parent_node.children[idx] = child
342
+
343
+ for child in current_layer_nodes:
344
+ parent_coord = self._get_parent_coord(child.coord)
345
+ parent_node = parent_layer_map[parent_coord]
346
+ child_idx = self._get_local_index(child.coord)
347
+
348
+ for sibling_coord in child.intra_neighbors:
349
+ sibling_idx = self._get_local_index(sibling_coord)
350
+ parent_node.intra_matrix[child_idx, sibling_idx] = 1
351
+
352
+ for neighbor_coord in child.inter_neighbors:
353
+ neighbor_parent_coord = self._get_parent_coord(neighbor_coord)
354
+ if neighbor_parent_coord not in parent_node.inter_matrices:
355
+ parent_node.inter_matrices[neighbor_parent_coord] = np.zeros((8, 8), dtype=np.int8)
356
+ neighbor_child_idx = self._get_local_index(neighbor_coord)
357
+ parent_node.inter_matrices[neighbor_parent_coord][child_idx, neighbor_child_idx] = 1
358
+
359
+ grandparent_p = self._get_parent_coord(parent_coord)
360
+ grandparent_n = self._get_parent_coord(neighbor_parent_coord)
361
+ if grandparent_p == grandparent_n:
362
+ parent_node.intra_neighbors.add(neighbor_parent_coord)
363
+ else:
364
+ parent_node.inter_neighbors.add(neighbor_parent_coord)
365
+
366
+ current_layer_nodes = list(parent_layer_map.values())
367
+ if current_layer_nodes:
368
+ self.root = current_layer_nodes[0]
369
+
370
+
371
+ def reconstruct_next_level_optimized(output_tokens, resolution, max_children=NUM_OCTANTS):
372
+ """octant 绝对原位重建:M 已是 8x8、行列=octant;用 F mask 掉非 active 行列。"""
373
+ indices = np.arange(8)
374
+ offsets = np.stack([indices & 1, (indices >> 1) & 1, (indices >> 2) & 1], axis=1).astype(np.int32)
375
+
376
+ tri_blocks = [] # 逐 face 向量化收集的三角形 (N,3,3),最后统一去重
377
+ edge_keys = set() # 全局所有边: canonical (key_a, key_b)
378
+ tri_edge_keys = set() # 被三角形覆盖的边(全局)
379
+ active_keys = set() # 全局 active 节点 key
380
+ key_to_coord = {} # key(int 三元组) -> np.int32 坐标(3,)
381
+
382
+ for f_token in output_tokens:
383
+ v0_c, v1_c, v2_c, F0, F1, F2, M00, M11, M22, M01, M02, M12 = f_token
384
+ fa = [np.asarray(F0, dtype=bool), np.asarray(F1, dtype=bool), np.asarray(F2, dtype=bool)]
385
+
386
+ def intra(F, M):
387
+ return np.asarray(M, dtype=bool) & np.outer(F, F)
388
+
389
+ def inter(Fa, Fb, M):
390
+ return np.asarray(M, dtype=bool) & np.outer(Fa, Fb)
391
+
392
+ in0 = intra(fa[0], M00); in1 = intra(fa[1], M11); in2 = intra(fa[2], M22)
393
+ m01 = inter(fa[0], fa[1], M01); m02 = inter(fa[0], fa[2], M02); m12 = inter(fa[1], fa[2], M12)
394
+
395
+ A = np.zeros((24, 24), dtype=bool)
396
+ A[0:8, 0:8] = in0 | in0.T
397
+ A[8:16, 8:16] = in1 | in1.T
398
+ A[16:24, 16:24] = in2 | in2.T
399
+ A[0:8, 8:16] = m01; A[8:16, 0:8] = m01.T
400
+ A[0:8, 16:24] = m02; A[16:24, 0:8] = m02.T
401
+ A[8:16, 16:24] = m12; A[16:24, 8:16] = m12.T
402
+
403
+ # 24 个节点(slot p, octant c)的世界坐标 = v_parents[p]*2 + offset[c]
404
+ v_parents_arr = np.stack([np.array(v0_c[:3], dtype=np.int32),
405
+ np.array(v1_c[:3], dtype=np.int32),
406
+ np.array(v2_c[:3], dtype=np.int32)])
407
+ node_coords = v_parents_arr[np.arange(24) // 8] * 2 + offsets[np.arange(24) % 8] # (24,3)
408
+ keys = [tuple(int(x) for x in node_coords[i]) for i in range(24)]
409
+ for i in range(24):
410
+ if keys[i] not in key_to_coord:
411
+ key_to_coord[keys[i]] = node_coords[i]
412
+
413
+ # 三角形(逐 face 向量化);同时把三角形的边记入全局 covered 集合
414
+ valid_mask = A[_IDX_I, _IDX_J] & A[_IDX_J, _IDX_K] & A[_IDX_I, _IDX_K]
415
+ tris = _TRI_INDICES[valid_mask]
416
+ if len(tris) > 0:
417
+ p_idx = tris // 8
418
+ c_idx = tris % 8
419
+ tri_blocks.append(v_parents_arr[p_idx] * 2 + offsets[c_idx])
420
+ for i, j, k in tris:
421
+ ka, kb, kc = sorted((keys[int(i)], keys[int(j)], keys[int(k)]))
422
+ tri_edge_keys.add((ka, kb)); tri_edge_keys.add((kb, kc)); tri_edge_keys.add((ka, kc))
423
+
424
+ # 全局收集所有边
425
+ ei, ej = np.where(np.triu(A, 1))
426
+ for i, j in zip(ei.tolist(), ej.tolist()):
427
+ ki, kj = keys[i], keys[j]
428
+ edge_keys.add((ki, kj) if ki <= kj else (kj, ki))
429
+
430
+ # 全局收集 active 节点
431
+ for i in np.where(np.concatenate(fa))[0].tolist():
432
+ active_keys.add(keys[i])
433
+
434
+ all_blocks = list(tri_blocks)
435
+
436
+ # 未被任何三角形覆盖的边 -> 退化面 (A, B, B) [全局判断,与 raw 的 get_level_info 一致]
437
+ for a, b in (edge_keys - tri_edge_keys):
438
+ all_blocks.append(np.stack([key_to_coord[a], key_to_coord[b], key_to_coord[b]])[None])
439
+
440
+ # 孤立的 active 节点(全局无任何边) -> 点 (A, A, A)
441
+ edge_nodes = set()
442
+ for a, b in edge_keys:
443
+ edge_nodes.add(a); edge_nodes.add(b)
444
+ for a in (active_keys - edge_nodes):
445
+ c = key_to_coord[a]
446
+ all_blocks.append(np.stack([c, c, c])[None])
447
+
448
+ if not all_blocks:
449
+ return np.array([]), np.array([])
450
+
451
+ flat_v = np.concatenate(all_blocks, axis=0).reshape(-1, 3)
452
+ V, inv = np.unique(flat_v, axis=0, return_inverse=True)
453
+ F_raw = inv.reshape(-1, 3)
454
+ F_sorted = np.sort(F_raw, axis=1)
455
+ _, unique_face_indices = np.unique(F_sorted, axis=0, return_index=True)
456
+ F = F_raw[unique_face_indices]
457
+ V = undiscretize(V, num_discrete=resolution)
458
+ return V, F
459
+
460
+
461
+ def reconstruct_base_mesh(output_tokens, resolution):
462
+ all_face_coords = []
463
+ for f_token in output_tokens:
464
+ v0_c, v1_c, v2_c = f_token
465
+ c0 = np.array(v0_c[:3], dtype=np.int32)
466
+ c1 = np.array(v1_c[:3], dtype=np.int32)
467
+ c2 = np.array(v2_c[:3], dtype=np.int32)
468
+ all_face_coords.append(np.stack([c0, c1, c2]))
469
+
470
+ if not all_face_coords:
471
+ return np.array([]), np.array([])
472
+
473
+ flat_v = np.concatenate(all_face_coords, axis=0).reshape(-1, 3)
474
+ V, inv = np.unique(flat_v, axis=0, return_inverse=True)
475
+ F_raw = inv.reshape(-1, 3)
476
+ F_sorted = np.sort(F_raw, axis=1)
477
+ _, unique_face_indices = np.unique(F_sorted, axis=0, return_index=True)
478
+ F = F_raw[unique_face_indices]
479
+ V = undiscretize(V, num_discrete=resolution)
480
+ return V, F
481
+
482
+
483
+ class MeshCodec:
484
+ def __init__(self, args):
485
+ self.max_depth = args.max_depth
486
+ self.max_resolution = args.max_resolution
487
+ self.use_hourglass = args.use_hourglass
488
+ self.max_children = NUM_OCTANTS # octant 原位固定 8(忽略 config 的 max_children)
489
+
490
+ self.tokenizer = DynamicMeshTokenizer(resolution=self.max_resolution)
491
+
492
+ def encode_mesh(self, mesh: trimesh.Trimesh, target_depth: int, corrupt=None) -> tuple:
493
+ if isinstance(mesh, trimesh.Scene):
494
+ mesh = mesh.dump(concatenate=True)
495
+ V = np.asarray(mesh.vertices, dtype=np.float64)
496
+
497
+ res = 2 ** (target_depth + 1)
498
+ v_restored = undiscretize(discretize(V, num_discrete=res), num_discrete=res)
499
+ quantized_mesh = trimesh.Trimesh(vertices=v_restored, faces=mesh.faces, process=False)
500
+
501
+ # corrupt(仅训练时由 dataset 传入)= 2a 暴露偏差腐蚀; None 时与原版逐位一致(推理不受影响)
502
+ raw_seq, seq = _fast_fix3_level_tokens(V, mesh.faces, target_depth, self.tokenizer, corrupt=corrupt)
503
+ return raw_seq, seq, quantized_mesh
504
+
505
+
506
+ def decode_tokens(self, token_sequence: list, target_depth: int):
507
+ if os.environ.get("FIX3_FAST_DECODE", "1") != "1":
508
+ return super().decode_tokens(token_sequence, target_depth)
509
+ if token_sequence is None or len(token_sequence) == 0:
510
+ return None
511
+ return _fast_decode_tokens(token_sequence, target_depth, self.tokenizer)
512
+
513
+
514
+ def decode_raw_tokens(self, raw_token_sequence: list, target_depth: int) -> trimesh.Trimesh:
515
+ if not raw_token_sequence:
516
+ return None
517
+ resolution = 2 ** (target_depth)
518
+ raw_t = self.tokenizer.decode_raw(raw_token_sequence)
519
+ v, f = reconstruct_base_mesh(raw_t, resolution)
520
+ if len(v) == 0 or len(f) == 0:
521
+ return None
522
+ return trimesh.Trimesh(vertices=v, faces=f, process=False)
523
+
524
+ def get_model_config(self) -> dict:
525
+ t = self.tokenizer
526
+ return {
527
+ "total_vocab_size": t.vocab_size,
528
+ "coord_vocab_size": t.resolution,
529
+ "f_vocab_size": t.f_vocab,
530
+ "row_vocab_size": t.row_vocab, # intra/inter 行子码各 256
531
+ "coord_offset": t.coord_offset,
532
+ "f_offset": t.f_offset,
533
+ "intra_row_offset": t.intra_row_offset,
534
+ "inter_row_offset": t.inter_row_offset,
535
+ "pad_token_id": t.pad_id,
536
+ "num_octants": NUM_OCTANTS,
537
+ "subcodes_per_position": NUM_OCTANTS, # 每个 refine 位置 8 个子码槽(F 复制;intra/inter 为 8 行)
538
+ "tokens_per_face": DynamicMeshTokenizer.TOKENS_PER_FACE, # 60 扁平 token/face (9+3+6*8)
539
+ "coord_per_face": 9,
540
+ "refine_positions_per_face": DynamicMeshTokenizer.REFINE_POS_PER_FACE, # 9 (3F+3intra+3inter)
541
+ "input_num_categories": t.resolution + 1,
542
+ # 输出端:F / intra_row / inter_row 三类,每类 256;coord 为输入端条件
543
+ "output_num_categories": 1 + t.f_vocab + 2 * t.row_vocab,
544
+ }
545
+
546
+
547
+ # ==========================================
548
+ # 自测:encode -> decode -> reconstruct 往返
549
+ # ==========================================
550
+ if __name__ == "__main__":
551
+ # ==========================================
552
+ # 编解码 + 重构导出流程 (参照 fix2 的 __main__)
553
+ # 每个 mesh: 导出 GT;对每个 depth 做
554
+ # encode -> decode_tokens(细分/Fine) / decode_raw_tokens(粗糙/Coarse) / 量化网格
555
+ # 并打印计时、token 数(60/face)与 token 级往返一致性。
556
+ # ==========================================
557
+ import time
558
+
559
+ # 1. 输入/输出文件夹 (按需修改)
560
+ INPUT_DIRECTORY = "/root/junkai_lin/mul_res_diffusion/demo/complex_test_data/files"
561
+ OUTPUT_DIRECTORY = "./output/fix3_octant_factorized2"
562
+ os.makedirs(OUTPUT_DIRECTORY, exist_ok=True)
563
+
564
+ @dataclass
565
+ class MeshCodecArgs:
566
+ max_depth: int = 10
567
+ max_resolution: int = 1024
568
+ use_hourglass: bool = True
569
+ max_children: int = 8 # fix3 octant 原位固定 8 (此字段实际被忽略)
570
+
571
+ # 2. 初始化 Codec
572
+ args = MeshCodecArgs()
573
+ codec = MeshCodec(args=args)
574
+ print(f"[*] Initialized fix3 Codec. Total Vocab Size: {codec.tokenizer.vocab_size} "
575
+ f"(tokens/face={DynamicMeshTokenizer.TOKENS_PER_FACE})")
576
+
577
+ # 3. 收集所有 .obj / .glb
578
+ input_dir_path = Path(INPUT_DIRECTORY)
579
+ mesh_files = sorted(input_dir_path.rglob("*.obj")) + sorted(input_dir_path.rglob("*.glb"))
580
+ if not mesh_files:
581
+ print(f"[!] No .obj or .glb files found in {INPUT_DIRECTORY}")
582
+
583
+ DEPTHS = [0, 1, 2, 3, 4, 5, 6]
584
+
585
+ # 4. 逐文件处理
586
+ for idx, file_path in enumerate(mesh_files, 1):
587
+ obj_name = file_path.stem
588
+ print(f"\n{'=' * 50}")
589
+ print(f"[{idx}/{len(mesh_files)}] Processing: {file_path.name}")
590
+ print(f"{'=' * 50}")
591
+ file_start_time = time.perf_counter()
592
+
593
+ try:
594
+ raw_mesh = trimesh.load(str(file_path), force="mesh")
595
+ normalize(raw_mesh)
596
+ except Exception as e:
597
+ print(f"[!] load/normalize failed: {e}")
598
+ continue
599
+
600
+ # 导出 GT
601
+ gt_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_gt.obj")
602
+ raw_mesh.export(gt_path)
603
+ print(f"[*] Saved GT -> {gt_path}")
604
+
605
+ # 不同深度的编解码
606
+ for depth in DEPTHS:
607
+ print(f"\n--- Depth {depth} | res(fine)={2 ** (depth + 1)} | octant 原位 ---")
608
+
609
+ # Encoding
610
+ t0 = time.perf_counter()
611
+ raw_tokens, tokens, quantized_mesh = codec.encode_mesh(raw_mesh, target_depth=depth)
612
+ encode_time = time.perf_counter() - t0
613
+ nface = len(tokens) // DynamicMeshTokenizer.TOKENS_PER_FACE
614
+ print(f"[Timer] Encoding: {encode_time:.4f}s | flat_tokens={len(tokens)} "
615
+ f"(nface={nface}, 模型逻辑位置={18 * nface})")
616
+
617
+ # token 级往返一致性 (encode->decode->re-encode)
618
+ same = (tokens == codec.tokenizer.encode(codec.tokenizer.decode(tokens)))
619
+ print(f"[Check] token 往返一致: {same}")
620
+
621
+ # Decoding (Fine)
622
+ if len(tokens) > 0:
623
+ t1 = time.perf_counter()
624
+ recon_mesh = codec.decode_tokens(tokens, target_depth=depth)
625
+ decode_fine_time = time.perf_counter() - t1
626
+ if recon_mesh is not None:
627
+ out_path = os.path.join(
628
+ OUTPUT_DIRECTORY, f"{obj_name}_recon_d{depth}_res{2 ** (depth + 1)}_fix3.obj")
629
+ recon_mesh.export(out_path)
630
+ print(f"[Timer] Decoding (Fine): {decode_fine_time:.4f}s "
631
+ f"-> V={len(recon_mesh.vertices)} F={len(recon_mesh.faces)}")
632
+ else:
633
+ print(f"[Timer] Decoding (Fine): {decode_fine_time:.4f}s -> empty")
634
+
635
+ # Decoding (Coarse / Raw)
636
+ if len(raw_tokens) > 0:
637
+ t2 = time.perf_counter()
638
+ raw_recon_mesh = codec.decode_raw_tokens(raw_tokens, target_depth=depth)
639
+ decode_coarse_time = time.perf_counter() - t2
640
+ if raw_recon_mesh is not None:
641
+ raw_out_path = os.path.join(
642
+ OUTPUT_DIRECTORY, f"{obj_name}_raw_recon_d{depth}_res{2 ** depth}.obj")
643
+ raw_recon_mesh.export(raw_out_path)
644
+ print(f"[Timer] Decoding (Coarse): {decode_coarse_time:.4f}s "
645
+ f"-> V={len(raw_recon_mesh.vertices)} F={len(raw_recon_mesh.faces)}")
646
+
647
+ # 量化网格 (输入离散化后的 GT,用于对照重构质量上限)
648
+ if quantized_mesh is not None:
649
+ quant_out_path = os.path.join(
650
+ OUTPUT_DIRECTORY, f"{obj_name}_dis_d{depth + 1}_res{2 ** (depth + 1)}.obj")
651
+ quantized_mesh.export(quant_out_path)
652
+
653
+ file_total_time = time.perf_counter() - file_start_time
654
+ print(f"\n[*] Finished {file_path.name} in {file_total_time:.2f}s")
655
+
656
+
657
+ # ==========================================================================
658
+ # encode_mesh / decode_tokens 的 numba 加速实现细节(原 *_fast.py 并入)
659
+ # ==========================================================================
660
+
661
+ _OCT_W = np.array([1, 2, 4], dtype=np.int64) # octant 局部索引 = x + 2y + 4z
662
+
663
+ def _corrupt_fid_nodes(fid, nodes, und, nN, target_depth, corrupt):
664
+ """2a 暴露偏差训练腐蚀(仅训练): 模拟级联实测的两类粗输入误差。
665
+ (a) 伪面: 在已存在的 GT 节点间加不该有的面(过连接)。下游打包按 fid 查表 ->
666
+ 非边对的 inter 行自动全 0(empty),教模型"看 PC 把伪面 refine 成空"。
667
+ (b) 节点坐标抖动 ±jitter_max(位移): 只改 coord_part 的输入坐标,目标 F/intra/inter
668
+ 仍按真节点身份查表不变,教模型"输入坐标偏了也 refine 对"。
669
+ 目标无需重新导出 —— 全部沿用 fid 索引,与干净路径同一套向量化打包。返回 (fid_aug, nodes_coord)。
670
+ """
671
+ sp_rate = float(corrupt.get("spurious_rate", 0.0))
672
+ jp = float(corrupt.get("jitter_prob", 0.0))
673
+
674
+ if sp_rate > 0.0 and nN >= 3 and und.shape[0] > 0:
675
+ radius = int(corrupt.get("spatial_radius", 2))
676
+ n_add = int(sp_rate * fid.shape[0])
677
+ if n_add > 0:
678
+ coord2idx = {(int(c[0]), int(c[1]), int(c[2])): i for i, c in enumerate(nodes)}
679
+ existing = set(map(tuple, np.sort(fid, axis=1).tolist()))
680
+ ne = und.shape[0]
681
+ spurious, tries, maxtries = [], 0, n_add * 6
682
+ while len(spurious) < n_add and tries < maxtries:
683
+ tries += 1
684
+ ei = int(np.random.randint(ne))
685
+ ai, bi = int(und[ei, 0]), int(und[ei, 1])
686
+ base = nodes[ai] if np.random.random() < 0.5 else nodes[bi]
687
+ off = np.random.randint(-radius, radius + 1, size=3)
688
+ if not off.any():
689
+ continue
690
+ ck = (int(base[0] + off[0]), int(base[1] + off[1]), int(base[2] + off[2]))
691
+ ci = coord2idx.get(ck)
692
+ if ci is None or ci == ai or ci == bi:
693
+ continue
694
+ tri = tuple(sorted((ai, bi, ci)))
695
+ if tri in existing:
696
+ continue
697
+ existing.add(tri)
698
+ spurious.append(tri)
699
+ if spurious:
700
+ fid = np.concatenate([fid, np.asarray(spurious, dtype=fid.dtype)], axis=0)
701
+
702
+ nodes_coord = nodes
703
+ if jp > 0.0:
704
+ jmax = int(corrupt.get("jitter_max", 1))
705
+ res_c = 2 ** target_depth
706
+ nodes_coord = nodes.copy()
707
+ mask = np.random.random(nN) < jp
708
+ k = int(mask.sum())
709
+ if k > 0:
710
+ noise = np.random.randint(-jmax, jmax + 1, size=(k, 3))
711
+ nodes_coord[mask] = np.clip(nodes_coord[mask] + noise, 0, res_c - 1)
712
+ return fid, nodes_coord
713
+
714
+ def _fast_fix3_level_tokens(vertices, faces, target_depth, tok, corrupt=None):
715
+ """等价替换 MatrixOctree(max_depth=td+1).build + encode_level_tokens(td) + tokenizer.encode/encode_raw。
716
+ 返回 (raw_token_sequence, token_sequence),与原版逐位一致。"""
717
+ res = 2 ** (target_depth + 1)
718
+ Q = discretize(np.asarray(vertices, dtype=np.float64), num_discrete=res).astype(np.int64)
719
+ faces = np.asarray(faces, dtype=np.int64).reshape(-1, 3)
720
+
721
+ # ---- 节点(td) = 所有顶点叶子坐标的 parent;占据 F = 所有顶点的 octant ----
722
+ P_all = Q >> 1
723
+ i_all = (Q & 1) @ _OCT_W
724
+
725
+ # ---- 叶子边表(每面 3 条,跳过零长边 u==v,与原版一致) ----
726
+ if faces.size:
727
+ e = np.concatenate([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]], axis=0)
728
+ u, v = Q[e[:, 0]], Q[e[:, 1]]
729
+ keep = np.any(u != v, axis=1)
730
+ u, v = u[keep], v[keep]
731
+ else:
732
+ u = v = np.zeros((0, 3), dtype=np.int64)
733
+ pu, pv = u >> 1, v >> 1
734
+ iu = (u & 1) @ _OCT_W
735
+ iv = (v & 1) @ _OCT_W
736
+
737
+ # 节点表(np.unique 行字典序 = 坐标 (x,y,z) 升序 = 原版 face 内顶点 sorted() 同序)
738
+ nodes, inv = np.unique(np.concatenate([P_all, pu, pv], axis=0), axis=0, return_inverse=True)
739
+ nV, M = P_all.shape[0], pu.shape[0]
740
+ occ_idx = inv[:nV]
741
+ a, b = inv[nV:nV + M], inv[nV + M:]
742
+ nN = nodes.shape[0]
743
+ intra = a == b
744
+
745
+ # ---- F 行子码(8bit int):所有顶点的 octant 占据 ----
746
+ Fbits = np.zeros(nN, dtype=np.int64)
747
+ np.bitwise_or.at(Fbits, occ_idx, np.int64(1) << i_all)
748
+
749
+ # ---- intra 行子码 [nN,8]:绝对原位、对称双向、无对角填充(与原版一致) ----
750
+ intra_rows = np.zeros((nN, 8), dtype=np.int64)
751
+ ai, iui, ivi = a[intra], iu[intra], iv[intra]
752
+ np.bitwise_or.at(intra_rows, (ai, iui), np.int64(1) << ivi)
753
+ np.bitwise_or.at(intra_rows, (ai, ivi), np.int64(1) << iui)
754
+
755
+ # ---- inter 行子码:有向 (a,b) 分组(双向都存,与原版 build 一致) ----
756
+ im = ~intra
757
+ ea, eb, eiu, eiv = a[im], b[im], iu[im], iv[im]
758
+ dkey = np.concatenate([ea * nN + eb, eb * nN + ea])
759
+ drow = np.concatenate([eiu, eiv])
760
+ dcol = np.concatenate([eiv, eiu])
761
+ ukeys, kinv = np.unique(dkey, return_inverse=True)
762
+ inter_rows = np.zeros((max(len(ukeys), 1), 8), dtype=np.int64)
763
+ if dkey.size:
764
+ np.bitwise_or.at(inter_rows, (kinv, drow), np.int64(1) << dcol)
765
+
766
+ # ---- 无向节点邻接 -> CSR -> numba 三角形枚举 ----
767
+ if ea.size:
768
+ und = np.unique(np.stack([np.minimum(ea, eb), np.maximum(ea, eb)], axis=1), axis=0)
769
+ else:
770
+ und = np.zeros((0, 2), dtype=np.int64)
771
+ deg = np.zeros(nN, dtype=np.int64)
772
+ if und.size:
773
+ np.add.at(deg, und[:, 0], 1)
774
+ np.add.at(deg, und[:, 1], 1)
775
+ order = np.lexsort((und[:, 1], und[:, 0]))
776
+ fu, fv = und[order, 0], und[order, 1]
777
+ indptr = np.zeros(nN + 1, dtype=np.int64)
778
+ np.add.at(indptr, fu + 1, 1)
779
+ indptr = np.cumsum(indptr)
780
+ tu, tv, tw = _triangles_csr(indptr, fv.astype(np.int64))
781
+ else:
782
+ tu = tv = tw = np.zeros(0, dtype=np.int64)
783
+
784
+ # ---- faces_ids:三角形 + 未覆盖边退化 (A,B,B) + 孤立点 (A,A,A),集合与原版一致 ----
785
+ tris = np.stack([tu, tv, tw], axis=1) if tu.size else np.zeros((0, 3), dtype=np.int64)
786
+ covered = set()
787
+ for x, y, z in tris:
788
+ x, y, z = int(x), int(y), int(z)
789
+ covered.add((x, y)); covered.add((y, z)); covered.add((x, z))
790
+ extra = []
791
+ for x, y in und:
792
+ x, y = int(x), int(y)
793
+ if (x, y) not in covered:
794
+ extra.append((x, y, y))
795
+ for x in np.flatnonzero(deg == 0):
796
+ x = int(x)
797
+ extra.append((x, x, x))
798
+ fid = np.concatenate([tris, np.asarray(extra, dtype=np.int64).reshape(-1, 3)], axis=0)
799
+ if fid.shape[0] == 0:
800
+ return [], []
801
+
802
+ # ---- 原版排序键 + 决定性平局键 ----
803
+ c0, c1, c2 = nodes[fid[:, 0]], nodes[fid[:, 1]], nodes[fid[:, 2]]
804
+ order = np.lexsort((c2[:, 0], c2[:, 1], c2[:, 2],
805
+ c1[:, 0], c1[:, 1], c1[:, 2],
806
+ c0[:, 0], c0[:, 1], c0[:, 2])) # 主键=c0.z(最后) ... 与原版 7 键一致+补齐平局
807
+ fid = fid[order]
808
+ nodes_coord = nodes
809
+ if corrupt is not None:
810
+ fid, nodes_coord = _corrupt_fid_nodes(fid, nodes, und, nN, target_depth, corrupt)
811
+ c0, c1, c2 = nodes_coord[fid[:, 0]], nodes_coord[fid[:, 1]], nodes_coord[fid[:, 2]]
812
+
813
+ # ---- 60/face 扁平 token 向量化打包 ----
814
+ # 注: F_part/intra_part/inter_part 下面仍用 fid(节点身份)查表 -> 腐蚀的伪面自动得正确目标
815
+ # (非边对 inter 全 0),坐标抖动只影响上面 coord_part,目标不变。
816
+ co, fo = tok.coord_offset, tok.f_offset
817
+ io, eo = tok.intra_row_offset, tok.inter_row_offset
818
+ nF = fid.shape[0]
819
+
820
+ coord_part = np.concatenate([c0, c1, c2], axis=1) + co # (nF,9)
821
+ F_part = np.stack([Fbits[fid[:, 0]], Fbits[fid[:, 1]], Fbits[fid[:, 2]]], axis=1) + fo
822
+ intra_part = np.concatenate([intra_rows[fid[:, 0]],
823
+ intra_rows[fid[:, 1]],
824
+ intra_rows[fid[:, 2]]], axis=1) + io # (nF,24)
825
+
826
+ inter_blocks = []
827
+ for x, y in ((0, 1), (0, 2), (1, 2)):
828
+ A_, B_ = fid[:, x], fid[:, y]
829
+ rows = np.zeros((nF, 8), dtype=np.int64)
830
+ same = A_ == B_
831
+ rows[same] = intra_rows[A_[same]] # inter8(a,a)=intra8(a),与原版一致
832
+ ns = np.flatnonzero(~same)
833
+ if ns.size:
834
+ dk = A_[ns] * nN + B_[ns]
835
+ pos = np.searchsorted(ukeys, dk)
836
+ pos_c = np.minimum(pos, max(len(ukeys) - 1, 0))
837
+ found = (ukeys[pos_c] == dk) if len(ukeys) else np.zeros(ns.size, dtype=bool)
838
+ rows[ns[found]] = inter_rows[pos_c[found]] # 未找到 -> 保持全 0(原版 empty 矩阵)
839
+ inter_blocks.append(rows)
840
+ inter_part = np.concatenate(inter_blocks, axis=1) + eo # (nF,24)
841
+
842
+ token = np.concatenate([coord_part, F_part, intra_part, inter_part], axis=1) # (nF,60)
843
+ token_sequence = token.ravel().tolist()
844
+ raw_token_sequence = coord_part.reshape(-1, 1).tolist() # 9 coord/face, 各 [t]
845
+ return raw_token_sequence, token_sequence
846
+
847
+ _BIT_SHIFTS = np.arange(NUM_OCTANTS, dtype=np.int64) # _int_to_row: bit i = (v>>i)&1
848
+
849
+ _NODE_PARENT = np.arange(24) // NUM_OCTANTS # 24 个节点 -> 哪个父顶点 slot
850
+
851
+ _NODE_OCTANT = np.arange(24) % NUM_OCTANTS # 24 个节点 -> 哪个 octant
852
+
853
+ _OCT_OFFSETS = np.stack([np.arange(8) & 1, (np.arange(8) >> 1) & 1, (np.arange(8) >> 2) & 1],
854
+ axis=1).astype(np.int32) # octant -> (dx,dy,dz)
855
+
856
+ _INTER_ROW_SLOT = np.array([0, 0, 1])
857
+
858
+ _INTER_COL_SLOT = np.array([1, 2, 2])
859
+
860
+ def _unpack_rows(values):
861
+ """int 行子码 -> bool 位。values (...,) -> (..., 8), 与 _int_to_row 同序(bit i = (v>>i)&1)。"""
862
+ return ((values[..., None].astype(np.int64) >> _BIT_SHIFTS) & 1).astype(bool)
863
+
864
+ def _pack_keys(coords, resolution):
865
+ """(...,3) int 坐标 -> (...) int64 唯一 key。坐标范围 [0, resolution)。"""
866
+ c = coords.astype(np.int64)
867
+ return (c[..., 0] * resolution + c[..., 1]) * resolution + c[..., 2]
868
+
869
+ def _fast_reconstruct_next_level(token_array, resolution, tokenizer):
870
+ """token_array: (nface, TOKENS_PER_FACE) int。返回 (V, F), 语义同 reconstruct_next_level_optimized。"""
871
+ from modules.octree_codec import _TRI_INDICES
872
+
873
+ nface = token_array.shape[0]
874
+ parents = (token_array[:, :9] - tokenizer.coord_offset).reshape(nface, 3, 3).astype(np.int32)
875
+ occupancy = _unpack_rows(token_array[:, 9:12] - tokenizer.f_offset) # (nface,3,8) bool
876
+
877
+ def rows_to_mat(lo, hi, offset):
878
+ return _unpack_rows(token_array[:, lo:hi] - offset) # (nface,8,8) bool, [行,列]
879
+
880
+ intra = [rows_to_mat(12 + 8 * i, 20 + 8 * i, tokenizer.intra_row_offset) for i in range(3)]
881
+ inter = [rows_to_mat(36 + 8 * i, 44 + 8 * i, tokenizer.inter_row_offset) for i in range(3)]
882
+
883
+ # ---- 24x24 邻接: intra 块按 F&F 掩码后对称化, inter 块按 Fa&Fb 掩码 ----
884
+ adjacency = np.zeros((nface, 24, 24), dtype=bool)
885
+ for slot in range(3):
886
+ f = occupancy[:, slot] # (nface,8)
887
+ block = intra[slot] & (f[:, :, None] & f[:, None, :])
888
+ block = block | np.swapaxes(block, 1, 2)
889
+ adjacency[:, slot * 8:(slot + 1) * 8, slot * 8:(slot + 1) * 8] = block
890
+ for b in range(3):
891
+ ra, ca = _INTER_ROW_SLOT[b], _INTER_COL_SLOT[b]
892
+ block = inter[b] & (occupancy[:, ra][:, :, None] & occupancy[:, ca][:, None, :])
893
+ adjacency[:, ra * 8:(ra + 1) * 8, ca * 8:(ca + 1) * 8] = block
894
+ adjacency[:, ca * 8:(ca + 1) * 8, ra * 8:(ra + 1) * 8] = np.swapaxes(block, 1, 2)
895
+
896
+ # ---- 24 个节点的世界坐标与 key ----
897
+ node_coords = parents[:, _NODE_PARENT] * 2 + _OCT_OFFSETS[_NODE_OCTANT] # (nface,24,3)
898
+ node_keys = _pack_keys(node_coords, resolution) # (nface,24)
899
+
900
+ # ---- 三角形: 三条边都在邻接里 ----
901
+ i_idx, j_idx, k_idx = _TRI_INDICES[:, 0], _TRI_INDICES[:, 1], _TRI_INDICES[:, 2]
902
+ valid = (adjacency[:, i_idx, j_idx] & adjacency[:, j_idx, k_idx] & adjacency[:, i_idx, k_idx])
903
+ face_of_tri, tri_of_tri = np.nonzero(valid) # 各 (ntri,)
904
+ tri_nodes = _TRI_INDICES[tri_of_tri] # (ntri,3) 节点编号
905
+ tri_coords = node_coords[face_of_tri[:, None], tri_nodes] # (ntri,3,3)
906
+ tri_keys = node_keys[face_of_tri[:, None], tri_nodes] # (ntri,3)
907
+
908
+ # ---- 全部边(上三角) ----
909
+ upper = np.triu(np.ones((24, 24), dtype=bool), 1)
910
+ face_of_edge, node_i, node_j = np.nonzero(adjacency & upper)
911
+ key_i = node_keys[face_of_edge, node_i]
912
+ key_j = node_keys[face_of_edge, node_j]
913
+ edge_pairs = np.unique(np.stack([np.minimum(key_i, key_j), np.maximum(key_i, key_j)], axis=1), axis=0)
914
+
915
+ # ---- 被三角形覆盖的边 ----
916
+ if len(tri_keys):
917
+ tri_sorted = np.sort(tri_keys, axis=1)
918
+ covered = np.concatenate([tri_sorted[:, [0, 1]], tri_sorted[:, [1, 2]], tri_sorted[:, [0, 2]]], axis=0)
919
+ covered = np.unique(covered, axis=0)
920
+ else:
921
+ covered = np.zeros((0, 2), dtype=np.int64)
922
+
923
+ blocks = [tri_coords] if len(tri_coords) else []
924
+
925
+ # ---- 未被任何三角形覆盖的边 -> 退化面 (A,B,B) ----
926
+ if len(edge_pairs):
927
+ edge_flat = edge_pairs[:, 0] * (resolution ** 3) + edge_pairs[:, 1]
928
+ covered_flat = (covered[:, 0] * (resolution ** 3) + covered[:, 1]) if len(covered) \
929
+ else np.zeros(0, dtype=np.int64)
930
+ open_edges = edge_pairs[~np.isin(edge_flat, covered_flat)]
931
+ if len(open_edges):
932
+ key_to_coord = _build_key_lookup(node_keys, node_coords)
933
+ ca = key_to_coord[open_edges[:, 0]]
934
+ cb = key_to_coord[open_edges[:, 1]]
935
+ blocks.append(np.stack([ca, cb, cb], axis=1))
936
+
937
+ # ---- 孤立的 active 节点(无任何边) -> 点 (A,A,A) ----
938
+ active_keys = np.unique(node_keys[occupancy.reshape(nface, 24)])
939
+ if len(active_keys):
940
+ edge_nodes = np.unique(edge_pairs.reshape(-1)) if len(edge_pairs) else np.zeros(0, dtype=np.int64)
941
+ isolated = active_keys[~np.isin(active_keys, edge_nodes)]
942
+ if len(isolated):
943
+ key_to_coord = _build_key_lookup(node_keys, node_coords)
944
+ c = key_to_coord[isolated]
945
+ blocks.append(np.stack([c, c, c], axis=1))
946
+
947
+ if not blocks:
948
+ return np.array([]), np.array([])
949
+
950
+ flat_v = np.concatenate(blocks, axis=0).reshape(-1, 3)
951
+ V, inverse = np.unique(flat_v, axis=0, return_inverse=True)
952
+ faces_raw = inverse.reshape(-1, 3)
953
+ _, unique_face_indices = np.unique(np.sort(faces_raw, axis=1), axis=0, return_index=True)
954
+ F = faces_raw[unique_face_indices]
955
+ return undiscretize(V, num_discrete=resolution), F
956
+
957
+ def _build_key_lookup(node_keys, node_coords):
958
+ """key -> 坐标 的稠密查表(用 np.unique 的第一次出现), 供退化面/孤立点还原坐标。"""
959
+ flat_keys = node_keys.reshape(-1)
960
+ flat_coords = node_coords.reshape(-1, 3)
961
+ uniq, first = np.unique(flat_keys, return_index=True)
962
+ lookup = np.zeros((int(uniq.max()) + 1, 3), dtype=np.int32)
963
+ lookup[uniq] = flat_coords[first]
964
+ return lookup
965
+
966
+ def _fast_decode_tokens(token_sequence, target_depth, tokenizer):
967
+ """decode_tokens 的向量化实现。返回 trimesh.Trimesh 或 None。"""
968
+ arr = np.asarray(token_sequence).reshape(-1)
969
+ arr = arr[arr != tokenizer.pad_id] # 与原版 clean_seq 一致
970
+ per_face = tokenizer.TOKENS_PER_FACE
971
+ nface = arr.shape[0] // per_face
972
+ if nface == 0:
973
+ return None
974
+ token_array = arr[:nface * per_face].reshape(nface, per_face).astype(np.int64)
975
+ V, F = _fast_reconstruct_next_level(token_array, 2 ** (target_depth + 1), tokenizer)
976
+ if len(V) == 0 or len(F) == 0:
977
+ return None
978
+ return trimesh.Trimesh(vertices=V, faces=F, process=False)
979
+
980
+
981
+ FastMeshCodec = MeshCodec # 兼容别名: 加速实现已并入 MeshCodec
mulres_infer/modules/octree_codec_acc.py ADDED
@@ -0,0 +1,857 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import trimesh
3
+ from tqdm import tqdm
4
+ import os
5
+ import sys
6
+ import glob
7
+ import multiprocessing
8
+ import random
9
+ from collections import Counter
10
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+ from pathlib import Path
12
+ from utils.data_utils import undiscretize, discretize, normalize, clean_mesh
13
+ from utils.mesh_metric import compute_mesh_metrics
14
+ import itertools
15
+ from dataclasses import dataclass
16
+ # 预计算 24 个节点中选 3 个的所有可能组合 (共 2024 种)
17
+ _TRI_INDICES = np.array(list(itertools.combinations(range(24), 3)))
18
+ _IDX_I = _TRI_INDICES[:, 0]
19
+ _IDX_J = _TRI_INDICES[:, 1]
20
+ _IDX_K = _TRI_INDICES[:, 2]
21
+ # ==========================================
22
+ # 新增:动态网格 Tokenizer
23
+ # ==========================================
24
+ class DynamicMeshTokenizer:
25
+ def __init__(self, resolution, max_children):
26
+ self.resolution = resolution
27
+ self.max_children = max_children
28
+ self.m_bits = max_children * max_children
29
+
30
+ # 1. 特殊控制 Token
31
+ self.pad_id = 0
32
+
33
+ # 2. 计算各特征在词表中的偏移量 (Offsets)
34
+ self.coord_offset = 1
35
+ self.f_offset = self.coord_offset + self.resolution
36
+ self.m_intra_offset = self.f_offset + 256 # 1x8 对应 256 种可能
37
+
38
+ self.m_vocab_size = 2 ** self.m_bits
39
+ self.m_inter_offset = self.m_intra_offset + self.m_vocab_size
40
+
41
+ # 最终 Transformer 的总词表大小
42
+ self.vocab_size = self.m_inter_offset + self.m_vocab_size
43
+
44
+ def _list_to_int(self, bit_list):
45
+ res = 0
46
+ for i, bit in enumerate(bit_list):
47
+ if bit: res |= (1 << i)
48
+ return res
49
+
50
+ def _int_to_list(self, val, length):
51
+ return [(val >> i) & 1 for i in range(length)]
52
+
53
+ def _m_to_int(self, M):
54
+ flat = [bit for row in M for bit in row]
55
+ return self._list_to_int(flat)
56
+
57
+ def _int_to_m(self, val):
58
+ flat = self._int_to_list(val, self.m_bits)
59
+ return [flat[i*self.max_children : (i+1)*self.max_children] for i in range(self.max_children)]
60
+
61
+ # ------------------------------------
62
+ # 细分网格 (Fine Mesh) Tokenizer
63
+ # ------------------------------------
64
+ def encode(self, faces_data, use_hourglass=True):
65
+ """将细分几何特征列表编码为 1D Token 序列 (18 Tokens / Face)"""
66
+ seq = []
67
+
68
+ for face in faces_data:
69
+ v0, v1, v2, F0, F1, F2, M00, M11, M22, M01, M02, M12 = face
70
+
71
+ seq.extend([v0[0] + self.coord_offset, v0[1] + self.coord_offset, v0[2] + self.coord_offset])
72
+ seq.extend([v1[0] + self.coord_offset, v1[1] + self.coord_offset, v1[2] + self.coord_offset])
73
+ seq.extend([v2[0] + self.coord_offset, v2[1] + self.coord_offset, v2[2] + self.coord_offset])
74
+
75
+ seq.extend([self._list_to_int(F0) + self.f_offset,
76
+ self._list_to_int(F1) + self.f_offset,
77
+ self._list_to_int(F2) + self.f_offset])
78
+
79
+ seq.extend([self._m_to_int(M00) + self.m_intra_offset,
80
+ self._m_to_int(M11) + self.m_intra_offset,
81
+ self._m_to_int(M22) + self.m_intra_offset])
82
+
83
+ seq.extend([self._m_to_int(M01) + self.m_inter_offset,
84
+ self._m_to_int(M02) + self.m_inter_offset,
85
+ self._m_to_int(M12) + self.m_inter_offset])
86
+ return seq
87
+
88
+ def decode(self, seq):
89
+ """将细分网格的 1D Token 序列解码回几何特征列表"""
90
+ faces_data = []
91
+ clean_seq = [t for t in seq if t not in ([self.pad_id])]
92
+
93
+ for i in range(0, len(clean_seq), 18):
94
+ chunk = clean_seq[i:i+18]
95
+ if len(chunk) < 18:
96
+ break
97
+
98
+ v0 = (chunk[0] - self.coord_offset, chunk[1] - self.coord_offset, chunk[2] - self.coord_offset)
99
+ v1 = (chunk[3] - self.coord_offset, chunk[4] - self.coord_offset, chunk[5] - self.coord_offset)
100
+ v2 = (chunk[6] - self.coord_offset, chunk[7] - self.coord_offset, chunk[8] - self.coord_offset)
101
+
102
+ F0 = self._int_to_list(chunk[9] - self.f_offset, 8)
103
+ F1 = self._int_to_list(chunk[10] - self.f_offset, 8)
104
+ F2 = self._int_to_list(chunk[11] - self.f_offset, 8)
105
+
106
+ M00 = self._int_to_m(chunk[12] - self.m_intra_offset)
107
+ M11 = self._int_to_m(chunk[13] - self.m_intra_offset)
108
+ M22 = self._int_to_m(chunk[14] - self.m_intra_offset)
109
+
110
+ M01 = self._int_to_m(chunk[15] - self.m_inter_offset)
111
+ M02 = self._int_to_m(chunk[16] - self.m_inter_offset)
112
+ M12 = self._int_to_m(chunk[17] - self.m_inter_offset)
113
+
114
+ faces_data.append((v0, v1, v2, F0, F1, F2, M00, M11, M22, M01, M02, M12))
115
+
116
+ return faces_data
117
+
118
+ # ------------------------------------
119
+ # [新增] 基础/粗糙网格 (Raw Mesh) Tokenizer
120
+ # ------------------------------------
121
+ def encode_raw(self, raw_faces_data, use_hourglass=True):
122
+ """将粗糙层级特征(仅含顶点)编码为 1D Token 序列 (9 Tokens / Face)"""
123
+ seq = []
124
+
125
+ for face in raw_faces_data:
126
+ v0, v1, v2 = face
127
+ seq.extend([v0[0] + self.coord_offset, v0[1] + self.coord_offset, v0[2] + self.coord_offset])
128
+ seq.extend([v1[0] + self.coord_offset, v1[1] + self.coord_offset, v1[2] + self.coord_offset])
129
+ seq.extend([v2[0] + self.coord_offset, v2[1] + self.coord_offset, v2[2] + self.coord_offset])
130
+
131
+ return seq
132
+
133
+ def decode_raw(self, seq):
134
+ """将粗糙层级的 1D Token 序列解码回几何特征列表 [(v0, v1, v2), ...]"""
135
+ faces_data = []
136
+ clean_seq = [t for t in seq if t not in ([self.pad_id])]
137
+
138
+ # 每个面片固定转化为 9 个 Token
139
+ for i in range(0, len(clean_seq), 9):
140
+ chunk = clean_seq[i:i+9]
141
+ if len(chunk) < 9:
142
+ break
143
+
144
+ v0 = (chunk[0] - self.coord_offset, chunk[1] - self.coord_offset, chunk[2] - self.coord_offset)
145
+ v1 = (chunk[3] - self.coord_offset, chunk[4] - self.coord_offset, chunk[5] - self.coord_offset)
146
+ v2 = (chunk[6] - self.coord_offset, chunk[7] - self.coord_offset, chunk[8] - self.coord_offset)
147
+
148
+ faces_data.append((v0, v1, v2))
149
+
150
+ return faces_data
151
+
152
+
153
+ # --- 以下为原有的类声明 (OctreeFaceTokenBuilder, OctreeNode, MatrixOctree 等保持不变) ---
154
+ class OctreeFaceTokenBuilder:
155
+ def __init__(self, tree, max_children=4):
156
+ self.tree = tree
157
+ self.max_children = max_children
158
+
159
+ def get_level_info(self, depth):
160
+ nodes_at_depth = {}
161
+ def collect_nodes(current, target_depth):
162
+ if current.depth == target_depth:
163
+ nodes_at_depth[current.coord] = current
164
+ return
165
+ for child in current.children:
166
+ if child: collect_nodes(child, target_depth)
167
+
168
+ collect_nodes(self.tree.root, depth)
169
+
170
+ coords = sorted(nodes_at_depth.keys(), key=lambda x: (x[2], x[1], x[0]))
171
+ coord_to_idx = {c: i for i, c in enumerate(coords)}
172
+
173
+ adj = {i: set() for i in range(len(coords))}
174
+ for i, c in enumerate(coords):
175
+ node = nodes_at_depth[c]
176
+ all_neighbors = node.intra_neighbors | node.inter_neighbors
177
+ for neighbor_coord in all_neighbors:
178
+ if neighbor_coord in coord_to_idx:
179
+ j = coord_to_idx[neighbor_coord]
180
+ if i < j:
181
+ adj[i].add(j)
182
+
183
+ raw_faces_set = set()
184
+ covered_edges = set()
185
+
186
+ # 1. 提取所有互相连接的 3-clique (正常的三角形面片)
187
+ for u in range(len(coords)):
188
+ for v in adj[u]:
189
+ common_neighbors = adj[u] & adj[v]
190
+ for w in common_neighbors:
191
+ # 排序坐标点以确保集合去重时的唯一性
192
+ face_coords = tuple(sorted([coords[u], coords[v], coords[w]]))
193
+ raw_faces_set.add(face_coords)
194
+
195
+ # 记录已经被三角形覆盖的边
196
+ covered_edges.add(tuple(sorted([face_coords[0], face_coords[1]])))
197
+ covered_edges.add(tuple(sorted([face_coords[1], face_coords[2]])))
198
+ covered_edges.add(tuple(sorted([face_coords[0], face_coords[2]])))
199
+
200
+ # 2. 提取未形成三角形的独立边 (线段结构,例如细长桌腿)
201
+ for u in range(len(coords)):
202
+ for v in adj[u]:
203
+ edge = tuple(sorted([coords[u], coords[v]]))
204
+ if edge not in covered_edges:
205
+ # 将 1D 的线段转换为退化面片 (A, B, B) 以适配 Tokenizer 格式
206
+ degenerate_face = (edge[0], edge[1], edge[1])
207
+ raw_faces_set.add(degenerate_face)
208
+
209
+ # 3. (可选) 极端情况下处理没有任何连接的孤立顶点 (A, A, A)
210
+ for u in range(len(coords)):
211
+ has_edges = len(adj[u]) > 0 or any(u in adj[i] for i in range(u))
212
+ if not has_edges:
213
+ isolated_face = (coords[u], coords[u], coords[u])
214
+ raw_faces_set.add(isolated_face)
215
+
216
+ raw_faces_at_depth = list(raw_faces_set)
217
+
218
+ # 保持原始排序逻辑以确保输出的 Determinism
219
+ raw_faces_at_depth.sort(key=lambda f: (f[0][2], f[0][1], f[0][0],
220
+ f[1][2], f[1][1], f[1][0],
221
+ f[2][2]))
222
+
223
+ return raw_faces_at_depth, nodes_at_depth
224
+
225
+ def reduce_node_children_to_max(self, nodes_map):
226
+ def bit_dist(a, b):
227
+ return bin(a ^ b).count('1')
228
+
229
+ # Pass 1: 基于原始图结构为所有节点一次性计算 merge plan,消除顺序依赖。
230
+ merge_plan = {}
231
+ for coord, n in nodes_map.items():
232
+ active_indices = set()
233
+ has_intra_row = n.intra_matrix.any(axis=1)
234
+ has_intra_col = n.intra_matrix.any(axis=0)
235
+ for i in range(8):
236
+ if has_intra_row[i] or has_intra_col[i]:
237
+ active_indices.add(i)
238
+
239
+ for mat in n.inter_matrices.values():
240
+ has_inter_row = mat.any(axis=1)
241
+ for i in range(8):
242
+ if has_inter_row[i]:
243
+ active_indices.add(i)
244
+
245
+ for n_inter_coord in n.inter_neighbors:
246
+ if n_inter_coord in nodes_map:
247
+ neighbor = nodes_map[n_inter_coord]
248
+ if coord in neighbor.inter_matrices:
249
+ has_in_col = neighbor.inter_matrices[coord].any(axis=0)
250
+ for i in range(8):
251
+ if has_in_col[i]:
252
+ active_indices.add(i)
253
+
254
+ if len(active_indices) <= self.max_children:
255
+ merge_plan[coord] = None
256
+ continue
257
+
258
+ child_degrees = [0] * 8
259
+ intra_row_counts = np.count_nonzero(n.intra_matrix, axis=1)
260
+ intra_col_counts = np.count_nonzero(n.intra_matrix, axis=0)
261
+ for i in range(8):
262
+ child_degrees[i] += intra_row_counts[i] + intra_col_counts[i]
263
+
264
+ out_inter_counts = np.zeros(8, dtype=int)
265
+ for mat in n.inter_matrices.values():
266
+ out_inter_counts += np.count_nonzero(mat, axis=1)
267
+
268
+ in_inter_counts = np.zeros(8, dtype=int)
269
+ for n_inter_coord in n.inter_neighbors:
270
+ if n_inter_coord in nodes_map:
271
+ neighbor = nodes_map[n_inter_coord]
272
+ if coord in neighbor.inter_matrices:
273
+ in_inter_counts += np.count_nonzero(neighbor.inter_matrices[coord], axis=0)
274
+
275
+ for i in range(8):
276
+ child_degrees[i] += 2 * (out_inter_counts[i] + in_inter_counts[i])
277
+
278
+ active_list = sorted(list(active_indices))
279
+ scored = sorted(
280
+ active_list,
281
+ key=lambda idx: (-child_degrees[idx], idx)
282
+ )
283
+ keep_nodes = sorted(scored[:self.max_children])
284
+
285
+ mapping = {idx: idx for idx in range(8)}
286
+ for m_idx in active_list:
287
+ if m_idx in keep_nodes:
288
+ continue
289
+ target_merge = min(keep_nodes, key=lambda x: (bit_dist(x, m_idx), x))
290
+ mapping[m_idx] = target_merge
291
+
292
+ merge_plan[coord] = mapping
293
+
294
+ # Pass 2: 根据全局 merge plan 同步重映射所有 intra/inter 矩阵。
295
+ for coord, n in nodes_map.items():
296
+ mapping = merge_plan.get(coord)
297
+ if mapping is not None:
298
+ old_intra = n.intra_matrix
299
+ new_intra = np.zeros_like(old_intra)
300
+ nz = np.argwhere(old_intra != 0)
301
+ for i, j in nz:
302
+ new_intra[mapping[int(i)], mapping[int(j)]] = 1
303
+ np.fill_diagonal(new_intra, 0)
304
+ n.intra_matrix = new_intra
305
+
306
+ for neigh_coord, mat in list(n.inter_matrices.items()):
307
+ src_map = merge_plan.get(coord)
308
+ dst_map = merge_plan.get(neigh_coord)
309
+ if src_map is None and dst_map is None:
310
+ continue
311
+
312
+ new_mat = np.zeros_like(mat)
313
+ nz = np.argwhere(mat != 0)
314
+ for i, j in nz:
315
+ ni = src_map[int(i)] if src_map is not None else int(i)
316
+ nj = dst_map[int(j)] if dst_map is not None else int(j)
317
+ new_mat[ni, nj] = 1
318
+ n.inter_matrices[neigh_coord] = new_mat
319
+
320
+ def encode_level_tokens(self, depth):
321
+ raw_faces, nodes_map = self.get_level_info(depth)
322
+
323
+ self.reduce_node_children_to_max(nodes_map)
324
+
325
+ node_F = {}
326
+ node_active = {}
327
+ empty_mat = np.zeros((8, 8), dtype=np.int8)
328
+
329
+ for coord, n in nodes_map.items():
330
+ active_indices = set()
331
+ has_intra_row = n.intra_matrix.any(axis=1)
332
+ has_intra_col = n.intra_matrix.any(axis=0)
333
+ for i in range(8):
334
+ if has_intra_row[i] or has_intra_col[i]:
335
+ active_indices.add(i)
336
+
337
+ for mat in n.inter_matrices.values():
338
+ has_inter_row = mat.any(axis=1)
339
+ for i in range(8):
340
+ if has_inter_row[i]:
341
+ active_indices.add(i)
342
+
343
+ for n_inter_coord in n.inter_neighbors:
344
+ if n_inter_coord in nodes_map:
345
+ neighbor = nodes_map[n_inter_coord]
346
+ if n.coord in neighbor.inter_matrices:
347
+ has_in_col = neighbor.inter_matrices[n.coord].any(axis=0)
348
+ for i in range(8):
349
+ if has_in_col[i]:
350
+ active_indices.add(i)
351
+
352
+ F = [0] * 8
353
+ for idx in active_indices:
354
+ F[idx] = 1
355
+ node_F[coord] = F
356
+ node_active[coord] = sorted(list(active_indices))
357
+
358
+ memo_intra = {}
359
+ memo_inter = {}
360
+
361
+ def extract_NxN_intra(node_coord):
362
+ if node_coord in memo_intra:
363
+ return memo_intra[node_coord]
364
+
365
+ active = node_active[node_coord][:self.max_children]
366
+ n = nodes_map[node_coord]
367
+
368
+ if len(active) > 0:
369
+ M = np.zeros((self.max_children, self.max_children), dtype=np.int8)
370
+ sub = n.intra_matrix[np.ix_(active, active)]
371
+ M[:len(active), :len(active)] = sub
372
+ np.fill_diagonal(M[:len(active), :len(active)], 1)
373
+ res = M.tolist()
374
+ else:
375
+ res = np.zeros((self.max_children, self.max_children), dtype=np.int8).tolist()
376
+
377
+ memo_intra[node_coord] = res
378
+ return res
379
+
380
+ def extract_NxN_inter(coord_A, coord_B):
381
+ if coord_A == coord_B:
382
+ return extract_NxN_intra(coord_A)
383
+
384
+ key = (coord_A, coord_B)
385
+ if key in memo_inter:
386
+ return memo_inter[key]
387
+
388
+ active_A = node_active[coord_A][:self.max_children]
389
+ active_B = node_active[coord_B][:self.max_children]
390
+ nA = nodes_map[coord_A]
391
+ orig_M = nA.inter_matrices.get(coord_B, empty_mat)
392
+
393
+ if len(active_A) > 0 and len(active_B) > 0:
394
+ M = np.zeros((self.max_children, self.max_children), dtype=np.int8)
395
+ M[:len(active_A), :len(active_B)] = orig_M[np.ix_(active_A, active_B)]
396
+ res = M.tolist()
397
+ else:
398
+ res = np.zeros((self.max_children, self.max_children), dtype=np.int8).tolist()
399
+
400
+ memo_inter[key] = res
401
+ return res
402
+
403
+ raw_tokens = []
404
+ output_tokens = []
405
+
406
+ for face in raw_faces:
407
+ v0_c, v1_c, v2_c = face
408
+ raw_tokens.append([v0_c, v1_c, v2_c])
409
+
410
+ F0, F1, F2 = node_F[v0_c], node_F[v1_c], node_F[v2_c]
411
+
412
+ M00 = extract_NxN_intra(v0_c)
413
+ M11 = extract_NxN_intra(v1_c)
414
+ M22 = extract_NxN_intra(v2_c)
415
+
416
+ M01 = extract_NxN_inter(v0_c, v1_c)
417
+ M02 = extract_NxN_inter(v0_c, v2_c)
418
+ M12 = extract_NxN_inter(v1_c, v2_c)
419
+
420
+ f_encoded = (
421
+ v0_c, v1_c, v2_c,
422
+ F0, F1, F2,
423
+ M00, M11, M22, M01, M02, M12
424
+ )
425
+ output_tokens.append(f_encoded)
426
+
427
+ return raw_tokens, output_tokens
428
+
429
+ class OctreeNode:
430
+ def __init__(self, x, y, z, depth):
431
+ self.coord = (x, y, z)
432
+ self.depth = depth
433
+ self.children = [None] * 8
434
+ self.intra_neighbors = set()
435
+ self.inter_neighbors = set()
436
+ self.intra_matrix = np.zeros((8, 8), dtype=np.int8)
437
+ self.inter_matrices = {}
438
+
439
+ class MatrixOctree:
440
+ def __init__(self, max_depth=7):
441
+ self.max_depth = max_depth
442
+ self.root = None
443
+
444
+ def _get_parent_coord(self, coord):
445
+ return (coord[0] >> 1, coord[1] >> 1, coord[2] >> 1)
446
+
447
+ def _get_local_index(self, coord):
448
+ return ((coord[2] & 1) << 2) | ((coord[1] & 1) << 1) | (coord[0] & 1)
449
+
450
+ def build(self, vertices, faces):
451
+ resolution = 2 ** self.max_depth
452
+ quantized = discretize(vertices, num_discrete=resolution)
453
+
454
+ leaf_nodes_map = {}
455
+ for i in range(len(vertices)):
456
+ coord = tuple(quantized[i])
457
+ if coord not in leaf_nodes_map:
458
+ leaf_nodes_map[coord] = OctreeNode(coord[0], coord[1], coord[2], self.max_depth)
459
+
460
+ for face in faces:
461
+ coords = [tuple(quantized[i]) for i in face]
462
+ for i in range(3):
463
+ u, v = coords[i], coords[(i+1)%3]
464
+ if u == v: continue
465
+
466
+ parent_u = self._get_parent_coord(u)
467
+ parent_v = self._get_parent_coord(v)
468
+
469
+ node_u = leaf_nodes_map[u]
470
+ node_v = leaf_nodes_map[v]
471
+
472
+ if parent_u == parent_v:
473
+ node_u.intra_neighbors.add(v)
474
+ node_v.intra_neighbors.add(u)
475
+ else:
476
+ node_u.inter_neighbors.add(v)
477
+ node_v.inter_neighbors.add(u)
478
+
479
+ current_layer_nodes = list(leaf_nodes_map.values())
480
+
481
+ for d in range(self.max_depth, 0, -1):
482
+ parent_layer_map = {}
483
+ for child in current_layer_nodes:
484
+ parent_coord = self._get_parent_coord(child.coord)
485
+ if parent_coord not in parent_layer_map:
486
+ parent_layer_map[parent_coord] = OctreeNode(*parent_coord, d - 1)
487
+
488
+ parent_node = parent_layer_map[parent_coord]
489
+ idx = self._get_local_index(child.coord)
490
+ parent_node.children[idx] = child
491
+
492
+ for child in current_layer_nodes:
493
+ parent_coord = self._get_parent_coord(child.coord)
494
+ parent_node = parent_layer_map[parent_coord]
495
+ child_idx = self._get_local_index(child.coord)
496
+
497
+ for sibling_coord in child.intra_neighbors:
498
+ sibling_idx = self._get_local_index(sibling_coord)
499
+ parent_node.intra_matrix[child_idx, sibling_idx] = 1
500
+
501
+ for neighbor_coord in child.inter_neighbors:
502
+ neighbor_parent_coord = self._get_parent_coord(neighbor_coord)
503
+ if neighbor_parent_coord not in parent_node.inter_matrices:
504
+ parent_node.inter_matrices[neighbor_parent_coord] = np.zeros((8, 8), dtype=np.int8)
505
+
506
+ neighbor_child_idx = self._get_local_index(neighbor_coord)
507
+ parent_node.inter_matrices[neighbor_parent_coord][child_idx, neighbor_child_idx] = 1
508
+
509
+ grandparent_p = self._get_parent_coord(parent_coord)
510
+ grandparent_n = self._get_parent_coord(neighbor_parent_coord)
511
+
512
+ if grandparent_p == grandparent_n:
513
+ parent_node.intra_neighbors.add(neighbor_parent_coord)
514
+ else:
515
+ parent_node.inter_neighbors.add(neighbor_parent_coord)
516
+
517
+ current_layer_nodes = list(parent_layer_map.values())
518
+ if current_layer_nodes:
519
+ self.root = current_layer_nodes[0]
520
+
521
+
522
+ def reconstruct_next_level_optimized(output_tokens, resolution, max_children=4):
523
+ all_face_coords = []
524
+
525
+ indices = np.arange(8)
526
+ offsets = np.stack([indices & 1, (indices >> 1) & 1, (indices >> 2) & 1], axis=1).astype(np.int32)
527
+
528
+ def expand_intra(F, M_NxN):
529
+ M8x8 = np.zeros((8, 8), dtype=bool)
530
+ active = [i for i, val in enumerate(F) if val]
531
+ for i_n, i_o in enumerate(active):
532
+ for j_n, j_o in enumerate(active):
533
+ if i_n < max_children and j_n < max_children and M_NxN[i_n][j_n]:
534
+ M8x8[i_o][j_o] = True
535
+ return M8x8
536
+
537
+ def expand_inter(FA, FB, M_NxN):
538
+ M8x8 = np.zeros((8, 8), dtype=bool)
539
+ activeA = [i for i, val in enumerate(FA) if val]
540
+ activeB = [i for i, val in enumerate(FB) if val]
541
+ for i_n, i_o in enumerate(activeA):
542
+ for j_n, j_o in enumerate(activeB):
543
+ if i_n < max_children and j_n < max_children and M_NxN[i_n][j_n]:
544
+ M8x8[i_o][j_o] = True
545
+ return M8x8
546
+
547
+ for f_token in output_tokens:
548
+ v0_c, v1_c, v2_c, F0, F1, F2, M00, M11, M22, M01, M02, M12 = f_token
549
+
550
+ v_parents = [np.array(v[:3], dtype=np.int32) for v in [v0_c, v1_c, v2_c]]
551
+
552
+ in0 = expand_intra(F0, M00)
553
+ in1 = expand_intra(F1, M11)
554
+ in2 = expand_intra(F2, M22)
555
+
556
+ m01 = expand_inter(F0, F1, M01)
557
+ m02 = expand_inter(F0, F2, M02)
558
+ m12 = expand_inter(F1, F2, M12)
559
+
560
+ # in_mats = [in0, in1, in2]
561
+ # inter_edges = {
562
+ # (0, 1): m01,
563
+ # (1, 2): m12,
564
+ # (0, 2): m02
565
+ # }
566
+
567
+ # def are_connected_opt(p_a, c_a, p_b, c_b):
568
+ # # p_a 和 p_b 是 0, 1, 2 这三个顶点索引。
569
+ # # 只要索引相同,说明是查询同一个顶点内部;索引不同,无脑查 inter_edges
570
+ # if p_a == p_b:
571
+ # return in_mats[p_a][c_a, c_b]
572
+ # if p_a < p_b:
573
+ # return inter_edges[(p_a, p_b)][c_a, c_b]
574
+ # else:
575
+ # return inter_edges[(p_b, p_a)][c_b, c_a]
576
+
577
+ # from itertools import combinations_with_replacement
578
+ # for pi, pj, pk in combinations_with_replacement(range(3), 3):
579
+ # for i in range(8):
580
+ # start_j = i + 1 if pi == pj else 0
581
+ # for j in range(start_j, 8):
582
+ # if not are_connected_opt(pi, i, pj, j): continue
583
+
584
+ # start_k = j + 1 if pj == pk else 0
585
+ # for k in range(start_k, 8):
586
+ # if are_connected_opt(pj, j, pk, k) and are_connected_opt(pk, k, pi, i):
587
+ # c0 = v_parents[pi] * 2 + offsets[i]
588
+ # c1 = v_parents[pj] * 2 + offsets[j]
589
+ # c2 = v_parents[pk] * 2 + offsets[k]
590
+ # all_face_coords.append(np.stack([c0, c1, c2]))
591
+ # 1. 构建 24x24 全局邻接矩阵 A
592
+ A = np.zeros((24, 24), dtype=bool)
593
+
594
+ # 填充内部矩阵 (Intra),使用 | .T 确保无向图的对称性
595
+ A[0:8, 0:8] = in0 | in0.T
596
+ A[8:16, 8:16] = in1 | in1.T
597
+ A[16:24, 16:24] = in2 | in2.T
598
+
599
+ # 填充外部跨体素矩阵 (Inter)
600
+ A[0:8, 8:16] = m01
601
+ A[8:16, 0:8] = m01.T
602
+ A[0:8, 16:24] = m02
603
+ A[16:24, 0:8] = m02.T
604
+ A[8:16, 16:24] = m12
605
+ A[16:24, 8:16] = m12.T
606
+
607
+ # 2. 向量化寻找所有的 3-clique (三角形)
608
+ # 只需要一行代码,判断 i-j, j-k, i-k 是否同时连通
609
+ valid_mask = A[_IDX_I, _IDX_J] & A[_IDX_J, _IDX_K] & A[_IDX_I, _IDX_K]
610
+ valid_triangles = _TRI_INDICES[valid_mask] # 过滤出所有形成三角形的节点组合
611
+
612
+ # 3. 批量计算物理坐标
613
+ if len(valid_triangles) > 0:
614
+ # 将 0~23 的索引还原为父节点索引 (p_idx: 0~2) 和子节点索引 (c_idx: 0~7)
615
+ p_idx = valid_triangles // 8
616
+ c_idx = valid_triangles % 8
617
+
618
+ v_parents_arr = np.stack(v_parents) # shape: (3, 3)
619
+
620
+ # 批量算出所有符合条件的三角面的世界坐标,shape: (N, 3, 3)
621
+ coords = v_parents_arr[p_idx] * 2 + offsets[c_idx]
622
+ all_face_coords.append(coords)
623
+
624
+ if not all_face_coords:
625
+ return np.array([]), np.array([])
626
+
627
+ flat_v = np.concatenate(all_face_coords, axis=0).reshape(-1, 3)
628
+ V, inv = np.unique(flat_v, axis=0, return_inverse=True)
629
+ F_raw = inv.reshape(-1, 3)
630
+
631
+ # mask_not_degenerate = (F_raw[:, 0] != F_raw[:, 1]) & \
632
+ # (F_raw[:, 1] != F_raw[:, 2]) & \
633
+ # (F_raw[:, 2] != F_raw[:, 0])
634
+ # F_valid = F_raw[mask_not_degenerate]
635
+ F_valid = F_raw
636
+
637
+ F_sorted = np.sort(F_valid, axis=1)
638
+ _, unique_face_indices = np.unique(F_sorted, axis=0, return_index=True)
639
+ F = F_valid[unique_face_indices]
640
+ V = undiscretize(V, num_discrete=resolution)
641
+ return V, F
642
+
643
+ def reconstruct_base_mesh(output_tokens, resolution):
644
+ all_face_coords = []
645
+ for f_token in output_tokens:
646
+ v0_c, v1_c, v2_c = f_token
647
+ c0 = np.array(v0_c[:3], dtype=np.int32)
648
+ c1 = np.array(v1_c[:3], dtype=np.int32)
649
+ c2 = np.array(v2_c[:3], dtype=np.int32)
650
+ all_face_coords.append(np.stack([c0, c1, c2]))
651
+
652
+ if not all_face_coords:
653
+ return np.array([]), np.array([])
654
+
655
+ flat_v = np.concatenate(all_face_coords, axis=0).reshape(-1, 3)
656
+ V, inv = np.unique(flat_v, axis=0, return_inverse=True)
657
+ F_raw = inv.reshape(-1, 3)
658
+
659
+ # mask_not_degenerate = (F_raw[:, 0] != F_raw[:, 1]) & \
660
+ # (F_raw[:, 1] != F_raw[:, 2]) & \
661
+ # (F_raw[:, 2] != F_raw[:, 0])
662
+ # F_valid = F_raw[mask_not_degenerate]
663
+ F_valid = F_raw
664
+
665
+ F_sorted = np.sort(F_valid, axis=1)
666
+ _, unique_face_indices = np.unique(F_sorted, axis=0, return_index=True)
667
+ F = F_valid[unique_face_indices]
668
+ V = undiscretize(V, num_discrete=resolution)
669
+ return V, F
670
+
671
+
672
+ class MeshCodec:
673
+ def __init__(self, args):
674
+ self.max_depth = args.max_depth
675
+ self.max_resolution = args.max_resolution
676
+ self.use_hourglass = args.use_hourglass
677
+ self.max_children = args.max_children
678
+
679
+ self.tokenizer = DynamicMeshTokenizer(resolution=self.max_resolution, max_children=self.max_children)
680
+
681
+ def encode_mesh(self, mesh: trimesh.Trimesh, target_depth: int) -> tuple:
682
+
683
+ mesh_copy = mesh.copy()
684
+ if isinstance(mesh_copy, trimesh.Scene):
685
+ mesh_copy = mesh_copy.dump(concatenate=True)
686
+
687
+ v_restored = discretize(mesh_copy.vertices, num_discrete=2**(target_depth+1))
688
+ v_restored = undiscretize(v_restored, num_discrete=2**(target_depth+1))
689
+
690
+ # 4. 生成新的量化网格
691
+ quantized_mesh = trimesh.Trimesh(vertices=v_restored, faces=mesh.faces, process=False)
692
+
693
+ # tree = MatrixOctree(max_depth=self.max_depth)
694
+ tree = MatrixOctree(max_depth=target_depth+1)
695
+ tree.build(mesh_copy.vertices, mesh_copy.faces)
696
+
697
+ builder = OctreeFaceTokenBuilder(tree, max_children=self.max_children)
698
+
699
+ raw_t, out_t = builder.encode_level_tokens(depth=target_depth)
700
+
701
+ token_sequence = self.tokenizer.encode(out_t, self.use_hourglass)
702
+ raw_token_sequence = self.tokenizer.encode_raw(raw_t, self.use_hourglass)
703
+
704
+ return raw_token_sequence, token_sequence, quantized_mesh
705
+
706
+ def decode_tokens(self, token_sequence: list, target_depth: int) -> trimesh.Trimesh:
707
+ if not token_sequence:
708
+ return None
709
+
710
+ resolution = 2 ** (target_depth + 1)
711
+ out_t = self.tokenizer.decode(token_sequence)
712
+ v_next, f_next = reconstruct_next_level_optimized(out_t, resolution, max_children=self.max_children)
713
+
714
+ if len(v_next) == 0 or len(f_next) == 0:
715
+ return None
716
+
717
+ detokenized_mesh = trimesh.Trimesh(vertices=v_next, faces=f_next, process=False)
718
+ # trimesh.repair.fix_normals(detokenized_mesh)
719
+ # clean_mesh(detokenized_mesh)
720
+ return detokenized_mesh
721
+
722
+ def decode_raw_tokens(self, raw_token_sequence: list, target_depth:int) -> trimesh.Trimesh:
723
+ if not raw_token_sequence:
724
+ return None
725
+
726
+ resolution = 2 ** (target_depth)
727
+
728
+ # [修改] 先使用 tokenizer 从 1D 整数序列解码回 raw_t 结构
729
+ raw_t = self.tokenizer.decode_raw(raw_token_sequence)
730
+
731
+ v,f = reconstruct_base_mesh(raw_t, resolution)
732
+ if len(v) == 0 or len(f) == 0:
733
+ return None
734
+
735
+ detokenized_mesh = trimesh.Trimesh(vertices=v, faces=f, process=False)
736
+ # trimesh.repair.fix_normals(detokenized_mesh)
737
+ # clean_mesh(detokenized_mesh)
738
+
739
+ return detokenized_mesh
740
+
741
+ def get_model_config(self) -> dict:
742
+ return {
743
+ "total_vocab_size": self.tokenizer.vocab_size,
744
+ "coord_vocab_size": self.tokenizer.resolution,
745
+ "f_vocab_size": 256,
746
+ "m_vocab_size": self.tokenizer.m_vocab_size,
747
+ "coord_offset": self.tokenizer.coord_offset,
748
+ "f_offset": self.tokenizer.f_offset,
749
+ "m_intra_offset": self.tokenizer.m_intra_offset,
750
+ "m_inter_offset": self.tokenizer.m_inter_offset,
751
+ "pad_token_id": self.tokenizer.pad_id,
752
+ "max_children": self.max_children,
753
+ "input_num_categories": self.tokenizer.resolution+1,
754
+ "output_num_categories": 1 + 2*2**self.tokenizer.m_bits + 256,
755
+ }
756
+
757
+ # ==========================================
758
+ # 测试重构与无损化流程
759
+ # ==========================================
760
+ if __name__ == "__main__":
761
+ # 1. 定义输入和输出文件夹
762
+ INPUT_DIRECTORY = "/root/junkai_lin/mul_res_diffusion/demo/complex_test_data/files" # 【修改这里】替换为你的模型文件夹路径
763
+ OUTPUT_DIRECTORY = "./output/complex_child8"
764
+ os.makedirs(OUTPUT_DIRECTORY, exist_ok=True)
765
+ import time
766
+ @dataclass
767
+ class MeshCodecArgs:
768
+ max_depth: int = 10
769
+ max_resolution: int = 1024
770
+ use_hourglass: bool = True
771
+ max_children: int = 8
772
+
773
+ # 2. 初始化 Codec (放在循环外,避免重复加载)
774
+ args = MeshCodecArgs()
775
+ codec = MeshCodec(args=args)
776
+
777
+ print(f"[*] Initialized Codec. Total Vocab Size: {codec.tokenizer.vocab_size}")
778
+
779
+ # 3. 获取所有 .obj 和 .glb 文件
780
+ input_dir_path = Path(INPUT_DIRECTORY)
781
+ # rglob 会递归查找子文件夹,如果只需要当前层级,可以把 rglob 改成 glob
782
+ mesh_files = list(input_dir_path.rglob("*.obj")) + list(input_dir_path.rglob("*.glb"))
783
+
784
+ if not mesh_files:
785
+ print(f"[!] No .obj or .glb files found in {INPUT_DIRECTORY}")
786
+
787
+ # 4. 遍历处理每个文件
788
+ for idx, file_path in enumerate(mesh_files, 1):
789
+ obj_name = file_path.stem # 获取不带后缀的文件名
790
+ print(f"\n{'='*50}")
791
+ print(f"[{idx}/{len(mesh_files)}] Processing: {file_path.name}")
792
+ print(f"{'='*50}")
793
+ file_start_time = time.perf_counter()
794
+ # try:
795
+ # 加载并归一化 Mesh
796
+ raw_mesh = trimesh.load(str(file_path), force="mesh")
797
+ normalize(raw_mesh)
798
+ # clean_mesh(raw_mesh)
799
+
800
+ if raw_mesh:
801
+ out_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_gt.obj")
802
+ raw_mesh.export(out_path)
803
+ print(f"[*] Successfully saved GT to: {out_path}")
804
+
805
+ # 开始不同深度 (Depth) 的推理
806
+ for idx, file_path in enumerate(mesh_files, 1):
807
+ obj_name = file_path.stem
808
+ print(f"\n{'='*50}")
809
+ print(f"[{idx}/{len(mesh_files)}] Processing: {file_path.name}")
810
+ print(f"{'='*50}")
811
+
812
+ file_start_time = time.perf_counter() # 记录单个文件处理开始
813
+
814
+ raw_mesh = trimesh.load(str(file_path), force="mesh")
815
+ normalize(raw_mesh)
816
+
817
+ if raw_mesh:
818
+ out_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_gt.obj")
819
+ raw_mesh.export(out_path)
820
+
821
+ # 开始不同深度 (Depth) 的推理
822
+ for depth in [0,1,2,3,4,5,6]:
823
+ print(f"\n--- Depth {depth} | N={args.max_children} ---")
824
+
825
+ # --- 计时:Encoding ---
826
+ t0 = time.perf_counter()
827
+ raw_tokens, tokens, quantized_mesh = codec.encode_mesh(raw_mesh, target_depth=depth)
828
+ encode_time = time.perf_counter() - t0
829
+ print(f"[Timer] Encoding: {encode_time:.4f}s")
830
+
831
+ # --- 计时:Decoding (Fine) ---
832
+ if len(tokens) > 0:
833
+ t1 = time.perf_counter()
834
+ recon_mesh = codec.decode_tokens(tokens, target_depth=depth)
835
+ decode_fine_time = time.perf_counter() - t1
836
+ print(f"[Timer] Decoding (Fine): {decode_fine_time:.4f}s")
837
+
838
+ # 导出保存
839
+ out_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_recon_d{depth}_res{2**(depth+1)}_N{args.max_children}.obj")
840
+ recon_mesh.export(out_path)
841
+
842
+ # --- 计时:Decoding (Coarse/Raw) ---
843
+ if len(raw_tokens) > 0:
844
+ t2 = time.perf_counter()
845
+ raw_recon_mesh = codec.decode_raw_tokens(raw_tokens, target_depth=depth)
846
+ decode_coarse_time = time.perf_counter() - t2
847
+ print(f"[Timer] Decoding (Coarse): {decode_coarse_time:.4f}s")
848
+
849
+ raw_out_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_raw_recon_d{depth}_res{2**depth}.obj")
850
+ raw_recon_mesh.export(raw_out_path)
851
+
852
+ if quantized_mesh:
853
+ quant_out_path = os.path.join(OUTPUT_DIRECTORY, f"{obj_name}_dis_d{depth+1}_res{2**(depth+1)}.obj")
854
+ quantized_mesh.export(quant_out_path)
855
+
856
+ file_total_time = time.perf_counter() - file_start_time
857
+ print(f"\n[*] Finished {file_path.name} in {file_total_time:.2f}s")
mulres_infer/modules/octree_decode.py ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import glob
3
+ import math
4
+ import os
5
+ import random
6
+ import re
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import torch
11
+ import trimesh
12
+ from accelerate import Accelerator, DistributedDataParallelKwargs
13
+ from tqdm import tqdm
14
+
15
+ import importlib.util
16
+
17
+ from utils.config import load_config
18
+ from dataset.mesh_dataset import mul_encode_mesh, discrete_undiscrete_and_clean
19
+ from models.generator import FactorizedDiffusionTransformer
20
+ from modules.octree_codec import MeshCodec
21
+ from utils.postprocessing import _fill_holes
22
+ from utils.data_utils import normalize
23
+
24
+ # fix3 编码格式的数据通路(与训练同一份实现, 已抽成独立模块)
25
+ from modules.token_utils import split_fix3_token_list, subcodes_to_flat_token
26
+ def set_seed(seed):
27
+ random.seed(seed)
28
+ np.random.seed(seed)
29
+ torch.manual_seed(seed)
30
+ if torch.cuda.is_available():
31
+ torch.cuda.manual_seed(seed)
32
+ torch.cuda.manual_seed_all(seed)
33
+ torch.backends.cudnn.deterministic = True
34
+ torch.backends.cudnn.benchmark = False
35
+ os.environ["PYTHONHASHSEED"] = str(seed)
36
+
37
+ def _depth_from_resolution(resolution):
38
+ if resolution <= 0:
39
+ raise ValueError(f"resolution must be positive, got {resolution}")
40
+ depth = int(round(math.log2(resolution)))
41
+ if (1 << depth) != resolution:
42
+ raise ValueError(f"resolution must be power of 2, got {resolution}")
43
+ return depth
44
+
45
+
46
+ def read_dataset_paths(txt_path):
47
+ mesh_paths = []
48
+ with open(txt_path, "r") as f:
49
+ for line in f:
50
+ path = line.strip()
51
+ if path:
52
+ mesh_paths.append(path)
53
+ return mesh_paths
54
+
55
+
56
+ def raw_stem_to_pair_key(raw_path):
57
+ stem = Path(raw_path).stem
58
+ # raw name example: 1(42)_y_f_4244 -> pair key: 1(42)
59
+ return re.sub(r"_y_f_\d+$", "", stem)
60
+
61
+
62
+ def gt_stem_to_pair_key(gt_path):
63
+ return Path(gt_path).stem
64
+
65
+ def get_face_count(file_path):
66
+ """
67
+ 使用正则表达式从文件名中提取面片数量
68
+ 支持格式: *_f{num}_v*.obj 或 *_facenum_{num}_*.glb
69
+ """
70
+ filename = Path(file_path).name
71
+ match = re.search(r'_f(\d+)_v|_facenum_(\d+)', filename)
72
+ if match:
73
+ # group(1) 对应 _f(\d+)_v 里的数字
74
+ # group(2) 对应 _facenum_(\d+) 里的数字
75
+ return int(match.group(1) or match.group(2))
76
+ return None
77
+
78
+ # def build_raw_gt_pairs(raw_txt_path, gt_txt_path):
79
+ # raw_paths = read_dataset_paths(raw_txt_path)
80
+ # gt_paths = read_dataset_paths(gt_txt_path)
81
+
82
+ # # If two txt paths are the same source, pair each mesh with itself directly.
83
+ # if Path(raw_txt_path).resolve() == Path(gt_txt_path).resolve():
84
+ # return [(p, p) for p in raw_paths], []
85
+
86
+ # gt_by_key = {}
87
+ # for gt_path in gt_paths:
88
+ # gt_by_key[gt_stem_to_pair_key(gt_path)] = gt_path
89
+
90
+ # pairs = []
91
+ # missing_gt = []
92
+ # for raw_path in raw_paths:
93
+ # key = raw_stem_to_pair_key(raw_path)
94
+ # gt_path = gt_by_key.get(key)
95
+ # if gt_path is None:
96
+ # missing_gt.append(raw_path)
97
+ # continue
98
+ # pairs.append((raw_path, gt_path))
99
+
100
+ # return pairs, missing_gt
101
+ def build_raw_gt_pairs(raw_txt_path, gt_txt_path, min_faces=None, max_faces=None):
102
+ raw_paths = read_dataset_paths(raw_txt_path)
103
+ gt_paths = read_dataset_paths(gt_txt_path)
104
+
105
+ # ===== 新增过滤逻辑 =====
106
+ filtered_raw_paths = []
107
+ for p in raw_paths:
108
+ if min_faces is None and max_faces is None:
109
+ filtered_raw_paths.append(p)
110
+ continue
111
+
112
+ faces = get_face_count(p)
113
+ if faces is not None:
114
+ # 检查是否在指定范围内
115
+ if min_faces is not None and faces < min_faces:
116
+ continue
117
+ if max_faces is not None and faces > max_faces:
118
+ continue
119
+ filtered_raw_paths.append(p)
120
+ else:
121
+ # 如果文件名中没找到面片数特征,默认保留(你也可以改为 continue 丢弃它)
122
+ filtered_raw_paths.append(p)
123
+
124
+ raw_paths = filtered_raw_paths
125
+ # ========================
126
+
127
+ # If two txt paths are the same source, pair each mesh with itself directly.
128
+ if Path(raw_txt_path).resolve() == Path(gt_txt_path).resolve():
129
+ return [(p, p) for p in raw_paths], []
130
+
131
+ gt_by_key = {}
132
+ for gt_path in gt_paths:
133
+ gt_by_key[gt_stem_to_pair_key(gt_path)] = gt_path
134
+
135
+ pairs = []
136
+ missing_gt = []
137
+ for raw_path in raw_paths:
138
+ key = raw_stem_to_pair_key(raw_path)
139
+ gt_path = gt_by_key.get(key)
140
+ if gt_path is None:
141
+ missing_gt.append(raw_path)
142
+ continue
143
+ pairs.append((raw_path, gt_path))
144
+
145
+ return pairs, missing_gt
146
+
147
+ def save_gt_mesh(mesh, out_dir, name):
148
+ gt_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False)
149
+ gt_mesh.vertices = gt_mesh.vertices[:, [1, 2, 0]]
150
+ gt_mesh.export(os.path.join(out_dir, f"{name}_gt.obj"))
151
+
152
+ def save_raw_gt_mesh(mesh, out_dir, name):
153
+ gt_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False)
154
+ gt_mesh.vertices = gt_mesh.vertices[:, [1, 2, 0]]
155
+ gt_mesh.export(os.path.join(out_dir, f"{name}_raw_gt.obj"))
156
+
157
+
158
+ def save_stage_input_mesh(mesh, out_dir, name, stage_in_res, stage_out_res, stage_in_depth):
159
+ coarse_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False)
160
+ coarse_mesh.vertices = coarse_mesh.vertices[:, [1, 2, 0]]
161
+ suffix = f"_stage_{stage_in_res}to{stage_out_res}_input_coarse"
162
+ coarse_mesh.export(os.path.join(out_dir, f"{name}{suffix}_depth{stage_in_depth}.obj"))
163
+
164
+
165
+ 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):
166
+ # fix3 数据通路:coord(粗层条件, 含 offset) + 采样得到的 refine 子码(local[0,256)) ->
167
+ # subcodes_to_flat_token 还原成扁平 60/face token(含 offset) -> codec.decode_tokens 解成 mesh。
168
+ # 与 acc_v2_fix3.fix3_generate 完全一致;不再做旧 18/face 的 coord/refine 交错。
169
+ flat = subcodes_to_flat_token(coord_seq, gen_subcodes, codec_cfg)
170
+ if not flat:
171
+ return None
172
+
173
+ mesh = codec.decode_tokens(flat, target_depth=target_depth - 1)
174
+ if mesh is None:
175
+ return None
176
+
177
+ # 先剔除退化面(三顶点存在重合的零面积面),再做 normals/holes 修复,避免退化面干扰后续几何修复。
178
+ if remove_degenerate:
179
+ mesh = remove_degenerate_faces(mesh)
180
+
181
+ trimesh.repair.fix_normals(mesh)
182
+ if fix_holes:
183
+ verts = torch.tensor(mesh.vertices, device="cuda", dtype=torch.float32)
184
+ faces = torch.tensor(mesh.faces, device="cuda", dtype=torch.int32)
185
+ new_verts, new_faces = _fill_holes(verts, faces, debug=False, verbose=False, only_fill_holes=True)
186
+ mesh = trimesh.Trimesh(vertices=new_verts.cpu().numpy(), faces=new_faces.cpu().numpy())
187
+
188
+ return_mesh = mesh.copy()
189
+ mesh.vertices = mesh.vertices[:, [1, 2, 0]]
190
+ mesh.export(os.path.join(out_dir, f"{name}{suffix}_depth{target_depth}.obj"))
191
+
192
+ if raw_dis_mesh is not None:
193
+ raw_dis_mesh.vertices = raw_dis_mesh.vertices[:, [1, 2, 0]]
194
+ raw_dis_mesh.export(os.path.join(out_dir, f"{name}{suffix}_raw_dis_depth{target_depth-1}.obj"))
195
+ if gt_dis_mesh is not None:
196
+ gt_dis_mesh.vertices = gt_dis_mesh.vertices[:, [1, 2, 0]]
197
+ gt_dis_mesh.export(os.path.join(out_dir, f"{name}{suffix}_gt_dis_depth{target_depth}.obj"))
198
+ return return_mesh
199
+
200
+
201
+ def _decode_subcodes_to_mesh(codec, coord_seq, gen_subcodes, codec_cfg, target_depth):
202
+ """与 save_stage_prediction 同款 decode(不 fix_normals/fix_holes/不导出),用于 best-of-N 打分。"""
203
+ flat = subcodes_to_flat_token(coord_seq, gen_subcodes, codec_cfg)
204
+ if not flat:
205
+ return None
206
+ return codec.decode_tokens(flat, target_depth=target_depth - 1)
207
+
208
+
209
+ def _pc_chamfer_score(mesh, pc_xyz, n_sample=4096):
210
+ """候选 mesh 表面采样点 与 条件点云 pc_xyz 的对称 chamfer(越小越贴合目标点云)。
211
+ 两者同处归一化 frame(mul_encode_mesh 内 normalize + decode 反离散化),可直接算。
212
+ 退化面(零面积)不会被 sample_surface 采到 -> 天然只评有效曲面;无有效面返回 inf。"""
213
+ if mesh is None or len(getattr(mesh, "faces", [])) == 0 or len(mesh.vertices) == 0:
214
+ return float("inf")
215
+ try:
216
+ pts, _ = trimesh.sample.sample_surface(mesh, n_sample)
217
+ pts = np.asarray(pts)
218
+ except Exception:
219
+ pts = np.asarray(mesh.vertices)
220
+ if pts.shape[0] == 0:
221
+ return float("inf")
222
+ pts = torch.as_tensor(pts, dtype=torch.float32, device=pc_xyz.device)
223
+ ref = pc_xyz
224
+ if ref.shape[0] > n_sample:
225
+ idx = torch.randperm(ref.shape[0], device=ref.device)[:n_sample]
226
+ ref = ref[idx]
227
+ d = torch.cdist(pts, ref) # [Np, Nr]
228
+ return float((d.min(1).values.mean() + d.min(0).values.mean()).item())
229
+
230
+
231
+ def remove_degenerate_faces(mesh):
232
+ """删除退化面:三角形三个顶点中存在重合顶点(任意两顶点坐标相同 -> 零面积/退化)。
233
+ 按顶点坐标判定(而非仅 index),因为 octree 解码后不同 index 可能落到同一离散坐标。
234
+ 返回过滤后的新 mesh;若没有退化面则原样返回;若全部退化则返回原 mesh(避免产出空 mesh)。"""
235
+ if mesh is None or len(getattr(mesh, "faces", [])) == 0 or len(getattr(mesh, "vertices", [])) == 0:
236
+ return mesh
237
+ faces = np.asarray(mesh.faces)
238
+ verts = np.asarray(mesh.vertices)
239
+ v0, v1, v2 = verts[faces[:, 0]], verts[faces[:, 1]], verts[faces[:, 2]]
240
+ # 三条边都非零长度(三顶点两两不重合)才保留
241
+ keep = (
242
+ np.any(v0 != v1, axis=1)
243
+ & np.any(v1 != v2, axis=1)
244
+ & np.any(v0 != v2, axis=1)
245
+ )
246
+ if keep.all():
247
+ return mesh
248
+ if not keep.any():
249
+ return mesh
250
+ return trimesh.Trimesh(vertices=verts.copy(), faces=faces[keep].copy(), process=False)
251
+
252
+
253
+ def process_one_mesh(
254
+ raw_mesh_path,
255
+ gt_mesh_path,
256
+ model,
257
+ codec,
258
+ codec_cfg,
259
+ device,
260
+ pad_id,
261
+ config,
262
+ output_folder,
263
+ stage_resolutions,
264
+ fix_holes=True,
265
+ use_gt_pc=True,
266
+ remove_degenerate=True,
267
+ ):
268
+ import time
269
+ t_start = time.time()
270
+ import time
271
+ t_total_start = time.time()
272
+
273
+ name = Path(raw_mesh_path).stem
274
+ t_start = time.time()
275
+ mesh = trimesh.load(raw_mesh_path, force="mesh")
276
+ mesh.vertices = mesh.vertices[:, [2, 0, 1]]
277
+ gt_mesh = trimesh.load(gt_mesh_path, force="mesh")
278
+ gt_mesh.vertices = gt_mesh.vertices[:, [2, 0, 1]]
279
+ normalize(gt_mesh)
280
+ normalize(mesh)
281
+ save_raw_gt_mesh(mesh, output_folder, name)
282
+ save_gt_mesh(gt_mesh, output_folder, name)
283
+ t_load = time.time()
284
+ print(f"[{name}] Load & normalize time: {t_load - t_start:.4f}s")
285
+
286
+ stage_depths = [_depth_from_resolution(x) for x in stage_resolutions]
287
+
288
+ # First-stage input uses codec encode-decode result at the minimum resolution.
289
+ min_stage_depth = stage_depths[0]
290
+ t_start = time.time()
291
+ init_raw_token, _, init_quantized_mesh = codec.encode_mesh(mesh, target_depth=min_stage_depth)
292
+ init_mesh = codec.decode_raw_tokens(init_raw_token, target_depth=min_stage_depth)
293
+ t_init = time.time()
294
+ print(f"[{name}] Init encode & decode (depth={min_stage_depth}): {t_init - t_start:.4f}s")
295
+
296
+ if init_mesh is not None:
297
+ current_mesh = init_mesh
298
+ elif init_quantized_mesh is not None:
299
+ current_mesh = init_quantized_mesh
300
+ else:
301
+ current_mesh = mesh.copy()
302
+
303
+ for stage_idx in range(len(stage_depths) - 1):
304
+ stage_start_time = time.time()
305
+ stage_in_depth = stage_depths[stage_idx]
306
+ stage_out_depth = stage_depths[stage_idx + 1]
307
+ stage_in_res = stage_resolutions[stage_idx]
308
+ stage_out_res = stage_resolutions[stage_idx + 1]
309
+
310
+ save_stage_input_mesh(
311
+ mesh=current_mesh,
312
+ out_dir=output_folder,
313
+ name=name,
314
+ stage_in_res=stage_in_res,
315
+ stage_out_res=stage_out_res,
316
+ stage_in_depth=stage_in_depth,
317
+ )
318
+
319
+ t_start = time.time()
320
+ stage_data = mul_encode_mesh(
321
+ raw_mesh=current_mesh,
322
+ gt_mesh=gt_mesh,
323
+ codec=codec,
324
+ tokenizer_target_depth=stage_in_depth,
325
+ pc_num=config.data_processing.pc_num,
326
+ total_pc_num=config.data_processing.total_pc_num,
327
+ use_gt_pc=use_gt_pc,
328
+ clean=False
329
+ )
330
+ t_mul_encode = time.time()
331
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | mul_encode_mesh time: {t_mul_encode - t_start:.4f}s")
332
+
333
+ pc = stage_data["pc"].unsqueeze(0).to(device)
334
+
335
+ # fix3 数据通路(与 acc_v2_fix3.fix3_generate 一致):
336
+ # 扁平 60/face token -> 取 coord(粗层条件) -> 因子分解采样 refine 子码。
337
+ # 这里只用 coord 作为条件,refine 子码全部由模型 sample 生成(GT refine 不参与推理)。
338
+ coord_list, _ = split_fix3_token_list([stage_data["token"]], pad_id, device, codec_cfg)
339
+ depth_t = torch.tensor([stage_in_depth], device=device, dtype=torch.long)
340
+
341
+ cfg_scale = float(getattr(config.evaluation, "cfg_scale", 1.0))
342
+ # CFG ramp:linear 时每级采样的最初几步用低 cfg(cfg_start),按步线性升到 cfg_scale,
343
+ # 避免高 cfg 在早期步把没把握的位置置信度抬高、过早 commit。constant=旧行为。
344
+ cfg_schedule = str(getattr(config.evaluation, "cfg_schedule", "constant"))
345
+ cfg_start = float(getattr(config.evaluation, "cfg_start", 1.0))
346
+ best_of_n = int(getattr(config.evaluation, "best_of_n", 1))
347
+ # best-of-N 对抗级联累积误差:每级采 N 个候选 -> 各自 decode -> 选 chamfer-to-pc 最小者(最贴合
348
+ # 目标点云)作为本级输出喂下一级。点云 pc 是跨级不漂移的全局锚,用它选优能降低单级错误率、抑制误差累积。
349
+ # N=1 退化为单次贪心采样(确定性, 行为同前);N>1 需 temperature>0 才能采出多样候选。
350
+ sample_temp = float(getattr(config.evaluation, "sample_temperature", 1.0)) if best_of_n > 1 else 0.0
351
+ pc_xyz = stage_data["pc"][:, :3].to(device).float()
352
+
353
+ t_start = time.time()
354
+ gen_subcodes, best_score = None, float("inf")
355
+ for cand in range(max(1, best_of_n)):
356
+ gs = model.sample(
357
+ coord_list,
358
+ depth_t,
359
+ pc=pc,
360
+ T_infer=config.evaluation.diffusion_sample_steps,
361
+ cfg_scale=cfg_scale,
362
+ temperature=sample_temp,
363
+ cfg_schedule=cfg_schedule,
364
+ cfg_start=cfg_start,
365
+ )
366
+ if best_of_n <= 1:
367
+ gen_subcodes = gs
368
+ break
369
+ cand_mesh = _decode_subcodes_to_mesh(codec, coord_list[0], gs[0], codec_cfg, stage_out_depth)
370
+ score = _pc_chamfer_score(cand_mesh, pc_xyz)
371
+ if score < best_score:
372
+ best_score, gen_subcodes = score, gs
373
+ torch.cuda.synchronize()
374
+ t_sample = time.time()
375
+ if best_of_n > 1:
376
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | best-of-{best_of_n} (T={sample_temp}, cfg={cfg_scale}) "
377
+ f"chamfer={best_score:.6f} | sample time: {t_sample - t_start:.4f}s")
378
+ else:
379
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | factorized sample time: {t_sample - t_start:.4f}s")
380
+
381
+ t_start = time.time()
382
+ stage_suffix = f"_stage_{stage_in_res}to{stage_out_res}"
383
+ stage_mesh = save_stage_prediction(
384
+ codec=codec,
385
+ coord_seq=coord_list[0],
386
+ gen_subcodes=gen_subcodes[0],
387
+ codec_cfg=codec_cfg,
388
+ out_dir=output_folder,
389
+ name=name,
390
+ target_depth=stage_out_depth,
391
+ suffix=stage_suffix,
392
+ fix_holes=fix_holes,
393
+ raw_dis_mesh=stage_data.get("raw_dis_mesh"),
394
+ gt_dis_mesh=stage_data.get("gt_dis_mesh"),
395
+ remove_degenerate=remove_degenerate,
396
+ )
397
+ t_save_pred = time.time()
398
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | save_stage_prediction (decode + fix) time: {t_save_pred - t_start:.4f}s")
399
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | total stage time: {time.time() - stage_start_time:.4f}s\n")
400
+
401
+ if stage_mesh is None:
402
+ break
403
+
404
+ current_mesh = stage_mesh
405
+
406
+ print(f"[{name}] Total process_one_mesh time: {time.time() - t_total_start:.4f}s")
407
+
408
+
409
+ class EMA:
410
+ def __init__(self, model, decay=0.999):
411
+ self.decay = decay
412
+ self.shadow = {}
413
+ self.register(model)
414
+
415
+ def register(self, model):
416
+ self.shadow = {}
417
+ for name, param in model.named_parameters():
418
+ if param.requires_grad:
419
+ self.shadow[name] = param.data.clone()
420
+
421
+ def load_state_dict(self, state):
422
+ self.decay = state.get("decay", self.decay)
423
+ self.shadow = {k: v.clone() for k, v in state["shadow"].items()}
424
+
425
+ @torch.no_grad()
426
+ def copy_to(self, model):
427
+ """Swap shadow weights into the model in-place (for inference)."""
428
+ copied, missing = 0, 0
429
+ for name, param in model.named_parameters():
430
+ if name in self.shadow:
431
+ param.data.copy_(self.shadow[name].to(device=param.device, dtype=param.dtype))
432
+ copied += 1
433
+ else:
434
+ missing += 1
435
+ print(f"[EMA.copy_to] copied {copied} params into model ({missing} not present in shadow)")
436
+
437
+
438
+ def run_inference_on_all_checkpoints(model, accelerator, config, device, pad_id, codec, codec_cfg, ema=None):
439
+ ckpt_dir = "ckpt_fix"
440
+ cfg_scale = float(getattr(config.evaluation, "cfg_scale", 1.0)) # 采样 CFG 强度(与细分循环 model.sample 一致)
441
+ output_folder = f"./pretrain_model_mul_more_data_1_to_64_eval_fix_easy2_cfg_{cfg_scale}_step_20"
442
+
443
+ # raw_data_txt = "dataset/bpt_res_list.txt" /root/junkai_lin/mul_res_diffusion/demo/bpt_retrain_gt/data.txt
444
+ # gt_data_txt = "dataset/trellis_clean_mesh.txt" /root/junkai_lin/mul_res_diffusion/demo/bpt_gt/data.txt
445
+ # raw_data_txt = "/root/dataset/unique_files_glb_under20000face_compress_glb_with_info_val.txt"
446
+ # gt_data_txt = "/root/dataset/unique_files_glb_under20000face_compress_glb_with_info_val.txt"
447
+ raw_data_txt = "/root/dataset/unique_files_glb_under6000face_2degree_30ratio_0.01_with_info_val.txt"
448
+ gt_data_txt = "/root/dataset/unique_files_glb_under6000face_2degree_30ratio_0.01_with_info_val.txt"
449
+
450
+ # raw_data_txt = "/root/junkai_lin/mul_res_diffusion/demo/hun_yuan/data.txt"
451
+ # gt_data_txt = "/root/junkai_lin/mul_res_diffusion/demo/hun_yuan/data.txt"
452
+ stage_resolutions = [1,2,4,8,16,32,64]
453
+ # raw_data_txt = "demo/tripo 1.0/data.txt"
454
+ # gt_data_txt = "demo/tripo 1.0/data.txt"
455
+
456
+ use_gt_pc=True
457
+ fix_holes=False
458
+ # 退化面删除开关:True=导出前剔除三顶点存在重合的零面积面。可在 config.evaluation.remove_degenerate_faces 覆盖。
459
+ remove_degenerate = bool(getattr(config.evaluation, "remove_degenerate_faces", True))
460
+ if accelerator.is_main_process:
461
+ print(f"Using raw data txt: {raw_data_txt}")
462
+ print(f"Using gt data txt: {gt_data_txt}")
463
+ print(f"Searching checkpoints in: {ckpt_dir}")
464
+
465
+ # 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)
466
+ mesh_pairs, missing_gt = build_raw_gt_pairs(raw_data_txt, gt_data_txt, max_faces=20000, min_faces=500)
467
+ if accelerator.is_main_process:
468
+ print(f"Matched raw/gt pairs: {len(mesh_pairs)}")
469
+ if missing_gt:
470
+ print(f"Raw without matched gt: {len(missing_gt)}")
471
+
472
+ search_pattern = os.path.join(ckpt_dir, "checkpoint_*_epoch_*_loss.pth")
473
+ ckpt_files = glob.glob(search_pattern)
474
+ if not ckpt_files:
475
+ if accelerator.is_main_process:
476
+ print("No checkpoints found!")
477
+ return
478
+
479
+ ckpt_data = []
480
+ for f in ckpt_files:
481
+ match = re.search(r"checkpoint_(\d+)_epoch_", f)
482
+ if match:
483
+ epoch = int(match.group(1))
484
+ ckpt_data.append((epoch, f))
485
+ ckpt_data.sort(key=lambda x: x[0])
486
+
487
+ # stage_resolutions = [32, 64, 128, 256]
488
+
489
+
490
+ # stage_resolutions = [128, 256]
491
+
492
+ for epoch, ckpt_path in ckpt_data:
493
+ if accelerator.is_main_process:
494
+ print(f"\n[{'='*40}]")
495
+ print(f"Starting inference for epoch {epoch}")
496
+ print(f"Loading weights from: {ckpt_path}")
497
+
498
+ output_folder_ply = os.path.join(output_folder, f"{epoch}_epoch_val_generate_mul")
499
+ if accelerator.is_main_process:
500
+ os.makedirs(output_folder_ply, exist_ok=True)
501
+
502
+ checkpoint = torch.load(ckpt_path, map_location=device)
503
+ model_to_load = accelerator.unwrap_model(model)
504
+ # 加载的是用「新模型代码(AR 连接头 + factorized)」训练出的 ckpt,必须 strict=True:
505
+ # key/shape 一旦不匹配立即报错,避免 strict=False 把 AR 连接头静默留成随机初始化 -> 输出垃圾还不报警。
506
+ # (只有训练脚本 acc_v2_fix3 从旧 Linear 头 ckpt 暖启动时才用 load_strict=false,推理这里不允许。)
507
+ model_to_load.load_state_dict(checkpoint["model_state_dict"], strict=True)
508
+
509
+ # Auto-detect EMA: if the ckpt has an `ema_state_dict`, swap those
510
+ # shadow weights into the model for inference. Otherwise fall back to
511
+ # the raw `model_state_dict` we already loaded above. No reliance on
512
+ # config `use_ema` / a separate EMA object — just check the ckpt.
513
+ ema_state = checkpoint.get("ema_state_dict")
514
+ shadow = ema_state.get("shadow") if isinstance(ema_state, dict) else None
515
+ if shadow:
516
+ with torch.no_grad():
517
+ copied, skipped = 0, 0
518
+ for name, param in model_to_load.named_parameters():
519
+ if name in shadow:
520
+ param.data.copy_(shadow[name].to(device=param.device, dtype=param.dtype))
521
+ copied += 1
522
+ else:
523
+ skipped += 1
524
+ if accelerator.is_main_process:
525
+ msg = f"[Epoch {epoch}] EMA shadow detected in ckpt -> applied to model ({copied} params"
526
+ if skipped:
527
+ msg += f", {skipped} model params not in shadow"
528
+ print(msg + ")")
529
+ else:
530
+ if accelerator.is_main_process:
531
+ print(f"[Epoch {epoch}] no EMA in ckpt -> running with raw model_state_dict weights.")
532
+
533
+ with torch.no_grad():
534
+ # 用 unwrap 后的网络做推理:FactorizedDiffusionTransformer.sample 定义在裸模型上,
535
+ # DDP 包装层没有 .sample 方法。
536
+ model_to_load.eval()
537
+ pbar = tqdm(
538
+ enumerate(mesh_pairs),
539
+ total=len(mesh_pairs),
540
+ desc=f"Infer Epoch {epoch}",
541
+ disable=not accelerator.is_local_main_process,
542
+ dynamic_ncols=True,
543
+ )
544
+ for line_idx, (raw_mesh_path, gt_mesh_path) in pbar:
545
+ if line_idx % accelerator.num_processes != accelerator.process_index:
546
+ continue
547
+ try:
548
+ process_one_mesh(
549
+ raw_mesh_path=raw_mesh_path,
550
+ gt_mesh_path=gt_mesh_path,
551
+ model=model_to_load,
552
+ codec=codec,
553
+ codec_cfg=codec_cfg,
554
+ device=device,
555
+ pad_id=pad_id,
556
+ config=config,
557
+ output_folder=output_folder_ply,
558
+ stage_resolutions=stage_resolutions,
559
+ fix_holes=fix_holes,
560
+ use_gt_pc=use_gt_pc,
561
+ remove_degenerate=remove_degenerate,
562
+ )
563
+ except Exception as e:
564
+ print(f"[infer error] raw={raw_mesh_path}, gt={gt_mesh_path}: {str(e)}")
565
+
566
+ torch.cuda.empty_cache()
567
+ gc.collect()
568
+
569
+ if accelerator.is_main_process:
570
+ print("\nBatch inference complete for all checkpoints.")
571
+
572
+
573
+ def main(config):
574
+ print("start init Accelerator...")
575
+ ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True)
576
+ accelerator = Accelerator(
577
+ gradient_accumulation_steps=config.accelerator.gradient_accumulation_steps,
578
+ mixed_precision=config.accelerator.mixed_precision,
579
+ kwargs_handlers=[ddp_kwargs],
580
+ )
581
+ device = accelerator.device
582
+
583
+ codec = MeshCodec(args=config.tokenizer)
584
+ codec_cfg = codec.get_model_config()
585
+ input_num_categories = codec_cfg["input_num_categories"] # coord 词表 = resolution + 1 (含 offset)
586
+ pad_id = codec_cfg["pad_token_id"]
587
+
588
+ # fix3 因子分解模型(与 acc_v2_fix3 训练时一致的构造方式)。
589
+ # rotary 只需覆盖「模型位置」(9/face),用 config.model.max_seq_len(回退到 max_len)。
590
+ model_max_seq_len = int(getattr(config.model, "max_seq_len", config.data_processing.max_len))
591
+ model = FactorizedDiffusionTransformer(
592
+ coord_vocab_size=input_num_categories,
593
+ max_seq_len=model_max_seq_len,
594
+ d_model=config.model.feature_dim,
595
+ nhead=config.model.num_heads,
596
+ depth=config.model.hourglass_depth,
597
+ dropout=config.model.dropout,
598
+ encoder_freeze=getattr(config.model, "encoder_freeze", True),
599
+ use_pc=True,
600
+ time_embed_scale=config.training.diffusion_train_steps,
601
+ nonempty_row_weight=float(getattr(config.training, "nonempty_row_weight", 1.0)), # 与 acc_v2_fix3 构造一致(推理不算 loss, 仅对齐)
602
+ )
603
+
604
+ use_ema = getattr(config.training, "use_ema", False)
605
+ ema_decay = getattr(config.training, "ema_decay", 0.999)
606
+ ema = EMA(model, decay=ema_decay) if use_ema else None
607
+
608
+ model = accelerator.prepare(model)
609
+
610
+ run_inference_on_all_checkpoints(
611
+ model=model,
612
+ accelerator=accelerator,
613
+ config=config,
614
+ device=device,
615
+ pad_id=pad_id,
616
+ codec=codec,
617
+ codec_cfg=codec_cfg,
618
+ ema=ema,
619
+ )
620
+
621
+
622
+ if __name__ == "__main__":
623
+ set_seed(42)
624
+ config = load_config()
625
+ main(config)
mulres_infer/modules/pipeline.py ADDED
@@ -0,0 +1,964 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """多级"从零生成": res2 -> 4 -> 8 -> ... -> 目标分辨率 的级联生成。
2
+
3
+ 基于 infer_remask_geom_altpc.py 的级联骨架, 但用 infer_single_layer.py 的干净基建 + 新 model.sample
4
+ (align_fm / align_inter / align_per_step)。**不含 remask/requery**(按需求省去), 只保留:
5
+ - 从最粗分辨率 STAGE[0] 起(GT 八叉树解码作粗起点), 每一级把"上一级预测的 mesh"当输入喂下一级(级联累积);
6
+ - 每级 model.sample 细分一层, 可选末步跨面一致性对齐(F/intra/inter);
7
+ - 可选 fill_small_holes 补小洞(不考虑法向)。
8
+
9
+ 与单级(infer_single_layer)的区别: 单级每级喂 GT@in_depth(teacher-forced, 无累积); 本脚本喂上一级预测(真实从零级联)。
10
+
11
+ ⚠ v2 口径(2026-08-17, 实测校准): 所有文件名 = 内容的**真实体素格**(顶点格步实测 2/res)。
12
+ - fix3: 深度 d 的完整表示真实格 = 2^(d+1); 一个级联 stage = "在深度 d 上把占用补全成格 2^(d+1) 的 mesh",
13
+ 分辨率翻倍发生在下一级重编码——所以 STAGE 每项即该级完成的格, fix3 终点 = 格 32(深度4)。
14
+ - acc_uni 细分器: 深度 D 真实格 = 2^D(与 fix3 差一档!), 细分终点 SUPERRES_TO=512 = 深度9。
15
+ - 生成产物 res{r} 与 GT 天花板 gt_res{r} 同格同名, 可直接叠加; 起点若是 raw 占用块命名 raw_res{r}。
16
+ - 历史目录三代命名: 旧标签(≤08-14, 生成侧=真实格/GT侧=一半), 08-14~17(生成侧虚高一倍), v2(全对齐);
17
+ 新目录一律带 _v2 后缀防覆盖。塌缩自 08-17 起先按坐标焊接(WELD_DEC, 修 UV 接缝撕裂)。
18
+
19
+ 用法:
20
+ CUDA_VISIBLE_DEVICES=7 STAGE=2,4,8,16,32 DATASET=trellis NCASE=8 STEPS=20 \
21
+ GEOM_STAGES=8,16,32 SUPERRES=1 SUPERRES_TO=512 python infer_multilevel.py
22
+ """
23
+ import os, sys, glob as _glob, re as _re, time, math
24
+ from collections import defaultdict
25
+ import numpy as np, torch, trimesh
26
+ CONFIG = os.environ.get("CONFIG", "configs/generator.yaml")
27
+ sys.argv = ["x", "--config", CONFIG]
28
+ from utils.config import load_config
29
+ from dataset.mesh_dataset import mul_encode_mesh, sample_pc, dec
30
+ from dataset.mesh_dataset_altpc import sample_pc_with_degen
31
+ from utils.data_utils import normalize
32
+ from models.generator import REFINE_POS_PER_FACE, NUM_OCTANTS, MASK_ID, TYPE_F, TYPE_INTRA, TYPE_INTER
33
+ from models.generator import FactorizedDiffusionTransformerAltPC as BaseModel
34
+ from modules.octree_codec import MeshCodec as FastMeshCodec
35
+ import utils.distance as metric
36
+ from scipy.spatial import cKDTree
37
+ import importlib.util as _ilu
38
+ from utils.postprocessing import _fill_holes
39
+ _v2_spec = _ilu.spec_from_file_location("fix3_v2", os.path.join(os.path.dirname(os.path.abspath(__file__)), "octree_decode.py"))
40
+ fix3_v2 = _ilu.module_from_spec(_v2_spec); _v2_spec.loader.exec_module(fix3_v2)
41
+ decode_subcodes_to_mesh = fix3_v2._decode_subcodes_to_mesh
42
+ # acc_uni 高分辨率细分器(SUPERRES 用): model_acc + 旧 octree-codebook tokenizer(与 fix3 不同模型/tokenizer)
43
+ from models.subdivider import DiscreteDiffusionTransformer, sample_refine_tokens_with_topology
44
+ from modules.octree_codec_acc import MeshCodec as OctreeMeshCodec
45
+ import pyvista as pv
46
+ from tqdm import tqdm
47
+ TOKENS_PER_FACE = 60
48
+ STAGE = [int(x) for x in os.environ.get("STAGE", "2,4,8,16,32").split(",")] # 级联阶梯: 每项 = 该级"完成"的真实体素格
49
+ # (2026-08-17 v2 口径: 一级 = 在模型深度 d=log2(r)-1 上把占用补全成格 r 的完整 mesh; 翻倍发生在下一级重编码。
50
+ # fix3 深度 d 的表示真实格 = 2^(d+1); 生成产物文件 res{r} 与 GT 天花板 gt_res{r} 同格可直接对照)
51
+ DATASET = os.environ.get("DATASET", "trellis")
52
+ NCASE = int(os.environ.get("NCASE", "8")); CASE_STEM = os.environ.get("CASE_STEM", "")
53
+ STEPS = int(os.environ.get("STEPS", "20")); PCT_DEG = os.environ.get("PCT_DEG", "1") == "1"
54
+ ALIGN_FM = os.environ.get("ALIGN_FM", "0") == "1"
55
+ ALIGN_INTER = os.environ.get("ALIGN_INTER", "0") == "1"
56
+ ALIGN_PER_STEP = os.environ.get("ALIGN_PER_STEP", "0") == "1"
57
+ FILL = os.environ.get("FILL", "0") == "1"; FILL_MAX_SIDES = int(os.environ.get("FILL_MAX_SIDES", "4"))
58
+ FILL_MIN_RES = int(os.environ.get("FILL_MIN_RES", "0")) # >0: FILL=1 的级联内补洞只在"产物真实格 >= 此值"的级做
59
+ # (默认 0 = 历史行为 stage_i>=4, 在 STAGE=2..64 下即 res32/res64 两级)
60
+ # 例: FILL_MIN_RES=64 -> 只在 res64 补, res32 不补
61
+ # method=fill: 只用直接补洞(_fill_holes)做级联修复, 在这些"输出分辨率"阶段补洞后喂下一级(默认 8/16/32/64)
62
+ FILL_STAGES = set(int(x) for x in os.environ.get("FILL_STAGES", "8,16,32").split(",") if x.strip()) # 按产物真实格
63
+ SAVE = os.environ.get("SAVE", "1") == "1"
64
+ SAVE_GT_LEVELS = os.environ.get("SAVE_GT_LEVELS", "1") == "1" # 每 case 导出 GT 经 tokenizer 逐分辨率编码->解码的天花板(gt_res{N}.obj, 真实res命名, 含退化面), 用户要求所有多级生成默认带
65
+ SAVE_PC_LEVELS = os.environ.get("SAVE_PC_LEVELS", "1") == "1" # 每级导出实际注入的多级点云 pc_target_res{N}.ply(云B, 该级条件) + pc_full.ply(云A, 全分辨率GT云), 用户要求默认带
66
+ EMPTY_BIAS = float(os.environ.get("EMPTY_BIAS", "0")) # >0: 连接AR头空字节类(0=无连接)logit+bias -> 稀疏化控密度(移植自 infer_remask_geom_altpc, δ=0.5 历史面数比→1.10); requery 补洞期间自动关
67
+ EB_ACTIVE = [True]
68
+ QUAD_FILL = os.environ.get("QUAD_FILL", "0") == "1" # 每级产物确定性微补洞: 开边图长度3/4简单环(4须双对角线缺失+bbox≤2格)补1-2三角。
69
+ # 数据背书: GT天花板"无对角线四边洞"=0(20case×res8/16/32), 误补风险极低; 修在级间阻断洞传播
70
+ QUAD_FILL_STATS = [0, 0] # [补的三角洞数, 补的四边洞数]
71
+ SAVE_REMASK_VIS = os.environ.get("SAVE_REMASK_VIS", "0") == "1" # geom 级导出可视化: *_remask_targets.ply(重采目标区=开边1-ring, 橙) / *_remask_changed.ply(重采后实际改变的面, 红)
72
+
73
+
74
+ def _face_keys(mesh):
75
+ v = np.round(np.asarray(mesh.vertices), 6)
76
+ return [tuple(sorted(map(tuple, v[f]))) for f in np.asarray(mesh.faces)]
77
+
78
+
79
+ def save_remask_vis(pre_mesh, post_mesh, out_prefix):
80
+ """pre=修补前解码, post=修补后。targets: pre 上开边顶点 1-ring 面(≈build_remask目标)橙色;
81
+ changed: post 上'pre 中不存在'的面红色。PLY 带面色, y-up 帧与 obj 一致。"""
82
+ GRAY, ORANGE, RED = [200, 200, 200, 255], [255, 140, 0, 255], [220, 30, 30, 255]
83
+ def yup(m):
84
+ mm = m.copy(); mm.vertices = mm.vertices[:, [1, 2, 0]]; return mm
85
+ # targets on pre
86
+ v = np.asarray(pre_mesh.vertices); f = np.asarray(pre_mesh.faces)
87
+ key = np.round(v, 6); uniq, inv = np.unique(key, axis=0, return_inverse=True)
88
+ f2 = inv[f]
89
+ fg = f2[(f2[:, 0] != f2[:, 1]) & (f2[:, 1] != f2[:, 2]) & (f2[:, 0] != f2[:, 2])]
90
+ e = np.sort(np.vstack([fg[:, [0, 1]], fg[:, [1, 2]], fg[:, [0, 2]]]), axis=1)
91
+ ue, cnt = np.unique(e, axis=0, return_counts=True)
92
+ open_verts = set(ue[cnt == 1].reshape(-1).tolist())
93
+ tmask = np.array([any(x in open_verts for x in row) for row in f2])
94
+ mp = yup(pre_mesh); mp.visual.face_colors = np.where(tmask[:, None], ORANGE, GRAY)
95
+ mp.export(out_prefix + "_remask_targets.ply")
96
+ # changed on post
97
+ pre_keys = set(_face_keys(pre_mesh))
98
+ cmask = np.array([k not in pre_keys for k in _face_keys(post_mesh)])
99
+ mq = yup(post_mesh); mq.visual.face_colors = np.where(cmask[:, None], RED, GRAY)
100
+ mq.export(out_prefix + "_remask_changed.ply")
101
+ print(f" [REMASK_VIS] {os.path.basename(out_prefix)}: 目标面 {int(tmask.sum())}/{len(tmask)}, 改变面 {int(cmask.sum())}/{len(cmask)}")
102
+
103
+
104
+ def quad_fill_mesh(mesh, res):
105
+ """长度3/4边界环确定性补洞(见 QUAD_FILL 注释)。返回(焊接后的)新 mesh。"""
106
+ v = np.asarray(mesh.vertices); f = np.asarray(mesh.faces)
107
+ key = np.round(v, 6); uniq, first_idx, inv = np.unique(key, axis=0, return_index=True, return_inverse=True)
108
+ f2 = inv[f]; f2 = f2[(f2[:, 0] != f2[:, 1]) & (f2[:, 1] != f2[:, 2]) & (f2[:, 0] != f2[:, 2])] # 图只用正常面; 输出保留原mesh全部面(含退化载体)
109
+ e = np.sort(np.vstack([f2[:, [0, 1]], f2[:, [1, 2]], f2[:, [0, 2]]]), axis=1)
110
+ ue, cnt = np.unique(e, axis=0, return_counts=True)
111
+ all_edges = set(map(tuple, ue))
112
+ open_e = ue[cnt == 1]
113
+ from collections import defaultdict
114
+ adj = defaultdict(set)
115
+ for a, b in open_e:
116
+ adj[int(a)].add(int(b)); adj[int(b)].add(int(a))
117
+ new_faces = []; seen = set(); step = 2.0 / res
118
+ for s in list(adj):
119
+ if s in seen:
120
+ continue
121
+ comp = [s]; seen.add(s); st = [s]
122
+ while st:
123
+ x = st.pop()
124
+ for y in adj[x]:
125
+ if y not in seen:
126
+ seen.add(y); comp.append(y); st.append(y)
127
+ n = len(comp); ne = sum(len(adj[x]) for x in comp) // 2
128
+ if not (ne == n and all(len(adj[x]) == 2 for x in comp)):
129
+ continue # 非简单环不碰
130
+ if n == 3:
131
+ new_faces.append(tuple(comp)); QUAD_FILL_STATS[0] += 1
132
+ elif n == 4:
133
+ ring = [comp[0]]; prev = None
134
+ while len(ring) < 4:
135
+ nxt = [y for y in adj[ring[-1]] if y != prev and y in comp]
136
+ prev = ring[-1]; ring.append(nxt[0])
137
+ d1 = (min(ring[0], ring[2]), max(ring[0], ring[2]))
138
+ d2 = (min(ring[1], ring[3]), max(ring[1], ring[3]))
139
+ if d1 in all_edges or d2 in all_edges:
140
+ continue # 已有对角线(半填充/薄结构合法边界)不碰
141
+ if (uniq[comp].max(0) - uniq[comp].min(0)).max() > 2 * step + 1e-6:
142
+ continue # 跨区大环不碰(只修 raw face 内量级)
143
+ if np.linalg.norm(uniq[ring[0]] - uniq[ring[2]]) <= np.linalg.norm(uniq[ring[1]] - uniq[ring[3]]):
144
+ new_faces += [(ring[0], ring[1], ring[2]), (ring[0], ring[2], ring[3])]
145
+ else:
146
+ new_faces += [(ring[1], ring[2], ring[3]), (ring[1], ring[3], ring[0])]
147
+ QUAD_FILL_STATS[1] += 1
148
+ if not new_faces:
149
+ return mesh
150
+ nf = first_idx[np.array(new_faces)] # 焊接索引 -> 原顶点索引; 原 mesh 零改动只追加
151
+ return trimesh.Trimesh(vertices=v, faces=np.vstack([f, nf]), process=False)
152
+ DECIMATE_RATIO = float(os.environ.get("DECIMATE_RATIO", "0.5")) # GT 预塌缩比例(pyvista decimate 目标削减比, 0=不塌缩)
153
+ DEC_FILL = os.environ.get("DEC_FILL", "0") == "1" # 塌缩后先补洞(TRELLIS _fill_holes only_fill_holes=pymeshfix)再作输入(条件PC/天花板/粗起点); 指标参照仍用未补洞塌缩GT, 与基线可比
154
+ # 级间是否按预测 mesh 自身 bbox 重新归一化(2026-08-22 修复, 默认关): 级间 mesh 是上一级解码产物,
155
+ # 天生在规范 [-1,1] 帧; 重归一化在预测带极值杂散面时把脚手架帧拉离 GT/点云帧 -> z 轴撕裂
156
+ # (1(10)/1(101): res4 顶层 3-4% 杂散面 -> z中心+0.25/缩放4/3 -> 本体父层劈成 {-0.75,+0.25} 中间断层,
157
+ # res8 出现 -0.625 悬空板 + -0.375/-0.125 全空, 级联放大成面数腰斩)。=1 复现历史(≤08-22)行为。
158
+ CASCADE_RENORM = os.environ.get("CASCADE_RENORM", "0") == "1"
159
+ # 每级采样方法(可多选逗号): plain / consensus(align) / log / log_fmm / geom / geom_fmm / fill。ALIGN_* 仍对 plain 生效(向后兼容)。
160
+ METHODS = [m.strip() for m in os.environ.get("METHODS", "plain").split(",") if m.strip()]
161
+ MULTI_PC = os.environ.get("MULTI_PC", "1") == "1" # 0=单点云(pc_target=None, focal 单PC ckpt 必须)
162
+ SINGLE_TARGET_PC = os.environ.get("SINGLE_TARGET_PC", "0") == "1" # 1=云B作唯一云每层恒注入(sfocal 线口径); 需 MULTI_PC=1 以构建云B
163
+ NITER_GEOM = int(os.environ.get("NITER_GEOM", "3")) # geom: 每级终点 remask+requery 轮数
164
+ KREQ_GEOM = int(os.environ.get("KREQ_GEOM", "5")) # geom: requery 每轮步数(5=甜点, 质量≈10 快~3x)
165
+ MAXFACE_REMASK = int(os.environ.get("MAXFACE", "1200")) # geom: 每轮最多重掩多少 face 的连接
166
+ REMASK_UNION = os.environ.get("REMASK_UNION", "1") == "1" # geom: 开边两父 token 取并集(1-ring)
167
+ REMASK_LOOP_MAX = int(os.environ.get("REMASK_LOOP_MAX", "0")) # >0: 只对边界环长<=K的开边做 remask(外科修小洞; 0=全部开边)
168
+ REMASK_GROUPS = int(os.environ.get("REMASK_GROUPS", "1")) # >1: 每轮候选面按 BFS 序分 G 批逐批 requery(后批看得到前批修复, 用户方案); 1=整轮一次性
169
+ RUN_SEED = int(os.environ.get("RUN_SEED", "0")) # >0: 整体偏移随机种子, 用于"同配置多次独立采样各自保存"(看运行间方差); 0=与历史逐位一致
170
+ BO_N = int(os.environ.get("BO_N", "1")) # >1: 级内 best-of-N——重采条件云B+换torch种子造候选(实测±40开边方差来源), 结构惩罚(开边+10*NM%)选优
171
+ BO_STAGES = set(int(x) for x in os.environ.get("BO_STAGES", "32").split(",") if x.strip()) # 只在这些产物格做 best-of-N
172
+ REQUERY_EMPTY_BIAS = float(os.environ.get("REQUERY_EMPTY_BIAS", "0")) # requery(缺陷区重问)期的空行偏置: 负值="不许再答无连接"推向补上; 0=关(现状)
173
+ GEOM_STAGE_PARAMS = {} # 按产物格覆盖 remask 轮数x步数, 如 "32:6x10"(res32 用 6x10, 其余用方法默认); 扫参结论: res8/16 甜点 3x5, res32 甜点 6x10
174
+ for _kv in (x for x in os.environ.get("GEOM_STAGE_PARAMS", "").split(",") if x.strip()):
175
+ _r, _p = _kv.split(":"); _a, _b = _p.lower().split("x")
176
+ GEOM_STAGE_PARAMS[int(_r)] = (int(_a), int(_b))
177
+ REMASK_DEGEN = os.environ.get("REMASK_DEGEN", "0") == "1" # geom: 额外纳入"有退化三角的 raw face"一起 remask
178
+ REMASK_NM = os.environ.get("REMASK_NM", "0") == "1" # geom: 非流形边(覆盖>2)的 1-ring face 也纳入 remask+requery。
179
+ # 实测(val20@res32 A/B, ab_remask_nm/)净负: 开边131->171/非流形12.9->14.5%双变差, 调大MAXFACE无关(cap不触顶)、
180
+ # 加迭代不救; 模型对 NM 区域重采仍给过连接答案, 反在原闭合处造新开边 -> 默认关, 仅留开关。
181
+ GEOM_STAGES = set(int(x) for x in os.environ.get("GEOM_STAGES", "").split(",") if x.strip()) # geom: 只在这些"产物真实格"级做 remask(空=每级都做)
182
+ GT_START_RES = int(os.environ.get("GT_START_RES", "0")) # >0: 用 GT 编码->解码天花板@该真实分辨率作级联起点, 跳过更粗级(暴露偏差上界实验); 须在 STAGE 中
183
+ EXT_START_DIR = os.environ.get("EXT_START_DIR", "") # 非空: 外部预测 mesh 作级联起点, 文件 {case_stem}{EXT_START_SUFFIX}, 保存帧(与 gt_original.obj 同帧), 只跑有该文件的 case
184
+ EXT_START_RES = int(os.environ.get("EXT_START_RES", "32")) # 外部起点的真实分辨率(须在 STAGE 中)
185
+ EXT_START_SUFFIX = os.environ.get("EXT_START_SUFFIX", "_pred.obj")
186
+ assert not (GT_START_RES and EXT_START_DIR), "GT_START_RES 与 EXT_START_DIR 互斥"
187
+ _START_RES = GT_START_RES or (EXT_START_RES if EXT_START_DIR else 0)
188
+ # 起点(GT天花板/外部预测)已是"完成的格 _START_RES mesh" -> 只跑更细的级(v2 语义: 不再重做起点那一级)
189
+ STAGE_RUN = [r for r in STAGE if r > _START_RES] if _START_RES else STAGE # 实际执行的阶梯(每项=完成该格)
190
+ OFF = np.stack([(np.arange(8) >> k) & 1 for k in range(3)], 1).astype(np.int64) # [8,3]
191
+ TRI = np.array([(i, j, k) for i in range(24) for j in range(i + 1, 24) for k in range(j + 1, 24)]) # C(24,3)
192
+ POS = {(0, 0): 3, (1, 1): 4, (2, 2): 5, (0, 1): 6, (0, 2): 7, (1, 2): 8}
193
+ # ---- SUPERRES: fix3 级联到 STAGE[-1] 后, 用 acc_uni 细分器继续细分到更高分辨率 ----
194
+ SUPERRES = os.environ.get("SUPERRES", "0") == "1" # 是否开启更高分辨率细分
195
+ SUPERRES_TO = int(os.environ.get("SUPERRES_TO", "512")) # 细分终点真实格(从 STAGE[-1] 倍增: 32->64->128->256->512)
196
+ # acc_uni 与 fix3 口径不同: acc_uni 深度 D 的真实格 = 2^D(fix3 是 2^(d+1)); 细分用 ACC_UN._depth_from_resolution(log2)
197
+ SUPERRES_STEPS = int(os.environ.get("SUPERRES_STEPS", "10")); SUPERRES_CFG = float(os.environ.get("SUPERRES_CFG", "1.0"))
198
+ SUPERRES_FIX_HOLES = os.environ.get("SUPERRES_FIX_HOLES", "1") == "1"
199
+ SR_FILL_IN = os.environ.get("SR_FILL_IN", "1") == "1" # 细分入口先 pymeshfix 补洞(对齐旧 cross512_fillall: 粗输入进细分器前补洞)
200
+ SUPERRES_CONFIG = os.environ.get("SUPERRES_CONFIG", "configs/subdivider.yaml")
201
+ SUPERRES_CKPT = os.environ.get("SUPERRES_CKPT", os.path.join(os.path.dirname(os.path.abspath(__file__)), "ckpt", "subdivider.pth"))
202
+ FINAL_RES = SUPERRES_TO if SUPERRES else STAGE[-1]
203
+ NSAMP = int(os.environ.get("NSAMP", "1024")); TAU = 2.0 / FINAL_RES # v2: FINAL_RES=512 真实格 -> TAU=2/512, 数值与历史协议一致; NSAMP=1024 严格对齐 metric_mul_dense_mesh_v2
204
+
205
+ BASE_DIR = os.environ.get("BASE_DIR", "pretrained_model/gobjaverse_fix3_octant_factorized_1_to_1024_ce_altpc_decoded_deg_w1")
206
+ _ckpt_cands = sorted(_glob.glob(os.path.join(BASE_DIR, "checkpoint_*.pth")))
207
+ CKPT = os.environ.get("RAIL_CKPT") or (_ckpt_cands[-1] if _ckpt_cands else "")
208
+ if not CKPT or not os.path.exists(CKPT):
209
+ raise SystemExit(f"未找到生成器权重: 用 scripts/infer.py --ckpt <path> 指定, 或设环境变量 RAIL_CKPT\n"
210
+ f" (已查找: {os.path.join(BASE_DIR, 'checkpoint_*.pth')})")
211
+ _ep_m = _re.search(r"checkpoint_(\d+)_", os.path.basename(CKPT))
212
+ EPOCH = _ep_m.group(1) if _ep_m else os.path.splitext(os.path.basename(CKPT))[0] # 权重文件名不含 epoch 时用文件名作标签
213
+ _RUN_TAG = f"ep{EPOCH}" if _ep_m else EPOCH
214
+ TRELLIS = os.environ.get("TRELLIS_DIR", "") # 可选: 内置数据集快捷方式; 一般用 DATASET_DIR 指定任意目录
215
+ VAL = os.environ.get("VAL_LIST", "") # 可选: DATASET=val 时的列表文件
216
+ CAR = "1307d3253490441ab12f765c23fda2c5"
217
+ TOYS4K = os.environ.get("TOYS4K_DIR", "") # 可选: 域外玩具集
218
+ _tag = ("_fm" if ALIGN_FM else "") + ("_inter" if ALIGN_INTER else "") + ("_perstep" if ALIGN_PER_STEP else "") + ("_fill" if FILL else "") + (f"_sr{SUPERRES_TO}" if SUPERRES else "") + (f"_gtstart{GT_START_RES}" if GT_START_RES else "") + (f"_ext{EXT_START_RES}" if EXT_START_DIR else "") + ("_decfill" if DEC_FILL else "") + "_v2" # v2=2026-08-17 真实格命名+焊接塌缩口径, 防与历史目录同名覆盖
219
+ OUT = os.path.join(os.environ.get("OUT_ROOT") or os.path.dirname(CKPT),
220
+ f"multilevel_{_RUN_TAG}_res{FINAL_RES}_s{STEPS}_{DATASET}{_tag}") + os.environ.get("OUT_SUFFIX", "")
221
+ os.makedirs(OUT, exist_ok=True)
222
+
223
+
224
+ WELD_DEC = os.environ.get("WELD_DEC", "1") == "1" # 塌缩前按坐标焊接顶点。修根本bug: GLB 的 UV 接缝重复顶点
225
+ # 使 vtkQuadricDecimation 把接缝当边界、两侧独立塌缩 -> 沿接缝撕出
226
+ # 数千条真裂缝(1(11): 3699条 -> 焊接后 0)。0=旧撕裂行为(复现历史用)
227
+ def decimate_mesh(vertices, faces, simplify=True, simplify_ratio=0.9, verbose=True):
228
+ # Simplify
229
+ if simplify and simplify_ratio > 0:
230
+ if WELD_DEC:
231
+ _key = np.round(np.asarray(vertices, dtype=np.float64), 6)
232
+ _uniq, _inv = np.unique(_key, axis=0, return_inverse=True)
233
+ _f = _inv[np.asarray(faces)]
234
+ _f = _f[(_f[:, 0] != _f[:, 1]) & (_f[:, 1] != _f[:, 2]) & (_f[:, 0] != _f[:, 2])]
235
+ vertices, faces = _uniq, _f
236
+ mesh = pv.PolyData(vertices, np.concatenate([np.full((faces.shape[0], 1), 3), faces], axis=1))
237
+ mesh = mesh.decimate(simplify_ratio, progress_bar=verbose)
238
+ vertices, faces = mesh.points, mesh.faces.reshape(-1, 4)[:, 1:]
239
+ if verbose:
240
+ tqdm.write(f'After decimate: {vertices.shape[0]} vertices, {faces.shape[0]} faces')
241
+ return vertices, faces
242
+
243
+ def depth_from_resolution(res):
244
+ """真实体素分辨率 -> 八叉树深度(res = 2^(depth+1), 与训练/单级脚本口径一致)。
245
+ 旧口径(2026-08-14 前)是级别标签 res = 2^depth; 旧输出目录文件名按旧标签, 见文件头说明。"""
246
+ d = int(round(np.log2(res))) - 1
247
+ assert d >= 0 and 2 ** (d + 1) == res, f"res={res} 须为 >=2 的 2 的幂"
248
+ return d
249
+
250
+
251
+ def mesh_open_edge_stats(mesh):
252
+ """网格拓扑统计。返回 (开边数, 面数, 非流形边占比%)。
253
+ 先 merge_vertices 合并重合顶点再统计(否则补洞类输出的重复顶点会把开边/非流形虚高)。"""
254
+ if mesh is None or len(getattr(mesh, "faces", [])) == 0:
255
+ return -1, 0, 0.0
256
+ m = mesh.copy(); m.merge_vertices()
257
+ edges = np.sort(np.asarray(m.edges), axis=1)
258
+ _, counts = np.unique(edges, axis=0, return_counts=True)
259
+ return int((counts == 1).sum()), len(m.faces), float((counts > 2).mean() * 100)
260
+
261
+
262
+ def fill_small_holes(mesh, max_sides=4):
263
+ """补 <=max_sides 边的小闭环洞(**不考虑法向**, 直接扇形三角化连上)。返回 (新mesh, 补洞数)。"""
264
+ if mesh is None or len(getattr(mesh, "faces", [])) == 0:
265
+ return mesh, 0
266
+ verts = np.asarray(mesh.vertices); faces = np.asarray(mesh.faces)
267
+ directed_edges = np.concatenate([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]], axis=0)
268
+ undirected_key = np.sort(directed_edges, axis=1)
269
+ _, inverse, count = np.unique(undirected_key, axis=0, return_inverse=True, return_counts=True)
270
+ boundary_edges = directed_edges[count[inverse] == 1] # 无向边只被 1 面用 = 边界
271
+ if len(boundary_edges) == 0:
272
+ return mesh, 0
273
+ next_vertex = {} # a -> [b,...] 边界后继
274
+ for a, b in boundary_edges:
275
+ next_vertex.setdefault(int(a), []).append(int(b))
276
+ new_faces = []; n_filled = 0; visited = set()
277
+ for start in list(next_vertex.keys()):
278
+ if start in visited:
279
+ continue
280
+ loop = [start]; cur = start; closed = False
281
+ for _ in range(max_sides + 1):
282
+ outs = next_vertex.get(cur)
283
+ if not outs or len(outs) != 1: # 断裂 / 非流形边界顶点 -> 放弃该环
284
+ break
285
+ nb = outs[0]
286
+ if nb == start:
287
+ closed = True; break
288
+ if nb in loop: # 自交 -> 放弃
289
+ break
290
+ loop.append(nb); cur = nb
291
+ if closed and 3 <= len(loop) <= max_sides:
292
+ for v in loop:
293
+ visited.add(v)
294
+ v0 = loop[0]
295
+ for i in range(1, len(loop) - 1): # 扇形三角化(不考虑法向/绕序)
296
+ new_faces.append([v0, loop[i], loop[i + 1]])
297
+ n_filled += 1
298
+ if not new_faces:
299
+ return mesh, 0
300
+ faces_out = np.concatenate([faces, np.asarray(new_faces, dtype=faces.dtype)], axis=0)
301
+ return trimesh.Trimesh(vertices=verts, faces=faces_out, process=False), n_filled
302
+
303
+
304
+ def _trim_trailing_pad(token, pad_id):
305
+ pad_pos = (token == pad_id).nonzero(as_tuple=True)[0]
306
+ return token[:pad_pos[0].item()] if pad_pos.numel() > 0 else token
307
+
308
+
309
+ def split_fix3_token_list(token_list, pad_id, device, codec_cfg):
310
+ """扁平 60/face token -> (coord_list, subcode_list[9*nface,8])。位置 [F0..2, M00 M11 M22, M01 M02 M12]。"""
311
+ f_offset = codec_cfg["f_offset"]; intra_offset = codec_cfg["intra_row_offset"]; inter_offset = codec_cfg["inter_row_offset"]
312
+ coord_list, subcode_list = [], []
313
+ for token in token_list:
314
+ token = _trim_trailing_pad(token.to(device=device, dtype=torch.long), pad_id)
315
+ if token.numel() % TOKENS_PER_FACE != 0:
316
+ raise ValueError(f"token 长度 {token.numel()} 不是 {TOKENS_PER_FACE} 的倍数")
317
+ token = token.view(-1, TOKENS_PER_FACE); nface = token.size(0)
318
+ coord = token[:, :9].reshape(-1)
319
+ occupancy = token[:, 9:12] - f_offset
320
+ intra = (token[:, 12:36] - intra_offset).view(nface, 3, NUM_OCTANTS)
321
+ inter = (token[:, 36:60] - inter_offset).view(nface, 3, NUM_OCTANTS)
322
+ subcode = torch.zeros(nface, REFINE_POS_PER_FACE, NUM_OCTANTS, dtype=torch.long, device=device)
323
+ subcode[:, 0:3, 0] = occupancy; subcode[:, 3:6, :] = intra; subcode[:, 6:9, :] = inter
324
+ subcode_list.append(subcode.reshape(-1, NUM_OCTANTS)); coord_list.append(coord)
325
+ return coord_list, subcode_list
326
+
327
+
328
+ def _metric_normalize(mesh):
329
+ """metric_mul_dense_mesh_v2 协议: 每 mesh 独立归一化到 [-0.5, 0.5] 盒。"""
330
+ m = mesh.copy()
331
+ bounds = np.array([m.vertices.min(axis=0), m.vertices.max(axis=0)])
332
+ m.vertices = m.vertices - (bounds[0] + bounds[1])[None, :] / 2
333
+ m.vertices = m.vertices / (bounds[1] - bounds[0]).max()
334
+ m.vertices = m.vertices.clip(-0.5, 0.5)
335
+ return m
336
+
337
+
338
+ def chamfer_hd_fscore(pred_mesh, ref_mesh):
339
+ """返回 (CD-L2*1e3, HD*1e3, F@2voxel, |NC|)。
340
+ CD/HD/|NC| 按 metric_mul_dense_mesh_v2 协议: 每 mesh 独立归一化到 [-0.5,0.5] 后采样(带法向),
341
+ |NC| = 双向最近邻法向 |点积| 均值。F-score 沿用原 [-1,1] 管线帧 + TAU=4/真实FINAL_RES(=旧 2/标签res, 阈值数值与历史可比)。"""
342
+ if pred_mesh is None or ref_mesh is None or len(getattr(pred_mesh, "faces", [])) == 0:
343
+ return -1., -1., -1., -1.
344
+ ref_pts = np.asarray(trimesh.sample.sample_surface(ref_mesh, NSAMP)[0])
345
+ pred_pts = np.asarray(trimesh.sample.sample_surface(pred_mesh, NSAMP)[0])
346
+ recall = (cKDTree(pred_pts).query(ref_pts)[0] < TAU).mean()
347
+ precision = (cKDTree(ref_pts).query(pred_pts)[0] < TAU).mean()
348
+ fscore = 200 * precision * recall / (precision + recall + 1e-9)
349
+ # ---- v2 协议: 独立归一化 [-0.5,0.5] + 带法向采样 ----
350
+ ref_m, pred_m = _metric_normalize(ref_mesh), _metric_normalize(pred_mesh)
351
+ ref_p, ref_fi = trimesh.sample.sample_surface(ref_m, NSAMP)
352
+ pred_p, pred_fi = trimesh.sample.sample_surface(pred_m, NSAMP)
353
+ ref_p, pred_p = np.asarray(ref_p), np.asarray(pred_p)
354
+ ref_n, pred_n = np.asarray(ref_m.face_normals[ref_fi]), np.asarray(pred_m.face_normals[pred_fi])
355
+ chamfer_l2, hausdorff, _ = metric.calculate_distances_numpy(ref_p, pred_p)
356
+ i1 = cKDTree(pred_p).query(ref_p)[1] # GT->Pred 最近邻
357
+ i2 = cKDTree(ref_p).query(pred_p)[1] # Pred->GT 最近邻
358
+ nc_abs = 0.5 * (np.abs(np.einsum('ij,ij->i', ref_n, pred_n[i1])).mean()
359
+ + np.abs(np.einsum('ij,ij->i', pred_n, ref_n[i2])).mean())
360
+ return chamfer_l2 * 1e3, hausdorff * 1e3, fscore, nc_abs
361
+
362
+
363
+ # ===== geom (方案A 几何 remask+requery, 移植自 infer_single_layer.py; requery 支持 consensus) =====
364
+ def open_edges_and_faces(coord_seq, sub_seq, coff):
365
+ """coord 空间复现解码(全向量化, 无逐面 Python 循环) ->
366
+ (open_edges set, parent2faces dict, degen_faces set, nonmanifold_edges set)。
367
+ 开边=覆盖数1, 非流形边=覆盖数>2(与 mesh_open_edge_stats 同判据)。与原逐面版逐位等价, 快 ~10x。"""
368
+ nface = coord_seq.numel() // 9
369
+ P = (coord_seq.view(nface, 9).reshape(nface, 3, 3).cpu().numpy() - coff).astype(np.int64) # [nface,3(父),3(xyz)]
370
+ S = sub_seq.view(nface, 9, 8).cpu().numpy().astype(np.int64) # [nface,9,8]
371
+ ar = np.arange(8)
372
+ fa = ((S[:, 0:3, 0][:, :, None] >> ar[None, None]) & 1).astype(bool) # [nface,3,8] 各父 F
373
+ A = np.zeros((nface, 24, 24), bool) # 24=3父x8子 邻接
374
+ for (pi, pj), pos in POS.items():
375
+ M = ((S[:, pos, :][:, :, None] >> ar[None, None]) & 1).astype(bool) # [nface,8(子a),8(bit b)]
376
+ M = M & fa[:, pi, :, None] & fa[:, pj, None, :]
377
+ if pi == pj:
378
+ A[:, pi * 8:pi * 8 + 8, pi * 8:pi * 8 + 8] = M | M.transpose(0, 2, 1)
379
+ else:
380
+ A[:, pi * 8:pi * 8 + 8, pj * 8:pj * 8 + 8] = M
381
+ A[:, pj * 8:pj * 8 + 8, pi * 8:pi * 8 + 8] = M.transpose(0, 2, 1)
382
+ vm = A[:, TRI[:, 0], TRI[:, 1]] & A[:, TRI[:, 1], TRI[:, 2]] & A[:, TRI[:, 0], TRI[:, 2]] # [nface,nTRI] 3-clique
383
+ # parent2faces: 每个父坐标 -> 所属 face 集(所有 face, 与 tris 无关)
384
+ parent2faces = defaultdict(set)
385
+ Pl = P.reshape(-1, 3); fl = np.repeat(np.arange(nface), 3)
386
+ for i in range(Pl.shape[0]):
387
+ parent2faces[(int(Pl[i, 0]), int(Pl[i, 1]), int(Pl[i, 2]))].add(int(fl[i]))
388
+ fidx, tidx = np.nonzero(vm)
389
+ if fidx.size == 0:
390
+ return set(), parent2faces, set(), set()
391
+ pnode = TRI[tidx] // 8; cnode = TRI[tidx] % 8 # [N,3] 父/子索引
392
+ Pf = P[fidx] # [N,3,3]
393
+ tri = np.take_along_axis(Pf, np.broadcast_to(pnode[:, :, None], (fidx.size, 3, 3)), axis=1) * 2 + OFF[cnode] # [N,3,3]
394
+ cross = np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0]) # 面积 x2
395
+ degen_faces = set(np.unique(fidx[np.abs(cross).sum(1) == 0]).tolist()) # 有零面积三角的 raw face
396
+ # 开边: 全局边覆盖计数==1。点->单调标量(保持字典序), 边->(小,大)标量对
397
+ B = int(tri.max()) + 1
398
+ scal = tri[:, :, 0] * B * B + tri[:, :, 1] * B + tri[:, :, 2] # [N,3] 每顶点标量
399
+ # ---- 修复(2026-08-02, 同 infer_single_layer): 只在「唯一且非退化」三角形上数边覆盖 ----
400
+ # 同一子三角形可被多个父块重复编码, decode 后 mesh 合并成一个 -> 实为开边, 旧统计判"已覆盖",
401
+ # build_remask 永远不掩其直接父面; 重复覆盖同样虚增非流形计数。去重+去退化后与 mesh 拓扑对齐。
402
+ nondeg = np.abs(cross).sum(1) > 0
403
+ sig = np.sort(scal[nondeg], axis=1)
404
+ _, first_idx = np.unique(sig, axis=0, return_index=True)
405
+ scal_u = scal[nondeg][first_idx] # [Nu,3] 唯一非退化三角
406
+ sa = scal_u[:, [0, 1, 2]].reshape(-1); sb = scal_u[:, [1, 2, 0]].reshape(-1) # [3Nu] 三边端���
407
+ key = np.stack([np.minimum(sa, sb), np.maximum(sa, sb)], axis=1) # [3Nu,2] 无向边
408
+ uk, cnt = np.unique(key, axis=0, return_counts=True)
409
+ ok = uk[cnt == 1] # 开边 (小,大) 标量对
410
+ nk = uk[cnt > 2] # 非流形边 (覆盖>2)
411
+ dec = lambda s: (int(s // (B * B)), int((s // B) % B), int(s % B))
412
+ open_edges = {(dec(mn), dec(mx)) for mn, mx in ok}
413
+ nm_edges = {(dec(mn), dec(mx)) for mn, mx in nk}
414
+ return open_edges, parent2faces, degen_faces, nm_edges
415
+
416
+ def build_remask(coord_seq, sub_seq, coff, total, cap=None, use_degen=False):
417
+ """几何缺陷驱动选 remask 目标: 触及缺陷边(开边 + REMASK_NM 时非流形边)的 face
418
+ (两端点 parent 的1-ring)的连接位置(3-8)置 MASK。
419
+ use_degen=True: 额外把"有退化三角(零面积)的 raw face"也纳入 remask(直接按 face)。"""
420
+ if cap is None: cap = MAXFACE_REMASK
421
+ oe, p2f, degen, nm = open_edges_and_faces(coord_seq, sub_seq, coff)
422
+ if REMASK_LOOP_MAX and oe: # 只用"边界环长<=K"的开边作种子: 小洞外科修复, 薄结构大环/复杂边界不触发
423
+ from collections import defaultdict as _dd
424
+ _adj = _dd(set)
425
+ for (c1, c2) in oe:
426
+ _adj[c1].add(c2); _adj[c2].add(c1)
427
+ _seen = set(); _keep = set()
428
+ for _s in list(_adj):
429
+ if _s in _seen: continue
430
+ _comp = [_s]; _seen.add(_s); _st = [_s]
431
+ while _st:
432
+ _x = _st.pop()
433
+ for _y in _adj[_x]:
434
+ if _y not in _seen: _seen.add(_y); _comp.append(_y); _st.append(_y)
435
+ _ne = sum(len(_adj[_x]) for _x in _comp) // 2
436
+ if _ne <= REMASK_LOOP_MAX:
437
+ _keep.update(_comp)
438
+ oe = {(c1, c2) for (c1, c2) in oe if c1 in _keep and c2 in _keep}
439
+ cand = defaultdict(int) # face -> 触及缺陷数(开边 + 非流形 + 退化)
440
+ oe_touch = set() # 触及开边的 face: cap 内永远优先(补洞不被非流形稀释)
441
+ for edges, is_oe in ((oe, True), (nm if REMASK_NM else set(), False)): # 覆盖1 vs >2 判据互斥
442
+ for (c1, c2) in edges:
443
+ g1 = tuple(np.array(c1) // 2); g2 = tuple(np.array(c2) // 2)
444
+ fs = (p2f.get(g1, set()) | p2f.get(g2, set())) if REMASK_UNION else (p2f.get(g1, set()) & p2f.get(g2, set()))
445
+ for f in fs:
446
+ cand[f] += 1
447
+ if is_oe: oe_touch.add(f)
448
+ if use_degen: # 退化面 raw face 直接进 remask
449
+ for f in degen:
450
+ cand[f] += 1
451
+ faces = sorted(cand, key=lambda x: (-(x in oe_touch), -cand[x]))[:cap]
452
+ rem = torch.zeros(total, dtype=torch.bool)
453
+ for f in faces:
454
+ rem[f * 9 + 3: f * 9 + 9] = True # 只重掩连接位置(3-8),保 F(0-2)+coord
455
+ return rem, len(oe) + (len(nm) if REMASK_NM else 0), faces
456
+
457
+
458
+ @torch.no_grad()
459
+ def requery(model, coord_list, subcodes, decided, depth, pc, K, cfg_scale, cfg_schedule, cfg_start, pc_target=None, consensus=False):
460
+ EB_ACTIVE[0] = False # requery=补洞期: 关 empty_bias, 否则补洞也被推向空(移植自 infer_remask_geom_altpc)
461
+ try:
462
+ return _requery_impl(model, coord_list, subcodes, decided, depth, pc, K, cfg_scale, cfg_schedule, cfg_start, pc_target, consensus)
463
+ finally:
464
+ EB_ACTIVE[0] = True
465
+
466
+
467
+ def _requery_impl(model, coord_list, subcodes, decided, depth, pc, K, cfg_scale, cfg_schedule, cfg_start, pc_target=None, consensus=False):
468
+ """冻结已 commit, 对被 remask 的位置按真实掩码比例设噪声级 t, K 步 in-distribution 重采。
469
+ consensus=True(geom_fmm): 重采时也做跨面共识——F+intra 概率平均(按共享父/顶点)、inter 8-subcode
470
+ 众数投票(按共享父对/边), 组原子揭示; 末步对全体做一次组投影(F 字节众数/intra,inter 向量众数),
471
+ 保证同顶点/同边一致(含之前冻结的 token)。等价于把 log_fmm 的共识折进 geom 的补洞 requery。"""
472
+ device = subcodes.device
473
+ lengths = torch.tensor([c.numel() for c in coord_list], device=device)
474
+ types = model.types_from_lengths(lengths)
475
+ total = int(lengths.sum().item())
476
+ subcodes = subcodes.clone(); decided = decided.clone()
477
+ subcodes[~decided] = MASK_ID
478
+ n0 = int((~decided).sum().item())
479
+ want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and model.use_pc and (pc is not None)
480
+ pred = subcodes.clone()
481
+
482
+ if consensus: # ---- 跨面共识分组 + 工具(F/intra 按共享父, inter 按共享父对) ----
483
+ C = 4096; nface = total // REFINE_POS_PER_FACE
484
+ cr = torch.cat([c.to(device) for c in coord_list]).view(nface, 3, 3)
485
+ def _pcode(p3): return p3[:, 0] * C * C + p3[:, 1] * C + p3[:, 2]
486
+ f_pos = (types == TYPE_F).nonzero(as_tuple=True)[0]
487
+ codef = _pcode(cr[f_pos // REFINE_POS_PER_FACE, f_pos % REFINE_POS_PER_FACE])
488
+ _uf, f_group = torch.unique(codef, return_inverse=True); n_fgroup = int(_uf.numel())
489
+ i_pos = (types == TYPE_INTRA).nonzero(as_tuple=True)[0]
490
+ codei = _pcode(cr[i_pos // REFINE_POS_PER_FACE, (i_pos % REFINE_POS_PER_FACE) - 3])
491
+ _ui, i_group = torch.unique(codei, return_inverse=True); n_igroup = int(_ui.numel())
492
+ allpos = torch.cat([f_pos, i_pos]) # 统一顶点分组(揭示原子)
493
+ _uv, vid_all = torch.unique(torch.cat([codef, codei]), return_inverse=True); n_vgroup = int(_uv.numel())
494
+ is_fi = torch.zeros(total, dtype=torch.bool, device=device); is_fi[allpos] = True
495
+ tok2vid = torch.full((total,), -1, dtype=torch.long, device=device); tok2vid[allpos] = vid_all
496
+ e_pos = (types == TYPE_INTER).nonzero(as_tuple=True)[0] # inter 按共享父对(边)
497
+ e_face = e_pos // REFINE_POS_PER_FACE; e_local = (e_pos % REFINE_POS_PER_FACE) - 6
498
+ PA = torch.tensor([0, 0, 1], device=device); PB = torch.tensor([1, 2, 2], device=device)
499
+ ca = _pcode(cr[e_face, PA[e_local]]); cb = _pcode(cr[e_face, PB[e_local]])
500
+ pair = torch.stack([torch.minimum(ca, cb), torch.maximum(ca, cb)], dim=1)
501
+ _ue, e_group = torch.unique(pair, dim=0, return_inverse=True); n_egroup = int(_ue.shape[0])
502
+ is_inter = torch.zeros(total, dtype=torch.bool, device=device); is_inter[e_pos] = True
503
+ tok2eid = torch.full((total,), -1, dtype=torch.long, device=device); tok2eid[e_pos] = e_group
504
+ def _gmode(group, vid, G):
505
+ U = int(vid.max().item()) + 1 if vid.numel() else 1
506
+ key, cc = torch.unique(group.long() * U + vid.long(), return_counts=True)
507
+ g, v = key // U, key % U
508
+ order = torch.argsort(g * (int(cc.max().item()) + 1) - cc); g, v = g[order], v[order]
509
+ first = torch.ones_like(g, dtype=torch.bool)
510
+ if g.numel() > 1: first[1:] = g[1:] != g[:-1]
511
+ mode = torch.zeros(G, dtype=torch.long, device=device); mode[g[first]] = v[first]
512
+ return mode[group]
513
+ def vote_vec(vecs, group, ngroup):
514
+ uv, vid = torch.unique(vecs, dim=0, return_inverse=True); mvid = _gmode(group, vid, ngroup)
515
+ return vid != mvid, uv[mvid]
516
+ def group_avg_argmax(prob, group, ngroup):
517
+ agg = torch.zeros(ngroup, prob.shape[-1], device=device, dtype=prob.dtype); agg.index_add_(0, group, prob)
518
+ agg = agg / torch.bincount(group, minlength=ngroup).clamp(min=1).unsqueeze(1)
519
+ am_g = agg.argmax(-1); return am_g[group], agg.gather(-1, am_g.unsqueeze(-1)).squeeze(-1)[group]
520
+ def group_rollout(head, hh, hh_u, group, ngroup, cfg_t):
521
+ feats = [hh]; feats_u = [hh_u] if hh_u is not None else None; preds, confs = [], []
522
+ for r in range(head.num_rows):
523
+ logit = head.mlps[r](torch.cat(feats, dim=-1))
524
+ if feats_u is not None:
525
+ lu = head.mlps[r](torch.cat(feats_u, dim=-1)); logit = lu + cfg_t * (logit - lu)
526
+ am, pm = group_avg_argmax(logit.float().softmax(-1), group, ngroup); preds.append(am); confs.append(pm)
527
+ if r < head.num_rows - 1:
528
+ e = head.row_emb[r](am).to(hh.dtype); feats.append(e)
529
+ if feats_u is not None: feats_u.append(e)
530
+ return torch.stack(preds, 1), torch.stack(confs, 1)
531
+
532
+ _rq_cc = {} # 条件跨步缓存(点云在 K 步重采内不变, 严格等价)
533
+ for step in range(K):
534
+ mf = float((~decided).sum().item()) / max(1, total)
535
+ tval = (2.0 / math.pi) * math.acos(min(1.0, max(0.0, mf))) # 真实掩码比例 -> 噪声时间
536
+ cfg_t = float(cfg_scale) if cfg_schedule != "linear" else float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(K - 1, 1)
537
+ use_cfg = want_cfg and (cfg_t != 1.0)
538
+ t = torch.full((len(coord_list),), tval, device=device, dtype=torch.float)
539
+ sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
540
+ h, _ = model.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, cond_cache=_rq_cc)
541
+ h_u = model.forward_varlen(coord_list, sub_list, t, depth, None, None, cond_cache=_rq_cc)[0] if use_cfg else None
542
+ h = h.float() # 头部权重 fp32; BF16_RESIDUAL=1 时需 cast(fp32 下 no-op)
543
+ if h_u is not None: h_u = h_u.float()
544
+ pred = subcodes.clone(); conf = torch.zeros(total, device=device)
545
+ m = types == TYPE_F
546
+ if m.any():
547
+ lg = model.f_head(h[m])
548
+ if use_cfg: lu = model.f_head(h_u[m]); lg = lu + cfg_t * (lg - lu)
549
+ if consensus:
550
+ am, pm = group_avg_argmax(lg.float().softmax(-1), f_group, n_fgroup)
551
+ else:
552
+ prob = lg.softmax(-1); pm, am = prob.max(-1)
553
+ pred[m, :] = am.unsqueeze(1).expand(-1, NUM_OCTANTS); conf[m] = pm.to(conf.dtype)
554
+ mI = types == TYPE_INTRA
555
+ if mI.any():
556
+ if consensus:
557
+ p, pmx = group_rollout(model.intra_head, h[mI], h_u[mI] if use_cfg else None, i_group, n_igroup, cfg_t)
558
+ else:
559
+ p, pmx = model.intra_head.rollout(h[mI], h_u[mI] if use_cfg else None, cfg_t, 0.0)
560
+ pred[i_pos if consensus else mI] = p; conf[i_pos if consensus else mI] = pmx.mean(1).to(conf.dtype)
561
+ mE = types == TYPE_INTER
562
+ if mE.any():
563
+ p, pmx = model.inter_head.rollout(h[mE], h_u[mE] if use_cfg else None, cfg_t, 0.0)
564
+ pred[mE] = p; conf[mE] = pmx.mean(1).to(conf.dtype)
565
+ if consensus and e_pos.numel(): # inter 8-subcode 众数投票 + 组均置信度
566
+ _, vE = vote_vec(pred[e_pos], e_group, n_egroup); pred[e_pos] = vE
567
+ cagg = torch.zeros(n_egroup, device=device); cagg.index_add_(0, e_group, conf[e_pos])
568
+ cagg = cagg / torch.bincount(e_group, minlength=n_egroup).clamp(min=1); conf[e_pos] = cagg[e_group].to(conf.dtype)
569
+ n_keep = int(round(n0 * (1 - (step + 1) / K)))
570
+ n_dec = max(0, (total - int(decided.sum().item())) - n_keep)
571
+ if n_dec > 0:
572
+ und = (~decided).nonzero(as_tuple=True)[0]
573
+ order = conf[und].argsort(descending=True); ch = und[order[:n_dec]]
574
+ if consensus: # 组原子揭示: 选中某顶点/边 -> 该顶点全部 F+intra / 该边全部 inter 一起
575
+ take = torch.zeros(total, dtype=torch.bool, device=device); take[ch] = True
576
+ ch_fi = ch[is_fi[ch]]
577
+ if ch_fi.numel():
578
+ sel_v = torch.zeros(n_vgroup, dtype=torch.bool, device=device); sel_v[tok2vid[ch_fi]] = True
579
+ take[allpos[sel_v[vid_all]]] = True
580
+ ch_e = ch[is_inter[ch]]
581
+ if ch_e.numel():
582
+ sel_e = torch.zeros(n_egroup, dtype=torch.bool, device=device); sel_e[tok2eid[ch_e]] = True
583
+ take[e_pos[sel_e[e_group]]] = True
584
+ take &= ~decided; subcodes[take] = pred[take]; decided[take] = True
585
+ else:
586
+ subcodes[ch] = pred[ch]; decided[ch] = True
587
+ subcodes[~decided] = MASK_ID
588
+ subcodes[~decided] = pred[~decided]
589
+ if consensus: # 末步全体组投影: 同顶点/同边强制一致(含之前冻结 token)
590
+ if f_pos.numel():
591
+ cnt = torch.bincount(f_group * 256 + subcodes[f_pos, 0].clamp(0, 255), minlength=n_fgroup * 256).view(n_fgroup, 256)
592
+ subcodes[f_pos, :] = cnt.argmax(1)[f_group].unsqueeze(1).expand(-1, NUM_OCTANTS)
593
+ if i_pos.numel(): subcodes[i_pos] = vote_vec(subcodes[i_pos], i_group, n_igroup)[1]
594
+ if e_pos.numel(): subcodes[e_pos] = vote_vec(subcodes[e_pos], e_group, n_egroup)[1]
595
+ return list(torch.split(subcodes, lengths.tolist(), dim=0))
596
+
597
+
598
+ def main():
599
+ device = "cuda"; cfg = load_config()
600
+ codec = FastMeshCodec(args=cfg.tokenizer); codec_cfg = codec.get_model_config(); pad_id = codec_cfg["pad_token_id"]
601
+ coord_offset = int(codec_cfg["coord_offset"]) # geom: coord 空间复现解码用
602
+ pc_num = int(cfg.data_processing.pc_num); total_pc_num = int(cfg.data_processing.total_pc_num)
603
+ cfg_scale = float(os.environ.get("CFG_SCALE", cfg.evaluation.cfg_scale))
604
+ cfg_schedule = cfg.evaluation.cfg_schedule; cfg_start = float(cfg.evaluation.cfg_start)
605
+ # 模型变体: config model.variant="fpsq" -> FPS-query 1024 latent + 稀疏注入(与 trainer/infer_remask 同开关)
606
+ _MC, _vk = BaseModel, {} # 发布版固定 altpc 系(fix3 -> focal -> altpc -> tssr)
607
+ _margs = dict(_vk,
608
+ coord_vocab_size=codec_cfg["input_num_categories"], max_seq_len=int(cfg.model.max_seq_len),
609
+ d_model=cfg.model.feature_dim, nhead=cfg.model.num_heads, depth=cfg.model.hourglass_depth,
610
+ dropout=0.0, encoder_freeze=True, use_pc=True, time_embed_scale=cfg.training.diffusion_train_steps,
611
+ nonempty_row_weight=1.0,
612
+ rope_mode=getattr(cfg.model, "rope_mode", "flat"), # geo/geo_vcat 线 config 带(零新增参数, 不传会静默装成 flat)
613
+ rope_theta_vcat=float(getattr(cfg.model, "rope_theta_vcat", 300.0)),
614
+ ffn_hidden=(int(cfg.model.ffn_hidden) if getattr(cfg.model, "ffn_hidden", None) else None),
615
+ pc_num_latents=(int(getattr(cfg.model, "pc_num_latents", 0)) or None))
616
+
617
+ def _load_model(ckpt_path):
618
+ m = _MC(**_margs).to(device)
619
+ ck = torch.load(ckpt_path, map_location=device)
620
+ m.load_state_dict(ck["model_state_dict"], strict=True)
621
+ sh = (ck.get("ema_state_dict") or {}).get("shadow")
622
+ if sh:
623
+ for name, param in m.named_parameters():
624
+ if name in sh:
625
+ param.data.copy_(sh[name].to(device=param.device, dtype=param.dtype))
626
+ m.eval()
627
+ if EMPTY_BIAS or REQUERY_EMPTY_BIAS: # 连接 rollout 空字节类加 bias; 生成期用 EMPTY_BIAS, requery(缺陷重��)期用 REQUERY_EMPTY_BIAS
628
+ import types as _types
629
+ def _biased_rollout(self, h, h_u=None, cfg_scale=1.0, temperature=0.0):
630
+ feats = [h]; feats_u = [h_u] if h_u is not None else None; preds = []; confs = []
631
+ for r in range(self.num_rows):
632
+ logit = self.mlps[r](torch.cat(feats, dim=-1))
633
+ if feats_u is not None:
634
+ lu = self.mlps[r](torch.cat(feats_u, dim=-1)); logit = lu + cfg_scale * (logit - lu)
635
+ _b = EMPTY_BIAS if EB_ACTIVE[0] else REQUERY_EMPTY_BIAS
636
+ if _b:
637
+ logit = logit.clone(); logit[:, 0] = logit[:, 0] + _b
638
+ if temperature and temperature > 0:
639
+ prob = (logit / temperature).softmax(-1); am = torch.multinomial(prob.float(), 1).squeeze(-1); pm = prob.gather(-1, am.unsqueeze(-1)).squeeze(-1)
640
+ else:
641
+ prob = logit.softmax(-1); pm, am = prob.max(-1)
642
+ preds.append(am); confs.append(pm)
643
+ if r < self.num_rows - 1:
644
+ e = self.row_emb[r](am).to(h.dtype); feats.append(e)
645
+ if feats_u is not None: feats_u.append(e)
646
+ return torch.stack(preds, dim=1), torch.stack(confs, dim=1)
647
+ m.intra_head.rollout = _types.MethodType(_biased_rollout, m.intra_head)
648
+ m.inter_head.rollout = _types.MethodType(_biased_rollout, m.inter_head)
649
+ print(f"[EMPTY_BIAS={EMPTY_BIAS}] 连接空字节类已加 bias -> 稀疏化(requery 期自动关)")
650
+ raw = m.forward_varlen
651
+ def _autocast_forward(*args, **kwargs):
652
+ with torch.autocast("cuda", dtype=torch.bfloat16):
653
+ out = raw(*args, **kwargs)
654
+ # 关键: geo 线的 f_head / AR 头是 fp32(rope fp32 精度), 且 model.sample() 在 autocast 外调用它们。
655
+ # 把 backbone 输出 h 上采回 fp32, 才能与下游 fp32 头匹配(否则 bf16×fp32 -> mat dtype 崩)。
656
+ if isinstance(out, tuple):
657
+ return tuple(o.float() if torch.is_tensor(o) else o for o in out)
658
+ return out.float() if torch.is_tensor(out) else out
659
+ m.forward_varlen = _autocast_forward
660
+ return m
661
+
662
+ model = _load_model(CKPT)
663
+ # ---- 按 in_depth 换模型(专家组装): STAGE_CKPTS="2:a.pth,3:b.pth,4:c.pth", 未列出的深度用主 CKPT ----
664
+ stage_models = {}
665
+ for _kv in (x for x in os.environ.get("STAGE_CKPTS", "").split(",") if x.strip()):
666
+ _d, _p = _kv.split(":", 1)
667
+ stage_models[int(_d)] = _load_model(_p)
668
+ print(f"[STAGE_CKPTS] in_depth={_d} <- {os.path.basename(_p)}")
669
+
670
+ # ---- acc_uni 高分辨率细分器(SUPERRES): 独立 config/codec/model, 载入一次 ----
671
+ run_superres = None
672
+ if SUPERRES:
673
+ _acc_spec = _ilu.spec_from_file_location("acc_un", os.path.join(os.path.dirname(os.path.abspath(__file__)), "subdivider_runtime.py"))
674
+ ACC_UN = _ilu.module_from_spec(_acc_spec); _acc_spec.loader.exec_module(ACC_UN)
675
+ _saved_argv = sys.argv; sys.argv = ["x", "--config", SUPERRES_CONFIG]
676
+ sr_cfg = load_config(); sys.argv = _saved_argv # 单独加载 acc_uni config
677
+ sr_codec = OctreeMeshCodec(args=sr_cfg.tokenizer); sr_cc = sr_codec.get_model_config()
678
+ sr_model = DiscreteDiffusionTransformer(
679
+ vocab_A=sr_cc["input_num_categories"], vocab_B=sr_cc["output_num_categories"],
680
+ max_seq_len=sr_cfg.data_processing.max_len, d_model=sr_cfg.model.feature_dim,
681
+ encoder_freeze=sr_cfg.model.encoder_freeze, nhead=sr_cfg.model.num_heads,
682
+ depth=sr_cfg.model.hourglass_depth, dropout=sr_cfg.model.dropout,
683
+ time_embed_scale=sr_cfg.training.diffusion_train_steps).to(device)
684
+ sr_ckpt = torch.load(SUPERRES_CKPT, map_location=device)
685
+ sr_model.load_state_dict(sr_ckpt["model_state_dict"], strict=False)
686
+ _sr_shadow = (sr_ckpt.get("ema_state_dict") or {}).get("shadow") if isinstance(sr_ckpt.get("ema_state_dict"), dict) else None
687
+ if _sr_shadow:
688
+ for name, param in sr_model.named_parameters():
689
+ if name in _sr_shadow: param.data.copy_(_sr_shadow[name].to(device=param.device, dtype=param.dtype))
690
+ sr_model.eval()
691
+ sr_token_shift = sr_cc["input_num_categories"] - 1
692
+ # 细分阶梯起点 = 级联实际终点(真实完成的格): 正常跑完 STAGE_RUN 时是 STAGE[-1];
693
+ # 但 GT_START_RES/EXT_START_RES 起点高于整条 STAGE 时 STAGE_RUN=[](级联被跳过,
694
+ # current 就是外部/GT 起点本身) -> 阶梯必须从 _START_RES 起, 不能想当然用 STAGE[-1]
695
+ # (否则会把已经是 res128 的 mesh 又拿去按 res32 重新离散化, 白丢 3 级细节)。
696
+ _sr_start = STAGE_RUN[-1] if STAGE_RUN else (_START_RES or STAGE[-1])
697
+ sr_ladder = [_sr_start] # 细分阶梯: 倍增到 SUPERRES_TO
698
+ while sr_ladder[-1] < SUPERRES_TO:
699
+ sr_ladder.append(sr_ladder[-1] * 2)
700
+ print(f"[SUPERRES] acc_uni 细分器 {os.path.basename(SUPERRES_CKPT)} | 阶梯 {sr_ladder} | steps={SUPERRES_STEPS} cfg={SUPERRES_CFG} fix_holes={int(SUPERRES_FIX_HOLES)}")
701
+
702
+ def run_superres(cur, gt_mesh, stem, case_dir, hill_holes=True):
703
+ """用 acc_uni 细分器把 cur 从 STAGE[-1] 继续细分到 SUPERRES_TO(每级 save_stage_prediction 自存)。"""
704
+ sr_depth = ACC_UN._depth_from_resolution # acc_uni 真实格=2^D -> log2 正确(实测: decode@D 格步=2/2^D); 勿用 fix3 的 log2-1
705
+ if SR_FILL_IN and cur is not None and len(getattr(cur, "faces", [])): # 入口补洞: 对齐旧 fillall(粗输入先补再细分)
706
+ _v = torch.as_tensor(np.asarray(cur.vertices), dtype=torch.float32, device=device)
707
+ _f = torch.as_tensor(np.asarray(cur.faces), dtype=torch.long, device=device)
708
+ _v, _f = _fill_holes(_v, _f, only_fill_holes=True)
709
+ cur = trimesh.Trimesh(vertices=(_v.detach().cpu().numpy() if torch.is_tensor(_v) else np.asarray(_v)),
710
+ faces=(_f.detach().cpu().numpy() if torch.is_tensor(_f) else np.asarray(_f)), process=False)
711
+ sr_bar = tqdm(range(len(sr_ladder) - 1), desc=f"{stem[:12]} 细分", leave=False)
712
+ for i in sr_bar:
713
+ in_res, out_res = sr_ladder[i], sr_ladder[i + 1]
714
+ sr_bar.set_postfix_str(f"res{in_res}->{out_res}")
715
+ in_d, out_d = sr_depth(in_res), sr_depth(out_res)
716
+ enc = mul_encode_mesh(raw_mesh=cur.copy(), gt_mesh=gt_mesh.copy(), codec=sr_codec, tokenizer_target_depth=in_d,
717
+ pc_num=pc_num, total_pc_num=total_pc_num, use_gt_pc=True, clean=False,
718
+ normalize_input=CASCADE_RENORM)
719
+ raw_tok = enc["raw_token"].unsqueeze(0).to(device); pc = enc["pc"].unsqueeze(0).to(device)
720
+ with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16):
721
+ refine = sample_refine_tokens_with_topology(sr_model, sr_model.mask_token_id, raw_tok, pc,
722
+ use_gumbel=False, use_soft_consensus=False,
723
+ consensus_start_step_ratio=0, cfg_scale=SUPERRES_CFG, T_infer=SUPERRES_STEPS)
724
+
725
+ refine = ACC_UN.restore_non_pad_token(refine, shift=sr_token_shift, pad_id=sr_cc["pad_token_id"])
726
+ m = ACC_UN.save_stage_prediction(codec=sr_codec, raw_token=raw_tok, refine_token=refine,
727
+ out_dir=case_dir, name=stem, target_depth=out_d,
728
+ suffix=f"_sr_{in_res}to{out_res}", fix_holes=SUPERRES_FIX_HOLES,
729
+ raw_dis_mesh=None, gt_dis_mesh=None)
730
+ if m is None:
731
+ break
732
+ cur = m
733
+ return cur
734
+
735
+ def save_yup(mesh, name, out_dir):
736
+ if SAVE and mesh is not None and len(getattr(mesh, "faces", [])):
737
+ m = mesh.copy(); m.vertices = m.vertices[:, [1, 2, 0]]; m.export(os.path.join(out_dir, name))
738
+
739
+ if DATASET == "val":
740
+ paths = [line.split()[0] for line in open(VAL) if line.strip()]
741
+ car_path = next((p for p in paths if CAR in p), None)
742
+ cases = (([car_path] if car_path else []) + [p for p in paths if (not car_path or CAR not in p)])[:NCASE]
743
+ elif DATASET == "toys4k":
744
+ cases = sorted(_glob.glob(os.path.join(TOYS4K, "*.glb")))[:NCASE]
745
+ elif os.environ.get("DATASET_DIR"): # 任意外部目录: DATASET 只作命名标签, 实际目录由 DATASET_DIR 给
746
+ _dd = os.environ["DATASET_DIR"]
747
+ cases = sorted(set(sum((_glob.glob(os.path.join(_dd, f"*.{_e}")) for _e in ("glb", "obj", "ply")), [])))[:NCASE]
748
+ else:
749
+ cases = sorted(_glob.glob(os.path.join(TRELLIS, "*.glb")))[:NCASE]
750
+ if CASE_STEM: # 逗号分隔多个子串, 任一命中即保留(如 "1(10).,1(101)." 用点号防前缀误配)
751
+ _stems = [s for s in CASE_STEM.split(",") if s]
752
+ cases = [p for p in cases if any(s in p for s in _stems)]
753
+ if EXT_START_DIR: # 只跑有外部起点文件的 case
754
+ cases = [p for p in cases if os.path.exists(os.path.join(EXT_START_DIR, os.path.splitext(os.path.basename(p))[0] + EXT_START_SUFFIX))]
755
+ print(f"[EXT_START] {EXT_START_DIR} 匹配到 {len(cases)} case (后缀 {EXT_START_SUFFIX}, res{EXT_START_RES})")
756
+
757
+ print(f"多级生成(真实res口径) STAGE={STAGE_RUN}{f' + SUPERRES->{SUPERRES_TO}' if SUPERRES else ''}{f' | GT起点@res{GT_START_RES}' if GT_START_RES else ''}{f' | 外部起点@res{EXT_START_RES}' if EXT_START_DIR else ''} | ep{EPOCH} STEPS={STEPS} | "
758
+ f"METHODS={METHODS} 多级PC={int(MULTI_PC)} ALIGN fm={int(ALIGN_FM)} inter={int(ALIGN_INTER)} FILL={int(FILL)} | {len(cases)} case -> {OUT}")
759
+ print(f"{'case':12s} {'method':9s} | 开边 面 非流形 | CD/HD/F@GT(CD/HD为v2协议[-0.5,0.5]帧) |NC| | 用时")
760
+ print("-" * 82)
761
+ results = {m: [] for m in METHODS}
762
+ for path in cases:
763
+ stem = os.path.splitext(os.path.basename(path))[0]
764
+ gt = trimesh.load(path, force="mesh"); gt.vertices = gt.vertices[:, [2, 0, 1]]; normalize(gt)
765
+ case_dir = os.path.join(OUT, stem); os.makedirs(case_dir, exist_ok=True)
766
+ save_yup(gt, "gt_original.obj", case_dir)
767
+ gt.vertices, gt.faces = decimate_mesh(gt.vertices, gt.faces, simplify=True, simplify_ratio=DECIMATE_RATIO, verbose=False)
768
+ save_yup(gt, "gt_original_dec.obj", case_dir)
769
+ gt_eval = gt.copy() # 指标参照: 固定为未补洞的塌缩GT, DEC_FILL 只改输入侧
770
+ if DEC_FILL and len(getattr(gt, "faces", [])):
771
+ _v = torch.as_tensor(np.asarray(gt.vertices), dtype=torch.float32, device=device)
772
+ _f = torch.as_tensor(np.asarray(gt.faces), dtype=torch.long, device=device)
773
+ _v, _f = _fill_holes(_v, _f, only_fill_holes=True)
774
+ gt = trimesh.Trimesh(vertices=(_v.detach().cpu().numpy() if torch.is_tensor(_v) else np.asarray(_v)),
775
+ faces=(_f.detach().cpu().numpy() if torch.is_tensor(_f) else np.asarray(_f)), process=False)
776
+ save_yup(gt, "gt_original_dec_fill.obj", case_dir)
777
+ # ---- 粗起点(所有方法共用) ----
778
+ if EXT_START_DIR: # 外部预测起点(如 BPT res32 预测): 保存帧 -> 内部帧(与 gt 载入同款 [2,0,1] 交换)
779
+ coarse_start = trimesh.load(os.path.join(EXT_START_DIR, stem + EXT_START_SUFFIX), force="mesh", process=False)
780
+ coarse_start.vertices = np.asarray(coarse_start.vertices)[:, [2, 0, 1]]
781
+ assert len(getattr(coarse_start, "faces", [])), f"外部起点为空 mesh: {stem}"
782
+ elif GT_START_RES: # GT 天花板起点: GT 编码->解码(含连接)@该分辨率, 测级联暴露偏差上界
783
+ _d0 = depth_from_resolution(GT_START_RES)
784
+ _enc0 = mul_encode_mesh(raw_mesh=gt.copy(), gt_mesh=gt.copy(), codec=codec, tokenizer_target_depth=_d0,
785
+ pc_num=pc_num, total_pc_num=total_pc_num, use_gt_pc=True, clean=False)
786
+ coarse_start = codec.decode_tokens(_enc0["token"].tolist(), target_depth=_d0)
787
+ assert coarse_start is not None and len(getattr(coarse_start, "faces", [])), f"GT 天花板@res{GT_START_RES} 解码为空: {stem}"
788
+ else: # GT 八叉树解码到最粗级(只有占用的 raw 起点, 非完整 mesh -> 文件名带 raw_ 前缀)
789
+ raw_token, _, init_quant = codec.encode_mesh(gt.copy(), target_depth=depth_from_resolution(STAGE_RUN[0]))
790
+ coarse_start = codec.decode_raw_tokens(raw_token, target_depth=depth_from_resolution(STAGE_RUN[0]))
791
+ if coarse_start is None or len(getattr(coarse_start, "faces", [])) == 0:
792
+ coarse_start = init_quant if init_quant is not None else gt.copy()
793
+ save_yup(coarse_start, (f"res{_START_RES}.obj" if _START_RES else f"raw_res{STAGE_RUN[0]}.obj"), case_dir)
794
+ if SAVE_GT_LEVELS and SAVE: # GT 逐分辨率天花板(tokenizer 量化口径), 供逐级对照
795
+ for _r in STAGE_RUN:
796
+ try:
797
+ _d = depth_from_resolution(_r)
798
+ _enc = mul_encode_mesh(raw_mesh=gt.copy(), gt_mesh=gt.copy(), codec=codec, tokenizer_target_depth=_d,
799
+ pc_num=pc_num, total_pc_num=total_pc_num, use_gt_pc=True, clean=False)
800
+ _gm = codec.decode_tokens(_enc["token"].tolist(), target_depth=_d)
801
+ save_yup(_gm, f"gt_res{_r}.obj", case_dir)
802
+ except Exception as _e:
803
+ print(f" [SAVE_GT_LEVELS] {stem} res{_r} 失败: {_e}")
804
+ for method in METHODS:
805
+ np.random.seed(777 + 7919 * RUN_SEED) # 每方法同种子 -> 点云/退化采样一致, 干净对比; RUN_SEED 偏移用于多次独立采样
806
+ t_start = time.time()
807
+ current = coarse_start.copy()
808
+ base = method.split(":")[0] # geom 可用 "geom_fmm:2x5" 覆盖 NITER=2/KREQ=5
809
+ ni_g, kq_g = NITER_GEOM, KREQ_GEOM
810
+ if ":" in method:
811
+ _s = method.split(":")[1].lower().split("x"); ni_g, kq_g = int(_s[0]), int(_s[1])
812
+ msave = method.replace(":", "_") # 文件名去冒号
813
+ # ---- 级联: 每级把上一级产物重编码成占用, 在模型深度 d=log2(out_res)-1 补全成格 out_res 的完整 mesh ----
814
+ stage_bar = tqdm(range(len(STAGE_RUN)), desc=f"{stem[:12]} {method} 级联", leave=False)
815
+ for stage_i in stage_bar:
816
+ out_res = STAGE_RUN[stage_i]
817
+ d = depth_from_resolution(out_res)
818
+ stage_bar.set_postfix_str(f"->res{out_res}")
819
+ encoded = mul_encode_mesh(raw_mesh=current.copy(), gt_mesh=gt.copy(), codec=codec, tokenizer_target_depth=d,
820
+ pc_num=pc_num, total_pc_num=total_pc_num, use_gt_pc=True, clean=False,
821
+ normalize_input=CASCADE_RENORM)
822
+ coord_list, _ = split_fix3_token_list([encoded["token"]], pad_id, device, codec_cfg)
823
+ pc_full = encoded["pc"].unsqueeze(0).to(device)
824
+ if MULTI_PC: # altpc pc_target: GT 天花板@d 解码 -> 退化面感知采点
825
+ # resolution=out_res=2^(d+1) 严格对齐训练(mesh_dataset_altpc_decoded: decode@sd + 2^(sd+1))
826
+ gt_encoded = mul_encode_mesh(raw_mesh=gt.copy(), gt_mesh=gt.copy(), codec=codec, tokenizer_target_depth=d,
827
+ pc_num=pc_num, total_pc_num=total_pc_num, use_gt_pc=True, clean=False)
828
+ gt_ceiling_mesh = codec.decode_tokens(gt_encoded["token"].tolist(), target_depth=d)
829
+ pc_target_np = (sample_pc_with_degen(gt_ceiling_mesh, pc_num, total_pc_num, aug=False, overfit=False, resolution=out_res)
830
+ if PCT_DEG else sample_pc(gt_ceiling_mesh, pc_num=pc_num, total_pc_num=total_pc_num, with_normal=True, aug=False))
831
+ pc_target = torch.from_numpy(pc_target_np).unsqueeze(0).to(device)
832
+ if SAVE and SAVE_PC_LEVELS: # 实际注入的云B(帧与 obj 一致); 多方法时同名覆盖(同种子, 云几乎一致)
833
+ trimesh.PointCloud(pc_target_np[:, :3][:, [1, 2, 0]]).export(os.path.join(case_dir, f"pc_target_res{out_res}.ply"))
834
+ else: # 单点云(focal 单PC ckpt): pc_target=None -> altpc 退化为单 PC
835
+ pc_target = None
836
+ if SINGLE_TARGET_PC and pc_target is not None: # sfocal: 云B进 pc 位作唯一云(每层恒注入), 云A弃用
837
+ pc_full = pc_target
838
+ pc_target = None
839
+ if SAVE and SAVE_PC_LEVELS and stage_i == 0 and not os.path.exists(os.path.join(case_dir, "pc_full.ply")):
840
+ _pa = encoded["pc"].cpu().numpy() # 云A: 全分辨率 GT 云(每级重采, 分布相同, 存首级一份)
841
+ trimesh.PointCloud(_pa[:, :3][:, [1, 2, 0]]).export(os.path.join(case_dir, "pc_full.ply"))
842
+ torch.manual_seed(20000 + stage_i + 7919 * RUN_SEED)
843
+ depth_t = torch.tensor([d], device=device)
844
+ model_d = stage_models.get(d, model) # 专家组装: 该深度有专属 ckpt 则用之
845
+ if base in ("log", "log_fmm"): # 概率平均共识(log_fmm 再加 inter 众数投票)
846
+ gen_subcodes = model_d.sample_log(coord_list, depth_t, pc=pc_full, pc_target=pc_target, T_infer=STEPS,
847
+ cfg_scale=cfg_scale, temperature=0.0, cfg_schedule=cfg_schedule, cfg_start=cfg_start,
848
+ inter_vote=(base == "log_fmm"))
849
+ elif base in ("geom", "geom_fmm", "geom_deg", "geom_fmm_deg"): # plain 采样 + 终点几何 remask+requery
850
+ # _fmm -> requery 内共识; _deg -> remask 额外纳入退化面(否则用全局 REMASK_DEGEN)
851
+ use_cons = ("fmm" in base); use_deg = base.endswith("_deg") or REMASK_DEGEN
852
+ ni_g, kq_g = GEOM_STAGE_PARAMS.get(out_res, (ni_g, kq_g)) # 按级覆盖 remask 预算
853
+ _n_cand = BO_N if (BO_N > 1 and out_res in BO_STAGES) else 1
854
+ _best = None # (score, gen_cur, rem_union)
855
+ for _cand in range(_n_cand):
856
+ _pt = pc_target
857
+ if _cand > 0 and pc_target is not None: # 候选多样性: 重采条件云B(实测±40方差来源)
858
+ _ptn = (sample_pc_with_degen(gt_ceiling_mesh, pc_num, total_pc_num, aug=False, overfit=False, resolution=out_res)
859
+ if PCT_DEG else sample_pc(gt_ceiling_mesh, pc_num=pc_num, total_pc_num=total_pc_num, with_normal=True, aug=False))
860
+ _pt = torch.from_numpy(_ptn).unsqueeze(0).to(device)
861
+ torch.manual_seed(20000 + stage_i + 7919 * _cand)
862
+ gen_list = model_d.sample(coord_list, depth_t, pc=pc_full, pc_target=_pt, T_infer=STEPS,
863
+ cfg_scale=cfg_scale, temperature=0.0, cfg_schedule=cfg_schedule, cfg_start=cfg_start,
864
+ align_fm=False, align_inter=False, align_per_step=False, align_debug=False)
865
+ gen_cur = gen_list[0].to(device); total_tok = coord_list[0].numel()
866
+ rem_union = set() # SAVE_REMASK_VIS: 各轮被重掩 token 的并集
867
+ for _ in range(ni_g if (not GEOM_STAGES or out_res in GEOM_STAGES) else 0):
868
+ rem, _, rfaces = build_remask(coord_list[0].cpu(), gen_cur.cpu(), coord_offset, total_tok, use_degen=use_deg)
869
+ if int(rem.sum().item()) == 0:
870
+ break
871
+ rem_union.update(torch.nonzero(rem, as_tuple=False).reshape(-1).tolist() if rem.dtype == torch.bool else rem.reshape(-1).tolist())
872
+ if REMASK_GROUPS > 1 and len(rfaces) > REMASK_GROUPS: # 分批: BFS序(≈空间局部)分 G 批逐批重采
873
+ for _ch in np.array_split(np.array(sorted(rfaces)), REMASK_GROUPS):
874
+ if len(_ch) == 0:
875
+ continue
876
+ _rm = torch.zeros(total_tok, dtype=torch.bool)
877
+ for _f in _ch:
878
+ _rm[int(_f) * 9 + 3: int(_f) * 9 + 9] = True
879
+ dec_b = torch.ones(total_tok, dtype=torch.bool, device=device); dec_b[_rm.to(device)] = False
880
+ gen_cur = requery(model_d, coord_list, gen_cur, dec_b, depth_t, pc_full, kq_g,
881
+ cfg_scale, "constant", 1.0, pc_target=_pt, consensus=use_cons)[0]
882
+ else:
883
+ dec_b = torch.ones(total_tok, dtype=torch.bool, device=device); dec_b[rem.to(device)] = False
884
+ gen_cur = requery(model_d, coord_list, gen_cur, dec_b, depth_t, pc_full, kq_g,
885
+ cfg_scale, "constant", 1.0, pc_target=_pt, consensus=use_cons)[0]
886
+ if _n_cand == 1:
887
+ _best = (0.0, gen_cur, rem_union)
888
+ break
889
+ _cm = decode_subcodes_to_mesh(codec, coord_list[0], gen_cur, codec_cfg, d + 1)
890
+ _oe, _nf, _nmp = mesh_open_edge_stats(_cm)
891
+ _score = _oe + 10.0 * _nmp # 结构惩罚: 开边 + 10*非流形%(bo4 同思路)
892
+ if _best is None or _score < _best[0]:
893
+ _best = (_score, gen_cur, rem_union)
894
+ gen_cur, rem_union = _best[1], _best[2]
895
+ gen_subcodes = [gen_cur]
896
+ else: # plain / consensus(末步 align 众数表决)
897
+ is_cons = (base == "consensus")
898
+ gen_subcodes = model_d.sample(coord_list, depth_t, pc=pc_full, pc_target=pc_target, T_infer=STEPS,
899
+ cfg_scale=cfg_scale, temperature=0.0, cfg_schedule=cfg_schedule, cfg_start=cfg_start,
900
+ align_fm=(is_cons or ALIGN_FM), align_inter=(is_cons or ALIGN_INTER),
901
+ align_per_step=ALIGN_PER_STEP, align_debug=False)
902
+ stage_mesh = decode_subcodes_to_mesh(codec, coord_list[0], gen_subcodes[0], codec_cfg, d + 1) # 产物=深度d完整mesh, 真实格 out_res(实测)
903
+ if SAVE_REMASK_VIS and base.startswith("geom") and SAVE:
904
+ _pre = decode_subcodes_to_mesh(codec, coord_list[0], gen_list[0].to(device), codec_cfg, d + 1)
905
+ if _pre is not None:
906
+ save_yup(_pre, f"res{out_res}_{msave}_pre.obj", case_dir) # 补洞(remask/requery)前的 plain 采样解码
907
+ if _pre is not None and stage_mesh is not None:
908
+ save_remask_vis(_pre, stage_mesh, os.path.join(case_dir, f"res{out_res}_{msave}"))
909
+ if rem_union: # remask token -> 输入侧 raw face block(每块 REFINE_POS_PER_FACE=9 token) 投影上色
910
+ try:
911
+ _rt, _, _ = codec.encode_mesh(current.copy(), target_depth=d)
912
+ _raw = codec.decode_raw_tokens(_rt, target_depth=d)
913
+ _nb = coord_list[0].numel() // REFINE_POS_PER_FACE
914
+ if _raw is not None and len(_raw.faces) == _nb:
915
+ _bm = np.zeros(_nb, bool)
916
+ _bm[sorted({i // REFINE_POS_PER_FACE for i in rem_union})] = True
917
+ _mr = _raw.copy(); _mr.vertices = _mr.vertices[:, [1, 2, 0]]
918
+ _mr.visual.face_colors = np.where(_bm[:, None], [255, 140, 0, 255], [200, 200, 200, 255])
919
+ _mr.export(os.path.join(case_dir, f"res{out_res}_{msave}_remask_rawface.ply"))
920
+ print(f" [REMASK_VIS] raw face 投影: 被重掩块 {int(_bm.sum())}/{_nb}")
921
+ else:
922
+ print(f" [REMASK_VIS] raw face 数不匹配: raw {0 if _raw is None else len(_raw.faces)} vs 块 {_nb}, 跳过投影")
923
+ except Exception as _e:
924
+ print(f" [REMASK_VIS] raw face 投影失败: {_e}")
925
+ if QUAD_FILL and stage_mesh is not None and len(getattr(stage_mesh, "faces", [])):
926
+ stage_mesh = quad_fill_mesh(stage_mesh, out_res)
927
+ # 直接补洞: method=fill 在 FILL_STAGES 各级补洞喂下一级(纯几何, 不过模型); 或全局 FILL 开关(stage_i>=4)
928
+ _fill_hit = (out_res >= FILL_MIN_RES) if FILL_MIN_RES else (stage_i >= 4)
929
+ do_fill = (FILL and _fill_hit) or (base == "fill" and out_res in FILL_STAGES)
930
+ if do_fill and stage_mesh is not None and len(getattr(stage_mesh, "faces", [])):
931
+ _v = torch.as_tensor(np.asarray(stage_mesh.vertices), dtype=torch.float32, device=device)
932
+ _f = torch.as_tensor(np.asarray(stage_mesh.faces), dtype=torch.long, device=device)
933
+ _v, _f = _fill_holes(_v, _f, only_fill_holes=True, keep_degenerate_faces=True)
934
+ _vn = _v.detach().cpu().numpy() if torch.is_tensor(_v) else np.asarray(_v)
935
+ _fn = _f.detach().cpu().numpy() if torch.is_tensor(_f) else np.asarray(_f)
936
+ stage_mesh = trimesh.Trimesh(vertices=_vn, faces=_fn, process=False)
937
+ current = stage_mesh if stage_mesh is not None else current
938
+ save_yup(current, f"res{out_res}_{msave}.obj", case_dir)
939
+ if SUPERRES and run_superres is not None and current is not None and len(getattr(current, "faces", [])):
940
+ current = run_superres(current, gt, stem, case_dir)
941
+ save_yup(current, f"final_res{FINAL_RES}_{msave}.obj", case_dir)
942
+ elapsed = time.time() - t_start
943
+ open_edges, num_faces, nonmanifold = mesh_open_edge_stats(current)
944
+ n_degen = int((np.asarray(current.area_faces) <= 1e-12).sum()) if (current is not None and len(getattr(current, "faces", []))) else 0
945
+ cd, hd, fscore, nc = chamfer_hd_fscore(current, gt_eval)
946
+ print(f"{stem[:12]:12s} {method:12s} | {open_edges:5d} {num_faces:6d} {nonmanifold:4.1f} 退化{n_degen:4d} | {cd:5.1f}/{hd:5.0f}/{fscore:4.0f} |NC|{nc:.3f} | {elapsed:.1f}s")
947
+ results[method].append((open_edges, num_faces, nonmanifold, n_degen, cd, hd, fscore, nc, elapsed))
948
+ print("=" * 88)
949
+ for method in METHODS:
950
+ r = results[method]
951
+ if not r:
952
+ continue
953
+ mr = np.array(r, np.float64).mean(0)
954
+ print(f"{'均值':12s} {method:12s} | {mr[0]:5.0f} {mr[1]:6.0f} {mr[2]:4.1f} 退化{mr[3]:4.0f} | {mr[4]:5.1f}/{mr[5]:5.0f}/{mr[6]:4.0f} |NC|{mr[7]:.3f} | {mr[8]:.1f}s")
955
+
956
+
957
+ if __name__ == "__main__":
958
+ main()
959
+
960
+ # 运行(GPU7; 训练占 0-6):
961
+ # cd /root/junkai_lin/mul_res_diffusion && source /root/miniconda3/etc/profile.d/conda.sh && conda activate mul_res
962
+ # HG=pretrained_model/gobjaverse_fix3_octant_factorized_1_to_1024_ce_altpc_decoded_deg_w1_hg246
963
+ # CUDA_VISIBLE_DEVICES=7 STAGE=2,4,8,16,32 DATASET=trellis NCASE=8 STEPS=20 ALIGN_INTER=1 FILL=1 \
964
+ # BASE_DIR=$HG RAIL_CKPT=$HG/checkpoint_30_epoch_0.05907_loss.pth python infer_multilevel.py
mulres_infer/modules/subdivider_runtime.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gc
2
+ import glob
3
+ import math
4
+ import os
5
+ import random
6
+ import re
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import torch
11
+ import trimesh
12
+ from accelerate import Accelerator, DistributedDataParallelKwargs
13
+ from tqdm import tqdm
14
+
15
+ from utils.config import load_config
16
+ from dataset.mesh_dataset import mul_encode_mesh, discrete_undiscrete_and_clean
17
+ from models.subdivider import DiscreteDiffusionTransformer, sample_refine_tokens_with_topology, sample_refine_tokens_with_topology_fm
18
+ from modules.octree_codec_acc import MeshCodec
19
+ from utils.postprocessing import _fill_holes
20
+ from utils.data_utils import normalize
21
+ def set_seed(seed):
22
+ random.seed(seed)
23
+ np.random.seed(seed)
24
+ torch.manual_seed(seed)
25
+ if torch.cuda.is_available():
26
+ torch.cuda.manual_seed(seed)
27
+ torch.cuda.manual_seed_all(seed)
28
+ torch.backends.cudnn.deterministic = True
29
+ torch.backends.cudnn.benchmark = False
30
+ os.environ["PYTHONHASHSEED"] = str(seed)
31
+
32
+ def _depth_from_resolution(resolution):
33
+ if resolution <= 0:
34
+ raise ValueError(f"resolution must be positive, got {resolution}")
35
+ depth = int(round(math.log2(resolution)))
36
+ if (1 << depth) != resolution:
37
+ raise ValueError(f"resolution must be power of 2, got {resolution}")
38
+ return depth
39
+
40
+
41
+ def restore_non_pad_token(token: torch.Tensor, shift: int, pad_id: int = 0):
42
+ if shift == 0:
43
+ return token
44
+ restored = token.clone()
45
+ non_pad_mask = restored != pad_id
46
+ restored[non_pad_mask] = restored[non_pad_mask] + shift
47
+ return restored
48
+
49
+
50
+ def read_dataset_paths(txt_path):
51
+ mesh_paths = []
52
+ with open(txt_path, "r") as f:
53
+ for line in f:
54
+ path = line.strip()
55
+ if path:
56
+ mesh_paths.append(path)
57
+ return mesh_paths
58
+
59
+
60
+ def raw_stem_to_pair_key(raw_path):
61
+ stem = Path(raw_path).stem
62
+ # raw name example: 1(42)_y_f_4244 -> pair key: 1(42)
63
+ return re.sub(r"_y_f_\d+$", "", stem)
64
+
65
+
66
+ def gt_stem_to_pair_key(gt_path):
67
+ return Path(gt_path).stem
68
+
69
+ def get_face_count(file_path):
70
+ """
71
+ 使用正则表达式从文件名中提取面片数量
72
+ 支持格式: *_f{num}_v*.obj 或 *_facenum_{num}_*.glb
73
+ """
74
+ filename = Path(file_path).name
75
+ match = re.search(r'_f(\d+)_v|_facenum_(\d+)', filename)
76
+ if match:
77
+ # group(1) 对应 _f(\d+)_v 里的数字
78
+ # group(2) 对应 _facenum_(\d+) 里的数字
79
+ return int(match.group(1) or match.group(2))
80
+ return None
81
+
82
+ # def build_raw_gt_pairs(raw_txt_path, gt_txt_path):
83
+ # raw_paths = read_dataset_paths(raw_txt_path)
84
+ # gt_paths = read_dataset_paths(gt_txt_path)
85
+
86
+ # # If two txt paths are the same source, pair each mesh with itself directly.
87
+ # if Path(raw_txt_path).resolve() == Path(gt_txt_path).resolve():
88
+ # return [(p, p) for p in raw_paths], []
89
+
90
+ # gt_by_key = {}
91
+ # for gt_path in gt_paths:
92
+ # gt_by_key[gt_stem_to_pair_key(gt_path)] = gt_path
93
+
94
+ # pairs = []
95
+ # missing_gt = []
96
+ # for raw_path in raw_paths:
97
+ # key = raw_stem_to_pair_key(raw_path)
98
+ # gt_path = gt_by_key.get(key)
99
+ # if gt_path is None:
100
+ # missing_gt.append(raw_path)
101
+ # continue
102
+ # pairs.append((raw_path, gt_path))
103
+
104
+ # return pairs, missing_gt
105
+ def build_raw_gt_pairs(raw_txt_path, gt_txt_path, min_faces=None, max_faces=None):
106
+ raw_paths = read_dataset_paths(raw_txt_path)
107
+ gt_paths = read_dataset_paths(gt_txt_path)
108
+
109
+ # ===== 新增过滤逻辑 =====
110
+ filtered_raw_paths = []
111
+ for p in raw_paths:
112
+ if min_faces is None and max_faces is None:
113
+ filtered_raw_paths.append(p)
114
+ continue
115
+
116
+ faces = get_face_count(p)
117
+ if faces is not None:
118
+ # 检查是否在指定范围内
119
+ if min_faces is not None and faces < min_faces:
120
+ continue
121
+ if max_faces is not None and faces > max_faces:
122
+ continue
123
+ filtered_raw_paths.append(p)
124
+ else:
125
+ # 如果文件名中没找到面片数特征,默认保留(你也可以改为 continue 丢弃它)
126
+ filtered_raw_paths.append(p)
127
+
128
+ raw_paths = filtered_raw_paths
129
+ # ========================
130
+
131
+ # If two txt paths are the same source, pair each mesh with itself directly.
132
+ if Path(raw_txt_path).resolve() == Path(gt_txt_path).resolve():
133
+ return [(p, p) for p in raw_paths], []
134
+
135
+ gt_by_key = {}
136
+ for gt_path in gt_paths:
137
+ gt_by_key[gt_stem_to_pair_key(gt_path)] = gt_path
138
+
139
+ pairs = []
140
+ missing_gt = []
141
+ for raw_path in raw_paths:
142
+ key = raw_stem_to_pair_key(raw_path)
143
+ gt_path = gt_by_key.get(key)
144
+ if gt_path is None:
145
+ missing_gt.append(raw_path)
146
+ continue
147
+ pairs.append((raw_path, gt_path))
148
+
149
+ return pairs, missing_gt
150
+
151
+ def save_gt_mesh(mesh, out_dir, name):
152
+ gt_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False)
153
+ gt_mesh.vertices = gt_mesh.vertices[:, [1, 2, 0]]
154
+ gt_mesh.export(os.path.join(out_dir, f"{name}_gt.obj"))
155
+
156
+ def save_raw_gt_mesh(mesh, out_dir, name):
157
+ gt_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False)
158
+ gt_mesh.vertices = gt_mesh.vertices[:, [1, 2, 0]]
159
+ gt_mesh.export(os.path.join(out_dir, f"{name}_raw_gt.obj"))
160
+
161
+
162
+ def save_stage_input_mesh(mesh, out_dir, name, stage_in_res, stage_out_res, stage_in_depth):
163
+ coarse_mesh = trimesh.Trimesh(vertices=mesh.vertices.copy(), faces=mesh.faces.copy(), process=False)
164
+ coarse_mesh.vertices = coarse_mesh.vertices[:, [1, 2, 0]]
165
+ suffix = f"_stage_{stage_in_res}to{stage_out_res}_input_coarse"
166
+ coarse_mesh.export(os.path.join(out_dir, f"{name}{suffix}_depth{stage_in_depth}.obj"))
167
+
168
+
169
+ def save_stage_prediction(codec, raw_token, refine_token, out_dir, name, target_depth, suffix, fix_holes, raw_dis_mesh=None, gt_dis_mesh=None):
170
+ eos_mask = raw_token[0] == 0
171
+ if eos_mask.any():
172
+ raw_tokens = raw_token[0, : int(torch.where(eos_mask)[0][0])].detach().cpu().long().tolist()
173
+ else:
174
+ raw_tokens = raw_token[0].detach().cpu().long().tolist()
175
+
176
+ eos_mask_refine = refine_token[0] == 0
177
+ if eos_mask_refine.any():
178
+ refine_tokens = refine_token[0, : int(torch.where(eos_mask_refine)[0][0])].detach().cpu().long().tolist()
179
+ else:
180
+ refine_tokens = refine_token[0].detach().cpu().long().tolist()
181
+
182
+ shared_len = min(len(raw_tokens), len(refine_tokens))
183
+ block = (shared_len // 9) * 9
184
+ if block <= 0:
185
+ return None
186
+
187
+ raw_tokens = raw_tokens[:block]
188
+ refine_tokens = refine_tokens[:block]
189
+
190
+ interleaved = []
191
+ for i in range(0, block, 9):
192
+ interleaved.extend(raw_tokens[i : i + 9])
193
+ interleaved.extend(refine_tokens[i : i + 9])
194
+
195
+ mesh = codec.decode_tokens(interleaved, target_depth=target_depth-1)
196
+ if mesh is None:
197
+ return None
198
+
199
+ trimesh.repair.fix_normals(mesh)
200
+ if fix_holes:
201
+ verts = torch.tensor(mesh.vertices, device="cuda", dtype=torch.float32)
202
+ faces = torch.tensor(mesh.faces, device="cuda", dtype=torch.int32)
203
+ new_verts, new_faces = _fill_holes(verts, faces, debug=False, verbose=False, only_fill_holes=True)
204
+ mesh = trimesh.Trimesh(vertices=new_verts.cpu().numpy(), faces=new_faces.cpu().numpy())
205
+
206
+ return_mesh = mesh.copy()
207
+ mesh.vertices = mesh.vertices[:, [1, 2, 0]]
208
+ mesh.export(os.path.join(out_dir, f"{name}{suffix}_depth{target_depth}.obj"))
209
+
210
+ if raw_dis_mesh is not None:
211
+ raw_dis_mesh.vertices = raw_dis_mesh.vertices[:, [1, 2, 0]]
212
+ raw_dis_mesh.export(os.path.join(out_dir, f"{name}{suffix}_raw_dis_depth{target_depth-1}.obj"))
213
+ if gt_dis_mesh is not None:
214
+ gt_dis_mesh.vertices = gt_dis_mesh.vertices[:, [1, 2, 0]]
215
+ gt_dis_mesh.export(os.path.join(out_dir, f"{name}{suffix}_gt_dis_depth{target_depth}.obj"))
216
+ return return_mesh
217
+
218
+
219
+ def process_one_mesh(
220
+ raw_mesh_path,
221
+ gt_mesh_path,
222
+ model,
223
+ mask_token_id,
224
+ codec,
225
+ device,
226
+ pad_id,
227
+ token_shift,
228
+ config,
229
+ output_folder,
230
+ stage_resolutions,
231
+ fix_holes=True,
232
+ use_gt_pc=True
233
+ ):
234
+ import time
235
+ t_start = time.time()
236
+ import time
237
+ t_total_start = time.time()
238
+
239
+ name = Path(raw_mesh_path).stem
240
+ t_start = time.time()
241
+ mesh = trimesh.load(raw_mesh_path, force="mesh")
242
+ mesh.vertices = mesh.vertices[:, [2, 0, 1]]
243
+ gt_mesh = trimesh.load(gt_mesh_path, force="mesh")
244
+ gt_mesh.vertices = gt_mesh.vertices[:, [2, 0, 1]]
245
+ normalize(gt_mesh)
246
+ normalize(mesh)
247
+ save_raw_gt_mesh(mesh, output_folder, name)
248
+ save_gt_mesh(gt_mesh, output_folder, name)
249
+ t_load = time.time()
250
+ print(f"[{name}] Load & normalize time: {t_load - t_start:.4f}s")
251
+
252
+ stage_depths = [_depth_from_resolution(x) for x in stage_resolutions]
253
+
254
+ # First-stage input uses codec encode-decode result at the minimum resolution.
255
+ min_stage_depth = stage_depths[0]
256
+ t_start = time.time()
257
+ init_raw_token, _, init_quantized_mesh = codec.encode_mesh(mesh, target_depth=min_stage_depth)
258
+ init_mesh = codec.decode_raw_tokens(init_raw_token, target_depth=min_stage_depth)
259
+ t_init = time.time()
260
+ print(f"[{name}] Init encode & decode (depth={min_stage_depth}): {t_init - t_start:.4f}s")
261
+
262
+ if init_mesh is not None:
263
+ current_mesh = init_mesh
264
+ elif init_quantized_mesh is not None:
265
+ current_mesh = init_quantized_mesh
266
+ else:
267
+ current_mesh = mesh.copy()
268
+
269
+ for stage_idx in range(len(stage_depths) - 1):
270
+ stage_start_time = time.time()
271
+ stage_in_depth = stage_depths[stage_idx]
272
+ stage_out_depth = stage_depths[stage_idx + 1]
273
+ stage_in_res = stage_resolutions[stage_idx]
274
+ stage_out_res = stage_resolutions[stage_idx + 1]
275
+
276
+ save_stage_input_mesh(
277
+ mesh=current_mesh,
278
+ out_dir=output_folder,
279
+ name=name,
280
+ stage_in_res=stage_in_res,
281
+ stage_out_res=stage_out_res,
282
+ stage_in_depth=stage_in_depth,
283
+ )
284
+
285
+ t_start = time.time()
286
+ stage_data = mul_encode_mesh(
287
+ raw_mesh=current_mesh,
288
+ gt_mesh=gt_mesh,
289
+ codec=codec,
290
+ tokenizer_target_depth=stage_in_depth,
291
+ pc_num=config.data_processing.pc_num,
292
+ total_pc_num=config.data_processing.total_pc_num,
293
+ use_gt_pc=use_gt_pc,
294
+ clean=False
295
+ )
296
+ t_mul_encode = time.time()
297
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | mul_encode_mesh time: {t_mul_encode - t_start:.4f}s")
298
+
299
+ raw_token = stage_data["raw_token"].unsqueeze(0).to(device)
300
+ pc = stage_data["pc"].unsqueeze(0).to(device)
301
+
302
+ t_start = time.time()
303
+ # refine_token = sample_refine_tokens_with_topology(
304
+ # model,
305
+ # mask_token_id,
306
+ # raw_token,
307
+ # pc,
308
+ # use_gumbel=False,
309
+ # gumbel_temp_init=1.0,
310
+ # use_soft_consensus=False,
311
+ # consensus_start_step_ratio=0,
312
+ # cfg_scale=1.0,
313
+ # T_infer=config.evaluation.diffusion_sample_steps,
314
+ # )
315
+ refine_token = sample_refine_tokens_with_topology_fm(
316
+ model,
317
+ mask_token_id,
318
+ raw_token,
319
+ pc,
320
+ use_gumbel=False,
321
+ gumbel_temp_init=1.0,
322
+ use_soft_consensus=False,
323
+ consensus_start_step_ratio=0,
324
+ cfg_scale=1.0,
325
+ T_infer=config.evaluation.diffusion_sample_steps,
326
+ )
327
+
328
+ torch.cuda.synchronize()
329
+ t_sample = time.time()
330
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | sample_refine_tokens_with_topology time: {t_sample - t_start:.4f}s")
331
+
332
+ refine_token = restore_non_pad_token(refine_token, shift=token_shift, pad_id=pad_id)
333
+
334
+ t_start = time.time()
335
+ stage_suffix = f"_stage_{stage_in_res}to{stage_out_res}"
336
+ stage_mesh = save_stage_prediction(
337
+ codec=codec,
338
+ raw_token=raw_token,
339
+ refine_token=refine_token,
340
+ out_dir=output_folder,
341
+ name=name,
342
+ target_depth=stage_out_depth,
343
+ suffix=stage_suffix,
344
+ fix_holes=fix_holes,
345
+ raw_dis_mesh=stage_data.get("raw_dis_mesh"),
346
+ gt_dis_mesh=stage_data.get("gt_dis_mesh"),
347
+ )
348
+ t_save_pred = time.time()
349
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | save_stage_prediction (decode + fix) time: {t_save_pred - t_start:.4f}s")
350
+ print(f"[{name}] Stage {stage_in_res}->{stage_out_res} | total stage time: {time.time() - stage_start_time:.4f}s\n")
351
+
352
+ if stage_mesh is None:
353
+ break
354
+
355
+ current_mesh = stage_mesh
356
+
357
+ print(f"[{name}] Total process_one_mesh time: {time.time() - t_total_start:.4f}s")
358
+
359
+
360
+ class EMA:
361
+ def __init__(self, model, decay=0.999):
362
+ self.decay = decay
363
+ self.shadow = {}
364
+ self.register(model)
365
+
366
+ def register(self, model):
367
+ self.shadow = {}
368
+ for name, param in model.named_parameters():
369
+ if param.requires_grad:
370
+ self.shadow[name] = param.data.clone()
371
+
372
+ def load_state_dict(self, state):
373
+ self.decay = state.get("decay", self.decay)
374
+ self.shadow = {k: v.clone() for k, v in state["shadow"].items()}
375
+
376
+ @torch.no_grad()
377
+ def copy_to(self, model):
378
+ """Swap shadow weights into the model in-place (for inference)."""
379
+ copied, missing = 0, 0
380
+ for name, param in model.named_parameters():
381
+ if name in self.shadow:
382
+ param.data.copy_(self.shadow[name].to(device=param.device, dtype=param.dtype))
383
+ copied += 1
384
+ else:
385
+ missing += 1
386
+ print(f"[EMA.copy_to] copied {copied} params into model ({missing} not present in shadow)")
387
+
388
+
389
+ def run_inference_on_all_checkpoints(model, mask_token_id, accelerator, config, device, pad_id, codec, token_shift, ema=None):
390
+ ckpt_dir = "ckpt"
391
+ output_folder = "./pretrain_model_mul_more_data_64_to_256_eval_complex_test_no_fix_holes_no_clean_cfg_1.0_fm"
392
+
393
+ # raw_data_txt = "dataset/bpt_res_list.txt" /root/junkai_lin/mul_res_diffusion/demo/bpt_retrain_gt/data.txt
394
+ # gt_data_txt = "dataset/trellis_clean_mesh.txt" /root/junkai_lin/mul_res_diffusion/demo/bpt_gt/data.txt
395
+ raw_data_txt = "demo/complex_test_data/files/data.txt"
396
+ gt_data_txt = "demo/complex_test_data/files/data.txt"
397
+ # raw_data_txt = "/root/junkai_lin/mul_res_diffusion/demo/hun_yuan/data.txt"
398
+ # gt_data_txt = "/root/junkai_lin/mul_res_diffusion/demo/hun_yuan/data.txt"
399
+ stage_resolutions = [64, 128, 256]
400
+ # raw_data_txt = "demo/tripo 1.0/data.txt"
401
+ # gt_data_txt = "demo/tripo 1.0/data.txt"
402
+
403
+ use_gt_pc=True
404
+ fix_holes=False
405
+ if accelerator.is_main_process:
406
+ print(f"Using raw data txt: {raw_data_txt}")
407
+ print(f"Using gt data txt: {gt_data_txt}")
408
+ print(f"Searching checkpoints in: {ckpt_dir}")
409
+
410
+ # 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)
411
+ mesh_pairs, missing_gt = build_raw_gt_pairs(raw_data_txt, gt_data_txt, max_faces=20000, min_faces=500)
412
+ if accelerator.is_main_process:
413
+ print(f"Matched raw/gt pairs: {len(mesh_pairs)}")
414
+ if missing_gt:
415
+ print(f"Raw without matched gt: {len(missing_gt)}")
416
+
417
+ search_pattern = os.path.join(ckpt_dir, "checkpoint_*_epoch_*_loss.pth")
418
+ ckpt_files = glob.glob(search_pattern)
419
+ if not ckpt_files:
420
+ if accelerator.is_main_process:
421
+ print("No checkpoints found!")
422
+ return
423
+
424
+ ckpt_data = []
425
+ for f in ckpt_files:
426
+ match = re.search(r"checkpoint_(\d+)_epoch_", f)
427
+ if match:
428
+ epoch = int(match.group(1))
429
+ ckpt_data.append((epoch, f))
430
+ ckpt_data.sort(key=lambda x: x[0])
431
+
432
+ # stage_resolutions = [32, 64, 128, 256]
433
+
434
+
435
+ # stage_resolutions = [128, 256]
436
+
437
+ for epoch, ckpt_path in ckpt_data:
438
+ if accelerator.is_main_process:
439
+ print(f"\n[{'='*40}]")
440
+ print(f"Starting inference for epoch {epoch}")
441
+ print(f"Loading weights from: {ckpt_path}")
442
+
443
+ output_folder_ply = os.path.join(output_folder, f"{epoch}_epoch_val_generate_mul")
444
+ if accelerator.is_main_process:
445
+ os.makedirs(output_folder_ply, exist_ok=True)
446
+
447
+ checkpoint = torch.load(ckpt_path, map_location=device)
448
+ model_to_load = accelerator.unwrap_model(model)
449
+ # Use strict=False so we can load older ckpts (no null_cond) into the
450
+ # newer model class, and vice versa. Missing/unexpected keys are logged.
451
+ load_result = model_to_load.load_state_dict(checkpoint["model_state_dict"], strict=False)
452
+ missing = list(getattr(load_result, "missing_keys", []) or [])
453
+ unexpected = list(getattr(load_result, "unexpected_keys", []) or [])
454
+ if (missing or unexpected) and accelerator.is_main_process:
455
+ print(f"[load_state_dict] strict=False -> missing={len(missing)} unexpected={len(unexpected)}")
456
+ if missing:
457
+ print(f" missing: {missing[:8]}{' ...' if len(missing) > 8 else ''}")
458
+ if unexpected:
459
+ print(f" unexpected: {unexpected[:8]}{' ...' if len(unexpected) > 8 else ''}")
460
+
461
+ # Auto-detect EMA: if the ckpt has an `ema_state_dict`, swap those
462
+ # shadow weights into the model for inference. Otherwise fall back to
463
+ # the raw `model_state_dict` we already loaded above. No reliance on
464
+ # config `use_ema` / a separate EMA object — just check the ckpt.
465
+ ema_state = checkpoint.get("ema_state_dict")
466
+ shadow = ema_state.get("shadow") if isinstance(ema_state, dict) else None
467
+ if shadow:
468
+ with torch.no_grad():
469
+ copied, skipped = 0, 0
470
+ for name, param in model_to_load.named_parameters():
471
+ if name in shadow:
472
+ param.data.copy_(shadow[name].to(device=param.device, dtype=param.dtype))
473
+ copied += 1
474
+ else:
475
+ skipped += 1
476
+ if accelerator.is_main_process:
477
+ msg = f"[Epoch {epoch}] EMA shadow detected in ckpt -> applied to model ({copied} params"
478
+ if skipped:
479
+ msg += f", {skipped} model params not in shadow"
480
+ print(msg + ")")
481
+ else:
482
+ if accelerator.is_main_process:
483
+ print(f"[Epoch {epoch}] no EMA in ckpt -> running with raw model_state_dict weights.")
484
+
485
+ with torch.no_grad():
486
+ model.eval()
487
+ pbar = tqdm(
488
+ enumerate(mesh_pairs),
489
+ total=len(mesh_pairs),
490
+ desc=f"Infer Epoch {epoch}",
491
+ disable=not accelerator.is_local_main_process,
492
+ dynamic_ncols=True,
493
+ )
494
+ for line_idx, (raw_mesh_path, gt_mesh_path) in pbar:
495
+ if line_idx % accelerator.num_processes != accelerator.process_index:
496
+ continue
497
+ try:
498
+ process_one_mesh(
499
+ raw_mesh_path=raw_mesh_path,
500
+ gt_mesh_path=gt_mesh_path,
501
+ model=model,
502
+ mask_token_id=mask_token_id,
503
+ codec=codec,
504
+ device=device,
505
+ pad_id=pad_id,
506
+ token_shift=token_shift,
507
+ config=config,
508
+ output_folder=output_folder_ply,
509
+ stage_resolutions=stage_resolutions,
510
+ fix_holes=fix_holes,
511
+ use_gt_pc=use_gt_pc,
512
+ )
513
+ except Exception as e:
514
+ print(f"[infer error] raw={raw_mesh_path}, gt={gt_mesh_path}: {str(e)}")
515
+
516
+ torch.cuda.empty_cache()
517
+ gc.collect()
518
+
519
+ if accelerator.is_main_process:
520
+ print("\nBatch inference complete for all checkpoints.")
521
+
522
+
523
+ def main(config):
524
+ print("start init Accelerator...")
525
+ ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=True)
526
+ accelerator = Accelerator(
527
+ gradient_accumulation_steps=config.accelerator.gradient_accumulation_steps,
528
+ mixed_precision=config.accelerator.mixed_precision,
529
+ kwargs_handlers=[ddp_kwargs],
530
+ )
531
+ device = accelerator.device
532
+
533
+ codec = MeshCodec(args=config.tokenizer)
534
+ codec_config = codec.get_model_config()
535
+ input_num_categories = codec_config["input_num_categories"]
536
+ lm_num_categories = codec_config["output_num_categories"]
537
+ pad_id = codec_config["pad_token_id"]
538
+ token_shift = input_num_categories - 1
539
+
540
+ model = DiscreteDiffusionTransformer(
541
+ vocab_A=input_num_categories,
542
+ vocab_B=lm_num_categories,
543
+ max_seq_len=config.data_processing.max_len,
544
+ d_model=config.model.feature_dim,
545
+ encoder_freeze=config.model.encoder_freeze,
546
+ nhead=config.model.num_heads,
547
+ depth=config.model.hourglass_depth,
548
+ dropout=config.model.dropout,
549
+ time_embed_scale=config.training.diffusion_train_steps,
550
+ )
551
+ mask_token_id = model.mask_token_id
552
+
553
+ use_ema = getattr(config.training, "use_ema", False)
554
+ ema_decay = getattr(config.training, "ema_decay", 0.999)
555
+ ema = EMA(model, decay=ema_decay) if use_ema else None
556
+
557
+ model = accelerator.prepare(model)
558
+
559
+ run_inference_on_all_checkpoints(
560
+ model=model,
561
+ mask_token_id=mask_token_id,
562
+ accelerator=accelerator,
563
+ config=config,
564
+ device=device,
565
+ pad_id=pad_id,
566
+ codec=codec,
567
+ token_shift=token_shift,
568
+ ema=ema,
569
+ )
570
+
571
+
572
+ if __name__ == "__main__":
573
+ set_seed(42)
574
+ config = load_config()
575
+ main(config)
mulres_infer/modules/token_utils.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """fix3 编码格式的数据通路工具 —— 从训练脚本抽出, 保证推理与训练用同一份实现。
2
+
3
+ split_fix3_token_list : 扁平 60/face token -> (coord_list[9*nface], subcode_list[9*nface,8] local)
4
+ subcodes_to_flat_token : (coord, 采样得到的 subcodes) -> fix3 扁平 60/face token(含 offset), 供 decode_tokens
5
+ """
6
+ import torch
7
+
8
+ from models.generator import REFINE_POS_PER_FACE, NUM_OCTANTS
9
+
10
+ TOKENS_PER_FACE = 60 # fix3 扁平: 9 coord + 3 F + 6 矩阵 x 8 行
11
+
12
+ def _trim_trailing_pad(token, pad_id):
13
+ pad_pos = (token == pad_id).nonzero(as_tuple=True)[0]
14
+ return token[:pad_pos[0].item()] if pad_pos.numel() > 0 else token
15
+ def split_fix3_token_list(token_list, pad_id, device, codec_cfg):
16
+ """扁平 60/face token -> (coord_list, subcode_list)。
17
+ coord_list[i] : [9*nface] 坐标(保留 coord_offset, 直接喂 raw_embedding)
18
+ subcode_list[i] : [9*nface, 8] refine 子码(去 offset, local[0,256))
19
+ 位置顺序 [F0 F1 F2 M00 M11 M22 M01 M02 M12]
20
+ F 位置只用 slot0(其余槽填 0, 模型按类型只读 slot0);intra/inter 用 8 行
21
+ """
22
+ f_off = codec_cfg["f_offset"]; ia = codec_cfg["intra_row_offset"]; ie = codec_cfg["inter_row_offset"]
23
+ coord_list, sub_list = [], []
24
+ for tok in token_list:
25
+ tok = _trim_trailing_pad(tok.to(device=device, dtype=torch.long), pad_id)
26
+ if tok.numel() % TOKENS_PER_FACE != 0:
27
+ raise ValueError(f"token 长度 {tok.numel()} 不是 {TOKENS_PER_FACE} 的倍数")
28
+ tok = tok.view(-1, TOKENS_PER_FACE) # [nface, 60]
29
+ nface = tok.size(0)
30
+ coord = tok[:, :9].reshape(-1) # [9*nface]
31
+ Fv = tok[:, 9:12] - f_off # [nface, 3]
32
+ intra = (tok[:, 12:36] - ia).view(nface, 3, NUM_OCTANTS) # [nface,3,8]
33
+ inter = (tok[:, 36:60] - ie).view(nface, 3, NUM_OCTANTS) # [nface,3,8]
34
+ sub = torch.zeros(nface, REFINE_POS_PER_FACE, NUM_OCTANTS, dtype=torch.long, device=device)
35
+ sub[:, 0:3, 0] = Fv # F -> slot0
36
+ sub[:, 3:6, :] = intra
37
+ sub[:, 6:9, :] = inter
38
+ sub_list.append(sub.reshape(-1, NUM_OCTANTS)) # [9*nface, 8]
39
+ coord_list.append(coord)
40
+ return coord_list, sub_list
41
+ def subcodes_to_flat_token(coord_seq, sub_seq, codec_cfg):
42
+ """采样得到的 (coord[9*nface], subcodes[9*nface,8] local) 还原成 fix3 扁平 60/face token(含 offset),
43
+ 供 codec.decode_tokens 使用。F 位置取 slot0。"""
44
+ f_off = codec_cfg["f_offset"]; ia = codec_cfg["intra_row_offset"]; ie = codec_cfg["inter_row_offset"]
45
+ nface = coord_seq.numel() // REFINE_POS_PER_FACE
46
+ coord = coord_seq.view(nface, REFINE_POS_PER_FACE) # [nface,9]
47
+ sub = sub_seq.view(nface, REFINE_POS_PER_FACE, NUM_OCTANTS) # [nface,9,8]
48
+ Fv = sub[:, 0:3, 0] + f_off # [nface,3] 取 slot0
49
+ intra = sub[:, 3:6, :] + ia # [nface,3,8]
50
+ inter = sub[:, 6:9, :] + ie
51
+ flat = torch.cat([coord, Fv, intra.reshape(nface, 24), inter.reshape(nface, 24)], dim=1) # [nface,60]
52
+ return flat.reshape(-1).tolist()
mulres_infer/readme.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 多分辨率网格生成 · 推理
2
+
3
+ 点云条件下的**多分辨率八叉树网格生成**:低分辨率生成器逐级长出粗网格,高分辨率细分器把它推到 512 格。
4
+
5
+ ## 流程
6
+
7
+ ```
8
+ 输入 mesh ──采样点云──┐
9
+ ▼
10
+ ┌───────── 生成器 (TSSR 三前向离散扩散) ─────────┐
11
+ │ 格2 → 格4 → 格8 → 格16 → 格32 → 格64 每级20步 │
12
+ │ └补洞┘ └补洞┘ │
13
+ └──────────────────────┬─────────────────────────┘
14
+ ▼
15
+ ┌────────── 细分器 (acc_uni) ──────────┐
16
+ │ 格64 → 格128 → 格256 → 格512 每级20步 │
17
+ └──────────────────────────────────────┘
18
+ ▼
19
+ final_res512_plain.obj
20
+ ```
21
+
22
+ **生成器**每级把上一级网格重新编码成八叉树 token,用离散扩散预测该级完整子码。
23
+ TSSR 解码每步三次前向:mask 预测 → no-mask 精修 → 逐 token 分类器挑可疑位置 re-mask,
24
+ 避免 MaskGIT 式"一旦揭示即定死"的错误累积。
25
+
26
+ **细分器**只做细分不做生成,粗输入上的破洞它补不出来——这是 res32/res64 要先补洞再喂下去的原因。
27
+
28
+ ## 目录
29
+
30
+ ```
31
+ .
32
+ ├── scripts/ 入口
33
+ │ ├── infer.py 端到端推理
34
+ │ ├── eval.py 评测(CD / HD / 法向一致性)
35
+ │ └── strip_ckpt.py 权重瘦身: 剥离优化器、合并 EMA
36
+ ├── models/ 模型
37
+ │ ├── generator.py 生成器(骨干 → 双云注入 → TSSR)
38
+ │ ├── subdivider.py 细分器
39
+ │ └── point_conditioner.py 点云条件编码器
40
+ ├── modules/ 算子与管线
41
+ │ ├── pipeline.py 多级级联主管线
42
+ │ ├── attention.py MLA 等注意力原语
43
+ │ ├── octree_codec.py 八叉树 tokenizer(numba 加速)
44
+ │ ├── octree_codec_acc.py 细分器口径的 tokenizer
45
+ │ ├── octree_decode.py 子码 → mesh 解码
46
+ │ ├── token_utils.py token 通路
47
+ │ └── miche/ Michelangelo 点云编码器(第三方)
48
+ ├── dataset/ 数据处理与点云采样
49
+ ├── utils/ 配置、网格工具、补洞、指标
50
+ ├── configs/ generator.yaml / subdivider.yaml
51
+ └── ckpt/ 权重(见 ckpt/README.md)
52
+ ```
53
+
54
+ ## 安装
55
+
56
+ ```bash
57
+ conda create -n mulres python=3.12 -y && conda activate mulres
58
+ pip install -r requirements.txt
59
+ # flash-attn 用与 torch/CUDA 匹配的预编译 wheel, 不要从源码构建
60
+ ```
61
+
62
+ ## 使用
63
+
64
+ ```bash
65
+ # 全部 mesh
66
+ python scripts/infer.py --data /path/to/meshes --out_dir outputs/run1
67
+
68
+ # 只跑前 20 个
69
+ python scripts/infer.py --data /path/to/meshes --out_dir outputs/run1 --ncase 20
70
+
71
+ # 指定 case(文件名子串, 带尾点防前缀误配: "a." 不匹配 "a1.glb")
72
+ python scripts/infer.py --data /path/to/meshes --out_dir outputs/run1 --case "model_a.,model_b."
73
+
74
+ # 面数太密: 给连接头"空连接"类加 logit 偏置(0.3 约 -25% 面, 保真度几乎不变)
75
+ python scripts/infer.py --data /path/to/meshes --out_dir outputs/run1 --empty_bias 0.3
76
+ ```
77
+
78
+ 每个 case 输出一个子目录:
79
+
80
+ | 文件 | 说明 |
81
+ |---|---|
82
+ | `final_res512_plain.obj` | **最终结果** |
83
+ | `res{2,4,8,16,32,64}_plain.obj` | 生成器各级产物(补洞后) |
84
+ | `*_sr_64to128_depth7.obj` 等 | 细分器各级产物 |
85
+ | `gt_original.obj` | 归一化后的原始输入(塌缩前) |
86
+ | `gt_original_dec.obj` | 塌缩后的输入(级联起点与条件点云的来源) |
87
+ | `gt_res{N}.obj` | 输入经 tokenizer 编解码的天花板, 用于逐级对照 |
88
+ | `pc_full.ply` / `pc_target_res{N}.ply` | 注入的条件点云 |
89
+
90
+ ## 评测
91
+
92
+ ```bash
93
+ # 预测目录需扁平: <case>_y.obj ; GT 目录: <case>.glb 或 .obj
94
+ python scripts/eval.py /path/to/pred_flat --gt /path/to/gt_meshes
95
+ ```
96
+
97
+ 输出倒角距离(L1/L2)、豪斯多夫距离、法向一致性 |NC| 与 NC。
98
+
99
+ > 解码不编码面的绕序,有向法向随机,所以看 **|NC|**(约 0.85),`NC (Real)` 接近 0 属预期。
100
+
101
+ ## 参数
102
+
103
+ `scripts/infer.py` 固化了默认配方,更细的控制用环境变量(定义见 `modules/pipeline.py` 顶部):
104
+
105
+ | 变量 | 默认 | 说明 |
106
+ |---|---|---|
107
+ | `STAGE` | `2,4,8,16,32,64` | 生成器级联阶梯(产物真实体素格) |
108
+ | `STEPS` | `20` | 生成器每级扩散步数 |
109
+ | `SUPERRES_TO` / `SUPERRES_STEPS` | `512` / `20` | 细分终点与每级步数 |
110
+ | `FILL` / `FILL_MIN_RES` | `1` / `0` | 级联内补洞; `FILL_MIN_RES=64` 则只在格 64 补 |
111
+ | `SR_FILL_IN` / `SUPERRES_FIX_HOLES` | `1` / `1` | 细分入口 / 每级输出补洞 |
112
+ | `DECIMATE_RATIO` | `0.5` | 输入预塌缩比(0=不塌缩) |
113
+ | `EMPTY_BIAS` | `0` | >0 稀疏化控面数 |
114
+ | `CASE_STEM` | — | 文件名子串过滤(在 `NCASE` 截断**之后**生效) |
115
+
116
+ ## 已知行为
117
+
118
+ - **步数**:20 与 40 步保真度无差别;10 步会让开边 +27%、非流形 +34%。20 是性价比点。
119
+ - **补洞是置换不是净赚**:开边 −16% 的代价是非流形 +12%。要纯净拓扑用 `--no_fill`。
120
+ - **分辨率终点**:生成器自己走到格 64 比走到格 32 再交给细分器更好(非流形 −59%、CD −6%),
121
+ 代价是开边变多——细分器会把粗输入的破洞一路放大。
122
+ - **GPU 非确定性**:flash-attn / bf16 的归约顺序不固定,同配置重跑面数有约 3% 波动,单 case 开边波动更大。
mulres_infer/requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 核心
2
+ torch==2.7.0
3
+ numpy
4
+ scipy
5
+ trimesh==5.1.0
6
+ numba
7
+ einops
8
+ omegaconf
9
+ transformers
10
+ accelerate
11
+ timm
12
+ scikit-image
13
+ pymeshfix
14
+ pyvista
15
+ tqdm
16
+
17
+ # flash-attn 需与 torch/CUDA 匹配, 用官方预编译 wheel 安装, 不要从源码构建:
18
+ # pip install flash_attn-2.8.3+cu12torch2.7cxx11abiFALSE-cp312-cp312-linux_x86_64.whl
19
+
20
+ # 可选: utils/postprocessing_utils.py 顶部导入, 但补洞路径实际不调用其功能
21
+ # utils3d @ git+https://github.com/EasternJournalist/utils3d.git
22
+ # nvdiffrast @ git+https://github.com/NVlabs/nvdiffrast.git
mulres_infer/scripts/__init__.py ADDED
File without changes
mulres_infer/scripts/eval.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os, sys
3
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
4
+
5
+ from utils.distance import calculate_distances_numpy, calculate_normal_consistency_score
6
+ import os
7
+ import numpy as np
8
+ from tqdm import tqdm
9
+ import trimesh
10
+ import json
11
+ import glob
12
+ import multiprocessing # 导入多进程模块
13
+ MIN_FACE_AMOUNT = 0
14
+ MAX_FACE_AMOUNT = 100000
15
+ # ===============================================================
16
+ # 第一步:你的距离计算函数 (无变化)
17
+ # ===============================================================
18
+ pc_num = 1024
19
+
20
+ def normalize(mesh, file_path):
21
+ # ... 现有代码 ...
22
+ bounds = np.array([mesh.vertices.min(axis=0), mesh.vertices.max(axis=0)])
23
+ mesh.vertices = mesh.vertices - (bounds[0] + bounds[1])[None, :] / 2
24
+ mesh.vertices = mesh.vertices / (bounds[1] - bounds[0]).max()
25
+ mesh.vertices = mesh.vertices.clip(-0.5, 0.5) # 限制顶点坐标在[-0.5, 0.5]
26
+
27
+ # ===============================================================
28
+ # 第二步:读取 .obj 文件的函数 (无变化)
29
+ # ===============================================================
30
+ def sample_from_obj(file_path, alter=False):
31
+ # ... 现有代码 ...
32
+ """
33
+ 从 .obj 文件中加载顶点坐标。
34
+ """
35
+ mesh = trimesh.load(file_path, force="mesh")
36
+ if alter:
37
+ mesh.vertices = mesh.vertices[:, [1,2,0]]
38
+ normalize(mesh, file_path)
39
+ points, face_idx = mesh.sample(pc_num, return_index=True)
40
+ normals = mesh.face_normals[face_idx]
41
+ return points,normals
42
+
43
+ # ===============================================================
44
+ # [修改] 法向量一致性计算
45
+ # ===============================================================
46
+
47
+ # ===============================================================
48
+ # 第三步:[修改] 多进程工作函数
49
+ # ===============================================================
50
+
51
+ def process_common_file_worker(base_name, gt_folder, folder_paths):
52
+ """
53
+ [已修改] 这是一个工作进程执行的任务:
54
+ 处理单个 base_name 对应的所有文件夹。
55
+ 它会加载一次GT文件,然后遍历所有预测文件夹进行计算。
56
+ 返回 L1, L2 CD 距离, |NC| 和 NC。
57
+ """
58
+
59
+ # 1. 查找并加载 GT 文件
60
+ gt_filename = base_name + '.glb'
61
+ # ... 现有代码 ...
62
+ gt_filepath = os.path.join(gt_folder, gt_filename)
63
+
64
+ if not os.path.exists(gt_filepath):
65
+ gt_filename = base_name + '.obj'
66
+ # ... 现有代码 ...
67
+ gt_filepath = os.path.join(gt_folder, gt_filename)
68
+ if not os.path.exists(gt_filepath):
69
+ # 警告:在工作进程中打印 (print) 不是线程安全的,
70
+ # ... 现有代码 ...
71
+ # 但返回空列表是安全的,主进程会处理它。
72
+ # print(f"\n警告:共同文件 '{base_name}' 对应的GT文件 '{gt_filepath}' 不存在,已跳过。")
73
+ return [] # 返回空列表表示此文件处理失败
74
+ mesh = trimesh.load(gt_filepath, force="mesh")
75
+ if len(mesh.faces) < MIN_FACE_AMOUNT or len(mesh.faces) > MAX_FACE_AMOUNT:
76
+ return []
77
+
78
+ try:
79
+ pcd_gt, normal_gt = sample_from_obj(gt_filepath)
80
+ if pcd_gt is None or pcd_gt.size == 0:
81
+ # ... 现有代码 ...
82
+ # print(f"\n警告:加载GT文件 {gt_filename} 失败或点云为空。已跳过。")
83
+ return []
84
+ except Exception as e:
85
+ # ... 现有代码 ...
86
+ # print(f"\n加载GT文件 {gt_filename} 时发生错误: {e}。已跳过。")
87
+ return []
88
+
89
+ # 2. 遍历所有文件夹,加载 Pred 文件并计算
90
+ results_for_this_file = []
91
+
92
+ for folder_path in folder_paths:
93
+ pred_filename = base_name + '_y.obj'
94
+ # ... 现有代码 ...
95
+ pred_filepath = os.path.join(folder_path, pred_filename)
96
+
97
+ try:
98
+ pcd_pred, normal_pred = sample_from_obj(pred_filepath)
99
+ if pcd_pred is None or pcd_pred.size == 0:
100
+ # ... 现有代码 ...
101
+ # print(f"\n警告:加载文件 {pred_filepath} 失败或点云为空。已跳过。")
102
+ results_for_this_file.append((folder_path, None, None, None, None, None)) # [修改] 6 个 None
103
+ continue
104
+
105
+ # [修改] 接收 L1 和 L2 CD
106
+ chamfer_l2, hausdorff, chamfer_l1 = calculate_distances_numpy(pcd_gt, pcd_pred)
107
+ # [修改] 接收两个 NC 分数
108
+ normal_error_abs, normal_error_real = calculate_normal_consistency_score(pcd_gt, normal_gt, pcd_pred, normal_pred)
109
+
110
+ # [修改] 存储有效结果 (6个值)
111
+ results_for_this_file.append((folder_path, chamfer_l2, hausdorff, normal_error_abs, chamfer_l1, normal_error_real))
112
+
113
+ except Exception as e:
114
+ # ... 现有代码 ...
115
+ # print(f"\n计算文件对 {pred_filepath} 时发生错误: {e}。已跳过。")
116
+ results_for_this_file.append((folder_path, None, None, None, None, None)) # [修改] 6 个 None
117
+
118
+ # 3. 返回此 base_name 对应的所有结果
119
+ return results_for_this_file
120
+
121
+ def starmap_helper(args):
122
+ # ... 现有代码 ...
123
+ """
124
+ [新增] 辅助函数,用于解包 pool.imap 的参数,使其能接受多个参数。
125
+ """
126
+ return process_common_file_worker(*args)
127
+
128
+ # ===============================================================
129
+ # 第四步:[修改] 评估函数,使用多进程
130
+ # ===============================================================
131
+
132
+ def evaluate_common_files(folder_paths, gt_folder, base_dir):
133
+ """
134
+ [已修改] 评估多个文件夹中 *共有* 的文件,并计算总平均指标和逐文件夹的平均指标。
135
+ 现在使用 multiprocessing.Pool 来并行处理文件。
136
+ """
137
+ if not folder_paths:
138
+ # ... 现有代码 ...
139
+ print("错误:没有提供用于评估共同文件的文件夹。")
140
+ return None
141
+
142
+ print("\n" + "="*80)
143
+ # ... 现有代码 ...
144
+ print(" 正在计算共同文件指标 ".center(80, "="))
145
+ print(f"总共 {len(folder_paths)} 个文件夹参与比较。")
146
+
147
+ # 1. 查找每个文件夹中的文件基础名(不含 '_y.obj')(无变化)
148
+ files_by_folder = {}
149
+ # ... 现有代码 ...
150
+ for folder_path in folder_paths:
151
+ try:
152
+ all_files = os.listdir(folder_path)
153
+ # ... 现有代码 ...
154
+ base_names = set([f.replace('_y.obj', '') for f in all_files
155
+ if (f.endswith('_y.obj') and not f.endswith('_gt_y.obj') and not f.endswith("input_y.obj"))]
156
+ )
157
+ files_by_folder[folder_path] = base_names
158
+ # ... 现有代码 ...
159
+ print(f" ‐ 文件夹 '{os.path.basename(folder_path)}' 找到 {len(base_names)} 个文件。")
160
+ except Exception as e:
161
+ print(f" ‐ 错误:无法读取文件夹 {folder_path}: {e}")
162
+ # ... 现有代码 ...
163
+ files_by_folder[folder_path] = set()
164
+
165
+ # 2. 找到所有文件夹中 *共有* 的文件基础名 (无变化)
166
+ common_base_names = set.intersection(*files_by_folder.values())
167
+
168
+ if not common_base_names:
169
+ # ... 现有代码 ...
170
+ print("\n错误:在所有文件夹中没有找到任何共有的 `_y.obj` 文件。")
171
+ print("="*80)
172
+ return None
173
+
174
+ print(f"\n在所有文件夹中找到了 {len(common_base_names)} 个共同文件。将使用多进程处理...")
175
+
176
+ # 3. [修改] 初始化指标存储
177
+ global_chamfer_l2 = []
178
+ global_hausdorff = []
179
+ global_normal_abs = [] # [修改]
180
+ global_chamfer_l1 = []
181
+ global_normal_real = [] # [新增]
182
+
183
+ per_folder_metrics = {path: {'chamfer_l2': [], 'hausdorff': [], 'normal_abs': [], 'chamfer_l1': [], 'normal_real': []} for path in folder_paths} # [修改]
184
+
185
+ # 4. [修改] 设置并运行多进程任务
186
+
187
+ # 准备任务参数列表
188
+ # ... 现有代码 ...
189
+ task_args = [(base_name, gt_folder, folder_paths) for base_name in common_base_names]
190
+ all_results_list = []
191
+
192
+ # 确定进程数,可以适当调整,例如 os.cpu_count()
193
+ # ... 现有代码 ...
194
+ # 留出2个核心给系统,防止卡死
195
+ num_processes = max(1, 60)
196
+ print(f"启动 {num_processes} 个工作进程...")
197
+
198
+ # 创建进程池
199
+ # ... 现有代码 ...
200
+ with multiprocessing.Pool(processes=num_processes) as pool:
201
+ # 使用 pool.imap 来迭代任务,并用 tqdm 显示进度条
202
+ # imap 会按顺序返回结果,同时保持并行执行
203
+ # ... 现有代码 ...
204
+ for result in tqdm(pool.imap(starmap_helper, task_args),
205
+ total=len(common_base_names),
206
+ desc="评估共同文件"):
207
+ all_results_list.append(result)
208
+
209
+ # 5. [修改] 聚合所有进程返回的结果
210
+ print("\n正在聚合所有进程的结果...")
211
+ valid_pairs_processed = 0
212
+ for results_for_this_file in all_results_list:
213
+ if not results_for_this_file:
214
+ # ... 现有代码 ...
215
+ # GT 文件加载失败或跳过
216
+ continue
217
+
218
+ # [修改] 解包 L1, L2, |NC|, NC
219
+ for (folder_path, chamfer_l2, hausdorff, normal_error_abs, chamfer_l1, normal_error_real) in results_for_this_file:
220
+ if chamfer_l2 is not None and hausdorff is not None and normal_error_abs is not None and chamfer_l1 is not None and normal_error_real is not None:
221
+ # 这是一个有效的结果
222
+ global_chamfer_l2.append(chamfer_l2)
223
+ global_hausdorff.append(hausdorff)
224
+ global_normal_abs.append(normal_error_abs) # [修改]
225
+ global_chamfer_l1.append(chamfer_l1)
226
+ global_normal_real.append(normal_error_real) # [新增]
227
+
228
+ per_folder_metrics[folder_path]['chamfer_l2'].append(chamfer_l2)
229
+ per_folder_metrics[folder_path]['hausdorff'].append(hausdorff)
230
+ per_folder_metrics[folder_path]['normal_abs'].append(normal_error_abs) # [修改]
231
+ per_folder_metrics[folder_path]['chamfer_l1'].append(chamfer_l1)
232
+ per_folder_metrics[folder_path]['normal_real'].append(normal_error_real) # [新增]
233
+ valid_pairs_processed += 1
234
+ else:
235
+ # ... 现有代码 ...
236
+ # 预测文件加载失败或计算错误
237
+ # 可以在这里添加日志
238
+ pass
239
+
240
+ print(f"聚合完成。总共处理了 {valid_pairs_processed} 个有效的 (GT, Pred) 文件对。")
241
+
242
+ # 6. [修改] 计算最终平均值
243
+ if not global_chamfer_l2: # [修改]
244
+ print("\n错误:未能成功计算任何共同文件的指标。")
245
+ print("="*80)
246
+ return None
247
+
248
+ # [修改] 计算总平均
249
+ global_avg_results = {
250
+ "evaluated_pairs_total": len(global_chamfer_l2),
251
+ "common_files_count": len(common_base_names),
252
+ "folders_count": len(folder_paths),
253
+ "avg_chamfer_distance_l2": np.mean(global_chamfer_l2),
254
+ "avg_hausdorff_distance": np.mean(global_hausdorff),
255
+ "avg_normal_consistency_abs": np.mean(global_normal_abs), # [修改]
256
+ "avg_chamfer_distance_l1": np.mean(global_chamfer_l1),
257
+ "avg_normal_consistency_real": np.mean(global_normal_real) # [新增]
258
+ }
259
+
260
+ # [修改] 计算逐文件夹平均
261
+ per_folder_avg_results = {}
262
+ for folder_path, metrics in per_folder_metrics.items():
263
+ folder_name = os.path.basename(folder_path)
264
+ if metrics['chamfer_l2']: # [修改]
265
+ per_folder_avg_results[folder_name] = {
266
+ "evaluated_pairs": len(metrics['chamfer_l2']),
267
+ "avg_chamfer_distance_l2": np.mean(metrics['chamfer_l2']),
268
+ "avg_hausdorff_distance": np.mean(metrics['hausdorff']),
269
+ "avg_normal_consistency_abs": np.mean(metrics['normal_abs']), # [修改]
270
+ "avg_chamfer_distance_l1": np.mean(metrics['chamfer_l1']),
271
+ "avg_normal_consistency_real": np.mean(metrics['normal_real']) # [新增]
272
+ }
273
+ else:
274
+ per_folder_avg_results[folder_name] = {
275
+ "evaluated_pairs": 0,
276
+ "avg_chamfer_distance_l2": 0,
277
+ "avg_hausdorff_distance": 0,
278
+ "avg_normal_consistency_abs": 0, # [修改]
279
+ "avg_chamfer_distance_l1": 0,
280
+ "avg_normal_consistency_real": 0 # [新增]
281
+ }
282
+
283
+ # 7. [修改] 打印共同文件评估总结
284
+ print("\n--- 共同文件评估总结 ---")
285
+ print(f"\n全局平均 (基于 {len(global_chamfer_l2)} 个总比较对, {len(common_base_names)} 个文件 x {len(folder_paths)} 个文件夹):")
286
+ print(f" ‐ 平均倒角距离 (L2): {global_avg_results['avg_chamfer_distance_l2']:.6f}")
287
+ print(f" ‐ 平均倒角距离 (L1): {global_avg_results['avg_chamfer_distance_l1']:.6f}")
288
+ print(f" ‐ 平均豪斯多夫距离: {global_avg_results['avg_hausdorff_distance']:.6f}")
289
+ print(f" ‐ 平均法向量 |NC| (Abs): {global_avg_results['avg_normal_consistency_abs']:.6f}") # [修改]
290
+ print(f" ‐ 平均法向量 NC (Real): {global_avg_results['avg_normal_consistency_real']:.6f}") # [新增]
291
+
292
+ print("\n逐文件夹平均 (仅基于共同文件):")
293
+ for folder_name, results in per_folder_avg_results.items():
294
+ print(f" ‐ 文件夹: {folder_name}")
295
+ # [修改] 打印所有6个指标
296
+ print(f" ‐ 倒角(L2): {results['avg_chamfer_distance_l2']:.6f}, 倒角(L1): {results['avg_chamfer_distance_l1']:.6f}, 豪斯多夫: {results['avg_hausdorff_distance']:.6f}, 法向量|NC|: {results['avg_normal_consistency_abs']:.6f}, 法向量NC: {results['avg_normal_consistency_real']:.6f} (共 {results['evaluated_pairs']} 对)")
297
+
298
+ print("="*80)
299
+
300
+ # 8. 保存结果 (无变化)
301
+ final_results = {
302
+ # ... 现有代码 ...
303
+ "global_average_on_common_files": global_avg_results,
304
+ "per_folder_average_on_common_files": per_folder_avg_results
305
+ }
306
+
307
+ output_filepath = os.path.join(base_dir, "common_evaluation_results.json")
308
+ # ... 现有代码 ...
309
+ try:
310
+ with open(output_filepath, 'w', encoding='utf-8') as f:
311
+ json.dump(final_results, f, ensure_ascii=False, indent=4)
312
+ # ... 现有代码 ...
313
+ print(f"\n共同文件评估结果已保存到: {output_filepath}")
314
+ except Exception as e:
315
+ print(f"\n错误:保存共同文件结果时失败: {e}")
316
+ # ... 现有代码 ...
317
+
318
+ return final_results
319
+
320
+
321
+ # --- [主函数:run_evaluation_on_common_files_only] (打印部分已修改) ---
322
+ def run_evaluation_on_common_files_only(target_folders, gt_folder, base_dir=None):
323
+ """
324
+ [无变化]
325
+ 在手动指定的多个文件夹上 *仅* 运行 *共同文件* 评估。
326
+ """
327
+
328
+ # 1. 验证输入的文件夹列表
329
+ # ... 现有代码 ...
330
+ if not isinstance(target_folders, list) or not target_folders:
331
+ print("错误:未提供任何文件夹用于评估。请检查 'target_folders' 列表。")
332
+ return
333
+
334
+ # 2. 检查路径有效性并过滤非文件夹
335
+ # ... 现有代码 ...
336
+ valid_folders = []
337
+ for f_path in target_folders:
338
+ if os.path.isdir(f_path):
339
+ # ... 现有代码 ...
340
+ valid_folders.append(f_path)
341
+ else:
342
+ print(f"警告:提供的路径 '{f_path}' 不是一个有效的文件夹,已跳过。")
343
+
344
+ if not valid_folders:
345
+ # ... 现有代码 ...
346
+ print("错误:提供的列表中没有找到任何有效的文件夹。")
347
+ return
348
+
349
+ # 确保排序一致
350
+ # ... 现有代码 ...
351
+ valid_folders.sort()
352
+
353
+ # 3. 确定用于保存汇总文件的 base_dir
354
+ if base_dir is None:
355
+ # ... 现有代码 ...
356
+ # 如果未指定,使用第一个有效文件夹的父目录
357
+ base_dir = os.path.dirname(valid_folders[0])
358
+ print(f"信息:未指定 base_dir,将使用 '{base_dir}' 存放共同文件评估结果。")
359
+ elif not os.path.isdir(base_dir):
360
+ # ... 现有代码 ...
361
+ print(f"警告:指定的 base_dir '{base_dir}' 不存在。将改用第一个文件夹的父目录。")
362
+ base_dir = os.path.dirname(valid_folders[0])
363
+
364
+ print(f"\n找到了 {len(valid_folders)} 个有效的文件夹,将评估它们的共同文件:")
365
+ # ... 现有代码 ...
366
+ for folder in valid_folders:
367
+ print(f" - {folder}") # 打印完整路径
368
+
369
+ # 4. 第1部分:直接进行共同文件评估
370
+ # ... 现有代码 ...
371
+ print("\n" + "#"*80)
372
+ print(" 开始共同文件评估 (仅计算所有文件夹共有的文件) ".center(80, "#"))
373
+ print("#"*80)
374
+
375
+ # [调用修改后的函数]
376
+ # ... 现有代码 ...
377
+ common_results = evaluate_common_files(valid_folders, gt_folder, base_dir)
378
+
379
+ # 5. [修改] 第2部分:打印最终的汇总报告
380
+ print("\n" + "#"*80)
381
+ print(" 最终汇总报告 ".center(80, "#"))
382
+ print("#"*80)
383
+
384
+ print("\n--- 共同文件评估结果 (仅基于共同文件) ---")
385
+ if common_results:
386
+ # 打印全局平均
387
+ global_avg = common_results.get("global_average_on_common_files", {})
388
+ if global_avg:
389
+ print(f"\n [总平均 (基于 {global_avg.get('common_files_count', 0)} 个共同文件)]")
390
+ print(f" ‐ 倒角(L2): {global_avg.get('avg_chamfer_distance_l2', 0):.6f}, "
391
+ f"倒角(L1): {global_avg.get('avg_chamfer_distance_l1', 0):.6f}, "
392
+ f"豪斯多夫: {global_avg.get('avg_hausdorff_distance', 0):.6f}, "
393
+ f"法向量|NC|: {global_avg.get('avg_normal_consistency_abs', 0):.6f}, " # [修改]
394
+ f"法向量NC: {global_avg.get('avg_normal_consistency_real', 0):.6f}") # [新增]
395
+
396
+ # 打印逐文件夹平均
397
+ per_folder_avg = common_results.get("per_folder_average_on_common_files", {})
398
+ if per_folder_avg:
399
+ print("\n [逐文件夹平均 (仅共同文件)]")
400
+ for folder_name, results in per_folder_avg.items():
401
+ print(f" ‐ 文件夹: {folder_name}")
402
+ print(f" ‐ 倒角(L2): {results.get('avg_chamfer_distance_l2', 0):.6f}, "
403
+ f"倒角(L1): {results.get('avg_chamfer_distance_l1', 0):.6f}, "
404
+ f"豪斯多夫: {results.get('avg_hausdorff_distance', 0):.6f}, "
405
+ f"法向量|NC|: {results.get('avg_normal_consistency_abs', 0):.6f}, " # [修改]
406
+ f"法向量NC: {results.get('avg_normal_consistency_real', 0):.6f} " # [新增]
407
+ f"(共 {results.get('evaluated_pairs', 0)} 对)")
408
+ else:
409
+ print(" (没有共同文件的评估结果)")
410
+
411
+ print("\n" + "="*80)
412
+ print("所有评估任务已完成。")
413
+ print("="*80)
414
+
415
+
416
+ # ===============================================================
417
+ # 第五步:运行主程序 (无变化)
418
+ # ===============================================================
419
+
420
+ if __name__ == "__main__":
421
+ import argparse
422
+ ap = argparse.ArgumentParser(description="评测: 预测目录(含 <case>_y.obj) vs GT 目录(<case>.glb/.obj)")
423
+ ap.add_argument("folders", nargs="+", help="一个或多个预测目录(只评估各目录共有的 case)")
424
+ ap.add_argument("--gt", required=True, help="Ground Truth 目录")
425
+ ap.add_argument("--out", default=None, help="汇总 JSON 的输出目录(默认第一个预测目录)")
426
+ a = ap.parse_args()
427
+ if not os.path.isdir(a.gt):
428
+ raise SystemExit(f"GT 目录不存在: {a.gt}")
429
+ run_evaluation_on_common_files_only(a.folders, a.gt, base_dir=a.out)