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 0000000..34ca7c4 Binary files /dev/null and b/_Example_Workflows/_Example_Inputs_Files/a_pikachu_with_smily_face-removebg.png differ 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