From b95a0e475aa495e6272afd5db9405b542c45f087 Mon Sep 17 00:00:00 2001 From: MrForExample <62230687+MrForExample@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:09:07 +0100 Subject: [PATCH] Integrated Triplane Gaussian Transformers & Better 3DGS Import & Export --- .gitignore | 11 +- README.md | 94 +- .../3DGS_Multi-View_Renders_to_ply.json | 468 +++---- ...o_ply_with_Stack_Orbit_Cam_Poses_node.json | 418 +++--- .../Images_to_3DGS_to_DMTetMesh.json | 612 +++++---- .../Load_3DGS_to_DMTetMesh.json | 244 ++-- ...riplane_Gaussian_Transformers_to_3DGS.json | 242 ++++ .../a_pikachu_with_smily_face-removebg.png | Bin 0 -> 402764 bytes algorithms/diff_texturing.py | 1 + algorithms/main_3DGS_renderer.py | 60 +- algorithms/triplane_gaussian_transformers.py | 167 +++ configs/tgs_config.yaml | 150 +++ install.bat | 9 +- mesh_processer/mesh.py | 4 - mesh_processer/mesh_utils.py | 44 + nodes.py | 261 +++- requirements.txt | 13 +- test.bat | 6 +- tgs/__init__.py | 11 + tgs/data.py | 237 ++++ tgs/models/__init__.py | 0 tgs/models/image_feature.py | 48 + tgs/models/networks.py | 204 +++ tgs/models/pointclouds/LICENSE_POINTNET | 21 + tgs/models/pointclouds/pointnet.py | 121 ++ tgs/models/pointclouds/simplepoint.py | 110 ++ tgs/models/renderer.py | 427 ++++++ tgs/models/snowflake/LICENSE | 21 + tgs/models/snowflake/SPD.py | 68 + tgs/models/snowflake/SPD_crossattn.py | 81 ++ tgs/models/snowflake/SPD_pp.py | 71 + tgs/models/snowflake/attention.py | 239 ++++ tgs/models/snowflake/model_spdpp.py | 188 +++ .../pointnet2_ops/__init__.py | 3 + .../_ext-src/include/ball_query.h | 5 + .../_ext-src/include/cuda_utils.h | 41 + .../_ext-src/include/group_points.h | 5 + .../_ext-src/include/interpolate.h | 10 + .../pointnet2_ops/_ext-src/include/sampling.h | 6 + .../pointnet2_ops/_ext-src/include/utils.h | 25 + .../pointnet2_ops/_ext-src/src/ball_query.cpp | 32 + .../_ext-src/src/ball_query_gpu.cu | 54 + .../pointnet2_ops/_ext-src/src/bindings.cpp | 19 + .../_ext-src/src/group_points.cpp | 62 + .../_ext-src/src/group_points_gpu.cu | 75 ++ .../_ext-src/src/interpolate.cpp | 99 ++ .../_ext-src/src/interpolate_gpu.cu | 154 +++ .../pointnet2_ops/_ext-src/src/sampling.cpp | 87 ++ .../_ext-src/src/sampling_gpu.cu | 229 ++++ .../pointnet2_ops/_version.py | 1 + .../pointnet2_ops/pointnet2_modules.py | 209 +++ .../pointnet2_ops/pointnet2_utils.py | 379 ++++++ .../snowflake/pointnet2_ops_lib/setup.py | 41 + tgs/models/snowflake/skip_transformer.py | 69 + tgs/models/snowflake/utils.py | 741 +++++++++++ tgs/models/tokenizers/dinov2.py | 1175 +++++++++++++++++ tgs/models/tokenizers/image.py | 135 ++ tgs/models/tokenizers/point.py | 29 + tgs/models/tokenizers/triplane.py | 52 + tgs/models/transformers.py | 908 +++++++++++++ tgs/utils/__init__.py | 0 tgs/utils/base.py | 129 ++ tgs/utils/config.py | 74 ++ tgs/utils/misc.py | 88 ++ tgs/utils/ops.py | 279 ++++ tgs/utils/saving.py | 314 +++++ tgs/utils/typing.py | 40 + 67 files changed, 9212 insertions(+), 1008 deletions(-) create mode 100644 _Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS.json create mode 100644 _Example_Workflows/_Example_Inputs_Files/a_pikachu_with_smily_face-removebg.png create mode 100644 algorithms/triplane_gaussian_transformers.py create mode 100644 configs/tgs_config.yaml create mode 100644 tgs/__init__.py create mode 100644 tgs/data.py create mode 100644 tgs/models/__init__.py create mode 100644 tgs/models/image_feature.py create mode 100644 tgs/models/networks.py create mode 100644 tgs/models/pointclouds/LICENSE_POINTNET create mode 100644 tgs/models/pointclouds/pointnet.py create mode 100644 tgs/models/pointclouds/simplepoint.py create mode 100644 tgs/models/renderer.py create mode 100644 tgs/models/snowflake/LICENSE create mode 100644 tgs/models/snowflake/SPD.py create mode 100644 tgs/models/snowflake/SPD_crossattn.py create mode 100644 tgs/models/snowflake/SPD_pp.py create mode 100644 tgs/models/snowflake/attention.py create mode 100644 tgs/models/snowflake/model_spdpp.py create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/__init__.py create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/ball_query.h create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/cuda_utils.h create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/group_points.h create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/interpolate.h create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/sampling.h create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/utils.h create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query.cpp create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query_gpu.cu create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/bindings.cpp create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points.cpp create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points_gpu.cu create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate.cpp create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate_gpu.cu create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling.cpp create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling_gpu.cu create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_version.py create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_modules.py create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_utils.py create mode 100644 tgs/models/snowflake/pointnet2_ops_lib/setup.py create mode 100644 tgs/models/snowflake/skip_transformer.py create mode 100644 tgs/models/snowflake/utils.py create mode 100644 tgs/models/tokenizers/dinov2.py create mode 100644 tgs/models/tokenizers/image.py create mode 100644 tgs/models/tokenizers/point.py create mode 100644 tgs/models/tokenizers/triplane.py create mode 100644 tgs/models/transformers.py create mode 100644 tgs/utils/__init__.py create mode 100644 tgs/utils/base.py create mode 100644 tgs/utils/config.py create mode 100644 tgs/utils/misc.py create mode 100644 tgs/utils/ops.py create mode 100644 tgs/utils/saving.py create mode 100644 tgs/utils/typing.py diff --git a/.gitignore b/.gitignore index 7c0ae4b..0c507ed 100644 --- a/.gitignore +++ b/.gitignore @@ -155,4 +155,13 @@ cython_debug/ diff-gaussian-rasterization/ # Nvdiffrast -nvdiffrast/ \ No newline at end of file +nvdiffrast/ + +# Model files +*.ckpt +*.bin +*.pth +*.safetensors + +# Ignore test outputs +outputs/ \ No newline at end of file diff --git a/README.md b/README.md index 0cce225..3c422f2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,50 @@ ps: I'll show some generated result soon - I tried install this package with ComfyUI embed python env first, but I can't find a way to build CUDA related libraries, e.g. diff-gaussian-rasterization, nvdiffrast, simple-knn. - Torch version: 2.1.2+cu121/V.2.1.2+cu118 +## Currently support: +- For use case please check [example workflows](./_Example_Workflows/) + - **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 +- Triplane Gaussian Transformers: + - Enable single image to 3D Gaussian, then you can use Deep Marching Tetrahedrons node to convert it to mesh + + +- Stack Orbit Camera Poses node to 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 + - You can use it to generate the orbit camera poses and directly input to other 3D process node (e.g. GaussianSplatting and BakeTextureToMesh) + - Example usage: + + + +- Load 3D file (.obj, .ply, .glb) + - Also have node that support save and load 3DGS .ply file +- 3D Gaussian Splatting, with: + - [Improved Differential Gaussian Rasterization](https://github.com/ashawkey/diff-gaussian-rasterization) + - Better Compactness-based Densification method from [Gsgen](https://gsgen3d.github.io/), + - Support initialize gaussians from given 3D mesh (Optional) + - Support mini-batch optimazation + - Multi-View images as inputs + - Export to .ply support + +- Bake Multi-View images into UVTexture of given 3D mesh using [Nvdiffrast](https://github.com/NVlabs/nvdiffrast), supports: + - Export to .obj, .ply, .glb + +- Deep Marching Tetrahedrons + - Allow convert 3DGS .ply file to 3D mesh
+ *Note: I didn't spent time to turn the hyperprameters yet, the result will be improved in the future!* + +## Roadmap: +- [x] Add DMTet algorithm to allow convertion 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) + +- [ ] Add interactive 3D UI inside ComfuUI to visulaize training and generated results for 3D representations + +- [ ] Improve DMTet result & add support to training it with images(RGB, Alpha, Normal Map) + +- [ ] Add a general SDS/VSD Optimization algorithm to allow training 3D representations with diffusion model, *The real fun begins here* ;) + +- [ ] Add a few best Nerf algorithms (No idea yet, [instant-ngp](https://github.com/NVlabs/instant-ngp) maybe?) + ## Install: Assume you have already downloaded [ComfyUI](https://github.com/comfyanonymous/ComfyUI) @@ -56,53 +100,21 @@ install.bat git clone --recursive https://github.com/NVlabs/nvdiffrast/` pip install ./nvdiffrast + + # Install pointnet2_ops + cd tgs/models/snowflake/pointnet2_ops_lib && python setup.py install && cd ../../../../ + + # Install pytorch_scatter + pip install git+https://github.com/rusty1s/pytorch_scatter.git + + # Install pytorch3d + pip install git+https://github.com/facebookresearch/pytorch3d.git@stable ``` ## Run: Copy the files inside folder [__New_ComfyUI_Bats](./_New_ComfyUI_Bats/) to your ComfyUI root directory, and double click run_nvidia_gpu_miniconda.bat to start ComfyUI! - Alternatively you can just activate the Conda env: python_miniconda_env\ComfyUI, and go to your ComfyUI root directory then run command `python ./ComfyUI/main.py` -## Currently support: -- For use case please check [example workflows](./_Example_Workflows/) - - **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 -- Stack Orbit Camera Poses node to 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 - - You can use it to generate the orbit camera poses and directly input to other 3D process node (e.g. GaussianSplatting and BakeTextureToMesh) - - Example usage: - - - -- Load 3D file (.obj, .ply, .glb) - - Also have node that support save and load 3DGS .ply file -- 3D Gaussian Splatting, with: - - [Improved Differential Gaussian Rasterization](https://github.com/ashawkey/diff-gaussian-rasterization) - - Better Compactness-based Densification method from [Gsgen](https://gsgen3d.github.io/), - - Support initialize gaussians from given 3D mesh (Optional) - - Support mini-batch optimazation - - Multi-View images as inputs - - Export to .ply support - -- Bake Multi-View images into UVTexture of given 3D mesh using [Nvdiffrast](https://github.com/NVlabs/nvdiffrast), supports: - - Export to .obj, .ply, .glb - -- Deep Marching Tetrahedrons - - Allow convert 3DGS .ply file to 3D mesh
- *Note: I didn't spent time to turn the hyperprameters yet, the result will be improved in the future!* - -## Roadmap: -- [x] Add DMTet algorithm to allow convertion from points cloud(Gaussian/.ply) to mesh (.obj, .ply, .glb) - -- [ ] Integrate [Triplane Meets Gaussian Splatting: Fast and Generalizable Single-View 3D Reconstruction with Transformers](https://github.com/VAST-AI-Research/TriplaneGaussian) - -- [ ] Add interactive 3D UI inside ComfuUI to visulaize training and generated results for 3D representations - -- [ ] Improve DMTet result & add support to training it with images(RGB, Alpha, Normal Map) - -- [ ] Add a general SDS/VSD Optimization algorithm to allow training 3D representations with diffusion model, *The real fun begins here* ;) - -- [ ] Add a few best Nerf algorithms (No idea yet, [instant-ngp](https://github.com/NVlabs/instant-ngp) maybe?) - - ## Tips * The world & camera coordinate system is the same as OpenGL: ``` diff --git a/_Example_Workflows/3DGS_Multi-View_Renders_to_ply.json b/_Example_Workflows/3DGS_Multi-View_Renders_to_ply.json index 400b719..9bc5965 100644 --- a/_Example_Workflows/3DGS_Multi-View_Renders_to_ply.json +++ b/_Example_Workflows/3DGS_Multi-View_Renders_to_ply.json @@ -1,6 +1,6 @@ { - "last_node_id": 19, - "last_link_id": 22, + "last_node_id": 21, + "last_link_id": 26, "nodes": [ { "id": 10, @@ -63,191 +63,6 @@ "Node name for S&R": "MaskToImage" } }, - { - "id": 17, - "type": "[ComfyUI-3D] Save 3DGS", - "pos": [ - 2122, - 54 - ], - "size": { - "0": 315, - "1": 58 - }, - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "link": 21 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Save 3DGS" - }, - "widgets_values": [ - "MeshTest\\3DGS_01.ply" - ] - }, - { - "id": 8, - "type": "InvertMask", - "pos": [ - 746, - -14 - ], - "size": { - "0": 210, - "1": 26 - }, - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [ - { - "name": "mask", - "type": "MASK", - "link": 2 - } - ], - "outputs": [ - { - "name": "MASK", - "type": "MASK", - "links": [ - 8, - 18 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "InvertMask" - } - }, - { - "id": 4, - "type": "[ComfyUI-3D] Generate Orbit Camera Poses", - "pos": [ - 830, - 132 - ], - "size": { - "0": 400, - "1": 200 - }, - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [ - { - "name": "reference_images", - "type": "IMAGE", - "link": 1 - } - ], - "outputs": [ - { - "name": "orbit_camposes", - "type": "ORBIT_CAMPOSES", - "links": [ - 19 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Generate Orbit Camera Poses" - }, - "widgets_values": [ - "([0:120], 1.75, 0, 0, 360)" - ] - }, - { - "id": 18, - "type": "[ComfyUI-3D] Gaussian Splatting", - "pos": [ - 1486, - 53 - ], - "size": { - "0": 468.5999755859375, - "1": 718 - }, - "flags": { - "collapsed": false - }, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "reference_images", - "type": "IMAGE", - "link": 17 - }, - { - "name": "reference_masks", - "type": "MASK", - "link": 18 - }, - { - "name": "reference_orbit_camera_poses", - "type": "ORBIT_CAMPOSES", - "link": 19 - }, - { - "name": "mesh_to_initialize_gaussian", - "type": "MESH", - "link": null - } - ], - "outputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "links": [ - 21 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" - }, - "widgets_values": [ - 49.1, - 1000, - 1, - 10000, - 0.2, - 3, - 0, - 0, - 0.5, - 0.01, - 0.05, - 0.005, - 0.005, - 0.001, - 0.00002, - 0.02, - 500, - 5000, - 3, - 0.01, - 100, - 100000, - 100, - 700, - 0.01, - 0 - ] - }, { "id": 13, "type": "[ComfyUI-3D] Load 3D Mesh", @@ -283,6 +98,31 @@ false ] }, + { + "id": 19, + "type": "PreviewImage", + "pos": [ + 420, + -320 + ], + "size": [ + 210, + 246 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 22 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, { "id": 7, "type": "VHS_LoadImages", @@ -303,8 +143,8 @@ "type": "IMAGE", "links": [ 1, - 17, - 22 + 22, + 23 ], "shape": 3, "slot_index": 0 @@ -349,29 +189,197 @@ } }, { - "id": 19, - "type": "PreviewImage", + "id": 8, + "type": "InvertMask", "pos": [ - 420, - -320 + 746, + -14 ], "size": { "0": 210, "1": 26 }, "flags": {}, - "order": 3, + "order": 4, "mode": 0, "inputs": [ { - "name": "images", - "type": "IMAGE", - "link": 22 + "name": "mask", + "type": "MASK", + "link": 2 + } + ], + "outputs": [ + { + "name": "MASK", + "type": "MASK", + "links": [ + 8, + 24 + ], + "shape": 3, + "slot_index": 0 } ], "properties": { - "Node name for S&R": "PreviewImage" + "Node name for S&R": "InvertMask" } + }, + { + "id": 4, + "type": "[ComfyUI-3D] Generate Orbit Camera Poses", + "pos": [ + 830, + 132 + ], + "size": { + "0": 400, + "1": 200 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "reference_images", + "type": "IMAGE", + "link": 1 + } + ], + "outputs": [ + { + "name": "orbit_camposes", + "type": "ORBIT_CAMPOSES", + "links": [ + 25 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Generate Orbit Camera Poses" + }, + "widgets_values": [ + "([0:120], 1.75, 0, 0, 360)" + ] + }, + { + "id": 20, + "type": "[ComfyUI-3D] Gaussian Splatting", + "pos": [ + 1381, + 54 + ], + "size": { + "0": 443.4000244140625, + "1": 758 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "reference_images", + "type": "IMAGE", + "link": 23 + }, + { + "name": "reference_masks", + "type": "MASK", + "link": 24 + }, + { + "name": "reference_orbit_camera_poses", + "type": "ORBIT_CAMPOSES", + "link": 25 + }, + { + "name": "points_cloud", + "type": "POINTCLOUD", + "link": null + }, + { + "name": "ply_to_initialize_gaussian", + "type": "GS_PLY", + "link": null + }, + { + "name": "mesh_to_initialize_gaussian", + "type": "MESH", + "link": null + } + ], + "outputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "links": [ + 26 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" + }, + "widgets_values": [ + 49.1, + 1000, + 3, + 10000, + 0.2, + 3, + 0, + 0, + 0.5, + 0.01, + 0.05, + 0.005, + 0.005, + 0.001, + 0.00002, + 0.02, + 500, + 5000, + 3, + 0.01, + 100, + 100000, + 100, + 700, + 0.01, + 0 + ] + }, + { + "id": 21, + "type": "[ComfyUI-3D] Save 3DGS", + "pos": [ + 1994, + 55 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "link": 26 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Save 3DGS" + }, + "widgets_values": [ + "3DGS_[time(%Y-%m-%d)].ply" + ] } ], "links": [ @@ -407,38 +415,6 @@ 0, "MASK" ], - [ - 17, - 7, - 0, - 18, - 0, - "IMAGE" - ], - [ - 18, - 8, - 0, - 18, - 1, - "MASK" - ], - [ - 19, - 4, - 0, - 18, - 2, - "ORBIT_CAMPOSES" - ], - [ - 21, - 18, - 0, - 17, - 0, - "GS_RAW" - ], [ 22, 7, @@ -446,6 +422,38 @@ 19, 0, "IMAGE" + ], + [ + 23, + 7, + 0, + 20, + 0, + "IMAGE" + ], + [ + 24, + 8, + 0, + 20, + 1, + "MASK" + ], + [ + 25, + 4, + 0, + 20, + 2, + "ORBIT_CAMPOSES" + ], + [ + 26, + 20, + 0, + 21, + 0, + "GS_PLY" ] ], "groups": [], diff --git a/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json b/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json index 96ae54d..d6470ff 100644 --- a/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json +++ b/_Example_Workflows/3DGS_Multi-View_Renders_to_ply_with_Stack_Orbit_Cam_Poses_node.json @@ -1,6 +1,6 @@ { - "last_node_id": 22, - "last_link_id": 25, + "last_node_id": 24, + "last_link_id": 29, "nodes": [ { "id": 10, @@ -64,150 +64,29 @@ } }, { - "id": 17, - "type": "[ComfyUI-3D] Save 3DGS", + "id": 19, + "type": "PreviewImage", "pos": [ - 2122, - 54 - ], - "size": { - "0": 315, - "1": 58 - }, - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "link": 21 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Save 3DGS" - }, - "widgets_values": [ - "MeshTest\\3DGS_01.ply" - ] - }, - { - "id": 8, - "type": "InvertMask", - "pos": [ - 746, - -14 + 420, + -320 ], "size": { "0": 210, - "1": 26 + "1": 246 }, "flags": {}, - "order": 4, + "order": 3, "mode": 0, "inputs": [ { - "name": "mask", - "type": "MASK", - "link": 2 - } - ], - "outputs": [ - { - "name": "MASK", - "type": "MASK", - "links": [ - 8, - 18 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "InvertMask" - } - }, - { - "id": 18, - "type": "[ComfyUI-3D] Gaussian Splatting", - "pos": [ - 1486, - 53 - ], - "size": { - "0": 468.5999755859375, - "1": 718 - }, - "flags": { - "collapsed": false - }, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "reference_images", + "name": "images", "type": "IMAGE", - "link": 17 - }, - { - "name": "reference_masks", - "type": "MASK", - "link": 18 - }, - { - "name": "reference_orbit_camera_poses", - "type": "ORBIT_CAMPOSES", - "link": 25 - }, - { - "name": "mesh_to_initialize_gaussian", - "type": "MESH", - "link": null - } - ], - "outputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "links": [ - 21 - ], - "shape": 3, - "slot_index": 0 + "link": 22 } ], "properties": { - "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" - }, - "widgets_values": [ - 49.1, - 1000, - 1, - 10000, - 0.2, - 3, - 0, - 0, - 0.5, - 0.01, - 0.05, - 0.005, - 0.005, - 0.001, - 0.00002, - 0.02, - 500, - 5000, - 3, - 0.01, - 100, - 100000, - 100, - 700, - 0.01, - 0 - ] + "Node name for S&R": "PreviewImage" + } }, { "id": 7, @@ -228,8 +107,8 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 17, - 22 + 22, + 26 ], "shape": 3, "slot_index": 0 @@ -274,85 +153,62 @@ } }, { - "id": 19, - "type": "PreviewImage", + "id": 8, + "type": "InvertMask", "pos": [ - 420, - -320 + 746, + -14 ], "size": { "0": 210, - "1": 246 + "1": 26 }, "flags": {}, - "order": 3, + "order": 4, "mode": 0, "inputs": [ { - "name": "images", - "type": "IMAGE", - "link": 22 + "name": "mask", + "type": "MASK", + "link": 2 } ], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, - { - "id": 13, - "type": "[ComfyUI-3D] Load 3D Mesh", - "pos": [ - 1100, - 315 - ], - "size": { - "0": 315, - "1": 154 - }, - "flags": {}, - "order": 1, - "mode": 0, "outputs": [ { - "name": "mesh", - "type": "MESH", - "links": [], + "name": "MASK", + "type": "MASK", + "links": [ + 8, + 27 + ], "shape": 3, "slot_index": 0 } ], - "title": "Load 3D Mesh [Optional]", "properties": { - "Node name for S&R": "[ComfyUI-3D] Load 3D Mesh" - }, - "widgets_values": [ - "Meshes\\Cammy_Uniform.obj", - false, - true, - false, - false - ] + "Node name for S&R": "InvertMask" + } }, { "id": 22, "type": "[ComfyUI-3D] Stack Orbit Camera Poses", "pos": [ - 726, - 160 + 696, + 174 ], "size": { "0": 315, "1": 586 }, "flags": {}, - "order": 2, + "order": 1, "mode": 0, "outputs": [ { "name": "orbit_camposes", "type": "ORBIT_CAMPOSES", "links": [ - 25 + 28 ], "shape": 3, "slot_index": 0 @@ -417,6 +273,158 @@ 0, 0.1 ] + }, + { + "id": 23, + "type": "[ComfyUI-3D] Save 3DGS", + "pos": [ + 2178, + 54 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "link": 29 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Save 3DGS" + }, + "widgets_values": [ + "MeshTest\\3DGS_01.ply" + ] + }, + { + "id": 13, + "type": "[ComfyUI-3D] Load 3D Mesh", + "pos": [ + 1109, + 327 + ], + "size": { + "0": 315, + "1": 154 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "mesh", + "type": "MESH", + "links": [], + "shape": 3, + "slot_index": 0 + } + ], + "title": "Load 3D Mesh [Optional]", + "properties": { + "Node name for S&R": "[ComfyUI-3D] Load 3D Mesh" + }, + "widgets_values": [ + "Meshes\\Cammy_Uniform.obj", + false, + true, + false, + false + ] + }, + { + "id": 24, + "type": "[ComfyUI-3D] Gaussian Splatting", + "pos": [ + 1516, + 54 + ], + "size": { + "0": 443.4000244140625, + "1": 758 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "reference_images", + "type": "IMAGE", + "link": 26 + }, + { + "name": "reference_masks", + "type": "MASK", + "link": 27 + }, + { + "name": "reference_orbit_camera_poses", + "type": "ORBIT_CAMPOSES", + "link": 28 + }, + { + "name": "points_cloud", + "type": "POINTCLOUD", + "link": null + }, + { + "name": "ply_to_initialize_gaussian", + "type": "GS_PLY", + "link": null + }, + { + "name": "mesh_to_initialize_gaussian", + "type": "MESH", + "link": null + } + ], + "outputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "links": [ + 29 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" + }, + "widgets_values": [ + 49.1, + 1000, + 3, + 10000, + 0.2, + 3, + 0, + 0, + 0.5, + 0.01, + 0.05, + 0.005, + 0.005, + 0.001, + 0.00002, + 0.02, + 500, + 5000, + 3, + 0.01, + 100, + 100000, + 100, + 700, + 0.01, + 0 + ] } ], "links": [ @@ -444,30 +452,6 @@ 0, "MASK" ], - [ - 17, - 7, - 0, - 18, - 0, - "IMAGE" - ], - [ - 18, - 8, - 0, - 18, - 1, - "MASK" - ], - [ - 21, - 18, - 0, - 17, - 0, - "GS_RAW" - ], [ 22, 7, @@ -477,12 +461,36 @@ "IMAGE" ], [ - 25, + 26, + 7, + 0, + 24, + 0, + "IMAGE" + ], + [ + 27, + 8, + 0, + 24, + 1, + "MASK" + ], + [ + 28, 22, 0, - 18, + 24, 2, "ORBIT_CAMPOSES" + ], + [ + 29, + 24, + 0, + 23, + 0, + "GS_PLY" ] ], "groups": [], diff --git a/_Example_Workflows/Images_to_3DGS_to_DMTetMesh.json b/_Example_Workflows/Images_to_3DGS_to_DMTetMesh.json index 09df809..2500522 100644 --- a/_Example_Workflows/Images_to_3DGS_to_DMTetMesh.json +++ b/_Example_Workflows/Images_to_3DGS_to_DMTetMesh.json @@ -1,6 +1,6 @@ { - "last_node_id": 22, - "last_link_id": 26, + "last_node_id": 27, + "last_link_id": 36, "nodes": [ { "id": 10, @@ -14,7 +14,7 @@ "1": 246 }, "flags": {}, - "order": 7, + "order": 8, "mode": 0, "inputs": [ { @@ -39,7 +39,7 @@ "1": 26 }, "flags": {}, - "order": 5, + "order": 6, "mode": 0, "inputs": [ { @@ -64,79 +64,63 @@ } }, { - "id": 8, - "type": "InvertMask", + "id": 19, + "type": "PreviewImage", "pos": [ - 746, - -14 + 420, + -320 ], "size": { "0": 210, - "1": 26 + "1": 246 }, "flags": {}, "order": 4, "mode": 0, "inputs": [ { - "name": "mask", - "type": "MASK", - "link": 2 - } - ], - "outputs": [ - { - "name": "MASK", - "type": "MASK", - "links": [ - 8, - 18 - ], - "shape": 3, - "slot_index": 0 + "name": "images", + "type": "IMAGE", + "link": 22 } ], "properties": { - "Node name for S&R": "InvertMask" + "Node name for S&R": "PreviewImage" } }, { - "id": 4, - "type": "[ComfyUI-3D] Generate Orbit Camera Poses", + "id": 13, + "type": "[ComfyUI-3D] Load 3D Mesh", "pos": [ - 830, - 132 + 1053, + 415 ], "size": { - "0": 400, - "1": 200 + "0": 315, + "1": 154 }, "flags": {}, - "order": 2, + "order": 0, "mode": 0, - "inputs": [ - { - "name": "reference_images", - "type": "IMAGE", - "link": 1 - } - ], "outputs": [ { - "name": "orbit_camposes", - "type": "ORBIT_CAMPOSES", - "links": [ - 19 - ], + "name": "mesh", + "type": "MESH", + "links": [], "shape": 3, "slot_index": 0 } ], + "title": "Load 3D Mesh [Optional]", "properties": { - "Node name for S&R": "[ComfyUI-3D] Generate Orbit Camera Poses" + "Node name for S&R": "[ComfyUI-3D] Load 3D Mesh" }, "widgets_values": [ - "([0:120], 1.75, 0, 0, 360)" + "Meshes\\Cammy_Uniform.obj", + false, + true, + false, + false ] }, { @@ -151,7 +135,7 @@ 194 ], "flags": {}, - "order": 0, + "order": 1, "mode": 0, "outputs": [ { @@ -159,8 +143,8 @@ "type": "IMAGE", "links": [ 1, - 17, - 22 + 22, + 27 ], "shape": 3, "slot_index": 0 @@ -205,258 +189,128 @@ } }, { - "id": 19, - "type": "PreviewImage", + "id": 8, + "type": "InvertMask", "pos": [ - 420, - -320 + 746, + -14 ], "size": { "0": 210, - "1": 246 + "1": 26 + }, + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 2 + } + ], + "outputs": [ + { + "name": "MASK", + "type": "MASK", + "links": [ + 8, + 28 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "InvertMask" + } + }, + { + "id": 4, + "type": "[ComfyUI-3D] Generate Orbit Camera Poses", + "pos": [ + 830, + 132 + ], + "size": { + "0": 400, + "1": 200 }, "flags": {}, "order": 3, "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 22 - } - ], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, - { - "id": 18, - "type": "[ComfyUI-3D] Gaussian Splatting", - "pos": [ - 1486, - 53 - ], - "size": { - "0": 468.5999755859375, - "1": 718 - }, - "flags": { - "collapsed": false - }, - "order": 6, - "mode": 0, "inputs": [ { "name": "reference_images", "type": "IMAGE", - "link": 17 - }, - { - "name": "reference_masks", - "type": "MASK", - "link": 18 - }, - { - "name": "reference_orbit_camera_poses", - "type": "ORBIT_CAMPOSES", - "link": 19 - }, - { - "name": "mesh_to_initialize_gaussian", - "type": "MESH", - "link": null + "link": 1 } ], "outputs": [ { - "name": "raw_3DGS", - "type": "GS_RAW", + "name": "orbit_camposes", + "type": "ORBIT_CAMPOSES", "links": [ - 21, - 24 + 29 ], "shape": 3, "slot_index": 0 } ], "properties": { - "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" + "Node name for S&R": "[ComfyUI-3D] Generate Orbit Camera Poses" }, "widgets_values": [ - 49.1, - 1000, - 1, - 10000, - 0.2, - 3, - 0, - 0, - 0.5, - 0.01, - 0.05, - 0.005, - 0.005, - 0.001, - 0.00002, - 0.02, - 500, - 5000, - 3, - 0.01, - 100, - 100000, - 100, - 700, - 0.01, - 0 + "([0:120], 1.75, 0, 0, 360)" ] }, { - "id": 17, + "id": 24, "type": "[ComfyUI-3D] Save 3DGS", "pos": [ - 2243, - 52 + 2122, + -131 ], "size": { "0": 315, "1": 58 }, "flags": {}, - "order": 8, + "order": 9, "mode": 0, "inputs": [ { - "name": "raw_3DGS", - "type": "GS_RAW", - "link": 21 + "name": "gs_ply", + "type": "GS_PLY", + "link": 30 } ], "properties": { "Node name for S&R": "[ComfyUI-3D] Save 3DGS" }, "widgets_values": [ - "MeshTest\\3DGS_01.ply" - ] - }, - { - "id": 20, - "type": "[ComfyUI-3D] Convert 3DGS To Pointcloud", - "pos": [ - 2242, - 251 - ], - "size": { - "0": 267, - "1": 26 - }, - "flags": {}, - "order": 9, - "mode": 0, - "inputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "link": 24 - } - ], - "outputs": [ - { - "name": "points_cloud", - "type": "POINTCLOUD", - "links": [ - 25 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Convert 3DGS To Pointcloud" - } - }, - { - "id": 13, - "type": "[ComfyUI-3D] Load 3D Mesh", - "pos": [ - 1053, - 415 - ], - "size": { - "0": 315, - "1": 154 - }, - "flags": {}, - "order": 1, - "mode": 0, - "outputs": [ - { - "name": "mesh", - "type": "MESH", - "links": [], - "shape": 3, - "slot_index": 0 - } - ], - "title": "Load 3D Mesh [Optional]", - "properties": { - "Node name for S&R": "[ComfyUI-3D] Load 3D Mesh" - }, - "widgets_values": [ - "Meshes\\Cammy_Uniform.obj", - false, - true, - false, - false - ] - }, - { - "id": 22, - "type": "[ComfyUI-3D] Save 3D Mesh", - "pos": [ - 3261, - 55 - ], - "size": { - "0": 353.38916015625, - "1": 61.879417419433594 - }, - "flags": {}, - "order": 11, - "mode": 0, - "inputs": [ - { - "name": "mesh", - "type": "MESH", - "link": 26 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Save 3D Mesh" - }, - "widgets_values": [ - "MeshTest\\Cammy_Uniform_DMTet_01.obj" + "MeshTest/3DGS_01.ply" ] }, { "id": 21, "type": "[ComfyUI-3D] Deep Marching Tetrahedrons", "pos": [ - 2710, - 56 + 2963, + 55 ], "size": { "0": 443.4000244140625, "1": 314 }, "flags": {}, - "order": 10, + "order": 11, "mode": 0, "inputs": [ { "name": "reference_points_cloud", "type": "POINTCLOUD", - "link": 25 + "link": 35 }, { "name": "reference_images", @@ -495,6 +349,190 @@ 128, 5 ] + }, + { + "id": 22, + "type": "[ComfyUI-3D] Save 3D Mesh", + "pos": [ + 3534, + 56 + ], + "size": { + "0": 353.38916015625, + "1": 61.879417419433594 + }, + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "mesh", + "type": "MESH", + "link": 26 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Save 3D Mesh" + }, + "widgets_values": [ + "MeshTest\\Cammy_Uniform_DMTet_01.obj" + ] + }, + { + "id": 26, + "type": "[ComfyUI-3D] Load 3DGS", + "pos": [ + 2107, + 194 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "links": [], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Load 3DGS" + }, + "widgets_values": [ + "Meshes/3DGS_01.ply" + ] + }, + { + "id": 23, + "type": "[ComfyUI-3D] Gaussian Splatting", + "pos": [ + 1501, + 53 + ], + "size": { + "0": 443.4000244140625, + "1": 758 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "reference_images", + "type": "IMAGE", + "link": 27 + }, + { + "name": "reference_masks", + "type": "MASK", + "link": 28 + }, + { + "name": "reference_orbit_camera_poses", + "type": "ORBIT_CAMPOSES", + "link": 29 + }, + { + "name": "points_cloud", + "type": "POINTCLOUD", + "link": null + }, + { + "name": "ply_to_initialize_gaussian", + "type": "GS_PLY", + "link": null + }, + { + "name": "mesh_to_initialize_gaussian", + "type": "MESH", + "link": null + } + ], + "outputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "links": [ + 30, + 36 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Gaussian Splatting" + }, + "widgets_values": [ + 49.1, + 1000, + 3, + 10000, + 0.2, + 3, + 0, + 0, + 0.5, + 0.01, + 0.05, + 0.005, + 0.005, + 0.001, + 0.00002, + 0.02, + 500, + 5000, + 3, + 0.01, + 100, + 100000, + 100, + 700, + 0.01, + 0 + ] + }, + { + "id": 27, + "type": "[ComfyUI-3D] Convert 3DGS To Pointcloud", + "pos": [ + 2555, + 53 + ], + "size": { + "0": 241.79998779296875, + "1": 26 + }, + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "link": 36 + } + ], + "outputs": [ + { + "name": "points_cloud", + "type": "POINTCLOUD", + "links": [ + 35 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Convert 3DGS To Pointcloud" + } } ], "links": [ @@ -530,38 +568,6 @@ 0, "MASK" ], - [ - 17, - 7, - 0, - 18, - 0, - "IMAGE" - ], - [ - 18, - 8, - 0, - 18, - 1, - "MASK" - ], - [ - 19, - 4, - 0, - 18, - 2, - "ORBIT_CAMPOSES" - ], - [ - 21, - 18, - 0, - 17, - 0, - "GS_RAW" - ], [ 22, 7, @@ -570,22 +576,6 @@ 0, "IMAGE" ], - [ - 24, - 18, - 0, - 20, - 0, - "GS_RAW" - ], - [ - 25, - 20, - 0, - 21, - 0, - "POINTCLOUD" - ], [ 26, 21, @@ -593,6 +583,54 @@ 22, 0, "MESH" + ], + [ + 27, + 7, + 0, + 23, + 0, + "IMAGE" + ], + [ + 28, + 8, + 0, + 23, + 1, + "MASK" + ], + [ + 29, + 4, + 0, + 23, + 2, + "ORBIT_CAMPOSES" + ], + [ + 30, + 23, + 0, + 24, + 0, + "GS_PLY" + ], + [ + 35, + 27, + 0, + 21, + 0, + "POINTCLOUD" + ], + [ + 36, + 23, + 0, + 27, + 0, + "GS_PLY" ] ], "groups": [], diff --git a/_Example_Workflows/Load_3DGS_to_DMTetMesh.json b/_Example_Workflows/Load_3DGS_to_DMTetMesh.json index 06e2fab..9672e25 100644 --- a/_Example_Workflows/Load_3DGS_to_DMTetMesh.json +++ b/_Example_Workflows/Load_3DGS_to_DMTetMesh.json @@ -1,109 +1,13 @@ { - "last_node_id": 4, - "last_link_id": 3, + "last_node_id": 28, + "last_link_id": 37, "nodes": [ { - "id": 2, - "type": "[ComfyUI-3D] Convert 3DGS To Pointcloud", - "pos": [ - 561, - 484 - ], - "size": { - "0": 267, - "1": 26 - }, - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "link": 1 - } - ], - "outputs": [ - { - "name": "points_cloud", - "type": "POINTCLOUD", - "links": [ - 2 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Convert 3DGS To Pointcloud" - } - }, - { - "id": 1, - "type": "[ComfyUI-3D] Load 3DGS", - "pos": [ - 166, - 484 - ], - "size": { - "0": 315, - "1": 58 - }, - "flags": {}, - "order": 0, - "mode": 0, - "outputs": [ - { - "name": "raw_3DGS", - "type": "GS_RAW", - "links": [ - 1 - ], - "shape": 3, - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Load 3DGS" - }, - "widgets_values": [ - "Meshes\\Generated\\Cammy_Uniform_3DGS_01.ply" - ] - }, - { - "id": 4, - "type": "[ComfyUI-3D] Save 3D Mesh", - "pos": [ - 1446, - 485 - ], - "size": { - "0": 315, - "1": 58 - }, - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [ - { - "name": "mesh", - "type": "MESH", - "link": 3 - } - ], - "properties": { - "Node name for S&R": "[ComfyUI-3D] Save 3D Mesh" - }, - "widgets_values": [ - "MeshTest\\Cammy_Uniform_DMTet_01.obj" - ] - }, - { - "id": 3, + "id": 21, "type": "[ComfyUI-3D] Deep Marching Tetrahedrons", "pos": [ - 913, - 484 + 1450, + 420 ], "size": { "0": 443.4000244140625, @@ -116,7 +20,7 @@ { "name": "reference_points_cloud", "type": "POINTCLOUD", - "link": 2 + "link": 37 }, { "name": "reference_images", @@ -134,7 +38,7 @@ "name": "trained_mesh", "type": "MESH", "links": [ - 3 + 26 ], "shape": 3, "slot_index": 0 @@ -155,32 +59,128 @@ 128, 5 ] + }, + { + "id": 22, + "type": "[ComfyUI-3D] Save 3D Mesh", + "pos": [ + 2020, + 420 + ], + "size": { + "0": 353.38916015625, + "1": 61.879417419433594 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "mesh", + "type": "MESH", + "link": 26 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Save 3D Mesh" + }, + "widgets_values": [ + "MeshTest\\picacho.obj" + ] + }, + { + "id": 28, + "type": "[ComfyUI-3D] Convert 3DGS To Pointcloud", + "pos": [ + 1035, + 422 + ], + "size": { + "0": 241.79998779296875, + "1": 26 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "link": 36 + } + ], + "outputs": [ + { + "name": "points_cloud", + "type": "POINTCLOUD", + "links": [ + 37 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Convert 3DGS To Pointcloud" + } + }, + { + "id": 26, + "type": "[ComfyUI-3D] Load 3DGS", + "pos": [ + 592, + 423 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "links": [ + 36 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Load 3DGS" + }, + "widgets_values": [ + "Meshes/3DGS_TGS.ply" + ] } ], "links": [ [ - 1, - 1, + 26, + 21, 0, - 2, - 0, - "GS_RAW" - ], - [ - 2, - 2, - 0, - 3, - 0, - "POINTCLOUD" - ], - [ - 3, - 3, - 0, - 4, + 22, 0, "MESH" + ], + [ + 36, + 26, + 0, + 28, + 0, + "GS_PLY" + ], + [ + 37, + 28, + 0, + 21, + 0, + "POINTCLOUD" ] ], "groups": [], diff --git a/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS.json b/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS.json new file mode 100644 index 0000000..57ea542 --- /dev/null +++ b/_Example_Workflows/Triplane_Gaussian_Transformers_to_3DGS.json @@ -0,0 +1,242 @@ +{ + "last_node_id": 9, + "last_link_id": 11, + "nodes": [ + { + "id": 7, + "type": "[ComfyUI-3D] Run Triplane Gaussian Transformers", + "pos": [ + 1032, + 627 + ], + "size": { + "0": 430.8000183105469, + "1": 98 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "reference_image", + "type": "IMAGE", + "link": 6 + }, + { + "name": "reference_mask", + "type": "MASK", + "link": 11 + }, + { + "name": "tgs_model", + "type": "TGS_MODEL", + "link": 8 + } + ], + "outputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "links": [ + 9 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "gaussian_sh_degree", + "type": "FLOAT", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Run Triplane Gaussian Transformers" + }, + "widgets_values": [ + 1.9 + ] + }, + { + "id": 9, + "type": "InvertMask", + "pos": [ + 506, + 647 + ], + "size": { + "0": 210, + "1": 26 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "mask", + "type": "MASK", + "link": 10 + } + ], + "outputs": [ + { + "name": "MASK", + "type": "MASK", + "links": [ + 11 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "InvertMask" + } + }, + { + "id": 2, + "type": "[ComfyUI-3D] Load Triplane Gaussian Transformers", + "pos": [ + 579, + 735 + ], + "size": { + "0": 369, + "1": 47.66668701171875 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "tgs_model", + "type": "TGS_MODEL", + "links": [ + 8 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Load Triplane Gaussian Transformers" + } + }, + { + "id": 8, + "type": "[ComfyUI-3D] Save 3DGS", + "pos": [ + 1531, + 627 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "gs_ply", + "type": "GS_PLY", + "link": 9 + } + ], + "properties": { + "Node name for S&R": "[ComfyUI-3D] Save 3DGS" + }, + "widgets_values": [ + "MeshTest/3DGS_TGS.ply" + ] + }, + { + "id": 4, + "type": "LoadImage", + "pos": [ + 87, + 211 + ], + "size": { + "0": 354.32330322265625, + "1": 315.4895935058594 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 6 + ], + "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": [ + [ + 6, + 4, + 0, + 7, + 0, + "IMAGE" + ], + [ + 8, + 2, + 0, + 7, + 2, + "TGS_MODEL" + ], + [ + 9, + 7, + 0, + 8, + 0, + "GS_PLY" + ], + [ + 10, + 4, + 1, + 9, + 0, + "MASK" + ], + [ + 11, + 9, + 0, + 7, + 1, + "MASK" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file diff --git a/_Example_Workflows/_Example_Inputs_Files/a_pikachu_with_smily_face-removebg.png b/_Example_Workflows/_Example_Inputs_Files/a_pikachu_with_smily_face-removebg.png new file mode 100644 index 0000000000000000000000000000000000000000..34ca7c48931478bbbb3c583594e71f53149c6a2d GIT binary patch literal 402764 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∈YwV1XUF`q-qX|5y8PzW$n3|&* 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

pv@-ClNEgsLLs`UpO7j`rxX;M4r2&q4%En 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%0DP$pp6yZ0)`|f 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 z yCD?%R<=3{xZ?CJS0;b-OtL}kQ<#IG0D1xG!Zp;Ay$2mlM94k4hJQJ}t74HC9eb(r?ymDf9x&r1@l7*1{MU#fJ#3L+A z{xPd>K62nVo3d#!DgKAxR9aMX)5V%V)9t=LJm138(|%Ux7i34;oCOo0lyQTSZ4{_C z#l)@w)D-r2DB)1hj!zmCuq7$vI*bwl1OjBA#5M6bja(0aol81qdJ?F_=80=1&m7ra zYvgWQX%q^CQ?G?WV1p$(Wh zK~@zU3b2Vpi9VK`J6)~ev>-MnIJ3-PJ*JzgKB#w@0Z+37>p!Hx%IDfF`?QciFG0q} zFNMSG;e&VhOT5osm0`<{HlzK&5m@c$~(z}cX`v3^yv=LNoQQh0D> z!|w6hXiKN+`BCbU@*vBR~b09?$m% z4%9x!9ap~AO)5fu;KJ&0?&ed4%P;|1OVN4p`(k$o*g%4e2hTa$X&Ir~X3Ol=%_OaQ zM?Q>);}8sfo22pnvUE0Qs6W#88oZ7o#S$5V<9y2-aUGj3!XOq7RF@VO@nfYaNHuTM zQnBNy&oC}WNRe!dp#>$=eUC?fWtnBN;KBRb6}Pyy;&hQF0oLEF>a$J{GSZ|EXp`N( z$|lt^h5plqk!ATCimvz)2>xn+XhFMYWyY-fwTW9o&H=&Mi^h85@|n6r##!0Xn~$NV z%B!+u5C$QT0c_)w_%yEf;f2&wWvtw5iX-KDe=Sc_QNA$wW}CCYcdp8D2QQeXW!{-IDF8Kiq*P==o;b00ZonyB|O;#b)W-W%W!L|XD z@M2SP{i?($Il5uhe`}br;oYs-4}K#<>DhDRXEK|s4w^deEBCm2UVss@z@SEuwHC#S zBNX9#!o2ZJLt#G`^(d@Ie$0ju9IjQ;3T?{!q4N5W{43>q6l{%nDI-sm%VDOH*eQ&N z{etypdUu1iba0M8>lpIREeB~Py{JOw5YfP!VT5lUHgkp;9}1)}DM6`VA74&j2X3KW z@z01d462Iv?`%W`oV*WkKra7$TZ0+7B{S>J`)^K=rT=>n``z&te5T%LvOXHj0 zPJ?<%Lo@Z4Qjn@yK#g8LhnJ|f5CoOZ*jqBvB3F)}m zS38ZzskfTAyMOn}e7!U(naSc*ie+8&QPKFOo=dVH3>}Od3c1%T1cuo7oAhZ}Az=Xc zKJmf&UD5!nRJOtExMvV+NYFefF!b|ehzP*{<)8)F&4ttBcvm%n8MT~Gms^Yp>s(|{ z3a}g}i2JbfuoawJ!<|q$3J;|l>mUdns{a{YJ}7;o{8V%bD2nR4VhvM1dRMA}9r$92 zu3nveHMjclUrB@)?ca1!e8lxngNHH@btaXs($E>zv$`kvWpq8B)v|P1&-39YeHvBz z`)U%ejpIEFHp3A6Hlv>_e&MHvT+9Xo9_(h<>oNTXD<$}~M_Cb8kBW{WcF?7g2F7e3 zqX{tlIuaG2hb^w)D~0na4SY%Y01msf?zKb9Ypg;$9ig}+pg9oPq6MU}hir!$f&A3U zS8Fz~Lf!M#ID+(Ud`8NvX$6M3MHV6UX=4SKHjJ|-STj2bccQ_)$mezfQ24Ltk&$Ew z;bHV1!9l0|SjWXwt=85?<`NubzHX8vYG=guH)@BI~8Epr?3Gl#MW9ispS^-#9iul$ir+HqGJiKebe}e9MLu zh{Guk6TEGfMtA||uh?4K79Fz|_Mgn5HC^TZjL-bGv+QD)9>- zBlwo%Op1Xg%%29o(^oRk*P}WDo1YL!lE`6+lpx{J zJVioGK%WX|ujEBQ8@)%K*QuA??=XQ9d5uJLuGU!^I%;}XH6wJ!OBBpgnt#51OsKQn zF-e*KHo%Bs1ba^)BK0}K9Rvs5*qFTnydarEJO;-c)yH^}B1;Swr35P;r5LRo$Ub__ za2VYf5DwJ705UsSbFfNKYZ!$a!O*%W>fS{(J8j7nV=KZ zUS}n#%fPNsaIAno%)EuT&=9&^0cU&fqtB;xa{I9K$vcE3>T}brhl^o-^ZLoex+p@_ z^%f*sCS?-lVGHp==dwr}lPDNQ^lFCQcn?oMzk%V9%#`j5fTBTQjj>EU%oRp z|COg|LoUsIY~JP5MP2P-joGTa;hF6E9QLcn$1>g;xuE6;r3fE*ns!r3e8>C;73>t} z*v&_vkALEyf&g3pIQ>!wJTZ%Ijc2dp#1rHUR`A?BgRqdZ{A-(6XYWcZ&}(gB&*;wj`eU+LV)??Kh>X)5 zuF4s#=A=E*$g2{A64t3+j3C}+2T2zeI^UCo<480?IWJg->E5gyP8c^#^kXEoRs;!G){YY zhyGs@%^Zq9E7{GL-Ti;3@UJ0ffs`)?3KQ*CPcR6VLfj}F(`5cP!A01uXA3U(#N}1n z7V`_cbYl6Wy2jp^7ZysUs6Ex@Cmn6Q2sM=vX^MX9rh9{FkT~0l4!W%`2>cG^3XAYB zDNuT#zyaV*4IUoY87q(vQ;NbXNt#7b#ef0k$00?pTy18J;APh3e4H<~yGEqL^{~Th zjRf>}=s*exEv{?*Ak1vQyI!-1c8ySi55fu-CrVL9kh^MVkjxdw?T_xw(;bOi3=BGq zi7)b_4!m!4%q}L>PBWerPK?<}p7`NtNzSK_=r-PE1X$-ujWlGXatUi)KS>aLyO!WW z2uK5WH@ftH`ixcs02=^7eD@A=BwS$?lKHdM=@{Rn-nDy=>M|^*QA8JozNC^bXge~i z$vfdPM^4;6J|GSh(j01*poiQsM@E#LdDwj@6r67E8&qnJCwSr~*c?dm{`+LF+ko&5(0_t@DnSJF+#9nis^hM@>VyVa2tUugwwIh;e$~Tg?3FFvbDnq!vv>HZNOOMWf;JWPL z%@ZGfbN?;`umw0pZvbtHNNM273L$3%Au!9Sn;nLwk^$=$*3e1(}d|U_N=_k(YrIQ*LC%E?9NwDo&d&VC%TS(e-=?cPX3iea@FO zi)Y<`h+&md&gZ4)^XqLsq zH|Nz28}Az^vEJ)iwa6XSry_?2&Z5r-hFX;0{ocsEA`Y-f$ZEBsiubb%CuoRJY{9g^ zA%cJclL%f%2?HLQs%K`DwV5CyK`Q}xKqhwQ4J2MC&=()SzuS{&FJ$ELpv?+#TKBA}7iyTVPZ;1*4^Z(VkjUV6RxnVMP9y~MKQg0V?ijoD+&bo4 zzH0cK z97Di({QH=GICu8rtX zNW~i4GBE*Kv?$6Q66np9ZsPlDFNQg2`eHmrUfZp8)oIVwVMofYHs&$ki&_2`^Y8>O zR^jjK0()L)|9xE(_}9hyKGxKCXsSm}*i0J6%Jxm$z%{Ph*d8-uVL-EG5NTA{?{R3T zY0$pXLoqt0M-$Dv=_BK}0%w+~?6S>~UrQ++zyE(6d7V+_!H|W83!3R5EsO)1fEMb_y`UaG_s2 zxd7}_%*;G~)d(Rb5Me}_s0*gmf_GCt8WoFsm3vv+Ir611FC z*YFwbVOX5LIecC{Ei$6zf)|~rEy!kg*gwA&kvrj{Y?`-(uu!u96;H^rF&+cv!=2hA!*1?;8nyb=0 zRyO%h*ctUL_=+^OJoWTs5{C_q<=*5yt5xaIkEJZ^5zq#)UhCKf3f$E%zOPwYM}nM` zaYbMkd@6X(ea`g%+Pki(CYP=qih_r(fJm=Wr6Uk}mm-`HI0#A;=|$<%Q4x^ddr^7< zQl+;D3K$4T#{kk>0!k4ul;jH>{f_6q{%^ju%+0*-teJVI?Cd?W%QGdD$W4~V%4%&O zYpsXkclXsaSnC5=c$iiD!rI*lh?CfEwMBIqU~^TU`UNblEsA!Cw6PKsQ*x@Oj`EAg z`cU_U-1^qolRgNtZfDAdSBzV{&S)`%I0Zljw)kxZi0XY0P1lCfylV{2)eG;sIaXY> z?Aa5rte2yH{0U5N=h)zDo>ul1<+Eg8e2FFc{?N4((gUJ}-miJ&Jbf1;t#>BJHn?Z# zVbk1iFC=*1k6O4G$rWsYiL$ekKzWJM!wh2#G@?Q%35LMNvSt<*tMeU1B=io!!FfGP zVvQT?>fkK_0o2o~orx#i!T>U9{eelPr-1&;m-F38+YEC<)r8M%tFK;ay>%n_C zJl;J&U@5ga7UACz$hw=sOLypy5n%X%fJuV{>)=Y;ex!8r?jdgWi%ho%?N2=f#hD%g zc`f<$ES6%A8paKP7WU>V?j@R(I&Jth$YlO?3SLGlVgl70-aLp(n2A@~lr8Q&Flx!t z5pcv`;9c3VS~HftMWYib%mxk#N*?V5?N@oIVWz|{i85Sr5NafOSvB{`YJY8B-fr?0 zVV}x`_~DvM(T{l+SEa$0Qq$JF&v#hbdE{*hYf3b%jwP- z$vsgdxyne* zS>*cSgTwx)IqZLa@Q4G(3G+Ij%hlgyanuS5qAxE5Edqh!E(=<;fWF|Ymm#08^js{t zMu?@=n4zb>FdZ^+qGuB&X+}bh*g_jNMnhYl1ZznN;AEk=cavL>WD1}H{Ag~kZ|k64 zotntf#YhhcJ3%Gv?ay9Vfz&~xw767G``KvFTrh2w=9-dHDQ#VMc#uk zW^j(Ub`Ym&r%n>jQdISLMyhD7gnH7zu38&i?u_REJqj=t0I45KwHw_xk*1SS|1l?2q;>HQs86%C3`&sfh!_ z2#d?yRENx_l`2kWa4rxZixp$j*c{{j`g(gb{IdyTz(Z4$!*M^qs+wWbhw#`q9*&cE zxN&={&mYMIE#TqtT%!n0MsU;w zUJv0o;0mC=xQ#qfjV~$X^kDRqW}!=c<+KSAkGgpJv?`*$-ne1QGH~a)q=KAUq7HS5 zN+D;&F6{m_vtZ?K?Y;YH)JWg$hsuhTWff0t2san!Imu7{Ra^D%isNUX zW}~!y$S_}Lx2!+REM%nhdq>u~-nCeg$r()>$aepQ{T0o59s?k9^mwbdU0q2||CFp& zSV36-Q+be6LhGiQ3iD|Zc z({MOp(teP(A|j354f?^-o&s>Q{Gxw5XpPcr>o)4oweB8@Ft`Qp+QM^!bkxx_soAuz zCOgU?>KdXXJMkZk-d5fX(0z`8NGA`|#W#lucc45fXDEj79HrD?#=o8FCaJ z`5fuYVUBTK<^1Mt^XnWV;}Rt$>ljllC50D@h3*Bll+a_br`X6AJd7Gqs49b10pF4k zfO;pxtDtN&)?p4Se0!)%mRi$p)$caiS&flGfRR!FzLylW9 zkKj6DZ@92VZssq!77(Lp!$_ek5EaFu)r?HG!3XgIcf7V|-AYG;^rfRk5J`_Y zUWid9$V`c>B!)(f5ElT50Wl}KeEq8B%J#rl&Ci7q13r9+uI11jnbPrM0#}!1NycSK z<`p9ox|NlTU66)$$p%!g@)e)9X_1;;=o*Bf$wzFfumpi1*gsb5n3z6b408#`;=W86 zJsB-E*efD<5VFot8Jfa89mFiV<>+LW1*|)Eunp2X6}w?7eb`%^k7=H4--%bcet)LW z-59Qj2Nl3ODoW#Jhty zqOvHs$B2u#`W+iK91bk~Y^2&NBCBKI2lrk!*Dp1gRYN=LJ}A$Ma{q7xaB;%QXyGoc6}2q3p03vczO9n1L2aBP2yNp zG{_<*#+}L7dZO3Q&oXf`f3O_&Mrt3%-|pt&i$ph787CPG8Kk0fM~k@+z8)2KI8$!( z7!>50<}_Dd_05|=^9*fxC-IiNP|V?Rx~WUo3K)5JB{%|*(34k=V<81VN!H*T2US$^LU_@~|q9;A@06f~@5OZDN@Sb@`~_swIgxIB!M*tF5b**64)+^>v{9&~XLkT7BG z#C}G-yEJ$qC7ZpPi{|kH8Gn|dTzi7Ie|%;hTHXelEdM%r!?~5EU1nU@Zl1Ky@cvb0 zI2R!os^}6Kp@b(`vawUkvu*=J8JbGt>K5bZ+B~G6`_?WW3Y~XmCB{7^6UGaTOm;w1 zmKt!l)ABDViX&MH%|}=m@~ienSjyxB1yBVS9H@Zu0TyvR0OTx%uq6yVX$a-sss{8; z0;zywDPE;Rv+jqPa(kRdb(m-A?XF+C&|ekZ7sKw=$$8Yn{|1ApmOG<=-yAL3Kk%nW z2kJW=r<3+K|1w~cZw%ap7klcar(nK!Em#GrC<=>+_OM9S=R=&cjlDDd!S8xjhrfA2 ztJ+t0zd7e&xXmnrLvKzD8k)xNv5`XrC}bTIeGzeO}&t7&MHET5u2)Y?7 z_l(bwtI!S}eWbN}rE!+tkZ9#P>83c|nd@jNH`6VM{R?-xchWk)DJs9E`XLG>Ss&5z zB1;!spdO$j7u8}4rwcK{+ONq;%0>5*e)=ZK8t>@pt=FxhU9OugZ{7_I*T~`QMpeME~jxu}>DJD6s1X=*dUw~~|NCt40pH*6hdCh3q`w_|d{IUuk zZx5L!$mlT(Rgwz|8}LoB^Yn?!vHY_`as9()e1Si9ec^c~uM#FJZerZa z+{Nw`QGY9-pn$JHyeb94BJ{SS^0n_oKr!c4$Qa+qMUCJ!I&p$>B&OhIhxo)w>MM}x z_$kP3ve&l8%-F6OirR-ZWE(pkYfCfP0a<&%qkhnWw%V4yTgD$^Abb3Ga}@$|n^)5A zCS2x|R4y@e$!^cB`T}u!TvfSdLD?&+U4>KG5B5aJut z)&&&r(f#(~w=3_icKfBKTpjv~7CJEW)`Q%ysnmS&8vO=Jo4svE>DfE8bEy??ZlFt4+ zPxrNenh@uhJI^|&ByN12yFm)0NwDk2SR}FJXT^0mwJ@2FqDv+ZT6T6Ucyo0nl%yoe z9Tt=$R@Z>{5GJ1=p_F=X9IZC4w#lw1QXT+k&IQ1?O*X~HDR|6^-h^I*s?1F$U8A(u zq@nZ5LsgE?{|M31&M6q0qth^TtDw?k>h`$DBv;*ozgs9@dvp-OeOtd5 z8w=m(wPr;Q4)~tdO^SGnw5`y)UBTLascgGVJq0FhTYYX)Y9AH*sY1MW#KLQ9W?-zI zWVN&^H!n`)i`UlTOrX59M7EDyyQmm!EKRdJ3qJA@xqCf1WF~PUo;a?y($RgviX9s{ z^s#K)8Jf7TaK8adMW*)5T04~^k}xjt-sKH3KLvKfaz6{<*M}r)Hz%X%a(!4MlfM`3 z9mO2%8!`@&C+X*O?$72k>Wtv>%%C3C=ajU07U4G~R;uG;gqK+zIi%mO@JhC?hIMX~ zTtT4>0##2(r5Yb!nmP8!BftosIo^-;=lNpvW=2W;+rK;Tj+(HVZ7Ns?D>nQ1-vs6r zNiy-LU$eO!7G$R8UoC57Y)fnJWpu$0-6x|f0Y1Lvh*&!EB9|4u{_F({ki!6^0C#Pd z9oWN`F{Cwr5)$-n5&y#KJYZf_Fq-86DLeH@yX`um$mr|x@ZLaBMZzWK+WUBVKhwYa z5S}e%SRMB<-`^n_XK;2gFx|XwMclwP`JnarfyDRSo+fOHl6$G9ye*M^118>|%Ku1r zys9DCkJbjI!OgNWTJ$PvkO5V-E&^Szr=#n;Qk0@{cAy>t5W%YB!Zx!Fs_5iGcZ{<2 zEKyI>v7mu{LCsvX;x!h1ubm#8r#%l{uxY~iEaH*$e%{>4 z)pIcBa4&Bl*HqA0AtPH?vvY^(P{+Ts;s>*RVty9?+PWwS|B22;N*KU|53;L_->`^^ z@VPOPUCe5iXlY?ll8F>d&nOeO7zay0m+Br1#3nVnF+=(s^BvZb2bS|t9%bcDPWAG^ zC!cl9f)5V-lM2rjFG9|QeA35*!O4t*Zy2tz;YBEKXDfU{M4k}MSQ z%PRfaLL`9Q`IquPZT;sOdZIH8J&ArT^O+I($BFx231Mi(&zb%ye15cLMRktIAoIZ^ z^4G8ZQX3yOeDT+>{WE|k3ZeK(4VdTd=lH>Gf2nrf6a0V~|0(~kF8nVI7~AQynE}B6 zRPNv=h^=XIEH1MvhXS5F2_>@nvo?~Ll=|$IfbjeQMqows?;I=kc>qI-5a*8qKT19V zO#Q6#BZq-h25^xl`qqD&I)7jW-2A8U^PG!>l%XmgIM4g=Pn#s66u)-kclUnJ9-bb4 z(;l7-e*ZOoGnW5ZEdtQ^wld> Ht)Bh|(*iF& literal 0 HcmV?d00001 diff --git a/algorithms/diff_texturing.py b/algorithms/diff_texturing.py index dab262a..5176a8f 100644 --- a/algorithms/diff_texturing.py +++ b/algorithms/diff_texturing.py @@ -29,6 +29,7 @@ class DiffTextureBaker: self.batch_size = batch_size def prepare_img(self, img): + # (H, W, C) -> (C, H, W) img_new = img.permute(2, 0, 1).unsqueeze(0).to(self.device) img_new = F.interpolate(img_new, (self.ref_size_H, self.ref_size_W), mode="bilinear", align_corners=False).contiguous() return img_new diff --git a/algorithms/main_3DGS_renderer.py b/algorithms/main_3DGS_renderer.py index 65999c0..70270d1 100644 --- a/algorithms/main_3DGS_renderer.py +++ b/algorithms/main_3DGS_renderer.py @@ -31,7 +31,7 @@ from simple_knn._C import distCUDA2 from ..shared_utils.sh_utils import eval_sh, SH2RGB, RGB2SH from ..mesh_processer.mesh import Mesh -from ..mesh_processer.mesh_utils import decimate_mesh, clean_mesh +from ..mesh_processer.mesh_utils import read_gs_ply def inverse_sigmoid(x): return torch.log(x/(1-x)) @@ -500,6 +500,11 @@ class GaussianModel: lr = self.xyz_scheduler_args(iteration) param_group['lr'] = lr return lr + + def reset_opacity(self): + opacities_new = inverse_sigmoid(torch.min(self.get_opacity, torch.ones_like(self.get_opacity)*0.01)) + optimizable_tensors = self.replace_tensor_to_optimizer(opacities_new, "opacity") + self._opacity = optimizable_tensors["opacity"] def construct_list_of_attributes(self): l = ['x', 'y', 'z', 'nx', 'ny', 'nz'] @@ -525,9 +530,7 @@ class GaussianModel: def get_mesh(self): xyz = self._xyz.detach().cpu().numpy() - def save_ply(self, path): - os.makedirs(os.path.dirname(path), exist_ok=True) - + def to_ply(self): xyz = self._xyz.detach().cpu().numpy() normals = np.zeros_like(xyz) f_dc = self._features_dc.detach().transpose(1, 2).flatten(start_dim=1).contiguous().cpu().numpy() @@ -542,45 +545,10 @@ class GaussianModel: attributes = np.concatenate((xyz, normals, f_dc, f_rest, opacities, scale, rotation), axis=1) elements[:] = list(map(tuple, attributes)) el = PlyElement.describe(elements, 'vertex') - PlyData([el]).write(path) + return PlyData([el]) - def reset_opacity(self): - opacities_new = inverse_sigmoid(torch.min(self.get_opacity, torch.ones_like(self.get_opacity)*0.01)) - optimizable_tensors = self.replace_tensor_to_optimizer(opacities_new, "opacity") - self._opacity = optimizable_tensors["opacity"] - - def load_ply(self, path): - plydata = PlyData.read(path) - - xyz = np.stack((np.asarray(plydata.elements[0]["x"]), - np.asarray(plydata.elements[0]["y"]), - np.asarray(plydata.elements[0]["z"])), axis=1) - opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis] - - print("Number of points at loading : ", xyz.shape[0]) - - features_dc = np.zeros((xyz.shape[0], 3, 1)) - features_dc[:, 0, 0] = np.asarray(plydata.elements[0]["f_dc_0"]) - features_dc[:, 1, 0] = np.asarray(plydata.elements[0]["f_dc_1"]) - features_dc[:, 2, 0] = np.asarray(plydata.elements[0]["f_dc_2"]) - - extra_f_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("f_rest_")] - assert len(extra_f_names)==3*(self.max_sh_degree + 1) ** 2 - 3 - features_extra = np.zeros((xyz.shape[0], len(extra_f_names))) - for idx, attr_name in enumerate(extra_f_names): - features_extra[:, idx] = np.asarray(plydata.elements[0][attr_name]) - # Reshape (P,F*SH_coeffs) to (P, F, SH_coeffs except DC) - features_extra = features_extra.reshape((features_extra.shape[0], 3, (self.max_sh_degree + 1) ** 2 - 1)) - - 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]) + def create_from_ply(self, plydata): + xyz, features_dc, features_extra, opacities, scales, rots = read_gs_ply(plydata) self._xyz = nn.Parameter(torch.tensor(xyz, dtype=torch.float, device="cuda").requires_grad_(True)) self._features_dc = nn.Parameter(torch.tensor(features_dc, dtype=torch.float, device="cuda").transpose(1, 2).contiguous().requires_grad_(True)) @@ -894,6 +862,11 @@ class Renderer: if isinstance(input, Mesh): # load from 3D mesh self.gaussians.create_from_mesh(input, num_pts) + elif isinstance(input, PlyData): + self.gaussians.create_from_ply(input) + elif isinstance(input, PointCloud): + # load from a provided pcd + self.gaussians.create_from_pcd(input, 1) elif input is None: # init from random point cloud @@ -913,9 +886,6 @@ class Renderer: points=xyz, colors=SH2RGB(shs), normals=np.zeros((num_pts, 3)) ) self.gaussians.create_from_pcd(pcd, 10) - elif isinstance(input, PointCloud): - # load from a provided pcd - self.gaussians.create_from_pcd(input, 1) else: self.gaussians.create_from_uv_data(input) diff --git a/algorithms/triplane_gaussian_transformers.py b/algorithms/triplane_gaussian_transformers.py new file mode 100644 index 0000000..61c6fde --- /dev/null +++ b/algorithms/triplane_gaussian_transformers.py @@ -0,0 +1,167 @@ +import torch +from dataclasses import dataclass, field +from einops import rearrange + +from ..tgs import find +from ..tgs.models.image_feature import ImageFeature +from ..tgs.utils.saving import SaverMixin +from ..tgs.utils.config import parse_structured +from ..tgs.utils.ops import points_projection +from ..tgs.utils.misc import load_module_weights +from ..tgs.utils.typing import * + +class TGS(torch.nn.Module, SaverMixin): + @dataclass + class Config: + weights: Optional[str] = None + weights_ignore_modules: Optional[List[str]] = None + + camera_embedder_cls: str = "" + camera_embedder: dict = field(default_factory=dict) + + image_feature: dict = field(default_factory=dict) + + image_tokenizer_cls: str = "" + image_tokenizer: dict = field(default_factory=dict) + + tokenizer_cls: str = "" + tokenizer: dict = field(default_factory=dict) + + backbone_cls: str = "" + backbone: dict = field(default_factory=dict) + + post_processor_cls: str = "" + post_processor: dict = field(default_factory=dict) + + renderer_cls: str = "" + renderer: dict = field(default_factory=dict) + + pointcloud_generator_cls: str = "" + pointcloud_generator: dict = field(default_factory=dict) + + pointcloud_encoder_cls: str = "" + pointcloud_encoder: dict = field(default_factory=dict) + + cfg: Config + + def load_weights(self, weights: str, ignore_modules: Optional[List[str]] = None): + state_dict = load_module_weights( + weights, ignore_modules=ignore_modules, map_location="cpu" + ) + self.load_state_dict(state_dict, strict=False) + + def __init__(self, cfg): + super().__init__() + self.cfg = parse_structured(self.Config, cfg) + + self.image_tokenizer = find(self.cfg.image_tokenizer_cls)( + self.cfg.image_tokenizer + ) + + assert self.cfg.camera_embedder_cls == 'tgs.models.networks.MLP' + weights = self.cfg.camera_embedder.pop("weights") if "weights" in self.cfg.camera_embedder else None + self.camera_embedder = find(self.cfg.camera_embedder_cls)(**self.cfg.camera_embedder) + if weights: + from ..tgs.utils.misc import load_module_weights + weights_path, module_name = weights.split(":") + state_dict = load_module_weights( + weights_path, module_name=module_name, map_location="cpu" + ) + self.camera_embedder.load_state_dict(state_dict) + + self.image_feature = ImageFeature(self.cfg.image_feature) + + self.tokenizer = find(self.cfg.tokenizer_cls)(self.cfg.tokenizer) + + self.backbone = find(self.cfg.backbone_cls)(self.cfg.backbone) + + self.post_processor = find(self.cfg.post_processor_cls)( + self.cfg.post_processor + ) + + self.renderer = find(self.cfg.renderer_cls)(self.cfg.renderer) + + # pointcloud generator + self.pointcloud_generator = find(self.cfg.pointcloud_generator_cls)(self.cfg.pointcloud_generator) + + self.point_encoder = find(self.cfg.pointcloud_encoder_cls)(self.cfg.pointcloud_encoder) + + # load checkpoint + if self.cfg.weights is not None: + self.load_weights(self.cfg.weights, self.cfg.weights_ignore_modules) + + def _forward(self, batch: Dict[str, Any]) -> Dict[str, Any]: + # generate point cloud + out = self.pointcloud_generator(batch) + pointclouds = out["points"] + + batch_size, n_input_views = batch["rgb_cond"].shape[:2] + + # Camera modulation + camera_extri = batch["c2w_cond"].view(*batch["c2w_cond"].shape[:-2], -1) + camera_intri = batch["intrinsic_normed_cond"].view(*batch["intrinsic_normed_cond"].shape[:-2], -1) + camera_feats = torch.cat([camera_intri, camera_extri], dim=-1) + + camera_feats = self.camera_embedder(camera_feats) + + input_image_tokens: Float[Tensor, "B Cit Nit"] = self.image_tokenizer( + rearrange(batch["rgb_cond"], 'B Nv H W C -> B Nv C H W'), + modulation_cond=camera_feats, + ) + input_image_tokens = rearrange(input_image_tokens, 'B Nv C Nt -> B (Nv Nt) C', Nv=n_input_views) + + # get image features for projection + image_features = self.image_feature( + rgb = batch["rgb_cond"], + mask = batch.get("mask_cond", None), + feature = input_image_tokens + ) + + # only support number of input view is one + c2w_cond = batch["c2w_cond"].squeeze(1) + intrinsic_cond = batch["intrinsic_cond"].squeeze(1) + proj_feats = points_projection(pointclouds, c2w_cond, intrinsic_cond, image_features) + + point_cond_embeddings = self.point_encoder(torch.cat([pointclouds, proj_feats], dim=-1)) + tokens: Float[Tensor, "B Ct Nt"] = self.tokenizer(batch_size, cond_embeddings=point_cond_embeddings) + + tokens = self.backbone( + tokens, + encoder_hidden_states=input_image_tokens, + modulation_cond=None, + ) + + scene_codes = self.post_processor(self.tokenizer.detokenize(tokens)) + rend_out = self.renderer(scene_codes, + query_points=pointclouds, + additional_features=proj_feats, + **batch) + + return {**out, **rend_out} + + def forward(self, batch): + plys = [] + + out = self._forward(batch) + batch_size = batch["index"].shape[0] + for b in range(batch_size): + if batch["view_index"][b, 0] == 0: + plys.append(out["3dgs"][b].to_ply()) + + #self.render_test_video(out, batch, b) + + return plys + + def render_test_video(self, out, batch, b): + for index, render_image in enumerate(out["comp_rgb"][b]): + view_index = batch["view_index"][b, index] + self.save_image_grid( + f"video/{batch['instance_id'][b]}/{view_index}.png", + [ + { + "type": "rgb", + "img": render_image, + "kwargs": {"data_format": "HWC"}, + } + ] + ) \ No newline at end of file diff --git a/configs/tgs_config.yaml b/configs/tgs_config.yaml new file mode 100644 index 0000000..50eab4e --- /dev/null +++ b/configs/tgs_config.yaml @@ -0,0 +1,150 @@ +data: + background_color: [1.0, 1.0, 1.0] + cond_width: 252 # multiply of 14 + cond_height: 252 + + relative_pose: true + + num_workers: 16 + eval_batch_size: 1 + eval_height: 512 + eval_width: 512 + +system: + camera_embedder_cls: tgs.models.networks.MLP + camera_embedder: + dim_in: 25 # c2w + [fx, fy, cx, cy] + dim_out: 768 + n_neurons: 768 + n_hidden_layers: 1 + activation: silu + + image_feature: + out_dim: 773 + + image_tokenizer_cls: tgs.models.tokenizers.image.DINOV2SingleImageTokenizer + image_tokenizer: + pretrained_model_name_or_path: "facebook/dinov2-base" + width: ${data.cond_width} + height: ${data.cond_height} + + modulation: true + modulation_zero_init: true + modulation_single_layer: true + modulation_cond_dim: ${system.camera_embedder.dim_out} # c2w + intrinsic + + freeze_backbone_params: false + enable_memory_efficient_attention: ${system.backbone.enable_memory_efficient_attention} + enable_gradient_checkpointing: ${system.backbone.gradient_checkpointing} + + tokenizer_cls: tgs.models.tokenizers.triplane.TriplaneLearnablePositionalEmbedding + tokenizer: + plane_size: 32 + num_channels: 512 + + backbone_cls: tgs.models.transformers.Transformer1D + backbone: + in_channels: ${system.tokenizer.num_channels} + num_attention_heads: 8 + attention_head_dim: 64 + num_layers: 10 + cross_attention_dim: 768 # hard-code, =DINO feature dim + + norm_type: "layer_norm" + + enable_memory_efficient_attention: false + gradient_checkpointing: false + + post_processor_cls: tgs.models.networks.TriplaneUpsampleNetwork + post_processor: + in_channels: ${system.tokenizer.num_channels} + out_channels: 80 + + pointcloud_generator_cls: tgs.models.pointclouds.simplepoint.SimplePointGenerator + pointcloud_generator: + camera_embedder_cls: tgs.models.networks.MLP + camera_embedder: + dim_in: 25 # c2w + [fx, fy, cx, cy] + dim_out: 768 + n_neurons: 768 + n_hidden_layers: 1 + activation: silu + + image_tokenizer_cls: tgs.models.tokenizers.image.DINOV2SingleImageTokenizer + image_tokenizer: + pretrained_model_name_or_path: "facebook/dinov2-base" + width: ${data.cond_width} + height: ${data.cond_height} + + modulation: true + modulation_zero_init: true + modulation_single_layer: true + modulation_cond_dim: ${system.camera_embedder.dim_out} # c2w + intrinsic + + freeze_backbone_params: true + enable_memory_efficient_attention: ${system.backbone.enable_memory_efficient_attention} + enable_gradient_checkpointing: false + + tokenizer_cls: tgs.models.tokenizers.point.PointLearnablePositionalEmbedding + tokenizer: + num_pcl: 2048 + num_channels: 512 + + backbone_cls: tgs.models.transformers.Transformer1D + backbone: + in_channels: ${system.pointcloud_generator.tokenizer.num_channels} + num_attention_heads: 8 + attention_head_dim: 64 + num_layers: 10 + cross_attention_dim: 768 # hard-code, =DINO feature dim + + norm_type: "layer_norm" + + enable_memory_efficient_attention: ${system.backbone.enable_memory_efficient_attention} + gradient_checkpointing: ${system.backbone.gradient_checkpointing} + + post_processor_cls: tgs.models.networks.PointOutLayer + post_processor: + in_channels: 512 + out_channels: 3 + + pointcloud_upsampling_cls: tgs.models.snowflake.model_spdpp.SnowflakeModelSPDPP + pointcloud_upsampling: + input_channels: 768 + dim_feat: 128 + num_p0: 2048 + radius: 1 + bounding: true + use_fps: true + up_factors: [2,4] + token_type: "image_token" + + pointcloud_encoder_cls: tgs.models.pointclouds.pointnet.LocalPoolPointnet + pointcloud_encoder: + input_channels: 776 # 3 + 3 + 768 + 1 + 1 [xyz, local features] + c_dim: ${system.tokenizer.num_channels} + hidden_dim: 128 + plane_size: ${system.tokenizer.plane_size} + n_blocks: 5 + radius: ${system.renderer.radius} + + renderer_cls: tgs.models.renderer.GS3DRenderer + renderer: + sh_degree: 3 + radius: 0.6 + mlp_network_config: + n_neurons: ${system.renderer.gs_out.in_channels} + n_hidden_layers: 2 + activation: silu + gs_out: + in_channels: 128 + xyz_offset: true + restrict_offset: true + use_rgb: false + feature_channels: + xyz: 3 + scaling: 3 + rotation: 4 + opacity: 1 + shs: ${shsdim:${system.renderer.sh_degree}} + clip_scaling: 0.2 \ No newline at end of file diff --git a/install.bat b/install.bat index df5a491..96ddaab 100644 --- a/install.bat +++ b/install.bat @@ -16,8 +16,13 @@ if exist "%python_miniconda_exec%" ( 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 -) -else ( + + "%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 ../../../../ +) else ( echo ERROR: Cannot find Miniconda Environment "%python_miniconda_exec%" ) diff --git a/mesh_processer/mesh.py b/mesh_processer/mesh.py index 43d6935..7fcc263 100644 --- a/mesh_processer/mesh.py +++ b/mesh_processer/mesh.py @@ -240,10 +240,6 @@ class Mesh: albedo = albedo.astype(np.float32) / 255 print(f"[load_obj] load texture: {albedo.shape}") - # import matplotlib.pyplot as plt - # plt.imshow(albedo) - # plt.show() - mesh.albedo = torch.tensor(albedo, dtype=torch.float32, device=device) return mesh diff --git a/mesh_processer/mesh_utils.py b/mesh_processer/mesh_utils.py index f454dad..698b130 100644 --- a/mesh_processer/mesh_utils.py +++ b/mesh_processer/mesh_utils.py @@ -2,6 +2,9 @@ import torch import numpy as np import pymeshlab as pml +from .mesh import PointCloud +from ..shared_utils.sh_utils import SH2RGB, RGB2SH + def _base_face_areas(face_vertices_0, face_vertices_1, face_vertices_2): """Base function to compute the face areas.""" x1, x2, x3 = torch.split(face_vertices_0 - face_vertices_1, 1, dim=-1) @@ -297,3 +300,44 @@ def clean_mesh( ) return verts, faces + +def read_gs_ply(plydata): + xyz = np.stack((np.asarray(plydata.elements[0]["x"]), + np.asarray(plydata.elements[0]["y"]), + np.asarray(plydata.elements[0]["z"])), axis=1) + opacities = np.asarray(plydata.elements[0]["opacity"])[..., np.newaxis] + + features_dc = np.zeros((xyz.shape[0], 3, 1)) + features_dc[:, 0, 0] = np.asarray(plydata.elements[0]["f_dc_0"]) + features_dc[:, 1, 0] = np.asarray(plydata.elements[0]["f_dc_1"]) + features_dc[:, 2, 0] = np.asarray(plydata.elements[0]["f_dc_2"]) + + extra_f_names = [p.name for p in plydata.elements[0].properties if p.name.startswith("f_rest_")] + #assert len(extra_f_names)!=3*(max_sh_degree + 1) ** 2 - 3: + max_sh_degree = int(((len(extra_f_names) + 3) / 3) ** 0.5 - 1) + features_extra = np.zeros((xyz.shape[0], len(extra_f_names))) + for idx, attr_name in enumerate(extra_f_names): + features_extra[:, idx] = np.asarray(plydata.elements[0][attr_name]) + # Reshape (P,F*SH_coeffs) to (P, F, SH_coeffs except DC) + features_extra = features_extra.reshape((features_extra.shape[0], 3, (max_sh_degree + 1) ** 2 - 1)) + + 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]) + + return xyz, features_dc, features_extra, opacities, scales, rots + +def ply_to_points_cloud(plydata): + xyz, features_dc, features_extra, opacities, scales, rots = read_gs_ply(plydata) + features_dc = np.transpose(features_dc, (0, 2, 1)) # equivalent of torch.transpose(features_dc, 1, 2) + features_extra = np.transpose(features_extra, (0, 2, 1)) + shs = np.concatenate((features_dc, features_extra), axis=1) + normals = np.zeros_like(xyz) + pcd = PointCloud(points=xyz, colors=SH2RGB(shs), normals=normals) + return pcd \ No newline at end of file diff --git a/nodes.py b/nodes.py index 86d0822..c165cd1 100644 --- a/nodes.py +++ b/nodes.py @@ -1,18 +1,31 @@ import os +import sys import re import math import copy from enum import Enum +import subprocess import folder_paths as comfy_paths import torch +from torch.utils.data import DataLoader import numpy as np +from plyfile import PlyData + from .shared_utils.common_utils import cstr from .mesh_processer.mesh import Mesh +from .mesh_processer.mesh_utils import ply_to_points_cloud from .algorithms.main_3DGS import GaussianSplatting, GSParams from .algorithms.diff_texturing import DiffTextureBaker from .algorithms.dmtet import DMTetMesh +from .algorithms.triplane_gaussian_transformers import TGS +from .tgs.utils.config import ExperimentConfig, load_config +from .tgs.data import CustomImageOrbitDataset +from .tgs.utils.misc import todevice, get_device + +ROOT_PATH = os.path.join(comfy_paths.get_folder_paths("custom_nodes")[0], "ComfyUI-3D-Pack") +sys.path.append(ROOT_PATH) MANIFEST = { "name": "ComfyUI-3D-Pack", @@ -83,38 +96,34 @@ class Load_3DGS: def INPUT_TYPES(cls): return { "required": { - "raw_3DGS_file_path": ("STRING", {"default": '', "multiline": False}), + "gs_ply_file_path": ("STRING", {"default": '', "multiline": False}), }, } RETURN_TYPES = ( - "GS_RAW", - + "GS_PLY", ) RETURN_NAMES = ( - "raw_3DGS", + "gs_ply", ) - FUNCTION = "load_mesh" + FUNCTION = "load_gs" CATEGORY = "ComfyUI3D/Import|Export" - def load_mesh(self, raw_3DGS_file_path): - raw_3DGS = None + def load_gs(self, gs_ply_file_path): + gs_ply = None - if not os.path.isabs(raw_3DGS_file_path): - raw_3DGS_file_path = os.path.join(comfy_paths.input_directory, raw_3DGS_file_path) + if not os.path.isabs(gs_ply_file_path): + gs_ply_file_path = os.path.join(comfy_paths.input_directory, gs_ply_file_path) - if os.path.exists(raw_3DGS_file_path): - folder, filename = os.path.split(raw_3DGS_file_path) + if os.path.exists(gs_ply_file_path): + folder, filename = os.path.split(gs_ply_file_path) if filename.lower().endswith(SUPPORTED_3DGS_EXTENSIONS): - with torch.inference_mode(False): - raw_3DGS = GaussianSplatting() - raw_3DGS.renderer.gaussians.load_ply(raw_3DGS_file_path) - # TODO: better way of loading 3DGS, since this method omit render & training parameters + gs_ply = PlyData.read(gs_ply_file_path) else: cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3DGS file extensions: {SUPPORTED_3DGS_EXTENSIONS}").error.print() else: - cstr(f"[{self.__class__.__name__}] File {raw_3DGS_file_path} does not exist").error.print() - return (raw_3DGS, ) + cstr(f"[{self.__class__.__name__}] File {gs_ply_file_path} does not exist").error.print() + return (gs_ply, ) class Save_3D_Mesh: @@ -156,7 +165,7 @@ class Save_3DGS: def INPUT_TYPES(cls): return { "required": { - "raw_3DGS": ("GS_RAW",), + "gs_ply": ("GS_PLY",), "save_path": ("STRING", {"default": '3DGS_[time(%Y-%m-%d)].ply', "multiline": False}), }, } @@ -166,7 +175,7 @@ class Save_3DGS: FUNCTION = "save_3DGS" CATEGORY = "ComfyUI3D/Import|Export" - def save_3DGS(self, raw_3DGS, save_path): + def save_3DGS(self, gs_ply, save_path): folder_path, filename = os.path.split(save_path) @@ -177,8 +186,7 @@ class Save_3DGS: os.makedirs(folder_path, exist_ok=True) - raw_3DGS.renderer.gaussians.save_ply(save_path) - # TODO: better way of saving 3DGS, since this method omit render & training parameters + gs_ply.write(save_path) else: cstr(f"[{self.__class__.__name__}] File name {filename} does not end with supported 3DGS file extensions: {SUPPORTED_3DGS_EXTENSIONS}").error.print() @@ -190,7 +198,7 @@ class Convert_3DGS_To_Pointcloud: def INPUT_TYPES(cls): return { "required": { - "raw_3DGS": ("GS_RAW",), + "gs_ply": ("GS_PLY",), }, } @@ -200,12 +208,12 @@ class Convert_3DGS_To_Pointcloud: RETURN_NAMES = ( "points_cloud", ) - FUNCTION = "convert_3DGS" + FUNCTION = "convert_gs_ply" CATEGORY = "ComfyUI3D/Preprocessor" - def convert_3DGS(self, raw_3DGS): + def convert_gs_ply(self, gs_ply): - points_cloud = raw_3DGS.renderer.gaussians.get_points_cloud() + points_cloud = ply_to_points_cloud(gs_ply) return (points_cloud, ) @@ -510,7 +518,7 @@ class Gaussian_Splatting: "reference_orbit_camera_poses": ("ORBIT_CAMPOSES",), # (orbit radius, elevation, azimuth, orbit center X, orbit center Y, orbit center Z) "reference_orbit_camera_fovy": ("FLOAT", {"default": 49.1, "min": 0.0, "max": 180.0, "step": 0.1}), "training_iterations": ("INT", {"default": 1000, "min": 1, "max": 100000}), - "batch_size": ("INT", {"default": 5, "min": 1, "max": 0xffffffffffffffff}), + "batch_size": ("INT", {"default": 3, "min": 1, "max": 0xffffffffffffffff}), "loss_value_scale": ("FLOAT", {"default": 10000.0, "min": 1.0}), "ms_ssim_loss_weight": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, }), "alpha_loss_weight": ("FLOAT", {"default": 3, "min": 0.0, }), @@ -537,53 +545,58 @@ class Gaussian_Splatting: }, "optional": { + "points_cloud": ("POINTCLOUD",), + "ply_to_initialize_gaussian": ("GS_PLY",), "mesh_to_initialize_gaussian": ("MESH",), } } RETURN_TYPES = ( - "GS_RAW", + "GS_PLY", ) RETURN_NAMES = ( - "raw_3DGS", + "gs_ply", ) FUNCTION = "run_3DGS" CATEGORY = "ComfyUI3D/Algorithm" - def run_3DGS(self, - reference_images, - reference_masks, - reference_orbit_camera_poses, - reference_orbit_camera_fovy, - training_iterations, - batch_size, - loss_value_scale, - ms_ssim_loss_weight, - alpha_loss_weight, - offset_loss_weight, - offset_opacity_loss_weight, - invert_background_probability, - feature_learning_rate, - opacity_learning_rate, - scaling_learning_rate, - rotation_learning_rate, - position_learning_rate_init, - position_learning_rate_final, - position_learning_rate_delay_mult, - position_learning_rate_max_steps, - initial_gaussians_num, - K_nearest_neighbors, - percent_dense, - density_start_iterations, - density_end_iterations, - densification_interval, - opacity_reset_interval, - densify_grad_threshold, - gaussian_sh_degree, - mesh_to_initialize_gaussian=None): + def run_3DGS( + self, + reference_images, + reference_masks, + reference_orbit_camera_poses, + reference_orbit_camera_fovy, + training_iterations, + batch_size, + loss_value_scale, + ms_ssim_loss_weight, + alpha_loss_weight, + offset_loss_weight, + offset_opacity_loss_weight, + invert_background_probability, + feature_learning_rate, + opacity_learning_rate, + scaling_learning_rate, + rotation_learning_rate, + position_learning_rate_init, + position_learning_rate_final, + position_learning_rate_delay_mult, + position_learning_rate_max_steps, + initial_gaussians_num, + K_nearest_neighbors, + percent_dense, + density_start_iterations, + density_end_iterations, + densification_interval, + opacity_reset_interval, + densify_grad_threshold, + gaussian_sh_degree, + points_cloud=None, + ply_to_initialize_gaussian=None, + mesh_to_initialize_gaussian=None, + ): - - raw_3DGS = None + gs_ply = None ref_imgs_num = len(reference_images) ref_masks_num = len(reference_masks) @@ -624,18 +637,26 @@ class Gaussian_Splatting: densify_grad_threshold, gaussian_sh_degree) - gs = GaussianSplatting(gs_params, mesh_to_initialize_gaussian) + + if points_cloud is not None: + gs_init_input = points_cloud + elif ply_to_initialize_gaussian is not None: + gs_init_input = ply_to_initialize_gaussian + else: + gs_init_input = mesh_to_initialize_gaussian + + gs = GaussianSplatting(gs_params, gs_init_input) gs.prepare_training(reference_images, reference_masks, reference_orbit_camera_poses, reference_orbit_camera_fovy) gs.training() - raw_3DGS = gs + gs_ply = gs.renderer.gaussians.to_ply() else: cstr(f"[{self.__class__.__name__}] Number of reference images {ref_imgs_num} does not equal to number of reference camera poses {ref_cam_poses_num}").error.print() else: cstr(f"[{self.__class__.__name__}] Number of reference images {ref_imgs_num} does not equal to number of masks {ref_masks_num}").error.print() - return (raw_3DGS, ) + return (gs_ply, ) class Bake_Texture_To_Mesh: @@ -652,7 +673,7 @@ class Bake_Texture_To_Mesh: "reference_orbit_camera_fovy": ("FLOAT", {"default": 49.1, "min": 0.0, "max": 180.0, "step": 0.1}), "mesh": ("MESH",), "training_iterations": ("INT", {"default": 1000, "min": 1, "max": 100000}), - "batch_size": ("INT", {"default": 5, "min": 1, "max": 0xffffffffffffffff}), + "batch_size": ("INT", {"default": 3, "min": 1, "max": 0xffffffffffffffff}), "texture_learning_rate": ("FLOAT", {"default": 0.1, "min": 0.00001, "step": 0.00001}), "train_mesh_geometry": ("BOOLEAN", {"default": False},), "geometry_learning_rate": ("FLOAT", {"default": 0.01, "min": 0.00001, "step": 0.00001}), @@ -761,4 +782,112 @@ class Deep_Marching_Tetrahedrons: dmtet_mesh.training(reference_points_cloud, reference_images, reference_masks) mesh = dmtet_mesh.get_mesh() - return (mesh, ) \ No newline at end of file + return (mesh, ) + +TGS_CONFIG_PATH = "configs/tgs_config.yaml" + +class Load_Triplane_Gaussian_Transformers: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + }, + + "optional": { + } + } + + RETURN_TYPES = ( + "TGS_MODEL", + ) + RETURN_NAMES = ( + "tgs_model", + ) + FUNCTION = "load_TGS" + CATEGORY = "ComfyUI3D/Import|Export" + + def load_TGS(self): + + device = get_device() + + config_path = os.path.join(ROOT_PATH, TGS_CONFIG_PATH) + 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): + 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 + 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() + + return (tgs_model, ) + +class Run_Triplane_Gaussian_Transformers: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "reference_image": ("IMAGE", ), + "reference_mask": ("MASK",), + "tgs_model": ("TGS_MODEL", ), + "cam_dist": ("FLOAT", {"default": 1.9, "min": 0.01, "step": 0.01}), + }, + } + + OUTPUT_NODE = True + RETURN_TYPES = ( + "GS_PLY", + "FLOAT" + ) + RETURN_NAMES = ( + "gs_ply", + "gaussian_sh_degree" + ) + FUNCTION = "run_TGS" + CATEGORY = "ComfyUI3D/Algorithm" + + def run_TGS(self, reference_image, reference_mask, tgs_model, cam_dist): + config_path = os.path.join(ROOT_PATH, TGS_CONFIG_PATH) + cfg: ExperimentConfig = load_config(config_path) + + #save_path = os.path.join(ROOT_PATH, "outputs") + #tgs_model.set_save_dir(save_path) + + cfg.data.cond_camera_distance = cam_dist + cfg.data.eval_camera_distance = cam_dist + dataset = CustomImageOrbitDataset(reference_image, reference_mask, cfg.data) + dataloader = DataLoader(dataset, + batch_size=cfg.data.eval_batch_size, + shuffle=False, + collate_fn=dataset.collate + ) + + + gs_ply = [] + for batch in dataloader: + batch = todevice(batch) + gs_ply.extend(tgs_model(batch)) + + """ + # Output rendered video, for testing this node only + tgs_model.save_img_sequences( + "video", + "(\d+)\.png", + save_format="mp4", + fps=30, + delete=True, + ) + """ + + return (gs_ply[0], tgs_model.renderer.cfg.sh_degree) + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ce532f5..5cf47fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,24 @@ #base cmake ninja +kornia +OmegaConf numpy +einops scipy opencv-python -kornia + +# for use ML models +diffusers +transformers # for training differentiable tensors pytorch_msssim +# for process images & videos +imageio +imageio-ffmpeg + # for dmtet and mesh import & export trimesh plyfile @@ -19,3 +29,4 @@ pymeshlab # extra torchtyping tqdm +jaxtyping diff --git a/test.bat b/test.bat index f8ae281..b12512a 100644 --- a/test.bat +++ b/test.bat @@ -1,5 +1,3 @@ -set "python_miniconda_exec=..\..\..\python_miniconda_env\ComfyUI\python.exe" +set "python_miniconda_exec=..\..\..\..\..\..\..\python_miniconda_env\ComfyUI\python.exe" -echo ERROR: Cannot find Miniconda Environment "%python_miniconda_exec%" - -pause \ No newline at end of file +cd tgs/models/snowflake/pointnet2_ops_lib && "%python_miniconda_exec%" -s setup.py install && cd ../../../../ \ No newline at end of file diff --git a/tgs/__init__.py b/tgs/__init__.py new file mode 100644 index 0000000..889caab --- /dev/null +++ b/tgs/__init__.py @@ -0,0 +1,11 @@ +import importlib +from .utils.typing import * + +def find(cls_string) -> Type: + cls_full_name = cls_string.split(".") + module_string = ".".join(cls_full_name[:-1]) + cls_name = cls_full_name[-1] + print(module_string) + module = importlib.import_module(module_string, package=__name__) + cls = getattr(module, cls_name) + return cls \ No newline at end of file diff --git a/tgs/data.py b/tgs/data.py new file mode 100644 index 0000000..153dd41 --- /dev/null +++ b/tgs/data.py @@ -0,0 +1,237 @@ +import json +import math +from dataclasses import dataclass, field + +import os +import imageio +import numpy as np +import torch +import torch.nn.functional as F +from PIL import Image +from torch.utils.data import Dataset + +from .utils.config import parse_structured +from .utils.ops import get_intrinsic_from_fov, get_ray_directions, get_rays +from .utils.typing import * + +@dataclass +class CustomImageDataModuleConfig: + background_color: Tuple[float, float, float] = field( + default_factory=lambda: (1.0, 1.0, 1.0) + ) + + relative_pose: bool = False + cond_height: int = 512 + cond_width: int = 512 + cond_camera_distance: float = 1.6 + cond_fovy_deg: float = 40.0 + cond_elevation_deg: float = 0.0 + cond_azimuth_deg: float = 0.0 + num_workers: int = 16 + + eval_height: int = 512 + eval_width: int = 512 + eval_batch_size: int = 1 + eval_elevation_deg: float = 0.0 + eval_camera_distance: float = 1.6 + eval_fovy_deg: float = 40.0 + n_test_views: int = 120 + num_views_output: int = 120 + only_3dgs: bool = False + +class CustomImageOrbitDataset(Dataset): + def __init__(self, reference_image, reference_mask, cfg: Any) -> None: + super().__init__() + self.cfg: CustomImageDataModuleConfig = parse_structured(CustomImageDataModuleConfig, cfg) + + self.n_views = self.cfg.n_test_views + assert self.n_views % self.cfg.num_views_output == 0 + + self.all_images = reference_image + self.all_masks = reference_mask + + azimuth_deg: Float[Tensor, "B"] = torch.linspace(0, 360.0, self.n_views + 1)[ + : self.n_views + ] + elevation_deg: Float[Tensor, "B"] = torch.full_like( + azimuth_deg, self.cfg.eval_elevation_deg + ) + camera_distances: Float[Tensor, "B"] = torch.full_like( + elevation_deg, self.cfg.eval_camera_distance + ) + + elevation = elevation_deg * math.pi / 180 + azimuth = azimuth_deg * math.pi / 180 + + # convert spherical coordinates to cartesian coordinates + # right hand coordinate system, x back, y right, z up + # elevation in (-90, 90), azimuth from +x to +y in (-180, 180) + camera_positions: Float[Tensor, "B 3"] = torch.stack( + [ + camera_distances * torch.cos(elevation) * torch.cos(azimuth), + camera_distances * torch.cos(elevation) * torch.sin(azimuth), + camera_distances * torch.sin(elevation), + ], + dim=-1, + ) + + # default scene center at origin + center: Float[Tensor, "B 3"] = torch.zeros_like(camera_positions) + # default camera up direction as +z + up: Float[Tensor, "B 3"] = torch.as_tensor([0, 0, 1], dtype=torch.float32)[ + None, : + ].repeat(self.n_views, 1) + + fovy_deg: Float[Tensor, "B"] = torch.full_like( + elevation_deg, self.cfg.eval_fovy_deg + ) + fovy = fovy_deg * math.pi / 180 + + lookat: Float[Tensor, "B 3"] = F.normalize(center - camera_positions, dim=-1) + right: Float[Tensor, "B 3"] = F.normalize(torch.cross(lookat, up), dim=-1) + up = F.normalize(torch.cross(right, lookat), dim=-1) + c2w3x4: Float[Tensor, "B 3 4"] = torch.cat( + [torch.stack([right, up, -lookat], dim=-1), camera_positions[:, :, None]], + dim=-1, + ) + c2w: Float[Tensor, "B 4 4"] = torch.cat( + [c2w3x4, torch.zeros_like(c2w3x4[:, :1])], dim=1 + ) + c2w[:, 3, 3] = 1.0 + + # get directions by dividing directions_unit_focal by focal length + focal_length: Float[Tensor, "B"] = ( + 0.5 * self.cfg.eval_height / torch.tan(0.5 * fovy) + ) + directions_unit_focal = get_ray_directions( + H=self.cfg.eval_height, + W=self.cfg.eval_width, + focal=1.0, + ) + directions: Float[Tensor, "B H W 3"] = directions_unit_focal[ + None, :, :, : + ].repeat(self.n_views, 1, 1, 1) + directions[:, :, :, :2] = ( + directions[:, :, :, :2] / focal_length[:, None, None, None] + ) + # must use normalize=True to normalize directions here + rays_o, rays_d = get_rays(directions, c2w, keepdim=True) + + intrinsic: Float[Tensor, "B 3 3"] = get_intrinsic_from_fov( + self.cfg.eval_fovy_deg * math.pi / 180, + H=self.cfg.eval_height, + W=self.cfg.eval_width, + bs=self.n_views, + ) + intrinsic_normed: Float[Tensor, "B 3 3"] = intrinsic.clone() + intrinsic_normed[..., 0, 2] /= self.cfg.eval_width + intrinsic_normed[..., 1, 2] /= self.cfg.eval_height + intrinsic_normed[..., 0, 0] /= self.cfg.eval_width + intrinsic_normed[..., 1, 1] /= self.cfg.eval_height + + self.rays_o, self.rays_d = rays_o, rays_d + self.intrinsic = intrinsic + self.intrinsic_normed = intrinsic_normed + self.c2w = c2w + self.camera_positions = camera_positions + + self.background_color = torch.as_tensor(self.cfg.background_color) + + # condition + self.intrinsic_cond = get_intrinsic_from_fov( + np.deg2rad(self.cfg.cond_fovy_deg), + H=self.cfg.cond_height, + W=self.cfg.cond_width, + ) + self.intrinsic_normed_cond = self.intrinsic_cond.clone() + self.intrinsic_normed_cond[..., 0, 2] /= self.cfg.cond_width + self.intrinsic_normed_cond[..., 1, 2] /= self.cfg.cond_height + self.intrinsic_normed_cond[..., 0, 0] /= self.cfg.cond_width + self.intrinsic_normed_cond[..., 1, 1] /= self.cfg.cond_height + + + if self.cfg.relative_pose: + self.c2w_cond = torch.as_tensor( + [ + [0, 0, 1, self.cfg.cond_camera_distance], + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 1], + ] + ).float() + else: + cond_elevation = self.cfg.cond_elevation_deg * math.pi / 180 + cond_azimuth = self.cfg.cond_azimuth_deg * math.pi / 180 + cond_camera_position: Float[Tensor, "3"] = torch.as_tensor( + [ + self.cfg.cond_camera_distance * np.cos(cond_elevation) * np.cos(cond_azimuth), + self.cfg.cond_camera_distance * np.cos(cond_elevation) * np.sin(cond_azimuth), + self.cfg.cond_camera_distance * np.sin(cond_elevation), + ], dtype=torch.float32 + ) + + cond_center: Float[Tensor, "3"] = torch.zeros_like(cond_camera_position) + cond_up: Float[Tensor, "3"] = torch.as_tensor([0, 0, 1], dtype=torch.float32) + cond_lookat: Float[Tensor, "3"] = F.normalize(cond_center - cond_camera_position, dim=-1) + cond_right: Float[Tensor, "3"] = F.normalize(torch.cross(cond_lookat, cond_up), dim=-1) + cond_up = F.normalize(torch.cross(cond_right, cond_lookat), dim=-1) + cond_c2w3x4: Float[Tensor, "3 4"] = torch.cat( + [torch.stack([cond_right, cond_up, -cond_lookat], dim=-1), cond_camera_position[:, None]], + dim=-1, + ) + cond_c2w: Float[Tensor, "4 4"] = torch.cat( + [cond_c2w3x4, torch.zeros_like(cond_c2w3x4[:1])], dim=0 + ) + cond_c2w[3, 3] = 1.0 + self.c2w_cond = cond_c2w + + def __len__(self): + if self.cfg.only_3dgs: + return len(self.all_images) + else: + return len(self.all_images) * self.n_views // self.cfg.num_views_output + + def __getitem__(self, index): + if self.cfg.only_3dgs: + scene_index = index + view_index = [0] + else: + scene_index = index * self.cfg.num_views_output // self.n_views + view_start = index % (self.n_views // self.cfg.num_views_output) + view_index = list(range(self.n_views))[view_start * self.cfg.num_views_output : + (view_start + 1) * self.cfg.num_views_output] + + mask_cond: Float[Tensor, "Hc Wc 1"] = self.resize_image(self.all_masks[scene_index].unsqueeze(2)) + rgb_cond: Float[Tensor, "Hc Wc 3"] = self.resize_image(self.all_images[scene_index]) * mask_cond + self.background_color[None, None, :] * (1 - mask_cond) + + out = { + "rgb_cond": rgb_cond, + "c2w_cond": self.c2w_cond.unsqueeze(0), + "mask_cond": mask_cond, + "intrinsic_cond": self.intrinsic_cond.unsqueeze(0), + "intrinsic_normed_cond": self.intrinsic_normed_cond.unsqueeze(0), + "view_index": torch.as_tensor(view_index), + "rays_o": self.rays_o[view_index], + "rays_d": self.rays_d[view_index], + "intrinsic": self.intrinsic[view_index], + "intrinsic_normed": self.intrinsic_normed[view_index], + "c2w": self.c2w[view_index], + "camera_positions": self.camera_positions[view_index], + } + out["c2w"][..., :3, 1:3] *= -1 + out["c2w_cond"][..., :3, 1:3] *= -1 + out["index"] = torch.as_tensor(scene_index) + out["background_color"] = self.background_color + instance_id = str(scene_index) + out["instance_id"] = instance_id + return out + + def resize_image(self, img): + img_new = img.permute(2, 0, 1).unsqueeze(0) + img_new = F.interpolate(img_new, (self.cfg.cond_width, self.cfg.cond_height), mode="bilinear", align_corners=False).contiguous() + return img_new.permute(0, 2, 3, 1) + + def collate(self, batch): + batch = torch.utils.data.default_collate(batch) + batch.update({"height": self.cfg.eval_height, "width": self.cfg.eval_width}) + return batch \ No newline at end of file diff --git a/tgs/models/__init__.py b/tgs/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgs/models/image_feature.py b/tgs/models/image_feature.py new file mode 100644 index 0000000..88d3424 --- /dev/null +++ b/tgs/models/image_feature.py @@ -0,0 +1,48 @@ +from dataclasses import dataclass +import torch +import torch.nn.functional as F +from einops import rearrange + +from ..utils.base import BaseModule +from ..utils.ops import compute_distance_transform +from ..utils.typing import * + +class ImageFeature(BaseModule): + @dataclass + class Config(BaseModule.Config): + use_rgb: bool = True + use_feature: bool = True + use_mask: bool = True + feature_dim: int = 128 + out_dim: int = 133 + backbone: str = "default" + freeze_backbone_params: bool = True + + cfg: Config + + def forward(self, rgb, mask=None, feature=None): + B, Nv, H, W = rgb.shape[:4] + rgb = rearrange(rgb, "B Nv H W C -> (B Nv) C H W") + if mask is not None: + mask = rearrange(mask, "B Nv H W C -> (B Nv) C H W") + + assert feature is not None + # reshape dino tokens to image-like size + feature = rearrange(feature, "B (Nv Nt) C -> (B Nv) Nt C", Nv=Nv) + feature = feature[:, 1:].reshape(B * Nv, H // 14, W // 14, -1).permute(0, 3, 1, 2).contiguous() + feature = F.interpolate(feature, size=(H, W), mode='bilinear', align_corners=False) + + if mask is not None and mask.is_floating_point(): + mask = mask > 0.5 + + image_features = [] + if self.cfg.use_rgb: + image_features.append(rgb) + if self.cfg.use_feature: + image_features.append(feature) + if self.cfg.use_mask: + image_features += [mask, compute_distance_transform(mask)] + + # detach features, occur error when with grad + image_features = torch.cat(image_features, dim=1)#.detach() + return rearrange(image_features, "(B Nv) C H W -> B Nv C H W", B=B, Nv=Nv).squeeze(1) \ No newline at end of file diff --git a/tgs/models/networks.py b/tgs/models/networks.py new file mode 100644 index 0000000..082671e --- /dev/null +++ b/tgs/models/networks.py @@ -0,0 +1,204 @@ +from dataclasses import dataclass + +import torch +import torch.nn as nn +from einops import rearrange +import numpy as np + +from ..utils.base import BaseModule +from ..utils.ops import get_activation +from ..utils.typing import * + +class PointOutLayer(BaseModule): + @dataclass + class Config(BaseModule.Config): + in_channels: int = 1024 + out_channels: int = 3 + cfg: Config + def configure(self) -> None: + super().configure() + self.point_layer = nn.Linear(self.cfg.in_channels, self.cfg.out_channels) + self.initialize_weights() + + def initialize_weights(self): + nn.init.constant_(self.point_layer.weight, 0) + nn.init.constant_(self.point_layer.bias, 0) + + def forward(self, x): + return self.point_layer(x) + +class TriplaneUpsampleNetwork(BaseModule): + @dataclass + class Config(BaseModule.Config): + in_channels: int = 1024 + out_channels: int = 80 + + cfg: Config + + def configure(self) -> None: + super().configure() + self.upsample = nn.ConvTranspose2d( + self.cfg.in_channels, self.cfg.out_channels, kernel_size=2, stride=2 + ) + + def forward( + self, triplanes: Float[Tensor, "B 3 Ci Hp Wp"] + ) -> Float[Tensor, "B 3 Co Hp2 Wp2"]: + triplanes_up = rearrange( + self.upsample( + rearrange(triplanes, "B Np Ci Hp Wp -> (B Np) Ci Hp Wp", Np=3) + ), + "(B Np) Co Hp Wp -> B Np Co Hp Wp", + Np=3, + ) + return triplanes_up + + +class MLP(nn.Module): + def __init__( + self, + dim_in: int, + dim_out: int, + n_neurons: int, + n_hidden_layers: int, + activation: str = "relu", + output_activation: Optional[str] = None, + bias: bool = True, + ): + super().__init__() + layers = [ + self.make_linear( + dim_in, n_neurons, is_first=True, is_last=False, bias=bias + ), + self.make_activation(activation), + ] + for i in range(n_hidden_layers - 1): + layers += [ + self.make_linear( + n_neurons, n_neurons, is_first=False, is_last=False, bias=bias + ), + self.make_activation(activation), + ] + layers += [ + self.make_linear( + n_neurons, dim_out, is_first=False, is_last=True, bias=bias + ) + ] + self.layers = nn.Sequential(*layers) + self.output_activation = get_activation(output_activation) + + def forward(self, x): + x = self.layers(x) + x = self.output_activation(x) + return x + + def make_linear(self, dim_in, dim_out, is_first, is_last, bias=True): + layer = nn.Linear(dim_in, dim_out, bias=bias) + return layer + + def make_activation(self, activation): + if activation == "relu": + return nn.ReLU(inplace=True) + elif activation == "silu": + return nn.SiLU(inplace=True) + else: + raise NotImplementedError + +class GSProjection(nn.Module): + def __init__(self, + in_channels: int = 80, + sh_degree: int = 3, + init_scaling: float = -5.0, + init_density: float = 0.1) -> None: + super().__init__() + + self.out_keys = GS_KEYS + ["shs"] + self.out_channels = GS_CHANNELS + [(sh_degree + 1) ** 2 * 3] + + self.out_layers = nn.ModuleList() + for key, ch in zip(self.out_keys, self.out_channels): + layer = nn.Linear(in_channels, ch) + # initialize + nn.init.constant_(layer.weight, 0) + nn.init.constant_(layer.bias, 0) + + if key == "scaling": + nn.init.constant_(layer.bias, init_scaling) + elif key == "rotation": + nn.init.constant_(layer.bias, 0) + nn.init.constant_(layer.bias[0], 1.0) + elif key == "opacity": + inverse_sigmoid = lambda x: np.log(x / (1 - x)) + nn.init.constant_(layer.bias, inverse_sigmoid(init_density)) + + self.out_layers.append(layer) + + def forward(self, x): + ret = [] + for k, layer in zip(self.out_keys, self.out_layers): + v = layer(x) + if k == "rotation": + v = torch.nn.functional.normalize(v) + elif k == "scaling": + v = torch.exp(v) + # v = v.detach() # FIXME: for DEBUG + elif k == "opacity": + v = torch.sigmoid(v) + # elif k == "shs": + # v = torch.reshape(v, (v.shape[0], -1, 3)) + ret.append(v) + ret = torch.cat(ret, dim=-1) + return ret + +def get_encoding(n_input_dims: int, config) -> nn.Module: + raise NotImplementedError + + +def get_mlp(n_input_dims, n_output_dims, config) -> nn.Module: + raise NotImplementedError + + +# Resnet Blocks for pointnet +class ResnetBlockFC(nn.Module): + ''' Fully connected ResNet Block class. + + Args: + size_in (int): input dimension + size_out (int): output dimension + size_h (int): hidden dimension + ''' + + def __init__(self, size_in, size_out=None, size_h=None): + super().__init__() + # Attributes + if size_out is None: + size_out = size_in + + if size_h is None: + size_h = min(size_in, size_out) + + self.size_in = size_in + self.size_h = size_h + self.size_out = size_out + # Submodules + self.fc_0 = nn.Linear(size_in, size_h) + self.fc_1 = nn.Linear(size_h, size_out) + self.actvn = nn.ReLU() + + if size_in == size_out: + self.shortcut = None + else: + self.shortcut = nn.Linear(size_in, size_out, bias=False) + # Initialization + nn.init.zeros_(self.fc_1.weight) + + def forward(self, x): + net = self.fc_0(self.actvn(x)) + dx = self.fc_1(self.actvn(net)) + + if self.shortcut is not None: + x_s = self.shortcut(x) + else: + x_s = x + + return x_s + dx \ No newline at end of file diff --git a/tgs/models/pointclouds/LICENSE_POINTNET b/tgs/models/pointclouds/LICENSE_POINTNET new file mode 100644 index 0000000..5358ea9 --- /dev/null +++ b/tgs/models/pointclouds/LICENSE_POINTNET @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Songyou Peng, Michael Niemeyer, Lars Mescheder, Marc Pollefeys, Andreas Geiger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/tgs/models/pointclouds/pointnet.py b/tgs/models/pointclouds/pointnet.py new file mode 100644 index 0000000..14b5cbe --- /dev/null +++ b/tgs/models/pointclouds/pointnet.py @@ -0,0 +1,121 @@ +# modified from https://github.com/autonomousvision/convolutional_occupancy_networks/blob/master/src/encoder/pointnet.py +from dataclasses import dataclass +import torch +import torch.nn as nn +from torch_scatter import scatter_mean, scatter_max + +from tgs.utils.base import BaseModule +from tgs.models.networks import ResnetBlockFC +from tgs.utils.ops import scale_tensor + +class LocalPoolPointnet(BaseModule): + ''' PointNet-based encoder network with ResNet blocks for each point. + Number of input points are fixed. + + Args: + c_dim (int): dimension of latent code c + dim (int): input points dimension + hidden_dim (int): hidden dimension of the network + scatter_type (str): feature aggregation when doing local pooling + plane_resolution (int): defined resolution for plane feature + padding (float): conventional padding paramter of ONet for unit cube, so [-0.5, 0.5] -> [-0.55, 0.55] + n_blocks (int): number of blocks ResNetBlockFC layers + ''' + + @dataclass + class Config(BaseModule.Config): + input_channels: int = 3 + c_dim: int = 128 + hidden_dim: int = 128 + scatter_type: str = "max" + plane_size: int = 32 + n_blocks: int = 5 + radius: float = 1. + + cfg: Config + + def configure(self) -> None: + super().configure() + self.fc_pos = nn.Linear(self.cfg.input_channels, 2 * self.cfg.hidden_dim) + self.blocks = nn.ModuleList([ + ResnetBlockFC(2 * self.cfg.hidden_dim, self.cfg.hidden_dim) for i in range(self.cfg.n_blocks) + ]) + self.fc_c = nn.Linear(self.cfg.hidden_dim, self.cfg.c_dim) + + self.actvn = nn.ReLU() + + if self.cfg.scatter_type == 'max': + self.scatter = scatter_max + elif self.cfg.scatter_type == 'mean': + self.scatter = scatter_mean + else: + raise ValueError('incorrect scatter type') + + + def generate_plane_features(self, index, c): + # acquire indices of features in plane + # xy = normalize_coordinate(p.clone(), plane=plane, padding=self.padding) # normalize to the range of (0, 1) + # index = self.coordinate2index(x, self.cfg.plane_size) + + # scatter plane features from points + fea_plane = c.new_zeros(index.shape[0], self.cfg.c_dim, self.cfg.plane_size ** 2) + c = c.permute(0, 2, 1) # B x 512 x T + fea_plane = scatter_mean(c, index, out=fea_plane) # B x 512 x reso^2 + fea_plane = fea_plane.reshape(index.shape[0], self.cfg.c_dim, self.cfg.plane_size, self.cfg.plane_size) # sparce matrix (B x 512 x reso x reso) + + return fea_plane + + def pool_local(self, xy, index, c): + bs, fea_dim = c.shape[0], c.shape[2] + keys = xy.keys() + + c_out = 0 + for key in keys: + # scatter plane features from points + fea = self.scatter(c.permute(0, 2, 1), index[key], dim_size=self.cfg.plane_size ** 2) + if self.scatter == scatter_max: + fea = fea[0] + # gather feature back to points + fea = fea.gather(dim=2, index=index[key].expand(-1, fea_dim, -1)) + c_out += fea + return c_out.permute(0, 2, 1) + + def coordinate2index(self, x): + x = (x * self.cfg.plane_size).long() + index = x[..., 0] + self.cfg.plane_size * x[..., 1] + assert index.max() < self.cfg.plane_size ** 2 + return index[:, None, :] + + def forward(self, p): + batch_size, T, D = p.shape + + # acquire the index for each point + coord = {} + index = {} + + position = torch.clamp(p[..., :3], -self.cfg.radius + 1e-6, self.cfg.radius - 1e-6) + position_norm = scale_tensor(position, (-self.cfg.radius, self.cfg.radius), (0, 1)) + coord["xy"] = position_norm[..., [0, 1]] + coord["xz"] = position_norm[..., [0, 2]] + coord["yz"] = position_norm[..., [1, 2]] + index["xy"] = self.coordinate2index(coord["xy"]) + index["xz"] = self.coordinate2index(coord["xz"]) + index["yz"] = self.coordinate2index(coord["yz"]) + + net = self.fc_pos(p) + + net = self.blocks[0](net) + for block in self.blocks[1:]: + pooled = self.pool_local(coord, index, net) + net = torch.cat([net, pooled], dim=2) + net = block(net) + + c = self.fc_c(net) + + features = torch.stack([ + self.generate_plane_features(index["xy"], c), + self.generate_plane_features(index["xz"], c), + self.generate_plane_features(index["yz"], c) + ], dim=1) + + return features diff --git a/tgs/models/pointclouds/simplepoint.py b/tgs/models/pointclouds/simplepoint.py new file mode 100644 index 0000000..2fb028d --- /dev/null +++ b/tgs/models/pointclouds/simplepoint.py @@ -0,0 +1,110 @@ +from dataclasses import dataclass, field +import torch +from einops import rearrange + +import tgs +from tgs.utils.base import BaseModule +from tgs.utils.typing import * + +class SimplePointGenerator(BaseModule): + @dataclass + class Config(BaseModule.Config): + camera_embedder_cls: str = "" + camera_embedder: dict = field(default_factory=dict) + + image_tokenizer_cls: str = "" + image_tokenizer: dict = field(default_factory=dict) + + tokenizer_cls: str = "" + tokenizer: dict = field(default_factory=dict) + + backbone_cls: str = "" + backbone: dict = field(default_factory=dict) + + post_processor_cls: str = "" + post_processor: dict = field(default_factory=dict) + + pointcloud_upsampling_cls: str = "" + pointcloud_upsampling: dict = field(default_factory=dict) + + flip_c2w_cond: bool = True + + cfg: Config + + def configure(self) -> None: + super().configure() + + self.image_tokenizer = tgs.find(self.cfg.image_tokenizer_cls)( + self.cfg.image_tokenizer + ) + + assert self.cfg.camera_embedder_cls == 'tgs.models.networks.MLP' + weights = self.cfg.camera_embedder.pop("weights") if "weights" in self.cfg.camera_embedder else None + self.camera_embedder = tgs.find(self.cfg.camera_embedder_cls)(**self.cfg.camera_embedder) + if weights: + from tgs.utils.misc import load_module_weights + weights_path, module_name = weights.split(":") + state_dict = load_module_weights( + weights_path, module_name=module_name, map_location="cpu" + ) + self.camera_embedder.load_state_dict(state_dict) + + self.tokenizer = tgs.find(self.cfg.tokenizer_cls)(self.cfg.tokenizer) + + self.backbone = tgs.find(self.cfg.backbone_cls)(self.cfg.backbone) + + self.post_processor = tgs.find(self.cfg.post_processor_cls)( + self.cfg.post_processor + ) + + self.pointcloud_upsampling = tgs.find(self.cfg.pointcloud_upsampling_cls)(self.cfg.pointcloud_upsampling) + + def forward(self, batch, encoder_hidden_states=None, **kwargs): + batch_size, n_input_views = batch["rgb_cond"].shape[:2] + + if encoder_hidden_states is None: + # Camera modulation + c2w_cond = batch["c2w_cond"].clone() + if self.cfg.flip_c2w_cond: + c2w_cond[..., :3, 1:3] *= -1 + camera_extri = c2w_cond.view(*c2w_cond.shape[:-2], -1) + camera_intri = batch["intrinsic_normed_cond"].view( + *batch["intrinsic_normed_cond"].shape[:-2], -1) + camera_feats = torch.cat([camera_intri, camera_extri], dim=-1) + # camera_feats = rearrange(camera_feats, 'B Nv C -> (B Nv) C') + + camera_feats = self.camera_embedder(camera_feats) + + encoder_hidden_states: Float[Tensor, "B Cit Nit"] = self.image_tokenizer( + rearrange(batch["rgb_cond"], 'B Nv H W C -> B Nv C H W'), + modulation_cond=camera_feats, + ) + encoder_hidden_states = rearrange( + encoder_hidden_states, 'B Nv C Nt -> B (Nv Nt) C', Nv=n_input_views) + + tokens: Float[Tensor, "B Ct Nt"] = self.tokenizer(batch_size) + + tokens = self.backbone( + tokens, + encoder_hidden_states=encoder_hidden_states, + modulation_cond=None, + ) + pointclouds = self.post_processor(self.tokenizer.detokenize(tokens)) + + upsampling_input = { + "input_image_tokens": encoder_hidden_states.permute(0, 2, 1), + "input_image_tokens_global": encoder_hidden_states[:, :1], + "c2w_cond": c2w_cond, + "rgb_cond": batch["rgb_cond"], + "intrinsic_cond": batch["intrinsic_cond"], + "intrinsic_normed_cond": batch["intrinsic_normed_cond"], + "points": pointclouds.float() + } + up_results = self.pointcloud_upsampling(upsampling_input) + up_results.insert(0, pointclouds) + pointclouds = up_results[-1] + out = { + "points": pointclouds, + "up_results": up_results + } + return out \ No newline at end of file diff --git a/tgs/models/renderer.py b/tgs/models/renderer.py new file mode 100644 index 0000000..b897303 --- /dev/null +++ b/tgs/models/renderer.py @@ -0,0 +1,427 @@ +from dataclasses import dataclass, field +from collections import defaultdict +from diff_gaussian_rasterization import GaussianRasterizationSettings, GaussianRasterizer +from plyfile import PlyData, PlyElement +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np +import math + +from ..utils.typing import * +from ..utils.base import BaseModule +from ..utils.ops import trunc_exp +from ..utils.ops import scale_tensor +from .networks import MLP +from einops import rearrange, reduce + +inverse_sigmoid = lambda x: np.log(x / (1 - x)) + +def getWorld2View2(R, t, translate=np.array([.0, .0, .0]), scale=1.0): + Rt = np.zeros((4, 4)) + Rt[:3, :3] = R.transpose() + Rt[:3, 3] = t + Rt[3, 3] = 1.0 + + C2W = np.linalg.inv(Rt) + cam_center = C2W[:3, 3] + cam_center = (cam_center + translate) * scale + C2W[:3, 3] = cam_center + Rt = np.linalg.inv(C2W) + return np.float32(Rt) + +def getProjectionMatrix(znear, zfar, fovX, fovY): + tanHalfFovY = math.tan((fovY / 2)) + tanHalfFovX = math.tan((fovX / 2)) + + top = tanHalfFovY * znear + bottom = -top + right = tanHalfFovX * znear + left = -right + + P = torch.zeros(4, 4) + + z_sign = 1.0 + + P[0, 0] = 2.0 * znear / (right - left) + P[1, 1] = 2.0 * znear / (top - bottom) + P[0, 2] = (right + left) / (right - left) + P[1, 2] = (top + bottom) / (top - bottom) + P[3, 2] = z_sign + P[2, 2] = z_sign * zfar / (zfar - znear) + P[2, 3] = -(zfar * znear) / (zfar - znear) + return P + +def intrinsic_to_fov(intrinsic, w, h): + fx, fy = intrinsic[0, 0], intrinsic[1, 1] + fov_x = 2 * torch.arctan2(w, 2 * fx) + fov_y = 2 * torch.arctan2(h, 2 * fy) + return fov_x, fov_y + + +class Camera: + def __init__(self, w2c, intrinsic, FoVx, FoVy, height, width, trans=np.array([0.0, 0.0, 0.0]), scale=1.0) -> None: + self.FoVx = FoVx + self.FoVy = FoVy + self.height = height + self.width = width + self.world_view_transform = w2c.transpose(0, 1) + + self.zfar = 100.0 + self.znear = 0.01 + + self.trans = trans + self.scale = scale + + self.projection_matrix = getProjectionMatrix(znear=self.znear, zfar=self.zfar, fovX=self.FoVx, fovY=self.FoVy).transpose(0,1).to(w2c.device) + self.full_proj_transform = (self.world_view_transform.unsqueeze(0).bmm(self.projection_matrix.unsqueeze(0))).squeeze(0) + self.camera_center = self.world_view_transform.inverse()[3, :3] + + @staticmethod + def from_c2w(c2w, intrinsic, height, width): + w2c = torch.inverse(c2w) + FoVx, FoVy = intrinsic_to_fov(intrinsic, w=torch.tensor(width, device=w2c.device), h=torch.tensor(height, device=w2c.device)) + return Camera(w2c=w2c, intrinsic=intrinsic, FoVx=FoVx, FoVy=FoVy, height=height, width=width) + +class GaussianModel(NamedTuple): + xyz: Tensor + opacity: Tensor + rotation: Tensor + scaling: Tensor + shs: Tensor + + def construct_list_of_attributes(self): + l = ['x', 'y', 'z', 'nx', 'ny', 'nz'] + features_dc = self.shs[:, :1] + features_rest = self.shs[:, 1:] + for i in range(features_dc.shape[1]*features_dc.shape[2]): + l.append('f_dc_{}'.format(i)) + for i in range(features_rest.shape[1]*features_rest.shape[2]): + l.append('f_rest_{}'.format(i)) + l.append('opacity') + for i in range(self.scaling.shape[1]): + l.append('scale_{}'.format(i)) + for i in range(self.rotation.shape[1]): + l.append('rot_{}'.format(i)) + return l + + def to_ply(self): + + xyz = self.xyz.detach().cpu().numpy() + normals = np.zeros_like(xyz) + features_dc = self.shs[:, :1] + features_rest = self.shs[:, 1:] + f_dc = features_dc.detach().flatten(start_dim=1).contiguous().cpu().numpy() + f_rest = features_rest.detach().flatten(start_dim=1).contiguous().cpu().numpy() + opacities = inverse_sigmoid(torch.clamp(self.opacity, 1e-3, 1 - 1e-3).detach().cpu().numpy()) + scale = np.log(self.scaling.detach().cpu().numpy()) + rotation = self.rotation.detach().cpu().numpy() + + dtype_full = [(attribute, 'f4') for attribute in self.construct_list_of_attributes()] + + elements = np.empty(xyz.shape[0], dtype=dtype_full) + attributes = np.concatenate((xyz, normals, f_dc, f_rest, opacities, scale, rotation), axis=1) + elements[:] = list(map(tuple, attributes)) + el = PlyElement.describe(elements, 'vertex') + return PlyData([el]) + +class GSLayer(BaseModule): + @dataclass + class Config(BaseModule.Config): + in_channels: int = 128 + feature_channels: dict = field(default_factory=dict) + xyz_offset: bool = True + restrict_offset: bool = False + use_rgb: bool = False + clip_scaling: Optional[float] = None + init_scaling: float = -5.0 + init_density: float = 0.1 + + cfg: Config + + def configure(self, *args, **kwargs) -> None: + self.out_layers = nn.ModuleList() + for key, out_ch in self.cfg.feature_channels.items(): + if key == "shs" and self.cfg.use_rgb: + out_ch = 3 + layer = nn.Linear(self.cfg.in_channels, out_ch) + + # initialize + if not (key == "shs" and self.cfg.use_rgb): + nn.init.constant_(layer.weight, 0) + nn.init.constant_(layer.bias, 0) + if key == "scaling": + nn.init.constant_(layer.bias, self.cfg.init_scaling) + elif key == "rotation": + nn.init.constant_(layer.bias, 0) + nn.init.constant_(layer.bias[0], 1.0) + elif key == "opacity": + nn.init.constant_(layer.bias, inverse_sigmoid(self.cfg.init_density)) + + self.out_layers.append(layer) + + def forward(self, x, pts): + ret = {} + for k, layer in zip(self.cfg.feature_channels.keys(), self.out_layers): + v = layer(x) + if k == "rotation": + v = torch.nn.functional.normalize(v) + elif k == "scaling": + v = trunc_exp(v) + if self.cfg.clip_scaling is not None: + v = torch.clamp(v, min=0, max=self.cfg.clip_scaling) + elif k == "opacity": + v = torch.sigmoid(v) + elif k == "shs": + if self.cfg.use_rgb: + v = torch.sigmoid(v) + v = torch.reshape(v, (v.shape[0], -1, 3)) + elif k == "xyz": + if self.cfg.restrict_offset: + max_step = 1.2 / 32 + v = (torch.sigmoid(v) - 0.5) * max_step + v = v + pts if self.cfg.xyz_offset else pts + ret[k] = v + + return GaussianModel(**ret) + +class GS3DRenderer(BaseModule): + @dataclass + class Config(BaseModule.Config): + mlp_network_config: Optional[dict] = None + gs_out: dict = field(default_factory=dict) + sh_degree: int = 3 + scaling_modifier: float = 1.0 + random_background: bool = False + radius: float = 1.0 + feature_reduction: str = "concat" + projection_feature_dim: int = 773 + background_color: Tuple[float, float, float] = field( + default_factory=lambda: (1.0, 1.0, 1.0) + ) + + cfg: Config + + def configure(self, *args, **kwargs) -> None: + if self.cfg.feature_reduction == "mean": + mlp_in = 80 + elif self.cfg.feature_reduction == "concat": + mlp_in = 80 * 3 + else: + raise NotImplementedError + mlp_in = mlp_in + self.cfg.projection_feature_dim + if self.cfg.mlp_network_config is not None: + self.mlp_net = MLP(mlp_in, self.cfg.gs_out.in_channels, **self.cfg.mlp_network_config) + else: + self.cfg.gs_out.in_channels = mlp_in + self.gs_net = GSLayer(self.cfg.gs_out) + + def forward_gs(self, x, p): + if self.cfg.mlp_network_config is not None: + x = self.mlp_net(x) + return self.gs_net(x, p) + + def forward_single_view(self, + gs: GaussianModel, + viewpoint_camera: Camera, + background_color: Optional[Float[Tensor, "3"]], + ret_mask: bool = True, + ): + # Create zero tensor. We will use it to make pytorch return gradients of the 2D (screen-space) means + screenspace_points = torch.zeros_like(gs.xyz, dtype=gs.xyz.dtype, requires_grad=True, device=self.device) + 0 + try: + screenspace_points.retain_grad() + except: + pass + + bg_color = background_color + # Set up rasterization configuration + tanfovx = math.tan(viewpoint_camera.FoVx * 0.5) + tanfovy = math.tan(viewpoint_camera.FoVy * 0.5) + + raster_settings = GaussianRasterizationSettings( + image_height=int(viewpoint_camera.height), + image_width=int(viewpoint_camera.width), + tanfovx=tanfovx, + tanfovy=tanfovy, + bg=bg_color, + scale_modifier=self.cfg.scaling_modifier, + viewmatrix=viewpoint_camera.world_view_transform, + projmatrix=viewpoint_camera.full_proj_transform.float(), + sh_degree=self.cfg.sh_degree, + campos=viewpoint_camera.camera_center, + prefiltered=False, + debug=False + ) + + rasterizer = GaussianRasterizer(raster_settings=raster_settings) + + means3D = gs.xyz + means2D = screenspace_points + opacity = gs.opacity + + # If precomputed 3d covariance is provided, use it. If not, then it will be computed from + # scaling / rotation by the rasterizer. + scales = None + rotations = None + cov3D_precomp = None + scales = gs.scaling + rotations = gs.rotation + + # If precomputed colors are provided, use them. Otherwise, if it is desired to precompute colors + # from SHs in Python, do it. If not, then SH -> RGB conversion will be done by rasterizer. + shs = None + colors_precomp = None + if self.gs_net.cfg.use_rgb: + colors_precomp = gs.shs.squeeze(1) + else: + shs = gs.shs + + # Rasterize visible Gaussians to image, obtain their radii (on screen). + with torch.autocast(device_type=self.device.type, dtype=torch.float32): + rendered_image, radii, rendered_depth, rendered_alpha = rasterizer( + means3D = means3D, + means2D = means2D, + shs = shs, + colors_precomp = colors_precomp, + opacities = opacity, + scales = scales, + rotations = rotations, + cov3D_precomp = cov3D_precomp) + + ret = { + "comp_rgb": rendered_image.permute(1, 2, 0), + "comp_rgb_bg": bg_color + } + + if ret_mask: + mask_bg_color = torch.zeros(3, dtype=torch.float32, device=self.device) + raster_settings = GaussianRasterizationSettings( + image_height=int(viewpoint_camera.height), + image_width=int(viewpoint_camera.width), + tanfovx=tanfovx, + tanfovy=tanfovy, + bg=mask_bg_color, + scale_modifier=self.cfg.scaling_modifier, + viewmatrix=viewpoint_camera.world_view_transform, + projmatrix=viewpoint_camera.full_proj_transform.float(), + sh_degree=0, + campos=viewpoint_camera.camera_center, + prefiltered=False, + debug=False + ) + rasterizer = GaussianRasterizer(raster_settings=raster_settings) + + with torch.autocast(device_type=self.device.type, dtype=torch.float32): + rendered_mask, radii, rendered_depth, rendered_alpha = rasterizer( + means3D = means3D, + means2D = means2D, + # shs = , + colors_precomp = torch.ones_like(means3D), + opacities = opacity, + scales = scales, + rotations = rotations, + cov3D_precomp = cov3D_precomp) + ret["comp_mask"] = rendered_mask.permute(1, 2, 0) + + return ret + + def query_triplane( + self, + positions: Float[Tensor, "*B N 3"], + triplanes: Float[Tensor, "*B 3 Cp Hp Wp"], + ) -> Dict[str, Tensor]: + batched = positions.ndim == 3 + if not batched: + # no batch dimension + triplanes = triplanes[None, ...] + positions = positions[None, ...] + + positions = scale_tensor(positions, (-self.cfg.radius, self.cfg.radius), (-1, 1)) + indices2D: Float[Tensor, "B 3 N 2"] = torch.stack( + (positions[..., [0, 1]], positions[..., [0, 2]], positions[..., [1, 2]]), + dim=-3, + ) + out: Float[Tensor, "B3 Cp 1 N"] = F.grid_sample( + rearrange(triplanes, "B Np Cp Hp Wp -> (B Np) Cp Hp Wp", Np=3), + rearrange(indices2D, "B Np N Nd -> (B Np) () N Nd", Np=3), + align_corners=False, + mode="bilinear", + ) + if self.cfg.feature_reduction == "concat": + out = rearrange(out, "(B Np) Cp () N -> B N (Np Cp)", Np=3) + elif self.cfg.feature_reduction == "mean": + out = reduce(out, "(B Np) Cp () N -> B N Cp", Np=3, reduction="mean") + else: + raise NotImplementedError + + if not batched: + out = out.squeeze(0) + + return out + + def forward_single_batch( + self, + gs_hidden_features: Float[Tensor, "Np Cp"], + query_points: Float[Tensor, "Np 3"], + c2ws: Float[Tensor, "Nv 4 4"], + intrinsics: Float[Tensor, "Nv 4 4"], + height: int, + width: int, + background_color: Optional[Float[Tensor, "3"]], + ): + gs: GaussianModel = self.forward_gs(gs_hidden_features, query_points) + out_list = [] + + for c2w, intrinsic in zip(c2ws, intrinsics): + out_list.append(self.forward_single_view( + gs, + Camera.from_c2w(c2w, intrinsic, height, width), + background_color + )) + + out = defaultdict(list) + for out_ in out_list: + for k, v in out_.items(): + out[k].append(v) + out = {k: torch.stack(v, dim=0) for k, v in out.items()} + out["3dgs"] = gs + + return out + + def forward(self, + gs_hidden_features: Float[Tensor, "B Np Cp"], + query_points: Float[Tensor, "B Np 3"], + c2w: Float[Tensor, "B Nv 4 4"], + intrinsic: Float[Tensor, "B Nv 4 4"], + height, + width, + additional_features: Optional[Float[Tensor, "B C H W"]] = None, + background_color: Optional[Float[Tensor, "B 3"]] = None, + **kwargs): + batch_size = gs_hidden_features.shape[0] + out_list = [] + gs_hidden_features = self.query_triplane(query_points, gs_hidden_features) + if additional_features is not None: + gs_hidden_features = torch.cat([gs_hidden_features, additional_features], dim=-1) + + for b in range(batch_size): + out_list.append(self.forward_single_batch( + gs_hidden_features[b], + query_points[b], + c2w[b], + intrinsic[b], + height, width, + background_color[b] if background_color is not None else None)) + + out = defaultdict(list) + for out_ in out_list: + for k, v in out_.items(): + out[k].append(v) + for k, v in out.items(): + if isinstance(v[0], torch.Tensor): + out[k] = torch.stack(v, dim=0) + else: + out[k] = v + return out + \ No newline at end of file diff --git a/tgs/models/snowflake/LICENSE b/tgs/models/snowflake/LICENSE new file mode 100644 index 0000000..01c2b54 --- /dev/null +++ b/tgs/models/snowflake/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 AllenXiang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tgs/models/snowflake/SPD.py b/tgs/models/snowflake/SPD.py new file mode 100644 index 0000000..eae592c --- /dev/null +++ b/tgs/models/snowflake/SPD.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# @Author: Peng Xiang + +import torch +import torch.nn as nn +from .utils import MLP_Res, MLP_CONV +from .skip_transformer import SkipTransformer + + +class SPD(nn.Module): + def __init__(self, dim_feat=512, up_factor=2, i=0, radius=1, bounding=True, global_feat=True): + """Snowflake Point Deconvolution""" + super(SPD, self).__init__() + self.i = i + self.up_factor = up_factor + + self.bounding = bounding + self.radius = radius + + self.global_feat = global_feat + self.ps_dim = 32 if global_feat else 64 + + self.mlp_1 = MLP_CONV(in_channel=3, layer_dims=[64, 128]) + self.mlp_2 = MLP_CONV(in_channel=128 * 2 + dim_feat if self.global_feat else 128, layer_dims=[256, 128]) + + self.skip_transformer = SkipTransformer(in_channel=128, dim=64) + + self.mlp_ps = MLP_CONV(in_channel=128, layer_dims=[64, self.ps_dim]) + self.ps = nn.ConvTranspose1d(self.ps_dim, 128, up_factor, up_factor, bias=False) # point-wise splitting + + self.up_sampler = nn.Upsample(scale_factor=up_factor) + self.mlp_delta_feature = MLP_Res(in_dim=256, hidden_dim=128, out_dim=128) + + self.mlp_delta = MLP_CONV(in_channel=128, layer_dims=[64, 3]) + + def forward(self, pcd_prev, feat_global=None, K_prev=None): + """ + Args: + pcd_prev: Tensor, (B, 3, N_prev) + feat_global: Tensor, (B, dim_feat, 1) + K_prev: Tensor, (B, 128, N_prev) + + Returns: + pcd_child: Tensor, up sampled point cloud, (B, 3, N_prev * up_factor) + K_curr: Tensor, displacement feature of current step, (B, 128, N_prev * up_factor) + """ + b, _, n_prev = pcd_prev.shape + feat_1 = self.mlp_1(pcd_prev) + feat_1 = torch.cat([feat_1, + torch.max(feat_1, 2, keepdim=True)[0].repeat((1, 1, feat_1.size(2))), + feat_global.repeat(1, 1, feat_1.size(2))], 1) if self.global_feat else feat_1 + Q = self.mlp_2(feat_1) + + H = self.skip_transformer(pcd_prev, K_prev if K_prev is not None else Q, Q) + + feat_child = self.mlp_ps(H) + feat_child = self.ps(feat_child) # (B, 128, N_prev * up_factor) + H_up = self.up_sampler(H) + K_curr = self.mlp_delta_feature(torch.cat([feat_child, H_up], 1)) + + delta = self.mlp_delta(torch.relu(K_curr)) + if self.bounding: + delta = torch.tanh(delta) / self.radius**self.i # (B, 3, N_prev * up_factor) + + pcd_child = self.up_sampler(pcd_prev) + pcd_child = pcd_child + delta + + return pcd_child, K_curr \ No newline at end of file diff --git a/tgs/models/snowflake/SPD_crossattn.py b/tgs/models/snowflake/SPD_crossattn.py new file mode 100644 index 0000000..90273e9 --- /dev/null +++ b/tgs/models/snowflake/SPD_crossattn.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# @Author: Peng Xiang + +import torch +import torch.nn as nn +from .utils import MLP_Res, MLP_CONV +from .skip_transformer import SkipTransformer +from .attention import ResidualTransformerBlock + +class SPD_crossattn(nn.Module): + def __init__(self, dim_feat=512, up_factor=2, i=0, radius=1, bounding=True, global_feat=True): + """Snowflake Point Deconvolution""" + super().__init__() + self.i = i + self.up_factor = up_factor + + self.bounding = bounding + self.radius = radius + + self.global_feat = global_feat + self.ps_dim = 32 if global_feat else 64 + + self.mlp_1 = MLP_CONV(in_channel=3, layer_dims=[64, 128]) + self.pcd_image_attn = ResidualTransformerBlock( + device=torch.device('cuda'), + dtype=torch.float32, + n_data=128, + width=128, + heads=8, + init_scale=1.0, + ) + + self.mlp_2 = MLP_CONV(in_channel=128 * 2 + dim_feat if self.global_feat else 128, layer_dims=[256, 128]) + + self.skip_transformer = SkipTransformer(in_channel=128, dim=64) + + self.mlp_ps = MLP_CONV(in_channel=128, layer_dims=[64, self.ps_dim]) + self.ps = nn.ConvTranspose1d(self.ps_dim, 128, up_factor, up_factor, bias=False) # point-wise splitting + + self.up_sampler = nn.Upsample(scale_factor=up_factor) + self.mlp_delta_feature = MLP_Res(in_dim=256, hidden_dim=128, out_dim=128) + + self.mlp_delta = MLP_CONV(in_channel=128, layer_dims=[64, 3]) + + def forward(self, pcd_prev, feat_global=None, K_prev=None): + """ + Args: + pcd_prev: Tensor, (B, 3, N_prev) + feat_global: Tensor, (B, dim_feat, 1) + K_prev: Tensor, (B, 128, N_prev) + + Returns: + pcd_child: Tensor, up sampled point cloud, (B, 3, N_prev * up_factor) + K_curr: Tensor, displacement feature of current step, (B, 128, N_prev * up_factor) + """ + b, _, n_prev = pcd_prev.shape + feat_1 = self.mlp_1(pcd_prev) + # feat_1 = torch.cat([feat_1, + # torch.max(feat_1, 2, keepdim=True)[0].repeat((1, 1, feat_1.size(2))), + # feat_global.repeat(1, 1, feat_1.size(2))], 1) if self.global_feat else feat_1 + feat_1 = torch.permute(feat_1, (0, 2, 1)) + feat_global = torch.permute(feat_global, (0, 2, 1)) + feat_1 = self.pcd_image_attn(feat_1, feat_global) + Q = torch.permute(feat_1, (0, 2, 1)) + # Q = self.mlp_2(feat_1) + + H = self.skip_transformer(pcd_prev, K_prev if K_prev is not None else Q, Q) + + feat_child = self.mlp_ps(H) + feat_child = self.ps(feat_child) # (B, 128, N_prev * up_factor) + H_up = self.up_sampler(H) + K_curr = self.mlp_delta_feature(torch.cat([feat_child, H_up], 1)) + + delta = self.mlp_delta(torch.relu(K_curr)) + if self.bounding: + delta = torch.tanh(delta) / self.radius**self.i # (B, 3, N_prev * up_factor) + + pcd_child = self.up_sampler(pcd_prev) + pcd_child = pcd_child + delta + + return pcd_child, K_curr \ No newline at end of file diff --git a/tgs/models/snowflake/SPD_pp.py b/tgs/models/snowflake/SPD_pp.py new file mode 100644 index 0000000..886444e --- /dev/null +++ b/tgs/models/snowflake/SPD_pp.py @@ -0,0 +1,71 @@ + +import torch +import torch.nn as nn +from .utils import MLP_Res, MLP_CONV +from .skip_transformer import SkipTransformer + +class SPD_pp(nn.Module): + def __init__(self, dim_feat=512, up_factor=2, i=0, radius=1, bounding=True, global_feat=True): + """Snowflake Point Deconvolution""" + super(SPD_pp, self).__init__() + self.i = i + self.up_factor = up_factor + + self.bounding = bounding + self.radius = radius + + self.global_feat = global_feat + self.ps_dim = 32 if global_feat else 64 + + self.mlp_1 = MLP_CONV(in_channel=3, layer_dims=[64, 128]) + self.mlp_2 = MLP_CONV( + in_channel=128 * 2 + dim_feat if self.global_feat else 128, layer_dims=[256, 128]) + + self.skip_transformer = SkipTransformer(in_channel=128, dim=64) + + self.mlp_ps = MLP_CONV(in_channel=128, layer_dims=[64, self.ps_dim]) + self.ps = nn.ConvTranspose1d( + self.ps_dim, 128, up_factor, up_factor, bias=False) # point-wise splitting + + self.up_sampler = nn.Upsample(scale_factor=up_factor) + self.mlp_delta_feature = MLP_Res( + in_dim=256, hidden_dim=128, out_dim=128) + + self.mlp_delta = MLP_CONV(in_channel=128, layer_dims=[64, 3]) + + def forward(self, pcd_prev, feat_cond=None, K_prev=None): + """ + Args: + pcd_prev: Tensor, (B, 3, N_prev) + feat_cond: Tensor, (B, dim_feat, N_prev) + K_prev: Tensor, (B, 128, N_prev) + + Returns: + pcd_child: Tensor, up sampled point cloud, (B, 3, N_prev * up_factor) + K_curr: Tensor, displacement feature of current step, (B, 128, N_prev * up_factor) + """ + b, _, n_prev = pcd_prev.shape + feat_1 = self.mlp_1(pcd_prev) + feat_1 = torch.cat([feat_1, + torch.max(feat_1, 2, keepdim=True)[ + 0].repeat((1, 1, feat_1.size(2))), + feat_cond], 1) if self.global_feat else feat_1 + Q = self.mlp_2(feat_1) + + H = self.skip_transformer( + pcd_prev, K_prev if K_prev is not None else Q, Q) + + feat_child = self.mlp_ps(H) + feat_child = self.ps(feat_child) # (B, 128, N_prev * up_factor) + H_up = self.up_sampler(H) + K_curr = self.mlp_delta_feature(torch.cat([feat_child, H_up], 1)) + + delta = self.mlp_delta(torch.relu(K_curr)) + if self.bounding: + # (B, 3, N_prev * up_factor) + delta = torch.tanh(delta) / self.radius**self.i + + pcd_child = self.up_sampler(pcd_prev) + pcd_child = pcd_child + delta + + return pcd_child, K_curr diff --git a/tgs/models/snowflake/attention.py b/tgs/models/snowflake/attention.py new file mode 100644 index 0000000..51d4d80 --- /dev/null +++ b/tgs/models/snowflake/attention.py @@ -0,0 +1,239 @@ +import torch +import torch.nn as nn +import math +import math +from typing import Optional +from typing import Callable, Iterable, Sequence, Union + +import torch + +def checkpoint( + func: Callable[..., Union[torch.Tensor, Sequence[torch.Tensor]]], + inputs: Sequence[torch.Tensor], + params: Iterable[torch.Tensor], + flag: bool, +): + """ + Evaluate a function without caching intermediate activations, allowing for + reduced memory at the expense of extra compute in the backward pass. + :param func: the function to evaluate. + :param inputs: the argument sequence to pass to `func`. + :param params: a sequence of parameters `func` depends on but does not + explicitly take as arguments. + :param flag: if False, disable gradient checkpointing. + """ + if flag: + args = tuple(inputs) + tuple(params) + return CheckpointFunction.apply(func, len(inputs), *args) + else: + return func(*inputs) + + +class CheckpointFunction(torch.autograd.Function): + @staticmethod + def forward(ctx, run_function, length, *args): + ctx.run_function = run_function + ctx.input_tensors = list(args[:length]) + ctx.input_params = list(args[length:]) + with torch.no_grad(): + output_tensors = ctx.run_function(*ctx.input_tensors) + return output_tensors + + @staticmethod + def backward(ctx, *output_grads): + ctx.input_tensors = [x.detach().requires_grad_(True) + for x in ctx.input_tensors] + with torch.enable_grad(): + # Fixes a bug where the first op in run_function modifies the + # Tensor storage in place, which is not allowed for detach()'d + # Tensors. + shallow_copies = [x.view_as(x) for x in ctx.input_tensors] + output_tensors = ctx.run_function(*shallow_copies) + input_grads = torch.autograd.grad( + output_tensors, + ctx.input_tensors + ctx.input_params, + output_grads, + allow_unused=True, + ) + del ctx.input_tensors + del ctx.input_params + del output_tensors + return (None, None) + input_grads + + +def init_linear(l, stddev): + nn.init.normal_(l.weight, std=stddev) + if l.bias is not None: + nn.init.constant_(l.bias, 0.0) + +class MLP(nn.Module): + def __init__(self, *, device: torch.device, dtype: torch.dtype, width: int, init_scale: float): + super().__init__() + self.width = width + self.c_fc = nn.Linear(width, width * 4, device=device, dtype=dtype) + self.c_proj = nn.Linear(width * 4, width, device=device, dtype=dtype) + self.gelu = nn.GELU() + init_linear(self.c_fc, init_scale) + init_linear(self.c_proj, init_scale) + + def forward(self, x): + return self.c_proj(self.gelu(self.c_fc(x))) + +class QKVMultiheadCrossAttention(nn.Module): + def __init__(self, *, device: torch.device, dtype: torch.dtype, heads: int, n_data: int): + super().__init__() + self.device = device + self.dtype = dtype + self.heads = heads + self.n_data = n_data + + def forward(self, q, kv): + _, n_ctx, _ = q.shape + bs, n_data, width = kv.shape + attn_ch = width // self.heads // 2 + scale = 1 / math.sqrt(math.sqrt(attn_ch)) + q = q.view(bs, n_ctx, self.heads, -1) + kv = kv.view(bs, n_data, self.heads, -1) + k, v = torch.split(kv, attn_ch, dim=-1) + weight = torch.einsum( + "bthc,bshc->bhts", q * scale, k * scale + ) # More stable with f16 than dividing afterwards + wdtype = weight.dtype + weight = torch.softmax(weight.float(), dim=-1).type(wdtype) + return torch.einsum("bhts,bshc->bthc", weight, v).reshape(bs, n_ctx, -1) + + + +class QKVMultiheadAttention(nn.Module): + def __init__(self, *, device: torch.device, dtype: torch.dtype, heads: int, n_ctx: int): + super().__init__() + self.device = device + self.dtype = dtype + self.heads = heads + self.n_ctx = n_ctx + + def forward(self, qkv): + bs, n_ctx, width = qkv.shape + attn_ch = width // self.heads // 3 + scale = 1 / math.sqrt(math.sqrt(attn_ch)) + qkv = qkv.view(bs, n_ctx, self.heads, -1) + q, k, v = torch.split(qkv, attn_ch, dim=-1) + weight = torch.einsum( + "bthc,bshc->bhts", q * scale, k * scale + ) # More stable with f16 than dividing afterwards + wdtype = weight.dtype + weight = torch.softmax(weight.float(), dim=-1).type(wdtype) + return torch.einsum("bhts,bshc->bthc", weight, v).reshape(bs, n_ctx, -1) + + + +class MultiheadCrossAttention(nn.Module): + def __init__( + self, + *, + device: torch.device, + dtype: torch.dtype, + n_data: int, + width: int, + heads: int, + init_scale: float, + data_width: Optional[int] = None, + ): + super().__init__() + self.n_data = n_data + self.width = width + self.heads = heads + self.data_width = width if data_width is None else data_width + self.c_q = nn.Linear(width, width, device=device, dtype=dtype) + self.c_kv = nn.Linear(self.data_width, width * 2, + device=device, dtype=dtype) + self.c_proj = nn.Linear(width, width, device=device, dtype=dtype) + self.attention = QKVMultiheadCrossAttention( + device=device, dtype=dtype, heads=heads, n_data=n_data + ) + init_linear(self.c_q, init_scale) + init_linear(self.c_kv, init_scale) + init_linear(self.c_proj, init_scale) + + def forward(self, x, data): + x = self.c_q(x) + data = self.c_kv(data) + x = checkpoint(self.attention, (x, data), (), True) + x = self.c_proj(x) + return x + + +class MultiheadAttention(nn.Module): + def __init__( + self, + *, + device: torch.device, + dtype: torch.dtype, + n_ctx: int, + width: int, + heads: int, + init_scale: float, + ): + super().__init__() + self.n_ctx = n_ctx + self.width = width + self.heads = heads + self.c_qkv = nn.Linear(width, width * 3, device=device, dtype=dtype) + self.c_proj = nn.Linear(width, width, device=device, dtype=dtype) + self.attention = QKVMultiheadAttention(device=device, dtype=dtype, heads=heads, n_ctx=n_ctx) + init_linear(self.c_qkv, init_scale) + init_linear(self.c_proj, init_scale) + + def forward(self, x): + x = self.c_qkv(x) + x = checkpoint(self.attention, (x,), (), True) + x = self.c_proj(x) + return x + + +class ResidualTransformerBlock(nn.Module): + def __init__( + self, + *, + device: torch.device, + dtype: torch.dtype, + n_data: int, + width: int, + heads: int, + data_width: Optional[int] = None, + init_scale: float = 1.0, + ): + super().__init__() + + if data_width is None: + data_width = width + + self.attn_cross = MultiheadCrossAttention( + device=device, + dtype=dtype, + n_data=n_data, + width=width, + heads=heads, + data_width=data_width, + init_scale=init_scale, + ) + self.attn_self = MultiheadAttention( + device=device, + dtype=dtype, + n_ctx=n_data, + width=width, + heads=heads, + init_scale=init_scale, + ) + self.ln_1 = nn.LayerNorm(width, device=device, dtype=dtype) + self.ln_2 = nn.LayerNorm(data_width, device=device, dtype=dtype) + self.ln_3 = nn.LayerNorm(width, device=device, dtype=dtype) + self.mlp = MLP(device=device, dtype=dtype, + width=width, init_scale=init_scale) + self.ln_4 = nn.LayerNorm(width, device=device, dtype=dtype) + + def forward(self, x: torch.Tensor, data: torch.Tensor): + x = x + self.attn_cross(self.ln_1(x), self.ln_2(data)) + x = x + self.attn_self(self.ln_3(x)) + x = x + self.mlp(self.ln_4(x)) + return x \ No newline at end of file diff --git a/tgs/models/snowflake/model_spdpp.py b/tgs/models/snowflake/model_spdpp.py new file mode 100644 index 0000000..2cbb192 --- /dev/null +++ b/tgs/models/snowflake/model_spdpp.py @@ -0,0 +1,188 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +from tgs.utils.base import BaseModule +from tgs.utils.typing import * +from dataclasses import dataclass, field + +from pytorch3d.renderer import ( + AlphaCompositor, + NormWeightedCompositor, + PointsRasterizationSettings, + PointsRasterizer, + PointsRenderer) +from pytorch3d.renderer.cameras import CamerasBase +from pytorch3d.structures import Pointclouds +from pytorch3d.utils.camera_conversions import cameras_from_opencv_projection + +from .utils import fps_subsample +from einops import rearrange + +from .utils import MLP_CONV +from .SPD import SPD +from .SPD_crossattn import SPD_crossattn +from .SPD_pp import SPD_pp + +SPD_BLOCK = { + 'SPD': SPD, + 'SPD_crossattn': SPD_crossattn, + 'SPD_PP': SPD_pp, +} + + +def points_projection(points: Float[Tensor, "B Np 3"], + c2ws: Float[Tensor, "B 4 4"], + intrinsics: Float[Tensor, "B 3 3"], + local_features: Float[Tensor, "B C H W"], + raster_point_radius: float = 0.0075, # point size + raster_points_per_pixel: int = 1, # a single point per pixel, for now + bin_size: int = 0): + """ + points: (B, Np, 3) + """ + + B, C, H, W = local_features.shape + device = local_features.device + raster_settings = PointsRasterizationSettings( + image_size=(H, W), + radius=raster_point_radius, + points_per_pixel=raster_points_per_pixel, + bin_size=bin_size, + ) + Np = points.shape[1] + c2ws = c2ws.transpose(0, 1).flatten(0, 1) + intrinsics = intrinsics.transpose(0, 1).flatten(0, 1) + R = raster_settings.points_per_pixel + w2cs = torch.inverse(c2ws) + image_size = torch.as_tensor([H, W]).view( + 1, 2).expand(w2cs.shape[0], -1).to(device) + cameras = cameras_from_opencv_projection( + w2cs[:, :3, :3], w2cs[:, :3, 3], intrinsics, image_size) + rasterize = PointsRasterizer( + cameras=cameras, raster_settings=raster_settings) + fragments = rasterize(Pointclouds(points)) + fragments_idx: Tensor = fragments.idx.long() + visible_pixels = (fragments_idx > -1) # (B, H, W, R) + points_to_visible_pixels = fragments_idx[visible_pixels] + # Reshape local features to (B, H, W, R, C) + local_features = local_features.permute( + 0, 2, 3, 1).unsqueeze(-2).expand(-1, -1, -1, R, -1) # (B, H, W, R, C) + # Get local features corresponding to visible points + local_features_proj = torch.zeros(B * Np, C, device=device) + local_features_proj[points_to_visible_pixels] = local_features[visible_pixels] + local_features_proj = local_features_proj.reshape(B, Np, C) + return local_features_proj + + +class Decoder(nn.Module): + def __init__(self, input_channels=1152, dim_feat=512, num_p0=512, + radius=1, bounding=True, up_factors=None, + SPD_type='SPD', + token_type='image_token' + ): + super(Decoder, self).__init__() + # self.decoder_coarse = SeedGenerator(dim_feat=dim_feat, num_pc=num_p0) + if up_factors is None: + up_factors = [1] + else: + up_factors = up_factors + uppers = [] + self.num_p0 = num_p0 + self.mlp_feat_cond = MLP_CONV(in_channel=input_channels, + layer_dims=[dim_feat*2, dim_feat]) + + for i, factor in enumerate(up_factors): + uppers.append( + SPD_BLOCK[SPD_type](dim_feat=dim_feat, up_factor=factor, + i=i, bounding=bounding, radius=radius)) + self.uppers = nn.ModuleList(uppers) + self.token_type = token_type + + def calculate_pcl_token(self, pcl_token, up_factor): + up_token = F.interpolate(pcl_token, scale_factor=up_factor, mode='nearest') + return up_token + + def calculate_image_token(self, pcd, input_image_tokens, batch): + """ + Args: + """ + batch_size, n_input_views = batch["rgb_cond"].shape[:2] + h_cond, w_cond = batch["rgb_cond"].shape[2:4] + input_image_tokens = rearrange( + input_image_tokens, '(B Nv) C Nt -> B (Nv Nt) C', Nv=n_input_views) + local_features = input_image_tokens[:, 1:].reshape( + batch_size, h_cond // 14, w_cond // 14, -1).permute(0, 3, 1, 2) + local_features = F.interpolate(local_features, size=( + h_cond, w_cond), mode='bilinear', align_corners=False) + batch['c2w_cond'][..., :3, 1:3] *= -1 + local_features_proj = points_projection( + pcd, + batch['c2w_cond'], + batch['intrinsic_cond'], + local_features, + ) + local_features_proj = local_features_proj.permute(0, 2, 1).contiguous() + return local_features_proj + + def forward(self, x): + """ + Args: + points: Tensor, (b, num_p0, 3) + feat_cond: Tensor, (b, dim_feat) dinov2: 325x768 + # partial_coarse: Tensor, (b, n_coarse, 3) + """ + points = x['points'] + if self.token_type == 'pcl_token': + feat_cond = x['pcl_token'] + elif self.token_type == 'image_token': + feat_cond = x['input_image_tokens'] + feat_cond = self.mlp_feat_cond(feat_cond) + arr_pcd = [] + feat_prev = None + + pcd = torch.permute(points, (0, 2, 1)).contiguous() + pcl_up_scale = 1 + for upper in self.uppers: + if self.token_type == 'pcl_token': + up_cond = self.calculate_pcl_token( + feat_cond, pcl_up_scale) + pcl_up_scale *= upper.up_factor + elif self.token_type == 'image_token': + up_cond = self.calculate_image_token(points, feat_cond, x) + pcd, feat_prev = upper(pcd, up_cond, feat_prev) + points = torch.permute(pcd, (0, 2, 1)).contiguous() + arr_pcd.append(points) + return arr_pcd + + +class SnowflakeModelSPDPP(BaseModule): + """ + apply PC^2 / PCL token to decoder + """ + @dataclass + class Config(BaseModule.Config): + input_channels: int = 1152 + dim_feat: int = 128 + num_p0: int = 512 + radius: float = 1 + bounding: bool = True + use_fps: bool = True + up_factors: List[int] = field(default_factory=lambda: [2, 2]) + image_full_token_cond: bool = False + SPD_type: str = 'SPD_PP' + token_type: str = 'pcl_token' + cfg: Config + + def configure(self) -> None: + super().configure() + self.decoder = Decoder(input_channels=self.cfg.input_channels, + dim_feat=self.cfg.dim_feat, num_p0=self.cfg.num_p0, + radius=self.cfg.radius, up_factors=self.cfg.up_factors, bounding=self.cfg.bounding, + SPD_type=self.cfg.SPD_type, + token_type=self.cfg.token_type + ) + + def forward(self, x): + results = self.decoder(x) + return results \ No newline at end of file diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/__init__.py b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/__init__.py new file mode 100644 index 0000000..5fd361f --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/__init__.py @@ -0,0 +1,3 @@ +import pointnet2_ops.pointnet2_modules +import pointnet2_ops.pointnet2_utils +from pointnet2_ops._version import __version__ diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/ball_query.h b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/ball_query.h new file mode 100644 index 0000000..1bbc638 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/ball_query.h @@ -0,0 +1,5 @@ +#pragma once +#include + +at::Tensor ball_query(at::Tensor new_xyz, at::Tensor xyz, const float radius, + const int nsample); diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/cuda_utils.h b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/cuda_utils.h new file mode 100644 index 0000000..0fd5b6e --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/cuda_utils.h @@ -0,0 +1,41 @@ +#ifndef _CUDA_UTILS_H +#define _CUDA_UTILS_H + +#include +#include +#include + +#include +#include + +#include + +#define TOTAL_THREADS 512 + +inline int opt_n_threads(int work_size) { + const int pow_2 = std::log(static_cast(work_size)) / std::log(2.0); + + return max(min(1 << pow_2, TOTAL_THREADS), 1); +} + +inline dim3 opt_block_config(int x, int y) { + const int x_threads = opt_n_threads(x); + const int y_threads = + max(min(opt_n_threads(y), TOTAL_THREADS / x_threads), 1); + dim3 block_config(x_threads, y_threads, 1); + + return block_config; +} + +#define CUDA_CHECK_ERRORS() \ + do { \ + cudaError_t err = cudaGetLastError(); \ + if (cudaSuccess != err) { \ + fprintf(stderr, "CUDA kernel failed : %s\n%s at L:%d in %s\n", \ + cudaGetErrorString(err), __PRETTY_FUNCTION__, __LINE__, \ + __FILE__); \ + exit(-1); \ + } \ + } while (0) + +#endif diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/group_points.h b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/group_points.h new file mode 100644 index 0000000..ad20cda --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/group_points.h @@ -0,0 +1,5 @@ +#pragma once +#include + +at::Tensor group_points(at::Tensor points, at::Tensor idx); +at::Tensor group_points_grad(at::Tensor grad_out, at::Tensor idx, const int n); diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/interpolate.h b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/interpolate.h new file mode 100644 index 0000000..26b3464 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/interpolate.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +std::vector three_nn(at::Tensor unknowns, at::Tensor knows); +at::Tensor three_interpolate(at::Tensor points, at::Tensor idx, + at::Tensor weight); +at::Tensor three_interpolate_grad(at::Tensor grad_out, at::Tensor idx, + at::Tensor weight, const int m); diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/sampling.h b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/sampling.h new file mode 100644 index 0000000..d795271 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/sampling.h @@ -0,0 +1,6 @@ +#pragma once +#include + +at::Tensor gather_points(at::Tensor points, at::Tensor idx); +at::Tensor gather_points_grad(at::Tensor grad_out, at::Tensor idx, const int n); +at::Tensor furthest_point_sampling(at::Tensor points, const int nsamples); diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/utils.h b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/utils.h new file mode 100644 index 0000000..5f080ed --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/include/utils.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include + +#define CHECK_CUDA(x) \ + do { \ + AT_ASSERT(x.is_cuda(), #x " must be a CUDA tensor"); \ + } while (0) + +#define CHECK_CONTIGUOUS(x) \ + do { \ + AT_ASSERT(x.is_contiguous(), #x " must be a contiguous tensor"); \ + } while (0) + +#define CHECK_IS_INT(x) \ + do { \ + AT_ASSERT(x.scalar_type() == at::ScalarType::Int, \ + #x " must be an int tensor"); \ + } while (0) + +#define CHECK_IS_FLOAT(x) \ + do { \ + AT_ASSERT(x.scalar_type() == at::ScalarType::Float, \ + #x " must be a float tensor"); \ + } while (0) diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query.cpp b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query.cpp new file mode 100644 index 0000000..b1797c1 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query.cpp @@ -0,0 +1,32 @@ +#include "ball_query.h" +#include "utils.h" + +void query_ball_point_kernel_wrapper(int b, int n, int m, float radius, + int nsample, const float *new_xyz, + const float *xyz, int *idx); + +at::Tensor ball_query(at::Tensor new_xyz, at::Tensor xyz, const float radius, + const int nsample) { + CHECK_CONTIGUOUS(new_xyz); + CHECK_CONTIGUOUS(xyz); + CHECK_IS_FLOAT(new_xyz); + CHECK_IS_FLOAT(xyz); + + if (new_xyz.is_cuda()) { + CHECK_CUDA(xyz); + } + + at::Tensor idx = + torch::zeros({new_xyz.size(0), new_xyz.size(1), nsample}, + at::device(new_xyz.device()).dtype(at::ScalarType::Int)); + + if (new_xyz.is_cuda()) { + query_ball_point_kernel_wrapper(xyz.size(0), xyz.size(1), new_xyz.size(1), + radius, nsample, new_xyz.data_ptr(), + xyz.data_ptr(), idx.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return idx; +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query_gpu.cu b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query_gpu.cu new file mode 100644 index 0000000..559aef9 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/ball_query_gpu.cu @@ -0,0 +1,54 @@ +#include +#include +#include + +#include "cuda_utils.h" + +// input: new_xyz(b, m, 3) xyz(b, n, 3) +// output: idx(b, m, nsample) +__global__ void query_ball_point_kernel(int b, int n, int m, float radius, + int nsample, + const float *__restrict__ new_xyz, + const float *__restrict__ xyz, + int *__restrict__ idx) { + int batch_index = blockIdx.x; + xyz += batch_index * n * 3; + new_xyz += batch_index * m * 3; + idx += m * nsample * batch_index; + + int index = threadIdx.x; + int stride = blockDim.x; + + float radius2 = radius * radius; + for (int j = index; j < m; j += stride) { + float new_x = new_xyz[j * 3 + 0]; + float new_y = new_xyz[j * 3 + 1]; + float new_z = new_xyz[j * 3 + 2]; + for (int k = 0, cnt = 0; k < n && cnt < nsample; ++k) { + float x = xyz[k * 3 + 0]; + float y = xyz[k * 3 + 1]; + float z = xyz[k * 3 + 2]; + float d2 = (new_x - x) * (new_x - x) + (new_y - y) * (new_y - y) + + (new_z - z) * (new_z - z); + if (d2 < radius2) { + if (cnt == 0) { + for (int l = 0; l < nsample; ++l) { + idx[j * nsample + l] = k; + } + } + idx[j * nsample + cnt] = k; + ++cnt; + } + } + } +} + +void query_ball_point_kernel_wrapper(int b, int n, int m, float radius, + int nsample, const float *new_xyz, + const float *xyz, int *idx) { + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + query_ball_point_kernel<<>>( + b, n, m, radius, nsample, new_xyz, xyz, idx); + + CUDA_CHECK_ERRORS(); +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/bindings.cpp b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/bindings.cpp new file mode 100644 index 0000000..d1916ce --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/bindings.cpp @@ -0,0 +1,19 @@ +#include "ball_query.h" +#include "group_points.h" +#include "interpolate.h" +#include "sampling.h" + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def("gather_points", &gather_points); + m.def("gather_points_grad", &gather_points_grad); + m.def("furthest_point_sampling", &furthest_point_sampling); + + m.def("three_nn", &three_nn); + m.def("three_interpolate", &three_interpolate); + m.def("three_interpolate_grad", &three_interpolate_grad); + + m.def("ball_query", &ball_query); + + m.def("group_points", &group_points); + m.def("group_points_grad", &group_points_grad); +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points.cpp b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points.cpp new file mode 100644 index 0000000..285a4bd --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points.cpp @@ -0,0 +1,62 @@ +#include "group_points.h" +#include "utils.h" + +void group_points_kernel_wrapper(int b, int c, int n, int npoints, int nsample, + const float *points, const int *idx, + float *out); + +void group_points_grad_kernel_wrapper(int b, int c, int n, int npoints, + int nsample, const float *grad_out, + const int *idx, float *grad_points); + +at::Tensor group_points(at::Tensor points, at::Tensor idx) { + CHECK_CONTIGUOUS(points); + CHECK_CONTIGUOUS(idx); + CHECK_IS_FLOAT(points); + CHECK_IS_INT(idx); + + if (points.is_cuda()) { + CHECK_CUDA(idx); + } + + at::Tensor output = + torch::zeros({points.size(0), points.size(1), idx.size(1), idx.size(2)}, + at::device(points.device()).dtype(at::ScalarType::Float)); + + if (points.is_cuda()) { + group_points_kernel_wrapper(points.size(0), points.size(1), points.size(2), + idx.size(1), idx.size(2), + points.data_ptr(), idx.data_ptr(), + output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} + +at::Tensor group_points_grad(at::Tensor grad_out, at::Tensor idx, const int n) { + CHECK_CONTIGUOUS(grad_out); + CHECK_CONTIGUOUS(idx); + CHECK_IS_FLOAT(grad_out); + CHECK_IS_INT(idx); + + if (grad_out.is_cuda()) { + CHECK_CUDA(idx); + } + + at::Tensor output = + torch::zeros({grad_out.size(0), grad_out.size(1), n}, + at::device(grad_out.device()).dtype(at::ScalarType::Float)); + + if (grad_out.is_cuda()) { + group_points_grad_kernel_wrapper( + grad_out.size(0), grad_out.size(1), n, idx.size(1), idx.size(2), + grad_out.data_ptr(), idx.data_ptr(), + output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points_gpu.cu b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points_gpu.cu new file mode 100644 index 0000000..57c2b1b --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/group_points_gpu.cu @@ -0,0 +1,75 @@ +#include +#include + +#include "cuda_utils.h" + +// input: points(b, c, n) idx(b, npoints, nsample) +// output: out(b, c, npoints, nsample) +__global__ void group_points_kernel(int b, int c, int n, int npoints, + int nsample, + const float *__restrict__ points, + const int *__restrict__ idx, + float *__restrict__ out) { + int batch_index = blockIdx.x; + points += batch_index * n * c; + idx += batch_index * npoints * nsample; + out += batch_index * npoints * nsample * c; + + const int index = threadIdx.y * blockDim.x + threadIdx.x; + const int stride = blockDim.y * blockDim.x; + for (int i = index; i < c * npoints; i += stride) { + const int l = i / npoints; + const int j = i % npoints; + for (int k = 0; k < nsample; ++k) { + int ii = idx[j * nsample + k]; + out[(l * npoints + j) * nsample + k] = points[l * n + ii]; + } + } +} + +void group_points_kernel_wrapper(int b, int c, int n, int npoints, int nsample, + const float *points, const int *idx, + float *out) { + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + group_points_kernel<<>>( + b, c, n, npoints, nsample, points, idx, out); + + CUDA_CHECK_ERRORS(); +} + +// input: grad_out(b, c, npoints, nsample), idx(b, npoints, nsample) +// output: grad_points(b, c, n) +__global__ void group_points_grad_kernel(int b, int c, int n, int npoints, + int nsample, + const float *__restrict__ grad_out, + const int *__restrict__ idx, + float *__restrict__ grad_points) { + int batch_index = blockIdx.x; + grad_out += batch_index * npoints * nsample * c; + idx += batch_index * npoints * nsample; + grad_points += batch_index * n * c; + + const int index = threadIdx.y * blockDim.x + threadIdx.x; + const int stride = blockDim.y * blockDim.x; + for (int i = index; i < c * npoints; i += stride) { + const int l = i / npoints; + const int j = i % npoints; + for (int k = 0; k < nsample; ++k) { + int ii = idx[j * nsample + k]; + atomicAdd(grad_points + l * n + ii, + grad_out[(l * npoints + j) * nsample + k]); + } + } +} + +void group_points_grad_kernel_wrapper(int b, int c, int n, int npoints, + int nsample, const float *grad_out, + const int *idx, float *grad_points) { + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + group_points_grad_kernel<<>>( + b, c, n, npoints, nsample, grad_out, idx, grad_points); + + CUDA_CHECK_ERRORS(); +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate.cpp b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate.cpp new file mode 100644 index 0000000..cdee31c --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate.cpp @@ -0,0 +1,99 @@ +#include "interpolate.h" +#include "utils.h" + +void three_nn_kernel_wrapper(int b, int n, int m, const float *unknown, + const float *known, float *dist2, int *idx); +void three_interpolate_kernel_wrapper(int b, int c, int m, int n, + const float *points, const int *idx, + const float *weight, float *out); +void three_interpolate_grad_kernel_wrapper(int b, int c, int n, int m, + const float *grad_out, + const int *idx, const float *weight, + float *grad_points); + +std::vector three_nn(at::Tensor unknowns, at::Tensor knows) { + CHECK_CONTIGUOUS(unknowns); + CHECK_CONTIGUOUS(knows); + CHECK_IS_FLOAT(unknowns); + CHECK_IS_FLOAT(knows); + + if (unknowns.is_cuda()) { + CHECK_CUDA(knows); + } + + at::Tensor idx = + torch::zeros({unknowns.size(0), unknowns.size(1), 3}, + at::device(unknowns.device()).dtype(at::ScalarType::Int)); + at::Tensor dist2 = + torch::zeros({unknowns.size(0), unknowns.size(1), 3}, + at::device(unknowns.device()).dtype(at::ScalarType::Float)); + + if (unknowns.is_cuda()) { + three_nn_kernel_wrapper(unknowns.size(0), unknowns.size(1), knows.size(1), + unknowns.data_ptr(), knows.data_ptr(), + dist2.data_ptr(), idx.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return {dist2, idx}; +} + +at::Tensor three_interpolate(at::Tensor points, at::Tensor idx, + at::Tensor weight) { + CHECK_CONTIGUOUS(points); + CHECK_CONTIGUOUS(idx); + CHECK_CONTIGUOUS(weight); + CHECK_IS_FLOAT(points); + CHECK_IS_INT(idx); + CHECK_IS_FLOAT(weight); + + if (points.is_cuda()) { + CHECK_CUDA(idx); + CHECK_CUDA(weight); + } + + at::Tensor output = + torch::zeros({points.size(0), points.size(1), idx.size(1)}, + at::device(points.device()).dtype(at::ScalarType::Float)); + + if (points.is_cuda()) { + three_interpolate_kernel_wrapper( + points.size(0), points.size(1), points.size(2), idx.size(1), + points.data_ptr(), idx.data_ptr(), weight.data_ptr(), + output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} +at::Tensor three_interpolate_grad(at::Tensor grad_out, at::Tensor idx, + at::Tensor weight, const int m) { + CHECK_CONTIGUOUS(grad_out); + CHECK_CONTIGUOUS(idx); + CHECK_CONTIGUOUS(weight); + CHECK_IS_FLOAT(grad_out); + CHECK_IS_INT(idx); + CHECK_IS_FLOAT(weight); + + if (grad_out.is_cuda()) { + CHECK_CUDA(idx); + CHECK_CUDA(weight); + } + + at::Tensor output = + torch::zeros({grad_out.size(0), grad_out.size(1), m}, + at::device(grad_out.device()).dtype(at::ScalarType::Float)); + + if (grad_out.is_cuda()) { + three_interpolate_grad_kernel_wrapper( + grad_out.size(0), grad_out.size(1), grad_out.size(2), m, + grad_out.data_ptr(), idx.data_ptr(), + weight.data_ptr(), output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate_gpu.cu b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate_gpu.cu new file mode 100644 index 0000000..81c5548 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/interpolate_gpu.cu @@ -0,0 +1,154 @@ +#include +#include +#include + +#include "cuda_utils.h" + +// input: unknown(b, n, 3) known(b, m, 3) +// output: dist2(b, n, 3), idx(b, n, 3) +__global__ void three_nn_kernel(int b, int n, int m, + const float *__restrict__ unknown, + const float *__restrict__ known, + float *__restrict__ dist2, + int *__restrict__ idx) { + int batch_index = blockIdx.x; + unknown += batch_index * n * 3; + known += batch_index * m * 3; + dist2 += batch_index * n * 3; + idx += batch_index * n * 3; + + int index = threadIdx.x; + int stride = blockDim.x; + for (int j = index; j < n; j += stride) { + float ux = unknown[j * 3 + 0]; + float uy = unknown[j * 3 + 1]; + float uz = unknown[j * 3 + 2]; + + double best1 = 1e40, best2 = 1e40, best3 = 1e40; + int besti1 = 0, besti2 = 0, besti3 = 0; + for (int k = 0; k < m; ++k) { + float x = known[k * 3 + 0]; + float y = known[k * 3 + 1]; + float z = known[k * 3 + 2]; + float d = (ux - x) * (ux - x) + (uy - y) * (uy - y) + (uz - z) * (uz - z); + if (d < best1) { + best3 = best2; + besti3 = besti2; + best2 = best1; + besti2 = besti1; + best1 = d; + besti1 = k; + } else if (d < best2) { + best3 = best2; + besti3 = besti2; + best2 = d; + besti2 = k; + } else if (d < best3) { + best3 = d; + besti3 = k; + } + } + dist2[j * 3 + 0] = best1; + dist2[j * 3 + 1] = best2; + dist2[j * 3 + 2] = best3; + + idx[j * 3 + 0] = besti1; + idx[j * 3 + 1] = besti2; + idx[j * 3 + 2] = besti3; + } +} + +void three_nn_kernel_wrapper(int b, int n, int m, const float *unknown, + const float *known, float *dist2, int *idx) { + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + three_nn_kernel<<>>(b, n, m, unknown, known, + dist2, idx); + + CUDA_CHECK_ERRORS(); +} + +// input: points(b, c, m), idx(b, n, 3), weight(b, n, 3) +// output: out(b, c, n) +__global__ void three_interpolate_kernel(int b, int c, int m, int n, + const float *__restrict__ points, + const int *__restrict__ idx, + const float *__restrict__ weight, + float *__restrict__ out) { + int batch_index = blockIdx.x; + points += batch_index * m * c; + + idx += batch_index * n * 3; + weight += batch_index * n * 3; + + out += batch_index * n * c; + + const int index = threadIdx.y * blockDim.x + threadIdx.x; + const int stride = blockDim.y * blockDim.x; + for (int i = index; i < c * n; i += stride) { + const int l = i / n; + const int j = i % n; + float w1 = weight[j * 3 + 0]; + float w2 = weight[j * 3 + 1]; + float w3 = weight[j * 3 + 2]; + + int i1 = idx[j * 3 + 0]; + int i2 = idx[j * 3 + 1]; + int i3 = idx[j * 3 + 2]; + + out[i] = points[l * m + i1] * w1 + points[l * m + i2] * w2 + + points[l * m + i3] * w3; + } +} + +void three_interpolate_kernel_wrapper(int b, int c, int m, int n, + const float *points, const int *idx, + const float *weight, float *out) { + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + three_interpolate_kernel<<>>( + b, c, m, n, points, idx, weight, out); + + CUDA_CHECK_ERRORS(); +} + +// input: grad_out(b, c, n), idx(b, n, 3), weight(b, n, 3) +// output: grad_points(b, c, m) + +__global__ void three_interpolate_grad_kernel( + int b, int c, int n, int m, const float *__restrict__ grad_out, + const int *__restrict__ idx, const float *__restrict__ weight, + float *__restrict__ grad_points) { + int batch_index = blockIdx.x; + grad_out += batch_index * n * c; + idx += batch_index * n * 3; + weight += batch_index * n * 3; + grad_points += batch_index * m * c; + + const int index = threadIdx.y * blockDim.x + threadIdx.x; + const int stride = blockDim.y * blockDim.x; + for (int i = index; i < c * n; i += stride) { + const int l = i / n; + const int j = i % n; + float w1 = weight[j * 3 + 0]; + float w2 = weight[j * 3 + 1]; + float w3 = weight[j * 3 + 2]; + + int i1 = idx[j * 3 + 0]; + int i2 = idx[j * 3 + 1]; + int i3 = idx[j * 3 + 2]; + + atomicAdd(grad_points + l * m + i1, grad_out[i] * w1); + atomicAdd(grad_points + l * m + i2, grad_out[i] * w2); + atomicAdd(grad_points + l * m + i3, grad_out[i] * w3); + } +} + +void three_interpolate_grad_kernel_wrapper(int b, int c, int n, int m, + const float *grad_out, + const int *idx, const float *weight, + float *grad_points) { + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + three_interpolate_grad_kernel<<>>( + b, c, n, m, grad_out, idx, weight, grad_points); + + CUDA_CHECK_ERRORS(); +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling.cpp b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling.cpp new file mode 100644 index 0000000..ddbdc11 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling.cpp @@ -0,0 +1,87 @@ +#include "sampling.h" +#include "utils.h" + +void gather_points_kernel_wrapper(int b, int c, int n, int npoints, + const float *points, const int *idx, + float *out); +void gather_points_grad_kernel_wrapper(int b, int c, int n, int npoints, + const float *grad_out, const int *idx, + float *grad_points); + +void furthest_point_sampling_kernel_wrapper(int b, int n, int m, + const float *dataset, float *temp, + int *idxs); + +at::Tensor gather_points(at::Tensor points, at::Tensor idx) { + CHECK_CONTIGUOUS(points); + CHECK_CONTIGUOUS(idx); + CHECK_IS_FLOAT(points); + CHECK_IS_INT(idx); + + if (points.is_cuda()) { + CHECK_CUDA(idx); + } + + at::Tensor output = + torch::zeros({points.size(0), points.size(1), idx.size(1)}, + at::device(points.device()).dtype(at::ScalarType::Float)); + + if (points.is_cuda()) { + gather_points_kernel_wrapper(points.size(0), points.size(1), points.size(2), + idx.size(1), points.data_ptr(), + idx.data_ptr(), output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} + +at::Tensor gather_points_grad(at::Tensor grad_out, at::Tensor idx, + const int n) { + CHECK_CONTIGUOUS(grad_out); + CHECK_CONTIGUOUS(idx); + CHECK_IS_FLOAT(grad_out); + CHECK_IS_INT(idx); + + if (grad_out.is_cuda()) { + CHECK_CUDA(idx); + } + + at::Tensor output = + torch::zeros({grad_out.size(0), grad_out.size(1), n}, + at::device(grad_out.device()).dtype(at::ScalarType::Float)); + + if (grad_out.is_cuda()) { + gather_points_grad_kernel_wrapper(grad_out.size(0), grad_out.size(1), n, + idx.size(1), grad_out.data_ptr(), + idx.data_ptr(), + output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} +at::Tensor furthest_point_sampling(at::Tensor points, const int nsamples) { + CHECK_CONTIGUOUS(points); + CHECK_IS_FLOAT(points); + + at::Tensor output = + torch::zeros({points.size(0), nsamples}, + at::device(points.device()).dtype(at::ScalarType::Int)); + + at::Tensor tmp = + torch::full({points.size(0), points.size(1)}, 1e10, + at::device(points.device()).dtype(at::ScalarType::Float)); + + if (points.is_cuda()) { + furthest_point_sampling_kernel_wrapper( + points.size(0), points.size(1), nsamples, points.data_ptr(), + tmp.data_ptr(), output.data_ptr()); + } else { + AT_ASSERT(false, "CPU not supported"); + } + + return output; +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling_gpu.cu b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling_gpu.cu new file mode 100644 index 0000000..fc573f0 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_ext-src/src/sampling_gpu.cu @@ -0,0 +1,229 @@ +#include +#include + +#include "cuda_utils.h" + +// input: points(b, c, n) idx(b, m) +// output: out(b, c, m) +__global__ void gather_points_kernel(int b, int c, int n, int m, + const float *__restrict__ points, + const int *__restrict__ idx, + float *__restrict__ out) { + for (int i = blockIdx.x; i < b; i += gridDim.x) { + for (int l = blockIdx.y; l < c; l += gridDim.y) { + for (int j = threadIdx.x; j < m; j += blockDim.x) { + int a = idx[i * m + j]; + out[(i * c + l) * m + j] = points[(i * c + l) * n + a]; + } + } + } +} + +void gather_points_kernel_wrapper(int b, int c, int n, int npoints, + const float *points, const int *idx, + float *out) { + gather_points_kernel<<>>(b, c, n, npoints, + points, idx, out); + + CUDA_CHECK_ERRORS(); +} + +// input: grad_out(b, c, m) idx(b, m) +// output: grad_points(b, c, n) +__global__ void gather_points_grad_kernel(int b, int c, int n, int m, + const float *__restrict__ grad_out, + const int *__restrict__ idx, + float *__restrict__ grad_points) { + for (int i = blockIdx.x; i < b; i += gridDim.x) { + for (int l = blockIdx.y; l < c; l += gridDim.y) { + for (int j = threadIdx.x; j < m; j += blockDim.x) { + int a = idx[i * m + j]; + atomicAdd(grad_points + (i * c + l) * n + a, + grad_out[(i * c + l) * m + j]); + } + } + } +} + +void gather_points_grad_kernel_wrapper(int b, int c, int n, int npoints, + const float *grad_out, const int *idx, + float *grad_points) { + gather_points_grad_kernel<<>>( + b, c, n, npoints, grad_out, idx, grad_points); + + CUDA_CHECK_ERRORS(); +} + +__device__ void __update(float *__restrict__ dists, int *__restrict__ dists_i, + int idx1, int idx2) { + const float v1 = dists[idx1], v2 = dists[idx2]; + const int i1 = dists_i[idx1], i2 = dists_i[idx2]; + dists[idx1] = max(v1, v2); + dists_i[idx1] = v2 > v1 ? i2 : i1; +} + +// Input dataset: (b, n, 3), tmp: (b, n) +// Ouput idxs (b, m) +template +__global__ void furthest_point_sampling_kernel( + int b, int n, int m, const float *__restrict__ dataset, + float *__restrict__ temp, int *__restrict__ idxs) { + if (m <= 0) return; + __shared__ float dists[block_size]; + __shared__ int dists_i[block_size]; + + int batch_index = blockIdx.x; + dataset += batch_index * n * 3; + temp += batch_index * n; + idxs += batch_index * m; + + int tid = threadIdx.x; + const int stride = block_size; + + int old = 0; + if (threadIdx.x == 0) idxs[0] = old; + + __syncthreads(); + for (int j = 1; j < m; j++) { + int besti = 0; + float best = -1; + float x1 = dataset[old * 3 + 0]; + float y1 = dataset[old * 3 + 1]; + float z1 = dataset[old * 3 + 2]; + for (int k = tid; k < n; k += stride) { + float x2, y2, z2; + x2 = dataset[k * 3 + 0]; + y2 = dataset[k * 3 + 1]; + z2 = dataset[k * 3 + 2]; + float mag = (x2 * x2) + (y2 * y2) + (z2 * z2); + if (mag <= 1e-3) continue; + + float d = + (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1); + + float d2 = min(d, temp[k]); + temp[k] = d2; + besti = d2 > best ? k : besti; + best = d2 > best ? d2 : best; + } + dists[tid] = best; + dists_i[tid] = besti; + __syncthreads(); + + if (block_size >= 512) { + if (tid < 256) { + __update(dists, dists_i, tid, tid + 256); + } + __syncthreads(); + } + if (block_size >= 256) { + if (tid < 128) { + __update(dists, dists_i, tid, tid + 128); + } + __syncthreads(); + } + if (block_size >= 128) { + if (tid < 64) { + __update(dists, dists_i, tid, tid + 64); + } + __syncthreads(); + } + if (block_size >= 64) { + if (tid < 32) { + __update(dists, dists_i, tid, tid + 32); + } + __syncthreads(); + } + if (block_size >= 32) { + if (tid < 16) { + __update(dists, dists_i, tid, tid + 16); + } + __syncthreads(); + } + if (block_size >= 16) { + if (tid < 8) { + __update(dists, dists_i, tid, tid + 8); + } + __syncthreads(); + } + if (block_size >= 8) { + if (tid < 4) { + __update(dists, dists_i, tid, tid + 4); + } + __syncthreads(); + } + if (block_size >= 4) { + if (tid < 2) { + __update(dists, dists_i, tid, tid + 2); + } + __syncthreads(); + } + if (block_size >= 2) { + if (tid < 1) { + __update(dists, dists_i, tid, tid + 1); + } + __syncthreads(); + } + + old = dists_i[0]; + if (tid == 0) idxs[j] = old; + } +} + +void furthest_point_sampling_kernel_wrapper(int b, int n, int m, + const float *dataset, float *temp, + int *idxs) { + unsigned int n_threads = opt_n_threads(n); + + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + switch (n_threads) { + case 512: + furthest_point_sampling_kernel<512> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 256: + furthest_point_sampling_kernel<256> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 128: + furthest_point_sampling_kernel<128> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 64: + furthest_point_sampling_kernel<64> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 32: + furthest_point_sampling_kernel<32> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 16: + furthest_point_sampling_kernel<16> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 8: + furthest_point_sampling_kernel<8> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 4: + furthest_point_sampling_kernel<4> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 2: + furthest_point_sampling_kernel<2> + <<>>(b, n, m, dataset, temp, idxs); + break; + case 1: + furthest_point_sampling_kernel<1> + <<>>(b, n, m, dataset, temp, idxs); + break; + default: + furthest_point_sampling_kernel<512> + <<>>(b, n, m, dataset, temp, idxs); + } + + CUDA_CHECK_ERRORS(); +} diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_version.py b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_version.py new file mode 100644 index 0000000..528787c --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/_version.py @@ -0,0 +1 @@ +__version__ = "3.0.0" diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_modules.py b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_modules.py new file mode 100644 index 0000000..a0ad4f6 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_modules.py @@ -0,0 +1,209 @@ +from typing import List, Optional, Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F +from pointnet2_ops import pointnet2_utils + + +def build_shared_mlp(mlp_spec: List[int], bn: bool = True): + layers = [] + for i in range(1, len(mlp_spec)): + layers.append( + nn.Conv2d(mlp_spec[i - 1], mlp_spec[i], kernel_size=1, bias=not bn) + ) + if bn: + layers.append(nn.BatchNorm2d(mlp_spec[i])) + layers.append(nn.ReLU(True)) + + return nn.Sequential(*layers) + + +class _PointnetSAModuleBase(nn.Module): + def __init__(self): + super(_PointnetSAModuleBase, self).__init__() + self.npoint = None + self.groupers = None + self.mlps = None + + def forward( + self, xyz: torch.Tensor, features: Optional[torch.Tensor] + ) -> Tuple[torch.Tensor, torch.Tensor]: + r""" + Parameters + ---------- + xyz : torch.Tensor + (B, N, 3) tensor of the xyz coordinates of the features + features : torch.Tensor + (B, C, N) tensor of the descriptors of the the features + + Returns + ------- + new_xyz : torch.Tensor + (B, npoint, 3) tensor of the new features' xyz + new_features : torch.Tensor + (B, \sum_k(mlps[k][-1]), npoint) tensor of the new_features descriptors + """ + + new_features_list = [] + + xyz_flipped = xyz.transpose(1, 2).contiguous() + new_xyz = ( + pointnet2_utils.gather_operation( + xyz_flipped, pointnet2_utils.furthest_point_sample(xyz, self.npoint) + ) + .transpose(1, 2) + .contiguous() + if self.npoint is not None + else None + ) + + for i in range(len(self.groupers)): + new_features = self.groupers[i]( + xyz, new_xyz, features + ) # (B, C, npoint, nsample) + + new_features = self.mlps[i](new_features) # (B, mlp[-1], npoint, nsample) + new_features = F.max_pool2d( + new_features, kernel_size=[1, new_features.size(3)] + ) # (B, mlp[-1], npoint, 1) + new_features = new_features.squeeze(-1) # (B, mlp[-1], npoint) + + new_features_list.append(new_features) + + return new_xyz, torch.cat(new_features_list, dim=1) + + +class PointnetSAModuleMSG(_PointnetSAModuleBase): + r"""Pointnet set abstrction layer with multiscale grouping + + Parameters + ---------- + npoint : int + Number of features + radii : list of float32 + list of radii to group with + nsamples : list of int32 + Number of samples in each ball query + mlps : list of list of int32 + Spec of the pointnet before the global max_pool for each scale + bn : bool + Use batchnorm + """ + + def __init__(self, npoint, radii, nsamples, mlps, bn=True, use_xyz=True): + # type: (PointnetSAModuleMSG, int, List[float], List[int], List[List[int]], bool, bool) -> None + super(PointnetSAModuleMSG, self).__init__() + + assert len(radii) == len(nsamples) == len(mlps) + + self.npoint = npoint + self.groupers = nn.ModuleList() + self.mlps = nn.ModuleList() + for i in range(len(radii)): + radius = radii[i] + nsample = nsamples[i] + self.groupers.append( + pointnet2_utils.QueryAndGroup(radius, nsample, use_xyz=use_xyz) + if npoint is not None + else pointnet2_utils.GroupAll(use_xyz) + ) + mlp_spec = mlps[i] + if use_xyz: + mlp_spec[0] += 3 + + self.mlps.append(build_shared_mlp(mlp_spec, bn)) + + +class PointnetSAModule(PointnetSAModuleMSG): + r"""Pointnet set abstrction layer + + Parameters + ---------- + npoint : int + Number of features + radius : float + Radius of ball + nsample : int + Number of samples in the ball query + mlp : list + Spec of the pointnet before the global max_pool + bn : bool + Use batchnorm + """ + + def __init__( + self, mlp, npoint=None, radius=None, nsample=None, bn=True, use_xyz=True + ): + # type: (PointnetSAModule, List[int], int, float, int, bool, bool) -> None + super(PointnetSAModule, self).__init__( + mlps=[mlp], + npoint=npoint, + radii=[radius], + nsamples=[nsample], + bn=bn, + use_xyz=use_xyz, + ) + + +class PointnetFPModule(nn.Module): + r"""Propigates the features of one set to another + + Parameters + ---------- + mlp : list + Pointnet module parameters + bn : bool + Use batchnorm + """ + + def __init__(self, mlp, bn=True): + # type: (PointnetFPModule, List[int], bool) -> None + super(PointnetFPModule, self).__init__() + self.mlp = build_shared_mlp(mlp, bn=bn) + + def forward(self, unknown, known, unknow_feats, known_feats): + # type: (PointnetFPModule, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor) -> torch.Tensor + r""" + Parameters + ---------- + unknown : torch.Tensor + (B, n, 3) tensor of the xyz positions of the unknown features + known : torch.Tensor + (B, m, 3) tensor of the xyz positions of the known features + unknow_feats : torch.Tensor + (B, C1, n) tensor of the features to be propigated to + known_feats : torch.Tensor + (B, C2, m) tensor of features to be propigated + + Returns + ------- + new_features : torch.Tensor + (B, mlp[-1], n) tensor of the features of the unknown features + """ + + if known is not None: + dist, idx = pointnet2_utils.three_nn(unknown, known) + dist_recip = 1.0 / (dist + 1e-8) + norm = torch.sum(dist_recip, dim=2, keepdim=True) + weight = dist_recip / norm + + interpolated_feats = pointnet2_utils.three_interpolate( + known_feats, idx, weight + ) + else: + interpolated_feats = known_feats.expand( + *(known_feats.size()[0:2] + [unknown.size(1)]) + ) + + if unknow_feats is not None: + new_features = torch.cat( + [interpolated_feats, unknow_feats], dim=1 + ) # (B, C2 + C1, n) + else: + new_features = interpolated_feats + + new_features = new_features.unsqueeze(-1) + new_features = self.mlp(new_features) + + return new_features.squeeze(-1) diff --git a/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_utils.py b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_utils.py new file mode 100644 index 0000000..150fccc --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/pointnet2_ops/pointnet2_utils.py @@ -0,0 +1,379 @@ +import torch +import torch.nn as nn +import warnings +from torch.autograd import Function +from typing import * + +try: + import pointnet2_ops._ext as _ext +except ImportError: + from torch.utils.cpp_extension import load + import glob + import os.path as osp + import os + + warnings.warn("Unable to load pointnet2_ops cpp extension. JIT Compiling.") + + _ext_src_root = osp.join(osp.dirname(__file__), "_ext-src") + _ext_sources = glob.glob(osp.join(_ext_src_root, "src", "*.cpp")) + glob.glob( + osp.join(_ext_src_root, "src", "*.cu") + ) + _ext_headers = glob.glob(osp.join(_ext_src_root, "include", "*")) + + os.environ["TORCH_CUDA_ARCH_LIST"] = "3.7+PTX;5.0;6.0;6.1;6.2;7.0;7.5" + _ext = load( + "_ext", + sources=_ext_sources, + extra_include_paths=[osp.join(_ext_src_root, "include")], + extra_cflags=["-O3"], + extra_cuda_cflags=["-O3", "-Xfatbin", "-compress-all"], + with_cuda=True, + ) + + +class FurthestPointSampling(Function): + @staticmethod + def forward(ctx, xyz, npoint): + # type: (Any, torch.Tensor, int) -> torch.Tensor + r""" + Uses iterative furthest point sampling to select a set of npoint features that have the largest + minimum distance + + Parameters + ---------- + xyz : torch.Tensor + (B, N, 3) tensor where N > npoint + npoint : int32 + number of features in the sampled set + + Returns + ------- + torch.Tensor + (B, npoint) tensor containing the set + """ + out = _ext.furthest_point_sampling(xyz, npoint) + + ctx.mark_non_differentiable(out) + + return out + + @staticmethod + def backward(ctx, grad_out): + return () + + +furthest_point_sample = FurthestPointSampling.apply + + +class GatherOperation(Function): + @staticmethod + def forward(ctx, features, idx): + # type: (Any, torch.Tensor, torch.Tensor) -> torch.Tensor + r""" + + Parameters + ---------- + features : torch.Tensor + (B, C, N) tensor + + idx : torch.Tensor + (B, npoint) tensor of the features to gather + + Returns + ------- + torch.Tensor + (B, C, npoint) tensor + """ + + ctx.save_for_backward(idx, features) + + return _ext.gather_points(features, idx) + + @staticmethod + def backward(ctx, grad_out): + idx, features = ctx.saved_tensors + N = features.size(2) + + grad_features = _ext.gather_points_grad(grad_out.contiguous(), idx, N) + return grad_features, None + + +gather_operation = GatherOperation.apply + + +class ThreeNN(Function): + @staticmethod + def forward(ctx, unknown, known): + # type: (Any, torch.Tensor, torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor] + r""" + Find the three nearest neighbors of unknown in known + Parameters + ---------- + unknown : torch.Tensor + (B, n, 3) tensor of known features + known : torch.Tensor + (B, m, 3) tensor of unknown features + + Returns + ------- + dist : torch.Tensor + (B, n, 3) l2 distance to the three nearest neighbors + idx : torch.Tensor + (B, n, 3) index of 3 nearest neighbors + """ + dist2, idx = _ext.three_nn(unknown, known) + dist = torch.sqrt(dist2) + + ctx.mark_non_differentiable(dist, idx) + + return dist, idx + + @staticmethod + def backward(ctx, grad_dist, grad_idx): + return () + + +three_nn = ThreeNN.apply + + +class ThreeInterpolate(Function): + @staticmethod + def forward(ctx, features, idx, weight): + # type(Any, torch.Tensor, torch.Tensor, torch.Tensor) -> Torch.Tensor + r""" + Performs weight linear interpolation on 3 features + Parameters + ---------- + features : torch.Tensor + (B, c, m) Features descriptors to be interpolated from + idx : torch.Tensor + (B, n, 3) three nearest neighbors of the target features in features + weight : torch.Tensor + (B, n, 3) weights + + Returns + ------- + torch.Tensor + (B, c, n) tensor of the interpolated features + """ + ctx.save_for_backward(idx, weight, features) + + return _ext.three_interpolate(features, idx, weight) + + @staticmethod + def backward(ctx, grad_out): + # type: (Any, torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor] + r""" + Parameters + ---------- + grad_out : torch.Tensor + (B, c, n) tensor with gradients of ouputs + + Returns + ------- + grad_features : torch.Tensor + (B, c, m) tensor with gradients of features + + None + + None + """ + idx, weight, features = ctx.saved_tensors + m = features.size(2) + + grad_features = _ext.three_interpolate_grad( + grad_out.contiguous(), idx, weight, m + ) + + return grad_features, torch.zeros_like(idx), torch.zeros_like(weight) + + +three_interpolate = ThreeInterpolate.apply + + +class GroupingOperation(Function): + @staticmethod + def forward(ctx, features, idx): + # type: (Any, torch.Tensor, torch.Tensor) -> torch.Tensor + r""" + + Parameters + ---------- + features : torch.Tensor + (B, C, N) tensor of features to group + idx : torch.Tensor + (B, npoint, nsample) tensor containing the indicies of features to group with + + Returns + ------- + torch.Tensor + (B, C, npoint, nsample) tensor + """ + ctx.save_for_backward(idx, features) + + return _ext.group_points(features, idx) + + @staticmethod + def backward(ctx, grad_out): + # type: (Any, torch.tensor) -> Tuple[torch.Tensor, torch.Tensor] + r""" + + Parameters + ---------- + grad_out : torch.Tensor + (B, C, npoint, nsample) tensor of the gradients of the output from forward + + Returns + ------- + torch.Tensor + (B, C, N) gradient of the features + None + """ + idx, features = ctx.saved_tensors + N = features.size(2) + + grad_features = _ext.group_points_grad(grad_out.contiguous(), idx, N) + + return grad_features, torch.zeros_like(idx) + + +grouping_operation = GroupingOperation.apply + + +class BallQuery(Function): + @staticmethod + def forward(ctx, radius, nsample, xyz, new_xyz): + # type: (Any, float, int, torch.Tensor, torch.Tensor) -> torch.Tensor + r""" + + Parameters + ---------- + radius : float + radius of the balls + nsample : int + maximum number of features in the balls + xyz : torch.Tensor + (B, N, 3) xyz coordinates of the features + new_xyz : torch.Tensor + (B, npoint, 3) centers of the ball query + + Returns + ------- + torch.Tensor + (B, npoint, nsample) tensor with the indicies of the features that form the query balls + """ + output = _ext.ball_query(new_xyz, xyz, radius, nsample) + + ctx.mark_non_differentiable(output) + + return output + + @staticmethod + def backward(ctx, grad_out): + return () + + +ball_query = BallQuery.apply + + +class QueryAndGroup(nn.Module): + r""" + Groups with a ball query of radius + + Parameters + --------- + radius : float32 + Radius of ball + nsample : int32 + Maximum number of features to gather in the ball + """ + + def __init__(self, radius, nsample, use_xyz=True): + # type: (QueryAndGroup, float, int, bool) -> None + super(QueryAndGroup, self).__init__() + self.radius, self.nsample, self.use_xyz = radius, nsample, use_xyz + + def forward(self, xyz, new_xyz, features=None): + # type: (QueryAndGroup, torch.Tensor. torch.Tensor, torch.Tensor) -> Tuple[Torch.Tensor] + r""" + Parameters + ---------- + xyz : torch.Tensor + xyz coordinates of the features (B, N, 3) + new_xyz : torch.Tensor + centriods (B, npoint, 3) + features : torch.Tensor + Descriptors of the features (B, C, N) + + Returns + ------- + new_features : torch.Tensor + (B, 3 + C, npoint, nsample) tensor + """ + + idx = ball_query(self.radius, self.nsample, xyz, new_xyz) + xyz_trans = xyz.transpose(1, 2).contiguous() + grouped_xyz = grouping_operation(xyz_trans, idx) # (B, 3, npoint, nsample) + grouped_xyz -= new_xyz.transpose(1, 2).unsqueeze(-1) + + if features is not None: + grouped_features = grouping_operation(features, idx) + if self.use_xyz: + new_features = torch.cat( + [grouped_xyz, grouped_features], dim=1 + ) # (B, C + 3, npoint, nsample) + else: + new_features = grouped_features + else: + assert ( + self.use_xyz + ), "Cannot have not features and not use xyz as a feature!" + new_features = grouped_xyz + + return new_features + + +class GroupAll(nn.Module): + r""" + Groups all features + + Parameters + --------- + """ + + def __init__(self, use_xyz=True): + # type: (GroupAll, bool) -> None + super(GroupAll, self).__init__() + self.use_xyz = use_xyz + + def forward(self, xyz, new_xyz, features=None): + # type: (GroupAll, torch.Tensor, torch.Tensor, torch.Tensor) -> Tuple[torch.Tensor] + r""" + Parameters + ---------- + xyz : torch.Tensor + xyz coordinates of the features (B, N, 3) + new_xyz : torch.Tensor + Ignored + features : torch.Tensor + Descriptors of the features (B, C, N) + + Returns + ------- + new_features : torch.Tensor + (B, C + 3, 1, N) tensor + """ + + grouped_xyz = xyz.transpose(1, 2).unsqueeze(2) + if features is not None: + grouped_features = features.unsqueeze(2) + if self.use_xyz: + new_features = torch.cat( + [grouped_xyz, grouped_features], dim=1 + ) # (B, 3 + C, 1, N) + else: + new_features = grouped_features + else: + new_features = grouped_xyz + + return new_features diff --git a/tgs/models/snowflake/pointnet2_ops_lib/setup.py b/tgs/models/snowflake/pointnet2_ops_lib/setup.py new file mode 100644 index 0000000..5ebde53 --- /dev/null +++ b/tgs/models/snowflake/pointnet2_ops_lib/setup.py @@ -0,0 +1,41 @@ +import glob +import os +import os.path as osp + +from setuptools import find_packages, setup +import torch +from torch.utils.cpp_extension import BuildExtension, CUDAExtension + +this_dir = osp.dirname(osp.abspath(__file__)) +_ext_src_root = osp.join("pointnet2_ops", "_ext-src") +_ext_sources = glob.glob(osp.join(_ext_src_root, "src", "*.cpp")) + glob.glob( + osp.join(_ext_src_root, "src", "*.cu") +) +_ext_headers = glob.glob(osp.join(_ext_src_root, "include", "*")) + +requirements = ["torch>=1.4"] + +exec(open(osp.join("pointnet2_ops", "_version.py")).read()) + +os.environ["TORCH_CUDA_ARCH_LIST"] = ".".join(map(str, torch.cuda.get_device_capability())) +# os.environ["TORCH_CUDA_ARCH_LIST"] = "5.0;6.0;6.1;6.2;7.0;7.5;8.0;8.6" +setup( + name="pointnet2_ops", + version=__version__, + author="Erik Wijmans", + packages=find_packages(), + install_requires=requirements, + ext_modules=[ + CUDAExtension( + name="pointnet2_ops._ext", + sources=_ext_sources, + extra_compile_args={ + "cxx": ["-O3"], + "nvcc": ["-O3", "-Xfatbin", "-compress-all"], + }, + include_dirs=[osp.join(this_dir, _ext_src_root, "include")], + ) + ], + cmdclass={"build_ext": BuildExtension}, + include_package_data=True, +) diff --git a/tgs/models/snowflake/skip_transformer.py b/tgs/models/snowflake/skip_transformer.py new file mode 100644 index 0000000..36462f7 --- /dev/null +++ b/tgs/models/snowflake/skip_transformer.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# @Author: Peng Xiang + +import torch +from torch import nn, einsum +from .utils import MLP_Res, grouping_operation, query_knn + + +class SkipTransformer(nn.Module): + def __init__(self, in_channel, dim=256, n_knn=16, pos_hidden_dim=64, attn_hidden_multiplier=4): + super(SkipTransformer, self).__init__() + self.mlp_v = MLP_Res(in_dim=in_channel*2, hidden_dim=in_channel, out_dim=in_channel) + self.n_knn = n_knn + self.conv_key = nn.Conv1d(in_channel, dim, 1) + self.conv_query = nn.Conv1d(in_channel, dim, 1) + self.conv_value = nn.Conv1d(in_channel, dim, 1) + + self.pos_mlp = nn.Sequential( + nn.Conv2d(3, pos_hidden_dim, 1), + nn.BatchNorm2d(pos_hidden_dim), + nn.ReLU(), + nn.Conv2d(pos_hidden_dim, dim, 1) + ) + + self.attn_mlp = nn.Sequential( + nn.Conv2d(dim, dim * attn_hidden_multiplier, 1), + nn.BatchNorm2d(dim * attn_hidden_multiplier), + nn.ReLU(), + nn.Conv2d(dim * attn_hidden_multiplier, dim, 1) + ) + + self.conv_end = nn.Conv1d(dim, in_channel, 1) + + def forward(self, pos, key, query, include_self=True): + """ + Args: + pos: (B, 3, N) + key: (B, in_channel, N) + query: (B, in_channel, N) + include_self: boolean + + Returns: + Tensor: (B, in_channel, N), shape context feature + """ + value = self.mlp_v(torch.cat([key, query], 1)) + identity = value + key = self.conv_key(key) + query = self.conv_query(query) + value = self.conv_value(value) + b, dim, n = value.shape + + pos_flipped = pos.permute(0, 2, 1).contiguous() + idx_knn = query_knn(self.n_knn, pos_flipped, pos_flipped, include_self=include_self) + + key = grouping_operation(key, idx_knn) # b, dim, n, n_knn + qk_rel = query.reshape((b, -1, n, 1)) - key + + pos_rel = pos.reshape((b, -1, n, 1)) - grouping_operation(pos, idx_knn) # b, 3, n, n_knn + pos_embedding = self.pos_mlp(pos_rel) + + attention = self.attn_mlp(qk_rel + pos_embedding) # b, dim, n, n_knn + attention = torch.softmax(attention, -1) + + value = value.reshape((b, -1, n, 1)) + pos_embedding # + + agg = einsum('b c i j, b c i j -> b c i', attention, value) # b, dim, n + y = self.conv_end(agg) + + return y + identity diff --git a/tgs/models/snowflake/utils.py b/tgs/models/snowflake/utils.py new file mode 100644 index 0000000..6661e38 --- /dev/null +++ b/tgs/models/snowflake/utils.py @@ -0,0 +1,741 @@ +# -*- coding: utf-8 -*- +# @Author: Peng Xiang + +import types +import torch +import torch.nn.functional as F +import numpy as np +from torch import nn, einsum +from pointnet2_ops.pointnet2_utils import furthest_point_sample, \ + gather_operation, ball_query, three_nn, three_interpolate, grouping_operation + +class Conv1d(nn.Module): + def __init__(self, in_channel, out_channel, kernel_size=1, stride=1, if_bn=True, activation_fn=torch.relu): + super(Conv1d, self).__init__() + self.conv = nn.Conv1d(in_channel, out_channel, kernel_size, stride=stride) + self.if_bn = if_bn + self.bn = nn.BatchNorm1d(out_channel) + self.activation_fn = activation_fn + + def forward(self, input): + out = self.conv(input) + if self.if_bn: + out = self.bn(out) + + if self.activation_fn is not None: + out = self.activation_fn(out) + + return out + +class Conv2d(nn.Module): + def __init__(self, in_channel, out_channel, kernel_size=(1, 1), stride=(1, 1), if_bn=True, activation_fn=torch.relu): + super(Conv2d, self).__init__() + self.conv = nn.Conv2d(in_channel, out_channel, kernel_size, stride=stride) + self.if_bn = if_bn + self.bn = nn.BatchNorm2d(out_channel) + self.activation_fn = activation_fn + + def forward(self, input): + out = self.conv(input) + if self.if_bn: + out = self.bn(out) + + if self.activation_fn is not None: + out = self.activation_fn(out) + + return out + +class MLP(nn.Module): + def __init__(self, in_channel, layer_dims, bn=None): + super(MLP, self).__init__() + layers = [] + last_channel = in_channel + for out_channel in layer_dims[:-1]: + layers.append(nn.Linear(last_channel, out_channel)) + if bn: + layers.append(nn.BatchNorm1d(out_channel)) + layers.append(nn.ReLU()) + last_channel = out_channel + layers.append(nn.Linear(last_channel, layer_dims[-1])) + self.mlp = nn.Sequential(*layers) + + def forward(self, inputs): + return self.mlp(inputs) + +class MLP_CONV(nn.Module): + def __init__(self, in_channel, layer_dims, bn=None): + super(MLP_CONV, self).__init__() + layers = [] + last_channel = in_channel + for out_channel in layer_dims[:-1]: + layers.append(nn.Conv1d(last_channel, out_channel, 1)) + if bn: + layers.append(nn.BatchNorm1d(out_channel)) + layers.append(nn.ReLU()) + last_channel = out_channel + layers.append(nn.Conv1d(last_channel, layer_dims[-1], 1)) + self.mlp = nn.Sequential(*layers) + + def forward(self, inputs): + return self.mlp(inputs) + +class MLP_Res(nn.Module): + def __init__(self, in_dim=128, hidden_dim=None, out_dim=128): + super(MLP_Res, self).__init__() + if hidden_dim is None: + hidden_dim = in_dim + self.conv_1 = nn.Conv1d(in_dim, hidden_dim, 1) + self.conv_2 = nn.Conv1d(hidden_dim, out_dim, 1) + self.conv_shortcut = nn.Conv1d(in_dim, out_dim, 1) + + def forward(self, x): + """ + Args: + x: (B, out_dim, n) + """ + shortcut = self.conv_shortcut(x) + out = self.conv_2(torch.relu(self.conv_1(x))) + shortcut + return out + + +def sample_and_group(xyz, points, npoint, nsample, radius, use_xyz=True): + """ + Args: + xyz: Tensor, (B, 3, N) + points: Tensor, (B, f, N) + npoint: int + nsample: int + radius: float + use_xyz: boolean + + Returns: + new_xyz: Tensor, (B, 3, npoint) + new_points: Tensor, (B, 3 | f+3 | f, npoint, nsample) + idx_local: Tensor, (B, npoint, nsample) + grouped_xyz: Tensor, (B, 3, npoint, nsample) + + """ + xyz_flipped = xyz.permute(0, 2, 1).contiguous() # (B, N, 3) + new_xyz = gather_operation(xyz, furthest_point_sample(xyz_flipped, npoint)) # (B, 3, npoint) + + idx = ball_query(radius, nsample, xyz_flipped, new_xyz.permute(0, 2, 1).contiguous()) # (B, npoint, nsample) + grouped_xyz = grouping_operation(xyz, idx) # (B, 3, npoint, nsample) + grouped_xyz -= new_xyz.unsqueeze(3).repeat(1, 1, 1, nsample) + + if points is not None: + grouped_points = grouping_operation(points, idx) # (B, f, npoint, nsample) + if use_xyz: + new_points = torch.cat([grouped_xyz, grouped_points], 1) + else: + new_points = grouped_points + else: + new_points = grouped_xyz + + return new_xyz, new_points, idx, grouped_xyz + + +def sample_and_group_all(xyz, points, use_xyz=True): + """ + Args: + xyz: Tensor, (B, 3, nsample) + points: Tensor, (B, f, nsample) + use_xyz: boolean + + Returns: + new_xyz: Tensor, (B, 3, 1) + new_points: Tensor, (B, f|f+3|3, 1, nsample) + idx: Tensor, (B, 1, nsample) + grouped_xyz: Tensor, (B, 3, 1, nsample) + """ + b, _, nsample = xyz.shape + device = xyz.device + new_xyz = torch.zeros((1, 3, 1), dtype=torch.float, device=device).repeat(b, 1, 1) + grouped_xyz = xyz.reshape((b, 3, 1, nsample)) + idx = torch.arange(nsample, device=device).reshape(1, 1, nsample).repeat(b, 1, 1) + if points is not None: + if use_xyz: + new_points = torch.cat([xyz, points], 1) + else: + new_points = points + new_points = new_points.unsqueeze(2) + else: + new_points = grouped_xyz + + return new_xyz, new_points, idx, grouped_xyz + + +class PointNet_SA_Module(nn.Module): + def __init__(self, npoint, nsample, radius, in_channel, mlp, if_bn=True, group_all=False, use_xyz=True): + """ + Args: + npoint: int, number of points to sample + nsample: int, number of points in each local region + radius: float + in_channel: int, input channel of features(points) + mlp: list of int, + """ + super(PointNet_SA_Module, self).__init__() + self.npoint = npoint + self.nsample = nsample + self.radius = radius + self.mlp = mlp + self.group_all = group_all + self.use_xyz = use_xyz + if use_xyz: + in_channel += 3 + + last_channel = in_channel + self.mlp_conv = [] + for out_channel in mlp: + self.mlp_conv.append(Conv2d(last_channel, out_channel, if_bn=if_bn)) + last_channel = out_channel + + self.mlp_conv = nn.Sequential(*self.mlp_conv) + + def forward(self, xyz, points): + """ + Args: + xyz: Tensor, (B, 3, N) + points: Tensor, (B, f, N) + + Returns: + new_xyz: Tensor, (B, 3, npoint) + new_points: Tensor, (B, mlp[-1], npoint) + """ + if self.group_all: + new_xyz, new_points, idx, grouped_xyz = sample_and_group_all(xyz, points, self.use_xyz) + else: + new_xyz, new_points, idx, grouped_xyz = sample_and_group(xyz, points, self.npoint, self.nsample, self.radius, self.use_xyz) + + new_points = self.mlp_conv(new_points) + new_points = torch.max(new_points, 3)[0] + + return new_xyz, new_points + + +class PointNet_FP_Module(nn.Module): + def __init__(self, in_channel, mlp, use_points1=False, in_channel_points1=None, if_bn=True): + """ + Args: + in_channel: int, input channel of points2 + mlp: list of int + use_points1: boolean, if use points + in_channel_points1: int, input channel of points1 + """ + super(PointNet_FP_Module, self).__init__() + self.use_points1 = use_points1 + + if use_points1: + in_channel += in_channel_points1 + + last_channel = in_channel + self.mlp_conv = [] + for out_channel in mlp: + self.mlp_conv.append(Conv1d(last_channel, out_channel, if_bn=if_bn)) + last_channel = out_channel + + self.mlp_conv = nn.Sequential(*self.mlp_conv) + + def forward(self, xyz1, xyz2, points1, points2): + """ + Args: + xyz1: Tensor, (B, 3, N) + xyz2: Tensor, (B, 3, M) + points1: Tensor, (B, in_channel, N) + points2: Tensor, (B, in_channel, M) + + Returns:MLP_CONV + new_points: Tensor, (B, mlp[-1], N) + """ + dist, idx = three_nn(xyz1.permute(0, 2, 1).contiguous(), xyz2.permute(0, 2, 1).contiguous()) + dist = torch.clamp_min(dist, 1e-10) # (B, N, 3) + recip_dist = 1.0/dist + norm = torch.sum(recip_dist, 2, keepdim=True).repeat((1, 1, 3)) + weight = recip_dist / norm + interpolated_points = three_interpolate(points2, idx, weight) # B, in_channel, N + + if self.use_points1: + new_points = torch.cat([interpolated_points, points1], 1) + else: + new_points = interpolated_points + + new_points = self.mlp_conv(new_points) + return new_points + + +def square_distance(src, dst): + """ + Calculate Euclid distance between each two points. + + src^T * dst = xn * xm + yn * ym + zn * zm; + sum(src^2, dim=-1) = xn*xn + yn*yn + zn*zn; + sum(dst^2, dim=-1) = xm*xm + ym*ym + zm*zm; + dist = (xn - xm)^2 + (yn - ym)^2 + (zn - zm)^2 + = sum(src**2,dim=-1)+sum(dst**2,dim=-1)-2*src^T*dst + + Input: + src: source points, [B, N, C] + dst: target points, [B, M, C] + Output: + dist: per-point square distance, [B, N, M] + """ + B, N, _ = src.shape + _, M, _ = dst.shape + dist = -2 * torch.matmul(src, dst.permute(0, 2, 1)) # B, N, M + dist += torch.sum(src ** 2, -1).view(B, N, 1) + dist += torch.sum(dst ** 2, -1).view(B, 1, M) + return dist + + +def query_knn(nsample, xyz, new_xyz, include_self=True): + """Find k-NN of new_xyz in xyz""" + pad = 0 if include_self else 1 + sqrdists = square_distance(new_xyz, xyz) # B, S, N + idx = torch.argsort(sqrdists, dim=-1, descending=False)[:, :, pad: nsample+pad] + return idx.int() + + +def sample_and_group_knn(xyz, points, npoint, k, use_xyz=True, idx=None): + """ + Args: + xyz: Tensor, (B, 3, N) + points: Tensor, (B, f, N) + npoint: int + nsample: int + radius: float + use_xyz: boolean + + Returns: + new_xyz: Tensor, (B, 3, npoint) + new_points: Tensor, (B, 3 | f+3 | f, npoint, nsample) + idx_local: Tensor, (B, npoint, nsample) + grouped_xyz: Tensor, (B, 3, npoint, nsample) + + """ + xyz_flipped = xyz.permute(0, 2, 1).contiguous() # (B, N, 3) + new_xyz = gather_operation(xyz, furthest_point_sample(xyz_flipped, npoint)) # (B, 3, npoint) + if idx is None: + idx = query_knn(k, xyz_flipped, new_xyz.permute(0, 2, 1).contiguous()) + grouped_xyz = grouping_operation(xyz, idx) # (B, 3, npoint, nsample) + grouped_xyz -= new_xyz.unsqueeze(3).repeat(1, 1, 1, k) + + if points is not None: + grouped_points = grouping_operation(points, idx) # (B, f, npoint, nsample) + if use_xyz: + new_points = torch.cat([grouped_xyz, grouped_points], 1) + else: + new_points = grouped_points + else: + new_points = grouped_xyz + + return new_xyz, new_points, idx, grouped_xyz + + +class PointNet_SA_Module_KNN(nn.Module): + def __init__(self, npoint, nsample, in_channel, mlp, if_bn=True, group_all=False, use_xyz=True, if_idx=False): + """ + Args: + npoint: int, number of points to sample + nsample: int, number of points in each local region + radius: float + in_channel: int, input channel of features(points) + mlp: list of int, + """ + super(PointNet_SA_Module_KNN, self).__init__() + self.npoint = npoint + self.nsample = nsample + self.mlp = mlp + self.group_all = group_all + self.use_xyz = use_xyz + self.if_idx = if_idx + if use_xyz: + in_channel += 3 + + last_channel = in_channel + self.mlp_conv = [] + for out_channel in mlp[:-1]: + self.mlp_conv.append(Conv2d(last_channel, out_channel, if_bn=if_bn)) + last_channel = out_channel + self.mlp_conv.append(Conv2d(last_channel, mlp[-1], if_bn=False, activation_fn=None)) + self.mlp_conv = nn.Sequential(*self.mlp_conv) + + def forward(self, xyz, points, idx=None): + """ + Args: + xyz: Tensor, (B, 3, N) + points: Tensor, (B, f, N) + + Returns: + new_xyz: Tensor, (B, 3, npoint) + new_points: Tensor, (B, mlp[-1], npoint) + """ + if self.group_all: + new_xyz, new_points, idx, grouped_xyz = sample_and_group_all(xyz, points, self.use_xyz) + else: + new_xyz, new_points, idx, grouped_xyz = sample_and_group_knn(xyz, points, self.npoint, self.nsample, self.use_xyz, idx=idx) + + new_points = self.mlp_conv(new_points) + new_points = torch.max(new_points, 3)[0] + + if self.if_idx: + return new_xyz, new_points, idx + else: + return new_xyz, new_points + + +def fps_subsample(pcd, n_points=2048): + """ + Args + pcd: (b, 16384, 3) + + returns + new_pcd: (b, n_points, 3) + """ + new_pcd = gather_operation(pcd.permute(0, 2, 1).contiguous(), furthest_point_sample(pcd, n_points)) + new_pcd = new_pcd.permute(0, 2, 1).contiguous() + return new_pcd + + +class Transformer(nn.Module): + def __init__(self, in_channel, dim=256, n_knn=16, pos_hidden_dim=64, attn_hidden_multiplier=4): + super(Transformer, self).__init__() + self.n_knn = n_knn + self.conv_key = nn.Conv1d(dim, dim, 1) + self.conv_query = nn.Conv1d(dim, dim, 1) + self.conv_value = nn.Conv1d(dim, dim, 1) + + self.pos_mlp = nn.Sequential( + nn.Conv2d(3, pos_hidden_dim, 1), + nn.BatchNorm2d(pos_hidden_dim), + nn.ReLU(), + nn.Conv2d(pos_hidden_dim, dim, 1) + ) + + self.attn_mlp = nn.Sequential( + nn.Conv2d(dim, dim * attn_hidden_multiplier, 1), + nn.BatchNorm2d(dim * attn_hidden_multiplier), + nn.ReLU(), + nn.Conv2d(dim * attn_hidden_multiplier, dim, 1) + ) + + self.linear_start = nn.Conv1d(in_channel, dim, 1) + self.linear_end = nn.Conv1d(dim, in_channel, 1) + + def forward(self, x, pos): + """feed forward of transformer + Args: + x: Tensor of features, (B, in_channel, n) + pos: Tensor of positions, (B, 3, n) + + Returns: + y: Tensor of features with attention, (B, in_channel, n) + """ + + identity = x + + x = self.linear_start(x) + b, dim, n = x.shape + + pos_flipped = pos.permute(0, 2, 1).contiguous() + idx_knn = query_knn(self.n_knn, pos_flipped, pos_flipped) + key = self.conv_key(x) + value = self.conv_value(x) + query = self.conv_query(x) + + key = grouping_operation(key, idx_knn) # b, dim, n, n_knn + qk_rel = query.reshape((b, -1, n, 1)) - key + + pos_rel = pos.reshape((b, -1, n, 1)) - grouping_operation(pos, idx_knn) # b, 3, n, n_knn + pos_embedding = self.pos_mlp(pos_rel) # b, dim, n, n_knn + + attention = self.attn_mlp(qk_rel + pos_embedding) + attention = torch.softmax(attention, -1) + + value = value.reshape((b, -1, n, 1)) + pos_embedding + + agg = einsum('b c i j, b c i j -> b c i', attention, value) # b, dim, n + y = self.linear_end(agg) + + return y+identity + + +class CouplingLayer(nn.Module): + + def __init__(self, d, intermediate_dim, swap=False): + nn.Module.__init__(self) + self.d = d - (d // 2) + self.swap = swap + self.net_s_t = nn.Sequential( + nn.Linear(self.d, intermediate_dim), + nn.ReLU(inplace=True), + nn.Linear(intermediate_dim, intermediate_dim), + nn.ReLU(inplace=True), + nn.Linear(intermediate_dim, (d - self.d) * 2), + ) + + def forward(self, x, logpx=None, reverse=False): + + if self.swap: + x = torch.cat([x[:, self.d:], x[:, :self.d]], 1) + + in_dim = self.d + out_dim = x.shape[1] - self.d + + s_t = self.net_s_t(x[:, :in_dim]) + scale = torch.sigmoid(s_t[:, :out_dim] + 2.) + shift = s_t[:, out_dim:] + + logdetjac = torch.sum(torch.log(scale).view(scale.shape[0], -1), 1, keepdim=True) + + if not reverse: + y1 = x[:, self.d:] * scale + shift + delta_logp = -logdetjac + else: + y1 = (x[:, self.d:] - shift) / scale + delta_logp = logdetjac + + y = torch.cat([x[:, :self.d], y1], 1) if not self.swap else torch.cat([y1, x[:, :self.d]], 1) + + if logpx is None: + return y + else: + return y, logpx + delta_logp + + +class SequentialFlow(nn.Module): + """A generalized nn.Sequential container for normalizing flows. + """ + + def __init__(self, layersList): + super(SequentialFlow, self).__init__() + self.chain = nn.ModuleList(layersList) + + def forward(self, x, logpx=None, reverse=False, inds=None): + if inds is None: + if reverse: + inds = range(len(self.chain) - 1, -1, -1) + else: + inds = range(len(self.chain)) + + if logpx is None: + for i in inds: + x = self.chain[i](x, reverse=reverse) + return x + else: + for i in inds: + x, logpx = self.chain[i](x, logpx, reverse=reverse) + return x, logpx + + +def build_latent_flow(args): + chain = [] + for i in range(args.latent_flow_depth): + chain.append(CouplingLayer(args.latent_dim, args.latent_flow_hidden_dim, swap=(i % 2 == 0))) + return SequentialFlow(chain) + + +################## +## SpectralNorm ## +################## + +POWER_ITERATION_FN = "spectral_norm_power_iteration" + + +class SpectralNorm(object): + def __init__(self, name='weight', dim=0, eps=1e-12): + self.name = name + self.dim = dim + self.eps = eps + + def compute_weight(self, module, n_power_iterations): + if n_power_iterations < 0: + raise ValueError( + 'Expected n_power_iterations to be non-negative, but ' + 'got n_power_iterations={}'.format(n_power_iterations) + ) + + weight = getattr(module, self.name + '_orig') + u = getattr(module, self.name + '_u') + v = getattr(module, self.name + '_v') + weight_mat = weight + if self.dim != 0: + # permute dim to front + weight_mat = weight_mat.permute(self.dim, * [d for d in range(weight_mat.dim()) if d != self.dim]) + height = weight_mat.size(0) + weight_mat = weight_mat.reshape(height, -1) + with torch.no_grad(): + for _ in range(n_power_iterations): + # Spectral norm of weight equals to `u^T W v`, where `u` and `v` + # are the first left and right singular vectors. + # This power iteration produces approximations of `u` and `v`. + v = F.normalize(torch.matmul(weight_mat.t(), u), dim=0, eps=self.eps) + u = F.normalize(torch.matmul(weight_mat, v), dim=0, eps=self.eps) + setattr(module, self.name + '_u', u) + setattr(module, self.name + '_v', v) + + sigma = torch.dot(u, torch.matmul(weight_mat, v)) + weight = weight / sigma + setattr(module, self.name, weight) + + def remove(self, module): + weight = getattr(module, self.name) + delattr(module, self.name) + delattr(module, self.name + '_u') + delattr(module, self.name + '_orig') + module.register_parameter(self.name, torch.nn.Parameter(weight)) + + def get_update_method(self, module): + def update_fn(module, n_power_iterations): + self.compute_weight(module, n_power_iterations) + + return update_fn + + def __call__(self, module, unused_inputs): + del unused_inputs + self.compute_weight(module, n_power_iterations=0) + + # requires_grad might be either True or False during inference. + if not module.training: + r_g = getattr(module, self.name + '_orig').requires_grad + setattr(module, self.name, getattr(module, self.name).detach().requires_grad_(r_g)) + + @staticmethod + def apply(module, name, dim, eps): + fn = SpectralNorm(name, dim, eps) + weight = module._parameters[name] + height = weight.size(dim) + + u = F.normalize(weight.new_empty(height).normal_(0, 1), dim=0, eps=fn.eps) + v = F.normalize(weight.new_empty(int(weight.numel() / height)).normal_(0, 1), dim=0, eps=fn.eps) + delattr(module, fn.name) + module.register_parameter(fn.name + "_orig", weight) + # We still need to assign weight back as fn.name because all sorts of + # things may assume that it exists, e.g., when initializing weights. + # However, we can't directly assign as it could be an nn.Parameter and + # gets added as a parameter. Instead, we register weight.data as a + # buffer, which will cause weight to be included in the state dict + # and also supports nn.init due to shared storage. + module.register_buffer(fn.name, weight.data) + module.register_buffer(fn.name + "_u", u) + module.register_buffer(fn.name + "_v", v) + + setattr(module, POWER_ITERATION_FN, types.MethodType(fn.get_update_method(module), module)) + + module.register_forward_pre_hook(fn) + return fn + + +def inplace_spectral_norm(module, name='weight', dim=None, eps=1e-12): + r"""Applies spectral normalization to a parameter in the given module. + .. math:: + \mathbf{W} = \dfrac{\mathbf{W}}{\sigma(\mathbf{W})} \\ + \sigma(\mathbf{W}) = \max_{\mathbf{h}: \mathbf{h} \ne 0} \dfrac{\|\mathbf{W} \mathbf{h}\|_2}{\|\mathbf{h}\|_2} + Spectral normalization stabilizes the training of discriminators (critics) + in Generaive Adversarial Networks (GANs) by rescaling the weight tensor + with spectral norm :math:`\sigma` of the weight matrix calculated using + power iteration method. If the dimension of the weight tensor is greater + than 2, it is reshaped to 2D in power iteration method to get spectral + norm. This is implemented via a hook that calculates spectral norm and + rescales weight before every :meth:`~Module.forward` call. + See `Spectral Normalization for Generative Adversarial Networks`_ . + .. _`Spectral Normalization for Generative Adversarial Networks`: https://arxiv.org/abs/1802.05957 + Args: + module (nn.Module): containing module + name (str, optional): name of weight parameter + n_power_iterations (int, optional): number of power iterations to + calculate spectal norm + dim (int, optional): dimension corresponding to number of outputs, + the default is 0, except for modules that are instances of + ConvTranspose1/2/3d, when it is 1 + eps (float, optional): epsilon for numerical stability in + calculating norms + Returns: + The original module with the spectal norm hook + Example:: + >>> m = spectral_norm(nn.Linear(20, 40)) + Linear (20 -> 40) + >>> m.weight_u.size() + torch.Size([20]) + """ + if dim is None: + if isinstance(module, (torch.nn.ConvTranspose1d, torch.nn.ConvTranspose2d, torch.nn.ConvTranspose3d)): + dim = 1 + else: + dim = 0 + SpectralNorm.apply(module, name, dim=dim, eps=eps) + return module + + +def remove_spectral_norm(module, name='weight'): + r"""Removes the spectral normalization reparameterization from a module. + Args: + module (nn.Module): containing module + name (str, optional): name of weight parameter + Example: + >>> m = spectral_norm(nn.Linear(40, 10)) + >>> remove_spectral_norm(m) + """ + for k, hook in module._forward_pre_hooks.items(): + if isinstance(hook, SpectralNorm) and hook.name == name: + hook.remove(module) + del module._forward_pre_hooks[k] + return module + + raise ValueError("spectral_norm of '{}' not found in {}".format(name, module)) + + +def add_spectral_norm(model, logger=None): + """Applies spectral norm to all modules within the scope of a CNF.""" + + def apply_spectral_norm(module): + if 'weight' in module._parameters: + if logger: logger.info("Adding spectral norm to {}".format(module)) + inplace_spectral_norm(module, 'weight') + + def find_coupling_layer(module): + if isinstance(module, CouplingLayer): + module.apply(apply_spectral_norm) + else: + for child in module.children(): + find_coupling_layer(child) + + find_coupling_layer(model) + + +def spectral_norm_power_iteration(model, n_power_iterations=1): + + def recursive_power_iteration(module): + if hasattr(module, POWER_ITERATION_FN): + getattr(module, POWER_ITERATION_FN)(n_power_iterations) + + model.apply(recursive_power_iteration) + +def reparameterize_gaussian(mean, logvar): + std = torch.exp(0.5 * logvar) + eps = torch.randn(std.size()).to(mean) + return mean + std * eps + + + +def gaussian_entropy(logvar): + const = 0.5 * float(logvar.size(1)) * (1. + np.log(np.pi * 2)) + ent = 0.5 * logvar.sum(dim=1, keepdim=False) + const + return ent + + +def standard_normal_logprob(z): + dim = z.size(-1) + log_z = -0.5 * dim * np.log(2 * np.pi) + return log_z - z.pow(2) / 2 + +def truncated_normal_(tensor, mean=0, std=1, trunc_std=2): + """ + Taken from https://discuss.pytorch.org/t/implementing-truncated-normal-initializer/4778/15 + """ + size = tensor.shape + tmp = tensor.new_empty(size + (4,)).normal_() + valid = (tmp < trunc_std) & (tmp > -trunc_std) + ind = valid.max(-1, keepdim=True)[1] + tensor.data.copy_(tmp.gather(-1, ind).squeeze(-1)) + tensor.data.mul_(std).add_(mean) + return tensor \ No newline at end of file diff --git a/tgs/models/tokenizers/dinov2.py b/tgs/models/tokenizers/dinov2.py new file mode 100644 index 0000000..853aa89 --- /dev/null +++ b/tgs/models/tokenizers/dinov2.py @@ -0,0 +1,1175 @@ +# coding=utf-8 +# Copyright 2023 Meta AI and The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" PyTorch DINOv2 model.""" + + +import collections.abc +import math +from typing import Dict, List, Optional, Set, Tuple, Union +from dataclasses import dataclass + +import torch +import torch.utils.checkpoint +from torch import nn +from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss + +from transformers.activations import ACT2FN +from transformers.modeling_outputs import ( + BackboneOutput, + BaseModelOutput, + BaseModelOutputWithPooling, + ImageClassifierOutput, +) +from transformers.modeling_utils import PreTrainedModel +from transformers.pytorch_utils import ( + find_pruneable_heads_and_indices, + prune_linear_layer, +) +from transformers.utils import ( + add_code_sample_docstrings, + add_start_docstrings, + add_start_docstrings_to_model_forward, + logging, + replace_return_docstrings, +) +from transformers.utils.backbone_utils import BackboneMixin +from transformers.models.dinov2.configuration_dinov2 import Dinov2Config + +from tgs.models.transformers import MemoryEfficientAttentionMixin +from tgs.utils.typing import * + + +logger = logging.get_logger(__name__) + +# General docstring +_CONFIG_FOR_DOC = "Dinov2Config" + +# Base docstring +_CHECKPOINT_FOR_DOC = "facebook/dinov2-base" +_EXPECTED_OUTPUT_SHAPE = [1, 257, 768] + +# Image classification docstring +_IMAGE_CLASS_CHECKPOINT = "facebook/dinov2-base" + + +DINOV2_PRETRAINED_MODEL_ARCHIVE_LIST = [ + "facebook/dinov2-base", + # See all DINOv2 models at https://huggingface.co/models?filter=dinov2 +] + + +class Dinov2Embeddings(nn.Module): + """ + Construct the CLS token, mask token, position and patch embeddings. + """ + + def __init__(self, config: Dinov2Config) -> None: + super().__init__() + + self.cls_token = nn.Parameter(torch.randn(1, 1, config.hidden_size)) + # register as mask token as it's not used in optimization + # to avoid the use of find_unused_parameters_true + # self.mask_token = nn.Parameter(torch.zeros(1, config.hidden_size)) + self.register_buffer("mask_token", torch.zeros(1, config.hidden_size)) + self.patch_embeddings = Dinov2PatchEmbeddings(config) + num_patches = self.patch_embeddings.num_patches + self.position_embeddings = nn.Parameter( + torch.randn(1, num_patches + 1, config.hidden_size) + ) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + self.config = config + + def interpolate_pos_encoding( + self, embeddings: torch.Tensor, height: int, width: int + ) -> torch.Tensor: + """ + This method allows to interpolate the pre-trained position encodings, to be able to use the model on higher + resolution images. + + Source: + https://github.com/facebookresearch/dino/blob/de9ee3df6cf39fac952ab558447af1fa1365362a/vision_transformer.py#L174 + """ + + num_patches = embeddings.shape[1] - 1 + num_positions = self.position_embeddings.shape[1] - 1 + if num_patches == num_positions and height == width: + return self.position_embeddings + class_pos_embed = self.position_embeddings[:, 0] + patch_pos_embed = self.position_embeddings[:, 1:] + dim = embeddings.shape[-1] + height = height // self.config.patch_size + width = width // self.config.patch_size + # we add a small number to avoid floating point error in the interpolation + # see discussion at https://github.com/facebookresearch/dino/issues/8 + height, width = height + 0.1, width + 0.1 + patch_pos_embed = patch_pos_embed.reshape( + 1, int(math.sqrt(num_positions)), int(math.sqrt(num_positions)), dim + ) + patch_pos_embed = patch_pos_embed.permute(0, 3, 1, 2) + patch_pos_embed = nn.functional.interpolate( + patch_pos_embed, + scale_factor=( + height / math.sqrt(num_positions), + width / math.sqrt(num_positions), + ), + mode="bicubic", + align_corners=False, + ) + if ( + int(height) != patch_pos_embed.shape[-2] + or int(width) != patch_pos_embed.shape[-1] + ): + raise ValueError( + "Width or height does not match with the interpolated position embeddings" + ) + patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim) + return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1) + + def forward( + self, pixel_values: torch.Tensor, bool_masked_pos: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + batch_size, _, height, width = pixel_values.shape + patch_embeddings = self.patch_embeddings(pixel_values) + embeddings = patch_embeddings + + if bool_masked_pos is not None: + embeddings = torch.where( + bool_masked_pos.unsqueeze(-1), + self.mask_token.to(embeddings.dtype).unsqueeze(0), + embeddings, + ) + + # add the [CLS] token to the embedded patch tokens + cls_tokens = self.cls_token.expand(batch_size, -1, -1) + embeddings = torch.cat((cls_tokens, embeddings), dim=1) + + # add positional encoding to each token + embeddings = embeddings + self.interpolate_pos_encoding( + embeddings, height, width + ) + + embeddings = self.dropout(embeddings) + + return embeddings + + +class Dinov2PatchEmbeddings(nn.Module): + """ + This class turns `pixel_values` of shape `(batch_size, num_channels, height, width)` into the initial + `hidden_states` (patch embeddings) of shape `(batch_size, seq_length, hidden_size)` to be consumed by a + Transformer. + """ + + def __init__(self, config): + super().__init__() + image_size, patch_size = config.image_size, config.patch_size + num_channels, hidden_size = config.num_channels, config.hidden_size + + image_size = ( + image_size + if isinstance(image_size, collections.abc.Iterable) + else (image_size, image_size) + ) + patch_size = ( + patch_size + if isinstance(patch_size, collections.abc.Iterable) + else (patch_size, patch_size) + ) + num_patches = (image_size[1] // patch_size[1]) * ( + image_size[0] // patch_size[0] + ) + self.image_size = image_size + self.patch_size = patch_size + self.num_channels = num_channels + self.num_patches = num_patches + + self.projection = nn.Conv2d( + num_channels, hidden_size, kernel_size=patch_size, stride=patch_size + ) + + def forward(self, pixel_values: torch.Tensor) -> torch.Tensor: + num_channels = pixel_values.shape[1] + if num_channels != self.num_channels: + raise ValueError( + "Make sure that the channel dimension of the pixel values match with the one set in the configuration." + f" Expected {self.num_channels} but got {num_channels}." + ) + embeddings = self.projection(pixel_values).flatten(2).transpose(1, 2) + return embeddings + + +# Copied from transformers.models.vit.modeling_vit.ViTSelfAttention with ViT->Dinov2 +class Dinov2SelfAttention(nn.Module): + def __init__(self, config: Dinov2Config) -> None: + super().__init__() + if config.hidden_size % config.num_attention_heads != 0 and not hasattr( + config, "embedding_size" + ): + raise ValueError( + f"The hidden size {config.hidden_size,} is not a multiple of the number of attention " + f"heads {config.num_attention_heads}." + ) + + self.num_attention_heads = config.num_attention_heads + self.attention_head_size = int(config.hidden_size / config.num_attention_heads) + self.all_head_size = self.num_attention_heads * self.attention_head_size + self.attention_probs_dropout_prob = config.attention_probs_dropout_prob + + self.query = nn.Linear( + config.hidden_size, self.all_head_size, bias=config.qkv_bias + ) + self.key = nn.Linear( + config.hidden_size, self.all_head_size, bias=config.qkv_bias + ) + self.value = nn.Linear( + config.hidden_size, self.all_head_size, bias=config.qkv_bias + ) + + self.dropout = nn.Dropout(config.attention_probs_dropout_prob) + + self.use_memory_efficient_attention_xformers: bool = False + + def transpose_for_scores(self, x: torch.Tensor) -> torch.Tensor: + new_x_shape = x.size()[:-1] + ( + self.num_attention_heads, + self.attention_head_size, + ) + x = x.view(new_x_shape) + return x.permute(0, 2, 1, 3) + + def forward( + self, + hidden_states, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]: + mixed_query_layer = self.query(hidden_states) + + if self.use_memory_efficient_attention_xformers: + import xformers + assert head_mask is None and not output_attentions + new_size = hidden_states.size()[:-1] + ( + self.num_attention_heads, + self.attention_head_size, + ) + key_layer = self.key(hidden_states).view(new_size) + value_layer = self.value(hidden_states).view(new_size) + query_layer = mixed_query_layer.view(new_size) + context_layer = xformers.ops.memory_efficient_attention( + query_layer, key_layer, value_layer, p=self.attention_probs_dropout_prob + ) + context_layer = context_layer.view(*hidden_states.size()[:-1], -1) + else: + key_layer = self.transpose_for_scores(self.key(hidden_states)) + value_layer = self.transpose_for_scores(self.value(hidden_states)) + query_layer = self.transpose_for_scores(mixed_query_layer) + + # Take the dot product between "query" and "key" to get the raw attention scores. + attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2)) + + attention_scores = attention_scores / math.sqrt(self.attention_head_size) + + # Normalize the attention scores to probabilities. + attention_probs = nn.functional.softmax(attention_scores, dim=-1) + + # This is actually dropping out entire tokens to attend to, which might + # seem a bit unusual, but is taken from the original Transformer paper. + attention_probs = self.dropout(attention_probs) + + # Mask heads if we want to + if head_mask is not None: + attention_probs = attention_probs * head_mask + + context_layer = torch.matmul(attention_probs, value_layer) + + context_layer = context_layer.permute(0, 2, 1, 3).contiguous() + new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,) + context_layer = context_layer.view(new_context_layer_shape) + + outputs = ( + (context_layer, attention_probs) if output_attentions else (context_layer,) + ) + + return outputs + + def set_use_memory_efficient_attention_xformers( + self, valid: bool, attention_op: Optional[Callable] = None + ): + self.use_memory_efficient_attention_xformers = valid + + +# Copied from transformers.models.vit.modeling_vit.ViTSelfOutput with ViT->Dinov2 +class Dinov2SelfOutput(nn.Module): + """ + The residual connection is defined in Dinov2Layer instead of here (as is the case with other models), due to the + layernorm applied before each block. + """ + + def __init__(self, config: Dinov2Config) -> None: + super().__init__() + self.dense = nn.Linear(config.hidden_size, config.hidden_size) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + + def forward( + self, hidden_states: torch.Tensor, input_tensor: torch.Tensor + ) -> torch.Tensor: + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + + return hidden_states + + +# Copied from transformers.models.vit.modeling_vit.ViTAttention with ViT->Dinov2 +class Dinov2Attention(nn.Module): + def __init__(self, config: Dinov2Config) -> None: + super().__init__() + self.attention = Dinov2SelfAttention(config) + self.output = Dinov2SelfOutput(config) + self.pruned_heads = set() + + def prune_heads(self, heads: Set[int]) -> None: + if len(heads) == 0: + return + heads, index = find_pruneable_heads_and_indices( + heads, + self.attention.num_attention_heads, + self.attention.attention_head_size, + self.pruned_heads, + ) + + # Prune linear layers + self.attention.query = prune_linear_layer(self.attention.query, index) + self.attention.key = prune_linear_layer(self.attention.key, index) + self.attention.value = prune_linear_layer(self.attention.value, index) + self.output.dense = prune_linear_layer(self.output.dense, index, dim=1) + + # Update hyper params and store pruned heads + self.attention.num_attention_heads = self.attention.num_attention_heads - len( + heads + ) + self.attention.all_head_size = ( + self.attention.attention_head_size * self.attention.num_attention_heads + ) + self.pruned_heads = self.pruned_heads.union(heads) + + def forward( + self, + hidden_states: torch.Tensor, + head_mask: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]: + self_outputs = self.attention(hidden_states, head_mask, output_attentions) + + attention_output = self.output(self_outputs[0], hidden_states) + + outputs = (attention_output,) + self_outputs[ + 1: + ] # add attentions if we output them + return outputs + + +class Dinov2LayerScale(nn.Module): + def __init__(self, config) -> None: + super().__init__() + self.lambda1 = nn.Parameter( + config.layerscale_value * torch.ones(config.hidden_size) + ) + + def forward(self, hidden_state: torch.Tensor) -> torch.Tensor: + return hidden_state * self.lambda1 + + +# Copied from transformers.models.beit.modeling_beit.drop_path +def drop_path( + input: torch.Tensor, drop_prob: float = 0.0, training: bool = False +) -> torch.Tensor: + """ + Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + + Comment by Ross Wightman: This is the same as the DropConnect impl I created for EfficientNet, etc networks, + however, the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper... + See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for changing the + layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use 'survival rate' as the + argument. + """ + if drop_prob == 0.0 or not training: + return input + keep_prob = 1 - drop_prob + shape = (input.shape[0],) + (1,) * ( + input.ndim - 1 + ) # work with diff dim tensors, not just 2D ConvNets + random_tensor = keep_prob + torch.rand( + shape, dtype=input.dtype, device=input.device + ) + random_tensor.floor_() # binarize + output = input.div(keep_prob) * random_tensor + return output + + +# Copied from transformers.models.beit.modeling_beit.BeitDropPath +class Dinov2DropPath(nn.Module): + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + + def __init__(self, drop_prob: Optional[float] = None) -> None: + super().__init__() + self.drop_prob = drop_prob + + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: + return drop_path(hidden_states, self.drop_prob, self.training) + + def extra_repr(self) -> str: + return "p={}".format(self.drop_prob) + + +class Dinov2MLP(nn.Module): + def __init__(self, config) -> None: + super().__init__() + in_features = out_features = config.hidden_size + hidden_features = int(config.hidden_size * config.mlp_ratio) + self.fc1 = nn.Linear(in_features, hidden_features, bias=True) + if isinstance(config.hidden_act, str): + self.activation = ACT2FN[config.hidden_act] + else: + self.activation = config.hidden_act + self.fc2 = nn.Linear(hidden_features, out_features, bias=True) + + def forward(self, hidden_state: torch.Tensor) -> torch.Tensor: + hidden_state = self.fc1(hidden_state) + hidden_state = self.activation(hidden_state) + hidden_state = self.fc2(hidden_state) + return hidden_state + + +class Dinov2SwiGLUFFN(nn.Module): + def __init__(self, config) -> None: + super().__init__() + in_features = out_features = config.hidden_size + hidden_features = int(config.hidden_size * config.mlp_ratio) + hidden_features = (int(hidden_features * 2 / 3) + 7) // 8 * 8 + + self.weights_in = nn.Linear(in_features, 2 * hidden_features, bias=True) + self.weights_out = nn.Linear(hidden_features, out_features, bias=True) + + def forward(self, hidden_state: torch.Tensor) -> torch.Tensor: + hidden_state = self.weights_in(hidden_state) + x1, x2 = hidden_state.chunk(2, dim=-1) + hidden = nn.functional.silu(x1) * x2 + return self.weights_out(hidden) + + +class Dinov2Layer(nn.Module, MemoryEfficientAttentionMixin): + """This corresponds to the Block class in the original implementation.""" + + def __init__(self, config: Dinov2Config) -> None: + super().__init__() + + self.norm1 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + self.norm1_modulation = None + self.attention = Dinov2Attention(config) + self.layer_scale1 = Dinov2LayerScale(config) + self.drop_path1 = ( + Dinov2DropPath(config.drop_path_rate) + if config.drop_path_rate > 0.0 + else nn.Identity() + ) + + self.norm2 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + self.norm2_modulation = None + + if config.use_swiglu_ffn: + self.mlp = Dinov2SwiGLUFFN(config) + else: + self.mlp = Dinov2MLP(config) + self.layer_scale2 = Dinov2LayerScale(config) + self.drop_path2 = ( + Dinov2DropPath(config.drop_path_rate) + if config.drop_path_rate > 0.0 + else nn.Identity() + ) + + def forward( + self, + hidden_states: torch.Tensor, + head_mask: Optional[torch.Tensor] = None, + modulation_cond: Optional[torch.Tensor] = None, + output_attentions: bool = False, + ) -> Union[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor]]: + hidden_states_norm = self.norm1(hidden_states) + if self.norm1_modulation is not None: + assert modulation_cond is not None + hidden_states_norm = self.norm1_modulation( + hidden_states_norm, modulation_cond + ) + self_attention_outputs = self.attention( + hidden_states_norm, # in Dinov2, layernorm is applied before self-attention + head_mask, + output_attentions=output_attentions, + ) + attention_output = self_attention_outputs[0] + + attention_output = self.layer_scale1(attention_output) + outputs = self_attention_outputs[ + 1: + ] # add self attentions if we output attention weights + + # first residual connection + hidden_states = attention_output + hidden_states + + # in Dinov2, layernorm is also applied after self-attention + layer_output = self.norm2(hidden_states) + if self.norm2_modulation is not None: + assert modulation_cond is not None + layer_output = self.norm2_modulation(layer_output, modulation_cond) + layer_output = self.mlp(layer_output) + layer_output = self.layer_scale2(layer_output) + + # second residual connection + layer_output = layer_output + hidden_states + + outputs = (layer_output,) + outputs + + return outputs + + def register_ada_norm_modulation(self, norm1_mod: nn.Module, norm2_mod: nn.Module): + self.norm1_modulation = norm1_mod + self.norm2_modulation = norm2_mod + + +# Copied from transformers.models.vit.modeling_vit.ViTEncoder with ViT->Dinov2 +class Dinov2Encoder(nn.Module, MemoryEfficientAttentionMixin): + def __init__(self, config: Dinov2Config) -> None: + super().__init__() + self.config = config + self.layer = nn.ModuleList( + [Dinov2Layer(config) for _ in range(config.num_hidden_layers)] + ) + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.Tensor, + head_mask: Optional[torch.Tensor] = None, + modulation_cond: Optional[torch.Tensor] = None, + output_attentions: bool = False, + output_hidden_states: bool = False, + return_dict: bool = True, + ) -> Union[tuple, BaseModelOutput]: + all_hidden_states = () if output_hidden_states else None + all_self_attentions = () if output_attentions else None + + for i, layer_module in enumerate(self.layer): + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + layer_head_mask = head_mask[i] if head_mask is not None else None + + if self.gradient_checkpointing and self.training: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs, output_attentions) + + return custom_forward + + layer_outputs = torch.utils.checkpoint.checkpoint( + create_custom_forward(layer_module), + hidden_states, + layer_head_mask, + modulation_cond, + ) + else: + layer_outputs = layer_module( + hidden_states, layer_head_mask, modulation_cond, output_attentions + ) + + hidden_states = layer_outputs[0] + + if output_attentions: + all_self_attentions = all_self_attentions + (layer_outputs[1],) + + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + if not return_dict: + return tuple( + v + for v in [hidden_states, all_hidden_states, all_self_attentions] + if v is not None + ) + return BaseModelOutput( + last_hidden_state=hidden_states, + hidden_states=all_hidden_states, + attentions=all_self_attentions, + ) + + +class Dinov2PreTrainedModel(PreTrainedModel): + """ + An abstract class to handle weights initialization and a simple interface for downloading and loading pretrained + models. + """ + + config_class = Dinov2Config + base_model_prefix = "dinov2" + main_input_name = "pixel_values" + supports_gradient_checkpointing = True + + def _init_weights(self, module: Union[nn.Linear, nn.Conv2d, nn.LayerNorm]) -> None: + """Initialize the weights""" + if isinstance(module, (nn.Linear, nn.Conv2d)): + # Upcast the input in `fp32` and cast it back to desired `dtype` to avoid + # `trunc_normal_cpu` not implemented in `half` issues + module.weight.data = nn.init.trunc_normal_( + module.weight.data.to(torch.float32), + mean=0.0, + std=self.config.initializer_range, + ).to(module.weight.dtype) + if module.bias is not None: + module.bias.data.zero_() + elif isinstance(module, nn.LayerNorm): + module.bias.data.zero_() + module.weight.data.fill_(1.0) + elif isinstance(module, Dinov2Embeddings): + module.position_embeddings.data = nn.init.trunc_normal_( + module.position_embeddings.data.to(torch.float32), + mean=0.0, + std=self.config.initializer_range, + ).to(module.position_embeddings.dtype) + + module.cls_token.data = nn.init.trunc_normal_( + module.cls_token.data.to(torch.float32), + mean=0.0, + std=self.config.initializer_range, + ).to(module.cls_token.dtype) + + def _set_gradient_checkpointing( + self, module: Dinov2Encoder, value: bool = False + ) -> None: + if isinstance(module, Dinov2Encoder): + module.gradient_checkpointing = value + + +DINOV2_START_DOCSTRING = r""" + This model is a PyTorch [torch.nn.Module](https://pytorch.org/docs/stable/nn.html#torch.nn.Module) subclass. Use it + as a regular PyTorch Module and refer to the PyTorch documentation for all matter related to general usage and + behavior. + + Parameters: + config ([`Dinov2Config`]): Model configuration class with all the parameters of the model. + Initializing with a config file does not load the weights associated with the model, only the + configuration. Check out the [`~PreTrainedModel.from_pretrained`] method to load the model weights. +""" + +DINOV2_BASE_INPUTS_DOCSTRING = r""" + Args: + pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Pixel values. Pixel values can be obtained using [`AutoImageProcessor`]. See + [`BitImageProcessor.preprocess`] for details. + + bool_masked_pos (`torch.BoolTensor` of shape `(batch_size, sequence_length)`): + Boolean masked positions. Indicates which patches are masked (1) and which aren't (0). Only relevant for + pre-training. + + head_mask (`torch.FloatTensor` of shape `(num_heads,)` or `(num_layers, num_heads)`, *optional*): + Mask to nullify selected heads of the self-attention modules. Mask values selected in `[0, 1]`: + + - 1 indicates the head is **not masked**, + - 0 indicates the head is **masked**. + + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned + tensors for more detail. + output_hidden_states (`bool`, *optional*): + Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for + more detail. + return_dict (`bool`, *optional*): + Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple. +""" + +DINOV2_INPUTS_DOCSTRING = r""" + Args: + pixel_values (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + Pixel values. Pixel values can be obtained using [`AutoImageProcessor`]. See + [`BitImageProcessor.preprocess`] for details. + + head_mask (`torch.FloatTensor` of shape `(num_heads,)` or `(num_layers, num_heads)`, *optional*): + Mask to nullify selected heads of the self-attention modules. Mask values selected in `[0, 1]`: + + - 1 indicates the head is **not masked**, + - 0 indicates the head is **masked**. + + output_attentions (`bool`, *optional*): + Whether or not to return the attentions tensors of all attention layers. See `attentions` under returned + tensors for more detail. + output_hidden_states (`bool`, *optional*): + Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors for + more detail. + return_dict (`bool`, *optional*): + Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple. +""" + +@dataclass +class CustomBaseModelOutputWithPooling(BaseModelOutputWithPooling): + patch_embeddings: Optional[torch.FloatTensor] = None + + +@add_start_docstrings( + "The bare DINOv2 Model transformer outputting raw hidden-states without any specific head on top.", + DINOV2_START_DOCSTRING, +) +class Dinov2Model(Dinov2PreTrainedModel, MemoryEfficientAttentionMixin): + def __init__(self, config: Dinov2Config): + super().__init__(config) + self.config = config + + self.embeddings = Dinov2Embeddings(config) + self.encoder = Dinov2Encoder(config) + + self.layernorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self) -> Dinov2PatchEmbeddings: + return self.embeddings.patch_embeddings + + def _prune_heads(self, heads_to_prune: Dict[int, List[int]]) -> None: + """ + Prunes heads of the model. heads_to_prune: dict of {layer_num: list of heads to prune in this layer} See base + class PreTrainedModel + """ + for layer, heads in heads_to_prune.items(): + self.encoder.layer[layer].attention.prune_heads(heads) + + @add_start_docstrings_to_model_forward(DINOV2_BASE_INPUTS_DOCSTRING) + @add_code_sample_docstrings( + checkpoint=_CHECKPOINT_FOR_DOC, + output_type=BaseModelOutputWithPooling, + config_class=_CONFIG_FOR_DOC, + modality="vision", + expected_output=_EXPECTED_OUTPUT_SHAPE, + ) + def forward( + self, + pixel_values: Optional[torch.Tensor] = None, + bool_masked_pos: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + modulation_cond: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ) -> Union[Tuple, BaseModelOutputWithPooling]: + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + if pixel_values is None: + raise ValueError("You have to specify pixel_values") + + # Prepare head mask if needed + # 1.0 in head_mask indicate we keep the head + # attention_probs has shape bsz x n_heads x N x N + # input head_mask has shape [num_heads] or [num_hidden_layers x num_heads] + # and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length] + head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers) + + embedding_output = self.embeddings( + pixel_values, bool_masked_pos=bool_masked_pos + ) + + encoder_outputs = self.encoder( + embedding_output, + head_mask=head_mask, + modulation_cond=modulation_cond, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + sequence_output = encoder_outputs[0] + sequence_output = self.layernorm(sequence_output) + pooled_output = sequence_output[:, 0, :] + + if not return_dict: + head_outputs = (sequence_output, pooled_output) + return head_outputs + encoder_outputs[1:] + + return CustomBaseModelOutputWithPooling( + last_hidden_state=sequence_output, + pooler_output=pooled_output, + hidden_states=encoder_outputs.hidden_states, + attentions=encoder_outputs.attentions, + patch_embeddings=embedding_output + ) + + def set_gradient_checkpointing(self, value: bool = False) -> None: + self._set_gradient_checkpointing(self.encoder, value) + + +@add_start_docstrings( + """ + Dinov2 Model transformer with an image classification head on top (a linear layer on top of the final hidden state + of the [CLS] token) e.g. for ImageNet. + """, + DINOV2_START_DOCSTRING, +) +class Dinov2ForImageClassification(Dinov2PreTrainedModel): + def __init__(self, config: Dinov2Config) -> None: + super().__init__(config) + + self.num_labels = config.num_labels + self.dinov2 = Dinov2Model(config) + + # Classifier head + self.classifier = ( + nn.Linear(config.hidden_size * 2, config.num_labels) + if config.num_labels > 0 + else nn.Identity() + ) + + # Initialize weights and apply final processing + self.post_init() + + @add_start_docstrings_to_model_forward(DINOV2_INPUTS_DOCSTRING) + @add_code_sample_docstrings( + checkpoint=_IMAGE_CLASS_CHECKPOINT, + output_type=ImageClassifierOutput, + config_class=_CONFIG_FOR_DOC, + ) + def forward( + self, + pixel_values: Optional[torch.Tensor] = None, + head_mask: Optional[torch.Tensor] = None, + labels: Optional[torch.Tensor] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ) -> Union[tuple, ImageClassifierOutput]: + r""" + labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*): + Labels for computing the image classification/regression loss. Indices should be in `[0, ..., + config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If + `config.num_labels > 1` a classification loss is computed (Cross-Entropy). + """ + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + + outputs = self.dinov2( + pixel_values, + head_mask=head_mask, + output_attentions=output_attentions, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + + sequence_output = outputs[0] # batch_size, sequence_length, hidden_size + + cls_token = sequence_output[:, 0] + patch_tokens = sequence_output[:, 1:] + + linear_input = torch.cat([cls_token, patch_tokens.mean(dim=1)], dim=1) + + logits = self.classifier(linear_input) + + loss = None + if labels is not None: + # move labels to correct device to enable model parallelism + labels = labels.to(logits.device) + if self.config.problem_type is None: + if self.num_labels == 1: + self.config.problem_type = "regression" + elif self.num_labels > 1 and ( + labels.dtype == torch.long or labels.dtype == torch.int + ): + self.config.problem_type = "single_label_classification" + else: + self.config.problem_type = "multi_label_classification" + + if self.config.problem_type == "regression": + loss_fct = MSELoss() + if self.num_labels == 1: + loss = loss_fct(logits.squeeze(), labels.squeeze()) + else: + loss = loss_fct(logits, labels) + elif self.config.problem_type == "single_label_classification": + loss_fct = CrossEntropyLoss() + loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) + elif self.config.problem_type == "multi_label_classification": + loss_fct = BCEWithLogitsLoss() + loss = loss_fct(logits, labels) + + if not return_dict: + output = (logits,) + outputs[2:] + return ((loss,) + output) if loss is not None else output + + return ImageClassifierOutput( + loss=loss, + logits=logits, + hidden_states=outputs.hidden_states, + attentions=outputs.attentions, + ) + + +@add_start_docstrings( + """ + Dinov2 backbone, to be used with frameworks like DETR and MaskFormer. + """, + DINOV2_START_DOCSTRING, +) +class Dinov2Backbone(Dinov2PreTrainedModel, BackboneMixin): + def __init__(self, config): + super().__init__(config) + super()._init_backbone(config) + + self.num_features = [ + config.hidden_size for _ in range(config.num_hidden_layers + 1) + ] + self.embeddings = Dinov2Embeddings(config) + self.encoder = Dinov2Encoder(config) + + self.layernorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) + + # Initialize weights and apply final processing + self.post_init() + + def get_input_embeddings(self) -> Dinov2PatchEmbeddings: + return self.embeddings.patch_embeddings + + @add_start_docstrings_to_model_forward(DINOV2_INPUTS_DOCSTRING) + @replace_return_docstrings(output_type=BackboneOutput, config_class=_CONFIG_FOR_DOC) + def forward( + self, + pixel_values: torch.Tensor, + output_hidden_states: Optional[bool] = None, + output_attentions: Optional[bool] = None, + return_dict: Optional[bool] = None, + ) -> BackboneOutput: + """ + Returns: + + Examples: + + ```python + >>> from transformers import AutoImageProcessor, AutoBackbone + >>> import torch + >>> from PIL import Image + >>> import requests + + >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg" + >>> image = Image.open(requests.get(url, stream=True).raw) + + >>> processor = AutoImageProcessor.from_pretrained("facebook/dinov2-base") + >>> model = AutoBackbone.from_pretrained( + ... "facebook/dinov2-base", out_features=["stage2", "stage5", "stage8", "stage11"] + ... ) + + >>> inputs = processor(image, return_tensors="pt") + + >>> outputs = model(**inputs) + >>> feature_maps = outputs.feature_maps + >>> list(feature_maps[-1].shape) + [1, 768, 16, 16] + ```""" + return_dict = ( + return_dict if return_dict is not None else self.config.use_return_dict + ) + output_hidden_states = ( + output_hidden_states + if output_hidden_states is not None + else self.config.output_hidden_states + ) + output_attentions = ( + output_attentions + if output_attentions is not None + else self.config.output_attentions + ) + + embedding_output = self.embeddings(pixel_values) + + outputs = self.encoder( + embedding_output, + output_hidden_states=True, + output_attentions=output_attentions, + return_dict=return_dict, + ) + + hidden_states = outputs.hidden_states if return_dict else outputs[1] + + feature_maps = () + for stage, hidden_state in zip(self.stage_names, hidden_states): + if stage in self.out_features: + if self.config.apply_layernorm: + hidden_state = self.layernorm(hidden_state) + if self.config.reshape_hidden_states: + batch_size, _, height, width = pixel_values.shape + patch_size = self.config.patch_size + hidden_state = hidden_state[:, 1:, :].reshape( + batch_size, width // patch_size, height // patch_size, -1 + ) + hidden_state = hidden_state.permute(0, 3, 1, 2).contiguous() + feature_maps += (hidden_state,) + + if not return_dict: + if output_hidden_states: + output = (feature_maps,) + outputs[1:] + else: + output = (feature_maps,) + outputs[2:] + return output + + return BackboneOutput( + feature_maps=feature_maps, + hidden_states=outputs.hidden_states if output_hidden_states else None, + attentions=outputs.attentions if output_attentions else None, + ) + + + +class CustomPatchEmbeddings(nn.Module): + """ + This class turns `pixel_values` of shape `(batch_size, num_channels, height, width)` into the initial + `hidden_states` (patch embeddings) of shape `(batch_size, seq_length, hidden_size)` to be consumed by a + Transformer. + """ + + def __init__(self, image_size: int, patch_size: int, num_channels: int, hidden_size: int): + super().__init__() + + image_size = ( + image_size + if isinstance(image_size, collections.abc.Iterable) + else (image_size, image_size) + ) + patch_size = ( + patch_size + if isinstance(patch_size, collections.abc.Iterable) + else (patch_size, patch_size) + ) + num_patches = (image_size[1] // patch_size[1]) * ( + image_size[0] // patch_size[0] + ) + self.image_size = image_size + self.patch_size = patch_size + self.num_channels = num_channels + self.num_patches = num_patches + + self.projection = nn.Conv2d( + num_channels, hidden_size, kernel_size=patch_size, stride=patch_size + ) + + def forward(self, pixel_values: torch.Tensor) -> torch.Tensor: + num_channels = pixel_values.shape[1] + if num_channels != self.num_channels: + raise ValueError( + "Make sure that the channel dimension of the pixel values match with the one set in the configuration." + f" Expected {self.num_channels} but got {num_channels}." + ) + embeddings = self.projection(pixel_values).flatten(2).transpose(1, 2) + return embeddings + + +class CustomEmbeddings(nn.Module): + """ + Construct the CLS token, mask token, position and patch embeddings. + """ + + def __init__(self, image_size: int, patch_size: int, num_channels: int, hidden_size: int) -> None: + super().__init__() + + self.image_size = image_size + self.patch_size = patch_size + self.num_channels = num_channels + self.hidden_size = hidden_size + + self.cls_token = nn.Parameter(torch.randn(1, 1, self.hidden_size)) + + self.patch_embeddings = CustomPatchEmbeddings(image_size, patch_size, num_channels, hidden_size) + num_patches = self.patch_embeddings.num_patches + self.position_embeddings = nn.Parameter( + torch.randn(1, num_patches + 1, self.hidden_size) + ) + + def interpolate_pos_encoding( + self, embeddings: torch.Tensor, height: int, width: int + ) -> torch.Tensor: + """ + This method allows to interpolate the pre-trained position encodings, to be able to use the model on higher + resolution images. + + Source: + https://github.com/facebookresearch/dino/blob/de9ee3df6cf39fac952ab558447af1fa1365362a/vision_transformer.py#L174 + """ + + num_patches = embeddings.shape[1] - 1 + num_positions = self.position_embeddings.shape[1] - 1 + if num_patches == num_positions and height == width: + return self.position_embeddings + class_pos_embed = self.position_embeddings[:, 0] + patch_pos_embed = self.position_embeddings[:, 1:] + dim = embeddings.shape[-1] + height = height // self.patch_size + width = width // self.patch_size + # we add a small number to avoid floating point error in the interpolation + # see discussion at https://github.com/facebookresearch/dino/issues/8 + height, width = height + 0.1, width + 0.1 + patch_pos_embed = patch_pos_embed.reshape( + 1, int(math.sqrt(num_positions)), int(math.sqrt(num_positions)), dim + ) + patch_pos_embed = patch_pos_embed.permute(0, 3, 1, 2) + patch_pos_embed = nn.functional.interpolate( + patch_pos_embed, + scale_factor=( + height / math.sqrt(num_positions), + width / math.sqrt(num_positions), + ), + mode="bicubic", + align_corners=False, + ) + if ( + int(height) != patch_pos_embed.shape[-2] + or int(width) != patch_pos_embed.shape[-1] + ): + raise ValueError( + "Width or height does not match with the interpolated position embeddings" + ) + patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim) + return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1) + + def forward( + self, pixel_values: torch.Tensor, + ) -> torch.Tensor: + batch_size, _, height, width = pixel_values.shape + patch_embeddings = self.patch_embeddings(pixel_values) + embeddings = patch_embeddings + + # add the [CLS] token to the embedded patch tokens + cls_tokens = self.cls_token.expand(batch_size, -1, -1) + embeddings = torch.cat((cls_tokens, embeddings), dim=1) + + # add positional encoding to each token + embeddings = embeddings + self.interpolate_pos_encoding( + embeddings, height, width + ) + + return embeddings \ No newline at end of file diff --git a/tgs/models/tokenizers/image.py b/tgs/models/tokenizers/image.py new file mode 100644 index 0000000..eb8de83 --- /dev/null +++ b/tgs/models/tokenizers/image.py @@ -0,0 +1,135 @@ +from dataclasses import dataclass + +import torch +import torch.nn as nn +from einops import rearrange + +from tgs.utils.base import BaseModule +from tgs.models.tokenizers.dinov2 import Dinov2Model +from tgs.models.transformers import Modulation +from tgs.utils.typing import * + +class DINOV2SingleImageTokenizer(BaseModule): + @dataclass + class Config(BaseModule.Config): + pretrained_model_name_or_path: str = "facebook/dinov2-base" + width: int = 224 + height: int = 224 + modulation: bool = False + modulation_zero_init: bool = False + modulation_single_layer: bool = False + modulation_cond_dim: int = 16 + freeze_backbone_params: bool = True + enable_memory_efficient_attention: bool = False + enable_gradient_checkpointing: bool = False + use_patch_embeddings: bool = False + patch_embeddings_aggr_method: str = 'concat' + + cfg: Config + + def configure(self) -> None: + super().configure() + model: Dinov2Model + + if self.cfg.freeze_backbone_params: + # freeze dino backbone parameters + self.register_non_module( + "model", + Dinov2Model.from_pretrained(self.cfg.pretrained_model_name_or_path).to( + self.device + ), + ) + + model = self.non_module("model") + for p in model.parameters(): + p.requires_grad_(False) + model.eval() + else: + self.model = Dinov2Model.from_pretrained( + self.cfg.pretrained_model_name_or_path + ).to(self.device) + model = self.model + + model.set_use_memory_efficient_attention_xformers( + self.cfg.enable_memory_efficient_attention + ) + model.set_gradient_checkpointing(self.cfg.enable_gradient_checkpointing) + + # add modulation + if self.cfg.modulation: + modulations = [] + for layer in model.encoder.layer: + norm1_modulation = Modulation( + model.config.hidden_size, + self.cfg.modulation_cond_dim, + zero_init=self.cfg.modulation_zero_init, + single_layer=self.cfg.modulation_single_layer, + ) + norm2_modulation = Modulation( + model.config.hidden_size, + self.cfg.modulation_cond_dim, + zero_init=self.cfg.modulation_zero_init, + single_layer=self.cfg.modulation_single_layer, + ) + layer.register_ada_norm_modulation(norm1_modulation, norm2_modulation) + modulations += [norm1_modulation, norm2_modulation] + self.modulations = nn.ModuleList(modulations) + + self.register_buffer( + "image_mean", + torch.as_tensor([0.485, 0.456, 0.406]).reshape(1, 1, 3, 1, 1), + persistent=False, + ) + self.register_buffer( + "image_std", + torch.as_tensor([0.229, 0.224, 0.225]).reshape(1, 1, 3, 1, 1), + persistent=False, + ) + + def forward( + self, + images: Float[Tensor, "B *N C H W"], + modulation_cond: Optional[Float[Tensor, "B *N Cc"]], + ) -> Float[Tensor, "B *N Ct Nt"]: + model: Dinov2Model + if self.cfg.freeze_backbone_params: + model = self.non_module("model") + else: + model = self.model + + packed = False + if images.ndim == 4: + packed = True + images = images.unsqueeze(1) + if modulation_cond is not None: + assert modulation_cond.ndim == 2 + modulation_cond = modulation_cond.unsqueeze(1) + + batch_size, n_input_views = images.shape[:2] + images = (images - self.image_mean) / self.image_std + out = model( + rearrange(images, "B N C H W -> (B N) C H W"), + modulation_cond=rearrange(modulation_cond, "B N Cc -> (B N) Cc") + if modulation_cond is not None + else None, + ) + local_features, global_features = out.last_hidden_state, out.pooler_output + if self.cfg.use_patch_embeddings: + patch_embeddings = out.patch_embeddings + if self.cfg.patch_embeddings_aggr_method == 'concat': + local_features = torch.cat([local_features, patch_embeddings], dim=1) + elif self.cfg.patch_embeddings_aggr_method == 'add': + local_features = local_features + patch_embeddings + else: + raise NotImplementedError + local_features = local_features.permute(0, 2, 1) + local_features = rearrange( + local_features, "(B N) Ct Nt -> B N Ct Nt", B=batch_size + ) + if packed: + local_features = local_features.squeeze(1) + + return local_features + + def detokenize(self, *args, **kwargs): + raise NotImplementedError \ No newline at end of file diff --git a/tgs/models/tokenizers/point.py b/tgs/models/tokenizers/point.py new file mode 100644 index 0000000..99afb6c --- /dev/null +++ b/tgs/models/tokenizers/point.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +import torch.nn as nn +from tgs.utils.base import BaseModule +from tgs.utils.typing import * +import torch + +class PointLearnablePositionalEmbedding(BaseModule): + @dataclass + class Config(BaseModule.Config): + num_pcl: int = 2048 + num_channels: int = 512 + + cfg: Config + + def configure(self) -> None: + super().configure() + self.pcl_embeddings = nn.Embedding( + self.cfg.num_pcl , self.cfg.num_channels + ) + + def forward(self, batch_size: int) -> Float[Tensor, "B Ct Nt"]: + range_ = torch.arange(self.cfg.num_pcl, device=self.device) + embeddings = self.pcl_embeddings(range_).unsqueeze(0).repeat((batch_size,1,1)) + return torch.permute(embeddings, (0,2,1)) + + def detokenize( + self, tokens: Float[Tensor, "B Ct Nt"] + ) -> Float[Tensor, "B 3 Ct Hp Wp"]: + return torch.permute(tokens, (0,2,1)) \ No newline at end of file diff --git a/tgs/models/tokenizers/triplane.py b/tgs/models/tokenizers/triplane.py new file mode 100644 index 0000000..8bd6774 --- /dev/null +++ b/tgs/models/tokenizers/triplane.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +import math + +import torch +import torch.nn as nn +from einops import rearrange, repeat + +from tgs.utils.base import BaseModule +from tgs.utils.typing import * + + +class TriplaneLearnablePositionalEmbedding(BaseModule): + @dataclass + class Config(BaseModule.Config): + plane_size: int = 32 + num_channels: int = 1024 + + cfg: Config + + def configure(self) -> None: + super().configure() + self.embeddings = nn.Parameter( + torch.randn( + (3, self.cfg.num_channels, self.cfg.plane_size, self.cfg.plane_size), + dtype=torch.float32, + ) + * 1 + / math.sqrt(self.cfg.num_channels) + ) + + def forward(self, batch_size: int, cond_embeddings: Float[Tensor, "B Ct"] = None) -> Float[Tensor, "B Ct Nt"]: + embeddings = repeat(self.embeddings, "Np Ct Hp Wp -> B Np Ct Hp Wp", B=batch_size) + if cond_embeddings is not None: + embeddings = embeddings + cond_embeddings + return rearrange( + embeddings, + "B Np Ct Hp Wp -> B Ct (Np Hp Wp)", + ) + + def detokenize( + self, tokens: Float[Tensor, "B Ct Nt"] + ) -> Float[Tensor, "B 3 Ct Hp Wp"]: + batch_size, Ct, Nt = tokens.shape + assert Nt == self.cfg.plane_size**2 * 3 + assert Ct == self.cfg.num_channels + return rearrange( + tokens, + "B Ct (Np Hp Wp) -> B Np Ct Hp Wp", + Np=3, + Hp=self.cfg.plane_size, + Wp=self.cfg.plane_size, + ) diff --git a/tgs/models/transformers.py b/tgs/models/transformers.py new file mode 100644 index 0000000..82ccb14 --- /dev/null +++ b/tgs/models/transformers.py @@ -0,0 +1,908 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch +import torch.nn.functional as F +from torch import nn + +from diffusers.utils.torch_utils import maybe_allow_in_graph +from diffusers.models.activations import get_activation +from diffusers.models.attention_processor import Attention +from diffusers.models.embeddings import CombinedTimestepLabelEmbeddings + +from dataclasses import dataclass +from ..utils.base import BaseModule +from ..utils.typing import * + + +class MemoryEfficientAttentionMixin: + def enable_xformers_memory_efficient_attention( + self, attention_op: Optional[Callable] = None + ): + r""" + Enable memory efficient attention from [xFormers](https://facebookresearch.github.io/xformers/). When this + option is enabled, you should observe lower GPU memory usage and a potential speed up during inference. Speed + up during training is not guaranteed. + + + + ⚠️ When memory efficient attention and sliced attention are both enabled, memory efficient attention takes + precedent. + + + + Parameters: + attention_op (`Callable`, *optional*): + Override the default `None` operator for use as `op` argument to the + [`memory_efficient_attention()`](https://facebookresearch.github.io/xformers/components/ops.html#xformers.ops.memory_efficient_attention) + function of xFormers. + + Examples: + + ```py + >>> import torch + >>> from diffusers import DiffusionPipeline + >>> from xformers.ops import MemoryEfficientAttentionFlashAttentionOp + + >>> pipe = DiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1", torch_dtype=torch.float16) + >>> pipe = pipe.to("cuda") + >>> pipe.enable_xformers_memory_efficient_attention(attention_op=MemoryEfficientAttentionFlashAttentionOp) + >>> # Workaround for not accepting attention shape using VAE for Flash Attention + >>> pipe.vae.enable_xformers_memory_efficient_attention(attention_op=None) + ``` + """ + self.set_use_memory_efficient_attention_xformers(True, attention_op) + + def disable_xformers_memory_efficient_attention(self): + r""" + Disable memory efficient attention from [xFormers](https://facebookresearch.github.io/xformers/). + """ + self.set_use_memory_efficient_attention_xformers(False) + + def set_use_memory_efficient_attention_xformers( + self, valid: bool, attention_op: Optional[Callable] = None + ) -> None: + # Recursively walk through all the children. + # Any children which exposes the set_use_memory_efficient_attention_xformers method + # gets the message + def fn_recursive_set_mem_eff(module: torch.nn.Module): + if hasattr(module, "set_use_memory_efficient_attention_xformers"): + module.set_use_memory_efficient_attention_xformers(valid, attention_op) + + for child in module.children(): + fn_recursive_set_mem_eff(child) + + for module in self.children(): + if isinstance(module, torch.nn.Module): + fn_recursive_set_mem_eff(module) + + +@maybe_allow_in_graph +class GatedSelfAttentionDense(nn.Module): + r""" + A gated self-attention dense layer that combines visual features and object features. + + Parameters: + query_dim (`int`): The number of channels in the query. + context_dim (`int`): The number of channels in the context. + n_heads (`int`): The number of heads to use for attention. + d_head (`int`): The number of channels in each head. + """ + + def __init__(self, query_dim: int, context_dim: int, n_heads: int, d_head: int): + super().__init__() + + # we need a linear projection since we need cat visual feature and obj feature + self.linear = nn.Linear(context_dim, query_dim) + + self.attn = Attention(query_dim=query_dim, heads=n_heads, dim_head=d_head) + self.ff = FeedForward(query_dim, activation_fn="geglu") + + self.norm1 = nn.LayerNorm(query_dim) + self.norm2 = nn.LayerNorm(query_dim) + + self.register_parameter("alpha_attn", nn.Parameter(torch.tensor(0.0))) + self.register_parameter("alpha_dense", nn.Parameter(torch.tensor(0.0))) + + self.enabled = True + + def forward(self, x: torch.Tensor, objs: torch.Tensor) -> torch.Tensor: + if not self.enabled: + return x + + n_visual = x.shape[1] + objs = self.linear(objs) + + x = ( + x + + self.alpha_attn.tanh() + * self.attn(self.norm1(torch.cat([x, objs], dim=1)))[:, :n_visual, :] + ) + x = x + self.alpha_dense.tanh() * self.ff(self.norm2(x)) + + return x + + +@maybe_allow_in_graph +class BasicTransformerBlock(nn.Module, MemoryEfficientAttentionMixin): + r""" + A basic Transformer block. + + Parameters: + dim (`int`): The number of channels in the input and output. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + num_embeds_ada_norm (: + obj: `int`, *optional*): The number of diffusion steps used during training. See `Transformer2DModel`. + attention_bias (: + obj: `bool`, *optional*, defaults to `False`): Configure if the attentions should contain a bias parameter. + only_cross_attention (`bool`, *optional*): + Whether to use only cross-attention layers. In this case two cross attention layers are used. + double_self_attention (`bool`, *optional*): + Whether to use two self-attention layers. In this case no cross attention layers are used. + upcast_attention (`bool`, *optional*): + Whether to upcast the attention computation to float32. This is useful for mixed precision training. + norm_elementwise_affine (`bool`, *optional*, defaults to `True`): + Whether to use learnable elementwise affine parameters for normalization. + norm_type (`str`, *optional*, defaults to `"layer_norm"`): + The normalization layer to use. Can be `"layer_norm"`, `"ada_norm"` or `"ada_norm_zero"`. + final_dropout (`bool` *optional*, defaults to False): + Whether to apply a final dropout after the last feed-forward layer. + attention_type (`str`, *optional*, defaults to `"default"`): + The type of attention to use. Can be `"default"` or `"gated"` or `"gated-text-image"`. + """ + + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + dropout=0.0, + cross_attention_dim: Optional[int] = None, + activation_fn: str = "geglu", + num_embeds_ada_norm: Optional[int] = None, + cond_dim_ada_norm_continuous: Optional[int] = None, + attention_bias: bool = False, + only_cross_attention: bool = False, + double_self_attention: bool = False, + upcast_attention: bool = False, + norm_elementwise_affine: bool = True, + norm_type: str = "layer_norm", + final_dropout: bool = False, + attention_type: str = "default", + ): + super().__init__() + self.only_cross_attention = only_cross_attention + + self.use_ada_layer_norm_zero = ( + num_embeds_ada_norm is not None + ) and norm_type == "ada_norm_zero" + self.use_ada_layer_norm = ( + num_embeds_ada_norm is not None + ) and norm_type == "ada_norm" + self.use_ada_layer_norm_continuous = ( + cond_dim_ada_norm_continuous is not None + ) and norm_type == "ada_norm_continuous" + + assert ( + int(self.use_ada_layer_norm) + + int(self.use_ada_layer_norm_continuous) + + int(self.use_ada_layer_norm_zero) + <= 1 + ) + + if norm_type in ("ada_norm", "ada_norm_zero") and num_embeds_ada_norm is None: + raise ValueError( + f"`norm_type` is set to {norm_type}, but `num_embeds_ada_norm` is not defined. Please make sure to" + f" define `num_embeds_ada_norm` if setting `norm_type` to {norm_type}." + ) + + # Define 3 blocks. Each block has its own normalization layer. + # 1. Self-Attn + if self.use_ada_layer_norm: + self.norm1 = AdaLayerNorm(dim, num_embeds_ada_norm) + elif self.use_ada_layer_norm_continuous: + self.norm1 = AdaLayerNormContinuous(dim, cond_dim_ada_norm_continuous) + elif self.use_ada_layer_norm_zero: + self.norm1 = AdaLayerNormZero(dim, num_embeds_ada_norm) + else: + self.norm1 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine) + self.attn1 = Attention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=cross_attention_dim if only_cross_attention else None, + upcast_attention=upcast_attention, + ) + + # 2. Cross-Attn + if cross_attention_dim is not None or double_self_attention: + # We currently only use AdaLayerNormZero for self attention where there will only be one attention block. + # I.e. the number of returned modulation chunks from AdaLayerZero would not make sense if returned during + # the second cross attention block. + if self.use_ada_layer_norm: + self.norm2 = AdaLayerNorm(dim, num_embeds_ada_norm) + elif self.use_ada_layer_norm_continuous: + self.norm2 = AdaLayerNormContinuous(dim, cond_dim_ada_norm_continuous) + else: + self.norm2 = nn.LayerNorm( + dim, elementwise_affine=norm_elementwise_affine + ) + + self.attn2 = Attention( + query_dim=dim, + cross_attention_dim=cross_attention_dim + if not double_self_attention + else None, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + upcast_attention=upcast_attention, + ) # is self-attn if encoder_hidden_states is none + else: + self.norm2 = None + self.attn2 = None + + # 3. Feed-forward + if self.use_ada_layer_norm_continuous: + self.norm3 = AdaLayerNormContinuous(dim, cond_dim_ada_norm_continuous) + else: + self.norm3 = nn.LayerNorm(dim, elementwise_affine=norm_elementwise_affine) + self.ff = FeedForward( + dim, + dropout=dropout, + activation_fn=activation_fn, + final_dropout=final_dropout, + ) + + # 4. Fuser + if attention_type == "gated" or attention_type == "gated-text-image": + self.fuser = GatedSelfAttentionDense( + dim, cross_attention_dim, num_attention_heads, attention_head_dim + ) + + # let chunk size default to None + self._chunk_size = None + self._chunk_dim = 0 + + def set_chunk_feed_forward(self, chunk_size: Optional[int], dim: int): + # Sets chunk feed-forward + self._chunk_size = chunk_size + self._chunk_dim = dim + + def forward( + self, + hidden_states: torch.FloatTensor, + attention_mask: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + timestep: Optional[torch.LongTensor] = None, + modulation_cond: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Dict[str, Any] = None, + class_labels: Optional[torch.LongTensor] = None, + ) -> torch.FloatTensor: + # Notice that normalization is always applied before the real computation in the following blocks. + # 0. Self-Attention + if self.use_ada_layer_norm: + norm_hidden_states = self.norm1(hidden_states, timestep) + elif self.use_ada_layer_norm_continuous: + norm_hidden_states = self.norm1(hidden_states, modulation_cond) + elif self.use_ada_layer_norm_zero: + norm_hidden_states, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.norm1( + hidden_states, timestep, class_labels, hidden_dtype=hidden_states.dtype + ) + else: + norm_hidden_states = self.norm1(hidden_states) + + # 1. Retrieve lora scale. + lora_scale = ( + cross_attention_kwargs.get("scale", 1.0) + if cross_attention_kwargs is not None + else 1.0 + ) + + # 2. Prepare GLIGEN inputs + cross_attention_kwargs = ( + cross_attention_kwargs.copy() if cross_attention_kwargs is not None else {} + ) + gligen_kwargs = cross_attention_kwargs.pop("gligen", None) + + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states + if self.only_cross_attention + else None, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + if self.use_ada_layer_norm_zero: + attn_output = gate_msa.unsqueeze(1) * attn_output + hidden_states = attn_output + hidden_states + + # 2.5 GLIGEN Control + if gligen_kwargs is not None: + hidden_states = self.fuser(hidden_states, gligen_kwargs["objs"]) + # 2.5 ends + + # 3. Cross-Attention + if self.attn2 is not None: + if self.use_ada_layer_norm: + norm_hidden_states = self.norm2(hidden_states, timestep) + elif self.use_ada_layer_norm_continuous: + norm_hidden_states = self.norm2(hidden_states, modulation_cond) + else: + norm_hidden_states = self.norm2(hidden_states) + + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=encoder_attention_mask, + **cross_attention_kwargs, + ) + hidden_states = attn_output + hidden_states + + # 4. Feed-forward + if self.use_ada_layer_norm_continuous: + norm_hidden_states = self.norm3(hidden_states, modulation_cond) + else: + norm_hidden_states = self.norm3(hidden_states) + + if self.use_ada_layer_norm_zero: + norm_hidden_states = ( + norm_hidden_states * (1 + scale_mlp[:, None]) + shift_mlp[:, None] + ) + + if self._chunk_size is not None: + # "feed_forward_chunk_size" can be used to save memory + if norm_hidden_states.shape[self._chunk_dim] % self._chunk_size != 0: + raise ValueError( + f"`hidden_states` dimension to be chunked: {norm_hidden_states.shape[self._chunk_dim]} has to be divisible by chunk size: {self._chunk_size}. Make sure to set an appropriate `chunk_size` when calling `unet.enable_forward_chunking`." + ) + + num_chunks = norm_hidden_states.shape[self._chunk_dim] // self._chunk_size + ff_output = torch.cat( + [ + self.ff(hid_slice, scale=lora_scale) + for hid_slice in norm_hidden_states.chunk( + num_chunks, dim=self._chunk_dim + ) + ], + dim=self._chunk_dim, + ) + else: + ff_output = self.ff(norm_hidden_states, scale=lora_scale) + + if self.use_ada_layer_norm_zero: + ff_output = gate_mlp.unsqueeze(1) * ff_output + + hidden_states = ff_output + hidden_states + + return hidden_states + + +class FeedForward(nn.Module): + r""" + A feed-forward layer. + + Parameters: + dim (`int`): The number of channels in the input. + dim_out (`int`, *optional*): The number of channels in the output. If not given, defaults to `dim`. + mult (`int`, *optional*, defaults to 4): The multiplier to use for the hidden dimension. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to be used in feed-forward. + final_dropout (`bool` *optional*, defaults to False): Apply a final dropout. + """ + + def __init__( + self, + dim: int, + dim_out: Optional[int] = None, + mult: int = 4, + dropout: float = 0.0, + activation_fn: str = "geglu", + final_dropout: bool = False, + ): + super().__init__() + inner_dim = int(dim * mult) + dim_out = dim_out if dim_out is not None else dim + linear_cls = nn.Linear + + if activation_fn == "gelu": + act_fn = GELU(dim, inner_dim) + if activation_fn == "gelu-approximate": + act_fn = GELU(dim, inner_dim, approximate="tanh") + elif activation_fn == "geglu": + act_fn = GEGLU(dim, inner_dim) + elif activation_fn == "geglu-approximate": + act_fn = ApproximateGELU(dim, inner_dim) + + self.net = nn.ModuleList([]) + # project in + self.net.append(act_fn) + # project dropout + self.net.append(nn.Dropout(dropout)) + # project out + self.net.append(linear_cls(inner_dim, dim_out)) + # FF as used in Vision Transformer, MLP-Mixer, etc. have a final dropout + if final_dropout: + self.net.append(nn.Dropout(dropout)) + + def forward(self, hidden_states: torch.Tensor, scale: float = 1.0) -> torch.Tensor: + for module in self.net: + hidden_states = module(hidden_states) + return hidden_states + + +class GELU(nn.Module): + r""" + GELU activation function with tanh approximation support with `approximate="tanh"`. + + Parameters: + dim_in (`int`): The number of channels in the input. + dim_out (`int`): The number of channels in the output. + approximate (`str`, *optional*, defaults to `"none"`): If `"tanh"`, use tanh approximation. + """ + + def __init__(self, dim_in: int, dim_out: int, approximate: str = "none"): + super().__init__() + self.proj = nn.Linear(dim_in, dim_out) + self.approximate = approximate + + def gelu(self, gate: torch.Tensor) -> torch.Tensor: + if gate.device.type != "mps": + return F.gelu(gate, approximate=self.approximate) + # mps: gelu is not implemented for float16 + return F.gelu(gate.to(dtype=torch.float32), approximate=self.approximate).to( + dtype=gate.dtype + ) + + def forward(self, hidden_states): + hidden_states = self.proj(hidden_states) + hidden_states = self.gelu(hidden_states) + return hidden_states + + +class GEGLU(nn.Module): + r""" + A variant of the gated linear unit activation function from https://arxiv.org/abs/2002.05202. + + Parameters: + dim_in (`int`): The number of channels in the input. + dim_out (`int`): The number of channels in the output. + """ + + def __init__(self, dim_in: int, dim_out: int): + super().__init__() + linear_cls = nn.Linear + + self.proj = linear_cls(dim_in, dim_out * 2) + + def gelu(self, gate: torch.Tensor) -> torch.Tensor: + if gate.device.type != "mps": + return F.gelu(gate) + # mps: gelu is not implemented for float16 + return F.gelu(gate.to(dtype=torch.float32)).to(dtype=gate.dtype) + + def forward(self, hidden_states, scale: float = 1.0): + args = () + hidden_states, gate = self.proj(hidden_states, *args).chunk(2, dim=-1) + return hidden_states * self.gelu(gate) + + +class ApproximateGELU(nn.Module): + r""" + The approximate form of Gaussian Error Linear Unit (GELU). For more details, see section 2: + https://arxiv.org/abs/1606.08415. + + Parameters: + dim_in (`int`): The number of channels in the input. + dim_out (`int`): The number of channels in the output. + """ + + def __init__(self, dim_in: int, dim_out: int): + super().__init__() + self.proj = nn.Linear(dim_in, dim_out) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.proj(x) + return x * torch.sigmoid(1.702 * x) + + +class AdaLayerNorm(nn.Module): + r""" + Norm layer modified to incorporate timestep embeddings. + + Parameters: + embedding_dim (`int`): The size of each embedding vector. + num_embeddings (`int`): The size of the dictionary of embeddings. + """ + + def __init__(self, embedding_dim: int, num_embeddings: int): + super().__init__() + self.emb = nn.Embedding(num_embeddings, embedding_dim) + self.silu = nn.SiLU() + self.linear = nn.Linear(embedding_dim, embedding_dim * 2) + self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False) + + def forward(self, x: torch.Tensor, timestep: torch.Tensor) -> torch.Tensor: + emb = self.linear(self.silu(self.emb(timestep))) + scale, shift = torch.chunk(emb, 2, dim=1) + x = self.norm(x) * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + return x + + +class AdaLayerNormContinuous(nn.Module): + r""" + Norm layer modified to incorporate arbitrary continuous embeddings. + + Parameters: + embedding_dim (`int`): The size of each embedding vector. + """ + + def __init__(self, embedding_dim: int, condition_dim: int): + super().__init__() + self.silu = nn.SiLU() + self.linear1 = nn.Linear(condition_dim, condition_dim) + self.linear2 = nn.Linear(condition_dim, embedding_dim * 2) + self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False) + + def forward(self, x: torch.Tensor, condition: torch.Tensor) -> torch.Tensor: + emb = self.linear2(self.silu(self.linear1(condition))) + scale, shift = torch.chunk(emb, 2, dim=1) + x = self.norm(x) * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + return x + + +class Modulation(nn.Module): + def __init__(self, embedding_dim: int, condition_dim: int, zero_init: bool = False, single_layer: bool = False): + super().__init__() + self.silu = nn.SiLU() + if single_layer: + self.linear1 = nn.Identity() + else: + self.linear1 = nn.Linear(condition_dim, condition_dim) + + self.linear2 = nn.Linear(condition_dim, embedding_dim * 2) + + # Only zero init the last linear layer + if zero_init: + nn.init.zeros_(self.linear2.weight) + nn.init.zeros_(self.linear2.bias) + + def forward(self, x: torch.Tensor, condition: torch.Tensor) -> torch.Tensor: + emb = self.linear2(self.silu(self.linear1(condition))) + scale, shift = torch.chunk(emb, 2, dim=1) + x = x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + return x + + +class AdaLayerNormZero(nn.Module): + r""" + Norm layer adaptive layer norm zero (adaLN-Zero). + + Parameters: + embedding_dim (`int`): The size of each embedding vector. + num_embeddings (`int`): The size of the dictionary of embeddings. + """ + + def __init__(self, embedding_dim: int, num_embeddings: int): + super().__init__() + + self.emb = CombinedTimestepLabelEmbeddings(num_embeddings, embedding_dim) + + self.silu = nn.SiLU() + self.linear = nn.Linear(embedding_dim, 6 * embedding_dim, bias=True) + self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False, eps=1e-6) + + def forward( + self, + x: torch.Tensor, + timestep: torch.Tensor, + class_labels: torch.LongTensor, + hidden_dtype: Optional[torch.dtype] = None, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + emb = self.linear( + self.silu(self.emb(timestep, class_labels, hidden_dtype=hidden_dtype)) + ) + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = emb.chunk( + 6, dim=1 + ) + x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None] + return x, gate_msa, shift_mlp, scale_mlp, gate_mlp + + +class AdaGroupNorm(nn.Module): + r""" + GroupNorm layer modified to incorporate timestep embeddings. + + Parameters: + embedding_dim (`int`): The size of each embedding vector. + num_embeddings (`int`): The size of the dictionary of embeddings. + num_groups (`int`): The number of groups to separate the channels into. + act_fn (`str`, *optional*, defaults to `None`): The activation function to use. + eps (`float`, *optional*, defaults to `1e-5`): The epsilon value to use for numerical stability. + """ + + def __init__( + self, + embedding_dim: int, + out_dim: int, + num_groups: int, + act_fn: Optional[str] = None, + eps: float = 1e-5, + ): + super().__init__() + self.num_groups = num_groups + self.eps = eps + + if act_fn is None: + self.act = None + else: + self.act = get_activation(act_fn) + + self.linear = nn.Linear(embedding_dim, out_dim * 2) + + def forward(self, x: torch.Tensor, emb: torch.Tensor) -> torch.Tensor: + if self.act: + emb = self.act(emb) + emb = self.linear(emb) + emb = emb[:, :, None, None] + scale, shift = emb.chunk(2, dim=1) + + x = F.group_norm(x, self.num_groups, eps=self.eps) + x = x * (1 + scale) + shift + return x + +class Transformer1D(BaseModule, MemoryEfficientAttentionMixin): + """ + A 1D Transformer model for sequence data. + + Parameters: + num_attention_heads (`int`, *optional*, defaults to 16): The number of heads to use for multi-head attention. + attention_head_dim (`int`, *optional*, defaults to 88): The number of channels in each head. + in_channels (`int`, *optional*): + The number of channels in the input and output (specify if the input is **continuous**). + num_layers (`int`, *optional*, defaults to 1): The number of layers of Transformer blocks to use. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The number of `encoder_hidden_states` dimensions to use. + activation_fn (`str`, *optional*, defaults to `"geglu"`): Activation function to use in feed-forward. + num_embeds_ada_norm ( `int`, *optional*): + The number of diffusion steps used during training. Pass if at least one of the norm_layers is + `AdaLayerNorm`. This is fixed during training since it is used to learn a number of embeddings that are + added to the hidden states. + + During inference, you can denoise for up to but not more steps than `num_embeds_ada_norm`. + attention_bias (`bool`, *optional*): + Configure if the `TransformerBlocks` attention should contain a bias parameter. + """ + + @dataclass + class Config(BaseModule.Config): + num_attention_heads: int = 16 + attention_head_dim: int = 88 + in_channels: Optional[int] = None + out_channels: Optional[int] = None + num_layers: int = 1 + dropout: float = 0.0 + norm_num_groups: int = 32 + cross_attention_dim: Optional[int] = None + attention_bias: bool = False + activation_fn: str = "geglu" + num_embeds_ada_norm: Optional[int] = None + cond_dim_ada_norm_continuous: Optional[int] = None + only_cross_attention: bool = False + double_self_attention: bool = False + upcast_attention: bool = False + norm_type: str = "layer_norm" + norm_elementwise_affine: bool = True + attention_type: str = "default" + enable_memory_efficient_attention: bool = False + gradient_checkpointing: bool = False + + cfg: Config + + def configure(self) -> None: + super().configure() + + self.num_attention_heads = self.cfg.num_attention_heads + self.attention_head_dim = self.cfg.attention_head_dim + inner_dim = self.num_attention_heads * self.attention_head_dim + + linear_cls = nn.Linear + + if self.cfg.norm_type == "layer_norm" and ( + self.cfg.num_embeds_ada_norm is not None + or self.cfg.cond_dim_ada_norm_continuous is not None + ): + raise ValueError("Incorrect norm_type.") + + # 2. Define input layers + self.in_channels = self.cfg.in_channels + + self.norm = torch.nn.GroupNorm( + num_groups=self.cfg.norm_num_groups, + num_channels=self.cfg.in_channels, + eps=1e-6, + affine=True, + ) + self.proj_in = linear_cls(self.cfg.in_channels, inner_dim) + + # 3. Define transformers blocks + self.transformer_blocks = nn.ModuleList( + [ + BasicTransformerBlock( + inner_dim, + self.num_attention_heads, + self.attention_head_dim, + dropout=self.cfg.dropout, + cross_attention_dim=self.cfg.cross_attention_dim, + activation_fn=self.cfg.activation_fn, + num_embeds_ada_norm=self.cfg.num_embeds_ada_norm, + cond_dim_ada_norm_continuous=self.cfg.cond_dim_ada_norm_continuous, + attention_bias=self.cfg.attention_bias, + only_cross_attention=self.cfg.only_cross_attention, + double_self_attention=self.cfg.double_self_attention, + upcast_attention=self.cfg.upcast_attention, + norm_type=self.cfg.norm_type, + norm_elementwise_affine=self.cfg.norm_elementwise_affine, + attention_type=self.cfg.attention_type, + ) + for d in range(self.cfg.num_layers) + ] + ) + + # 4. Define output layers + self.out_channels = ( + self.cfg.in_channels + if self.cfg.out_channels is None + else self.cfg.out_channels + ) + + self.proj_out = linear_cls(inner_dim, self.cfg.in_channels) + + self.gradient_checkpointing = self.cfg.gradient_checkpointing + + self.set_use_memory_efficient_attention_xformers( + self.cfg.enable_memory_efficient_attention + ) + + def forward( + self, + hidden_states: torch.Tensor, + encoder_hidden_states: Optional[torch.Tensor] = None, + timestep: Optional[torch.LongTensor] = None, + modulation_cond: Optional[torch.FloatTensor] = None, + class_labels: Optional[torch.LongTensor] = None, + cross_attention_kwargs: Dict[str, Any] = None, + attention_mask: Optional[torch.Tensor] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + ): + """ + The [`Transformer1DModel`] forward method. + + Args: + hidden_states (`torch.LongTensor` of shape `(batch size, num latent pixels)` if discrete, `torch.FloatTensor` of shape `(batch size, channel, height, width)` if continuous): + Input `hidden_states`. + encoder_hidden_states ( `torch.FloatTensor` of shape `(batch size, sequence len, embed dims)`, *optional*): + Conditional embeddings for cross attention layer. If not given, cross-attention defaults to + self-attention. + timestep ( `torch.LongTensor`, *optional*): + Used to indicate denoising step. Optional timestep to be applied as an embedding in `AdaLayerNorm`. + class_labels ( `torch.LongTensor` of shape `(batch size, num classes)`, *optional*): + Used to indicate class labels conditioning. Optional class labels to be applied as an embedding in + `AdaLayerZeroNorm`. + cross_attention_kwargs ( `Dict[str, Any]`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under + `self.processor` in + [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + attention_mask ( `torch.Tensor`, *optional*): + An attention mask of shape `(batch, key_tokens)` is applied to `encoder_hidden_states`. If `1` the mask + is kept, otherwise if `0` it is discarded. Mask will be converted into a bias, which adds large + negative values to the attention scores corresponding to "discard" tokens. + encoder_attention_mask ( `torch.Tensor`, *optional*): + Cross-attention mask applied to `encoder_hidden_states`. Two formats supported: + + * Mask `(batch, sequence_length)` True = keep, False = discard. + * Bias `(batch, 1, sequence_length)` 0 = keep, -10000 = discard. + + If `ndim == 2`: will be interpreted as a mask, then converted into a bias consistent with the format + above. This bias will be added to the cross-attention scores. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~models.unet_2d_condition.UNet2DConditionOutput`] instead of a plain + tuple. + + Returns: + If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a + `tuple` where the first element is the sample tensor. + """ + # ensure attention_mask is a bias, and give it a singleton query_tokens dimension. + # we may have done this conversion already, e.g. if we came here via UNet2DConditionModel#forward. + # we can tell by counting dims; if ndim == 2: it's a mask rather than a bias. + # expects mask of shape: + # [batch, key_tokens] + # adds singleton query_tokens dimension: + # [batch, 1, key_tokens] + # this helps to broadcast it as a bias over attention scores, which will be in one of the following shapes: + # [batch, heads, query_tokens, key_tokens] (e.g. torch sdp attn) + # [batch * heads, query_tokens, key_tokens] (e.g. xformers or classic attn) + if attention_mask is not None and attention_mask.ndim == 2: + # assume that mask is expressed as: + # (1 = keep, 0 = discard) + # convert mask into a bias that can be added to attention scores: + # (keep = +0, discard = -10000.0) + attention_mask = (1 - attention_mask.to(hidden_states.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # convert encoder_attention_mask to a bias the same way we do for attention_mask + if encoder_attention_mask is not None and encoder_attention_mask.ndim == 2: + encoder_attention_mask = ( + 1 - encoder_attention_mask.to(hidden_states.dtype) + ) * -10000.0 + encoder_attention_mask = encoder_attention_mask.unsqueeze(1) + + # 1. Input + batch, _, seq_len = hidden_states.shape + residual = hidden_states + + hidden_states = self.norm(hidden_states) + inner_dim = hidden_states.shape[1] + hidden_states = hidden_states.permute(0, 2, 1).reshape( + batch, seq_len, inner_dim + ) + hidden_states = self.proj_in(hidden_states) + + # 2. Blocks + for block in self.transformer_blocks: + if self.training and self.gradient_checkpointing: + hidden_states = torch.utils.checkpoint.checkpoint( + block, + hidden_states, + attention_mask, + encoder_hidden_states, + encoder_attention_mask, + timestep, + modulation_cond, + cross_attention_kwargs, + class_labels, + use_reentrant=False, + ) + else: + hidden_states = block( + hidden_states, + attention_mask=attention_mask, + encoder_hidden_states=encoder_hidden_states, + encoder_attention_mask=encoder_attention_mask, + timestep=timestep, + modulation_cond=modulation_cond, + cross_attention_kwargs=cross_attention_kwargs, + class_labels=class_labels, + ) + + # 3. Output + hidden_states = self.proj_out(hidden_states) + hidden_states = ( + hidden_states.reshape(batch, seq_len, inner_dim) + .permute(0, 2, 1) + .contiguous() + ) + + output = hidden_states + residual + + return output diff --git a/tgs/utils/__init__.py b/tgs/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgs/utils/base.py b/tgs/utils/base.py new file mode 100644 index 0000000..e98f63c --- /dev/null +++ b/tgs/utils/base.py @@ -0,0 +1,129 @@ +from dataclasses import dataclass + +import torch.nn as nn + +from .config import parse_structured +from .misc import get_device, load_module_weights +from .typing import * + + +class Configurable: + @dataclass + class Config: + pass + + def __init__(self, cfg: Optional[dict] = None) -> None: + super().__init__() + self.cfg = parse_structured(self.Config, cfg) + + +class Updateable: + def do_update_step( + self, epoch: int, global_step: int, on_load_weights: bool = False + ): + for attr in self.__dir__(): + if attr.startswith("_"): + continue + try: + module = getattr(self, attr) + except: + continue # ignore attributes like property, which can't be retrived using getattr? + if isinstance(module, Updateable): + module.do_update_step( + epoch, global_step, on_load_weights=on_load_weights + ) + self.update_step(epoch, global_step, on_load_weights=on_load_weights) + + def do_update_step_end(self, epoch: int, global_step: int): + for attr in self.__dir__(): + if attr.startswith("_"): + continue + try: + module = getattr(self, attr) + except: + continue # ignore attributes like property, which can't be retrived using getattr? + if isinstance(module, Updateable): + module.do_update_step_end(epoch, global_step) + self.update_step_end(epoch, global_step) + + def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False): + # override this method to implement custom update logic + # if on_load_weights is True, you should be careful doing things related to model evaluations, + # as the models and tensors are not guarenteed to be on the same device + pass + + def update_step_end(self, epoch: int, global_step: int): + pass + + +def update_if_possible(module: Any, epoch: int, global_step: int) -> None: + if isinstance(module, Updateable): + module.do_update_step(epoch, global_step) + + +def update_end_if_possible(module: Any, epoch: int, global_step: int) -> None: + if isinstance(module, Updateable): + module.do_update_step_end(epoch, global_step) + + +class BaseObject(Updateable): + @dataclass + class Config: + pass + + cfg: Config # add this to every subclass of BaseObject to enable static type checking + + def __init__( + self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs + ) -> None: + super().__init__() + self.cfg = parse_structured(self.Config, cfg) + self.device = get_device() + self.configure(*args, **kwargs) + + def configure(self, *args, **kwargs) -> None: + pass + + +class BaseModule(nn.Module, Updateable): + @dataclass + class Config: + weights: Optional[str] = None + freeze: Optional[bool] = False + + cfg: Config # add this to every subclass of BaseModule to enable static type checking + + def __init__( + self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs + ) -> None: + super().__init__() + self.cfg = parse_structured(self.Config, cfg) + self.device = get_device() + self._non_modules = {} + self.configure(*args, **kwargs) + if self.cfg.weights is not None: + # format: path/to/weights:module_name + weights_path, module_name = self.cfg.weights.split(":") + state_dict = load_module_weights( + weights_path, module_name=module_name, map_location="cpu" + ) + self.load_state_dict(state_dict, strict=False) + # self.do_update_step( + # epoch, global_step, on_load_weights=True + # ) # restore states + + if self.cfg.freeze: + for params in self.parameters(): + params.requires_grad = False + + def configure(self, *args, **kwargs) -> None: + pass + + def register_non_module(self, name: str, module: nn.Module) -> None: + # non-modules won't be treated as model parameters + if name in self._non_modules: + raise ValueError(f"Non-module {name} already exists!") + self._non_modules[name] = module + + def non_module(self, name: str): + return self._non_modules.get(name, None) diff --git a/tgs/utils/config.py b/tgs/utils/config.py new file mode 100644 index 0000000..abd8520 --- /dev/null +++ b/tgs/utils/config.py @@ -0,0 +1,74 @@ +import os +from dataclasses import dataclass, field + +from omegaconf import OmegaConf + +from .typing import * + +# ============ Register OmegaConf Recolvers ============= # +OmegaConf.register_new_resolver( + "calc_exp_lr_decay_rate", lambda factor, n: factor ** (1.0 / n), replace=True +) +OmegaConf.register_new_resolver("add", lambda a, b: a + b, replace=True) +OmegaConf.register_new_resolver("sub", lambda a, b: a - b, replace=True) +OmegaConf.register_new_resolver("mul", lambda a, b: a * b, replace=True) +OmegaConf.register_new_resolver("div", lambda a, b: a / b, replace=True) +OmegaConf.register_new_resolver("idiv", lambda a, b: a // b, replace=True) +OmegaConf.register_new_resolver("basename", lambda p: os.path.basename(p), replace=True) +OmegaConf.register_new_resolver("rmspace", lambda s, sub: s.replace(" ", sub), replace=True) +OmegaConf.register_new_resolver("tuple2", lambda s: [float(s), float(s)], replace=True) +OmegaConf.register_new_resolver("gt0", lambda s: s > 0, replace=True) +OmegaConf.register_new_resolver("not", lambda s: not s, replace=True) +OmegaConf.register_new_resolver("shsdim", lambda sh_degree: (sh_degree + 1) ** 2 * 3, replace=True) +# ======================================================= # + +# ============== Automatic Name Resolvers =============== # +def get_naming_convention(cfg): + # TODO + name = f"tgs_{cfg.system.backbone.num_layers}" + return name + +# ======================================================= # + +@dataclass +class ExperimentConfig: + n_gpus: int = 1 + data: dict = field(default_factory=dict) + system: dict = field(default_factory=dict) + +def load_config( + *yamls: str, cli_args: list = [], from_string=False, makedirs=True, **kwargs +) -> Any: + if from_string: + parse_func = OmegaConf.create + else: + parse_func = OmegaConf.load + yaml_confs = [] + for y in yamls: + conf = parse_func(y) + extends = conf.pop("extends", None) + if extends: + assert os.path.exists(extends), f"File {extends} does not exist." + yaml_confs.append(OmegaConf.load(extends)) + yaml_confs.append(conf) + cli_conf = OmegaConf.from_cli(cli_args) + cfg = OmegaConf.merge(*yaml_confs, cli_conf, kwargs) + OmegaConf.resolve(cfg) + assert isinstance(cfg, DictConfig) + scfg: ExperimentConfig = parse_structured(ExperimentConfig, cfg) + + return scfg + + +def config_to_primitive(config, resolve: bool = True) -> Any: + return OmegaConf.to_container(config, resolve=resolve) + + +def dump_config(path: str, config) -> None: + with open(path, "w") as fp: + OmegaConf.save(config=config, f=fp) + + +def parse_structured(fields: Any, cfg: Optional[Union[dict, DictConfig]] = None) -> Any: + scfg = OmegaConf.structured(fields(**cfg)) + return scfg diff --git a/tgs/utils/misc.py b/tgs/utils/misc.py new file mode 100644 index 0000000..7daf634 --- /dev/null +++ b/tgs/utils/misc.py @@ -0,0 +1,88 @@ +import os +import re + +import torch +from packaging import version + +from .typing import * + + +def parse_version(ver: str): + return version.parse(ver) + + +def get_rank(): + # SLURM_PROCID can be set even if SLURM is not managing the multiprocessing, + # therefore LOCAL_RANK needs to be checked first + rank_keys = ("RANK", "LOCAL_RANK", "SLURM_PROCID", "JSM_NAMESPACE_RANK") + for key in rank_keys: + rank = os.environ.get(key) + if rank is not None: + return int(rank) + return 0 + + +def get_device(): + return torch.device(f"cuda:{get_rank()}") + + +def load_module_weights( + path, module_name=None, ignore_modules=None, map_location=None +) -> Tuple[dict, int, int]: + if module_name is not None and ignore_modules is not None: + raise ValueError("module_name and ignore_modules cannot be both set") + if map_location is None: + map_location = get_device() + + ckpt = torch.load(path, map_location=map_location) + state_dict = ckpt["state_dict"] + state_dict_to_load = state_dict + + if ignore_modules is not None: + state_dict_to_load = {} + for k, v in state_dict.items(): + ignore = any( + [k.startswith(ignore_module + ".") for ignore_module in ignore_modules] + ) + if ignore: + continue + state_dict_to_load[k] = v + + if module_name is not None: + state_dict_to_load = {} + for k, v in state_dict.items(): + m = re.match(rf"^{module_name}\.(.*)$", k) + if m is None: + continue + state_dict_to_load[m.group(1)] = v + + return state_dict_to_load + +# convert a function into recursive style to handle nested dict/list/tuple variables +def make_recursive_func(func): + def wrapper(vars, *args, **kwargs): + if isinstance(vars, list): + return [wrapper(x, *args, **kwargs) for x in vars] + elif isinstance(vars, tuple): + return tuple([wrapper(x, *args, **kwargs) for x in vars]) + elif isinstance(vars, dict): + return {k: wrapper(v, *args, **kwargs) for k, v in vars.items()} + else: + return func(vars, *args, **kwargs) + + return wrapper + +@make_recursive_func +def todevice(vars, device="cuda"): + if isinstance(vars, torch.Tensor): + return vars.to(device) + elif isinstance(vars, str): + return vars + elif isinstance(vars, bool): + return vars + elif isinstance(vars, float): + return vars + elif isinstance(vars, int): + return vars + else: + raise NotImplementedError("invalid input type {} for tensor2numpy".format(type(vars))) diff --git a/tgs/utils/ops.py b/tgs/utils/ops.py new file mode 100644 index 0000000..29f0fd4 --- /dev/null +++ b/tgs/utils/ops.py @@ -0,0 +1,279 @@ +import math + +import numpy as np +import torch +import torch.nn.functional as F +from torch.autograd import Function +from torch.cuda.amp import custom_bwd, custom_fwd +from pytorch3d import io +from pytorch3d.renderer import ( + PointsRasterizationSettings, + PointsRasterizer) +from pytorch3d.structures import Pointclouds +from pytorch3d.utils.camera_conversions import cameras_from_opencv_projection +import cv2 + +from .typing import * + +ValidScale = Union[Tuple[float, float], Num[Tensor, "2 D"]] + +def scale_tensor( + dat: Num[Tensor, "... D"], inp_scale: ValidScale, tgt_scale: ValidScale +): + if inp_scale is None: + inp_scale = (0, 1) + if tgt_scale is None: + tgt_scale = (0, 1) + if isinstance(tgt_scale, Tensor): + assert dat.shape[-1] == tgt_scale.shape[-1] + dat = (dat - inp_scale[0]) / (inp_scale[1] - inp_scale[0]) + dat = dat * (tgt_scale[1] - tgt_scale[0]) + tgt_scale[0] + return dat + + +class _TruncExp(Function): # pylint: disable=abstract-method + # Implementation from torch-ngp: + # https://github.com/ashawkey/torch-ngp/blob/93b08a0d4ec1cc6e69d85df7f0acdfb99603b628/activation.py + @staticmethod + @custom_fwd(cast_inputs=torch.float32) + def forward(ctx, x): # pylint: disable=arguments-differ + ctx.save_for_backward(x) + return torch.exp(x) + + @staticmethod + @custom_bwd + def backward(ctx, g): # pylint: disable=arguments-differ + x = ctx.saved_tensors[0] + return g * torch.exp(torch.clamp(x, max=15)) + + +trunc_exp = _TruncExp.apply + + +def get_activation(name) -> Callable: + if name is None: + return lambda x: x + name = name.lower() + if name == "none": + return lambda x: x + elif name == "lin2srgb": + return lambda x: torch.where( + x > 0.0031308, + torch.pow(torch.clamp(x, min=0.0031308), 1.0 / 2.4) * 1.055 - 0.055, + 12.92 * x, + ).clamp(0.0, 1.0) + elif name == "exp": + return lambda x: torch.exp(x) + elif name == "shifted_exp": + return lambda x: torch.exp(x - 1.0) + elif name == "trunc_exp": + return trunc_exp + elif name == "shifted_trunc_exp": + return lambda x: trunc_exp(x - 1.0) + elif name == "sigmoid": + return lambda x: torch.sigmoid(x) + elif name == "tanh": + return lambda x: torch.tanh(x) + elif name == "shifted_softplus": + return lambda x: F.softplus(x - 1.0) + elif name == "scale_-11_01": + return lambda x: x * 0.5 + 0.5 + else: + try: + return getattr(F, name) + except AttributeError: + raise ValueError(f"Unknown activation function: {name}") + +def get_ray_directions( + H: int, + W: int, + focal: Union[float, Tuple[float, float]], + principal: Optional[Tuple[float, float]] = None, + use_pixel_centers: bool = True, +) -> Float[Tensor, "H W 3"]: + """ + Get ray directions for all pixels in camera coordinate. + Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/ + ray-tracing-generating-camera-rays/standard-coordinate-systems + + Inputs: + H, W, focal, principal, use_pixel_centers: image height, width, focal length, principal point and whether use pixel centers + Outputs: + directions: (H, W, 3), the direction of the rays in camera coordinate + """ + pixel_center = 0.5 if use_pixel_centers else 0 + + if isinstance(focal, float): + fx, fy = focal, focal + cx, cy = W / 2, H / 2 + else: + fx, fy = focal + assert principal is not None + cx, cy = principal + + i, j = torch.meshgrid( + torch.arange(W, dtype=torch.float32) + pixel_center, + torch.arange(H, dtype=torch.float32) + pixel_center, + indexing="xy", + ) + + directions: Float[Tensor, "H W 3"] = torch.stack( + [(i - cx) / fx, -(j - cy) / fy, -torch.ones_like(i)], -1 + ) + + return directions + + +def get_rays( + directions: Float[Tensor, "... 3"], + c2w: Float[Tensor, "... 4 4"], + keepdim=False, + noise_scale=0.0, +) -> Tuple[Float[Tensor, "... 3"], Float[Tensor, "... 3"]]: + # Rotate ray directions from camera coordinate to the world coordinate + assert directions.shape[-1] == 3 + + if directions.ndim == 2: # (N_rays, 3) + if c2w.ndim == 2: # (4, 4) + c2w = c2w[None, :, :] + assert c2w.ndim == 3 # (N_rays, 4, 4) or (1, 4, 4) + rays_d = (directions[:, None, :] * c2w[:, :3, :3]).sum(-1) # (N_rays, 3) + rays_o = c2w[:, :3, 3].expand(rays_d.shape) + elif directions.ndim == 3: # (H, W, 3) + assert c2w.ndim in [2, 3] + if c2w.ndim == 2: # (4, 4) + rays_d = (directions[:, :, None, :] * c2w[None, None, :3, :3]).sum( + -1 + ) # (H, W, 3) + rays_o = c2w[None, None, :3, 3].expand(rays_d.shape) + elif c2w.ndim == 3: # (B, 4, 4) + rays_d = (directions[None, :, :, None, :] * c2w[:, None, None, :3, :3]).sum( + -1 + ) # (B, H, W, 3) + rays_o = c2w[:, None, None, :3, 3].expand(rays_d.shape) + elif directions.ndim == 4: # (B, H, W, 3) + assert c2w.ndim == 3 # (B, 4, 4) + rays_d = (directions[:, :, :, None, :] * c2w[:, None, None, :3, :3]).sum( + -1 + ) # (B, H, W, 3) + rays_o = c2w[:, None, None, :3, 3].expand(rays_d.shape) + + # add camera noise to avoid grid-like artifect + # https://github.com/ashawkey/stable-dreamfusion/blob/49c3d4fa01d68a4f027755acf94e1ff6020458cc/nerf/utils.py#L373 + if noise_scale > 0: + rays_o = rays_o + torch.randn(3, device=rays_o.device) * noise_scale + rays_d = rays_d + torch.randn(3, device=rays_d.device) * noise_scale + + rays_d = F.normalize(rays_d, dim=-1) + if not keepdim: + rays_o, rays_d = rays_o.reshape(-1, 3), rays_d.reshape(-1, 3) + + return rays_o, rays_d + + +def get_projection_matrix( + fovy: Union[float, Float[Tensor, "B"]], aspect_wh: float, near: float, far: float +) -> Float[Tensor, "*B 4 4"]: + if isinstance(fovy, float): + proj_mtx = torch.zeros(4, 4, dtype=torch.float32) + proj_mtx[0, 0] = 1.0 / (math.tan(fovy / 2.0) * aspect_wh) + proj_mtx[1, 1] = -1.0 / math.tan( + fovy / 2.0 + ) # add a negative sign here as the y axis is flipped in nvdiffrast output + proj_mtx[2, 2] = -(far + near) / (far - near) + proj_mtx[2, 3] = -2.0 * far * near / (far - near) + proj_mtx[3, 2] = -1.0 + else: + batch_size = fovy.shape[0] + proj_mtx = torch.zeros(batch_size, 4, 4, dtype=torch.float32) + proj_mtx[:, 0, 0] = 1.0 / (torch.tan(fovy / 2.0) * aspect_wh) + proj_mtx[:, 1, 1] = -1.0 / torch.tan( + fovy / 2.0 + ) # add a negative sign here as the y axis is flipped in nvdiffrast output + proj_mtx[:, 2, 2] = -(far + near) / (far - near) + proj_mtx[:, 2, 3] = -2.0 * far * near / (far - near) + proj_mtx[:, 3, 2] = -1.0 + return proj_mtx + + +def get_mvp_matrix( + c2w: Float[Tensor, "*B 4 4"], proj_mtx: Float[Tensor, "*B 4 4"] +) -> Float[Tensor, "*B 4 4"]: + # calculate w2c from c2w: R' = Rt, t' = -Rt * t + # mathematically equivalent to (c2w)^-1 + if c2w.ndim == 2: + assert proj_mtx.ndim == 2 + w2c: Float[Tensor, "4 4"] = torch.zeros(4, 4).to(c2w) + w2c[:3, :3] = c2w[:3, :3].permute(1, 0) + w2c[:3, 3:] = -c2w[:3, :3].permute(1, 0) @ c2w[:3, 3:] + w2c[3, 3] = 1.0 + else: + w2c: Float[Tensor, "B 4 4"] = torch.zeros(c2w.shape[0], 4, 4).to(c2w) + w2c[:, :3, :3] = c2w[:, :3, :3].permute(0, 2, 1) + w2c[:, :3, 3:] = -c2w[:, :3, :3].permute(0, 2, 1) @ c2w[:, :3, 3:] + w2c[:, 3, 3] = 1.0 + # calculate mvp matrix by proj_mtx @ w2c (mv_mtx) + mvp_mtx = proj_mtx @ w2c + return mvp_mtx + +def get_intrinsic_from_fov(fov, H, W, bs=-1): + focal_length = 0.5 * H / np.tan(0.5 * fov) + intrinsic = np.identity(3, dtype=np.float32) + intrinsic[0, 0] = focal_length + intrinsic[1, 1] = focal_length + intrinsic[0, 2] = W / 2.0 + intrinsic[1, 2] = H / 2.0 + + if bs > 0: + intrinsic = intrinsic[None].repeat(bs, axis=0) + + return torch.from_numpy(intrinsic) + +def points_projection(points: Float[Tensor, "B Np 3"], + c2ws: Float[Tensor, "B 4 4"], + intrinsics: Float[Tensor, "B 3 3"], + local_features: Float[Tensor, "B C H W"], + # Rasterization settings + raster_point_radius: float = 0.0075, # point size + raster_points_per_pixel: int = 1, # a single point per pixel, for now + bin_size: int = 0): + B, C, H, W = local_features.shape + device = local_features.device + raster_settings = PointsRasterizationSettings( + image_size=(H, W), + radius=raster_point_radius, + points_per_pixel=raster_points_per_pixel, + bin_size=bin_size, + ) + Np = points.shape[1] + R = raster_settings.points_per_pixel + + w2cs = torch.inverse(c2ws) + image_size = torch.as_tensor([H, W]).view(1, 2).expand(w2cs.shape[0], -1).to(device) + cameras = cameras_from_opencv_projection(w2cs[:, :3, :3], w2cs[:, :3, 3], intrinsics, image_size) + + rasterize = PointsRasterizer(cameras=cameras, raster_settings=raster_settings) + fragments = rasterize(Pointclouds(points)) + fragments_idx: Tensor = fragments.idx.long() + visible_pixels = (fragments_idx > -1) # (B, H, W, R) + points_to_visible_pixels = fragments_idx[visible_pixels] + + # Reshape local features to (B, H, W, R, C) + local_features = local_features.permute(0, 2, 3, 1).unsqueeze(-2).expand(-1, -1, -1, R, -1) # (B, H, W, R, C) + + # Get local features corresponding to visible points + local_features_proj = torch.zeros(B * Np, C, device=device) + local_features_proj[points_to_visible_pixels] = local_features[visible_pixels] + local_features_proj = local_features_proj.reshape(B, Np, C) + + return local_features_proj + +def compute_distance_transform(mask: torch.Tensor): + image_size = mask.shape[-1] + distance_transform = torch.stack([ + torch.from_numpy(cv2.distanceTransform( + (1 - m), distanceType=cv2.DIST_L2, maskSize=cv2.DIST_MASK_3 + ) / (image_size / 2)) + for m in mask.squeeze(1).detach().cpu().numpy().astype(np.uint8) + ]).unsqueeze(1).clip(0, 1).to(mask.device) + return distance_transform \ No newline at end of file diff --git a/tgs/utils/saving.py b/tgs/utils/saving.py new file mode 100644 index 0000000..f8be1bc --- /dev/null +++ b/tgs/utils/saving.py @@ -0,0 +1,314 @@ +import os +import re +import shutil + +import cv2 +import imageio +import matplotlib.pyplot as plt +import numpy as np +import torch +from matplotlib import cm +from matplotlib.colors import LinearSegmentedColormap +from PIL import Image, ImageDraw + +from .typing import * + +class SaverMixin: + _save_dir: Optional[str] = None + + def set_save_dir(self, save_dir: str): + self._save_dir = save_dir + + def get_save_dir(self): + if self._save_dir is None: + raise ValueError("Save dir is not set") + return self._save_dir + + def convert_data(self, data): + if data is None: + return None + elif isinstance(data, np.ndarray): + return data + elif isinstance(data, torch.Tensor): + return data.detach().cpu().numpy() + elif isinstance(data, list): + return [self.convert_data(d) for d in data] + elif isinstance(data, dict): + return {k: self.convert_data(v) for k, v in data.items()} + else: + raise TypeError( + "Data must be in type numpy.ndarray, torch.Tensor, list or dict, getting", + type(data), + ) + + def get_save_path(self, filename): + save_path = os.path.join(self.get_save_dir(), filename) + os.makedirs(os.path.dirname(save_path), exist_ok=True) + return save_path + + DEFAULT_RGB_KWARGS = {"data_format": "HWC", "data_range": (0, 1)} + DEFAULT_UV_KWARGS = { + "data_format": "HWC", + "data_range": (0, 1), + "cmap": "checkerboard", + } + DEFAULT_GRAYSCALE_KWARGS = {"data_range": None, "cmap": "jet"} + DEFAULT_GRID_KWARGS = {"align": "max"} + + def get_rgb_image_(self, img, data_format, data_range, rgba=False): + img = self.convert_data(img) + assert data_format in ["CHW", "HWC"] + if data_format == "CHW": + img = img.transpose(1, 2, 0) + if img.dtype != np.uint8: + img = img.clip(min=data_range[0], max=data_range[1]) + img = ( + (img - data_range[0]) / (data_range[1] - data_range[0]) * 255.0 + ).astype(np.uint8) + nc = 4 if rgba else 3 + imgs = [img[..., start : start + nc] for start in range(0, img.shape[-1], nc)] + imgs = [ + img_ + if img_.shape[-1] == nc + else np.concatenate( + [ + img_, + np.zeros( + (img_.shape[0], img_.shape[1], nc - img_.shape[2]), + dtype=img_.dtype, + ), + ], + axis=-1, + ) + for img_ in imgs + ] + img = np.concatenate(imgs, axis=1) + if rgba: + img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) + else: + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + return img + + def _save_rgb_image( + self, + filename, + img, + data_format, + data_range + ): + img = self.get_rgb_image_(img, data_format, data_range) + cv2.imwrite(filename, img) + + def save_rgb_image( + self, + filename, + img, + data_format=DEFAULT_RGB_KWARGS["data_format"], + data_range=DEFAULT_RGB_KWARGS["data_range"], + ) -> str: + save_path = self.get_save_path(filename) + self._save_rgb_image(save_path, img, data_format, data_range) + return save_path + + def get_grayscale_image_(self, img, data_range, cmap): + img = self.convert_data(img) + img = np.nan_to_num(img) + if data_range is None: + img = (img - img.min()) / (img.max() - img.min()) + else: + img = img.clip(data_range[0], data_range[1]) + img = (img - data_range[0]) / (data_range[1] - data_range[0]) + assert cmap in [None, "jet", "magma", "spectral"] + if cmap == None: + img = (img * 255.0).astype(np.uint8) + img = np.repeat(img[..., None], 3, axis=2) + elif cmap == "jet": + img = (img * 255.0).astype(np.uint8) + img = cv2.applyColorMap(img, cv2.COLORMAP_JET) + elif cmap == "magma": + img = 1.0 - img + base = cm.get_cmap("magma") + num_bins = 256 + colormap = LinearSegmentedColormap.from_list( + f"{base.name}{num_bins}", base(np.linspace(0, 1, num_bins)), num_bins + )(np.linspace(0, 1, num_bins))[:, :3] + a = np.floor(img * 255.0) + b = (a + 1).clip(max=255.0) + f = img * 255.0 - a + a = a.astype(np.uint16).clip(0, 255) + b = b.astype(np.uint16).clip(0, 255) + img = colormap[a] + (colormap[b] - colormap[a]) * f[..., None] + img = (img * 255.0).astype(np.uint8) + elif cmap == "spectral": + colormap = plt.get_cmap("Spectral") + + def blend_rgba(image): + image = image[..., :3] * image[..., -1:] + ( + 1.0 - image[..., -1:] + ) # blend A to RGB + return image + + img = colormap(img) + img = blend_rgba(img) + img = (img * 255).astype(np.uint8) + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + return img + + def _save_grayscale_image( + self, + filename, + img, + data_range, + cmap, + ): + img = self.get_grayscale_image_(img, data_range, cmap) + cv2.imwrite(filename, img) + + def save_grayscale_image( + self, + filename, + img, + data_range=DEFAULT_GRAYSCALE_KWARGS["data_range"], + cmap=DEFAULT_GRAYSCALE_KWARGS["cmap"], + ) -> str: + save_path = self.get_save_path(filename) + self._save_grayscale_image(save_path, img, data_range, cmap) + return save_path + + def get_image_grid_(self, imgs, align): + if isinstance(imgs[0], list): + return np.concatenate( + [self.get_image_grid_(row, align) for row in imgs], axis=0 + ) + cols = [] + for col in imgs: + assert col["type"] in ["rgb", "uv", "grayscale"] + if col["type"] == "rgb": + rgb_kwargs = self.DEFAULT_RGB_KWARGS.copy() + rgb_kwargs.update(col["kwargs"]) + cols.append(self.get_rgb_image_(col["img"], **rgb_kwargs)) + elif col["type"] == "uv": + uv_kwargs = self.DEFAULT_UV_KWARGS.copy() + uv_kwargs.update(col["kwargs"]) + cols.append(self.get_uv_image_(col["img"], **uv_kwargs)) + elif col["type"] == "grayscale": + grayscale_kwargs = self.DEFAULT_GRAYSCALE_KWARGS.copy() + grayscale_kwargs.update(col["kwargs"]) + cols.append(self.get_grayscale_image_(col["img"], **grayscale_kwargs)) + + if align == "max": + h = max([col.shape[0] for col in cols]) + w = max([col.shape[1] for col in cols]) + elif align == "min": + h = min([col.shape[0] for col in cols]) + w = min([col.shape[1] for col in cols]) + elif isinstance(align, int): + h = align + w = align + elif ( + isinstance(align, tuple) + and isinstance(align[0], int) + and isinstance(align[1], int) + ): + h, w = align + else: + raise ValueError( + f"Unsupported image grid align: {align}, should be min, max, int or (int, int)" + ) + + for i in range(len(cols)): + if cols[i].shape[0] != h or cols[i].shape[1] != w: + cols[i] = cv2.resize(cols[i], (w, h), interpolation=cv2.INTER_LINEAR) + return np.concatenate(cols, axis=1) + + def save_image_grid( + self, + filename, + imgs, + align=DEFAULT_GRID_KWARGS["align"], + texts: Optional[List[float]] = None, + ): + save_path = self.get_save_path(filename) + img = self.get_image_grid_(imgs, align=align) + + if texts is not None: + img = Image.fromarray(img) + draw = ImageDraw.Draw(img) + black, white = (0, 0, 0), (255, 255, 255) + for i, text in enumerate(texts): + draw.text((2, (img.size[1] // len(texts)) * i + 1), f"{text}", white) + draw.text((0, (img.size[1] // len(texts)) * i + 1), f"{text}", white) + draw.text((2, (img.size[1] // len(texts)) * i - 1), f"{text}", white) + draw.text((0, (img.size[1] // len(texts)) * i - 1), f"{text}", white) + draw.text((1, (img.size[1] // len(texts)) * i), f"{text}", black) + img = np.asarray(img) + + cv2.imwrite(save_path, img) + return save_path + + def save_image(self, filename, img) -> str: + save_path = self.get_save_path(filename) + img = self.convert_data(img) + assert img.dtype == np.uint8 or img.dtype == np.uint16 + if img.ndim == 3 and img.shape[-1] == 3: + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + elif img.ndim == 3 and img.shape[-1] == 4: + img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) + cv2.imwrite(save_path, img) + return save_path + + def save_img_sequence( + self, + filename, + img_dir, + matcher, + save_format="mp4", + fps=30, + ) -> str: + assert save_format in ["gif", "mp4"] + if not filename.endswith(save_format): + filename += f".{save_format}" + save_path = self.get_save_path(filename) + matcher = re.compile(matcher) + img_dir = os.path.join(self.get_save_dir(), img_dir) + imgs = [] + for f in os.listdir(img_dir): + if matcher.search(f): + imgs.append(f) + imgs = sorted(imgs, key=lambda f: int(matcher.search(f).groups()[0])) + imgs = [cv2.imread(os.path.join(img_dir, f)) for f in imgs] + + if save_format == "gif": + imgs = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in imgs] + imageio.mimsave(save_path, imgs, fps=fps, palettesize=256) + elif save_format == "mp4": + imgs = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in imgs] + imageio.mimsave(save_path, imgs, fps=fps) + return save_path + + def save_img_sequences( + self, + seq_dir, + matcher, + save_format="mp4", + fps=30, + delete=True + ): + seq_dir_ = os.path.join(self.get_save_dir(), seq_dir) + for f in os.listdir(seq_dir_): + img_dir_ = os.path.join(seq_dir_, f) + if not os.path.isdir(img_dir_): + continue + try: + self.save_img_sequence( + os.path.join(seq_dir, f), + os.path.join(seq_dir, f), + matcher, + save_format=save_format, + fps=fps + ) + except: + raise ValueError(f"Video saving for directory {seq_dir_} failed!") + + if delete: + shutil.rmtree(img_dir_) diff --git a/tgs/utils/typing.py b/tgs/utils/typing.py new file mode 100644 index 0000000..dee9f96 --- /dev/null +++ b/tgs/utils/typing.py @@ -0,0 +1,40 @@ +""" +This module contains type annotations for the project, using +1. Python type hints (https://docs.python.org/3/library/typing.html) for Python objects +2. jaxtyping (https://github.com/google/jaxtyping/blob/main/API.md) for PyTorch tensors + +Two types of typing checking can be used: +1. Static type checking with mypy (install with pip and enabled as the default linter in VSCode) +2. Runtime type checking with typeguard (install with pip and triggered at runtime, mainly for tensor dtype and shape checking) +""" + +# Basic types +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Literal, + NamedTuple, + NewType, + Optional, + Sized, + Tuple, + Type, + TypeVar, + Union, +) + +# Tensor dtype +# for jaxtyping usage, see https://github.com/google/jaxtyping/blob/main/API.md +from jaxtyping import Bool, Complex, Float, Inexact, Int, Integer, Num, Shaped, UInt + +# Config type +from omegaconf import DictConfig + +# PyTorch Tensor type +from torch import Tensor + +# Runtime type checking decorator +from typeguard import typechecked as typechecker