apairo-preprocess 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apairo_preprocess/__init__.py +27 -0
- apairo_preprocess/ground/__init__.py +9 -0
- apairo_preprocess/ground/csf.py +95 -0
- apairo_preprocess/ground/from_labels.py +55 -0
- apairo_preprocess/ground/ransac.py +95 -0
- apairo_preprocess/odometry/__init__.py +4 -0
- apairo_preprocess/odometry/gicp.py +96 -0
- apairo_preprocess/odometry/kissicp.py +84 -0
- apairo_preprocess/pointcloud/__init__.py +4 -0
- apairo_preprocess/pointcloud/robot_filter.py +121 -0
- apairo_preprocess/pointcloud/voxelise.py +281 -0
- apairo_preprocess/priors/__init__.py +7 -0
- apairo_preprocess/priors/ground_height.py +60 -0
- apairo_preprocess/priors/trajectory_distance.py +97 -0
- apairo_preprocess/projection/__init__.py +12 -0
- apairo_preprocess/projection/bev.py +166 -0
- apairo_preprocess/projection/image_mask.py +181 -0
- apairo_preprocess/projection/lidar_to_camera.py +177 -0
- apairo_preprocess/projection/point_features.py +100 -0
- apairo_preprocess/traversability/__init__.py +7 -0
- apairo_preprocess/traversability/from_labels.py +61 -0
- apairo_preprocess/traversability/from_trajectory.py +181 -0
- apairo_preprocess-0.2.0.dist-info/METADATA +255 -0
- apairo_preprocess-0.2.0.dist-info/RECORD +27 -0
- apairo_preprocess-0.2.0.dist-info/WHEEL +5 -0
- apairo_preprocess-0.2.0.dist-info/licenses/LICENSE +21 -0
- apairo_preprocess-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Trajectory-based traversability ground truth.
|
|
2
|
+
|
|
3
|
+
For each scan, a point is labelled traversable (1) if it lies within the
|
|
4
|
+
robot's 2-D footprint projected along any **future** pose — i.e. positions
|
|
5
|
+
the robot has not yet visited.
|
|
6
|
+
|
|
7
|
+
Poses are read from the dataset via ``poses_key`` (declared in
|
|
8
|
+
``input_keys``). The full trajectory is available in ``__call__`` because
|
|
9
|
+
:class:`SequencePreprocessor` receives an iterator over all frames at once.
|
|
10
|
+
|
|
11
|
+
Poses must be ``(4, 4)`` float64 homogeneous matrices. Apply
|
|
12
|
+
``ds = ds.transform(poses_key, PoseTo4x4())`` from ``apairo_transform`` beforehand
|
|
13
|
+
if your poses are stored in another format (quaternion, Euler, compact 3×4).
|
|
14
|
+
|
|
15
|
+
Typical usage::
|
|
16
|
+
|
|
17
|
+
Goose3DDataset.run_preprocess(
|
|
18
|
+
TraversabilityFromTrajectory(poses_key="kissicp_poses", robot_radius=0.75),
|
|
19
|
+
split_dir,
|
|
20
|
+
)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import ClassVar, Iterator
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
from scipy.spatial import KDTree
|
|
29
|
+
|
|
30
|
+
from apairo.core.preprocessor import SequencePreprocessor
|
|
31
|
+
from apairo.core.sample import Sample
|
|
32
|
+
from apairo_transform import RangeFilter
|
|
33
|
+
|
|
34
|
+
class TraversabilityFromTrajectory(SequencePreprocessor):
|
|
35
|
+
"""Label each point traversable if it lies in the robot's forward footprint.
|
|
36
|
+
|
|
37
|
+
Poses and point clouds are loaded from the dataset via ``input_keys``.
|
|
38
|
+
Sequence boundary detection and look-ahead are computed in ``__call__``
|
|
39
|
+
where the full trajectory is available.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
lidar_key: Input channel for point cloud data.
|
|
43
|
+
poses_key: Input channel for per-frame poses — must be ``(4, 4)``
|
|
44
|
+
float64. Apply ``PoseTo4x4()`` from ``apairo_transform``
|
|
45
|
+
if needed.
|
|
46
|
+
robot_radius: Half-width of the robot footprint in XY (metres).
|
|
47
|
+
height_min: Minimum point height relative to the nearest robot
|
|
48
|
+
position to be traversable (metres, ≤ 0).
|
|
49
|
+
height_max: Maximum point height relative to the nearest robot
|
|
50
|
+
position (metres, ≥ 0).
|
|
51
|
+
forward_window: Maximum number of future poses to look ahead.
|
|
52
|
+
``None`` (default) uses the entire remaining trajectory.
|
|
53
|
+
forward_distance: Maximum *path length* (metres) of future trajectory
|
|
54
|
+
to consider. Caps the look-ahead where the path
|
|
55
|
+
loops back near already-seen terrain: without it, a
|
|
56
|
+
point can be labelled traversable because a much
|
|
57
|
+
later part of the trajectory passes nearby.
|
|
58
|
+
``None`` (default) does not cap by distance.
|
|
59
|
+
sequence_gap: Distance threshold (metres) to detect sequence
|
|
60
|
+
boundaries and avoid look-ahead across discontinuous
|
|
61
|
+
sessions.
|
|
62
|
+
near_exclusion_radius: Sensor-frame distance below which a point is never
|
|
63
|
+
labelled traversable, suppressing dynamic objects
|
|
64
|
+
(e.g. humans) close to the robot. The shape of the
|
|
65
|
+
exclusion zone is controlled by ``near_exclusion_norm``.
|
|
66
|
+
``0.0`` disables the exclusion (default).
|
|
67
|
+
near_exclusion_norm: Norm order for the exclusion distance, forwarded to
|
|
68
|
+
:class:`~apairo_transform.RangeFilter`. Use
|
|
69
|
+
``np.inf`` (L∞) for a cube, ``2`` for a sphere.
|
|
70
|
+
Defaults to ``np.inf``.
|
|
71
|
+
output_key: Override the default output channel name ``"trav_traj"``.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
output_key: ClassVar[str] = "trav_traj"
|
|
75
|
+
input_keys: ClassVar[list[str]] = ["lidar", "poses"]
|
|
76
|
+
timestamps_from: ClassVar[str] = "lidar"
|
|
77
|
+
sources: ClassVar[list[str]] = ["lidar", "poses"]
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
lidar_key: str = "lidar",
|
|
82
|
+
poses_key: str = "poses",
|
|
83
|
+
robot_radius: float = 0.75,
|
|
84
|
+
height_min: float = -0.3,
|
|
85
|
+
height_max: float = 0.5,
|
|
86
|
+
forward_window: int | None = None,
|
|
87
|
+
forward_distance: float | None = None,
|
|
88
|
+
sequence_gap: float = 5.0,
|
|
89
|
+
near_exclusion_radius: float = 0.0,
|
|
90
|
+
near_exclusion_norm: float = np.inf,
|
|
91
|
+
output_key: str | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
self._lidar_key = lidar_key
|
|
94
|
+
self._poses_key = poses_key
|
|
95
|
+
self._robot_radius = robot_radius
|
|
96
|
+
self._height_min = height_min
|
|
97
|
+
self._height_max = height_max
|
|
98
|
+
self._forward_window = forward_window
|
|
99
|
+
self._forward_distance = forward_distance
|
|
100
|
+
self._sequence_gap = sequence_gap
|
|
101
|
+
self._near_filter = (
|
|
102
|
+
RangeFilter(min=near_exclusion_radius, max=None, norm=near_exclusion_norm)
|
|
103
|
+
if near_exclusion_radius > 0
|
|
104
|
+
else None
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self.input_keys = [lidar_key, poses_key]
|
|
108
|
+
self.sources = [lidar_key, poses_key]
|
|
109
|
+
if output_key is not None:
|
|
110
|
+
self.output_key = output_key
|
|
111
|
+
|
|
112
|
+
def __call__(self, frames: Iterator[Sample]) -> np.ndarray:
|
|
113
|
+
all_samples = list(frames)
|
|
114
|
+
n = len(all_samples)
|
|
115
|
+
|
|
116
|
+
poses = np.asarray(
|
|
117
|
+
np.stack([s.data[self._poses_key] for s in all_samples]), dtype=np.float64
|
|
118
|
+
)
|
|
119
|
+
if poses.shape[1:] != (4, 4):
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"poses must be (N, 4, 4) — apply PoseTo4x4() from apairo_transform first. "
|
|
122
|
+
f"Got {poses.shape}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Sequence boundary detection — done here where poses are available.
|
|
126
|
+
positions = poses[:, :3, 3]
|
|
127
|
+
dists = np.linalg.norm(np.diff(positions, axis=0), axis=1)
|
|
128
|
+
boundaries = np.where(dists > self._sequence_gap)[0]
|
|
129
|
+
ends = np.concatenate([boundaries, [n - 1]])
|
|
130
|
+
seq_end = ends[np.searchsorted(ends, np.arange(n))]
|
|
131
|
+
|
|
132
|
+
# Cumulative path length, for the forward_distance cap.
|
|
133
|
+
path = np.concatenate([[0.0], np.cumsum(dists)])
|
|
134
|
+
|
|
135
|
+
results = []
|
|
136
|
+
for idx, sample in enumerate(all_samples):
|
|
137
|
+
pc = np.asarray(sample.data[self._lidar_key])
|
|
138
|
+
xyz_sensor = pc[:, :3].astype(np.float64)
|
|
139
|
+
n_pts = len(xyz_sensor)
|
|
140
|
+
|
|
141
|
+
T = poses[idx]
|
|
142
|
+
xyz_h = np.column_stack([xyz_sensor, np.ones(n_pts)])
|
|
143
|
+
xyz_world = (T @ xyz_h.T).T[:, :3]
|
|
144
|
+
|
|
145
|
+
end = int(seq_end[idx]) + 1
|
|
146
|
+
if self._forward_window is not None:
|
|
147
|
+
end = min(end, idx + 1 + self._forward_window)
|
|
148
|
+
if self._forward_distance is not None:
|
|
149
|
+
end = min(
|
|
150
|
+
end,
|
|
151
|
+
int(np.searchsorted(path, path[idx] + self._forward_distance, side="right")),
|
|
152
|
+
)
|
|
153
|
+
future_pos = poses[idx + 1 : end, :3, 3]
|
|
154
|
+
|
|
155
|
+
if len(future_pos) == 0:
|
|
156
|
+
results.append(np.zeros(n_pts, dtype=np.uint8))
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
tree = KDTree(future_pos[:, :2])
|
|
160
|
+
# workers=-1 deadlocks when called from a ThreadPoolExecutor (e.g. viewer)
|
|
161
|
+
dist_xy, nn_idx = tree.query(xyz_world[:, :2], k=1)
|
|
162
|
+
dz = xyz_world[:, 2] - future_pos[nn_idx, 2]
|
|
163
|
+
|
|
164
|
+
trav = (
|
|
165
|
+
(dist_xy < self._robot_radius)
|
|
166
|
+
& (dz >= self._height_min)
|
|
167
|
+
& (dz <= self._height_max)
|
|
168
|
+
)
|
|
169
|
+
if self._near_filter is not None:
|
|
170
|
+
trav &= self._near_filter.compute_mask(xyz_sensor)
|
|
171
|
+
|
|
172
|
+
results.append(trav.astype(np.uint8))
|
|
173
|
+
|
|
174
|
+
# Uniform scans stack to (N, P) as before; ragged scans (real lidar,
|
|
175
|
+
# variable return count) become an object array of per-frame rows,
|
|
176
|
+
# which the per-frame runner and direct callers iterate identically.
|
|
177
|
+
if len({len(r) for r in results}) <= 1:
|
|
178
|
+
return np.stack(results)
|
|
179
|
+
out = np.empty(n, dtype=object)
|
|
180
|
+
out[:] = results
|
|
181
|
+
return out
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apairo-preprocess
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Preprocessing for apairo datasets: odometry, ground segmentation, projection, traversability labels
|
|
5
|
+
Author-email: Augustin Bresset <augustin.bresset@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://apairo-robotics.github.io/
|
|
8
|
+
Project-URL: Repository, https://github.com/apairo-robotics/apairo_preprocess
|
|
9
|
+
Project-URL: Issues, https://github.com/apairo-robotics/apairo_preprocess/issues
|
|
10
|
+
Keywords: apairo,lidar,point-cloud,traversability,ground-segmentation,robotics
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.26
|
|
23
|
+
Requires-Dist: scipy
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: tqdm
|
|
26
|
+
Requires-Dist: cloth-simulation-filter
|
|
27
|
+
Requires-Dist: apairo>=0.9
|
|
28
|
+
Requires-Dist: apairo-transform>=0.3
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# apairo-preprocess
|
|
34
|
+
|
|
35
|
+
Preprocessing pipelines for [apairo](https://github.com/apairo-robotics/apairo) datasets — LiDAR odometry, ground segmentation, camera projection, and traversability ground truth generation.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install apairo-preprocess
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It brings `apairo` (≥ 0.9) and `apairo-transform` (≥ 0.3).
|
|
46
|
+
|
|
47
|
+
### Optional dependencies
|
|
48
|
+
|
|
49
|
+
| Preprocessor | Dependency | Install |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `KissICPOdometry` | KISS-ICP | `pip install kiss-icp` |
|
|
52
|
+
| `GICPOdometry` | Open3D | `pip install open3d` |
|
|
53
|
+
| `GroundSegmentationCSF` / `GroundHeightFromLabels` (CSF backend) | CSF | `pip install cloth-simulation-filter` |
|
|
54
|
+
|
|
55
|
+
Requires Python ≥ 3.11.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Preprocessors
|
|
60
|
+
|
|
61
|
+
### Odometry
|
|
62
|
+
|
|
63
|
+
| Class | Output channel | Backend | Output |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `KissICPOdometry` | `kissicp_poses` | [KISS-ICP](https://github.com/PRBonn/kiss-icp) | `(4, 4)` float64 pose per scan |
|
|
66
|
+
| `GICPOdometry` | `gicp_poses` | Open3D GICP | `(4, 4)` float64 pose per scan |
|
|
67
|
+
|
|
68
|
+
### Ground segmentation
|
|
69
|
+
|
|
70
|
+
Binary ground/non-ground labels (0 = ground, 1 = non-ground). All three algorithms share the same label convention and can be compared directly.
|
|
71
|
+
|
|
72
|
+
| Class | Output channel | Method | Extra dep |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `GroundSegmentationCSF` | `ground_csf` | Cloth Simulation Filter — accurate on uneven terrain | `CSF` |
|
|
75
|
+
| `GroundSegmentationRANSAC` | `ground_ransac` | RANSAC plane fitting — fast, assumes flat ground | — |
|
|
76
|
+
| `GroundSegmentationFromLabels` | `ground_labels` | Maps existing semantic class IDs to ground/non-ground | — |
|
|
77
|
+
|
|
78
|
+
### Priors
|
|
79
|
+
|
|
80
|
+
Scalar per-point features used as traversability priors. Depend on previously computed channels.
|
|
81
|
+
|
|
82
|
+
| Class | Output channel | Input channels | Output |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| `GroundHeightFromLabels` | `ground_height` | any ground segmentation + `voxelised` | float32 height above nearest ground point (m) |
|
|
85
|
+
| `TrajectoryDistance` | `trajectory_distance` | `voxelised` + `poses` | float32 distance to nearest trajectory waypoint (m) |
|
|
86
|
+
|
|
87
|
+
### Traversability
|
|
88
|
+
|
|
89
|
+
Binary traversability labels (1 = traversable, 0 = not).
|
|
90
|
+
|
|
91
|
+
| Class | Output channel | Method |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `TraversabilityFromLabels` | `trav_label` | Maps semantic class IDs to binary traversable/non-traversable |
|
|
94
|
+
| `TraversabilityFromTrajectory` | `trav_traj` | Labels points inside the robot's forward footprint along the trajectory |
|
|
95
|
+
|
|
96
|
+
### Camera projection
|
|
97
|
+
|
|
98
|
+
Atomic bridges between the point cloud and image spaces. All outputs are
|
|
99
|
+
**row-aligned** with the lidar scan, so any per-point channel composes with
|
|
100
|
+
the projection by row index.
|
|
101
|
+
|
|
102
|
+
| Class | Output channel | Input channels | Output |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `LidarCameraProjection` | `lidar_uv` | lidar only (extrinsics/intrinsics are static) | `(N, 3)` float32 `[u, v, depth]`, NaN outside the frustum |
|
|
105
|
+
| `PointFeaturesFromImage` | `point_features` | `lidar_uv` + image | `(N, C)` image values per point (e.g. RGB) — image → cloud |
|
|
106
|
+
| `ImageMaskFromPointLabels` | `trav_mask` | `trav_traj` + `lidar_uv` | `(H, W)` uint8 label mask, 255 = no data — cloud → image |
|
|
107
|
+
|
|
108
|
+
Pixel conflicts resolve to the nearest point (depth ordering); an optional
|
|
109
|
+
occlusion filter (`occlusion_bin_px`) drops points hidden behind nearer
|
|
110
|
+
returns so ground behind an obstacle does not bleed onto its pixels.
|
|
111
|
+
Both halves of the projection come from the dataset calibration: the
|
|
112
|
+
extrinsic via `ds.calibration.get_tf(lidar_frame, camera_frame)`, the
|
|
113
|
+
intrinsics via `ds.calibration.get_intrinsics(camera_frame)`.
|
|
114
|
+
|
|
115
|
+
**Asynchronous datasets:** `LidarCameraProjection` streams a single channel
|
|
116
|
+
and runs anywhere. The two multi-channel preprocessors run via
|
|
117
|
+
`run_preprocess` on synchronous (profiled) datasets only; on an async dataset
|
|
118
|
+
(TartanDrive, raw rigs) run them over a `synchronize()` view and persist with
|
|
119
|
+
`ChannelWriter` — see
|
|
120
|
+
[`examples/traversability_image_mask.py`](examples/traversability_image_mask.py).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Quickstart
|
|
125
|
+
|
|
126
|
+
### Ground segmentation
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from apairo.dataset.rellis import Rellis3DDataset
|
|
130
|
+
from apairo_preprocess import GroundSegmentationCSF, GroundSegmentationFromLabels, GroundSegmentationRANSAC
|
|
131
|
+
|
|
132
|
+
dataset_dir = "/data/Rellis-3D/00000"
|
|
133
|
+
|
|
134
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationRANSAC(), dataset_dir)
|
|
135
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationCSF(), dataset_dir) # requires: pip install CSF
|
|
136
|
+
|
|
137
|
+
# From existing semantic labels (default ground IDs are for RELLIS-3D)
|
|
138
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationFromLabels(), dataset_dir)
|
|
139
|
+
|
|
140
|
+
# writes ground_ransac/, ground_csf/, ground_labels/ (uint8: 0=ground, 1=non-ground)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Height above ground (prior)
|
|
144
|
+
|
|
145
|
+
Ground segmentation must be computed first.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from apairo_preprocess import GroundSegmentationCSF, GroundHeightFromLabels
|
|
149
|
+
|
|
150
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationCSF(), dataset_dir)
|
|
151
|
+
Rellis3DDataset.run_preprocess(
|
|
152
|
+
GroundHeightFromLabels(ground_key="ground_csf"),
|
|
153
|
+
dataset_dir,
|
|
154
|
+
)
|
|
155
|
+
# writes ground_height/ (float32, metres above nearest ground point)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`GroundHeightFromLabels` accepts any ground key — swap `"ground_csf"` for `"ground_ransac"` or `"ground_labels"` to change the backend without re-running CSF.
|
|
159
|
+
|
|
160
|
+
### Traversability from semantic labels
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from apairo_preprocess import TraversabilityFromLabels
|
|
164
|
+
|
|
165
|
+
# Default traversable IDs for RELLIS-3D: {dirt, grass, asphalt, concrete, puddle, mud}
|
|
166
|
+
Rellis3DDataset.run_preprocess(TraversabilityFromLabels(), dataset_dir)
|
|
167
|
+
# writes trav_label/ (uint8: 1=traversable, 0=not)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Custom IDs for SemanticKITTI:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from apairo.dataset.semantic_kitti import SemanticKittiDataset
|
|
174
|
+
from apairo_preprocess import TraversabilityFromLabels
|
|
175
|
+
|
|
176
|
+
SemanticKittiDataset.run_preprocess(
|
|
177
|
+
TraversabilityFromLabels(traversable_ids=frozenset({40, 44, 48, 49, 60, 72})),
|
|
178
|
+
"/data/sequences/00",
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Traversability ground truth from trajectory
|
|
183
|
+
|
|
184
|
+
Requires poses to be computed first (e.g. with `KissICPOdometry`).
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from apairo.dataset.goose import Goose3DDataset
|
|
188
|
+
from apairo_preprocess import KissICPOdometry, TraversabilityFromTrajectory
|
|
189
|
+
|
|
190
|
+
Goose3DDataset.run_preprocess(KissICPOdometry(voxel_size=1.0), "/data/goose/seq_001")
|
|
191
|
+
Goose3DDataset.run_preprocess(
|
|
192
|
+
TraversabilityFromTrajectory(poses_key="kissicp_poses", robot_radius=0.75),
|
|
193
|
+
"/data/goose/seq_001",
|
|
194
|
+
)
|
|
195
|
+
# writes trav_traj/ (uint8: 1=traversable, 0=not)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Traversability mask in image space
|
|
199
|
+
|
|
200
|
+
Projects the trajectory ground truth into the camera: `trav_traj` labels the
|
|
201
|
+
points, `lidar_uv` places them in the image, `ImageMaskFromPointLabels`
|
|
202
|
+
paints the mask.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from apairo_preprocess import ImageMaskFromPointLabels, LidarCameraProjection
|
|
206
|
+
|
|
207
|
+
cal = Rellis3DDataset(dataset_dir, keys=["lidar"]).calibration
|
|
208
|
+
cam = cal.get_intrinsics("camera") # K, distortion, image size
|
|
209
|
+
T = cal.get_tf("lidar", "camera") # lidar -> camera optical frame
|
|
210
|
+
|
|
211
|
+
Rellis3DDataset.run_preprocess(
|
|
212
|
+
LidarCameraProjection(intrinsics=cam, extrinsics=T),
|
|
213
|
+
dataset_dir,
|
|
214
|
+
)
|
|
215
|
+
Rellis3DDataset.run_preprocess(
|
|
216
|
+
ImageMaskFromPointLabels(image_size=(cam.height, cam.width), radius=2, occlusion_bin_px=8),
|
|
217
|
+
dataset_dir,
|
|
218
|
+
)
|
|
219
|
+
# writes lidar_uv/ (float32 [u, v, depth] per point)
|
|
220
|
+
# and trav_mask/ (uint8 per pixel: 1=traversable, 0=not, 255=no data)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
To sanity-check calibration and projection first, colour the cloud with
|
|
224
|
+
camera RGB and inspect it in a 3-D viewer (projector, apairo_rr):
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from apairo_preprocess import PointFeaturesFromImage
|
|
228
|
+
|
|
229
|
+
Rellis3DDataset.run_preprocess(
|
|
230
|
+
PointFeaturesFromImage(image_key="image", output_key="lidar_rgb"),
|
|
231
|
+
dataset_dir,
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Examples
|
|
238
|
+
|
|
239
|
+
Ready-to-run scripts in [`examples/`](examples/):
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
python examples/kissicp_odometry.py /data/Rellis-3D/00000 --dataset rellis
|
|
243
|
+
python examples/traversability_from_labels.py /data/Rellis-3D/00000
|
|
244
|
+
python examples/traversability_from_trajectory.py /data/goose/seq_001
|
|
245
|
+
python examples/traversability_image_mask.py /data/tartan/seq --lidar-frame velodyne --camera-frame multisense_left --rgb
|
|
246
|
+
|
|
247
|
+
# no dataset needed — synthetic scene, renders every pipeline stage (needs matplotlib)
|
|
248
|
+
python examples/projection_pipeline_demo.py --out-dir /tmp/projection_demo
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
apairo_preprocess/__init__.py,sha256=wEAn4dD45rqJAFfaWqEqk8UCjiR8bNpZ7ca5Fqv4DRQ,1133
|
|
2
|
+
apairo_preprocess/ground/__init__.py,sha256=3UvuSZhAdVvUqZZjFFNLH90LBs-ZSmhLmEXl1x2bjq4,322
|
|
3
|
+
apairo_preprocess/ground/csf.py,sha256=6DmUABDq6Of4Wi1JHdCAE8OH3iaYA9CNKdqVbkq5zfo,3157
|
|
4
|
+
apairo_preprocess/ground/from_labels.py,sha256=Bcj8bpB6_mRf_9MnYKx5-7Zl8lFhAyaAWvreRl8BFfs,1911
|
|
5
|
+
apairo_preprocess/ground/ransac.py,sha256=IlhK4oeo7JJSm_xe1SRcs1cSOcg-0tGdZ1XAh4VjUKo,3115
|
|
6
|
+
apairo_preprocess/odometry/__init__.py,sha256=erzuos15IRNHoBm0q3Hf-IqvP3lpnSLKNc5doKX2lks,167
|
|
7
|
+
apairo_preprocess/odometry/gicp.py,sha256=RM3NQjSTJMGpe9p8bPPVbubdTMk6HHtnIwVcHRnvTgk,3337
|
|
8
|
+
apairo_preprocess/odometry/kissicp.py,sha256=pqnbmmZ4Q7J2cYUOEP_RYZux851A_4roMMuZcjX_Lao,2884
|
|
9
|
+
apairo_preprocess/pointcloud/__init__.py,sha256=0p975u6q-KfblA6CJwI1U7sfsSDZVD35Gie1M7PQwP8,264
|
|
10
|
+
apairo_preprocess/pointcloud/robot_filter.py,sha256=ckh8eTE_0FtJNn-QEQHq_jPF1B8WAFJPUaZp-tXRGDU,4388
|
|
11
|
+
apairo_preprocess/pointcloud/voxelise.py,sha256=IVuzNjNbNMhZbJmlHeBZaF9yHcQTEx9IXMhchk_SnQo,10861
|
|
12
|
+
apairo_preprocess/priors/__init__.py,sha256=3_s47SMwLczKFM7LhO43wtW77Db-ZGBSXwPWANkcatI,221
|
|
13
|
+
apairo_preprocess/priors/ground_height.py,sha256=SKbVN0Yz5gYx5f9imOSTxXSmi2QU9tzTx61__XWKvd4,2264
|
|
14
|
+
apairo_preprocess/priors/trajectory_distance.py,sha256=4FmNxJ4KX-rl8iacuYPofWsYAQQOehMUv9iYscXWqxM,3576
|
|
15
|
+
apairo_preprocess/projection/__init__.py,sha256=RWy_75qtgOtvNQqGAdp4rMrstAvVTLe5tSaNw8UYUHY,465
|
|
16
|
+
apairo_preprocess/projection/bev.py,sha256=PUyc9HyU9UkVvHBjgwm5wRA7wpiT47fuP8oc75UdcCc,6711
|
|
17
|
+
apairo_preprocess/projection/image_mask.py,sha256=vH8TxwVbt5mPCTM07S6E0wQeX0YooF-qGoOaqREN3s0,7712
|
|
18
|
+
apairo_preprocess/projection/lidar_to_camera.py,sha256=oC-ZWEc5lJGFtTSSTN1sAmKflGU9_dMJrz5zKA0XXlM,7350
|
|
19
|
+
apairo_preprocess/projection/point_features.py,sha256=sjpJT-JMtHaZRkq-F0xNGucFZbNlq3of-4PlWm8oONo,4014
|
|
20
|
+
apairo_preprocess/traversability/__init__.py,sha256=PJXnR-IFpylesJBP3uJJhVae51yRPsF70R_tvvAIifk,255
|
|
21
|
+
apairo_preprocess/traversability/from_labels.py,sha256=Ko-Oa-xJgbYux61BbPcNeXJ-YiwgKJY9mP7o7_rkfew,1979
|
|
22
|
+
apairo_preprocess/traversability/from_trajectory.py,sha256=JhXbRCltlWGQ6GP08ErNP09305ILowzAZXoiCRtxbb8,7967
|
|
23
|
+
apairo_preprocess-0.2.0.dist-info/licenses/LICENSE,sha256=17AGWvla1McJcPrt-Tyos8l2V1W4rgNJiRujo1lKzTY,1073
|
|
24
|
+
apairo_preprocess-0.2.0.dist-info/METADATA,sha256=IQU6Q_SdQeWjlCOamkGucx-jSuqLTNUPd3-TAZNFBZY,9434
|
|
25
|
+
apairo_preprocess-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
26
|
+
apairo_preprocess-0.2.0.dist-info/top_level.txt,sha256=WT1v0QlQpiFhHPUuIHrZwbVGU2zxkvwUwSl19clk8WA,18
|
|
27
|
+
apairo_preprocess-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Augustin BRESSET
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
apairo_preprocess
|