euler-preprocess 3.17.0__tar.gz → 3.18.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.
- euler_preprocess-3.18.0/CHANGELOG.md +14 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/PKG-INFO +36 -2
- euler_preprocess-3.17.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.18.0/README.md +34 -20
- euler_preprocess-3.18.0/docs/plans/coherent-cropping.md +446 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/cli.py +13 -1
- euler_preprocess-3.18.0/euler_preprocess/lidar_motion/compensation.py +78 -0
- euler_preprocess-3.18.0/euler_preprocess/lidar_motion/transform.py +162 -0
- euler_preprocess-3.18.0/euler_preprocess/sky_depth/__init__.py +0 -0
- euler_preprocess-3.17.0/README.md → euler_preprocess-3.18.0/euler_preprocess.egg-info/PKG-INFO +54 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess.egg-info/SOURCES.txt +5 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess.egg-info/requires.txt +1 -1
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/pyproject.toml +2 -2
- euler_preprocess-3.18.0/tests/test_lidar_motion.py +227 -0
- euler_preprocess-3.17.0/CHANGELOG.md +0 -5
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/LICENSE +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/MANIFEST.in +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/docs/spatial.md +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/color.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/dataset.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/intrinsics.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/normalize.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/output.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/common/transform.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/augmentations.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/capture.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/inference.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/logging.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/models.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/pipeline.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/fog/transform.py +0 -0
- {euler_preprocess-3.17.0/euler_preprocess/radial → euler_preprocess-3.18.0/euler_preprocess/lidar_motion}/__init__.py +0 -0
- {euler_preprocess-3.17.0/euler_preprocess/sky_depth → euler_preprocess-3.18.0/euler_preprocess/radial}/__init__.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/radial/transform.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/sky_depth/transform.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess/spatial.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/examples/spatial-five-field.json +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/setup.cfg +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_cli_sample_selection.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_fog_aux_outputs.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_fog_aware_auto_exposure.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_fog_input_normalization.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_fog_integration.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_modality_roles.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_radial.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_scene_illumination.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_sensor_identity.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_sky_depth.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_spatial.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_tone_map_lut.py +0 -0
- {euler_preprocess-3.17.0 → euler_preprocess-3.18.0}/tests/test_zip_output.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Add `euler-preprocess lidar-motion`, which compensates scanning-lidar points for
|
|
6
|
+
the vehicle's motion during the sweep. It follows the MUSES SDK, moving each
|
|
7
|
+
point to the frame camera's mid-exposure using the `gnss`, `sensor_timing`
|
|
8
|
+
and `lidar_to_gnss` modalities.
|
|
9
|
+
- Require euler-loading 2.32.0, the first release with `muses.gnss`,
|
|
10
|
+
`muses.sensor_timing`, `muses.read_lidar_to_gnss` and `muses.write_sparse_depth`.
|
|
11
|
+
|
|
12
|
+
## 3.14.0 (unreleased)
|
|
13
|
+
|
|
14
|
+
Consume shared preprocessing config and add the opt-in spatial producer, explicit variants/mappings and captured source-backed outputs. Preserve ordinary pipeline APIs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.18.0
|
|
4
4
|
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
5
|
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: numpy
|
|
11
11
|
Requires-Dist: Pillow
|
|
12
|
-
Requires-Dist: euler-loading>=2.
|
|
12
|
+
Requires-Dist: euler-loading>=2.32.0
|
|
13
13
|
Requires-Dist: ds-crawler>=2.11.0
|
|
14
14
|
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
15
15
|
Provides-Extra: gpu
|
|
@@ -64,6 +64,7 @@ qualitative review.
|
|
|
64
64
|
| `euler-preprocess fog` | Synthetic fog via the Koschmieder atmospheric scattering model |
|
|
65
65
|
| `euler-preprocess sky-depth` | Override depth values in sky regions with a constant |
|
|
66
66
|
| `euler-preprocess radial` | Convert planar (z-buffer) depth to radial (Euclidean) depth |
|
|
67
|
+
| `euler-preprocess lidar-motion` | Compensate scanning-lidar points for the vehicle's motion during a sweep |
|
|
67
68
|
|
|
68
69
|
## Installation
|
|
69
70
|
|
|
@@ -77,6 +78,7 @@ uv pip install "euler-preprocess[gpu,progress]"
|
|
|
77
78
|
euler-preprocess fog -c configs/example_dataset_config.json
|
|
78
79
|
euler-preprocess sky-depth -c configs/sky_depth_dataset_config.json
|
|
79
80
|
euler-preprocess radial -c configs/radial_dataset_config.json
|
|
81
|
+
euler-preprocess lidar-motion -c configs/lidar_motion_dataset_config.json
|
|
80
82
|
```
|
|
81
83
|
|
|
82
84
|
## Dataset Config
|
|
@@ -118,6 +120,7 @@ loader.
|
|
|
118
120
|
| `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
|
|
119
121
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
120
122
|
| `radial` | `depth`, `intrinsics` | — |
|
|
123
|
+
| `lidar-motion` | `sparse_depth`, `gnss`, `sensor_timing`, `lidar_to_gnss` | — |
|
|
121
124
|
|
|
122
125
|
`fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
|
|
123
126
|
it with the `sky_mask` function of the euler-loading module that the index's
|
|
@@ -703,6 +706,37 @@ Standalone `RadialTransform(...)` usage keeps the legacy `.npy` output behaviour
|
|
|
703
706
|
|
|
704
707
|
---
|
|
705
708
|
|
|
709
|
+
## LiDAR Motion Compensation
|
|
710
|
+
|
|
711
|
+
A scanning lidar measures its points one after another while the vehicle moves.
|
|
712
|
+
The MUSES lidar sweeps its field of view top to bottom in about 85 ms, so
|
|
713
|
+
uncorrected points projected into the frame camera match tall objects at their
|
|
714
|
+
base and drift away towards the top. Across MUSES, 92 % of frames are taken
|
|
715
|
+
above 10 km/h. `lidar-motion` moves every point to where it was at the
|
|
716
|
+
frame camera's mid-exposure. It follows the official MUSES SDK
|
|
717
|
+
(`motion_compensate_pcd`): constant ground speed along the GNSS frame's `x`,
|
|
718
|
+
constant roll, pitch and heading rates.
|
|
719
|
+
|
|
720
|
+
| Modality | Content | MUSES loader (euler-loading) |
|
|
721
|
+
|---|---|---|
|
|
722
|
+
| `sparse_depth` | `(N, C)` points with each point's time in seconds | `muses.sparse_depth` |
|
|
723
|
+
| `gnss` | The frame's GNSS/IMU record | `muses.gnss` |
|
|
724
|
+
| `sensor_timing` | `camera_exposure_start_us` / `camera_exposure_end_us` | `muses.sensor_timing` (`meta.json`) |
|
|
725
|
+
| `lidar_to_gnss` | `(4, 4)` lidar → GNSS/IMU transform | `muses.read_lidar_to_gnss` (`calib.json`) |
|
|
726
|
+
|
|
727
|
+
Index `gnss/` like the `lidar` folder, so samples join it by ID. Index
|
|
728
|
+
`meta.json` and `calib.json` as root-level files; the euler-loading docs
|
|
729
|
+
describe the MUSES ego-motion inputs. The transform config takes one optional
|
|
730
|
+
key, `timestamp_column` (default `5`, MUSES' `timestamp` column).
|
|
731
|
+
|
|
732
|
+
The output is a `sparse_depth` dataset mirroring the input's paths, file names
|
|
733
|
+
and `.bin` format. Only `x, y, z` change. The head records
|
|
734
|
+
`meta.motion_compensated`, and each file's `attributes.motion_compensation`
|
|
735
|
+
holds the target time and the speed and rates used. Point
|
|
736
|
+
`dmd3c-euler`, `euler-eval` or a crop at it in place of the raw lidar.
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
706
740
|
## License
|
|
707
741
|
|
|
708
742
|
[MIT](LICENSE) © Daniel Rothenpieler
|
euler_preprocess-3.17.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.18.0/README.md
RENAMED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: euler-preprocess
|
|
3
|
-
Version: 3.17.0
|
|
4
|
-
Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
|
|
5
|
-
Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.9
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: numpy
|
|
11
|
-
Requires-Dist: Pillow
|
|
12
|
-
Requires-Dist: euler-loading>=2.29.0
|
|
13
|
-
Requires-Dist: ds-crawler>=2.11.0
|
|
14
|
-
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
15
|
-
Provides-Extra: gpu
|
|
16
|
-
Requires-Dist: torch; extra == "gpu"
|
|
17
|
-
Provides-Extra: progress
|
|
18
|
-
Requires-Dist: tqdm; extra == "progress"
|
|
19
|
-
Dynamic: license-file
|
|
20
|
-
|
|
21
1
|
<!-- euler header — shared across the euler packages.
|
|
22
2
|
Per package, change only: the <h1>, the tagline, and the badge URLs. -->
|
|
23
3
|
<p align="center">
|
|
@@ -64,6 +44,7 @@ qualitative review.
|
|
|
64
44
|
| `euler-preprocess fog` | Synthetic fog via the Koschmieder atmospheric scattering model |
|
|
65
45
|
| `euler-preprocess sky-depth` | Override depth values in sky regions with a constant |
|
|
66
46
|
| `euler-preprocess radial` | Convert planar (z-buffer) depth to radial (Euclidean) depth |
|
|
47
|
+
| `euler-preprocess lidar-motion` | Compensate scanning-lidar points for the vehicle's motion during a sweep |
|
|
67
48
|
|
|
68
49
|
## Installation
|
|
69
50
|
|
|
@@ -77,6 +58,7 @@ uv pip install "euler-preprocess[gpu,progress]"
|
|
|
77
58
|
euler-preprocess fog -c configs/example_dataset_config.json
|
|
78
59
|
euler-preprocess sky-depth -c configs/sky_depth_dataset_config.json
|
|
79
60
|
euler-preprocess radial -c configs/radial_dataset_config.json
|
|
61
|
+
euler-preprocess lidar-motion -c configs/lidar_motion_dataset_config.json
|
|
80
62
|
```
|
|
81
63
|
|
|
82
64
|
## Dataset Config
|
|
@@ -118,6 +100,7 @@ loader.
|
|
|
118
100
|
| `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
|
|
119
101
|
| `sky-depth` | `depth`, `semantic_segmentation` | — |
|
|
120
102
|
| `radial` | `depth`, `intrinsics` | — |
|
|
103
|
+
| `lidar-motion` | `sparse_depth`, `gnss`, `sensor_timing`, `lidar_to_gnss` | — |
|
|
121
104
|
|
|
122
105
|
`fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
|
|
123
106
|
it with the `sky_mask` function of the euler-loading module that the index's
|
|
@@ -703,6 +686,37 @@ Standalone `RadialTransform(...)` usage keeps the legacy `.npy` output behaviour
|
|
|
703
686
|
|
|
704
687
|
---
|
|
705
688
|
|
|
689
|
+
## LiDAR Motion Compensation
|
|
690
|
+
|
|
691
|
+
A scanning lidar measures its points one after another while the vehicle moves.
|
|
692
|
+
The MUSES lidar sweeps its field of view top to bottom in about 85 ms, so
|
|
693
|
+
uncorrected points projected into the frame camera match tall objects at their
|
|
694
|
+
base and drift away towards the top. Across MUSES, 92 % of frames are taken
|
|
695
|
+
above 10 km/h. `lidar-motion` moves every point to where it was at the
|
|
696
|
+
frame camera's mid-exposure. It follows the official MUSES SDK
|
|
697
|
+
(`motion_compensate_pcd`): constant ground speed along the GNSS frame's `x`,
|
|
698
|
+
constant roll, pitch and heading rates.
|
|
699
|
+
|
|
700
|
+
| Modality | Content | MUSES loader (euler-loading) |
|
|
701
|
+
|---|---|---|
|
|
702
|
+
| `sparse_depth` | `(N, C)` points with each point's time in seconds | `muses.sparse_depth` |
|
|
703
|
+
| `gnss` | The frame's GNSS/IMU record | `muses.gnss` |
|
|
704
|
+
| `sensor_timing` | `camera_exposure_start_us` / `camera_exposure_end_us` | `muses.sensor_timing` (`meta.json`) |
|
|
705
|
+
| `lidar_to_gnss` | `(4, 4)` lidar → GNSS/IMU transform | `muses.read_lidar_to_gnss` (`calib.json`) |
|
|
706
|
+
|
|
707
|
+
Index `gnss/` like the `lidar` folder, so samples join it by ID. Index
|
|
708
|
+
`meta.json` and `calib.json` as root-level files; the euler-loading docs
|
|
709
|
+
describe the MUSES ego-motion inputs. The transform config takes one optional
|
|
710
|
+
key, `timestamp_column` (default `5`, MUSES' `timestamp` column).
|
|
711
|
+
|
|
712
|
+
The output is a `sparse_depth` dataset mirroring the input's paths, file names
|
|
713
|
+
and `.bin` format. Only `x, y, z` change. The head records
|
|
714
|
+
`meta.motion_compensated`, and each file's `attributes.motion_compensation`
|
|
715
|
+
holds the target time and the speed and rates used. Point
|
|
716
|
+
`dmd3c-euler`, `euler-eval` or a crop at it in place of the raw lidar.
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
706
720
|
## License
|
|
707
721
|
|
|
708
722
|
[MIT](LICENSE) © Daniel Rothenpieler
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# Coherent cropping plan
|
|
2
|
+
|
|
3
|
+
Status: plan only, nothing implemented yet. First target: **SeeingThroughFog**
|
|
4
|
+
(euler-loading's `princeton_dense` loader) with `rgb`, `sparse_depth`,
|
|
5
|
+
`intrinsics` and `camera_extrinsics`. Written 2026-11-06 against
|
|
6
|
+
euler-preprocess 3.17.0 and euler-loading 2.30.0.
|
|
7
|
+
|
|
8
|
+
## 1. Is there a pathway today?
|
|
9
|
+
|
|
10
|
+
Not one that covers RGB, sparse depth, intrinsics and extrinsics together.
|
|
11
|
+
|
|
12
|
+
| Path | What it does | Why it doesn't fit |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `euler-preprocess spatial` (`euler_preprocess/spatial.py`, `docs/spatial.md`) | Fixed-pixel crop/resize export with verified receipts | No percentages. Rejects passthrough fields (`euler_loading/transform_descriptors.py:174`), so lidar point clouds and extrinsics can't be exported. Writes NPY/PNG `materialized` datasets, not native formats. Needs a ~260-line binding config (`examples/spatial-five-field.json`). |
|
|
15
|
+
| `preprocessing` block in any dataset config (`euler_preprocess/common/dataset.py:221`) | Crops in memory before fog / sky-depth / radial run | Only the transform's own output is written, and it destroys sparse depth and extrinsics (table below). |
|
|
16
|
+
| `euler_loading.crop_intrinsics` / `geometry.crop_matrix` | The K maths | Building block only. |
|
|
17
|
+
|
|
18
|
+
Verified hazard (euler-loading 2.30.0,
|
|
19
|
+
`SamplePreprocessor.from_config({"crop": {"size": [576, 1024]}})` on one sample):
|
|
20
|
+
|
|
21
|
+
| Field | In | Out |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `rgb` | 720×1280×3 | 576×1024×3 ✓ |
|
|
24
|
+
| `intrinsics` (3×3) | cx 640 | cx 512 ✓ |
|
|
25
|
+
| `sparse_depth` (N, 6) | 120000×6 | **576×0** |
|
|
26
|
+
| `camera_extrinsics` (4×4) | 4×4 | **0×0** |
|
|
27
|
+
| `intrinsics` as `{file_id: K}` (hierarchical) | — | **AttributeError** |
|
|
28
|
+
|
|
29
|
+
Cause: the field kind is inferred from the name (`sparse_depth` → `depth`,
|
|
30
|
+
`preprocessing.py:100-112`) or from a 2-D shape (4×4 → `generic`, `:447`), and
|
|
31
|
+
`_crop_spatial_value` (`:695`) then slices axes 0 and 1 of any 2-D array.
|
|
32
|
+
|
|
33
|
+
## 2. What a crop changes
|
|
34
|
+
|
|
35
|
+
Box `(top, left, h, w)` in RGB pixels.
|
|
36
|
+
|
|
37
|
+
| Item | Correction |
|
|
38
|
+
|---|---|
|
|
39
|
+
| RGB, dense depth, sparse-depth rasters, masks | Slice. No resampling, so values are unchanged (planar and radial depth alike). |
|
|
40
|
+
| Pinhole matrix (`K`, or `P` for rectified images) | `K' = C·K` with `C = [[1,0,-left],[0,1,-top],[0,0,1]]`: `cx -= left`, `cy -= top`; fx, fy and skew unchanged |
|
|
41
|
+
| Distortion coefficients | Unchanged |
|
|
42
|
+
| Image-size metadata | Update: head `modality.meta.dimensions`, ROS `width`/`height`, Synscapes `resx`/`resy`, RDS `fov` |
|
|
43
|
+
| Extrinsics | **Unchanged** |
|
|
44
|
+
| Lidar point clouds | **Unchanged** |
|
|
45
|
+
|
|
46
|
+
The extrinsics need no correction because `u' = C·u = C·K·[R|t]·X = K'·[R|t]·X`:
|
|
47
|
+
the whole correction lives in the pixel matrix. The official SeeingThroughFog
|
|
48
|
+
tooling does exactly this. `resize.get_image_scaling()` returns
|
|
49
|
+
`[[sx,0,-sx·left],[0,sy,-sy·top],[0,0,1]]`, which is left-multiplied onto
|
|
50
|
+
`P·velodyne_to_camera`, and the lidar→camera transform is left alone
|
|
51
|
+
(`tools/CreateTFRecords/generic_tf_tools/resize.py:173`,
|
|
52
|
+
`tools/ProjectionTools/Lidar2RGB/lib/visi.py:41`).
|
|
53
|
+
|
|
54
|
+
So the cropped bundle carries the extrinsics and point clouds as unchanged
|
|
55
|
+
copies. Consumers that project at load time (dmd3c-euler `project_sparse_depth`,
|
|
56
|
+
`euler_complete.py:175`, and euler-eval) then get exactly the crop of the
|
|
57
|
+
original projection, bounded by the cropped RGB's h×w.
|
|
58
|
+
|
|
59
|
+
Later datasets have one exception. Where a single file holds both K and the
|
|
60
|
+
extrinsics (Real Drive Sim and MUSES `calib.json`, Synscapes `meta/<id>.json`),
|
|
61
|
+
the extrinsics output must be the patched file too. Otherwise the bundle carries
|
|
62
|
+
two different K. SeeingThroughFog keeps them in separate files.
|
|
63
|
+
|
|
64
|
+
## 3. SeeingThroughFog
|
|
65
|
+
|
|
66
|
+
### 3.1 Inputs and outputs
|
|
67
|
+
|
|
68
|
+
| Modality | Source (paths illustrative) | Loader (`princeton_dense`) | Stored as | Crop role | Output |
|
|
69
|
+
|---|---|---|---|---|---|
|
|
70
|
+
| `rgb` | `cam_stereo_left_lut/*.png` | `rgb` (HWC float32) | 8-bit PNG, 1920×1024, rectified | raster | Cropped PNG via `write_rgb`; the round trip is bit-exact (checked) |
|
|
71
|
+
| `sparse_depth` | `lidar_hdl64_strongest/*.bin` (or `_last`) | `sparse_depth` (`NC`) | (N, 5) float32 `x, y, z, intensity, ring` in `lidar_hdl64_s3_roof` | point set | Byte copy |
|
|
72
|
+
| `intrinsics` | `calib_cam_stereo_left.json`, one file, matched hierarchically | `read_intrinsics` (3×3) | ROS CameraInfo: `K, D, R, P, width, height, roi, …` | intrinsics | Patched copy via `princeton_dense.write_intrinsics`: K and P `cx`/`cy`, `width`, `height` |
|
|
73
|
+
| `camera_extrinsics` | `calib_tf_tree_full.json`, one file | `read_extrinsics` (4×4, HDL64 → `cam_stereo_left_optical`) | ROS TF list | unchanged | Byte copy |
|
|
74
|
+
|
|
75
|
+
The byte copies need no writer. `princeton_dense.write_intrinsics` now exists
|
|
76
|
+
in the euler-loading working tree (§3.2), so the intrinsics patch is covered.
|
|
77
|
+
|
|
78
|
+
### 3.2 Fixed in euler-loading (not yet committed): `read_intrinsics` returned the wrong matrix for `_lut` images
|
|
79
|
+
|
|
80
|
+
- `princeton_dense.read_intrinsics` returns `K`
|
|
81
|
+
(`euler_loading/loaders/_princeton_dense.py:170`).
|
|
82
|
+
- The `cam_stereo_left_lut` PNGs are rectified to `P`. The official conversion
|
|
83
|
+
calls `rectifyImage` → `cv2.initUndistortRectifyMap(K, D, R, P)`
|
|
84
|
+
(`tools/Raw2LUTImages/conversion_lib/process.py:115`,
|
|
85
|
+
`pinhole_camera_model.py:125`). The official lidar projection uses `P`:
|
|
86
|
+
`vtc = P @ R @ velodyne_to_camera` with `R = I`
|
|
87
|
+
(`tools/DatasetViewer/lib/read.py:176-183`).
|
|
88
|
+
- The two matrices differ a lot: fx 2612.86 vs 2355.72, cx 966.53 vs 988.14.
|
|
89
|
+
Projecting lidar with `K` into a `_lut` image misplaces points by −21.6 px at
|
|
90
|
+
the principal point, −129 px at the left edge, +80 px at the right edge and
|
|
91
|
+
about ±55 px at the top and bottom.
|
|
92
|
+
- This is independent of cropping, but a crop corrects whichever matrix the
|
|
93
|
+
loader returns, so it had to be settled first.
|
|
94
|
+
- Done in the euler-loading working tree: `read_intrinsics` now returns the
|
|
95
|
+
rectified `P[:, :3]` by default (a behaviour change, in its CHANGELOG).
|
|
96
|
+
`intrinsics_matrix: "K"` (alias `"raw"`) in per-file attributes or head
|
|
97
|
+
meta selects the raw K.
|
|
98
|
+
- `write_intrinsics` writes that matrix back into a camera-info JSON. An
|
|
99
|
+
existing file is updated: the change from the stored matrix must be a crop
|
|
100
|
+
or rescale, and it is applied to K and P alike (whole-pixel crops as exact
|
|
101
|
+
float64 integer shifts). D, R, roi and header are kept; `meta["width"]` and
|
|
102
|
+
`meta["height"]` set the size. Any other change is refused. New files and
|
|
103
|
+
streams get an ideal pinhole camera.
|
|
104
|
+
- `K` (with `D`) belongs only to the raw 12-bit `cam_stereo_left` TIFFs (`rccb`).
|
|
105
|
+
- `R` (a 0.256° rotation, about 10 px) is left out, as the official tools and
|
|
106
|
+
their KITTI-format calibration (`R0_rect = I`) do. On two real dense-fog
|
|
107
|
+
frames (2018-10-08_08-18-59_00650 / 01050), a road-marking correlation scan
|
|
108
|
+
puts the best fit for `P` at (−3, +8) px, against (+7, +13) px for `P·R`.
|
|
109
|
+
The raw TIFFs agree independently: `K+D` with `Rᵀ` is closer than plain
|
|
110
|
+
`K+D` (12 vs 24 px). So the TF camera frame is the rectified one. Both image
|
|
111
|
+
types keep a ~+8–10 px vertical residual, which looks like lidar/camera
|
|
112
|
+
timing or a small pitch error. Re-check on static, clear-weather frames.
|
|
113
|
+
Plain `K` is off by 26 px and visibly misses a road sign by about 45 px.
|
|
114
|
+
|
|
115
|
+
### 3.3 Other STF pitfalls
|
|
116
|
+
|
|
117
|
+
- The `lidar_hdl64_*_stereo_left` and `*_gated` point clouds are already in a
|
|
118
|
+
camera frame (official README table). Don't pair them with the default
|
|
119
|
+
HDL64→camera extrinsics; use `lidar_hdl64_strongest` or `_last`.
|
|
120
|
+
- `rccb` (raw Bayer TIFF) has no writer. A crop would need even offsets to keep
|
|
121
|
+
the Bayer phase, and demosaicing makes the 1-px border differ from cropping
|
|
122
|
+
the demosaiced image. Out of scope for phase 1.
|
|
123
|
+
- Raw TIFF datasets (`princeton_stf+rgb`):
|
|
124
|
+
- `princeton_dense.rgb` silently returns an all-white image for them: Pillow
|
|
125
|
+
clips 12-bit data. Their head must name `rccb`.
|
|
126
|
+
- `rccb` swapped red and blue; it now demosaics as GRBG (fixed in the
|
|
127
|
+
euler-loading working tree).
|
|
128
|
+
- The raw frames are not pinhole images (k1 = −0.57), so use the `_lut`
|
|
129
|
+
images for depth evaluation.
|
|
130
|
+
- The official crops are pixel boxes, e.g. `RGB2Gatedv2` keeps rows 202–970 and
|
|
131
|
+
columns 280–1560 (1280×768, `resize.py:91`). Accept pixel units alongside
|
|
132
|
+
fractions so these reproduce exactly.
|
|
133
|
+
- Invalid rectification border. By the calibration, 5.1 % of each `_lut` frame
|
|
134
|
+
samples outside the raw sensor: bands at the middle of the top, bottom and
|
|
135
|
+
left edges. OpenCV's default `remap` fills those with black.
|
|
136
|
+
`remove: {top: 34, bottom: 46, left: 16, right: 0}` (pixel units) leaves a
|
|
137
|
+
fully valid 1904×944 image (91.4 % of the frame). That's a sensible default
|
|
138
|
+
crop for depth evaluation, or else mask those pixels. Computed from
|
|
139
|
+
`calib_cam_stereo_left.json` and the official conversion code; confirm on a
|
|
140
|
+
real frame.
|
|
141
|
+
- Resampling in the official rectification is mild but uneven: about 1.11 raw
|
|
142
|
+
px per output px at the centre, but 0.76–0.81 near the left and right edges
|
|
143
|
+
(≈1.3× upsampled, so softer there). The `_lut` tone mapping also applies
|
|
144
|
+
CLAHE, which raises local contrast in fog. A pinhole image re-rendered from
|
|
145
|
+
the raw TIFFs with the same K, D, R and P keeps the geometry (and `P`) but
|
|
146
|
+
can avoid CLAHE. That's a photometric choice, not needed for geometric
|
|
147
|
+
correctness.
|
|
148
|
+
|
|
149
|
+
### 3.4 Phase 1 acceptance
|
|
150
|
+
|
|
151
|
+
Run on an STF-format fixture (official calibration file layout, image size
|
|
152
|
+
consistent with `width`/`height`), and optionally on real frames via
|
|
153
|
+
`EULER_PREPROCESS_STF_ROOT`, the same opt-in pattern as the RDS test.
|
|
154
|
+
|
|
155
|
+
1. `rgb_out == rgb_in[top:top+h, left:left+w]`, bit-exact.
|
|
156
|
+
2. The `.bin` files and `calib_tf_tree_full.json` are byte-identical.
|
|
157
|
+
3. In the calibration JSON, `K[2]` and `P[2]` drop by `left` and `K[5]` and
|
|
158
|
+
`P[6]` by `top`, computed in float64 (966.525 → 686.525, not 686.5250244).
|
|
159
|
+
`width`/`height` become w/h. `D`, `R`, `roi`, `header` and `binning_*` are
|
|
160
|
+
untouched.
|
|
161
|
+
4. Projecting the points with the cropped `P` and unchanged `T` into h×w equals
|
|
162
|
+
the crop of the uncropped projection. Compare pixel coordinates to 1e-6 px.
|
|
163
|
+
Rasters must match except for points within 1e-6 px of a rounding boundary,
|
|
164
|
+
because `np.rint` rounds half to even.
|
|
165
|
+
5. The output roots reload through `MultiModalDataset` with the roles
|
|
166
|
+
re-inferred (`sparse_depth` per sample, calibration hierarchical), and
|
|
167
|
+
`dmd3c-euler/euler_complete.py` runs on them unchanged.
|
|
168
|
+
|
|
169
|
+
## 4. Design
|
|
170
|
+
|
|
171
|
+
### 4.1 Command and config
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
euler-preprocess crop -c configs/crop_stf_dataset_config.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The dataset config keeps the existing shape:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"transform_config_path": "crop_config.json",
|
|
182
|
+
"output_path": "/data/stf_crop",
|
|
183
|
+
"modalities": {
|
|
184
|
+
"rgb": "/data/stf/cam_stereo_left_lut",
|
|
185
|
+
"sparse_depth": "/data/stf/lidar_hdl64_strongest",
|
|
186
|
+
"intrinsics": "/data/stf/calib_cam_stereo_left",
|
|
187
|
+
"camera_extrinsics": "/data/stf/calib_tf_tree"
|
|
188
|
+
},
|
|
189
|
+
"samples": {"start": 0, "count": 100}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Each modality is written to `<output_path>/<name>`, or wherever a
|
|
194
|
+
`pipeline.output_targets[]` entry with `slot: "<name>"` points (directory or
|
|
195
|
+
zip). A crop config containing `preprocessing` is rejected, since it would crop
|
|
196
|
+
twice.
|
|
197
|
+
|
|
198
|
+
`crop_config.json`:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"reference": "rgb",
|
|
203
|
+
"crop": {"remove": {"top": 0.20, "bottom": 0.05, "left": 0.15, "right": 0.19}},
|
|
204
|
+
"point_clouds": "keep",
|
|
205
|
+
"verify": "all"
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Other `crop` forms:
|
|
210
|
+
|
|
211
|
+
- `{"keep": {"height": 0.75, "width": 0.67}, "anchor": "bottom"}`. Anchors:
|
|
212
|
+
`center`, `top`, `bottom`, `left`, `right`, `top_left`, `top_right`,
|
|
213
|
+
`bottom_left`, `bottom_right`.
|
|
214
|
+
- `{"remove": {"top": 202, "bottom": 54, "left": 280, "right": 360}, "unit": "pixel"}`,
|
|
215
|
+
which equals `RGB2Gatedv2`.
|
|
216
|
+
- Optional `"multiple_of": 32` trims the kept size down to a multiple (DMD3C
|
|
217
|
+
pads to its stride today).
|
|
218
|
+
|
|
219
|
+
Fractions become pixels once per distinct reference size, and the result is
|
|
220
|
+
logged:
|
|
221
|
+
|
|
222
|
+
- `remove`: per edge, `px = floor(f·size + 0.5)`. That is round half up, not
|
|
223
|
+
Python's half-to-even `round`.
|
|
224
|
+
- `keep`: `out = floor(k·size + 0.5)`, with offsets from the anchor (center is
|
|
225
|
+
`(H - h) // 2`, as `euler_loading.Crop.resolve_box` does).
|
|
226
|
+
- Fractions must lie in [0, 1) and leave at least 1 px. A value like `10`, meant
|
|
227
|
+
as 10 %, is an error.
|
|
228
|
+
|
|
229
|
+
The resolved box is recorded per file (`attributes.crop`: `top`, `left`,
|
|
230
|
+
`height`, `width`, `source_height`, `source_width`) and in each output head's
|
|
231
|
+
meta.
|
|
232
|
+
|
|
233
|
+
### 4.2 Roles come from the loader annotation, not the name
|
|
234
|
+
|
|
235
|
+
Every euler-loading loader carries `@modality_meta(shape=…, type=…)`, available
|
|
236
|
+
as `loader._modality_meta`. The crop reads that and never guesses from the
|
|
237
|
+
modality name, which is what causes the hazard in §1.
|
|
238
|
+
|
|
239
|
+
| Annotated shape | Role | Operation |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `HW`, `1HW`, `HWC`, `CHW`, `3HW` | raster | Slice with the box; must match the reference H×W, else error |
|
|
242
|
+
| `NC` | point set | Byte copy (`prune_to_crop` later) |
|
|
243
|
+
| `3x3` with type `intrinsics` | intrinsics | `K' = crop_intrinsics(K, top, left)` |
|
|
244
|
+
| `4x4`, `Nx1` | extrinsics | Byte copy (patched copy where the file also holds K) |
|
|
245
|
+
| `dict` (Real Drive Sim `calibration`) | calibration map | Phase 2 |
|
|
246
|
+
| anything else | — | Error; override with `"roles": {"<name>": "raster" \| "copy"}` |
|
|
247
|
+
|
|
248
|
+
### 4.3 Writing
|
|
249
|
+
|
|
250
|
+
1. **Rasters**: a `SourceBackedOutputBackend` per modality, using the dataset's
|
|
251
|
+
writer. The head is mirrored from the source with
|
|
252
|
+
`modality.meta.dimensions` replaced (if present) and a `crop` record added.
|
|
253
|
+
This is the hook radial uses for `radial_depth` (`common/output.py:685-691`),
|
|
254
|
+
made independent of `transform_class`.
|
|
255
|
+
2. **Point sets**: a new byte-copy path. Read the source file (directory or
|
|
256
|
+
zip) and register it at the same full ID and basename through
|
|
257
|
+
`DatasetWriter.get_path` or `ZipDatasetWriter.write`, keeping its
|
|
258
|
+
attributes. Bit-exact, and no writer needed.
|
|
259
|
+
3. **Hierarchical calibration** (both STF calibration files): walk the
|
|
260
|
+
modality's own index (`collect_hierarchical_files`), not the samples. The
|
|
261
|
+
current backend can't write these (`common/output.py:463`): hierarchical
|
|
262
|
+
values arrive as `{file_id: value}` with no `sample["meta"]` entry
|
|
263
|
+
(euler-loading `dataset.py:1396`), and `write_sample` refuses them
|
|
264
|
+
(`dataset.py:1157`). For each file:
|
|
265
|
+
- collect the boxes of the selected samples that attach to it
|
|
266
|
+
(`match_hierarchical_files`);
|
|
267
|
+
- require exactly one box, else raise an error naming the conflicting sizes;
|
|
268
|
+
- write it once at its original full ID.
|
|
269
|
+
|
|
270
|
+
Files that no selected sample uses are skipped.
|
|
271
|
+
4. **Intrinsics files**: copy the source file to the destination (a temp file
|
|
272
|
+
for zip), then call the module's merge-capable
|
|
273
|
+
`write_intrinsics(dest, K', meta | {"width": w, "height": h})`. It derives
|
|
274
|
+
the crop from the stored matrix and rewrites only the camera fields;
|
|
275
|
+
Synscapes' writer already merges this way. Before writing, check that the
|
|
276
|
+
stored `width`/`height` equal the source RGB size, which catches a K
|
|
277
|
+
describing another resolution.
|
|
278
|
+
5. **Verify** (default: every file): reopen the output roots with
|
|
279
|
+
`build_dataset` and compare each sample with the cropped source. Rasters and
|
|
280
|
+
K' must be equal, and point sets and extrinsics identical. On a mismatch,
|
|
281
|
+
fail with the file name. This guards against writer surprises (§7).
|
|
282
|
+
|
|
283
|
+
### 4.4 Code layout (euler-preprocess)
|
|
284
|
+
|
|
285
|
+
- `euler_preprocess/crop/spec.py`: config parsing and box resolution (pure
|
|
286
|
+
functions).
|
|
287
|
+
- `euler_preprocess/crop/roles.py`: role from the loader annotation, plus
|
|
288
|
+
overrides.
|
|
289
|
+
- `euler_preprocess/crop/run.py`: sample loop, raster slicing (NumPy HWC or
|
|
290
|
+
torch CHW, from the annotation), hierarchical bookkeeping.
|
|
291
|
+
- `euler_preprocess/crop/calibration.py`: hierarchical writer (unique-box
|
|
292
|
+
check, copy then merge).
|
|
293
|
+
- `euler_preprocess/crop/verify.py`.
|
|
294
|
+
- `euler_preprocess/common/output.py`: byte-copy registration helper, and a
|
|
295
|
+
source-backed backend factory for any modality with head-meta overrides.
|
|
296
|
+
- `euler_preprocess/common/intrinsics.py`: `single_calibration(value)`. A
|
|
297
|
+
`{id: K}` with one entry yields K; several entries are an error. Today
|
|
298
|
+
`extract_intrinsics` only finds the file ID `"intrinsics"` (`:23`), so it
|
|
299
|
+
misses STF's `calib_cam_stereo_left`.
|
|
300
|
+
- `euler_preprocess/cli.py`: a `crop` subcommand with its own `_cmd_crop`, like
|
|
301
|
+
`_cmd_spatial`, because the config drives the modality set.
|
|
302
|
+
- `configs/crop_config.json`, `configs/crop_stf_dataset_config.json`, a README
|
|
303
|
+
section and a CHANGELOG entry. Bump `pyproject.toml` to
|
|
304
|
+
`euler-loading>=2.31.0`.
|
|
305
|
+
- Optional `tools/check_crop_projection.py`: overlay projected lidar on the
|
|
306
|
+
cropped RGB for a few frames, as a visual K-vs-P and crop check.
|
|
307
|
+
|
|
308
|
+
## 5. Work plan
|
|
309
|
+
|
|
310
|
+
### Phase 1: SeeingThroughFog end to end
|
|
311
|
+
|
|
312
|
+
euler-loading (done in the working tree, uncommitted; ships with the next
|
|
313
|
+
release):
|
|
314
|
+
|
|
315
|
+
1. `princeton_dense.write_intrinsics` (CPU and GPU share one function) merges
|
|
316
|
+
into an existing ROS CameraInfo JSON, as described in §3.2.
|
|
317
|
+
2. `princeton_dense.read_intrinsics` defaults to `P[:, :3]`, with an
|
|
318
|
+
`intrinsics_matrix` selector (`"P"` | `"K"`).
|
|
319
|
+
3. Tests in `tests/test_princeton_dense.py`, including a lidar projection
|
|
320
|
+
check that a crop needs no extrinsics change. The full suite passes.
|
|
321
|
+
|
|
322
|
+
euler-preprocess:
|
|
323
|
+
|
|
324
|
+
4. `spec.py` and tests.
|
|
325
|
+
5. `roles.py` and tests, including that `sparse_depth` and `camera_extrinsics`
|
|
326
|
+
are never sliced.
|
|
327
|
+
6. Output helpers (byte copy, head-meta overrides) and tests for directory and
|
|
328
|
+
zip outputs.
|
|
329
|
+
7. `run.py`, `calibration.py`, `verify.py` and the `crop` CLI.
|
|
330
|
+
8. The STF fixture test (§3.4), a hierarchical conflict test (two image sizes
|
|
331
|
+
under one calibration file → error), and the optional real-data test.
|
|
332
|
+
9. Docs and config examples.
|
|
333
|
+
|
|
334
|
+
### Phase 2: other euler-loading datasets, when needed
|
|
335
|
+
|
|
336
|
+
| Dataset | Missing |
|
|
337
|
+
|---|---|
|
|
338
|
+
| `muses` | `write_rgb`; merge `write_intrinsics` for `calib.json` `intrinsics[sensor].K`; extrinsics output = patched `calib.json`. The lidar must be ego-motion compensated first (see below). |
|
|
339
|
+
| `real_drive_sim` | Merge `write_intrinsics` for the calibration JSON (`CS_FRONT` fx/fy/cx/cy; `fov` is the horizontal FOV for 2048 px, so recompute or drop it); extrinsics output = patched file; RGBA sources become RGB PNGs |
|
|
340
|
+
| `vkitti2` | `write_intrinsics` must keep every frame/camera row (it writes one today); `read_extrinsics` header bug (§7) |
|
|
341
|
+
| `synscapes` | Nothing new (merge writers exist; pass `resx`/`resy`); extrinsics output = patched meta JSON |
|
|
342
|
+
| `generic_dense_depth` | K lives in the head's `meta["intrinsics"]`, so patch the output head instead; PNG depth with a scale override can't be re-encoded (§7), so it needs a format-level crop |
|
|
343
|
+
| `generic` | Nothing |
|
|
344
|
+
|
|
345
|
+
Also in phase 2: a format-level raster crop (PNG/TIFF via Pillow, NPY/NPZ via
|
|
346
|
+
NumPy, preserving mode, dtype and keys) for formats whose writers are missing or
|
|
347
|
+
lossy.
|
|
348
|
+
|
|
349
|
+
**MUSES lidar needs motion compensation.** The sensor is not a spinning lidar.
|
|
350
|
+
It scans a ~124° × 25° forward window top to bottom in 84.5 ms: point
|
|
351
|
+
timestamps correlate −0.86 to −0.94 with elevation, and there are 5 rings.
|
|
352
|
+
euler-loading's `muses.sparse_depth` returns the raw points, so a moving car
|
|
353
|
+
smears them. Upright poles in real frames lean by 0.1–0.2 m (forward motion,
|
|
354
|
+
about 15–30 km/h) or about 1.5° (yaw while turning). That's the "matches at
|
|
355
|
+
the bottom, misses at the top" seen in projections, about 20–30 px.
|
|
356
|
+
|
|
357
|
+
The official SDK (`timbroed/MUSES`, `processing/utils.py:motion_compensate_pcd`)
|
|
358
|
+
moves each point to mid-exposure of the camera. It uses GNSS `gSpeed` and the
|
|
359
|
+
angular rates (`meta.json`, `gnss/`, `calib.json:lidar2gnss`) and writes the
|
|
360
|
+
same `(N, 6)` `.bin`.
|
|
361
|
+
|
|
362
|
+
Verified with the real side data:
|
|
363
|
+
- A port of it straightens the poles (lean 0.12–0.25 m → ≤0.06 m), and the
|
|
364
|
+
projections land on the poles in both a straight-ahead and a turning frame.
|
|
365
|
+
- Projecting with `K` + `D` is clearly wrong, so the released frame-camera
|
|
366
|
+
PNGs are already undistorted and plain `K` is right.
|
|
367
|
+
- Across all 2500 frames: median 29 km/h; 92 % above 10 km/h; 20 % turning
|
|
368
|
+
faster than 5°/s; the camera fires 29–89 % of the way through the sweep.
|
|
369
|
+
- The GNSS timestamp precedes mid-exposure by up to 16.6 ms, so `meta.json`'s
|
|
370
|
+
exposure times are needed.
|
|
371
|
+
|
|
372
|
+
Implemented (2026-11-07; uncommitted in both repos). The side data became
|
|
373
|
+
ordinary euler-loading modalities, consumed by euler-preprocess. ds-crawler is
|
|
374
|
+
unchanged: it only interprets paths, never file contents.
|
|
375
|
+
|
|
376
|
+
| Modality | ds-crawler indexing | euler-loading |
|
|
377
|
+
|---|---|---|
|
|
378
|
+
| `gnss` | `gnss/` as its own dataset, same hierarchy and frame-number ID regex as `lidar`, so it joins per sample. `include_regex` `_gnss\.txt$` leaves out `dataformat.txt`. | `muses.gnss`: the u-blox record in physical units |
|
|
379
|
+
| `sensor_timing` | `meta.json` as a root-level file (like `calib.json`), `include_regex` `^meta\.json$` | `muses.sensor_timing` decodes it once. A new generic hook, `selects_per_sample` / `SampleContext`, gives each sample its record, matched through the `path_to_*` file names `meta.json` lists. |
|
|
380
|
+
| `lidar_to_gnss` | `calib.json` indexed once more | `muses.read_lidar_to_gnss` (`extrinsics.lidar2gnss`) |
|
|
381
|
+
| output | — | `muses.write_sparse_depth`: `(N, 6)` float64 `.bin` |
|
|
382
|
+
|
|
383
|
+
`euler-preprocess lidar-motion -c <config>` reads the four modalities per
|
|
384
|
+
sample. It writes a `sparse_depth` dataset mirroring the input paths, with
|
|
385
|
+
`meta.motion_compensated` and per-file `attributes.motion_compensation`.
|
|
386
|
+
|
|
387
|
+
Results:
|
|
388
|
+
- **Tests:** euler-loading 891 passed; euler-preprocess 215 passed.
|
|
389
|
+
- **Real data:** on the three real frames the CLI output reproduces the
|
|
390
|
+
verified pole straightening.
|
|
391
|
+
|
|
392
|
+
Registered in euler-dataset-contract (uncommitted) as `kinematics.platform.state`,
|
|
393
|
+
`timing.platform.acquisition` and `geometry.platform.extrinsics`. Still open:
|
|
394
|
+
release euler-loading with these loaders, then raise euler-preprocess's
|
|
395
|
+
`euler-loading` pin.
|
|
396
|
+
|
|
397
|
+
The side data stays with the local dataset: MUSES license §2.3 forbids
|
|
398
|
+
distributing it.
|
|
399
|
+
|
|
400
|
+
### Phase 3: optional
|
|
401
|
+
|
|
402
|
+
- `point_clouds: "prune_to_crop"`: drop points that project outside the box.
|
|
403
|
+
This needs sparse-depth writers, is lossy, and must use the same projection as
|
|
404
|
+
the consumers.
|
|
405
|
+
- A single scoped output root (one `metadata_scope` per modality), matching
|
|
406
|
+
dmd3c's `--dataset-root`.
|
|
407
|
+
- Resize, with the same maths: `K' = S·C·K`. The official `Sweden2PSMNet` mode
|
|
408
|
+
halves 1920×1024.
|
|
409
|
+
- A percentage helper for `spatial` that emits `crop: {size, offset}`.
|
|
410
|
+
|
|
411
|
+
## 6. Decisions needed
|
|
412
|
+
|
|
413
|
+
| # | Decision | Recommendation |
|
|
414
|
+
|---|---|---|
|
|
415
|
+
| D1 | STF matrix | Resolved: the default is now `P`, with `intrinsics_matrix: "K"` for the raw matrix |
|
|
416
|
+
| D2 | Crop semantics: does "percentage" mean the share removed or the share kept? | Support both (`remove` per edge, `keep` + `anchor`) plus `unit: "pixel"`, with no default guessing |
|
|
417
|
+
| D3 | Point clouds: keep every point or prune to the crop | Keep: lossless, and consumers already clip to the image |
|
|
418
|
+
| D4 | Where calibration writers live | euler-loading, next to the loaders that parse the same formats |
|
|
419
|
+
| D5 | Output layout: one root per modality or one scoped root | One root per modality (current convention); scoped root in phase 3 |
|
|
420
|
+
| D6 | Build on `spatial`, or a separate source-backed `crop` command | Separate `crop`. Extending `spatial` first needs contract support for passthrough fields. Reuse `euler_loading.crop_intrinsics` so the K maths matches. |
|
|
421
|
+
|
|
422
|
+
## 7. Pre-existing issues found while planning
|
|
423
|
+
|
|
424
|
+
1. `SamplePreprocessor` crops silently destroy point clouds and 4×4 extrinsics
|
|
425
|
+
(§1). Anyone using a `preprocessing` block with STF lidar is affected. Fix in
|
|
426
|
+
euler-loading: crop or resize only values whose spatial size matches the
|
|
427
|
+
reference, or classify fields from the loader annotations.
|
|
428
|
+
2. `princeton_dense.read_intrinsics` returned raw `K` for rectified `_lut`
|
|
429
|
+
images (§3.2). Fixed in the euler-loading working tree.
|
|
430
|
+
3. `generic_dense_depth` PNG depth with `scale_to_meters_override` doesn't
|
|
431
|
+
survive decode → write: 10.0 m reloads as 0.039 m. The loader applies the
|
|
432
|
+
scale (`generic_dense_depth.py:133`), `write_depth` (`:226`) writes
|
|
433
|
+
`rint(metres)` as uint16, and the output entry keeps the override. The same
|
|
434
|
+
applies to sky-depth and radial on such data.
|
|
435
|
+
4. `vkitti2.read_extrinsics` calls `np.loadtxt(path)` without `skiprows`
|
|
436
|
+
(`vkitti2.py:208`), so it raises on the documented header line
|
|
437
|
+
`frame cameraID r1,1 …`. This is also open question 4 in
|
|
438
|
+
`euler-dataset-contract/docs/ecosystem-streamlining-plan.md`.
|
|
439
|
+
5. `extract_intrinsics` only resolves hierarchical intrinsics whose file ID is
|
|
440
|
+
`"intrinsics"`, so radial and fog miss STF's `calib_cam_stereo_left`.
|
|
441
|
+
6. RGB writers always emit PNG bytes. VKITTI2 `.jpg` sources become PNG data
|
|
442
|
+
under `.jpg` names, and Real Drive Sim RGBA sources lose their alpha channel.
|
|
443
|
+
7. `princeton_dense.rccb` demosaiced as GBRG, swapping red and blue. Fixed in
|
|
444
|
+
the euler-loading working tree (GRBG).
|
|
445
|
+
8. `princeton_dense.rgb` on a 12-bit TIFF returns an all-white image without
|
|
446
|
+
an error. Not fixed: a guard that raises would be a small follow-up.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""CLI entry point for euler-preprocess.
|
|
2
2
|
|
|
3
3
|
Used both by ``python main.py`` and by the installed ``euler-preprocess`` console
|
|
4
|
-
script. Supports subcommands: ``fog``, ``sky-depth``, ``radial
|
|
4
|
+
script. Supports subcommands: ``fog``, ``sky-depth``, ``radial``, ``lidar-motion``
|
|
5
|
+
and ``spatial``.
|
|
5
6
|
"""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
@@ -251,6 +252,11 @@ def _cmd_radial(args: argparse.Namespace) -> int:
|
|
|
251
252
|
return _run_transform(args, RadialTransform)
|
|
252
253
|
|
|
253
254
|
|
|
255
|
+
def _cmd_lidar_motion(args: argparse.Namespace) -> int:
|
|
256
|
+
from euler_preprocess.lidar_motion.transform import LidarMotionTransform
|
|
257
|
+
return _run_transform(args, LidarMotionTransform)
|
|
258
|
+
|
|
259
|
+
|
|
254
260
|
def _cmd_spatial(args: argparse.Namespace) -> int:
|
|
255
261
|
from euler_preprocess.spatial import export_spatial
|
|
256
262
|
|
|
@@ -305,6 +311,12 @@ def parse_args() -> argparse.Namespace:
|
|
|
305
311
|
)
|
|
306
312
|
radial_parser.set_defaults(func=_cmd_radial)
|
|
307
313
|
|
|
314
|
+
lidar_motion_parser = subparsers.add_parser(
|
|
315
|
+
"lidar-motion", parents=[common],
|
|
316
|
+
help="Compensate scanning-lidar points for ego motion during the sweep.",
|
|
317
|
+
)
|
|
318
|
+
lidar_motion_parser.set_defaults(func=_cmd_lidar_motion)
|
|
319
|
+
|
|
308
320
|
spatial_parser = subparsers.add_parser(
|
|
309
321
|
"spatial", parents=[common], help="Export deterministic crop/resize with verified execution receipts.",
|
|
310
322
|
)
|