swcgeom 0.15.0__py3-none-any.whl → 0.18.3__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/__init__.py +26 -1
- swcgeom/analysis/__init__.py +21 -8
- swcgeom/analysis/feature_extractor.py +43 -18
- swcgeom/analysis/features.py +250 -0
- swcgeom/analysis/lmeasure.py +857 -0
- swcgeom/analysis/sholl.py +55 -29
- swcgeom/analysis/trunk.py +27 -11
- swcgeom/analysis/visualization.py +24 -9
- swcgeom/analysis/visualization3d.py +100 -0
- swcgeom/analysis/volume.py +19 -4
- swcgeom/core/__init__.py +32 -9
- swcgeom/core/branch.py +28 -7
- swcgeom/core/branch_tree.py +18 -4
- swcgeom/core/{segment.py → compartment.py} +31 -10
- swcgeom/core/node.py +31 -10
- swcgeom/core/path.py +37 -10
- swcgeom/core/population.py +103 -34
- swcgeom/core/swc.py +26 -10
- swcgeom/core/swc_utils/__init__.py +21 -7
- swcgeom/core/swc_utils/assembler.py +27 -1
- swcgeom/core/swc_utils/base.py +25 -12
- swcgeom/core/swc_utils/checker.py +31 -14
- swcgeom/core/swc_utils/io.py +24 -7
- swcgeom/core/swc_utils/normalizer.py +20 -4
- swcgeom/core/swc_utils/subtree.py +17 -2
- swcgeom/core/tree.py +85 -72
- swcgeom/core/tree_utils.py +31 -16
- swcgeom/core/tree_utils_impl.py +18 -3
- swcgeom/images/__init__.py +17 -2
- swcgeom/images/augmentation.py +24 -4
- swcgeom/images/contrast.py +122 -0
- swcgeom/images/folder.py +97 -39
- swcgeom/images/io.py +108 -121
- swcgeom/transforms/__init__.py +28 -10
- swcgeom/transforms/base.py +17 -2
- swcgeom/transforms/branch.py +74 -8
- swcgeom/transforms/branch_tree.py +82 -0
- swcgeom/transforms/geometry.py +22 -7
- swcgeom/transforms/image_preprocess.py +115 -0
- swcgeom/transforms/image_stack.py +37 -13
- swcgeom/transforms/images.py +184 -7
- swcgeom/transforms/mst.py +20 -5
- swcgeom/transforms/neurolucida_asc.py +508 -0
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +16 -3
- swcgeom/transforms/tree.py +89 -31
- swcgeom/transforms/tree_assembler.py +23 -7
- swcgeom/utils/__init__.py +27 -11
- swcgeom/utils/debug.py +15 -0
- swcgeom/utils/download.py +59 -21
- swcgeom/utils/dsu.py +15 -0
- swcgeom/utils/ellipse.py +18 -4
- swcgeom/utils/file.py +15 -0
- swcgeom/utils/neuromorpho.py +439 -302
- swcgeom/utils/numpy_helper.py +29 -4
- swcgeom/utils/plotter_2d.py +151 -0
- swcgeom/utils/plotter_3d.py +48 -0
- swcgeom/utils/renderer.py +49 -145
- swcgeom/utils/sdf.py +24 -8
- swcgeom/utils/solid_geometry.py +16 -3
- swcgeom/utils/transforms.py +17 -4
- swcgeom/utils/volumetric_object.py +23 -10
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
- swcgeom-0.18.3.dist-info/RECORD +67 -0
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
- swcgeom/_version.py +0 -16
- swcgeom/analysis/branch_features.py +0 -67
- swcgeom/analysis/node_features.py +0 -121
- swcgeom/analysis/path_features.py +0 -37
- swcgeom-0.15.0.dist-info/RECORD +0 -62
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
swcgeom/utils/numpy_helper.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Numpy related utils."""
|
|
2
17
|
|
|
3
18
|
from contextlib import contextmanager
|
|
@@ -11,7 +26,7 @@ __all__ = ["padding1d", "numpy_err"]
|
|
|
11
26
|
|
|
12
27
|
def padding1d(
|
|
13
28
|
n: int,
|
|
14
|
-
v: npt.
|
|
29
|
+
v: npt.ArrayLike | None,
|
|
15
30
|
padding_value: Any = 0,
|
|
16
31
|
dtype: npt.DTypeLike | None = None,
|
|
17
32
|
) -> npt.NDArray:
|
|
@@ -31,9 +46,19 @@ def padding1d(
|
|
|
31
46
|
x will used, otherwise defaults to `~numpy.float32`.
|
|
32
47
|
"""
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
if not isinstance(v, np.ndarray):
|
|
50
|
+
dtype = dtype or np.float32
|
|
51
|
+
if v is not None:
|
|
52
|
+
v = np.array(v, dtype=dtype)
|
|
53
|
+
else:
|
|
54
|
+
v = np.zeros(n, dtype=dtype)
|
|
55
|
+
|
|
56
|
+
if dtype is None:
|
|
57
|
+
dtype = v.dtype
|
|
58
|
+
|
|
59
|
+
if v.dtype != dtype:
|
|
60
|
+
v = v.astype(dtype)
|
|
61
|
+
|
|
37
62
|
assert v.ndim == 1
|
|
38
63
|
|
|
39
64
|
if v.shape[0] >= n:
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""2D Plotting utils."""
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import numpy as np
|
|
22
|
+
import numpy.typing as npt
|
|
23
|
+
from matplotlib import cm
|
|
24
|
+
from matplotlib.axes import Axes
|
|
25
|
+
from matplotlib.collections import LineCollection, PatchCollection
|
|
26
|
+
from matplotlib.colors import Colormap, Normalize
|
|
27
|
+
from matplotlib.figure import Figure
|
|
28
|
+
from matplotlib.patches import Circle
|
|
29
|
+
|
|
30
|
+
from swcgeom.utils.renderer import Camera
|
|
31
|
+
from swcgeom.utils.transforms import to_homogeneous, translate3d
|
|
32
|
+
|
|
33
|
+
__all__ = ["draw_lines", "draw_direction_indicator", "draw_circles", "get_fig_ax"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def draw_lines(
|
|
37
|
+
ax: Axes,
|
|
38
|
+
lines: npt.NDArray[np.floating],
|
|
39
|
+
camera: Camera,
|
|
40
|
+
joinstyle="round",
|
|
41
|
+
capstyle="round",
|
|
42
|
+
**kwargs,
|
|
43
|
+
) -> LineCollection:
|
|
44
|
+
"""Draw lines.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
ax : ~matplotlib.axes.Axes
|
|
49
|
+
lines : A collection of coords of lines
|
|
50
|
+
Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
|
|
51
|
+
and the axis-3 holds the coordinates (x, y, z).
|
|
52
|
+
camera : Camera
|
|
53
|
+
Camera position.
|
|
54
|
+
**kwargs : dict[str, Unknown]
|
|
55
|
+
Forwarded to `~matplotlib.collections.LineCollection`.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
T = camera.MVP
|
|
59
|
+
T = translate3d(*camera.position).dot(T) # keep origin
|
|
60
|
+
|
|
61
|
+
starts, ends = lines[:, 0], lines[:, 1]
|
|
62
|
+
starts, ends = to_homogeneous(starts, 1), to_homogeneous(ends, 1)
|
|
63
|
+
starts, ends = np.dot(T, starts.T).T[:, 0:2], np.dot(T, ends.T).T[:, 0:2]
|
|
64
|
+
|
|
65
|
+
edges = np.stack([starts, ends], axis=1)
|
|
66
|
+
collection = LineCollection(edges, joinstyle=joinstyle, capstyle=capstyle, **kwargs) # type: ignore
|
|
67
|
+
return ax.add_collection(collection) # type: ignore
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def draw_direction_indicator(
|
|
71
|
+
ax: Axes, camera: Camera, loc: tuple[float, float]
|
|
72
|
+
) -> None:
|
|
73
|
+
x, y = loc
|
|
74
|
+
direction = camera.MV.dot(
|
|
75
|
+
[
|
|
76
|
+
[1, 0, 0, 1],
|
|
77
|
+
[0, 1, 0, 1],
|
|
78
|
+
[0, 0, 1, 1],
|
|
79
|
+
[0, 0, 0, 1],
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
arrow_length, text_offset = 0.05, 0.05 # TODO: may still overlap
|
|
84
|
+
text_colors = [["x", "red"], ["y", "green"], ["z", "blue"]]
|
|
85
|
+
for (dx, dy, dz, _), (text, color) in zip(direction, text_colors):
|
|
86
|
+
if 1 - abs(dz) < 1e-5:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
ax.arrow(
|
|
90
|
+
x,
|
|
91
|
+
y,
|
|
92
|
+
arrow_length * dx,
|
|
93
|
+
arrow_length * dy,
|
|
94
|
+
head_length=0.02,
|
|
95
|
+
head_width=0.01,
|
|
96
|
+
color=color,
|
|
97
|
+
transform=ax.transAxes,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ax.text(
|
|
101
|
+
x + (arrow_length + text_offset) * dx,
|
|
102
|
+
y + (arrow_length + text_offset) * dy,
|
|
103
|
+
text,
|
|
104
|
+
color=color,
|
|
105
|
+
transform=ax.transAxes,
|
|
106
|
+
horizontalalignment="center",
|
|
107
|
+
verticalalignment="center",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def draw_circles(
|
|
112
|
+
ax: Axes,
|
|
113
|
+
x: npt.NDArray,
|
|
114
|
+
y: npt.NDArray,
|
|
115
|
+
*,
|
|
116
|
+
y_min: Optional[float] = None,
|
|
117
|
+
y_max: Optional[float] = None,
|
|
118
|
+
cmap: str | Colormap = "viridis",
|
|
119
|
+
) -> PatchCollection:
|
|
120
|
+
"""Draw a sequential of circles."""
|
|
121
|
+
|
|
122
|
+
y_min = y.min() if y_min is None else y_min
|
|
123
|
+
y_max = y.max() if y_max is None else y_max
|
|
124
|
+
norm = Normalize(y_min, y_max)
|
|
125
|
+
|
|
126
|
+
color_map = cmap if isinstance(cmap, Colormap) else cm.get_cmap(name=cmap)
|
|
127
|
+
colors = color_map(norm(y))
|
|
128
|
+
|
|
129
|
+
circles = [
|
|
130
|
+
Circle((0, 0), xi, color=color) for xi, color in reversed(list(zip(x, colors)))
|
|
131
|
+
]
|
|
132
|
+
patches = PatchCollection(circles, match_original=True)
|
|
133
|
+
patches.set_cmap(color_map)
|
|
134
|
+
patches.set_norm(norm)
|
|
135
|
+
patches: PatchCollection = ax.add_collection(patches) # type: ignore
|
|
136
|
+
|
|
137
|
+
ax.set_aspect(1)
|
|
138
|
+
ax.autoscale()
|
|
139
|
+
return patches
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_fig_ax(
|
|
143
|
+
fig: Optional[Figure] = None, ax: Optional[Axes] = None
|
|
144
|
+
) -> tuple[Figure, Axes]:
|
|
145
|
+
if fig is None and ax is not None:
|
|
146
|
+
fig = ax.get_figure()
|
|
147
|
+
assert fig is not None, "expecting a figure from the axes"
|
|
148
|
+
|
|
149
|
+
fig = fig or plt.gcf()
|
|
150
|
+
ax = ax or plt.gca()
|
|
151
|
+
return fig, ax
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""3D Plotting utils."""
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import numpy.typing as npt
|
|
20
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
21
|
+
from mpl_toolkits.mplot3d.art3d import Line3DCollection
|
|
22
|
+
|
|
23
|
+
__all__ = ["draw_lines_3d"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def draw_lines_3d(
|
|
27
|
+
ax: Axes3D,
|
|
28
|
+
lines: npt.NDArray[np.floating],
|
|
29
|
+
joinstyle="round",
|
|
30
|
+
capstyle="round",
|
|
31
|
+
**kwargs,
|
|
32
|
+
):
|
|
33
|
+
"""Draw lines.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
ax : ~matplotlib.axes.Axes
|
|
38
|
+
lines : A collection of coords of lines
|
|
39
|
+
Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
|
|
40
|
+
and the axis-3 holds the coordinates (x, y, z).
|
|
41
|
+
**kwargs : dict[str, Unknown]
|
|
42
|
+
Forwarded to `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
line_collection = Line3DCollection(
|
|
46
|
+
lines, joinstyle=joinstyle, capstyle=capstyle, **kwargs
|
|
47
|
+
) # type: ignore
|
|
48
|
+
return ax.add_collection3d(line_collection)
|
swcgeom/utils/renderer.py
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Rendering related utils."""
|
|
2
17
|
|
|
3
18
|
from functools import cached_property
|
|
4
|
-
from typing import
|
|
19
|
+
from typing import Literal, cast
|
|
5
20
|
|
|
6
|
-
import matplotlib.pyplot as plt
|
|
7
21
|
import numpy as np
|
|
8
22
|
import numpy.typing as npt
|
|
9
|
-
from matplotlib import cm
|
|
10
|
-
from matplotlib.axes import Axes
|
|
11
|
-
from matplotlib.collections import LineCollection, PatchCollection
|
|
12
|
-
from matplotlib.colors import Colormap, Normalize
|
|
13
|
-
from matplotlib.figure import Figure
|
|
14
|
-
from matplotlib.patches import Circle
|
|
15
23
|
from typing_extensions import Self
|
|
16
24
|
|
|
17
25
|
from swcgeom.utils.transforms import (
|
|
18
26
|
Vec3f,
|
|
19
27
|
model_view_transformation,
|
|
20
28
|
orthographic_projection_simple,
|
|
21
|
-
to_homogeneous,
|
|
22
|
-
translate3d,
|
|
23
29
|
)
|
|
24
30
|
|
|
25
|
-
__all__ = [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"palette",
|
|
29
|
-
"draw_lines",
|
|
30
|
-
"draw_direction_indicator",
|
|
31
|
-
"draw_circles",
|
|
32
|
-
"get_fig_ax",
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
CameraOption = Vec3f | Tuple[Vec3f, Vec3f] | Tuple[Vec3f, Vec3f, Vec3f]
|
|
31
|
+
__all__ = ["CameraOptions", "Camera", "SimpleCamera", "palette"]
|
|
32
|
+
|
|
33
|
+
CameraOption = Vec3f | tuple[Vec3f, Vec3f] | tuple[Vec3f, Vec3f, Vec3f]
|
|
36
34
|
CameraPreset = Literal["xy", "yz", "zx", "yx", "zy", "xz"]
|
|
37
|
-
CameraPresets:
|
|
35
|
+
CameraPresets: dict[CameraPreset, tuple[Vec3f, Vec3f, Vec3f]] = {
|
|
38
36
|
"xy": ((0.0, 0.0, 0.0), (+0.0, +0.0, -1.0), (+0.0, +1.0, +0.0)),
|
|
39
37
|
"yz": ((0.0, 0.0, 0.0), (-1.0, +0.0, +0.0), (+0.0, +0.0, +1.0)),
|
|
40
38
|
"zx": ((0.0, 0.0, 0.0), (+0.0, -1.0, +0.0), (+1.0, +0.0, +0.0)),
|
|
@@ -45,13 +43,35 @@ CameraPresets: Dict[CameraPreset, Tuple[Vec3f, Vec3f, Vec3f]] = {
|
|
|
45
43
|
CameraOptions = CameraOption | CameraPreset
|
|
46
44
|
|
|
47
45
|
|
|
48
|
-
class
|
|
46
|
+
class Camera:
|
|
47
|
+
_position: Vec3f
|
|
48
|
+
_look_at: Vec3f
|
|
49
|
+
_up: Vec3f
|
|
50
|
+
|
|
51
|
+
# fmt: off
|
|
52
|
+
@property
|
|
53
|
+
def position(self) -> Vec3f: return self._position
|
|
54
|
+
@property
|
|
55
|
+
def look_at(self) -> Vec3f: return self._look_at
|
|
56
|
+
@property
|
|
57
|
+
def up(self) -> Vec3f: return self._up
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def MV(self) -> npt.NDArray[np.float32]: raise NotImplementedError()
|
|
61
|
+
@property
|
|
62
|
+
def P(self) -> npt.NDArray[np.float32]: raise NotImplementedError()
|
|
63
|
+
@property
|
|
64
|
+
def MVP(self) -> npt.NDArray[np.float32]: return self.P.dot(self.MV)
|
|
65
|
+
# fmt: on
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SimpleCamera(Camera):
|
|
49
69
|
"""Simplest camera."""
|
|
50
70
|
|
|
51
71
|
def __init__(self, position: Vec3f, look_at: Vec3f, up: Vec3f):
|
|
52
|
-
self.
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
72
|
+
self._position = position
|
|
73
|
+
self._look_at = look_at
|
|
74
|
+
self._up = up
|
|
55
75
|
|
|
56
76
|
@cached_property
|
|
57
77
|
def MV(self) -> npt.NDArray[np.float32]: # pylint: disable=invalid-name
|
|
@@ -61,10 +81,6 @@ class SimpleCamera:
|
|
|
61
81
|
def P(self) -> npt.NDArray[np.float32]: # pylint: disable=invalid-name
|
|
62
82
|
return orthographic_projection_simple()
|
|
63
83
|
|
|
64
|
-
@cached_property
|
|
65
|
-
def MVP(self) -> npt.NDArray[np.float32]: # pylint: disable=invalid-name
|
|
66
|
-
return self.P.dot(self.MV)
|
|
67
|
-
|
|
68
84
|
@classmethod
|
|
69
85
|
def from_options(cls, camera: CameraOptions) -> Self:
|
|
70
86
|
if isinstance(camera, str):
|
|
@@ -76,7 +92,7 @@ class SimpleCamera:
|
|
|
76
92
|
if isinstance(camera[0], tuple):
|
|
77
93
|
return cls((0, 0, 0), cast(Vec3f, camera), (0, 1, 0))
|
|
78
94
|
|
|
79
|
-
return cls(*cast(
|
|
95
|
+
return cls(*cast(tuple[Vec3f, Vec3f, Vec3f], camera))
|
|
80
96
|
|
|
81
97
|
|
|
82
98
|
class Palette:
|
|
@@ -84,8 +100,8 @@ class Palette:
|
|
|
84
100
|
|
|
85
101
|
# pylint: disable=too-few-public-methods
|
|
86
102
|
|
|
87
|
-
default:
|
|
88
|
-
vaa3d:
|
|
103
|
+
default: dict[int, str]
|
|
104
|
+
vaa3d: dict[int, str]
|
|
89
105
|
|
|
90
106
|
def __init__(self):
|
|
91
107
|
default = [
|
|
@@ -130,115 +146,3 @@ class Palette:
|
|
|
130
146
|
|
|
131
147
|
|
|
132
148
|
palette = Palette()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def draw_lines(
|
|
136
|
-
ax: Axes, lines: npt.NDArray[np.floating], camera: SimpleCamera, **kwargs
|
|
137
|
-
) -> LineCollection:
|
|
138
|
-
"""Draw lines.
|
|
139
|
-
|
|
140
|
-
Parameters
|
|
141
|
-
----------
|
|
142
|
-
ax : ~matplotlib.axes.Axes
|
|
143
|
-
lines : A collection of coords of lines
|
|
144
|
-
Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
|
|
145
|
-
and the axis-3 holds the coordinates (x, y, z).
|
|
146
|
-
camera : Camera
|
|
147
|
-
Camera position.
|
|
148
|
-
**kwargs : dict[str, Unknown]
|
|
149
|
-
Forwarded to `~matplotlib.collections.LineCollection`.
|
|
150
|
-
"""
|
|
151
|
-
|
|
152
|
-
T = camera.MVP
|
|
153
|
-
T = translate3d(*camera.position).dot(T) # keep origin
|
|
154
|
-
|
|
155
|
-
starts, ends = lines[:, 0], lines[:, 1]
|
|
156
|
-
starts, ends = to_homogeneous(starts, 1), to_homogeneous(ends, 1)
|
|
157
|
-
starts, ends = np.dot(T, starts.T).T[:, 0:2], np.dot(T, ends.T).T[:, 0:2]
|
|
158
|
-
|
|
159
|
-
edges = np.stack([starts, ends], axis=1)
|
|
160
|
-
return ax.add_collection(LineCollection(edges, **kwargs)) # type: ignore
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def draw_direction_indicator(
|
|
164
|
-
ax: Axes, camera: SimpleCamera, loc: Tuple[float, float]
|
|
165
|
-
) -> None:
|
|
166
|
-
x, y = loc
|
|
167
|
-
direction = camera.MV.dot(
|
|
168
|
-
[
|
|
169
|
-
[1, 0, 0, 1],
|
|
170
|
-
[0, 1, 0, 1],
|
|
171
|
-
[0, 0, 1, 1],
|
|
172
|
-
[0, 0, 0, 1],
|
|
173
|
-
]
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
arrow_length, text_offset = 0.05, 0.05 # TODO: may still overlap
|
|
177
|
-
text_colors = [["x", "red"], ["y", "green"], ["z", "blue"]]
|
|
178
|
-
for (dx, dy, dz, _), (text, color) in zip(direction, text_colors):
|
|
179
|
-
if 1 - abs(dz) < 1e-5:
|
|
180
|
-
continue
|
|
181
|
-
|
|
182
|
-
ax.arrow(
|
|
183
|
-
x,
|
|
184
|
-
y,
|
|
185
|
-
arrow_length * dx,
|
|
186
|
-
arrow_length * dy,
|
|
187
|
-
head_length=0.02,
|
|
188
|
-
head_width=0.01,
|
|
189
|
-
color=color,
|
|
190
|
-
transform=ax.transAxes,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
ax.text(
|
|
194
|
-
x + (arrow_length + text_offset) * dx,
|
|
195
|
-
y + (arrow_length + text_offset) * dy,
|
|
196
|
-
text,
|
|
197
|
-
color=color,
|
|
198
|
-
transform=ax.transAxes,
|
|
199
|
-
horizontalalignment="center",
|
|
200
|
-
verticalalignment="center",
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def draw_circles(
|
|
205
|
-
ax: Axes,
|
|
206
|
-
x: npt.NDArray,
|
|
207
|
-
y: npt.NDArray,
|
|
208
|
-
*,
|
|
209
|
-
y_min: Optional[float] = None,
|
|
210
|
-
y_max: Optional[float] = None,
|
|
211
|
-
cmap: str | Colormap = "viridis",
|
|
212
|
-
) -> PatchCollection:
|
|
213
|
-
"""Draw a sequential of circles."""
|
|
214
|
-
|
|
215
|
-
y_min = y.min() if y_min is None else y_min
|
|
216
|
-
y_max = y.max() if y_max is None else y_max
|
|
217
|
-
norm = Normalize(y_min, y_max)
|
|
218
|
-
|
|
219
|
-
color_map = cmap if isinstance(cmap, Colormap) else cm.get_cmap(name=cmap)
|
|
220
|
-
colors = color_map(norm(y))
|
|
221
|
-
|
|
222
|
-
circles = [
|
|
223
|
-
Circle((0, 0), xi, color=color) for xi, color in reversed(list(zip(x, colors)))
|
|
224
|
-
]
|
|
225
|
-
patches = PatchCollection(circles, match_original=True)
|
|
226
|
-
patches.set_cmap(color_map)
|
|
227
|
-
patches.set_norm(norm)
|
|
228
|
-
patches: PatchCollection = ax.add_collection(patches) # type: ignore
|
|
229
|
-
|
|
230
|
-
ax.set_aspect(1)
|
|
231
|
-
ax.autoscale()
|
|
232
|
-
return patches
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def get_fig_ax(
|
|
236
|
-
fig: Figure | None = None, ax: Axes | None = None
|
|
237
|
-
) -> Tuple[Figure, Axes]:
|
|
238
|
-
if fig is None:
|
|
239
|
-
fig = plt.gcf() if ax is None else ax.get_figure()
|
|
240
|
-
|
|
241
|
-
if ax is None:
|
|
242
|
-
ax = fig.gca()
|
|
243
|
-
|
|
244
|
-
return fig, ax
|
swcgeom/utils/sdf.py
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Signed distance functions.
|
|
2
17
|
|
|
3
18
|
Refs: https://iquilezles.org/articles/distfunctions/
|
|
@@ -10,10 +25,11 @@ the future, use `sdflit` instead.
|
|
|
10
25
|
|
|
11
26
|
import warnings
|
|
12
27
|
from abc import ABC, abstractmethod
|
|
13
|
-
from
|
|
28
|
+
from collections.abc import Iterable
|
|
14
29
|
|
|
15
30
|
import numpy as np
|
|
16
31
|
import numpy.typing as npt
|
|
32
|
+
from typing_extensions import deprecated
|
|
17
33
|
|
|
18
34
|
from swcgeom.utils.solid_geometry import project_vector_on_plane
|
|
19
35
|
|
|
@@ -29,7 +45,7 @@ __all__ = [
|
|
|
29
45
|
]
|
|
30
46
|
|
|
31
47
|
# Axis-aligned bounding box, tuple of array of shape (3,)
|
|
32
|
-
AABB =
|
|
48
|
+
AABB = tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]
|
|
33
49
|
|
|
34
50
|
|
|
35
51
|
class SDF(ABC):
|
|
@@ -173,15 +189,15 @@ class SDFDifference(SDF):
|
|
|
173
189
|
return flags
|
|
174
190
|
|
|
175
191
|
|
|
192
|
+
@deprecated("Use `SDFUnion` instead")
|
|
176
193
|
class SDFCompose(SDFUnion):
|
|
177
|
-
"""Compose multiple SDFs.
|
|
194
|
+
"""Compose multiple SDFs.
|
|
195
|
+
|
|
196
|
+
.. deprecated:: 0.14.0
|
|
197
|
+
Use :cls:`SDFUnion` instead.
|
|
198
|
+
"""
|
|
178
199
|
|
|
179
200
|
def __init__(self, sdfs: Iterable[SDF]) -> None:
|
|
180
|
-
warnings.warn(
|
|
181
|
-
"`SDFCompose` has been replace by `SDFUnion` since v0.14.0, "
|
|
182
|
-
"and will be removed in next version",
|
|
183
|
-
DeprecationWarning,
|
|
184
|
-
)
|
|
185
201
|
sdfs = list(sdfs)
|
|
186
202
|
if len(sdfs) == 1:
|
|
187
203
|
warnings.warn("compose only one SDF, use SDFCompose.compose instead")
|
swcgeom/utils/solid_geometry.py
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
"""Solid Geometry."""
|
|
4
17
|
|
|
5
18
|
import numpy as np
|
|
6
19
|
import numpy.typing as npt
|
|
@@ -31,7 +44,7 @@ def find_sphere_line_intersection(
|
|
|
31
44
|
sphere_radius: float,
|
|
32
45
|
line_point_a: npt.NDArray,
|
|
33
46
|
line_point_b: npt.NDArray,
|
|
34
|
-
) ->
|
|
47
|
+
) -> list[tuple[float, npt.NDArray[np.float64]]]:
|
|
35
48
|
A = np.array(line_point_a)
|
|
36
49
|
B = np.array(line_point_b)
|
|
37
50
|
C = np.array(sphere_center)
|
swcgeom/utils/transforms.py
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
"""3D geometry transformations."""
|
|
4
17
|
|
|
5
18
|
import numpy as np
|
|
6
19
|
import numpy.typing as npt
|
|
@@ -19,11 +32,11 @@ __all__ = [
|
|
|
19
32
|
"orthographic_projection_simple",
|
|
20
33
|
]
|
|
21
34
|
|
|
22
|
-
Vec3f =
|
|
35
|
+
Vec3f = tuple[float, float, float]
|
|
23
36
|
|
|
24
37
|
|
|
25
38
|
def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float:
|
|
26
|
-
"""Get the
|
|
39
|
+
"""Get the angle of vectors.
|
|
27
40
|
|
|
28
41
|
Returns
|
|
29
42
|
-------
|