swcgeom 0.21.0__cp313-cp313-win_amd64.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.
Files changed (72) hide show
  1. swcgeom/__init__.py +21 -0
  2. swcgeom/analysis/__init__.py +13 -0
  3. swcgeom/analysis/feature_extractor.py +454 -0
  4. swcgeom/analysis/features.py +218 -0
  5. swcgeom/analysis/lmeasure.py +750 -0
  6. swcgeom/analysis/sholl.py +201 -0
  7. swcgeom/analysis/trunk.py +183 -0
  8. swcgeom/analysis/visualization.py +191 -0
  9. swcgeom/analysis/visualization3d.py +81 -0
  10. swcgeom/analysis/volume.py +143 -0
  11. swcgeom/core/__init__.py +19 -0
  12. swcgeom/core/branch.py +129 -0
  13. swcgeom/core/branch_tree.py +65 -0
  14. swcgeom/core/compartment.py +107 -0
  15. swcgeom/core/node.py +130 -0
  16. swcgeom/core/path.py +155 -0
  17. swcgeom/core/population.py +398 -0
  18. swcgeom/core/swc.py +247 -0
  19. swcgeom/core/swc_utils/__init__.py +19 -0
  20. swcgeom/core/swc_utils/assembler.py +35 -0
  21. swcgeom/core/swc_utils/base.py +180 -0
  22. swcgeom/core/swc_utils/checker.py +112 -0
  23. swcgeom/core/swc_utils/io.py +335 -0
  24. swcgeom/core/swc_utils/normalizer.py +163 -0
  25. swcgeom/core/swc_utils/subtree.py +70 -0
  26. swcgeom/core/tree.py +387 -0
  27. swcgeom/core/tree_utils.py +277 -0
  28. swcgeom/core/tree_utils_impl.py +58 -0
  29. swcgeom/images/__init__.py +9 -0
  30. swcgeom/images/augmentation.py +149 -0
  31. swcgeom/images/contrast.py +87 -0
  32. swcgeom/images/folder.py +217 -0
  33. swcgeom/images/io.py +604 -0
  34. swcgeom/images/loaders/__init__.py +8 -0
  35. swcgeom/images/loaders/pbd.c +38785 -0
  36. swcgeom/images/loaders/pbd.cp313-win_amd64.pyd +0 -0
  37. swcgeom/images/loaders/raw.c +17408 -0
  38. swcgeom/images/loaders/raw.cp313-win_amd64.pyd +0 -0
  39. swcgeom/transforms/__init__.py +20 -0
  40. swcgeom/transforms/base.py +136 -0
  41. swcgeom/transforms/branch.py +223 -0
  42. swcgeom/transforms/branch_tree.py +74 -0
  43. swcgeom/transforms/geometry.py +270 -0
  44. swcgeom/transforms/image_preprocess.py +107 -0
  45. swcgeom/transforms/image_stack.py +219 -0
  46. swcgeom/transforms/images.py +206 -0
  47. swcgeom/transforms/mst.py +183 -0
  48. swcgeom/transforms/neurolucida_asc.py +498 -0
  49. swcgeom/transforms/path.py +56 -0
  50. swcgeom/transforms/population.py +36 -0
  51. swcgeom/transforms/tree.py +298 -0
  52. swcgeom/transforms/tree_assembler.py +160 -0
  53. swcgeom/utils/__init__.py +18 -0
  54. swcgeom/utils/debug.py +23 -0
  55. swcgeom/utils/download.py +119 -0
  56. swcgeom/utils/dsu.py +58 -0
  57. swcgeom/utils/ellipse.py +131 -0
  58. swcgeom/utils/file.py +90 -0
  59. swcgeom/utils/neuromorpho.py +581 -0
  60. swcgeom/utils/numpy_helper.py +70 -0
  61. swcgeom/utils/plotter_2d.py +134 -0
  62. swcgeom/utils/plotter_3d.py +35 -0
  63. swcgeom/utils/renderer.py +145 -0
  64. swcgeom/utils/sdf.py +324 -0
  65. swcgeom/utils/solid_geometry.py +154 -0
  66. swcgeom/utils/transforms.py +367 -0
  67. swcgeom/utils/volumetric_object.py +483 -0
  68. swcgeom-0.21.0.dist-info/METADATA +86 -0
  69. swcgeom-0.21.0.dist-info/RECORD +72 -0
  70. swcgeom-0.21.0.dist-info/WHEEL +5 -0
  71. swcgeom-0.21.0.dist-info/licenses/LICENSE +201 -0
  72. swcgeom-0.21.0.dist-info/top_level.txt +1 -0
swcgeom/images/io.py ADDED
@@ -0,0 +1,604 @@
1
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Read and write image stack."""
6
+
7
+ import os
8
+ import re
9
+ import warnings
10
+ from abc import ABC, abstractmethod
11
+ from collections.abc import Callable, Iterable
12
+ from functools import cache, lru_cache
13
+ from typing import Any, Generic, Literal, TypeVar, cast, overload
14
+
15
+ import nrrd
16
+ import numpy as np
17
+ import numpy.typing as npt
18
+ import tifffile
19
+ from typing_extensions import deprecated
20
+
21
+ from swcgeom.images.loaders import PBD, Raw
22
+
23
+ __all__ = ["read_imgs", "save_tiff", "read_images"]
24
+
25
+ Vec3i = tuple[int, int, int]
26
+ ScalarType = TypeVar("ScalarType", bound=np.generic, covariant=True)
27
+
28
+ RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
29
+ RE_TERAFLY_NAME = re.compile(r"^\d+(_\d+)?(_\d+)?")
30
+
31
+ UINT_MAX = {
32
+ np.dtype(np.uint8): (2**8) - 1,
33
+ np.dtype(np.uint16): (2**16) - 1,
34
+ np.dtype(np.uint32): (2**32) - 1,
35
+ np.dtype(np.uint64): (2**64) - 1,
36
+ }
37
+
38
+ AXES_ORDER = {
39
+ "X": 0,
40
+ "Y": 1,
41
+ "Z": 2,
42
+ "C": 3,
43
+ "I": 2, # vaa3d compatibility
44
+ }
45
+
46
+
47
+ class ImageStack(ABC, Generic[ScalarType]):
48
+ """Image stack."""
49
+
50
+ # fmt: off
51
+ @overload
52
+ @abstractmethod
53
+ def __getitem__(self, key: int) -> npt.NDArray[ScalarType]: ... # array of shape (Y, Z, C)
54
+ @overload
55
+ @abstractmethod
56
+ def __getitem__(self, key: tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
57
+ @overload
58
+ @abstractmethod
59
+ def __getitem__(self, key: tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
60
+ @overload
61
+ @abstractmethod
62
+ def __getitem__(self, key: tuple[int, int, int, int]) -> ScalarType: ... # value
63
+ @overload
64
+ @abstractmethod
65
+ def __getitem__(
66
+ self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice] | tuple[slice, slice, slice, slice],
67
+ ) -> npt.NDArray[ScalarType]: ... # array of shape (X, Y, Z, C)
68
+ @overload
69
+ @abstractmethod
70
+ def __getitem__(self, key: npt.NDArray[np.integer[Any]]) -> npt.NDArray[ScalarType]: ...
71
+ # fmt: on
72
+ @abstractmethod
73
+ def __getitem__(self, key):
74
+ """Get pixel/patch of image stack.
75
+
76
+ Returns:
77
+ value: NDArray which shape depends on key. If key is tuple of ints,
78
+ """
79
+ raise NotImplementedError()
80
+
81
+ def get_full(self) -> npt.NDArray[ScalarType]:
82
+ """Get full image stack.
83
+
84
+ NOTE: this will load the full image stack into memory.
85
+ """
86
+ return self[:, :, :, :]
87
+
88
+ @property
89
+ def shape(self) -> tuple[int, int, int, int]:
90
+ raise NotImplementedError()
91
+
92
+
93
+ @overload
94
+ def read_imgs(fname: str, *, dtype: ScalarType, **kwargs) -> ImageStack[ScalarType]: ...
95
+ @overload
96
+ def read_imgs(fname: str, *, dtype: None = ..., **kwargs) -> ImageStack[np.float32]: ...
97
+ def read_imgs(fname: str, **kwargs): # type: ignore
98
+ """Read image stack.
99
+
100
+ Args:
101
+ fname: The path of image stack.
102
+ dtype: Casting data to specified dtype.
103
+ If integer and float conversions occur, they will be scaled (assuming floats
104
+ are between 0 and 1). Default to `np.float32`.
105
+ **kwargs: Forwarding to the corresponding reader.
106
+ """
107
+ kwargs.setdefault("dtype", np.float32)
108
+ if not os.path.exists(fname):
109
+ raise ValueError(f"image stack not exists: {fname}")
110
+ elif os.path.isdir(fname):
111
+ # try to read as terafly
112
+ if TeraflyImageStack.is_root(fname):
113
+ return TeraflyImageStack(fname, **kwargs)
114
+ else:
115
+ # match file extension
116
+ match os.path.splitext(fname)[-1]:
117
+ case ".tif" | ".tiff":
118
+ return TiffImageStack(fname, **kwargs)
119
+ case ".nrrd":
120
+ return NrrdImageStack(fname, **kwargs)
121
+ case ".v3dpbd":
122
+ return V3dpbdImageStack(fname, **kwargs)
123
+ case ".v3draw":
124
+ return V3drawImageStack(fname, **kwargs)
125
+ case ".npy":
126
+ return NDArrayImageStack(np.load(fname), **kwargs)
127
+ case ".raw":
128
+ with open(fname, "rb") as f:
129
+ header = f.read(24)
130
+ if header in (
131
+ b"raw_image_stack_by_hpeng",
132
+ b"raw5image_stack_by_hpeng", # TODO: full support for 5 channels
133
+ ):
134
+ return V3drawImageStack(fname, **kwargs)
135
+
136
+ raise ValueError("unsupported image stack")
137
+
138
+
139
+ def save_tiff(
140
+ data: npt.NDArray | ImageStack,
141
+ fname: str,
142
+ *,
143
+ dtype: np.unsignedinteger | np.floating | None = None,
144
+ compression: str | Literal[False] = "zlib",
145
+ **kwargs,
146
+ ) -> None:
147
+ """Save image stack as tiff.
148
+
149
+ Args:
150
+ data: The image stack.
151
+ fname: str
152
+ dtype: Casting data to specified dtype.
153
+ If integer and float conversions occur, they will be scaled (assuming
154
+ floats are between 0 and 1).
155
+ compression: Compression algorithm, forwarding to `tifffile.imwrite`.
156
+ If no algorithnm is specify specified, we will use the zlib algorithm with
157
+ compression level 6 by default.
158
+ **kwargs: Forwarding to `tifffile.imwrite`
159
+ """
160
+ if isinstance(data, ImageStack):
161
+ data = data.get_full() # TODO: avoid load full imgs to memory
162
+
163
+ if data.ndim == 3:
164
+ data = np.expand_dims(data, -1) # (_, _, _) -> (_, _, _, C), C === 1
165
+
166
+ axes = "ZXYC"
167
+ assert data.ndim == 4, "should be an array of shape (X, Y, Z, C)"
168
+ assert data.shape[-1] in [1, 3], "support 'miniblack' or 'rgb'"
169
+
170
+ if dtype is not None:
171
+ if np.issubdtype(data.dtype, np.floating) and np.issubdtype(
172
+ dtype, np.unsignedinteger
173
+ ):
174
+ scaler_factor = UINT_MAX[np.dtype(dtype)]
175
+ elif np.issubdtype(data.dtype, np.unsignedinteger) and np.issubdtype(
176
+ dtype, np.floating
177
+ ):
178
+ scaler_factor = 1 / UINT_MAX[np.dtype(data.dtype)]
179
+ else:
180
+ scaler_factor = 1
181
+
182
+ data = (data * scaler_factor).astype(dtype)
183
+
184
+ if compression is not False:
185
+ kwargs.setdefault("compression", compression)
186
+ if compression == "zlib":
187
+ kwargs.setdefault("compressionargs", {"level": 6})
188
+
189
+ data = np.moveaxis(data, 2, 0) # (_, _, Z, _) -> (Z, _, _, _)
190
+ kwargs.setdefault("photometric", "rgb" if data.shape[-1] == 3 else "minisblack")
191
+ metadata = kwargs.get("metadata", {})
192
+ metadata.setdefault("axes", axes)
193
+ kwargs.update(metadata=metadata)
194
+ tifffile.imwrite(fname, data, **kwargs)
195
+
196
+
197
+ class NDArrayImageStack(ImageStack[ScalarType]):
198
+ """NDArray image stack."""
199
+
200
+ def __init__(
201
+ self, imgs: npt.NDArray[Any], *, dtype: ScalarType | None = None
202
+ ) -> None:
203
+ super().__init__()
204
+
205
+ if imgs.ndim == 3: # (_, _, _) -> (_, _, _, C)
206
+ imgs = np.expand_dims(imgs, -1)
207
+ assert imgs.ndim == 4, "Should be shape of (X, Y, Z, C)"
208
+
209
+ if dtype is not None:
210
+ dtype_raw = imgs.dtype
211
+ if np.issubdtype(dtype, np.floating) and np.issubdtype(
212
+ dtype_raw, np.unsignedinteger
213
+ ):
214
+ scalar_factor = 1.0 / UINT_MAX[dtype_raw]
215
+ imgs = scalar_factor * imgs.astype(dtype)
216
+ elif np.issubdtype(dtype, np.unsignedinteger) and np.issubdtype(
217
+ dtype_raw, np.floating
218
+ ):
219
+ scalar_factor = UINT_MAX[dtype]
220
+ imgs *= (scalar_factor * imgs).astype(dtype)
221
+ else:
222
+ imgs = imgs.astype(dtype)
223
+
224
+ self.imgs = imgs
225
+
226
+ def __getitem__(self, key):
227
+ return self.imgs.__getitem__(key)
228
+
229
+ def get_full(self) -> npt.NDArray[ScalarType]:
230
+ return self.imgs
231
+
232
+ @property
233
+ def shape(self) -> tuple[int, int, int, int]:
234
+ return cast(tuple[int, int, int, int], self.imgs.shape)
235
+
236
+
237
+ class TiffImageStack(NDArrayImageStack[ScalarType]):
238
+ """Tiff image stack."""
239
+
240
+ def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
241
+ with tifffile.TiffFile(fname, **kwargs) as f:
242
+ s = f.series[0]
243
+ imgs, axes = s.asarray(), s.axes
244
+
245
+ if len(axes) != imgs.ndim or any(c not in AXES_ORDER for c in axes):
246
+ axes_raw = axes
247
+ axes = "ZXYC" if imgs.ndim == 4 else "ZXY"
248
+ warnings.warn(f"reset unexcept axes `{axes_raw}` to `{axes}` in: {fname}")
249
+
250
+ orders = [AXES_ORDER[c] for c in axes]
251
+ imgs = imgs.transpose(np.argsort(orders))
252
+ super().__init__(imgs, dtype=dtype)
253
+
254
+
255
+ class NrrdImageStack(NDArrayImageStack[ScalarType]):
256
+ """Nrrd image stack."""
257
+
258
+ def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
259
+ imgs, header = nrrd.read(fname, **kwargs)
260
+ super().__init__(imgs, dtype=dtype)
261
+ self.header = header
262
+
263
+
264
+ class V3dImageStack(NDArrayImageStack[ScalarType]):
265
+ """v3d image stack."""
266
+
267
+ def __init_subclass__(cls, loader: Raw | PBD) -> None:
268
+ super().__init_subclass__()
269
+ cls._loader = loader
270
+
271
+ def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
272
+ r = self._loader()
273
+ imgs = r.load(fname)
274
+ super().__init__(imgs, dtype=dtype, **kwargs)
275
+
276
+
277
+ class V3drawImageStack(V3dImageStack[ScalarType], loader=Raw):
278
+ """v3draw image stack."""
279
+
280
+
281
+ class V3dpbdImageStack(V3dImageStack[ScalarType], loader=PBD):
282
+ """v3dpbd image stack."""
283
+
284
+
285
+ class TeraflyImageStack(ImageStack[ScalarType]):
286
+ """TeraFly image stack.
287
+
288
+ TeraFly is a terabytes of multidimensional volumetric images file
289
+ format as described in [1]_.
290
+
291
+ NOTE: Terafly and Vaa3d use a especial right-handed coordinate system
292
+ (with origin point in the left-top and z-axis points front), but we
293
+ flip y-axis to makes it a left-handed coordinate system (with origin
294
+ point in the left-bottom and z-axis points front). If you need to
295
+ use its coordinate system, remember to FLIP Y-AXIS BACK.
296
+
297
+ References:
298
+ .. [1] Bria, Alessandro, Giulio Iannello, Leonardo Onofri, and Hanchuan Peng.
299
+ “TeraFly: Real-Time Three-Dimensional Visualization and Annotation of Terabytes
300
+ of Multidimensional Volumetric Images.” Nature Methods 13, no. 3 (March 2016):
301
+ 192-94. https://doi.org/10.1038/nmeth.3767.
302
+ """
303
+
304
+ _listdir: Callable[[str], list[str]]
305
+ _read_patch: Callable[[str], npt.NDArray]
306
+
307
+ def __init__(
308
+ self, root: str, *, dtype: ScalarType, lru_maxsize: int | None = 128
309
+ ) -> None:
310
+ r"""
311
+ Args:
312
+ root: The root of terafly which contains directories named as `RES(YxXxZ)`.
313
+ dtype: np.dtype
314
+ lru_maxsize: Forwarding to `functools.lru_cache`.
315
+ A decompressed array size of (256, 256, 256, 1), which is the typical
316
+ size of terafly image stack, takes about 256 * 256 * 256 * 1 * 4B = 64MB.
317
+ A cache size of 128 requires about 8GB memory.
318
+ """
319
+
320
+ super().__init__()
321
+ self.root = root
322
+ self.dtype = dtype
323
+ self.res, self.res_dirs, self.res_patch_sizes = self.get_resolutions(root)
324
+
325
+ @cache
326
+ def listdir(path: str) -> list[str]:
327
+ return os.listdir(path)
328
+
329
+ @lru_cache(maxsize=lru_maxsize)
330
+ def read_patch(path: str) -> npt.NDArray[ScalarType]:
331
+ imgs = read_imgs(path, dtype=dtype)
332
+ data = imgs.get_full()
333
+ if (
334
+ isinstance(imgs, (V3dpbdImageStack, V3drawImageStack))
335
+ and data.ndim == 4
336
+ ):
337
+ # (C, Z, Y, X) -> (X, Y, Z, C) for v3d raw/pbd
338
+ return data.reshape(
339
+ data.shape[3], data.shape[2], data.shape[1], data.shape[0]
340
+ )
341
+
342
+ return data
343
+
344
+ self._listdir, self._read_patch = listdir, read_patch
345
+
346
+ def __getitem__(self, key):
347
+ """Get images in max resolution.
348
+
349
+ >>> imgs[0, 0, 0, 0] # get value # doctest: +SKIP
350
+ >>> imgs[0:64, 0:64, 0:64, :] # get patch # doctest: +SKIP
351
+ """
352
+ if not isinstance(key, tuple):
353
+ raise IndexError(
354
+ "Potential memory issue, you are loading large images "
355
+ "into memory, if sure, load it explicitly with "
356
+ "`get_full`"
357
+ )
358
+
359
+ if not isinstance(key[0], slice):
360
+ offset = [key[i] for i in range(3)]
361
+ return self.get_patch(offset, np.add(offset, 1)).item()
362
+
363
+ # TODO: support multi-channels
364
+ # TODO: support imgs[0:64, 0, 0, 0]?
365
+ if len(key) == 4:
366
+ if not (
367
+ key[3] == 0
368
+ or (
369
+ isinstance(key[3], slice) and list(range(*key[3].indices(1))) == [0]
370
+ )
371
+ ):
372
+ raise ValueError("currently only support single channel")
373
+
374
+ slices = [k.indices(self.res[-1][i]) for i, k in enumerate(key[:3])]
375
+ else:
376
+ slices = [k.indices(self.res[-1][i]) for i, k in enumerate(key)]
377
+
378
+ starts, ends, strides = np.array(slices).transpose()
379
+ return self.get_patch(starts, ends, strides)
380
+
381
+ def get_patch(
382
+ self, starts, ends, strides: int | Vec3i = 1, res_level=-1
383
+ ) -> npt.NDArray[ScalarType]:
384
+ """Get patch of image stack.
385
+
386
+ Returns:
387
+ patch: array of shape (X, Y, Z, C)
388
+ """
389
+ if isinstance(strides, int):
390
+ strides = (strides, strides, strides)
391
+
392
+ starts, ends = np.array(starts), np.array(ends)
393
+ self._check_params(res_level, starts, np.subtract(ends, 1))
394
+ assert np.equal(strides, [1, 1, 1]).all() # TODO: support stride
395
+
396
+ shape_out = np.concatenate([ends - starts, [1]])
397
+ out = np.zeros(shape_out, dtype=self.dtype)
398
+ self._get_range(starts, ends, res_level, out=out)
399
+
400
+ # flip y-axis to makes it a left-handed coordinate system
401
+ out = np.flip(out, axis=1)
402
+ return out
403
+
404
+ def find_correspond_imgs(self, p, res_level=-1):
405
+ """Find the image which contain this point.
406
+
407
+ Returns:
408
+ patch: array of shape (X, Y, Z, C)
409
+ patch_offset: (int, int, int)
410
+ """
411
+ p = np.array(p)
412
+ self._check_params(res_level, p)
413
+ return self._find_correspond_imgs(p, res_level)
414
+
415
+ def get_correspond_coord(self, p, in_res_level: int, out_res_level: int):
416
+ raise NotImplementedError() # TODO
417
+
418
+ @property
419
+ def shape(self) -> tuple[int, int, int, int]:
420
+ res_max = self.res[-1]
421
+ return res_max[0], res_max[1], res_max[2], 1 # TODO: support multi-channels
422
+
423
+ @classmethod
424
+ def get_resolutions(cls, root: str) -> tuple[list[Vec3i], list[str], list[Vec3i]]:
425
+ """Get all resolutions.
426
+
427
+ Returns:
428
+ resolutions: Sequence of sorted resolutions (from small to large).
429
+ roots: Sequence of root of resolutions respectively.
430
+ patch_sizes: Sequence of patch size of resolutions respectively.
431
+ """
432
+
433
+ roots = list(cls.get_resolution_dirs(root))
434
+ assert len(roots) > 0, "no resolution detected"
435
+
436
+ res = [RE_TERAFLY_ROOT.search(d) for d in roots]
437
+ res = [[int(a) for a in d.groups()] for d in res if d is not None]
438
+ res = np.array(res)
439
+ res[:, [0, 1]] = res[:, [1, 0]] # (Y, X, _) -> (X, Y, _)
440
+
441
+ def listdir(d: str):
442
+ return filter(RE_TERAFLY_NAME.match, os.listdir(d))
443
+
444
+ def get_patch_size(src: str):
445
+ y0 = next(listdir(src))
446
+ x0 = next(listdir(os.path.join(src, y0)))
447
+ z0 = next(listdir(os.path.join(src, y0, x0)))
448
+ patch = read_imgs(os.path.join(src, y0, x0, z0))
449
+ return patch.shape
450
+
451
+ patch_sizes = [get_patch_size(os.path.join(root, d)) for d in roots]
452
+
453
+ # sort
454
+ indices = np.argsort(np.prod(res, axis=1, dtype=np.longlong))
455
+ res = res[indices]
456
+ roots = np.take(roots, indices)
457
+ patch_sizes = np.take(patch_sizes, indices)
458
+ return res, roots, patch_sizes # type: ignore
459
+
460
+ @staticmethod
461
+ def is_root(root: str) -> bool:
462
+ return os.path.isdir(root) and any(
463
+ RE_TERAFLY_ROOT.match(d) for d in os.listdir(root)
464
+ )
465
+
466
+ @staticmethod
467
+ def get_resolution_dirs(root: str) -> Iterable[str]:
468
+ return filter(RE_TERAFLY_ROOT.match, os.listdir(root))
469
+
470
+ def _check_params(self, res_level, *coords):
471
+ assert res_level == -1 # TODO: support multi-resolutions
472
+
473
+ res_level = len(self.res) + res_level if res_level < 0 else res_level
474
+ assert 0 <= res_level < len(self.res), "invalid resolution level"
475
+
476
+ res = self.res[res_level]
477
+ for p in coords:
478
+ assert np.less_equal([0, 0, 0], p).all(), (
479
+ f"indices ({p[0]}, {p[1]}, {p[2]}) out of range (0, 0, 0)"
480
+ )
481
+
482
+ assert np.greater(res, p).all(), (
483
+ f"indices ({p[0]}, {p[1]}, {p[2]}) out of range ({res[0]}, {res[1]}, {res[2]})"
484
+ )
485
+
486
+ def _get_range(self, starts, ends, res_level, out):
487
+ # pylint: disable=too-many-locals
488
+ shape = ends - starts
489
+ patch, offset = self._find_correspond_imgs(starts, res_level=res_level)
490
+ if patch is not None:
491
+ coords = starts - offset
492
+ lens = np.min([patch.shape[:3] - coords, shape], axis=0)
493
+ out[: lens[0], : lens[1], : lens[2]] = patch[
494
+ coords[0] : coords[0] + lens[0],
495
+ coords[1] : coords[1] + lens[1],
496
+ coords[2] : coords[2] + lens[2],
497
+ ]
498
+ else:
499
+ size = self.res_patch_sizes[res_level]
500
+ lens = (np.floor(starts / size).astype(np.int64) + 1) * size - starts
501
+
502
+ if shape[0] > lens[0]:
503
+ starts_x = starts + [lens[0], 0, 0]
504
+ ends_x = ends
505
+ self._get_range(starts_x, ends_x, res_level, out[lens[0] :, :, :])
506
+
507
+ if shape[1] > lens[1]:
508
+ starts_y = starts + [0, lens[1], 0]
509
+ ends_y = np.array([starts[0], ends[1], ends[2]])
510
+ ends_y += [min(shape[0], lens[0]), 0, 0]
511
+ self._get_range(starts_y, ends_y, res_level, out[:, lens[1] :, :])
512
+
513
+ if shape[2] > lens[2]:
514
+ starts_z = starts + [0, 0, lens[2]]
515
+ ends_z = np.array([starts[0], starts[1], ends[2]])
516
+ ends_z += [min(shape[0], lens[0]), min(shape[1], lens[1]), 0]
517
+ self._get_range(starts_z, ends_z, res_level, out[:, :, lens[2] :])
518
+
519
+ def _find_correspond_imgs(self, p, res_level):
520
+ # pylint: disable=too-many-locals
521
+ x, y, z = p
522
+ cur = os.path.join(self.root, self.res_dirs[res_level])
523
+
524
+ def get_v(f: str):
525
+ return float(os.path.splitext(f.split("_")[-1])[0])
526
+
527
+ for v in [y, x, z]:
528
+ # extract v from `y/`, `y_x/`, `y_x_z.tif`
529
+ dirs = [d for d in self._listdir(cur) if RE_TERAFLY_NAME.match(d)]
530
+ diff = np.array([get_v(d) for d in dirs])
531
+ if (invalid := diff > 10 * v).all():
532
+ return None, None
533
+
534
+ diff[invalid] = -np.inf # remove values which greater than v
535
+
536
+ # find the index of the value smaller than v and closest to v
537
+ idx = np.argmax(diff)
538
+ cur = os.path.join(cur, dirs[idx])
539
+
540
+ patch = self._read_patch(cur)
541
+ name = os.path.splitext(os.path.basename(cur))[0]
542
+ offset = [int(int(i) / 10) for i in name.split("_")]
543
+ offset[0], offset[1] = offset[1], offset[0] # (Y, X, _) -> (X, Y, _)
544
+ if np.less_equal(np.add(offset, patch.shape[:3]), p).any():
545
+ return None, None
546
+
547
+ return patch, offset
548
+
549
+
550
+ # Legacy
551
+
552
+
553
+ class GrayImageStack:
554
+ """Gray Image stack."""
555
+
556
+ imgs: ImageStack
557
+
558
+ def __init__(self, imgs: ImageStack) -> None:
559
+ self.imgs = imgs
560
+
561
+ @overload
562
+ def __getitem__(self, key: Vec3i) -> np.float32: ...
563
+ @overload
564
+ def __getitem__(self, key: npt.NDArray[np.integer[Any]]) -> np.float32: ...
565
+ @overload
566
+ def __getitem__(
567
+ self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice]
568
+ ) -> npt.NDArray[np.float32]: ...
569
+ def __getitem__(self, key):
570
+ """Get pixel/patch of image stack."""
571
+ v = self[key]
572
+ if not isinstance(v, np.ndarray):
573
+ return v
574
+ if v.ndim == 4:
575
+ return v[:, :, :, 0]
576
+ if v.ndim == 3:
577
+ return v[:, :, 0]
578
+ if v.ndim == 2:
579
+ return v[:, 0]
580
+ if v.ndim == 1:
581
+ return v[0]
582
+ raise ValueError("unsupported key")
583
+
584
+ def get_full(self) -> npt.NDArray[np.float32]:
585
+ """Get full image stack.
586
+
587
+ NOTE: this will load the full image stack into memory.
588
+ """
589
+ return self.imgs.get_full()[:, :, :, 0]
590
+
591
+ @property
592
+ def shape(self) -> tuple[int, int, int]:
593
+ return self.imgs.shape[:-1]
594
+
595
+
596
+ @deprecated("Use `read_imgs` instead")
597
+ def read_images(*args, **kwargs) -> GrayImageStack:
598
+ """Read images.
599
+
600
+ .. deprecated:: 0.16.0
601
+ Use :meth:`read_imgs` instead.
602
+ """
603
+
604
+ return GrayImageStack(read_imgs(*args, **kwargs))
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from swcgeom.images.loaders.pbd import PBD # noqa: F401
6
+ from swcgeom.images.loaders.raw import Raw # noqa: F401
7
+
8
+ __all__ = ["Raw", "PBD"]