181 lines
5.5 KiB
Python
181 lines
5.5 KiB
Python
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()
|