euler-loading 2.2.2__tar.gz → 2.4.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 (49) hide show
  1. {euler_loading-2.2.2 → euler_loading-2.4.0}/PKG-INFO +1 -1
  2. {euler_loading-2.2.2 → euler_loading-2.4.0}/README.md +13 -0
  3. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/__init__.py +2 -0
  4. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/dataset.py +82 -0
  5. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/preprocessing.py +106 -0
  6. {euler_loading-2.2.2 → euler_loading-2.4.0}/package-lock.json +1 -1
  7. {euler_loading-2.2.2 → euler_loading-2.4.0}/pyproject.toml +1 -1
  8. euler_loading-2.4.0/tests/test_id_schema.py +116 -0
  9. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/test_preprocessing.py +58 -1
  10. {euler_loading-2.2.2 → euler_loading-2.4.0}/.github/workflows/workflow.yml +0 -0
  11. {euler_loading-2.2.2 → euler_loading-2.4.0}/.gitignore +0 -0
  12. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/_dataset_contract.py +0 -0
  13. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/_ds_crawler_utils.py +0 -0
  14. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/_metadata.py +0 -0
  15. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/_resolution.py +0 -0
  16. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/_writing.py +0 -0
  17. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/indexing.py +0 -0
  18. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/__init__.py +0 -0
  19. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/_annotations.py +0 -0
  20. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/_writer_utils.py +0 -0
  21. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/contracts.py +0 -0
  22. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/cpu/__init__.py +0 -0
  23. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/cpu/generic.py +0 -0
  24. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/cpu/generic_dense_depth.py +0 -0
  25. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/cpu/real_drive_sim.py +0 -0
  26. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/cpu/vkitti2.py +0 -0
  27. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/generate/__init__.py +0 -0
  28. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/generate/__main__.py +0 -0
  29. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/generate/loaders.json +0 -0
  30. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/generic.py +0 -0
  31. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/gpu/__init__.py +0 -0
  32. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/gpu/generic.py +0 -0
  33. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/gpu/generic_dense_depth.py +0 -0
  34. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/gpu/real_drive_sim.py +0 -0
  35. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/gpu/vkitti2.py +0 -0
  36. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/real_drive_sim.py +0 -0
  37. {euler_loading-2.2.2 → euler_loading-2.4.0}/euler_loading/loaders/vkitti2.py +0 -0
  38. {euler_loading-2.2.2 → euler_loading-2.4.0}/example.py +0 -0
  39. {euler_loading-2.2.2 → euler_loading-2.4.0}/sample_rds.py +0 -0
  40. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/__init__.py +0 -0
  41. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/conftest.py +0 -0
  42. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/example_rds_calib.json +0 -0
  43. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/test_dataset.py +0 -0
  44. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/test_indexing.py +0 -0
  45. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/test_loaders.py +0 -0
  46. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/test_real_dataset.py +0 -0
  47. {euler_loading-2.2.2 → euler_loading-2.4.0}/tests/test_writing.py +0 -0
  48. {euler_loading-2.2.2 → euler_loading-2.4.0}/vkitti_cpu_example_output.json +0 -0
  49. {euler_loading-2.2.2 → euler_loading-2.4.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.2
3
+ Version: 2.4.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",
@@ -118,6 +118,36 @@ def _get_index_meta(index_output: Mapping[str, Any]) -> dict[str, Any] | None:
118
118
  return None
119
119
 
120
120
 
121
+ def _extract_id_schema(index_output: Mapping[str, Any]) -> dict[str, Any]:
122
+ """Pull id/hierarchy separators from a ds-crawler output payload.
123
+
124
+ Supports both modern (``indexing.id.join_char``) and legacy
125
+ (top-level ``id_regex_join_char``) layouts. Missing keys fall back to
126
+ ds-crawler defaults so the returned dict is always usable.
127
+ """
128
+ indexing = index_output.get("indexing")
129
+ indexing_map = indexing if isinstance(indexing, Mapping) else {}
130
+ id_cfg = indexing_map.get("id")
131
+ id_map = id_cfg if isinstance(id_cfg, Mapping) else {}
132
+ hierarchy_cfg = indexing_map.get("hierarchy")
133
+ hierarchy_map = hierarchy_cfg if isinstance(hierarchy_cfg, Mapping) else {}
134
+
135
+ id_join_char = id_map.get("join_char") or index_output.get("id_regex_join_char") or "+"
136
+ hierarchy_separator = hierarchy_map.get("separator") or "-"
137
+ id_regex = id_map.get("regex") or index_output.get("id_regex")
138
+ hierarchy_regex = hierarchy_map.get("regex") or index_output.get("hierarchy_regex")
139
+
140
+ schema: dict[str, Any] = {
141
+ "hierarchy_separator": hierarchy_separator,
142
+ "id_join_char": id_join_char,
143
+ }
144
+ if id_regex:
145
+ schema["id_regex"] = id_regex
146
+ if hierarchy_regex:
147
+ schema["hierarchy_regex"] = hierarchy_regex
148
+ return schema
149
+
150
+
121
151
  #TODO: might want to add slots=True in a 3.10+ only codebase
122
152
  @dataclass(frozen=True)
123
153
  class Modality:
@@ -488,6 +518,58 @@ class MultiModalDataset(_BaseDataset):
488
518
  meta = _get_index_meta(index)
489
519
  return meta or {}
490
520
 
521
+ def describe_id_schema(self) -> dict[str, Any]:
522
+ """Return the id-construction schema used by this dataset.
523
+
524
+ Downstream consumers that want to split or match ``sample["full_id"]``
525
+ / ``sample["id"]`` values back to ds-crawler fields need to know the
526
+ separators used to build them. This method exposes that schema so it
527
+ can be embedded in experiment manifests.
528
+
529
+ Schema keys:
530
+ - ``hierarchy_separator``: the character between the named
531
+ capture group *name* and its *value* within a single id part
532
+ (ds-crawler ``indexing.hierarchy.separator``, typically ``"-"``).
533
+ - ``id_join_char``: the character between id parts (ds-crawler
534
+ ``indexing.id.join_char`` or legacy ``id_regex_join_char``,
535
+ default ``"+"``).
536
+ - ``full_id_separator``: the character between hierarchy levels
537
+ and the file id in ``sample["full_id"]`` (currently ``"/"``).
538
+ - ``id_regex`` / ``hierarchy_regex``: the raw regexes, when
539
+ recorded in the ds-crawler output.
540
+ - ``modalities``: per-modality overrides populated only when
541
+ modalities disagree on the above.
542
+
543
+ Example::
544
+
545
+ {
546
+ "hierarchy_separator": "-",
547
+ "id_join_char": "+",
548
+ "full_id_separator": "/",
549
+ "id_regex": "...",
550
+ }
551
+ """
552
+ per_modality: dict[str, dict[str, Any]] = {}
553
+ for name, index in self._index_outputs.items():
554
+ per_modality[name] = _extract_id_schema(index)
555
+
556
+ if not per_modality:
557
+ return {}
558
+
559
+ base = dict(next(iter(per_modality.values())))
560
+ base["full_id_separator"] = "/"
561
+
562
+ overrides: dict[str, dict[str, Any]] = {}
563
+ for mod_name, mod_schema in per_modality.items():
564
+ mod_with_separator = dict(mod_schema)
565
+ mod_with_separator["full_id_separator"] = "/"
566
+ if mod_with_separator != base:
567
+ overrides[mod_name] = mod_with_separator
568
+
569
+ if overrides:
570
+ base["modalities"] = overrides
571
+ return base
572
+
491
573
  def get_modality_index(self, modality_name: str) -> dict[str, Any]:
492
574
  """Return the cached ds-crawler index for a modality."""
493
575
  if modality_name in self._modalities:
@@ -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,5 +1,5 @@
1
1
  {
2
- "name": "l-more",
2
+ "name": "loading-better-save-outputs",
3
3
  "lockfileVersion": 3,
4
4
  "requires": true,
5
5
  "packages": {}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "euler-loading"
3
- version = "2.2.2"
3
+ version = "2.4.0"
4
4
  description = "Multi-modal PyTorch dataloader using ds-crawler indices"
5
5
  requires-python = ">=3.9"
6
6
  dependencies = [
@@ -0,0 +1,116 @@
1
+ """Tests for MultiModalDataset.describe_id_schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from unittest.mock import patch
7
+
8
+ from euler_loading import Modality, MultiModalDataset
9
+
10
+ from .conftest import dummy_loader
11
+
12
+
13
+ def _flat_index(
14
+ file_ids: list[str],
15
+ *,
16
+ indexing: dict[str, Any] | None = None,
17
+ legacy_join_char: str | None = None,
18
+ ) -> dict[str, Any]:
19
+ index: dict[str, Any] = {
20
+ "name": "test-ds",
21
+ "type": "depth",
22
+ "euler_train": {"used_as": "input", "modality_type": "depth"},
23
+ "dataset": {
24
+ "files": [
25
+ {
26
+ "id": fid,
27
+ "path": f"scene/{fid}.png",
28
+ "path_properties": {},
29
+ "basename_properties": {},
30
+ }
31
+ for fid in file_ids
32
+ ]
33
+ },
34
+ }
35
+ if indexing is not None:
36
+ index["indexing"] = indexing
37
+ if legacy_join_char is not None:
38
+ index["id_regex_join_char"] = legacy_join_char
39
+ return index
40
+
41
+
42
+ class TestDescribeIdSchema:
43
+ def test_modern_indexing_block(self):
44
+ index = _flat_index(
45
+ ["f001"],
46
+ indexing={
47
+ "id": {"regex": "^(.+)$", "join_char": "+"},
48
+ "hierarchy": {"separator": "-"},
49
+ },
50
+ )
51
+ with patch(
52
+ "euler_loading.dataset.index_dataset_from_path",
53
+ return_value=index,
54
+ ):
55
+ ds = MultiModalDataset(
56
+ modalities={"depth": Modality("/data/depth", loader=dummy_loader)}
57
+ )
58
+ schema = ds.describe_id_schema()
59
+ assert schema["hierarchy_separator"] == "-"
60
+ assert schema["id_join_char"] == "+"
61
+ assert schema["full_id_separator"] == "/"
62
+ assert schema["id_regex"] == "^(.+)$"
63
+
64
+ def test_legacy_top_level_join_char(self):
65
+ index = _flat_index(["f001"], legacy_join_char="|")
66
+ with patch(
67
+ "euler_loading.dataset.index_dataset_from_path",
68
+ return_value=index,
69
+ ):
70
+ ds = MultiModalDataset(
71
+ modalities={"depth": Modality("/data/depth", loader=dummy_loader)}
72
+ )
73
+ schema = ds.describe_id_schema()
74
+ assert schema["id_join_char"] == "|"
75
+ assert schema["hierarchy_separator"] == "-"
76
+
77
+ def test_missing_indexing_falls_back_to_defaults(self):
78
+ index = _flat_index(["f001"])
79
+ with patch(
80
+ "euler_loading.dataset.index_dataset_from_path",
81
+ return_value=index,
82
+ ):
83
+ ds = MultiModalDataset(
84
+ modalities={"depth": Modality("/data/depth", loader=dummy_loader)}
85
+ )
86
+ schema = ds.describe_id_schema()
87
+ assert schema["hierarchy_separator"] == "-"
88
+ assert schema["id_join_char"] == "+"
89
+ assert schema["full_id_separator"] == "/"
90
+
91
+ def test_divergent_modalities_record_overrides(self):
92
+ rgb_index = _flat_index(
93
+ ["f001"],
94
+ indexing={"id": {"join_char": "+"}, "hierarchy": {"separator": "-"}},
95
+ )
96
+ depth_index = _flat_index(
97
+ ["f001"],
98
+ indexing={"id": {"join_char": "|"}, "hierarchy": {"separator": "-"}},
99
+ )
100
+
101
+ def mock_index(path: str, **_: Any) -> dict[str, Any]:
102
+ return rgb_index if "rgb" in path else depth_index
103
+
104
+ with patch(
105
+ "euler_loading.dataset.index_dataset_from_path",
106
+ side_effect=mock_index,
107
+ ):
108
+ ds = MultiModalDataset(
109
+ modalities={
110
+ "rgb": Modality("/data/rgb", loader=dummy_loader),
111
+ "depth": Modality("/data/depth", loader=dummy_loader),
112
+ }
113
+ )
114
+ schema = ds.describe_id_schema()
115
+ assert "modalities" in schema
116
+ assert schema["modalities"]["depth"]["id_join_char"] == "|"
@@ -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"])
File without changes
File without changes