chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/dataset
|
||||
@@ -0,0 +1,40 @@
|
||||
<!--[metadata]
|
||||
title = "Drone LiDAR"
|
||||
tags = ["3D", "drone", "Lidar"]
|
||||
description = "Display drone-based LiDAR data"
|
||||
-->
|
||||
|
||||
<picture>
|
||||
<img src="https://static.rerun.io/drone_lidar/95c49d78abc01513d344c06e2d9a0c8b84376a0d/full.png" alt="">
|
||||
<source media="(max-width: 480px)" srcset="https://static.rerun.io/drone_lidar/95c49d78abc01513d344c06e2d9a0c8b84376a0d/480w.png">
|
||||
<source media="(max-width: 768px)" srcset="https://static.rerun.io/drone_lidar/95c49d78abc01513d344c06e2d9a0c8b84376a0d/768w.png">
|
||||
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/drone_lidar/95c49d78abc01513d344c06e2d9a0c8b84376a0d/1024w.png">
|
||||
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/drone_lidar/95c49d78abc01513d344c06e2d9a0c8b84376a0d/1200w.png">
|
||||
</picture>
|
||||
|
||||
|
||||
## Background
|
||||
|
||||
This example displays drone-based indoor LiDAR data loaded from a [`.las`](https://en.wikipedia.org/wiki/LAS_file_format) file. This dataset contains 18.7M points, acquired at 4013 distinct time points (~4650 points per time point). The point data is loaded using the [laspy](https://laspy.readthedocs.io/en/latest/) Python package, and then sent in one go to the viewer thanks to the [`rr.send_columns()`](https://ref.rerun.io/docs/python/0.18.2/common/columnar_api/#rerun.send_columns) API and its `.partition()` helper. Together, these APIs enable associating subgroups of points with each of their corresponding, non-repeating timestamps.
|
||||
|
||||
|
||||
[Flyability](https://www.flyability.com) kindly provided the data for this example.
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
Install the example package:
|
||||
```bash
|
||||
pip install -e examples/python/drone_lidar
|
||||
```
|
||||
|
||||
To experiment with the provided example, simply execute the main Python script:
|
||||
```bash
|
||||
python -m drone_lidar
|
||||
```
|
||||
|
||||
If you wish to customize it, explore additional features, or save it, use the CLI with the `--help` option for guidance:
|
||||
|
||||
```bash
|
||||
python -m drone_lidar --help
|
||||
```
|
||||
@@ -0,0 +1,180 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import typing
|
||||
import zipfile
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
|
||||
import laspy
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
import rerun as rr
|
||||
import rerun.blueprint as rrb
|
||||
|
||||
DATASET_DIR = Path(__file__).parent / "dataset"
|
||||
if not DATASET_DIR.exists():
|
||||
DATASET_DIR.mkdir()
|
||||
|
||||
LIDAR_DATA_FILE = DATASET_DIR / "livemap.las"
|
||||
TRAJECTORY_DATA_FILE = DATASET_DIR / "livetraj.csv"
|
||||
|
||||
LIDAR_DATA_URL = "https://storage.googleapis.com/rerun-example-datasets/flyability/basement/livemap.las.zip"
|
||||
TRAJECTORY_DATA_URL = "https://storage.googleapis.com/rerun-example-datasets/flyability/basement/livetraj.csv"
|
||||
|
||||
|
||||
def download_with_progress(url: str, what: str) -> io.BytesIO:
|
||||
"""Download a file with a tqdm progress bar."""
|
||||
chunk_size = 1024 * 1024
|
||||
resp = requests.get(url, stream=True)
|
||||
total_size = int(resp.headers.get("content-length", 0))
|
||||
with tqdm(
|
||||
desc=f"Downloading {what}",
|
||||
total=total_size,
|
||||
unit="iB",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
) as progress:
|
||||
download_file = io.BytesIO()
|
||||
for data in resp.iter_content(chunk_size):
|
||||
download_file.write(data)
|
||||
progress.update(len(data))
|
||||
|
||||
download_file.seek(0)
|
||||
return download_file
|
||||
|
||||
|
||||
def unzip_file_from_archive_with_progress(zip_data: typing.BinaryIO, file_name: str, dest_dir: Path) -> None:
|
||||
"""Unzip the file named `file_name` from the zip archive contained in `zip_data` to `dest_dir`."""
|
||||
with zipfile.ZipFile(zip_data, "r") as zip_ref:
|
||||
file_info = zip_ref.getinfo(file_name)
|
||||
total_size = file_info.file_size
|
||||
|
||||
with (
|
||||
tqdm(
|
||||
total=total_size,
|
||||
desc=f"Extracting file {file_name}",
|
||||
unit="iB",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024,
|
||||
) as progress,
|
||||
zip_ref.open(file_name) as source,
|
||||
open(dest_dir / file_name, "wb") as target,
|
||||
):
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
target.write(chunk)
|
||||
progress.update(len(chunk))
|
||||
|
||||
|
||||
def download_dataset() -> None:
|
||||
if not LIDAR_DATA_FILE.exists():
|
||||
unzip_file_from_archive_with_progress(
|
||||
download_with_progress(LIDAR_DATA_URL, LIDAR_DATA_FILE.name),
|
||||
LIDAR_DATA_FILE.name,
|
||||
LIDAR_DATA_FILE.parent,
|
||||
)
|
||||
|
||||
if not TRAJECTORY_DATA_FILE.exists():
|
||||
TRAJECTORY_DATA_FILE.write_bytes(
|
||||
download_with_progress(TRAJECTORY_DATA_URL, TRAJECTORY_DATA_FILE.name).getvalue(),
|
||||
)
|
||||
|
||||
|
||||
# TODO(#7333): this utility should be included in the Rerun SDK
|
||||
def compute_partitions(
|
||||
times: npt.NDArray[np.float64],
|
||||
) -> tuple[typing.Sequence[float], typing.Sequence[np.uintp]]:
|
||||
"""
|
||||
Compute partitions given possibly repeating times.
|
||||
|
||||
This function returns two arrays:
|
||||
- Non-repeating times: a filtered version of `times` where repeated times are removed.
|
||||
- Partitions: an array of integers where each element indicates the number of elements for the corresponding time
|
||||
values in the original `times` array.
|
||||
|
||||
By construction, both arrays should have the same length, and the sum of all elements in `partitions` should be
|
||||
equal to the length of `times`.
|
||||
"""
|
||||
|
||||
change_indices = (np.argwhere(times != np.concatenate([times[1:], np.array([np.nan])])).T + 1).reshape(-1)
|
||||
partitions = np.concatenate([[change_indices[0]], np.diff(change_indices)])
|
||||
non_repeating_times = times[change_indices - 1]
|
||||
|
||||
assert np.sum(partitions) == len(times)
|
||||
assert len(non_repeating_times) == len(partitions)
|
||||
|
||||
return non_repeating_times, partitions # type: ignore[return-value]
|
||||
|
||||
|
||||
def log_lidar_data() -> None:
|
||||
las_data = laspy.read(LIDAR_DATA_FILE)
|
||||
|
||||
# get positions and convert to meters
|
||||
points = las_data.points
|
||||
positions = np.column_stack((points.X / 1000.0, points.Y / 1000.0, points.Z / 1000.0))
|
||||
times = las_data.gps_time
|
||||
|
||||
non_repeating_times, partitions = compute_partitions(times)
|
||||
|
||||
# log all positions at once using the computed partitions
|
||||
rr.send_columns(
|
||||
"/lidar",
|
||||
[rr.TimeColumn("time", duration=non_repeating_times)],
|
||||
rr.Points3D.columns(positions=positions).partition(partitions),
|
||||
)
|
||||
|
||||
rr.log(
|
||||
"/lidar",
|
||||
# negative radii are interpreted in UI units (instead of scene units)
|
||||
rr.Points3D.from_fields(colors=(128, 128, 255), radii=-0.1),
|
||||
static=True,
|
||||
)
|
||||
|
||||
|
||||
def log_drone_trajectory() -> None:
|
||||
data = np.genfromtxt(TRAJECTORY_DATA_FILE, delimiter=" ", skip_header=1)
|
||||
timestamp = data[:, 0]
|
||||
positions = data[:, 1:4]
|
||||
|
||||
rr.send_columns(
|
||||
"/drone",
|
||||
[rr.TimeColumn("time", duration=timestamp)],
|
||||
rr.Points3D.columns(positions=positions),
|
||||
)
|
||||
|
||||
rr.log(
|
||||
"/drone",
|
||||
rr.Points3D.from_fields(colors=(255, 0, 0), radii=0.5),
|
||||
static=True,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = ArgumentParser(description="Visualize drone-based LiDAR data")
|
||||
rr.script_add_args(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
download_dataset()
|
||||
|
||||
blueprint = rrb.Spatial3DView(
|
||||
origin="/",
|
||||
time_ranges=[
|
||||
rrb.VisibleTimeRange(
|
||||
timeline="time",
|
||||
start=rrb.TimeRangeBoundary.cursor_relative(seconds=-60.0),
|
||||
end=rrb.TimeRangeBoundary.cursor_relative(),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
rr.script_setup(args, "rerun_example_drone_lidar", default_blueprint=blueprint)
|
||||
|
||||
log_lidar_data()
|
||||
log_drone_trajectory()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "drone_lidar"
|
||||
version = "0.1.0"
|
||||
readme = "README.md"
|
||||
dependencies = ["laspy", "numpy", "requests", "rerun-sdk", "tqdm"]
|
||||
|
||||
[project.scripts]
|
||||
drone_lidar = "drone_lidar:main"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
Reference in New Issue
Block a user