├── .gitignore ├── ACKNOWLEDGEMENTS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Pointcept-wrapper ├── README.md ├── configs │ └── scannet │ │ ├── semseg-kpconvx-base.py │ │ └── semseg-kpnext-base.py └── models │ ├── kpconvx │ ├── __init__.py │ ├── cpp_wrappers │ │ ├── cpp_neighbors │ │ │ ├── build.bat │ │ │ ├── neighbors │ │ │ │ ├── neighbors.cpp │ │ │ │ └── neighbors.h │ │ │ ├── setup.py │ │ │ └── wrapper.cpp │ │ ├── cpp_subsampling │ │ │ ├── build.bat │ │ │ ├── fps_subsampling │ │ │ │ ├── fps_subsampling.cpp │ │ │ │ └── fps_subsampling.h │ │ │ ├── grid_subsampling │ │ │ │ ├── grid_subsampling.cpp │ │ │ │ └── grid_subsampling.h │ │ │ ├── setup.py │ │ │ └── wrapper.cpp │ │ └── cpp_utils │ │ │ ├── cloud │ │ │ ├── cloud.cpp │ │ │ └── cloud.h │ │ │ └── nanoflann │ │ │ └── nanoflann.hpp │ ├── kpconvx_base.py │ └── utils │ │ ├── batch_conversion.py │ │ ├── cpp_funcs.py │ │ ├── dispositions │ │ ├── k_015_center_3D_0.ply │ │ ├── k_016_center_3D_0.ply │ │ ├── k_022_center_3D_0.ply │ │ ├── k_043_center_3D_0.ply │ │ ├── k_045_center_3D_0.ply │ │ ├── k_058_center_3D_0.ply │ │ ├── k_062_center_3D_0.ply │ │ ├── k_103_center_3D_0.ply │ │ ├── k_105_center_3D_0.ply │ │ └── k_125_center_3D_0.ply │ │ ├── generic_blocks.py │ │ ├── gpu_neigbors.py │ │ ├── gpu_subsampling.py │ │ ├── kernel_points.py │ │ ├── kpconv_blocks.py │ │ ├── kpnext_blocks.py │ │ ├── ply.py │ │ └── torch_pyramid.py │ └── kpnext │ ├── __init__.py │ ├── dispositions │ ├── k_015_center_3D_0.ply │ ├── k_016_center_3D_0.ply │ ├── k_022_center_3D_0.ply │ ├── k_043_center_3D_0.ply │ ├── k_045_center_3D_0.ply │ ├── k_058_center_3D_0.ply │ ├── k_062_center_3D_0.ply │ ├── k_103_center_3D_0.ply │ ├── k_105_center_3D_0.ply │ └── k_125_center_3D_0.ply │ ├── generic_blocks.py │ ├── kernel_points.py │ ├── kpconv_blocks.py │ ├── kpnext_base.py │ ├── kpnext_blocks.py │ ├── ply.py │ └── torch_pyramid.py ├── README.md ├── Standalone ├── KPConvX │ ├── cpp_wrappers │ │ ├── cpp_neighbors │ │ │ ├── build.bat │ │ │ ├── neighbors │ │ │ │ ├── neighbors.cpp │ │ │ │ └── neighbors.h │ │ │ ├── setup.py │ │ │ └── wrapper.cpp │ │ ├── cpp_subsampling │ │ │ ├── build.bat │ │ │ ├── fps_subsampling │ │ │ │ ├── fps_subsampling.cpp │ │ │ │ └── fps_subsampling.h │ │ │ ├── grid_subsampling │ │ │ │ ├── grid_subsampling.cpp │ │ │ │ └── grid_subsampling.h │ │ │ ├── setup.py │ │ │ └── wrapper.cpp │ │ ├── cpp_utils │ │ │ ├── cloud │ │ │ │ ├── cloud.cpp │ │ │ │ └── cloud.h │ │ │ └── nanoflann │ │ │ │ └── nanoflann.hpp │ │ └── pointnet2_batch │ │ │ ├── setup.py │ │ │ └── src │ │ │ ├── cuda_utils.h │ │ │ ├── pointnet2_api.cpp │ │ │ ├── sampling.cpp │ │ │ ├── sampling_gpu.cu │ │ │ └── sampling_gpu.h │ ├── data_handlers │ │ ├── object_classification.py │ │ └── scene_seg.py │ ├── experiments │ │ ├── S3DIS │ │ │ ├── S3DIS.py │ │ │ ├── S3DIS_rooms.py │ │ │ ├── plot_S3DIS.py │ │ │ ├── test_S3DIS.py │ │ │ └── train_S3DIS.py │ │ └── ScanObjectNN │ │ │ ├── ScanObjectNN.py │ │ │ ├── plot_ScanObj.py │ │ │ ├── test_ScanObj.py │ │ │ └── train_ScanObj.py │ ├── kernels │ │ ├── dispositions │ │ │ ├── k_015_center_3D_0.ply │ │ │ ├── k_016_center_3D_0.ply │ │ │ ├── k_022_center_3D_0.ply │ │ │ ├── k_043_center_3D_0.ply │ │ │ ├── k_045_center_3D_0.ply │ │ │ ├── k_058_center_3D_0.ply │ │ │ ├── k_062_center_3D_0.ply │ │ │ ├── k_103_center_3D_0.ply │ │ │ ├── k_105_center_3D_0.ply │ │ │ └── k_125_center_3D_0.ply │ │ └── kernel_points.py │ ├── models │ │ ├── InvolutionNet.py │ │ ├── KPConvNet.py │ │ ├── KPInvNet.py │ │ ├── KPNext.py │ │ ├── generic_blocks.py │ │ ├── kpconv_blocks.py │ │ ├── kpinv_blocks.py │ │ ├── kpnext_blocks.py │ │ ├── kptran_blocks.py │ │ └── pi_blocks.py │ ├── tasks │ │ ├── test.py │ │ ├── training.py │ │ ├── trainval.py │ │ └── validation.py │ └── utils │ │ ├── batch_conversion.py │ │ ├── config.py │ │ ├── cpp_funcs.py │ │ ├── cuda_funcs.py │ │ ├── gpu_init.py │ │ ├── gpu_neigbors.py │ │ ├── gpu_subsampling.py │ │ ├── mayavi_visu.py │ │ ├── metrics.py │ │ ├── plot_utilities.py │ │ ├── ply.py │ │ ├── printing.py │ │ ├── rotation.py │ │ ├── rsmix_provider.py │ │ ├── torch_pyramid.py │ │ └── transform.py ├── README.md ├── test_S3DIS.sh ├── test_ScanObjectNN.sh ├── train_S3DIS.sh └── train_ScanObjectNN.sh └── assets └── fig_kpconvx.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **/__pycache__ 3 | **/build 4 | **/*.egg-info 5 | **/dist 6 | *.so 7 | 8 | .vscode 9 | .idea 10 | .DS_Store 11 | **/.DS_Store 12 | **/*.out 13 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | Portions of this Software may utilize the following copyrighted 3 | material, the use of which is hereby acknowledged. 4 | 5 | ------------------------------------------------ 6 | Pointcept 7 | 8 | MIT License 9 | 10 | Copyright (c) 2023 Pointcept 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | 30 | ------------------------------------------------ 31 | KPConv-PyTorch 32 | 33 | MIT License 34 | 35 | Copyright (c) 2019 HuguesTHOMAS 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy 38 | of this software and associated documentation files (the "Software"), to deal 39 | in the Software without restriction, including without limitation the rights 40 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the Software is 42 | furnished to do so, subject to the following conditions: 43 | 44 | The above copyright notice and this permission notice shall be included in all 45 | copies or substantial portions of the Software. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 50 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 52 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 53 | SOFTWARE. 54 | 55 | ------------------------------------------------ 56 | nanoflann 57 | 58 | Software License Agreement (BSD License) 59 | 60 | Copyright 2008-2009 Marius Muja (mariusm@cs.ubc.ca). All rights reserved. 61 | Copyright 2008-2009 David G. Lowe (lowe@cs.ubc.ca). All rights reserved. 62 | Copyright 2011 Jose L. Blanco (joseluisblancoc@gmail.com). All rights reserved. 63 | 64 | THE BSD LICENSE 65 | 66 | Redistribution and use in source and binary forms, with or without 67 | modification, are permitted provided that the following conditions 68 | are met: 69 | 70 | 1. Redistributions of source code must retain the above copyright 71 | notice, this list of conditions and the following disclaimer. 72 | 2. Redistributions in binary form must reproduce the above copyright 73 | notice, this list of conditions and the following disclaimer in the 74 | documentation and/or other materials provided with the distribution. 75 | 76 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 77 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 78 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 79 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 80 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 81 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 82 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 83 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 84 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 85 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 86 | 87 | ------------------------------------------------ 88 | RSMix 89 | 90 | MIT License 91 | 92 | Copyright (c) 2020 dogyoonlee 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to deal 96 | in the Software without restriction, including without limitation the rights 97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 98 | copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. 111 | 112 | 113 | ------------------------------------------------ 114 | PointNeXt 115 | 116 | MIT License 117 | 118 | Copyright (c) Guocheng Qian. 119 | 120 | Permission is hereby granted, free of charge, to any person obtaining a copy 121 | of this software and associated documentation files (the "Software"), to deal 122 | in the Software without restriction, including without limitation the rights 123 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 124 | copies of the Software, and to permit persons to whom the Software is 125 | furnished to do so, subject to the following conditions: 126 | 127 | The above copyright notice and this permission notice shall be included in all 128 | copies or substantial portions of the Software. 129 | 130 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 131 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 132 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 133 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 134 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 135 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 136 | SOFTWARE -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the open source team at [opensource-conduct@group.apple.com](mailto:opensource-conduct@group.apple.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | Thanks for your interest in contributing. This project was released to accompany a research paper for purposes of reproducibility, and beyond its publication there are limited plans for future development of the repository. 4 | 5 | While we welcome new pull requests and issues please note that our response may be limited. Forks and out-of-tree improvements are strongly encouraged. 6 | 7 | ## Before you get started 8 | 9 | By submitting a pull request, you represent that you have the right to license your contribution to Apple and the community, and agree by submitting the patch that your contributions are licensed under the [LICENSE](LICENSE). 10 | 11 | We ask that all community members read and observe our [Code of Conduct](CODE_OF_CONDUCT.md). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2024 Apple Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ------------------------------------------------------------------------------- 24 | SOFTWARE DISTRIBUTED WITH ML-KPConvX: 25 | 26 | The ML-KPConvX software includes a number of subcomponents with separate 27 | copyright notices and license terms - please see the file ACKNOWLEDGEMENTS. 28 | ------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /Pointcept-wrapper/README.md: -------------------------------------------------------------------------------- 1 | # KPConvX: Pointcept wrappers 2 | 3 | This folder contains wrappers to use KPConvX with the Pointcept library. This allows training KPConvX with multiple GPUs and on multiple datasets. 4 | 5 | ## Usage 6 | 7 | In order to use these wrappers, follow these step-by-step instructions: 8 | 9 | - Install Pointcept following the [original intructions](https://github.com/Pointcept/Pointcept?tab=readme-ov-file#installation). 10 | 11 | - Copy the files from `Pointcept-wrapper/configs` to the `pointcept/configs` directory. 12 | 13 | - Copy the folders from `Pointcept-wrapper/models` in the `pointcept/models` directory. 14 | 15 | - Add the following lines to `pointcept/models/__init__.py` 16 | ```python 17 | from .kpconvx import * 18 | from .kpnext import * 19 | ``` 20 | 21 | - Compile the cpp wrappers in `models/kpconvx/cpp_wrappers`. 22 | ```bash 23 | cd models/kpconvx/cpp_wrappers/cpp_subsampling 24 | python3 setup.py build_ext --inplace 25 | cd ../cpp_neighbors 26 | python3 setup.py build_ext --inplace 27 | ``` 28 | 29 | At this point you should be able to train a KPConvX model with the Poincept library. 30 | 31 | If you clone Poincept in this folder, you can use the following bash commands: 32 | 33 | ```bash 34 | # Install KPConvX with Pointcept 35 | cp configs/scannet/* Pointcept/configs/scannet/ 36 | cp -r models/* Pointcept/pointcept/models/ 37 | echo "from .kpconvx import *" >> Pointcept/pointcept/models/__init__.py 38 | echo "from .kpnext import *" >> Pointcept/pointcept/models/__init__.py 39 | cd Pointcept/libs/pointops 40 | python3 setup.py install --user 41 | cd ../../pointcept/models/kpconvx/cpp_wrappers/cpp_subsampling 42 | python3 setup.py build_ext --inplace 43 | cd ../cpp_neighbors 44 | python3 setup.py build_ext --inplace 45 | cd ../../../../.. 46 | 47 | # Start a training 48 | sh scripts/train.sh -p python -d scannet -c semseg-kpconvx-base -n semseg-kpconvx-base -g 1 49 | #or 50 | sh scripts/train.sh -p python -d scannet -c semseg-kpnext-base -n semseg-kpnext-base -g 1 51 | ``` 52 | 53 | 54 | 55 | 56 | ## Details 57 | 58 | We provide two different models to use with Pointcept: 59 | 60 | ### KPNeXt 61 | 62 | `kpnext` is our first attempt at using KPConvX convolution with the pointcept library and aimed to be a direct comparison with PointTransformer v2. We used the `point_transformer_v2m2_base` model file and modified it to use kpconvx layers instead of self-attention layers. 63 | 64 | 65 | ### KPConvX 66 | 67 | `kpconvx` is a strict conversion of the Standalone model to be used with Poincept training pipeline. Although the backbone operations are identical, the Poincept and the Standalone training pipelines are different, leading to varying final performances. In the paper, we use the Standalone pipeline for S3DIS and the Pointcept pipeline for Scannet 68 | 69 | -------------------------------------------------------------------------------- /Pointcept-wrapper/configs/scannet/semseg-kpconvx-base.py: -------------------------------------------------------------------------------- 1 | _base_ = ["../_base_/default_runtime.py"] 2 | 3 | # minimal example settings 4 | num_worker = 8 # total worker in all gpu 5 | batch_size = 2 6 | mix_prob = 0.8 7 | max_input_pts = 40000 8 | 9 | # # Settings for multigpu training with 8 80GB gpus 10 | # num_worker = 64 # total worker in all gpu 11 | # batch_size = 12 12 | # mix_prob = 0.8 13 | # empty_cache = False 14 | # enable_amp = True 15 | # sync_bn = True 16 | # max_input_pts = 80000 17 | 18 | 19 | # model settings 20 | model = dict( 21 | type="DefaultSegmentor", 22 | backbone=dict( 23 | type="kpconvx_base", 24 | input_channels=9, 25 | num_classes=20, 26 | dim=3, 27 | task='cloud_segmentation', 28 | kp_mode='kpconvx', 29 | shell_sizes=(1, 14, 28), 30 | kp_radius=2.3, 31 | kp_aggregation='nearest', 32 | kp_influence='constant', 33 | kp_sigma=2.3, 34 | share_kp=False, 35 | conv_groups=-1, # Only for old KPConv blocks 36 | inv_groups=8, 37 | inv_act='sigmoid', 38 | inv_grp_norm=True, 39 | kpx_upcut=False, 40 | subsample_size=0.02, 41 | neighbor_limits=(12, 16, 20, 20, 20), 42 | layer_blocks=(3, 3, 9, 12, 3), 43 | init_channels=64, 44 | channel_scaling=1.414, 45 | radius_scaling=2.2, 46 | decoder_layer=True, 47 | grid_pool=True, 48 | upsample_n=3, # Ignored if grid_pool is True 49 | first_inv_layer=1, 50 | drop_path_rate=0.3, 51 | norm='batch', 52 | bn_momentum=0.1, 53 | smooth_labels=False, # True only for classification 54 | class_w=(), 55 | ), 56 | criteria=[ 57 | dict(type="CrossEntropyLoss", 58 | loss_weight=1.0, 59 | ignore_index=-1), 60 | dict(type="LovaszLoss", 61 | mode="multiclass", 62 | loss_weight=1.0, 63 | ignore_index=-1) 64 | ] 65 | ) 66 | 67 | # scheduler settings 68 | epoch = 1000 69 | eval_epoch = 200 70 | optimizer = dict(type="AdamW", lr=0.005, weight_decay=0.02) 71 | scheduler = dict(type="OneCycleLR", 72 | max_lr=optimizer["lr"], 73 | pct_start=0.05, 74 | anneal_strategy="cos", 75 | div_factor=100.0, 76 | final_div_factor=1000.0) 77 | 78 | # dataset settings 79 | dataset_type = "ScanNetDataset" 80 | data_root = "data/scannet" 81 | 82 | data = dict( 83 | num_classes=20, 84 | ignore_index=-1, 85 | names=["wall", "floor", "cabinet", "bed", "chair", 86 | "sofa", "table", "door", "window", "bookshelf", 87 | "picture", "counter", "desk", "curtain", "refridgerator", 88 | "shower curtain", "toilet", "sink", "bathtub", "otherfurniture"], 89 | train=dict( 90 | type=dataset_type, 91 | split="train", 92 | data_root=data_root, 93 | transform=[ 94 | dict(type="CenterShift", apply_z=True), 95 | dict(type="RandomDropout", dropout_ratio=0.2, dropout_application_ratio=0.2), 96 | dict(type="RandomRotateTargetAngle", angle=(1/2, 1, 3/2), center=[0, 0, 0], axis="z", p=0.75), 97 | dict(type="RandomRotate", angle=[-1, 1], axis="z", center=[0, 0, 0], p=0.0), 98 | dict(type="RandomRotate", angle=[-1/64, 1/64], axis="x", p=0.5), 99 | dict(type="RandomRotate", angle=[-1/64, 1/64], axis="y", p=0.5), 100 | dict(type="RandomScale", scale=[0.9, 1.1]), 101 | # dict(type="RandomShift", shift=[0.2, 0.2, 0.2]), 102 | dict(type="RandomFlip", p=0.5), 103 | dict(type="RandomJitter", sigma=0.005, clip=0.02), 104 | dict(type="ElasticDistortion", distortion_params=[[0.2, 0.4], [0.8, 1.6]]), 105 | dict(type="ChromaticAutoContrast", p=0.2, blend_factor=None), 106 | dict(type="ChromaticTranslation", p=0.95, ratio=0.05), 107 | dict(type="ChromaticJitter", p=0.95, std=0.05), 108 | # dict(type="HueSaturationTranslation", hue_max=0.2, saturation_max=0.2), 109 | # dict(type="RandomColorDrop", p=0.2, color_augment=0.0), 110 | dict(type="GridSample", 111 | grid_size=0.02, 112 | hash_type="fnv", 113 | mode="train", 114 | keys=("coord", "color", "normal", "segment"), 115 | return_min_coord=True), 116 | dict(type="SphereCrop", point_max=max_input_pts, mode="random"), 117 | dict(type="CenterShift", apply_z=False), 118 | dict(type="NormalizeColor"), 119 | dict(type="ShufflePoint"), 120 | dict(type="ToTensor"), 121 | dict(type="Collect", keys=("coord", "segment"), feat_keys=("coord", "color", "normal")) 122 | ], 123 | test_mode=False, 124 | ), 125 | 126 | val=dict( 127 | type=dataset_type, 128 | split="val", 129 | data_root=data_root, 130 | transform=[ 131 | dict(type="CenterShift", apply_z=True), 132 | dict(type="GridSample", 133 | grid_size=0.02, 134 | hash_type="fnv", 135 | mode="train", 136 | keys=("coord", "color", "normal", "segment"), 137 | return_min_coord=True), 138 | # dict(type="SphereCrop", point_max=1000000, mode="center"), 139 | dict(type="CenterShift", apply_z=False), 140 | dict(type="NormalizeColor"), 141 | dict(type="ToTensor"), 142 | dict(type="Collect", keys=("coord", "segment"), feat_keys=("coord", "color", "normal")) 143 | ], 144 | test_mode=False, 145 | ), 146 | 147 | test=dict( 148 | type=dataset_type, 149 | split="val", 150 | data_root=data_root, 151 | transform=[ 152 | dict(type="CenterShift", apply_z=True), 153 | dict(type="NormalizeColor"), 154 | ], 155 | test_mode=True, 156 | test_cfg=dict( 157 | voxelize=dict(type="GridSample", 158 | grid_size=0.02, 159 | hash_type="fnv", 160 | mode="test", 161 | keys=("coord", "color", "normal") 162 | ), 163 | crop=None, 164 | post_transform=[ 165 | dict(type="CenterShift", apply_z=False), 166 | dict(type="ToTensor"), 167 | dict(type="Collect", keys=("coord", "index"), feat_keys=("coord", "color", "normal")) 168 | ], 169 | aug_transform=[ 170 | [dict(type="RandomRotateTargetAngle", angle=[0], axis="z", center=[0, 0, 0], p=1)], 171 | [dict(type="RandomRotateTargetAngle", angle=[1/2], axis="z", center=[0, 0, 0], p=1)], 172 | [dict(type="RandomRotateTargetAngle", angle=[1], axis="z", center=[0, 0, 0], p=1)], 173 | [dict(type="RandomRotateTargetAngle", angle=[3/2], axis="z", center=[0, 0, 0], p=1)], 174 | [dict(type="RandomRotateTargetAngle", angle=[0], axis="z", center=[0, 0, 0], p=1), 175 | dict(type="RandomScale", scale=[0.95, 0.95])], 176 | [dict(type="RandomRotateTargetAngle", angle=[1 / 2], axis="z", center=[0, 0, 0], p=1), 177 | dict(type="RandomScale", scale=[0.95, 0.95])], 178 | [dict(type="RandomRotateTargetAngle", angle=[1], axis="z", center=[0, 0, 0], p=1), 179 | dict(type="RandomScale", scale=[0.95, 0.95])], 180 | [dict(type="RandomRotateTargetAngle", angle=[3 / 2], axis="z", center=[0, 0, 0], p=1), 181 | dict(type="RandomScale", scale=[0.95, 0.95])], 182 | [dict(type="RandomRotateTargetAngle", angle=[0], axis="z", center=[0, 0, 0], p=1), 183 | dict(type="RandomScale", scale=[1.05, 1.05])], 184 | [dict(type="RandomRotateTargetAngle", angle=[1 / 2], axis="z", center=[0, 0, 0], p=1), 185 | dict(type="RandomScale", scale=[1.05, 1.05])], 186 | [dict(type="RandomRotateTargetAngle", angle=[1], axis="z", center=[0, 0, 0], p=1), 187 | dict(type="RandomScale", scale=[1.05, 1.05])], 188 | [dict(type="RandomRotateTargetAngle", angle=[3 / 2], axis="z", center=[0, 0, 0], p=1), 189 | dict(type="RandomScale", scale=[1.05, 1.05])], 190 | [dict(type="RandomFlip", p=1)] 191 | ] 192 | ) 193 | ), 194 | ) 195 | -------------------------------------------------------------------------------- /Pointcept-wrapper/configs/scannet/semseg-kpnext-base.py: -------------------------------------------------------------------------------- 1 | _base_ = ["../_base_/default_runtime.py"] 2 | 3 | # minimal example settings 4 | num_worker = 8 # total worker in all gpu 5 | batch_size = 2 6 | mix_prob = 0.8 7 | max_input_pts = 40000 8 | 9 | # # Settings for multigpu training with 8 80GB gpus 10 | # num_worker = 64 # total worker in all gpu 11 | # batch_size = 12 12 | # mix_prob = 0.8 13 | # empty_cache = False 14 | # enable_amp = True 15 | # sync_bn = True 16 | # max_input_pts = 80000 17 | 18 | 19 | # model settings 20 | model = dict( 21 | type="DefaultSegmentor", 22 | backbone=dict( 23 | type="kpnext", 24 | in_channels=9, 25 | num_classes=20, 26 | patch_embed_depth=2, 27 | patch_embed_channels=64, 28 | patch_embed_groups=0, 29 | patch_embed_neighbours=12, 30 | enc_depths=(4, 4, 12, 4), # (3, 3, 9, 3) (4, 4, 12, 4) (4, 8, 16, 4) (4, 12, 20, 4) 31 | enc_channels=(96, 128, 256, 384), 32 | enc_groups=(0, 8, 8, 8), 33 | enc_neighbours=(16, 20, 20, 20), # (12, 12, 16, 16) (16, 16, 16, 16) 34 | dec_depths=(1, 1, 1, 1), 35 | dec_channels=(96, 128, 256, 384), 36 | dec_groups=(0, 8, 8, 8), 37 | dec_neighbours=(16, 20, 20, 20), 38 | grid_sizes=(0.044, 0.097, 0.213, 0.469), # 0.06, 0.15, 0.375, 0.9375 / 0.06, 0.123, 0.29, 0.639 / 0.044, 0.097, 0.213, 0.469 39 | shell_sizes=(1, 14, 28), 40 | subsample_size=0.02, 41 | kp_radius=2.3, 42 | attn_qkv_bias=True, 43 | pe_multiplier=False, 44 | pe_bias=True, 45 | attn_drop_rate=0., 46 | drop_path_rate=0.3, 47 | enable_checkpoint=False, 48 | unpool_backend="map", # map / interp 49 | ), 50 | criteria=[ 51 | dict(type="CrossEntropyLoss", 52 | loss_weight=1.0, 53 | ignore_index=-1), 54 | dict(type="LovaszLoss", 55 | mode="multiclass", 56 | loss_weight=1.0, 57 | ignore_index=-1) 58 | ] 59 | ) 60 | 61 | # scheduler settings 62 | epoch = 1000 63 | eval_epoch = 200 64 | optimizer = dict(type="AdamW", lr=0.005, weight_decay=0.02) 65 | scheduler = dict(type="OneCycleLR", 66 | max_lr=optimizer["lr"], 67 | pct_start=0.05, 68 | anneal_strategy="cos", 69 | div_factor=100.0, 70 | final_div_factor=1000.0) 71 | 72 | # dataset settings 73 | dataset_type = "ScanNetDataset" 74 | data_root = "data/scannet" 75 | 76 | data = dict( 77 | num_classes=20, 78 | ignore_index=-1, 79 | names=["wall", "floor", "cabinet", "bed", "chair", 80 | "sofa", "table", "door", "window", "bookshelf", 81 | "picture", "counter", "desk", "curtain", "refridgerator", 82 | "shower curtain", "toilet", "sink", "bathtub", "otherfurniture"], 83 | train=dict( 84 | type=dataset_type, 85 | split="train", 86 | data_root=data_root, 87 | transform=[ 88 | dict(type="CenterShift", apply_z=True), 89 | dict(type="RandomDropout", dropout_ratio=0.2, dropout_application_ratio=0.2), 90 | dict(type="RandomRotateTargetAngle", angle=(1/2, 1, 3/2), center=[0, 0, 0], axis="z", p=0.75), 91 | dict(type="RandomRotate", angle=[-1, 1], axis="z", center=[0, 0, 0], p=0.5), 92 | dict(type="RandomRotate", angle=[-1/64, 1/64], axis="x", p=0.5), 93 | dict(type="RandomRotate", angle=[-1/64, 1/64], axis="y", p=0.5), 94 | dict(type="RandomScale", scale=[0.9, 1.1]), 95 | # dict(type="RandomShift", shift=[0.2, 0.2, 0.2]), 96 | dict(type="RandomFlip", p=0.5), 97 | dict(type="RandomJitter", sigma=0.005, clip=0.02), 98 | dict(type="ElasticDistortion", distortion_params=[[0.2, 0.4], [0.8, 1.6]]), 99 | dict(type="ChromaticAutoContrast", p=0.2, blend_factor=None), 100 | dict(type="ChromaticTranslation", p=0.95, ratio=0.05), 101 | dict(type="ChromaticJitter", p=0.95, std=0.05), 102 | # dict(type="HueSaturationTranslation", hue_max=0.2, saturation_max=0.2), 103 | # dict(type="RandomColorDrop", p=0.2, color_augment=0.0), 104 | dict(type="GridSample", 105 | grid_size=0.02, 106 | hash_type="fnv", 107 | mode="train", 108 | keys=("coord", "color", "normal", "segment"), 109 | return_min_coord=True), 110 | dict(type="SphereCrop", point_max=max_input_pts, mode="random"), 111 | dict(type="CenterShift", apply_z=False), 112 | dict(type="NormalizeColor"), 113 | dict(type="ShufflePoint"), 114 | dict(type="ToTensor"), 115 | dict(type="Collect", keys=("coord", "segment"), feat_keys=("coord", "color", "normal")) 116 | ], 117 | test_mode=False, 118 | ), 119 | 120 | val=dict( 121 | type=dataset_type, 122 | split="val", 123 | data_root=data_root, 124 | transform=[ 125 | dict(type="CenterShift", apply_z=True), 126 | dict(type="GridSample", 127 | grid_size=0.02, 128 | hash_type="fnv", 129 | mode="train", 130 | keys=("coord", "color", "normal", "segment"), 131 | return_min_coord=True), 132 | # dict(type="SphereCrop", point_max=1000000, mode="center"), 133 | dict(type="CenterShift", apply_z=False), 134 | dict(type="NormalizeColor"), 135 | dict(type="ToTensor"), 136 | dict(type="Collect", keys=("coord", "segment"), feat_keys=("coord", "color", "normal")) 137 | ], 138 | test_mode=False, 139 | ), 140 | 141 | test=dict( 142 | type=dataset_type, 143 | split="val", 144 | data_root=data_root, 145 | transform=[ 146 | dict(type="CenterShift", apply_z=True), 147 | dict(type="NormalizeColor"), 148 | ], 149 | test_mode=True, 150 | test_cfg=dict( 151 | voxelize=dict(type="GridSample", 152 | grid_size=0.02, 153 | hash_type="fnv", 154 | mode="test", 155 | keys=("coord", "color", "normal") 156 | ), 157 | crop=None, 158 | post_transform=[ 159 | dict(type="CenterShift", apply_z=False), 160 | dict(type="ToTensor"), 161 | dict(type="Collect", keys=("coord", "index"), feat_keys=("coord", "color", "normal")) 162 | ], 163 | aug_transform=[ 164 | [dict(type="RandomRotateTargetAngle", angle=[0], axis="z", center=[0, 0, 0], p=1)], 165 | [dict(type="RandomRotateTargetAngle", angle=[1/2], axis="z", center=[0, 0, 0], p=1)], 166 | [dict(type="RandomRotateTargetAngle", angle=[1], axis="z", center=[0, 0, 0], p=1)], 167 | [dict(type="RandomRotateTargetAngle", angle=[3/2], axis="z", center=[0, 0, 0], p=1)], 168 | [dict(type="RandomRotateTargetAngle", angle=[0], axis="z", center=[0, 0, 0], p=1), 169 | dict(type="RandomScale", scale=[0.95, 0.95])], 170 | [dict(type="RandomRotateTargetAngle", angle=[1 / 2], axis="z", center=[0, 0, 0], p=1), 171 | dict(type="RandomScale", scale=[0.95, 0.95])], 172 | [dict(type="RandomRotateTargetAngle", angle=[1], axis="z", center=[0, 0, 0], p=1), 173 | dict(type="RandomScale", scale=[0.95, 0.95])], 174 | [dict(type="RandomRotateTargetAngle", angle=[3 / 2], axis="z", center=[0, 0, 0], p=1), 175 | dict(type="RandomScale", scale=[0.95, 0.95])], 176 | [dict(type="RandomRotateTargetAngle", angle=[0], axis="z", center=[0, 0, 0], p=1), 177 | dict(type="RandomScale", scale=[1.05, 1.05])], 178 | [dict(type="RandomRotateTargetAngle", angle=[1 / 2], axis="z", center=[0, 0, 0], p=1), 179 | dict(type="RandomScale", scale=[1.05, 1.05])], 180 | [dict(type="RandomRotateTargetAngle", angle=[1], axis="z", center=[0, 0, 0], p=1), 181 | dict(type="RandomScale", scale=[1.05, 1.05])], 182 | [dict(type="RandomRotateTargetAngle", angle=[3 / 2], axis="z", center=[0, 0, 0], p=1), 183 | dict(type="RandomScale", scale=[1.05, 1.05])], 184 | [dict(type="RandomFlip", p=1)] 185 | ] 186 | ) 187 | ), 188 | ) 189 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | 6 | from .kpconvx_base import KPConvXBase 7 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_neighbors/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | py setup.py build_ext --inplace 3 | 4 | 5 | pause -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_neighbors/neighbors/neighbors.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | #include "../../cpp_utils/nanoflann/nanoflann.hpp" 5 | 6 | #include 7 | #include 8 | 9 | using namespace std; 10 | 11 | 12 | void ordered_neighbors(vector& queries, 13 | vector& supports, 14 | vector& neighbors_indices, 15 | float radius); 16 | 17 | void batch_ordered_neighbors(vector& queries, 18 | vector& supports, 19 | vector& q_batches, 20 | vector& s_batches, 21 | vector& neighbors_indices, 22 | float radius); 23 | 24 | void batch_nanoflann_neighbors(vector& queries, 25 | vector& supports, 26 | vector& q_batches, 27 | vector& s_batches, 28 | vector& neighbors_indices, 29 | float radius); 30 | 31 | void batch_nanoflann_knns(vector& queries, 32 | vector& supports, 33 | vector& q_batches, 34 | vector& s_batches, 35 | vector& neighbors_indices, 36 | vector& neighbors_sqdist, 37 | size_t n_neighbors); 38 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_neighbors/setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | from distutils.core import setup, Extension 6 | import numpy.distutils.misc_util 7 | 8 | # Adding OpenCV to project 9 | # ************************ 10 | 11 | # Adding sources of the project 12 | # ***************************** 13 | 14 | SOURCES = ["../cpp_utils/cloud/cloud.cpp", 15 | "neighbors/neighbors.cpp", 16 | "wrapper.cpp"] 17 | 18 | module = Extension(name="cpp_neighbors", 19 | sources=SOURCES, 20 | extra_compile_args=['-std=c++11', 21 | '-D_GLIBCXX_USE_CXX11_ABI=0']) 22 | 23 | 24 | setup(ext_modules=[module], include_dirs=numpy.distutils.misc_util.get_numpy_include_dirs()) 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_subsampling/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | py setup.py build_ext --inplace 3 | 4 | 5 | pause -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_subsampling/fps_subsampling/fps_subsampling.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "fps_subsampling.h" 3 | 4 | 5 | void fps_subsampling(vector& original_points, 6 | vector& subsampled_inds, 7 | int new_n, 8 | float min_d, 9 | int verbose) 10 | { 11 | 12 | // Initialize variables 13 | // ******************** 14 | 15 | // New max number of points 16 | if (new_n < 0) 17 | new_n = original_points.size(); 18 | 19 | // square radius 20 | float min_d2 = 0; 21 | if (min_d > 0) 22 | min_d2 = min_d * min_d; 23 | 24 | // Number of points in the cloud 25 | size_t N = original_points.size(); 26 | 27 | // Variables 28 | vector remaining_mask(original_points.size(), true); 29 | int best_j = 0; 30 | float best_d2 = 0; 31 | 32 | // Fisrt select a random point 33 | subsampled_inds.reserve(new_n); 34 | subsampled_inds.push_back(0); 35 | PointXYZ p0 = original_points[0]; 36 | remaining_mask[0] = false; 37 | 38 | 39 | // Init remaining distances 40 | // ************************ 41 | 42 | vector all_d2(original_points.size()); 43 | int j = 0; 44 | for (auto& p : original_points) 45 | { 46 | // Get distance to first point 47 | float d2 = (p0 - p).sq_norm(); 48 | all_d2[j] = d2; 49 | 50 | // Update max dist 51 | if (d2 > best_d2) 52 | { 53 | best_j = j; 54 | best_d2 = d2; 55 | } 56 | j++; 57 | } 58 | 59 | 60 | // Start FPS 61 | // ********* 62 | 63 | for (int i = 1; i < new_n; i++) 64 | { 65 | // Stop if we reach minimum distance 66 | if (best_d2 < min_d2) 67 | break; 68 | 69 | // Add the fursthest point 70 | subsampled_inds.push_back(best_j); 71 | p0 = original_points[best_j]; 72 | 73 | // Remove from remaining 74 | remaining_mask[best_j] = false; 75 | 76 | // Loop on all remaining points to get the distances 77 | j = 0; 78 | best_d2 = 0; 79 | for (auto& p : original_points) 80 | { 81 | if (remaining_mask[j]) 82 | { 83 | // Update all the minimum distances 84 | float d2 = (p0 - p).sq_norm(); 85 | if (d2 < all_d2[j]) 86 | all_d2[j] = d2; 87 | else 88 | d2 = all_d2[j]; 89 | 90 | // Update max of the min distances 91 | if (d2 > best_d2) 92 | { 93 | best_j = j; 94 | best_d2 = d2; 95 | } 96 | } 97 | j++; 98 | } 99 | } 100 | 101 | return; 102 | } 103 | 104 | 105 | 106 | // void fps_subsampling_2(vector& original_points, 107 | // vector& subsampled_inds, 108 | // int new_n, 109 | // float sampleDl, 110 | // int verbose) 111 | // { 112 | 113 | // // Initialize variables 114 | // // ****************** 115 | 116 | // // square radius 117 | // float min_d2 = sampleDl * sampleDl; 118 | 119 | // // Number of points in the cloud 120 | // size_t N = original_points.size(); 121 | 122 | // // Variables 123 | // vector remaining_mask(original_points.size(), true); 124 | 125 | 126 | // // Variables 127 | // vector remaining_i(original_points.size()); 128 | // std::iota(remaining_i.begin(), remaining_i.end(), 0); 129 | // int best_j = 0; 130 | // float best_d2 = 0; 131 | 132 | // // Fisrt select a random point 133 | // subsampled_inds.reserve(new_n); 134 | // subsampled_inds.push_back(0); 135 | // PointXYZ p0 = original_points[0]; 136 | // remaining_i.erase(remaining_i.begin()); 137 | 138 | 139 | // // Init remaining distances 140 | // // ************************ 141 | 142 | // vector all_d2(original_points.size()); 143 | // int j = 0; 144 | // for (auto& p : original_points) 145 | // { 146 | // // Get distance to first point 147 | // float d2 = (p0 - p).sq_norm(); 148 | // all_d2[j] = d2; 149 | 150 | // // Update max dist 151 | // if (d2 > best_d2) 152 | // { 153 | // best_j = j; 154 | // best_d2 = d2; 155 | // } 156 | // j++; 157 | // } 158 | 159 | 160 | // // Start FPS 161 | // // ********* 162 | 163 | // for (int i = 1; i < new_n; i++) 164 | // { 165 | // // Add the fursthest point 166 | // subsampled_inds.push_back(best_j); 167 | 168 | // // Remove from remaining 169 | // remaining_i.erase(remaining_i.begin() + best_j); 170 | // remaining_d2.erase(remaining_i.begin() + best_j); 171 | 172 | 173 | 174 | // // Loop on all reamining points to get the distances 175 | // j = 0; 176 | // for (auto& p : remaining_i) 177 | // { 178 | // float d2 = (p0 - p).sq_norm(); 179 | // if (d2 < remaining_d2[j]) 180 | // remaining_d2[j] = d2; 181 | // j++; 182 | // } 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | // } 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | // // Limits of the cloud 220 | // PointXYZ minCorner = min_point(original_points); 221 | // PointXYZ maxCorner = max_point(original_points); 222 | // PointXYZ originCorner = floor(minCorner * (1/sampleDl)) * sampleDl; 223 | 224 | // // Dimensions of the grid 225 | // size_t sampleNX = (size_t)floor((maxCorner.x - originCorner.x) / sampleDl) + 1; 226 | // size_t sampleNY = (size_t)floor((maxCorner.y - originCorner.y) / sampleDl) + 1; 227 | // //size_t sampleNZ = (size_t)floor((maxCorner.z - originCorner.z) / sampleDl) + 1; 228 | 229 | 230 | 231 | // // Create the sampled map 232 | // // ********************** 233 | 234 | // // Verbose parameters 235 | // int i = 0; 236 | // int nDisp = N / 100; 237 | 238 | // // Initialize variables 239 | // size_t iX, iY, iZ, mapIdx; 240 | // unordered_map data; 241 | 242 | // for (auto& p : original_points) 243 | // { 244 | // // Position of point in sample map 245 | // iX = (size_t)floor((p.x - originCorner.x) / sampleDl); 246 | // iY = (size_t)floor((p.y - originCorner.y) / sampleDl); 247 | // iZ = (size_t)floor((p.z - originCorner.z) / sampleDl); 248 | // mapIdx = iX + sampleNX*iY + sampleNX*sampleNY*iZ; 249 | 250 | // // If not already created, create key 251 | // if (data.count(mapIdx) < 1) 252 | // data.emplace(mapIdx, SampledData(fdim, ldim)); 253 | 254 | // // Fill the sample map 255 | // if (use_feature && use_classes) 256 | // data[mapIdx].update_all(p, original_features.begin() + i * fdim, original_classes.begin() + i * ldim); 257 | // else if (use_feature) 258 | // data[mapIdx].update_features(p, original_features.begin() + i * fdim); 259 | // else if (use_classes) 260 | // data[mapIdx].update_classes(p, original_classes.begin() + i * ldim); 261 | // else 262 | // data[mapIdx].update_points(p); 263 | 264 | // // Display 265 | // i++; 266 | // if (verbose > 1 && i%nDisp == 0) 267 | // std::cout << "\rSampled Map : " << std::setw(3) << i / nDisp << "%"; 268 | 269 | // } 270 | 271 | // // Divide for barycentre and transfer to a vector 272 | // subsampled_points.reserve(data.size()); 273 | // if (use_feature) 274 | // subsampled_features.reserve(data.size() * fdim); 275 | // if (use_classes) 276 | // subsampled_classes.reserve(data.size() * ldim); 277 | // for (auto& v : data) 278 | // { 279 | // subsampled_points.push_back(v.second.point * (1.0 / v.second.count)); 280 | // if (use_feature) 281 | // { 282 | // float count = (float)v.second.count; 283 | // transform(v.second.features.begin(), 284 | // v.second.features.end(), 285 | // v.second.features.begin(), 286 | // [count](float f) { return f / count;}); 287 | // subsampled_features.insert(subsampled_features.end(),v.second.features.begin(),v.second.features.end()); 288 | // } 289 | // if (use_classes) 290 | // { 291 | // for (int i = 0; i < ldim; i++) 292 | // subsampled_classes.push_back(max_element(v.second.labels[i].begin(), v.second.labels[i].end(), 293 | // [](const pair&a, const pair&b){return a.second < b.second;})->first); 294 | // } 295 | // } 296 | 297 | // return; 298 | // } 299 | 300 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_subsampling/fps_subsampling/fps_subsampling.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | void fps_subsampling(vector& original_points, 11 | vector& subsampled_inds, 12 | int new_n, 13 | float min_d, 14 | int verbose); 15 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_subsampling/grid_subsampling/grid_subsampling.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | class SampledData 11 | { 12 | public: 13 | 14 | // Elements 15 | // ******** 16 | 17 | int count; 18 | PointXYZ point; 19 | vector features; 20 | vector> labels; 21 | 22 | 23 | // Methods 24 | // ******* 25 | 26 | // Constructor 27 | SampledData() 28 | { 29 | count = 0; 30 | point = PointXYZ(); 31 | } 32 | 33 | SampledData(const size_t fdim, const size_t ldim) 34 | { 35 | count = 0; 36 | point = PointXYZ(); 37 | features = vector(fdim); 38 | labels = vector>(ldim); 39 | } 40 | 41 | // Method Update 42 | void update_all(const PointXYZ p, vector::iterator f_begin, vector::iterator l_begin) 43 | { 44 | count += 1; 45 | point += p; 46 | transform (features.begin(), features.end(), f_begin, features.begin(), plus()); 47 | int i = 0; 48 | for(vector::iterator it = l_begin; it != l_begin + labels.size(); ++it) 49 | { 50 | labels[i][*it] += 1; 51 | i++; 52 | } 53 | return; 54 | } 55 | void update_features(const PointXYZ p, vector::iterator f_begin) 56 | { 57 | count += 1; 58 | point += p; 59 | transform (features.begin(), features.end(), f_begin, features.begin(), plus()); 60 | return; 61 | } 62 | void update_classes(const PointXYZ p, vector::iterator l_begin) 63 | { 64 | count += 1; 65 | point += p; 66 | int i = 0; 67 | for(vector::iterator it = l_begin; it != l_begin + labels.size(); ++it) 68 | { 69 | labels[i][*it] += 1; 70 | i++; 71 | } 72 | return; 73 | } 74 | void update_points(const PointXYZ p) 75 | { 76 | count += 1; 77 | point += p; 78 | return; 79 | } 80 | }; 81 | 82 | void grid_pools_and_ups(vector &original_points, 83 | vector &subsampled_points, 84 | vector &poolings, 85 | vector &upsamplings, 86 | float sampleDl); 87 | 88 | void grid_subsampling(vector& original_points, 89 | vector& subsampled_points, 90 | vector& original_features, 91 | vector& subsampled_features, 92 | vector& original_classes, 93 | vector& subsampled_classes, 94 | float sampleDl, 95 | int verbose); 96 | 97 | void batch_grid_subsampling(vector& original_points, 98 | vector& subsampled_points, 99 | vector& original_features, 100 | vector& subsampled_features, 101 | vector& original_classes, 102 | vector& subsampled_classes, 103 | vector& original_batches, 104 | vector& subsampled_batches, 105 | float sampleDl, 106 | int max_p); 107 | 108 | void partition_batch_grid(vector& original_points, 109 | vector& subsampled_points, 110 | vector& original_batches, 111 | vector& subsampled_batches, 112 | vector& pooling_inds, 113 | vector& upsampling_inds, 114 | float sampleDl); 115 | 116 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_subsampling/setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | from distutils.core import setup, Extension 6 | import numpy.distutils.misc_util 7 | 8 | # Adding OpenCV to project 9 | # ************************ 10 | 11 | # Adding sources of the project 12 | # ***************************** 13 | 14 | SOURCES = ["../cpp_utils/cloud/cloud.cpp", 15 | "grid_subsampling/grid_subsampling.cpp", 16 | "fps_subsampling/fps_subsampling.cpp", 17 | "wrapper.cpp"] 18 | 19 | module = Extension(name="cpp_subsampling", 20 | sources=SOURCES, 21 | extra_compile_args=['-std=c++11', 22 | '-D_GLIBCXX_USE_CXX11_ABI=0']) 23 | 24 | 25 | setup(ext_modules=[module], include_dirs=numpy.distutils.misc_util.get_numpy_include_dirs()) 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_utils/cloud/cloud.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud source : 13 | // Define usefull Functions/Methods 14 | // 15 | //---------------------------------------------------- 16 | // 17 | // Hugues THOMAS - 10/02/2017 18 | // 19 | 20 | 21 | #include "cloud.h" 22 | 23 | 24 | // Getters 25 | // ******* 26 | 27 | PointXYZ max_point(std::vector points) 28 | { 29 | // Initialize limits 30 | PointXYZ maxP(points[0]); 31 | 32 | // Loop over all points 33 | for (auto p : points) 34 | { 35 | if (p.x > maxP.x) 36 | maxP.x = p.x; 37 | 38 | if (p.y > maxP.y) 39 | maxP.y = p.y; 40 | 41 | if (p.z > maxP.z) 42 | maxP.z = p.z; 43 | } 44 | 45 | return maxP; 46 | } 47 | 48 | PointXYZ min_point(std::vector points) 49 | { 50 | // Initialize limits 51 | PointXYZ minP(points[0]); 52 | 53 | // Loop over all points 54 | for (auto p : points) 55 | { 56 | if (p.x < minP.x) 57 | minP.x = p.x; 58 | 59 | if (p.y < minP.y) 60 | minP.y = p.y; 61 | 62 | if (p.z < minP.z) 63 | minP.z = p.z; 64 | } 65 | 66 | return minP; 67 | } -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/cpp_wrappers/cpp_utils/cloud/cloud.h: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud header 13 | // 14 | //---------------------------------------------------- 15 | // 16 | // Hugues THOMAS - 10/02/2017 17 | // 18 | 19 | 20 | # pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | 33 | 34 | 35 | 36 | // Point class 37 | // *********** 38 | 39 | 40 | class PointXYZ 41 | { 42 | public: 43 | 44 | // Elements 45 | // ******** 46 | 47 | float x, y, z; 48 | 49 | 50 | // Methods 51 | // ******* 52 | 53 | // Constructor 54 | PointXYZ() { x = 0; y = 0; z = 0; } 55 | PointXYZ(float x0, float y0, float z0) { x = x0; y = y0; z = z0; } 56 | 57 | // array type accessor 58 | float operator [] (int i) const 59 | { 60 | if (i == 0) return x; 61 | else if (i == 1) return y; 62 | else return z; 63 | } 64 | 65 | // opperations 66 | float dot(const PointXYZ P) const 67 | { 68 | return x * P.x + y * P.y + z * P.z; 69 | } 70 | 71 | float sq_norm() 72 | { 73 | return x*x + y*y + z*z; 74 | } 75 | 76 | PointXYZ cross(const PointXYZ P) const 77 | { 78 | return PointXYZ(y*P.z - z*P.y, z*P.x - x*P.z, x*P.y - y*P.x); 79 | } 80 | 81 | PointXYZ& operator+=(const PointXYZ& P) 82 | { 83 | x += P.x; 84 | y += P.y; 85 | z += P.z; 86 | return *this; 87 | } 88 | 89 | PointXYZ& operator-=(const PointXYZ& P) 90 | { 91 | x -= P.x; 92 | y -= P.y; 93 | z -= P.z; 94 | return *this; 95 | } 96 | 97 | PointXYZ& operator*=(const float& a) 98 | { 99 | x *= a; 100 | y *= a; 101 | z *= a; 102 | return *this; 103 | } 104 | }; 105 | 106 | 107 | // Point Opperations 108 | // ***************** 109 | 110 | inline PointXYZ operator + (const PointXYZ A, const PointXYZ B) 111 | { 112 | return PointXYZ(A.x + B.x, A.y + B.y, A.z + B.z); 113 | } 114 | 115 | inline PointXYZ operator - (const PointXYZ A, const PointXYZ B) 116 | { 117 | return PointXYZ(A.x - B.x, A.y - B.y, A.z - B.z); 118 | } 119 | 120 | inline PointXYZ operator * (const PointXYZ P, const float a) 121 | { 122 | return PointXYZ(P.x * a, P.y * a, P.z * a); 123 | } 124 | 125 | inline PointXYZ operator * (const float a, const PointXYZ P) 126 | { 127 | return PointXYZ(P.x * a, P.y * a, P.z * a); 128 | } 129 | 130 | inline std::ostream& operator << (std::ostream& os, const PointXYZ P) 131 | { 132 | return os << "[" << P.x << ", " << P.y << ", " << P.z << "]"; 133 | } 134 | 135 | inline bool operator == (const PointXYZ A, const PointXYZ B) 136 | { 137 | return A.x == B.x && A.y == B.y && A.z == B.z; 138 | } 139 | 140 | inline PointXYZ floor(const PointXYZ P) 141 | { 142 | return PointXYZ(std::floor(P.x), std::floor(P.y), std::floor(P.z)); 143 | } 144 | 145 | 146 | PointXYZ max_point(std::vector points); 147 | PointXYZ min_point(std::vector points); 148 | 149 | 150 | struct PointCloud 151 | { 152 | 153 | std::vector pts; 154 | 155 | // Must return the number of data points 156 | inline size_t kdtree_get_point_count() const { return pts.size(); } 157 | 158 | // Returns the dim'th component of the idx'th point in the class: 159 | // Since this is inlined and the "dim" argument is typically an immediate value, the 160 | // "if/else's" are actually solved at compile time. 161 | inline float kdtree_get_pt(const size_t idx, const size_t dim) const 162 | { 163 | if (dim == 0) return pts[idx].x; 164 | else if (dim == 1) return pts[idx].y; 165 | else return pts[idx].z; 166 | } 167 | 168 | // Optional bounding-box computation: return false to default to a standard bbox computation loop. 169 | // Return true if the BBOX was already computed by the class and returned in "bb" so it can be avoided to redo it again. 170 | // Look at bb.size() to find out the expected dimensionality (e.g. 2 or 3 for point clouds) 171 | template 172 | bool kdtree_get_bbox(BBOX& /* bb */) const { return false; } 173 | 174 | }; 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/batch_conversion.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: batch_conversion.py 10 | # > batch conversion utilities 11 | # 12 | 13 | 14 | from typing import List, Optional, Tuple 15 | 16 | import torch 17 | from torch import Tensor 18 | 19 | @torch.no_grad() 20 | def _get_indices_from_lengths(lengths: Tensor, max_length: int) -> Tensor: 21 | """Compute the indices in flattened batch tensor from the lengths in pack mode.""" 22 | length_list = lengths.detach().cpu().numpy().tolist() 23 | chunks = [(i * max_length, i * max_length + length) for i, length in enumerate(length_list)] 24 | indices = torch.cat([torch.arange(i1, i2) for i1, i2 in chunks], dim=0).to(lengths.device) 25 | return indices 26 | 27 | @torch.no_grad() 28 | def _get_slices_from_lengths(lengths: Tensor) -> Tensor: 29 | """Compute the slices indices from lengths.""" 30 | batch_end_index = torch.cumsum(lengths, dim=0) 31 | batch_start_index = batch_end_index - lengths 32 | batch_slices_index = torch.stack((batch_start_index, batch_end_index), dim=1) 33 | return batch_slices_index 34 | 35 | 36 | def batch_to_pack(batch_tensor: Tensor, masks: Optional[Tensor] = None) -> Tuple[Tensor, Tensor]: 37 | """Convert Tensor from batch mode to stack mode with masks. 38 | Args: 39 | batch_tensor (Tensor): the input tensor in batch mode (B, N, C) or (B, N). 40 | masks (BoolTensor): the masks of items of each sample in the batch (B, N). 41 | Returns: 42 | A Tensor in pack mode in the shape of (M, C) or (M). 43 | A LongTensor of the length of each sample in the batch in the shape of (B). 44 | """ 45 | if masks is not None: 46 | pack_tensor = batch_tensor[masks] 47 | lengths = masks.sum(dim=1) 48 | else: 49 | lengths = torch.full(size=(batch_tensor.shape[0],), fill_value=batch_tensor.shape[1], dtype=torch.long).to(batch_tensor.device) 50 | pack_tensor = batch_tensor 51 | return pack_tensor, lengths 52 | 53 | 54 | def pack_to_batch(pack_tensor: Tensor, lengths: Tensor, max_length=None, fill_value=0.0) -> Tuple[Tensor, Tensor]: 55 | """Convert Tensor from pack mode to batch mode. 56 | Args: 57 | pack_tensor (Tensor): The input tensors in pack mode (M, C). 58 | lengths (LongTensor): The number of items of each sample in the batch (B) 59 | max_length (int, optional): The maximal length of each sample in the batch. 60 | fill_value (float or int or bool): The default value in the empty regions. Default: 0. 61 | Returns: 62 | A Tensor in stack mode in the shape of (B, N, C), where N is max(lengths). 63 | A BoolTensor of the masks of each sample in the batch in the shape of (B, N). 64 | """ 65 | 66 | # Get batch size and maximum length of elements 67 | batch_size = lengths.shape[0] 68 | if max_length is None: 69 | max_length = lengths.max().item() 70 | 71 | # Get the batch index for each packed point 72 | tgt_indices = _get_indices_from_lengths(lengths, max_length) 73 | 74 | # Create batch tensor (B*N, C) 75 | num_channels = pack_tensor.shape[1] 76 | batch_tensor = pack_tensor.new_full(size=(batch_size * max_length, num_channels), fill_value=fill_value) 77 | batch_tensor[tgt_indices] = pack_tensor 78 | 79 | # Reshape batch tensor (B, N, C) 80 | batch_tensor = batch_tensor.view(batch_size, max_length, num_channels) 81 | 82 | # Get valid points masks 83 | masks = torch.zeros(size=(batch_size * max_length,), dtype=torch.bool).to(pack_tensor.device) 84 | masks[tgt_indices] = True 85 | masks = masks.view(batch_size, max_length) 86 | 87 | return batch_tensor, masks 88 | 89 | 90 | def pack_to_list(pack_tensor: Tensor, lengths: Tensor) -> List[Tensor]: 91 | """Convert Tensor from pack mode to list mode. 92 | Args: 93 | pack_tensor (Tensor): The input tensors in pack mode (M, C). 94 | lengths (LongTensor): The number of items of each sample in the batch (B) 95 | Returns: 96 | A List of Tensors of length B, where each element has a shape (?, C). 97 | """ 98 | 99 | # Get slices indices 100 | b_slices = _get_slices_from_lengths(lengths) 101 | 102 | # Slice packed tensor in a list of tensors 103 | batch_tensor_list = [pack_tensor[b_slice[0]:b_slice[1]] for b_slice in b_slices] 104 | 105 | return batch_tensor_list 106 | 107 | 108 | def list_to_pack(tensor_list: List[Tensor]) -> Tuple[Tensor, Tensor]: 109 | """Convert Tensor from list mode to stack mode. 110 | Args: 111 | tensor_list (Tensor): the input tensors in a list. 112 | Returns: 113 | A Tensor in pack mode in the shape of (M, C) or (M). 114 | A LongTensor of the length of each sample in the batch in the shape of (B). 115 | """ 116 | pack_tensor = torch.cat(tensor_list, dim=0) 117 | lengths = torch.LongTensor([tens.shape[0] for tens in tensor_list]).to(pack_tensor.device) 118 | return pack_tensor, lengths -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/cpp_funcs.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: cpp_funcs.py 10 | # > cpp functions wrapped in python 11 | # 12 | 13 | 14 | import numpy as np 15 | import torch 16 | 17 | # Subsampling extension 18 | from pointcept.models.kpconvx.cpp_wrappers.cpp_subsampling import cpp_subsampling 19 | 20 | 21 | # ---------------------------------------------------------------------------------------------------------------------- 22 | # 23 | # Utility functions 24 | # \***********************/ 25 | # 26 | 27 | 28 | def batch_grid_partition(points, batches_len, sampleDl=0.1): 29 | """ 30 | CPP wrapper for a grid subsampling (method = barycenter for points and features). 31 | Also returns pooling and upsampling inds 32 | :param points: (N, 3) matrix of input points 33 | :param sampleDl: parameter defining the size of grid voxels 34 | :return: subsampled points, with features and/or labels depending of the input 35 | """ 36 | 37 | if torch.is_tensor(points): 38 | points1 = points.cpu().numpy() 39 | batches_len1 = batches_len.cpu().numpy() 40 | 41 | s_points, s_len, pools, ups = cpp_subsampling.batch_grid_partitionning(points1, 42 | batches_len1, 43 | sampleDl=sampleDl) 44 | 45 | if torch.is_tensor(points): 46 | s_points = torch.from_numpy(s_points).to(points.device) 47 | s_len = torch.from_numpy(s_len).to(points.device) 48 | pools = torch.from_numpy(pools).to(points.device) 49 | ups = torch.from_numpy(ups).to(points.device) 50 | 51 | return s_points, s_len, pools, ups 52 | 53 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_015_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_015_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_016_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_016_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_022_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_022_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_043_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_043_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_045_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_045_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_058_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_058_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_062_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_062_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_103_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_103_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_105_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_105_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/dispositions/k_125_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpconvx/utils/dispositions/k_125_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpconvx/utils/torch_pyramid.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: torch_pyramid.py 10 | # > input pyramid strucure of subsamplings and neighbors 11 | # 12 | 13 | 14 | import torch 15 | from torch import Tensor 16 | from typing import Tuple, List 17 | from easydict import EasyDict 18 | 19 | 20 | from pointcept.models.kpconvx.utils.gpu_subsampling import subsample_pack_batch 21 | from pointcept.models.kpconvx.utils.gpu_neigbors import radius_search_pack_mode, keops_radius_count 22 | 23 | from pointcept.models.kpconvx.utils.cpp_funcs import batch_grid_partition 24 | 25 | from pointcept.models.utils import offset2batch, batch2offset 26 | from torch_geometric.nn.pool import voxel_grid 27 | from torch_scatter import segment_csr 28 | import pointops 29 | 30 | 31 | def scatter_grid_pool(points, lengths, grid_size, feat=None, start=None): 32 | 33 | 34 | # Get offset and batch indices 35 | offset = torch.cumsum(lengths, dim=0) 36 | offset0 = torch.cat([torch.zeros(1, dtype=offset.dtype, device=offset.device), offset], dim=0) 37 | batch = offset2batch(offset) 38 | 39 | start = segment_csr(points, offset0, reduce="min") if start is None else start 40 | 41 | vox_cluster = voxel_grid(pos=points - start[batch], size=grid_size, batch=batch, start=0) 42 | unique, cluster, counts = torch.unique(vox_cluster, sorted=True, return_inverse=True, return_counts=True) 43 | 44 | _, sorted_cluster_indices = torch.sort(cluster) 45 | idx_ptr = torch.cat([counts.new_zeros(1), torch.cumsum(counts, dim=0)]) 46 | sub_points = segment_csr(points[sorted_cluster_indices], idx_ptr, reduce="mean") 47 | batch = batch[idx_ptr[:-1]] 48 | offset = batch2offset(batch) 49 | 50 | offset = torch.cat([torch.zeros(1, dtype=offset.dtype, device=offset.device), offset], dim=0) 51 | sub_lengths = offset[1:] - offset[:-1] 52 | 53 | # Optionnaly apply maxpool to features 54 | if feat is not None: 55 | feat = segment_csr(feat[sorted_cluster_indices], idx_ptr, reduce="max") 56 | 57 | return sub_points, sub_lengths, sorted_cluster_indices, idx_ptr, cluster 58 | 59 | 60 | # ---------------------------------------------------------------------------------------------------------------------- 61 | # 62 | # Input pyramid functions 63 | # \*****************************/ 64 | # 65 | 66 | 67 | 68 | 69 | @torch.no_grad() 70 | def build_base_pyramid(points: Tensor, 71 | lengths: Tensor): 72 | """ 73 | Only build the base of the graph pyramid, consisting of: 74 | > The subampled points for the first layer, in pack mode. 75 | > The lengths of the pack at first layer. 76 | """ 77 | 78 | # Results lists 79 | pyramid = EasyDict() 80 | pyramid.points = [] 81 | pyramid.lengths = [] 82 | pyramid.neighbors = [] 83 | pyramid.pools = [] 84 | pyramid.upsamples = [] 85 | pyramid.up_distances = [] 86 | 87 | pyramid.points.append(points) 88 | pyramid.lengths.append(lengths) 89 | 90 | return pyramid 91 | 92 | 93 | def fill_pyramid(pyramid: EasyDict, 94 | num_layers: int, 95 | sub_size: float, 96 | search_radius: float, 97 | radius_scaling: float, 98 | neighbor_limits: List[int], 99 | upsample_n: int = 1, 100 | sub_mode: str = 'grid', 101 | grid_pool_mode: bool = False): 102 | """ 103 | Fill the graph pyramid, with: 104 | > The subampled points for each layer, in pack mode. 105 | > The lengths of the pack for each layer. 106 | > The neigbors indices for convolutions. 107 | > The pooling indices (neighbors from one layer to another). 108 | > The upsampling indices (opposite of pooling indices). 109 | """ 110 | 111 | 112 | # Check if pyramid is already full 113 | if len(pyramid.neighbors) > 0: 114 | raise ValueError('Trying to fill a pyramid that already have neighbors') 115 | if len(pyramid.pools) > 0: 116 | raise ValueError('Trying to fill a pyramid that already have pools') 117 | if len(pyramid.upsamples) > 0: 118 | raise ValueError('Trying to fill a pyramid that already have upsamples') 119 | if len(pyramid.points) < 1: 120 | raise ValueError('Trying to fill a pyramid that does not have first points') 121 | if len(pyramid.lengths) < 1: 122 | raise ValueError('Trying to fill a pyramid that does not have first lengths') 123 | if len(pyramid.points) > 1: 124 | raise ValueError('Trying to fill a pyramid that already have more than one points') 125 | if len(pyramid.lengths) > 1: 126 | raise ValueError('Trying to fill a pyramid that already have more than one lengths') 127 | 128 | # Grid pool mode can only happen if method is grid 129 | grid_pool_mode = sub_mode == 'grid' and grid_pool_mode 130 | 131 | # Choose neighbor function depending on device 132 | if 'cuda' in pyramid.points[0].device.type: 133 | neighb_func = radius_search_pack_mode 134 | else: 135 | # neighb_func = batch_radius_neighbors 136 | neighb_func = batch_knn_neighbors 137 | 138 | # Subsample all point clouds on GPU 139 | points0 = pyramid.points[0] 140 | lengths0 = pyramid.lengths[0] 141 | for i in range(num_layers): 142 | 143 | if i > 0: 144 | 145 | if grid_pool_mode: 146 | 147 | sub_points, sub_lengths, pool_sorting, pool_ptr, upsamplings = scatter_grid_pool(points0, lengths0, sub_size) 148 | 149 | pyramid.pools.append((pool_sorting, pool_ptr)) 150 | pyramid.upsamples.append(upsamplings.reshape(-1, 1)) 151 | 152 | # sub_points, sub_lengths, poolings, upsamplings = batch_grid_partition(points0, lengths0, sub_size) 153 | # pyramid.pools.append(poolings) 154 | # pyramid.upsamples.append(upsamplings) 155 | 156 | 157 | points0 = sub_points 158 | lengths0 = sub_lengths 159 | 160 | else: 161 | sub_points, sub_lengths = subsample_pack_batch(points0, lengths0, sub_size, method=sub_mode) 162 | if sub_mode == 'fps': 163 | points0 = sub_points 164 | lengths0 = sub_lengths 165 | 166 | pyramid.points.append(sub_points) 167 | pyramid.lengths.append(sub_lengths) 168 | 169 | if sub_size > 0: 170 | sub_size *= radius_scaling 171 | 172 | 173 | # Find all neighbors 174 | for i in range(num_layers): 175 | 176 | # Get current points 177 | cur_points = pyramid.points[i] 178 | cur_lengths = pyramid.lengths[i] 179 | 180 | # # Get convolution indices 181 | # neighbors = neighb_func(cur_points, cur_points, cur_lengths, cur_lengths, search_radius, neighbor_limits[i]) 182 | # pyramid.neighbors.append(neighbors) 183 | 184 | # Get knn with pointops 185 | offset = torch.cumsum(cur_lengths, dim=0) 186 | reference_index, test0 = pointops.knn_query(neighbor_limits[i], cur_points, offset) 187 | # Any index == -1 becomes the max index 188 | reference_index[reference_index == -1] = cur_points.shape[0] 189 | pyramid.neighbors.append(reference_index) 190 | 191 | # Relation with next layer 192 | if not grid_pool_mode and i < num_layers - 1: 193 | sub_points = pyramid.points[i + 1] 194 | sub_lengths = pyramid.lengths[i + 1] 195 | 196 | # Get pooling indices 197 | subsampling_inds = neighb_func(sub_points, cur_points, sub_lengths, cur_lengths, search_radius, neighbor_limits[i]) 198 | pyramid.pools.append(subsampling_inds) 199 | 200 | if upsample_n > 0: 201 | upsampling_inds, up_dists = neighb_func(cur_points, sub_points, cur_lengths, sub_lengths, search_radius * radius_scaling, upsample_n, return_dist=True) 202 | pyramid.upsamples.append(upsampling_inds) 203 | pyramid.up_distances.append(up_dists) 204 | 205 | 206 | # Increase radius for next layer 207 | search_radius *= radius_scaling 208 | 209 | # mean_dt = 1000 * (np.array(t[1:]) - np.array(t[:-1])) 210 | # message = ' ' * 2 211 | # for dt in mean_dt: 212 | # message += ' {:5.1f}'.format(dt) 213 | # print(message) 214 | 215 | return 216 | 217 | 218 | @torch.no_grad() 219 | def build_full_pyramid(points: Tensor, 220 | lengths: Tensor, 221 | num_layers: int, 222 | sub_size: float, 223 | search_radius: float, 224 | radius_scaling: float, 225 | neighbor_limits: List[int], 226 | upsample_n: int = 1, 227 | sub_mode: str = 'grid', 228 | grid_pool_mode: bool = False): 229 | """ 230 | Build the graph pyramid, consisting of: 231 | > The subampled points for each layer, in pack mode. 232 | > The lengths of the pack. 233 | > The neigbors indices for convolutions. 234 | > The pooling indices (neighbors from one layer to another). 235 | > The upsampling indices (opposite of pooling indices). 236 | """ 237 | 238 | pyramid = build_base_pyramid(points, lengths) 239 | 240 | fill_pyramid(pyramid, 241 | num_layers, 242 | sub_size, 243 | search_radius, 244 | radius_scaling, 245 | neighbor_limits, 246 | upsample_n, 247 | sub_mode, 248 | grid_pool_mode) 249 | 250 | return pyramid 251 | 252 | 253 | @torch.no_grad() 254 | def pyramid_neighbor_stats(points: Tensor, 255 | num_layers: int, 256 | sub_size: float, 257 | search_radius: float, 258 | radius_scaling: float, 259 | sub_mode: str = 'grid'): 260 | """ 261 | Function used for neighbors calibration. Return the average number of neigbors at each layer. 262 | Args: 263 | points (Tensor): initial layer points (M, 3). 264 | num_layers (int): number of layers. 265 | sub_size (float): initial subsampling size 266 | radius (float): search radius. 267 | sub_mode (str): the subsampling method ('grid', 'ph', 'fps'). 268 | Returns: 269 | counts_list (List[Tensor]): All neigbors counts at each layers 270 | """ 271 | 272 | counts_list = [] 273 | lengths = [points.shape[0]] 274 | for i in range(num_layers): 275 | if i > 0: 276 | points, lengths = subsample_pack_batch(points, lengths, sub_size, method=sub_mode) 277 | counts = keops_radius_count(points, points, search_radius) 278 | counts_list.append(counts) 279 | if sub_size > 0: 280 | sub_size *= radius_scaling 281 | search_radius *= radius_scaling 282 | return counts_list -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | 6 | from .kpnext_base import KPNeXt 7 | -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_015_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_015_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_016_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_016_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_022_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_022_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_043_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_043_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_045_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_045_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_058_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_058_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_062_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_062_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_103_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_103_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_105_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_105_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/dispositions/k_125_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Pointcept-wrapper/models/kpnext/dispositions/k_125_center_3D_0.ply -------------------------------------------------------------------------------- /Pointcept-wrapper/models/kpnext/torch_pyramid.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: torch_pyramid.py 10 | # > input pyramid strucure of subsamplings and neighbors 11 | # 12 | 13 | 14 | import torch 15 | from torch import Tensor 16 | from typing import Tuple, List 17 | from easydict import EasyDict 18 | 19 | from utils.gpu_subsampling import subsample_pack_batch 20 | 21 | from utils.gpu_neigbors import radius_search_pack_mode, keops_radius_count 22 | from utils.cpp_funcs import batch_knn_neighbors, batch_grid_partition 23 | 24 | 25 | 26 | # ---------------------------------------------------------------------------------------------------------------------- 27 | # 28 | # Input pyramid functions 29 | # \*****************************/ 30 | # 31 | 32 | 33 | 34 | 35 | @torch.no_grad() 36 | def build_base_pyramid(points: Tensor, 37 | lengths: Tensor): 38 | """ 39 | Only build the base of the graph pyramid, consisting of: 40 | > The subampled points for the first layer, in pack mode. 41 | > The lengths of the pack at first layer. 42 | """ 43 | 44 | # Results lists 45 | pyramid = EasyDict() 46 | pyramid.points = [] 47 | pyramid.lengths = [] 48 | pyramid.neighbors = [] 49 | pyramid.pools = [] 50 | pyramid.upsamples = [] 51 | pyramid.up_distances = [] 52 | 53 | pyramid.points.append(points) 54 | pyramid.lengths.append(lengths) 55 | 56 | return pyramid 57 | 58 | 59 | def fill_pyramid(pyramid: EasyDict, 60 | num_layers: int, 61 | sub_size: float, 62 | search_radius: float, 63 | radius_scaling: float, 64 | neighbor_limits: List[int], 65 | upsample_n: int = 1, 66 | sub_mode: str = 'grid', 67 | grid_pool_mode: bool = False): 68 | """ 69 | Fill the graph pyramid, with: 70 | > The subampled points for each layer, in pack mode. 71 | > The lengths of the pack for each layer. 72 | > The neigbors indices for convolutions. 73 | > The pooling indices (neighbors from one layer to another). 74 | > The upsampling indices (opposite of pooling indices). 75 | """ 76 | 77 | 78 | # Check if pyramid is already full 79 | if len(pyramid.neighbors) > 0: 80 | raise ValueError('Trying to fill a pyramid that already have neighbors') 81 | if len(pyramid.pools) > 0: 82 | raise ValueError('Trying to fill a pyramid that already have pools') 83 | if len(pyramid.upsamples) > 0: 84 | raise ValueError('Trying to fill a pyramid that already have upsamples') 85 | if len(pyramid.points) < 1: 86 | raise ValueError('Trying to fill a pyramid that does not have first points') 87 | if len(pyramid.lengths) < 1: 88 | raise ValueError('Trying to fill a pyramid that does not have first lengths') 89 | if len(pyramid.points) > 1: 90 | raise ValueError('Trying to fill a pyramid that already have more than one points') 91 | if len(pyramid.lengths) > 1: 92 | raise ValueError('Trying to fill a pyramid that already have more than one lengths') 93 | 94 | # Grid pool mode can only happen if method is grid 95 | grid_pool_mode = sub_mode == 'grid' and grid_pool_mode 96 | 97 | # Choose neighbor function depending on device 98 | if 'cuda' in pyramid.points[0].device.type: 99 | neighb_func = radius_search_pack_mode 100 | else: 101 | # neighb_func = batch_radius_neighbors 102 | neighb_func = batch_knn_neighbors 103 | 104 | # Subsample all point clouds on GPU 105 | points0 = pyramid.points[0] 106 | lengths0 = pyramid.lengths[0] 107 | for i in range(num_layers): 108 | 109 | if i > 0: 110 | 111 | if grid_pool_mode: 112 | sub_points, sub_lengths, poolings, upsamplings = batch_grid_partition(points0, lengths0, sub_size) 113 | pyramid.pools.append(poolings) 114 | pyramid.upsamples.append(upsamplings) 115 | points0 = sub_points 116 | lengths0 = sub_lengths 117 | 118 | 119 | else: 120 | sub_points, sub_lengths = subsample_pack_batch(points0, lengths0, sub_size, method=sub_mode) 121 | if sub_mode == 'fps': 122 | points0 = sub_points 123 | lengths0 = sub_lengths 124 | 125 | pyramid.points.append(sub_points) 126 | pyramid.lengths.append(sub_lengths) 127 | 128 | if sub_size > 0: 129 | sub_size *= radius_scaling 130 | 131 | 132 | # Find all neighbors 133 | for i in range(num_layers): 134 | 135 | # Get current points 136 | cur_points = pyramid.points[i] 137 | cur_lengths = pyramid.lengths[i] 138 | 139 | # Get convolution indices 140 | neighbors = neighb_func(cur_points, cur_points, cur_lengths, cur_lengths, search_radius, neighbor_limits[i]) 141 | pyramid.neighbors.append(neighbors) 142 | 143 | # Relation with next layer 144 | if not grid_pool_mode and i < num_layers - 1: 145 | sub_points = pyramid.points[i + 1] 146 | sub_lengths = pyramid.lengths[i + 1] 147 | 148 | # Get pooling indices 149 | subsampling_inds = neighb_func(sub_points, cur_points, sub_lengths, cur_lengths, search_radius, neighbor_limits[i]) 150 | pyramid.pools.append(subsampling_inds) 151 | 152 | if upsample_n > 0: 153 | upsampling_inds, up_dists = neighb_func(cur_points, sub_points, cur_lengths, sub_lengths, search_radius * radius_scaling, upsample_n, return_dist=True) 154 | pyramid.upsamples.append(upsampling_inds) 155 | pyramid.up_distances.append(up_dists) 156 | 157 | 158 | # Increase radius for next layer 159 | search_radius *= radius_scaling 160 | 161 | # mean_dt = 1000 * (np.array(t[1:]) - np.array(t[:-1])) 162 | # message = ' ' * 2 163 | # for dt in mean_dt: 164 | # message += ' {:5.1f}'.format(dt) 165 | # print(message) 166 | 167 | return 168 | 169 | 170 | @torch.no_grad() 171 | def build_full_pyramid(points: Tensor, 172 | lengths: Tensor, 173 | num_layers: int, 174 | sub_size: float, 175 | search_radius: float, 176 | radius_scaling: float, 177 | neighbor_limits: List[int], 178 | upsample_n: int = 1, 179 | sub_mode: str = 'grid', 180 | grid_pool_mode: bool = False): 181 | """ 182 | Build the graph pyramid, consisting of: 183 | > The subampled points for each layer, in pack mode. 184 | > The lengths of the pack. 185 | > The neigbors indices for convolutions. 186 | > The pooling indices (neighbors from one layer to another). 187 | > The upsampling indices (opposite of pooling indices). 188 | """ 189 | 190 | pyramid = build_base_pyramid(points, lengths) 191 | 192 | fill_pyramid(pyramid, 193 | num_layers, 194 | sub_size, 195 | search_radius, 196 | radius_scaling, 197 | neighbor_limits, 198 | upsample_n, 199 | sub_mode, 200 | grid_pool_mode) 201 | 202 | return pyramid 203 | 204 | 205 | @torch.no_grad() 206 | def pyramid_neighbor_stats(points: Tensor, 207 | num_layers: int, 208 | sub_size: float, 209 | search_radius: float, 210 | radius_scaling: float, 211 | sub_mode: str = 'grid'): 212 | """ 213 | Function used for neighbors calibration. Return the average number of neigbors at each layer. 214 | Args: 215 | points (Tensor): initial layer points (M, 3). 216 | num_layers (int): number of layers. 217 | sub_size (float): initial subsampling size 218 | radius (float): search radius. 219 | sub_mode (str): the subsampling method ('grid', 'ph', 'fps'). 220 | Returns: 221 | counts_list (List[Tensor]): All neigbors counts at each layers 222 | """ 223 | 224 | counts_list = [] 225 | lengths = [points.shape[0]] 226 | for i in range(num_layers): 227 | if i > 0: 228 | points, lengths = subsample_pack_batch(points, lengths, sub_size, method=sub_mode) 229 | counts = keops_radius_count(points, points, search_radius) 230 | counts_list.append(counts) 231 | if sub_size > 0: 232 | sub_size *= radius_scaling 233 | search_radius *= radius_scaling 234 | return counts_list -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KPConvX: Modernizing Kernel Point Convolution with Kernel Attention 2 | 3 |
4 | teaser 5 |
6 | 7 | 18 | 19 |
20 | 21 | [Blog] 22 |      23 | [ArXiv] 24 | 25 |
26 | 27 | This repo is the official project repository of the paper **KPConvX: Modernizing Kernel Point Convolution with Kernel Attention**. 28 | 29 | 30 | ## Highlights 31 | 32 | - Nov 14, 2024: We release the trained models for ScanObjectNN and S3DIS datasets 33 | - Nov 14, 2024: KPConvX repository is officially realeased. We provide two implementation for KPConvX, an standalone training pipeline, and wrappers to use it in the Pointcept library. 34 | - Feb 28, 2024: KPConvX is accepted by CVPR 2024! 🎉 35 | 36 | 37 | ## Setup 38 | 39 | The setup instructions are different if you plan to use the Standalone or the Pointcept version: 40 | - [Setup the standalone version](./Standalone/). 41 | - [Setup the Pointcept version](./Pointcept-wrapper/). 42 | 43 | 44 | ## Citation 45 | If you found this code useful, please cite the following paper: 46 | ``` 47 | @inproceedings{thomas2024kpconvx, 48 | title={KPConvX: Modernizing Kernel Point Convolution with Kernel Attention}, 49 | author={Thomas, Hugues and Tsai, Yao-Hung Hubert and Barfoot, Timothy D and Zhang, Jian}, 50 | booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition}, 51 | pages={5525--5535}, 52 | year={2024} 53 | } 54 | ``` 55 | 56 | ## Acknowledgements 57 | Our codebase is built using multiple opensource contributions, please see [ACKNOWLEDGEMENTS](ACKNOWLEDGEMENTS) for more details. 58 | 59 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_neighbors/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | py setup.py build_ext --inplace 3 | 4 | 5 | pause -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_neighbors/neighbors/neighbors.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | #include "../../cpp_utils/nanoflann/nanoflann.hpp" 5 | 6 | #include 7 | #include 8 | 9 | using namespace std; 10 | 11 | 12 | void ordered_neighbors(vector& queries, 13 | vector& supports, 14 | vector& neighbors_indices, 15 | float radius); 16 | 17 | void batch_ordered_neighbors(vector& queries, 18 | vector& supports, 19 | vector& q_batches, 20 | vector& s_batches, 21 | vector& neighbors_indices, 22 | float radius); 23 | 24 | void batch_nanoflann_neighbors(vector& queries, 25 | vector& supports, 26 | vector& q_batches, 27 | vector& s_batches, 28 | vector& neighbors_indices, 29 | float radius); 30 | 31 | void batch_nanoflann_knns(vector& queries, 32 | vector& supports, 33 | vector& q_batches, 34 | vector& s_batches, 35 | vector& neighbors_indices, 36 | vector& neighbors_sqdist, 37 | size_t n_neighbors); 38 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_neighbors/setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | from distutils.core import setup, Extension 6 | import numpy.distutils.misc_util 7 | 8 | # Adding OpenCV to project 9 | # ************************ 10 | 11 | # Adding sources of the project 12 | # ***************************** 13 | 14 | SOURCES = ["../cpp_utils/cloud/cloud.cpp", 15 | "neighbors/neighbors.cpp", 16 | "wrapper.cpp"] 17 | 18 | module = Extension(name="cpp_neighbors", 19 | sources=SOURCES, 20 | extra_compile_args=['-std=c++11', 21 | '-D_GLIBCXX_USE_CXX11_ABI=0']) 22 | 23 | 24 | setup(ext_modules=[module], include_dirs=numpy.distutils.misc_util.get_numpy_include_dirs()) 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_subsampling/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | py setup.py build_ext --inplace 3 | 4 | 5 | pause -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_subsampling/fps_subsampling/fps_subsampling.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "fps_subsampling.h" 3 | 4 | 5 | void fps_subsampling(vector& original_points, 6 | vector& subsampled_inds, 7 | int new_n, 8 | float min_d, 9 | int verbose) 10 | { 11 | 12 | // Initialize variables 13 | // ******************** 14 | 15 | // New max number of points 16 | if (new_n < 0) 17 | new_n = original_points.size(); 18 | 19 | // square radius 20 | float min_d2 = 0; 21 | if (min_d > 0) 22 | min_d2 = min_d * min_d; 23 | 24 | // Number of points in the cloud 25 | size_t N = original_points.size(); 26 | 27 | // Variables 28 | vector remaining_mask(original_points.size(), true); 29 | int best_j = 0; 30 | float best_d2 = 0; 31 | 32 | // Fisrt select a random point 33 | subsampled_inds.reserve(new_n); 34 | subsampled_inds.push_back(0); 35 | PointXYZ p0 = original_points[0]; 36 | remaining_mask[0] = false; 37 | 38 | 39 | // Init remaining distances 40 | // ************************ 41 | 42 | vector all_d2(original_points.size()); 43 | int j = 0; 44 | for (auto& p : original_points) 45 | { 46 | // Get distance to first point 47 | float d2 = (p0 - p).sq_norm(); 48 | all_d2[j] = d2; 49 | 50 | // Update max dist 51 | if (d2 > best_d2) 52 | { 53 | best_j = j; 54 | best_d2 = d2; 55 | } 56 | j++; 57 | } 58 | 59 | 60 | // Start FPS 61 | // ********* 62 | 63 | for (int i = 1; i < new_n; i++) 64 | { 65 | // Stop if we reach minimum distance 66 | if (best_d2 < min_d2) 67 | break; 68 | 69 | // Add the fursthest point 70 | subsampled_inds.push_back(best_j); 71 | p0 = original_points[best_j]; 72 | 73 | // Remove from remaining 74 | remaining_mask[best_j] = false; 75 | 76 | // Loop on all remaining points to get the distances 77 | j = 0; 78 | best_d2 = 0; 79 | for (auto& p : original_points) 80 | { 81 | if (remaining_mask[j]) 82 | { 83 | // Update all the minimum distances 84 | float d2 = (p0 - p).sq_norm(); 85 | if (d2 < all_d2[j]) 86 | all_d2[j] = d2; 87 | else 88 | d2 = all_d2[j]; 89 | 90 | // Update max of the min distances 91 | if (d2 > best_d2) 92 | { 93 | best_j = j; 94 | best_d2 = d2; 95 | } 96 | } 97 | j++; 98 | } 99 | } 100 | 101 | return; 102 | } 103 | 104 | 105 | 106 | // void fps_subsampling_2(vector& original_points, 107 | // vector& subsampled_inds, 108 | // int new_n, 109 | // float sampleDl, 110 | // int verbose) 111 | // { 112 | 113 | // // Initialize variables 114 | // // ****************** 115 | 116 | // // square radius 117 | // float min_d2 = sampleDl * sampleDl; 118 | 119 | // // Number of points in the cloud 120 | // size_t N = original_points.size(); 121 | 122 | // // Variables 123 | // vector remaining_mask(original_points.size(), true); 124 | 125 | 126 | // // Variables 127 | // vector remaining_i(original_points.size()); 128 | // std::iota(remaining_i.begin(), remaining_i.end(), 0); 129 | // int best_j = 0; 130 | // float best_d2 = 0; 131 | 132 | // // Fisrt select a random point 133 | // subsampled_inds.reserve(new_n); 134 | // subsampled_inds.push_back(0); 135 | // PointXYZ p0 = original_points[0]; 136 | // remaining_i.erase(remaining_i.begin()); 137 | 138 | 139 | // // Init remaining distances 140 | // // ************************ 141 | 142 | // vector all_d2(original_points.size()); 143 | // int j = 0; 144 | // for (auto& p : original_points) 145 | // { 146 | // // Get distance to first point 147 | // float d2 = (p0 - p).sq_norm(); 148 | // all_d2[j] = d2; 149 | 150 | // // Update max dist 151 | // if (d2 > best_d2) 152 | // { 153 | // best_j = j; 154 | // best_d2 = d2; 155 | // } 156 | // j++; 157 | // } 158 | 159 | 160 | // // Start FPS 161 | // // ********* 162 | 163 | // for (int i = 1; i < new_n; i++) 164 | // { 165 | // // Add the fursthest point 166 | // subsampled_inds.push_back(best_j); 167 | 168 | // // Remove from remaining 169 | // remaining_i.erase(remaining_i.begin() + best_j); 170 | // remaining_d2.erase(remaining_i.begin() + best_j); 171 | 172 | 173 | 174 | // // Loop on all reamining points to get the distances 175 | // j = 0; 176 | // for (auto& p : remaining_i) 177 | // { 178 | // float d2 = (p0 - p).sq_norm(); 179 | // if (d2 < remaining_d2[j]) 180 | // remaining_d2[j] = d2; 181 | // j++; 182 | // } 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | // } 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | // // Limits of the cloud 220 | // PointXYZ minCorner = min_point(original_points); 221 | // PointXYZ maxCorner = max_point(original_points); 222 | // PointXYZ originCorner = floor(minCorner * (1/sampleDl)) * sampleDl; 223 | 224 | // // Dimensions of the grid 225 | // size_t sampleNX = (size_t)floor((maxCorner.x - originCorner.x) / sampleDl) + 1; 226 | // size_t sampleNY = (size_t)floor((maxCorner.y - originCorner.y) / sampleDl) + 1; 227 | // //size_t sampleNZ = (size_t)floor((maxCorner.z - originCorner.z) / sampleDl) + 1; 228 | 229 | 230 | 231 | // // Create the sampled map 232 | // // ********************** 233 | 234 | // // Verbose parameters 235 | // int i = 0; 236 | // int nDisp = N / 100; 237 | 238 | // // Initialize variables 239 | // size_t iX, iY, iZ, mapIdx; 240 | // unordered_map data; 241 | 242 | // for (auto& p : original_points) 243 | // { 244 | // // Position of point in sample map 245 | // iX = (size_t)floor((p.x - originCorner.x) / sampleDl); 246 | // iY = (size_t)floor((p.y - originCorner.y) / sampleDl); 247 | // iZ = (size_t)floor((p.z - originCorner.z) / sampleDl); 248 | // mapIdx = iX + sampleNX*iY + sampleNX*sampleNY*iZ; 249 | 250 | // // If not already created, create key 251 | // if (data.count(mapIdx) < 1) 252 | // data.emplace(mapIdx, SampledData(fdim, ldim)); 253 | 254 | // // Fill the sample map 255 | // if (use_feature && use_classes) 256 | // data[mapIdx].update_all(p, original_features.begin() + i * fdim, original_classes.begin() + i * ldim); 257 | // else if (use_feature) 258 | // data[mapIdx].update_features(p, original_features.begin() + i * fdim); 259 | // else if (use_classes) 260 | // data[mapIdx].update_classes(p, original_classes.begin() + i * ldim); 261 | // else 262 | // data[mapIdx].update_points(p); 263 | 264 | // // Display 265 | // i++; 266 | // if (verbose > 1 && i%nDisp == 0) 267 | // std::cout << "\rSampled Map : " << std::setw(3) << i / nDisp << "%"; 268 | 269 | // } 270 | 271 | // // Divide for barycentre and transfer to a vector 272 | // subsampled_points.reserve(data.size()); 273 | // if (use_feature) 274 | // subsampled_features.reserve(data.size() * fdim); 275 | // if (use_classes) 276 | // subsampled_classes.reserve(data.size() * ldim); 277 | // for (auto& v : data) 278 | // { 279 | // subsampled_points.push_back(v.second.point * (1.0 / v.second.count)); 280 | // if (use_feature) 281 | // { 282 | // float count = (float)v.second.count; 283 | // transform(v.second.features.begin(), 284 | // v.second.features.end(), 285 | // v.second.features.begin(), 286 | // [count](float f) { return f / count;}); 287 | // subsampled_features.insert(subsampled_features.end(),v.second.features.begin(),v.second.features.end()); 288 | // } 289 | // if (use_classes) 290 | // { 291 | // for (int i = 0; i < ldim; i++) 292 | // subsampled_classes.push_back(max_element(v.second.labels[i].begin(), v.second.labels[i].end(), 293 | // [](const pair&a, const pair&b){return a.second < b.second;})->first); 294 | // } 295 | // } 296 | 297 | // return; 298 | // } 299 | 300 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_subsampling/fps_subsampling/fps_subsampling.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | void fps_subsampling(vector& original_points, 11 | vector& subsampled_inds, 12 | int new_n, 13 | float min_d, 14 | int verbose); 15 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_subsampling/grid_subsampling/grid_subsampling.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "../../cpp_utils/cloud/cloud.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | class SampledData 11 | { 12 | public: 13 | 14 | // Elements 15 | // ******** 16 | 17 | int count; 18 | PointXYZ point; 19 | vector features; 20 | vector> labels; 21 | 22 | 23 | // Methods 24 | // ******* 25 | 26 | // Constructor 27 | SampledData() 28 | { 29 | count = 0; 30 | point = PointXYZ(); 31 | } 32 | 33 | SampledData(const size_t fdim, const size_t ldim) 34 | { 35 | count = 0; 36 | point = PointXYZ(); 37 | features = vector(fdim); 38 | labels = vector>(ldim); 39 | } 40 | 41 | // Method Update 42 | void update_all(const PointXYZ p, vector::iterator f_begin, vector::iterator l_begin) 43 | { 44 | count += 1; 45 | point += p; 46 | transform (features.begin(), features.end(), f_begin, features.begin(), plus()); 47 | int i = 0; 48 | for(vector::iterator it = l_begin; it != l_begin + labels.size(); ++it) 49 | { 50 | labels[i][*it] += 1; 51 | i++; 52 | } 53 | return; 54 | } 55 | void update_features(const PointXYZ p, vector::iterator f_begin) 56 | { 57 | count += 1; 58 | point += p; 59 | transform (features.begin(), features.end(), f_begin, features.begin(), plus()); 60 | return; 61 | } 62 | void update_classes(const PointXYZ p, vector::iterator l_begin) 63 | { 64 | count += 1; 65 | point += p; 66 | int i = 0; 67 | for(vector::iterator it = l_begin; it != l_begin + labels.size(); ++it) 68 | { 69 | labels[i][*it] += 1; 70 | i++; 71 | } 72 | return; 73 | } 74 | void update_points(const PointXYZ p) 75 | { 76 | count += 1; 77 | point += p; 78 | return; 79 | } 80 | }; 81 | 82 | void grid_pools_and_ups(vector &original_points, 83 | vector &subsampled_points, 84 | vector &poolings, 85 | vector &upsamplings, 86 | float sampleDl); 87 | 88 | void grid_subsampling(vector& original_points, 89 | vector& subsampled_points, 90 | vector& original_features, 91 | vector& subsampled_features, 92 | vector& original_classes, 93 | vector& subsampled_classes, 94 | float sampleDl, 95 | int verbose); 96 | 97 | void batch_grid_subsampling(vector& original_points, 98 | vector& subsampled_points, 99 | vector& original_features, 100 | vector& subsampled_features, 101 | vector& original_classes, 102 | vector& subsampled_classes, 103 | vector& original_batches, 104 | vector& subsampled_batches, 105 | float sampleDl, 106 | int max_p); 107 | 108 | void partition_batch_grid(vector& original_points, 109 | vector& subsampled_points, 110 | vector& original_batches, 111 | vector& subsampled_batches, 112 | vector& pooling_inds, 113 | vector& upsampling_inds, 114 | float sampleDl); 115 | 116 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_subsampling/setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | from distutils.core import setup, Extension 6 | import numpy.distutils.misc_util 7 | 8 | # Adding OpenCV to project 9 | # ************************ 10 | 11 | # Adding sources of the project 12 | # ***************************** 13 | 14 | SOURCES = ["../cpp_utils/cloud/cloud.cpp", 15 | "grid_subsampling/grid_subsampling.cpp", 16 | "fps_subsampling/fps_subsampling.cpp", 17 | "wrapper.cpp"] 18 | 19 | module = Extension(name="cpp_subsampling", 20 | sources=SOURCES, 21 | extra_compile_args=['-std=c++11', 22 | '-D_GLIBCXX_USE_CXX11_ABI=0']) 23 | 24 | 25 | setup(ext_modules=[module], include_dirs=numpy.distutils.misc_util.get_numpy_include_dirs()) 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_utils/cloud/cloud.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud source : 13 | // Define usefull Functions/Methods 14 | // 15 | //---------------------------------------------------- 16 | // 17 | // Hugues THOMAS - 10/02/2017 18 | // 19 | 20 | 21 | #include "cloud.h" 22 | 23 | 24 | // Getters 25 | // ******* 26 | 27 | PointXYZ max_point(std::vector points) 28 | { 29 | // Initialize limits 30 | PointXYZ maxP(points[0]); 31 | 32 | // Loop over all points 33 | for (auto p : points) 34 | { 35 | if (p.x > maxP.x) 36 | maxP.x = p.x; 37 | 38 | if (p.y > maxP.y) 39 | maxP.y = p.y; 40 | 41 | if (p.z > maxP.z) 42 | maxP.z = p.z; 43 | } 44 | 45 | return maxP; 46 | } 47 | 48 | PointXYZ min_point(std::vector points) 49 | { 50 | // Initialize limits 51 | PointXYZ minP(points[0]); 52 | 53 | // Loop over all points 54 | for (auto p : points) 55 | { 56 | if (p.x < minP.x) 57 | minP.x = p.x; 58 | 59 | if (p.y < minP.y) 60 | minP.y = p.y; 61 | 62 | if (p.z < minP.z) 63 | minP.z = p.z; 64 | } 65 | 66 | return minP; 67 | } -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/cpp_utils/cloud/cloud.h: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // 0==========================0 4 | // | Local feature test | 5 | // 0==========================0 6 | // 7 | // version 1.0 : 8 | // > 9 | // 10 | //--------------------------------------------------- 11 | // 12 | // Cloud header 13 | // 14 | //---------------------------------------------------- 15 | // 16 | // Hugues THOMAS - 10/02/2017 17 | // 18 | 19 | 20 | # pragma once 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | 33 | 34 | 35 | 36 | // Point class 37 | // *********** 38 | 39 | 40 | class PointXYZ 41 | { 42 | public: 43 | 44 | // Elements 45 | // ******** 46 | 47 | float x, y, z; 48 | 49 | 50 | // Methods 51 | // ******* 52 | 53 | // Constructor 54 | PointXYZ() { x = 0; y = 0; z = 0; } 55 | PointXYZ(float x0, float y0, float z0) { x = x0; y = y0; z = z0; } 56 | 57 | // array type accessor 58 | float operator [] (int i) const 59 | { 60 | if (i == 0) return x; 61 | else if (i == 1) return y; 62 | else return z; 63 | } 64 | 65 | // opperations 66 | float dot(const PointXYZ P) const 67 | { 68 | return x * P.x + y * P.y + z * P.z; 69 | } 70 | 71 | float sq_norm() 72 | { 73 | return x*x + y*y + z*z; 74 | } 75 | 76 | PointXYZ cross(const PointXYZ P) const 77 | { 78 | return PointXYZ(y*P.z - z*P.y, z*P.x - x*P.z, x*P.y - y*P.x); 79 | } 80 | 81 | PointXYZ& operator+=(const PointXYZ& P) 82 | { 83 | x += P.x; 84 | y += P.y; 85 | z += P.z; 86 | return *this; 87 | } 88 | 89 | PointXYZ& operator-=(const PointXYZ& P) 90 | { 91 | x -= P.x; 92 | y -= P.y; 93 | z -= P.z; 94 | return *this; 95 | } 96 | 97 | PointXYZ& operator*=(const float& a) 98 | { 99 | x *= a; 100 | y *= a; 101 | z *= a; 102 | return *this; 103 | } 104 | }; 105 | 106 | 107 | // Point Opperations 108 | // ***************** 109 | 110 | inline PointXYZ operator + (const PointXYZ A, const PointXYZ B) 111 | { 112 | return PointXYZ(A.x + B.x, A.y + B.y, A.z + B.z); 113 | } 114 | 115 | inline PointXYZ operator - (const PointXYZ A, const PointXYZ B) 116 | { 117 | return PointXYZ(A.x - B.x, A.y - B.y, A.z - B.z); 118 | } 119 | 120 | inline PointXYZ operator * (const PointXYZ P, const float a) 121 | { 122 | return PointXYZ(P.x * a, P.y * a, P.z * a); 123 | } 124 | 125 | inline PointXYZ operator * (const float a, const PointXYZ P) 126 | { 127 | return PointXYZ(P.x * a, P.y * a, P.z * a); 128 | } 129 | 130 | inline std::ostream& operator << (std::ostream& os, const PointXYZ P) 131 | { 132 | return os << "[" << P.x << ", " << P.y << ", " << P.z << "]"; 133 | } 134 | 135 | inline bool operator == (const PointXYZ A, const PointXYZ B) 136 | { 137 | return A.x == B.x && A.y == B.y && A.z == B.z; 138 | } 139 | 140 | inline PointXYZ floor(const PointXYZ P) 141 | { 142 | return PointXYZ(std::floor(P.x), std::floor(P.y), std::floor(P.z)); 143 | } 144 | 145 | 146 | PointXYZ max_point(std::vector points); 147 | PointXYZ min_point(std::vector points); 148 | 149 | 150 | struct PointCloud 151 | { 152 | 153 | std::vector pts; 154 | 155 | // Must return the number of data points 156 | inline size_t kdtree_get_point_count() const { return pts.size(); } 157 | 158 | // Returns the dim'th component of the idx'th point in the class: 159 | // Since this is inlined and the "dim" argument is typically an immediate value, the 160 | // "if/else's" are actually solved at compile time. 161 | inline float kdtree_get_pt(const size_t idx, const size_t dim) const 162 | { 163 | if (dim == 0) return pts[idx].x; 164 | else if (dim == 1) return pts[idx].y; 165 | else return pts[idx].z; 166 | } 167 | 168 | // Optional bounding-box computation: return false to default to a standard bbox computation loop. 169 | // Return true if the BBOX was already computed by the class and returned in "bb" so it can be avoided to redo it again. 170 | // Look at bb.size() to find out the expected dimensionality (e.g. 2 or 3 for point clouds) 171 | template 172 | bool kdtree_get_bbox(BBOX& /* bb */) const { return false; } 173 | 174 | }; 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/pointnet2_batch/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from torch.utils.cpp_extension import BuildExtension, CUDAExtension 3 | 4 | setup( 5 | name='pointnet2_cuda', 6 | ext_modules=[ 7 | CUDAExtension('pointnet2_batch_cuda', [ 8 | 'src/pointnet2_api.cpp', 9 | 'src/sampling.cpp', 10 | 'src/sampling_gpu.cu', 11 | ], 12 | extra_compile_args={'cxx': ['-g'], 13 | 'nvcc': ['-O2']}) 14 | ], 15 | cmdclass={'build_ext': BuildExtension} 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/pointnet2_batch/src/cuda_utils.h: -------------------------------------------------------------------------------- 1 | #ifndef _CUDA_UTILS_H 2 | #define _CUDA_UTILS_H 3 | 4 | #include 5 | 6 | #define TOTAL_THREADS 1024 7 | #define THREADS_PER_BLOCK 256 8 | #define DIVUP(m,n) ((m) / (n) + ((m) % (n) > 0)) 9 | 10 | inline int opt_n_threads(int work_size) { 11 | 12 | // This function return the highest power of two that is < N 13 | const int pow_2 = std::log(static_cast(work_size)) / std::log(2.0); 14 | 15 | // Result clipped [in 1, 1024] 16 | return max(min(1 << pow_2, TOTAL_THREADS), 1); 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/pointnet2_batch/src/pointnet2_api.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "sampling_gpu.h" 5 | 6 | 7 | PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { 8 | m.def("furthest_point_sampling_wrapper", &furthest_point_sampling_wrapper, "furthest_point_sampling_wrapper"); 9 | } 10 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/pointnet2_batch/src/sampling.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | batch version of point sampling and gathering, modified from the original implementation of official PointNet++ codes. 3 | Written by Shaoshuai Shi 4 | All Rights Reserved 2018. 5 | */ 6 | 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "sampling_gpu.h" 13 | 14 | 15 | int furthest_point_sampling_wrapper(int b, int n, int m, float min_d, 16 | at::Tensor points_tensor, at::Tensor temp_tensor, at::Tensor idx_tensor) 17 | { 18 | 19 | const float *points = points_tensor.data(); 20 | float *temp = temp_tensor.data(); 21 | int *idx = idx_tensor.data(); 22 | 23 | float min_d2 = min_d * min_d; 24 | 25 | furthest_point_sampling_kernel_launcher(b, n, m, min_d2, points, temp, idx); 26 | 27 | return 1; 28 | } 29 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/pointnet2_batch/src/sampling_gpu.cu: -------------------------------------------------------------------------------- 1 | /* 2 | batch version of point sampling and gathering, modified from the original implementation of official PointNet++ codes. 3 | Written by Shaoshuai Shi 4 | All Rights Reserved 2018. 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | #include "cuda_utils.h" 11 | #include "sampling_gpu.h" 12 | 13 | 14 | __device__ void __update(float *__restrict__ dists, int *__restrict__ dists_i, int idx1, int idx2) 15 | { 16 | const float v1 = dists[idx1], v2 = dists[idx2]; 17 | const int i1 = dists_i[idx1], i2 = dists_i[idx2]; 18 | dists[idx1] = max(v1, v2); 19 | dists_i[idx1] = v2 > v1 ? i2 : i1; 20 | } 21 | 22 | 23 | template 24 | __global__ void furthest_point_sampling_kernel(int b, int n, int m, float min_d2, 25 | const float *__restrict__ dataset, 26 | float *__restrict__ temp, 27 | int *__restrict__ idxs) 28 | { 29 | // dataset: (B, N, 3) 30 | // tmp: (B, N) 31 | // output: 32 | // idx: (B, M) 33 | 34 | if (m <= 0) 35 | return; 36 | __shared__ float dists[block_size]; 37 | __shared__ int dists_i[block_size]; 38 | 39 | int batch_index = blockIdx.x; 40 | dataset += batch_index * n * 3; 41 | temp += batch_index * n; 42 | idxs += batch_index * m; 43 | 44 | int tid = threadIdx.x; 45 | const int stride = block_size; 46 | 47 | int old = 0; 48 | if (threadIdx.x == 0) 49 | idxs[0] = old; 50 | 51 | __syncthreads(); 52 | for (int j = 1; j < m; j++) 53 | { 54 | int besti = 0; 55 | float best = -1; 56 | float x1 = dataset[old * 3 + 0]; 57 | float y1 = dataset[old * 3 + 1]; 58 | float z1 = dataset[old * 3 + 2]; 59 | for (int k = tid; k < n; k += stride) 60 | { 61 | float x2, y2, z2; 62 | x2 = dataset[k * 3 + 0]; 63 | y2 = dataset[k * 3 + 1]; 64 | z2 = dataset[k * 3 + 2]; 65 | // float mag = (x2 * x2) + (y2 * y2) + (z2 * z2); 66 | // if (mag <= 1e-3) 67 | // continue; 68 | 69 | float dx = x2 - x1; 70 | float dy = y2 - y1; 71 | float dz = z2 - z1; 72 | float d = dx * dx + dy * dy + dz * dz; 73 | float d2 = min(d, temp[k]); 74 | temp[k] = d2; 75 | besti = d2 > best ? k : besti; 76 | best = d2 > best ? d2 : best; 77 | } 78 | dists[tid] = best; 79 | dists_i[tid] = besti; 80 | __syncthreads(); 81 | 82 | if (block_size >= 1024) 83 | { 84 | if (tid < 512) 85 | { 86 | __update(dists, dists_i, tid, tid + 512); 87 | } 88 | __syncthreads(); 89 | } 90 | 91 | if (block_size >= 512) 92 | { 93 | if (tid < 256) 94 | { 95 | __update(dists, dists_i, tid, tid + 256); 96 | } 97 | __syncthreads(); 98 | } 99 | if (block_size >= 256) 100 | { 101 | if (tid < 128) 102 | { 103 | __update(dists, dists_i, tid, tid + 128); 104 | } 105 | __syncthreads(); 106 | } 107 | if (block_size >= 128) 108 | { 109 | if (tid < 64) 110 | { 111 | __update(dists, dists_i, tid, tid + 64); 112 | } 113 | __syncthreads(); 114 | } 115 | if (block_size >= 64) 116 | { 117 | if (tid < 32) 118 | { 119 | __update(dists, dists_i, tid, tid + 32); 120 | } 121 | __syncthreads(); 122 | } 123 | if (block_size >= 32) 124 | { 125 | if (tid < 16) 126 | { 127 | __update(dists, dists_i, tid, tid + 16); 128 | } 129 | __syncthreads(); 130 | } 131 | if (block_size >= 16) 132 | { 133 | if (tid < 8) 134 | { 135 | __update(dists, dists_i, tid, tid + 8); 136 | } 137 | __syncthreads(); 138 | } 139 | if (block_size >= 8) 140 | { 141 | if (tid < 4) 142 | { 143 | __update(dists, dists_i, tid, tid + 4); 144 | } 145 | __syncthreads(); 146 | } 147 | if (block_size >= 4) 148 | { 149 | if (tid < 2) 150 | { 151 | __update(dists, dists_i, tid, tid + 2); 152 | } 153 | __syncthreads(); 154 | } 155 | if (block_size >= 2) 156 | { 157 | if (tid < 1) 158 | { 159 | __update(dists, dists_i, tid, tid + 1); 160 | } 161 | __syncthreads(); 162 | } 163 | 164 | // Save best indice for next step 165 | old = dists_i[0]; 166 | 167 | __syncthreads(); 168 | const float v1 = dists[0]; 169 | __syncthreads(); 170 | if (v1 < min_d2) 171 | break; 172 | 173 | // Update the result list 174 | if (tid == 0) 175 | { 176 | // idxs[j] = v1 > min_d2 ? old : -1; 177 | idxs[j] = old; 178 | } 179 | } 180 | } 181 | 182 | 183 | void furthest_point_sampling_kernel_launcher(int b, int n, int m, float min_d2, 184 | const float *dataset, float *temp, int *idxs) 185 | { 186 | // dataset: (B, N, 3) 187 | // tmp: (B, N) 188 | // output: 189 | // idx: (B, M) 190 | 191 | 192 | cudaError_t err; 193 | unsigned int n_threads = opt_n_threads(n); 194 | 195 | switch (n_threads) 196 | { 197 | case 1024: 198 | furthest_point_sampling_kernel<1024><<>>(b, n, m, min_d2, dataset, temp, idxs); 199 | break; 200 | case 512: 201 | furthest_point_sampling_kernel<512><<>>(b, n, m, min_d2, dataset, temp, idxs); 202 | break; 203 | case 256: 204 | furthest_point_sampling_kernel<256><<>>(b, n, m, min_d2, dataset, temp, idxs); 205 | break; 206 | case 128: 207 | furthest_point_sampling_kernel<128><<>>(b, n, m, min_d2, dataset, temp, idxs); 208 | break; 209 | case 64: 210 | furthest_point_sampling_kernel<64><<>>(b, n, m, min_d2, dataset, temp, idxs); 211 | break; 212 | case 32: 213 | furthest_point_sampling_kernel<32><<>>(b, n, m, min_d2, dataset, temp, idxs); 214 | break; 215 | case 16: 216 | furthest_point_sampling_kernel<16><<>>(b, n, m, min_d2, dataset, temp, idxs); 217 | break; 218 | case 8: 219 | furthest_point_sampling_kernel<8><<>>(b, n, m, min_d2, dataset, temp, idxs); 220 | break; 221 | case 4: 222 | furthest_point_sampling_kernel<4><<>>(b, n, m, min_d2, dataset, temp, idxs); 223 | break; 224 | case 2: 225 | furthest_point_sampling_kernel<2><<>>(b, n, m, min_d2, dataset, temp, idxs); 226 | break; 227 | case 1: 228 | furthest_point_sampling_kernel<1><<>>(b, n, m, min_d2, dataset, temp, idxs); 229 | break; 230 | default: 231 | furthest_point_sampling_kernel<512><<>>(b, n, m, min_d2, dataset, temp, idxs); 232 | } 233 | 234 | err = cudaGetLastError(); 235 | if (cudaSuccess != err) 236 | { 237 | fprintf(stderr, "CUDA kernel failed : %s\n", cudaGetErrorString(err)); 238 | exit(-1); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Standalone/KPConvX/cpp_wrappers/pointnet2_batch/src/sampling_gpu.h: -------------------------------------------------------------------------------- 1 | #ifndef _SAMPLING_GPU_H 2 | #define _SAMPLING_GPU_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | 10 | int furthest_point_sampling_wrapper(int b, int n, int m, float min_d, 11 | at::Tensor points_tensor, at::Tensor temp_tensor, at::Tensor idx_tensor); 12 | 13 | void furthest_point_sampling_kernel_launcher(int b, int n, int m, float min_d2, 14 | const float *dataset, float *temp, int *idxs); 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /Standalone/KPConvX/experiments/S3DIS/S3DIS.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | 6 | # ---------------------------------------------------------------------------------------------------------------------- 7 | # 8 | # Script Intro 9 | # \******************/ 10 | # 11 | # 12 | # Use this script to define the dataset specific configuration. You should be able to adapt this file for other dataset 13 | # that share the same file structure as S3DIS. 14 | # 15 | # This is the legacy full area dataset used in the original KPConv paper. We recommend using S3DIS_rooms.py. 16 | # 17 | # 18 | 19 | # ---------------------------------------------------------------------------------------------------------------------- 20 | # 21 | # Imports and global variables 22 | # \**********************************/ 23 | # 24 | 25 | import time 26 | import numpy as np 27 | from os.path import join 28 | 29 | from utils.config import init_cfg 30 | from data_handlers.scene_seg import SceneSegDataset 31 | 32 | 33 | 34 | # ---------------------------------------------------------------------------------------------------------------------- 35 | # 36 | # Config Class 37 | # \******************/ 38 | # 39 | # ---------------------------------------------------------------------------------------------------------------------- 40 | # 41 | # Hugues THOMAS - 06/10/2023 42 | # 43 | # KPConvX project: S3DIS.py 44 | # > Legacy dataset class for the old S3DIS (Areas are not split in rooms) 45 | # 46 | 47 | 48 | def S3DIS_cfg(cfg, dataset_path='../data/S3DIS'): 49 | 50 | # cfg = init_cfg() 51 | 52 | # Dataset path 53 | cfg.data.name = 'S3DIS' 54 | cfg.data.path = dataset_path 55 | cfg.data.task = 'cloud_segmentation' 56 | 57 | # Dataset dimension 58 | cfg.data.dim = 3 59 | 60 | # Dict from labels to names 61 | cfg.data.label_and_names = [(0, 'ceiling'), 62 | (1, 'floor'), 63 | (2, 'wall'), 64 | (3, 'beam'), 65 | (4, 'column'), 66 | (5, 'window'), 67 | (6, 'door'), 68 | (7, 'chair'), 69 | (8, 'table'), 70 | (9, 'bookcase'), 71 | (10, 'sofa'), 72 | (11, 'board'), 73 | (12, 'clutter')] 74 | 75 | # Initialize all label parameters given the label_and_names list 76 | cfg.data.num_classes = len(cfg.data.label_and_names) 77 | cfg.data.label_values = [k for k, v in cfg.data.label_and_names] 78 | cfg.data.label_names = [v for k, v in cfg.data.label_and_names] 79 | cfg.data.name_to_label = {v: k for k, v in cfg.data.label_and_names} 80 | cfg.data.name_to_idx = {v: i for i, v in enumerate(cfg.data.label_names)} 81 | 82 | # Ignored labels 83 | cfg.data.ignored_labels = [] 84 | cfg.data.pred_values = [k for k in cfg.data.label_values if k not in cfg.data.ignored_labels] 85 | 86 | return cfg 87 | 88 | 89 | # ---------------------------------------------------------------------------------------------------------------------- 90 | # 91 | # Dataset class definition 92 | # \******************************/ 93 | # 94 | 95 | 96 | class S3DISDataset(SceneSegDataset): 97 | 98 | def __init__(self, cfg, chosen_set='training', precompute_pyramid=False, load_data=True): 99 | """ 100 | Class to handle S3DIS dataset. 101 | Simple implementation. 102 | > Input only consist of the first cloud with features 103 | > Neigborhood and subsamplings are computed on the fly in the network 104 | > Sampling is done simply with random picking (X spheres per class) 105 | """ 106 | SceneSegDataset.__init__(self, 107 | cfg, 108 | chosen_set=chosen_set, 109 | precompute_pyramid=precompute_pyramid) 110 | 111 | ############ 112 | # S3DIS data 113 | ############ 114 | 115 | # Here provide the list of .ply files depending on the set (training/validation/test) 116 | self.scene_names, self.scene_files = self.S3DIS_files() 117 | 118 | # Stop data is not needed 119 | if not load_data: 120 | return 121 | 122 | # Properties of input files 123 | self.label_property = 'class' 124 | self.f_properties = ['red', 'green', 'blue'] 125 | 126 | # Start loading (merge when testing) 127 | self.load_scenes_in_memory(label_property=self.label_property, 128 | f_properties=self.f_properties, 129 | f_scales=[1/255, 1/255, 1/255]) 130 | 131 | ########################### 132 | # Sampling data preparation 133 | ########################### 134 | 135 | if self.data_sampler == 'regular': 136 | # In case regular sampling, generate the first sampling points 137 | self.new_reg_sampling_pts() 138 | 139 | else: 140 | # To pick points randomly per class, we need every point index from each class 141 | self.prepare_label_inds() 142 | 143 | return 144 | 145 | 146 | def S3DIS_files(self): 147 | """ 148 | Function returning a list of file path. One for each scene in the dataset. 149 | """ 150 | 151 | # Path where files can be found 152 | ply_path = join(self.path, 'original_ply') 153 | 154 | # Scene names 155 | scene_names = ['Area_1', 'Area_2', 'Area_3', 'Area_4', 'Area_5', 'Area_6'] 156 | 157 | # Scene files 158 | scene_files = [join(ply_path, f + '.ply') for f in scene_names] 159 | 160 | # Only get a specific split 161 | all_splits = [0, 1, 2, 3, 4, 5] 162 | val_split = 4 163 | 164 | if self.set == 'training': 165 | scene_names = [f for i, f in enumerate(scene_names) if all_splits[i] != val_split] 166 | scene_files = [f for i, f in enumerate(scene_files) if all_splits[i] != val_split] 167 | 168 | elif self.set in ['validation', 'test']: 169 | scene_names = [f for i, f in enumerate(scene_names) if all_splits[i] == val_split] 170 | scene_files = [f for i, f in enumerate(scene_files) if all_splits[i] == val_split] 171 | 172 | return scene_names, scene_files 173 | 174 | 175 | def select_features(self, in_features): 176 | 177 | # Input features 178 | selected_features = np.ones_like(in_features[:, :1], dtype=np.float32) 179 | if self.cfg.model.input_channels == 1: 180 | pass 181 | elif self.cfg.model.input_channels == 4: 182 | selected_features = np.hstack((selected_features, in_features[:, :3])) 183 | elif self.cfg.model.input_channels == 5: 184 | selected_features = np.hstack((selected_features, in_features)) 185 | else: 186 | raise ValueError('Only accepted input dimensions are 1, 4 and 5') 187 | 188 | return selected_features 189 | 190 | -------------------------------------------------------------------------------- /Standalone/KPConvX/experiments/S3DIS/S3DIS_rooms.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: S3DIS_rooms.py 10 | # > Dataset class for S3DIS (rooms) 11 | # 12 | 13 | 14 | # ---------------------------------------------------------------------------------------------------------------------- 15 | # 16 | # Script Intro 17 | # \******************/ 18 | # 19 | # 20 | # Use this script to define the dataset specific configuration. You should be able to adapt this file for other dataset 21 | # that share the same file structure as S3DIR. 22 | # 23 | # We call this the S3DIR dataset as it is the room version of the S3DIS dataset. 24 | # 25 | # 26 | 27 | # ---------------------------------------------------------------------------------------------------------------------- 28 | # 29 | # Imports and global variables 30 | # \**********************************/ 31 | # 32 | 33 | import time 34 | import numpy as np 35 | from os import listdir, makedirs 36 | from os.path import join, exists 37 | 38 | from utils.config import init_cfg 39 | from data_handlers.scene_seg import SceneSegDataset 40 | from utils.ply import read_ply, write_ply 41 | 42 | 43 | 44 | # ---------------------------------------------------------------------------------------------------------------------- 45 | # 46 | # Config Class 47 | # \******************/ 48 | # 49 | 50 | 51 | def S3DIR_cfg(cfg, dataset_path='../data/s3dis'): 52 | 53 | # cfg = init_cfg() 54 | 55 | # Dataset path 56 | cfg.data.name = 'S3DIR' 57 | cfg.data.path = dataset_path 58 | cfg.data.task = 'cloud_segmentation' 59 | 60 | # Dataset dimension 61 | cfg.data.dim = 3 62 | 63 | # Dict from labels to names 64 | cfg.data.label_and_names = [(0, 'ceiling'), 65 | (1, 'floor'), 66 | (2, 'wall'), 67 | (3, 'beam'), 68 | (4, 'column'), 69 | (5, 'window'), 70 | (6, 'door'), 71 | (7, 'chair'), 72 | (8, 'table'), 73 | (9, 'bookcase'), 74 | (10, 'sofa'), 75 | (11, 'board'), 76 | (12, 'clutter')] 77 | 78 | # Initialize all label parameters given the label_and_names list 79 | cfg.data.num_classes = len(cfg.data.label_and_names) 80 | cfg.data.label_values = [k for k, v in cfg.data.label_and_names] 81 | cfg.data.label_names = [v for k, v in cfg.data.label_and_names] 82 | cfg.data.name_to_label = {v: k for k, v in cfg.data.label_and_names} 83 | cfg.data.name_to_idx = {v: i for i, v in enumerate(cfg.data.label_names)} 84 | 85 | # Ignored labels 86 | cfg.data.ignored_labels = [] 87 | cfg.data.pred_values = [k for k in cfg.data.label_values if k not in cfg.data.ignored_labels] 88 | 89 | return cfg 90 | 91 | 92 | # ---------------------------------------------------------------------------------------------------------------------- 93 | # 94 | # Dataset class definition 95 | # \******************************/ 96 | # 97 | 98 | 99 | class S3DIRDataset(SceneSegDataset): 100 | 101 | def __init__(self, cfg, chosen_set='training', precompute_pyramid=False, load_data=True): 102 | """ 103 | Class to handle S3DIR dataset. 104 | Simple implementation. 105 | > Input only consist of the first cloud with features 106 | > Neigborhood and subsamplings are computed on the fly in the network 107 | > Sampling is done simply with random picking (X spheres per class) 108 | """ 109 | SceneSegDataset.__init__(self, 110 | cfg, 111 | chosen_set=chosen_set, 112 | precompute_pyramid=precompute_pyramid) 113 | 114 | ############ 115 | # S3DIR data 116 | ############ 117 | 118 | # Here provide the list of .ply files depending on the set (training/validation/test) 119 | self.scene_names, self.scene_files = self.S3DIR_files() 120 | 121 | # Stop data is not needed 122 | if not load_data: 123 | return 124 | 125 | # Properties of input files 126 | self.label_property = 'class' 127 | self.f_properties = ['red', 'green', 'blue'] 128 | 129 | # Start loading (merge when testing) 130 | self.load_scenes_in_memory(label_property=self.label_property, 131 | f_properties=self.f_properties, 132 | f_scales=[1/255, 1/255, 1/255]) 133 | 134 | ########################### 135 | # Sampling data preparation 136 | ########################### 137 | 138 | if self.data_sampler == 'regular': 139 | # In case regular sampling, generate the first sampling points 140 | self.new_reg_sampling_pts() 141 | 142 | else: 143 | # To pick points randomly per class, we need every point index from each class 144 | self.prepare_label_inds() 145 | 146 | return 147 | 148 | 149 | def S3DIR_files(self): 150 | """ 151 | Function returning a list of file path. One for each scene in the dataset. 152 | """ 153 | 154 | # Get Areas 155 | area_paths = np.sort([join(self.path, f) for f in listdir(self.path) if f.startswith('Area')]) 156 | 157 | # Get room names 158 | scene_paths = [np.sort([join(area_path, sc) for sc in listdir(str(area_path))]) 159 | for area_path in area_paths] 160 | 161 | # Only get a specific split 162 | if self.set == 'training': 163 | split_inds = [0, 1, 2, 3, 5] 164 | elif self.set in ['validation', 'test']: 165 | split_inds = [4,] 166 | 167 | scene_files = np.concatenate([scene_paths[i] for i in split_inds], axis=0) 168 | scene_names = [f.split('/')[-2] + "_" + f.split('/')[-1] for f in scene_files] 169 | 170 | # In case of merge, change the files 171 | self.room_lists = None 172 | 173 | return scene_names, scene_files 174 | 175 | 176 | def select_features(self, in_features): 177 | 178 | # Input features 179 | selected_features = np.ones_like(in_features[:, :1], dtype=np.float32) 180 | if self.cfg.model.input_channels == 1: 181 | pass 182 | elif self.cfg.model.input_channels == 4: 183 | selected_features = np.hstack((selected_features, in_features[:, :3])) 184 | elif self.cfg.model.input_channels == 5: 185 | selected_features = np.hstack((selected_features, in_features)) 186 | else: 187 | raise ValueError('Only accepted input dimensions are 1, 4 and 5') 188 | 189 | return selected_features 190 | 191 | def load_scene_file(self, file_path): 192 | 193 | if file_path.endswith('.ply'): 194 | 195 | data = read_ply(file_path) 196 | points = np.vstack((data['x'], data['y'], data['z'])).T 197 | if self.label_property in [p for p, _ in data.dtype.fields.items()]: 198 | labels = data[self.label_property].astype(np.int32) 199 | else: 200 | labels = None 201 | features = np.vstack([data[f_prop].astype(np.float32) for f_prop in self.f_properties]).T 202 | 203 | elif file_path.endswith('.npy'): 204 | 205 | cdata = np.load(file_path) 206 | 207 | points = cdata[:,0:3].astype(np.float32) 208 | features = cdata[:, 3:6].astype(np.float32) 209 | labels = cdata[:, 6:7].astype(np.int32) 210 | 211 | elif file_path.endswith('.merge'): # loads all the files that share a same root 212 | 213 | # Merge data 214 | all_points = [] 215 | all_features = [] 216 | all_labels = [] 217 | for room_file in self.room_lists[file_path]: 218 | points, features, labels = self.load_scene_file(room_file) 219 | all_points.append(points) 220 | all_features.append(features) 221 | all_labels.append(labels) 222 | points = np.concatenate(all_points, axis=0) 223 | features = np.concatenate(all_features, axis=0) 224 | labels = np.concatenate(all_labels, axis=0) 225 | 226 | else: 227 | 228 | # New dataset format has each room as a folder 229 | points = np.load(join(file_path, 'coord.npy')) 230 | colors = np.load(join(file_path, 'color.npy')) 231 | # instances = np.load(join(file_path, 'instance.npy')) 232 | # normals = np.load(join(file_path, 'normal.npy')) 233 | segments = np.load(join(file_path, 'segment.npy')) 234 | 235 | # features = np.concatenate((colors.astype(np.float32) / 255, normals), axis=1) 236 | features = colors.astype(np.float32) 237 | 238 | 239 | labels = segments 240 | 241 | return points, features, np.squeeze(labels) 242 | -------------------------------------------------------------------------------- /Standalone/KPConvX/experiments/S3DIS/plot_S3DIS.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: plot_S3DIS.py 10 | # > Plotting script for S3DIS experiments 11 | # 12 | 13 | 14 | # ---------------------------------------------------------------------------------------------------------------------- 15 | # 16 | # Script Intro 17 | # \******************/ 18 | # 19 | # 20 | # This script is used to plot the results of our networks. You can run it while a network is still training to see 21 | # if it has converged yet. 22 | # 23 | # 24 | 25 | 26 | # ---------------------------------------------------------------------------------------------------------------------- 27 | # 28 | # Imports and global variables 29 | # \**********************************/ 30 | # 31 | 32 | # Common libs 33 | from signal import raise_signal 34 | import sys 35 | import os 36 | import time 37 | import numpy as np 38 | import matplotlib.pyplot as plt 39 | from os.path import isfile, join, exists 40 | from os import listdir, remove, getcwd, makedirs 41 | 42 | 43 | # Common libs 44 | import matplotlib.pyplot as plt 45 | from os.path import isfile, join, exists 46 | from os import listdir, remove, getcwd 47 | 48 | # Local libs 49 | current = os.path.dirname(os.path.realpath(__file__)) 50 | parent = os.path.dirname(os.path.dirname(current)) 51 | sys.path.append(parent) 52 | 53 | 54 | # My libs 55 | from utils.ply import read_ply 56 | 57 | 58 | # My libs 59 | from utils.printing import frame_lines_1, underline 60 | from utils.ply import read_ply, write_ply 61 | from utils.config import load_cfg 62 | 63 | from utils.plot_utilities import listdir_str, print_cfg_diffs, compare_trainings, compare_convergences_segment, \ 64 | compare_on_test_set, cleanup 65 | 66 | 67 | # ---------------------------------------------------------------------------------------------------------------------- 68 | # 69 | # Experiments 70 | # \*****************/ 71 | # 72 | 73 | 74 | def experiment_name_1(): 75 | """ 76 | In this function you choose the results you want to plot together, to compare them as an experiment. 77 | Just return the list of log paths (like 'results/Log_2020-04-04_10-04-42' for example), and the associated names 78 | of these logs. 79 | Below an example of how to automatically gather all logs between two dates, and name them. 80 | """ 81 | 82 | # Using the dates of the logs, you can easily gather consecutive ones. All logs should be of the same dataset. 83 | start = 'Log_2020-04-22_11-52-58' 84 | end = 'Log_2023-07-29_12-40-27' 85 | 86 | # Name of the result path 87 | res_path = 'results' 88 | 89 | # Gather logs and sort by date 90 | logs = np.sort([join(res_path, l) for l in listdir_str(res_path) if start <= l <= end]) 91 | 92 | # Give names to the logs (for plot legends) 93 | logs_names = ['name_log_1', 94 | 'name_log_2', 95 | 'name_log_3', 96 | 'name_log_4'] 97 | 98 | # safe check log names 99 | logs_names = np.array(logs_names[:len(logs)]) 100 | 101 | return logs, logs_names 102 | 103 | 104 | def experiment_name_2(): 105 | """ 106 | In this function you choose the results you want to plot together, to compare them as an experiment. 107 | Just return the list of log paths (like 'results/Log_2020-04-04_10-04-42' for example), and the associated names 108 | of these logs. 109 | Below an example of how to automatically gather all logs between two dates, and name them. 110 | """ 111 | 112 | # Using the dates of the logs, you can easily gather consecutive ones. All logs should be of the same dataset. 113 | start = 'Log_2022-04-22_11-52-58' 114 | end = 'Log_2024-05-22_11-52-58' 115 | 116 | # Name of the result path 117 | res_path = 'results' 118 | 119 | # Gather logs and sort by date 120 | logs = np.sort([join(res_path, l) for l in listdir_str(res_path) if start <= l <= end]) 121 | 122 | # Optionally add a specific log at a specific place in the log list 123 | logs = logs.astype(' Dataset class for ScanObjectNN 11 | # 12 | 13 | 14 | # ---------------------------------------------------------------------------------------------------------------------- 15 | # 16 | # Script Intro 17 | # \******************/ 18 | # 19 | # 20 | # Use this script to define the dataset specific configuration. You should be able to adapt this file for other dataset 21 | # that share the same file structure as ScanObjectNN. 22 | # 23 | # 24 | 25 | # ---------------------------------------------------------------------------------------------------------------------- 26 | # 27 | # Imports and global variables 28 | # \**********************************/ 29 | # 30 | 31 | import numpy as np 32 | from os.path import join, isfile 33 | import h5py 34 | import pickle 35 | 36 | from utils.config import init_cfg 37 | from data_handlers.object_classification import ObjClassifDataset 38 | 39 | from utils.ply import write_ply 40 | 41 | 42 | 43 | # ---------------------------------------------------------------------------------------------------------------------- 44 | # 45 | # Config Class 46 | # \******************/ 47 | # 48 | 49 | 50 | def ScanObjectNN_cfg(cfg, dataset_path='../data/ScanObjectNN/main_split'): 51 | 52 | # Dataset path 53 | cfg.data.name = 'ScanObjectNN' 54 | cfg.data.path = dataset_path 55 | cfg.data.task = 'classification' 56 | 57 | # Dataset dimension 58 | cfg.data.dim = 3 59 | 60 | # Dict from labels to names 61 | cfg.data.label_and_names = [(0, 'bag'), 62 | (1, 'bin'), 63 | (2, 'box'), 64 | (3, 'cabinet'), 65 | (4, 'chair'), 66 | (5, 'desk'), 67 | (6, 'display'), 68 | (7, 'door'), 69 | (8, 'shelf'), 70 | (9, 'table'), 71 | (10, 'bed'), 72 | (11, 'pillow'), 73 | (12, 'sink'), 74 | (13, 'sofa'), 75 | (14, 'toilet')] 76 | 77 | 78 | 79 | 80 | # Initialize all label parameters given the label_and_names list 81 | cfg.data.num_classes = len(cfg.data.label_and_names) 82 | cfg.data.label_values = [k for k, v in cfg.data.label_and_names] 83 | cfg.data.label_names = [v for k, v in cfg.data.label_and_names] 84 | cfg.data.name_to_label = {v: k for k, v in cfg.data.label_and_names} 85 | cfg.data.name_to_idx = {v: i for i, v in enumerate(cfg.data.label_names)} 86 | 87 | # Ignored labels 88 | cfg.data.ignored_labels = [] 89 | cfg.data.pred_values = [k for k in cfg.data.label_values if k not in cfg.data.ignored_labels] 90 | 91 | return cfg 92 | 93 | 94 | # ---------------------------------------------------------------------------------------------------------------------- 95 | # 96 | # Dataset class definition 97 | # \******************************/ 98 | # 99 | 100 | 101 | class ScanObjectNNDataset(ObjClassifDataset): 102 | 103 | def __init__(self, cfg, chosen_set='training', precompute_pyramid=False, load_data=True, uniform_sample=True): 104 | """ 105 | Class to handle ScanObjectNN dataset. 106 | Simple implementation. 107 | > Input only consist of the first cloud with features 108 | > Neigborhood and subsamplings are computed on the fly in the network 109 | > Sampling is done simply with random picking (X spheres per class) 110 | """ 111 | ObjClassifDataset.__init__(self, 112 | cfg, 113 | chosen_set=chosen_set, 114 | precompute_pyramid=precompute_pyramid) 115 | 116 | ################### 117 | # ScanObjectNN data 118 | ################### 119 | 120 | if chosen_set == 'training': 121 | h5_file = join(cfg.data.path, 'training_objectdataset_augmentedrot_scale75.h5') 122 | 123 | elif chosen_set in ['validation', 'test']: 124 | sampled_file = join(cfg.data.path, 'test_objectdataset_augmentedrot_scale75_1024_fps.pkl') 125 | h5_file = join(cfg.data.path, 'test_objectdataset_augmentedrot_scale75.h5') 126 | else: 127 | raise ValueError('chosen_set set not recognised') 128 | 129 | # Check file 130 | if not isfile(h5_file): 131 | raise FileExistsError(f'{h5_file} does not exist, please download dataset at first') 132 | 133 | # Load file (points and label) 134 | with h5py.File(h5_file, 'r') as f: 135 | self.input_points = np.array(f['data']).astype(np.float32) 136 | self.input_labels = np.array(f['label']).astype(int) 137 | 138 | # Reload sampled point for test 139 | if chosen_set in ['validation', 'test'] and uniform_sample: 140 | if not isfile(sampled_file): 141 | raise FileExistsError(f'{sampled_file} does not exist, please download dataset at first') 142 | with open(sampled_file, 'rb') as f: 143 | self.input_points = pickle.load(f) 144 | 145 | # Height is in y coordinates so permute 146 | self.input_points = np.ascontiguousarray(self.input_points[:, :, [2, 0, 1]]) 147 | 148 | # This dataset does not have features use point coordinates 149 | self.input_features = np.copy(self.input_points) 150 | 151 | # # Test random objects 152 | # idx = [0, 100, 1000, 2000, 3000, 4000, 5000, 6000, 10000] 153 | # for i in idx: 154 | # write_ply(join(cfg.data.path, 'test{:d}.ply'.format(i)), 155 | # (self.input_points[i]), 156 | # ['x', 'y', 'z']) 157 | # print(self.labels[i]) 158 | 159 | # Handle number of objects and number of steps per epoch 160 | self.objects_per_epoch(cfg) 161 | 162 | return 163 | 164 | 165 | def select_features(self, in_features): 166 | 167 | # Input features 168 | selected_features = np.ones_like(in_features[:, :1], dtype=np.float32) 169 | if self.cfg.model.input_channels == 1: 170 | pass 171 | elif self.cfg.model.input_channels == 2: 172 | selected_features = np.hstack((selected_features, in_features[:, 2:3])) 173 | elif self.cfg.model.input_channels == 4: 174 | selected_features = np.hstack((selected_features, in_features[:, :3])) 175 | elif self.cfg.model.input_channels == 7: 176 | selected_features = np.hstack((selected_features, in_features[:, :6])) 177 | else: 178 | raise ValueError('Only accepted input dimensions are 1, 4 and 5') 179 | 180 | return selected_features 181 | 182 | -------------------------------------------------------------------------------- /Standalone/KPConvX/experiments/ScanObjectNN/plot_ScanObj.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: plot_ScanObj.py 10 | # > Plotting script for ScanObjectNN experiments 11 | # 12 | 13 | 14 | # ---------------------------------------------------------------------------------------------------------------------- 15 | # 16 | # Script Intro 17 | # \******************/ 18 | # 19 | # 20 | # This script is used to plot the results of our networks. You can run it while a network is still training to see 21 | # if it has converged yet. 22 | # 23 | # 24 | 25 | 26 | # ---------------------------------------------------------------------------------------------------------------------- 27 | # 28 | # Imports and global variables 29 | # \**********************************/ 30 | # 31 | 32 | # Common libs 33 | from signal import raise_signal 34 | import sys 35 | import os 36 | import time 37 | import numpy as np 38 | import matplotlib.pyplot as plt 39 | from os.path import isfile, join, exists 40 | from os import listdir, remove, getcwd, makedirs 41 | 42 | 43 | # Common libs 44 | import matplotlib.pyplot as plt 45 | from os.path import isfile, join, exists 46 | from os import listdir, remove, getcwd 47 | 48 | # Local libs 49 | current = os.path.dirname(os.path.realpath(__file__)) 50 | parent = os.path.dirname(os.path.dirname(current)) 51 | sys.path.append(parent) 52 | 53 | 54 | # My libs 55 | from utils.ply import read_ply 56 | 57 | 58 | # My libs 59 | from utils.printing import frame_lines_1, underline 60 | from utils.ply import read_ply, write_ply 61 | from utils.config import load_cfg 62 | 63 | from utils.plot_utilities import listdir_str, print_cfg_diffs, compare_trainings, compare_convergences_classif, \ 64 | compare_on_test_set, cleanup 65 | 66 | 67 | # ---------------------------------------------------------------------------------------------------------------------- 68 | # 69 | # Experiments 70 | # \*****************/ 71 | # 72 | 73 | 74 | def experiment_name_1(): 75 | """ 76 | In this function you choose the results you want to plot together, to compare them as an experiment. 77 | Just return the list of log paths (like 'results/Log_2020-04-04_10-04-42' for example), and the associated names 78 | of these logs. 79 | Below an example of how to automatically gather all logs between two dates, and name them. 80 | """ 81 | 82 | # Using the dates of the logs, you can easily gather consecutive ones. All logs should be of the same dataset. 83 | start = 'Log_2020-04-22_11-52-58' 84 | end = 'Log_2023-07-29_12-40-27' 85 | 86 | # Name of the result path 87 | res_path = 'results' 88 | 89 | # Gather logs and sort by date 90 | logs = np.sort([join(res_path, l) for l in listdir_str(res_path) if start <= l <= end]) 91 | 92 | # Give names to the logs (for plot legends) 93 | logs_names = ['name_log_1', 94 | 'name_log_2', 95 | 'name_log_3', 96 | 'name_log_4'] 97 | 98 | # safe check log names 99 | logs_names = np.array(logs_names[:len(logs)]) 100 | 101 | return logs, logs_names 102 | 103 | 104 | def experiment_name_2(): 105 | """ 106 | In this function you choose the results you want to plot together, to compare them as an experiment. 107 | Just return the list of log paths (like 'results/Log_2020-04-04_10-04-42' for example), and the associated names 108 | of these logs. 109 | Below an example of how to automatically gather all logs between two dates, and name them. 110 | """ 111 | 112 | # Using the dates of the logs, you can easily gather consecutive ones. All logs should be of the same dataset. 113 | start = 'Log_2022-04-22_11-52-58' 114 | end = 'Log_2024-05-22_11-52-58' 115 | 116 | # Name of the result path 117 | res_path = 'results' 118 | 119 | # Gather logs and sort by date 120 | logs = np.sort([join(res_path, l) for l in listdir_str(res_path) if start <= l <= end]) 121 | 122 | # Optionally add a specific log at a specific place in the log list 123 | logs = logs.astype(' Test script for ScanObjectNN experiments 11 | # 12 | 13 | 14 | # ---------------------------------------------------------------------------------------------------------------------- 15 | # 16 | # Imports and global variables 17 | # \**********************************/ 18 | # 19 | 20 | # Common libs 21 | from operator import mod 22 | import os 23 | import sys 24 | import time 25 | import signal 26 | import argparse 27 | import torch 28 | import numpy as np 29 | from torch.utils.data import DataLoader 30 | 31 | # Local libs 32 | current = os.path.dirname(os.path.realpath(__file__)) 33 | parent = os.path.dirname(os.path.dirname(current)) 34 | sys.path.append(parent) 35 | 36 | from utils.config import load_cfg, save_cfg, get_directories 37 | from utils.printing import frame_lines_1, underline 38 | 39 | from models.KPConvNet import KPFCNN as KPConvFCNN 40 | from models.KPInvNet import KPInvFCNN 41 | from models.InvolutionNet import InvolutionFCNN 42 | from models.KPNext import KPNeXt 43 | 44 | from data_handlers.object_classification import ObjClassifSampler, ObjClassifCollate 45 | from experiments.ScanObjectNN.ScanObjectNN import ScanObjectNN_cfg, ScanObjectNNDataset 46 | 47 | from tasks.test import test_model 48 | 49 | 50 | # ---------------------------------------------------------------------------------------------------------------------- 51 | # 52 | # Main Function 53 | # \*******************/ 54 | # 55 | 56 | 57 | def test_ScanObj_log(chosen_log, new_cfg, weight_path='', save_visu=False): 58 | 59 | ############## 60 | # Prepare Data 61 | ############## 62 | 63 | print('\n') 64 | frame_lines_1(['Data Preparation']) 65 | 66 | # Load dataset 67 | underline('Loading validation dataset') 68 | test_dataset = ScanObjectNNDataset(new_cfg, 69 | chosen_set='test', 70 | precompute_pyramid=True) 71 | 72 | # Calib from training data 73 | # test_dataset.calib_batch(new_cfg) 74 | # test_dataset.calib_neighbors(new_cfg) 75 | 76 | # Initialize samplers 77 | test_sampler = ObjClassifSampler(test_dataset) 78 | test_loader = DataLoader(test_dataset, 79 | batch_size=1, 80 | sampler=test_sampler, 81 | collate_fn=ObjClassifCollate, 82 | num_workers=new_cfg.test.num_workers, 83 | pin_memory=True) 84 | 85 | 86 | ############### 87 | # Build network 88 | ############### 89 | 90 | print() 91 | frame_lines_1(['Model preparation']) 92 | 93 | underline('Loading network') 94 | modulated = False 95 | if 'mod' in new_cfg.model.kp_mode: 96 | modulated = True 97 | 98 | if new_cfg.model.kp_mode in ['kpconvx', 'kpconvd']: 99 | net = KPNeXt(new_cfg) 100 | 101 | elif new_cfg.model.kp_mode.startswith('kpconv') or new_cfg.model.kp_mode.startswith('kpmini'): 102 | net = KPConvFCNN(new_cfg, modulated=modulated, deformable=False) 103 | elif new_cfg.model.kp_mode.startswith('kpdef'): 104 | net = KPConvFCNN(new_cfg, modulated=modulated, deformable=True) 105 | elif new_cfg.model.kp_mode.startswith('kpinv'): 106 | net = KPInvFCNN(new_cfg) 107 | elif new_cfg.model.kp_mode.startswith('transformer') or new_cfg.model.kp_mode.startswith('inv_'): 108 | net = InvolutionFCNN(new_cfg) 109 | elif new_cfg.model.kp_mode.startswith('kpnext'): 110 | net = KPNeXt(new_cfg, modulated=modulated, deformable=False) 111 | 112 | 113 | ######################### 114 | # Load pretrained weights 115 | ######################### 116 | 117 | if weight_path: 118 | chosen_chkp = weight_path 119 | else: 120 | chosen_chkp = os.path.join(chosen_log, 'checkpoints', 'current_chkp.tar') 121 | 122 | # Load previous checkpoint 123 | checkpoint = torch.load(chosen_chkp, map_location=torch.device('cpu')) 124 | net.load_state_dict(checkpoint['model_state_dict']) 125 | net.eval() 126 | print("\nModel and training state restored from:") 127 | print(chosen_chkp) 128 | print() 129 | 130 | 131 | ############ 132 | # Start test 133 | ############ 134 | 135 | print('\n') 136 | frame_lines_1(['Testing pretrained model']) 137 | 138 | test_path = os.path.join(chosen_log, 'test') 139 | 140 | # Go 141 | test_model(net, test_loader, new_cfg, save_visu=save_visu, test_path=test_path) 142 | 143 | return 144 | 145 | 146 | # ---------------------------------------------------------------------------------------------------------------------- 147 | # 148 | # Main Call 149 | # \***************/ 150 | # 151 | 152 | 153 | if __name__ == '__main__': 154 | 155 | 156 | ############### 157 | # Get arguments 158 | ############### 159 | 160 | # This script accepts the following arguments: 161 | # --log_path: Path to the log folder that we want to test 162 | # --weight_path: Path to the weight file that we want to test 163 | # 164 | # If you provide the weight path, it has to be in the log_path folder. 165 | # It allows you to choose a specific weight file from the log folder. 166 | 167 | # Add argument here to handle it 168 | parser = argparse.ArgumentParser() 169 | parser.add_argument('--log_path', type=str) 170 | parser.add_argument('--weight_path', type=str) 171 | parser.add_argument('--dataset_path', type=str) 172 | 173 | # Read arguments 174 | args = parser.parse_args() 175 | log_dir = args.log_path 176 | weights = args.weight_path 177 | 178 | assert os.path.exists(log_dir), f"Log folder {log_dir} does not exist." 179 | if weights: 180 | assert os.path.exists(weights), f"Weight file {weights} does not exist." 181 | assert log_dir in weights, f"Weight file {weights} is not in log folder {log_dir}." 182 | 183 | 184 | ############# 185 | # Load config 186 | ############# 187 | 188 | # Configuration parameters 189 | new_cfg = load_cfg(log_dir) 190 | 191 | # Change dataset path if provided 192 | if args.dataset_path: 193 | new_cfg.data.path = args.dataset_path 194 | 195 | 196 | ################### 197 | # Define parameters 198 | ################### 199 | 200 | # Optionally you can change some parameters from the config file. For example: 201 | # new_cfg.test.batch_limit = 1 202 | # new_cfg.test.max_votes = 15 203 | # new_cfg.augment_test.anisotropic = False 204 | # new_cfg.augment_test.scale = [0.99, 1.01] 205 | # new_cfg.augment_test.flips = [0.5, 0, 0] 206 | # new_cfg.augment_test.rotations = 'vertical' 207 | # new_cfg.augment_test.color_drop = 0.0 208 | 209 | 210 | test_ScanObj_log(log_dir, new_cfg, weight_path=weights) 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_015_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_015_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_016_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_016_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_022_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_022_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_043_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_043_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_045_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_045_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_058_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_058_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_062_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_062_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_103_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_103_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_105_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_105_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/kernels/dispositions/k_125_center_3D_0.ply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/Standalone/KPConvX/kernels/dispositions/k_125_center_3D_0.ply -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/batch_conversion.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: batch_conversion.py 10 | # > batch conversion utilities 11 | # 12 | 13 | from typing import List, Optional, Tuple 14 | 15 | import torch 16 | from torch import Tensor 17 | 18 | @torch.no_grad() 19 | def _get_indices_from_lengths(lengths: Tensor, max_length: int) -> Tensor: 20 | """Compute the indices in flattened batch tensor from the lengths in pack mode.""" 21 | length_list = lengths.detach().cpu().numpy().tolist() 22 | chunks = [(i * max_length, i * max_length + length) for i, length in enumerate(length_list)] 23 | indices = torch.cat([torch.arange(i1, i2) for i1, i2 in chunks], dim=0).to(lengths.device) 24 | return indices 25 | 26 | @torch.no_grad() 27 | def _get_slices_from_lengths(lengths: Tensor) -> Tensor: 28 | """Compute the slices indices from lengths.""" 29 | batch_end_index = torch.cumsum(lengths, dim=0) 30 | batch_start_index = batch_end_index - lengths 31 | batch_slices_index = torch.stack((batch_start_index, batch_end_index), dim=1) 32 | return batch_slices_index 33 | 34 | 35 | def batch_to_pack(batch_tensor: Tensor, masks: Optional[Tensor] = None) -> Tuple[Tensor, Tensor]: 36 | """Convert Tensor from batch mode to stack mode with masks. 37 | Args: 38 | batch_tensor (Tensor): the input tensor in batch mode (B, N, C) or (B, N). 39 | masks (BoolTensor): the masks of items of each sample in the batch (B, N). 40 | Returns: 41 | A Tensor in pack mode in the shape of (M, C) or (M). 42 | A LongTensor of the length of each sample in the batch in the shape of (B). 43 | """ 44 | if masks is not None: 45 | pack_tensor = batch_tensor[masks] 46 | lengths = masks.sum(dim=1) 47 | else: 48 | lengths = torch.full(size=(batch_tensor.shape[0],), fill_value=batch_tensor.shape[1], dtype=torch.long).to(batch_tensor.device) 49 | pack_tensor = batch_tensor 50 | return pack_tensor, lengths 51 | 52 | 53 | def pack_to_batch(pack_tensor: Tensor, lengths: Tensor, max_length=None, fill_value=0.0) -> Tuple[Tensor, Tensor]: 54 | """Convert Tensor from pack mode to batch mode. 55 | Args: 56 | pack_tensor (Tensor): The input tensors in pack mode (M, C). 57 | lengths (LongTensor): The number of items of each sample in the batch (B) 58 | max_length (int, optional): The maximal length of each sample in the batch. 59 | fill_value (float or int or bool): The default value in the empty regions. Default: 0. 60 | Returns: 61 | A Tensor in stack mode in the shape of (B, N, C), where N is max(lengths). 62 | A BoolTensor of the masks of each sample in the batch in the shape of (B, N). 63 | """ 64 | 65 | # Get batch size and maximum length of elements 66 | batch_size = lengths.shape[0] 67 | if max_length is None: 68 | max_length = lengths.max().item() 69 | 70 | # Get the batch index for each packed point 71 | tgt_indices = _get_indices_from_lengths(lengths, max_length) 72 | 73 | # Create batch tensor (B*N, C) 74 | num_channels = pack_tensor.shape[1] 75 | batch_tensor = pack_tensor.new_full(size=(batch_size * max_length, num_channels), fill_value=fill_value) 76 | batch_tensor[tgt_indices] = pack_tensor 77 | 78 | # Reshape batch tensor (B, N, C) 79 | batch_tensor = batch_tensor.view(batch_size, max_length, num_channels) 80 | 81 | # Get valid points masks 82 | masks = torch.zeros(size=(batch_size * max_length,), dtype=torch.bool).to(pack_tensor.device) 83 | masks[tgt_indices] = True 84 | masks = masks.view(batch_size, max_length) 85 | 86 | return batch_tensor, masks 87 | 88 | 89 | def pack_to_list(pack_tensor: Tensor, lengths: Tensor) -> List[Tensor]: 90 | """Convert Tensor from pack mode to list mode. 91 | Args: 92 | pack_tensor (Tensor): The input tensors in pack mode (M, C). 93 | lengths (LongTensor): The number of items of each sample in the batch (B) 94 | Returns: 95 | A List of Tensors of length B, where each element has a shape (?, C). 96 | """ 97 | 98 | # Get slices indices 99 | b_slices = _get_slices_from_lengths(lengths) 100 | 101 | # Slice packed tensor in a list of tensors 102 | batch_tensor_list = [pack_tensor[b_slice[0]:b_slice[1]] for b_slice in b_slices] 103 | 104 | return batch_tensor_list 105 | 106 | 107 | def list_to_pack(tensor_list: List[Tensor]) -> Tuple[Tensor, Tensor]: 108 | """Convert Tensor from list mode to stack mode. 109 | Args: 110 | tensor_list (Tensor): the input tensors in a list. 111 | Returns: 112 | A Tensor in pack mode in the shape of (M, C) or (M). 113 | A LongTensor of the length of each sample in the batch in the shape of (B). 114 | """ 115 | pack_tensor = torch.cat(tensor_list, dim=0) 116 | lengths = torch.LongTensor([tens.shape[0] for tens in tensor_list]).to(pack_tensor.device) 117 | return pack_tensor, lengths -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/cuda_funcs.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: cuda_funcs.py 10 | # > FPS implementation from from PointNet++ (for debug) 11 | # 12 | 13 | 14 | # Cuda extensions 15 | # import cpp_wrappers.pointnet2_batch.pointnet2_batch_cuda as pointnet2_cuda 16 | 17 | import os 18 | import torch 19 | import numpy as np 20 | 21 | 22 | @torch.no_grad() 23 | def furthest_point_sample(points, new_n=None, stride=4, min_d=0): 24 | """ 25 | Fursthest sampling controlled by the new number of points wanted. Single point cloud. 26 | Args: 27 | points (Tensor): the original points (N, D). 28 | new_n (int): the new number of points wanted . 29 | stride (int): If new_n is not provided, N is divided by stride 30 | Returns: 31 | sample_inds (LongTensor): the indices of the subsampled points (M,). 32 | """ 33 | 34 | # Verify contiguous 35 | assert points.is_contiguous() 36 | 37 | # Get dimensions 38 | N = points.shape[0] 39 | B = 1 40 | points = points.view(B, N, -1) 41 | 42 | # Create new_lengths if not provided 43 | if new_n is None: 44 | new_n = int(np.floor(N / stride)) 45 | 46 | idx = torch.cuda.IntTensor(B, new_n, device=points.device).fill_(-1) 47 | temp = torch.cuda.FloatTensor(B, N, device=points.device).fill_(1e10) 48 | pointnet2_cuda.furthest_point_sampling_wrapper(B, N, new_n, min_d, points, temp, idx) 49 | del temp 50 | idx = idx.view(-1).long() 51 | 52 | # Remove -1 indices as they are just ignored values 53 | return idx[idx > -1] 54 | 55 | 56 | @torch.no_grad() 57 | def furthest_point_sample_3(points, new_n=None, stride=4): 58 | """ 59 | Naive Fursthest sampling 60 | Args: 61 | points (Tensor): the original points (N, D). 62 | new_n (int): the new number of points wanted . 63 | stride (int): If new_n is not provided, N is divided by stride 64 | Returns: 65 | sample_inds (LongTensor): the indices of the subsampled points (M,). 66 | """ 67 | 68 | # In case no batch 69 | no_batch = points.ndim < 3 70 | if no_batch: 71 | points = points.unsqueeze(0) 72 | 73 | # Dimensions 74 | device = points.device 75 | B, N, C = points.shape 76 | 77 | # Create new_lengths if not provided 78 | if new_n is None: 79 | new_n = N // stride 80 | 81 | centroids = torch.zeros(B, new_n, dtype=torch.long).to(device) 82 | distance = torch.ones(B, N).to(device) * 1e10 83 | farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device) 84 | batch_indices = torch.arange(B, dtype=torch.long).to(device) 85 | for i in range(new_n): 86 | centroids[:, i] = farthest 87 | centroid = points[batch_indices, farthest, :].view(B, 1, 3) 88 | dist = torch.sum((points - centroid) ** 2, -1) 89 | mask = dist < distance 90 | distance[mask] = dist[mask] 91 | farthest = torch.max(distance, -1)[1] 92 | 93 | if no_batch: 94 | centroids = centroids.squeeze() 95 | return centroids 96 | -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/gpu_init.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: gpu_init.py 10 | # > GPU utilities 11 | # 12 | 13 | import os 14 | import torch 15 | 16 | global USED_GPU 17 | USED_GPU = '' 18 | 19 | 20 | # ---------------------------------------------------------------------------------------------------------------------- 21 | # 22 | # GPU Init 23 | # \**************/ 24 | # 25 | 26 | 27 | def init_gpu(gpu_id='0'): 28 | """ 29 | Initialize the USED_GPU global variable to a free GPU, or retrieve its actual value. 30 | Args: 31 | gpu_id (str): Index of the wanted GPU or 'auto' to choose a free GPU automatically. 32 | """ 33 | 34 | global USED_GPU 35 | 36 | if len(USED_GPU) == 0: 37 | if gpu_id == 'auto': 38 | 39 | # Automatic GPU choice, find a free GPU on the machine 40 | # (need pynvml to be installed) 41 | print('\nSearching a free GPU:') 42 | for i in range(torch.cuda.device_count()): 43 | a = torch.cuda.list_gpu_processes(i) 44 | print(torch.cuda.list_gpu_processes(i)) 45 | a = a.split() 46 | if a[1] == 'no': 47 | gpu_id = a[0][-1:] 48 | 49 | # Safe check no free GPU 50 | if gpu_id == 'auto': 51 | print('\nNo free GPU found!\n') 52 | a = 1 / 0 53 | 54 | else: 55 | print('Using GPU:', gpu_id, '\n') 56 | 57 | # Set GPU visible device 58 | USED_GPU = gpu_id 59 | 60 | return torch.device("cuda:" + USED_GPU) 61 | 62 | 63 | def tensor_MB(a): 64 | return round(a.element_size() * a.nelement() / 1024 / 1024, 2) -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/printing.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: printing.py 10 | # > Printing utilities 11 | # 12 | 13 | import numpy as np 14 | 15 | # Colors for printing 16 | class bcolors: 17 | HEADER = '\033[95m' 18 | OKBLUE = '\033[94m' 19 | OKGREEN = '\033[92m' 20 | WARNING = '\033[93m' 21 | FAIL = '\033[91m' 22 | ENDC = '\033[0m' 23 | BOLD = '\033[1m' 24 | UNDERLINE = '\033[4m' 25 | 26 | 27 | def underline(str0): 28 | 29 | print() 30 | print(str0) 31 | print(len(str0) * '*') 32 | 33 | return 34 | 35 | 36 | def frame_lines_1(lines, no_print=False): 37 | """ 38 | Frame a list of str lines. 39 | """ 40 | 41 | max_l = np.max([len(line) for line in lines]) 42 | lines = ['| {:<{width}s} |'.format(line, width=max_l) for line in lines] 43 | 44 | s = '\n' 45 | s += '+---' + max_l * '-' + '---+\n' 46 | for line in lines: 47 | s += line + '\n' 48 | s += '+---' + max_l * '-' + '---+\n' 49 | s += '\n' 50 | 51 | if not no_print: 52 | print(s) 53 | 54 | return s 55 | 56 | 57 | def colored_str(s, color): 58 | """ 59 | Return a colored string. 60 | """ 61 | return '{:}{:s}{:}'.format(color, s, bcolors.ENDC) 62 | 63 | 64 | def print_color(line): 65 | 66 | # color the checkmarks 67 | line = line.replace(u'\u2713', '{:}{:s}{:}'.format(bcolors.OKBLUE, u'\u2713', bcolors.ENDC)) 68 | line = line.replace(u'\u2718', '{:}{:s}{:}'.format(bcolors.FAIL, u'\u2718', bcolors.ENDC)) 69 | 70 | print(line) 71 | 72 | return 73 | 74 | 75 | def color_str(s, color): 76 | c = getattr(bcolors, color) 77 | return '{:}{:s}{:}'.format(c, s, bcolors.ENDC) 78 | 79 | 80 | def table_to_str(labels, columns, formats): 81 | """ 82 | return a string of a table representing an array of data 83 | labels (list): first table line, names of each column 84 | columns (list of list): data for each line of the table 85 | formats (list): format applied to each column 86 | """ 87 | 88 | # format columns 89 | columns_str = [[f.format(d) for d in c] for c, f in zip(columns, formats)] 90 | 91 | # Get the max length of the column 92 | columns_l = np.max([[len(d) for d in c] for c in columns_str], axis=1) 93 | columns_l = [max(len(lbl), columns_l[i]) + 2 for i, lbl in enumerate(labels)] 94 | 95 | s = '' 96 | for lbl, l in zip(labels, columns_l): 97 | s += '{:^{width}s}|'.format(lbl, width=l) 98 | s += '\n' 99 | for l in columns_l: 100 | s += '{:s}|'.format('-'*l) 101 | s += '\n' 102 | 103 | for line_i in range(len(columns_str[0])): 104 | for col_str, l in zip(columns_str, columns_l): 105 | s += '{:^{width}s}|'.format(col_str[line_i], width=l) 106 | s += '\n' 107 | 108 | return s 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/rotation.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: rotation.py 10 | # > Rotation related functions 11 | # 12 | 13 | import numpy as np 14 | 15 | import torch 16 | 17 | 18 | def create_3D_rotations(axis, angle): 19 | """ 20 | Create rotation matrices from a list of axes and angles. Code from wikipedia on quaternions 21 | :param axis: float32[N, 3] 22 | :param angle: float32[N,] 23 | :return: float32[N, 3, 3] 24 | """ 25 | 26 | t1 = np.cos(angle) 27 | t2 = 1 - t1 28 | t3 = axis[:, 0] * axis[:, 0] 29 | t6 = t2 * axis[:, 0] 30 | t7 = t6 * axis[:, 1] 31 | t8 = np.sin(angle) 32 | t9 = t8 * axis[:, 2] 33 | t11 = t6 * axis[:, 2] 34 | t12 = t8 * axis[:, 1] 35 | t15 = axis[:, 1] * axis[:, 1] 36 | t19 = t2 * axis[:, 1] * axis[:, 2] 37 | t20 = t8 * axis[:, 0] 38 | t24 = axis[:, 2] * axis[:, 2] 39 | R = np.stack([t1 + t2 * t3, 40 | t7 - t9, 41 | t11 + t12, 42 | t7 + t9, 43 | t1 + t2 * t15, 44 | t19 - t20, 45 | t11 - t12, 46 | t19 + t20, 47 | t1 + t2 * t24], axis=1) 48 | 49 | return np.reshape(R, (-1, 3, 3)) 50 | 51 | 52 | def get_random_rotations(shape=None): 53 | """ 54 | Creates random rotations in the shape asked 55 | :param shape: a list/tuple of the wanted shape: (d1, ..., dn). If None, returns a single rotation 56 | :return: AS many rotations as asked, shaped like asked: (d1, ..., dn, 3, 3) 57 | """ 58 | 59 | if shape is None: 60 | shape_tuple = (3, 3) 61 | n_rot = 1 62 | else: 63 | shape_tuple = tuple(shape) + (3, 3) 64 | n_rot = np.prod(shape) 65 | 66 | # Choose two random angles for the first vector in polar coordinates 67 | theta = np.random.rand(n_rot) * 2 * np.pi 68 | phi = (np.random.rand(n_rot) - 0.5) * np.pi 69 | 70 | # Create the first vector in carthesian coordinates 71 | u = np.stack([np.cos(theta) * np.cos(phi), np.sin(theta) * np.cos(phi), np.sin(phi)], axis=1) 72 | 73 | # Choose a random rotation angle 74 | alpha = np.random.rand(n_rot) * 2 * np.pi 75 | 76 | # Create the rotation matrix with this vector and angle 77 | R = create_3D_rotations(u, alpha) 78 | 79 | return R.reshape(shape_tuple) 80 | 81 | 82 | def get_random_vertical_rotations(shape=None): 83 | """ 84 | Creates random rotations in the shape asked 85 | :param shape: a list/tuple of the wanted shape: (d1, ..., dn). If None, returns a single rotation 86 | :return: AS many rotations as asked, shaped like asked: (d1, ..., dn, 3, 3) 87 | """ 88 | 89 | if shape is None: 90 | shape_tuple = (3, 3) 91 | n_rot = 1 92 | else: 93 | shape_tuple = tuple(shape) + (3, 3) 94 | n_rot = np.prod(shape) 95 | 96 | # Create the first vector in carthesian coordinates 97 | u = np.zeros((n_rot, 3), dtype=np.float32) 98 | u[:, 2] = 1 99 | 100 | # Choose a random rotation angle 101 | theta = np.random.rand(n_rot) * 2 * np.pi 102 | 103 | # Create the rotation matrix with this vector and angle 104 | R = create_3D_rotations(u, theta) 105 | 106 | return R.reshape(shape_tuple) 107 | -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/rsmix_provider.py: -------------------------------------------------------------------------------- 1 | # 2 | # File from RSMix https://github.com/dogyoonlee/RSMix 3 | # 4 | 5 | import os 6 | import sys 7 | import numpy as np 8 | import h5py 9 | # import tensorflow as tf 10 | import random 11 | 12 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 13 | sys.path.append(BASE_DIR) 14 | 15 | 16 | # for rsmix @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 17 | def knn_points(k, xyz, query, nsample=512): 18 | B, N, C = xyz.shape 19 | _, S, _ = query.shape # S=1 20 | 21 | tmp_idx = np.arange(N) 22 | group_idx = np.repeat(tmp_idx[np.newaxis,np.newaxis,:], B, axis=0) 23 | sqrdists = square_distance(query, xyz) # Bx1,N #제곱거리 24 | tmp = np.sort(sqrdists, axis=2) 25 | knn_dist = np.zeros((B,1)) 26 | for i in range(B): 27 | knn_dist[i][0] = tmp[i][0][k] 28 | group_idx[i][sqrdists[i]>knn_dist[i][0]]=N 29 | # group_idx[sqrdists > radius ** 2] = N 30 | # print("group idx : \n",group_idx) 31 | # group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample] # for torch.tensor 32 | group_idx = np.sort(group_idx, axis=2)[:, :, :nsample] 33 | # group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample]) 34 | tmp_idx = group_idx[:,:,0] 35 | group_first = np.repeat(tmp_idx[:,np.newaxis,:], nsample, axis=2) 36 | # repeat the first value of the idx in each batch 37 | mask = group_idx == N 38 | group_idx[mask] = group_first[mask] 39 | return group_idx 40 | 41 | 42 | def cut_points_knn(data_batch, idx, radius, nsample=512, k=512): 43 | """ 44 | input 45 | points : BxNx3(=6 with normal) 46 | idx : Bx1 one scalar(int) between 0~len(points) 47 | 48 | output 49 | idx : Bxn_sample 50 | """ 51 | B, N, C = data_batch.shape 52 | B, S = idx.shape 53 | query_points = np.zeros((B,1,C)) 54 | # print("idx : \n",idx) 55 | for i in range(B): 56 | query_points[i][0]=data_batch[i][idx[i][0]] # Bx1x3(=6 with normal) 57 | # B x n_sample 58 | group_idx = knn_points(k=k, xyz=data_batch[:,:,:3], query=query_points[:,:,:3], nsample=nsample) 59 | return group_idx, query_points # group_idx: 16x?x6, query_points: 16x1x6 60 | 61 | 62 | def cut_points(data_batch, idx, radius, nsample=512): 63 | """ 64 | input 65 | points : BxNx3(=6 with normal) 66 | idx : Bx1 one scalar(int) between 0~len(points) 67 | 68 | output 69 | idx : Bxn_sample 70 | """ 71 | B, N, C = data_batch.shape 72 | B, S = idx.shape 73 | query_points = np.zeros((B,1,C)) 74 | # print("idx : \n",idx) 75 | for i in range(B): 76 | query_points[i][0]=data_batch[i][idx[i][0]] # Bx1x3(=6 with normal) 77 | # B x n_sample 78 | group_idx = query_ball_point_for_rsmix(radius, nsample, data_batch[:,:,:3], query_points[:,:,:3]) 79 | return group_idx, query_points # group_idx: 16x?x6, query_points: 16x1x6 80 | 81 | 82 | def query_ball_point_for_rsmix(radius, nsample, xyz, new_xyz): 83 | """ 84 | Input: 85 | radius: local region radius 86 | nsample: max sample number in local region 87 | xyz: all points, [B, N, 3] 88 | new_xyz: query points, [B, S, 3] 89 | Return: 90 | group_idx: grouped points index, [B, S, nsample], S=1 91 | """ 92 | # device = xyz.device 93 | B, N, C = xyz.shape 94 | _, S, _ = new_xyz.shape 95 | # group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1]) 96 | tmp_idx = np.arange(N) 97 | group_idx = np.repeat(tmp_idx[np.newaxis,np.newaxis,:], B, axis=0) 98 | 99 | sqrdists = square_distance(new_xyz, xyz) 100 | group_idx[sqrdists > radius ** 2] = N 101 | 102 | # group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample] # for torch.tensor 103 | group_idx = np.sort(group_idx, axis=2)[:, :, :nsample] 104 | # group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample]) 105 | tmp_idx = group_idx[:,:,0] 106 | group_first = np.repeat(tmp_idx[:,np.newaxis,:], nsample, axis=2) 107 | # repeat the first value of the idx in each batch 108 | mask = group_idx == N 109 | group_idx[mask] = group_first[mask] 110 | return group_idx 111 | 112 | 113 | def square_distance(src, dst): 114 | """ 115 | Calculate Euclid distance between each two points. 116 | 117 | src^T * dst = xn * xm + yn * ym + zn * zm; 118 | sum(src^2, dim=-1) = xn*xn + yn*yn + zn*zn; 119 | sum(dst^2, dim=-1) = xm*xm + ym*ym + zm*zm; 120 | dist = (xn - xm)^2 + (yn - ym)^2 + (zn - zm)^2 121 | = sum(src**2,dim=-1)+sum(dst**2,dim=-1)-2*src^T*dst 122 | 123 | Input: 124 | src: source points, [B, N, C] 125 | dst: target points, [B, M, C] 126 | Output: 127 | dist: per-point square distance, [B, N, M] 128 | """ 129 | B, N, _ = src.shape 130 | _, M, _ = dst.shape 131 | # dist = -2 * torch.matmul(src, dst.permute(0, 2, 1)) 132 | # dist += torch.sum(src ** 2, -1).view(B, N, 1) 133 | # dist += torch.sum(dst ** 2, -1).view(B, 1, M) 134 | 135 | dist = -2 * np.matmul(src, dst.transpose(0, 2, 1)) 136 | dist += np.sum(src ** 2, -1).reshape(B, N, 1) 137 | dist += np.sum(dst ** 2, -1).reshape(B, 1, M) 138 | 139 | return dist 140 | 141 | 142 | def pts_num_ctrl(pts_erase_idx, pts_add_idx): 143 | ''' 144 | input : pts - to erase 145 | pts - to add 146 | output :pts - to add (number controled) 147 | ''' 148 | if len(pts_erase_idx)>=len(pts_add_idx): 149 | num_diff = len(pts_erase_idx)-len(pts_add_idx) 150 | if num_diff == 0: 151 | pts_add_idx_ctrled = pts_add_idx 152 | else: 153 | pts_add_idx_ctrled = np.append(pts_add_idx, pts_add_idx[np.random.randint(0, len(pts_add_idx), size=num_diff)]) 154 | else: 155 | pts_add_idx_ctrled = np.sort(np.random.choice(pts_add_idx, size=len(pts_erase_idx), replace=False)) 156 | return pts_add_idx_ctrled 157 | 158 | 159 | def rsmix(data_batch, label_batch, beta=1.0, n_sample=512, KNN=False): 160 | 161 | # Choose radius for cutting 162 | cut_rad = np.random.beta(beta, beta) 163 | 164 | # Select random pairs 165 | rand_index = np.random.choice(data_batch.shape[0],data_batch.shape[0], replace=False) # label dim : (16,) for model 166 | 167 | # Define pair labels 168 | if len(label_batch.shape) is 1: 169 | label_batch = np.expand_dims(label_batch, axis=1) 170 | label_a = label_batch[:,0] 171 | label_b = label_batch[rand_index][:,0] 172 | 173 | # Points of the second cloud of each pairs 174 | data_batch_rand = data_batch[rand_index] # BxNx3(with normal=6) 175 | 176 | # Select Cut points 177 | rand_idx_1 = np.random.randint(0,data_batch.shape[1], (data_batch.shape[0],1)) 178 | rand_idx_2 = np.random.randint(0,data_batch.shape[1], (data_batch.shape[0],1)) 179 | 180 | # Perform cut 181 | if KNN: 182 | knn_para = min(int(np.ceil(cut_rad*n_sample)),n_sample) 183 | pts_erase_idx, query_point_1 = cut_points_knn(data_batch, rand_idx_1, cut_rad, nsample=n_sample, k=knn_para) # B x num_points_in_radius_1 x 3(or 6) 184 | pts_add_idx, query_point_2 = cut_points_knn(data_batch_rand, rand_idx_2, cut_rad, nsample=n_sample, k=knn_para) # B x num_points_in_radius_2 x 3(or 6) 185 | else: 186 | pts_erase_idx, query_point_1 = cut_points(data_batch, rand_idx_1, cut_rad, nsample=n_sample) # B x num_points_in_radius_1 x 3(or 6) 187 | pts_add_idx, query_point_2 = cut_points(data_batch_rand, rand_idx_2, cut_rad, nsample=n_sample) # B x num_points_in_radius_2 x 3(or 6) 188 | 189 | # difference between query points 190 | query_dist = query_point_1[:,:,:3] - query_point_2[:,:,:3] 191 | 192 | # Replace points 193 | pts_replaced = np.zeros((1,data_batch.shape[1],data_batch.shape[2])) 194 | lam = np.zeros(data_batch.shape[0],dtype=float) 195 | for i in range(data_batch.shape[0]): 196 | 197 | 198 | if pts_erase_idx[i][0][0]==data_batch.shape[1]: 199 | tmp_pts_replaced = np.expand_dims(data_batch[i], axis=0) 200 | lam_tmp = 0 201 | 202 | 203 | elif pts_add_idx[i][0][0]==data_batch.shape[1]: 204 | pts_erase_idx_tmp = np.unique(pts_erase_idx[i].reshape(n_sample,),axis=0) 205 | tmp_pts_erased = np.delete(data_batch[i], pts_erase_idx_tmp, axis=0) # B x N-num_rad_1 x 3(or 6) 206 | dup_points_idx = np.random.randint(0,len(tmp_pts_erased), size=len(pts_erase_idx_tmp)) 207 | tmp_pts_replaced = np.expand_dims(np.concatenate((tmp_pts_erased, data_batch[i][dup_points_idx]), axis=0), axis=0) 208 | lam_tmp = 0 209 | 210 | 211 | else: 212 | pts_erase_idx_tmp = np.unique(pts_erase_idx[i].reshape(n_sample,),axis=0) 213 | pts_add_idx_tmp = np.unique(pts_add_idx[i].reshape(n_sample,),axis=0) 214 | pts_add_idx_ctrled_tmp = pts_num_ctrl(pts_erase_idx_tmp,pts_add_idx_tmp) 215 | tmp_pts_erased = np.delete(data_batch[i], pts_erase_idx_tmp, axis=0) # B x N-num_rad_1 x 3(or 6) 216 | # input("INPUT : ") 217 | tmp_pts_to_add = np.take(data_batch_rand[i], pts_add_idx_ctrled_tmp, axis=0) 218 | tmp_pts_to_add[:,:3] = query_dist[i]+tmp_pts_to_add[:,:3] 219 | 220 | tmp_pts_replaced = np.expand_dims(np.vstack((tmp_pts_erased,tmp_pts_to_add)), axis=0) 221 | 222 | lam_tmp = len(pts_add_idx_ctrled_tmp)/(len(pts_add_idx_ctrled_tmp)+len(tmp_pts_erased)) 223 | 224 | pts_replaced = np.concatenate((pts_replaced, tmp_pts_replaced),axis=0) 225 | lam[i] = lam_tmp 226 | 227 | data_batch_mixed = np.delete(pts_replaced, [0], axis=0) 228 | 229 | 230 | return data_batch_mixed, lam, label_a, label_b 231 | 232 | -------------------------------------------------------------------------------- /Standalone/KPConvX/utils/torch_pyramid.py: -------------------------------------------------------------------------------- 1 | # 2 | # For licensing see accompanying LICENSE file. 3 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 4 | # 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Hugues THOMAS - 06/10/2023 8 | # 9 | # KPConvX project: torch_pyramid.py 10 | # > input pyramid strucure of subsamplings and neighbors 11 | # 12 | 13 | import torch 14 | from torch import Tensor 15 | from typing import Tuple, List 16 | from easydict import EasyDict 17 | 18 | from utils.batch_conversion import batch_to_pack, pack_to_batch, pack_to_list, list_to_pack 19 | from utils.gpu_subsampling import subsample_pack_batch 20 | 21 | from utils.gpu_neigbors import radius_search_pack_mode, keops_radius_count 22 | from utils.cpp_funcs import furthest_point_sample_cpp, batch_knn_neighbors, batch_grid_partition 23 | 24 | from utils.cuda_funcs import furthest_point_sample 25 | 26 | 27 | # ---------------------------------------------------------------------------------------------------------------------- 28 | # 29 | # Input pyramid functions 30 | # \*****************************/ 31 | # 32 | 33 | 34 | 35 | 36 | @torch.no_grad() 37 | def build_base_pyramid(points: Tensor, 38 | lengths: Tensor): 39 | """ 40 | Only build the base of the graph pyramid, consisting of: 41 | > The subampled points for the first layer, in pack mode. 42 | > The lengths of the pack at first layer. 43 | """ 44 | 45 | # Results lists 46 | pyramid = EasyDict() 47 | pyramid.points = [] 48 | pyramid.lengths = [] 49 | pyramid.neighbors = [] 50 | pyramid.pools = [] 51 | pyramid.upsamples = [] 52 | pyramid.up_distances = [] 53 | 54 | pyramid.points.append(points) 55 | pyramid.lengths.append(lengths) 56 | 57 | return pyramid 58 | 59 | 60 | def fill_pyramid(pyramid: EasyDict, 61 | num_layers: int, 62 | sub_size: float, 63 | search_radius: float, 64 | radius_scaling: float, 65 | neighbor_limits: List[int], 66 | upsample_n: int = 1, 67 | sub_mode: str = 'grid', 68 | grid_pool_mode: bool = False): 69 | """ 70 | Fill the graph pyramid, with: 71 | > The subampled points for each layer, in pack mode. 72 | > The lengths of the pack for each layer. 73 | > The neigbors indices for convolutions. 74 | > The pooling indices (neighbors from one layer to another). 75 | > The upsampling indices (opposite of pooling indices). 76 | """ 77 | 78 | 79 | # Check if pyramid is already full 80 | if len(pyramid.neighbors) > 0: 81 | raise ValueError('Trying to fill a pyramid that already have neighbors') 82 | if len(pyramid.pools) > 0: 83 | raise ValueError('Trying to fill a pyramid that already have pools') 84 | if len(pyramid.upsamples) > 0: 85 | raise ValueError('Trying to fill a pyramid that already have upsamples') 86 | if len(pyramid.points) < 1: 87 | raise ValueError('Trying to fill a pyramid that does not have first points') 88 | if len(pyramid.lengths) < 1: 89 | raise ValueError('Trying to fill a pyramid that does not have first lengths') 90 | if len(pyramid.points) > 1: 91 | raise ValueError('Trying to fill a pyramid that already have more than one points') 92 | if len(pyramid.lengths) > 1: 93 | raise ValueError('Trying to fill a pyramid that already have more than one lengths') 94 | 95 | # Grid pool mode can only happen if method is grid 96 | grid_pool_mode = sub_mode == 'grid' and grid_pool_mode 97 | 98 | # Choose neighbor function depending on device 99 | if 'cuda' in pyramid.points[0].device.type: 100 | neighb_func = radius_search_pack_mode 101 | else: 102 | # neighb_func = batch_radius_neighbors 103 | neighb_func = batch_knn_neighbors 104 | 105 | # Subsample all point clouds on GPU 106 | points0 = pyramid.points[0] 107 | lengths0 = pyramid.lengths[0] 108 | for i in range(num_layers): 109 | 110 | if i > 0: 111 | 112 | if grid_pool_mode: 113 | sub_points, sub_lengths, poolings, upsamplings = batch_grid_partition(points0, lengths0, sub_size) 114 | pyramid.pools.append(poolings) 115 | pyramid.upsamples.append(upsamplings) 116 | points0 = sub_points 117 | lengths0 = sub_lengths 118 | 119 | 120 | else: 121 | sub_points, sub_lengths = subsample_pack_batch(points0, lengths0, sub_size, method=sub_mode) 122 | if sub_mode == 'fps': 123 | points0 = sub_points 124 | lengths0 = sub_lengths 125 | 126 | pyramid.points.append(sub_points) 127 | pyramid.lengths.append(sub_lengths) 128 | 129 | if sub_size > 0: 130 | sub_size *= radius_scaling 131 | 132 | 133 | # Find all neighbors 134 | for i in range(num_layers): 135 | 136 | # Get current points 137 | cur_points = pyramid.points[i] 138 | cur_lengths = pyramid.lengths[i] 139 | 140 | # Get convolution indices 141 | neighbors = neighb_func(cur_points, cur_points, cur_lengths, cur_lengths, search_radius, neighbor_limits[i]) 142 | pyramid.neighbors.append(neighbors) 143 | 144 | # Relation with next layer 145 | if not grid_pool_mode and i < num_layers - 1: 146 | sub_points = pyramid.points[i + 1] 147 | sub_lengths = pyramid.lengths[i + 1] 148 | 149 | # Get pooling indices 150 | subsampling_inds = neighb_func(sub_points, cur_points, sub_lengths, cur_lengths, search_radius, neighbor_limits[i]) 151 | pyramid.pools.append(subsampling_inds) 152 | 153 | if upsample_n > 0: 154 | upsampling_inds, up_dists = neighb_func(cur_points, sub_points, cur_lengths, sub_lengths, search_radius * radius_scaling, upsample_n, return_dist=True) 155 | pyramid.upsamples.append(upsampling_inds) 156 | pyramid.up_distances.append(up_dists) 157 | 158 | 159 | # Increase radius for next layer 160 | search_radius *= radius_scaling 161 | 162 | # mean_dt = 1000 * (np.array(t[1:]) - np.array(t[:-1])) 163 | # message = ' ' * 2 164 | # for dt in mean_dt: 165 | # message += ' {:5.1f}'.format(dt) 166 | # print(message) 167 | 168 | return 169 | 170 | 171 | @torch.no_grad() 172 | def build_full_pyramid(points: Tensor, 173 | lengths: Tensor, 174 | num_layers: int, 175 | sub_size: float, 176 | search_radius: float, 177 | radius_scaling: float, 178 | neighbor_limits: List[int], 179 | upsample_n: int = 1, 180 | sub_mode: str = 'grid', 181 | grid_pool_mode: bool = False): 182 | """ 183 | Build the graph pyramid, consisting of: 184 | > The subampled points for each layer, in pack mode. 185 | > The lengths of the pack. 186 | > The neigbors indices for convolutions. 187 | > The pooling indices (neighbors from one layer to another). 188 | > The upsampling indices (opposite of pooling indices). 189 | """ 190 | 191 | pyramid = build_base_pyramid(points, lengths) 192 | 193 | fill_pyramid(pyramid, 194 | num_layers, 195 | sub_size, 196 | search_radius, 197 | radius_scaling, 198 | neighbor_limits, 199 | upsample_n, 200 | sub_mode, 201 | grid_pool_mode) 202 | 203 | return pyramid 204 | 205 | 206 | @torch.no_grad() 207 | def pyramid_neighbor_stats(points: Tensor, 208 | num_layers: int, 209 | sub_size: float, 210 | search_radius: float, 211 | radius_scaling: float, 212 | sub_mode: str = 'grid'): 213 | """ 214 | Function used for neighbors calibration. Return the average number of neigbors at each layer. 215 | Args: 216 | points (Tensor): initial layer points (M, 3). 217 | num_layers (int): number of layers. 218 | sub_size (float): initial subsampling size 219 | radius (float): search radius. 220 | sub_mode (str): the subsampling method ('grid', 'ph', 'fps'). 221 | Returns: 222 | counts_list (List[Tensor]): All neigbors counts at each layers 223 | """ 224 | 225 | counts_list = [] 226 | lengths = [points.shape[0]] 227 | for i in range(num_layers): 228 | if i > 0: 229 | points, lengths = subsample_pack_batch(points, lengths, sub_size, method=sub_mode) 230 | counts = keops_radius_count(points, points, search_radius) 231 | counts_list.append(counts) 232 | if sub_size > 0: 233 | sub_size *= radius_scaling 234 | search_radius *= radius_scaling 235 | return counts_list -------------------------------------------------------------------------------- /Standalone/README.md: -------------------------------------------------------------------------------- 1 | # KPConvX: Standalone code 2 | 3 | This folder contains a standalone version of KPConvX code. 4 | 5 | ## Setup 6 | 7 | Our code was tested with multiple environments and should be straightforward to setup. Addapt the following lines to your environment and version of CUDA: 8 | 9 | ```bash 10 | conda create -n kpconvx python=3.10 11 | conda activate kpconvx 12 | conda install pytorch==2.3.0 torchvision==0.18.0 torchaudio==2.3.0 pytorch-cuda=12.1 -c pytorch -c nvidia 13 | pip install easydict h5py matplotlib numpy scikit-learn timm pykeops 14 | pip install 'pyvista[all,trame]' jupyterlab 15 | ``` 16 | 17 | 18 | ## Prepare data 19 | 20 | ### S3DIS 21 | 22 | We use the preprocessed data from Pointcept that you can download [here](https://huggingface.co/datasets/Pointcept/s3dis-compressed). Please agree with the official license before downloading it. From the `s3dis.tar.gz` archive, extract the `s3dis` folder to the `Standalone/data` directory. 23 | 24 | ### ScanObjectNN 25 | 26 | We use the h5_files from [official website](https://hkust-vgd.github.io/scanobjectnn/). Download the preprocessed version and place it in the `Standalone/data` directory. Rename the folder `h5_files` to `ScanObjectNN`. You should have (and only need) the three following files: 27 | ``` 28 | data/ScanObjectNN/main_split/test_objectdataset_augmentedrot_scale75_1024_fps.pkl 29 | data/ScanObjectNN/main_split/test_objectdataset_augmentedrot_scale75.h5 30 | data/ScanObjectNN/main_split/training_objectdataset_augmentedrot_scale75.h5 31 | ``` 32 | 33 | ## Experiments 34 | 35 | 36 | ### Training networks 37 | 38 | We provide scripts to train our model on ScanObjectNN or S3DIS. 39 | 40 | ```bash 41 | # Training on ScanObjectNN 42 | ./train_ScanObjectNN.sh 43 | 44 | # Training on S3DIS 45 | ./train_S3DIS.sh 46 | ``` 47 | 48 | Follow instructions in these scripts to change parameters. 49 | 50 | 51 | ### Plotting functions 52 | 53 | We provide plotting functions to plot the performances during and after training. They are in the `plot_ScanObj.py` and `plot_S3DIS.py` script. Here are 54 | 55 | - Step 1: Define the dates of the logs you want to plot in the experiment functions. See example functions `experiment_name_1()` and `experiment_name_2()` 56 | ```python 57 | def experiment_name_1(): 58 | ... 59 | start = 'Log_2020-04-22_11-52-58' 60 | end = 'Log_2023-07-29_12-40-27' 61 | ... 62 | ``` 63 | 64 | - Step 2: Choose the log to show 65 | ```python 66 | # Choose the logs to show 67 | logs, logs_names = experiment_name_1() 68 | ``` 69 | 70 | - Step 3: Run the script 71 | ```bash 72 | python3 experiments/ScanObjectNN/plot_ScanObj.py 73 | # or 74 | python3 experiments/S3DIS/plot_S3DIS.py 75 | ``` 76 | 77 | Once the trainings are finished, you can change to test mode with `perform_test = True`. This will start tests for the selected trained weights and show a summary of the test results. 78 | 79 | 80 | ### Test models 81 | 82 | You can also directly test a trained network with the following scripts. 83 | 84 | ```bash 85 | # Test a model on ScanObjectNN 86 | ./test_ScanObjectNN.sh 87 | 88 | # Test a model on S3DIS 89 | ./test_S3DIS.sh 90 | ``` 91 | 92 | More detailed instructions are in these scripts. 93 | 94 | 95 | ### Pretrained weights 96 | 97 | We provide the following pretrained models: 98 | 99 | | Model | Benchmark | OA | mAcc | Size | Archive | 100 | | :---: | :---: | :---: | :---: | :---: | :---: | 101 | | KPConvD-L | ScanObjectNN | 89.7% | 88.5% | 80 MB | [link](https://ml-site.cdn-apple.com/models/kpconvx/ScanObjectNN_KPConvD-L.zip) | 102 | | KPConvX-L | ScanObjectNN | 89.1% | 87.6% | 138 MB | [link](https://ml-site.cdn-apple.com/models/kpconvx/ScanObjectNN_KPConvX-L.zip) | 103 | 104 | | Model | Benchmark | Val mIoU | Size | Archive | 105 | | :---: | :---: | :---: | :---: | :---: | 106 | | KPConvD-L | S3DIS (Area5) | 72.3% | 151 MB | [link](https://ml-site.cdn-apple.com/models/kpconvx/S3DIS_KPConvD-L.zip) | 107 | | KPConvX-L | S3DIS (Area5) | 73.5% | 169 MB | [link](https://ml-site.cdn-apple.com/models/kpconvx/S3DIS_KPConvX-L.zip) | 108 | 109 | You can download and extract them to the result folder and use our scripts to test them. 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Standalone/test_S3DIS.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # For licensing see accompanying LICENSE file. 4 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 5 | # 6 | 7 | # Go to the root directory 8 | cd KPConvX 9 | export PYTHONPATH=$PWD:$PYTHONPATH 10 | 11 | ############ 12 | # Parameters 13 | ############ 14 | 15 | # Change the path to your dataset here 16 | ARGS="--dataset_path $PWD/../data/s3dis" 17 | 18 | # Choose the log path here 19 | LOG_PATH="$PWD/results/S3DIS_KPConvX-L" 20 | ARGS="$ARGS --log_path $LOG_PATH" 21 | 22 | # # Optionally, you can choose a specific weight file 23 | # ARGS="$ARGS --weight_path $LOG_PATH/checkpoints/current_chkp.tar" 24 | 25 | # If you provide the weight path, it has to be in the log_path folder. 26 | # It allows you to choose a specific weight file from the log folder. 27 | 28 | # Finally you can decide to profile the network speed instead of testing its performances 29 | # ARGS="$ARGS --profile" 30 | 31 | 32 | ############### 33 | # Main function 34 | ############### 35 | 36 | # Define the experiment and script to run 37 | EXP="S3DIS" 38 | SCRIPT="test_S3DIS.py" 39 | 40 | # Start the training 41 | python3 experiments/$EXP/$SCRIPT $ARGS 42 | 43 | -------------------------------------------------------------------------------- /Standalone/test_ScanObjectNN.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # For licensing see accompanying LICENSE file. 4 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 5 | # 6 | 7 | # Go to the root directory 8 | cd KPConvX 9 | export PYTHONPATH=$PWD:$PYTHONPATH 10 | 11 | ############ 12 | # Parameters 13 | ############ 14 | 15 | # Change the path to your dataset here 16 | ARGS="--dataset_path $PWD/../data/ScanObjectNN/main_split" 17 | 18 | # Choose the log path here 19 | LOG_PATH="$PWD/results/ScanObjectNN_KPConvD-L" 20 | ARGS="$ARGS --log_path $LOG_PATH" 21 | 22 | # # Optionally, you can choose a specific weight file 23 | # ARGS="$ARGS --weight_path $LOG_PATH/checkpoints/current_chkp.tar" 24 | 25 | # If you provide the weight path, it has to be in the log_path folder. 26 | # It allows you to choose a specific weight file from the log folder. 27 | 28 | 29 | ############### 30 | # Main function 31 | ############### 32 | 33 | # Define the experiment and script to run 34 | EXP="ScanObjectNN" 35 | SCRIPT="test_ScanObj.py" 36 | 37 | # Start the training 38 | python3 experiments/$EXP/$SCRIPT $ARGS 39 | 40 | -------------------------------------------------------------------------------- /Standalone/train_S3DIS.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # For licensing see accompanying LICENSE file. 4 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 5 | # 6 | 7 | # Go to the root directory 8 | cd KPConvX 9 | export PYTHONPATH=$PWD:$PYTHONPATH 10 | 11 | 12 | ############ 13 | # Parameters 14 | ############ 15 | 16 | # You can change the path to your dataset here 17 | ARGS="--dataset_path $PWD/../data/s3dis" 18 | 19 | # Here you can define arguments to change network/training parameters. 20 | # For example: 21 | # ARGS="$ARGS --in_radius 1.5" 22 | # ARGS="$ARGS --layer_blocks 3 3 9 12 3" 23 | # ARGS="$ARGS --batch_size 32 --accum_batch 2" 24 | # Have a look at the train.py file to see all available parameters. 25 | # You can also change the values of these parameters directly in the train.py file. 26 | 27 | 28 | ############### 29 | # Main function 30 | ############### 31 | 32 | # Define the experiment and script to run 33 | EXP="S3DIS" 34 | SCRIPT="train_S3DIS.py" 35 | 36 | # Start the training 37 | python3 experiments/$EXP/$SCRIPT $ARGS 38 | 39 | -------------------------------------------------------------------------------- /Standalone/train_ScanObjectNN.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # For licensing see accompanying LICENSE file. 4 | # Copyright (C) 2024 Apple Inc. All Rights Reserved. 5 | # 6 | 7 | # Go to the root directory 8 | cd KPConvX 9 | export PYTHONPATH=$PWD:$PYTHONPATH 10 | 11 | 12 | ############ 13 | # Parameters 14 | ############ 15 | 16 | # You can change the path to your dataset here 17 | ARGS="--dataset_path $PWD/../data/ScanObjectNN/main_split" 18 | 19 | # Here you can define arguments to change network/training parameters. 20 | # For example: 21 | # ARGS="$ARGS --in_radius 1.5" 22 | # ARGS="$ARGS --layer_blocks 3 3 9 12 3" 23 | # ARGS="$ARGS --batch_size 32 --accum_batch 2" 24 | # Have a look at the train.py file to see all available parameters. 25 | # You can also change the values of these parameters directly in the train.py file. 26 | 27 | 28 | ############### 29 | # Main function 30 | ############### 31 | 32 | # Define the experiment and script to run 33 | EXP="ScanObjectNN" 34 | SCRIPT="train_ScanObj.py" 35 | 36 | # Start the training 37 | python3 experiments/$EXP/$SCRIPT $ARGS 38 | 39 | -------------------------------------------------------------------------------- /assets/fig_kpconvx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apple/ml-kpconvx/54e644a9f3bddd4c344a58193897a44582b0fea4/assets/fig_kpconvx.png --------------------------------------------------------------------------------