euler-preprocess 3.17.0__tar.gz → 3.19.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 (84) hide show
  1. euler_preprocess-3.19.0/CHANGELOG.md +44 -0
  2. {euler_preprocess-3.17.0/euler_preprocess.egg-info → euler_preprocess-3.19.0}/PKG-INFO +144 -3
  3. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/README.md +141 -0
  4. euler_preprocess-3.19.0/docs/plans/coherent-cropping.md +521 -0
  5. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/cli.py +55 -1
  6. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/intrinsics.py +26 -6
  7. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/output.py +162 -0
  8. euler_preprocess-3.19.0/euler_preprocess/crop/__init__.py +1 -0
  9. euler_preprocess-3.19.0/euler_preprocess/crop/calibration.py +103 -0
  10. euler_preprocess-3.19.0/euler_preprocess/crop/files.py +84 -0
  11. euler_preprocess-3.19.0/euler_preprocess/crop/roles.py +169 -0
  12. euler_preprocess-3.19.0/euler_preprocess/crop/run.py +770 -0
  13. euler_preprocess-3.19.0/euler_preprocess/crop/spec.py +444 -0
  14. euler_preprocess-3.19.0/euler_preprocess/crop/verify.py +109 -0
  15. euler_preprocess-3.19.0/euler_preprocess/lidar_motion/compensation.py +78 -0
  16. euler_preprocess-3.19.0/euler_preprocess/lidar_motion/transform.py +162 -0
  17. euler_preprocess-3.19.0/euler_preprocess/sky_depth/__init__.py +0 -0
  18. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0/euler_preprocess.egg-info}/PKG-INFO +144 -3
  19. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess.egg-info/SOURCES.txt +16 -0
  20. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess.egg-info/requires.txt +2 -2
  21. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/pyproject.toml +3 -3
  22. euler_preprocess-3.19.0/tests/test_crop.py +423 -0
  23. euler_preprocess-3.19.0/tests/test_crop_roles.py +84 -0
  24. euler_preprocess-3.19.0/tests/test_crop_spec.py +195 -0
  25. euler_preprocess-3.19.0/tests/test_intrinsics.py +30 -0
  26. euler_preprocess-3.19.0/tests/test_lidar_motion.py +227 -0
  27. euler_preprocess-3.17.0/CHANGELOG.md +0 -5
  28. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/LICENSE +0 -0
  29. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/MANIFEST.in +0 -0
  30. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/docs/spatial.md +0 -0
  31. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/__init__.py +0 -0
  32. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/__init__.py +0 -0
  33. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/color.py +0 -0
  34. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/dataset.py +0 -0
  35. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/device.py +0 -0
  36. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/io.py +0 -0
  37. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/logging.py +0 -0
  38. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/noise.py +0 -0
  39. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/normalize.py +0 -0
  40. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/sampling.py +0 -0
  41. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/common/transform.py +0 -0
  42. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/__init__.py +0 -0
  43. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  44. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
  45. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/augmentations.py +0 -0
  46. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/capture.py +0 -0
  47. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  48. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  49. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  50. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  51. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/inference.py +0 -0
  52. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/logging.py +0 -0
  53. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/models.py +0 -0
  54. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/pipeline.py +0 -0
  55. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/fog/transform.py +0 -0
  56. {euler_preprocess-3.17.0/euler_preprocess/radial → euler_preprocess-3.19.0/euler_preprocess/lidar_motion}/__init__.py +0 -0
  57. {euler_preprocess-3.17.0/euler_preprocess/sky_depth → euler_preprocess-3.19.0/euler_preprocess/radial}/__init__.py +0 -0
  58. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/radial/transform.py +0 -0
  59. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/sky_depth/transform.py +0 -0
  60. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess/spatial.py +0 -0
  61. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  62. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  63. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  64. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/examples/spatial-five-field.json +0 -0
  65. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/setup.cfg +0 -0
  66. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_airlight_fallback.py +0 -0
  67. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_cli_sample_selection.py +0 -0
  68. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  69. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
  70. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_fog_aux_outputs.py +0 -0
  71. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_fog_aware_auto_exposure.py +0 -0
  72. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
  73. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_fog_input_normalization.py +0 -0
  74. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_fog_integration.py +0 -0
  75. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_modality_roles.py +0 -0
  76. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_radial.py +0 -0
  77. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
  78. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_scene_illumination.py +0 -0
  79. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_sensor_identity.py +0 -0
  80. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_sky_depth.py +0 -0
  81. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_source_backed_output.py +0 -0
  82. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_spatial.py +0 -0
  83. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_tone_map_lut.py +0 -0
  84. {euler_preprocess-3.17.0 → euler_preprocess-3.19.0}/tests/test_zip_output.py +0 -0
@@ -0,0 +1,44 @@
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
+ - Add `euler-preprocess crop`, which crops RGB, depth, intrinsics and
12
+ extrinsics coherently. The box is given as fractions or pixels, either removed
13
+ per edge or kept with an anchor, relative to a reference raster. Rasters are
14
+ sliced, pinhole intrinsics get `cx -= left`, `cy -= top` merged into a copy of
15
+ their file, and lidar point clouds and extrinsics are copied unchanged. Each
16
+ modality's treatment comes from its loader annotation, not its name. Shared
17
+ calibration files are written once, and samples with different crops cannot
18
+ share one. The outputs are reopened and checked against the crop.
19
+ `configs/crop_stf_valid_region_config.json` removes SeeingThroughFog's black
20
+ rectification border.
21
+ - `crop` phase 3:
22
+ - `point_clouds: "prune_to_crop"` keeps only the lidar points that project
23
+ into the output image (`z > 0`, `rint(K'·T·X)` inside), the projection
24
+ consumers such as dmd3c-euler use;
25
+ - `resize` (`size` or `scale`) after the crop, with `K' = S·C·K`; colour
26
+ images are interpolated bilinearly, all other rasters take the nearest
27
+ pixel;
28
+ - `"output_layout": "scoped"` writes one root (directory or zip) with one
29
+ metadata scope per modality, as dmd3c-euler's `--dataset-root` reads;
30
+ - every encoded raster or pruned cloud is decoded again and must reproduce
31
+ the values;
32
+ - `euler-preprocess crop-box` prints the pixel box and the matching
33
+ `spatial` preprocessing block.
34
+ - `crop` uses the new public euler-loading accessors (`get_loader_meta`,
35
+ `get_hierarchical_files`) and ds-crawler's `set_modality_meta` / `path_prefix`.
36
+ Requires euler-loading 2.33.0 and ds-crawler 2.14.0.
37
+ - `extract_intrinsics` (radial, fog) also uses a hierarchical intrinsics file
38
+ that is not named `intrinsics` when it is the only one, such as SeeingThroughFog's
39
+ `calib_cam_stereo_left`. Several matching files now raise instead of being
40
+ ignored.
41
+
42
+ ## 3.14.0 (unreleased)
43
+
44
+ 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.17.0
3
+ Version: 3.19.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,8 +9,8 @@ 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.29.0
13
- Requires-Dist: ds-crawler>=2.11.0
12
+ Requires-Dist: euler-loading>=2.33.0
13
+ Requires-Dist: ds-crawler>=2.14.0
14
14
  Requires-Dist: euler-dataset-contract>=0.5.0
15
15
  Provides-Extra: gpu
16
16
  Requires-Dist: torch; extra == "gpu"
@@ -64,6 +64,9 @@ 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 |
68
+ | `euler-preprocess crop` | Crop RGB, depth, intrinsics and extrinsics coherently |
69
+ | `euler-preprocess crop-box` | Print the pixel box a crop config gives for an image size |
67
70
 
68
71
  ## Installation
69
72
 
@@ -77,6 +80,8 @@ uv pip install "euler-preprocess[gpu,progress]"
77
80
  euler-preprocess fog -c configs/example_dataset_config.json
78
81
  euler-preprocess sky-depth -c configs/sky_depth_dataset_config.json
79
82
  euler-preprocess radial -c configs/radial_dataset_config.json
83
+ euler-preprocess lidar-motion -c configs/lidar_motion_dataset_config.json
84
+ euler-preprocess crop -c configs/crop_stf_dataset_config.json
80
85
  ```
81
86
 
82
87
  ## Dataset Config
@@ -118,6 +123,7 @@ loader.
118
123
  | `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
119
124
  | `sky-depth` | `depth`, `semantic_segmentation` | — |
120
125
  | `radial` | `depth`, `intrinsics` | — |
126
+ | `lidar-motion` | `sparse_depth`, `gnss`, `sensor_timing`, `lidar_to_gnss` | — |
121
127
 
122
128
  `fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
123
129
  it with the `sky_mask` function of the euler-loading module that the index's
@@ -703,6 +709,141 @@ Standalone `RadialTransform(...)` usage keeps the legacy `.npy` output behaviour
703
709
 
704
710
  ---
705
711
 
712
+ ## LiDAR Motion Compensation
713
+
714
+ A scanning lidar measures its points one after another while the vehicle moves.
715
+ The MUSES lidar sweeps its field of view top to bottom in about 85 ms, so
716
+ uncorrected points projected into the frame camera match tall objects at their
717
+ base and drift away towards the top. Across MUSES, 92 % of frames are taken
718
+ above 10 km/h. `lidar-motion` moves every point to where it was at the
719
+ frame camera's mid-exposure. It follows the official MUSES SDK
720
+ (`motion_compensate_pcd`): constant ground speed along the GNSS frame's `x`,
721
+ constant roll, pitch and heading rates.
722
+
723
+ | Modality | Content | MUSES loader (euler-loading) |
724
+ |---|---|---|
725
+ | `sparse_depth` | `(N, C)` points with each point's time in seconds | `muses.sparse_depth` |
726
+ | `gnss` | The frame's GNSS/IMU record | `muses.gnss` |
727
+ | `sensor_timing` | `camera_exposure_start_us` / `camera_exposure_end_us` | `muses.sensor_timing` (`meta.json`) |
728
+ | `lidar_to_gnss` | `(4, 4)` lidar → GNSS/IMU transform | `muses.read_lidar_to_gnss` (`calib.json`) |
729
+
730
+ Index `gnss/` like the `lidar` folder, so samples join it by ID. Index
731
+ `meta.json` and `calib.json` as root-level files; the euler-loading docs
732
+ describe the MUSES ego-motion inputs. The transform config takes one optional
733
+ key, `timestamp_column` (default `5`, MUSES' `timestamp` column).
734
+
735
+ The output is a `sparse_depth` dataset mirroring the input's paths, file names
736
+ and `.bin` format. Only `x, y, z` change. The head records
737
+ `meta.motion_compensated`, and each file's `attributes.motion_compensation`
738
+ holds the target time and the speed and rates used. Point
739
+ `dmd3c-euler`, `euler-eval` or a crop at it in place of the raw lidar.
740
+
741
+ ---
742
+
743
+ ## Coherent Crop
744
+
745
+ `crop` cuts the same box out of every image-shaped modality and corrects
746
+ everything that refers to the pixel grid, so a cropped bundle projects exactly
747
+ like the original:
748
+
749
+ | Modality kind | Crop |
750
+ |---|---|
751
+ | Rasters: RGB, dense depth, masks | Sliced; no resampling, so values are unchanged |
752
+ | Pinhole intrinsics `K` (or `P` for rectified images) | `cx -= left`, `cy -= top`; `fx`, `fy`, skew unchanged |
753
+ | Lidar point clouds | Copied byte for byte, or pruned to the points that project into the crop |
754
+ | Extrinsics | Copied byte for byte |
755
+
756
+ The extrinsics need no change, because `C·K·[R|t]·X = K'·[R|t]·X` with
757
+ `C = [[1,0,-left],[0,1,-top],[0,0,1]]`: the whole correction lives in the
758
+ camera matrix, which is also what the official SeeingThroughFog tools do. An
759
+ optional resize afterwards adds `S` (an `align_corners=False` rescale), so
760
+ `K' = S·C·K`, still with untouched extrinsics.
761
+ Which kind a modality is comes from its euler-loading loader annotation
762
+ (`shape`, `type`), never from its name.
763
+
764
+ The dataset config lists every modality to crop; whether a modality is
765
+ per-sample or hierarchical (shared calibration) is inferred from its index:
766
+
767
+ ```json
768
+ {
769
+ "transform_config_path": "crop_stf_valid_region_config.json",
770
+ "output_path": "/data/stf_crop",
771
+ "modalities": {
772
+ "rgb": "/data/stf/cam_stereo_left_lut",
773
+ "sparse_depth": "/data/stf/lidar_hdl64_strongest",
774
+ "intrinsics": "/data/stf/calib_cam_stereo_left",
775
+ "camera_extrinsics": "/data/stf/calib_tf_tree"
776
+ },
777
+ "samples": {"start": 0, "count": 100}
778
+ }
779
+ ```
780
+
781
+ Each modality is written to `<output_path>/<name>`, or to its
782
+ `pipeline.output_targets[]` entry (`slot` = the modality name; directory or zip).
783
+ With pipeline targets, every modality needs one and the outputs manifest lists
784
+ them all. `"output_layout": "scoped"` writes one root at `output_path` instead
785
+ (a directory or `.zip`): one ds-crawler metadata scope per modality, named after
786
+ it, with its files below `<name>/`. That is the layout dmd3c-euler's
787
+ `--dataset-root` reads. The dataset config must not contain `preprocessing`.
788
+
789
+ The crop config (`configs/crop_config.json`) describes the box relative to the
790
+ `reference` raster:
791
+
792
+ ```json
793
+ {
794
+ "reference": "rgb",
795
+ "crop": {"remove": {"top": 0.20, "bottom": 0.05, "left": 0.15, "right": 0.19}},
796
+ "point_clouds": "keep",
797
+ "verify": "all"
798
+ }
799
+ ```
800
+
801
+ | Key | Meaning |
802
+ |---|---|
803
+ | `crop.remove` | What to cut off each edge (`top`, `bottom`, `left`, `right`) |
804
+ | `crop.keep` + `crop.anchor` | The kept `height` / `width`, placed at `center` (default), `top`, `bottom`, `left`, `right`, `top_left`, `top_right`, `bottom_left` or `bottom_right` |
805
+ | `crop.unit` | `fraction` (default) or `pixel`. Fractions lie in `[0, 1)` (`keep`: `(0, 1]`) and round half up per edge: `floor(f · size + 0.5)` |
806
+ | `crop.multiple_of` | Trim the kept size down to a multiple, e.g. a network stride; the extra rows and columns come off both sides evenly |
807
+ | `resize` | Optional, after the crop: `{"size": [h, w]}` or `{"scale": 0.5}` (or `[sy, sx]`); `interpolation` maps modality names to `nearest`, `bilinear` or `bicubic` |
808
+ | `point_clouds` | `keep` (default): copy every point, consumers clip to the image. `prune_to_crop`: keep only the points with `z > 0` whose pixel `rint(K'·T·X)` lies in the output image, as consumers such as dmd3c-euler project them |
809
+ | `projection` | For `prune_to_crop`: the `intrinsics` and `extrinsics` modalities to project with, when the dataset has more than one candidate |
810
+ | `verify` | `all` (default), `none`, or a sample count: reopen the outputs and check every file against the crop (rasters and pruned clouds as encoded, intrinsics to 1e-3 px, copies byte-identical) |
811
+ | `roles` | Override a modality's kind with `raster` or `copy`, for loaders whose annotation the crop does not know |
812
+
813
+ Pixel units reproduce the official boxes exactly, e.g. `RGB2Gatedv2` is
814
+ `{"remove": {"top": 202, "bottom": 54, "left": 280, "right": 360}, "unit": "pixel"}`.
815
+ `configs/crop_stf_valid_region_config.json` removes the black border the
816
+ SeeingThroughFog rectification leaves on `_lut` frames (top 34, bottom 46,
817
+ left 16 px), keeping a fully valid 1904×944 image.
818
+
819
+ Rasters are re-encoded with the dataset's euler-loading writer, so a raster
820
+ modality needs one (SeeingThroughFog's raw `rccb` TIFFs have none). Each
821
+ encoded file is decoded again right away and must give back the cropped values:
822
+ exactly, or for an interpolated resize within half an 8-bit step. A resize
823
+ interpolates colour images (`rgb`, `rccb`) bilinearly and takes the nearest
824
+ pixel for everything else, so depth and label values stay real values;
825
+ sparse-depth rasters are not resized. Pruned point clouds need a writer too
826
+ (`princeton_dense.write_sparse_depth`, `muses.write_sparse_depth`). Intrinsics
827
+ are written by copying the source file and letting its writer move the matrices
828
+ onto the new grid: for a ROS CameraInfo file such as
829
+ `calib_cam_stereo_left.json`, `K` and `P` shift by exact integers, `width` and
830
+ `height` become the crop size, and `D`, `R`, `roi` and the header are kept. A
831
+ calibration file that records another image size than the reference rasters
832
+ is refused. A hierarchical file is written once, so every sample sharing it
833
+ must get the same box; mixed image sizes under one calibration file are an
834
+ error. Every output head records `meta.crop` (reference, spec and boxes), and
835
+ every file its own `attributes.crop` box; pruned clouds also record
836
+ `attributes.point_clouds` (source and kept point counts).
837
+
838
+ To reproduce a crop with `euler-preprocess spatial`, print its pixel box and
839
+ the matching `preprocessing` block for a reference size:
840
+
841
+ ```bash
842
+ euler-preprocess crop-box -c configs/crop_config.json --size 1024 1920
843
+ ```
844
+
845
+ ---
846
+
706
847
  ## License
707
848
 
708
849
  [MIT](LICENSE) © Daniel Rothenpieler
@@ -44,6 +44,9 @@ qualitative review.
44
44
  | `euler-preprocess fog` | Synthetic fog via the Koschmieder atmospheric scattering model |
45
45
  | `euler-preprocess sky-depth` | Override depth values in sky regions with a constant |
46
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 |
48
+ | `euler-preprocess crop` | Crop RGB, depth, intrinsics and extrinsics coherently |
49
+ | `euler-preprocess crop-box` | Print the pixel box a crop config gives for an image size |
47
50
 
48
51
  ## Installation
49
52
 
@@ -57,6 +60,8 @@ uv pip install "euler-preprocess[gpu,progress]"
57
60
  euler-preprocess fog -c configs/example_dataset_config.json
58
61
  euler-preprocess sky-depth -c configs/sky_depth_dataset_config.json
59
62
  euler-preprocess radial -c configs/radial_dataset_config.json
63
+ euler-preprocess lidar-motion -c configs/lidar_motion_dataset_config.json
64
+ euler-preprocess crop -c configs/crop_stf_dataset_config.json
60
65
  ```
61
66
 
62
67
  ## Dataset Config
@@ -98,6 +103,7 @@ loader.
98
103
  | `fog` | `rgb`, `depth`, `semantic_segmentation` | `intrinsics`; used for radial depth conversion and camera-profile optics |
99
104
  | `sky-depth` | `depth`, `semantic_segmentation` | — |
100
105
  | `radial` | `depth`, `intrinsics` | — |
106
+ | `lidar-motion` | `sparse_depth`, `gnss`, `sensor_timing`, `lidar_to_gnss` | — |
101
107
 
102
108
  `fog` and `sky-depth` need `semantic_segmentation` as a boolean sky mask. They load
103
109
  it with the `sky_mask` function of the euler-loading module that the index's
@@ -683,6 +689,141 @@ Standalone `RadialTransform(...)` usage keeps the legacy `.npy` output behaviour
683
689
 
684
690
  ---
685
691
 
692
+ ## LiDAR Motion Compensation
693
+
694
+ A scanning lidar measures its points one after another while the vehicle moves.
695
+ The MUSES lidar sweeps its field of view top to bottom in about 85 ms, so
696
+ uncorrected points projected into the frame camera match tall objects at their
697
+ base and drift away towards the top. Across MUSES, 92 % of frames are taken
698
+ above 10 km/h. `lidar-motion` moves every point to where it was at the
699
+ frame camera's mid-exposure. It follows the official MUSES SDK
700
+ (`motion_compensate_pcd`): constant ground speed along the GNSS frame's `x`,
701
+ constant roll, pitch and heading rates.
702
+
703
+ | Modality | Content | MUSES loader (euler-loading) |
704
+ |---|---|---|
705
+ | `sparse_depth` | `(N, C)` points with each point's time in seconds | `muses.sparse_depth` |
706
+ | `gnss` | The frame's GNSS/IMU record | `muses.gnss` |
707
+ | `sensor_timing` | `camera_exposure_start_us` / `camera_exposure_end_us` | `muses.sensor_timing` (`meta.json`) |
708
+ | `lidar_to_gnss` | `(4, 4)` lidar → GNSS/IMU transform | `muses.read_lidar_to_gnss` (`calib.json`) |
709
+
710
+ Index `gnss/` like the `lidar` folder, so samples join it by ID. Index
711
+ `meta.json` and `calib.json` as root-level files; the euler-loading docs
712
+ describe the MUSES ego-motion inputs. The transform config takes one optional
713
+ key, `timestamp_column` (default `5`, MUSES' `timestamp` column).
714
+
715
+ The output is a `sparse_depth` dataset mirroring the input's paths, file names
716
+ and `.bin` format. Only `x, y, z` change. The head records
717
+ `meta.motion_compensated`, and each file's `attributes.motion_compensation`
718
+ holds the target time and the speed and rates used. Point
719
+ `dmd3c-euler`, `euler-eval` or a crop at it in place of the raw lidar.
720
+
721
+ ---
722
+
723
+ ## Coherent Crop
724
+
725
+ `crop` cuts the same box out of every image-shaped modality and corrects
726
+ everything that refers to the pixel grid, so a cropped bundle projects exactly
727
+ like the original:
728
+
729
+ | Modality kind | Crop |
730
+ |---|---|
731
+ | Rasters: RGB, dense depth, masks | Sliced; no resampling, so values are unchanged |
732
+ | Pinhole intrinsics `K` (or `P` for rectified images) | `cx -= left`, `cy -= top`; `fx`, `fy`, skew unchanged |
733
+ | Lidar point clouds | Copied byte for byte, or pruned to the points that project into the crop |
734
+ | Extrinsics | Copied byte for byte |
735
+
736
+ The extrinsics need no change, because `C·K·[R|t]·X = K'·[R|t]·X` with
737
+ `C = [[1,0,-left],[0,1,-top],[0,0,1]]`: the whole correction lives in the
738
+ camera matrix, which is also what the official SeeingThroughFog tools do. An
739
+ optional resize afterwards adds `S` (an `align_corners=False` rescale), so
740
+ `K' = S·C·K`, still with untouched extrinsics.
741
+ Which kind a modality is comes from its euler-loading loader annotation
742
+ (`shape`, `type`), never from its name.
743
+
744
+ The dataset config lists every modality to crop; whether a modality is
745
+ per-sample or hierarchical (shared calibration) is inferred from its index:
746
+
747
+ ```json
748
+ {
749
+ "transform_config_path": "crop_stf_valid_region_config.json",
750
+ "output_path": "/data/stf_crop",
751
+ "modalities": {
752
+ "rgb": "/data/stf/cam_stereo_left_lut",
753
+ "sparse_depth": "/data/stf/lidar_hdl64_strongest",
754
+ "intrinsics": "/data/stf/calib_cam_stereo_left",
755
+ "camera_extrinsics": "/data/stf/calib_tf_tree"
756
+ },
757
+ "samples": {"start": 0, "count": 100}
758
+ }
759
+ ```
760
+
761
+ Each modality is written to `<output_path>/<name>`, or to its
762
+ `pipeline.output_targets[]` entry (`slot` = the modality name; directory or zip).
763
+ With pipeline targets, every modality needs one and the outputs manifest lists
764
+ them all. `"output_layout": "scoped"` writes one root at `output_path` instead
765
+ (a directory or `.zip`): one ds-crawler metadata scope per modality, named after
766
+ it, with its files below `<name>/`. That is the layout dmd3c-euler's
767
+ `--dataset-root` reads. The dataset config must not contain `preprocessing`.
768
+
769
+ The crop config (`configs/crop_config.json`) describes the box relative to the
770
+ `reference` raster:
771
+
772
+ ```json
773
+ {
774
+ "reference": "rgb",
775
+ "crop": {"remove": {"top": 0.20, "bottom": 0.05, "left": 0.15, "right": 0.19}},
776
+ "point_clouds": "keep",
777
+ "verify": "all"
778
+ }
779
+ ```
780
+
781
+ | Key | Meaning |
782
+ |---|---|
783
+ | `crop.remove` | What to cut off each edge (`top`, `bottom`, `left`, `right`) |
784
+ | `crop.keep` + `crop.anchor` | The kept `height` / `width`, placed at `center` (default), `top`, `bottom`, `left`, `right`, `top_left`, `top_right`, `bottom_left` or `bottom_right` |
785
+ | `crop.unit` | `fraction` (default) or `pixel`. Fractions lie in `[0, 1)` (`keep`: `(0, 1]`) and round half up per edge: `floor(f · size + 0.5)` |
786
+ | `crop.multiple_of` | Trim the kept size down to a multiple, e.g. a network stride; the extra rows and columns come off both sides evenly |
787
+ | `resize` | Optional, after the crop: `{"size": [h, w]}` or `{"scale": 0.5}` (or `[sy, sx]`); `interpolation` maps modality names to `nearest`, `bilinear` or `bicubic` |
788
+ | `point_clouds` | `keep` (default): copy every point, consumers clip to the image. `prune_to_crop`: keep only the points with `z > 0` whose pixel `rint(K'·T·X)` lies in the output image, as consumers such as dmd3c-euler project them |
789
+ | `projection` | For `prune_to_crop`: the `intrinsics` and `extrinsics` modalities to project with, when the dataset has more than one candidate |
790
+ | `verify` | `all` (default), `none`, or a sample count: reopen the outputs and check every file against the crop (rasters and pruned clouds as encoded, intrinsics to 1e-3 px, copies byte-identical) |
791
+ | `roles` | Override a modality's kind with `raster` or `copy`, for loaders whose annotation the crop does not know |
792
+
793
+ Pixel units reproduce the official boxes exactly, e.g. `RGB2Gatedv2` is
794
+ `{"remove": {"top": 202, "bottom": 54, "left": 280, "right": 360}, "unit": "pixel"}`.
795
+ `configs/crop_stf_valid_region_config.json` removes the black border the
796
+ SeeingThroughFog rectification leaves on `_lut` frames (top 34, bottom 46,
797
+ left 16 px), keeping a fully valid 1904×944 image.
798
+
799
+ Rasters are re-encoded with the dataset's euler-loading writer, so a raster
800
+ modality needs one (SeeingThroughFog's raw `rccb` TIFFs have none). Each
801
+ encoded file is decoded again right away and must give back the cropped values:
802
+ exactly, or for an interpolated resize within half an 8-bit step. A resize
803
+ interpolates colour images (`rgb`, `rccb`) bilinearly and takes the nearest
804
+ pixel for everything else, so depth and label values stay real values;
805
+ sparse-depth rasters are not resized. Pruned point clouds need a writer too
806
+ (`princeton_dense.write_sparse_depth`, `muses.write_sparse_depth`). Intrinsics
807
+ are written by copying the source file and letting its writer move the matrices
808
+ onto the new grid: for a ROS CameraInfo file such as
809
+ `calib_cam_stereo_left.json`, `K` and `P` shift by exact integers, `width` and
810
+ `height` become the crop size, and `D`, `R`, `roi` and the header are kept. A
811
+ calibration file that records another image size than the reference rasters
812
+ is refused. A hierarchical file is written once, so every sample sharing it
813
+ must get the same box; mixed image sizes under one calibration file are an
814
+ error. Every output head records `meta.crop` (reference, spec and boxes), and
815
+ every file its own `attributes.crop` box; pruned clouds also record
816
+ `attributes.point_clouds` (source and kept point counts).
817
+
818
+ To reproduce a crop with `euler-preprocess spatial`, print its pixel box and
819
+ the matching `preprocessing` block for a reference size:
820
+
821
+ ```bash
822
+ euler-preprocess crop-box -c configs/crop_config.json --size 1024 1920
823
+ ```
824
+
825
+ ---
826
+
686
827
  ## License
687
828
 
688
829
  [MIT](LICENSE) © Daniel Rothenpieler