├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── MANIFEST.in ├── README.md ├── parse_header.py ├── pyproject.toml ├── requirements-dev.txt ├── scripts ├── install_xerces_c.ps1 └── install_xerces_c.sh ├── setup.py ├── src └── pye57 │ ├── __init__.py │ ├── __version__.py │ ├── e57.py │ ├── exception.py │ ├── libe57_wrapper.cpp │ ├── scan_header.py │ └── utils.py └── tests ├── __init__.py ├── test_data ├── pumpAVisualReferenceImage.e57 ├── test.e57 ├── testSpherical.e57 └── testWithNormals.e57 └── test_main.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-and-test-ubuntu: 7 | name: Ubuntu (python ${{ matrix.python-version }}) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: recursive 17 | 18 | - name: Setup Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | allow-prereleases: true 23 | 24 | - name: Install build dependencies 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install -y gcc g++ libxerces-c-dev 28 | 29 | - name: Install package 30 | run: pip install . 31 | 32 | - name: Install pytest 33 | run: pip install pytest 34 | 35 | - name: Run tests 36 | run: python -m pytest tests 37 | 38 | build-and-test-windows: 39 | name: Windows (python ${{ matrix.python-version }}) 40 | runs-on: windows-2019 41 | strategy: 42 | matrix: 43 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 44 | 45 | steps: 46 | - uses: actions/checkout@v4 47 | with: 48 | submodules: recursive 49 | 50 | - name: Setup miniconda 51 | uses: conda-incubator/setup-miniconda@v2 52 | with: 53 | auto-update-conda: true 54 | python-version: ${{ matrix.python-version }} 55 | activate-environment: pye57 56 | channels: conda-forge,conda-forge/label/python_rc 57 | miniconda-version: "latest" 58 | 59 | - name: Install Dependencies 60 | shell: pwsh 61 | run: | 62 | conda install -y xerces-c 63 | 64 | - name: Configure MSVC console 65 | uses: ilammy/msvc-dev-cmd@v1 66 | 67 | - name: Install package 68 | shell: pwsh 69 | run: pip install . 70 | 71 | - name: Install pytest 72 | shell: pwsh 73 | run: pip install pytest 74 | 75 | - name: Run tests 76 | shell: pwsh 77 | run: python -m pytest tests 78 | 79 | build-and-test-macos: 80 | name: macOS (python ${{ matrix.python-version }}) 81 | runs-on: macos-latest 82 | strategy: 83 | matrix: 84 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 85 | 86 | steps: 87 | - uses: actions/checkout@v4 88 | with: 89 | submodules: recursive 90 | 91 | - name: Setup Python 92 | uses: actions/setup-python@v5 93 | with: 94 | python-version: ${{ matrix.python-version }} 95 | allow-prereleases: true 96 | 97 | - name: Install build dependencies 98 | working-directory: ./scripts 99 | run: | 100 | bash install_xerces_c.sh 101 | 102 | - name: Install package 103 | run: pip install . 104 | 105 | - name: Install pytest 106 | run: pip install pytest 107 | 108 | - name: Run tests 109 | run: python -m pytest tests 110 | 111 | build_wheels: 112 | name: Build wheels 113 | runs-on: ${{ matrix.os }} 114 | needs: 115 | - build-and-test-ubuntu 116 | - build-and-test-windows 117 | - build-and-test-macos 118 | strategy: 119 | matrix: 120 | os: ["ubuntu-latest", "windows-2019", "macos-latest"] 121 | 122 | steps: 123 | - uses: actions/checkout@v4 124 | with: 125 | submodules: recursive 126 | 127 | - name: Setup Python 128 | uses: actions/setup-python@v5 129 | with: 130 | python-version: "3.13" 131 | allow-prereleases: true 132 | 133 | - name: Install cibuildwheel 134 | run: | 135 | python -m pip install -U pip 136 | python -m pip install cibuildwheel 137 | 138 | - name: Build wheels (Ubuntu) 139 | if: matrix.os == 'ubuntu-latest' 140 | run: | 141 | python -m cibuildwheel --platform linux --output-dir wheelhouse 142 | env: 143 | CIBW_BEFORE_ALL_LINUX: "bash scripts/install_xerces_c.sh" 144 | CIBW_BUILD: "cp*-manylinux_x86_64" 145 | CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9, <3.14" 146 | 147 | - name: Build wheels (Windows) 148 | if: matrix.os == 'windows-2019' 149 | run: | 150 | python -m cibuildwheel --platform windows --output-dir wheelhouse 151 | env: 152 | CIBW_BEFORE_ALL_WINDOWS: "powershell scripts/install_xerces_c.ps1" 153 | CIBW_BUILD: "cp*-win_amd64*" 154 | CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9, <3.14" 155 | 156 | - name: Build wheels (macOS) 157 | if: matrix.os == 'macos-latest' 158 | run: | 159 | python -m cibuildwheel --platform macos --output-dir wheelhouse 160 | env: 161 | CIBW_BEFORE_ALL_MACOS: "bash scripts/install_xerces_c.sh" 162 | CIBW_BUILD: "cp*-macosx*" 163 | CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9, <3.14" 164 | 165 | - uses: actions/upload-artifact@v4 166 | with: 167 | name: wheels-${{ matrix.os }} 168 | path: wheelhouse/*.whl 169 | 170 | make_sdist: 171 | name: Make sdist 172 | runs-on: ubuntu-latest 173 | needs: 174 | - build-and-test-ubuntu 175 | - build-and-test-windows 176 | - build-and-test-macos 177 | steps: 178 | - uses: actions/checkout@v4 179 | with: 180 | submodules: recursive 181 | 182 | - name: Build SDist 183 | run: pipx run build --sdist 184 | 185 | - uses: actions/upload-artifact@v4 186 | with: 187 | name: sdist 188 | path: dist/*.tar.gz 189 | 190 | test_built_wheels: 191 | name: Test Built Wheels 192 | runs-on: ${{ matrix.os }} 193 | needs: build_wheels 194 | strategy: 195 | matrix: 196 | os: [ "ubuntu-latest", "windows-2019", "macos-latest" ] 197 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 198 | steps: 199 | - uses: actions/checkout@v4 200 | with: 201 | submodules: recursive 202 | 203 | - name: Setup Python 204 | uses: actions/setup-python@v5 205 | with: 206 | python-version: ${{ matrix.python-version }} 207 | allow-prereleases: true 208 | 209 | - uses: actions/download-artifact@v4 210 | with: 211 | name: wheels-${{ matrix.os }} 212 | path: dist 213 | 214 | - name: Install wheel 215 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' 216 | run: | 217 | python_version=$(python -c "import sys; print('cp{}{}'.format(sys.version_info.major, sys.version_info.minor))") 218 | wheel_file=$(ls dist/*$python_version*.whl) 219 | pip install $wheel_file 220 | 221 | - name: Install wheel(Windows) 222 | if: matrix.os == 'windows-2019' 223 | shell: pwsh 224 | run: | 225 | $python_version = (python -c "import sys; print('cp{}{}'.format(sys.version_info.major, sys.version_info.minor))").Trim() 226 | $wheel_file = Get-ChildItem -Path dist -Filter "*$python_version*.whl" | Select-Object -First 1 227 | python -m pip install $wheel_file.FullName 228 | 229 | - name: Verify installation 230 | run: | 231 | pip install pytest 232 | python -m pytest tests 233 | 234 | upload_all: 235 | name: Upload to pypi 236 | needs: [build_wheels, make_sdist] 237 | runs-on: ubuntu-latest 238 | if: startsWith(github.ref, 'refs/tags/v') 239 | steps: 240 | - uses: actions/download-artifact@v4 241 | with: 242 | name: wheels-ubuntu-latest 243 | path: dist 244 | 245 | - uses: actions/download-artifact@v4 246 | with: 247 | name: wheels-windows-2019 248 | path: dist 249 | 250 | - uses: actions/download-artifact@v4 251 | with: 252 | name: wheels-macos-latest 253 | path: dist 254 | 255 | - uses: actions/download-artifact@v4 256 | with: 257 | name: sdist 258 | path: dist 259 | 260 | - uses: pypa/gh-action-pypi-publish@v1.4.2 261 | with: 262 | user: __token__ 263 | password: ${{ secrets.pypi_password }} 264 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdb 2 | *.cache 3 | *.pytest_cache 4 | *.idea 5 | .vscode 6 | 7 | *.egg* 8 | *.pyd 9 | 10 | build 11 | dist 12 | wheelhouse 13 | .venv 14 | tmp 15 | 16 | __pycache__ 17 | libe57.cpython* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libE57Format"] 2 | path = libE57Format 3 | url = https://github.com/asmaloney/libE57Format 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 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 | global-include *.cpp *.h README* LICENSE* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pye57 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/pye57.svg)](https://pypi.org/project/pye57) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pye57.svg)](https://pypi.org/project/pye57) 5 | ![GitHub](https://img.shields.io/github/actions/workflow/status/davidcaron/pye57/build.yml?branch=master) 6 | 7 | Python wrapper of [LibE57Format](https://github.com/asmaloney/libE57Format) to read and write .e57 point cloud files 8 | 9 | ## Example usage 10 | 11 | ```python 12 | import numpy as np 13 | import pye57 14 | 15 | e57 = pye57.E57("e57_file.e57") 16 | 17 | # read scan at index 0 18 | data = e57.read_scan(0) 19 | 20 | # 'data' is a dictionary with the point types as keys 21 | assert isinstance(data["cartesianX"], np.ndarray) 22 | assert isinstance(data["cartesianY"], np.ndarray) 23 | assert isinstance(data["cartesianZ"], np.ndarray) 24 | 25 | # other attributes can be read using: 26 | data = e57.read_scan(0, intensity=True, colors=True, row_column=True) 27 | assert isinstance(data["cartesianX"], np.ndarray) 28 | assert isinstance(data["cartesianY"], np.ndarray) 29 | assert isinstance(data["cartesianZ"], np.ndarray) 30 | assert isinstance(data["intensity"], np.ndarray) 31 | assert isinstance(data["colorRed"], np.ndarray) 32 | assert isinstance(data["colorGreen"], np.ndarray) 33 | assert isinstance(data["colorBlue"], np.ndarray) 34 | assert isinstance(data["rowIndex"], np.ndarray) 35 | assert isinstance(data["columnIndex"], np.ndarray) 36 | 37 | # the 'read_scan' method filters points using the 'cartesianInvalidState' field 38 | # if you want to get everything as raw, untransformed data, use: 39 | data_raw = e57.read_scan_raw(0) 40 | 41 | # writing is also possible, but only using raw data for now 42 | with pye57.E57("e57_file_write.e57", mode='w') as e57_write: 43 | e57_write.write_scan_raw(data_raw) 44 | # you can specify a header to copy information from 45 | e57_write.write_scan_raw(data_raw, scan_header=e57.get_header(0)) 46 | 47 | # the ScanHeader object wraps most of the scan information: 48 | header = e57.get_header(0) 49 | print(header.point_count) 50 | print(header.rotation_matrix) 51 | print(header.translation) 52 | 53 | # all the header information can be printed using: 54 | for line in header.pretty_print(): 55 | print(line) 56 | 57 | # the scan position can be accessed with: 58 | position_scan_0 = e57.scan_position(0) 59 | 60 | # the binding is very close to the E57Foundation API 61 | # you can modify the nodes easily from python 62 | imf = e57.image_file 63 | root = imf.root() 64 | data3d = root["data3D"] 65 | scan_0 = data3d[0] 66 | translation_x = scan_0["pose"]["translation"]["x"] 67 | ``` 68 | 69 | ## Installation 70 | 71 | On linux, Windows or Apple Silicon: 72 | 73 | `python -m pip install pye57` 74 | 75 | On macOS with Intel CPU you can try to build from source (advanced users): 76 | 77 | ## Building from source (for developers) 78 | 79 | ### Cloning the repository with required submodule 80 | 81 | Clone a new repository along with the libe57Format submodule 82 | 83 | `git clone https://github.com/davidcaron/pye57.git --recursive` 84 | 85 | If the repository has already been previously cloned, but without the --recursive flag 86 | 87 | ```Bash 88 | cd pye57 # go to the cloned repository 89 | git submodule init # this will initialise the submodules in the repository 90 | git submodule update # this will update the submodules in the repository 91 | ``` 92 | 93 | ### Dependencies on Linux 94 | 95 | Install libxerces-c-dev first. 96 | 97 | `sudo apt install libxerces-c-dev` 98 | 99 | ### Dependencies on Windows 100 | 101 | To get xerces-c, you can either build from source or if you're using conda: 102 | 103 | `conda install -y xerces-c` 104 | 105 | ### Dependencies on MacOS 106 | 107 | To get xerces-c, run: 108 | 109 | `bash ./scripts/install_xerces_c.sh` 110 | 111 | ### Run `pip install` from the repo source 112 | 113 | ```Bash 114 | cd pye57 115 | python -m pip install . 116 | ``` 117 | 118 | ### Uninstalling 119 | 120 | Use pip again 121 | 122 | ```Bash 123 | python -m pip uninstall pye57 124 | ``` 125 | -------------------------------------------------------------------------------- /parse_header.py: -------------------------------------------------------------------------------- 1 | import CppHeaderParser 2 | from CppHeaderParser import CppVariable, CppClass, CppMethod, CppEnum 3 | from typing import List, Dict 4 | 5 | 6 | def gen_variables(variables: List[CppVariable]): 7 | out = [] 8 | for var in variables: 9 | string = 'm.attr("{name}") = {name};' 10 | name = var["name"] 11 | if "using" in var["type"]: 12 | continue 13 | out.append(string.format(name=name)) 14 | return out 15 | 16 | 17 | def pybind_overload(method): 18 | overload_call = "py::overload_cast<{types}>(" 19 | overload_call = overload_call.format( 20 | types=gen_args_types(method["parameters"]), 21 | ) 22 | overload_close = ")" 23 | if method["const"]: 24 | overload_close = ", py::const_)" 25 | return overload_call, overload_close 26 | 27 | 28 | def gen_method(class_name, method: CppMethod, needs_overload=False): 29 | string = 'cls_{class_name}.def("{name}", {overload_call}&{class_name}::{name}{overload_close}{args});' 30 | args = "" 31 | if method["parameters"]: 32 | args = ", " + gen_args_names(method["parameters"]) 33 | 34 | overload_call, overload_close = "", "" 35 | if needs_overload: 36 | overload_call, overload_close = pybind_overload(method) 37 | 38 | formatted = string.format( 39 | class_name=class_name, 40 | name=method["name"], 41 | args=args, 42 | overload_call=overload_call, 43 | overload_close=overload_close, 44 | ) 45 | return formatted 46 | 47 | 48 | def gen_args_types(params: List): 49 | args_types = [] 50 | for p in params: 51 | const = "" if not p["constant"] else "const " 52 | ref = "" if not p["reference"] else " &" 53 | type_ = p["type"] if p.get("enum") else p["raw_type"] 54 | ptr = "" if not p["pointer"] else " *" 55 | args_types.append(const + type_ + ref + ptr) 56 | return ", ".join(args_types) 57 | 58 | 59 | def gen_args_names(params: List): 60 | string = '"{name}"_a{default}' 61 | args_names = [] 62 | for p in params: 63 | default = "" 64 | if p.get("defaultValue"): 65 | default = "=" + p["defaultValue"].replace(" ", "") 66 | args_names.append(string.format(name=p["name"], default=default)) 67 | return ", ".join(args_names) 68 | 69 | 70 | def gen_constructor(class_name, method: CppMethod): 71 | string = 'cls_{class_name}.def(py::init<{args_types}>(){args_names});' 72 | args_names = gen_args_names(method["parameters"]) 73 | formatted = string.format( 74 | class_name=class_name, 75 | args_types=gen_args_types(method["parameters"]), 76 | args_names=", " + args_names if args_names else "", 77 | ) 78 | return formatted 79 | 80 | 81 | def gen_classes(classes: Dict[str, CppClass]): 82 | out = [] 83 | for name, class_ in classes.items(): 84 | string = 'py::class_<{name}> cls_{name}(m, "{name}");' 85 | out.append(string.format(name=name)) 86 | method_names = [m["name"] for m in class_["methods"]["public"]] 87 | for method in class_["methods"]["public"]: 88 | if "operator" in method["name"]: 89 | continue 90 | if method["constructor"]: 91 | if name in ["Node", "E57Exception"]: 92 | continue 93 | out.append(gen_constructor(name, method)) 94 | elif method["destructor"]: 95 | continue 96 | elif method["name"] in ("dump", "report"): 97 | continue 98 | else: 99 | needs_overload = method_names.count(method["name"]) >= 2 100 | out.append(gen_method(name, method, needs_overload=needs_overload)) 101 | out.append("") 102 | out.append("") 103 | 104 | return out 105 | 106 | 107 | def gen_enums(enums: List[CppEnum]): 108 | out = [] 109 | for e in enums: 110 | enum_lines = ['py::enum_<{name}>(m, "{name}")'] 111 | for value in e["values"]: 112 | enum_lines.append(' .value("%s", {name}::%s)' % (value["name"], value["name"])) 113 | enum_lines.append(" .export_values();") 114 | for line in enum_lines: 115 | out.append(line.format(name=e["name"])) 116 | return out 117 | 118 | 119 | def generate_lines(lines, indent=""): 120 | line_break = "\n" + indent 121 | return indent + line_break.join(lines) 122 | 123 | 124 | def main(path): 125 | base_indent = " " 126 | header = CppHeaderParser.CppHeader(path) 127 | variables = gen_variables(header.variables) 128 | enums = gen_enums(header.enums) 129 | classes = gen_classes(header.classes) 130 | print(generate_lines(variables + enums + classes, base_indent)) 131 | 132 | 133 | if __name__ == '__main__': 134 | path = "../libE57Format/include/E57Format.h" 135 | class_order = ["Node", 136 | "StructureNode", 137 | "VectorNode", 138 | "SourceDestBuffer", 139 | "CompressedVectorNode", 140 | "CompressedVectorReader", 141 | "CompressedVectorWriter", 142 | "IntegerNode", 143 | "ScaledIntegerNode", 144 | "FloatNode", 145 | "StringNode", 146 | "BlobNode", 147 | "ImageFile", 148 | "E57Exception", 149 | "E57Utilities", 150 | ] 151 | main(path) 152 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel", 5 | "pybind11>=2.8.0", 6 | ] 7 | 8 | build-backend = "setuptools.build_meta" 9 | 10 | [tool.cibuildwheel] 11 | # enable this when testing: faster than building from source, 12 | # but the library is not compatible with manylinux 13 | # before-all = "yum install -y xerces-c-devel" 14 | # before-all = "bash scripts/install_xerces_c.sh" 15 | # before-all = "powershell scripts/install_xerces_c.ps1" 16 | test-requires = "pytest" 17 | build = "cp*-manylinux_x86_64 cp*-win_amd64* cp*-macosx*" 18 | test-command = "python -m pytest {project}/tests" 19 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /scripts/install_xerces_c.ps1: -------------------------------------------------------------------------------- 1 | 2 | $ErrorActionPreference="Stop" 3 | 4 | $XERCES_MAJOR=3 5 | $XERCES_MINOR=2 6 | $XERCES_PATCH=3 7 | 8 | $XERCES_VERSION="$XERCES_MAJOR.$XERCES_MINOR.$XERCES_PATCH" 9 | $XSD_BUILD_AREA="$env:TEMP/build_xerces_c" 10 | $INSTALL_PREFIX="$env:TEMP/xerces_c" 11 | 12 | echo "Create Xerces-C build area..." 13 | mkdir -p ${XSD_BUILD_AREA} -ea 0 14 | cd ${XSD_BUILD_AREA} 15 | 16 | echo "Download Xerces-C..." 17 | Invoke-WebRequest -Uri "https://github.com/apache/xerces-c/archive/v${XERCES_VERSION}.tar.gz" -OutFile "xerces-c-${XERCES_VERSION}.tar.gz" 18 | 19 | echo "Extract Xerces-C..." 20 | tar xzf xerces-c-${XERCES_VERSION}.tar.gz 21 | cd xerces-c-${XERCES_VERSION} 22 | 23 | echo "Configure Xerces-C..." 24 | mkdir build -ea 0 25 | cd build 26 | cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" .. 27 | 28 | echo "Build Xerces-C..." 29 | cmake --build . --config Release --target install 30 | -------------------------------------------------------------------------------- /scripts/install_xerces_c.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | XERCES_MAJOR=3 6 | XERCES_MINOR=2 7 | XERCES_PATCH=3 8 | 9 | XERCES_VERSION=${XERCES_MAJOR}.${XERCES_MINOR}.${XERCES_PATCH} 10 | XSD_BUILD_AREA=/tmp/build_xerces_c 11 | 12 | echo "Create Xerces-C build area..." 13 | mkdir -p ${XSD_BUILD_AREA} 14 | cd ${XSD_BUILD_AREA} 15 | 16 | echo "Download Xerces-C..." 17 | curl -L https://github.com/apache/xerces-c/archive/v${XERCES_VERSION}.tar.gz --output xerces-c-${XERCES_VERSION}.tar.gz 18 | echo "Extract Xerces-C..." 19 | tar xzf xerces-c-${XERCES_VERSION}.tar.gz 20 | cd xerces-c-${XERCES_VERSION} 21 | 22 | # Check the operating system and install dependencies accordingly 23 | if [[ "$(uname)" == "Darwin" ]]; then 24 | echo "Installing dependencies on macOS..." 25 | brew install autoconf automake libtool 26 | echo "Generate configure script using autoreconf..." 27 | autoreconf -i 28 | echo "Configure Xerces-C for macOS..." 29 | ./configure --prefix=/usr/local --enable-static 30 | echo "Build Xerces-C..." 31 | make 32 | echo "Install Xerces-C..." 33 | sudo make install 34 | else 35 | echo "Installing dependencies on Linux..." 36 | ./reconf 37 | ./configure 38 | echo "Build Xerces-C..." 39 | make 40 | echo "Install Xerces-C..." 41 | make install 42 | fi 43 | 44 | echo "Clean up Xerces-C..." 45 | cd / 46 | rm -rf ${XSD_BUILD_AREA} 47 | 48 | echo "Xerces-C installed successfully." 49 | 50 | 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import shutil 4 | import sys 5 | from pathlib import Path 6 | 7 | # Available at setup time due to pyproject.toml 8 | from pybind11.setup_helpers import Pybind11Extension, build_ext 9 | from setuptools import setup 10 | 11 | HERE = Path(__file__).parent 12 | 13 | about = {} 14 | with open(HERE / "src" / "pye57" / "__version__.py") as f: 15 | exec(f.read(), about) 16 | version = about["__version__"] 17 | 18 | libE57_cpp = sorted(map(str, (HERE / "libE57Format" / "src").glob("*.cpp"))) 19 | 20 | libraries = [] 21 | library_dirs = [] 22 | include_dirs = [ 23 | "libE57Format/include", 24 | "libE57Format/src", 25 | "libE57Format/extern/CRCpp/inc", 26 | ] 27 | package_data = [] 28 | extra_link_args = [] 29 | 30 | if platform.system() == "Windows": 31 | libraries.append("xerces-c_3") 32 | 33 | # using conda 34 | conda_library_dir = Path(sys.executable).parent / "Library" 35 | if conda_library_dir.exists(): 36 | library_dirs.append(str(conda_library_dir / "lib")) 37 | include_dirs.append(str(conda_library_dir / "include")) 38 | 39 | # using cibuildwheel 40 | xerces_dir = Path(os.environ["TEMP"]) / "xerces_c" 41 | if xerces_dir.exists(): 42 | library_dirs.append(str(xerces_dir / "lib")) 43 | include_dirs.append(str(xerces_dir / "include")) 44 | # include xerces-c dll in the package 45 | shutil.copy2(xerces_dir / "bin" / "xerces-c_3_2.dll", HERE / "src" / "pye57") 46 | package_data.append("xerces-c_3_2.dll") 47 | elif platform.system() == "Darwin": 48 | xerces_dir = Path("/usr/local/") 49 | if xerces_dir.exists(): 50 | # include xerces-c dylib in the package 51 | shutil.copy2(xerces_dir / "lib" / "libxerces-c-3.2.dylib", HERE / "src" / "pye57") 52 | library_dirs.append(str(xerces_dir / "lib")) 53 | include_dirs.append(str(xerces_dir / "include")) 54 | package_data.append("libxerces-c-3.2.dylib") 55 | libraries.append("xerces-c") 56 | extra_link_args = [ 57 | f"-Wl,-rpath,@loader_path", 58 | f"-L{str(HERE / 'src' / 'pye57')}", 59 | "-lxerces-c", 60 | ] 61 | else: 62 | libraries.append("xerces-c") 63 | 64 | ext_modules = [ 65 | Pybind11Extension( 66 | "pye57.libe57", 67 | ["src/pye57/libe57_wrapper.cpp"] + libE57_cpp, 68 | define_macros=[("E57_DLL", "")], 69 | include_dirs=include_dirs, 70 | libraries=libraries, 71 | library_dirs=library_dirs, 72 | language="c++", 73 | extra_link_args=extra_link_args, 74 | ), 75 | ] 76 | 77 | export_header_path = HERE / "libE57Format" / "include" / "E57Export.h" 78 | 79 | 80 | class BuildExt(build_ext): 81 | """A custom build extension for adding compiler-specific options.""" 82 | 83 | def build_extensions(self): 84 | ct = self.compiler.compiler_type 85 | opts = [] 86 | revision_id = "pye57-" + version 87 | if ct == "unix": 88 | opts.append(f'-DVERSION_INFO="{version}"') 89 | opts.append(f'-DREVISION_ID="{revision_id}"') 90 | opts.append("-DCRCPP_USE_CPP11") 91 | opts.append("-DCRCPP_BRANCHLESS") 92 | opts.append("-Wno-unused-variable") 93 | opts.append("-DE57_ENABLE_DIAGNOSTIC_OUTPUT") 94 | elif ct == "msvc": 95 | opts.append(f'/DVERSION_INFO="{version}"') 96 | opts.append(rf'/DREVISION_ID="\"{revision_id}\""') 97 | opts.append("/DCRCPP_USE_CPP11") 98 | opts.append("/DCRCPP_BRANCHLESS") 99 | opts.append("/DWINDOWS") 100 | opts.append("/DE57_ENABLE_DIAGNOSTIC_OUTPUT") 101 | for ext in self.extensions: 102 | ext.extra_compile_args = opts 103 | 104 | export_header_path.touch() 105 | try: 106 | super().build_extensions() 107 | finally: 108 | export_header_path.unlink() 109 | 110 | 111 | with open(HERE / "README.md") as f: 112 | long_description = "\n" + f.read() 113 | 114 | setup( 115 | name="pye57", 116 | version=version, 117 | author="David Caron", 118 | author_email="dcaron05@gmail.com", 119 | maintainer="Graham Knapp", 120 | maintainer_email="graham.knapp@gmail.com", 121 | url="https://www.github.com/davidcaron/pye57", 122 | description="Python .e57 files reader/writer", 123 | long_description=long_description, 124 | long_description_content_type="text/markdown", 125 | install_requires=["numpy", "pyquaternion"], 126 | ext_modules=ext_modules, 127 | packages=["pye57"], 128 | package_dir={"": "src"}, 129 | # include_package_data=True, 130 | package_data={"pye57": package_data}, 131 | extras_require={"test": "pytest"}, 132 | license="MIT", 133 | classifiers=[ 134 | "License :: OSI Approved :: MIT License", 135 | "Programming Language :: Python", 136 | "Programming Language :: Python :: 3", 137 | "Programming Language :: Python :: 3.9", 138 | "Programming Language :: Python :: 3.10", 139 | "Programming Language :: Python :: 3.11", 140 | "Programming Language :: Python :: 3.12", 141 | "Programming Language :: Python :: 3.13", 142 | "Programming Language :: Python :: Implementation :: CPython", 143 | ], 144 | cmdclass={"build_ext": BuildExt}, 145 | zip_safe=False, 146 | python_requires=">=3.9", 147 | ) 148 | -------------------------------------------------------------------------------- /src/pye57/__init__.py: -------------------------------------------------------------------------------- 1 | from pye57 import libe57 2 | from pye57.scan_header import ScanHeader 3 | from pye57.e57 import E57 4 | -------------------------------------------------------------------------------- /src/pye57/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.4.17" 2 | -------------------------------------------------------------------------------- /src/pye57/e57.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import os 3 | from typing import Dict 4 | from enum import Enum 5 | 6 | import numpy as np 7 | from pyquaternion import Quaternion 8 | 9 | from pye57.__version__ import __version__ 10 | from pye57 import libe57 11 | from pye57 import ScanHeader 12 | from pye57.utils import convert_spherical_to_cartesian 13 | 14 | try: 15 | from exceptions import WindowsError 16 | except ImportError: 17 | class WindowsError(OSError): 18 | pass 19 | 20 | 21 | SUPPORTED_CARTESIAN_POINT_FIELDS = { 22 | "cartesianX": "d", 23 | "cartesianY": "d", 24 | "cartesianZ": "d", 25 | } 26 | 27 | SUPPORTED_SPHERICAL_POINT_FIELDS = { 28 | "sphericalRange": "d", 29 | "sphericalAzimuth": "d", 30 | "sphericalElevation": "d", 31 | } 32 | 33 | class COORDINATE_SYSTEMS(Enum): 34 | CARTESIAN = SUPPORTED_CARTESIAN_POINT_FIELDS 35 | SPHERICAL = SUPPORTED_SPHERICAL_POINT_FIELDS 36 | 37 | SUPPORTED_POINT_FIELDS = { 38 | **SUPPORTED_CARTESIAN_POINT_FIELDS, 39 | **SUPPORTED_SPHERICAL_POINT_FIELDS, 40 | "intensity": "f", 41 | "colorRed": "B", 42 | "colorGreen": "B", 43 | "colorBlue": "B", 44 | "rowIndex": "H", 45 | "columnIndex": "H", 46 | "cartesianInvalidState": "b", 47 | "sphericalInvalidState": "b", 48 | } 49 | 50 | 51 | class E57: 52 | def __init__(self, path, mode="r"): 53 | if mode not in "rw": 54 | raise ValueError("Only 'r' and 'w' modes are supported") 55 | self.path = path 56 | try: 57 | self.image_file = libe57.ImageFile(path, mode) 58 | if mode == "w": 59 | self.write_default_header() 60 | except Exception as e: 61 | try: 62 | self.image_file.close() 63 | os.remove(path) 64 | except (AttributeError, WindowsError, PermissionError): 65 | pass 66 | raise e 67 | 68 | def __del__(self): 69 | self.close() 70 | 71 | def __enter__(self): 72 | return self 73 | 74 | def __exit__(self, exc_type, exc_val, exc_tb): 75 | self.close() 76 | 77 | def close(self): 78 | if hasattr(self, "image_file"): 79 | self.image_file.close() 80 | 81 | @property 82 | def root(self): 83 | return self.image_file.root() 84 | 85 | @property 86 | def data3d(self): 87 | return self.root["data3D"] 88 | 89 | @property 90 | def scan_count(self): 91 | return len(self.data3d) 92 | 93 | def get_header(self, index): 94 | return ScanHeader(self.data3d[index]) 95 | 96 | def write_default_header(self): 97 | imf = self.image_file 98 | imf.extensionsAdd("", libe57.E57_V1_0_URI) 99 | self.root.set("formatName", libe57.StringNode(imf, "ASTM E57 3D Imaging Data File")) 100 | self.root.set("guid", libe57.StringNode(imf, "{%s}" % uuid.uuid4())) 101 | self.root.set("versionMajor", libe57.IntegerNode(imf, libe57.E57_FORMAT_MAJOR)) 102 | self.root.set("versionMinor", libe57.IntegerNode(imf, libe57.E57_FORMAT_MINOR)) 103 | self.root.set("e57LibraryVersion", libe57.StringNode(imf, libe57.E57_LIBRARY_ID)) 104 | self.root.set("coordinateMetadata", libe57.StringNode(imf, "")) 105 | creation_date_time = libe57.StructureNode(imf) 106 | creation_date_time.set("dateTimeValue", libe57.FloatNode(imf, 0.0)) 107 | creation_date_time.set("isAtomicClockReferenced", libe57.IntegerNode(imf, 0)) 108 | self.root.set("creationDateTime", creation_date_time) 109 | self.root.set("data3D", libe57.VectorNode(imf, True)) 110 | self.root.set("images2D", libe57.VectorNode(imf, True)) 111 | 112 | def make_buffer(self, field_name, capacity, do_conversion=True, do_scaling=True): 113 | # now this exception should never get hit through read_scan or read_scan_raw 114 | # for read_scan, the headers are constructed, so they should all be supported 115 | # for read_scan_raw, it now filters out the unsupported headers 116 | # however, if make_buffer or make_buffers gets called, an unsupported field name could be passed in directly 117 | # if we don't want users calling them, maybe we could make them private, and this would be an assertion 118 | if field_name not in SUPPORTED_POINT_FIELDS: 119 | raise ValueError("Unsupported point field: %s" % field_name) 120 | 121 | np_array = np.empty(capacity, SUPPORTED_POINT_FIELDS[field_name]) 122 | buffer = libe57.SourceDestBuffer(self.image_file, 123 | field_name, 124 | np_array, 125 | capacity, 126 | do_conversion, 127 | do_scaling) 128 | return np_array, buffer 129 | 130 | def make_buffers(self, field_names, capacity, do_conversion=True, do_scaling=True): 131 | data = {} 132 | buffers = libe57.VectorSourceDestBuffer() 133 | for field in field_names: 134 | d, b = self.make_buffer(field, capacity, do_conversion=do_conversion, do_scaling=do_scaling) 135 | data[field] = d 136 | buffers.append(b) 137 | return data, buffers 138 | 139 | def read_scan_raw(self, index, ignore_unsupported_fields=False) -> Dict: 140 | header = self.get_header(index) 141 | supported_point_fields = [] 142 | unsupported_point_fields = [] 143 | for field in header.point_fields: 144 | if field in SUPPORTED_POINT_FIELDS: 145 | supported_point_fields.append(field) 146 | else: 147 | unsupported_point_fields.append(field) 148 | if unsupported_point_fields != [] and not ignore_unsupported_fields: 149 | raise ValueError("Unsupported point fields: %s.\n" 150 | "Consider using 'ignore_unsupported_fields' to skip them." % unsupported_point_fields) 151 | # could it call make_buffers instead, it looks like the code's identical 152 | data = {} 153 | buffers = libe57.VectorSourceDestBuffer() 154 | for field in supported_point_fields: 155 | np_array, buffer = self.make_buffer(field, header.point_count) 156 | data[field] = np_array 157 | buffers.append(buffer) 158 | 159 | header.points.reader(buffers).read() 160 | 161 | return data 162 | 163 | def scan_position(self, index): 164 | pt = np.array([[0, 0, 0]]) 165 | header = self.get_header(index) 166 | return self.to_global(pt, header.rotation, header.translation) 167 | 168 | @staticmethod 169 | def to_global(points, rotation, translation): 170 | rotation_matrix = Quaternion(rotation).rotation_matrix 171 | return (np.dot(rotation_matrix, points.T) + translation.reshape(3, 1)).T 172 | 173 | def read_scan(self, 174 | index, 175 | *, 176 | intensity=False, 177 | colors=False, 178 | row_column=False, 179 | transform=True, 180 | ignore_missing_fields=False) -> Dict: 181 | header = self.get_header(index) 182 | n_points = header.point_count 183 | 184 | coordinate_system = header.get_coordinate_system(COORDINATE_SYSTEMS) 185 | if coordinate_system is COORDINATE_SYSTEMS.CARTESIAN: 186 | validState = "cartesianInvalidState" 187 | fields = list(SUPPORTED_CARTESIAN_POINT_FIELDS.keys()) 188 | elif coordinate_system is COORDINATE_SYSTEMS.SPHERICAL: 189 | validState = "sphericalInvalidState" 190 | fields = list(SUPPORTED_SPHERICAL_POINT_FIELDS.keys()) 191 | if intensity: 192 | fields.append("intensity") 193 | if colors: 194 | fields.append("colorRed") 195 | fields.append("colorGreen") 196 | fields.append("colorBlue") 197 | if row_column: 198 | fields.append("rowIndex") 199 | fields.append("columnIndex") 200 | fields.append(validState) 201 | 202 | for field in fields[:]: 203 | if field not in header.point_fields: 204 | if ignore_missing_fields: 205 | fields.remove(field) 206 | else: 207 | raise ValueError("Requested to read a field (%s) with is absent from the e57 file. " 208 | "Consider using 'ignore_missing_fields' to skip it." % field) 209 | 210 | data, buffers = self.make_buffers(fields, n_points) 211 | header.points.reader(buffers).read() 212 | 213 | if validState in data: 214 | valid = ~data[validState].astype("?") 215 | 216 | for field in data: 217 | data[field] = data[field][valid] 218 | 219 | del data[validState] 220 | 221 | if transform: 222 | if coordinate_system is COORDINATE_SYSTEMS.CARTESIAN: 223 | xyz = np.array([data["cartesianX"], data["cartesianY"], data["cartesianZ"]]).T 224 | elif coordinate_system is COORDINATE_SYSTEMS.SPHERICAL: 225 | rae = np.array([data["sphericalRange"], data["sphericalAzimuth"], data["sphericalElevation"]]).T 226 | # rae to xyz 227 | xyz = convert_spherical_to_cartesian(rae) 228 | # translation to global coordinates 229 | if header.has_pose(): 230 | xyz = self.to_global(xyz, header.rotation, header.translation) 231 | data["cartesianX"] = xyz[:, 0] 232 | data["cartesianY"] = xyz[:, 1] 233 | data["cartesianZ"] = xyz[:, 2] 234 | return data 235 | 236 | def write_scan_raw(self, data: Dict, *, name=None, rotation=None, translation=None, scan_header=None): 237 | for field in data.keys(): 238 | if field not in SUPPORTED_POINT_FIELDS: 239 | raise ValueError("Unsupported point field: %s" % field) 240 | 241 | if rotation is None: 242 | rotation = getattr(scan_header, "rotation", np.array([1, 0, 0, 0])) 243 | 244 | if translation is None: 245 | translation = getattr(scan_header, "translation", np.array([0, 0, 0])) 246 | 247 | if name is None: 248 | name = getattr(scan_header, "name", "Scan %s" % len(self.data3d)) 249 | 250 | temperature = getattr(scan_header, "temperature", 0) 251 | relativeHumidity = getattr(scan_header, "relativeHumidity", 0) 252 | atmosphericPressure = getattr(scan_header, "atmosphericPressure", 0) 253 | 254 | scan_node = libe57.StructureNode(self.image_file) 255 | scan_node.set("guid", libe57.StringNode(self.image_file, "{%s}" % uuid.uuid4())) 256 | scan_node.set("name", libe57.StringNode(self.image_file, name)) 257 | scan_node.set("temperature", libe57.FloatNode(self.image_file, temperature)) 258 | scan_node.set("relativeHumidity", libe57.FloatNode(self.image_file, relativeHumidity)) 259 | scan_node.set("atmosphericPressure", libe57.FloatNode(self.image_file, atmosphericPressure)) 260 | scan_node.set("description", libe57.StringNode(self.image_file, "pye57 v%s" % __version__)) 261 | 262 | n_points = data["cartesianX"].shape[0] 263 | 264 | ibox = libe57.StructureNode(self.image_file) 265 | if "rowIndex" in data and "columnIndex" in data: 266 | min_row = np.min(data["rowIndex"]) 267 | max_row = np.max(data["rowIndex"]) 268 | min_col = np.min(data["columnIndex"]) 269 | max_col = np.max(data["columnIndex"]) 270 | ibox.set("rowMinimum", libe57.IntegerNode(self.image_file, min_row)) 271 | ibox.set("rowMaximum", libe57.IntegerNode(self.image_file, max_row)) 272 | ibox.set("columnMinimum", libe57.IntegerNode(self.image_file, min_col)) 273 | ibox.set("columnMaximum", libe57.IntegerNode(self.image_file, max_col)) 274 | else: 275 | ibox.set("rowMinimum", libe57.IntegerNode(self.image_file, 0)) 276 | ibox.set("rowMaximum", libe57.IntegerNode(self.image_file, n_points - 1)) 277 | ibox.set("columnMinimum", libe57.IntegerNode(self.image_file, 0)) 278 | ibox.set("columnMaximum", libe57.IntegerNode(self.image_file, 0)) 279 | ibox.set("returnMinimum", libe57.IntegerNode(self.image_file, 0)) 280 | ibox.set("returnMaximum", libe57.IntegerNode(self.image_file, 0)) 281 | scan_node.set("indexBounds", ibox) 282 | 283 | if "intensity" in data: 284 | int_min = getattr(scan_header, "intensityMinimum", np.min(data["intensity"])) 285 | int_max = getattr(scan_header, "intensityMaximum", np.max(data["intensity"])) 286 | intbox = libe57.StructureNode(self.image_file) 287 | intbox.set("intensityMinimum", libe57.FloatNode(self.image_file, int_min)) 288 | intbox.set("intensityMaximum", libe57.FloatNode(self.image_file, int_max)) 289 | scan_node.set("intensityLimits", intbox) 290 | 291 | color = all(c in data for c in ["colorRed", "colorGreen", "colorBlue"]) 292 | if color: 293 | colorbox = libe57.StructureNode(self.image_file) 294 | colorbox.set("colorRedMinimum", libe57.IntegerNode(self.image_file, 0)) 295 | colorbox.set("colorRedMaximum", libe57.IntegerNode(self.image_file, 255)) 296 | colorbox.set("colorGreenMinimum", libe57.IntegerNode(self.image_file, 0)) 297 | colorbox.set("colorGreenMaximum", libe57.IntegerNode(self.image_file, 255)) 298 | colorbox.set("colorBlueMinimum", libe57.IntegerNode(self.image_file, 0)) 299 | colorbox.set("colorBlueMaximum", libe57.IntegerNode(self.image_file, 255)) 300 | scan_node.set("colorLimits", colorbox) 301 | 302 | bbox_node = libe57.StructureNode(self.image_file) 303 | x, y, z = data["cartesianX"], data["cartesianY"], data["cartesianZ"] 304 | valid = None 305 | if "cartesianInvalidState" in data: 306 | valid = ~data["cartesianInvalidState"].astype("?") 307 | x, y, z = x[valid], y[valid], z[valid] 308 | bb_min = np.array([x.min(), y.min(), z.min()]) 309 | bb_max = np.array([x.max(), y.max(), z.max()]) 310 | del valid, x, y, z 311 | 312 | if scan_header is not None: 313 | bb_min_scaled = np.array([scan_header.xMinimum, scan_header.yMinimum, scan_header.zMinimum]) 314 | bb_max_scaled = np.array([scan_header.xMaximum, scan_header.yMaximum, scan_header.zMaximum]) 315 | else: 316 | bb_min_scaled = self.to_global(bb_min.reshape(-1, 3), rotation, translation)[0] 317 | bb_max_scaled = self.to_global(bb_max.reshape(-1, 3), rotation, translation)[0] 318 | 319 | bbox_node.set("xMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[0])) 320 | bbox_node.set("xMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[0])) 321 | bbox_node.set("yMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[1])) 322 | bbox_node.set("yMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[1])) 323 | bbox_node.set("zMinimum", libe57.FloatNode(self.image_file, bb_min_scaled[2])) 324 | bbox_node.set("zMaximum", libe57.FloatNode(self.image_file, bb_max_scaled[2])) 325 | scan_node.set("cartesianBounds", bbox_node) 326 | 327 | if rotation is not None and translation is not None: 328 | pose_node = libe57.StructureNode(self.image_file) 329 | scan_node.set("pose", pose_node) 330 | rotation_node = libe57.StructureNode(self.image_file) 331 | rotation_node.set("w", libe57.FloatNode(self.image_file, rotation[0])) 332 | rotation_node.set("x", libe57.FloatNode(self.image_file, rotation[1])) 333 | rotation_node.set("y", libe57.FloatNode(self.image_file, rotation[2])) 334 | rotation_node.set("z", libe57.FloatNode(self.image_file, rotation[3])) 335 | pose_node.set("rotation", rotation_node) 336 | translation_node = libe57.StructureNode(self.image_file) 337 | translation_node.set("x", libe57.FloatNode(self.image_file, translation[0])) 338 | translation_node.set("y", libe57.FloatNode(self.image_file, translation[1])) 339 | translation_node.set("z", libe57.FloatNode(self.image_file, translation[2])) 340 | pose_node.set("translation", translation_node) 341 | 342 | start_datetime = getattr(scan_header, "acquisitionStart_dateTimeValue", 0) 343 | start_atomic = getattr(scan_header, "acquisitionStart_isAtomicClockReferenced", False) 344 | end_datetime = getattr(scan_header, "acquisitionEnd_dateTimeValue", 0) 345 | end_atomic = getattr(scan_header, "acquisitionEnd_isAtomicClockReferenced", False) 346 | acquisition_start = libe57.StructureNode(self.image_file) 347 | scan_node.set("acquisitionStart", acquisition_start) 348 | acquisition_start.set("dateTimeValue", libe57.FloatNode(self.image_file, start_datetime)) 349 | acquisition_start.set("isAtomicClockReferenced", libe57.IntegerNode(self.image_file, start_atomic)) 350 | acquisition_end = libe57.StructureNode(self.image_file) 351 | scan_node.set("acquisitionEnd", acquisition_end) 352 | acquisition_end.set("dateTimeValue", libe57.FloatNode(self.image_file, end_datetime)) 353 | acquisition_end.set("isAtomicClockReferenced", libe57.IntegerNode(self.image_file, end_atomic)) 354 | 355 | # todo: pointGroupingSchemes 356 | 357 | points_prototype = libe57.StructureNode(self.image_file) 358 | 359 | is_scaled = False 360 | precision = libe57.E57_DOUBLE if is_scaled else libe57.E57_SINGLE 361 | 362 | center = (bb_max + bb_min) / 2 363 | 364 | chunk_size = 5000000 365 | 366 | x_node = libe57.FloatNode(self.image_file, center[0], precision, bb_min[0], bb_max[0]) 367 | y_node = libe57.FloatNode(self.image_file, center[1], precision, bb_min[1], bb_max[1]) 368 | z_node = libe57.FloatNode(self.image_file, center[2], precision, bb_min[2], bb_max[2]) 369 | points_prototype.set("cartesianX", x_node) 370 | points_prototype.set("cartesianY", y_node) 371 | points_prototype.set("cartesianZ", z_node) 372 | 373 | field_names = ["cartesianX", "cartesianY", "cartesianZ"] 374 | 375 | if "intensity" in data: 376 | intensity_min = np.min(data["intensity"]) 377 | intensity_max = np.max(data["intensity"]) 378 | intensity_node = libe57.FloatNode(self.image_file, intensity_min, precision, intensity_min, intensity_max) 379 | points_prototype.set("intensity", intensity_node) 380 | field_names.append("intensity") 381 | 382 | if all(color in data for color in ["colorRed", "colorGreen", "colorBlue"]): 383 | points_prototype.set("colorRed", libe57.IntegerNode(self.image_file, 0, 0, 255)) 384 | points_prototype.set("colorGreen", libe57.IntegerNode(self.image_file, 0, 0, 255)) 385 | points_prototype.set("colorBlue", libe57.IntegerNode(self.image_file, 0, 0, 255)) 386 | field_names.append("colorRed") 387 | field_names.append("colorGreen") 388 | field_names.append("colorBlue") 389 | 390 | if "rowIndex" in data and "columnIndex" in data: 391 | min_row = np.min(data["rowIndex"]) 392 | max_row = np.max(data["rowIndex"]) 393 | min_col = np.min(data["columnIndex"]) 394 | max_col = np.max(data["columnIndex"]) 395 | points_prototype.set("rowIndex", libe57.IntegerNode(self.image_file, min_row, min_row, max_row)) 396 | field_names.append("rowIndex") 397 | points_prototype.set("columnIndex", libe57.IntegerNode(self.image_file, min_col, min_col, max_col)) 398 | field_names.append("columnIndex") 399 | 400 | if "cartesianInvalidState" in data: 401 | min_state = np.min(data["cartesianInvalidState"]) 402 | max_state = np.max(data["cartesianInvalidState"]) 403 | points_prototype.set("cartesianInvalidState", libe57.IntegerNode(self.image_file, 0, min_state, max_state)) 404 | field_names.append("cartesianInvalidState") 405 | 406 | # other fields 407 | # // "sphericalRange" 408 | # // "sphericalAzimuth" 409 | # // "sphericalElevation" 410 | # // "timeStamp" 411 | # // "sphericalInvalidState" 412 | # // "isColorInvalid" 413 | # // "isIntensityInvalid" 414 | # // "isTimeStampInvalid" 415 | 416 | arrays, buffers = self.make_buffers(field_names, chunk_size) 417 | 418 | codecs = libe57.VectorNode(self.image_file, True) 419 | points = libe57.CompressedVectorNode(self.image_file, points_prototype, codecs) 420 | scan_node.set("points", points) 421 | 422 | self.data3d.append(scan_node) 423 | 424 | writer = points.writer(buffers) 425 | 426 | current_index = 0 427 | while current_index != n_points: 428 | current_chunk = min(n_points - current_index, chunk_size) 429 | 430 | for type_ in SUPPORTED_POINT_FIELDS: 431 | if type_ in arrays: 432 | arrays[type_][:current_chunk] = data[type_][current_index:current_index + current_chunk] 433 | 434 | writer.write(current_chunk) 435 | 436 | current_index += current_chunk 437 | 438 | writer.close() 439 | -------------------------------------------------------------------------------- /src/pye57/exception.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class PyE57Exception(BaseException): 4 | pass -------------------------------------------------------------------------------- /src/pye57/libe57_wrapper.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace py = pybind11; 14 | using namespace pybind11::literals; 15 | 16 | using namespace e57; 17 | 18 | PYBIND11_MAKE_OPAQUE(std::vector); 19 | 20 | auto cast_node (Node &n) { 21 | NodeType type = n.type(); 22 | if (type == NodeType::E57_BLOB) 23 | return py::cast(BlobNode(n)); 24 | else if (type == NodeType::E57_COMPRESSED_VECTOR) 25 | return py::cast(CompressedVectorNode(n)); 26 | else if (type == NodeType::E57_FLOAT) 27 | return py::cast(FloatNode(n)); 28 | else if (type == NodeType::E57_INTEGER) 29 | return py::cast(IntegerNode(n)); 30 | else if (type == NodeType::E57_SCALED_INTEGER) 31 | return py::cast(ScaledIntegerNode(n)); 32 | else if (type == NodeType::E57_STRING) 33 | return py::cast(StringNode(n)); 34 | else if (type == NodeType::E57_STRUCTURE) 35 | return py::cast(StructureNode(n)); 36 | else if (type == NodeType::E57_VECTOR) 37 | return py::cast(VectorNode(n)); 38 | } 39 | 40 | PYBIND11_MODULE(libe57, m) { 41 | m.doc() = "E57 reader/writer for python."; 42 | 43 | static py::exception exc(m, "E57Exception"); 44 | py::register_exception_translator([](std::exception_ptr p) { 45 | try { 46 | if (p) std::rethrow_exception(p); 47 | } catch (const E57Exception &e) { 48 | std::stringstream ss; 49 | e.report(__FILE__, __LINE__, __FUNCTION__, ss); 50 | std::string output = e.errorStr() + std::string("\n\n") + ss.str(); 51 | exc(output.c_str()); 52 | } 53 | }); 54 | 55 | m.attr("E57_FORMAT_MAJOR") = E57_FORMAT_MAJOR; 56 | m.attr("E57_FORMAT_MINOR") = E57_FORMAT_MINOR; 57 | m.attr("E57_LIBRARY_ID") = REVISION_ID; 58 | m.attr("E57_V1_0_URI") = "http://www.astm.org/COMMIT/E57/2010-e57-v1.0"; 59 | m.attr("CHECKSUM_POLICY_NONE") = CHECKSUM_POLICY_NONE; 60 | m.attr("CHECKSUM_POLICY_SPARSE") = CHECKSUM_POLICY_SPARSE; 61 | m.attr("CHECKSUM_POLICY_HALF") = CHECKSUM_POLICY_HALF; 62 | m.attr("CHECKSUM_POLICY_ALL") = CHECKSUM_POLICY_ALL; 63 | m.attr("E57_INT8_MIN") = INT8_MIN; 64 | // for some reason INT8_MAX casts to a string not to an int ! 65 | m.attr("E57_INT8_MAX") = 127; 66 | m.attr("E57_INT16_MIN") = INT16_MIN; 67 | m.attr("E57_INT16_MAX") = INT16_MAX; 68 | m.attr("E57_INT32_MIN") = INT32_MIN; 69 | m.attr("E57_INT32_MAX") = INT32_MAX; 70 | m.attr("E57_INT64_MIN") = INT64_MIN; 71 | m.attr("E57_INT64_MAX") = INT64_MAX; 72 | m.attr("E57_UINT8_MIN") = UINT8_MIN; 73 | m.attr("E57_UINT8_MAX") = UINT8_MAX; 74 | m.attr("E57_UINT16_MIN") = UINT16_MIN; 75 | m.attr("E57_UINT16_MAX") = UINT16_MAX; 76 | m.attr("E57_UINT32_MIN") = UINT32_MIN; 77 | m.attr("E57_UINT32_MAX") = UINT32_MAX; 78 | m.attr("E57_UINT64_MIN") = UINT64_MIN; 79 | m.attr("E57_UINT64_MAX") = UINT64_MAX; 80 | m.attr("E57_FLOAT_MIN") = FLOAT_MIN; 81 | m.attr("E57_FLOAT_MAX") = FLOAT_MAX; 82 | m.attr("E57_DOUBLE_MIN") = DOUBLE_MIN; 83 | m.attr("E57_DOUBLE_MAX") = DOUBLE_MAX; 84 | py::enum_(m, "NodeType") 85 | .value("E57_STRUCTURE", NodeType::E57_STRUCTURE) 86 | .value("E57_VECTOR", NodeType::E57_VECTOR) 87 | .value("E57_COMPRESSED_VECTOR", NodeType::E57_COMPRESSED_VECTOR) 88 | .value("E57_INTEGER", NodeType::E57_INTEGER) 89 | .value("E57_SCALED_INTEGER", NodeType::E57_SCALED_INTEGER) 90 | .value("E57_FLOAT", NodeType::E57_FLOAT) 91 | .value("E57_STRING", NodeType::E57_STRING) 92 | .value("E57_BLOB", NodeType::E57_BLOB) 93 | .export_values(); 94 | py::enum_(m, "FloatPrecision") 95 | .value("E57_SINGLE", FloatPrecision::E57_SINGLE) 96 | .value("E57_DOUBLE", FloatPrecision::E57_DOUBLE) 97 | .export_values(); 98 | py::enum_(m, "MemoryRepresentation") 99 | .value("E57_INT8", MemoryRepresentation::E57_INT8) 100 | .value("E57_UINT8", MemoryRepresentation::E57_UINT8) 101 | .value("E57_INT16", MemoryRepresentation::E57_INT16) 102 | .value("E57_UINT16", MemoryRepresentation::E57_UINT16) 103 | .value("E57_INT32", MemoryRepresentation::E57_INT32) 104 | .value("E57_UINT32", MemoryRepresentation::E57_UINT32) 105 | .value("E57_INT64", MemoryRepresentation::E57_INT64) 106 | .value("E57_BOOL", MemoryRepresentation::E57_BOOL) 107 | .value("E57_REAL32", MemoryRepresentation::E57_REAL32) 108 | .value("E57_REAL64", MemoryRepresentation::E57_REAL64) 109 | .value("E57_USTRING", MemoryRepresentation::E57_USTRING) 110 | .export_values(); 111 | py::enum_(m, "ErrorCode") 112 | .value("E57_SUCCESS", ErrorCode::E57_SUCCESS) 113 | .value("E57_ERROR_BAD_CV_HEADER", ErrorCode::E57_ERROR_BAD_CV_HEADER) 114 | .value("E57_ERROR_BAD_CV_PACKET", ErrorCode::E57_ERROR_BAD_CV_PACKET) 115 | .value("E57_ERROR_CHILD_INDEX_OUT_OF_BOUNDS", ErrorCode::E57_ERROR_CHILD_INDEX_OUT_OF_BOUNDS) 116 | .value("E57_ERROR_SET_TWICE", ErrorCode::E57_ERROR_SET_TWICE) 117 | .value("E57_ERROR_HOMOGENEOUS_VIOLATION", ErrorCode::E57_ERROR_HOMOGENEOUS_VIOLATION) 118 | .value("E57_ERROR_VALUE_NOT_REPRESENTABLE", ErrorCode::E57_ERROR_VALUE_NOT_REPRESENTABLE) 119 | .value("E57_ERROR_SCALED_VALUE_NOT_REPRESENTABLE", ErrorCode::E57_ERROR_SCALED_VALUE_NOT_REPRESENTABLE) 120 | .value("E57_ERROR_REAL64_TOO_LARGE", ErrorCode::E57_ERROR_REAL64_TOO_LARGE) 121 | .value("E57_ERROR_EXPECTING_NUMERIC", ErrorCode::E57_ERROR_EXPECTING_NUMERIC) 122 | .value("E57_ERROR_EXPECTING_USTRING", ErrorCode::E57_ERROR_EXPECTING_USTRING) 123 | .value("E57_ERROR_INTERNAL", ErrorCode::E57_ERROR_INTERNAL) 124 | .value("E57_ERROR_BAD_XML_FORMAT", ErrorCode::E57_ERROR_BAD_XML_FORMAT) 125 | .value("E57_ERROR_XML_PARSER", ErrorCode::E57_ERROR_XML_PARSER) 126 | .value("E57_ERROR_BAD_API_ARGUMENT", ErrorCode::E57_ERROR_BAD_API_ARGUMENT) 127 | .value("E57_ERROR_FILE_IS_READ_ONLY", ErrorCode::E57_ERROR_FILE_IS_READ_ONLY) 128 | .value("E57_ERROR_BAD_CHECKSUM", ErrorCode::E57_ERROR_BAD_CHECKSUM) 129 | .value("E57_ERROR_OPEN_FAILED", ErrorCode::E57_ERROR_OPEN_FAILED) 130 | .value("E57_ERROR_CLOSE_FAILED", ErrorCode::E57_ERROR_CLOSE_FAILED) 131 | .value("E57_ERROR_READ_FAILED", ErrorCode::E57_ERROR_READ_FAILED) 132 | .value("E57_ERROR_WRITE_FAILED", ErrorCode::E57_ERROR_WRITE_FAILED) 133 | .value("E57_ERROR_LSEEK_FAILED", ErrorCode::E57_ERROR_LSEEK_FAILED) 134 | .value("E57_ERROR_PATH_UNDEFINED", ErrorCode::E57_ERROR_PATH_UNDEFINED) 135 | .value("E57_ERROR_BAD_BUFFER", ErrorCode::E57_ERROR_BAD_BUFFER) 136 | .value("E57_ERROR_NO_BUFFER_FOR_ELEMENT", ErrorCode::E57_ERROR_NO_BUFFER_FOR_ELEMENT) 137 | .value("E57_ERROR_BUFFER_SIZE_MISMATCH", ErrorCode::E57_ERROR_BUFFER_SIZE_MISMATCH) 138 | .value("E57_ERROR_BUFFER_DUPLICATE_PATHNAME", ErrorCode::E57_ERROR_BUFFER_DUPLICATE_PATHNAME) 139 | .value("E57_ERROR_BAD_FILE_SIGNATURE", ErrorCode::E57_ERROR_BAD_FILE_SIGNATURE) 140 | .value("E57_ERROR_UNKNOWN_FILE_VERSION", ErrorCode::E57_ERROR_UNKNOWN_FILE_VERSION) 141 | .value("E57_ERROR_BAD_FILE_LENGTH", ErrorCode::E57_ERROR_BAD_FILE_LENGTH) 142 | .value("E57_ERROR_XML_PARSER_INIT", ErrorCode::E57_ERROR_XML_PARSER_INIT) 143 | .value("E57_ERROR_DUPLICATE_NAMESPACE_PREFIX", ErrorCode::E57_ERROR_DUPLICATE_NAMESPACE_PREFIX) 144 | .value("E57_ERROR_DUPLICATE_NAMESPACE_URI", ErrorCode::E57_ERROR_DUPLICATE_NAMESPACE_URI) 145 | .value("E57_ERROR_BAD_PROTOTYPE", ErrorCode::E57_ERROR_BAD_PROTOTYPE) 146 | .value("E57_ERROR_BAD_CODECS", ErrorCode::E57_ERROR_BAD_CODECS) 147 | .value("E57_ERROR_VALUE_OUT_OF_BOUNDS", ErrorCode::E57_ERROR_VALUE_OUT_OF_BOUNDS) 148 | .value("E57_ERROR_CONVERSION_REQUIRED", ErrorCode::E57_ERROR_CONVERSION_REQUIRED) 149 | .value("E57_ERROR_BAD_PATH_NAME", ErrorCode::E57_ERROR_BAD_PATH_NAME) 150 | .value("E57_ERROR_NOT_IMPLEMENTED", ErrorCode::E57_ERROR_NOT_IMPLEMENTED) 151 | .value("E57_ERROR_BAD_NODE_DOWNCAST", ErrorCode::E57_ERROR_BAD_NODE_DOWNCAST) 152 | .value("E57_ERROR_WRITER_NOT_OPEN", ErrorCode::E57_ERROR_WRITER_NOT_OPEN) 153 | .value("E57_ERROR_READER_NOT_OPEN", ErrorCode::E57_ERROR_READER_NOT_OPEN) 154 | .value("E57_ERROR_NODE_UNATTACHED", ErrorCode::E57_ERROR_NODE_UNATTACHED) 155 | .value("E57_ERROR_ALREADY_HAS_PARENT", ErrorCode::E57_ERROR_ALREADY_HAS_PARENT) 156 | .value("E57_ERROR_DIFFERENT_DEST_IMAGEFILE", ErrorCode::E57_ERROR_DIFFERENT_DEST_IMAGEFILE) 157 | .value("E57_ERROR_IMAGEFILE_NOT_OPEN", ErrorCode::E57_ERROR_IMAGEFILE_NOT_OPEN) 158 | .value("E57_ERROR_BUFFERS_NOT_COMPATIBLE", ErrorCode::E57_ERROR_BUFFERS_NOT_COMPATIBLE) 159 | .value("E57_ERROR_TOO_MANY_WRITERS", ErrorCode::E57_ERROR_TOO_MANY_WRITERS) 160 | .value("E57_ERROR_TOO_MANY_READERS", ErrorCode::E57_ERROR_TOO_MANY_READERS) 161 | .value("E57_ERROR_BAD_CONFIGURATION", ErrorCode::E57_ERROR_BAD_CONFIGURATION) 162 | .value("E57_ERROR_INVARIANCE_VIOLATION", ErrorCode::E57_ERROR_INVARIANCE_VIOLATION) 163 | .export_values(); 164 | py::class_ cls_Node(m, "Node"); 165 | cls_Node.def("type", &Node::type); 166 | cls_Node.def("isRoot", &Node::isRoot); 167 | cls_Node.def("parent", &Node::parent); 168 | cls_Node.def("pathName", &Node::pathName); 169 | cls_Node.def("elementName", &Node::elementName); 170 | cls_Node.def("destImageFile", &Node::destImageFile); 171 | cls_Node.def("isAttached", &Node::isAttached); 172 | cls_Node.def("checkInvariant", &Node::checkInvariant, "doRecurse"_a=true, "doDowncast"_a=true); 173 | cls_Node.def("__repr__", [](const Node &node) { 174 | return ""; 175 | }); 176 | 177 | py::class_ cls_StructureNode(m, "StructureNode"); 178 | cls_StructureNode.def(py::init(), "destImageFile"_a); 179 | cls_StructureNode.def("childCount", &StructureNode::childCount); 180 | cls_StructureNode.def("isDefined", &StructureNode::isDefined, "pathName"_a); 181 | cls_StructureNode.def("get", (Node (StructureNode::*)(int64_t) const) &StructureNode::get, "index"_a); 182 | cls_StructureNode.def("get", (Node (StructureNode::*)(const std::string &) const) &StructureNode::get, "pathName"_a); 183 | // Maybe there is a more elegant way to do this 184 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, StructureNode &n){ 185 | node.set(pathName, n); 186 | }, "pathName"_a, "n"_a); 187 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, VectorNode &n){ 188 | node.set(pathName, n); 189 | }, "pathName"_a, "n"_a); 190 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, CompressedVectorNode &n){ 191 | node.set(pathName, n); 192 | }, "pathName"_a, "n"_a); 193 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, IntegerNode &n){ 194 | node.set(pathName, n); 195 | }, "pathName"_a, "n"_a); 196 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, ScaledIntegerNode &n){ 197 | node.set(pathName, n); 198 | }, "pathName"_a, "n"_a); 199 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, FloatNode &n){ 200 | node.set(pathName, n); 201 | }, "pathName"_a, "n"_a); 202 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, StringNode &n){ 203 | node.set(pathName, n); 204 | }, "pathName"_a, "n"_a); 205 | cls_StructureNode.def("set", [](StructureNode &node, const std::string &pathName, BlobNode &n){ 206 | node.set(pathName, n); 207 | }, "pathName"_a, "n"_a); 208 | cls_StructureNode.def(py::init(), "n"_a); 209 | cls_StructureNode.def("isRoot", &StructureNode::isRoot); 210 | cls_StructureNode.def("parent", &StructureNode::parent); 211 | cls_StructureNode.def("pathName", &StructureNode::pathName); 212 | cls_StructureNode.def("elementName", &StructureNode::elementName); 213 | cls_StructureNode.def("destImageFile", &StructureNode::destImageFile); 214 | cls_StructureNode.def("isAttached", &StructureNode::isAttached); 215 | cls_StructureNode.def("checkInvariant", &StructureNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 216 | cls_StructureNode.def("__len__", &StructureNode::childCount); 217 | cls_StructureNode.def("__getitem__", [](const StructureNode &node, const std::string &pathName) { 218 | Node n = node.get(pathName); 219 | return cast_node(n); 220 | }); 221 | cls_StructureNode.def("__getitem__", [](const StructureNode &node, int64_t index) { 222 | if (index >= node.childCount() || index < 0) 223 | throw py::index_error(); 224 | Node n = node.get(index); 225 | return cast_node(n); 226 | }); 227 | cls_StructureNode.def("__repr__", [](const StructureNode &node) { 228 | return ""; 229 | }); 230 | 231 | py::class_ cls_VectorNode(m, "VectorNode"); 232 | cls_VectorNode.def(py::init(), "destImageFile"_a, "allowHeteroChildren"_a=false); 233 | cls_VectorNode.def("allowHeteroChildren", &VectorNode::allowHeteroChildren); 234 | cls_VectorNode.def("childCount", &VectorNode::childCount); 235 | cls_VectorNode.def("isDefined", &VectorNode::isDefined, "pathName"_a); 236 | cls_VectorNode.def("get", (Node (VectorNode::*)(int64_t) const) &VectorNode::get, "index"_a); 237 | cls_VectorNode.def("get", (Node (VectorNode::*)(const std::string &) const) &VectorNode::get, "pathName"_a); 238 | // Maybe there is a more elegant way to do this 239 | cls_VectorNode.def("append", [](VectorNode &v, StructureNode &node) { v.append(node); }); 240 | cls_VectorNode.def("append", [](VectorNode &v, VectorNode &node) { v.append(node); }); 241 | cls_VectorNode.def("append", [](VectorNode &v, CompressedVectorNode &node) { v.append(node); }); 242 | cls_VectorNode.def("append", [](VectorNode &v, IntegerNode &node) { v.append(node); }); 243 | cls_VectorNode.def("append", [](VectorNode &v, ScaledIntegerNode &node) { v.append(node); }); 244 | cls_VectorNode.def("append", [](VectorNode &v, FloatNode &node) { v.append(node); }); 245 | cls_VectorNode.def("append", [](VectorNode &v, StringNode &node) { v.append(node); }); 246 | cls_VectorNode.def(py::init(), "n"_a); 247 | cls_VectorNode.def("isRoot", &VectorNode::isRoot); 248 | cls_VectorNode.def("parent", &VectorNode::parent); 249 | cls_VectorNode.def("pathName", &VectorNode::pathName); 250 | cls_VectorNode.def("elementName", &VectorNode::elementName); 251 | cls_VectorNode.def("destImageFile", &VectorNode::destImageFile); 252 | cls_VectorNode.def("isAttached", &VectorNode::isAttached); 253 | cls_VectorNode.def("checkInvariant", &VectorNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 254 | cls_VectorNode.def("__len__", &VectorNode::childCount); 255 | cls_VectorNode.def("__getitem__", [](const VectorNode &node, const std::string &pathName) { 256 | Node n = node.get(pathName); 257 | return cast_node(n); 258 | }); 259 | cls_VectorNode.def("__getitem__", [](const VectorNode &node, int64_t index) { 260 | if (index >= node.childCount() || index < 0) 261 | throw py::index_error(); 262 | Node n = node.get(index); 263 | return cast_node(n); 264 | }); 265 | cls_VectorNode.def("__repr__", [](const VectorNode &node) { 266 | return ""; 267 | }); 268 | 269 | py::class_ cls_SourceDestBuffer(m, "SourceDestBuffer"); 270 | cls_SourceDestBuffer.def("__init__", [](SourceDestBuffer &s, 271 | e57::ImageFile imf, 272 | const std::string pathName, 273 | py::buffer np_array, 274 | const size_t capacity, 275 | bool doConversion, 276 | bool doScaling, 277 | size_t stride=0) { 278 | py::buffer_info info = np_array.request(); 279 | const std::string dtype = info.format; 280 | 281 | if (info.ndim != 1) 282 | throw std::runtime_error("Incompatible buffer dimension!"); 283 | 284 | if (info.format == "b") 285 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(int8_t) : stride); 286 | else if (info.format == "B") 287 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(uint8_t) : stride); 288 | // Handle fixed or native byte order from https://docs.python.org/3/library/struct.html 289 | // Note - these may be platform dependent. Could they cause strange bugs on some platforms ? 290 | else if (dtype == "h" || dtype == "=h") 291 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(int16_t) : stride); 292 | else if (dtype == "H" || dtype == "=H") 293 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(uint16_t) : stride); 294 | else if (dtype == "l" || dtype == "=l") 295 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(int32_t) : stride); 296 | else if (dtype == "L" || dtype == "=L") 297 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(uint32_t) : stride); 298 | else if (dtype == "q" || dtype == "=q") 299 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(int64_t) : stride); 300 | else if (dtype == "?") 301 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(bool) : stride); 302 | else if (dtype == "f" || dtype == "=f") 303 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(float) : stride); 304 | else if (dtype == "d" || dtype == "=d") 305 | new (&s) SourceDestBuffer(imf, pathName, static_cast(info.ptr), capacity, doConversion, doScaling, (stride == 0) ? sizeof(double) : stride); 306 | else 307 | throw py::value_error("Incompatible type (integers: bBhHlLq, bool: ?, floats: fd), got: " + dtype); 308 | }, 309 | "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=0); 310 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(int8_t)); 311 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(uint8_t)); 312 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(int16_t)); 313 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(uint16_t)); 314 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(int32_t)); 315 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(uint32_t)); 316 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(int64_t)); 317 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(bool)); 318 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(float)); 319 | // cls_SourceDestBuffer.def(py::init(), "destImageFile"_a, "pathName"_a, "b"_a, "capacity"_a, "doConversion"_a=false, "doScaling"_a=false, "stride"_a=sizeof(double)); 320 | // cls_SourceDestBuffer.def(py::init *>(), "destImageFile"_a, "pathName"_a, "b"_a); 321 | cls_SourceDestBuffer.def("pathName", &SourceDestBuffer::pathName); 322 | cls_SourceDestBuffer.def("capacity", &SourceDestBuffer::capacity); 323 | cls_SourceDestBuffer.def("doConversion", &SourceDestBuffer::doConversion); 324 | cls_SourceDestBuffer.def("doScaling", &SourceDestBuffer::doScaling); 325 | cls_SourceDestBuffer.def("stride", &SourceDestBuffer::stride); 326 | cls_SourceDestBuffer.def("checkInvariant", &SourceDestBuffer::checkInvariant, "doRecurse"_a=true); 327 | cls_SourceDestBuffer.def("__repr__", [](const SourceDestBuffer &bf) { 328 | return ""; 329 | }); 330 | 331 | py::class_ cls_CompressedVectorReader(m, "CompressedVectorReader"); 332 | cls_CompressedVectorReader.def("read", (unsigned (CompressedVectorReader::*)(void)) &CompressedVectorReader::read); 333 | cls_CompressedVectorReader.def("read", (unsigned (CompressedVectorReader::*)(std::vector &)) &CompressedVectorReader::read, "dbufs"_a); 334 | cls_CompressedVectorReader.def("seek", &CompressedVectorReader::seek, "recordNumber"_a); 335 | cls_CompressedVectorReader.def("close", &CompressedVectorReader::close); 336 | cls_CompressedVectorReader.def("isOpen", &CompressedVectorReader::isOpen); 337 | cls_CompressedVectorReader.def("compressedVectorNode", &CompressedVectorReader::compressedVectorNode); 338 | cls_CompressedVectorReader.def("checkInvariant", &CompressedVectorReader::checkInvariant, "doRecurse"_a=true); 339 | cls_CompressedVectorReader.def("__del__", [](CompressedVectorReader &r) { r.close(); }); 340 | 341 | py::class_ cls_CompressedVectorWriter(m, "CompressedVectorWriter"); 342 | cls_CompressedVectorWriter.def("write", (void (CompressedVectorWriter::*)(const size_t)) &CompressedVectorWriter::write, "requestedRecordCount"_a); 343 | cls_CompressedVectorWriter.def("write", (void (CompressedVectorWriter::*)(std::vector &, const size_t)) &CompressedVectorWriter::write, "sbufs"_a, "requestedRecordCount"_a); 344 | cls_CompressedVectorWriter.def("close", &CompressedVectorWriter::close); 345 | cls_CompressedVectorWriter.def("isOpen", &CompressedVectorWriter::isOpen); 346 | cls_CompressedVectorWriter.def("compressedVectorNode", &CompressedVectorWriter::compressedVectorNode); 347 | cls_CompressedVectorWriter.def("checkInvariant", &CompressedVectorWriter::checkInvariant, "doRecurse"_a=true); 348 | cls_CompressedVectorWriter.def("__del__", [](CompressedVectorWriter &r) { r.close(); }); 349 | 350 | py::class_ cls_CompressedVectorNode(m, "CompressedVectorNode"); 351 | cls_CompressedVectorNode.def(py::init(), "destImageFile"_a, "prototype"_a, "codecs"_a); 352 | cls_CompressedVectorNode.def("__init__", [](CompressedVectorNode &n, e57::ImageFile &imf, e57::StructureNode &node, e57::VectorNode &vector_node) { 353 | new (&n) CompressedVectorNode(imf, node, vector_node); 354 | }); 355 | cls_CompressedVectorNode.def("childCount", &CompressedVectorNode::childCount); 356 | cls_CompressedVectorNode.def("prototype", &CompressedVectorNode::prototype); 357 | cls_CompressedVectorNode.def("codecs", &CompressedVectorNode::codecs); 358 | cls_CompressedVectorNode.def("writer", &CompressedVectorNode::writer, "sbufs"_a); 359 | cls_CompressedVectorNode.def("reader", &CompressedVectorNode::reader, "dbufs"_a); 360 | cls_CompressedVectorNode.def(py::init(), "n"_a); 361 | cls_CompressedVectorNode.def("isRoot", &CompressedVectorNode::isRoot); 362 | cls_CompressedVectorNode.def("parent", &CompressedVectorNode::parent); 363 | cls_CompressedVectorNode.def("pathName", &CompressedVectorNode::pathName); 364 | cls_CompressedVectorNode.def("elementName", &CompressedVectorNode::elementName); 365 | cls_CompressedVectorNode.def("destImageFile", &CompressedVectorNode::destImageFile); 366 | cls_CompressedVectorNode.def("isAttached", &CompressedVectorNode::isAttached); 367 | cls_CompressedVectorNode.def("checkInvariant", &CompressedVectorNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 368 | cls_CompressedVectorNode.def("__repr__", [](const CompressedVectorNode &node) { 369 | return ""; 370 | }); 371 | 372 | py::class_ cls_IntegerNode(m, "IntegerNode"); 373 | cls_IntegerNode.def(py::init(), "destImageFile"_a, "value"_a=0, "minimum"_a=INT64_MIN, "maximum"_a=INT64_MAX); 374 | cls_IntegerNode.def("value", &IntegerNode::value); 375 | cls_IntegerNode.def("minimum", &IntegerNode::minimum); 376 | cls_IntegerNode.def("maximum", &IntegerNode::maximum); 377 | cls_IntegerNode.def(py::init(), "n"_a); 378 | cls_IntegerNode.def("isRoot", &IntegerNode::isRoot); 379 | cls_IntegerNode.def("parent", &IntegerNode::parent); 380 | cls_IntegerNode.def("pathName", &IntegerNode::pathName); 381 | cls_IntegerNode.def("elementName", &IntegerNode::elementName); 382 | cls_IntegerNode.def("destImageFile", &IntegerNode::destImageFile); 383 | cls_IntegerNode.def("isAttached", &IntegerNode::isAttached); 384 | cls_IntegerNode.def("checkInvariant", &IntegerNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 385 | cls_IntegerNode.def("__repr__", [](const IntegerNode &node) { 386 | return ""; 387 | }); 388 | 389 | py::class_ cls_ScaledIntegerNode(m, "ScaledIntegerNode"); 390 | cls_ScaledIntegerNode.def(py::init(), "destImageFile"_a, "value"_a, "minimum"_a, "maximum"_a, "scale"_a=1.0, "offset"_a=0.0); 391 | cls_ScaledIntegerNode.def(py::init(), "destImageFile"_a, "value"_a, "minimum"_a, "maximum"_a, "scale"_a=1.0, "offset"_a=0.0); 392 | cls_ScaledIntegerNode.def(py::init(), "destImageFile"_a, "value"_a, "minimum"_a, "maximum"_a, "scale"_a=1.0, "offset"_a=0.0); 393 | cls_ScaledIntegerNode.def(py::init(), "destImageFile"_a, "scaledValue"_a, "scaledMinimum"_a, "scaledMaximum"_a, "scale"_a=1.0, "offset"_a=0.0); 394 | cls_ScaledIntegerNode.def("rawValue", &ScaledIntegerNode::rawValue); 395 | cls_ScaledIntegerNode.def("scaledValue", &ScaledIntegerNode::scaledValue); 396 | cls_ScaledIntegerNode.def("minimum", &ScaledIntegerNode::minimum); 397 | cls_ScaledIntegerNode.def("scaledMinimum", &ScaledIntegerNode::scaledMinimum); 398 | cls_ScaledIntegerNode.def("maximum", &ScaledIntegerNode::maximum); 399 | cls_ScaledIntegerNode.def("scaledMaximum", &ScaledIntegerNode::scaledMaximum); 400 | cls_ScaledIntegerNode.def("scale", &ScaledIntegerNode::scale); 401 | cls_ScaledIntegerNode.def("offset", &ScaledIntegerNode::offset); 402 | cls_ScaledIntegerNode.def(py::init(), "n"_a); 403 | cls_ScaledIntegerNode.def("isRoot", &ScaledIntegerNode::isRoot); 404 | cls_ScaledIntegerNode.def("parent", &ScaledIntegerNode::parent); 405 | cls_ScaledIntegerNode.def("pathName", &ScaledIntegerNode::pathName); 406 | cls_ScaledIntegerNode.def("elementName", &ScaledIntegerNode::elementName); 407 | cls_ScaledIntegerNode.def("destImageFile", &ScaledIntegerNode::destImageFile); 408 | cls_ScaledIntegerNode.def("isAttached", &ScaledIntegerNode::isAttached); 409 | cls_ScaledIntegerNode.def("checkInvariant", &ScaledIntegerNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 410 | cls_ScaledIntegerNode.def("__repr__", [](const ScaledIntegerNode &node) { 411 | return ""; 412 | }); 413 | 414 | py::class_ cls_FloatNode(m, "FloatNode"); 415 | cls_FloatNode.def(py::init(), "destImageFile"_a, "value"_a=0.0, "precision"_a=E57_DOUBLE, "minimum"_a=-DBL_MAX, "maximum"_a=DBL_MAX); 416 | cls_FloatNode.def("value", &FloatNode::value); 417 | cls_FloatNode.def("precision", &FloatNode::precision); 418 | cls_FloatNode.def("minimum", &FloatNode::minimum); 419 | cls_FloatNode.def("maximum", &FloatNode::maximum); 420 | cls_FloatNode.def(py::init(), "n"_a); 421 | cls_FloatNode.def("isRoot", &FloatNode::isRoot); 422 | cls_FloatNode.def("parent", &FloatNode::parent); 423 | cls_FloatNode.def("pathName", &FloatNode::pathName); 424 | cls_FloatNode.def("elementName", &FloatNode::elementName); 425 | cls_FloatNode.def("destImageFile", &FloatNode::destImageFile); 426 | cls_FloatNode.def("isAttached", &FloatNode::isAttached); 427 | cls_FloatNode.def("checkInvariant", &FloatNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 428 | cls_FloatNode.def("__repr__", [](const FloatNode &node) { 429 | return ""; 430 | }); 431 | 432 | py::class_ cls_StringNode(m, "StringNode"); 433 | cls_StringNode.def(py::init(), "destImageFile"_a, "value"_a=""); 434 | cls_StringNode.def("value", &StringNode::value); 435 | cls_StringNode.def(py::init(), "n"_a); 436 | cls_StringNode.def("isRoot", &StringNode::isRoot); 437 | cls_StringNode.def("parent", &StringNode::parent); 438 | cls_StringNode.def("pathName", &StringNode::pathName); 439 | cls_StringNode.def("elementName", &StringNode::elementName); 440 | cls_StringNode.def("destImageFile", &StringNode::destImageFile); 441 | cls_StringNode.def("isAttached", &StringNode::isAttached); 442 | cls_StringNode.def("checkInvariant", &StringNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 443 | cls_StringNode.def("__repr__", [](const StringNode &node) { 444 | return ""; 445 | }); 446 | 447 | py::class_ cls_BlobNode(m, "BlobNode"); 448 | cls_BlobNode.def(py::init(), "destImageFile"_a, "byteCount"_a); 449 | cls_BlobNode.def("byteCount", &BlobNode::byteCount); 450 | cls_BlobNode.def("read", [](BlobNode& node, py::buffer buf, int64_t start, size_t count) { 451 | py::buffer_info info = buf.request(); 452 | 453 | if (info.ndim != 1) { 454 | throw std::runtime_error("Incompatible buffer dimension!"); 455 | } 456 | 457 | if (info.format != "B") { 458 | throw std::runtime_error("Incompatible buffer type!"); 459 | } 460 | 461 | if (static_cast(info.shape[0]) < count) { 462 | throw std::runtime_error("Buffer not large enough to read."); 463 | } 464 | 465 | node.read(reinterpret_cast(info.ptr), start, count); 466 | }); 467 | cls_BlobNode.def("write", [](BlobNode& node, py::buffer buf, int64_t start, size_t count) { 468 | py::buffer_info info = buf.request(); 469 | 470 | if (info.ndim != 1) { 471 | throw std::runtime_error("Incompatible buffer dimension!"); 472 | } 473 | 474 | if (info.format != "B") { 475 | throw std::runtime_error("Incompatible buffer type!"); 476 | } 477 | 478 | if (static_cast(info.shape[0]) < count) { 479 | throw std::runtime_error("Buffer not large enough to write."); 480 | } 481 | 482 | node.write(reinterpret_cast(info.ptr), start, count); 483 | }); 484 | cls_BlobNode.def(py::init(), "n"_a); 485 | cls_BlobNode.def("isRoot", &BlobNode::isRoot); 486 | cls_BlobNode.def("parent", &BlobNode::parent); 487 | cls_BlobNode.def("pathName", &BlobNode::pathName); 488 | cls_BlobNode.def("elementName", &BlobNode::elementName); 489 | cls_BlobNode.def("destImageFile", &BlobNode::destImageFile); 490 | cls_BlobNode.def("isAttached", &BlobNode::isAttached); 491 | cls_BlobNode.def("checkInvariant", &BlobNode::checkInvariant, "doRecurse"_a=true, "doUpcast"_a=true); 492 | cls_BlobNode.def("__repr__", [](const BlobNode &node) { 493 | return ""; 494 | }); 495 | cls_BlobNode.def("read_buffer", [](BlobNode &node) -> py::array { 496 | int64_t bufferSizeExpected = node.byteCount(); 497 | py::array_t arr(bufferSizeExpected); 498 | node.read(arr.mutable_data(), 0, bufferSizeExpected); 499 | return arr; 500 | }); 501 | 502 | py::class_ (m, "ImageFile") 503 | .def(py::init(), "fname"_a, "mode"_a, "checksumPolicy"_a=CHECKSUM_POLICY_ALL) 504 | .def("root", &ImageFile::root) 505 | .def("close", &ImageFile::close) 506 | .def("cancel", &ImageFile::cancel) 507 | .def("isOpen", &ImageFile::isOpen) 508 | .def("isWritable", &ImageFile::isWritable) 509 | .def("fileName", &ImageFile::fileName) 510 | .def("writerCount", &ImageFile::writerCount) 511 | .def("readerCount", &ImageFile::readerCount) 512 | .def("extensionsAdd", &ImageFile::extensionsAdd, "prefix"_a, "uri"_a) 513 | // I couldn't wrap the overloaded function so I call it directly 514 | .def("extensionsLookupPrefix", [](const ImageFile &im, std::string &prefix, std::string &uri) { 515 | return im.extensionsLookupPrefix(prefix, uri); 516 | }, "prefix"_a, "uri"_a) 517 | .def("extensionsLookupUri", &ImageFile::extensionsLookupUri, "uri"_a, "prefix"_a) 518 | .def("extensionsCount", &ImageFile::extensionsCount) 519 | .def("extensionsPrefix", &ImageFile::extensionsPrefix, "index"_a) 520 | .def("extensionsUri", &ImageFile::extensionsUri, "index"_a) 521 | .def("isElementNameExtended", &ImageFile::isElementNameExtended, "elementName"_a) 522 | .def("elementNameParse", &ImageFile::elementNameParse, "elementName"_a, "prefix"_a, "localPart"_a) 523 | .def("checkInvariant", &ImageFile::checkInvariant, "doRecurse"_a=true) 524 | .def("__repr__", [](const ImageFile &im) { 525 | return ""; 526 | }); 527 | 528 | // py::class_ cls_E57Exception(m, "E57Exception"); 529 | // cls_E57Exception.def("errorCode", &E57Exception::errorCode); 530 | // cls_E57Exception.def("context", &E57Exception::context); 531 | // cls_E57Exception.def("what", &E57Exception::what); 532 | // cls_E57Exception.def("sourceFileName", &E57Exception::sourceFileName); 533 | // cls_E57Exception.def("sourceFunctionName", &E57Exception::sourceFunctionName); 534 | // cls_E57Exception.def("sourceLineNumber", &E57Exception::sourceLineNumber); 535 | 536 | // py::class_ cls_E57Utilities(m, "E57Utilities"); 537 | // cls_E57Utilities.def(py::init(), "&"_a=""); 538 | // cls_E57Utilities.def("getVersions", &E57Utilities::getVersions, "astmMajor"_a, "astmMinor"_a, "libraryId"_a); 539 | // cls_E57Utilities.def("errorCodeToString", &E57Utilities::errorCodeToString, "ecode"_a); 540 | 541 | py::bind_vector>(m, "VectorSourceDestBuffer"); 542 | } 543 | -------------------------------------------------------------------------------- /src/pye57/scan_header.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pyquaternion import Quaternion 3 | 4 | from pye57 import libe57 5 | from pye57.utils import get_fields, get_node 6 | 7 | class ScanHeader: 8 | """Provides summary statistics for an individual lidar scan in an E57 file. 9 | 10 | Including the number of points, bounds and pose of the scan. 11 | """ 12 | def __init__(self, scan_node): 13 | self.node = scan_node 14 | points = self.node["points"] 15 | self.point_fields = get_fields(libe57.StructureNode(points.prototype())) 16 | self.scan_fields = get_fields(self.node) 17 | 18 | @classmethod 19 | def from_data3d(cls, data3d): 20 | return [cls(scan) for scan in data3d] 21 | 22 | def has_pose(self): 23 | return self.node.isDefined("pose") 24 | 25 | @property 26 | def point_count(self): 27 | return self.points.childCount() 28 | 29 | @property 30 | def rotation_matrix(self) -> np.array: 31 | q = Quaternion([e.value() for e in self.node["pose"]["rotation"]]) 32 | return q.rotation_matrix 33 | 34 | @property 35 | def rotation(self) -> np.array: 36 | try: 37 | rotation = self.node["pose"]["rotation"] 38 | q = Quaternion([e.value() for e in rotation]) 39 | except KeyError: 40 | q = Quaternion() 41 | return q.elements 42 | 43 | @property 44 | def translation(self): 45 | return np.array([e.value() for e in self.node["pose"]["translation"]]) 46 | 47 | def pretty_print(self, node=None, indent=""): 48 | if node is None: 49 | node = self.node 50 | lines = [] 51 | for field in get_fields(node): 52 | child_node = node[field] 53 | value = "" 54 | if hasattr(child_node, "value"): 55 | value = ": %s" % child_node.value() 56 | lines.append(indent + str(child_node) + value) 57 | if isinstance(child_node, libe57.StructureNode): 58 | lines += self.pretty_print(child_node, indent + " ") 59 | return lines 60 | 61 | def __getitem__(self, item): 62 | return self.node[item] 63 | 64 | def get_coordinate_system(self, COORDINATE_SYSTEMS): 65 | if all(x in self.point_fields for x in COORDINATE_SYSTEMS.CARTESIAN.value): 66 | coordinate_system = COORDINATE_SYSTEMS.CARTESIAN 67 | elif all(x in self.point_fields for x in COORDINATE_SYSTEMS.SPHERICAL.value): 68 | coordinate_system = COORDINATE_SYSTEMS.SPHERICAL 69 | else: 70 | raise Exception(f"Scans coordinate system not supported, unsupported point field {self.point_fields}") 71 | return coordinate_system 72 | 73 | @property 74 | def guid(self): 75 | return self["guid"].value() 76 | 77 | @property 78 | def temperature(self): 79 | return self["temperature"].value() 80 | 81 | @property 82 | def relativeHumidity(self): 83 | return self["relativeHumidity"].value() 84 | 85 | @property 86 | def atmosphericPressure(self): 87 | return self["atmosphericPressure"].value() 88 | 89 | @property 90 | def indexBounds(self): 91 | return self["indexBounds"] 92 | 93 | @property 94 | def rowMinimum(self): 95 | return self.indexBounds["rowMinimum"].value() 96 | 97 | @property 98 | def rowMaximum(self): 99 | return self.indexBounds["rowMaximum"].value() 100 | 101 | @property 102 | def columnMinimum(self): 103 | return self.indexBounds["columnMinimum"].value() 104 | 105 | @property 106 | def columnMaximum(self): 107 | return self.indexBounds["columnMaximum"].value() 108 | 109 | @property 110 | def returnMinimum(self): 111 | return self.indexBounds["returnMinimum"].value() 112 | 113 | @property 114 | def returnMaximum(self): 115 | return self.indexBounds["returnMaximum"].value() 116 | 117 | @property 118 | def intensityLimits(self): 119 | return self["intensityLimits"] 120 | 121 | @property 122 | def intensityMinimum(self): 123 | return self.intensityLimits["intensityMinimum"].value() 124 | 125 | @property 126 | def intensityMaximum(self): 127 | return self.intensityLimits["intensityMaximum"].value() 128 | 129 | @property 130 | def cartesianBounds(self): 131 | return self["cartesianBounds"] 132 | 133 | @property 134 | def xMinimum(self): 135 | return self.cartesianBounds["xMinimum"].value() 136 | 137 | @property 138 | def xMaximum(self): 139 | return self.cartesianBounds["xMaximum"].value() 140 | 141 | @property 142 | def yMinimum(self): 143 | return self.cartesianBounds["yMinimum"].value() 144 | 145 | @property 146 | def yMaximum(self): 147 | return self.cartesianBounds["yMaximum"].value() 148 | 149 | @property 150 | def zMinimum(self): 151 | return self.cartesianBounds["zMinimum"].value() 152 | 153 | @property 154 | def zMaximum(self): 155 | return self.cartesianBounds["zMaximum"].value() 156 | 157 | @property 158 | def sphericalBounds(self): 159 | return self["sphericalBounds"] 160 | 161 | @property 162 | def rangeMinimum(self): 163 | return self.sphericalBounds["rangeMinimum"].value() 164 | 165 | @property 166 | def rangeMaximum(self): 167 | return self.sphericalBounds["rangeMaximum"].value() 168 | 169 | @property 170 | def elevationMinimum(self): 171 | return self.sphericalBounds["elevationMinimum"].value() 172 | 173 | @property 174 | def elevationMaximum(self): 175 | return self.sphericalBounds["elevationMaximum"].value() 176 | 177 | @property 178 | def azimuthStart(self): 179 | return self.sphericalBounds["azimuthStart"].value() 180 | 181 | @property 182 | def azimuthEnd(self): 183 | return self.sphericalBounds["azimuthEnd"].value() 184 | 185 | @property 186 | def pose(self): 187 | return self["pose"] 188 | 189 | @property 190 | def acquisitionStart(self): 191 | return self["acquisitionStart"] 192 | 193 | @property 194 | def acquisitionStart_dateTimeValue(self): 195 | return self.acquisitionStart["dateTimeValue"].value() 196 | 197 | @property 198 | def acquisitionStart_isAtomicClockReferenced(self): 199 | return self.acquisitionStart["isAtomicClockReferenced"].value() 200 | 201 | @property 202 | def acquisitionEnd(self): 203 | return self["acquisitionEnd"] 204 | 205 | @property 206 | def acquisitionEnd_dateTimeValue(self): 207 | return self.acquisitionEnd["dateTimeValue"].value() 208 | 209 | @property 210 | def acquisitionEnd_isAtomicClockReferenced(self): 211 | return self.acquisitionEnd["isAtomicClockReferenced"].value() 212 | 213 | @property 214 | def pointGroupingSchemes(self): 215 | return self["pointGroupingSchemes"] 216 | 217 | @property 218 | def points(self): 219 | return self["points"] 220 | -------------------------------------------------------------------------------- /src/pye57/utils.py: -------------------------------------------------------------------------------- 1 | from pye57 import libe57 2 | from pye57.libe57 import NodeType 3 | 4 | import numpy as np 5 | 6 | def get_fields(node): 7 | return [node.get(id_).elementName() for id_ in range(node.childCount())] 8 | 9 | 10 | def get_node(node, name): 11 | cast = { 12 | NodeType.E57_BLOB: libe57.BlobNode, 13 | NodeType.E57_COMPRESSED_VECTOR: libe57.CompressedVectorNode, 14 | NodeType.E57_FLOAT: libe57.FloatNode, 15 | NodeType.E57_INTEGER: libe57.IntegerNode, 16 | NodeType.E57_SCALED_INTEGER: libe57.ScaledIntegerNode, 17 | NodeType.E57_STRING: libe57.StringNode, 18 | NodeType.E57_STRUCTURE: libe57.StructureNode, 19 | NodeType.E57_VECTOR: libe57.VectorNode 20 | } 21 | n = node.get(name) 22 | return cast[n.type()](n) 23 | 24 | def convert_spherical_to_cartesian(rae): 25 | """ 26 | Converts spherical(rae) to cartesian(xyz), where rae = range, azimuth(theta), 27 | elevation(phi). Where range is in meters and angles are in radians. 28 | 29 | Reference for formula: http://www.libe57.org/bestCoordinates.html (Note: the 30 | formula is different from the one online, so please use formula at the above reference) 31 | """ 32 | range_ = rae[:, :1] 33 | theta = rae[:, 1:2] 34 | phi = rae[:, 2:3] 35 | range_cos_phi = range_ * np.cos(phi) 36 | return np.concatenate(( 37 | range_cos_phi * np.cos(theta), 38 | range_cos_phi * np.sin(theta), 39 | range_ * np.sin(phi) 40 | ), axis=1) 41 | 42 | 43 | def copy_node(node, dest_image): 44 | compressed_node_pairs = [] 45 | blob_node_pairs = [] 46 | 47 | out_node = None 48 | # 'Element' Types 49 | if (isinstance(node, libe57.FloatNode)): 50 | out_node = libe57.FloatNode( 51 | dest_image, 52 | value=node.value(), 53 | precision=node.precision(), 54 | minimum=node.minimum(), 55 | maximum=node.maximum()) 56 | 57 | elif (isinstance(node, libe57.IntegerNode)): 58 | out_node = libe57.IntegerNode( 59 | dest_image, 60 | value=node.value(), 61 | minimum=node.minimum(), 62 | maximum=node.maximum()) 63 | 64 | elif (isinstance(node, libe57.ScaledIntegerNode)): 65 | out_node = libe57.ScaledIntegerNode( 66 | dest_image, 67 | node.rawValue(), 68 | minimum=node.minimum(), 69 | maximum=node.maximum(), 70 | scale=node.scale(), 71 | offset=node.offset()) 72 | 73 | elif (isinstance(node, libe57.StringNode)): 74 | out_node = libe57.StringNode( 75 | dest_image, 76 | node.value()) 77 | 78 | elif (isinstance(node, libe57.BlobNode)): 79 | out_node = libe57.BlobNode(dest_image, node.byteCount()) 80 | blob_node_pairs.append({ 'in': node, 'out': out_node }) 81 | 82 | # 'Container' Types 83 | elif (isinstance(node, libe57.CompressedVectorNode)): 84 | in_prototype = libe57.StructureNode(node.prototype()) 85 | out_prototype, _, _ = copy_node(in_prototype, dest_image) 86 | out_codecs, _, _ = copy_node(node.codecs(), dest_image) 87 | 88 | out_node = libe57.CompressedVectorNode(dest_image, out_prototype, out_codecs) 89 | 90 | compressed_node_pairs.append({ 91 | 'in': node, 92 | 'out': out_node 93 | }) 94 | 95 | elif isinstance(node, libe57.StructureNode): 96 | out_node = libe57.StructureNode(dest_image) 97 | for i in range(node.childCount()): 98 | in_child = get_node(node, i) 99 | in_child_name = in_child.elementName() 100 | out_child, out_child_compressed_node_pairs, out_child_blob_node_pairs = copy_node(in_child, dest_image) 101 | 102 | out_node.set(in_child_name, out_child) 103 | compressed_node_pairs.extend(out_child_compressed_node_pairs) 104 | blob_node_pairs.extend(out_child_blob_node_pairs) 105 | 106 | elif isinstance(node, libe57.VectorNode): 107 | out_node = libe57.VectorNode(dest_image, allowHeteroChildren=node.allowHeteroChildren()) 108 | for i in range(node.childCount()): 109 | in_child = get_node(node, i) 110 | in_child_name = f'{i}' 111 | out_child, out_child_compressed_node_pairs, out_child_blob_node_pairs = copy_node(in_child, dest_image) 112 | 113 | out_node.append(out_child) 114 | compressed_node_pairs.extend(out_child_compressed_node_pairs) 115 | blob_node_pairs.extend(out_child_blob_node_pairs) 116 | 117 | return out_node, compressed_node_pairs, blob_node_pairs -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pye57/f974b97697506ea38944cbe02121b4b17b7fe0e8/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_data/pumpAVisualReferenceImage.e57: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pye57/f974b97697506ea38944cbe02121b4b17b7fe0e8/tests/test_data/pumpAVisualReferenceImage.e57 -------------------------------------------------------------------------------- /tests/test_data/test.e57: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pye57/f974b97697506ea38944cbe02121b4b17b7fe0e8/tests/test_data/test.e57 -------------------------------------------------------------------------------- /tests/test_data/testSpherical.e57: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pye57/f974b97697506ea38944cbe02121b4b17b7fe0e8/tests/test_data/testSpherical.e57 -------------------------------------------------------------------------------- /tests/test_data/testWithNormals.e57: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidcaron/pye57/f974b97697506ea38944cbe02121b4b17b7fe0e8/tests/test_data/testWithNormals.e57 -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import time 4 | 5 | import numpy as np 6 | 7 | import pye57 8 | from pye57 import libe57 9 | from pye57.utils import get_node, copy_node 10 | 11 | try: 12 | from exceptions import WindowsError 13 | except ImportError: 14 | class WindowsError(OSError): 15 | pass 16 | 17 | 18 | def test_hi(): 19 | assert libe57.__doc__ 20 | 21 | 22 | def sample_data(*args): 23 | here = os.path.split(__file__)[0] 24 | return os.path.join(here, "test_data", *args) 25 | 26 | 27 | def delete_retry(path): 28 | try: 29 | if os.path.exists(path): 30 | os.remove(path) 31 | except WindowsError: 32 | time.sleep(0.1) 33 | if os.path.exists(path): 34 | os.remove(path) 35 | 36 | 37 | @pytest.fixture 38 | def e57_path(): 39 | return sample_data("test.e57") 40 | 41 | 42 | @pytest.fixture 43 | def e57_spherical_path(): 44 | return sample_data("testSpherical.e57") 45 | 46 | 47 | @pytest.fixture 48 | def e57_with_data_and_images_path(): 49 | # From http://www.libe57.org/data.html 50 | return sample_data("pumpAVisualReferenceImage.e57") 51 | 52 | @pytest.fixture 53 | def e57_with_normals_path(): 54 | # created using Python's open3d and CloudCompare 55 | return sample_data("testWithNormals.e57") 56 | 57 | @pytest.fixture 58 | def temp_e57_write(request): 59 | path = sample_data("test_write.e57") 60 | request.addfinalizer(lambda: delete_retry(path)) 61 | return path 62 | 63 | 64 | @pytest.fixture 65 | def image_and_points(e57_path): 66 | f = libe57.ImageFile(e57_path, mode="r") 67 | scan_0 = libe57.StructureNode(libe57.VectorNode(f.root().get("/data3D")).get(0)) 68 | points = libe57.CompressedVectorNode(scan_0.get("points")) 69 | return f, points 70 | 71 | 72 | def test_constants(): 73 | assert libe57.CHECKSUM_POLICY_NONE == 0 74 | assert libe57.CHECKSUM_POLICY_SPARSE == 25 75 | assert libe57.CHECKSUM_POLICY_HALF == 50 76 | assert libe57.CHECKSUM_POLICY_ALL == 100 77 | assert libe57.E57_INT8_MIN == -128 78 | assert libe57.E57_INT8_MAX == 127 79 | assert libe57.E57_INT16_MIN == -32768 80 | assert libe57.E57_INT16_MAX == 32767 81 | assert libe57.E57_INT32_MIN == -2147483647 - 1 82 | assert libe57.E57_INT32_MAX == 2147483647 83 | assert libe57.E57_INT64_MIN == -9223372036854775807 - 1 84 | assert libe57.E57_INT64_MAX == 9223372036854775807 85 | assert libe57.E57_UINT8_MIN == 0 86 | assert libe57.E57_UINT8_MAX == 255 87 | assert libe57.E57_UINT16_MIN == 0 88 | assert libe57.E57_UINT16_MAX == 65535 89 | assert libe57.E57_UINT32_MIN == 0 90 | assert libe57.E57_UINT32_MAX == 4294967295 91 | assert libe57.E57_UINT64_MIN == 0 92 | assert libe57.E57_UINT64_MAX == 18446744073709551615 93 | 94 | 95 | def test_open_imagefile(e57_path): 96 | f = libe57.ImageFile(e57_path, mode="r") 97 | assert f.isOpen() 98 | f.close() 99 | 100 | 101 | def test_open_imagefile_write(temp_e57_write): 102 | f = libe57.ImageFile(temp_e57_write, mode="w") 103 | assert f.isOpen() 104 | f.close() 105 | 106 | 107 | def test_e57_mode_error(temp_e57_write): 108 | with pytest.raises(ValueError): 109 | f = pye57.E57(temp_e57_write, mode="pasta") 110 | 111 | 112 | def test_get_structure_names(e57_path): 113 | f = libe57.ImageFile(e57_path, "r") 114 | root = f.root() 115 | names = [] 116 | for id_ in range(root.childCount()): 117 | names.append(root.get(id_).pathName()) 118 | assert names == ['/formatName', '/guid', '/versionMajor', '/versionMinor', '/e57LibraryVersion', 119 | '/coordinateMetadata', '/creationDateTime', '/data3D', '/images2D'] 120 | 121 | 122 | def test_get_data3d_nodes(e57_path): 123 | f = libe57.ImageFile(e57_path, "r") 124 | root = f.root() 125 | node = root.get("data3D") 126 | data3d = libe57.VectorNode(node) 127 | scan_count = data3d.childCount() 128 | assert scan_count == 4 129 | for scan_id in range(scan_count): 130 | assert isinstance(data3d.get(scan_id), libe57.Node) 131 | 132 | 133 | def test_get_read_data3d(e57_path): 134 | f = libe57.ImageFile(e57_path, "r") 135 | scan_0 = libe57.StructureNode(libe57.VectorNode(f.root().get("/data3D")).get(0)) 136 | points = libe57.CompressedVectorNode(scan_0.get("points")) 137 | assert points.childCount() == 281300 138 | 139 | 140 | def test_source_dest_buffers(e57_path): 141 | f = libe57.ImageFile(e57_path, "r") 142 | capacity = 1000 143 | types = list("bBhHlLq?fd") 144 | sizes = [1, 1, 2, 2, 4, 4, 8, 1, 4, 8] 145 | buffers = libe57.VectorSourceDestBuffer() 146 | for t in types: 147 | data = np.zeros(capacity, t) 148 | sdb = libe57.SourceDestBuffer(f, "something", data, capacity, True, True) 149 | buffers.append(sdb) 150 | 151 | for t, sdb, size, in zip(types, buffers, sizes): 152 | assert sdb.pathName() == "something" 153 | assert sdb.capacity() == capacity 154 | assert sdb.stride() == size 155 | assert sdb.doScaling() 156 | assert sdb.doConversion() 157 | 158 | 159 | def test_unsupported_point_field(temp_e57_write): 160 | with pye57.E57(temp_e57_write, mode="w") as f: 161 | with pytest.raises(ValueError): 162 | data = {"cartesianX": np.random.rand(10), 163 | "bananas": np.random.rand(10)} 164 | f.write_scan_raw(data) 165 | 166 | 167 | def test_ignore_unsupported_fields(e57_with_normals_path): 168 | e57 = pye57.E57(e57_with_normals_path) 169 | with pytest.raises(ValueError): 170 | e57.read_scan_raw(0) 171 | e57.read_scan_raw(0, ignore_unsupported_fields=True) 172 | 173 | 174 | def test_source_dest_buffers_raises(e57_path): 175 | f = libe57.ImageFile(e57_path, "r") 176 | capacity = 1000 177 | data = np.zeros(capacity, "i") 178 | with pytest.raises(ValueError): 179 | libe57.SourceDestBuffer(f, "something", data, capacity, True, True) 180 | 181 | 182 | def test_read_points_x(image_and_points): 183 | imf, points = image_and_points 184 | bufs = libe57.VectorSourceDestBuffer() 185 | capacity = 10000 186 | X = np.zeros(capacity, "f") 187 | bufs.append(libe57.SourceDestBuffer(imf, "cartesianX", X, capacity, True, True)) 188 | data_reader = points.reader(bufs) 189 | size = data_reader.read() 190 | assert size == capacity 191 | assert not np.all(np.zeros(capacity, "f") == X) 192 | 193 | 194 | def test_index_out_of_range(e57_path): 195 | f = libe57.ImageFile(e57_path, "r") 196 | with pytest.raises(IndexError): 197 | scan = f.root()["data3D"][-1] 198 | with pytest.raises(IndexError): 199 | scan = f.root()["data3D"][5] 200 | scan_0 = f.root()["data3D"][0] 201 | with pytest.raises(IndexError): 202 | r = scan_0["pose"]["rotation"][-1] 203 | with pytest.raises(IndexError): 204 | r = scan_0["pose"]["rotation"][5] 205 | 206 | 207 | def test_read_header(e57_path): 208 | f = libe57.ImageFile(e57_path, "r") 209 | data3d = f.root()["data3D"] 210 | headers = pye57.ScanHeader.from_data3d(data3d) 211 | fields = ['cartesianX', 'cartesianY', 'cartesianZ', 'intensity', 'rowIndex', 'columnIndex', 'cartesianInvalidState'] 212 | for header in headers: 213 | assert fields == header.point_fields 214 | assert headers[0].pretty_print() 215 | scan_0_rot = [[-0.4443, 0.8958, 0.], 216 | [-0.8958, -0.4443, 0.], 217 | [0., 0., 1.]] 218 | assert np.allclose(scan_0_rot, headers[0].rotation_matrix, atol=1e-3) 219 | scan_0_tra = [301336.23199, 5042597.23676, 15.46649] 220 | assert np.allclose(scan_0_tra, headers[0].translation) 221 | 222 | 223 | def test_read_xyz(e57_path): 224 | e57 = pye57.E57(e57_path) 225 | xyz = e57.read_scan(0) 226 | assert np.any(xyz) 227 | 228 | 229 | def test_read_header_spherical(e57_spherical_path): 230 | f = libe57.ImageFile(e57_spherical_path, "r") 231 | data3d = f.root()["data3D"] 232 | headers = pye57.ScanHeader.from_data3d(data3d) 233 | fields = ['sphericalRange', 'sphericalAzimuth', 'sphericalElevation', 'intensity', 'colorRed', 'colorGreen', 'colorBlue', 'sphericalInvalidState'] 234 | for header in headers: 235 | assert fields == header.point_fields 236 | assert headers[0].pretty_print() 237 | 238 | 239 | def test_read_xyz_spherical(e57_spherical_path): 240 | e57 = pye57.E57(e57_spherical_path) 241 | xyz = e57.read_scan(0) 242 | assert np.any(xyz) 243 | 244 | 245 | def test_read_raw(e57_path): 246 | e57 = pye57.E57(e57_path) 247 | header = e57.get_header(0) 248 | fields = header.point_fields 249 | data = e57.read_scan_raw(0) 250 | assert sorted(fields) == sorted(data.keys()) 251 | assert np.any(data["cartesianX"]) 252 | assert len(data["cartesianX"]) == header.point_count 253 | 254 | 255 | def test_read_write_single_scan(e57_path, temp_e57_write): 256 | e57 = pye57.E57(e57_path) 257 | header_source = e57.get_header(0) 258 | with pye57.E57(temp_e57_write, mode="w") as e57_write: 259 | raw_data_0 = e57.read_scan_raw(0) 260 | e57_write.write_scan_raw(raw_data_0, rotation=header_source.rotation, translation=header_source.translation) 261 | scan_0 = pye57.E57(e57_path).read_scan_raw(0) 262 | written = pye57.E57(temp_e57_write) 263 | header = written.get_header(0) 264 | assert np.allclose(header.rotation, header_source.rotation) 265 | assert np.allclose(header.translation, header_source.translation) 266 | scan_0_written = written.read_scan_raw(0) 267 | fields = "cartesianX cartesianY cartesianZ intensity rowIndex columnIndex cartesianInvalidState".split() 268 | for field in fields: 269 | assert np.allclose(scan_0[field], scan_0_written[field]) 270 | 271 | scan_0 = e57.read_scan(0) 272 | scan_0_written = written.read_scan(0) 273 | for field in scan_0: 274 | assert np.allclose(scan_0[field], scan_0_written[field]) 275 | 276 | 277 | def test_copy_file(e57_path, temp_e57_write): 278 | e57 = pye57.E57(e57_path) 279 | with pye57.E57(temp_e57_write, mode="w") as f: 280 | for scan_id in range(e57.scan_count): 281 | header = e57.get_header(scan_id) 282 | data = e57.read_scan_raw(scan_id) 283 | f.write_scan_raw(data, scan_header=header) 284 | header_written = f.get_header(scan_id) 285 | assert header_written.guid 286 | assert header_written.temperature == header_written.temperature 287 | assert header_written.relativeHumidity == header_written.relativeHumidity 288 | assert header_written.atmosphericPressure == header_written.atmosphericPressure 289 | assert header_written.rowMinimum == header.rowMinimum 290 | assert header_written.rowMaximum == header.rowMaximum 291 | assert header_written.columnMinimum == header.columnMinimum 292 | assert header_written.columnMaximum == header.columnMaximum 293 | assert header_written.returnMinimum == header.returnMinimum 294 | assert header_written.returnMaximum == header.returnMaximum 295 | assert header_written.intensityMinimum == header.intensityMinimum 296 | assert header_written.intensityMaximum == header.intensityMaximum 297 | assert header_written.xMinimum == header.xMinimum 298 | assert header_written.xMaximum == header.xMaximum 299 | assert header_written.yMinimum == header.yMinimum 300 | assert header_written.yMaximum == header.yMaximum 301 | assert header_written.zMinimum == header.zMinimum 302 | assert header_written.zMaximum == header.zMaximum 303 | assert np.allclose(header_written.rotation, header.rotation) 304 | assert np.allclose(header_written.translation, header.translation) 305 | assert header_written.acquisitionStart_dateTimeValue == header.acquisitionStart_dateTimeValue 306 | assert header_written.acquisitionStart_isAtomicClockReferenced == header.acquisitionStart_isAtomicClockReferenced 307 | assert header_written.acquisitionEnd_dateTimeValue == header.acquisitionEnd_dateTimeValue 308 | assert header_written.acquisitionEnd_isAtomicClockReferenced == header.acquisitionEnd_isAtomicClockReferenced 309 | # todo: point groups 310 | # header.pointGroupingSchemes["groupingByLine"]["idElementName"].value() 311 | # header.pointGroupingSchemes["groupingByLine"]["groups"] 312 | 313 | assert f.scan_count == e57.scan_count 314 | 315 | 316 | def test_read_color_absent(e57_path): 317 | e57 = pye57.E57(e57_path) 318 | with pytest.raises(ValueError): 319 | data = e57.read_scan(0, colors=True) 320 | 321 | 322 | def test_scan_position(e57_path): 323 | e57 = pye57.E57(e57_path) 324 | assert np.allclose(e57.scan_position(3), np.array([[3.01323456e+05, 5.04260184e+06, 1.56040279e+01]])) 325 | 326 | 327 | BUFFER_TYPES = { 328 | libe57.FloatNode: 'd', 329 | libe57.IntegerNode: 'l', 330 | libe57.ScaledIntegerNode: 'd' 331 | } 332 | 333 | 334 | def make_buffer(node: libe57.Node, capacity: int): 335 | node_type = type(node) 336 | if node_type not in BUFFER_TYPES: 337 | raise ValueError("Unsupported field type!") 338 | 339 | buffer_type = BUFFER_TYPES[node_type] 340 | 341 | np_array = np.empty(capacity, buffer_type) 342 | buffer = libe57.SourceDestBuffer( 343 | node.destImageFile(), node.elementName(), np_array, capacity, True, True) 344 | return np_array, buffer 345 | 346 | 347 | def make_buffers(node: libe57.StructureNode, capacity: int): 348 | data = {} 349 | buffers = libe57.VectorSourceDestBuffer() 350 | for i in range(node.childCount()): 351 | field = get_node(node, i) 352 | d, b = make_buffer(field, capacity) 353 | data[field.elementName()] = d 354 | buffers.append(b) 355 | return data, buffers 356 | 357 | 358 | def copy_compressed_vector_data(in_node: libe57.Node, out_node: libe57.Node): 359 | chunk_size = 100000 360 | 361 | in_prototype = libe57.StructureNode(in_node.prototype()) 362 | out_prototype = libe57.StructureNode(out_node.prototype()) 363 | 364 | in_data, in_buffers = make_buffers(in_prototype, chunk_size) 365 | out_data, out_buffers = make_buffers(out_prototype, chunk_size) 366 | 367 | in_reader = in_node.reader(in_buffers) 368 | out_writer = out_node.writer(out_buffers) 369 | 370 | n_points = in_node.childCount() 371 | current_index = 0 372 | while current_index != n_points: 373 | current_chunk = min(n_points - current_index, chunk_size) 374 | 375 | in_reader.read() 376 | for field in in_data: 377 | out_data[field][:current_chunk] = in_data[field][:current_chunk] 378 | 379 | out_writer.write(current_chunk) 380 | 381 | current_index += current_chunk 382 | 383 | in_reader.close() 384 | out_writer.close() 385 | 386 | 387 | def copy_blob_data(in_node, out_node): 388 | chunk_size = 100000 389 | 390 | byte_count = in_node.byteCount() 391 | blob_buffer = np.empty(chunk_size, np.ubyte) 392 | current_index = 0 393 | while current_index != byte_count: 394 | current_chunk = min(byte_count - current_index, chunk_size) 395 | 396 | in_node.read(blob_buffer, current_index, current_chunk) 397 | out_node.write(blob_buffer, current_index, current_chunk) 398 | 399 | current_index += current_chunk 400 | 401 | 402 | def test_clone_e57(e57_with_data_and_images_path, temp_e57_write): 403 | 404 | in_image = libe57.ImageFile(e57_with_data_and_images_path, "r") 405 | out_image = libe57.ImageFile(temp_e57_write, "w") 406 | 407 | for i in range(in_image.extensionsCount()): 408 | out_image.extensionsAdd( 409 | in_image.extensionsPrefix(i), 410 | in_image.extensionsUri(i) 411 | ) 412 | 413 | in_root = in_image.root() 414 | out_root = out_image.root() 415 | 416 | compressed_node_pairs = [] 417 | for i in range(in_root.childCount()): 418 | in_child = get_node(in_root, i) 419 | in_child_name = in_child.elementName() 420 | out_child, out_child_compressed_node_pairs, out_child_blob_node_pairs = copy_node(in_child, out_image) 421 | 422 | out_root.set(in_child_name, out_child) 423 | compressed_node_pairs.extend(out_child_compressed_node_pairs) 424 | 425 | for compressed_node_pair in compressed_node_pairs: 426 | copy_compressed_vector_data(compressed_node_pair['in'], compressed_node_pair['out']) 427 | 428 | for blob_node_pair in out_child_blob_node_pairs: 429 | copy_blob_data(blob_node_pair['in'], blob_node_pair['out']) 430 | 431 | in_image.close() 432 | out_image.close() 433 | 434 | 435 | def test_write_e57_with_rowindex_and_columnindex_omiting_low_values(temp_e57_write): 436 | 437 | with pye57.E57(temp_e57_write, mode='w') as e57: 438 | # set some test points with missing row and column 0 (so np.min of it in write_scan_raw is larger than 0) 439 | data_raw = {} 440 | data_raw["cartesianX"] = np.array([0, 1, 2, 3]).astype(float) 441 | data_raw["cartesianY"] = np.array([0, 1, 2, 3]).astype(float) 442 | data_raw["cartesianZ"] = np.array([0, 1, 2, 3]).astype(float) 443 | data_raw["rowIndex"] = np.array([1, 1, 2, 3]) 444 | data_raw["columnIndex"] = np.array([1, 1, 2, 3]) 445 | 446 | try: 447 | # the next line will throw without the suggested fix 448 | e57.write_scan_raw(data_raw, name='test_output_with_row_and_column') 449 | except pye57.libe57.E57Exception as ex: 450 | print(ex) 451 | assert False 452 | 453 | assert os.path.isfile(temp_e57_write) 454 | --------------------------------------------------------------------------------