HeshamAI commited on
Commit
4030d56
·
verified ·
1 Parent(s): 069672f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -132
app.py CHANGED
@@ -3,42 +3,10 @@ import cv2
3
  import numpy as np
4
  import pandas as pd
5
  import pydicom
6
- import logging
7
- import time
8
- import traceback
9
- from functools import wraps
10
- import sys
11
  from PIL import Image
12
 
13
- # Set up logging
14
- logging.basicConfig(
15
- level=logging.DEBUG,
16
- format='%(asctime)s - %(levelname)s - %(message)s',
17
- handlers=[
18
- logging.FileHandler('dicom_analyzer_debug.log'),
19
- logging.StreamHandler(sys.stdout)
20
- ]
21
- )
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
- def debug_decorator(func):
26
- @wraps(func)
27
- def wrapper(*args, **kwargs):
28
- logger.debug(f"Entering {func.__name__}")
29
- start_time = time.time()
30
- try:
31
- result = func(*args, **kwargs)
32
- logger.debug(f"Function {func.__name__} completed successfully")
33
- return result
34
- except Exception as e:
35
- logger.error(f"Error in {func.__name__}: {str(e)}")
36
- logger.error(traceback.format_exc())
37
- raise
38
- finally:
39
- end_time = time.time()
40
- logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
41
- return wrapper
42
 
43
  class DicomAnalyzer:
44
  def __init__(self):
@@ -48,23 +16,18 @@ class DicomAnalyzer:
48
  self.current_image = None
49
  self.dicom_data = None
50
  self.display_image = None
51
- self.marks = []
52
  self.original_image = None
53
  self.original_display = None
 
54
  self.pan_x = 0
55
  self.pan_y = 0
56
  self.max_pan_x = 0
57
  self.max_pan_y = 0
58
-
59
- # Constants
60
- self.CIRCLE_COLOR = (255, 255, 0) # BGR Yellow
61
- self.MIN_ZOOM = 1.0
62
- self.MAX_ZOOM = 20.0
63
- self.ZOOM_STEP = 0.5
64
-
65
- logger.info("DicomAnalyzer initialized")
66
 
67
- @debug_decorator
68
  def load_dicom(self, file):
69
  try:
70
  if file is None:
@@ -88,13 +51,15 @@ class DicomAnalyzer:
88
  self.display_image = self.normalize_image(image)
89
  self.original_display = self.display_image.copy()
90
 
 
91
  self.reset_view()
 
 
92
  return self.display_image, "DICOM file loaded successfully"
93
  except Exception as e:
94
- logger.error(f"Error loading DICOM: {str(e)}")
95
- return None, f"Error loading DICOM: {str(e)}"
96
 
97
- @debug_decorator
98
  def normalize_image(self, image):
99
  try:
100
  normalized = cv2.normalize(
@@ -109,10 +74,9 @@ class DicomAnalyzer:
109
  normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
110
  return normalized
111
  except Exception as e:
112
- logger.error(f"Error normalizing image: {str(e)}")
113
  return None
114
 
115
- @debug_decorator
116
  def reset_view(self):
117
  self.zoom_factor = 1.0
118
  self.pan_x = 0
@@ -121,35 +85,24 @@ class DicomAnalyzer:
121
  return self.update_display()
122
  return None
123
 
124
- @debug_decorator
125
  def zoom_in(self, image):
126
- try:
127
- new_zoom = self.zoom_factor + self.ZOOM_STEP
128
- if new_zoom <= self.MAX_ZOOM:
129
- self.zoom_factor = new_zoom
130
- logger.debug(f"Zooming in. New zoom factor: {self.zoom_factor}")
131
- return self.update_display()
132
- except Exception as e:
133
- logger.error(f"Error in zoom_in: {str(e)}")
134
- return image
135
 
136
- @debug_decorator
137
  def zoom_out(self, image):
138
- try:
139
- new_zoom = self.zoom_factor - self.ZOOM_STEP
140
- if new_zoom >= self.MIN_ZOOM:
141
- self.zoom_factor = new_zoom
142
- logger.debug(f"Zooming out. New zoom factor: {self.zoom_factor}")
143
- return self.update_display()
144
- except Exception as e:
145
- logger.error(f"Error in zoom_out: {str(e)}")
146
- return image
147
 
148
- @debug_decorator
149
  def handle_keyboard(self, key):
150
  try:
 
151
  pan_amount = int(5 * self.zoom_factor)
152
 
 
 
 
153
  if key == 'ArrowLeft':
154
  self.pan_x = max(0, self.pan_x - pan_amount)
155
  elif key == 'ArrowRight':
@@ -159,69 +112,79 @@ class DicomAnalyzer:
159
  elif key == 'ArrowDown':
160
  self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
161
 
 
 
 
 
 
162
  return self.update_display()
163
  except Exception as e:
164
- logger.error(f"Error handling keyboard: {str(e)}")
165
  return self.display_image
166
 
167
- @debug_decorator
168
  def analyze_roi(self, evt: gr.SelectData):
169
  try:
170
  if self.current_image is None:
171
  return None, "No image loaded"
172
 
 
173
  clicked_x, clicked_y = evt.index[0], evt.index[1]
174
 
175
- # Transform coordinates
176
  x = (clicked_x + self.pan_x) / self.zoom_factor
177
  y = (clicked_y + self.pan_y) / self.zoom_factor
178
-
 
 
 
 
179
  # Get image dimensions
180
  height, width = self.current_image.shape[:2]
181
 
182
- # Ensure coordinates are within bounds
183
- x = max(0, min(x, width-1))
184
- y = max(0, min(y, height-1))
185
-
186
- # Create circular mask
187
- mask = np.zeros_like(self.current_image, dtype=np.uint8)
188
- y_indices, x_indices = np.ogrid[:height, :width]
189
- radius = self.circle_diameter / 2
190
- distance_from_center = np.sqrt(
191
- (x_indices - x)**2 + (y_indices - y)**2
192
- )
193
- mask[distance_from_center <= radius] = 1
 
194
 
195
- # Calculate statistics
196
- roi_pixels = self.current_image[mask == 1]
197
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
198
 
199
- area_pixels = np.sum(mask)
200
- area_mm2 = area_pixels * (pixel_spacing ** 2)
201
-
202
- mean = np.mean(roi_pixels)
203
- stddev = np.std(roi_pixels)
204
  min_val = np.min(roi_pixels)
205
  max_val = np.max(roi_pixels)
 
 
 
206
 
 
207
  result = {
208
- 'Area (mm²)': f"{area_mm2:.3f}",
209
- 'Mean': f"{mean:.3f}",
210
- 'StdDev': f"{stddev:.3f}",
211
  'Min': f"{min_val:.3f}",
212
  'Max': f"{max_val:.3f}",
213
- 'Point': f"({x:.1f}, {y:.1f})"
214
  }
215
 
216
  self.results.append(result)
217
- self.marks.append((x, y, self.circle_diameter))
218
 
219
  return self.update_display(), self.format_results()
220
  except Exception as e:
221
- logger.error(f"Error analyzing ROI: {str(e)}")
222
  return self.display_image, f"Error analyzing ROI: {str(e)}"
223
-
224
- @debug_decorator
225
  def update_display(self):
226
  try:
227
  if self.original_display is None:
@@ -238,30 +201,30 @@ class DicomAnalyzer:
238
  # Convert to BGR for drawing
239
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
240
 
241
- # Draw marks with dots
242
  for x, y, diameter in self.marks:
243
  zoomed_x = int(x * self.zoom_factor)
244
  zoomed_y = int(y * self.zoom_factor)
245
- zoomed_diameter = int(diameter * self.zoom_factor)
246
 
247
  # Draw main circle
248
  cv2.circle(zoomed_bgr,
249
  (zoomed_x, zoomed_y),
250
- zoomed_diameter // 2,
251
- self.CIRCLE_COLOR,
252
  1,
253
  lineType=cv2.LINE_AA)
254
 
255
- # Draw dots on circle
256
  num_points = 8
257
  for i in range(num_points):
258
  angle = 2 * np.pi * i / num_points
259
- point_x = int(zoomed_x + (zoomed_diameter/2) * np.cos(angle))
260
- point_y = int(zoomed_y + (zoomed_diameter/2) * np.sin(angle))
261
  cv2.circle(zoomed_bgr,
262
  (point_x, point_y),
263
  1,
264
- self.CIRCLE_COLOR,
265
  -1,
266
  lineType=cv2.LINE_AA)
267
 
@@ -282,15 +245,16 @@ class DicomAnalyzer:
282
 
283
  return visible
284
  except Exception as e:
285
- logger.error(f"Error updating display: {str(e)}")
286
  return self.original_display
287
 
288
  def format_results(self):
289
  if not self.results:
290
  return "No measurements yet"
291
  df = pd.DataFrame(self.results)
292
- columns = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
293
- return df[columns].to_string(index=False)
 
294
 
295
  def add_blank_row(self, image):
296
  self.results.append({
@@ -321,15 +285,14 @@ class DicomAnalyzer:
321
  self.marks.pop()
322
  return self.update_display(), self.format_results()
323
 
324
- @debug_decorator
325
  def save_results(self):
326
  try:
327
  if not self.results:
328
  return None, "No results to save"
329
 
330
  df = pd.DataFrame(self.results)
331
- columns = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
332
- df = df[columns]
333
 
334
  temp_file = "analysis_results.xlsx"
335
  df.to_excel(temp_file, index=False)
@@ -339,6 +302,7 @@ class DicomAnalyzer:
339
  return None, f"Error saving results: {str(e)}"
340
 
341
  def create_interface():
 
342
  analyzer = DicomAnalyzer()
343
 
344
  with gr.Blocks(css="#image_display { outline: none; }") as interface:
@@ -361,11 +325,7 @@ def create_interface():
361
  reset_btn = gr.Button("Reset View")
362
 
363
  with gr.Column():
364
- image_display = gr.Image(
365
- label="DICOM Image",
366
- interactive=True,
367
- elem_id="image_display"
368
- )
369
 
370
  with gr.Row():
371
  blank_btn = gr.Button("Add Blank Row")
@@ -377,6 +337,18 @@ def create_interface():
377
  file_output = gr.File(label="Download Results")
378
  key_press = gr.Textbox(visible=False, elem_id="key_press")
379
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  # Event handlers
381
  file_input.change(
382
  fn=analyzer.load_dicom,
@@ -390,23 +362,21 @@ def create_interface():
390
  )
391
 
392
  diameter_slider.change(
393
- fn=lambda x: setattr(analyzer, 'circle_diameter', x),
394
  inputs=diameter_slider,
395
- outputs=None
396
  )
397
 
398
  zoom_in_btn.click(
399
  fn=analyzer.zoom_in,
400
  inputs=image_display,
401
- outputs=image_display,
402
- queue=False
403
  )
404
 
405
  zoom_out_btn.click(
406
  fn=analyzer.zoom_out,
407
  inputs=image_display,
408
- outputs=image_display,
409
- queue=False
410
  )
411
 
412
  reset_btn.click(
@@ -443,7 +413,7 @@ def create_interface():
443
  outputs=[file_output, results_display]
444
  )
445
 
446
- gr.HTML("""
447
  <script>
448
  document.addEventListener('keydown', function(e) {
449
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
@@ -456,14 +426,17 @@ def create_interface():
456
  }
457
  });
458
  </script>
459
- """)
460
-
 
 
461
  return interface
462
 
463
  if __name__ == "__main__":
464
  try:
465
- logger.info("Starting DICOM Analyzer application")
466
  interface = create_interface()
 
467
  interface.launch(
468
  server_name="0.0.0.0",
469
  server_port=7860,
@@ -471,6 +444,5 @@ if __name__ == "__main__":
471
  debug=True
472
  )
473
  except Exception as e:
474
- logger.error(f"Error launching application: {str(e)}")
475
- logger.error(traceback.format_exc())
476
  raise e
 
3
  import numpy as np
4
  import pandas as pd
5
  import pydicom
6
+ import io
 
 
 
 
7
  from PIL import Image
8
 
9
+ print("Starting imports completed...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  class DicomAnalyzer:
12
  def __init__(self):
 
16
  self.current_image = None
17
  self.dicom_data = None
18
  self.display_image = None
19
+ self.marks = [] # Store (x, y, diameter) for each mark
20
  self.original_image = None
21
  self.original_display = None
22
+ # Pan position
23
  self.pan_x = 0
24
  self.pan_y = 0
25
  self.max_pan_x = 0
26
  self.max_pan_y = 0
27
+ # Circle color in BGR
28
+ self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
29
+ print("DicomAnalyzer initialized...")
 
 
 
 
 
30
 
 
31
  def load_dicom(self, file):
32
  try:
33
  if file is None:
 
51
  self.display_image = self.normalize_image(image)
52
  self.original_display = self.display_image.copy()
53
 
54
+ # Reset view on new image
55
  self.reset_view()
56
+ print("DICOM file loaded successfully")
57
+
58
  return self.display_image, "DICOM file loaded successfully"
59
  except Exception as e:
60
+ print(f"Error loading DICOM file: {str(e)}")
61
+ return None, f"Error loading DICOM file: {str(e)}"
62
 
 
63
  def normalize_image(self, image):
64
  try:
65
  normalized = cv2.normalize(
 
74
  normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
75
  return normalized
76
  except Exception as e:
77
+ print(f"Error normalizing image: {str(e)}")
78
  return None
79
 
 
80
  def reset_view(self):
81
  self.zoom_factor = 1.0
82
  self.pan_x = 0
 
85
  return self.update_display()
86
  return None
87
 
 
88
  def zoom_in(self, image):
89
+ print("Zooming in...")
90
+ self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
91
+ return self.update_display()
 
 
 
 
 
 
92
 
 
93
  def zoom_out(self, image):
94
+ print("Zooming out...")
95
+ self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
96
+ return self.update_display()
 
 
 
 
 
 
97
 
 
98
  def handle_keyboard(self, key):
99
  try:
100
+ print(f"Handling key press: {key}")
101
  pan_amount = int(5 * self.zoom_factor)
102
 
103
+ original_pan_x = self.pan_x
104
+ original_pan_y = self.pan_y
105
+
106
  if key == 'ArrowLeft':
107
  self.pan_x = max(0, self.pan_x - pan_amount)
108
  elif key == 'ArrowRight':
 
112
  elif key == 'ArrowDown':
113
  self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
114
 
115
+ print(f"Pan X: {self.pan_x} (was {original_pan_x})")
116
+ print(f"Pan Y: {self.pan_y} (was {original_pan_y})")
117
+ print(f"Max Pan X: {self.max_pan_x}")
118
+ print(f"Max Pan Y: {self.max_pan_y}")
119
+
120
  return self.update_display()
121
  except Exception as e:
122
+ print(f"Error handling keyboard input: {str(e)}")
123
  return self.display_image
124
 
 
125
  def analyze_roi(self, evt: gr.SelectData):
126
  try:
127
  if self.current_image is None:
128
  return None, "No image loaded"
129
 
130
+ # Get clicked coordinates
131
  clicked_x, clicked_y = evt.index[0], evt.index[1]
132
 
133
+ # Transform coordinates to match ImageJ
134
  x = (clicked_x + self.pan_x) / self.zoom_factor
135
  y = (clicked_y + self.pan_y) / self.zoom_factor
136
+
137
+ # ImageJ coordinate correction
138
+ x = x - 0.5 # ImageJ starts from 0.5, not 0
139
+ y = y - 0.5
140
+
141
  # Get image dimensions
142
  height, width = self.current_image.shape[:2]
143
 
144
+ # Create precise circular mask (ImageJ method)
145
+ y_coords, x_coords = np.ogrid[:height, :width]
146
+ radius = self.circle_diameter / 2.0
147
+
148
+ # Use exact ImageJ distance calculation
149
+ dist_squared = (x_coords - x)**2 + (y_coords - y)**2
150
+ mask = dist_squared <= radius**2
151
+
152
+ # Get ROI pixels
153
+ roi_pixels = self.current_image[mask]
154
+
155
+ if len(roi_pixels) == 0:
156
+ return self.display_image, "Error: No pixels selected"
157
 
158
+ # Get pixel spacing (mm/pixel)
 
159
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
160
 
161
+ # Calculate statistics exactly like ImageJ
162
+ n_pixels = len(roi_pixels)
163
+ mean_value = np.mean(roi_pixels)
164
+ std_dev = np.std(roi_pixels, ddof=1) # ImageJ uses n-1
 
165
  min_val = np.min(roi_pixels)
166
  max_val = np.max(roi_pixels)
167
+
168
+ # Calculate area using ImageJ method
169
+ area = n_pixels * (pixel_spacing ** 2)
170
 
171
+ # Store results with ImageJ precision
172
  result = {
173
+ 'Area (mm²)': f"{area:.3f}",
174
+ 'Mean': f"{mean_value:.3f}",
175
+ 'StdDev': f"{std_dev:.3f}",
176
  'Min': f"{min_val:.3f}",
177
  'Max': f"{max_val:.3f}",
178
+ 'Point': f"({x+0.5:.1f}, {y+0.5:.1f})" # Add 0.5 back for display
179
  }
180
 
181
  self.results.append(result)
182
+ self.marks.append((x+0.5, y+0.5, self.circle_diameter))
183
 
184
  return self.update_display(), self.format_results()
185
  except Exception as e:
186
+ print(f"Error analyzing ROI: {str(e)}")
187
  return self.display_image, f"Error analyzing ROI: {str(e)}"
 
 
188
  def update_display(self):
189
  try:
190
  if self.original_display is None:
 
201
  # Convert to BGR for drawing
202
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
203
 
204
+ # Draw marks with ImageJ-style dots
205
  for x, y, diameter in self.marks:
206
  zoomed_x = int(x * self.zoom_factor)
207
  zoomed_y = int(y * self.zoom_factor)
208
+ zoomed_radius = int((diameter/2) * self.zoom_factor)
209
 
210
  # Draw main circle
211
  cv2.circle(zoomed_bgr,
212
  (zoomed_x, zoomed_y),
213
+ zoomed_radius,
214
+ self.CIRCLE_COLOR, # BGR Yellow (0, 255, 255)
215
  1,
216
  lineType=cv2.LINE_AA)
217
 
218
+ # Draw dots like ImageJ
219
  num_points = 8
220
  for i in range(num_points):
221
  angle = 2 * np.pi * i / num_points
222
+ point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
223
+ point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
224
  cv2.circle(zoomed_bgr,
225
  (point_x, point_y),
226
  1,
227
+ self.CIRCLE_COLOR, # BGR Yellow (0, 255, 255)
228
  -1,
229
  lineType=cv2.LINE_AA)
230
 
 
245
 
246
  return visible
247
  except Exception as e:
248
+ print(f"Error updating display: {str(e)}")
249
  return self.original_display
250
 
251
  def format_results(self):
252
  if not self.results:
253
  return "No measurements yet"
254
  df = pd.DataFrame(self.results)
255
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
256
+ df = df[columns_order]
257
+ return df.to_string(index=False)
258
 
259
  def add_blank_row(self, image):
260
  self.results.append({
 
285
  self.marks.pop()
286
  return self.update_display(), self.format_results()
287
 
 
288
  def save_results(self):
289
  try:
290
  if not self.results:
291
  return None, "No results to save"
292
 
293
  df = pd.DataFrame(self.results)
294
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
295
+ df = df[columns_order]
296
 
297
  temp_file = "analysis_results.xlsx"
298
  df.to_excel(temp_file, index=False)
 
302
  return None, f"Error saving results: {str(e)}"
303
 
304
  def create_interface():
305
+ print("Creating interface...")
306
  analyzer = DicomAnalyzer()
307
 
308
  with gr.Blocks(css="#image_display { outline: none; }") as interface:
 
325
  reset_btn = gr.Button("Reset View")
326
 
327
  with gr.Column():
328
+ image_display = gr.Image(label="DICOM Image", interactive=True, elem_id="image_display")
 
 
 
 
329
 
330
  with gr.Row():
331
  blank_btn = gr.Button("Add Blank Row")
 
337
  file_output = gr.File(label="Download Results")
338
  key_press = gr.Textbox(visible=False, elem_id="key_press")
339
 
340
+ gr.Markdown("""
341
+ ### Controls:
342
+ - Use arrow keys to pan when zoomed in
343
+ - Click points to measure
344
+ - Use Zoom In/Out buttons or Reset View to adjust zoom level
345
+ """)
346
+
347
+ def update_diameter(x):
348
+ analyzer.circle_diameter = x
349
+ print(f"Diameter updated to: {x}")
350
+ return f"Diameter set to {x} pixels"
351
+
352
  # Event handlers
353
  file_input.change(
354
  fn=analyzer.load_dicom,
 
362
  )
363
 
364
  diameter_slider.change(
365
+ fn=update_diameter,
366
  inputs=diameter_slider,
367
+ outputs=gr.Textbox(label="Status")
368
  )
369
 
370
  zoom_in_btn.click(
371
  fn=analyzer.zoom_in,
372
  inputs=image_display,
373
+ outputs=image_display
 
374
  )
375
 
376
  zoom_out_btn.click(
377
  fn=analyzer.zoom_out,
378
  inputs=image_display,
379
+ outputs=image_display
 
380
  )
381
 
382
  reset_btn.click(
 
413
  outputs=[file_output, results_display]
414
  )
415
 
416
+ js = """
417
  <script>
418
  document.addEventListener('keydown', function(e) {
419
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
 
426
  }
427
  });
428
  </script>
429
+ """
430
+ gr.HTML(js)
431
+
432
+ print("Interface created successfully")
433
  return interface
434
 
435
  if __name__ == "__main__":
436
  try:
437
+ print("Starting application...")
438
  interface = create_interface()
439
+ print("Launching interface...")
440
  interface.launch(
441
  server_name="0.0.0.0",
442
  server_port=7860,
 
444
  debug=True
445
  )
446
  except Exception as e:
447
+ print(f"Error launching application: {str(e)}")
 
448
  raise e