swcgeom 0.16.0__py3-none-any.whl → 0.17.1__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.
Potentially problematic release.
This version of swcgeom might be problematic. Click here for more details.
- swcgeom/_version.py +2 -2
- swcgeom/analysis/__init__.py +1 -3
- swcgeom/analysis/feature_extractor.py +16 -15
- swcgeom/analysis/{node_features.py → features.py} +105 -3
- swcgeom/analysis/lmeasure.py +5 -5
- swcgeom/analysis/sholl.py +4 -4
- swcgeom/analysis/trunk.py +12 -11
- swcgeom/analysis/visualization.py +9 -9
- swcgeom/analysis/visualization3d.py +85 -0
- swcgeom/analysis/volume.py +4 -4
- swcgeom/core/branch.py +4 -3
- swcgeom/core/branch_tree.py +3 -4
- swcgeom/core/compartment.py +3 -2
- swcgeom/core/node.py +2 -2
- swcgeom/core/path.py +3 -2
- swcgeom/core/population.py +16 -27
- swcgeom/core/swc.py +11 -10
- swcgeom/core/swc_utils/base.py +8 -17
- swcgeom/core/swc_utils/io.py +7 -6
- swcgeom/core/swc_utils/normalizer.py +4 -3
- swcgeom/core/swc_utils/subtree.py +2 -2
- swcgeom/core/tree.py +22 -34
- swcgeom/core/tree_utils.py +11 -10
- swcgeom/core/tree_utils_impl.py +3 -3
- swcgeom/images/augmentation.py +3 -3
- swcgeom/images/folder.py +10 -16
- swcgeom/images/io.py +76 -111
- swcgeom/transforms/image_stack.py +6 -5
- swcgeom/transforms/images.py +105 -5
- swcgeom/transforms/neurolucida_asc.py +4 -6
- swcgeom/transforms/population.py +1 -3
- swcgeom/transforms/tree.py +8 -7
- swcgeom/transforms/tree_assembler.py +4 -3
- swcgeom/utils/ellipse.py +3 -4
- swcgeom/utils/neuromorpho.py +17 -16
- swcgeom/utils/plotter_2d.py +12 -6
- swcgeom/utils/plotter_3d.py +31 -0
- swcgeom/utils/renderer.py +6 -6
- swcgeom/utils/sdf.py +2 -2
- swcgeom/utils/solid_geometry.py +1 -3
- swcgeom/utils/transforms.py +1 -3
- swcgeom/utils/volumetric_object.py +8 -10
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/METADATA +1 -1
- swcgeom-0.17.1.dist-info/RECORD +67 -0
- swcgeom/analysis/branch_features.py +0 -67
- swcgeom/analysis/path_features.py +0 -37
- swcgeom-0.16.0.dist-info/RECORD +0 -67
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/LICENSE +0 -0
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/WHEEL +0 -0
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.1.dist-info}/top_level.txt +0 -0
swcgeom/core/tree_utils_impl.py
CHANGED
|
@@ -5,7 +5,7 @@ Notes
|
|
|
5
5
|
Do not import `Tree` and keep this file minimized.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Optional
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import numpy.typing as npt
|
|
@@ -15,8 +15,8 @@ from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
|
|
|
15
15
|
|
|
16
16
|
__all__ = ["get_subtree_impl", "to_subtree_impl"]
|
|
17
17
|
|
|
18
|
-
Mapping =
|
|
19
|
-
TreeArgs =
|
|
18
|
+
Mapping = dict[int, int] | list[int]
|
|
19
|
+
TreeArgs = tuple[int, dict[str, npt.NDArray[Any]], str, SWCNames]
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def get_subtree_impl(
|
swcgeom/images/augmentation.py
CHANGED
|
@@ -6,7 +6,7 @@ This is expremental code, and the API is subject to change.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import random
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Literal, Optional
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import numpy.typing as npt
|
|
@@ -54,7 +54,7 @@ class Augmentation:
|
|
|
54
54
|
|
|
55
55
|
def swapaxes(self, x, mode: Optional[Literal["xy", "xz", "yz"]] = None) -> NDArrf32:
|
|
56
56
|
if mode is None:
|
|
57
|
-
modes:
|
|
57
|
+
modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
|
|
58
58
|
mode = modes[self.rand.randint(0, 2)]
|
|
59
59
|
|
|
60
60
|
match mode:
|
|
@@ -69,7 +69,7 @@ class Augmentation:
|
|
|
69
69
|
|
|
70
70
|
def flip(self, x, mode: Optional[Literal["xy", "xz", "yz"]] = None) -> NDArrf32:
|
|
71
71
|
if mode is None:
|
|
72
|
-
modes:
|
|
72
|
+
modes: list[Literal["xy", "xz", "yz"]] = ["xy", "xz", "yz"]
|
|
73
73
|
mode = modes[random.randint(0, 2)]
|
|
74
74
|
|
|
75
75
|
match mode:
|
swcgeom/images/folder.py
CHANGED
|
@@ -4,18 +4,9 @@ import math
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import warnings
|
|
7
|
+
from collections.abc import Callable, Iterable
|
|
7
8
|
from dataclasses import dataclass
|
|
8
|
-
from typing import
|
|
9
|
-
Callable,
|
|
10
|
-
Generic,
|
|
11
|
-
Iterable,
|
|
12
|
-
List,
|
|
13
|
-
Literal,
|
|
14
|
-
Optional,
|
|
15
|
-
Tuple,
|
|
16
|
-
TypeVar,
|
|
17
|
-
overload,
|
|
18
|
-
)
|
|
9
|
+
from typing import Generic, Literal, Optional, TypeVar, overload
|
|
19
10
|
|
|
20
11
|
import numpy as np
|
|
21
12
|
import numpy.typing as npt
|
|
@@ -33,7 +24,7 @@ T = TypeVar("T")
|
|
|
33
24
|
class ImageStackFolderBase(Generic[ScalarType, T]):
|
|
34
25
|
"""Image stack folder base."""
|
|
35
26
|
|
|
36
|
-
files:
|
|
27
|
+
files: list[str]
|
|
37
28
|
transform: Transform[npt.NDArray[ScalarType], T]
|
|
38
29
|
|
|
39
30
|
# fmt: off
|
|
@@ -61,7 +52,10 @@ class ImageStackFolderBase(Generic[ScalarType, T]):
|
|
|
61
52
|
return read_imgs(fname, dtype=self.dtype).get_full() # type: ignore
|
|
62
53
|
|
|
63
54
|
@staticmethod
|
|
64
|
-
def scan(root: str, *, pattern: Optional[str] = None) ->
|
|
55
|
+
def scan(root: str, *, pattern: Optional[str] = None) -> list[str]:
|
|
56
|
+
if not os.path.isdir(root):
|
|
57
|
+
raise NotADirectoryError(f"not a directory: {root}")
|
|
58
|
+
|
|
65
59
|
is_valid = re.compile(pattern).match if pattern is not None else truthly
|
|
66
60
|
|
|
67
61
|
fs = []
|
|
@@ -168,13 +162,13 @@ class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
|
168
162
|
class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
169
163
|
"""Image stack folder with label."""
|
|
170
164
|
|
|
171
|
-
labels:
|
|
165
|
+
labels: list[int]
|
|
172
166
|
|
|
173
167
|
def __init__(self, files: Iterable[str], labels: Iterable[int], **kwargs):
|
|
174
168
|
super().__init__(files, **kwargs)
|
|
175
169
|
self.labels = list(labels)
|
|
176
170
|
|
|
177
|
-
def __getitem__(self, idx: int) ->
|
|
171
|
+
def __getitem__(self, idx: int) -> tuple[T, int]:
|
|
178
172
|
return self._get(self.files[idx]), self.labels[idx]
|
|
179
173
|
|
|
180
174
|
@classmethod
|
|
@@ -205,7 +199,7 @@ class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
|
|
|
205
199
|
super().__init__(files, **kwargs)
|
|
206
200
|
self.root = root
|
|
207
201
|
|
|
208
|
-
def __getitem__(self, idx: int) ->
|
|
202
|
+
def __getitem__(self, idx: int) -> tuple[T, str]:
|
|
209
203
|
relpath = os.path.relpath(self.files[idx], self.root)
|
|
210
204
|
return self._get(self.files[idx]), relpath
|
|
211
205
|
|
swcgeom/images/io.py
CHANGED
|
@@ -4,20 +4,9 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
import warnings
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable, Iterable
|
|
7
8
|
from functools import cache, lru_cache
|
|
8
|
-
from typing import
|
|
9
|
-
Any,
|
|
10
|
-
Callable,
|
|
11
|
-
Generic,
|
|
12
|
-
Iterable,
|
|
13
|
-
List,
|
|
14
|
-
Literal,
|
|
15
|
-
Optional,
|
|
16
|
-
Tuple,
|
|
17
|
-
TypeVar,
|
|
18
|
-
cast,
|
|
19
|
-
overload,
|
|
20
|
-
)
|
|
9
|
+
from typing import Any, Generic, Literal, Optional, TypeVar, cast, overload
|
|
21
10
|
|
|
22
11
|
import nrrd
|
|
23
12
|
import numpy as np
|
|
@@ -27,7 +16,7 @@ from v3dpy.loaders import PBD, Raw
|
|
|
27
16
|
|
|
28
17
|
__all__ = ["read_imgs", "save_tiff", "read_images"]
|
|
29
18
|
|
|
30
|
-
Vec3i =
|
|
19
|
+
Vec3i = tuple[int, int, int]
|
|
31
20
|
ScalarType = TypeVar("ScalarType", bound=np.generic, covariant=True)
|
|
32
21
|
|
|
33
22
|
RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
|
|
@@ -58,17 +47,17 @@ class ImageStack(ABC, Generic[ScalarType]):
|
|
|
58
47
|
def __getitem__(self, key: int) -> npt.NDArray[ScalarType]: ... # array of shape (Y, Z, C)
|
|
59
48
|
@overload
|
|
60
49
|
@abstractmethod
|
|
61
|
-
def __getitem__(self, key:
|
|
50
|
+
def __getitem__(self, key: tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
|
|
62
51
|
@overload
|
|
63
52
|
@abstractmethod
|
|
64
|
-
def __getitem__(self, key:
|
|
53
|
+
def __getitem__(self, key: tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
|
|
65
54
|
@overload
|
|
66
55
|
@abstractmethod
|
|
67
|
-
def __getitem__(self, key:
|
|
56
|
+
def __getitem__(self, key: tuple[int, int, int, int]) -> ScalarType: ... # value
|
|
68
57
|
@overload
|
|
69
58
|
@abstractmethod
|
|
70
59
|
def __getitem__(
|
|
71
|
-
self, key: slice |
|
|
60
|
+
self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice] | tuple[slice, slice, slice, slice],
|
|
72
61
|
) -> npt.NDArray[ScalarType]: ... # array of shape (X, Y, Z, C)
|
|
73
62
|
@overload
|
|
74
63
|
@abstractmethod
|
|
@@ -95,7 +84,7 @@ class ImageStack(ABC, Generic[ScalarType]):
|
|
|
95
84
|
return self[:, :, :, :]
|
|
96
85
|
|
|
97
86
|
@property
|
|
98
|
-
def shape(self) ->
|
|
87
|
+
def shape(self) -> tuple[int, int, int, int]:
|
|
99
88
|
raise NotImplementedError()
|
|
100
89
|
|
|
101
90
|
|
|
@@ -107,26 +96,42 @@ def read_imgs(fname: str, *, dtype: None =..., **kwargs) -> ImageStack[np.float3
|
|
|
107
96
|
# fmt:on
|
|
108
97
|
|
|
109
98
|
|
|
110
|
-
def read_imgs(fname: str,
|
|
111
|
-
"""Read image stack.
|
|
99
|
+
def read_imgs(fname: str, **kwargs): # type: ignore
|
|
100
|
+
"""Read image stack.
|
|
112
101
|
|
|
113
|
-
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
fname : str
|
|
105
|
+
The path of image stack.
|
|
106
|
+
dtype : np.dtype, default to `np.float32`
|
|
107
|
+
Casting data to specified dtype. If integer and float
|
|
108
|
+
conversions occur, they will be scaled (assuming floats are
|
|
109
|
+
between 0 and 1).
|
|
110
|
+
**kwargs : dict[str, Any]
|
|
111
|
+
Forwarding to the corresponding reader.
|
|
112
|
+
"""
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
kwargs.setdefault("dtype", np.float32)
|
|
115
|
+
if not os.path.exists(fname):
|
|
116
|
+
raise ValueError(f"image stack not exists: {fname}")
|
|
117
|
+
|
|
118
|
+
# match file extension
|
|
119
|
+
match os.path.splitext(fname)[-1]:
|
|
120
|
+
case ".tif" | ".tiff":
|
|
121
|
+
return TiffImageStack(fname, **kwargs)
|
|
122
|
+
case ".nrrd":
|
|
123
|
+
return NrrdImageStack(fname, **kwargs)
|
|
124
|
+
case ".v3dpbd":
|
|
125
|
+
return V3dpbdImageStack(fname, **kwargs)
|
|
126
|
+
case ".v3draw":
|
|
127
|
+
return V3drawImageStack(fname, **kwargs)
|
|
128
|
+
case ".npy":
|
|
129
|
+
return NDArrayImageStack(np.load(fname), **kwargs)
|
|
130
|
+
|
|
131
|
+
# try to read as terafly
|
|
126
132
|
if TeraflyImageStack.is_root(fname):
|
|
127
133
|
return TeraflyImageStack(fname, **kwargs)
|
|
128
|
-
|
|
129
|
-
raise ValueError("image stack not exists")
|
|
134
|
+
|
|
130
135
|
raise ValueError("unsupported image stack")
|
|
131
136
|
|
|
132
137
|
|
|
@@ -135,7 +140,6 @@ def save_tiff(
|
|
|
135
140
|
fname: str,
|
|
136
141
|
*,
|
|
137
142
|
dtype: Optional[np.unsignedinteger | np.floating] = None,
|
|
138
|
-
swap_xy: Optional[bool] = None,
|
|
139
143
|
compression: str | Literal[False] = "zlib",
|
|
140
144
|
**kwargs,
|
|
141
145
|
) -> None:
|
|
@@ -154,7 +158,7 @@ def save_tiff(
|
|
|
154
158
|
Compression algorithm, forwarding to `tifffile.imwrite`. If no
|
|
155
159
|
algorithnm is specify specified, we will use the zlib algorithm
|
|
156
160
|
with compression level 6 by default.
|
|
157
|
-
**kwargs :
|
|
161
|
+
**kwargs : dict[str, Any]
|
|
158
162
|
Forwarding to `tifffile.imwrite`
|
|
159
163
|
"""
|
|
160
164
|
if isinstance(data, ImageStack):
|
|
@@ -164,17 +168,6 @@ def save_tiff(
|
|
|
164
168
|
data = np.expand_dims(data, -1) # (_, _, _) -> (_, _, _, C), C === 1
|
|
165
169
|
|
|
166
170
|
axes = "ZXYC"
|
|
167
|
-
if swap_xy is not None:
|
|
168
|
-
warnings.warn(
|
|
169
|
-
"flag `swap_xy` is easy to implement in user space and "
|
|
170
|
-
"is more flexiable. Since this flag is rarely used, we "
|
|
171
|
-
"decided to remove it in the next version",
|
|
172
|
-
DeprecationWarning,
|
|
173
|
-
)
|
|
174
|
-
if swap_xy is True:
|
|
175
|
-
axes = "ZYXC"
|
|
176
|
-
data = data.swapaxes(0, 1) # (X, Y, _, _) -> (Y, X, _, _)
|
|
177
|
-
|
|
178
171
|
assert data.ndim == 4, "should be an array of shape (X, Y, Z, C)"
|
|
179
172
|
assert data.shape[-1] in [1, 3], "support 'miniblack' or 'rgb'"
|
|
180
173
|
|
|
@@ -209,12 +202,7 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
209
202
|
"""NDArray image stack."""
|
|
210
203
|
|
|
211
204
|
def __init__(
|
|
212
|
-
self,
|
|
213
|
-
imgs: npt.NDArray[Any],
|
|
214
|
-
swap_xy: Optional[bool] = None,
|
|
215
|
-
filp_xy: Optional[bool] = None,
|
|
216
|
-
*,
|
|
217
|
-
dtype: ScalarType,
|
|
205
|
+
self, imgs: npt.NDArray[Any], *, dtype: Optional[ScalarType] = None
|
|
218
206
|
) -> None:
|
|
219
207
|
super().__init__()
|
|
220
208
|
|
|
@@ -222,34 +210,22 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
222
210
|
imgs = np.expand_dims(imgs, -1)
|
|
223
211
|
assert imgs.ndim == 4, "Should be shape of (X, Y, Z, C)"
|
|
224
212
|
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"is more flexiable. Since this flag is rarely used, we "
|
|
240
|
-
"decided to remove it in the next version",
|
|
241
|
-
DeprecationWarning,
|
|
242
|
-
)
|
|
243
|
-
if filp_xy is True:
|
|
244
|
-
imgs = np.flip(imgs, (0, 1)) # (X, Y, Z, C)
|
|
213
|
+
if dtype is not None:
|
|
214
|
+
dtype_raw = imgs.dtype
|
|
215
|
+
if np.issubdtype(dtype, np.floating) and np.issubdtype(
|
|
216
|
+
dtype_raw, np.unsignedinteger
|
|
217
|
+
):
|
|
218
|
+
sclar_factor = 1.0 / UINT_MAX[dtype_raw]
|
|
219
|
+
imgs = sclar_factor * imgs.astype(dtype)
|
|
220
|
+
elif np.issubdtype(dtype, np.unsignedinteger) and np.issubdtype(
|
|
221
|
+
dtype_raw, np.floating
|
|
222
|
+
):
|
|
223
|
+
sclar_factor = UINT_MAX[dtype] # type: ignore
|
|
224
|
+
imgs *= (sclar_factor * imgs).astype(dtype)
|
|
225
|
+
else:
|
|
226
|
+
imgs = imgs.astype(dtype)
|
|
245
227
|
|
|
246
|
-
|
|
247
|
-
self.imgs = imgs.astype(dtype)
|
|
248
|
-
if np.issubdtype(dtype, np.floating) and np.issubdtype(
|
|
249
|
-
dtype_raw, np.unsignedinteger
|
|
250
|
-
): # TODO: add a option to disable this
|
|
251
|
-
sclar_factor = 1.0 / UINT_MAX[imgs.dtype]
|
|
252
|
-
self.imgs *= sclar_factor
|
|
228
|
+
self.imgs = imgs
|
|
253
229
|
|
|
254
230
|
def __getitem__(self, key):
|
|
255
231
|
return self.imgs.__getitem__(key)
|
|
@@ -258,22 +234,14 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
258
234
|
return self.imgs
|
|
259
235
|
|
|
260
236
|
@property
|
|
261
|
-
def shape(self) ->
|
|
262
|
-
return cast(
|
|
237
|
+
def shape(self) -> tuple[int, int, int, int]:
|
|
238
|
+
return cast(tuple[int, int, int, int], self.imgs.shape)
|
|
263
239
|
|
|
264
240
|
|
|
265
241
|
class TiffImageStack(NDArrayImageStack[ScalarType]):
|
|
266
242
|
"""Tiff image stack."""
|
|
267
243
|
|
|
268
|
-
def __init__(
|
|
269
|
-
self,
|
|
270
|
-
fname: str,
|
|
271
|
-
swap_xy: Optional[bool] = None,
|
|
272
|
-
filp_xy: Optional[bool] = None,
|
|
273
|
-
*,
|
|
274
|
-
dtype: ScalarType,
|
|
275
|
-
**kwargs,
|
|
276
|
-
) -> None:
|
|
244
|
+
def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
|
|
277
245
|
with tifffile.TiffFile(fname, **kwargs) as f:
|
|
278
246
|
s = f.series[0]
|
|
279
247
|
imgs, axes = s.asarray(), s.axes
|
|
@@ -285,23 +253,15 @@ class TiffImageStack(NDArrayImageStack[ScalarType]):
|
|
|
285
253
|
|
|
286
254
|
orders = [AXES_ORDER[c] for c in axes]
|
|
287
255
|
imgs = imgs.transpose(np.argsort(orders))
|
|
288
|
-
super().__init__(imgs,
|
|
256
|
+
super().__init__(imgs, dtype=dtype)
|
|
289
257
|
|
|
290
258
|
|
|
291
259
|
class NrrdImageStack(NDArrayImageStack[ScalarType]):
|
|
292
260
|
"""Nrrd image stack."""
|
|
293
261
|
|
|
294
|
-
def __init__(
|
|
295
|
-
self,
|
|
296
|
-
fname: str,
|
|
297
|
-
swap_xy: Optional[bool] = None,
|
|
298
|
-
filp_xy: Optional[bool] = None,
|
|
299
|
-
*,
|
|
300
|
-
dtype: ScalarType,
|
|
301
|
-
**kwargs,
|
|
302
|
-
) -> None:
|
|
262
|
+
def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
|
|
303
263
|
imgs, header = nrrd.read(fname, **kwargs)
|
|
304
|
-
super().__init__(imgs,
|
|
264
|
+
super().__init__(imgs, dtype=dtype)
|
|
305
265
|
self.header = header
|
|
306
266
|
|
|
307
267
|
|
|
@@ -353,7 +313,7 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
353
313
|
use its coordinate system, remember to FLIP Y-AXIS BACK.
|
|
354
314
|
"""
|
|
355
315
|
|
|
356
|
-
_listdir: Callable[[str],
|
|
316
|
+
_listdir: Callable[[str], list[str]]
|
|
357
317
|
_read_patch: Callable[[str], npt.NDArray]
|
|
358
318
|
|
|
359
319
|
def __init__(
|
|
@@ -379,12 +339,17 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
379
339
|
self.res, self.res_dirs, self.res_patch_sizes = self.get_resolutions(root)
|
|
380
340
|
|
|
381
341
|
@cache
|
|
382
|
-
def listdir(path: str) ->
|
|
342
|
+
def listdir(path: str) -> list[str]:
|
|
383
343
|
return os.listdir(path)
|
|
384
344
|
|
|
385
345
|
@lru_cache(maxsize=lru_maxsize)
|
|
386
346
|
def read_patch(path: str) -> npt.NDArray[ScalarType]:
|
|
387
|
-
|
|
347
|
+
match os.path.splitext(path)[-1]:
|
|
348
|
+
case "raw":
|
|
349
|
+
# Treat it as a v3draw file
|
|
350
|
+
return V3drawImageStack(path, dtype=dtype).get_full()
|
|
351
|
+
case _:
|
|
352
|
+
return read_imgs(path, dtype=dtype).get_full()
|
|
388
353
|
|
|
389
354
|
self._listdir, self._read_patch = listdir, read_patch
|
|
390
355
|
|
|
@@ -453,19 +418,19 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
453
418
|
raise NotImplementedError() # TODO
|
|
454
419
|
|
|
455
420
|
@property
|
|
456
|
-
def shape(self) ->
|
|
421
|
+
def shape(self) -> tuple[int, int, int, int]:
|
|
457
422
|
res_max = self.res[-1]
|
|
458
423
|
return res_max[0], res_max[1], res_max[2], 1
|
|
459
424
|
|
|
460
425
|
@classmethod
|
|
461
|
-
def get_resolutions(cls, root: str) ->
|
|
426
|
+
def get_resolutions(cls, root: str) -> tuple[list[Vec3i], list[str], list[Vec3i]]:
|
|
462
427
|
"""Get all resolutions.
|
|
463
428
|
|
|
464
429
|
Returns
|
|
465
430
|
-------
|
|
466
431
|
resolutions : List of (int, int, int)
|
|
467
432
|
Sequence of sorted resolutions (from small to large).
|
|
468
|
-
roots :
|
|
433
|
+
roots : list[str]
|
|
469
434
|
Sequence of root of resolutions respectively.
|
|
470
435
|
patch_sizes : List of (int, int, int)
|
|
471
436
|
Sequence of patch size of resolutions respectively.
|
|
@@ -605,7 +570,7 @@ class GrayImageStack:
|
|
|
605
570
|
@overload
|
|
606
571
|
def __getitem__(self, key: npt.NDArray[np.integer[Any]]) -> np.float32: ...
|
|
607
572
|
@overload
|
|
608
|
-
def __getitem__(self, key: slice |
|
|
573
|
+
def __getitem__(self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice]) -> npt.NDArray[np.float32]: ...
|
|
609
574
|
# fmt: on
|
|
610
575
|
def __getitem__(self, key):
|
|
611
576
|
"""Get pixel/patch of image stack."""
|
|
@@ -632,7 +597,7 @@ class GrayImageStack:
|
|
|
632
597
|
return self.imgs.get_full()[:, :, :, 0]
|
|
633
598
|
|
|
634
599
|
@property
|
|
635
|
-
def shape(self) ->
|
|
600
|
+
def shape(self) -> tuple[int, int, int]:
|
|
636
601
|
return self.imgs.shape[:-1]
|
|
637
602
|
|
|
638
603
|
|
|
@@ -12,7 +12,8 @@ pip install swcgeom[all]
|
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
14
|
import time
|
|
15
|
-
from
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
from typing import Optional
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
18
19
|
import numpy.typing as npt
|
|
@@ -69,7 +70,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
69
70
|
x: Tree,
|
|
70
71
|
verbose: bool = True,
|
|
71
72
|
*,
|
|
72
|
-
ranges: Optional[
|
|
73
|
+
ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
|
|
73
74
|
) -> Iterable[npt.NDArray[np.uint8]]:
|
|
74
75
|
if verbose:
|
|
75
76
|
print("To image stack: " + x.source)
|
|
@@ -133,7 +134,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
133
134
|
scene = ObjectsScene()
|
|
134
135
|
scene.set_background((0, 0, 0))
|
|
135
136
|
|
|
136
|
-
def leave(n: Tree.Node, children:
|
|
137
|
+
def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
|
|
137
138
|
for c in children:
|
|
138
139
|
sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
|
|
139
140
|
scene.add_object(SDFObject(sdf, material).into())
|
|
@@ -175,7 +176,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
175
176
|
def save_tif(
|
|
176
177
|
fname: str,
|
|
177
178
|
frames: Iterable[npt.NDArray[np.uint8]],
|
|
178
|
-
resolution:
|
|
179
|
+
resolution: tuple[float, float] = (1, 1),
|
|
179
180
|
) -> None:
|
|
180
181
|
with tifffile.TiffWriter(fname) as tif:
|
|
181
182
|
for frame in frames:
|
|
@@ -191,7 +192,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
|
|
|
191
192
|
)
|
|
192
193
|
|
|
193
194
|
|
|
194
|
-
def _tp3f(x: npt.NDArray) ->
|
|
195
|
+
def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
|
|
195
196
|
"""Convert to tuple of 3 floats."""
|
|
196
197
|
assert len(x) == 3
|
|
197
198
|
return (float(x[0]), float(x[1]), float(x[2]))
|
swcgeom/transforms/images.py
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
"""Image stack related transform."""
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
-
from typing import Tuple
|
|
5
4
|
|
|
6
5
|
import numpy as np
|
|
7
6
|
import numpy.typing as npt
|
|
8
7
|
|
|
9
|
-
from swcgeom.transforms.base import Transform
|
|
8
|
+
from swcgeom.transforms.base import Identity, Transform
|
|
10
9
|
|
|
11
10
|
__all__ = [
|
|
12
11
|
"ImagesCenterCrop",
|
|
13
12
|
"ImagesScale",
|
|
14
13
|
"ImagesClip",
|
|
14
|
+
"ImagesFlip",
|
|
15
|
+
"ImagesFlipY",
|
|
15
16
|
"ImagesNormalizer",
|
|
16
17
|
"ImagesMeanVarianceAdjustment",
|
|
18
|
+
"ImagesScaleToUnitRange",
|
|
19
|
+
"ImagesHistogramEqualization",
|
|
17
20
|
"Center", # legacy
|
|
18
21
|
]
|
|
19
22
|
|
|
@@ -24,7 +27,7 @@ NDArrayf32 = npt.NDArray[np.float32]
|
|
|
24
27
|
class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
25
28
|
"""Get image stack center."""
|
|
26
29
|
|
|
27
|
-
def __init__(self, shape_out: int |
|
|
30
|
+
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
28
31
|
super().__init__()
|
|
29
32
|
self.shape_out = (
|
|
30
33
|
shape_out
|
|
@@ -45,11 +48,11 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
|
45
48
|
class Center(ImagesCenterCrop):
|
|
46
49
|
"""Get image stack center.
|
|
47
50
|
|
|
48
|
-
.. deprecated:: 0.
|
|
51
|
+
.. deprecated:: 0.16.0
|
|
49
52
|
Use :class:`ImagesCenterCrop` instead.
|
|
50
53
|
"""
|
|
51
54
|
|
|
52
|
-
def __init__(self, shape_out: int |
|
|
55
|
+
def __init__(self, shape_out: int | tuple[int, int, int]):
|
|
53
56
|
warnings.warn(
|
|
54
57
|
"`Center` is deprecated, use `ImagesCenterCrop` instead",
|
|
55
58
|
DeprecationWarning,
|
|
@@ -66,6 +69,9 @@ class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
|
|
|
66
69
|
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
67
70
|
return self.scaler * x
|
|
68
71
|
|
|
72
|
+
def extra_repr(self) -> str:
|
|
73
|
+
return f"scaler={self.scaler}"
|
|
74
|
+
|
|
69
75
|
|
|
70
76
|
class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
|
|
71
77
|
def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
|
|
@@ -75,6 +81,41 @@ class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
|
|
|
75
81
|
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
76
82
|
return np.clip(x, self.vmin, self.vmax)
|
|
77
83
|
|
|
84
|
+
def extra_repr(self) -> str:
|
|
85
|
+
return f"vmin={self.vmin}, vmax={self.vmax}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ImagesFlip(Transform[NDArrayf32, NDArrayf32]):
|
|
89
|
+
"""Flip image stack along axis."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, axis: int, /) -> None:
|
|
92
|
+
super().__init__()
|
|
93
|
+
self.axis = axis
|
|
94
|
+
|
|
95
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
96
|
+
return np.flip(x, axis=self.axis)
|
|
97
|
+
|
|
98
|
+
def extra_repr(self) -> str:
|
|
99
|
+
return f"axis={self.axis}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ImagesFlipY(ImagesFlip):
|
|
103
|
+
"""Flip image stack along Y-axis.
|
|
104
|
+
|
|
105
|
+
See Also
|
|
106
|
+
--------
|
|
107
|
+
~.images.io.TeraflyImageStack:
|
|
108
|
+
Terafly and Vaa3d use a especial right-handed coordinate system
|
|
109
|
+
(with origin point in the left-top and z-axis points front),
|
|
110
|
+
but we flip y-axis to makes it a left-handed coordinate system
|
|
111
|
+
(with orgin point in the left-bottom and z-axis points front).
|
|
112
|
+
If you need to use its coordinate system, remember to FLIP
|
|
113
|
+
Y-AXIS BACK.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, axis: int = 1, /) -> None:
|
|
117
|
+
super().__init__(axis) # (X, Y, Z, C)
|
|
118
|
+
|
|
78
119
|
|
|
79
120
|
class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
|
|
80
121
|
"""Normalize image stack."""
|
|
@@ -100,3 +141,62 @@ class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
|
|
|
100
141
|
|
|
101
142
|
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
102
143
|
return (x - self.mean) / self.variance
|
|
144
|
+
|
|
145
|
+
def extra_repr(self) -> str:
|
|
146
|
+
return f"mean={self.mean}, variance={self.variance}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ImagesScaleToUnitRange(Transform[NDArrayf32, NDArrayf32]):
|
|
150
|
+
"""Scale image stack to unit range."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, vmin: float, vmax: float, *, clip: bool = True) -> None:
|
|
153
|
+
"""Scale image stack to unit range.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
vmin : float
|
|
158
|
+
Minimum value.
|
|
159
|
+
vmax : float
|
|
160
|
+
Maximum value.
|
|
161
|
+
clip : bool, default True
|
|
162
|
+
Clip values to [0, 1] to avoid numerical issues.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
super().__init__()
|
|
166
|
+
self.vmin = vmin
|
|
167
|
+
self.vmax = vmax
|
|
168
|
+
self.diff = vmax - vmin
|
|
169
|
+
self.clip = clip
|
|
170
|
+
self.post = ImagesClip(0, 1) if self.clip else Identity()
|
|
171
|
+
|
|
172
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
173
|
+
return self.post((x - self.vmin) / self.diff)
|
|
174
|
+
|
|
175
|
+
def extra_repr(self) -> str:
|
|
176
|
+
return f"vmin={self.vmin}, vmax={self.vmax}, clip={self.clip}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ImagesHistogramEqualization(Transform[NDArrayf32, NDArrayf32]):
|
|
180
|
+
"""Image histogram equalization.
|
|
181
|
+
|
|
182
|
+
References
|
|
183
|
+
----------
|
|
184
|
+
http://www.janeriksolem.net/histogram-equalization-with-python-and.html
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, bins: int = 256) -> None:
|
|
188
|
+
super().__init__()
|
|
189
|
+
self.bins = bins
|
|
190
|
+
|
|
191
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
192
|
+
# get image histogram
|
|
193
|
+
hist, bin_edges = np.histogram(x.flatten(), self.bins, density=True)
|
|
194
|
+
cdf = hist.cumsum() # cumulative distribution function
|
|
195
|
+
cdf = cdf / cdf[-1] # normalize
|
|
196
|
+
|
|
197
|
+
# use linear interpolation of cdf to find new pixel values
|
|
198
|
+
equalized = np.interp(x.flatten(), bin_edges[:-1], cdf)
|
|
199
|
+
return equalized.reshape(x.shape).astype(np.float32)
|
|
200
|
+
|
|
201
|
+
def extra_repr(self) -> str:
|
|
202
|
+
return f"bins={self.bins}"
|