HeshamAI commited on
Commit
9c15806
·
verified ·
1 Parent(s): 6de46a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -72
app.py CHANGED
@@ -61,7 +61,10 @@ class DicomAnalyzer:
61
  self.pan_y = 0
62
  self.max_pan_x = 0
63
  self.max_pan_y = 0
64
- self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
 
 
 
65
  print("DicomAnalyzer initialized...")
66
 
67
  def save_results(self):
@@ -173,12 +176,12 @@ class DicomAnalyzer:
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)
 
182
 
183
  if key == 'ArrowLeft':
184
  self.pan_x = max(0, self.pan_x - pan_amount)
@@ -196,7 +199,8 @@ class DicomAnalyzer:
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:
@@ -217,8 +221,9 @@ class DicomAnalyzer:
217
  for x, y, diameter in self.marks:
218
  zoomed_x = int(x * self.zoom_factor)
219
  zoomed_y = int(y * self.zoom_factor)
220
- zoomed_radius = int((diameter/2.0) * self.zoom_factor)
221
 
 
222
  cv2.circle(
223
  zoomed_bgr,
224
  (zoomed_x, zoomed_y),
@@ -228,7 +233,7 @@ class DicomAnalyzer:
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
@@ -238,7 +243,7 @@ class DicomAnalyzer:
238
  zoomed_bgr,
239
  (point_x, point_y),
240
  1,
241
- self.CIRCLE_COLOR,
242
  -1,
243
  lineType=cv2.LINE_AA
244
  )
@@ -262,8 +267,7 @@ class DicomAnalyzer:
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:
@@ -272,11 +276,11 @@ class DicomAnalyzer:
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
@@ -291,7 +295,7 @@ class DicomAnalyzer:
291
 
292
  dx = X - x
293
  dy = Y - y
294
- dist_squared = dx*dx + dy*dy
295
 
296
  mask = np.zeros((height, width), dtype=bool)
297
  mask[dist_squared <= r_squared] = True
@@ -345,7 +349,7 @@ class DicomAnalyzer:
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,7 +357,7 @@ class DicomAnalyzer:
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
@@ -367,10 +371,7 @@ class DicomAnalyzer:
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
- NOTE: CNR Averages uses AVERAGE() and we skip any cell references where
373
- Mean1, Mean2, StdDev2 are all zero. So we avoid AVERAGEIF complexities.
374
  """
375
  try:
376
  if not self.results:
@@ -381,7 +382,7 @@ class DicomAnalyzer:
381
  red_font = openpyxl.styles.Font(color="FF0000")
382
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
383
 
384
- # Define headers
385
  headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
386
 
387
  column_groups = [
@@ -394,7 +395,6 @@ class DicomAnalyzer:
394
  ('BV', 'BW', 'BX', 'BY', 'BZ')
395
  ]
396
 
397
- # Add headers for each column group
398
  for cols in column_groups:
399
  for i, header in enumerate(headers):
400
  cell = ws[f"{cols[i]}1"]
@@ -411,13 +411,13 @@ class DicomAnalyzer:
411
  '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
412
  ]
413
 
414
- # Insert Phantom size labels
415
  for i, size in enumerate(phantom_sizes):
416
  header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
417
  header_cell.font = red_font
418
  header_cell.alignment = center_alignment
419
 
420
- # Fill data from self.results
421
  result_idx = 0
422
  current_col_group = 0
423
  current_row_pair = 0
@@ -429,19 +429,19 @@ class DicomAnalyzer:
429
  cols = column_groups[current_col_group]
430
  rows = row_pairs[current_row_pair]
431
 
432
- # First row in pair
433
  if result_idx < len(self.results):
434
  result = self.results[result_idx]
435
  self._write_result_to_cells(ws, result, cols, rows[0])
436
  result_idx += 1
437
 
438
- # Second row in pair
439
  if result_idx < len(self.results):
440
  result = self.results[result_idx]
441
  self._write_result_to_cells(ws, result, cols, rows[1])
442
  result_idx += 1
443
 
444
- # Insert SNR/CNR formulas for these two rows
445
  self.add_formulas_to_template(ws, rows, cols, red_font)
446
 
447
  current_col_group += 1
@@ -457,23 +457,23 @@ class DicomAnalyzer:
457
  if cell.value is not None:
458
  cell.alignment = center_alignment
459
 
460
- # ---- Create tables for StdDev Averages and CNR Averages ----
461
  current_row = 32
462
 
463
- # 1) StdDev Averages
464
  stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages")
465
  stddev_header.font = red_font
466
  stddev_header.alignment = center_alignment
467
  current_row += 1
468
 
469
  for i, size in enumerate(phantom_sizes):
470
- row_number = row_pairs[i][0] # First row only for std
471
  stddev_values = []
472
 
473
  for cols in column_groups:
474
- stddev_col = cols[2] # StdDev column
475
  cell_value = ws[f"{stddev_col}{row_number}"].value
476
- if cell_value not in [0, None, '']: # Ignore zeros and empty
477
  stddev_values.append(float(cell_value))
478
 
479
  size_cell = ws.cell(row=current_row, column=1, value=size)
@@ -486,15 +486,14 @@ class DicomAnalyzer:
486
  avg_cell.alignment = center_alignment
487
  current_row += 1
488
 
489
- current_row += 2 # Space between the two tables
490
 
491
- # 2) CNR Averages
492
  cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages")
493
  cnr_header.font = red_font
494
  cnr_header.alignment = center_alignment
495
  current_row += 1
496
 
497
- # حساب متوسط CNR للصف الثاني في كل pair باستخدام AVERAGE فقط
498
  for i, size in enumerate(phantom_sizes):
499
  row_number = row_pairs[i][1]
500
  cnr_cells = []
@@ -503,7 +502,7 @@ class DicomAnalyzer:
503
  formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
504
  cnr_cell_ref = f"{formula_col}{row_number}"
505
 
506
- # نقرأ Mean1,Mean2,Std2 للتحقق
507
  mean_col = cols[1]
508
  std_col = cols[2]
509
 
@@ -518,7 +517,7 @@ class DicomAnalyzer:
518
  except:
519
  mean1_val, mean2_val, std2_val = 0, 0, 0
520
 
521
- # لو كلها صفر => لا نضيف الخلية
522
  if not (mean1_val == 0 and mean2_val == 0 and std2_val == 0):
523
  cnr_cells.append(cnr_cell_ref)
524
 
@@ -526,7 +525,7 @@ class DicomAnalyzer:
526
  size_cell.alignment = center_alignment
527
 
528
  if cnr_cells:
529
- # نبني صيغة Excel: =IFERROR(AVERAGE(G3,M3,...),"")
530
  average_formula = f'=IFERROR(AVERAGE({",".join(cnr_cells)}), "")'
531
 
532
  avg_cell = ws.cell(row=current_row, column=2)
@@ -536,7 +535,7 @@ class DicomAnalyzer:
536
 
537
  current_row += 1
538
 
539
- # تنسيق العناوين الأخيرة
540
  for row in range(32, current_row):
541
  for col in range(1, 3):
542
  cell = ws.cell(row=row, column=col)
@@ -550,7 +549,7 @@ class DicomAnalyzer:
550
  return None, f"Error saving results: {str(e)}"
551
 
552
  def _write_result_to_cells(self, ws, result, cols, row):
553
- """Helper method to write a single result (dict) into a row of cells."""
554
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
555
 
556
  value_mapping = {
@@ -564,7 +563,6 @@ class DicomAnalyzer:
564
  for i, (header, key) in enumerate(value_mapping.items()):
565
  cell = ws[f"{cols[i]}{row}"]
566
  val = result[key]
567
- # Handle empty strings
568
  cell.value = float(val) if val not in ['', None] else ''
569
  cell.alignment = center_alignment
570
 
@@ -577,18 +575,10 @@ class DicomAnalyzer:
577
  df = df[columns_order]
578
  return df.to_string(index=False)
579
 
580
- def add_blank_row(self, image):
581
- self.results.append({
582
- 'Area (mm²)': '',
583
- 'Mean': '',
584
- 'StdDev': '',
585
- 'Min': '',
586
- 'Max': '',
587
- 'Point': ''
588
- })
589
- return image, self.format_results()
590
-
591
  def add_zero_row(self, image):
 
 
 
592
  self.results.append({
593
  'Area (mm²)': '0.000',
594
  'Mean': '0.000',
@@ -601,7 +591,7 @@ class DicomAnalyzer:
601
 
602
  def add_two_zero_rows(self, image):
603
  """
604
- Adds two consecutive rows of zeros.
605
  """
606
  for _ in range(2):
607
  self.results.append({
@@ -653,10 +643,10 @@ def create_interface():
653
  elem_id="image_display"
654
  )
655
 
 
656
  with gr.Row():
657
- blank_btn = gr.Button("Add Blank Row")
658
  zero_btn = gr.Button("Add Zero Row")
659
- zero2_btn = gr.Button("Add Two Zero Rows") # <-- New button
660
  undo_btn = gr.Button("Undo Last")
661
  save_btn = gr.Button("Save Results")
662
  save_formatted_btn = gr.Button("Save Formatted Results")
@@ -667,26 +657,24 @@ def create_interface():
667
 
668
  gr.Markdown("""
669
  ### Controls:
670
- - Use arrow keys to pan when zoomed in (multiple times).
671
  - Click points to measure ROI.
672
  - Use Zoom In/Out buttons or Reset View to adjust zoom level.
673
  - Use Reset All to clear all measurements.
674
- - "Save Results" => Basic Excel with raw data.
675
- - "Save Formatted Results" => Excel with advanced formatting & formulas.
676
  """)
677
 
678
- # Update diameter callback
679
  def update_diameter(x):
680
  analyzer.circle_diameter = float(x)
681
  print(f"Diameter updated to: {x}")
682
  return f"Diameter set to {x} pixels"
683
 
684
- # Save formatted callback
685
  def save_formatted():
686
  output_path = "analysis_results_formatted.xlsx"
687
  return analyzer.save_formatted_results(output_path)
688
 
689
- # Event handlers
690
  file_input.change(
691
  fn=analyzer.load_dicom,
692
  inputs=file_input,
@@ -735,19 +723,13 @@ def create_interface():
735
  outputs=image_display
736
  )
737
 
738
- blank_btn.click(
739
- fn=analyzer.add_blank_row,
740
- inputs=image_display,
741
- outputs=[image_display, results_display]
742
- )
743
-
744
  zero_btn.click(
745
  fn=analyzer.add_zero_row,
746
  inputs=image_display,
747
  outputs=[image_display, results_display]
748
  )
749
-
750
- # زر إضافة صفين صفر
751
  zero2_btn.click(
752
  fn=analyzer.add_two_zero_rows,
753
  inputs=image_display,
@@ -771,7 +753,6 @@ def create_interface():
771
  )
772
 
773
  # JavaScript snippet to allow multiple arrow key presses
774
- # by resetting the value in key_press.
775
  js = """
776
  <script>
777
  document.addEventListener('keydown', function(e) {
@@ -779,11 +760,8 @@ def create_interface():
779
  e.preventDefault();
780
  const keyPressElement = document.querySelector('#key_press textarea');
781
  if (keyPressElement) {
782
- // Set the value to the pressed key and dispatch
783
  keyPressElement.value = e.key;
784
  keyPressElement.dispatchEvent(new Event('input'));
785
-
786
- // Clear value after short delay so next press is recognized
787
  setTimeout(() => {
788
  keyPressElement.value = '';
789
  keyPressElement.dispatchEvent(new Event('input'));
 
61
  self.pan_y = 0
62
  self.max_pan_x = 0
63
  self.max_pan_y = 0
64
+ # Main circle color remains yellow:
65
+ self.CIRCLE_COLOR = (0, 255, 255) # BGR format
66
+ # Small circles inside the main circle will be black:
67
+ self.SMALL_CIRCLES_COLOR = (0, 0, 0) # BGR black
68
  print("DicomAnalyzer initialized...")
69
 
70
  def save_results(self):
 
176
 
177
  def handle_keyboard(self, key):
178
  """
179
+ Handle arrow keys. Pan movement is increased to be more noticeable.
 
180
  """
181
  try:
182
  print(f"Handling key press: {key}")
183
+ # Increase pan step for bigger movement:
184
+ pan_amount = int(10 * self.zoom_factor)
185
 
186
  if key == 'ArrowLeft':
187
  self.pan_x = max(0, self.pan_x - pan_amount)
 
199
 
200
  def update_display(self):
201
  """
202
+ Updates the displayed image according to the current zoom/pan and draws the circles.
203
+ The big circle is in CIRCLE_COLOR, and the small circles inside are in SMALL_CIRCLES_COLOR.
204
  """
205
  try:
206
  if self.original_display is None:
 
221
  for x, y, diameter in self.marks:
222
  zoomed_x = int(x * self.zoom_factor)
223
  zoomed_y = int(y * self.zoom_factor)
224
+ zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
225
 
226
+ # Draw the main yellow circle
227
  cv2.circle(
228
  zoomed_bgr,
229
  (zoomed_x, zoomed_y),
 
233
  lineType=cv2.LINE_AA
234
  )
235
 
236
+ # Draw 8 small black circles around
237
  num_points = 8
238
  for i in range(num_points):
239
  angle = 2 * np.pi * i / num_points
 
243
  zoomed_bgr,
244
  (point_x, point_y),
245
  1,
246
+ self.SMALL_CIRCLES_COLOR,
247
  -1,
248
  lineType=cv2.LINE_AA
249
  )
 
267
 
268
  def analyze_roi(self, evt: gr.SelectData):
269
  """
270
+ Called when user clicks on the displayed image to measure an ROI.
 
271
  """
272
  try:
273
  if self.current_image is None:
 
276
  clicked_x = evt.index[0]
277
  clicked_y = evt.index[1]
278
 
279
+ # Adjust by pan
280
  x = clicked_x + self.pan_x
281
  y = clicked_y + self.pan_y
282
 
283
+ # Adjust by zoom
284
  if self.zoom_factor != 1.0:
285
  x = x / self.zoom_factor
286
  y = y / self.zoom_factor
 
295
 
296
  dx = X - x
297
  dy = Y - y
298
+ dist_squared = dx * dx + dy * dy
299
 
300
  mask = np.zeros((height, width), dtype=bool)
301
  mask[dist_squared <= r_squared] = True
 
349
 
350
  row1, row2 = row_pair
351
 
352
+ # SNR formula
353
  formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
354
  formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
355
  cell1 = ws[f"{formula_col}{row1}"]
 
357
  cell1.font = red_font
358
  cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
359
 
360
+ # CNR formula
361
  formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
362
  cell2 = ws[f"{formula_col}{row2}"]
363
  cell2.value = formula2
 
371
  def save_formatted_results(self, output_path):
372
  """
373
  Creates an Excel file with results in a formatted table,
374
+ including SNR/CNR formulas and average calculations.
 
 
 
375
  """
376
  try:
377
  if not self.results:
 
382
  red_font = openpyxl.styles.Font(color="FF0000")
383
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
384
 
385
+ # Column group headers
386
  headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
387
 
388
  column_groups = [
 
395
  ('BV', 'BW', 'BX', 'BY', 'BZ')
396
  ]
397
 
 
398
  for cols in column_groups:
399
  for i, header in enumerate(headers):
400
  cell = ws[f"{cols[i]}1"]
 
411
  '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
412
  ]
413
 
414
+ # Set the phantom size row labels
415
  for i, size in enumerate(phantom_sizes):
416
  header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
417
  header_cell.font = red_font
418
  header_cell.alignment = center_alignment
419
 
420
+ # Fill the data from self.results
421
  result_idx = 0
422
  current_col_group = 0
423
  current_row_pair = 0
 
429
  cols = column_groups[current_col_group]
430
  rows = row_pairs[current_row_pair]
431
 
432
+ # First row
433
  if result_idx < len(self.results):
434
  result = self.results[result_idx]
435
  self._write_result_to_cells(ws, result, cols, rows[0])
436
  result_idx += 1
437
 
438
+ # Second row
439
  if result_idx < len(self.results):
440
  result = self.results[result_idx]
441
  self._write_result_to_cells(ws, result, cols, rows[1])
442
  result_idx += 1
443
 
444
+ # Add SNR/CNR formulas
445
  self.add_formulas_to_template(ws, rows, cols, red_font)
446
 
447
  current_col_group += 1
 
457
  if cell.value is not None:
458
  cell.alignment = center_alignment
459
 
460
+ # Additional tables: StdDev Averages and CNR Averages
461
  current_row = 32
462
 
463
+ # StdDev
464
  stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages")
465
  stddev_header.font = red_font
466
  stddev_header.alignment = center_alignment
467
  current_row += 1
468
 
469
  for i, size in enumerate(phantom_sizes):
470
+ row_number = row_pairs[i][0]
471
  stddev_values = []
472
 
473
  for cols in column_groups:
474
+ stddev_col = cols[2] # The StdDev column
475
  cell_value = ws[f"{stddev_col}{row_number}"].value
476
+ if cell_value not in [0, None, '']:
477
  stddev_values.append(float(cell_value))
478
 
479
  size_cell = ws.cell(row=current_row, column=1, value=size)
 
486
  avg_cell.alignment = center_alignment
487
  current_row += 1
488
 
489
+ current_row += 2
490
 
491
+ # CNR
492
  cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages")
493
  cnr_header.font = red_font
494
  cnr_header.alignment = center_alignment
495
  current_row += 1
496
 
 
497
  for i, size in enumerate(phantom_sizes):
498
  row_number = row_pairs[i][1]
499
  cnr_cells = []
 
502
  formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
503
  cnr_cell_ref = f"{formula_col}{row_number}"
504
 
505
+ # Read Mean1, Mean2, Std2 to skip zeros
506
  mean_col = cols[1]
507
  std_col = cols[2]
508
 
 
517
  except:
518
  mean1_val, mean2_val, std2_val = 0, 0, 0
519
 
520
+ # If not all zero, add the cell reference
521
  if not (mean1_val == 0 and mean2_val == 0 and std2_val == 0):
522
  cnr_cells.append(cnr_cell_ref)
523
 
 
525
  size_cell.alignment = center_alignment
526
 
527
  if cnr_cells:
528
+ # Using AVERAGE(...) instead of AVERAGEIF
529
  average_formula = f'=IFERROR(AVERAGE({",".join(cnr_cells)}), "")'
530
 
531
  avg_cell = ws.cell(row=current_row, column=2)
 
535
 
536
  current_row += 1
537
 
538
+ # Align the extra rows
539
  for row in range(32, current_row):
540
  for col in range(1, 3):
541
  cell = ws.cell(row=row, column=col)
 
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 to worksheet cells."""
553
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
554
 
555
  value_mapping = {
 
563
  for i, (header, key) in enumerate(value_mapping.items()):
564
  cell = ws[f"{cols[i]}{row}"]
565
  val = result[key]
 
566
  cell.value = float(val) if val not in ['', None] else ''
567
  cell.alignment = center_alignment
568
 
 
575
  df = df[columns_order]
576
  return df.to_string(index=False)
577
 
 
 
 
 
 
 
 
 
 
 
 
578
  def add_zero_row(self, image):
579
+ """
580
+ Adds one row with zero-values.
581
+ """
582
  self.results.append({
583
  'Area (mm²)': '0.000',
584
  'Mean': '0.000',
 
591
 
592
  def add_two_zero_rows(self, image):
593
  """
594
+ Adds two consecutive rows with zero-values.
595
  """
596
  for _ in range(2):
597
  self.results.append({
 
643
  elem_id="image_display"
644
  )
645
 
646
+ # Removed the "Add Blank Row" button
647
  with gr.Row():
 
648
  zero_btn = gr.Button("Add Zero Row")
649
+ zero2_btn = gr.Button("Add Two Zero Rows")
650
  undo_btn = gr.Button("Undo Last")
651
  save_btn = gr.Button("Save Results")
652
  save_formatted_btn = gr.Button("Save Formatted Results")
 
657
 
658
  gr.Markdown("""
659
  ### Controls:
660
+ - Use arrow keys to pan when zoomed in. Movement is now larger.
661
  - Click points to measure ROI.
662
  - Use Zoom In/Out buttons or Reset View to adjust zoom level.
663
  - Use Reset All to clear all measurements.
664
+ - "Save Results": basic Excel with raw data.
665
+ - "Save Formatted Results": Excel with advanced formatting & formulas.
666
  """)
667
 
 
668
  def update_diameter(x):
669
  analyzer.circle_diameter = float(x)
670
  print(f"Diameter updated to: {x}")
671
  return f"Diameter set to {x} pixels"
672
 
 
673
  def save_formatted():
674
  output_path = "analysis_results_formatted.xlsx"
675
  return analyzer.save_formatted_results(output_path)
676
 
677
+ # Handlers
678
  file_input.change(
679
  fn=analyzer.load_dicom,
680
  inputs=file_input,
 
723
  outputs=image_display
724
  )
725
 
726
+ # Removed blank_btn
 
 
 
 
 
727
  zero_btn.click(
728
  fn=analyzer.add_zero_row,
729
  inputs=image_display,
730
  outputs=[image_display, results_display]
731
  )
732
+
 
733
  zero2_btn.click(
734
  fn=analyzer.add_two_zero_rows,
735
  inputs=image_display,
 
753
  )
754
 
755
  # JavaScript snippet to allow multiple arrow key presses
 
756
  js = """
757
  <script>
758
  document.addEventListener('keydown', function(e) {
 
760
  e.preventDefault();
761
  const keyPressElement = document.querySelector('#key_press textarea');
762
  if (keyPressElement) {
 
763
  keyPressElement.value = e.key;
764
  keyPressElement.dispatchEvent(new Event('input'));
 
 
765
  setTimeout(() => {
766
  keyPressElement.value = '';
767
  keyPressElement.dispatchEvent(new Event('input'));