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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +319 -212
app.py CHANGED
@@ -232,237 +232,344 @@ class DicomAnalyzer:
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)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ def update_display(self):
236
+ try:
237
+ if self.original_display is None:
238
+ return None
 
 
 
 
 
 
 
 
 
 
239
 
240
+ height, width = self.original_display.shape[:2]
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
264
+ point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
265
+ point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
266
+ cv2.circle(zoomed_bgr,
267
+ (point_x, point_y),
268
+ 1,
269
+ self.CIRCLE_COLOR,
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)
283
+ ]
284
+
285
+ return visible
286
  except Exception as e:
287
+ print(f"Error updating display: {str(e)}")
288
+ return self.original_display
289
+
290
+ def format_results(self):
291
+ if not self.results:
292
+ return "No measurements yet"
293
+ df = pd.DataFrame(self.results)
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
+ df = pd.DataFrame(self.results)
304
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
305
+ df = df[columns_order]
306
 
307
+ temp_file = "analysis_results.xlsx"
308
+ df.to_excel(temp_file, index=False)
 
 
309
 
310
+ return temp_file, "Results saved successfully"
 
 
 
 
 
 
 
 
 
 
311
  except Exception as e:
312
+ print(f"Error saving results: {str(e)}")
313
+ return None, f"Error saving results: {str(e)}"
314
 
315
+ def save_results_to_template(self, template_path, output_path):
316
  try:
317
+ if not self.results:
318
+ return None, "No results to save"
319
+
320
+ # Load the Excel template
321
+ wb = openpyxl.load_workbook(template_path)
322
+ ws = wb.active
323
+
324
+ # Define row groups and phantom sizes
325
+ row_groups = {
326
+ "7mm": [41, 42],
327
+ "6.5mm": [67, 68],
328
+ "6mm": [93, 94],
329
+ "5.5mm": [119, 120],
330
+ "5mm": [145, 146],
331
+ "4.5mm": [171, 172],
332
+ "4mm": [197, 198],
333
+ "3.5mm": [223, 224],
334
+ "3mm": [249, 250],
335
+ "2.5mm": [275, 276]
336
+ }
 
 
337
 
338
+ # Define columns for measurements
339
+ columns = {
340
+ 'Area (mm²)': "B",
341
+ 'Mean': "C",
342
+ 'StdDev': "D",
343
+ 'Min': "E",
344
+ 'Max': "F"
345
+ }
346
 
347
+ # Map results to phantom sizes (assuming results are ordered for each size)
348
+ result_idx = 0
 
 
349
 
350
+ for size, rows in row_groups.items():
351
+ for row in rows:
352
+ if result_idx >= len(self.results):
353
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
+ result = self.results[result_idx]
 
 
 
356
 
357
+ # Write each metric to the appropriate cell
358
+ for metric, col in columns.items():
359
+ cell = f"{col}{row}"
360
+ value = result.get(metric, '')
361
+ if value:
362
+ try:
363
+ ws[cell] = float(value)
364
+ except ValueError:
365
+ ws[cell] = value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
+ result_idx += 1
 
 
 
 
 
 
 
 
368
 
369
+ # Save the workbook
370
+ wb.save(output_path)
 
 
 
 
 
371
 
372
+ return output_path, "Results saved successfully in the template format"
 
 
 
 
 
 
 
 
 
 
373
 
 
374
  except Exception as e:
375
+ print(f"Error saving results to template: {str(e)}")
376
+ return None, f"Error saving results to template: {str(e)}"
377
+
378
+ def add_blank_row(self, image):
379
+ self.results.append({
380
+ 'Area (mm²)': '',
381
+ 'Mean': '',
382
+ 'StdDev': '',
383
+ 'Min': '',
384
+ 'Max': '',
385
+ 'Point': ''
386
+ })
387
+ return image, self.format_results()
388
+
389
+ def add_zero_row(self, image):
390
+ self.results.append({
391
+ 'Area (mm²)': '0.000',
392
+ 'Mean': '0.000',
393
+ 'StdDev': '0.000',
394
+ 'Min': '0.000',
395
+ 'Max': '0.000',
396
+ 'Point': '(0, 0)'
397
+ })
398
+ return image, self.format_results()
399
+
400
+ def undo_last(self, image):
401
+ if self.results:
402
+ self.results.pop()
403
+ if self.marks:
404
+ self.marks.pop()
405
+ return self.update_display(), self.format_results()
406
+
407
+ def create_interface():
408
+ print("Creating interface...")
409
+ analyzer = DicomAnalyzer()
410
+
411
+ with gr.Blocks(css="#image_display { outline: none; }") as interface:
412
+ gr.Markdown("# DICOM Image Analyzer")
413
+
414
+ with gr.Row():
415
+ with gr.Column():
416
+ file_input = gr.File(label="Upload DICOM file")
417
+ template_input = gr.File(label="Upload Excel Template")
418
+ diameter_slider = gr.Slider(
419
+ minimum=1,
420
+ maximum=20,
421
+ value=9,
422
+ step=1,
423
+ label="ROI Diameter (pixels)"
424
+ )
425
+
426
+ with gr.Row():
427
+ zoom_in_btn = gr.Button("Zoom In (+)")
428
+ zoom_out_btn = gr.Button("Zoom Out (-)")
429
+ reset_btn = gr.Button("Reset View")
430
+
431
+ with gr.Column():
432
+ image_display = gr.Image(
433
+ label="DICOM Image",
434
+ interactive=True,
435
+ elem_id="image_display"
436
+ )
437
+
438
+ with gr.Row():
439
+ blank_btn = gr.Button("Add Blank Row")
440
+ zero_btn = gr.Button("Add Zero Row")
441
+ undo_btn = gr.Button("Undo Last")
442
+ save_btn = gr.Button("Save Results")
443
+ save_template_btn = gr.Button("Save to Template")
444
+
445
+ results_display = gr.Textbox(label="Results", interactive=False)
446
+ file_output = gr.File(label="Download Results")
447
+ key_press = gr.Textbox(visible=False, elem_id="key_press")
448
+
449
+ gr.Markdown("""
450
+ ### Controls:
451
+ - Use arrow keys to pan when zoomed in
452
+ - Click points to measure
453
+ - Use Zoom In/Out buttons or Reset View to adjust zoom level
454
+ - Upload template and use Save to Template for formatted results
455
+ """)
456
+
457
+ def update_diameter(x):
458
+ analyzer.circle_diameter = float(x)
459
+ print(f"Diameter updated to: {x}")
460
+ return f"Diameter set to {x} pixels"
461
+
462
+ def save_to_template(template_file):
463
+ if template_file is None:
464
+ return None, "No template file provided"
465
+
466
+ output_path = "analysis_results_from_template.xlsx"
467
+ return analyzer.save_results_to_template(template_file.name, output_path)
468
+
469
+ # Event handlers
470
+ file_input.change(
471
+ fn=analyzer.load_dicom,
472
+ inputs=file_input,
473
+ outputs=[image_display, results_display]
474
+ )
475
+
476
+ image_display.select(
477
+ fn=analyzer.analyze_roi,
478
+ outputs=[image_display, results_display]
479
+ )
480
+
481
+ diameter_slider.change(
482
+ fn=update_diameter,
483
+ inputs=diameter_slider,
484
+ outputs=gr.Textbox(label="Status")
485
+ )
486
+
487
+ zoom_in_btn.click(
488
+ fn=analyzer.zoom_in,
489
+ inputs=image_display,
490
+ outputs=image_display,
491
+ queue=False
492
+ )
493
+
494
+ zoom_out_btn.click(
495
+ fn=analyzer.zoom_out,
496
+ inputs=image_display,
497
+ outputs=image_display,
498
+ queue=False
499
+ )
500
+
501
+ reset_btn.click(
502
+ fn=analyzer.reset_view,
503
+ outputs=image_display
504
+ )
505
+
506
+ key_press.change(
507
+ fn=analyzer.handle_keyboard,
508
+ inputs=key_press,
509
+ outputs=image_display
510
+ )
511
+
512
+ blank_btn.click(
513
+ fn=analyzer.add_blank_row,
514
+ inputs=image_display,
515
+ outputs=[image_display, results_display]
516
+ )
517
+
518
+ zero_btn.click(
519
+ fn=analyzer.add_zero_row,
520
+ inputs=image_display,
521
+ outputs=[image_display, results_display]
522
+ )
523
+
524
+ undo_btn.click(
525
+ fn=analyzer.undo_last,
526
+ inputs=image_display,
527
+ outputs=[image_display, results_display]
528
+ )
529
+
530
+ save_btn.click(
531
+ fn=analyzer.save_results,
532
+ outputs=[file_output, results_display]
533
+ )
534
+
535
+ save_template_btn.click(
536
+ fn=save_to_template,
537
+ inputs=[template_input],
538
+ outputs=[file_output, results_display]
539
+ )
540
+
541
+ js = """
542
+ <script>
543
+ document.addEventListener('keydown', function(e) {
544
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
545
+ e.preventDefault();
546
+ const keyPressElement = document.querySelector('#key_press textarea');
547
+ if (keyPressElement) {
548
+ keyPressElement.value = e.key;
549
+ keyPressElement.dispatchEvent(new Event('input'));
550
+ }
551
+ }
552
+ });
553
+ </script>
554
+ """
555
+ gr.HTML(js)
556
+
557
+ print("Interface created successfully")
558
+ return interface
559
+
560
+ if __name__ == "__main__":
561
+ try:
562
+ print("Starting application...")
563
+ interface = create_interface()
564
+ print("Launching interface...")
565
+ interface.launch(
566
+ server_name="0.0.0.0",
567
+ server_port=7860,
568
+ share=True,
569
+ debug=True
570
+ )
571
+ except Exception as e:
572
+ print(f"Error launching application: {str(e)}")
573
+ logger.error(f"Error launching application: {str(e)}")
574
+ logger.error(traceback.format_exc())
575
+ raise e