toolsandogh 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,23 @@
1
+ """
2
+ A collection of tools for working with large-scale microscopy data.
3
+
4
+ Provided to you by the Sandoghdar Division of the Max Planck Institute for the Physics of Light.
5
+ """
6
+
7
+ from ._canonicalize_video import canonicalize_video
8
+ from ._generate_video import generate_video
9
+ from ._load_video import load_video
10
+ from ._rolling import differential_rolling_average, rolling_average, rolling_sum
11
+ from ._rvt import radial_variance_transform
12
+ from ._store_video import store_video
13
+
14
+ __all__: list[str] = [
15
+ "canonicalize_video",
16
+ "rolling_sum",
17
+ "rolling_average",
18
+ "differential_rolling_average",
19
+ "generate_video",
20
+ "load_video",
21
+ "store_video",
22
+ "radial_variance_transform",
23
+ ]
@@ -0,0 +1,242 @@
1
+ import sys
2
+ from datetime import datetime, timezone
3
+
4
+ import dask.array as da
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ import xarray as xr
8
+ from ome_types.model import (
9
+ OME,
10
+ Image,
11
+ Pixels,
12
+ Pixels_DimensionOrder,
13
+ PixelType,
14
+ UnitsLength,
15
+ UnitsTime,
16
+ )
17
+
18
+ from ._validate_video import validate_video
19
+
20
+
21
+ def canonicalize_video(
22
+ video: npt.ArrayLike,
23
+ # optional metadata
24
+ acquisition_date: datetime | None = None,
25
+ creator: str | None = None,
26
+ dt: float | None = None,
27
+ dz: float | None = None,
28
+ dy: float | None = None,
29
+ dx: float | None = None,
30
+ ) -> xr.DataArray:
31
+ """
32
+ Turn the supplied data into its canonical TCZYX video representation.
33
+
34
+ Parameters
35
+ ----------
36
+ video : xr.DataArray
37
+ An xarray.
38
+ acquisition_date : datetime.datetime
39
+ A timestamp of when the video was created. Defaults to datetime.now().
40
+ creator : str
41
+ A string describing who created the video.
42
+ dt : float
43
+ The time interval in milliseconds between one video frame and the next.
44
+ Defaults to 1/60 of a second.
45
+ dz : float
46
+ The spatial distance in micrometer between one Z slice and the next.
47
+ Defaults to 1 micrometer.
48
+ dy : float
49
+ The spatial distance in micrometer between one row of pixels and the next.
50
+ Defaults to 1 micrometer.
51
+ dx : float
52
+ The spatial distance in micrometer between one column of pixels and the next.
53
+ Defaults to 1 micrometer.
54
+
55
+ Returns
56
+ -------
57
+ xarray.DataArray
58
+ A TCZYX video with the supplied parameters.
59
+ """
60
+ # Turn video into an xarray.
61
+ if not isinstance(video, xr.DataArray):
62
+ video = video_from_array(video)
63
+
64
+ # Ensure the video's data is a Dask array.
65
+ if not isinstance(video.data, da.Array):
66
+ video = video.copy(data=da.from_array(video.data), deep=False)
67
+
68
+ # Ensure the TZYX axes exist and are continuous.
69
+ for dim in ("T", "Z", "Y", "X"):
70
+ if dim not in video.dims:
71
+ video = video.expand_dims({dim: np.arange(1.0)})
72
+ elif video[dim].dtype != np.float64:
73
+ values = np.array(video[dim])
74
+ video = video.assign_coords({dim: values.astype(np.float64)})
75
+
76
+ # Ensure the C axis exists.
77
+ if "C" not in video.dims:
78
+ video = video.expand_dims({"C": 1})
79
+
80
+ # If there is a S axis, merge its entries.
81
+ if "S" in video.dims:
82
+ video = video.mean("S", dtype=np.float32).astype(video.dtype)
83
+
84
+ # Ensure the correct ordering of axes.
85
+ video = video.transpose("T", "C", "Z", "Y", "X")
86
+
87
+ # Ensure there is metadata attached.
88
+ if not hasattr(video, "processed"):
89
+ (size_t, size_c, size_z, size_y, size_x) = video.shape
90
+ pixels = Pixels(
91
+ type=dtype_pixel_type(video.dtype),
92
+ big_endian=dtype_is_big_endian(video.dtype),
93
+ dimension_order=Pixels_DimensionOrder.XYZCT,
94
+ size_t=size_t,
95
+ size_c=size_c,
96
+ size_z=size_z,
97
+ size_y=size_y,
98
+ size_x=size_x,
99
+ time_increment=dt or (1000 / 60),
100
+ physical_size_z=dz or 1.0,
101
+ physical_size_y=dy or 1.0,
102
+ physical_size_x=dx or 1.0,
103
+ time_increment_unit=UnitsTime.MILLISECOND,
104
+ physical_size_z_unit=UnitsLength.MICROMETER,
105
+ physical_size_y_unit=UnitsLength.MICROMETER,
106
+ physical_size_x_unit=UnitsLength.MICROMETER,
107
+ )
108
+ image = Image(
109
+ acquisition_date=(acquisition_date or datetime.now(timezone.utc)),
110
+ description="Video with auto-generated metadata.",
111
+ pixels=pixels,
112
+ )
113
+ ome = OME(
114
+ images=[image],
115
+ creator=(creator or "MPL Erlangen, Sandoghdar Division, toolsandogh"),
116
+ )
117
+ video = video.assign_attrs({"processed": ome})
118
+ # TODO: Update metadata with any supplied parameters.
119
+
120
+ # Raise an exception if the video is still not in canonical form.
121
+ validate_video(video)
122
+
123
+ # Done.
124
+ return video
125
+
126
+
127
+ def video_from_array(array: npt.ArrayLike) -> xr.DataArray:
128
+ """
129
+ Turn a supplied array into an xarray.
130
+
131
+ Parameters
132
+ ----------
133
+ array : npt.ArrayLike
134
+ An object designating an array.
135
+
136
+ Returns
137
+ -------
138
+ xr.DataArray
139
+ An xarray with the same content and dtype as the supplied array.
140
+ """
141
+ # Determine the Dask array holding the video's data.
142
+ if isinstance(array, da.Array):
143
+ data = array
144
+ else:
145
+ data = da.from_array(array)
146
+
147
+ # Determine the appropriate dims
148
+ rank = len(data.shape)
149
+ match rank:
150
+ case 0:
151
+ dims = ()
152
+ case 1:
153
+ dims = ("X",)
154
+ case 2:
155
+ dims = ("Y", "X")
156
+ case 3:
157
+ dims = ("T", "Y", "X")
158
+ case 4:
159
+ dims = ("T", "Z", "Y", "X")
160
+ case 5:
161
+ dims = ("T", "C", "Z", "Y", "X")
162
+ case 6:
163
+ dims = ("T", "C", "Z", "Y", "X", "S")
164
+ case _:
165
+ raise RuntimeError(f"Cannot interpret {rank}-dimensional data as a video.")
166
+
167
+ # Create the xarray.
168
+ return xr.DataArray(data=data, dims=dims)
169
+
170
+
171
+ def dtype_pixel_type(dtype: npt.DTypeLike) -> PixelType:
172
+ """
173
+ Return the OME Pixel type corresponding to the supplied dtype.
174
+
175
+ Parameters
176
+ ----------
177
+ dtype : npt.DTypeLike
178
+ The Numpy dtype to be used for representing pixel data.
179
+
180
+ Returns
181
+ -------
182
+ ome_types.model.PixelType
183
+ A suitable OME pixel type.
184
+ """
185
+ match np.dtype(dtype):
186
+ case np.int8:
187
+ return PixelType.INT8
188
+ case np.int16:
189
+ return PixelType.INT16
190
+ case np.int32:
191
+ return PixelType.INT32
192
+ case np.uint8:
193
+ return PixelType.UINT8
194
+ case np.uint16:
195
+ return PixelType.UINT16
196
+ case np.uint32:
197
+ return PixelType.UINT32
198
+ case np.float32:
199
+ return PixelType.FLOAT
200
+ case np.float64:
201
+ return PixelType.DOUBLE
202
+ case np.complex64:
203
+ return PixelType.COMPLEXFLOAT
204
+ case np.complex128:
205
+ return PixelType.COMPLEXDOUBLE
206
+ # The dtypes np.int64 and np.uint64 have no OME equivalent.
207
+ case np.int64:
208
+ return PixelType.BIT
209
+ case np.uint64:
210
+ return PixelType.BIT
211
+ case _:
212
+ raise RuntimeError(f"Cannot interpret {dtype} as a OME pixel type.")
213
+
214
+
215
+ def dtype_is_big_endian(dtype: npt.DTypeLike) -> bool:
216
+ """
217
+ Return whether the video's data is stored in big endian byte order.
218
+
219
+ Parameters
220
+ ----------
221
+ dtype : npt.DTypeLike
222
+ The Numpy dtype to be used for representing pixel data.
223
+
224
+ Returns
225
+ -------
226
+ bool
227
+ True when the dtype is big endian, False otherwise.
228
+ """
229
+ dtype = np.dtype(dtype)
230
+
231
+ if dtype.itemsize == 1:
232
+ return False
233
+
234
+ match np.dtype(dtype).byteorder:
235
+ case ">":
236
+ return True
237
+ case "<":
238
+ return False
239
+ case "=":
240
+ return sys.byteorder == "big"
241
+ case _:
242
+ raise RuntimeError(f"Cannot determine endianness of {dtype}.")
@@ -0,0 +1,72 @@
1
+ import dask.array as da
2
+ import numpy as np
3
+ import numpy.typing as npt
4
+ import xarray as xr
5
+
6
+ from ._canonicalize_video import canonicalize_video
7
+
8
+
9
+ def generate_video(
10
+ T: int = 1,
11
+ C: int = 1,
12
+ Z: int = 1,
13
+ Y: int = 1,
14
+ X: int = 1,
15
+ dt: float = 1.0,
16
+ dz: float = 1.0,
17
+ dy: float = 1.0,
18
+ dx: float = 1.0,
19
+ dtype: npt.DTypeLike = np.float32,
20
+ ) -> xr.DataArray:
21
+ """
22
+ Generate a video filled with random noise.
23
+
24
+ Parameters
25
+ ----------
26
+ T : int
27
+ The T (time) extent of the video.
28
+ C : int
29
+ The C (channel) extent of the video.
30
+ Z : int
31
+ The Z (height) extent of the video.
32
+ Y : int
33
+ The Y (row) extent of the video.
34
+ X : int
35
+ The X (column) extent of the video.
36
+ dt : float
37
+ The T (time) step size of the video.
38
+ dz : float
39
+ The Z (height) step size of the video.
40
+ dy : float
41
+ The Y (row) step size of the video.
42
+ dx : float
43
+ The X (column) step size of the video.
44
+ dtype : npt.DtypeLike
45
+ The dtype of the resulting video.
46
+
47
+ Returns
48
+ -------
49
+ xarray.DataArray
50
+ A TCZYX video with the supplied parameters.
51
+ """
52
+ rng = da.random.default_rng()
53
+ video = canonicalize_video(
54
+ rng.random(
55
+ size=(T, C, Z, Y, X),
56
+ dtype=dtype, # type: ignore
57
+ ),
58
+ )
59
+
60
+ # Check that the video matches the supplied parameters.
61
+ for dim, size, step in zip("TCZYX", (T, C, Z, Y, X), (dt, None, dz, dy, dx)):
62
+ coord = video[dim]
63
+ assert len(coord) == size
64
+ if len(coord) > 1 and step is not None:
65
+ array = coord.values
66
+ delta = array[1] - array[0]
67
+ error = abs(delta - step)
68
+ assert (error / step) <= 1e-3
69
+ assert video.dtype == dtype
70
+
71
+ # Done.
72
+ return video
@@ -0,0 +1,263 @@
1
+ """Define a function for loading videos from a wide range of sources."""
2
+
3
+ import os
4
+ import os.path
5
+ import pathlib
6
+ import urllib.parse
7
+
8
+ import bioio
9
+ import bioio_czi
10
+ import bioio_nd2
11
+ import dask.array as da
12
+ import fsspec
13
+ import numpy as np
14
+ import numpy.typing as npt
15
+ import xarray as xr
16
+ from fsspec.utils import math
17
+
18
+ from ._canonicalize_video import canonicalize_video
19
+
20
+
21
+ def load_video(
22
+ path: str | os.PathLike,
23
+ T: int | None = None,
24
+ C: int | None = None,
25
+ Z: int | None = None,
26
+ Y: int | None = None,
27
+ X: int | None = None,
28
+ dt: float | None = None,
29
+ dz: float | None = None,
30
+ dy: float | None = None,
31
+ dx: float | None = None,
32
+ dtype: npt.DTypeLike | None = None,
33
+ ) -> xr.DataArray:
34
+ """
35
+ Load a video from the given path.
36
+
37
+ The resulting video is a 5D xarray with dimensions TCZYX. If any keyword
38
+ arguments are supplied, this function asserts that the resulting video
39
+ matches these arguments, e.g., Z=1 may be used assert that the result is a
40
+ 2D video.
41
+
42
+ The suffix of the supplied path determines the method that is used for
43
+ reading the file. For .raw and .bin files, the X, Y arguments are
44
+ mandatory, the number of frames T is inferred automatically, and the
45
+ remaining arguments default to Z=1, C=1, dz=1.0, dy=dx=1.0, dt=1.0, and
46
+ dtype=uint8.
47
+
48
+ Parameters
49
+ ----------
50
+ path : os.PathLike
51
+ The name of a file, a URI, or a path.
52
+ T : int
53
+ The expected T (time) extent of the video.
54
+ C : int
55
+ The expected C (channel) extent of the video.
56
+ Z : int
57
+ The expected Z (height) extent of the video.
58
+ Y : int
59
+ The expected Y (row) extent of the video.
60
+ X : int
61
+ The expected X (column) extent of the video.
62
+ dt : float
63
+ The expected T (time) step size of the video.
64
+ dz : float
65
+ The expected Z (height) step size of the video.
66
+ dy : float
67
+ The expected Y (row) step size of the video.
68
+ dx : float
69
+ The expected X (column) step size of the video.
70
+ dtype : npt.DtypeLike
71
+ The expected dtype of the video.
72
+
73
+ Returns
74
+ -------
75
+ xarray.DataArray
76
+ A canonical TCZYX array that matches the supplied parameters.
77
+ """
78
+ # Parse the supplied path.
79
+ pathstr = str(path)
80
+ url = urllib.parse.urlparse(pathstr)
81
+ path = pathlib.Path(url.path)
82
+
83
+ # Gather all keyword arguments for those load functions that require them.
84
+ kwargs = {
85
+ "T": T,
86
+ "C": C,
87
+ "Z": Z,
88
+ "Y": Y,
89
+ "X": X,
90
+ "dt": dt,
91
+ "dz": dz,
92
+ "dy": dy,
93
+ "dx": dx,
94
+ "dtype": dtype,
95
+ }
96
+
97
+ # Load the video.
98
+ protocols = fsspec.available_protocols()
99
+ match (url.scheme or "file", path.suffix):
100
+ case (scheme, ".bin" | ".raw") if scheme in protocols:
101
+ video = load_raw_video(path, **kwargs)
102
+ case (_, ".czi"):
103
+ img = bioio.BioImage(pathstr, reader=bioio_czi.Reader)
104
+ video = img.xarray_dask_data
105
+ case (_, ".nd2"):
106
+ img = bioio.BioImage(pathstr, reader=bioio_nd2.Reader)
107
+ video = img.xarray_dask_data
108
+ case (_, _):
109
+ # Let bioio figure out the rest or raise an exception
110
+ img = bioio.BioImage(pathstr)
111
+ video = img.xarray_dask_data
112
+
113
+ # Ensure the video is canonical and return.
114
+ return canonicalize_video(video)
115
+
116
+
117
+ def load_raw_video(
118
+ path: os.PathLike,
119
+ T: int | None = None,
120
+ C: int | None = None,
121
+ Z: int | None = None,
122
+ Y: int | None = None,
123
+ X: int | None = None,
124
+ dt: float | None = None,
125
+ dz: float | None = None,
126
+ dy: float | None = None,
127
+ dx: float | None = None,
128
+ dtype: npt.DTypeLike | None = None,
129
+ ) -> xr.DataArray:
130
+ """
131
+ Load a video from the specified raw file.
132
+
133
+ Parameters
134
+ ----------
135
+ path : os.PathLike
136
+ The name of a file, a URI, or a path.
137
+ T : int
138
+ The expected T (time) extent of the video.
139
+ C : int
140
+ The expected C (channel) extent of the video.
141
+ Z : int
142
+ The expected Z (height) extent of the video.
143
+ Y : int
144
+ The expected Y (row) extent of the video.
145
+ X : int
146
+ The expected X (column) extent of the video.
147
+ dt : float
148
+ The expected T (time) step size of the video.
149
+ dz : float
150
+ The expected Z (height) step size of the video.
151
+ dy : float
152
+ The expected Y (row) step size of the video.
153
+ dx : float
154
+ The expected X (column) step size of the video.
155
+ dtype : npt.DtypeLike
156
+ The expected dtype of the video.
157
+
158
+ Returns
159
+ -------
160
+ xarray.DataArray
161
+ A canonical TCZYX array that matches the supplied parameters.
162
+ """
163
+ # The parameters X and Y are mandatory for loading raw files.
164
+ if Y is None:
165
+ raise RuntimeError("The parameter Y is required for loading raw files.")
166
+ if X is None:
167
+ raise RuntimeError("The parameter X is required for loading raw files.")
168
+ # Z and C default to one.
169
+ if Z is None:
170
+ Z = 1
171
+ if C is None:
172
+ C = 1
173
+ # The default dtype is np.uint8
174
+ if dtype is None:
175
+ dtype = np.uint8
176
+ # The number of frames T can be inferred from the file's size.
177
+ bits_per_item = 12 if (dtype == "uint12") else (np.dtype(dtype).itemsize * 8)
178
+ nbits = os.path.getsize(path) * 8
179
+ nitems = nbits // bits_per_item
180
+ items_per_T = C * Z * Y * X
181
+ if T is None:
182
+ T = nitems // items_per_T
183
+ nexpected = T * items_per_T
184
+ if nexpected > nitems:
185
+ raise RuntimeError(
186
+ f"The file {path} has only {nitems} items, but {nexpected} were expected."
187
+ )
188
+ # All step sizes default to 1.0.
189
+ if dt is None:
190
+ dt = 1.0
191
+ if dz is None:
192
+ dz = 1.0
193
+ if dy is None:
194
+ dy = dx or 1.0
195
+ if dx is None:
196
+ dx = dy or 1.0
197
+ # Load the file as a dask array. Treat the uint12 case specially.
198
+ shape = (T, C, Z, Y, X)
199
+ chunks = ("auto", "auto", None, None, None)
200
+ if dtype == "uint12":
201
+ nbytes = (nexpected * 12) // 8
202
+ mmap_array = np.memmap(path, dtype=np.uint8, mode="r", shape=(nbytes,))
203
+ dask_array = da.from_array(mmap_array)
204
+ a = dask_array[0::3].astype(np.uint16)
205
+ b = dask_array[1::3].astype(np.uint16)
206
+ c = dask_array[2::3].astype(np.uint16)
207
+ assert len(a) == len(b) == len(c)
208
+ evens = ((b & 0x0F) << 8) | a
209
+ odds = ((b & 0xF0) >> 4) | (c << 4)
210
+ flat = da.stack([evens, odds], axis=1).ravel()
211
+ dask_array = flat.reshape((T, C, Z, Y, X), chunks=chunks)
212
+ else:
213
+ dtype = np.dtype(dtype)
214
+ mmap_array = np.memmap(path, dtype=dtype, mode="r", shape=shape)
215
+ dask_array = da.from_array(mmap_array, chunks=chunks) # type: ignore
216
+ # Wrap the dask array as a xarray.DataArray and return it.
217
+ return xr.DataArray(
218
+ dask_array,
219
+ coords={
220
+ "T": dt * np.arange(T),
221
+ "C": range(C),
222
+ "Z": dz * np.arange(Z),
223
+ "Y": dy * np.arange(Y),
224
+ "X": dx * np.arange(X),
225
+ },
226
+ )
227
+
228
+
229
+ def load_raw_chunk(
230
+ path: os.PathLike,
231
+ shape: tuple[int, ...],
232
+ dtype: npt.DTypeLike,
233
+ offset: int,
234
+ ) -> np.ndarray:
235
+ """
236
+ Load a portion of the supplied raw file as a Numpy array.
237
+
238
+ Parameters
239
+ ----------
240
+ path : os.PathLike
241
+ Path (local or remote) to the raw file.
242
+ shape : tuple[int, ...]
243
+ Desired shape of the returned array (e.g. ``(T, C, Z, Y, X)``). The
244
+ product of the dimensions must match the number of items that will be
245
+ read.
246
+ dtype : npt.DTypeLike
247
+ Numpy data type of the stored items.
248
+ offset : int
249
+ Index of the first element to read **in items**, *not* in bytes.
250
+
251
+ Returns
252
+ -------
253
+ np.ndarray
254
+ A dense NumPy array with the requested shape and dtype.
255
+ """
256
+ count = math.prod(shape)
257
+ with fsspec.open(path, newline="") as f:
258
+ return np.fromfile(
259
+ f, # type: ignore
260
+ dtype=dtype,
261
+ offset=offset,
262
+ count=count,
263
+ ).reshape(shape)
File without changes