HeshamAI commited on
Commit
5da7c41
·
verified ·
1 Parent(s): f7fddc5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -53
app.py CHANGED
@@ -5,9 +5,46 @@ 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):
13
  self.results = []
@@ -124,17 +161,14 @@ class DicomAnalyzer:
124
  except Exception as e:
125
  print(f"Error handling keyboard input: {str(e)}")
126
  return self.display_image
127
-
128
  def analyze_roi(self, evt: gr.SelectData):
129
  try:
130
  if self.current_image is None:
131
  return None, "No image loaded"
132
 
133
- # Get clicked coordinates
134
  clicked_x = evt.index[0]
135
  clicked_y = evt.index[1]
136
 
137
- # Transform coordinates
138
  x = clicked_x + self.pan_x
139
  y = clicked_y + self.pan_y
140
  if self.zoom_factor != 1.0:
@@ -144,63 +178,43 @@ class DicomAnalyzer:
144
  x = int(round(x))
145
  y = int(round(y))
146
 
147
- # Get image dimensions
148
  height, width = self.original_image.shape[:2]
149
 
150
- # Create mask using ImageJ's exact method
151
  Y, X = np.ogrid[:height, :width]
152
 
153
- # Use exact 9-pixel diameter
154
  radius = self.circle_diameter / 2.0
155
  r_squared = radius * radius
156
 
157
- # Calculate distances exactly as ImageJ does
158
  dx = X - x
159
  dy = Y - y
160
  dist_squared = dx*dx + dy*dy
161
 
162
- # Create mask with ImageJ's method
163
  mask = np.zeros((height, width), dtype=bool)
164
  mask[dist_squared <= r_squared] = True
165
 
166
- # Get ROI pixels from original DICOM values
167
  roi_pixels = self.original_image[mask]
168
 
169
  if len(roi_pixels) == 0:
170
  return self.display_image, "Error: No pixels selected"
171
 
172
- # Get pixel spacing (mm/pixel)
173
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
174
 
175
- # Calculate area (this part is correct)
176
  n_pixels = np.sum(mask)
177
  area = n_pixels * (pixel_spacing ** 2)
178
 
179
- # Calculate statistics using ImageJ's methods
180
  mean_value = np.mean(roi_pixels)
181
- std_dev = np.std(roi_pixels, ddof=1) # ImageJ uses n-1
182
  min_val = np.min(roi_pixels)
183
  max_val = np.max(roi_pixels)
184
 
185
- # Apply any necessary scaling from DICOM
186
  rescale_slope = getattr(self.dicom_data, 'RescaleSlope', 1)
187
  rescale_intercept = getattr(self.dicom_data, 'RescaleIntercept', 0)
188
 
189
- # Adjust values using DICOM scaling
190
  mean_value = (mean_value * rescale_slope) + rescale_intercept
191
  std_dev = std_dev * rescale_slope
192
  min_val = (min_val * rescale_slope) + rescale_intercept
193
  max_val = (max_val * rescale_slope) + rescale_intercept
194
 
195
- print(f"\nImageJ-compatible Analysis:")
196
- print(f"Position: ({x}, {y})")
197
- print(f"Pixel count: {n_pixels}")
198
- print(f"Area: {area:.3f} mm²")
199
- print(f"Mean: {mean_value:.3f}")
200
- print(f"StdDev: {std_dev:.3f}")
201
- print(f"Min: {min_val}")
202
- print(f"Max: {max_val}")
203
-
204
  result = {
205
  'Area (mm²)': f"{area:.3f}",
206
  'Mean': f"{mean_value:.3f}",
@@ -217,6 +231,7 @@ class DicomAnalyzer:
217
  except Exception as e:
218
  print(f"Error analyzing ROI: {str(e)}")
219
  return self.display_image, f"Error analyzing ROI: {str(e)}"
 
220
  def update_display(self):
221
  try:
222
  if self.original_display is None:
@@ -226,29 +241,23 @@ class DicomAnalyzer:
226
  new_height = int(height * self.zoom_factor)
227
  new_width = int(width * self.zoom_factor)
228
 
229
- # Create zoomed image
230
  zoomed = cv2.resize(self.original_display, (new_width, new_height),
231
  interpolation=cv2.INTER_CUBIC)
232
 
233
- # Convert to BGR for drawing
234
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
235
 
236
- # Draw marks with ImageJ-style dots
237
  for x, y, diameter in self.marks:
238
  zoomed_x = int(x * self.zoom_factor)
239
  zoomed_y = int(y * self.zoom_factor)
240
- # Use exact radius without any additions
241
  zoomed_radius = int((diameter/2.0) * self.zoom_factor)
242
 
243
- # Draw main circle
244
  cv2.circle(zoomed_bgr,
245
  (zoomed_x, zoomed_y),
246
  zoomed_radius,
247
- self.CIRCLE_COLOR, # BGR Yellow
248
  1,
249
  lineType=cv2.LINE_AA)
250
 
251
- # Draw dots like ImageJ
252
  num_points = 8
253
  for i in range(num_points):
254
  angle = 2 * np.pi * i / num_points
@@ -261,16 +270,13 @@ class DicomAnalyzer:
261
  -1,
262
  lineType=cv2.LINE_AA)
263
 
264
- # Convert back to RGB for display
265
  zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
266
 
267
- # Calculate pan limits
268
  self.max_pan_x = max(0, new_width - width)
269
  self.max_pan_y = max(0, new_height - height)
270
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
271
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
272
 
273
- # Extract visible portion
274
  visible = zoomed[
275
  int(self.pan_y):int(self.pan_y + height),
276
  int(self.pan_x):int(self.pan_x + width)
@@ -289,6 +295,79 @@ class DicomAnalyzer:
289
  df = df[columns_order]
290
  return df.to_string(index=False)
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  def add_blank_row(self, image):
293
  self.results.append({
294
  'Area (mm²)': '',
@@ -318,22 +397,7 @@ class DicomAnalyzer:
318
  self.marks.pop()
319
  return self.update_display(), self.format_results()
320
 
321
- def save_results(self):
322
- try:
323
- if not self.results:
324
- return None, "No results to save"
325
-
326
- df = pd.DataFrame(self.results)
327
- columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
328
- df = df[columns_order]
329
-
330
- temp_file = "analysis_results.xlsx"
331
- df.to_excel(temp_file, index=False)
332
-
333
- return temp_file, "Results saved successfully"
334
- except Exception as e:
335
- return None, f"Error saving results: {str(e)}"
336
-
337
  def create_interface():
338
  print("Creating interface...")
339
  analyzer = DicomAnalyzer()
@@ -358,7 +422,11 @@ def create_interface():
358
  reset_btn = gr.Button("Reset View")
359
 
360
  with gr.Column():
361
- image_display = gr.Image(label="DICOM Image", interactive=True, elem_id="image_display")
 
 
 
 
362
 
363
  with gr.Row():
364
  blank_btn = gr.Button("Add Blank Row")
@@ -375,6 +443,7 @@ def create_interface():
375
  - Use arrow keys to pan when zoomed in
376
  - Click points to measure
377
  - Use Zoom In/Out buttons or Reset View to adjust zoom level
 
378
  """)
379
 
380
  def update_diameter(x):
@@ -403,13 +472,15 @@ def create_interface():
403
  zoom_in_btn.click(
404
  fn=analyzer.zoom_in,
405
  inputs=image_display,
406
- outputs=image_display
 
407
  )
408
 
409
  zoom_out_btn.click(
410
  fn=analyzer.zoom_out,
411
  inputs=image_display,
412
- outputs=image_display
 
413
  )
414
 
415
  reset_btn.click(
@@ -478,4 +549,6 @@ if __name__ == "__main__":
478
  )
479
  except Exception as e:
480
  print(f"Error launching application: {str(e)}")
 
 
481
  raise e
 
5
  import pydicom
6
  import io
7
  from PIL import Image
8
+ import openpyxl
9
+ from openpyxl.utils import get_column_letter, column_index_from_string
10
+ import logging
11
+ import time
12
+ import traceback
13
+ from functools import wraps
14
+ import sys
15
 
16
  print("Starting imports completed...")
17
 
18
+ # Set up logging
19
+ logging.basicConfig(
20
+ level=logging.DEBUG,
21
+ format='%(asctime)s - %(levelname)s - %(message)s',
22
+ handlers=[
23
+ logging.FileHandler('dicom_analyzer_debug.log'),
24
+ logging.StreamHandler(sys.stdout)
25
+ ]
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ def debug_decorator(func):
31
+ @wraps(func)
32
+ def wrapper(*args, **kwargs):
33
+ logger.debug(f"Entering {func.__name__}")
34
+ start_time = time.time()
35
+ try:
36
+ result = func(*args, **kwargs)
37
+ logger.debug(f"Function {func.__name__} completed successfully")
38
+ return result
39
+ except Exception as e:
40
+ logger.error(f"Error in {func.__name__}: {str(e)}")
41
+ logger.error(traceback.format_exc())
42
+ raise
43
+ finally:
44
+ end_time = time.time()
45
+ logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
46
+ return wrapper
47
+
48
  class DicomAnalyzer:
49
  def __init__(self):
50
  self.results = []
 
161
  except Exception as e:
162
  print(f"Error handling keyboard input: {str(e)}")
163
  return self.display_image
 
164
  def analyze_roi(self, evt: gr.SelectData):
165
  try:
166
  if self.current_image is None:
167
  return None, "No image loaded"
168
 
 
169
  clicked_x = evt.index[0]
170
  clicked_y = evt.index[1]
171
 
 
172
  x = clicked_x + self.pan_x
173
  y = clicked_y + self.pan_y
174
  if self.zoom_factor != 1.0:
 
178
  x = int(round(x))
179
  y = int(round(y))
180
 
 
181
  height, width = self.original_image.shape[:2]
182
 
 
183
  Y, X = np.ogrid[:height, :width]
184
 
 
185
  radius = self.circle_diameter / 2.0
186
  r_squared = radius * radius
187
 
 
188
  dx = X - x
189
  dy = Y - y
190
  dist_squared = dx*dx + dy*dy
191
 
 
192
  mask = np.zeros((height, width), dtype=bool)
193
  mask[dist_squared <= r_squared] = True
194
 
 
195
  roi_pixels = self.original_image[mask]
196
 
197
  if len(roi_pixels) == 0:
198
  return self.display_image, "Error: No pixels selected"
199
 
 
200
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
201
 
 
202
  n_pixels = np.sum(mask)
203
  area = n_pixels * (pixel_spacing ** 2)
204
 
 
205
  mean_value = np.mean(roi_pixels)
206
+ std_dev = np.std(roi_pixels, ddof=1)
207
  min_val = np.min(roi_pixels)
208
  max_val = np.max(roi_pixels)
209
 
 
210
  rescale_slope = getattr(self.dicom_data, 'RescaleSlope', 1)
211
  rescale_intercept = getattr(self.dicom_data, 'RescaleIntercept', 0)
212
 
 
213
  mean_value = (mean_value * rescale_slope) + rescale_intercept
214
  std_dev = std_dev * rescale_slope
215
  min_val = (min_val * rescale_slope) + rescale_intercept
216
  max_val = (max_val * rescale_slope) + rescale_intercept
217
 
 
 
 
 
 
 
 
 
 
218
  result = {
219
  'Area (mm²)': f"{area:.3f}",
220
  'Mean': f"{mean_value:.3f}",
 
231
  except Exception as e:
232
  print(f"Error analyzing ROI: {str(e)}")
233
  return self.display_image, f"Error analyzing ROI: {str(e)}"
234
+
235
  def update_display(self):
236
  try:
237
  if self.original_display is None:
 
241
  new_height = int(height * self.zoom_factor)
242
  new_width = int(width * self.zoom_factor)
243
 
 
244
  zoomed = cv2.resize(self.original_display, (new_width, new_height),
245
  interpolation=cv2.INTER_CUBIC)
246
 
 
247
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
248
 
 
249
  for x, y, diameter in self.marks:
250
  zoomed_x = int(x * self.zoom_factor)
251
  zoomed_y = int(y * self.zoom_factor)
 
252
  zoomed_radius = int((diameter/2.0) * self.zoom_factor)
253
 
 
254
  cv2.circle(zoomed_bgr,
255
  (zoomed_x, zoomed_y),
256
  zoomed_radius,
257
+ self.CIRCLE_COLOR,
258
  1,
259
  lineType=cv2.LINE_AA)
260
 
 
261
  num_points = 8
262
  for i in range(num_points):
263
  angle = 2 * np.pi * i / num_points
 
270
  -1,
271
  lineType=cv2.LINE_AA)
272
 
 
273
  zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
274
 
 
275
  self.max_pan_x = max(0, new_width - width)
276
  self.max_pan_y = max(0, new_height - height)
277
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
278
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
279
 
 
280
  visible = zoomed[
281
  int(self.pan_y):int(self.pan_y + height),
282
  int(self.pan_x):int(self.pan_x + width)
 
295
  df = df[columns_order]
296
  return df.to_string(index=False)
297
 
298
+ def save_results(self):
299
+ try:
300
+ if not self.results:
301
+ return None, "No results to save"
302
+
303
+ # Create a new workbook
304
+ wb = openpyxl.Workbook()
305
+ ws = wb.active
306
+
307
+ # Define the equation slots
308
+ equation_slots = [
309
+ ('B', 'F'), ('H', 'L'), ('N', 'R'), ('T', 'X'), ('Z', 'AD'),
310
+ ('AF', 'AJ'), ('AL', 'AP'), ('AR', 'AV'), ('AX', 'BB'), ('BD', 'BH'),
311
+ ('BJ', 'BN'), ('BP', 'BT'), ('BV', 'BZ'),
312
+ ]
313
+
314
+ # Define row groups
315
+ row_groups = [
316
+ (2, 3), (5, 6), (8, 9), (11, 12), (14, 15),
317
+ (17, 18), (20, 21), (23, 24), (26, 27), (29, 30),
318
+ ]
319
+
320
+ # Add headers for different phantom sizes
321
+ phantom_sizes = ['(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)', '(4.5mm)']
322
+ for i, size in enumerate(phantom_sizes):
323
+ row_index = row_groups[i][0] - 1
324
+ ws.cell(row=row_index, column=1, value=size)
325
+
326
+ # Process results in pairs
327
+ result_pairs = [self.results[i:i+2] for i in range(0, len(self.results), 2)]
328
+
329
+ for pair_idx, result_pair in enumerate(result_pairs):
330
+ if pair_idx >= len(equation_slots) * len(row_groups):
331
+ break
332
+
333
+ slot_idx = pair_idx % len(equation_slots)
334
+ group_idx = pair_idx // len(equation_slots)
335
+
336
+ if group_idx >= len(row_groups):
337
+ break
338
+
339
+ start_col, _ = equation_slots[slot_idx]
340
+ dest_rows = row_groups[group_idx]
341
+
342
+ # Fill data for the pair
343
+ for row_idx, result in enumerate(result_pair):
344
+ if row_idx < 2: # Only process up to 2 rows
345
+ dest_row = dest_rows[row_idx]
346
+
347
+ # Write row number
348
+ ws.cell(row=dest_row, column=1, value=row_idx + 1)
349
+
350
+ # Write values in correct columns
351
+ ws.cell(row=dest_row, column=openpyxl.utils.column_index_from_string(start_col),
352
+ value=float(result['Area (mm²)']))
353
+ ws.cell(row=dest_row, column=openpyxl.utils.column_index_from_string(start_col) + 1,
354
+ value=float(result['Mean']))
355
+ ws.cell(row=dest_row, column=openpyxl.utils.column_index_from_string(start_col) + 2,
356
+ value=float(result['StdDev']))
357
+ ws.cell(row=dest_row, column=openpyxl.utils.column_index_from_string(start_col) + 3,
358
+ value=float(result['Min']))
359
+ ws.cell(row=dest_row, column=openpyxl.utils.column_index_from_string(start_col) + 4,
360
+ value=float(result['Max']))
361
+
362
+ # Save the workbook
363
+ output_file = "analysis_results.xlsx"
364
+ wb.save(output_file)
365
+
366
+ return output_file, "Results saved successfully in the required format"
367
+ except Exception as e:
368
+ print(f"Error saving results: {str(e)}")
369
+ return None, f"Error saving results: {str(e)}"
370
+
371
  def add_blank_row(self, image):
372
  self.results.append({
373
  'Area (mm²)': '',
 
397
  self.marks.pop()
398
  return self.update_display(), self.format_results()
399
 
400
+ # ... (rest of the code with create_interface and main remains the same)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  def create_interface():
402
  print("Creating interface...")
403
  analyzer = DicomAnalyzer()
 
422
  reset_btn = gr.Button("Reset View")
423
 
424
  with gr.Column():
425
+ image_display = gr.Image(
426
+ label="DICOM Image",
427
+ interactive=True,
428
+ elem_id="image_display"
429
+ )
430
 
431
  with gr.Row():
432
  blank_btn = gr.Button("Add Blank Row")
 
443
  - Use arrow keys to pan when zoomed in
444
  - Click points to measure
445
  - Use Zoom In/Out buttons or Reset View to adjust zoom level
446
+ - Results will be saved in ImageJ-compatible format
447
  """)
448
 
449
  def update_diameter(x):
 
472
  zoom_in_btn.click(
473
  fn=analyzer.zoom_in,
474
  inputs=image_display,
475
+ outputs=image_display,
476
+ queue=False # Allow continuous clicking
477
  )
478
 
479
  zoom_out_btn.click(
480
  fn=analyzer.zoom_out,
481
  inputs=image_display,
482
+ outputs=image_display,
483
+ queue=False # Allow continuous clicking
484
  )
485
 
486
  reset_btn.click(
 
549
  )
550
  except Exception as e:
551
  print(f"Error launching application: {str(e)}")
552
+ logger.error(f"Error launching application: {str(e)}")
553
+ logger.error(traceback.format_exc())
554
  raise e