diffct 2.0.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.
- diffct/__init__.py +83 -0
- diffct/analytical.py +569 -0
- diffct/constants.py +72 -0
- diffct/differentiable.py +74 -0
- diffct/geometry.py +991 -0
- diffct/kernels/__init__.py +43 -0
- diffct/kernels/cone_beam.py +617 -0
- diffct/kernels/fan_beam.py +451 -0
- diffct/kernels/geometry_vjp.py +562 -0
- diffct/kernels/parallel_beam.py +400 -0
- diffct/operators.py +546 -0
- diffct/projectors.py +910 -0
- diffct/utils.py +357 -0
- diffct-2.0.0.dist-info/METADATA +240 -0
- diffct-2.0.0.dist-info/RECORD +18 -0
- diffct-2.0.0.dist-info/WHEEL +4 -0
- diffct-2.0.0.dist-info/licenses/LICENSE +201 -0
- diffct-2.0.0.dist-info/licenses/NOTICE +18 -0
diffct/geometry.py
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
"""Geometry and trajectory generation for cone beam CT.
|
|
2
|
+
|
|
3
|
+
This module provides functions for generating circular, spiral, sinusoidal, saddle,
|
|
4
|
+
and custom trajectories for cone beam CT scanning geometries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import torch
|
|
9
|
+
from typing import Callable, Tuple
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ============================================================================
|
|
13
|
+
# Trajectory Generation Functions
|
|
14
|
+
# ============================================================================
|
|
15
|
+
|
|
16
|
+
def circular_trajectory_3d(n_views, sid, sdd, start_angle=0.0, end_angle=None, device='cuda', dtype=torch.float32):
|
|
17
|
+
"""Generate circular trajectory geometry for cone-beam CT.
|
|
18
|
+
|
|
19
|
+
Creates source and detector position matrices for a standard circular
|
|
20
|
+
orbit around the z-axis, commonly used in cone-beam CT imaging.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
n_views : int
|
|
25
|
+
Number of projection views.
|
|
26
|
+
sid : float
|
|
27
|
+
Source-to-Isocenter Distance (SID), in physical units.
|
|
28
|
+
sdd : float
|
|
29
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
30
|
+
start_angle : float, optional
|
|
31
|
+
Starting angle in radians (default: 0.0).
|
|
32
|
+
end_angle : float, optional
|
|
33
|
+
Ending angle in radians (default: 2*pi, full rotation).
|
|
34
|
+
device : str or torch.device, optional
|
|
35
|
+
Device for tensors (default: 'cuda').
|
|
36
|
+
dtype : torch.dtype, optional
|
|
37
|
+
Data type for tensors (default: torch.float32).
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
src_pos : torch.Tensor
|
|
42
|
+
Source positions, shape (n_views, 3).
|
|
43
|
+
det_center : torch.Tensor
|
|
44
|
+
Detector center positions, shape (n_views, 3).
|
|
45
|
+
det_u_vec : torch.Tensor
|
|
46
|
+
Detector u-direction unit vectors, shape (n_views, 3).
|
|
47
|
+
det_v_vec : torch.Tensor
|
|
48
|
+
Detector v-direction unit vectors, shape (n_views, 3).
|
|
49
|
+
|
|
50
|
+
Examples
|
|
51
|
+
--------
|
|
52
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(
|
|
53
|
+
... n_views=360, sid=1000.0, sdd=1500.0, device='cuda'
|
|
54
|
+
... )
|
|
55
|
+
>>> print(src_pos.shape) # (360, 3)
|
|
56
|
+
"""
|
|
57
|
+
import math
|
|
58
|
+
|
|
59
|
+
if end_angle is None:
|
|
60
|
+
end_angle = 2 * math.pi
|
|
61
|
+
|
|
62
|
+
# Generate angles (equivalent to linspace with endpoint=False)
|
|
63
|
+
step = (end_angle - start_angle) / n_views
|
|
64
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
65
|
+
|
|
66
|
+
# Preallocate position matrices
|
|
67
|
+
src_pos = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
68
|
+
det_center = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
69
|
+
det_u_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
70
|
+
det_v_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
71
|
+
|
|
72
|
+
# Compute trigonometric values
|
|
73
|
+
cos_angles = torch.cos(angles)
|
|
74
|
+
sin_angles = torch.sin(angles)
|
|
75
|
+
|
|
76
|
+
# Source rotates around isocenter at distance sid
|
|
77
|
+
# Convention: source at (x, y, 0), rotating in xy-plane
|
|
78
|
+
src_pos[:, 0] = -sid * sin_angles # x
|
|
79
|
+
src_pos[:, 1] = sid * cos_angles # y
|
|
80
|
+
src_pos[:, 2] = 0.0 # z
|
|
81
|
+
|
|
82
|
+
# Detector center is opposite to source at distance (sdd - sid) from isocenter
|
|
83
|
+
idd = sdd - sid # Isocenter-to-Detector Distance
|
|
84
|
+
det_center[:, 0] = idd * sin_angles # x
|
|
85
|
+
det_center[:, 1] = -idd * cos_angles # y
|
|
86
|
+
det_center[:, 2] = 0.0 # z
|
|
87
|
+
|
|
88
|
+
# Detector u-direction (tangent to rotation, in xy-plane)
|
|
89
|
+
det_u_vec[:, 0] = cos_angles # x
|
|
90
|
+
det_u_vec[:, 1] = sin_angles # y
|
|
91
|
+
det_u_vec[:, 2] = 0.0 # z
|
|
92
|
+
|
|
93
|
+
# Detector v-direction (vertical, along z-axis)
|
|
94
|
+
det_v_vec[:, 0] = 0.0 # x
|
|
95
|
+
det_v_vec[:, 1] = 0.0 # y
|
|
96
|
+
det_v_vec[:, 2] = 1.0 # z
|
|
97
|
+
|
|
98
|
+
return src_pos, det_center, det_u_vec, det_v_vec
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def random_trajectory_3d(n_views, sid_mean, sdd_mean, sid_std=0.0, pos_std=0.0,
|
|
102
|
+
angle_std=0.0, device='cuda', dtype=torch.float32, seed=None):
|
|
103
|
+
"""Generate random trajectory geometry for cone-beam CT.
|
|
104
|
+
|
|
105
|
+
Creates source and detector position matrices with random perturbations,
|
|
106
|
+
useful for testing reconstruction with non-ideal trajectories or for
|
|
107
|
+
simulating robotic C-arm systems with positioning uncertainties.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
n_views : int
|
|
112
|
+
Number of projection views.
|
|
113
|
+
sid_mean : float
|
|
114
|
+
Mean Source-to-Isocenter Distance, in physical units.
|
|
115
|
+
sdd_mean : float
|
|
116
|
+
Mean Source-to-Detector Distance, in physical units.
|
|
117
|
+
sid_std : float, optional
|
|
118
|
+
Standard deviation for SID variations (default: 0.0).
|
|
119
|
+
pos_std : float, optional
|
|
120
|
+
Standard deviation for random position offsets (default: 0.0).
|
|
121
|
+
angle_std : float, optional
|
|
122
|
+
Standard deviation for angular perturbations in radians (default: 0.0).
|
|
123
|
+
device : str or torch.device, optional
|
|
124
|
+
Device for tensors (default: 'cuda').
|
|
125
|
+
dtype : torch.dtype, optional
|
|
126
|
+
Data type for tensors (default: torch.float32).
|
|
127
|
+
seed : int, optional
|
|
128
|
+
Random seed for reproducibility (default: None).
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
src_pos : torch.Tensor
|
|
133
|
+
Source positions, shape (n_views, 3).
|
|
134
|
+
det_center : torch.Tensor
|
|
135
|
+
Detector center positions, shape (n_views, 3).
|
|
136
|
+
det_u_vec : torch.Tensor
|
|
137
|
+
Detector u-direction unit vectors, shape (n_views, 3).
|
|
138
|
+
det_v_vec : torch.Tensor
|
|
139
|
+
Detector v-direction unit vectors, shape (n_views, 3).
|
|
140
|
+
|
|
141
|
+
Examples
|
|
142
|
+
--------
|
|
143
|
+
>>> # Generate random trajectory with 10% SID variation and 5mm position noise
|
|
144
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = random_trajectory_3d(
|
|
145
|
+
... n_views=180, sid_mean=1000.0, sdd_mean=1500.0,
|
|
146
|
+
... sid_std=100.0, pos_std=5.0, seed=42, device='cuda'
|
|
147
|
+
... )
|
|
148
|
+
"""
|
|
149
|
+
import math
|
|
150
|
+
|
|
151
|
+
if seed is not None:
|
|
152
|
+
torch.manual_seed(seed)
|
|
153
|
+
|
|
154
|
+
# Start with circular trajectory
|
|
155
|
+
src_pos, det_center, det_u_vec, det_v_vec = circular_trajectory_3d(
|
|
156
|
+
n_views, sid_mean, sdd_mean, device=device, dtype=dtype
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Add random perturbations
|
|
160
|
+
if sid_std > 0.0:
|
|
161
|
+
# Random SID variations
|
|
162
|
+
sid_perturbations = torch.randn(n_views, device=device, dtype=dtype) * sid_std
|
|
163
|
+
sdd_perturbations = sid_perturbations.clone() # Keep SDD-SID distance approximately constant
|
|
164
|
+
|
|
165
|
+
# Compute angles from existing positions
|
|
166
|
+
angles = torch.atan2(-src_pos[:, 0], src_pos[:, 1])
|
|
167
|
+
cos_angles = torch.cos(angles)
|
|
168
|
+
sin_angles = torch.sin(angles)
|
|
169
|
+
|
|
170
|
+
# Apply SID perturbations
|
|
171
|
+
sid_actual = sid_mean + sid_perturbations
|
|
172
|
+
src_pos[:, 0] = -sid_actual * sin_angles
|
|
173
|
+
src_pos[:, 1] = sid_actual * cos_angles
|
|
174
|
+
|
|
175
|
+
# Apply SDD perturbations
|
|
176
|
+
sdd_actual = sdd_mean + sdd_perturbations
|
|
177
|
+
idd = sdd_actual - sid_actual
|
|
178
|
+
det_center[:, 0] = idd * sin_angles
|
|
179
|
+
det_center[:, 1] = -idd * cos_angles
|
|
180
|
+
|
|
181
|
+
if pos_std > 0.0:
|
|
182
|
+
# Random 3D position offsets
|
|
183
|
+
src_pos += torch.randn_like(src_pos) * pos_std
|
|
184
|
+
det_center += torch.randn_like(det_center) * pos_std
|
|
185
|
+
|
|
186
|
+
if angle_std > 0.0:
|
|
187
|
+
# Random angular perturbations (rotate detector orientation)
|
|
188
|
+
angle_perturbations = torch.randn(n_views, device=device, dtype=dtype) * angle_std
|
|
189
|
+
cos_perturb = torch.cos(angle_perturbations)
|
|
190
|
+
sin_perturb = torch.sin(angle_perturbations)
|
|
191
|
+
|
|
192
|
+
# Rotate u and v vectors
|
|
193
|
+
u_new_x = det_u_vec[:, 0] * cos_perturb - det_u_vec[:, 1] * sin_perturb
|
|
194
|
+
u_new_y = det_u_vec[:, 0] * sin_perturb + det_u_vec[:, 1] * cos_perturb
|
|
195
|
+
det_u_vec[:, 0] = u_new_x
|
|
196
|
+
det_u_vec[:, 1] = u_new_y
|
|
197
|
+
|
|
198
|
+
# Renormalize direction vectors to ensure they remain unit vectors
|
|
199
|
+
det_u_vec = det_u_vec / torch.norm(det_u_vec, dim=1, keepdim=True)
|
|
200
|
+
det_v_vec = det_v_vec / torch.norm(det_v_vec, dim=1, keepdim=True)
|
|
201
|
+
|
|
202
|
+
return src_pos, det_center, det_u_vec, det_v_vec
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def spiral_trajectory_3d(n_views, sid, sdd, z_range=100.0, n_turns=2.0,
|
|
206
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
207
|
+
"""Generate spiral (helical) trajectory geometry for cone-beam CT.
|
|
208
|
+
|
|
209
|
+
Creates a helical trajectory where the source moves in a spiral pattern
|
|
210
|
+
around the z-axis, commonly used in helical CT scanners.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
n_views : int
|
|
215
|
+
Number of projection views.
|
|
216
|
+
sid : float
|
|
217
|
+
Source-to-Isocenter Distance (SID), in physical units.
|
|
218
|
+
sdd : float
|
|
219
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
220
|
+
z_range : float, optional
|
|
221
|
+
Total z-axis range covered during the spiral (default: 100.0).
|
|
222
|
+
n_turns : float, optional
|
|
223
|
+
Number of complete rotations (default: 2.0).
|
|
224
|
+
start_angle : float, optional
|
|
225
|
+
Starting angle in radians (default: 0.0).
|
|
226
|
+
device : str or torch.device, optional
|
|
227
|
+
Device for tensors (default: 'cuda').
|
|
228
|
+
dtype : torch.dtype, optional
|
|
229
|
+
Data type for tensors (default: torch.float32).
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
src_pos : torch.Tensor
|
|
234
|
+
Source positions, shape (n_views, 3).
|
|
235
|
+
det_center : torch.Tensor
|
|
236
|
+
Detector center positions, shape (n_views, 3).
|
|
237
|
+
det_u_vec : torch.Tensor
|
|
238
|
+
Detector u-direction unit vectors, shape (n_views, 3).
|
|
239
|
+
det_v_vec : torch.Tensor
|
|
240
|
+
Detector v-direction unit vectors, shape (n_views, 3).
|
|
241
|
+
|
|
242
|
+
Examples
|
|
243
|
+
--------
|
|
244
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = spiral_trajectory_3d(
|
|
245
|
+
... n_views=360, sid=1000.0, sdd=1500.0, z_range=200.0, n_turns=3.0
|
|
246
|
+
... )
|
|
247
|
+
"""
|
|
248
|
+
# Generate angles for spiral motion
|
|
249
|
+
end_angle = start_angle + 2 * math.pi * n_turns
|
|
250
|
+
step = (end_angle - start_angle) / n_views
|
|
251
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
252
|
+
|
|
253
|
+
# Generate z positions (linear motion along z-axis)
|
|
254
|
+
z_positions = torch.linspace(-z_range/2, z_range/2, n_views, device=device, dtype=dtype)
|
|
255
|
+
|
|
256
|
+
# Preallocate position matrices
|
|
257
|
+
src_pos = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
258
|
+
det_center = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
259
|
+
det_u_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
260
|
+
det_v_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
261
|
+
|
|
262
|
+
# Compute trigonometric values
|
|
263
|
+
cos_angles = torch.cos(angles)
|
|
264
|
+
sin_angles = torch.sin(angles)
|
|
265
|
+
|
|
266
|
+
# Source rotates around z-axis with z-motion
|
|
267
|
+
src_pos[:, 0] = -sid * sin_angles
|
|
268
|
+
src_pos[:, 1] = sid * cos_angles
|
|
269
|
+
src_pos[:, 2] = z_positions
|
|
270
|
+
|
|
271
|
+
# Detector center opposite to source
|
|
272
|
+
idd = sdd - sid
|
|
273
|
+
det_center[:, 0] = idd * sin_angles
|
|
274
|
+
det_center[:, 1] = -idd * cos_angles
|
|
275
|
+
det_center[:, 2] = z_positions
|
|
276
|
+
|
|
277
|
+
# Detector u-direction (tangent to rotation)
|
|
278
|
+
det_u_vec[:, 0] = cos_angles
|
|
279
|
+
det_u_vec[:, 1] = sin_angles
|
|
280
|
+
det_u_vec[:, 2] = 0.0
|
|
281
|
+
|
|
282
|
+
# Detector v-direction (vertical)
|
|
283
|
+
det_v_vec[:, 0] = 0.0
|
|
284
|
+
det_v_vec[:, 1] = 0.0
|
|
285
|
+
det_v_vec[:, 2] = 1.0
|
|
286
|
+
|
|
287
|
+
return src_pos, det_center, det_u_vec, det_v_vec
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def sinusoidal_trajectory_3d(n_views, sid, sdd, amplitude=50.0, frequency=2.0,
|
|
291
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
292
|
+
"""Generate sinusoidal trajectory geometry for cone-beam CT.
|
|
293
|
+
|
|
294
|
+
Creates a trajectory where the source follows a sinusoidal path in 3D space,
|
|
295
|
+
with radial oscillations as it rotates around the z-axis.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
n_views : int
|
|
300
|
+
Number of projection views.
|
|
301
|
+
sid : float
|
|
302
|
+
Mean Source-to-Isocenter Distance (SID), in physical units.
|
|
303
|
+
sdd : float
|
|
304
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
305
|
+
amplitude : float, optional
|
|
306
|
+
Amplitude of sinusoidal radial variation (default: 50.0).
|
|
307
|
+
frequency : float, optional
|
|
308
|
+
Number of oscillation cycles per full rotation (default: 2.0).
|
|
309
|
+
start_angle : float, optional
|
|
310
|
+
Starting angle in radians (default: 0.0).
|
|
311
|
+
device : str or torch.device, optional
|
|
312
|
+
Device for tensors (default: 'cuda').
|
|
313
|
+
dtype : torch.dtype, optional
|
|
314
|
+
Data type for tensors (default: torch.float32).
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
src_pos : torch.Tensor
|
|
319
|
+
Source positions, shape (n_views, 3).
|
|
320
|
+
det_center : torch.Tensor
|
|
321
|
+
Detector center positions, shape (n_views, 3).
|
|
322
|
+
det_u_vec : torch.Tensor
|
|
323
|
+
Detector u-direction unit vectors, shape (n_views, 3).
|
|
324
|
+
det_v_vec : torch.Tensor
|
|
325
|
+
Detector v-direction unit vectors, shape (n_views, 3).
|
|
326
|
+
|
|
327
|
+
Examples
|
|
328
|
+
--------
|
|
329
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = sinusoidal_trajectory_3d(
|
|
330
|
+
... n_views=360, sid=1000.0, sdd=1500.0, amplitude=100.0, frequency=3.0
|
|
331
|
+
... )
|
|
332
|
+
"""
|
|
333
|
+
# Generate angles
|
|
334
|
+
end_angle = start_angle + 2 * math.pi
|
|
335
|
+
step = (end_angle - start_angle) / n_views
|
|
336
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
337
|
+
|
|
338
|
+
# Compute sinusoidal radial variation
|
|
339
|
+
radial_variation = amplitude * torch.sin(frequency * angles)
|
|
340
|
+
sid_varying = sid + radial_variation
|
|
341
|
+
|
|
342
|
+
# Preallocate position matrices
|
|
343
|
+
src_pos = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
344
|
+
det_center = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
345
|
+
det_u_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
346
|
+
det_v_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
347
|
+
|
|
348
|
+
# Compute trigonometric values
|
|
349
|
+
cos_angles = torch.cos(angles)
|
|
350
|
+
sin_angles = torch.sin(angles)
|
|
351
|
+
|
|
352
|
+
# Source with sinusoidal radial variation
|
|
353
|
+
src_pos[:, 0] = -sid_varying * sin_angles
|
|
354
|
+
src_pos[:, 1] = sid_varying * cos_angles
|
|
355
|
+
src_pos[:, 2] = 0.0
|
|
356
|
+
|
|
357
|
+
# Detector center
|
|
358
|
+
idd = sdd - sid_varying
|
|
359
|
+
det_center[:, 0] = idd * sin_angles
|
|
360
|
+
det_center[:, 1] = -idd * cos_angles
|
|
361
|
+
det_center[:, 2] = 0.0
|
|
362
|
+
|
|
363
|
+
# Detector u-direction
|
|
364
|
+
det_u_vec[:, 0] = cos_angles
|
|
365
|
+
det_u_vec[:, 1] = sin_angles
|
|
366
|
+
det_u_vec[:, 2] = 0.0
|
|
367
|
+
|
|
368
|
+
# Detector v-direction
|
|
369
|
+
det_v_vec[:, 0] = 0.0
|
|
370
|
+
det_v_vec[:, 1] = 0.0
|
|
371
|
+
det_v_vec[:, 2] = 1.0
|
|
372
|
+
|
|
373
|
+
return src_pos, det_center, det_u_vec, det_v_vec
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def saddle_trajectory_3d(n_views, sid, sdd, z_amplitude=50.0, radial_amplitude=30.0,
|
|
377
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
378
|
+
"""Generate saddle-shaped trajectory geometry for cone-beam CT.
|
|
379
|
+
|
|
380
|
+
Creates a saddle-shaped trajectory where the source moves in both z and radial
|
|
381
|
+
directions following a saddle surface (hyperbolic paraboloid).
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
n_views : int
|
|
386
|
+
Number of projection views.
|
|
387
|
+
sid : float
|
|
388
|
+
Mean Source-to-Isocenter Distance (SID), in physical units.
|
|
389
|
+
sdd : float
|
|
390
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
391
|
+
z_amplitude : float, optional
|
|
392
|
+
Amplitude of z-axis variation (default: 50.0).
|
|
393
|
+
radial_amplitude : float, optional
|
|
394
|
+
Amplitude of radial variation (default: 30.0).
|
|
395
|
+
start_angle : float, optional
|
|
396
|
+
Starting angle in radians (default: 0.0).
|
|
397
|
+
device : str or torch.device, optional
|
|
398
|
+
Device for tensors (default: 'cuda').
|
|
399
|
+
dtype : torch.dtype, optional
|
|
400
|
+
Data type for tensors (default: torch.float32).
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
src_pos : torch.Tensor
|
|
405
|
+
Source positions, shape (n_views, 3).
|
|
406
|
+
det_center : torch.Tensor
|
|
407
|
+
Detector center positions, shape (n_views, 3).
|
|
408
|
+
det_u_vec : torch.Tensor
|
|
409
|
+
Detector u-direction unit vectors, shape (n_views, 3).
|
|
410
|
+
det_v_vec : torch.Tensor
|
|
411
|
+
Detector v-direction unit vectors, shape (n_views, 3).
|
|
412
|
+
|
|
413
|
+
Examples
|
|
414
|
+
--------
|
|
415
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = saddle_trajectory_3d(
|
|
416
|
+
... n_views=360, sid=1000.0, sdd=1500.0, z_amplitude=80.0, radial_amplitude=50.0
|
|
417
|
+
... )
|
|
418
|
+
"""
|
|
419
|
+
# Generate angles
|
|
420
|
+
end_angle = start_angle + 2 * math.pi
|
|
421
|
+
step = (end_angle - start_angle) / n_views
|
|
422
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
423
|
+
|
|
424
|
+
# Saddle surface: z varies with cos(2*theta), radius varies with sin(2*theta)
|
|
425
|
+
z_positions = z_amplitude * torch.cos(2 * angles)
|
|
426
|
+
radial_variation = radial_amplitude * torch.sin(2 * angles)
|
|
427
|
+
sid_varying = sid + radial_variation
|
|
428
|
+
|
|
429
|
+
# Preallocate position matrices
|
|
430
|
+
src_pos = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
431
|
+
det_center = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
432
|
+
det_u_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
433
|
+
det_v_vec = torch.zeros((n_views, 3), device=device, dtype=dtype)
|
|
434
|
+
|
|
435
|
+
# Compute trigonometric values
|
|
436
|
+
cos_angles = torch.cos(angles)
|
|
437
|
+
sin_angles = torch.sin(angles)
|
|
438
|
+
|
|
439
|
+
# Source with saddle shape
|
|
440
|
+
src_pos[:, 0] = -sid_varying * sin_angles
|
|
441
|
+
src_pos[:, 1] = sid_varying * cos_angles
|
|
442
|
+
src_pos[:, 2] = z_positions
|
|
443
|
+
|
|
444
|
+
# Detector center
|
|
445
|
+
idd = sdd - sid_varying
|
|
446
|
+
det_center[:, 0] = idd * sin_angles
|
|
447
|
+
det_center[:, 1] = -idd * cos_angles
|
|
448
|
+
det_center[:, 2] = z_positions
|
|
449
|
+
|
|
450
|
+
# Detector u-direction
|
|
451
|
+
det_u_vec[:, 0] = cos_angles
|
|
452
|
+
det_u_vec[:, 1] = sin_angles
|
|
453
|
+
det_u_vec[:, 2] = 0.0
|
|
454
|
+
|
|
455
|
+
# Detector v-direction
|
|
456
|
+
det_v_vec[:, 0] = 0.0
|
|
457
|
+
det_v_vec[:, 1] = 0.0
|
|
458
|
+
det_v_vec[:, 2] = 1.0
|
|
459
|
+
|
|
460
|
+
return src_pos, det_center, det_u_vec, det_v_vec
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def custom_trajectory_3d(n_views, sid, sdd,
|
|
464
|
+
source_path_fn: Callable[[torch.Tensor, float], torch.Tensor],
|
|
465
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
466
|
+
"""Generate custom trajectory geometry for cone-beam CT using user-defined function.
|
|
467
|
+
|
|
468
|
+
Creates a trajectory based on a user-provided function that defines the source
|
|
469
|
+
position as a function of angle and SID.
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
n_views : int
|
|
474
|
+
Number of projection views.
|
|
475
|
+
sid : float
|
|
476
|
+
Source-to-Isocenter Distance (SID), in physical units.
|
|
477
|
+
sdd : float
|
|
478
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
479
|
+
source_path_fn : Callable[[torch.Tensor, float], torch.Tensor]
|
|
480
|
+
Function that takes (angles, sid) and returns source positions (n_views, 3).
|
|
481
|
+
The function signature should be: f(angles: torch.Tensor, sid: float) -> torch.Tensor
|
|
482
|
+
where angles has shape (n_views,) and output has shape (n_views, 3).
|
|
483
|
+
start_angle : float, optional
|
|
484
|
+
Starting angle in radians (default: 0.0).
|
|
485
|
+
device : str or torch.device, optional
|
|
486
|
+
Device for tensors (default: 'cuda').
|
|
487
|
+
dtype : torch.dtype, optional
|
|
488
|
+
Data type for tensors (default: torch.float32).
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
src_pos : torch.Tensor
|
|
493
|
+
Source positions, shape (n_views, 3).
|
|
494
|
+
det_center : torch.Tensor
|
|
495
|
+
Detector center positions, shape (n_views, 3).
|
|
496
|
+
det_u_vec : torch.Tensor
|
|
497
|
+
Detector u-direction unit vectors, shape (n_views, 3).
|
|
498
|
+
det_v_vec : torch.Tensor
|
|
499
|
+
Detector v-direction unit vectors, shape (n_views, 3).
|
|
500
|
+
|
|
501
|
+
Examples
|
|
502
|
+
--------
|
|
503
|
+
>>> # Define a custom figure-8 trajectory
|
|
504
|
+
>>> def figure8_path(angles, sid):
|
|
505
|
+
... src_pos = torch.zeros((len(angles), 3), device=angles.device, dtype=angles.dtype)
|
|
506
|
+
... src_pos[:, 0] = -sid * torch.sin(angles)
|
|
507
|
+
... src_pos[:, 1] = sid * torch.cos(angles) * torch.sin(angles)
|
|
508
|
+
... src_pos[:, 2] = 50 * torch.sin(2 * angles)
|
|
509
|
+
... return src_pos
|
|
510
|
+
>>>
|
|
511
|
+
>>> src_pos, det_center, det_u_vec, det_v_vec = custom_trajectory_3d(
|
|
512
|
+
... n_views=360, sid=1000.0, sdd=1500.0, source_path_fn=figure8_path
|
|
513
|
+
... )
|
|
514
|
+
"""
|
|
515
|
+
# Generate angles
|
|
516
|
+
end_angle = start_angle + 2 * math.pi
|
|
517
|
+
step = (end_angle - start_angle) / n_views
|
|
518
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
519
|
+
|
|
520
|
+
# Get source positions from user-defined function
|
|
521
|
+
src_pos = source_path_fn(angles, sid)
|
|
522
|
+
|
|
523
|
+
if src_pos.shape != (n_views, 3):
|
|
524
|
+
raise ValueError(f"source_path_fn must return tensor of shape ({n_views}, 3), "
|
|
525
|
+
f"got {src_pos.shape}")
|
|
526
|
+
|
|
527
|
+
# Keep the entire frame in the source-path autograd graph. Constructing
|
|
528
|
+
# tensors from scalar tensors detaches them; rewriting view slices in
|
|
529
|
+
# place also invalidates tensors saved by cross/norm for backward.
|
|
530
|
+
src_unit = src_pos / torch.linalg.vector_norm(src_pos, dim=1, keepdim=True)
|
|
531
|
+
det_center = src_pos - sdd * src_unit
|
|
532
|
+
u_unnorm = torch.stack((-src_pos[:, 1], src_pos[:, 0],
|
|
533
|
+
torch.zeros_like(src_pos[:, 0])), dim=1)
|
|
534
|
+
along_z = (src_pos[:, :2].abs() < 1e-6).all(dim=1, keepdim=True)
|
|
535
|
+
u_unnorm = torch.where(along_z, src_pos.new_tensor([1.0, 0.0, 0.0]), u_unnorm)
|
|
536
|
+
det_u_vec = u_unnorm / torch.linalg.vector_norm(u_unnorm, dim=1, keepdim=True)
|
|
537
|
+
det_v_vec = torch.cross(src_unit, det_u_vec, dim=1)
|
|
538
|
+
det_v_vec = det_v_vec / torch.linalg.vector_norm(det_v_vec, dim=1, keepdim=True)
|
|
539
|
+
|
|
540
|
+
return src_pos, det_center, det_u_vec, det_v_vec
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
# ============================================================================
|
|
544
|
+
# 2D Fan Beam Trajectory Generation Functions
|
|
545
|
+
# ============================================================================
|
|
546
|
+
|
|
547
|
+
def circular_trajectory_2d_fan(n_views, sid, sdd, start_angle=0.0, end_angle=None, device='cuda', dtype=torch.float32):
|
|
548
|
+
"""Generate circular trajectory geometry for 2D fan-beam CT.
|
|
549
|
+
|
|
550
|
+
Creates source and detector position matrices for a standard circular
|
|
551
|
+
orbit around the origin, commonly used in 2D fan-beam CT imaging.
|
|
552
|
+
|
|
553
|
+
Parameters
|
|
554
|
+
----------
|
|
555
|
+
n_views : int
|
|
556
|
+
Number of projection views.
|
|
557
|
+
sid : float
|
|
558
|
+
Source-to-Isocenter Distance (SID), in physical units.
|
|
559
|
+
sdd : float
|
|
560
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
561
|
+
start_angle : float, optional
|
|
562
|
+
Starting angle in radians (default: 0.0).
|
|
563
|
+
end_angle : float, optional
|
|
564
|
+
Ending angle in radians (default: 2*pi, full rotation).
|
|
565
|
+
device : str or torch.device, optional
|
|
566
|
+
Device for tensors (default: 'cuda').
|
|
567
|
+
dtype : torch.dtype, optional
|
|
568
|
+
Data type for tensors (default: torch.float32).
|
|
569
|
+
|
|
570
|
+
Returns
|
|
571
|
+
-------
|
|
572
|
+
src_pos : torch.Tensor
|
|
573
|
+
Source positions, shape (n_views, 2).
|
|
574
|
+
det_center : torch.Tensor
|
|
575
|
+
Detector center positions, shape (n_views, 2).
|
|
576
|
+
det_u_vec : torch.Tensor
|
|
577
|
+
Detector u-direction unit vectors, shape (n_views, 2).
|
|
578
|
+
|
|
579
|
+
Examples
|
|
580
|
+
--------
|
|
581
|
+
>>> src_pos, det_center, det_u_vec = circular_trajectory_2d_fan(
|
|
582
|
+
... n_views=360, sid=1000.0, sdd=1500.0, device='cuda'
|
|
583
|
+
... )
|
|
584
|
+
>>> print(src_pos.shape) # (360, 2)
|
|
585
|
+
"""
|
|
586
|
+
if end_angle is None:
|
|
587
|
+
end_angle = 2 * math.pi
|
|
588
|
+
|
|
589
|
+
# Generate angles
|
|
590
|
+
step = (end_angle - start_angle) / n_views
|
|
591
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
592
|
+
|
|
593
|
+
# Preallocate position matrices
|
|
594
|
+
src_pos = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
595
|
+
det_center = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
596
|
+
det_u_vec = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
597
|
+
|
|
598
|
+
# Compute trigonometric values
|
|
599
|
+
cos_angles = torch.cos(angles)
|
|
600
|
+
sin_angles = torch.sin(angles)
|
|
601
|
+
|
|
602
|
+
# Source rotates around isocenter at distance sid
|
|
603
|
+
src_pos[:, 0] = -sid * sin_angles # x
|
|
604
|
+
src_pos[:, 1] = sid * cos_angles # y
|
|
605
|
+
|
|
606
|
+
# Detector center is opposite to source at distance (sdd - sid) from isocenter
|
|
607
|
+
idd = sdd - sid # Isocenter-to-Detector Distance
|
|
608
|
+
det_center[:, 0] = idd * sin_angles # x
|
|
609
|
+
det_center[:, 1] = -idd * cos_angles # y
|
|
610
|
+
|
|
611
|
+
# Detector u-direction (tangent to rotation)
|
|
612
|
+
det_u_vec[:, 0] = cos_angles # x
|
|
613
|
+
det_u_vec[:, 1] = sin_angles # y
|
|
614
|
+
|
|
615
|
+
return src_pos, det_center, det_u_vec
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def sinusoidal_trajectory_2d_fan(n_views, sid, sdd, amplitude=50.0, frequency=2.0,
|
|
619
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
620
|
+
"""Generate sinusoidal trajectory geometry for 2D fan-beam CT.
|
|
621
|
+
|
|
622
|
+
Creates a trajectory where the source follows a sinusoidal path,
|
|
623
|
+
with radial oscillations as it rotates around the origin.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
n_views : int
|
|
628
|
+
Number of projection views.
|
|
629
|
+
sid : float
|
|
630
|
+
Mean Source-to-Isocenter Distance (SID), in physical units.
|
|
631
|
+
sdd : float
|
|
632
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
633
|
+
amplitude : float, optional
|
|
634
|
+
Amplitude of sinusoidal radial variation (default: 50.0).
|
|
635
|
+
frequency : float, optional
|
|
636
|
+
Number of oscillation cycles per full rotation (default: 2.0).
|
|
637
|
+
start_angle : float, optional
|
|
638
|
+
Starting angle in radians (default: 0.0).
|
|
639
|
+
device : str or torch.device, optional
|
|
640
|
+
Device for tensors (default: 'cuda').
|
|
641
|
+
dtype : torch.dtype, optional
|
|
642
|
+
Data type for tensors (default: torch.float32).
|
|
643
|
+
|
|
644
|
+
Returns
|
|
645
|
+
-------
|
|
646
|
+
src_pos : torch.Tensor
|
|
647
|
+
Source positions, shape (n_views, 2).
|
|
648
|
+
det_center : torch.Tensor
|
|
649
|
+
Detector center positions, shape (n_views, 2).
|
|
650
|
+
det_u_vec : torch.Tensor
|
|
651
|
+
Detector u-direction unit vectors, shape (n_views, 2).
|
|
652
|
+
|
|
653
|
+
Examples
|
|
654
|
+
--------
|
|
655
|
+
>>> src_pos, det_center, det_u_vec = sinusoidal_trajectory_2d_fan(
|
|
656
|
+
... n_views=360, sid=1000.0, sdd=1500.0, amplitude=100.0, frequency=3.0
|
|
657
|
+
... )
|
|
658
|
+
"""
|
|
659
|
+
# Generate angles
|
|
660
|
+
end_angle = start_angle + 2 * math.pi
|
|
661
|
+
step = (end_angle - start_angle) / n_views
|
|
662
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
663
|
+
|
|
664
|
+
# Compute sinusoidal radial variation
|
|
665
|
+
radial_variation = amplitude * torch.sin(frequency * angles)
|
|
666
|
+
sid_varying = sid + radial_variation
|
|
667
|
+
|
|
668
|
+
# Preallocate position matrices
|
|
669
|
+
src_pos = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
670
|
+
det_center = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
671
|
+
det_u_vec = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
672
|
+
|
|
673
|
+
# Compute trigonometric values
|
|
674
|
+
cos_angles = torch.cos(angles)
|
|
675
|
+
sin_angles = torch.sin(angles)
|
|
676
|
+
|
|
677
|
+
# Source with sinusoidal radial variation
|
|
678
|
+
src_pos[:, 0] = -sid_varying * sin_angles
|
|
679
|
+
src_pos[:, 1] = sid_varying * cos_angles
|
|
680
|
+
|
|
681
|
+
# Detector center
|
|
682
|
+
idd = sdd - sid_varying
|
|
683
|
+
det_center[:, 0] = idd * sin_angles
|
|
684
|
+
det_center[:, 1] = -idd * cos_angles
|
|
685
|
+
|
|
686
|
+
# Detector u-direction
|
|
687
|
+
det_u_vec[:, 0] = cos_angles
|
|
688
|
+
det_u_vec[:, 1] = sin_angles
|
|
689
|
+
|
|
690
|
+
return src_pos, det_center, det_u_vec
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def custom_trajectory_2d_fan(n_views, sid, sdd,
|
|
694
|
+
source_path_fn: Callable[[torch.Tensor, float], torch.Tensor],
|
|
695
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
696
|
+
"""Generate custom trajectory geometry for 2D fan-beam CT using user-defined function.
|
|
697
|
+
|
|
698
|
+
Creates a trajectory based on a user-provided function that defines the source
|
|
699
|
+
position as a function of angle and SID.
|
|
700
|
+
|
|
701
|
+
Parameters
|
|
702
|
+
----------
|
|
703
|
+
n_views : int
|
|
704
|
+
Number of projection views.
|
|
705
|
+
sid : float
|
|
706
|
+
Source-to-Isocenter Distance (SID), in physical units.
|
|
707
|
+
sdd : float
|
|
708
|
+
Source-to-Detector Distance (SDD), in physical units.
|
|
709
|
+
source_path_fn : Callable[[torch.Tensor, float], torch.Tensor]
|
|
710
|
+
Function that takes (angles, sid) and returns source positions (n_views, 2).
|
|
711
|
+
The function signature should be: f(angles: torch.Tensor, sid: float) -> torch.Tensor
|
|
712
|
+
where angles has shape (n_views,) and output has shape (n_views, 2).
|
|
713
|
+
start_angle : float, optional
|
|
714
|
+
Starting angle in radians (default: 0.0).
|
|
715
|
+
device : str or torch.device, optional
|
|
716
|
+
Device for tensors (default: 'cuda').
|
|
717
|
+
dtype : torch.dtype, optional
|
|
718
|
+
Data type for tensors (default: torch.float32).
|
|
719
|
+
|
|
720
|
+
Returns
|
|
721
|
+
-------
|
|
722
|
+
src_pos : torch.Tensor
|
|
723
|
+
Source positions, shape (n_views, 2).
|
|
724
|
+
det_center : torch.Tensor
|
|
725
|
+
Detector center positions, shape (n_views, 2).
|
|
726
|
+
det_u_vec : torch.Tensor
|
|
727
|
+
Detector u-direction unit vectors, shape (n_views, 2).
|
|
728
|
+
|
|
729
|
+
Examples
|
|
730
|
+
--------
|
|
731
|
+
>>> # Define a custom elliptical trajectory
|
|
732
|
+
>>> def ellipse_path(angles, sid):
|
|
733
|
+
... src_pos = torch.zeros((len(angles), 2), device=angles.device, dtype=angles.dtype)
|
|
734
|
+
... src_pos[:, 0] = -sid * 1.5 * torch.sin(angles) # wider in x
|
|
735
|
+
... src_pos[:, 1] = sid * torch.cos(angles)
|
|
736
|
+
... return src_pos
|
|
737
|
+
>>>
|
|
738
|
+
>>> src_pos, det_center, det_u_vec = custom_trajectory_2d_fan(
|
|
739
|
+
... n_views=360, sid=1000.0, sdd=1500.0, source_path_fn=ellipse_path
|
|
740
|
+
... )
|
|
741
|
+
"""
|
|
742
|
+
# Generate angles
|
|
743
|
+
end_angle = start_angle + 2 * math.pi
|
|
744
|
+
step = (end_angle - start_angle) / n_views
|
|
745
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
746
|
+
|
|
747
|
+
# Get source positions from user-defined function
|
|
748
|
+
src_pos = source_path_fn(angles, sid)
|
|
749
|
+
|
|
750
|
+
if src_pos.shape != (n_views, 2):
|
|
751
|
+
raise ValueError(f"source_path_fn must return tensor of shape ({n_views}, 2), "
|
|
752
|
+
f"got {src_pos.shape}")
|
|
753
|
+
|
|
754
|
+
# Preallocate remaining position matrices
|
|
755
|
+
det_center = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
756
|
+
det_u_vec = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
757
|
+
|
|
758
|
+
# For each view, compute detector position and orientation
|
|
759
|
+
for i in range(n_views):
|
|
760
|
+
# Vector from isocenter to source
|
|
761
|
+
src_vec = src_pos[i]
|
|
762
|
+
src_vec_norm = torch.norm(src_vec)
|
|
763
|
+
src_unit = src_vec / src_vec_norm
|
|
764
|
+
|
|
765
|
+
# Detector center is opposite to source
|
|
766
|
+
det_center[i] = -src_unit * (sdd - src_vec_norm)
|
|
767
|
+
|
|
768
|
+
# Detector u-direction: perpendicular to source direction (rotate 90 degrees)
|
|
769
|
+
det_u_vec[i, 0] = -src_unit[1] # perpendicular: (-y, x)
|
|
770
|
+
det_u_vec[i, 1] = src_unit[0]
|
|
771
|
+
|
|
772
|
+
return src_pos, det_center, det_u_vec
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# ============================================================================
|
|
776
|
+
# 2D Parallel Beam Trajectory Generation Functions
|
|
777
|
+
# ============================================================================
|
|
778
|
+
|
|
779
|
+
def circular_trajectory_2d_parallel(n_views, detector_distance=0.0, start_angle=0.0, end_angle=None,
|
|
780
|
+
device='cuda', dtype=torch.float32):
|
|
781
|
+
"""Generate circular trajectory geometry for 2D parallel-beam CT.
|
|
782
|
+
|
|
783
|
+
Creates ray direction and detector position matrices for a standard circular
|
|
784
|
+
scanning geometry, commonly used in 2D parallel-beam CT imaging.
|
|
785
|
+
|
|
786
|
+
Parameters
|
|
787
|
+
----------
|
|
788
|
+
n_views : int
|
|
789
|
+
Number of projection views.
|
|
790
|
+
detector_distance : float, optional
|
|
791
|
+
Distance from isocenter to detector origin (default: 0.0).
|
|
792
|
+
start_angle : float, optional
|
|
793
|
+
Starting angle in radians (default: 0.0).
|
|
794
|
+
end_angle : float, optional
|
|
795
|
+
Ending angle in radians (default: pi, half rotation for parallel beam).
|
|
796
|
+
device : str or torch.device, optional
|
|
797
|
+
Device for tensors (default: 'cuda').
|
|
798
|
+
dtype : torch.dtype, optional
|
|
799
|
+
Data type for tensors (default: torch.float32).
|
|
800
|
+
|
|
801
|
+
Returns
|
|
802
|
+
-------
|
|
803
|
+
ray_dir : torch.Tensor
|
|
804
|
+
Ray direction unit vectors for each view, shape (n_views, 2).
|
|
805
|
+
det_origin : torch.Tensor
|
|
806
|
+
Detector origin positions for each view, shape (n_views, 2).
|
|
807
|
+
det_u_vec : torch.Tensor
|
|
808
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
809
|
+
|
|
810
|
+
Examples
|
|
811
|
+
--------
|
|
812
|
+
>>> ray_dir, det_origin, det_u_vec = circular_trajectory_2d_parallel(
|
|
813
|
+
... n_views=180, device='cuda'
|
|
814
|
+
... )
|
|
815
|
+
>>> print(ray_dir.shape) # (180, 2)
|
|
816
|
+
"""
|
|
817
|
+
if end_angle is None:
|
|
818
|
+
end_angle = math.pi # Half rotation for parallel beam
|
|
819
|
+
|
|
820
|
+
# Generate angles
|
|
821
|
+
step = (end_angle - start_angle) / n_views
|
|
822
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
823
|
+
|
|
824
|
+
# Preallocate position matrices
|
|
825
|
+
ray_dir = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
826
|
+
det_origin = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
827
|
+
det_u_vec = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
828
|
+
|
|
829
|
+
# Compute trigonometric values
|
|
830
|
+
cos_angles = torch.cos(angles)
|
|
831
|
+
sin_angles = torch.sin(angles)
|
|
832
|
+
|
|
833
|
+
# Ray direction: perpendicular to detector
|
|
834
|
+
ray_dir[:, 0] = cos_angles
|
|
835
|
+
ray_dir[:, 1] = sin_angles
|
|
836
|
+
|
|
837
|
+
# Detector origin: offset from isocenter perpendicular to ray direction
|
|
838
|
+
det_origin[:, 0] = -detector_distance * sin_angles
|
|
839
|
+
det_origin[:, 1] = detector_distance * cos_angles
|
|
840
|
+
|
|
841
|
+
# Detector u-direction: tangent to rotation (perpendicular to ray)
|
|
842
|
+
det_u_vec[:, 0] = -sin_angles
|
|
843
|
+
det_u_vec[:, 1] = cos_angles
|
|
844
|
+
|
|
845
|
+
return ray_dir, det_origin, det_u_vec
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def sinusoidal_trajectory_2d_parallel(n_views, amplitude=50.0, frequency=2.0,
|
|
849
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
850
|
+
"""Generate sinusoidal trajectory geometry for 2D parallel-beam CT.
|
|
851
|
+
|
|
852
|
+
Creates a trajectory where the detector follows a sinusoidal path
|
|
853
|
+
while maintaining parallel rays.
|
|
854
|
+
|
|
855
|
+
Parameters
|
|
856
|
+
----------
|
|
857
|
+
n_views : int
|
|
858
|
+
Number of projection views.
|
|
859
|
+
amplitude : float, optional
|
|
860
|
+
Amplitude of sinusoidal detector displacement (default: 50.0).
|
|
861
|
+
frequency : float, optional
|
|
862
|
+
Number of oscillation cycles per rotation (default: 2.0).
|
|
863
|
+
start_angle : float, optional
|
|
864
|
+
Starting angle in radians (default: 0.0).
|
|
865
|
+
device : str or torch.device, optional
|
|
866
|
+
Device for tensors (default: 'cuda').
|
|
867
|
+
dtype : torch.dtype, optional
|
|
868
|
+
Data type for tensors (default: torch.float32).
|
|
869
|
+
|
|
870
|
+
Returns
|
|
871
|
+
-------
|
|
872
|
+
ray_dir : torch.Tensor
|
|
873
|
+
Ray direction unit vectors for each view, shape (n_views, 2).
|
|
874
|
+
det_origin : torch.Tensor
|
|
875
|
+
Detector origin positions for each view, shape (n_views, 2).
|
|
876
|
+
det_u_vec : torch.Tensor
|
|
877
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
878
|
+
|
|
879
|
+
Examples
|
|
880
|
+
--------
|
|
881
|
+
>>> ray_dir, det_origin, det_u_vec = sinusoidal_trajectory_2d_parallel(
|
|
882
|
+
... n_views=180, amplitude=100.0, frequency=3.0
|
|
883
|
+
... )
|
|
884
|
+
"""
|
|
885
|
+
# Generate angles
|
|
886
|
+
end_angle = start_angle + math.pi
|
|
887
|
+
step = (end_angle - start_angle) / n_views
|
|
888
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
889
|
+
|
|
890
|
+
# Compute sinusoidal detector displacement
|
|
891
|
+
detector_offset = amplitude * torch.sin(frequency * angles)
|
|
892
|
+
|
|
893
|
+
# Preallocate position matrices
|
|
894
|
+
ray_dir = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
895
|
+
det_origin = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
896
|
+
det_u_vec = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
897
|
+
|
|
898
|
+
# Compute trigonometric values
|
|
899
|
+
cos_angles = torch.cos(angles)
|
|
900
|
+
sin_angles = torch.sin(angles)
|
|
901
|
+
|
|
902
|
+
# Ray direction: perpendicular to detector
|
|
903
|
+
ray_dir[:, 0] = cos_angles
|
|
904
|
+
ray_dir[:, 1] = sin_angles
|
|
905
|
+
|
|
906
|
+
# Detector origin with sinusoidal offset
|
|
907
|
+
det_origin[:, 0] = -detector_offset * sin_angles
|
|
908
|
+
det_origin[:, 1] = detector_offset * cos_angles
|
|
909
|
+
|
|
910
|
+
# Detector u-direction: tangent to rotation
|
|
911
|
+
det_u_vec[:, 0] = -sin_angles
|
|
912
|
+
det_u_vec[:, 1] = cos_angles
|
|
913
|
+
|
|
914
|
+
return ray_dir, det_origin, det_u_vec
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def custom_trajectory_2d_parallel(n_views,
|
|
918
|
+
ray_dir_fn: Callable[[torch.Tensor], torch.Tensor],
|
|
919
|
+
det_origin_fn: Callable[[torch.Tensor], torch.Tensor],
|
|
920
|
+
start_angle=0.0, device='cuda', dtype=torch.float32):
|
|
921
|
+
"""Generate custom trajectory geometry for 2D parallel-beam CT using user-defined functions.
|
|
922
|
+
|
|
923
|
+
Creates a trajectory based on user-provided functions that define the ray
|
|
924
|
+
direction and detector origin as functions of angle.
|
|
925
|
+
|
|
926
|
+
Parameters
|
|
927
|
+
----------
|
|
928
|
+
n_views : int
|
|
929
|
+
Number of projection views.
|
|
930
|
+
ray_dir_fn : Callable[[torch.Tensor], torch.Tensor]
|
|
931
|
+
Function that takes angles and returns ray directions (n_views, 2).
|
|
932
|
+
det_origin_fn : Callable[[torch.Tensor], torch.Tensor]
|
|
933
|
+
Function that takes angles and returns detector origins (n_views, 2).
|
|
934
|
+
start_angle : float, optional
|
|
935
|
+
Starting angle in radians (default: 0.0).
|
|
936
|
+
device : str or torch.device, optional
|
|
937
|
+
Device for tensors (default: 'cuda').
|
|
938
|
+
dtype : torch.dtype, optional
|
|
939
|
+
Data type for tensors (default: torch.float32).
|
|
940
|
+
|
|
941
|
+
Returns
|
|
942
|
+
-------
|
|
943
|
+
ray_dir : torch.Tensor
|
|
944
|
+
Ray direction unit vectors for each view, shape (n_views, 2).
|
|
945
|
+
det_origin : torch.Tensor
|
|
946
|
+
Detector origin positions for each view, shape (n_views, 2).
|
|
947
|
+
det_u_vec : torch.Tensor
|
|
948
|
+
Detector u-direction unit vectors for each view, shape (n_views, 2).
|
|
949
|
+
|
|
950
|
+
Examples
|
|
951
|
+
--------
|
|
952
|
+
>>> # Define custom trajectory functions
|
|
953
|
+
>>> def ray_dirs(angles):
|
|
954
|
+
... dirs = torch.zeros((len(angles), 2), device=angles.device, dtype=angles.dtype)
|
|
955
|
+
... dirs[:, 0] = torch.cos(angles)
|
|
956
|
+
... dirs[:, 1] = torch.sin(angles)
|
|
957
|
+
... return dirs
|
|
958
|
+
>>>
|
|
959
|
+
>>> def det_origins(angles):
|
|
960
|
+
... origins = torch.zeros((len(angles), 2), device=angles.device, dtype=angles.dtype)
|
|
961
|
+
... return origins
|
|
962
|
+
>>>
|
|
963
|
+
>>> ray_dir, det_origin, det_u_vec = custom_trajectory_2d_parallel(
|
|
964
|
+
... n_views=180, ray_dir_fn=ray_dirs, det_origin_fn=det_origins
|
|
965
|
+
... )
|
|
966
|
+
"""
|
|
967
|
+
# Generate angles
|
|
968
|
+
end_angle = start_angle + math.pi
|
|
969
|
+
step = (end_angle - start_angle) / n_views
|
|
970
|
+
angles = start_angle + torch.arange(n_views, device=device, dtype=dtype) * step
|
|
971
|
+
|
|
972
|
+
# Get ray directions and detector origins from user-defined functions
|
|
973
|
+
ray_dir = ray_dir_fn(angles)
|
|
974
|
+
det_origin = det_origin_fn(angles)
|
|
975
|
+
|
|
976
|
+
if ray_dir.shape != (n_views, 2):
|
|
977
|
+
raise ValueError(f"ray_dir_fn must return tensor of shape ({n_views}, 2), "
|
|
978
|
+
f"got {ray_dir.shape}")
|
|
979
|
+
if det_origin.shape != (n_views, 2):
|
|
980
|
+
raise ValueError(f"det_origin_fn must return tensor of shape ({n_views}, 2), "
|
|
981
|
+
f"got {det_origin.shape}")
|
|
982
|
+
|
|
983
|
+
# Normalize ray directions
|
|
984
|
+
ray_dir = ray_dir / torch.norm(ray_dir, dim=1, keepdim=True)
|
|
985
|
+
|
|
986
|
+
# Detector u-direction: perpendicular to ray direction (rotate 90 degrees)
|
|
987
|
+
det_u_vec = torch.zeros((n_views, 2), device=device, dtype=dtype)
|
|
988
|
+
det_u_vec[:, 0] = -ray_dir[:, 1] # perpendicular: (-y, x)
|
|
989
|
+
det_u_vec[:, 1] = ray_dir[:, 0]
|
|
990
|
+
|
|
991
|
+
return ray_dir, det_origin, det_u_vec
|