Skip to content

Commit 3c627f0

Browse files
authored
Add HeLiMOS (#25)
* Add helimos dataloader * Update readme * Update readme * Update readme * Take fix from 4DMOS * Switch to polyscope * Fix name * Fix voxelization * Add belief visualization * Revert "Fix voxelization" This reverts commit 3b2198f. * Fix wrong voxel size * Fix voxelization * Fix voxelization * Fix transformations in visualizer * Readme * Add link to paper * Not needed
1 parent 3713ce4 commit 3c627f0

File tree

15 files changed

+240
-40
lines changed

15 files changed

+240
-40
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ mapmos_pipeline --visualize /path/to/weights.ckpt /path/to/data
7171
<details>
7272
<summary>Want to evaluate with ground truth labels?</summary>
7373

74-
Because these lables come in all shapes, you need to specify a dataloader. This is currently available for SemanticKITTI and NuScenes as well as our post-processed KITTI Tracking sequence 19 and Apollo sequences (see [Downloads](#downloads)).
74+
Because these labels come in all shapes, you need to specify a dataloader. This is currently available for SemanticKITTI, NuScenes, HeLiMOS, and our labeled KITTI Tracking sequence 19 and Apollo sequences (see [Downloads](#downloads)).
7575

7676
</details>
7777

@@ -108,6 +108,21 @@ The training log and checkpoints will be saved by default to the current working
108108

109109
</details>
110110

111+
## HeLiMOS
112+
We provide additional training and evaluation data for different sensor types in our [HeLiMOS paper](https://www.ipb.uni-bonn.de/pdfs/lim2024iros.pdf). To train on the HeLiMOS data, use the following commands:
113+
114+
```shell
115+
python3 scripts/precache.py /path/to/HeLiMOS helimos /path/to/cache --config config/helimos/*_training.yaml
116+
python3 scripts/train.py /path/to/HeLiMOS helimos /path/to/cache --config config/helimos/*_training.yaml
117+
```
118+
119+
by replacing the paths and the config file names. To evaluate for example on the Velodyne test data, run
120+
121+
```shell
122+
mapmos_pipeline /path/to/weights.ckpt /path/to/HeLiMOS --dataloader helimos -s Velodyne/test.txt
123+
```
124+
125+
Note that our sequence `-s` encodes both the sensor type `Velodyne` and split `test.txt`, just replace these with `Ouster`, `Aeva`, or `Avia` and/or `train.txt` or `val.txt` to run MapMOS on different sensors and/or splits.
111126

112127
## Downloads
113128
You can download the post-processed and labeled [Apollo dataset](https://www.ipb.uni-bonn.de/html/projects/apollo_dataset/LiDAR-MOS.zip) and [KITTI Tracking sequence 19](https://www.ipb.uni-bonn.de/html/projects/kitti-tracking/post-processed/kitti-tracking.zip) from our website.

config/example.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ odometry:
1313

1414
mos:
1515
voxel_size_mos: 0.1
16+
delay_mos: 10
1617
max_range_mos: 50.0
1718
min_range_mos: 0.0
1819
voxel_size_belief: 0.25
1920
max_range_belief: 150
20-
delay_belief: 10
2121

2222
training:
2323
id: "experiment_id"

config/helimos/all_training.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
training:
2+
id: "helimos_all"
3+
train:
4+
- "Avia/train.txt"
5+
- "Aeva/train.txt"
6+
- "Velodyne/train.txt"
7+
- "Ouster/train.txt"
8+
val:
9+
- "Avia/val.txt"
10+
- "Aeva/val.txt"
11+
- "Velodyne/val.txt"
12+
- "Ouster/val.txt"

config/helimos/inference.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mos:
2+
max_range_mos: -1.0
3+
max_range_belief: 50

config/helimos/omni_training.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
training:
2+
id: "helimos_omni"
3+
train:
4+
- "Velodyne/train.txt"
5+
- "Ouster/train.txt"
6+
val:
7+
- "Velodyne/val.txt"
8+
- "Ouster/val.txt"

config/helimos/solid_training.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
training:
2+
id: "helimos_solid"
3+
train:
4+
- "Avia/train.txt"
5+
- "Aeva/train.txt"
6+
val:
7+
- "Avia/val.txt"
8+
- "Aeva/val.txt"

scripts/cache_to_ply.py

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def cache_to_ply(
8282
dataloader=dataloader,
8383
data_dir=data,
8484
config=cfg,
85-
sequences=sequence,
85+
sequences=[sequence],
8686
cache_dir=cache_dir,
8787
),
8888
batch_size=1,
@@ -103,36 +103,37 @@ def cache_to_ply(
103103
for idx, batch in enumerate(
104104
tqdm(data_iterable, desc="Writing data to ply", unit=" items", dynamic_ncols=True)
105105
):
106-
mask_scan = batch[:, 4] == idx
107-
scan_points = batch[mask_scan, 1:4]
108-
scan_labels = batch[mask_scan, 6]
109-
110-
map_points = batch[~mask_scan, 1:4]
111-
map_timestamps = batch[~mask_scan, 5]
112-
map_labels = batch[~mask_scan, 6]
113-
114-
min_time = torch.min(batch[:, 5])
115-
max_time = torch.max(batch[:, 5])
116-
117-
pcd_scan = o3d.geometry.PointCloud(
118-
o3d.utility.Vector3dVector(scan_points.numpy())
119-
).paint_uniform_color([0, 0, 1])
120-
scan_colors = np.array(pcd_scan.colors)
121-
scan_colors[scan_labels == 1] = [1, 0, 0]
122-
pcd_scan.colors = o3d.utility.Vector3dVector(scan_colors)
123-
124-
pcd_map = o3d.geometry.PointCloud(
125-
o3d.utility.Vector3dVector(map_points.numpy())
126-
).paint_uniform_color([0, 0, 0])
127-
map_colors = np.array(pcd_map.colors)
128-
map_timestamps_norm = (map_timestamps - min_time) / (max_time - min_time)
129-
for i in range(len(map_colors)):
130-
t = map_timestamps_norm[i]
131-
map_colors[i, :] = [t, t, t]
132-
map_colors[map_labels == 1] = [1, 0, 0]
133-
pcd_map.colors = o3d.utility.Vector3dVector(map_colors)
134-
135-
o3d.io.write_point_cloud(os.path.join(path, f"{idx:06}.ply"), pcd_scan + pcd_map)
106+
if len(batch) > 0:
107+
mask_scan = batch[:, 4] == idx
108+
scan_points = batch[mask_scan, 1:4]
109+
scan_labels = batch[mask_scan, 6]
110+
111+
map_points = batch[~mask_scan, 1:4]
112+
map_timestamps = batch[~mask_scan, 5]
113+
map_labels = batch[~mask_scan, 6]
114+
115+
min_time = torch.min(batch[:, 5])
116+
max_time = torch.max(batch[:, 5])
117+
118+
pcd_scan = o3d.geometry.PointCloud(
119+
o3d.utility.Vector3dVector(scan_points.numpy())
120+
).paint_uniform_color([0, 0, 1])
121+
scan_colors = np.array(pcd_scan.colors)
122+
scan_colors[scan_labels == 1] = [1, 0, 0]
123+
pcd_scan.colors = o3d.utility.Vector3dVector(scan_colors)
124+
125+
pcd_map = o3d.geometry.PointCloud(
126+
o3d.utility.Vector3dVector(map_points.numpy())
127+
).paint_uniform_color([0, 0, 0])
128+
map_colors = np.array(pcd_map.colors)
129+
map_timestamps_norm = (map_timestamps - min_time) / (max_time - min_time)
130+
for i in range(len(map_colors)):
131+
t = map_timestamps_norm[i]
132+
map_colors[i, :] = [t, t, t]
133+
map_colors[map_labels == 1] = [1, 0, 0]
134+
pcd_map.colors = o3d.utility.Vector3dVector(map_colors)
135+
136+
o3d.io.write_point_cloud(os.path.join(path, f"{idx:06}.ply"), pcd_scan + pcd_map)
136137

137138

138139
if __name__ == "__main__":

src/mapmos/config/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ class OdometryConfig(BaseModel):
3939

4040
class MOSConfig(BaseModel):
4141
voxel_size_mos: float = 0.1
42+
delay_mos: int = 10
4243
max_range_mos: float = 50.0
4344
min_range_mos: float = 0.0
4445
voxel_size_belief: float = 0.25
4546
max_range_belief: float = 150
46-
delay_belief: int = 10
4747

4848

4949
class TrainingConfig(BaseModel):

src/mapmos/datasets/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def sequence_dataloaders():
4444
"kitti_tracking",
4545
"nuscenes",
4646
"apollo",
47+
"helimos",
4748
]
4849

4950

src/mapmos/datasets/helimos.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# MIT License
2+
#
3+
# Copyright (c) 2023 Benedikt Mersch, Tiziano Guadagnino, Ignacio Vizzo, Cyrill Stachniss
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
import glob
24+
import os
25+
import numpy as np
26+
27+
28+
class HeliMOSDataset:
29+
def __init__(self, data_dir, sequence: str, *_, **__):
30+
self.sequence_id = sequence.split("/")[0]
31+
split_file = sequence.split("/")[1]
32+
self.sequence_dir = os.path.join(data_dir, self.sequence_id)
33+
self.scan_dir = os.path.join(self.sequence_dir, "velodyne/")
34+
35+
self.scan_files = sorted(glob.glob(self.scan_dir + "*.bin"))
36+
self.calibration = self.read_calib_file(os.path.join(self.sequence_dir, "calib.txt"))
37+
38+
# Load GT Poses (if available)
39+
self.poses_fn = os.path.join(self.sequence_dir, "poses.txt")
40+
if os.path.exists(self.poses_fn):
41+
self.gt_poses = self.load_poses(self.poses_fn)
42+
43+
# No correction
44+
self.correct_kitti_scan = lambda frame: frame
45+
46+
# Load labels
47+
self.label_dir = os.path.join(self.sequence_dir, "labels/")
48+
label_files = sorted(glob.glob(self.label_dir + "*.label"))
49+
50+
# Get labels for train/val split if desired
51+
label_indices = np.loadtxt(os.path.join(data_dir, split_file), dtype=int).tolist()
52+
53+
# Filter based on split if desired
54+
getIndex = lambda filename: int(os.path.basename(filename).split(".label")[0])
55+
self.dict_label_files = {
56+
getIndex(filename): filename
57+
for filename in label_files
58+
if getIndex(filename) in label_indices
59+
}
60+
61+
def __getitem__(self, idx):
62+
points = self.scans(idx)
63+
timestamps = np.zeros(len(points))
64+
labels = (
65+
self.read_labels(self.dict_label_files[idx])
66+
if idx in self.dict_label_files.keys()
67+
else np.full(len(points), -1, dtype=np.int32)
68+
)
69+
return points, timestamps, labels
70+
71+
def __len__(self):
72+
return len(self.scan_files)
73+
74+
def scans(self, idx):
75+
return self.read_point_cloud(self.scan_files[idx])
76+
77+
def apply_calibration(self, poses: np.ndarray) -> np.ndarray:
78+
"""Converts from Velodyne to Camera Frame"""
79+
Tr = np.eye(4, dtype=np.float64)
80+
Tr[:3, :4] = self.calibration["Tr"].reshape(3, 4)
81+
return Tr @ poses @ np.linalg.inv(Tr)
82+
83+
def read_point_cloud(self, scan_file: str):
84+
points = np.fromfile(scan_file, dtype=np.float32).reshape((-1, 4))[:, :3].astype(np.float64)
85+
return points
86+
87+
def load_poses(self, poses_file):
88+
def _lidar_pose_gt(poses_gt):
89+
_tr = self.calibration["Tr"].reshape(3, 4)
90+
tr = np.eye(4, dtype=np.float64)
91+
tr[:3, :4] = _tr
92+
left = np.einsum("...ij,...jk->...ik", np.linalg.inv(tr), poses_gt)
93+
right = np.einsum("...ij,...jk->...ik", left, tr)
94+
return right
95+
96+
poses = np.loadtxt(poses_file, delimiter=" ")
97+
n = poses.shape[0]
98+
poses = np.concatenate(
99+
(poses, np.zeros((n, 3), dtype=np.float32), np.ones((n, 1), dtype=np.float32)), axis=1
100+
)
101+
poses = poses.reshape((n, 4, 4)) # [N, 4, 4]
102+
103+
# Ensure rotations are SO3
104+
rotations = poses[:, :3, :3]
105+
U, _, Vh = np.linalg.svd(rotations)
106+
poses[:, :3, :3] = U @ Vh
107+
108+
return _lidar_pose_gt(poses)
109+
110+
@staticmethod
111+
def read_calib_file(file_path: str) -> dict:
112+
calib_dict = {}
113+
with open(file_path, "r") as calib_file:
114+
for line in calib_file.readlines():
115+
tokens = line.split(" ")
116+
if tokens[0] == "calib_time:":
117+
continue
118+
# Only read with float data
119+
if len(tokens) > 0:
120+
values = [float(token) for token in tokens[1:]]
121+
values = np.array(values, dtype=np.float32)
122+
123+
# The format in KITTI's file is <key>: <f1> <f2> <f3> ...\n -> Remove the ':'
124+
key = tokens[0][:-1]
125+
calib_dict[key] = values
126+
return calib_dict
127+
128+
def read_labels(self, filename):
129+
"""Load moving object labels from .label file"""
130+
orig_labels = np.fromfile(filename, dtype=np.int32).reshape((-1))
131+
orig_labels = orig_labels & 0xFFFF # Mask semantics in lower half
132+
133+
labels = np.zeros_like(orig_labels)
134+
labels[orig_labels <= 1] = -1 # Unlabeled (0), outlier (1)
135+
labels[orig_labels > 250] = 1 # Moving
136+
labels = labels.astype(dtype=np.int32).reshape(-1)
137+
return labels

0 commit comments

Comments
 (0)