AOT-biomaps 2.1.3__py3-none-any.whl → 2.9.233__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/AcousticEnums.py +64 -0
- AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
- AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
- AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
- AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
- AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
- AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
- AOT_biomaps/AOT_Experiment/Focus.py +55 -0
- AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
- AOT_biomaps/AOT_Experiment/__init__.py +9 -0
- AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
- AOT_biomaps/AOT_Optic/Absorber.py +24 -0
- AOT_biomaps/AOT_Optic/Laser.py +70 -0
- AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
- AOT_biomaps/AOT_Optic/__init__.py +10 -0
- AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
- AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
- AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
- AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
- AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
- AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
- AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
- AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
- AOT_biomaps/AOT_Recon/__init__.py +11 -0
- AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
- AOT_biomaps/Config.py +95 -0
- AOT_biomaps/Settings.py +45 -13
- AOT_biomaps/__init__.py +271 -18
- aot_biomaps-2.9.233.dist-info/METADATA +22 -0
- aot_biomaps-2.9.233.dist-info/RECORD +43 -0
- {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
- AOT_biomaps/AOT_Acoustic.py +0 -1881
- AOT_biomaps/AOT_Experiment.py +0 -541
- AOT_biomaps/AOT_Optic.py +0 -219
- AOT_biomaps/AOT_Reconstruction.py +0 -1416
- AOT_biomaps/config.py +0 -54
- AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
- AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
- {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
from AOT_biomaps.Settings import Params
|
|
2
|
+
from AOT_biomaps.AOT_Optic._mainOptic import Phantom
|
|
3
|
+
from AOT_biomaps.AOT_Acoustic.AcousticEnums import WaveType, FormatSave
|
|
4
|
+
from AOT_biomaps.AOT_Acoustic.StructuredWave import StructuredWave
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
import os
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
import torch.nn.functional as F
|
|
11
|
+
from tqdm import trange
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import matplotlib.animation as animation
|
|
15
|
+
import matplotlib as mpl
|
|
16
|
+
import copy
|
|
17
|
+
|
|
18
|
+
class Experiment(ABC):
|
|
19
|
+
def __init__(self, params, acousticType=WaveType.StructuredWave, formatSave=FormatSave.HDR_IMG):
|
|
20
|
+
self.params = params
|
|
21
|
+
self.OpticImage = None
|
|
22
|
+
self.AcousticFields = None
|
|
23
|
+
self.AOsignal_withTumor = None
|
|
24
|
+
self.AOsignal_withoutTumor = None
|
|
25
|
+
|
|
26
|
+
if type(acousticType).__name__ != "WaveType":
|
|
27
|
+
raise TypeError("acousticType must be an instance of the WaveType class")
|
|
28
|
+
|
|
29
|
+
self.FormatSave = formatSave
|
|
30
|
+
self.TypeAcoustic = acousticType
|
|
31
|
+
|
|
32
|
+
if type(self.params) != Params:
|
|
33
|
+
raise TypeError("params must be an instance of the Params class")
|
|
34
|
+
|
|
35
|
+
def copy(self):
|
|
36
|
+
"""Retourne une copie profonde de l'objet."""
|
|
37
|
+
return copy.deepcopy(self)
|
|
38
|
+
|
|
39
|
+
def generatePhantom(self):
|
|
40
|
+
"""
|
|
41
|
+
Generate the phantom for the experiment.
|
|
42
|
+
This method initializes the OpticImage attribute with a Phantom instance.
|
|
43
|
+
"""
|
|
44
|
+
self.OpticImage = Phantom(params=self.params)
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def generateAcousticFields(self, fieldDataPath, fieldParamPath, show_log=True):
|
|
48
|
+
"""
|
|
49
|
+
Generate the acoustic fields for simulation.
|
|
50
|
+
Args:
|
|
51
|
+
fieldDataPath: Path to save the generated fields.
|
|
52
|
+
fieldParamPath: Path to the field parameters file.
|
|
53
|
+
Returns:
|
|
54
|
+
systemMatrix: A numpy array of the generated fields.
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def cutAcousticFields(self, max_t, min_t=0):
|
|
59
|
+
|
|
60
|
+
max_t = float(max_t)
|
|
61
|
+
min_t = float(min_t)
|
|
62
|
+
|
|
63
|
+
min_sample = int(np.floor(min_t * float(self.params.acoustic['f_saving'])))
|
|
64
|
+
max_sample = int(np.floor(max_t * float(self.params.acoustic['f_saving'])))
|
|
65
|
+
|
|
66
|
+
if min_sample < 0 or max_sample < 0:
|
|
67
|
+
raise ValueError("min_sample and max_sample must be non-negative integers.")
|
|
68
|
+
if min_sample >= max_sample:
|
|
69
|
+
raise ValueError("min_sample must be less than max_sample.")
|
|
70
|
+
|
|
71
|
+
if not self.AcousticFields:
|
|
72
|
+
raise ValueError("AcousticFields is empty. Cannot cut fields.")
|
|
73
|
+
|
|
74
|
+
for i in trange(len(self.AcousticFields), desc=f"Cutting Acoustic Fields ({min_sample} to {max_sample} samples)"):
|
|
75
|
+
field = self.AcousticFields[i]
|
|
76
|
+
if field.field.shape[0] < max_sample:
|
|
77
|
+
raise ValueError(f"Field {field.getName_field()} has an invalid shape: {field.field.shape}. Expected shape to be at least ({max_sample},).")
|
|
78
|
+
self.AcousticFields[i].field = field.field[min_sample:max_sample, :, :]
|
|
79
|
+
|
|
80
|
+
def addNoise(self, noiseType='gaussian', noiseLvl=0.1, withTumor=True):
|
|
81
|
+
"""
|
|
82
|
+
Ajoute du bruit (gaussien ou poisson) au signal AO sélectionné.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
noiseType (str): Type de bruit à ajouter ('gaussian' ou 'poisson').
|
|
86
|
+
noiseLvl (float): Niveau de bruit (écart-type pour le bruit gaussien, facteur multiplicatif pour le bruit de Poisson).
|
|
87
|
+
withTumor (bool): Si True, ajoute le bruit au signal avec tumeur, sinon au signal sans tumeur.
|
|
88
|
+
"""
|
|
89
|
+
if withTumor and self.AOsignal_withTumor is None:
|
|
90
|
+
raise ValueError("AO signal with tumor is not generated. Please generate it first.")
|
|
91
|
+
if not withTumor and self.AOsignal_withoutTumor is None:
|
|
92
|
+
raise ValueError("AO signal without tumor is not generated. Please generate it first.")
|
|
93
|
+
|
|
94
|
+
if withTumor:
|
|
95
|
+
AOsignals = self.AOsignal_withTumor
|
|
96
|
+
else:
|
|
97
|
+
AOsignals = self.AOsignal_withoutTumor
|
|
98
|
+
|
|
99
|
+
noiseSignals = np.zeros_like(AOsignals)
|
|
100
|
+
for i in trange(AOsignals.shape[1], desc=f"Adding {noiseType} noise to AO signal {'with' if withTumor else 'without'} tumor"):
|
|
101
|
+
AOsignal = AOsignals[:, i]
|
|
102
|
+
if noiseType.lower() == 'gaussian':
|
|
103
|
+
noise = np.random.normal(0, noiseLvl*np.max(AOsignal), AOsignal.shape)
|
|
104
|
+
noisy_signal = AOsignal + noise
|
|
105
|
+
elif noiseType.lower() == 'poisson':
|
|
106
|
+
# Pour le bruit de Poisson, on utilise souvent un facteur multiplicatif
|
|
107
|
+
# car le bruit de Poisson est proportionnel à la racine carrée du signal.
|
|
108
|
+
# Ici, on multiplie le signal par un facteur aléatoire centré autour de 1.
|
|
109
|
+
noise = np.random.poisson(noiseLvl * np.abs(AOsignal)) / (noiseLvl * np.abs(AOsignal).max())
|
|
110
|
+
noisy_signal = AOsignal * noise
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("noiseType must be either 'gaussian' or 'poisson'.")
|
|
113
|
+
noisy_signal = np.clip(noisy_signal, a_min=0, a_max=None) # Assurer que le signal reste non négatif
|
|
114
|
+
noiseSignals[:, i] = noisy_signal
|
|
115
|
+
return noiseSignals
|
|
116
|
+
|
|
117
|
+
def reduceDims(self, mode='avg'):
|
|
118
|
+
"""
|
|
119
|
+
Réduit les dimensions T, X, Z d'un numpy array (T, X, Z) par 2 en utilisant une convolution.
|
|
120
|
+
Retourne un numpy array et met à jour les paramètres numériques.
|
|
121
|
+
"""
|
|
122
|
+
for i in trange(len(self.AcousticFields),
|
|
123
|
+
desc="Downsampling Acoustic Fields (T, X, Z → T//2, X//2, Z//2)"):
|
|
124
|
+
# Conversion en tenseur PyTorch
|
|
125
|
+
field = self.AcousticFields[i].field
|
|
126
|
+
if not isinstance(field, torch.Tensor):
|
|
127
|
+
field = torch.from_numpy(field)
|
|
128
|
+
|
|
129
|
+
# Vérification de la forme (doit être 3D : T, X, Z)
|
|
130
|
+
if field.dim() != 3:
|
|
131
|
+
raise ValueError(f"Forme non supportée : {field.shape}. Attendu (T, X, Z).")
|
|
132
|
+
|
|
133
|
+
# Ajout des dimensions pour conv3d : (1, 1, T, X, Z)
|
|
134
|
+
x = field.unsqueeze(0).unsqueeze(0)
|
|
135
|
+
|
|
136
|
+
# Réduction par convolution 3D
|
|
137
|
+
if mode == 'avg':
|
|
138
|
+
x_down = F.avg_pool3d(x, kernel_size=(2, 2, 2), stride=(2, 2, 2))
|
|
139
|
+
else: # mode == 'max'
|
|
140
|
+
x_down = F.max_pool3d(x, kernel_size=(2, 2, 2), stride=(2, 2, 2))
|
|
141
|
+
|
|
142
|
+
# Conversion en numpy array et suppression des dimensions ajoutées
|
|
143
|
+
self.AcousticFields[i].field = x_down.squeeze(0).squeeze(0).cpu().numpy()
|
|
144
|
+
|
|
145
|
+
# Fonction utilitaire pour convertir et mettre à jour un paramètre
|
|
146
|
+
def convert_and_update(param_dict, key, operation):
|
|
147
|
+
if key in param_dict:
|
|
148
|
+
if isinstance(param_dict[key], str):
|
|
149
|
+
param_dict[key] = float(param_dict[key])
|
|
150
|
+
param_dict[key] = operation(param_dict[key])
|
|
151
|
+
|
|
152
|
+
# Mise à jour des paramètres
|
|
153
|
+
convert_and_update(self.params.acoustic, 'f_saving', lambda x: x / 2)
|
|
154
|
+
for param in ['dx', 'dy', 'dz']:
|
|
155
|
+
convert_and_update(self.params.general, param, lambda x: x * 2)
|
|
156
|
+
|
|
157
|
+
def normalizeAOsignals(self, withTumor=True):
|
|
158
|
+
if withTumor and self.AOsignal_withTumor is None:
|
|
159
|
+
raise ValueError("AO signal with tumor is not generated. Please generate it first.")
|
|
160
|
+
if not withTumor and self.AOsignal_withoutTumor is None:
|
|
161
|
+
raise ValueError("AO signal without tumor is not generated. Please generate it first.")
|
|
162
|
+
if withTumor:
|
|
163
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor - np.min(self.AOsignal_withTumor)/(np.max(self.AOsignal_withTumor)-np.min(self.AOsignal_withTumor))
|
|
164
|
+
else:
|
|
165
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor - np.min(self.AOsignal_withoutTumor)/(np.max(self.AOsignal_withoutTumor)-np.min(self.AOsignal_withoutTumor))
|
|
166
|
+
|
|
167
|
+
def saveAcousticFields(self, save_directory):
|
|
168
|
+
progress_bar = trange(len(self.AcousticFields), desc="Saving Acoustic Fields")
|
|
169
|
+
for i in progress_bar:
|
|
170
|
+
progress_bar.set_postfix_str(f"-- {self.AcousticFields[i].getName_field()}")
|
|
171
|
+
self.AcousticFields[i].save_field(save_directory, formatSave=self.FormatSave)
|
|
172
|
+
|
|
173
|
+
def show_animated_Acoustic(self, wave_name=None, desired_duration_ms=5000, save_dir=None):
|
|
174
|
+
"""
|
|
175
|
+
Plot synchronized animations of A_matrix slices for selected angles.
|
|
176
|
+
Args:
|
|
177
|
+
wave_name: optional name for labeling the subplots (e.g., "wave1")
|
|
178
|
+
desired_duration_ms: Total duration of the animation in milliseconds.
|
|
179
|
+
save_dir: directory to save the animation gif; if None, animation will not be saved
|
|
180
|
+
Returns:
|
|
181
|
+
ani: Matplotlib FuncAnimation object
|
|
182
|
+
"""
|
|
183
|
+
mpl.rcParams['animation.embed_limit'] = 100
|
|
184
|
+
if save_dir is not None:
|
|
185
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
186
|
+
|
|
187
|
+
num_plots = len(self.AcousticFields)
|
|
188
|
+
if num_plots <= 5:
|
|
189
|
+
nrows, ncols = 1, num_plots
|
|
190
|
+
else:
|
|
191
|
+
ncols = 5
|
|
192
|
+
nrows = (num_plots + ncols - 1) // ncols
|
|
193
|
+
|
|
194
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 5.3 * nrows))
|
|
195
|
+
if isinstance(axes, plt.Axes):
|
|
196
|
+
axes = np.array([axes])
|
|
197
|
+
axes = axes.flatten()
|
|
198
|
+
ims = []
|
|
199
|
+
|
|
200
|
+
fig.suptitle(f"System Matrix Animation {wave_name}", fontsize=12, y=0.98)
|
|
201
|
+
|
|
202
|
+
for idx in range(num_plots):
|
|
203
|
+
ax = axes[idx]
|
|
204
|
+
im = ax.imshow(self.AcousticFields[0, :, :, idx],
|
|
205
|
+
extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
|
|
206
|
+
vmax=1, aspect='equal', cmap='jet', animated=True)
|
|
207
|
+
ax.set_xlabel("x (mm)", fontsize=8)
|
|
208
|
+
ax.set_ylabel("z (mm)", fontsize=8)
|
|
209
|
+
ims.append((im, ax, idx))
|
|
210
|
+
|
|
211
|
+
for j in range(num_plots, len(axes)):
|
|
212
|
+
fig.delaxes(axes[j])
|
|
213
|
+
|
|
214
|
+
plt.tight_layout(rect=[0, 0, 1, 0.95])
|
|
215
|
+
|
|
216
|
+
def update(frame):
|
|
217
|
+
artists = []
|
|
218
|
+
for im, ax, idx in ims:
|
|
219
|
+
im.set_array(self.AcousticFields[frame, :, :, idx])
|
|
220
|
+
fig.suptitle(f"System Matrix Animation {wave_name} t = {frame * 25e-6 * 1000:.2f} ms", fontsize=10)
|
|
221
|
+
artists.append(im)
|
|
222
|
+
return artists
|
|
223
|
+
|
|
224
|
+
interval = desired_duration_ms / self.AcousticFields.shape[0]
|
|
225
|
+
ani = animation.FuncAnimation(
|
|
226
|
+
fig, update,
|
|
227
|
+
frames=range(0, self.AcousticFields.shape[0]),
|
|
228
|
+
interval=interval, blit=True
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if save_dir is not None:
|
|
232
|
+
now = datetime.now()
|
|
233
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
234
|
+
save_filename = f"AcousticField_{wave_name}_{date_str}.gif"
|
|
235
|
+
save_path = os.path.join(save_dir, save_filename)
|
|
236
|
+
ani.save(save_path, writer='pillow', fps=20)
|
|
237
|
+
print(f"Saved: {save_path}")
|
|
238
|
+
|
|
239
|
+
plt.close(fig)
|
|
240
|
+
return ani
|
|
241
|
+
|
|
242
|
+
def generateAOsignal(self, withTumor=True, AOsignalDataPath=None):
|
|
243
|
+
|
|
244
|
+
if AOsignalDataPath is not None:
|
|
245
|
+
if not os.path.exists(AOsignalDataPath):
|
|
246
|
+
raise FileNotFoundError(f"AO file {AOsignalDataPath} not found.")
|
|
247
|
+
if withTumor:
|
|
248
|
+
self.AOsignal_withTumor = self._loadAOSignal(AOsignalDataPath)
|
|
249
|
+
if self.AOsignal_withTumor.shape[0] != self.AcousticFields[0].field.shape[0]:
|
|
250
|
+
print(f"AO signal shape {self.AOsignal_withTumor.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Resizing Acoustic fields...")
|
|
251
|
+
self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0] / float(self.params.acoustic['f_saving']), min_t=0)
|
|
252
|
+
else:
|
|
253
|
+
self.AOsignal_withoutTumor = self._loadAOSignal(AOsignalDataPath)
|
|
254
|
+
if self.AOsignal_withoutTumor.shape[0] != self.AcousticFields[0].field.shape[0]:
|
|
255
|
+
print(f"AO signal shape {self.AOsignal_withoutTumor.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Resizing Acoustic fields...")
|
|
256
|
+
self.cutAcousticFields(max_t=self.AOsignal_withoutTumor.shape[0] / float(self.params.acoustic['f_saving']), min_t=0)
|
|
257
|
+
else:
|
|
258
|
+
if self.AcousticFields is None:
|
|
259
|
+
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
260
|
+
|
|
261
|
+
if self.OpticImage is None:
|
|
262
|
+
raise ValueError("OpticImage is not initialized. Please generate the phantom first.")
|
|
263
|
+
|
|
264
|
+
if not all(field.field.shape == self.AcousticFields[0].field.shape for field in self.AcousticFields):
|
|
265
|
+
minShape = min([field.field.shape[0] for field in self.AcousticFields])
|
|
266
|
+
self.cutAcousticFields(max_t=minShape * self.params['fs_aq'])
|
|
267
|
+
else:
|
|
268
|
+
shape_field = self.AcousticFields[0].field.shape
|
|
269
|
+
|
|
270
|
+
AOsignal = np.zeros((shape_field[0], len(self.AcousticFields)), dtype=np.float32)
|
|
271
|
+
|
|
272
|
+
if withTumor:
|
|
273
|
+
description = "Generating AO Signal with Tumor"
|
|
274
|
+
else:
|
|
275
|
+
description = "Generating AO Signal without Tumor"
|
|
276
|
+
|
|
277
|
+
for i in trange(len(self.AcousticFields), desc=description):
|
|
278
|
+
for t in range(self.AcousticFields[i].field.shape[0]):
|
|
279
|
+
if withTumor:
|
|
280
|
+
interaction = self.OpticImage.phantom * self.AcousticFields[i].field[t, :, :]
|
|
281
|
+
else:
|
|
282
|
+
interaction = self.OpticImage.laser.intensity * self.AcousticFields[i].field[t, :, :]
|
|
283
|
+
AOsignal[t, i] = np.sum(interaction)
|
|
284
|
+
|
|
285
|
+
if withTumor:
|
|
286
|
+
self.AOsignal_withTumor = AOsignal
|
|
287
|
+
else:
|
|
288
|
+
self.AOsignal_withoutTumor = AOsignal
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _loadAOSignal(AOsignalPath):
|
|
292
|
+
if AOsignalPath.endswith(".cdh"):
|
|
293
|
+
with open(AOsignalPath, "r") as file:
|
|
294
|
+
cdh_content = file.readlines()
|
|
295
|
+
|
|
296
|
+
cdf_path = AOsignalPath.replace(".cdh", ".cdf")
|
|
297
|
+
|
|
298
|
+
# Extraire les paramètres depuis le fichier .cdh
|
|
299
|
+
n_scans = int([line.split(":")[1].strip() for line in cdh_content if "Number of events" in line][0])
|
|
300
|
+
n_acquisitions_per_event = int([line.split(":")[1].strip() for line in cdh_content if "Number of acquisitions per event" in line][0])
|
|
301
|
+
num_elements = int([line.split(":")[1].strip() for line in cdh_content if "Number of US transducers" in line][0])
|
|
302
|
+
|
|
303
|
+
# Initialisation des structures
|
|
304
|
+
AO_signal = np.zeros((n_acquisitions_per_event, n_scans), dtype=np.float32)
|
|
305
|
+
active_lists = []
|
|
306
|
+
angles = []
|
|
307
|
+
|
|
308
|
+
# Lecture du fichier binaire
|
|
309
|
+
with open(cdf_path, "rb") as file:
|
|
310
|
+
for j in trange(n_scans, desc="Lecture des événements"):
|
|
311
|
+
# Lire l'activeList : 48 caractères hex = 24 bytes
|
|
312
|
+
active_list_bytes = file.read(24)
|
|
313
|
+
active_list_hex = active_list_bytes.hex()
|
|
314
|
+
active_lists.append(active_list_hex)
|
|
315
|
+
|
|
316
|
+
# Lire l'angle (1 byte signé)
|
|
317
|
+
angle_byte = file.read(1)
|
|
318
|
+
angle = np.frombuffer(angle_byte, dtype=np.int8)[0]
|
|
319
|
+
angles.append(angle)
|
|
320
|
+
|
|
321
|
+
# Lire le signal AO (float32)
|
|
322
|
+
data = np.frombuffer(file.read(n_acquisitions_per_event * 4), dtype=np.float32)
|
|
323
|
+
if len(data) != n_acquisitions_per_event:
|
|
324
|
+
raise ValueError(f"Erreur à l'événement {j} : attendu {n_acquisitions_per_event}, obtenu {len(data)}")
|
|
325
|
+
AO_signal[:, j] = data
|
|
326
|
+
|
|
327
|
+
return AO_signal
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
elif AOsignalPath.endswith(".npy"):
|
|
331
|
+
return np.load(AOsignalPath) # Supposé déjà au bon format
|
|
332
|
+
else:
|
|
333
|
+
raise ValueError("Format de fichier non supporté. Utilisez .cdh/.cdf ou .npy.")
|
|
334
|
+
|
|
335
|
+
def saveAOsignals_Castor(self, save_directory, withTumor=True):
|
|
336
|
+
if withTumor:
|
|
337
|
+
AO_signal = self.AOsignal_withTumor
|
|
338
|
+
cdf_location = os.path.join(save_directory, "AOSignals_withTumor.cdf")
|
|
339
|
+
cdh_location = os.path.join(save_directory, "AOSignals_withTumor.cdh")
|
|
340
|
+
else:
|
|
341
|
+
AO_signal = self.AOsignal_withoutTumor
|
|
342
|
+
cdf_location = os.path.join(save_directory, "AOSignals_withoutTumor.cdf")
|
|
343
|
+
cdh_location = os.path.join(save_directory, "AOSignals_withoutTumor.cdh")
|
|
344
|
+
|
|
345
|
+
info_location = os.path.join(save_directory, "info.txt")
|
|
346
|
+
nScan = AO_signal.shape[1]
|
|
347
|
+
|
|
348
|
+
with open(cdf_location, "wb") as fileID:
|
|
349
|
+
for j in range(AO_signal.shape[1]):
|
|
350
|
+
active_list_hex = self.AcousticFields[j].pattern.activeList
|
|
351
|
+
for i in range(0, len(active_list_hex), 2):
|
|
352
|
+
byte_value = int(active_list_hex[i:i+2], 16)
|
|
353
|
+
fileID.write(byte_value.to_bytes(1, byteorder='big'))
|
|
354
|
+
angle = self.AcousticFields[j].angle
|
|
355
|
+
fileID.write(np.int8(angle).tobytes())
|
|
356
|
+
fileID.write(AO_signal[:, j].astype(np.float32).tobytes())
|
|
357
|
+
|
|
358
|
+
header_content = (
|
|
359
|
+
f"Data filename: {'AOSignals_withTumor.cdf' if withTumor else 'AOSignals_withoutTumor.cdf'}\n"
|
|
360
|
+
f"Number of events: {nScan}\n"
|
|
361
|
+
f"Number of acquisitions per event: {AO_signal.shape[0]}\n"
|
|
362
|
+
f"Start time (s): 0\n"
|
|
363
|
+
f"Duration (s): 1\n"
|
|
364
|
+
f"Acquisition frequency (Hz): {self.params.acoustic['f_saving']}\n"
|
|
365
|
+
f"Data mode: histogram\n"
|
|
366
|
+
f"Data type: AOT\n"
|
|
367
|
+
f"Number of US transducers: {self.params.acoustic['num_elements']}"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
with open(cdh_location, "w") as fileID:
|
|
371
|
+
fileID.write(header_content)
|
|
372
|
+
|
|
373
|
+
with open(info_location, "w") as fileID:
|
|
374
|
+
for field in self.AcousticFields:
|
|
375
|
+
fileID.write(field.getName_field() + "\n")
|
|
376
|
+
|
|
377
|
+
print(f"Fichiers .cdf, .cdh et info.txt sauvegardés dans {save_directory}")
|
|
378
|
+
|
|
379
|
+
def showAOsignal(self, withTumor=True, save_dir=None, wave_name=None):
|
|
380
|
+
if withTumor and self.AOsignal_withTumor is None:
|
|
381
|
+
raise ValueError("AO signal with tumor is not generated. Please generate it first.")
|
|
382
|
+
if not withTumor and self.AOsignal_withoutTumor is None:
|
|
383
|
+
raise ValueError("AO signal without tumor is not generated. Please generate it first.")
|
|
384
|
+
|
|
385
|
+
if withTumor:
|
|
386
|
+
AOsignal = self.AOsignal_withTumor
|
|
387
|
+
else:
|
|
388
|
+
AOsignal = self.AOsignal_withoutTumor
|
|
389
|
+
|
|
390
|
+
time_axis = np.arange(AOsignal.shape[0]) / float(self.params.acoustic['f_AQ']) * 1e6
|
|
391
|
+
|
|
392
|
+
num_plots = AOsignal.shape[1]
|
|
393
|
+
if num_plots <= 5:
|
|
394
|
+
nrows, ncols = 1, num_plots
|
|
395
|
+
else:
|
|
396
|
+
ncols = 5
|
|
397
|
+
nrows = (num_plots + ncols - 1) // ncols
|
|
398
|
+
|
|
399
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 5.3 * nrows))
|
|
400
|
+
if isinstance(axes, plt.Axes):
|
|
401
|
+
axes = np.array([axes])
|
|
402
|
+
axes = axes.flatten()
|
|
403
|
+
|
|
404
|
+
if wave_name is None:
|
|
405
|
+
title = "AO Signal -- all plots"
|
|
406
|
+
else:
|
|
407
|
+
title = f"AO Signal -- {wave_name}"
|
|
408
|
+
|
|
409
|
+
fig.suptitle(title, fontsize=12, y=0.98)
|
|
410
|
+
|
|
411
|
+
for idx in range(num_plots):
|
|
412
|
+
ax = axes[idx]
|
|
413
|
+
ax.plot(time_axis, AOsignal[:, idx])
|
|
414
|
+
ax.set_xlabel("Time (µs)", fontsize=8)
|
|
415
|
+
ax.set_ylabel("Value", fontsize=8)
|
|
416
|
+
|
|
417
|
+
for j in range(num_plots, len(axes)):
|
|
418
|
+
fig.delaxes(axes[j])
|
|
419
|
+
|
|
420
|
+
plt.tight_layout(rect=[0, 0, 1, 0.95])
|
|
421
|
+
|
|
422
|
+
if save_dir is not None:
|
|
423
|
+
now = datetime.now()
|
|
424
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
425
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
426
|
+
save_filename = f"Static_y_Plot{wave_name}_{date_str}.png"
|
|
427
|
+
save_path = os.path.join(save_dir, save_filename)
|
|
428
|
+
plt.savefig(save_path, dpi=200)
|
|
429
|
+
print(f"Saved: {save_path}")
|
|
430
|
+
|
|
431
|
+
plt.show()
|
|
432
|
+
plt.close(fig)
|
|
433
|
+
|
|
434
|
+
def show_animated_all(self, fileOfAcousticField=None, save_dir=None, desired_duration_ms=5000):
|
|
435
|
+
mpl.rcParams['animation.embed_limit'] = 100
|
|
436
|
+
pattern_str = StructuredWave.getPattern(fileOfAcousticField)
|
|
437
|
+
angle = StructuredWave.getAngle(fileOfAcousticField)
|
|
438
|
+
fieldToPlot = None
|
|
439
|
+
|
|
440
|
+
for field in self.AcousticFields:
|
|
441
|
+
if field.get_path() == fileOfAcousticField:
|
|
442
|
+
fieldToPlot = field
|
|
443
|
+
idx = self.AcousticFields.index(field)
|
|
444
|
+
break
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError(f"Field {fileOfAcousticField} not found in AcousticFields.")
|
|
447
|
+
|
|
448
|
+
if wave_name is None:
|
|
449
|
+
wave_name = f"Pattern structure {pattern_str}"
|
|
450
|
+
|
|
451
|
+
fig, axs = plt.subplots(1, 2, figsize=(6 * 2, 5.3 * 1))
|
|
452
|
+
if isinstance(axs, plt.Axes):
|
|
453
|
+
axs = np.array([axs])
|
|
454
|
+
|
|
455
|
+
fig.suptitle(f"AO Signal Animation {wave_name} | Angle {angle}°", fontsize=12, y=0.98)
|
|
456
|
+
|
|
457
|
+
axs[0].imshow(self.OpticImage.T, cmap='hot', alpha=1, origin='upper',
|
|
458
|
+
extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
|
|
459
|
+
aspect='equal')
|
|
460
|
+
|
|
461
|
+
im_field = axs[0].imshow(fieldToPlot[0, :, :, idx], cmap='jet', origin='upper',
|
|
462
|
+
extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
|
|
463
|
+
vmax=1, vmin=0.01, alpha=0.8, aspect='equal')
|
|
464
|
+
|
|
465
|
+
axs[0].set_title(f"{wave_name} | Angle {angle}° | t = 0.00 ms", fontsize=10)
|
|
466
|
+
axs[0].set_xlabel("x (mm)", fontsize=8)
|
|
467
|
+
axs[0].set_ylabel("z (mm)", fontsize=8)
|
|
468
|
+
|
|
469
|
+
time_axis = np.arange(self.AOsignal.shape[0]) * 25e-6 * 1000
|
|
470
|
+
line_y, = axs[1].plot(time_axis, self.AOsignal[:, idx])
|
|
471
|
+
vertical_line, = axs[1].plot([time_axis[0], time_axis[0]], [0, self.AOsignal[0, idx]], 'r--')
|
|
472
|
+
|
|
473
|
+
axs[1].set_xlabel("Time (ms)", fontsize=8)
|
|
474
|
+
axs[1].set_ylabel("Value", fontsize=8)
|
|
475
|
+
axs[1].set_title(f"{wave_name} | Angle {angle}° | t = 0.00 ms", fontsize=10)
|
|
476
|
+
|
|
477
|
+
plt.tight_layout(rect=[0, 0, 1, 0.95])
|
|
478
|
+
|
|
479
|
+
def update(frame):
|
|
480
|
+
current_time_ms = frame * 25e-6 * 1000
|
|
481
|
+
frame_data = fieldToPlot[frame, :, :, idx]
|
|
482
|
+
masked_data = np.where(frame_data > 0.02, frame_data, np.nan)
|
|
483
|
+
im_field.set_data(masked_data)
|
|
484
|
+
axs[0].set_title(f"{wave_name} | Angle {angle}° | t = {current_time_ms:.2f} ms", fontsize=10)
|
|
485
|
+
|
|
486
|
+
y_vals = self.AOsignal[:, idx]
|
|
487
|
+
y_copy = np.full_like(y_vals, np.nan)
|
|
488
|
+
y_copy[:frame + 1] = y_vals[:frame + 1]
|
|
489
|
+
line_y.set_data(time_axis, y_copy)
|
|
490
|
+
|
|
491
|
+
vertical_line.set_data([time_axis[frame], time_axis[frame]], [0, y_vals[frame]])
|
|
492
|
+
axs[1].set_title(f"{wave_name} | Angle {angle}° | t = {current_time_ms:.2f} ms", fontsize=10)
|
|
493
|
+
|
|
494
|
+
return [im_field, vertical_line, line_y]
|
|
495
|
+
|
|
496
|
+
interval = desired_duration_ms / fieldToPlot.shape[0]
|
|
497
|
+
ani = animation.FuncAnimation(
|
|
498
|
+
fig, update,
|
|
499
|
+
frames=range(0, self.AcousticFields.shape[0]),
|
|
500
|
+
interval=interval, blit=True
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if save_dir is not None:
|
|
504
|
+
now = datetime.now()
|
|
505
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
506
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
507
|
+
save_filename = f"A_y_LAMBDA_overlay_{pattern_str}_{angle}_{date_str}.gif"
|
|
508
|
+
save_path = os.path.join(save_dir, save_filename)
|
|
509
|
+
ani.save(save_path, writer='pillow', fps=20)
|
|
510
|
+
print(f"Saved: {save_path}")
|
|
511
|
+
|
|
512
|
+
plt.close(fig)
|
|
513
|
+
return ani
|
|
514
|
+
|
|
515
|
+
def showPhantom(self, withROI=False):
|
|
516
|
+
"""
|
|
517
|
+
Displays the optical phantom with absorbers.
|
|
518
|
+
"""
|
|
519
|
+
try:
|
|
520
|
+
if withROI:
|
|
521
|
+
self.OpticImage.show_ROI()
|
|
522
|
+
else:
|
|
523
|
+
self.OpticImage.show_phantom()
|
|
524
|
+
except Exception as e:
|
|
525
|
+
raise RuntimeError(f"Error plotting phantom: {e}")
|
|
526
|
+
|
|
527
|
+
@abstractmethod
|
|
528
|
+
def check(self):
|
|
529
|
+
"""
|
|
530
|
+
Check if the experiment is correctly initialized.
|
|
531
|
+
"""
|
|
532
|
+
pass
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
class Absorber:
|
|
3
|
+
def __init__(self, name, type, center, radius, amplitude):
|
|
4
|
+
"""
|
|
5
|
+
Initializes an absorber with the given parameters.
|
|
6
|
+
:param name: Name of the absorber.
|
|
7
|
+
:param type: Type of the absorber.
|
|
8
|
+
:param center: Center of the absorber.
|
|
9
|
+
:param radius: Radius of the absorber.
|
|
10
|
+
:param amplitude: Amplitude of the absorber.
|
|
11
|
+
"""
|
|
12
|
+
self.name = name
|
|
13
|
+
self.type = type
|
|
14
|
+
self.center = center
|
|
15
|
+
self.radius = radius
|
|
16
|
+
self.amplitude = amplitude
|
|
17
|
+
|
|
18
|
+
def __repr__(self):
|
|
19
|
+
"""
|
|
20
|
+
String representation of the absorber.
|
|
21
|
+
:return: String representing the absorber.
|
|
22
|
+
"""
|
|
23
|
+
return (f"Absorber(name={self.name}, type={self.type}, "
|
|
24
|
+
f"center={self.center}, radius={self.radius}, amplitude={self.amplitude})")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from .OpticEnums import OpticFieldType
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
class Laser:
|
|
6
|
+
def __init__(self, params):
|
|
7
|
+
"""
|
|
8
|
+
Initializes the laser with the given parameters.
|
|
9
|
+
:param params: Configuration parameters for the laser.
|
|
10
|
+
"""
|
|
11
|
+
try:
|
|
12
|
+
self.x = np.arange(params.general['Xrange'][0], params.general['Xrange'][1], params.general['dx']) * 1000
|
|
13
|
+
self.z = np.arange(params.general['Zrange'][0], params.general['Zrange'][1], params.general['dz']) * 1000
|
|
14
|
+
self.shape = OpticFieldType(params.optic['laser']['shape'].capitalize())
|
|
15
|
+
self.center = params.optic['laser']['center']
|
|
16
|
+
self.w0 = params.optic['laser']['w0'] * 1000
|
|
17
|
+
self._set_intensity()
|
|
18
|
+
except KeyError as e:
|
|
19
|
+
raise ValueError(f"Missing parameter: {e}")
|
|
20
|
+
except ValueError as e:
|
|
21
|
+
raise ValueError(f"Invalid laser shape: {e}")
|
|
22
|
+
|
|
23
|
+
def _set_intensity(self):
|
|
24
|
+
"""
|
|
25
|
+
Sets the intensity of the beam based on its shape.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
if self.shape == OpticFieldType.GAUSSIAN:
|
|
29
|
+
self.intensity = self._gaussian_beam()
|
|
30
|
+
elif self.shape == OpticFieldType.UNIFORM:
|
|
31
|
+
raise NotImplementedError("Uniform beam not implemented yet.")
|
|
32
|
+
elif self.shape == OpticFieldType.SPHERICAL:
|
|
33
|
+
raise NotImplementedError("Spherical beam not implemented yet.")
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError("Unknown beam shape.")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise RuntimeError(f"Error setting intensity: {e}")
|
|
38
|
+
|
|
39
|
+
def _gaussian_beam(self):
|
|
40
|
+
"""
|
|
41
|
+
Generates a Gaussian laser beam in the XZ plane.
|
|
42
|
+
:return: Intensity matrix of the Gaussian beam.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
if self.center == 'center':
|
|
46
|
+
x0 = (self.x[0] + self.x[-1]) / 2
|
|
47
|
+
z0 = (self.z[0] + self.z[-1]) / 2
|
|
48
|
+
else:
|
|
49
|
+
x0 = self.center[0] * 1000
|
|
50
|
+
z0 = self.center[1] * 1000
|
|
51
|
+
X, Z = np.meshgrid(self.x, self.z, indexing='ij')
|
|
52
|
+
return np.exp(-2 * ((X - x0)**2 + (Z - z0)**2) / self.w0**2)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise RuntimeError(f"Error generating Gaussian beam: {e}")
|
|
55
|
+
|
|
56
|
+
def show_laser(self):
|
|
57
|
+
"""
|
|
58
|
+
Displays the laser intensity distribution.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
import matplotlib.pyplot as plt
|
|
62
|
+
plt.imshow(self.intensity, extent=(self.x[0], self.x[-1] + 1, self.z[-1], self.z[0]), aspect='auto', cmap='hot')
|
|
63
|
+
plt.colorbar(label='Intensity')
|
|
64
|
+
plt.xlabel('X (mm)', fontsize=20)
|
|
65
|
+
plt.ylabel('Z (mm)', fontsize=20)
|
|
66
|
+
plt.tick_params(axis='both', which='major', labelsize=20)
|
|
67
|
+
plt.title('Laser Intensity Distribution')
|
|
68
|
+
plt.show()
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise RuntimeError(f"Error plotting laser intensity: {e}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class OpticFieldType(Enum):
|
|
4
|
+
"""
|
|
5
|
+
Enumeration of available optic field types.
|
|
6
|
+
|
|
7
|
+
Selection of optic field types:
|
|
8
|
+
- GAUSSIAN: A Gaussian optic field type.
|
|
9
|
+
- UNIFORM: A uniform optic field type.
|
|
10
|
+
- SPHERICAL: A spherical optic field type.
|
|
11
|
+
"""
|
|
12
|
+
GAUSSIAN = "Gaussian"
|
|
13
|
+
"""A Gaussian optic field type."""
|
|
14
|
+
UNIFORM = "Uniform"
|
|
15
|
+
"""A uniform optic field type."""
|
|
16
|
+
SPHERICAL = "Spherical"
|
|
17
|
+
"""A spherical optic field type."""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ._mainOptic import *
|
|
2
|
+
from .Absorber import *
|
|
3
|
+
from .Laser import *
|
|
4
|
+
from .OpticEnums import *
|
|
5
|
+
|
|
6
|
+
# Docstring for the AOT_Optic package
|
|
7
|
+
"""
|
|
8
|
+
AOT_Optic is a package for optical components in Acousto-Optic Tomography.
|
|
9
|
+
It provides tools and classes for working with optical elements such as absorbers and lasers.
|
|
10
|
+
"""
|