Skip to content

Commit 2ac823e

Browse files
authored
simple backend engine for image formats (#355)
* imageio backend and example data * add imageio * create a time offset coordinate + index * bump versions and format * exclude `pixi.lock` from `prettier` * relock * don't show diffs of `pixi.lock`
1 parent 9461d92 commit 2ac823e

6 files changed

Lines changed: 16076 additions & 13336 deletions

File tree

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# SCM syntax highlighting & preventing 3-way merges
2-
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
2+
pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff

.pre-commit-config.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ repos:
1313
- id: check-yaml
1414

1515
- repo: https://github.com/codespell-project/codespell
16-
rev: 'v2.4.1'
16+
rev: v2.4.2
1717
hooks:
1818
- id: codespell
1919

2020
- repo: https://github.com/psf/black-pre-commit-mirror
21-
rev: 26.1.0
21+
rev: 26.5.1
2222
hooks:
2323
- id: black
2424
- id: black-jupyter
@@ -27,20 +27,21 @@ repos:
2727
rev: v0.4.6
2828
hooks:
2929
- id: blackdoc
30-
additional_dependencies: ['black==26.1.0']
30+
additional_dependencies: ['black==26.5.1']
3131
- id: blackdoc-autoupdate-black
3232

3333
- repo: https://github.com/astral-sh/ruff-pre-commit
34-
rev: v0.15.15
34+
rev: v0.15.20
3535
hooks:
3636
- id: ruff-check
3737
args: ['--fix', '--show-fixes']
3838

3939
- repo: https://github.com/rbubley/mirrors-prettier
40-
rev: v3.8.3
40+
rev: v3.9.4
4141
hooks:
4242
- id: prettier
4343
args: ['--cache-location=.prettier_cache/cache']
44+
exclude: 'pixi.lock'
4445

4546
- repo: https://github.com/ComPWA/taplo-pre-commit
4647
rev: v0.9.3

advanced/backends/imageio_.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
import numpy as np
7+
import pandas as pd
8+
import xarray as xr
9+
10+
if TYPE_CHECKING:
11+
import os
12+
from typing import Any, Literal, Protocol
13+
14+
import numpy.typing as npt
15+
16+
class FileLike(Protocol): ...
17+
18+
class LockType(Protocol):
19+
def __enter__(self) -> Any: ...
20+
21+
def acquire(self, blocking: bool = True, timeout: int = -1) -> bool: ...
22+
def release(self) -> None: ...
23+
24+
IndexerType = int | slice | npt.NDArray[np.integer]
25+
FilenameOrObjectType = str | os.PathLike | bytes | FileLike
26+
27+
28+
@dataclass
29+
class ImageIOBackendArray(xr.backends.BackendArray):
30+
filename_or_obj: FilenameOrObjectType
31+
32+
shape: tuple[int]
33+
dtype: npt.DtypeLike
34+
35+
lock: LockType
36+
37+
def __getitem__(self, key: tuple[IndexerType]):
38+
return xr.core.indexing.explicit_indexing_adapter(
39+
key,
40+
self.shape,
41+
xr.core.indexing.IndexingSupport.BASIC,
42+
self.basic_indexing,
43+
)
44+
45+
def basic_indexing(self, key: tuple[IndexerType]) -> npt.NDArray:
46+
import imageio.v3 as iio
47+
48+
with self.lock, iio.imopen(self.filename_or_obj, io_mode="r") as f:
49+
if key == (slice(None),) * len(self.shape):
50+
return f.read()
51+
52+
first_indexer = key[0]
53+
if isinstance(first_indexer, int):
54+
data = f.read(index=first_indexer, mode="P", writable=True)
55+
56+
remaining_indexers = key[1:]
57+
else:
58+
if isinstance(first_indexer, slice):
59+
indices = range(*first_indexer.indices(self.shape[0]))
60+
else:
61+
indices = first_indexer
62+
63+
data = np.concatenate([f.read(index=index, mode="P") for index in indices], axis=0)
64+
65+
remaining_indexers = (..., *key[1:])
66+
67+
return data[remaining_indexers]
68+
69+
70+
class ImageIOBackend(xr.backends.BackendEntrypoint):
71+
def open_dataset(
72+
self,
73+
filename_or_obj: FilenameOrObjectType,
74+
*,
75+
drop_variables: bool | None = None,
76+
mode: Literal["grayscale", "color"] = "color",
77+
) -> xr.Dataset:
78+
import imageio.v3 as iio
79+
80+
with iio.imopen(filename_or_obj, io_mode="r") as f:
81+
properties = f.properties()
82+
metadata = f.metadata()
83+
84+
dims = ["time", "height", "width", "color"]
85+
86+
background = metadata["background"]
87+
duration = metadata["duration"]
88+
loop = metadata["loop"]
89+
90+
shape = properties.shape
91+
dtype = properties.dtype
92+
93+
if isinstance(duration, (int, float)):
94+
time_values = np.timedelta64(duration, "ms") * np.arange(shape[0])
95+
else:
96+
time_values = np.array(duration, dtype="timedelta64[ms]")
97+
98+
time = xr.indexes.PandasIndex(pd.Index(time_values), dim="time")
99+
100+
backend_array = ImageIOBackendArray(
101+
filename_or_obj=filename_or_obj,
102+
shape=shape,
103+
dtype=dtype,
104+
lock=xr.backends.locks.SerializableLock(),
105+
)
106+
data = xr.core.indexing.LazilyIndexedArray(backend_array)
107+
108+
var = xr.Variable(
109+
dims=dims,
110+
data=data,
111+
attrs={"loop": loop},
112+
encoding={
113+
"preferred_chunks": dict(zip(dims, (1, *shape[1:]))),
114+
"fill_value": background,
115+
},
116+
)
117+
coords = xr.Coordinates.from_xindex(time).assign(color=["red", "green", "blue"])
118+
119+
return xr.Dataset({"data": var}, coords=coords)

advanced/backends/ocean.gif

1.42 MB
Loading

0 commit comments

Comments
 (0)