swcgeom 0.18.3__py3-none-any.whl → 0.19.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.

Files changed (62) hide show
  1. swcgeom/analysis/feature_extractor.py +22 -24
  2. swcgeom/analysis/features.py +18 -40
  3. swcgeom/analysis/lmeasure.py +227 -323
  4. swcgeom/analysis/sholl.py +17 -23
  5. swcgeom/analysis/trunk.py +23 -28
  6. swcgeom/analysis/visualization.py +37 -44
  7. swcgeom/analysis/visualization3d.py +16 -25
  8. swcgeom/analysis/volume.py +33 -47
  9. swcgeom/core/__init__.py +1 -6
  10. swcgeom/core/branch.py +10 -17
  11. swcgeom/core/branch_tree.py +3 -2
  12. swcgeom/core/compartment.py +1 -1
  13. swcgeom/core/node.py +3 -6
  14. swcgeom/core/path.py +11 -16
  15. swcgeom/core/population.py +32 -51
  16. swcgeom/core/swc.py +25 -16
  17. swcgeom/core/swc_utils/__init__.py +4 -6
  18. swcgeom/core/swc_utils/assembler.py +5 -12
  19. swcgeom/core/swc_utils/base.py +40 -31
  20. swcgeom/core/swc_utils/checker.py +3 -8
  21. swcgeom/core/swc_utils/io.py +32 -47
  22. swcgeom/core/swc_utils/normalizer.py +17 -23
  23. swcgeom/core/swc_utils/subtree.py +13 -20
  24. swcgeom/core/tree.py +61 -51
  25. swcgeom/core/tree_utils.py +36 -49
  26. swcgeom/core/tree_utils_impl.py +4 -6
  27. swcgeom/images/augmentation.py +23 -39
  28. swcgeom/images/contrast.py +22 -46
  29. swcgeom/images/folder.py +32 -34
  30. swcgeom/images/io.py +80 -121
  31. swcgeom/transforms/base.py +28 -19
  32. swcgeom/transforms/branch.py +31 -41
  33. swcgeom/transforms/branch_tree.py +3 -1
  34. swcgeom/transforms/geometry.py +13 -4
  35. swcgeom/transforms/image_preprocess.py +2 -0
  36. swcgeom/transforms/image_stack.py +40 -35
  37. swcgeom/transforms/images.py +31 -24
  38. swcgeom/transforms/mst.py +27 -40
  39. swcgeom/transforms/neurolucida_asc.py +13 -13
  40. swcgeom/transforms/path.py +4 -0
  41. swcgeom/transforms/population.py +4 -0
  42. swcgeom/transforms/tree.py +16 -11
  43. swcgeom/transforms/tree_assembler.py +37 -54
  44. swcgeom/utils/download.py +7 -14
  45. swcgeom/utils/dsu.py +12 -0
  46. swcgeom/utils/ellipse.py +26 -14
  47. swcgeom/utils/file.py +8 -13
  48. swcgeom/utils/neuromorpho.py +78 -92
  49. swcgeom/utils/numpy_helper.py +15 -12
  50. swcgeom/utils/plotter_2d.py +10 -16
  51. swcgeom/utils/plotter_3d.py +7 -9
  52. swcgeom/utils/renderer.py +16 -8
  53. swcgeom/utils/sdf.py +12 -23
  54. swcgeom/utils/solid_geometry.py +58 -2
  55. swcgeom/utils/transforms.py +164 -100
  56. swcgeom/utils/volumetric_object.py +29 -53
  57. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/METADATA +5 -4
  58. swcgeom-0.19.0.dist-info/RECORD +67 -0
  59. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/WHEEL +1 -1
  60. swcgeom-0.18.3.dist-info/RECORD +0 -67
  61. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info/licenses}/LICENSE +0 -0
  62. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/top_level.txt +0 -0
@@ -15,8 +15,6 @@
15
15
 
16
16
  """2D Plotting utils."""
17
17
 
18
- from typing import Optional
19
-
20
18
  import matplotlib.pyplot as plt
21
19
  import numpy as np
22
20
  import numpy.typing as npt
@@ -43,18 +41,14 @@ def draw_lines(
43
41
  ) -> LineCollection:
44
42
  """Draw lines.
45
43
 
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`.
44
+ Args:
45
+ ax: The plot axes.
46
+ lines: A collection of coords of lines
47
+ Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
48
+ and the axis-3 holds the coordinates (x, y, z).
49
+ camera: Camera position.
50
+ **kwargs: Forwarded to `~matplotlib.collections.LineCollection`.
56
51
  """
57
-
58
52
  T = camera.MVP
59
53
  T = translate3d(*camera.position).dot(T) # keep origin
60
54
 
@@ -113,8 +107,8 @@ def draw_circles(
113
107
  x: npt.NDArray,
114
108
  y: npt.NDArray,
115
109
  *,
116
- y_min: Optional[float] = None,
117
- y_max: Optional[float] = None,
110
+ y_min: float | None = None,
111
+ y_max: float | None = None,
118
112
  cmap: str | Colormap = "viridis",
119
113
  ) -> PatchCollection:
120
114
  """Draw a sequential of circles."""
@@ -140,7 +134,7 @@ def draw_circles(
140
134
 
141
135
 
142
136
  def get_fig_ax(
143
- fig: Optional[Figure] = None, ax: Optional[Axes] = None
137
+ fig: Figure | None = None, ax: Axes | None = None
144
138
  ) -> tuple[Figure, Axes]:
145
139
  if fig is None and ax is not None:
146
140
  fig = ax.get_figure()
@@ -32,17 +32,15 @@ def draw_lines_3d(
32
32
  ):
33
33
  """Draw lines.
34
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`.
35
+ Args:
36
+ ax: The plot axes.
37
+ lines: A collection of coords of lines
38
+ Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
39
+ and the axis-3 holds the coordinates (x, y, z).
40
+ **kwargs: Forwarded to `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
43
41
  """
44
42
 
45
43
  line_collection = Line3DCollection(
46
44
  lines, joinstyle=joinstyle, capstyle=capstyle, **kwargs
47
- ) # type: ignore
45
+ )
48
46
  return ax.add_collection3d(line_collection)
swcgeom/utils/renderer.py CHANGED
@@ -48,21 +48,29 @@ class Camera:
48
48
  _look_at: Vec3f
49
49
  _up: Vec3f
50
50
 
51
- # fmt: off
52
51
  @property
53
- def position(self) -> Vec3f: return self._position
52
+ def position(self) -> Vec3f:
53
+ return self._position
54
+
54
55
  @property
55
- def look_at(self) -> Vec3f: return self._look_at
56
+ def look_at(self) -> Vec3f:
57
+ return self._look_at
58
+
56
59
  @property
57
- def up(self) -> Vec3f: return self._up
60
+ def up(self) -> Vec3f:
61
+ return self._up
58
62
 
59
63
  @property
60
- def MV(self) -> npt.NDArray[np.float32]: raise NotImplementedError()
64
+ def MV(self) -> npt.NDArray[np.float32]:
65
+ raise NotImplementedError()
66
+
61
67
  @property
62
- def P(self) -> npt.NDArray[np.float32]: raise NotImplementedError()
68
+ def P(self) -> npt.NDArray[np.float32]:
69
+ raise NotImplementedError()
70
+
63
71
  @property
64
- def MVP(self) -> npt.NDArray[np.float32]: return self.P.dot(self.MV)
65
- # fmt: on
72
+ def MVP(self) -> npt.NDArray[np.float32]:
73
+ return self.P.dot(self.MV)
66
74
 
67
75
 
68
76
  class SimpleCamera(Camera):
swcgeom/utils/sdf.py CHANGED
@@ -17,10 +17,8 @@
17
17
 
18
18
  Refs: https://iquilezles.org/articles/distfunctions/
19
19
 
20
- Note
21
- ----
22
- This module has been deprecated since v0.14.0, and will be removed in
23
- the future, use `sdflit` instead.
20
+ NOTE: This module has been deprecated since v0.14.0, and will be removed in the future,
21
+ use `sdflit` instead.
24
22
  """
25
23
 
26
24
  import warnings
@@ -60,15 +58,11 @@ class SDF(ABC):
60
58
  def distance(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
61
59
  """Calculate signed distance.
62
60
 
63
- Parmeters
64
- ---------
65
- p: ArrayLike
66
- Hit point p of shape (N, 3).
61
+ Args:
62
+ p: Hit point p of shape (N, 3).
67
63
 
68
- Returns
69
- -------
70
- distance : npt.NDArray[np.float32]
71
- Distance array of shape (3,).
64
+ Returns:
65
+ distance: Distance array of shape (3,).
72
66
  """
73
67
  raise NotImplementedError()
74
68
 
@@ -84,11 +78,9 @@ class SDF(ABC):
84
78
  def is_in_bounding_box(self, p: npt.NDArray[np.float32]) -> npt.NDArray[np.bool_]:
85
79
  """Is p in bounding box.
86
80
 
87
- Returns
88
- -------
89
- is_in : npt.NDArray[np.bool_]
90
- Array of shape (N,), if bounding box is `None`, `True` will
91
- be returned.
81
+ Returns:
82
+ is_in: Array of shape (N,).
83
+ If bounding box is `None`, `True` will be returned.
92
84
  """
93
85
 
94
86
  if self.bounding_box is None:
@@ -285,12 +277,9 @@ class SDFRoundCone(SDF):
285
277
  ) -> None:
286
278
  """SDF of round cone.
287
279
 
288
- Parmeters
289
- ---------
290
- a, b : ArrayLike
291
- Coordinates of point A/B of shape (3,).
292
- ra, rb : float
293
- Radius of point A/B.
280
+ Args:
281
+ a, b: Coordinates of point A/B of shape (3,).
282
+ ra, rb: Radius of point A/B.
294
283
  """
295
284
 
296
285
  self.a = np.array(a, dtype=np.float32)
@@ -28,6 +28,15 @@ __all__ = [
28
28
 
29
29
 
30
30
  def find_unit_vector_on_plane(normal_vec3: npt.NDArray) -> npt.NDArray:
31
+ """Find a random unit vector on the plane defined by the normal vector.
32
+
33
+ >>> normal = np.array([0, 0, 1])
34
+ >>> u = find_unit_vector_on_plane(normal)
35
+ >>> np.allclose(np.dot(u, normal), 0) # Should be perpendicular
36
+ True
37
+ >>> np.allclose(np.linalg.norm(u), 1) # Should be unit length
38
+ True
39
+ """
31
40
  r = np.random.rand(3)
32
41
  r /= np.linalg.norm(r)
33
42
  while np.allclose(r, normal_vec3) or np.allclose(r, -normal_vec3):
@@ -45,6 +54,21 @@ def find_sphere_line_intersection(
45
54
  line_point_a: npt.NDArray,
46
55
  line_point_b: npt.NDArray,
47
56
  ) -> list[tuple[float, npt.NDArray[np.float64]]]:
57
+ """Find intersection points between a sphere and a line.
58
+
59
+ >>> center = np.array([0, 0, 0])
60
+ >>> radius = 1.0
61
+ >>> p1 = np.array([-2, 0, 0])
62
+ >>> p2 = np.array([2, 0, 0])
63
+ >>> intersections = find_sphere_line_intersection(center, radius, p1, p2)
64
+ >>> len(intersections)
65
+ 2
66
+ >>> np.allclose(intersections[0][1], [-1, 0, 0])
67
+ True
68
+ >>> np.allclose(intersections[1][1], [1, 0, 0])
69
+ True
70
+ """
71
+
48
72
  A = np.array(line_point_a)
49
73
  B = np.array(line_point_b)
50
74
  C = np.array(sphere_center)
@@ -74,8 +98,20 @@ def find_sphere_line_intersection(
74
98
 
75
99
 
76
100
  def project_point_on_line(
77
- point_a: npt.ArrayLike, direction_vector: npt.ArrayLike, point_p: npt.ArrayLike
101
+ point_a: npt.ArrayLike,
102
+ direction_vector: npt.ArrayLike,
103
+ point_p: npt.ArrayLike,
78
104
  ) -> npt.NDArray:
105
+ """Project a point onto a line defined by a point and direction vector.
106
+
107
+ >>> a = np.array([0, 0, 0])
108
+ >>> d = np.array([1, 0, 0])
109
+ >>> p = np.array([1, 1, 0])
110
+ >>> projection = project_point_on_line(a, d, p)
111
+ >>> np.allclose(projection, [1, 0, 0])
112
+ True
113
+ """
114
+
79
115
  A = np.array(point_a)
80
116
  n = np.array(direction_vector)
81
117
  P = np.array(point_p)
@@ -86,6 +122,14 @@ def project_point_on_line(
86
122
 
87
123
 
88
124
  def project_vector_on_vector(vec: npt.ArrayLike, target: npt.ArrayLike) -> npt.NDArray:
125
+ """Project one vector onto another.
126
+
127
+ >>> v = np.array([1, 1, 0])
128
+ >>> t = np.array([1, 0, 0])
129
+ >>> proj = project_vector_on_vector(v, t)
130
+ >>> np.allclose(proj, [1, 0, 0])
131
+ True
132
+ """
89
133
  v = np.array(vec)
90
134
  n = np.array(target)
91
135
 
@@ -95,8 +139,20 @@ def project_vector_on_vector(vec: npt.ArrayLike, target: npt.ArrayLike) -> npt.N
95
139
 
96
140
 
97
141
  def project_vector_on_plane(
98
- vec: npt.ArrayLike, plane_normal_vec: npt.ArrayLike
142
+ vec: npt.ArrayLike,
143
+ plane_normal_vec: npt.ArrayLike,
99
144
  ) -> npt.NDArray:
145
+ """Project a vector onto a plane defined by its normal vector.
146
+
147
+ >>> v = np.array([1, 1, 1])
148
+ >>> n = np.array([0, 0, 1])
149
+ >>> proj = project_vector_on_plane(v, n)
150
+ >>> np.allclose(proj, [1, 1, 0]) # Z component removed
151
+ True
152
+ >>> np.allclose(np.dot(proj, n), 0) # Should be perpendicular to normal
153
+ True
154
+ """
155
+
100
156
  v = np.array(vec)
101
157
  n = np.array(plane_normal_vec)
102
158
 
@@ -36,26 +36,56 @@ Vec3f = tuple[float, float, float]
36
36
 
37
37
 
38
38
  def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float:
39
- """Get the angle of vectors.
39
+ """Get the signed angle between two vectors.
40
40
 
41
- Returns
42
- -------
43
- angle : float
44
- Angle in radians.
41
+ The angle is positive if the rotation from a to b is counter-clockwise, and
42
+ negative if clockwise.
43
+
44
+ >>> angle([1, 0, 0], [1, 0, 0]) # identical
45
+ 0.0
46
+ >>> angle([1, 0, 0], [0, 1, 0]) # 90 degrees counter-clockwise
47
+ 1.5707963267948966
48
+ >>> angle([1, 0, 0], [0, -1, 0]) # 90 degrees clockwise
49
+ -1.5707963267948966
50
+
51
+ Returns:
52
+ angle: Angle in radians between -π and π.
45
53
  """
46
- a, b = np.array(a), np.array(b)
47
- costheta = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
48
- theta = np.arccos(costheta)
49
- return theta if np.cross(a, b) > 0 else -theta
50
54
 
55
+ a = np.asarray(a)
56
+ b = np.asarray(b)
51
57
 
52
- def scale3d(sx: float, sy: float, sz: float) -> npt.NDArray[np.float32]:
53
- """Get the 3D scale transfomation matrix.
58
+ # Normalize vectors
59
+ a_norm = a / np.linalg.norm(a)
60
+ b_norm = b / np.linalg.norm(b)
61
+
62
+ # Calculate cosine of angle
63
+ cos_theta = np.dot(a_norm, b_norm)
64
+ cos_theta = np.clip(cos_theta, -1.0, 1.0) # Ensure within valid range
65
+ theta = np.arccos(cos_theta)
66
+
67
+ # Determine sign using cross product
68
+ cross = np.cross(a_norm, b_norm)
69
+ sign = np.sign(cross[2]) # Use z-component for 3D
70
+ return float(sign * theta)
54
71
 
55
- Returns
56
- -------
57
- T : np.NDArray
58
- The homogeneous transfomation matrix, shape (4, 4).
72
+
73
+ def scale3d(sx: float, sy: float, sz: float) -> npt.NDArray[np.float32]:
74
+ """Get the 3D scale transformation matrix.
75
+
76
+ >>> np.allclose(
77
+ ... scale3d(2, 3, 4),
78
+ ... [
79
+ ... [2.0, 0.0, 0.0, 0.0],
80
+ ... [0.0, 3.0, 0.0, 0.0],
81
+ ... [0.0, 0.0, 4.0, 0.0],
82
+ ... [0.0, 0.0, 0.0, 1.0],
83
+ ... ],
84
+ ... )
85
+ True
86
+
87
+ Returns:
88
+ T: The homogeneous transformation matrix, shape (4, 4).
59
89
  """
60
90
  return np.array(
61
91
  [
@@ -69,12 +99,21 @@ def scale3d(sx: float, sy: float, sz: float) -> npt.NDArray[np.float32]:
69
99
 
70
100
 
71
101
  def translate3d(tx: float, ty: float, tz: float) -> npt.NDArray[np.float32]:
72
- """Get the 3D translate transfomation matrix.
73
-
74
- Returns
75
- -------
76
- T : np.NDArray
77
- The homogeneous transfomation matrix, shape (4, 4).
102
+ """Get the 3D translate transformation matrix.
103
+
104
+ >>> np.allclose(
105
+ ... translate3d(1, 2, 3),
106
+ ... [
107
+ ... [1.0, 0.0, 0.0, 1.0],
108
+ ... [0.0, 1.0, 0.0, 2.0],
109
+ ... [0.0, 0.0, 1.0, 3.0],
110
+ ... [0.0, 0.0, 0.0, 1.0],
111
+ ... ],
112
+ ... )
113
+ True
114
+
115
+ Returns:
116
+ T: The homogeneous transformation matrix, shape (4, 4).
78
117
  """
79
118
  return np.array(
80
119
  [
@@ -88,10 +127,10 @@ def translate3d(tx: float, ty: float, tz: float) -> npt.NDArray[np.float32]:
88
127
 
89
128
 
90
129
  def rotate3d(n: npt.ArrayLike, theta: float) -> npt.NDArray[np.float32]:
91
- r"""Get the 3D rotation transfomation matrix.
130
+ r"""Get the 3D rotation transformation matrix.
92
131
 
93
- Rotate v with axis n by an angle theta according to the right hand rule,
94
- follow rodrigues' rotaion formula.
132
+ Rotate v with axis n by an angle theta according to the right hand rule, follow
133
+ rodrigues' rotation formula.
95
134
 
96
135
  .. math::
97
136
 
@@ -105,17 +144,12 @@ def rotate3d(n: npt.ArrayLike, theta: float) -> npt.NDArray[np.float32]:
105
144
  -n_y & n_x & 0
106
145
  \end{pmatrix}
107
146
 
108
- Parameters
109
- ----------
110
- n : ArrayLike
111
- Rotation axis.
112
- theta : float
113
- Rotation angle in radians.
114
-
115
- Returns
116
- -------
117
- T : np.NDArray
118
- The homogeneous transfomation matrix, shape (4, 4).
147
+ Args:
148
+ n: Rotation axis.
149
+ theta: Rotation angle in radians.
150
+
151
+ Returns:
152
+ T: The homogeneous transformation matrix, shape (4, 4).
119
153
  """
120
154
 
121
155
  n = np.array(n)
@@ -138,20 +172,28 @@ def rotate3d(n: npt.ArrayLike, theta: float) -> npt.NDArray[np.float32]:
138
172
 
139
173
 
140
174
  def rotate3d_x(theta: float) -> npt.NDArray[np.float32]:
141
- """Get the 3D rotation transfomation matrix.
175
+ """Get the 3D rotation transformation matrix.
142
176
 
143
177
  Rotate 3D vector `v` with `x`-axis by an angle theta according to the right
144
178
  hand rule.
145
179
 
146
- Parameters
147
- ----------
148
- theta : float
149
- Rotation angle in radians.
150
-
151
- Returns
152
- -------
153
- T : np.NDArray
154
- The homogeneous transfomation matrix, shape (4, 4).
180
+ >>> np.allclose(
181
+ ... rotate3d_x(np.pi / 2), # 90 degree rotation
182
+ ... [
183
+ ... [+1.0, +0.0, +0.0, +0.0],
184
+ ... [+0.0, +0.0, -1.0, +0.0],
185
+ ... [+0.0, +1.0, +0.0, +0.0],
186
+ ... [+0.0, +0.0, +0.0, +1.0],
187
+ ... ],
188
+ ... )
189
+ True
190
+
191
+ Args:
192
+ theta: float
193
+ Rotation angle in radians.
194
+
195
+ Returns:
196
+ T: The homogeneous transformation matrix, shape (4, 4).
155
197
  """
156
198
 
157
199
  return np.array(
@@ -166,20 +208,27 @@ def rotate3d_x(theta: float) -> npt.NDArray[np.float32]:
166
208
 
167
209
 
168
210
  def rotate3d_y(theta: float) -> npt.NDArray[np.float32]:
169
- """Get the 3D rotation transfomation matrix.
211
+ """Get the 3D rotation transformation matrix.
170
212
 
171
213
  Rotate 3D vector `v` with `y`-axis by an angle theta according to the right
172
214
  hand rule.
173
215
 
174
- Parameters
175
- ----------
176
- theta : float
177
- Rotation angle in radians.
178
-
179
- Returns
180
- -------
181
- T : np.NDArray
182
- The homogeneous transfomation matrix, shape (4, 4).
216
+ >>> np.allclose(
217
+ ... rotate3d_y(np.pi / 2), # 90 degree rotation
218
+ ... [
219
+ ... [+0.0, +0.0, +1.0, +0.0],
220
+ ... [+0.0, +1.0, +0.0, +0.0],
221
+ ... [-1.0, +0.0, +0.0, +0.0],
222
+ ... [+0.0, +0.0, +0.0, +1.0],
223
+ ... ],
224
+ ... )
225
+ True
226
+
227
+ Args:
228
+ theta: Rotation angle in radians.
229
+
230
+ Returns:
231
+ T: The homogeneous transformation matrix, shape (4, 4).
183
232
  """
184
233
  return np.array(
185
234
  [
@@ -193,20 +242,29 @@ def rotate3d_y(theta: float) -> npt.NDArray[np.float32]:
193
242
 
194
243
 
195
244
  def rotate3d_z(theta: float) -> npt.NDArray[np.float32]:
196
- """Get the 3D rotation transfomation matrix.
197
-
198
- Rotate 3D vector `v` with `z`-axis by an angle theta according to the right
199
- hand rule.
200
-
201
- Parameters
202
- ----------
203
- theta : float
204
- Rotation angle in radians.
205
-
206
- Returns
207
- -------
208
- T : np.NDArray
209
- The homogeneous transfomation matrix, shape (4, 4).
245
+ """Get the 3D rotation transformation matrix.
246
+
247
+ Rotate 3D vector `v` with `z`-axis by an angle theta according to the right hand
248
+ rule.
249
+
250
+ >>> np.allclose(
251
+ ... rotate3d_z(np.pi / 2), # 90 degree rotation
252
+ ... [
253
+ ... [+0.0, -1.0, +0.0, +0.0],
254
+ ... [+1.0, +0.0, +0.0, +0.0],
255
+ ... [+0.0, +0.0, +1.0, +0.0],
256
+ ... [+0.0, +0.0, +0.0, +1.0],
257
+ ... ],
258
+ ... )
259
+ True
260
+
261
+ Args:
262
+ theta: float
263
+ Rotation angle in radians.
264
+
265
+ Returns:
266
+ T: np.NDArray
267
+ The homogeneous transformation matrix, shape (4, 4).
210
268
  """
211
269
  return np.array(
212
270
  [
@@ -222,17 +280,20 @@ def rotate3d_z(theta: float) -> npt.NDArray[np.float32]:
222
280
  def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
223
281
  """Fill xyz to homogeneous coordinates.
224
282
 
225
- Parameters
226
- ----------
227
- xyz : ArrayLike
228
- Coordinate of shape (..., 3)
229
- w : float
230
- w of homogeneous coordinate, 1 for dot, 0 for vector.
231
-
232
- Returns
233
- -------
234
- xyz4 : npt.NDArray[np.float32]
235
- Array of shape (..., 4)
283
+ >>> np.allclose(to_homogeneous([1, 2, 3], 1), [1.0, 2.0, 3.0, 1.0])
284
+ True
285
+ >>> np.allclose(
286
+ ... to_homogeneous([[1, 2, 3], [4, 5, 6]], 0),
287
+ ... [[1.0, 2.0, 3.0, 0.0], [4.0, 5.0, 6.0, 0.0]],
288
+ ... )
289
+ True
290
+
291
+ Args:
292
+ xyz: Coordinate of shape (..., 3)
293
+ w: w of homogeneous coordinate, 1 for dot, 0 for vector.
294
+
295
+ Returns:
296
+ xyz4: Array of shape (..., 4)
236
297
  """
237
298
  xyz = np.array(xyz)
238
299
  if xyz.ndim == 1:
@@ -247,17 +308,12 @@ def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
247
308
  def _to_homogeneous(xyz: npt.NDArray, w: float) -> npt.NDArray[np.float32]:
248
309
  """Fill xyz to homogeneous coordinates.
249
310
 
250
- Parameters
251
- ----------
252
- xyz : npt.NDArray
253
- Coordinate of shape (N, 3)
254
- w : float
255
- w of homogeneous coordinate, 1 for dot, 0 for vector.
256
-
257
- Returns
258
- -------
259
- xyz4 : npt.NDArray[np.float32]
260
- Array of shape (N, 4)
311
+ Args:
312
+ xyz: Coordinate of shape (N, 3)
313
+ w: w of homogeneous coordinate, 1 for dot, 0 for vector.
314
+
315
+ Returns:
316
+ xyz4: Array of shape (N, 4)
261
317
  """
262
318
  if xyz.shape[1] == 4:
263
319
  return xyz
@@ -273,14 +329,10 @@ def model_view_transformation(
273
329
  ) -> npt.NDArray[np.float32]:
274
330
  r"""Play model/view transformation.
275
331
 
276
- Parameters
277
- ----------
278
- position: Tuple[float, float, float]
279
- Camera position \vec{e}.
280
- look_at: Tuple[float, float, float]
281
- Camera look-at \vec{g}.
282
- up: Tuple[float, float, float]
283
- Camera up direction \vec{t}.
332
+ Args:
333
+ position: Camera position \vec{e}.
334
+ look_at: Camera look-at \vec{g}.
335
+ up: Camera up direction \vec{t}.
284
336
  """
285
337
 
286
338
  e = np.array(position, dtype=np.float32)
@@ -301,7 +353,19 @@ def model_view_transformation(
301
353
 
302
354
 
303
355
  def orthographic_projection_simple() -> npt.NDArray[np.float32]:
304
- """Simple orthographic projection by drop z-axis"""
356
+ """Simple orthographic projection by drop z-axis
357
+
358
+ >>> np.allclose(
359
+ ... orthographic_projection_simple(),
360
+ ... [
361
+ ... [1.0, 0.0, 0.0, 0.0],
362
+ ... [0.0, 1.0, 0.0, 0.0],
363
+ ... [0.0, 0.0, 0.0, 0.0],
364
+ ... [0.0, 0.0, 0.0, 0.0],
365
+ ... ],
366
+ ... )
367
+ True
368
+ """
305
369
  return np.array(
306
370
  [
307
371
  [1, 0, 0, 0],