├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── conda ├── bld.bat ├── build.sh ├── conda_build_config.yaml └── meta.yaml ├── environment.yml ├── generators ├── __init__.py ├── config.py ├── definitions │ ├── __init__.py │ ├── constructor.py │ ├── enum.py │ ├── function.py │ ├── method.py │ ├── method_parameters.py │ ├── module_variable.py │ ├── submodule_loader.py │ ├── templated_class.py │ └── variable.py ├── dependency_tree.py ├── generate_point_types.py ├── generate_pybind11_bindings.py ├── generate_subsample.py ├── generate_yaml_point_types.py ├── instantiations.py ├── point_types_extra.yml ├── point_types_generated.yml ├── point_types_utils.py └── utils.py ├── pclpy ├── __init__.py ├── __version__.py ├── api.py ├── io │ ├── __init__.py │ ├── functions.py │ └── las.py ├── src │ ├── eigen_bind.hpp │ ├── make_opaque_vectors.hpp │ ├── pclpy.cpp │ ├── point_cloud_buffers.hpp │ ├── point_cloud_from_array.hpp │ ├── point_types.hpp │ └── vector_classes.hpp ├── tests │ ├── __init__.py │ ├── test.py │ ├── test_data │ │ ├── bunny.pcd │ │ ├── street.las │ │ ├── street_thinned.las │ │ └── street_thinned_with_normals.las │ ├── test_eigen.py │ ├── test_features.py │ ├── test_filters.py │ ├── test_io.py │ ├── test_octree.py │ ├── test_point_types.py │ ├── test_sample_consensus.py │ ├── test_segmentation.py │ ├── test_surface.py │ ├── tests_initialize.py │ └── utils.py ├── utils.py └── view │ ├── __init__.py │ └── cloudcompare.py ├── pkgconfig_utils.py ├── requirements-dev.txt ├── requirements.txt ├── scripts ├── conda_build.bat ├── conda_build.sh ├── download_pcl.ps1 ├── download_pcl.sh ├── generate_hpp_point_types.sh ├── generate_points_and_bindings.bat └── generate_points_and_bindings.sh └── setup.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "*" 5 | pull_request: 6 | branches: 7 | - "*" 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | build-linux: 13 | name: Build Linux 14 | runs-on: "ubuntu-latest" 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: goanpeca/setup-miniconda@v1.0.2 18 | with: 19 | auto-update-conda: true 20 | activate-environment: pclpy 21 | environment-file: environment.yml 22 | python-version: 3.6 23 | - name: Install ccache 24 | run: sudo apt-get install -y ccache 25 | - name: Get timestamp 26 | uses: gerred/actions/current-time@master 27 | id: current-time 28 | - uses: actions/cache@v1 29 | id: cache 30 | with: 31 | path: ~/.ccache 32 | key: ${{ runner.os }}-ccache-${{ steps.current-time.outputs.time }} 33 | restore-keys: | 34 | ${{ runner.os }}-ccache- 35 | - name: Ccache stats 36 | run: | 37 | ccache -s 38 | ccache -z 39 | - name: Setup build environment 40 | shell: bash -l {0} 41 | run: | 42 | python -m pip install -r requirements-dev.txt 43 | scripts/download_pcl.sh 44 | scripts/generate_points_and_bindings.sh 45 | - shell: bash -l {0} 46 | run: python setup.py build 47 | - shell: bash -l {0} 48 | run: python -m pip install --no-dependencies -e . 49 | - shell: bash -l {0} 50 | run: pytest 51 | - name: Ccache stats 52 | if: always() 53 | run: ccache -s 54 | build-windows: 55 | name: Build Windows 56 | runs-on: "windows-latest" 57 | steps: 58 | - uses: actions/checkout@v2 59 | - uses: s-weigand/setup-conda@v1 60 | with: 61 | activate-conda: true 62 | update-conda: true 63 | conda-channels: conda-forge 64 | - run: conda create -n pclpy -c conda-forge python=3.6 65 | - run: conda env update -n pclpy -f environment.yml 66 | - run: conda run -n pclpy python -c "import sys; print(sys.version)" 67 | - run: conda run -n pclpy python -m pip install -r requirements-dev.txt 68 | - name: Download pcl 69 | shell: pwsh 70 | run: scripts/download_pcl.ps1 71 | - run: conda run -n pclpy scripts/generate_points_and_bindings.bat 72 | - run: conda run -n pclpy python setup.py build 73 | - run: conda run -n pclpy python -m pip install --no-dependencies -e . 74 | - run: conda run -n pclpy pytest 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | .cache 4 | .idea 5 | build 6 | dist 7 | *egg* 8 | vc140.pdb 9 | *.pyd 10 | street2.las 11 | generated_modules 12 | var 13 | *.env 14 | tmp 15 | pcl-pcl-* 16 | errors.txt 17 | log.txt 18 | 19 | pclpy/pcl.cpython* 20 | 21 | .vscode 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 David Caron 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt pkgconfig_utils.py 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pclpy: PCL for python 2 | 3 | 4 | [![conda](https://img.shields.io/conda/v/davidcaron/pclpy?label=conda)](https://anaconda.org/davidcaron/pclpy) 5 | [![Python version](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8-blue)](https://anaconda.org/davidcaron/pclpy) 6 | [![conda](https://img.shields.io/conda/pn/davidcaron/pclpy?color=orange)](https://anaconda.org/davidcaron/pclpy) 7 | [![PCL version](https://img.shields.io/badge/PCL-1.9.1-blue)](https://anaconda.org/conda-forge/pcl) 8 | 9 | ![.github/workflows/ci.yml](https://github.com/davidcaron/pclpy/workflows/.github/workflows/ci.yml/badge.svg) 10 | 11 | Python bindings for the Point Cloud Library (PCL). 12 | Generated from headers using CppHeaderParser and pybind11. 13 | 14 | Install using conda: `conda install -c conda-forge -c davidcaron pclpy` (see _Installation_ below) 15 | 16 | Contributions, issues, comments are welcome! 17 | 18 | Github repository: https://www.github.com/davidcaron/pclpy 19 | 20 | ## Motivation 21 | 22 | Many other python libraries tried to bind PCL. 23 | The most popular one being python-pcl, which uses Cython. 24 | While Cython is really powerful, binding C++ templates isn't one of 25 | its strenghts (and PCL uses templates heavily). 26 | The result for python-pcl is a lot of code repetition, which is hard 27 | to maintain and to add features to, and incomplete bindings of PCL's classes 28 | and point types. 29 | 30 | Using pybind11, we use C++ directly. Templates, boost::smart_ptr and 31 | the buffer protocol are examples of things that are simpler to implement. 32 | 33 | The results so far are very promising. A large percentage of PCL is covered. 34 | 35 | ## Installation 36 | 37 | We use conda to release pclpy. To install, use this command: 38 | 39 | `conda install -c conda-forge -c davidcaron pclpy` 40 | 41 | Don't forget to add both channels, or else conda won't be able to find all dependencies. 42 | 43 | **Windows**: python **3.6** and **3.7** are supported 44 | 45 | **Linux**: python **3.6**, **3.7** and **3.8** are supported 46 | 47 | 48 | ## Features 49 | 50 | - Most point types are implemented (those specified by `PCL_ONLY_CORE_POINT_TYPES` in PCL) 51 | - You can get a numpy view of point cloud data using python properties (e.g. `cloud.x` or `cloud.xyz`) 52 | - boost::shared_ptr is handled by pybind11 so it's completely abstracted at the python level 53 | - laspy integration for reading/writing las files 54 | 55 | ## Example 56 | 57 | You can use either a high level, more pythonic api, or the wrapper over the PCL api. 58 | The wrapper is meant to be as close as possible to the original PCL C++ api. 59 | 60 | Here is how you would use the library to process Moving Least Squares. 61 | See the PCL documentation: http://pointclouds.org/documentation/tutorials/resampling.php 62 | 63 | Using the higher level api: 64 | 65 | ```python 66 | import pclpy 67 | 68 | # read a las file 69 | point_cloud = pclpy.read("street.las", "PointXYZRGBA") 70 | # compute mls 71 | output = point_cloud.moving_least_squares(search_radius=0.05, compute_normals=True, num_threads=8) 72 | ``` 73 | 74 | Or the wrapper over the PCL api: 75 | 76 | ```python 77 | import pclpy 78 | from pclpy import pcl 79 | 80 | point_cloud = pclpy.read("street.las", "PointXYZRGBA") 81 | mls = pcl.surface.MovingLeastSquaresOMP.PointXYZRGBA_PointNormal() 82 | tree = pcl.search.KdTree.PointXYZRGBA() 83 | mls.setSearchRadius(0.05) 84 | mls.setPolynomialFit(False) 85 | mls.setNumberOfThreads(12) 86 | mls.setInputCloud(point_cloud) 87 | mls.setSearchMethod(tree) 88 | mls.setComputeNormals(True) 89 | output = pcl.PointCloud.PointNormal() 90 | mls.process(output) 91 | ``` 92 | 93 | You can see the wrapper is very close to the C++ version: 94 | 95 | ``` c++ 96 | // C++ version 97 | 98 | pcl::PointCloud::Ptr point_cloud (new pcl::PointCloud ()); 99 | pcl::io::loadPCDFile ("bunny.pcd", *point_cloud); 100 | pcl::MovingLeastSquaresOMP mls; 101 | pcl::search::KdTree::Ptr tree (new pcl::search::KdTree); 102 | mls.setSearchRadius (0.05); 103 | mls.setPolynomialFit (false); 104 | mls.setNumberOfThreads (12); 105 | mls.setInputCloud (point_cloud); 106 | mls.setSearchMethod (tree); 107 | mls.setComputeNormals (true); 108 | pcl::PointCloud output; 109 | mls.process (output); 110 | ``` 111 | 112 | ## Modules 113 | - 2d 114 | - common 115 | - geometry 116 | - features 117 | - filters 118 | - io (meshes are not implemented) 119 | - kdtree 120 | - keypoints 121 | - octree 122 | - recognition 123 | - sample_consensus 124 | - search 125 | - segmentation 126 | - stereo 127 | - surface 128 | - tracking 129 | #### These modules are skipped for now 130 | - ml 131 | - people 132 | - outofcore 133 | - registration 134 | - visualization 135 | - every module not in the PCL Windows release (gpu, cuda, etc.) 136 | 137 | ## Not Implemented 138 | (see [github issues](https://github.com/davidcaron/pclpy/issues) 139 | and the _what to skip_ section in `generators/config.py`) 140 | 141 | ## Building 142 | 143 | Build scripts are in the `scripts` folder. 144 | 145 | 1. Create your conda environment: 146 | `conda env create -n pclpy -f environment.yml` 147 | 148 | 2. Activate your environment: 149 | `conda activate pclpy` 150 | 151 | 3. Install development dependencies: 152 | `pip install -r requirements-dev.txt` 153 | 154 | 4. Download a copy of PCL 155 | Windows: `powershell scripts\download_pcl.ps1` 156 | Linux: `scripts\download_pcl.sh` 157 | 158 | 5. Generate pybind11 bindings 159 | Windows: `powershell scripts\generate_points_and_bindings.ps1` 160 | Linux: `scripts\generate_points_and_bindings.sh` 161 | 162 | 6. For development, build inplace using python 163 | `python setup.py build_ext -i` 164 | 165 | For a release, use the scripts/conda_build.bat (or conda_build.sh) script 166 | 167 | On Windows, these setup.py arguments can cpeed up the build: 168 | - --msvc-mp-build enables a multiprocessed build 169 | - --msvc-no-code-link makes the linking step faster (not meant for releases) 170 | - --use-clcache to cache msvc builds (clcache must be installed) 171 | 172 | ## Roadmap 173 | - Wrap as much of PCL as reasonably possible 174 | - More tests 175 | -------------------------------------------------------------------------------- /conda/bld.bat: -------------------------------------------------------------------------------- 1 | 2 | IF not exist pcl-pcl-1.9.1/nul ( powershell -ExecutionPolicy ByPass -File scripts\download_pcl.ps1 ) 3 | 4 | IF not exist pclpy/src/generated_modules/nul ( call scripts\generate_points_and_bindings.bat ) 5 | 6 | %PYTHON% -m pip install . --no-deps -vv 7 | if errorlevel 1 exit 1 8 | -------------------------------------------------------------------------------- /conda/build.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | version="1.9.1" 4 | 5 | 6 | if [[ ! -d "pcl-pcl-$version" ]]; then 7 | scripts/download_pcl.sh 8 | fi 9 | 10 | if [[ ! -d "pclpy/src/generated_modules" ]]; then 11 | python -m pip install https://github.com/davidcaron/CppHeaderParser/archive/master.zip 12 | scripts/generate_points_and_bindings.sh 13 | python -m pip uninstall CppHeaderParser 14 | fi 15 | 16 | 17 | python -m pip install . --no-deps -vv 18 | -------------------------------------------------------------------------------- /conda/conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | python: 2 | - 3.6 3 | - 3.7 4 | - 3.8 # [not win] 5 | -------------------------------------------------------------------------------- /conda/meta.yaml: -------------------------------------------------------------------------------- 1 | package: 2 | name: "pclpy" 3 | version: "0.12.0" 4 | 5 | source: 6 | path: .. 7 | 8 | build: 9 | number: 1 10 | 11 | requirements: 12 | channels: 13 | - conda-forge 14 | build: 15 | - {{ compiler('cxx') }} # [win] 16 | host: 17 | - python {{ python }} 18 | - pip 19 | - pkgconfig # [win] 20 | - pcl=1.9.1 21 | - eigen 22 | - numpy>=1.18 23 | - pybind11>=2.4 24 | - PyYAML 25 | - inflection 26 | - unidecode 27 | - ply 28 | - qhull=2019.1 # exact version needed for pcl 1.9.1 29 | # - https://github.com/davidcaron/CppHeaderParser/archive/master.zip 30 | run: 31 | - python {{ python }} 32 | - pcl=1.9.1 33 | - eigen 34 | - numpy>=1.18 35 | - laspy 36 | 37 | test: 38 | requires: 39 | - pytest 40 | imports: 41 | - pclpy 42 | commands: 43 | - pytest --pyargs pclpy.tests 44 | 45 | about: 46 | home: https://github.com/davidcaron/pclpy 47 | license: MIT 48 | license_family: MIT 49 | summary: Python bindings for the Point Cloud Library (PCL) 50 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pclpy 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - pcl=1.9.1 6 | - eigen 7 | - numpy 8 | - laspy 9 | - pkgconfig 10 | -------------------------------------------------------------------------------- /generators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pclpy/506a57b60633d1c4280d2f42505997ba9bd4df3a/generators/__init__.py -------------------------------------------------------------------------------- /generators/definitions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pclpy/506a57b60633d1c4280d2f42505997ba9bd4df3a/generators/definitions/__init__.py -------------------------------------------------------------------------------- /generators/definitions/constructor.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from CppHeaderParser import CppMethod 4 | 5 | from generators.config import INHERITED_ENUMS, CUSTOM_OVERLOAD_TYPES 6 | from generators.utils import clean_doxygen 7 | 8 | 9 | class Constructor: 10 | def __init__(self, constructor: CppMethod): 11 | """ 12 | Generates definition for a class constructor 13 | Example: 14 | cls.def(py::init(), "info"_a="something"); 15 | """ 16 | self.cppconstructor = constructor 17 | self.params = self.cppconstructor["parameters"] 18 | self.filter_parameters() 19 | 20 | def filter_parameters(self): 21 | filtered = [] 22 | for param in self.params: 23 | if param["type"] == "void": 24 | continue 25 | filtered.append(param) 26 | self.params = filtered 27 | 28 | def to_str(self, class_var_name, class_enums_names): 29 | 30 | def default(p): 31 | val = p.get("defaultValue", "") 32 | if val: 33 | if val in class_enums_names: 34 | val = "Class::" + val 35 | elif p.get('enum') and p.get('enum') not in val: 36 | val = p.get('enum') + '::' + val 37 | val = val.replace(" ", "") # CppHeaderParser adds a space to float values 38 | if re.search(r"(? "50.0f" 39 | val = val.replace('f', '.0f') 40 | search = re.search(r"1e(-?\d+)", val) # "1e-4.0" -> "0.0001" 41 | if search: 42 | val = str(1 * 10 ** int(search.group(1))) 43 | val = "=" + val 44 | return val 45 | 46 | def init_param_type(param): 47 | type_ = param["raw_type"] 48 | type_only_last_element = type_.split("::")[-1] 49 | class_with_param_name = (param["method"]["name"], param["raw_type"]) 50 | class_typedefs = param["method"]["parent"]["typedefs"]["public"] 51 | custom = CUSTOM_OVERLOAD_TYPES.get((param["method"]["parent"]["name"], type_)) 52 | if custom: 53 | type_ = custom 54 | elif param.get("enum"): 55 | if param.get('enum').startswith('pcl::'): 56 | type_ = param.get('enum') 57 | else: 58 | type_ = "Class::%s" % param.get("enum").split("::")[-1] 59 | elif type_only_last_element in class_typedefs: 60 | type_ = type_only_last_element 61 | elif class_with_param_name in INHERITED_ENUMS: 62 | type_ = "Class::" + type_only_last_element 63 | if all(c in type_ for c in "<>"): 64 | type_ = "typename " + type_ 65 | if param.get("pointer"): 66 | type_ += "*" 67 | if param["constant"] and not type_.startswith("const "): 68 | type_ = "const " + type_ 69 | if param["reference"] and not type_.endswith("&"): 70 | type_ += " &" 71 | return type_ 72 | 73 | if any("**" in param["type"].replace(" ", "") for param in self.params): 74 | message = "Double pointer arguments are not supported by pybind11 (%s)" % (self.cppconstructor["name"],) 75 | return "// " + message 76 | 77 | if len(self.params): 78 | s = '{cls_var}.def(py::init<{params_types}>(), {params_names}, R"({doc})")' 79 | types = ", ".join([init_param_type(p) for p in self.params]) 80 | names = ", ".join(['"%s"_a%s' % (p["name"], default(p)) for p in self.params]) 81 | data = {"params_types": types, 82 | "params_names": names, 83 | "cls_var": class_var_name} 84 | else: 85 | s = '{cls_var}.def(py::init<>(), R"({doc})")' 86 | data = {"cls_var": class_var_name} 87 | data["doc"] = clean_doxygen(self.cppconstructor.get("doxygen", "")) 88 | return s.format(**data) 89 | -------------------------------------------------------------------------------- /generators/definitions/enum.py: -------------------------------------------------------------------------------- 1 | from CppHeaderParser import CppVariable 2 | 3 | from generators.config import INDENT 4 | 5 | 6 | class Enum: 7 | def __init__(self, enum: CppVariable): 8 | """ 9 | Generates definition for an enum 10 | Example: 11 | py::enum_(pet, "Kind") 12 | .value("Dog", Pet::Kind::Dog) 13 | .value("Cat", Pet::Kind::Cat) 14 | .export_values(); 15 | """ 16 | self.cppenum = enum 17 | self.name = enum["name"] 18 | 19 | def to_str(self, prefix, class_var_name): 20 | prefix = prefix[:-2] if prefix.endswith("::") else prefix 21 | s = [] 22 | a = s.append 23 | a('using {cppname_lower} = {typename}{class_name}{cppname};') 24 | a('{i}py::enum_<{cppname_lower}>({parent_var}, "{name}")') 25 | for value in self.cppenum["values"]: 26 | a('{i}{i}.value("%s", {class_name}{cppname}::%s)' % (value["name"], value["name"])) 27 | a("{i}{i}.export_values()") 28 | typename = "typename " if "<" in self.cppenum["name"] or prefix == "Class" else "" 29 | data = {"name": self.name, 30 | "i": INDENT, 31 | "cppname": self.cppenum["name"], 32 | "cppname_lower": self.cppenum["name"].lower(), 33 | "class_name": ("%s::" % prefix) if prefix else "", 34 | "parent_var": class_var_name, 35 | "typename": typename 36 | } 37 | ret_val = "\n".join(s).format(**data) 38 | return ret_val 39 | 40 | def __repr__(self): 41 | return "" % (self.name,) 42 | -------------------------------------------------------------------------------- /generators/definitions/function.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, OrderedDict 2 | from itertools import product 3 | from typing import List 4 | 5 | from CppHeaderParser import CppMethod 6 | from inflection import camelize 7 | 8 | from generators.config import INDENT, FUNCTIONS_TO_SKIP 9 | from generators.definitions.method import Method 10 | from generators.definitions.method import filter_template_types, template_types_generator 11 | from generators.point_types_utils import filter_types 12 | from generators.utils import function_definition_name 13 | 14 | 15 | def filter_functions(cppfunctions, header_name): 16 | filtered = [] 17 | 18 | for f in cppfunctions: 19 | if "<" in f["name"] or ">" in f["name"]: 20 | continue 21 | if (header_name, f["name"]) in FUNCTIONS_TO_SKIP: 22 | continue 23 | if "::" in f["rtnType"].replace(" ", ""): # bug in CppHeaderParser for methods defined outside class 24 | continue 25 | if f["template"] and f["template"].replace(" ", "") == "template<>": # skip specialized templated functions 26 | continue 27 | filtered.append(f) 28 | 29 | return filtered 30 | 31 | 32 | def split_templated_functions_with_same_name(cppfunctions): 33 | filtered, has_other_templated_with_same_name = [], [] 34 | by_name = defaultdict(list) 35 | for f in cppfunctions: 36 | by_name[f["name"]].append(f) 37 | 38 | for f in cppfunctions: 39 | other_templated_with_same_name = any(other["template"] and other["template"].replace(" ", "") 40 | for other in by_name[f["name"]] 41 | if not other == f) 42 | 43 | if other_templated_with_same_name: 44 | if f["template"]: 45 | # Todo: Implement templated functions disambiguation here? 46 | continue 47 | has_other_templated_with_same_name.append(f) 48 | else: 49 | filtered.append(f) 50 | 51 | return filtered, has_other_templated_with_same_name 52 | 53 | 54 | def get_methods_defined_outside(cppfunctions): 55 | filtered = [] 56 | for f in cppfunctions: 57 | if "::" in f["rtnType"].replace(" ", ""): # bug in CppHeaderParser for methods defined outside class 58 | filtered.append(f) 59 | return filtered 60 | 61 | 62 | def generate_function_definitions(cppfunctions: List[CppMethod], 63 | module_name, 64 | header_name, 65 | indent="", 66 | not_every_point_type=False): 67 | cppfunctions = filter_functions(cppfunctions, header_name) 68 | cppfunctions = list(sorted(cppfunctions, key=lambda x: x["name"])) 69 | cppfunctions, cppfunctions_with_other_templated = split_templated_functions_with_same_name(cppfunctions) 70 | 71 | functions = [Method(f, is_an_overload=True) for f in cppfunctions] 72 | for f in cppfunctions_with_other_templated: 73 | functions.append(Method(f, is_an_overload=True, use_c_overload=True)) 74 | 75 | s = [] 76 | a = s.append 77 | i = INDENT 78 | 79 | # group functions by template types 80 | templated_functions_grouped = defaultdict(list) 81 | for f in functions: 82 | template = f.cppmethod["template"] 83 | if template: 84 | template = template.replace("\n", "") 85 | pos = template.find("<") 86 | template_types = filter_template_types(template[pos + 1:-1], keep=["typename", "class"]) 87 | all_templated_types = filter_template_types(template[pos + 1:-1], keep_all=True) 88 | f.templated_types = OrderedDict(((t, [t]) for t in all_templated_types)) 89 | templated_functions_grouped[template_types].append(f) 90 | else: 91 | templated_functions_grouped[tuple()].append(f) 92 | 93 | templated_functions_grouped = OrderedDict(sorted(templated_functions_grouped.items())) 94 | 95 | for n, (type_names, group) in enumerate(templated_functions_grouped.items(), 1): 96 | if type_names: 97 | a(group[0].cppmethod.get("template")) 98 | a("{ind}void define{sub}{name}Functions%s(py::module &m) {ob}" % n) 99 | for f in group: 100 | function_prefix = f.cppmethod["namespace"] 101 | function_prefix = function_prefix[:-2] if function_prefix.endswith("::") else function_prefix 102 | value = f.to_str(function_prefix, "m") 103 | if f.templated_types: 104 | value = value[0] 105 | a("{ind}{i}%s;" % value) 106 | a("{cb}") 107 | a("") 108 | 109 | a("{ind}void define{sub}{name}Functions(py::module &m) {ob}") 110 | for n, (type_names, group) in enumerate(templated_functions_grouped.items(), 1): 111 | if type_names: 112 | types = [t[1] for t in template_types_generator(type_names, header_name, group[0].cppmethod["name"])] 113 | if isinstance(types[0], str): 114 | all_types = filter_types(types) if not_every_point_type else types 115 | elif isinstance(types[0], list): 116 | if not_every_point_type: 117 | types = list(map(filter_types, types)) 118 | all_types = list(product(*types)) 119 | else: 120 | raise NotImplementedError 121 | for t in all_types: 122 | a("{ind}{i}define{sub}{name}Functions%s<%s>(m);" % (n, ", ".join(t))) 123 | else: 124 | a("{ind}{i}define{sub}{name}Functions%s(m);" % (n,)) 125 | 126 | a("{ind}{cb}") 127 | a("") 128 | 129 | data = { 130 | "ind": indent, 131 | "i": i, 132 | "name": function_definition_name(header_name), 133 | "sub": camelize(module_name), 134 | "ob": "{", 135 | "cb": "}" 136 | } 137 | return "\n".join([line.format(**data) for line in s]) 138 | -------------------------------------------------------------------------------- /generators/definitions/method.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | from itertools import chain, product 4 | from typing import List 5 | 6 | from CppHeaderParser import CppMethod, CppVariable 7 | 8 | from generators.config import CUSTOM_OVERLOAD_TYPES, EXPLICIT_IMPORTED_TYPES, KEEP_ASIS_TYPES, \ 9 | EXTERNAL_INHERITANCE, TEMPLATED_METHOD_TYPES, SPECIFIC_TEMPLATED_METHOD_TYPES, GLOBAL_PCL_IMPORTS 10 | from generators.definitions.constructor import Constructor 11 | from generators.definitions.method_parameters import make_pybind_argument_list 12 | from generators.definitions.variable import Variable 13 | from generators.point_types_utils import PCL_POINT_TYPES 14 | from generators.utils import make_namespace_class, clean_doxygen 15 | 16 | 17 | class Method: 18 | def __init__(self, method: CppMethod, is_an_overload=False, use_c_overload=False): 19 | """ 20 | Generates definition for a method 21 | Example: 22 | cls.def("go", &Class::go); 23 | """ 24 | self.cppmethod = method 25 | self.name = method["name"] 26 | self.is_an_overload = is_an_overload 27 | self.use_c_overload = use_c_overload 28 | self.templated_types = OrderedDict() 29 | self.needs_lambda_call = False 30 | 31 | def make_disambiguation(self, prefix, template_types=None): 32 | type_ = self.list_parameter_types(prefix, template_types) 33 | 34 | template = ("<%s>" % ", ".join([t[1] for t in template_types])) if template_types else "" 35 | template_keyword = "template " if template_types else "" 36 | constant_method = ", py::const_" if self.cppmethod["const"] else "" 37 | return_type = self.cppmethod["rtnType"].replace("inline", "").strip() 38 | if self.use_c_overload: 39 | disamb = "({return_type} (*)({type_}))(&{cls}{name})" 40 | else: 41 | disamb = "py::overload_cast<{type_}> (&{cls}{template_keyword}{name}{template}{const})" 42 | disamb = disamb.format(type_=type_, 43 | cls=(prefix + "::") if prefix else "", 44 | return_type=return_type, 45 | name=self.cppmethod["name"], 46 | template=template, 47 | template_keyword=template_keyword, 48 | const=constant_method) 49 | return disamb 50 | 51 | def list_parameter_types(self, prefix, template_types): 52 | if len(self.cppmethod["parameters"]): 53 | types = [] 54 | for param in self.cppmethod["parameters"]: 55 | if param["name"] == "&": # fix for CppHeaderParser bug 56 | param["type"] += " &" 57 | param["reference"] = 1 58 | type_ = param["type"] if not param["unresolved"] else param["raw_type"] 59 | 60 | if "boost::function"): 81 | if type_.startswith("const"): 82 | type_ = "const typename " + type_.replace("const ", "", 1) 83 | else: 84 | type_ = "typename " + type_ 85 | if param["reference"] and "&" not in type_: 86 | type_ += " &" 87 | 88 | if param.get("array_size"): 89 | type_ += "[%s]" % param.get("array_size") 90 | 91 | types.append(type_) 92 | type_ = ", ".join(types) 93 | else: 94 | type_ = "" 95 | return type_ 96 | 97 | def clean_unresolved_type(self, param, type_, prefix): 98 | const = "const " if param["constant"] or "const" in param["type"] else "" 99 | type_ = type_.replace("typename ", "") 100 | if "const" in type_: # fix for CppHeaderParser parsing error 'std::vectorconst' (no space) 101 | const = "" 102 | ref = " &" if param["reference"] else "" 103 | parent_name = param["method"].get("parent", {}).get("name") 104 | custom = None 105 | if parent_name: 106 | custom = CUSTOM_OVERLOAD_TYPES.get((parent_name, type_)) 107 | type_no_template = type_[:type_.find("<")] if "<" in type_ else type_ 108 | template_string = self.cppmethod.get("parent", {}).get("template", "") 109 | if type_ == "pcl::PCLPointCloud2::" and param["name"].startswith("Const"): 110 | type_ = type_ + param["name"] # fix for CppHeaderParser bug 111 | elif type_.startswith("pcl::"): 112 | type_ = make_namespace_class("pcl", type_) 113 | elif any(type_.startswith(base) for base in KEEP_ASIS_TYPES): 114 | pass 115 | elif type_ in template_string: # templated argument 116 | pass 117 | elif type_no_template in EXPLICIT_IMPORTED_TYPES: 118 | pass 119 | elif type_no_template in GLOBAL_PCL_IMPORTS: 120 | pass 121 | elif custom: 122 | type_ = custom 123 | elif any(type_.startswith(t) for t in EXTERNAL_INHERITANCE): 124 | pass 125 | elif prefix == "pcl": 126 | type_ = make_namespace_class("pcl", type_) 127 | else: 128 | type_ = "%s::%s" % (prefix, type_) 129 | for global_import in GLOBAL_PCL_IMPORTS: 130 | if re.search(r"[^a-zA-Z:]%s" % global_import, type_): 131 | type_ = type_.replace(global_import, "pcl::" + global_import) 132 | type_ = const + type_ + ref 133 | if param.get("pointer"): 134 | type_ = type_.strip() + "*" 135 | type_ = type_.replace("constpcl::", "const pcl::") # parser error for this expression 136 | return type_ 137 | 138 | def static_value(self): 139 | return "_static" if self.cppmethod["static"] else "" 140 | 141 | def disambiguated_function_call(self, cls_var, disamb, args): 142 | doc = clean_doxygen(self.cppmethod.get("doxygen", "")) 143 | return '{cls_var}.def{static}("{name}", {disamb}{args}, R"({doc})")'.format(cls_var=cls_var, 144 | name=self.name, 145 | disamb=disamb, 146 | static=self.static_value(), 147 | args=args, 148 | doc=doc) 149 | 150 | def to_str(self, prefix, class_var_name): 151 | params = self.cppmethod["parameters"] 152 | doc = clean_doxygen(self.cppmethod.get("doxygen", "")) 153 | args = make_pybind_argument_list(params) 154 | if "operator" in self.cppmethod["name"]: 155 | message = "Operators not implemented (%s)" % (self.cppmethod["name"],) 156 | ret_val = "// " + message 157 | elif self.needs_lambda_call: 158 | message = "Non templated function disambiguation not implemented (%s)" % (self.cppmethod["name"],) 159 | ret_val = "// " + message 160 | # """ Example of how to implement for kdtreeFlann: 161 | # .def("nearest_k_search", [] 162 | # (Class &class_, const PointT &point, int k, 163 | # std::vector &k_indices, std::vector &k_sqr_distances) 164 | # {return class_.nearestKSearch(point, k, k_indices, k_sqr_distances);}, 165 | # "point"_a, 166 | # "k"_a, 167 | # "k_indices"_a, 168 | # "k_sqr_distances"_a 169 | # ) 170 | # """ 171 | elif any("**" in param["type"].replace(" ", "") for param in params): 172 | message = "Double pointer arguments are not supported by pybind11 (%s)" % (self.cppmethod["name"],) 173 | ret_val = "// " + message 174 | elif self.is_boost_function_callback(): 175 | s = '{cls_var}.def("{name}", []({cls} &v, {types} &cb) {ob} v.{name}(cb); {cb}, R"({doc})")' 176 | types = self.list_parameter_types(prefix, template_types=None) 177 | types = types.replace("boost::function", "std::function") 178 | data = {"name": self.name, 179 | "cls": prefix, 180 | "cls_var": class_var_name, 181 | "types": types, 182 | "ob": "{ob}", # will get formatted later 183 | "cb": "{cb}", 184 | "doc": doc, 185 | } 186 | ret_val = s.format(**data) 187 | elif self.templated_types: 188 | return_values = [] 189 | names = list(self.templated_types.keys()) 190 | types = list(self.templated_types.values()) 191 | for types_combination in product(*types): 192 | template_types = tuple(zip(names, types_combination)) 193 | disamb = self.make_disambiguation(prefix, template_types=template_types) 194 | return_values.append(self.disambiguated_function_call(class_var_name, disamb, args)) 195 | ret_val = return_values 196 | elif self.is_an_overload: 197 | disamb = self.make_disambiguation(prefix) 198 | ret_val = self.disambiguated_function_call(class_var_name, disamb, args) 199 | else: 200 | s = '{cls_var}.def{static}("{name}", &{cls}{name}{args}, R"({doc})")' 201 | data = {"name": self.name, 202 | "static": self.static_value(), 203 | "cls": (prefix + "::") if prefix else "", 204 | "cls_var": class_var_name, 205 | "args": args, 206 | "doc": doc, 207 | } 208 | ret_val = s.format(**data) 209 | return ret_val 210 | 211 | def is_boost_function_callback(self): 212 | callback = "Callback" in self.name 213 | if callback: 214 | single_argument = len(self.cppmethod["parameters"]) == 1 215 | if single_argument: 216 | if self.cppmethod["parameters"][0]["type"].startswith("boost::function"): 217 | return True 218 | return False 219 | 220 | def __repr__(self): 221 | return "" % (self.name,) 222 | 223 | 224 | def flag_templated_methods(methods: List[Method]): 225 | for method in methods: 226 | method_name = method.cppmethod["name"] 227 | if "operator" in method_name: 228 | continue 229 | template = method.cppmethod["template"] 230 | if template: 231 | class_name = method.cppmethod["parent"]["name"] 232 | pos = template.find("<") 233 | type_names = filter_template_types(template[pos + 1:-1], keep=["typename", "class"]) 234 | for type_name, types in template_types_generator(type_names, class_name, method_name): 235 | method.templated_types[type_name] = types 236 | 237 | 238 | def template_types_generator(type_names, header_or_class_name, method_name): 239 | method_key = (header_or_class_name, method_name, type_names) 240 | all_methods_key = (header_or_class_name, "", type_names) 241 | specific = SPECIFIC_TEMPLATED_METHOD_TYPES 242 | pcl_point_types = specific.get(method_key, specific.get(all_methods_key)) 243 | if not pcl_point_types: 244 | pcl_point_types = [TEMPLATED_METHOD_TYPES.get(type_name) for type_name in type_names] 245 | 246 | for type_name, pcl_types in zip(type_names, pcl_point_types): 247 | if not pcl_types: 248 | attrs = (type_name, method_name, header_or_class_name) 249 | message = "Templated method name not implemented (name=%s method=%s header=%s)" 250 | raise NotImplementedError(message % attrs) 251 | if isinstance(pcl_types, list): 252 | types = pcl_types 253 | elif pcl_types in PCL_POINT_TYPES: 254 | types = [t[1:-1] for t in PCL_POINT_TYPES[pcl_types]] # remove parentheses 255 | else: 256 | raise ValueError 257 | yield type_name, types 258 | 259 | 260 | def flag_overloaded_methods(methods: List[Method], needs_overloading: List[str] = None): 261 | templated_method_names = [m.cppmethod["name"] for m in methods if m.templated_types] 262 | # flag methods that need to be called with a lambda (same name and same parameters as a templated method) 263 | for method in methods: 264 | name_ = method.cppmethod["name"] 265 | method.is_an_overload = True 266 | if method.templated_types: 267 | pass 268 | elif name_ in templated_method_names: 269 | method.needs_lambda_call = True 270 | elif name_ in needs_overloading: 271 | pass 272 | else: 273 | method.is_an_overload = False 274 | 275 | 276 | def filter_static_and_non_static_methods(methods: List[Method]): 277 | static = set(m.cppmethod["name"] for m in methods if m.cppmethod["static"]) 278 | not_static = set(m.cppmethod["name"] for m in methods if not m.cppmethod["static"]) 279 | both = static & not_static 280 | filtered = [] 281 | for method in methods: 282 | name_ = method.cppmethod["name"] 283 | is_static = method.cppmethod["static"] 284 | if name_ in both and is_static: 285 | print("Warning: Overloading a method with both static " 286 | "and instance methods is not supported by pybind11 (%s)" % name_) 287 | continue 288 | filtered.append(method) 289 | return filtered 290 | 291 | 292 | def split_methods_by_type(methods: List[CppMethod], 293 | class_variables: List[CppVariable], 294 | needs_overloading: List[str]): 295 | constructors_methods = [m for m in methods if m["constructor"] and 296 | not is_copy_constructor(m) and m["name"] == m["parent"]["name"]] 297 | copy_const = [m for m in methods if m["constructor"] and is_copy_constructor(m)] 298 | destructors = [m for m in methods if m["destructor"]] 299 | setters = [m for m in methods if m["name"].startswith("set")] 300 | getters = [m for m in methods if m["name"].startswith("get")] 301 | 302 | identified_methods = chain(constructors_methods, copy_const, destructors, setters, getters) 303 | identified_methods_line_numbers = set([m["line_number"] for m in identified_methods]) 304 | other_methods = [m for m in methods if m["line_number"] not in identified_methods_line_numbers] 305 | 306 | others = [Method(m) for m in chain(other_methods, setters, getters)] 307 | 308 | flag_templated_methods(others) 309 | flag_overloaded_methods(others, needs_overloading) 310 | others = filter_static_and_non_static_methods(others) 311 | 312 | variables = list(map(Variable, class_variables)) 313 | constructors = list(map(Constructor, constructors_methods)) 314 | 315 | return constructors, variables, others 316 | 317 | 318 | def filter_template_types(template_string, keep=None, keep_all=False): 319 | if keep is None: 320 | keep = ["typename", "class", "unsigned"] 321 | if not template_string: 322 | return tuple() 323 | types = template_string.split(", ") 324 | types = [s.strip().split(" ")[1] for s in types if keep_all or any(k in s for k in keep)] 325 | return tuple(types) 326 | 327 | 328 | def is_copy_constructor(method): 329 | name = method["name"] 330 | params = method["parameters"] 331 | if params and name in params[0]["type"]: 332 | try: 333 | in_doxygen = "Copy constructor" in method["doxygen"].split("\n")[0] 334 | except KeyError: 335 | in_doxygen = False 336 | 337 | # some doxygen strings are wrong, we need to use the number of parameters also 338 | if in_doxygen and len(params) >= 2: 339 | return False 340 | if len(params) == 1 or in_doxygen: 341 | return True 342 | return False 343 | -------------------------------------------------------------------------------- /generators/definitions/method_parameters.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Dict, Set 3 | 4 | from generators.config import GLOBAL_PCL_IMPORTS 5 | 6 | all_default_types_by_namespace = defaultdict(set) 7 | all_return_values = defaultdict(set) 8 | 9 | 10 | def parameter_default_value(param): 11 | val = param.get("defaultValue", "") 12 | if val: 13 | namespace = param["method"]["namespace"] 14 | if any(val.startswith(g) for g in GLOBAL_PCL_IMPORTS): 15 | val = "pcl::" + val 16 | all_default_types_by_namespace[namespace].add(param["raw_type"]) 17 | # fix for exponent and float values parsed with added spaces 18 | val = "=" + val.replace(" ", "") 19 | # fix for std::numeric_limits::max() 20 | val = val.replace("unsignedshort", "unsigned short") 21 | return val 22 | 23 | 24 | def make_pybind_argument_list(cpp_parameters): 25 | if len(cpp_parameters) == 0: 26 | return "" 27 | names = ", ".join(['"%s"_a%s' % (p["name"], parameter_default_value(p)) for p in cpp_parameters]) 28 | return ", " + names 29 | 30 | 31 | def get_default_parameters_types() -> Dict[str, Set[str]]: 32 | default_parameters_types = defaultdict(set) 33 | for param in all_default_types_by_namespace: 34 | namespace = param["method"]["namespace"] 35 | default_parameters_types[namespace].add(param["raw_type"]) 36 | 37 | return default_parameters_types 38 | -------------------------------------------------------------------------------- /generators/definitions/module_variable.py: -------------------------------------------------------------------------------- 1 | from CppHeaderParser import CppVariable 2 | 3 | from generators.config import BASE_SUB_MODULE_NAME 4 | 5 | 6 | def define_variable(var: CppVariable): 7 | variable_def = '{sub_name}.attr("%s") = %s;' % (var["name"], var["defaultValue"]) 8 | return variable_def.format(sub_name=BASE_SUB_MODULE_NAME) 9 | -------------------------------------------------------------------------------- /generators/definitions/submodule_loader.py: -------------------------------------------------------------------------------- 1 | from inflection import camelize 2 | 3 | from generators.config import common_includes, INDENT, cpp_header 4 | from generators.utils import function_definition_name 5 | 6 | 7 | def generate_loader(module, headers): 8 | lines = [common_includes] 9 | a = lines.append 10 | a(cpp_header) 11 | 12 | for header in headers: 13 | inc_name = '%s/%s' % (module, header) if module != "base" else header 14 | include_expression = '#include "%spp"' % (inc_name,) 15 | a(include_expression) 16 | a("\n") 17 | a("void define%sClasses(py::module &m) {" % camelize(module)) 18 | 19 | # add submodule 20 | submodule_object_name = ("m_%s" % module) if module != "base" else "m" 21 | module_python = module if module != "2d" else "module_2d" 22 | if submodule_object_name != "m": 23 | module_creation = '{i}py::module {obj_name} = m.def_submodule("{module_python}", "Submodule {sub}");' 24 | a(module_creation.format(obj_name=submodule_object_name, module_python=module_python, sub=module, i=INDENT)) 25 | 26 | # add function calls 27 | sub = camelize(module) if module != "base" else "" 28 | for header in headers: 29 | name = function_definition_name(header_name=header) 30 | function_call = "{i}define{sub}{name}Classes({sub_name});".format(i=INDENT, 31 | name=name, 32 | sub_name=submodule_object_name, 33 | sub=sub) 34 | a(function_call) 35 | a("}") 36 | 37 | return "\n".join(lines) 38 | -------------------------------------------------------------------------------- /generators/definitions/templated_class.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List 3 | 4 | from CppHeaderParser import CppClass 5 | from inflection import camelize 6 | 7 | from generators.config import INDENT, DONT_HOLD_WITH_BOOST_SHARED_PTR, EXTRA_FUNCTIONS, GLOBAL_PCL_IMPORTS 8 | from generators.definitions.constructor import Constructor 9 | from generators.definitions.enum import Enum 10 | from generators.definitions.method import Method 11 | from generators.definitions.method import filter_template_types 12 | from generators.definitions.variable import Variable 13 | from generators.point_types_utils import clean_inheritance 14 | from generators.utils import clean_doxygen 15 | 16 | 17 | class ClassDefinition: 18 | CLS_VAR = "cls" 19 | 20 | def __init__(self, 21 | class_: CppClass, 22 | constructors: List[Constructor], 23 | variables: List[Variable], 24 | other_methods: List[Method], 25 | sub_module: str): 26 | """ 27 | Generates a templated function to define a pybind11 py::class_ with its methods and properties 28 | 29 | Example: 30 | template 31 | void defineSuperFunction(py::module &m, std::string const & suffix) { 32 | using Class = SuperFunction; 33 | py::class_, boost::shared_ptr> cls(m, suffix.c_str()); 34 | cls.def(py::init<>()); 35 | cls.def_property("stuff", &Class::getStuff, &Class::setStuff); 36 | cls.def("go", &Class::go); 37 | } 38 | """ 39 | self.sub_module = sub_module 40 | self.class_ = class_ 41 | self.inherits = None 42 | if class_["inherits"]: 43 | inherits_list = clean_inheritance(self.class_, 44 | replace_with_templated_typename=False, 45 | formatted=True) 46 | self.inherits = ", ".join(inherits_list) 47 | self.class_name = class_["name"] 48 | self.constructors = constructors 49 | self.variables = variables 50 | named_enums = [e for e in class_["enums"]["public"] if e.get("name")] 51 | self.enums = list(map(Enum, named_enums)) 52 | self.other_methods = other_methods 53 | self.template = self.parse_template() 54 | self.is_templated = False 55 | 56 | def parse_template(self): 57 | template = self.class_.get("template") 58 | if template: 59 | for global_import in GLOBAL_PCL_IMPORTS: 60 | # add pcl:: to explicit global pcl imports 61 | with_pcl = "pcl::%s" % global_import 62 | if re.search(r"[ =]%s\W" % global_import, template): 63 | template = template.replace(global_import, with_pcl) 64 | return template 65 | 66 | def to_str(self): 67 | if self.is_templated: 68 | s = 'py::class_ {cls_var}(m, suffix.c_str(), R"({doc})")' 69 | else: 70 | s = 'py::class_ {cls_var}(m, "{name}", R"({doc})")' 71 | ptr = ", boost::shared_ptr" 72 | if self.class_["name"] in DONT_HOLD_WITH_BOOST_SHARED_PTR: 73 | ptr = "" 74 | data = { 75 | "name": self.class_name, 76 | "cls_var": self.CLS_VAR, 77 | # "original_name": self.class_name, 78 | "inherits": (", %s" % self.inherits) if self.inherits else "", 79 | "ptr": ptr, 80 | "doc": clean_doxygen(self.class_.get("doxygen", "")), 81 | } 82 | return s.format(**data) 83 | 84 | def get_namespace(self): 85 | namespace = self.class_["namespace"] 86 | if namespace: 87 | namespace += "::" 88 | return namespace 89 | 90 | def typedefs(self): 91 | return self.class_["typedefs"]["public"] 92 | 93 | def to_class_function_definition(self, ind="") -> str: 94 | """ 95 | template 96 | void define...(py::module &m, std::string const & suffix) { ... } 97 | """ 98 | i = INDENT 99 | s = [] 100 | if self.template: 101 | template_info = re.findall(r"<(.+)>", str(self.template.replace("\n", ""))) 102 | 103 | types = ", ".join(filter_template_types(template_info[0])) 104 | # -> 105 | types = re.sub(r"=[:\w]+", "", types) 106 | if types: 107 | self.is_templated = True 108 | s = [] 109 | a = s.append 110 | a("{ind}{template}") 111 | a("{ind}void define{sub}{name}(py::module &m, std::string const & suffix) {ob}") 112 | templated_name = "{name}<%s>;" % types 113 | a("{ind}{i}using Class = typename {namespace}%s" % templated_name) 114 | for typedef in self.typedefs(): 115 | a("{ind}{i}using {typedef} = typename Class::{typedef};".format(ind=ind, i=i, typedef=typedef)) 116 | a(self.py_class_definition(ind=ind + i) + "\n{ind}{i}{i}") 117 | a("{cb}") 118 | if not self.is_templated: 119 | a = s.append 120 | a("{ind}void define{sub}{name}(py::module &m) {ob}") 121 | a("{ind}{i}using Class = {namespace}{name}{empty_template};") 122 | for typedef in self.typedefs(): 123 | a("{ind}{i}using %s = Class::%s;" % (typedef, typedef)) 124 | a(self.py_class_definition(ind=ind + i)) 125 | a("{cb}") 126 | data = { 127 | "name": self.class_name, 128 | "ind": ind, 129 | "i": i, 130 | "namespace": self.get_namespace(), 131 | "sub": camelize(self.sub_module), 132 | "template": self.template, 133 | "empty_template": "<>" if not self.is_templated and self.template else "", 134 | "ob": "{", 135 | "cb": "}" 136 | } 137 | return "\n".join([line.format(**data) for line in s]) 138 | 139 | def py_class_definition(self, ind=""): 140 | i = INDENT 141 | class_enums_names = [v["name"] for e in self.enums for v in e.cppenum["values"]] 142 | s = ["{ind}%s;" % self.to_str()] 143 | s += ["{ind}%s;" % enum.to_str("Class", class_var_name=self.CLS_VAR) for enum in self.enums] 144 | s += ["{ind}%s;" % c.to_str(class_var_name=self.CLS_VAR, class_enums_names=class_enums_names) 145 | for c in self.constructors] 146 | s += ["{ind}%s;" % v.to_str("Class", class_var_name=self.CLS_VAR) for v in self.variables] 147 | templated_methods = [m for m in self.other_methods if m.templated_types] 148 | s += ["{ind}%s;" % m.to_str("Class", class_var_name=self.CLS_VAR) for m in self.other_methods 149 | if not m in templated_methods] 150 | s += ["{ind}%s;" % m for method in templated_methods for m in 151 | method.to_str("Class", class_var_name=self.CLS_VAR)] 152 | if self.class_name in EXTRA_FUNCTIONS: 153 | s.append(EXTRA_FUNCTIONS[self.class_name]) 154 | data = { 155 | "ind": ind, 156 | "i": i, 157 | "ob": "{ob}", # will get formatted later 158 | "cb": "{cb}", 159 | } 160 | return "\n".join([line.format(**data) for line in s]) 161 | -------------------------------------------------------------------------------- /generators/definitions/variable.py: -------------------------------------------------------------------------------- 1 | from CppHeaderParser import CppVariable 2 | 3 | 4 | class Variable: 5 | def __init__(self, variable: CppVariable): 6 | """ 7 | Generates definition for a variable 8 | Example: 9 | cls.def("indices", &Class::indices); 10 | """ 11 | self.cppvariable = variable 12 | self.name = variable["name"] 13 | self.is_an_override = False 14 | 15 | def to_str(self, class_name, class_var_name): 16 | if self.cppvariable.get("array"): 17 | s = ('{cls_var}.def("{name}", [](py::object &obj) {ob} ' 18 | '{cls} &a = obj.cast<{cls}&>(); ' 19 | 'return py::array_t<{type_}>({ob}{size}{cb}, {ob}sizeof({type_}) * {size}{cb}, a.{name}, obj); ' 20 | '{cb})') 21 | else: 22 | s = '{cls_var}.def_read{only}{static}("{name}", &{cls}::{name})' 23 | readonly = self.cppvariable["constant"] 24 | data = {"name": self.name, 25 | "cls": class_name, 26 | "cls_var": class_var_name, 27 | "size": self.cppvariable.get("array_size", ""), 28 | "type_": self.cppvariable["type"], 29 | "only": "only" if readonly else "write", 30 | "static": "_static" if self.cppvariable["static"] else "", 31 | "ob": "{ob}", # will be formatted later 32 | "cb": "{cb}", 33 | } 34 | ret_val = s.format(**data) 35 | return ret_val 36 | 37 | def __repr__(self): 38 | return "" % (self.name,) 39 | -------------------------------------------------------------------------------- /generators/dependency_tree.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, OrderedDict, deque 2 | from typing import List, Dict, Generator 3 | 4 | from CppHeaderParser import CppClass 5 | 6 | from generators.config import EXTERNAL_INHERITANCE, IGNORE_INHERITED_INSTANTIATIONS, INHERITED_TEMPLATED_TYPES_FILTER 7 | from generators.point_types_utils import clean_inheritance, get_class_namespace 8 | from generators.utils import make_namespace_class 9 | 10 | 11 | class DependencyTree: 12 | def __init__(self, classes: List[CppClass]): 13 | """ 14 | 15 | :param classes: 16 | """ 17 | self.namespace_by_class_name = defaultdict(list) 18 | for c in classes: 19 | self.namespace_by_class_name[c["name"]].append(c["namespace"]) 20 | 21 | self.tree = {} 22 | for c in classes: 23 | class_name = make_namespace_class(c["namespace"], c["name"]) 24 | inheritance = dict(clean_inheritance(c, self.namespace_by_class_name)) 25 | self.tree[class_name] = inheritance 26 | 27 | self.n_template_point_types = {k: len(v) for inheritance in self.tree.values() for k, v in inheritance.items()} 28 | 29 | def reversed_breadth_first_iterator(self) -> List[str]: 30 | """Like breadth_first_iterator, but starts from the base classes.""" 31 | classes_breadth_first = list(self.breadth_first_iterator()) 32 | seen = set() 33 | unique = [] 34 | for c in classes_breadth_first: 35 | if c not in seen: 36 | unique.append(c) 37 | seen.add(c) 38 | return list(reversed(unique)) 39 | 40 | def breadth_first_iterator(self, start_class=None) -> Generator[str, None, None]: 41 | """ 42 | Yields every class that aren't inherited elsewhere, then all their first inherited classes, etc. 43 | :param start_class: class name to start from. 44 | If None, start from all classes that aren't inherited by any other class. 45 | """ 46 | all_inheritances = {make_namespace_class(get_class_namespace(class_), i) 47 | for class_, inheritance in self.tree.items() 48 | for i in inheritance} 49 | if start_class is None: 50 | queue = deque(elem for elem in self.tree if elem not in all_inheritances) 51 | else: 52 | queue = deque([start_class]) 53 | while queue: 54 | class_ = queue.popleft() 55 | if class_ != start_class: 56 | yield class_ 57 | for inherits in self.tree.get(class_, []): 58 | queue.append(inherits) 59 | 60 | def leaf_iterator(self): 61 | stack = list(sorted(self.tree.keys())) # sort to output same result everytime 62 | seen = set() 63 | while stack: 64 | for class_name in stack: 65 | inheritance = set(self.tree.get(class_name).keys()) 66 | inheritance = set((i[:i.find("<")] if "<" in i else i for i in inheritance)) 67 | inheritance_current_namespace = set([make_namespace_class(class_name[:class_name.rfind("::")], i) 68 | for i in inheritance]) 69 | is_base_class = not inheritance 70 | inheritance_is_seen = not inheritance - seen or not (inheritance_current_namespace - seen) 71 | all_external = all(any(i.startswith(e) for e in EXTERNAL_INHERITANCE) for i in inheritance) 72 | if any([is_base_class, inheritance_is_seen, all_external]): 73 | yield class_name 74 | seen.add(class_name) 75 | stack.remove(class_name) 76 | break 77 | else: 78 | raise ValueError("Couldn't find base classes for: %s" % stack) 79 | 80 | def split_namespace_class(self, class_): 81 | return class_[:class_.rfind("::")], class_[class_.rfind("::") + 2:] 82 | 83 | def get_point_types_with_dependencies(self, classes_point_types: Dict) -> OrderedDict: 84 | types = OrderedDict() 85 | 86 | def add_to_dict(key, data): 87 | if key not in types: 88 | types[key] = data[:] 89 | else: 90 | types[key] += data 91 | 92 | for class_ in self.reversed_breadth_first_iterator(): 93 | namespace, class_name = self.split_namespace_class(class_) 94 | point_types = classes_point_types.get(class_name) 95 | if point_types: 96 | n_point_types = len(point_types[0]) 97 | add_to_dict(class_name, point_types) 98 | for base_class_ in self.breadth_first_iterator(class_): 99 | _, base_class_name = self.split_namespace_class(base_class_) 100 | if base_class_name in IGNORE_INHERITED_INSTANTIATIONS: 101 | continue 102 | n_point_types_base = self.n_template_point_types[base_class_] 103 | if n_point_types_base != n_point_types: 104 | template_filter = INHERITED_TEMPLATED_TYPES_FILTER 105 | types_filter = template_filter.get(base_class_, template_filter.get(base_class_name)) 106 | if types_filter: 107 | for types_ in point_types: 108 | filtered = tuple(t for i, t in enumerate(types_) if i in types_filter) 109 | add_to_dict(base_class_name, [filtered]) 110 | add_to_dict(base_class_name, [types_[:n_point_types_base] for types_ in point_types]) 111 | else: 112 | add_to_dict(base_class_name, point_types) 113 | for k, v in types.items(): 114 | types[k] = list(sorted(set(v))) 115 | return types 116 | -------------------------------------------------------------------------------- /generators/generate_point_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | PCL point types are generated using boost macros. So it's easier to manually bind them in pclpy. 3 | """ 4 | 5 | import re 6 | from collections import OrderedDict 7 | from os.path import join 8 | 9 | from generators import config 10 | from generators.config import PCL_BASE, INDENT 11 | 12 | 13 | def get_point_types(header_path): 14 | point_types = OrderedDict() 15 | pt_type = None 16 | fields = [] 17 | for line in open(header_path): 18 | if line.startswith("POINT_CLOUD_REGISTER_POINT_STRUCT"): 19 | pt_type = re.search("_POINT_STRUCT.*\((.+),", line).group(1).replace("_", "") 20 | elif line.startswith(")"): 21 | point_types[pt_type] = fields 22 | fields = [] 23 | pt_type = None 24 | elif pt_type: 25 | # re_info = re.search("\((.+?)(\[\d+\])?, ?(.+), ?.+\)", line) 26 | re_info = re.search("\((.+), ?(.+), ?.+\)", line) 27 | type_, field_name = re_info.group(1), re_info.group(2) 28 | fields.append((type_, field_name)) 29 | return point_types 30 | 31 | 32 | def generate_class_point_type(module_name, point_name, fields, indent=INDENT): 33 | lines = [] 34 | a = lines.append 35 | a('{i}py::class_<{point_name}, boost::shared_ptr<{point_name}>> ({module_name}, "{point_py_name}")') 36 | types_string = ", ".join(type_ for type_, _ in fields) 37 | args_string = ", ".join('"%s"_a' % name for _, name in fields) 38 | # if "[" in types_string: 39 | a('{i}{i}.def(py::init<>())') 40 | # else: 41 | # a('{i}{i}.def(py::init<{types_string}>(), {args_string})') 42 | for type_, field_name in fields: 43 | write = "only" if "[" in type_ else "write" 44 | a('{i}{i}.def_read%s("%s", &{point_name}::%s)' % (write, field_name, field_name)) 45 | data = { 46 | "i": indent, 47 | "types_string": types_string, 48 | "args_string": args_string, 49 | "point_name": point_name, 50 | "module_name": module_name, 51 | "point_py_name": point_name.replace("pcl::", ""), 52 | } 53 | lines[-1] += ";" 54 | return "\n".join(lines).format(**data) 55 | 56 | 57 | def generate_point_types(): 58 | header_path = join(PCL_BASE, "point_types.h") 59 | point_types = get_point_types(header_path) 60 | module_name = "m_pts" 61 | lines = [config.common_includes] 62 | a = lines.append 63 | a("void definePointTypes(py::module &m) {") 64 | module_def = '%spy::module %s = m.def_submodule("point_types", "Submodule for point types");' 65 | a(module_def % (INDENT, module_name)) 66 | a("") 67 | for point_name, fields in point_types.items(): 68 | a(generate_class_point_type(module_name, point_name, fields)) 69 | a("") 70 | 71 | a("}") 72 | return "\n".join(lines) 73 | 74 | 75 | def write_to_file(text): 76 | path = join(config.PATH_SRC, "point_types.hpp") 77 | open(path, "w").write(text) 78 | 79 | 80 | def main(): 81 | text = generate_point_types() 82 | write_to_file(text) 83 | 84 | 85 | if __name__ == '__main__': 86 | main() 87 | -------------------------------------------------------------------------------- /generators/generate_subsample.py: -------------------------------------------------------------------------------- 1 | from generators.generate_pybind11_bindings import generate, write_stuff_if_needed, get_headers 2 | 3 | 4 | def ensure_required(headers): 5 | required = [ 6 | ("", "point_cloud.h", "point_cloud.h"), 7 | ] 8 | for r in required: 9 | if r not in headers: 10 | headers.append(r) 11 | 12 | 13 | def main(): 14 | # headers = [ 15 | # ("filters", "filter.h", ""), 16 | # ("filters", "filter_indices.h", ""), 17 | # ("", "pcl_base.h", ""), 18 | # ] 19 | modules = ["registration"] 20 | headers = get_headers(modules) 21 | 22 | ensure_required(headers) 23 | generated_headers = generate(headers) 24 | write_stuff_if_needed(generated_headers, delete_others=True) 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /generators/generate_yaml_point_types.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from collections import defaultdict 4 | from os.path import join 5 | from typing import List, Tuple 6 | 7 | import yaml 8 | 9 | from generators.config import MODULES_TO_BUILD 10 | from generators.point_types_utils import PCL_POINT_TYPES, PCL_ALL_POINT_TYPES 11 | from generators.utils import parentheses_are_balanced 12 | 13 | PCL_REPO_PATH = os.environ["PCL_REPO_PATH"] 14 | POINT_GROUPS = PCL_POINT_TYPES 15 | if os.getenv("POINT_GROUPS") == 'all': 16 | POINT_GROUPS = PCL_ALL_POINT_TYPES 17 | 18 | re_instantiate = re.compile(r"^PCL_INSTANTIATE\((.+?),(.+)\);?$") 19 | re_product = re.compile(r"^PCL_INSTANTIATE_PRODUCT\((.+?),(.+)\);?$") 20 | 21 | CLASS_EXCEPTIONS_ADD_NORMAL_T = ["RegionGrowing", 22 | "RegionGrowingRGB", ] 23 | 24 | src_files = [] 25 | for m in MODULES_TO_BUILD: 26 | path = join(PCL_REPO_PATH, m, "src") 27 | files = [] 28 | if os.path.isdir(path): 29 | files = os.listdir(path) 30 | src_files.append(files) 31 | 32 | 33 | # http://stackoverflow.com/questions/4284991/parsing-nested-parentheses-in-python-grab-content-by-level 34 | def parenthetic_contents(string): 35 | """Generate parenthesized contents in string as pairs (level, contents).""" 36 | stack = [] 37 | for i, c in enumerate(string): 38 | if c == '(': 39 | stack.append(i) 40 | elif c == ')' and stack: 41 | start = stack.pop() 42 | yield (len(stack), string[start + 1: i]) 43 | 44 | 45 | def parse_point_list(point_list: str) -> List[str]: 46 | for group, types in POINT_GROUPS.items(): 47 | if group in point_list: 48 | point_list = point_list.replace(group, ",".join(types)) 49 | point_list = point_list.replace(")(", ",").replace(")", "").replace("(", "").replace("pcl::", "") 50 | return point_list.split(",") 51 | 52 | 53 | def parse_instantiation_line(line: str) -> Tuple[str, List[List[str]]]: 54 | line = line.replace(" ", "") 55 | single = re_instantiate.match(line) 56 | if single: 57 | class_name = single.group(1) 58 | point_list = single.group(2) 59 | types = parse_point_list(point_list) 60 | if class_name in CLASS_EXCEPTIONS_ADD_NORMAL_T: 61 | types = [types, ["Normal"]] 62 | else: 63 | product = re_product.match(line) 64 | class_name = product.group(1) 65 | point_list = product.group(2) 66 | types = [parse_point_list(info) for level, info in parenthetic_contents(point_list) if level == 0] 67 | return class_name, types 68 | 69 | 70 | def get_instantiations(lines): 71 | inside_core_point_types_def = False 72 | skip_if = False 73 | complete_line = "" 74 | for line in lines: 75 | complete_line += line.strip() 76 | if complete_line.startswith("PCL_INSTANTIATE") and not skip_if: 77 | if parentheses_are_balanced(complete_line, "()"): 78 | yield parse_instantiation_line(complete_line) 79 | else: 80 | continue 81 | elif complete_line.startswith("#ifdef PCL_ONLY_CORE_POINT_TYPES"): 82 | inside_core_point_types_def = True 83 | elif inside_core_point_types_def and complete_line.startswith("#else"): 84 | inside_core_point_types_def = False 85 | skip_if = True 86 | elif skip_if and complete_line.startswith("#endif"): 87 | skip_if = False 88 | complete_line = "" 89 | 90 | 91 | def main(): 92 | all_classes = defaultdict(list) 93 | for files_list, module in zip(src_files, MODULES_TO_BUILD): 94 | cpp_files = [f for f in files_list if f.endswith("cpp")] 95 | for cpp in cpp_files: 96 | full = join(PCL_REPO_PATH, module, "src", cpp) 97 | for class_name, types in get_instantiations(open(full).readlines()): 98 | all_classes[class_name].append(types) 99 | yaml.dump(dict(all_classes), open("point_types_generated.yml", "w"), indent=2) 100 | 101 | 102 | if __name__ == '__main__': 103 | main() 104 | -------------------------------------------------------------------------------- /generators/instantiations.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | from typing import List, Dict 4 | 5 | from CppHeaderParser import CppClass, CppVariable 6 | from inflection import camelize 7 | 8 | from generators.config import INDENT, KEEP_ASIS_TYPES, BASE_SUB_MODULE_NAME 9 | from generators.definitions.enum import Enum 10 | from generators.definitions.method import filter_template_types 11 | from generators.definitions.module_variable import define_variable 12 | from generators.utils import function_definition_name 13 | 14 | 15 | class Instantiations: 16 | def __init__(self, 17 | sorted_classes: List[CppClass], 18 | module: str, 19 | header_name: str, 20 | classes_point_types: OrderedDict, 21 | variables: List[CppVariable], 22 | enums: List[CppVariable], 23 | ): 24 | """ 25 | Generate templated function calls that instantiate pybind11 classes 26 | Example: 27 | void defineSuperFunctionClasses(py::module &sub_module) { 28 | defineSuperFunction(sub_module, "PointXYZ"); 29 | defineSuperFunction(sub_module, "PointXYZI"); 30 | defineSuperFunction(sub_module, "PointXYZRGBA"); 31 | defineSuperFunction(sub_module, "PointXYZRGB"); 32 | } 33 | """ 34 | self.sorted_classes = sorted_classes 35 | self.module = module 36 | self.header_name = header_name 37 | self.classes_point_types = classes_point_types 38 | self.variables = variables 39 | self.enums = enums 40 | 41 | def repr_sub_module(self, class_name: str): 42 | """ 43 | Create a submodule for each templated class 44 | """ 45 | s = 'py::module {sub} = {base}.def_submodule("{name}", "Submodule {name}")' 46 | data = { 47 | "sub": self.sub_module_name(class_name), 48 | "base": BASE_SUB_MODULE_NAME, 49 | "name": class_name 50 | } 51 | return s.format(**data) 52 | 53 | def generate_instantiation_function(self, global_indent="", has_functions=False): 54 | """ 55 | void define...Classes(py::module &m) { ... } 56 | """ 57 | s = [] 58 | a = s.append 59 | i = INDENT 60 | a("{ind}void define{sub}{name}Classes(py::module &{base}) {ob}") 61 | for var in self.variables: 62 | a("{ind}{i}%s" % define_variable(var)) 63 | for enum in self.enums: 64 | enum = Enum(enum) 65 | a("{ind}{i}%s;" % enum.to_str(prefix=enum.cppenum["namespace"], class_var_name=BASE_SUB_MODULE_NAME)) 66 | for line in self.generate_templated_class_calls(): 67 | a("{ind}{i}%s;" % line) 68 | if has_functions: 69 | a("{ind}{i}define{sub}{name}Functions({base});") 70 | a("{ind}{cb}") 71 | 72 | data = { 73 | "ind": global_indent, 74 | "i": i, 75 | "base": BASE_SUB_MODULE_NAME, 76 | "name": function_definition_name(self.header_name), 77 | "sub": camelize(self.module), 78 | "ob": "{", 79 | "cb": "}" 80 | } 81 | return "\n".join([line.format(**data) for line in s]) 82 | 83 | def sub_module_name(self, class_name): 84 | return "%s_%s" % (BASE_SUB_MODULE_NAME, class_name) 85 | 86 | def generate_templated_class_calls(self): 87 | s = [] 88 | for c in self.sorted_classes: 89 | template_types = [] 90 | if c.get("template"): 91 | template_info = re.findall(r"<(.+)>", str(c["template"].replace("\n", ""))) 92 | if template_info: 93 | template_types = filter_template_types(template_info[0]) 94 | 95 | if template_types: 96 | sub_module_name = self.sub_module_name(c["name"]) 97 | types_list = self.classes_point_types.get(c["name"], []) 98 | if types_list: 99 | s.append(self.repr_sub_module(c["name"])) 100 | for types in types_list: 101 | type_names = "_".join(map(format_class_name, types)) 102 | define = 'define{sub}{name}<{types}>({sub_module_name}, "{types_names}")' 103 | add_pcl = lambda t: ("pcl::" + t) if t not in KEEP_ASIS_TYPES else t 104 | types_str = ", ".join([add_pcl(t) for t in types]) 105 | define = define.format(sub=camelize(self.module), 106 | name=c["name"], 107 | sub_module_name=sub_module_name, 108 | types=types_str, 109 | types_names=type_names) 110 | s.append(define) 111 | else: 112 | define = 'define{sub}{name}({sub_name})' 113 | define = define.format(sub=camelize(self.module), name=c["name"], sub_name=BASE_SUB_MODULE_NAME) 114 | s.append(define) 115 | return s 116 | 117 | 118 | def format_class_name(value): 119 | """ 120 | Example: 121 | octree::OctreePointCloudVoxelCentroidContainer -> OctreePointCloudVoxelCentroidContainer_PointXYZ 122 | """ 123 | if "<" in value: 124 | before = format_class_name(value[:value.find("<")]) 125 | inside = format_class_name(value[value.find("<") + 1:value.rfind(">")]) 126 | after = format_class_name(value[value.rfind(">")+1:]) 127 | return "_".join([v for v in (before, inside, after) if v]) 128 | elif "::" in value: 129 | return value[value.rfind("::") + 2:] 130 | else: 131 | return value 132 | -------------------------------------------------------------------------------- /generators/point_types_extra.yml: -------------------------------------------------------------------------------- 1 | PointCloudColorHandler: 2 | - [PointSurfel, PointXYZ, PointXYZL, PointXYZI, PointXYZRGB, 3 | PointXYZRGBA, PointNormal, PointXYZRGBNormal, PointXYZRGBL, 4 | PointWithRange] 5 | PointCloudColorHandlerCustom: 6 | - [PointSurfel, PointXYZ, PointXYZL, PointXYZI, PointXYZRGB, 7 | PointXYZRGBA, PointNormal, PointXYZRGBNormal, PointXYZRGBL, 8 | PointWithRange] 9 | PointCloudColorHandlerGenericField: 10 | - [PointSurfel, PointXYZ, PointXYZL, PointXYZI, PointXYZRGB, 11 | PointXYZRGBA, PointNormal, PointXYZRGBNormal, PointXYZRGBL, 12 | PointWithRange] 13 | PointCloudColorHandlerHSVField: 14 | - [PointXYZHSV] 15 | PointCloudColorHandlerLabelField: 16 | - [PointXYZL, PointXYZRGBL, PointXYZLNormal] 17 | PointCloudColorHandlerRGBAField: 18 | - [PointXYZRGBA] 19 | PointCloudColorHandlerRGBField: 20 | - [PointXYZRGB, PointXYZRGBNormal, PointXYZRGBL] 21 | PointCloudColorHandlerRandom: 22 | - [PointSurfel, PointXYZ, PointXYZL, PointXYZI, PointXYZRGB, 23 | PointXYZRGBA, PointNormal, PointXYZRGBNormal, PointXYZRGBL, 24 | PointWithRange] 25 | FeatureWithLocalReferenceFrames: 26 | - - [PointXYZ, PointXYZI, PointXYZRGBA, PointXYZRGB] 27 | - [ReferenceFrame] 28 | PointCloud: 29 | - [PointXYZ, PointXYZI, PointXYZL, Label, PointXYZRGBA, PointXYZRGB, PointXYZRGBL, 30 | PointXYZHSV, PointXY, InterestPoint, Axis, Normal, PointNormal, PointXYZRGBNormal, 31 | PointXYZINormal, PointXYZLNormal, PointWithRange, PointWithViewpoint, MomentInvariants, 32 | PrincipalRadiiRSD, Boundary, PrincipalCurvatures, PFHSignature125, PFHRGBSignature250, 33 | PPFSignature, CPPFSignature, PPFRGBSignature, NormalBasedSignature12, FPFHSignature33, 34 | VFHSignature308, GRSDSignature21, GASDSignature512, GASDSignature984, GASDSignature7992, 35 | ESFSignature640, BRISKSignature512, Narf36, IntensityGradient, 36 | PointWithScale, PointSurfel, ShapeContext1980, UniqueShapeContext1960, SHOT352, 37 | SHOT1344, PointUV, ReferenceFrame, PointDEM] 38 | OctreePointCloudVoxelCentroid: 39 | - [PointXYZ, PointXYZI, PointXYZL, PointXYZRGBA, PointXYZRGB] 40 | OctreePointCloudVoxelCentroidContainer: 41 | - [PointXYZ, PointXYZI, PointXYZL, PointXYZRGBA, PointXYZRGB] 42 | OctreePointCloudSinglePoint: 43 | - [PointXYZ, PointXYZI, PointXYZL, PointXYZRGBA, PointXYZRGB] 44 | OctreePointCloud: 45 | - - [PointXYZ, PointXYZI, PointXYZL, PointXYZRGBA, PointXYZRGB] 46 | - ["octree::OctreePointCloudVoxelCentroidContainer", 47 | "octree::OctreePointCloudVoxelCentroidContainer", 48 | "octree::OctreePointCloudVoxelCentroidContainer", 49 | "octree::OctreePointCloudVoxelCentroidContainer", 50 | "octree::OctreePointCloudVoxelCentroidContainer", 51 | "octree::OctreeContainerPointIndex"] 52 | - ["octree::OctreeContainerEmpty"] 53 | OctreeBase: 54 | - - ["octree::OctreePointCloudVoxelCentroidContainer", 55 | "octree::OctreePointCloudVoxelCentroidContainer", 56 | "octree::OctreePointCloudVoxelCentroidContainer", 57 | "octree::OctreePointCloudVoxelCentroidContainer", 58 | "octree::OctreePointCloudVoxelCentroidContainer", 59 | "octree::OctreeContainerPointIndex", 60 | int, 61 | "octree::OctreeContainerEmpty", 62 | "octree::OctreeContainerPointIndices"] 63 | - ["octree::OctreeContainerEmpty"] 64 | Octree2BufBase: 65 | - - [int, "octree::OctreeContainerPointIndices"] 66 | - ["octree::OctreeContainerEmpty"] 67 | MeshIO: 68 | - ["geometry::PolygonMesh>", "geometry::TriangleMesh>"] 69 | SupervoxelClustering: 70 | - [PointXYZRGBA] 71 | Supervoxel: 72 | - [PointXYZRGBA] -------------------------------------------------------------------------------- /generators/point_types_utils.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from itertools import product 3 | from typing import List, Tuple, Dict 4 | 5 | import yaml 6 | 7 | from generators.config import EXTERNAL_INHERITANCE, SKIPPED_INHERITANCE, GLOBAL_PCL_IMPORTS, KEEP_ASIS_TYPES 8 | from generators.utils import parentheses_are_balanced, make_namespace_class 9 | 10 | PCL_ALL_POINT_TYPES = { 11 | "PCL_POINT_TYPES": [ 12 | "(pcl::PointXYZ)", 13 | "(pcl::PointXYZI)", 14 | "(pcl::PointXYZL)", 15 | "(pcl::Label)", 16 | "(pcl::PointXYZRGBA)", 17 | "(pcl::PointXYZRGB)", 18 | "(pcl::PointXYZRGBL)", 19 | "(pcl::PointXYZHSV)", 20 | "(pcl::PointXY)", 21 | "(pcl::InterestPoint)", 22 | "(pcl::Axis)", 23 | "(pcl::Normal)", 24 | "(pcl::PointNormal)", 25 | "(pcl::PointXYZRGBNormal)", 26 | "(pcl::PointXYZINormal)", 27 | "(pcl::PointXYZLNormal)", 28 | "(pcl::PointWithRange)", 29 | "(pcl::PointWithViewpoint)", 30 | "(pcl::MomentInvariants)", 31 | "(pcl::PrincipalRadiiRSD)", 32 | "(pcl::Boundary)", 33 | "(pcl::PrincipalCurvatures)", 34 | "(pcl::PFHSignature125)", 35 | "(pcl::PFHRGBSignature250)", 36 | "(pcl::PPFSignature)", 37 | "(pcl::CPPFSignature)", 38 | "(pcl::PPFRGBSignature)", 39 | "(pcl::NormalBasedSignature12)", 40 | "(pcl::FPFHSignature33)", 41 | "(pcl::VFHSignature308)", 42 | "(pcl::GASDSignature512)", 43 | "(pcl::GASDSignature984)", 44 | "(pcl::GASDSignature7992)", 45 | "(pcl::GRSDSignature21)", 46 | "(pcl::ESFSignature640)", 47 | "(pcl::BRISKSignature512)", 48 | "(pcl::Narf36)", 49 | "(pcl::IntensityGradient)", 50 | "(pcl::PointWithScale)", 51 | "(pcl::PointSurfel)", 52 | "(pcl::ShapeContext1980)", 53 | "(pcl::UniqueShapeContext1960)", 54 | "(pcl::SHOT352)", 55 | "(pcl::SHOT1344)", 56 | "(pcl::PointUV)", 57 | "(pcl::ReferenceFrame)", 58 | "(pcl::PointDEM)", 59 | ], 60 | 61 | "PCL_RGB_POINT_TYPES": [ 62 | "(pcl::PointXYZRGBA)", 63 | "(pcl::PointXYZRGB)", 64 | "(pcl::PointXYZRGBL)", 65 | "(pcl::PointXYZRGBNormal)", 66 | "(pcl::PointSurfel)", 67 | ], 68 | 69 | "PCL_XYZ_POINT_TYPES": [ 70 | "(pcl::PointXYZ)", 71 | "(pcl::PointXYZI)", 72 | "(pcl::PointXYZL)", 73 | "(pcl::PointXYZRGBA)", 74 | "(pcl::PointXYZRGB)", 75 | "(pcl::PointXYZRGBL)", 76 | "(pcl::PointXYZHSV)", 77 | "(pcl::InterestPoint)", 78 | "(pcl::PointNormal)", 79 | "(pcl::PointXYZRGBNormal)", 80 | "(pcl::PointXYZINormal)", 81 | "(pcl::PointXYZLNormal)", 82 | "(pcl::PointWithRange)", 83 | "(pcl::PointWithViewpoint)", 84 | "(pcl::PointWithScale)", 85 | "(pcl::PointSurfel)", 86 | "(pcl::PointDEM)", 87 | ], 88 | 89 | "PCL_XYZL_POINT_TYPES": [ 90 | "(pcl::PointXYZL)", 91 | "(pcl::PointXYZRGBL)", 92 | "(pcl::PointXYZLNormal)", 93 | ], 94 | 95 | "PCL_NORMAL_POINT_TYPES": [ 96 | "(pcl::Normal)", 97 | "(pcl::PointNormal)", 98 | "(pcl::PointXYZRGBNormal)", 99 | "(pcl::PointXYZINormal)", 100 | "(pcl::PointXYZLNormal)", 101 | "(pcl::PointSurfel)", 102 | ], 103 | 104 | "PCL_FEATURE_POINT_TYPES": [ 105 | "(pcl::PFHSignature125)", 106 | "(pcl::PFHRGBSignature250)", 107 | "(pcl::PPFSignature)", 108 | "(pcl::CPPFSignature)", 109 | "(pcl::PPFRGBSignature)", 110 | "(pcl::NormalBasedSignature12)", 111 | "(pcl::FPFHSignature33)", 112 | "(pcl::VFHSignature308)", 113 | "(pcl::GASDSignature512)", 114 | "(pcl::GASDSignature984)", 115 | "(pcl::GASDSignature7992)", 116 | "(pcl::GRSDSignature21)", 117 | "(pcl::ESFSignature640)", 118 | "(pcl::BRISKSignature512)", 119 | "(pcl::Narf36)", 120 | ], 121 | 122 | "PCL_STATE_POINT_TYPES": [ 123 | "(pcl::tracking::ParticleXYR)", 124 | "(pcl::tracking::ParticleXYZRPY)", 125 | "(pcl::tracking::ParticleXYZR)", 126 | "(pcl::tracking::ParticleXYRPY)", 127 | "(pcl::tracking::ParticleXYRP)", 128 | ], 129 | 130 | "PCL_POINT_CLOUD_TYPES": [ 131 | "(pcl::PointCloud)", 132 | "(pcl::PointCloud)", 133 | "(pcl::PointCloud)", 134 | "(pcl::PointCloud)", 135 | "(pcl::PointCloud)", 136 | "(pcl::PointCloud)", 137 | "(pcl::PointCloud)", 138 | "(pcl::PointCloud)", 139 | "(pcl::PointCloud)", 140 | "(pcl::PointCloud)", 141 | "(pcl::PointCloud)", 142 | "(pcl::PointCloud)", 143 | "(pcl::PointCloud)", 144 | "(pcl::PointCloud)", 145 | "(pcl::PointCloud)", 146 | "(pcl::PointCloud)", 147 | "(pcl::PointCloud)", 148 | "(pcl::PointCloud)", 149 | "(pcl::PointCloud)", 150 | "(pcl::PointCloud)", 151 | "(pcl::PointCloud)", 152 | "(pcl::PointCloud)", 153 | "(pcl::PointCloud)", 154 | "(pcl::PointCloud)", 155 | "(pcl::PointCloud)", 156 | "(pcl::PointCloud)", 157 | "(pcl::PointCloud)", 158 | "(pcl::PointCloud)", 159 | "(pcl::PointCloud)", 160 | "(pcl::PointCloud)", 161 | "(pcl::PointCloud)", 162 | "(pcl::PointCloud)", 163 | "(pcl::PointCloud)", 164 | "(pcl::PointCloud)", 165 | "(pcl::PointCloud)", 166 | "(pcl::PointCloud)", 167 | "(pcl::PointCloud)", 168 | "(pcl::PointCloud)", 169 | "(pcl::PointCloud)", 170 | "(pcl::PointCloud)", 171 | "(pcl::PointCloud)", 172 | "(pcl::PointCloud)", 173 | "(pcl::PointCloud)", 174 | "(pcl::PointCloud)", 175 | "(pcl::PointCloud)", 176 | "(pcl::PointCloud)", 177 | "(pcl::PointCloud)", 178 | ], 179 | } 180 | 181 | PCL_POINT_TYPES = { 182 | "PCL_POINT_TYPES": [ 183 | "(pcl::PointXYZ)", 184 | "(pcl::PointXYZI)", 185 | "(pcl::PointXYZL)", 186 | "(pcl::Label)", 187 | "(pcl::PointXYZRGBA)", 188 | "(pcl::PointXYZRGB)", 189 | "(pcl::PointXYZRGBL)", 190 | "(pcl::PointXYZHSV)", 191 | "(pcl::PointXY)", 192 | "(pcl::InterestPoint)", 193 | "(pcl::Axis)", 194 | "(pcl::Normal)", 195 | "(pcl::PointNormal)", 196 | "(pcl::PointXYZRGBNormal)", 197 | "(pcl::PointXYZINormal)", 198 | "(pcl::PointXYZLNormal)", 199 | "(pcl::PointWithRange)", 200 | "(pcl::PointWithViewpoint)", 201 | "(pcl::MomentInvariants)", 202 | "(pcl::PrincipalRadiiRSD)", 203 | "(pcl::Boundary)", 204 | "(pcl::PrincipalCurvatures)", 205 | "(pcl::PFHSignature125)", 206 | "(pcl::PFHRGBSignature250)", 207 | "(pcl::PPFSignature)", 208 | "(pcl::CPPFSignature)", 209 | "(pcl::PPFRGBSignature)", 210 | "(pcl::NormalBasedSignature12)", 211 | "(pcl::FPFHSignature33)", 212 | "(pcl::VFHSignature308)", 213 | "(pcl::GASDSignature512)", 214 | "(pcl::GASDSignature984)", 215 | "(pcl::GASDSignature7992)", 216 | "(pcl::GRSDSignature21)", 217 | "(pcl::ESFSignature640)", 218 | "(pcl::BRISKSignature512)", 219 | "(pcl::Narf36)", 220 | "(pcl::IntensityGradient)", 221 | "(pcl::PointWithScale)", 222 | "(pcl::PointSurfel)", 223 | "(pcl::ShapeContext1980)", 224 | "(pcl::UniqueShapeContext1960)", 225 | "(pcl::SHOT352)", 226 | "(pcl::SHOT1344)", 227 | "(pcl::PointUV)", 228 | "(pcl::ReferenceFrame)", 229 | "(pcl::PointDEM)", 230 | ], 231 | 232 | "PCL_RGB_POINT_TYPES": [ 233 | "(pcl::PointXYZRGBA)", 234 | # "(pcl::PointXYZRGB)", 235 | # "(pcl::PointXYZRGBL)", 236 | # "(pcl::PointXYZRGBNormal)", 237 | # "(pcl::PointSurfel)", 238 | ], 239 | 240 | "PCL_XYZ_POINT_TYPES": [ 241 | "(pcl::PointXYZ)", 242 | "(pcl::PointXYZI)", 243 | # "(pcl::PointXYZL)", 244 | "(pcl::PointXYZRGBA)", 245 | # "(pcl::PointXYZRGB)", 246 | # "(pcl::PointXYZRGBL)", 247 | # "(pcl::PointXYZHSV)", 248 | # "(pcl::InterestPoint)", 249 | "(pcl::PointNormal)", 250 | # "(pcl::PointXYZRGBNormal)", 251 | # "(pcl::PointXYZINormal)", 252 | # "(pcl::PointXYZLNormal)", 253 | # "(pcl::PointWithRange)", 254 | # "(pcl::PointWithViewpoint)", 255 | # "(pcl::PointWithScale)", 256 | # "(pcl::PointSurfel)", 257 | # "(pcl::PointDEM)", 258 | ], 259 | 260 | "PCL_XYZL_POINT_TYPES": [ 261 | "(pcl::PointXYZL)", 262 | # "(pcl::PointXYZRGBL)", 263 | # "(pcl::PointXYZLNormal)", 264 | ], 265 | 266 | "PCL_NORMAL_POINT_TYPES": [ 267 | "(pcl::Normal)", 268 | "(pcl::PointNormal)", 269 | # "(pcl::PointXYZRGBNormal)", 270 | # "(pcl::PointXYZINormal)", 271 | # "(pcl::PointXYZLNormal)", 272 | # "(pcl::PointSurfel)", 273 | ], 274 | 275 | "PCL_FEATURE_POINT_TYPES": [ 276 | "(pcl::PFHSignature125)", 277 | "(pcl::PFHRGBSignature250)", 278 | "(pcl::PPFSignature)", 279 | "(pcl::CPPFSignature)", 280 | "(pcl::PPFRGBSignature)", 281 | "(pcl::NormalBasedSignature12)", 282 | "(pcl::FPFHSignature33)", 283 | "(pcl::VFHSignature308)", 284 | "(pcl::GASDSignature512)", 285 | "(pcl::GASDSignature984)", 286 | "(pcl::GASDSignature7992)", 287 | "(pcl::GRSDSignature21)", 288 | "(pcl::ESFSignature640)", 289 | "(pcl::BRISKSignature512)", 290 | "(pcl::Narf36)", 291 | ], 292 | 293 | "PCL_STATE_POINT_TYPES": [ 294 | "(pcl::tracking::ParticleXYR)", 295 | "(pcl::tracking::ParticleXYZRPY)", 296 | "(pcl::tracking::ParticleXYZR)", 297 | "(pcl::tracking::ParticleXYRPY)", 298 | "(pcl::tracking::ParticleXYRP)", 299 | ], 300 | 301 | "PCL_POINT_CLOUD_TYPES": [ 302 | "(pcl::PointCloud)", 303 | "(pcl::PointCloud)", 304 | # "(pcl::PointCloud)", 305 | # "(pcl::PointCloud)", 306 | "(pcl::PointCloud)", 307 | # "(pcl::PointCloud)", 308 | # "(pcl::PointCloud)", 309 | # "(pcl::PointCloud)", 310 | # "(pcl::PointCloud)", 311 | # "(pcl::PointCloud)", 312 | # "(pcl::PointCloud)", 313 | "(pcl::PointCloud)", 314 | "(pcl::PointCloud)", 315 | # "(pcl::PointCloud)", 316 | # "(pcl::PointCloud)", 317 | # "(pcl::PointCloud)", 318 | "(pcl::PointCloud)", 319 | "(pcl::PointCloud)", 320 | "(pcl::PointCloud)", 321 | "(pcl::PointCloud)", 322 | "(pcl::PointCloud)", 323 | "(pcl::PointCloud)", 324 | "(pcl::PointCloud)", 325 | "(pcl::PointCloud)", 326 | "(pcl::PointCloud)", 327 | "(pcl::PointCloud)", 328 | "(pcl::PointCloud)", 329 | "(pcl::PointCloud)", 330 | "(pcl::PointCloud)", 331 | "(pcl::PointCloud)", 332 | "(pcl::PointCloud)", 333 | "(pcl::PointCloud)", 334 | "(pcl::PointCloud)", 335 | "(pcl::PointCloud)", 336 | "(pcl::PointCloud)", 337 | "(pcl::PointCloud)", 338 | "(pcl::PointCloud)", 339 | "(pcl::PointCloud)", 340 | "(pcl::PointCloud)", 341 | "(pcl::PointCloud)", 342 | "(pcl::PointCloud)", 343 | "(pcl::PointCloud)", 344 | "(pcl::PointCloud)", 345 | "(pcl::PointCloud)", 346 | "(pcl::PointCloud)", 347 | "(pcl::PointCloud)", 348 | "(pcl::PointCloud)", 349 | ], 350 | } 351 | 352 | 353 | def unpack_yaml_point_types(path, not_every_point_type=False): 354 | data = yaml.safe_load(open(path)) 355 | for k, v in data.items(): 356 | data[k] = unpack_point_types(v, not_every_point_type) 357 | return data 358 | 359 | 360 | def filter_types(types): 361 | rgba = ["PointXYZRGBA"] if "PointXYZRGBA" in types else [] 362 | xyz = ["PointXYZ"] if "PointXYZ" in types else [] 363 | return list(set(types[:1] + rgba + xyz)) 364 | 365 | 366 | def unpack_point_types(types_info: List, not_every_point_type: bool): 367 | point_types = [] 368 | for info in types_info: 369 | if isinstance(info[0], str): 370 | if not_every_point_type: 371 | info = filter_types(info) 372 | point_types += [(t,) for t in info] 373 | else: 374 | if not_every_point_type: 375 | info = list(map(filter_types, info)) 376 | point_types += list(map(tuple, product(*info))) 377 | return point_types 378 | 379 | 380 | def fix_templated_inheritance(inherits): 381 | while True: 382 | for n, i in enumerate(inherits[:]): 383 | if not parentheses_are_balanced(i, "<>"): 384 | inherits = inherits[:n] + ["%s, %s" % (i, inherits[n + 1])] + inherits[n + 2:] 385 | break 386 | else: 387 | break 388 | return inherits 389 | 390 | 391 | def get_template_typenames_with_defaults(template: str) -> Dict[str, str]: 392 | template_typenames = {} 393 | splitters = ["typename", "class"] 394 | if template: 395 | template_string = template[template.find("<") + 1: template.rfind(">")] 396 | for name in splitters: 397 | splitted = template_string.split(name) 398 | for s in splitted: 399 | s = s.strip(", ") 400 | if "=" in s: 401 | pos = s.find("=") 402 | val = s[pos + 1:].strip(", ") 403 | template_typenames[s[:pos].strip(", ")] = val 404 | elif s: 405 | template_typenames[s] = "" 406 | template_typenames = {k: v for k, v in template_typenames.items() if not any(s in k for s in splitters)} 407 | return template_typenames 408 | 409 | 410 | def get_class_namespace(class_name): 411 | assert "::" in class_name 412 | return class_name[class_name.rfind("::"):] 413 | 414 | 415 | def split_templated_class_name(class_name: str) -> Tuple: 416 | """ 417 | Example: 418 | "OctreePointCloud::Ptr" 419 | ("OctreePointCloud", (PointT, LeafContainerT, BranchContainerT), "::Ptr") 420 | """ 421 | template_types = tuple() 422 | pos = class_name.find("<", 1) 423 | pos_end = class_name.rfind(">") 424 | is_templated = pos > 0 425 | end_pos_basename = pos if is_templated else None 426 | class_base_name = class_name[:end_pos_basename] 427 | after_template = class_name[pos_end + 1:] if is_templated else "" 428 | if is_templated: 429 | template_types = tuple([s.strip() for s in class_name[pos + 1:pos_end].split(",")]) 430 | return class_base_name, template_types, after_template 431 | 432 | 433 | def format_type_with_namespace(type_, 434 | base_namespace, 435 | namespace_by_class_name, 436 | template_typenames_defaults, 437 | replace_with_templated_typename): 438 | # handle const prefix 439 | const = "" 440 | if type_.startswith("const "): 441 | type_ = type_.replace("const ", "", 1) 442 | const = "const " 443 | 444 | is_template_name = type_ in template_typenames_defaults 445 | typename_default = template_typenames_defaults.get(type_) 446 | has_default_and_replace = typename_default and replace_with_templated_typename 447 | 448 | if has_default_and_replace: 449 | type_ = typename_default 450 | 451 | keep_asis = any(type_.startswith(i) for i in KEEP_ASIS_TYPES) 452 | 453 | if keep_asis: 454 | pass 455 | elif is_template_name: 456 | pass 457 | elif not is_template_name or has_default_and_replace: 458 | if type_ in GLOBAL_PCL_IMPORTS: 459 | base_namespace = "pcl" 460 | 461 | is_external_inheritance = any(type_.startswith(i) for i in EXTERNAL_INHERITANCE) 462 | if not is_external_inheritance: 463 | namespaces = namespace_by_class_name and namespace_by_class_name.get(type_) 464 | if namespaces: 465 | base_namespace = namespaces[0] 466 | type_ = make_namespace_class(base_namespace, type_) 467 | 468 | type_ = const + type_ 469 | return type_ 470 | 471 | 472 | def fix_cppheaderparser_bugs(inheritance: List[str]) -> List[str]: 473 | replace = { 474 | "constOctreeNode": "const OctreeNode", 475 | } 476 | fixed = [] 477 | for i in inheritance: 478 | for k, v in replace.items(): 479 | if k in i: 480 | i = i.replace(k, v) 481 | fixed.append(i) 482 | return fixed 483 | 484 | 485 | def clean_inheritance(class_, 486 | namespace_by_class_name=None, 487 | replace_with_templated_typename=True, 488 | formatted=False): 489 | inheritance = [i["class"] for i in class_["inherits"]] 490 | inheritance = fix_templated_inheritance(inheritance) 491 | inheritance = fix_cppheaderparser_bugs(inheritance) 492 | 493 | template_str = class_.get("template", "").replace("\n", "") 494 | template_typenames_defaults = get_template_typenames_with_defaults(template_str) 495 | 496 | args = { 497 | "base_namespace": class_["namespace"], 498 | "namespace_by_class_name": namespace_by_class_name, 499 | "template_typenames_defaults": template_typenames_defaults, 500 | "replace_with_templated_typename": replace_with_templated_typename, 501 | } 502 | format_namespace = partial(format_type_with_namespace, **args) 503 | 504 | for inherits in inheritance: 505 | if any(inherits.startswith(i) for i in SKIPPED_INHERITANCE): 506 | continue 507 | 508 | class_base_name, template_types, after_template = split_templated_class_name(inherits) 509 | 510 | class_base_name = format_namespace(class_base_name) 511 | template_types = tuple(map(format_namespace, template_types)) 512 | if formatted: 513 | template = ("<%s>" % ", ".join(template_types)) if template_types else "" 514 | yield "%s%s%s" % (class_base_name, template, after_template) 515 | else: 516 | yield (class_base_name, template_types) 517 | -------------------------------------------------------------------------------- /generators/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from collections import OrderedDict 3 | from os.path import join 4 | from typing import List 5 | from unidecode import unidecode 6 | 7 | from inflection import camelize 8 | 9 | from generators.config import PCL_BASE, common_includes, INDENT, TEMPLATED_METHOD_TYPES 10 | 11 | 12 | def make_header_include_name(module, header_name, path=None, path_only=False): 13 | name = path.replace("\\", "/") if path else "/".join([module, header_name]) if module else header_name 14 | if path_only: 15 | return "pcl/%s" % name 16 | else: 17 | return "#include " % name 18 | 19 | 20 | def make_namespace_class(namespace, class_name) -> str: 21 | while namespace.endswith(":"): 22 | namespace = namespace[:-1] 23 | class_name = class_name.strip() 24 | template_info = "" 25 | if "<" in class_name: 26 | p1, p2 = class_name.find("<"), class_name.rfind(">") 27 | template_string = class_name[p1 + 1:p2] 28 | other_stuff = class_name[p2 + 1:] 29 | class_name = class_name[:p1] 30 | template_info = ", ".join([make_namespace_class(namespace, t.strip()) for t in template_string.split(",")]) 31 | template_info = "<%s>%s" % (template_info, other_stuff) 32 | if not namespace.startswith("pcl"): 33 | namespace = "pcl::%s" % namespace 34 | 35 | if class_name.startswith("pcl"): 36 | pass 37 | elif class_name in TEMPLATED_METHOD_TYPES: 38 | pass 39 | else: 40 | merged = "%s::%s" % (namespace, class_name) 41 | nonrepeating = [] 42 | for name in merged.split("::"): 43 | if not nonrepeating or name != nonrepeating[-1]: 44 | nonrepeating.append(name) 45 | class_name = "::".join(nonrepeating) 46 | return class_name + template_info 47 | 48 | 49 | def function_definition_name(header_name): 50 | return camelize(header_name.replace(".h", "")).replace(" ", "") 51 | 52 | 53 | def sort_headers_by_dependencies(headers, skip_macros=None): 54 | headers = list(sorted(headers)) 55 | 56 | if skip_macros is None: 57 | skip_macros = [] 58 | 59 | def get_include_lines(path, module): 60 | lines = read_header_file(path, skip_macros).split("\n") 61 | headers = [] 62 | for line in lines: 63 | stripped = line.strip() 64 | if stripped.startswith("#include"): 65 | include_string = stripped[10:-1] 66 | headers.append(include_string) 67 | # fix for relative_imports 68 | headers.append(make_header_include_name(module, include_string, path_only=True)) 69 | return headers 70 | 71 | make_header_path = lambda module, header, path: join(PCL_BASE, path) if path else join(PCL_BASE, module, header) 72 | headers_dependencies = {header: get_include_lines(make_header_path(*header), header[0]) for header in headers} 73 | 74 | headers_include_names = OrderedDict() # output is sorted in the same way always 75 | for h in headers: 76 | headers_include_names[h] = make_header_include_name(h[0], h[1], path=h[2], path_only=True) 77 | 78 | sorted_headers = [] 79 | while headers_include_names: 80 | for header in headers_include_names: 81 | dependencies = headers_dependencies[header] 82 | if not any(h in dependencies for h in headers_include_names.values()): 83 | sorted_headers.append(header) 84 | del headers_include_names[header] 85 | break 86 | else: 87 | if all(h[0] == "outofcore" for h in headers_include_names.keys()): 88 | # special case for outofcore which seems to contain circular dependencies 89 | for header in sorted(headers_include_names.keys(), key=lambda x: len(x[1])): 90 | sorted_headers.append(header) 91 | del headers_include_names[header] 92 | else: 93 | print("Error: circular dependencies?") 94 | for h in headers_include_names: 95 | print(h) 96 | sys.exit(1) 97 | return sorted_headers 98 | 99 | 100 | def generate_main_loader(modules): 101 | modules = list(sorted(modules)) 102 | s = [common_includes] 103 | a = s.append 104 | for module in modules: 105 | a("void define%sClasses(py::module &);" % camelize(module)) 106 | a("") 107 | a("void defineClasses(py::module &m) {") 108 | for module in modules: 109 | a("%sdefine%sClasses(m);" % (INDENT, camelize(module))) 110 | a("}") 111 | return "\n".join(s) 112 | 113 | 114 | def parentheses_are_balanced(line, parenthesis): 115 | stack = [] 116 | opened, closed = parenthesis 117 | for c in line: 118 | if c == opened: 119 | stack.append(c) 120 | elif c == closed: 121 | if not stack.pop() == opened: 122 | return False 123 | return not stack 124 | 125 | 126 | def split_overloads(methods, needs_overloading: List[str] = None): 127 | if needs_overloading is None: 128 | needs_overloading = [] 129 | overloads, unique = [], [] 130 | for n, m1 in enumerate(methods): 131 | other_methods = methods[:n] + methods[n + 1:] 132 | if any(m1["name"] == m2["name"] for m2 in other_methods) or m1["name"] in needs_overloading: 133 | overloads.append(m1) 134 | else: 135 | unique.append(m1) 136 | return overloads, unique 137 | 138 | 139 | def clean_doxygen(doxygen): 140 | replace = [ 141 | ("/** ", ""), 142 | ("* ", ""), 143 | ("\n*/", ""), 144 | ("*\n", "\n"), 145 | ("{", ""), 146 | ("}", ""), 147 | ("", ""), 148 | ("", ""), 149 | ] 150 | for k, v in replace: 151 | doxygen = doxygen.replace(k, v) 152 | doxygen = unidecode(doxygen) 153 | return doxygen 154 | 155 | 156 | def replace_some_terms(raw_lines): 157 | lines = [] 158 | append = lines.append 159 | for line in raw_lines: 160 | line_strip = line.strip() 161 | if line_strip.startswith("BOOST_CONCEPT_"): 162 | pass 163 | elif line_strip.startswith("BOOST_MPL_ASSERT"): 164 | pass 165 | elif line_strip.startswith("PCL_DEPRECATED"): 166 | pass 167 | elif line_strip.startswith("POINT_CLOUD_REGISTER_POINT_STRUCT"): 168 | pass 169 | else: 170 | append(line) 171 | text = "".join(lines) 172 | text = text.replace("EIGEN_ALIGN16", "") 173 | text = text.replace("PCL_EXPORTS", "") 174 | text = text.replace("", "") # parser chokes on "boost::function" 175 | text = text.replace("->operator", "-> operator") # parser error for this expression 176 | return text 177 | 178 | 179 | def read_header_file(header_path, skip_macros): 180 | multiple_pcl_header_encodings = ["utf8", "ascii", "windows-1252"] 181 | for encoding in multiple_pcl_header_encodings: 182 | try: 183 | header_lines = open(header_path, encoding=encoding).readlines() 184 | break 185 | except UnicodeDecodeError: 186 | if encoding == multiple_pcl_header_encodings[-1]: 187 | raise 188 | 189 | active_macros = [] 190 | filtered_lines = [] 191 | a = filtered_lines.append 192 | for line in header_lines: 193 | if line.startswith("#ifdef"): 194 | active_macros.append(line.replace("#ifdef ", "").strip()) 195 | if line.startswith("#endif") and active_macros: 196 | active_macros.pop() 197 | if not any(m in active_macros for m in skip_macros): 198 | a(line) 199 | 200 | header_file_str = replace_some_terms(filtered_lines) 201 | return header_file_str 202 | -------------------------------------------------------------------------------- /pclpy/__init__.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | import pclpy.pcl as pcl 4 | 5 | from pclpy.io.functions import read 6 | from pclpy.io.las import read as read_las 7 | from pclpy.io.las import write as write_las 8 | 9 | from pclpy.api import ( 10 | extract_clusters, 11 | compute_normals, 12 | region_growing, 13 | moving_least_squares, 14 | mls, 15 | radius_outlier_removal, 16 | ror, 17 | octree_voxel_downsample, 18 | fit, 19 | ) 20 | -------------------------------------------------------------------------------- /pclpy/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.12.0" 2 | -------------------------------------------------------------------------------- /pclpy/api.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | from . import pcl 6 | 7 | from . import utils 8 | from .utils import register_point_cloud_function, register_alias 9 | 10 | 11 | @register_point_cloud_function 12 | def extract_clusters(cloud, tolerance, min_size, max_size, merge_clusters=False): 13 | """ 14 | Compute an EuclideanClusterExtraction on an input point cloud 15 | :param cloud: input point cloud 16 | :param tolerance: distance tolerance 17 | :param min_size: minimum cluster size 18 | :param max_size: maximum cluster size 19 | :param merge_clusters: set to True to output a single cloud or False to output a pcl.vectors.PointIndices 20 | :return: either an instance of pcl.PointCloud.* or pcl.vectors.PointIndices depending on merge_clusters 21 | """ 22 | pc_type = utils.get_point_cloud_type(cloud) 23 | ext = getattr(pcl.segmentation.EuclideanClusterExtraction, pc_type)() 24 | ext.setInputCloud(cloud) 25 | ext.setClusterTolerance(tolerance) 26 | ext.setMinClusterSize(min_size) 27 | ext.setMaxClusterSize(max_size) 28 | vector_indices = pcl.vectors.PointIndices() 29 | ext.extract(vector_indices) 30 | 31 | if merge_clusters: 32 | indices = pcl.vectors.Int() 33 | for i in vector_indices: 34 | indices.extend(i.indices) 35 | return getattr(pcl.PointCloud, pc_type)(cloud, indices) 36 | 37 | return vector_indices 38 | 39 | 40 | @register_point_cloud_function 41 | def region_growing( 42 | cloud, 43 | normals, 44 | n_neighbours, 45 | min_size, 46 | max_size, 47 | smooth_threshold, 48 | curvature_threshold, 49 | residual_threshold 50 | ): 51 | """ 52 | Compute a region growing segmentation 53 | :param cloud: input point cloud 54 | :param normals: input normals (can be the same as input point cloud) 55 | :param n_neighbours: number of neighbors to use 56 | :param min_size: minimum cluster size 57 | :param max_size: maximum cluster size 58 | :param smooth_threshold: (in degrees) converted to radians and passed to setSmoothnessThreshold 59 | :param curvature_threshold: passed to setCurvatureThreshold 60 | :param residual_threshold: passed to setResidualThreshold 61 | :return: a vector of pcl.PointIndices (pcl.vectors.PointIndices) 62 | """ 63 | pc_type = utils.get_point_cloud_type(cloud, normals) 64 | rg = getattr(pcl.segmentation.RegionGrowing, pc_type)() 65 | 66 | rg.setInputCloud(cloud) 67 | rg.setInputNormals(normals) 68 | 69 | rg.setMinClusterSize(min_size) 70 | rg.setMaxClusterSize(max_size) 71 | rg.setNumberOfNeighbours(n_neighbours) 72 | rg.setSmoothnessThreshold(smooth_threshold / 180 * math.pi) 73 | rg.setCurvatureThreshold(curvature_threshold) 74 | rg.setResidualThreshold(residual_threshold) 75 | vector_indices = pcl.vectors.PointIndices() 76 | rg.extract(vector_indices) 77 | return vector_indices 78 | 79 | 80 | @register_point_cloud_function 81 | def moving_least_squares(cloud, 82 | search_radius, 83 | output_cloud=None, 84 | compute_normals=False, 85 | polynomial_fit=True, 86 | polynomial_order=2, 87 | num_threads=1, 88 | ): 89 | """ 90 | Compute a moving least squares on the input cloud 91 | :param cloud: input point cloud 92 | :param search_radius: radius search distance 93 | :param output_cloud: optional point cloud to compute into 94 | :param compute_normals: boolean, set to compute normals 95 | :param polynomial_order: order of the polynomial function to fit 96 | :param num_threads: optional number of threads to use 97 | :return: a smoothed point cloud 98 | """ 99 | if output_cloud is None: 100 | if compute_normals: 101 | output_cloud = pcl.PointCloud.PointNormal() 102 | else: 103 | cloud_type = utils.get_point_cloud_type(cloud) 104 | output_cloud = getattr(pcl.PointCloud, cloud_type)() 105 | 106 | pc_type = utils.get_point_cloud_type(cloud, output_cloud) 107 | 108 | mls = getattr(pcl.surface.MovingLeastSquares, pc_type)() 109 | mls.setInputCloud(cloud) 110 | mls.setComputeNormals(compute_normals) 111 | mls.setPolynomialOrder(polynomial_order) 112 | mls.setSearchRadius(search_radius) 113 | if num_threads >= 1: 114 | mls.setNumberOfThreads(num_threads) 115 | mls.process(output_cloud) 116 | return output_cloud 117 | 118 | 119 | mls = register_alias("mls", moving_least_squares) 120 | 121 | 122 | @register_point_cloud_function 123 | def radius_outlier_removal(cloud, 124 | search_radius, 125 | min_neighbors, 126 | negative=False, 127 | indices=None, 128 | ): 129 | """ 130 | Compute a radius outlier removal 131 | :param cloud: input point cloud 132 | :param search_radius: radius search distance 133 | :param min_neighbors: minimum number of neighbors 134 | :param negative: passed to setNegative 135 | :param indices: optional indices of the input cloud to use 136 | :return: 137 | """ 138 | pc_type = utils.get_point_cloud_type(cloud) 139 | ror_filter = getattr(pcl.filters.RadiusOutlierRemoval, pc_type)() 140 | ror_filter.setInputCloud(cloud) 141 | ror_filter.setRadiusSearch(search_radius) 142 | ror_filter.setNegative(negative) 143 | ror_filter.setMinNeighborsInRadius(min_neighbors) 144 | if indices is not None: 145 | ror_filter.setIndices(indices) 146 | output = getattr(pcl.PointCloud, pc_type)() 147 | ror_filter.filter(output) 148 | return output 149 | 150 | 151 | ror = register_alias("ror", radius_outlier_removal) 152 | 153 | 154 | @register_point_cloud_function 155 | def compute_normals(cloud, 156 | radius=None, 157 | k=None, 158 | indices=None, 159 | num_threads=1, 160 | output_cloud=None, 161 | search_surface=None, 162 | ): 163 | """ 164 | Compute normals for a point cloud 165 | :param cloud: input point cloud 166 | :param radius: radius search distance 167 | :param k: use k nearest neighbors 168 | :param indices: optional indices of the input cloud to use 169 | :param num_threads: number of threads to do the computation 170 | :param output_cloud: optional point cloud to compute the normals into 171 | :param search_surface: optional point cloud search surface 172 | :return: a point cloud with normals 173 | """ 174 | if output_cloud is None: 175 | output_cloud = pcl.PointCloud.Normal() 176 | pc_type = utils.get_point_cloud_type(cloud, output_cloud) 177 | 178 | if num_threads == 1: 179 | normals_estimation = getattr(pcl.features.NormalEstimation, pc_type)() 180 | else: 181 | normals_estimation = getattr(pcl.features.NormalEstimationOMP, pc_type)(num_threads) 182 | normals_estimation.setInputCloud(cloud) 183 | if radius is not None: 184 | normals_estimation.setRadiusSearch(radius) 185 | if k is not None: 186 | normals_estimation.setKSearch(k) 187 | if indices is not None: 188 | normals_estimation.setIndices(indices) 189 | if search_surface is not None: 190 | normals_estimation.setSearchSurface(search_surface) 191 | 192 | normals_estimation.compute(output_cloud) 193 | return output_cloud 194 | 195 | 196 | @register_point_cloud_function 197 | def octree_voxel_downsample(cloud, resolution, epsilon=None, centroids=True): 198 | """ 199 | Subsample the input cloud to the octree's centroids 200 | :param cloud: input point cloud 201 | :param resolution: float size of the smallest leaf 202 | :param epsilon: epsilon precision for nearest neighbor searches, passed to setEpsilon 203 | :param centroids: Downsample using the centroid of points or the center of the voxel 204 | :return: pcl.PointCloud.* same type as input 205 | """ 206 | pc_type = utils.get_point_cloud_type(cloud) 207 | if centroids: 208 | vox = getattr(pcl.octree.OctreePointCloudVoxelCentroid, pc_type)(resolution) 209 | else: 210 | vox = getattr(pcl.octree.OctreePointCloudSinglePoint, pc_type)(resolution) 211 | vox.setInputCloud(cloud) 212 | vox.addPointsFromInputCloud() 213 | if epsilon: 214 | vox.setEpsilon(epsilon) 215 | voxel_centroids = getattr(pcl.PointCloud, pc_type)() 216 | if centroids: 217 | vox.getVoxelCentroids(voxel_centroids.points) 218 | else: 219 | vox.getOccupiedVoxelCenters(voxel_centroids.points) 220 | return voxel_centroids 221 | 222 | 223 | @register_point_cloud_function 224 | def fit(cloud, model, distance, method=pcl.sample_consensus.SAC_RANSAC, indices=None, optimize=True): 225 | """ 226 | Fit a model to a cloud using a sample consensus method 227 | :param cloud: input point cloud 228 | :param model: str (ex.: 'line', 'sphere', ...) or an instance of pcl.sample_consensus.SacModel 229 | :param distance: distance threshold 230 | :param method: SAC method to use 231 | :param indices: optional indices of the input cloud to use 232 | :param optimize: passed to setOptimizeCoefficients 233 | :return: (inliers: pcl.PointIndices, coefficients: pcl.ModelCoefficients) 234 | """ 235 | models = [ 236 | "circle2d", 237 | "circle3d", 238 | "cone", 239 | "cylinder", # needs normals 240 | "line", # needs normals 241 | "normal_parallel_plane", # needs normals 242 | "normal_plane", # needs normals 243 | "normal_sphere", # needs normals 244 | "parallel_line", 245 | "parallel_lines", 246 | "parallel_plane", 247 | "perpendicular_plane", 248 | "plane", 249 | "registration", 250 | "registration_2d", 251 | "sphere", 252 | "stick", 253 | "torus", # needs normals 254 | ] 255 | if isinstance(model, str): 256 | model = model.lower() 257 | for model_name in models: 258 | if model_name == model: 259 | model = getattr(pcl.sample_consensus, "SACMODEL_" + model_name.upper()) 260 | break 261 | 262 | if not isinstance(model, pcl.sample_consensus.SacModel): # pcl.sample_consensus.SACMODEL_* 263 | message = ("Unrecognized model: %s. Must be either a string " 264 | "or an enum from pcl.sample_consensus.SACMODEL_*") 265 | raise ValueError(message) 266 | 267 | pc_type = utils.get_point_cloud_type(cloud) 268 | seg = getattr(pcl.segmentation.SACSegmentation, pc_type)() 269 | seg.setOptimizeCoefficients(optimize) 270 | seg.setModelType(model) 271 | seg.setMethodType(method) 272 | seg.setDistanceThreshold(distance) 273 | seg.setInputCloud(cloud) 274 | 275 | if indices is not None: 276 | if isinstance(indices, np.ndarray): 277 | indices = pcl.vectors.Int(indices) 278 | seg.setIndices(indices) 279 | coefficients = pcl.ModelCoefficients() 280 | inliers = pcl.PointIndices() 281 | seg.segment(inliers, coefficients) 282 | return inliers, coefficients 283 | -------------------------------------------------------------------------------- /pclpy/io/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from .functions import read 4 | -------------------------------------------------------------------------------- /pclpy/io/functions.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from . import las 4 | 5 | 6 | def read(path: str, point_type: str, xyz_offset=None): 7 | """ 8 | Read a point cloud by guessing the filetype from its extension. 9 | """ 10 | extension = os.path.splitext(path)[1][1:].lower() 11 | if extension == "las" or extension == "laz": 12 | return las.read(path=path, point_type=point_type, xyz_offset=xyz_offset) 13 | 14 | raise ValueError("Can't guess filetype from extension ('%s')" % extension) 15 | 16 | -------------------------------------------------------------------------------- /pclpy/io/las.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import laspy 3 | 4 | from pclpy import pcl 5 | 6 | def get_las_data_type(array): 7 | las_data_types = [ 8 | "int8", 9 | "uint8", 10 | "int16", 11 | "uint16", 12 | "int32", 13 | "uint32", 14 | "int64", 15 | "uint64", 16 | "float32", 17 | "float64", 18 | # "S", # strings not implemented 19 | ] 20 | type_ = str(array.dtype) 21 | if type_ not in las_data_types: 22 | raise NotImplementedError("Array type not implemented: %s" % type_) 23 | return las_data_types.index(type_) + 1 24 | 25 | def get_offset(path): 26 | with laspy.file.File(path) as f: 27 | return f.header.offset 28 | 29 | 30 | def read(path, point_type, xyz_offset=None): 31 | assert point_type in "PointXYZ PointXYZI PointXYZINormal PointNormal PointXYZRGBNormal PointXYZRGBA" 32 | if xyz_offset is None: 33 | xyz_offset = np.array([0, 0, 0]) 34 | with laspy.file.File(path) as f: 35 | supported_attrs = "x y z intensity normal_x normal_y normal_z curvature".split() # rgb below 36 | point_type_attrs = [a for a in dir(getattr(pcl.point_types, point_type)) if not a.startswith("_")] 37 | pcl_attrs = [attr for attr in supported_attrs if attr in point_type_attrs] 38 | xyz_data = np.zeros((f.header.count, len(pcl_attrs)), "f") 39 | for n, attr in enumerate(pcl_attrs): 40 | val = getattr(f, attr) 41 | pos = "xyz".find(attr) 42 | if pos != -1: 43 | val -= xyz_offset[pos] 44 | xyz_data[:, n] = val 45 | data = [xyz_data] 46 | 47 | if all(c in point_type_attrs for c in "rgb") or "rgba" in point_type_attrs: 48 | data.append((np.array([f.red, f.green, f.blue]) / 2 ** 8).astype("u1").T) 49 | 50 | pc = getattr(pcl.PointCloud, point_type).from_array(*data) 51 | return pc 52 | 53 | 54 | def get_extra_dims(cloud): 55 | standard_dims = {"x", "y", "z", "intensity", "sensor_origin_", "sensor_orientation", "r", "g", "b"} 56 | extra_dims = [] 57 | for attr in dir(cloud): 58 | if attr in standard_dims: 59 | continue 60 | a = getattr(cloud, attr) 61 | if isinstance(a, np.ndarray) and a.ndim == 1: 62 | extra_dims.append(attr) 63 | return sorted(extra_dims) 64 | 65 | 66 | def write(cloud, path, write_extra_dimensions=True, scale=0.0001, xyz_offset=None): 67 | has = lambda x: hasattr(cloud, x) 68 | 69 | if not all([has("x") and has("y") and has("z")]): 70 | raise ValueError("Not a XYZ point type %s" % type(cloud)) 71 | 72 | has_color = has("r") and has("g") and has("b") 73 | point_format = 0 74 | if has_color: 75 | point_format = 2 76 | header = laspy.header.Header(point_format=point_format) 77 | 78 | with laspy.file.File(path, mode="w", header=header) as f: 79 | extra_dims = [] 80 | if write_extra_dimensions: 81 | extra_dims = get_extra_dims(cloud) 82 | for dim in extra_dims: 83 | data_type = get_las_data_type(getattr(cloud, dim)) 84 | f.define_new_dimension(dim, data_type, dim) 85 | 86 | f.header.scale = (scale, scale, scale) 87 | 88 | if xyz_offset is not None: 89 | f.header.offset = xyz_offset 90 | f.x = cloud.x.astype("d") + xyz_offset[0] 91 | f.y = cloud.y.astype("d") + xyz_offset[1] 92 | f.z = cloud.z.astype("d") + xyz_offset[2] 93 | else: 94 | f.header.offset = cloud.xyz.min(axis=0) 95 | f.x = cloud.x 96 | f.y = cloud.y 97 | f.z = cloud.z 98 | 99 | if has_color: 100 | f.red = cloud.r * 2 ** 8 101 | f.green = cloud.g * 2 ** 8 102 | f.blue = cloud.b * 2 ** 8 103 | if has("intensity"): 104 | f.intensity = cloud.intensity * 65535 105 | 106 | for dim in extra_dims: 107 | setattr(f, dim, getattr(cloud, dim)) 108 | -------------------------------------------------------------------------------- /pclpy/src/eigen_bind.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | namespace py = pybind11; 9 | using namespace pybind11::literals; 10 | 11 | void defineQuaternion(py::module &m) 12 | { 13 | using Class = Eigen::Quaternionf; 14 | py::class_> cls(m, "Quaternionf"); 15 | cls.def(py::init<>()); 16 | // todo: make this usable... 17 | // For now, this enables compilation because Quaternion is not in pybind11/eigen.h 18 | } 19 | 20 | //void defineVectorXf(py::module & m) { 21 | // /* Bind MatrixXd (or some other Eigen type) to Python */ 22 | // typedef Eigen::MatrixXf Matrix; 23 | // 24 | // typedef Matrix::Scalar Scalar; 25 | //// constexpr bool rowMajor = Matrix::Flags & Eigen::RowMajorBit; 26 | // bool rowMajor = true; 27 | // 28 | // py::class_ cls(m, "Matrix", py::buffer_protocol()); 29 | // cls.def("__init__", [](Matrix &m, py::buffer b) { 30 | // typedef Eigen::Stride Strides; 31 | // 32 | // /* Request a buffer descriptor from Python */ 33 | // py::buffer_info info = b.request(); 34 | // 35 | // /* Some sanity checks ... */ 36 | // if (info.format != py::format_descriptor::format()) 37 | // throw std::runtime_error("Incompatible format: expected a float array!"); 38 | // 39 | // if (info.ndim != 2) 40 | // throw std::runtime_error("Incompatible buffer dimension!"); 41 | // 42 | // auto strides = Strides( 43 | // info.strides[0] / (py::ssize_t)sizeof(Scalar), 44 | //// info.strides[rowMajor ? 0 : 1] / sizeof(Scalar), 45 | // info.strides[1] / (py::ssize_t)sizeof(Scalar)); 46 | //// info.strides[rowMajor ? 1 : 0] / sizeof(Scalar)); 47 | // 48 | // auto map = Eigen::Map( 49 | // static_cast(info.ptr), info.shape[0], info.shape[1], strides); 50 | // 51 | //// new (&m) Eigen::Map> (static_cast(info.ptr), 52 | //// info.shape[0],info.shape[1]); 53 | // 54 | // new (&m) Matrix(map); 55 | // }); 56 | // cls.def_buffer([](Matrix &m) -> py::buffer_info { 57 | // return py::buffer_info( 58 | // m.data(), /* Pointer to buffer */ 59 | // sizeof(Scalar), /* Size of one scalar */ 60 | // /* Python struct-style format descriptor */ 61 | // py::format_descriptor::format(), 62 | // /* Number of dimensions */ 63 | // 2, 64 | // /* Buffer dimensions */ 65 | // { m.rows(), 66 | // m.cols() }, 67 | // /* Strides (in bytes) for each index */ 68 | // { sizeof(Scalar) * (m.cols()), 69 | // sizeof(Scalar) * (1) } 70 | // ); 71 | // }); 72 | // 73 | //} 74 | 75 | void defineVectorXf(py::module &m) 76 | { 77 | /* Bind MatrixXd (or some other Eigen type) to Python */ 78 | typedef Eigen::VectorXf VectorXf; 79 | 80 | typedef VectorXf::Scalar Scalar; 81 | 82 | py::class_> cls(m, "VectorXf", py::buffer_protocol()); 83 | cls.def(py::init<>()); 84 | cls.def("__init__", [](VectorXf &v, py::buffer b) { 85 | typedef Eigen::Stride Strides; 86 | 87 | /* Request a buffer descriptor from Python */ 88 | py::buffer_info info = b.request(); 89 | 90 | /* Some sanity checks ... */ 91 | if (info.format != py::format_descriptor::format()) 92 | throw std::runtime_error("Incompatible format: expected a float array!"); 93 | 94 | if (info.ndim != 1) 95 | throw std::runtime_error("Incompatible buffer dimension!"); 96 | 97 | auto strides = Strides((py::ssize_t)sizeof(Scalar), 1); 98 | 99 | auto map = Eigen::Map( 100 | static_cast(info.ptr), info.shape[0], strides); 101 | 102 | // new (&m) Eigen::Map> (static_cast(info.ptr), 103 | // info.shape[0],info.shape[1]); 104 | 105 | new (&v) VectorXf(map); 106 | }); 107 | cls.def_buffer([](VectorXf &v) -> py::buffer_info { 108 | return py::buffer_info( 109 | v.data(), /* Pointer to buffer */ 110 | sizeof(Scalar), /* Size of one scalar */ 111 | /* Python struct-style format descriptor */ 112 | py::format_descriptor::format(), 113 | /* Number of dimensions */ 114 | 1, 115 | /* Buffer dimensions */ 116 | {v.size()}, 117 | /* Strides (in bytes) for each index */ 118 | {sizeof(Scalar)}); 119 | }); 120 | } 121 | 122 | void defineEigenClasses(py::module &m) 123 | { 124 | defineQuaternion(m); 125 | defineVectorXf(m); 126 | } -------------------------------------------------------------------------------- /pclpy/src/make_opaque_vectors.hpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | // #include 12 | #include 13 | 14 | #include 15 | 16 | PYBIND11_MAKE_OPAQUE(std::vector); 17 | PYBIND11_MAKE_OPAQUE(std::vector); 18 | PYBIND11_MAKE_OPAQUE(std::vector); 19 | PYBIND11_MAKE_OPAQUE(std::vector); 20 | // PYBIND11_MAKE_OPAQUE(std::vector); 21 | 22 | PYBIND11_MAKE_OPAQUE(Eigen::VectorXf); 23 | PYBIND11_MAKE_OPAQUE(Eigen::Quaternionf); 24 | 25 | //all pcl point types 26 | typedef std::vector> make_opaque_PointXYZ; 27 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZ); 28 | typedef std::vector> make_opaque_PointXYZI; 29 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZI); 30 | typedef std::vector> make_opaque_PointXYZL; 31 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZL); 32 | typedef std::vector> make_opaque_Label; 33 | PYBIND11_MAKE_OPAQUE(make_opaque_Label); 34 | typedef std::vector> make_opaque_PointXYZRGBA; 35 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZRGBA); 36 | typedef std::vector> make_opaque_PointXYZRGB; 37 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZRGB); 38 | typedef std::vector> make_opaque_PointXYZRGBL; 39 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZRGBL); 40 | typedef std::vector> make_opaque_PointXYZHSV; 41 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZHSV); 42 | typedef std::vector> make_opaque_PointXY; 43 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXY); 44 | typedef std::vector> make_opaque_InterestPoint; 45 | PYBIND11_MAKE_OPAQUE(make_opaque_InterestPoint); 46 | typedef std::vector> make_opaque_Axis; 47 | PYBIND11_MAKE_OPAQUE(make_opaque_Axis); 48 | typedef std::vector> make_opaque_Normal; 49 | PYBIND11_MAKE_OPAQUE(make_opaque_Normal); 50 | typedef std::vector> make_opaque_PointNormal; 51 | PYBIND11_MAKE_OPAQUE(make_opaque_PointNormal); 52 | typedef std::vector> make_opaque_PointXYZRGBNormal; 53 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZRGBNormal); 54 | typedef std::vector> make_opaque_PointXYZINormal; 55 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZINormal); 56 | typedef std::vector> make_opaque_PointXYZLNormal; 57 | PYBIND11_MAKE_OPAQUE(make_opaque_PointXYZLNormal); 58 | typedef std::vector> make_opaque_PointWithRange; 59 | PYBIND11_MAKE_OPAQUE(make_opaque_PointWithRange); 60 | typedef std::vector> make_opaque_PointWithViewpoint; 61 | PYBIND11_MAKE_OPAQUE(make_opaque_PointWithViewpoint); 62 | typedef std::vector> make_opaque_MomentInvariants; 63 | PYBIND11_MAKE_OPAQUE(make_opaque_MomentInvariants); 64 | typedef std::vector> make_opaque_PrincipalRadiiRSD; 65 | PYBIND11_MAKE_OPAQUE(make_opaque_PrincipalRadiiRSD); 66 | typedef std::vector> make_opaque_Boundary; 67 | PYBIND11_MAKE_OPAQUE(make_opaque_Boundary); 68 | typedef std::vector> make_opaque_PrincipalCurvatures; 69 | PYBIND11_MAKE_OPAQUE(make_opaque_PrincipalCurvatures); 70 | typedef std::vector> make_opaque_PFHSignature125; 71 | PYBIND11_MAKE_OPAQUE(make_opaque_PFHSignature125); 72 | typedef std::vector> make_opaque_PFHRGBSignature250; 73 | PYBIND11_MAKE_OPAQUE(make_opaque_PFHRGBSignature250); 74 | typedef std::vector> make_opaque_PPFSignature; 75 | PYBIND11_MAKE_OPAQUE(make_opaque_PPFSignature); 76 | typedef std::vector> make_opaque_CPPFSignature; 77 | PYBIND11_MAKE_OPAQUE(make_opaque_CPPFSignature); 78 | typedef std::vector> make_opaque_PPFRGBSignature; 79 | PYBIND11_MAKE_OPAQUE(make_opaque_PPFRGBSignature); 80 | typedef std::vector> make_opaque_NormalBasedSignature12; 81 | PYBIND11_MAKE_OPAQUE(make_opaque_NormalBasedSignature12); 82 | typedef std::vector> make_opaque_FPFHSignature33; 83 | PYBIND11_MAKE_OPAQUE(make_opaque_FPFHSignature33); 84 | typedef std::vector> make_opaque_VFHSignature308; 85 | PYBIND11_MAKE_OPAQUE(make_opaque_VFHSignature308); 86 | typedef std::vector> make_opaque_GASDSignature512; 87 | PYBIND11_MAKE_OPAQUE(make_opaque_GASDSignature512); 88 | typedef std::vector> make_opaque_GASDSignature984; 89 | PYBIND11_MAKE_OPAQUE(make_opaque_GASDSignature984); 90 | typedef std::vector> make_opaque_GASDSignature7992; 91 | PYBIND11_MAKE_OPAQUE(make_opaque_GASDSignature7992); 92 | // Linking error 93 | // typedef std::vector> make_opaque_GRSDSignature21; 94 | // PYBIND11_MAKE_OPAQUE(make_opaque_GRSDSignature21); 95 | typedef std::vector> make_opaque_ESFSignature640; 96 | PYBIND11_MAKE_OPAQUE(make_opaque_ESFSignature640); 97 | typedef std::vector> make_opaque_BRISKSignature512; 98 | PYBIND11_MAKE_OPAQUE(make_opaque_BRISKSignature512); 99 | typedef std::vector> make_opaque_Narf36; 100 | PYBIND11_MAKE_OPAQUE(make_opaque_Narf36); 101 | typedef std::vector> make_opaque_IntensityGradient; 102 | PYBIND11_MAKE_OPAQUE(make_opaque_IntensityGradient); 103 | typedef std::vector> make_opaque_PointWithScale; 104 | PYBIND11_MAKE_OPAQUE(make_opaque_PointWithScale); 105 | typedef std::vector> make_opaque_PointSurfel; 106 | PYBIND11_MAKE_OPAQUE(make_opaque_PointSurfel); 107 | typedef std::vector> make_opaque_ShapeContext1980; 108 | PYBIND11_MAKE_OPAQUE(make_opaque_ShapeContext1980); 109 | typedef std::vector> make_opaque_UniqueShapeContext1960; 110 | PYBIND11_MAKE_OPAQUE(make_opaque_UniqueShapeContext1960); 111 | typedef std::vector> make_opaque_SHOT352; 112 | PYBIND11_MAKE_OPAQUE(make_opaque_SHOT352); 113 | typedef std::vector> make_opaque_SHOT1344; 114 | PYBIND11_MAKE_OPAQUE(make_opaque_SHOT1344); 115 | typedef std::vector> make_opaque_PointUV; 116 | PYBIND11_MAKE_OPAQUE(make_opaque_PointUV); 117 | typedef std::vector> make_opaque_ReferenceFrame; 118 | PYBIND11_MAKE_OPAQUE(make_opaque_ReferenceFrame); 119 | typedef std::vector> make_opaque_PointDEM; 120 | PYBIND11_MAKE_OPAQUE(make_opaque_PointDEM); 121 | 122 | //Supervoxel 123 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZ; 124 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZ); 125 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZI; 126 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZI); 127 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZL; 128 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZL); 129 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZRGBA; 130 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZRGBA); 131 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZRGB; 132 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZRGB); 133 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZRGBL; 134 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZRGBL); 135 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZHSV; 136 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZHSV); 137 | typedef std::map::Ptr> make_opaque_Supervoxel_InterestPoint; 138 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_InterestPoint); 139 | typedef std::map::Ptr> make_opaque_Supervoxel_PointNormal; 140 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointNormal); 141 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZRGBNormal; 142 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZRGBNormal); 143 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZINormal; 144 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZINormal); 145 | typedef std::map::Ptr> make_opaque_Supervoxel_PointXYZLNormal; 146 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointXYZLNormal); 147 | typedef std::map::Ptr> make_opaque_Supervoxel_PointWithRange; 148 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointWithRange); 149 | typedef std::map::Ptr> make_opaque_Supervoxel_PointWithViewpoint; 150 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointWithViewpoint); 151 | typedef std::map::Ptr> make_opaque_Supervoxel_PointWithScale; 152 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointWithScale); 153 | typedef std::map::Ptr> make_opaque_Supervoxel_PointSurfel; 154 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointSurfel); 155 | typedef std::map::Ptr> make_opaque_Supervoxel_PointDEM; 156 | PYBIND11_MAKE_OPAQUE(make_opaque_Supervoxel_PointDEM); -------------------------------------------------------------------------------- /pclpy/src/pclpy.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | PYBIND11_DECLARE_HOLDER_TYPE(T, boost::shared_ptr); 11 | 12 | #include "make_opaque_vectors.hpp" // must be first for PYBIND11_MAKE_OPAQUE to work 13 | #include "eigen_bind.hpp" 14 | #include "generated_modules/__main_loader.hpp" 15 | #include "point_cloud_buffers.hpp" 16 | #include "point_cloud_from_array.hpp" 17 | #include "vector_classes.hpp" 18 | #include "point_types.hpp" 19 | 20 | namespace py = pybind11; 21 | using namespace pybind11::literals; 22 | 23 | using namespace pcl; 24 | 25 | template 26 | void definePointCloudBuffers(py::object &m, const char *suffix) 27 | { 28 | using PointCloud = pcl::PointCloud; 29 | using Type = py::class_>; 30 | auto pc = static_cast(m.attr(suffix)); 31 | pc.def(py::init([](py::array_t &points) { return fromArray(points); }), "array"_a); 32 | pc.def_static("from_array", &fromArray, "array"_a); 33 | defineBuffers(pc); 34 | } 35 | 36 | template 37 | void definePointCloudBuffersRGB(py::object &m, const char *suffix) 38 | { 39 | using PointCloud = pcl::PointCloud; 40 | using Type = py::class_>; 41 | auto pc = static_cast(m.attr(suffix)); 42 | pc.def(py::init([](py::array_t &points, py::array_t &rgb) { return fromArrayRGB(points, rgb); }), "array"_a, "rgb"_a); 43 | pc.def_static("from_array", &fromArrayRGB, "array"_a, "rgb"_a); 44 | defineBuffers(pc); 45 | } 46 | 47 | PYBIND11_MODULE(pcl, m) 48 | { 49 | m.doc() = "PCL python bindings"; 50 | 51 | definePointTypes(m); 52 | 53 | py::module m_vector = m.def_submodule("vectors", "Submodule for vectors"); 54 | defineEigenClasses(m_vector); 55 | defineVectorClasses(m_vector); 56 | 57 | defineClasses(m); 58 | 59 | py::object pc = m.attr("PointCloud"); 60 | definePointCloudBuffers(pc, "PointXYZ"); 61 | definePointCloudBuffers(pc, "PointXYZI"); 62 | definePointCloudBuffers(pc, "PointXYZL"); 63 | definePointCloudBuffers