sang-w00 commited on
Commit
b1d2f2d
·
verified ·
1 Parent(s): 5a294cb

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +49 -0
  2. home/ubuntu/aaaaa/data/rgbmr/.gitignore +46 -0
  3. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/MeshRender.py +1519 -0
  4. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/__init__.py +0 -0
  5. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/camera_utils.py +93 -0
  6. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/compile_mesh_painter.sh +1 -0
  7. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_inpaint_processor.cpp +550 -0
  8. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_inpaint_processor.cpython-310-x86_64-linux-gnu.so +3 -0
  9. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_utils.py +270 -0
  10. home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/obj_to_glb.py +332 -0
  11. home/ubuntu/aaaaa/data/rgbmr/MCVAE_CONFIG_UPGRADE_SUMMARY.md +212 -0
  12. home/ubuntu/aaaaa/data/rgbmr/README.md +207 -0
  13. home/ubuntu/aaaaa/data/rgbmr/complex_object_ids.json +1678 -0
  14. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/default.yaml +130 -0
  15. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/dual_caa.yaml +22 -0
  16. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/dual_full.yaml +21 -0
  17. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa.yaml +21 -0
  18. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_100k_vae.yaml +21 -0
  19. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_global_pos.yaml +21 -0
  20. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_global_token.yaml +21 -0
  21. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_tmp.yaml +21 -0
  22. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caaa.yaml +21 -0
  23. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_full.yaml +21 -0
  24. home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_fulll.yaml +21 -0
  25. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/config.json +38 -0
  26. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/default.yaml +92 -0
  27. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/layerdiffuse.yaml +34 -0
  28. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/no_crop.yaml +29 -0
  29. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/orchid.yaml +29 -0
  30. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/ours.yaml +29 -0
  31. home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/variant_example.yaml +24 -0
  32. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/custom_rasterizer/__init__.py +4 -0
  33. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/custom_rasterizer/render.py +18 -0
  34. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/__init__.py +0 -0
  35. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/grid_neighbor.cpp +574 -0
  36. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/rasterizer.cpp +139 -0
  37. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/rasterizer.h +54 -0
  38. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/rasterizer_gpu.cu +127 -0
  39. home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/setup.py +26 -0
  40. home/ubuntu/aaaaa/data/rgbmr/data/__pycache__/rgbmr_dataset.cpython-310.pyc +0 -0
  41. home/ubuntu/aaaaa/data/rgbmr/data/generate_rgbmr_dataset.py +389 -0
  42. home/ubuntu/aaaaa/data/rgbmr/data/rgbmr_dataset.py +845 -0
  43. home/ubuntu/aaaaa/data/rgbmr/debug_uv_mask.png +0 -0
  44. home/ubuntu/aaaaa/data/rgbmr/filter_complex.py +165 -0
  45. home/ubuntu/aaaaa/data/rgbmr/inference.py +1211 -0
  46. home/ubuntu/aaaaa/data/rgbmr/inference_batch.py +658 -0
  47. home/ubuntu/aaaaa/data/rgbmr/inference_list.py +1270 -0
  48. home/ubuntu/aaaaa/data/rgbmr/latent_vis/base.png +0 -0
  49. home/ubuntu/aaaaa/data/rgbmr/latent_vis/full.png +0 -0
  50. home/ubuntu/aaaaa/data/rgbmr/latent_vis/offset.png +0 -0
.gitattributes CHANGED
@@ -33,3 +33,52 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_inpaint_processor.cpython-310-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
37
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val/images/01_0_818ac8b7e01b99f90b70.png filter=lfs diff=lfs merge=lfs -text
38
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val/images/03_0_49256f230cfc33e43100.png filter=lfs diff=lfs merge=lfs -text
39
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val/images/04_0_7b88252be613ce946fe4.png filter=lfs diff=lfs merge=lfs -text
40
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val/images/05_0_b250851f3c9169b00a69.png filter=lfs diff=lfs merge=lfs -text
41
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_crop/images/01_0_adea7798f8f45252a73c.png filter=lfs diff=lfs merge=lfs -text
42
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_crop/images/03_0_904944c01f3ae7bbd5f0.png filter=lfs diff=lfs merge=lfs -text
43
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_rot180/images/00_0_a606811f00914a368e75.png filter=lfs diff=lfs merge=lfs -text
44
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_rot180/images/01_0_4dded08813b724931309.png filter=lfs diff=lfs merge=lfs -text
45
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_rot180/images/02_0_ab41c15b1bb8525149c9.png filter=lfs diff=lfs merge=lfs -text
46
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_rot180/images/03_0_659fedbaf33a6c219944.png filter=lfs diff=lfs merge=lfs -text
47
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_rot180/images/04_0_b049d61d3085ba5f13ce.png filter=lfs diff=lfs merge=lfs -text
48
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_rot180/images/05_0_cc7d3bfcb29578a7e53d.png filter=lfs diff=lfs merge=lfs -text
49
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_scale/images/01_0_403bc0d6d88454fc8e84.png filter=lfs diff=lfs merge=lfs -text
50
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135527-ak3g766j/files/media/images/val_scale/images/03_0_ea5147228ff3b0d1b87a.png filter=lfs diff=lfs merge=lfs -text
51
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/train/examples/grid_0_661a5d74ca7dfd67e0b4.png filter=lfs diff=lfs merge=lfs -text
52
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val/images/01_0_2c96edd5ae4a47af8f53.png filter=lfs diff=lfs merge=lfs -text
53
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val/images/03_0_5e7b75cb7860a73cb9d1.png filter=lfs diff=lfs merge=lfs -text
54
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val/images/04_0_abb699ae435cf8b83003.png filter=lfs diff=lfs merge=lfs -text
55
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val/images/05_0_212056244411ef30fddf.png filter=lfs diff=lfs merge=lfs -text
56
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_crop/images/01_0_28ff21fc45bd9c10180a.png filter=lfs diff=lfs merge=lfs -text
57
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_crop/images/03_0_2b30a4f246fdc42315cc.png filter=lfs diff=lfs merge=lfs -text
58
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_rot180/images/00_0_c0472612a8a5d6d1d45a.png filter=lfs diff=lfs merge=lfs -text
59
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_rot180/images/01_0_25e541b74f270bb6fb37.png filter=lfs diff=lfs merge=lfs -text
60
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_rot180/images/02_0_d52eb7d14cbb426697f9.png filter=lfs diff=lfs merge=lfs -text
61
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_rot180/images/03_0_a3e9121765db9014f693.png filter=lfs diff=lfs merge=lfs -text
62
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_rot180/images/04_0_5c43ab551120d3156681.png filter=lfs diff=lfs merge=lfs -text
63
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_rot180/images/05_0_2bdfa81bfae10c494d15.png filter=lfs diff=lfs merge=lfs -text
64
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_scale/images/01_0_027e0601542d52d31398.png filter=lfs diff=lfs merge=lfs -text
65
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/files/media/images/val_scale/images/03_0_64a50028a9bbc978f275.png filter=lfs diff=lfs merge=lfs -text
66
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_135634-urqlwymz/run-urqlwymz.wandb filter=lfs diff=lfs merge=lfs -text
67
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/train/examples/grid_0_3e76f9aa156a193517c5.png filter=lfs diff=lfs merge=lfs -text
68
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val/images/00_0_7024f940af3f95bd447f.png filter=lfs diff=lfs merge=lfs -text
69
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val/images/01_0_85da5ed7facb325c1059.png filter=lfs diff=lfs merge=lfs -text
70
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val/images/02_0_5586337d302b720bd290.png filter=lfs diff=lfs merge=lfs -text
71
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val/images/03_0_d8179a4c647bd3e2936a.png filter=lfs diff=lfs merge=lfs -text
72
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val/images/04_0_c79975a8744557d039ea.png filter=lfs diff=lfs merge=lfs -text
73
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val/images/05_0_1bd6ec356b4f77ccd60b.png filter=lfs diff=lfs merge=lfs -text
74
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_crop/images/01_0_681e3f44716ca0c74ce5.png filter=lfs diff=lfs merge=lfs -text
75
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_crop/images/03_0_9bd579ebc1f440a1c78c.png filter=lfs diff=lfs merge=lfs -text
76
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_rot180/images/00_0_b0daad34dff08759d68e.png filter=lfs diff=lfs merge=lfs -text
77
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_rot180/images/01_0_dc0c32e37935a5371ce7.png filter=lfs diff=lfs merge=lfs -text
78
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_rot180/images/02_0_cd238a63711660b82256.png filter=lfs diff=lfs merge=lfs -text
79
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_rot180/images/03_0_18e54e8add02725ae043.png filter=lfs diff=lfs merge=lfs -text
80
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_rot180/images/04_0_bc02825b394e9c404d4d.png filter=lfs diff=lfs merge=lfs -text
81
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_rot180/images/05_0_28a6eacbf019405eb25a.png filter=lfs diff=lfs merge=lfs -text
82
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_scale/images/01_0_920e55b3eed4248203e4.png filter=lfs diff=lfs merge=lfs -text
83
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/files/media/images/val_scale/images/03_0_49e0c325869d63e8686c.png filter=lfs diff=lfs merge=lfs -text
84
+ home/ubuntu/aaaaa/data/rgbmr/outputs/MCVAE_v1.1.0/wandb/run-20251109_143631-069z520z/run-069z520z.wandb filter=lfs diff=lfs merge=lfs -text
home/ubuntu/aaaaa/data/rgbmr/.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .gitignore for mcgen project
2
+
3
+ # Training / experiment outputs
4
+ outputs/
5
+ wandb/
6
+
7
+ # Model / checkpoint files
8
+ *.pt
9
+ *.pth
10
+ *.ckpt
11
+ *.safetensors
12
+
13
+ # Notebooks
14
+ *.ipynb
15
+ .ipynb_checkpoints/
16
+
17
+ # Python cache / build
18
+ __pycache__/
19
+ *.py[cod]
20
+ *.egg-info/
21
+ dist/
22
+ build/
23
+
24
+ # Virtual environments
25
+ .venv/
26
+ venv/
27
+ env/
28
+
29
+ # Numpy array dumps
30
+ *.npy
31
+ *.npz
32
+
33
+ # Editors / OS noise
34
+ .vscode/
35
+ .idea/
36
+ .DS_Store
37
+
38
+ # MCGEN stuff
39
+ taming/
40
+ outputs*/
41
+ temp_*/
42
+ metrics_eval/
43
+ metrics_eval_final/
44
+ removethis/
45
+ timing_results/
46
+ metrics_summary.csv
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/MeshRender.py ADDED
@@ -0,0 +1,1519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import torch
3
+ import trimesh
4
+ import numpy as np
5
+ from PIL import Image
6
+ import torch.nn.functional as F
7
+ from typing import Union, Optional, Tuple, List, Any, Callable
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from .camera_utils import (
11
+ transform_pos,
12
+ get_mv_matrix,
13
+ get_orthographic_projection_matrix,
14
+ get_perspective_projection_matrix,
15
+ )
16
+
17
+ try:
18
+ from .mesh_utils import load_mesh, save_mesh
19
+ except:
20
+ print("Bpy IO CAN NOT BE Imported!!!")
21
+
22
+ from .obj_to_glb import obj_to_pbr_glb
23
+
24
+ try:
25
+ from .mesh_inpaint_processor import meshVerticeInpaint # , meshVerticeColor
26
+ except ImportError as e:
27
+ print("InPaint Function CAN NOT BE Imported!!!")
28
+
29
+
30
+ class RenderMode(Enum):
31
+ """Rendering mode enumeration."""
32
+
33
+ NORMAL = "normal"
34
+ POSITION = "position"
35
+ ALPHA = "alpha"
36
+ UV_POS = "uvpos"
37
+
38
+
39
+ class ReturnType(Enum):
40
+ """Return type enumeration."""
41
+
42
+ TENSOR = "th"
43
+ NUMPY = "np"
44
+ PIL = "pl"
45
+
46
+
47
+ class TextureType(Enum):
48
+ """Texture type enumeration."""
49
+
50
+ DIFFUSE = "diffuse"
51
+ METALLIC_ROUGHNESS = "mr"
52
+ NORMAL = "normal"
53
+
54
+
55
+ @dataclass
56
+ class RenderConfig:
57
+ """Unified rendering configuration."""
58
+
59
+ elev: float = 0
60
+ azim: float = 0
61
+ camera_distance: Optional[float] = None
62
+ center: Optional[List[float]] = None
63
+ resolution: Optional[Union[int, Tuple[int, int]]] = None
64
+ bg_color: List[float] = None
65
+ return_type: str = "th"
66
+
67
+ def __post_init__(self):
68
+ if self.bg_color is None:
69
+ self.bg_color = [1, 1, 1]
70
+
71
+
72
+ @dataclass
73
+ class ViewState:
74
+ """Camera view state for rendering pipeline."""
75
+
76
+ proj_mat: torch.Tensor
77
+ mv_mat: torch.Tensor
78
+ pos_camera: torch.Tensor
79
+ pos_clip: torch.Tensor
80
+ resolution: Tuple[int, int]
81
+
82
+
83
+ def stride_from_shape(shape):
84
+ """
85
+ Calculate stride values from a given shape for multi-dimensional indexing.
86
+
87
+ Args:
88
+ shape: Tuple or list representing tensor dimensions
89
+
90
+ Returns:
91
+ List of stride values for each dimension
92
+ """
93
+ stride = [1]
94
+ for x in reversed(shape[1:]):
95
+ stride.append(stride[-1] * x)
96
+ return list(reversed(stride))
97
+
98
+
99
+ def scatter_add_nd_with_count(input, count, indices, values, weights=None):
100
+ """
101
+ Perform scatter-add operation on N-dimensional tensors with counting.
102
+
103
+ Args:
104
+ input: Input tensor [..., C] with D dimensions + C channels
105
+ count: Count tensor [..., 1] with D dimensions
106
+ indices: Index tensor [N, D] of type long
107
+ values: Value tensor [N, C] to scatter
108
+ weights: Optional weight tensor [N, C], defaults to ones if None
109
+
110
+ Returns:
111
+ Tuple of (updated_input, updated_count) tensors
112
+ """
113
+ # input: [..., C], D dimension + C channel
114
+ # count: [..., 1], D dimension
115
+ # indices: [N, D], long
116
+ # values: [N, C]
117
+
118
+ D = indices.shape[-1]
119
+ C = input.shape[-1]
120
+ size = input.shape[:-1]
121
+ stride = stride_from_shape(size)
122
+
123
+ assert len(size) == D
124
+
125
+ input = input.view(-1, C) # [HW, C]
126
+ count = count.view(-1, 1)
127
+
128
+ flatten_indices = (indices * torch.tensor(stride, dtype=torch.long, device=indices.device)).sum(-1) # [N]
129
+
130
+ if weights is None:
131
+ weights = torch.ones_like(values[..., :1])
132
+
133
+ input.scatter_add_(0, flatten_indices.unsqueeze(1).repeat(1, C), values)
134
+ count.scatter_add_(0, flatten_indices.unsqueeze(1), weights)
135
+
136
+ return input.view(*size, C), count.view(*size, 1)
137
+
138
+
139
+ def linear_grid_put_2d(H, W, coords, values, return_count=False):
140
+ """
141
+ Place values on a 2D grid using bilinear interpolation.
142
+
143
+ Args:
144
+ H: Grid height
145
+ W: Grid width
146
+ coords: Coordinate tensor [N, 2] with values in range [0, 1]
147
+ values: Value tensor [N, C] to place on grid
148
+ return_count: Whether to return count information
149
+
150
+ Returns:
151
+ 2D grid tensor [H, W, C] with interpolated values, optionally with count tensor
152
+ """
153
+ # coords: [N, 2], float in [0, 1]
154
+ # values: [N, C]
155
+
156
+ C = values.shape[-1]
157
+
158
+ indices = coords * torch.tensor([H - 1, W - 1], dtype=torch.float32, device=coords.device)
159
+ indices_00 = indices.floor().long() # [N, 2]
160
+ indices_00[:, 0].clamp_(0, H - 2)
161
+ indices_00[:, 1].clamp_(0, W - 2)
162
+ indices_01 = indices_00 + torch.tensor([0, 1], dtype=torch.long, device=indices.device)
163
+ indices_10 = indices_00 + torch.tensor([1, 0], dtype=torch.long, device=indices.device)
164
+ indices_11 = indices_00 + torch.tensor([1, 1], dtype=torch.long, device=indices.device)
165
+
166
+ h = indices[..., 0] - indices_00[..., 0].float()
167
+ w = indices[..., 1] - indices_00[..., 1].float()
168
+ w_00 = (1 - h) * (1 - w)
169
+ w_01 = (1 - h) * w
170
+ w_10 = h * (1 - w)
171
+ w_11 = h * w
172
+
173
+ result = torch.zeros(H, W, C, device=values.device, dtype=values.dtype) # [H, W, C]
174
+ count = torch.zeros(H, W, 1, device=values.device, dtype=values.dtype) # [H, W, 1]
175
+ weights = torch.ones_like(values[..., :1]) # [N, 1]
176
+
177
+ result, count = scatter_add_nd_with_count(
178
+ result, count, indices_00, values * w_00.unsqueeze(1), weights * w_00.unsqueeze(1)
179
+ )
180
+ result, count = scatter_add_nd_with_count(
181
+ result, count, indices_01, values * w_01.unsqueeze(1), weights * w_01.unsqueeze(1)
182
+ )
183
+ result, count = scatter_add_nd_with_count(
184
+ result, count, indices_10, values * w_10.unsqueeze(1), weights * w_10.unsqueeze(1)
185
+ )
186
+ result, count = scatter_add_nd_with_count(
187
+ result, count, indices_11, values * w_11.unsqueeze(1), weights * w_11.unsqueeze(1)
188
+ )
189
+
190
+ if return_count:
191
+ return result, count
192
+
193
+ mask = count.squeeze(-1) > 0
194
+ result[mask] = result[mask] / count[mask].repeat(1, C)
195
+
196
+ return result
197
+
198
+
199
+ def mipmap_linear_grid_put_2d(H, W, coords, values, min_resolution=128, return_count=False):
200
+ """
201
+ Place values on 2D grid using mipmap-based multiresolution interpolation to fill holes.
202
+
203
+ Args:
204
+ H: Grid height
205
+ W: Grid width
206
+ coords: Coordinate tensor [N, 2] with values in range [0, 1]
207
+ values: Value tensor [N, C] to place on grid
208
+ min_resolution: Minimum resolution for mipmap levels
209
+ return_count: Whether to return count information
210
+
211
+ Returns:
212
+ 2D grid tensor [H, W, C] with filled values, optionally with count tensor
213
+ """
214
+ # coords: [N, 2], float in [0, 1]
215
+ # values: [N, C]
216
+
217
+ C = values.shape[-1]
218
+
219
+ result = torch.zeros(H, W, C, device=values.device, dtype=values.dtype) # [H, W, C]
220
+ count = torch.zeros(H, W, 1, device=values.device, dtype=values.dtype) # [H, W, 1]
221
+
222
+ cur_H, cur_W = H, W
223
+
224
+ while min(cur_H, cur_W) > min_resolution:
225
+
226
+ # try to fill the holes
227
+ mask = count.squeeze(-1) == 0
228
+ if not mask.any():
229
+ break
230
+
231
+ cur_result, cur_count = linear_grid_put_2d(cur_H, cur_W, coords, values, return_count=True)
232
+ result[mask] = (
233
+ result[mask]
234
+ + F.interpolate(
235
+ cur_result.permute(2, 0, 1).unsqueeze(0).contiguous(), (H, W), mode="bilinear", align_corners=False
236
+ )
237
+ .squeeze(0)
238
+ .permute(1, 2, 0)
239
+ .contiguous()[mask]
240
+ )
241
+ count[mask] = (
242
+ count[mask]
243
+ + F.interpolate(cur_count.view(1, 1, cur_H, cur_W), (H, W), mode="bilinear", align_corners=False).view(
244
+ H, W, 1
245
+ )[mask]
246
+ )
247
+ cur_H //= 2
248
+ cur_W //= 2
249
+
250
+ if return_count:
251
+ return result, count
252
+
253
+ mask = count.squeeze(-1) > 0
254
+ result[mask] = result[mask] / count[mask].repeat(1, C)
255
+
256
+ return result
257
+
258
+
259
+ # ============ Core utility functions for reducing duplication ============
260
+
261
+
262
+ def _normalize_image_input(image: Union[np.ndarray, torch.Tensor, Image.Image]) -> Union[np.ndarray, torch.Tensor]:
263
+ """Normalize image input to consistent format."""
264
+ if isinstance(image, Image.Image):
265
+ return np.array(image) / 255.0
266
+ elif isinstance(image, torch.Tensor):
267
+ return image.cpu().numpy() if image.is_cuda else image
268
+ return image
269
+
270
+
271
+ def _convert_texture_format(
272
+ tex: Union[np.ndarray, torch.Tensor, Image.Image],
273
+ texture_size: Tuple[int, int],
274
+ device: str,
275
+ force_set: bool = False,
276
+ ) -> torch.Tensor:
277
+ """Unified texture format conversion logic."""
278
+ if not force_set:
279
+ if isinstance(tex, np.ndarray):
280
+ tex = Image.fromarray((tex * 255).astype(np.uint8))
281
+ elif isinstance(tex, torch.Tensor):
282
+ tex_np = tex.cpu().numpy()
283
+ tex = Image.fromarray((tex_np * 255).astype(np.uint8))
284
+
285
+ tex = tex.resize(texture_size).convert("RGB")
286
+ tex = np.array(tex) / 255.0
287
+ return torch.from_numpy(tex).to(device).float()
288
+ else:
289
+ if isinstance(tex, np.ndarray):
290
+ tex = torch.from_numpy(tex)
291
+ return tex.to(device).float()
292
+
293
+
294
+ def _format_output(image: torch.Tensor, return_type: str) -> Union[torch.Tensor, np.ndarray, Image.Image]:
295
+ """Convert output to requested format."""
296
+ if return_type == ReturnType.NUMPY.value:
297
+ return image.cpu().numpy()
298
+ elif return_type == ReturnType.PIL.value:
299
+ img_np = image.cpu().numpy() * 255
300
+ return Image.fromarray(img_np.astype(np.uint8))
301
+ return image
302
+
303
+
304
+ def _ensure_resolution_format(
305
+ resolution: Optional[Union[int, Tuple[int, int]]], default: Tuple[int, int]
306
+ ) -> Tuple[int, int]:
307
+ """Ensure resolution is in (height, width) format."""
308
+ if resolution is None:
309
+ return default
310
+ if isinstance(resolution, (int, float)):
311
+ return (int(resolution), int(resolution))
312
+ return tuple(resolution)
313
+
314
+
315
+ def _apply_background_mask(
316
+ content: torch.Tensor, visible_mask: torch.Tensor, bg_color: List[float], device: str
317
+ ) -> torch.Tensor:
318
+ """Apply background color to masked regions."""
319
+ bg_tensor = torch.tensor(bg_color, dtype=torch.float32, device=device)
320
+ return content * visible_mask + bg_tensor * (1 - visible_mask)
321
+
322
+
323
+ class MeshRender:
324
+ def __init__(
325
+ self,
326
+ camera_distance=1.45,
327
+ camera_type="orth",
328
+ default_resolution=1024,
329
+ texture_size=1024,
330
+ use_antialias=True,
331
+ max_mip_level=None,
332
+ filter_mode="linear-mipmap-linear",
333
+ bake_mode="back_sample",
334
+ raster_mode="cr",
335
+ shader_type="face",
336
+ use_opengl=False,
337
+ device="cuda",
338
+ ):
339
+ """
340
+ Initialize mesh renderer with configurable parameters.
341
+
342
+ Args:
343
+ camera_distance: Distance from camera to object center
344
+ camera_type: Type of camera projection ("orth" or "perspective")
345
+ default_resolution: Default rendering resolution
346
+ texture_size: Size of texture maps
347
+ use_antialias: Whether to use antialiasing
348
+ max_mip_level: Maximum mipmap level for texture filtering
349
+ filter_mode: Texture filtering mode
350
+ bake_mode: Texture baking method ("back_sample", "linear", "mip-map")
351
+ raster_mode: Rasterization backend ("cr" for custom rasterizer)
352
+ shader_type: Shading type ("face" or "vertex")
353
+ use_opengl: Whether to use OpenGL backend (deprecated)
354
+ device: Computing device ("cuda" or "cpu")
355
+ """
356
+
357
+ self.device = device
358
+
359
+ self.set_default_render_resolution(default_resolution)
360
+ self.set_default_texture_resolution(texture_size)
361
+
362
+ self.camera_distance = camera_distance
363
+ self.use_antialias = use_antialias
364
+ self.max_mip_level = max_mip_level
365
+ self.filter_mode = filter_mode
366
+ self.bake_angle_thres = 75
367
+ self.set_boundary_unreliable_scale(4)
368
+ self.bake_mode = bake_mode
369
+ self.shader_type = shader_type
370
+
371
+ self.raster_mode = raster_mode
372
+ if self.raster_mode == "cr":
373
+ import custom_rasterizer as cr
374
+
375
+ self.raster = cr
376
+ else:
377
+ raise f"No raster named {self.raster_mode}"
378
+
379
+ if camera_type == "orth":
380
+ self.set_orth_scale(1.1)
381
+ elif camera_type == "perspective":
382
+ self.camera_proj_mat = get_perspective_projection_matrix(
383
+ 49.13, self.default_resolution[1] / self.default_resolution[0], 0.01, 100.0
384
+ )
385
+ else:
386
+ raise f"No camera type {camera_type}"
387
+
388
+ # Removed multiprocessing components for single-threaded version
389
+
390
+ def _create_view_state(self, config: RenderConfig) -> ViewState:
391
+ """Create unified view state for rendering pipeline."""
392
+ proj = self.camera_proj_mat
393
+ r_mv = get_mv_matrix(
394
+ elev=config.elev,
395
+ azim=config.azim,
396
+ camera_distance=self.camera_distance if config.camera_distance is None else config.camera_distance,
397
+ center=config.center,
398
+ )
399
+
400
+ pos_camera = transform_pos(r_mv, self.vtx_pos, keepdim=True)
401
+ pos_clip = transform_pos(proj, pos_camera)
402
+ resolution = _ensure_resolution_format(config.resolution, self.default_resolution)
403
+
404
+ return ViewState(proj, r_mv, pos_camera, pos_clip, resolution)
405
+
406
+ def _compute_face_normals(self, triangles: torch.Tensor) -> torch.Tensor:
407
+ """Compute face normals from triangle vertices."""
408
+ return F.normalize(
409
+ torch.cross(
410
+ triangles[:, 1, :] - triangles[:, 0, :],
411
+ triangles[:, 2, :] - triangles[:, 0, :],
412
+ dim=-1,
413
+ ),
414
+ dim=-1,
415
+ )
416
+
417
+ def _get_normals_for_shading(self, view_state: ViewState, use_abs_coor: bool = False) -> torch.Tensor:
418
+ """Get normals based on shader type and coordinate system."""
419
+ if use_abs_coor:
420
+ mesh_triangles = self.vtx_pos[self.pos_idx[:, :3], :]
421
+ else:
422
+ pos_camera = view_state.pos_camera[:, :3] / view_state.pos_camera[:, 3:4]
423
+ mesh_triangles = pos_camera[self.pos_idx[:, :3], :]
424
+
425
+ face_normals = self._compute_face_normals(mesh_triangles)
426
+
427
+ # Common rasterization
428
+ rast_out, _ = self.raster_rasterize(view_state.pos_clip, self.pos_idx, resolution=view_state.resolution)
429
+
430
+ if self.shader_type == "vertex":
431
+ vertex_normals = trimesh.geometry.mean_vertex_normals(
432
+ vertex_count=self.vtx_pos.shape[0],
433
+ faces=self.pos_idx.cpu(),
434
+ face_normals=face_normals.cpu(),
435
+ )
436
+ vertex_normals = torch.from_numpy(vertex_normals).float().to(self.device).contiguous()
437
+ normal, _ = self.raster_interpolate(vertex_normals[None, ...], rast_out, self.pos_idx)
438
+
439
+ elif self.shader_type == "face":
440
+ tri_ids = rast_out[..., 3]
441
+ tri_ids_mask = tri_ids > 0
442
+ tri_ids = ((tri_ids - 1) * tri_ids_mask).long()
443
+ normal = torch.zeros(rast_out.shape[0], rast_out.shape[1], rast_out.shape[2], 3).to(rast_out)
444
+ normal.reshape(-1, 3)[tri_ids_mask.view(-1)] = face_normals.reshape(-1, 3)[tri_ids[tri_ids_mask].view(-1)]
445
+
446
+ return normal, rast_out
447
+
448
+ def _unified_render_pipeline(self, config: RenderConfig, mode: RenderMode, **kwargs) -> torch.Tensor:
449
+ """Unified rendering pipeline for all render modes."""
450
+ view_state = self._create_view_state(config)
451
+
452
+ if mode == RenderMode.ALPHA:
453
+ rast_out, _ = self.raster_rasterize(view_state.pos_clip, self.pos_idx, resolution=view_state.resolution)
454
+ return rast_out[..., -1:].long()
455
+
456
+ elif mode == RenderMode.UV_POS:
457
+ return self.uv_feature_map(self.vtx_pos * 0.5 + 0.5)
458
+
459
+ elif mode == RenderMode.NORMAL:
460
+ use_abs_coor = kwargs.get("use_abs_coor", False)
461
+ normalize_rgb = kwargs.get("normalize_rgb", True)
462
+
463
+ normal, rast_out = self._get_normals_for_shading(view_state, use_abs_coor)
464
+ visible_mask = torch.clamp(rast_out[..., -1:], 0, 1)
465
+
466
+ result = _apply_background_mask(normal, visible_mask, config.bg_color, self.device)
467
+
468
+ if normalize_rgb:
469
+ result = (result + 1) * 0.5
470
+
471
+ if self.use_antialias:
472
+ result = self.raster_antialias(result, rast_out, view_state.pos_clip, self.pos_idx)
473
+
474
+ return result[0, ...]
475
+
476
+ elif mode == RenderMode.POSITION:
477
+ rast_out, _ = self.raster_rasterize(view_state.pos_clip, self.pos_idx, resolution=view_state.resolution)
478
+
479
+ tex_position = 0.5 - self.vtx_pos[:, :3] / self.scale_factor
480
+ tex_position = tex_position.contiguous()
481
+
482
+ position, _ = self.raster_interpolate(tex_position[None, ...], rast_out, self.pos_idx)
483
+ visible_mask = torch.clamp(rast_out[..., -1:], 0, 1)
484
+
485
+ result = _apply_background_mask(position, visible_mask, config.bg_color, self.device)
486
+
487
+ if self.use_antialias:
488
+ result = self.raster_antialias(result, rast_out, view_state.pos_clip, self.pos_idx)
489
+
490
+ return result[0, ...]
491
+
492
+ def set_orth_scale(self, ortho_scale):
493
+ """
494
+ Set the orthographic projection scale and update camera projection matrix.
495
+
496
+ Args:
497
+ ortho_scale: Scale factor for orthographic projection
498
+ """
499
+ self.ortho_scale = ortho_scale
500
+ self.camera_proj_mat = get_orthographic_projection_matrix(
501
+ left=-self.ortho_scale * 0.5,
502
+ right=self.ortho_scale * 0.5,
503
+ bottom=-self.ortho_scale * 0.5,
504
+ top=self.ortho_scale * 0.5,
505
+ near=0.1,
506
+ far=100,
507
+ )
508
+
509
+ def raster_rasterize(self, pos, tri, resolution, ranges=None, grad_db=True):
510
+ """
511
+ Rasterize triangular mesh using the configured rasterization backend.
512
+
513
+ Args:
514
+ pos: Vertex positions in clip space
515
+ tri: Triangle indices
516
+ resolution: Rendering resolution [height, width]
517
+ ranges: Optional rendering ranges (unused in current implementation)
518
+ grad_db: Whether to compute gradients (unused in current implementation)
519
+
520
+ Returns:
521
+ Tuple of (rasterization_output, gradient_info)
522
+ """
523
+
524
+ if self.raster_mode == "cr":
525
+ rast_out_db = None
526
+ if pos.dim() == 2:
527
+ pos = pos.unsqueeze(0)
528
+
529
+ # 确保pos是float32类型
530
+ if pos.dtype == torch.float64:
531
+ pos = pos.to(torch.float32)
532
+
533
+ # 确保tri是int32类型
534
+ if tri.dtype == torch.int64:
535
+ tri = tri.to(torch.int32)
536
+
537
+ findices, barycentric = self.raster.rasterize(pos, tri, resolution)
538
+ rast_out = torch.cat((barycentric, findices.unsqueeze(-1)), dim=-1)
539
+ rast_out = rast_out.unsqueeze(0)
540
+ else:
541
+ raise f"No raster named {self.raster_mode}"
542
+
543
+ return rast_out, rast_out_db
544
+
545
+ def raster_interpolate(self, uv, rast_out, uv_idx):
546
+ """
547
+ Interpolate texture coordinates or vertex attributes across rasterized triangles.
548
+
549
+ Args:
550
+ uv: UV coordinates or vertex attributes to interpolate
551
+ rast_out: Rasterization output containing barycentric coordinates
552
+ uv_idx: UV or vertex indices for triangles
553
+
554
+ Returns:
555
+ Tuple of (interpolated_values, gradient_info)
556
+ """
557
+
558
+ if self.raster_mode == "cr":
559
+ textd = None
560
+ barycentric = rast_out[0, ..., :-1]
561
+ findices = rast_out[0, ..., -1]
562
+ if uv.dim() == 2:
563
+ uv = uv.unsqueeze(0)
564
+ textc = self.raster.interpolate(uv, findices, barycentric, uv_idx)
565
+ else:
566
+ raise f"No raster named {self.raster_mode}"
567
+
568
+ return textc, textd
569
+
570
+ def raster_antialias(self, color, rast, pos, tri, topology_hash=None, pos_gradient_boost=1.0):
571
+ """
572
+ Apply antialiasing to rendered colors (currently returns input unchanged).
573
+
574
+ Args:
575
+ color: Input color values
576
+ rast: Rasterization output
577
+ pos: Vertex positions
578
+ tri: Triangle indices
579
+ topology_hash: Optional topology hash for optimization
580
+ pos_gradient_boost: Gradient boosting factor
581
+
582
+ Returns:
583
+ Antialiased color values
584
+ """
585
+
586
+ if self.raster_mode == "cr":
587
+ color = color
588
+ else:
589
+ raise f"No raster named {self.raster_mode}"
590
+
591
+ return color
592
+
593
+ def set_boundary_unreliable_scale(self, scale):
594
+ """
595
+ Set the kernel size for boundary unreliable region detection during texture baking.
596
+
597
+ Args:
598
+ scale: Scale factor relative to 512 resolution baseline
599
+ """
600
+ self.bake_unreliable_kernel_size = int(
601
+ (scale / 512) * max(self.default_resolution[0], self.default_resolution[1])
602
+ )
603
+
604
+ def load_mesh(
605
+ self,
606
+ mesh,
607
+ scale_factor=1.0,
608
+ auto_center=True,
609
+ ):
610
+ """
611
+ Load mesh from file and set up rendering data structures.
612
+
613
+ Args:
614
+ mesh: Path to mesh file or mesh object
615
+ scale_factor: Scaling factor for mesh normalization
616
+ auto_center: Whether to automatically center the mesh
617
+ """
618
+ vtx_pos, pos_idx, vtx_uv, uv_idx, texture_data = load_mesh(mesh)
619
+ self.set_mesh(
620
+ vtx_pos, pos_idx, vtx_uv=vtx_uv, uv_idx=uv_idx, scale_factor=scale_factor, auto_center=auto_center
621
+ )
622
+ if texture_data is not None:
623
+ self.set_texture(texture_data)
624
+
625
+ def save_mesh(self, mesh_path, downsample=False):
626
+ """
627
+ Save current mesh with textures to GLB file.
628
+
629
+ Args:
630
+ mesh_path: Output file path (will be saved as .glb)
631
+ downsample: Whether to downsample textures by half
632
+ """
633
+ # Ensure the output path has .glb extension
634
+ if not mesh_path.endswith('.glb'):
635
+ mesh_path = mesh_path.rsplit('.', 1)[0] + '.glb'
636
+
637
+ # Get mesh geometry
638
+ vtx_pos, pos_idx, vtx_uv, uv_idx = self.get_mesh(normalize=False)
639
+
640
+ # Get textures
641
+ texture_data = self.get_texture() # [H, W, 3], numpy array in [0, 1]
642
+ texture_metallic, texture_roughness = self.get_texture_mr() # each [H, W, 3] or None
643
+
644
+ # Downsample if requested
645
+ if downsample:
646
+ texture_data = cv2.resize(texture_data, (texture_data.shape[1] // 2, texture_data.shape[0] // 2))
647
+ if texture_metallic is not None:
648
+ texture_metallic = cv2.resize(
649
+ texture_metallic, (texture_metallic.shape[1] // 2, texture_metallic.shape[0] // 2)
650
+ )
651
+ if texture_roughness is not None:
652
+ texture_roughness = cv2.resize(
653
+ texture_roughness, (texture_roughness.shape[1] // 2, texture_roughness.shape[0] // 2)
654
+ )
655
+
656
+ # Convert texture_data from [0, 1] float to [0, 255] uint8 for PIL
657
+ albedo_uint8 = (np.clip(texture_data, 0, 1) * 255).astype(np.uint8)
658
+ albedo_pil = Image.fromarray(albedo_uint8, mode='RGB')
659
+
660
+ # Create ORM texture (Occlusion, Roughness, Metallic)
661
+ # glTF standard: R=unused/occlusion, G=roughness, B=metallic
662
+ orm_pil = None
663
+ if texture_metallic is not None and texture_roughness is not None:
664
+ # Extract single channel from the repeated 3-channel textures
665
+ # texture_metallic and texture_roughness are [H, W, 3] with repeated channels
666
+ roughness_channel = (np.clip(texture_roughness[:, :, 0], 0, 1) * 255).astype(np.uint8)
667
+ metallic_channel = (np.clip(texture_metallic[:, :, 0], 0, 1) * 255).astype(np.uint8)
668
+
669
+ # Create ORM image: R=1 (full occlusion/unused), G=roughness, B=metallic
670
+ h, w = roughness_channel.shape
671
+ orm_array = np.ones((h, w, 3), dtype=np.uint8) * 255
672
+ orm_array[:, :, 1] = roughness_channel # G channel = roughness
673
+ orm_array[:, :, 2] = metallic_channel # B channel = metallic
674
+
675
+ orm_pil = Image.fromarray(orm_array, mode='RGB')
676
+
677
+ # Create trimesh object
678
+ # Note: trimesh expects vertices and faces, UV data will be set via visual
679
+ mesh = trimesh.Trimesh(
680
+ vertices=vtx_pos,
681
+ faces=pos_idx,
682
+ process=False # Don't process to preserve UV mapping
683
+ )
684
+
685
+ # Set UV coordinates
686
+ # trimesh uses per-vertex UVs, but we have per-face-vertex UVs
687
+ # We need to create a TextureVisuals object
688
+ if vtx_uv is not None and uv_idx is not None:
689
+ # Check if UV indices match position indices (simple case)
690
+ if np.array_equal(uv_idx, pos_idx):
691
+ # Simple case: per-vertex UVs
692
+ mesh.visual = trimesh.visual.TextureVisuals(uv=vtx_uv)
693
+ else:
694
+ # Complex case: need to expand vertices to match UV mapping
695
+ # Create new vertices where each face corner gets its own vertex
696
+ new_vertices = vtx_pos[pos_idx.flatten()]
697
+ new_uvs = vtx_uv[uv_idx.flatten()]
698
+ new_faces = np.arange(len(pos_idx) * 3).reshape(-1, 3)
699
+
700
+ mesh = trimesh.Trimesh(
701
+ vertices=new_vertices,
702
+ faces=new_faces,
703
+ process=False
704
+ )
705
+ mesh.visual = trimesh.visual.TextureVisuals(uv=new_uvs)
706
+
707
+ # Use obj_to_pbr_glb to save as GLB
708
+ if obj_to_pbr_glb is not None:
709
+ obj_to_pbr_glb(
710
+ obj_path_or_mesh=mesh,
711
+ base_color_path=albedo_pil,
712
+ orm_path=orm_pil,
713
+ output_glb_path=mesh_path,
714
+ flip_uv=False,
715
+ center=False,
716
+ scale_to_unit=False,
717
+ )
718
+ else:
719
+ # Fallback to old method if obj_to_pbr_glb is not available
720
+ print("Warning: obj_to_pbr_glb not available, falling back to OBJ export")
721
+ texture_normal = self.get_texture_normal()
722
+ if downsample and texture_normal is not None:
723
+ texture_normal = cv2.resize(
724
+ texture_normal, (texture_normal.shape[1] // 2, texture_normal.shape[0] // 2)
725
+ )
726
+ save_mesh(
727
+ mesh_path.replace('.glb', '.obj'),
728
+ vtx_pos,
729
+ pos_idx,
730
+ vtx_uv,
731
+ uv_idx,
732
+ texture_data,
733
+ metallic=texture_metallic,
734
+ roughness=texture_roughness,
735
+ normal=texture_normal,
736
+ )
737
+
738
+ def set_mesh(self, vtx_pos, pos_idx, vtx_uv=None, uv_idx=None, scale_factor=1.0, auto_center=True):
739
+ """
740
+ Set mesh geometry data and perform coordinate transformations.
741
+
742
+ Args:
743
+ vtx_pos: Vertex positions [N, 3]
744
+ pos_idx: Triangle vertex indices [F, 3]
745
+ vtx_uv: UV coordinates [N, 2], optional
746
+ uv_idx: Triangle UV indices [F, 3], optional
747
+ scale_factor: Scaling factor for mesh normalization
748
+ auto_center: Whether to automatically center and scale the mesh
749
+ """
750
+ self.vtx_pos = torch.from_numpy(vtx_pos).to(self.device)
751
+ self.pos_idx = torch.from_numpy(pos_idx).to(self.device)
752
+
753
+ # 确保顶点位置是float32类型
754
+ if self.vtx_pos.dtype == torch.float64:
755
+ self.vtx_pos = self.vtx_pos.to(torch.float32)
756
+
757
+ # 确保索引类型为int32
758
+ if self.pos_idx.dtype == torch.int64:
759
+ self.pos_idx = self.pos_idx.to(torch.int32)
760
+
761
+ if (vtx_uv is not None) and (uv_idx is not None):
762
+ self.vtx_uv = torch.from_numpy(vtx_uv).to(self.device)
763
+ self.uv_idx = torch.from_numpy(uv_idx).to(self.device)
764
+
765
+ # 确保UV坐标是float32类型
766
+ if self.vtx_uv.dtype == torch.float64:
767
+ self.vtx_uv = self.vtx_uv.to(torch.float32)
768
+
769
+ # 确保UV索引类型为int32
770
+ if self.uv_idx.dtype == torch.int64:
771
+ self.uv_idx = self.uv_idx.to(torch.int32)
772
+ else:
773
+ self.vtx_uv = None
774
+ self.uv_idx = None
775
+
776
+ self.vtx_pos[:, [0, 1]] = -self.vtx_pos[:, [0, 1]]
777
+ self.vtx_pos[:, [1, 2]] = self.vtx_pos[:, [2, 1]]
778
+ # if (vtx_uv is not None) and (uv_idx is not None):
779
+ # self.vtx_uv[:, 1] = 1.0 - self.vtx_uv[:, 1]
780
+ # pass
781
+
782
+ if auto_center:
783
+ # Calculate bounding box center (equivalent to mesh.bounds.mean(axis=0))
784
+ max_bb = self.vtx_pos.max(0)[0]
785
+ min_bb = self.vtx_pos.min(0)[0]
786
+ bbox_center = (max_bb + min_bb) / 2
787
+
788
+ # Move to center (equivalent to: mesh.vertices -= bbox_center)
789
+ self.vtx_pos = self.vtx_pos - bbox_center
790
+ transform_offset = bbox_center
791
+
792
+ # Rescale (equivalent to: max_scale = np.abs(mesh.vertices).max())
793
+ max_scale = torch.abs(self.vtx_pos).max()
794
+ if max_scale > 0:
795
+ transform_scale = max_scale / scale_factor * 2
796
+ self.vtx_pos = self.vtx_pos / transform_scale
797
+ else:
798
+ transform_scale = torch.tensor(1.0, device=self.device)
799
+
800
+ # Store transformation parameters for inverse operation
801
+ self.scale_factor = scale_factor
802
+ self.mesh_normalize_scale_factor = float(transform_scale)
803
+ self.mesh_normalize_scale_center = transform_offset.unsqueeze(0).cpu().numpy()
804
+ else:
805
+ self.scale_factor = 1.0
806
+ self.mesh_normalize_scale_factor = 1.0
807
+ self.mesh_normalize_scale_center = np.array([[0, 0, 0]])
808
+
809
+ if uv_idx is not None:
810
+ self.extract_textiles()
811
+
812
+ def _set_texture_unified(
813
+ self, tex: Union[np.ndarray, torch.Tensor, Image.Image], texture_type: TextureType, force_set: bool = False
814
+ ):
815
+ """Unified texture setting method."""
816
+ converted_tex = _convert_texture_format(tex, self.texture_size, self.device, force_set)
817
+
818
+ if texture_type == TextureType.DIFFUSE:
819
+ self.tex = converted_tex
820
+ elif texture_type == TextureType.METALLIC_ROUGHNESS:
821
+ self.tex_mr = converted_tex
822
+ elif texture_type == TextureType.NORMAL:
823
+ self.tex_normalMap = converted_tex
824
+
825
+ def set_texture(self, tex, force_set=False):
826
+ """Set the main diffuse texture for the mesh."""
827
+ self._set_texture_unified(tex, TextureType.DIFFUSE, force_set)
828
+
829
+ def set_texture_mr(self, mr, force_set=False):
830
+ """Set metallic-roughness texture for PBR rendering."""
831
+ self._set_texture_unified(mr, TextureType.METALLIC_ROUGHNESS, force_set)
832
+
833
+ def set_texture_normal(self, normal, force_set=False):
834
+ """Set normal map texture for surface detail."""
835
+ self._set_texture_unified(normal, TextureType.NORMAL, force_set)
836
+
837
+ def set_default_render_resolution(self, default_resolution):
838
+ """
839
+ Set the default resolution for rendering operations.
840
+
841
+ Args:
842
+ default_resolution: Resolution as int (square) or tuple (height, width)
843
+ """
844
+ if isinstance(default_resolution, int):
845
+ default_resolution = (default_resolution, default_resolution)
846
+ self.default_resolution = default_resolution
847
+
848
+ def set_default_texture_resolution(self, texture_size):
849
+ """
850
+ Set the default texture resolution for UV mapping operations.
851
+
852
+ Args:
853
+ texture_size: Texture size as int (square) or tuple (height, width)
854
+ """
855
+ if isinstance(texture_size, int):
856
+ texture_size = (texture_size, texture_size)
857
+ self.texture_size = texture_size
858
+
859
+ def get_face_num(self):
860
+ """
861
+ Get the number of triangular faces in the mesh.
862
+
863
+ Returns:
864
+ Number of faces as integer
865
+ """
866
+ return self.pos_idx.shape[0]
867
+
868
+ def get_vertex_num(self):
869
+ """
870
+ Get the number of vertices in the mesh.
871
+
872
+ Returns:
873
+ Number of vertices as integer
874
+ """
875
+ return self.vtx_pos.shape[0]
876
+
877
+ def get_face_areas(self, from_one_index=False):
878
+ """
879
+ Calculate the area of each triangular face in the mesh.
880
+
881
+ Args:
882
+ from_one_index: If True, insert zero at beginning for 1-indexed face IDs
883
+
884
+ Returns:
885
+ Numpy array of face areas
886
+ """
887
+ v0 = self.vtx_pos[self.pos_idx[:, 0], :]
888
+ v1 = self.vtx_pos[self.pos_idx[:, 1], :]
889
+ v2 = self.vtx_pos[self.pos_idx[:, 2], :]
890
+
891
+ # 计算两个边向量
892
+ edge1 = v1 - v0
893
+ edge2 = v2 - v0
894
+
895
+ # 计算叉积的模长的一半即为面积
896
+ areas = torch.norm(torch.cross(edge1, edge2, dim=-1), dim=-1) * 0.5
897
+
898
+ areas = areas.cpu().numpy()
899
+
900
+ if from_one_index:
901
+ # 在数组前面插入一个0,因为三角片索引是从1开始的
902
+ areas = np.insert(areas, 0, 0)
903
+
904
+ return areas
905
+
906
+ def get_mesh(self, normalize=True):
907
+ """
908
+ Get mesh geometry with optional coordinate denormalization.
909
+
910
+ Args:
911
+ normalize: Whether to keep normalized coordinates (True) or restore original scale (False)
912
+
913
+ Returns:
914
+ Tuple of (vertex_positions, face_indices, uv_coordinates, uv_indices)
915
+ """
916
+ vtx_pos = self.vtx_pos.cpu().numpy()
917
+ pos_idx = self.pos_idx.cpu().numpy()
918
+ vtx_uv = self.vtx_uv.cpu().numpy()
919
+ uv_idx = self.uv_idx.cpu().numpy()
920
+
921
+ # 坐标变换的逆变换
922
+ if not normalize:
923
+ vtx_pos = vtx_pos / self.mesh_normalize_scale_factor
924
+ vtx_pos = vtx_pos + self.mesh_normalize_scale_center
925
+ vtx_pos[:, [1, 2]] = vtx_pos[:, [2, 1]]
926
+ vtx_pos[:, [0, 1]] = -vtx_pos[:, [0, 1]]
927
+
928
+ # vtx_uv[:, 1] = 1.0 - vtx_uv[:, 1]
929
+ return vtx_pos, pos_idx, vtx_uv, uv_idx
930
+
931
+ def get_texture(self):
932
+ """
933
+ Get the current diffuse texture as numpy array.
934
+
935
+ Returns:
936
+ Texture as numpy array in range [0, 1]
937
+ """
938
+ return self.tex.cpu().numpy()
939
+
940
+ def get_texture_mr(self):
941
+ """
942
+ Get metallic and roughness textures as separate channels.
943
+
944
+ Returns:
945
+ Tuple of (metallic_texture, roughness_texture) as numpy arrays, or (None, None) if not set
946
+ """
947
+ metallic, roughness = None, None
948
+ if hasattr(self, "tex_mr"):
949
+ mr = self.tex_mr.cpu().numpy()
950
+ metallic = np.repeat(mr[:, :, 2:3], repeats=3, axis=2)
951
+ roughness = np.repeat(mr[:, :, 1:2], repeats=3, axis=2)
952
+ return metallic, roughness
953
+
954
+ def get_texture_normal(self):
955
+ """
956
+ Get the normal map texture as numpy array.
957
+
958
+ Returns:
959
+ Normal map as numpy array, or None if not set
960
+ """
961
+ normal = None
962
+ if hasattr(self, "tex_normalMap"):
963
+ normal = self.tex_normalMap.cpu().numpy()
964
+ return normal
965
+
966
+ def to(self, device):
967
+ """
968
+ Move all tensor attributes to the specified device.
969
+
970
+ Args:
971
+ device: Target device ("cuda", "cpu", etc.)
972
+ """
973
+ self.device = device
974
+
975
+ for attr_name in dir(self):
976
+ attr_value = getattr(self, attr_name)
977
+ if isinstance(attr_value, torch.Tensor):
978
+ setattr(self, attr_name, attr_value.to(self.device))
979
+
980
+ def color_rgb_to_srgb(self, image):
981
+ """
982
+ Convert RGB color values to sRGB color space using gamma correction.
983
+
984
+ Args:
985
+ image: Input image as PIL Image, numpy array, or torch tensor
986
+
987
+ Returns:
988
+ sRGB corrected image in same format as input
989
+ """
990
+ if isinstance(image, Image.Image):
991
+ image_rgb = torch.tesnor(np.array(image) / 255.0).float().to(self.device)
992
+ elif isinstance(image, np.ndarray):
993
+ image_rgb = torch.tensor(image).float()
994
+ else:
995
+ image_rgb = image.to(self.device)
996
+
997
+ image_srgb = torch.where(
998
+ image_rgb <= 0.0031308, 12.92 * image_rgb, 1.055 * torch.pow(image_rgb, 1 / 2.4) - 0.055
999
+ )
1000
+
1001
+ if isinstance(image, Image.Image):
1002
+ image_srgb = Image.fromarray((image_srgb.cpu().numpy() * 255).astype(np.uint8))
1003
+ elif isinstance(image, np.ndarray):
1004
+ image_srgb = image_srgb.cpu().numpy()
1005
+ else:
1006
+ image_srgb = image_srgb.to(image.device)
1007
+
1008
+ return image_srgb
1009
+
1010
+ def extract_textiles(self):
1011
+ """
1012
+ Extract texture-space position and normal information by rasterizing
1013
+ the mesh in UV coordinate space. Creates texture-space geometry mappings.
1014
+ """
1015
+
1016
+ vnum = self.vtx_uv.shape[0]
1017
+ vtx_uv = torch.cat(
1018
+ (self.vtx_uv, torch.zeros_like(self.vtx_uv[:, 0:1]), torch.ones_like(self.vtx_uv[:, 0:1])), axis=1
1019
+ )
1020
+ vtx_uv = vtx_uv.view(1, vnum, 4) * 2 - 1
1021
+
1022
+ rast_out, rast_out_db = self.raster_rasterize(vtx_uv, self.uv_idx, resolution=self.texture_size)
1023
+ position, _ = self.raster_interpolate(self.vtx_pos, rast_out, self.pos_idx)
1024
+
1025
+ v0 = self.vtx_pos[self.pos_idx[:, 0], :]
1026
+ v1 = self.vtx_pos[self.pos_idx[:, 1], :]
1027
+ v2 = self.vtx_pos[self.pos_idx[:, 2], :]
1028
+ face_normals = F.normalize(torch.cross(v1 - v0, v2 - v0, dim=-1), dim=-1)
1029
+ vertex_normals = trimesh.geometry.mean_vertex_normals(
1030
+ vertex_count=self.vtx_pos.shape[0],
1031
+ faces=self.pos_idx.cpu(),
1032
+ face_normals=face_normals.cpu(),
1033
+ )
1034
+ vertex_normals = torch.from_numpy(vertex_normals).to(self.vtx_pos).contiguous()
1035
+ position_normal, _ = self.raster_interpolate(vertex_normals[None, ...], rast_out, self.pos_idx)
1036
+ visible_mask = torch.clamp(rast_out[..., -1:], 0, 1)[0, ..., 0]
1037
+ position = position[0]
1038
+ position_normal = position_normal[0]
1039
+ tri_ids = rast_out[0, ..., 3]
1040
+ tri_ids_mask = tri_ids > 0
1041
+ tri_ids = ((tri_ids - 1) * tri_ids_mask).long()
1042
+ position_normal.reshape(-1, 3)[tri_ids_mask.view(-1)] = face_normals.reshape(-1, 3)[
1043
+ tri_ids[tri_ids_mask].view(-1)
1044
+ ]
1045
+
1046
+ row = torch.arange(position.shape[0]).to(visible_mask.device)
1047
+ col = torch.arange(position.shape[1]).to(visible_mask.device)
1048
+ grid_i, grid_j = torch.meshgrid(row, col, indexing="ij")
1049
+
1050
+ mask = visible_mask.reshape(-1) > 0
1051
+ position = position.reshape(-1, 3)[mask]
1052
+ position_normal = position_normal.reshape(-1, 3)[mask]
1053
+ position = torch.cat((position, torch.ones_like(position[:, :1])), axis=-1)
1054
+ grid = torch.stack((grid_i, grid_j), -1).reshape(-1, 2)[mask]
1055
+
1056
+ texture_indices = (
1057
+ torch.ones(self.texture_size[0], self.texture_size[1], device=self.device, dtype=torch.long) * -1
1058
+ )
1059
+ texture_indices.view(-1)[grid[:, 0] * self.texture_size[1] + grid[:, 1]] = torch.arange(grid.shape[0]).to(
1060
+ device=self.device, dtype=torch.long
1061
+ )
1062
+
1063
+ self.tex_position = position
1064
+ self.tex_normal = position_normal
1065
+ self.tex_grid = grid
1066
+ self.texture_indices = texture_indices
1067
+
1068
+ def render_normal(
1069
+ self,
1070
+ elev,
1071
+ azim,
1072
+ camera_distance=None,
1073
+ center=None,
1074
+ resolution=None,
1075
+ bg_color=[1, 1, 1],
1076
+ use_abs_coor=False,
1077
+ normalize_rgb=True,
1078
+ return_type="th",
1079
+ ):
1080
+ """Render surface normals of the mesh from specified viewpoint."""
1081
+ config = RenderConfig(elev, azim, camera_distance, center, resolution, bg_color, return_type)
1082
+ image = self._unified_render_pipeline(
1083
+ config, RenderMode.NORMAL, use_abs_coor=use_abs_coor, normalize_rgb=normalize_rgb
1084
+ )
1085
+ return _format_output(image, return_type)
1086
+
1087
+ def convert_normal_map(self, image):
1088
+ """
1089
+ Convert normal map from standard format to renderer's coordinate system.
1090
+ Applies coordinate transformations for proper normal interpretation.
1091
+
1092
+ Args:
1093
+ image: Input normal map as PIL Image or numpy array
1094
+
1095
+ Returns:
1096
+ Converted normal map as PIL Image
1097
+ """
1098
+ # blue is front, red is left, green is top
1099
+ if isinstance(image, Image.Image):
1100
+ image = np.array(image)
1101
+ mask = (image == [255, 255, 255]).all(axis=-1)
1102
+
1103
+ image = (image / 255.0) * 2.0 - 1.0
1104
+
1105
+ image[..., [1]] = -image[..., [1]]
1106
+ image[..., [1, 2]] = image[..., [2, 1]]
1107
+ image[..., [0]] = -image[..., [0]]
1108
+
1109
+ image = (image + 1.0) * 0.5
1110
+
1111
+ image = (image * 255).astype(np.uint8)
1112
+ image[mask] = [127, 127, 255]
1113
+
1114
+ return Image.fromarray(image)
1115
+
1116
+ def render_position(
1117
+ self, elev, azim, camera_distance=None, center=None, resolution=None, bg_color=[1, 1, 1], return_type="th"
1118
+ ):
1119
+ """Render world-space positions of visible mesh surface points."""
1120
+ config = RenderConfig(elev, azim, camera_distance, center, resolution, bg_color, return_type)
1121
+ image = self._unified_render_pipeline(config, RenderMode.POSITION)
1122
+
1123
+ if return_type == ReturnType.PIL.value:
1124
+ image = image.squeeze(-1).cpu().numpy() * 255
1125
+ return Image.fromarray(image.astype(np.uint8))
1126
+ return _format_output(image, return_type)
1127
+
1128
+ def render_uvpos(self, return_type="th"):
1129
+ """Render vertex positions mapped to UV texture space."""
1130
+ config = RenderConfig(return_type=return_type)
1131
+ image = self._unified_render_pipeline(config, RenderMode.UV_POS)
1132
+ return _format_output(image, return_type)
1133
+
1134
+ def render_alpha(self, elev, azim, camera_distance=None, center=None, resolution=None, return_type="th"):
1135
+ """Render binary alpha mask indicating visible mesh regions."""
1136
+ config = RenderConfig(elev, azim, camera_distance, center, resolution, return_type=return_type)
1137
+ image = self._unified_render_pipeline(config, RenderMode.ALPHA)
1138
+
1139
+ if return_type == ReturnType.PIL.value:
1140
+ raise Exception("PIL format not supported for alpha rendering")
1141
+ return _format_output(image, return_type)
1142
+
1143
+ def uv_feature_map(self, vert_feat, bg=None):
1144
+ """
1145
+ Map per-vertex features to UV texture space using mesh topology.
1146
+
1147
+ Args:
1148
+ vert_feat: Per-vertex feature tensor [N, C]
1149
+ bg: Background value for unmapped regions (optional)
1150
+
1151
+ Returns:
1152
+ Feature map in UV texture space [H, W, C]
1153
+ """
1154
+ vtx_uv = self.vtx_uv * 2 - 1.0
1155
+ vtx_uv = torch.cat([vtx_uv, torch.zeros_like(self.vtx_uv)], dim=1).unsqueeze(0)
1156
+ vtx_uv[..., -1] = 1
1157
+ uv_idx = self.uv_idx
1158
+ rast_out, rast_out_db = self.raster_rasterize(vtx_uv, uv_idx, resolution=self.texture_size)
1159
+ feat_map, _ = self.raster_interpolate(vert_feat[None, ...], rast_out, uv_idx)
1160
+ feat_map = feat_map[0, ...]
1161
+ if bg is not None:
1162
+ visible_mask = torch.clamp(rast_out[..., -1:], 0, 1)[0, ...]
1163
+ feat_map[visible_mask == 0] = bg
1164
+ return feat_map
1165
+
1166
+ def render_sketch_from_geometry(self, normal_image, depth_image):
1167
+ """
1168
+ Generate sketch-style edge image from rendered normal and depth maps.
1169
+
1170
+ Args:
1171
+ normal_image: Rendered normal map tensor
1172
+ depth_image: Rendered depth map tensor
1173
+
1174
+ Returns:
1175
+ Binary edge sketch image as tensor
1176
+ """
1177
+ normal_image_np = normal_image.cpu().numpy()
1178
+ depth_image_np = depth_image.cpu().numpy()
1179
+
1180
+ normal_image_np = (normal_image_np * 255).astype(np.uint8)
1181
+ depth_image_np = (depth_image_np * 255).astype(np.uint8)
1182
+ normal_image_np = cv2.cvtColor(normal_image_np, cv2.COLOR_RGB2GRAY)
1183
+
1184
+ normal_edges = cv2.Canny(normal_image_np, 80, 150)
1185
+ depth_edges = cv2.Canny(depth_image_np, 30, 80)
1186
+
1187
+ combined_edges = np.maximum(normal_edges, depth_edges)
1188
+
1189
+ sketch_image = torch.from_numpy(combined_edges).to(normal_image.device).float() / 255.0
1190
+ sketch_image = sketch_image.unsqueeze(-1)
1191
+
1192
+ return sketch_image
1193
+
1194
+ def render_sketch_from_depth(self, depth_image):
1195
+ """
1196
+ Generate sketch-style edge image from depth map using edge detection.
1197
+
1198
+ Args:
1199
+ depth_image: Input depth map tensor
1200
+
1201
+ Returns:
1202
+ Binary edge sketch image as tensor
1203
+ """
1204
+ depth_image_np = depth_image.cpu().numpy()
1205
+ depth_image_np = (depth_image_np * 255).astype(np.uint8)
1206
+ depth_edges = cv2.Canny(depth_image_np, 30, 80)
1207
+ combined_edges = depth_edges
1208
+ sketch_image = torch.from_numpy(combined_edges).to(depth_image.device).float() / 255.0
1209
+ sketch_image = sketch_image.unsqueeze(-1)
1210
+ return sketch_image
1211
+
1212
+ def back_project(self, image, elev, azim, camera_distance=None, center=None, method=None):
1213
+ """
1214
+ Back-project a rendered image onto the mesh's UV texture space.
1215
+ Handles visibility, viewing angle, and boundary detection for texture baking.
1216
+
1217
+ Args:
1218
+ image: Input image to back-project (PIL Image, numpy array, or tensor)
1219
+ elev: Camera elevation angle in degrees used for rendering
1220
+ azim: Camera azimuth angle in degrees used for rendering
1221
+ camera_distance: Camera distance (uses default if None)
1222
+ center: Camera focus center (uses origin if None)
1223
+ method: Back-projection method ("linear", "mip-map", "back_sample", uses default if None)
1224
+
1225
+ Returns:
1226
+ Tuple of (texture, cosine_map, boundary_map) tensors in UV space
1227
+ """
1228
+
1229
+ if isinstance(image, Image.Image):
1230
+ image = torch.tensor(np.array(image) / 255.0)
1231
+ elif isinstance(image, np.ndarray):
1232
+ image = torch.tensor(image)
1233
+ if image.dim() == 2:
1234
+ image = image.unsqueeze(-1)
1235
+ image = image.float().to(self.device)
1236
+ resolution = image.shape[:2]
1237
+ channel = image.shape[-1]
1238
+ texture = torch.zeros(self.texture_size + (channel,)).to(self.device)
1239
+ cos_map = torch.zeros(self.texture_size + (1,)).to(self.device)
1240
+
1241
+ proj = self.camera_proj_mat
1242
+ r_mv = get_mv_matrix(
1243
+ elev=elev,
1244
+ azim=azim,
1245
+ camera_distance=self.camera_distance if camera_distance is None else camera_distance,
1246
+ center=center,
1247
+ )
1248
+ pos_camera = transform_pos(r_mv, self.vtx_pos, keepdim=True)
1249
+ pos_clip = transform_pos(proj, pos_camera)
1250
+ pos_camera = pos_camera[:, :3] / pos_camera[:, 3:4]
1251
+
1252
+ v0 = pos_camera[self.pos_idx[:, 0], :]
1253
+ v1 = pos_camera[self.pos_idx[:, 1], :]
1254
+ v2 = pos_camera[self.pos_idx[:, 2], :]
1255
+ face_normals = F.normalize(torch.cross(v1 - v0, v2 - v0, dim=-1), dim=-1)
1256
+
1257
+ tex_depth = pos_camera[:, 2].reshape(1, -1, 1).contiguous()
1258
+ rast_out, rast_out_db = self.raster_rasterize(pos_clip, self.pos_idx, resolution=resolution)
1259
+ visible_mask = torch.clamp(rast_out[..., -1:], 0, 1)[0, ...]
1260
+
1261
+ if self.shader_type == "vertex":
1262
+ vertex_normals = trimesh.geometry.mean_vertex_normals(
1263
+ vertex_count=self.vtx_pos.shape[0],
1264
+ faces=self.pos_idx.cpu(),
1265
+ face_normals=face_normals.cpu(),
1266
+ )
1267
+ vertex_normals = torch.from_numpy(vertex_normals).float().to(self.device).contiguous()
1268
+ normal, _ = self.raster_interpolate(vertex_normals[None, ...], rast_out, self.pos_idx)
1269
+ elif self.shader_type == "face":
1270
+ tri_ids = rast_out[..., 3]
1271
+ tri_ids_mask = tri_ids > 0
1272
+ tri_ids = ((tri_ids - 1) * tri_ids_mask).long()
1273
+ normal = torch.zeros(rast_out.shape[0], rast_out.shape[1], rast_out.shape[2], 3).to(rast_out)
1274
+ normal.reshape(-1, 3)[tri_ids_mask.view(-1)] = face_normals.reshape(-1, 3)[tri_ids[tri_ids_mask].view(-1)]
1275
+
1276
+ normal = normal[0, ...]
1277
+ uv, _ = self.raster_interpolate(self.vtx_uv[None, ...], rast_out, self.uv_idx)
1278
+ depth, _ = self.raster_interpolate(tex_depth, rast_out, self.pos_idx)
1279
+ depth = depth[0, ...]
1280
+
1281
+ depth_max, depth_min = depth[visible_mask > 0].max(), depth[visible_mask > 0].min()
1282
+ depth_normalized = (depth - depth_min) / (depth_max - depth_min)
1283
+ depth_image = depth_normalized * visible_mask # Mask out background.
1284
+
1285
+ sketch_image = self.render_sketch_from_depth(depth_image)
1286
+
1287
+ lookat = torch.tensor([[0, 0, -1]], device=self.device)
1288
+ # cos_image = torch.nn.functional.cosine_similarity(lookat, normal.view(-1, 3))
1289
+ # cos_image = cos_image.view(normal.shape[0], normal.shape[1], 1)
1290
+ cos_raw = torch.nn.functional.cosine_similarity(lookat, normal.view(-1, 3)).view(normal.shape[0], normal.shape[1], 1)
1291
+ cos_image = cos_raw.abs() # 양면 처리
1292
+
1293
+ cos_thres = np.cos(self.bake_angle_thres / 180 * np.pi)
1294
+ cos_image[cos_image < cos_thres] = 0
1295
+
1296
+ # shrink
1297
+ if self.bake_unreliable_kernel_size > 0:
1298
+ kernel_size = self.bake_unreliable_kernel_size * 2 + 1
1299
+ kernel = torch.ones((1, 1, kernel_size, kernel_size), dtype=torch.float32).to(sketch_image.device)
1300
+
1301
+ visible_mask = visible_mask.permute(2, 0, 1).unsqueeze(0).float()
1302
+ visible_mask = F.conv2d(1.0 - visible_mask, kernel, padding=kernel_size // 2)
1303
+ visible_mask = 1.0 - (visible_mask > 0).float() # 二值化
1304
+ visible_mask = visible_mask.squeeze(0).permute(1, 2, 0)
1305
+
1306
+ sketch_image = sketch_image.permute(2, 0, 1).unsqueeze(0)
1307
+ sketch_image = F.conv2d(sketch_image, kernel, padding=kernel_size // 2)
1308
+ sketch_image = (sketch_image > 0).float() # 二值化
1309
+ sketch_image = sketch_image.squeeze(0).permute(1, 2, 0)
1310
+ visible_mask = visible_mask * (sketch_image < 0.5)
1311
+
1312
+ cos_image[visible_mask == 0] = 0
1313
+
1314
+ method = self.bake_mode if method is None else method
1315
+
1316
+ if method == "linear":
1317
+ proj_mask = (visible_mask != 0).view(-1)
1318
+ uv = uv.squeeze(0).contiguous().view(-1, 2)[proj_mask]
1319
+ image = image.squeeze(0).contiguous().view(-1, channel)[proj_mask]
1320
+ cos_image = cos_image.contiguous().view(-1, 1)[proj_mask]
1321
+ sketch_image = sketch_image.contiguous().view(-1, 1)[proj_mask]
1322
+
1323
+ texture = linear_grid_put_2d(self.texture_size[1], self.texture_size[0], uv[..., [1, 0]], image)
1324
+ cos_map = linear_grid_put_2d(self.texture_size[1], self.texture_size[0], uv[..., [1, 0]], cos_image)
1325
+ boundary_map = linear_grid_put_2d(self.texture_size[1], self.texture_size[0], uv[..., [1, 0]], sketch_image)
1326
+ elif method == "mip-map":
1327
+ proj_mask = (visible_mask != 0).view(-1)
1328
+ uv = uv.squeeze(0).contiguous().view(-1, 2)[proj_mask]
1329
+ image = image.squeeze(0).contiguous().view(-1, channel)[proj_mask]
1330
+ cos_image = cos_image.contiguous().view(-1, 1)[proj_mask]
1331
+
1332
+ texture = mipmap_linear_grid_put_2d(
1333
+ self.texture_size[1], self.texture_size[0], uv[..., [1, 0]], image, min_resolution=128
1334
+ )
1335
+ cos_map = mipmap_linear_grid_put_2d(
1336
+ self.texture_size[1], self.texture_size[0], uv[..., [1, 0]], cos_image, min_resolution=256
1337
+ )
1338
+
1339
+ if self.vtx_map is not None:
1340
+ vertex_normals = vertex_normals[self.vtx_map, :]
1341
+ normal_map = self.uv_feature_map(vertex_normals)
1342
+ # cos_map_uv = torch.nn.functional.cosine_similarity(lookat, normal_map.view(-1, 3)) # .abs()
1343
+ cos_map_uv = torch.nn.functional.cosine_similarity(lookat, normal_map.view(-1, 3)).abs()
1344
+ cos_map_uv = cos_map_uv.view(1, 1, normal_map.shape[0], normal_map.shape[1])
1345
+ cos_map_uv = torch.nn.functional.max_pool2d(cos_map_uv, kernel_size=3, stride=1, padding=1)
1346
+ cos_map_uv = cos_map_uv.reshape(self.texture_size[0], self.texture_size[1], 1)
1347
+ cos_map_uv[cos_map_uv < cos_thres] = 0
1348
+ # cos_map = torch.min(cos_map, cos_map_uv)
1349
+ cos_map[cos_map_uv < cos_thres] = 0
1350
+ elif method == "back_sample":
1351
+
1352
+ img_proj = torch.from_numpy(
1353
+ np.array(((proj[0, 0], 0, 0, 0), (0, proj[1, 1], 0, 0), (0, 0, 1, 0), (0, 0, 0, 1)))
1354
+ ).to(self.tex_position)
1355
+ w2c = torch.from_numpy(r_mv).to(self.tex_position)
1356
+ v_proj = self.tex_position @ w2c.T @ img_proj
1357
+ inner_mask = (v_proj[:, 0] <= 1.0) & (v_proj[:, 0] >= -1.0) & (v_proj[:, 1] <= 1.0) & (v_proj[:, 1] >= -1.0)
1358
+ inner_valid_idx = torch.where(inner_mask)[0].long()
1359
+ img_x = torch.clamp(
1360
+ ((v_proj[:, 0].clamp(-1, 1) * 0.5 + 0.5) * (resolution[0])).long(), 0, resolution[0] - 1
1361
+ )
1362
+ img_y = torch.clamp(
1363
+ ((v_proj[:, 1].clamp(-1, 1) * 0.5 + 0.5) * (resolution[1])).long(), 0, resolution[1] - 1
1364
+ )
1365
+
1366
+ indices = img_y * resolution[0] + img_x
1367
+ sampled_z = depth.reshape(-1)[indices]
1368
+ sampled_m = visible_mask.reshape(-1)[indices]
1369
+ v_z = v_proj[:, 2]
1370
+
1371
+ sampled_w = cos_image.reshape(-1)[indices]
1372
+ depth_thres = 3e-3
1373
+
1374
+ # valid_idx = torch.where((torch.abs(v_z - sampled_z) < depth_thres) * (sampled_m*sampled_w>0))[0]
1375
+ valid_idx = torch.where((torch.abs(v_z - sampled_z) < depth_thres) & (sampled_m * sampled_w > 0))[0]
1376
+
1377
+ intersection_mask = torch.isin(valid_idx, inner_valid_idx)
1378
+ valid_idx = valid_idx[intersection_mask].to(inner_valid_idx)
1379
+
1380
+ indices = indices[valid_idx]
1381
+ sampled_b = sketch_image.reshape(-1)[indices]
1382
+ sampled_w = sampled_w[valid_idx]
1383
+
1384
+ # bilinear sampling rgb
1385
+ wx = ((v_proj[:, 0] * 0.5 + 0.5) * resolution[0] - img_x)[valid_idx].reshape(-1, 1)
1386
+ wy = ((v_proj[:, 1] * 0.5 + 0.5) * resolution[1] - img_y)[valid_idx].reshape(-1, 1)
1387
+ img_x = img_x[valid_idx]
1388
+ img_y = img_y[valid_idx]
1389
+ img_x_r = torch.clamp(img_x + 1, 0, resolution[0] - 1)
1390
+ img_y_r = torch.clamp(img_y + 1, 0, resolution[1] - 1)
1391
+ indices_lr = img_y * resolution[0] + img_x_r
1392
+ indices_rl = img_y_r * resolution[0] + img_x
1393
+ indices_rr = img_y_r * resolution[0] + img_x_r
1394
+ rgb = image.reshape(-1, channel)
1395
+ sampled_rgb = (rgb[indices] * (1 - wx) + rgb[indices_lr] * wx) * (1 - wy) + (
1396
+ rgb[indices_rl] * (1 - wx) + rgb[indices_rr] * wx
1397
+ ) * wy
1398
+
1399
+ # return sampled_rgb, sampled_w, sampled_b, valid_idx
1400
+ texture = torch.zeros(self.texture_size[0], self.texture_size[1], channel, device=self.device).reshape(
1401
+ -1, channel
1402
+ )
1403
+ cos_map = torch.zeros(self.texture_size[0], self.texture_size[1], 1, device=self.device).reshape(-1)
1404
+ boundary_map = torch.zeros(self.texture_size[0], self.texture_size[1], 1, device=self.device).reshape(-1)
1405
+
1406
+ valid_tex_indices = self.tex_grid[valid_idx, 0] * self.texture_size[1] + self.tex_grid[valid_idx, 1]
1407
+ texture[valid_tex_indices, :] = sampled_rgb
1408
+ cos_map[valid_tex_indices] = sampled_w
1409
+ boundary_map[valid_tex_indices] = sampled_b
1410
+
1411
+ texture = texture.view(self.texture_size[0], self.texture_size[1], channel)
1412
+ cos_map = cos_map.view(self.texture_size[0], self.texture_size[1], 1)
1413
+ # texture = torch.clamp(texture,0,1)
1414
+
1415
+ else:
1416
+ raise f"No bake mode {method}"
1417
+ return texture, cos_map, boundary_map
1418
+
1419
+ def bake_texture(self, colors, elevs, azims, camera_distance=None, center=None, exp=6, weights=None):
1420
+ """
1421
+ Bake multiple view images into a single UV texture using weighted blending.
1422
+
1423
+ Args:
1424
+ colors: List of input images (tensors, numpy arrays, or PIL Images)
1425
+ elevs: List of elevation angles for each view
1426
+ azims: List of azimuth angles for each view
1427
+ camera_distance: Camera distance (uses default if None)
1428
+ center: Camera focus center (uses origin if None)
1429
+ exp: Exponent for cosine weighting (higher values favor front-facing views)
1430
+ weights: Optional per-view weights (defaults to 1.0 for all views)
1431
+
1432
+ Returns:
1433
+ Tuple of (merged_texture, trust_map) tensors in UV space
1434
+ """
1435
+ if isinstance(colors, torch.Tensor):
1436
+ colors = [colors[i, ...].float().permute(1, 2, 0) for i in range(colors.shape[0])]
1437
+ else:
1438
+ for i in range(len(colors)):
1439
+ if isinstance(colors[i], Image.Image):
1440
+ colors[i] = torch.tensor(np.array(colors[i]) / 255.0, device=self.device).float()
1441
+ if weights is None:
1442
+ weights = [1.0 for _ in range(len(colors))]
1443
+ textures = []
1444
+ cos_maps = []
1445
+ for color, elev, azim, weight in zip(colors, elevs, azims, weights):
1446
+ texture, cos_map, _ = self.back_project(color, elev, azim, camera_distance, center)
1447
+ cos_map = weight * (cos_map**exp)
1448
+ textures.append(texture)
1449
+ cos_maps.append(cos_map)
1450
+
1451
+ texture_merge, trust_map_merge = self.fast_bake_texture(textures, cos_maps)
1452
+ return texture_merge, trust_map_merge
1453
+
1454
+ @torch.no_grad()
1455
+ def fast_bake_texture(self, textures, cos_maps):
1456
+ """
1457
+ Efficiently merge multiple textures using cosine-weighted blending.
1458
+ Optimizes by skipping views that don't contribute new information.
1459
+
1460
+ Args:
1461
+ textures: List of texture tensors to merge
1462
+ cos_maps: List of corresponding cosine weight maps
1463
+
1464
+ Returns:
1465
+ Tuple of (merged_texture, valid_mask) tensors
1466
+ """
1467
+
1468
+ channel = textures[0].shape[-1]
1469
+ texture_merge = torch.zeros(self.texture_size + (channel,)).to(self.device)
1470
+ trust_map_merge = torch.zeros(self.texture_size + (1,)).to(self.device)
1471
+ for texture, cos_map in zip(textures, cos_maps):
1472
+ view_sum = (cos_map > 0).sum()
1473
+ painted_sum = ((cos_map > 0) * (trust_map_merge > 0)).sum()
1474
+ if painted_sum / view_sum > 0.99:
1475
+ continue
1476
+ texture_merge += texture * cos_map
1477
+ trust_map_merge += cos_map
1478
+ texture_merge = texture_merge / torch.clamp(trust_map_merge, min=1e-8)
1479
+
1480
+ return texture_merge, trust_map_merge > 1e-8
1481
+
1482
+ @torch.no_grad()
1483
+ def uv_inpaint(self, texture, mask, vertex_inpaint=True, method="NS", return_float=False):
1484
+ """
1485
+ Inpaint missing regions in UV texture using mesh-aware and traditional methods.
1486
+
1487
+ Args:
1488
+ texture: Input texture as tensor, numpy array, or PIL Image
1489
+ mask: Binary mask indicating regions to inpaint (1 = keep, 0 = inpaint)
1490
+ vertex_inpaint: Whether to use mesh vertex connectivity for inpainting
1491
+ method: Inpainting method ("NS" for Navier-Stokes)
1492
+ return_float: Whether to return float values (False returns uint8)
1493
+
1494
+ Returns:
1495
+ Inpainted texture as numpy array
1496
+ """
1497
+
1498
+ if isinstance(texture, torch.Tensor):
1499
+ texture_np = texture.cpu().numpy()
1500
+ elif isinstance(texture, np.ndarray):
1501
+ texture_np = texture
1502
+ elif isinstance(texture, Image.Image):
1503
+ texture_np = np.array(texture) / 255.0
1504
+
1505
+ if isinstance(mask, torch.Tensor):
1506
+ mask = (mask.squeeze(-1).cpu().numpy() * 255).astype(np.uint8)
1507
+
1508
+ if vertex_inpaint:
1509
+ vtx_pos, pos_idx, vtx_uv, uv_idx = self.get_mesh()
1510
+ texture_np, mask = meshVerticeInpaint(texture_np, mask, vtx_pos, vtx_uv, pos_idx, uv_idx)
1511
+ # save the mask image for debug
1512
+ mask_image = Image.fromarray(255 - mask)
1513
+ mask_image.save("debug_uv_mask.png")
1514
+
1515
+ if method == "NS":
1516
+ texture_np = cv2.inpaint((texture_np * 255).astype(np.uint8), 255 - mask, 3, cv2.INPAINT_NS)
1517
+ assert return_float == False
1518
+
1519
+ return texture_np
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/__init__.py ADDED
File without changes
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/camera_utils.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import numpy as np
4
+ import torch
5
+
6
+
7
+ def transform_pos(mtx, pos, keepdim=False):
8
+ t_mtx = torch.from_numpy(mtx).to(pos.device) if isinstance(mtx, np.ndarray) else mtx
9
+ if pos.shape[-1] == 3:
10
+ posw = torch.cat([pos, torch.ones([pos.shape[0], 1]).to(pos.device)], axis=1)
11
+ else:
12
+ posw = pos
13
+
14
+ if keepdim:
15
+ return torch.matmul(posw, t_mtx.t())[...]
16
+ else:
17
+ return torch.matmul(posw, t_mtx.t())[None, ...]
18
+
19
+
20
+ def get_mv_matrix(elev, azim, camera_distance, center=None):
21
+ elev = -elev
22
+ azim += 90
23
+
24
+ elev_rad = math.radians(elev)
25
+ azim_rad = math.radians(azim)
26
+
27
+ camera_position = np.array(
28
+ [
29
+ camera_distance * math.cos(elev_rad) * math.cos(azim_rad),
30
+ camera_distance * math.cos(elev_rad) * math.sin(azim_rad),
31
+ camera_distance * math.sin(elev_rad),
32
+ ]
33
+ )
34
+
35
+ if center is None:
36
+ center = np.array([0, 0, 0])
37
+ else:
38
+ center = np.array(center)
39
+
40
+ lookat = center - camera_position
41
+ lookat = lookat / np.linalg.norm(lookat)
42
+
43
+ up = np.array([0, 0, 1.0])
44
+ right = np.cross(lookat, up)
45
+ right = right / np.linalg.norm(right)
46
+ up = np.cross(right, lookat)
47
+ up = up / np.linalg.norm(up)
48
+
49
+ c2w = np.concatenate([np.stack([right, up, -lookat], axis=-1), camera_position[:, None]], axis=-1)
50
+
51
+ w2c = np.zeros((4, 4))
52
+ w2c[:3, :3] = np.transpose(c2w[:3, :3], (1, 0))
53
+ w2c[:3, 3:] = -np.matmul(np.transpose(c2w[:3, :3], (1, 0)), c2w[:3, 3:])
54
+ w2c[3, 3] = 1.0
55
+
56
+ return w2c.astype(np.float32)
57
+
58
+
59
+ def get_orthographic_projection_matrix(left=-1, right=1, bottom=-1, top=1, near=0, far=2):
60
+ """
61
+ 计算正交投影矩阵。
62
+
63
+ 参数:
64
+ left (float): 投影区域左侧边界。
65
+ right (float): 投影区域右侧边界。
66
+ bottom (float): 投影区域底部边界。
67
+ top (float): 投影区域顶部边界。
68
+ near (float): 投影区域近裁剪面距离。
69
+ far (float): 投影区域远裁剪面距离。
70
+
71
+ 返回:
72
+ numpy.ndarray: 正交投影矩阵。
73
+ """
74
+ ortho_matrix = np.eye(4, dtype=np.float32)
75
+ ortho_matrix[0, 0] = 2 / (right - left)
76
+ ortho_matrix[1, 1] = 2 / (top - bottom)
77
+ ortho_matrix[2, 2] = -2 / (far - near)
78
+ ortho_matrix[0, 3] = -(right + left) / (right - left)
79
+ ortho_matrix[1, 3] = -(top + bottom) / (top - bottom)
80
+ ortho_matrix[2, 3] = -(far + near) / (far - near)
81
+ return ortho_matrix
82
+
83
+
84
+ def get_perspective_projection_matrix(fovy, aspect_wh, near, far):
85
+ fovy_rad = math.radians(fovy)
86
+ return np.array(
87
+ [
88
+ [1.0 / (math.tan(fovy_rad / 2.0) * aspect_wh), 0, 0, 0],
89
+ [0, 1.0 / math.tan(fovy_rad / 2.0), 0, 0],
90
+ [0, 0, -(far + near) / (far - near), -2.0 * far * near / (far - near)],
91
+ [0, 0, -1, 0],
92
+ ]
93
+ ).astype(np.float32)
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/compile_mesh_painter.sh ADDED
@@ -0,0 +1 @@
 
 
1
+ c++ -O3 -Wall -shared -std=c++11 -fPIC `python -m pybind11 --includes` mesh_inpaint_processor.cpp -o mesh_inpaint_processor`python3-config --extension-suffix`
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_inpaint_processor.cpp ADDED
@@ -0,0 +1,550 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <pybind11/numpy.h>
2
+ #include <pybind11/pybind11.h>
3
+ #include <pybind11/stl.h>
4
+
5
+ #include <algorithm>
6
+ #include <array>
7
+ #include <cmath>
8
+ #include <queue>
9
+ #include <vector>
10
+ #include <functional>
11
+
12
+ namespace py = pybind11;
13
+ using namespace std;
14
+
15
+ namespace {
16
+
17
+ // =========================
18
+ // Core mesh data container
19
+ // =========================
20
+ struct MeshData {
21
+ int texture_height, texture_width, texture_channel;
22
+ int vtx_num;
23
+ float* texture_ptr;
24
+ uint8_t* mask_ptr;
25
+ float* vtx_pos_ptr;
26
+ float* vtx_uv_ptr;
27
+ int* pos_idx_ptr;
28
+ int* uv_idx_ptr;
29
+
30
+ // Keep buffers alive
31
+ py::buffer_info texture_buf, mask_buf, vtx_pos_buf, vtx_uv_buf, pos_idx_buf, uv_idx_buf;
32
+
33
+ MeshData(py::array_t<float>& texture, py::array_t<uint8_t>& mask,
34
+ py::array_t<float>& vtx_pos, py::array_t<float>& vtx_uv,
35
+ py::array_t<int>& pos_idx, py::array_t<int>& uv_idx) {
36
+ texture_buf = texture.request();
37
+ mask_buf = mask.request();
38
+ vtx_pos_buf = vtx_pos.request();
39
+ vtx_uv_buf = vtx_uv.request();
40
+ pos_idx_buf = pos_idx.request();
41
+ uv_idx_buf = uv_idx.request();
42
+
43
+ texture_height = static_cast<int>(texture_buf.shape[0]);
44
+ texture_width = static_cast<int>(texture_buf.shape[1]);
45
+ texture_channel= static_cast<int>(texture_buf.shape[2]);
46
+ texture_ptr = static_cast<float*>(texture_buf.ptr);
47
+ mask_ptr = static_cast<uint8_t*>(mask_buf.ptr);
48
+
49
+ vtx_num = static_cast<int>(vtx_pos_buf.shape[0]);
50
+ vtx_pos_ptr = static_cast<float*>(vtx_pos_buf.ptr);
51
+ vtx_uv_ptr = static_cast<float*>(vtx_uv_buf.ptr);
52
+ pos_idx_ptr = static_cast<int*>(pos_idx_buf.ptr);
53
+ uv_idx_ptr = static_cast<int*>(uv_idx_buf.ptr);
54
+ }
55
+ };
56
+
57
+ // =========================
58
+ // Helpers
59
+ // =========================
60
+
61
+ // Integer pixel (y, x) from UV with vertical flip (V -> image y)
62
+ pair<int,int> calculateUVCoordinates(int vtx_uv_idx, const MeshData& data) {
63
+ int x = static_cast<int>(std::round(data.vtx_uv_ptr[vtx_uv_idx * 2] * (data.texture_width - 1)));
64
+ int y = static_cast<int>(std::round((1.0f - data.vtx_uv_ptr[vtx_uv_idx * 2 + 1]) * (data.texture_height - 1)));
65
+ return {y, x}; // (y, x)
66
+ }
67
+
68
+ // Float pixel (x, y) from UV with vertical flip
69
+ inline pair<float,float> uvIdxToXYFloat(int vtx_uv_idx, const MeshData& data) {
70
+ float x = data.vtx_uv_ptr[vtx_uv_idx * 2] * (data.texture_width - 1);
71
+ float y = (1.0f - data.vtx_uv_ptr[vtx_uv_idx * 2 + 1]) * (data.texture_height - 1);
72
+ return {x, y}; // (x, y)
73
+ }
74
+
75
+ float calculateDistanceWeight(const array<float,3>& v0, const array<float,3>& v1) {
76
+ float dx = v0[0] - v1[0];
77
+ float dy = v0[1] - v1[1];
78
+ float dz = v0[2] - v1[2];
79
+ float d = std::sqrt(dx*dx + dy*dy + dz*dz);
80
+ float inv = 1.0f / std::max(d, 1e-4f);
81
+ return inv * inv;
82
+ }
83
+
84
+ array<float,3> getVertexPosition(int vtx_idx, const MeshData& data) {
85
+ return { data.vtx_pos_ptr[vtx_idx*3+0],
86
+ data.vtx_pos_ptr[vtx_idx*3+1],
87
+ data.vtx_pos_ptr[vtx_idx*3+2] };
88
+ }
89
+
90
+ // Undirected 1-ring graph from faces
91
+ void buildGraph(vector<vector<int>>& G, const MeshData& data) {
92
+ G.clear();
93
+ G.resize(data.vtx_num);
94
+ const int F = static_cast<int>(data.uv_idx_buf.shape[0]);
95
+ for (int i = 0; i < F; ++i) {
96
+ for (int k = 0; k < 3; ++k) {
97
+ int a = data.pos_idx_ptr[i * 3 + k];
98
+ int b = data.pos_idx_ptr[i * 3 + (k + 1) % 3];
99
+ G[a].push_back(b);
100
+ G[b].push_back(a);
101
+ }
102
+ }
103
+ }
104
+
105
+ // =========================
106
+ // Vertex-aware initialization
107
+ // - For each 3D vertex, aggregate samples from all its UV corners.
108
+ // - If a UV corner pixel is invalid (mask==0), search a small neighborhood.
109
+ // - Average the found samples to get a stable vertex color.
110
+ // =========================
111
+
112
+ // Search nearest valid (mask>0) pixel around (x0,y0). Returns true if found.
113
+ bool sample_color_near_uv(const MeshData& data, int x0, int y0, int r_max, vector<float>& out_color) {
114
+ const int W = data.texture_width;
115
+ const int H = data.texture_height;
116
+ const int C = data.texture_channel;
117
+ out_color.assign(C, 0.0f);
118
+
119
+ // Clamp seed
120
+ x0 = std::min(std::max(x0, 0), W - 1);
121
+ y0 = std::min(std::max(y0, 0), H - 1);
122
+
123
+ // If seed is valid, take it.
124
+ if (data.mask_ptr[y0 * W + x0] > 0) {
125
+ for (int c = 0; c < C; ++c) {
126
+ out_color[c] = data.texture_ptr[(y0 * W + x0) * C + c];
127
+ }
128
+ return true;
129
+ }
130
+
131
+ // Expand square neighborhood up to r_max
132
+ float best_d2 = std::numeric_limits<float>::infinity();
133
+ int best_x = -1, best_y = -1;
134
+
135
+ for (int r = 1; r <= r_max; ++r) {
136
+ int xmin = std::max(0, x0 - r);
137
+ int xmax = std::min(W - 1, x0 + r);
138
+ int ymin = std::max(0, y0 - r);
139
+ int ymax = std::min(H - 1, y0 + r);
140
+ bool found_this_ring = false;
141
+
142
+ for (int y = ymin; y <= ymax; ++y) {
143
+ for (int x = xmin; x <= xmax; ++x) {
144
+ if (data.mask_ptr[y * W + x] == 0) continue;
145
+ float dx = float(x - x0), dy = float(y - y0);
146
+ float d2 = dx*dx + dy*dy;
147
+ if (d2 < best_d2) {
148
+ best_d2 = d2;
149
+ best_x = x;
150
+ best_y = y;
151
+ found_this_ring = true;
152
+ }
153
+ }
154
+ }
155
+ // Early exit when ring finds something (closest for this r)
156
+ if (found_this_ring) break;
157
+ }
158
+
159
+ if (best_x >= 0) {
160
+ for (int c = 0; c < C; ++c) {
161
+ out_color[c] = data.texture_ptr[(best_y * W + best_x) * C + c];
162
+ }
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+
168
+ template<typename MaskType>
169
+ void initializeVertexDataVertexAware(const MeshData& data,
170
+ vector<MaskType>& vtx_mask,
171
+ vector<vector<float>>& vtx_color,
172
+ vector<int>* uncolored_vtxs = nullptr,
173
+ MaskType mask_value = static_cast<MaskType>(1),
174
+ int r_max = 5) {
175
+ const int V = data.vtx_num;
176
+ const int C = data.texture_channel;
177
+ const int F = static_cast<int>(data.uv_idx_buf.shape[0]);
178
+
179
+ vtx_mask.assign(V, static_cast<MaskType>(0));
180
+ vtx_color.assign(V, vector<float>(C, 0.0f));
181
+ vector<float> accum_w(V, 0.0f);
182
+ if (uncolored_vtxs) uncolored_vtxs->clear();
183
+
184
+ for (int face_idx = 0; face_idx < F; ++face_idx) {
185
+ for (int k = 0; k < 3; ++k) {
186
+ int vtx_idx = data.pos_idx_ptr[face_idx * 3 + k];
187
+ int vtx_uv_idx = data.uv_idx_ptr[face_idx * 3 + k];
188
+
189
+ // Seed pixel from this UV corner
190
+ auto uv_xy_int = calculateUVCoordinates(vtx_uv_idx, data); // (y, x)
191
+ int sx = uv_xy_int.second;
192
+ int sy = uv_xy_int.first;
193
+
194
+ vector<float> col;
195
+ if (!sample_color_near_uv(data, sx, sy, r_max, col)) {
196
+ continue; // no contribution from this corner
197
+ }
198
+
199
+ // Equal weight per found corner (can be replaced by distance-based if needed)
200
+ float w = 1.0f;
201
+ for (int c = 0; c < C; ++c) {
202
+ vtx_color[vtx_idx][c] += w * col[c];
203
+ }
204
+ accum_w[vtx_idx] += w;
205
+ }
206
+ }
207
+
208
+ for (int v = 0; v < V; ++v) {
209
+ if (accum_w[v] > 0.0f) {
210
+ float inv = 1.0f / accum_w[v];
211
+ for (int c = 0; c < C; ++c) vtx_color[v][c] *= inv;
212
+ vtx_mask[v] = mask_value;
213
+ } else {
214
+ if (uncolored_vtxs) uncolored_vtxs->push_back(v);
215
+ }
216
+ }
217
+ }
218
+
219
+ // =========================
220
+ // Smoothing / Propagation
221
+ // =========================
222
+ template<typename MaskType>
223
+ void performSmoothingAlgorithm(const MeshData& data, const vector<vector<int>>& G,
224
+ vector<MaskType>& vtx_mask, vector<vector<float>>& vtx_color,
225
+ const vector<int>& uncolored_vtxs,
226
+ function<bool(MaskType)> is_colored_func,
227
+ function<void(MaskType&)> set_colored_func) {
228
+ // Limit to a small fixed number of passes for stability
229
+ const int max_pass = 8;
230
+ const int C = data.texture_channel;
231
+
232
+ for (int pass = 0; pass < max_pass; ++pass) {
233
+ int remained = 0;
234
+
235
+ for (int vtx_idx : uncolored_vtxs) {
236
+ if (is_colored_func(vtx_mask[vtx_idx])) continue;
237
+
238
+ vector<float> sum_color(C, 0.0f);
239
+ float total_w = 0.0f;
240
+
241
+ array<float,3> v0 = getVertexPosition(vtx_idx, data);
242
+ for (int nb : G[vtx_idx]) {
243
+ if (!is_colored_func(vtx_mask[nb])) continue;
244
+ array<float,3> v1 = getVertexPosition(nb, data);
245
+ float w = calculateDistanceWeight(v0, v1);
246
+ for (int c = 0; c < C; ++c) sum_color[c] += vtx_color[nb][c] * w;
247
+ total_w += w;
248
+ }
249
+
250
+ if (total_w > 0.0f) {
251
+ float inv = 1.0f / total_w;
252
+ for (int c = 0; c < C; ++c) vtx_color[vtx_idx][c] = sum_color[c] * inv;
253
+ set_colored_func(vtx_mask[vtx_idx]);
254
+ } else {
255
+ remained++;
256
+ }
257
+ }
258
+
259
+ if (remained == 0) break; // converged
260
+ }
261
+ }
262
+
263
+ void performForwardPropagation(const MeshData& data, const vector<vector<int>>& G,
264
+ vector<float>& vtx_mask, vector<vector<float>>& vtx_color,
265
+ queue<int>& active_vtxs) {
266
+ const int C = data.texture_channel;
267
+
268
+ while (!active_vtxs.empty()) {
269
+ queue<int> pending;
270
+
271
+ while (!active_vtxs.empty()) {
272
+ int vtx_idx = active_vtxs.front();
273
+ active_vtxs.pop();
274
+
275
+ array<float,3> v0 = getVertexPosition(vtx_idx, data);
276
+ for (int nb : G[vtx_idx]) {
277
+ if (vtx_mask[nb] > 0) continue;
278
+
279
+ array<float,3> v1 = getVertexPosition(nb, data);
280
+ float w = calculateDistanceWeight(v0, v1);
281
+ for (int c = 0; c < C; ++c) vtx_color[nb][c] += vtx_color[vtx_idx][c] * w;
282
+
283
+ if (vtx_mask[nb] == 0.0f) pending.push(nb);
284
+ vtx_mask[nb] -= w;
285
+ }
286
+ }
287
+
288
+ while (!pending.empty()) {
289
+ int v = pending.front(); pending.pop();
290
+ for (int c = 0; c < C; ++c) vtx_color[v][c] /= -vtx_mask[v];
291
+ vtx_mask[v] = 1.0f;
292
+ active_vtxs.push(v);
293
+ }
294
+ }
295
+ }
296
+
297
+ // =========================
298
+ // Triangle rasterization
299
+ // =========================
300
+ inline float edgeFunc(float x0,float y0,float x1,float y1,float x,float y){
301
+ return (x - x0)*(y1 - y0) - (y - y0)*(x1 - x0);
302
+ }
303
+
304
+ // Create output by rasterizing per-face colors from vertex colors.
305
+ // Only fills pixels where original mask == 0 (keeps existing valid texels).
306
+ pair<py::array_t<float>, py::array_t<uint8_t>> createOutputArrays(
307
+ const MeshData& data, const vector<float>& vtx_mask,
308
+ const vector<vector<float>>& vtx_color) {
309
+
310
+ // Allocate and copy originals
311
+ py::array_t<float> new_texture(data.texture_buf.size);
312
+ py::array_t<uint8_t> new_mask(data.mask_buf.size);
313
+
314
+ auto new_texture_buf = new_texture.request();
315
+ auto new_mask_buf = new_mask.request();
316
+
317
+ float* new_texture_ptr = static_cast<float*>(new_texture_buf.ptr);
318
+ uint8_t* new_mask_ptr = static_cast<uint8_t*>(new_mask_buf.ptr);
319
+
320
+ std::copy(data.texture_ptr, data.texture_ptr + data.texture_buf.size, new_texture_ptr);
321
+ std::copy(data.mask_ptr, data.mask_ptr + data.mask_buf.size, new_mask_ptr);
322
+
323
+ const int W = data.texture_width;
324
+ const int H = data.texture_height;
325
+ const int C = data.texture_channel;
326
+ const int F = static_cast<int>(data.uv_idx_buf.shape[0]);
327
+
328
+ for (int face_idx = 0; face_idx < F; ++face_idx) {
329
+ int v0 = data.pos_idx_ptr[face_idx*3 + 0];
330
+ int v1 = data.pos_idx_ptr[face_idx*3 + 1];
331
+ int v2 = data.pos_idx_ptr[face_idx*3 + 2];
332
+
333
+ // If no vertex has color, skip
334
+ if (!(vtx_mask[v0] > 0.0f || vtx_mask[v1] > 0.0f || vtx_mask[v2] > 0.0f)) continue;
335
+
336
+ int u0 = data.uv_idx_ptr[face_idx*3 + 0];
337
+ int u1 = data.uv_idx_ptr[face_idx*3 + 1];
338
+ int u2 = data.uv_idx_ptr[face_idx*3 + 2];
339
+
340
+ auto xy0 = uvIdxToXYFloat(u0, data); float x0 = xy0.first, y0 = xy0.second;
341
+ auto xy1 = uvIdxToXYFloat(u1, data); float x1 = xy1.first, y1 = xy1.second;
342
+ auto xy2 = uvIdxToXYFloat(u2, data); float x2 = xy2.first, y2 = xy2.second;
343
+
344
+ // Prepare vertex colors; if some are missing, use average of existing ones
345
+ vector<float> col0(C, 0.0f), col1(C, 0.0f), col2(C, 0.0f);
346
+ int cnt = 0;
347
+ if (vtx_mask[v0] > 0.0f) { for (int c = 0; c < C; ++c) col0[c] = vtx_color[v0][c]; cnt++; }
348
+ if (vtx_mask[v1] > 0.0f) { for (int c = 0; c < C; ++c) col1[c] = vtx_color[v1][c]; cnt++; }
349
+ if (vtx_mask[v2] > 0.0f) { for (int c = 0; c < C; ++c) col2[c] = vtx_color[v2][c]; cnt++; }
350
+
351
+ if (cnt > 0 && cnt < 3) {
352
+ // compute average of available
353
+ vector<float> avg(C, 0.0f);
354
+ if (vtx_mask[v0] > 0.0f) for (int c = 0; c < C; ++c) avg[c] += vtx_color[v0][c];
355
+ if (vtx_mask[v1] > 0.0f) for (int c = 0; c < C; ++c) avg[c] += vtx_color[v1][c];
356
+ if (vtx_mask[v2] > 0.0f) for (int c = 0; c < C; ++c) avg[c] += vtx_color[v2][c];
357
+ for (int c = 0; c < C; ++c) avg[c] /= float(cnt);
358
+ if (!(vtx_mask[v0] > 0.0f)) col0 = avg;
359
+ if (!(vtx_mask[v1] > 0.0f)) col1 = avg;
360
+ if (!(vtx_mask[v2] > 0.0f)) col2 = avg;
361
+ }
362
+
363
+ // Bounding box
364
+ int xmin = std::max(0, (int)std::floor(std::min({x0, x1, x2})));
365
+ int xmax = std::min(W-1, (int)std::ceil (std::max({x0, x1, x2})));
366
+ int ymin = std::max(0, (int)std::floor(std::min({y0, y1, y2})));
367
+ int ymax = std::min(H-1, (int)std::ceil (std::max({y0, y1, y2})));
368
+
369
+ float area = edgeFunc(x0,y0, x1,y1, x2,y2);
370
+ if (std::fabs(area) < 1e-6f) continue;
371
+
372
+ for (int y = ymin; y <= ymax; ++y) {
373
+ for (int x = xmin; x <= xmax; ++x) {
374
+ // Respect original valid texels
375
+ if (new_mask_ptr[y * W + x] > 0) continue;
376
+
377
+ float px = x + 0.5f, py = y + 0.5f;
378
+ float w0 = edgeFunc(x1,y1, x2,y2, px,py);
379
+ float w1 = edgeFunc(x2,y2, x0,y0, px,py);
380
+ float w2 = edgeFunc(x0,y0, x1,y1, px,py);
381
+
382
+ if ((w0 >= 0 && w1 >= 0 && w2 >= 0) || (w0 <= 0 && w1 <= 0 && w2 <= 0)) {
383
+ w0 /= area; w1 /= area; w2 /= area;
384
+ for (int c = 0; c < C; ++c) {
385
+ float v = w0*col0[c] + w1*col1[c] + w2*col2[c];
386
+ new_texture_ptr[(y * W + x) * C + c] = v;
387
+ }
388
+ new_mask_ptr[y * W + x] = 255;
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ // Reshape to HxWxC and HxW
395
+ new_texture.resize({ data.texture_height, data.texture_width, data.texture_channel });
396
+ new_mask.resize({ data.texture_height, data.texture_width });
397
+ return { new_texture, new_mask };
398
+ }
399
+
400
+ // Vertex color dump (unchanged interface)
401
+ pair<py::array_t<float>, py::array_t<uint8_t>> createVertexColorOutput(
402
+ const MeshData& data, const vector<int>& vtx_mask,
403
+ const vector<vector<float>>& vtx_color) {
404
+
405
+ py::array_t<float> py_vtx_color({ data.vtx_num, data.texture_channel });
406
+ py::array_t<uint8_t> py_vtx_mask({ data.vtx_num });
407
+
408
+ auto py_vtx_color_buf = py_vtx_color.request();
409
+ auto py_vtx_mask_buf = py_vtx_mask.request();
410
+
411
+ float* py_vtx_color_ptr = static_cast<float*>(py_vtx_color_buf.ptr);
412
+ uint8_t* py_vtx_mask_ptr = static_cast<uint8_t*>(py_vtx_mask_buf.ptr);
413
+
414
+ for (int i = 0; i < data.vtx_num; ++i) {
415
+ py_vtx_mask_ptr[i] = static_cast<uint8_t>(vtx_mask[i]);
416
+ for (int c = 0; c < data.texture_channel; ++c) {
417
+ py_vtx_color_ptr[i * data.texture_channel + c] = vtx_color[i][c];
418
+ }
419
+ }
420
+ return { py_vtx_color, py_vtx_mask };
421
+ }
422
+
423
+ } // anonymous namespace
424
+
425
+ // =========================
426
+ // meshVerticeInpaint - smooth
427
+ // =========================
428
+ pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeInpaint_smooth(
429
+ py::array_t<float> texture, py::array_t<uint8_t> mask,
430
+ py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
431
+ py::array_t<int> pos_idx, py::array_t<int> uv_idx) {
432
+
433
+ MeshData data(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
434
+
435
+ vector<float> vtx_mask;
436
+ vector<vector<float>> vtx_color;
437
+ vector<int> uncolored_vtxs;
438
+ vector<vector<int>> G;
439
+
440
+ // Vertex-aware initialization (multi-corner with neighborhood search)
441
+ initializeVertexDataVertexAware<float>(data, vtx_mask, vtx_color, &uncolored_vtxs, 1.0f, 5);
442
+ buildGraph(G, data);
443
+
444
+ performSmoothingAlgorithm<float>(
445
+ data, G, vtx_mask, vtx_color, uncolored_vtxs,
446
+ [](float m){ return m > 0.0f; },
447
+ [](float& m){ m = 1.0f; }
448
+ );
449
+
450
+ return createOutputArrays(data, vtx_mask, vtx_color);
451
+ }
452
+
453
+ // =========================
454
+ // meshVerticeInpaint - forward
455
+ // =========================
456
+ pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeInpaint_forward(
457
+ py::array_t<float> texture, py::array_t<uint8_t> mask,
458
+ py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
459
+ py::array_t<int> pos_idx, py::array_t<int> uv_idx) {
460
+
461
+ MeshData data(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
462
+
463
+ vector<float> vtx_mask;
464
+ vector<vector<float>> vtx_color;
465
+ vector<vector<int>> G;
466
+ queue<int> active_vtxs;
467
+
468
+ // Vertex-aware seeds
469
+ initializeVertexDataVertexAware<float>(data, vtx_mask, vtx_color, nullptr, 1.0f, 5);
470
+ buildGraph(G, data);
471
+
472
+ for (int i = 0; i < data.vtx_num; ++i) {
473
+ if (vtx_mask[i] == 1.0f) active_vtxs.push(i);
474
+ }
475
+
476
+ performForwardPropagation(data, G, vtx_mask, vtx_color, active_vtxs);
477
+
478
+ return createOutputArrays(data, vtx_mask, vtx_color);
479
+ }
480
+
481
+ // =========================
482
+ // Public inpaint API
483
+ // =========================
484
+ pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeInpaint(
485
+ py::array_t<float> texture, py::array_t<uint8_t> mask,
486
+ py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
487
+ py::array_t<int> pos_idx, py::array_t<int> uv_idx,
488
+ const string& method = "smooth") {
489
+
490
+ if (method == "smooth") {
491
+ return meshVerticeInpaint_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
492
+ } else if (method == "forward") {
493
+ return meshVerticeInpaint_forward(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
494
+ } else {
495
+ throw std::invalid_argument("Invalid method. Use 'smooth' or 'forward'.");
496
+ }
497
+ }
498
+
499
+ // =========================
500
+ // meshVerticeColor (vertex dump)
501
+ // =========================
502
+ pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeColor_smooth(
503
+ py::array_t<float> texture, py::array_t<uint8_t> mask,
504
+ py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
505
+ py::array_t<int> pos_idx, py::array_t<int> uv_idx) {
506
+
507
+ MeshData data(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
508
+
509
+ vector<int> vtx_mask;
510
+ vector<vector<float>> vtx_color;
511
+ vector<int> uncolored_vtxs;
512
+ vector<vector<int>> G;
513
+
514
+ // Vertex-aware init for INT mask type
515
+ initializeVertexDataVertexAware<int>(data, vtx_mask, vtx_color, &uncolored_vtxs, 1, 5);
516
+ buildGraph(G, data);
517
+
518
+ performSmoothingAlgorithm<int>(
519
+ data, G, vtx_mask, vtx_color, uncolored_vtxs,
520
+ [](int m){ return m > 0; },
521
+ [](int& m){ m = 2; } // mark as colored
522
+ );
523
+
524
+ return createVertexColorOutput(data, vtx_mask, vtx_color);
525
+ }
526
+
527
+ pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeColor(
528
+ py::array_t<float> texture, py::array_t<uint8_t> mask,
529
+ py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
530
+ py::array_t<int> pos_idx, py::array_t<int> uv_idx,
531
+ const string& method = "smooth") {
532
+
533
+ if (method == "smooth") {
534
+ return meshVerticeColor_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
535
+ } else {
536
+ throw std::invalid_argument("Invalid method. Use 'smooth' or 'forward'.");
537
+ }
538
+ }
539
+
540
+ // =========================
541
+ // PyBind11 module
542
+ // =========================
543
+ PYBIND11_MODULE(mesh_inpaint_processor, m) {
544
+ m.def("meshVerticeInpaint", &meshVerticeInpaint, "A function to process mesh",
545
+ py::arg("texture"), py::arg("mask"), py::arg("vtx_pos"), py::arg("vtx_uv"),
546
+ py::arg("pos_idx"), py::arg("uv_idx"), py::arg("method") = "smooth");
547
+ m.def("meshVerticeColor", &meshVerticeColor, "A function to process mesh",
548
+ py::arg("texture"), py::arg("mask"), py::arg("vtx_pos"), py::arg("vtx_uv"),
549
+ py::arg("pos_idx"), py::arg("uv_idx"), py::arg("method") = "smooth");
550
+ }
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_inpaint_processor.cpython-310-x86_64-linux-gnu.so ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b13b87ac483bc2a86adb0812fa7950eef76092e4d6031d3dc4e01f28ecd92609
3
+ size 242392
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/mesh_utils.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ # import bpy
4
+ import math
5
+ import numpy as np
6
+ from io import StringIO
7
+ from typing import Optional, Tuple, Dict, Any
8
+
9
+
10
+ def _safe_extract_attribute(obj: Any, attr_path: str, default: Any = None) -> Any:
11
+ """Extract nested attribute safely from object."""
12
+ try:
13
+ for attr in attr_path.split("."):
14
+ obj = getattr(obj, attr)
15
+ return obj
16
+ except AttributeError:
17
+ return default
18
+
19
+
20
+ def _convert_to_numpy(data: Any, dtype: np.dtype) -> Optional[np.ndarray]:
21
+ """Convert data to numpy array with specified dtype, handling None values."""
22
+ if data is None:
23
+ return None
24
+ return np.asarray(data, dtype=dtype)
25
+
26
+
27
+ def load_mesh(mesh):
28
+ """Load mesh data including vertices, faces, UV coordinates and texture."""
29
+ # Extract vertex positions and face indices
30
+ vtx_pos = _safe_extract_attribute(mesh, "vertices")
31
+ pos_idx = _safe_extract_attribute(mesh, "faces")
32
+
33
+ # Extract UV coordinates (reusing face indices for UV indices)
34
+ vtx_uv = _safe_extract_attribute(mesh, "visual.uv")
35
+ uv_idx = pos_idx # Reuse face indices for UV mapping
36
+
37
+ # Convert to numpy arrays with appropriate dtypes
38
+ vtx_pos = _convert_to_numpy(vtx_pos, np.float32)
39
+ pos_idx = _convert_to_numpy(pos_idx, np.int32)
40
+ vtx_uv = _convert_to_numpy(vtx_uv, np.float32)
41
+ uv_idx = _convert_to_numpy(uv_idx, np.int32)
42
+
43
+ texture_data = None
44
+ return vtx_pos, pos_idx, vtx_uv, uv_idx, texture_data
45
+
46
+
47
+ def _get_base_path_and_name(mesh_path: str) -> Tuple[str, str]:
48
+ """Get base path without extension and mesh name."""
49
+ base_path = os.path.splitext(mesh_path)[0]
50
+ name = os.path.basename(base_path)
51
+ return base_path, name
52
+
53
+
54
+ def _save_texture_map(
55
+ texture: np.ndarray,
56
+ base_path: str,
57
+ suffix: str = "",
58
+ image_format: str = ".jpg",
59
+ color_convert: Optional[int] = None,
60
+ ) -> str:
61
+ """Save texture map with optional color conversion."""
62
+ path = f"{base_path}{suffix}{image_format}"
63
+ processed_texture = (texture * 255).astype(np.uint8)
64
+
65
+ if color_convert is not None:
66
+ processed_texture = cv2.cvtColor(processed_texture, color_convert)
67
+ cv2.imwrite(path, processed_texture)
68
+ else:
69
+ cv2.imwrite(path, processed_texture[..., ::-1]) # RGB to BGR
70
+
71
+ return os.path.basename(path)
72
+
73
+
74
+ def _write_mtl_properties(f, properties: Dict[str, Any]):
75
+ """Write material properties to MTL file."""
76
+ for key, value in properties.items():
77
+ if isinstance(value, (list, tuple)):
78
+ f.write(f"{key} {' '.join(map(str, value))}\n")
79
+ else:
80
+ f.write(f"{key} {value}\n")
81
+
82
+
83
+ def _create_obj_content(
84
+ vtx_pos: np.ndarray, vtx_uv: np.ndarray, pos_idx: np.ndarray, uv_idx: np.ndarray, name: str
85
+ ) -> str:
86
+ """Create OBJ file content."""
87
+ buffer = StringIO()
88
+
89
+ # Write header and vertices
90
+ buffer.write(f"mtllib {name}.mtl\no {name}\n")
91
+ np.savetxt(buffer, vtx_pos, fmt="v %.6f %.6f %.6f")
92
+ np.savetxt(buffer, vtx_uv, fmt="vt %.6f %.6f")
93
+ buffer.write("s 0\nusemtl Material\n")
94
+
95
+ # Write faces
96
+ pos_idx_plus1 = pos_idx + 1
97
+ uv_idx_plus1 = uv_idx + 1
98
+ face_format = np.frompyfunc(lambda *x: f"{int(x[0])}/{int(x[1])}", 2, 1)
99
+ faces = face_format(pos_idx_plus1, uv_idx_plus1)
100
+ face_strings = [f"f {' '.join(face)}" for face in faces]
101
+ buffer.write("\n".join(face_strings) + "\n")
102
+
103
+ return buffer.getvalue()
104
+
105
+
106
+ def save_obj_mesh(mesh_path, vtx_pos, pos_idx, vtx_uv, uv_idx, texture, metallic=None, roughness=None, normal=None):
107
+ """Save mesh as OBJ file with textures and material."""
108
+ # Convert inputs to numpy arrays
109
+ vtx_pos = _convert_to_numpy(vtx_pos, np.float32)
110
+ vtx_uv = _convert_to_numpy(vtx_uv, np.float32)
111
+ pos_idx = _convert_to_numpy(pos_idx, np.int32)
112
+ uv_idx = _convert_to_numpy(uv_idx, np.int32)
113
+
114
+ base_path, name = _get_base_path_and_name(mesh_path)
115
+
116
+ # Create and save OBJ content
117
+ obj_content = _create_obj_content(vtx_pos, vtx_uv, pos_idx, uv_idx, name)
118
+ with open(mesh_path, "w") as obj_file:
119
+ obj_file.write(obj_content)
120
+
121
+ # Save texture maps
122
+ texture_maps = {}
123
+ texture_maps["diffuse"] = _save_texture_map(texture, base_path)
124
+
125
+ if metallic is not None:
126
+ texture_maps["metallic"] = _save_texture_map(metallic, base_path, "_metallic", color_convert=cv2.COLOR_RGB2GRAY)
127
+ if roughness is not None:
128
+ texture_maps["roughness"] = _save_texture_map(
129
+ roughness, base_path, "_roughness", color_convert=cv2.COLOR_RGB2GRAY
130
+ )
131
+ if normal is not None:
132
+ texture_maps["normal"] = _save_texture_map(normal, base_path, "_normal")
133
+
134
+ # Create MTL file
135
+ _create_mtl_file(base_path, texture_maps, metallic is not None)
136
+
137
+
138
+ def _create_mtl_file(base_path: str, texture_maps: Dict[str, str], is_pbr: bool):
139
+ """Create MTL material file."""
140
+ mtl_path = f"{base_path}.mtl"
141
+
142
+ with open(mtl_path, "w") as f:
143
+ f.write("newmtl Material\n")
144
+
145
+ if is_pbr:
146
+ # PBR material properties
147
+ properties = {
148
+ "Kd": [0.800, 0.800, 0.800],
149
+ "Ke": [0.000, 0.000, 0.000], # 鐜鍏夐伄钄�
150
+ "Ni": 1.500, # 鎶樺皠绯绘暟
151
+ "d": 1.0, # 閫忔槑搴�
152
+ "illum": 2, # 鍏夌収妯″瀷
153
+ "map_Kd": texture_maps["diffuse"],
154
+ }
155
+ _write_mtl_properties(f, properties)
156
+
157
+ # Additional PBR maps
158
+ map_configs = [("metallic", "map_Pm"), ("roughness", "map_Pr"), ("normal", "map_Bump -bm 1.0")]
159
+
160
+ for texture_key, mtl_key in map_configs:
161
+ if texture_key in texture_maps:
162
+ f.write(f"{mtl_key} {texture_maps[texture_key]}\n")
163
+ else:
164
+ # Standard material properties
165
+ properties = {
166
+ "Ns": 250.000000,
167
+ "Ka": [0.200, 0.200, 0.200],
168
+ "Kd": [0.800, 0.800, 0.800],
169
+ "Ks": [0.500, 0.500, 0.500],
170
+ "Ke": [0.000, 0.000, 0.000],
171
+ "Ni": 1.500,
172
+ "d": 1.0,
173
+ "illum": 3,
174
+ "map_Kd": texture_maps["diffuse"],
175
+ }
176
+ _write_mtl_properties(f, properties)
177
+
178
+
179
+ def save_mesh(mesh_path, vtx_pos, pos_idx, vtx_uv, uv_idx, texture, metallic=None, roughness=None, normal=None):
180
+ """Save mesh using OBJ format."""
181
+ save_obj_mesh(
182
+ mesh_path, vtx_pos, pos_idx, vtx_uv, uv_idx, texture, metallic=metallic, roughness=roughness, normal=normal
183
+ )
184
+
185
+
186
+ # def _setup_blender_scene():
187
+ # """Setup Blender scene for conversion."""
188
+ # if "convert" not in bpy.data.scenes:
189
+ # bpy.data.scenes.new("convert")
190
+ # bpy.context.window.scene = bpy.data.scenes["convert"]
191
+
192
+
193
+ # def _clear_scene_objects():
194
+ # """Clear all objects from current Blender scene."""
195
+ # for obj in bpy.context.scene.objects:
196
+ # obj.select_set(True)
197
+ # bpy.data.objects.remove(obj, do_unlink=True)
198
+
199
+
200
+ # def _select_mesh_objects():
201
+ # """Select all mesh objects in scene."""
202
+ # bpy.ops.object.select_all(action="DESELECT")
203
+ # for obj in bpy.context.scene.objects:
204
+ # if obj.type == "MESH":
205
+ # obj.select_set(True)
206
+
207
+
208
+ # def _merge_vertices_if_needed(merge_vertices: bool):
209
+ # """Merge duplicate vertices if requested."""
210
+ # if not merge_vertices:
211
+ # return
212
+
213
+ # for obj in bpy.context.selected_objects:
214
+ # if obj.type == "MESH":
215
+ # bpy.context.view_layer.objects.active = obj
216
+ # bpy.ops.object.mode_set(mode="EDIT")
217
+ # bpy.ops.mesh.select_all(action="SELECT")
218
+ # bpy.ops.mesh.remove_doubles()
219
+ # bpy.ops.object.mode_set(mode="OBJECT")
220
+
221
+
222
+ # def _apply_shading(shade_type: str, auto_smooth_angle: float):
223
+ # """Apply shading to selected objects."""
224
+ # shading_ops = {
225
+ # "SMOOTH": lambda: bpy.ops.object.shade_smooth(),
226
+ # "FLAT": lambda: bpy.ops.object.shade_flat(),
227
+ # "AUTO_SMOOTH": lambda: _apply_auto_smooth(auto_smooth_angle),
228
+ # }
229
+
230
+ # if shade_type in shading_ops:
231
+ # shading_ops[shade_type]()
232
+
233
+
234
+ # def _apply_auto_smooth(auto_smooth_angle: float):
235
+ # """Apply auto smooth based on Blender version."""
236
+ # angle_rad = math.radians(auto_smooth_angle)
237
+
238
+ # if bpy.app.version < (4, 1, 0):
239
+ # bpy.ops.object.shade_smooth(use_auto_smooth=True, auto_smooth_angle=angle_rad)
240
+ # elif bpy.app.version < (4, 2, 0):
241
+ # bpy.ops.object.shade_smooth_by_angle(angle=angle_rad)
242
+ # else:
243
+ # bpy.ops.object.shade_auto_smooth(angle=angle_rad)
244
+
245
+
246
+ # def convert_obj_to_glb(
247
+ # obj_path: str,
248
+ # glb_path: str,
249
+ # shade_type: str = "SMOOTH",
250
+ # auto_smooth_angle: float = 60,
251
+ # merge_vertices: bool = False,
252
+ # ) -> bool:
253
+ # """Convert OBJ file to GLB format using Blender."""
254
+ # try:
255
+ # _setup_blender_scene()
256
+ # _clear_scene_objects()
257
+
258
+ # # Import OBJ file
259
+ # bpy.ops.wm.obj_import(filepath=obj_path)
260
+ # _select_mesh_objects()
261
+
262
+ # # Process meshes
263
+ # _merge_vertices_if_needed(merge_vertices)
264
+ # _apply_shading(shade_type, auto_smooth_angle)
265
+
266
+ # # Export to GLB
267
+ # bpy.ops.export_scene.gltf(filepath=glb_path, use_active_scene=True)
268
+ # return True
269
+ # except Exception:
270
+ # return False
home/ubuntu/aaaaa/data/rgbmr/DifferentiableRenderer/obj_to_glb.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ from typing import Optional, Tuple, Union
4
+
5
+ import numpy as np
6
+ from PIL import Image
7
+ import trimesh
8
+
9
+ from gltflib import (
10
+ GLTF, GLTFModel, Asset, Scene, Node, Mesh, Primitive, Attributes,
11
+ Buffer, BufferView, Accessor, AccessorType, BufferTarget, ComponentType,
12
+ Material, PBRMetallicRoughness, Texture, Image as GLTFImage, TextureInfo,
13
+ FileResource, Sampler
14
+ )
15
+
16
+ def _mime_from_path(path: str) -> str:
17
+ ext = os.path.splitext(path)[1].lower()
18
+ if ext in [".jpg", ".jpeg"]:
19
+ return "image/jpeg"
20
+ if ext == ".png":
21
+ return "image/png"
22
+ if ext == ".webp":
23
+ return "image/webp"
24
+ # PIL로 최후 판정
25
+ try:
26
+ with Image.open(path) as im:
27
+ fmt = (im.format or "").lower()
28
+ return {"jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(fmt, "image/png")
29
+ except Exception:
30
+ return "image/png"
31
+
32
+ def _mime_from_image(img: Image.Image) -> str:
33
+ """PIL Image로부터 MIME 타입 추론"""
34
+ fmt = (img.format or "").lower()
35
+ return {"jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(fmt, "image/png")
36
+
37
+ def _align4(n: int) -> int:
38
+ return (n + 3) & ~3
39
+
40
+ def _pack_f32(arr: np.ndarray) -> bytes:
41
+ return arr.astype("<f4", copy=False).tobytes(order="C")
42
+
43
+ def _pack_indices(arr: np.ndarray) -> Tuple[bytes, int]:
44
+ max_idx = int(arr.max()) if arr.size > 0 else 0
45
+ if max_idx < 65536:
46
+ return arr.astype("<u2", copy=False).tobytes(order="C"), ComponentType.UNSIGNED_SHORT.value
47
+ return arr.astype("<u4", copy=False).tobytes(order="C"), ComponentType.UNSIGNED_INT.value
48
+
49
+ def _mins_maxs(vec: np.ndarray) -> Tuple[list, list]:
50
+ if vec.size == 0:
51
+ return [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]
52
+ mins = [float(vec[:, i].min()) for i in range(vec.shape[1])]
53
+ maxs = [float(vec[:, i].max()) for i in range(vec.shape[1])]
54
+ return mins, maxs
55
+
56
+ def obj_to_pbr_glb(
57
+ obj_path_or_mesh: Union[str, trimesh.Trimesh],
58
+ base_color_path: Union[str, Image.Image],
59
+ orm_path: Optional[Union[str, Image.Image]],
60
+ output_glb_path: str,
61
+ *,
62
+ flip_uv: bool = True,
63
+ center: bool = False,
64
+ scale_to_unit: bool = False,
65
+ generator: str = "obj2pbr-glb (gltflib)",
66
+ material_name: str = "Material",
67
+ node_name: str = "Node",
68
+ mesh_name: str = "Mesh",
69
+ ) -> None:
70
+ # ---- 1) OBJ 로드 (단일 메시로 직접 로드) ----
71
+ # OBJ는 단일 메시이므로 force="mesh"로 직접 로드
72
+ if isinstance(obj_path_or_mesh, str):
73
+ mesh = trimesh.load(obj_path_or_mesh, force="mesh", process=True)
74
+ elif isinstance(obj_path_or_mesh, trimesh.Trimesh):
75
+ mesh = obj_path_or_mesh
76
+ else:
77
+ raise ValueError("Invalid input type for obj_path_or_mesh.")
78
+
79
+ if not isinstance(mesh, trimesh.Trimesh):
80
+ raise ValueError("Failed to load OBJ as a Trimesh.")
81
+
82
+ if center:
83
+ mesh.vertices -= mesh.vertices.mean(axis=0)
84
+ if scale_to_unit:
85
+ s = np.max(np.linalg.norm(mesh.vertices, axis=1))
86
+ if s > 0:
87
+ mesh.vertices /= s
88
+ if mesh.faces is None or len(mesh.faces) == 0:
89
+ raise ValueError("Mesh has no faces. Ensure the OBJ is triangulated.")
90
+
91
+ # ---- 2) 속성 수집 ----
92
+ positions = np.asarray(mesh.vertices, dtype=np.float32)
93
+ normals = np.asarray(mesh.vertex_normals, dtype=np.float32)
94
+
95
+ # UV 좌표 추출 (여러 경로 시도)
96
+ texcoords = None
97
+
98
+ # 방법 1: mesh.visual.uv 확인
99
+ if hasattr(mesh.visual, "uv") and mesh.visual.uv is not None and len(mesh.visual.uv) > 0:
100
+ texcoords = np.asarray(mesh.visual.uv, dtype=np.float32)
101
+ print(f"UV loaded from mesh.visual.uv: shape={texcoords.shape}")
102
+
103
+ # 방법 2: TextureVisuals의 uv 속성 확인
104
+ elif hasattr(mesh.visual, "kind") and mesh.visual.kind == "texture":
105
+ if hasattr(mesh.visual, "uv") and mesh.visual.uv is not None:
106
+ texcoords = np.asarray(mesh.visual.uv, dtype=np.float32)
107
+ print(f"UV loaded from TextureVisuals: shape={texcoords.shape}")
108
+
109
+ # 방법 3: metadata에서 UV 확인
110
+ elif hasattr(mesh, "metadata") and mesh.metadata is not None:
111
+ if "uv" in mesh.metadata:
112
+ texcoords = np.asarray(mesh.metadata["uv"], dtype=np.float32)
113
+ print(f"UV loaded from metadata: shape={texcoords.shape}")
114
+
115
+ if texcoords is None or len(texcoords) == 0:
116
+ # 디버깅 정보 출력
117
+ print(f"Debug info:")
118
+ print(f" mesh.visual type: {type(mesh.visual)}")
119
+ print(f" hasattr(mesh.visual, 'uv'): {hasattr(mesh.visual, 'uv')}")
120
+ if hasattr(mesh.visual, 'uv'):
121
+ print(f" mesh.visual.uv: {mesh.visual.uv}")
122
+ print(f" mesh.vertices.shape: {mesh.vertices.shape}")
123
+ print(f" mesh.faces.shape: {mesh.faces.shape}")
124
+ raise ValueError(
125
+ "OBJ has no UVs but textures were provided. "
126
+ "The OBJ file may not have proper UV mapping or the UV data is not being loaded correctly. "
127
+ "Please check if the OBJ file has 'vt' (texture coordinates) entries."
128
+ )
129
+
130
+ # UV 좌표 정규화 및 플립
131
+ texcoords = texcoords - np.floor(texcoords) # wrap to [0, 1]
132
+ if flip_uv:
133
+ texcoords[:, 1] = 1.0 - texcoords[:, 1]
134
+
135
+ faces = np.asarray(mesh.faces, dtype=np.uint32)
136
+ indices = faces.reshape(-1)
137
+
138
+ # ---- 3) 단일 버퍼(bin_data) 구성 (기하 + 이미지) ----
139
+ bin_data = bytearray()
140
+ views = {} # name -> (offset, length, target)
141
+
142
+ def _append_bytes(name: str, payload: bytes, target: Optional[int]) -> int:
143
+ """payload를 4바이트 정렬로 추가하고 BufferView 인덱스를 반환하지 않고, 오프셋만 기록한다."""
144
+ nonlocal bin_data, views
145
+ off = _align4(len(bin_data))
146
+ if off > len(bin_data):
147
+ bin_data.extend(b"\x00" * (off - len(bin_data)))
148
+ bin_data.extend(payload)
149
+ views[name] = (off, len(payload), target)
150
+ return off
151
+
152
+ # 기하 데이터
153
+ _append_bytes("POSITION", _pack_f32(positions), BufferTarget.ARRAY_BUFFER.value)
154
+ _append_bytes("NORMAL", _pack_f32(normals), BufferTarget.ARRAY_BUFFER.value)
155
+ _append_bytes("TEXCOORD_0", _pack_f32(texcoords), BufferTarget.ARRAY_BUFFER.value)
156
+ idx_bytes, idx_ct = _pack_indices(indices)
157
+ _append_bytes("INDICES", idx_bytes, BufferTarget.ELEMENT_ARRAY_BUFFER.value)
158
+
159
+ # 이미지(알베도 / ORM)를 bufferView로 임베드
160
+ def _embed_image_to_buffer(name: str, path_or_image: Union[str, Image.Image]) -> Tuple[str, str]:
161
+ if isinstance(path_or_image, Image.Image):
162
+ # PIL Image인 경우
163
+ mime = _mime_from_image(path_or_image)
164
+ # Image를 바이트로 변환 (PNG 형식으로 저장)
165
+ img_buffer = io.BytesIO()
166
+ # format이 없거나 알 수 없는 경우 PNG로 저장
167
+ save_format = (path_or_image.format or "PNG").upper()
168
+ if save_format not in ["PNG", "JPEG", "WEBP"]:
169
+ save_format = "PNG"
170
+ mime = "image/png"
171
+ path_or_image.save(img_buffer, format=save_format)
172
+ img_bytes = img_buffer.getvalue()
173
+ else:
174
+ # 파일 경로인 경우
175
+ mime = _mime_from_path(path_or_image)
176
+ with open(path_or_image, "rb") as f:
177
+ img_bytes = f.read()
178
+ _append_bytes(name, img_bytes, None) # images는 target 없음
179
+ return name, mime
180
+
181
+ img_entries = [] # (key, mime)
182
+ base_key, base_mime = _embed_image_to_buffer("IMG_BASE", base_color_path)
183
+ img_entries.append((base_key, base_mime))
184
+ orm_key, orm_mime = (None, None)
185
+ if orm_path is not None:
186
+ orm_key, orm_mime = _embed_image_to_buffer("IMG_ORM", orm_path)
187
+ img_entries.append((orm_key, orm_mime))
188
+
189
+ # 마지막 정렬 패딩
190
+ final_len = _align4(len(bin_data))
191
+ if final_len > len(bin_data):
192
+ bin_data.extend(b"\x00" * (final_len - len(bin_data)))
193
+
194
+ # ---- 4) Buffer / BufferViews / Accessors ----
195
+ # Buffer는 하나, uri는 리소스 이름으로 설정(내보내기 시 GLB에 임베드됨)
196
+ buffer = Buffer(byteLength=final_len, uri="buffer0.bin")
197
+
198
+ # BufferView들(기하)
199
+ bviews = []
200
+ bv_index = {}
201
+ for key in ["POSITION", "NORMAL", "TEXCOORD_0", "INDICES"]:
202
+ off, ln, tgt = views[key]
203
+ bv_idx = len(bviews)
204
+ bviews.append(BufferView(buffer=0, byteOffset=off, byteLength=ln, target=tgt))
205
+ bv_index[key] = bv_idx
206
+
207
+ # BufferView들(이미지) — target 없음
208
+ image_view_indices = {}
209
+ for key, mime in img_entries:
210
+ off, ln, _ = views[key]
211
+ bv_idx = len(bviews)
212
+ bviews.append(BufferView(buffer=0, byteOffset=off, byteLength=ln))
213
+ image_view_indices[key] = (bv_idx, mime)
214
+
215
+ # Accessors
216
+ pos_min, pos_max = _mins_maxs(positions)
217
+ accessors = [
218
+ Accessor( # 0: POSITION
219
+ bufferView=bv_index["POSITION"], byteOffset=0,
220
+ componentType=ComponentType.FLOAT.value, count=positions.shape[0],
221
+ type=AccessorType.VEC3.value, min=pos_min, max=pos_max
222
+ ),
223
+ Accessor( # 1: NORMAL
224
+ bufferView=bv_index["NORMAL"], byteOffset=0,
225
+ componentType=ComponentType.FLOAT.value, count=normals.shape[0],
226
+ type=AccessorType.VEC3.value
227
+ ),
228
+ Accessor( # 2: TEXCOORD_0
229
+ bufferView=bv_index["TEXCOORD_0"], byteOffset=0,
230
+ componentType=ComponentType.FLOAT.value, count=texcoords.shape[0],
231
+ type=AccessorType.VEC2.value
232
+ ),
233
+ Accessor( # 3: INDICES
234
+ bufferView=bv_index["INDICES"], byteOffset=0,
235
+ componentType=idx_ct, count=indices.size,
236
+ type=AccessorType.SCALAR.value
237
+ ),
238
+ ]
239
+
240
+ # ---- 5) Images / Textures / Material (PBR) ----
241
+ # 이미지: bufferView 기반, uri 없음!
242
+ images = []
243
+ textures = []
244
+ samplers = [Sampler()]
245
+
246
+ # base color
247
+ base_view_idx, base_mime = image_view_indices["IMG_BASE"]
248
+ base_img_idx = len(images)
249
+ images.append(GLTFImage(bufferView=base_view_idx, mimeType=base_mime))
250
+ base_tex_idx = len(textures)
251
+ textures.append(Texture(source=base_img_idx, sampler=0))
252
+
253
+ # orm (선택)
254
+ orm_tex_idx = None
255
+ if orm_key is not None:
256
+ orm_view_idx, orm_mime = image_view_indices["IMG_ORM"]
257
+ orm_img_idx = len(images)
258
+ images.append(GLTFImage(bufferView=orm_view_idx, mimeType=orm_mime))
259
+ orm_tex_idx = len(textures)
260
+ textures.append(Texture(source=orm_img_idx, sampler=0))
261
+
262
+ # PBR 머티리얼
263
+ pbr = PBRMetallicRoughness(
264
+ baseColorTexture=TextureInfo(index=base_tex_idx),
265
+ baseColorFactor=[1.0, 1.0, 1.0, 1.0],
266
+ )
267
+ if orm_tex_idx is not None:
268
+ # glTF 표준: G=roughness, B=metallic
269
+ pbr.metallicRoughnessTexture = TextureInfo(index=orm_tex_idx)
270
+ pbr.metallicFactor = 1.0
271
+ pbr.roughnessFactor = 1.0
272
+ else:
273
+ pbr.metallicFactor = 0.0
274
+ pbr.roughnessFactor = 0.9
275
+
276
+ material = Material(name=material_name, pbrMetallicRoughness=pbr)
277
+
278
+ # ---- 6) Mesh / Node / Scene ----
279
+ primitive = Primitive(
280
+ attributes=Attributes(POSITION=0, NORMAL=1, TEXCOORD_0=2),
281
+ indices=3,
282
+ material=0,
283
+ mode=4 # TRIANGLES
284
+ )
285
+ gltf_mesh = Mesh(name=mesh_name, primitives=[primitive])
286
+ node = Node(name=node_name, mesh=0)
287
+ scene = Scene(nodes=[0])
288
+
289
+ # ---- 7) GLTF 모델 조립 및 GLB 내보내기 ----
290
+ model = GLTFModel(
291
+ asset=Asset(version="2.0", generator=generator),
292
+ scenes=[scene],
293
+ scene=0,
294
+ nodes=[node],
295
+ meshes=[gltf_mesh],
296
+ materials=[material],
297
+ buffers=[buffer],
298
+ bufferViews=bviews,
299
+ accessors=accessors,
300
+ images=images,
301
+ textures=textures,
302
+ samplers=samplers,
303
+ )
304
+
305
+ # 단일 리소스(버퍼)만 GLB에 임베드
306
+ resources = [FileResource("buffer0.bin", data=bytearray(bin_data))]
307
+ gltf = GLTF(model=model, resources=resources)
308
+ gltf.export(output_glb_path)
309
+
310
+ if __name__ == "__main__":
311
+ import argparse
312
+ parser = argparse.ArgumentParser(description="Convert OBJ to GLB")
313
+ parser.add_argument("input_obj", help="Path to the input OBJ file")
314
+ parser.add_argument("--base_color", required=True, help="Path to the base color texture (RGB)")
315
+ parser.add_argument("--orm", required=True, help="Path to the ORM texture (R:unused, G:roughness, B:metallic)")
316
+ parser.add_argument("--output_glb", required=False, help="Path to the output GLB file")
317
+ parser.add_argument("--flip_uv", action="store_true", help="Flip the V coordinate of UVs")
318
+ parser.add_argument("--center", action="store_true", help="Center the model")
319
+ parser.add_argument("--scale_to_unit", action="store_true", help="Scale to unit bounding radius")
320
+ args = parser.parse_args()
321
+
322
+ out = args.output_glb or os.path.splitext(args.input_obj)[0] + ".glb"
323
+ obj_to_pbr_glb(
324
+ obj_path_or_mesh=args.input_obj,
325
+ base_color_path=args.base_color,
326
+ orm_path=args.orm,
327
+ output_glb_path=out,
328
+ flip_uv=args.flip_uv,
329
+ center=args.center,
330
+ scale_to_unit=args.scale_to_unit,
331
+ )
332
+ print(f"Converted '{args.input_obj}' to '{out}' successfully.")
home/ubuntu/aaaaa/data/rgbmr/MCVAE_CONFIG_UPGRADE_SUMMARY.md ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCVAE Config System Upgrade - Implementation Summary
2
+
3
+ ## Overview
4
+ Successfully upgraded MCVAE's config management system to match MCDiff's base-variant inheritance structure. The new system is **fully backward compatible** while adding powerful new configuration management capabilities.
5
+
6
+ ## Changes Made
7
+
8
+ ### 1. Updated `train_mcvae.py`
9
+
10
+ #### Added Import
11
+ ```python
12
+ from mcgen.utils.config import load_config
13
+ ```
14
+
15
+ #### Removed Old Simple Config Loader
16
+ Removed the basic `load_config()` function that only supported simple OmegaConf merging:
17
+ ```python
18
+ # REMOVED:
19
+ def load_config(path: str, overrides: list[str] | None = None):
20
+ base = OmegaConf.load(path)
21
+ cli = OmegaConf.from_cli(overrides or [])
22
+ cfg = OmegaConf.merge(base, cli)
23
+ OmegaConf.resolve(cfg)
24
+ return cfg
25
+ ```
26
+
27
+ #### Updated `main()` Function
28
+ - Enhanced argument parsing with better documentation
29
+ - Now uses `mcgen.utils.config.load_config()` which supports base-variant inheritance
30
+ - Changed `override_tokens` to `cli_overrides` parameter name for consistency
31
+ - Added comprehensive docstring with usage examples
32
+
33
+ ### 2. Created Example Variant Config
34
+ Created `configs/mcvae/variant_example.yaml` demonstrating the inheritance pattern:
35
+ ```yaml
36
+ base: configs/mcvae/default.yaml
37
+
38
+ # Override specific parameters
39
+ project:
40
+ run_name: MCVAE_v1.1.0_variant_example
41
+ trainer:
42
+ max_steps: 50001
43
+ optim:
44
+ lr: 1.0e-4
45
+ data:
46
+ train:
47
+ batch_size: 6
48
+ ```
49
+
50
+ ### 3. Created Comprehensive Test Suite
51
+ Created `test_mcvae_config.py` that validates:
52
+ - ✅ Loading `default.yaml` directly (backward compatibility)
53
+ - ✅ Applying CLI overrides to default config
54
+ - ✅ Loading variant configs with base inheritance
55
+ - ✅ Combining variant inheritance with CLI overrides
56
+ - ✅ Verifying all original default.yaml parameters remain unchanged
57
+
58
+ ## Features Now Supported
59
+
60
+ ### 1. Base-Variant Inheritance
61
+ Variant configs can inherit from `default.yaml`:
62
+ ```yaml
63
+ base: configs/mcvae/default.yaml
64
+
65
+ # Only override what you need
66
+ trainer:
67
+ max_steps: 100000
68
+ ```
69
+
70
+ ### 2. CLI Overrides
71
+ Override any config parameter from command line:
72
+ ```bash
73
+ python train_mcvae.py trainer.max_steps=50000 optim.lr=1e-4
74
+ ```
75
+
76
+ ### 3. Priority System
77
+ Configuration values are merged with proper precedence:
78
+ **default.yaml < variant.yaml < CLI arguments**
79
+
80
+ ### 4. Full Backward Compatibility
81
+ Existing workflows continue to work exactly as before:
82
+ ```bash
83
+ # Still works identically
84
+ python train_mcvae.py --config configs/mcvae/default.yaml
85
+
86
+ # Also still works
87
+ python train_mcvae.py trainer.max_steps=50000
88
+ ```
89
+
90
+ ## Usage Examples
91
+
92
+ ### Example 1: Use Default Config
93
+ ```bash
94
+ python train_mcvae.py
95
+ # or explicitly:
96
+ python train_mcvae.py --config configs/mcvae/default.yaml
97
+ ```
98
+
99
+ ### Example 2: Quick Parameter Tweaks
100
+ ```bash
101
+ python train_mcvae.py trainer.max_steps=100000 optim.lr=1e-4 data.train.batch_size=4
102
+ ```
103
+
104
+ ### Example 3: Create and Use Variant Config
105
+ Create `configs/mcvae/my_experiment.yaml`:
106
+ ```yaml
107
+ base: configs/mcvae/default.yaml
108
+
109
+ project:
110
+ run_name: MCVAE_my_experiment
111
+
112
+ trainer:
113
+ max_steps: 200000
114
+
115
+ optim:
116
+ lr: 5e-5
117
+ ```
118
+
119
+ Run it:
120
+ ```bash
121
+ python train_mcvae.py --config configs/mcvae/my_experiment.yaml
122
+ ```
123
+
124
+ ### Example 4: Variant + CLI Override
125
+ ```bash
126
+ python train_mcvae.py --config configs/mcvae/my_experiment.yaml seed=999
127
+ ```
128
+
129
+ ## Validation Results
130
+
131
+ All tests passed successfully:
132
+
133
+ ### Test 1: Default Config Loading ✅
134
+ - All 23 key parameters match original `default.yaml`
135
+ - Seed: 43, max_steps: 400001, lr: 3e-05, batch_size: 8, etc.
136
+
137
+ ### Test 2: CLI Overrides ✅
138
+ - Successfully overrode: max_steps, lr, batch_size
139
+ - Non-overridden values remain from default
140
+
141
+ ### Test 3: Variant Inheritance ✅
142
+ - Inherited values: seed, offset_mode, etc.
143
+ - Overridden values: run_name, max_steps, lr, batch_size
144
+
145
+ ### Test 4: Priority Order ✅
146
+ - Confirmed: default < variant < CLI
147
+
148
+ ### Test 5: Backward Compatibility ✅
149
+ - Original default.yaml produces **identical** config as before
150
+ - Zero breaking changes
151
+
152
+ ## Files Modified
153
+
154
+ 1. **train_mcvae.py**: Updated config loading to use centralized system
155
+ 2. **configs/mcvae/variant_example.yaml**: New - example variant config
156
+ 3. **test_mcvae_config.py**: New - comprehensive test suite
157
+
158
+ ## Files Using Centralized Config System
159
+
160
+ Both training scripts now use the same config management:
161
+ - ✅ `train_mcdiff.py` - Already using `mcgen.utils.config.load_config`
162
+ - ✅ `train_mcvae.py` - Now using `mcgen.utils.config.load_config`
163
+
164
+ ## Verification Commands
165
+
166
+ ```bash
167
+ # Run comprehensive test suite
168
+ python test_mcvae_config.py
169
+
170
+ # Test default config loading
171
+ python train_mcvae.py --config configs/mcvae/default.yaml
172
+
173
+ # Test CLI overrides
174
+ python train_mcvae.py trainer.max_steps=1000
175
+
176
+ # Test variant inheritance
177
+ python train_mcvae.py --config configs/mcvae/variant_example.yaml
178
+ ```
179
+
180
+ ## Migration Guide for Existing Users
181
+
182
+ **Good news: No migration needed!**
183
+
184
+ Your existing commands will continue to work exactly as before:
185
+ - ✅ `python train_mcvae.py` - works
186
+ - ✅ `python train_mcvae.py --config configs/mcvae/default.yaml` - works
187
+ - ✅ `python train_mcvae.py trainer.max_steps=50000` - works
188
+
189
+ **New capabilities** are available whenever you want to use them:
190
+ - Create variant configs to organize experiments
191
+ - Mix variants with CLI overrides
192
+ - Enjoy better config management
193
+
194
+ ## Implementation Quality
195
+
196
+ ✅ **Zero Breaking Changes**: All existing functionality preserved
197
+ ✅ **Thoroughly Tested**: 5 comprehensive test scenarios
198
+ ✅ **Consistent with MCDiff**: Same config system across codebase
199
+ ✅ **Well Documented**: Inline docs + examples + this summary
200
+ ✅ **Production Ready**: No errors, all tests passing
201
+
202
+ ## Conclusion
203
+
204
+ The MCVAE config system has been successfully upgraded to match MCDiff's sophisticated base-variant inheritance pattern. The implementation is:
205
+
206
+ 1. **Backward Compatible**: Existing workflows unchanged
207
+ 2. **Feature Rich**: Supports inheritance, overrides, and proper precedence
208
+ 3. **Well Tested**: Comprehensive test suite validates all scenarios
209
+ 4. **Consistent**: Same system used by both training scripts
210
+ 5. **Production Ready**: Zero errors, all tests passing
211
+
212
+ Users can continue using MCVAE exactly as before, or take advantage of the new config management features whenever needed.
home/ubuntu/aaaaa/data/rgbmr/README.md ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCGen: RGBMR Dataset · Rendering · Evaluation
2
+
3
+ ## TL;DR (Quick Start)
4
+
5
+ ### 1) GLB 생성 – Run Mesh Texturing
6
+
7
+ ```bash
8
+ python inference_batch.py \
9
+ --glb_list glbs_eval_ref.txt \
10
+ --prompt_csv prompts_bs.csv \
11
+ --output_dir ./outputs_eval/ours_glbs \
12
+ --devices 0 1 2 3 4 5 6 7
13
+ ```
14
+
15
+ ### 2) 렌더링 – 무작위 envmap으로 GLB list 전체에 대해 실행
16
+
17
+ ```bash
18
+ python tools/render_eval_batch.py \
19
+ --glb_list glbs_eval_ours.txt \
20
+ --hdr_list /home/USER/data/envmaps/hdr_list.txt \
21
+ --output_dir ./outputs_eval/ours \
22
+ --devices 0 1 2 3 4 5 6 7
23
+ ```
24
+
25
+ ### 3) 평가 – reference에 대한 비교 지표 산출
26
+
27
+ ```bash
28
+ python tools/evaluate.py \
29
+ --ref_root outputs_eval/ref/ \
30
+ --method_root outputs_eval/ours/ \
31
+ --prompts_csv prompts_bs.csv \
32
+ --out_csv metrics_summary.csv
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 1. RGBMR Dataset
38
+
39
+ ### 1.1 스키마 개요
40
+
41
+ * **SCHEMA_RGBMR** 기반 `views`와 오브젝트 단위 필드로 구성된 WebDataset.
42
+ * 내부 후처리로 아래의 텐서들이 준비됩니다.
43
+
44
+ * `pixel_values` : ((V,5,H,W)) — **rgbRM** = `albedo`(RGB 3채널) + `orm`의 **[roughness, metallic]** 2채널 → 범위 ([-1,1])
45
+ * `cond_values` : ((V,6,H,W)) — **position (x,y,z)** + **normal (nx,ny,nz)** → 범위 ([-1,1])
46
+ * `normal_fullres` : 원본 해상도 노멀(([-1,1]))
47
+ * `depth` : Raw depth map ranging from 0.5 ~ 1.5. Background region has the value **NaN**.
48
+ * `pos_token` : Block-average downsampled position map for token position embedding.
49
+ * `w2c` : `c2w`의 역행렬
50
+ * 모든 키(`albedo`,`orm`,`pos`,`normal`)는 `size×size`로 리사이즈 후 ([-1,1]) 스케일링
51
+ * `prompt_embeds` : preprocessed prompt embeddings for SD3.5.
52
+ * `pooled_prompt_embeds` : preprocessed pooled prompt embeddings for SD3.5.
53
+
54
+
55
+ ### 1.2 `build_mcdiff_dataset(...)` 인자
56
+
57
+ | 인자 | 타입 | 기본값 | 설명 |
58
+ | -------------------- | --------- | ------- | ---------------------------------------------- |
59
+ | `dataset_path` | str | - | WebDataset Shard(s) 경로 또는 패턴 |
60
+ | `schema` | Dict | - | 반드시 `{"views": {...}}` 포함 |
61
+ | `num_views` | int | - | 샘플당 선택할 뷰 수 (`1 ≤ num_views ≤ meta.num_views`) |
62
+ | `size` | int | 1024 | 최종 해상도. **16의 배수 & ≤1024** 제약 |
63
+ | `shardshuffle` | int | 1000 | 샤드 셔플 윈도우 |
64
+ | `split_by_node` | bool | True | 분산에서 노드별 샤드 분할 |
65
+ | `split_by_worker` | bool | True | 워커별 샤드 분할 |
66
+ | `fixed_view_indices` | List[int] | None | validation 시 고정 뷰 인덱스. **길이 = `num_views`** |
67
+
68
+ ### 1.3 DataLoader 예시
69
+
70
+ ```python
71
+ from data.rgbmr_dataset import SCHEMA_RGBMR, build_mcdiff_dataset
72
+ from webdataset import WebLoader
73
+
74
+ train_dataset = build_mcdiff_dataset(
75
+ cfg.data.train.root,
76
+ SCHEMA_RGBMR,
77
+ cfg.model.num_views,
78
+ size=cfg.model.resolution,
79
+ ).shuffle(8).batched(cfg.data.train.batch_size, partial=False)
80
+
81
+ train_dataloader = WebLoader(
82
+ train_dataset,
83
+ batch_size=None,
84
+ num_workers=cfg.data.train.num_workers,
85
+ pin_memory=True,
86
+ persistent_workers=False,
87
+ )
88
+
89
+ val_dataset = build_mcdiff_dataset(
90
+ cfg.data.val.root,
91
+ SCHEMA_RGBMR,
92
+ cfg.model.num_views,
93
+ size=cfg.model.resolution,
94
+ split_by_node=False,
95
+ split_by_worker=False,
96
+ shardshuffle=False,
97
+ fixed_view_indices=[0, 14, 1, 15, 2, 3],
98
+ )
99
+
100
+ # 배치 사용 예
101
+ pixel_values_mv = batch["pixel_values"].to(device) # (B,V,5,H,W) in [-1,1]
102
+ cond_values_mv = batch["cond_values"].to(device) # (B,V,6,H,W) in [-1,1]
103
+ prompt_embeds_mv = batch["prompt_embeds"] # (B, T, D)
104
+ pooled_prompt_embeds_mv = batch["pooled_prompt_embeds"] # (B, Dp)
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 2. Rendering (for Eval)
110
+
111
+ `tools/render_eval_batch.py`는 다수의 GLB를 **Deterministic**한 방식으로 HDR과 매핑해 Blender로 렌더링을 수행합니다.
112
+
113
+ ### 2.1 인자 설명
114
+
115
+ | 인자 | 필수 | 설명 |
116
+ | -------------- | :-: | -------------------------------------------------- |
117
+ | `--glb_list` | ✓ | GLB 경로 목록(.txt, 라인당 1개) |
118
+ | `--hdr_list` | ✓ | HDR/HDRI 경로 목록(.txt) |
119
+ | `--output_dir` | ✓ | 결과 베이스 디렉토리 |
120
+ | `--devices` | ✓ | 사용할 CUDA 디바이스 인덱스들(e.g., `0 1 2`) |
121
+ | `--timeout` | | 각 작업 최대 실행 시간(초), 기본 `999999` |
122
+ | `--blender` | | Blender 실행 파일 경로. 기본은 `blender` 또는 `$BLENDER_EXEC` |
123
+ | `--script` | | 실행할 `render_eval.py` 경로 (기본: runner와 같은 폴더) |
124
+ | `--extra` | | 렌더 스크립트에 그대로 전달할 추가 인자 문자열 |
125
+
126
+ * **GLB→HDR 매핑**: 내부 해시를 사용해 각 GLB에 대해 일관된 HDR이 선택됩니다(재현성 확보).
127
+ * **멀티 GPU 큐잉**: 지정한 `--devices`를 라운드로빈으로 할당하여 병렬 실행.
128
+
129
+ ### 2.2 사용 예시
130
+
131
+ ```bash
132
+ python tools/render_eval_batch.py \
133
+ --glb_list glbs_eval_ours.txt \
134
+ --hdr_list /home/USER/data/envmaps/hdr_list.txt \
135
+ --output_dir ./outputs_eval/ours \
136
+ --devices 0 1 2 3 4 5 6 7
137
+ ```
138
+
139
+ > 환경 변수: `BLENDER_EXEC`를 설정하면 `--blender`를 생략할 수 있습니다.
140
+
141
+ ---
142
+
143
+ ## 3. Batched Inference (GLB Generation)
144
+
145
+ `inference_batch.py`는 GLB 스템 이름을 **프롬프트 CSV**와 매칭하여 `inference.py`를 병렬 실행합니다.
146
+
147
+ ### 3.1 인자 설명
148
+
149
+ | 인자 | 필수 | 설명 |
150
+ | --------------- | :-: | ----------------------------------------- |
151
+ | `--glb_list` | ✓ | GLB 경로 목록(.txt, 라인당 1개) — 스템으로 프롬프트 매칭 |
152
+ | `--prompt_csv` | ✓ | CSV (`gid,prompt` 컬럼) |
153
+ | `--output_dir` | ✓ | `inference.py`의 `--out`으로 전달될 베이스 출력 디렉토리 |
154
+ | `--devices` | ✓ | CUDA 디바이스 인덱스 리스트 |
155
+ | `--timeout` | | 작업 최대 시간(초) |
156
+ | `--python_exec` | | 파이썬 실행 경로. 기본: 현재 파이썬 또는 `$PYTHON_EXEC` |
157
+ | `--script` | | 실행할 `inference.py` 경로 (기본: runner와 같은 폴더) |
158
+ | `--extra` | | `inference.py`에 그대로 전달할 추가 인자 문자열 |
159
+
160
+ ### 3.2 사용 예시
161
+
162
+ ```bash
163
+ python inference_batch.py \
164
+ --glb_list ../glbs_eval_ref.txt \
165
+ --prompt_csv ../prompts_bs.csv \
166
+ --output_dir ./outputs_eval/ours_glbs \
167
+ --devices 2
168
+ ```
169
+
170
+ > 매칭 불가 스템은 스킵되며, 요약 박스에 누락 개수가 표시됩니다.
171
+
172
+ ---
173
+
174
+ ## 4. Evaluation
175
+
176
+ `tools/evaluate.py`는 동일한 뷰 설정으로 렌더된 **참조(ref)** vs **방법(ours)** 결과를 비교합니다.
177
+
178
+ ### 4.1 인자 설명
179
+
180
+ | 인자 | 필수 | 설명 |
181
+ | --------------- | :-: | ------------------------------------------ |
182
+ | `--ref_root` | ✓ | 참조 렌더 폴더 루트 |
183
+ | `--method_root` | ✓ | 방법 렌더 폴더 루트 |
184
+ | `--prompts_csv` | ✓ | `gid,prompt` 컬럼 보유 CSV (텍스트-이미지 CLIP 점수용) |
185
+ | `--out_csv` | | 요약 메트릭 CSV 파일명 (기본: `metrics_summary.csv`) |
186
+ | `--device` | | `auto`/`cpu`/`cuda` (기본: `auto`) |
187
+ | `--batch_size` | | 피처 추출 배치 사이즈 (기본: 64) |
188
+ | `--num_workers` | | DataLoader 워커 수 (기본: 8) |
189
+
190
+ ### 4.2 산출 지표
191
+
192
+ * **FID‑CLIP (shaded, albedo)**: CLIP 임베딩 공간에서의 분포 차이 지표
193
+ * **KID (shaded, albedo)**: 커널 기반 분포 차이 지표
194
+ * **CLIP text‑image (shaded, albedo; ours만)**: 프롬프트와 결과 간 유사도
195
+ * **RMSE (roughness, metallic)**: 채널별 픽셀 RMSE
196
+
197
+ ### 4.3 출력
198
+
199
+ * 콘솔 요약 + `--out_csv`에 최종 metric이 저장됨.
200
+
201
+ ---
202
+
203
+ ## 5. 입출력 파일 규약
204
+
205
+ * **`glb_list`**: GLB 경로를 한 줄에 하나씩 기록한 `.txt`
206
+ * **`hdr_list`**: HDR/HDRI 경로를 한 줄에 하나씩 기록한 `.txt`
207
+ * **`prompts_csv`**: 두 컬럼 `gid,prompt`를 보유. **GLB 스템명 = `gid`**로 매칭
home/ubuntu/aaaaa/data/rgbmr/complex_object_ids.json ADDED
@@ -0,0 +1,1678 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "002f9bf02cdf4ff695ebbf61190ed89f",
3
+ "00366db8bb464e5c8b26703c70a686bc",
4
+ "00452de0ad4342daa0e139ad70ed2232",
5
+ "005e6a893d984538a7caee0cd403e0c6",
6
+ "00795a3f52d84b5d90fc352f220e6f8e",
7
+ "008be41361a44dfebe0d2003523e8f85",
8
+ "00e1ec5c6754447cbf3ccbf0a6383506",
9
+ "00ff6f491b1c4a74a5f90deb51c9a686",
10
+ "014012ec8eb3434d87adaaecb6fc9592",
11
+ "014d21f3b726425298b1a7a70a92b5a4",
12
+ "014ebfab735341248431da3d6447bbb5",
13
+ "017b9b167c654f18b2fee876dc5d4d25",
14
+ "01893d6934224c90a021ca9f1b592c59",
15
+ "01b077eae2de4aadac1e8de7d1cf3ba9",
16
+ "01d685032bb7401a90a57d2c775d7cd9",
17
+ "01e93883acac478386f049e339d5ae54",
18
+ "01f637d4083443548b382840d61a68a7",
19
+ "02583ecd6d02499daf73a1ba1981014e",
20
+ "02614a69beb24b10a01d34aa27ee7f58",
21
+ "02684df750894322ad1dcdbb9a798135",
22
+ "02873313e26f4543be9650357ee75448",
23
+ "02a5748af09c491780997e4af41fe316",
24
+ "03038d3742d6460db54ec989c8752831",
25
+ "030b98bbffd6481ebc86f6133e42ebcd",
26
+ "0319cbd39597460ba079c4737f856339",
27
+ "031debe8efad46dd9ba362604707ebb1",
28
+ "032ddebdff8b40e0bd2a51495753493a",
29
+ "03685bda9cb843d88f550a1f7f393184",
30
+ "037b2fde967243d883a0ee54b9aa4972",
31
+ "038348f33b27488e9b6ed0efe8415647",
32
+ "0397ff8b6a3148028f6db1e9e77925a8",
33
+ "03f33486821448e7961b111285525503",
34
+ "03f341ca580b47fb89fdc828a3248cd6",
35
+ "043353aec9b7403a8d2be7e075315528",
36
+ "043dbd8f74144e29bcd45ccfd59592b1",
37
+ "04629445e5b14f93bf18dcd211fa3d78",
38
+ "04ca24df177a4d14985a2e23b78def83",
39
+ "04db7c9a9ee9485b82999083b957b1c9",
40
+ "04e2f89dff504a1db0d62c2fc0788d6e",
41
+ "04f1cb7bb04d41e2bb7e291bf92f0744",
42
+ "04f51877ba25493d94d05f6086492f37",
43
+ "0534bc651ac84d0a912666060b8c0db1",
44
+ "0579a70d5a1b4e0fac473bb6861eeac5",
45
+ "059a7936ed89419ba9eae3153753ae86",
46
+ "05a396517a184002a943d309f4a2a317",
47
+ "05ee6c82045a4e3d9144d4cf56215f1f",
48
+ "05fc2a534c524c27ba7cfd74508ac482",
49
+ "0641654877a44c70b3ee5a44e163746b",
50
+ "0642122de7a04c8b8a694ce3735f8a2d",
51
+ "0657a328adde41ef978fa181c09955f5",
52
+ "0680fe1bd60343279824f50ba4c6f57f",
53
+ "068969f41a3245e8a4657e86db500e61",
54
+ "069b86d4b4a2492e92eb3349805479f9",
55
+ "06d400aa4d2343e490d07c23ff661e4c",
56
+ "06d5a80a04fc4c5ab552759e9a97d91a",
57
+ "06f867be028f44ed91f39471108c76de",
58
+ "0702225ca7e449c9ba85f3a142c65bee",
59
+ "07121d29537c4c70bccd4d02db6a6466",
60
+ "072d99792e534c22a163c20a917da4f9",
61
+ "076a510d663d41c380d8937581b68249",
62
+ "077c9dff3d824c8caf0e98df24debf57",
63
+ "07ff66a33eff4a0a93252317708d9cff",
64
+ "081f2783a8e44b559d37c08f7a402843",
65
+ "0846167f9302450cba1926c6fc5d6d42",
66
+ "084c7bc72c934da09d5aa2e00543a6b3",
67
+ "0853e9a4988b4953aae535dd23440e60",
68
+ "087d5869e9c34ac380a96ac0f892e05a",
69
+ "089327b8a40c48ec9d5b5946caf528df",
70
+ "089aa02943704e1aba9c953a89611d2c",
71
+ "08d237ffee3d4898b000ae0878a15a5a",
72
+ "08e02f5b80c34cb9b09c0a6a48deba9d",
73
+ "08eabca8888a41609cbc0043048dbd80",
74
+ "09146af7b959498482a73e717747ccd0",
75
+ "09415b04c271474a9757c53a7e27c414",
76
+ "0972ddcebed24084b47bab5eee38748c",
77
+ "09740836b5f04e8397a3b826ced11ab0",
78
+ "097fceeb1d85448882f5f9e385dbb144",
79
+ "09862077f68b4cb3a0c9c2ef82e359f5",
80
+ "09da8c6710654aa1b1ec8d6fb785b9b9",
81
+ "09fb79ac0c7c43ae9b1a1bd461619d9f",
82
+ "0a3e122170504d1dbddbde0a49f549e7",
83
+ "0a4c078d52e64241ad4da67e2f5ae3a0",
84
+ "0a4f3c74e1ee4ed5b9a5e36c7374aa37",
85
+ "0a5a267ffc814be49592cab5bdcf5d92",
86
+ "0a8293cd82b141d09b480a3f80c17b7e",
87
+ "0aa08cfba5bc422198828d1b720c53fb",
88
+ "0ad8b6a0fe5b40328bea1581000b9c9a",
89
+ "0ae52abcca054a57b58a0e9265b89b17",
90
+ "0af3d81c117e4e8aa84757b8f0df2ea8",
91
+ "0b04cb995fcc4c8cac5bfc8fc1aa4f3f",
92
+ "0b1e10b981194005b9fd842e45665fbd",
93
+ "0b73b0c1550746f38e4018f53e7bb56f",
94
+ "0b90478f3e1a4a1ba99fe0bfad77b96d",
95
+ "0b98efe575c446beb105a8dfd321f3e9",
96
+ "0ba8079c88fe44a49b84c051359a64e8",
97
+ "0bb43ca370104656959b91a27acfb6ab",
98
+ "0bc8be3751fd4af6b9171273a82592fc",
99
+ "0be09d151cfe4db5a6812306e9234ff3",
100
+ "0be84630c8e84508926c5922c541e1ce",
101
+ "0c1f1e1bc488444c8c5931d8c51ffedc",
102
+ "0c2d347c89b44323bc989ca85b6334a4",
103
+ "0c34e7efe79f4672ae5472fbe89e466d",
104
+ "0c9286b463384164877261b6ad6cd48c",
105
+ "0d322eeac13543c3b02ddf5a9dc5d66d",
106
+ "0d63a37b476a4338aa82084ba8d5e82e",
107
+ "0db114d7753344d6825aa4f21ec56db9",
108
+ "0ddefb81adf94e44a00ec5fa2db02630",
109
+ "0de0219a9d114947ae5626bd8a2abc75",
110
+ "0df3d7b24cba42ebbe2b6d542df0b2eb",
111
+ "0dfa5395abcb446c8779c1e369fc3727",
112
+ "0e09d9cd2efb473394e8977c57fba0f3",
113
+ "0e4cd5dff04143dda558b7b52d61f616",
114
+ "0e57497e42d74c76b5bd05021250dac0",
115
+ "0e5cd116d92c4100951056487260e026",
116
+ "0e80fb1e983943e78c64018efa7d8837",
117
+ "0eb317b09e5c44beb0e4681b06c15ae6",
118
+ "0eecf555488e4adfb57cbb0afe7836d6",
119
+ "0f03d940e6184871a0f38d429bfc006f",
120
+ "0f1a699145f441299d09ac3c0cf20e12",
121
+ "0f64e9fd042e4036a806cb1a4beb3690",
122
+ "0f88101d09d1408bb882fce581d9ec0c",
123
+ "0f9ebd68d56b443bae52cfebdbed0f24",
124
+ "0fdcb7df01d34aeea01ed1758d629f34",
125
+ "0feb0ffcfa9a47ba84ea7d3ac0ce5d70",
126
+ "1002c84db1ca45dc9d77c9e8e12a6437",
127
+ "1019140d9e354b489eaaf32895f8a7a3",
128
+ "1034844d4d564d1d9127b920c0f182da",
129
+ "1059e80a5f414388b2f240313acc5533",
130
+ "108635ed9e1c48b7a94f77e2405c4608",
131
+ "109ee24f61fe4c9bac7e4853dacca608",
132
+ "109f4aa661a24ffcb44758de9fdbff20",
133
+ "10b46ec4c10944faba5c00aa38a8c79c",
134
+ "10bff3bb4b474689968752ef9e2c0647",
135
+ "10d03a06c3474517b94a8d09f760aa41",
136
+ "10e1cceb4f1d4ab8bdb0c768afafad2c",
137
+ "11884dc5030840efa444dcfcc446389c",
138
+ "118ad6a688f24b75aa1751bbd05dd89d",
139
+ "119d5abe285545559f2a8b25760f7354",
140
+ "11aa844a77384a7cac91f1e7d0e39544",
141
+ "11aab5e450e640f5834da7c968b8d31c",
142
+ "11cc190943df4e69bf3b59c49defe013",
143
+ "11cdd06d68de4e42a1acf23093efddad",
144
+ "11ff1777e9254a5f8f510ff56108247c",
145
+ "122f100ebdec482eb5c81d01156246e1",
146
+ "1234ef64b5bd42be8f0e32725144790b",
147
+ "124ad78379bd4d259d9a253fb1a977a7",
148
+ "125c54f0b85746d496291418ab5607c5",
149
+ "12846c4c93d048feb07e96fb65f82ad1",
150
+ "12b5e7216c28431da88b63502faa7c6b",
151
+ "12d2ded2734a49aeabec371415409a18",
152
+ "12d76407cbd047d2a23a86c73e556c9e",
153
+ "12f2ba3da13c47f0a4a0e6c1521cf01a",
154
+ "13289bcc70ee4bd8a2f573b642aa1133",
155
+ "136494ed0f6c410caf53530bbe14ca64",
156
+ "138125d59ade4d328cc14cea5ff6f143",
157
+ "13987fff4df94d298c69027b98000516",
158
+ "13a1a1f76ec741e7abf4b4483ad36605",
159
+ "13aaa9a0462249f199ac808682fd57e8",
160
+ "13caef3c8cd344d6b696222deafa8515",
161
+ "14142eb541474335945c06a3630cafe1",
162
+ "141c59f3250c430d9e40c67887ba39bc",
163
+ "143f0bc1ebef48a4906597acbea6ea71",
164
+ "144051ab17b948359dc413c5a74968b6",
165
+ "147255b123c4419cb745c9a688f5a460",
166
+ "147a9dd1ec4d49e18ea793368ce4404b",
167
+ "14800755f6df45bcb07d71bee1464ab2",
168
+ "14c7c5d33a264a9c8de921aa77487941",
169
+ "1502810bac424f1fa91e3512dff3c59e",
170
+ "151f9f9b09a7455e9368d1cca370f545",
171
+ "15d86ea3b13444229dc8a9d3ca612369",
172
+ "15dfa9a9920d4508a5011a2d34a7ee9e",
173
+ "15edc8bf5aeb4185b6c0fb3753520c88",
174
+ "15fda9e154434cbc8a66bee0ccf71c65",
175
+ "1605c3b534de4de496c25012f8425dc2",
176
+ "162270d1e1f54c26b60cb691f5951f22",
177
+ "1633021c9ec94986b8f84ea703e1c979",
178
+ "16583a12813d41f1afe021389c6e8b7e",
179
+ "167151b4ff1842959d86ce589619b21f",
180
+ "16a421af043c438ca4cca51eb6ebe220",
181
+ "16ba45d18fd2443db23c858d3819952e",
182
+ "16d9b596dd384dbea7b88e5b3145b8f6",
183
+ "16e87b9d1c1c416db183baeb5aae8a50",
184
+ "16fa6474f4e6432fbfd33af10134d31f",
185
+ "1706b681036f41eab7156a461679420b",
186
+ "172615267260434f99da009ac53c22a0",
187
+ "17529cf13c1c4f7dac42b3a84e7915dc",
188
+ "176f8676f5214fdbbd2a5f780d062ff5",
189
+ "17f0b9d773fc4e968b162069fa5232d5",
190
+ "1852b23b73a94d4da46992c54e629da3",
191
+ "18c081f765854d249bb8dc580a1e9f7c",
192
+ "18d603a1f4b149d1b232563eba466a8a",
193
+ "18e60c14f46a4c3c9b361b80bf02beb2",
194
+ "18f67de6592a4cd4b8c31ff30cf4eaab",
195
+ "18f85fec08084a5d82a97347dc2b10be",
196
+ "193036f84cf34d489d49d0b8275e589e",
197
+ "193a7d0218b8400a9a7de60cbeae26df",
198
+ "1969eafcb62049e4b513aee276987ac7",
199
+ "19c02044ccf444038ac784dc88709a01",
200
+ "19c1558f1bbb403b943f338a9a70da89",
201
+ "19e4f6e71dba45ac8a3f027f33c4c3f1",
202
+ "1a0e4275e04442679edc4a8f09fff71f",
203
+ "1a35afde483f49f69a4a3c1da2df63e3",
204
+ "1a4a3013741e4fa5a70f5548958c21f0",
205
+ "1a5f0ad5860742c7be1da6757359230c",
206
+ "1a735bd0fcfc4e87ae3b89a7cdcecf4a",
207
+ "1a7aa42ca78c4e10ae98c1e77b0087be",
208
+ "1a7e9fe5bc284ea8ae54bbdb0fa8e87c",
209
+ "1ac4581e09b04ea6968a50e404a781ce",
210
+ "1ae3b14b5d5c4d0fb21abbf94655158b",
211
+ "1b03759aca22488a801662ed266068df",
212
+ "1b24bd7f2ca84cc4b9713065c211c5be",
213
+ "1b347387cd8741b5846534742ebc476b",
214
+ "1b3c4bcf091840b986c88c2a1740739a",
215
+ "1b5443261efb4c99be19ec82e3fcbecb",
216
+ "1b5ae52c77524c57b15f4a4c6a546087",
217
+ "1b834999835249228d4afb25cbc9b9ef",
218
+ "1b996dd59072468da9fcad2c8f0a8ef5",
219
+ "1b9c5e3b31fb4613b7be4ba640d70f0c",
220
+ "1be961388f3f4a858f9413232656c957",
221
+ "1beba9b24a9143df8559410f1522aad6",
222
+ "1c45284044f545d0888036ad34ac3657",
223
+ "1c670c8f241043d793dae59e1af693e1",
224
+ "1ca4dfff5736410fa3de378891b04eae",
225
+ "1cc0466a1cf14992b3329b13676e1295",
226
+ "1cc3e5baf8b54a358fc41062f74d97bd",
227
+ "1d0819976773469183e100765aa17244",
228
+ "1d0ceb703f704cd4ad3d0bf8e35f6b0d",
229
+ "1d133a74071c40bb9a49eb2bdb612827",
230
+ "1d13dbcab1224d54adef7dcf760ede5f",
231
+ "1d26bb676f044b6f81488ff7b43fd408",
232
+ "1d7537713c3c444fada9c650b84116c1",
233
+ "1db0bf90fad9413f9c70bd9afb8dd252",
234
+ "1dceb198db1d4c7db0777b85a57454ab",
235
+ "1ded9f27d0ae46e9a7f4c871eef7ed8e",
236
+ "1dfdb8070a8a42e1aab9fd38fa5630ac",
237
+ "1e025ab1bf234858ba48db14a54f26fc",
238
+ "1e61211efb1c49939ba6e45022e42873",
239
+ "1e704ff821fb4df5acb42117855e0370",
240
+ "1f2a9fc9bc0043328a40064dbe3fcd28",
241
+ "1f2ead206be24675bfbc63c3aec9aab7",
242
+ "1f5516c73928421cbc0ba1d7e1ff24d6",
243
+ "1f75786d2bf047a38f3971d7758aa990",
244
+ "1f8aabdf44424d72818bfcdc600bcad7",
245
+ "200ebda19ef44aeba975cf35e8d78cd6",
246
+ "204f47380bfc4d83901e19112c116bb9",
247
+ "2051d1d1a15b4d03bf3424fdd7717858",
248
+ "205a9efa37a54e8d8c3a1248de4c804a",
249
+ "20703acffbba47ce8b3755faa966ae22",
250
+ "2099b1f909a345d0be4ec934186ba866",
251
+ "20b77794b58d4f91b257676482fdf28d",
252
+ "20dd7f7bdc9a4c36aef491f12afa14d8",
253
+ "20ed29af326943a690ed71cee3efb5e5",
254
+ "20f9af9b8a404d5cb022ac6fe87f21f5",
255
+ "212980eff27d4d45abb2d72421a7ed9e",
256
+ "2144e2292abd45a58b75332a920fde98",
257
+ "2145f8891d0446b2ac4c25c285ff338d",
258
+ "215641b9f8e04a5585be12f050c29e45",
259
+ "216abeb437454c53bb556148bec71404",
260
+ "21705607294b4c1691e3770757f969fb",
261
+ "2177902e39d24548b3b1bbeb290a13ca",
262
+ "21851bfa7ee94bd49ae8161874e0b669",
263
+ "21ec37b286474fedb43307f6f289269e",
264
+ "22071b002d96499396a447b9235cec79",
265
+ "2221a5081f6d4ef9a1977d8d09d72941",
266
+ "22ac998056374bc3bd82d78b921debbd",
267
+ "22eb777ecde1455c9accc98b05fe1140",
268
+ "22f3cadbccad4ac68cec0a22a95a6d9b",
269
+ "232a246915e34a0cb7fd0485c5344401",
270
+ "2330d6dddc0d4a2b9fa9a644d249b766",
271
+ "2333799431754d42aab6712986a312c7",
272
+ "23466a346e9b4755a392d9cf69606a07",
273
+ "23b89235f11247e5a1dc1f55ba69f09c",
274
+ "23eb94cac33d4099b7185a81bdc038e4",
275
+ "23ede16550d84e81a4c255183510825f",
276
+ "23fa151346304c8bb8c58f58a76e6407",
277
+ "240d0bb449a949ca8b64cf1e776065c0",
278
+ "244b511ee2ca4f3794d118b872ee229c",
279
+ "254815aada9e445e927fea6a25a6d22e",
280
+ "254eb565be1b490bb64ef97e1274057f",
281
+ "2566ee4337b2410081cedc901b932618",
282
+ "256f723c10be45c0a2b662b3ca656026",
283
+ "2597f49cb567419f838c7df39c45d5ce",
284
+ "25ea2f17700e4f1ab9dc38b3fcaaba0b",
285
+ "26349afbeb094d1684e90884d98e38d9",
286
+ "264d5fe5038c408b87a7d2e62eaf0e35",
287
+ "266271bf84724e91ba76807e9d84e204",
288
+ "26847febe5af4c8e8615e1052ff69fac",
289
+ "269997245d764743a648d3ee4208e4a0",
290
+ "26f48e21a7f7487cada9cd59d840a9c4",
291
+ "2738b65016284ea3bdf041ea9135a121",
292
+ "273b53c4bd464b4daaba1fa94a9b8722",
293
+ "27c6ac2f6db648618186d4c00f3acfff",
294
+ "27da060494084e12a41403eec5a96498",
295
+ "27e541911b994ce0a0b546f0a861145e",
296
+ "27f6af85b7e9485da35e1bdbb5da680e",
297
+ "280a65bca20b425e96617b0dd78cce21",
298
+ "2819271389e1451eab2ef7e1333b45de",
299
+ "28215c6ceef6454cacebb92b5f2d4784",
300
+ "2834f838915b42f89f7dc299e6c39a0e",
301
+ "285420b2b0474a1199030d820f641d66",
302
+ "2871ff97b5d5478f9aa80a431a0049f1",
303
+ "2884eeef3d324910b50d10061dd884ae",
304
+ "28b76708813b402eaa71877738954df5",
305
+ "28dff4ee44a6444f98243982b6363b6a",
306
+ "28e7eedb0c4849ff8b0d744a74e9e424",
307
+ "28ec724bf8c141848b7910fa7ad25a89",
308
+ "290a75f5b21f4fc88c2a10849b4eb21d",
309
+ "292d33c6f0bb40c2b229cf8ba480f7b5",
310
+ "29360182779942f39104f227a161b987",
311
+ "293a20a6a9284de9a1c4d40092dd7bcf",
312
+ "293fc6beecd44a8aaf20df205822e742",
313
+ "2967048d83614382a43e1e9817b89573",
314
+ "296a53dcaca64681819a43b8a7ff7db9",
315
+ "29f78283c38d487aa8f34497d1233015",
316
+ "29fac935b6984498977832bcce0b1ffe",
317
+ "2a2927434cd14316a445044b69c6cf3e",
318
+ "2a6667ef214b4596b5dc407f4b073fdb",
319
+ "2a86f45c248d4b06a8ef86213916e089",
320
+ "2a8d375f2aef42259c6b6814a3ab72a6",
321
+ "2ac7e46b2ee6451e877c8f2a7d203859",
322
+ "2acda3ed2f9046129897f0f1e36092bd",
323
+ "2ad22ad416e74cc0a2a99567fd9c8f8e",
324
+ "2b1eab63ef4a48ad8e62be8d6b42d83b",
325
+ "2b273f7abb484ed29ace055984eb26b9",
326
+ "2b32f215f41d447c9fc1cbc75b64cd51",
327
+ "2b3e6f8e37344325b98a6165030f3340",
328
+ "2b4a5d2057824f44bf35056f531f8b69",
329
+ "2b4c4ce5ffef4646adf1c6b78d29b47a",
330
+ "2b4d6590fa6a4226a1366e2f636df7f6",
331
+ "2b9de21987c6419ebe72df19e9c4163a",
332
+ "2bb06b2f975545e0bf74b0be3c522d6e",
333
+ "2bd82aa8ca92482882241a3762684509",
334
+ "2ca8d4fbb25d49cb9da094549ed4ffaa",
335
+ "2cd2108f386f4b19aee80c782c3ee20d",
336
+ "2d1d858c4bca475b9eb5d6d95f7b6ea8",
337
+ "2d4f0a9502a346bba2718b7c43dd43a6",
338
+ "2d69c8441654486c872e7ef70ee913c6",
339
+ "2d76588c822942259b579e15640d7511",
340
+ "2d8b169f416f43df84fb679420e0a148",
341
+ "2da2098bab544c688fd106ed05ead14c",
342
+ "2dbd978c84da4270aa19091ed599007e",
343
+ "2dd385cde1c142deb8e3ae4b12e85dd1",
344
+ "2e09121c47cb493bb9fe754ee92653f9",
345
+ "2e0c2c37a55f48bba314c894ca5e5f0b",
346
+ "2e1fcfe8fd3b4ae4b41f79dfb2fcfc4b",
347
+ "2e283153e26146fc876895570d1ef74f",
348
+ "2e2bb2aa721748e785adbb86c98b7a23",
349
+ "2e352e9c26cd4109bb67132c40df8146",
350
+ "2e3fd6aa4e6b424391c1a13dbdffbfdc",
351
+ "2ea97968269c45e3abe17f84e625e609",
352
+ "2ed70b1329684b56a83d0490660cc066",
353
+ "2f09bba881d34aa1885f3b0476d94e61",
354
+ "2f1706233a3248cf9a74586fc2e7120c",
355
+ "2f1f291be93b4e09898a17f99e99d5d4",
356
+ "2f3b88cc0211484992b393a381a97fb4",
357
+ "2f6ab901c5a84ed6bbdf85a67b22a2ee",
358
+ "2f8b1430ff634501aa3a19717bed0319",
359
+ "2faa80cfd86a4af785c25ef632dc4309",
360
+ "2fb0edd32d1f4b1dac176f5d8352725d",
361
+ "2fbb8cea1ee740008ae5882e48fed8bb",
362
+ "2fcd9c9f248a46e5a2ea5b27379dcdf3",
363
+ "2fe47279d65d429f8101f97e7c89de7f",
364
+ "2ff892fcc6334a3d97d5bbf023b149a1",
365
+ "30017d8129674ed0b5d38dedf3f5686f",
366
+ "3030f6abd4854140ac3c278c3cdba25b",
367
+ "3086fd6d56e54573bdb00844ebca3c77",
368
+ "30cf4a33eaca4e53804f5abc0b8d00a7",
369
+ "30e22b51951945168006423813c26885",
370
+ "30fa4d77375349a49fcd60c4b2039a3c",
371
+ "31e0899e65f74cb9bfdc9df8e71288ee",
372
+ "31fd6b50eb5845d7b0e47d71639a07fe",
373
+ "3218791489fd47c59a33ee6ca38f0c72",
374
+ "32285d0de78b43b18defffcb3f647f37",
375
+ "324e3febdbc74b29a506760d6edb85dd",
376
+ "3269b8399110482cbf6bab889e70da25",
377
+ "3285d5f705604a74835c2b51df373d04",
378
+ "328840dc30c846d6833b57e7a33a41f0",
379
+ "328eb54341cc4b3f9c7a07fa1cb71a02",
380
+ "32c521425f124f24b86c64924d7fddd8",
381
+ "32f46385978241eaaae7ea692367d4e0",
382
+ "32fd9fd8240a4c49a1ed45f9d9c1e828",
383
+ "330b94a411bb465698c5eaf984c46bf1",
384
+ "331c237cc2044d87977cd3d3e9deabbd",
385
+ "3356fd1647c7403f90de64805545c77e",
386
+ "3366c19e79694be0896d569459dde186",
387
+ "3426ec60832b4f7c867ba29dc6d0e808",
388
+ "345fe9f4ebdd47b587f03cd3b81cd9db",
389
+ "3484a4e0862b4205aaeb1ef2c62e483b",
390
+ "349d730859c948b88f172fc5158e32ce",
391
+ "349e97902e844189b17cd86e01d5e2f8",
392
+ "34a383c7dab64f2a9267bd19acd0e8a0",
393
+ "34c2c5615fcc47b0a34de9fc9f77db8c",
394
+ "34ece7dccc4a42cd995a099f3c69e9ba",
395
+ "34eebb66d54b467888d446206bfe6ddf",
396
+ "35002d28d3fe4359be492bd444569744",
397
+ "351f5d4bfb0f416b8cffeb957f88d4c4",
398
+ "3529e97a616241f480a0115d74f97e80",
399
+ "356d3e62399b466fb6578fb1302bffa9",
400
+ "356d6ae5584f40aab1aced6bd6571a74",
401
+ "358be2d78ce8457b8fd37187d9f70976",
402
+ "3598d4508c8c4568a35e2ed00e4cd193",
403
+ "35bd8acad6f64c7eb7a9ff026868410e",
404
+ "3630b9ef86124610932a328aaf2ab023",
405
+ "36342df2dae44dd4894ad4658720c2a4",
406
+ "366d20b241a5446b85e0cd6fc399afaa",
407
+ "3687a268a444492bae2c81d83efde675",
408
+ "36d5b9c904ee4eea9a55ab0eccf7dee8",
409
+ "36d90cc8de3847379c8a636f548ffacf",
410
+ "36e269a8f74143a3b567f582781acaa2",
411
+ "36f06f5bc599499aa7a127c68668edc4",
412
+ "36f768d11a6342b7b36a121e014f521e",
413
+ "37071678dc284350b5268796adffb6d2",
414
+ "3708b0bedf8d47efafe22bea1c00a951",
415
+ "372200b925ef4dc69cf3364daadf786f",
416
+ "3724de1e56ba4f4bb0be2ca3170e39f2",
417
+ "374ed6172d2c405fabaef72b2b644149",
418
+ "3750fba31c2f445a864584cea5e2d78b",
419
+ "376b733d888c41d9ad3ac39eec4498d6",
420
+ "378988e0236c444daea672330e2686df",
421
+ "37b041d8521c4179b3c8679e2ff8dd17",
422
+ "383e4b856aed493a9ef70365cc3685de",
423
+ "387cd4307d1c472493254a659325fbd4",
424
+ "3886c1ca9a0942e19ef288493e1643cf",
425
+ "3893793770594f83a059317f587af8b8",
426
+ "38d1079a273f46349203fa2c3db35d29",
427
+ "38f04a4243f64e808fc7a6866c08dfcc",
428
+ "38f24c1eb8ae44d5bf2a4f9b940c96ec",
429
+ "392a59d222a647828cf3ee4c505edc38",
430
+ "394d1e8122554e00876789fafa68a03a",
431
+ "3953a48277d8460990162d60123c57dc",
432
+ "3959cdc6c8ca475eaa04f9a8d2dbf055",
433
+ "39a5dc35a05d4ac59f22c7d9ea050299",
434
+ "39a61e9a65994ebda5806039ef1afac6",
435
+ "39d799f20f4041228704749c854b60af",
436
+ "39db3486652b45548ab4dbb63a86911e",
437
+ "3a0d055986d74a9598a7a54c3fd05b9d",
438
+ "3a135daf87db490d9d9afb4b19a5a9d7",
439
+ "3a2954f4e11842f69482d1d6ff4eaf23",
440
+ "3a3afdfd1d2b41f68ed7d98c90b69d5e",
441
+ "3a4df884aff945eaa44e31d1b3a16f7f",
442
+ "3a5572342c5c4bdabaf0ac27b6577cb7",
443
+ "3a6019602c234c13a376c55a6c52c0b8",
444
+ "3a7fb4375e8941caae37767f0e30cf45",
445
+ "3a7ff8c34baa4caaa6098ee8ad447679",
446
+ "3a9a470a1d324a49bd362c580d046721",
447
+ "3af2417fa730468b81bcfcac715469b8",
448
+ "3af37c2c89d14ccdbc99563f969ce7c5",
449
+ "3af6861d9c604482a3a4354731c52c1a",
450
+ "3b1393cb24a74ab9a4a25003ea26ca6b",
451
+ "3b44c656b90341108cf27d3dbb88c4dc",
452
+ "3b6d35b6b4f44cb3832a8c0b7e124748",
453
+ "3b7d058289cc433da8338acccb4a59e0",
454
+ "3b84b153520c40048a1f2fa3fda8b866",
455
+ "3bae6520516e4d6d9f67c51837855144",
456
+ "3be2b07c8dab46dca538d21dae5b678e",
457
+ "3beec67e1ee84ed4b8d9e454d682e1f7",
458
+ "3bf8f2214b7043d3bdc543b8136d8e17",
459
+ "3c0cec271575485c83518d710384d084",
460
+ "3c1bd2de915941118890332de06d5df2",
461
+ "3c3ffeccb5be42ed8280d1e65ac2d200",
462
+ "3c53f38f89434099b973c2016e7d6535",
463
+ "3c7d73032bbf4b9eafb58bb943c2d859",
464
+ "3c8ecf379cde41bc8203945e00cf4487",
465
+ "3c8fabdb9e4c4bcd832e2c0b6913a17f",
466
+ "3cc81831328d405880c3fb5df62755a2",
467
+ "3cf7343ec2714fefaa21d4a9005e2f28",
468
+ "3d1429899c464501ad23cbf13449fde2",
469
+ "3d49477c17e9405683601bf9107b7513",
470
+ "3d5487df45fe47cca585e5c786abfde3",
471
+ "3d64b354413e48fca9fef934f25a882f",
472
+ "3d6f9421a9b54fe18b8cf894b17b3bff",
473
+ "3dc5d502413c41ac97eaa871deb32e9b",
474
+ "3de0ba7fb9ac41d6b438501487d2bd83",
475
+ "3e18491fe92c47e8bfae9f4064699e1c",
476
+ "3e3b24b28cde4650b71dbca053056995",
477
+ "3e83a01ee91b4e029f634b54439dc78b",
478
+ "3e8c37f3d701413897c6333446a76a6b",
479
+ "3e9b9f7841f54577829c0a9167b167d7",
480
+ "3f46bca9feaf43338e8956d227163738",
481
+ "3f49492bbed14d89b43cdc3efd5993c2",
482
+ "3f72a9830c2249e5ab397f30de1af84a",
483
+ "3f741c7f7d94427aa413683b71f2d31f",
484
+ "3fc51a48f58f42d28f325db7688be17c",
485
+ "3fda15638cd84814bda4f97327984e42",
486
+ "4047c9ac5e224f738bd67f6a409975bb",
487
+ "405351166c1d4f5d88162d941d44cd60",
488
+ "407bb4001ac746039d654f1332a3e40e",
489
+ "40b27a1c061242a89f8b313c7905533c",
490
+ "40db95838f9f4258bb4767a34fcf48e6",
491
+ "40dc15941e0b456692c83a97bfcd2474",
492
+ "40f09323341f436aa2814c3391e04223",
493
+ "410026f520dc4e6999006b38cfaf5eb5",
494
+ "41075d1b3e714637a16b4ed88bafe1c5",
495
+ "413d197f7c0f40f9a0aaafc323dae2da",
496
+ "41657917b9064ce6b4715e8ba282e100",
497
+ "417e3873fcb34f7ab9744506d7bcc838",
498
+ "41842c8d2ebd402da04def3c53c41633",
499
+ "418553c4a96347bfa4f9b349fb8d89b5",
500
+ "41873763c2c641599a3220b675a5fca7",
501
+ "41f03702289940b29831dcb771fb5f3a",
502
+ "42141d069d8148e3b6eb2d1d803f3596",
503
+ "421de5857e8c43f1b865d1f4d69fe708",
504
+ "423b8807f56147d68ee764c7ba7aa440",
505
+ "424df49ef1334360a9490da34c72b522",
506
+ "4282341a050f4fa9ac9d79c72948e042",
507
+ "4288f5d3a1554b90ade37894c40936a1",
508
+ "429e6e5cf5014e24b5e0c8dd8cb9259b",
509
+ "42ddfe3afe55443ea7ffd7e3e806a631",
510
+ "42fe263b572e4402bba90a41a3110a3d",
511
+ "430f3f8741d54753beb8b238d75c28b8",
512
+ "430f5651a39c46258abb30a22f7b5634",
513
+ "430f90b413c0491b8b3af457236449e4",
514
+ "435326b23a6240f48c2a48140522aa08",
515
+ "4390e3589d8a42e6838c001d697c79d5",
516
+ "43b44305830748d08e9a807006df124e",
517
+ "44112469dea546f396dbb6d35539e8e0",
518
+ "44358bf4519d4815b037cec09e92b7f7",
519
+ "4450069fee444ae0920aa5babd5d9239",
520
+ "4460c58b184244d8906ae6b822ff1ccd",
521
+ "449c75ac880f401eaf0fbc36fd17526b",
522
+ "44aeb49fd30549f08fe28a9c495c3ed6",
523
+ "44b7df77727143169c545e39956a77e2",
524
+ "4544f87c79d647bb985524155829d0ee",
525
+ "455c1b96e8214b45ac06bb2742d8b50d",
526
+ "459c229bb92a4f9481ea42b167082ff8",
527
+ "45a730f934154d0d94c99d1744c92564",
528
+ "45b619b9080a40968af0874e4ca30879",
529
+ "45e0eb9afc694a808fc847332b759bc0",
530
+ "46026de3201245fba3f64f42e502f32c",
531
+ "4608272844e143f3abce6473fd80d3de",
532
+ "462d3296495944f890e24749d251c573",
533
+ "4663bb43c0504c59bea8227084945502",
534
+ "4665bdec11204bfa8e1edc00020c63aa",
535
+ "46b3d3077d304c5695ef4ceef7685970",
536
+ "46e59cb1580e4de4884cc8bb93e91b6a",
537
+ "4713c91e35684df08700e4f75f4f6f60",
538
+ "472037a7db9a444094b92b55dfeca564",
539
+ "47267dcf929946c1816932b6961f83a2",
540
+ "4752dddced01498dbac8cfc0cb469b24",
541
+ "47ace76546294deb85503e7b5f8d8b55",
542
+ "47b7026b98d54cd7bbbada1ba1eb755b",
543
+ "47bf3947748f4d24afdddfe680734ab6",
544
+ "47d4b60361e747edb1f88deb29f26e14",
545
+ "47ec876aa4034f8f8af6f3856513faa4",
546
+ "48016ceb77654dd7af9cd699148afecd",
547
+ "48567d6a1702493080293ea6c21ebf94",
548
+ "48814a3c57f74772a9fcb68e6d720611",
549
+ "489d6b65fc484a25a3c0e26180630719",
550
+ "48ae4ca557d842c3aa3b5e5652dcdab0",
551
+ "48b4f5e4b6f04fa9a13d9df0b7a82309",
552
+ "48c4cfee42f548ecb86b46bf187fa222",
553
+ "49024fa002a14b6ea7b5b45b3087c7db",
554
+ "4903ff98262a43a99bba6184678d5103",
555
+ "492f2d0058104ec2ad5ca7ff09751930",
556
+ "494bb58e60984098afcbf2f095f92830",
557
+ "497228f579654318bd6a371880995a35",
558
+ "49bb763a0f804641ac6101872fb56422",
559
+ "49c59c03ea424fc496c82aa2e3143b85",
560
+ "49ed32b8617e49ec9f580b5f4a753b71",
561
+ "4a31f6063c3d4183b5faaf926540996c",
562
+ "4a3435ce074c4f35a28ff3d1bd7623a4",
563
+ "4abbe8f3478f408aa2c9a4f772976cda",
564
+ "4b18b0caed204f2d8be554b0336749d4",
565
+ "4b33cf70ffca4b95829bec5548eb00f1",
566
+ "4b78a721ae3b4e3aa4587153fed3f4c7",
567
+ "4b883942ae254cdbb32bfe9ff5420455",
568
+ "4bb4247ccc5740b8a5e1db834a62d1a1",
569
+ "4bc0da4c9df0406b85f03a08aeb42091",
570
+ "4c33ad28bbbb4c07bcb765bf7cd5327e",
571
+ "4c3d66f810e74a4bb0d16b41ba93204d",
572
+ "4c593d53f1e64f909de169125c977cbb",
573
+ "4c7033acf418436992d85319cb995851",
574
+ "4c7f765936cf4abdabdd6f48c1b2e4cf",
575
+ "4ce5c565a567461fa1d3fa124d2bf862",
576
+ "4cea6c63123149278f1f4c14a11986ef",
577
+ "4cf149f5d6ee4b8e98620568aa3947aa",
578
+ "4d0bd5f9eaee4bdba2360496fa83a3ce",
579
+ "4d18aabb12cd45e691a5eeae60feaa2f",
580
+ "4d31a5f2c68d4e41bd324e2b23767469",
581
+ "4d41a273fe4c42a59777ac80c50e7f65",
582
+ "4d490543088d43e9bf7e3817a4adddb4",
583
+ "4d5418cedfb9476ebf9093d7ed67133d",
584
+ "4d6ce2d72fd74a66b43d866e8e5f7530",
585
+ "4dea0b20b2fa4a699410274fca4d4947",
586
+ "4df51ea0c3974840b029a515ca975709",
587
+ "4e1634929caf4f85a608fa17b55a1ea3",
588
+ "4e22aca5a799466aa194b173ff53b815",
589
+ "4e3e9de83ea74dae9598b4dd47ada072",
590
+ "4e719b26038f4011a59f4b901c10596f",
591
+ "4e94a564395b4b148eba76f85a5068a2",
592
+ "4e9c3cccbaaa4adc947ad8e8ab351d0d",
593
+ "4eaadfaa977d43ed9afd8064a9932a20",
594
+ "4eb495cdd69e439591557481b7cb408f",
595
+ "4ef030bb0eda45648b34633e61fd5e99",
596
+ "4f0ddf5603b94885aa69279381c3c411",
597
+ "4f1478dc330f4da2a728795b729991dc",
598
+ "4f1ed455fb764463adb0ed1cb779b4c4",
599
+ "4f21c24a1f47436787798485138b6c2d",
600
+ "4f260979c88e4865bebfdd707cb7edbf",
601
+ "4f33982b99d6487084676ed5f2fb0f71",
602
+ "4f3882be7e184b129946d25a75df3b76",
603
+ "4f38ab83cacc477b9f5b5f6d0230f1c9",
604
+ "4f682b0b32204a2b88f1cec535e58242",
605
+ "4fb5fcff117a400ebf5514e69a654a9d",
606
+ "4fcf8b855af7464a98f26f11ccd4ade8",
607
+ "4fd48175b15b4047a2deba77ca22d44a",
608
+ "4febddbd35e64d4a946ece70ce564f82",
609
+ "4fef9ff01ba14955962d05be6e3205a1",
610
+ "4ff2fe86bf0a4f8c84f7894e151c39e4",
611
+ "4ff545286db441c1ad23a49cc0439262",
612
+ "502ff900e8ec4612acdde695650dd3cc",
613
+ "50b0c61bdcc7488f8a5c9fa40f1a04ed",
614
+ "50cfdb6dfc2d4bb399f84603eb9807ca",
615
+ "51341f3ba62a4d76a9216b4c36d673c5",
616
+ "518fe53890f5439bba014f47c5c1b272",
617
+ "5191cdca98ca4b4ba961f00747029b03",
618
+ "5191fd63062340de9173dc3157731917",
619
+ "51a661d02b804960bedb89ae0e753288",
620
+ "51acfb9670c34e18ad4678c8c0c2a9d9",
621
+ "51c73ff41be14fbb88f70f7a5e78e142",
622
+ "51d59c2e12044e90bed75ea1154dcbaf",
623
+ "520dd7d5295a48ebacfada13df7548fa",
624
+ "5227b0c53a794a5d8507d722e9deef03",
625
+ "5285e04a70d543c3a2d88cfb7b55665a",
626
+ "52b2283f5ad14f248dccf7a139662993",
627
+ "531b7f9d88f244729068fa024c28d7e8",
628
+ "535f1c0e90a64a1997d1e0a7fe972d07",
629
+ "536c5d02d1e94d02bcbcb17883cfe85d",
630
+ "537de3ffbd2649da865bbc842126f828",
631
+ "53880a84c7214c3486125b10803aa664",
632
+ "539fe5e6624341f3a0139322423eec85",
633
+ "53c53a112c674d29a2afdbddbe3cecb5",
634
+ "53ca491de67147539b60ea971cceb415",
635
+ "540606ed90d4449ca14d713cc6386d5b",
636
+ "5406fedb5aa6425f87ad0328a3313d56",
637
+ "540c1e3114a244d0b684f3aa9f8adf87",
638
+ "541b53467ad045d59a81fe79d740ea66",
639
+ "542e27eef15b41bc93105e4f1d847942",
640
+ "543b4973b8e246879712903e3f580219",
641
+ "54712cc9bbfe4937acbc0fe252336f54",
642
+ "54926ff096ac4b5686211685e834d908",
643
+ "54b4d912eead40c49c03ed90d07db8e4",
644
+ "54c43cf57ab34f74b349577ba28d6705",
645
+ "550402c03f5544d5b2a9824ceb7eacc7",
646
+ "5527751e780541658d1443693c3b02e4",
647
+ "5532b05f53504abeb8fb8646a6a68e16",
648
+ "553b1d7ee9a7455a8ca0aa71cfe1f289",
649
+ "5564981a8ee24599a780553fa2b88c90",
650
+ "557963f94d0043d9a991dc9306cc7e73",
651
+ "55de8d707c8a4bd6a92122c51bc00fa1",
652
+ "55ee397cf40c4f86af1ee31fb2096d82",
653
+ "560055d1f3294a0ba6586957521cae11",
654
+ "56280d1b819d47d081646a509b4ea50c",
655
+ "562aa156220a4c88b34ee564657c1119",
656
+ "563ab2826c5548d1878213506475cb5f",
657
+ "56591b27807d43d5ab96704449412667",
658
+ "5684c00652674451be036fc5d7dd7217",
659
+ "56c1e1a540a8451bb84c759a753050ff",
660
+ "56c2dc07f2674b80b1cc42b5cf9b4c89",
661
+ "571125aedf354c60a9efb91ef5c789ca",
662
+ "574728d8f8c248e59932f314704503a5",
663
+ "57477e790a354b07bbb319588992f06d",
664
+ "5780da17fceb40edb810cec008acada0",
665
+ "579082d20a7c4b37a9764c84f602c59a",
666
+ "57a9862778bd4a5c95f4d2cd2819763d",
667
+ "57deddbbaf4445c888b229660cea5e28",
668
+ "582f50faf1734840a0d2694a5f869157",
669
+ "58baf3bd5f2b453aae930394725fc228",
670
+ "58cd53de211e4a97b6172c43b82aafca",
671
+ "58cd73335e334ee5a7382b018f9285b4",
672
+ "58d223c211994c8b9f691c69a3742250",
673
+ "59037103a2664590af810ccdd0669c0a",
674
+ "593975b416ec47a8b9d04925bce78690",
675
+ "599261e8b6fd43c280da33b32cf6925d",
676
+ "599a25f9afa54718b686fbb5069747a3",
677
+ "59b1014a0d8c48519d210230045b7c1d",
678
+ "59c577503fcf43a992d061f76fd0e4b2",
679
+ "59cb77de6a8c417a90db182c30a60135",
680
+ "59ed5e0f87c349909aa621ddf27c0036",
681
+ "5a05dc5c2f8c4f0ab67fecd33ff08249",
682
+ "5a561e2ad060465fb9452eba307a76b1",
683
+ "5a8243d749264f319b2c9a9a985db956",
684
+ "5aa35c1999b34c21b97491698128775b",
685
+ "5aae6d93c5cd48c8a39bd561f0e411a3",
686
+ "5aba0956e8f048dead78bda3bf8b2ce1",
687
+ "5ac666618edd491b830f5993a1a361a8",
688
+ "5b313936cf1b4412b7ee0c418745308e",
689
+ "5b316b26d0f844fc987825b975adcad5",
690
+ "5b6f1f3e3d444e1bbf4d737fd262743b",
691
+ "5b7a57bede754e16adacaae90f9dcfd0",
692
+ "5babcc376eb744d894cecedda6a22e4b",
693
+ "5bb9ff97fb9f4e93aaa1f41ba7a47be0",
694
+ "5bf4fcbbc6ea40b69c734f54180fa38c",
695
+ "5c10edda4dc44aa482827753be742d82",
696
+ "5c231d0b680d4f04ae334907b4d7dc18",
697
+ "5c45848fbc8d4184bedbec4c3162d1ff",
698
+ "5c88598e4d65455796c32745d1172865",
699
+ "5c92fe5c54e44f9f95da2cf2e7a3e700",
700
+ "5ca514218156463f922e7b80ec1ed5fa",
701
+ "5ccc35586cb843c48b0b4fdbe16c46d9",
702
+ "5d1544f61cc74047a2453d55791882ba",
703
+ "5d6b77a9180043f0b2dcabac21bc27a7",
704
+ "5d74e5db6e554e0cbca82346602928ed",
705
+ "5d92d3a2be7640a190efee26d69d1347",
706
+ "5db5890c5fe7473aaad2b7507969ac66",
707
+ "5dd9ae47211c476f8d4226147fb1ed68",
708
+ "5e579948ac0b427da1b88306fc65f581",
709
+ "5e972cfb6d42405b8ac32477711bb22c",
710
+ "5eee9d9172dd4738adc6a1916f53eb1f",
711
+ "5f05a5cf07e6423ba546642af5debcab",
712
+ "5fc5ea17fa3043e196d3febae7802329",
713
+ "5fca91a6280d438a9512c21ba8d27e63",
714
+ "5ff9f5f3cbed4312b8db212c25457050",
715
+ "604a8cc0467d47e8b473d8aeb85f5c3a",
716
+ "60586aa3584d4f2d9f2a717d5f728c84",
717
+ "607d8d582d3545a5a11f1209863e25e2",
718
+ "60d5ff8cf56747fba0864be02022ef84",
719
+ "6119e54641db4c1bb3110186d66dd21d",
720
+ "615fd1374bea4eb6bc1689de109f3b23",
721
+ "6161c738388641b5b0803bc23111650e",
722
+ "617015b472d641a9b57269f1136337f5",
723
+ "6175a1681d0648de97b2d62730b2317a",
724
+ "6178e02461f9419a9d8fab1a28ff5c95",
725
+ "618758dd359e415eaa9dacdc212d00a2",
726
+ "61a66adce11d4093a269f92ee615b3e4",
727
+ "61acbe77d630470496e51d0c932ae5b3",
728
+ "61ba551451894a19855a6e9ac1c8ffeb",
729
+ "61e13992d9ff4af7bfbce8f08c23f0b2",
730
+ "6211a8e24151428a9ebef3678fd4b6ed",
731
+ "621948ce0a564a30857c3716ac7ebd0b",
732
+ "62260f2abbff4b1b82e982b515fcc652",
733
+ "6254e180378b416b8aa18fb1b5284c39",
734
+ "6267ab80b28745a68d6d1357e1e0543a",
735
+ "62a4c16ac8084bf88aef8daba8f15249",
736
+ "62c30a8ddb7c41c0a627e602c23fdd4e",
737
+ "62d3322948f849099efd16281a1079aa",
738
+ "62ef67ed44bc49caa8428116257f64f2",
739
+ "62f162c31a404e8d88c7c691c886b6de",
740
+ "634e34ce3c0b4d75addce41c96e7b5ba",
741
+ "635453e91f0f47b5bd81a2f2fdfa91b6",
742
+ "63c36c9b479c48dd9df620d4bef2a5e9",
743
+ "63ca6b766c004b2d917e871dd863b8c9",
744
+ "63e0c9e309df49f8a209229321153742",
745
+ "63ea7be8390a40938613da78ec736038",
746
+ "640b33ee7d784da68c25208c843b5419",
747
+ "6466a4a5cff4418a9d7f4e4f2415d282",
748
+ "6470a004a99b40ef859df84f045c6654",
749
+ "648b81c7b0ba432a865131844b749971",
750
+ "64998ee900d641d2b5096caaa5cdf006",
751
+ "64cc765c1dad479a8b19844775a2aef6",
752
+ "6592c60e5caf48d0b92458bb9e086bc7",
753
+ "65972fba83074d5581abbcc66260999b",
754
+ "659ff4224a2748bfa49ebd4791471530",
755
+ "65b130cabf4446d292880a00b4363555",
756
+ "65ce677e4d8142f89db7c10bbe0f16be",
757
+ "65cfc4b3a40c487e94254f2844eb86f6",
758
+ "65d2c1ff090c44ec8178ff7d14e9d7ef",
759
+ "65ee1e9da62d42609c533716bb98a411",
760
+ "6602fa4a62774841991c5c35783d884b",
761
+ "6613e638925247d6b90e31761dd04e00",
762
+ "661a23e81a5342d1a8225bf078d5c028",
763
+ "664b09353cc342159eb5039c0e8c5131",
764
+ "66cb2ab19a1e4320aae97049c19ef521",
765
+ "66e1041a107248f9a6a154c886b0583d",
766
+ "67175ce64e6b47838ec0eb90bbf4233e",
767
+ "671af27feac14c39826b6d6704806a4c",
768
+ "67a7bfd33fd04a7d91c127ca468bf047",
769
+ "67b08bb5755643c6b4a8e0aa409eed04",
770
+ "67b2d6983ae0405daa199b5ff2d06c5a",
771
+ "6802cd54a7c44ca586a89435e317af5d",
772
+ "68055038a07e4819bd38bc3ff3ee39be",
773
+ "68097329eafd41a4bd1d5f02c1926a72",
774
+ "681fff8a91354f71ad73e8622a28c588",
775
+ "68268b376b174123bf753f0beaf195f6",
776
+ "684b3d859f924c229ef67d0bd8aaf4b8",
777
+ "686496a328f04a42a54ca48c8c1e8c0b",
778
+ "686d33ba777740baa87ec2583a076dcf",
779
+ "6879bd0426894258b7118bfa74b612a9",
780
+ "688c0525bd564087a24e55a19b45e8d0",
781
+ "68b6a8afb6da4e829cbede4e50235db2",
782
+ "68dd5c8e5c7245548edcb533087bdbad",
783
+ "68f783c1fd6c49edb6ebc3478a30fb85",
784
+ "690832b29fd74f80adfb9c70a72aac8a",
785
+ "690afb5cd5114368939b5fb4728ba516",
786
+ "691c480c4e564fac90d64efbdc6a1a8c",
787
+ "69368f6373104f7dbd1fc3b9e5fde801",
788
+ "6941f28a27b742b08dadbf48b12ddf4c",
789
+ "695c62069ac44614acbfc6effcf7499d",
790
+ "6984a2f59b2642e2b021c1f1883d0c6c",
791
+ "69ae6c5ba6574ba1b9cf67ffd3052a79",
792
+ "69c164a0131f4170a6d747eaad66b458",
793
+ "69e273358c994685885a5cf43cbc2a87",
794
+ "69ee8a7b246948d3a8c4d00f5fe99548",
795
+ "6a0987a9fc1b43518a651d77a362fef0",
796
+ "6a1ac7dc4a72440f8aab912cd7c8c639",
797
+ "6a1c75c5fd984767ba49d3ae3ddd5fdb",
798
+ "6a35f92b163d4a54a4c227180262725b",
799
+ "6a65365b39a94b8f87bef73242a8cd4c",
800
+ "6acaac9c73214edeb4145bb3d4d75906",
801
+ "6afefa97b3e14024a0d498310f8fa3b2",
802
+ "6b00a054ee2e4cafbf126d6e7aceb935",
803
+ "6b023e2439a8440e8fe1f263ccbe489d",
804
+ "6b33d7c1a28c4459a1058b829b1d8022",
805
+ "6b37230169d44874998e0ddcb106ca8e",
806
+ "6baf3fa1245e4039baca7109bf602dba",
807
+ "6bb4b73d2daf4ca39f9044171c9c474c",
808
+ "6bcaee578c8e4dbbbaf5fdb746e5ff8c",
809
+ "6bd2f94a87ca45209520b431749b282c",
810
+ "6c04b2c2c8004dc18817e522e5543589",
811
+ "6c44adc2475b4710817508ef565f5cbf",
812
+ "6c574b913a84456a87dc631a0dfd69c4",
813
+ "6cda14331c9c401f8586ed9e93626594",
814
+ "6cdb44a5385047f48b2dcdf1c9edb771",
815
+ "6cdd6964383e41778e517abd6d11b3a7",
816
+ "6cf216e7c4b44eeeb8c32286c2ad3508",
817
+ "6cfe3dcd556d49e0b953042ec327b22b",
818
+ "6d38c30873c542eb984deea1be3a5fef",
819
+ "6d4f5b7a603b43ec80dd8689ebf092ec",
820
+ "6d6c3b8cfa54457a8ffaeb520744c0d9",
821
+ "6d6dc89a87554b58b1cdd477e2c5bee8",
822
+ "6d8bd1fa499b4815b234ab46244cdf83",
823
+ "6d946bfd1ab44200b97f63a39416eaf7",
824
+ "6dabbf3037f841a2a41cf7a6d4639324",
825
+ "6daf0cd669934d1cae88e31fedfa183f",
826
+ "6db9e7aad9d94e5bbf375ad5e23be4ab",
827
+ "6df9eea2a5be4f8fbec679cf1b34c059",
828
+ "6e444e52e656441d82394022f27053c4",
829
+ "6e68d8a8a909449991623605a46ff62c",
830
+ "6e9a71be6fb64785b09f2a4bbf2abcdf",
831
+ "6f29a99af529414a8c98f208dd9ef90e",
832
+ "6f2a81b4544c4518b705f6cad6244597",
833
+ "6f47d38de28b4a879481850b68bca501",
834
+ "6f5a60845785415eb34b556c65e290c5",
835
+ "6fde18e184154bfcb1c662333f54fb03",
836
+ "6fe0e1ee53eb4045b6d1a37f39221ad4",
837
+ "7054d2d8710b4213ad3857c1e37e57ec",
838
+ "7071b9cf40654ca68a618cc3aa3e8731",
839
+ "7095ad6338ec49ab86f026c5aabd1a2e",
840
+ "70a9556f1f2848eea9c1b4999c1d1d98",
841
+ "70c2c16114654e7e924f6254d4143c80",
842
+ "70c42fc642394764822e47f490c26471",
843
+ "70df779f759a471386ca79f735d0ccf8",
844
+ "711acde9db7149b79c6b90c8f9a736e7",
845
+ "711e1a7e980342c79428ef608d67653e",
846
+ "71416aa96c174625b4fd7b4fe5ed203f",
847
+ "715b5bd2832145c7b86ea7f785a2b73e",
848
+ "71893dc577d2466298b045f965482fbc",
849
+ "71a32216dea14f259a04745594bc1a69",
850
+ "71d34d3c50c84e9f8a7a22dfc6bce68d",
851
+ "724432b2187b496dabd74c8ffd119c1a",
852
+ "7291e2fd86b84a05844d23c4ed75d1f4",
853
+ "729f690692a5421592e8ab3217426de0",
854
+ "72d16ce9a267457680325b448853ff73",
855
+ "72dfd7ef4b564a55b5e1ab13da8d6a55",
856
+ "7328466df47b4c2c88c5194a26de281a",
857
+ "736bafbf63a045f080528e4f0b8cb552",
858
+ "739415ddd194434f81777eef8b041bc3",
859
+ "73f80ae78964472bbda19328b354fa35",
860
+ "742fbdd5bf314a488494a4afdba92b25",
861
+ "744715eecfa4452b8624e955679c3068",
862
+ "749ba33db8c243d986c864afe3955731",
863
+ "74e2f89c66c94d788a840ef7dc3cfc4e",
864
+ "750d9b866e5645ba9709bcb1dc9121ec",
865
+ "753427678aa74a248ec40c1a35d2c05e",
866
+ "7574c2f8e93a4628a00ca6fd58255e6d",
867
+ "75c60cb24bfa4ec4836f81a6ce6e6103",
868
+ "75ebaecaeebd4ccabd71729b296dc6e2",
869
+ "76069f879e974bdeb43af713082f2a21",
870
+ "761d9a3648eb4139aeb4c47cf93cfd1c",
871
+ "761dd9c19dbc4bfeae9d08c06722710a",
872
+ "761e5907082a4044a8417bfdf7ee4c66",
873
+ "762dbae73b7d46688d8b3657b7196423",
874
+ "7641ca70948247a48f57a8abb70db602",
875
+ "7684d220b9cf49c9ba1bd6256aa0416f",
876
+ "7689b79133234e5c9ccc5e881f8b8a7d",
877
+ "76a5864ca4f74c83a125041af4daa586",
878
+ "76fa02bee7e34413bd1f2af04a706107",
879
+ "7722d01005314531a1551a890127fc6b",
880
+ "7730b7282b254cd0a4f6163790e0bff2",
881
+ "7746283c634c4855b7e2b08351ab44dd",
882
+ "7758742ed2dd49f49c8a107c9845f597",
883
+ "775975bc98294259b76c44b0d75e9d58",
884
+ "776c21864f8341de9ee7454aa5b220fe",
885
+ "776d4b1c05ff463593d46fe3aad1f1a5",
886
+ "7777f9563f734375abf37aec8c4ff55e",
887
+ "77a20b0c965f4422885f558e94f69f87",
888
+ "77b1336e9c2042fabb5a7175427d5ded",
889
+ "77bc75c7ecbf435da1724062eced10c2",
890
+ "77bd325607974c4c8e3db77a0814842d",
891
+ "77ca17defa6845f8bc09fa350fd8c422",
892
+ "77e19e91f00e40a983d4d41ccb524ec7",
893
+ "7833f24887b14d1b9d2f98e9fd2e744b",
894
+ "78506b95bd5d4132ae00344e4c147d60",
895
+ "78514023844c4401a61b259630a39b88",
896
+ "789e28c00c72440d93b494bbcabf6bd5",
897
+ "78d89086a6f64b89b85158987ac366c0",
898
+ "78db1c0163b74cc8947edeb42dd79edd",
899
+ "78dbcac5eb3d4c9dafb9226cd78ce9ed",
900
+ "78fc5eaf6edd4c7f80fad7ed953822d6",
901
+ "790c40ccff6843eab0b7b4bd18421ff8",
902
+ "793d83afac59488cbfeee6ec4b0c0c18",
903
+ "794712e7d8a14367bafbeb2864f2695e",
904
+ "79477474139945a5a5a0164c8fa1b465",
905
+ "7973d98b789b44deb93c76aacbb8e66f",
906
+ "799b7ba3c0f242ba8d78dea38a54b916",
907
+ "79bc0a1e5ad2453b8ca761b7a12063e7",
908
+ "79c293346e5247ca84b31c4a6ea23131",
909
+ "79ce537a22044a3494b477739a650889",
910
+ "79ddf53a0a8b42b59540b29c42458620",
911
+ "79ef8744e7974cd69ba6c992f0ebaf1b",
912
+ "7a0496b2bfc5419e8d2e0b9b6cbb500b",
913
+ "7a6c27097bcc43859f146eacf34945c3",
914
+ "7a9f728fbf37420f9bd68cb3be8a2e5f",
915
+ "7aa8750fd1aa4dcd89a5c039caab5940",
916
+ "7ac460ae0e2d44fe977db67f4e90ec27",
917
+ "7b090422e3b745d2ac7632838adc2793",
918
+ "7b48292340ed466d831ef99b17df227d",
919
+ "7b863e9c3fda4147a35a2b5e4421dee2",
920
+ "7b8e7485eb0a4e9fb1cff992765f5be7",
921
+ "7c05c683425740febabd29a937de624f",
922
+ "7c1582b839fd4b318bfcc976e94e6c25",
923
+ "7c2652a35dbb48deb82bd6e87693a12e",
924
+ "7c65246246f74b7e83fb720fe062444c",
925
+ "7c66faf7b6104fa1b29035e3b5eb7102",
926
+ "7c86cf1acb4f4f7c9751c16101cc62ea",
927
+ "7c954751f56d4715ae7ce99aa0158b78",
928
+ "7cec9c91c93e48b185adb045404e8f2f",
929
+ "7cf2a742340647608e9ef7af049b6ac0",
930
+ "7d34a706b55d4acebea87abbe2e67d79",
931
+ "7d4dc0e32cd545109ed8a00bd85a7840",
932
+ "7d996ae0f69945e9a87fc3c542ce2683",
933
+ "7d9f4c78ef6f486fa94e3581548ed5bf",
934
+ "7dc5903c241c48d9989e10396e82f90e",
935
+ "7de0fde2ea434e378c0c948abcf5a91b",
936
+ "7e0ea5afa2464e3190f141d983382b6a",
937
+ "7e1fae1510be4ebeb41d0e7fca9b2fe9",
938
+ "7e45ad634364474798ba58c0db46ecde",
939
+ "7e464462d6fd463e860f91f70db01848",
940
+ "7e6f3be701224306be49375af2e39025",
941
+ "7e7703b966f44564a93b54cf5d61d2ab",
942
+ "7eab76e784054c7fb23afb9bf93feab2",
943
+ "7ec3859752304e1497fcad1dbc8fd27f",
944
+ "7edddfba4b51475f9438cfd8043ded86",
945
+ "7eee9870d8664dee9966228052d249ab",
946
+ "7f74cf149f434c378816bbc17084323e",
947
+ "7f7d9831de524660b6e2428ac19a674f",
948
+ "7fbd63691be243e996af064ba8e196fe",
949
+ "7fbfd3bef2d043729900839e6ca21355",
950
+ "7fc0b31e990c435ea4029ad2a1670cec",
951
+ "7ffeaf23240141f5b0974abb74c0e452",
952
+ "801da8da9e244c06a5af7b48e052b365",
953
+ "8026dc5f17254c9dbaeae2ee6e4f2779",
954
+ "8034f1419ad5453a8d79d9d641b3ef38",
955
+ "803cf6b3a1ac4de9b3306e9c119516c3",
956
+ "808cfda3601a4d978b2591133204c844",
957
+ "8094f24d861044c3a03b0fad4e8445f6",
958
+ "809fd40376ce47deaf0cba856ec2785d",
959
+ "81e14da144654169938f316d9962a27d",
960
+ "81ea8c1314fc4517995b7ce5218c676d",
961
+ "822dd26fa82143c98eb769729c245843",
962
+ "822e9386244749c98992fb5c09029c17",
963
+ "826d09ed54ab425a83ba09e82f082c70",
964
+ "82919f5c92a04c7cbabaed43ad02c33d",
965
+ "82de9078a05444e7b24bdca8f45b08a1",
966
+ "832e6fddc6104248bdc37545c7b506b6",
967
+ "8337f24d481b4b62a528b25ce544158c",
968
+ "83479a56bcfb45078bf65cfdccaf895b",
969
+ "834858034a794ce59d428379dd9b0f1f",
970
+ "83781a711aa9491f96bdeee95c819715",
971
+ "83ba11956b8c4c2ba48f00b27d3e076f",
972
+ "83e0f5401e15427a90cae22d0ec8fd02",
973
+ "840a2c9386c242d88bd1a6f8a3bf491b",
974
+ "840f27f03b464cb88423df7afb28c9ca",
975
+ "84a6575ac1d04589b7379f3b73944e2c",
976
+ "84a997ff428b4afb87d5e9ac3213eb09",
977
+ "84acd48c7e17498ea1d486556e635ba3",
978
+ "84b15921c86b4f7ba1bdbec7c854854b",
979
+ "84c113fd7225428c8aa8db4f28563b2c",
980
+ "854dbbb2ab994ec8a1b46289a333a9b4",
981
+ "85b31e7e54b74bcc92fdf9bf5dddd4cf",
982
+ "8606f632251943fab5c37de86ce86e44",
983
+ "8616350c05974bc28c6ae44ac788d8f3",
984
+ "8657d2c5e7734a1aba0b71abcb0e04ab",
985
+ "865ef12fcce64e61b97b83b04794d07d",
986
+ "867410393151465c8b48b4333602358d",
987
+ "86a5abfa102948119febdec47db00706",
988
+ "86c0e546489a426c941f26d522621c93",
989
+ "8700abc1bd4c4a90b97e6c462503cf88",
990
+ "8700d16e247d4a51956ef904643b95fa",
991
+ "8708af3768a743e7bc0c0b2c3bd2c5c7",
992
+ "872d01ee121c4d1ebfd38c5c2616e181",
993
+ "873144f3192a4b4490675151af5529a6",
994
+ "8772d82deffe4b8c868641618049bd3f",
995
+ "878781513c784e8c915a4df019faf22a",
996
+ "8795471e399040f6bf10813221f6dc22",
997
+ "880fa188c1704773b40aa222dcf485d2",
998
+ "881a9a5f16f04cb68ad1dfb5e3bec34e",
999
+ "8846701f077241e6bf4090b5abb19919",
1000
+ "8859ecb3dafd4bacb977839427e43317",
1001
+ "887796ef013c42c9837320edaa316d07",
1002
+ "888947939fb743fd9cea71a2a5c07684",
1003
+ "88e140a5035f4ab093fb62e494675b8a",
1004
+ "88eec6b796df4444a7b24383369f350c",
1005
+ "8926de526bdb487a94e3c3ce3dd49935",
1006
+ "89301025dd73470c89fd328407a12011",
1007
+ "895a7a7d01f64785b03ee102c0344371",
1008
+ "896222fc3f2c454ab9cbe701bdeb7b33",
1009
+ "897be755cbd04f1a8f33ea0ec852a280",
1010
+ "8983e5d47f12435bbd1189168b75abeb",
1011
+ "898feb7c028c4763b9627543038f5ea8",
1012
+ "899d758d99294a019ca24fb848d0a60c",
1013
+ "89f6779a95804832be2b52d2b1efd273",
1014
+ "8a20e56e69e14851bb157a3d0ce2eb9f",
1015
+ "8a4b9859fd6b4ce1a9619cee2dcd3e42",
1016
+ "8a547261211b45ca96a852ca1dc5ba61",
1017
+ "8a7d086e0a8949cbb9341f0763c98265",
1018
+ "8a9edc74f8444057a0a17d4a42ad3aad",
1019
+ "8ab0fec878924fd295e705ae080ce9a3",
1020
+ "8abdc0b307634afaa099b9e38132c151",
1021
+ "8acc1b8ae90c4ed78f9fc91285dedbe4",
1022
+ "8acd766e66b6478a8843f03ebb085ba2",
1023
+ "8b224a3b0b8d46c98e8cb572453d0eca",
1024
+ "8b297620c9664c09bd216395868e3fc2",
1025
+ "8b34d4b347154db5adad199575cb8fec",
1026
+ "8b39b8957231420d919b60febb627bd5",
1027
+ "8b7a073c112c48fbbcef3e02a14eb619",
1028
+ "8b8b6a5ba71741e39934826d268a09af",
1029
+ "8ba53f9043e64e89a7b7abb45651c932",
1030
+ "8c07ef9c69b3482a86904cf6f6d12457",
1031
+ "8c10d8b64f1d40afad35ebd9dc414177",
1032
+ "8c564dccf3184669880b729e36541c3b",
1033
+ "8ca5fcbf41734109928e08c3ba03b96d",
1034
+ "8cb42925a5c5487596ef76b27d3d373e",
1035
+ "8ccecf650a4a47d68f7d2ab27b87850f",
1036
+ "8cd40f962f2c440c9caf1db1a505357f",
1037
+ "8ce3d9559a134bb1895654770f721956",
1038
+ "8d7f958e192b454fbeacd2f789dd4cd0",
1039
+ "8d96c299671c4116bde407991e2ff04c",
1040
+ "8db2af4b6a474097b946b767dfff8a8f",
1041
+ "8db69d80a04e4afd82be3f95a66c9dde",
1042
+ "8de9c1b24cc741b19eed5e6ff572383f",
1043
+ "8df86390e8c34a42ae64100f03d2384e",
1044
+ "8dfbf3c06ea4426d9bb9ff7ec125393a",
1045
+ "8e46cd880daa4cb6ae7e18897cf98180",
1046
+ "8e65882e534245c5b283577c5521acaf",
1047
+ "8ea24adb9e9e4cd2b429b132ef049052",
1048
+ "8efa4f8b34d042d78b1512fc5135eb1f",
1049
+ "8f0d6e7582b44bb9b4a81e2f18b2584e",
1050
+ "8f1cbc19dc414ad89097eabe063ada88",
1051
+ "8f285bed98a641b383a45e7fe970175a",
1052
+ "8f31a85c22d14760974c23535d32c3f2",
1053
+ "8f33734764b64263b998610438addfe4",
1054
+ "8f485e3c4d7f4cf291869b53d4bfb04a",
1055
+ "8f79577525dd4c81bff24b86848785e8",
1056
+ "8f8a6439e1c64106afa27b28936d5ef8",
1057
+ "8fa283778cf84b6faf26114a40289e60",
1058
+ "8fb4d6efb8c94cfb8c5b6f986a009726",
1059
+ "8fc9e1a422c24deebbc955e871693709",
1060
+ "9055288483244d228a5d183b206ab9ec",
1061
+ "906df3a351564d2aa34e2fb6a85b71ba",
1062
+ "90789439e18c4f3cbdb5c51b6c43b531",
1063
+ "90acd51924864030ad8e019b332716e4",
1064
+ "90c364a220514d20b9be9e06c1a0a184",
1065
+ "90e943ba104e45f5bf8edb878d99ab95",
1066
+ "91277c9935a745ac83507bf6a1f1ce0a",
1067
+ "91413c86c92d47c4a61afc7e1875b1ac",
1068
+ "9228178641df4a4cac543f129d526b16",
1069
+ "92384abd65874b8eaecee88cacc299ae",
1070
+ "924d926858e84e6cbe124cc5e4d3d797",
1071
+ "92acb79620954d7faafe1b33f4ffadb9",
1072
+ "92ba44c964614eccb41c7b0b2fe9ac5f",
1073
+ "92cb6892fd2e46059a9fef031aa3ac11",
1074
+ "92fc926cadae41468a3976b931c58ba4",
1075
+ "9310ddb8efff44b3b59e1d553ce18a2c",
1076
+ "933435c0db8640ffb17191b029727048",
1077
+ "9338582032f348a49da8be159a650699",
1078
+ "937718e4bfb040d08ecfb57f88028cd9",
1079
+ "939c65e55bec4a27978f1480462d78de",
1080
+ "93a6d86f97ff49e18220c8d5af6f2410",
1081
+ "93d57f0c4ae04a89b698b84a5cc28620",
1082
+ "93e9d54392d34f45ab3bb9183c9e721e",
1083
+ "94007567ad674848baaa184dd69316c7",
1084
+ "9415b5a3d80646a6a04c95021eead04c",
1085
+ "9417265e5b1a44c9b96e4ea1c3133356",
1086
+ "9436fc1799344a0a95133338978cb575",
1087
+ "9442cb6575b3424fb608b1ef883f8ae4",
1088
+ "9458727177e44b6194f518712be17d61",
1089
+ "94c25097256547498d3119e1d3442aca",
1090
+ "94c3787a948349f288a83a9a7accc76d",
1091
+ "95028bb2100d48418161062f2d84e637",
1092
+ "952bcd70171d4c4b965192cb284e2fb1",
1093
+ "956cef1ca1af49f5953fc202b20dcfb9",
1094
+ "957da1be796943dc988d10f5b087d8c8",
1095
+ "95b6ed35955e4d13af13584f79fd96dd",
1096
+ "95b6fbc975624eb1b88afb92e15658e6",
1097
+ "95c53161769f4c259afbb41bfc94f5f3",
1098
+ "95ef0d8332c44c8cb2892900d6cb27d9",
1099
+ "95f6f7d8904f4a719eabff7d38e022ee",
1100
+ "960bdb8733104752a1327d89d9fc8046",
1101
+ "963c49228f73402ab93c59c8d8af8380",
1102
+ "964b92911b4e4fbfbe82360ea1d476d7",
1103
+ "967b58b39a244dea8ccf934d6973054f",
1104
+ "96d5e95a6627478e9a9b7ce59738b1fc",
1105
+ "9701c9240e5b40e7b5da140716a78d0e",
1106
+ "9706a8b7365945ecb40fb57693c9b23f",
1107
+ "9747c1a7c1e54e079fc5b8ecff22b96c",
1108
+ "975bf43d39fd4440aafa511aa85c4693",
1109
+ "976564c01fb341e3b75fc4dcc4e96ba8",
1110
+ "978b2d75c8964f969c6513d140d15f0b",
1111
+ "98161ac528d8469da7f1e98d05c8b4dd",
1112
+ "9833113cb50c4e96b931e1500996d12f",
1113
+ "9855438bf7d14f9b8a83e779c03e937f",
1114
+ "9865812391e1410ebf320ba485cdc2f7",
1115
+ "98cafbdff0c54ddb9f4b26d6a79768fa",
1116
+ "98f31e11891d4b92b03ba96745fae9f2",
1117
+ "991635b829a944768c92b65587279051",
1118
+ "993a9c9ec519406ba3957046b8ed07f0",
1119
+ "996d7f11a517464eaca8ac6fced3b1c8",
1120
+ "9992ec94247644ec8f86e75159749f8c",
1121
+ "9994a32ac7f041258ce42db6927109df",
1122
+ "99b9da73ba7b42f19de8c3008224eb72",
1123
+ "99c8aa6c73f7482ca9008a97393f8a3c",
1124
+ "9a1f55860bb44de1bb536cd01cabd155",
1125
+ "9a42e5c2337d43a69b7def22736cb369",
1126
+ "9a49e336092d4e5aacc01aba739f601a",
1127
+ "9a4c584a1e444676960846d8bc4cbbce",
1128
+ "9a78cc1efe5148af93df636a777019c4",
1129
+ "9a7d317fea29496ba0b2c5cd83be473f",
1130
+ "9a89d1c9bda24ca1b777f5f0b4637bd2",
1131
+ "9a93c60936614cb7bebc4791f8c7b318",
1132
+ "9ad64e4cc1de4b719a669ef7caf6e329",
1133
+ "9ad868279fe746cdbf64afe5668d7b02",
1134
+ "9b12c91d3fad415392a3956cff795bb6",
1135
+ "9b693a5b97c54adcb815701889f7fe1b",
1136
+ "9b7ae513a2294711a9e083de1a4dcae5",
1137
+ "9b82427e59aa44509713f4d5b2ae5b3c",
1138
+ "9b8a6c99aec9431b811121675bba0f08",
1139
+ "9bab8b2b13094066a36af285c2e20a9c",
1140
+ "9bc7394303a34558b0c6324e56f47d54",
1141
+ "9bf986e0ca924aca92462c5bf271e3d3",
1142
+ "9c41cd8aa389471f9e95b6ed5367fb35",
1143
+ "9c4b477d7d834ecf8b15cae2d2883438",
1144
+ "9c8a9a46e86e4bdfb084c658eee53dcb",
1145
+ "9c90baefd9444fd0a8d71e789e22d548",
1146
+ "9c9c46a8146443deb081230ef202e987",
1147
+ "9ccadc2945c34826a0ce1c37de13675e",
1148
+ "9cd02abf1e3f43e388bdbee8562d5687",
1149
+ "9d5502124c954756a347d7ab07557b93",
1150
+ "9d7456f25298471aa7b736f2e1c3ffd9",
1151
+ "9db3ddd3c9d549f3a35b92288bbd6a1d",
1152
+ "9dcb7e0e87194b0a9d0bc81fb3685920",
1153
+ "9dd25a357ed642b1b2607ce2b1cfc0f3",
1154
+ "9dee38c8c7d544018dd60410c8dd7059",
1155
+ "9df9e5041a48418ca9691045b4e59730",
1156
+ "9e0bdadfc8c24565b1160497fb0f4f27",
1157
+ "9e1fd2f3d79348b9a45350c644fb2e29",
1158
+ "9e4011bc059c4a1db5f8bea8bb96484c",
1159
+ "9e4b555e7d4a4e9d896b284d8faa03c1",
1160
+ "9e6b76fab17a4b40ad79cb72a46f3899",
1161
+ "9e6db7d9a00a417c978a71b371c68a5f",
1162
+ "9e74bafc9d0e4d739b33a08e5361e7db",
1163
+ "9ee0770e069448f6ab316ef8300bd5cb",
1164
+ "9eedfb865cf84cd59a15e65f7f2d393b",
1165
+ "9f0a3283cbca4454bf04c8d43494247a",
1166
+ "9f24973fc9c344539a6105a1e7357fb4",
1167
+ "9f4c5635597e4edea48b6f069f12a154",
1168
+ "9f691379311f40289c6b661973eb2633",
1169
+ "9fa2da2c42234b58896e8d23393cac24",
1170
+ "a022864ae7b642a39469652c3eda2b9a",
1171
+ "a0426fb42d044ce386d55ed4594a328f",
1172
+ "a067690ca5ba4734a6cecb51816c534f",
1173
+ "a0ae47edf24b4e8ea3b4d66afd70a95e",
1174
+ "a0bf19176b5245bfa4704242fb16aee4",
1175
+ "a0d0ea06fcea41fd843af40691e85f34",
1176
+ "a0fd7b70748f469e8a984b84ae570d6f",
1177
+ "a117781bb5724467bdb321aa7ac02d1e",
1178
+ "a11b00033ce344aba75918fea71b1863",
1179
+ "a14e7cc8ae6643d69395276456d813eb",
1180
+ "a1613311507a4fc8b6a4f3d859a447e6",
1181
+ "a1754867f6094ba28b32cfd72dce0ff3",
1182
+ "a193d18c46084d60b2500cf235a41a89",
1183
+ "a19f6e77e5f244188ae31c800e17fc1c",
1184
+ "a1bce4d33d3d4d47a32745b785de6003",
1185
+ "a1c46bc271bf44b3afcc7320de94797c",
1186
+ "a1caf2c91b9745949ab91cdc9902fea4",
1187
+ "a1f6743d821c41028f095731fabf54da",
1188
+ "a21b111d6bf34c6b8e3e553f653f83fd",
1189
+ "a2296d354281424188ab006d3ccc8b0f",
1190
+ "a268989869814587add9bce658d79ea6",
1191
+ "a28a424008f04771865712e5e3054f04",
1192
+ "a28f97adf9ad47d6806a4d6d85706b81",
1193
+ "a29496f73923481fb509e002671e6a62",
1194
+ "a2d8cd051fe04166909a8eb3fd2af233",
1195
+ "a2f31b5a37fc44c49c11026cf2a97f74",
1196
+ "a304a6be438241c5bf8eb89f574e3d7c",
1197
+ "a30cdc34303c42cd953dc935e1bf2110",
1198
+ "a35c9d88cc9b48d099a69dd1d3aa69d7",
1199
+ "a3911e5dafc44f9fafde9e8d424f4a4e",
1200
+ "a391c14193234bb9b654848a2b386c22",
1201
+ "a3a5829c7e5141f3a4ac46740bb440f3",
1202
+ "a3d79ffe82f94c1daa45b8ffe4933851",
1203
+ "a3f3028abd154ce3804f8711c30d4f6c",
1204
+ "a4070f7a072043ba86a590b2cb48a45c",
1205
+ "a4197833548745c2b13ef53fb2cc4d38",
1206
+ "a4909ba08ad540c289d03c3c67b9d856",
1207
+ "a4b0fa883f0649f4968c94f3c421d493",
1208
+ "a4efba8b24f0499c8c248b0ea366db88",
1209
+ "a5023a5400274762ae9c128c07c82443",
1210
+ "a52bc5f0be6746888734e7b6277bfabf",
1211
+ "a55418a161a14c369d01fefbf7f4c442",
1212
+ "a5cf428b41374afdac04c62137c87fb7",
1213
+ "a5ed5a1f630b4d689b2d65e5ae8b56fa",
1214
+ "a6004ae37b2247208e80b8b8412f3b61",
1215
+ "a63b41bf6b364c9f9177c9b1e3da049d",
1216
+ "a63b931003a44d45aa22e9bad9dc8b1a",
1217
+ "a645ef2489244f52bf3ccf6836b60984",
1218
+ "a646038eb9ae434f80dc6dc726c1dda0",
1219
+ "a654dcfdd5744259a88885c20d14cd9b",
1220
+ "a65ec17538c3429ea50ceda913fa565e",
1221
+ "a6b9bca28b9f44429e41b815976584ad",
1222
+ "a6d34a86d9444ebd9690ea1ae8ba594e",
1223
+ "a700978deeb14f67b2710afaedc2a807",
1224
+ "a707582f74e74ddb86bb109bb956e750",
1225
+ "a71b6d54f71f43e28c019cdb42be55b9",
1226
+ "a7229ddcde4043f6a0e34ef3f6df19ff",
1227
+ "a734f7a8acfc431cb19993dd98a2998a",
1228
+ "a7352824623f4629976067a996dbc37e",
1229
+ "a739fbc9d88c4daf9f730dd9f6778c68",
1230
+ "a73d63f44fd54f209bb6bac37fe247b9",
1231
+ "a7780e3667954bbf97bb619cb8a3e272",
1232
+ "a828fe9dc5a9415ba3d7f977a9825ea5",
1233
+ "a82cb8c7613a47a693e084fa52d0d9bb",
1234
+ "a87a76e7544a41de971814dc9a19eb11",
1235
+ "a893146b76cb470b9056e6b88ba8afb0",
1236
+ "a8ad20910503482db578f9298afd81c6",
1237
+ "a8c375384be9427b91a4f68125c3942a",
1238
+ "a9161e6c2ec14d7aa9fe10194468f8d6",
1239
+ "a93e93e3e41e49feac04747c6f540b05",
1240
+ "a977c54c25c74a0c9fc0fc4de99ecd45",
1241
+ "a9a6555b9cf949d3a01003c3bed6dac8",
1242
+ "a9cd031ef92f459995d4d2e8595583bd",
1243
+ "a9ce75a690c84f06ba0934c2a27e7fcb",
1244
+ "a9ddc86165ba4b819360231d29b9defa",
1245
+ "a9e576e05442468a94e07d5c6a652b3b",
1246
+ "a9fa5d325b1f4b2b84c212faf1254ccf",
1247
+ "aa0183f6bf584115a7d3edeec4d7dac9",
1248
+ "aa2be996b12f431cbb9a214519b0923c",
1249
+ "aa5f73e3d7774a9c9e72b25db2b89da9",
1250
+ "aa7f2187820c49d886e40c69c24c10e3",
1251
+ "aab20be23bde45cbbf26a46f51a09e91",
1252
+ "aac11115274f4fc78265b0f04df2e520",
1253
+ "aaeb0e31501e4f6d8724b6065f7a62be",
1254
+ "aaf795d26d2045a2ad366a62f661ef6e",
1255
+ "aaff1dffbcc44f2e8ba35516509185ed",
1256
+ "abbf5216c6a44b70a7a26d35ced8e1c1",
1257
+ "abcb0be28b944a49887d49935464be9e",
1258
+ "abe0b76027f6445ea496499cab9bf2d0",
1259
+ "abef4ff1da264d88b10f99e910759ddc",
1260
+ "abf756cea6e34b14835b7c0dcbf07c3a",
1261
+ "ac3483cf825249dfab4fdc00fe8e21ac",
1262
+ "ac37aa4de6394799bd3c1143f0a0742f",
1263
+ "ac6ae15a135a4e848036c0b4eef86d09",
1264
+ "ac723edacfe94c9a9233de5e82a227b2",
1265
+ "ac967d0e52d04e87a2e9accc69260636",
1266
+ "ac9f145a25fc4eacb6fe3904a7e2c9f6",
1267
+ "ad0a47e65810464e9d4ba6ef65208367",
1268
+ "ad224585dc804988b812821a51a573e4",
1269
+ "ad22be23ff8d40b9928500b13180b47d",
1270
+ "ad5f24c0c0544c12934a1e8be286f5e8",
1271
+ "ad82c1a7ee664dc5a260ff947c693fd8",
1272
+ "ad837c6229fe4a82a1a60dda23c8a1ef",
1273
+ "ad984cb5ced945d7a5c53a7ed2232c7b",
1274
+ "adb078c42d0746798efaa3e4686afd33",
1275
+ "adb24e49311b48e69a95c3a5324817ec",
1276
+ "adc46168b8c54bc4ab01343638c42e10",
1277
+ "adcc143d66344019bf695d63299486b3",
1278
+ "adea146fdc9c4fa5a9a68a257e1f7060",
1279
+ "ae0411f1f2b44d8189028448ab49b2d3",
1280
+ "ae07b87675b24418b6763cd18cab83e2",
1281
+ "ae1fc814ccd5461aa243c53fcd0d6c83",
1282
+ "ae314dc3de994d2a8d7ea9dc052b28e0",
1283
+ "ae3ce147938d47788cf9d5f29d32fa66",
1284
+ "ae5a5ef80af440daba1b97464429c519",
1285
+ "ae5e5283d56647beaaeb029d4fb92faa",
1286
+ "ae61592e73224a6c90897dfe4e51e0fe",
1287
+ "ae707476ba784da2bfa83406cbfc4f3d",
1288
+ "ae87e445e97e48648b073dee0abe72ef",
1289
+ "ae90aff7b8834654a6f5cabff7d35437",
1290
+ "ae9d7a4529134cb0b3601c0ca699ffe6",
1291
+ "aeef6d53b5ce4f2eb391281a2c04e89e",
1292
+ "aef67d7de76247969056225468ac1764",
1293
+ "af57904d39954ce09375f94134105ee1",
1294
+ "afe31faf1d5541d3bfed9f2cd86f29b5",
1295
+ "b0264b10cab6494f94b4073b7dd92357",
1296
+ "b02d58f07e7243a0ad4077e5f778abef",
1297
+ "b089ed83b2974a81a00b5ac6330c07d0",
1298
+ "b0b69dac8c0748578c5a9a948ca7c2a0",
1299
+ "b0c00b0f224c4b04b4ecf0468445d0d8",
1300
+ "b0cf6517af894b59a25ff95aa6e83aa4",
1301
+ "b0e026d0c08448bdb38fc60ce71e488a",
1302
+ "b0e695e8483f46f88943173a70be9709",
1303
+ "b0f83707ec294bb2813e67c8c3e5db31",
1304
+ "b101fd45b31c4719b7caca788619716a",
1305
+ "b13e35727e404b2b9fd3ea64c083ff2e",
1306
+ "b15c6bc6090f40009ed8e8511820827a",
1307
+ "b19b27b5bf0d498289c662177b465350",
1308
+ "b1a1aae682f241b280eab323cff8014a",
1309
+ "b1f89ce951194147bce1bc55f9248c28",
1310
+ "b207d9d282484037b072daecbf96c2a8",
1311
+ "b21b62f911e54b29863d37b2912105f7",
1312
+ "b228a29fa84544c2be501c295653ffe7",
1313
+ "b266a2134d9945f697123fc6c8e23522",
1314
+ "b2686595ad1d4907bc46e87e9131df1e",
1315
+ "b2698fff89454bd5ac826ac762fd0a32",
1316
+ "b28364e02a294f9bbd8b3fdfeb33cb6a",
1317
+ "b2935e579f4141dc96e16f77151997b1",
1318
+ "b2a9701db8b84d3a82f99d1f719af0d4",
1319
+ "b2f80a34e1cb428f9f0139870a18f1a3",
1320
+ "b2fe519bf8ca4aeabdb5f489c260294a",
1321
+ "b333b3e6066443debac98c0ab1897e13",
1322
+ "b3389be723684644b731483f31ccfc15",
1323
+ "b342af82a6ba4d3f94f546611782b963",
1324
+ "b34f1c1ef15b4a07a160b1792eb2ebca",
1325
+ "b369627b8a8f47c297a2a4e67929d3af",
1326
+ "b379e058279844a7b6bc4e7e16476171",
1327
+ "b3fc468b4c4c4e94816cf878f01fb0a4",
1328
+ "b43d9c7d138f49ab9f03ceec47d636b5",
1329
+ "b4621c3f42c04db3ac22ee782a77abdb",
1330
+ "b47899d1db7c4d068c9abb6f78c5f147",
1331
+ "b4dcf459298b43f19ccef6a999ca88ac",
1332
+ "b4e4c397f4f045adbe9999d414fae54c",
1333
+ "b522897d579b4520be476c586a5f14ab",
1334
+ "b587a8c1714c4dca8810ba06b6f65475",
1335
+ "b5ac89e44bd3413495ed6b6ec7fb6837",
1336
+ "b652c476dfeb46f283afb1b471bff652",
1337
+ "b6695c4c642a473ebc870cf303a6b78a",
1338
+ "b66b45a9f46045f7914dc2a5472a7e50",
1339
+ "b67ae7fa7aad4fe0865d0fce24a5ee07",
1340
+ "b6d4f852379f4504b2e7abc28a7dc740",
1341
+ "b6eb96e28f184cf5a8098f68570f2314",
1342
+ "b6fe8b5f49d04d1788829597bb65d410",
1343
+ "b7074ce8f0b34ebb8e11eb1af7a857f3",
1344
+ "b72b882899824e1aa4b7c361654948c9",
1345
+ "b73508fc71c045e7b8895e5b5e13c4a5",
1346
+ "b75ecd7e5126418cb1988a91efb324fa",
1347
+ "b7a85dc6d37a4f06a2275c70f045ce6e",
1348
+ "b7c2b3de4e6a48dda3909a6db64c4ac4",
1349
+ "b7d498a138f343c7910ea169ed1c2a3f",
1350
+ "b836c6e9ffac44f1bba88d57bf225f16",
1351
+ "b8c35388ecd443178c24a5e5cb4c972c",
1352
+ "b8ea5f41413f4c9baba3cbd38bfbc899",
1353
+ "b9057fa941aa41d0be9f20514c1bed24",
1354
+ "b920ba1ce9a14e41a222749c24e5abed",
1355
+ "b93effd9393b44e08b4b89911ff56c0a",
1356
+ "b961b284a61d4347bdbc63399cdcf8cd",
1357
+ "b969d3c8b4fb4222901c3de1ace9cabc",
1358
+ "b96cd6917b974bfe99116ecc288a98cb",
1359
+ "b97e39d020364038aec5809c10729c20",
1360
+ "b9a66b51a6ca4262a6b3d5c8e5f963ec",
1361
+ "b9f15edeb58647c08a9adab01c91683b",
1362
+ "b9fd273288e445bbafe8c149434214ea",
1363
+ "ba09a16bc68e47f28d9900ca2ee70cf8",
1364
+ "ba197f0c7dc147a78005c100a8c67cde",
1365
+ "ba3d56e0c6b843bdacabfcb4b4f90801",
1366
+ "ba5c799fb68b4ac8a1eedfa0586f04f8",
1367
+ "ba6c4234114f4378bf852a5ba12bb7c5",
1368
+ "ba7fbe198ac8410b8b27f81243cacc8a",
1369
+ "ba81bdfa19d54db2a15422341ca8ab87",
1370
+ "baabe69ab1c44cf0b29c147ff0dcedaf",
1371
+ "baad87b0ba9448399d7f72d0fbbc97ff",
1372
+ "bab447d504d94e9691c43ab851e37221",
1373
+ "badc51f2098a4f40a7d166fec9812f4f",
1374
+ "badee623199445cab579b25c535311e0",
1375
+ "bb3d01e7dfd941ecaa8757d59aa560b1",
1376
+ "bb5452b2711c444290834a31996537e1",
1377
+ "bb57ddbb1c2140358b027dfd16b7154b",
1378
+ "bb7483cfa54a47dd856bd529818673c3",
1379
+ "bb8ac243ce464df1af1922599fb3e901",
1380
+ "bb9468613b2f419788f74f94a0820878",
1381
+ "bb9f64ffbff74a0ab4db3d3fe2667388",
1382
+ "bbb693d729264b76a667c2db6d35b2e7",
1383
+ "bbddc0cc8d6a4c9a96a0fc4170594336",
1384
+ "bbe6eb2ff68e46c19f1bd22c1b24dc07",
1385
+ "bc013f0ea12d426d9338d3b437dd5593",
1386
+ "bc0cbd2413a148658db983de5dd1f80b",
1387
+ "bc2ef0701e464672ad8ce2bc3e78f8b3",
1388
+ "bc32d3d924774b3caab10356c1177505",
1389
+ "bc445514ea3c4fd9a316053928cbba12",
1390
+ "bc5030885a5548e595b0a3bea9599354",
1391
+ "bc63ebaf707f4185888bce9c54b6d59c",
1392
+ "bc872d79faa34693adfe6436bb806c39",
1393
+ "bc9c1ec4957b4e32bdbdaf8e70ce9966",
1394
+ "bcdb24f0968c4e1fb7f82154f8adc4ee",
1395
+ "bcfae6acaa254778921933e9ca0f52b9",
1396
+ "bd0065fbb3e745a3907a42b65ef74e23",
1397
+ "bd1b88e868a74e07b2da0512e5041820",
1398
+ "bd5889e7b8ce4c3693e7b58f7de60660",
1399
+ "bd5d5a1fdbc7470fab575738d1741a3d",
1400
+ "bd82374391904aea903d68e22c3b98d8",
1401
+ "bdbcf20dc4e44c34bd6783a025a25dbf",
1402
+ "bdc57a0bf55742edb3a819adb23f52ac",
1403
+ "bdcf5dad9bd4495ea32975f36bb536dd",
1404
+ "bdd3558ddd854a24a090e88595814508",
1405
+ "bdd8a24ad40c4b1facd2d463d2a8dadf",
1406
+ "be162fbb800a41188853844ff0779fc6",
1407
+ "be182bab94004469a07fb9283cd0966f",
1408
+ "be52a68f4f2a409486cf6c0bbceb7f4c",
1409
+ "beaa427961df495c970495d81cc684c3",
1410
+ "becfc582462b432cbd158ada35106a17",
1411
+ "befdd451a67e40698ce02773ca146e13",
1412
+ "bf1cadd21a704739bc19d535ae741728",
1413
+ "bf3e162cdda84afc8c1057bd8ca59b12",
1414
+ "bf429028039d40b1b6da66b27e5cec80",
1415
+ "bf4a3f32b0af40e58653049bdf2265c6",
1416
+ "bf530a7f485c42a2aa650aaa6a3b38f2",
1417
+ "bf859e10b7e74b13b1f76a90b33c13d8",
1418
+ "bfc22926b2f842ab83dc31ce1e95de8f",
1419
+ "bfdb7491cbe04dfd84a1f60bbac3f77e",
1420
+ "c0156044d4834a18a44eb9f01d406bcc",
1421
+ "c0618ebd49724b838060fb70859a23d2",
1422
+ "c063d1a11b0848a5ab12ce20e6f4fb51",
1423
+ "c06cba99d6cd47c6a5b0ad90545f211f",
1424
+ "c072b4097f894f34a2144c6d53c13ade",
1425
+ "c0951700653e4e978ef6199c38d07012",
1426
+ "c098dab6131b4e41a3aff26ab21fb72b",
1427
+ "c0e2a0ad0cf34e4cb8c8695c1470ad47",
1428
+ "c0e3e1f810c446ba943b7ae635846362",
1429
+ "c0f8d797244541948e916984c5b4b79d",
1430
+ "c1130e0746cd41a5b108c5c7a8bbe321",
1431
+ "c11c343241d54c1fbbf2bbc5f0f4f3c4",
1432
+ "c1322815d25c4e2aa50fcb93cd3417f2",
1433
+ "c1338e44401949c1be64e6668d38c100",
1434
+ "c15bff78b50442a39c821330cdd02fd1",
1435
+ "c17e8692f8d141b39ef30ff643163c42",
1436
+ "c1ffea27ec114116951a1d99bdf68aa7",
1437
+ "c205ed51b2554c9ba9fa2c3ccefb8473",
1438
+ "c23e4e9a003a4c1a8e7a12897cb2989b",
1439
+ "c2a66c76348b493196e765361b30b82f",
1440
+ "c2d9dfa3c9f74a54805b6266dffdcac6",
1441
+ "c301fe7e7b5f4ffd9984888ea676ac18",
1442
+ "c3505b11077b4b2d824c33b573fc6e08",
1443
+ "c35eac0ca1274ef7954f80dccf7c7fde",
1444
+ "c38973bee1784c6eb644e28057b1b207",
1445
+ "c3b896134b184ad9a52f542b0ad539da",
1446
+ "c3e41b2565094c56ba594cb9697d0c98",
1447
+ "c3e92af004704cc2b8f2ac99c706208c",
1448
+ "c3edeb36f0ca4dd698b4262abe128bc1",
1449
+ "c3f58532473d4796a9b0de9c0136a0bd",
1450
+ "c433d675d27d489b99db4683f092fda0",
1451
+ "c445e6ac66764779b262014cc97e9192",
1452
+ "c45475ead1024e42b6665339544791cf",
1453
+ "c46a254e08384b23883a570f483ededf",
1454
+ "c4d94ba01c1d44a5a998e09b7dbd52a5",
1455
+ "c4dd2ae8d8ee463a8eda2a8ed770ce34",
1456
+ "c4e9a94ae8d14822a9ec49778868a1d8",
1457
+ "c51100de7bb84e6882f92c9b6ab4b62f",
1458
+ "c51feb09710645bd9df7617f7655f065",
1459
+ "c547abcd2b594a449964b0123e96b2f4",
1460
+ "c54b4c5e6cb64da58d2bc48c7f8a2115",
1461
+ "c56c8aae879b43c292a0f2516b8d3715",
1462
+ "c573303be1f04e0c94cfa245c2f2ddcf",
1463
+ "c5740d905b0143a5831cac15c887bd8a",
1464
+ "c5fdf4ef354b4b6ead18dbfb6e07805c",
1465
+ "c60aa3efb391448faa955426411d485a",
1466
+ "c612eeac87ae4b19a7ade75418a3b42a",
1467
+ "c61a7acb3f7649c786cce53aa80df1fe",
1468
+ "c624b290938c411880ec5254091ab572",
1469
+ "c65995ebe77347ff8a782b3f22cd92b2",
1470
+ "c68f8b1a585a4de6ac0bb133e822b8e5",
1471
+ "c69887e12a164e20bfdc40c5a57c9d77",
1472
+ "c6b5a0a815174bd8bb7410668af0f67d",
1473
+ "c6cf38c5fcf446c6ad9c68f97ea9d924",
1474
+ "c7255f3e4e9d47dca1e66c42c3abf00b",
1475
+ "c7314a1d76c541df8d6723d772ad38c1",
1476
+ "c737dcaa92a74959a6dbf73ce9f6f80c",
1477
+ "c73a0aeaf5184b8d920e26209a381cd6",
1478
+ "c7cc79f24c3046958bb2e3b62fcba583",
1479
+ "c80234658ea34ea4b0fdbcf53f997651",
1480
+ "c84221e0bc74453f96c27c6440081f56",
1481
+ "c885e000d9964d3b89c72d1034414d40",
1482
+ "c8c39f135f324c188448cfa37d63b1a4",
1483
+ "c8d2ae976cec4fcabe1837f1caa40b8e",
1484
+ "c8f071466cd149968a5304b2267f270a",
1485
+ "c99e544549a244ce87938a9e3ab4e0e5",
1486
+ "c9b9c316434547c892e7ac7bde27a74b",
1487
+ "c9dc675a3b9e419da43020b005f2daf8",
1488
+ "c9e8e4f732304503a2f1dab5bd3575a8",
1489
+ "ca4c5fb30d4f4e33a145e0ec21004538",
1490
+ "ca788d3707e74a8eb2a11b4ba3c663fc",
1491
+ "ca98ed893c6e4279a27678a28bf12bca",
1492
+ "ca9ab0992cfa4704a7369bb8a1e354ce",
1493
+ "ca9ba816376147c39bbe86a0e963c2a6",
1494
+ "cae4483da950451dad042eaaa0ec3f71",
1495
+ "caef776ade4249beb2df98a19e640922",
1496
+ "caf3e1a20fec4d85b2b82b28b7c9748a",
1497
+ "cb0e7bf9c2c34681a1c46510298eca2e",
1498
+ "cb4868a7428e4c868112600d4258d467",
1499
+ "cb55690638a24306b5af6829fbf2fd3b",
1500
+ "cbaea52a561a485985187467cc4cebba",
1501
+ "cbdf402976724a82949f655eb2769db2",
1502
+ "cbea02f813454fdb88ddf695dad0ba95",
1503
+ "cc084f03563d4fc8a842cd1b144af160",
1504
+ "cc1b43705a4e41fb93278436dfd19aa7",
1505
+ "cc474a048f2d406fae7ddccc6e33b5e4",
1506
+ "cca59986dfb24be5bb3ef76ef03e6446",
1507
+ "ccb5addc6298440b9387eba77cd96b3f",
1508
+ "ccc764da89ab48f089ec24646b099fd2",
1509
+ "cd75d662c61e47be9b7eed198bbc4653",
1510
+ "cde1c503e1384e45a897ad33621c039c",
1511
+ "ce27aef8f565405ab97345111f56cbcf",
1512
+ "ce35cd13734c47f8b0dbfb1a7ce7d737",
1513
+ "ce4059f8cee64886b4900bae6081a412",
1514
+ "cef43d0b4ee54de191d8d6d1c607c3b4",
1515
+ "cefa7ff3c50d4b599dee919173771f10",
1516
+ "cefe8210e84b41efa52f932dd5bdbbf8",
1517
+ "cf09960a2b76439eac0dad00e36e321d",
1518
+ "cf15f213e497421abf1b0c559b9a7bf1",
1519
+ "cf32f764ec5a43ad9243aec1b7981817",
1520
+ "cf47293c05f1485eaba4437965ec7cb6",
1521
+ "cf4c2238567c42eb8bf5f3a1cf269f26",
1522
+ "cf7dc5fb55214706b2725e900515c9ea",
1523
+ "cf7f3eb8fdbe43b29e4f6ee5a17fe913",
1524
+ "cf96e6948340491c97370f2f75d6a715",
1525
+ "cfa8ae9850724ecc9137297a6cf48661",
1526
+ "cfb21be3208142e58302a954bb258638",
1527
+ "cfc48976f7474878bd2236b2e9fcbcfa",
1528
+ "cfd1a14262c9452a98265ff51df738da",
1529
+ "d01a7b47c5634beca150febdc9eb3804",
1530
+ "d023021664b14066ba2091b46796d48a",
1531
+ "d0296bd8998b4d64a156ec3614194cf8",
1532
+ "d02ba46d170143e58ec7f9e4c7edb09c",
1533
+ "d03d0eddd62b49e39b6916825fe2cc4a",
1534
+ "d081cded74804470bc3b0874cbeae248",
1535
+ "d0a9268f7fb24dcd8f77c66f2fee33d9",
1536
+ "d0e93f7b8717493b959d797cad7a7051",
1537
+ "d124e83b3bb243488241b8ac07f6ba3b",
1538
+ "d143036cc7cd4810aa1b135ce4b75186",
1539
+ "d1563c5e2d29424fb1ce03e03a4f0e2c",
1540
+ "d16736e243904210b938590a42c8ea88",
1541
+ "d18a91b5578c4e6fa9e8fa0f4f892655",
1542
+ "d18c53e5716941a79385dc1b7a20aa33",
1543
+ "d19600dfb01044819f0e2ad57c5b144f",
1544
+ "d1b8af21f2a7463a800dd21828143d73",
1545
+ "d1feaa811a43422eb9918abc60b6d03e",
1546
+ "d2134da7512c4a4ea3acb5efc038939c",
1547
+ "d21cf60625aa40aa984bc52a5c1d28b6",
1548
+ "d25283e6dc7c4af9858db4b33b2cd898",
1549
+ "d263b96494e046c3a7721039d52c70bd",
1550
+ "d281b292903d422aa0cef4e97f47c516",
1551
+ "d2840479d3764edaaec419baa528b686",
1552
+ "d29681d43409491ca48a1a4daa08c9c0",
1553
+ "d2ad89ca538043b5a705578f7d63e26b",
1554
+ "d2bca193811341a9b6626fb9440e1483",
1555
+ "d2c42715bc8f4ac793365a9e018a9154",
1556
+ "d2ec14c4188e4cbe9bf8f649d54e9026",
1557
+ "d341a70ad72d4aef890642bf4b351164",
1558
+ "d370a3eb0a164dc19ba4547a89956599",
1559
+ "d3df4b71522a4af492642fd8b4188bb2",
1560
+ "d446483c0dc44723a520d1dc5019ef41",
1561
+ "d44d57497d0e49378dc6cc030a3a795c",
1562
+ "d468bad5f0da414b80a7a10e0ce06963",
1563
+ "d4879dcacecd4e31841b6f85853059e3",
1564
+ "d4afdbf2942b4d028c8e5ae678748618",
1565
+ "d50988a3159f488193d45c33e1bc42d2",
1566
+ "d54709facd344eb68b60778618ee30b1",
1567
+ "d54b3349d9454dce842ba7cb29d2f55d",
1568
+ "d557afd3c789449a81deea08cf8b38e6",
1569
+ "d5658d7c19b54a7eb651fb90f49a1eb1",
1570
+ "d576f209de3448d9992b1e3475bef5ff",
1571
+ "d5879bee6d804e06b91eac1ab17c0865",
1572
+ "d5af027ff24a49478cfdb02ae813636e",
1573
+ "d5d862bcfd4441c98daeb64b81f478e5",
1574
+ "d604adba8218419984863c94d55e179a",
1575
+ "d645572faab04171a80b508787ce2e94",
1576
+ "d665b19e9b27446abffefed954dc08bd",
1577
+ "d67c689abb49402dbd5531e658e1228a",
1578
+ "d68520a7d7fd457da9abafec6b55bce6",
1579
+ "d6993387c8fc49c49ef427a5e43556dd",
1580
+ "d6c3ff1f15344a31b1120f48fd2718c5",
1581
+ "d6c53cc6b38d47b4bd44c1355151fa8a",
1582
+ "d6ee29b78ed54a51b99069dfb50f23ae",
1583
+ "d739778fc14a4edcafaa62e0d8722f2e",
1584
+ "d74022bfc72f452bbed363059bbb79d6",
1585
+ "d76d9d2174a14235af9ec89b6fbe2f37",
1586
+ "d7d6e3e059e2492188d7bb7b17626a45",
1587
+ "d7df7692001a427caee08f0997f52316",
1588
+ "d7ff32605636455a813c60c77e7817ba",
1589
+ "d831ace59eea441f971547b991d0b6ce",
1590
+ "d85e6f53c445408bab71e9c68eb59140",
1591
+ "d86759c3aa944793bd8e79a6339a7342",
1592
+ "d892496c52524e0ebfe1db7482c3c3af",
1593
+ "d896c972fc894772b47748fbbbf4b5eb",
1594
+ "d89e46167ef045bb8c2d279dc2b51ff7",
1595
+ "d8bc73eb1ead46fb8594b8caf926bda7",
1596
+ "d961bc05652f41b1b90f9358f69eb23a",
1597
+ "d9b5a2c032244ccf8ee63bd26ed8bb2b",
1598
+ "d9e3f302b8ef4b0297cbe4a042b1887c",
1599
+ "d9ecc99108a643ab887296cb986be937",
1600
+ "da256042b24f41469b605ec24f027301",
1601
+ "da26fbe1b21340cca9d2e31f4c70571f",
1602
+ "da4e172fddde4d2d8b2072aa6560ed6d",
1603
+ "da6ab3a8f5554d6ca485bfb5698fabe9",
1604
+ "da8b73929a5843e185c627bc675c730a",
1605
+ "da8bbcb226fc4b2a862e94a4c5ce1930",
1606
+ "da8fcc845fdf4227839784102dc182e7",
1607
+ "da97043fbee64b1d92c5406ef80a5ea9",
1608
+ "db49d1d472704b19836ab1a8aeb09d58",
1609
+ "db80c36bf818475bb43f84cc015d7803",
1610
+ "db9bc936d89d4f63aaf2f58c8048eec4",
1611
+ "dba737640da74746afa8189dfc867d98",
1612
+ "dbf9c15333814afa88345312b521ddab",
1613
+ "dbfd139a36ef42e59392c8a9cd878986",
1614
+ "dc436f07d85146a49747ee01e4259049",
1615
+ "dc4c91abf45342b4bb8822f50fa162b2",
1616
+ "dc5edb6be1c14fb1b5484541ae3aa673",
1617
+ "dd6ccc2389454afda4a5d34c8b6ce4d5",
1618
+ "ddabe7f3825643048541f8d570560c36",
1619
+ "ddf2459697d54117a2d0ac499f7fdb6b",
1620
+ "de522f0520794e8ba79361b235a08bd1",
1621
+ "de5b614cdb2042e29fcf0f848b26af21",
1622
+ "de8616a79cb74d9d8618c91ce254c9a6",
1623
+ "dec9e92b1a86422c8df32c835704387c",
1624
+ "df0511efc31b431daebe5825eefcc12d",
1625
+ "df14fec21675475c871568b7b89bac22",
1626
+ "df224818be2946f69e1cac929504b07b",
1627
+ "df232d48ce254cb8bf156b20dd096e16",
1628
+ "df261dc53ac1410f9cd462d6de578f5d",
1629
+ "df9f511762f44544a37799e18dce3f83",
1630
+ "dfa463836cdd48b4bb4caa3f868ed07a",
1631
+ "dfd3fab648834cd586230ecde5d80498",
1632
+ "dfd734a9ae4343fc8f572b11fe09ead3",
1633
+ "dfffd398b6cc4e14bb76aa73fc1bda33",
1634
+ "e02091a7f05d4a78a8a7ee07054c68f0",
1635
+ "e05d63a78f2c44c5a64fbc419998e603",
1636
+ "e06a0ddd061b4919a33868e84e0bb59c",
1637
+ "e06d06101cc8434e91034b97211d5dd5",
1638
+ "e088a1fc5a67448589f15e78517ebd39",
1639
+ "e0c7ddea147e4843a3f69d78362be91d",
1640
+ "e0ea25464d2442a183fd1d16b31d0d6d",
1641
+ "e1083bbb88ac4e55b31613f98bc39aee",
1642
+ "e108c545e1194c36841e43a32b007312",
1643
+ "e13434a196e842b3980aef22835e4ecb",
1644
+ "e16778a84a054a68a93c0a843ceaca9c",
1645
+ "e16ed5283fd1443da96d30b14fbe26a3",
1646
+ "e1773e5177454abea4410b442cd51bc3",
1647
+ "e17e25c330704fb9b75a14470d1602b2",
1648
+ "e19e12c46c5849feb4559bf12fb1a1d0",
1649
+ "e1afbb66a7974c2ca15efa9186af729c",
1650
+ "e1b76ff0ea3046dfbe81a05c61f28ef2",
1651
+ "e1bc7fd65d7b4a5b85624fc9cb483993",
1652
+ "e1e2705f28eb468f8995be58cc59e180",
1653
+ "e1eeec54327545438376e773dfbb95a3",
1654
+ "e204b769571444e5916c3d7ca67a4219",
1655
+ "e21fdbef2bca4ef5a46d2f0a523b530a",
1656
+ "e24ee911ddac4ab9b2869913c6a8e969",
1657
+ "e2e4bb23e9214105879b033692377580",
1658
+ "e311b9100c564c599b64c98d0ceb879b",
1659
+ "e317dd0031794072a86e1e9fb7fb0f3d",
1660
+ "e33e35f643334b8fa5861c14fde1edc1",
1661
+ "e35dab813fe34035b66ac15989b1e2be",
1662
+ "e378396052674803a8990bd1d8c0b04f",
1663
+ "e38c85b66b974f769446c8dbb0e15c2e",
1664
+ "e39ec3c45acc40b79db8bf133e1dfc77",
1665
+ "e3a23079ea72443f9b469806d4ddb002",
1666
+ "e3aca5ecbbdf4d3585c3e241faf97206",
1667
+ "e3c0b2df930b48f5bc1fa5d38155e54d",
1668
+ "e3df35afea5d4f3fa8ec2c0b9574bcd8",
1669
+ "e3fa8eda5cd648f8b3b2a88189fe3e2f",
1670
+ "e406259aefca4cb8844e55a42ea45c87",
1671
+ "e4087350c4d84d8391de09b405c96488",
1672
+ "e4158753fe574a86ac7f457179db6ecf",
1673
+ "e43ed4c1b7254bedb8ec34ea8bbf341a",
1674
+ "e4656569bcb44476b5d8676724c22b6f",
1675
+ "e47be5f609ef415f97b51b264cd16259",
1676
+ "e4cfa0c54f474f918f40277fe2bbacfb",
1677
+ "e4ec75222bb44422bf0b7c90453b7f94"
1678
+ ]
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/default.yaml ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===== MC-diffusion Training Config (SD3.5 + mcVAE) =====
2
+
3
+ seed: 43
4
+
5
+ project:
6
+ output_dir: outputs/mcdiff
7
+ logging_dir: logs
8
+ null_pe: precomputed_tensors/null_prompt_embeds.pt
9
+ null_ppe: precomputed_tensors/null_pooled_prompt_embeds.pt
10
+
11
+ data:
12
+ train:
13
+ root: /home/aaaaa/data/rgbmr_mv_web2/merged
14
+ num_workers: 2
15
+ batch_size: 4
16
+ val:
17
+ root: /home/aaaaa/data/rgbmr_mv_web2/validation
18
+
19
+ model:
20
+ pretrained_model_name_or_path: stabilityai/stable-diffusion-3.5-medium
21
+ init_transformer_weights: null
22
+ sd_vae_path: vae_sd35
23
+ mcvae_config: configs/mcvae/config.json
24
+ mcvae_ckpt: null
25
+ mcvae_offset_mode: true # If true, mcVAE encoder outputs offsets added to base mean/logvar. If false, directly predicts mean/logvar.
26
+ num_views: 6
27
+ use_dual_branch: false
28
+ use_caa: true
29
+ use_rope: true
30
+ use_global_token: false
31
+ use_global_pos: false # If true, use global frame (identity) instead of local query frame (w2c) for delta position
32
+ resolution: 512
33
+ condition_channels: 32 # 0 | 16 | 32
34
+ corr_dilate_iterations: 2 # Number of times to apply dilate_f2l to correspondence tensors
35
+
36
+ attn_lora:
37
+ enabled: true
38
+ r_q: 32
39
+ r_k: 32
40
+ r_v: 32
41
+ alpha_q: 32
42
+ alpha_k: 32
43
+ alpha_v: 32
44
+ apply_to_self: true
45
+ limit_joint: null
46
+ limit_self: null
47
+
48
+ lora:
49
+ rank: 0
50
+ lora_layers: null
51
+ lora_blocks: null
52
+ lora_dropout: 0.0
53
+
54
+ train:
55
+ max_train_steps: 40001
56
+ gradient_accumulation_steps: 1
57
+ gradient_checkpointing: false
58
+ allow_tf32: true
59
+ mixed_precision: bf16
60
+ upcast_before_saving: false
61
+ local_rank: -1
62
+ resume_from_checkpoint: null
63
+ checkpointing_steps: 1000
64
+ checkpoints_total_limit: 40
65
+ validation_steps: 1000
66
+ proportion_empty_prompts: 0.1
67
+ max_grad_norm: 1.0
68
+
69
+ optim:
70
+ optimizer: AdamW
71
+ use_8bit_adam: true
72
+ learning_rate: 5e-5
73
+ scale_lr: false
74
+ adam_beta1: 0.9
75
+ adam_beta2: 0.999
76
+ adam_weight_decay: 1e-4
77
+ adam_epsilon: 1e-8
78
+ prodigy_beta3: null
79
+ prodigy_decouple: true
80
+ prodigy_use_bias_correction: true
81
+ prodigy_safeguard_warmup: true
82
+
83
+ scheduler:
84
+ lr_scheduler: constant
85
+ lr_warmup_steps: 500
86
+ lr_num_cycles: 5
87
+ lr_power: 1.0
88
+
89
+ weighting:
90
+ weighting_scheme: logit_normal
91
+ logit_mean: 0.0
92
+ logit_std: 1.0
93
+ mode_scale: 1.29
94
+ precondition_outputs: false
95
+
96
+
97
+ # For Single-Full: (42hr)
98
+ # accelerate launch --num_processes 8 --main_process_port 0 train_mcdiff.py \
99
+ # train.max_train_steps=40001 data.train.batch_size=4 \
100
+ # model.mcvae_ckpt="outputs/mcvae_v1.8.1.pt" \
101
+ # project.output_dir=outputs/mcdiff_v.single_full \
102
+ # model.use_dual_branch=false model.use_caa=false
103
+
104
+ # For Single-CAA(5x5): (42hr)
105
+ # accelerate launch --num_processes 8 --main_process_port 0 train_mcdiff.py \
106
+ # train.max_train_steps=40001 data.train.batch_size=4 \
107
+ # model.mcvae_ckpt="outputs/mcvae_v1.8.1.pt" \
108
+ # project.output_dir=outputs/mcdiff_v.single_caa \
109
+ # model.use_dual_branch=false model.use_caa=true
110
+
111
+ # For Single-Full-Global(3x3): (36hr)
112
+ # accelerate launch --num_processes 8 --main_process_port 0 train_mcdiff.py \
113
+ # train.max_train_steps=40001 data.train.batch_size=4 \
114
+ # model.mcvae_ckpt="outputs/mcvae_v1.8.1.pt" \
115
+ # project.output_dir=outputs/mcdiff_v.single_caa_global \
116
+ # model.use_dual_branch=false model.use_caa=true model.use_global_token=true
117
+
118
+ # For Dual-Full: (72hr)
119
+ # accelerate launch --num_processes 8 --main_process_port 0 train_mcdiff.py \
120
+ # train.max_train_steps=40001 data.train.batch_size=1 train.gradient_accumulation_steps=4 \
121
+ # model.mcvae_ckpt="outputs/mcvae_v1.8.1.pt" \
122
+ # project.output_dir=outputs/mcdiff_v.dual_full \
123
+ # model.use_dual_branch=true model.use_caa=false
124
+
125
+ # For Dual-CAA: (72hr)
126
+ # accelerate launch --num_processes 8 --main_process_port 0 train_mcdiff.py \
127
+ # train.max_train_steps=40001 data.train.batch_size=1 train.gradient_accumulation_steps=4 \
128
+ # model.mcvae_ckpt="outputs/mcvae_v1.8.1.pt" \
129
+ # project.output_dir=outputs/mcdiff_v.dual_caa \
130
+ # model.use_dual_branch=true model.use_caa=true
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/dual_caa.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.dual_caa
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 1
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: true
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ max_train_steps: 80001
22
+ gradient_accumulation_steps: 4
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/dual_full.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.dual_full
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 1
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: true
14
+ use_caa: false
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 4
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_caa
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 4
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 1
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_100k_vae.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_caa_100k_vae
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 4
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1_100k.pt"
13
+ use_dual_branch: false
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 1
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_global_pos.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_caa_global_pos
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 4
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: true
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 1
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_global_token.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_caa_global_token
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 4
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: true
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 1
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caa_tmp.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_caa_tmp
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 4
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 1
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_caaa.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_caaa
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 4
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: true
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 1
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_full.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_full
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 2
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: false
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 2
home/ubuntu/aaaaa/data/rgbmr/configs/mcdiff/single_fulll.yaml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ base: configs/mcdiff/default.yaml
2
+
3
+ project:
4
+ output_dir: outputs/mcdiff_v.single_fulll
5
+
6
+ data:
7
+ train:
8
+ num_workers: 4
9
+ batch_size: 2
10
+
11
+ model:
12
+ mcvae_ckpt: "outputs/mcvae_v1.8.1.pt"
13
+ use_dual_branch: false
14
+ use_caa: false
15
+ use_rope: true
16
+ use_global_token: false
17
+ use_global_pos: false
18
+ corr_dilate_iterations: 2
19
+
20
+ train:
21
+ gradient_accumulation_steps: 2
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "AutoencoderKL",
3
+ "_diffusers_version": "0.31.0.dev0",
4
+ "_name_or_path": "../sdxl-vae/",
5
+ "act_fn": "silu",
6
+ "block_out_channels": [
7
+ 128,
8
+ 256,
9
+ 512,
10
+ 512
11
+ ],
12
+ "down_block_types": [
13
+ "DownEncoderBlock2D",
14
+ "DownEncoderBlock2D",
15
+ "DownEncoderBlock2D",
16
+ "DownEncoderBlock2D"
17
+ ],
18
+ "force_upcast": false,
19
+ "in_channels": 5,
20
+ "latent_channels": 16,
21
+ "latents_mean": null,
22
+ "latents_std": null,
23
+ "layers_per_block": 2,
24
+ "mid_block_add_attention": true,
25
+ "norm_num_groups": 32,
26
+ "out_channels": 5,
27
+ "sample_size": 1024,
28
+ "scaling_factor": null,
29
+ "shift_factor": null,
30
+ "up_block_types": [
31
+ "UpDecoderBlock2D",
32
+ "UpDecoderBlock2D",
33
+ "UpDecoderBlock2D",
34
+ "UpDecoderBlock2D"
35
+ ],
36
+ "use_post_quant_conv": false,
37
+ "use_quant_conv": false
38
+ }
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/default.yaml ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # ===== Multi-Channel VAE Training Config (SD3.5 VAE, z=16) =====
3
+
4
+ seed: 43
5
+ device: cuda
6
+
7
+ project:
8
+ name: MCVAE_revised
9
+ run_name: MCVAE_v1.1.0
10
+ notes: "SD3.5-like (z=16)"
11
+ save_dir: ./outputs/${project.run_name}
12
+
13
+ data:
14
+ train:
15
+ root: /scratch/aaaaa/data/rgbmr_mv_web2/merged
16
+ num_workers: 2
17
+ batch_size: 8
18
+ val:
19
+ root: /scratch/aaaaa/data/rgbmr_mv_web2/validation_mcvae
20
+ num_workers: 1
21
+ batch_size: 6
22
+
23
+ model:
24
+ resolution: 512
25
+ image_range: [-1.0, 1.0]
26
+ mcvae_config: ./configs/mcvae/config.json
27
+
28
+ init_layers_from: null # path to checkpoint to init layers from, or null to train from sd vae
29
+ init_layers_strict: false
30
+
31
+ offset_mode: true # If true, encoder outputs offsets added to base mean/logvar. If false, directly predicts mean/logvar.
32
+ deterministic_mode: false # If true, encoder outputs deterministic latents (logvar=-30, Dirac delta). KL terms auto-disabled.
33
+
34
+ sd_vae:
35
+ from_pretrained: ./vae_sd35
36
+ dtype: bfloat16
37
+
38
+ latent_transform:
39
+ type: scaling+crop+rotation # identity | scaling | crop | rotation | ...
40
+ identity_prob: 0.5 # Probability of NOT applying augmentation
41
+ tiebreaking_prob: 0.3 # When both scaling and crop are enabled, prob of choosing scaling
42
+ scaling_range: [16, 64] # Min and max for 64-based scaling
43
+ crop_range: [3, 32] # Min and max for 64-based crop
44
+
45
+ loss:
46
+ disc_start: 40001
47
+ disc_num_layers: 5
48
+ disc_in_channels: 5
49
+ disc_loss: hinge
50
+ disc_factor: 1.0
51
+ base_recon_weight: 3.0
52
+ disc_weight: 0.02
53
+ recon_loss_type: mse
54
+ perceptual_rgb_weight: 0.24 # should be 3/5 * 0.4
55
+ perceptual_orm_weight: 0.16 # should be 2/5 * 0.4
56
+ orm_loss_type: lpips_per_channel
57
+ orm_loss_params: {}
58
+ use_identity_loss: false # If true, adds identity loss: L2(vae.decode(mcvae.encode(x)), x[:,:3])
59
+
60
+ optim:
61
+ lr: 3.0e-5
62
+ betas: [0.9, 0.99]
63
+ weight_decay: 0.0
64
+ use_8bit_adam: true
65
+
66
+ trainer:
67
+ max_steps: 400001 # double counted by lightning, so 200k = 400001
68
+ val_check_interval: 750
69
+ log_every_n_steps: 50
70
+ limit_val_batches: 128
71
+ precision: bf16-mixed
72
+ accumulate_grad_batches: 1
73
+ gradient_clip_val: 0.0
74
+ devices: -1
75
+ accelerator: gpu
76
+ num_sanity_val_steps: 1
77
+
78
+ logging:
79
+ image_log_every_n_val_epochs: 1
80
+ n_log_images: 6
81
+ save_top_k: 5
82
+ monitor: val/recon_loss
83
+ mode: min
84
+ layers_save_every_n_steps: 10000
85
+ layers_weights_filename: layer_weights.pt
86
+ train_image_every_n_steps: 400001
87
+
88
+ lambdas:
89
+ lambda_recon: 1.0
90
+ lambda_kl: 1e-6
91
+ lambda_kl_base: 3e-9
92
+ lambda_identity: 3.0 # Weight for identity loss (only used if loss.use_identity_loss=true)
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/layerdiffuse.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ base: configs/mcvae/default.yaml
3
+
4
+ project:
5
+ name: MCVAE_FINAL
6
+ run_name: MCVAE_v.layerdiffuse
7
+
8
+ data:
9
+ train:
10
+ root: /home/ubuntu/aaaaa/data/rgbmr/merged
11
+ batch_size: 8
12
+ val:
13
+ root: /home/ubuntu/aaaaa/data/rgbmr/validation_mcvae
14
+
15
+ model:
16
+ offset_mode: true
17
+ deterministic_mode: true # Enable deterministic mode (logvar=-30, Dirac delta, KL auto-disabled)
18
+
19
+ latent_transform:
20
+ type: scaling+crop+rotation # identity | scaling | crop | rotation | ...
21
+ identity_prob: 0.5
22
+
23
+ loss:
24
+ use_identity_loss: true # If true, adds identity loss: L2(vae.decode(mcvae.encode(x)), x[:,:3])
25
+
26
+ trainer:
27
+ max_steps: 400001 # double counted by lightning, so 200k = 400001
28
+ accumulate_grad_batches: 1
29
+
30
+ lambdas:
31
+ lambda_recon: 1.0
32
+ lambda_kl: 0.0 # Auto-disabled by deterministic_mode
33
+ lambda_kl_base: 0.0 # Auto-disabled by deterministic_mode
34
+ lambda_identity: 3.0
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/no_crop.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ base: configs/mcvae/default.yaml
3
+
4
+ project:
5
+ name: MCVAE_FINAL
6
+ run_name: MCVAE_v.no_crop
7
+
8
+ data:
9
+ train:
10
+ root: /home/aaaaa/data/rgbmr_mv_web2/merged
11
+ batch_size: 8
12
+ val:
13
+ root: /home/aaaaa/data/rgbmr_mv_web2/validation_mcvae
14
+
15
+ model:
16
+ offset_mode: true
17
+
18
+ latent_transform:
19
+ type: identity # identity | scaling | crop | rotation | ...
20
+ identity_prob: 1.0
21
+
22
+ trainer:
23
+ max_steps: 400001 # double counted by lightning, so 200k = 400001
24
+ accumulate_grad_batches: 1
25
+
26
+ lambdas:
27
+ lambda_recon: 1.0
28
+ lambda_kl: 1e-6
29
+ lambda_kl_base: 3e-9
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/orchid.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ base: configs/mcvae/default.yaml
3
+
4
+ project:
5
+ name: MCVAE_FINAL
6
+ run_name: MCVAE_v.orchid
7
+
8
+ data:
9
+ train:
10
+ root: /home/ubuntu/aaaaa/data/rgbmr/merged
11
+ batch_size: 8
12
+ val:
13
+ root: /home/ubuntu/aaaaa/data/rgbmr/validation_mcvae
14
+
15
+ model:
16
+ offset_mode: false
17
+
18
+ latent_transform:
19
+ type: scaling+crop+rotation # identity | scaling | crop | rotation | ...
20
+ identity_prob: 0.5
21
+
22
+ trainer:
23
+ max_steps: 400001 # double counted by lightning, so 200k = 400001
24
+ accumulate_grad_batches: 1
25
+
26
+ lambdas:
27
+ lambda_recon: 1.0
28
+ lambda_kl: 1e-6
29
+ lambda_kl_base: 3e-9
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/ours.yaml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ base: configs/mcvae/default.yaml
3
+
4
+ project:
5
+ name: MCVAE_FINAL
6
+ run_name: MCVAE_v.ours
7
+
8
+ data:
9
+ train:
10
+ root: /home/aaaaa/data/rgbmr_mv_web2/merged
11
+ batch_size: 8
12
+ val:
13
+ root: /home/aaaaa/data/rgbmr_mv_web2/validation_mcvae
14
+
15
+ model:
16
+ offset_mode: true
17
+
18
+ latent_transform:
19
+ type: scaling+crop+rotation # identity | scaling | crop | rotation | ...
20
+ identity_prob: 0.5
21
+
22
+ trainer:
23
+ max_steps: 400001 # double counted by lightning, so 200k = 400001
24
+ accumulate_grad_batches: 1
25
+
26
+ lambdas:
27
+ lambda_recon: 1.0
28
+ lambda_kl: 1e-6
29
+ lambda_kl_base: 3e-9
home/ubuntu/aaaaa/data/rgbmr/configs/mcvae/variant_example.yaml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Example variant config that inherits from default.yaml
2
+ # This demonstrates the base-variant inheritance structure
3
+
4
+ base: configs/mcvae/default.yaml
5
+
6
+ # Override specific parameters
7
+ project:
8
+ run_name: MCVAE_v1.1.0_variant_example
9
+ notes: "Example variant configuration"
10
+
11
+ # Override training parameters
12
+ trainer:
13
+ max_steps: 50001
14
+ val_check_interval: 500
15
+
16
+ # Override optimizer parameters
17
+ optim:
18
+ lr: 1.0e-4
19
+
20
+ # Override data parameters
21
+ data:
22
+ train:
23
+ batch_size: 6
24
+ num_workers: 4
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/custom_rasterizer/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """
2
+ from .render import rasterize, interpolate
3
+ """
4
+ from .render import *
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/custom_rasterizer/render.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import custom_rasterizer_kernel
2
+ import torch
3
+
4
+
5
+ def rasterize(pos, tri, resolution, clamp_depth=torch.zeros(0), use_depth_prior=0):
6
+ assert pos.device == tri.device
7
+ findices, barycentric = custom_rasterizer_kernel.rasterize_image(
8
+ pos[0], tri, clamp_depth, resolution[1], resolution[0], 1e-6, use_depth_prior
9
+ )
10
+ return findices, barycentric
11
+
12
+
13
+ def interpolate(col, findices, barycentric, tri):
14
+ f = findices - 1 + (findices == 0)
15
+ vcol = col[0, tri.long()[f.long()]]
16
+ result = barycentric.view(*barycentric.shape, 1) * vcol
17
+ result = torch.sum(result, axis=-2)
18
+ return result.view(1, *result.shape)
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/__init__.py ADDED
File without changes
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/grid_neighbor.cpp ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "rasterizer.h"
2
+ #include <fstream>
3
+
4
+ inline int pos2key(float* p, int resolution) {
5
+ int x = (p[0] * 0.5 + 0.5) * resolution;
6
+ int y = (p[1] * 0.5 + 0.5) * resolution;
7
+ int z = (p[2] * 0.5 + 0.5) * resolution;
8
+ return (x * resolution + y) * resolution + z;
9
+ }
10
+
11
+ inline void key2pos(int key, int resolution, float* p) {
12
+ int x = key / resolution / resolution;
13
+ int y = key / resolution % resolution;
14
+ int z = key % resolution;
15
+ p[0] = ((x + 0.5) / resolution - 0.5) * 2;
16
+ p[1] = ((y + 0.5) / resolution - 0.5) * 2;
17
+ p[2] = ((z + 0.5) / resolution - 0.5) * 2;
18
+ }
19
+
20
+ inline void key2cornerpos(int key, int resolution, float* p) {
21
+ int x = key / resolution / resolution;
22
+ int y = key / resolution % resolution;
23
+ int z = key % resolution;
24
+ p[0] = ((x + 0.75) / resolution - 0.5) * 2;
25
+ p[1] = ((y + 0.25) / resolution - 0.5) * 2;
26
+ p[2] = ((z + 0.75) / resolution - 0.5) * 2;
27
+ }
28
+
29
+ inline float* pos_ptr(int l, int i, int j, torch::Tensor t) {
30
+ float* pdata = t.data_ptr<float>();
31
+ int height = t.size(1);
32
+ int width = t.size(2);
33
+ return &pdata[((l * height + i) * width + j) * 4];
34
+ }
35
+
36
+ struct Grid
37
+ {
38
+ std::vector<int> seq2oddcorner;
39
+ std::vector<int> seq2evencorner;
40
+ std::vector<int> seq2grid;
41
+ std::vector<int> seq2normal;
42
+ std::vector<int> seq2neighbor;
43
+ std::unordered_map<int, int> grid2seq;
44
+ std::vector<int> downsample_seq;
45
+ int num_origin_seq;
46
+ int resolution;
47
+ int stride;
48
+ };
49
+
50
+ inline void pos_from_seq(Grid& grid, int seq, float* p) {
51
+ auto k = grid.seq2grid[seq];
52
+ key2pos(k, grid.resolution, p);
53
+ }
54
+
55
+ inline int fetch_seq(Grid& grid, int l, int i, int j, torch::Tensor pdata) {
56
+ float* p = pos_ptr(l, i, j, pdata);
57
+ if (p[3] == 0)
58
+ return -1;
59
+ auto key = pos2key(p, grid.resolution);
60
+ int seq = grid.grid2seq[key];
61
+ return seq;
62
+ }
63
+
64
+ inline int fetch_last_seq(Grid& grid, int i, int j, torch::Tensor pdata) {
65
+ int num_layers = pdata.size(0);
66
+ int l = 0;
67
+ int idx = fetch_seq(grid, l, i, j, pdata);
68
+ while (l < num_layers - 1) {
69
+ l += 1;
70
+ int new_idx = fetch_seq(grid, l, i, j, pdata);
71
+ if (new_idx == -1)
72
+ break;
73
+ idx = new_idx;
74
+ }
75
+ return idx;
76
+ }
77
+
78
+ inline int fetch_nearest_seq(Grid& grid, int i, int j, int dim, float d, torch::Tensor pdata) {
79
+ float p[3];
80
+ float max_dist = 1e10;
81
+ int best_idx = -1;
82
+ int num_layers = pdata.size(0);
83
+ for (int l = 0; l < num_layers; ++l) {
84
+ int idx = fetch_seq(grid, l, i, j, pdata);
85
+ if (idx == -1)
86
+ break;
87
+ pos_from_seq(grid, idx, p);
88
+ float dist = std::abs(d - p[(dim + 2) % 3]);
89
+ if (dist < max_dist) {
90
+ max_dist = dist;
91
+ best_idx = idx;
92
+ }
93
+ }
94
+ return best_idx;
95
+ }
96
+
97
+ inline int fetch_nearest_seq_layer(Grid& grid, int i, int j, int dim, float d, torch::Tensor pdata) {
98
+ float p[3];
99
+ float max_dist = 1e10;
100
+ int best_layer = -1;
101
+ int num_layers = pdata.size(0);
102
+ for (int l = 0; l < num_layers; ++l) {
103
+ int idx = fetch_seq(grid, l, i, j, pdata);
104
+ if (idx == -1)
105
+ break;
106
+ pos_from_seq(grid, idx, p);
107
+ float dist = std::abs(d - p[(dim + 2) % 3]);
108
+ if (dist < max_dist) {
109
+ max_dist = dist;
110
+ best_layer = l;
111
+ }
112
+ }
113
+ return best_layer;
114
+ }
115
+
116
+ void FetchNeighbor(Grid& grid, int seq, float* pos, int dim, int boundary_info, std::vector<torch::Tensor>& view_layer_positions,
117
+ int* output_indices)
118
+ {
119
+ auto t = view_layer_positions[dim];
120
+ int height = t.size(1);
121
+ int width = t.size(2);
122
+ int top = 0;
123
+ int ci = 0;
124
+ int cj = 0;
125
+ if (dim == 0) {
126
+ ci = (pos[1]/2+0.5)*height;
127
+ cj = (pos[0]/2+0.5)*width;
128
+ }
129
+ else if (dim == 1) {
130
+ ci = (pos[1]/2+0.5)*height;
131
+ cj = (pos[2]/2+0.5)*width;
132
+ }
133
+ else {
134
+ ci = (-pos[2]/2+0.5)*height;
135
+ cj = (pos[0]/2+0.5)*width;
136
+ }
137
+ int stride = grid.stride;
138
+ for (int ni = ci + stride; ni >= ci - stride; ni -= stride) {
139
+ for (int nj = cj - stride; nj <= cj + stride; nj += stride) {
140
+ int idx = -1;
141
+ if (ni == ci && nj == cj)
142
+ idx = seq;
143
+ else if (!(ni < 0 || ni >= height || nj < 0 || nj >= width)) {
144
+ if (boundary_info == -1)
145
+ idx = fetch_seq(grid, 0, ni, nj, t);
146
+ else if (boundary_info == 1)
147
+ idx = fetch_last_seq(grid, ni, nj, t);
148
+ else
149
+ idx = fetch_nearest_seq(grid, ni, nj, dim, pos[(dim + 2) % 3], t);
150
+ }
151
+ output_indices[top] = idx;
152
+ top += 1;
153
+ }
154
+ }
155
+ }
156
+
157
+ void DownsampleGrid(Grid& src, Grid& tar)
158
+ {
159
+ src.downsample_seq.resize(src.seq2grid.size(), -1);
160
+ tar.resolution = src.resolution / 2;
161
+ tar.stride = src.stride * 2;
162
+ float pos[3];
163
+ std::vector<int> seq2normal_count;
164
+ for (int i = 0; i < src.seq2grid.size(); ++i) {
165
+ key2pos(src.seq2grid[i], src.resolution, pos);
166
+ int k = pos2key(pos, tar.resolution);
167
+ int s = seq2normal_count.size();
168
+ if (!tar.grid2seq.count(k)) {
169
+ tar.grid2seq[k] = tar.seq2grid.size();
170
+ tar.seq2grid.emplace_back(k);
171
+ seq2normal_count.emplace_back(0);
172
+ seq2normal_count.emplace_back(0);
173
+ seq2normal_count.emplace_back(0);
174
+ //tar.seq2normal.emplace_back(src.seq2normal[i]);
175
+ } else {
176
+ s = tar.grid2seq[k] * 3;
177
+ }
178
+ seq2normal_count[s + src.seq2normal[i]] += 1;
179
+ src.downsample_seq[i] = tar.grid2seq[k];
180
+ }
181
+ tar.seq2normal.resize(seq2normal_count.size() / 3);
182
+ for (int i = 0; i < seq2normal_count.size(); i += 3) {
183
+ int t = 0;
184
+ for (int j = 1; j < 3; ++j) {
185
+ if (seq2normal_count[i + j] > seq2normal_count[i + t])
186
+ t = j;
187
+ }
188
+ tar.seq2normal[i / 3] = t;
189
+ }
190
+ }
191
+
192
+ void NeighborGrid(Grid& grid, std::vector<torch::Tensor> view_layer_positions, int v)
193
+ {
194
+ grid.seq2evencorner.resize(grid.seq2grid.size(), 0);
195
+ grid.seq2oddcorner.resize(grid.seq2grid.size(), 0);
196
+ std::unordered_set<int> visited_seq;
197
+ for (int vd = 0; vd < 3; ++vd) {
198
+ auto t = view_layer_positions[vd];
199
+ auto t0 = view_layer_positions[v];
200
+ int height = t.size(1);
201
+ int width = t.size(2);
202
+ int num_layers = t.size(0);
203
+ int num_view_layers = t0.size(0);
204
+ for (int i = 0; i < height; ++i) {
205
+ for (int j = 0; j < width; ++j) {
206
+ for (int l = 0; l < num_layers; ++l) {
207
+ int seq = fetch_seq(grid, l, i, j, t);
208
+ if (seq == -1)
209
+ break;
210
+ int dim = grid.seq2normal[seq];
211
+ if (dim != v)
212
+ continue;
213
+
214
+ float pos[3];
215
+ pos_from_seq(grid, seq, pos);
216
+
217
+ int ci = 0;
218
+ int cj = 0;
219
+ if (dim == 0) {
220
+ ci = (pos[1]/2+0.5)*height;
221
+ cj = (pos[0]/2+0.5)*width;
222
+ }
223
+ else if (dim == 1) {
224
+ ci = (pos[1]/2+0.5)*height;
225
+ cj = (pos[2]/2+0.5)*width;
226
+ }
227
+ else {
228
+ ci = (-pos[2]/2+0.5)*height;
229
+ cj = (pos[0]/2+0.5)*width;
230
+ }
231
+
232
+ if ((ci % (grid.stride * 2) < grid.stride) && (cj % (grid.stride * 2) >= grid.stride))
233
+ grid.seq2evencorner[seq] = 1;
234
+
235
+ if ((ci % (grid.stride * 2) >= grid.stride) && (cj % (grid.stride * 2) < grid.stride))
236
+ grid.seq2oddcorner[seq] = 1;
237
+
238
+ bool is_boundary = false;
239
+ if (vd == v) {
240
+ if (l == 0 || l == num_layers - 1)
241
+ is_boundary = true;
242
+ else {
243
+ int seq_new = fetch_seq(grid, l + 1, i, j, t);
244
+ if (seq_new == -1)
245
+ is_boundary = true;
246
+ }
247
+ }
248
+ int boundary_info = 0;
249
+ if (is_boundary && (l == 0))
250
+ boundary_info = -1;
251
+ else if (is_boundary)
252
+ boundary_info = 1;
253
+ if (visited_seq.count(seq))
254
+ continue;
255
+ visited_seq.insert(seq);
256
+
257
+ FetchNeighbor(grid, seq, pos, dim, boundary_info, view_layer_positions, &grid.seq2neighbor[seq * 9]);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ void PadGrid(Grid& src, Grid& tar, std::vector<torch::Tensor>& view_layer_positions) {
265
+ auto& downsample_seq = src.downsample_seq;
266
+ auto& seq2evencorner = src.seq2evencorner;
267
+ auto& seq2oddcorner = src.seq2oddcorner;
268
+ int indices[9];
269
+ std::vector<int> mapped_even_corners(tar.seq2grid.size(), 0);
270
+ std::vector<int> mapped_odd_corners(tar.seq2grid.size(), 0);
271
+ for (int i = 0; i < downsample_seq.size(); ++i) {
272
+ if (seq2evencorner[i] > 0) {
273
+ mapped_even_corners[downsample_seq[i]] = 1;
274
+ }
275
+ if (seq2oddcorner[i] > 0) {
276
+ mapped_odd_corners[downsample_seq[i]] = 1;
277
+ }
278
+ }
279
+ auto& tar_seq2normal = tar.seq2normal;
280
+ auto& tar_seq2grid = tar.seq2grid;
281
+ for (int i = 0; i < tar_seq2grid.size(); ++i) {
282
+ if (mapped_even_corners[i] == 1 && mapped_odd_corners[i] == 1)
283
+ continue;
284
+ auto k = tar_seq2grid[i];
285
+ float p[3];
286
+ key2cornerpos(k, tar.resolution, p);
287
+
288
+ int src_key = pos2key(p, src.resolution);
289
+ if (!src.grid2seq.count(src_key)) {
290
+ int seq = src.seq2grid.size();
291
+ src.grid2seq[src_key] = seq;
292
+ src.seq2evencorner.emplace_back((mapped_even_corners[i] == 0));
293
+ src.seq2oddcorner.emplace_back((mapped_odd_corners[i] == 0));
294
+ src.seq2grid.emplace_back(src_key);
295
+ src.seq2normal.emplace_back(tar_seq2normal[i]);
296
+ FetchNeighbor(src, seq, p, tar_seq2normal[i], 0, view_layer_positions, indices);
297
+ for (int j = 0; j < 9; ++j) {
298
+ src.seq2neighbor.emplace_back(indices[j]);
299
+ }
300
+ src.downsample_seq.emplace_back(i);
301
+ } else {
302
+ int seq = src.grid2seq[src_key];
303
+ if (mapped_even_corners[i] == 0)
304
+ src.seq2evencorner[seq] = 1;
305
+ if (mapped_odd_corners[i] == 0)
306
+ src.seq2oddcorner[seq] = 1;
307
+ }
308
+ }
309
+ }
310
+
311
+ std::vector<std::vector<torch::Tensor>> build_hierarchy(std::vector<torch::Tensor> view_layer_positions,
312
+ std::vector<torch::Tensor> view_layer_normals, int num_level, int resolution)
313
+ {
314
+ if (view_layer_positions.size() != 3 || num_level < 1) {
315
+ printf("Alert! We require 3 layers and at least 1 level! (%d %d)\n", view_layer_positions.size(), num_level);
316
+ return {{},{},{},{}};
317
+ }
318
+
319
+ std::vector<Grid> grids;
320
+ grids.resize(num_level);
321
+
322
+ std::vector<float> seq2pos;
323
+ auto& seq2grid = grids[0].seq2grid;
324
+ auto& seq2normal = grids[0].seq2normal;
325
+ auto& grid2seq = grids[0].grid2seq;
326
+ grids[0].resolution = resolution;
327
+ grids[0].stride = 1;
328
+
329
+ auto int64_options = torch::TensorOptions().dtype(torch::kInt64).requires_grad(false);
330
+ auto float_options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false);
331
+
332
+ for (int v = 0; v < 3; ++v) {
333
+ int num_layers = view_layer_positions[v].size(0);
334
+ int height = view_layer_positions[v].size(1);
335
+ int width = view_layer_positions[v].size(2);
336
+ float* data = view_layer_positions[v].data_ptr<float>();
337
+ float* data_normal = view_layer_normals[v].data_ptr<float>();
338
+ for (int l = 0; l < num_layers; ++l) {
339
+ for (int i = 0; i < height; ++i) {
340
+ for (int j = 0; j < width; ++j) {
341
+ float* p = &data[(i * width + j) * 4];
342
+ float* n = &data_normal[(i * width + j) * 3];
343
+ if (p[3] == 0)
344
+ continue;
345
+ auto k = pos2key(p, resolution);
346
+ if (!grid2seq.count(k)) {
347
+ int dim = 0;
348
+ for (int d = 0; d < 3; ++d) {
349
+ if (std::abs(n[d]) > std::abs(n[dim]))
350
+ dim = d;
351
+ }
352
+ dim = (dim + 1) % 3;
353
+ grid2seq[k] = seq2grid.size();
354
+ seq2grid.emplace_back(k);
355
+ seq2pos.push_back(p[0]);
356
+ seq2pos.push_back(p[1]);
357
+ seq2pos.push_back(p[2]);
358
+ seq2normal.emplace_back(dim);
359
+ }
360
+ }
361
+ }
362
+ data += (height * width * 4);
363
+ data_normal += (height * width * 3);
364
+ }
365
+ }
366
+
367
+ for (int i = 0; i < num_level - 1; ++i) {
368
+ DownsampleGrid(grids[i], grids[i + 1]);
369
+ }
370
+
371
+ for (int l = 0; l < num_level; ++l) {
372
+ grids[l].seq2neighbor.resize(grids[l].seq2grid.size() * 9, -1);
373
+ grids[l].num_origin_seq = grids[l].seq2grid.size();
374
+ for (int d = 0; d < 3; ++d) {
375
+ NeighborGrid(grids[l], view_layer_positions, d);
376
+ }
377
+ }
378
+
379
+ for (int i = num_level - 2; i >= 0; --i) {
380
+ PadGrid(grids[i], grids[i + 1], view_layer_positions);
381
+ }
382
+ for (int i = grids[0].num_origin_seq; i < grids[0].seq2grid.size(); ++i) {
383
+ int k = grids[0].seq2grid[i];
384
+ float p[3];
385
+ key2pos(k, grids[0].resolution, p);
386
+ seq2pos.push_back(p[0]);
387
+ seq2pos.push_back(p[1]);
388
+ seq2pos.push_back(p[2]);
389
+ }
390
+
391
+ std::vector<torch::Tensor> texture_positions(2);
392
+ std::vector<torch::Tensor> grid_neighbors(grids.size());
393
+ std::vector<torch::Tensor> grid_downsamples(grids.size() - 1);
394
+ std::vector<torch::Tensor> grid_evencorners(grids.size());
395
+ std::vector<torch::Tensor> grid_oddcorners(grids.size());
396
+
397
+ texture_positions[0] = torch::zeros({seq2pos.size() / 3, 3}, float_options);
398
+ texture_positions[1] = torch::zeros({seq2pos.size() / 3}, float_options);
399
+ float* positions_out_ptr = texture_positions[0].data_ptr<float>();
400
+ memcpy(positions_out_ptr, seq2pos.data(), sizeof(float) * seq2pos.size());
401
+ positions_out_ptr = texture_positions[1].data_ptr<float>();
402
+ for (int i = 0; i < grids[0].seq2grid.size(); ++i) {
403
+ positions_out_ptr[i] = (i < grids[0].num_origin_seq);
404
+ }
405
+
406
+ for (int i = 0; i < grids.size(); ++i) {
407
+ grid_neighbors[i] = torch::zeros({grids[i].seq2grid.size(), 9}, int64_options);
408
+ long* nptr = grid_neighbors[i].data_ptr<long>();
409
+ for (int j = 0; j < grids[i].seq2neighbor.size(); ++j) {
410
+ nptr[j] = grids[i].seq2neighbor[j];
411
+ }
412
+
413
+ grid_evencorners[i] = torch::zeros({grids[i].seq2evencorner.size()}, int64_options);
414
+ grid_oddcorners[i] = torch::zeros({grids[i].seq2oddcorner.size()}, int64_options);
415
+ long* dptr = grid_evencorners[i].data_ptr<long>();
416
+ for (int j = 0; j < grids[i].seq2evencorner.size(); ++j) {
417
+ dptr[j] = grids[i].seq2evencorner[j];
418
+ }
419
+ dptr = grid_oddcorners[i].data_ptr<long>();
420
+ for (int j = 0; j < grids[i].seq2oddcorner.size(); ++j) {
421
+ dptr[j] = grids[i].seq2oddcorner[j];
422
+ }
423
+ if (i + 1 < grids.size()) {
424
+ grid_downsamples[i] = torch::zeros({grids[i].downsample_seq.size()}, int64_options);
425
+ long* dptr = grid_downsamples[i].data_ptr<long>();
426
+ for (int j = 0; j < grids[i].downsample_seq.size(); ++j) {
427
+ dptr[j] = grids[i].downsample_seq[j];
428
+ }
429
+ }
430
+
431
+ }
432
+ return {texture_positions, grid_neighbors, grid_downsamples, grid_evencorners, grid_oddcorners};
433
+ }
434
+
435
+ std::vector<std::vector<torch::Tensor>> build_hierarchy_with_feat(
436
+ std::vector<torch::Tensor> view_layer_positions,
437
+ std::vector<torch::Tensor> view_layer_normals,
438
+ std::vector<torch::Tensor> view_layer_feats,
439
+ int num_level, int resolution)
440
+ {
441
+ if (view_layer_positions.size() != 3 || num_level < 1) {
442
+ printf("Alert! We require 3 layers and at least 1 level! (%d %d)\n", view_layer_positions.size(), num_level);
443
+ return {{},{},{},{}};
444
+ }
445
+
446
+ std::vector<Grid> grids;
447
+ grids.resize(num_level);
448
+
449
+ std::vector<float> seq2pos;
450
+ std::vector<float> seq2feat;
451
+ auto& seq2grid = grids[0].seq2grid;
452
+ auto& seq2normal = grids[0].seq2normal;
453
+ auto& grid2seq = grids[0].grid2seq;
454
+ grids[0].resolution = resolution;
455
+ grids[0].stride = 1;
456
+
457
+ auto int64_options = torch::TensorOptions().dtype(torch::kInt64).requires_grad(false);
458
+ auto float_options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false);
459
+
460
+ int feat_channel = 3;
461
+ for (int v = 0; v < 3; ++v) {
462
+ int num_layers = view_layer_positions[v].size(0);
463
+ int height = view_layer_positions[v].size(1);
464
+ int width = view_layer_positions[v].size(2);
465
+ float* data = view_layer_positions[v].data_ptr<float>();
466
+ float* data_normal = view_layer_normals[v].data_ptr<float>();
467
+ float* data_feat = view_layer_feats[v].data_ptr<float>();
468
+ feat_channel = view_layer_feats[v].size(3);
469
+ for (int l = 0; l < num_layers; ++l) {
470
+ for (int i = 0; i < height; ++i) {
471
+ for (int j = 0; j < width; ++j) {
472
+ float* p = &data[(i * width + j) * 4];
473
+ float* n = &data_normal[(i * width + j) * 3];
474
+ float* f = &data_feat[(i * width + j) * feat_channel];
475
+ if (p[3] == 0)
476
+ continue;
477
+ auto k = pos2key(p, resolution);
478
+ if (!grid2seq.count(k)) {
479
+ int dim = 0;
480
+ for (int d = 0; d < 3; ++d) {
481
+ if (std::abs(n[d]) > std::abs(n[dim]))
482
+ dim = d;
483
+ }
484
+ dim = (dim + 1) % 3;
485
+ grid2seq[k] = seq2grid.size();
486
+ seq2grid.emplace_back(k);
487
+ seq2pos.push_back(p[0]);
488
+ seq2pos.push_back(p[1]);
489
+ seq2pos.push_back(p[2]);
490
+ for (int c = 0; c < feat_channel; ++c) {
491
+ seq2feat.emplace_back(f[c]);
492
+ }
493
+ seq2normal.emplace_back(dim);
494
+ }
495
+ }
496
+ }
497
+ data += (height * width * 4);
498
+ data_normal += (height * width * 3);
499
+ data_feat += (height * width * feat_channel);
500
+ }
501
+ }
502
+
503
+ for (int i = 0; i < num_level - 1; ++i) {
504
+ DownsampleGrid(grids[i], grids[i + 1]);
505
+ }
506
+
507
+ for (int l = 0; l < num_level; ++l) {
508
+ grids[l].seq2neighbor.resize(grids[l].seq2grid.size() * 9, -1);
509
+ grids[l].num_origin_seq = grids[l].seq2grid.size();
510
+ for (int d = 0; d < 3; ++d) {
511
+ NeighborGrid(grids[l], view_layer_positions, d);
512
+ }
513
+ }
514
+
515
+ for (int i = num_level - 2; i >= 0; --i) {
516
+ PadGrid(grids[i], grids[i + 1], view_layer_positions);
517
+ }
518
+ for (int i = grids[0].num_origin_seq; i < grids[0].seq2grid.size(); ++i) {
519
+ int k = grids[0].seq2grid[i];
520
+ float p[3];
521
+ key2pos(k, grids[0].resolution, p);
522
+ seq2pos.push_back(p[0]);
523
+ seq2pos.push_back(p[1]);
524
+ seq2pos.push_back(p[2]);
525
+ for (int c = 0; c < feat_channel; ++c) {
526
+ seq2feat.emplace_back(0.5);
527
+ }
528
+ }
529
+
530
+ std::vector<torch::Tensor> texture_positions(2);
531
+ std::vector<torch::Tensor> texture_feats(1);
532
+ std::vector<torch::Tensor> grid_neighbors(grids.size());
533
+ std::vector<torch::Tensor> grid_downsamples(grids.size() - 1);
534
+ std::vector<torch::Tensor> grid_evencorners(grids.size());
535
+ std::vector<torch::Tensor> grid_oddcorners(grids.size());
536
+
537
+ texture_positions[0] = torch::zeros({seq2pos.size() / 3, 3}, float_options);
538
+ texture_positions[1] = torch::zeros({seq2pos.size() / 3}, float_options);
539
+ texture_feats[0] = torch::zeros({seq2feat.size() / feat_channel, feat_channel}, float_options);
540
+ float* positions_out_ptr = texture_positions[0].data_ptr<float>();
541
+ memcpy(positions_out_ptr, seq2pos.data(), sizeof(float) * seq2pos.size());
542
+ positions_out_ptr = texture_positions[1].data_ptr<float>();
543
+ for (int i = 0; i < grids[0].seq2grid.size(); ++i) {
544
+ positions_out_ptr[i] = (i < grids[0].num_origin_seq);
545
+ }
546
+ float* feats_out_ptr = texture_feats[0].data_ptr<float>();
547
+ memcpy(feats_out_ptr, seq2feat.data(), sizeof(float) * seq2feat.size());
548
+
549
+ for (int i = 0; i < grids.size(); ++i) {
550
+ grid_neighbors[i] = torch::zeros({grids[i].seq2grid.size(), 9}, int64_options);
551
+ long* nptr = grid_neighbors[i].data_ptr<long>();
552
+ for (int j = 0; j < grids[i].seq2neighbor.size(); ++j) {
553
+ nptr[j] = grids[i].seq2neighbor[j];
554
+ }
555
+ grid_evencorners[i] = torch::zeros({grids[i].seq2evencorner.size()}, int64_options);
556
+ grid_oddcorners[i] = torch::zeros({grids[i].seq2oddcorner.size()}, int64_options);
557
+ long* dptr = grid_evencorners[i].data_ptr<long>();
558
+ for (int j = 0; j < grids[i].seq2evencorner.size(); ++j) {
559
+ dptr[j] = grids[i].seq2evencorner[j];
560
+ }
561
+ dptr = grid_oddcorners[i].data_ptr<long>();
562
+ for (int j = 0; j < grids[i].seq2oddcorner.size(); ++j) {
563
+ dptr[j] = grids[i].seq2oddcorner[j];
564
+ }
565
+ if (i + 1 < grids.size()) {
566
+ grid_downsamples[i] = torch::zeros({grids[i].downsample_seq.size()}, int64_options);
567
+ long* dptr = grid_downsamples[i].data_ptr<long>();
568
+ for (int j = 0; j < grids[i].downsample_seq.size(); ++j) {
569
+ dptr[j] = grids[i].downsample_seq[j];
570
+ }
571
+ }
572
+ }
573
+ return {texture_positions, texture_feats, grid_neighbors, grid_downsamples, grid_evencorners, grid_oddcorners};
574
+ }
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/rasterizer.cpp ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "rasterizer.h"
2
+
3
+ void rasterizeTriangleCPU(int idx, float* vt0, float* vt1, float* vt2, int width, int height, INT64* zbuffer, float* d, float occlusion_truncation) {
4
+ float x_min = std::min(vt0[0], std::min(vt1[0],vt2[0]));
5
+ float x_max = std::max(vt0[0], std::max(vt1[0],vt2[0]));
6
+ float y_min = std::min(vt0[1], std::min(vt1[1],vt2[1]));
7
+ float y_max = std::max(vt0[1], std::max(vt1[1],vt2[1]));
8
+
9
+ for (int px = x_min; px < x_max + 1; ++px) {
10
+ if (px < 0 || px >= width)
11
+ continue;
12
+ for (int py = y_min; py < y_max + 1; ++py) {
13
+ if (py < 0 || py >= height)
14
+ continue;
15
+ float vt[2] = {px + 0.5, py + 0.5};
16
+ float baryCentricCoordinate[3];
17
+ calculateBarycentricCoordinate(vt0, vt1, vt2, vt, baryCentricCoordinate);
18
+ if (isBarycentricCoordInBounds(baryCentricCoordinate)) {
19
+ int pixel = py * width + px;
20
+ if (zbuffer == 0) {
21
+ zbuffer[pixel] = (INT64)(idx + 1);
22
+ continue;
23
+ }
24
+
25
+ float depth = baryCentricCoordinate[0] * vt0[2] + baryCentricCoordinate[1] * vt1[2] + baryCentricCoordinate[2] * vt2[2];
26
+ float depth_thres = 0;
27
+ if (d) {
28
+ depth_thres = d[pixel] * 0.49999f + 0.5f + occlusion_truncation;
29
+ }
30
+
31
+ int z_quantize = depth * (2<<17);
32
+ INT64 token = (INT64)z_quantize * MAXINT + (INT64)(idx + 1);
33
+ if (depth < depth_thres)
34
+ continue;
35
+ zbuffer[pixel] = std::min(zbuffer[pixel], token);
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ void barycentricFromImgcoordCPU(float* V, int* F, int* findices, INT64* zbuffer, int width, int height, int num_vertices, int num_faces,
42
+ float* barycentric_map, int pix)
43
+ {
44
+ INT64 f = zbuffer[pix] % MAXINT;
45
+ if (f == (MAXINT-1)) {
46
+ findices[pix] = 0;
47
+ barycentric_map[pix * 3] = 0;
48
+ barycentric_map[pix * 3 + 1] = 0;
49
+ barycentric_map[pix * 3 + 2] = 0;
50
+ return;
51
+ }
52
+ findices[pix] = f;
53
+ f -= 1;
54
+ float barycentric[3] = {0, 0, 0};
55
+ if (f >= 0) {
56
+ float vt[2] = {float(pix % width) + 0.5f, float(pix / width) + 0.5f};
57
+ float* vt0_ptr = V + (F[f * 3] * 4);
58
+ float* vt1_ptr = V + (F[f * 3 + 1] * 4);
59
+ float* vt2_ptr = V + (F[f * 3 + 2] * 4);
60
+
61
+ float vt0[2] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f};
62
+ float vt1[2] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f};
63
+ float vt2[2] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f};
64
+
65
+ calculateBarycentricCoordinate(vt0, vt1, vt2, vt, barycentric);
66
+
67
+ barycentric[0] = barycentric[0] / vt0_ptr[3];
68
+ barycentric[1] = barycentric[1] / vt1_ptr[3];
69
+ barycentric[2] = barycentric[2] / vt2_ptr[3];
70
+ float w = 1.0f / (barycentric[0] + barycentric[1] + barycentric[2]);
71
+ barycentric[0] *= w;
72
+ barycentric[1] *= w;
73
+ barycentric[2] *= w;
74
+
75
+ }
76
+ barycentric_map[pix * 3] = barycentric[0];
77
+ barycentric_map[pix * 3 + 1] = barycentric[1];
78
+ barycentric_map[pix * 3 + 2] = barycentric[2];
79
+ }
80
+
81
+ void rasterizeImagecoordsKernelCPU(float* V, int* F, float* d, INT64* zbuffer, float occlusion_trunc, int width, int height, int num_vertices, int num_faces, int f)
82
+ {
83
+ float* vt0_ptr = V + (F[f * 3] * 4);
84
+ float* vt1_ptr = V + (F[f * 3 + 1] * 4);
85
+ float* vt2_ptr = V + (F[f * 3 + 2] * 4);
86
+
87
+ float vt0[3] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f, vt0_ptr[2] / vt0_ptr[3] * 0.49999f + 0.5f};
88
+ float vt1[3] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f, vt1_ptr[2] / vt1_ptr[3] * 0.49999f + 0.5f};
89
+ float vt2[3] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f, vt2_ptr[2] / vt2_ptr[3] * 0.49999f + 0.5f};
90
+
91
+ rasterizeTriangleCPU(f, vt0, vt1, vt2, width, height, zbuffer, d, occlusion_trunc);
92
+ }
93
+
94
+ std::vector<torch::Tensor> rasterize_image_cpu(torch::Tensor V, torch::Tensor F, torch::Tensor D,
95
+ int width, int height, float occlusion_truncation, int use_depth_prior)
96
+ {
97
+ int num_faces = F.size(0);
98
+ int num_vertices = V.size(0);
99
+ auto options = torch::TensorOptions().dtype(torch::kInt32).requires_grad(false);
100
+ auto INT64_options = torch::TensorOptions().dtype(torch::kInt64).requires_grad(false);
101
+ auto findices = torch::zeros({height, width}, options);
102
+ INT64 maxint = (INT64)MAXINT * (INT64)MAXINT + (MAXINT - 1);
103
+ auto z_min = torch::ones({height, width}, INT64_options) * (long)maxint;
104
+
105
+ if (!use_depth_prior) {
106
+ for (int i = 0; i < num_faces; ++i) {
107
+ rasterizeImagecoordsKernelCPU(V.data_ptr<float>(), F.data_ptr<int>(), 0,
108
+ (INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces, i);
109
+ }
110
+ } else {
111
+ for (int i = 0; i < num_faces; ++i)
112
+ rasterizeImagecoordsKernelCPU(V.data_ptr<float>(), F.data_ptr<int>(), D.data_ptr<float>(),
113
+ (INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces, i);
114
+ }
115
+
116
+ auto float_options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false);
117
+ auto barycentric = torch::zeros({height, width, 3}, float_options);
118
+ for (int i = 0; i < width * height; ++i)
119
+ barycentricFromImgcoordCPU(V.data_ptr<float>(), F.data_ptr<int>(),
120
+ findices.data_ptr<int>(), (INT64*)z_min.data_ptr<long>(), width, height, num_vertices, num_faces, barycentric.data_ptr<float>(), i);
121
+
122
+ return {findices, barycentric};
123
+ }
124
+
125
+ std::vector<torch::Tensor> rasterize_image(torch::Tensor V, torch::Tensor F, torch::Tensor D,
126
+ int width, int height, float occlusion_truncation, int use_depth_prior)
127
+ {
128
+ int device_id = V.get_device();
129
+ if (device_id == -1)
130
+ return rasterize_image_cpu(V, F, D, width, height, occlusion_truncation, use_depth_prior);
131
+ else
132
+ return rasterize_image_gpu(V, F, D, width, height, occlusion_truncation, use_depth_prior);
133
+ }
134
+
135
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
136
+ m.def("rasterize_image", &rasterize_image, "Custom image rasterization");
137
+ m.def("build_hierarchy", &build_hierarchy, "Custom image rasterization");
138
+ m.def("build_hierarchy_with_feat", &build_hierarchy_with_feat, "Custom image rasterization");
139
+ }
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/rasterizer.h ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef RASTERIZER_H_
2
+ #define RASTERIZER_H_
3
+
4
+ #include <torch/extension.h>
5
+ #include <vector>
6
+ #include <ATen/ATen.h>
7
+ #include <ATen/cuda/CUDAContext.h> // For CUDA context
8
+
9
+ #define INT64 unsigned long long
10
+ #define MAXINT 2147483647
11
+
12
+ __host__ __device__ inline float calculateSignedArea2(float* a, float* b, float* c) {
13
+ return ((c[0] - a[0]) * (b[1] - a[1]) - (b[0] - a[0]) * (c[1] - a[1]));
14
+ }
15
+
16
+ __host__ __device__ inline void calculateBarycentricCoordinate(float* a, float* b, float* c, float* p,
17
+ float* barycentric)
18
+ {
19
+ float beta_tri = calculateSignedArea2(a, p, c);
20
+ float gamma_tri = calculateSignedArea2(a, b, p);
21
+ float area = calculateSignedArea2(a, b, c);
22
+ if (area == 0) {
23
+ barycentric[0] = -1.0;
24
+ barycentric[1] = -1.0;
25
+ barycentric[2] = -1.0;
26
+ return;
27
+ }
28
+ float tri_inv = 1.0 / area;
29
+ float beta = beta_tri * tri_inv;
30
+ float gamma = gamma_tri * tri_inv;
31
+ float alpha = 1.0 - beta - gamma;
32
+ barycentric[0] = alpha;
33
+ barycentric[1] = beta;
34
+ barycentric[2] = gamma;
35
+ }
36
+
37
+ __host__ __device__ inline bool isBarycentricCoordInBounds(float* barycentricCoord) {
38
+ return barycentricCoord[0] >= 0.0 && barycentricCoord[0] <= 1.0 &&
39
+ barycentricCoord[1] >= 0.0 && barycentricCoord[1] <= 1.0 &&
40
+ barycentricCoord[2] >= 0.0 && barycentricCoord[2] <= 1.0;
41
+ }
42
+
43
+ std::vector<torch::Tensor> rasterize_image_gpu(torch::Tensor V, torch::Tensor F, torch::Tensor D,
44
+ int width, int height, float occlusion_truncation, int use_depth_prior);
45
+
46
+ std::vector<std::vector<torch::Tensor>> build_hierarchy(std::vector<torch::Tensor> view_layer_positions, std::vector<torch::Tensor> view_layer_normals, int num_level, int resolution);
47
+
48
+ std::vector<std::vector<torch::Tensor>> build_hierarchy_with_feat(
49
+ std::vector<torch::Tensor> view_layer_positions,
50
+ std::vector<torch::Tensor> view_layer_normals,
51
+ std::vector<torch::Tensor> view_layer_feats,
52
+ int num_level, int resolution);
53
+
54
+ #endif
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/lib/custom_rasterizer_kernel/rasterizer_gpu.cu ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "rasterizer.h"
2
+
3
+ __device__ void rasterizeTriangleGPU(int idx, float* vt0, float* vt1, float* vt2, int width, int height, INT64* zbuffer, float* d, float occlusion_truncation) {
4
+ float x_min = std::min(vt0[0], std::min(vt1[0],vt2[0]));
5
+ float x_max = std::max(vt0[0], std::max(vt1[0],vt2[0]));
6
+ float y_min = std::min(vt0[1], std::min(vt1[1],vt2[1]));
7
+ float y_max = std::max(vt0[1], std::max(vt1[1],vt2[1]));
8
+
9
+ for (int px = x_min; px < x_max + 1; ++px) {
10
+ if (px < 0 || px >= width)
11
+ continue;
12
+ for (int py = y_min; py < y_max + 1; ++py) {
13
+ if (py < 0 || py >= height)
14
+ continue;
15
+ float vt[2] = {px + 0.5f, py + 0.5f};
16
+ float baryCentricCoordinate[3];
17
+ calculateBarycentricCoordinate(vt0, vt1, vt2, vt, baryCentricCoordinate);
18
+ if (isBarycentricCoordInBounds(baryCentricCoordinate)) {
19
+ int pixel = py * width + px;
20
+ if (zbuffer == 0) {
21
+ atomicExch(&zbuffer[pixel], (INT64)(idx + 1));
22
+ continue;
23
+ }
24
+ float depth = baryCentricCoordinate[0] * vt0[2] + baryCentricCoordinate[1] * vt1[2] + baryCentricCoordinate[2] * vt2[2];
25
+ float depth_thres = 0;
26
+ if (d) {
27
+ depth_thres = d[pixel] * 0.49999f + 0.5f + occlusion_truncation;
28
+ }
29
+
30
+ int z_quantize = depth * (2<<17);
31
+ INT64 token = (INT64)z_quantize * MAXINT + (INT64)(idx + 1);
32
+ if (depth < depth_thres)
33
+ continue;
34
+ atomicMin(&zbuffer[pixel], token);
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ __global__ void barycentricFromImgcoordGPU(float* V, int* F, int* findices, INT64* zbuffer, int width, int height, int num_vertices, int num_faces,
41
+ float* barycentric_map)
42
+ {
43
+ int pix = blockIdx.x * blockDim.x + threadIdx.x;
44
+ if (pix >= width * height)
45
+ return;
46
+ INT64 f = zbuffer[pix] % MAXINT;
47
+ if (f == (MAXINT-1)) {
48
+ findices[pix] = 0;
49
+ barycentric_map[pix * 3] = 0;
50
+ barycentric_map[pix * 3 + 1] = 0;
51
+ barycentric_map[pix * 3 + 2] = 0;
52
+ return;
53
+ }
54
+ findices[pix] = f;
55
+ f -= 1;
56
+ float barycentric[3] = {0, 0, 0};
57
+ if (f >= 0) {
58
+ float vt[2] = {float(pix % width) + 0.5f, float(pix / width) + 0.5f};
59
+ float* vt0_ptr = V + (F[f * 3] * 4);
60
+ float* vt1_ptr = V + (F[f * 3 + 1] * 4);
61
+ float* vt2_ptr = V + (F[f * 3 + 2] * 4);
62
+
63
+ float vt0[2] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f};
64
+ float vt1[2] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f};
65
+ float vt2[2] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f};
66
+
67
+ calculateBarycentricCoordinate(vt0, vt1, vt2, vt, barycentric);
68
+
69
+ barycentric[0] = barycentric[0] / vt0_ptr[3];
70
+ barycentric[1] = barycentric[1] / vt1_ptr[3];
71
+ barycentric[2] = barycentric[2] / vt2_ptr[3];
72
+ float w = 1.0f / (barycentric[0] + barycentric[1] + barycentric[2]);
73
+ barycentric[0] *= w;
74
+ barycentric[1] *= w;
75
+ barycentric[2] *= w;
76
+
77
+ }
78
+ barycentric_map[pix * 3] = barycentric[0];
79
+ barycentric_map[pix * 3 + 1] = barycentric[1];
80
+ barycentric_map[pix * 3 + 2] = barycentric[2];
81
+ }
82
+
83
+ __global__ void rasterizeImagecoordsKernelGPU(float* V, int* F, float* d, INT64* zbuffer, float occlusion_trunc, int width, int height, int num_vertices, int num_faces)
84
+ {
85
+ int f = blockIdx.x * blockDim.x + threadIdx.x;
86
+ if (f >= num_faces)
87
+ return;
88
+
89
+ float* vt0_ptr = V + (F[f * 3] * 4);
90
+ float* vt1_ptr = V + (F[f * 3 + 1] * 4);
91
+ float* vt2_ptr = V + (F[f * 3 + 2] * 4);
92
+
93
+ float vt0[3] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f, vt0_ptr[2] / vt0_ptr[3] * 0.49999f + 0.5f};
94
+ float vt1[3] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f, vt1_ptr[2] / vt1_ptr[3] * 0.49999f + 0.5f};
95
+ float vt2[3] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f, vt2_ptr[2] / vt2_ptr[3] * 0.49999f + 0.5f};
96
+
97
+ rasterizeTriangleGPU(f, vt0, vt1, vt2, width, height, zbuffer, d, occlusion_trunc);
98
+ }
99
+
100
+ std::vector<torch::Tensor> rasterize_image_gpu(torch::Tensor V, torch::Tensor F, torch::Tensor D,
101
+ int width, int height, float occlusion_truncation, int use_depth_prior)
102
+ {
103
+ int device_id = V.get_device();
104
+ cudaSetDevice(device_id);
105
+ int num_faces = F.size(0);
106
+ int num_vertices = V.size(0);
107
+ auto options = torch::TensorOptions().dtype(torch::kInt32).device(torch::kCUDA, device_id).requires_grad(false);
108
+ auto INT64_options = torch::TensorOptions().dtype(torch::kInt64).device(torch::kCUDA, device_id).requires_grad(false);
109
+ auto findices = torch::zeros({height, width}, options);
110
+ INT64 maxint = (INT64)MAXINT * (INT64)MAXINT + (MAXINT - 1);
111
+ auto z_min = torch::ones({height, width}, INT64_options) * (long)maxint;
112
+
113
+ if (!use_depth_prior) {
114
+ rasterizeImagecoordsKernelGPU<<<(num_faces+255)/256,256,0,at::cuda::getCurrentCUDAStream()>>>(V.data_ptr<float>(), F.data_ptr<int>(), 0,
115
+ (INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces);
116
+ } else {
117
+ rasterizeImagecoordsKernelGPU<<<(num_faces+255)/256,256,0,at::cuda::getCurrentCUDAStream()>>>(V.data_ptr<float>(), F.data_ptr<int>(), D.data_ptr<float>(),
118
+ (INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces);
119
+ }
120
+
121
+ auto float_options = torch::TensorOptions().dtype(torch::kFloat32).device(torch::kCUDA, device_id).requires_grad(false);
122
+ auto barycentric = torch::zeros({height, width, 3}, float_options);
123
+ barycentricFromImgcoordGPU<<<(width * height + 255)/256, 256>>>(V.data_ptr<float>(), F.data_ptr<int>(),
124
+ findices.data_ptr<int>(), (INT64*)z_min.data_ptr<long>(), width, height, num_vertices, num_faces, barycentric.data_ptr<float>());
125
+
126
+ return {findices, barycentric};
127
+ }
home/ubuntu/aaaaa/data/rgbmr/custom_rasterizer/setup.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from setuptools import setup, find_packages
2
+ import torch
3
+ from torch.utils.cpp_extension import BuildExtension, CUDAExtension, CppExtension
4
+
5
+ # build custom rasterizer
6
+
7
+ custom_rasterizer_module = CUDAExtension(
8
+ "custom_rasterizer_kernel",
9
+ [
10
+ "lib/custom_rasterizer_kernel/rasterizer.cpp",
11
+ "lib/custom_rasterizer_kernel/grid_neighbor.cpp",
12
+ "lib/custom_rasterizer_kernel/rasterizer_gpu.cu",
13
+ ],
14
+ )
15
+
16
+ setup(
17
+ packages=find_packages(),
18
+ version="0.1",
19
+ name="custom_rasterizer",
20
+ include_package_data=True,
21
+ package_dir={"": "."},
22
+ ext_modules=[
23
+ custom_rasterizer_module,
24
+ ],
25
+ cmdclass={"build_ext": BuildExtension},
26
+ )
home/ubuntu/aaaaa/data/rgbmr/data/__pycache__/rgbmr_dataset.cpython-310.pyc ADDED
Binary file (21.5 kB). View file
 
home/ubuntu/aaaaa/data/rgbmr/data/generate_rgbmr_dataset.py ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os, sys
3
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # repo root append
4
+
5
+ import math
6
+ from typing import Any, Callable, Dict, List, Set, Tuple, Optional, Union
7
+ from pathlib import Path
8
+ import glob
9
+
10
+ import numpy as np
11
+ from PIL import Image
12
+ import torch
13
+
14
+ from tools.utils.mesh_utils import (
15
+ load_mesh,
16
+ get_orthogonal_camera,
17
+ NVDiffRastContextWrapper,
18
+ render,
19
+ )
20
+
21
+ from data.rgbmr_dataset import build_partial_dataset, DT, SCHEMA_RGBMR
22
+ from mcgen.utils.text_encoder_utils import load_text_ctx, encode_prompt
23
+
24
+
25
+ # -------------------------
26
+ # Helpers: find glbs, curated ids
27
+ # -------------------------
28
+
29
+ def load_curated_ids(path: Path) -> List[str]:
30
+ ids = []
31
+ with open(path, "r", encoding="utf-8") as f:
32
+ for line in f:
33
+ s = line.strip()
34
+ if not s or s.startswith("#"):
35
+ continue
36
+ ids.append(s)
37
+ return ids
38
+
39
+ def find_glb_index(root, curated_ids):
40
+ glbs = {}
41
+ for p in glob.glob(str(root / "**/*.glb"), recursive=True):
42
+ p = Path(p)
43
+ if p.stem in curated_ids:
44
+ glbs[p.stem] = p
45
+ return glbs
46
+
47
+ def shard_list(full_list, num_shards: int, shard_id: int):
48
+ n = len(full_list)
49
+ if num_shards <= 1:
50
+ return full_list
51
+ sz = (n + num_shards - 1) // num_shards
52
+ start = shard_id * sz
53
+ end = min(start + sz, n)
54
+ return full_list[start:end]
55
+
56
+ # -------------------------
57
+ # Camera/view sampling
58
+ # -------------------------
59
+ def default_view_sampler(num_elev: int = 1, num_azim: int = 24,
60
+ elev_deg: float = 20.0) -> Callable[[Any], List[Tuple[float, float]]]:
61
+ """
62
+ Returns a function gid -> list[(elevation_deg, azimuth_deg)].
63
+ By default: a single elevation ring with uniformly spaced azimuths.
64
+ """
65
+ azims = [i * (360.0 / num_azim) for i in range(num_azim)]
66
+ elevs = [elev_deg for _ in range(num_elev)]
67
+ views = [(e, a) for e in elevs for a in azims]
68
+
69
+ def sampler(_gid: Any) -> List[Tuple[float, float]]:
70
+ return views
71
+
72
+ return sampler
73
+
74
+ def random_view_sampler(num_views: int = 4,
75
+ elev_range: Tuple[float, float] = (-30.0, 60.0)) -> Callable[[Any], List[Tuple[float, float]]]:
76
+ """
77
+ Returns a function gid -> list[(elevation_deg, azimuth_deg)].
78
+ Each object gets num_views random views.
79
+ """
80
+ def sampler(_gid: Any) -> List[Tuple[float, float]]:
81
+ views = []
82
+ for _ in range(num_views):
83
+ az = np.random.uniform(0.0, 360.0)
84
+ el = np.random.uniform(elev_range[0], elev_range[1])
85
+ views.append((el, az))
86
+ return views
87
+ return sampler
88
+
89
+ def view_sampler_from_list(azim_list: List[float], elev_list: List[float]) -> Callable[[Any], List[Tuple[float, float]]]:
90
+ """
91
+ azim_list: list of azimuth angles in degrees
92
+ elev_list: list of elevation angles in degrees
93
+ Returns a function gid -> list[(elevation_deg, azimuth_deg)].
94
+ """
95
+ if len(azim_list) != len(elev_list):
96
+ raise ValueError("azim_list and elev_list must have the same length")
97
+ views = list(zip(elev_list, azim_list))
98
+
99
+ def sampler(_gid: Any) -> List[Tuple[float, float]]:
100
+ return views
101
+
102
+ return sampler
103
+
104
+
105
+ # -------------------------
106
+ # Backend interface
107
+ # -------------------------
108
+ class RenderBackend:
109
+ """
110
+ Implement this with your renderer. The contract is:
111
+ render(mesh_path, elevation, azimuth, image_size) -> dict with:
112
+ - 'albedo': BxHxWx3 uint8 or float[0,1]
113
+ - 'normal': BxHxWx3 float[-1,1] or uint8 in [0,255] (we'll convert)
114
+ - 'depth': BxHxW float meters
115
+ - 'orm': BxHxWx3 (O,R,M) either float[0,1] or uint8[0,255]
116
+ """
117
+
118
+ @torch.no_grad()
119
+ def render(
120
+ self,
121
+ ctx: NVDiffRastContextWrapper,
122
+ mesh_path: Any,
123
+ elevation: Union[List[float], float],
124
+ azimuth: Union[List[float], float],
125
+ image_size: Tuple[int, int],
126
+ device: Union[str, torch.device] = "cuda",
127
+ ) -> Dict[str, Any]:
128
+ # 로딩
129
+ mesh = load_mesh(
130
+ str(mesh_path),
131
+ rescale=True,
132
+ move_to_center=True,
133
+ flip_uv=True,
134
+ device=device,
135
+ )
136
+ elevation = elevation if isinstance(elevation, list) else [elevation]
137
+ azimuth = azimuth if isinstance(azimuth, list) else [azimuth]
138
+
139
+ # 카메라 배치(정사각형 ortho box; rescale=0.5 기준 안전 여유)
140
+ cams = get_orthogonal_camera(
141
+ elevation_deg=elevation,
142
+ distance=[1.0] * len(azimuth),
143
+ left=-0.55,
144
+ right=0.55,
145
+ bottom=-0.55,
146
+ top=0.55,
147
+ azimuth_deg=azimuth,
148
+ device=device,
149
+ dtype=torch.float32,
150
+ )
151
+
152
+ # 렌더 속성: rgb + roughness + metallic (5채널)
153
+ tex_ovr = torch.concatenate(
154
+ [mesh.texture, torch.zeros_like(mesh.roughness), mesh.roughness, mesh.metallic],
155
+ dim=-1,
156
+ )
157
+ out = render(
158
+ ctx, mesh, cams, height=image_size[0], width=image_size[1], render_attr=True, texture_override=tex_ovr
159
+ )
160
+
161
+ attr = out.attr # [B,H,W,5] float in [0,1]
162
+ pos_batch = out.pos
163
+ normal_batch = out.normal
164
+ depth_batch = out.raw_depth # [B,H,W] float in [0,inf]
165
+
166
+ rgb_batch = attr[..., :3].contiguous()
167
+ orm_batch = attr[..., 3:6].contiguous()
168
+ orm_batch[..., 0] = (orm_batch[..., 0] < 0.25).float() # binarize alpha to get foreground mask
169
+
170
+ pos_batch = pos_batch + 0.5 # [-0.5,0.5] → [0,1]
171
+ normal_batch = (normal_batch + 1.0) * 0.5 # [-1,1] → [0,1]
172
+
173
+ result: Dict[str, Any] = {
174
+ "albedo": rgb_batch,
175
+ "orm": orm_batch,
176
+ "depth": depth_batch,
177
+ "pos": pos_batch,
178
+ "normal": normal_batch,
179
+ "c2w": cams.c2w,
180
+ "scale": 1.1,
181
+ }
182
+ return result
183
+
184
+
185
+ # -------------------------
186
+ # Mesh resolver
187
+ # -------------------------
188
+ def default_mesh_resolver(mesh_index: Dict[str, Any]) -> Callable[[Any], Any]:
189
+ """
190
+ mesh_index: e.g., {'obj001': '/path/obj001.obj', ...} or preloaded mesh objects.
191
+ """
192
+ def resolve(gid: Any) -> Any:
193
+ key = str(gid)
194
+ if key not in mesh_index:
195
+ raise KeyError(f"Mesh for gid='{gid}' not found")
196
+ return mesh_index[key]
197
+ return resolve
198
+
199
+ def default_prompt_resolver(prompt_index: Dict[str, str]) -> Callable[[Any], str]:
200
+ """
201
+ prompt_index: e.g., {'obj001': 'a red apple', ...}
202
+ """
203
+ def resolve(gid: Any) -> str:
204
+ key = str(gid)
205
+ if key not in prompt_index:
206
+ raise KeyError(f"Prompt for gid='{gid}' not found")
207
+ return prompt_index[key]
208
+ return resolve
209
+
210
+
211
+ # -------------------------
212
+ # Factory: make generate_data(gid)
213
+ # -------------------------
214
+ def make_generate_data(
215
+ render_ctx,
216
+ text_ctx,
217
+ mesh_resolver: Callable[[Any], Any],
218
+ prompt_resolver: Callable[[Any], str],
219
+ backend: RenderBackend,
220
+ view_sampler: Callable[[Any], List[Tuple[float, float]]] = default_view_sampler(),
221
+ image_size: Tuple[int, int] = (512, 512),
222
+ max_seq_length: int = 77,
223
+ ):
224
+ """
225
+ Returns a single-argument function: generate_data(gid) -> dict
226
+ Output structure matches your schema requirements.
227
+ Per-object:
228
+ - 'gid' (str)
229
+ - 'prompt' (str) -> 'a sample text'
230
+ - 'prompt_embeds' (torch.Tensor) -> (L, D)
231
+ - 'pooled_prompt_embeds' (torch.Tensor) -> (D,)
232
+ Per-view (list under 'views'):
233
+ - 'albedo' (PNG-ready RGB)
234
+ - 'orm' (PNG-ready RGB)
235
+ - 'depth' (EXR-ready float32)
236
+ - 'pos' (PNG-ready RGB)
237
+ - 'normal' (PNG-ready RGB)
238
+ - 'elevation' (float, degrees)
239
+ - 'azimuth' (float, degrees)
240
+ """
241
+ H, W = image_size
242
+
243
+ def generate_data(gid: Any) -> Dict[str, Any]:
244
+ mesh_path = mesh_resolver(gid)
245
+ prompt = prompt_resolver(gid)
246
+ views_out: List[Dict[str, Any]] = []
247
+
248
+ # Collect all views and render in a single batched call
249
+ views = view_sampler(gid)
250
+ if len(views) > 0:
251
+ elev_list = [e for e, _ in views]
252
+ azim_list = [a for _, a in views]
253
+
254
+ out = backend.render(render_ctx, mesh_path, elev_list, azim_list, (H, W))
255
+
256
+ albedo_batch = out['albedo'] # [B,H,W,3] float in [0,1]
257
+ orm_batch = out['orm'] # [B,H,W,3] float in [0,1]
258
+ depth_batch = out['depth'] # [B,H,W] float in [0,inf]
259
+ pos_batch = out['pos'] # [B,H,W,3] float in [0,1]
260
+ normal_batch = out['normal'] # [B,H,W,3] float in [0,1]
261
+ c2w_batch = out['c2w'] # [B,4,4]
262
+ scale = out['scale'] # float scalar
263
+
264
+ for i, (elev_deg, azim_deg) in enumerate(views):
265
+ # ---- albedo ----
266
+ albedo = albedo_batch[i]
267
+ orm = orm_batch[i]
268
+ pos = pos_batch[i]
269
+ normal = normal_batch[i]
270
+
271
+ # ---- depth ----
272
+ depth = depth_batch[i]
273
+
274
+ views_out.append({
275
+ "albedo": albedo, # [H,W,3] float in [0,1], will be converted to PNG
276
+ "orm": orm, # [H,W,3] float in [0,1], will be converted to PNG
277
+ "depth": depth, # [H,W] float, will be saved as EXR
278
+ "pos": pos, # [H,W,3] float in [0,1], will be converted to PNG
279
+ "normal": normal, # [H,W,3] float in [0,1], will be converted to PNG
280
+ "elevation": float(elev_deg), # metadata (store as JSON via schema)
281
+ "azimuth": float(azim_deg),
282
+ "c2w": c2w_batch[i],
283
+ "scale": scale,
284
+ })
285
+
286
+ # encode prompt
287
+ prompt_embeds, pooled_prompt_embeds = encode_prompt(
288
+ text_ctx["encoders"], text_ctx["tokenizers"], prompt,
289
+ max_sequence_length=max_seq_length,
290
+ ) # (1, L, D), (1, D)
291
+
292
+ result: Dict[str, Any] = {
293
+ "prompt": prompt,
294
+ "prompt_embeds": prompt_embeds.squeeze(0).cpu(), # (L, D)
295
+ "pooled_prompt_embeds": pooled_prompt_embeds.squeeze(0).cpu(), # (D,)
296
+ "views": views_out,
297
+ }
298
+ return result
299
+
300
+ return generate_data
301
+
302
+
303
+ # -----------------------------------------
304
+ # Example schema to pair with this generator
305
+ # -----------------------------------------
306
+
307
+ if __name__ == "__main__":
308
+ ap = argparse.ArgumentParser()
309
+ ap.add_argument("--out-dir", default="/home/ubuntu/aaaaa/data/rgbmr", help="Dataset root directory")
310
+ ap.add_argument("--gid-path", default="../obj_keys.txt", help="Curated obj ID list path")
311
+ ap.add_argument("--prompt-path", default="../prompts_bs.csv", help="Prompt CSV path (id,prompt)")
312
+ ap.add_argument("--sd-model", default="stabilityai/stable-diffusion-3.5-medium", help="SD3.5 model name")
313
+ ap.add_argument("--dtype", default="float16", choices=["float16", "float32", "bfloat16"], help="Model dtype")
314
+ ap.add_argument("--device", default="cuda", help="Device")
315
+ ap.add_argument("--max_seq", type=int, default=77, help="T5 max token length")
316
+ ap.add_argument("--num_shards", type=int, default=1)
317
+ ap.add_argument("--shard_id", type=int, default=0)
318
+ ap.add_argument("--random-single-view", action="store_true", help="Use random single view per object")
319
+ ap.add_argument("--part_dir", default=None, help="If given, overrides run_name and uses this dir directly")
320
+ args = ap.parse_args()
321
+ # -------------------- schema example --------------------
322
+
323
+ dtype_map = {"float16": torch.float16, "float32": torch.float32, "bfloat16": torch.bfloat16}
324
+ text_ctx = load_text_ctx(args.device, dtype=dtype_map[args.dtype], sd_model_name=args.sd_model)
325
+ render_ctx = NVDiffRastContextWrapper(device=args.device, context_type="cuda")
326
+
327
+ gid_path = Path(args.gid_path)
328
+ out_dir = Path(args.out_dir)
329
+ glb_root = Path("/home/ubuntu/.objaverse/hf-objaverse-v1/glbs")
330
+
331
+ curated_ids = load_curated_ids(gid_path)
332
+ glbs_filtered = find_glb_index(glb_root, set(curated_ids))
333
+ print(f"Found {len(glbs_filtered)} / {len(curated_ids)} curated GLBs")
334
+
335
+ mesh_resolver = default_mesh_resolver(glbs_filtered)
336
+
337
+ # make prompt resolver
338
+ import csv
339
+ prompt_index = {}
340
+ with open(args.prompt_path, "r", encoding="utf-8") as f:
341
+ reader = csv.reader(f)
342
+ for row in reader:
343
+ if len(row) != 2:
344
+ raise ValueError(f"Expected 2 columns per row, got {len(row)}: {row}")
345
+ obj_id, prompt = row
346
+ prompt_index[obj_id] = prompt
347
+ prompt_resolver = default_prompt_resolver(prompt_index)
348
+
349
+
350
+ az = [
351
+ 0.0, 90.0, 180.0, 270.0, 0.0, 0.0, # face
352
+ 45.0, 135.0, 225.0, 315.0, 45.0, 135.0, 225.0, 315.0, # corner
353
+ 45.0, 135.0, 225.0, 315.0, 0.0, 90.0, 180.0, 270.0, 0.0, 90.0, 180.0, 270.0 # edge
354
+ ]
355
+ el = [
356
+ 0.0, 0.0, 0.0, 0.0, 89.99, -89.99, # face
357
+ 45.0, 45.0, 45.0, 45.0, -45.0, -45.0, -45.0, -45.0, # corner
358
+ 0.0, 0.0, 0.0, 0.0, 45.0, 45.0, 45.0, 45.0, -45.0, -45.0, -45.0, -45.0 # edge
359
+ ]
360
+ view_sampler = None
361
+ if args.random_single_view:
362
+ view_sampler = random_view_sampler(num_views=1, elev_range=(-30.0, 60.0))
363
+ else:
364
+ view_sampler = view_sampler_from_list(azim_list=az, elev_list=el)
365
+
366
+ generate_data = make_generate_data(
367
+ render_ctx=render_ctx,
368
+ text_ctx=text_ctx,
369
+ prompt_resolver=prompt_resolver,
370
+ mesh_resolver=mesh_resolver,
371
+ backend=RenderBackend(),
372
+ view_sampler=view_sampler,
373
+ image_size=(512, 512),
374
+ max_seq_length=args.max_seq,
375
+ )
376
+
377
+ glbs_list = curated_ids
378
+ # glbs_list.sort()
379
+ glbs_sharded = shard_list(glbs_list, args.num_shards, args.shard_id)
380
+
381
+ build_partial_dataset(
382
+ generate_data=generate_data,
383
+ gid_list=glbs_sharded,
384
+ schema=SCHEMA_RGBMR,
385
+ root_dir=out_dir,
386
+ run_name=f"render_{args.shard_id:02d}_of_{args.num_shards:02d}",
387
+ part_dir=args.part_dir,
388
+ progress=True,
389
+ )
home/ubuntu/aaaaa/data/rgbmr/data/rgbmr_dataset.py ADDED
@@ -0,0 +1,845 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rgbmr_dataset.py
2
+ import os
3
+ import io
4
+ import json
5
+ import glob
6
+ import uuid
7
+ import time
8
+ import shutil
9
+ import pathlib
10
+ import random
11
+ from enum import Enum
12
+ from typing import Any, Dict, List, Optional, Union
13
+
14
+ import numpy as np
15
+ from PIL import Image
16
+ import torch
17
+ import torch.nn.functional as F
18
+ import webdataset as wds
19
+ from tqdm import tqdm
20
+
21
+
22
+ # =========================
23
+ # Datatypes and schema utils
24
+ # =========================
25
+ class DT(Enum):
26
+ """Dataset field datatypes."""
27
+
28
+ STR = "str" # utf-8 text
29
+ TENSOR = "tensor" # arbitrary tensor/ndarray -> .npy
30
+ PNG = "png" # 3-channel PNG (uint8); float in [0,1] allowed
31
+ EXR = "exr" # single-channel EXR (depth as float), lossless PIZ by default
32
+ JSON = "json" # json-serializable python object
33
+ BYTES = "bytes" # raw bytes
34
+
35
+
36
+ SCHEMA_RGBMR = {
37
+ "prompt": DT.STR,
38
+ "prompt_embeds": DT.TENSOR,
39
+ "pooled_prompt_embeds": DT.TENSOR,
40
+ "views": {
41
+ "albedo": DT.PNG,
42
+ "orm": DT.PNG,
43
+ "depth": DT.EXR, # float32 depth
44
+ "pos": DT.PNG,
45
+ "normal": DT.PNG,
46
+ "elevation": DT.JSON, # float degree
47
+ "azimuth": DT.JSON, # float degree
48
+ "c2w": DT.TENSOR, # camera-to-world matrix (4x4)
49
+ "scale": DT.JSON, # float scale
50
+ },
51
+ }
52
+
53
+
54
+ def _ensure_dir(path: str) -> None:
55
+ os.makedirs(path, exist_ok=True)
56
+
57
+
58
+ def _timestamp() -> str:
59
+ return time.strftime("%Y-%m-%d %H:%M:%S")
60
+
61
+
62
+ def _sanitize_key(name: str) -> str:
63
+ safe = []
64
+ for ch in name:
65
+ if ch.isalnum() or ch in "._-":
66
+ safe.append(ch)
67
+ else:
68
+ safe.append("_")
69
+ return "".join(safe)
70
+
71
+
72
+ # Added: canonicalize DT specs (accept Enum or string)
73
+ def _dt_value(dt: Any) -> str:
74
+ # direct enum from this module
75
+ if isinstance(dt, DT):
76
+ return dt.value
77
+ # enum-like objects (possibly from reloaded module)
78
+ if hasattr(dt, "value") and isinstance(getattr(dt, "value"), str):
79
+ return getattr(dt, "value")
80
+ # already a string value
81
+ if isinstance(dt, str):
82
+ return dt
83
+ raise ValueError(f"Unsupported DT spec: {dt} (type: {type(dt)})")
84
+
85
+
86
+ def _dt_ext(dt: Any) -> str:
87
+ dtv = _dt_value(dt)
88
+ if dtv == DT.PNG.value:
89
+ return "png"
90
+ if dtv == DT.EXR.value:
91
+ return "exr"
92
+ if dtv == DT.TENSOR.value:
93
+ return "npy"
94
+ if dtv == DT.STR.value:
95
+ return "txt"
96
+ if dtv == DT.JSON.value:
97
+ return "json"
98
+ if dtv == DT.BYTES.value:
99
+ return "bin"
100
+ raise ValueError(f"Unhandled DT: {dt}, type: {type(dt)}")
101
+
102
+
103
+ def _schema_to_jsonable(schema: Dict[str, Any]) -> Dict[str, Any]:
104
+ """Convert a schema containing DT enums into a JSON-serializable dict.
105
+
106
+ The expected input shape is a dict where values are either DT or nested dicts
107
+ (e.g., {"views": {"rgb": DT.PNG, ...}}). We convert any DT to its string value.
108
+ """
109
+
110
+ def _conv(x: Any) -> Any:
111
+ if isinstance(x, DT):
112
+ return x.value
113
+ if isinstance(x, dict):
114
+ return {k: _conv(v) for k, v in x.items()}
115
+ return x
116
+
117
+ return {k: _conv(v) for k, v in schema.items()}
118
+
119
+
120
+ # =======================
121
+ # Encoders / Decoders I/O
122
+ # =======================
123
+ def _to_numpy(x: Any) -> np.ndarray:
124
+ if isinstance(x, np.ndarray):
125
+ return x
126
+ if torch.is_tensor(x):
127
+ return x.detach().cpu().numpy()
128
+ raise TypeError(f"Unsupported array/tensor type: {type(x)}")
129
+
130
+
131
+ def _encode_png_3ch(value: Any) -> bytes:
132
+ """Encode to 3-channel uint8 PNG. Accepts PIL.Image, np.ndarray, or torch.Tensor."""
133
+ if isinstance(value, Image.Image):
134
+ img = value.convert("RGB")
135
+ arr = np.array(img, dtype=np.uint8)
136
+ else:
137
+ arr = _to_numpy(value)
138
+ # normalize layout
139
+ if arr.ndim == 3 and arr.shape[0] in (1, 3) and arr.shape[2] not in (1, 3):
140
+ # likely CHW -> HWC
141
+ arr = np.transpose(arr, (1, 2, 0))
142
+ if arr.ndim == 2:
143
+ arr = np.stack([arr, arr, arr], axis=-1)
144
+ if arr.ndim != 3 or arr.shape[-1] not in (1, 3):
145
+ raise ValueError(f"PNG expects HxWx{1|3}, got shape {arr.shape}")
146
+ if arr.shape[-1] == 1:
147
+ arr = np.repeat(arr, 3, axis=-1)
148
+
149
+ # dtype + range handling
150
+ if np.issubdtype(arr.dtype, np.floating):
151
+ arr = np.clip(arr, 0.0, 1.0)
152
+ arr = (arr * 255.0 + 0.5).astype(np.uint8)
153
+ elif arr.dtype != np.uint8:
154
+ arr = np.clip(arr, 0, 255).astype(np.uint8)
155
+
156
+ im = Image.fromarray(arr, mode="RGB")
157
+ buf = io.BytesIO()
158
+ im.save(buf, format="PNG", compress_level=4)
159
+ return buf.getvalue()
160
+
161
+
162
+ def _encode_exr(value: Any, compression: str = "PIZ") -> bytes:
163
+ """
164
+ Encode depth (float16/float32) as single-channel EXR ("Z") with compression.
165
+ Prefers OpenEXR (allows setting compression = PIZ/ZIP/etc.) then imageio, else raises.
166
+
167
+ Expected shapes: HxW or HxWx1. Returns bytes.
168
+ """
169
+ arr = _to_numpy(value)
170
+ if arr.ndim == 3 and arr.shape[-1] == 1:
171
+ arr = arr[..., 0]
172
+ if arr.ndim != 2:
173
+ raise ValueError(f"EXR expects HxW (single channel), got shape {arr.shape}")
174
+ arr = arr.astype(np.float32)
175
+
176
+ # Try OpenEXR first (full control on compression; best for high ratios)
177
+ import OpenEXR, Imath, tempfile, os
178
+
179
+ h, w = arr.shape
180
+ header = OpenEXR.Header(w, h)
181
+ comp = {
182
+ "NONE": Imath.Compression.NO_COMPRESSION,
183
+ "ZIP": Imath.Compression.ZIP_COMPRESSION,
184
+ "ZIPS": Imath.Compression.ZIPS_COMPRESSION,
185
+ "PIZ": Imath.Compression.PIZ_COMPRESSION,
186
+ "DWAA": Imath.Compression.DWAA_COMPRESSION,
187
+ "DWAB": Imath.Compression.DWAB_COMPRESSION,
188
+ "B44": Imath.Compression.B44_COMPRESSION,
189
+ "B44A": Imath.Compression.B44A_COMPRESSION,
190
+ }.get(compression.upper(), Imath.Compression.PIZ_COMPRESSION)
191
+ header["compression"] = Imath.Compression(comp)
192
+ header["channels"] = {"Z": Imath.Channel(Imath.PixelType(Imath.PixelType.FLOAT))}
193
+ with tempfile.NamedTemporaryFile(suffix=".exr", delete=False) as tmp:
194
+ outpath = tmp.name
195
+ try:
196
+ out = OpenEXR.OutputFile(outpath, header)
197
+ out.writePixels({"Z": arr.tobytes()})
198
+ out.close()
199
+ with open(outpath, "rb") as f:
200
+ return f.read()
201
+ finally:
202
+ os.remove(outpath)
203
+
204
+
205
+ def _decode_exr(data: bytes) -> np.ndarray:
206
+ """
207
+ Decode EXR bytes → numpy float32 array (HxW or HxWxC).
208
+ Ensures the OpenEXR.InputFile handle is closed to avoid C-level memory leaks.
209
+ """
210
+ import OpenEXR, Imath
211
+
212
+ exr = None
213
+ try:
214
+ with io.BytesIO(data) as f:
215
+ # Note: OpenEXR Python bindings support file-like objects.
216
+ exr = OpenEXR.InputFile(f)
217
+ header = exr.header()
218
+ dw = header["dataWindow"]
219
+ w, h = dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1
220
+ channels = list(header["channels"].keys())
221
+ pt = header["channels"][channels[0]].type
222
+ if pt == Imath.PixelType(Imath.PixelType.FLOAT):
223
+ np_dtype = np.float32
224
+ elif pt == Imath.PixelType(Imath.PixelType.HALF):
225
+ np_dtype = np.float16
226
+ elif pt == Imath.PixelType(Imath.PixelType.UINT):
227
+ np_dtype = np.uint32
228
+ else:
229
+ raise ValueError(f"Unsupported EXR pixel type: {pt}")
230
+ arrs = []
231
+ for c in channels:
232
+ ch_str = exr.channel(c)
233
+ arr = np.frombuffer(ch_str, dtype=np_dtype)
234
+ arr = arr.reshape((h, w))
235
+ arrs.append(arr)
236
+ if len(arrs) == 1:
237
+ return arrs[0]
238
+ return np.stack(arrs, axis=-1)
239
+ finally:
240
+ try:
241
+ if exr is not None:
242
+ exr.close()
243
+ except Exception:
244
+ # Swallow close errors; decoder already produced output
245
+ pass
246
+
247
+
248
+ def _encode_tensor(value: Any) -> bytes:
249
+ arr = _to_numpy(value)
250
+ buf = io.BytesIO()
251
+ np.save(buf, arr, allow_pickle=False)
252
+ return buf.getvalue()
253
+
254
+
255
+ def _encode_str(value: Any) -> bytes:
256
+ return str(value).encode("utf-8")
257
+
258
+
259
+ def _encode_json(value: Any) -> bytes:
260
+ return json.dumps(value).encode("utf-8")
261
+
262
+
263
+ def _encode_bytes(value: Any) -> bytes:
264
+ if isinstance(value, (bytes, bytearray, memoryview)):
265
+ return bytes(value)
266
+ raise TypeError("BYTES expects bytes-like input")
267
+
268
+
269
+ def _encode_by_dt(value: Any, dt: Any) -> bytes:
270
+ if value is None:
271
+ raise ValueError("Cannot encode None value")
272
+ dtv = _dt_value(dt)
273
+ if dtv == DT.PNG.value:
274
+ return _encode_png_3ch(value)
275
+ if dtv == DT.EXR.value:
276
+ return _encode_exr(value, compression="ZIP")
277
+ if dtv == DT.TENSOR.value:
278
+ return _encode_tensor(value)
279
+ if dtv == DT.STR.value:
280
+ return _encode_str(value)
281
+ if dtv == DT.JSON.value:
282
+ return _encode_json(value)
283
+ if dtv == DT.BYTES.value:
284
+ return _encode_bytes(value)
285
+ raise ValueError(f"Unhandled DT: {dt}")
286
+
287
+
288
+ # ============================
289
+ # (1) Partial builder (generic)
290
+ # ============================
291
+ def build_partial_dataset(
292
+ generate_data, # callable: gid -> dict with per-object fields and 'views' list of per-view dicts
293
+ gid_list: List[Any],
294
+ schema: Dict[
295
+ str, Any
296
+ ], # same structure as generate_data output, except schema["views"] is dict of {key: DT}
297
+ root_dir: str,
298
+ run_name: Optional[str] = None,
299
+ part_dir: Optional[
300
+ str
301
+ ] = None, # if given, overrides run_name and uses this dir directly
302
+ max_objects_per_shard: int = 64,
303
+ progress: bool = False,
304
+ ) -> Dict[str, str]:
305
+ """
306
+ Writes object-level WebDataset shards under {root_dir}/parts/{run}/objects-*.tar.
307
+
308
+ File naming (generic, type-driven):
309
+ - Per-object field 'K' (DT -> ext): 'obj_{K}.{ext}'
310
+ - Per-view field 'K' at index i: '{i:04d}.{K}.{ext}'
311
+ - Minimal header with counts: 'meta.json' (contains {'id': str(gid), 'num_views': int})
312
+
313
+ Nothing in this function assumes any concrete keys besides 'views'.
314
+ """
315
+ if "views" not in schema or not isinstance(schema["views"], dict):
316
+ raise ValueError(
317
+ "Schema must contain a 'views' dict describing per-view fields and their DT."
318
+ )
319
+
320
+ run = run_name or f"part-{uuid.uuid4().hex[:8]}"
321
+ part_dir = part_dir or os.path.join(root_dir, "parts", run)
322
+ _ensure_dir(part_dir)
323
+
324
+ # persist the schema for provenance
325
+ with open(os.path.join(part_dir, "schema.json"), "w", encoding="utf-8") as f:
326
+ json.dump(_schema_to_jsonable(schema), f, indent=2)
327
+
328
+ shard_pattern = os.path.join(part_dir, "objects-%06d.tar")
329
+ shardlist_path = os.path.join(part_dir, "shards.txt")
330
+ created = []
331
+
332
+ obj_fields = {k: v for k, v in schema.items() if k != "views"}
333
+ view_fields = schema["views"]
334
+
335
+ def obj_key_for(gid: Any) -> str:
336
+ return _sanitize_key(str(gid))
337
+
338
+ with wds.ShardWriter(shard_pattern, maxcount=max_objects_per_shard) as sink:
339
+ if progress:
340
+ gid_list = tqdm(gid_list, desc=f"Building dataset {run}")
341
+ for gid in gid_list:
342
+ data = generate_data(gid)
343
+ if "views" not in data or not isinstance(data["views"], list):
344
+ raise ValueError(
345
+ "generate_data must return a dict with a 'views' list."
346
+ )
347
+
348
+ sample = {"__key__": obj_key_for(gid)}
349
+ # per-object fields
350
+ for k, dt in obj_fields.items():
351
+ if k == "views":
352
+ continue
353
+ if k in data and data[k] is not None:
354
+ ext = _dt_ext(dt)
355
+ sample[f"obj_{_sanitize_key(k)}.{ext}"] = _encode_by_dt(data[k], dt)
356
+
357
+ # per-view fields
358
+ num_views = len(data["views"])
359
+ for i, view in enumerate(data["views"]):
360
+ if not isinstance(view, dict):
361
+ raise ValueError("Each element of 'views' must be a dict.")
362
+ for vk, vdt in view_fields.items():
363
+ if vk in view and view[vk] is not None:
364
+ ext = _dt_ext(vdt)
365
+ name = f"{i:04d}.{_sanitize_key(vk)}.{ext}"
366
+ sample[name] = _encode_by_dt(view[vk], vdt)
367
+
368
+ # minimal meta
369
+ meta = {"id": sample["__key__"], "num_views": num_views}
370
+ sample["meta.json"] = json.dumps(meta).encode("utf-8")
371
+
372
+ sink.write(sample)
373
+ if getattr(sink, "fname", None) and (
374
+ not created or created[-1] != sink.fname
375
+ ):
376
+ created.append(sink.fname)
377
+
378
+ # finalize shard list
379
+ tars = sorted(glob.glob(os.path.join(part_dir, "objects-*.tar")))
380
+ if len(tars) != len(created):
381
+ created = tars
382
+ with open(shardlist_path, "w", encoding="utf-8") as f:
383
+ for p in created:
384
+ f.write(p + "\n")
385
+
386
+ with open(os.path.join(part_dir, "manifest.json"), "w", encoding="utf-8") as f:
387
+ json.dump(
388
+ {
389
+ "kind": "webdataset-object-generic-v1",
390
+ "created": _timestamp(),
391
+ "run": run,
392
+ "num_shards": len(created),
393
+ "root": os.path.abspath(root_dir),
394
+ },
395
+ f,
396
+ indent=2,
397
+ )
398
+
399
+ return {"part_dir": part_dir, "shardlist": shardlist_path}
400
+
401
+
402
+ # ===========================
403
+ # (2) Merge partials (O(1) IO)
404
+ # ===========================
405
+ def merge_partials(
406
+ root_dir: str,
407
+ part_dirs: List[str],
408
+ out_name: str = "merged",
409
+ materialize: bool = False, # if True, hardlink (or copy) shards into a single subdir
410
+ ) -> Dict[str, str]:
411
+ """
412
+ Creates {root_dir}/{out_name}/shards.txt covering all shards from given parts.
413
+ If materialize=True, hardlinks/copies shards into {root_dir}/{out_name}/shards/ and points shards.txt there.
414
+ """
415
+ out_dir = os.path.join(root_dir, out_name)
416
+ _ensure_dir(out_dir)
417
+ shards_txt = os.path.join(out_dir, "shards.txt")
418
+
419
+ all_shards: List[str] = []
420
+ for pd in part_dirs:
421
+ pd = os.path.abspath(pd)
422
+ txt = os.path.join(pd, "shards.txt")
423
+ if os.path.isfile(txt):
424
+ with open(txt, "r", encoding="utf-8") as f:
425
+ all_shards.extend([ln.strip() for ln in f if ln.strip()])
426
+ else:
427
+ all_shards.extend(sorted(glob.glob(os.path.join(pd, "objects-*.tar"))))
428
+
429
+ if materialize:
430
+ link_dir = os.path.join(out_dir, "shards")
431
+ _ensure_dir(link_dir)
432
+ linked = []
433
+ for i, src in enumerate(all_shards):
434
+ dst = os.path.join(link_dir, f"{i:06d}.tar")
435
+ try:
436
+ if os.path.exists(dst):
437
+ os.remove(dst)
438
+ os.link(src, dst) # fast if same filesystem
439
+ except OSError:
440
+ shutil.copy2(src, dst) # fallback across devices
441
+ linked.append(dst)
442
+ all_shards = linked
443
+
444
+ with open(shards_txt, "w", encoding="utf-8") as f:
445
+ for p in all_shards:
446
+ f.write(p + "\n")
447
+
448
+ # write a minimal manifest
449
+ with open(os.path.join(out_dir, "manifest.json"), "w", encoding="utf-8") as f:
450
+ json.dump(
451
+ {
452
+ "kind": "webdataset-merged",
453
+ "created": _timestamp(),
454
+ "num_shards": len(all_shards),
455
+ },
456
+ f,
457
+ indent=2,
458
+ )
459
+
460
+ # copy first available schema.json for convenience (optional)
461
+ for pd in part_dirs:
462
+ sch = os.path.join(pd, "schema.json")
463
+ if os.path.isfile(sch):
464
+ shutil.copy2(sch, os.path.join(out_dir, "schema.json"))
465
+ break
466
+
467
+ return {"dataset_dir": out_dir, "shardlist": shards_txt}
468
+
469
+
470
+ # =========================================
471
+ # (3) Loading: row-wise and object-wise views
472
+ # =========================================
473
+ def _resolve_urls(dataset_path: str) -> Union[str, List[str]]:
474
+ p = pathlib.Path(dataset_path)
475
+ if p.is_dir():
476
+ txt = p / "shards.txt"
477
+ if txt.exists():
478
+ with open(txt, "r", encoding="utf-8") as f:
479
+ return [ln.strip() for ln in f if ln.strip()]
480
+ tars = sorted(p.glob("*.tar"))
481
+ if tars:
482
+ return [str(x) for x in tars]
483
+ if p.suffix == ".txt" and p.exists():
484
+ with open(p, "r", encoding="utf-8") as f:
485
+ return [ln.strip() for ln in f if ln.strip()]
486
+ return dataset_path # brace/glob allowed by WebDataset
487
+
488
+
489
+ def build_mcvae_dataset(
490
+ dataset_path: str,
491
+ schema: Dict[str, Any],
492
+ size: int = 1024, # final image size (H=W=size)
493
+ shardshuffle: int = 1000,
494
+ decode: Union[str, Any] = "torch", # "pil" or "torch" or a handler
495
+ split_by_node: bool = True,
496
+ split_by_worker: bool = True,
497
+ ):
498
+ """
499
+ Returns an IterableDataset that yields per-view dicts.
500
+ Each item includes all per-object fields injected into the row.
501
+ Missing fields are omitted rather than set to None.
502
+ """
503
+ if "views" not in schema or not isinstance(schema["views"], dict):
504
+ raise ValueError("Schema must contain a 'views' dict.")
505
+
506
+ urls = _resolve_urls(dataset_path)
507
+ obj_fields = {k: v for k, v in schema.items() if k != "views"}
508
+ view_fields = schema["views"]
509
+
510
+ # ---- 1) Pre-process (unpacking rows) ----
511
+ def _preprocess(sample: Dict[str, Any]) -> List[Dict[str, Any]]:
512
+ # meta.json is bytes (we didn't register a JSON handler) → parse explicitly
513
+ meta = sample["meta.json"]
514
+ gid = meta["id"]
515
+ n = int(meta["num_views"])
516
+
517
+ # per-object fields
518
+ per_object: Dict[str, Any] = {}
519
+ for k, dt in obj_fields.items():
520
+ ext = _dt_ext(dt)
521
+ name = f"obj_{_sanitize_key(k)}.{ext}"
522
+ if name in sample:
523
+ per_object[k] = sample[name]
524
+
525
+ # per-view fields
526
+ rows: List[Dict[str, Any]] = []
527
+ for idx in range(n):
528
+ v = {"id": gid, "view_index": idx}
529
+ v.update(per_object)
530
+ for vk, vdt in view_fields.items():
531
+ ext = _dt_ext(vdt)
532
+ vname = f"{idx:04d}.{_sanitize_key(vk)}.{ext}"
533
+ if vname in sample:
534
+ v[vk] = sample[vname]
535
+ rows.append(v)
536
+ return rows
537
+
538
+ # ---- 2) Post-process (resizing, dataset-specific tweaks) ----
539
+ def _postprocess(sample: Dict[str, Any]) -> Dict[str, Any]:
540
+ out: Dict[str, Any] = {}
541
+ for k, v in sample.items():
542
+ if isinstance(v, np.ndarray):
543
+ out[k] = torch.from_numpy(v.copy())
544
+ else:
545
+ out[k] = v
546
+
547
+ # Dataset-specific postprocessing
548
+ def _resize_(key: str):
549
+ out[key] = F.interpolate(
550
+ out[key].unsqueeze(0),
551
+ size=(size, size),
552
+ mode="bilinear",
553
+ align_corners=False,
554
+ ).squeeze(0)
555
+
556
+ for key in ("albedo", "orm"):
557
+ _resize_(key)
558
+ out[key] = out[key] * 2 - 1
559
+
560
+ # ready-to-use tensors
561
+ # print(out['albedo'].shape, out['orm'].shape)
562
+ out["pixel_values"] = torch.cat(
563
+ [out["albedo"], out["orm"][1:, ...]], dim=0
564
+ ) # (5,H,W)
565
+
566
+ # cleanup
567
+ for k in (
568
+ "prompt",
569
+ "prompt_embeds",
570
+ "pooled_prompt_embeds",
571
+ "elevation",
572
+ "azimuth",
573
+ "id",
574
+ "view_index",
575
+ "c2w",
576
+ "scale",
577
+ "pos",
578
+ "normal",
579
+ "depth",
580
+ ):
581
+ out.pop(k, None)
582
+
583
+ return out
584
+
585
+ # For mcVAE training we don't use depth EXR; avoid decoding EXR entirely to reduce memory pressure
586
+ # and eliminate potential leaks from the EXR decoder. Keep PNG decoding for albedo/orm.
587
+ ds = (
588
+ wds.WebDataset(
589
+ urls,
590
+ nodesplitter=wds.split_by_node if split_by_node else None,
591
+ workersplitter=wds.split_by_worker if split_by_worker else None,
592
+ shardshuffle=shardshuffle,
593
+ )
594
+ .decode(
595
+ wds.imagehandler(decode),
596
+ )
597
+ .map(_preprocess)
598
+ .unlisted()
599
+ .map(_postprocess)
600
+ )
601
+ return ds
602
+
603
+
604
+ def build_mcdiff_dataset(
605
+ dataset_path: str,
606
+ schema: Dict[str, Any],
607
+ num_views: int,
608
+ size: int = 1024,
609
+ shardshuffle: int = 1000,
610
+ decode: Union[str, Any] = "torch",
611
+ split_by_node: bool = True,
612
+ split_by_worker: bool = True,
613
+ fixed_view_indices: Optional[List[int]] = None,
614
+ ):
615
+
616
+ if "views" not in schema or not isinstance(schema["views"], dict):
617
+ raise ValueError("Schema must contain a 'views' dict.")
618
+
619
+ if fixed_view_indices is not None:
620
+ assert (
621
+ len(fixed_view_indices) == num_views
622
+ ), "fixed_view_indices length must match num_views"
623
+
624
+ assert size % 16 == 0 and size <= 1024, "size must be multiple of 16 and <= 1024"
625
+
626
+ urls = _resolve_urls(dataset_path)
627
+ obj_fields = {k: v for k, v in schema.items() if k != "views"}
628
+ view_fields = schema["views"]
629
+
630
+ # ---- 1) Pre-sampling (view selection) ----
631
+ def _presample_filter(sample: Dict[str, Any]) -> Dict[str, Any]:
632
+ if "meta.json" not in sample:
633
+ return None
634
+ meta = json.loads(sample["meta.json"].decode("utf-8"))
635
+ n = int(meta["num_views"])
636
+ if not (1 <= num_views <= n):
637
+ return None
638
+
639
+ if fixed_view_indices is not None:
640
+ chosen = fixed_view_indices
641
+ else:
642
+ chosen = random.sample(
643
+ range(n), num_views
644
+ ) # epoch마다 DataLoader 시드에 따라 바뀜
645
+
646
+ kept: Dict[str, Any] = {}
647
+ # keep metadata
648
+ kept["meta.json"] = sample["meta.json"]
649
+ if "__key__" in sample:
650
+ kept["__key__"] = sample["__key__"]
651
+
652
+ # keep per-object fields
653
+ for k, dt in obj_fields.items():
654
+ ext = _dt_ext(dt)
655
+ name = f"obj_{_sanitize_key(k)}.{ext}"
656
+ if name in sample:
657
+ kept[name] = sample[name]
658
+
659
+ # keep selected per-view fields
660
+ for idx in chosen:
661
+ for vk, vdt in view_fields.items():
662
+ ext = _dt_ext(vdt)
663
+ vname = f"{idx:04d}.{_sanitize_key(vk)}.{ext}"
664
+ if vname in sample:
665
+ kept[vname] = sample[vname]
666
+
667
+ # store chosen indices for later use
668
+ kept["_sel.json"] = json.dumps(chosen).encode("utf-8")
669
+ return kept
670
+
671
+ # ---- 2) Post-process (view stacking, resizing, dataset-specific tweaks) ----
672
+ def _postprocess(sample: Dict[str, Any]) -> Dict[str, Any]:
673
+ # restore selection order
674
+ chosen = sample["_sel.json"]
675
+
676
+ # per-object fields
677
+ out: Dict[str, Any] = {}
678
+ for k, dt in obj_fields.items():
679
+ ext = _dt_ext(dt)
680
+ name = f"obj_{_sanitize_key(k)}.{ext}"
681
+ if name in sample:
682
+ out[k] = sample[name]
683
+
684
+ # selected per-view fields
685
+ views: List[Dict[str, Any]] = []
686
+ for idx in chosen:
687
+ v: Dict[str, Any] = {}
688
+ for vk, vdt in view_fields.items():
689
+ ext = _dt_ext(vdt)
690
+ vname = f"{idx:04d}.{_sanitize_key(vk)}.{ext}"
691
+ if vname in sample:
692
+ v[vk] = sample[vname]
693
+ if isinstance(v[vk], np.ndarray):
694
+ v[vk] = torch.from_numpy(v[vk].copy())
695
+ v["view_index"] = idx
696
+ views.append(v)
697
+
698
+ if len(views) == 0:
699
+ return None
700
+
701
+ for k in views[0].keys():
702
+ arrs = [v[k] for v in views]
703
+ if isinstance(arrs[0], torch.Tensor):
704
+ out[k] = torch.stack(arrs, dim=0)
705
+ elif isinstance(arrs[0], np.ndarray):
706
+ out[k] = torch.from_numpy(np.stack(arrs, axis=0))
707
+ else:
708
+ out[k] = arrs # e.g., list of strings
709
+
710
+ # Dataset-specific postprocessing
711
+
712
+ # normal, depth, pos: (V,3,H,W); albedo, orm: (V,3,H,W) , c2w: (V,4,4)
713
+ # Always operate in single-channel mode - batched postprocessing will handle dual-channel
714
+
715
+ out["normal_fullres"] = out["normal"] * 2 - 1 # [-1,1], for CAA
716
+
717
+ mask = out["orm"][:, 0, :, :] > 0.5
718
+ out["depth"][~mask] = float("nan")
719
+
720
+ pos_raw = out["pos"] - 0.5 # [-0.5, 0.5], for pos_token
721
+ df = int(out["pos"].shape[2] // (size // 16))
722
+ mask_f = mask.float().unsqueeze(1) # (V,1,H,W)
723
+
724
+ # Weighted block average over valid pixels
725
+ pos_sum = F.avg_pool2d(
726
+ pos_raw * mask_f, kernel_size=df, stride=df, divisor_override=1
727
+ )
728
+ cnt = F.avg_pool2d(mask_f, kernel_size=df, stride=df, divisor_override=1)
729
+ pos_ds = pos_sum / cnt.clamp_min(1.0)
730
+ out["pos_token"] = pos_ds
731
+
732
+ def _resize_(key: str):
733
+ out[key] = F.interpolate(
734
+ out[key], size=(size, size), mode="bilinear", align_corners=False
735
+ )
736
+
737
+ for key in ("albedo", "orm", "pos", "normal"):
738
+ _resize_(key)
739
+ out[key] = out[key] * 2 - 1
740
+
741
+ # ready-to-use tensors (always single-channel mode)
742
+ out["pixel_values"] = torch.cat(
743
+ [out["albedo"], out["orm"][:, 1:, ...]], dim=1
744
+ ) # (V,5,H,W)
745
+
746
+ out["cond_values"] = torch.cat([out["pos"], out["normal"]], dim=1) # (V,6,H,W)
747
+
748
+ out["scale"] = out["scale"][0] # common across views
749
+ out["w2c"] = torch.linalg.inv(out["c2w"]) # (V,4,4)
750
+ # albedo, orm, depth, pos, pos_token, normal, pixel_values, cond_values, w2c, normal_fullres
751
+
752
+ # cleanup
753
+ for k in ("prompt", "elevation", "azimuth"):
754
+ out.pop(k, None)
755
+ return out
756
+
757
+ # ---- 3) Build dataset ----
758
+ ds = (
759
+ wds.WebDataset(
760
+ urls,
761
+ nodesplitter=wds.split_by_node if split_by_node else None,
762
+ workersplitter=wds.split_by_worker if split_by_worker else None,
763
+ shardshuffle=shardshuffle,
764
+ )
765
+ .map(_presample_filter)
766
+ .decode(
767
+ wds.handle_extension("exr", _decode_exr),
768
+ wds.imagehandler(decode),
769
+ )
770
+ .map(_postprocess)
771
+ )
772
+
773
+ return ds
774
+
775
+ if __name__ == "__main__":
776
+ import argparse
777
+
778
+ parser = argparse.ArgumentParser(
779
+ description="Merge partial RGBMR dataset shards into a single dataset"
780
+ )
781
+ parser.add_argument(
782
+ "--root_dir",
783
+ type=str,
784
+ required=True,
785
+ help="Root directory where the merged dataset will be created"
786
+ )
787
+ parser.add_argument(
788
+ "--part_dirs",
789
+ type=str,
790
+ nargs="+",
791
+ required=True,
792
+ help="List of partial dataset directories to merge"
793
+ )
794
+ parser.add_argument(
795
+ "--out_name",
796
+ type=str,
797
+ default="merged",
798
+ help="Name of the output merged dataset directory (default: merged)"
799
+ )
800
+ parser.add_argument(
801
+ "--materialize",
802
+ action="store_true",
803
+ help="If set, hardlink/copy shards into a single directory instead of just listing paths"
804
+ )
805
+
806
+ args = parser.parse_args()
807
+
808
+ print(f"[{_timestamp()}] Starting merge operation...")
809
+ print(f" Root directory: {args.root_dir}")
810
+ print(f" Partial directories: {args.part_dirs}")
811
+ print(f" Output name: {args.out_name}")
812
+ print(f" Materialize: {args.materialize}")
813
+
814
+ result = merge_partials(
815
+ root_dir=args.root_dir,
816
+ part_dirs=args.part_dirs,
817
+ out_name=args.out_name,
818
+ materialize=args.materialize
819
+ )
820
+
821
+ print(f"\n[{_timestamp()}] Merge completed successfully!")
822
+ print(f" Dataset directory: {result['dataset_dir']}")
823
+ print(f" Shard list: {result['shardlist']}")
824
+
825
+ # Count total shards
826
+ with open(result['shardlist'], 'r') as f:
827
+ num_shards = len([line for line in f if line.strip()])
828
+ print(f" Total shards: {num_shards}")
829
+
830
+
831
+
832
+ '''
833
+ python data/rgbmr_dataset.py --root_dir /home/aaaaa/data/rgbmr_mv_web2/ \
834
+ --part_dirs \
835
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_00_of_07 \
836
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_01_of_07 \
837
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_02_of_07 \
838
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_03_of_07 \
839
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_04_of_07 \
840
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_05_of_07 \
841
+ /home/aaaaa/data/rgbmr_mv_web2/parts/render_06_of_07 \
842
+ --out_name merged --materialize
843
+
844
+
845
+ '''
home/ubuntu/aaaaa/data/rgbmr/debug_uv_mask.png ADDED
home/ubuntu/aaaaa/data/rgbmr/filter_complex.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, sys
2
+ import json
3
+ import glob
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torchvision.transforms import ToTensor, ToPILImage
8
+ from scipy.ndimage import distance_transform_edt
9
+ from PIL import Image
10
+ from PIL import ImageDraw, ImageFont
11
+ from tqdm.auto import tqdm
12
+
13
+ def grid_images(images, ncols=4, labels=None):
14
+ nrows = (len(images) + ncols - 1) // ncols
15
+ w, h = images[0].size
16
+ grid = Image.new('RGB', (ncols * w, nrows * h))
17
+ for idx, img in enumerate(images):
18
+ row = idx // ncols
19
+ col = idx % ncols
20
+ grid.paste(img, (col * w, row * h))
21
+
22
+ if labels is not None:
23
+ assert len(labels) == ncols
24
+ grid_tmp = Image.new('RGB', (ncols * w, nrows * h + 20), (255, 255, 255))
25
+ grid_tmp.paste(grid, (0, 20))
26
+ grid = grid_tmp
27
+
28
+ # draw labels
29
+ font = ImageFont.load_default(size=16)
30
+ draw = ImageDraw.Draw(grid)
31
+ for col, label in enumerate(labels):
32
+ draw.text((col * w + 5, 0), label, fill=(0, 0, 0), font=font)
33
+
34
+ return grid
35
+
36
+ # ----------------- 내부 유틸 -----------------
37
+ def _erode_mask(mask: torch.Tensor, pixels: int) -> torch.Tensor:
38
+ """
39
+ distance transform 기반 마스크 수축(AA 링 제거).
40
+ 입력은 torch.Tensor([H,W] 또는 [1,H,W]) 혹은 numpy.ndarray([H,W]).
41
+ 출력은 torch.bool 텐서([H,W]).
42
+ """
43
+ # normalize input
44
+ m = mask
45
+ orig_device = m.device
46
+ m = m.squeeze().to(dtype=torch.float32) # [H,W]
47
+ if pixels <= 0:
48
+ return (m > 0.5).to(dtype=torch.bool, device=orig_device)
49
+ # EDT in numpy (CPU)
50
+ m_np = (m.detach().cpu().numpy() > 0.5)
51
+ d = distance_transform_edt(m_np)
52
+ out_np = (d > pixels)
53
+ out = torch.from_numpy(out_np).to(dtype=torch.bool, device=orig_device)
54
+ return out
55
+
56
+ def _mean_grad(x: torch.Tensor, mask: torch.Tensor) -> float:
57
+ """평균 |∇| (Sobel 기반, torch 전용 경로).
58
+ - x: torch.Tensor([H,W] 또는 [1,H,W]) 혹은 np.ndarray([H,W])
59
+ - mask: torch.bool Tensor([H,W]) 또는 동일 shape의 numpy bool
60
+ 반환: float
61
+ """
62
+ # to torch tensors
63
+ xt = torch.from_numpy(x) if isinstance(x, np.ndarray) else x
64
+ mt = torch.from_numpy(mask) if isinstance(mask, np.ndarray) else mask
65
+ xt = xt.squeeze().to(dtype=torch.float32) # [H,W]
66
+ mt = mt.squeeze().to(dtype=torch.bool) # [H,W]
67
+ dev = xt.device
68
+ x4 = xt.unsqueeze(0).unsqueeze(0) # [1,1,H,W]
69
+ # Sobel kernels
70
+ kx = torch.tensor([[1, 0, -1],[2, 0, -2],[1, 0, -1]], device=dev, dtype=x4.dtype).view(1,1,3,3) / 8.0
71
+ ky = torch.tensor([[1, 2, 1],[0, 0, 0],[-1,-2,-1]], device=dev, dtype=x4.dtype).view(1,1,3,3) / 8.0
72
+ gx = F.conv2d(x4, kx, padding=1)
73
+ gy = F.conv2d(x4, ky, padding=1)
74
+ g = torch.sqrt(gx * gx + gy * gy + 1e-6).squeeze(0).squeeze(0) # [H,W]
75
+ vals = g[mt]
76
+ return float(vals.mean().item()) if vals.numel() else 0.0
77
+
78
+ # ----------------- 공개 API -----------------
79
+ def complexity_score_rm(
80
+ rough: torch.Tensor, metallic: torch.Tensor, mask: torch.Tensor,
81
+ erode_px: int = 2,
82
+ ) -> tuple[float, float]:
83
+ """
84
+ rough/metallic/마스크 -> (r_grad*100, m_grad*100).
85
+ 입력은 torch.Tensor([H,W]) 또는 numpy.ndarray([H,W]); mask는 {0,1} 또는 bool.
86
+ """
87
+ rmask = _erode_mask(mask, erode_px) # torch.bool [H,W]
88
+
89
+ r_grad = _mean_grad(rough, rmask)
90
+ m_grad = _mean_grad(metallic, rmask)
91
+ return r_grad * 100.0, m_grad * 100.0
92
+
93
+ def is_complex_rm(
94
+ rough: torch.Tensor, metallic: torch.Tensor, mask: torch.Tensor,
95
+ thres_avg=0.5, thres_r=0.05, thres_m=0.05, erode_px: int = 2
96
+ ) -> bool:
97
+ """
98
+ 복잡 여부(bool) 반환.
99
+ - 평균 임계치와 채널별 최소 임계치로 판단.
100
+ """
101
+ r_grad, m_grad = complexity_score_rm(rough, metallic, mask, erode_px=erode_px)
102
+ avg_grad = (r_grad + m_grad) / 2.0
103
+ return (avg_grad >= thres_avg) and (r_grad >= thres_r) and (m_grad >= thres_m)
104
+
105
+ to_tensor = ToTensor()
106
+
107
+ img_root = 'temp_outputs/'
108
+ # format: img_root/<object_id>/<albedo, metallic, roughness>/<00, 01>.png
109
+
110
+ object_ids = sorted(os.listdir(img_root))
111
+ # exclude id including 'warp'.
112
+ object_ids = [oid for oid in object_ids if 'warp' not in oid]
113
+
114
+ # filter folder having all required images
115
+ valid_ids = []
116
+ for obj_id in tqdm(object_ids):
117
+ required_paths = [
118
+ os.path.join(img_root, obj_id, 'albedo', '00.png'),
119
+ os.path.join(img_root, obj_id, 'albedo', '01.png'),
120
+ os.path.join(img_root, obj_id, 'metallic', '00.png'),
121
+ os.path.join(img_root, obj_id, 'metallic', '01.png'),
122
+ os.path.join(img_root, obj_id, 'roughness', '00.png'),
123
+ os.path.join(img_root, obj_id, 'roughness', '01.png'),
124
+ ]
125
+ if all(os.path.isfile(p) for p in required_paths):
126
+ valid_ids.append(obj_id)
127
+ object_ids = valid_ids
128
+
129
+ print(f'Found {len(object_ids)} objects.')
130
+
131
+ complex_ids = []
132
+ for obj_id in tqdm(object_ids):
133
+ ok = False
134
+ for view in ['00', '01']:
135
+ albedo = Image.open(os.path.join(img_root, obj_id, 'albedo', f'{view}.png'))
136
+ metallic = Image.open(os.path.join(img_root, obj_id, 'metallic', f'{view}.png'))
137
+ roughness = Image.open(os.path.join(img_root, obj_id, 'roughness', f'{view}.png'))
138
+ # infer mask: background region have r=g=b=M=R=0.5.
139
+ albedo_t = to_tensor(albedo) # [3,H,W], float32, [0,1]
140
+ metallic_t = to_tensor(metallic)[0:1] # [1,H,W], float32, [0,1]
141
+ roughness_t = to_tensor(roughness)[0:1] # [1,H,W], float32, [0,1]
142
+ h, w = albedo_t.shape[1], albedo_t.shape[2]
143
+ rgbmr = torch.cat([
144
+ albedo_t, metallic_t, roughness_t
145
+ ], dim=0) # [5,H,W]
146
+ mask_t = ~((rgbmr - 0.5).abs() < 2e-3).all(dim=0) # [H,W], bool
147
+ # complexity check
148
+ is_complex = is_complex_rm(
149
+ roughness_t.squeeze(0), metallic_t.squeeze(0), mask_t,
150
+ thres_avg=0.5, thres_r=0.05, thres_m=0.05, erode_px=5
151
+ )
152
+ if is_complex:
153
+ ok = True
154
+ break
155
+ if not ok:
156
+ continue
157
+ complex_ids.append(obj_id)
158
+ tqdm.write(f'Complex object: {obj_id}')
159
+
160
+ # sort
161
+ complex_ids = sorted(complex_ids)
162
+
163
+ # export complex ids
164
+ with open('complex_object_ids.json', 'w') as f:
165
+ json.dump(complex_ids, f, indent=2)
home/ubuntu/aaaaa/data/rgbmr/inference.py ADDED
@@ -0,0 +1,1211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mesh-to-Textured-Mesh Inference Pipeline
3
+ Mesh (.glb/.obj) + text prompt -> shaded .glb
4
+ """
5
+
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Tuple, Dict, Any, Optional, List
10
+ import math
11
+ import json
12
+ import filelock
13
+ import hashlib
14
+
15
+ import numpy as np
16
+ import torch
17
+ import torch.nn as nn
18
+ import torch.nn.functional as F
19
+ from diffusers import FlowMatchEulerDiscreteScheduler, SD3Transformer2DModel, AutoencoderKL
20
+ from torchvision.transforms import ToPILImage
21
+ from PIL import Image
22
+ from omegaconf import OmegaConf
23
+ from peft import set_peft_model_state_dict
24
+ from safetensors.torch import load_file
25
+
26
+ from mcgen.utils.model_utils import load_offset_autoencoder, initialize_transformer_weights
27
+ from mcgen.mcdiff.attention_processor_lora import LoRAMessagePassingAttnProcessor
28
+ from mcgen.mcdiff.attention_utils import apply_custom_processors
29
+ from mcgen.utils.text_encoder_utils import load_text_ctx, encode_prompt
30
+ from mcgen.mcdiff.latents import generate_latents
31
+ from mcgen.utils.pipeline_texture import TexturePipeline, ModProcessConfig
32
+ from mcgen.utils.correspondence import build_corr_pairs, downscale_pairs_to_f2l, dilate_f2l, correspondence_psnr
33
+ from mcgen.utils.config import load_config
34
+ from tools.utils.mesh_utils import load_mesh, get_orthogonal_camera, NVDiffRastContextWrapper, render
35
+ from mcgen.utils.image_super_utils import imageSuperNet
36
+ from mcgen.utils.pipeline_utils import ViewProcessor
37
+ from mcgen.utils.uvwrap_utils import mesh_uv_wrap
38
+ from mcgen.utils.geometry_inpaint_utils import texture_inpaint
39
+ from DifferentiableRenderer.MeshRender import MeshRender
40
+ import trimesh
41
+ import copy
42
+
43
+ # =============================================================================
44
+ # Configuration
45
+ # =============================================================================
46
+
47
+ # Model paths
48
+ SD35_REPO = "stabilityai/stable-diffusion-3.5-medium"
49
+ TRANSFORMER_PARTIAL_WEIGHTS = "outputs/mcdiff_v1.9.5/checkpoint-40000/transformer/diffusion_pytorch_model.safetensors"
50
+ SD_VAE_PATH = "./vae_sd35"
51
+ MCVAE_CONFIG_PATH = "./configs/mcvae/config.json"
52
+ MCVAE_CKPT_PATH = "./outputs/mcvae_v1.8.1.pt"
53
+ MCVAE_OFFSET_MODE = True # If True, mcVAE encoder outputs offsets added to base mean/logvar. If False, directly predicts mean/logvar.
54
+
55
+ # Device & precision
56
+ DEVICE = "cuda"
57
+ DTYPE = torch.float16
58
+
59
+ # Image & view settings
60
+ HEIGHT = WIDTH = 512
61
+ TOKEN_HW = HEIGHT // 16
62
+ NUM_VIEWS = 6
63
+ ELEV_DEG = [0.0, 0.0, 0.0, 0.0, 89.99, -89.99]
64
+ AZIM_DEG = [0.0, 90.0, 180.0, 270.0, 0.0, 0.0]
65
+ # ELEV_DEG = [0.0, 0.0, 0.0, 0.0, 45, 45, 90.0, -90.0]
66
+ # AZIM_DEG = [0.0, 90.0, 180.0, 270.0, 45, 135, 0.0, 0.0]
67
+
68
+ # MaterialMVP azimuth convention (90 degree offset from mvadapter)
69
+ AZIM_DEG_MATERIALMVP = [angle + 90 for angle in AZIM_DEG]
70
+
71
+ # Inference defaults
72
+ GUIDANCE = 4.0
73
+ STEPS = 30
74
+ NEGATIVE_PROMPT = ""
75
+
76
+
77
+ # =============================================================================
78
+ # Utility Functions
79
+ # =============================================================================
80
+
81
+ def seed_everything(seed: int = 42):
82
+ """Seed all random number generators for reproducibility."""
83
+ os.environ["PYTHONHASHSEED"] = str(seed)
84
+ np.random.seed(seed)
85
+ torch.manual_seed(seed)
86
+ torch.cuda.manual_seed_all(seed)
87
+ torch.backends.cudnn.deterministic = True
88
+ torch.backends.cudnn.benchmark = False
89
+
90
+
91
+ def _resize_to_pm1(x: torch.Tensor, *, height: int, width: int) -> torch.Tensor:
92
+ """Resize to target resolution and normalize to [-1, 1]."""
93
+ x = F.interpolate(x, size=(height, width), mode="bilinear", align_corners=False)
94
+ return x * 2.0 - 1.0
95
+
96
+
97
+ def _expand_embeddings_for_views(pe: torch.Tensor, pooled: torch.Tensor, *, num_views: int) -> Tuple[torch.Tensor, torch.Tensor]:
98
+ """Expand text embeddings from [1, seq, dim] to [num_views, seq, dim]."""
99
+ pe = pe.expand(1, -1, -1).unsqueeze(1).expand(-1, num_views, -1, -1).reshape(-1, pe.shape[1], pe.shape[2])
100
+ pooled = pooled.expand(1, -1).unsqueeze(1).expand(-1, num_views, -1).reshape(-1, pooled.shape[-1])
101
+ return pe, pooled
102
+
103
+
104
+ @torch.no_grad()
105
+ def _save_strip(tensor_bchw: torch.Tensor, path: str, nrow: int = 1) -> str:
106
+ """Save grid of images from batched tensors in [-1, 1] range.
107
+
108
+ Args:
109
+ tensor_bchw: Batch of images in BCHW format, range [-1, 1]
110
+ path: Output path for saved image
111
+ nrow: Number of rows in grid. If 1, creates horizontal strip.
112
+ """
113
+
114
+ to_pil = ToPILImage()
115
+ b, c, h, w = tensor_bchw.shape
116
+
117
+ nrow = max(1, nrow)
118
+ ncol = math.ceil(b / nrow)
119
+
120
+ canvas = Image.new('RGB', (ncol * w, nrow * h))
121
+ for i in range(b):
122
+ row = i // ncol
123
+ col = i % ncol
124
+ img = to_pil((tensor_bchw[i].clamp(-1, 1) * 0.5 + 0.5))
125
+ canvas.paste(img, (col * w, row * h))
126
+
127
+ os.makedirs(os.path.dirname(path), exist_ok=True)
128
+ canvas.save(path)
129
+ return path
130
+
131
+
132
+ def _update_cpsnr_json(json_path: str, gid: str, metrics: Dict[str, float]) -> None:
133
+ """Thread-safe update of cpsnr.json file using file locking.
134
+
135
+ Args:
136
+ json_path: Path to cpsnr.json file
137
+ gid: Geometry ID (mesh filename stem)
138
+ metrics: Dictionary containing 'albedo', 'roughness', 'metallic', 'total' keys
139
+ """
140
+ lock_path = json_path + ".lock"
141
+ lock = filelock.FileLock(lock_path, timeout=60)
142
+
143
+ try:
144
+ with lock:
145
+ # Read existing data
146
+ if os.path.exists(json_path):
147
+ with open(json_path, "r") as f:
148
+ data = json.load(f)
149
+ else:
150
+ data = {}
151
+
152
+ # Update with new metrics
153
+ data[gid] = metrics
154
+
155
+ # Write back atomically using temp file + rename
156
+ temp_path = json_path + ".tmp"
157
+ with open(temp_path, "w") as f:
158
+ json.dump(data, f, indent=2)
159
+ os.replace(temp_path, json_path)
160
+ finally:
161
+ # Clean up lock file if it exists and we can remove it
162
+ try:
163
+ if os.path.exists(lock_path):
164
+ os.remove(lock_path)
165
+ except OSError:
166
+ pass
167
+
168
+
169
+ def unproject_with_materialmvp(
170
+ mesh_path: str,
171
+ output_dir: str,
172
+ gid: str,
173
+ albedo_images: List[Image.Image],
174
+ mr_images: List[Image.Image],
175
+ *,
176
+ render_size: int = 512,
177
+ texture_size: int = 4096,
178
+ realesrgan_ckpt: str,
179
+ bake_exp: int = 2,
180
+ selected_view_weights: Optional[List[float]] = None,
181
+ ) -> str:
182
+ """Alternative unprojection backend using MeshRenderer and ViewProcessor.
183
+
184
+ This method uses the MaterialMVP approach with back-projection and baking,
185
+ using azimuth angles with a 90-degree offset from the mvadapter convention.
186
+
187
+ Args:
188
+ mesh_path: Path to input mesh
189
+ output_dir: Output directory
190
+ gid: Geometry ID (mesh filename stem)
191
+ albedo_images: List of albedo views (6 images)
192
+ mr_images: List of metallic-roughness views (6 images, ORM format)
193
+ render_size: Rendering resolution (default: 512)
194
+ texture_size: UV texture resolution (default: 4096)
195
+ realesrgan_ckpt: Path to RealESRGAN checkpoint
196
+ bake_exp: Baking exponent for view weighting
197
+ selected_view_weights: Optional list of per-view weights
198
+
199
+ Returns:
200
+ Path to saved GLB file
201
+ """
202
+ # Initialize renderer
203
+ render_obj = MeshRender(
204
+ default_resolution=render_size,
205
+ texture_size=texture_size,
206
+ bake_mode="back_sample",
207
+ raster_mode="cr",
208
+ )
209
+
210
+ # Load and prepare mesh
211
+ mesh = trimesh.load(mesh_path)
212
+ mesh = mesh_uv_wrap(mesh)
213
+ render_obj.load_mesh(mesh=mesh)
214
+
215
+ # Initialize processors
216
+ view_processor = ViewProcessor(bake_exp, render_obj)
217
+ super_model = imageSuperNet(realesrgan_ckpt)
218
+
219
+ # Use MaterialMVP azimuth convention (90-degree offset)
220
+ selected_camera_azims = AZIM_DEG_MATERIALMVP
221
+ selected_camera_elevs = ELEV_DEG
222
+
223
+ if selected_view_weights is None:
224
+ selected_view_weights = [1, 1, 1, 1, 0.5, 0.5]
225
+
226
+ # Prepare images dictionary
227
+ multiviews_pbr = {
228
+ "albedo": albedo_images,
229
+ "mr": mr_images,
230
+ }
231
+
232
+ # Enhance images
233
+ enhance_images = {
234
+ "albedo": copy.deepcopy(multiviews_pbr["albedo"]),
235
+ "mr": copy.deepcopy(multiviews_pbr["mr"]),
236
+ }
237
+
238
+ for i in range(len(enhance_images["albedo"])):
239
+ enhance_images["albedo"][i] = super_model(enhance_images["albedo"][i])
240
+ enhance_images["mr"][i] = super_model(enhance_images["mr"][i])
241
+
242
+ # Resize to double render size for better quality
243
+ for i in range(len(enhance_images["albedo"])):
244
+ enhance_images["albedo"][i] = enhance_images["albedo"][i].resize(
245
+ (render_size * 2, render_size * 2), Image.LANCZOS
246
+ )
247
+ enhance_images["mr"][i] = enhance_images["mr"][i].resize(
248
+ (render_size * 2, render_size * 2), Image.LANCZOS
249
+ )
250
+
251
+ # Bake albedo texture
252
+ texture, mask = view_processor.bake_from_multiview(
253
+ enhance_images["albedo"],
254
+ selected_camera_elevs,
255
+ selected_camera_azims,
256
+ selected_view_weights
257
+ )
258
+
259
+ # Bake metallic-roughness texture
260
+ texture_mr, mask_mr = view_processor.bake_from_multiview(
261
+ enhance_images["mr"],
262
+ selected_camera_elevs,
263
+ selected_camera_azims,
264
+ selected_view_weights
265
+ )
266
+
267
+ # Convert baked masks to boolean tensors
268
+ mask_bool = (mask.squeeze(-1) > 0.5).to(torch.bool)
269
+ mask_mr_bool = (mask_mr.squeeze(-1) > 0.5).to(torch.bool)
270
+
271
+ # Apply geometry-aware inpainting for albedo texture
272
+ texture_inpainted = texture_inpaint(
273
+ texture,
274
+ mask_bool,
275
+ render_obj,
276
+ uv_mask_erode_iters=10,
277
+ baked_mask_erode_iters=2,
278
+ vertex_merge_tolerance=1e-5,
279
+ vertex_color_K=11,
280
+ )
281
+ render_obj.set_texture(texture_inpainted, force_set=True)
282
+
283
+ # Apply geometry-aware inpainting for metallic-roughness texture
284
+ texture_mr_inpainted = texture_inpaint(
285
+ texture_mr,
286
+ mask_mr_bool,
287
+ render_obj,
288
+ uv_mask_erode_iters=10,
289
+ baked_mask_erode_iters=2,
290
+ vertex_merge_tolerance=1e-5,
291
+ vertex_color_K=11,
292
+ )
293
+ render_obj.set_texture_mr(texture_mr_inpainted)
294
+
295
+ # Save mesh
296
+ os.makedirs(output_dir, exist_ok=True)
297
+ output_path = os.path.join(output_dir, f"{gid}.glb")
298
+ render_obj.save_mesh(output_path, downsample=False)
299
+
300
+ return output_path
301
+
302
+ # =============================================================================
303
+ # Rendering & Preprocessing
304
+ # =============================================================================
305
+
306
+
307
+ @torch.no_grad()
308
+ def render_views(
309
+ mesh_path: str,
310
+ *,
311
+ num_views: int,
312
+ height: int,
313
+ width: int,
314
+ device: str = DEVICE,
315
+ ) -> Dict[str, Any]:
316
+ """Render multi-view geometry attributes from mesh."""
317
+ ctx = NVDiffRastContextWrapper(device=device, context_type="cuda")
318
+ mesh = load_mesh(str(mesh_path), rescale=True, move_to_center=True, flip_uv=True, device=device)
319
+ if len(ELEV_DEG) != num_views or len(AZIM_DEG) != num_views:
320
+ raise ValueError("ELEV_DEG and AZIM_DEG presets must match num_views.")
321
+
322
+ cams = get_orthogonal_camera(
323
+ elevation_deg=ELEV_DEG,
324
+ azimuth_deg=AZIM_DEG,
325
+ distance=[1.0] * num_views,
326
+ left=-0.55,
327
+ right=0.55,
328
+ bottom=-0.55,
329
+ top=0.55,
330
+ device=device, dtype=torch.float32,
331
+ )
332
+
333
+ # Build 5-channel texture override: [rgb, roughness(0), roughness, metallic]
334
+ tex_ovr = torch.cat([mesh.texture, torch.zeros_like(mesh.roughness), mesh.roughness, mesh.metallic], dim=-1)
335
+
336
+ out = render(ctx, mesh, cams, height=height, width=width, render_attr=True, texture_override=tex_ovr)
337
+
338
+ # Extract and normalize geometry attributes
339
+ attr = out.attr
340
+ rgb = attr[..., :3].contiguous()
341
+ orm = attr[..., 3:6].contiguous()
342
+ orm[..., 0] = (orm[..., 0] < 0.25).float() # Binary occupancy mask
343
+
344
+ return {
345
+ "albedo": rgb,
346
+ "orm": orm,
347
+ "pos": out.pos + 0.5, # [-0.5, 0.5] -> [0, 1]
348
+ "normal": (out.normal + 1.0) * 0.5, # [-1, 1] -> [0, 1]
349
+ "depth": out.raw_depth,
350
+ "c2w": cams.c2w,
351
+ "scale": torch.tensor(1.1, device=device),
352
+ }
353
+
354
+
355
+ def preprocess_geometry(
356
+ rend: Dict[str, Any],
357
+ *,
358
+ device: str,
359
+ dtype: torch.dtype,
360
+ height: int,
361
+ width: int,
362
+ token_hw: int,
363
+ num_views: int,
364
+ use_caa: bool,
365
+ corr_dilate_iterations: int = 1,
366
+ use_global_pos: bool = False,
367
+ ) -> Dict[str, torch.Tensor]:
368
+ """Preprocess rendered geometry into model-ready format."""
369
+ # Convert to channel-first
370
+ albedo = rend["albedo"].permute(0, 3, 1, 2).contiguous()
371
+ orm = rend["orm"].permute(0, 3, 1, 2).contiguous()
372
+ pos = rend["pos"].permute(0, 3, 1, 2).contiguous()
373
+ normal = rend["normal"].permute(0, 3, 1, 2).contiguous()
374
+ depth = rend["depth"]
375
+ c2w = rend["c2w"]
376
+ scale = rend["scale"]
377
+
378
+ V, _, H_raw, W_raw = albedo.shape
379
+ assert V == num_views, f"Expected {num_views} views, got {V}"
380
+
381
+ # Normal in [-1, 1] for correspondence computation
382
+ normal_fullres = normal * 2.0 - 1.0
383
+
384
+ # Mask from occupancy channel
385
+ mask = orm[:, 0, :, :] > 0.5
386
+ depth = depth.clone()
387
+ depth[~mask] = float("nan")
388
+
389
+ # Position tokens via weighted averaging at token grid
390
+ pos_raw = pos - 0.5 # Convert to [-0.5, 0.5]
391
+ df = H_raw // token_hw
392
+ mask_f = mask.float().unsqueeze(1)
393
+ pos_sum = F.avg_pool2d(pos_raw * mask_f, kernel_size=df, stride=df, divisor_override=1)
394
+ cnt = F.avg_pool2d(mask_f, kernel_size=df, stride=df, divisor_override=1)
395
+ pos_token = (pos_sum / cnt.clamp_min(1.0)).to(device=device, dtype=dtype)
396
+
397
+ # Resize and normalize to [-1, 1]
398
+ albedo_r = _resize_to_pm1(albedo, height=height, width=width)
399
+ orm_r = _resize_to_pm1(orm, height=height, width=width)
400
+ pos_r = _resize_to_pm1(pos, height=height, width=width)
401
+ normal_r = _resize_to_pm1(normal, height=height, width=width)
402
+
403
+ # Condition values: position + normal (6 channels)
404
+ cond_values = torch.cat([pos_r, normal_r], dim=1)
405
+
406
+ # World-to-camera transformation
407
+ w2c = torch.linalg.inv(c2w).to(device=device, dtype=dtype)
408
+
409
+ corr_f2l = None
410
+ corr_f2l_highres = None
411
+ pos_delta = None
412
+
413
+ # Build correspondence pairs
414
+ corr_pairs = build_corr_pairs(
415
+ c2w, scale, depth, nor_w=normal_fullres.permute(0, 2, 3, 1),
416
+ depth_tol=0.01, angle_tol=10, angle_cam_tol=80,
417
+ )
418
+
419
+ # High resolution correspondence for c-PSNR evaluation
420
+ corr_high = downscale_pairs_to_f2l(corr_pairs, out_hw=height, device=device)
421
+ corr_f2l_highres = corr_high.unsqueeze(0) # (1, V*H*W, K) for later use
422
+
423
+ # Downscale to token resolution
424
+ corr_low = downscale_pairs_to_f2l(corr_pairs, out_hw=token_hw, device=device)
425
+ for _ in range(corr_dilate_iterations):
426
+ corr_low = dilate_f2l(corr_low, V=num_views, out_hw=token_hw)
427
+
428
+ # Convert to global indices (single batch)
429
+ corr_f2l = corr_low.unsqueeze(0) # (1, Lq, K)
430
+ B, Lq, K = corr_f2l.shape
431
+ X = Lq
432
+ base = torch.arange(B, device=corr_f2l.device).view(B, 1, 1) * X
433
+ corr_f2l = torch.where(corr_f2l >= 0, corr_f2l + base, corr_f2l)
434
+ corr_f2l = corr_f2l.reshape(B * Lq, K) # (M, K) where M = num_views * token_hw * token_hw
435
+
436
+ # Compute position deltas
437
+ M = num_views * token_hw * token_hw
438
+
439
+ # pos_token: (V, 3, token_hw, token_hw) -> (M, 3)
440
+ pos_w_flat = pos_token.view(num_views, 3, -1).permute(0, 2, 1).reshape(M, 3) # (M, 3)
441
+
442
+ # Get camera transforms for each query token
443
+ batch_ids = torch.arange(num_views, device=device).repeat_interleave(token_hw * token_hw) # (M,)
444
+
445
+ # Use identity matrix for global frame, or w2c for local query frame
446
+ if use_global_pos:
447
+ # Global frame: use identity matrix (no transformation)
448
+ Tq = torch.eye(4, device=device, dtype=w2c.dtype).unsqueeze(0).expand(M, -1, -1) # (M, 4, 4)
449
+ else:
450
+ # Local query frame: use w2c transformation
451
+ Tq = w2c[batch_ids] # (M, 4, 4)
452
+
453
+ # Transform query positions to camera space
454
+ ones_M = torch.ones(M, 1, device=device, dtype=pos_w_flat.dtype)
455
+ pq_h = torch.cat([pos_w_flat, ones_M], dim=-1).unsqueeze(-1) # (M, 4, 1)
456
+ pq_cam = (Tq @ pq_h).squeeze(-1)[..., :3] # (M, 3)
457
+
458
+ # Gather key positions and transform to camera space
459
+ gather_idx = corr_f2l.clamp(min=0, max=M - 1).reshape(-1) # (M*K,)
460
+ pk_world = pos_w_flat.index_select(0, gather_idx).view(M, K, 3) # (M, K, 3)
461
+ ones_MK = torch.ones(M, K, 1, device=device, dtype=pk_world.dtype)
462
+ pk_h = torch.cat([pk_world, ones_MK], dim=-1).unsqueeze(-1) # (M, K, 4, 1)
463
+ pk_cam = (Tq[:, None, :, :] @ pk_h).squeeze(-1)[..., :3] # (M, K, 3)
464
+
465
+ # Compute delta
466
+ pos_delta = pk_cam - pq_cam[:, None, :] # (M, K, 3)
467
+
468
+ return {
469
+ "cond_values": cond_values,
470
+ "pos_token": pos_token,
471
+ "w2c": w2c,
472
+ "corr_f2l": corr_f2l,
473
+ "corr_f2l_highres": corr_f2l_highres,
474
+ "pos_delta": pos_delta,
475
+ "decoded_albedo": albedo_r,
476
+ "decoded_orm": orm_r,
477
+ }
478
+
479
+
480
+
481
+ # =============================================================================
482
+ # Model Setup
483
+ # =============================================================================
484
+
485
+
486
+
487
+ def _apply_lora_processors(
488
+ transformer: SD3Transformer2DModel,
489
+ model_cfg,
490
+ *,
491
+ use_caa: bool,
492
+ use_rope: bool,
493
+ use_global_token: bool,
494
+ num_views: int,
495
+ num_domains: int,
496
+ ) -> None:
497
+ """Attach LoRA message-passing processors mirroring training setup."""
498
+ lcfg = getattr(model_cfg, "attn_lora", None)
499
+ if not lcfg or not getattr(lcfg, "enabled", False):
500
+ return
501
+
502
+ select = {"joint": True, "self": bool(getattr(lcfg, "apply_to_self", False))}
503
+ limits = {}
504
+ if getattr(lcfg, "limit_joint", None) is not None:
505
+ limits["joint"] = lcfg.limit_joint
506
+ if getattr(lcfg, "limit_self", None) is not None:
507
+ limits["self"] = lcfg.limit_self
508
+
509
+ def processor_factory(_name, _mod, query_dim, inner_dim, out_dim, num_heads, kind="joint"):
510
+ return LoRAMessagePassingAttnProcessor(
511
+ r_q=int(getattr(lcfg, "r_q", 8)),
512
+ r_k=int(getattr(lcfg, "r_k", 8)),
513
+ r_v=int(getattr(lcfg, "r_v", 8)),
514
+ alpha_q=float(getattr(lcfg, "alpha_q", getattr(lcfg, "r_q", 8))),
515
+ alpha_k=float(getattr(lcfg, "alpha_k", getattr(lcfg, "r_k", 8))),
516
+ alpha_v=float(getattr(lcfg, "alpha_v", getattr(lcfg, "r_v", 8))),
517
+ query_dim=query_dim,
518
+ inner_dim=inner_dim,
519
+ out_dim=out_dim,
520
+ num_heads=num_heads,
521
+ num_views=num_views,
522
+ num_domains=num_domains,
523
+ use_caa=use_caa,
524
+ use_rope=use_rope,
525
+ use_global_token=use_global_token,
526
+ kind=kind,
527
+ )
528
+
529
+ apply_custom_processors(
530
+ transformer,
531
+ model_cfg,
532
+ processor_factory,
533
+ select=select,
534
+ limits=limits if limits else None,
535
+ as_factory=True,
536
+ )
537
+
538
+
539
+
540
+ def _extend_pos_embed_in(transformer: SD3Transformer2DModel, extra_channels: int) -> None:
541
+ """Extend transformer's pos_embed input channels by extra_channels."""
542
+ conv = transformer.pos_embed.proj
543
+ new_conv = nn.Conv2d(
544
+ in_channels=conv.in_channels + extra_channels,
545
+ out_channels=conv.out_channels,
546
+ kernel_size=conv.kernel_size, stride=conv.stride, padding=conv.padding,
547
+ dilation=conv.dilation, groups=conv.groups, padding_mode=conv.padding_mode,
548
+ device=conv.weight.device, dtype=conv.weight.dtype,
549
+ )
550
+ with torch.no_grad():
551
+ new_conv.weight.zero_()
552
+ new_conv.weight[:, :conv.in_channels].copy_(conv.weight)
553
+ new_conv.bias.copy_(conv.bias)
554
+ transformer.pos_embed.proj = new_conv
555
+
556
+
557
+ def _load_conditioning_conv(transformer: SD3Transformer2DModel, cond_path: Path) -> None:
558
+ if not cond_path.exists():
559
+ return
560
+ state = load_file(str(cond_path))
561
+ weight_key = "pos_embed.proj.weight"
562
+ bias_key = "pos_embed.proj.bias"
563
+ pe_proj = transformer.pos_embed.proj
564
+ if weight_key in state:
565
+ pe_proj.weight.data.copy_(state[weight_key].to(pe_proj.weight.device, dtype=pe_proj.weight.dtype))
566
+ if bias_key in state:
567
+ pe_proj.bias.data.copy_(state[bias_key].to(pe_proj.bias.device, dtype=pe_proj.bias.dtype))
568
+
569
+
570
+ def load_transformer_weights(
571
+ transformer: SD3Transformer2DModel,
572
+ weights_path: Optional[str],
573
+ *,
574
+ condition_channels: int,
575
+ ) -> None:
576
+ if not weights_path:
577
+ return
578
+ path = Path(weights_path)
579
+
580
+ if path.is_dir():
581
+ full_dir = path / "transformer"
582
+ lora_path = path / "pytorch_lora_weights.safetensors"
583
+ cond_path = path / "pytorch_cond_conv_weights.safetensors"
584
+
585
+ if full_dir.is_dir():
586
+ initialize_transformer_weights(transformer, str(full_dir))
587
+ elif lora_path.exists():
588
+ lora_state = load_file(str(lora_path))
589
+ set_peft_model_state_dict(transformer, lora_state, adapter_name="default")
590
+ else:
591
+ # Fallback: attempt direct load from directory
592
+ initialize_transformer_weights(transformer, str(path))
593
+
594
+ if condition_channels > 0:
595
+ _load_conditioning_conv(transformer, cond_path)
596
+ return
597
+
598
+ initialize_transformer_weights(transformer, str(path))
599
+
600
+
601
+ def setup_transformer(
602
+ *,
603
+ base_model: str,
604
+ model_cfg,
605
+ device: str,
606
+ dtype: torch.dtype,
607
+ condition_channels: int,
608
+ use_caa: bool,
609
+ use_rope: bool,
610
+ use_global_token: bool,
611
+ use_dual_branch: bool,
612
+ num_views: int,
613
+ weights_path: Optional[str],
614
+ ) -> Tuple[SD3Transformer2DModel, Any]:
615
+ """Setup transformer with structural modifications and load weights."""
616
+ transformer = SD3Transformer2DModel.from_pretrained(base_model, subfolder="transformer")
617
+
618
+ num_domains = 2 if use_dual_branch else 1
619
+ print(f'use_dual_branch: {use_dual_branch}, use_caa: {use_caa}, use_rope: {use_rope}, use_global_token: {use_global_token}, num_views: {num_views}, num_domains: {num_domains}')
620
+
621
+ _apply_lora_processors(
622
+ transformer,
623
+ model_cfg,
624
+ use_caa=use_caa,
625
+ use_rope=use_rope,
626
+ use_global_token=use_global_token,
627
+ num_views=num_views,
628
+ num_domains=num_domains,
629
+ )
630
+
631
+ if condition_channels > 0:
632
+ _extend_pos_embed_in(transformer, condition_channels)
633
+
634
+ load_transformer_weights(transformer, weights_path, condition_channels=condition_channels)
635
+
636
+ transformer = transformer.to(device=device, dtype=dtype).eval().requires_grad_(False)
637
+
638
+ scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(base_model, subfolder="scheduler")
639
+
640
+ return transformer, scheduler
641
+
642
+
643
+ def load_vae_models(
644
+ *,
645
+ sd_vae_path: str,
646
+ mcvae_config_path: str,
647
+ mcvae_ckpt_path: Optional[str],
648
+ mcvae_offset_mode: bool,
649
+ device: str,
650
+ dtype: torch.dtype,
651
+ use_dual_branch: bool,
652
+ ) -> Tuple[Any, Optional[Any]]:
653
+ """Load required autoencoders depending on branch configuration."""
654
+ if use_dual_branch:
655
+ vae = AutoencoderKL.from_pretrained(sd_vae_path)
656
+ vae = vae.to(device=device, dtype=dtype).eval().requires_grad_(False)
657
+ return vae, None
658
+
659
+ vae, mcvae = load_offset_autoencoder(
660
+ sd_vae_path=sd_vae_path,
661
+ mcvae_config_path=mcvae_config_path,
662
+ mcvae_ckpt_path=mcvae_ckpt_path,
663
+ offset_mode=mcvae_offset_mode,
664
+ )
665
+ vae = vae.to(device=device, dtype=dtype).eval().requires_grad_(False)
666
+ mcvae = mcvae.to(device=device, dtype=dtype).eval().requires_grad_(False)
667
+ return vae, mcvae
668
+
669
+
670
+ def encode_conditions(
671
+ vae: Any,
672
+ cond_values: torch.Tensor,
673
+ *,
674
+ height: int,
675
+ width: int,
676
+ out_dtype: torch.dtype,
677
+ ) -> torch.Tensor:
678
+ """Encode 6-channel condition values into latent space."""
679
+ cond_values = cond_values.to(device=vae.device, dtype=vae.dtype)
680
+ lat_chunks = []
681
+ for c in range(0, cond_values.shape[1], 3):
682
+ posterior = vae.encode(cond_values[:, c:c+3]).latent_dist
683
+ lat_chunks.append(posterior.mean)
684
+
685
+ cond_latents = torch.cat(lat_chunks, dim=1)
686
+ cond_latents = (cond_latents - vae.config.shift_factor) * vae.config.scaling_factor
687
+ Cc = cond_latents.shape[1]
688
+ cond_latents = cond_latents.reshape(-1, Cc, height // 8, width // 8)
689
+ return cond_latents.to(dtype=out_dtype)
690
+
691
+
692
+ def encode_text_prompts(
693
+ prompt: str,
694
+ *,
695
+ device: str,
696
+ dtype: torch.dtype,
697
+ base_model: str,
698
+ num_views: int,
699
+ negative_prompt: str,
700
+ ) -> Dict[str, torch.Tensor]:
701
+ """Encode positive and negative text prompts, expanded for all views."""
702
+ text_ctx = load_text_ctx(device=device, dtype=dtype, sd_model_name=base_model)
703
+
704
+ pe, pooled = encode_prompt(text_ctx["encoders"], text_ctx["tokenizers"], prompt, max_sequence_length=77)
705
+ npe, npooled = encode_prompt(text_ctx["encoders"], text_ctx["tokenizers"], negative_prompt, max_sequence_length=77)
706
+
707
+ # Clean up text context
708
+ del text_ctx
709
+ if torch.cuda.is_available():
710
+ torch.cuda.empty_cache()
711
+
712
+ # Move to device and expand for all views
713
+ pe, pooled = pe.to(device=device, dtype=dtype), pooled.to(device=device, dtype=dtype)
714
+ npe, npooled = npe.to(device=device, dtype=dtype), npooled.to(device=device, dtype=dtype)
715
+
716
+ pe, pooled = _expand_embeddings_for_views(pe, pooled, num_views=num_views)
717
+ npe, npooled = _expand_embeddings_for_views(npe, npooled, num_views=num_views)
718
+
719
+ return {
720
+ "prompt_embeds": pe,
721
+ "pooled_prompt_embeds": pooled,
722
+ "negative_prompt_embeds": npe,
723
+ "negative_pooled_prompt_embeds": npooled,
724
+ }
725
+
726
+
727
+ # =============================================================================
728
+ # Main Inference Pipeline
729
+ # =============================================================================
730
+
731
+ @torch.no_grad()
732
+ def run(
733
+ mesh_path: str,
734
+ prompt: str,
735
+ output_dir: str,
736
+ *,
737
+ settings: Dict[str, Any],
738
+ keep_strips: bool = False,
739
+ seed: int = 42,
740
+ ) -> str:
741
+ """Mesh + prompt → textured GLB using configured MC-Diff pipeline."""
742
+ gid = Path(mesh_path).stem
743
+ # Use hashlib.md5 for deterministic seed generation (hash() is not deterministic across runs)
744
+ hash_input = f"{gid}_{seed}".encode('utf-8')
745
+ combined_seed = int(hashlib.md5(hash_input).hexdigest(), 16) % (2**32)
746
+ seed_everything(combined_seed)
747
+
748
+ device = settings["device"]
749
+ dtype = settings["dtype"]
750
+ height = settings["resolution"]
751
+ width = settings["resolution"]
752
+ num_views = settings["num_views"]
753
+ token_hw = height // 16
754
+ use_caa = settings["use_caa"]
755
+ use_rope = settings["use_rope"]
756
+ use_global_token = settings["use_global_token"]
757
+ use_global_pos = settings.get("use_global_pos", False)
758
+ use_dual_branch = settings["use_dual_branch"]
759
+ branch_factor = 2 if use_dual_branch else 1
760
+
761
+ rend = render_views(mesh_path, num_views=num_views, height=height, width=width, device=device)
762
+ preprocessed = preprocess_geometry(
763
+ rend,
764
+ device=device,
765
+ dtype=dtype,
766
+ height=height,
767
+ width=width,
768
+ token_hw=token_hw,
769
+ num_views=num_views,
770
+ use_caa=use_caa,
771
+ corr_dilate_iterations=settings["corr_dilate_iterations"],
772
+ use_global_pos=use_global_pos,
773
+ )
774
+
775
+ vae, mcvae = load_vae_models(
776
+ sd_vae_path=settings["sd_vae_path"],
777
+ mcvae_config_path=settings["mcvae_config_path"],
778
+ mcvae_ckpt_path=settings["mcvae_ckpt_path"],
779
+ mcvae_offset_mode=settings.get("mcvae_offset_mode", True),
780
+ device=device,
781
+ dtype=dtype,
782
+ use_dual_branch=use_dual_branch,
783
+ )
784
+ vae_shift, vae_scale = vae.config.shift_factor, vae.config.scaling_factor
785
+
786
+ cond_latents = None
787
+ condition_channels = 0
788
+ if settings.get("condition_channels_cfg", 0) > 0:
789
+ cond_latents = encode_conditions(
790
+ vae,
791
+ preprocessed["cond_values"],
792
+ height=height,
793
+ width=width,
794
+ out_dtype=dtype,
795
+ )
796
+ condition_channels = cond_latents.shape[1]
797
+
798
+ text_embeds = encode_text_prompts(
799
+ prompt,
800
+ device=device,
801
+ dtype=dtype,
802
+ base_model=settings["base_model"],
803
+ num_views=num_views,
804
+ negative_prompt=settings["negative_prompt"],
805
+ )
806
+
807
+ transformer, scheduler = setup_transformer(
808
+ base_model=settings["base_model"],
809
+ model_cfg=settings["model_cfg"],
810
+ device=device,
811
+ dtype=dtype,
812
+ condition_channels=condition_channels,
813
+ use_caa=use_caa,
814
+ use_rope=use_rope,
815
+ use_global_token=use_global_token,
816
+ use_dual_branch=use_dual_branch,
817
+ num_views=num_views,
818
+ weights_path=settings["weights_path"],
819
+ )
820
+
821
+ def _repeat_branch(tensor: Optional[torch.Tensor]) -> Optional[torch.Tensor]:
822
+ if tensor is None:
823
+ return None
824
+ first_dim = tensor.shape[0]
825
+ if first_dim == num_views:
826
+ tensor = tensor.reshape(1, num_views, *tensor.shape[1:])
827
+ elif first_dim == 1:
828
+ tensor = tensor.reshape(1, *tensor.shape[1:]).unsqueeze(1)
829
+ else:
830
+ return tensor
831
+ tensor = tensor.repeat(branch_factor, 1, *[1] * (tensor.dim() - 2))
832
+ return tensor.reshape(branch_factor * num_views, *tensor.shape[2:])
833
+
834
+ prompt_embeds = _repeat_branch(text_embeds["prompt_embeds"])
835
+ pooled_prompt_embeds = _repeat_branch(text_embeds["pooled_prompt_embeds"])
836
+ negative_prompt_embeds = _repeat_branch(text_embeds["negative_prompt_embeds"])
837
+ negative_pooled_prompt_embeds = _repeat_branch(text_embeds["negative_pooled_prompt_embeds"])
838
+ cond_latents_branch = _repeat_branch(cond_latents) if cond_latents is not None else None
839
+
840
+ # Prepare correspondence data for CAA
841
+ corr_lookups_branch = None
842
+ pos_delta_branch = None
843
+ if use_caa:
844
+ corr_f2l = preprocessed["corr_f2l"] # (M, K) where M = num_views * token_hw^2
845
+ pos_delta = preprocessed["pos_delta"] # (M, K, 3)
846
+
847
+ if corr_f2l is not None and pos_delta is not None:
848
+ # For dual branch, duplicate correspondence data
849
+ if use_dual_branch:
850
+ # corr_f2l needs to be adjusted for dual branch
851
+ # Each branch has its own set of indices
852
+ M, K = corr_f2l.shape
853
+ corr_branch_list = []
854
+ for b in range(branch_factor):
855
+ corr_b = corr_f2l + (b * M) # Offset indices for each branch
856
+ corr_branch_list.append(corr_b)
857
+ corr_lookups_branch = torch.cat(corr_branch_list, dim=0) # (branch_factor * M, K)
858
+ pos_delta_branch = pos_delta.repeat(branch_factor, 1, 1) # (branch_factor * M, K, 3)
859
+ else:
860
+ corr_lookups_branch = corr_f2l
861
+ pos_delta_branch = pos_delta
862
+
863
+ latents = generate_latents(
864
+ transformer=transformer,
865
+ noise_scheduler=scheduler,
866
+ prompt_embeds=prompt_embeds,
867
+ pooled_prompt_embeds=pooled_prompt_embeds,
868
+ negative_prompt_embeds=negative_prompt_embeds,
869
+ negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
870
+ height=height,
871
+ width=width,
872
+ num_inference_steps=settings["steps"],
873
+ guidance_scale=settings["guidance_scale"],
874
+ weight_dtype=dtype,
875
+ device=device,
876
+ condition_channels=condition_channels,
877
+ condition_latents=cond_latents_branch,
878
+ corr_lookups=corr_lookups_branch,
879
+ pos_delta=pos_delta_branch,
880
+ progress=True,
881
+ )
882
+
883
+ inv = (latents / vae_scale) + vae_shift
884
+
885
+ if use_dual_branch:
886
+ decoded = vae.decode(inv.to(device=vae.device, dtype=vae.dtype)).sample
887
+ decoded = decoded.view(branch_factor, num_views, 3, height, width)
888
+ albedo_pred = decoded[0]
889
+ orm_pred_full = decoded[1]
890
+ combined = torch.cat([albedo_pred, orm_pred_full[:, 1:, :, :]], dim=1)
891
+ combined_flat = combined.reshape(num_views, 5, height, width)
892
+ else:
893
+ inv_mc = inv.to(device=mcvae.device, dtype=mcvae.dtype)
894
+ decoded = mcvae.decode_aug(inv_mc).sample
895
+ combined_flat = decoded.reshape(num_views, 5, height, width)
896
+ albedo_pred = combined_flat[:, :3]
897
+ orm_pred_full = combined_flat[:, 3:]
898
+
899
+ # Compute c-PSNR if correspondence data is available
900
+ cpsnr_metrics = None
901
+ if preprocessed.get("corr_f2l_highres") is not None:
902
+ corr_high = preprocessed["corr_f2l_highres"] # (1, V*H*W, K)
903
+
904
+ # Convert to global indices (similar to training)
905
+ B, VHW, K = corr_high.shape
906
+ base = torch.arange(B, device=corr_high.device).view(B, 1, 1) * VHW
907
+ corr_high_global = torch.where(corr_high >= 0, corr_high + base, corr_high)
908
+ corr_high_global = corr_high_global.reshape(B * VHW, K) # (V*H*W, K)
909
+
910
+ # Prepare predictions in (B, C, H, W) format
911
+ pred_rgb = combined_flat[:, :3].unsqueeze(0) # (1, V, 3, H, W) -> need (V, 3, H, W)
912
+ pred_rgb = pred_rgb.squeeze(0) # (V, 3, H, W)
913
+ pred_mr = combined_flat[:, 3:].unsqueeze(0).squeeze(0) # (V, 2, H, W)
914
+
915
+ # Compute c-PSNR for each component
916
+ albedo_psnr, _, num_elements = correspondence_psnr(pred_rgb, corr_high_global, data_range=2.0)
917
+ roughness_psnr, _, _ = correspondence_psnr(pred_mr[:, 0:1], corr_high_global, data_range=2.0)
918
+ metallic_psnr, _, _ = correspondence_psnr(pred_mr[:, 1:2], corr_high_global, data_range=2.0)
919
+
920
+ # Combined 5-channel c-PSNR
921
+ pred_combined = combined_flat.unsqueeze(0).squeeze(0) # (V, 5, H, W)
922
+ total_psnr, _, _ = correspondence_psnr(pred_combined, corr_high_global, data_range=2.0)
923
+
924
+ cpsnr_metrics = {
925
+ "albedo": float(albedo_psnr.item()),
926
+ "roughness": float(roughness_psnr.item()),
927
+ "metallic": float(metallic_psnr.item()),
928
+ "total": float(total_psnr.item()),
929
+ "num_elements": num_elements,
930
+ }
931
+
932
+ dec_albedo = albedo_pred
933
+ if use_dual_branch:
934
+ dec_orm = torch.cat([
935
+ torch.full_like(orm_pred_full[:, :1], fill_value=-1.0),
936
+ orm_pred_full[:, 1:, :, :],
937
+ ], dim=1)
938
+ else:
939
+ dec_orm = torch.cat([
940
+ torch.full_like(combined_flat[:, :1], fill_value=-1.0),
941
+ combined_flat[:, (3, 4)],
942
+ ], dim=1)
943
+
944
+ os.makedirs(output_dir, exist_ok=True)
945
+
946
+ if keep_strips:
947
+ albedo_path = os.path.join(output_dir, f"{gid}_albedo_strip.png")
948
+ orm_path = os.path.join(output_dir, f"{gid}_orm_strip.png")
949
+ pos = preprocessed["cond_values"][:, :3].clone()
950
+ normal = preprocessed["cond_values"][:, 3:].clone()
951
+ cond_values_path = os.path.join(output_dir, f"{gid}_cond_values_strip.png")
952
+ cond_values_strip = torch.cat([(pos + 1.0) * 0.5, (normal + 1.0) * 0.5], dim=0).to("cpu")
953
+ _save_strip(cond_values_strip, cond_values_path, nrow=2)
954
+ else:
955
+ albedo_fd, albedo_path = tempfile.mkstemp(suffix="_albedo_strip.png")
956
+ orm_fd, orm_path = tempfile.mkstemp(suffix="_orm_strip.png")
957
+ os.close(albedo_fd)
958
+ os.close(orm_fd)
959
+
960
+ dec_albedo_cpu = dec_albedo.detach().to("cpu")
961
+ dec_orm_cpu = dec_orm.detach().to("cpu")
962
+
963
+ _save_strip(dec_albedo_cpu, albedo_path)
964
+ _save_strip(dec_orm_cpu, orm_path)
965
+
966
+ unproj_backend = settings.get("unproj_backend", "mvadapter")
967
+
968
+ if unproj_backend == "materialmvp":
969
+ # MaterialMVP backend: use MeshRenderer + ViewProcessor
970
+ # Convert strips to individual view images
971
+ albedo_strip_img = Image.open(albedo_path)
972
+ orm_strip_img = Image.open(orm_path)
973
+
974
+ strip_width = albedo_strip_img.width
975
+ strip_height = albedo_strip_img.height
976
+ view_width = strip_width // num_views
977
+
978
+ albedo_views = []
979
+ mr_views = []
980
+
981
+ for i in range(num_views):
982
+ left = i * view_width
983
+ right = (i + 1) * view_width
984
+
985
+ # Extract albedo view
986
+ albedo_view = albedo_strip_img.crop((left, 0, right, strip_height))
987
+ albedo_views.append(albedo_view)
988
+
989
+ # Extract MR from ORM (channels 1 and 2)
990
+ orm_view = orm_strip_img.crop((left, 0, right, strip_height))
991
+ mr_views.append(orm_view)
992
+
993
+ output_path = unproject_with_materialmvp(
994
+ mesh_path=mesh_path,
995
+ output_dir=output_dir,
996
+ gid=gid,
997
+ albedo_images=albedo_views,
998
+ mr_images=mr_views,
999
+ render_size=height,
1000
+ texture_size=settings["uv_size"],
1001
+ realesrgan_ckpt=settings["realesrgan_ckpt"],
1002
+ bake_exp=2,
1003
+ )
1004
+
1005
+ # Clean up temp files if needed
1006
+ if not keep_strips:
1007
+ for path in (albedo_path, orm_path):
1008
+ if path and os.path.exists(path):
1009
+ try:
1010
+ os.remove(path)
1011
+ except OSError:
1012
+ pass
1013
+
1014
+ else: # mvadapter (default)
1015
+ tex_pipeline = TexturePipeline(
1016
+ upscaler_ckpt_path=settings["realesrgan_ckpt"],
1017
+ inpaint_ckpt_path=settings["lama_ckpt"],
1018
+ device=device,
1019
+ )
1020
+
1021
+ try:
1022
+ result = tex_pipeline(
1023
+ mesh_path=mesh_path,
1024
+ save_dir=output_dir,
1025
+ save_name=gid,
1026
+ uv_unwarp=True,
1027
+ preprocess_mesh=False,
1028
+ move_to_center=True,
1029
+ uv_size=settings["uv_size"],
1030
+ base_color_path=albedo_path,
1031
+ orm_path=orm_path,
1032
+ base_color_process_config=ModProcessConfig(view_upscale=True, inpaint_mode="view"),
1033
+ orm_process_config=ModProcessConfig(view_upscale=True, inpaint_mode="view"),
1034
+ camera_elevation_deg=ELEV_DEG,
1035
+ camera_azimuth_deg=AZIM_DEG,
1036
+ )
1037
+ output_path = result.pbr_model_save_path
1038
+ finally:
1039
+ if not keep_strips:
1040
+ for path in (albedo_path, orm_path):
1041
+ if path and os.path.exists(path):
1042
+ try:
1043
+ os.remove(path)
1044
+ except OSError:
1045
+ pass
1046
+
1047
+ # Save c-PSNR metrics if computed
1048
+ if cpsnr_metrics is not None:
1049
+ cpsnr_json_path = os.path.join(output_dir, "cpsnr.json")
1050
+ _update_cpsnr_json(cpsnr_json_path, gid, cpsnr_metrics)
1051
+
1052
+ return output_path
1053
+
1054
+
1055
+ def build_inference_settings(args, cfg) -> Dict[str, Any]:
1056
+ """Build inference settings from config file and CLI overrides.
1057
+
1058
+ Priority order (highest to lowest):
1059
+ 1. CLI arguments (--num-views, --resolution, etc.) - for backwards compatibility
1060
+ 2. Config file with CLI overrides (key=value format)
1061
+
1062
+ Args:
1063
+ args: Parsed argparse namespace with script-specific arguments
1064
+ cfg: OmegaConf config loaded with base-variant inheritance and CLI overrides
1065
+ """
1066
+ model_cfg = cfg.model
1067
+
1068
+ # Apply CLI argument overrides to config (backwards compatibility)
1069
+ # These take precedence over --opts to maintain expected behavior
1070
+ if args.num_views is not None:
1071
+ model_cfg.num_views = args.num_views
1072
+ if args.resolution is not None:
1073
+ model_cfg.resolution = args.resolution
1074
+ if args.sd_vae_path is not None:
1075
+ model_cfg.sd_vae_path = args.sd_vae_path
1076
+ if args.mcvae_config is not None:
1077
+ model_cfg.mcvae_config = args.mcvae_config
1078
+ if args.mcvae_ckpt is not None:
1079
+ model_cfg.mcvae_ckpt = args.mcvae_ckpt
1080
+
1081
+ dtype_map = {
1082
+ "fp16": torch.float16,
1083
+ "bf16": torch.bfloat16,
1084
+ "fp32": torch.float32,
1085
+ }
1086
+ dtype = dtype_map.get(args.precision.lower(), DTYPE)
1087
+
1088
+ settings = {
1089
+ "device": args.device,
1090
+ "dtype": dtype,
1091
+ "resolution": int(model_cfg.resolution),
1092
+ "num_views": int(model_cfg.num_views),
1093
+ "use_caa": bool(getattr(model_cfg, "use_caa", True)),
1094
+ "use_rope": bool(getattr(model_cfg, "use_rope", False)),
1095
+ "use_global_token": bool(getattr(model_cfg, "use_global_token", False)),
1096
+ "use_global_pos": bool(getattr(model_cfg, "use_global_pos", False)),
1097
+ "use_dual_branch": bool(getattr(model_cfg, "use_dual_branch", False)),
1098
+ "corr_dilate_iterations": int(getattr(model_cfg, "corr_dilate_iterations", 2)),
1099
+ "condition_channels_cfg": int(getattr(model_cfg, "condition_channels", 0)),
1100
+ "base_model": model_cfg.pretrained_model_name_or_path,
1101
+ "model_cfg": model_cfg,
1102
+ "weights_path": args.weights or TRANSFORMER_PARTIAL_WEIGHTS,
1103
+ "sd_vae_path": model_cfg.sd_vae_path,
1104
+ "mcvae_config_path": model_cfg.mcvae_config,
1105
+ "mcvae_ckpt_path": model_cfg.mcvae_ckpt or MCVAE_CKPT_PATH,
1106
+ "mcvae_offset_mode": bool(getattr(model_cfg, "mcvae_offset_mode", True)),
1107
+ "negative_prompt": args.negative_prompt,
1108
+ "steps": args.steps,
1109
+ "guidance_scale": args.guidance,
1110
+ "realesrgan_ckpt": args.realesrgan_ckpt,
1111
+ "lama_ckpt": args.lama_ckpt,
1112
+ "uv_size": args.uv_size,
1113
+ "unproj_backend": args.unproj_backend,
1114
+ "config": cfg,
1115
+ }
1116
+
1117
+ return settings
1118
+
1119
+
1120
+ # =============================================================================
1121
+ # CLI Entry Point
1122
+ # =============================================================================
1123
+
1124
+
1125
+
1126
+ if __name__ == "__main__":
1127
+ import argparse
1128
+
1129
+ parser = argparse.ArgumentParser(
1130
+ description="Mesh + Prompt → Textured GLB",
1131
+ epilog="""
1132
+ Additional config overrides can be provided as key=value arguments:
1133
+ python inference.py --mesh model.glb --prompt "..." model.num_views=8 model.use_caa=false
1134
+ """
1135
+ )
1136
+ parser.add_argument("--mesh", required=True, help="Path to input mesh (.glb or .obj)")
1137
+ parser.add_argument("--prompt", required=True, help="Text prompt for texture generation")
1138
+ parser.add_argument("--out", default="./temp_outputs", help="Output directory")
1139
+ parser.add_argument("--keep-strips", action="store_true", help="Keep intermediate albedo/ORM strip images")
1140
+ parser.add_argument("--guidance", type=float, default=GUIDANCE, help=f"Guidance scale (default: {GUIDANCE})")
1141
+ parser.add_argument("--steps", type=int, default=STEPS, help=f"Number of denoising steps (default: {STEPS})")
1142
+ parser.add_argument("--seed", type=int, default=42, help="Random seed (default: 42)")
1143
+ parser.add_argument("--negative-prompt", default=NEGATIVE_PROMPT, help="Negative prompt text")
1144
+ parser.add_argument("--device", default=DEVICE, help="Torch device to run inference on")
1145
+ parser.add_argument("--precision", default="fp16", choices=["fp16", "bf16", "fp32"], help="Computation precision")
1146
+ parser.add_argument("--num-views", type=int, default=None, help="Override number of camera views")
1147
+ parser.add_argument("--resolution", type=int, default=None, help="Override render resolution")
1148
+ parser.add_argument("--sd-vae-path", default=None, help="Override base SD VAE path")
1149
+ parser.add_argument("--mcvae-config", default=None, help="Override mcVAE config path")
1150
+ parser.add_argument("--mcvae-ckpt", default=None, help="Override mcVAE checkpoint path")
1151
+ parser.add_argument("--weights", default=None, help="Path to trained transformer weights (dir or file)")
1152
+ parser.add_argument("--realesrgan-ckpt", default="./checkpoints/RealESRGAN_x4plus.pth", help="RealESRGAN checkpoint path (x4plus for materialmvp, x2plus for mvadapter)")
1153
+ parser.add_argument("--lama-ckpt", default="./checkpoints/big-lama.pt", help="LaMa inpainting checkpoint path")
1154
+ parser.add_argument("--uv-size", type=int, default=4096, help="Final UV texture resolution")
1155
+ parser.add_argument("--config", default="configs/mcdiff/default.yaml", help="Config file (supports base-variant inheritance)")
1156
+ parser.add_argument("--unproj-backend", default="materialmvp", choices=["mvadapter", "materialmvp"],
1157
+ help="Unprojection backend: 'mvadapter' (default, uses TexturePipeline) or 'materialmvp' (uses MeshRenderer+ViewProcessor)")
1158
+
1159
+ # Parse known args (script-specific flags), unknown args will be config overrides (key=value)
1160
+ args, unknown = parser.parse_known_args()
1161
+
1162
+ # Load config with base-variant inheritance and CLI overrides
1163
+ cfg = load_config(args.config, cli_overrides=unknown)
1164
+
1165
+ # Build settings with both argparse args and loaded config
1166
+ settings = build_inference_settings(args, cfg)
1167
+
1168
+ output_path = run(
1169
+ mesh_path=args.mesh,
1170
+ prompt=args.prompt,
1171
+ output_dir=args.out,
1172
+ settings=settings,
1173
+ keep_strips=args.keep_strips,
1174
+ seed=args.seed,
1175
+ )
1176
+
1177
+ print(f"✓ Saved: {output_path}")
1178
+
1179
+
1180
+ '''
1181
+ python inference.py --keep-strips \
1182
+ --mesh /home/aaaaa/data/Arb-Objaverse/data/glb/000-132/21ec37b286474fedb43307f6f289269e.glb \
1183
+ --prompt "A wooden stool with a pink cushion and shiny, dark wood finish, detailed with tufting and decorative white buttons, suitable for interior design visualizations." \
1184
+ --config configs/mcdiff/dual_full.yaml \
1185
+ --weights outputs/mcdiff_v.dual_full/checkpoint-40000/transformer/diffusion_pytorch_model.safetensors \
1186
+ --out removethis/dual_full
1187
+
1188
+ python inference.py --keep-strips \
1189
+ --mesh /home/aaaaa/data/Arb-Objaverse/data/glb/000-132/21ec37b286474fedb43307f6f289269e.glb \
1190
+ --prompt "A wooden stool with a pink cushion and shiny, dark wood finish, detailed with tufting and decorative white buttons, suitable for interior design visualizations." \
1191
+ --config configs/mcdiff/single_caa.yaml \
1192
+ --weights outputs/mcdiff_v.single_caa/checkpoint-35000/transformer/diffusion_pytorch_model.safetensors \
1193
+ --out removethis/single_caa
1194
+
1195
+
1196
+ python inference.py --keep-strips \
1197
+ --mesh /home/aaaaa/data/Arb-Objaverse/data/glb/000-132/21ec37b286474fedb43307f6f289269e.glb \
1198
+ --prompt "A wooden stool with a pink cushion and shiny, dark wood finish, detailed with tufting and decorative white buttons, suitable for interior design visualizations." \
1199
+ --config configs/mcdiff/single_caa_global.yaml \
1200
+ --weights outputs/mcdiff_v.single_caa_global/checkpoint-5000/transformer/diffusion_pytorch_model.safetensors \
1201
+ --out removethis/single_caa_global
1202
+
1203
+ # With MaterialMVP unprojection backend:
1204
+ python inference.py --keep-strips \
1205
+ --mesh /home/aaaaa/data/Arb-Objaverse/data/glb/000-132/21ec37b286474fedb43307f6f289269e.glb \
1206
+ --prompt "A wooden stool with a pink cushion and shiny, dark wood finish, detailed with tufting and decorative white buttons, suitable for interior design visualizations." \
1207
+ --config configs/mcdiff/single_caa.yaml \
1208
+ --weights outputs/mcdiff_v.single_caa/checkpoint-35000/transformer/diffusion_pytorch_model.safetensors \
1209
+ --out removethis/single_caa_materialmvp \
1210
+ --unproj-backend materialmvp
1211
+ '''
home/ubuntu/aaaaa/data/rgbmr/inference_batch.py ADDED
@@ -0,0 +1,658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ run_infer_batch.py
4
+
5
+ Parallel runner for inference.py using queued GPU assignment.
6
+ - Reads GLB paths from a text file (one path per line; lines starting with '#' ignored).
7
+ - Reads GLB id (file stem) -> prompt mapping from a CSV file.
8
+ - For each GLB, looks up its stem in the CSV to get a text prompt.
9
+ - Schedules processes across provided CUDA devices with a fair queue.
10
+ - Applies a per-process timeout.
11
+
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import csv
17
+ import shlex
18
+ import argparse
19
+ import asyncio
20
+ from typing import Dict, List, Tuple
21
+
22
+ # -----------------------------------------------------------------------------
23
+ # print utils (robust import with fallbacks)
24
+ # -----------------------------------------------------------------------------
25
+ from mcgen.utils.print_utils import (
26
+ print_header,
27
+ print_footer,
28
+ print_info,
29
+ print_error,
30
+ print_warning,
31
+ )
32
+
33
+ # -----------------------------------------------------------------------------
34
+ # Try to import shared helpers from the reference script; provide fallbacks if needed
35
+ # -----------------------------------------------------------------------------
36
+ from tools.render_eval_batch import (
37
+ print_box,
38
+ read_list_file,
39
+ ensure_exists,
40
+ quote,
41
+ run_tests,
42
+ deterministic_hdr_index,
43
+ build_blender_command,
44
+ default_script_path as default_render_script_path,
45
+ )
46
+
47
+ # -----------------------------------------------------------------------------
48
+ # Script-specific utilities
49
+ # -----------------------------------------------------------------------------
50
+
51
+ def default_infer_script_path() -> str:
52
+ here = os.path.dirname(os.path.abspath(__file__))
53
+ return os.path.join(here, "inference.py")
54
+
55
+
56
+ def default_infer_list_script_path() -> str:
57
+ here = os.path.dirname(os.path.abspath(__file__))
58
+ return os.path.join(here, "inference_list.py")
59
+
60
+
61
+ def stem_of(path: str) -> str:
62
+ return os.path.splitext(os.path.basename(path))[0]
63
+
64
+
65
+ def read_prompt_csv(path: str) -> Dict[str, str]:
66
+ """Read headered CSV with columns: id, prompt."""
67
+ p = os.path.expanduser(path)
68
+ if not os.path.exists(p):
69
+ raise FileNotFoundError(f"Prompt CSV not found: {p}")
70
+
71
+ mapping: Dict[str, str] = {}
72
+ with open(p, "r", encoding="utf-8", newline="") as f:
73
+ for row in csv.DictReader(f):
74
+ rid = (row.get("id") or "").strip()
75
+ prompt = (row.get("prompt") or "").replace("\n", " ").strip()
76
+ if not rid:
77
+ continue
78
+ if not prompt:
79
+ print_warning(f"Empty prompt for id: {rid}; skipping")
80
+ continue
81
+ if rid in mapping:
82
+ print_warning(f"Duplicate id in CSV; last wins: {rid}")
83
+ mapping[rid] = prompt
84
+ return mapping
85
+
86
+
87
+ def build_infer_command(
88
+ python_exec: str,
89
+ script: str,
90
+ glb_path: str,
91
+ prompt: str,
92
+ out_dir: str,
93
+ extra_args: List[str] | None = None,
94
+ ) -> str:
95
+ """Build a shell command:
96
+ python_exec script --mesh GLB --prompt PROMPT --out OUT [extras...]
97
+ """
98
+ parts = [
99
+ quote(python_exec),
100
+ quote(script),
101
+ "--mesh", quote(glb_path),
102
+ "--prompt", quote(prompt),
103
+ "--out", quote(out_dir),
104
+ ]
105
+ if extra_args:
106
+ parts.extend(extra_args)
107
+ return " ".join(parts)
108
+
109
+
110
+ def build_infer_list_command(
111
+ python_exec: str,
112
+ script: str,
113
+ glb_paths: List[str],
114
+ prompts: List[str],
115
+ out_dir: str,
116
+ extra_args: List[str] | None = None,
117
+ ) -> str:
118
+ """Build a shell command for inference_list.py:
119
+ python_exec script --mesh GLB1 GLB2 ... --prompt P1 P2 ... --out OUT [extras...]
120
+ """
121
+ parts = [
122
+ quote(python_exec),
123
+ quote(script),
124
+ "--mesh",
125
+ ]
126
+ parts.extend([quote(p) for p in glb_paths])
127
+ parts.append("--prompt")
128
+ parts.extend([quote(p) for p in prompts])
129
+ parts.extend(["--out", quote(out_dir)])
130
+ if extra_args:
131
+ parts.extend(extra_args)
132
+ return " ".join(parts)
133
+
134
+
135
+ # -----------------------------------------------------------------------------
136
+ # CLI & main
137
+ # -----------------------------------------------------------------------------
138
+
139
+ def default_evaluate_script_path() -> str:
140
+ here = os.path.dirname(os.path.abspath(__file__))
141
+ return os.path.join(here, "tools", "evaluate.py")
142
+
143
+
144
+ def parse_args():
145
+ p = argparse.ArgumentParser(description="Parallel runner for inference.py (GLB generation) + rendering + evaluation")
146
+ p.add_argument("--glb_list", required=True, help="TXT file with GLB paths (one per line)")
147
+ p.add_argument("--prompt_csv", required=True, help="CSV file mapping GLB stem -> text prompt")
148
+ p.add_argument("--output_dir", required=True, help="Base output directory (passed to --out)")
149
+
150
+ # Parallel processing controls
151
+ p.add_argument("--devices", nargs="+", required=True, help="List of CUDA devices (e.g., 0 1 2)")
152
+ p.add_argument("--timeout", type=int, default=999999, help="Maximum runtime per command (seconds)")
153
+
154
+ # Optional QoL flags for inference
155
+ p.add_argument(
156
+ "--python_exec",
157
+ default=os.environ.get("PYTHON_EXEC", sys.executable),
158
+ help="Python interpreter to run inference (default: current Python or $PYTHON_EXEC)",
159
+ )
160
+ p.add_argument(
161
+ "--script",
162
+ default=default_infer_script_path(),
163
+ help="Path to inference.py (default: alongside this runner)",
164
+ )
165
+ p.add_argument(
166
+ "--extra_inference",
167
+ default="",
168
+ help="Extra args appended verbatim to the inference command",
169
+ )
170
+ p.add_argument(
171
+ "--group_inference",
172
+ action="store_true",
173
+ help="Use inference_list.py to process meshes in groups of 16 (faster due to shared model loading)",
174
+ )
175
+ p.add_argument(
176
+ "--group_size",
177
+ type=int,
178
+ default=16,
179
+ help="Number of meshes to process per group when --group_inference is enabled (default: 16)",
180
+ )
181
+
182
+ # Rendering options (required)
183
+ p.add_argument("--hdr_list", required=True, help="TXT file with HDR/HDRI paths (one per line) for rendering")
184
+ p.add_argument("--salt", default="", help="Salt string for deterministic HDR mapping (default: empty)")
185
+ p.add_argument(
186
+ "--blender",
187
+ default="/home/dbsghd363/blender-4.5.3-linux-x64/blender",
188
+ help="Path to Blender executable (default: /home/dbsghd363/blender-4.5.3-linux-x64/blender)",
189
+ )
190
+ p.add_argument(
191
+ "--render_script",
192
+ default=default_render_script_path(),
193
+ help="Path to render_eval.py (default: tools/render_eval.py)",
194
+ )
195
+ p.add_argument(
196
+ "--extra_render",
197
+ default="",
198
+ help="Extra args appended verbatim to the render command",
199
+ )
200
+
201
+ # Evaluation options (required)
202
+ p.add_argument("--ref_root", required=True, help="Reference root directory for evaluation")
203
+ p.add_argument(
204
+ "--evaluate_script",
205
+ default=default_evaluate_script_path(),
206
+ help="Path to evaluate.py (default: tools/evaluate.py)",
207
+ )
208
+
209
+ # Optional processing control flags
210
+ p.add_argument(
211
+ "--skip-vis",
212
+ action="store_true",
213
+ help="Skip visualization rendering (only perform inference, evaluation rendering, and metrics computation)",
214
+ )
215
+
216
+ return p.parse_args()
217
+
218
+
219
+ async def main_async():
220
+ args = parse_args()
221
+
222
+ # Inputs
223
+ glb_paths = ensure_exists(read_list_file(args.glb_list), "GLB")
224
+ if not glb_paths:
225
+ print_error("No valid GLB paths found.")
226
+ sys.exit(1)
227
+
228
+ prompt_map = read_prompt_csv(args.prompt_csv)
229
+ if not prompt_map:
230
+ print_error("No valid (id -> prompt) entries found in CSV.")
231
+ sys.exit(1)
232
+
233
+ # Prepare output
234
+ out_dir = os.path.expanduser(args.output_dir)
235
+ os.makedirs(out_dir, exist_ok=True)
236
+
237
+ # Create subdirectories for organized structure
238
+ glbs_dir = os.path.join(out_dir, "glbs")
239
+ eval_dir = os.path.join(out_dir, "eval")
240
+ vis_dir = os.path.join(out_dir, "vis")
241
+ os.makedirs(glbs_dir, exist_ok=True)
242
+ os.makedirs(eval_dir, exist_ok=True)
243
+ os.makedirs(vis_dir, exist_ok=True)
244
+
245
+ # Load HDR list for rendering
246
+ hdrs = ensure_exists(read_list_file(args.hdr_list), "HDR")
247
+ if not hdrs:
248
+ print_error("No valid HDR paths found.")
249
+ sys.exit(1)
250
+
251
+ # Build commands
252
+ infer_commands: List[str] = []
253
+ render_commands: List[str] = []
254
+ vis_commands: List[str] = []
255
+ extra_infer_args = [x for x in args.extra_inference.split() if x] if args.extra_inference else []
256
+ extra_render_args = [x for x in args.extra_render.split() if x] if args.extra_render else []
257
+
258
+ missing_prompts = 0
259
+ skip_vis = args.skip_vis
260
+
261
+ # Prepare data structures for group inference
262
+ pending_infer_items: List[Tuple[str, str]] = [] # [(glb_path, prompt), ...]
263
+
264
+ for glb in glb_paths:
265
+ sid = stem_of(glb)
266
+ output_glb_path = os.path.join(glbs_dir, f"{sid}.glb")
267
+
268
+ prompt = prompt_map.get(sid)
269
+ if not prompt:
270
+ print_warning(f"No prompt found for GLB stem '{sid}' (path: {glb}); skipping")
271
+ missing_prompts += 1
272
+ continue
273
+
274
+ # Build render command (output to eval_dir)
275
+ hdr_idx = deterministic_hdr_index(output_glb_path, len(hdrs), args.salt)
276
+ hdr = hdrs[hdr_idx]
277
+ render_cmd = build_blender_command(
278
+ blender=args.blender,
279
+ script=args.render_script,
280
+ glb_path=output_glb_path,
281
+ hdr_path=hdr,
282
+ out_dir=eval_dir,
283
+ extra_args=extra_render_args,
284
+ )
285
+
286
+ # Build visualization command (output to vis_dir)
287
+ vis_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tools", "render_vis.py")
288
+ vis_cmd = build_blender_command(
289
+ blender=args.blender,
290
+ script=vis_script,
291
+ glb_path=output_glb_path,
292
+ hdr_path=hdr,
293
+ out_dir=vis_dir,
294
+ extra_args=[], # No extra args for vis rendering
295
+ )
296
+
297
+ # if the output for this GLB already exists, skip it
298
+ if not os.path.exists(output_glb_path):
299
+ pending_infer_items.append((glb, prompt))
300
+
301
+ # Check if rendering is already complete (20 PNG files in each subdir of eval_dir/gid/)
302
+ eval_gid_dir = os.path.join(eval_dir, sid)
303
+ render_complete = False
304
+ if os.path.exists(eval_gid_dir):
305
+ subdirs = [d for d in os.listdir(eval_gid_dir) if os.path.isdir(os.path.join(eval_gid_dir, d))]
306
+ if subdirs:
307
+ # Check if all subdirs have 20 PNG files
308
+ all_complete = True
309
+ for subdir in subdirs:
310
+ subdir_path = os.path.join(eval_gid_dir, subdir)
311
+ png_files = [f for f in os.listdir(subdir_path) if f.endswith('.png')]
312
+ if len(png_files) < 20:
313
+ all_complete = False
314
+ break
315
+ render_complete = all_complete
316
+
317
+ if not render_complete:
318
+ render_commands.append(render_cmd)
319
+
320
+ # Check if visualization is already complete (gid.mp4 exists in vis_dir)
321
+ # Only add vis commands if --skip-vis is not set
322
+ if not skip_vis:
323
+ vis_mp4_path = os.path.join(vis_dir, f"{sid}.mp4")
324
+ if not os.path.exists(vis_mp4_path):
325
+ vis_commands.append(vis_cmd)
326
+
327
+ # Informative mapping line (truncate very long prompts when printing)
328
+ p_preview = (prompt[:16] + "…") if len(prompt) > 16 else prompt
329
+ print_info(f"Mapped GLB -> Prompt: {glb} ==> \"{p_preview}\" -> Render with HDR[{hdr_idx}]")
330
+
331
+ # Build inference commands based on group_inference flag
332
+ if args.group_inference and pending_infer_items:
333
+ # Use inference_list.py with grouping
334
+ list_script = default_infer_list_script_path()
335
+ group_size = args.group_size
336
+
337
+ for i in range(0, len(pending_infer_items), group_size):
338
+ group = pending_infer_items[i:i + group_size]
339
+ group_glbs = [item[0] for item in group]
340
+ group_prompts = [item[1] for item in group]
341
+
342
+ infer_cmd = build_infer_list_command(
343
+ python_exec=args.python_exec,
344
+ script=list_script,
345
+ glb_paths=group_glbs,
346
+ prompts=group_prompts,
347
+ out_dir=glbs_dir,
348
+ extra_args=extra_infer_args,
349
+ )
350
+ infer_commands.append(infer_cmd)
351
+
352
+ print_info(f"Using grouped inference: {len(pending_infer_items)} meshes in {len(infer_commands)} group(s) of up to {group_size}")
353
+ else:
354
+ # Use individual inference.py calls
355
+ for glb, prompt in pending_infer_items:
356
+ infer_cmd = build_infer_command(
357
+ python_exec=args.python_exec,
358
+ script=args.script,
359
+ glb_path=glb,
360
+ prompt=prompt,
361
+ out_dir=glbs_dir,
362
+ extra_args=extra_infer_args,
363
+ )
364
+ infer_commands.append(infer_cmd)
365
+
366
+ if not args.group_inference:
367
+ print_info(f"Using individual inference: {len(infer_commands)} separate job(s)")
368
+
369
+ # Summary box
370
+ salt_info = f"Salt: '{args.salt}'\n" if args.salt else ""
371
+ vis_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tools", "render_vis.py")
372
+ vis_info = f"Total vis jobs: {len(vis_commands)}\n" if not skip_vis else "Visualization: SKIPPED (--skip-vis)\n"
373
+ vis_script_info = f"Visualization script: {vis_script}\n" if not skip_vis else ""
374
+
375
+ # Group inference info
376
+ if args.group_inference:
377
+ infer_mode = f"Grouped inference (inference_list.py, group size: {args.group_size})\n"
378
+ infer_script_info = f"Inference script: {default_infer_list_script_path()}\n"
379
+ else:
380
+ infer_mode = "Individual inference (inference.py)\n"
381
+ infer_script_info = f"Inference script: {args.script}\n"
382
+
383
+ summary_text = (
384
+ f"Total inference jobs: {len(infer_commands)}\n"
385
+ + infer_mode +
386
+ f"Total render jobs: {len(render_commands)}\n"
387
+ + vis_info +
388
+ f"Devices: {args.devices}\n"
389
+ f"Timeout per job: {args.timeout}s\n"
390
+ f"Output dir (--out): {out_dir}\n"
391
+ f"Python: {args.python_exec}\n"
392
+ + infer_script_info +
393
+ f"Rendering: {len(hdrs)} HDRs\n"
394
+ f"Render script: {args.render_script}\n"
395
+ + vis_script_info +
396
+ f"Blender: {args.blender}\n"
397
+ + salt_info +
398
+ f"Evaluation ref root: {args.ref_root}\n"
399
+ f"Evaluation script: {args.evaluate_script}\n"
400
+ + (f"GLBs missing prompts: {missing_prompts}\n" if missing_prompts else "")
401
+ )
402
+ print_box(summary_text, title="Batch Configuration")
403
+
404
+ # Execute inference commands with queued device allocation
405
+ await run_tests(infer_commands, args.devices, args.timeout)
406
+
407
+ # Retry inference for failed GLBs (second attempt)
408
+ print_header("Checking for missing GLBs and retrying if needed")
409
+ retry_infer_items: List[Tuple[str, str]] = []
410
+
411
+ for glb in glb_paths:
412
+ sid = stem_of(glb)
413
+ output_glb_path = os.path.join(glbs_dir, f"{sid}.glb")
414
+
415
+ # Check if GLB was not created
416
+ if not os.path.exists(output_glb_path):
417
+ prompt = prompt_map.get(sid)
418
+ if prompt:
419
+ retry_infer_items.append((glb, prompt))
420
+ print_warning(f"GLB not found, will retry: {output_glb_path}")
421
+
422
+ if retry_infer_items:
423
+ retry_infer_commands: List[str] = []
424
+
425
+ if args.group_inference:
426
+ # Use inference_list.py with grouping for retry
427
+ list_script = default_infer_list_script_path()
428
+ group_size = min(args.group_size, 2) # Use smaller groups for retry
429
+
430
+ for i in range(0, len(retry_infer_items), group_size):
431
+ group = retry_infer_items[i:i + group_size]
432
+ group_glbs = [item[0] for item in group]
433
+ group_prompts = [item[1] for item in group]
434
+
435
+ infer_cmd = build_infer_list_command(
436
+ python_exec=args.python_exec,
437
+ script=list_script,
438
+ glb_paths=group_glbs,
439
+ prompts=group_prompts,
440
+ out_dir=glbs_dir,
441
+ extra_args=extra_infer_args,
442
+ )
443
+ retry_infer_commands.append(infer_cmd)
444
+ else:
445
+ # Use individual inference.py calls for retry
446
+ for glb, prompt in retry_infer_items:
447
+ infer_cmd = build_infer_command(
448
+ python_exec=args.python_exec,
449
+ script=args.script,
450
+ glb_path=glb,
451
+ prompt=prompt,
452
+ out_dir=glbs_dir,
453
+ extra_args=extra_infer_args,
454
+ )
455
+ retry_infer_commands.append(infer_cmd)
456
+
457
+ print_info(f"Retrying {len(retry_infer_items)} failed inference item(s) in {len(retry_infer_commands)} job(s)")
458
+ await run_tests(retry_infer_commands, args.devices, args.timeout)
459
+ else:
460
+ print_info("All GLBs generated successfully on first attempt")
461
+ print_footer()
462
+
463
+ # Execute render commands
464
+ print_header("Starting rendering phase")
465
+ await run_tests(render_commands, args.devices, args.timeout)
466
+ print_footer()
467
+
468
+ # Execute visualization commands (only if not skipped)
469
+ if not skip_vis:
470
+ print_header("Starting visualization rendering phase")
471
+ await run_tests(vis_commands, args.devices, args.timeout)
472
+ print_footer()
473
+ else:
474
+ print_info("Skipping visualization rendering (--skip-vis enabled)")
475
+
476
+ # After inference and rendering, collect all GLB files from glbs_dir and write their absolute paths
477
+ print_header("Collecting output GLB files")
478
+ glb_files = []
479
+ for root, dirs, files in os.walk(glbs_dir):
480
+ for file in files:
481
+ if file.endswith('.glb'):
482
+ abs_path = os.path.abspath(os.path.join(root, file))
483
+ glb_files.append(abs_path)
484
+
485
+ glb_files.sort() # Sort for consistency
486
+ print_footer()
487
+
488
+ # Write to glbs.txt
489
+ glbs_txt_path = os.path.join(out_dir, "glbs.txt")
490
+ with open(glbs_txt_path, 'w', encoding='utf-8') as f:
491
+ for glb_path in glb_files:
492
+ f.write(f"{glb_path}\n")
493
+
494
+ print_info(f"Found {len(glb_files)} GLB file(s)")
495
+ print_info(f"GLB paths written to: {glbs_txt_path}")
496
+
497
+ # Run evaluation (using eval_dir as method_root)
498
+ print_header("Starting evaluation phase")
499
+ metrics_csv_path = os.path.join(out_dir, "metrics.csv")
500
+ eval_cmd = " ".join([
501
+ quote(args.python_exec),
502
+ quote(args.evaluate_script),
503
+ "--ref_root", quote(args.ref_root),
504
+ "--method_root", quote(eval_dir),
505
+ "--prompts_csv", quote(args.prompt_csv),
506
+ "--out_csv", quote(metrics_csv_path),
507
+ ])
508
+
509
+ # Set CUDA_VISIBLE_DEVICES to the user-specified GPU range
510
+ eval_env = os.environ.copy()
511
+ eval_env["CUDA_VISIBLE_DEVICES"] = ",".join(args.devices)
512
+
513
+ print_info(f"Running evaluation command with CUDA_VISIBLE_DEVICES={eval_env['CUDA_VISIBLE_DEVICES']}: {eval_cmd}")
514
+ eval_process = await asyncio.create_subprocess_shell(eval_cmd, env=eval_env)
515
+ await eval_process.wait()
516
+ print_footer()
517
+
518
+ if eval_process.returncode == 0:
519
+ print_info(f"Evaluation completed successfully")
520
+ print_info(f"Metrics saved to: {metrics_csv_path}")
521
+ else:
522
+ print_error(f"Evaluation failed with return code {eval_process.returncode}")
523
+
524
+ # Move cpsnr.json from glbs_dir to out_dir and process it
525
+ print_header("Processing CPSNR metrics")
526
+ cpsnr_source = os.path.join(glbs_dir, "cpsnr.json")
527
+ cpsnr_dest = os.path.join(out_dir, "cpsnr.json")
528
+
529
+ if os.path.exists(cpsnr_source):
530
+ import shutil
531
+ import json
532
+ import math
533
+
534
+ # Copy the cpsnr.json file
535
+ shutil.copyfile(cpsnr_source, cpsnr_dest)
536
+ print_info(f"Copied cpsnr.json from {cpsnr_source} to {cpsnr_dest}")
537
+
538
+ # Read and process cpsnr.json
539
+ with open(cpsnr_dest, 'r', encoding='utf-8') as f:
540
+ cpsnr_data = json.load(f)
541
+
542
+ # Calculate averages for each metric, excluding NaN values
543
+ # Also calculate weighted averages based on num_elements
544
+ metric_sums = {}
545
+ metric_counts = {}
546
+ metric_weighted_sums = {}
547
+ metric_total_weights = {}
548
+
549
+ for gid, metrics in cpsnr_data.items():
550
+ # Extract num_elements for this sample (used as weight)
551
+ num_elements = metrics.get("num_elements", None)
552
+
553
+ for metric_name, value in metrics.items():
554
+ # Skip num_elements itself in metric calculations
555
+ if metric_name == "num_elements":
556
+ continue
557
+
558
+ # Check if value is a valid number (not NaN, not None, not inf)
559
+ if value is not None and isinstance(value, (int, float)) and math.isfinite(value):
560
+ # Arithmetic mean (unweighted)
561
+ if metric_name not in metric_sums:
562
+ metric_sums[metric_name] = 0.0
563
+ metric_counts[metric_name] = 0
564
+ metric_sums[metric_name] += value
565
+ metric_counts[metric_name] += 1
566
+
567
+ # Weighted mean (weighted by num_elements)
568
+ if num_elements is not None and isinstance(num_elements, (int, float)) and num_elements > 0:
569
+ if metric_name not in metric_weighted_sums:
570
+ metric_weighted_sums[metric_name] = 0.0
571
+ metric_total_weights[metric_name] = 0.0
572
+ metric_weighted_sums[metric_name] += value * num_elements
573
+ metric_total_weights[metric_name] += num_elements
574
+
575
+ # Calculate averages
576
+ cpsnr_averages = {}
577
+
578
+ # Arithmetic mean
579
+ for metric_name in metric_sums:
580
+ if metric_counts[metric_name] > 0:
581
+ avg_value = metric_sums[metric_name] / metric_counts[metric_name]
582
+ cpsnr_averages[f"cpsnr_{metric_name}"] = avg_value
583
+ print_info(f"cpsnr_{metric_name}: {avg_value:.4f} (from {metric_counts[metric_name]} valid values)")
584
+
585
+ # Weighted mean
586
+ for metric_name in metric_weighted_sums:
587
+ if metric_total_weights[metric_name] > 0:
588
+ weighted_avg_value = metric_weighted_sums[metric_name] / metric_total_weights[metric_name]
589
+ cpsnr_averages[f"cpsnr_weighted_{metric_name}"] = weighted_avg_value
590
+ print_info(f"cpsnr_weighted_{metric_name}: {weighted_avg_value:.4f} (weighted by num_elements)")
591
+
592
+ # Append cpsnr averages to metrics.csv in (metric, value) format
593
+ if os.path.exists(metrics_csv_path) and cpsnr_averages:
594
+ # Append new cpsnr metrics as new rows
595
+ with open(metrics_csv_path, 'a', encoding='utf-8', newline='') as f:
596
+ writer = csv.writer(f)
597
+ for cpsnr_key, cpsnr_value in sorted(cpsnr_averages.items()):
598
+ writer.writerow([cpsnr_key, cpsnr_value])
599
+
600
+ print_info(f"Added {len(cpsnr_averages)} CPSNR metric(s) to {metrics_csv_path}")
601
+ else:
602
+ print_warning(f"Could not update metrics.csv with CPSNR data (file exists: {os.path.exists(metrics_csv_path)}, has data: {bool(cpsnr_averages)})")
603
+ else:
604
+ print_warning(f"cpsnr.json not found at {cpsnr_source}, skipping CPSNR processing")
605
+
606
+ print_footer()
607
+
608
+ def main():
609
+ try:
610
+ asyncio.run(main_async())
611
+ except KeyboardInterrupt:
612
+ print_warning("Interrupted by user (Ctrl+C). Exiting.")
613
+
614
+
615
+ if __name__ == "__main__":
616
+ main()
617
+
618
+ """
619
+ EXAMPLE USAGE:
620
+
621
+ # Basic usage (individual inference.py calls):
622
+ METHOD=single_caa; ITER=40000; \
623
+ python inference_batch.py \
624
+ --glb_list /home/aaaaa/projects/cvpr2026/glbs_eval_ref.txt \
625
+ --prompt_csv /home/aaaaa/projects/cvpr2026/prompts_bs.csv \
626
+ --hdr_list /home/aaaaa/data/envmaps/hdr_list.txt \
627
+ --ref_root outputs_eval_omega/ref/ \
628
+ --output_dir ./outputs_eval_omega/glbs_${METHOD}_${ITER}/ \
629
+ --extra_inference "--guidance 4.0 --steps 30 --weights outputs/mcdiff_v.${METHOD}/checkpoint-${ITER}/transformer/diffusion_pytorch_model.safetensors --config configs/mcdiff/${METHOD}.yaml" \
630
+ --devices 0 --skip-vis
631
+
632
+ # Grouped inference (faster - uses inference_list.py with groups of 16):
633
+ METHOD=single_caa; ITER=40000; \
634
+ python inference_batch.py \
635
+ --glb_list /home/aaaaa/projects/cvpr2026/glbs_eval_ref.txt \
636
+ --prompt_csv /home/aaaaa/projects/cvpr2026/prompts_bs.csv \
637
+ --hdr_list /home/aaaaa/data/envmaps/hdr_list.txt \
638
+ --ref_root outputs_eval_omega/ref/ \
639
+ --output_dir ./outputs_eval_omega/glbs_${METHOD}_${ITER}/ \
640
+ --extra_inference "--guidance 4.0 --steps 30 --weights outputs/mcdiff_v.${METHOD}/checkpoint-${ITER}/transformer/diffusion_pytorch_model.safetensors --config configs/mcdiff/${METHOD}.yaml" \
641
+ --devices 0 --skip-vis \
642
+ --group_inference
643
+
644
+ # Grouped inference with custom group size:
645
+ METHOD=dual_full; ITER=40000; \
646
+ python inference_batch.py \
647
+ --glb_list /home/aaaaa/projects/cvpr2026/glbs_eval_ref.txt \
648
+ --prompt_csv /home/aaaaa/projects/cvpr2026/prompts_bs.csv \
649
+ --hdr_list /home/aaaaa/data/envmaps/hdr_list.txt \
650
+ --ref_root outputs_eval_omega/ref/ \
651
+ --output_dir ./outputs_eval_zenith/glbs_${METHOD}_${ITER}/ \
652
+ --extra_inference "--guidance 4.0 --steps 30 --weights outputs/mcdiff_v.${METHOD}/checkpoint-${ITER}/transformer/diffusion_pytorch_model.safetensors --config configs/mcdiff/${METHOD}.yaml" \
653
+ --devices 0 --skip-vis \
654
+ --group_inference
655
+ """
656
+
657
+ # 40000 30000 20000 15000 10000 5000
658
+ # ours baseline1 baseline2
home/ubuntu/aaaaa/data/rgbmr/inference_list.py ADDED
@@ -0,0 +1,1270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mesh-to-Textured-Mesh Batch Inference Pipeline
3
+ Multiple (mesh + prompt) pairs -> multiple textured .glb files
4
+
5
+ This script efficiently processes N mesh-prompt pairs by loading models once
6
+ and reusing them for all generations, avoiding redundant model loading overhead.
7
+ """
8
+
9
+ import os
10
+ import tempfile
11
+ from pathlib import Path
12
+ from typing import Tuple, Dict, Any, Optional, List
13
+ import math
14
+ import json
15
+ import filelock
16
+ import hashlib
17
+
18
+ import numpy as np
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+ from diffusers import FlowMatchEulerDiscreteScheduler, SD3Transformer2DModel, AutoencoderKL
23
+ from torchvision.transforms import ToPILImage
24
+ from PIL import Image
25
+ from omegaconf import OmegaConf
26
+ from peft import set_peft_model_state_dict
27
+ from safetensors.torch import load_file
28
+
29
+ from mcgen.utils.model_utils import load_offset_autoencoder, initialize_transformer_weights
30
+ from mcgen.mcdiff.attention_processor_lora import LoRAMessagePassingAttnProcessor
31
+ from mcgen.mcdiff.attention_utils import apply_custom_processors
32
+ from mcgen.utils.text_encoder_utils import load_text_ctx, encode_prompt
33
+ from mcgen.mcdiff.latents import generate_latents
34
+ from mcgen.utils.pipeline_texture import TexturePipeline, ModProcessConfig
35
+ from mcgen.utils.correspondence import build_corr_pairs, downscale_pairs_to_f2l, dilate_f2l, correspondence_psnr
36
+ from mcgen.utils.config import load_config
37
+ from tools.utils.mesh_utils import load_mesh, get_orthogonal_camera, NVDiffRastContextWrapper, render
38
+ from mcgen.utils.image_super_utils import imageSuperNet
39
+ from mcgen.utils.pipeline_utils import ViewProcessor
40
+ from mcgen.utils.uvwrap_utils import mesh_uv_wrap
41
+ from mcgen.utils.geometry_inpaint_utils import texture_inpaint
42
+ from DifferentiableRenderer.MeshRender import MeshRender
43
+ import trimesh
44
+ import copy
45
+
46
+ # =============================================================================
47
+ # Configuration
48
+ # =============================================================================
49
+
50
+ # Model paths
51
+ SD35_REPO = "stabilityai/stable-diffusion-3.5-medium"
52
+ TRANSFORMER_PARTIAL_WEIGHTS = "outputs/mcdiff_v1.9.5/checkpoint-40000/transformer/diffusion_pytorch_model.safetensors"
53
+ SD_VAE_PATH = "./vae_sd35"
54
+ MCVAE_CONFIG_PATH = "./configs/mcvae/config.json"
55
+ MCVAE_CKPT_PATH = "./outputs/mcvae_v1.8.1.pt"
56
+
57
+ # Device & precision
58
+ DEVICE = "cuda"
59
+ DTYPE = torch.float16
60
+
61
+ # Image & view settings
62
+ HEIGHT = WIDTH = 512
63
+ TOKEN_HW = HEIGHT // 16
64
+ NUM_VIEWS = 6
65
+ ELEV_DEG = [0.0, 0.0, 0.0, 0.0, 89.99, -89.99]
66
+ AZIM_DEG = [0.0, 90.0, 180.0, 270.0, 0.0, 0.0]
67
+
68
+ # MaterialMVP azimuth convention (90 degree offset from mvadapter)
69
+ AZIM_DEG_MATERIALMVP = [angle + 90 for angle in AZIM_DEG]
70
+
71
+ # Inference defaults
72
+ GUIDANCE = 4.0
73
+ STEPS = 30
74
+ NEGATIVE_PROMPT = ""
75
+
76
+
77
+ # =============================================================================
78
+ # Utility Functions
79
+ # =============================================================================
80
+
81
+ def seed_everything(seed: int = 42):
82
+ """Seed all random number generators for reproducibility."""
83
+ os.environ["PYTHONHASHSEED"] = str(seed)
84
+ np.random.seed(seed)
85
+ torch.manual_seed(seed)
86
+ torch.cuda.manual_seed_all(seed)
87
+ torch.backends.cudnn.deterministic = True
88
+ torch.backends.cudnn.benchmark = False
89
+
90
+
91
+ def _resize_to_pm1(x: torch.Tensor, *, height: int, width: int) -> torch.Tensor:
92
+ """Resize to target resolution and normalize to [-1, 1]."""
93
+ x = F.interpolate(x, size=(height, width), mode="bilinear", align_corners=False)
94
+ return x * 2.0 - 1.0
95
+
96
+
97
+ def _expand_embeddings_for_views(pe: torch.Tensor, pooled: torch.Tensor, *, num_views: int) -> Tuple[torch.Tensor, torch.Tensor]:
98
+ """Expand text embeddings from [1, seq, dim] to [num_views, seq, dim]."""
99
+ pe = pe.expand(1, -1, -1).unsqueeze(1).expand(-1, num_views, -1, -1).reshape(-1, pe.shape[1], pe.shape[2])
100
+ pooled = pooled.expand(1, -1).unsqueeze(1).expand(-1, num_views, -1).reshape(-1, pooled.shape[-1])
101
+ return pe, pooled
102
+
103
+
104
+ @torch.no_grad()
105
+ def _save_strip(tensor_bchw: torch.Tensor, path: str, nrow: int = 1) -> str:
106
+ """Save grid of images from batched tensors in [-1, 1] range.
107
+
108
+ Args:
109
+ tensor_bchw: Batch of images in BCHW format, range [-1, 1]
110
+ path: Output path for saved image
111
+ nrow: Number of rows in grid. If 1, creates horizontal strip.
112
+ """
113
+
114
+ to_pil = ToPILImage()
115
+ b, c, h, w = tensor_bchw.shape
116
+
117
+ nrow = max(1, nrow)
118
+ ncol = math.ceil(b / nrow)
119
+
120
+ canvas = Image.new('RGB', (ncol * w, nrow * h))
121
+ for i in range(b):
122
+ row = i // ncol
123
+ col = i % ncol
124
+ img = to_pil((tensor_bchw[i].clamp(-1, 1) * 0.5 + 0.5))
125
+ canvas.paste(img, (col * w, row * h))
126
+
127
+ os.makedirs(os.path.dirname(path), exist_ok=True)
128
+ canvas.save(path)
129
+ return path
130
+
131
+
132
+ def _update_cpsnr_json(json_path: str, gid: str, metrics: Dict[str, float]) -> None:
133
+ """Thread-safe update of cpsnr.json file using file locking.
134
+
135
+ Args:
136
+ json_path: Path to cpsnr.json file
137
+ gid: Geometry ID (mesh filename stem)
138
+ metrics: Dictionary containing 'albedo', 'roughness', 'metallic', 'total' keys
139
+ """
140
+ lock_path = json_path + ".lock"
141
+ lock = filelock.FileLock(lock_path, timeout=60)
142
+
143
+ try:
144
+ with lock:
145
+ # Read existing data
146
+ if os.path.exists(json_path):
147
+ with open(json_path, "r") as f:
148
+ data = json.load(f)
149
+ else:
150
+ data = {}
151
+
152
+ # Update with new metrics
153
+ data[gid] = metrics
154
+
155
+ # Write back atomically using temp file + rename
156
+ temp_path = json_path + ".tmp"
157
+ with open(temp_path, "w") as f:
158
+ json.dump(data, f, indent=2)
159
+ os.replace(temp_path, json_path)
160
+ finally:
161
+ # Clean up lock file if it exists and we can remove it
162
+ try:
163
+ if os.path.exists(lock_path):
164
+ os.remove(lock_path)
165
+ except OSError:
166
+ pass
167
+
168
+
169
+ # =============================================================================
170
+ # Reusable Model Context (loaded once, used for all meshes)
171
+ # =============================================================================
172
+
173
+ class UnprojectionContext:
174
+ """Manages reusable models for unprojection to avoid repeated loading."""
175
+
176
+ def __init__(
177
+ self,
178
+ *,
179
+ render_size: int = 512,
180
+ texture_size: int = 4096,
181
+ realesrgan_ckpt: str,
182
+ device: str = "cuda",
183
+ ):
184
+ self.render_size = render_size
185
+ self.texture_size = texture_size
186
+ self.device = device
187
+
188
+ # Load super-resolution model once
189
+ self.super_model = imageSuperNet(realesrgan_ckpt)
190
+
191
+ def unproject_with_materialmvp(
192
+ self,
193
+ mesh_path: str,
194
+ output_dir: str,
195
+ gid: str,
196
+ albedo_images: List[Image.Image],
197
+ mr_images: List[Image.Image],
198
+ *,
199
+ bake_exp: int = 2,
200
+ selected_view_weights: Optional[List[float]] = None,
201
+ ) -> str:
202
+ """Unproject multi-view images to textured mesh using MaterialMVP approach.
203
+
204
+ Uses pre-loaded super-resolution model from context.
205
+
206
+ Args:
207
+ mesh_path: Path to input mesh
208
+ output_dir: Output directory
209
+ gid: Geometry ID (mesh filename stem)
210
+ albedo_images: List of albedo views (6 images)
211
+ mr_images: List of metallic-roughness views (6 images, ORM format)
212
+ bake_exp: Baking exponent for view weighting
213
+ selected_view_weights: Optional list of per-view weights
214
+
215
+ Returns:
216
+ Path to saved GLB file
217
+ """
218
+ # Initialize renderer (lightweight, can be created per mesh)
219
+ render_obj = MeshRender(
220
+ default_resolution=self.render_size,
221
+ texture_size=self.texture_size,
222
+ bake_mode="back_sample",
223
+ raster_mode="cr",
224
+ )
225
+
226
+ # Load and prepare mesh
227
+ mesh = trimesh.load(mesh_path)
228
+ mesh = mesh_uv_wrap(mesh)
229
+ render_obj.load_mesh(mesh=mesh)
230
+
231
+ # Initialize view processor
232
+ view_processor = ViewProcessor(bake_exp, render_obj)
233
+
234
+ # Use MaterialMVP azimuth convention (90-degree offset)
235
+ selected_camera_azims = AZIM_DEG_MATERIALMVP
236
+ selected_camera_elevs = ELEV_DEG
237
+
238
+ if selected_view_weights is None:
239
+ selected_view_weights = [1, 1, 1, 1, 0.5, 0.5]
240
+
241
+ # Prepare images dictionary
242
+ multiviews_pbr = {
243
+ "albedo": albedo_images,
244
+ "mr": mr_images,
245
+ }
246
+
247
+ # Enhance images using pre-loaded super model
248
+ enhance_images = {
249
+ "albedo": copy.deepcopy(multiviews_pbr["albedo"]),
250
+ "mr": copy.deepcopy(multiviews_pbr["mr"]),
251
+ }
252
+
253
+ for i in range(len(enhance_images["albedo"])):
254
+ enhance_images["albedo"][i] = self.super_model(enhance_images["albedo"][i])
255
+ enhance_images["mr"][i] = self.super_model(enhance_images["mr"][i])
256
+
257
+ # Resize to double render size for better quality
258
+ for i in range(len(enhance_images["albedo"])):
259
+ enhance_images["albedo"][i] = enhance_images["albedo"][i].resize(
260
+ (self.render_size * 2, self.render_size * 2), Image.LANCZOS
261
+ )
262
+ enhance_images["mr"][i] = enhance_images["mr"][i].resize(
263
+ (self.render_size * 2, self.render_size * 2), Image.LANCZOS
264
+ )
265
+
266
+ # Bake albedo texture
267
+ texture, mask = view_processor.bake_from_multiview(
268
+ enhance_images["albedo"],
269
+ selected_camera_elevs,
270
+ selected_camera_azims,
271
+ selected_view_weights
272
+ )
273
+
274
+ # Bake metallic-roughness texture
275
+ texture_mr, mask_mr = view_processor.bake_from_multiview(
276
+ enhance_images["mr"],
277
+ selected_camera_elevs,
278
+ selected_camera_azims,
279
+ selected_view_weights
280
+ )
281
+
282
+ # Convert baked masks to boolean tensors
283
+ mask_bool = (mask.squeeze(-1) > 0.5).to(torch.bool)
284
+ mask_mr_bool = (mask_mr.squeeze(-1) > 0.5).to(torch.bool)
285
+
286
+ # Apply geometry-aware inpainting for albedo texture
287
+ texture_inpainted = texture_inpaint(
288
+ texture,
289
+ mask_bool,
290
+ render_obj,
291
+ uv_mask_erode_iters=10,
292
+ baked_mask_erode_iters=2,
293
+ vertex_merge_tolerance=1e-5,
294
+ vertex_color_K=11,
295
+ )
296
+ render_obj.set_texture(texture_inpainted, force_set=True)
297
+
298
+ # Apply geometry-aware inpainting for metallic-roughness texture
299
+ texture_mr_inpainted = texture_inpaint(
300
+ texture_mr,
301
+ mask_mr_bool,
302
+ render_obj,
303
+ uv_mask_erode_iters=10,
304
+ baked_mask_erode_iters=2,
305
+ vertex_merge_tolerance=1e-5,
306
+ vertex_color_K=11,
307
+ )
308
+ render_obj.set_texture_mr(texture_mr_inpainted)
309
+
310
+ # Save mesh
311
+ os.makedirs(output_dir, exist_ok=True)
312
+ output_path = os.path.join(output_dir, f"{gid}.glb")
313
+ render_obj.save_mesh(output_path, downsample=False)
314
+
315
+ return output_path
316
+
317
+
318
+ class InferenceContext:
319
+ """Manages all pre-loaded models for efficient batch inference."""
320
+
321
+ def __init__(self, settings: Dict[str, Any]):
322
+ self.settings = settings
323
+ self.device = settings["device"]
324
+ self.dtype = settings["dtype"]
325
+
326
+ # Load VAE models once
327
+ self.vae, self.mcvae = self._load_vae_models()
328
+ self.vae_shift = self.vae.config.shift_factor
329
+ self.vae_scale = self.vae.config.scaling_factor
330
+
331
+ # Load transformer and scheduler once
332
+ self.transformer, self.scheduler = self._setup_transformer()
333
+
334
+ # Load text encoders once
335
+ self.text_ctx = self._load_text_ctx()
336
+
337
+ # Initialize unprojection context if using materialmvp backend
338
+ self.unproj_ctx = None
339
+ if settings.get("unproj_backend") == "materialmvp":
340
+ self.unproj_ctx = UnprojectionContext(
341
+ render_size=settings["resolution"],
342
+ texture_size=settings["uv_size"],
343
+ realesrgan_ckpt=settings["realesrgan_ckpt"],
344
+ device=self.device,
345
+ )
346
+
347
+ # Initialize texture pipeline if using mvadapter backend
348
+ self.tex_pipeline = None
349
+ if settings.get("unproj_backend") == "mvadapter":
350
+ self.tex_pipeline = TexturePipeline(
351
+ upscaler_ckpt_path=settings["realesrgan_ckpt"],
352
+ inpaint_ckpt_path=settings["lama_ckpt"],
353
+ device=self.device,
354
+ )
355
+
356
+ def _load_vae_models(self):
357
+ """Load VAE and optionally mcVAE."""
358
+ use_dual_branch = self.settings["use_dual_branch"]
359
+
360
+ if use_dual_branch:
361
+ vae = AutoencoderKL.from_pretrained(self.settings["sd_vae_path"])
362
+ vae = vae.to(device=self.device, dtype=self.dtype).eval().requires_grad_(False)
363
+ return vae, None
364
+
365
+ vae, mcvae = load_offset_autoencoder(
366
+ sd_vae_path=self.settings["sd_vae_path"],
367
+ mcvae_config_path=self.settings["mcvae_config_path"],
368
+ mcvae_ckpt_path=self.settings["mcvae_ckpt_path"],
369
+ offset_mode=self.settings.get("mcvae_offset_mode", True),
370
+ )
371
+ vae = vae.to(device=self.device, dtype=self.dtype).eval().requires_grad_(False)
372
+ mcvae = mcvae.to(device=self.device, dtype=self.dtype).eval().requires_grad_(False)
373
+ return vae, mcvae
374
+
375
+ def _setup_transformer(self):
376
+ """Setup transformer with structural modifications and load weights."""
377
+ transformer = SD3Transformer2DModel.from_pretrained(
378
+ self.settings["base_model"],
379
+ subfolder="transformer"
380
+ )
381
+
382
+ model_cfg = self.settings["model_cfg"]
383
+ num_views = self.settings["num_views"]
384
+ use_caa = self.settings["use_caa"]
385
+ use_rope = self.settings["use_rope"]
386
+ use_global_token = self.settings["use_global_token"]
387
+ use_dual_branch = self.settings["use_dual_branch"]
388
+ condition_channels = self.settings["condition_channels_cfg"]
389
+
390
+ num_domains = 2 if use_dual_branch else 1
391
+ print(f'use_dual_branch: {use_dual_branch}, use_caa: {use_caa}, use_rope: {use_rope}, use_global_token: {use_global_token}, num_views: {num_views}, num_domains: {num_domains}')
392
+
393
+ # Apply LoRA processors
394
+ self._apply_lora_processors(
395
+ transformer,
396
+ model_cfg,
397
+ use_caa=use_caa,
398
+ use_rope=use_rope,
399
+ use_global_token=use_global_token,
400
+ num_views=num_views,
401
+ num_domains=num_domains,
402
+ )
403
+
404
+ # Extend position embedding input channels if needed
405
+ if condition_channels > 0:
406
+ self._extend_pos_embed_in(transformer, condition_channels)
407
+
408
+ # Load weights
409
+ self._load_transformer_weights(
410
+ transformer,
411
+ self.settings["weights_path"],
412
+ condition_channels=condition_channels,
413
+ )
414
+
415
+ transformer = transformer.to(device=self.device, dtype=self.dtype).eval().requires_grad_(False)
416
+
417
+ scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
418
+ self.settings["base_model"],
419
+ subfolder="scheduler"
420
+ )
421
+
422
+ return transformer, scheduler
423
+
424
+ def _apply_lora_processors(
425
+ self,
426
+ transformer: SD3Transformer2DModel,
427
+ model_cfg,
428
+ *,
429
+ use_caa: bool,
430
+ use_rope: bool,
431
+ use_global_token: bool,
432
+ num_views: int,
433
+ num_domains: int,
434
+ ) -> None:
435
+ """Attach LoRA message-passing processors."""
436
+ lcfg = getattr(model_cfg, "attn_lora", None)
437
+ if not lcfg or not getattr(lcfg, "enabled", False):
438
+ return
439
+
440
+ select = {"joint": True, "self": bool(getattr(lcfg, "apply_to_self", False))}
441
+ limits = {}
442
+ if getattr(lcfg, "limit_joint", None) is not None:
443
+ limits["joint"] = lcfg.limit_joint
444
+ if getattr(lcfg, "limit_self", None) is not None:
445
+ limits["self"] = lcfg.limit_self
446
+
447
+ def processor_factory(_name, _mod, query_dim, inner_dim, out_dim, num_heads, kind="joint"):
448
+ return LoRAMessagePassingAttnProcessor(
449
+ r_q=int(getattr(lcfg, "r_q", 8)),
450
+ r_k=int(getattr(lcfg, "r_k", 8)),
451
+ r_v=int(getattr(lcfg, "r_v", 8)),
452
+ alpha_q=float(getattr(lcfg, "alpha_q", getattr(lcfg, "r_q", 8))),
453
+ alpha_k=float(getattr(lcfg, "alpha_k", getattr(lcfg, "r_k", 8))),
454
+ alpha_v=float(getattr(lcfg, "alpha_v", getattr(lcfg, "r_v", 8))),
455
+ query_dim=query_dim,
456
+ inner_dim=inner_dim,
457
+ out_dim=out_dim,
458
+ num_heads=num_heads,
459
+ num_views=num_views,
460
+ num_domains=num_domains,
461
+ use_caa=use_caa,
462
+ use_rope=use_rope,
463
+ use_global_token=use_global_token,
464
+ kind=kind,
465
+ )
466
+
467
+ apply_custom_processors(
468
+ transformer,
469
+ model_cfg,
470
+ processor_factory,
471
+ select=select,
472
+ limits=limits if limits else None,
473
+ as_factory=True,
474
+ )
475
+
476
+ def _extend_pos_embed_in(self, transformer: SD3Transformer2DModel, extra_channels: int) -> None:
477
+ """Extend transformer's pos_embed input channels."""
478
+ conv = transformer.pos_embed.proj
479
+ new_conv = nn.Conv2d(
480
+ in_channels=conv.in_channels + extra_channels,
481
+ out_channels=conv.out_channels,
482
+ kernel_size=conv.kernel_size, stride=conv.stride, padding=conv.padding,
483
+ dilation=conv.dilation, groups=conv.groups, padding_mode=conv.padding_mode,
484
+ device=conv.weight.device, dtype=conv.weight.dtype,
485
+ )
486
+ with torch.no_grad():
487
+ new_conv.weight.zero_()
488
+ new_conv.weight[:, :conv.in_channels].copy_(conv.weight)
489
+ new_conv.bias.copy_(conv.bias)
490
+ transformer.pos_embed.proj = new_conv
491
+
492
+ def _load_conditioning_conv(self, transformer: SD3Transformer2DModel, cond_path: Path) -> None:
493
+ """Load conditioning convolution weights if available."""
494
+ if not cond_path.exists():
495
+ return
496
+ state = load_file(str(cond_path))
497
+ weight_key = "pos_embed.proj.weight"
498
+ bias_key = "pos_embed.proj.bias"
499
+ pe_proj = transformer.pos_embed.proj
500
+ if weight_key in state:
501
+ pe_proj.weight.data.copy_(state[weight_key].to(pe_proj.weight.device, dtype=pe_proj.weight.dtype))
502
+ if bias_key in state:
503
+ pe_proj.bias.data.copy_(state[bias_key].to(pe_proj.bias.device, dtype=pe_proj.bias.dtype))
504
+
505
+ def _load_transformer_weights(
506
+ self,
507
+ transformer: SD3Transformer2DModel,
508
+ weights_path: Optional[str],
509
+ *,
510
+ condition_channels: int,
511
+ ) -> None:
512
+ """Load transformer weights from checkpoint."""
513
+ if not weights_path:
514
+ return
515
+ path = Path(weights_path)
516
+
517
+ if path.is_dir():
518
+ full_dir = path / "transformer"
519
+ lora_path = path / "pytorch_lora_weights.safetensors"
520
+ cond_path = path / "pytorch_cond_conv_weights.safetensors"
521
+
522
+ if full_dir.is_dir():
523
+ initialize_transformer_weights(transformer, str(full_dir))
524
+ elif lora_path.exists():
525
+ lora_state = load_file(str(lora_path))
526
+ set_peft_model_state_dict(transformer, lora_state, adapter_name="default")
527
+ else:
528
+ initialize_transformer_weights(transformer, str(path))
529
+
530
+ if condition_channels > 0:
531
+ self._load_conditioning_conv(transformer, cond_path)
532
+ return
533
+
534
+ initialize_transformer_weights(transformer, str(path))
535
+
536
+ def _load_text_ctx(self):
537
+ """Load text encoding context (tokenizers and encoders)."""
538
+ return load_text_ctx(
539
+ device=self.device,
540
+ dtype=self.dtype,
541
+ sd_model_name=self.settings["base_model"]
542
+ )
543
+
544
+ def encode_text_prompts(self, prompt: str, negative_prompt: str) -> Dict[str, torch.Tensor]:
545
+ """Encode text prompts using pre-loaded text context."""
546
+ num_views = self.settings["num_views"]
547
+
548
+ pe, pooled = encode_prompt(
549
+ self.text_ctx["encoders"],
550
+ self.text_ctx["tokenizers"],
551
+ prompt,
552
+ max_sequence_length=77
553
+ )
554
+ npe, npooled = encode_prompt(
555
+ self.text_ctx["encoders"],
556
+ self.text_ctx["tokenizers"],
557
+ negative_prompt,
558
+ max_sequence_length=77
559
+ )
560
+
561
+ # Move to device and expand for all views
562
+ pe, pooled = pe.to(device=self.device, dtype=self.dtype), pooled.to(device=self.device, dtype=self.dtype)
563
+ npe, npooled = npe.to(device=self.device, dtype=self.dtype), npooled.to(device=self.device, dtype=self.dtype)
564
+
565
+ pe, pooled = _expand_embeddings_for_views(pe, pooled, num_views=num_views)
566
+ npe, npooled = _expand_embeddings_for_views(npe, npooled, num_views=num_views)
567
+
568
+ return {
569
+ "prompt_embeds": pe,
570
+ "pooled_prompt_embeds": pooled,
571
+ "negative_prompt_embeds": npe,
572
+ "negative_pooled_prompt_embeds": npooled,
573
+ }
574
+
575
+ def encode_conditions(self, cond_values: torch.Tensor) -> torch.Tensor:
576
+ """Encode 6-channel condition values into latent space."""
577
+ height = self.settings["resolution"]
578
+ width = self.settings["resolution"]
579
+
580
+ cond_values = cond_values.to(device=self.vae.device, dtype=self.vae.dtype)
581
+ lat_chunks = []
582
+ for c in range(0, cond_values.shape[1], 3):
583
+ posterior = self.vae.encode(cond_values[:, c:c+3]).latent_dist
584
+ lat_chunks.append(posterior.mean)
585
+
586
+ cond_latents = torch.cat(lat_chunks, dim=1)
587
+ cond_latents = (cond_latents - self.vae_shift) * self.vae_scale
588
+ Cc = cond_latents.shape[1]
589
+ cond_latents = cond_latents.reshape(-1, Cc, height // 8, width // 8)
590
+ return cond_latents.to(dtype=self.dtype)
591
+
592
+
593
+ # =============================================================================
594
+ # Rendering & Preprocessing
595
+ # =============================================================================
596
+
597
+ @torch.no_grad()
598
+ def render_views(
599
+ mesh_path: str,
600
+ *,
601
+ num_views: int,
602
+ height: int,
603
+ width: int,
604
+ device: str = DEVICE,
605
+ ) -> Dict[str, Any]:
606
+ """Render multi-view geometry attributes from mesh."""
607
+ ctx = NVDiffRastContextWrapper(device=device, context_type="cuda")
608
+ mesh = load_mesh(str(mesh_path), rescale=True, move_to_center=True, flip_uv=True, device=device)
609
+ if len(ELEV_DEG) != num_views or len(AZIM_DEG) != num_views:
610
+ raise ValueError("ELEV_DEG and AZIM_DEG presets must match num_views.")
611
+
612
+ cams = get_orthogonal_camera(
613
+ elevation_deg=ELEV_DEG,
614
+ azimuth_deg=AZIM_DEG,
615
+ distance=[1.0] * num_views,
616
+ left=-0.55,
617
+ right=0.55,
618
+ bottom=-0.55,
619
+ top=0.55,
620
+ device=device, dtype=torch.float32,
621
+ )
622
+
623
+ # Build 5-channel texture override: [rgb, roughness(0), roughness, metallic]
624
+ tex_ovr = torch.cat([mesh.texture, torch.zeros_like(mesh.roughness), mesh.roughness, mesh.metallic], dim=-1)
625
+
626
+ out = render(ctx, mesh, cams, height=height, width=width, render_attr=True, texture_override=tex_ovr)
627
+
628
+ # Extract and normalize geometry attributes
629
+ attr = out.attr
630
+ rgb = attr[..., :3].contiguous()
631
+ orm = attr[..., 3:6].contiguous()
632
+ orm[..., 0] = (orm[..., 0] < 0.25).float() # Binary occupancy mask
633
+
634
+ return {
635
+ "albedo": rgb,
636
+ "orm": orm,
637
+ "pos": out.pos + 0.5, # [-0.5, 0.5] -> [0, 1]
638
+ "normal": (out.normal + 1.0) * 0.5, # [-1, 1] -> [0, 1]
639
+ "depth": out.raw_depth,
640
+ "c2w": cams.c2w,
641
+ "scale": torch.tensor(1.1, device=device),
642
+ }
643
+
644
+
645
+ def preprocess_geometry(
646
+ rend: Dict[str, Any],
647
+ *,
648
+ device: str,
649
+ dtype: torch.dtype,
650
+ height: int,
651
+ width: int,
652
+ token_hw: int,
653
+ num_views: int,
654
+ use_caa: bool,
655
+ corr_dilate_iterations: int = 1,
656
+ use_global_pos: bool = False,
657
+ ) -> Dict[str, torch.Tensor]:
658
+ """Preprocess rendered geometry into model-ready format."""
659
+ # Convert to channel-first
660
+ albedo = rend["albedo"].permute(0, 3, 1, 2).contiguous()
661
+ orm = rend["orm"].permute(0, 3, 1, 2).contiguous()
662
+ pos = rend["pos"].permute(0, 3, 1, 2).contiguous()
663
+ normal = rend["normal"].permute(0, 3, 1, 2).contiguous()
664
+ depth = rend["depth"]
665
+ c2w = rend["c2w"]
666
+ scale = rend["scale"]
667
+
668
+ V, _, H_raw, W_raw = albedo.shape
669
+ assert V == num_views, f"Expected {num_views} views, got {V}"
670
+
671
+ # Normal in [-1, 1] for correspondence computation
672
+ normal_fullres = normal * 2.0 - 1.0
673
+
674
+ # Mask from occupancy channel
675
+ mask = orm[:, 0, :, :] > 0.5
676
+ depth = depth.clone()
677
+ depth[~mask] = float("nan")
678
+
679
+ # Position tokens via weighted averaging at token grid
680
+ pos_raw = pos - 0.5 # Convert to [-0.5, 0.5]
681
+ df = H_raw // token_hw
682
+ mask_f = mask.float().unsqueeze(1)
683
+ pos_sum = F.avg_pool2d(pos_raw * mask_f, kernel_size=df, stride=df, divisor_override=1)
684
+ cnt = F.avg_pool2d(mask_f, kernel_size=df, stride=df, divisor_override=1)
685
+ pos_token = (pos_sum / cnt.clamp_min(1.0)).to(device=device, dtype=dtype)
686
+
687
+ # Resize and normalize to [-1, 1]
688
+ albedo_r = _resize_to_pm1(albedo, height=height, width=width)
689
+ orm_r = _resize_to_pm1(orm, height=height, width=width)
690
+ pos_r = _resize_to_pm1(pos, height=height, width=width)
691
+ normal_r = _resize_to_pm1(normal, height=height, width=width)
692
+
693
+ # Condition values: position + normal (6 channels)
694
+ cond_values = torch.cat([pos_r, normal_r], dim=1)
695
+
696
+ # World-to-camera transformation
697
+ w2c = torch.linalg.inv(c2w).to(device=device, dtype=dtype)
698
+
699
+ corr_f2l = None
700
+ corr_f2l_highres = None
701
+ pos_delta = None
702
+
703
+ # Build correspondence pairs
704
+ corr_pairs = build_corr_pairs(
705
+ c2w, scale, depth, nor_w=normal_fullres.permute(0, 2, 3, 1),
706
+ depth_tol=0.01, angle_tol=10, angle_cam_tol=80,
707
+ )
708
+
709
+ # High resolution correspondence for c-PSNR evaluation
710
+ corr_high = downscale_pairs_to_f2l(corr_pairs, out_hw=height, device=device)
711
+ corr_f2l_highres = corr_high.unsqueeze(0) # (1, V*H*W, K) for later use
712
+
713
+ # Downscale to token resolution
714
+ corr_low = downscale_pairs_to_f2l(corr_pairs, out_hw=token_hw, device=device)
715
+ for _ in range(corr_dilate_iterations):
716
+ corr_low = dilate_f2l(corr_low, V=num_views, out_hw=token_hw)
717
+
718
+ # Convert to global indices (single batch)
719
+ corr_f2l = corr_low.unsqueeze(0) # (1, Lq, K)
720
+ B, Lq, K = corr_f2l.shape
721
+ X = Lq
722
+ base = torch.arange(B, device=corr_f2l.device).view(B, 1, 1) * X
723
+ corr_f2l = torch.where(corr_f2l >= 0, corr_f2l + base, corr_f2l)
724
+ corr_f2l = corr_f2l.reshape(B * Lq, K) # (M, K) where M = num_views * token_hw * token_hw
725
+
726
+ # Compute position deltas
727
+ M = num_views * token_hw * token_hw
728
+
729
+ # pos_token: (V, 3, token_hw, token_hw) -> (M, 3)
730
+ pos_w_flat = pos_token.view(num_views, 3, -1).permute(0, 2, 1).reshape(M, 3) # (M, 3)
731
+
732
+ # Get camera transforms for each query token
733
+ batch_ids = torch.arange(num_views, device=device).repeat_interleave(token_hw * token_hw) # (M,)
734
+
735
+ # Use identity matrix for global frame, or w2c for local query frame
736
+ if use_global_pos:
737
+ # Global frame: use identity matrix (no transformation)
738
+ Tq = torch.eye(4, device=device, dtype=w2c.dtype).unsqueeze(0).expand(M, -1, -1) # (M, 4, 4)
739
+ else:
740
+ # Local query frame: use w2c transformation
741
+ Tq = w2c[batch_ids] # (M, 4, 4)
742
+
743
+ # Transform query positions to camera space
744
+ ones_M = torch.ones(M, 1, device=device, dtype=pos_w_flat.dtype)
745
+ pq_h = torch.cat([pos_w_flat, ones_M], dim=-1).unsqueeze(-1) # (M, 4, 1)
746
+ pq_cam = (Tq @ pq_h).squeeze(-1)[..., :3] # (M, 3)
747
+
748
+ # Gather key positions and transform to camera space
749
+ gather_idx = corr_f2l.clamp(min=0, max=M - 1).reshape(-1) # (M*K,)
750
+ pk_world = pos_w_flat.index_select(0, gather_idx).view(M, K, 3) # (M, K, 3)
751
+ ones_MK = torch.ones(M, K, 1, device=device, dtype=pk_world.dtype)
752
+ pk_h = torch.cat([pk_world, ones_MK], dim=-1).unsqueeze(-1) # (M, K, 4, 1)
753
+ pk_cam = (Tq[:, None, :, :] @ pk_h).squeeze(-1)[..., :3] # (M, K, 3)
754
+
755
+ # Compute delta
756
+ pos_delta = pk_cam - pq_cam[:, None, :] # (M, K, 3)
757
+
758
+ return {
759
+ "cond_values": cond_values,
760
+ "pos_token": pos_token,
761
+ "w2c": w2c,
762
+ "corr_f2l": corr_f2l,
763
+ "corr_f2l_highres": corr_f2l_highres,
764
+ "pos_delta": pos_delta,
765
+ "decoded_albedo": albedo_r,
766
+ "decoded_orm": orm_r,
767
+ }
768
+
769
+
770
+ # =============================================================================
771
+ # Main Inference Function (using pre-loaded models)
772
+ # =============================================================================
773
+
774
+ @torch.no_grad()
775
+ def run_single(
776
+ mesh_path: str,
777
+ prompt: str,
778
+ output_dir: str,
779
+ context: InferenceContext,
780
+ *,
781
+ keep_strips: bool = False,
782
+ seed: int = 42,
783
+ ) -> str:
784
+ """Run inference on a single mesh using pre-loaded models from context.
785
+
786
+ This function produces identical results to running inference.py individually,
787
+ but reuses models from the context to avoid repeated loading.
788
+ """
789
+ gid = Path(mesh_path).stem
790
+ settings = context.settings
791
+
792
+ # Generate deterministic seed per mesh
793
+ hash_input = f"{gid}_{seed}".encode('utf-8')
794
+ combined_seed = int(hashlib.md5(hash_input).hexdigest(), 16) % (2**32)
795
+ seed_everything(combined_seed)
796
+
797
+ device = settings["device"]
798
+ dtype = settings["dtype"]
799
+ height = settings["resolution"]
800
+ width = settings["resolution"]
801
+ num_views = settings["num_views"]
802
+ token_hw = height // 16
803
+ use_caa = settings["use_caa"]
804
+ use_rope = settings["use_rope"]
805
+ use_global_token = settings["use_global_token"]
806
+ use_global_pos = settings.get("use_global_pos", False)
807
+ use_dual_branch = settings["use_dual_branch"]
808
+ branch_factor = 2 if use_dual_branch else 1
809
+
810
+ # Render and preprocess geometry
811
+ rend = render_views(mesh_path, num_views=num_views, height=height, width=width, device=device)
812
+ preprocessed = preprocess_geometry(
813
+ rend,
814
+ device=device,
815
+ dtype=dtype,
816
+ height=height,
817
+ width=width,
818
+ token_hw=token_hw,
819
+ num_views=num_views,
820
+ use_caa=use_caa,
821
+ corr_dilate_iterations=settings["corr_dilate_iterations"],
822
+ use_global_pos=use_global_pos,
823
+ )
824
+
825
+ # Encode conditions if needed
826
+ cond_latents = None
827
+ condition_channels = 0
828
+ if settings.get("condition_channels_cfg", 0) > 0:
829
+ cond_latents = context.encode_conditions(preprocessed["cond_values"])
830
+ condition_channels = cond_latents.shape[1]
831
+
832
+ # Encode text prompts using pre-loaded text context
833
+ text_embeds = context.encode_text_prompts(prompt, settings["negative_prompt"])
834
+
835
+ def _repeat_branch(tensor: Optional[torch.Tensor]) -> Optional[torch.Tensor]:
836
+ if tensor is None:
837
+ return None
838
+ first_dim = tensor.shape[0]
839
+ if first_dim == num_views:
840
+ tensor = tensor.reshape(1, num_views, *tensor.shape[1:])
841
+ elif first_dim == 1:
842
+ tensor = tensor.reshape(1, *tensor.shape[1:]).unsqueeze(1)
843
+ else:
844
+ return tensor
845
+ tensor = tensor.repeat(branch_factor, 1, *[1] * (tensor.dim() - 2))
846
+ return tensor.reshape(branch_factor * num_views, *tensor.shape[2:])
847
+
848
+ prompt_embeds = _repeat_branch(text_embeds["prompt_embeds"])
849
+ pooled_prompt_embeds = _repeat_branch(text_embeds["pooled_prompt_embeds"])
850
+ negative_prompt_embeds = _repeat_branch(text_embeds["negative_prompt_embeds"])
851
+ negative_pooled_prompt_embeds = _repeat_branch(text_embeds["negative_pooled_prompt_embeds"])
852
+ cond_latents_branch = _repeat_branch(cond_latents) if cond_latents is not None else None
853
+
854
+ # Prepare correspondence data for CAA
855
+ corr_lookups_branch = None
856
+ pos_delta_branch = None
857
+ if use_caa:
858
+ corr_f2l = preprocessed["corr_f2l"]
859
+ pos_delta = preprocessed["pos_delta"]
860
+
861
+ if corr_f2l is not None and pos_delta is not None:
862
+ if use_dual_branch:
863
+ M, K = corr_f2l.shape
864
+ corr_branch_list = []
865
+ for b in range(branch_factor):
866
+ corr_b = corr_f2l + (b * M)
867
+ corr_branch_list.append(corr_b)
868
+ corr_lookups_branch = torch.cat(corr_branch_list, dim=0)
869
+ pos_delta_branch = pos_delta.repeat(branch_factor, 1, 1)
870
+ else:
871
+ corr_lookups_branch = corr_f2l
872
+ pos_delta_branch = pos_delta
873
+
874
+ # Generate latents using pre-loaded transformer
875
+ latents = generate_latents(
876
+ transformer=context.transformer,
877
+ noise_scheduler=context.scheduler,
878
+ prompt_embeds=prompt_embeds,
879
+ pooled_prompt_embeds=pooled_prompt_embeds,
880
+ negative_prompt_embeds=negative_prompt_embeds,
881
+ negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
882
+ height=height,
883
+ width=width,
884
+ num_inference_steps=settings["steps"],
885
+ guidance_scale=settings["guidance_scale"],
886
+ weight_dtype=dtype,
887
+ device=device,
888
+ condition_channels=condition_channels,
889
+ condition_latents=cond_latents_branch,
890
+ corr_lookups=corr_lookups_branch,
891
+ pos_delta=pos_delta_branch,
892
+ progress=True,
893
+ )
894
+
895
+ # Decode using pre-loaded VAE/mcVAE
896
+ inv = (latents / context.vae_scale) + context.vae_shift
897
+
898
+ if use_dual_branch:
899
+ decoded = context.vae.decode(inv.to(device=context.vae.device, dtype=context.vae.dtype)).sample
900
+ decoded = decoded.view(branch_factor, num_views, 3, height, width)
901
+ albedo_pred = decoded[0]
902
+ orm_pred_full = decoded[1]
903
+ combined = torch.cat([albedo_pred, orm_pred_full[:, 1:, :, :]], dim=1)
904
+ combined_flat = combined.reshape(num_views, 5, height, width)
905
+ else:
906
+ inv_mc = inv.to(device=context.mcvae.device, dtype=context.mcvae.dtype)
907
+ decoded = context.mcvae.decode_aug(inv_mc).sample
908
+ combined_flat = decoded.reshape(num_views, 5, height, width)
909
+ albedo_pred = combined_flat[:, :3]
910
+ orm_pred_full = combined_flat[:, 3:]
911
+
912
+ # Compute c-PSNR metrics
913
+ cpsnr_metrics = None
914
+ if preprocessed.get("corr_f2l_highres") is not None:
915
+ corr_high = preprocessed["corr_f2l_highres"]
916
+
917
+ B, VHW, K = corr_high.shape
918
+ base = torch.arange(B, device=corr_high.device).view(B, 1, 1) * VHW
919
+ corr_high_global = torch.where(corr_high >= 0, corr_high + base, corr_high)
920
+ corr_high_global = corr_high_global.reshape(B * VHW, K)
921
+
922
+ pred_rgb = combined_flat[:, :3].unsqueeze(0).squeeze(0)
923
+ pred_mr = combined_flat[:, 3:].unsqueeze(0).squeeze(0)
924
+
925
+ albedo_psnr, _, num_elements = correspondence_psnr(pred_rgb, corr_high_global, data_range=2.0)
926
+ roughness_psnr, _, _ = correspondence_psnr(pred_mr[:, 0:1], corr_high_global, data_range=2.0)
927
+ metallic_psnr, _, _ = correspondence_psnr(pred_mr[:, 1:2], corr_high_global, data_range=2.0)
928
+
929
+ pred_combined = combined_flat.unsqueeze(0).squeeze(0)
930
+ total_psnr, _, _ = correspondence_psnr(pred_combined, corr_high_global, data_range=2.0)
931
+
932
+ cpsnr_metrics = {
933
+ "albedo": float(albedo_psnr.item()),
934
+ "roughness": float(roughness_psnr.item()),
935
+ "metallic": float(metallic_psnr.item()),
936
+ "total": float(total_psnr.item()),
937
+ "num_elements": num_elements,
938
+ }
939
+
940
+ dec_albedo = albedo_pred
941
+ if use_dual_branch:
942
+ dec_orm = torch.cat([
943
+ torch.full_like(orm_pred_full[:, :1], fill_value=-1.0),
944
+ orm_pred_full[:, 1:, :, :],
945
+ ], dim=1)
946
+ else:
947
+ dec_orm = torch.cat([
948
+ torch.full_like(combined_flat[:, :1], fill_value=-1.0),
949
+ combined_flat[:, (3, 4)],
950
+ ], dim=1)
951
+
952
+ os.makedirs(output_dir, exist_ok=True)
953
+
954
+ # Save or create temporary strips
955
+ if keep_strips:
956
+ albedo_path = os.path.join(output_dir, f"{gid}_albedo_strip.png")
957
+ orm_path = os.path.join(output_dir, f"{gid}_orm_strip.png")
958
+ pos = preprocessed["cond_values"][:, :3].clone()
959
+ normal = preprocessed["cond_values"][:, 3:].clone()
960
+ cond_values_path = os.path.join(output_dir, f"{gid}_cond_values_strip.png")
961
+ cond_values_strip = torch.cat([(pos + 1.0) * 0.5, (normal + 1.0) * 0.5], dim=0).to("cpu")
962
+ _save_strip(cond_values_strip, cond_values_path, nrow=2)
963
+ else:
964
+ albedo_fd, albedo_path = tempfile.mkstemp(suffix="_albedo_strip.png")
965
+ orm_fd, orm_path = tempfile.mkstemp(suffix="_orm_strip.png")
966
+ os.close(albedo_fd)
967
+ os.close(orm_fd)
968
+
969
+ dec_albedo_cpu = dec_albedo.detach().to("cpu")
970
+ dec_orm_cpu = dec_orm.detach().to("cpu")
971
+
972
+ _save_strip(dec_albedo_cpu, albedo_path)
973
+ _save_strip(dec_orm_cpu, orm_path)
974
+
975
+ # Unprojection
976
+ unproj_backend = settings.get("unproj_backend", "mvadapter")
977
+
978
+ if unproj_backend == "materialmvp":
979
+ # Use pre-loaded unprojection context
980
+ albedo_strip_img = Image.open(albedo_path)
981
+ orm_strip_img = Image.open(orm_path)
982
+
983
+ strip_width = albedo_strip_img.width
984
+ strip_height = albedo_strip_img.height
985
+ view_width = strip_width // num_views
986
+
987
+ albedo_views = []
988
+ mr_views = []
989
+
990
+ for i in range(num_views):
991
+ left = i * view_width
992
+ right = (i + 1) * view_width
993
+
994
+ albedo_view = albedo_strip_img.crop((left, 0, right, strip_height))
995
+ albedo_views.append(albedo_view)
996
+
997
+ orm_view = orm_strip_img.crop((left, 0, right, strip_height))
998
+ mr_views.append(orm_view)
999
+
1000
+ output_path = context.unproj_ctx.unproject_with_materialmvp(
1001
+ mesh_path=mesh_path,
1002
+ output_dir=output_dir,
1003
+ gid=gid,
1004
+ albedo_images=albedo_views,
1005
+ mr_images=mr_views,
1006
+ bake_exp=2,
1007
+ )
1008
+
1009
+ if not keep_strips:
1010
+ for path in (albedo_path, orm_path):
1011
+ if path and os.path.exists(path):
1012
+ try:
1013
+ os.remove(path)
1014
+ except OSError:
1015
+ pass
1016
+
1017
+ else: # mvadapter (default)
1018
+ try:
1019
+ result = context.tex_pipeline(
1020
+ mesh_path=mesh_path,
1021
+ save_dir=output_dir,
1022
+ save_name=gid,
1023
+ uv_unwarp=True,
1024
+ preprocess_mesh=False,
1025
+ move_to_center=True,
1026
+ uv_size=settings["uv_size"],
1027
+ base_color_path=albedo_path,
1028
+ orm_path=orm_path,
1029
+ base_color_process_config=ModProcessConfig(view_upscale=True, inpaint_mode="view"),
1030
+ orm_process_config=ModProcessConfig(view_upscale=True, inpaint_mode="view"),
1031
+ camera_elevation_deg=ELEV_DEG,
1032
+ camera_azimuth_deg=AZIM_DEG,
1033
+ )
1034
+ output_path = result.pbr_model_save_path
1035
+ finally:
1036
+ if not keep_strips:
1037
+ for path in (albedo_path, orm_path):
1038
+ if path and os.path.exists(path):
1039
+ try:
1040
+ os.remove(path)
1041
+ except OSError:
1042
+ pass
1043
+
1044
+ # Save c-PSNR metrics
1045
+ if cpsnr_metrics is not None:
1046
+ cpsnr_json_path = os.path.join(output_dir, "cpsnr.json")
1047
+ _update_cpsnr_json(cpsnr_json_path, gid, cpsnr_metrics)
1048
+
1049
+ return output_path
1050
+
1051
+
1052
+ # =============================================================================
1053
+ # Batch Processing
1054
+ # =============================================================================
1055
+
1056
+ def run_batch(
1057
+ mesh_paths: List[str],
1058
+ prompts: List[str],
1059
+ output_dir: str,
1060
+ settings: Dict[str, Any],
1061
+ *,
1062
+ keep_strips: bool = False,
1063
+ seed: int = 42,
1064
+ ) -> List[str]:
1065
+ """Process multiple mesh-prompt pairs efficiently.
1066
+
1067
+ Models are loaded once and reused for all pairs.
1068
+
1069
+ Args:
1070
+ mesh_paths: List of paths to input meshes
1071
+ prompts: List of text prompts (must match length of mesh_paths)
1072
+ output_dir: Output directory for all results
1073
+ settings: Inference settings dictionary
1074
+ keep_strips: Whether to keep intermediate strip images
1075
+ seed: Base random seed
1076
+
1077
+ Returns:
1078
+ List of output GLB paths
1079
+ """
1080
+ if len(mesh_paths) != len(prompts):
1081
+ raise ValueError(f"Number of meshes ({len(mesh_paths)}) must match number of prompts ({len(prompts)})")
1082
+
1083
+ print(f"Starting batch inference for {len(mesh_paths)} mesh-prompt pairs...")
1084
+ print(f"Loading models once for reuse...")
1085
+
1086
+ # Initialize context with all pre-loaded models
1087
+ context = InferenceContext(settings)
1088
+
1089
+ print(f"✓ Models loaded successfully")
1090
+ print()
1091
+
1092
+ output_paths = []
1093
+
1094
+ for idx, (mesh_path, prompt) in enumerate(zip(mesh_paths, prompts), 1):
1095
+ print(f"[{idx}/{len(mesh_paths)}] Processing: {Path(mesh_path).name}")
1096
+ print(f" Prompt: {prompt}")
1097
+
1098
+ try:
1099
+ output_path = run_single(
1100
+ mesh_path=mesh_path,
1101
+ prompt=prompt,
1102
+ output_dir=output_dir,
1103
+ context=context,
1104
+ keep_strips=keep_strips,
1105
+ seed=seed,
1106
+ )
1107
+ output_paths.append(output_path)
1108
+ print(f" ✓ Saved: {output_path}")
1109
+ except Exception as e:
1110
+ print(f" ✗ Error: {e}")
1111
+ output_paths.append(None)
1112
+
1113
+ print()
1114
+
1115
+ # Summary
1116
+ successful = sum(1 for p in output_paths if p is not None)
1117
+ print(f"Batch processing complete: {successful}/{len(mesh_paths)} successful")
1118
+
1119
+ return output_paths
1120
+
1121
+
1122
+ def build_inference_settings(args, cfg) -> Dict[str, Any]:
1123
+ """Build inference settings from config file and CLI overrides."""
1124
+ model_cfg = cfg.model
1125
+
1126
+ # Apply CLI argument overrides
1127
+ if args.num_views is not None:
1128
+ model_cfg.num_views = args.num_views
1129
+ if args.resolution is not None:
1130
+ model_cfg.resolution = args.resolution
1131
+ if args.sd_vae_path is not None:
1132
+ model_cfg.sd_vae_path = args.sd_vae_path
1133
+ if args.mcvae_config is not None:
1134
+ model_cfg.mcvae_config = args.mcvae_config
1135
+ if args.mcvae_ckpt is not None:
1136
+ model_cfg.mcvae_ckpt = args.mcvae_ckpt
1137
+
1138
+ dtype_map = {
1139
+ "fp16": torch.float16,
1140
+ "bf16": torch.bfloat16,
1141
+ "fp32": torch.float32,
1142
+ }
1143
+ dtype = dtype_map.get(args.precision.lower(), DTYPE)
1144
+
1145
+ settings = {
1146
+ "device": args.device,
1147
+ "dtype": dtype,
1148
+ "resolution": int(model_cfg.resolution),
1149
+ "num_views": int(model_cfg.num_views),
1150
+ "use_caa": bool(getattr(model_cfg, "use_caa", True)),
1151
+ "use_rope": bool(getattr(model_cfg, "use_rope", False)),
1152
+ "use_global_token": bool(getattr(model_cfg, "use_global_token", False)),
1153
+ "use_global_pos": bool(getattr(model_cfg, "use_global_pos", False)),
1154
+ "use_dual_branch": bool(getattr(model_cfg, "use_dual_branch", False)),
1155
+ "corr_dilate_iterations": int(getattr(model_cfg, "corr_dilate_iterations", 2)),
1156
+ "condition_channels_cfg": int(getattr(model_cfg, "condition_channels", 0)),
1157
+ "base_model": model_cfg.pretrained_model_name_or_path,
1158
+ "model_cfg": model_cfg,
1159
+ "weights_path": args.weights or TRANSFORMER_PARTIAL_WEIGHTS,
1160
+ "sd_vae_path": model_cfg.sd_vae_path,
1161
+ "mcvae_config_path": model_cfg.mcvae_config,
1162
+ "mcvae_ckpt_path": model_cfg.mcvae_ckpt or MCVAE_CKPT_PATH,
1163
+ "mcvae_offset_mode": bool(getattr(model_cfg, "mcvae_offset_mode", True)),
1164
+ "negative_prompt": args.negative_prompt,
1165
+ "steps": args.steps,
1166
+ "guidance_scale": args.guidance,
1167
+ "realesrgan_ckpt": args.realesrgan_ckpt,
1168
+ "lama_ckpt": args.lama_ckpt,
1169
+ "uv_size": args.uv_size,
1170
+ "unproj_backend": args.unproj_backend,
1171
+ "config": cfg,
1172
+ }
1173
+
1174
+ return settings
1175
+
1176
+
1177
+ # =============================================================================
1178
+ # CLI Entry Point
1179
+ # =============================================================================
1180
+
1181
+ if __name__ == "__main__":
1182
+ import argparse
1183
+
1184
+ parser = argparse.ArgumentParser(
1185
+ description="Batch Mesh + Prompt → Textured GLB (efficient multi-sample processing)",
1186
+ epilog="""
1187
+ Examples:
1188
+ # Single mesh-prompt pair (same as inference.py)
1189
+ python inference_list.py --mesh model.glb --prompt "wooden chair"
1190
+
1191
+ # Multiple mesh-prompt pairs
1192
+ python inference_list.py --mesh 1.glb 2.glb 3.glb --prompt "p1" "p2" "p3"
1193
+
1194
+ Additional config overrides can be provided as key=value arguments:
1195
+ python inference_list.py --mesh a.glb b.glb --prompt "p1" "p2" model.num_views=8
1196
+ """
1197
+ )
1198
+ parser.add_argument("--mesh", nargs="+", required=True, help="Path(s) to input mesh(es) (.glb or .obj)")
1199
+ parser.add_argument("--prompt", nargs="+", required=True, help="Text prompt(s) for texture generation (must match number of meshes)")
1200
+ parser.add_argument("--out", default="./temp_outputs", help="Output directory")
1201
+ parser.add_argument("--keep-strips", action="store_true", help="Keep intermediate albedo/ORM strip images")
1202
+ parser.add_argument("--guidance", type=float, default=GUIDANCE, help=f"Guidance scale (default: {GUIDANCE})")
1203
+ parser.add_argument("--steps", type=int, default=STEPS, help=f"Number of denoising steps (default: {STEPS})")
1204
+ parser.add_argument("--seed", type=int, default=42, help="Random seed (default: 42)")
1205
+ parser.add_argument("--negative-prompt", default=NEGATIVE_PROMPT, help="Negative prompt text")
1206
+ parser.add_argument("--device", default=DEVICE, help="Torch device to run inference on")
1207
+ parser.add_argument("--precision", default="fp16", choices=["fp16", "bf16", "fp32"], help="Computation precision")
1208
+ parser.add_argument("--num-views", type=int, default=None, help="Override number of camera views")
1209
+ parser.add_argument("--resolution", type=int, default=None, help="Override render resolution")
1210
+ parser.add_argument("--sd-vae-path", default=None, help="Override base SD VAE path")
1211
+ parser.add_argument("--mcvae-config", default=None, help="Override mcVAE config path")
1212
+ parser.add_argument("--mcvae-ckpt", default=None, help="Override mcVAE checkpoint path")
1213
+ parser.add_argument("--weights", default=None, help="Path to trained transformer weights (dir or file)")
1214
+ parser.add_argument("--realesrgan-ckpt", default="./checkpoints/RealESRGAN_x2plus.pth", help="RealESRGAN checkpoint path")
1215
+ parser.add_argument("--lama-ckpt", default="./checkpoints/big-lama.pt", help="LaMa inpainting checkpoint path")
1216
+ parser.add_argument("--uv-size", type=int, default=4096, help="Final UV texture resolution")
1217
+ parser.add_argument("--config", default="configs/mcdiff/default.yaml", help="Config file (supports base-variant inheritance)")
1218
+ parser.add_argument("--unproj-backend", default="mvadapter", choices=["mvadapter", "materialmvp"],
1219
+ help="Unprojection backend")
1220
+
1221
+ args, unknown = parser.parse_known_args()
1222
+
1223
+ # Validate mesh and prompt counts match
1224
+ if len(args.mesh) != len(args.prompt):
1225
+ parser.error(f"Number of --mesh arguments ({len(args.mesh)}) must match number of --prompt arguments ({len(args.prompt)})")
1226
+
1227
+ # Load config
1228
+ cfg = load_config(args.config, cli_overrides=unknown)
1229
+
1230
+ # Build settings
1231
+ settings = build_inference_settings(args, cfg)
1232
+
1233
+ # Run batch processing
1234
+ output_paths = run_batch(
1235
+ mesh_paths=args.mesh,
1236
+ prompts=args.prompt,
1237
+ output_dir=args.out,
1238
+ settings=settings,
1239
+ keep_strips=args.keep_strips,
1240
+ seed=args.seed,
1241
+ )
1242
+
1243
+ print("\n" + "="*60)
1244
+ print("Final Results:")
1245
+ print("="*60)
1246
+ for mesh_path, prompt, output_path in zip(args.mesh, args.prompt, output_paths):
1247
+ status = "✓" if output_path else "✗"
1248
+ print(f"{status} {Path(mesh_path).name}")
1249
+ if output_path:
1250
+ print(f" → {output_path}")
1251
+ print("="*60)
1252
+
1253
+
1254
+ '''
1255
+ # Single mesh (backward compatible with inference.py)
1256
+ python inference_list.py \
1257
+ --mesh /home/aaaaa/data/Arb-Objaverse/data/glb/000-132/21ec37b286474fedb43307f6f289269e.glb \
1258
+ --prompt "A wooden stool with a pink cushion" \
1259
+ --config configs/mcdiff/single_caa.yaml \
1260
+ --weights outputs/mcdiff_v.single_caa/checkpoint-35000/transformer/diffusion_pytorch_model.safetensors \
1261
+ --out temp_outputs_list
1262
+
1263
+ # Multiple meshes (efficient batch processing)
1264
+ python inference_list.py \
1265
+ --mesh mesh1.glb mesh2.glb mesh3.glb \
1266
+ --prompt "prompt 1" "prompt 2" "prompt 3" \
1267
+ --config configs/mcdiff/single_caa.yaml \
1268
+ --weights outputs/mcdiff_v.single_caa/checkpoint-35000/transformer/diffusion_pytorch_model.safetensors \
1269
+ --out temp_outputs_list_batch
1270
+ '''
home/ubuntu/aaaaa/data/rgbmr/latent_vis/base.png ADDED
home/ubuntu/aaaaa/data/rgbmr/latent_vis/full.png ADDED
home/ubuntu/aaaaa/data/rgbmr/latent_vis/offset.png ADDED