HeshamAI commited on
Commit
6793a80
·
verified ·
1 Parent(s): 822e7da

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -175
app.py CHANGED
@@ -63,17 +63,13 @@ class DicomAnalyzer:
63
  self.max_pan_y = 0
64
  self.CIRCLE_COLOR = (0, 255, 255) # BGR format
65
  self.SMALL_CIRCLES_COLOR = (255, 255, 255) # BGR white
 
66
  print("DicomAnalyzer initialized...")
67
 
68
- # If you rely on self.column_groups in your new snippet, define it here:
69
- # Adjust the column letters to match how your raw (Mean, StdDev) data are actually laid out.
70
- self.column_groups = [
71
- # Example: (AreaCol, MeanCol, StdDevCol) if you store area in B, mean in C, std in D, etc.
72
- # If you only store mean & std, just treat the first col as a placeholder.
73
- ('B','C','D'),
74
- ]
75
-
76
  def save_results(self):
 
 
 
77
  try:
78
  if not self.results:
79
  logger.warning("Attempted to save with no results")
@@ -143,6 +139,9 @@ class DicomAnalyzer:
143
  return None, f"Error loading DICOM file: {str(e)}"
144
 
145
  def normalize_image(self, image):
 
 
 
146
  try:
147
  normalized = cv2.normalize(
148
  image,
@@ -178,6 +177,9 @@ class DicomAnalyzer:
178
  return self.update_display()
179
 
180
  def handle_keyboard(self, key):
 
 
 
181
  try:
182
  print(f"Handling key press: {key}")
183
  pan_amount = int(10 * self.zoom_factor)
@@ -197,6 +199,10 @@ class DicomAnalyzer:
197
  return self.display_image
198
 
199
  def update_display(self):
 
 
 
 
200
  try:
201
  if self.original_display is None:
202
  return None
@@ -213,12 +219,13 @@ class DicomAnalyzer:
213
 
214
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
215
 
 
216
  for x, y, diameter in self.marks:
217
  zoomed_x = int(x * self.zoom_factor)
218
  zoomed_y = int(y * self.zoom_factor)
219
  zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
220
 
221
- # Draw the main yellow circle
222
  cv2.circle(
223
  zoomed_bgr,
224
  (zoomed_x, zoomed_y),
@@ -261,6 +268,10 @@ class DicomAnalyzer:
261
  return self.original_display
262
 
263
  def analyze_roi(self, evt: gr.SelectData):
 
 
 
 
264
  try:
265
  if self.current_image is None:
266
  return None, "No image loaded"
@@ -329,80 +340,135 @@ class DicomAnalyzer:
329
  print(f"Error analyzing ROI: {str(e)}")
330
  return self.display_image, f"Error analyzing ROI: {str(e)}"
331
 
332
- # -------------------------------------------------------------------
333
- # The following two helper methods were in your original code.
334
- # We keep them, in case you still need them. Adjust or remove if needed.
335
- # -------------------------------------------------------------------
336
- def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
337
  """
338
- Inserts SNR (first row) and CNR (second row) formulas with IFERROR.
339
  """
340
- try:
341
- base_col = col_group[1] # Mean column
342
- std_col = col_group[2] # StdDev column
343
-
344
- row1, row2 = row_pair
345
-
346
- # SNR formula
347
- formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
348
- formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
349
- cell1 = ws[f"{formula_col}{row1}"]
350
- cell1.value = formula1
351
- cell1.font = red_font
352
- cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
353
-
354
- # CNR formula
355
- formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
356
- cell2 = ws[f"{formula_col}{row2}"]
357
- cell2.value = formula2
358
- cell2.font = red_font
359
- cell2.alignment = openpyxl.styles.Alignment(horizontal='center')
360
-
361
- logger.debug(f"Added formulas for rows {row1},{row2} in column {formula_col}")
362
- except Exception as e:
363
- logger.error(f"Error adding formulas: {str(e)}")
364
 
365
- def _write_result_to_cells(self, ws, result, cols, row):
366
- center_alignment = openpyxl.styles.Alignment(horizontal='center')
367
-
368
- value_mapping = {
369
- 'Area': 'Area (mm²)',
370
- 'Mean': 'Mean',
371
- 'StdDev': 'StdDev',
372
- 'Min': 'Min',
373
- 'Max': 'Max'
374
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
- for i, (header, key) in enumerate(value_mapping.items()):
377
- cell = ws[f"{cols[i]}{row}"]
378
- val = result[key]
379
- cell.value = float(val) if val not in ['', None] else ''
380
- cell.alignment = center_alignment
381
 
382
 
383
- # -------------------------------------------------------------------
384
- # REPLACEMENT SAVE_FORMATTED_RESULTS (Your updated snippet):
385
- # -------------------------------------------------------------------
386
  def save_formatted_results(self, output_path):
 
 
 
 
 
387
  try:
388
  if not self.results:
389
  return None, "No results to save"
390
 
 
391
  wb = openpyxl.Workbook()
392
  ws = wb.active
393
-
394
- # Define styles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  red_font = openpyxl.styles.Font(color="FF0000")
396
  center_alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
397
-
398
- # Start creating the averages table at row 35
399
  start_row = 35
400
 
401
  # Write the "1-AVG" header
402
  ws['C35'] = "1-AVG"
403
  ws['C35'].alignment = center_alignment
404
 
405
- # Merge cells for headers and add titles
406
  ws.merge_cells('D35:E35')
407
  ws.merge_cells('F35:G35')
408
  ws.merge_cells('H35:I35')
@@ -413,143 +479,118 @@ class DicomAnalyzer:
413
  'H35': 'AVG CNR'
414
  }
415
 
416
- for cell, value in headers.items():
417
- ws[cell] = value
418
- ws[cell].alignment = center_alignment
419
- ws[cell].font = red_font
420
-
421
- # Add phantom sizes in red
422
  phantom_sizes = [
423
  '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
424
  '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
425
  ]
426
 
427
- for i, size in enumerate(phantom_sizes):
428
- row = start_row + i + 1 # Start from row 36
429
-
430
- # Merge cells for each row
431
  ws.merge_cells(f'D{row}:E{row}')
432
  ws.merge_cells(f'F{row}:G{row}')
433
  ws.merge_cells(f'H{row}:I{row}')
434
-
435
- cell = ws[f'C{row}']
436
- cell.value = size
437
- cell.font = red_font
438
- cell.alignment = center_alignment
439
-
440
- # Calculate averages as before
441
- mean_values = []
442
- stddev_values = []
443
- cnr_values = []
444
-
445
- # For example, row_pair = (2 + i*3, 3 + i*3).
446
- row_pair = (2 + i * 3, 3 + i * 3)
447
-
448
- for cols in self.column_groups:
449
- mean_col = cols[1]
450
- stddev_col = cols[2]
451
-
452
- mean1_val = ws[f"{mean_col}{row_pair[0]}"].value
453
- mean2_val = ws[f"{mean_col}{row_pair[1]}"].value
454
- stddev2_val = ws[f"{stddev_col}{row_pair[1]}"].value
455
-
456
- try:
457
- mean1_val = float(mean1_val) if mean1_val not in [None, ''] else None
458
- mean2_val = float(mean2_val) if mean2_val not in [None, ''] else None
459
- stddev2_val = float(stddev2_val) if stddev2_val not in [None, ''] else None
460
-
461
- if all(v is not None for v in [mean1_val, mean2_val, stddev2_val]):
462
- mean_values.append(mean1_val)
463
- stddev_values.append(stddev2_val)
464
- cnr_values.append((mean1_val - mean2_val) / stddev2_val)
465
- except (ValueError, TypeError):
466
- continue
467
-
468
- # Write averages to merged cells
469
- if mean_values:
470
- ws[f'D{row}'].value = sum(mean_values) / len(mean_values)
 
 
 
 
 
 
 
471
  ws[f'D{row}'].alignment = center_alignment
472
  ws[f'D{row}'].number_format = '0.0000'
473
 
474
- if stddev_values:
475
- ws[f'F{row}'].value = sum(stddev_values) / len(stddev_values)
476
  ws[f'F{row}'].alignment = center_alignment
477
  ws[f'F{row}'].number_format = '0.0000'
478
 
479
- if cnr_values:
480
- ws[f'H{row}'].value = sum(cnr_values) / len(cnr_values)
481
  ws[f'H{row}'].alignment = center_alignment
482
  ws[f'H{row}'].number_format = '0.0000'
 
 
 
 
483
 
484
- # Add borders to the table including merged cells
485
- border = openpyxl.styles.Border(
486
- left=openpyxl.styles.Side(style='thin'),
487
- right=openpyxl.styles.Side(style='thin'),
488
- top=openpyxl.styles.Side(style='thin'),
489
- bottom=openpyxl.styles.Side(style='thin')
490
- )
491
-
492
- for row in range(35, 46): # From row 35 to 45
493
- for col in ['C', 'D', 'E', 'F', 'G', 'H', 'I']:
494
- ws[f'{col}{row}'].border = border
495
-
496
- # Save the workbook
497
  wb.save(output_path)
498
- return output_path, "Results saved successfully with formatted averages table"
499
-
500
  except Exception as e:
501
  logger.error(f"Error saving formatted results: {str(e)}")
 
502
  return None, f"Error saving results: {str(e)}"
503
 
504
-
505
- def format_results(self):
506
- if not self.results:
507
- return "No measurements yet"
508
- df = pd.DataFrame(self.results)
509
- columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
510
- df = df[columns_order]
511
- return df.to_string(index=False)
512
-
513
- def add_zero_row(self, image):
514
- self.results.append({
515
- 'Area (mm²)': '0.000',
516
- 'Mean': '0.000',
517
- 'StdDev': '0.000',
518
- 'Min': '0.000',
519
- 'Max': '0.000',
520
- 'Point': '(0, 0)'
521
- })
522
- return image, self.format_results()
523
-
524
- def add_two_zero_rows(self, image):
525
- for _ in range(2):
526
- self.results.append({
527
- 'Area (mm²)': '0.000',
528
- 'Mean': '0.000',
529
- 'StdDev': '0.000',
530
- 'Min': '0.000',
531
- 'Max': '0.000',
532
- 'Point': '(0, 0)'
533
- })
534
- return image, self.format_results()
535
-
536
- def undo_last(self, image):
537
- if not self.results: # لو مفيش نتائج أصلاً
538
- return self.update_display(), self.format_results()
539
-
540
- last_result = self.results[-1]
541
- # نتحقق إذا كان آخر إجراء قياس حقيقي أم صف صفري
542
- is_measurement = last_result['Point'] != '(0, 0)'
543
 
544
- # نمسح آخر نتيجة
545
- self.results.pop()
 
 
 
 
 
 
 
 
546
 
547
- # لو كان قياس حقيقي، نمسح العلامة المقابلة له
548
- if is_measurement and self.marks:
549
- self.marks.pop()
550
-
551
- return self.update_display(), self.format_results()
552
-
553
 
554
  def create_interface():
555
  print("Creating interface...")
@@ -688,6 +729,7 @@ def create_interface():
688
  outputs=[file_output, results_display]
689
  )
690
 
 
691
  js = """
692
  <script>
693
  document.addEventListener('keydown', function(e) {
 
63
  self.max_pan_y = 0
64
  self.CIRCLE_COLOR = (0, 255, 255) # BGR format
65
  self.SMALL_CIRCLES_COLOR = (255, 255, 255) # BGR white
66
+
67
  print("DicomAnalyzer initialized...")
68
 
 
 
 
 
 
 
 
 
69
  def save_results(self):
70
+ """
71
+ Basic method to save raw results to an Excel sheet (one sheet, no formatting).
72
+ """
73
  try:
74
  if not self.results:
75
  logger.warning("Attempted to save with no results")
 
139
  return None, f"Error loading DICOM file: {str(e)}"
140
 
141
  def normalize_image(self, image):
142
+ """
143
+ Normalizes raw pixel data to [0..255], and ensures 3-channel BGR for display.
144
+ """
145
  try:
146
  normalized = cv2.normalize(
147
  image,
 
177
  return self.update_display()
178
 
179
  def handle_keyboard(self, key):
180
+ """
181
+ Pans the zoomed image with arrow keys.
182
+ """
183
  try:
184
  print(f"Handling key press: {key}")
185
  pan_amount = int(10 * self.zoom_factor)
 
199
  return self.display_image
200
 
201
  def update_display(self):
202
+ """
203
+ Returns a version of self.original_display that is zoomed/panned
204
+ and shows ROI circles.
205
+ """
206
  try:
207
  if self.original_display is None:
208
  return None
 
219
 
220
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
221
 
222
+ # Draw circles in the zoomed plane
223
  for x, y, diameter in self.marks:
224
  zoomed_x = int(x * self.zoom_factor)
225
  zoomed_y = int(y * self.zoom_factor)
226
  zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
227
 
228
+ # Draw the main circle in yellow
229
  cv2.circle(
230
  zoomed_bgr,
231
  (zoomed_x, zoomed_y),
 
268
  return self.original_display
269
 
270
  def analyze_roi(self, evt: gr.SelectData):
271
+ """
272
+ Called when a user clicks on the DICOM image.
273
+ We create a circular ROI, gather stats, store the results, and draw.
274
+ """
275
  try:
276
  if self.current_image is None:
277
  return None, "No image loaded"
 
340
  print(f"Error analyzing ROI: {str(e)}")
341
  return self.display_image, f"Error analyzing ROI: {str(e)}"
342
 
343
+
344
+ def format_results(self):
 
 
 
345
  """
346
+ Returns a simple text version of self.results for the UI.
347
  """
348
+ if not self.results:
349
+ return "No measurements yet"
350
+ df = pd.DataFrame(self.results)
351
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
352
+ df = df[columns_order]
353
+ return df.to_string(index=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
+ def add_zero_row(self, image):
356
+ """
357
+ For testing. Adds a zero row to self.results.
358
+ """
359
+ self.results.append({
360
+ 'Area (mm²)': '0.000',
361
+ 'Mean': '0.000',
362
+ 'StdDev': '0.000',
363
+ 'Min': '0.000',
364
+ 'Max': '0.000',
365
+ 'Point': '(0, 0)'
366
+ })
367
+ return image, self.format_results()
368
+
369
+ def add_two_zero_rows(self, image):
370
+ """
371
+ For testing. Adds two zero rows to self.results.
372
+ """
373
+ for _ in range(2):
374
+ self.results.append({
375
+ 'Area (mm²)': '0.000',
376
+ 'Mean': '0.000',
377
+ 'StdDev': '0.000',
378
+ 'Min': '0.000',
379
+ 'Max': '0.000',
380
+ 'Point': '(0, 0)'
381
+ })
382
+ return image, self.format_results()
383
+
384
+ def undo_last(self, image):
385
+ """
386
+ Undoes the last measurement or zero row.
387
+ If it was a real measurement, remove its circle too.
388
+ """
389
+ if not self.results:
390
+ return self.update_display(), self.format_results()
391
+
392
+ last_result = self.results[-1]
393
+ is_measurement = last_result['Point'] != '(0, 0)'
394
 
395
+ self.results.pop()
396
+ if is_measurement and self.marks:
397
+ self.marks.pop()
398
+
399
+ return self.update_display(), self.format_results()
400
 
401
 
402
+ @debug_decorator
 
 
403
  def save_formatted_results(self, output_path):
404
+ """
405
+ 1) Writes the raw data from self.results into rows (2,3,5,6,8,9,...).
406
+ 2) Builds the final table at rows 35..45 with merges & red headers, reading
407
+ from those raw cells to compute AVG MEAN, AVG STDDEV, and AVG CNR.
408
+ """
409
  try:
410
  if not self.results:
411
  return None, "No results to save"
412
 
413
+ # Create a fresh workbook
414
  wb = openpyxl.Workbook()
415
  ws = wb.active
416
+
417
+ # row_pairs: each pair is (row_for_first_measurement, row_for_second_measurement).
418
+ # Enough for 10 phantoms (20 measurements).
419
+ row_pairs = [
420
+ (2,3), (5,6), (8,9), (11,12), (14,15),
421
+ (17,18), (20,21), (23,24), (26,27), (29,30)
422
+ ]
423
+
424
+ # We can define the columns for storing data:
425
+ # For example, B=Area, C=Mean, D=StdDev (Min, Max we skip or store in E,F if you like).
426
+ # This code snippet only cares about reading from Mean & StdDev eventually.
427
+ column_groups = [
428
+ ('B','C','D') # (Area, Mean, StdDev)
429
+ ]
430
+
431
+ # We'll write up to 2 results in each pair of rows,
432
+ # then move to the next column group if we have more than 2 results in the same phantom.
433
+ # For simplicity, we'll assume we only have 1 column group.
434
+ # If you had multiple sets of columns, you could do more groups, e.g. ('F','G','H'), etc.
435
+
436
+ result_idx = 0
437
+ pair_idx = 0
438
+
439
+ # Step 1: Write the raw data from self.results into these rows/columns.
440
+ while result_idx < len(self.results) and pair_idx < len(row_pairs):
441
+ # We'll always write to the same column group here
442
+ area_col, mean_col, stddev_col = column_groups[0]
443
+
444
+ # For each phantom, we expect 2 measurements (row1 = object of interest, row2 = background, etc.)
445
+ # 1st measurement
446
+ row1 = row_pairs[pair_idx][0]
447
+ if result_idx < len(self.results):
448
+ r = self.results[result_idx]
449
+ self._write_single_result(ws, r, area_col, mean_col, stddev_col, row1)
450
+ result_idx += 1
451
+
452
+ # 2nd measurement
453
+ row2 = row_pairs[pair_idx][1]
454
+ if result_idx < len(self.results):
455
+ r = self.results[result_idx]
456
+ self._write_single_result(ws, r, area_col, mean_col, stddev_col, row2)
457
+ result_idx += 1
458
+
459
+ pair_idx += 1
460
+
461
+ # Step 2: Build the final merged table at row 35..45.
462
  red_font = openpyxl.styles.Font(color="FF0000")
463
  center_alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
464
+
 
465
  start_row = 35
466
 
467
  # Write the "1-AVG" header
468
  ws['C35'] = "1-AVG"
469
  ws['C35'].alignment = center_alignment
470
 
471
+ # Merge cells for headers and set text
472
  ws.merge_cells('D35:E35')
473
  ws.merge_cells('F35:G35')
474
  ws.merge_cells('H35:I35')
 
479
  'H35': 'AVG CNR'
480
  }
481
 
482
+ for cell_ref, hdr_text in headers.items():
483
+ ws[cell_ref] = hdr_text
484
+ ws[cell_ref].alignment = center_alignment
485
+ ws[cell_ref].font = red_font
486
+
487
+ # Phantom sizes in red
488
  phantom_sizes = [
489
  '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
490
  '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
491
  ]
492
 
493
+ for i, size_label in enumerate(phantom_sizes):
494
+ row = start_row + i + 1 # 36..45
495
+
496
+ # Merge the 3 sets of columns for each row
497
  ws.merge_cells(f'D{row}:E{row}')
498
  ws.merge_cells(f'F{row}:G{row}')
499
  ws.merge_cells(f'H{row}:I{row}')
500
+
501
+ c_cell = ws[f'C{row}']
502
+ c_cell.value = size_label
503
+ c_cell.font = red_font
504
+ c_cell.alignment = center_alignment
505
+
506
+ # We'll read from the row_pairs above: row_pair = (2 + i*3, 3 + i*3)
507
+ # But we can actually just use the same row_pairs we used above.
508
+ # Because we have 10 items in phantom_sizes, each corresponding to row_pairs[i].
509
+ # We'll do that directly:
510
+ if i < len(row_pairs):
511
+ (raw_row1, raw_row2) = row_pairs[i]
512
+ else:
513
+ # If we have fewer row_pairs than phantom sizes, skip
514
+ continue
515
+
516
+ # Let's read from the single column group for Mean & StdDev
517
+ # If you have multiple column groups, you can loop them. For now we use just one:
518
+ (area_col, mean_col, stddev_col) = column_groups[0]
519
+
520
+ # Fetch the data from the sheet:
521
+ mean1_val = ws[f"{mean_col}{raw_row1}"].value
522
+ mean2_val = ws[f"{mean_col}{raw_row2}"].value
523
+ stddev2_val = ws[f"{stddev_col}{raw_row2}"].value
524
+
525
+ # Convert them to float or None
526
+ try:
527
+ mean1_val = float(mean1_val) if mean1_val not in [None, ''] else None
528
+ mean2_val = float(mean2_val) if mean2_val not in [None, ''] else None
529
+ stddev2_val = float(stddev2_val) if stddev2_val not in [None, ''] else None
530
+ except:
531
+ mean1_val, mean2_val, stddev2_val = None, None, None
532
+
533
+ # Calculate
534
+ if (mean1_val is not None) and (mean2_val is not None) and (stddev2_val is not None) and (stddev2_val != 0):
535
+ avg_mean = mean1_val # or an average of multiple if you want
536
+ avg_std = stddev2_val
537
+ cnr = (mean1_val - mean2_val)/ stddev2_val
538
+ else:
539
+ avg_mean, avg_std, cnr = None, None, None
540
+
541
+ # Place the results in the merged cells:
542
+ if avg_mean is not None:
543
+ ws[f'D{row}'].value = avg_mean
544
  ws[f'D{row}'].alignment = center_alignment
545
  ws[f'D{row}'].number_format = '0.0000'
546
 
547
+ if avg_std is not None:
548
+ ws[f'F{row}'].value = avg_std
549
  ws[f'F{row}'].alignment = center_alignment
550
  ws[f'F{row}'].number_format = '0.0000'
551
 
552
+ if cnr is not None:
553
+ ws[f'H{row}'].value = cnr
554
  ws[f'H{row}'].alignment = center_alignment
555
  ws[f'H{row}'].number_format = '0.0000'
556
+
557
+ # Add borders around the block C35..I45
558
+ thin_side = openpyxl.styles.Side(style='thin')
559
+ border = openpyxl.styles.Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
560
 
561
+ for r in range(35, 46):
562
+ for c in ['C','D','E','F','G','H','I']:
563
+ ws[f'{c}{r}'].border = border
564
+
 
 
 
 
 
 
 
 
 
565
  wb.save(output_path)
566
+ return output_path, "Results saved successfully with formatted table"
 
567
  except Exception as e:
568
  logger.error(f"Error saving formatted results: {str(e)}")
569
+ logger.error(traceback.format_exc())
570
  return None, f"Error saving results: {str(e)}"
571
 
572
+ def _write_single_result(self, ws, result, area_col, mean_col, stddev_col, row):
573
+ """
574
+ Helper to write one measurement to a given row in columns for Area, Mean, StdDev, etc.
575
+ """
576
+ # Convert text to float if possible
577
+ def as_float(v):
578
+ try:
579
+ return float(v)
580
+ except:
581
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
+ area_val = as_float(result.get('Area (mm²)', None))
584
+ mean_val = as_float(result.get('Mean', None))
585
+ stddev_val = as_float(result.get('StdDev', None))
586
+
587
+ if area_val is not None:
588
+ ws[f"{area_col}{row}"].value = area_val
589
+ if mean_val is not None:
590
+ ws[f"{mean_col}{row}"].value = mean_val
591
+ if stddev_val is not None:
592
+ ws[f"{stddev_col}{row}"].value = stddev_val
593
 
 
 
 
 
 
 
594
 
595
  def create_interface():
596
  print("Creating interface...")
 
729
  outputs=[file_output, results_display]
730
  )
731
 
732
+ # Capture arrow keys for panning
733
  js = """
734
  <script>
735
  document.addEventListener('keydown', function(e) {