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.
Files changed (44) hide show
  1. apairo_preprocess-0.2.0/LICENSE +21 -0
  2. apairo_preprocess-0.2.0/PKG-INFO +255 -0
  3. apairo_preprocess-0.2.0/README.md +223 -0
  4. apairo_preprocess-0.2.0/apairo_preprocess/__init__.py +27 -0
  5. apairo_preprocess-0.2.0/apairo_preprocess/ground/__init__.py +9 -0
  6. apairo_preprocess-0.2.0/apairo_preprocess/ground/csf.py +95 -0
  7. apairo_preprocess-0.2.0/apairo_preprocess/ground/from_labels.py +55 -0
  8. apairo_preprocess-0.2.0/apairo_preprocess/ground/ransac.py +95 -0
  9. apairo_preprocess-0.2.0/apairo_preprocess/odometry/__init__.py +4 -0
  10. apairo_preprocess-0.2.0/apairo_preprocess/odometry/gicp.py +96 -0
  11. apairo_preprocess-0.2.0/apairo_preprocess/odometry/kissicp.py +84 -0
  12. apairo_preprocess-0.2.0/apairo_preprocess/pointcloud/__init__.py +4 -0
  13. apairo_preprocess-0.2.0/apairo_preprocess/pointcloud/robot_filter.py +121 -0
  14. apairo_preprocess-0.2.0/apairo_preprocess/pointcloud/voxelise.py +281 -0
  15. apairo_preprocess-0.2.0/apairo_preprocess/priors/__init__.py +7 -0
  16. apairo_preprocess-0.2.0/apairo_preprocess/priors/ground_height.py +60 -0
  17. apairo_preprocess-0.2.0/apairo_preprocess/priors/trajectory_distance.py +97 -0
  18. apairo_preprocess-0.2.0/apairo_preprocess/projection/__init__.py +12 -0
  19. apairo_preprocess-0.2.0/apairo_preprocess/projection/bev.py +166 -0
  20. apairo_preprocess-0.2.0/apairo_preprocess/projection/image_mask.py +181 -0
  21. apairo_preprocess-0.2.0/apairo_preprocess/projection/lidar_to_camera.py +177 -0
  22. apairo_preprocess-0.2.0/apairo_preprocess/projection/point_features.py +100 -0
  23. apairo_preprocess-0.2.0/apairo_preprocess/traversability/__init__.py +7 -0
  24. apairo_preprocess-0.2.0/apairo_preprocess/traversability/from_labels.py +61 -0
  25. apairo_preprocess-0.2.0/apairo_preprocess/traversability/from_trajectory.py +181 -0
  26. apairo_preprocess-0.2.0/apairo_preprocess.egg-info/PKG-INFO +255 -0
  27. apairo_preprocess-0.2.0/apairo_preprocess.egg-info/SOURCES.txt +42 -0
  28. apairo_preprocess-0.2.0/apairo_preprocess.egg-info/dependency_links.txt +1 -0
  29. apairo_preprocess-0.2.0/apairo_preprocess.egg-info/requires.txt +10 -0
  30. apairo_preprocess-0.2.0/apairo_preprocess.egg-info/top_level.txt +1 -0
  31. apairo_preprocess-0.2.0/pyproject.toml +49 -0
  32. apairo_preprocess-0.2.0/setup.cfg +4 -0
  33. apairo_preprocess-0.2.0/tests/test_bev_rasterisation.py +154 -0
  34. apairo_preprocess-0.2.0/tests/test_call_contract.py +20 -0
  35. apairo_preprocess-0.2.0/tests/test_gicp_odometry.py +50 -0
  36. apairo_preprocess-0.2.0/tests/test_image_mask_from_point_labels.py +173 -0
  37. apairo_preprocess-0.2.0/tests/test_kissicp_odometry.py +58 -0
  38. apairo_preprocess-0.2.0/tests/test_lidar_camera_projection.py +207 -0
  39. apairo_preprocess-0.2.0/tests/test_point_features_from_image.py +101 -0
  40. apairo_preprocess-0.2.0/tests/test_projection_pipeline.py +162 -0
  41. apairo_preprocess-0.2.0/tests/test_remove_robot_points.py +213 -0
  42. apairo_preprocess-0.2.0/tests/test_traversability_from_labels.py +50 -0
  43. apairo_preprocess-0.2.0/tests/test_traversability_from_trajectory.py +188 -0
  44. 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)