-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerator.py
317 lines (275 loc) · 12 KB
/
generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import numpy as np
import numba as nb
from data_structures import VoxelSet, sparse_type
gen_type = [
('_ranges', nb.float64[:,:]),
('_bins', nb.int64[:])
]
@nb.experimental.jitclass(gen_type)
class TrackGenerator:
'''
Class which calls Numba routines that produce
random tracks inside a box and voxelize them.
'''
def __init__(self: gen_type,
ranges: nb.float64[:,:],
bins: nb.int64[:] = np.empty(0, dtype=np.int64)):
'''
Initializes the cube in which to sample a track
Args:
ranges (array): (3,2) array of volume boundaries
bins (array): (3) array of number of bins in each dimension
'''
self._ranges = ranges
self._bins = bins
def get_point(self: gen_type) -> nb.float64[:]:
'''
Gets a random point inside the volume
Returns:
array: Coordinates of the point
'''
return self._ranges[:,0] + self.dimensions*np.random.rand(3)
def get_face_point(self: gen_type,
axis: nb.int64 = -1,
side: nb.int64 = -1,
exclude_axis: nb.int64 = -1,
exclude_side: nb.int64 = -1) -> (nb.float64[:], nb.int64, nb.int64):
'''
Gets a random point on the boundaries of the volume
Args:
axis (int) : Axis the first face to use is perpendicular to (-1: random)
side (int) : Side the first face to use is on (-1: random)
exclude_axis (int): Axis the face not to use is perpendicular to (-1: random)
exclude_axis (int): Side the face not to use is on (-1: random)
Returns:
array: Coordinates of the point
'''
assert axis < 0 or side < 0 or axis != exclude_axis or side != exclude_side
if axis < 0:
axis = np.random.randint(3)
if side < 0:
side = np.random.randint(2)
while axis == exclude_axis and side == exclude_side:
axis = np.random.randint(3)
side = np.random.randint(2)
center = 0.5*(np.ones(3) + (2*side - 1)*(np.arange(3)==axis))
offset = (np.arange(3)!=axis)*(np.random.rand(3)-0.5)
return self._ranges[:,0] + self.dimensions*(center + offset), axis, side
def get_segment(self: gen_type) -> (nb.float64[:], nb.float64[:]):
'''
Gets a random segment inside the volume
Returns:
array: Coordinates of the start points
array: Coordinates of the end points
'''
return self.get_point(), self.get_point()
def get_line(self: gen_type,
start_axis: nb.int64,
start_side: nb.int64,
vector_method: bool = True) -> (nb.float64[:], nb.float64[:]):
'''
Gets a random through line inside a cube
Args:
start_axis (int) : Axis the first face to use is perpendicular to (-1: random)
start_side (int) : Side the first face to use is on (-1: random)
vector_method (bool): If true, generates lines by taking a random direction vector
Returns:
array: Coordinates of the start points
array: Coordinates of the end points
'''
if start_axis > -1 or start_side > -1:
start, fa, fs = self.get_face_point(axis=start_axis, side=start_side)
else:
start = self.get_point()
if vector_method:
vec = np.array([np.random.normal() for _ in range(3)])
vec /= np.linalg.norm(vec)
vertices = []
tol = 1e-9
for axis in range(3):
for side in range(2):
xi = (self._ranges[axis,side]-start[axis])/vec[axis]
intc = start + xi*vec
if (intc >= self._ranges[:,0]-tol).all() & (intc <= self._ranges[:,1]+tol).all():
maskl, masku = intc < self._ranges[:,0] + tol, intc > self._ranges[:,1] - tol
intc[maskl] = self._ranges[maskl, 0]
intc[masku] = self._ranges[masku, 1]
vertices.append(intc)
assert len(vertices) == 2
vertices = np.vstack((vertices[0], vertices[1]))
np.random.shuffle(vertices)
start, end = vertices[0], vertices[1]
else:
end, _, __ = self.get_face_point(exclude_axis=fa, exclude_side=fs)
return start, end
def get_segments(self: gen_type,
n: nb.int64[:]) -> (nb.float64[:,:], nb.float64[:,:]):
'''
Get a set of random segments inside a cube
Args:
n (float): Number of lines to produces
Returns:
array: Coordinates of the start points
array: Coordinates of the end points
'''
starts, ends = np.empty((n,3), dtype=np.float64), np.empty((n,3), dtype=np.float64)
for i in range(n):
starts[i], ends[i] = self.get_segment()
return starts, ends
def get_lines(self: gen_type,
n: nb.int64 = 1,
start_axis: nb.int64 = -1,
start_side: nb.int64 = -1,
vector_method: bool = True) -> (nb.float64[:,:], nb.float64[:,:]):
'''
Gets a set of random through lines inside a cube
Args:
n (int) : Number of lines to produces
start_axis (int) : Axis the first face to use is perpendicular to (-1: random)
start_side (int) : Side the first face to use is on (-1: random)
vector_method (bool): If true, generates lines by taking a random direction vector
Returns:
array: Coordinates of the start points
array: Coordinates of the end points
'''
starts, ends = np.empty((n,3), dtype=np.float64), np.empty((n,3), dtype=np.float64)
for i in range(n):
starts[i], ends[i] = self.get_line(start_axis, start_side, vector_method)
return starts, ends
def get_segment_voxels(self: gen_type,
n: nb.int64 = 1) -> (nb.float64[:,:], nb.float64[:,:]):
'''
Generates segments an voxelizes them
Args:
n (int): Number of segments to produce
Returns:
np.ndarray: VoxelSet object
'''
voxel_set = VoxelSet(self._ranges, self._bins)
starts, ends = self.get_segments(n)
for i in range(n):
self.append_track_voxels(voxel_set, [starts[i], ends[i]])
return voxel_set, (starts, ends)
def get_line_voxels(self: gen_type,
n: nb.int64 = 1,
start_axis: nb.int64 = -1,
start_side: nb.int64 = -1,
vector_method: bool = True) -> (nb.float64[:,:], nb.float64[:,:]):
'''
Generates lines an voxelizes them
Args:
n (int) : Number of lines to produce
start_axis (int) : Axis the first face to use is perpendicular to (-1: random)
start_side (int) : Side the first face to use is on (-1: random)
vector_method (bool): If true, generates lines by taking a random direction vector
Returns:
np.ndarray: VoxelSet object
'''
voxel_set = VoxelSet(self._ranges, self._bins)
starts, ends = self.get_lines(n, start_axis, start_side, vector_method)
for i in range(n):
self.append_track_voxels(voxel_set, [starts[i], ends[i]])
return voxel_set, (starts, ends)
def append_track_voxels(self: gen_type,
voxel_set: sparse_type,
end_points: (nb.float64[:], nb.float64[:])):
'''
Converts a line (pair of end points) into a set of voxels
Args:
voxel_set (dict) : Set of voxels to append
end_points (array): End points of the line
'''
# Find line gradients
start, end = end_points
grads = end - start
# Find the range of bins in the first axis, loop over them
vox_size = voxel_set.bin_size
s0, e0 = min(start[0], end[0]), max(start[0], end[0])
binsi = self.segment_bin_range(start, end, 0, self._bins[0], vox_size[0])
for bini in binsi:
# Restrict the line to a segment contained within this 2D slice
si, ei = max(s0, bini*vox_size[0]), min(e0, (bini+1)*vox_size[0])
ssi, sei = self.segment_end_points(si, ei, 0, start, grads)
# Find the range of bins in the second axis, loop over them
binsj = self.segment_bin_range(ssi, sei, 1, self._bins[1], vox_size[1])
for binj in binsj:
# Restrict the line to a segment contained within this 1D slice
sj, ej = max(ssi[1], binj*vox_size[1]), min(sei[1], (binj+1)*vox_size[1])
ssij, seij = self.segment_end_points(sj, ej, 1, start, grads)
# Find the range of bins in the final axis, loop over them
binsk = self.segment_bin_range(ssij, seij, 2, self._bins[2], vox_size[2])
for bink in binsk:
# Restrict the line to a segment contained within this voxel
sk, ek = max(ssij[2], bink*vox_size[2]), min(seij[2], (bink+1)*vox_size[2])
ssijk, seijk = self.segment_end_points(sk, ek, 2, start, grads)
# Get the length of the segment inside the voxel
seg_len = np.linalg.norm(seijk-ssijk)
# Record bin IDs as voxel coordinates and length
idx = voxel_set.pos_to_id([bini, binj, bink])
voxel_set.add(idx, seg_len)
@staticmethod
def segment_end_points(si: nb.float64,
ei: nb.float64,
ai: nb.int64,
start: nb.int64,
grads: nb.float64[:]) -> (nb.float64[:], nb.float64[:]):
'''
Given a line start point and its gradients, find
the coordinates of the start en end points of a segment
of the line which starts and ends at given positions
along one specific axis.
Args:
si (float): Start coordinate of segment along axis i
ei (float): End coordinate of segment along axis i
ai (int): Axis id (0,1 or 2)
start (array): Track start point coordinates
grads (array): Track gradients
Returns:
array: Segment start coordinates
array: Segment end coordinates
'''
ss, se = np.empty(3), np.empty(3)
xis, xie = (si - start[ai])/grads[ai], (ei - start[ai])/grads[ai]
for a in range(3):
ss[a] = start[a] + xis*grads[a]
se[a] = start[a] + xie*grads[a]
ss[a], se[a] = min(ss[a], se[a]), max(ss[a], se[a])
return ss, se
@staticmethod
def segment_bin_range(ss: nb.float64[:],
se: nb.float64[:],
a: nb.int64,
n: nb.int64,
size: nb.float64) -> nb.int64[:]:
'''
Returns bin range of a segment along a certain axis
Args:
ss (array): Segment start point
se (array): Segment end point
a (int): Axis id (0,1 or 2)
n (int): Number of bins along axis
size (float): Bin size
Returns:
array: Range of bins
'''
s, e = min(ss[a], se[a]), max(ss[a], se[a])
bins = np.arange(n)
return bins[((bins+1)*size > s) & (bins*size < e)]
@property
def ranges(self):
'''
Returns the voxel ranges
'''
return self._ranges
@property
def bins(self):
'''
Returns the number of bins in each axis
'''
return self._bins
@property
def dimensions(self):
'''
Returns the dimensions of the image
'''
return self._ranges[:,1] - self._ranges[:,0]