HeshamAI commited on
Commit
55f9a9d
·
verified ·
1 Parent(s): f42875c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -244
app.py CHANGED
@@ -61,14 +61,13 @@ 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 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")
72
  return None, "No results to save"
73
 
74
  df = pd.DataFrame(self.results)
@@ -160,20 +159,16 @@ class DicomAnalyzer:
160
  return None
161
 
162
  def zoom_in(self, image):
163
- print("Zooming in...")
164
  self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
165
  return self.update_display()
166
 
167
  def zoom_out(self, image):
168
- print("Zooming out...")
169
  self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
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)
176
-
177
  if key == 'ArrowLeft':
178
  self.pan_x = max(0, self.pan_x - pan_amount)
179
  elif key == 'ArrowRight':
@@ -185,7 +180,6 @@ class DicomAnalyzer:
185
 
186
  return self.update_display()
187
  except Exception as e:
188
- print(f"Error handling keyboard input: {str(e)}")
189
  return self.display_image
190
 
191
  def update_display(self):
@@ -205,35 +199,21 @@ class DicomAnalyzer:
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),
217
- zoomed_radius,
218
- self.CIRCLE_COLOR,
219
- 1,
220
- lineType=cv2.LINE_AA
221
- )
222
 
223
- # Draw 8 small white circles around
224
  num_points = 8
225
  for i in range(num_points):
226
  angle = 2 * np.pi * i / num_points
227
  point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
228
  point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
229
- cv2.circle(
230
- zoomed_bgr,
231
- (point_x, point_y),
232
- 1,
233
- self.SMALL_CIRCLES_COLOR,
234
- -1,
235
- lineType=cv2.LINE_AA
236
- )
237
 
238
  zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
239
 
@@ -249,7 +229,6 @@ class DicomAnalyzer:
249
 
250
  return visible
251
  except Exception as e:
252
- print(f"Error updating display: {str(e)}")
253
  return self.original_display
254
 
255
  def analyze_roi(self, evt: gr.SelectData):
@@ -277,7 +256,7 @@ class DicomAnalyzer:
277
 
278
  dx = X - x
279
  dy = Y - y
280
- dist_squared = dx * dx + dy * dy
281
 
282
  mask = np.zeros((height, width), dtype=bool)
283
  mask[dist_squared <= r_squared] = True
@@ -288,8 +267,7 @@ class DicomAnalyzer:
288
  return self.display_image, "Error: No pixels selected"
289
 
290
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
291
- n_pixels = np.sum(mask)
292
- area = n_pixels * (pixel_spacing ** 2)
293
 
294
  mean_value = np.mean(roi_pixels)
295
  std_dev = np.std(roi_pixels, ddof=1)
@@ -318,47 +296,40 @@ class DicomAnalyzer:
318
 
319
  return self.update_display(), self.format_results()
320
  except Exception as e:
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
- # نفس الدالة الأصلية، لكن نهاية الدالة تضع الجدول في الصف 35 بالشكل المطلوب
355
- ########################################################################
356
  def save_formatted_results(self, output_path):
357
  try:
358
  if not self.results:
359
  return None, "No results to save"
360
 
361
- # هنا كل شيء مثل كودك: تنشئ Workbook جديد، تصنع الشيت، تضع الهيدرز...
362
  wb = openpyxl.Workbook()
363
  ws = wb.active
364
  red_font = openpyxl.styles.Font(color="FF0000")
@@ -375,12 +346,14 @@ class DicomAnalyzer:
375
  ('BV', 'BW', 'BX', 'BY', 'BZ')
376
  ]
377
 
 
378
  for cols in column_groups:
379
  for i, header in enumerate(headers):
380
  cell = ws[f"{cols[i]}1"]
381
  cell.value = header
382
  cell.alignment = center_alignment
383
 
 
384
  row_pairs = [
385
  (2, 3), (5, 6), (8, 9), (11, 12), (14, 15),
386
  (17, 18), (20, 21), (23, 24), (26, 27), (29, 30)
@@ -391,13 +364,13 @@ class DicomAnalyzer:
391
  '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
392
  ]
393
 
394
- # كتابة أسماء الفانتوم باللون الأحمر في العمود A (قبل الصف الأول)
395
  for i, size in enumerate(phantom_sizes):
396
- header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
397
- header_cell.font = red_font
398
- header_cell.alignment = center_alignment
399
 
400
- # توزيع نتائج ROI في هذه الصفوف والأعمدة كما في كودك
401
  result_idx = 0
402
  current_col_group = 0
403
  current_row_pair = 0
@@ -407,51 +380,41 @@ class DicomAnalyzer:
407
  break
408
 
409
  cols = column_groups[current_col_group]
410
- rows = row_pairs[current_row_pair]
411
 
412
  if result_idx < len(self.results):
413
- result = self.results[result_idx]
414
- self._write_result_to_cells(ws, result, cols, rows[0])
415
  result_idx += 1
416
 
417
  if result_idx < len(self.results):
418
- result = self.results[result_idx]
419
- self._write_result_to_cells(ws, result, cols, rows[1])
420
  result_idx += 1
421
 
422
- self.add_formulas_to_template(ws, rows, cols, red_font)
423
 
424
  current_col_group += 1
425
  if current_col_group >= len(column_groups):
426
  current_col_group = 0
427
  current_row_pair += 1
428
 
429
- # ضبط المحاذاة للخلايا التي كُتبت بالفعل
430
  for cols in column_groups:
431
  for col in cols:
432
  for row in range(2, 31):
433
  cell = ws[f"{col}{row}"]
434
- if cell.value is not None:
435
  cell.alignment = center_alignment
436
 
437
- ################################################################
438
- # وهنا بدلًا من وضع جداول STDDEV/Mean/CNR منفصلة،
439
- # نضع الجدول المدمج "1-AVG" من الصف 35 مثل الصورة التي أرسلتها
440
- ################################################################
441
-
442
- # سنستخدم نفس فكرة الجدول بالدمج من D35:E35, F35:G35, H35:I35
443
  start_row = 35
444
-
445
- # ضع عنوان "1-AVG" في C35
446
  ws['C35'] = "1-AVG"
447
  ws['C35'].alignment = center_alignment
448
 
449
- # ادمج الخلايا الخاصة بالعناوين
450
  ws.merge_cells('D35:E35')
451
  ws.merge_cells('F35:G35')
452
  ws.merge_cells('H35:I35')
453
 
454
- # ضع العناوين باللون الأحمر
455
  avg_headers = {
456
  'D35': 'AVG MEAN',
457
  'F35': 'AVG STDDEV',
@@ -462,7 +425,7 @@ class DicomAnalyzer:
462
  ws[c_ref].font = red_font
463
  ws[c_ref].alignment = center_alignment
464
 
465
- # نفس أسماء الفانتوم
466
  phantom_sizes2 = [
467
  '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
468
  '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
@@ -470,73 +433,58 @@ class DicomAnalyzer:
470
 
471
  for i, p_size in enumerate(phantom_sizes2):
472
  row = start_row + i + 1 # 36..45
473
-
474
- # ادمج لكل صف: D-E ، F-G ، H-I
475
  ws.merge_cells(f'D{row}:E{row}')
476
  ws.merge_cells(f'F{row}:G{row}')
477
  ws.merge_cells(f'H{row}:I{row}')
478
 
479
- # اكتب اسم الفانتوم باللون الأحمر في العمود C
480
- size_cell = ws[f'C{row}']
481
- size_cell.value = p_size
482
- size_cell.font = red_font
483
- size_cell.alignment = center_alignment
484
-
485
- # لكي نحسب الـAVG الحقيقي من الصفوف 2..3 إلخ،
486
- # نستخدم row_pairs[i] إن أردت. أو كما في المثال الأصلي:
487
- if i < len(row_pairs):
488
- (raw_row1, raw_row2) = row_pairs[i]
489
- else:
490
- continue # لو عندك 10 فقط
491
-
492
- # لنفترض أننا نقرأ من نفس الأعمدة اللي فيها mean & std
493
- # نفترض عندنا على الأقل عمود mean_col = cols[1], stddev_col = cols[2]
494
- # لكن عندك عدة col groups؛ إذا تريد تجمع من الأول فقط:
495
- # أو خذ فكرة snippetك السابق:
496
 
497
  mean_values = []
498
  stddev_values = []
499
  cnr_values = []
500
 
501
- # Example: لو تحب تجمع من نفس الـ column_groups الأولى بس
502
  for group in column_groups:
503
- mean_col = group[1] # 'C','I','O'...الخ
504
- std_col = group[2] # 'D','J','P'...الخ
 
 
 
 
 
505
 
506
- # raw_row1 = الصف الأول
507
- # raw_row2 = الصف الثاني
508
- m1 = ws[f"{mean_col}{raw_row1}"].value
509
- m2 = ws[f"{mean_col}{raw_row2}"].value
510
- std2 = ws[f"{std_col}{raw_row1}"].value
511
 
 
512
  try:
513
- m1 = float(m1) if m1 not in [None,''] else None
514
- m2 = float(m2) if m2 not in [None,''] else None
515
- std2 = float(std2) if std2 not in [None,''] else None
516
  except:
517
- m1,m2,std2 = None,None,None
518
- if (m1 is not None) and (m2 is not None) and (std2 is not None) and (std2!=0):
519
- mean_values.append(m1)
520
- stddev_values.append(std2)
521
- cnr_values.append((m1 - m2)/std2)
522
-
523
- # الآن نحسب المتوسط
524
- if len(mean_values) > 0:
525
- final_mean = sum(mean_values)/len(mean_values)
526
- else:
527
- final_mean = None
528
-
529
- if len(stddev_values) > 0:
530
- final_std = sum(stddev_values)/len(stddev_values)
531
- else:
532
- final_std = None
533
-
534
- if len(cnr_values) > 0:
535
- final_cnr = sum(cnr_values)/len(cnr_values)
536
- else:
537
- final_cnr = None
538
 
539
- # نكتبها في الخلايا المدمجة
 
 
 
 
 
540
  if final_mean is not None:
541
  ws[f'D{row}'].value = final_mean
542
  ws[f'D{row}'].alignment = center_alignment
@@ -552,7 +500,7 @@ class DicomAnalyzer:
552
  ws[f'H{row}'].alignment = center_alignment
553
  ws[f'H{row}'].number_format = '0.0000'
554
 
555
- # أخيراً، ضع الحدود حول C35..I45
556
  thin_side = openpyxl.styles.Side(style='thin')
557
  border = openpyxl.styles.Border(
558
  left=thin_side, right=thin_side, top=thin_side, bottom=thin_side
@@ -566,11 +514,9 @@ class DicomAnalyzer:
566
  except Exception as e:
567
  logger.error(f"Error saving formatted results: {str(e)}")
568
  return None, f"Error saving results: {str(e)}"
569
- ########################################################################
570
 
571
  def _write_result_to_cells(self, ws, result, cols, row):
572
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
573
-
574
  value_mapping = {
575
  'Area': 'Area (mm²)',
576
  'Mean': 'Mean',
@@ -578,7 +524,6 @@ class DicomAnalyzer:
578
  'Min': 'Min',
579
  'Max': 'Max'
580
  }
581
-
582
  for i, (header, key) in enumerate(value_mapping.items()):
583
  cell = ws[f"{cols[i]}{row}"]
584
  val = result[key]
@@ -617,25 +562,17 @@ class DicomAnalyzer:
617
  return image, self.format_results()
618
 
619
  def undo_last(self, image):
620
- if not self.results: # لو مفيش نتائج أصلاً
621
  return self.update_display(), self.format_results()
622
-
623
  last_result = self.results[-1]
624
- # نتحقق إذا كان آخر إجراء قياس حقيقي أم صف صفري
625
- is_measurement = last_result['Point'] != '(0, 0)'
626
-
627
- # نمسح آخر نتيجة
628
  self.results.pop()
629
-
630
- # لو كان قياس حقيقي، نمسح العلامة المقابلة له
631
  if is_measurement and self.marks:
632
  self.marks.pop()
633
-
634
  return self.update_display(), self.format_results()
635
 
636
 
637
  def create_interface():
638
- print("Creating interface...")
639
  analyzer = DicomAnalyzer()
640
 
641
  with gr.Blocks(css="#image_display { outline: none; }") as interface:
@@ -644,13 +581,7 @@ def create_interface():
644
  with gr.Row():
645
  with gr.Column():
646
  file_input = gr.File(label="Upload DICOM file")
647
- diameter_slider = gr.Slider(
648
- minimum=1,
649
- maximum=20,
650
- value=9,
651
- step=1,
652
- label="ROI Diameter (pixels)"
653
- )
654
 
655
  with gr.Row():
656
  zoom_in_btn = gr.Button("Zoom In (+)")
@@ -659,11 +590,7 @@ def create_interface():
659
  reset_all_btn = gr.Button("Reset All")
660
 
661
  with gr.Column():
662
- image_display = gr.Image(
663
- label="DICOM Image",
664
- interactive=True,
665
- elem_id="image_display"
666
- )
667
 
668
  with gr.Row():
669
  zero_btn = gr.Button("Add Zero Row")
@@ -678,111 +605,51 @@ def create_interface():
678
 
679
  gr.Markdown("""
680
  ### Controls:
681
- - Use arrow keys to pan when zoomed in. Movement is now larger.
682
  - Click points to measure ROI.
683
- - Use Zoom In/Out buttons or Reset View to adjust zoom level.
684
- - Use Reset All to clear all measurements.
685
- - "Save Results": basic Excel with raw data.
686
- - "Save Formatted Results": Excel with advanced formatting & the 1-AVG table.
687
  """)
688
 
689
  def update_diameter(x):
690
  analyzer.circle_diameter = float(x)
691
- print(f"Diameter updated to: {x}")
692
  return f"Diameter set to {x} pixels"
693
 
694
  def save_formatted():
695
- output_path = "analysis_results_formatted.xlsx"
696
- return analyzer.save_formatted_results(output_path)
697
-
698
- file_input.change(
699
- fn=analyzer.load_dicom,
700
- inputs=file_input,
701
- outputs=[image_display, results_display]
702
- )
703
-
704
- image_display.select(
705
- fn=analyzer.analyze_roi,
706
- outputs=[image_display, results_display]
707
- )
708
-
709
- diameter_slider.change(
710
- fn=update_diameter,
711
- inputs=diameter_slider,
712
- outputs=gr.Textbox(label="Status")
713
- )
714
-
715
- zoom_in_btn.click(
716
- fn=analyzer.zoom_in,
717
- inputs=image_display,
718
- outputs=image_display,
719
- queue=False
720
- )
721
-
722
- zoom_out_btn.click(
723
- fn=analyzer.zoom_out,
724
- inputs=image_display,
725
- outputs=image_display,
726
- queue=False
727
- )
728
-
729
- reset_btn.click(
730
- fn=analyzer.reset_view,
731
- outputs=image_display
732
- )
733
-
734
- reset_all_btn.click(
735
- fn=analyzer.reset_all,
736
- inputs=image_display,
737
- outputs=[image_display, results_display]
738
- )
739
 
740
- key_press.change(
741
- fn=analyzer.handle_keyboard,
742
- inputs=key_press,
743
- outputs=image_display
744
- )
745
 
746
- zero_btn.click(
747
- fn=analyzer.add_zero_row,
748
- inputs=image_display,
749
- outputs=[image_display, results_display]
750
- )
751
 
752
- zero2_btn.click(
753
- fn=analyzer.add_two_zero_rows,
754
- inputs=image_display,
755
- outputs=[image_display, results_display]
756
- )
757
-
758
- undo_btn.click(
759
- fn=analyzer.undo_last,
760
- inputs=image_display,
761
- outputs=[image_display, results_display]
762
- )
763
 
764
- save_btn.click(
765
- fn=analyzer.save_results,
766
- outputs=[file_output, results_display]
767
- )
768
-
769
- save_formatted_btn.click(
770
- fn=save_formatted,
771
- outputs=[file_output, results_display]
772
- )
773
 
 
774
  js = """
775
  <script>
776
  document.addEventListener('keydown', function(e) {
777
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
778
  e.preventDefault();
779
- const keyPressElement = document.querySelector('#key_press textarea');
780
- if (keyPressElement) {
781
- keyPressElement.value = e.key;
782
- keyPressElement.dispatchEvent(new Event('input'));
783
  setTimeout(() => {
784
- keyPressElement.value = '';
785
- keyPressElement.dispatchEvent(new Event('input'));
786
  }, 100);
787
  }
788
  }
@@ -791,26 +658,16 @@ def create_interface():
791
  """
792
  gr.HTML(js)
793
 
794
- print("Interface created successfully")
795
  return interface
796
 
797
 
798
  if __name__ == "__main__":
799
  try:
800
- print("Starting application...")
801
  interface = create_interface()
802
- print("Launching interface...")
803
  interface.queue()
804
- interface.launch(
805
- server_name="0.0.0.0",
806
- server_port=7860,
807
- share=True,
808
- debug=True,
809
- show_error=True,
810
- quiet=False
811
- )
812
  except Exception as e:
813
- print(f"Error launching application: {str(e)}")
814
  logger.error(f"Error launching application: {str(e)}")
815
  logger.error(traceback.format_exc())
816
  raise e
 
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
65
+ self.SMALL_CIRCLES_COLOR = (255, 255, 255) # BGR
66
  print("DicomAnalyzer initialized...")
67
 
68
  def save_results(self):
69
  try:
70
  if not self.results:
 
71
  return None, "No results to save"
72
 
73
  df = pd.DataFrame(self.results)
 
159
  return None
160
 
161
  def zoom_in(self, image):
 
162
  self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
163
  return self.update_display()
164
 
165
  def zoom_out(self, image):
 
166
  self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
167
  return self.update_display()
168
 
169
  def handle_keyboard(self, key):
170
  try:
 
171
  pan_amount = int(10 * self.zoom_factor)
 
172
  if key == 'ArrowLeft':
173
  self.pan_x = max(0, self.pan_x - pan_amount)
174
  elif key == 'ArrowRight':
 
180
 
181
  return self.update_display()
182
  except Exception as e:
 
183
  return self.display_image
184
 
185
  def update_display(self):
 
199
 
200
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
201
 
202
+ # Draw circles
203
  for x, y, diameter in self.marks:
204
  zoomed_x = int(x * self.zoom_factor)
205
  zoomed_y = int(y * self.zoom_factor)
206
  zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
207
 
208
+ cv2.circle(zoomed_bgr, (zoomed_x, zoomed_y), zoomed_radius, self.CIRCLE_COLOR, 1, lineType=cv2.LINE_AA)
 
 
 
 
 
 
 
 
209
 
210
+ # 8 small circles
211
  num_points = 8
212
  for i in range(num_points):
213
  angle = 2 * np.pi * i / num_points
214
  point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
215
  point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
216
+ cv2.circle(zoomed_bgr, (point_x, point_y), 1, self.SMALL_CIRCLES_COLOR, -1, lineType=cv2.LINE_AA)
 
 
 
 
 
 
 
217
 
218
  zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
219
 
 
229
 
230
  return visible
231
  except Exception as e:
 
232
  return self.original_display
233
 
234
  def analyze_roi(self, evt: gr.SelectData):
 
256
 
257
  dx = X - x
258
  dy = Y - y
259
+ dist_squared = dx*dx + dy*dy
260
 
261
  mask = np.zeros((height, width), dtype=bool)
262
  mask[dist_squared <= r_squared] = True
 
267
  return self.display_image, "Error: No pixels selected"
268
 
269
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
270
+ area = np.sum(mask) * (pixel_spacing ** 2)
 
271
 
272
  mean_value = np.mean(roi_pixels)
273
  std_dev = np.std(roi_pixels, ddof=1)
 
296
 
297
  return self.update_display(), self.format_results()
298
  except Exception as e:
 
299
  return self.display_image, f"Error analyzing ROI: {str(e)}"
300
 
301
  def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
302
+ """Inserts SNR (row1) and CNR (row2) in the next column after col_group."""
 
 
303
  try:
304
  base_col = col_group[1] # Mean column
305
  std_col = col_group[2] # StdDev column
306
 
307
  row1, row2 = row_pair
308
 
 
 
309
  formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
310
+
311
+ # SNR in row1
312
+ formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
313
  cell1 = ws[f"{formula_col}{row1}"]
314
  cell1.value = formula1
315
  cell1.font = red_font
316
  cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
317
 
318
+ # CNR in row2
319
  formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
320
  cell2 = ws[f"{formula_col}{row2}"]
321
  cell2.value = formula2
322
  cell2.font = red_font
323
  cell2.alignment = openpyxl.styles.Alignment(horizontal='center')
324
+
 
325
  except Exception as e:
326
  logger.error(f"Error adding formulas: {str(e)}")
327
 
 
 
 
328
  def save_formatted_results(self, output_path):
329
  try:
330
  if not self.results:
331
  return None, "No results to save"
332
 
 
333
  wb = openpyxl.Workbook()
334
  ws = wb.active
335
  red_font = openpyxl.styles.Font(color="FF0000")
 
346
  ('BV', 'BW', 'BX', 'BY', 'BZ')
347
  ]
348
 
349
+ # Write main headers (Area,Mean,StdDev,Min,Max)
350
  for cols in column_groups:
351
  for i, header in enumerate(headers):
352
  cell = ws[f"{cols[i]}1"]
353
  cell.value = header
354
  cell.alignment = center_alignment
355
 
356
+ # Each pair for phantom
357
  row_pairs = [
358
  (2, 3), (5, 6), (8, 9), (11, 12), (14, 15),
359
  (17, 18), (20, 21), (23, 24), (26, 27), (29, 30)
 
364
  '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
365
  ]
366
 
367
+ # Label each phantom in column A
368
  for i, size in enumerate(phantom_sizes):
369
+ label_row = row_pairs[i][0] - 1
370
+ ws.cell(row=label_row, column=1, value=size).font = red_font
371
+ ws.cell(row=label_row, column=1).alignment = center_alignment
372
 
373
+ # Fill raw ROI data + formulas
374
  result_idx = 0
375
  current_col_group = 0
376
  current_row_pair = 0
 
380
  break
381
 
382
  cols = column_groups[current_col_group]
383
+ row1, row2 = row_pairs[current_row_pair]
384
 
385
  if result_idx < len(self.results):
386
+ self._write_result_to_cells(ws, self.results[result_idx], cols, row1)
 
387
  result_idx += 1
388
 
389
  if result_idx < len(self.results):
390
+ self._write_result_to_cells(ws, self.results[result_idx], cols, row2)
 
391
  result_idx += 1
392
 
393
+ self.add_formulas_to_template(ws, (row1,row2), cols, red_font)
394
 
395
  current_col_group += 1
396
  if current_col_group >= len(column_groups):
397
  current_col_group = 0
398
  current_row_pair += 1
399
 
400
+ # Center alignment for the raw data
401
  for cols in column_groups:
402
  for col in cols:
403
  for row in range(2, 31):
404
  cell = ws[f"{col}{row}"]
405
+ if cell.value not in [None, '']:
406
  cell.alignment = center_alignment
407
 
408
+ # Now the 1-AVG table from row35..45
 
 
 
 
 
409
  start_row = 35
 
 
410
  ws['C35'] = "1-AVG"
411
  ws['C35'].alignment = center_alignment
412
 
413
+ # Merge headers
414
  ws.merge_cells('D35:E35')
415
  ws.merge_cells('F35:G35')
416
  ws.merge_cells('H35:I35')
417
 
 
418
  avg_headers = {
419
  'D35': 'AVG MEAN',
420
  'F35': 'AVG STDDEV',
 
425
  ws[c_ref].font = red_font
426
  ws[c_ref].alignment = center_alignment
427
 
428
+ # Phantom sizes in 1-AVG
429
  phantom_sizes2 = [
430
  '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
431
  '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
 
433
 
434
  for i, p_size in enumerate(phantom_sizes2):
435
  row = start_row + i + 1 # 36..45
 
 
436
  ws.merge_cells(f'D{row}:E{row}')
437
  ws.merge_cells(f'F{row}:G{row}')
438
  ws.merge_cells(f'H{row}:I{row}')
439
 
440
+ c_cell = ws[f'C{row}']
441
+ c_cell.value = p_size
442
+ c_cell.font = red_font
443
+ c_cell.alignment = center_alignment
444
+
445
+ if i >= len(row_pairs):
446
+ continue
447
+ (raw_row1, raw_row2) = row_pairs[i]
 
 
 
 
 
 
 
 
 
448
 
449
  mean_values = []
450
  stddev_values = []
451
  cnr_values = []
452
 
453
+ # Read from the same column groups
454
  for group in column_groups:
455
+ mean_col = group[1] # e.g. 'C'
456
+ std_col = group[2] # e.g. 'D'
457
+
458
+ # read mean from row1 if you want
459
+ val_mean = ws[f"{mean_col}{raw_row1}"].value
460
+ # read std from row1 or row2 if you want
461
+ val_std = ws[f"{std_col}{raw_row1}"].value
462
 
463
+ # Read the actual CNR formula cell from row2
464
+ formula_col = get_column_letter(column_index_from_string(group[-1]) + 1)
465
+ val_cnr = ws[f"{formula_col}{raw_row2}"].value
 
 
466
 
467
+ # Convert to float if possible
468
  try:
469
+ val_mean = float(val_mean) if val_mean not in [None, ''] else None
470
+ val_std = float(val_std) if val_std not in [None, ''] else None
471
+ val_cnr = float(val_cnr) if val_cnr not in [None, ''] else None
472
  except:
473
+ val_mean,val_std,val_cnr = None,None,None
474
+
475
+ if val_mean is not None:
476
+ mean_values.append(val_mean)
477
+ if val_std is not None:
478
+ stddev_values.append(val_std)
479
+ if val_cnr is not None:
480
+ cnr_values.append(val_cnr)
 
 
 
 
 
 
 
 
 
 
 
 
 
481
 
482
+ # compute final averages
483
+ final_mean = sum(mean_values)/len(mean_values) if mean_values else None
484
+ final_std = sum(stddev_values)/len(stddev_values) if stddev_values else None
485
+ final_cnr = sum(cnr_values)/len(cnr_values) if cnr_values else None
486
+
487
+ # place them
488
  if final_mean is not None:
489
  ws[f'D{row}'].value = final_mean
490
  ws[f'D{row}'].alignment = center_alignment
 
500
  ws[f'H{row}'].alignment = center_alignment
501
  ws[f'H{row}'].number_format = '0.0000'
502
 
503
+ # Put borders
504
  thin_side = openpyxl.styles.Side(style='thin')
505
  border = openpyxl.styles.Border(
506
  left=thin_side, right=thin_side, top=thin_side, bottom=thin_side
 
514
  except Exception as e:
515
  logger.error(f"Error saving formatted results: {str(e)}")
516
  return None, f"Error saving results: {str(e)}"
 
517
 
518
  def _write_result_to_cells(self, ws, result, cols, row):
519
  center_alignment = openpyxl.styles.Alignment(horizontal='center')
 
520
  value_mapping = {
521
  'Area': 'Area (mm²)',
522
  'Mean': 'Mean',
 
524
  'Min': 'Min',
525
  'Max': 'Max'
526
  }
 
527
  for i, (header, key) in enumerate(value_mapping.items()):
528
  cell = ws[f"{cols[i]}{row}"]
529
  val = result[key]
 
562
  return image, self.format_results()
563
 
564
  def undo_last(self, image):
565
+ if not self.results:
566
  return self.update_display(), self.format_results()
 
567
  last_result = self.results[-1]
568
+ is_measurement = (last_result['Point'] != '(0, 0)')
 
 
 
569
  self.results.pop()
 
 
570
  if is_measurement and self.marks:
571
  self.marks.pop()
 
572
  return self.update_display(), self.format_results()
573
 
574
 
575
  def create_interface():
 
576
  analyzer = DicomAnalyzer()
577
 
578
  with gr.Blocks(css="#image_display { outline: none; }") as interface:
 
581
  with gr.Row():
582
  with gr.Column():
583
  file_input = gr.File(label="Upload DICOM file")
584
+ diameter_slider = gr.Slider(1, 20, 9, 1, label="ROI Diameter (pixels)")
 
 
 
 
 
 
585
 
586
  with gr.Row():
587
  zoom_in_btn = gr.Button("Zoom In (+)")
 
590
  reset_all_btn = gr.Button("Reset All")
591
 
592
  with gr.Column():
593
+ image_display = gr.Image(label="DICOM Image", interactive=True, elem_id="image_display")
 
 
 
 
594
 
595
  with gr.Row():
596
  zero_btn = gr.Button("Add Zero Row")
 
605
 
606
  gr.Markdown("""
607
  ### Controls:
608
+ - Arrow keys to pan when zoomed in.
609
  - Click points to measure ROI.
610
+ - Zoom In/Out, Reset, etc.
611
+ - "Save Results": raw data
612
+ - "Save Formatted Results": advanced table
 
613
  """)
614
 
615
  def update_diameter(x):
616
  analyzer.circle_diameter = float(x)
 
617
  return f"Diameter set to {x} pixels"
618
 
619
  def save_formatted():
620
+ return analyzer.save_formatted_results("analysis_results_formatted.xlsx")
621
+
622
+ file_input.change(fn=analyzer.load_dicom, inputs=file_input, outputs=[image_display, results_display])
623
+ image_display.select(fn=analyzer.analyze_roi, outputs=[image_display, results_display])
624
+ diameter_slider.change(fn=update_diameter, inputs=diameter_slider, outputs=gr.Textbox(label="Status"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
 
626
+ zoom_in_btn.click(fn=analyzer.zoom_in, inputs=image_display, outputs=image_display, queue=False)
627
+ zoom_out_btn.click(fn=analyzer.zoom_out, inputs=image_display, outputs=image_display, queue=False)
628
+ reset_btn.click(fn=analyzer.reset_view, outputs=image_display)
629
+ reset_all_btn.click(fn=analyzer.reset_all, inputs=image_display, outputs=[image_display, results_display])
 
630
 
631
+ key_press.change(fn=analyzer.handle_keyboard, inputs=key_press, outputs=image_display)
 
 
 
 
632
 
633
+ zero_btn.click(fn=analyzer.add_zero_row, inputs=image_display, outputs=[image_display, results_display])
634
+ zero2_btn.click(fn=analyzer.add_two_zero_rows, inputs=image_display, outputs=[image_display, results_display])
635
+ undo_btn.click(fn=analyzer.undo_last, inputs=image_display, outputs=[image_display, results_display])
 
 
 
 
 
 
 
 
636
 
637
+ save_btn.click(fn=analyzer.save_results, outputs=[file_output, results_display])
638
+ save_formatted_btn.click(fn=save_formatted, outputs=[file_output, results_display])
 
 
 
 
 
 
 
639
 
640
+ # JS to capture arrow keys
641
  js = """
642
  <script>
643
  document.addEventListener('keydown', function(e) {
644
+ if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
645
  e.preventDefault();
646
+ const el = document.querySelector('#key_press textarea');
647
+ if (el) {
648
+ el.value = e.key;
649
+ el.dispatchEvent(new Event('input'));
650
  setTimeout(() => {
651
+ el.value = '';
652
+ el.dispatchEvent(new Event('input'));
653
  }, 100);
654
  }
655
  }
 
658
  """
659
  gr.HTML(js)
660
 
 
661
  return interface
662
 
663
 
664
  if __name__ == "__main__":
665
  try:
 
666
  interface = create_interface()
 
667
  interface.queue()
668
+ interface.launch(server_name="0.0.0.0", server_port=7860, share=True,
669
+ debug=True, show_error=True, quiet=False)
 
 
 
 
 
 
670
  except Exception as e:
 
671
  logger.error(f"Error launching application: {str(e)}")
672
  logger.error(traceback.format_exc())
673
  raise e