HeshamAI commited on
Commit
069672f
·
verified ·
1 Parent(s): 5c75a5b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +163 -152
app.py CHANGED
@@ -3,13 +3,12 @@ import cv2
3
  import numpy as np
4
  import pandas as pd
5
  import pydicom
6
- from PIL import Image
7
  import logging
8
  import time
9
  import traceback
10
  from functools import wraps
11
  import sys
12
- import os
13
 
14
  # Set up logging
15
  logging.basicConfig(
@@ -27,7 +26,6 @@ def debug_decorator(func):
27
  @wraps(func)
28
  def wrapper(*args, **kwargs):
29
  logger.debug(f"Entering {func.__name__}")
30
- logger.debug(f"Arguments: args={args}, kwargs={kwargs}")
31
  start_time = time.time()
32
  try:
33
  result = func(*args, **kwargs)
@@ -44,10 +42,6 @@ def debug_decorator(func):
44
 
45
  class DicomAnalyzer:
46
  def __init__(self):
47
- self.logger = logging.getLogger(__name__)
48
- self.logger.debug("Initializing DicomAnalyzer")
49
-
50
- # Initialize state variables
51
  self.results = []
52
  self.circle_diameter = 9
53
  self.zoom_factor = 1.0
@@ -63,19 +57,12 @@ 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
70
 
71
- # Debug state
72
- self.debug_info = {
73
- 'last_click': None,
74
- 'last_transformed_coords': None,
75
- 'zoom_history': [],
76
- 'pan_history': [],
77
- 'measurements': []
78
- }
79
 
80
  @debug_decorator
81
  def load_dicom(self, file):
@@ -87,11 +74,9 @@ class DicomAnalyzer:
87
  dicom_data = pydicom.dcmread(file.name)
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,20 +85,10 @@ class DicomAnalyzer:
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)}")
@@ -122,7 +97,14 @@ class DicomAnalyzer:
122
  @debug_decorator
123
  def normalize_image(self, image):
124
  try:
125
- normalized = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
 
 
 
 
 
 
 
126
  if len(normalized.shape) == 2:
127
  normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
128
  return normalized
@@ -142,11 +124,10 @@ class DicomAnalyzer:
142
  @debug_decorator
143
  def zoom_in(self, image):
144
  try:
145
- new_zoom = self.zoom_factor * self.ZOOM_STEP
146
  if new_zoom <= self.MAX_ZOOM:
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)}")
@@ -155,97 +136,90 @@ class DicomAnalyzer:
155
  @debug_decorator
156
  def zoom_out(self, image):
157
  try:
158
- new_zoom = self.zoom_factor / self.ZOOM_STEP
159
  if new_zoom >= self.MIN_ZOOM:
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # 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}",
212
- 'StdDev': f"{std_dev:.3f}",
213
  'Min': f"{min_val:.3f}",
214
  'Max': f"{max_val:.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)}")
248
- return self.display_image, f"Error in ROI analysis: {str(e)}"
249
 
250
  @debug_decorator
251
  def update_display(self):
@@ -254,39 +228,54 @@ class DicomAnalyzer:
254
  return None
255
 
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)
274
- display_image = cv2.resize(
275
- display_image,
276
- (new_width, new_height),
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)
292
  ]
@@ -296,26 +285,6 @@ class DicomAnalyzer:
296
  logger.error(f"Error updating display: {str(e)}")
297
  return self.original_display
298
 
299
- @debug_decorator
300
- def handle_keyboard(self, key):
301
- try:
302
- pan_amount = int(20 * self.zoom_factor)
303
-
304
- if key == 'ArrowLeft':
305
- self.pan_x = max(0, self.pan_x - pan_amount)
306
- elif key == 'ArrowRight':
307
- self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount)
308
- elif key == 'ArrowUp':
309
- self.pan_y = max(0, self.pan_y - pan_amount)
310
- elif key == 'ArrowDown':
311
- self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
312
-
313
- self.debug_info['pan_history'].append((self.pan_x, self.pan_y))
314
- return self.update_display()
315
- except Exception as e:
316
- logger.error(f"Error handling keyboard input: {str(e)}")
317
- return self.display_image
318
-
319
  def format_results(self):
320
  if not self.results:
321
  return "No measurements yet"
@@ -323,6 +292,35 @@ class DicomAnalyzer:
323
  columns = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
324
  return df[columns].to_string(index=False)
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  @debug_decorator
327
  def save_results(self):
328
  try:
@@ -330,13 +328,12 @@ class DicomAnalyzer:
330
  return None, "No results to save"
331
 
332
  df = pd.DataFrame(self.results)
 
 
 
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)}"
@@ -344,7 +341,7 @@ class DicomAnalyzer:
344
  def create_interface():
345
  analyzer = DicomAnalyzer()
346
 
347
- with gr.Blocks() as interface:
348
  gr.Markdown("# DICOM Image Analyzer")
349
 
350
  with gr.Row():
@@ -371,8 +368,10 @@ def create_interface():
371
  )
372
 
373
  with gr.Row():
 
 
 
374
  save_btn = gr.Button("Save Results")
375
- debug_btn = gr.Button("Print Debug Info")
376
 
377
  results_display = gr.Textbox(label="Results", interactive=False)
378
  file_output = gr.File(label="Download Results")
@@ -400,16 +399,14 @@ def create_interface():
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(
@@ -423,14 +420,28 @@ def create_interface():
423
  outputs=image_display
424
  )
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  save_btn.click(
427
  fn=analyzer.save_results,
428
  outputs=[file_output, results_display]
429
  )
430
-
431
- debug_btn.click(
432
- fn=lambda: logger.debug(f"Debug Info: {analyzer.debug_info}")
433
- )
434
 
435
  gr.HTML("""
436
  <script>
 
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(
 
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)
 
42
 
43
  class DicomAnalyzer:
44
  def __init__(self):
 
 
 
 
45
  self.results = []
46
  self.circle_diameter = 9
47
  self.zoom_factor = 1.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):
 
74
  dicom_data = pydicom.dcmread(file.name)
75
  else:
76
  dicom_data = pydicom.dcmread(file)
77
+
78
+ image = dicom_data.pixel_array.astype(np.float32)
79
 
 
 
 
 
80
  rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
81
  rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
82
  image = (image * rescale_slope) + rescale_intercept
 
85
  self.original_image = image.copy()
86
  self.dicom_data = dicom_data
87
 
 
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)}")
 
97
  @debug_decorator
98
  def normalize_image(self, image):
99
  try:
100
+ normalized = cv2.normalize(
101
+ image,
102
+ None,
103
+ alpha=0,
104
+ beta=255,
105
+ norm_type=cv2.NORM_MINMAX,
106
+ dtype=cv2.CV_8U
107
+ )
108
  if len(normalized.shape) == 2:
109
  normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
110
  return normalized
 
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)}")
 
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':
156
+ self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount)
157
+ elif key == 'ArrowUp':
158
+ self.pan_y = max(0, self.pan_y - pan_amount)
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):
 
228
  return None
229
 
230
  height, width = self.original_display.shape[:2]
231
+ new_height = int(height * self.zoom_factor)
232
+ new_width = int(width * self.zoom_factor)
233
+
234
+ # Create zoomed image
235
+ zoomed = cv2.resize(self.original_display, (new_width, new_height),
236
+ interpolation=cv2.INTER_CUBIC)
237
 
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
 
268
+ # Convert back to RGB for display
269
+ zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
 
 
 
 
 
 
 
270
 
271
+ # Calculate pan limits
272
+ self.max_pan_x = max(0, new_width - width)
273
+ self.max_pan_y = max(0, new_height - height)
 
 
274
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
275
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
276
 
277
  # Extract visible portion
278
+ visible = zoomed[
279
  int(self.pan_y):int(self.pan_y + height),
280
  int(self.pan_x):int(self.pan_x + width)
281
  ]
 
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"
 
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({
297
+ 'Area (mm²)': '',
298
+ 'Mean': '',
299
+ 'StdDev': '',
300
+ 'Min': '',
301
+ 'Max': '',
302
+ 'Point': ''
303
+ })
304
+ return image, self.format_results()
305
+
306
+ def add_zero_row(self, image):
307
+ self.results.append({
308
+ 'Area (mm²)': '0.000',
309
+ 'Mean': '0.000',
310
+ 'StdDev': '0.000',
311
+ 'Min': '0.000',
312
+ 'Max': '0.000',
313
+ 'Point': '(0, 0)'
314
+ })
315
+ return image, self.format_results()
316
+
317
+ def undo_last(self, image):
318
+ if self.results:
319
+ self.results.pop()
320
+ if self.marks:
321
+ self.marks.pop()
322
+ return self.update_display(), self.format_results()
323
+
324
  @debug_decorator
325
  def save_results(self):
326
  try:
 
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)
336
 
 
 
 
 
337
  return temp_file, "Results saved successfully"
338
  except Exception as e:
339
  return None, f"Error saving results: {str(e)}"
 
341
  def create_interface():
342
  analyzer = DicomAnalyzer()
343
 
344
+ with gr.Blocks(css="#image_display { outline: none; }") as interface:
345
  gr.Markdown("# DICOM Image Analyzer")
346
 
347
  with gr.Row():
 
368
  )
369
 
370
  with gr.Row():
371
+ blank_btn = gr.Button("Add Blank Row")
372
+ zero_btn = gr.Button("Add Zero Row")
373
+ undo_btn = gr.Button("Undo Last")
374
  save_btn = gr.Button("Save Results")
 
375
 
376
  results_display = gr.Textbox(label="Results", interactive=False)
377
  file_output = gr.File(label="Download Results")
 
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(
 
420
  outputs=image_display
421
  )
422
 
423
+ blank_btn.click(
424
+ fn=analyzer.add_blank_row,
425
+ inputs=image_display,
426
+ outputs=[image_display, results_display]
427
+ )
428
+
429
+ zero_btn.click(
430
+ fn=analyzer.add_zero_row,
431
+ inputs=image_display,
432
+ outputs=[image_display, results_display]
433
+ )
434
+
435
+ undo_btn.click(
436
+ fn=analyzer.undo_last,
437
+ inputs=image_display,
438
+ outputs=[image_display, results_display]
439
+ )
440
+
441
  save_btn.click(
442
  fn=analyzer.save_results,
443
  outputs=[file_output, results_display]
444
  )
 
 
 
 
445
 
446
  gr.HTML("""
447
  <script>