Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- mulres_infer/.gitignore +5 -0
- mulres_infer/ckpt/generator.pth +3 -0
- mulres_infer/ckpt/readme.md +28 -0
- mulres_infer/ckpt/subdivider.pth +3 -0
- mulres_infer/configs/generator.yaml +151 -0
- mulres_infer/configs/subdivider.yaml +126 -0
- mulres_infer/dataset/__init__.py +0 -0
- mulres_infer/dataset/mesh_dataset.py +940 -0
- mulres_infer/dataset/mesh_dataset_altpc.py +94 -0
- mulres_infer/models/__init__.py +0 -0
- mulres_infer/models/generator.py +1507 -0
- mulres_infer/models/point_conditioner.py +200 -0
- mulres_infer/models/subdivider.py +1188 -0
- mulres_infer/modules/__init__.py +0 -0
- mulres_infer/modules/attention.py +376 -0
- mulres_infer/modules/fast_octree_level.py +52 -0
- mulres_infer/modules/miche/LICENSE +674 -0
- mulres_infer/modules/miche/__init__.py +0 -0
- mulres_infer/modules/miche/download_ckpt.py +25 -0
- mulres_infer/modules/miche/encode.py +84 -0
- mulres_infer/modules/miche/michelangelo/__init__.py +1 -0
- mulres_infer/modules/miche/michelangelo/graphics/__init__.py +1 -0
- mulres_infer/modules/miche/michelangelo/graphics/primitives/__init__.py +4 -0
- mulres_infer/modules/miche/michelangelo/graphics/primitives/volume.py +21 -0
- mulres_infer/modules/miche/michelangelo/models/__init__.py +1 -0
- mulres_infer/modules/miche/michelangelo/models/modules/__init__.py +3 -0
- mulres_infer/modules/miche/michelangelo/models/modules/checkpoint.py +64 -0
- mulres_infer/modules/miche/michelangelo/models/modules/distributions.py +83 -0
- mulres_infer/modules/miche/michelangelo/models/modules/embedder.py +213 -0
- mulres_infer/modules/miche/michelangelo/models/modules/transformer_blocks.py +309 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/__init__.py +1 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/asl_pl_module.py +384 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/clip_asl_module.py +118 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/inference_utils.py +76 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/loss.py +130 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/sal_perceiver.py +459 -0
- mulres_infer/modules/miche/michelangelo/models/tsal/tsal_base.py +125 -0
- mulres_infer/modules/miche/michelangelo/utils/__init__.py +3 -0
- mulres_infer/modules/miche/michelangelo/utils/misc.py +83 -0
- mulres_infer/modules/miche/shapevae-256.yaml +46 -0
- mulres_infer/modules/octree_codec.py +981 -0
- mulres_infer/modules/octree_codec_acc.py +857 -0
- mulres_infer/modules/octree_decode.py +625 -0
- mulres_infer/modules/pipeline.py +964 -0
- mulres_infer/modules/subdivider_runtime.py +575 -0
- mulres_infer/modules/token_utils.py +52 -0
- mulres_infer/readme.md +122 -0
- mulres_infer/requirements.txt +22 -0
- mulres_infer/scripts/__init__.py +0 -0
- 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)
|