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,1023 @@
|
|
|
1
|
+
from ._mainRecon import Recon
|
|
2
|
+
from .ReconEnums import ReconType, OptimizerType, ProcessType, SparsingType
|
|
3
|
+
from .AOT_Optimizers import MLEM, LS
|
|
4
|
+
from AOT_biomaps.Config import config
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import numpy as np
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import matplotlib.animation as animation
|
|
11
|
+
from IPython.display import HTML
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from tempfile import gettempdir
|
|
14
|
+
import cupy as cp
|
|
15
|
+
import cupyx.scipy.sparse as cpsparse
|
|
16
|
+
import gc
|
|
17
|
+
from tqdm import trange
|
|
18
|
+
|
|
19
|
+
class AlgebraicRecon(Recon):
|
|
20
|
+
"""
|
|
21
|
+
This class implements the Algebraic reconstruction process.
|
|
22
|
+
It currently does not perform any operations but serves as a template for future implementations.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, opti = OptimizerType.MLEM, numIterations = 10000, numSubsets = 1, isSavingEachIteration=True, maxSaves = 5000, alpha = None, denominatorThreshold = 1e-6, useSparseSMatrix=True, sparseType = SparsingType.CSR, sparseThreshold=0.1, device = None, **kwargs):
|
|
25
|
+
super().__init__(**kwargs)
|
|
26
|
+
self.reconType = ReconType.Algebraic
|
|
27
|
+
self.optimizer = opti
|
|
28
|
+
self.reconPhantom = []
|
|
29
|
+
self.reconLaser = []
|
|
30
|
+
self.indices = []
|
|
31
|
+
self.numIterations = numIterations
|
|
32
|
+
self.numSubsets = numSubsets
|
|
33
|
+
self.isSavingEachIteration = isSavingEachIteration
|
|
34
|
+
self.maxSaves = maxSaves
|
|
35
|
+
self.denominatorThreshold = denominatorThreshold
|
|
36
|
+
self.alpha = alpha # Regularization parameter for LS
|
|
37
|
+
self.device = device
|
|
38
|
+
self.SMatrix_sparse = None # Sparse system matrix
|
|
39
|
+
self.sparseThreshold = sparseThreshold
|
|
40
|
+
self.useSparseSMatrix = useSparseSMatrix # Whether to use sparse SMatrix in optimizers
|
|
41
|
+
self.sparseType = sparseType
|
|
42
|
+
self.Z_dim = None # Used for sparse matrix reconstruction
|
|
43
|
+
|
|
44
|
+
if self.numIterations <= 0:
|
|
45
|
+
raise ValueError("Number of iterations must be greater than 0.")
|
|
46
|
+
if self.numSubsets <= 0:
|
|
47
|
+
raise ValueError("Number of subsets must be greater than 0.")
|
|
48
|
+
if type(self.numIterations) is not int:
|
|
49
|
+
raise TypeError("Number of iterations must be an integer.")
|
|
50
|
+
if type(self.numSubsets) is not int:
|
|
51
|
+
raise TypeError("Number of subsets must be an integer.")
|
|
52
|
+
|
|
53
|
+
print("Generating system matrix (processing acoustic fields)...")
|
|
54
|
+
self.SMatrix = self._sparseSMatrix()
|
|
55
|
+
|
|
56
|
+
# PUBLIC METHODS
|
|
57
|
+
|
|
58
|
+
def run(self, processType = ProcessType.PYTHON, withTumor= True, show_logs=True):
|
|
59
|
+
"""
|
|
60
|
+
This method is a placeholder for the Algebraic reconstruction process.
|
|
61
|
+
It currently does not perform any operations but serves as a template for future implementations.
|
|
62
|
+
"""
|
|
63
|
+
if(processType == ProcessType.CASToR):
|
|
64
|
+
self._AlgebraicReconCASToR(withTumor=withTumor, show_logs=show_logs)
|
|
65
|
+
elif(processType == ProcessType.PYTHON):
|
|
66
|
+
self._AlgebraicReconPython(withTumor=withTumor, show_logs=show_logs)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Unknown Algebraic reconstruction type: {processType}")
|
|
69
|
+
|
|
70
|
+
def sparse_SMatrix(self):
|
|
71
|
+
if self.sparseType == SparsingType.CSR:
|
|
72
|
+
self.SMatrix_sparse, self.Z_dim = self._sparseSMatrix_CSR(self.experiment.AcousticFields, threshold=self.sparseThreshold)
|
|
73
|
+
if self.sparseType == SparsingType.COO:
|
|
74
|
+
raise NotImplementedError("COO sparse matrix not implemented yet.")
|
|
75
|
+
|
|
76
|
+
def plot_MSE(self, isSaving=True, log_scale_x=False, log_scale_y=False, show_logs=True):
|
|
77
|
+
"""
|
|
78
|
+
Plot the Mean Squared Error (MSE) of the reconstruction.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
isSaving: bool, whether to save the plot.
|
|
82
|
+
log_scale_x: bool, if True, use logarithmic scale for the x-axis.
|
|
83
|
+
log_scale_y: bool, if True, use logarithmic scale for the y-axis.
|
|
84
|
+
Returns:
|
|
85
|
+
None
|
|
86
|
+
"""
|
|
87
|
+
if not self.MSE:
|
|
88
|
+
raise ValueError("MSE is empty. Please calculate MSE first.")
|
|
89
|
+
|
|
90
|
+
best_idx = self.indices[np.argmin(self.MSE)]
|
|
91
|
+
if show_logs:
|
|
92
|
+
print(f"Lowest MSE = {np.min(self.MSE):.4f} at iteration {best_idx+1}")
|
|
93
|
+
# Plot MSE curve
|
|
94
|
+
plt.figure(figsize=(7, 5))
|
|
95
|
+
plt.plot(self.indices, self.MSE, 'r-', label="MSE curve")
|
|
96
|
+
# Add blue dashed lines
|
|
97
|
+
plt.axhline(np.min(self.MSE), color='blue', linestyle='--', label=f"Min MSE = {np.min(self.MSE):.4f}")
|
|
98
|
+
plt.axvline(best_idx, color='blue', linestyle='--', label=f"Iteration = {best_idx+1}")
|
|
99
|
+
plt.xlabel("Iteration")
|
|
100
|
+
plt.ylabel("MSE")
|
|
101
|
+
plt.title("MSE vs. Iteration")
|
|
102
|
+
if log_scale_x:
|
|
103
|
+
plt.xscale('log')
|
|
104
|
+
if log_scale_y:
|
|
105
|
+
plt.yscale('log')
|
|
106
|
+
plt.legend()
|
|
107
|
+
plt.grid(True, which="both", ls="-")
|
|
108
|
+
plt.tight_layout()
|
|
109
|
+
if isSaving and self.saveDir is not None:
|
|
110
|
+
now = datetime.now()
|
|
111
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
112
|
+
scale_str = ""
|
|
113
|
+
if log_scale_x and log_scale_y:
|
|
114
|
+
scale_str = "_loglog"
|
|
115
|
+
elif log_scale_x:
|
|
116
|
+
scale_str = "_logx"
|
|
117
|
+
elif log_scale_y:
|
|
118
|
+
scale_str = "_logy"
|
|
119
|
+
SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_MSE_plot_{self.optimizer.name}_{scale_str}{date_str}.png')
|
|
120
|
+
plt.savefig(SavingFolder, dpi=300)
|
|
121
|
+
if show_logs:
|
|
122
|
+
print(f"MSE plot saved to {SavingFolder}")
|
|
123
|
+
|
|
124
|
+
plt.show()
|
|
125
|
+
|
|
126
|
+
def show_MSE_bestRecon(self, isSaving=True, show_logs=True):
|
|
127
|
+
if not self.MSE:
|
|
128
|
+
raise ValueError("MSE is empty. Please calculate MSE first.")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
best_idx = np.argmin(self.MSE)
|
|
132
|
+
best_recon = self.reconPhantom[best_idx]
|
|
133
|
+
|
|
134
|
+
# Crée la figure et les axes
|
|
135
|
+
fig, axs = plt.subplots(1, 3, figsize=(15, 5))
|
|
136
|
+
|
|
137
|
+
# Left: Best reconstructed image (normalized)
|
|
138
|
+
im0 = axs[0].imshow(best_recon,
|
|
139
|
+
extent=(self.experiment.params.general['Xrange'][0]*1000, self.experiment.params.general['Xrange'][1]*1000,
|
|
140
|
+
self.experiment.params.general['Zrange'][1]*1000, self.experiment.params.general['Zrange'][0]*1000),
|
|
141
|
+
cmap='hot', aspect='equal', vmin=0, vmax=1)
|
|
142
|
+
axs[0].set_title(f"Min MSE Reconstruction\nIter {self.indices[best_idx]}, MSE={np.min(self.MSE):.4f}")
|
|
143
|
+
axs[0].set_xlabel("x (mm)", fontsize=12)
|
|
144
|
+
axs[0].set_ylabel("z (mm)", fontsize=12)
|
|
145
|
+
axs[0].tick_params(axis='both', which='major', labelsize=8)
|
|
146
|
+
|
|
147
|
+
# Middle: Ground truth (normalized)
|
|
148
|
+
im1 = axs[1].imshow(self.experiment.OpticImage.phantom,
|
|
149
|
+
extent=(self.experiment.params.general['Xrange'][0]*1000, self.experiment.params.general['Xrange'][1]*1000,
|
|
150
|
+
self.experiment.params.general['Zrange'][1]*1000, self.experiment.params.general['Zrange'][0]*1000),
|
|
151
|
+
cmap='hot', aspect='equal', vmin=0, vmax=1)
|
|
152
|
+
axs[1].set_title(r"Ground Truth ($\lambda$)")
|
|
153
|
+
axs[1].set_xlabel("x (mm)", fontsize=12)
|
|
154
|
+
axs[1].set_ylabel("z (mm)", fontsize=12)
|
|
155
|
+
axs[1].tick_params(axis='both', which='major', labelsize=8)
|
|
156
|
+
axs[1].tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
|
|
157
|
+
|
|
158
|
+
# Right: Reconstruction at last iteration
|
|
159
|
+
lastRecon = self.reconPhantom[-1]
|
|
160
|
+
if self.experiment.OpticImage.phantom.shape != lastRecon.shape:
|
|
161
|
+
lastRecon = lastRecon.T
|
|
162
|
+
im2 = axs[2].imshow(lastRecon,
|
|
163
|
+
extent=(self.experiment.params.general['Xrange'][0]*1000, self.experiment.params.general['Xrange'][1]*1000,
|
|
164
|
+
self.experiment.params.general['Zrange'][1]*1000, self.experiment.params.general['Zrange'][0]*1000),
|
|
165
|
+
cmap='hot', aspect='equal', vmin=0, vmax=1)
|
|
166
|
+
axs[2].set_title(f"Last Reconstruction\nIter {self.numIterations * self.numSubsets}, MSE={np.mean((self.experiment.OpticImage.phantom - lastRecon) ** 2):.4f}")
|
|
167
|
+
axs[2].set_xlabel("x (mm)", fontsize=12)
|
|
168
|
+
axs[2].set_ylabel("z (mm)", fontsize=12)
|
|
169
|
+
axs[2].tick_params(axis='both', which='major', labelsize=8)
|
|
170
|
+
|
|
171
|
+
# Ajoute une colorbar horizontale centrée en dessous des trois plots
|
|
172
|
+
fig.subplots_adjust(bottom=0.2)
|
|
173
|
+
cbar_ax = fig.add_axes([0.25, 0.08, 0.5, 0.03])
|
|
174
|
+
cbar = fig.colorbar(im2, cax=cbar_ax, orientation='horizontal')
|
|
175
|
+
cbar.set_label('Normalized Intensity', fontsize=12)
|
|
176
|
+
cbar.ax.tick_params(labelsize=8)
|
|
177
|
+
|
|
178
|
+
plt.subplots_adjust(wspace=0.3)
|
|
179
|
+
|
|
180
|
+
if isSaving and self.saveDir is not None:
|
|
181
|
+
now = datetime.now()
|
|
182
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
183
|
+
savePath = os.path.join(self.saveDir, 'results')
|
|
184
|
+
if not os.path.exists(savePath):
|
|
185
|
+
os.makedirs(savePath)
|
|
186
|
+
SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_comparison_MSE_BestANDLastRecon_{self.optimizer.name}_{date_str}.png')
|
|
187
|
+
plt.savefig(SavingFolder, dpi=300, bbox_inches='tight')
|
|
188
|
+
if show_logs:
|
|
189
|
+
print(f"MSE plot saved to {SavingFolder}")
|
|
190
|
+
|
|
191
|
+
plt.show()
|
|
192
|
+
|
|
193
|
+
def show_theta_animation(self, vmin=None, vmax=None, total_duration_ms=3000, save_path=None, max_frames=1000, isPropMSE=True, show_logs=True):
|
|
194
|
+
"""
|
|
195
|
+
Show theta iteration animation with speed proportional to MSE acceleration.
|
|
196
|
+
In "propMSE" mode: slow down when MSE changes rapidly, speed up when MSE stagnates.
|
|
197
|
+
|
|
198
|
+
Parameters:
|
|
199
|
+
vmin, vmax: color limits (optional)
|
|
200
|
+
total_duration_ms: total duration of the animation in milliseconds
|
|
201
|
+
save_path: path to save animation (e.g., 'theta.gif')
|
|
202
|
+
max_frames: maximum number of frames to include (default: 1000)
|
|
203
|
+
isPropMSE: if True, use adaptive speed based on MSE (default: True)
|
|
204
|
+
"""
|
|
205
|
+
import matplotlib as mpl
|
|
206
|
+
mpl.rcParams['animation.embed_limit'] = 200
|
|
207
|
+
|
|
208
|
+
if len(self.reconPhantom) == 0 or len(self.reconPhantom) < 2:
|
|
209
|
+
raise ValueError("Not enough theta matrices available for animation.")
|
|
210
|
+
|
|
211
|
+
if isPropMSE and (self.MSE is None or len(self.MSE) == 0):
|
|
212
|
+
raise ValueError("MSE is empty or not calculated. Please calculate MSE first.")
|
|
213
|
+
|
|
214
|
+
frames = np.array(self.reconPhantom)
|
|
215
|
+
mse = np.array(self.MSE)
|
|
216
|
+
|
|
217
|
+
# Sous-échantillonnage initial
|
|
218
|
+
step = max(1, len(frames) // max_frames)
|
|
219
|
+
frames_subset = frames[::step]
|
|
220
|
+
indices_subset = self.indices[::step]
|
|
221
|
+
mse_subset = mse[::step]
|
|
222
|
+
|
|
223
|
+
if vmin is None:
|
|
224
|
+
vmin = np.min(frames_subset)
|
|
225
|
+
if vmax is None:
|
|
226
|
+
vmax = np.max(frames_subset)
|
|
227
|
+
|
|
228
|
+
fig, ax = plt.subplots(figsize=(4, 4), dpi=100)
|
|
229
|
+
im = ax.imshow(
|
|
230
|
+
frames_subset[0],
|
|
231
|
+
extent=(
|
|
232
|
+
self.experiment.params.general['Xrange'][0],
|
|
233
|
+
self.experiment.params.general['Xrange'][1],
|
|
234
|
+
self.experiment.params.general['Zrange'][1],
|
|
235
|
+
self.experiment.params.general['Zrange'][0]
|
|
236
|
+
),
|
|
237
|
+
vmin=vmin,
|
|
238
|
+
vmax=vmax,
|
|
239
|
+
aspect='equal',
|
|
240
|
+
cmap='hot'
|
|
241
|
+
)
|
|
242
|
+
title = ax.set_title(f"Iteration {indices_subset[0]}")
|
|
243
|
+
ax.set_xlabel("x (mm)")
|
|
244
|
+
ax.set_ylabel("z (mm)")
|
|
245
|
+
plt.tight_layout()
|
|
246
|
+
|
|
247
|
+
if isPropMSE:
|
|
248
|
+
# Calcule la dérivée première (variation du MSE)
|
|
249
|
+
mse_diff = np.gradient(mse_subset)
|
|
250
|
+
# Calcule la dérivée seconde (accélération du MSE)
|
|
251
|
+
mse_accel = np.gradient(mse_diff)
|
|
252
|
+
# Normalise l'accélération entre 0 et 1 (en valeur absolue)
|
|
253
|
+
mse_accel_normalized = np.abs(mse_accel)
|
|
254
|
+
mse_accel_normalized /= (np.max(mse_accel_normalized) + 1e-10)
|
|
255
|
+
|
|
256
|
+
# Prépare les frames pour le mode "propMSE"
|
|
257
|
+
all_frames = []
|
|
258
|
+
all_indices = []
|
|
259
|
+
|
|
260
|
+
for i in range(len(frames_subset)):
|
|
261
|
+
# Nombre de duplications inversement proportionnel à l'accélération (pour ralentir quand MSE change vite)
|
|
262
|
+
# Plus l'accélération est élevée, plus on duplique (pour ralentir)
|
|
263
|
+
num_duplicates = max(1, int(1 + 9 * mse_accel_normalized[i]))
|
|
264
|
+
all_frames.extend([frames_subset[i]] * num_duplicates)
|
|
265
|
+
all_indices.extend([indices_subset[i]] * num_duplicates)
|
|
266
|
+
|
|
267
|
+
# Ajuste le nombre total de frames pour respecter la durée
|
|
268
|
+
target_frames = int(total_duration_ms / 10) # 10 ms par frame
|
|
269
|
+
if len(all_frames) > target_frames:
|
|
270
|
+
step_prop = len(all_frames) // target_frames
|
|
271
|
+
all_frames = all_frames[::step_prop]
|
|
272
|
+
all_indices = all_indices[::step_prop]
|
|
273
|
+
|
|
274
|
+
else: # Mode "linéaire"
|
|
275
|
+
all_frames = frames_subset
|
|
276
|
+
all_indices = indices_subset
|
|
277
|
+
|
|
278
|
+
def update(frame_idx):
|
|
279
|
+
im.set_array(all_frames[frame_idx])
|
|
280
|
+
title.set_text(f"Iteration {all_indices[frame_idx]}")
|
|
281
|
+
return [im, title]
|
|
282
|
+
|
|
283
|
+
ani = animation.FuncAnimation(
|
|
284
|
+
fig,
|
|
285
|
+
update,
|
|
286
|
+
frames=len(all_frames),
|
|
287
|
+
interval=10, # 10 ms par frame
|
|
288
|
+
blit=False,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if save_path:
|
|
292
|
+
if save_path.endswith(".gif"):
|
|
293
|
+
ani.save(save_path, writer=animation.PillowWriter(fps=100))
|
|
294
|
+
elif save_path.endswith(".mp4"):
|
|
295
|
+
ani.save(save_path, writer="ffmpeg", fps=30)
|
|
296
|
+
if show_logs:
|
|
297
|
+
print(f"Animation saved to {save_path}")
|
|
298
|
+
|
|
299
|
+
plt.close(fig)
|
|
300
|
+
return HTML(ani.to_jshtml())
|
|
301
|
+
|
|
302
|
+
def plot_SSIM(self, isSaving=True, log_scale_x=False, log_scale_y=False, show_logs=True):
|
|
303
|
+
if not self.SSIM:
|
|
304
|
+
raise ValueError("SSIM is empty. Please calculate SSIM first.")
|
|
305
|
+
|
|
306
|
+
best_idx = self.indices[np.argmax(self.SSIM)]
|
|
307
|
+
if show_logs:
|
|
308
|
+
print(f"Highest SSIM = {np.max(self.SSIM):.4f} at iteration {best_idx+1}")
|
|
309
|
+
# Plot SSIM curve
|
|
310
|
+
plt.figure(figsize=(7, 5))
|
|
311
|
+
plt.plot(self.indices, self.SSIM, 'r-', label="SSIM curve")
|
|
312
|
+
# Add blue dashed lines
|
|
313
|
+
plt.axhline(np.max(self.SSIM), color='blue', linestyle='--', label=f"Max SSIM = {np.max(self.SSIM):.4f}")
|
|
314
|
+
plt.axvline(best_idx, color='blue', linestyle='--', label=f"Iteration = {best_idx}")
|
|
315
|
+
plt.xlabel("Iteration")
|
|
316
|
+
plt.ylabel("SSIM")
|
|
317
|
+
plt.title("SSIM vs. Iteration")
|
|
318
|
+
if log_scale_x:
|
|
319
|
+
plt.xscale('log')
|
|
320
|
+
if log_scale_y:
|
|
321
|
+
plt.yscale('log')
|
|
322
|
+
plt.legend()
|
|
323
|
+
plt.grid(True, which="both", ls="-")
|
|
324
|
+
plt.tight_layout()
|
|
325
|
+
if isSaving and self.saveDir is not None:
|
|
326
|
+
now = datetime.now()
|
|
327
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
328
|
+
scale_str = ""
|
|
329
|
+
if log_scale_x and log_scale_y:
|
|
330
|
+
scale_str = "_loglog"
|
|
331
|
+
elif log_scale_x:
|
|
332
|
+
scale_str = "_logx"
|
|
333
|
+
elif log_scale_y:
|
|
334
|
+
scale_str = "_logy"
|
|
335
|
+
SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_SSIM_plot_{self.optimizer.name}_{scale_str}{date_str}.png')
|
|
336
|
+
plt.savefig(SavingFolder, dpi=300)
|
|
337
|
+
if show_logs:
|
|
338
|
+
print(f"SSIM plot saved to {SavingFolder}")
|
|
339
|
+
|
|
340
|
+
plt.show()
|
|
341
|
+
|
|
342
|
+
def show_SSIM_bestRecon(self, isSaving=True, show_logs=True):
|
|
343
|
+
|
|
344
|
+
if not self.SSIM:
|
|
345
|
+
raise ValueError("SSIM is empty. Please calculate SSIM first.")
|
|
346
|
+
|
|
347
|
+
best_idx = np.argmax(self.SSIM)
|
|
348
|
+
best_recon = self.reconPhantom[best_idx]
|
|
349
|
+
|
|
350
|
+
# ----------------- Plotting -----------------
|
|
351
|
+
_, axs = plt.subplots(1, 3, figsize=(15, 5)) # 1 row, 3 columns
|
|
352
|
+
|
|
353
|
+
# Left: Best reconstructed image (normalized)
|
|
354
|
+
im0 = axs[0].imshow(best_recon,
|
|
355
|
+
extent=(self.experiment.params.general['Xrange'][0], self.experiment.params.general['Xrange'][1], self.experiment.params.general['Zrange'][1], self.experiment.params.general['Zrange'][0]),
|
|
356
|
+
cmap='hot', aspect='equal', vmin=0, vmax=1)
|
|
357
|
+
axs[0].set_title(f"Max SSIM Reconstruction\nIter {self.indices[best_idx]}, SSIM={np.min(self.MSE):.4f}")
|
|
358
|
+
axs[0].set_xlabel("x (mm)")
|
|
359
|
+
axs[0].set_ylabel("z (mm)")
|
|
360
|
+
plt.colorbar(im0, ax=axs[0])
|
|
361
|
+
|
|
362
|
+
# Middle: Ground truth (normalized)
|
|
363
|
+
im1 = axs[1].imshow(self.experiment.OpticImage.laser.intensity,
|
|
364
|
+
extent=(self.experiment.params.general['Xrange'][0], self.experiment.params.general['Xrange'][1], self.experiment.params.general['Zrange'][1], self.experiment.params.general['Zrange'][0]),
|
|
365
|
+
cmap='hot', aspect='equal', vmin=0, vmax=1)
|
|
366
|
+
axs[1].set_title(r"Ground Truth ($\lambda$)")
|
|
367
|
+
axs[1].set_xlabel("x (mm)")
|
|
368
|
+
axs[1].set_ylabel("z (mm)")
|
|
369
|
+
plt.colorbar(im1, ax=axs[1])
|
|
370
|
+
|
|
371
|
+
# Right: Reconstruction at iter 350
|
|
372
|
+
lastRecon = self.reconPhantom[-1]
|
|
373
|
+
im2 = axs[2].imshow(lastRecon,
|
|
374
|
+
extent=(self.experiment.params.general['Xrange'][0], self.experiment.params.general['Xrange'][1], self.experiment.params.general['Zrange'][1], self.experiment.params.general['Zrange'][0]),
|
|
375
|
+
cmap='hot', aspect='equal', vmin=0, vmax=1)
|
|
376
|
+
axs[2].set_title(f"Last Reconstruction\nIter {self.numIterations * self.numSubsets}, SSIM={self.SSIM[-1]:.4f}")
|
|
377
|
+
axs[2].set_xlabel("x (mm)")
|
|
378
|
+
axs[2].set_ylabel("z (mm)")
|
|
379
|
+
plt.colorbar(im2, ax=axs[2])
|
|
380
|
+
|
|
381
|
+
plt.tight_layout()
|
|
382
|
+
if isSaving:
|
|
383
|
+
now = datetime.now()
|
|
384
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
385
|
+
SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_comparison_SSIM_BestANDLastRecon_{self.optimizer.name}_{date_str}.png')
|
|
386
|
+
plt.savefig(SavingFolder, dpi=300)
|
|
387
|
+
if show_logs:
|
|
388
|
+
print(f"SSIM plot saved to {SavingFolder}")
|
|
389
|
+
plt.show()
|
|
390
|
+
|
|
391
|
+
def plot_CRC_vs_Noise(self, use_ROI=True, fin=None, isSaving=True, show_logs=True):
|
|
392
|
+
"""
|
|
393
|
+
Plot CRC (Contrast Recovery Coefficient) vs Noise for each iteration.
|
|
394
|
+
"""
|
|
395
|
+
if self.reconLaser is None or self.reconLaser == []:
|
|
396
|
+
raise ValueError("Reconstructed laser is empty. Run reconstruction first.")
|
|
397
|
+
if isinstance(self.reconLaser, list) and len(self.reconLaser) == 1:
|
|
398
|
+
raise ValueError("Reconstructed Image without tumor is a single frame. Run reconstruction with isSavingEachIteration=True to get a sequence of frames.")
|
|
399
|
+
if self.reconPhantom is None or self.reconPhantom == []:
|
|
400
|
+
raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
|
|
401
|
+
if isinstance(self.reconPhantom, list) and len(self.reconPhantom) == 1:
|
|
402
|
+
raise ValueError("Reconstructed Image with tumor is a single frame. Run reconstruction with isSavingEachIteration=True to get a sequence of frames.")
|
|
403
|
+
|
|
404
|
+
if fin is None:
|
|
405
|
+
fin = len(self.reconPhantom) - 1
|
|
406
|
+
|
|
407
|
+
iter_range = self.indices
|
|
408
|
+
|
|
409
|
+
if self.CRC is None:
|
|
410
|
+
self.calculateCRC(use_ROI=use_ROI)
|
|
411
|
+
|
|
412
|
+
noise_values = []
|
|
413
|
+
|
|
414
|
+
for i in iter_range:
|
|
415
|
+
recon_without_tumor = self.reconLaser[i].T
|
|
416
|
+
# Noise
|
|
417
|
+
noise = np.mean(np.abs(recon_without_tumor - self.experiment.OpticImage.laser.intensity))
|
|
418
|
+
noise_values.append(noise)
|
|
419
|
+
|
|
420
|
+
plt.figure(figsize=(6, 5))
|
|
421
|
+
plt.plot(noise_values, self.CRC, 'o-', label=self.optimizer.name)
|
|
422
|
+
for i, (x, y) in zip(iter_range, zip(noise_values, self.CRC)):
|
|
423
|
+
plt.text(x, y, str(i), fontsize=5.5, ha='left', va='bottom')
|
|
424
|
+
|
|
425
|
+
plt.xlabel("Noise (mean absolute error)")
|
|
426
|
+
plt.ylabel("CRC (Contrast Recovery Coefficient)")
|
|
427
|
+
|
|
428
|
+
plt.xscale('log')
|
|
429
|
+
plt.yscale('log')
|
|
430
|
+
|
|
431
|
+
plt.title("CRC vs Noise over Iterations")
|
|
432
|
+
plt.grid(True)
|
|
433
|
+
plt.legend()
|
|
434
|
+
if isSaving:
|
|
435
|
+
now = datetime.now()
|
|
436
|
+
date_str = now.strftime("%Y_%d_%m_%y")
|
|
437
|
+
SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_CRCvsNOISE_{self.optimizer.name}_{date_str}.png')
|
|
438
|
+
plt.savefig(SavingFolder, dpi=300)
|
|
439
|
+
if show_logs:
|
|
440
|
+
print(f"CRCvsNOISE plot saved to {SavingFolder}")
|
|
441
|
+
plt.show()
|
|
442
|
+
|
|
443
|
+
def show_reconstruction_progress(self, start=0, fin=None, save_path=None, with_tumor=True, show_logs=True):
|
|
444
|
+
"""
|
|
445
|
+
Show the reconstruction progress for either with or without tumor.
|
|
446
|
+
If isPropMSE is True, the frame selection is adapted to MSE changes.
|
|
447
|
+
Otherwise, indices are evenly spaced between start and fin.
|
|
448
|
+
|
|
449
|
+
Parameters:
|
|
450
|
+
start: int, starting iteration index
|
|
451
|
+
fin: int, ending iteration index (inclusive)
|
|
452
|
+
duration: int, duration of the animation in milliseconds
|
|
453
|
+
save_path: str, path to save the figure (optional)
|
|
454
|
+
with_tumor: bool, if True, show reconstruction with tumor; else without (default: True)
|
|
455
|
+
isPropMSE: bool, if True, use adaptive speed based on MSE (default: True)
|
|
456
|
+
"""
|
|
457
|
+
import matplotlib as mpl
|
|
458
|
+
mpl.rcParams['animation.embed_limit'] = 200
|
|
459
|
+
|
|
460
|
+
if fin is None:
|
|
461
|
+
fin = len(self.reconPhantom) - 1 if with_tumor else len(self.reconLaser) - 1
|
|
462
|
+
|
|
463
|
+
# Check data availability
|
|
464
|
+
if with_tumor:
|
|
465
|
+
if self.reconPhantom is None or self.reconPhantom == []:
|
|
466
|
+
raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
|
|
467
|
+
if isinstance(self.reconPhantom, list) and len(self.reconPhantom) == 1:
|
|
468
|
+
raise ValueError("Reconstructed Image with tumor is a single frame. Run reconstruction with isSavingEachIteration=True.")
|
|
469
|
+
recon_list = self.reconPhantom
|
|
470
|
+
ground_truth = self.experiment.OpticImage.phantom
|
|
471
|
+
title_suffix = "with_tumor"
|
|
472
|
+
else:
|
|
473
|
+
if self.reconLaser is None or self.reconLaser == []:
|
|
474
|
+
raise ValueError("Reconstructed laser is empty. Run reconstruction first.")
|
|
475
|
+
if isinstance(self.reconLaser, list) and len(self.reconLaser) == 1:
|
|
476
|
+
raise ValueError("Reconstructed Image without tumor is a single frame. Run reconstruction with isSavingEachIteration=True.")
|
|
477
|
+
recon_list = self.reconLaser
|
|
478
|
+
ground_truth = self.experiment.OpticImage.laser.intensity
|
|
479
|
+
title_suffix = "without_tumor"
|
|
480
|
+
|
|
481
|
+
# Collect data for all iterations
|
|
482
|
+
recon_list_data = []
|
|
483
|
+
diff_abs_list = []
|
|
484
|
+
mse_list = []
|
|
485
|
+
noise_list = []
|
|
486
|
+
|
|
487
|
+
for i in range(start, fin + 1):
|
|
488
|
+
recon = recon_list[i]
|
|
489
|
+
diff_abs = np.abs(recon - ground_truth)
|
|
490
|
+
mse = np.mean((ground_truth.flatten() - recon.flatten())**2)
|
|
491
|
+
noise = np.mean(np.abs(recon - ground_truth))
|
|
492
|
+
|
|
493
|
+
recon_list_data.append(recon)
|
|
494
|
+
diff_abs_list.append(diff_abs)
|
|
495
|
+
mse_list.append(mse)
|
|
496
|
+
noise_list.append(noise)
|
|
497
|
+
|
|
498
|
+
# Calculate global min/max for difference images
|
|
499
|
+
global_min_diff = np.min([d.min() for d in diff_abs_list[1:]])
|
|
500
|
+
global_max_diff = np.max([d.max() for d in diff_abs_list[1:]])
|
|
501
|
+
|
|
502
|
+
# Evenly spaced indices
|
|
503
|
+
num_frames = min(5, fin - start + 1)
|
|
504
|
+
all_indices = np.linspace(start, fin, num_frames, dtype=int).tolist()
|
|
505
|
+
|
|
506
|
+
# Plot
|
|
507
|
+
nrows = min(5, len(all_indices))
|
|
508
|
+
ncols = 3 # Recon, |Recon - GT|, Ground Truth
|
|
509
|
+
vmin, vmax = 0, 1
|
|
510
|
+
|
|
511
|
+
fig, axs = plt.subplots(nrows=nrows, ncols=ncols, figsize=(12, 3 * nrows))
|
|
512
|
+
|
|
513
|
+
for i, iter_idx in enumerate(all_indices[:nrows]):
|
|
514
|
+
idx_in_list = iter_idx - start # Index in the collected data lists
|
|
515
|
+
recon = recon_list_data[idx_in_list]
|
|
516
|
+
diff_abs = diff_abs_list[idx_in_list]
|
|
517
|
+
mse_val = mse_list[idx_in_list]
|
|
518
|
+
noise = noise_list[idx_in_list]
|
|
519
|
+
|
|
520
|
+
im0 = axs[i, 0].imshow(recon, cmap='hot', vmin=vmin, vmax=vmax, aspect='equal')
|
|
521
|
+
axs[i, 0].set_title(f"Reconstruction\nIter {self.indices[iter_idx]}, MSE={mse_val:.2e}", fontsize=10)
|
|
522
|
+
axs[i, 0].axis('off')
|
|
523
|
+
plt.colorbar(im0, ax=axs[i, 0])
|
|
524
|
+
|
|
525
|
+
im1 = axs[i, 1].imshow(diff_abs, cmap='viridis',
|
|
526
|
+
vmin=global_min_diff,
|
|
527
|
+
vmax=global_max_diff,
|
|
528
|
+
aspect='equal')
|
|
529
|
+
axs[i, 1].set_title(f"|Recon - Ground Truth|\nNoise={noise:.2e}", fontsize=10)
|
|
530
|
+
axs[i, 1].axis('off')
|
|
531
|
+
plt.colorbar(im1, ax=axs[i, 1])
|
|
532
|
+
|
|
533
|
+
im2 = axs[i, 2].imshow(ground_truth, cmap='hot', vmin=vmin, vmax=vmax, aspect='equal')
|
|
534
|
+
axs[i, 2].set_title(r"Ground Truth", fontsize=10)
|
|
535
|
+
axs[i, 2].axis('off')
|
|
536
|
+
plt.colorbar(im2, ax=axs[i, 2])
|
|
537
|
+
|
|
538
|
+
plt.tight_layout()
|
|
539
|
+
|
|
540
|
+
if save_path:
|
|
541
|
+
# Add suffix to filename based on with_tumor parameter
|
|
542
|
+
if '.' in save_path:
|
|
543
|
+
name, ext = save_path.rsplit('.', 1)
|
|
544
|
+
save_path = f"{name}_{title_suffix}.{ext}"
|
|
545
|
+
else:
|
|
546
|
+
save_path = f"{save_path}_{title_suffix}"
|
|
547
|
+
plt.savefig(save_path, dpi=300)
|
|
548
|
+
if show_logs:
|
|
549
|
+
print(f"Figure saved to: {save_path}")
|
|
550
|
+
|
|
551
|
+
plt.show()
|
|
552
|
+
|
|
553
|
+
def checkExistingFile(self, date = None):
|
|
554
|
+
"""
|
|
555
|
+
Check if the reconstruction file already exists, based on current instance parameters.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
withTumor (bool): If True, checks reconPhantom.npy; otherwise, checks reconLaser.npy.
|
|
559
|
+
overwrite (bool): If False, returns False if the file exists.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
tuple: (bool: whether to save, str: the filepath)
|
|
563
|
+
"""
|
|
564
|
+
if self.saveDir is None:
|
|
565
|
+
raise ValueError("Save directory is not specified.")
|
|
566
|
+
if date is None:
|
|
567
|
+
date = datetime.now().strftime("%d%m")
|
|
568
|
+
results_dir = os.path.join(self.saveDir, f'results_{date}_{self.optimizer.value}')
|
|
569
|
+
if not os.path.exists(results_dir):
|
|
570
|
+
os.makedirs(results_dir)
|
|
571
|
+
|
|
572
|
+
if os.path.exists(os.path.join(results_dir,"indices.npy")):
|
|
573
|
+
return (True, results_dir)
|
|
574
|
+
|
|
575
|
+
return (False, results_dir)
|
|
576
|
+
|
|
577
|
+
def load(self, withTumor=True, results_date=None, optimizer=None, filePath=None, show_logs=True):
|
|
578
|
+
"""
|
|
579
|
+
Load the reconstruction results (reconPhantom or reconLaser) and indices as lists of 2D np arrays into self.
|
|
580
|
+
If the loaded file is a 3D array, it is split into a list of 2D arrays.
|
|
581
|
+
Args:
|
|
582
|
+
withTumor: If True, loads reconPhantom (with tumor), else reconLaser (without tumor).
|
|
583
|
+
results_date: Date string (format "ddmm") to specify which results to load. If None, uses the most recent date in saveDir.
|
|
584
|
+
optimizer: Optimizer name (as string or enum) to filter results. If None, uses the current optimizer of the instance.
|
|
585
|
+
filePath: Optional. If provided, loads directly from this path (overrides saveDir and results_date).
|
|
586
|
+
"""
|
|
587
|
+
if filePath is not None:
|
|
588
|
+
# Mode chargement direct depuis un fichier
|
|
589
|
+
recon_key = 'reconPhantom' if withTumor else 'reconLaser'
|
|
590
|
+
recon_path = filePath
|
|
591
|
+
if not os.path.exists(recon_path):
|
|
592
|
+
raise FileNotFoundError(f"No reconstruction file found at {recon_path}.")
|
|
593
|
+
# Charge le fichier (3D ou liste de 2D)
|
|
594
|
+
data = np.load(recon_path, allow_pickle=True)
|
|
595
|
+
# Découpe en liste de 2D si c'est un tableau 3D
|
|
596
|
+
if isinstance(data, np.ndarray) and data.ndim == 3:
|
|
597
|
+
if withTumor:
|
|
598
|
+
self.reconPhantom = [data[i, :, :] for i in range(data.shape[0])]
|
|
599
|
+
else:
|
|
600
|
+
self.reconLaser = [data[i, :, :] for i in range(data.shape[0])]
|
|
601
|
+
else:
|
|
602
|
+
# Sinon, suppose que c'est déjà une liste de 2D
|
|
603
|
+
if withTumor:
|
|
604
|
+
self.reconPhantom = data
|
|
605
|
+
else:
|
|
606
|
+
self.reconLaser = data
|
|
607
|
+
# Essayer de charger les indices
|
|
608
|
+
base_dir, _ = os.path.split(recon_path)
|
|
609
|
+
indices_path = os.path.join(base_dir, 'indices.npy')
|
|
610
|
+
if os.path.exists(indices_path):
|
|
611
|
+
indices_data = np.load(indices_path, allow_pickle=True)
|
|
612
|
+
if isinstance(indices_data, np.ndarray) and indices_data.ndim == 3:
|
|
613
|
+
self.indices = [indices_data[i, :, :] for i in range(indices_data.shape[0])]
|
|
614
|
+
else:
|
|
615
|
+
self.indices = indices_data
|
|
616
|
+
else:
|
|
617
|
+
self.indices = None
|
|
618
|
+
|
|
619
|
+
if show_logs:
|
|
620
|
+
print(f"Loaded reconstruction results and indices from {recon_path}")
|
|
621
|
+
else:
|
|
622
|
+
# Mode chargement depuis le répertoire de résultats
|
|
623
|
+
if self.saveDir is None:
|
|
624
|
+
raise ValueError("Save directory is not specified. Please set saveDir before loading.")
|
|
625
|
+
# Use current optimizer and potential function if not provided
|
|
626
|
+
opt_name = optimizer.value if optimizer is not None else self.optimizer.value
|
|
627
|
+
# Build the base directory pattern
|
|
628
|
+
dir_pattern = f'results_*_{opt_name}'
|
|
629
|
+
# Add parameters to the pattern based on the optimizer
|
|
630
|
+
if optimizer is None:
|
|
631
|
+
optimizer = self.optimizer
|
|
632
|
+
if optimizer == OptimizerType.PPGMLEM:
|
|
633
|
+
beta_str = f'_Beta_{self.beta}'
|
|
634
|
+
delta_str = f'_Delta_{self.delta}'
|
|
635
|
+
gamma_str = f'_Gamma_{self.gamma}'
|
|
636
|
+
sigma_str = f'_Sigma_{self.sigma}'
|
|
637
|
+
dir_pattern += f'{beta_str}{delta_str}{gamma_str}{sigma_str}'
|
|
638
|
+
elif optimizer in (OptimizerType.PGC, OptimizerType.DEPIERRO95):
|
|
639
|
+
beta_str = f'_Beta_{self.beta}'
|
|
640
|
+
sigma_str = f'_Sigma_{self.sigma}'
|
|
641
|
+
dir_pattern += f'{beta_str}{sigma_str}'
|
|
642
|
+
# Find the most recent results directory if no date is specified
|
|
643
|
+
if results_date is None:
|
|
644
|
+
dirs = [d for d in os.listdir(self.saveDir) if os.path.isdir(os.path.join(self.saveDir, d)) and dir_pattern in d]
|
|
645
|
+
if not dirs:
|
|
646
|
+
raise FileNotFoundError(f"No matching results directory found for pattern '{dir_pattern}' in {self.saveDir}.")
|
|
647
|
+
dirs.sort(reverse=True) # Most recent first
|
|
648
|
+
results_dir = os.path.join(self.saveDir, dirs[0])
|
|
649
|
+
else:
|
|
650
|
+
results_dir = os.path.join(self.saveDir, f'results_{results_date}_{opt_name}')
|
|
651
|
+
if optimizer == OptimizerType.MLEM:
|
|
652
|
+
pass
|
|
653
|
+
elif optimizer == OptimizerType.LS:
|
|
654
|
+
results_dir += f'_Alpha_{self.alpha}'
|
|
655
|
+
if not os.path.exists(results_dir):
|
|
656
|
+
raise FileNotFoundError(f"Directory {results_dir} does not exist.")
|
|
657
|
+
# Load reconstruction results
|
|
658
|
+
recon_key = 'reconPhantom' if withTumor else 'reconLaser'
|
|
659
|
+
recon_path = os.path.join(results_dir, f'{recon_key}.npy')
|
|
660
|
+
if not os.path.exists(recon_path):
|
|
661
|
+
raise FileNotFoundError(f"No reconstruction file found at {recon_path}.")
|
|
662
|
+
data = np.load(recon_path, allow_pickle=True)
|
|
663
|
+
if isinstance(data, np.ndarray) and data.ndim == 3:
|
|
664
|
+
if withTumor:
|
|
665
|
+
self.reconPhantom = [data[i, :, :] for i in range(data.shape[0])]
|
|
666
|
+
else:
|
|
667
|
+
self.reconLaser = [data[i, :, :] for i in range(data.shape[0])]
|
|
668
|
+
else:
|
|
669
|
+
if withTumor:
|
|
670
|
+
self.reconPhantom = data
|
|
671
|
+
else:
|
|
672
|
+
self.reconLaser = data
|
|
673
|
+
# Load saved indices as list of 2D arrays
|
|
674
|
+
indices_path = os.path.join(results_dir, 'indices.npy')
|
|
675
|
+
if not os.path.exists(indices_path):
|
|
676
|
+
raise FileNotFoundError(f"No indices file found at {indices_path}.")
|
|
677
|
+
indices_data = np.load(indices_path, allow_pickle=True)
|
|
678
|
+
if isinstance(indices_data, np.ndarray) and indices_data.ndim == 3:
|
|
679
|
+
self.indices = [indices_data[i, :, :] for i in range(indices_data.shape[0])]
|
|
680
|
+
else:
|
|
681
|
+
self.indices = indices_data
|
|
682
|
+
if show_logs:
|
|
683
|
+
print(f"Loaded reconstruction results and indices from {results_dir}")
|
|
684
|
+
|
|
685
|
+
def normalizeSMatrix(self):
|
|
686
|
+
self.SMatrix = self.SMatrix / (float(self.experiment.params.acoustic['voltage'])*float(self.experiment.params.acoustic['sensitivity']))
|
|
687
|
+
|
|
688
|
+
# PRIVATE METHODS
|
|
689
|
+
|
|
690
|
+
def _sparseSMatrix_CSR(AcousticFields, threshold_factor=0.1, normalize=False):
|
|
691
|
+
"""
|
|
692
|
+
Construit une matrice sparse CSR par morceaux sans concaténation intermédiaire.
|
|
693
|
+
Libère toute la mémoire temporaire à chaque étape.
|
|
694
|
+
"""
|
|
695
|
+
device_index = config.select_best_gpu()
|
|
696
|
+
# Configuration GPU
|
|
697
|
+
cp.cuda.Device(device_index).use()
|
|
698
|
+
dtype = cp.float32
|
|
699
|
+
dtype_indices = cp.int32
|
|
700
|
+
|
|
701
|
+
# Mesure mémoire initiale
|
|
702
|
+
total_mem, free_mem = cp.cuda.Device(device_index).mem_info
|
|
703
|
+
initial_mem = total_mem - free_mem
|
|
704
|
+
print(f"VRAM initiale: {initial_mem / 1024**3:.3f} Go")
|
|
705
|
+
|
|
706
|
+
N = len(AcousticFields)
|
|
707
|
+
if N == 0:
|
|
708
|
+
raise ValueError("Aucun champ acoustique fourni.")
|
|
709
|
+
|
|
710
|
+
# Déterminer les dimensions
|
|
711
|
+
field = AcousticFields[0].field
|
|
712
|
+
if isinstance(field, np.ndarray):
|
|
713
|
+
field = cp.asarray(field, dtype=dtype)
|
|
714
|
+
T, Z, X = field.shape
|
|
715
|
+
TN, ZX = T * N, Z * X
|
|
716
|
+
|
|
717
|
+
# Création d'une matrice CSR vide
|
|
718
|
+
SMatrix = cpsparse.csr_matrix((TN, ZX), dtype=dtype)
|
|
719
|
+
|
|
720
|
+
try:
|
|
721
|
+
# Traitement field par field
|
|
722
|
+
for n in trange(N, desc="Sparsing fields", unit="field"):
|
|
723
|
+
field = AcousticFields[n].field
|
|
724
|
+
if isinstance(field, np.ndarray):
|
|
725
|
+
field = cp.asarray(field, dtype=dtype)
|
|
726
|
+
|
|
727
|
+
# Liste pour stocker les données du field courant
|
|
728
|
+
field_rows = []
|
|
729
|
+
field_cols = []
|
|
730
|
+
field_values = []
|
|
731
|
+
|
|
732
|
+
for t in range(T):
|
|
733
|
+
field_t = field[t]
|
|
734
|
+
threshold = threshold_factor * cp.max(cp.abs(field_t))
|
|
735
|
+
mask = cp.abs(field_t) > threshold
|
|
736
|
+
|
|
737
|
+
if not cp.any(mask):
|
|
738
|
+
continue
|
|
739
|
+
|
|
740
|
+
z_idx, x_idx = cp.where(mask)
|
|
741
|
+
rows = cp.full_like(z_idx, t * N + n, dtype=dtype_indices)
|
|
742
|
+
cols = z_idx * X + x_idx
|
|
743
|
+
vals = field_t[mask].astype(dtype)
|
|
744
|
+
|
|
745
|
+
if normalize:
|
|
746
|
+
max_val = cp.max(cp.abs(vals)) if vals.size > 0 else 1.0
|
|
747
|
+
vals = vals / max_val if max_val > 0 else vals
|
|
748
|
+
|
|
749
|
+
field_rows.append(rows)
|
|
750
|
+
field_cols.append(cols)
|
|
751
|
+
field_values.append(vals)
|
|
752
|
+
|
|
753
|
+
# Libération immédiate
|
|
754
|
+
del rows, cols, vals, z_idx, x_idx, mask, field_t
|
|
755
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
756
|
+
|
|
757
|
+
# Si des données pour ce field
|
|
758
|
+
if field_rows:
|
|
759
|
+
# Création d'une matrice COO temporaire pour ce field
|
|
760
|
+
temp_rows = cp.concatenate(field_rows)
|
|
761
|
+
temp_cols = cp.concatenate(field_cols)
|
|
762
|
+
temp_values = cp.concatenate(field_values)
|
|
763
|
+
|
|
764
|
+
# Création d'une matrice COO puis conversion en CSR
|
|
765
|
+
field_matrix = cpsparse.coo_matrix(
|
|
766
|
+
(temp_values, (temp_rows, temp_cols)),
|
|
767
|
+
shape=(TN, ZX),
|
|
768
|
+
dtype=dtype
|
|
769
|
+
).tocsr()
|
|
770
|
+
|
|
771
|
+
# Ajout à la matrice globale
|
|
772
|
+
SMatrix += field_matrix
|
|
773
|
+
|
|
774
|
+
# Libération mémoire
|
|
775
|
+
del field_rows, field_cols, field_values
|
|
776
|
+
del temp_rows, temp_cols, temp_values, field_matrix
|
|
777
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
778
|
+
gc.collect()
|
|
779
|
+
|
|
780
|
+
# Libération du field
|
|
781
|
+
del field
|
|
782
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
783
|
+
gc.collect()
|
|
784
|
+
|
|
785
|
+
# Optimisation finale de la matrice
|
|
786
|
+
SMatrix.sum_duplicates()
|
|
787
|
+
SMatrix.eliminate_zeros()
|
|
788
|
+
|
|
789
|
+
# Calcul des métriques
|
|
790
|
+
nnz = SMatrix.nnz
|
|
791
|
+
density = nnz / (TN * ZX)
|
|
792
|
+
size_bytes = nnz * (4 + 4) # 4 octets pour int32 + 4 pour float32
|
|
793
|
+
|
|
794
|
+
print(f"Dimensions: {TN} x {ZX}, NNZ={nnz:,} (density={density:.2%}) using {size_bytes / 1024**3:.2f} Go of VRAM")
|
|
795
|
+
|
|
796
|
+
# Dernière libération avant le return
|
|
797
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
798
|
+
gc.collect()
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
'DATA': SMatrix,
|
|
802
|
+
'density': density,
|
|
803
|
+
'size_bytes': size_bytes
|
|
804
|
+
}, Z
|
|
805
|
+
|
|
806
|
+
except Exception as e:
|
|
807
|
+
print(f"Erreur: {str(e)}")
|
|
808
|
+
del SMatrix
|
|
809
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
810
|
+
gc.collect()
|
|
811
|
+
raise
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _AlgebraicReconPython(self,withTumor, show_logs):
|
|
815
|
+
|
|
816
|
+
if withTumor:
|
|
817
|
+
if self.experiment.AOsignal_withTumor is None:
|
|
818
|
+
raise ValueError("AO signal with tumor is not available. Please generate AO signal with tumor the experiment first in the experiment object.")
|
|
819
|
+
else:
|
|
820
|
+
if self.experiment.AOsignal_withoutTumor is None:
|
|
821
|
+
raise ValueError("AO signal without tumor is not available. Please generate AO signal without tumor the experiment first in the experiment object.")
|
|
822
|
+
|
|
823
|
+
if self.useSparseSMatrix and self.SMatrix_sparse is None:
|
|
824
|
+
raise ValueError("Sparse SMatrix is not available. Please generate sparse SMatrix first.")
|
|
825
|
+
|
|
826
|
+
if self.optimizer.value == OptimizerType.MLEM.value:
|
|
827
|
+
if withTumor:
|
|
828
|
+
self.reconPhantom, self.indices = MLEM(SMatrix=self.SMatrix,
|
|
829
|
+
y=self.experiment.AOsignal_withTumor,
|
|
830
|
+
numIterations=self.numIterations,
|
|
831
|
+
isSavingEachIteration=self.isSavingEachIteration,
|
|
832
|
+
withTumor=withTumor,
|
|
833
|
+
device=self.device,
|
|
834
|
+
use_numba=self.isMultiCPU,
|
|
835
|
+
denominator_threshold=self.denominatorThreshold,
|
|
836
|
+
max_saves=self.maxSaves,
|
|
837
|
+
show_logs=show_logs,
|
|
838
|
+
useSparseSMatrix=self.useSparseSMatrix,
|
|
839
|
+
Z=self.Z_dim
|
|
840
|
+
)
|
|
841
|
+
else:
|
|
842
|
+
self.reconLaser, self.indices = MLEM(SMatrix=self.SMatrix,
|
|
843
|
+
y=self.experiment.AOsignal_withoutTumor,
|
|
844
|
+
numIterations=self.numIterations,
|
|
845
|
+
isSavingEachIteration=self.isSavingEachIteration,
|
|
846
|
+
withTumor=withTumor,
|
|
847
|
+
device=self.device,
|
|
848
|
+
use_numba=self.isMultiCPU,
|
|
849
|
+
denominator_threshold=self.denominatorThreshold,
|
|
850
|
+
max_saves=self.maxSaves,
|
|
851
|
+
show_logs=show_logs,
|
|
852
|
+
useSparseSMatrix=self.useSparseSMatrix,
|
|
853
|
+
Z=self.Z_dim
|
|
854
|
+
)
|
|
855
|
+
elif self.optimizer.value == OptimizerType.LS.value:
|
|
856
|
+
if self.alpha is None:
|
|
857
|
+
raise ValueError("Alpha (regularization parameter) must be set for LS reconstruction.")
|
|
858
|
+
if withTumor:
|
|
859
|
+
self.reconPhantom, self.indices = LS(SMatrix=self.SMatrix,
|
|
860
|
+
y=self.experiment.AOsignal_withTumor,
|
|
861
|
+
numIterations=self.numIterations,
|
|
862
|
+
isSavingEachIteration=self.isSavingEachIteration,
|
|
863
|
+
withTumor=withTumor,
|
|
864
|
+
alpha=self.alpha,
|
|
865
|
+
max_saves=self.maxSaves,
|
|
866
|
+
show_logs=show_logs
|
|
867
|
+
)
|
|
868
|
+
else:
|
|
869
|
+
self.reconLaser, self.indices = LS(SMatrix=self.SMatrix,
|
|
870
|
+
y=self.experiment.AOsignal_withoutTumor,
|
|
871
|
+
numIterations=self.numIterations,
|
|
872
|
+
isSavingEachIteration=self.isSavingEachIteration,
|
|
873
|
+
withTumor=withTumor,
|
|
874
|
+
alpha=self.alpha,
|
|
875
|
+
max_saves=self.maxSaves,
|
|
876
|
+
show_logs=show_logs
|
|
877
|
+
)
|
|
878
|
+
else:
|
|
879
|
+
raise ValueError(f"Only MLEM and LS are supported for simple algebraic reconstruction. {self.optimizer.value} need Bayesian reconstruction")
|
|
880
|
+
|
|
881
|
+
def _AlgebraicReconCASToR(self,withTumor, show_logs):
|
|
882
|
+
# Définir les chemins
|
|
883
|
+
smatrix = os.path.join(self.saveDir, "system_matrix")
|
|
884
|
+
if withTumor:
|
|
885
|
+
fileName = 'AOSignals_withTumor.cdh'
|
|
886
|
+
else:
|
|
887
|
+
fileName = 'AOSignals_withoutTumor.cdh'
|
|
888
|
+
|
|
889
|
+
# Vérifier et générer les fichiers d'entrée si nécessaire
|
|
890
|
+
if not os.path.isfile(os.path.join(self.saveDir, fileName)):
|
|
891
|
+
if show_logs:
|
|
892
|
+
print(f"Fichier .cdh manquant. Génération de {fileName}...")
|
|
893
|
+
self.experiment.saveAOsignals_Castor(self.saveDir)
|
|
894
|
+
|
|
895
|
+
# Vérifier/générer la matrice système
|
|
896
|
+
if not os.path.isdir(smatrix):
|
|
897
|
+
os.makedirs(smatrix, exist_ok=True)
|
|
898
|
+
if not os.listdir(smatrix):
|
|
899
|
+
if show_logs:
|
|
900
|
+
print("Matrice système manquante. Génération...")
|
|
901
|
+
self.experiment.saveAcousticFields(self.saveDir)
|
|
902
|
+
|
|
903
|
+
# Vérifier que le fichier .cdh existe (redondant mais sûr)
|
|
904
|
+
if not os.path.isfile(os.path.join(self.saveDir, fileName)):
|
|
905
|
+
raise FileNotFoundError(f"Le fichier .cdh n'existe toujours pas : {fileName}")
|
|
906
|
+
|
|
907
|
+
# Créer le dossier de sortie
|
|
908
|
+
os.makedirs(os.path.join(self.saveDir, 'results', 'recon'), exist_ok=True)
|
|
909
|
+
|
|
910
|
+
# Configuration de l'environnement pour CASToR
|
|
911
|
+
env = os.environ.copy()
|
|
912
|
+
env.update({
|
|
913
|
+
"CASTOR_DIR": self.experiment.params.reconstruction['castor_executable'],
|
|
914
|
+
"CASTOR_CONFIG": os.path.join(self.experiment.params.reconstruction['castor_executable'], "config"),
|
|
915
|
+
"CASTOR_64bits": "1",
|
|
916
|
+
"CASTOR_OMP": "1",
|
|
917
|
+
"CASTOR_SIMD": "1",
|
|
918
|
+
"CASTOR_ROOT": "1",
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
# Construire la commande
|
|
922
|
+
cmd = [
|
|
923
|
+
os.path.join(self.experiment.params.reconstruction['castor_executable'], "bin", "castor-recon"),
|
|
924
|
+
"-df", os.path.join(self.saveDir, fileName),
|
|
925
|
+
"-opti", self.optimizer.value,
|
|
926
|
+
"-it", f"{self.numIterations}:{self.numSubsets}",
|
|
927
|
+
"-proj", "matrix",
|
|
928
|
+
"-dout", os.path.join(self.saveDir, 'results', 'recon'),
|
|
929
|
+
"-th", str(os.cpu_count()),
|
|
930
|
+
"-vb", "5",
|
|
931
|
+
"-proj-comp", "1",
|
|
932
|
+
"-ignore-scanner",
|
|
933
|
+
"-data-type", "AOT",
|
|
934
|
+
"-ignore-corr", "cali,fdur",
|
|
935
|
+
"-system-matrix", smatrix,
|
|
936
|
+
]
|
|
937
|
+
|
|
938
|
+
# Afficher la commande (pour débogage)
|
|
939
|
+
if show_logs:
|
|
940
|
+
print("Commande CASToR :")
|
|
941
|
+
print(" ".join(cmd))
|
|
942
|
+
|
|
943
|
+
# Chemin du script temporaire
|
|
944
|
+
recon_script_path = os.path.join(gettempdir(), 'recon.sh')
|
|
945
|
+
|
|
946
|
+
# Écrire le script bash
|
|
947
|
+
with open(recon_script_path, 'w') as f:
|
|
948
|
+
f.write("#!/bin/bash\n")
|
|
949
|
+
f.write(f"export PATH={env['CASTOR_DIR']}/bin:$PATH\n") # Ajoute le chemin de CASToR au PATH
|
|
950
|
+
f.write(f"export LD_LIBRARY_PATH={env['CASTOR_DIR']}/lib:$LD_LIBRARY_PATH\n") # Ajoute les bibliothèques si nécessaire
|
|
951
|
+
f.write(" ".join(cmd) + "\n")
|
|
952
|
+
|
|
953
|
+
# Rendre le script exécutable et l'exécuter
|
|
954
|
+
subprocess.run(["chmod", "+x", recon_script_path], check=True)
|
|
955
|
+
if show_logs:
|
|
956
|
+
print(f"Exécution de la reconstruction avec CASToR...")
|
|
957
|
+
result = subprocess.run(recon_script_path, env=env, check=True, capture_output=True, text=True)
|
|
958
|
+
|
|
959
|
+
# Afficher la sortie de CASToR (pour débogage)
|
|
960
|
+
if show_logs:
|
|
961
|
+
print("Sortie CASToR :")
|
|
962
|
+
print(result.stdout)
|
|
963
|
+
if result.stderr:
|
|
964
|
+
print("Erreurs :")
|
|
965
|
+
print(result.stderr)
|
|
966
|
+
|
|
967
|
+
if show_logs:
|
|
968
|
+
print("Reconstruction terminée avec succès.")
|
|
969
|
+
self.load_reconCASToR(withTumor=withTumor)
|
|
970
|
+
|
|
971
|
+
# STATIC METHODS
|
|
972
|
+
@staticmethod
|
|
973
|
+
def plot_mse_comparison(recon_list, labels=None):
|
|
974
|
+
"""
|
|
975
|
+
Affiche les courbes de MSE pour chaque reconstruction dans recon_list.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
recon_list (list): Liste d'objets recon (doivent avoir les attributs 'indices' et 'MSE').
|
|
979
|
+
labels (list, optional): Liste des labels pour chaque courbe. Si None, utilise "Recon i".
|
|
980
|
+
"""
|
|
981
|
+
if labels is None:
|
|
982
|
+
labels = [f"Recon {i+1}" for i in range(len(recon_list))]
|
|
983
|
+
|
|
984
|
+
plt.figure(figsize=(4.5, 3.5))
|
|
985
|
+
colors = ['red', 'green', 'blue', 'orange', 'purple'] # Ajoute d'autres couleurs si nécessaire
|
|
986
|
+
|
|
987
|
+
for i, recon in enumerate(recon_list):
|
|
988
|
+
color = colors[i % len(colors)]
|
|
989
|
+
label = labels[i] if i < len(labels) else f"Recon {i+1}"
|
|
990
|
+
|
|
991
|
+
# Trouve l'index et la valeur minimale du MSE
|
|
992
|
+
best_idx = recon.indices[np.argmin(recon.MSE)]
|
|
993
|
+
min_mse = np.min(recon.MSE)
|
|
994
|
+
|
|
995
|
+
# Trace la courbe de MSE
|
|
996
|
+
plt.plot(recon.indices, recon.MSE, f'{color}-', label=label)
|
|
997
|
+
# Ligne horizontale pour le min MSE
|
|
998
|
+
plt.axhline(min_mse, color=color, linestyle='--', alpha=0.5)
|
|
999
|
+
# Ligne verticale pour l'itération du min MSE
|
|
1000
|
+
plt.axvline(best_idx, color=color, linestyle='--', alpha=0.5)
|
|
1001
|
+
|
|
1002
|
+
plt.xlabel("Iteration")
|
|
1003
|
+
plt.ylabel("MSE")
|
|
1004
|
+
plt.title("MSE vs. Iteration (Comparison)")
|
|
1005
|
+
plt.xscale('log')
|
|
1006
|
+
plt.yscale('log')
|
|
1007
|
+
plt.grid(True, which="both", ls="-")
|
|
1008
|
+
|
|
1009
|
+
# Légende personnalisée
|
|
1010
|
+
handles = []
|
|
1011
|
+
for i, recon in enumerate(recon_list):
|
|
1012
|
+
color = colors[i % len(colors)]
|
|
1013
|
+
best_idx = recon.indices[np.argmin(recon.MSE)]
|
|
1014
|
+
min_mse = np.min(recon.MSE)
|
|
1015
|
+
handles.append(
|
|
1016
|
+
plt.Line2D([0], [0], color=color,
|
|
1017
|
+
label=f"{labels[i] if labels and i < len(labels) else f'Recon {i+1}'} (min={min_mse:.4f} @ it.{best_idx+1})")
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
plt.legend(handles=handles, loc='upper right')
|
|
1021
|
+
plt.tight_layout()
|
|
1022
|
+
plt.show()
|
|
1023
|
+
|