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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +258 -185
app.py CHANGED
@@ -66,8 +66,12 @@ class DicomAnalyzer:
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)
@@ -104,6 +108,9 @@ class DicomAnalyzer:
104
  return self.update_display(), "All data has been reset"
105
 
106
  def load_dicom(self, file):
 
 
 
107
  try:
108
  if file is None:
109
  return None, "No file uploaded"
@@ -134,6 +141,9 @@ class DicomAnalyzer:
134
  return None, f"Error loading DICOM file: {str(e)}"
135
 
136
  def normalize_image(self, image):
 
 
 
137
  try:
138
  normalized = cv2.normalize(
139
  image,
@@ -151,6 +161,9 @@ class DicomAnalyzer:
151
  return None
152
 
153
  def reset_view(self):
 
 
 
154
  self.zoom_factor = 1.0
155
  self.pan_x = 0
156
  self.pan_y = 0
@@ -167,6 +180,9 @@ class DicomAnalyzer:
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':
@@ -183,6 +199,9 @@ class DicomAnalyzer:
183
  return self.display_image
184
 
185
  def update_display(self):
 
 
 
186
  try:
187
  if self.original_display is None:
188
  return None
@@ -191,15 +210,10 @@ class DicomAnalyzer:
191
  new_height = int(height * self.zoom_factor)
192
  new_width = int(width * self.zoom_factor)
193
 
194
- zoomed = cv2.resize(
195
- self.original_display,
196
- (new_width, new_height),
197
- interpolation=cv2.INTER_CUBIC
198
- )
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)
@@ -207,13 +221,13 @@ class DicomAnalyzer:
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
 
@@ -222,16 +236,16 @@ class DicomAnalyzer:
222
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
223
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
224
 
225
- visible = zoomed[
226
- int(self.pan_y):int(self.pan_y + height),
227
- int(self.pan_x):int(self.pan_x + width)
228
- ]
229
-
230
  return visible
231
  except Exception as e:
232
  return self.original_display
233
 
234
  def analyze_roi(self, evt: gr.SelectData):
 
 
 
235
  try:
236
  if self.current_image is None:
237
  return None, "No image loaded"
@@ -243,8 +257,8 @@ class DicomAnalyzer:
243
  y = clicked_y + self.pan_y
244
 
245
  if self.zoom_factor != 1.0:
246
- x = x / self.zoom_factor
247
- y = y / self.zoom_factor
248
 
249
  x = int(round(x))
250
  y = int(round(y))
@@ -298,14 +312,35 @@ class DicomAnalyzer:
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
@@ -326,6 +361,12 @@ class DicomAnalyzer:
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"
@@ -333,63 +374,47 @@ class DicomAnalyzer:
333
  wb = openpyxl.Workbook()
334
  ws = wb.active
335
  red_font = openpyxl.styles.Font(color="FF0000")
336
- center_alignment = openpyxl.styles.Alignment(horizontal='center')
337
 
 
 
338
  headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
339
  column_groups = [
340
  ('B', 'C', 'D', 'E', 'F'), ('H', 'I', 'J', 'K', 'L'),
341
  ('N', 'O', 'P', 'Q', 'R'), ('T', 'U', 'V', 'W', 'X'),
342
- ('Z', 'AA', 'AB', 'AC', 'AD'), ('AF', 'AG', 'AH', 'AI', 'AJ'),
343
- ('AL', 'AM', 'AN', 'AO', 'AP'), ('AR', 'AS', 'AT', 'AU', 'AV'),
344
- ('AX', 'AY', 'AZ', 'BA', 'BB'), ('BD', 'BE', 'BF', 'BG', 'BH'),
345
- ('BJ', 'BK', 'BL', 'BM', 'BN'), ('BP', 'BQ', 'BR', 'BS', 'BT'),
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)
360
- ]
361
 
362
- phantom_sizes = [
363
- '(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)',
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
377
-
378
  while result_idx < len(self.results):
379
  if current_row_pair >= len(row_pairs):
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
@@ -397,20 +422,12 @@ class DicomAnalyzer:
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')
@@ -422,90 +439,75 @@ class DicomAnalyzer:
422
  }
423
  for c_ref, text_val in avg_headers.items():
424
  ws[c_ref] = text_val
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)'
432
- ]
 
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
491
- ws[f'D{row}'].number_format = '0.0000'
492
-
493
- if final_std is not None:
494
- ws[f'F{row}'].value = final_std
495
- ws[f'F{row}'].alignment = center_alignment
496
- ws[f'F{row}'].number_format = '0.0000'
497
-
498
- if final_cnr is not None:
499
- ws[f'H{row}'].value = final_cnr
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
 
 
 
507
  )
508
- for r in range(35, 46):
509
  for c in ['C','D','E','F','G','H','I']:
510
  ws[f'{c}{r}'].border = border
511
 
@@ -515,22 +517,8 @@ class DicomAnalyzer:
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',
523
- 'StdDev': 'StdDev',
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]
530
- cell.value = float(val) if val not in ['', None] else ''
531
- cell.alignment = center_alignment
532
-
533
  def format_results(self):
 
534
  if not self.results:
535
  return "No measurements yet"
536
  df = pd.DataFrame(self.results)
@@ -539,6 +527,7 @@ class DicomAnalyzer:
539
  return df.to_string(index=False)
540
 
541
  def add_zero_row(self, image):
 
542
  self.results.append({
543
  'Area (mm²)': '0.000',
544
  'Mean': '0.000',
@@ -550,6 +539,7 @@ class DicomAnalyzer:
550
  return image, self.format_results()
551
 
552
  def add_two_zero_rows(self, image):
 
553
  for _ in range(2):
554
  self.results.append({
555
  'Area (mm²)': '0.000',
@@ -562,10 +552,11 @@ class DicomAnalyzer:
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()
@@ -573,6 +564,10 @@ class DicomAnalyzer:
573
 
574
 
575
  def create_interface():
 
 
 
 
576
  analyzer = DicomAnalyzer()
577
 
578
  with gr.Blocks(css="#image_display { outline: none; }") as interface:
@@ -581,7 +576,13 @@ def create_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,7 +591,11 @@ def create_interface():
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,39 +610,98 @@ def create_interface():
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) {
@@ -658,15 +722,24 @@ def create_interface():
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())
 
66
  print("DicomAnalyzer initialized...")
67
 
68
  def save_results(self):
69
+ """
70
+ Saves raw ROI data as an Excel file with minimal formatting.
71
+ """
72
  try:
73
  if not self.results:
74
+ logger.warning("Attempted to save with no results")
75
  return None, "No results to save"
76
 
77
  df = pd.DataFrame(self.results)
 
108
  return self.update_display(), "All data has been reset"
109
 
110
  def load_dicom(self, file):
111
+ """
112
+ Loads DICOM from user file and normalizes for display.
113
+ """
114
  try:
115
  if file is None:
116
  return None, "No file uploaded"
 
141
  return None, f"Error loading DICOM file: {str(e)}"
142
 
143
  def normalize_image(self, image):
144
+ """
145
+ Converts raw data to 0..255 range for display.
146
+ """
147
  try:
148
  normalized = cv2.normalize(
149
  image,
 
161
  return None
162
 
163
  def reset_view(self):
164
+ """
165
+ Resets zoom and pan to defaults.
166
+ """
167
  self.zoom_factor = 1.0
168
  self.pan_x = 0
169
  self.pan_y = 0
 
180
  return self.update_display()
181
 
182
  def handle_keyboard(self, key):
183
+ """
184
+ Pans image with arrow keys.
185
+ """
186
  try:
187
  pan_amount = int(10 * self.zoom_factor)
188
  if key == 'ArrowLeft':
 
199
  return self.display_image
200
 
201
  def update_display(self):
202
+ """
203
+ Returns a zoomed/panned copy of original_display with circles drawn.
204
+ """
205
  try:
206
  if self.original_display is None:
207
  return None
 
210
  new_height = int(height * self.zoom_factor)
211
  new_width = int(width * self.zoom_factor)
212
 
213
+ zoomed = cv2.resize(self.original_display, (new_width, new_height), interpolation=cv2.INTER_CUBIC)
 
 
 
 
 
214
  zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
215
 
216
+ # Draw ROI marks
217
  for x, y, diameter in self.marks:
218
  zoomed_x = int(x * self.zoom_factor)
219
  zoomed_y = int(y * self.zoom_factor)
 
221
 
222
  cv2.circle(zoomed_bgr, (zoomed_x, zoomed_y), zoomed_radius, self.CIRCLE_COLOR, 1, lineType=cv2.LINE_AA)
223
 
224
+ # 8 small circles around
225
  num_points = 8
226
  for i in range(num_points):
227
  angle = 2 * np.pi * i / num_points
228
+ px = int(zoomed_x + zoomed_radius * np.cos(angle))
229
+ py = int(zoomed_y + zoomed_radius * np.sin(angle))
230
+ cv2.circle(zoomed_bgr, (px, py), 1, self.SMALL_CIRCLES_COLOR, -1, lineType=cv2.LINE_AA)
231
 
232
  zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
233
 
 
236
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
237
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
238
 
239
+ visible = zoomed[int(self.pan_y):int(self.pan_y + height),
240
+ int(self.pan_x):int(self.pan_x + width)]
 
 
 
241
  return visible
242
  except Exception as e:
243
  return self.original_display
244
 
245
  def analyze_roi(self, evt: gr.SelectData):
246
+ """
247
+ Called when user clicks the image. We gather ROI stats and store them.
248
+ """
249
  try:
250
  if self.current_image is None:
251
  return None, "No image loaded"
 
257
  y = clicked_y + self.pan_y
258
 
259
  if self.zoom_factor != 1.0:
260
+ x /= self.zoom_factor
261
+ y /= self.zoom_factor
262
 
263
  x = int(round(x))
264
  y = int(round(y))
 
312
  except Exception as e:
313
  return self.display_image, f"Error analyzing ROI: {str(e)}"
314
 
315
+ def _write_result_to_cells(self, ws, result, cols, row):
316
+ """
317
+ Writes one ROI measurement into the given row/columns.
318
+ (If you still want to store raw data in some pattern, you can keep this.)
319
+ """
320
+ center_alignment = openpyxl.styles.Alignment(horizontal='center')
321
+ value_mapping = {
322
+ 'Area': 'Area (mm²)',
323
+ 'Mean': 'Mean',
324
+ 'StdDev': 'StdDev',
325
+ 'Min': 'Min',
326
+ 'Max': 'Max'
327
+ }
328
+ for i, (header, key) in enumerate(value_mapping.items()):
329
+ cell = ws[f"{cols[i]}{row}"]
330
+ val = result[key]
331
+ cell.value = float(val) if val not in ['', None] else ''
332
+ cell.alignment = center_alignment
333
+
334
  def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
335
+ """
336
+ Example function: inserts SNR (row1) and CNR (row2) formulas with IFERROR.
337
+ If you don't need them, you can keep or remove them.
338
+ """
339
  try:
340
+ base_col = col_group[1] # Mean col
341
+ std_col = col_group[2] # StdDev col
342
 
343
  row1, row2 = row_pair
 
344
  formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
345
 
346
  # SNR in row1
 
361
  logger.error(f"Error adding formulas: {str(e)}")
362
 
363
  def save_formatted_results(self, output_path):
364
+ """
365
+ Saves an Excel with:
366
+ 1) A normal layout of ROI data if you want (row_pairs, columns).
367
+ 2) A final "1-AVG" table in row35 using *all* measurements in self.results,
368
+ so you see an average even if you only have 1 measurement.
369
+ """
370
  try:
371
  if not self.results:
372
  return None, "No results to save"
 
374
  wb = openpyxl.Workbook()
375
  ws = wb.active
376
  red_font = openpyxl.styles.Font(color="FF0000")
377
+ center_alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
378
 
379
+ # (1) Optionally store the raw data in some pattern if you want
380
+ # Example: We define column_groups, row_pairs, etc.
381
  headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
382
  column_groups = [
383
  ('B', 'C', 'D', 'E', 'F'), ('H', 'I', 'J', 'K', 'L'),
384
  ('N', 'O', 'P', 'Q', 'R'), ('T', 'U', 'V', 'W', 'X'),
385
+ # add more if needed
 
 
 
 
386
  ]
 
 
387
  for cols in column_groups:
388
  for i, header in enumerate(headers):
389
+ ws[f"{cols[i]}1"].value = header
390
+ ws[f"{cols[i]}1"].alignment = center_alignment
 
 
 
 
 
 
 
391
 
392
+ row_pairs = [(2, 3), (5, 6), (8, 9), (11, 12)]
393
+ # (adjust how many pairs you want)
 
 
394
 
395
+ # Write each result into these row/columns as a demonstration
 
 
 
 
 
 
396
  result_idx = 0
397
  current_col_group = 0
398
  current_row_pair = 0
 
399
  while result_idx < len(self.results):
400
  if current_row_pair >= len(row_pairs):
401
  break
 
402
  cols = column_groups[current_col_group]
403
  row1, row2 = row_pairs[current_row_pair]
404
 
405
+ # Write first measurement
406
  if result_idx < len(self.results):
407
+ r = self.results[result_idx]
408
+ self._write_result_to_cells(ws, r, cols, row1)
409
  result_idx += 1
410
 
411
+ # Write second measurement
412
  if result_idx < len(self.results):
413
+ r = self.results[result_idx]
414
+ self._write_result_to_cells(ws, r, cols, row2)
415
  result_idx += 1
416
+
417
+ # Insert formulas if you want
418
  self.add_formulas_to_template(ws, (row1,row2), cols, red_font)
419
 
420
  current_col_group += 1
 
422
  current_col_group = 0
423
  current_row_pair += 1
424
 
425
+ # (2) Now create the 1-AVG table at row35, but read from self.results directly
 
 
 
 
 
 
 
 
426
  start_row = 35
427
  ws['C35'] = "1-AVG"
428
  ws['C35'].alignment = center_alignment
429
 
430
+ # Merge some cells for the headers
431
  ws.merge_cells('D35:E35')
432
  ws.merge_cells('F35:G35')
433
  ws.merge_cells('H35:I35')
 
439
  }
440
  for c_ref, text_val in avg_headers.items():
441
  ws[c_ref] = text_val
 
442
  ws[c_ref].alignment = center_alignment
443
+ ws[c_ref].font = red_font
444
 
445
+ # We just put everything in row 36 for demonstration.
446
+ # Or you could put phantom sizes if you want multiple lines.
447
+ row = 36
448
+ ws.merge_cells(f'D{row}:E{row}')
449
+ ws.merge_cells(f'F{row}:G{row}')
450
+ ws.merge_cells(f'H{row}:I{row}')
451
 
452
+ # Now gather *all* results from self.results
453
+ all_means = []
454
+ all_stds = []
455
+ # We'll do CNR from first two if we have at least 2 measurements
456
+ # Or you can define your own logic if you want multiple pairs
457
+ cnr_value = None
458
+
459
+ for i, r in enumerate(self.results):
460
+ try:
461
+ m = float(r["Mean"])
462
+ s = float(r["StdDev"])
463
+ all_means.append(m)
464
+ all_stds.append(s)
465
+ except:
466
+ pass
467
+
468
+ # If you only have 1 measurement, you still get an average = same value
469
+ avg_mean = sum(all_means)/len(all_means) if all_means else None
470
+ avg_std = sum(all_stds)/len(all_stds) if all_stds else None
471
+
472
+ # For CNR, let's just do a simple logic: if we have >=2 measurements,
473
+ # we do (mean1 - mean2) / std2 as an example
474
+ # or you can define your own approach
475
+ if len(self.results) >= 2:
476
+ try:
477
+ r1 = self.results[0]
478
+ r2 = self.results[1]
479
+ m1 = float(r1["Mean"])
480
+ m2 = float(r2["Mean"])
481
+ s2 = float(r2["StdDev"])
482
+ if s2 != 0:
483
+ cnr_value = (m1 - m2)/s2
484
+ except:
485
+ cnr_value = None
486
+
487
+ # Write them in row 36
488
+ if avg_mean is not None:
489
+ ws[f'D{row}'].value = avg_mean
490
+ ws[f'D{row}'].alignment = center_alignment
491
+ ws[f'D{row}'].number_format = '0.0000'
492
+
493
+ if avg_std is not None:
494
+ ws[f'F{row}'].value = avg_std
495
+ ws[f'F{row}'].alignment = center_alignment
496
+ ws[f'F{row}'].number_format = '0.0000'
497
+
498
+ if cnr_value is not None:
499
+ ws[f'H{row}'].value = cnr_value
500
+ ws[f'H{row}'].alignment = center_alignment
501
+ ws[f'H{row}'].number_format = '0.0000'
502
+
503
+ # Add thin border to the table
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  border = openpyxl.styles.Border(
505
+ left=openpyxl.styles.Side(style='thin'),
506
+ right=openpyxl.styles.Side(style='thin'),
507
+ top=openpyxl.styles.Side(style='thin'),
508
+ bottom=openpyxl.styles.Side(style='thin')
509
  )
510
+ for r in range(35, 37):
511
  for c in ['C','D','E','F','G','H','I']:
512
  ws[f'{c}{r}'].border = border
513
 
 
517
  logger.error(f"Error saving formatted results: {str(e)}")
518
  return None, f"Error saving results: {str(e)}"
519
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  def format_results(self):
521
+ """Just prints self.results as a string in the UI."""
522
  if not self.results:
523
  return "No measurements yet"
524
  df = pd.DataFrame(self.results)
 
527
  return df.to_string(index=False)
528
 
529
  def add_zero_row(self, image):
530
+ """Test function to push a zero row into self.results."""
531
  self.results.append({
532
  'Area (mm²)': '0.000',
533
  'Mean': '0.000',
 
539
  return image, self.format_results()
540
 
541
  def add_two_zero_rows(self, image):
542
+ """Adds 2 zero rows for testing."""
543
  for _ in range(2):
544
  self.results.append({
545
  'Area (mm²)': '0.000',
 
552
  return image, self.format_results()
553
 
554
  def undo_last(self, image):
555
+ """Removes the last measurement (or zero row)."""
556
  if not self.results:
557
  return self.update_display(), self.format_results()
558
  last_result = self.results[-1]
559
+ is_measurement = last_result['Point'] != '(0, 0)'
560
  self.results.pop()
561
  if is_measurement and self.marks:
562
  self.marks.pop()
 
564
 
565
 
566
  def create_interface():
567
+ """
568
+ Creates the Gradio interface.
569
+ """
570
+ print("Creating interface...")
571
  analyzer = DicomAnalyzer()
572
 
573
  with gr.Blocks(css="#image_display { outline: none; }") as interface:
 
576
  with gr.Row():
577
  with gr.Column():
578
  file_input = gr.File(label="Upload DICOM file")
579
+ diameter_slider = gr.Slider(
580
+ minimum=1,
581
+ maximum=20,
582
+ value=9,
583
+ step=1,
584
+ label="ROI Diameter (pixels)"
585
+ )
586
 
587
  with gr.Row():
588
  zoom_in_btn = gr.Button("Zoom In (+)")
 
591
  reset_all_btn = gr.Button("Reset All")
592
 
593
  with gr.Column():
594
+ image_display = gr.Image(
595
+ label="DICOM Image",
596
+ interactive=True,
597
+ elem_id="image_display"
598
+ )
599
 
600
  with gr.Row():
601
  zero_btn = gr.Button("Add Zero Row")
 
610
 
611
  gr.Markdown("""
612
  ### Controls:
613
+ - Use arrow keys to pan when zoomed in.
614
+ - Click on the image to analyze ROI.
615
+ - "Save Results": basic raw data Excel
616
+ - "Save Formatted Results": advanced table with 1-AVG, even if 1 measurement only
 
617
  """)
618
 
619
  def update_diameter(x):
620
  analyzer.circle_diameter = float(x)
621
+ print(f"Diameter updated to: {x}")
622
  return f"Diameter set to {x} pixels"
623
 
624
  def save_formatted():
625
+ output_path = "analysis_results_formatted.xlsx"
626
+ return analyzer.save_formatted_results(output_path)
627
+
628
+ file_input.change(
629
+ fn=analyzer.load_dicom,
630
+ inputs=file_input,
631
+ outputs=[image_display, results_display]
632
+ )
633
+
634
+ image_display.select(
635
+ fn=analyzer.analyze_roi,
636
+ outputs=[image_display, results_display]
637
+ )
638
+
639
+ diameter_slider.change(
640
+ fn=update_diameter,
641
+ inputs=diameter_slider,
642
+ outputs=gr.Textbox(label="Status")
643
+ )
644
 
645
+ zoom_in_btn.click(
646
+ fn=analyzer.zoom_in,
647
+ inputs=image_display,
648
+ outputs=image_display,
649
+ queue=False
650
+ )
651
 
652
+ zoom_out_btn.click(
653
+ fn=analyzer.zoom_out,
654
+ inputs=image_display,
655
+ outputs=image_display,
656
+ queue=False
657
+ )
658
 
659
+ reset_btn.click(
660
+ fn=analyzer.reset_view,
661
+ outputs=image_display
662
+ )
663
 
664
+ reset_all_btn.click(
665
+ fn=analyzer.reset_all,
666
+ inputs=image_display,
667
+ outputs=[image_display, results_display]
668
+ )
669
+
670
+ key_press.change(
671
+ fn=analyzer.handle_keyboard,
672
+ inputs=key_press,
673
+ outputs=image_display
674
+ )
675
+
676
+ zero_btn.click(
677
+ fn=analyzer.add_zero_row,
678
+ inputs=image_display,
679
+ outputs=[image_display, results_display]
680
+ )
681
+
682
+ zero2_btn.click(
683
+ fn=analyzer.add_two_zero_rows,
684
+ inputs=image_display,
685
+ outputs=[image_display, results_display]
686
+ )
687
+
688
+ undo_btn.click(
689
+ fn=analyzer.undo_last,
690
+ inputs=image_display,
691
+ outputs=[image_display, results_display]
692
+ )
693
+
694
+ save_btn.click(
695
+ fn=analyzer.save_results,
696
+ outputs=[file_output, results_display]
697
+ )
698
+
699
+ save_formatted_btn.click(
700
+ fn=save_formatted,
701
+ outputs=[file_output, results_display]
702
+ )
703
 
704
+ # JavaScript to capture arrow keys
705
  js = """
706
  <script>
707
  document.addEventListener('keydown', function(e) {
 
722
  """
723
  gr.HTML(js)
724
 
725
+ print("Interface created successfully")
726
  return interface
727
 
728
 
729
  if __name__ == "__main__":
730
  try:
731
+ print("Starting application...")
732
  interface = create_interface()
733
+ print("Launching interface...")
734
  interface.queue()
735
+ interface.launch(
736
+ server_name="0.0.0.0",
737
+ server_port=7860,
738
+ share=True,
739
+ debug=True,
740
+ show_error=True,
741
+ quiet=False
742
+ )
743
  except Exception as e:
744
  logger.error(f"Error launching application: {str(e)}")
745
  logger.error(traceback.format_exc())