Skip to content

Commit

Permalink
Add drone lidar example
Browse files Browse the repository at this point in the history
  • Loading branch information
abey79 committed Sep 2, 2024
1 parent 554ef56 commit 3fc0299
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/python/drone_lidar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/data
27 changes: 27 additions & 0 deletions examples/python/drone_lidar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!--[metadata]
title = "Drone LiDAR"
tags = ["3d", "drone", "lidar"]
description = "Display drone-based LiDAR data"
-->


Display drone LiDAR data kindly provided by [Flyability](https://www.flyability.com).


## Running

Install the example pacakge

Check warning on line 13 in examples/python/drone_lidar/README.md

View workflow job for this annotation

GitHub Actions / Checks / Spell Check

"pacakge" should be "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
```
171 changes: 171 additions & 0 deletions examples/python/drone_lidar/drone_lidar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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
import rerun as rr
from tqdm import tqdm

DATA_DIR = Path(__file__).parent / "dataset"
MAP_DATA_DIR = DATA_DIR / "map_data"
if not DATA_DIR.exists():
DATA_DIR.mkdir()

LIDAR_DATA_FILE = DATA_DIR / "livemap.las"
TRAJECTORY_DATA_FILE = DATA_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:
with 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.TimeSecondsColumn("time", non_repeating_times)],
[rr.components.Position3DBatch(positions).partition(partitions)],
)

rr.log_components(
"/lidar",
[
# TODO(#6889): indicator component no longer needed not needed when we have tagged components
rr.Points3D.indicator(),
rr.components.Radius(-0.2), # negative radii are interpreted in UI units (instead of scene units)
rr.components.Color((0, 0, 128)),
],
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.TimeSecondsColumn("time", timestamp)],
[rr.components.Position3DBatch(positions)],
)

rr.log_components(
"/drone",
[
# TODO(#6889): indicator component no longer needed not needed when we have tagged components
rr.Points3D.indicator(),
rr.components.Radius(0.5),
rr.components.Color([255, 0, 0]),
],
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.Horizontal(rrb.Spatial3DView(origin="/"), rrb.TimeSeriesView(origin="/aircraft"))
# rr.script_setup(args, "rerun_example_air_traffic_data", default_blueprint=blueprint)

rr.script_setup(args, "rerun_example_drone_lidar")

log_lidar_data()
log_drone_trajectory()


if __name__ == "__main__":
main()
19 changes: 19 additions & 0 deletions examples/python/drone_lidar/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[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"
27 changes: 27 additions & 0 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ controlnet = { path = "examples/python/controlnet", editable = true }
detect_and_track_objects = { path = "examples/python/detect_and_track_objects", editable = true }
dicom_mri = { path = "examples/python/dicom_mri", editable = true }
dna = { path = "examples/python/dna", editable = true }
drone_lidar = { path = "examples/python/drone_lidar", editable = true }
incremental_logging = { path = "examples/python/incremental_logging", editable = true }
lidar = { path = "examples/python/lidar", editable = true }
live_camera_edge_detection = { path = "examples/python/live_camera_edge_detection", editable = true }
Expand Down

0 comments on commit 3fc0299

Please sign in to comment.