-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathpinhole_camera_module.py
257 lines (205 loc) · 7.28 KB
/
pinhole_camera_module.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
import numpy as np
import numba
epsf = np.finfo(float).eps
sqrt_epsf = np.sqrt(epsf)
# @numba.njit(nogil=True, cache=False, parallel=False)
@numba.njit
def row_norm(a, out=None):
n, dim = a.shape
out = out if out is not None else np.empty((n,))
for i in range(n):
sqr_norm = a[i, 0] * a[i, 0]
for j in range(1, dim):
sqr_norm += a[i, j]*a[i, j]
out[i] = np.sqrt(sqr_norm)
return out
# @numba.njit(parallel=True, nogil=True)
@numba.njit
def row_sqrnorm(a, out=None):
n, dim = a.shape
out = out if out is not None else np.empty((n,))
for i in range(n):
sqr_norm = a[i, 0] * a[i, 0]
for j in range(1, dim):
sqr_norm += a[i, j]*a[i, j]
out[i] = sqr_norm
return out
# @numba.njit(nogil=True, cache=False, parallel=False)
def unit_vector(a, out=None):
out = out if out is not None else np.empty_like(a)
n, dim = a.shape
for i in range(n):
sqr_norm = a[i, 0] * a[i, 0]
for j in range(1, dim):
sqr_norm += a[i, j]*a[i, j]
if sqr_norm > sqrt_epsf:
recip_norm = 1.0 / np.sqrt(sqr_norm)
out[i, :] = a[i, :] * recip_norm
else:
out[i, :] = a[i, :]
return out
# @numba.njit(nogil=True, cache=False, parallel=True)
def compute_offset_beam_vector(bv, rho, tv, out=None):
out = out if out is not None else np.empty_like(bv)
return -unit_vector(-bv*rho - tv)
# @numba.njit(nogil=True, cache=False, parallel=True)
def ray_plane(rays, rmat, tvec):
"""
Calculate the primitive ray-plane intersection.
Parameters
----------
rays : array_like, (n, 3)
The vstacked collection of rays to intersect with the specified plane.
rmat : array_like, (3, 3)
The rotation matrix defining the orientation of the plane in the
reference CS. The plane normal is the last column.
tvec : array_like, (3,)
The translation vector components in the reference frame used to
describe a point on the plane (in this case the origin of the local
planar CS).
Returns
-------
ndarray, (n, 2)
The local planar (x, y) coordinates of the intersection points, NaN if
no intersection.
NOTES
-----
"""
nhat = np.ascontiguousarray(rmat[:, 2])
rays = np.atleast_2d(rays) # shape (npts, 3)
output = np.nan*np.ones_like(rays)
numerator = np.dot(tvec, nhat)
for i in range(len(rays)):
denominator = np.dot(rays[i, :], nhat)
if denominator < 0:
output[i, :] = rays[i, :] * numerator / denominator
return np.dot(output - tvec, rmat)[:, :2]
# @numba.njit(parallel=True, nogil=True)
@numba.njit
def ray_plane_trivial(rays, tvec):
"""
Calculate the primitive ray-plane intersection _without_ rotation
Parameters
----------
rays : array_like, (n, 3)
The vstacked collection of rays to intersect with the specified plane.
tvec : array_like, (3,)
The translation vector components in the reference frame used to
describe a point on the plane (in this case the origin of the local
planar CS).
Returns
-------
ndarray, (n, 2)
The local planar (x, y) coordinates of the intersection points, NaN if
no intersection.
NOTES
-----
"""
rays = np.atleast_2d(rays) # shape (npts, 3)
nrays = len(rays)
assert 3 == rays.shape[-1]
# output = np.nan*np.ones_like(rays)
output = np.empty((nrays, 2))
output.fill(np.nan)
numerator = tvec[2]
for i in range(len(rays)):
denominator = rays[i, 2]
if denominator < 0:
factor = numerator/denominator
output[i, 0] = rays[i, 0] * factor - tvec[0]
output[i, 1] = rays[i, 1] * factor - tvec[1]
return output
# numba parallel is underwherlming in this case
# @numba.njit(parallel=True, nogil=True)
@numba.njit
def pinhole_constraint_helper(rays, tvecs, radius, result):
"""
The whole operations of the pinhole constraint put together in
a single function.
returns an array with booleans for the rays that pass the constraint
"""
nrays = len(rays)
nvecs = len(tvecs)
sqr_radius = radius*radius
for ray_index in numba.prange(nrays):
denominator = rays[ray_index, 2]
if denominator > 0.:
result[ray_index] = False
continue
is_valid = True
for tvec_index in range(nvecs):
numerator = tvecs[tvec_index, 2]
factor = numerator/denominator
plane_x = rays[ray_index, 0]*factor - tvecs[tvec_index, 0]
plane_y = rays[ray_index, 1]*factor - tvecs[tvec_index, 1]
sqr_norm = plane_x*plane_x + plane_y*plane_y
if sqr_norm > sqr_radius:
is_valid = False
break
result[ray_index] = is_valid
return result
# @numba.njit(nogil=True, cache=False, parallel=True)
def pinhole_constraint(pixel_xys, voxel_vec, rmat_d_reduced, tvec_d,
radius, thickness):
"""
Applies pinhole aperature constraint to a collection of rays.
Parameters
----------
pixel_xys : contiguous ndarray
(n, 2) array of pixel (x, y) coordinates in the detector frame.
voxel_vec : contiguous ndarray
(3, ) vector of voxel COM in the lab frame.
rmat_d_reduced : contiguous ndarray
(2, 3) array taken from the (3, 3) detector rotation matrix;
specifically rmat[:, :2].T
tvec_d : contiguous ndarray
(3, ) detector tranlastion vector.
radius : scalar
pinhole radius in mm.
thickness : scalar
pinhole thickness in mm.
Returns
-------
ndarray, bool
(n, ) boolean vector where True denotes a pixel that can "see" the
specified voxel.
Notes
-----
!!! Pinhole plane normal is currently FIXED to [0, 0, 1]
"""
# '''
pv_ray_lab = np.dot(pixel_xys, rmat_d_reduced) + (tvec_d - voxel_vec)
tvecs = np.empty((2, 3))
tvecs[0] = -voxel_vec
tvecs[1] = np.r_[0., 0., -thickness] - voxel_vec
result = np.empty((len(pixel_xys)), dtype=np.bool_)
return pinhole_constraint_helper(pv_ray_lab, tvecs, radius, result)
'''
tvec_ph_b = np.array([0., 0., -thickness])
pv_ray_lab = np.dot(pixel_xys, rmat_d_reduced) + (tvec_d - voxel_vec)
# !!! was previously performing unnecessary trival operations
# rmat_ph = np.eye(3)
# fint = row_norm(ray_plane(pv_ray_lab, rmat_ph, -voxel_vec))
# bint = row_norm(ray_plane(pv_ray_lab, rmat_ph, tvec_ph_b - voxel_vec))
fint = row_sqrnorm(ray_plane_trivial(pv_ray_lab, -voxel_vec))
bint = row_sqrnorm(ray_plane_trivial(pv_ray_lab, tvec_ph_b - voxel_vec))
sqr_radius = radius * radius
return np.logical_and(fint <= sqr_radius, bint <= sqr_radius)
'''
def compute_critical_voxel_radius(offset, radius, thickness):
"""
Compute the offset-sepcific critical radius of a pinhole aperture.
Parameters
----------
offset : scalar
The offset from the front of the pinhole to the layer position.
radius : scalar
pinhole radius.
thickness : scalar
pinhole thickness (cylinder height).
Returns
-------
scalar
the critical _radius_ for a voxel to ray to clear the pinhole aperture.
"""
return radius*(2*offset/thickness + 1)