diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..db2491d --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,47 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-and-push: + permissions: write-all + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Scantools Docker image + run: | + DATE=$(date +%Y-%m-%d) + docker build . --tag ghcr.io/microsoft/lamar-benchmark/scantools:$DATE --target scantools + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + docker tag ghcr.io/microsoft/lamar-benchmark/scantools:$DATE \ + ghcr.io/microsoft/lamar-benchmark/scantools:latest + docker push ghcr.io/microsoft/lamar-benchmark/scantools:$DATE + docker push ghcr.io/microsoft/lamar-benchmark/scantools:latest + fi + + - name: Build and push Lamar Docker image + run: | + DATE=$(date +%Y-%m-%d) + docker build . --tag ghcr.io/microsoft/lamar-benchmark/lamar:$DATE --target lamar + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + docker tag ghcr.io/microsoft/lamar-benchmark/lamar:$DATE \ + ghcr.io/microsoft/lamar-benchmark/lamar:latest + docker push ghcr.io/microsoft/lamar-benchmark/lamar:$DATE + docker push ghcr.io/microsoft/lamar-benchmark/lamar:latest + fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3c72e87 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,176 @@ +ARG UBUNTU_VERSION=22.04 +FROM mcr.microsoft.com/mirror/docker/library/ubuntu:${UBUNTU_VERSION} AS common + +# Minimal toolings. +RUN apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + bash \ + git \ + python-is-python3 \ + python3-minimal \ + python3-pip \ + sudo \ + wget + +RUN python3 -m pip install --upgrade pip + +ADD . /lamar + +# +# Builder stage. +# +FROM common AS builder + +RUN apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + build-essential \ + cmake \ + libeigen3-dev \ + python3-dev \ + python3-setuptools + +# Build raybender. +COPY docker/scripts/build_raybender.sh /tmp/ +RUN bash /tmp/build_raybender.sh && rm /tmp/build_raybender.sh + +# Build pcdmeshing. +COPY docker/scripts/build_pcdmeshing.sh /tmp/ +RUN bash /tmp/build_pcdmeshing.sh && rm /tmp/build_pcdmeshing.sh + +# Build hloc. +COPY docker/scripts/build_hloc.sh /tmp/ +RUN bash /tmp/build_hloc.sh && rm /tmp/build_hloc.sh + +# +# Scantools stage. +# +FROM common AS scantools + +RUN apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + libgomp1 \ + libgl1 \ + libglib2.0-0 \ + libsm6 \ + libxrender1 \ + libxext6 \ + libzbar0 + +# Install raybender. +COPY --from=builder /raybender/embree-3.12.2/lib /raybender/embree-3.12.2/lib +COPY --from=builder /raybender/dist-wheel /tmp/dist-wheel +RUN cd /tmp && whl_path=$(cat dist-wheel/whl_path.txt) && python3 -m pip install $whl_path +RUN rm -rfv /tmp/* + +# Install pcdmeshing. +COPY --from=builder /pcdmeshing/dist-wheel /tmp/dist-wheel +RUN sudo apt-get install -y --no-install-recommends --no-install-suggests \ + libmpfrc++-dev +RUN cd /tmp && whl_path=$(cat dist-wheel/whl_path.txt) && python3 -m pip install $whl_path +RUN rm -rfv /tmp/* + +RUN python3 -m pip install --no-deps \ + astral==3.2 \ + beautifulsoup4==4.12.2 \ + lxml==4.9.2 \ + matplotlib \ + open3d==0.18.0 \ + opencv-python==4.7.0.72 \ + plyfile==1.0.3 \ + pytijo==0.0.2 \ + pyzbar-upright==0.1.8 \ + scipy==1.11.4 +RUN cd lamar && python3 -m pip install -e .[scantools] --no-deps +WORKDIR /lamar + +# +# pyceres-builder stage. +# +FROM mcr.microsoft.com/mirror/docker/library/ubuntu:${UBUNTU_VERSION} AS pyceres-builder + +# Prepare and empty machine for building. +RUN apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + git \ + cmake \ + ninja-build \ + build-essential \ + libeigen3-dev \ + libgoogle-glog-dev \ + libgflags-dev \ + libgtest-dev \ + libatlas-base-dev \ + libsuitesparse-dev \ + python-is-python3 \ + python3-minimal \ + python3-pip \ + python3-dev \ + python3-setuptools + +# Install Ceres. +RUN apt-get install -y --no-install-recommends --no-install-suggests wget && \ + wget "http://ceres-solver.org/ceres-solver-2.1.0.tar.gz" && \ + tar zxf ceres-solver-2.1.0.tar.gz && \ + mkdir ceres-build && \ + cd ceres-build && \ + cmake ../ceres-solver-2.1.0 -GNinja \ + -DCMAKE_INSTALL_PREFIX=/ceres_installed && \ + ninja install +RUN cp -r /ceres_installed/* /usr/local/ + +# Build pyceres. +RUN git clone --depth 1 --recursive https://github.com/cvg/pyceres +RUN python3 -m pip install --upgrade pip +RUN cd pyceres && \ + pip wheel . --no-deps -w dist-wheel -vv && \ + whl_path=$(find dist-wheel/ -name "*.whl") && \ + echo $whl_path >dist-wheel/whl_path.txt + +# +# pyceres stage. +# +FROM scantools as pyceres + +# Install minimal runtime dependencies. +RUN apt-get update && \ + apt-get install -y --no-install-recommends --no-install-suggests \ + libgoogle-glog0v5 \ + libspqr2 \ + libcxsparse3 \ + libatlas3-base \ + python-is-python3 \ + python3-minimal \ + python3-pip + +# Copy installed library in the builder stage. +COPY --from=pyceres-builder /ceres_installed/ /usr/local/ + +# Install pyceres. +COPY --from=pyceres-builder /pyceres/dist-wheel /tmp/dist-wheel +RUN pip install --upgrade pip +RUN cd /tmp && whl_path=$(cat dist-wheel/whl_path.txt) && pip install $whl_path +RUN rm -rfv /tmp/* + +# +# lamar stage. +# +FROM pyceres as lamar + +# Install hloc. +COPY --from=builder /hloc/dist-wheel /tmp/dist-wheel +RUN cd /tmp && whl_path=$(cat dist-wheel/whl_path.txt) && python3 -m pip install $whl_path +RUN rm -rfv /tmp/* + +# Note: The dependencies listed in pyproject.toml also include pyceres, already +# installed in previous Docker stages. Attempting to compile it in this stage +# will lead to failure due to missing necessary development dependencies. +# Therefore, we replicate the dependencies here, excluding pyceres +RUN python3 -m pip install --no-deps \ + h5py==3.10.0 \ + numpy==1.26.3 \ + torch>=1.1 \ + tqdm>=4.36.0 \ + pycolmap==0.6.0 + +RUN cd /lamar && python3 -m pip install -e . --no-deps +WORKDIR /lamar diff --git a/README.md b/README.md index 42b952f..f9c01f7 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,9 @@ T_w_i = sessions.trajectories[keys[0]] # first pose, from sensor/rig to world :one: Install the core dependencies: -- Python >= 3.8 -- [hloc](https://github.com/cvg/Hierarchical-Localization) and its dependencies, including [COLMAP](https://colmap.github.io/install.html) built from source +- Python >= 3.9 +- [hloc](https://github.com/cvg/Hierarchical-Localization) and its dependencies, including [COLMAP](https://colmap.github.io/install.html) built from source. +- [pyceres][https://github.com/cvg/pyceres.git] built from source. :two: Install the LaMAR libraries and pull the remaining pip dependencies: ```bash @@ -87,6 +88,35 @@ python -m pip install -e . python -m pip install -e .[dev] ``` +## Docker images + +The Dockerfile provided in this project has multiple stages, two of which are: +`scantools` and `lamar`. + +### Building the Docker Images + +You can build the Docker images for these stages using the following commands: +```bash +# Build the 'scantools' stage +docker build --target scantools -t lamar:scantools -f Dockerfile ./ + +# Build the 'lamar' stage +docker build --target lamar -t lamar:lamar -f Dockerfile ./ +``` + +### Pulling the Docker Images from GitHub Docker Registry + +Alternatively, if you don't want to build the images yourself, you can pull them +from the GitHub Docker Registry using the following commands: +```bash +# Pull the 'scantools' image +docker pull ghcr.io/microsoft/lamar-benchmark/scantools:latest + +# Pull the 'lamar' image +docker pull ghcr.io/microsoft/lamar-benchmark/lamar:latest +``` + + ## Benchmark :one: __Obtain the evaluation data:__ [visit the dataset page](https://lamar.ethz.ch/lamar/) and place the 3 scenes in `./data` : diff --git a/docker/scripts/build_hloc.sh b/docker/scripts/build_hloc.sh new file mode 100755 index 0000000..7c21172 --- /dev/null +++ b/docker/scripts/build_hloc.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +PS4='\033[1;96m$(date +%H:%M:%S)\033[0m ' +set -exo pipefail + +# Clone hloc. +git clone --recursive https://github.com/cvg/Hierarchical-Localization/ hloc --depth=1 +cd hloc + +# Build the wheel. +pip wheel --no-deps -w dist-wheel . +whl_path=$(find dist-wheel/ -name "*.whl") +echo $whl_path >dist-wheel/whl_path.txt diff --git a/docker/scripts/build_pcdmeshing.sh b/docker/scripts/build_pcdmeshing.sh new file mode 100755 index 0000000..c2de30d --- /dev/null +++ b/docker/scripts/build_pcdmeshing.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +PS4='\033[1;96m$(date +%H:%M:%S)\033[0m ' +set -exo pipefail + +sudo apt-get install -y --no-install-recommends --no-install-suggests \ + libboost-dev libgmp3-dev libmpfrc++-dev +git clone --recursive https://github.com/cvg/pcdmeshing.git --depth=1 +cd pcdmeshing + +# Build the wheel. +pip wheel --no-deps -w dist-wheel . +whl_path=$(find dist-wheel/ -name "*.whl") +echo $whl_path >dist-wheel/whl_path.txt diff --git a/docker/scripts/build_raybender.sh b/docker/scripts/build_raybender.sh new file mode 100755 index 0000000..53697d9 --- /dev/null +++ b/docker/scripts/build_raybender.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +PS4='\033[1;96m$(date +%H:%M:%S)\033[0m ' +set -exo pipefail + +# Clone raybender. +git clone --recursive https://github.com/cvg/raybender.git --depth=1 +cd raybender + +# Install Embree following the official instructions and set the environmental +# variable embree_DIR to point to embree-config.cmake. On Linux, this can be +# done as follows: +wget https://github.com/embree/embree/releases/download/v3.12.2/embree-3.12.2.x86_64.linux.tar.gz +tar xvzf embree-3.12.2.x86_64.linux.tar.gz +rm embree-3.12.2.x86_64.linux.tar.gz +mv embree-3.12.2.x86_64.linux embree-3.12.2 +export embree_DIR=`readlink -f embree-3.12.2/lib/cmake/embree-3.12.2` + +# Build the wheel. +pip wheel --no-deps -w dist-wheel . +whl_path=$(find dist-wheel/ -name "*.whl") +echo $whl_path >dist-wheel/whl_path.txt diff --git a/pipelines/pipeline_navvis_rig.py b/pipelines/pipeline_navvis_rig.py new file mode 100644 index 0000000..61c97bb --- /dev/null +++ b/pipelines/pipeline_navvis_rig.py @@ -0,0 +1,307 @@ +import argparse +import shutil +import tempfile +from pathlib import Path +from typing import List, Optional + +from scantools import ( + logger, + run_meshing, + run_navvis_to_capture, + run_rendering, + to_meshlab_visualization, +) +from scantools.capture import Capture +from scantools.run_qrcode_detection import run_qrcode_detection_session +from scantools.scanners.navvis.camera_tiles import TileFormat + +TILE_CHOICES = sorted([attr.name.split("_")[1] for attr in TileFormat]) + +description = """ +-------------------------------------------------------------------------------- + +Pipeline description +==================== + +Converts Navvis data to Lamar/Capture format exported as rig, including meshing, +depth maps rendering, and QR code detection. + +**Input Parameters** + + a) The input path for the data to be processed. Example: + + datasets_proc/ (--input_path "datasets_proc/") + ├── 2023-12-08_10.51.38 + ├── 2023-12-15_15.45.51 + └── 2023-12-15_15.58.10 + + b) Large camera images can be tiled into smaller also overlapping images. + The tile format specifies the tiling algorithm. The following values are + accepted: ["2x2","3x3","5x5","center","cross","none"]. Default: "3x3". + + c) The output path where processed data will be saved. Defaults to the + current working directory. Example: + + pipeline_output/ (--output_path "pipeline_output/") + └── datasets_proc/ + └── sessions + ├── 2023-12-08_10.51.38 + ├── 2023-12-15_15.45.51 + └── 2023-12-15_15.58.10 + ├── proc + │ ├── meshes + │ │ ├── mesh.ply + │ │ └── mesh_simplified.ply + │ └── qrcodes + │ ├── images_undistr + │ ├── qr_map_filtered_by_area.txt + │ └── qr_map.txt + ├── raw_data + │ ├── images_undistr_3x3 + │ ├── LUT + │ ├── render + │ └── pointcloud.ply + ├── bt.txt + ├── depths.txt + ├── images.txt + ├── pointclouds.txt + ├── rigs.txt + ├── sensors.txt + ├── trajectories.txt + └── wifi.txt + +1. **Mesh Generation** + + We create two meshes: + * full size (mesh.ply), and + * simplified one (mesh_simplified.ply), + from the provided point cloud. + +2. **Depth Map Generation** + + Generate depth maps using the previously computed mesh. This step, which can + be computationally intensive, calculates the distance from the camera sensor + to scene objects for each pixel. It can utilize either the default or + simplified mesh, influencing the level of detail and computational + complexity. We recommend to use the simplified mesh for large scene. + +3. **QR Code Detection** + + This step generates an additional output folder for processing QR codes in + full undistorted images, not their tiled versions. It uses the previously + computed mesh from the NavVis point cloud to determine the 3D position of + detected QR code corners via raycasting. Outputs are saved in TXT (default) + and optionally in JSON for readability. +""" + + +def run( + input_path: Path, + output_path: Optional[Path] = None, + sessions: Optional[List[str]] = None, + tiles_format: Optional[str] = "3x3", + meshing_method: str = "advancing_front", + meshing: bool = True, + rendering: bool = True, + qrcode_detection: bool = True, + use_simplified_mesh: bool = False, + visualization: bool = True, + **kargs, +): + capture_path = output_path or Path.cwd() / "lamar-capture-format" + + if capture_path.exists(): + capture = Capture.load(capture_path) + else: + capture_path.mkdir(exist_ok=True, parents=True) + capture = Capture(sessions={}, path=capture_path) + + mesh_id = "mesh" + if use_simplified_mesh: + mesh_id += "_simplified" + + # Run the rendring and QR codes dectection requires a mesh. + if rendering or qrcode_detection: + meshing = True + + # If `sessions` is not provided, run for all sessions in the `input_path`. + if sessions is None: + sessions = [p.name for p in input_path.iterdir() if p.is_dir()] + + for session in sessions: + navvis_path = input_path / session + if session not in capture.sessions: + logger.info("Exporting NavVis session %s.", session) + run_navvis_to_capture.run( + navvis_path, + capture, + tiles_format, + session, + export_as_rig=True, + copy_pointcloud=True, + ) + + if meshing and ( + not capture.sessions[session].proc + or mesh_id not in capture.sessions[session].proc.meshes + ): + logger.info("Meshing session %s.", session) + run_meshing.run( + capture, + session, + "point_cloud_final", + method=meshing_method, + ) + + if rendering and not capture.sessions[session].depths: + logger.info("Rendering session %s.", session) + run_rendering.run(capture, session, mesh_id=mesh_id) + + if qrcode_detection: + capture_qrcode_path = Path(tempfile.mkdtemp()) + logger.info( + "Create a temporal folder for the QR code detection output %s.", + capture_qrcode_path, + ) + capture_qrcode_path.mkdir(exist_ok=True, parents=True) + capture_qrcode = Capture(sessions={}, path=capture_qrcode_path) + + # Copy mesh from capture_path to capture_qrcode_path. + shutil.copytree( + str(capture.proc_path(session) / "meshes"), + str(capture_qrcode.proc_path(session) / "meshes"), + dirs_exist_ok=True, + ) + + # QR codes are captured intentionally by approaching them closely. + # As a result, we don't use tiling which could potentially split + # the QR code across multiple tiles. + tiles_format_qrcode = "none" + run_navvis_to_capture.run( + navvis_path, capture_qrcode, tiles_format_qrcode + ) + + # Reload capture_qrcode, so we have access to the copied meshes. + capture_qrcode = Capture.load(capture_qrcode_path) + + # Detect QR codes in the session. + run_qrcode_detection_session( + capture_qrcode, session, mesh_id, **kargs + ) + + # Copy QR code output from capture_qrcode to capture. + shutil.copytree( + str(capture_qrcode.proc_path(session) / "qrcodes"), + str(capture.proc_path(session) / "qrcodes"), + dirs_exist_ok=True, + ) + + if visualization: + to_meshlab_visualization.run( + capture, + session, + f"trajectory_{session}", + export_mesh=meshing, + export_poses=True, + mesh_id=mesh_id, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=description, formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "--input_path", + type=Path, + required=True, + help="Specifies NavVis data path. Inside this path, there should be " + "a folder for each session. Each session folder should be the output " + "of the NavVis data conversion tool (proc/ folders).", + ) + parser.add_argument( + "--output_path", + type=Path, + required=False, + default=None, + help="Output path. Default: current working directory. If the path " + "does not exist, it will be created.", + ) + parser.add_argument( + "--tiles_format", + type=str, + required=False, + default="3x3", + choices=TILE_CHOICES, + ) + parser.add_argument( + "--sessions", + nargs="*", + type=str, + required=False, + default=None, + help="[Optional] List of sessions to process. Useful when " + "processing only specific sessions.", + ) + parser.add_argument( + "--use_simplified_mesh", + action=argparse.BooleanOptionalAction, + required=False, + default=False, + help="Use simplified mesh. Default: False. Pass --use_simplified_mesh " + "to set to True. This is useful for large scenes.", + ) + parser.add_argument( + "--meshing_method", + type=str, + required=False, + default="advancing_front", + choices=["advancing_front", "poisson"], + help="Meshing method. Default: advancing_front.", + ) + parser.add_argument( + "--meshing", + action=argparse.BooleanOptionalAction, + required=False, + default=True, + help="Run meshing creation. Default: True. ", + ) + parser.add_argument( + "--rendering", + action=argparse.BooleanOptionalAction, + required=False, + default=True, + help="Run depth maps rendering. Default: True. ", + ) + parser.add_argument( + "--qrcode_detection", + action=argparse.BooleanOptionalAction, + required=False, + default=True, + help="Run QR code detection. Default: True. ", + ) + parser.add_argument( + "--visualization", + action=argparse.BooleanOptionalAction, + required=False, + default=True, + help="Write out MeshLab visualization. Default: True. " + "Pass --no-visualization to set to False.", + ) + parser.add_argument( + "--txt_format", + action=argparse.BooleanOptionalAction, + required=False, + default=True, + help="Write out QR maps in txt format. Default: True.", + ) + parser.add_argument( + "--json_format", + action=argparse.BooleanOptionalAction, + required=False, + default=False, + help="Write out QR maps in json format. Default: False.", + ) + args = parser.parse_args().__dict__ + + run(**args) diff --git a/pyproject.toml b/pyproject.toml index 513fa3c..8a1d39b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ description = "LaMAR: Benchmarking Localization and Mapping for Augmented Realit version = "1.0" authors = [{name = "Microsoft"}] readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {file = "LICENSE-CODE"} classifiers = [ "Programming Language :: Python :: 3", @@ -12,27 +12,27 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "numpy", - "scipy", - "h5py", + "h5py==3.10.0", + "numpy==1.26.3", "torch>=1.1", "tqdm>=4.36.0", - "opencv-python", - "pycolmap @ git+https://github.com/colmap/pycolmap.git@v0.4.0", + "pycolmap==0.6.0", "pyceres @ git+https://github.com/cvg/pyceres.git@v1.0", ] urls = {Repository = "https://github.com/microsoft/lamar-benchmark"} [project.optional-dependencies] scantools = [ - "matplotlib", - "lxml", - "beautifulsoup4==4.10.0", - "open3d", - "pytijo", - "plyfile", - "astral", - "pyzbar-upright", + "astral==3.2", + "beautifulsoup4==4.12.2", + "lxml==4.9.2", + "matplotlib==3.8.2", + "open3d==0.18.0", + "opencv-python==4.7.0.72", + "plyfile==1.0.3", + "pytijo==0.0.2", + "pyzbar-upright==0.1.8", + "scipy==1.11.4", ] dev = [ "pytest", diff --git a/scantools/capture/session.py b/scantools/capture/session.py index 63f3bbe..541d2f5 100644 --- a/scantools/capture/session.py +++ b/scantools/capture/session.py @@ -123,7 +123,7 @@ def get_pose(self, ts: int, sensor_id: str, poses: Optional[Trajectories] = None pose = T_rig2world * T_sensor2rig return pose - def save(self, path: Path): + def save(self, path: Path, overwrite : bool = True): path.mkdir(exist_ok=True, parents=True) for attr in fields(self): if attr.name == 'id': @@ -132,7 +132,7 @@ def save(self, path: Path): if data is None: continue filepath = path / self.filename(attr) - if filepath.exists() and attr.name != 'proc': + if not overwrite and filepath.exists() and attr.name != 'proc': raise IOError(f'File exists: {filepath}') data.save(filepath) self.id = path.name diff --git a/scantools/proc/qrcode/map.py b/scantools/proc/qrcode/map.py index a978054..88c7752 100644 --- a/scantools/proc/qrcode/map.py +++ b/scantools/proc/qrcode/map.py @@ -10,8 +10,8 @@ from scantools import logger from scantools.capture import Capture, Pose -from scantools.proc.rendering import Renderer, compute_rays from scantools.proc.qrcode.detector import QRCodeDetector +from scantools.proc.rendering import Renderer, compute_rays from scantools.utils.io import read_mesh @@ -224,6 +224,7 @@ def save_txt(qr_map: list[dict], path: Path): - path (str): The file path where the QR map will be saved. """ try: + logger.info(f"Saving qr_map to file: {path}") with open(path, "w", newline="") as f: writer = csv.writer(f) diff --git a/scantools/run_navvis_to_capture.py b/scantools/run_navvis_to_capture.py index 82b1940..a198e12 100644 --- a/scantools/run_navvis_to_capture.py +++ b/scantools/run_navvis_to_capture.py @@ -67,8 +67,6 @@ def run(input_path: Path, capture: Capture, tiles_format: str, session_id: Optio camera_ids = nv.get_camera_indexes() tiles = nv.get_tiles() - num_frames = len(frame_ids) - num_cameras = len(camera_ids) num_tiles = nv.get_num_tiles() K = nv.get_camera_intrinsics() @@ -194,7 +192,8 @@ def run(input_path: Path, capture: Capture, tiles_format: str, session_id: Optio if copy_pointcloud: shutil.copy(str(nv.get_pointcloud_path()), str(output_path)) else: - (output_path / pointcloud_filename).symlink_to(nv.get_pointcloud_path()) + if not (output_path / pointcloud_filename).exists(): + (output_path / pointcloud_filename).symlink_to(nv.get_pointcloud_path()) if __name__ == '__main__': diff --git a/scantools/run_qrcode_detection.py b/scantools/run_qrcode_detection.py index 2da615e..525a556 100644 --- a/scantools/run_qrcode_detection.py +++ b/scantools/run_qrcode_detection.py @@ -44,7 +44,7 @@ def run( else: capture = Capture(sessions={}, path=capture_path) - logger.info("Convert NavVis data to CAPTURE format: ", navvis_path) + logger.info("Convert NavVis data to CAPTURE format: %s", navvis_path) # Typically, QR codes are captured intentionally by approaching them # closely. As a result, we don't need tiling, which could potentially # split the QR code across multiple tiles.