swcgeom 0.16.0__py3-none-any.whl → 0.17.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.
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 +3 -3
- swcgeom/analysis/{node_features.py → features.py} +105 -3
- swcgeom/images/io.py +60 -84
- swcgeom/transforms/images.py +103 -2
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.0.dist-info}/METADATA +1 -1
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.0.dist-info}/RECORD +11 -13
- swcgeom/analysis/branch_features.py +0 -67
- swcgeom/analysis/path_features.py +0 -37
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.0.dist-info}/LICENSE +0 -0
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.0.dist-info}/WHEEL +0 -0
- {swcgeom-0.16.0.dist-info → swcgeom-0.17.0.dist-info}/top_level.txt +0 -0
swcgeom/_version.py
CHANGED
swcgeom/analysis/__init__.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Analysis for neuron trees."""
|
|
2
2
|
|
|
3
|
-
from swcgeom.analysis.branch_features import *
|
|
4
3
|
from swcgeom.analysis.feature_extractor import *
|
|
5
|
-
from swcgeom.analysis.
|
|
6
|
-
from swcgeom.analysis.path_features import *
|
|
4
|
+
from swcgeom.analysis.features import *
|
|
7
5
|
from swcgeom.analysis.sholl import *
|
|
8
6
|
from swcgeom.analysis.trunk import *
|
|
9
7
|
from swcgeom.analysis.visualization import *
|
|
@@ -17,13 +17,13 @@ import numpy.typing as npt
|
|
|
17
17
|
import seaborn as sns
|
|
18
18
|
from matplotlib.axes import Axes
|
|
19
19
|
|
|
20
|
-
from swcgeom.analysis.
|
|
21
|
-
from swcgeom.analysis.node_features import (
|
|
20
|
+
from swcgeom.analysis.features import (
|
|
22
21
|
BifurcationFeatures,
|
|
22
|
+
BranchFeatures,
|
|
23
23
|
NodeFeatures,
|
|
24
|
+
PathFeatures,
|
|
24
25
|
TipFeatures,
|
|
25
26
|
)
|
|
26
|
-
from swcgeom.analysis.path_features import PathFeatures
|
|
27
27
|
from swcgeom.analysis.sholl import Sholl
|
|
28
28
|
from swcgeom.analysis.volume import get_volume
|
|
29
29
|
from swcgeom.core import Population, Populations, Tree
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Feature anlysis of tree."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from functools import cached_property
|
|
5
|
+
from typing import List, TypeVar
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import numpy.typing as npt
|
|
8
9
|
from typing_extensions import Self
|
|
9
10
|
|
|
10
|
-
from swcgeom.core import BranchTree, Tree
|
|
11
|
+
from swcgeom.core import Branch, BranchTree, Tree
|
|
11
12
|
|
|
12
|
-
__all__ = [
|
|
13
|
+
__all__ = [
|
|
14
|
+
"NodeFeatures",
|
|
15
|
+
"BifurcationFeatures",
|
|
16
|
+
"TipFeatures",
|
|
17
|
+
"PathFeatures",
|
|
18
|
+
"BranchFeatures",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T", bound=Branch)
|
|
22
|
+
|
|
23
|
+
# Node Level
|
|
13
24
|
|
|
14
25
|
|
|
15
26
|
class NodeFeatures:
|
|
@@ -31,6 +42,7 @@ class NodeFeatures:
|
|
|
31
42
|
-------
|
|
32
43
|
count : array of shape (1,)
|
|
33
44
|
"""
|
|
45
|
+
|
|
34
46
|
return np.array([self.tree.number_of_nodes()], dtype=np.float32)
|
|
35
47
|
|
|
36
48
|
def get_radial_distance(self) -> npt.NDArray[np.float32]:
|
|
@@ -41,6 +53,7 @@ class NodeFeatures:
|
|
|
41
53
|
radial_distance : npt.NDArray[np.float32]
|
|
42
54
|
Array of shape (N,).
|
|
43
55
|
"""
|
|
56
|
+
|
|
44
57
|
xyz = self.tree.xyz() - self.tree.soma().xyz()
|
|
45
58
|
radial_distance = np.linalg.norm(xyz, axis=1)
|
|
46
59
|
return radial_distance
|
|
@@ -58,6 +71,7 @@ class NodeFeatures:
|
|
|
58
71
|
order : npt.NDArray[np.int32]
|
|
59
72
|
Array of shape (N,), which k is the number of branchs.
|
|
60
73
|
"""
|
|
74
|
+
|
|
61
75
|
order = np.zeros_like(self._branch_tree.id(), dtype=np.int32)
|
|
62
76
|
|
|
63
77
|
def assign_depth(n: Tree.Node, pre_depth: int | None) -> int:
|
|
@@ -88,6 +102,7 @@ class _SubsetNodesFeatures(ABC):
|
|
|
88
102
|
count : npt.NDArray[np.float32]
|
|
89
103
|
Array of shape (1,).
|
|
90
104
|
"""
|
|
105
|
+
|
|
91
106
|
return np.array([np.count_nonzero(self.nodes)], dtype=np.float32)
|
|
92
107
|
|
|
93
108
|
def get_radial_distance(self) -> npt.NDArray[np.float32]:
|
|
@@ -98,6 +113,7 @@ class _SubsetNodesFeatures(ABC):
|
|
|
98
113
|
radial_distance : npt.NDArray[np.float32]
|
|
99
114
|
Array of shape (N,).
|
|
100
115
|
"""
|
|
116
|
+
|
|
101
117
|
return self._features.get_radial_distance()[self.nodes]
|
|
102
118
|
|
|
103
119
|
@classmethod
|
|
@@ -119,3 +135,89 @@ class TipFeatures(_SubsetNodesFeatures):
|
|
|
119
135
|
@cached_property
|
|
120
136
|
def nodes(self) -> npt.NDArray[np.bool_]:
|
|
121
137
|
return np.array([n.is_tip() for n in self._features.tree])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Path Level
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class PathFeatures:
|
|
144
|
+
"""Path analysis of tree."""
|
|
145
|
+
|
|
146
|
+
tree: Tree
|
|
147
|
+
|
|
148
|
+
def __init__(self, tree: Tree) -> None:
|
|
149
|
+
self.tree = tree
|
|
150
|
+
|
|
151
|
+
def get_count(self) -> int:
|
|
152
|
+
return len(self._paths)
|
|
153
|
+
|
|
154
|
+
def get_length(self) -> npt.NDArray[np.float32]:
|
|
155
|
+
"""Get length of paths."""
|
|
156
|
+
|
|
157
|
+
length = [path.length() for path in self._paths]
|
|
158
|
+
return np.array(length, dtype=np.float32)
|
|
159
|
+
|
|
160
|
+
def get_tortuosity(self) -> npt.NDArray[np.float32]:
|
|
161
|
+
"""Get tortuosity of path."""
|
|
162
|
+
|
|
163
|
+
return np.array([path.tortuosity() for path in self._paths], dtype=np.float32)
|
|
164
|
+
|
|
165
|
+
@cached_property
|
|
166
|
+
def _paths(self) -> List[Tree.Path]:
|
|
167
|
+
return self.tree.get_paths()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class BranchFeatures:
|
|
171
|
+
"""Analysis bransh of tree."""
|
|
172
|
+
|
|
173
|
+
tree: Tree
|
|
174
|
+
|
|
175
|
+
def __init__(self, tree: Tree) -> None:
|
|
176
|
+
self.tree = tree
|
|
177
|
+
|
|
178
|
+
def get_count(self) -> int:
|
|
179
|
+
return len(self._branches)
|
|
180
|
+
|
|
181
|
+
def get_length(self) -> npt.NDArray[np.float32]:
|
|
182
|
+
"""Get length of branches."""
|
|
183
|
+
|
|
184
|
+
length = [br.length() for br in self._branches]
|
|
185
|
+
return np.array(length, dtype=np.float32)
|
|
186
|
+
|
|
187
|
+
def get_tortuosity(self) -> npt.NDArray[np.float32]:
|
|
188
|
+
"""Get tortuosity of path."""
|
|
189
|
+
|
|
190
|
+
return np.array([br.tortuosity() for br in self._branches], dtype=np.float32)
|
|
191
|
+
|
|
192
|
+
def get_angle(self, eps: float = 1e-7) -> npt.NDArray[np.float32]:
|
|
193
|
+
"""Get agnle between branches.
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
angle : npt.NDArray[np.float32]
|
|
198
|
+
An array of shape (N, N), which N is length of branches.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
return self.calc_angle(self._branches, eps=eps)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def calc_angle(branches: List[T], eps: float = 1e-7) -> npt.NDArray[np.float32]:
|
|
205
|
+
"""Calc agnle between branches.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
angle : npt.NDArray[np.float32]
|
|
210
|
+
An array of shape (N, N), which N is length of branches.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
vector = np.array([br[-1].xyz() - br[0].xyz() for br in branches])
|
|
214
|
+
vector_dot = np.matmul(vector, vector.T)
|
|
215
|
+
vector_norm = np.linalg.norm(vector, ord=2, axis=1, keepdims=True)
|
|
216
|
+
vector_norm_dot = np.matmul(vector_norm, vector_norm.T) + eps
|
|
217
|
+
arccos = np.clip(vector_dot / vector_norm_dot, -1, 1)
|
|
218
|
+
angle = np.arccos(arccos)
|
|
219
|
+
return angle
|
|
220
|
+
|
|
221
|
+
@cached_property
|
|
222
|
+
def _branches(self) -> List[Tree.Branch]:
|
|
223
|
+
return self.tree.get_branches()
|
swcgeom/images/io.py
CHANGED
|
@@ -107,26 +107,42 @@ def read_imgs(fname: str, *, dtype: None =..., **kwargs) -> ImageStack[np.float3
|
|
|
107
107
|
# fmt:on
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
def read_imgs(fname: str,
|
|
111
|
-
"""Read image stack.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
110
|
+
def read_imgs(fname: str, **kwargs): # type: ignore
|
|
111
|
+
"""Read image stack.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
fname : str
|
|
116
|
+
The path of image stack.
|
|
117
|
+
dtype : np.dtype, default to `np.float32`
|
|
118
|
+
Casting data to specified dtype. If integer and float
|
|
119
|
+
conversions occur, they will be scaled (assuming floats are
|
|
120
|
+
between 0 and 1).
|
|
121
|
+
**kwargs : Dict[str, Any]
|
|
122
|
+
Forwarding to the corresponding reader.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
kwargs.setdefault("dtype", np.float32)
|
|
126
|
+
if not os.path.exists(fname):
|
|
127
|
+
raise ValueError(f"image stack not exists: {fname}")
|
|
128
|
+
|
|
129
|
+
# match file extension
|
|
130
|
+
match os.path.splitext(fname)[-1]:
|
|
131
|
+
case ".tif" | ".tiff":
|
|
132
|
+
return TiffImageStack(fname, **kwargs)
|
|
133
|
+
case ".nrrd":
|
|
134
|
+
return NrrdImageStack(fname, **kwargs)
|
|
135
|
+
case ".v3dpbd":
|
|
136
|
+
return V3dpbdImageStack(fname, **kwargs)
|
|
137
|
+
case ".v3draw":
|
|
138
|
+
return V3drawImageStack(fname, **kwargs)
|
|
139
|
+
case ".npy":
|
|
140
|
+
return NDArrayImageStack(np.load(fname), **kwargs)
|
|
141
|
+
|
|
142
|
+
# try to read as terafly
|
|
126
143
|
if TeraflyImageStack.is_root(fname):
|
|
127
144
|
return TeraflyImageStack(fname, **kwargs)
|
|
128
|
-
|
|
129
|
-
raise ValueError("image stack not exists")
|
|
145
|
+
|
|
130
146
|
raise ValueError("unsupported image stack")
|
|
131
147
|
|
|
132
148
|
|
|
@@ -135,7 +151,6 @@ def save_tiff(
|
|
|
135
151
|
fname: str,
|
|
136
152
|
*,
|
|
137
153
|
dtype: Optional[np.unsignedinteger | np.floating] = None,
|
|
138
|
-
swap_xy: Optional[bool] = None,
|
|
139
154
|
compression: str | Literal[False] = "zlib",
|
|
140
155
|
**kwargs,
|
|
141
156
|
) -> None:
|
|
@@ -164,17 +179,6 @@ def save_tiff(
|
|
|
164
179
|
data = np.expand_dims(data, -1) # (_, _, _) -> (_, _, _, C), C === 1
|
|
165
180
|
|
|
166
181
|
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
182
|
assert data.ndim == 4, "should be an array of shape (X, Y, Z, C)"
|
|
179
183
|
assert data.shape[-1] in [1, 3], "support 'miniblack' or 'rgb'"
|
|
180
184
|
|
|
@@ -209,12 +213,7 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
209
213
|
"""NDArray image stack."""
|
|
210
214
|
|
|
211
215
|
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,
|
|
216
|
+
self, imgs: npt.NDArray[Any], *, dtype: Optional[ScalarType] = None
|
|
218
217
|
) -> None:
|
|
219
218
|
super().__init__()
|
|
220
219
|
|
|
@@ -222,34 +221,22 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
222
221
|
imgs = np.expand_dims(imgs, -1)
|
|
223
222
|
assert imgs.ndim == 4, "Should be shape of (X, Y, Z, C)"
|
|
224
223
|
|
|
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)
|
|
224
|
+
if dtype is not None:
|
|
225
|
+
dtype_raw = imgs.dtype
|
|
226
|
+
if np.issubdtype(dtype, np.floating) and np.issubdtype(
|
|
227
|
+
dtype_raw, np.unsignedinteger
|
|
228
|
+
):
|
|
229
|
+
sclar_factor = 1.0 / UINT_MAX[dtype_raw]
|
|
230
|
+
imgs = sclar_factor * imgs.astype(dtype)
|
|
231
|
+
elif np.issubdtype(dtype, np.unsignedinteger) and np.issubdtype(
|
|
232
|
+
dtype_raw, np.floating
|
|
233
|
+
):
|
|
234
|
+
sclar_factor = UINT_MAX[dtype] # type: ignore
|
|
235
|
+
imgs *= (sclar_factor * imgs).astype(dtype)
|
|
236
|
+
else:
|
|
237
|
+
imgs = imgs.astype(dtype)
|
|
245
238
|
|
|
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
|
|
239
|
+
self.imgs = imgs
|
|
253
240
|
|
|
254
241
|
def __getitem__(self, key):
|
|
255
242
|
return self.imgs.__getitem__(key)
|
|
@@ -265,15 +252,7 @@ class NDArrayImageStack(ImageStack[ScalarType]):
|
|
|
265
252
|
class TiffImageStack(NDArrayImageStack[ScalarType]):
|
|
266
253
|
"""Tiff image stack."""
|
|
267
254
|
|
|
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:
|
|
255
|
+
def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
|
|
277
256
|
with tifffile.TiffFile(fname, **kwargs) as f:
|
|
278
257
|
s = f.series[0]
|
|
279
258
|
imgs, axes = s.asarray(), s.axes
|
|
@@ -285,23 +264,15 @@ class TiffImageStack(NDArrayImageStack[ScalarType]):
|
|
|
285
264
|
|
|
286
265
|
orders = [AXES_ORDER[c] for c in axes]
|
|
287
266
|
imgs = imgs.transpose(np.argsort(orders))
|
|
288
|
-
super().__init__(imgs,
|
|
267
|
+
super().__init__(imgs, dtype=dtype)
|
|
289
268
|
|
|
290
269
|
|
|
291
270
|
class NrrdImageStack(NDArrayImageStack[ScalarType]):
|
|
292
271
|
"""Nrrd image stack."""
|
|
293
272
|
|
|
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:
|
|
273
|
+
def __init__(self, fname: str, *, dtype: ScalarType, **kwargs) -> None:
|
|
303
274
|
imgs, header = nrrd.read(fname, **kwargs)
|
|
304
|
-
super().__init__(imgs,
|
|
275
|
+
super().__init__(imgs, dtype=dtype)
|
|
305
276
|
self.header = header
|
|
306
277
|
|
|
307
278
|
|
|
@@ -384,7 +355,12 @@ class TeraflyImageStack(ImageStack[ScalarType]):
|
|
|
384
355
|
|
|
385
356
|
@lru_cache(maxsize=lru_maxsize)
|
|
386
357
|
def read_patch(path: str) -> npt.NDArray[ScalarType]:
|
|
387
|
-
|
|
358
|
+
match os.path.splitext(path)[-1]:
|
|
359
|
+
case "raw":
|
|
360
|
+
# Treat it as a v3draw file
|
|
361
|
+
return V3drawImageStack(path, dtype=dtype).get_full()
|
|
362
|
+
case _:
|
|
363
|
+
return read_imgs(path, dtype=dtype).get_full()
|
|
388
364
|
|
|
389
365
|
self._listdir, self._read_patch = listdir, read_patch
|
|
390
366
|
|
swcgeom/transforms/images.py
CHANGED
|
@@ -6,14 +6,18 @@ from typing import Tuple
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import numpy.typing as npt
|
|
8
8
|
|
|
9
|
-
from swcgeom.transforms.base import Transform
|
|
9
|
+
from swcgeom.transforms.base import Identity, Transform
|
|
10
10
|
|
|
11
11
|
__all__ = [
|
|
12
12
|
"ImagesCenterCrop",
|
|
13
13
|
"ImagesScale",
|
|
14
14
|
"ImagesClip",
|
|
15
|
+
"ImagesFlip",
|
|
16
|
+
"ImagesFlipY",
|
|
15
17
|
"ImagesNormalizer",
|
|
16
18
|
"ImagesMeanVarianceAdjustment",
|
|
19
|
+
"ImagesScaleToUnitRange",
|
|
20
|
+
"ImagesHistogramEqualization",
|
|
17
21
|
"Center", # legacy
|
|
18
22
|
]
|
|
19
23
|
|
|
@@ -45,7 +49,7 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
|
|
|
45
49
|
class Center(ImagesCenterCrop):
|
|
46
50
|
"""Get image stack center.
|
|
47
51
|
|
|
48
|
-
.. deprecated:: 0.
|
|
52
|
+
.. deprecated:: 0.16.0
|
|
49
53
|
Use :class:`ImagesCenterCrop` instead.
|
|
50
54
|
"""
|
|
51
55
|
|
|
@@ -66,6 +70,9 @@ class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
|
|
|
66
70
|
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
67
71
|
return self.scaler * x
|
|
68
72
|
|
|
73
|
+
def extra_repr(self) -> str:
|
|
74
|
+
return f"scaler={self.scaler}"
|
|
75
|
+
|
|
69
76
|
|
|
70
77
|
class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
|
|
71
78
|
def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
|
|
@@ -75,6 +82,41 @@ class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
|
|
|
75
82
|
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
76
83
|
return np.clip(x, self.vmin, self.vmax)
|
|
77
84
|
|
|
85
|
+
def extra_repr(self) -> str:
|
|
86
|
+
return f"vmin={self.vmin}, vmax={self.vmax}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ImagesFlip(Transform[NDArrayf32, NDArrayf32]):
|
|
90
|
+
"""Flip image stack along axis."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, axis: int, /) -> None:
|
|
93
|
+
super().__init__()
|
|
94
|
+
self.axis = axis
|
|
95
|
+
|
|
96
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
97
|
+
return np.flip(x, axis=self.axis)
|
|
98
|
+
|
|
99
|
+
def extra_repr(self) -> str:
|
|
100
|
+
return f"axis={self.axis}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ImagesFlipY(ImagesFlip):
|
|
104
|
+
"""Flip image stack along Y-axis.
|
|
105
|
+
|
|
106
|
+
See Also
|
|
107
|
+
--------
|
|
108
|
+
~.images.io.TeraflyImageStack:
|
|
109
|
+
Terafly and Vaa3d use a especial right-handed coordinate system
|
|
110
|
+
(with origin point in the left-top and z-axis points front),
|
|
111
|
+
but we flip y-axis to makes it a left-handed coordinate system
|
|
112
|
+
(with orgin point in the left-bottom and z-axis points front).
|
|
113
|
+
If you need to use its coordinate system, remember to FLIP
|
|
114
|
+
Y-AXIS BACK.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, axis: int = 1, /) -> None:
|
|
118
|
+
super().__init__(axis) # (X, Y, Z, C)
|
|
119
|
+
|
|
78
120
|
|
|
79
121
|
class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
|
|
80
122
|
"""Normalize image stack."""
|
|
@@ -100,3 +142,62 @@ class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
|
|
|
100
142
|
|
|
101
143
|
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
102
144
|
return (x - self.mean) / self.variance
|
|
145
|
+
|
|
146
|
+
def extra_repr(self) -> str:
|
|
147
|
+
return f"mean={self.mean}, variance={self.variance}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ImagesScaleToUnitRange(Transform[NDArrayf32, NDArrayf32]):
|
|
151
|
+
"""Scale image stack to unit range."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, vmin: float, vmax: float, *, clip: bool = True) -> None:
|
|
154
|
+
"""Scale image stack to unit range.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
vmin : float
|
|
159
|
+
Minimum value.
|
|
160
|
+
vmax : float
|
|
161
|
+
Maximum value.
|
|
162
|
+
clip : bool, default True
|
|
163
|
+
Clip values to [0, 1] to avoid numerical issues.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
super().__init__()
|
|
167
|
+
self.vmin = vmin
|
|
168
|
+
self.vmax = vmax
|
|
169
|
+
self.diff = vmax - vmin
|
|
170
|
+
self.clip = clip
|
|
171
|
+
self.post = ImagesClip(0, 1) if self.clip else Identity()
|
|
172
|
+
|
|
173
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
174
|
+
return self.post((x - self.vmin) / self.diff)
|
|
175
|
+
|
|
176
|
+
def extra_repr(self) -> str:
|
|
177
|
+
return f"vmin={self.vmin}, vmax={self.vmax}, clip={self.clip}"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ImagesHistogramEqualization(Transform[NDArrayf32, NDArrayf32]):
|
|
181
|
+
"""Image histogram equalization.
|
|
182
|
+
|
|
183
|
+
References
|
|
184
|
+
----------
|
|
185
|
+
http://www.janeriksolem.net/histogram-equalization-with-python-and.html
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, bins: int = 256) -> None:
|
|
189
|
+
super().__init__()
|
|
190
|
+
self.bins = bins
|
|
191
|
+
|
|
192
|
+
def __call__(self, x: NDArrayf32) -> NDArrayf32:
|
|
193
|
+
# get image histogram
|
|
194
|
+
hist, bin_edges = np.histogram(x.flatten(), self.bins, density=True)
|
|
195
|
+
cdf = hist.cumsum() # cumulative distribution function
|
|
196
|
+
cdf = cdf / cdf[-1] # normalize
|
|
197
|
+
|
|
198
|
+
# use linear interpolation of cdf to find new pixel values
|
|
199
|
+
equalized = np.interp(x.flatten(), bin_edges[:-1], cdf)
|
|
200
|
+
return equalized.reshape(x.shape).astype(np.float32)
|
|
201
|
+
|
|
202
|
+
def extra_repr(self) -> str:
|
|
203
|
+
return f"bins={self.bins}"
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
swcgeom/__init__.py,sha256=z88Zwcjv-ii7c7dYd9QPg9XrUVorQjtrgGbQCsEnQhc,265
|
|
2
|
-
swcgeom/_version.py,sha256=
|
|
3
|
-
swcgeom/analysis/__init__.py,sha256=
|
|
4
|
-
swcgeom/analysis/
|
|
5
|
-
swcgeom/analysis/
|
|
2
|
+
swcgeom/_version.py,sha256=kBQrirg2HABnJUda1U0a6ZmIqeCqRLaPZOws884sTW8,413
|
|
3
|
+
swcgeom/analysis/__init__.py,sha256=NurxIAyjsxjYv9rbzNf65y2sv-iBcGTmLap645hYq9Y,280
|
|
4
|
+
swcgeom/analysis/feature_extractor.py,sha256=MBhnE6JbgCMhfTO8jEXqbLkVIiQh2WvVH2C4JR0IcpI,13948
|
|
5
|
+
swcgeom/analysis/features.py,sha256=meUXUOIqV1PgiRtgYQdFrVuTmyPYQRSJ0C1Ll97XDhU,5965
|
|
6
6
|
swcgeom/analysis/lmeasure.py,sha256=GI5HoIXkRp_GEDHd_JXJOMeAtZ26HP6lbSfF_0L-2r8,27559
|
|
7
|
-
swcgeom/analysis/node_features.py,sha256=fevnyrF-t4PX39ifLypiDW6EUWB8i-a3PpBnQZU3VOc,3407
|
|
8
|
-
swcgeom/analysis/path_features.py,sha256=iE21HBxAoGLxk_qK7MBwQhyUOBqNPcnk4urVHr9SVqk,889
|
|
9
7
|
swcgeom/analysis/sholl.py,sha256=KeUyEXLatHjmn4hOSs8y_0o8UKDq9VoIufJ_81SOtgw,7249
|
|
10
8
|
swcgeom/analysis/trunk.py,sha256=L2tjUIUmrRQpah_W3ZETGWd16bDXJ5F8Sk2XBNGms0Q,5558
|
|
11
9
|
swcgeom/analysis/visualization.py,sha256=mKOpzTPkLpr1ggGL1MZPZRTG92GEg4idLT4eN5z5KOs,5654
|
|
@@ -32,14 +30,14 @@ swcgeom/images/__init__.py,sha256=QBP1ZGGo2nWAcV7Krz-vbvW_jN4ChqXrrpoScXcUURs,96
|
|
|
32
30
|
swcgeom/images/augmentation.py,sha256=cV4k4KR_WcsRajyr0DuhHVDRRZcN4FQ7OIzB_rb2FUo,4173
|
|
33
31
|
swcgeom/images/contrast.py,sha256=ViZVW6XI-l2sLVTODLRLtHinv_7lVgtH-xZmaw1nQLw,2160
|
|
34
32
|
swcgeom/images/folder.py,sha256=YY9YjF17nDwOQEXhzFe-Dj0zPTcG0WP1-gisscImmYg,6674
|
|
35
|
-
swcgeom/images/io.py,sha256=
|
|
33
|
+
swcgeom/images/io.py,sha256=05VlDcrtt3Un7M2hFddV0aWopgj55TuukSBjHLYwLHg,20704
|
|
36
34
|
swcgeom/transforms/__init__.py,sha256=1rr4X--qY_lBi7l7_NHyvvkoWpQOQOqkioRT8I20olI,562
|
|
37
35
|
swcgeom/transforms/base.py,sha256=gN5Iqi-OHkYrsjllSOdxI6Yzav3jJGoi6kUPy-38FAs,4101
|
|
38
36
|
swcgeom/transforms/branch.py,sha256=R0rVti--u70IiUKyHSx6MsDYJyy6zSCf18Uia2Cmh28,5410
|
|
39
37
|
swcgeom/transforms/geometry.py,sha256=XR73fO_8T7otUFIllqKOWW0OnrsXBc7yA01oDT99yMc,7385
|
|
40
38
|
swcgeom/transforms/image_preprocess.py,sha256=ZVPpRoO69dmLF5K7CWsGaQJXB2G5gxdvA-FcDmfz4yQ,3662
|
|
41
39
|
swcgeom/transforms/image_stack.py,sha256=RIldGAOI3QeoeBtr0VKeBKJVg-fWSmzWll63SvsaTfI,5775
|
|
42
|
-
swcgeom/transforms/images.py,sha256=
|
|
40
|
+
swcgeom/transforms/images.py,sha256=3j8X8L9q0nSMJ_fP-73jL-zYtgi3fn3Erti9Ej1UZYo,5760
|
|
43
41
|
swcgeom/transforms/mst.py,sha256=Oc_HnaXjg5EXC7ZnOPneHX0-rXizDAEUcjq63GTj-ac,6251
|
|
44
42
|
swcgeom/transforms/neurolucida_asc.py,sha256=O4fK1OMropPnIEVdMenbyT_sV39gEGIv_6vIl6yUOVg,14146
|
|
45
43
|
swcgeom/transforms/path.py,sha256=Gk2iunGQMX7vE83bdo8xoDO-KAT1Vvep0iZs7oFLzFQ,1089
|
|
@@ -60,8 +58,8 @@ swcgeom/utils/sdf.py,sha256=zNDgwXKRNIVcV4ORMmDXup6Bhf_vlHqwa-b3WZn6KhE,10684
|
|
|
60
58
|
swcgeom/utils/solid_geometry.py,sha256=TV02jhcoCLCqtYA9hfE50LFD_VRfixMiOSiHB5Jb2_U,2431
|
|
61
59
|
swcgeom/utils/transforms.py,sha256=PmP5fL_iVguq4GR2aqXhM0TeCsiFVnrPZMZG6zLohrE,6983
|
|
62
60
|
swcgeom/utils/volumetric_object.py,sha256=DVRGGmQrAL0oaW6hbNtp5TStbic9DfyJdCzsv2FNw2c,15134
|
|
63
|
-
swcgeom-0.
|
|
64
|
-
swcgeom-0.
|
|
65
|
-
swcgeom-0.
|
|
66
|
-
swcgeom-0.
|
|
67
|
-
swcgeom-0.
|
|
61
|
+
swcgeom-0.17.0.dist-info/LICENSE,sha256=JPtohhZ4XURqoKI0ZqnMYb7dobCOoZR_n5EpnaLTp3E,11344
|
|
62
|
+
swcgeom-0.17.0.dist-info/METADATA,sha256=wtsF7F3P38QJtBXm0wpsAQRofs2sjXAbNHrlrFDLM-4,2332
|
|
63
|
+
swcgeom-0.17.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
64
|
+
swcgeom-0.17.0.dist-info/top_level.txt,sha256=hmLyUXWS61Gxl07haswFEKKefYPBVJYlUlol8ghNkjY,8
|
|
65
|
+
swcgeom-0.17.0.dist-info/RECORD,,
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
"""Branch anlysis of tree."""
|
|
2
|
-
|
|
3
|
-
from functools import cached_property
|
|
4
|
-
from typing import List, TypeVar
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
import numpy.typing as npt
|
|
8
|
-
|
|
9
|
-
from swcgeom.core import Branch, Tree
|
|
10
|
-
|
|
11
|
-
__all__ = ["BranchFeatures"]
|
|
12
|
-
|
|
13
|
-
T = TypeVar("T", bound=Branch)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class BranchFeatures:
|
|
17
|
-
"""Analysis bransh of tree."""
|
|
18
|
-
|
|
19
|
-
tree: Tree
|
|
20
|
-
|
|
21
|
-
def __init__(self, tree: Tree) -> None:
|
|
22
|
-
self.tree = tree
|
|
23
|
-
|
|
24
|
-
def get_count(self) -> int:
|
|
25
|
-
return len(self._branches)
|
|
26
|
-
|
|
27
|
-
def get_length(self) -> npt.NDArray[np.float32]:
|
|
28
|
-
"""Get length of branches."""
|
|
29
|
-
length = [br.length() for br in self._branches]
|
|
30
|
-
return np.array(length, dtype=np.float32)
|
|
31
|
-
|
|
32
|
-
def get_tortuosity(self) -> npt.NDArray[np.float32]:
|
|
33
|
-
"""Get tortuosity of path."""
|
|
34
|
-
return np.array([br.tortuosity() for br in self._branches], dtype=np.float32)
|
|
35
|
-
|
|
36
|
-
def get_angle(self, eps: float = 1e-7) -> npt.NDArray[np.float32]:
|
|
37
|
-
"""Get agnle between branches.
|
|
38
|
-
|
|
39
|
-
Returns
|
|
40
|
-
-------
|
|
41
|
-
angle : npt.NDArray[np.float32]
|
|
42
|
-
An array of shape (N, N), which N is length of branches.
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
return self.calc_angle(self._branches, eps=eps)
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def calc_angle(branches: List[T], eps: float = 1e-7) -> npt.NDArray[np.float32]:
|
|
49
|
-
"""Calc agnle between branches.
|
|
50
|
-
|
|
51
|
-
Returns
|
|
52
|
-
-------
|
|
53
|
-
angle : npt.NDArray[np.float32]
|
|
54
|
-
An array of shape (N, N), which N is length of branches.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
vector = np.array([br[-1].xyz() - br[0].xyz() for br in branches])
|
|
58
|
-
vector_dot = np.matmul(vector, vector.T)
|
|
59
|
-
vector_norm = np.linalg.norm(vector, ord=2, axis=1, keepdims=True)
|
|
60
|
-
vector_norm_dot = np.matmul(vector_norm, vector_norm.T) + eps
|
|
61
|
-
arccos = np.clip(vector_dot / vector_norm_dot, -1, 1)
|
|
62
|
-
angle = np.arccos(arccos)
|
|
63
|
-
return angle
|
|
64
|
-
|
|
65
|
-
@cached_property
|
|
66
|
-
def _branches(self) -> List[Tree.Branch]:
|
|
67
|
-
return self.tree.get_branches()
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""Depth distribution of tree."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from functools import cached_property
|
|
5
|
-
from typing import List
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
|
-
import numpy.typing as npt
|
|
9
|
-
|
|
10
|
-
from swcgeom.core import Tree
|
|
11
|
-
|
|
12
|
-
__all__ = ["PathFeatures"]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class PathFeatures:
|
|
16
|
-
"""Path analysis of tree."""
|
|
17
|
-
|
|
18
|
-
tree: Tree
|
|
19
|
-
|
|
20
|
-
def __init__(self, tree: Tree) -> None:
|
|
21
|
-
self.tree = tree
|
|
22
|
-
|
|
23
|
-
def get_count(self) -> int:
|
|
24
|
-
return len(self._paths)
|
|
25
|
-
|
|
26
|
-
def get_length(self) -> npt.NDArray[np.float32]:
|
|
27
|
-
"""Get length of paths."""
|
|
28
|
-
length = [path.length() for path in self._paths]
|
|
29
|
-
return np.array(length, dtype=np.float32)
|
|
30
|
-
|
|
31
|
-
def get_tortuosity(self) -> npt.NDArray[np.float32]:
|
|
32
|
-
"""Get tortuosity of path."""
|
|
33
|
-
return np.array([path.tortuosity() for path in self._paths], dtype=np.float32)
|
|
34
|
-
|
|
35
|
-
@cached_property
|
|
36
|
-
def _paths(self) -> List[Tree.Path]:
|
|
37
|
-
return self.tree.get_paths()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|