euler-preprocess 3.19.0__tar.gz → 3.20.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.19.0 → euler_preprocess-3.20.0}/CHANGELOG.md +29 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/PKG-INFO +33 -5
- euler_preprocess-3.19.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.20.0/README.md +31 -23
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/docs/plans/coherent-cropping.md +87 -4
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/output.py +23 -28
- euler_preprocess-3.20.0/euler_preprocess/crop/label_files.py +183 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/roles.py +41 -2
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/run.py +163 -18
- euler_preprocess-3.19.0/README.md → euler_preprocess-3.20.0/euler_preprocess.egg-info/PKG-INFO +51 -3
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess.egg-info/SOURCES.txt +3 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess.egg-info/requires.txt +1 -1
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/pyproject.toml +2 -2
- euler_preprocess-3.20.0/tests/test_crop_labels.py +255 -0
- euler_preprocess-3.20.0/tests/test_crop_muses.py +267 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/LICENSE +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/MANIFEST.in +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/docs/spatial.md +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/cli.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/color.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/dataset.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/device.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/intrinsics.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/io.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/logging.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/noise.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/normalize.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/sampling.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/common/transform.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/calibration.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/files.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/spec.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/crop/verify.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/augmentations.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/capture.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/inference.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/logging.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/models.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/pipeline.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/fog/transform.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/lidar_motion/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/lidar_motion/compensation.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/lidar_motion/transform.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/radial/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/radial/transform.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/sky_depth/__init__.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/sky_depth/transform.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess/spatial.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/euler_preprocess.egg-info/top_level.txt +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/examples/spatial-five-field.json +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/setup.cfg +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_airlight_fallback.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_cli_sample_selection.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_crop.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_crop_roles.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_crop_spec.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_dcp_heuristic_airlight.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_dense_gloomy_daylight_config.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_fog_aux_outputs.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_fog_aware_auto_exposure.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_fog_cpu_gpu_parity.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_fog_input_normalization.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_fog_integration.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_intrinsics.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_lidar_motion.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_modality_roles.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_radial.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_real_drive_sim_scenario_profiles.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_scene_illumination.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_sensor_identity.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_sky_depth.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_source_backed_output.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_spatial.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_tone_map_lut.py +0 -0
- {euler_preprocess-3.19.0 → euler_preprocess-3.20.0}/tests/test_zip_output.py +0 -0
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- `crop` crops label rasters in the stored file: semantic segmentation (the
|
|
6
|
+
ground truth that also holds the sky), instance and panoptic segmentation,
|
|
7
|
+
and sky masks.
|
|
8
|
+
- PNG, TIFF and BMP keep their mode, palette, bit depth and every channel;
|
|
9
|
+
`.npy`/`.npz` keep their dtype and other arrays.
|
|
10
|
+
- The result must decode exactly as the crop of the source labels.
|
|
11
|
+
- So no class is lost to a writer that re-encodes only what one loader
|
|
12
|
+
function returns: Real Drive Sim's non-red channels, or every class but
|
|
13
|
+
the sky for a dataset read through `sky_mask`. Label modalities no longer
|
|
14
|
+
need a writer.
|
|
15
|
+
- Files that cannot be kept (JPEG, 16-bit colour PNGs) are re-encoded with
|
|
16
|
+
the writer, with a warning.
|
|
17
|
+
- Labels can only be resized with `nearest`; another `resize.interpolation`
|
|
18
|
+
for them is an error.
|
|
19
|
+
|
|
20
|
+
- `crop` supports MUSES (with euler-loading 2.35.0's MUSES writers):
|
|
21
|
+
- `calib.json` holds the frame camera's `K` and every extrinsic. The
|
|
22
|
+
`intrinsics` modality patches it once, and every other modality indexing
|
|
23
|
+
the same file, or a byte-identical copy (`camera_extrinsics`,
|
|
24
|
+
`lidar_to_gnss`), is written with the patched bytes, so the bundle holds
|
|
25
|
+
one `K`. What each of them reads from the patched file must stay as it was;
|
|
26
|
+
two intrinsics modalities patching one file differently are refused.
|
|
27
|
+
- GNSS records (`gnss`) and `meta.json` timing (`sensor_timing`) are copied
|
|
28
|
+
unchanged by default.
|
|
29
|
+
- `configs/crop_muses_hood_config.json` cuts the engine hood off the
|
|
30
|
+
1920×1080 frame camera images (bottom 160 rows, 1920×920 stay), with
|
|
31
|
+
`configs/crop_muses_dataset_config.json` as the dataset config.
|
|
32
|
+
- Require euler-loading 2.35.0 for the MUSES writers.
|
|
33
|
+
|
|
5
34
|
- Add `euler-preprocess lidar-motion`, which compensates scanning-lidar points for
|
|
6
35
|
the vehicle's motion during the sweep. It follows the MUSES SDK, moving each
|
|
7
36
|
point to the frame camera's mid-exposure using the `gnss`, `sensor_timing`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: euler-preprocess
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.20.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.35.0
|
|
13
13
|
Requires-Dist: ds-crawler>=2.14.0
|
|
14
14
|
Requires-Dist: euler-dataset-contract>=0.5.0
|
|
15
15
|
Provides-Extra: gpu
|
|
@@ -748,7 +748,8 @@ like the original:
|
|
|
748
748
|
|
|
749
749
|
| Modality kind | Crop |
|
|
750
750
|
|---|---|
|
|
751
|
-
| Rasters: RGB, dense depth
|
|
751
|
+
| Rasters: RGB, dense depth | Sliced; no resampling, so values are unchanged |
|
|
752
|
+
| Label rasters: semantic segmentation (the ground truth that also holds the sky), instance and panoptic segmentation, sky masks | Cropped in the stored file, so every class, channel, palette and bit depth is kept; resized only by nearest |
|
|
752
753
|
| Pinhole intrinsics `K` (or `P` for rectified images) | `cx -= left`, `cy -= top`; `fx`, `fy`, skew unchanged |
|
|
753
754
|
| Lidar point clouds | Copied byte for byte, or pruned to the points that project into the crop |
|
|
754
755
|
| Extrinsics | Copied byte for byte |
|
|
@@ -808,7 +809,7 @@ The crop config (`configs/crop_config.json`) describes the box relative to the
|
|
|
808
809
|
| `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
810
|
| `projection` | For `prune_to_crop`: the `intrinsics` and `extrinsics` modalities to project with, when the dataset has more than one candidate |
|
|
810
811
|
| `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
|
+
| `roles` | Override a modality's kind with `raster` or `copy`, for loaders whose annotation the crop does not know, or to copy an intrinsics file of another camera (e.g. MUSES `sensor: event`) instead of cropping it |
|
|
812
813
|
|
|
813
814
|
Pixel units reproduce the official boxes exactly, e.g. `RGB2Gatedv2` is
|
|
814
815
|
`{"remove": {"top": 202, "bottom": 54, "left": 280, "right": 360}, "unit": "pixel"}`.
|
|
@@ -816,7 +817,34 @@ Pixel units reproduce the official boxes exactly, e.g. `RGB2Gatedv2` is
|
|
|
816
817
|
SeeingThroughFog rectification leaves on `_lut` frames (top 34, bottom 46,
|
|
817
818
|
left 16 px), keeping a fully valid 1904×944 image.
|
|
818
819
|
|
|
819
|
-
|
|
820
|
+
MUSES keeps the frame camera's `K` and every extrinsic in one `calib.json`,
|
|
821
|
+
indexed as `intrinsics`, `camera_extrinsics` (and `lidar_to_gnss`). The crop
|
|
822
|
+
patches `intrinsics.rgb.K` once and writes that file for every modality that
|
|
823
|
+
indexes it, also when it is indexed as separate byte-identical copies, so the
|
|
824
|
+
bundle holds one `K`. `D`, the event camera and all extrinsics stay as they
|
|
825
|
+
are. `configs/crop_muses_hood_config.json` cuts the engine hood off the
|
|
826
|
+
1920×1080 frame camera images: the bottom 160 rows, leaving 1920×920. Rows come
|
|
827
|
+
off the bottom only, so `K` and every `calib.json` stay byte-identical. Run
|
|
828
|
+
`lidar-motion` first and crop its output (`configs/crop_muses_dataset_config.json`).
|
|
829
|
+
GNSS records and `meta.json` sensor timing are copied unchanged, like the
|
|
830
|
+
extrinsics. MUSES label images (`gt_semantic`, `gt_panoptic`) are cropped in place like
|
|
831
|
+
any label raster.
|
|
832
|
+
|
|
833
|
+
Label rasters (loader types `semantic_segmentation`, `semantic_segmentation_color`,
|
|
834
|
+
`instance_segmentation`, `panoptic_segmentation`, `sky_mask`) are cropped in the
|
|
835
|
+
file itself: PNG, TIFF and BMP through Pillow in their own mode and palette,
|
|
836
|
+
`.npy`/`.npz` through NumPy. A semantic segmentation is the ground truth for
|
|
837
|
+
every class including the sky, and consumers read it through different
|
|
838
|
+
functions (`class_segmentation`, or `sky_mask` for sky-depth). Re-encoding the
|
|
839
|
+
decoded labels would drop what one function does not return: Real Drive Sim's
|
|
840
|
+
non-red channels, an indexed PNG's palette, or every class but the sky when a
|
|
841
|
+
dataset only has a `sky_mask` function. The in-place crop must decode exactly
|
|
842
|
+
as the crop of the source labels. Files it cannot keep (JPEG, 16-bit colour
|
|
843
|
+
PNGs, ambiguous array layouts) fall back to the writer with a warning. Labels
|
|
844
|
+
are never interpolated: a `resize.interpolation` other than `nearest` for them
|
|
845
|
+
is an error.
|
|
846
|
+
|
|
847
|
+
Other rasters are re-encoded with the dataset's euler-loading writer, so a raster
|
|
820
848
|
modality needs one (SeeingThroughFog's raw `rccb` TIFFs have none). Each
|
|
821
849
|
encoded file is decoded again right away and must give back the cropped values:
|
|
822
850
|
exactly, or for an interpolated resize within half an 8-bit step. A resize
|
euler_preprocess-3.19.0/euler_preprocess.egg-info/PKG-INFO → euler_preprocess-3.20.0/README.md
RENAMED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: euler-preprocess
|
|
3
|
-
Version: 3.19.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.33.0
|
|
13
|
-
Requires-Dist: ds-crawler>=2.14.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">
|
|
@@ -748,7 +728,8 @@ like the original:
|
|
|
748
728
|
|
|
749
729
|
| Modality kind | Crop |
|
|
750
730
|
|---|---|
|
|
751
|
-
| Rasters: RGB, dense depth
|
|
731
|
+
| Rasters: RGB, dense depth | Sliced; no resampling, so values are unchanged |
|
|
732
|
+
| Label rasters: semantic segmentation (the ground truth that also holds the sky), instance and panoptic segmentation, sky masks | Cropped in the stored file, so every class, channel, palette and bit depth is kept; resized only by nearest |
|
|
752
733
|
| Pinhole intrinsics `K` (or `P` for rectified images) | `cx -= left`, `cy -= top`; `fx`, `fy`, skew unchanged |
|
|
753
734
|
| Lidar point clouds | Copied byte for byte, or pruned to the points that project into the crop |
|
|
754
735
|
| Extrinsics | Copied byte for byte |
|
|
@@ -808,7 +789,7 @@ The crop config (`configs/crop_config.json`) describes the box relative to the
|
|
|
808
789
|
| `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
790
|
| `projection` | For `prune_to_crop`: the `intrinsics` and `extrinsics` modalities to project with, when the dataset has more than one candidate |
|
|
810
791
|
| `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 |
|
|
792
|
+
| `roles` | Override a modality's kind with `raster` or `copy`, for loaders whose annotation the crop does not know, or to copy an intrinsics file of another camera (e.g. MUSES `sensor: event`) instead of cropping it |
|
|
812
793
|
|
|
813
794
|
Pixel units reproduce the official boxes exactly, e.g. `RGB2Gatedv2` is
|
|
814
795
|
`{"remove": {"top": 202, "bottom": 54, "left": 280, "right": 360}, "unit": "pixel"}`.
|
|
@@ -816,7 +797,34 @@ Pixel units reproduce the official boxes exactly, e.g. `RGB2Gatedv2` is
|
|
|
816
797
|
SeeingThroughFog rectification leaves on `_lut` frames (top 34, bottom 46,
|
|
817
798
|
left 16 px), keeping a fully valid 1904×944 image.
|
|
818
799
|
|
|
819
|
-
|
|
800
|
+
MUSES keeps the frame camera's `K` and every extrinsic in one `calib.json`,
|
|
801
|
+
indexed as `intrinsics`, `camera_extrinsics` (and `lidar_to_gnss`). The crop
|
|
802
|
+
patches `intrinsics.rgb.K` once and writes that file for every modality that
|
|
803
|
+
indexes it, also when it is indexed as separate byte-identical copies, so the
|
|
804
|
+
bundle holds one `K`. `D`, the event camera and all extrinsics stay as they
|
|
805
|
+
are. `configs/crop_muses_hood_config.json` cuts the engine hood off the
|
|
806
|
+
1920×1080 frame camera images: the bottom 160 rows, leaving 1920×920. Rows come
|
|
807
|
+
off the bottom only, so `K` and every `calib.json` stay byte-identical. Run
|
|
808
|
+
`lidar-motion` first and crop its output (`configs/crop_muses_dataset_config.json`).
|
|
809
|
+
GNSS records and `meta.json` sensor timing are copied unchanged, like the
|
|
810
|
+
extrinsics. MUSES label images (`gt_semantic`, `gt_panoptic`) are cropped in place like
|
|
811
|
+
any label raster.
|
|
812
|
+
|
|
813
|
+
Label rasters (loader types `semantic_segmentation`, `semantic_segmentation_color`,
|
|
814
|
+
`instance_segmentation`, `panoptic_segmentation`, `sky_mask`) are cropped in the
|
|
815
|
+
file itself: PNG, TIFF and BMP through Pillow in their own mode and palette,
|
|
816
|
+
`.npy`/`.npz` through NumPy. A semantic segmentation is the ground truth for
|
|
817
|
+
every class including the sky, and consumers read it through different
|
|
818
|
+
functions (`class_segmentation`, or `sky_mask` for sky-depth). Re-encoding the
|
|
819
|
+
decoded labels would drop what one function does not return: Real Drive Sim's
|
|
820
|
+
non-red channels, an indexed PNG's palette, or every class but the sky when a
|
|
821
|
+
dataset only has a `sky_mask` function. The in-place crop must decode exactly
|
|
822
|
+
as the crop of the source labels. Files it cannot keep (JPEG, 16-bit colour
|
|
823
|
+
PNGs, ambiguous array layouts) fall back to the writer with a warning. Labels
|
|
824
|
+
are never interpolated: a `resize.interpolation` other than `nearest` for them
|
|
825
|
+
is an error.
|
|
826
|
+
|
|
827
|
+
Other rasters are re-encoded with the dataset's euler-loading writer, so a raster
|
|
820
828
|
modality needs one (SeeingThroughFog's raw `rccb` TIFFs have none). Each
|
|
821
829
|
encoded file is decoded again right away and must give back the cropped values:
|
|
822
830
|
exactly, or for an interpolated resize within half an 8-bit step. A resize
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Status: **phases 1 and 3 implemented** (2026-11-07) as `euler-preprocess crop`
|
|
4
4
|
(plus `crop-box`), for **SeeingThroughFog** (euler-loading's `princeton_dense`
|
|
5
5
|
loader) with `rgb`, `sparse_depth`, `intrinsics` and `camera_extrinsics`; see §5
|
|
6
|
-
for results. Phase 2 (other datasets)
|
|
6
|
+
for results. Phase 2 (other datasets): **MUSES implemented** (2026-11-08, §5,
|
|
7
|
+
including the engine-hood crop); the rest is open. The euler-loading side shipped in
|
|
7
8
|
2.31.0 (STF fixes) and 2.32.0 (MUSES inputs); phase 3 uses euler-loading 2.33.0
|
|
8
9
|
and ds-crawler 2.14.0 (both released), which euler-preprocess now requires.
|
|
9
10
|
euler-view launches it with the `euler-preprocess-crop` addon. Written 2026-11-06 against
|
|
@@ -244,10 +245,11 @@ modality name, which is what causes the hazard in §1.
|
|
|
244
245
|
|
|
245
246
|
| Annotated shape | Role | Operation |
|
|
246
247
|
|---|---|---|
|
|
247
|
-
| `HW`, `1HW`, `HWC`, `CHW`, `3HW` | raster | Slice with the box; must match the reference H×W, else error |
|
|
248
|
+
| `HW`, `1HW`, `HWC`, `CHW`, `3HW` | raster | Slice with the box; must match the reference H×W, else error. Label types (`semantic_segmentation`, `semantic_segmentation_color`, `instance_segmentation`, `panoptic_segmentation`, `sky_mask`) are cropped in the stored file (2026-11-08) |
|
|
248
249
|
| `NC` | point set | Byte copy (`prune_to_crop` later) |
|
|
249
250
|
| `3x3` with type `intrinsics` | intrinsics | `K' = crop_intrinsics(K, top, left)` |
|
|
250
251
|
| `4x4`, `Nx1` | extrinsics | Byte copy (patched copy where the file also holds K) |
|
|
252
|
+
| `dict` with type `gnss` / `sensor_timing` | copy | Byte copy (MUSES side data) |
|
|
251
253
|
| `dict` (Real Drive Sim `calibration`) | calibration map | Phase 2 |
|
|
252
254
|
| anything else | — | Error; override with `"roles": {"<name>": "raster" \| "copy"}` |
|
|
253
255
|
|
|
@@ -372,7 +374,7 @@ euler-preprocess:
|
|
|
372
374
|
|
|
373
375
|
| Dataset | Missing |
|
|
374
376
|
|---|---|
|
|
375
|
-
| `muses` |
|
|
377
|
+
| `muses` | Done (2026-11-08, see "MUSES crop" below). |
|
|
376
378
|
| `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 |
|
|
377
379
|
| `vkitti2` | `write_intrinsics` must keep every frame/camera row (it writes one today); `read_extrinsics` header bug (§7) |
|
|
378
380
|
| `synscapes` | Nothing new (merge writers exist; pass `resx`/`resy`); extrinsics output = patched meta JSON |
|
|
@@ -381,7 +383,18 @@ euler-preprocess:
|
|
|
381
383
|
|
|
382
384
|
Also in phase 2: a format-level raster crop (PNG/TIFF via Pillow, NPY/NPZ via
|
|
383
385
|
NumPy, preserving mode, dtype and keys) for formats whose writers are missing or
|
|
384
|
-
lossy.
|
|
386
|
+
lossy. **Done for label rasters** (2026-11-08,
|
|
387
|
+
`euler_preprocess/crop/label_files.py`), since a semantic segmentation is the
|
|
388
|
+
ground truth for every class, the sky included:
|
|
389
|
+
- The stored pixels are sliced, and for a resize the source pixels are picked
|
|
390
|
+
with euler-loading's own nearest indices, in the same mode, palette and
|
|
391
|
+
format.
|
|
392
|
+
- The result must decode exactly as the crop of the decoded source.
|
|
393
|
+
- Unsupported files fall back to the writer.
|
|
394
|
+
- Labels are never interpolated.
|
|
395
|
+
|
|
396
|
+
euler-view's crop addon offers `semantic_segmentation` instead of `sky_mask`,
|
|
397
|
+
which is only a model output. Depth PNGs (§7.3) could use the same path later.
|
|
385
398
|
|
|
386
399
|
**MUSES lidar needs motion compensation.** The sensor is not a spinning lidar.
|
|
387
400
|
It scans a ~124° × 25° forward window top to bottom in 84.5 ms: point
|
|
@@ -436,6 +449,76 @@ loaders.
|
|
|
436
449
|
The side data stays with the local dataset: MUSES license §2.3 forbids
|
|
437
450
|
distributing it.
|
|
438
451
|
|
|
452
|
+
### MUSES crop — implemented 2026-11-08
|
|
453
|
+
|
|
454
|
+
euler-loading (unreleased, for 2.35.0): MUSES writers for every image and
|
|
455
|
+
calibration modality.
|
|
456
|
+
- `write_rgb` / `write_reference_rgb`: 8-bit PNG, a bit-exact round trip on
|
|
457
|
+
real frames.
|
|
458
|
+
- `write_semantic_segmentation`, `write_semantic_segmentation_color`,
|
|
459
|
+
`write_panoptic_segmentation` and `write_sky_mask`.
|
|
460
|
+
- `write_intrinsics` updates `intrinsics[<sensor>].K` in `calib.json` like
|
|
461
|
+
`princeton_dense.write_intrinsics`: only a crop or rescale of the stored `K`,
|
|
462
|
+
applied to its float64 values. It keeps `D`, the event camera and all
|
|
463
|
+
extrinsics, and does not rewrite an unchanged matrix. The crop/rescale check
|
|
464
|
+
is now shared (`_writer_utils.pixel_grid_transform`).
|
|
465
|
+
|
|
466
|
+
euler-preprocess:
|
|
467
|
+
- A copied file whose source bytes equal a file an intrinsics modality
|
|
468
|
+
patched is written with the patched bytes. This covers MUSES `calib.json`
|
|
469
|
+
as `camera_extrinsics` / `lidar_to_gnss`, whether it is indexed in place
|
|
470
|
+
(metadata scopes) or as separate copies, so the bundle carries one `K`.
|
|
471
|
+
- Each such copy is decoded again and must read as before.
|
|
472
|
+
- Two intrinsics modalities patching one file differently are refused (copy
|
|
473
|
+
the event camera's view with `roles`).
|
|
474
|
+
- Intrinsics are written first in each pass.
|
|
475
|
+
- `dict` loaders of type `gnss` and `sensor_timing` are copied by default.
|
|
476
|
+
- Configs: `configs/crop_muses_hood_config.json` and
|
|
477
|
+
`crop_muses_dataset_config.json`; the euler-view crop addon has a
|
|
478
|
+
`muses-hood` preset.
|
|
479
|
+
|
|
480
|
+
**Engine hood.** Measured on the five sample frames (all REC0176, test/fog/day):
|
|
481
|
+
- **Method.** The hood is rigid with the camera, so its silhouette is the
|
|
482
|
+
vertical edge that is strong in every frame. Take the per-column maximum of
|
|
483
|
+
the across-frame minimum of the blurred vertical luma gradient.
|
|
484
|
+
- **Shape.** The silhouette is a dome over columns 216–1619. At its bottom
|
|
485
|
+
it reaches rows 1061 (column 250) and 1066 (column 1600). Its apex is at
|
|
486
|
+
**row 931–932, near column 915**.
|
|
487
|
+
- **Consistency.** All five frames agree to ±1 px. Luma at the apex steps
|
|
488
|
+
from road (44–53) to hood (65–70) between rows 930 and 933.
|
|
489
|
+
- **Crop.** Cutting the bottom 160 rows (1920×920 stay) leaves an 11-row
|
|
490
|
+
margin. The cut is free in lidar terms: the MUSES lidar's lowest return
|
|
491
|
+
projects to row 811–820 in the three frames with lidar, so no point lands in
|
|
492
|
+
rows 920–1079. Pruning drops only points outside the camera's horizontal
|
|
493
|
+
field of view (66.7 % kept).
|
|
494
|
+
- **Divisibility.** For a /32 height, cut 184 rows (1920×896).
|
|
495
|
+
- **Open.** These are frames of one recording out of 136. The camera is
|
|
496
|
+
calibrated once (`calib.json`), but a remount between recordings could
|
|
497
|
+
shift the hood by a few rows. Check frames from a few other recordings
|
|
498
|
+
(other days, night) before relying on the margin.
|
|
499
|
+
|
|
500
|
+
Real data (three frames with motion-compensated lidar, the real `calib.json`):
|
|
501
|
+
- the CLI crop gives a bit-exact RGB slice;
|
|
502
|
+
- `calib.json` for `intrinsics` and `camera_extrinsics` is byte-identical to
|
|
503
|
+
the source, since K is unchanged;
|
|
504
|
+
- verification passes in both point-cloud modes.
|
|
505
|
+
|
|
506
|
+
Real data, general crop (top 40, bottom 160, left 24, right 8, then 0.5x):
|
|
507
|
+
`K' = S·C·K` to 1e-9, with both `calib.json` outputs identical, `D`, the event
|
|
508
|
+
camera and the extrinsics kept, and verification passing.
|
|
509
|
+
|
|
510
|
+
Tests: euler-loading 1038 passed (15 new writer tests); euler-preprocess 298
|
|
511
|
+
passed. The MUSES-format fixture covers:
|
|
512
|
+
- the hood crop;
|
|
513
|
+
- a general crop patching `calib.json` for three modalities;
|
|
514
|
+
- pruning;
|
|
515
|
+
- the event-camera conflict;
|
|
516
|
+
- a scoped zip like the production `/data/muses.zip`;
|
|
517
|
+
- the configs.
|
|
518
|
+
|
|
519
|
+
The contract's loader evidence was refreshed (only source hashes and line
|
|
520
|
+
numbers changed), and its ecosystem check passes.
|
|
521
|
+
|
|
439
522
|
### Phase 3 — implemented 2026-11-07
|
|
440
523
|
|
|
441
524
|
- `point_clouds: "prune_to_crop"`: keeps the points with `z > 0` whose pixel
|
|
@@ -569,46 +569,41 @@ class SourceBackedOutputBackend:
|
|
|
569
569
|
target_path.write_bytes(data)
|
|
570
570
|
return target_path
|
|
571
571
|
|
|
572
|
-
def
|
|
572
|
+
def updated_bytes(
|
|
573
573
|
self,
|
|
574
|
-
full_id: str,
|
|
575
574
|
source_entry: Mapping[str, Any],
|
|
576
575
|
data: bytes,
|
|
577
576
|
value: Any,
|
|
578
577
|
meta: dict[str, Any] | None,
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
) -> Path:
|
|
582
|
-
"""Store a copy of the source file, then let the writer update it to *value*.
|
|
578
|
+
) -> bytes:
|
|
579
|
+
"""Return *data*, the source file's bytes, after the writer has updated them to *value*.
|
|
583
580
|
|
|
584
581
|
For writers that merge into an existing file (e.g. camera-info
|
|
585
582
|
``write_intrinsics``), so every field the new value does not describe
|
|
586
|
-
is kept. The writer
|
|
583
|
+
is kept. The writer works on a temporary copy named like the source.
|
|
587
584
|
"""
|
|
588
585
|
if self.modality_writer is None:
|
|
589
586
|
raise ValueError(f"No writer is available for modality '{self.source_modality}'")
|
|
590
587
|
basename = Path(str(source_entry["path"])).name
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
self.modality_writer(str(target_path), value, meta)
|
|
611
|
-
return target_path
|
|
588
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
589
|
+
temp_path = Path(tmpdir) / basename
|
|
590
|
+
temp_path.write_bytes(data)
|
|
591
|
+
self.modality_writer(str(temp_path), value, meta)
|
|
592
|
+
return temp_path.read_bytes()
|
|
593
|
+
|
|
594
|
+
def update_source(
|
|
595
|
+
self,
|
|
596
|
+
full_id: str,
|
|
597
|
+
source_entry: Mapping[str, Any],
|
|
598
|
+
data: bytes,
|
|
599
|
+
value: Any,
|
|
600
|
+
meta: dict[str, Any] | None,
|
|
601
|
+
*,
|
|
602
|
+
attributes: dict[str, Any] | None = None,
|
|
603
|
+
) -> Path:
|
|
604
|
+
"""Store a copy of the source file, updated by the writer to *value* (see :meth:`updated_bytes`)."""
|
|
605
|
+
updated = self.updated_bytes(source_entry, data, value, meta)
|
|
606
|
+
return self.copy_source(full_id, source_entry, updated, attributes=attributes)
|
|
612
607
|
|
|
613
608
|
def update_head_meta(self, updates: Mapping[str, Any]) -> None:
|
|
614
609
|
"""Merge *updates* into the output head's ``modality.meta``; ``None`` removes a key."""
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Crop label rasters in the stored file, so ground-truth labels keep every class.
|
|
2
|
+
|
|
3
|
+
A semantic segmentation is ground truth: sky, road, the ego vehicle and every
|
|
4
|
+
other class are encoded in one file, which consumers read through different
|
|
5
|
+
functions (``class_segmentation`` for the labels, ``sky_mask`` for the sky).
|
|
6
|
+
Re-encoding the decoded value through a dataset writer keeps what one loader
|
|
7
|
+
returns, but not necessarily the file:
|
|
8
|
+
|
|
9
|
+
- palettes and bit depths change (an indexed or 16-bit PNG comes back as 8-bit);
|
|
10
|
+
- channels the loader ignores are dropped (Real Drive Sim keeps class IDs in
|
|
11
|
+
the red channel of an RGBA PNG; its writer zeroes the others);
|
|
12
|
+
- a semantic map read through ``sky_mask`` would be written back as sky / not sky.
|
|
13
|
+
|
|
14
|
+
So label rasters are cropped in the file itself: the stored pixels are sliced,
|
|
15
|
+
and resized by picking the source pixels nearest resampling picks, in the
|
|
16
|
+
same image mode, palette and format. PNG, TIFF and BMP go through Pillow, and
|
|
17
|
+
``.npy`` / ``.npz`` through NumPy. The caller then checks that the modality's
|
|
18
|
+
loader reads the result as the crop of the source values. Anything else
|
|
19
|
+
(JPEG, 16-bit colour PNGs Pillow cannot hold, multi-frame images, ambiguous
|
|
20
|
+
array layouts) returns ``None`` and is re-encoded with the dataset's writer.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import io
|
|
26
|
+
import zipfile
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from euler_preprocess.crop.spec import CropBox
|
|
32
|
+
|
|
33
|
+
#: Formats Pillow writes back losslessly.
|
|
34
|
+
_LOSSLESS_IMAGE_FORMATS = {"PNG", "TIFF", "BMP"}
|
|
35
|
+
_IMAGE_SUFFIXES = {".png", ".tif", ".tiff", ".bmp"}
|
|
36
|
+
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
37
|
+
#: Pillow modes that hold 16-bit samples.
|
|
38
|
+
_WIDE_MODES = {"I", "I;16", "I;16B", "I;16L", "I;16N"}
|
|
39
|
+
#: TIFF compressions that lose nothing.
|
|
40
|
+
_LOSSLESS_TIFF_COMPRESSIONS = {None, "raw", "tiff_lzw", "tiff_adobe_deflate", "tiff_deflate", "packbits"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def source_grid(box: CropBox) -> tuple[np.ndarray, np.ndarray]:
|
|
44
|
+
"""Return the source row of every output row and the source column of every output column.
|
|
45
|
+
|
|
46
|
+
Without a resize these are the crop's rows and columns. With one they are
|
|
47
|
+
the pixels euler-loading's nearest resize picks: it is separable, so
|
|
48
|
+
resizing a column of row indices (and a row of column indices) gives them
|
|
49
|
+
exactly.
|
|
50
|
+
"""
|
|
51
|
+
rows = np.arange(box.top, box.top + box.height)
|
|
52
|
+
cols = np.arange(box.left, box.left + box.width)
|
|
53
|
+
if box.resized:
|
|
54
|
+
from euler_loading import resize_raster
|
|
55
|
+
|
|
56
|
+
pick_rows = resize_raster(
|
|
57
|
+
np.arange(box.height, dtype=np.int64)[:, None],
|
|
58
|
+
layout="HW",
|
|
59
|
+
size=(box.output_height, 1),
|
|
60
|
+
interpolation="nearest",
|
|
61
|
+
)
|
|
62
|
+
pick_cols = resize_raster(
|
|
63
|
+
np.arange(box.width, dtype=np.int64)[None, :],
|
|
64
|
+
layout="HW",
|
|
65
|
+
size=(1, box.output_width),
|
|
66
|
+
interpolation="nearest",
|
|
67
|
+
)
|
|
68
|
+
rows = rows[np.asarray(pick_rows)[:, 0]]
|
|
69
|
+
cols = cols[np.asarray(pick_cols)[0, :]]
|
|
70
|
+
return rows, cols
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def crop_label_file(
|
|
74
|
+
data: bytes, name: str, rows: np.ndarray, cols: np.ndarray, source_size: tuple[int, int]
|
|
75
|
+
) -> bytes | None:
|
|
76
|
+
"""Return the file *data* with only pixels ``rows`` x ``cols`` kept, or ``None``.
|
|
77
|
+
|
|
78
|
+
*name* gives the format (its suffix) and *source_size* the ``(height,
|
|
79
|
+
width)`` the stored raster must have. ``None`` means the file cannot be
|
|
80
|
+
cropped in place without loss.
|
|
81
|
+
"""
|
|
82
|
+
suffix = Path(name).suffix.lower()
|
|
83
|
+
if suffix in _IMAGE_SUFFIXES:
|
|
84
|
+
return _crop_image(data, rows, cols, source_size)
|
|
85
|
+
if suffix == ".npy":
|
|
86
|
+
return _crop_npy(data, rows, cols, source_size)
|
|
87
|
+
if suffix == ".npz":
|
|
88
|
+
return _crop_npz(data, rows, cols, source_size)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _png_bit_depth(data: bytes) -> int | None:
|
|
93
|
+
if data[:8] != _PNG_SIGNATURE or data[12:16] != b"IHDR" or len(data) < 25:
|
|
94
|
+
return None
|
|
95
|
+
return data[24]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _crop_image(data: bytes, rows: np.ndarray, cols: np.ndarray, source_size: tuple[int, int]) -> bytes | None:
|
|
99
|
+
from PIL import Image
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
image = Image.open(io.BytesIO(data))
|
|
103
|
+
image.load()
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
with image:
|
|
107
|
+
if image.format not in _LOSSLESS_IMAGE_FORMATS or getattr(image, "n_frames", 1) != 1:
|
|
108
|
+
return None
|
|
109
|
+
if (image.height, image.width) != tuple(source_size):
|
|
110
|
+
return None
|
|
111
|
+
if _png_bit_depth(data) == 16 and image.mode not in _WIDE_MODES:
|
|
112
|
+
# Pillow reads 16-bit colour PNGs as 8-bit: the file cannot be kept.
|
|
113
|
+
return None
|
|
114
|
+
params: dict[str, object] = {}
|
|
115
|
+
if image.format == "TIFF":
|
|
116
|
+
compression = image.info.get("compression")
|
|
117
|
+
if compression not in _LOSSLESS_TIFF_COMPRESSIONS:
|
|
118
|
+
return None
|
|
119
|
+
if compression is not None:
|
|
120
|
+
params["compression"] = compression
|
|
121
|
+
if "transparency" in image.info:
|
|
122
|
+
params["transparency"] = image.info["transparency"]
|
|
123
|
+
|
|
124
|
+
pixels = np.asarray(image)
|
|
125
|
+
cropped = Image.fromarray(np.ascontiguousarray(pixels[rows][:, cols]))
|
|
126
|
+
if image.mode == "P":
|
|
127
|
+
# Pillow turns the indices into an "L" image; the palette makes it "P" again.
|
|
128
|
+
cropped.putpalette(image.getpalette())
|
|
129
|
+
if cropped.mode != image.mode:
|
|
130
|
+
return None
|
|
131
|
+
buffer = io.BytesIO()
|
|
132
|
+
cropped.save(buffer, format=image.format, **params)
|
|
133
|
+
return buffer.getvalue()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _spatial_axes(shape: tuple[int, ...], source_size: tuple[int, int]) -> tuple[int, int] | None | bool:
|
|
137
|
+
"""Return the adjacent ``(height, width)`` axes of *shape*; ``None`` if absent, ``False`` if ambiguous."""
|
|
138
|
+
height, width = source_size
|
|
139
|
+
matches = [axis for axis in range(len(shape) - 1) if shape[axis] == height and shape[axis + 1] == width]
|
|
140
|
+
if not matches:
|
|
141
|
+
return None
|
|
142
|
+
if len(matches) > 1:
|
|
143
|
+
return False
|
|
144
|
+
return matches[0], matches[0] + 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _take(array: np.ndarray, axes: tuple[int, int], rows: np.ndarray, cols: np.ndarray) -> np.ndarray:
|
|
148
|
+
return np.ascontiguousarray(np.take(np.take(array, rows, axis=axes[0]), cols, axis=axes[1]))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _crop_npy(data: bytes, rows: np.ndarray, cols: np.ndarray, source_size: tuple[int, int]) -> bytes | None:
|
|
152
|
+
try:
|
|
153
|
+
array = np.load(io.BytesIO(data), allow_pickle=False)
|
|
154
|
+
except Exception:
|
|
155
|
+
return None
|
|
156
|
+
axes = _spatial_axes(array.shape, source_size)
|
|
157
|
+
if not axes:
|
|
158
|
+
return None
|
|
159
|
+
buffer = io.BytesIO()
|
|
160
|
+
np.save(buffer, _take(array, axes, rows, cols), allow_pickle=False)
|
|
161
|
+
return buffer.getvalue()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _crop_npz(data: bytes, rows: np.ndarray, cols: np.ndarray, source_size: tuple[int, int]) -> bytes | None:
|
|
165
|
+
try:
|
|
166
|
+
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
|
167
|
+
compressed = any(info.compress_type != zipfile.ZIP_STORED for info in archive.infolist())
|
|
168
|
+
with np.load(io.BytesIO(data), allow_pickle=False) as npz:
|
|
169
|
+
arrays = {key: npz[key] for key in npz.files}
|
|
170
|
+
except Exception:
|
|
171
|
+
return None
|
|
172
|
+
cropped: dict[str, np.ndarray] = {}
|
|
173
|
+
for key, array in arrays.items():
|
|
174
|
+
axes = _spatial_axes(array.shape, source_size)
|
|
175
|
+
if axes is False:
|
|
176
|
+
return None
|
|
177
|
+
# Arrays without the image's axes (class names, metadata) are kept as they are.
|
|
178
|
+
cropped[key] = array if axes is None else _take(array, axes, rows, cols)
|
|
179
|
+
if all(cropped[key] is arrays[key] for key in arrays):
|
|
180
|
+
return None
|
|
181
|
+
buffer = io.BytesIO()
|
|
182
|
+
(np.savez_compressed if compressed else np.savez)(buffer, **cropped)
|
|
183
|
+
return buffer.getvalue()
|
|
@@ -10,13 +10,25 @@ sliced like an image.
|
|
|
10
10
|
| ``HW``, ``HWC``, ``CHW``, ``1HW``, ``3HW`` | raster | Slice with the crop box |
|
|
11
11
|
| ``3x3`` with type ``intrinsics`` | intrinsics | ``K' = crop_intrinsics(K, top, left)`` |
|
|
12
12
|
| ``NC`` (point sets), ``4x4`` / ``Nx1`` (extrinsics) | copy | Copy the file unchanged |
|
|
13
|
+
| ``dict`` with type ``gnss`` or ``sensor_timing`` | copy | Copy the file unchanged |
|
|
13
14
|
|
|
14
|
-
Anything else needs a ``roles`` override (``"raster"`` or ``"copy"``).
|
|
15
|
+
Anything else needs a ``roles`` override (``"raster"`` or ``"copy"``). Other
|
|
16
|
+
``dict`` loaders can hold pixel-grid values (Real Drive Sim's calibration map
|
|
17
|
+
holds the camera matrix), so they are never copied by default.
|
|
18
|
+
|
|
19
|
+
A copied file that an ``intrinsics`` modality also indexes (MUSES
|
|
20
|
+
``calib.json`` holds the camera matrix and every extrinsic) is written as the
|
|
21
|
+
patched file, so the output carries one camera matrix.
|
|
15
22
|
|
|
16
23
|
When the crop also resizes, colour images (types ``rgb`` and ``rccb``) are
|
|
17
24
|
interpolated bilinearly and every other raster (depth, masks, labels) takes the
|
|
18
25
|
nearest source pixel, so no new depth or label values appear. Sparse-depth
|
|
19
26
|
rasters cannot be resized: nearest sampling would drop points at random.
|
|
27
|
+
|
|
28
|
+
Label rasters (semantic, instance and panoptic segmentation, sky masks) are
|
|
29
|
+
ground truth whose classes all live in one file. They are cropped in the
|
|
30
|
+
stored file (:mod:`euler_preprocess.crop.label_files`) and can only be resized
|
|
31
|
+
with ``nearest``.
|
|
20
32
|
"""
|
|
21
33
|
|
|
22
34
|
from __future__ import annotations
|
|
@@ -38,10 +50,23 @@ RASTER_AXES: dict[str, tuple[int, int]] = {
|
|
|
38
50
|
"3HW": (1, 2),
|
|
39
51
|
}
|
|
40
52
|
_COPY_SHAPES = {"NC", "4x4", "Nx1"}
|
|
53
|
+
#: ``dict`` loaders whose values never refer to the image grid (MUSES GNSS
|
|
54
|
+
#: records and ``meta.json`` sensor timing).
|
|
55
|
+
_COPY_DICT_TYPES = {"gnss", "sensor_timing"}
|
|
41
56
|
#: euler-loading's resize layouts for the annotated raster layouts.
|
|
42
57
|
_RESIZE_LAYOUTS = {"HW": "HW", "HWC": "HWC", "CHW": "CHW", "1HW": "CHW", "3HW": "CHW"}
|
|
43
58
|
#: Raster types that are interpolated when resized; all others use nearest.
|
|
44
59
|
INTERPOLATED_TYPES = {"rgb", "rccb"}
|
|
60
|
+
#: Categorical raster types: their values are class, instance or segment IDs.
|
|
61
|
+
LABEL_TYPES = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"semantic_segmentation",
|
|
64
|
+
"semantic_segmentation_color",
|
|
65
|
+
"instance_segmentation",
|
|
66
|
+
"panoptic_segmentation",
|
|
67
|
+
"sky_mask",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
45
70
|
|
|
46
71
|
|
|
47
72
|
@dataclass(frozen=True)
|
|
@@ -83,6 +108,11 @@ class ModalityRole:
|
|
|
83
108
|
def is_point_set(self) -> bool:
|
|
84
109
|
return self.role == COPY and self.shape == "NC"
|
|
85
110
|
|
|
111
|
+
@property
|
|
112
|
+
def is_label(self) -> bool:
|
|
113
|
+
"""A categorical raster: cropped in the stored file, resized only by nearest."""
|
|
114
|
+
return self.role == RASTER and self.modality_type in LABEL_TYPES
|
|
115
|
+
|
|
86
116
|
def default_interpolation(self) -> str:
|
|
87
117
|
return "bilinear" if self.modality_type in INTERPOLATED_TYPES else "nearest"
|
|
88
118
|
|
|
@@ -95,6 +125,8 @@ class ModalityRole:
|
|
|
95
125
|
f"Modality '{self.name}' is a sparse-depth raster and cannot be resized: "
|
|
96
126
|
"nearest sampling would drop points at random. Resize from point clouds instead."
|
|
97
127
|
)
|
|
128
|
+
if self.is_label and interpolation not in (None, "nearest"):
|
|
129
|
+
raise ValueError(label_interpolation_error(self.name, self.modality_type, interpolation))
|
|
98
130
|
return resize_raster(
|
|
99
131
|
value,
|
|
100
132
|
layout=_RESIZE_LAYOUTS[self.layout or ""],
|
|
@@ -103,6 +135,13 @@ class ModalityRole:
|
|
|
103
135
|
)
|
|
104
136
|
|
|
105
137
|
|
|
138
|
+
def label_interpolation_error(name: str, modality_type: str | None, interpolation: str) -> str:
|
|
139
|
+
return (
|
|
140
|
+
f"Modality '{name}' holds labels ({modality_type}) and can only be resized with "
|
|
141
|
+
f"'nearest', not {interpolation!r}: interpolating would invent class IDs"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
106
145
|
def describe_annotation(annotation: Mapping[str, Any] | None) -> str:
|
|
107
146
|
if not annotation:
|
|
108
147
|
return "no @modality_meta annotation"
|
|
@@ -152,7 +191,7 @@ def infer_role(
|
|
|
152
191
|
return role(RASTER, shape)
|
|
153
192
|
if shape == "3x3" and modality_type == "intrinsics":
|
|
154
193
|
return role(INTRINSICS)
|
|
155
|
-
if shape in _COPY_SHAPES:
|
|
194
|
+
if shape in _COPY_SHAPES or (shape == "dict" and modality_type in _COPY_DICT_TYPES):
|
|
156
195
|
return role(COPY)
|
|
157
196
|
raise ValueError(
|
|
158
197
|
f"Don't know how to crop modality '{name}' ({describe_annotation(annotation)}). "
|