HeshamAI commited on
Commit
49dd762
·
verified ·
1 Parent(s): 2c34aa3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +218 -306
app.py CHANGED
@@ -64,6 +64,7 @@ class DicomAnalyzer:
64
  # Circle color in BGR
65
  self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
66
  print("DicomAnalyzer initialized...")
 
67
  def load_dicom(self, file):
68
  try:
69
  if file is None:
@@ -160,6 +161,7 @@ class DicomAnalyzer:
160
  except Exception as e:
161
  print(f"Error handling keyboard input: {str(e)}")
162
  return self.display_image
 
163
  def analyze_roi(self, evt: gr.SelectData):
164
  try:
165
  if self.current_image is None:
@@ -230,327 +232,237 @@ class DicomAnalyzer:
230
  except Exception as e:
231
  print(f"Error analyzing ROI: {str(e)}")
232
  return self.display_image, f"Error analyzing ROI: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- def update_display(self):
235
- try:
236
- if self.original_display is None:
237
- return None
238
 
239
- height, width = self.original_display.shape[:2]
240
- new_height = int(height * self.zoom_factor)
241
- new_width = int(width * self.zoom_factor)
 
 
 
 
 
 
242
 
243
- zoomed = cv2.resize(self.original_display, (new_width, new_height),
244
- interpolation=cv2.INTER_CUBIC)
245
 
246
- zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
- for x, y, diameter in self.marks:
249
- zoomed_x = int(x * self.zoom_factor)
250
- zoomed_y = int(y * self.zoom_factor)
251
- zoomed_radius = int((diameter/2.0) * self.zoom_factor)
252
-
253
- cv2.circle(zoomed_bgr,
254
- (zoomed_x, zoomed_y),
255
- zoomed_radius,
256
- self.CIRCLE_COLOR,
257
- 1,
258
- lineType=cv2.LINE_AA)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- num_points = 8
261
- for i in range(num_points):
262
- angle = 2 * np.pi * i / num_points
263
- point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
264
- point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
265
- cv2.circle(zoomed_bgr,
266
- (point_x, point_y),
267
- 1,
268
- self.CIRCLE_COLOR,
269
- -1,
270
- lineType=cv2.LINE_AA)
271
-
272
- zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
273
-
274
- self.max_pan_x = max(0, new_width - width)
275
- self.max_pan_y = max(0, new_height - height)
276
- self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
277
- self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
278
-
279
- visible = zoomed[
280
- int(self.pan_y):int(self.pan_y + height),
281
- int(self.pan_x):int(self.pan_x + width)
282
- ]
283
-
284
- return visible
285
  except Exception as e:
286
- print(f"Error updating display: {str(e)}")
287
- return self.original_display
288
-
289
- def format_results(self):
290
- if not self.results:
291
- return "No measurements yet"
292
- df = pd.DataFrame(self.results)
293
- columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
294
- df = df[columns_order]
295
- return df.to_string(index=False)
296
- def save_results_to_template(self, template_path, output_path):
297
  try:
298
- if not self.results:
299
- return None, "No results to save"
300
-
301
- # Load the Excel template
302
- wb = openpyxl.load_workbook(template_path)
303
- ws = wb.active
304
-
305
- # Define row groups and phantom sizes
306
- row_groups = {
307
- "7mm": [41, 42],
308
- "6.5mm": [67, 68],
309
- "6mm": [93, 94],
310
- "5.5mm": [119, 120],
311
- "5mm": [145, 146],
312
- "4.5mm": [171, 172],
313
- "4mm": [197, 198],
314
- "3.5mm": [223, 224],
315
- "3mm": [249, 250],
316
- "2.5mm": [275, 276]
317
- }
318
 
319
- # Define columns for measurements
320
- columns = {
321
- 'Area (mm²)': "B",
322
- 'Mean': "C",
323
- 'StdDev': "D",
324
- 'Min': "E",
325
- 'Max': "F"
326
- }
 
 
 
 
327
 
328
- # Map results to phantom sizes (assuming results are ordered for each size)
329
- result_idx = 0
 
 
330
 
331
- for size, rows in row_groups.items():
332
- for row in rows:
333
- if result_idx >= len(self.results):
334
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
- result = self.results[result_idx]
 
 
 
337
 
338
- # Write each metric to the appropriate cell
339
- for metric, col in columns.items():
340
- cell = f"{col}{row}"
341
- value = result.get(metric, '')
342
- if value:
343
- try:
344
- ws[cell] = float(value)
345
- except ValueError:
346
- ws[cell] = value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
- result_idx += 1
 
 
 
 
 
 
 
 
349
 
350
- # Save the workbook
351
- wb.save(output_path)
 
 
 
 
 
352
 
353
- return output_path, "Results saved successfully in the template format"
 
 
 
 
 
 
 
 
 
 
354
 
 
355
  except Exception as e:
356
- print(f"Error saving results to template: {str(e)}")
357
- return None, f"Error saving results to template: {str(e)}"
358
-
359
- def add_blank_row(self, image):
360
- self.results.append({
361
- 'Area (mm²)': '',
362
- 'Mean': '',
363
- 'StdDev': '',
364
- 'Min': '',
365
- 'Max': '',
366
- 'Point': ''
367
- })
368
- return image, self.format_results()
369
-
370
- def add_zero_row(self, image):
371
- self.results.append({
372
- 'Area (mm²)': '0.000',
373
- 'Mean': '0.000',
374
- 'StdDev': '0.000',
375
- 'Min': '0.000',
376
- 'Max': '0.000',
377
- 'Point': '(0, 0)'
378
- })
379
- return image, self.format_results()
380
-
381
- def undo_last(self, image):
382
- if self.results:
383
- self.results.pop()
384
- if self.marks:
385
- self.marks.pop()
386
- return self.update_display(), self.format_results()
387
-
388
- def create_interface():
389
- print("Creating interface...")
390
- analyzer = DicomAnalyzer()
391
-
392
- with gr.Blocks(css="#image_display { outline: none; }") as interface:
393
- gr.Markdown("# DICOM Image Analyzer")
394
-
395
- with gr.Row():
396
- with gr.Column():
397
- file_input = gr.File(label="Upload DICOM file")
398
- template_input = gr.File(label="Upload Excel Template")
399
- diameter_slider = gr.Slider(
400
- minimum=1,
401
- maximum=20,
402
- value=9,
403
- step=1,
404
- label="ROI Diameter (pixels)"
405
- )
406
-
407
- with gr.Row():
408
- zoom_in_btn = gr.Button("Zoom In (+)")
409
- zoom_out_btn = gr.Button("Zoom Out (-)")
410
- reset_btn = gr.Button("Reset View")
411
-
412
- with gr.Column():
413
- image_display = gr.Image(
414
- label="DICOM Image",
415
- interactive=True,
416
- elem_id="image_display"
417
- )
418
-
419
- with gr.Row():
420
- blank_btn = gr.Button("Add Blank Row")
421
- zero_btn = gr.Button("Add Zero Row")
422
- undo_btn = gr.Button("Undo Last")
423
- save_btn = gr.Button("Save Results")
424
- save_template_btn = gr.Button("Save to Template")
425
-
426
- results_display = gr.Textbox(label="Results", interactive=False)
427
- file_output = gr.File(label="Download Results")
428
- key_press = gr.Textbox(visible=False, elem_id="key_press")
429
-
430
- gr.Markdown("""
431
- ### Controls:
432
- - Use arrow keys to pan when zoomed in
433
- - Click points to measure
434
- - Use Zoom In/Out buttons or Reset View to adjust zoom level
435
- - Upload template and use Save to Template for formatted results
436
- """)
437
-
438
- def update_diameter(x):
439
- analyzer.circle_diameter = float(x)
440
- print(f"Diameter updated to: {x}")
441
- return f"Diameter set to {x} pixels"
442
-
443
- def save_to_template(template_file):
444
- if template_file is None:
445
- return None, "No template file provided"
446
-
447
- output_path = "analysis_results_from_template.xlsx"
448
- return analyzer.save_results_to_template(template_file.name, output_path)
449
-
450
- # Event handlers
451
- file_input.change(
452
- fn=analyzer.load_dicom,
453
- inputs=file_input,
454
- outputs=[image_display, results_display]
455
- )
456
-
457
- image_display.select(
458
- fn=analyzer.analyze_roi,
459
- outputs=[image_display, results_display]
460
- )
461
-
462
- diameter_slider.change(
463
- fn=update_diameter,
464
- inputs=diameter_slider,
465
- outputs=gr.Textbox(label="Status")
466
- )
467
-
468
- zoom_in_btn.click(
469
- fn=analyzer.zoom_in,
470
- inputs=image_display,
471
- outputs=image_display,
472
- queue=False
473
- )
474
-
475
- zoom_out_btn.click(
476
- fn=analyzer.zoom_out,
477
- inputs=image_display,
478
- outputs=image_display,
479
- queue=False
480
- )
481
-
482
- reset_btn.click(
483
- fn=analyzer.reset_view,
484
- outputs=image_display
485
- )
486
-
487
- key_press.change(
488
- fn=analyzer.handle_keyboard,
489
- inputs=key_press,
490
- outputs=image_display
491
- )
492
-
493
- blank_btn.click(
494
- fn=analyzer.add_blank_row,
495
- inputs=image_display,
496
- outputs=[image_display, results_display]
497
- )
498
-
499
- zero_btn.click(
500
- fn=analyzer.add_zero_row,
501
- inputs=image_display,
502
- outputs=[image_display, results_display]
503
- )
504
-
505
- undo_btn.click(
506
- fn=analyzer.undo_last,
507
- inputs=image_display,
508
- outputs=[image_display, results_display]
509
- )
510
-
511
- save_btn.click(
512
- fn=analyzer.save_results,
513
- outputs=[file_output, results_display]
514
- )
515
-
516
- save_template_btn.click(
517
- fn=save_to_template,
518
- inputs=[template_input],
519
- outputs=[file_output, results_display]
520
- )
521
-
522
- js = """
523
- <script>
524
- document.addEventListener('keydown', function(e) {
525
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
526
- e.preventDefault();
527
- const keyPressElement = document.querySelector('#key_press textarea');
528
- if (keyPressElement) {
529
- keyPressElement.value = e.key;
530
- keyPressElement.dispatchEvent(new Event('input'));
531
- }
532
- }
533
- });
534
- </script>
535
- """
536
- gr.HTML(js)
537
-
538
- print("Interface created successfully")
539
- return interface
540
-
541
- if __name__ == "__main__":
542
- try:
543
- print("Starting application...")
544
- interface = create_interface()
545
- print("Launching interface...")
546
- interface.launch(
547
- server_name="0.0.0.0",
548
- server_port=7860,
549
- share=True,
550
- debug=True
551
- )
552
- except Exception as e:
553
- print(f"Error launching application: {str(e)}")
554
- logger.error(f"Error launching application: {str(e)}")
555
- logger.error(traceback.format_exc())
556
- raise e
 
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:
 
161
  except Exception as e:
162
  print(f"Error handling keyboard input: {str(e)}")
163
  return self.display_image
164
+
165
  def analyze_roi(self, evt: gr.SelectData):
166
  try:
167
  if self.current_image is None:
 
232
  except Exception as e:
233
  print(f"Error analyzing ROI: {str(e)}")
234
  return self.display_image, f"Error analyzing ROI: {str(e)}"
235
+ import gradio as gr
236
+ import cv2
237
+ import numpy as np
238
+ import pandas as pd
239
+ import pydicom
240
+ import io
241
+ from PIL import Image
242
+ import openpyxl
243
+ from openpyxl.utils import get_column_letter, column_index_from_string
244
+ import logging
245
+ import time
246
+ import traceback
247
+ from functools import wraps
248
+ import sys
249
 
250
+ print("Starting imports completed...")
 
 
 
251
 
252
+ # Set up logging
253
+ logging.basicConfig(
254
+ level=logging.DEBUG,
255
+ format='%(asctime)s - %(levelname)s - %(message)s',
256
+ handlers=[
257
+ logging.FileHandler('dicom_analyzer_debug.log'),
258
+ logging.StreamHandler(sys.stdout)
259
+ ]
260
+ )
261
 
262
+ logger = logging.getLogger(__name__)
 
263
 
264
+ def debug_decorator(func):
265
+ @wraps(func)
266
+ def wrapper(*args, **kwargs):
267
+ logger.debug(f"Entering {func.__name__}")
268
+ start_time = time.time()
269
+ try:
270
+ result = func(*args, **kwargs)
271
+ logger.debug(f"Function {func.__name__} completed successfully")
272
+ return result
273
+ except Exception as e:
274
+ logger.error(f"Error in {func.__name__}: {str(e)}")
275
+ logger.error(traceback.format_exc())
276
+ raise
277
+ finally:
278
+ end_time = time.time()
279
+ logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
280
+ return wrapper
281
 
282
+ class DicomAnalyzer:
283
+ def __init__(self):
284
+ self.results = []
285
+ self.circle_diameter = 9.0 # Changed to float for precise calculations
286
+ self.zoom_factor = 1.0
287
+ self.current_image = None
288
+ self.dicom_data = None
289
+ self.display_image = None
290
+ self.marks = [] # Store (x, y, diameter) for each mark
291
+ self.original_image = None
292
+ self.original_display = None
293
+ # Pan position
294
+ self.pan_x = 0
295
+ self.pan_y = 0
296
+ self.max_pan_x = 0
297
+ self.max_pan_y = 0
298
+ # Circle color in BGR
299
+ self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
300
+ print("DicomAnalyzer initialized...")
301
+
302
+ def load_dicom(self, file):
303
+ try:
304
+ if file is None:
305
+ return None, "No file uploaded"
306
+
307
+ if hasattr(file, 'name'):
308
+ dicom_data = pydicom.dcmread(file.name)
309
+ else:
310
+ dicom_data = pydicom.dcmread(file)
311
 
312
+ image = dicom_data.pixel_array.astype(np.float32)
313
+
314
+ # Store original pixel values before any scaling
315
+ self.original_image = image.copy()
316
+
317
+ # Apply DICOM scaling for display
318
+ rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
319
+ rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
320
+ image = (image * rescale_slope) + rescale_intercept
321
+
322
+ self.current_image = image
323
+ self.dicom_data = dicom_data
324
+
325
+ self.display_image = self.normalize_image(image)
326
+ self.original_display = self.display_image.copy()
327
+
328
+ # Reset view on new image
329
+ self.reset_view()
330
+ print("DICOM file loaded successfully")
331
+
332
+ return self.display_image, "DICOM file loaded successfully"
 
 
 
 
333
  except Exception as e:
334
+ print(f"Error loading DICOM file: {str(e)}")
335
+ return None, f"Error loading DICOM file: {str(e)}"
336
+
337
+ def normalize_image(self, image):
 
 
 
 
 
 
 
338
  try:
339
+ normalized = cv2.normalize(
340
+ image,
341
+ None,
342
+ alpha=0,
343
+ beta=255,
344
+ norm_type=cv2.NORM_MINMAX,
345
+ dtype=cv2.CV_8U
346
+ )
347
+ if len(normalized.shape) == 2:
348
+ normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
349
+ return normalized
350
+ except Exception as e:
351
+ print(f"Error normalizing image: {str(e)}")
352
+ return None
 
 
 
 
 
 
353
 
354
+ def reset_view(self):
355
+ self.zoom_factor = 1.0
356
+ self.pan_x = 0
357
+ self.pan_y = 0
358
+ if self.original_display is not None:
359
+ return self.update_display()
360
+ return None
361
+
362
+ def zoom_in(self, image):
363
+ print("Zooming in...")
364
+ self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
365
+ return self.update_display()
366
 
367
+ def zoom_out(self, image):
368
+ print("Zooming out...")
369
+ self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
370
+ return self.update_display()
371
 
372
+ def handle_keyboard(self, key):
373
+ try:
374
+ print(f"Handling key press: {key}")
375
+ pan_amount = int(5 * self.zoom_factor)
376
+
377
+ original_pan_x = self.pan_x
378
+ original_pan_y = self.pan_y
379
+
380
+ if key == 'ArrowLeft':
381
+ self.pan_x = max(0, self.pan_x - pan_amount)
382
+ elif key == 'ArrowRight':
383
+ self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount)
384
+ elif key == 'ArrowUp':
385
+ self.pan_y = max(0, self.pan_y - pan_amount)
386
+ elif key == 'ArrowDown':
387
+ self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
388
+
389
+ print(f"Pan X: {self.pan_x} (was {original_pan_x})")
390
+ print(f"Pan Y: {self.pan_y} (was {original_pan_y})")
391
+ print(f"Max Pan X: {self.max_pan_x}")
392
+ print(f"Max Pan Y: {self.max_pan_y}")
393
+
394
+ return self.update_display()
395
+ except Exception as e:
396
+ print(f"Error handling keyboard input: {str(e)}")
397
+ return self.display_image
398
 
399
+ def analyze_roi(self, evt: gr.SelectData):
400
+ try:
401
+ if self.current_image is None:
402
+ return None, "No image loaded"
403
 
404
+ clicked_x = evt.index[0]
405
+ clicked_y = evt.index[1]
406
+
407
+ x = clicked_x + self.pan_x
408
+ y = clicked_y + self.pan_y
409
+ if self.zoom_factor != 1.0:
410
+ x = x / self.zoom_factor
411
+ y = y / self.zoom_factor
412
+
413
+ x = int(round(x))
414
+ y = int(round(y))
415
+
416
+ height, width = self.original_image.shape[:2]
417
+
418
+ Y, X = np.ogrid[:height, :width]
419
+
420
+ radius = self.circle_diameter / 2.0
421
+ r_squared = radius * radius
422
+
423
+ dx = X - x
424
+ dy = Y - y
425
+ dist_squared = dx*dx + dy*dy
426
+
427
+ mask = np.zeros((height, width), dtype=bool)
428
+ mask[dist_squared <= r_squared] = True
429
+
430
+ roi_pixels = self.original_image[mask]
431
+
432
+ if len(roi_pixels) == 0:
433
+ return self.display_image, "Error: No pixels selected"
434
 
435
+ pixel_spacing = float(self.dicom_data.PixelSpacing[0])
436
+
437
+ n_pixels = np.sum(mask)
438
+ area = n_pixels * (pixel_spacing ** 2)
439
+
440
+ mean_value = np.mean(roi_pixels)
441
+ std_dev = np.std(roi_pixels, ddof=1)
442
+ min_val = np.min(roi_pixels)
443
+ max_val = np.max(roi_pixels)
444
 
445
+ rescale_slope = getattr(self.dicom_data, 'RescaleSlope', 1)
446
+ rescale_intercept = getattr(self.dicom_data, 'RescaleIntercept', 0)
447
+
448
+ mean_value = (mean_value * rescale_slope) + rescale_intercept
449
+ std_dev = std_dev * rescale_slope
450
+ min_val = (min_val * rescale_slope) + rescale_intercept
451
+ max_val = (max_val * rescale_slope) + rescale_intercept
452
 
453
+ result = {
454
+ 'Area (mm²)': f"{area:.3f}",
455
+ 'Mean': f"{mean_value:.3f}",
456
+ 'StdDev': f"{std_dev:.3f}",
457
+ 'Min': f"{min_val:.3f}",
458
+ 'Max': f"{max_val:.3f}",
459
+ 'Point': f"({x}, {y})"
460
+ }
461
+
462
+ self.results.append(result)
463
+ self.marks.append((x, y, self.circle_diameter))
464
 
465
+ return self.update_display(), self.format_results()
466
  except Exception as e:
467
+ print(f"Error analyzing ROI: {str(e)}")
468
+ return self.display_image, f"Error analyzing ROI: {str(e)}"