euler-loading 2.2.1__tar.gz → 2.3.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 (48) hide show
  1. {euler_loading-2.2.1 → euler_loading-2.3.0}/PKG-INFO +1 -1
  2. {euler_loading-2.2.1 → euler_loading-2.3.0}/README.md +13 -0
  3. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/__init__.py +2 -0
  4. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/dataset.py +3 -3
  5. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/preprocessing.py +106 -0
  6. {euler_loading-2.2.1 → euler_loading-2.3.0}/pyproject.toml +1 -1
  7. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/test_preprocessing.py +60 -2
  8. {euler_loading-2.2.1 → euler_loading-2.3.0}/.github/workflows/workflow.yml +0 -0
  9. {euler_loading-2.2.1 → euler_loading-2.3.0}/.gitignore +0 -0
  10. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/_dataset_contract.py +0 -0
  11. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/_ds_crawler_utils.py +0 -0
  12. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/_metadata.py +0 -0
  13. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/_resolution.py +0 -0
  14. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/_writing.py +0 -0
  15. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/indexing.py +0 -0
  16. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/__init__.py +0 -0
  17. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/_annotations.py +0 -0
  18. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/_writer_utils.py +0 -0
  19. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/contracts.py +0 -0
  20. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/cpu/__init__.py +0 -0
  21. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/cpu/generic.py +0 -0
  22. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  23. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  24. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  25. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/generate/__init__.py +0 -0
  26. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/generate/__main__.py +0 -0
  27. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/generate/loaders.json +0 -0
  28. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/generic.py +0 -0
  29. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/gpu/__init__.py +0 -0
  30. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/gpu/generic.py +0 -0
  31. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  32. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  33. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  34. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  35. {euler_loading-2.2.1 → euler_loading-2.3.0}/euler_loading/loaders/vkitti2.py +0 -0
  36. {euler_loading-2.2.1 → euler_loading-2.3.0}/example.py +0 -0
  37. {euler_loading-2.2.1 → euler_loading-2.3.0}/package-lock.json +0 -0
  38. {euler_loading-2.2.1 → euler_loading-2.3.0}/sample_rds.py +0 -0
  39. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/__init__.py +0 -0
  40. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/conftest.py +0 -0
  41. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/example_rds_calib.json +0 -0
  42. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/test_dataset.py +0 -0
  43. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/test_indexing.py +0 -0
  44. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/test_loaders.py +0 -0
  45. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/test_real_dataset.py +0 -0
  46. {euler_loading-2.2.1 → euler_loading-2.3.0}/tests/test_writing.py +0 -0
  47. {euler_loading-2.2.1 → euler_loading-2.3.0}/vkitti_cpu_example_output.json +0 -0
  48. {euler_loading-2.2.1 → euler_loading-2.3.0}/vkitti_gpu_example_output.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-loading
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: Multi-modal PyTorch dataloader using ds-crawler indices
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: ds-crawler
@@ -218,6 +218,19 @@ def mask_sky_in_depth(sample: dict) -> dict:
218
218
  return sample
219
219
  ```
220
220
 
221
+ For common mask-driven replacements, `MaskedValueOverride` can do the same
222
+ without writing a custom callable:
223
+
224
+ ```python
225
+ from euler_loading import MaskedValueOverride
226
+
227
+ replace_sky_depth = MaskedValueOverride(
228
+ target_key="depth",
229
+ mask_key="sky_mask",
230
+ value=300.0,
231
+ )
232
+ ```
233
+
221
234
  ### Built-in spatial preprocessing
222
235
 
223
236
  `euler_loading.SamplePreprocessor` applies shared spatial ops such as resize and crop
@@ -9,6 +9,7 @@ from .loaders.contracts import DenseDepthCodec, DenseDepthLoader, DenseDepthWrit
9
9
  from .preprocessing import (
10
10
  Crop,
11
11
  FieldSpec,
12
+ MaskedValueOverride,
12
13
  Resize,
13
14
  SamplePreprocessor,
14
15
  crop_intrinsics,
@@ -23,6 +24,7 @@ __all__ = [
23
24
  "FileRecord",
24
25
  "Crop",
25
26
  "FieldSpec",
27
+ "MaskedValueOverride",
26
28
  "Modality",
27
29
  "MultiModalDataset",
28
30
  "Resize",
@@ -471,16 +471,16 @@ class MultiModalDataset(_BaseDataset):
471
471
  if callable(bind):
472
472
  bind(self)
473
473
  if self._transforms:
474
- logger.info("Configured %d sample transform(s):", len(self._transforms))
474
+ logger.warning("Configured %d sample transform(s):", len(self._transforms))
475
475
  for idx, transform in enumerate(self._transforms, start=1):
476
- logger.info(
476
+ logger.warning(
477
477
  "Transform %d/%d: %s",
478
478
  idx,
479
479
  len(self._transforms),
480
480
  _describe_transform(transform),
481
481
  )
482
482
  else:
483
- logger.info("Configured 0 sample transforms.")
483
+ logger.warning("Configured 0 sample transforms.")
484
484
 
485
485
  def get_modality_metadata(self, modality_name: str) -> dict[str, Any]:
486
486
  """Return the metadata dict for a given modality name."""
@@ -17,6 +17,7 @@ except ImportError: # pragma: no cover - exercised in CPU-only environments
17
17
 
18
18
  __all__ = [
19
19
  "FieldSpec",
20
+ "MaskedValueOverride",
20
21
  "Resize",
21
22
  "Crop",
22
23
  "SamplePreprocessor",
@@ -310,6 +311,111 @@ class Crop:
310
311
  return top, left, target_h, target_w
311
312
 
312
313
 
314
+ @dataclass(frozen=True)
315
+ class MaskedValueOverride:
316
+ """Set values in one sample field wherever a boolean mask field is true."""
317
+
318
+ target_key: str
319
+ mask_key: str
320
+ value: Any
321
+ invert_mask: bool = False
322
+ copy: bool = True
323
+ ignore_missing: bool = False
324
+
325
+ @classmethod
326
+ def from_config(cls, cfg: Mapping[str, Any]) -> "MaskedValueOverride":
327
+ if not isinstance(cfg, Mapping):
328
+ raise TypeError(
329
+ "MaskedValueOverride config must be a mapping, "
330
+ f"got {type(cfg).__name__}."
331
+ )
332
+ return cls(
333
+ target_key=str(cfg.get("target_key", cfg.get("target", ""))),
334
+ mask_key=str(cfg.get("mask_key", cfg.get("mask", ""))),
335
+ value=cfg.get("value"),
336
+ invert_mask=bool(cfg.get("invert_mask", False)),
337
+ copy=bool(cfg.get("copy", True)),
338
+ ignore_missing=bool(cfg.get("ignore_missing", False)),
339
+ )
340
+
341
+ def __post_init__(self) -> None:
342
+ if not self.target_key:
343
+ raise ValueError("MaskedValueOverride requires target_key.")
344
+ if not self.mask_key:
345
+ raise ValueError("MaskedValueOverride requires mask_key.")
346
+
347
+ def describe(self) -> str:
348
+ parts = [
349
+ f"masked_value_override(target={self.target_key}",
350
+ f"mask={self.mask_key}",
351
+ f"value={self.value!r}",
352
+ ]
353
+ if self.invert_mask:
354
+ parts.append("invert_mask=True")
355
+ if not self.copy:
356
+ parts.append("copy=False")
357
+ if self.ignore_missing:
358
+ parts.append("ignore_missing=True")
359
+ return ", ".join(parts) + ")"
360
+
361
+ def _missing(self, key: str) -> None:
362
+ if self.ignore_missing:
363
+ return
364
+ raise KeyError(f"MaskedValueOverride missing sample key: {key!r}")
365
+
366
+ def __call__(self, sample: dict[str, Any]) -> dict[str, Any]:
367
+ if self.target_key not in sample:
368
+ self._missing(self.target_key)
369
+ return sample
370
+ if self.mask_key not in sample:
371
+ self._missing(self.mask_key)
372
+ return sample
373
+
374
+ target = sample[self.target_key]
375
+ mask = sample[self.mask_key]
376
+
377
+ processed = dict(sample) if self.copy else sample
378
+
379
+ if _is_torch_tensor(target):
380
+ if _is_torch_tensor(mask):
381
+ mask_tensor = mask.to(device=target.device, dtype=torch.bool)
382
+ else:
383
+ mask_tensor = torch.as_tensor(
384
+ mask,
385
+ device=target.device,
386
+ dtype=torch.bool,
387
+ )
388
+ if self.invert_mask:
389
+ mask_tensor = ~mask_tensor
390
+ if tuple(mask_tensor.shape) != tuple(target.shape):
391
+ mask_tensor = torch.broadcast_to(mask_tensor, target.shape)
392
+ value_tensor = torch.as_tensor(
393
+ self.value,
394
+ device=target.device,
395
+ dtype=target.dtype,
396
+ )
397
+ result = target.clone() if self.copy else target
398
+ result[mask_tensor] = value_tensor
399
+ processed[self.target_key] = result
400
+ return processed
401
+
402
+ if isinstance(target, np.ndarray):
403
+ mask_array = np.asarray(mask, dtype=np.bool_)
404
+ if self.invert_mask:
405
+ mask_array = ~mask_array
406
+ if tuple(mask_array.shape) != tuple(target.shape):
407
+ mask_array = np.broadcast_to(mask_array, target.shape)
408
+ result = np.array(target, copy=self.copy)
409
+ result[mask_array] = self.value
410
+ processed[self.target_key] = result
411
+ return processed
412
+
413
+ raise TypeError(
414
+ f"MaskedValueOverride target {self.target_key!r} must be a torch.Tensor "
415
+ f"or numpy.ndarray, got {type(target).__name__}."
416
+ )
417
+
418
+
313
419
  def infer_field_spec(name: str, value: Any | None = None) -> FieldSpec | None:
314
420
  """Best-effort field inference for common modality names."""
315
421
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "euler-loading"
3
- version = "2.2.1"
3
+ version = "2.3.0"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  dependencies = [
@@ -9,7 +9,7 @@ import numpy as np
9
9
  import pytest
10
10
  import torch
11
11
 
12
- from euler_loading import Modality, MultiModalDataset
12
+ from euler_loading import MaskedValueOverride, Modality, MultiModalDataset
13
13
  from euler_loading.preprocessing import SamplePreprocessor
14
14
 
15
15
 
@@ -143,6 +143,63 @@ class TestSamplePreprocessorNumpy:
143
143
  assert np.allclose(processed["intrinsics"], expected_intrinsics)
144
144
 
145
145
 
146
+ class TestMaskedValueOverride:
147
+ def test_overrides_torch_target_with_broadcast_mask(self):
148
+ depth = torch.tensor(
149
+ [
150
+ [[1.0, 2.0], [3.0, 4.0]],
151
+ [[5.0, 6.0], [7.0, 8.0]],
152
+ ],
153
+ dtype=torch.float32,
154
+ )
155
+ sky_mask = torch.tensor([[[True, False], [False, True]]])
156
+
157
+ transform = MaskedValueOverride(
158
+ target_key="depth",
159
+ mask_key="sky_mask",
160
+ value=0.5,
161
+ )
162
+ processed = transform({"depth": depth, "sky_mask": sky_mask})
163
+
164
+ expected = torch.tensor(
165
+ [
166
+ [[0.5, 2.0], [3.0, 0.5]],
167
+ [[0.5, 6.0], [7.0, 0.5]],
168
+ ],
169
+ dtype=torch.float32,
170
+ )
171
+ assert torch.equal(processed["depth"], expected)
172
+ assert torch.equal(
173
+ depth,
174
+ torch.tensor(
175
+ [
176
+ [[1.0, 2.0], [3.0, 4.0]],
177
+ [[5.0, 6.0], [7.0, 8.0]],
178
+ ],
179
+ dtype=torch.float32,
180
+ ),
181
+ )
182
+
183
+ def test_overrides_numpy_target_with_inverted_mask(self):
184
+ depth = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)
185
+ valid_mask = np.array([[True, False], [False, True]], dtype=np.bool_)
186
+
187
+ transform = MaskedValueOverride(
188
+ target_key="depth",
189
+ mask_key="valid_mask",
190
+ value=9.0,
191
+ invert_mask=True,
192
+ )
193
+ processed = transform({"depth": depth, "valid_mask": valid_mask})
194
+
195
+ expected = np.array([[1.0, 9.0], [9.0, 4.0]], dtype=np.float32)
196
+ assert np.array_equal(processed["depth"], expected)
197
+ assert np.array_equal(
198
+ depth,
199
+ np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32),
200
+ )
201
+
202
+
146
203
  class TestDatasetTransformBinding:
147
204
  def test_dataset_binds_modality_types_into_preprocessor(self):
148
205
  rgb_index = _flat_index("rgb", ["f001"])
@@ -208,7 +265,7 @@ class TestDatasetTransformBinding:
208
265
  with patch(
209
266
  "euler_loading.dataset.index_dataset_from_path",
210
267
  return_value=rgb_index,
211
- ), caplog.at_level(logging.INFO, logger="euler_loading.dataset"):
268
+ ), caplog.at_level(logging.WARNING, logger="euler_loading.dataset"):
212
269
  MultiModalDataset(
213
270
  modalities={
214
271
  "rgb": Modality(
@@ -222,6 +279,7 @@ class TestDatasetTransformBinding:
222
279
 
223
280
  messages = [record.message for record in caplog.records]
224
281
  assert messages.count("Configured 2 sample transform(s):") == 1
282
+ assert all(record.levelno == logging.WARNING for record in caplog.records)
225
283
 
226
284
  joined = "\n".join(messages)
227
285
  assert "SamplePreprocessor(" in joined
File without changes
File without changes