Skip to content

Commit 3c552fd

Browse files
authored
Merge pull request #9 from hsorby/main
Add vector operation functions used by scaffoldmaker here.
2 parents 19a4cc5 + 2ab75f1 commit 3c552fd

File tree

3 files changed

+228
-51
lines changed

3 files changed

+228
-51
lines changed

src/cmlibs/maths/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.5.0'
1+
__version__ = '0.6.0'

src/cmlibs/maths/octree.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"""
44
import math
55

6-
from cmlibs.maths.vectorops import sub, dot
6+
from cmlibs.maths.vectorops import sub, dot, add, div
77

88

99
class Octree:
1010
"""
11-
Octree for searching for objects by coordinates
11+
Octree for searching for objects by coordinates.
1212
"""
1313

1414
def __init__(self, tolerance=None):
@@ -104,3 +104,106 @@ def __repr__(self):
104104
return f'\tleaf {self._coordinates}\n'
105105

106106
return f'{self._coordinates} - \n' + ''.join([f'{c}' for c in self._children])
107+
108+
109+
class VolumeOctreeObject:
110+
"""
111+
Interface object for the volume octree.
112+
This class defines the required interface for objects
113+
given to the VolumeOctree.
114+
115+
It is not required that you use this object directly.
116+
It is required that any objects added to the volume octree
117+
follow this interface.
118+
"""
119+
120+
def __init__(self, data_object):
121+
self._data_object = data_object
122+
123+
def identifier(self):
124+
return self._data_object.identifier()
125+
126+
def points(self):
127+
return self._data_object.points()
128+
129+
def distance(self, pt, tol):
130+
return self._data_object.distance(pt, tol)
131+
132+
133+
class VolumeOctree:
134+
"""
135+
An octree for managing objects with a volume.
136+
"""
137+
138+
def __init__(self, bounding_box, tolerance=1e-08, depth=0):
139+
self._bounding_box = bounding_box
140+
self._centre = div(add(bounding_box[0], bounding_box[1]), 2)
141+
self._tolerance = tolerance
142+
self._objects = {}
143+
self._children = {}
144+
self._depth = depth
145+
146+
def _child_index_lookup(self, x):
147+
switch = [0 if x[i] < c_i else 1 for i, c_i in enumerate(self._centre)]
148+
return sum([2**i * s for i, s in enumerate(switch)])
149+
150+
def _sub_bounding_box(self, index):
151+
masks = [2**i for i in range(len(self._centre))]
152+
p = [self._bounding_box[0 if index & m == 0 else 1][i] for i, m in enumerate(masks)]
153+
return [[min(i) for i in zip(p, self._centre)], [max(i) for i in zip(p, self._centre)]]
154+
155+
def insert_object(self, obj):
156+
indices = [self._child_index_lookup(pt) for pt in obj.points()]
157+
if len(set(indices)) == 1:
158+
required_child = indices[0]
159+
if required_child not in self._children:
160+
self._children[required_child] = VolumeOctree(self._sub_bounding_box(required_child), self._tolerance, depth=self._depth + 1)
161+
# if self._children is None:
162+
# self._children = [VolumeOctree(self._sub_bounding_box(i), self._tolerance, depth=self._depth + 1) for i in range(2**len(self._centre))]
163+
164+
self._children[required_child].insert_object(obj)
165+
else:
166+
for i in set(indices):
167+
if i not in self._objects:
168+
self._objects[i] = []
169+
self._objects[i].append(obj)
170+
171+
def find_object(self, x):
172+
"""
173+
Find the closest existing object with |x - ox| < tolerance.
174+
:param x: Coordinates in a list.
175+
:return: Nearest object or None.
176+
"""
177+
octant_index = self._child_index_lookup(x)
178+
target = self._children[octant_index].find_object(x) if octant_index in self._children else None
179+
180+
if target is not None:
181+
return target
182+
183+
distances = [obj.distance(x, self._tolerance) for obj in self._objects.get(octant_index, [])]
184+
index = next((i for i, x in enumerate(distances) if x < self._tolerance), None)
185+
if index is not None:
186+
return self._objects[octant_index][index]
187+
188+
return None # self._children[octant_index].find_object(x) if octant_index in self._children else None
189+
190+
def __repr__(self):
191+
indent = [' '] * 2 * self._depth
192+
indent = ''.join(indent)
193+
194+
objects_len = ', '.join([f'{o}: {len(self._objects[o])}' for o in self._objects])
195+
if self._children is None:
196+
return f'{indent}leaf {self._centre} [{objects_len}]\n'
197+
198+
return f'{indent}{self._centre} [{objects_len}]- \n' + ''.join([f'{self._children[c]}' for c in self._children])
199+
200+
201+
def define_bounding_box():
202+
"""
203+
From an initial point define a bounding box that covers everything.
204+
Return the top, back, left and bottom, front, right points to define
205+
the box.
206+
207+
:return: A list of two 3D points defining the extent of the bounding box.
208+
"""
209+
return [[math.inf, math.inf, math.inf], [-math.inf, -math.inf, -math.inf]]

src/cmlibs/maths/vectorops.py

Lines changed: 122 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,66 @@
33
they were vectors. A basic implementation to forgo the need
44
to use numpy.
55
"""
6-
import math
7-
import sys
8-
from math import sqrt, cos, sin, fabs, atan2
6+
from math import acos, sqrt, cos, sin, fabs, atan2
97

108

119
def magnitude(v):
12-
return sqrt(sum(v[i] * v[i] for i in range(len(v))))
10+
"""
11+
return: scalar magnitude of vector v
12+
"""
13+
return sqrt(sum(c * c for c in v))
14+
15+
16+
def set_magnitude(v, mag):
17+
"""
18+
return: Vector v with magnitude set to mag.
19+
"""
20+
scale = mag / magnitude(v)
21+
return mult(v, scale)
1322

1423

1524
def add(u, v):
16-
return [u[i] + v[i] for i in range(len(u))]
25+
return [u_i + v_i for u_i, v_i in zip(u, v)]
1726

1827

1928
def sub(u, v):
20-
return [u[i] - v[i] for i in range(len(u))]
29+
return [u_i - v_i for u_i, v_i in zip(u, v)]
30+
31+
32+
def mult(v, s):
33+
"""
34+
Calculate s * v
35+
:param v: Vector.
36+
:param s: Scalar.
37+
:return: [s * v_1, s * v_2, ..., s * v_n]
38+
"""
39+
return [c * s for c in v]
40+
41+
42+
def div(v, s):
43+
"""
44+
Calculate v / s
45+
:param v: Vector.
46+
:param s: Scalar.
47+
:return: [v_1 / s, v_2 / s, ..., v_n / s]
48+
"""
49+
return [c / s for c in v]
2150

2251

2352
def dot(u, v):
24-
return sum(u[i] * v[i] for i in range(len(u)))
53+
return sum(u_i * v_i for u_i, v_i in zip(u, v))
2554

2655

2756
def eldiv(u, v):
28-
return [u[i] / v[i] for i in range(len(u))]
57+
return [u_i / v_i for u_i, v_i in zip(u, v)]
2958

3059

3160
def elmult(u, v):
32-
return [u[i] * v[i] for i in range(len(u))]
61+
return [u_i * v_i for u_i, v_i in zip(u, v)]
3362

3463

3564
def normalize(v):
36-
vmag = magnitude(v)
37-
return [v[i] / vmag for i in range(len(v))]
65+
return div(v, magnitude(v))
3866

3967

4068
def cross(u, v):
@@ -45,36 +73,66 @@ def cross(u, v):
4573
return c
4674

4775

48-
def mult(u, c):
49-
return [u[i] * c for i in range(len(u))]
76+
def scalar_projection(v1, v2):
77+
"""
78+
:return: Scalar projection of v1 onto v2.
79+
"""
80+
return dot(v1, normalize(v2))
5081

5182

52-
def div(u, c):
53-
return [u[i] / c for i in range(len(u))]
83+
def projection(v1, v2):
84+
"""
85+
Calculate vector projection of v1 on v2
86+
:return: A projection vector.
87+
"""
88+
s1 = scalar_projection(v1, v2)
89+
return mult(normalize(v2), s1)
5490

5591

56-
def quaternion_to_rotation_matrix(quaternion):
92+
def rejection(v1, v2):
5793
"""
58-
This method takes a quaternion representing a rotation
59-
and turns it into a rotation matrix.
60-
:return: 3x3 rotation matrix suitable for pre-multiplying vector v:
61-
i.e. v' = Mv
94+
Calculate vector rejection of v1 on v2
95+
:return: A rejection vector.
6296
"""
63-
mag_q = magnitude(quaternion)
64-
norm_q = div(quaternion, mag_q)
65-
qw, qx, qy, qz = norm_q
66-
ww, xx, yy, zz = qw * qw, qx * qx, qy * qy, qz * qz
67-
wx, wy, wz, xy, xz, yz = qw * qx, qw * qy, qw * qz, qx * qy, qx * qz, qy * qz
68-
# mx = [[qw * qw + qx * qx - qy * qy - qz * qz, 2 * qx * qy - 2 * qw * qz, 2 * qx * qz + 2 * qw * qy],
69-
# [2 * qx * qy + 2 * qw * qz, qw * qw - qx * qx + qy * qy - qz * qz, 2 * qy * qz - 2 * qw * qx],
70-
# [2 * qx * qz - 2 * qw * qy, 2 * qy * qz + 2 * qw * qx, qw * qw - qx * qx - qy * qy + qz * qz]]
71-
# aa, bb, cc, dd = a * a, b * b, c * c, d * d
72-
# bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
97+
v1p = projection(v1, v2)
98+
return add_vectors([v1, v1p], [1.0, -1.0])
7399

74-
mx = [[ww + xx - yy - zz, 2 * (xy + wz), 2 * (xz - wy)],
75-
[2 * (xy - wz), ww + yy - xx - zz, 2 * (yz + wx)],
76-
[2 * (xz + wy), 2 * (yz - wx), ww + zz - xx - yy]]
77-
return mx
100+
101+
def angle(u, v):
102+
"""
103+
Calculate the angle between two non-zero vectors.
104+
:return: The angle between them in radians.
105+
"""
106+
d = magnitude(u) * magnitude(v)
107+
return acos(dot(u, v) / d)
108+
109+
110+
def add_vectors(vectors, scalars=None):
111+
"""
112+
returns s1*v1+s2*v2+... where scalars = [s1, s2, ...] and vectors=[v1, v2, ...].
113+
:return: Resultant vector
114+
"""
115+
if not scalars:
116+
scalars = [1] * len(vectors)
117+
else:
118+
assert len(vectors) == len(scalars)
119+
120+
vector_dimension = len(vectors[0])
121+
resultant = [0] * vector_dimension
122+
for i, vector in enumerate(vectors):
123+
resultant = [resultant[c] + scalars[i] * vector[c] for c in range(vector_dimension)]
124+
return resultant
125+
126+
127+
def rotate_vector_around_vector(v, k, a):
128+
"""
129+
Rotate vector v, by an angle a (right-hand rule) in radians around vector k.
130+
:return: rotated vector.
131+
"""
132+
k = normalize(k)
133+
vperp = add_vectors([v, cross(k, v)], [cos(a), sin(a)])
134+
vparal = mult(k, dot(k, v) * (1 - cos(a)))
135+
return add_vectors([vperp, vparal])
78136

79137

80138
def matrix_constant_mult(m, c):
@@ -100,7 +158,7 @@ def vector_matrix_mult(v, m):
100158
raise ValueError('vector_matrix_mult mismatched rows')
101159
columns = len(m[0])
102160
result = []
103-
for c in range(0, columns):
161+
for c in range(columns):
104162
result.append(sum(v[r] * m[r][c] for r in range(rows)))
105163
return result
106164

@@ -132,15 +190,16 @@ def matrix_inv(a):
132190
"""
133191
Invert a square matrix by compouting the determinant and cofactor matrix.
134192
"""
193+
len_a = len(a)
135194
det = matrix_det(a)
136195

137-
if len(a) == 2:
196+
if len_a == 2:
138197
return matrix_constant_mult([[a[1][1], -1 * a[0][1]], [-1 * a[1][0], a[0][0]]], 1 / det)
139198

140199
cofactor = []
141-
for r in range(len(a)):
200+
for r in range(len_a):
142201
row = []
143-
for c in range(len(a)):
202+
for c in range(len_a):
144203
minor = matrix_minor(a, r, c)
145204
row.append(((-1) ** (r + c)) * matrix_det(minor))
146205
cofactor.append(row)
@@ -151,7 +210,7 @@ def matrix_inv(a):
151210

152211
def identity_matrix(size):
153212
"""
154-
Create an identity matrix of size size.
213+
Create an identity matrix of size x size.
155214
"""
156215
identity = []
157216
for r in range(size):
@@ -165,18 +224,31 @@ def identity_matrix(size):
165224

166225

167226
def transpose(a):
168-
if sys.version_info < (3, 0):
169-
return map(list, zip(*a))
170227
return list(map(list, zip(*a)))
171228

172229

173-
def angle(u, v):
230+
def quaternion_to_rotation_matrix(quaternion):
174231
"""
175-
Calculate the angle between two non-zero vectors.
176-
:return: The angle between them in radians.
232+
This method takes a quaternion representing a rotation
233+
and turns it into a rotation matrix.
234+
:return: 3x3 rotation matrix suitable for pre-multiplying vector v:
235+
i.e. v' = Mv
177236
"""
178-
d = magnitude(u) * magnitude(v)
179-
return math.acos(dot(u, v) / d)
237+
mag_q = magnitude(quaternion)
238+
norm_q = div(quaternion, mag_q)
239+
qw, qx, qy, qz = norm_q
240+
ww, xx, yy, zz = qw * qw, qx * qx, qy * qy, qz * qz
241+
wx, wy, wz, xy, xz, yz = qw * qx, qw * qy, qw * qz, qx * qy, qx * qz, qy * qz
242+
# mx = [[qw * qw + qx * qx - qy * qy - qz * qz, 2 * qx * qy - 2 * qw * qz, 2 * qx * qz + 2 * qw * qy],
243+
# [2 * qx * qy + 2 * qw * qz, qw * qw - qx * qx + qy * qy - qz * qz, 2 * qy * qz - 2 * qw * qx],
244+
# [2 * qx * qz - 2 * qw * qy, 2 * qy * qz + 2 * qw * qx, qw * qw - qx * qx - qy * qy + qz * qz]]
245+
# aa, bb, cc, dd = a * a, b * b, c * c, d * d
246+
# bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
247+
248+
mx = [[ww + xx - yy - zz, 2 * (xy + wz), 2 * (xz - wy)],
249+
[2 * (xy - wz), ww + yy - xx - zz, 2 * (yz + wx)],
250+
[2 * (xz + wy), 2 * (yz - wx), ww + zz - xx - yy]]
251+
return mx
180252

181253

182254
def euler_to_rotation_matrix(euler_angles):
@@ -198,8 +270,10 @@ def euler_to_rotation_matrix(euler_angles):
198270
cos_roll = cos(euler_angles[2])
199271
sin_roll = sin(euler_angles[2])
200272
mat3x3 = [
201-
[cos_azimuth * cos_elevation, cos_azimuth * sin_elevation * sin_roll - sin_azimuth * cos_roll, cos_azimuth * sin_elevation * cos_roll + sin_azimuth * sin_roll],
202-
[sin_azimuth * cos_elevation, sin_azimuth * sin_elevation * sin_roll + cos_azimuth * cos_roll, sin_azimuth * sin_elevation * cos_roll - cos_azimuth * sin_roll],
273+
[cos_azimuth * cos_elevation, cos_azimuth * sin_elevation * sin_roll - sin_azimuth * cos_roll,
274+
cos_azimuth * sin_elevation * cos_roll + sin_azimuth * sin_roll],
275+
[sin_azimuth * cos_elevation, sin_azimuth * sin_elevation * sin_roll + cos_azimuth * cos_roll,
276+
sin_azimuth * sin_elevation * cos_roll - cos_azimuth * sin_roll],
203277
[-sin_elevation, cos_elevation * sin_roll, cos_elevation * cos_roll]]
204278
return mat3x3
205279

0 commit comments

Comments
 (0)