diff --git a/README.md b/README.md
index dee4782..56386cd 100644
--- a/README.md
+++ b/README.md
@@ -12,26 +12,23 @@
### Note: this project is still a WIP and not been released into ComFyUI package database yet
## Currently support:
-- For use case please check [Example Workflows](./_Example_Workflows/). [**Last update: 04/02/2024**]
+- For use case please check [Example Workflows](./_Example_Workflows/). [**Last update: 09/02/2024**]
- **Note:** you need to put [Example Inputs Files & Folders](_Example_Workflows/_Example_Inputs_Files/) under ComfyUI Root Directory\ComfyUI\input folder before you can run the example workflow
+
+- **Large Multiview Gaussian Model**: [3DTopia/LGM](https://github.com/3DTopia/LGM)
+ - Enable single image to 3D Gaussian in less than 30 seconds on a RTX3080 GPU, later you can also convert 3D Gaussian to mesh
+
+
+
+- **Triplane Gaussian Transformers**: [VAST-AI-Research/TriplaneGaussian](https://github.com/VAST-AI-Research/TriplaneGaussian)
+ - Enable single image to 3D Gaussian in less than 10 seconds on a RTX3080 GPU, later you can also convert 3D Gaussian to mesh
+
+
+
- **Preview 3DGS and 3D Mesh**: 3D Visualization inside ComfyUI:
- Using [gsplat.js](https://github.com/huggingface/gsplat.js/tree/main) and [three.js](https://github.com/mrdoob/three.js/tree/dev) for 3DGS & 3D Mesh visualization respectively
- https://github.com/MrForExample/ComfyUI-3D-Pack/assets/62230687/9f3c56b1-afb3-4bf1-8845-ab1025a87463
-
-- **Triplane Gaussian Transformers**: [VAST-AI-Research/TriplaneGaussian](https://github.com/VAST-AI-Research/TriplaneGaussian)
- - Enable single image to 3D Gaussian, then you can use Deep Marching Tetrahedrons node to convert it to mesh
-
-
-
-
-
- |
-
-
- |
-
-
+
- **Stack Orbit Camera Poses**: Automatically generate all range of camera pose combinations
- You can use it to conditioning the [StableZero123 (You need to Download the checkpoint first)](https://comfyanonymous.github.io/ComfyUI_examples/3d/), with full range of camera poses in one prompt pass
@@ -71,17 +68,20 @@
- Since different algorithms likely use different coordinate system, so the ability to re-mapping the axis of coordinate is crucial for passing generated result between differnt nodes.
## Roadmap:
-- [x] Add DMTet algorithm to allow convertion from points cloud(Gaussian/.ply) to mesh (.obj, .ply, .glb)
+- [x] Add DMTet algorithm to allow conversion from points cloud(Gaussian/.ply) to mesh (.obj, .ply, .glb)
-- [x] Integrate [Triplane Meets Gaussian Splatting: Fast and Generalizable Single-View 3D Reconstruction with Transformers](https://github.com/VAST-AI-Research/TriplaneGaussian)
+- [x] Integrate [Triplane Meets Gaussian Splatting: Fast and Generalizable Single-View 3D Reconstruction with Transformers](https://zouzx.github.io/TriplaneGaussian/)
- [x] Add interactive 3D UI inside ComfuUI to visulaize training and generated results for 3D representations
- [x] Add a new node to generate renderer image sequence given a 3D gaussians and orbit camera poses (So we can later feed it to the differentiable renderer to bake it onto a given mesh)
-- [ ] Add a general SDS/VSD Optimization algorithm to allow training 3D representations with diffusion model, *The real fun begins here* ;)
+- [x] Integrate [LGM: Large Multi-View Gaussian Model for High-Resolution 3D Content Creation](https://me.kiui.moe/lgm/)
-- [ ] Improve 3DGS to Mesh:
+- [ ] Add a general SDS/ISM Optimization algorithm to allow training 3D representations with diffusion model, *The real fun starts here* ;)
+ - Need to do some in-depth research on Interval Score Matching (ISM), since math behind it makes perfect sense and also there are so many ways we could improve upon the result obtained from [LucidDreamer](https://github.com/EnVision-Research/LucidDreamer)
+
+- [ ] Improve 3DGS to Mesh conversion algorithms:
- Support to training DMTet with images(RGB, Alpha, Normal Map)
- Find better methods to converts 3DGS or Points Cloud to Mesh (Normal maps reconstruction maybe?)
diff --git a/_Example_Workflows/Large_Multiview_Gaussian_Model_to_3DGS and_3DMesh.json b/_Example_Workflows/Large_Multiview_Gaussian_Model_to_3DGS and_3DMesh.json
new file mode 100644
index 0000000..27b84e1
--- /dev/null
+++ b/_Example_Workflows/Large_Multiview_Gaussian_Model_to_3DGS and_3DMesh.json
@@ -0,0 +1,627 @@
+{
+ "last_node_id": 16,
+ "last_link_id": 18,
+ "nodes": [
+ {
+ "id": 3,
+ "type": "[Comfy3D] MVDream Model",
+ "pos": [
+ 792,
+ 460
+ ],
+ "size": {
+ "0": 405.5999755859375,
+ "1": 238
+ },
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mvdream_pipe",
+ "type": "DIFFUSERS_PIPE",
+ "link": 1
+ },
+ {
+ "name": "reference_image",
+ "type": "IMAGE",
+ "link": 2
+ },
+ {
+ "name": "reference_mask",
+ "type": "MASK",
+ "link": 5
+ }
+ ],
+ "outputs": [
+ {
+ "name": "multiview_images",
+ "type": "IMAGE",
+ "links": [
+ 6
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] MVDream Model"
+ },
+ "widgets_values": [
+ "",
+ "ugly, blurry, pixelated obscure, unnatural colors, poor lighting, dull, unclear, cropped, lowres, low quality, artifacts, duplicate",
+ 5,
+ 30,
+ 0
+ ]
+ },
+ {
+ "id": 4,
+ "type": "LoadImage",
+ "pos": [
+ 177,
+ 479
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 2
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": [
+ 3
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "Cat_Coffee_rgba.png",
+ "image"
+ ]
+ },
+ {
+ "id": 5,
+ "type": "InvertMask",
+ "pos": [
+ 539,
+ 689
+ ],
+ "size": {
+ "0": 210,
+ "1": 26
+ },
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 3
+ }
+ ],
+ "outputs": [
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": [
+ 5
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "InvertMask"
+ }
+ },
+ {
+ "id": 2,
+ "type": "[Comfy3D] Load Diffusers Pipeline",
+ "pos": [
+ 381,
+ 319
+ ],
+ "size": {
+ "0": 375.3948669433594,
+ "1": 83.33004760742188
+ },
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "pipe",
+ "type": "DIFFUSERS_PIPE",
+ "links": [
+ 1,
+ 8
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Load Diffusers Pipeline"
+ },
+ "widgets_values": [
+ "MVDreamPipeline",
+ "ashawkey/imagedream-ipmv-diffusers"
+ ]
+ },
+ {
+ "id": 12,
+ "type": "[Comfy3D] Switch 3DGS Axis",
+ "pos": [
+ 1554,
+ 460
+ ],
+ "size": {
+ "0": 210,
+ "1": 106
+ },
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 12
+ }
+ ],
+ "outputs": [
+ {
+ "name": "switched_gs_ply",
+ "type": "GS_PLY",
+ "links": [
+ 13
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Switch 3DGS Axis"
+ },
+ "widgets_values": [
+ "+x",
+ "-y",
+ "-z"
+ ]
+ },
+ {
+ "id": 8,
+ "type": "Reroute",
+ "pos": [
+ 1150,
+ 319
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 8
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "DIFFUSERS_PIPE",
+ "links": [
+ 9
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 9,
+ "type": "[Comfy3D] Save 3DGS",
+ "pos": [
+ 1798,
+ 459
+ ],
+ "size": {
+ "0": 315,
+ "1": 58
+ },
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 13
+ }
+ ],
+ "outputs": [
+ {
+ "name": "save_path",
+ "type": "STRING",
+ "links": [
+ 11
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Save 3DGS"
+ },
+ "widgets_values": [
+ "LGMTest/FatCat.ply"
+ ]
+ },
+ {
+ "id": 10,
+ "type": "[Comfy3D] Preview 3DGS",
+ "pos": [
+ 2309.8418660992434,
+ 768.9263430496216
+ ],
+ "size": [
+ 600,
+ 500
+ ],
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_file_path",
+ "type": "STRING",
+ "link": 11,
+ "widget": {
+ "name": "gs_file_path"
+ }
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Preview 3DGS"
+ },
+ "widgets_values": [
+ "",
+ null
+ ]
+ },
+ {
+ "id": 1,
+ "type": "[Comfy3D] Load Large Multiview Gaussian Model",
+ "pos": [
+ 879,
+ 752
+ ],
+ "size": {
+ "0": 315,
+ "1": 82
+ },
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "lgm_model",
+ "type": "LGM_MODEL",
+ "links": [
+ 7
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Load Large Multiview Gaussian Model"
+ },
+ "widgets_values": [
+ "model_fp16.safetensors",
+ "big"
+ ]
+ },
+ {
+ "id": 7,
+ "type": "[Comfy3D] Large Multiview Gaussian Model",
+ "pos": [
+ 1270,
+ 460
+ ],
+ "size": {
+ "0": 252,
+ "1": 66
+ },
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "multiview_images",
+ "type": "IMAGE",
+ "link": 6
+ },
+ {
+ "name": "lgm_model",
+ "type": "LGM_MODEL",
+ "link": 7
+ },
+ {
+ "name": "mvdream_pipe",
+ "type": "DIFFUSERS_PIPE",
+ "link": 9
+ }
+ ],
+ "outputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "links": [
+ 12,
+ 18
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Large Multiview Gaussian Model"
+ }
+ },
+ {
+ "id": 15,
+ "type": "[Comfy3D] Preview 3DMesh",
+ "pos": [
+ 2312,
+ 226
+ ],
+ "size": [
+ 600,
+ 500
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mesh_file_path",
+ "type": "STRING",
+ "link": 16,
+ "widget": {
+ "name": "mesh_file_path"
+ }
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Preview 3DMesh"
+ },
+ "widgets_values": [
+ "",
+ null
+ ]
+ },
+ {
+ "id": 16,
+ "type": "[Comfy3D] Convert 3DGS to Mesh with NeRF and Marching Cubes",
+ "pos": [
+ 1550,
+ 224
+ ],
+ "size": [
+ 411.6000061035156,
+ 65.46688079833984
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 18
+ }
+ ],
+ "outputs": [
+ {
+ "name": "mesh",
+ "type": "MESH",
+ "links": [
+ 17
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Convert 3DGS to Mesh with NeRF and Marching Cubes"
+ },
+ "widgets_values": [
+ "big"
+ ]
+ },
+ {
+ "id": 14,
+ "type": "[Comfy3D] Save 3D Mesh",
+ "pos": [
+ 1980,
+ 225
+ ],
+ "size": {
+ "0": 307.8253173828125,
+ "1": 60.248291015625
+ },
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mesh",
+ "type": "MESH",
+ "link": 17
+ }
+ ],
+ "outputs": [
+ {
+ "name": "save_path",
+ "type": "STRING",
+ "links": [
+ 16
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Save 3D Mesh"
+ },
+ "widgets_values": [
+ "LGMTest/FatCat.obj"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 1,
+ 2,
+ 0,
+ 3,
+ 0,
+ "DIFFUSERS_PIPE"
+ ],
+ [
+ 2,
+ 4,
+ 0,
+ 3,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 3,
+ 4,
+ 1,
+ 5,
+ 0,
+ "MASK"
+ ],
+ [
+ 5,
+ 5,
+ 0,
+ 3,
+ 2,
+ "MASK"
+ ],
+ [
+ 6,
+ 3,
+ 0,
+ 7,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 7,
+ 1,
+ 0,
+ 7,
+ 1,
+ "LGM_MODEL"
+ ],
+ [
+ 8,
+ 2,
+ 0,
+ 8,
+ 0,
+ "*"
+ ],
+ [
+ 9,
+ 8,
+ 0,
+ 7,
+ 2,
+ "DIFFUSERS_PIPE"
+ ],
+ [
+ 11,
+ 9,
+ 0,
+ 10,
+ 0,
+ "STRING"
+ ],
+ [
+ 12,
+ 7,
+ 0,
+ 12,
+ 0,
+ "GS_PLY"
+ ],
+ [
+ 13,
+ 12,
+ 0,
+ 9,
+ 0,
+ "GS_PLY"
+ ],
+ [
+ 16,
+ 14,
+ 0,
+ 15,
+ 0,
+ "STRING"
+ ],
+ [
+ 17,
+ 16,
+ 0,
+ 14,
+ 0,
+ "MESH"
+ ],
+ [
+ 18,
+ 7,
+ 0,
+ 16,
+ 0,
+ "GS_PLY"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS.json b/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS(DMTet and DiffRast).json
similarity index 100%
rename from _Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS.json
rename to _Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS(DMTet and DiffRast).json
diff --git a/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS_and_3DMesh.json b/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS_and_3DMesh.json
new file mode 100644
index 0000000..506d65a
--- /dev/null
+++ b/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS_and_3DMesh.json
@@ -0,0 +1,580 @@
+{
+ "last_node_id": 34,
+ "last_link_id": 52,
+ "nodes": [
+ {
+ "id": 9,
+ "type": "InvertMask",
+ "pos": [
+ 576,
+ 690
+ ],
+ "size": {
+ "0": 140,
+ "1": 26
+ },
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 10
+ }
+ ],
+ "outputs": [
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": [
+ 15
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "InvertMask"
+ }
+ },
+ {
+ "id": 27,
+ "type": "ImpactFloat",
+ "pos": [
+ 915,
+ -357
+ ],
+ "size": {
+ "0": 315,
+ "1": 58
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "FLOAT",
+ "type": "FLOAT",
+ "links": [],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "title": "Camera Fovy",
+ "properties": {
+ "Node name for S&R": "ImpactFloat"
+ },
+ "widgets_values": [
+ 49.1
+ ]
+ },
+ {
+ "id": 12,
+ "type": "[Comfy3D] Preview 3DGS",
+ "pos": [
+ 1590,
+ 800
+ ],
+ "size": [
+ 600,
+ 500
+ ],
+ "flags": {
+ "collapsed": false
+ },
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_file_path",
+ "type": "STRING",
+ "link": 13,
+ "widget": {
+ "name": "gs_file_path"
+ }
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Preview 3DGS"
+ },
+ "widgets_values": [
+ "",
+ null
+ ]
+ },
+ {
+ "id": 14,
+ "type": "[Comfy3D] Triplane Gaussian Transformers",
+ "pos": [
+ 767,
+ 631
+ ],
+ "size": {
+ "0": 252,
+ "1": 98
+ },
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "reference_image",
+ "type": "IMAGE",
+ "link": 14
+ },
+ {
+ "name": "reference_mask",
+ "type": "MASK",
+ "link": 15
+ },
+ {
+ "name": "tgs_model",
+ "type": "TGS_MODEL",
+ "link": 16
+ }
+ ],
+ "outputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "links": [
+ 17
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Triplane Gaussian Transformers"
+ },
+ "widgets_values": [
+ 1.9000000000000001
+ ]
+ },
+ {
+ "id": 26,
+ "type": "[Comfy3D] Preview 3DMesh",
+ "pos": [
+ 2328,
+ 796
+ ],
+ "size": [
+ 600,
+ 500
+ ],
+ "flags": {},
+ "order": 12,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mesh_file_path",
+ "type": "STRING",
+ "link": 32,
+ "widget": {
+ "name": "mesh_file_path"
+ }
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Preview 3DMesh"
+ },
+ "widgets_values": [
+ "",
+ null
+ ]
+ },
+ {
+ "id": 11,
+ "type": "Note",
+ "pos": [
+ 953,
+ 429
+ ],
+ "size": {
+ "0": 347.9384460449219,
+ "1": 141.6276092529297
+ },
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "title": "Switch 3DGS Axis Note",
+ "properties": {
+ "text": ""
+ },
+ "widgets_values": [
+ "Triplane Gaussian Transformers to Preview:\n axis x to: -y\n axis y to: +z\n axis z to: -x\n\nGaussian Splatting to Preview:\n axis x to: +x\n axis y to: -y\n axis z to: -z"
+ ],
+ "color": "#432",
+ "bgcolor": "#653"
+ },
+ {
+ "id": 13,
+ "type": "[Comfy3D] Switch 3DGS Axis",
+ "pos": [
+ 1052,
+ 631
+ ],
+ "size": {
+ "0": 210,
+ "1": 106
+ },
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 17
+ }
+ ],
+ "outputs": [
+ {
+ "name": "switched_gs_ply",
+ "type": "GS_PLY",
+ "links": [
+ 12,
+ 51
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Switch 3DGS Axis"
+ },
+ "widgets_values": [
+ "-y",
+ "+z",
+ "-x"
+ ]
+ },
+ {
+ "id": 10,
+ "type": "[Comfy3D] Save 3DGS",
+ "pos": [
+ 1288,
+ 800
+ ],
+ "size": {
+ "0": 275.57391357421875,
+ "1": 58
+ },
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 12
+ }
+ ],
+ "outputs": [
+ {
+ "name": "save_path",
+ "type": "STRING",
+ "links": [
+ 13
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Save 3DGS"
+ },
+ "widgets_values": [
+ "MeshTest\\3DGS_Picacho.ply"
+ ]
+ },
+ {
+ "id": 34,
+ "type": "[Comfy3D] Switch 3DGS Axis",
+ "pos": [
+ 1294,
+ 629
+ ],
+ "size": {
+ "0": 210,
+ "1": 106
+ },
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 51
+ }
+ ],
+ "outputs": [
+ {
+ "name": "switched_gs_ply",
+ "type": "GS_PLY",
+ "links": [
+ 52
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Switch 3DGS Axis"
+ },
+ "widgets_values": [
+ "+x",
+ "-y",
+ "-z"
+ ]
+ },
+ {
+ "id": 33,
+ "type": "[Comfy3D] Convert 3DGS to Mesh with NeRF and Marching Cubes",
+ "pos": [
+ 1531,
+ 629
+ ],
+ "size": {
+ "0": 411.6000061035156,
+ "1": 58
+ },
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "gs_ply",
+ "type": "GS_PLY",
+ "link": 52
+ }
+ ],
+ "outputs": [
+ {
+ "name": "mesh",
+ "type": "MESH",
+ "links": [
+ 50
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Convert 3DGS to Mesh with NeRF and Marching Cubes"
+ },
+ "widgets_values": [
+ "big"
+ ]
+ },
+ {
+ "id": 25,
+ "type": "[Comfy3D] Save 3D Mesh",
+ "pos": [
+ 1974,
+ 629
+ ],
+ "size": {
+ "0": 315,
+ "1": 58
+ },
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mesh",
+ "type": "MESH",
+ "link": 50
+ }
+ ],
+ "outputs": [
+ {
+ "name": "save_path",
+ "type": "STRING",
+ "links": [
+ 32
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Save 3D Mesh"
+ },
+ "widgets_values": [
+ "MeshTest\\3DMesh_Picacho.obj"
+ ]
+ },
+ {
+ "id": 2,
+ "type": "[Comfy3D] Load Triplane Gaussian Transformers",
+ "pos": [
+ 444,
+ 768
+ ],
+ "size": {
+ "0": 294,
+ "1": 58
+ },
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "tgs_model",
+ "type": "TGS_MODEL",
+ "links": [
+ 16
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "[Comfy3D] Load Triplane Gaussian Transformers"
+ },
+ "widgets_values": [
+ "model_lvis_rel.ckpt"
+ ]
+ },
+ {
+ "id": 4,
+ "type": "LoadImage",
+ "pos": [
+ 52,
+ 632
+ ],
+ "size": {
+ "0": 354.32330322265625,
+ "1": 315.4895935058594
+ },
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 14
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": [
+ 10
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "a_pikachu_with_smily_face-removebg.png",
+ "image"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 10,
+ 4,
+ 1,
+ 9,
+ 0,
+ "MASK"
+ ],
+ [
+ 12,
+ 13,
+ 0,
+ 10,
+ 0,
+ "GS_PLY"
+ ],
+ [
+ 13,
+ 10,
+ 0,
+ 12,
+ 0,
+ "STRING"
+ ],
+ [
+ 14,
+ 4,
+ 0,
+ 14,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 15,
+ 9,
+ 0,
+ 14,
+ 1,
+ "MASK"
+ ],
+ [
+ 16,
+ 2,
+ 0,
+ 14,
+ 2,
+ "TGS_MODEL"
+ ],
+ [
+ 17,
+ 14,
+ 0,
+ 13,
+ 0,
+ "GS_PLY"
+ ],
+ [
+ 32,
+ 25,
+ 0,
+ 26,
+ 0,
+ "STRING"
+ ],
+ [
+ 50,
+ 33,
+ 0,
+ 25,
+ 0,
+ "MESH"
+ ],
+ [
+ 51,
+ 13,
+ 0,
+ 34,
+ 0,
+ "GS_PLY"
+ ],
+ [
+ 52,
+ 34,
+ 0,
+ 33,
+ 0,
+ "GS_PLY"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {},
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/_Example_Workflows/_Example_Inputs_Files/Cat_Coffee_rgba.png b/_Example_Workflows/_Example_Inputs_Files/Cat_Coffee_rgba.png
new file mode 100644
index 0000000..1dc0b52
Binary files /dev/null and b/_Example_Workflows/_Example_Inputs_Files/Cat_Coffee_rgba.png differ
diff --git a/_Example_Workflows/_Example_Inputs_Files/Cucumber_Shark_rgba.png b/_Example_Workflows/_Example_Inputs_Files/Cucumber_Shark_rgba.png
new file mode 100644
index 0000000..70deed2
Binary files /dev/null and b/_Example_Workflows/_Example_Inputs_Files/Cucumber_Shark_rgba.png differ
diff --git a/_Example_Workflows/_Example_Inputs_Files/ghost_rgba.png b/_Example_Workflows/_Example_Inputs_Files/ghost_rgba.png
new file mode 100644
index 0000000..6ed0538
Binary files /dev/null and b/_Example_Workflows/_Example_Inputs_Files/ghost_rgba.png differ
diff --git a/algorithms/diff_texturing.py b/algorithms/diff_texturing.py
index 3388a78..2377830 100644
--- a/algorithms/diff_texturing.py
+++ b/algorithms/diff_texturing.py
@@ -4,12 +4,13 @@ import tqdm
import torch
import torch.nn.functional as F
import numpy as np
+from kiui.cam import orbit_camera
from pytorch_msssim import SSIM, MS_SSIM
import comfy.utils
from .diff_mesh_renderer import DiffRastRenderer
-from ..shared_utils.camera_utils import orbit_camera, OrbitCamera
+from ..shared_utils.camera_utils import OrbitCamera
from ..shared_utils.image_utils import prepare_torch_img
class DiffTextureBaker:
@@ -44,8 +45,8 @@ class DiffTextureBaker:
ref_imgs_torch_list = []
ref_masks_torch_list = []
for i in range(self.ref_imgs_num):
- ref_imgs_torch_list.append(prepare_torch_img(reference_images[i], self.ref_size_H, self.ref_size_W, self.device))
- ref_masks_torch_list.append(prepare_torch_img(reference_masks[i].unsqueeze(2), self.ref_size_H, self.ref_size_W, self.device))
+ ref_imgs_torch_list.append(prepare_torch_img(reference_images[i].unsqueeze(0), self.ref_size_H, self.ref_size_W, self.device))
+ ref_masks_torch_list.append(prepare_torch_img(reference_masks[i].unsqueeze(2).unsqueeze(0), self.ref_size_H, self.ref_size_W, self.device))
self.ref_imgs_torch = torch.cat(ref_imgs_torch_list, dim=0)
self.ref_masks_torch = torch.cat(ref_masks_torch_list, dim=0)
diff --git a/algorithms/large_multiview_gaussian_model.py b/algorithms/large_multiview_gaussian_model.py
new file mode 100644
index 0000000..0e6fcc4
--- /dev/null
+++ b/algorithms/large_multiview_gaussian_model.py
@@ -0,0 +1,171 @@
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from kiui.lpips import LPIPS
+
+import kiui
+from kiui.cam import orbit_camera
+
+from ..lgm.core.unet import UNet
+from ..lgm.core.options import Options
+from ..lgm.core.gs import GaussianRenderer
+from ..lgm.core.utils import get_rays
+
+class LGM(nn.Module):
+ def __init__(
+ self,
+ opt: Options,
+ ):
+ super().__init__()
+
+ self.opt = opt
+
+ # unet
+ self.unet = UNet(
+ 9, 14,
+ down_channels=self.opt.down_channels,
+ down_attention=self.opt.down_attention,
+ mid_attention=self.opt.mid_attention,
+ up_channels=self.opt.up_channels,
+ up_attention=self.opt.up_attention,
+ )
+
+ # last conv
+ self.conv = nn.Conv2d(14, 14, kernel_size=1) # NOTE: maybe remove it if train again
+
+ # Gaussian Renderer
+ self.gs = GaussianRenderer(opt)
+
+ # activations...
+ self.pos_act = lambda x: x.clamp(-1, 1)
+ self.scale_act = lambda x: 0.1 * F.softplus(x)
+ self.opacity_act = lambda x: torch.sigmoid(x)
+ self.rot_act = F.normalize
+ self.rgb_act = lambda x: 0.5 * torch.tanh(x) + 0.5 # NOTE: may use sigmoid if train again
+
+ # LPIPS loss
+ if self.opt.lambda_lpips > 0:
+ self.lpips_loss = LPIPS(net='vgg')
+ self.lpips_loss.requires_grad_(False)
+
+
+ def state_dict(self, **kwargs):
+ # remove lpips_loss
+ state_dict = super().state_dict(**kwargs)
+ for k in list(state_dict.keys()):
+ if 'lpips_loss' in k:
+ del state_dict[k]
+ return state_dict
+
+
+ def prepare_default_rays(self, device, elevation=0):
+ cam_poses = np.stack([
+ orbit_camera(elevation, 0, radius=self.opt.cam_radius),
+ orbit_camera(elevation, 90, radius=self.opt.cam_radius),
+ orbit_camera(elevation, 180, radius=self.opt.cam_radius),
+ orbit_camera(elevation, 270, radius=self.opt.cam_radius),
+ ], axis=0) # [4, 4, 4]
+ cam_poses = torch.from_numpy(cam_poses)
+
+ rays_embeddings = []
+ for i in range(cam_poses.shape[0]):
+ rays_o, rays_d = get_rays(cam_poses[i], self.opt.input_size, self.opt.input_size, self.opt.fovy) # [h, w, 3]
+ rays_plucker = torch.cat([torch.cross(rays_o, rays_d, dim=-1), rays_d], dim=-1) # [h, w, 6]
+ rays_embeddings.append(rays_plucker)
+
+ ## visualize rays for plotting figure
+ # kiui.vis.plot_image(rays_d * 0.5 + 0.5, save=True)
+
+ rays_embeddings = torch.stack(rays_embeddings, dim=0).permute(0, 3, 1, 2).contiguous().to(device) # [V, 6, h, w]
+
+ return rays_embeddings
+
+
+ def forward_gaussians(self, images):
+ # images: [B, 4, 9, H, W]
+ # return: Gaussians: [B, dim_t]
+
+ B, V, C, H, W = images.shape
+ images = images.view(B*V, C, H, W)
+
+ x = self.unet(images) # [B*4, 14, h, w]
+ x = self.conv(x) # [B*4, 14, h, w]
+
+ x = x.reshape(B, 4, 14, self.opt.splat_size, self.opt.splat_size)
+
+ ## visualize multi-view gaussian features for plotting figure
+ # tmp_alpha = self.opacity_act(x[0, :, 3:4])
+ # tmp_img_rgb = self.rgb_act(x[0, :, 11:]) * tmp_alpha + (1 - tmp_alpha)
+ # tmp_img_pos = self.pos_act(x[0, :, 0:3]) * 0.5 + 0.5
+ # kiui.vis.plot_image(tmp_img_rgb, save=True)
+ # kiui.vis.plot_image(tmp_img_pos, save=True)
+
+ x = x.permute(0, 1, 3, 4, 2).reshape(B, -1, 14)
+
+ pos = self.pos_act(x[..., 0:3]) # [B, N, 3]
+ opacity = self.opacity_act(x[..., 3:4])
+ scale = self.scale_act(x[..., 4:7])
+ rotation = self.rot_act(x[..., 7:11])
+ rgbs = self.rgb_act(x[..., 11:])
+
+ gaussians = torch.cat([pos, opacity, scale, rotation, rgbs], dim=-1) # [B, N, 14]
+
+ return gaussians
+
+
+ def forward(self, data, step_ratio=1):
+ # data: output of the dataloader
+ # return: loss
+
+ results = {}
+ loss = 0
+
+ images = data['input'] # [B, 4, 9, h, W], input features
+
+ # use the first view to predict gaussians
+ gaussians = self.forward_gaussians(images) # [B, N, 14]
+
+ results['gaussians'] = gaussians
+
+ # random bg for training
+ if self.training:
+ bg_color = torch.rand(3, dtype=torch.float32, device=gaussians.device)
+ else:
+ bg_color = torch.ones(3, dtype=torch.float32, device=gaussians.device)
+
+ # use the other views for rendering and supervision
+ results = self.gs.render(gaussians, data['cam_view'], data['cam_view_proj'], data['cam_pos'], bg_color=bg_color)
+ pred_images = results['image'] # [B, V, C, output_size, output_size]
+ pred_alphas = results['alpha'] # [B, V, 1, output_size, output_size]
+
+ results['images_pred'] = pred_images
+ results['alphas_pred'] = pred_alphas
+
+ gt_images = data['images_output'] # [B, V, 3, output_size, output_size], ground-truth novel views
+ gt_masks = data['masks_output'] # [B, V, 1, output_size, output_size], ground-truth masks
+
+ gt_images = gt_images * gt_masks + bg_color.view(1, 1, 3, 1, 1) * (1 - gt_masks)
+
+ loss_mse = F.mse_loss(pred_images, gt_images) + F.mse_loss(pred_alphas, gt_masks)
+ loss = loss + loss_mse
+
+ if self.opt.lambda_lpips > 0:
+ loss_lpips = self.lpips_loss(
+ # gt_images.view(-1, 3, self.opt.output_size, self.opt.output_size) * 2 - 1,
+ # pred_images.view(-1, 3, self.opt.output_size, self.opt.output_size) * 2 - 1,
+ # downsampled to at most 256 to reduce memory cost
+ F.interpolate(gt_images.view(-1, 3, self.opt.output_size, self.opt.output_size) * 2 - 1, (256, 256), mode='bilinear', align_corners=False),
+ F.interpolate(pred_images.view(-1, 3, self.opt.output_size, self.opt.output_size) * 2 - 1, (256, 256), mode='bilinear', align_corners=False),
+ ).mean()
+ results['loss_lpips'] = loss_lpips
+ loss = loss + self.opt.lambda_lpips * loss_lpips
+
+ results['loss'] = loss
+
+ # metric
+ with torch.no_grad():
+ psnr = -10 * torch.log10(torch.mean((pred_images.detach() - gt_images) ** 2))
+ results['psnr'] = psnr
+
+ return results
\ No newline at end of file
diff --git a/algorithms/main_3DGS.py b/algorithms/main_3DGS.py
index 4c28f9e..a0604d5 100644
--- a/algorithms/main_3DGS.py
+++ b/algorithms/main_3DGS.py
@@ -4,12 +4,13 @@ import tqdm
import torch
import torch.nn.functional as F
import numpy as np
+from kiui.cam import orbit_camera
from pytorch_msssim import SSIM, MS_SSIM
import comfy.utils
from .main_3DGS_renderer import GaussianSplattingRenderer
-from ..shared_utils.camera_utils import orbit_camera, OrbitCamera, MiniCam, calculate_fovX, get_projection_matrix
+from ..shared_utils.camera_utils import OrbitCamera, MiniCam, calculate_fovX, get_projection_matrix
from ..shared_utils.image_utils import prepare_torch_img
class GSParams:
@@ -153,8 +154,8 @@ class GaussianSplatting:
ref_imgs_torch_list = []
ref_masks_torch_list = []
for i in range(self.ref_imgs_num):
- ref_imgs_torch_list.append(prepare_torch_img(reference_images[i], self.ref_size_H, self.ref_size_W, self.device))
- ref_masks_torch_list.append(prepare_torch_img(reference_masks[i].unsqueeze(2), self.ref_size_H, self.ref_size_W, self.device))
+ ref_imgs_torch_list.append(prepare_torch_img(reference_images[i].unsqueeze(0), self.ref_size_H, self.ref_size_W, self.device))
+ ref_masks_torch_list.append(prepare_torch_img(reference_masks[i].unsqueeze(2).unsqueeze(0), self.ref_size_H, self.ref_size_W, self.device))
self.ref_imgs_torch = torch.cat(ref_imgs_torch_list, dim=0)
self.ref_masks_torch = torch.cat(ref_masks_torch_list, dim=0)
diff --git a/algorithms/nerf_marching_cubes_converter.py b/algorithms/nerf_marching_cubes_converter.py
new file mode 100644
index 0000000..a402c06
--- /dev/null
+++ b/algorithms/nerf_marching_cubes_converter.py
@@ -0,0 +1,416 @@
+import tqdm
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+import mcubes
+import nerfacc
+import nvdiffrast.torch as dr
+
+from kiui.mesh_utils import clean_mesh, decimate_mesh
+from kiui.mesh_utils import laplacian_smooth_loss, normal_consistency
+from kiui.op import uv_padding, inverse_sigmoid
+from kiui.cam import orbit_camera, get_perspective
+from kiui.nn import MLP, trunc_exp
+
+from ..mesh_processer.mesh import Mesh
+from ..lgm.core.options import Options
+from ..lgm.core.gs import GaussianRenderer
+from ..lgm.core.utils import get_rays
+
+# Triple renderer of gaussians, gaussian, and diso mesh.
+# gaussian --> nerf --> mesh
+class GSConverterNeRFMarchingCubes(nn.Module):
+ def __init__(self, opt: Options, gs_ply):
+ super().__init__()
+ from kiui.gridencoder import GridEncoder
+
+ self.opt = opt
+ self.device = torch.device("cuda")
+
+ # gs renderer
+ self.tan_half_fov = np.tan(0.5 * np.deg2rad(opt.fovy))
+ self.proj_matrix = torch.zeros(4, 4, dtype=torch.float32, device=self.device)
+ self.proj_matrix[0, 0] = 1 / self.tan_half_fov
+ self.proj_matrix[1, 1] = 1 / self.tan_half_fov
+ self.proj_matrix[2, 2] = (opt.zfar + opt.znear) / (opt.zfar - opt.znear)
+ self.proj_matrix[3, 2] = - (opt.zfar * opt.znear) / (opt.zfar - opt.znear)
+ self.proj_matrix[2, 3] = 1
+
+ self.gs_renderer = GaussianRenderer(opt)
+
+ self.gaussians = self.gs_renderer.create_from_ply(gs_ply).to(self.device)
+
+ # nerf renderer
+ if not self.opt.force_cuda_rast:
+ self.glctx = dr.RasterizeGLContext()
+ else:
+ self.glctx = dr.RasterizeCudaContext()
+
+ self.step = 0
+ self.render_step_size = 5e-3
+ self.aabb = torch.tensor([-1.0, -1.0, -1.0, 1.0, 1.0, 1.0], device=self.device)
+ self.estimator = nerfacc.OccGridEstimator(roi_aabb=self.aabb, resolution=64, levels=1)
+
+ self.encoder_density = GridEncoder(num_levels=12) # VMEncoder(output_dim=16, mode='sum')
+ self.encoder = GridEncoder(num_levels=12)
+ self.mlp_density = MLP(self.encoder_density.output_dim, 1, 32, 2, bias=False)
+ self.mlp = MLP(self.encoder.output_dim, 3, 32, 2, bias=False)
+
+ # mesh renderer
+ self.proj = torch.from_numpy(get_perspective(self.opt.fovy)).float().to(self.device)
+ self.v = self.f = None
+ self.vt = self.ft = None
+ self.deform = None
+ self.albedo = None
+
+
+ @torch.no_grad()
+ def render_gs(self, pose):
+
+ cam_poses = torch.from_numpy(pose).unsqueeze(0).to(self.device)
+ cam_poses[:, :3, 1:3] *= -1 # invert up & forward direction
+
+ # cameras needed by gaussian rasterizer
+ cam_view = torch.inverse(cam_poses).transpose(1, 2) # [V, 4, 4]
+ cam_view_proj = cam_view @ self.proj_matrix # [V, 4, 4]
+ cam_pos = - cam_poses[:, :3, 3] # [V, 3]
+
+ out = self.gs_renderer.render(self.gaussians.unsqueeze(0), cam_view.unsqueeze(0), cam_view_proj.unsqueeze(0), cam_pos.unsqueeze(0))
+ image = out['image'].squeeze(1).squeeze(0) # [C, H, W]
+ alpha = out['alpha'].squeeze(2).squeeze(1).squeeze(0) # [H, W]
+
+ return image, alpha
+
+ def get_density(self, xs):
+ # xs: [..., 3]
+ prefix = xs.shape[:-1]
+ xs = xs.view(-1, 3)
+ feats = self.encoder_density(xs)
+ density = trunc_exp(self.mlp_density(feats))
+ density = density.view(*prefix, 1)
+ return density
+
+ def render_nerf(self, pose):
+
+ pose = torch.from_numpy(pose.astype(np.float32)).to(self.device)
+
+ # get rays
+ resolution = self.opt.output_size
+ rays_o, rays_d = get_rays(pose, resolution, resolution, self.opt.fovy)
+ hw = rays_o.shape[0] * rays_o.shape[1]
+ rays_o = rays_o.view(hw, 3)
+ rays_d = rays_d.view(hw, 3)
+
+ # update occ grid
+ if self.training:
+ def occ_eval_fn(xs):
+ sigmas = self.get_density(xs)
+ return self.render_step_size * sigmas
+
+ self.estimator.update_every_n_steps(self.step, occ_eval_fn=occ_eval_fn, occ_thre=0.01, n=8)
+ self.step += 1
+
+ # render
+ def sigma_fn(t_starts, t_ends, ray_indices):
+ t_origins = rays_o[ray_indices]
+ t_dirs = rays_d[ray_indices]
+ xs = t_origins + t_dirs * (t_starts + t_ends)[:, None] / 2.0
+ sigmas = self.get_density(xs)
+ return sigmas.squeeze(-1)
+
+ with torch.no_grad():
+ ray_indices, t_starts, t_ends = self.estimator.sampling(
+ rays_o,
+ rays_d,
+ sigma_fn=sigma_fn,
+ near_plane=0.01,
+ far_plane=100,
+ render_step_size=self.render_step_size,
+ stratified=self.training,
+ cone_angle=0,
+ )
+
+ t_origins = rays_o[ray_indices]
+ t_dirs = rays_d[ray_indices]
+ xs = t_origins + t_dirs * (t_starts + t_ends)[:, None] / 2.0
+ sigmas = self.get_density(xs).squeeze(-1)
+ rgbs = torch.sigmoid(self.mlp(self.encoder(xs)))
+
+ n_rays=rays_o.shape[0]
+ weights, trans, alphas = nerfacc.render_weight_from_density(t_starts, t_ends, sigmas, ray_indices=ray_indices, n_rays=n_rays)
+ color = nerfacc.accumulate_along_rays(weights, values=rgbs, ray_indices=ray_indices, n_rays=n_rays)
+ alpha = nerfacc.accumulate_along_rays(weights, values=None, ray_indices=ray_indices, n_rays=n_rays)
+
+ color = color + 1 * (1.0 - alpha)
+
+ color = color.view(resolution, resolution, 3).clamp(0, 1).permute(2, 0, 1).contiguous()
+ alpha = alpha.view(resolution, resolution).clamp(0, 1).contiguous()
+
+ return color, alpha
+
+ def fit_nerf(self, iters=512, resolution=128):
+
+ self.opt.output_size = resolution
+
+ optimizer = torch.optim.Adam([
+ {'params': self.encoder_density.parameters(), 'lr': 1e-2},
+ {'params': self.encoder.parameters(), 'lr': 1e-2},
+ {'params': self.mlp_density.parameters(), 'lr': 1e-3},
+ {'params': self.mlp.parameters(), 'lr': 1e-3},
+ ])
+
+ print(f"[INFO] fitting nerf...")
+ pbar = tqdm.trange(iters)
+ for i in pbar:
+
+ ver = np.random.randint(-45, 45)
+ hor = np.random.randint(-180, 180)
+ rad = np.random.uniform(1.5, 3.0)
+
+ pose = orbit_camera(ver, hor, rad)
+
+ image_gt, alpha_gt = self.render_gs(pose)
+ image_pred, alpha_pred = self.render_nerf(pose)
+
+ # if i % 200 == 0:
+ # kiui.vis.plot_image(image_gt, alpha_gt, image_pred, alpha_pred)
+
+ loss_mse = F.mse_loss(image_pred, image_gt) + 0.1 * F.mse_loss(alpha_pred, alpha_gt)
+ loss = loss_mse #+ 0.1 * self.encoder_density.tv_loss() #+ 0.0001 * self.encoder_density.density_loss()
+
+ loss.backward()
+ self.encoder_density.grad_total_variation(1e-8)
+
+ optimizer.step()
+ optimizer.zero_grad()
+
+ pbar.set_description(f"MSE = {loss_mse.item():.6f}")
+
+ print(f"[INFO] finished fitting nerf!")
+
+ def render_mesh(self, pose):
+
+ h = w = self.opt.output_size
+
+ v = self.v + self.deform
+ f = self.f
+
+ pose = torch.from_numpy(pose.astype(np.float32)).to(v.device)
+
+ # get v_clip and render rgb
+ v_cam = torch.matmul(F.pad(v, pad=(0, 1), mode='constant', value=1.0), torch.inverse(pose).T).float().unsqueeze(0)
+ v_clip = v_cam @ self.proj.T
+
+ rast, rast_db = dr.rasterize(self.glctx, v_clip, f, (h, w))
+
+ alpha = torch.clamp(rast[..., -1:], 0, 1).contiguous() # [1, H, W, 1]
+ alpha = dr.antialias(alpha, rast, v_clip, f).clamp(0, 1).squeeze(-1).squeeze(0) # [H, W] important to enable gradients!
+
+ if self.albedo is None:
+ xyzs, _ = dr.interpolate(v.unsqueeze(0), rast, f) # [1, H, W, 3]
+ xyzs = xyzs.view(-1, 3)
+ mask = (alpha > 0).view(-1)
+ image = torch.zeros_like(xyzs, dtype=torch.float32)
+ if mask.any():
+ masked_albedo = torch.sigmoid(self.mlp(self.encoder(xyzs[mask].detach(), bound=1)))
+ image[mask] = masked_albedo.float()
+ else:
+ texc, texc_db = dr.interpolate(self.vt.unsqueeze(0), rast, self.ft, rast_db=rast_db, diff_attrs='all')
+ image = torch.sigmoid(dr.texture(self.albedo.unsqueeze(0), texc, uv_da=texc_db)) # [1, H, W, 3]
+
+ image = image.view(1, h, w, 3)
+ # image = dr.antialias(image, rast, v_clip, f).clamp(0, 1)
+ image = image.squeeze(0).permute(2, 0, 1).contiguous() # [3, H, W]
+ image = alpha * image + (1 - alpha)
+
+ return image, alpha
+
+ def fit_mesh(self, iters=2048, resolution=512, decimate_target=5e4):
+
+ self.opt.output_size = resolution
+
+ # init mesh from nerf
+ grid_size = 256
+ sigmas = np.zeros([grid_size, grid_size, grid_size], dtype=np.float32)
+
+ S = 128
+ density_thresh = 10
+
+ X = torch.linspace(-1, 1, grid_size).split(S)
+ Y = torch.linspace(-1, 1, grid_size).split(S)
+ Z = torch.linspace(-1, 1, grid_size).split(S)
+
+ for xi, xs in enumerate(X):
+ for yi, ys in enumerate(Y):
+ for zi, zs in enumerate(Z):
+ xx, yy, zz = torch.meshgrid(xs, ys, zs, indexing='ij')
+ pts = torch.cat([xx.reshape(-1, 1), yy.reshape(-1, 1), zz.reshape(-1, 1)], dim=-1) # [S, 3]
+ val = self.get_density(pts.to(self.device))
+ sigmas[xi * S: xi * S + len(xs), yi * S: yi * S + len(ys), zi * S: zi * S + len(zs)] = val.reshape(len(xs), len(ys), len(zs)).detach().cpu().numpy() # [S, 1] --> [x, y, z]
+
+ print(f'[INFO] marching cubes thresh: {density_thresh} ({sigmas.min()} ~ {sigmas.max()})')
+
+ vertices, triangles = mcubes.marching_cubes(sigmas, density_thresh)
+ vertices = vertices / (grid_size - 1.0) * 2 - 1
+
+ # clean
+ vertices = vertices.astype(np.float32)
+ triangles = triangles.astype(np.int32)
+ vertices, triangles = clean_mesh(vertices, triangles, remesh=True, remesh_size=0.01)
+ if triangles.shape[0] > decimate_target:
+ vertices, triangles = decimate_mesh(vertices, triangles, decimate_target, optimalplacement=False)
+
+ self.v = torch.from_numpy(vertices).contiguous().float().to(self.device)
+ self.f = torch.from_numpy(triangles).contiguous().int().to(self.device)
+ self.deform = nn.Parameter(torch.zeros_like(self.v)).to(self.device)
+
+ # fit mesh from gs
+ lr_factor = 1
+ optimizer = torch.optim.Adam([
+ {'params': self.encoder.parameters(), 'lr': 1e-3 * lr_factor},
+ {'params': self.mlp.parameters(), 'lr': 1e-3 * lr_factor},
+ {'params': self.deform, 'lr': 1e-4},
+ ])
+
+ print(f"[INFO] fitting mesh...")
+ pbar = tqdm.trange(iters)
+ for i in pbar:
+
+ ver = np.random.randint(-10, 10)
+ hor = np.random.randint(-180, 180)
+ rad = self.opt.cam_radius # np.random.uniform(1, 2)
+
+ pose = orbit_camera(ver, hor, rad)
+
+ image_gt, alpha_gt = self.render_gs(pose)
+ image_pred, alpha_pred = self.render_mesh(pose)
+
+ loss_mse = F.mse_loss(image_pred, image_gt) + 0.1 * F.mse_loss(alpha_pred, alpha_gt)
+ # loss_lap = laplacian_smooth_loss(self.v + self.deform, self.f)
+ loss_normal = normal_consistency(self.v + self.deform, self.f)
+ loss_offsets = (self.deform ** 2).sum(-1).mean()
+ loss = loss_mse + 0.001 * loss_normal + 0.1 * loss_offsets
+
+ loss.backward()
+
+ optimizer.step()
+ optimizer.zero_grad()
+
+ # remesh periodically
+ if i > 0 and i % 512 == 0:
+ vertices = (self.v + self.deform).detach().cpu().numpy()
+ triangles = self.f.detach().cpu().numpy()
+ vertices, triangles = clean_mesh(vertices, triangles, remesh=True, remesh_size=0.01)
+ if triangles.shape[0] > decimate_target:
+ vertices, triangles = decimate_mesh(vertices, triangles, decimate_target, optimalplacement=False)
+ self.v = torch.from_numpy(vertices).contiguous().float().to(self.device)
+ self.f = torch.from_numpy(triangles).contiguous().int().to(self.device)
+ self.deform = nn.Parameter(torch.zeros_like(self.v)).to(self.device)
+ lr_factor *= 0.5
+ optimizer = torch.optim.Adam([
+ {'params': self.encoder.parameters(), 'lr': 1e-3 * lr_factor},
+ {'params': self.mlp.parameters(), 'lr': 1e-3 * lr_factor},
+ {'params': self.deform, 'lr': 1e-4},
+ ])
+
+ pbar.set_description(f"MSE = {loss_mse.item():.6f}")
+
+ # last clean
+ vertices = (self.v + self.deform).detach().cpu().numpy()
+ triangles = self.f.detach().cpu().numpy()
+ vertices, triangles = clean_mesh(vertices, triangles, remesh=False)
+ self.v = torch.from_numpy(vertices).contiguous().float().to(self.device)
+ self.f = torch.from_numpy(triangles).contiguous().int().to(self.device)
+ self.deform = nn.Parameter(torch.zeros_like(self.v).to(self.device))
+
+ print(f"[INFO] finished fitting mesh!")
+
+ # uv mesh refine
+ def fit_mesh_uv(self, iters=512, resolution=512, texture_resolution=1024, padding=2):
+
+ self.opt.output_size = resolution
+
+ # unwrap uv
+ print(f"[INFO] uv unwrapping...")
+ mesh = Mesh(v=self.v, f=self.f, albedo=None, device=self.device)
+ mesh.auto_normal()
+ mesh.auto_uv()
+
+ self.vt = mesh.vt
+ self.ft = mesh.ft
+
+ # render uv maps
+ h = w = texture_resolution
+ uv = mesh.vt * 2.0 - 1.0 # uvs to range [-1, 1]
+ uv = torch.cat((uv, torch.zeros_like(uv[..., :1]), torch.ones_like(uv[..., :1])), dim=-1) # [N, 4]
+
+ rast, _ = dr.rasterize(self.glctx, uv.unsqueeze(0), mesh.ft, (h, w)) # [1, h, w, 4]
+ xyzs, _ = dr.interpolate(mesh.v.unsqueeze(0), rast, mesh.f) # [1, h, w, 3]
+ mask, _ = dr.interpolate(torch.ones_like(mesh.v[:, :1]).unsqueeze(0), rast, mesh.f) # [1, h, w, 1]
+
+ # masked query
+ xyzs = xyzs.view(-1, 3)
+ mask = (mask > 0).view(-1)
+
+ albedo = torch.zeros(h * w, 3, device=self.device, dtype=torch.float32)
+
+ if mask.any():
+ print(f"[INFO] querying texture...")
+
+ xyzs = xyzs[mask] # [M, 3]
+
+ # batched inference to avoid OOM
+ batch = []
+ head = 0
+ while head < xyzs.shape[0]:
+ tail = min(head + 640000, xyzs.shape[0])
+ batch.append(torch.sigmoid(self.mlp(self.encoder(xyzs[head:tail]))).float())
+ head += 640000
+
+ albedo[mask] = torch.cat(batch, dim=0)
+
+ albedo = albedo.view(h, w, -1)
+ mask = mask.view(h, w)
+ albedo = uv_padding(albedo, mask, padding)
+
+ # optimize texture
+ self.albedo = nn.Parameter(inverse_sigmoid(albedo)).to(self.device)
+
+ optimizer = torch.optim.Adam([
+ {'params': self.albedo, 'lr': 1e-3},
+ ])
+
+ print(f"[INFO] fitting mesh texture...")
+ pbar = tqdm.trange(iters)
+ for i in pbar:
+
+ # shrink to front view as we care more about it...
+ ver = np.random.randint(-5, 5)
+ hor = np.random.randint(-15, 15)
+ rad = self.opt.cam_radius # np.random.uniform(1, 2)
+
+ pose = orbit_camera(ver, hor, rad)
+
+ image_gt, alpha_gt = self.render_gs(pose)
+ image_pred, alpha_pred = self.render_mesh(pose)
+
+ loss_mse = F.mse_loss(image_pred, image_gt)
+ loss = loss_mse
+
+ loss.backward()
+
+ optimizer.step()
+ optimizer.zero_grad()
+
+ pbar.set_description(f"MSE = {loss_mse.item():.6f}")
+
+ print(f"[INFO] finished fitting mesh texture!")
+
+
+ @torch.no_grad()
+ def get_mesh(self):
+ mesh = Mesh(v=self.v, f=self.f, vt=self.vt, ft=self.ft, albedo=torch.sigmoid(self.albedo), device=self.device)
+ mesh.auto_normal()
+ return mesh
diff --git a/install.bat b/install.bat
index 96ddaab..6521da2 100644
--- a/install.bat
+++ b/install.bat
@@ -10,15 +10,6 @@ if exist "%python_miniconda_exec%" (
echo Installing with Miniconda Environment
"%python_miniconda_exec%" -s -m pip install -r "%requirements_txt%"
- git clone --recursive https://github.com/ashawkey/diff-gaussian-rasterization
- "%python_miniconda_exec%" -s -m pip install ./diff-gaussian-rasterization
- "%python_miniconda_exec%" -s -m pip install ./simple-knn
- git clone --recursive https://github.com/NVlabs/nvdiffrast
- "%python_miniconda_exec%" -s -m pip install ./nvdiffrast
- "%python_miniconda_exec%" -s -m pip install git+https://github.com/facebookresearch/pytorch3d.git@stable
-
- "%python_miniconda_exec%" -s -m pip install git+https://github.com/rusty1s/pytorch_scatter.git
-
set "python_miniconda_exec=..\..\..\..\..\..\..\python_miniconda_env\ComfyUI\python.exe"
cd tgs/models/snowflake/pointnet2_ops_lib && "%python_miniconda_exec%" -s setup.py install && cd ../../../../
diff --git a/lgm/acc_configs/gpu1.yaml b/lgm/acc_configs/gpu1.yaml
new file mode 100644
index 0000000..bbbabf2
--- /dev/null
+++ b/lgm/acc_configs/gpu1.yaml
@@ -0,0 +1,15 @@
+compute_environment: LOCAL_MACHINE
+debug: false
+distributed_type: 'NO'
+downcast_bf16: 'no'
+machine_rank: 0
+main_training_function: main
+mixed_precision: bf16
+num_machines: 1
+num_processes: 1
+rdzv_backend: static
+same_network: true
+tpu_env: []
+tpu_use_cluster: false
+tpu_use_sudo: false
+use_cpu: false
diff --git a/lgm/acc_configs/gpu4.yaml b/lgm/acc_configs/gpu4.yaml
new file mode 100644
index 0000000..6f4c538
--- /dev/null
+++ b/lgm/acc_configs/gpu4.yaml
@@ -0,0 +1,15 @@
+compute_environment: LOCAL_MACHINE
+debug: false
+distributed_type: MULTI_GPU
+downcast_bf16: 'no'
+machine_rank: 0
+main_training_function: main
+mixed_precision: fp16
+num_machines: 1
+num_processes: 4
+rdzv_backend: static
+same_network: true
+tpu_env: []
+tpu_use_cluster: false
+tpu_use_sudo: false
+use_cpu: false
diff --git a/lgm/acc_configs/gpu6.yaml b/lgm/acc_configs/gpu6.yaml
new file mode 100644
index 0000000..32ef41c
--- /dev/null
+++ b/lgm/acc_configs/gpu6.yaml
@@ -0,0 +1,15 @@
+compute_environment: LOCAL_MACHINE
+debug: false
+distributed_type: MULTI_GPU
+downcast_bf16: 'no'
+machine_rank: 0
+main_training_function: main
+mixed_precision: fp16
+num_machines: 1
+num_processes: 6
+rdzv_backend: static
+same_network: true
+tpu_env: []
+tpu_use_cluster: false
+tpu_use_sudo: false
+use_cpu: false
diff --git a/lgm/acc_configs/gpu8.yaml b/lgm/acc_configs/gpu8.yaml
new file mode 100644
index 0000000..46b5c35
--- /dev/null
+++ b/lgm/acc_configs/gpu8.yaml
@@ -0,0 +1,15 @@
+compute_environment: LOCAL_MACHINE
+debug: false
+distributed_type: MULTI_GPU
+downcast_bf16: 'no'
+machine_rank: 0
+main_training_function: main
+mixed_precision: bf16
+num_machines: 1
+num_processes: 8
+rdzv_backend: static
+same_network: true
+tpu_env: []
+tpu_use_cluster: false
+tpu_use_sudo: false
+use_cpu: false
diff --git a/lgm/core/__init__.py b/lgm/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lgm/core/attention.py b/lgm/core/attention.py
new file mode 100644
index 0000000..ea1382f
--- /dev/null
+++ b/lgm/core/attention.py
@@ -0,0 +1,156 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the Apache License, Version 2.0
+# found in the LICENSE file in the root directory of this source tree.
+
+# References:
+# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
+# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py
+
+import os
+import warnings
+
+from torch import Tensor
+from torch import nn
+
+XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
+try:
+ if XFORMERS_ENABLED:
+ from xformers.ops import memory_efficient_attention, unbind
+
+ XFORMERS_AVAILABLE = True
+ warnings.warn("xFormers is available (Attention)")
+ else:
+ warnings.warn("xFormers is disabled (Attention)")
+ raise ImportError
+except ImportError:
+ XFORMERS_AVAILABLE = False
+ warnings.warn("xFormers is not available (Attention)")
+
+
+class Attention(nn.Module):
+ def __init__(
+ self,
+ dim: int,
+ num_heads: int = 8,
+ qkv_bias: bool = False,
+ proj_bias: bool = True,
+ attn_drop: float = 0.0,
+ proj_drop: float = 0.0,
+ ) -> None:
+ super().__init__()
+ self.num_heads = num_heads
+ head_dim = dim // num_heads
+ self.scale = head_dim**-0.5
+
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
+ self.attn_drop = nn.Dropout(attn_drop)
+ self.proj = nn.Linear(dim, dim, bias=proj_bias)
+ self.proj_drop = nn.Dropout(proj_drop)
+
+ def forward(self, x: Tensor) -> Tensor:
+ B, N, C = x.shape
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
+
+ q, k, v = qkv[0] * self.scale, qkv[1], qkv[2]
+ attn = q @ k.transpose(-2, -1)
+
+ attn = attn.softmax(dim=-1)
+ attn = self.attn_drop(attn)
+
+ x = (attn @ v).transpose(1, 2).reshape(B, N, C)
+ x = self.proj(x)
+ x = self.proj_drop(x)
+ return x
+
+
+class MemEffAttention(Attention):
+ def forward(self, x: Tensor, attn_bias=None) -> Tensor:
+ if not XFORMERS_AVAILABLE:
+ if attn_bias is not None:
+ raise AssertionError("xFormers is required for using nested tensors")
+ return super().forward(x)
+
+ B, N, C = x.shape
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
+
+ q, k, v = unbind(qkv, 2)
+
+ x = memory_efficient_attention(q, k, v, attn_bias=attn_bias)
+ x = x.reshape([B, N, C])
+
+ x = self.proj(x)
+ x = self.proj_drop(x)
+ return x
+
+
+class CrossAttention(nn.Module):
+ def __init__(
+ self,
+ dim: int,
+ dim_q: int,
+ dim_k: int,
+ dim_v: int,
+ num_heads: int = 8,
+ qkv_bias: bool = False,
+ proj_bias: bool = True,
+ attn_drop: float = 0.0,
+ proj_drop: float = 0.0,
+ ) -> None:
+ super().__init__()
+ self.dim = dim
+ self.num_heads = num_heads
+ head_dim = dim // num_heads
+ self.scale = head_dim**-0.5
+
+ self.to_q = nn.Linear(dim_q, dim, bias=qkv_bias)
+ self.to_k = nn.Linear(dim_k, dim, bias=qkv_bias)
+ self.to_v = nn.Linear(dim_v, dim, bias=qkv_bias)
+ self.attn_drop = nn.Dropout(attn_drop)
+ self.proj = nn.Linear(dim, dim, bias=proj_bias)
+ self.proj_drop = nn.Dropout(proj_drop)
+
+ def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor:
+ # q: [B, N, Cq]
+ # k: [B, M, Ck]
+ # v: [B, M, Cv]
+ # return: [B, N, C]
+
+ B, N, _ = q.shape
+ M = k.shape[1]
+
+ q = self.scale * self.to_q(q).reshape(B, N, self.num_heads, self.dim // self.num_heads).permute(0, 2, 1, 3) # [B, nh, N, C/nh]
+ k = self.to_k(k).reshape(B, M, self.num_heads, self.dim // self.num_heads).permute(0, 2, 1, 3) # [B, nh, M, C/nh]
+ v = self.to_v(v).reshape(B, M, self.num_heads, self.dim // self.num_heads).permute(0, 2, 1, 3) # [B, nh, M, C/nh]
+
+ attn = q @ k.transpose(-2, -1) # [B, nh, N, M]
+
+ attn = attn.softmax(dim=-1) # [B, nh, N, M]
+ attn = self.attn_drop(attn)
+
+ x = (attn @ v).transpose(1, 2).reshape(B, N, -1) # [B, nh, N, M] @ [B, nh, M, C/nh] --> [B, nh, N, C/nh] --> [B, N, nh, C/nh] --> [B, N, C]
+ x = self.proj(x)
+ x = self.proj_drop(x)
+ return x
+
+
+class MemEffCrossAttention(CrossAttention):
+ def forward(self, q: Tensor, k: Tensor, v: Tensor, attn_bias=None) -> Tensor:
+ if not XFORMERS_AVAILABLE:
+ if attn_bias is not None:
+ raise AssertionError("xFormers is required for using nested tensors")
+ return super().forward(x)
+
+ B, N, _ = q.shape
+ M = k.shape[1]
+
+ q = self.scale * self.to_q(q).reshape(B, N, self.num_heads, self.dim // self.num_heads) # [B, N, nh, C/nh]
+ k = self.to_k(k).reshape(B, M, self.num_heads, self.dim // self.num_heads) # [B, M, nh, C/nh]
+ v = self.to_v(v).reshape(B, M, self.num_heads, self.dim // self.num_heads) # [B, M, nh, C/nh]
+
+ x = memory_efficient_attention(q, k, v, attn_bias=attn_bias)
+ x = x.reshape(B, N, -1)
+
+ x = self.proj(x)
+ x = self.proj_drop(x)
+ return x
diff --git a/lgm/core/gs.py b/lgm/core/gs.py
new file mode 100644
index 0000000..f84274d
--- /dev/null
+++ b/lgm/core/gs.py
@@ -0,0 +1,186 @@
+import numpy as np
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+from diff_gaussian_rasterization import (
+ GaussianRasterizationSettings,
+ GaussianRasterizer,
+)
+
+from lgm.core.options import Options
+
+import kiui
+
+class GaussianRenderer:
+ def __init__(self, opt: Options):
+
+ self.opt = opt
+ self.bg_color = torch.tensor([1, 1, 1], dtype=torch.float32, device="cuda")
+
+ # intrinsics
+ self.tan_half_fov = np.tan(0.5 * np.deg2rad(self.opt.fovy))
+ self.proj_matrix = torch.zeros(4, 4, dtype=torch.float32)
+ self.proj_matrix[0, 0] = 1 / self.tan_half_fov
+ self.proj_matrix[1, 1] = 1 / self.tan_half_fov
+ self.proj_matrix[2, 2] = (opt.zfar + opt.znear) / (opt.zfar - opt.znear)
+ self.proj_matrix[3, 2] = - (opt.zfar * opt.znear) / (opt.zfar - opt.znear)
+ self.proj_matrix[2, 3] = 1
+
+ def render(self, gaussians, cam_view, cam_view_proj, cam_pos, bg_color=None, scale_modifier=1):
+ # gaussians: [B, N, 14]
+ # cam_view, cam_view_proj: [B, V, 4, 4]
+ # cam_pos: [B, V, 3]
+
+ device = gaussians.device
+ B, V = cam_view.shape[:2]
+
+ # loop of loop...
+ images = []
+ alphas = []
+ for b in range(B):
+
+ # pos, opacity, scale, rotation, shs
+ means3D = gaussians[b, :, 0:3].contiguous().float()
+ opacity = gaussians[b, :, 3:4].contiguous().float()
+ scales = gaussians[b, :, 4:7].contiguous().float()
+ rotations = gaussians[b, :, 7:11].contiguous().float()
+ rgbs = gaussians[b, :, 11:].contiguous().float() # [N, 3]
+
+ for v in range(V):
+
+ # render novel views
+ view_matrix = cam_view[b, v].float()
+ view_proj_matrix = cam_view_proj[b, v].float()
+ campos = cam_pos[b, v].float()
+
+ raster_settings = GaussianRasterizationSettings(
+ image_height=self.opt.output_size,
+ image_width=self.opt.output_size,
+ tanfovx=self.tan_half_fov,
+ tanfovy=self.tan_half_fov,
+ bg=self.bg_color if bg_color is None else bg_color,
+ scale_modifier=scale_modifier,
+ viewmatrix=view_matrix,
+ projmatrix=view_proj_matrix,
+ sh_degree=0,
+ campos=campos,
+ prefiltered=False,
+ debug=False,
+ )
+
+ rasterizer = GaussianRasterizer(raster_settings=raster_settings)
+
+ # Rasterize visible Gaussians to image, obtain their radii (on screen).
+ rendered_image, radii, rendered_depth, rendered_alpha = rasterizer(
+ means3D=means3D,
+ means2D=torch.zeros_like(means3D, dtype=torch.float32, device=device),
+ shs=None,
+ colors_precomp=rgbs,
+ opacities=opacity,
+ scales=scales,
+ rotations=rotations,
+ cov3D_precomp=None,
+ )
+
+ rendered_image = rendered_image.clamp(0, 1)
+
+ images.append(rendered_image)
+ alphas.append(rendered_alpha)
+
+ images = torch.stack(images, dim=0).view(B, V, 3, self.opt.output_size, self.opt.output_size)
+ alphas = torch.stack(alphas, dim=0).view(B, V, 1, self.opt.output_size, self.opt.output_size)
+
+ return {
+ "image": images, # [B, V, 3, H, W]
+ "alpha": alphas, # [B, V, 1, H, W]
+ }
+
+
+ def to_ply(self, gaussians, compatible=True):
+ # gaussians: [B, N, 14]
+ # compatible: save pre-activated gaussians as in the original paper
+
+ assert gaussians.shape[0] == 1, 'only support batch size 1'
+
+ from plyfile import PlyData, PlyElement
+
+ means3D = gaussians[0, :, 0:3].contiguous().float()
+ opacity = gaussians[0, :, 3:4].contiguous().float()
+ scales = gaussians[0, :, 4:7].contiguous().float()
+ rotations = gaussians[0, :, 7:11].contiguous().float()
+ shs = gaussians[0, :, 11:].unsqueeze(1).contiguous().float() # [N, 1, 3]
+
+ # prune by opacity
+ mask = opacity.squeeze(-1) >= 0.005
+ means3D = means3D[mask]
+ opacity = opacity[mask]
+ scales = scales[mask]
+ rotations = rotations[mask]
+ shs = shs[mask]
+
+ # invert activation to make it compatible with the original ply format
+ if compatible:
+ opacity = kiui.op.inverse_sigmoid(opacity)
+ scales = torch.log(scales + 1e-8)
+ shs = (shs - 0.5) / 0.28209479177387814
+
+ xyzs = means3D.detach().cpu().numpy()
+ f_dc = shs.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy()
+ opacities = opacity.detach().cpu().numpy()
+ scales = scales.detach().cpu().numpy()
+ rotations = rotations.detach().cpu().numpy()
+
+ l = ['x', 'y', 'z']
+ # All channels except the 3 DC
+ for i in range(f_dc.shape[1]):
+ l.append('f_dc_{}'.format(i))
+ l.append('opacity')
+ for i in range(scales.shape[1]):
+ l.append('scale_{}'.format(i))
+ for i in range(rotations.shape[1]):
+ l.append('rot_{}'.format(i))
+
+ dtype_full = [(attribute, 'f4') for attribute in l]
+
+ elements = np.empty(xyzs.shape[0], dtype=dtype_full)
+ attributes = np.concatenate((xyzs, f_dc, opacities, scales, rotations), axis=1)
+ elements[:] = list(map(tuple, attributes))
+ el = PlyElement.describe(elements, 'vertex')
+
+ return PlyData([el])
+
+ def create_from_ply(self, plydata, compatible=True):
+
+ xyz = np.stack((np.asarray(plydata.elements[0]["x"]),
+ np.asarray(plydata.elements[0]["y"]),
+ np.asarray(plydata.elements[0]["z"])), axis=1)
+ print("Number of points at loading : ", xyz.shape[0])
+
+ opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis]
+
+ shs = np.zeros((xyz.shape[0], 3))
+ shs[:, 0] = np.asarray(plydata.elements[0]["f_dc_0"])
+ shs[:, 1] = np.asarray(plydata.elements[0]["f_dc_1"])
+ shs[:, 2] = np.asarray(plydata.elements[0]["f_dc_2"])
+
+ scale_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("scale_")]
+ scales = np.zeros((xyz.shape[0], len(scale_names)))
+ for idx, attr_name in enumerate(scale_names):
+ scales[:, idx] = np.asarray(plydata.elements[0][attr_name])
+
+ rot_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("rot_")]
+ rots = np.zeros((xyz.shape[0], len(rot_names)))
+ for idx, attr_name in enumerate(rot_names):
+ rots[:, idx] = np.asarray(plydata.elements[0][attr_name])
+
+ gaussians = np.concatenate([xyz, opacities, scales, rots, shs], axis=1)
+ gaussians = torch.from_numpy(gaussians).float() # cpu
+
+ if compatible:
+ gaussians[..., 3:4] = torch.sigmoid(gaussians[..., 3:4])
+ gaussians[..., 4:7] = torch.exp(gaussians[..., 4:7])
+ gaussians[..., 11:] = 0.28209479177387814 * gaussians[..., 11:] + 0.5
+
+ return gaussians
\ No newline at end of file
diff --git a/lgm/core/options.py b/lgm/core/options.py
new file mode 100644
index 0000000..ba6e8f1
--- /dev/null
+++ b/lgm/core/options.py
@@ -0,0 +1,117 @@
+from dataclasses import dataclass
+from typing import Tuple, Literal, Dict, Optional
+
+
+@dataclass
+class Options:
+ ### model
+ # Unet image input size
+ input_size: int = 256
+ # Unet definition
+ down_channels: Tuple[int, ...] = (64, 128, 256, 512, 1024, 1024)
+ down_attention: Tuple[bool, ...] = (False, False, False, True, True, True)
+ mid_attention: bool = True
+ up_channels: Tuple[int, ...] = (1024, 1024, 512, 256)
+ up_attention: Tuple[bool, ...] = (True, True, True, False)
+ # Unet output size, dependent on the input_size and U-Net structure!
+ splat_size: int = 64
+ # gaussian render size
+ output_size: int = 256
+
+ ### dataset
+ # data mode (only support s3 now)
+ data_mode: Literal['s3'] = 's3'
+ # fovy of the dataset
+ fovy: float = 49.1
+ # camera near plane
+ znear: float = 0.5
+ # camera far plane
+ zfar: float = 2.5
+ # number of all views (input + output)
+ num_views: int = 12
+ # number of views
+ num_input_views: int = 4
+ # camera radius
+ cam_radius: float = 1.5 # to better use [-1, 1]^3 space
+ # num workers
+ num_workers: int = 8
+
+ ### training
+ # workspace
+ workspace: str = './workspace'
+ # resume
+ resume: Optional[str] = None
+ # batch size (per-GPU)
+ batch_size: int = 8
+ # gradient accumulation
+ gradient_accumulation_steps: int = 1
+ # training epochs
+ num_epochs: int = 30
+ # lpips loss weight
+ lambda_lpips: float = 1.0
+ # gradient clip
+ gradient_clip: float = 1.0
+ # mixed precision
+ mixed_precision: str = 'bf16'
+ # learning rate
+ lr: float = 4e-4
+ # augmentation prob for grid distortion
+ prob_grid_distortion: float = 0.5
+ # augmentation prob for camera jitter
+ prob_cam_jitter: float = 0.5
+
+ ### testing
+ # test image path
+ test_path: Optional[str] = None
+
+ ### misc
+ # nvdiffrast backend setting
+ force_cuda_rast: bool = False
+ # render fancy video with gaussian scaling effect
+ fancy_video: bool = False
+
+
+# all the default settings
+config_defaults: Dict[str, Options] = {}
+config_doc: Dict[str, str] = {}
+
+config_doc['default'] = 'the default settings for LGM'
+config_defaults['default'] = Options()
+
+config_doc['small'] = 'small model with lower resolution Gaussians'
+config_defaults['small'] = Options(
+ input_size=256,
+ splat_size=64,
+ output_size=256,
+ batch_size=8,
+ gradient_accumulation_steps=1,
+ mixed_precision='bf16',
+)
+
+config_doc['big'] = 'big model with higher resolution Gaussians'
+config_defaults['big'] = Options(
+ input_size=256,
+ up_channels=(1024, 1024, 512, 256, 128), # one more decoder
+ up_attention=(True, True, True, False, False),
+ splat_size=128,
+ output_size=512, # render & supervise Gaussians at a higher resolution.
+ batch_size=8,
+ num_views=8,
+ gradient_accumulation_steps=1,
+ mixed_precision='bf16',
+)
+
+config_doc['tiny'] = 'tiny model for ablation'
+config_defaults['tiny'] = Options(
+ input_size=256,
+ down_channels=(32, 64, 128, 256, 512),
+ down_attention=(False, False, False, False, True),
+ up_channels=(512, 256, 128),
+ up_attention=(True, False, False, False),
+ splat_size=64,
+ output_size=256,
+ batch_size=16,
+ num_views=8,
+ gradient_accumulation_steps=1,
+ mixed_precision='bf16',
+)
diff --git a/lgm/core/provider_objaverse.py b/lgm/core/provider_objaverse.py
new file mode 100644
index 0000000..d73f646
--- /dev/null
+++ b/lgm/core/provider_objaverse.py
@@ -0,0 +1,172 @@
+import os
+import cv2
+import random
+import numpy as np
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import torchvision.transforms.functional as TF
+from torch.utils.data import Dataset
+
+import kiui
+from lgm.core.options import Options
+from lgm.core.utils import get_rays, grid_distortion, orbit_camera_jitter
+
+IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406)
+IMAGENET_DEFAULT_STD = (0.229, 0.224, 0.225)
+
+
+class ObjaverseDataset(Dataset):
+
+ def _warn(self):
+ raise NotImplementedError('this dataset is just an example and cannot be used directly, you should modify it to your own setting! (search keyword TODO)')
+
+ def __init__(self, opt: Options, training=True):
+
+ self.opt = opt
+ self.training = training
+
+ # TODO: remove this barrier
+ self._warn()
+
+ # TODO: load the list of objects for training
+ self.items = []
+ with open('TODO: file containing the list', 'r') as f:
+ for line in f.readlines():
+ self.items.append(line.strip())
+
+ # naive split
+ if self.training:
+ self.items = self.items[:-self.opt.batch_size]
+ else:
+ self.items = self.items[-self.opt.batch_size:]
+
+ # default camera intrinsics
+ self.tan_half_fov = np.tan(0.5 * np.deg2rad(self.opt.fovy))
+ self.proj_matrix = torch.zeros(4, 4, dtype=torch.float32)
+ self.proj_matrix[0, 0] = 1 / self.tan_half_fov
+ self.proj_matrix[1, 1] = 1 / self.tan_half_fov
+ self.proj_matrix[2, 2] = (self.opt.zfar + self.opt.znear) / (self.opt.zfar - self.opt.znear)
+ self.proj_matrix[3, 2] = - (self.opt.zfar * self.opt.znear) / (self.opt.zfar - self.opt.znear)
+ self.proj_matrix[2, 3] = 1
+
+
+ def __len__(self):
+ return len(self.items)
+
+ def __getitem__(self, idx):
+
+ uid = self.items[idx]
+ results = {}
+
+ # load num_views images
+ images = []
+ masks = []
+ cam_poses = []
+
+ vid_cnt = 0
+
+ # TODO: choose views, based on your rendering settings
+ if self.training:
+ # input views are in (36, 72), other views are randomly selected
+ vids = np.random.permutation(np.arange(36, 73))[:self.opt.num_input_views].tolist() + np.random.permutation(100).tolist()
+ else:
+ # fixed views
+ vids = np.arange(36, 73, 4).tolist() + np.arange(100).tolist()
+
+ for vid in vids:
+
+ image_path = os.path.join(uid, 'rgb', f'{vid:03d}.png')
+ camera_path = os.path.join(uid, 'pose', f'{vid:03d}.txt')
+
+ try:
+ # TODO: load data (modify self.client here)
+ image = np.frombuffer(self.client.get(image_path), np.uint8)
+ image = torch.from_numpy(cv2.imdecode(image, cv2.IMREAD_UNCHANGED).astype(np.float32) / 255) # [512, 512, 4] in [0, 1]
+ c2w = [float(t) for t in self.client.get(camera_path).decode().strip().split(' ')]
+ c2w = torch.tensor(c2w, dtype=torch.float32).reshape(4, 4)
+ except Exception as e:
+ # print(f'[WARN] dataset {uid} {vid}: {e}')
+ continue
+
+ # TODO: you may have a different camera system
+ # blender world + opencv cam --> opengl world & cam
+ c2w[1] *= -1
+ c2w[[1, 2]] = c2w[[2, 1]]
+ c2w[:3, 1:3] *= -1 # invert up and forward direction
+
+ # scale up radius to fully use the [-1, 1]^3 space!
+ c2w[:3, 3] *= self.opt.cam_radius / 1.5 # 1.5 is the default scale
+
+ image = image.permute(2, 0, 1) # [4, 512, 512]
+ mask = image[3:4] # [1, 512, 512]
+ image = image[:3] * mask + (1 - mask) # [3, 512, 512], to white bg
+ image = image[[2,1,0]].contiguous() # bgr to rgb
+
+ images.append(image)
+ masks.append(mask.squeeze(0))
+ cam_poses.append(c2w)
+
+ vid_cnt += 1
+ if vid_cnt == self.opt.num_views:
+ break
+
+ if vid_cnt < self.opt.num_views:
+ print(f'[WARN] dataset {uid}: not enough valid views, only {vid_cnt} views found!')
+ n = self.opt.num_views - vid_cnt
+ images = images + [images[-1]] * n
+ masks = masks + [masks[-1]] * n
+ cam_poses = cam_poses + [cam_poses[-1]] * n
+
+ images = torch.stack(images, dim=0) # [V, C, H, W]
+ masks = torch.stack(masks, dim=0) # [V, H, W]
+ cam_poses = torch.stack(cam_poses, dim=0) # [V, 4, 4]
+
+ # normalized camera feats as in paper (transform the first pose to a fixed position)
+ transform = torch.tensor([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, self.opt.cam_radius], [0, 0, 0, 1]], dtype=torch.float32) @ torch.inverse(cam_poses[0])
+ cam_poses = transform.unsqueeze(0) @ cam_poses # [V, 4, 4]
+
+ images_input = F.interpolate(images[:self.opt.num_input_views].clone(), size=(self.opt.input_size, self.opt.input_size), mode='bilinear', align_corners=False) # [V, C, H, W]
+ cam_poses_input = cam_poses[:self.opt.num_input_views].clone()
+
+ # data augmentation
+ if self.training:
+ # apply random grid distortion to simulate 3D inconsistency
+ if random.random() < self.opt.prob_grid_distortion:
+ images_input[1:] = grid_distortion(images_input[1:])
+ # apply camera jittering (only to input!)
+ if random.random() < self.opt.prob_cam_jitter:
+ cam_poses_input[1:] = orbit_camera_jitter(cam_poses_input[1:])
+
+ images_input = TF.normalize(images_input, IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD)
+
+ # resize render ground-truth images, range still in [0, 1]
+ results['images_output'] = F.interpolate(images, size=(self.opt.output_size, self.opt.output_size), mode='bilinear', align_corners=False) # [V, C, output_size, output_size]
+ results['masks_output'] = F.interpolate(masks.unsqueeze(1), size=(self.opt.output_size, self.opt.output_size), mode='bilinear', align_corners=False) # [V, 1, output_size, output_size]
+
+ # build rays for input views
+ rays_embeddings = []
+ for i in range(self.opt.num_input_views):
+ rays_o, rays_d = get_rays(cam_poses_input[i], self.opt.input_size, self.opt.input_size, self.opt.fovy) # [h, w, 3]
+ rays_plucker = torch.cat([torch.cross(rays_o, rays_d, dim=-1), rays_d], dim=-1) # [h, w, 6]
+ rays_embeddings.append(rays_plucker)
+
+
+ rays_embeddings = torch.stack(rays_embeddings, dim=0).permute(0, 3, 1, 2).contiguous() # [V, 6, h, w]
+ final_input = torch.cat([images_input, rays_embeddings], dim=1) # [V=4, 9, H, W]
+ results['input'] = final_input
+
+ # opengl to colmap camera for gaussian renderer
+ cam_poses[:, :3, 1:3] *= -1 # invert up & forward direction
+
+ # cameras needed by gaussian rasterizer
+ cam_view = torch.inverse(cam_poses).transpose(1, 2) # [V, 4, 4]
+ cam_view_proj = cam_view @ self.proj_matrix # [V, 4, 4]
+ cam_pos = - cam_poses[:, :3, 3] # [V, 3]
+
+ results['cam_view'] = cam_view
+ results['cam_view_proj'] = cam_view_proj
+ results['cam_pos'] = cam_pos
+
+ return results
\ No newline at end of file
diff --git a/lgm/core/unet.py b/lgm/core/unet.py
new file mode 100644
index 0000000..dcf75cd
--- /dev/null
+++ b/lgm/core/unet.py
@@ -0,0 +1,319 @@
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+import numpy as np
+from typing import Tuple, Literal
+from functools import partial
+
+from lgm.core.attention import MemEffAttention
+
+class MVAttention(nn.Module):
+ def __init__(
+ self,
+ dim: int,
+ num_heads: int = 8,
+ qkv_bias: bool = False,
+ proj_bias: bool = True,
+ attn_drop: float = 0.0,
+ proj_drop: float = 0.0,
+ groups: int = 32,
+ eps: float = 1e-5,
+ residual: bool = True,
+ skip_scale: float = 1,
+ num_frames: int = 4, # WARN: hardcoded!
+ ):
+ super().__init__()
+
+ self.residual = residual
+ self.skip_scale = skip_scale
+ self.num_frames = num_frames
+
+ self.norm = nn.GroupNorm(num_groups=groups, num_channels=dim, eps=eps, affine=True)
+ self.attn = MemEffAttention(dim, num_heads, qkv_bias, proj_bias, attn_drop, proj_drop)
+
+ def forward(self, x):
+ # x: [B*V, C, H, W]
+ BV, C, H, W = x.shape
+ B = BV // self.num_frames # assert BV % self.num_frames == 0
+
+ res = x
+ x = self.norm(x)
+
+ x = x.reshape(B, self.num_frames, C, H, W).permute(0, 1, 3, 4, 2).reshape(B, -1, C)
+ x = self.attn(x)
+ x = x.reshape(B, self.num_frames, H, W, C).permute(0, 1, 4, 2, 3).reshape(BV, C, H, W)
+
+ if self.residual:
+ x = (x + res) * self.skip_scale
+ return x
+
+class ResnetBlock(nn.Module):
+ def __init__(
+ self,
+ in_channels: int,
+ out_channels: int,
+ resample: Literal['default', 'up', 'down'] = 'default',
+ groups: int = 32,
+ eps: float = 1e-5,
+ skip_scale: float = 1, # multiplied to output
+ ):
+ super().__init__()
+
+ self.in_channels = in_channels
+ self.out_channels = out_channels
+ self.skip_scale = skip_scale
+
+ self.norm1 = nn.GroupNorm(num_groups=groups, num_channels=in_channels, eps=eps, affine=True)
+ self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1)
+
+ self.norm2 = nn.GroupNorm(num_groups=groups, num_channels=out_channels, eps=eps, affine=True)
+ self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
+
+ self.act = F.silu
+
+ self.resample = None
+ if resample == 'up':
+ self.resample = partial(F.interpolate, scale_factor=2.0, mode="nearest")
+ elif resample == 'down':
+ self.resample = nn.AvgPool2d(kernel_size=2, stride=2)
+
+ self.shortcut = nn.Identity()
+ if self.in_channels != self.out_channels:
+ self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=True)
+
+
+ def forward(self, x):
+ res = x
+
+ x = self.norm1(x)
+ x = self.act(x)
+
+ if self.resample:
+ res = self.resample(res)
+ x = self.resample(x)
+
+ x = self.conv1(x)
+ x = self.norm2(x)
+ x = self.act(x)
+ x = self.conv2(x)
+
+ x = (x + self.shortcut(res)) * self.skip_scale
+
+ return x
+
+class DownBlock(nn.Module):
+ def __init__(
+ self,
+ in_channels: int,
+ out_channels: int,
+ num_layers: int = 1,
+ downsample: bool = True,
+ attention: bool = True,
+ attention_heads: int = 16,
+ skip_scale: float = 1,
+ ):
+ super().__init__()
+
+ nets = []
+ attns = []
+ for i in range(num_layers):
+ in_channels = in_channels if i == 0 else out_channels
+ nets.append(ResnetBlock(in_channels, out_channels, skip_scale=skip_scale))
+ if attention:
+ attns.append(MVAttention(out_channels, attention_heads, skip_scale=skip_scale))
+ else:
+ attns.append(None)
+ self.nets = nn.ModuleList(nets)
+ self.attns = nn.ModuleList(attns)
+
+ self.downsample = None
+ if downsample:
+ self.downsample = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=2, padding=1)
+
+ def forward(self, x):
+ xs = []
+
+ for attn, net in zip(self.attns, self.nets):
+ x = net(x)
+ if attn:
+ x = attn(x)
+ xs.append(x)
+
+ if self.downsample:
+ x = self.downsample(x)
+ xs.append(x)
+
+ return x, xs
+
+
+class MidBlock(nn.Module):
+ def __init__(
+ self,
+ in_channels: int,
+ num_layers: int = 1,
+ attention: bool = True,
+ attention_heads: int = 16,
+ skip_scale: float = 1,
+ ):
+ super().__init__()
+
+ nets = []
+ attns = []
+ # first layer
+ nets.append(ResnetBlock(in_channels, in_channels, skip_scale=skip_scale))
+ # more layers
+ for i in range(num_layers):
+ nets.append(ResnetBlock(in_channels, in_channels, skip_scale=skip_scale))
+ if attention:
+ attns.append(MVAttention(in_channels, attention_heads, skip_scale=skip_scale))
+ else:
+ attns.append(None)
+ self.nets = nn.ModuleList(nets)
+ self.attns = nn.ModuleList(attns)
+
+ def forward(self, x):
+ x = self.nets[0](x)
+ for attn, net in zip(self.attns, self.nets[1:]):
+ if attn:
+ x = attn(x)
+ x = net(x)
+ return x
+
+
+class UpBlock(nn.Module):
+ def __init__(
+ self,
+ in_channels: int,
+ prev_out_channels: int,
+ out_channels: int,
+ num_layers: int = 1,
+ upsample: bool = True,
+ attention: bool = True,
+ attention_heads: int = 16,
+ skip_scale: float = 1,
+ ):
+ super().__init__()
+
+ nets = []
+ attns = []
+ for i in range(num_layers):
+ cin = in_channels if i == 0 else out_channels
+ cskip = prev_out_channels if (i == num_layers - 1) else out_channels
+
+ nets.append(ResnetBlock(cin + cskip, out_channels, skip_scale=skip_scale))
+ if attention:
+ attns.append(MVAttention(out_channels, attention_heads, skip_scale=skip_scale))
+ else:
+ attns.append(None)
+ self.nets = nn.ModuleList(nets)
+ self.attns = nn.ModuleList(attns)
+
+ self.upsample = None
+ if upsample:
+ self.upsample = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
+
+ def forward(self, x, xs):
+
+ for attn, net in zip(self.attns, self.nets):
+ res_x = xs[-1]
+ xs = xs[:-1]
+ x = torch.cat([x, res_x], dim=1)
+ x = net(x)
+ if attn:
+ x = attn(x)
+
+ if self.upsample:
+ x = F.interpolate(x, scale_factor=2.0, mode='nearest')
+ x = self.upsample(x)
+
+ return x
+
+
+# it could be asymmetric!
+class UNet(nn.Module):
+ def __init__(
+ self,
+ in_channels: int = 3,
+ out_channels: int = 3,
+ down_channels: Tuple[int, ...] = (64, 128, 256, 512, 1024),
+ down_attention: Tuple[bool, ...] = (False, False, False, True, True),
+ mid_attention: bool = True,
+ up_channels: Tuple[int, ...] = (1024, 512, 256),
+ up_attention: Tuple[bool, ...] = (True, True, False),
+ layers_per_block: int = 2,
+ skip_scale: float = np.sqrt(0.5),
+ ):
+ super().__init__()
+
+ # first
+ self.conv_in = nn.Conv2d(in_channels, down_channels[0], kernel_size=3, stride=1, padding=1)
+
+ # down
+ down_blocks = []
+ cout = down_channels[0]
+ for i in range(len(down_channels)):
+ cin = cout
+ cout = down_channels[i]
+
+ down_blocks.append(DownBlock(
+ cin, cout,
+ num_layers=layers_per_block,
+ downsample=(i != len(down_channels) - 1), # not final layer
+ attention=down_attention[i],
+ skip_scale=skip_scale,
+ ))
+ self.down_blocks = nn.ModuleList(down_blocks)
+
+ # mid
+ self.mid_block = MidBlock(down_channels[-1], attention=mid_attention, skip_scale=skip_scale)
+
+ # up
+ up_blocks = []
+ cout = up_channels[0]
+ for i in range(len(up_channels)):
+ cin = cout
+ cout = up_channels[i]
+ cskip = down_channels[max(-2 - i, -len(down_channels))] # for assymetric
+
+ up_blocks.append(UpBlock(
+ cin, cskip, cout,
+ num_layers=layers_per_block + 1, # one more layer for up
+ upsample=(i != len(up_channels) - 1), # not final layer
+ attention=up_attention[i],
+ skip_scale=skip_scale,
+ ))
+ self.up_blocks = nn.ModuleList(up_blocks)
+
+ # last
+ self.norm_out = nn.GroupNorm(num_channels=up_channels[-1], num_groups=32, eps=1e-5)
+ self.conv_out = nn.Conv2d(up_channels[-1], out_channels, kernel_size=3, stride=1, padding=1)
+
+
+ def forward(self, x):
+ # x: [B, Cin, H, W]
+
+ # first
+ x = self.conv_in(x)
+
+ # down
+ xss = [x]
+ for block in self.down_blocks:
+ x, xs = block(x)
+ xss.extend(xs)
+
+ # mid
+ x = self.mid_block(x)
+
+ # up
+ for block in self.up_blocks:
+ xs = xss[-len(block.nets):]
+ xss = xss[:-len(block.nets)]
+ x = block(x, xs)
+
+ # last
+ x = self.norm_out(x)
+ x = F.silu(x)
+ x = self.conv_out(x) # [B, Cout, H', W']
+
+ return x
diff --git a/lgm/core/utils.py b/lgm/core/utils.py
new file mode 100644
index 0000000..17ab4f5
--- /dev/null
+++ b/lgm/core/utils.py
@@ -0,0 +1,109 @@
+import numpy as np
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+import roma
+from kiui.op import safe_normalize
+
+def get_rays(pose, h, w, fovy, opengl=True):
+
+ x, y = torch.meshgrid(
+ torch.arange(w, device=pose.device),
+ torch.arange(h, device=pose.device),
+ indexing="xy",
+ )
+ x = x.flatten()
+ y = y.flatten()
+
+ cx = w * 0.5
+ cy = h * 0.5
+
+ focal = h * 0.5 / np.tan(0.5 * np.deg2rad(fovy))
+
+ camera_dirs = F.pad(
+ torch.stack(
+ [
+ (x - cx + 0.5) / focal,
+ (y - cy + 0.5) / focal * (-1.0 if opengl else 1.0),
+ ],
+ dim=-1,
+ ),
+ (0, 1),
+ value=(-1.0 if opengl else 1.0),
+ ) # [hw, 3]
+
+ rays_d = camera_dirs @ pose[:3, :3].transpose(0, 1) # [hw, 3]
+ rays_o = pose[:3, 3].unsqueeze(0).expand_as(rays_d) # [hw, 3]
+
+ rays_o = rays_o.view(h, w, 3)
+ rays_d = safe_normalize(rays_d).view(h, w, 3)
+
+ return rays_o, rays_d
+
+def orbit_camera_jitter(poses, strength=0.1):
+ # poses: [B, 4, 4], assume orbit camera in opengl format
+ # random orbital rotate
+
+ B = poses.shape[0]
+ rotvec_x = poses[:, :3, 1] * strength * np.pi * (torch.rand(B, 1, device=poses.device) * 2 - 1)
+ rotvec_y = poses[:, :3, 0] * strength * np.pi / 2 * (torch.rand(B, 1, device=poses.device) * 2 - 1)
+
+ rot = roma.rotvec_to_rotmat(rotvec_x) @ roma.rotvec_to_rotmat(rotvec_y)
+ R = rot @ poses[:, :3, :3]
+ T = rot @ poses[:, :3, 3:]
+
+ new_poses = poses.clone()
+ new_poses[:, :3, :3] = R
+ new_poses[:, :3, 3:] = T
+
+ return new_poses
+
+def grid_distortion(images, strength=0.5):
+ # images: [B, C, H, W]
+ # num_steps: int, grid resolution for distortion
+ # strength: float in [0, 1], strength of distortion
+
+ B, C, H, W = images.shape
+
+ num_steps = np.random.randint(8, 17)
+ grid_steps = torch.linspace(-1, 1, num_steps)
+
+ # have to loop batch...
+ grids = []
+ for b in range(B):
+ # construct displacement
+ x_steps = torch.linspace(0, 1, num_steps) # [num_steps], inclusive
+ x_steps = (x_steps + strength * (torch.rand_like(x_steps) - 0.5) / (num_steps - 1)).clamp(0, 1) # perturb
+ x_steps = (x_steps * W).long() # [num_steps]
+ x_steps[0] = 0
+ x_steps[-1] = W
+ xs = []
+ for i in range(num_steps - 1):
+ xs.append(torch.linspace(grid_steps[i], grid_steps[i + 1], x_steps[i + 1] - x_steps[i]))
+ xs = torch.cat(xs, dim=0) # [W]
+
+ y_steps = torch.linspace(0, 1, num_steps) # [num_steps], inclusive
+ y_steps = (y_steps + strength * (torch.rand_like(y_steps) - 0.5) / (num_steps - 1)).clamp(0, 1) # perturb
+ y_steps = (y_steps * H).long() # [num_steps]
+ y_steps[0] = 0
+ y_steps[-1] = H
+ ys = []
+ for i in range(num_steps - 1):
+ ys.append(torch.linspace(grid_steps[i], grid_steps[i + 1], y_steps[i + 1] - y_steps[i]))
+ ys = torch.cat(ys, dim=0) # [H]
+
+ # construct grid
+ grid_x, grid_y = torch.meshgrid(xs, ys, indexing='xy') # [H, W]
+ grid = torch.stack([grid_x, grid_y], dim=-1) # [H, W, 2]
+
+ grids.append(grid)
+
+ grids = torch.stack(grids, dim=0).to(images.device) # [B, H, W, 2]
+
+ # grid sample
+ images = F.grid_sample(images, grids, align_corners=False)
+
+ return images
+
diff --git a/lgm/mvdream/mv_unet.py b/lgm/mvdream/mv_unet.py
new file mode 100644
index 0000000..7d9ad4d
--- /dev/null
+++ b/lgm/mvdream/mv_unet.py
@@ -0,0 +1,1005 @@
+import math
+import numpy as np
+from inspect import isfunction
+from typing import Optional, Any, List
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from einops import rearrange, repeat
+
+from diffusers.configuration_utils import ConfigMixin
+from diffusers.models.modeling_utils import ModelMixin
+
+# require xformers!
+import xformers
+import xformers.ops
+
+from kiui.cam import orbit_camera
+
+def get_camera(
+ num_frames, elevation=0, azimuth_start=0, azimuth_span=360, blender_coord=True, extra_view=False,
+):
+ angle_gap = azimuth_span / num_frames
+ cameras = []
+ for azimuth in np.arange(azimuth_start, azimuth_span + azimuth_start, angle_gap):
+
+ pose = orbit_camera(elevation, azimuth, radius=1) # [4, 4]
+
+ # opengl to blender
+ if blender_coord:
+ pose[2] *= -1
+ pose[[1, 2]] = pose[[2, 1]]
+
+ cameras.append(pose.flatten())
+
+ if extra_view:
+ cameras.append(np.zeros_like(cameras[0]))
+
+ return torch.from_numpy(np.stack(cameras, axis=0)).float() # [num_frames, 16]
+
+
+def timestep_embedding(timesteps, dim, max_period=10000, repeat_only=False):
+ """
+ Create sinusoidal timestep embeddings.
+ :param timesteps: a 1-D Tensor of N indices, one per batch element.
+ These may be fractional.
+ :param dim: the dimension of the output.
+ :param max_period: controls the minimum frequency of the embeddings.
+ :return: an [N x dim] Tensor of positional embeddings.
+ """
+ if not repeat_only:
+ half = dim // 2
+ freqs = torch.exp(
+ -math.log(max_period)
+ * torch.arange(start=0, end=half, dtype=torch.float32)
+ / half
+ ).to(device=timesteps.device)
+ args = timesteps[:, None] * freqs[None]
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
+ if dim % 2:
+ embedding = torch.cat(
+ [embedding, torch.zeros_like(embedding[:, :1])], dim=-1
+ )
+ else:
+ embedding = repeat(timesteps, "b -> b d", d=dim)
+ # import pdb; pdb.set_trace()
+ return embedding
+
+
+def zero_module(module):
+ """
+ Zero out the parameters of a module and return it.
+ """
+ for p in module.parameters():
+ p.detach().zero_()
+ return module
+
+
+def conv_nd(dims, *args, **kwargs):
+ """
+ Create a 1D, 2D, or 3D convolution module.
+ """
+ if dims == 1:
+ return nn.Conv1d(*args, **kwargs)
+ elif dims == 2:
+ return nn.Conv2d(*args, **kwargs)
+ elif dims == 3:
+ return nn.Conv3d(*args, **kwargs)
+ raise ValueError(f"unsupported dimensions: {dims}")
+
+
+def avg_pool_nd(dims, *args, **kwargs):
+ """
+ Create a 1D, 2D, or 3D average pooling module.
+ """
+ if dims == 1:
+ return nn.AvgPool1d(*args, **kwargs)
+ elif dims == 2:
+ return nn.AvgPool2d(*args, **kwargs)
+ elif dims == 3:
+ return nn.AvgPool3d(*args, **kwargs)
+ raise ValueError(f"unsupported dimensions: {dims}")
+
+
+def default(val, d):
+ if val is not None:
+ return val
+ return d() if isfunction(d) else d
+
+
+class GEGLU(nn.Module):
+ def __init__(self, dim_in, dim_out):
+ super().__init__()
+ self.proj = nn.Linear(dim_in, dim_out * 2)
+
+ def forward(self, x):
+ x, gate = self.proj(x).chunk(2, dim=-1)
+ return x * F.gelu(gate)
+
+
+class FeedForward(nn.Module):
+ def __init__(self, dim, dim_out=None, mult=4, glu=False, dropout=0.0):
+ super().__init__()
+ inner_dim = int(dim * mult)
+ dim_out = default(dim_out, dim)
+ project_in = (
+ nn.Sequential(nn.Linear(dim, inner_dim), nn.GELU())
+ if not glu
+ else GEGLU(dim, inner_dim)
+ )
+
+ self.net = nn.Sequential(
+ project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out)
+ )
+
+ def forward(self, x):
+ return self.net(x)
+
+
+class MemoryEfficientCrossAttention(nn.Module):
+ # https://github.com/MatthieuTPHR/diffusers/blob/d80b531ff8060ec1ea982b65a1b8df70f73aa67c/src/diffusers/models/attention.py#L223
+ def __init__(
+ self,
+ query_dim,
+ context_dim=None,
+ heads=8,
+ dim_head=64,
+ dropout=0.0,
+ ip_dim=0,
+ ip_weight=1,
+ ):
+ super().__init__()
+
+ inner_dim = dim_head * heads
+ context_dim = default(context_dim, query_dim)
+
+ self.heads = heads
+ self.dim_head = dim_head
+
+ self.ip_dim = ip_dim
+ self.ip_weight = ip_weight
+
+ if self.ip_dim > 0:
+ self.to_k_ip = nn.Linear(context_dim, inner_dim, bias=False)
+ self.to_v_ip = nn.Linear(context_dim, inner_dim, bias=False)
+
+ self.to_q = nn.Linear(query_dim, inner_dim, bias=False)
+ self.to_k = nn.Linear(context_dim, inner_dim, bias=False)
+ self.to_v = nn.Linear(context_dim, inner_dim, bias=False)
+
+ self.to_out = nn.Sequential(
+ nn.Linear(inner_dim, query_dim), nn.Dropout(dropout)
+ )
+ self.attention_op: Optional[Any] = None
+
+ def forward(self, x, context=None):
+ q = self.to_q(x)
+ context = default(context, x)
+
+ if self.ip_dim > 0:
+ # context: [B, 77 + 16(ip), 1024]
+ token_len = context.shape[1]
+ context_ip = context[:, -self.ip_dim :, :]
+ k_ip = self.to_k_ip(context_ip)
+ v_ip = self.to_v_ip(context_ip)
+ context = context[:, : (token_len - self.ip_dim), :]
+
+ k = self.to_k(context)
+ v = self.to_v(context)
+
+ b, _, _ = q.shape
+ q, k, v = map(
+ lambda t: t.unsqueeze(3)
+ .reshape(b, t.shape[1], self.heads, self.dim_head)
+ .permute(0, 2, 1, 3)
+ .reshape(b * self.heads, t.shape[1], self.dim_head)
+ .contiguous(),
+ (q, k, v),
+ )
+
+ # actually compute the attention, what we cannot get enough of
+ out = xformers.ops.memory_efficient_attention(
+ q, k, v, attn_bias=None, op=self.attention_op
+ )
+
+ if self.ip_dim > 0:
+ k_ip, v_ip = map(
+ lambda t: t.unsqueeze(3)
+ .reshape(b, t.shape[1], self.heads, self.dim_head)
+ .permute(0, 2, 1, 3)
+ .reshape(b * self.heads, t.shape[1], self.dim_head)
+ .contiguous(),
+ (k_ip, v_ip),
+ )
+ # actually compute the attention, what we cannot get enough of
+ out_ip = xformers.ops.memory_efficient_attention(
+ q, k_ip, v_ip, attn_bias=None, op=self.attention_op
+ )
+ out = out + self.ip_weight * out_ip
+
+ out = (
+ out.unsqueeze(0)
+ .reshape(b, self.heads, out.shape[1], self.dim_head)
+ .permute(0, 2, 1, 3)
+ .reshape(b, out.shape[1], self.heads * self.dim_head)
+ )
+ return self.to_out(out)
+
+
+class BasicTransformerBlock3D(nn.Module):
+
+ def __init__(
+ self,
+ dim,
+ n_heads,
+ d_head,
+ context_dim,
+ dropout=0.0,
+ gated_ff=True,
+ ip_dim=0,
+ ip_weight=1,
+ ):
+ super().__init__()
+
+ self.attn1 = MemoryEfficientCrossAttention(
+ query_dim=dim,
+ context_dim=None, # self-attention
+ heads=n_heads,
+ dim_head=d_head,
+ dropout=dropout,
+ )
+ self.ff = FeedForward(dim, dropout=dropout, glu=gated_ff)
+ self.attn2 = MemoryEfficientCrossAttention(
+ query_dim=dim,
+ context_dim=context_dim,
+ heads=n_heads,
+ dim_head=d_head,
+ dropout=dropout,
+ # ip only applies to cross-attention
+ ip_dim=ip_dim,
+ ip_weight=ip_weight,
+ )
+ self.norm1 = nn.LayerNorm(dim)
+ self.norm2 = nn.LayerNorm(dim)
+ self.norm3 = nn.LayerNorm(dim)
+
+ def forward(self, x, context=None, num_frames=1):
+ x = rearrange(x, "(b f) l c -> b (f l) c", f=num_frames).contiguous()
+ x = self.attn1(self.norm1(x), context=None) + x
+ x = rearrange(x, "b (f l) c -> (b f) l c", f=num_frames).contiguous()
+ x = self.attn2(self.norm2(x), context=context) + x
+ x = self.ff(self.norm3(x)) + x
+ return x
+
+
+class SpatialTransformer3D(nn.Module):
+
+ def __init__(
+ self,
+ in_channels,
+ n_heads,
+ d_head,
+ context_dim, # cross attention input dim
+ depth=1,
+ dropout=0.0,
+ ip_dim=0,
+ ip_weight=1,
+ ):
+ super().__init__()
+
+ if not isinstance(context_dim, list):
+ context_dim = [context_dim]
+
+ self.in_channels = in_channels
+
+ inner_dim = n_heads * d_head
+ self.norm = nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
+ self.proj_in = nn.Linear(in_channels, inner_dim)
+
+ self.transformer_blocks = nn.ModuleList(
+ [
+ BasicTransformerBlock3D(
+ inner_dim,
+ n_heads,
+ d_head,
+ context_dim=context_dim[d],
+ dropout=dropout,
+ ip_dim=ip_dim,
+ ip_weight=ip_weight,
+ )
+ for d in range(depth)
+ ]
+ )
+
+ self.proj_out = zero_module(nn.Linear(in_channels, inner_dim))
+
+
+ def forward(self, x, context=None, num_frames=1):
+ # note: if no context is given, cross-attention defaults to self-attention
+ if not isinstance(context, list):
+ context = [context]
+ b, c, h, w = x.shape
+ x_in = x
+ x = self.norm(x)
+ x = rearrange(x, "b c h w -> b (h w) c").contiguous()
+ x = self.proj_in(x)
+ for i, block in enumerate(self.transformer_blocks):
+ x = block(x, context=context[i], num_frames=num_frames)
+ x = self.proj_out(x)
+ x = rearrange(x, "b (h w) c -> b c h w", h=h, w=w).contiguous()
+
+ return x + x_in
+
+
+class PerceiverAttention(nn.Module):
+ def __init__(self, *, dim, dim_head=64, heads=8):
+ super().__init__()
+ self.scale = dim_head ** -0.5
+ self.dim_head = dim_head
+ self.heads = heads
+ inner_dim = dim_head * heads
+
+ self.norm1 = nn.LayerNorm(dim)
+ self.norm2 = nn.LayerNorm(dim)
+
+ self.to_q = nn.Linear(dim, inner_dim, bias=False)
+ self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False)
+ self.to_out = nn.Linear(inner_dim, dim, bias=False)
+
+ def forward(self, x, latents):
+ """
+ Args:
+ x (torch.Tensor): image features
+ shape (b, n1, D)
+ latent (torch.Tensor): latent features
+ shape (b, n2, D)
+ """
+ x = self.norm1(x)
+ latents = self.norm2(latents)
+
+ b, l, _ = latents.shape
+
+ q = self.to_q(latents)
+ kv_input = torch.cat((x, latents), dim=-2)
+ k, v = self.to_kv(kv_input).chunk(2, dim=-1)
+
+ q, k, v = map(
+ lambda t: t.reshape(b, t.shape[1], self.heads, -1)
+ .transpose(1, 2)
+ .reshape(b, self.heads, t.shape[1], -1)
+ .contiguous(),
+ (q, k, v),
+ )
+
+ # attention
+ scale = 1 / math.sqrt(math.sqrt(self.dim_head))
+ weight = (q * scale) @ (k * scale).transpose(-2, -1) # More stable with f16 than dividing afterwards
+ weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype)
+ out = weight @ v
+
+ out = out.permute(0, 2, 1, 3).reshape(b, l, -1)
+
+ return self.to_out(out)
+
+
+class Resampler(nn.Module):
+ def __init__(
+ self,
+ dim=1024,
+ depth=8,
+ dim_head=64,
+ heads=16,
+ num_queries=8,
+ embedding_dim=768,
+ output_dim=1024,
+ ff_mult=4,
+ ):
+ super().__init__()
+ self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim ** 0.5)
+ self.proj_in = nn.Linear(embedding_dim, dim)
+ self.proj_out = nn.Linear(dim, output_dim)
+ self.norm_out = nn.LayerNorm(output_dim)
+
+ self.layers = nn.ModuleList([])
+ for _ in range(depth):
+ self.layers.append(
+ nn.ModuleList(
+ [
+ PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads),
+ nn.Sequential(
+ nn.LayerNorm(dim),
+ nn.Linear(dim, dim * ff_mult, bias=False),
+ nn.GELU(),
+ nn.Linear(dim * ff_mult, dim, bias=False),
+ )
+ ]
+ )
+ )
+
+ def forward(self, x):
+ latents = self.latents.repeat(x.size(0), 1, 1)
+ x = self.proj_in(x)
+ for attn, ff in self.layers:
+ latents = attn(x, latents) + latents
+ latents = ff(latents) + latents
+
+ latents = self.proj_out(latents)
+ return self.norm_out(latents)
+
+
+class CondSequential(nn.Sequential):
+ """
+ A sequential module that passes timestep embeddings to the children that
+ support it as an extra input.
+ """
+
+ def forward(self, x, emb, context=None, num_frames=1):
+ for layer in self:
+ if isinstance(layer, ResBlock):
+ x = layer(x, emb)
+ elif isinstance(layer, SpatialTransformer3D):
+ x = layer(x, context, num_frames=num_frames)
+ else:
+ x = layer(x)
+ return x
+
+
+class Upsample(nn.Module):
+ """
+ An upsampling layer with an optional convolution.
+ :param channels: channels in the inputs and outputs.
+ :param use_conv: a bool determining if a convolution is applied.
+ :param dims: determines if the signal is 1D, 2D, or 3D. If 3D, then
+ upsampling occurs in the inner-two dimensions.
+ """
+
+ def __init__(self, channels, use_conv, dims=2, out_channels=None, padding=1):
+ super().__init__()
+ self.channels = channels
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.dims = dims
+ if use_conv:
+ self.conv = conv_nd(
+ dims, self.channels, self.out_channels, 3, padding=padding
+ )
+
+ def forward(self, x):
+ assert x.shape[1] == self.channels
+ if self.dims == 3:
+ x = F.interpolate(
+ x, (x.shape[2], x.shape[3] * 2, x.shape[4] * 2), mode="nearest"
+ )
+ else:
+ x = F.interpolate(x, scale_factor=2, mode="nearest")
+ if self.use_conv:
+ x = self.conv(x)
+ return x
+
+
+class Downsample(nn.Module):
+ """
+ A downsampling layer with an optional convolution.
+ :param channels: channels in the inputs and outputs.
+ :param use_conv: a bool determining if a convolution is applied.
+ :param dims: determines if the signal is 1D, 2D, or 3D. If 3D, then
+ downsampling occurs in the inner-two dimensions.
+ """
+
+ def __init__(self, channels, use_conv, dims=2, out_channels=None, padding=1):
+ super().__init__()
+ self.channels = channels
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.dims = dims
+ stride = 2 if dims != 3 else (1, 2, 2)
+ if use_conv:
+ self.op = conv_nd(
+ dims,
+ self.channels,
+ self.out_channels,
+ 3,
+ stride=stride,
+ padding=padding,
+ )
+ else:
+ assert self.channels == self.out_channels
+ self.op = avg_pool_nd(dims, kernel_size=stride, stride=stride)
+
+ def forward(self, x):
+ assert x.shape[1] == self.channels
+ return self.op(x)
+
+
+class ResBlock(nn.Module):
+ """
+ A residual block that can optionally change the number of channels.
+ :param channels: the number of input channels.
+ :param emb_channels: the number of timestep embedding channels.
+ :param dropout: the rate of dropout.
+ :param out_channels: if specified, the number of out channels.
+ :param use_conv: if True and out_channels is specified, use a spatial
+ convolution instead of a smaller 1x1 convolution to change the
+ channels in the skip connection.
+ :param dims: determines if the signal is 1D, 2D, or 3D.
+ :param up: if True, use this block for upsampling.
+ :param down: if True, use this block for downsampling.
+ """
+
+ def __init__(
+ self,
+ channels,
+ emb_channels,
+ dropout,
+ out_channels=None,
+ use_conv=False,
+ use_scale_shift_norm=False,
+ dims=2,
+ up=False,
+ down=False,
+ ):
+ super().__init__()
+ self.channels = channels
+ self.emb_channels = emb_channels
+ self.dropout = dropout
+ self.out_channels = out_channels or channels
+ self.use_conv = use_conv
+ self.use_scale_shift_norm = use_scale_shift_norm
+
+ self.in_layers = nn.Sequential(
+ nn.GroupNorm(32, channels),
+ nn.SiLU(),
+ conv_nd(dims, channels, self.out_channels, 3, padding=1),
+ )
+
+ self.updown = up or down
+
+ if up:
+ self.h_upd = Upsample(channels, False, dims)
+ self.x_upd = Upsample(channels, False, dims)
+ elif down:
+ self.h_upd = Downsample(channels, False, dims)
+ self.x_upd = Downsample(channels, False, dims)
+ else:
+ self.h_upd = self.x_upd = nn.Identity()
+
+ self.emb_layers = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(
+ emb_channels,
+ 2 * self.out_channels if use_scale_shift_norm else self.out_channels,
+ ),
+ )
+ self.out_layers = nn.Sequential(
+ nn.GroupNorm(32, self.out_channels),
+ nn.SiLU(),
+ nn.Dropout(p=dropout),
+ zero_module(
+ conv_nd(dims, self.out_channels, self.out_channels, 3, padding=1)
+ ),
+ )
+
+ if self.out_channels == channels:
+ self.skip_connection = nn.Identity()
+ elif use_conv:
+ self.skip_connection = conv_nd(
+ dims, channels, self.out_channels, 3, padding=1
+ )
+ else:
+ self.skip_connection = conv_nd(dims, channels, self.out_channels, 1)
+
+ def forward(self, x, emb):
+ if self.updown:
+ in_rest, in_conv = self.in_layers[:-1], self.in_layers[-1]
+ h = in_rest(x)
+ h = self.h_upd(h)
+ x = self.x_upd(x)
+ h = in_conv(h)
+ else:
+ h = self.in_layers(x)
+ emb_out = self.emb_layers(emb).type(h.dtype)
+ while len(emb_out.shape) < len(h.shape):
+ emb_out = emb_out[..., None]
+ if self.use_scale_shift_norm:
+ out_norm, out_rest = self.out_layers[0], self.out_layers[1:]
+ scale, shift = torch.chunk(emb_out, 2, dim=1)
+ h = out_norm(h) * (1 + scale) + shift
+ h = out_rest(h)
+ else:
+ h = h + emb_out
+ h = self.out_layers(h)
+ return self.skip_connection(x) + h
+
+
+class MultiViewUNetModel(ModelMixin, ConfigMixin):
+ """
+ The full multi-view UNet model with attention, timestep embedding and camera embedding.
+ :param in_channels: channels in the input Tensor.
+ :param model_channels: base channel count for the model.
+ :param out_channels: channels in the output Tensor.
+ :param num_res_blocks: number of residual blocks per downsample.
+ :param attention_resolutions: a collection of downsample rates at which
+ attention will take place. May be a set, list, or tuple.
+ For example, if this contains 4, then at 4x downsampling, attention
+ will be used.
+ :param dropout: the dropout probability.
+ :param channel_mult: channel multiplier for each level of the UNet.
+ :param conv_resample: if True, use learned convolutions for upsampling and
+ downsampling.
+ :param dims: determines if the signal is 1D, 2D, or 3D.
+ :param num_classes: if specified (as an int), then this model will be
+ class-conditional with `num_classes` classes.
+ :param num_heads: the number of attention heads in each attention layer.
+ :param num_heads_channels: if specified, ignore num_heads and instead use
+ a fixed channel width per attention head.
+ :param num_heads_upsample: works with num_heads to set a different number
+ of heads for upsampling. Deprecated.
+ :param use_scale_shift_norm: use a FiLM-like conditioning mechanism.
+ :param resblock_updown: use residual blocks for up/downsampling.
+ :param use_new_attention_order: use a different attention pattern for potentially
+ increased efficiency.
+ :param camera_dim: dimensionality of camera input.
+ """
+
+ def __init__(
+ self,
+ image_size,
+ in_channels,
+ model_channels,
+ out_channels,
+ num_res_blocks,
+ attention_resolutions,
+ dropout=0,
+ channel_mult=(1, 2, 4, 8),
+ conv_resample=True,
+ dims=2,
+ num_classes=None,
+ num_heads=-1,
+ num_head_channels=-1,
+ num_heads_upsample=-1,
+ use_scale_shift_norm=False,
+ resblock_updown=False,
+ transformer_depth=1,
+ context_dim=None,
+ n_embed=None,
+ num_attention_blocks=None,
+ adm_in_channels=None,
+ camera_dim=None,
+ ip_dim=0, # imagedream uses ip_dim > 0
+ ip_weight=1.0,
+ **kwargs,
+ ):
+ super().__init__()
+ assert context_dim is not None
+
+ if num_heads_upsample == -1:
+ num_heads_upsample = num_heads
+
+ if num_heads == -1:
+ assert (
+ num_head_channels != -1
+ ), "Either num_heads or num_head_channels has to be set"
+
+ if num_head_channels == -1:
+ assert (
+ num_heads != -1
+ ), "Either num_heads or num_head_channels has to be set"
+
+ self.image_size = image_size
+ self.in_channels = in_channels
+ self.model_channels = model_channels
+ self.out_channels = out_channels
+ if isinstance(num_res_blocks, int):
+ self.num_res_blocks = len(channel_mult) * [num_res_blocks]
+ else:
+ if len(num_res_blocks) != len(channel_mult):
+ raise ValueError(
+ "provide num_res_blocks either as an int (globally constant) or "
+ "as a list/tuple (per-level) with the same length as channel_mult"
+ )
+ self.num_res_blocks = num_res_blocks
+
+ if num_attention_blocks is not None:
+ assert len(num_attention_blocks) == len(self.num_res_blocks)
+ assert all(
+ map(
+ lambda i: self.num_res_blocks[i] >= num_attention_blocks[i],
+ range(len(num_attention_blocks)),
+ )
+ )
+ print(
+ f"Constructor of UNetModel received num_attention_blocks={num_attention_blocks}. "
+ f"This option has LESS priority than attention_resolutions {attention_resolutions}, "
+ f"i.e., in cases where num_attention_blocks[i] > 0 but 2**i not in attention_resolutions, "
+ f"attention will still not be set."
+ )
+
+ self.attention_resolutions = attention_resolutions
+ self.dropout = dropout
+ self.channel_mult = channel_mult
+ self.conv_resample = conv_resample
+ self.num_classes = num_classes
+ self.num_heads = num_heads
+ self.num_head_channels = num_head_channels
+ self.num_heads_upsample = num_heads_upsample
+ self.predict_codebook_ids = n_embed is not None
+
+ self.ip_dim = ip_dim
+ self.ip_weight = ip_weight
+
+ if self.ip_dim > 0:
+ self.image_embed = Resampler(
+ dim=context_dim,
+ depth=4,
+ dim_head=64,
+ heads=12,
+ num_queries=ip_dim, # num token
+ embedding_dim=1280,
+ output_dim=context_dim,
+ ff_mult=4,
+ )
+
+ time_embed_dim = model_channels * 4
+ self.time_embed = nn.Sequential(
+ nn.Linear(model_channels, time_embed_dim),
+ nn.SiLU(),
+ nn.Linear(time_embed_dim, time_embed_dim),
+ )
+
+ if camera_dim is not None:
+ time_embed_dim = model_channels * 4
+ self.camera_embed = nn.Sequential(
+ nn.Linear(camera_dim, time_embed_dim),
+ nn.SiLU(),
+ nn.Linear(time_embed_dim, time_embed_dim),
+ )
+
+ if self.num_classes is not None:
+ if isinstance(self.num_classes, int):
+ self.label_emb = nn.Embedding(self.num_classes, time_embed_dim)
+ elif self.num_classes == "continuous":
+ # print("setting up linear c_adm embedding layer")
+ self.label_emb = nn.Linear(1, time_embed_dim)
+ elif self.num_classes == "sequential":
+ assert adm_in_channels is not None
+ self.label_emb = nn.Sequential(
+ nn.Sequential(
+ nn.Linear(adm_in_channels, time_embed_dim),
+ nn.SiLU(),
+ nn.Linear(time_embed_dim, time_embed_dim),
+ )
+ )
+ else:
+ raise ValueError()
+
+ self.input_blocks = nn.ModuleList(
+ [
+ CondSequential(
+ conv_nd(dims, in_channels, model_channels, 3, padding=1)
+ )
+ ]
+ )
+ self._feature_size = model_channels
+ input_block_chans = [model_channels]
+ ch = model_channels
+ ds = 1
+ for level, mult in enumerate(channel_mult):
+ for nr in range(self.num_res_blocks[level]):
+ layers: List[Any] = [
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ out_channels=mult * model_channels,
+ dims=dims,
+ use_scale_shift_norm=use_scale_shift_norm,
+ )
+ ]
+ ch = mult * model_channels
+ if ds in attention_resolutions:
+ if num_head_channels == -1:
+ dim_head = ch // num_heads
+ else:
+ num_heads = ch // num_head_channels
+ dim_head = num_head_channels
+
+ if num_attention_blocks is None or nr < num_attention_blocks[level]:
+ layers.append(
+ SpatialTransformer3D(
+ ch,
+ num_heads,
+ dim_head,
+ context_dim=context_dim,
+ depth=transformer_depth,
+ ip_dim=self.ip_dim,
+ ip_weight=self.ip_weight,
+ )
+ )
+ self.input_blocks.append(CondSequential(*layers))
+ self._feature_size += ch
+ input_block_chans.append(ch)
+ if level != len(channel_mult) - 1:
+ out_ch = ch
+ self.input_blocks.append(
+ CondSequential(
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ out_channels=out_ch,
+ dims=dims,
+ use_scale_shift_norm=use_scale_shift_norm,
+ down=True,
+ )
+ if resblock_updown
+ else Downsample(
+ ch, conv_resample, dims=dims, out_channels=out_ch
+ )
+ )
+ )
+ ch = out_ch
+ input_block_chans.append(ch)
+ ds *= 2
+ self._feature_size += ch
+
+ if num_head_channels == -1:
+ dim_head = ch // num_heads
+ else:
+ num_heads = ch // num_head_channels
+ dim_head = num_head_channels
+
+ self.middle_block = CondSequential(
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ dims=dims,
+ use_scale_shift_norm=use_scale_shift_norm,
+ ),
+ SpatialTransformer3D(
+ ch,
+ num_heads,
+ dim_head,
+ context_dim=context_dim,
+ depth=transformer_depth,
+ ip_dim=self.ip_dim,
+ ip_weight=self.ip_weight,
+ ),
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ dims=dims,
+ use_scale_shift_norm=use_scale_shift_norm,
+ ),
+ )
+ self._feature_size += ch
+
+ self.output_blocks = nn.ModuleList([])
+ for level, mult in list(enumerate(channel_mult))[::-1]:
+ for i in range(self.num_res_blocks[level] + 1):
+ ich = input_block_chans.pop()
+ layers = [
+ ResBlock(
+ ch + ich,
+ time_embed_dim,
+ dropout,
+ out_channels=model_channels * mult,
+ dims=dims,
+ use_scale_shift_norm=use_scale_shift_norm,
+ )
+ ]
+ ch = model_channels * mult
+ if ds in attention_resolutions:
+ if num_head_channels == -1:
+ dim_head = ch // num_heads
+ else:
+ num_heads = ch // num_head_channels
+ dim_head = num_head_channels
+
+ if num_attention_blocks is None or i < num_attention_blocks[level]:
+ layers.append(
+ SpatialTransformer3D(
+ ch,
+ num_heads,
+ dim_head,
+ context_dim=context_dim,
+ depth=transformer_depth,
+ ip_dim=self.ip_dim,
+ ip_weight=self.ip_weight,
+ )
+ )
+ if level and i == self.num_res_blocks[level]:
+ out_ch = ch
+ layers.append(
+ ResBlock(
+ ch,
+ time_embed_dim,
+ dropout,
+ out_channels=out_ch,
+ dims=dims,
+ use_scale_shift_norm=use_scale_shift_norm,
+ up=True,
+ )
+ if resblock_updown
+ else Upsample(ch, conv_resample, dims=dims, out_channels=out_ch)
+ )
+ ds //= 2
+ self.output_blocks.append(CondSequential(*layers))
+ self._feature_size += ch
+
+ self.out = nn.Sequential(
+ nn.GroupNorm(32, ch),
+ nn.SiLU(),
+ zero_module(conv_nd(dims, model_channels, out_channels, 3, padding=1)),
+ )
+ if self.predict_codebook_ids:
+ self.id_predictor = nn.Sequential(
+ nn.GroupNorm(32, ch),
+ conv_nd(dims, model_channels, n_embed, 1),
+ # nn.LogSoftmax(dim=1) # change to cross_entropy and produce non-normalized logits
+ )
+
+ def forward(
+ self,
+ x,
+ timesteps=None,
+ context=None,
+ y=None,
+ camera=None,
+ num_frames=1,
+ ip=None,
+ ip_img=None,
+ **kwargs,
+ ):
+ """
+ Apply the model to an input batch.
+ :param x: an [(N x F) x C x ...] Tensor of inputs. F is the number of frames (views).
+ :param timesteps: a 1-D batch of timesteps.
+ :param context: conditioning plugged in via crossattn
+ :param y: an [N] Tensor of labels, if class-conditional.
+ :param num_frames: a integer indicating number of frames for tensor reshaping.
+ :return: an [(N x F) x C x ...] Tensor of outputs. F is the number of frames (views).
+ """
+ assert (
+ x.shape[0] % num_frames == 0
+ ), "input batch size must be dividable by num_frames!"
+ assert (y is not None) == (
+ self.num_classes is not None
+ ), "must specify y if and only if the model is class-conditional"
+
+ hs = []
+
+ t_emb = timestep_embedding(timesteps, self.model_channels, repeat_only=False).to(x.dtype)
+
+ emb = self.time_embed(t_emb)
+
+ if self.num_classes is not None:
+ assert y is not None
+ assert y.shape[0] == x.shape[0]
+ emb = emb + self.label_emb(y)
+
+ # Add camera embeddings
+ if camera is not None:
+ emb = emb + self.camera_embed(camera)
+
+ # imagedream variant
+ if self.ip_dim > 0:
+ x[(num_frames - 1) :: num_frames, :, :, :] = ip_img # place at [4, 9]
+ ip_emb = self.image_embed(ip)
+ context = torch.cat((context, ip_emb), 1)
+
+ h = x
+ for module in self.input_blocks:
+ h = module(h, emb, context, num_frames=num_frames)
+ hs.append(h)
+ h = self.middle_block(h, emb, context, num_frames=num_frames)
+ for module in self.output_blocks:
+ h = torch.cat([h, hs.pop()], dim=1)
+ h = module(h, emb, context, num_frames=num_frames)
+ h = h.type(x.dtype)
+ if self.predict_codebook_ids:
+ return self.id_predictor(h)
+ else:
+ return self.out(h)
\ No newline at end of file
diff --git a/lgm/mvdream/pipeline_mvdream.py b/lgm/mvdream/pipeline_mvdream.py
new file mode 100644
index 0000000..1cf37f4
--- /dev/null
+++ b/lgm/mvdream/pipeline_mvdream.py
@@ -0,0 +1,559 @@
+import torch
+import torch.nn.functional as F
+import inspect
+import numpy as np
+from typing import Callable, List, Optional, Union
+from transformers import CLIPTextModel, CLIPTokenizer, CLIPVisionModel, CLIPImageProcessor
+from diffusers import AutoencoderKL, DiffusionPipeline
+from diffusers.utils import (
+ deprecate,
+ is_accelerate_available,
+ is_accelerate_version,
+ logging,
+)
+from diffusers.configuration_utils import FrozenDict
+from diffusers.schedulers import DDIMScheduler
+from diffusers.utils.torch_utils import randn_tensor
+
+from lgm.mvdream.mv_unet import MultiViewUNetModel, get_camera
+
+logger = logging.get_logger(__name__) # pylint: disable=invalid-name
+
+
+class MVDreamPipeline(DiffusionPipeline):
+
+ _optional_components = ["feature_extractor", "image_encoder"]
+
+ def __init__(
+ self,
+ vae: AutoencoderKL,
+ unet: MultiViewUNetModel,
+ tokenizer: CLIPTokenizer,
+ text_encoder: CLIPTextModel,
+ scheduler: DDIMScheduler,
+ # imagedream variant
+ feature_extractor: CLIPImageProcessor,
+ image_encoder: CLIPVisionModel,
+ requires_safety_checker: bool = False,
+ ):
+ super().__init__()
+
+ if hasattr(scheduler.config, "steps_offset") and scheduler.config.steps_offset != 1: # type: ignore
+ deprecation_message = (
+ f"The configuration file of this scheduler: {scheduler} is outdated. `steps_offset`"
+ f" should be set to 1 instead of {scheduler.config.steps_offset}. Please make sure " # type: ignore
+ "to update the config accordingly as leaving `steps_offset` might led to incorrect results"
+ " in future versions. If you have downloaded this checkpoint from the Hugging Face Hub,"
+ " it would be very nice if you could open a Pull request for the `scheduler/scheduler_config.json`"
+ " file"
+ )
+ deprecate(
+ "steps_offset!=1", "1.0.0", deprecation_message, standard_warn=False
+ )
+ new_config = dict(scheduler.config)
+ new_config["steps_offset"] = 1
+ scheduler._internal_dict = FrozenDict(new_config)
+
+ if hasattr(scheduler.config, "clip_sample") and scheduler.config.clip_sample is True: # type: ignore
+ deprecation_message = (
+ f"The configuration file of this scheduler: {scheduler} has not set the configuration `clip_sample`."
+ " `clip_sample` should be set to False in the configuration file. Please make sure to update the"
+ " config accordingly as not setting `clip_sample` in the config might lead to incorrect results in"
+ " future versions. If you have downloaded this checkpoint from the Hugging Face Hub, it would be very"
+ " nice if you could open a Pull request for the `scheduler/scheduler_config.json` file"
+ )
+ deprecate(
+ "clip_sample not set", "1.0.0", deprecation_message, standard_warn=False
+ )
+ new_config = dict(scheduler.config)
+ new_config["clip_sample"] = False
+ scheduler._internal_dict = FrozenDict(new_config)
+
+ self.register_modules(
+ vae=vae,
+ unet=unet,
+ scheduler=scheduler,
+ tokenizer=tokenizer,
+ text_encoder=text_encoder,
+ feature_extractor=feature_extractor,
+ image_encoder=image_encoder,
+ )
+ self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
+ self.register_to_config(requires_safety_checker=requires_safety_checker)
+
+ def enable_vae_slicing(self):
+ r"""
+ Enable sliced VAE decoding.
+
+ When this option is enabled, the VAE will split the input tensor in slices to compute decoding in several
+ steps. This is useful to save some memory and allow larger batch sizes.
+ """
+ self.vae.enable_slicing()
+
+ def disable_vae_slicing(self):
+ r"""
+ Disable sliced VAE decoding. If `enable_vae_slicing` was previously invoked, this method will go back to
+ computing decoding in one step.
+ """
+ self.vae.disable_slicing()
+
+ def enable_vae_tiling(self):
+ r"""
+ Enable tiled VAE decoding.
+
+ When this option is enabled, the VAE will split the input tensor into tiles to compute decoding and encoding in
+ several steps. This is useful to save a large amount of memory and to allow the processing of larger images.
+ """
+ self.vae.enable_tiling()
+
+ def disable_vae_tiling(self):
+ r"""
+ Disable tiled VAE decoding. If `enable_vae_tiling` was previously invoked, this method will go back to
+ computing decoding in one step.
+ """
+ self.vae.disable_tiling()
+
+ def enable_sequential_cpu_offload(self, gpu_id=0):
+ r"""
+ Offloads all models to CPU using accelerate, significantly reducing memory usage. When called, unet,
+ text_encoder, vae and safety checker have their state dicts saved to CPU and then are moved to a
+ `torch.device('meta') and loaded to GPU only when their specific submodule has its `forward` method called.
+ Note that offloading happens on a submodule basis. Memory savings are higher than with
+ `enable_model_cpu_offload`, but performance is lower.
+ """
+ if is_accelerate_available() and is_accelerate_version(">=", "0.14.0"):
+ from accelerate import cpu_offload
+ else:
+ raise ImportError(
+ "`enable_sequential_cpu_offload` requires `accelerate v0.14.0` or higher"
+ )
+
+ device = torch.device(f"cuda:{gpu_id}")
+
+ if self.device.type != "cpu":
+ self.to("cpu", silence_dtype_warnings=True)
+ torch.cuda.empty_cache() # otherwise we don't see the memory savings (but they probably exist)
+
+ for cpu_offloaded_model in [self.unet, self.text_encoder, self.vae]:
+ cpu_offload(cpu_offloaded_model, device)
+
+ def enable_model_cpu_offload(self, gpu_id=0):
+ r"""
+ Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared
+ to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward`
+ method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with
+ `enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`.
+ """
+ if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
+ from accelerate import cpu_offload_with_hook
+ else:
+ raise ImportError(
+ "`enable_model_offload` requires `accelerate v0.17.0` or higher."
+ )
+
+ device = torch.device(f"cuda:{gpu_id}")
+
+ if self.device.type != "cpu":
+ self.to("cpu", silence_dtype_warnings=True)
+ torch.cuda.empty_cache() # otherwise we don't see the memory savings (but they probably exist)
+
+ hook = None
+ for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]:
+ _, hook = cpu_offload_with_hook(
+ cpu_offloaded_model, device, prev_module_hook=hook
+ )
+
+ # We'll offload the last model manually.
+ self.final_offload_hook = hook
+
+ @property
+ def _execution_device(self):
+ r"""
+ Returns the device on which the pipeline's models will be executed. After calling
+ `pipeline.enable_sequential_cpu_offload()` the execution device can only be inferred from Accelerate's module
+ hooks.
+ """
+ if not hasattr(self.unet, "_hf_hook"):
+ return self.device
+ for module in self.unet.modules():
+ if (
+ hasattr(module, "_hf_hook")
+ and hasattr(module._hf_hook, "execution_device")
+ and module._hf_hook.execution_device is not None
+ ):
+ return torch.device(module._hf_hook.execution_device)
+ return self.device
+
+ def _encode_prompt(
+ self,
+ prompt,
+ device,
+ num_images_per_prompt,
+ do_classifier_free_guidance: bool,
+ negative_prompt=None,
+ ):
+ r"""
+ Encodes the prompt into text encoder hidden states.
+
+ Args:
+ prompt (`str` or `List[str]`, *optional*):
+ prompt to be encoded
+ device: (`torch.device`):
+ torch device
+ num_images_per_prompt (`int`):
+ number of images that should be generated per prompt
+ do_classifier_free_guidance (`bool`):
+ whether to use classifier free guidance or not
+ negative_prompt (`str` or `List[str]`, *optional*):
+ The prompt or prompts not to guide the image generation. If not defined, one has to pass
+ `negative_prompt_embeds`. instead. If not defined, one has to pass `negative_prompt_embeds`. instead.
+ Ignored when not using guidance (i.e., ignored if `guidance_scale` is less than `1`).
+ prompt_embeds (`torch.FloatTensor`, *optional*):
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
+ provided, text embeddings will be generated from `prompt` input argument.
+ negative_prompt_embeds (`torch.FloatTensor`, *optional*):
+ Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
+ weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
+ argument.
+ """
+ if prompt is not None and isinstance(prompt, str):
+ batch_size = 1
+ elif prompt is not None and isinstance(prompt, list):
+ batch_size = len(prompt)
+ else:
+ raise ValueError(
+ f"`prompt` should be either a string or a list of strings, but got {type(prompt)}."
+ )
+
+ text_inputs = self.tokenizer(
+ prompt,
+ padding="max_length",
+ max_length=self.tokenizer.model_max_length,
+ truncation=True,
+ return_tensors="pt",
+ )
+ text_input_ids = text_inputs.input_ids
+ untruncated_ids = self.tokenizer(
+ prompt, padding="longest", return_tensors="pt"
+ ).input_ids
+
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(
+ text_input_ids, untruncated_ids
+ ):
+ removed_text = self.tokenizer.batch_decode(
+ untruncated_ids[:, self.tokenizer.model_max_length - 1 : -1]
+ )
+ logger.warning(
+ "The following part of your input was truncated because CLIP can only handle sequences up to"
+ f" {self.tokenizer.model_max_length} tokens: {removed_text}"
+ )
+
+ if (
+ hasattr(self.text_encoder.config, "use_attention_mask")
+ and self.text_encoder.config.use_attention_mask
+ ):
+ attention_mask = text_inputs.attention_mask.to(device)
+ else:
+ attention_mask = None
+
+ prompt_embeds = self.text_encoder(
+ text_input_ids.to(device),
+ attention_mask=attention_mask,
+ )
+ prompt_embeds = prompt_embeds[0]
+
+ prompt_embeds = prompt_embeds.to(dtype=self.text_encoder.dtype, device=device)
+
+ bs_embed, seq_len, _ = prompt_embeds.shape
+ # duplicate text embeddings for each generation per prompt, using mps friendly method
+ prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
+ prompt_embeds = prompt_embeds.view(
+ bs_embed * num_images_per_prompt, seq_len, -1
+ )
+
+ # get unconditional embeddings for classifier free guidance
+ if do_classifier_free_guidance:
+ uncond_tokens: List[str]
+ if negative_prompt is None:
+ uncond_tokens = [""] * batch_size
+ elif type(prompt) is not type(negative_prompt):
+ raise TypeError(
+ f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
+ f" {type(prompt)}."
+ )
+ elif isinstance(negative_prompt, str):
+ uncond_tokens = [negative_prompt]
+ elif batch_size != len(negative_prompt):
+ raise ValueError(
+ f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
+ f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
+ " the batch size of `prompt`."
+ )
+ else:
+ uncond_tokens = negative_prompt
+
+ max_length = prompt_embeds.shape[1]
+ uncond_input = self.tokenizer(
+ uncond_tokens,
+ padding="max_length",
+ max_length=max_length,
+ truncation=True,
+ return_tensors="pt",
+ )
+
+ if (
+ hasattr(self.text_encoder.config, "use_attention_mask")
+ and self.text_encoder.config.use_attention_mask
+ ):
+ attention_mask = uncond_input.attention_mask.to(device)
+ else:
+ attention_mask = None
+
+ negative_prompt_embeds = self.text_encoder(
+ uncond_input.input_ids.to(device),
+ attention_mask=attention_mask,
+ )
+ negative_prompt_embeds = negative_prompt_embeds[0]
+
+ # duplicate unconditional embeddings for each generation per prompt, using mps friendly method
+ seq_len = negative_prompt_embeds.shape[1]
+
+ negative_prompt_embeds = negative_prompt_embeds.to(
+ dtype=self.text_encoder.dtype, device=device
+ )
+
+ negative_prompt_embeds = negative_prompt_embeds.repeat(
+ 1, num_images_per_prompt, 1
+ )
+ negative_prompt_embeds = negative_prompt_embeds.view(
+ batch_size * num_images_per_prompt, seq_len, -1
+ )
+
+ # For classifier free guidance, we need to do two forward passes.
+ # Here we concatenate the unconditional and text embeddings into a single batch
+ # to avoid doing two forward passes
+ prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
+
+ return prompt_embeds
+
+ def decode_latents(self, latents):
+ latents = 1 / self.vae.config.scaling_factor * latents
+ image = self.vae.decode(latents).sample
+ image = (image / 2 + 0.5).clamp(0, 1)
+ # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16
+ image = image.cpu().permute(0, 2, 3, 1).float().numpy()
+ return image
+
+ def prepare_extra_step_kwargs(self, generator, eta):
+ # prepare extra kwargs for the scheduler step, since not all schedulers have the same signature
+ # eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers.
+ # eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502
+ # and should be between [0, 1]
+
+ accepts_eta = "eta" in set(
+ inspect.signature(self.scheduler.step).parameters.keys()
+ )
+ extra_step_kwargs = {}
+ if accepts_eta:
+ extra_step_kwargs["eta"] = eta
+
+ # check if the scheduler accepts generator
+ accepts_generator = "generator" in set(
+ inspect.signature(self.scheduler.step).parameters.keys()
+ )
+ if accepts_generator:
+ extra_step_kwargs["generator"] = generator
+ return extra_step_kwargs
+
+ def prepare_latents(
+ self,
+ batch_size,
+ num_channels_latents,
+ height,
+ width,
+ dtype,
+ device,
+ generator,
+ latents=None,
+ ):
+ shape = (
+ batch_size,
+ num_channels_latents,
+ height // self.vae_scale_factor,
+ width // self.vae_scale_factor,
+ )
+ if isinstance(generator, list) and len(generator) != batch_size:
+ raise ValueError(
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
+ )
+
+ if latents is None:
+ latents = randn_tensor(
+ shape, generator=generator, device=device, dtype=dtype
+ )
+ else:
+ latents = latents.to(device)
+
+ # scale the initial noise by the standard deviation required by the scheduler
+ latents = latents * self.scheduler.init_noise_sigma
+ return latents
+
+ def encode_image(self, image, device, num_images_per_prompt):
+ dtype = next(self.image_encoder.parameters()).dtype
+
+ if image.dtype == np.float32:
+ image = (image * 255).astype(np.uint8)
+
+ image = self.feature_extractor(image, return_tensors="pt").pixel_values
+ image = image.to(device=device, dtype=dtype)
+
+ image_embeds = self.image_encoder(image, output_hidden_states=True).hidden_states[-2]
+ image_embeds = image_embeds.repeat_interleave(num_images_per_prompt, dim=0)
+
+ return torch.zeros_like(image_embeds), image_embeds
+
+ def encode_image_latents(self, image, device, num_images_per_prompt):
+
+ dtype = next(self.image_encoder.parameters()).dtype
+
+ image = torch.from_numpy(image).unsqueeze(0).permute(0, 3, 1, 2).to(device=device) # [1, 3, H, W]
+ image = 2 * image - 1
+ image = F.interpolate(image, (256, 256), mode='bilinear', align_corners=False)
+ image = image.to(dtype=dtype)
+
+ posterior = self.vae.encode(image).latent_dist
+ latents = posterior.sample() * self.vae.config.scaling_factor # [B, C, H, W]
+ latents = latents.repeat_interleave(num_images_per_prompt, dim=0)
+
+ return torch.zeros_like(latents), latents
+
+ @torch.no_grad()
+ def __call__(
+ self,
+ prompt: str = "",
+ image: Optional[np.ndarray] = None,
+ height: int = 256,
+ width: int = 256,
+ elevation: float = 0,
+ num_inference_steps: int = 50,
+ guidance_scale: float = 7.0,
+ negative_prompt: str = "",
+ num_images_per_prompt: int = 1,
+ eta: float = 0.0,
+ generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
+ output_type: Optional[str] = "numpy", # pil, numpy, latents
+ callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,
+ callback_steps: int = 1,
+ num_frames: int = 4,
+ device=torch.device("cuda:0"),
+ ):
+ self.unet = self.unet.to(device=device)
+ self.vae = self.vae.to(device=device)
+ self.text_encoder = self.text_encoder.to(device=device)
+
+ # here `guidance_scale` is defined analog to the guidance weight `w` of equation (2)
+ # of the Imagen paper: https://arxiv.org/pdf/2205.11487.pdf . `guidance_scale = 1`
+ # corresponds to doing no classifier free guidance.
+ do_classifier_free_guidance = guidance_scale > 1.0
+
+ # Prepare timesteps
+ self.scheduler.set_timesteps(num_inference_steps, device=device)
+ timesteps = self.scheduler.timesteps
+
+ # imagedream variant
+ if image is not None:
+ assert isinstance(image, np.ndarray) and image.dtype == np.float32
+ self.image_encoder = self.image_encoder.to(device=device)
+ image_embeds_neg, image_embeds_pos = self.encode_image(image, device, num_images_per_prompt)
+ image_latents_neg, image_latents_pos = self.encode_image_latents(image, device, num_images_per_prompt)
+
+ _prompt_embeds = self._encode_prompt(
+ prompt=prompt,
+ device=device,
+ num_images_per_prompt=num_images_per_prompt,
+ do_classifier_free_guidance=do_classifier_free_guidance,
+ negative_prompt=negative_prompt,
+ ) # type: ignore
+ prompt_embeds_neg, prompt_embeds_pos = _prompt_embeds.chunk(2)
+
+ # Prepare latent variables
+ actual_num_frames = num_frames if image is None else num_frames + 1
+ latents: torch.Tensor = self.prepare_latents(
+ actual_num_frames * num_images_per_prompt,
+ 4,
+ height,
+ width,
+ prompt_embeds_pos.dtype,
+ device,
+ generator,
+ None,
+ )
+
+ if image is not None:
+ camera = get_camera(num_frames, elevation=elevation, extra_view=True).to(dtype=latents.dtype, device=device)
+ else:
+ camera = get_camera(num_frames, elevation=elevation, extra_view=False).to(dtype=latents.dtype, device=device)
+ camera = camera.repeat_interleave(num_images_per_prompt, dim=0)
+
+ # Prepare extra step kwargs.
+ extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta)
+
+ # Denoising loop
+ num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
+ for i, t in enumerate(timesteps):
+ # expand the latents if we are doing classifier free guidance
+ multiplier = 2 if do_classifier_free_guidance else 1
+ latent_model_input = torch.cat([latents] * multiplier)
+ latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
+
+ unet_inputs = {
+ 'x': latent_model_input,
+ 'timesteps': torch.tensor([t] * actual_num_frames * multiplier, dtype=latent_model_input.dtype, device=device),
+ 'context': torch.cat([prompt_embeds_neg] * actual_num_frames + [prompt_embeds_pos] * actual_num_frames),
+ 'num_frames': actual_num_frames,
+ 'camera': torch.cat([camera] * multiplier),
+ }
+
+ if image is not None:
+ unet_inputs['ip'] = torch.cat([image_embeds_neg] * actual_num_frames + [image_embeds_pos] * actual_num_frames)
+ unet_inputs['ip_img'] = torch.cat([image_latents_neg] + [image_latents_pos]) # no repeat
+
+ # predict the noise residual
+ noise_pred = self.unet.forward(**unet_inputs)
+
+ # perform guidance
+ if do_classifier_free_guidance:
+ noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
+ noise_pred = noise_pred_uncond + guidance_scale * (
+ noise_pred_text - noise_pred_uncond
+ )
+
+ # compute the previous noisy sample x_t -> x_t-1
+ latents: torch.Tensor = self.scheduler.step(
+ noise_pred, t, latents, **extra_step_kwargs, return_dict=False
+ )[0]
+
+ # call the callback, if provided
+ if i == len(timesteps) - 1 or (
+ (i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0
+ ):
+ progress_bar.update()
+ if callback is not None and i % callback_steps == 0:
+ callback(i, t, latents) # type: ignore
+
+ # Post-processing
+ if output_type == "latent":
+ image = latents
+ elif output_type == "pil":
+ image = self.decode_latents(latents)
+ image = self.numpy_to_pil(image)
+ else: # numpy
+ image = self.decode_latents(latents)
+
+ # Offload last model to CPU
+ if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
+ self.final_offload_hook.offload()
+
+ return image
\ No newline at end of file
diff --git a/mesh_processer/mesh.py b/mesh_processer/mesh.py
index 6e2f85b..f3082a8 100644
--- a/mesh_processer/mesh.py
+++ b/mesh_processer/mesh.py
@@ -336,7 +336,7 @@ class Mesh:
texture = np.ones((res_H, res_W, 3), dtype=np.float32) * np.array([0.5, 0.5, 0.5])
self.albedo = torch.tensor(texture, dtype=torch.float32, device=self.device)
else:
- self.albedo = prepare_torch_img(self.albedo, res_H, res_W, self.device).squeeze(0).permute(1, 2, 0).contiguous() # (1, 3, H, W) -> (H, W, 3)
+ self.albedo = prepare_torch_img(self.albedo.unsqueeze(0), res_H, res_W, self.device).squeeze(0).permute(1, 2, 0).contiguous() # (1, 3, H, W) -> (H, W, 3)
# aabb
def aabb(self):
diff --git a/nodes.py b/nodes.py
index b88954f..c9a0602 100644
--- a/nodes.py
+++ b/nodes.py
@@ -4,15 +4,17 @@ import re
import math
import copy
from enum import Enum
+from collections import OrderedDict
import folder_paths as comfy_paths
import torch
from torch.utils.data import DataLoader
+import torchvision.transforms.functional as TF
import numpy as np
+from safetensors.torch import load_file
from plyfile import PlyData
-from .shared_utils.common_utils import cstr, parse_save_filename
from .mesh_processer.mesh import Mesh
from .mesh_processer.mesh_utils import (
ply_to_points_cloud,
@@ -26,9 +28,20 @@ from .algorithms.main_3DGS_renderer import GaussianSplattingRenderer
from .algorithms.diff_texturing import DiffTextureBaker
from .algorithms.dmtet import DMTetMesh
from .algorithms.triplane_gaussian_transformers import TGS
+from .algorithms.large_multiview_gaussian_model import LGM
+from .algorithms.nerf_marching_cubes_converter import GSConverterNeRFMarchingCubes
from .tgs.utils.config import ExperimentConfig, load_config
from .tgs.data import CustomImageOrbitDataset
from .tgs.utils.misc import todevice, get_device
+from .lgm.core.options import config_defaults
+from .lgm.mvdream.pipeline_mvdream import MVDreamPipeline
+
+from .shared_utils.image_utils import prepare_torch_img
+from .shared_utils.common_utils import cstr, parse_save_filename, get_list_filenames
+
+DIFFUSERS_PIPE_DICT = OrderedDict([
+ ("MVDreamPipeline", MVDreamPipeline),
+])
ROOT_PATH = os.path.join(comfy_paths.get_folder_paths("custom_nodes")[0], "ComfyUI-3D-Pack")
@@ -688,7 +701,7 @@ class Gaussian_Splatting_Orbit_Renderer:
all_rendered_images, all_rendered_masks = cam_controller.render_all_pose(render_orbit_camera_poses)
all_rendered_images = all_rendered_images.permute(0, 2, 3, 1) # [N, 3, H, W] -> [N, H, W, 3]
- all_rendered_masks = all_rendered_masks.permute(0, 2, 3, 1).squeeze(3) # [N, 1, H, W] -> [N, H, W]
+ all_rendered_masks = all_rendered_masks.squeeze(1) # [N, 1, H, W] -> [N, H, W]
return (all_rendered_images, all_rendered_masks)
@@ -990,14 +1003,15 @@ TGS_CONFIG_PATH = "configs/tgs_config.yaml"
class Load_Triplane_Gaussian_Transformers:
+ checkpoints_dir = "checkpoints/tgs"
+ default_ckpt_name = "model_lvis_rel.ckpt"
+
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
+ "model_name": (get_list_filenames(os.path.join(ROOT_PATH, cls.checkpoints_dir)), ),
},
-
- "optional": {
- }
}
RETURN_TYPES = (
@@ -1009,7 +1023,7 @@ class Load_Triplane_Gaussian_Transformers:
FUNCTION = "load_TGS"
CATEGORY = "Comfy3D/Import|Export"
- def load_TGS(self):
+ def load_TGS(self, model_name):
device = get_device()
@@ -1017,19 +1031,20 @@ class Load_Triplane_Gaussian_Transformers:
cfg: ExperimentConfig = load_config(config_path)
# Download pre-trained model if it not exist locally
- model_name = "model_lvis_rel.ckpt"
- model_path = os.path.join(ROOT_PATH, f"checkpoints/{model_name}")
- if not os.path.isfile(model_path):
+ ckpt_path = os.path.join(ROOT_PATH, self.checkpoints_dir, model_name)
+ if not os.path.isfile(ckpt_path):
from huggingface_hub import hf_hub_download
- hf_hub_download(repo_id="VAST-AI/TriplaneGaussian", local_dir="./checkpoints", filename=model_name, repo_type="model")
-
- cfg.system.weights=model_path
+ hf_hub_download(repo_id="VAST-AI/TriplaneGaussian", local_dir=self.checkpoints_dir, filename=self.default_ckpt_name, repo_type="model")
+ cstr(f"[{self.__class__.__name__}] can't find checkpoint {ckpt_path}, will download it from repo VAST-AI/TriplaneGaussian instead").warning.print()
+ ckpt_path = os.path.join(ROOT_PATH, self.checkpoints_dir, self.default_ckpt_name)
+
+ cfg.system.weights=ckpt_path
tgs_model = TGS(cfg=cfg.system).to(device)
save_path = os.path.join(ROOT_PATH, "outputs")
tgs_model.set_save_dir(save_path)
- cstr(f"[{self.__class__.__name__}] loaded model ckpt from {model_path}").msg.print()
+ cstr(f"[{self.__class__.__name__}] loaded model ckpt from {ckpt_path}").msg.print()
return (tgs_model, )
@@ -1046,7 +1061,6 @@ class Triplane_Gaussian_Transformers:
},
}
- OUTPUT_NODE = True
RETURN_TYPES = (
"GS_PLY",
)
@@ -1090,4 +1104,217 @@ class Triplane_Gaussian_Transformers:
"""
return (gs_ply[0],)
-
\ No newline at end of file
+
+class Load_Diffusers_Pipeline:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "diffusers_pipeline_name": (list(DIFFUSERS_PIPE_DICT.keys()),),
+ "pretrained_model_name": ("STRING", {"default": "ashawkey/imagedream-ipmv-diffusers", "multiline": False}),
+ },
+ }
+
+ RETURN_TYPES = (
+ "DIFFUSERS_PIPE",
+ )
+ RETURN_NAMES = (
+ "pipe",
+ )
+ FUNCTION = "load_diffusers_pipe"
+ CATEGORY = "Comfy3D/Import|Export"
+
+ def load_diffusers_pipe(self, diffusers_pipeline_name, pretrained_model_name):
+
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+
+ diffusers_pipeline_class = DIFFUSERS_PIPE_DICT[diffusers_pipeline_name]
+
+ # load image dream
+ pipe = diffusers_pipeline_class.from_pretrained(
+ pretrained_model_name, # remote weights
+ torch_dtype=torch.float16,
+ trust_remote_code=True,
+ # local_files_only=True,
+ ).to(device)
+
+ return (pipe,)
+
+class MVDream_Model:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "mvdream_pipe": ("DIFFUSERS_PIPE",),
+ "reference_image": ("IMAGE",),
+ "reference_mask": ("MASK",),
+ "prompt": ("STRING", {
+ "default": "",
+ "multiline": True
+ }),
+ "prompt_neg": ("STRING", {
+ "default": "ugly, blurry, pixelated obscure, unnatural colors, poor lighting, dull, unclear, cropped, lowres, low quality, artifacts, duplicate",
+ "multiline": True
+ }),
+ "mv_guidance_scale": ("FLOAT", {"default": 5.0, "min": 0.0, "step": 0.01}),
+ "num_inference_steps": ("INT", {"default": 30, "min": 1}),
+ "elevation": ("FLOAT", {"default": 0.0, "min": ELEVATION_MIN, "max": ELEVATION_MAX, 'step': 0.0001}),
+ },
+ }
+
+ RETURN_TYPES = (
+ "IMAGE",
+ )
+ RETURN_NAMES = (
+ "multiview_images",
+ )
+ FUNCTION = "run_mvdream"
+ CATEGORY = "Comfy3D/Algorithm"
+
+ def run_mvdream(
+ self,
+ mvdream_pipe,
+ reference_image, # [1, H, W, 3]
+ reference_mask, # [1, H, W]
+ prompt,
+ prompt_neg,
+ mv_guidance_scale,
+ num_inference_steps,
+ elevation,
+ ):
+ if len(reference_image.shape) == 4:
+ reference_image = reference_image.squeeze(0)
+ if len(reference_mask.shape) == 3:
+ reference_mask = reference_mask.squeeze(0)
+
+ reference_mask = reference_mask.unsqueeze(2)
+ # give the white background to reference_image
+ reference_image = (reference_image * reference_mask + (1 - reference_mask)).detach().cpu().numpy()
+
+ # generate multi-view images
+ mv_images = mvdream_pipe(prompt, reference_image, negative_prompt=prompt_neg, guidance_scale=mv_guidance_scale, num_inference_steps=num_inference_steps, elevation=elevation)
+ mv_images = torch.from_numpy(np.stack([mv_images[1], mv_images[2], mv_images[3], mv_images[0]], axis=0)).float() # [4, H, W, 3], float32
+
+ return (mv_images,)
+
+class Load_Large_Multiview_Gaussian_Model:
+
+ checkpoints_dir = "checkpoints/lgm"
+ default_ckpt_name = "model_fp16.safetensors"
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "model_name": (get_list_filenames(os.path.join(ROOT_PATH, cls.checkpoints_dir)), ),
+ "lgb_config": (['big', 'default', 'small', 'tiny'], )
+ },
+ }
+
+ RETURN_TYPES = (
+ "LGM_MODEL",
+ )
+ RETURN_NAMES = (
+ "lgm_model",
+ )
+ FUNCTION = "load_LGM"
+ CATEGORY = "Comfy3D/Import|Export"
+
+ def load_LGM(self, model_name, lgb_config):
+
+ lgm_model = LGM(config_defaults[lgb_config])
+
+ # resume pretrained checkpoint
+ ckpt_path = os.path.join(ROOT_PATH, self.checkpoints_dir, model_name)
+ if not os.path.isfile(ckpt_path):
+ from huggingface_hub import hf_hub_download
+ hf_hub_download(repo_id="ashawkey/LGM", local_dir=self.checkpoints_dir, filename=self.default_ckpt_name, repo_type="model")
+ cstr(f"[{self.__class__.__name__}] can't find checkpoint {ckpt_path}, will download it from repo ashawkey/LGM instead").warning.print()
+ ckpt_path = os.path.join(ROOT_PATH, self.checkpoints_dir, self.default_ckpt_name)
+
+ elif ckpt_path.endswith('safetensors'):
+ ckpt = load_file(ckpt_path, device='cpu')
+ else:
+ ckpt = torch.load(ckpt_path, map_location='cpu')
+
+ lgm_model.load_state_dict(ckpt, strict=False)
+ cstr(f"[{self.__class__.__name__}] loaded model ckpt from {ckpt_path}").msg.print()
+
+ # device
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
+ lgm_model = lgm_model.half().to(device)
+ lgm_model.eval()
+
+ return (lgm_model,)
+
+IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406)
+IMAGENET_DEFAULT_STD = (0.229, 0.224, 0.225)
+
+class Large_Multiview_Gaussian_Model:
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "multiview_images": ("IMAGE", ),
+ "lgm_model": ("LGM_MODEL", ),
+ "mvdream_pipe": ("DIFFUSERS_PIPE",),
+ },
+ }
+
+ OUTPUT_NODE = True
+ RETURN_TYPES = (
+ "GS_PLY",
+ )
+ RETURN_NAMES = (
+ "gs_ply",
+ )
+ FUNCTION = "run_LGM"
+ CATEGORY = "Comfy3D/Algorithm"
+
+ @torch.no_grad()
+ def run_LGM(self, multiview_images, lgm_model, mvdream_pipe):
+ device = "cuda"
+ ref_image_torch = prepare_torch_img(multiview_images, lgm_model.opt.input_size, lgm_model.opt.input_size, device) # [4, 3, 256, 256]
+ ref_image_torch = TF.normalize(ref_image_torch, IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD)
+ rays_embeddings = lgm_model.prepare_default_rays(device)
+ ref_image_torch = torch.cat([ref_image_torch, rays_embeddings], dim=1).unsqueeze(0) # [1, 4, 9, 256, 256]
+
+ with torch.autocast(device_type=device, dtype=torch.float16):
+ # generate gaussians
+ gaussians = lgm_model.forward_gaussians(ref_image_torch)
+
+ # convert gaussians to ply
+ gs_ply = lgm_model.gs.to_ply(gaussians)
+
+ return (gs_ply, )
+
+class Convert_3DGS_to_Mesh_with_NeRF_and_Marching_Cubes:
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "gs_ply": ("GS_PLY",),
+ "gs_config": (['big', 'default', 'small', 'tiny'], )
+ },
+ }
+
+ RETURN_TYPES = (
+ "MESH",
+ )
+ RETURN_NAMES = (
+ "mesh",
+ )
+ FUNCTION = "convert_gs_ply"
+ CATEGORY = "Comfy3D/Algorithm"
+
+ def convert_gs_ply(self, gs_ply, gs_config):
+ with torch.inference_mode(False):
+ converter = GSConverterNeRFMarchingCubes(config_defaults[gs_config], gs_ply).cuda()
+ converter.fit_nerf()
+ converter.fit_mesh()
+ converter.fit_mesh_uv()
+
+ return(converter.get_mesh(), )
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 5cf47fc..ab334cf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,27 @@
-#base
+# base
cmake
ninja
-kornia
-OmegaConf
+
+# computational libraries
numpy
einops
scipy
+kornia
opencv-python
+roma
+nerfacc>=0.5.3
+PyMCubes
+git+https://github.com/ashawkey/diff-gaussian-rasterization
+git+https://github.com/NVlabs/nvdiffrast/
+git+https://github.com/facebookresearch/pytorch3d.git@stable
+git+https://github.com/rusty1s/pytorch_scatter.git
+git+https://github.com/YixunLiang/simple-knn.git
+git+https://github.com/ashawkey/kiuikit.git
# for use ML models
diffusers
transformers
+safetensors
# for training differentiable tensors
pytorch_msssim
@@ -26,7 +37,9 @@ pygltflib
xatlas
pymeshlab
-# extra
+# configs & extra
torchtyping
tqdm
jaxtyping
+packaging
+OmegaConf
diff --git a/shared_utils/camera_utils.py b/shared_utils/camera_utils.py
index d079496..af73e8d 100644
--- a/shared_utils/camera_utils.py
+++ b/shared_utils/camera_utils.py
@@ -41,27 +41,6 @@ def look_at(campos, target, opengl=True):
R = np.stack([right_vector, up_vector, forward_vector], axis=1)
return R
-
-# elevation & azimuth to pose (cam2world) matrix
-def orbit_camera(elevation, azimuth, radius=1, is_degree=True, target=None, opengl=True):
- # radius: scalar
- # elevation: scalar, in (-90, 90), from +y to -y is (-90, 90)
- # azimuth: scalar, in (-180, 180), from +z to +x is (0, 90)
- # return: [4, 4], camera pose matrix
- if is_degree:
- elevation = np.deg2rad(elevation)
- azimuth = np.deg2rad(azimuth)
- x = radius * np.cos(elevation) * np.sin(azimuth)
- y = - radius * np.sin(elevation)
- z = radius * np.cos(elevation) * np.cos(azimuth)
- if target is None:
- target = np.zeros([3], dtype=np.float32)
- campos = np.array([x, y, z]) + target # [3]
- T = np.eye(4, dtype=np.float32)
- T[:3, :3] = look_at(campos, target, opengl)
- T[:3, 3] = campos
- return T
-
def get_look_at_camera_pose(target, target_to_cam_offset, look_distance=0.1, opengl=True):
"""
Calculate the pose (cam2world) matrix from target position the camera suppose to look at and offset vector from target to camera
diff --git a/shared_utils/common_utils.py b/shared_utils/common_utils.py
index 699c856..bfa891f 100644
--- a/shared_utils/common_utils.py
+++ b/shared_utils/common_utils.py
@@ -1,4 +1,6 @@
import os
+from os import listdir
+from os.path import isfile, join
import sys
from datetime import datetime
@@ -83,9 +85,9 @@ cstr.color.add_code("error", f"{cstr.color.RED}[Comfy3D] [ERROR] {cstr.color.END
def get_persistent_directory(folder_name):
if sys.platform == "win32":
- folder = os.path.join(os.path.expanduser("~"), "AppData", "Local", folder_name)
+ folder = join(os.path.expanduser("~"), "AppData", "Local", folder_name)
else:
- folder = os.path.join(os.path.expanduser("~"), "." + folder_name)
+ folder = join(os.path.expanduser("~"), "." + folder_name)
os.makedirs(folder, exist_ok=True)
return folder
@@ -96,7 +98,7 @@ def parse_save_filename(save_path, output_directory, supported_extensions, class
filename, file_extension = os.path.splitext(filename)
if file_extension.lower() in supported_extensions:
if not os.path.isabs(save_path):
- folder_path = os.path.join(output_directory, folder_path)
+ folder_path = join(output_directory, folder_path)
os.makedirs(folder_path, exist_ok=True)
@@ -107,10 +109,14 @@ def parse_save_filename(save_path, output_directory, supported_extensions, class
if date_format in filename:
filename = filename.replace(date_format, now.strftime(date_format))
- save_path = os.path.join(folder_path, filename) + file_extension
+ save_path = join(folder_path, filename) + file_extension
cstr(f"[{class_name}] Saving model to {save_path}").msg.print()
return save_path
else:
cstr(f"[{class_name}] File name {filename} does not end with supported file extensions: {supported_extensions}").error.print()
- return None
\ No newline at end of file
+ return None
+
+# Get all files in a folder, if extension_filter is provided then only reture the files with extensions in extension_filter
+def get_list_filenames(directory, extension_filter=None):
+ return [f for f in listdir(directory) if isfile(join(directory, f)) and (extension_filter is None or f.lower().endswith(extension_filter))]
\ No newline at end of file
diff --git a/shared_utils/image_utils.py b/shared_utils/image_utils.py
index 3cc3673..a0ee7f5 100644
--- a/shared_utils/image_utils.py
+++ b/shared_utils/image_utils.py
@@ -3,7 +3,7 @@ import torch.nn.functional as F
import numpy as np
def prepare_torch_img(img, size_H, size_W, device="cuda"):
- # [H, W, C] -> [1, C, H, W]
- img_new = img.permute(2, 0, 1).unsqueeze(0).to(device)
+ # [N, H, W, C] -> [N, C, H, W]
+ img_new = img.permute(0, 3, 1, 2).to(device)
img_new = F.interpolate(img_new, (size_H, size_W), mode="bilinear", align_corners=False).contiguous()
return img_new
\ No newline at end of file