AOT-biomaps 2.9.138__py3-none-any.whl → 2.9.279__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 +35 -115
- AOT_biomaps/AOT_Acoustic/StructuredWave.py +2 -2
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +22 -18
- AOT_biomaps/AOT_Experiment/Tomography.py +74 -4
- AOT_biomaps/AOT_Experiment/_mainExperiment.py +102 -68
- AOT_biomaps/AOT_Optic/_mainOptic.py +124 -58
- AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +72 -108
- AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +474 -289
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +173 -68
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +360 -154
- AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +150 -111
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +10 -14
- AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +281 -0
- AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +328 -0
- AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +2 -0
- AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
- AOT_biomaps/AOT_Recon/AlgebraicRecon.py +359 -238
- AOT_biomaps/AOT_Recon/AnalyticRecon.py +29 -41
- AOT_biomaps/AOT_Recon/BayesianRecon.py +165 -91
- AOT_biomaps/AOT_Recon/DeepLearningRecon.py +4 -1
- AOT_biomaps/AOT_Recon/PrimalDualRecon.py +175 -31
- AOT_biomaps/AOT_Recon/ReconEnums.py +38 -3
- AOT_biomaps/AOT_Recon/ReconTools.py +184 -77
- AOT_biomaps/AOT_Recon/__init__.py +1 -0
- AOT_biomaps/AOT_Recon/_mainRecon.py +144 -74
- AOT_biomaps/__init__.py +4 -36
- {aot_biomaps-2.9.138.dist-info → aot_biomaps-2.9.279.dist-info}/METADATA +2 -1
- aot_biomaps-2.9.279.dist-info/RECORD +47 -0
- aot_biomaps-2.9.138.dist-info/RECORD +0 -43
- {aot_biomaps-2.9.138.dist-info → aot_biomaps-2.9.279.dist-info}/WHEEL +0 -0
- {aot_biomaps-2.9.138.dist-info → aot_biomaps-2.9.279.dist-info}/top_level.txt +0 -0
|
@@ -56,6 +56,7 @@ class Experiment(ABC):
|
|
|
56
56
|
pass
|
|
57
57
|
|
|
58
58
|
def cutAcousticFields(self, max_t, min_t=0):
|
|
59
|
+
|
|
59
60
|
max_t = float(max_t)
|
|
60
61
|
min_t = float(min_t)
|
|
61
62
|
|
|
@@ -111,11 +112,7 @@ class Experiment(ABC):
|
|
|
111
112
|
raise ValueError("noiseType must be either 'gaussian' or 'poisson'.")
|
|
112
113
|
noisy_signal = np.clip(noisy_signal, a_min=0, a_max=None) # Assurer que le signal reste non négatif
|
|
113
114
|
noiseSignals[:, i] = noisy_signal
|
|
114
|
-
|
|
115
|
-
if withTumor:
|
|
116
|
-
self.AOsignal_withTumor = noiseSignals
|
|
117
|
-
else:
|
|
118
|
-
self.AOsignal_withoutTumor = noiseSignals
|
|
115
|
+
return noiseSignals
|
|
119
116
|
|
|
120
117
|
def reduceDims(self, mode='avg'):
|
|
121
118
|
"""
|
|
@@ -157,6 +154,16 @@ class Experiment(ABC):
|
|
|
157
154
|
for param in ['dx', 'dy', 'dz']:
|
|
158
155
|
convert_and_update(self.params.general, param, lambda x: x * 2)
|
|
159
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
|
+
|
|
160
167
|
def saveAcousticFields(self, save_directory):
|
|
161
168
|
progress_bar = trange(len(self.AcousticFields), desc="Saving Acoustic Fields")
|
|
162
169
|
for i in progress_bar:
|
|
@@ -233,67 +240,97 @@ class Experiment(ABC):
|
|
|
233
240
|
return ani
|
|
234
241
|
|
|
235
242
|
def generateAOsignal(self, withTumor=True, AOsignalDataPath=None):
|
|
236
|
-
if self.AcousticFields is None:
|
|
237
|
-
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
238
|
-
|
|
239
|
-
if self.OpticImage is None:
|
|
240
|
-
raise ValueError("OpticImage is not initialized. Please generate the phantom first.")
|
|
241
243
|
|
|
242
244
|
if AOsignalDataPath is not None:
|
|
243
245
|
if not os.path.exists(AOsignalDataPath):
|
|
244
246
|
raise FileNotFoundError(f"AO file {AOsignalDataPath} not found.")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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)
|
|
248
252
|
else:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
263
269
|
|
|
264
|
-
|
|
265
|
-
for t in range(self.AcousticFields[i].field.shape[0]):
|
|
266
|
-
if withTumor:
|
|
267
|
-
interaction = self.OpticImage.phantom * self.AcousticFields[i].field[t, :, :]
|
|
268
|
-
else:
|
|
269
|
-
interaction = self.OpticImage.laser.intensity * self.AcousticFields[i].field[t, :, :]
|
|
270
|
-
AOsignal[t, i] = np.sum(interaction)
|
|
270
|
+
AOsignal = np.zeros((shape_field[0], len(self.AcousticFields)), dtype=np.float32)
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
276
289
|
|
|
277
290
|
@staticmethod
|
|
278
|
-
def _loadAOSignal(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
for
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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.")
|
|
297
334
|
|
|
298
335
|
def saveAOsignals_Castor(self, save_directory, withTumor=True):
|
|
299
336
|
if withTumor:
|
|
@@ -319,12 +356,12 @@ class Experiment(ABC):
|
|
|
319
356
|
fileID.write(AO_signal[:, j].astype(np.float32).tobytes())
|
|
320
357
|
|
|
321
358
|
header_content = (
|
|
322
|
-
f"Data filename:
|
|
359
|
+
f"Data filename: {'AOSignals_withTumor.cdf' if withTumor else 'AOSignals_withoutTumor.cdf'}\n"
|
|
323
360
|
f"Number of events: {nScan}\n"
|
|
324
|
-
f"Number of acquisitions per event: {AO_signal.shape[
|
|
361
|
+
f"Number of acquisitions per event: {AO_signal.shape[0]}\n"
|
|
325
362
|
f"Start time (s): 0\n"
|
|
326
363
|
f"Duration (s): 1\n"
|
|
327
|
-
f"Acquisition frequency (Hz): {
|
|
364
|
+
f"Acquisition frequency (Hz): {self.params.acoustic['f_saving']}\n"
|
|
328
365
|
f"Data mode: histogram\n"
|
|
329
366
|
f"Data type: AOT\n"
|
|
330
367
|
f"Number of US transducers: {self.params.acoustic['num_elements']}"
|
|
@@ -475,18 +512,15 @@ class Experiment(ABC):
|
|
|
475
512
|
plt.close(fig)
|
|
476
513
|
return ani
|
|
477
514
|
|
|
478
|
-
def showPhantom(self):
|
|
515
|
+
def showPhantom(self, withROI=False):
|
|
479
516
|
"""
|
|
480
517
|
Displays the optical phantom with absorbers.
|
|
481
518
|
"""
|
|
482
519
|
try:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
plt.tick_params(axis='both', which='major', labelsize=20)
|
|
488
|
-
plt.title('Optical Phantom with Absorbers')
|
|
489
|
-
plt.show()
|
|
520
|
+
if withROI:
|
|
521
|
+
self.OpticImage.show_ROI()
|
|
522
|
+
else:
|
|
523
|
+
self.OpticImage.show_phantom()
|
|
490
524
|
except Exception as e:
|
|
491
525
|
raise RuntimeError(f"Error plotting phantom: {e}")
|
|
492
526
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
from .Laser import Laser
|
|
2
2
|
from .Absorber import Absorber
|
|
3
|
-
|
|
4
3
|
import numpy as np
|
|
5
4
|
import matplotlib.pyplot as plt
|
|
6
5
|
import matplotlib.patches as patches
|
|
6
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
7
7
|
|
|
8
8
|
class Phantom:
|
|
9
9
|
"""
|
|
10
10
|
Class to apply absorbers to a laser field in the XZ plane.
|
|
11
11
|
"""
|
|
12
|
+
|
|
12
13
|
def __init__(self, params):
|
|
13
14
|
"""
|
|
14
15
|
Initializes the phantom with the given parameters.
|
|
@@ -20,27 +21,13 @@ class Phantom:
|
|
|
20
21
|
self.laser = Laser(params)
|
|
21
22
|
self.phantom = self._apply_absorbers()
|
|
22
23
|
self.phantom = np.transpose(self.phantom)
|
|
24
|
+
self.laser.intensity = np.transpose(self.laser.intensity)
|
|
25
|
+
self.maskList = None # List to store ROI masks
|
|
23
26
|
except KeyError as e:
|
|
24
27
|
raise ValueError(f"Missing parameter: {e}")
|
|
25
28
|
except Exception as e:
|
|
26
29
|
raise RuntimeError(f"Error initializing Phantom: {e}")
|
|
27
30
|
|
|
28
|
-
def _apply_absorbers(self):
|
|
29
|
-
"""
|
|
30
|
-
Applies the absorbers to the laser field.
|
|
31
|
-
:return: Intensity matrix of the phantom with applied absorbers.
|
|
32
|
-
"""
|
|
33
|
-
try:
|
|
34
|
-
X, Z = np.meshgrid(self.laser.x, self.laser.z, indexing='ij')
|
|
35
|
-
intensity = np.copy(self.laser.intensity)
|
|
36
|
-
for absorber in self.absorbers:
|
|
37
|
-
r2 = (X - absorber.center[0] * 1000)**2 + (Z - absorber.center[1] * 1000)**2
|
|
38
|
-
absorption = -absorber.amplitude * np.exp(-r2 / (absorber.radius * 1000)**2)
|
|
39
|
-
intensity += absorption
|
|
40
|
-
return np.clip(intensity, 0, None)
|
|
41
|
-
except Exception as e:
|
|
42
|
-
raise RuntimeError(f"Error applying absorbers: {e}")
|
|
43
|
-
|
|
44
31
|
def __str__(self):
|
|
45
32
|
"""
|
|
46
33
|
Returns a string representation of the Phantom object,
|
|
@@ -55,6 +42,7 @@ class Phantom:
|
|
|
55
42
|
'w0': self.laser.w0,
|
|
56
43
|
}
|
|
57
44
|
laser_attr_lines = [f" {k}: {v}" for k, v in laser_attrs.items()]
|
|
45
|
+
|
|
58
46
|
# Absorber attributes
|
|
59
47
|
absorber_lines = []
|
|
60
48
|
for absorber in self.absorbers:
|
|
@@ -63,27 +51,79 @@ class Phantom:
|
|
|
63
51
|
absorber_lines.append(f" center: {absorber.center}")
|
|
64
52
|
absorber_lines.append(f" radius: {absorber.radius}")
|
|
65
53
|
absorber_lines.append(f" amplitude: {absorber.amplitude}")
|
|
54
|
+
|
|
66
55
|
# Define borders and titles
|
|
67
56
|
border = "+" + "-" * 40 + "+"
|
|
68
|
-
title = f"| Type
|
|
57
|
+
title = f"| Type: {self.__class__.__name__} |"
|
|
69
58
|
laser_title = "| Laser Parameters |"
|
|
70
59
|
absorber_title = "| Absorbers |"
|
|
60
|
+
|
|
71
61
|
# Assemble the final result
|
|
72
62
|
result = f"{border}\n{title}\n{border}\n{laser_title}\n{border}\n"
|
|
73
63
|
result += "\n".join(laser_attr_lines)
|
|
74
64
|
result += f"\n{border}\n{absorber_title}\n{border}\n"
|
|
75
65
|
result += "\n".join(absorber_lines)
|
|
76
66
|
result += f"\n{border}"
|
|
67
|
+
|
|
77
68
|
return result
|
|
78
69
|
except Exception as e:
|
|
79
70
|
raise RuntimeError(f"Error generating string representation: {e}")
|
|
71
|
+
|
|
72
|
+
def find_ROI(self):
|
|
73
|
+
"""
|
|
74
|
+
Computes binary masks for each ROI and stores them in self.maskList.
|
|
75
|
+
:return: True if pixels are detected in any ROI, False otherwise.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
X_mm, Z_mm = np.meshgrid(self.laser.x, self.laser.z, indexing='xy')
|
|
79
|
+
assert self.phantom.shape == X_mm.shape, (
|
|
80
|
+
f"Shape mismatch: phantom={self.phantom.shape}, grid={X_mm.shape}"
|
|
81
|
+
)
|
|
82
|
+
self.maskList = [] # Reset the list
|
|
83
|
+
roi_found = False
|
|
84
|
+
|
|
85
|
+
for absorber in self.absorbers:
|
|
86
|
+
center_x_mm = absorber.center[0] * 1000 # Convert to mm
|
|
87
|
+
center_z_mm = absorber.center[1] * 1000 # Convert to mm
|
|
88
|
+
radius_mm = absorber.radius * 1000 # Convert to mm
|
|
89
|
+
|
|
90
|
+
# Create mask for this ROI
|
|
91
|
+
mask_i = (X_mm - center_x_mm)**2 + (Z_mm - center_z_mm)**2 <= radius_mm**2
|
|
92
|
+
self.maskList.append(mask_i)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise RuntimeError(f"Error in find_ROI: {e}")
|
|
96
|
+
|
|
97
|
+
def _apply_absorbers(self):
|
|
98
|
+
"""
|
|
99
|
+
Applies the absorbers to the laser field.
|
|
100
|
+
:return: Intensity matrix of the phantom with applied absorbers.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
X, Z = np.meshgrid(self.laser.x, self.laser.z, indexing='ij')
|
|
104
|
+
intensity = np.copy(self.laser.intensity)
|
|
105
|
+
|
|
106
|
+
for absorber in self.absorbers:
|
|
107
|
+
r2 = (X - absorber.center[0] * 1000)**2 + (Z - absorber.center[1] * 1000)**2
|
|
108
|
+
absorption = -absorber.amplitude * np.exp(-r2 / (absorber.radius * 1000)**2)
|
|
109
|
+
intensity += absorption
|
|
110
|
+
|
|
111
|
+
return np.clip(intensity, 0, None)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise RuntimeError(f"Error applying absorbers: {e}")
|
|
80
114
|
|
|
81
115
|
def show_phantom(self):
|
|
82
116
|
"""
|
|
83
117
|
Displays the optical phantom with absorbers.
|
|
84
118
|
"""
|
|
85
119
|
try:
|
|
86
|
-
plt.
|
|
120
|
+
plt.figure(figsize=(6, 6))
|
|
121
|
+
plt.imshow(
|
|
122
|
+
self.phantom,
|
|
123
|
+
extent=(self.laser.x[0], self.laser.x[-1] + 1, self.laser.z[-1], self.laser.z[0]),
|
|
124
|
+
aspect='equal',
|
|
125
|
+
cmap='hot'
|
|
126
|
+
)
|
|
87
127
|
plt.colorbar(label='Intensity')
|
|
88
128
|
plt.xlabel('X (mm)', fontsize=20)
|
|
89
129
|
plt.ylabel('Z (mm)', fontsize=20)
|
|
@@ -95,44 +135,70 @@ class Phantom:
|
|
|
95
135
|
|
|
96
136
|
def show_ROI(self):
|
|
97
137
|
"""
|
|
98
|
-
Displays the optical image with
|
|
99
|
-
|
|
100
|
-
It also calculates and prints the average intensity within these regions.
|
|
138
|
+
Displays the optical image with ROIs and average intensities.
|
|
139
|
+
Calls find_ROI() if self.maskList is empty.
|
|
101
140
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
141
|
+
try:
|
|
142
|
+
if not self.maskList:
|
|
143
|
+
self.find_ROI()
|
|
144
|
+
|
|
145
|
+
fig, ax = plt.subplots(figsize=(6, 6))
|
|
146
|
+
im = ax.imshow(
|
|
147
|
+
self.phantom,
|
|
148
|
+
extent=(
|
|
149
|
+
np.min(self.laser.x), np.max(self.laser.x),
|
|
150
|
+
np.max(self.laser.z), np.min(self.laser.z)
|
|
151
|
+
),
|
|
152
|
+
aspect='equal',
|
|
153
|
+
cmap='hot'
|
|
115
154
|
)
|
|
116
|
-
ax
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
divider = make_axes_locatable(ax)
|
|
156
|
+
cax = divider.append_axes("right", size="5%", pad=0.05)
|
|
157
|
+
plt.colorbar(im, cax=cax, label='Intensity')
|
|
158
|
+
|
|
159
|
+
# Draw ROIs
|
|
160
|
+
for i, absorber in enumerate(self.absorbers):
|
|
161
|
+
center_x_mm = absorber.center[0] * 1000 # Convert to mm
|
|
162
|
+
center_z_mm = absorber.center[1] * 1000 # Convert to mm
|
|
163
|
+
radius_mm = absorber.radius * 1000 # Convert to mm
|
|
164
|
+
|
|
165
|
+
circle = patches.Circle(
|
|
166
|
+
(center_x_mm, center_z_mm),
|
|
167
|
+
radius_mm,
|
|
168
|
+
edgecolor='limegreen',
|
|
169
|
+
facecolor='none',
|
|
170
|
+
linewidth=2
|
|
171
|
+
)
|
|
172
|
+
ax.add_patch(circle)
|
|
173
|
+
ax.text(
|
|
174
|
+
center_x_mm,
|
|
175
|
+
center_z_mm - 2,
|
|
176
|
+
str(i + 1),
|
|
177
|
+
color='limegreen',
|
|
178
|
+
ha='center',
|
|
179
|
+
va='center',
|
|
180
|
+
fontsize=12,
|
|
181
|
+
fontweight='bold'
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Global mask (union of all ROIs)
|
|
185
|
+
ROI_mask = np.zeros_like(self.phantom, dtype=bool)
|
|
186
|
+
for mask in self.maskList:
|
|
187
|
+
ROI_mask |= mask
|
|
188
|
+
|
|
189
|
+
roi_values = self.phantom[ROI_mask]
|
|
190
|
+
if roi_values.size == 0:
|
|
191
|
+
print("❌ NO PIXELS IN ROIs! Check positions:")
|
|
192
|
+
for i, abs in enumerate(self.absorbers):
|
|
193
|
+
print(f" Absorber {i}: center=({abs.center[0]*1000:.3f}, {abs.center[1]*1000:.3f}) mm")
|
|
194
|
+
print(f" radius={abs.radius*1000:.3f} mm")
|
|
195
|
+
else:
|
|
196
|
+
print(f"✅ Average intensity in ROIs: {np.mean(roi_values):.4f}")
|
|
197
|
+
|
|
198
|
+
ax.set_xlabel('x (mm)')
|
|
199
|
+
ax.set_ylabel('z (mm)')
|
|
200
|
+
ax.set_title('Phantom with ROIs')
|
|
201
|
+
plt.tight_layout()
|
|
202
|
+
plt.show()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise RuntimeError(f"Error in show_ROI: {e}")
|