HeshamAI commited on
Commit
5c75a5b
·
verified ·
1 Parent(s): 24a7f4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -23
app.py CHANGED
@@ -9,6 +9,7 @@ import time
9
  import traceback
10
  from functools import wraps
11
  import sys
 
12
 
13
  # Set up logging
14
  logging.basicConfig(
@@ -38,7 +39,7 @@ def debug_decorator(func):
38
  raise
39
  finally:
40
  end_time = time.time()
41
- logger.debug(f"Exiting {func.__name__}. Execution time: {end_time - start_time:.4f} seconds")
42
  return wrapper
43
 
44
  class DicomAnalyzer:
@@ -62,7 +63,7 @@ class DicomAnalyzer:
62
  self.max_pan_y = 0
63
 
64
  # Constants
65
- self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
66
  self.MIN_ZOOM = 1.0
67
  self.MAX_ZOOM = 20.0
68
  self.ZOOM_STEP = 1.2
@@ -72,7 +73,8 @@ class DicomAnalyzer:
72
  'last_click': None,
73
  'last_transformed_coords': None,
74
  'zoom_history': [],
75
- 'pan_history': []
 
76
  }
77
 
78
  @debug_decorator
@@ -86,8 +88,10 @@ class DicomAnalyzer:
86
  else:
87
  dicom_data = pydicom.dcmread(file)
88
 
 
89
  image = dicom_data.pixel_array.astype(float)
90
 
 
91
  rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
92
  rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
93
  image = (image * rescale_slope) + rescale_intercept
@@ -96,10 +100,20 @@ class DicomAnalyzer:
96
  self.original_image = image.copy()
97
  self.dicom_data = dicom_data
98
 
 
99
  self.display_image = self.normalize_image(image)
100
  self.original_display = self.display_image.copy()
101
 
 
102
  self.reset_view()
 
 
 
 
 
 
 
 
103
  return self.display_image, "DICOM file loaded successfully"
104
  except Exception as e:
105
  logger.error(f"Error loading DICOM: {str(e)}")
@@ -133,8 +147,7 @@ class DicomAnalyzer:
133
  self.zoom_factor = new_zoom
134
  logger.debug(f"Zooming in. New zoom factor: {self.zoom_factor}")
135
  self.debug_info['zoom_history'].append(('in', self.zoom_factor))
136
- return self.update_display()
137
- return image
138
  except Exception as e:
139
  logger.error(f"Error in zoom_in: {str(e)}")
140
  return image
@@ -147,8 +160,7 @@ class DicomAnalyzer:
147
  self.zoom_factor = new_zoom
148
  logger.debug(f"Zooming out. New zoom factor: {self.zoom_factor}")
149
  self.debug_info['zoom_history'].append(('out', self.zoom_factor))
150
- return self.update_display()
151
- return image
152
  except Exception as e:
153
  logger.error(f"Error in zoom_out: {str(e)}")
154
  return image
@@ -158,32 +170,42 @@ class DicomAnalyzer:
158
  if self.current_image is None:
159
  return None, "No image loaded"
160
 
 
161
  clicked_x, clicked_y = evt.index[0], evt.index[1]
162
  self.debug_info['last_click'] = (clicked_x, clicked_y)
163
 
 
164
  image_x = (clicked_x + self.pan_x) / self.zoom_factor
165
  image_y = (clicked_y + self.pan_y) / self.zoom_factor
166
 
167
  self.debug_info['last_transformed_coords'] = (image_x, image_y)
168
 
 
169
  height, width = self.current_image.shape[:2]
170
- mask = np.zeros((height, width), dtype=np.uint8)
171
- cv2.circle(mask, (int(image_x), int(image_y)),
172
- self.circle_diameter // 2, 1, -1)
173
 
 
174
  roi_pixels = self.current_image[mask == 1]
175
 
176
  if len(roi_pixels) == 0:
177
  return self.display_image, "Error: No pixels selected"
178
 
 
179
  pixel_spacing = getattr(self.dicom_data, 'PixelSpacing', [1.0, 1.0])[0]
180
 
 
 
181
  mean_value = np.mean(roi_pixels)
182
- std_dev = np.std(roi_pixels, ddof=1)
183
  min_val = np.min(roi_pixels)
184
  max_val = np.max(roi_pixels)
185
- area = len(roi_pixels) * (pixel_spacing ** 2)
 
 
186
 
 
187
  result = {
188
  'Area (mm²)': f"{area:.3f}",
189
  'Mean': f"{mean_value:.3f}",
@@ -193,17 +215,33 @@ class DicomAnalyzer:
193
  'Point': f"({image_x:.1f}, {image_y:.1f})"
194
  }
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  self.results.append(result)
197
  self.marks.append((image_x, image_y, self.circle_diameter))
198
 
199
- logger.debug(f"ROI Analysis Results:")
200
- logger.debug(f"Position: ({image_x:.1f}, {image_y:.1f})")
201
- logger.debug(f"Area: {area:.3f} mm²")
202
- logger.debug(f"Mean: {mean_value:.3f}")
203
- logger.debug(f"StdDev: {std_dev:.3f}")
204
- logger.debug(f"Min: {min_val:.3f}")
205
- logger.debug(f"Max: {max_val:.3f}")
206
-
207
  return self.update_display(), self.format_results()
208
  except Exception as e:
209
  logger.error(f"Error in ROI analysis: {str(e)}")
@@ -218,16 +256,18 @@ class DicomAnalyzer:
218
  height, width = self.original_display.shape[:2]
219
  display_image = self.original_display.copy()
220
 
 
221
  for x, y, diameter in self.marks:
222
  cv2.circle(
223
  display_image,
224
  (int(x), int(y)),
225
  diameter // 2,
226
- self.CIRCLE_COLOR,
227
  1,
228
  lineType=cv2.LINE_AA
229
  )
230
 
 
231
  if self.zoom_factor != 1.0:
232
  new_width = int(width * self.zoom_factor)
233
  new_height = int(height * self.zoom_factor)
@@ -237,12 +277,15 @@ class DicomAnalyzer:
237
  interpolation=cv2.INTER_LINEAR
238
  )
239
 
 
240
  self.max_pan_x = max(0, display_image.shape[1] - width)
241
  self.max_pan_y = max(0, display_image.shape[0] - height)
242
 
 
243
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
244
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
245
 
 
246
  visible = display_image[
247
  int(self.pan_y):int(self.pan_y + height),
248
  int(self.pan_x):int(self.pan_x + width)
@@ -290,6 +333,10 @@ class DicomAnalyzer:
290
  temp_file = "analysis_results.xlsx"
291
  df.to_excel(temp_file, index=False)
292
 
 
 
 
 
293
  return temp_file, "Results saved successfully"
294
  except Exception as e:
295
  return None, f"Error saving results: {str(e)}"
@@ -353,14 +400,16 @@ def create_interface():
353
  fn=analyzer.zoom_in,
354
  inputs=image_display,
355
  outputs=image_display,
356
- queue=False
 
357
  )
358
 
359
  zoom_out_btn.click(
360
  fn=analyzer.zoom_out,
361
  inputs=image_display,
362
  outputs=image_display,
363
- queue=False
 
364
  )
365
 
366
  reset_btn.click(
 
9
  import traceback
10
  from functools import wraps
11
  import sys
12
+ import os
13
 
14
  # Set up logging
15
  logging.basicConfig(
 
39
  raise
40
  finally:
41
  end_time = time.time()
42
+ logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
43
  return wrapper
44
 
45
  class DicomAnalyzer:
 
63
  self.max_pan_y = 0
64
 
65
  # Constants
66
+ self.CIRCLE_COLOR = (255, 255, 0) # BGR Yellow (corrected)
67
  self.MIN_ZOOM = 1.0
68
  self.MAX_ZOOM = 20.0
69
  self.ZOOM_STEP = 1.2
 
73
  'last_click': None,
74
  'last_transformed_coords': None,
75
  'zoom_history': [],
76
+ 'pan_history': [],
77
+ 'measurements': []
78
  }
79
 
80
  @debug_decorator
 
88
  else:
89
  dicom_data = pydicom.dcmread(file)
90
 
91
+ # Convert to float for accurate calculations
92
  image = dicom_data.pixel_array.astype(float)
93
 
94
+ # Apply DICOM rescale parameters
95
  rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
96
  rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
97
  image = (image * rescale_slope) + rescale_intercept
 
100
  self.original_image = image.copy()
101
  self.dicom_data = dicom_data
102
 
103
+ # Prepare display image
104
  self.display_image = self.normalize_image(image)
105
  self.original_display = self.display_image.copy()
106
 
107
+ # Reset view settings
108
  self.reset_view()
109
+
110
+ # Log DICOM info for comparison
111
+ self.logger.debug(f"DICOM Info:")
112
+ self.logger.debug(f"Image Size: {image.shape}")
113
+ self.logger.debug(f"Pixel Spacing: {getattr(dicom_data, 'PixelSpacing', [1.0, 1.0])}")
114
+ self.logger.debug(f"Rescale Slope: {rescale_slope}")
115
+ self.logger.debug(f"Rescale Intercept: {rescale_intercept}")
116
+
117
  return self.display_image, "DICOM file loaded successfully"
118
  except Exception as e:
119
  logger.error(f"Error loading DICOM: {str(e)}")
 
147
  self.zoom_factor = new_zoom
148
  logger.debug(f"Zooming in. New zoom factor: {self.zoom_factor}")
149
  self.debug_info['zoom_history'].append(('in', self.zoom_factor))
150
+ return self.update_display()
 
151
  except Exception as e:
152
  logger.error(f"Error in zoom_in: {str(e)}")
153
  return image
 
160
  self.zoom_factor = new_zoom
161
  logger.debug(f"Zooming out. New zoom factor: {self.zoom_factor}")
162
  self.debug_info['zoom_history'].append(('out', self.zoom_factor))
163
+ return self.update_display()
 
164
  except Exception as e:
165
  logger.error(f"Error in zoom_out: {str(e)}")
166
  return image
 
170
  if self.current_image is None:
171
  return None, "No image loaded"
172
 
173
+ # Get click coordinates and transform them
174
  clicked_x, clicked_y = evt.index[0], evt.index[1]
175
  self.debug_info['last_click'] = (clicked_x, clicked_y)
176
 
177
+ # Transform to image coordinates (ImageJ-compatible)
178
  image_x = (clicked_x + self.pan_x) / self.zoom_factor
179
  image_y = (clicked_y + self.pan_y) / self.zoom_factor
180
 
181
  self.debug_info['last_transformed_coords'] = (image_x, image_y)
182
 
183
+ # Create circular ROI mask (ImageJ-compatible method)
184
  height, width = self.current_image.shape[:2]
185
+ y, x = np.ogrid[:height, :width]
186
+ radius = self.circle_diameter / 2
187
+ mask = ((x - image_x)**2 + (y - image_y)**2 <= radius**2).astype(np.uint8)
188
 
189
+ # Get ROI pixels
190
  roi_pixels = self.current_image[mask == 1]
191
 
192
  if len(roi_pixels) == 0:
193
  return self.display_image, "Error: No pixels selected"
194
 
195
+ # Get pixel spacing (mm/pixel)
196
  pixel_spacing = getattr(self.dicom_data, 'PixelSpacing', [1.0, 1.0])[0]
197
 
198
+ # Calculate statistics (ImageJ-compatible)
199
+ n_pixels = len(roi_pixels)
200
  mean_value = np.mean(roi_pixels)
201
+ std_dev = np.std(roi_pixels, ddof=1) # ImageJ uses n-1
202
  min_val = np.min(roi_pixels)
203
  max_val = np.max(roi_pixels)
204
+
205
+ # Calculate area (mm²)
206
+ area = n_pixels * (pixel_spacing ** 2)
207
 
208
+ # Store results
209
  result = {
210
  'Area (mm²)': f"{area:.3f}",
211
  'Mean': f"{mean_value:.3f}",
 
215
  'Point': f"({image_x:.1f}, {image_y:.1f})"
216
  }
217
 
218
+ # Store measurement for ImageJ comparison
219
+ measurement = {
220
+ 'coordinates': (image_x, image_y),
221
+ 'diameter': self.circle_diameter,
222
+ 'pixel_count': n_pixels,
223
+ 'area': area,
224
+ 'mean': mean_value,
225
+ 'stddev': std_dev,
226
+ 'min': min_val,
227
+ 'max': max_val
228
+ }
229
+ self.debug_info['measurements'].append(measurement)
230
+
231
+ # Log for ImageJ comparison
232
+ logger.debug("\nImageJ Comparison Values:")
233
+ logger.debug(f"ROI Center: ({image_x:.1f}, {image_y:.1f})")
234
+ logger.debug(f"Diameter: {self.circle_diameter} pixels")
235
+ logger.debug(f"Pixel Count: {n_pixels}")
236
+ logger.debug(f"Area: {area:.6f} mm²")
237
+ logger.debug(f"Mean: {mean_value:.6f}")
238
+ logger.debug(f"StdDev: {std_dev:.6f}")
239
+ logger.debug(f"Min: {min_val:.6f}")
240
+ logger.debug(f"Max: {max_val:.6f}\n")
241
+
242
  self.results.append(result)
243
  self.marks.append((image_x, image_y, self.circle_diameter))
244
 
 
 
 
 
 
 
 
 
245
  return self.update_display(), self.format_results()
246
  except Exception as e:
247
  logger.error(f"Error in ROI analysis: {str(e)}")
 
256
  height, width = self.original_display.shape[:2]
257
  display_image = self.original_display.copy()
258
 
259
+ # Draw all marks with correct yellow color
260
  for x, y, diameter in self.marks:
261
  cv2.circle(
262
  display_image,
263
  (int(x), int(y)),
264
  diameter // 2,
265
+ self.CIRCLE_COLOR, # BGR Yellow (255, 255, 0)
266
  1,
267
  lineType=cv2.LINE_AA
268
  )
269
 
270
+ # Apply zoom
271
  if self.zoom_factor != 1.0:
272
  new_width = int(width * self.zoom_factor)
273
  new_height = int(height * self.zoom_factor)
 
277
  interpolation=cv2.INTER_LINEAR
278
  )
279
 
280
+ # Update pan limits
281
  self.max_pan_x = max(0, display_image.shape[1] - width)
282
  self.max_pan_y = max(0, display_image.shape[0] - height)
283
 
284
+ # Apply panning with bounds checking
285
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
286
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
287
 
288
+ # Extract visible portion
289
  visible = display_image[
290
  int(self.pan_y):int(self.pan_y + height),
291
  int(self.pan_x):int(self.pan_x + width)
 
333
  temp_file = "analysis_results.xlsx"
334
  df.to_excel(temp_file, index=False)
335
 
336
+ # Also save detailed results for ImageJ comparison
337
+ detailed_results = pd.DataFrame(self.debug_info['measurements'])
338
+ detailed_results.to_excel("detailed_results.xlsx", index=False)
339
+
340
  return temp_file, "Results saved successfully"
341
  except Exception as e:
342
  return None, f"Error saving results: {str(e)}"
 
400
  fn=analyzer.zoom_in,
401
  inputs=image_display,
402
  outputs=image_display,
403
+ queue=False,
404
+ api_name="zoom_in"
405
  )
406
 
407
  zoom_out_btn.click(
408
  fn=analyzer.zoom_out,
409
  inputs=image_display,
410
  outputs=image_display,
411
+ queue=False,
412
+ api_name="zoom_out"
413
  )
414
 
415
  reset_btn.click(