AOT-biomaps 2.9.339__py3-none-any.whl → 2.9.373__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 AOT-biomaps might be problematic. Click here for more details.
- AOT_biomaps/AOT_Acoustic/AcousticTools.py +17 -0
- AOT_biomaps/AOT_Acoustic/StructuredWave.py +15 -1
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +42 -15
- AOT_biomaps/AOT_Experiment/ExperimentTools.py +15 -25
- AOT_biomaps/AOT_Experiment/Tomography.py +61 -48
- AOT_biomaps/AOT_Recon/AnalyticRecon.py +321 -179
- AOT_biomaps/AOT_Recon/ReconTools.py +162 -1
- AOT_biomaps/AOT_Recon/_mainRecon.py +117 -10
- AOT_biomaps/__init__.py +35 -1
- {aot_biomaps-2.9.339.dist-info → aot_biomaps-2.9.373.dist-info}/METADATA +1 -1
- {aot_biomaps-2.9.339.dist-info → aot_biomaps-2.9.373.dist-info}/RECORD +13 -13
- {aot_biomaps-2.9.339.dist-info → aot_biomaps-2.9.373.dist-info}/WHEEL +0 -0
- {aot_biomaps-2.9.339.dist-info → aot_biomaps-2.9.373.dist-info}/top_level.txt +0 -0
|
@@ -1,108 +1,23 @@
|
|
|
1
1
|
from ._mainRecon import Recon
|
|
2
2
|
from .ReconEnums import ReconType, AnalyticType, ProcessType
|
|
3
3
|
from AOT_biomaps.AOT_Experiment.Tomography import hex_to_binary_profile
|
|
4
|
-
from .ReconTools import get_phase_deterministic
|
|
4
|
+
from .ReconTools import fourierz_gpu, get_phase_deterministic, add_sincos_cpu, EvalDelayLawOS_center, ifourierx_gpu, rotate_theta_gpu, filter_radon_gpu, ifourierz_gpu
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from tqdm import trange
|
|
8
|
-
import
|
|
9
|
-
import tqdm
|
|
8
|
+
import cupy as cp
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class AnalyticRecon(Recon):
|
|
13
|
-
def __init__(self, analyticType,
|
|
12
|
+
def __init__(self, analyticType, Lc = None,**kwargs):
|
|
14
13
|
super().__init__(**kwargs)
|
|
15
14
|
self.reconType = ReconType.Analytic
|
|
16
15
|
self.analyticType = analyticType
|
|
16
|
+
if self.analyticType == AnalyticType.iRADON and Lc is None:
|
|
17
|
+
raise ValueError("Lc parameter must be provided for iRADON analytic reconstruction.")
|
|
18
|
+
self.Lc = Lc # in meters
|
|
17
19
|
self.AOsignal_demoldulated = None
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def parse_and_demodulate(self, withTumor=True):
|
|
22
|
-
|
|
23
|
-
if withTumor:
|
|
24
|
-
AOsignal = self.experiment.AOsignal_withTumor
|
|
25
|
-
else:
|
|
26
|
-
AOsignal = self.experiment.AOsignal_withoutTumor
|
|
27
|
-
delta_x = self.experiment.params.general['dx'] # en m
|
|
28
|
-
n_piezos = self.experiment.params.acoustic['num_elements']
|
|
29
|
-
demodulated_data = {}
|
|
30
|
-
structured_buffer = {}
|
|
31
|
-
|
|
32
|
-
for i in trange(len(self.experiment.AcousticFields), desc="Demodulating AO signals"):
|
|
33
|
-
label = self.experiment.AcousticFields[i].getName_field()
|
|
34
|
-
|
|
35
|
-
parts = label.split("_")
|
|
36
|
-
hex_pattern = parts[0]
|
|
37
|
-
angle_code = parts[-1]
|
|
38
|
-
|
|
39
|
-
# Angle
|
|
40
|
-
if angle_code.startswith("1"):
|
|
41
|
-
angle_deg = -int(angle_code[1:])
|
|
42
|
-
else:
|
|
43
|
-
angle_deg = int(angle_code)
|
|
44
|
-
angle_rad = np.deg2rad(angle_deg)
|
|
45
|
-
|
|
46
|
-
# Onde Plane (f_s = 0)
|
|
47
|
-
if set(hex_pattern.lower().replace(" ", "")) == {'f'}:
|
|
48
|
-
fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
|
|
49
|
-
demodulated_data[(fs_key, angle_rad)] = np.array(AOsignal[i])
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
# Onde Structurée
|
|
53
|
-
profile = hex_to_binary_profile(hex_pattern, n_piezos)
|
|
54
|
-
|
|
55
|
-
# Calcul FS (Fréquence de Structuration)
|
|
56
|
-
ft_prof = np.fft.fft(profile)
|
|
57
|
-
# On regarde uniquement la partie positive non DC
|
|
58
|
-
idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
|
|
59
|
-
freqs = np.fft.fftfreq(len(profile), d=delta_x)
|
|
60
|
-
|
|
61
|
-
# freqs est en m^-1 car delta_x est en mètres.
|
|
62
|
-
fs_m_inv = abs(freqs[idx_max])
|
|
63
|
-
|
|
64
|
-
# *** CORRECTION 1: Conversion de f_s en mm^-1 (mm^-1 est utilisé dans iRadon) ***
|
|
65
|
-
fs_key = fs_m_inv / 1000.0 # Fréquence spatiale en mm^-1
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if fs_key == 0: continue
|
|
69
|
-
|
|
70
|
-
# Calcul de la Phase (Shift)
|
|
71
|
-
phase = get_phase_deterministic(profile)
|
|
72
|
-
|
|
73
|
-
# Stockage par (fs, theta) et phase
|
|
74
|
-
key = (fs_key, angle_rad)
|
|
75
|
-
if key not in structured_buffer:
|
|
76
|
-
structured_buffer[key] = {}
|
|
77
|
-
|
|
78
|
-
# La moyenne est nécessaire si plusieurs acquisitions ont la même phase (pour le SNR)
|
|
79
|
-
if phase in structured_buffer[key]:
|
|
80
|
-
structured_buffer[key][phase] = (structured_buffer[key][phase] + np.array(AOsignal[i])) / 2
|
|
81
|
-
else:
|
|
82
|
-
structured_buffer[key][phase] = np.array(AOsignal[i])
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for (fs, theta), phases in structured_buffer.items():
|
|
87
|
-
s0 = phases.get(0.0, 0)
|
|
88
|
-
s_pi_2 = phases.get(np.pi/2, 0)
|
|
89
|
-
s_pi = phases.get(np.pi, 0)
|
|
90
|
-
s_3pi_2 = phases.get(3*np.pi/2, 0)
|
|
91
|
-
|
|
92
|
-
# Assurer que les zéros sont des vecteurs de la bonne taille
|
|
93
|
-
example = next(val for val in phases.values() if not isinstance(val, int))
|
|
94
|
-
if isinstance(s0, int): s0 = np.zeros_like(example)
|
|
95
|
-
if isinstance(s_pi, int): s_pi = np.zeros_like(example)
|
|
96
|
-
if isinstance(s_pi_2, int): s_pi_2 = np.zeros_like(example)
|
|
97
|
-
if isinstance(s_3pi_2, int): s_3pi_2 = np.zeros_like(example)
|
|
98
|
-
|
|
99
|
-
real = s0 - s_pi
|
|
100
|
-
imag = s_pi_2 - s_3pi_2
|
|
101
|
-
|
|
102
|
-
demodulated_data[(fs, theta)] = (real - 1j * imag) / (2/np.pi)
|
|
103
|
-
|
|
104
|
-
return demodulated_data
|
|
105
|
-
|
|
106
21
|
def run(self, processType = ProcessType.PYTHON, withTumor= True):
|
|
107
22
|
"""
|
|
108
23
|
This method is a placeholder for the analytic reconstruction process.
|
|
@@ -126,109 +41,336 @@ class AnalyticRecon(Recon):
|
|
|
126
41
|
Parameters:
|
|
127
42
|
analyticType: The type of analytic reconstruction to perform (default is iFOURIER).
|
|
128
43
|
"""
|
|
44
|
+
if withTumor:
|
|
45
|
+
AOsignal = self.experiment.AOsignal_withTumor
|
|
46
|
+
else:
|
|
47
|
+
AOsignal = self.experiment.AOsignal_withoutTumor
|
|
48
|
+
|
|
49
|
+
d_t = 1 / float(self.experiment.params.acoustic['f_saving'])
|
|
50
|
+
t_array = np.arange(0, AOsignal.shape[0])*d_t
|
|
51
|
+
Z = t_array * self.experiment.params.acoustic['c0']
|
|
52
|
+
X_m = np.arange(0, self.experiment.params.acoustic['num_elements'])* self.experiment.params.general['dx']
|
|
53
|
+
dfX = 1 / (X_m[1] - X_m[0]) / len(X_m)
|
|
129
54
|
if withTumor:
|
|
130
55
|
self.AOsignal_demoldulated = self.parse_and_demodulate(withTumor=True)
|
|
131
56
|
if self.analyticType == AnalyticType.iFOURIER:
|
|
132
|
-
self.reconPhantom = self._iFourierRecon(
|
|
57
|
+
self.reconPhantom = self._iFourierRecon(
|
|
58
|
+
R = AOsignal,
|
|
59
|
+
z = Z,
|
|
60
|
+
X_m=X_m,
|
|
61
|
+
theta=self.experiment.theta,
|
|
62
|
+
decimation=self.experiment.decimations,
|
|
63
|
+
c=self.experiment.params.acoustic['c0'],
|
|
64
|
+
DelayLAWS=self.experiment.DelayLaw,
|
|
65
|
+
ActiveLIST=self.experiment.ActiveList,
|
|
66
|
+
withTumor=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
133
69
|
elif self.analyticType == AnalyticType.iRADON:
|
|
134
|
-
self.reconPhantom = self._iRadonRecon(
|
|
70
|
+
self.reconPhantom = self._iRadonRecon(
|
|
71
|
+
R=AOsignal,
|
|
72
|
+
z=Z,
|
|
73
|
+
X_m=X_m,
|
|
74
|
+
theta=self.experiment.theta,
|
|
75
|
+
decimation=self.experiment.decimations,
|
|
76
|
+
df0x=dfX,
|
|
77
|
+
Lc =self.Lc,
|
|
78
|
+
c=self.experiment.params.acoustic['c0'],
|
|
79
|
+
DelayLAWS=self.experiment.DelayLaw,
|
|
80
|
+
ActiveLIST=self.experiment.ActiveList,
|
|
81
|
+
withTumor=True)
|
|
135
82
|
else:
|
|
136
83
|
raise ValueError(f"Unknown analytic type: {self.analyticType}")
|
|
137
84
|
else:
|
|
138
85
|
self.AOsignal_demoldulated = self.parse_and_demodulate(withTumor=False)
|
|
139
86
|
if self.analyticType == AnalyticType.iFOURIER:
|
|
140
|
-
self.reconLaser = self._iFourierRecon(
|
|
87
|
+
self.reconLaser = self._iFourierRecon(
|
|
88
|
+
R = AOsignal ,
|
|
89
|
+
z = Z,
|
|
90
|
+
X_m=X_m,
|
|
91
|
+
theta=self.experiment.theta,
|
|
92
|
+
decimation=self.experiment.decimations,
|
|
93
|
+
c=self.experiment.params.acoustic['c0'],
|
|
94
|
+
DelayLAWS=self.experiment.DelayLaw,
|
|
95
|
+
ActiveLIST=self.experiment.ActiveList,
|
|
96
|
+
withTumor=False,
|
|
97
|
+
)
|
|
141
98
|
elif self.analyticType == AnalyticType.iRADON:
|
|
142
|
-
self.reconLaser = self._iRadonRecon(
|
|
99
|
+
self.reconLaser = self._iRadonRecon(
|
|
100
|
+
R=AOsignal ,
|
|
101
|
+
z=Z,
|
|
102
|
+
X_m=X_m,
|
|
103
|
+
theta=self.experiment.theta,
|
|
104
|
+
decimation=self.experiment.decimations,
|
|
105
|
+
df0x=dfX,
|
|
106
|
+
Lc = self.Lc,
|
|
107
|
+
c=self.experiment.params.acoustic['c0'],
|
|
108
|
+
DelayLAWS=self.experiment.DelayLaw,
|
|
109
|
+
ActiveLIST=self.experiment.ActiveList,
|
|
110
|
+
withTumor=False)
|
|
143
111
|
else:
|
|
144
112
|
raise ValueError(f"Unknown analytic type: {self.analyticType}")
|
|
145
113
|
|
|
146
|
-
def _iFourierRecon(
|
|
114
|
+
def _iFourierRecon(
|
|
115
|
+
self,
|
|
116
|
+
R,
|
|
117
|
+
z,
|
|
118
|
+
X_m,
|
|
119
|
+
theta,
|
|
120
|
+
decimation,
|
|
121
|
+
c,
|
|
122
|
+
DelayLAWS,
|
|
123
|
+
ActiveLIST,
|
|
124
|
+
withTumor,
|
|
125
|
+
):
|
|
147
126
|
"""
|
|
148
|
-
Reconstruction d'image utilisant la
|
|
149
|
-
|
|
150
|
-
:return: Image reconstruite dans le domaine spatial.
|
|
127
|
+
Reconstruction d'image utilisant la méthode iFourier (GPU).
|
|
128
|
+
Normalisation physique complète incluse.
|
|
151
129
|
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
z =
|
|
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
|
-
|
|
130
|
+
|
|
131
|
+
# ======================================================
|
|
132
|
+
# 1. Préparation GPU
|
|
133
|
+
# ======================================================
|
|
134
|
+
R = cp.asarray(R)
|
|
135
|
+
z = cp.asarray(z)
|
|
136
|
+
X_m = cp.asarray(X_m)
|
|
137
|
+
theta = cp.asarray(theta)
|
|
138
|
+
decimation = cp.asarray(decimation)
|
|
139
|
+
DelayLAWS = cp.asarray(DelayLAWS)
|
|
140
|
+
ActiveLIST = cp.asarray(ActiveLIST)
|
|
141
|
+
|
|
142
|
+
# Normalisation DelayLAWS (ms -> s si nécessaire)
|
|
143
|
+
DelayLAWS_s = cp.where(cp.max(DelayLAWS) > 1e-3, DelayLAWS / 1000.0, DelayLAWS)
|
|
144
|
+
|
|
145
|
+
# Regroupement tirs (CPU pour np.unique plus rapide)
|
|
146
|
+
ScanParam_cpu = cp.asnumpy(cp.stack([decimation, cp.round(theta, 4)], axis=1))
|
|
147
|
+
_, ia_cpu, ib_cpu = np.unique(ScanParam_cpu, axis=0, return_index=True, return_inverse=True)
|
|
148
|
+
ia = cp.asarray(ia_cpu)
|
|
149
|
+
ib = cp.asarray(ib_cpu)
|
|
150
|
+
|
|
151
|
+
# ======================================================
|
|
152
|
+
# 2. Structuration complexe
|
|
153
|
+
# ======================================================
|
|
154
|
+
F_complex_cpu, theta_u_cpu, decim_u_cpu = add_sincos_cpu(
|
|
155
|
+
cp.asnumpy(R),
|
|
156
|
+
cp.asnumpy(decimation),
|
|
157
|
+
np.radians(cp.asnumpy(theta))
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Calcul des centres de rotation
|
|
161
|
+
M0 = EvalDelayLawOS_center(
|
|
162
|
+
X_m,
|
|
163
|
+
theta_u_cpu,
|
|
164
|
+
DelayLAWS_s.T[:, ia],
|
|
165
|
+
ActiveLIST.T[:, ia],
|
|
166
|
+
c
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Transfert GPU
|
|
170
|
+
F_complex = cp.asarray(F_complex_cpu)
|
|
171
|
+
theta_u = cp.asarray(theta_u_cpu)
|
|
172
|
+
decim_u = cp.asarray(decim_u_cpu)
|
|
173
|
+
M0_gpu = cp.asarray(M0)
|
|
174
|
+
|
|
175
|
+
# ======================================================
|
|
176
|
+
# 3. Paramètres de la grille
|
|
177
|
+
# ======================================================
|
|
178
|
+
Nz = z.size
|
|
179
|
+
Nx = X_m.size
|
|
180
|
+
dx = X_m[1] - X_m[0]
|
|
181
|
+
X_grid, Z_grid = cp.meshgrid(X_m, z)
|
|
182
|
+
idx0_x = Nx // 2
|
|
183
|
+
|
|
184
|
+
# Angles uniques
|
|
185
|
+
angles_group, ia_u, ib_u = cp.unique(theta_u, return_index=True, return_inverse=True)
|
|
186
|
+
Ntheta = angles_group.size
|
|
187
|
+
|
|
188
|
+
# Initialisation reconstruction
|
|
189
|
+
I_final = cp.zeros((Nz, Nx), dtype=cp.complex64)
|
|
190
|
+
|
|
191
|
+
# ======================================================
|
|
192
|
+
# 4. Boucle Inverse Fourier X
|
|
193
|
+
# ======================================================
|
|
194
|
+
for i_ang in trange(
|
|
195
|
+
Ntheta,
|
|
196
|
+
desc=f"AOT-BioMaps -- iFourier ({'with tumor' if withTumor else 'without tumor'}) -- GPU",
|
|
197
|
+
unit="angle"
|
|
198
|
+
):
|
|
199
|
+
|
|
200
|
+
# Grille Fourier locale (z, fx)
|
|
201
|
+
F_fx_z = cp.zeros((Nz, Nx), dtype=cp.complex64)
|
|
202
|
+
|
|
203
|
+
# Indices correspondant à cet angle
|
|
204
|
+
indices = cp.where(ib_u == i_ang)[0]
|
|
205
|
+
|
|
206
|
+
for idx in indices:
|
|
207
|
+
n = int(decim_u[idx])
|
|
208
|
+
trace_z = F_complex[:, idx]
|
|
209
|
+
|
|
210
|
+
# Mapping positif
|
|
211
|
+
ip = idx0_x + n
|
|
212
|
+
if 0 <= ip < Nx:
|
|
213
|
+
F_fx_z[:, ip] = trace_z
|
|
214
|
+
|
|
215
|
+
# Mapping négatif (symétrie hermitienne MATLAB)
|
|
216
|
+
if n != 0:
|
|
217
|
+
im = idx0_x - n
|
|
218
|
+
if 0 <= im < Nx:
|
|
219
|
+
col_conj = cp.zeros(Nz, dtype=cp.complex64)
|
|
220
|
+
col_conj[1:] = cp.conj(trace_z[:-1])
|
|
221
|
+
F_fx_z[:, im] = col_conj
|
|
222
|
+
|
|
223
|
+
# Correction DC
|
|
224
|
+
F_fx_z[:, idx0_x] *= 0.5
|
|
225
|
+
|
|
226
|
+
# Inverse Fourier X (GPU) + facteur Nx pour correspondance MATLAB
|
|
227
|
+
I_spatial = ifourierx_gpu(F_fx_z, dx) * Nx
|
|
228
|
+
|
|
229
|
+
# Rotation spatiale autour du centre M0
|
|
230
|
+
I_rot = rotate_theta_gpu(
|
|
231
|
+
X_grid,
|
|
232
|
+
Z_grid,
|
|
233
|
+
I_spatial,
|
|
234
|
+
-angles_group[i_ang],
|
|
235
|
+
M0_gpu[i_ang, :]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Somme incohérente
|
|
239
|
+
I_final += I_rot
|
|
240
|
+
|
|
241
|
+
# ======================================================
|
|
242
|
+
# 5. Normalisation physique finale
|
|
243
|
+
# ======================================================
|
|
244
|
+
Ntheta_total = len(theta_u)
|
|
245
|
+
Ntirs_complex = (R.shape[1] - Ntheta_total) / 4.0 # 4 phases par tir
|
|
246
|
+
|
|
247
|
+
I_final /= (Ntheta_total * Ntirs_complex)
|
|
248
|
+
I_final *= dx # normalisation physique sur l’axe x
|
|
249
|
+
|
|
250
|
+
return cp.real(I_final).get()
|
|
251
|
+
|
|
252
|
+
def _iRadonRecon(
|
|
253
|
+
self,
|
|
254
|
+
R,
|
|
255
|
+
z,
|
|
256
|
+
X_m,
|
|
257
|
+
theta,
|
|
258
|
+
decimation,
|
|
259
|
+
df0x,
|
|
260
|
+
Lc,
|
|
261
|
+
c,
|
|
262
|
+
DelayLAWS,
|
|
263
|
+
ActiveLIST,
|
|
264
|
+
withTumor,
|
|
265
|
+
):
|
|
185
266
|
"""
|
|
186
267
|
Reconstruction d'image utilisant la méthode iRadon.
|
|
187
|
-
|
|
188
|
-
:return: Image reconstruite.
|
|
268
|
+
Normalisation physique correcte (phases, angles, dz).
|
|
189
269
|
"""
|
|
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
|
-
|
|
270
|
+
|
|
271
|
+
# ======================================================
|
|
272
|
+
# 1. AddSinCos (structuration) — CPU volontairement
|
|
273
|
+
# ======================================================
|
|
274
|
+
theta = np.radians(theta)
|
|
275
|
+
F_ct_kx, theta_u, decim_u = add_sincos_cpu(R, decimation, theta)
|
|
276
|
+
|
|
277
|
+
ScanParam = np.stack([decimation, theta], axis=1)
|
|
278
|
+
_, ia, _ = np.unique(ScanParam, axis=0, return_index=True, return_inverse=True)
|
|
279
|
+
|
|
280
|
+
ActiveLIST = np.asarray(ActiveLIST).T
|
|
281
|
+
DelayLAWS = np.asarray(DelayLAWS).T
|
|
282
|
+
ActiveLIST_unique = ActiveLIST[:, ia]
|
|
283
|
+
|
|
284
|
+
# ======================================================
|
|
285
|
+
# 2. FFT z
|
|
286
|
+
# ======================================================
|
|
287
|
+
z_gpu = cp.asarray(z)
|
|
288
|
+
Fin = fourierz_gpu(z, F_ct_kx)
|
|
289
|
+
|
|
290
|
+
dz = float(z[1] - z[0]) # <<< Δz PHYSIQUE
|
|
291
|
+
fz = cp.fft.fftshift(cp.fft.fftfreq(len(z), d=dz))
|
|
292
|
+
|
|
293
|
+
Nz, Nk = Fin.shape
|
|
294
|
+
|
|
295
|
+
# ======================================================
|
|
296
|
+
# 3. Filtrage OS exact
|
|
297
|
+
# ======================================================
|
|
298
|
+
decim_gpu = cp.asarray(decim_u)
|
|
299
|
+
I0 = decim_gpu == 0
|
|
300
|
+
F0 = Fin * I0[None, :]
|
|
301
|
+
|
|
302
|
+
DEC, FZ = cp.meshgrid(decim_gpu, fz)
|
|
303
|
+
|
|
304
|
+
Hinf = cp.abs(FZ) < cp.abs(DEC) * df0x
|
|
305
|
+
Hsup = FZ >= 0
|
|
306
|
+
|
|
307
|
+
Fc = 1 / Lc
|
|
308
|
+
FILTER = filter_radon_gpu(fz, Fc)[:, None]
|
|
309
|
+
|
|
310
|
+
Finf = F0 * FILTER[:, :F0.shape[1]] * Hinf[:, :F0.shape[1]]
|
|
311
|
+
Fsup = Fin * FILTER * Hsup
|
|
312
|
+
|
|
313
|
+
# ======================================================
|
|
314
|
+
# 4. Retour espace z
|
|
315
|
+
# ======================================================
|
|
316
|
+
Finf = ifourierz_gpu(z, Finf)
|
|
317
|
+
Fsup = ifourierz_gpu(z, Fsup)
|
|
318
|
+
|
|
319
|
+
# ======================================================
|
|
320
|
+
# 5. Grille image
|
|
321
|
+
# ======================================================
|
|
322
|
+
X_gpu = cp.asarray(X_m)
|
|
323
|
+
X, Z = cp.meshgrid(X_gpu, z_gpu)
|
|
324
|
+
Xc = float(np.mean(X_m))
|
|
325
|
+
|
|
326
|
+
# ======================================================
|
|
327
|
+
# 6. Centre de rotation M0
|
|
328
|
+
# ======================================================
|
|
329
|
+
M0 = EvalDelayLawOS_center(X_m, theta, DelayLAWS[:, ia], ActiveLIST_unique, c)
|
|
330
|
+
M0_gpu = cp.asarray(M0)
|
|
331
|
+
|
|
332
|
+
# ======================================================
|
|
333
|
+
# 7. Rétroprojection
|
|
334
|
+
# ======================================================
|
|
335
|
+
Irec = cp.zeros_like(X, dtype=cp.complex64)
|
|
336
|
+
|
|
337
|
+
for i in trange(
|
|
338
|
+
len(theta_u),
|
|
339
|
+
desc=f"AOT-BioMaps -- iRadon ({'with tumor' if withTumor else 'without tumor'}) -- GPU",
|
|
340
|
+
unit="angle"
|
|
341
|
+
):
|
|
342
|
+
th = float(theta_u[i])
|
|
343
|
+
|
|
344
|
+
T = (X - M0_gpu[i, 0]) * cp.sin(th) + (Z - M0_gpu[i, 1]) * cp.cos(th) + M0_gpu[i, 1]
|
|
345
|
+
S = (X - Xc) * cp.cos(th) - (Z - M0_gpu[i, 1]) * cp.sin(th)
|
|
346
|
+
h0 = cp.exp(1j * 2 * cp.pi * decim_u[i] * df0x * S)
|
|
347
|
+
|
|
348
|
+
# interpolation linéaire en z
|
|
349
|
+
Tind = (T - z_gpu[0]) / dz
|
|
350
|
+
i0 = cp.floor(Tind).astype(cp.int32)
|
|
351
|
+
i1 = i0 + 1
|
|
352
|
+
i0 = cp.clip(i0, 0, Nz - 1)
|
|
353
|
+
i1 = cp.clip(i1, 0, Nz - 1)
|
|
354
|
+
w = Tind - i0
|
|
355
|
+
|
|
356
|
+
proj_sup = (1 - w) * Fsup[i0, i] + w * Fsup[i1, i]
|
|
357
|
+
proj_inf = (1 - w) * Finf[i0, i] + w * Finf[i1, i]
|
|
358
|
+
|
|
359
|
+
# >>> SOMME BRUTE (correcte)
|
|
360
|
+
Irec += 2 * h0 * proj_sup + proj_inf
|
|
361
|
+
|
|
362
|
+
# ======================================================
|
|
363
|
+
# 8. NORMALISATION PHYSIQUE GLOBALE
|
|
364
|
+
# ======================================================
|
|
365
|
+
|
|
366
|
+
Ntheta = len(theta_u)
|
|
367
|
+
|
|
368
|
+
# nombre de tirs complexes indépendants (4 phases)
|
|
369
|
+
Ntirs_complex = (R.shape[1] - Ntheta) / 4.0
|
|
370
|
+
|
|
371
|
+
# normalisation finale
|
|
372
|
+
Irec /= (Ntheta * Ntirs_complex)
|
|
373
|
+
print(f"dz normalization: {dz}")
|
|
374
|
+
Irec *= dz
|
|
375
|
+
|
|
376
|
+
return cp.real(Irec).get()
|