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.

Files changed (31) hide show
  1. AOT_biomaps/AOT_Acoustic/AcousticTools.py +35 -115
  2. AOT_biomaps/AOT_Acoustic/StructuredWave.py +2 -2
  3. AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +22 -18
  4. AOT_biomaps/AOT_Experiment/Tomography.py +74 -4
  5. AOT_biomaps/AOT_Experiment/_mainExperiment.py +102 -68
  6. AOT_biomaps/AOT_Optic/_mainOptic.py +124 -58
  7. AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +72 -108
  8. AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +474 -289
  9. AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +173 -68
  10. AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +360 -154
  11. AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +150 -111
  12. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +10 -14
  13. AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +281 -0
  14. AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +328 -0
  15. AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +2 -0
  16. AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
  17. AOT_biomaps/AOT_Recon/AlgebraicRecon.py +359 -238
  18. AOT_biomaps/AOT_Recon/AnalyticRecon.py +29 -41
  19. AOT_biomaps/AOT_Recon/BayesianRecon.py +165 -91
  20. AOT_biomaps/AOT_Recon/DeepLearningRecon.py +4 -1
  21. AOT_biomaps/AOT_Recon/PrimalDualRecon.py +175 -31
  22. AOT_biomaps/AOT_Recon/ReconEnums.py +38 -3
  23. AOT_biomaps/AOT_Recon/ReconTools.py +184 -77
  24. AOT_biomaps/AOT_Recon/__init__.py +1 -0
  25. AOT_biomaps/AOT_Recon/_mainRecon.py +144 -74
  26. AOT_biomaps/__init__.py +4 -36
  27. {aot_biomaps-2.9.138.dist-info → aot_biomaps-2.9.279.dist-info}/METADATA +2 -1
  28. aot_biomaps-2.9.279.dist-info/RECORD +47 -0
  29. aot_biomaps-2.9.138.dist-info/RECORD +0 -43
  30. {aot_biomaps-2.9.138.dist-info → aot_biomaps-2.9.279.dist-info}/WHEEL +0 -0
  31. {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
- AOmatrix = self._load_AOSignal(AOsignalDataPath)
246
- if AOmatrix.shape[0] != self.AcousticFields[0].field.shape[0]:
247
- print(f"AO signal shape {AOmatrix.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Generating corrected AO signal to match...")
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
- return AOmatrix
250
-
251
- if not all(field.field.shape == self.AcousticFields[0].field.shape for field in self.AcousticFields):
252
- minShape = min([field.field.shape[0] for field in self.AcousticFields])
253
- self.cutAcousticFields(minShape * self.params['fs_aq'])
254
- else:
255
- shape_field = self.AcousticFields[0].field.shape
256
-
257
- AOsignal = np.zeros((shape_field[0], len(self.AcousticFields)), dtype=np.float32)
258
-
259
- if withTumor:
260
- description = "Generating AO Signal with Tumor"
261
- else:
262
- description = "Generating AO Signal without Tumor"
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
- for i in trange(len(self.AcousticFields), desc=description):
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
- if withTumor:
273
- self.AOsignal_withTumor = AOsignal
274
- else:
275
- self.AOsignal_withoutTumor = AOsignal
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(cdh_file):
279
- with open(cdh_file, "r") as file:
280
- cdh_content = file.readlines()
281
-
282
- n_events = int([line.split(":")[1].strip() for line in cdh_content if "Number of events" in line][0])
283
- n_acquisitions = int([line.split(":")[1].strip() for line in cdh_content if "Number of acquisitions per event" in line][0])
284
-
285
- AOsignal_matrix = np.zeros((n_events, n_acquisitions), dtype=np.float32)
286
-
287
- with open(cdh_file.replace(".cdh", ".cdf"), "rb") as file:
288
- for event in range(n_events):
289
- num_elements = int([line.split(":")[1].strip() for line in cdh_content if "Number of US transducers" in line][0])
290
- hex_length = (num_elements + 3) // 4
291
- file.read(hex_length // 2)
292
-
293
- signal = np.frombuffer(file.read(n_acquisitions * 4), dtype=np.float32)
294
- AOsignal_matrix[event, :] = signal
295
-
296
- return AOsignal_matrix
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: AOSignals.cdf\n"
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[1]}\n"
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): {1/self.AcousticFields[0].kgrid.dt}\n"
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
- plt.imshow(self.OpticImage.phantom, extent=(self.OpticImage.laser.x[0], self.OpticImage.laser.x[-1] + 1, self.OpticImage.laser.z[-1], self.OpticImage.laser.z[0]), aspect='equal', cmap='hot')
484
- plt.colorbar(label='Intensity')
485
- plt.xlabel('X (mm)', fontsize=20)
486
- plt.ylabel('Z (mm)', fontsize=20)
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 : {self.__class__.__name__} |"
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.imshow(self.phantom, extent=(self.laser.x[0], self.laser.x[-1] + 1, self.laser.z[-1], self.laser.z[0]), aspect='equal', cmap='hot')
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 regions of interest (ROIs) highlighted.
99
- This method overlays dashed green circles on the optical image to indicate the regions of interest defined by the absorbers.
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
- X, Z = np.meshgrid(self.laser.x, self.laser.z, indexing='ij')
103
- # --- Plot the optical image and overlay the regions of interest ---
104
- _, ax = plt.subplots(figsize=(5, 5))
105
- # Plot the optical intensity image
106
- _ = ax.imshow(self.phantom,
107
- extent=(self.laser.x[0]*1000, self.laser.x[-1]*1000, self.laser.z[-1]*1000, self.laser.z[0]*1000),
108
- aspect='equal', cmap='hot')
109
- # Overlay dashed green circles for each region of interest
110
- for region in self.absorbers:
111
- circle = patches.Circle(
112
- (region.center[0] * 1000, region.center[1] * 1000), # Convert to mm
113
- region.radius * 1000,
114
- edgecolor='limegreen', facecolor='none', linewidth=0.8, linestyle='--', alpha=0.8
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.add_patch(circle)
117
- ax.set_title("Optical image with regions of interest")
118
- ax.set_xlabel("x (mm)")
119
- ax.set_ylabel("z (mm)")
120
- ax.tick_params(axis='both', which='major')
121
- plt.show()
122
- # --- Create a mask for the regions of interest and calculate average intensity ---
123
- # Initialize an empty mask for the regions
124
- ROI_mask = np.zeros_like(X, dtype=bool)
125
- # Iterate over the regions to create and combine masks
126
- for region in self.absorbers:
127
- cx, cz = region.center
128
- r = region.radius
129
- # Calculate squared distance from the center
130
- dist_sq = (X - cx)**2 + (Z - cz)**2
131
- # Create mask for points within the current region
132
- current_mask = dist_sq <= r**2
133
- # Combine with the overall region mask
134
- ROI_mask = np.logical_or(ROI_mask, current_mask)
135
- # Extract intensity values in the regions of interest and compute the average
136
- region_intensity_values = self.phantom[ROI_mask]
137
- average_intensity = np.mean(region_intensity_values)
138
- print("Average intensity in regions of interest:", average_intensity)
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}")