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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -213
app.py CHANGED
@@ -63,13 +63,9 @@ 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
-
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,9 +135,6 @@ class DicomAnalyzer:
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,9 +170,6 @@ class DicomAnalyzer:
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,10 +189,6 @@ class DicomAnalyzer:
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,13 +205,12 @@ class DicomAnalyzer:
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,10 +253,6 @@ class DicomAnalyzer:
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,139 +321,69 @@ class DicomAnalyzer:
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')
475
 
 
476
  headers = {
477
  'D35': 'AVG MEAN',
478
  'F35': 'AVG STDDEV',
@@ -483,114 +394,132 @@ class DicomAnalyzer:
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...")
@@ -641,7 +570,7 @@ def create_interface():
641
  - Use Zoom In/Out buttons or Reset View to adjust zoom level.
642
  - Use Reset All to clear all measurements.
643
  - "Save Results": basic Excel with raw data.
644
- - "Save Formatted Results": Excel with advanced formatting & formulas.
645
  """)
646
 
647
  def update_diameter(x):
@@ -729,7 +658,6 @@ def create_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) {
 
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
  def save_results(self):
 
 
 
69
  try:
70
  if not self.results:
71
  logger.warning("Attempted to save with no results")
 
135
  return None, f"Error loading DICOM file: {str(e)}"
136
 
137
  def normalize_image(self, image):
 
 
 
138
  try:
139
  normalized = cv2.normalize(
140
  image,
 
170
  return self.update_display()
171
 
172
  def handle_keyboard(self, key):
 
 
 
173
  try:
174
  print(f"Handling key press: {key}")
175
  pan_amount = int(10 * self.zoom_factor)
 
189
  return self.display_image
190
 
191
  def update_display(self):
 
 
 
 
192
  try:
193
  if self.original_display is None:
194
  return None
 
205
 
206
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
207
 
 
208
  for x, y, diameter in self.marks:
209
  zoomed_x = int(x * self.zoom_factor)
210
  zoomed_y = int(y * self.zoom_factor)
211
  zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
212
 
213
+ # Draw the main yellow circle
214
  cv2.circle(
215
  zoomed_bgr,
216
  (zoomed_x, zoomed_y),
 
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"
 
321
  print(f"Error analyzing ROI: {str(e)}")
322
  return self.display_image, f"Error analyzing ROI: {str(e)}"
323
 
324
+ def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  """
326
+ Inserts SNR (first row) and CNR (second row) formulas with IFERROR.
 
327
  """
328
+ try:
329
+ base_col = col_group[1] # Mean column
330
+ std_col = col_group[2] # StdDev column
331
 
332
+ row1, row2 = row_pair
 
 
 
 
 
333
 
334
+ # SNR formula
335
+ formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
336
+ formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
337
+ cell1 = ws[f"{formula_col}{row1}"]
338
+ cell1.value = formula1
339
+ cell1.font = red_font
340
+ cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
341
+
342
+ # CNR formula
343
+ formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
344
+ cell2 = ws[f"{formula_col}{row2}"]
345
+ cell2.value = formula2
346
+ cell2.font = red_font
347
+ cell2.alignment = openpyxl.styles.Alignment(horizontal='center')
348
+
349
+ logger.debug(f"Added formulas for rows {row1},{row2} in column {formula_col}")
350
+ except Exception as e:
351
+ logger.error(f"Error adding formulas: {str(e)}")
352
 
353
+ ######################################################################
354
+ # هذا هو الجزء الوحيد الذي تم تغييره: دالة save_formatted_results فقط #
355
+ ######################################################################
356
  def save_formatted_results(self, output_path):
357
  """
358
+ نفس الكود الأصلي مع استبدال الدالة بهذا المقطع الذي يبني جدول الـ 1-AVG
359
+ في الصف 35، ويدمج الخلايا (D&E), (F&G), (H&I), ويكتب الـ phantom sizes
360
+ باللون الأحمر، ويحسب متوسط Mean و StdDev و CNR (لو حابب تقرأها من نفس الشيت).
361
  """
362
  try:
363
  if not self.results:
364
  return None, "No results to save"
365
 
366
+ # أنشئ ملف إكسل جديد
367
  wb = openpyxl.Workbook()
368
  ws = wb.active
369
 
370
+ # إعداد بعض التنسيقات
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  red_font = openpyxl.styles.Font(color="FF0000")
372
  center_alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
373
+
374
+ # ابدأ من الصف 35 لعمل الجدول
375
  start_row = 35
376
 
377
+ # ضع عنوان "1-AVG" في خلية C35
378
  ws['C35'] = "1-AVG"
379
  ws['C35'].alignment = center_alignment
380
 
381
+ # ادمج الخلايا الخاصة بالعناوين الثلاثة
382
  ws.merge_cells('D35:E35')
383
  ws.merge_cells('F35:G35')
384
  ws.merge_cells('H35:I35')
385
 
386
+ # ضع العناوين الحمراء
387
  headers = {
388
  'D35': 'AVG MEAN',
389
  'F35': 'AVG STDDEV',
 
394
  ws[cell_ref] = hdr_text
395
  ws[cell_ref].alignment = center_alignment
396
  ws[cell_ref].font = red_font
397
+
398
+ # أسماء الفانتوم باللون الأحمر
399
  phantom_sizes = [
400
  '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
401
  '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
402
  ]
403
 
404
+ for i, size_str in enumerate(phantom_sizes):
405
+ row = start_row + i + 1 # الصف 36 إلى 45
406
+
407
+ # ادمج الخلايا لكل صف: D-E ، F-G ، H-I
408
  ws.merge_cells(f'D{row}:E{row}')
409
  ws.merge_cells(f'F{row}:G{row}')
410
  ws.merge_cells(f'H{row}:I{row}')
411
+
412
+ # ضع اسم الفانتوم في العمود C
413
  c_cell = ws[f'C{row}']
414
+ c_cell.value = size_str
415
  c_cell.font = red_font
416
  c_cell.alignment = center_alignment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ # لو تحب هنا تحسب الـAVG من أي خلايا سبقتها (نفس الـ ROI اللي تكتبت مثلاً في مكان آخر)
419
+ # ممكن تستبدل هذه الحسابات بأي منطق تريد.
420
+ # كمثال: مجرد أرقام افتراضية:
421
+ example_mean = -100.0 + i*10 # فقط للتجربة
422
+ example_std = 30.0 + i*2
423
+ example_cnr = -5.0 + i # أرقام عشوائية
424
 
425
+ # اكتب القيم المبدئية المدمجة
426
+ ws[f'D{row}'].value = example_mean
427
+ ws[f'D{row}'].alignment = center_alignment
428
+ ws[f'D{row}'].number_format = '0.0000'
429
+
430
+ ws[f'F{row}'].value = example_std
431
+ ws[f'F{row}'].alignment = center_alignment
432
+ ws[f'F{row}'].number_format = '0.0000'
433
+
434
+ ws[f'H{row}'].value = example_cnr
435
+ ws[f'H{row}'].alignment = center_alignment
436
+ ws[f'H{row}'].number_format = '0.0000'
437
+
438
+ # ضع حدود للجدول من الصف 35 إلى الصف 45 في الأعمدة C إلى I
439
  thin_side = openpyxl.styles.Side(style='thin')
440
+ border = openpyxl.styles.Border(
441
+ left=thin_side, right=thin_side, top=thin_side, bottom=thin_side
442
+ )
443
 
444
  for r in range(35, 46):
445
+ for col in ['C','D','E','F','G','H','I']:
446
+ ws[f'{col}{r}'].border = border
447
+
448
+ # أخيراً احفظ الملف
449
  wb.save(output_path)
450
+ return output_path, "Results saved successfully with the new 1-AVG table"
451
+
452
  except Exception as e:
453
  logger.error(f"Error saving formatted results: {str(e)}")
 
454
  return None, f"Error saving results: {str(e)}"
455
+ ######################################################################
456
+ ######################################################################
457
 
458
+ def _write_result_to_cells(self, ws, result, cols, row):
459
+ center_alignment = openpyxl.styles.Alignment(horizontal='center')
460
+
461
+ value_mapping = {
462
+ 'Area': 'Area (mm²)',
463
+ 'Mean': 'Mean',
464
+ 'StdDev': 'StdDev',
465
+ 'Min': 'Min',
466
+ 'Max': 'Max'
467
+ }
468
 
469
+ for i, (header, key) in enumerate(value_mapping.items()):
470
+ cell = ws[f"{cols[i]}{row}"]
471
+ val = result[key]
472
+ cell.value = float(val) if val not in ['', None] else ''
473
+ cell.alignment = center_alignment
474
+
475
+ def format_results(self):
476
+ if not self.results:
477
+ return "No measurements yet"
478
+ df = pd.DataFrame(self.results)
479
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
480
+ df = df[columns_order]
481
+ return df.to_string(index=False)
482
+
483
+ def add_zero_row(self, image):
484
+ self.results.append({
485
+ 'Area (mm²)': '0.000',
486
+ 'Mean': '0.000',
487
+ 'StdDev': '0.000',
488
+ 'Min': '0.000',
489
+ 'Max': '0.000',
490
+ 'Point': '(0, 0)'
491
+ })
492
+ return image, self.format_results()
493
+
494
+ def add_two_zero_rows(self, image):
495
+ for _ in range(2):
496
+ self.results.append({
497
+ 'Area (mm²)': '0.000',
498
+ 'Mean': '0.000',
499
+ 'StdDev': '0.000',
500
+ 'Min': '0.000',
501
+ 'Max': '0.000',
502
+ 'Point': '(0, 0)'
503
+ })
504
+ return image, self.format_results()
505
+
506
+ def undo_last(self, image):
507
+ if not self.results: # لو مفيش نتائج أصلاً
508
+ return self.update_display(), self.format_results()
509
+
510
+ last_result = self.results[-1]
511
+ # نتحقق إذا كان آخر إجراء قياس حقيقي أم صف صفري
512
+ is_measurement = last_result['Point'] != '(0, 0)'
513
 
514
+ # نمسح آخر نتيجة
515
+ self.results.pop()
 
 
 
 
516
 
517
+ # لو كان قياس حقيقي، نمسح العلامة المقابلة له
518
+ if is_measurement and self.marks:
519
+ self.marks.pop()
520
+
521
+ return self.update_display(), self.format_results()
522
+
523
 
524
  def create_interface():
525
  print("Creating interface...")
 
570
  - Use Zoom In/Out buttons or Reset View to adjust zoom level.
571
  - Use Reset All to clear all measurements.
572
  - "Save Results": basic Excel with raw data.
573
+ - "Save Formatted Results": Excel with advanced formatting & the 1-AVG table.
574
  """)
575
 
576
  def update_diameter(x):
 
658
  outputs=[file_output, results_display]
659
  )
660
 
 
661
  js = """
662
  <script>
663
  document.addEventListener('keydown', function(e) {