euler-eval 2.35.0__tar.gz → 2.36.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 (114) hide show
  1. {euler_eval-2.35.0/euler_eval.egg-info → euler_eval-2.36.0}/PKG-INFO +1 -1
  2. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/cli.md +4 -0
  3. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/configuration.md +11 -0
  4. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/output.md +20 -6
  5. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/cli.py +66 -2
  6. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/data.py +54 -0
  7. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/evaluate.py +17 -0
  8. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/protocol.py +1 -1
  9. {euler_eval-2.35.0 → euler_eval-2.36.0/euler_eval.egg-info}/PKG-INFO +1 -1
  10. {euler_eval-2.35.0 → euler_eval-2.36.0}/pyproject.toml +1 -1
  11. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_mask_sky_pairing.py +127 -0
  12. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_protocol.py +12 -1
  13. {euler_eval-2.35.0 → euler_eval-2.36.0}/LICENSE +0 -0
  14. {euler_eval-2.35.0 → euler_eval-2.36.0}/MANIFEST.in +0 -0
  15. {euler_eval-2.35.0 → euler_eval-2.36.0}/README.md +0 -0
  16. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/README.md +0 -0
  17. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/alignment.md +0 -0
  18. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/distributions.md +0 -0
  19. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/feature-request-unified-evaluation-framework.md +0 -0
  20. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/metrics.md +0 -0
  21. {euler_eval-2.35.0 → euler_eval-2.36.0}/docs/validation.md +0 -0
  22. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/THIRD_PARTY_NOTICES.md +0 -0
  23. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/__init__.py +0 -0
  24. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/calibration.py +0 -0
  25. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/config_paths.py +0 -0
  26. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/distribution_contract.py +0 -0
  27. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/distributions.py +0 -0
  28. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metric_sets.py +0 -0
  29. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/__init__.py +0 -0
  30. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/_natural_scene_models.py +0 -0
  31. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/absrel.py +0 -0
  32. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/daniel_error.py +0 -0
  33. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/depth_binned_error.py +0 -0
  34. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/depth_edge_f1.py +0 -0
  35. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/depth_standard.py +0 -0
  36. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/fade.py +0 -0
  37. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/fid_kid.py +0 -0
  38. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/gpu_depth_batch.py +0 -0
  39. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/gpu_image_batch.py +0 -0
  40. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/high_freq_energy.py +0 -0
  41. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/lpips_metric.py +0 -0
  42. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/niqe.py +0 -0
  43. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/normal_consistency.py +0 -0
  44. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/points3d_cloud.py +0 -0
  45. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/points3d_decomposition.py +0 -0
  46. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/points3d_distance.py +0 -0
  47. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/points3d_geometry.py +0 -0
  48. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/psnr.py +0 -0
  49. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/rgb_edge_f1.py +0 -0
  50. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/rgb_lpips.py +0 -0
  51. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/rgb_psnr_ssim.py +0 -0
  52. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/rho_a.py +0 -0
  53. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/rmse.py +0 -0
  54. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/scale_invariant_log.py +0 -0
  55. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/sky_iou.py +0 -0
  56. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/ssim.py +0 -0
  57. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/tail_errors.py +0 -0
  58. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/metrics/utils.py +0 -0
  59. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/sanity_checker.py +0 -0
  60. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/schemas/distribution-v1.schema.json +0 -0
  61. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/utils/hierarchy_parser.py +0 -0
  62. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval/validation.py +0 -0
  63. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval.egg-info/SOURCES.txt +0 -0
  64. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval.egg-info/dependency_links.txt +0 -0
  65. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval.egg-info/entry_points.txt +0 -0
  66. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval.egg-info/requires.txt +0 -0
  67. {euler_eval-2.35.0 → euler_eval-2.36.0}/euler_eval.egg-info/top_level.txt +0 -0
  68. {euler_eval-2.35.0 → euler_eval-2.36.0}/init_cache.py +0 -0
  69. {euler_eval-2.35.0 → euler_eval-2.36.0}/scripts/generate_distribution_fixtures.py +0 -0
  70. {euler_eval-2.35.0 → euler_eval-2.36.0}/setup.cfg +0 -0
  71. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/__init__.py +0 -0
  72. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/conftest.py +0 -0
  73. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/distribution_contract_utils.py +0 -0
  74. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/eval_output.py +0 -0
  75. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/README.md +0 -0
  76. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/counts-depth.eval.json +0 -0
  77. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/counts-points3d.eval.json +0 -0
  78. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/counts-sparsedepth.eval.json +0 -0
  79. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/future-sum-by-depth.eval.json +0 -0
  80. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/histogram-only.eval.json +0 -0
  81. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/per-file-only.eval.json +0 -0
  82. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/fixtures/distributions/scalar-only.eval.json +0 -0
  83. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/mock_files/calibration_output.json +0 -0
  84. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/mock_files/depth_output.json +0 -0
  85. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/mock_files/rgb_output.json +0 -0
  86. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/mock_files/segmentation_output.json +0 -0
  87. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_alignment.py +0 -0
  88. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_cli_device.py +0 -0
  89. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_cli_sparse_outputs.py +0 -0
  90. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_config.py +0 -0
  91. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_data.py +0 -0
  92. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_data_builders.py +0 -0
  93. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_depth_alignment_output.py +0 -0
  94. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_depth_standard.py +0 -0
  95. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_distribution_contract.py +0 -0
  96. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_distributions.py +0 -0
  97. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_domain_metrics.py +0 -0
  98. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_evaluate_helpers.py +0 -0
  99. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_experiments.py +0 -0
  100. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_init_cache.py +0 -0
  101. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_integration.py +0 -0
  102. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_mask_sky_prediction.py +0 -0
  103. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_meta_output.py +0 -0
  104. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_metrics_utils.py +0 -0
  105. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_normal_consistency.py +0 -0
  106. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_points_3d.py +0 -0
  107. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_points_3d_sparse.py +0 -0
  108. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_rgb_fid_output.py +0 -0
  109. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_rho_a.py +0 -0
  110. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_save_results.py +0 -0
  111. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_sky_iou.py +0 -0
  112. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_sky_iou_cli.py +0 -0
  113. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_sparse_depth.py +0 -0
  114. {euler_eval-2.35.0 → euler_eval-2.36.0}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.35.0
3
+ Version: 2.36.0
4
4
  Summary: Evaluation toolkit for depth, RGB, ray and 3D point-map predictions
5
5
  Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
6
6
  License: MIT
@@ -23,6 +23,7 @@ validation exits with status `1` before any dataset is opened.
23
23
  | `--skip-rays` | flag | off | Skip rays (spherical direction map) evaluation |
24
24
  | `--skip-points-3d` | flag | off | Skip points_3d (per-pixel 3D point map) evaluation |
25
25
  | `--mask-sky` | flag | off | Mask sky regions from metrics using GT segmentation |
26
+ | `--missing-segmentation` | `{drop,unmasked}` | `drop` | Frames without their own GT segmentation: `drop` leaves them out; `unmasked` evaluates them without GT sky masking and leaves them out of the sky IoU ([details](configuration.md#gt-segmentation)) |
26
27
  | `--mask-sky-prediction` | flag | off | Exclude pixels in a prediction's `datasets[].sky_mask` from dense and sparse depth metrics and the alignment fit; predictions without a `sky_mask` are scored in full ([details](configuration.md#masking-predicted-sky)) |
27
28
  | `--sky-depth` | `float` | none | Cap GT and predicted depths in meters and fill predicted `sky_mask` pixels with this value; must be finite and positive |
28
29
  | `--sky-iou` | flag | off | Score predicted sky against GT segmentation sky: IoU, precision and recall for the `explicit` (`datasets[].sky_mask`) and `implicit` (`depth >= --sky-depth`, metric predictions only) variants. Requires `gt.segmentation`; depth metrics are unchanged ([details](metrics.md#sky-segmentation)) |
@@ -58,6 +59,9 @@ euler-eval config.json --batch-size 32
58
59
  # Sky masking (requires gt.segmentation)
59
60
  euler-eval config.json --mask-sky -v
60
61
 
62
+ # Keep frames that have no GT segmentation, evaluated without sky masking
63
+ euler-eval config.json --mask-sky --missing-segmentation unmasked
64
+
61
65
  # Exclude each prediction's own sky (datasets[].sky_mask) from depth metrics
62
66
  euler-eval config.json --mask-sky-prediction
63
67
 
@@ -103,6 +103,17 @@ warns and the sky IoU is skipped. A segmentation whose IDs match no frame
103
103
  stops the run with euler-loading's "No common IDs" error and per-modality file
104
104
  counts.
105
105
 
106
+ When a split has segmentation for only some frames (MUSES lacks some), pass
107
+ `--missing-segmentation unmasked` to keep the other frames: they are evaluated
108
+ without GT sky masking and left out of the sky IoU, while frames with
109
+ segmentation are scored exactly as before. The metrics then mix masked and
110
+ unmasked frames, so `meta.gt_sky_masking` records how many frames had no
111
+ segmentation, and `protocol.parameters` records the policy
112
+ (`population.missing_segmentation`). Unmasked frames score their sky wherever
113
+ the GT has valid depth there: little for a lidar GT, which has few returns in
114
+ the sky, but possibly a lot for a dense GT with sky depth. The default,
115
+ `drop`, leaves those frames out.
116
+
106
117
  The config key is also the default metadata scope. If the path has no
107
118
  `#scope=` selector, no root-level metadata, and no scope of that name, the
108
119
  scope is chosen among the archive's scopes: the one whose name or
@@ -74,7 +74,7 @@ set.
74
74
  "metricDescriptions": { "absrel": { "…": "…" } }
75
75
  },
76
76
  "protocol": {
77
- "schemaVersion": "1.1",
77
+ "schemaVersion": "1.2",
78
78
  "parameters": { "reference.gt.dataset": "muses", "method.depth_alignment": "auto_affine", "…": "…" }
79
79
  },
80
80
  "dataset_info": { "num_pairs": 500, "gt_name": "GT", "pred_name": "model_a" },
@@ -171,6 +171,19 @@ sky removed, since that differs from model to model:
171
171
  depth) that would be scored without the flag, after `--mask-sky` if set;
172
172
  `excluded_gt_pixels` is how many of them fell in predicted sky.
173
173
 
174
+ ### GT sky masking coverage
175
+
176
+ With `--mask-sky`, every modality it applies to records how many frames had
177
+ their own GT segmentation:
178
+
179
+ ```json
180
+ "gt_sky_masking": { "samples": 500, "samples_without_segmentation": 100, "samples_with_sky": 396 }
181
+ ```
182
+
183
+ `samples_without_segmentation` is `0` unless `--missing-segmentation unmasked`
184
+ kept frames without one; those frames were scored without sky masking.
185
+ `samples_with_sky` counts the frames whose mask removed any pixel.
186
+
174
187
  When sky keys are written, the metric-set axes also declare them:
175
188
  `sky_segmentation` joins the `category` values, an optional `variant` axis
176
189
  (`explicit`, `implicit`) follows `category`, and `space` becomes optional
@@ -188,7 +201,7 @@ values differ say where. Nothing else in the file needs to be compared for that;
188
201
 
189
202
  ```json
190
203
  "protocol": {
191
- "schemaVersion": "1.1",
204
+ "schemaVersion": "1.2",
192
205
  "parameters": {
193
206
  "reference.gt.modality": "sparse_depth",
194
207
  "reference.gt.dataset": "seeingthroughfog",
@@ -208,6 +221,7 @@ values differ say where. Nothing else in the file needs to be compared for that;
208
221
  "reference.segmentation.split": null,
209
222
  "population.samples": 943,
210
223
  "population.gt_sky_masking": false,
224
+ "population.missing_segmentation": null,
211
225
  "population.pred_sky_masking": false,
212
226
  "population.evaluated_resolution": [1024, 1920],
213
227
  "population.benchmark_depth_range_m": [0.01, 80.0],
@@ -231,12 +245,12 @@ What is recorded, per modality:
231
245
 
232
246
  | Modality | `reference` inputs besides `gt` | `population` | `method` |
233
247
  |---|---|---|---|
234
- | `depth` | `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `pred_sky_masking`, `evaluated_resolution`, `benchmark_depth_range_m` | `depth_alignment`, `sky_depth_m`, `pred_sky_mask`, `spatial_alignment` |
248
+ | `depth` | `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `missing_segmentation`, `pred_sky_masking`, `evaluated_resolution`, `benchmark_depth_range_m` | `depth_alignment`, `sky_depth_m`, `pred_sky_mask`, `spatial_alignment` |
235
249
  | `sparsedepth` | `intrinsics`, `camera_extrinsics`, `lidar_extrinsics`, `segmentation` | as `depth` | `depth_alignment`, `sky_depth_m`, `pred_sky_mask` |
236
- | `rgb` | `depth`, `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `evaluated_resolution`, `benchmark_depth_range_m` | `fid_backend`, `spatial_alignment` |
250
+ | `rgb` | `depth`, `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `missing_segmentation`, `evaluated_resolution`, `benchmark_depth_range_m` | `fid_backend`, `spatial_alignment` |
237
251
  | `rays` | `calibration` | `samples`, `evaluated_resolution` | `spatial_alignment` |
238
- | `points3d` (dense) | `intrinsics`, `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `evaluated_resolution` | `points_3d_alignment`, `spatial_alignment` |
239
- | `points3d` (sparse GT) | `intrinsics`, `camera_extrinsics`, `lidar_extrinsics`, `segmentation` | `samples`, `gt_sky_masking`, `evaluated_resolution` | `points_3d_alignment` |
252
+ | `points3d` (dense) | `intrinsics`, `calibration`, `segmentation` | `samples`, `gt_sky_masking`, `missing_segmentation`, `evaluated_resolution` | `points_3d_alignment`, `spatial_alignment` |
253
+ | `points3d` (sparse GT) | `intrinsics`, `camera_extrinsics`, `lidar_extrinsics`, `segmentation` | `samples`, `gt_sky_masking`, `missing_segmentation`, `evaluated_resolution` | `points_3d_alignment` |
240
254
 
241
255
  Every modality also has `method.metrics_revision`.
242
256
 
@@ -29,6 +29,7 @@ from .config_paths import (
29
29
  resolve_gt_segmentation_path,
30
30
  )
31
31
  from .data import (
32
+ PartialInputDataset,
32
33
  build_depth_eval_dataset,
33
34
  build_points_3d_eval_dataset,
34
35
  build_points_3d_sparse_eval_dataset,
@@ -1478,7 +1479,12 @@ _PAIRED_OPTIONAL_INPUTS = {
1478
1479
 
1479
1480
 
1480
1481
  def _build_eval_dataset(
1481
- build, kwargs: dict, *, ds_name: str, iou_only: tuple[str, ...] = ()
1482
+ build,
1483
+ kwargs: dict,
1484
+ *,
1485
+ ds_name: str,
1486
+ iou_only: tuple[str, ...] = (),
1487
+ missing_segmentation: str = "drop",
1482
1488
  ) -> tuple:
1483
1489
  """Build an eval dataset and account for the frames its optional inputs drop.
1484
1490
 
@@ -1488,13 +1494,24 @@ def _build_eval_dataset(
1488
1494
  only for ``--sky-iou``, which must not change the evaluated pairs: if it
1489
1495
  lacks any, the dataset is rebuilt without it.
1490
1496
 
1497
+ With *missing_segmentation* ``"unmasked"``, frames without their own GT
1498
+ segmentation are kept instead: they are read without it, so ``--mask-sky``
1499
+ leaves them unmasked and ``--sky-iou`` leaves them out of the IoU.
1500
+
1491
1501
  Returns ``(dataset, skipped)`` with ``{input: reason}`` for every
1492
1502
  *iou_only* input that was left out.
1493
1503
  """
1494
1504
  optional = tuple(_PAIRED_OPTIONAL_INPUTS)
1495
1505
  dataset = build(**kwargs)
1506
+ partial_segmentation = (
1507
+ missing_segmentation == "unmasked"
1508
+ and kwargs.get("segmentation_path") is not None
1509
+ and bool(count_ids_dropped_by(dataset, "segmentation", optional=optional))
1510
+ )
1496
1511
  skipped = {}
1497
1512
  for name in iou_only:
1513
+ if name == "segmentation" and partial_segmentation:
1514
+ continue
1498
1515
  dropped = count_ids_dropped_by(dataset, name, optional=optional)
1499
1516
  if not dropped:
1500
1517
  continue
@@ -1510,11 +1527,23 @@ def _build_eval_dataset(
1510
1527
  dataset = build(**kwargs)
1511
1528
  for name, (_, label) in _PAIRED_OPTIONAL_INPUTS.items():
1512
1529
  dropped = count_ids_dropped_by(dataset, name, optional=optional)
1513
- if dropped:
1530
+ if not dropped:
1531
+ continue
1532
+ if name == "segmentation" and partial_segmentation:
1533
+ print(
1534
+ f"Note: the {label} of '{ds_name}' lacks {dropped} pairs that the "
1535
+ "other inputs share; with --missing-segmentation unmasked they "
1536
+ "are evaluated without GT sky masking and left out of the sky IoU."
1537
+ )
1538
+ else:
1514
1539
  print(
1515
1540
  f"Warning: the {label} of '{ds_name}' lacks {dropped} pairs that "
1516
1541
  "the other inputs share; those pairs are not evaluated."
1517
1542
  )
1543
+ if partial_segmentation:
1544
+ keys, _ = _PAIRED_OPTIONAL_INPUTS["segmentation"]
1545
+ complete = build(**{**kwargs, **dict.fromkeys(keys)})
1546
+ dataset = PartialInputDataset(complete, dataset)
1518
1547
  return dataset, skipped
1519
1548
 
1520
1549
 
@@ -1828,6 +1857,16 @@ def _build_parser() -> argparse.ArgumentParser:
1828
1857
  action="store_true",
1829
1858
  help="Mask sky regions from metrics using GT segmentation",
1830
1859
  )
1860
+ parser.add_argument(
1861
+ "--missing-segmentation",
1862
+ choices=["drop", "unmasked"],
1863
+ default="drop",
1864
+ help=(
1865
+ "Frames without their own GT segmentation: 'drop' leaves them out "
1866
+ "(default); 'unmasked' evaluates them without GT sky masking and "
1867
+ "leaves them out of the sky IoU"
1868
+ ),
1869
+ )
1831
1870
  parser.add_argument(
1832
1871
  "--mask-sky-prediction",
1833
1872
  action="store_true",
@@ -2172,6 +2211,7 @@ def _evaluate(
2172
2211
  ),
2173
2212
  ds_name=ds_name,
2174
2213
  iou_only=iou_only_inputs,
2214
+ missing_segmentation=args.missing_segmentation,
2175
2215
  )
2176
2216
  et_eval_datasets["depth"] = depth_dataset
2177
2217
 
@@ -2259,6 +2299,9 @@ def _evaluate(
2259
2299
  population={
2260
2300
  "samples": depth_dataset_info.get("num_pairs"),
2261
2301
  "gt_sky_masking": bool(args.mask_sky),
2302
+ "missing_segmentation": (
2303
+ args.missing_segmentation if args.mask_sky else None
2304
+ ),
2262
2305
  "pred_sky_masking": bool(args.mask_sky_prediction),
2263
2306
  "evaluated_resolution": evaluated_resolution(depth_spatial),
2264
2307
  "benchmark_depth_range_m": protocol_benchmark_range,
@@ -2302,6 +2345,7 @@ def _evaluate(
2302
2345
  ),
2303
2346
  },
2304
2347
  "modality_params": depth_meta,
2348
+ "gt_sky_masking": depth_results.get("gt_sky_masking"),
2305
2349
  "sky_segmentation": _sky_segmentation_meta(
2306
2350
  depth_results, segmentation_key, skipped_inputs
2307
2351
  ),
@@ -2438,6 +2482,7 @@ def _evaluate(
2438
2482
  ),
2439
2483
  ds_name=ds_name,
2440
2484
  iou_only=iou_only_inputs,
2485
+ missing_segmentation=args.missing_segmentation,
2441
2486
  )
2442
2487
  et_eval_datasets["sparse_depth"] = sparse_depth_dataset
2443
2488
 
@@ -2523,6 +2568,9 @@ def _evaluate(
2523
2568
  population={
2524
2569
  "samples": sparse_depth_dataset_info.get("num_pairs"),
2525
2570
  "gt_sky_masking": bool(args.mask_sky),
2571
+ "missing_segmentation": (
2572
+ args.missing_segmentation if args.mask_sky else None
2573
+ ),
2526
2574
  "pred_sky_masking": bool(args.mask_sky_prediction),
2527
2575
  "evaluated_resolution": evaluated_resolution(
2528
2576
  sparse_depth_spatial
@@ -2577,6 +2625,7 @@ def _evaluate(
2577
2625
  ),
2578
2626
  },
2579
2627
  "modality_params": sparse_depth_meta,
2628
+ "gt_sky_masking": sparse_depth_results.get("gt_sky_masking"),
2580
2629
  "sky_segmentation": _sky_segmentation_meta(
2581
2630
  sparse_depth_results, segmentation_key, skipped_inputs
2582
2631
  ),
@@ -2707,6 +2756,7 @@ def _evaluate(
2707
2756
  segmentation_modality_key=segmentation_key or "segmentation",
2708
2757
  ),
2709
2758
  ds_name=ds_name,
2759
+ missing_segmentation=args.missing_segmentation,
2710
2760
  )
2711
2761
  et_eval_datasets["rgb"] = rgb_dataset
2712
2762
 
@@ -2776,6 +2826,9 @@ def _evaluate(
2776
2826
  population={
2777
2827
  "samples": rgb_dataset_info.get("num_pairs"),
2778
2828
  "gt_sky_masking": bool(args.mask_sky),
2829
+ "missing_segmentation": (
2830
+ args.missing_segmentation if args.mask_sky else None
2831
+ ),
2779
2832
  "evaluated_resolution": evaluated_resolution(rgb_spatial),
2780
2833
  "benchmark_depth_range_m": protocol_benchmark_range,
2781
2834
  },
@@ -2807,6 +2860,7 @@ def _evaluate(
2807
2860
  ),
2808
2861
  },
2809
2862
  "modality_params": rgb_meta,
2863
+ "gt_sky_masking": rgb_results.get("gt_sky_masking"),
2810
2864
  "eval_params": {
2811
2865
  "sky_masking": args.mask_sky,
2812
2866
  "fid_backend": args.rgb_fid_backend,
@@ -3028,6 +3082,7 @@ def _evaluate(
3028
3082
  segmentation_modality_key=segmentation_key or "segmentation",
3029
3083
  ),
3030
3084
  ds_name=ds_name,
3085
+ missing_segmentation=args.missing_segmentation,
3031
3086
  )
3032
3087
  et_eval_datasets["points_3d"] = points_3d_dataset
3033
3088
 
@@ -3099,6 +3154,9 @@ def _evaluate(
3099
3154
  population={
3100
3155
  "samples": p3_dataset_info.get("num_pairs"),
3101
3156
  "gt_sky_masking": bool(args.mask_sky),
3157
+ "missing_segmentation": (
3158
+ args.missing_segmentation if args.mask_sky else None
3159
+ ),
3102
3160
  "evaluated_resolution": evaluated_resolution(p3_spatial),
3103
3161
  },
3104
3162
  method={
@@ -3130,6 +3188,7 @@ def _evaluate(
3130
3188
  ),
3131
3189
  },
3132
3190
  "modality_params": points_3d_meta,
3191
+ "gt_sky_masking": points_3d_results.get("gt_sky_masking"),
3133
3192
  "eval_params": {
3134
3193
  "sky_masking": args.mask_sky,
3135
3194
  "points_3d_alignment_mode": args.points_3d_alignment,
@@ -3223,6 +3282,7 @@ def _evaluate(
3223
3282
  segmentation_split=segmentation_split,
3224
3283
  ),
3225
3284
  ds_name=ds_name,
3285
+ missing_segmentation=args.missing_segmentation,
3226
3286
  )
3227
3287
  p3ds_meta = get_points_3d_metadata(points_3d_sparse_dataset)
3228
3288
  p3s_alignment_mode = args.points_3d_alignment
@@ -3298,6 +3358,9 @@ def _evaluate(
3298
3358
  population={
3299
3359
  "samples": p3s_dataset_info.get("num_pairs"),
3300
3360
  "gt_sky_masking": bool(args.mask_sky),
3361
+ "missing_segmentation": (
3362
+ args.missing_segmentation if args.mask_sky else None
3363
+ ),
3301
3364
  "evaluated_resolution": evaluated_resolution(p3s_spatial),
3302
3365
  },
3303
3366
  method={
@@ -3337,6 +3400,7 @@ def _evaluate(
3337
3400
  ),
3338
3401
  },
3339
3402
  "modality_params": p3ds_meta,
3403
+ "gt_sky_masking": points_3d_sparse_results.get("gt_sky_masking"),
3340
3404
  "eval_params": {
3341
3405
  "sky_masking": args.mask_sky,
3342
3406
  "alignment_mode": p3s_alignment_mode,
@@ -1117,6 +1117,60 @@ def build_rgb_eval_dataset(
1117
1117
  )
1118
1118
 
1119
1119
 
1120
+ class PartialInputDataset:
1121
+ """Every frame of *complete*, with an optional input where a frame has it.
1122
+
1123
+ euler-loading intersects every modality by ID, so an input such as GT
1124
+ segmentation that covers only some frames removes the others from the
1125
+ pairing. This dataset keeps them: *complete* is built without the input
1126
+ and *with_input* with it, from the same other inputs, so its frames are a
1127
+ subset of *complete*'s. A frame of *with_input* is read from it, any
1128
+ other frame from *complete*, whose sample then has no entry for the
1129
+ input. Frames keep *complete*'s order and identities.
1130
+
1131
+ Public attributes such as ``get_modality_metadata`` and ``modality_paths``
1132
+ come from *with_input*, which describes every input.
1133
+ """
1134
+
1135
+ def __init__(self, complete: Any, with_input: Any) -> None:
1136
+ self._complete = complete
1137
+ self._with_input = with_input
1138
+ index = {
1139
+ with_input.sample_full_id(j): j for j in range(len(with_input))
1140
+ }
1141
+ self._source = [
1142
+ index.pop(complete.sample_full_id(i), None) for i in range(len(complete))
1143
+ ]
1144
+ if index:
1145
+ raise ValueError(
1146
+ f"{len(index)} frames with the optional input are not among the "
1147
+ "frames without it; both datasets must share their other inputs"
1148
+ )
1149
+
1150
+ @property
1151
+ def frames_without_input(self) -> int:
1152
+ return sum(source is None for source in self._source)
1153
+
1154
+ def __len__(self) -> int:
1155
+ return len(self._source)
1156
+
1157
+ def __getitem__(self, index: int) -> dict[str, Any]:
1158
+ source = self._source[index]
1159
+ if source is None:
1160
+ return self._complete[index]
1161
+ return self._with_input[source]
1162
+
1163
+ def sample_full_id(self, index: int, modality_name: Optional[str] = None) -> str:
1164
+ return self._complete.sample_full_id(index, modality_name)
1165
+
1166
+ def __getattr__(self, name: str) -> Any:
1167
+ # Private and dunder lookups (pickling, ``_lookups``) are not forwarded:
1168
+ # they would describe *with_input*'s frames, not this dataset's.
1169
+ if name.startswith("_"):
1170
+ raise AttributeError(name)
1171
+ return getattr(self._with_input, name)
1172
+
1173
+
1120
1174
  def count_ids_dropped_by(
1121
1175
  dataset: Any, modality: str, *, optional: tuple[str, ...] = ()
1122
1176
  ) -> Optional[int]:
@@ -588,6 +588,14 @@ class _SkyMaskCoverage:
588
588
  elif not np.all(sky_valid):
589
589
  self.with_sky += 1
590
590
 
591
+ def info(self) -> dict:
592
+ """Return provenance for ``meta.gt_sky_masking`` in ``eval.json``."""
593
+ return {
594
+ "samples": self.samples,
595
+ "samples_without_segmentation": self.missing,
596
+ "samples_with_sky": self.with_sky,
597
+ }
598
+
591
599
  def warn_if_empty(self, modality: str) -> None:
592
600
  if self.missing:
593
601
  print(
@@ -2042,6 +2050,8 @@ def evaluate_depth_samples(
2042
2050
  result["sky_segmentation_info"] = sky_iou_scores.info()
2043
2051
  if pred_sky_masking is not None:
2044
2052
  result["pred_sky_masking"] = pred_sky_masking.info()
2053
+ if sky_mask_enabled:
2054
+ result["gt_sky_masking"] = sky_coverage.info()
2045
2055
  return _finalize_depth_distributions(
2046
2056
  result, distribution_config, "depth", benchmark_depth_range, sky_depth
2047
2057
  )
@@ -2676,6 +2686,8 @@ def evaluate_sparse_depth_samples(
2676
2686
  result["sky_segmentation_info"] = sky_iou_scores.info()
2677
2687
  if pred_sky_masking is not None:
2678
2688
  result["pred_sky_masking"] = pred_sky_masking.info()
2689
+ if sky_mask_enabled:
2690
+ result["gt_sky_masking"] = sky_coverage.info()
2679
2691
  return _finalize_depth_distributions(
2680
2692
  result, distribution_config, "sparse_depth",
2681
2693
  benchmark_depth_range, sky_depth,
@@ -3246,6 +3258,7 @@ def evaluate_rgb_samples(
3246
3258
  return {
3247
3259
  "rgb": rgb_results,
3248
3260
  "rgb_benchmark": rgb_benchmark,
3261
+ "gt_sky_masking": sky_coverage.info() if sky_mask_enabled else None,
3249
3262
  "per_file_metrics": per_file_metrics,
3250
3263
  "dataset_info": {
3251
3264
  "num_pairs": num_samples,
@@ -4083,6 +4096,8 @@ def evaluate_points_3d_samples(
4083
4096
  else None,
4084
4097
  },
4085
4098
  }
4099
+ if sky_mask_enabled:
4100
+ result["gt_sky_masking"] = sky_coverage.info()
4086
4101
  return finalize_distributions(result, distribution_config, "rmse3d", "points_3d")
4087
4102
  finally:
4088
4103
  for st in stores.values():
@@ -4819,6 +4834,8 @@ def evaluate_points_3d_sparse_samples(
4819
4834
  else None,
4820
4835
  },
4821
4836
  }
4837
+ if sky_mask_enabled:
4838
+ result["gt_sky_masking"] = sky_coverage.info()
4822
4839
  return finalize_distributions(result, distribution_config, "rmse3d", "points_3d")
4823
4840
  finally:
4824
4841
  for st in stores.values():
@@ -30,7 +30,7 @@ from .config_paths import (
30
30
 
31
31
  #: Version of the ``protocol`` block. Renaming or removing a key is a major
32
32
  #: bump; adding one is a minor bump.
33
- SCHEMA_VERSION = "1.1"
33
+ SCHEMA_VERSION = "1.2"
34
34
 
35
35
  #: Bump whenever a release changes metric values for identical inputs and
36
36
  #: settings, and say so in the CHANGELOG. Evaluations with different revisions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-eval
3
- Version: 2.35.0
3
+ Version: 2.36.0
4
4
  Summary: Evaluation toolkit for depth, RGB, ray and 3D point-map predictions
5
5
  Author-email: Daniel Rothenpieler <rothenpielerdaniel@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "euler-eval"
7
- version = "2.35.0"
7
+ version = "2.36.0"
8
8
  description = "Evaluation toolkit for depth, RGB, ray and 3D point-map predictions"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -195,3 +195,130 @@ def test_every_builder_pairs_segmentation_by_id(monkeypatch, build, kwargs):
195
195
  dataset = build(**kwargs, segmentation_path="/seg")
196
196
  assert dataset.modalities["segmentation"].loader == "sky"
197
197
  assert "segmentation" not in dataset.hierarchical_modalities
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # --missing-segmentation unmasked
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ def test_missing_segmentation_unmasked_evaluates_those_frames_unmasked(
206
+ monkeypatch, capsys, flat_frames
207
+ ):
208
+ _patch_depth_metrics(monkeypatch)
209
+ config_path, paths = flat_frames(segmentation_frames=("f0", "f1"))
210
+
211
+ saved = _run(
212
+ monkeypatch, config_path, paths["pred"],
213
+ "--mask-sky", "--missing-segmentation", "unmasked",
214
+ )
215
+
216
+ assert saved["dataset_info"]["num_pairs"] == 3
217
+ # f0 and f1 are masked with their own sky; f2 keeps its sky row: 900 m
218
+ # error on 4 of 16 pixels.
219
+ assert _per_file_mae(saved) == {"f0": 0.0, "f1": 0.0, "f2": 225.0}
220
+ assert saved["meta"]["gt_sky_masking"] == {
221
+ "samples": 3, "samples_without_segmentation": 1, "samples_with_sky": 2,
222
+ }
223
+ parameters = saved["protocol"]["parameters"]
224
+ assert parameters["population.gt_sky_masking"] is True
225
+ assert parameters["population.missing_segmentation"] == "unmasked"
226
+ out = capsys.readouterr().out
227
+ assert (
228
+ "the GT segmentation of 'model' lacks 1 pairs that the other inputs "
229
+ "share; with --missing-segmentation unmasked they are evaluated without "
230
+ "GT sky masking"
231
+ ) in out
232
+ assert "found no GT segmentation of their own frame for 1 of the 3" in out
233
+
234
+
235
+ def test_missing_segmentation_unmasked_matches_drop_on_covered_frames(
236
+ monkeypatch, flat_frames
237
+ ):
238
+ _patch_depth_metrics(monkeypatch)
239
+ config_path, paths = flat_frames(segmentation_frames=("f0", "f1"))
240
+
241
+ dropped = _run(monkeypatch, config_path, paths["pred"], "--mask-sky")
242
+ kept = _run(
243
+ monkeypatch, config_path, paths["pred"],
244
+ "--mask-sky", "--missing-segmentation", "unmasked",
245
+ )
246
+
247
+ covered = {k: v for k, v in _per_file_mae(kept).items() if k != "f2"}
248
+ assert covered == _per_file_mae(dropped)
249
+ assert dropped["protocol"]["parameters"]["population.missing_segmentation"] == "drop"
250
+
251
+
252
+ def test_missing_segmentation_unmasked_scores_sky_iou_on_covered_frames(
253
+ monkeypatch, flat_frames
254
+ ):
255
+ _patch_depth_metrics(monkeypatch)
256
+ config_path, paths = flat_frames(segmentation_frames=("f0", "f1"))
257
+
258
+ saved = _run(
259
+ monkeypatch, config_path, paths["pred"],
260
+ "--sky-iou", "--sky-depth", "50", "--missing-segmentation", "unmasked",
261
+ )
262
+
263
+ assert saved["dataset_info"]["num_pairs"] == 3
264
+ meta = saved["meta"]["sky_segmentation"]
265
+ assert meta["implicit"]["available"] is True
266
+ assert meta["samples"] == 3
267
+ assert meta["samples_without_gt"] == 1
268
+ # Without --mask-sky, the policy is not a comparison parameter.
269
+ assert saved["protocol"]["parameters"]["population.missing_segmentation"] is None
270
+
271
+
272
+ def test_missing_segmentation_unmasked_with_prefetch_workers(monkeypatch, flat_frames):
273
+ """Worker processes receive the combined dataset intact."""
274
+ _patch_depth_metrics(monkeypatch)
275
+ config_path, paths = flat_frames(segmentation_frames=("f0", "f1"))
276
+
277
+ saved = _run(
278
+ monkeypatch, config_path, paths["pred"],
279
+ "--mask-sky", "--missing-segmentation", "unmasked", "--num-workers", "2",
280
+ )
281
+
282
+ assert _per_file_mae(saved) == {"f0": 0.0, "f1": 0.0, "f2": 225.0}
283
+
284
+
285
+ class _Frames:
286
+ def __init__(self, ids, tag):
287
+ self.ids, self.tag = list(ids), tag
288
+ self.public = f"{tag} attribute"
289
+
290
+ def __len__(self):
291
+ return len(self.ids)
292
+
293
+ def __getitem__(self, index):
294
+ return {"id": self.ids[index], "source": self.tag}
295
+
296
+ def sample_full_id(self, index, modality_name=None):
297
+ return f"/{self.ids[index]}"
298
+
299
+
300
+ def test_partial_input_dataset_keeps_every_frame_in_order():
301
+ import pickle
302
+
303
+ dataset = data.PartialInputDataset(
304
+ _Frames(["a", "b", "c"], "complete"), _Frames(["a", "c"], "with")
305
+ )
306
+
307
+ assert len(dataset) == 3
308
+ assert [dataset[i] for i in range(3)] == [
309
+ {"id": "a", "source": "with"},
310
+ {"id": "b", "source": "complete"},
311
+ {"id": "c", "source": "with"},
312
+ ]
313
+ assert dataset.frames_without_input == 1
314
+ assert dataset.sample_full_id(1) == "/b"
315
+ assert dataset.public == "with attribute"
316
+ with pytest.raises(AttributeError):
317
+ dataset._lookups
318
+ restored = pickle.loads(pickle.dumps(dataset))
319
+ assert [restored[i]["source"] for i in range(3)] == ["with", "complete", "with"]
320
+
321
+
322
+ def test_partial_input_dataset_rejects_frames_missing_from_the_complete_set():
323
+ with pytest.raises(ValueError, match="1 frames with the optional input"):
324
+ data.PartialInputDataset(_Frames(["a"], "complete"), _Frames(["a", "z"], "with"))
@@ -37,6 +37,7 @@ EXPECTED_KEYS = {
37
37
  "depth": _reference_keys("calibration", "segmentation") | {
38
38
  "population.samples",
39
39
  "population.gt_sky_masking",
40
+ "population.missing_segmentation",
40
41
  "population.pred_sky_masking",
41
42
  "population.evaluated_resolution",
42
43
  "population.benchmark_depth_range_m",
@@ -49,6 +50,7 @@ EXPECTED_KEYS = {
49
50
  "sparsedepth": _reference_keys(*SPARSE_INPUTS) | {
50
51
  "population.samples",
51
52
  "population.gt_sky_masking",
53
+ "population.missing_segmentation",
52
54
  "population.pred_sky_masking",
53
55
  "population.evaluated_resolution",
54
56
  "population.benchmark_depth_range_m",
@@ -60,6 +62,7 @@ EXPECTED_KEYS = {
60
62
  "rgb": _reference_keys("depth", "calibration", "segmentation") | {
61
63
  "population.samples",
62
64
  "population.gt_sky_masking",
65
+ "population.missing_segmentation",
63
66
  "population.evaluated_resolution",
64
67
  "population.benchmark_depth_range_m",
65
68
  "method.metrics_revision",
@@ -75,6 +78,7 @@ EXPECTED_KEYS = {
75
78
  "points3d": _reference_keys("intrinsics", "calibration", "segmentation") | {
76
79
  "population.samples",
77
80
  "population.gt_sky_masking",
81
+ "population.missing_segmentation",
78
82
  "population.evaluated_resolution",
79
83
  "method.metrics_revision",
80
84
  "method.points_3d_alignment",
@@ -83,6 +87,7 @@ EXPECTED_KEYS = {
83
87
  "points3d_sparse": _reference_keys(*SPARSE_INPUTS) | {
84
88
  "population.samples",
85
89
  "population.gt_sky_masking",
90
+ "population.missing_segmentation",
86
91
  "population.evaluated_resolution",
87
92
  "method.metrics_revision",
88
93
  "method.points_3d_alignment",
@@ -250,6 +255,7 @@ def test_depth_parameters(monkeypatch, tmp_path, depth_paths):
250
255
  assert parameters["reference.calibration.dataset"] is None
251
256
  assert parameters["population.samples"] == 2
252
257
  assert parameters["population.gt_sky_masking"] is False
258
+ assert parameters["population.missing_segmentation"] is None
253
259
  assert parameters["population.evaluated_resolution"] == [2, 2]
254
260
  assert parameters["population.benchmark_depth_range_m"] is None
255
261
  assert parameters["method.metrics_revision"] == METRICS_REVISION
@@ -267,7 +273,12 @@ def test_depth_parameters(monkeypatch, tmp_path, depth_paths):
267
273
  (("--depth-alignment", "affine"), {"method.depth_alignment"}),
268
274
  # The config has a sky_mask, which both settings then score with.
269
275
  (("--sky-depth", "50"), {"method.sky_depth_m", "method.pred_sky_mask"}),
270
- (("--mask-sky",), {"population.gt_sky_masking"}),
276
+ (
277
+ ("--mask-sky",),
278
+ {"population.gt_sky_masking", "population.missing_segmentation"},
279
+ ),
280
+ # Without --mask-sky, what happens to unsegmented frames changes nothing.
281
+ (("--missing-segmentation", "unmasked"), set()),
271
282
  (
272
283
  ("--mask-sky-prediction",),
273
284
  {"population.pred_sky_masking", "method.pred_sky_mask"},
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes