HeshamAI commited on
Commit
a6a3ff0
·
verified ·
1 Parent(s): 64ce736

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -126
app.py CHANGED
@@ -1,70 +1,99 @@
1
- import gradio as gr
2
- import cv2
3
- import numpy as np
4
- import pandas as pd
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 = []
51
- self.circle_diameter = 9.0 # Changed to float for precise calculations
52
  self.zoom_factor = 1.0
53
- self.current_image = None
54
- self.dicom_data = None
55
- self.display_image = None
56
- self.marks = [] # Store (x, y, diameter) for each mark
57
- self.original_image = None
58
- self.original_display = None
59
- # Pan position
60
  self.pan_x = 0
61
  self.pan_y = 0
62
- self.max_pan_x = 0
63
- self.max_pan_y = 0
64
- # Circle color in BGR
65
- self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
66
- print("DicomAnalyzer initialized...")
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def load_dicom(self, file):
69
  try:
70
  if file is None:
@@ -294,79 +323,68 @@ class DicomAnalyzer:
294
  columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
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({
@@ -397,7 +415,6 @@ class DicomAnalyzer:
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()
@@ -408,6 +425,7 @@ def create_interface():
408
  with gr.Row():
409
  with gr.Column():
410
  file_input = gr.File(label="Upload DICOM file")
 
411
  diameter_slider = gr.Slider(
412
  minimum=1,
413
  maximum=20,
@@ -433,6 +451,7 @@ def create_interface():
433
  zero_btn = gr.Button("Add Zero Row")
434
  undo_btn = gr.Button("Undo Last")
435
  save_btn = gr.Button("Save Results")
 
436
 
437
  results_display = gr.Textbox(label="Results", interactive=False)
438
  file_output = gr.File(label="Download Results")
@@ -443,14 +462,21 @@ def create_interface():
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):
450
- analyzer.circle_diameter = float(x) # Convert to float
451
  print(f"Diameter updated to: {x}")
452
  return f"Diameter set to {x} pixels"
453
 
 
 
 
 
 
 
 
454
  # Event handlers
455
  file_input.change(
456
  fn=analyzer.load_dicom,
@@ -473,14 +499,14 @@ def create_interface():
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(
@@ -517,6 +543,12 @@ def create_interface():
517
  outputs=[file_output, results_display]
518
  )
519
 
 
 
 
 
 
 
520
  js = """
521
  <script>
522
  document.addEventListener('keydown', function(e) {
 
1
+ def load_dicom(self, file):
2
+ try:
3
+ if file is None:
4
+ return None, "No file uploaded"
5
+
6
+ if hasattr(file, 'name'):
7
+ dicom_data = pydicom.dcmread(file.name)
8
+ else:
9
+ dicom_data = pydicom.dcmread(file)
10
+
11
+ image = dicom_data.pixel_array.astype(np.float32)
12
+
13
+ # Store original pixel values before any scaling
14
+ self.original_image = image.copy()
15
+
16
+ # Apply DICOM scaling for display
17
+ rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
18
+ rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
19
+ image = (image * rescale_slope) + rescale_intercept
20
+
21
+ self.current_image = image
22
+ self.dicom_data = dicom_data
23
+
24
+ self.display_image = self.normalize_image(image)
25
+ self.original_display = self.display_image.copy()
26
+
27
+ # Reset view on new image
28
+ self.reset_view()
29
+ print("DICOM file loaded successfully")
30
+
31
+ return self.display_image, "DICOM file loaded successfully"
32
+ except Exception as e:
33
+ print(f"Error loading DICOM file: {str(e)}")
34
+ return None, f"Error loading DICOM file: {str(e)}"
35
+
36
+ def normalize_image(self, image):
37
  try:
38
+ normalized = cv2.normalize(
39
+ image,
40
+ None,
41
+ alpha=0,
42
+ beta=255,
43
+ norm_type=cv2.NORM_MINMAX,
44
+ dtype=cv2.CV_8U
45
+ )
46
+ if len(normalized.shape) == 2:
47
+ normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
48
+ return normalized
49
  except Exception as e:
50
+ print(f"Error normalizing image: {str(e)}")
51
+ return None
52
+
53
+ def reset_view(self):
 
 
 
 
 
 
 
 
54
  self.zoom_factor = 1.0
 
 
 
 
 
 
 
55
  self.pan_x = 0
56
  self.pan_y = 0
57
+ if self.original_display is not None:
58
+ return self.update_display()
59
+ return None
 
 
60
 
61
+ def zoom_in(self, image):
62
+ print("Zooming in...")
63
+ self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
64
+ return self.update_display()
65
+
66
+ def zoom_out(self, image):
67
+ print("Zooming out...")
68
+ self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
69
+ return self.update_display()
70
+
71
+ def handle_keyboard(self, key):
72
+ try:
73
+ print(f"Handling key press: {key}")
74
+ pan_amount = int(5 * self.zoom_factor)
75
+
76
+ original_pan_x = self.pan_x
77
+ original_pan_y = self.pan_y
78
+
79
+ if key == 'ArrowLeft':
80
+ self.pan_x = max(0, self.pan_x - pan_amount)
81
+ elif key == 'ArrowRight':
82
+ self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount)
83
+ elif key == 'ArrowUp':
84
+ self.pan_y = max(0, self.pan_y - pan_amount)
85
+ elif key == 'ArrowDown':
86
+ self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
87
+
88
+ print(f"Pan X: {self.pan_x} (was {original_pan_x})")
89
+ print(f"Pan Y: {self.pan_y} (was {original_pan_y})")
90
+ print(f"Max Pan X: {self.max_pan_x}")
91
+ print(f"Max Pan Y: {self.max_pan_y}")
92
+
93
+ return self.update_display()
94
+ except Exception as e:
95
+ print(f"Error handling keyboard input: {str(e)}")
96
+ return self.display_image
97
  def load_dicom(self, file):
98
  try:
99
  if file is None:
 
323
  columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
324
  df = df[columns_order]
325
  return df.to_string(index=False)
326
+ def save_results_to_template(self, template_path, output_path):
 
327
  try:
328
  if not self.results:
329
  return None, "No results to save"
330
 
331
+ # Load the Excel template
332
+ wb = openpyxl.load_workbook(template_path)
333
  ws = wb.active
334
 
335
+ # Define row groups and phantom sizes
336
+ row_groups = {
337
+ "7mm": [41, 42],
338
+ "6.5mm": [67, 68],
339
+ "6mm": [93, 94],
340
+ "5.5mm": [119, 120],
341
+ "5mm": [145, 146],
342
+ "4.5mm": [171, 172],
343
+ "4mm": [197, 198],
344
+ "3.5mm": [223, 224],
345
+ "3mm": [249, 250],
346
+ "2.5mm": [275, 276]
347
+ }
348
 
349
+ # Define columns for measurements
350
+ columns = {
351
+ 'Area (mm²)': "B",
352
+ 'Mean': "C",
353
+ 'StdDev': "D",
354
+ 'Min': "E",
355
+ 'Max': "F"
356
+ }
357
 
358
+ # Map results to phantom sizes (assuming results are ordered for each size)
359
+ result_idx = 0
 
 
 
360
 
361
+ for size, rows in row_groups.items():
362
+ for row in rows:
363
+ if result_idx >= len(self.results):
364
+ break
 
 
365
 
366
+ result = self.results[result_idx]
367
+
368
+ # Write each metric to the appropriate cell
369
+ for metric, col in columns.items():
370
+ cell = f"{col}{row}"
371
+ value = result.get(metric, '')
372
+ if value:
373
+ try:
374
+ ws[cell] = float(value)
375
+ except ValueError:
376
+ ws[cell] = value
377
+
378
+ result_idx += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  # Save the workbook
381
+ wb.save(output_path)
382
+
383
+ return output_path, "Results saved successfully in the template format"
384
+
385
  except Exception as e:
386
+ print(f"Error saving results to template: {str(e)}")
387
+ return None, f"Error saving results to template: {str(e)}"
388
 
389
  def add_blank_row(self, image):
390
  self.results.append({
 
415
  self.marks.pop()
416
  return self.update_display(), self.format_results()
417
 
 
418
  def create_interface():
419
  print("Creating interface...")
420
  analyzer = DicomAnalyzer()
 
425
  with gr.Row():
426
  with gr.Column():
427
  file_input = gr.File(label="Upload DICOM file")
428
+ template_input = gr.File(label="Upload Excel Template")
429
  diameter_slider = gr.Slider(
430
  minimum=1,
431
  maximum=20,
 
451
  zero_btn = gr.Button("Add Zero Row")
452
  undo_btn = gr.Button("Undo Last")
453
  save_btn = gr.Button("Save Results")
454
+ save_template_btn = gr.Button("Save to Template")
455
 
456
  results_display = gr.Textbox(label="Results", interactive=False)
457
  file_output = gr.File(label="Download Results")
 
462
  - Use arrow keys to pan when zoomed in
463
  - Click points to measure
464
  - Use Zoom In/Out buttons or Reset View to adjust zoom level
465
+ - Upload template and use Save to Template for formatted results
466
  """)
467
 
468
  def update_diameter(x):
469
+ analyzer.circle_diameter = float(x)
470
  print(f"Diameter updated to: {x}")
471
  return f"Diameter set to {x} pixels"
472
 
473
+ def save_to_template(template_file):
474
+ if template_file is None:
475
+ return None, "No template file provided"
476
+
477
+ output_path = "analysis_results_from_template.xlsx"
478
+ return analyzer.save_results_to_template(template_file.name, output_path)
479
+
480
  # Event handlers
481
  file_input.change(
482
  fn=analyzer.load_dicom,
 
499
  fn=analyzer.zoom_in,
500
  inputs=image_display,
501
  outputs=image_display,
502
+ queue=False
503
  )
504
 
505
  zoom_out_btn.click(
506
  fn=analyzer.zoom_out,
507
  inputs=image_display,
508
  outputs=image_display,
509
+ queue=False
510
  )
511
 
512
  reset_btn.click(
 
543
  outputs=[file_output, results_display]
544
  )
545
 
546
+ save_template_btn.click(
547
+ fn=save_to_template,
548
+ inputs=[template_input],
549
+ outputs=[file_output, results_display]
550
+ )
551
+
552
  js = """
553
  <script>
554
  document.addEventListener('keydown', function(e) {