HeshamAI commited on
Commit
9df1ac5
·
verified ·
1 Parent(s): 2d893a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +74 -30
app.py CHANGED
@@ -172,6 +172,10 @@ class DicomAnalyzer:
172
  return self.update_display()
173
 
174
  def handle_keyboard(self, key):
 
 
 
 
175
  try:
176
  print(f"Handling key press: {key}")
177
  pan_amount = int(5 * self.zoom_factor)
@@ -191,6 +195,9 @@ class DicomAnalyzer:
191
  return self.display_image
192
 
193
  def update_display(self):
 
 
 
194
  try:
195
  if self.original_display is None:
196
  return None
@@ -221,6 +228,7 @@ class DicomAnalyzer:
221
  lineType=cv2.LINE_AA
222
  )
223
 
 
224
  num_points = 8
225
  for i in range(num_points):
226
  angle = 2 * np.pi * i / num_points
@@ -253,6 +261,10 @@ class DicomAnalyzer:
253
  return self.original_display
254
 
255
  def analyze_roi(self, evt: gr.SelectData):
 
 
 
 
256
  try:
257
  if self.current_image is None:
258
  return None, "No image loaded"
@@ -260,8 +272,11 @@ class DicomAnalyzer:
260
  clicked_x = evt.index[0]
261
  clicked_y = evt.index[1]
262
 
 
263
  x = clicked_x + self.pan_x
264
  y = clicked_y + self.pan_y
 
 
265
  if self.zoom_factor != 1.0:
266
  x = x / self.zoom_factor
267
  y = y / self.zoom_factor
@@ -321,13 +336,16 @@ class DicomAnalyzer:
321
  return self.display_image, f"Error analyzing ROI: {str(e)}"
322
 
323
  def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
 
 
 
324
  try:
325
  base_col = col_group[1] # Mean column
326
  std_col = col_group[2] # StdDev column
327
 
328
  row1, row2 = row_pair
329
 
330
- # SNR Formula for the first row with IFERROR
331
  formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
332
  formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
333
  cell1 = ws[f"{formula_col}{row1}"]
@@ -335,7 +353,7 @@ class DicomAnalyzer:
335
  cell1.font = red_font
336
  cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
337
 
338
- # CNR Formula for the second row with IFERROR
339
  formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
340
  cell2 = ws[f"{formula_col}{row2}"]
341
  cell2.value = formula2
@@ -347,6 +365,10 @@ class DicomAnalyzer:
347
  logger.error(f"Error adding formulas: {str(e)}")
348
 
349
  def save_formatted_results(self, output_path):
 
 
 
 
350
  try:
351
  if not self.results:
352
  return None, "No results to save"
@@ -386,11 +408,13 @@ class DicomAnalyzer:
386
  '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
387
  ]
388
 
 
389
  for i, size in enumerate(phantom_sizes):
390
  header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
391
  header_cell.font = red_font
392
  header_cell.alignment = center_alignment
393
 
 
394
  result_idx = 0
395
  current_col_group = 0
396
  current_row_pair = 0
@@ -402,16 +426,19 @@ class DicomAnalyzer:
402
  cols = column_groups[current_col_group]
403
  rows = row_pairs[current_row_pair]
404
 
 
405
  if result_idx < len(self.results):
406
  result = self.results[result_idx]
407
  self._write_result_to_cells(ws, result, cols, rows[0])
408
  result_idx += 1
409
 
 
410
  if result_idx < len(self.results):
411
  result = self.results[result_idx]
412
  self._write_result_to_cells(ws, result, cols, rows[1])
413
  result_idx += 1
414
 
 
415
  self.add_formulas_to_template(ws, rows, cols, red_font)
416
 
417
  current_col_group += 1
@@ -427,18 +454,18 @@ class DicomAnalyzer:
427
  if cell.value is not None:
428
  cell.alignment = center_alignment
429
 
430
- # Add space before averages tables
 
431
  current_row = 32
432
 
433
- # Headers for StdDev Averages
434
  stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages")
435
  stddev_header.font = red_font
436
  stddev_header.alignment = center_alignment
437
  current_row += 1
438
 
439
- # Calculate StdDev averages for each row pair (top row only)
440
  for i, size in enumerate(phantom_sizes):
441
- row_number = row_pairs[i][0] # First row
442
  stddev_values = []
443
 
444
  for cols in column_groups:
@@ -447,30 +474,29 @@ class DicomAnalyzer:
447
  if cell_value not in [0, None, '']: # Ignore zeros and empty
448
  stddev_values.append(float(cell_value))
449
 
 
 
 
450
  if stddev_values:
451
- avg = sum(stddev_values) / len(stddev_values)
452
- size_cell = ws.cell(row=current_row, column=1, value=size)
453
- size_cell.alignment = center_alignment
454
-
455
- avg_cell = ws.cell(row=current_row, column=2, value=avg)
456
  avg_cell.number_format = '0.000'
457
  avg_cell.alignment = center_alignment
458
  current_row += 1
459
 
460
- current_row += 2 # Space between tables
461
 
462
- # Headers for CNR Averages
463
  cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages")
464
  cnr_header.font = red_font
465
  cnr_header.alignment = center_alignment
466
  current_row += 1
467
 
468
- # Calculate CNR averages for each row pair (second row)
469
  for i, size in enumerate(phantom_sizes):
470
  row_number = row_pairs[i][1] # second row for CNR
471
  valid_cnr_refs = []
472
 
473
- # Build references only if Mean and StdDev are not all zero
474
  for cols in column_groups:
475
  formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
476
  mean_col = cols[1]
@@ -488,42 +514,44 @@ class DicomAnalyzer:
488
  except (ValueError, TypeError):
489
  continue
490
 
491
- # If not all zero, we consider the cell for average
492
  if not (mean1 == 0 and mean2 == 0 and std2 == 0):
493
- valid_cnr_refs.append(f"{formula_col}{row_number}")
 
494
 
495
  size_cell = ws.cell(row=current_row, column=1, value=size)
496
  size_cell.alignment = center_alignment
497
 
498
  if valid_cnr_refs:
499
- refs = ",".join(valid_cnr_refs)
500
- array_formula = f'=IFERROR(AVERAGE(IF(ISNUMBER({refs}),{refs})),"")'
 
 
501
 
502
  avg_cell = ws.cell(row=current_row, column=2)
503
- avg_cell.value = array_formula
504
  avg_cell.number_format = '0.000'
505
  avg_cell.alignment = center_alignment
506
 
507
  current_row += 1
508
 
509
- # Format the tables
510
  for row in range(32, current_row):
511
  for col in range(1, 3):
512
  cell = ws.cell(row=row, column=col)
513
  cell.alignment = center_alignment
514
 
515
  wb.save(output_path)
516
- return output_path, f"Results saved successfully ({result_idx} measurements)"
517
 
518
  except Exception as e:
519
  logger.error(f"Error saving formatted results: {str(e)}")
520
  return None, f"Error saving results: {str(e)}"
521
 
522
  def _write_result_to_cells(self, ws, result, cols, row):
523
- """Helper method to write a single result to worksheet cells"""
524
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
525
 
526
- # Map the headers to the result keys
527
  value_mapping = {
528
  'Area': 'Area (mm²)',
529
  'Mean': 'Mean',
@@ -534,10 +562,13 @@ class DicomAnalyzer:
534
 
535
  for i, (header, key) in enumerate(value_mapping.items()):
536
  cell = ws[f"{cols[i]}{row}"]
537
- cell.value = float(result[key]) if result[key] != '' else ''
 
 
538
  cell.alignment = center_alignment
539
 
540
  def format_results(self):
 
541
  if not self.results:
542
  return "No measurements yet"
543
  df = pd.DataFrame(self.results)
@@ -619,18 +650,21 @@ def create_interface():
619
 
620
  gr.Markdown("""
621
  ### Controls:
622
- - Use arrow keys to pan when zoomed in
623
- - Click points to measure
624
- - Use Zoom In/Out buttons or Reset View to adjust zoom level
625
- - Use Reset All to clear all measurements
626
- - Save Formatted Results will create Excel file with formulas
 
627
  """)
628
 
 
629
  def update_diameter(x):
630
  analyzer.circle_diameter = float(x)
631
  print(f"Diameter updated to: {x}")
632
  return f"Diameter set to {x} pixels"
633
 
 
634
  def save_formatted():
635
  output_path = "analysis_results_formatted.xlsx"
636
  return analyzer.save_formatted_results(output_path)
@@ -712,6 +746,8 @@ def create_interface():
712
  outputs=[file_output, results_display]
713
  )
714
 
 
 
715
  js = """
716
  <script>
717
  document.addEventListener('keydown', function(e) {
@@ -719,8 +755,15 @@ def create_interface():
719
  e.preventDefault();
720
  const keyPressElement = document.querySelector('#key_press textarea');
721
  if (keyPressElement) {
 
722
  keyPressElement.value = e.key;
723
  keyPressElement.dispatchEvent(new Event('input'));
 
 
 
 
 
 
724
  }
725
  }
726
  });
@@ -731,6 +774,7 @@ def create_interface():
731
  print("Interface created successfully")
732
  return interface
733
 
 
734
  if __name__ == "__main__":
735
  try:
736
  print("Starting application...")
 
172
  return self.update_display()
173
 
174
  def handle_keyboard(self, key):
175
+ """
176
+ Called when the hidden textbox 'key_press' changes value.
177
+ key is expected to be 'ArrowLeft','ArrowRight','ArrowUp','ArrowDown'.
178
+ """
179
  try:
180
  print(f"Handling key press: {key}")
181
  pan_amount = int(5 * self.zoom_factor)
 
195
  return self.display_image
196
 
197
  def update_display(self):
198
+ """
199
+ Updates the displayed image according to the current zoom/pan and draws the ROI circles.
200
+ """
201
  try:
202
  if self.original_display is None:
203
  return None
 
228
  lineType=cv2.LINE_AA
229
  )
230
 
231
+ # Draw small points along the circle
232
  num_points = 8
233
  for i in range(num_points):
234
  angle = 2 * np.pi * i / num_points
 
261
  return self.original_display
262
 
263
  def analyze_roi(self, evt: gr.SelectData):
264
+ """
265
+ Called when user clicks on the displayed image.
266
+ Extracts the ROI of diameter circle_diameter and computes stats.
267
+ """
268
  try:
269
  if self.current_image is None:
270
  return None, "No image loaded"
 
272
  clicked_x = evt.index[0]
273
  clicked_y = evt.index[1]
274
 
275
+ # Adjust by current pan
276
  x = clicked_x + self.pan_x
277
  y = clicked_y + self.pan_y
278
+
279
+ # Account for zoom
280
  if self.zoom_factor != 1.0:
281
  x = x / self.zoom_factor
282
  y = y / self.zoom_factor
 
336
  return self.display_image, f"Error analyzing ROI: {str(e)}"
337
 
338
  def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
339
+ """
340
+ Inserts SNR (first row) and CNR (second row) formulas with IFERROR.
341
+ """
342
  try:
343
  base_col = col_group[1] # Mean column
344
  std_col = col_group[2] # StdDev column
345
 
346
  row1, row2 = row_pair
347
 
348
+ # SNR Formula
349
  formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
350
  formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
351
  cell1 = ws[f"{formula_col}{row1}"]
 
353
  cell1.font = red_font
354
  cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
355
 
356
+ # CNR Formula
357
  formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
358
  cell2 = ws[f"{formula_col}{row2}"]
359
  cell2.value = formula2
 
365
  logger.error(f"Error adding formulas: {str(e)}")
366
 
367
  def save_formatted_results(self, output_path):
368
+ """
369
+ Creates an Excel file with results in a formatted table,
370
+ adding SNR/CNR formulas and averaging blocks for StdDev and CNR.
371
+ """
372
  try:
373
  if not self.results:
374
  return None, "No results to save"
 
408
  '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
409
  ]
410
 
411
+ # Insert Phantom size labels
412
  for i, size in enumerate(phantom_sizes):
413
  header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
414
  header_cell.font = red_font
415
  header_cell.alignment = center_alignment
416
 
417
+ # Fill data from self.results
418
  result_idx = 0
419
  current_col_group = 0
420
  current_row_pair = 0
 
426
  cols = column_groups[current_col_group]
427
  rows = row_pairs[current_row_pair]
428
 
429
+ # First row in pair
430
  if result_idx < len(self.results):
431
  result = self.results[result_idx]
432
  self._write_result_to_cells(ws, result, cols, rows[0])
433
  result_idx += 1
434
 
435
+ # Second row in pair
436
  if result_idx < len(self.results):
437
  result = self.results[result_idx]
438
  self._write_result_to_cells(ws, result, cols, rows[1])
439
  result_idx += 1
440
 
441
+ # Insert SNR/CNR formulas for these two rows
442
  self.add_formulas_to_template(ws, rows, cols, red_font)
443
 
444
  current_col_group += 1
 
454
  if cell.value is not None:
455
  cell.alignment = center_alignment
456
 
457
+ # ---- Create tables for StdDev Averages and CNR Averages ----
458
+
459
  current_row = 32
460
 
461
+ # 1) StdDev Averages
462
  stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages")
463
  stddev_header.font = red_font
464
  stddev_header.alignment = center_alignment
465
  current_row += 1
466
 
 
467
  for i, size in enumerate(phantom_sizes):
468
+ row_number = row_pairs[i][0] # First row only for std
469
  stddev_values = []
470
 
471
  for cols in column_groups:
 
474
  if cell_value not in [0, None, '']: # Ignore zeros and empty
475
  stddev_values.append(float(cell_value))
476
 
477
+ size_cell = ws.cell(row=current_row, column=1, value=size)
478
+ size_cell.alignment = center_alignment
479
+
480
  if stddev_values:
481
+ avg_stddev = sum(stddev_values) / len(stddev_values)
482
+ avg_cell = ws.cell(row=current_row, column=2, value=avg_stddev)
 
 
 
483
  avg_cell.number_format = '0.000'
484
  avg_cell.alignment = center_alignment
485
  current_row += 1
486
 
487
+ current_row += 2 # Space between the two tables
488
 
489
+ # 2) CNR Averages
490
  cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages")
491
  cnr_header.font = red_font
492
  cnr_header.alignment = center_alignment
493
  current_row += 1
494
 
 
495
  for i, size in enumerate(phantom_sizes):
496
  row_number = row_pairs[i][1] # second row for CNR
497
  valid_cnr_refs = []
498
 
499
+ # We'll check each column group to see if there's a (Mean1, Mean2, Std2) not all zero
500
  for cols in column_groups:
501
  formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
502
  mean_col = cols[1]
 
514
  except (ValueError, TypeError):
515
  continue
516
 
517
+ # If not all zero => there's something measured
518
  if not (mean1 == 0 and mean2 == 0 and std2 == 0):
519
+ cell_ref = f"{formula_col}{row_number}"
520
+ valid_cnr_refs.append(cell_ref)
521
 
522
  size_cell = ws.cell(row=current_row, column=1, value=size)
523
  size_cell.alignment = center_alignment
524
 
525
  if valid_cnr_refs:
526
+ # Use AVERAGEIF to ignore zero or empty cells
527
+ # Must wrap references like (A1,A2,A3) not A1,A2,A3 alone
528
+ formula_range = f"({','.join(valid_cnr_refs)})"
529
+ cnr_formula = f'=IFERROR(AVERAGEIF({formula_range}, "<>0"), "")'
530
 
531
  avg_cell = ws.cell(row=current_row, column=2)
532
+ avg_cell.value = cnr_formula
533
  avg_cell.number_format = '0.000'
534
  avg_cell.alignment = center_alignment
535
 
536
  current_row += 1
537
 
538
+ # Final alignment in the last table
539
  for row in range(32, current_row):
540
  for col in range(1, 3):
541
  cell = ws.cell(row=row, column=col)
542
  cell.alignment = center_alignment
543
 
544
  wb.save(output_path)
545
+ return output_path, f"Results saved successfully ({len(self.results)} measurements)"
546
 
547
  except Exception as e:
548
  logger.error(f"Error saving formatted results: {str(e)}")
549
  return None, f"Error saving results: {str(e)}"
550
 
551
  def _write_result_to_cells(self, ws, result, cols, row):
552
+ """Helper method to write a single result (dict) into a row of cells."""
553
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
554
 
 
555
  value_mapping = {
556
  'Area': 'Area (mm²)',
557
  'Mean': 'Mean',
 
562
 
563
  for i, (header, key) in enumerate(value_mapping.items()):
564
  cell = ws[f"{cols[i]}{row}"]
565
+ val = result[key]
566
+ # Handle empty strings
567
+ cell.value = float(val) if val not in ['', None] else ''
568
  cell.alignment = center_alignment
569
 
570
  def format_results(self):
571
+ """Returns a string representation of self.results for display."""
572
  if not self.results:
573
  return "No measurements yet"
574
  df = pd.DataFrame(self.results)
 
650
 
651
  gr.Markdown("""
652
  ### Controls:
653
+ - Use arrow keys to pan when zoomed in (multiple times).
654
+ - Click points to measure ROI.
655
+ - Use Zoom In/Out buttons or Reset View to adjust zoom level.
656
+ - Use Reset All to clear all measurements.
657
+ - "Save Results" => Basic Excel with raw data.
658
+ - "Save Formatted Results" => Excel with advanced formatting & formulas.
659
  """)
660
 
661
+ # Update diameter callback
662
  def update_diameter(x):
663
  analyzer.circle_diameter = float(x)
664
  print(f"Diameter updated to: {x}")
665
  return f"Diameter set to {x} pixels"
666
 
667
+ # Save formatted callback
668
  def save_formatted():
669
  output_path = "analysis_results_formatted.xlsx"
670
  return analyzer.save_formatted_results(output_path)
 
746
  outputs=[file_output, results_display]
747
  )
748
 
749
+ # JavaScript snippet to allow multiple arrow key presses
750
+ # by resetting the value in key_press.
751
  js = """
752
  <script>
753
  document.addEventListener('keydown', function(e) {
 
755
  e.preventDefault();
756
  const keyPressElement = document.querySelector('#key_press textarea');
757
  if (keyPressElement) {
758
+ // Set the value to the pressed key and dispatch
759
  keyPressElement.value = e.key;
760
  keyPressElement.dispatchEvent(new Event('input'));
761
+
762
+ // Clear value after short delay so next press is recognized
763
+ setTimeout(() => {
764
+ keyPressElement.value = '';
765
+ keyPressElement.dispatchEvent(new Event('input'));
766
+ }, 100);
767
  }
768
  }
769
  });
 
774
  print("Interface created successfully")
775
  return interface
776
 
777
+
778
  if __name__ == "__main__":
779
  try:
780
  print("Starting application...")