[
  {
    "path": ".github/workflows/pre-commit.yml",
    "content": "name: Style Check\n\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  pre-commit:\n    name: Pre-commit checks\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: \"3.10\"\n      - name: set PY\n        run: echo \"PY=$(python -VV | sha256sum | cut -d' ' -f1)\" >> $GITHUB_ENV\n      - uses: actions/cache@v1\n        with:\n          path: ~/.cache/pre-commit\n          key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}\n      - uses: pre-commit/action@v3.0.0\n"
  },
  {
    "path": ".github/workflows/pypi.yml",
    "content": "name: Publish to PyPI.org\non:\n  release:\n    types: [published]\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  pypi:\n    if: github.event_name == 'release'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Build sdist\n        run: pipx run build --sdist\n\n      - name: Build wheel\n        run: pipx run build --wheel\n\n      - uses: pypa/gh-action-pypi-publish@release/v1\n        with:\n          password: ${{ secrets.PYPI_API_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/python.yml",
    "content": "name: Python API\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n\njobs:\n  python_package:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-22.04, ubuntu-20.04, windows-2022, macos-14]\n\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python3\n        uses: actions/setup-python@v4\n        with:\n          python-version: \"3.10\"\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n      - name: Build pip package\n        run: |\n          python -m pip install --verbose .\n      - name: Test installation\n        run: |\n          lidar_visualizer --version\n"
  },
  {
    "path": ".gitignore",
    "content": ".polyscope.ini\nimgui.ini\n\n# Created by https://www.toptal.com/developers/gitignore/api/python\n# Edit at https://www.toptal.com/developers/gitignore?templates=python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n### Python Patch ###\n# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration\npoetry.toml\n\n# ruff\n.ruff_cache/\n\n# LSP config files\npyrightconfig.json\n\n# End of https://www.toptal.com/developers/gitignore/api/python\nn\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n-   repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.4.0\n    hooks:\n    -   id: trailing-whitespace\n    -   id: end-of-file-fixer\n    -   id: check-yaml\n-   repo: https://github.com/psf/black\n    rev: 23.1.0\n    hooks:\n    -   id: black\n-   repo: https://github.com/pycqa/isort\n    rev: 5.12.0\n    hooks:\n     - id: isort\n"
  },
  {
    "path": "CITATION.cff",
    "content": "cff-version: 1.2.0\npreferred-citation:\n  title: \"KISS-ICP: In Defense of Point-to-Point ICP - Simple, Accurate, and Robust Registration If Done the Right Way\"\n  doi: \"10.1109/LRA.2023.3236571\"\n  year: \"2023\"\n  type: article\n  journal: \"IEEE Robotics and Automation Letters (RA-L)\"\n  url: https://www.ipb.uni-bonn.de/wp-content/papercite-data/pdf/vizzo2023ral.pdf\n  codeurl: https://github.com/PRBonn/kiss-icp\n  authors:\n    - family-names: Vizzo\n      given-names: Ignacio\n    - family-names: Guadagnino\n      given-names: Tiziano\n    - family-names: Mersch\n      given-names: Benedikt\n    - family-names: Wiesmann\n      given-names: Louis\n    - family-names: Behley\n      given-names: Jens\n    - family-names: Stachniss\n      given-names: Cyrill\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\nStachniss.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "install:\n\t@pip install --verbose .\n\nuninstall:\n\t@pip -v uninstall lidar_visualizer\n\nclean:\n\t@git clean -xff .\n\neditable:\n\t@pip install --verbose --editable .\n"
  },
  {
    "path": "README.md",
    "content": "# LiDAR Visualizer 🚀\n\nA flexible, easy-to-use, LiDAR (or any point cloud) visualizer for Linux, Windows, and macOS.\n\n![out](https://user-images.githubusercontent.com/21349875/234777083-eeb4ec57-cb50-4c69-babd-4cc8e63cff86.png)\n\nIf you also need to obtain poses from your dataset, consider checking out [KISS-ICP](https://github.com/PRBonn/kiss-icp).\n\n## Install (\\*)\n\n```sh\npip install lidar-visualizer\n```\n\n(\\*) This package relies on the power of [Open3D](https://www.open3d.org) but does not list it as a dependency. If you haven't installed `open3d` then `pip install open3d` or check [the official instructions](https://www.open3d.org/docs/release/getting_started.html)\n\n## Optional dependencies\n\nDepending on the [dataloaders](./src/lidar_visualizer/datasets/) you plan to use you might need to install optional dependencies. The tool will prompt which tools is the one you are requesting and is not accessible, but if you want to go for brute force and install all of it just run:\n\n```sh\npip install lidar-visualizer[all]\n```\n\n## Usage\n\n```sh\nlidar_visualizer --help\n```\n\n## Citation\n\nIf you use this visualizer for any academic work, please cite our original [paper](https://www.ipb.uni-bonn.de/wp-content/papercite-data/pdf/vizzo2023ral.pdf).\n\n```bibtex\n@article{vizzo2023ral,\n  author    = {Vizzo, Ignacio and Guadagnino, Tiziano and Mersch, Benedikt and Wiesmann, Louis and Behley, Jens and Stachniss, Cyrill},\n  title     = {{KISS-ICP: In Defense of Point-to-Point ICP -- Simple, Accurate, and Robust Registration If Done the Right Way}},\n  journal   = {IEEE Robotics and Automation Letters (RA-L)},\n  pages     = {1029--1036},\n  doi       = {10.1109/LRA.2023.3236571},\n  volume    = {8},\n  number    = {2},\n  year      = {2023},\n  codeurl   = {https://github.com/PRBonn/kiss-icp},\n}\n```\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"lidar_visualizer\"\ndescription = \"A LiDAR visualization tool for all your datasets\"\nversion = \"0.1.4\"\nreadme = \"README.md\"\nlicense = {file = \"LICENSE\"}\nkeywords = [\"LiDAR\", \"Robotics\", \"Visualization\"]\nauthors = [\n  {name = \"Ignacio Vizzo\", email = \"ignaciovizzo@gmail.com\"},\n]\n\nclassifiers  = [\n  \"Operating System :: Unix\",\n  \"Operating System :: MacOS\",\n  \"Operating System :: Microsoft :: Windows\",\n  \"Programming Language :: C++\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.7\",\n  \"Programming Language :: Python :: 3.8\",\n  \"Programming Language :: Python :: 3.9\",\n  \"Programming Language :: Python :: 3.10\",\n  \"Programming Language :: Python :: 3.11\",\n  \"Intended Audience :: Developers\",\n  \"Intended Audience :: Education\",\n  \"Intended Audience :: Other Audience\",\n  \"Intended Audience :: Science/Research\",\n  \"License :: OSI Approved :: MIT License\",\n]\n\n\nrequires-python = \">=3.7\"\ndependencies = [\n  \"natsort\",\n  \"numpy\",\n  \"tqdm\",\n  \"polyscope>=2.2.1\",\n  \"typer[all]>=0.6.0\",\n]\n\n[project.optional-dependencies]\nall = [\n  \"pyntcloud\",\n  \"trimesh\",\n  \"ouster-sdk\",\n]\n\n[project.urls]\nhomepage = \"https://github.com/PRBonn/lidar-visualizer\"\n\n[project.scripts]\nlidar_visualizer = \"lidar_visualizer.lidar_visualizer:main\"\n\n[tool.black]\nline-length = 100\n\n[tool.isort]\nprofile = \"black\"\n"
  },
  {
    "path": "src/lidar_visualizer/__init__.py",
    "content": "# MIT License\n#\n# Copyright (c) 2022 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n__version__ = \"0.1.4\"\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/__init__.py",
    "content": "# MIT License\n#\n# Copyright (c) 2022 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nfrom pathlib import Path\nfrom typing import Dict, List\n\n\ndef supported_file_extensions():\n    return [\n        \"bin\",\n        \"pcd\",\n        \"ply\",\n        \"xyz\",\n        \"obj\",\n        \"ctm\",\n        \"off\",\n        \"stl\",\n        \"e57\",\n    ]\n\n\ndef available_dataloaders() -> List:\n    import os.path\n    import pkgutil\n\n    pkgpath = os.path.dirname(__file__)\n    dataloaders = [name for _, name, _ in pkgutil.iter_modules([pkgpath])]\n    dataloaders.remove(\"point_cloud2\")\n    return dataloaders\n\n\ndef jumpable_dataloaders():\n    _jumpable_dataloaders = available_dataloaders()\n    _jumpable_dataloaders.remove(\"mcap\")\n    _jumpable_dataloaders.remove(\"ouster\")\n    _jumpable_dataloaders.remove(\"rosbag\")\n    return _jumpable_dataloaders\n\n\ndef dataloader_types() -> Dict:\n    import ast\n    import importlib\n\n    dataloaders = available_dataloaders()\n    _types = {}\n    for dataloader in dataloaders:\n        script = importlib.util.find_spec(f\".{dataloader}\", __name__).origin\n        with open(script) as f:\n            tree = ast.parse(f.read(), script)\n            classes = [cls for cls in tree.body if isinstance(cls, ast.ClassDef)]\n            _types[dataloader] = classes[0].name  # assuming there is only 1 class\n    return _types\n\n\ndef dataset_factory(dataloader: str, data_dir: Path, *args, **kwargs):\n    import importlib\n\n    dataloader_type = dataloader_types()[dataloader]\n    module = importlib.import_module(f\".{dataloader}\", __name__)\n    assert hasattr(module, dataloader_type), f\"{dataloader_type} is not defined in {module}\"\n    dataset = getattr(module, dataloader_type)\n    return dataset(data_dir=data_dir, *args, **kwargs)\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/generic.py",
    "content": "# MIT License\n#\n# Copyright (c) 2022 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport importlib\nimport os\nimport sys\nfrom pathlib import Path\n\nimport natsort\nimport numpy as np\n\nfrom lidar_visualizer.datasets import supported_file_extensions\n\n\nclass GenericDataset:\n    def __init__(self, data_dir: Path, *_, **__):\n        try:\n            self.o3d = importlib.import_module(\"open3d\")\n        except ModuleNotFoundError as e:\n            raise ModuleNotFoundError(\n                \"Open3D is not installed on your system, to fix this either \"\n                'run \"pip install open3d\" '\n                \"or check https://www.open3d.org/docs/release/getting_started.html\"\n            ) from e\n        # Intensity stuff\n        import matplotlib.cm as cm\n\n        self.cmap = cm.viridis\n\n        # Config stuff\n        self.sequence_id = os.path.basename(data_dir)\n        self.scans_dir = os.path.join(os.path.realpath(data_dir), \"\")\n        self.scan_files = np.array(\n            natsort.natsorted(\n                [\n                    os.path.join(self.scans_dir, fn)\n                    for fn in os.listdir(self.scans_dir)\n                    if any(fn.endswith(ext) for ext in supported_file_extensions())\n                ]\n            ),\n            dtype=str,\n        )\n        if len(self.scan_files) == 0:\n            raise ValueError(f\"Tried to read point cloud files in {self.scans_dir} but none found\")\n        self.file_extension = self.scan_files[0].split(\".\")[-1]\n        if self.file_extension not in supported_file_extensions():\n            raise ValueError(f\"Supported formats are: {supported_file_extensions()}\")\n\n        # Obtain the pointcloud reader for the given data folder\n        self._read_point_cloud = self._get_point_cloud_reader()\n\n    def __len__(self):\n        return len(self.scan_files)\n\n    def __getitem__(self, idx):\n        return self._read_point_cloud(self.scan_files[idx])\n\n    def _get_point_cloud_reader(self):\n        \"\"\"Attempt to guess with try/catch blocks which is the best point cloud reader to use for\n        the given dataset folder. Supported readers so far are:\n\n        File readers are functions which take a filename as an input and return a tuple of points and colors.\n            - np.fromfile\n            - pye57\n            - open3d\n            - trimesh.load\n            - PyntCloud\n        \"\"\"\n        # 1. The old KITTI format\n        if self.file_extension == \"bin\":\n            print(\"[WARNING] Reading .bin files, the only format supported is the KITTI format\")\n\n            def read_kitti_scan(file):\n                points_xyzi = (\n                    np.fromfile(file, dtype=np.float32).reshape((-1, 4)).astype(np.float64)\n                )\n                points = points_xyzi[:, 0:3]\n                intensity = points_xyzi[:, -1]\n                intensity = intensity / intensity.max()\n                colors = self.cmap(intensity)[:, :3].reshape(-1, 3)\n                return points, colors\n\n            return read_kitti_scan\n\n        first_scan_file = self.scan_files[0]\n        tried_libraries = []\n        missing_libraries = []\n\n        # 2 Try with pye57\n        if self.file_extension == \"e57\":\n            try:\n                import pye57\n\n                def read_e57_scan(file):\n                    e57 = pye57.E57(file)\n                    point_data = None\n                    color_data = None\n                    # One e57 file can contain multiple scans, scanned from different positions\n                    for i in range(e57.scan_count):\n                        i = e57.read_scan(i, colors=True, ignore_missing_fields=True)\n                        scan_data = np.stack(\n                            [i[\"cartesianX\"], i[\"cartesianY\"], i[\"cartesianZ\"]], axis=1\n                        )\n                        point_data = (\n                            np.concat([point_data, scan_data])\n                            if point_data is not None\n                            else scan_data\n                        )\n                        try:\n                            scan_color_data = np.stack(\n                                [i[\"colorRed\"], i[\"colorGreen\"], i[\"colorBlue\"]], axis=1\n                            )\n                            color_data = (\n                                np.concat([color_data, scan_color_data])\n                                if color_data is not None\n                                else scan_color_data\n                            )\n                        except KeyError:\n                            pass\n                    # e57 file colors are in 0-255 range\n                    color_data = color_data / 255.0 if color_data is not None else None\n                    return point_data, color_data\n\n                return read_e57_scan\n            except ModuleNotFoundError:\n                missing_libraries.append(\"pye57\")\n                print(\"[WARNING] pye57 not installed\")\n            except:\n                tried_libraries.append(\"pye57\")\n\n        # 3. Try with Open3D\n        try:\n            self.o3d.t.io.read_point_cloud(first_scan_file)\n\n            def read_scan_with_intensities(file):\n                scan = self.o3d.t.io.read_point_cloud(file)\n\n                if \"colors\" in dir(scan.point):\n                    scan = scan.to_legacy()\n                    return np.asarray(scan.points), np.asarray(scan.colors)\n\n                if \"intensity\" in dir(scan.point):\n                    intensity = scan.point.intensity.numpy()\n                    intensity = intensity / intensity.max()\n                    colors = self.cmap(intensity)[:, :, :3].reshape(-1, 3)\n                    return scan.point.positions.numpy(), colors\n\n                # else\n                scan = scan.to_legacy()\n                return np.asarray(scan.points), None\n\n            return read_scan_with_intensities\n        except ModuleNotFoundError:\n            missing_libraries.append(\"open3d\")\n        except:\n            tried_libraries.append(\"open3d\")\n\n        # 4. Try with trimesh\n        try:\n            import trimesh\n\n            trimesh.load(first_scan_file)\n            return lambda file: np.asarray(trimesh.load(file).vertices), None\n        except ModuleNotFoundError:\n            missing_libraries.append(\"trimesh\")\n        except:\n            tried_libraries.append(\"trimesh\")\n\n        # 5. Try with PyntCloud\n        try:\n            from pyntcloud import PyntCloud\n\n            PyntCloud.from_file(first_scan_file)\n            return lambda file: PyntCloud.from_file(file).points[[\"x\", \"y\", \"z\"]].to_numpy(), None\n        except ModuleNotFoundError:\n            missing_libraries.append(\"pyntcloud\")\n        except:\n            tried_libraries.append(\"pyntcloud\")\n\n        # If reach this point means that none of the libraries exist/could read the file\n        if not tried_libraries:\n            print(\n                \"No 3D library is installed in your system. Install one of the following \"\n                \"to read the pointclouds\"\n            )\n            print(\"\\n\".join(missing_libraries))\n        else:\n            print(\"[ERROR] File format not supported\")\n\n            print(\"Tried to load the point cloud with:\")\n            print(\"\\n\".join(tried_libraries))\n            print(\"Skipped libraries (not installed):\")\n            print(\"\\n\".join(missing_libraries))\n        sys.exit(1)\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/helipr.py",
    "content": "# MIT License\n#\n# Copyright (c) 2024 Saurabh Gupta, Ignacio Vizzo, Tiziano Guadagnino,\n# Benedikt Mersch, Cyrill Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport struct\nimport sys\nfrom pathlib import Path\n\nimport natsort\nimport numpy as np\n\nfrom lidar_visualizer.datasets import supported_file_extensions\n\n\nclass HeLiPRDataset:\n    def __init__(self, data_dir: Path, *_, **__):\n        # Intensity stuff\n        import matplotlib.cm as cm\n\n        self.cmap = cm.viridis\n\n        self.sequence_id = os.path.basename(data_dir)\n        self.scan_files = np.array(\n            natsort.natsorted(\n                [\n                    os.path.join(data_dir, fn)\n                    for fn in os.listdir(data_dir)\n                    if any(fn.endswith(ext) for ext in supported_file_extensions())\n                ]\n            ),\n            dtype=str,\n        )\n        if len(self.scan_files) == 0:\n            raise ValueError(f\"Tried to read point cloud files in {data_dir} but none found\")\n        self.file_extension = self.scan_files[0].split(\".\")[-1]\n        if self.file_extension not in supported_file_extensions():\n            raise ValueError(f\"Supported formats are: {supported_file_extensions()}\")\n\n        # Obtain the pointcloud reader for the given data folder\n        if self.sequence_id == \"Avia\":\n            self.format_string = \"fffBBBL\"\n            self.intensity_channel = None\n        elif self.sequence_id == \"Aeva\":\n            self.format_string = \"ffffflBf\"\n            self.format_string_no_intensity = \"ffffflB\"\n            self.intensity_channel = 7\n        elif self.sequence_id == \"Ouster\":\n            self.format_string = \"ffffIHHH\"\n            self.intensity_channel = 3\n        elif self.sequence_id == \"Velodyne\":\n            self.format_string = \"ffffHf\"\n            self.intensity_channel = 3\n        else:\n            print(\"[ERROR] Unsupported LiDAR Type\")\n            sys.exit()\n\n    def __len__(self):\n        return len(self.scan_files)\n\n    def __getitem__(self, idx):\n        return self.read_point_cloud(idx)\n\n    def get_data(self, idx: int):\n        file_path = self.scan_files[idx]\n        list_lines = []\n\n        # Special case, see https://github.com/minwoo0611/HeLiPR-File-Player/blob/e8d95e390454ece1415ae9deb51515f63730c10a/src/ROSThread.cpp#L632\n        if self.sequence_id == \"Aeva\" and int(Path(file_path).stem) <= 1691936557946849179:\n            self.intensity_channel = None\n            format_string = self.format_string_no_intensity\n        else:\n            format_string = self.format_string\n\n        chunk_size = struct.calcsize(f\"={format_string}\")\n        with open(file_path, \"rb\") as f:\n            binary = f.read()\n            offset = 0\n            while offset < len(binary):\n                list_lines.append(struct.unpack_from(f\"={format_string}\", binary, offset))\n                offset += chunk_size\n        data = np.stack(list_lines)\n        return data\n\n    def read_point_cloud(self, idx: int):\n        data = self.get_data(idx)\n        points = data[:, :3]\n        colors = None\n        if self.intensity_channel is not None:\n            intensity = data[:, self.intensity_channel]\n            intensity = (intensity - intensity.min()) / (intensity.max() - intensity.min())\n            colors = self.cmap(intensity)[:, :3].reshape(-1, 3)\n        return points, colors\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/mcap.py",
    "content": "# MIT License\n#\n# Copyright (c) 2023 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport sys\n\n\nclass McapDataloader:\n    def __init__(self, data_dir: str, topic: str, *_, **__):\n        \"\"\"Standalone .mcap dataloader withouth any ROS distribution.\"\"\"\n        # First try rosbags\n        from lidar_visualizer.datasets.point_cloud2 import read_point_cloud\n\n        # Then MCAP support\n        try:\n            from mcap.reader import make_reader\n            from mcap_ros2.reader import read_ros2_messages\n        except ModuleNotFoundError as e:\n            raise ModuleNotFoundError(\n                \"mcap plugins not installed: 'pip install mcap-ros2-support'\"\n            ) from e\n\n        # we expect `data_dir` param to be a path to the .mcap file, so rename for clarity\n        assert os.path.isfile(data_dir), \"mcap dataloader expects an existing MCAP file\"\n        self.sequence_id = os.path.basename(data_dir).split(\".\")[0]\n        mcap_file = str(data_dir)\n\n        self.bag = make_reader(open(mcap_file, \"rb\"))\n        self.summary = self.bag.get_summary()\n        self.topic = self.check_topic(topic)\n        self.n_scans = self._get_n_scans()\n        self.msgs = read_ros2_messages(mcap_file, topics=self.topic)\n        self.read_point_cloud = read_point_cloud\n        self.use_global_visualizer = True\n\n    def __del__(self):\n        if hasattr(self, \"bag\"):\n            del self.bag\n\n    def __getitem__(self, idx):\n        msg = next(self.msgs).ros_msg\n        return self.read_point_cloud(msg)\n\n    def __len__(self):\n        return self.n_scans\n\n    def _get_n_scans(self) -> int:\n        return sum(\n            count\n            for (id, count) in self.summary.statistics.channel_message_counts.items()\n            if self.summary.channels[id].topic == self.topic\n        )\n\n    def check_topic(self, topic: str) -> str:\n        # Extract schema id from the .mcap file that encodes the PointCloud2 msg\n        schema_id = [\n            schema.id\n            for schema in self.summary.schemas.values()\n            if schema.name == \"sensor_msgs/msg/PointCloud2\"\n        ][0]\n\n        point_cloud_topics = [\n            channel.topic\n            for channel in self.summary.channels.values()\n            if channel.schema_id == schema_id\n        ]\n\n        def print_available_topics_and_exit():\n            print(50 * \"-\")\n            for t in point_cloud_topics:\n                print(f\"--topic {t}\")\n            print(50 * \"-\")\n            sys.exit(1)\n\n        if topic and topic in point_cloud_topics:\n            return topic\n        # when user specified the topic check that exists\n        if topic and topic not in point_cloud_topics:\n            print(\n                f'[ERROR] Dataset does not containg any msg with the topic name \"{topic}\". '\n                \"Please select one of the following topics with the --topic flag\"\n            )\n            print_available_topics_and_exit()\n        if len(point_cloud_topics) > 1:\n            print(\n                \"Multiple sensor_msgs/msg/PointCloud2 topics available.\"\n                \"Please select one of the following topics with the --topic flag\"\n            )\n            print_available_topics_and_exit()\n\n        if len(point_cloud_topics) == 0:\n            print(\"[ERROR] Your dataset does not contain any sensor_msgs/msg/PointCloud2 topic\")\n        if len(point_cloud_topics) == 1:\n            return point_cloud_topics[0]\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/ouster.py",
    "content": "# MIT License\n#\n# Copyright (c) 2022 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n# Copyright (c) 2023 Pavlo Bashmakov\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport glob\nimport os\nfrom typing import Optional\n\n\ndef find_metadata_json(pcap_file: str) -> str:\n    \"\"\"Attempts to resolve the metadata json file for a provided pcap file.\"\"\"\n    dir_path, filename = os.path.split(pcap_file)\n    if not filename:\n        return \"\"\n    if not dir_path:\n        dir_path = os.getcwd()\n    json_candidates = sorted(glob.glob(f\"{dir_path}/*.json\"))\n    if not json_candidates:\n        return \"\"\n    prefix_sizes = list(\n        map(lambda p: len(os.path.commonprefix((filename, os.path.basename(p)))), json_candidates)\n    )\n    max_elem = max(range(len(prefix_sizes)), key=lambda i: prefix_sizes[i])\n    return json_candidates[max_elem]\n\n\nclass OusterDataloader:\n    \"\"\"Ouster pcap dataloader\"\"\"\n\n    def __init__(\n        self,\n        data_dir: str,\n        meta: Optional[str] = None,\n        *_,\n        **__,\n    ):\n        \"\"\"Create Ouster pcap dataloader to read scans from a pcap file.\n\n        Ouster pcap can be recorded with a `tcpdump` command or programmatically.\n        Pcap file should contain raw lidar_packets and `meta` file (i.e. metadata.json)\n        should be a corresponding sensor metadata stored at the time of pcap recording.\n\n\n        NOTE: It's critical to have a metadata json stored in the same recording session\n        as a pcap file, because pcap reader checks the `init_id` field in the UDP\n        lidar_packets and expects it to match `initialization_id`\n        in the metadata json, packets with different `init_id` just skipped.\n\n        Metadata json can be obtainer with Ouster SDK:\n        See examples here https://static.ouster.dev/sdk-docs/python/examples/basics-sensor.html#obtaining-sensor-metadata\n\n        or with Sensor HTTP API endpoint GET /api/v1/sensor/metadata directly:\n        See doc for details https://static.ouster.dev/sensor-docs/image_route1/image_route2/common_sections/API/http-api-v1.html#get-api-v1-sensor-metadata\n\n        Args:\n            data_dir: path to a pcap file (not a directory)\n            meta: path to a metadata json file that should be recorded together with\n            a pcap file. If `meta` is not provided attempts to find the best matching\n            json file with the longest commong prefix of the pcap file (`data_dir`) in\n            the same directory.\n        \"\"\"\n\n        try:\n            import ouster.pcap as pcap\n            from ouster import client\n            from ouster.client import _utils\n            from ouster.sdk.examples.colormaps import colorize\n        except ModuleNotFoundError as e:\n            raise ModuleNotFoundError(\n                f'ouster-sdk is not installed on your system, run \"pip install ouster-sdk\"'\n            ) from e\n\n        # since we import ouster-sdk's client module locally, we keep it locally as well\n        self._client = client\n        self._colorize = colorize\n        self._utils = _utils\n\n        assert os.path.isfile(data_dir), \"Ouster pcap dataloader expects an existing PCAP file\"\n\n        # we expect `data_dir` param to be a path to the .pcap file, so rename for clarity\n        pcap_file = data_dir\n\n        metadata_json = meta or find_metadata_json(pcap_file)\n        if not metadata_json:\n            print(\"Ouster pcap dataloader can't find metadata json file.\")\n            exit(1)\n        print(\"Ouster pcap dataloader: using metadata json: \", metadata_json)\n\n        self.data_dir = os.path.dirname(data_dir)\n\n        with open(metadata_json) as json:\n            self._info_json = json.read()\n            self._info = client.SensorInfo(self._info_json)\n\n        # lookup table for 2D range image projection to a 3D point cloud\n        self._xyz_lut = client.XYZLut(self._info)\n\n        self._pcap_file = str(data_dir)\n\n        # read pcap file for the first pass to count scans\n        print(\"Pre-reading Ouster pcap to count the scans number ...\")\n        self._source = pcap.Pcap(self._pcap_file, self._info)\n        self._scans_num = sum((1 for _ in client.Scans(self._source)))\n        print(f\"Ouster pcap total scans number:  {self._scans_num}\")\n\n        # start Scans iterator for consumption in __getitem__\n        self._source = pcap.Pcap(self._pcap_file, self._info)\n        self._scans_iter = iter(client.Scans(self._source))\n        self._next_idx = 0\n\n    def get_color_image(self, scan):\n        \"\"\"This function was taken from the Ouster SDK. All rights reserved to Ouster, Inc\n        https://github.com/ouster-lidar/ouster_example/blob/master/python/src/ouster/sdk/examples/open3d.py\n        \"\"\"\n        fields = list(scan.fields)\n        aes = {}\n        for field_ind, field in enumerate(fields):\n            if field in (self._client.ChanField.SIGNAL, self._client.ChanField.SIGNAL2):\n                aes[field_ind] = self._utils.AutoExposure(0.02, 0.1, 3)\n            else:\n                aes[field_ind] = self._utils.AutoExposure()\n        field_ind = 2\n\n        # Obtain reflectivity for colorizing the cloud\n        key = scan.field(fields[field_ind]).astype(float)\n        aes[field_ind](key)\n        return self._colorize(key)\n\n    def __getitem__(self, idx):\n        # we assume that users always reads sequentially and do not\n        # pass idx as for a random access collection\n        assert self._next_idx == idx, (\n            \"Ouster pcap dataloader supports only sequential reads. \"\n            f\"Expected idx: {self._next_idx}, but got {idx}\"\n        )\n        scan = next(self._scans_iter)\n        self._next_idx += 1\n\n        # filtering our zero returns makes it substantially faster for kiss-icp\n        sel_flag = scan.field(self._client.ChanField.RANGE) != 0\n\n        # Extract XYZ and Intensity channels form scan\n        xyz = self._xyz_lut(scan)[sel_flag]\n        ref = self.get_color_image(scan)[sel_flag]\n\n        points = xyz.reshape((-1, 3))\n        colors = ref.reshape((-1, 3))\n\n        return points, colors\n\n    def __len__(self):\n        return self._scans_num\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/point_cloud2.py",
    "content": "# Copyright 2008 Willow Garage, Inc.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are met:\n#\n#    * Redistributions of source code must retain the above copyright\n#      notice, this list of conditions and the following disclaimer.\n#\n#    * Redistributions in binary form must reproduce the above copyright\n#      notice, this list of conditions and the following disclaimer in the\n#      documentation and/or other materials provided with the distribution.\n#\n#    * Neither the name of the Willow Garage, Inc. nor the names of its\n#      contributors may be used to endorse or promote products derived from\n#      this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"\nThis file is based on https://github.com/ros2/common_interfaces/blob/4bac182a0a582b5e6b784d9fa9f0dabc1aca4d35/sensor_msgs_py/sensor_msgs_py/point_cloud2.py\nAll rights reserved to the original authors: Tim Field and Florian Vahl.\n\nThe current implementation is based on the one from the KISS-ICP project, but modified\n\"\"\"\n\nimport sys\nfrom typing import Iterable, List, Optional\n\nimport matplotlib.cm as cm\nimport numpy as np\n\ntry:\n    from rosbags.typesys.types import sensor_msgs__msg__PointCloud2 as PointCloud2\n    from rosbags.typesys.types import sensor_msgs__msg__PointField as PointField\nexcept ModuleNotFoundError as e:\n    raise ModuleNotFoundError('rosbags library not installed, run \"pip install -U rosbags\"') from e\n\n\n_DATATYPES = {}\n_DATATYPES[PointField.INT8] = np.dtype(np.int8)\n_DATATYPES[PointField.UINT8] = np.dtype(np.uint8)\n_DATATYPES[PointField.INT16] = np.dtype(np.int16)\n_DATATYPES[PointField.UINT16] = np.dtype(np.uint16)\n_DATATYPES[PointField.INT32] = np.dtype(np.int32)\n_DATATYPES[PointField.UINT32] = np.dtype(np.uint32)\n_DATATYPES[PointField.FLOAT32] = np.dtype(np.float32)\n_DATATYPES[PointField.FLOAT64] = np.dtype(np.float64)\n\nDUMMY_FIELD_PREFIX = \"unnamed_field\"\n\n\ndef read_point_cloud(msg: PointCloud2):\n    field_names = [\"x\", \"y\", \"z\"]\n    intensity_field = None\n    for field in msg.fields:\n        if field.name in [\"intensity\"]:\n            intensity_field = field.name\n            field_names.append(intensity_field)\n            break\n\n    points_structured = read_points(msg, field_names=field_names)\n    points = np.column_stack(\n        [\n            points_structured[\"x\"],\n            points_structured[\"y\"],\n            points_structured[\"z\"],\n        ]\n    ).astype(np.float64)\n    colors = None\n    if intensity_field:\n        intensity = points_structured[intensity_field].astype(np.float64)\n        intensity = intensity / intensity.max()\n        colors = cm.viridis(intensity)[:, :3].reshape(-1, 3)\n    return points, colors\n\n\ndef read_points(\n    cloud: PointCloud2,\n    field_names: Optional[List[str]] = None,\n    uvs: Optional[Iterable] = None,\n    reshape_organized_cloud: bool = False,\n) -> np.ndarray:\n    \"\"\"\n    Read points from a sensor_msgs.PointCloud2 message.\n    :param cloud: The point cloud to read from sensor_msgs.PointCloud2.\n    :param field_names: The names of fields to read. If None, read all fields.\n                        (Type: Iterable, Default: None)\n    :param uvs: If specified, then only return the points at the given\n        coordinates. (Type: Iterable, Default: None)\n    :param reshape_organized_cloud: Returns the array as an 2D organized point cloud if set.\n    :return: Structured NumPy array containing all points.\n    \"\"\"\n    # Cast bytes to numpy array\n    points = np.ndarray(\n        shape=(cloud.width * cloud.height,),\n        dtype=dtype_from_fields(cloud.fields, point_step=cloud.point_step),\n        buffer=cloud.data,\n    )\n\n    # Keep only the requested fields\n    if field_names is not None:\n        assert all(\n            field_name in points.dtype.names for field_name in field_names\n        ), \"Requests field is not in the fields of the PointCloud!\"\n        # Mask fields\n        points = points[list(field_names)]\n\n    # Swap array if byte order does not match\n    if bool(sys.byteorder != \"little\") != bool(cloud.is_bigendian):\n        points = points.byteswap(inplace=True)\n\n    # Select points indexed by the uvs field\n    if uvs is not None:\n        # Don't convert to numpy array if it is already one\n        if not isinstance(uvs, np.ndarray):\n            uvs = np.fromiter(uvs, int)\n        # Index requested points\n        points = points[uvs]\n\n    # Cast into 2d array if cloud is 'organized'\n    if reshape_organized_cloud and cloud.height > 1:\n        points = points.reshape(cloud.width, cloud.height)\n\n    return points\n\n\ndef dtype_from_fields(fields: Iterable[PointField], point_step: Optional[int] = None) -> np.dtype:\n    \"\"\"\n    Convert a Iterable of sensor_msgs.msg.PointField messages to a np.dtype.\n    :param fields: The point cloud fields.\n                   (Type: iterable of sensor_msgs.msg.PointField)\n    :param point_step: Point step size in bytes. Calculated from the given fields by default.\n                       (Type: optional of integer)\n    :returns: NumPy datatype\n    \"\"\"\n    # Create a lists containing the names, offsets and datatypes of all fields\n    field_names = []\n    field_offsets = []\n    field_datatypes = []\n    for i, field in enumerate(fields):\n        # Datatype as numpy datatype\n        datatype = _DATATYPES[field.datatype]\n        # Name field\n        if field.name == \"\":\n            name = f\"{DUMMY_FIELD_PREFIX}_{i}\"\n        else:\n            name = field.name\n        # Handle fields with count > 1 by creating subfields with a suffix consiting\n        # of \"_\" followed by the subfield counter [0 -> (count - 1)]\n        assert field.count > 0, \"Can't process fields with count = 0.\"\n        for a in range(field.count):\n            # Add suffix if we have multiple subfields\n            if field.count > 1:\n                subfield_name = f\"{name}_{a}\"\n            else:\n                subfield_name = name\n            assert subfield_name not in field_names, \"Duplicate field names are not allowed!\"\n            field_names.append(subfield_name)\n            # Create new offset that includes subfields\n            field_offsets.append(field.offset + a * datatype.itemsize)\n            field_datatypes.append(datatype.str)\n\n    # Create dtype\n    dtype_dict = {\"names\": field_names, \"formats\": field_datatypes, \"offsets\": field_offsets}\n    if point_step is not None:\n        dtype_dict[\"itemsize\"] = point_step\n    return np.dtype(dtype_dict)\n"
  },
  {
    "path": "src/lidar_visualizer/datasets/rosbag.py",
    "content": "# MIT License\n#\n# Copyright (c) 2022 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport os\nimport sys\nfrom pathlib import Path\nfrom typing import Sequence\n\nimport natsort\n\n\nclass RosbagDataset:\n    def __init__(self, data_dir: Sequence[Path], topic: str, *_, **__):\n        \"\"\"ROS1 / ROS2 bagfile dataloader.\n\n        It can take either one ROS2 bag file or one or more ROS1 bag files belonging to a split bag.\n        The reader will replay ROS1 split bags in correct timestamp order.\n\n        TODO: Merge mcap and rosbag dataloaders into 1\n        \"\"\"\n        try:\n            from rosbags.highlevel import AnyReader\n        except ModuleNotFoundError as e:\n            raise ModuleNotFoundError(\n                'rosbags library not installed, run \"pip install -U rosbags\"'\n            ) from e\n\n        from lidar_visualizer.datasets.point_cloud2 import read_point_cloud\n\n        self.read_point_cloud = read_point_cloud\n\n        # FIXME: This is quite hacky, trying to guess if we have multiple .bag, one or a dir\n        if isinstance(data_dir, Path):\n            self.sequence_id = os.path.basename(data_dir).split(\".\")[0]\n            self.bag = AnyReader([data_dir])\n        else:\n            self.sequence_id = os.path.basename(data_dir[0]).split(\".\")[0]\n            self.bag = AnyReader(data_dir)\n            print(\"Reading multiple .bag files in directory:\")\n            print(\"\\n\".join(natsort.natsorted([path.name for path in self.bag.paths])))\n        self.bag.open()\n        self.topic = self.check_topic(topic)\n        self.n_scans = self.bag.topics[self.topic].msgcount\n\n        # limit connections to selected topic\n        connections = [x for x in self.bag.connections if x.topic == self.topic]\n        self.msgs = self.bag.messages(connections=connections)\n        self.timestamps = []\n\n        # Visualization Options\n        self.use_global_visualizer = True\n\n    def __del__(self):\n        if hasattr(self, \"bag\"):\n            self.bag.close()\n\n    def __len__(self):\n        return self.n_scans\n\n    def __getitem__(self, idx):\n        connection, timestamp, rawdata = next(self.msgs)\n        self.timestamps.append(self.to_sec(timestamp))\n        msg = self.bag.deserialize(rawdata, connection.msgtype)\n        return self.read_point_cloud(msg)\n\n    @staticmethod\n    def to_sec(nsec: int):\n        return float(nsec) / 1e9\n\n    def get_frames_timestamps(self) -> list:\n        return self.timestamps\n\n    def check_topic(self, topic: str) -> str:\n        # Extract all PointCloud2 msg topics from the bagfile\n        point_cloud_topics = [\n            topic[0]\n            for topic in self.bag.topics.items()\n            if topic[1].msgtype == \"sensor_msgs/msg/PointCloud2\"\n        ]\n\n        def print_available_topics_and_exit():\n            print(50 * \"-\")\n            for t in point_cloud_topics:\n                print(f\"--topic {t}\")\n            print(50 * \"-\")\n            sys.exit(1)\n\n        if topic and topic in point_cloud_topics:\n            return topic\n        # when user specified the topic check that exists\n        if topic and topic not in point_cloud_topics:\n            print(\n                f'[ERROR] Dataset does not containg any msg with the topic name \"{topic}\". '\n                \"Please select one of the following topics with the --topic flag\"\n            )\n            print_available_topics_and_exit()\n        if len(point_cloud_topics) > 1:\n            print(\n                \"Multiple sensor_msgs/msg/PointCloud2 topics available.\"\n                \"Please select one of the following topics with the --topic flag\"\n            )\n            print_available_topics_and_exit()\n\n        if len(point_cloud_topics) == 0:\n            print(\"[ERROR] Your dataset does not contain any sensor_msgs/msg/PointCloud2 topic\")\n        if len(point_cloud_topics) == 1:\n            return point_cloud_topics[0]\n"
  },
  {
    "path": "src/lidar_visualizer/lidar_visualizer.py",
    "content": "# MIT License\n#\n# Copyright (c) 2023 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport glob\nimport os\nfrom pathlib import Path\nfrom typing import Optional\n\nimport typer\n\nfrom lidar_visualizer.datasets import (\n    available_dataloaders,\n    dataset_factory,\n    jumpable_dataloaders,\n    supported_file_extensions,\n)\nfrom lidar_visualizer.visualizer import Visualizer\n\n\ndef version_callback(value: bool):\n    if value:\n        import lidar_visualizer\n\n        print(f\"Lidar Visualizer Version: {lidar_visualizer.__version__}\")\n        raise typer.Exit(0)\n\n\ndef guess_dataloader(data: Path, default_dataloader: str):\n    if data.is_file():\n        if data.name == \"metadata.yaml\":\n            return \"rosbag\", data.parent  # database is in directory, not in .yml\n        if data.name.split(\".\")[-1] in \"bag\":\n            return \"rosbag\", data\n        if data.name.split(\".\")[-1] == \"pcap\":\n            return \"ouster\", data\n        if data.name.split(\".\")[-1] == \"mcap\":\n            return \"mcap\", data\n    elif data.is_dir():\n        if (data / \"metadata.yaml\").exists():\n            # a directory with a metadata.yaml must be a ROS2 bagfile\n            return \"rosbag\", data\n        bagfiles = [Path(path) for path in glob.glob(os.path.join(data, \"*.bag\"))]\n        if len(bagfiles) > 0:\n            return \"rosbag\", bagfiles\n    return default_dataloader, data\n\n\ndef name_callback(value: str):\n    if not value:\n        return value\n    dl = available_dataloaders()\n    if value not in dl:\n        raise typer.BadParameter(f\"Supported dataloaders are:\\n{', '.join(dl)}\")\n    return value\n\n\ndocstring = f\"\"\"\n:kiss: LiDAR visualizer :kiss:\\n\n\\b\n[bold green]Examples: [/bold green]\n# Visualize all pointclouds in the given <data-dir> \\[{\", \".join(supported_file_extensions())}]\n$ lidar_visualizer <data-dir>:open_file_folder:\n\n# Visualize a given [bold]ROS1/ROS2 [/bold]rosbag file (directory:open_file_folder:, \".bag\":page_facing_up:, or \"metadata.yaml\":page_facing_up:)\n$ lidar_visualizer <path-to-my-rosbag>[:open_file_folder:/:page_facing_up:]\n\n# Visualize [bold]mcap [/bold] recording\n$ lidar_visualizer <path-to-file.mcap>:page_facing_up:\n\n# Visualize [bold]Ouster pcap[/bold] recording (requires ouster-sdk Python package installed)\n$ lidar_visualizer <path-to-ouster.pcap>:page_facing_up: \\[--meta <path-to-metadata.json>:page_facing_up:]\n\"\"\"\napp = typer.Typer(add_completion=False, rich_markup_mode=\"rich\")\n\n\n@app.command(help=docstring)\ndef lidar_visualizer(\n    data: Path = typer.Argument(\n        ...,\n        help=\"The data directory used by the specified dataloader\",\n        show_default=False,\n    ),\n    dataloader: str = typer.Option(\n        None,\n        show_default=False,\n        case_sensitive=False,\n        autocompletion=available_dataloaders,\n        callback=name_callback,\n        help=\"[Optional] Use a specific dataloader from those supported by lidar-visualizer\",\n    ),\n    topic: Optional[str] = typer.Option(\n        None,\n        \"--topic\",\n        \"-t\",\n        show_default=False,\n        help=\"[Optional] Only valid when processing rosbag files\",\n        rich_help_panel=\"Additional Options\",\n    ),\n    n_scans: int = typer.Option(\n        -1,\n        \"--n-scans\",\n        \"-n\",\n        show_default=False,\n        help=\"[Optional] Specify the number of scans to process, default is the entire dataset\",\n        rich_help_panel=\"Additional Options\",\n    ),\n    jump: int = typer.Option(\n        0,\n        \"--jump\",\n        \"-j\",\n        show_default=False,\n        help=\"[Optional] Specify if you want to start to process scans from a given starting point\",\n        rich_help_panel=\"Additional Options\",\n    ),\n    meta: Optional[Path] = typer.Option(\n        None,\n        \"--meta\",\n        \"-m\",\n        exists=True,\n        show_default=False,\n        help=\"[Optional] For Ouster pcap dataloader, specify metadata json file path explicitly\",\n        rich_help_panel=\"Additional Options\",\n    ),\n    version: Optional[bool] = typer.Option(\n        None,\n        \"--version\",\n        help=\"Show the current version of lidar-visualizer\",\n        callback=version_callback,\n        is_eager=True,\n    ),\n):\n    if not dataloader:\n        dataloader, data = guess_dataloader(data, default_dataloader=\"generic\")\n\n    Visualizer(\n        dataset=dataset_factory(\n            dataloader=dataloader,\n            data_dir=data,\n            # Additional options\n            topic=topic,\n            meta=meta,\n        ),\n        random_accessible_dataset=dataloader in jumpable_dataloaders(),\n        n_scans=n_scans,\n        jump=jump,\n    ).run()\n\n\ndef main():\n    app()\n"
  },
  {
    "path": "src/lidar_visualizer/visualizer.py",
    "content": "# MIT License\n#\n# Copyright (c) 2022 Ignacio Vizzo, Tiziano Guadagnino, Benedikt Mersch, Cyrill\n# Stachniss.\n# Copyright (c) 2024  Luca Lobefaro\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\nimport importlib\nimport os\nimport time\n\n# Button names\nSTART_BUTTON = \"START [SPACE]\"\nPAUSE_BUTTON = \"PAUSE [SPACE]\"\nNEXT_FRAME_BUTTON = \"NEXT FRAME [N]\"\nPREVIOUS_FRAME_BUTTON = \"PREVIOUS FRAME [P]\"\nCENTER_VIEWPOINT_BUTTON = \"CENTER VIEWPOINT [C]\"\nQUIT_BUTTON = \"QUIT [Q]\"\n\n# Colors\nBACKGROUND_COLOR = [0.0, 0.0, 0.0]\nFRAME_COLOR = [0.8470, 0.1058, 0.3764]  # Only used if no color in original cloud\n\n# Size constants\nFRAME_PTS_SIZE_N_STEPS = 20\nFRAME_PTS_SIZE_MIN = 0.005\nFRAME_PTS_SIZE_MAX = 0.1\n\n\nclass Visualizer:\n    def __init__(self, dataset, random_accessible_dataset: bool, n_scans: int = -1, jump: int = 0):\n        try:\n            self._ps = importlib.import_module(\"polyscope\")\n            self._gui = self._ps.imgui\n        except ModuleNotFoundError:\n            print(f'polyscope is not installed on your system, run \"pip install polyscope\"')\n            exit(1)\n\n        # Initialize GUI controls\n        self._background_color = BACKGROUND_COLOR\n        self._frame_size_step = (FRAME_PTS_SIZE_MAX - FRAME_PTS_SIZE_MIN) / (\n            FRAME_PTS_SIZE_N_STEPS - 1\n        )\n        self._frame_size = 0.5 * FRAME_PTS_SIZE_N_STEPS * self._frame_size_step\n        self._play_mode = False\n        self._toggle_frame = True\n        self._playback_delay = 0.0\n\n        # Initialize dataset and fix input based on its nature\n        self._dataset = dataset\n        self._random_accessible_dataset = random_accessible_dataset\n        self.start_idx = min(jump, len(self._dataset) - 1) if self._random_accessible_dataset else 0\n        self.n_scans = len(self._dataset) if n_scans == -1 else min(len(self._dataset), n_scans)\n        self.stop_idx = min(len(self._dataset), self.n_scans + self.start_idx)\n        self.idx = self.start_idx\n        self.current_filename = self._get_current_filename(self.idx)\n        self.end_reached = False\n\n        # Initialize visualizer\n        self._initialize_visualizer()\n\n    def run(self):\n        while True:\n            self.update()\n            self.advance()\n\n    def update(self):\n        self._update_visualized_frame()\n        while True:\n            time.sleep(self._playback_delay)\n            self._ps.frame_tick()\n            if self._play_mode and not self.end_reached:\n                break\n\n    def advance(self):\n        self.idx = self.start_idx if self.idx == self.stop_idx - 1 else self.idx + 1\n        self.end_reached = self.idx == self.stop_idx - 1 and not self._random_accessible_dataset\n\n    def rewind(self):\n        self.idx = self.stop_idx - 1 if self.idx == self.start_idx else self.idx - 1\n\n    # Private Interface ---------------------------------------------------------------------------\n    def _initialize_visualizer(self):\n        self._ps.set_program_name(\"LIDAR Visualizer\")\n        self._ps.init()\n        self._ps.set_ground_plane_mode(\"none\")\n        self._ps.set_background_color(BACKGROUND_COLOR)\n        self._ps.set_verbosity(0)\n        self._ps.set_user_callback(self._main_gui_callback)\n        self._ps.set_build_default_gui_panels(False)\n\n    def _get_current_filename(self, idx):\n        # Try to fetch the current filename\n        try:\n            filename = self._dataset.scan_files[idx]\n            return os.path.splitext(os.path.basename(filename))[0]\n        except:\n            return None\n\n    def _get_frame(self, idx):\n        # Let's do a bit of duck typing to support eating different monsters\n        dataframe = self._dataset[idx]\n        points, colors = dataframe\n        return points, colors\n\n    def _update_visualized_frame(self):\n        self.current_filename = self._get_current_filename(self.idx)\n        points, colors = self._get_frame(self.idx)\n        self._register_frame(points, colors)\n\n    def _register_frame(self, points, colors):\n        frame_cloud = self._ps.register_point_cloud(\n            \"current_frame\",\n            points,\n            point_render_mode=\"quad\",\n        )\n        if colors is None:\n            frame_cloud.set_color(FRAME_COLOR)\n        else:\n            frame_cloud.add_color_quantity(\"colors\", colors, enabled=True)\n        frame_cloud.set_radius(self._frame_size, relative=False)\n        frame_cloud.set_enabled(self._toggle_frame)\n\n    # GUI Callbacks ---------------------------------------------------------------------------\n    def _main_gui_callback(self):\n        self._gui.TextUnformatted(\"Controls:\")\n        if not self.end_reached:\n            self._start_pause_callback()\n            if not self._play_mode:\n                self._gui.SameLine()\n                self._next_frame_callback()\n                if self._random_accessible_dataset:\n                    self._gui.SameLine()\n                    self._previous_frame_callback()\n        self._gui.Separator()\n        self._progress_bar_callback()\n        self._playback_delay_callback()\n        self._gui.Separator()\n        self._gui.TextUnformatted(\"Scene Options:\")\n        self._background_color_callback()\n        self._points_controlles_callback()\n        if not self._random_accessible_dataset:\n            self._gui.Separator()\n            self._information_callback()\n        self._gui.Separator()\n        self._center_viewpoint_callback()\n        self._gui.SameLine()\n        self._quit_callback()\n\n    def _start_pause_callback(self):\n        button_name = PAUSE_BUTTON if self._play_mode else START_BUTTON\n        if self._gui.Button(button_name) or self._gui.IsKeyPressed(self._gui.ImGuiKey_Space):\n            self._play_mode = not self._play_mode\n\n    def _next_frame_callback(self):\n        if self._gui.Button(NEXT_FRAME_BUTTON) or self._gui.IsKeyPressed(self._gui.ImGuiKey_N):\n            self.advance()\n            self._update_visualized_frame()\n\n    def _previous_frame_callback(self):\n        if self._gui.Button(PREVIOUS_FRAME_BUTTON) or self._gui.IsKeyPressed(self._gui.ImGuiKey_P):\n            self.rewind()\n            self._update_visualized_frame()\n\n    def _progress_bar_callback(self):\n        changed, idx = self._gui.SliderInt(\n            f\"\\t{self.stop_idx} Frames###Progress Bar\",\n            self.idx,\n            v_min=self.start_idx,\n            v_max=self.stop_idx - 1,\n            format=\"Frame: %d\",\n        )\n        if changed and self._random_accessible_dataset:\n            self.idx = idx\n            self._update_visualized_frame()\n\n    def _playback_delay_callback(self):\n        _, self._playback_delay = self._gui.SliderFloat(\n            \"\\tPlayback Delay\",\n            self._playback_delay,\n            v_min=0.0,\n            v_max=0.1,\n            format=\"%.2f s\",\n        )\n\n    def _points_controlles_callback(self):\n        key_changed = False\n        if self._gui.IsKeyPressed(self._gui.ImGuiKey_Minus):\n            self._frame_size = max(FRAME_PTS_SIZE_MIN, self._frame_size - self._frame_size_step)\n            key_changed = True\n        if self._gui.IsKeyPressed(self._gui.ImGuiKey_Equal):\n            self._frame_size = min(FRAME_PTS_SIZE_MAX, self._frame_size + self._frame_size_step)\n            key_changed = True\n        changed, self._frame_size = self._gui.SliderFloat(\n            \"Points Size\", self._frame_size, v_min=FRAME_PTS_SIZE_MIN, v_max=FRAME_PTS_SIZE_MAX\n        )\n        if changed or key_changed:\n            self._ps.get_point_cloud(\"current_frame\").set_radius(self._frame_size, relative=False)\n\n    def _background_color_callback(self):\n        changed, self._background_color = self._gui.ColorEdit3(\n            \"Background Color\",\n            self._background_color,\n        )\n        if changed:\n            self._ps.set_background_color(self._background_color)\n\n    def _information_callback(self):\n        self._gui.TextUnformatted(\n            f\"[WARNING] The current dataloader does not allow you to access frames\\nrandomly...\"\n        )\n\n    def _center_viewpoint_callback(self):\n        if self._gui.Button(CENTER_VIEWPOINT_BUTTON) or self._gui.IsKeyPressed(\n            self._gui.ImGuiKey_C\n        ):\n            self._ps.reset_camera_to_home_view()\n\n    def _quit_callback(self):\n        posX = (\n            self._gui.GetCursorPosX()\n            + self._gui.GetColumnWidth()\n            - self._gui.CalcTextSize(QUIT_BUTTON)[0]\n            - self._gui.GetScrollX()\n            - self._gui.ImGuiStyleVar_ItemSpacing\n        )\n        self._gui.SetCursorPosX(posX)\n        if (\n            self._gui.Button(QUIT_BUTTON)\n            or self._gui.IsKeyPressed(self._gui.ImGuiKey_Escape)\n            or self._gui.IsKeyPressed(self._gui.ImGuiKey_Q)\n        ):\n            print(\"Destroying Visualizer\")\n            self._ps.unshow()\n            os._exit(0)\n"
  }
]