apairo-preprocess 0.2.0__tar.gz
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-0.2.0/LICENSE +21 -0
- apairo_preprocess-0.2.0/PKG-INFO +255 -0
- apairo_preprocess-0.2.0/README.md +223 -0
- apairo_preprocess-0.2.0/apairo_preprocess/__init__.py +27 -0
- apairo_preprocess-0.2.0/apairo_preprocess/ground/__init__.py +9 -0
- apairo_preprocess-0.2.0/apairo_preprocess/ground/csf.py +95 -0
- apairo_preprocess-0.2.0/apairo_preprocess/ground/from_labels.py +55 -0
- apairo_preprocess-0.2.0/apairo_preprocess/ground/ransac.py +95 -0
- apairo_preprocess-0.2.0/apairo_preprocess/odometry/__init__.py +4 -0
- apairo_preprocess-0.2.0/apairo_preprocess/odometry/gicp.py +96 -0
- apairo_preprocess-0.2.0/apairo_preprocess/odometry/kissicp.py +84 -0
- apairo_preprocess-0.2.0/apairo_preprocess/pointcloud/__init__.py +4 -0
- apairo_preprocess-0.2.0/apairo_preprocess/pointcloud/robot_filter.py +121 -0
- apairo_preprocess-0.2.0/apairo_preprocess/pointcloud/voxelise.py +281 -0
- apairo_preprocess-0.2.0/apairo_preprocess/priors/__init__.py +7 -0
- apairo_preprocess-0.2.0/apairo_preprocess/priors/ground_height.py +60 -0
- apairo_preprocess-0.2.0/apairo_preprocess/priors/trajectory_distance.py +97 -0
- apairo_preprocess-0.2.0/apairo_preprocess/projection/__init__.py +12 -0
- apairo_preprocess-0.2.0/apairo_preprocess/projection/bev.py +166 -0
- apairo_preprocess-0.2.0/apairo_preprocess/projection/image_mask.py +181 -0
- apairo_preprocess-0.2.0/apairo_preprocess/projection/lidar_to_camera.py +177 -0
- apairo_preprocess-0.2.0/apairo_preprocess/projection/point_features.py +100 -0
- apairo_preprocess-0.2.0/apairo_preprocess/traversability/__init__.py +7 -0
- apairo_preprocess-0.2.0/apairo_preprocess/traversability/from_labels.py +61 -0
- apairo_preprocess-0.2.0/apairo_preprocess/traversability/from_trajectory.py +181 -0
- apairo_preprocess-0.2.0/apairo_preprocess.egg-info/PKG-INFO +255 -0
- apairo_preprocess-0.2.0/apairo_preprocess.egg-info/SOURCES.txt +42 -0
- apairo_preprocess-0.2.0/apairo_preprocess.egg-info/dependency_links.txt +1 -0
- apairo_preprocess-0.2.0/apairo_preprocess.egg-info/requires.txt +10 -0
- apairo_preprocess-0.2.0/apairo_preprocess.egg-info/top_level.txt +1 -0
- apairo_preprocess-0.2.0/pyproject.toml +49 -0
- apairo_preprocess-0.2.0/setup.cfg +4 -0
- apairo_preprocess-0.2.0/tests/test_bev_rasterisation.py +154 -0
- apairo_preprocess-0.2.0/tests/test_call_contract.py +20 -0
- apairo_preprocess-0.2.0/tests/test_gicp_odometry.py +50 -0
- apairo_preprocess-0.2.0/tests/test_image_mask_from_point_labels.py +173 -0
- apairo_preprocess-0.2.0/tests/test_kissicp_odometry.py +58 -0
- apairo_preprocess-0.2.0/tests/test_lidar_camera_projection.py +207 -0
- apairo_preprocess-0.2.0/tests/test_point_features_from_image.py +101 -0
- apairo_preprocess-0.2.0/tests/test_projection_pipeline.py +162 -0
- apairo_preprocess-0.2.0/tests/test_remove_robot_points.py +213 -0
- apairo_preprocess-0.2.0/tests/test_traversability_from_labels.py +50 -0
- apairo_preprocess-0.2.0/tests/test_traversability_from_trajectory.py +188 -0
- apairo_preprocess-0.2.0/tests/test_voxelise_coords.py +152 -0
|
@@ -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,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,223 @@
|
|
|
1
|
+
# apairo-preprocess
|
|
2
|
+
|
|
3
|
+
Preprocessing pipelines for [apairo](https://github.com/apairo-robotics/apairo) datasets — LiDAR odometry, ground segmentation, camera projection, and traversability ground truth generation.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install apairo-preprocess
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It brings `apairo` (≥ 0.9) and `apairo-transform` (≥ 0.3).
|
|
14
|
+
|
|
15
|
+
### Optional dependencies
|
|
16
|
+
|
|
17
|
+
| Preprocessor | Dependency | Install |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `KissICPOdometry` | KISS-ICP | `pip install kiss-icp` |
|
|
20
|
+
| `GICPOdometry` | Open3D | `pip install open3d` |
|
|
21
|
+
| `GroundSegmentationCSF` / `GroundHeightFromLabels` (CSF backend) | CSF | `pip install cloth-simulation-filter` |
|
|
22
|
+
|
|
23
|
+
Requires Python ≥ 3.11.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Preprocessors
|
|
28
|
+
|
|
29
|
+
### Odometry
|
|
30
|
+
|
|
31
|
+
| Class | Output channel | Backend | Output |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| `KissICPOdometry` | `kissicp_poses` | [KISS-ICP](https://github.com/PRBonn/kiss-icp) | `(4, 4)` float64 pose per scan |
|
|
34
|
+
| `GICPOdometry` | `gicp_poses` | Open3D GICP | `(4, 4)` float64 pose per scan |
|
|
35
|
+
|
|
36
|
+
### Ground segmentation
|
|
37
|
+
|
|
38
|
+
Binary ground/non-ground labels (0 = ground, 1 = non-ground). All three algorithms share the same label convention and can be compared directly.
|
|
39
|
+
|
|
40
|
+
| Class | Output channel | Method | Extra dep |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `GroundSegmentationCSF` | `ground_csf` | Cloth Simulation Filter — accurate on uneven terrain | `CSF` |
|
|
43
|
+
| `GroundSegmentationRANSAC` | `ground_ransac` | RANSAC plane fitting — fast, assumes flat ground | — |
|
|
44
|
+
| `GroundSegmentationFromLabels` | `ground_labels` | Maps existing semantic class IDs to ground/non-ground | — |
|
|
45
|
+
|
|
46
|
+
### Priors
|
|
47
|
+
|
|
48
|
+
Scalar per-point features used as traversability priors. Depend on previously computed channels.
|
|
49
|
+
|
|
50
|
+
| Class | Output channel | Input channels | Output |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `GroundHeightFromLabels` | `ground_height` | any ground segmentation + `voxelised` | float32 height above nearest ground point (m) |
|
|
53
|
+
| `TrajectoryDistance` | `trajectory_distance` | `voxelised` + `poses` | float32 distance to nearest trajectory waypoint (m) |
|
|
54
|
+
|
|
55
|
+
### Traversability
|
|
56
|
+
|
|
57
|
+
Binary traversability labels (1 = traversable, 0 = not).
|
|
58
|
+
|
|
59
|
+
| Class | Output channel | Method |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `TraversabilityFromLabels` | `trav_label` | Maps semantic class IDs to binary traversable/non-traversable |
|
|
62
|
+
| `TraversabilityFromTrajectory` | `trav_traj` | Labels points inside the robot's forward footprint along the trajectory |
|
|
63
|
+
|
|
64
|
+
### Camera projection
|
|
65
|
+
|
|
66
|
+
Atomic bridges between the point cloud and image spaces. All outputs are
|
|
67
|
+
**row-aligned** with the lidar scan, so any per-point channel composes with
|
|
68
|
+
the projection by row index.
|
|
69
|
+
|
|
70
|
+
| Class | Output channel | Input channels | Output |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `LidarCameraProjection` | `lidar_uv` | lidar only (extrinsics/intrinsics are static) | `(N, 3)` float32 `[u, v, depth]`, NaN outside the frustum |
|
|
73
|
+
| `PointFeaturesFromImage` | `point_features` | `lidar_uv` + image | `(N, C)` image values per point (e.g. RGB) — image → cloud |
|
|
74
|
+
| `ImageMaskFromPointLabels` | `trav_mask` | `trav_traj` + `lidar_uv` | `(H, W)` uint8 label mask, 255 = no data — cloud → image |
|
|
75
|
+
|
|
76
|
+
Pixel conflicts resolve to the nearest point (depth ordering); an optional
|
|
77
|
+
occlusion filter (`occlusion_bin_px`) drops points hidden behind nearer
|
|
78
|
+
returns so ground behind an obstacle does not bleed onto its pixels.
|
|
79
|
+
Both halves of the projection come from the dataset calibration: the
|
|
80
|
+
extrinsic via `ds.calibration.get_tf(lidar_frame, camera_frame)`, the
|
|
81
|
+
intrinsics via `ds.calibration.get_intrinsics(camera_frame)`.
|
|
82
|
+
|
|
83
|
+
**Asynchronous datasets:** `LidarCameraProjection` streams a single channel
|
|
84
|
+
and runs anywhere. The two multi-channel preprocessors run via
|
|
85
|
+
`run_preprocess` on synchronous (profiled) datasets only; on an async dataset
|
|
86
|
+
(TartanDrive, raw rigs) run them over a `synchronize()` view and persist with
|
|
87
|
+
`ChannelWriter` — see
|
|
88
|
+
[`examples/traversability_image_mask.py`](examples/traversability_image_mask.py).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Quickstart
|
|
93
|
+
|
|
94
|
+
### Ground segmentation
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from apairo.dataset.rellis import Rellis3DDataset
|
|
98
|
+
from apairo_preprocess import GroundSegmentationCSF, GroundSegmentationFromLabels, GroundSegmentationRANSAC
|
|
99
|
+
|
|
100
|
+
dataset_dir = "/data/Rellis-3D/00000"
|
|
101
|
+
|
|
102
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationRANSAC(), dataset_dir)
|
|
103
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationCSF(), dataset_dir) # requires: pip install CSF
|
|
104
|
+
|
|
105
|
+
# From existing semantic labels (default ground IDs are for RELLIS-3D)
|
|
106
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationFromLabels(), dataset_dir)
|
|
107
|
+
|
|
108
|
+
# writes ground_ransac/, ground_csf/, ground_labels/ (uint8: 0=ground, 1=non-ground)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Height above ground (prior)
|
|
112
|
+
|
|
113
|
+
Ground segmentation must be computed first.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from apairo_preprocess import GroundSegmentationCSF, GroundHeightFromLabels
|
|
117
|
+
|
|
118
|
+
Rellis3DDataset.run_preprocess(GroundSegmentationCSF(), dataset_dir)
|
|
119
|
+
Rellis3DDataset.run_preprocess(
|
|
120
|
+
GroundHeightFromLabels(ground_key="ground_csf"),
|
|
121
|
+
dataset_dir,
|
|
122
|
+
)
|
|
123
|
+
# writes ground_height/ (float32, metres above nearest ground point)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`GroundHeightFromLabels` accepts any ground key — swap `"ground_csf"` for `"ground_ransac"` or `"ground_labels"` to change the backend without re-running CSF.
|
|
127
|
+
|
|
128
|
+
### Traversability from semantic labels
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from apairo_preprocess import TraversabilityFromLabels
|
|
132
|
+
|
|
133
|
+
# Default traversable IDs for RELLIS-3D: {dirt, grass, asphalt, concrete, puddle, mud}
|
|
134
|
+
Rellis3DDataset.run_preprocess(TraversabilityFromLabels(), dataset_dir)
|
|
135
|
+
# writes trav_label/ (uint8: 1=traversable, 0=not)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Custom IDs for SemanticKITTI:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from apairo.dataset.semantic_kitti import SemanticKittiDataset
|
|
142
|
+
from apairo_preprocess import TraversabilityFromLabels
|
|
143
|
+
|
|
144
|
+
SemanticKittiDataset.run_preprocess(
|
|
145
|
+
TraversabilityFromLabels(traversable_ids=frozenset({40, 44, 48, 49, 60, 72})),
|
|
146
|
+
"/data/sequences/00",
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Traversability ground truth from trajectory
|
|
151
|
+
|
|
152
|
+
Requires poses to be computed first (e.g. with `KissICPOdometry`).
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from apairo.dataset.goose import Goose3DDataset
|
|
156
|
+
from apairo_preprocess import KissICPOdometry, TraversabilityFromTrajectory
|
|
157
|
+
|
|
158
|
+
Goose3DDataset.run_preprocess(KissICPOdometry(voxel_size=1.0), "/data/goose/seq_001")
|
|
159
|
+
Goose3DDataset.run_preprocess(
|
|
160
|
+
TraversabilityFromTrajectory(poses_key="kissicp_poses", robot_radius=0.75),
|
|
161
|
+
"/data/goose/seq_001",
|
|
162
|
+
)
|
|
163
|
+
# writes trav_traj/ (uint8: 1=traversable, 0=not)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Traversability mask in image space
|
|
167
|
+
|
|
168
|
+
Projects the trajectory ground truth into the camera: `trav_traj` labels the
|
|
169
|
+
points, `lidar_uv` places them in the image, `ImageMaskFromPointLabels`
|
|
170
|
+
paints the mask.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from apairo_preprocess import ImageMaskFromPointLabels, LidarCameraProjection
|
|
174
|
+
|
|
175
|
+
cal = Rellis3DDataset(dataset_dir, keys=["lidar"]).calibration
|
|
176
|
+
cam = cal.get_intrinsics("camera") # K, distortion, image size
|
|
177
|
+
T = cal.get_tf("lidar", "camera") # lidar -> camera optical frame
|
|
178
|
+
|
|
179
|
+
Rellis3DDataset.run_preprocess(
|
|
180
|
+
LidarCameraProjection(intrinsics=cam, extrinsics=T),
|
|
181
|
+
dataset_dir,
|
|
182
|
+
)
|
|
183
|
+
Rellis3DDataset.run_preprocess(
|
|
184
|
+
ImageMaskFromPointLabels(image_size=(cam.height, cam.width), radius=2, occlusion_bin_px=8),
|
|
185
|
+
dataset_dir,
|
|
186
|
+
)
|
|
187
|
+
# writes lidar_uv/ (float32 [u, v, depth] per point)
|
|
188
|
+
# and trav_mask/ (uint8 per pixel: 1=traversable, 0=not, 255=no data)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
To sanity-check calibration and projection first, colour the cloud with
|
|
192
|
+
camera RGB and inspect it in a 3-D viewer (projector, apairo_rr):
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from apairo_preprocess import PointFeaturesFromImage
|
|
196
|
+
|
|
197
|
+
Rellis3DDataset.run_preprocess(
|
|
198
|
+
PointFeaturesFromImage(image_key="image", output_key="lidar_rgb"),
|
|
199
|
+
dataset_dir,
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Examples
|
|
206
|
+
|
|
207
|
+
Ready-to-run scripts in [`examples/`](examples/):
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
python examples/kissicp_odometry.py /data/Rellis-3D/00000 --dataset rellis
|
|
211
|
+
python examples/traversability_from_labels.py /data/Rellis-3D/00000
|
|
212
|
+
python examples/traversability_from_trajectory.py /data/goose/seq_001
|
|
213
|
+
python examples/traversability_image_mask.py /data/tartan/seq --lidar-frame velodyne --camera-frame multisense_left --rgb
|
|
214
|
+
|
|
215
|
+
# no dataset needed — synthetic scene, renders every pipeline stage (needs matplotlib)
|
|
216
|
+
python examples/projection_pipeline_demo.py --out-dir /tmp/projection_demo
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from apairo_preprocess.traversability import TraversabilityFromLabels, TraversabilityFromTrajectory
|
|
2
|
+
from apairo_preprocess.ground import GroundSegmentationCSF, GroundSegmentationFromLabels, GroundSegmentationRANSAC
|
|
3
|
+
from apairo_preprocess.priors import GroundHeightFromLabels, TrajectoryDistance
|
|
4
|
+
from apairo_preprocess.odometry import GICPOdometry, KissICPOdometry
|
|
5
|
+
from apairo_preprocess.pointcloud import VoxeliseCoords, VoxeliseLabels, VoxelisePointCloud, RemoveRobotPoints
|
|
6
|
+
from apairo_preprocess.projection import BEVRasterisation, ImageMaskFromPointLabels, LidarCameraProjection, PointFeaturesFromImage, to_uint8_image
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"TraversabilityFromLabels",
|
|
10
|
+
"TraversabilityFromTrajectory",
|
|
11
|
+
"GroundSegmentationCSF",
|
|
12
|
+
"GroundSegmentationFromLabels",
|
|
13
|
+
"GroundSegmentationRANSAC",
|
|
14
|
+
"GroundHeightFromLabels",
|
|
15
|
+
"TrajectoryDistance",
|
|
16
|
+
"GICPOdometry",
|
|
17
|
+
"KissICPOdometry",
|
|
18
|
+
"VoxelisePointCloud",
|
|
19
|
+
"VoxeliseLabels",
|
|
20
|
+
"VoxeliseCoords",
|
|
21
|
+
"RemoveRobotPoints",
|
|
22
|
+
"LidarCameraProjection",
|
|
23
|
+
"PointFeaturesFromImage",
|
|
24
|
+
"ImageMaskFromPointLabels",
|
|
25
|
+
"BEVRasterisation",
|
|
26
|
+
"to_uint8_image",
|
|
27
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from apairo_preprocess.ground.csf import GroundSegmentationCSF
|
|
2
|
+
from apairo_preprocess.ground.from_labels import GroundSegmentationFromLabels
|
|
3
|
+
from apairo_preprocess.ground.ransac import GroundSegmentationRANSAC
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"GroundSegmentationCSF",
|
|
7
|
+
"GroundSegmentationFromLabels",
|
|
8
|
+
"GroundSegmentationRANSAC",
|
|
9
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Ground segmentation via Cloth Simulation Filter (CSF)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from apairo.core.preprocessor import FramePreprocessor
|
|
10
|
+
from apairo.core.sample import Sample
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_csf(
|
|
14
|
+
xyz: np.ndarray,
|
|
15
|
+
cloth_resolution: float,
|
|
16
|
+
class_threshold: float,
|
|
17
|
+
rigidness: int,
|
|
18
|
+
) -> tuple[list[int], list[int]]:
|
|
19
|
+
"""Run CSF and return (ground_indices, non_ground_indices).
|
|
20
|
+
|
|
21
|
+
Requires ``pip install CSF``.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
import CSF # type: ignore[import-untyped]
|
|
25
|
+
except ImportError as exc:
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"GroundSegmentationCSF requires the CSF package: pip install CSF"
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
csf = CSF.CSF()
|
|
31
|
+
csf.params.bSloopSmooth = False
|
|
32
|
+
csf.params.cloth_resolution = cloth_resolution
|
|
33
|
+
csf.params.rigidness = rigidness
|
|
34
|
+
csf.params.time_step = 0.65
|
|
35
|
+
csf.params.class_threshold = class_threshold
|
|
36
|
+
csf.params.interations = 500
|
|
37
|
+
|
|
38
|
+
csf.setPointCloud(xyz.tolist())
|
|
39
|
+
ground_idx = CSF.VecInt()
|
|
40
|
+
non_ground_idx = CSF.VecInt()
|
|
41
|
+
csf.do_filtering(ground_idx, non_ground_idx)
|
|
42
|
+
return list(ground_idx), list(non_ground_idx)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GroundSegmentationCSF(FramePreprocessor):
|
|
46
|
+
"""Per-point ground/non-ground labels via Cloth Simulation Filter.
|
|
47
|
+
|
|
48
|
+
Runs CSF on the voxelised point cloud. More accurate than RANSAC on
|
|
49
|
+
uneven terrain; requires ``pip install CSF``.
|
|
50
|
+
|
|
51
|
+
Output labels: **0 = ground, 1 = non-ground**.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
voxelised_key: Input channel (voxelised point cloud, shape (N, D≥3)).
|
|
55
|
+
cloth_resolution: CSF cloth resolution (metres). Default 0.5 m.
|
|
56
|
+
class_threshold: CSF point-to-cloth distance threshold (metres).
|
|
57
|
+
Default 0.5 m.
|
|
58
|
+
rigidness: CSF cloth rigidness (1–3). 3 = flat terrain.
|
|
59
|
+
Default 3.
|
|
60
|
+
output_key: Override the default channel name ``"ground_csf"``.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
output_key: ClassVar[str] = "ground_csf"
|
|
64
|
+
input_keys: ClassVar[list[str]] = ["voxelised"]
|
|
65
|
+
timestamps_from: ClassVar[str] = "voxelised"
|
|
66
|
+
sources: ClassVar[list[str]] = ["voxelised"]
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
voxelised_key: str = "voxelised",
|
|
71
|
+
cloth_resolution: float = 0.5,
|
|
72
|
+
class_threshold: float = 0.5,
|
|
73
|
+
rigidness: int = 3,
|
|
74
|
+
output_key: str | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
self._voxelised_key = voxelised_key
|
|
77
|
+
self._cloth_resolution = cloth_resolution
|
|
78
|
+
self._class_threshold = class_threshold
|
|
79
|
+
self._rigidness = rigidness
|
|
80
|
+
self.input_keys = [voxelised_key]
|
|
81
|
+
self.sources = [voxelised_key]
|
|
82
|
+
if output_key is not None:
|
|
83
|
+
self.output_key = output_key
|
|
84
|
+
|
|
85
|
+
def __call__(self, sample: Sample) -> np.ndarray:
|
|
86
|
+
xyz = np.asarray(sample.data[self._voxelised_key], dtype=np.float64)[:, :3]
|
|
87
|
+
ground_idx, _ = _run_csf(
|
|
88
|
+
xyz,
|
|
89
|
+
cloth_resolution=self._cloth_resolution,
|
|
90
|
+
class_threshold=self._class_threshold,
|
|
91
|
+
rigidness=self._rigidness,
|
|
92
|
+
)
|
|
93
|
+
labels = np.ones(len(xyz), dtype=np.uint8)
|
|
94
|
+
labels[ground_idx] = 0
|
|
95
|
+
return labels
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Ground segmentation from semantic class IDs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from apairo.core.preprocessor import FramePreprocessor
|
|
10
|
+
from apairo.core.sample import Sample
|
|
11
|
+
|
|
12
|
+
_RELLIS_GROUND_IDS: frozenset[int] = frozenset({1, 3, 10, 23, 31, 33})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GroundSegmentationFromLabels(FramePreprocessor):
|
|
16
|
+
"""Per-point ground/non-ground labels from semantic class IDs.
|
|
17
|
+
|
|
18
|
+
Each point is labelled ground (0) if its semantic class ID is in
|
|
19
|
+
``ground_ids``, non-ground (1) otherwise.
|
|
20
|
+
|
|
21
|
+
Output labels: **0 = ground, 1 = non-ground** — same convention as
|
|
22
|
+
``GroundSegmentationCSF`` and ``GroundSegmentationRANSAC``.
|
|
23
|
+
|
|
24
|
+
Default ground IDs for RELLIS-3D::
|
|
25
|
+
|
|
26
|
+
{1: dirt, 3: grass, 10: asphalt, 23: concrete, 31: puddle, 33: mud}
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
labels_key: Input channel for per-point semantic labels.
|
|
30
|
+
ground_ids: Set of semantic class IDs considered ground.
|
|
31
|
+
Defaults to the RELLIS-3D ground classes.
|
|
32
|
+
output_key: Override the default channel name ``"ground_labels"``.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
output_key: ClassVar[str] = "ground_labels"
|
|
36
|
+
input_keys: ClassVar[list[str]] = ["labels"]
|
|
37
|
+
timestamps_from: ClassVar[str] = "lidar"
|
|
38
|
+
sources: ClassVar[list[str]] = ["labels"]
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
labels_key: str = "labels",
|
|
43
|
+
ground_ids: frozenset[int] | None = None,
|
|
44
|
+
output_key: str | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._labels_key = labels_key
|
|
47
|
+
self._ground_ids = ground_ids if ground_ids is not None else _RELLIS_GROUND_IDS
|
|
48
|
+
self.input_keys = [labels_key]
|
|
49
|
+
self.sources = [labels_key]
|
|
50
|
+
if output_key is not None:
|
|
51
|
+
self.output_key = output_key
|
|
52
|
+
|
|
53
|
+
def __call__(self, sample: Sample) -> np.ndarray:
|
|
54
|
+
labels = np.asarray(sample.data[self._labels_key])
|
|
55
|
+
return (~np.isin(labels, list(self._ground_ids))).astype(np.uint8)
|