swcgeom 0.19.4__cp312-cp312-macosx_14_0_arm64.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/__init__.py +21 -0
- swcgeom/analysis/__init__.py +13 -0
- swcgeom/analysis/feature_extractor.py +454 -0
- swcgeom/analysis/features.py +218 -0
- swcgeom/analysis/lmeasure.py +750 -0
- swcgeom/analysis/sholl.py +201 -0
- swcgeom/analysis/trunk.py +183 -0
- swcgeom/analysis/visualization.py +191 -0
- swcgeom/analysis/visualization3d.py +81 -0
- swcgeom/analysis/volume.py +143 -0
- swcgeom/core/__init__.py +19 -0
- swcgeom/core/branch.py +129 -0
- swcgeom/core/branch_tree.py +65 -0
- swcgeom/core/compartment.py +107 -0
- swcgeom/core/node.py +130 -0
- swcgeom/core/path.py +155 -0
- swcgeom/core/population.py +341 -0
- swcgeom/core/swc.py +247 -0
- swcgeom/core/swc_utils/__init__.py +19 -0
- swcgeom/core/swc_utils/assembler.py +35 -0
- swcgeom/core/swc_utils/base.py +180 -0
- swcgeom/core/swc_utils/checker.py +107 -0
- swcgeom/core/swc_utils/io.py +204 -0
- swcgeom/core/swc_utils/normalizer.py +163 -0
- swcgeom/core/swc_utils/subtree.py +70 -0
- swcgeom/core/tree.py +384 -0
- swcgeom/core/tree_utils.py +277 -0
- swcgeom/core/tree_utils_impl.py +58 -0
- swcgeom/images/__init__.py +9 -0
- swcgeom/images/augmentation.py +149 -0
- swcgeom/images/contrast.py +87 -0
- swcgeom/images/folder.py +217 -0
- swcgeom/images/io.py +578 -0
- swcgeom/images/loaders/__init__.py +8 -0
- swcgeom/images/loaders/pbd.cpython-312-darwin.so +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cpython-312-darwin.so +0 -0
- swcgeom/images/loaders/raw.pyx +183 -0
- swcgeom/transforms/__init__.py +20 -0
- swcgeom/transforms/base.py +136 -0
- swcgeom/transforms/branch.py +223 -0
- swcgeom/transforms/branch_tree.py +74 -0
- swcgeom/transforms/geometry.py +270 -0
- swcgeom/transforms/image_preprocess.py +107 -0
- swcgeom/transforms/image_stack.py +219 -0
- swcgeom/transforms/images.py +206 -0
- swcgeom/transforms/mst.py +183 -0
- swcgeom/transforms/neurolucida_asc.py +498 -0
- swcgeom/transforms/path.py +56 -0
- swcgeom/transforms/population.py +36 -0
- swcgeom/transforms/tree.py +265 -0
- swcgeom/transforms/tree_assembler.py +161 -0
- swcgeom/utils/__init__.py +18 -0
- swcgeom/utils/debug.py +23 -0
- swcgeom/utils/download.py +119 -0
- swcgeom/utils/dsu.py +58 -0
- swcgeom/utils/ellipse.py +131 -0
- swcgeom/utils/file.py +90 -0
- swcgeom/utils/neuromorpho.py +581 -0
- swcgeom/utils/numpy_helper.py +70 -0
- swcgeom/utils/plotter_2d.py +134 -0
- swcgeom/utils/plotter_3d.py +35 -0
- swcgeom/utils/renderer.py +145 -0
- swcgeom/utils/sdf.py +324 -0
- swcgeom/utils/solid_geometry.py +154 -0
- swcgeom/utils/transforms.py +367 -0
- swcgeom/utils/volumetric_object.py +483 -0
- swcgeom-0.19.4.dist-info/METADATA +86 -0
- swcgeom-0.19.4.dist-info/RECORD +72 -0
- swcgeom-0.19.4.dist-info/WHEEL +5 -0
- swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
- swcgeom-0.19.4.dist-info/top_level.txt +1 -0
swcgeom/utils/ellipse.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Finds the Minimum Volume Enclosing Ellipsoid."""
|
|
6
|
+
|
|
7
|
+
# pylint: disable=invalid-name
|
|
8
|
+
|
|
9
|
+
from functools import cached_property
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import numpy.linalg as la
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
|
|
15
|
+
__all__ = ["mvee"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Ellipse:
|
|
19
|
+
"""Ellipse wrapper."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self, A: npt.NDArray[np.float64], centroid: npt.NDArray[np.float64]
|
|
23
|
+
) -> None:
|
|
24
|
+
self.A = A
|
|
25
|
+
self.centroid = centroid
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def radii(self) -> tuple[float, float]:
|
|
29
|
+
# x, y radii.
|
|
30
|
+
_U, D, _V = self.svd
|
|
31
|
+
rx, ry = 1.0 / np.sqrt(D)
|
|
32
|
+
return rx, ry
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def a(self) -> float:
|
|
36
|
+
a, _b = self.axes
|
|
37
|
+
return a
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def b(self) -> float:
|
|
41
|
+
_a, b = self.axes
|
|
42
|
+
return b
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def axes(self) -> tuple[float, float]:
|
|
46
|
+
# Major and minor semi-axis of the ellipse.
|
|
47
|
+
rx, ry = self.radii
|
|
48
|
+
dx, dy = 2 * rx, 2 * ry
|
|
49
|
+
a, b = max(dx, dy), min(dx, dy)
|
|
50
|
+
return a, b
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def eccentricity(self) -> float:
|
|
54
|
+
a, b = self.axes
|
|
55
|
+
e = np.sqrt(a**2 - b**2) / a
|
|
56
|
+
return e
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def alpha(self) -> float:
|
|
60
|
+
# Orientation angle (with respect to the x axis counterclockwise).
|
|
61
|
+
_U, _D, V = self.svd
|
|
62
|
+
arcsin = -1.0 * np.rad2deg(np.arcsin(V[0][0]))
|
|
63
|
+
arccos = np.rad2deg(np.arccos(V[0][1]))
|
|
64
|
+
alpha = arccos if arcsin > 0.0 else -1.0 * arccos
|
|
65
|
+
return alpha
|
|
66
|
+
|
|
67
|
+
@cached_property
|
|
68
|
+
def svd(self):
|
|
69
|
+
# V is the rotation matrix that gives the orientation of the ellipsoid.
|
|
70
|
+
# https://en.wikipedia.org/wiki/Rotation_matrix
|
|
71
|
+
# http://mathworld.wolfram.com/RotationMatrix.html
|
|
72
|
+
U, D, V = la.svd(self.A)
|
|
73
|
+
return U, D, V
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def mvee(points: npt.NDArray[np.floating], tol: float = 1e-3) -> Ellipse:
|
|
77
|
+
"""Finds the Minimum Volume Enclosing Ellipsoid.
|
|
78
|
+
|
|
79
|
+
>>> # Create a set of 2D points
|
|
80
|
+
>>> points = np.array([[0, 0], [1, 0], [0, 1], [1, 1]], dtype=np.float64)
|
|
81
|
+
>>> ellipse = mvee(points)
|
|
82
|
+
|
|
83
|
+
>>> # Check centroid is at center of points
|
|
84
|
+
>>> np.allclose(ellipse.centroid, [0.5, 0.5])
|
|
85
|
+
True
|
|
86
|
+
|
|
87
|
+
>>> # Check ellipse properties
|
|
88
|
+
>>> rx, ry = ellipse.radii
|
|
89
|
+
>>> np.allclose([rx, ry], [np.sqrt(2) / 2, np.sqrt(2) / 2], rtol=1e-5)
|
|
90
|
+
True
|
|
91
|
+
|
|
92
|
+
Reference:
|
|
93
|
+
1. http://stackoverflow.com/questions/14016898/port-matlab-bounding-ellipsoid-code-to-python
|
|
94
|
+
2. http://stackoverflow.com/questions/1768197/bounding-ellipse/1768440#1768440
|
|
95
|
+
3. https://minillinim.github.io/GroopM/dev_docs/groopm.ellipsoid-pysrc.html
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
points: Array of shape (N, d) where N is number of points and d is dimension
|
|
99
|
+
tol: Tolerance for convergence
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Ellipse: An Ellipse object containing the minimum volume enclosing ellipse
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
A, centroid = _mvee(points, tol=tol)
|
|
106
|
+
return Ellipse(A, centroid)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _mvee(
|
|
110
|
+
points: npt.NDArray[np.floating], tol: float = 1e-3
|
|
111
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
|
112
|
+
N, d = points.shape
|
|
113
|
+
Q = np.column_stack((points, np.ones(N))).T
|
|
114
|
+
err = tol + 1.0
|
|
115
|
+
u = np.ones(N) / N
|
|
116
|
+
while err > tol:
|
|
117
|
+
# assert u.sum() == 1 # invariant
|
|
118
|
+
X = np.dot(np.dot(Q, np.diag(u)), Q.T)
|
|
119
|
+
M = np.diag(np.dot(np.dot(Q.T, la.inv(X)), Q))
|
|
120
|
+
jdx = np.argmax(M)
|
|
121
|
+
step_size = (M[jdx] - d - 1.0) / ((d + 1) * (M[jdx] - 1.0))
|
|
122
|
+
new_u = (1 - step_size) * u
|
|
123
|
+
new_u[jdx] += step_size
|
|
124
|
+
err = la.norm(new_u - u)
|
|
125
|
+
u = new_u
|
|
126
|
+
c = np.dot(u, points)
|
|
127
|
+
A = (
|
|
128
|
+
la.inv(np.dot(np.dot(points.T, np.diag(u)), points) - np.multiply.outer(c, c))
|
|
129
|
+
/ d
|
|
130
|
+
)
|
|
131
|
+
return A, c
|
swcgeom/utils/file.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""File related utils.
|
|
6
|
+
|
|
7
|
+
NOTE: If character coding is enabled, all denpendencies need to be installed, try:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install swcgeom[all]
|
|
11
|
+
```
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import warnings
|
|
15
|
+
from io import BytesIO, TextIOBase, TextIOWrapper
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
__all__ = ["FileReader", "PathOrIO"]
|
|
19
|
+
|
|
20
|
+
PathOrIO = int | str | bytes | BytesIO | TextIOBase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FileReader:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
fname: PathOrIO,
|
|
27
|
+
*,
|
|
28
|
+
encoding: Literal["detect"] | str = "utf-8",
|
|
29
|
+
low_confidence: float = 0.9,
|
|
30
|
+
**kwargs,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Read file.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
fname: PathOrIO
|
|
36
|
+
encoding: The name of the encoding used to decode the file.
|
|
37
|
+
If is `detect`, we will try to detect the character encoding.
|
|
38
|
+
low_confidence: The confidence threshold for character encoding detection.
|
|
39
|
+
Used for detect character endocing, raising warning when parsing with
|
|
40
|
+
low confidence.
|
|
41
|
+
"""
|
|
42
|
+
# TODO: support StringIO
|
|
43
|
+
self.fname, self.fb, self.f = "", None, None
|
|
44
|
+
if isinstance(fname, TextIOBase):
|
|
45
|
+
self.f = fname
|
|
46
|
+
encoding = fname.encoding # skip detect
|
|
47
|
+
elif isinstance(fname, BytesIO):
|
|
48
|
+
self.fb = fname
|
|
49
|
+
else:
|
|
50
|
+
self.fname = fname
|
|
51
|
+
|
|
52
|
+
if encoding == "detect":
|
|
53
|
+
encoding = detect_encoding(fname, low_confidence=low_confidence)
|
|
54
|
+
self.encoding = encoding
|
|
55
|
+
self.kwargs = kwargs
|
|
56
|
+
|
|
57
|
+
def __enter__(self) -> TextIOBase:
|
|
58
|
+
if isinstance(self.fb, BytesIO):
|
|
59
|
+
self.f = TextIOWrapper(self.fb, encoding=self.encoding)
|
|
60
|
+
elif self.f is None:
|
|
61
|
+
self.f = open(self.fname, "r", encoding=self.encoding, **self.kwargs)
|
|
62
|
+
|
|
63
|
+
return self.f
|
|
64
|
+
|
|
65
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
66
|
+
if self.f:
|
|
67
|
+
self.f.close()
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def detect_encoding(fname: PathOrIO, *, low_confidence: float = 0.9) -> str:
|
|
72
|
+
import chardet
|
|
73
|
+
|
|
74
|
+
if isinstance(fname, TextIOBase):
|
|
75
|
+
return fname.encoding
|
|
76
|
+
elif isinstance(fname, BytesIO):
|
|
77
|
+
data = fname.read()
|
|
78
|
+
fname.seek(0, 0)
|
|
79
|
+
else:
|
|
80
|
+
with open(fname, "rb") as f:
|
|
81
|
+
data = f.read()
|
|
82
|
+
|
|
83
|
+
result = chardet.detect(data)
|
|
84
|
+
encoding = result["encoding"] or "utf-8"
|
|
85
|
+
if result["confidence"] < low_confidence:
|
|
86
|
+
warnings.warn(
|
|
87
|
+
f"parse as `{encoding}` with low confidence "
|
|
88
|
+
f"{result['confidence']} in `{fname}`"
|
|
89
|
+
)
|
|
90
|
+
return encoding
|