HeshamAI commited on
Commit
73270a3
·
verified ·
1 Parent(s): 118d595

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +225 -175
app.py CHANGED
@@ -61,14 +61,11 @@ 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
65
- self.SMALL_CIRCLES_COLOR = (255, 255, 255) # BGR
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")
@@ -108,9 +105,6 @@ class DicomAnalyzer:
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,9 +135,6 @@ class DicomAnalyzer:
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,9 +152,6 @@ class DicomAnalyzer:
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
@@ -172,19 +160,20 @@ class DicomAnalyzer:
172
  return None
173
 
174
  def zoom_in(self, image):
 
175
  self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
176
  return self.update_display()
177
 
178
  def zoom_out(self, image):
 
179
  self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
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':
189
  self.pan_x = max(0, self.pan_x - pan_amount)
190
  elif key == 'ArrowRight':
@@ -196,12 +185,10 @@ class DicomAnalyzer:
196
 
197
  return self.update_display()
198
  except Exception as e:
 
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,24 +197,43 @@ class DicomAnalyzer:
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)
220
  zoomed_radius = int((diameter / 2.0) * 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,16 +242,17 @@ class DicomAnalyzer:
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,8 +264,8 @@ class DicomAnalyzer:
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))
@@ -270,7 +277,7 @@ class DicomAnalyzer:
270
 
271
  dx = X - x
272
  dy = Y - y
273
- dist_squared = dx*dx + dy*dy
274
 
275
  mask = np.zeros((height, width), dtype=bool)
276
  mask[dist_squared <= r_squared] = True
@@ -281,7 +288,8 @@ class DicomAnalyzer:
281
  return self.display_image, "Error: No pixels selected"
282
 
283
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
284
- area = np.sum(mask) * (pixel_spacing ** 2)
 
285
 
286
  mean_value = np.mean(roi_pixels)
287
  std_dev = np.std(roi_pixels, ddof=1)
@@ -310,63 +318,42 @@ class DicomAnalyzer:
310
 
311
  return self.update_display(), self.format_results()
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
347
  formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
 
348
  cell1 = ws[f"{formula_col}{row1}"]
349
  cell1.value = formula1
350
  cell1.font = red_font
351
  cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
352
 
353
- # CNR in row2
354
  formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
355
  cell2 = ws[f"{formula_col}{row2}"]
356
  cell2.value = formula2
357
  cell2.font = red_font
358
  cell2.alignment = openpyxl.styles.Alignment(horizontal='center')
359
-
 
360
  except Exception as e:
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,60 +361,84 @@ class DicomAnalyzer:
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
421
  if current_col_group >= len(column_groups):
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,86 +450,125 @@ class DicomAnalyzer:
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
 
 
514
  wb.save(output_path)
515
  return output_path, f"Results saved successfully ({len(self.results)} measurements)"
516
  except Exception as e:
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,7 +577,6 @@ class DicomAnalyzer:
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,7 +588,6 @@ class DicomAnalyzer:
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,21 +600,21 @@ class DicomAnalyzer:
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()
 
563
  return self.update_display(), self.format_results()
564
 
565
 
566
  def create_interface():
567
- """
568
- Creates the Gradio interface.
569
- """
570
  print("Creating interface...")
571
  analyzer = DicomAnalyzer()
572
 
@@ -610,10 +658,12 @@ def create_interface():
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):
@@ -701,19 +751,18 @@ def create_interface():
701
  outputs=[file_output, results_display]
702
  )
703
 
704
- # JavaScript to capture arrow keys
705
  js = """
706
  <script>
707
  document.addEventListener('keydown', function(e) {
708
- if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
709
  e.preventDefault();
710
- const el = document.querySelector('#key_press textarea');
711
- if (el) {
712
- el.value = e.key;
713
- el.dispatchEvent(new Event('input'));
714
  setTimeout(() => {
715
- el.value = '';
716
- el.dispatchEvent(new Event('input'));
717
  }, 100);
718
  }
719
  }
@@ -741,6 +790,7 @@ if __name__ == "__main__":
741
  quiet=False
742
  )
743
  except Exception as e:
 
744
  logger.error(f"Error launching application: {str(e)}")
745
  logger.error(traceback.format_exc())
746
  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 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")
 
105
  return self.update_display(), "All data has been reset"
106
 
107
  def load_dicom(self, file):
 
 
 
108
  try:
109
  if file is None:
110
  return None, "No file uploaded"
 
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,
 
152
  return None
153
 
154
  def reset_view(self):
 
 
 
155
  self.zoom_factor = 1.0
156
  self.pan_x = 0
157
  self.pan_y = 0
 
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
 
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):
 
 
 
192
  try:
193
  if self.original_display is None:
194
  return None
 
197
  new_height = int(height * self.zoom_factor)
198
  new_width = int(width * self.zoom_factor)
199
 
200
+ zoomed = cv2.resize(
201
+ self.original_display,
202
+ (new_width, new_height),
203
+ interpolation=cv2.INTER_CUBIC
204
+ )
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
 
 
242
  self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
243
  self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
244
 
245
+ visible = zoomed[
246
+ int(self.pan_y):int(self.pan_y + height),
247
+ int(self.pan_x):int(self.pan_x + width)
248
+ ]
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):
 
 
 
256
  try:
257
  if self.current_image is None:
258
  return None, "No image loaded"
 
264
  y = clicked_y + self.pan_y
265
 
266
  if self.zoom_factor != 1.0:
267
+ x = x / self.zoom_factor
268
+ y = y / self.zoom_factor
269
 
270
  x = int(round(x))
271
  y = int(round(y))
 
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
  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
 
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 in row1
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 in row2
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
+ # التعديل الوحيد: في تجميع الـCNR نقرأ من خلية المعادلة بدلا من (m1 - m2)/std2
355
+ ########################################################################
356
  def save_formatted_results(self, output_path):
 
 
 
 
 
 
357
  try:
358
  if not self.results:
359
  return None, "No results to save"
 
361
  wb = openpyxl.Workbook()
362
  ws = wb.active
363
  red_font = openpyxl.styles.Font(color="FF0000")
364
+ center_alignment = openpyxl.styles.Alignment(horizontal='center')
365
 
 
 
366
  headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
367
  column_groups = [
368
  ('B', 'C', 'D', 'E', 'F'), ('H', 'I', 'J', 'K', 'L'),
369
  ('N', 'O', 'P', 'Q', 'R'), ('T', 'U', 'V', 'W', 'X'),
370
+ ('Z', 'AA', 'AB', 'AC', 'AD'), ('AF', 'AG', 'AH', 'AI', 'AJ'),
371
+ ('AL', 'AM', 'AN', 'AO', 'AP'), ('AR', 'AS', 'AT', 'AU', 'AV'),
372
+ ('AX', 'AY', 'AZ', 'BA', 'BB'), ('BD', 'BE', 'BF', 'BG', 'BH'),
373
+ ('BJ', 'BK', 'BL', 'BM', 'BN'), ('BP', 'BQ', 'BR', 'BS', 'BT'),
374
+ ('BV', 'BW', 'BX', 'BY', 'BZ')
375
  ]
376
+
377
+ # Write table headers in row1
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)
387
+ ]
388
+
389
+ phantom_sizes = [
390
+ '(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)',
391
+ '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
392
+ ]
393
 
394
+ # Phantom labels in column 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
+ # Write ROI results into the defined rows/columns
401
  result_idx = 0
402
  current_col_group = 0
403
  current_row_pair = 0
404
+
405
  while result_idx < len(self.results):
406
  if current_row_pair >= len(row_pairs):
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
+ # Align cells for existing data
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
+ # Now build the "1-AVG" table from row 35
438
  start_row = 35
439
  ws['C35'] = "1-AVG"
440
  ws['C35'].alignment = center_alignment
441
 
 
442
  ws.merge_cells('D35:E35')
443
  ws.merge_cells('F35:G35')
444
  ws.merge_cells('H35:I35')
 
450
  }
451
  for c_ref, text_val in avg_headers.items():
452
  ws[c_ref] = text_val
 
453
  ws[c_ref].font = red_font
454
+ ws[c_ref].alignment = center_alignment
455
+
456
+ phantom_sizes2 = [
457
+ '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
458
+ '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
459
+ ]
460
 
461
+ # For each phantom row, gather Mean, STD, and read the CNR from the formula column
462
+ for i, p_size in enumerate(phantom_sizes2):
463
+ row = start_row + i + 1 # rows 36..45
464
+
465
+ ws.merge_cells(f'D{row}:E{row}')
466
+ ws.merge_cells(f'F{row}:G{row}')
467
+ ws.merge_cells(f'H{row}:I{row}')
468
+
469
+ size_cell = ws[f'C{row}']
470
+ size_cell.value = p_size
471
+ size_cell.font = red_font
472
+ size_cell.alignment = center_alignment
473
+
474
+ if i >= len(row_pairs):
475
+ continue
476
+
477
+ (raw_row1, raw_row2) = row_pairs[i]
478
+
479
+ mean_values = []
480
+ stddev_values = []
481
+ cnr_values = []
482
+
483
+ # بدل ما نحسب (m1 - m2)/std2 مباشرة
484
+ # سنقرأ خلية المعادلة في row2 لكل مجموعة
485
+ for group in column_groups:
486
+ mean_col = group[1] # 'C' مثلاً
487
+ std_col = group[2] # 'D' مثلاً
488
+
489
+ # Read mean from row1 (if you want to average them)
490
+ m1_val = ws[f"{mean_col}{raw_row1}"].value
491
+ if m1_val not in [None,'']:
492
+ try:
493
+ mean_values.append(float(m1_val))
494
+ except:
495
+ pass
496
+
497
+ # Read std from row1 (if you want to average them)
498
+ std_val = ws[f"{std_col}{raw_row1}"].value
499
+ if std_val not in [None,'']:
500
+ try:
501
+ stddev_values.append(float(std_val))
502
+ except:
503
+ pass
504
+
505
+ # ---- الأهم: CNR يأتي من صيغة المعادلة (العمود الذي يلي group[-1]) في الصف الثاني ----
506
+ formula_col_index = column_index_from_string(group[-1]) + 1
507
+ formula_col = get_column_letter(formula_col_index)
508
+ cnr_cell_value = ws[f"{formula_col}{raw_row2}"].value # حيث توجد =IFERROR((Mean1-Mean2)/Std2,"")
509
+
510
+ if cnr_cell_value not in [None,'']:
511
+ try:
512
+ cnr_values.append(float(cnr_cell_value))
513
+ except:
514
+ pass
515
+
516
+ # احسب المتوسطات
517
+ final_mean = (sum(mean_values)/len(mean_values)) if mean_values else None
518
+ final_std = (sum(stddev_values)/len(stddev_values)) if stddev_values else None
519
+ final_cnr = (sum(cnr_values)/len(cnr_values)) if cnr_values else None
520
+
521
+ # اكتبهم في الجدول
522
+ if final_mean is not None:
523
+ ws[f'D{row}'].value = final_mean
524
+ ws[f'D{row}'].alignment = center_alignment
525
+ ws[f'D{row}'].number_format = '0.0000'
526
+
527
+ if final_std is not None:
528
+ ws[f'F{row}'].value = final_std
529
+ ws[f'F{row}'].alignment = center_alignment
530
+ ws[f'F{row}'].number_format = '0.0000'
531
+
532
+ if final_cnr is not None:
533
+ ws[f'H{row}'].value = final_cnr
534
+ ws[f'H{row}'].alignment = center_alignment
535
+ ws[f'H{row}'].number_format = '0.0000'
536
+
537
+ # Border around C35..I45
538
+ thin_side = openpyxl.styles.Side(style='thin')
539
  border = openpyxl.styles.Border(
540
+ left=thin_side, right=thin_side, top=thin_side, bottom=thin_side
 
 
 
541
  )
542
+ for r in range(35, 46):
543
  for c in ['C','D','E','F','G','H','I']:
544
  ws[f'{c}{r}'].border = border
545
 
546
+ # Finally save
547
  wb.save(output_path)
548
  return output_path, f"Results saved successfully ({len(self.results)} measurements)"
549
  except Exception as e:
550
  logger.error(f"Error saving formatted results: {str(e)}")
551
  return None, f"Error saving results: {str(e)}"
552
+ ########################################################################
553
+
554
+ def _write_result_to_cells(self, ws, result, cols, row):
555
+ center_alignment = openpyxl.styles.Alignment(horizontal='center')
556
+
557
+ value_mapping = {
558
+ 'Area': 'Area (mm²)',
559
+ 'Mean': 'Mean',
560
+ 'StdDev': 'StdDev',
561
+ 'Min': 'Min',
562
+ 'Max': 'Max'
563
+ }
564
+
565
+ for i, (header, key) in enumerate(value_mapping.items()):
566
+ cell = ws[f"{cols[i]}{row}"]
567
+ val = result[key]
568
+ cell.value = float(val) if val not in ['', None] else ''
569
+ cell.alignment = center_alignment
570
 
571
  def format_results(self):
 
572
  if not self.results:
573
  return "No measurements yet"
574
  df = pd.DataFrame(self.results)
 
577
  return df.to_string(index=False)
578
 
579
  def add_zero_row(self, image):
 
580
  self.results.append({
581
  'Area (mm²)': '0.000',
582
  'Mean': '0.000',
 
588
  return image, self.format_results()
589
 
590
  def add_two_zero_rows(self, image):
 
591
  for _ in range(2):
592
  self.results.append({
593
  'Area (mm²)': '0.000',
 
600
  return image, self.format_results()
601
 
602
  def undo_last(self, image):
603
+ if not self.results:
 
604
  return self.update_display(), self.format_results()
605
+
606
  last_result = self.results[-1]
607
  is_measurement = last_result['Point'] != '(0, 0)'
608
+
609
  self.results.pop()
610
+
611
  if is_measurement and self.marks:
612
  self.marks.pop()
613
+
614
  return self.update_display(), self.format_results()
615
 
616
 
617
  def create_interface():
 
 
 
618
  print("Creating interface...")
619
  analyzer = DicomAnalyzer()
620
 
 
658
 
659
  gr.Markdown("""
660
  ### Controls:
661
+ - Use arrow keys to pan when zoomed in. Movement is now larger.
662
+ - Click points to measure ROI.
663
+ - Use Zoom In/Out buttons or Reset View to adjust zoom level.
664
+ - Use Reset All to clear all measurements.
665
+ - "Save Results": basic Excel with raw data.
666
+ - "Save Formatted Results": Excel with advanced formatting & the 1-AVG table (including correct CNR averaging).
667
  """)
668
 
669
  def update_diameter(x):
 
751
  outputs=[file_output, results_display]
752
  )
753
 
 
754
  js = """
755
  <script>
756
  document.addEventListener('keydown', function(e) {
757
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
758
  e.preventDefault();
759
+ const keyPressElement = document.querySelector('#key_press textarea');
760
+ if (keyPressElement) {
761
+ keyPressElement.value = e.key;
762
+ keyPressElement.dispatchEvent(new Event('input'));
763
  setTimeout(() => {
764
+ keyPressElement.value = '';
765
+ keyPressElement.dispatchEvent(new Event('input'));
766
  }, 100);
767
  }
768
  }
 
790
  quiet=False
791
  )
792
  except Exception as e:
793
+ print(f"Error launching application: {str(e)}")
794
  logger.error(f"Error launching application: {str(e)}")
795
  logger.error(traceback.format_exc())
796
  raise e