HeshamAI commited on
Commit
8723257
·
verified ·
1 Parent(s): 1929f10

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -167
app.py CHANGED
@@ -48,28 +48,25 @@ def debug_decorator(func):
48
  class DicomAnalyzer:
49
  def __init__(self):
50
  self.results = []
51
- self.circle_diameter = 9.0 # Changed to float for precise calculations
52
  self.zoom_factor = 1.0
53
  self.current_image = None
54
  self.dicom_data = None
55
  self.display_image = None
56
- self.marks = [] # Store (x, y, diameter) for each mark
57
  self.original_image = None
58
  self.original_display = None
59
- # Pan position
60
  self.pan_x = 0
61
  self.pan_y = 0
62
  self.max_pan_x = 0
63
  self.max_pan_y = 0
64
- # Circle color in BGR
65
  self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
66
  print("DicomAnalyzer initialized...")
67
 
68
  def reset_all(self, image):
69
- """Reset all data and view settings"""
70
- self.results = [] # Clear all results
71
- self.marks = [] # Clear all marks
72
- self.reset_view() # Reset view settings
73
  return self.update_display(), "All data has been reset"
74
 
75
  def load_dicom(self, file):
@@ -83,22 +80,17 @@ class DicomAnalyzer:
83
  dicom_data = pydicom.dcmread(file)
84
 
85
  image = dicom_data.pixel_array.astype(np.float32)
86
-
87
- # Store original pixel values before any scaling
88
  self.original_image = image.copy()
89
 
90
- # Apply DICOM scaling for display
91
  rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
92
  rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
93
  image = (image * rescale_slope) + rescale_intercept
94
 
95
  self.current_image = image
96
  self.dicom_data = dicom_data
97
-
98
  self.display_image = self.normalize_image(image)
99
  self.original_display = self.display_image.copy()
100
 
101
- # Reset all data when loading new image
102
  self.reset_all(None)
103
  print("DICOM file loaded successfully")
104
 
@@ -159,11 +151,6 @@ class DicomAnalyzer:
159
  elif key == 'ArrowDown':
160
  self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
161
 
162
- print(f"Pan X: {self.pan_x} (was {original_pan_x})")
163
- print(f"Pan Y: {self.pan_y} (was {original_pan_y})")
164
- print(f"Max Pan X: {self.max_pan_x}")
165
- print(f"Max Pan Y: {self.max_pan_y}")
166
-
167
  return self.update_display()
168
  except Exception as e:
169
  print(f"Error handling keyboard input: {str(e)}")
@@ -189,7 +176,6 @@ class DicomAnalyzer:
189
  height, width = self.original_image.shape[:2]
190
 
191
  Y, X = np.ogrid[:height, :width]
192
-
193
  radius = self.circle_diameter / 2.0
194
  r_squared = radius * radius
195
 
@@ -206,7 +192,6 @@ class DicomAnalyzer:
206
  return self.display_image, "Error: No pixels selected"
207
 
208
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
209
-
210
  n_pixels = np.sum(mask)
211
  area = n_pixels * (pixel_spacing ** 2)
212
 
@@ -239,7 +224,6 @@ class DicomAnalyzer:
239
  except Exception as e:
240
  print(f"Error analyzing ROI: {str(e)}")
241
  return self.display_image, f"Error analyzing ROI: {str(e)}"
242
-
243
  def update_display(self):
244
  try:
245
  if self.original_display is None:
@@ -295,161 +279,110 @@ class DicomAnalyzer:
295
  print(f"Error updating display: {str(e)}")
296
  return self.original_display
297
 
298
- def save_results(self):
299
- """
300
- Basic save function for raw results with improved error handling and logging
301
-
302
- Returns:
303
- tuple: (str, str) - (filepath, message) or (None, error_message)
304
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  try:
306
  if not self.results:
307
- logger.warning("Attempted to save with no results")
308
  return None, "No results to save"
309
 
310
- # Create DataFrame with ordered columns
311
- df = pd.DataFrame(self.results)
312
- columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
313
- df = df[columns_order]
314
-
315
- # Generate timestamp for unique filename
316
- timestamp = time.strftime("%Y%m%d_%H%M%S")
317
- output_file = f"analysis_results_{timestamp}.xlsx"
318
-
319
- # Save with Excel writer to properly format columns
320
- with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
321
- df.to_excel(writer, index=False, sheet_name='Results')
322
-
323
- # Auto-adjust column widths
324
- worksheet = writer.sheets['Results']
325
- for idx, col in enumerate(df.columns):
326
- max_length = max(
327
- df[col].astype(str).apply(len).max(),
328
- len(str(col))
329
- ) + 2
330
- worksheet.column_dimensions[get_column_letter(idx + 1)].width = max_length
331
-
332
- logger.info(f"Results saved successfully to {output_file}")
333
- return output_file, f"Results saved successfully to {output_file}"
334
 
335
- except Exception as e:
336
- error_msg = f"Error saving results: {str(e)}"
337
- logger.error(error_msg)
338
- logger.error(traceback.format_exc())
339
- return None, error_msg
 
 
 
 
 
 
 
340
 
341
- def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
342
- """Add formulas for each row pair and column group"""
343
- try:
344
- # Get base column for formulas
345
- base_col = col_group[1] # Mean column
346
- std_col = col_group[2] # StdDev column
347
-
348
- row1, row2 = row_pair
349
-
350
- # Formula for first row: =Mean/StdDev
351
- formula1 = f"={base_col}{row1}/{std_col}{row1}"
352
- formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
353
- cell1 = ws[f"{formula_col}{row1}"] # Get cell reference first
354
- cell1.value = formula1
355
- cell1.font = red_font
356
-
357
- # Formula for second row: =(Mean1-Mean2)/StdDev2
358
- formula2 = f"=({base_col}{row1}-{base_col}{row2})/{std_col}{row2}"
359
- cell2 = ws[f"{formula_col}{row2}"] # Get cell reference first
360
- cell2.value = formula2
361
- cell2.font = red_font
362
-
363
- logger.debug(f"Added formulas for rows {row1},{row2} in column {formula_col}")
364
- except Exception as e:
365
- logger.error(f"Error adding formulas: {str(e)}")
366
 
367
- def save_formatted_results(self, output_path):
368
- try:
369
- if not self.results:
370
- return None, "No results to save"
371
-
372
- # Create new workbook
373
- wb = openpyxl.Workbook()
374
- ws = wb.active
375
-
376
- # Create font styles
377
- red_font = openpyxl.styles.Font(color="FF0000") # Red color in RGB
378
-
379
- # Define column groups and row pairs
380
- column_groups = [
381
- ('B', 'C', 'D', 'E', 'F'), # First group
382
- ('H', 'I', 'J', 'K', 'L'), # Second group
383
- ('N', 'O', 'P', 'Q', 'R'), # Third group
384
- ('T', 'U', 'V', 'W', 'X'), # Fourth group
385
- ('Z', 'AA', 'AB', 'AC', 'AD'), # Fifth group
386
- ('AF', 'AG', 'AH', 'AI', 'AJ'), # Sixth group
387
- ('AL', 'AM', 'AN', 'AO', 'AP'), # Seventh group
388
- ('AR', 'AS', 'AT', 'AU', 'AV'), # Eighth group
389
- ('AX', 'AY', 'AZ', 'BA', 'BB'), # Ninth group
390
- ('BD', 'BE', 'BF', 'BG', 'BH'), # Tenth group
391
- ('BJ', 'BK', 'BL', 'BM', 'BN'), # Eleventh group
392
- ('BP', 'BQ', 'BR', 'BS', 'BT'), # Twelfth group
393
- ('BV', 'BW', 'BX', 'BY', 'BZ') # Thirteenth group
394
- ]
395
-
396
- row_pairs = [
397
- (2, 3), # 7mm
398
- (5, 6), # 6.5mm
399
- (8, 9), # 6mm
400
- (11, 12), # 5.5mm
401
- (14, 15), # 5mm
402
- (17, 18), # 4.5mm
403
- (20, 21), # 4mm
404
- (23, 24), # 3.5mm
405
- (26, 27), # 3mm
406
- (29, 30) # 2.5mm
407
- ]
408
-
409
- # Add headers with red font
410
- phantom_sizes = ['(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)',
411
- '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)']
412
- for i, size in enumerate(phantom_sizes):
413
- header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
414
- header_cell.font = red_font
415
-
416
- # Process results
417
- result_idx = 0
418
- current_col_group = 0
419
- current_row_pair = 0
420
-
421
- while result_idx < len(self.results):
422
- if current_row_pair >= len(row_pairs):
423
- break
424
-
425
- cols = column_groups[current_col_group]
426
- rows = row_pairs[current_row_pair]
427
-
428
- # Write data and add formulas
429
- if result_idx < len(self.results):
430
- result = self.results[result_idx]
431
- self._write_result_to_cells(ws, result, cols, rows[0])
432
- result_idx += 1
433
-
434
- if result_idx < len(self.results):
435
- result = self.results[result_idx]
436
- self._write_result_to_cells(ws, result, cols, rows[1])
437
- result_idx += 1
438
 
439
- # Add formulas for this group
440
- self.add_formulas_to_template(ws, rows, cols, red_font)
441
-
442
- current_col_group += 1
443
- if current_col_group >= len(column_groups):
444
- current_col_group = 0
445
- current_row_pair += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
446
 
447
- wb.save(output_path)
448
- return output_path, f"Results saved successfully ({result_idx} measurements)"
449
 
450
- except Exception as e:
451
- logger.error(f"Error saving formatted results: {str(e)}")
452
- return None, f"Error saving results: {str(e)}"
453
 
454
  def _write_result_to_cells(self, ws, result, cols, row):
455
  """Helper method to write a single result to worksheet cells"""
@@ -468,6 +401,7 @@ def save_formatted_results(self, output_path):
468
  return df.to_string(index=False)
469
 
470
  def add_blank_row(self, image):
 
471
  self.results.append({
472
  'Area (mm²)': '',
473
  'Mean': '',
@@ -479,6 +413,7 @@ def save_formatted_results(self, output_path):
479
  return image, self.format_results()
480
 
481
  def add_zero_row(self, image):
 
482
  self.results.append({
483
  'Area (mm²)': '0.000',
484
  'Mean': '0.000',
@@ -490,12 +425,12 @@ def save_formatted_results(self, output_path):
490
  return image, self.format_results()
491
 
492
  def undo_last(self, image):
 
493
  if self.results:
494
  self.results.pop()
495
  if self.marks:
496
  self.marks.pop()
497
  return self.update_display(), self.format_results()
498
-
499
  def create_interface():
500
  print("Creating interface...")
501
  analyzer = DicomAnalyzer()
 
48
  class DicomAnalyzer:
49
  def __init__(self):
50
  self.results = []
51
+ self.circle_diameter = 9.0
52
  self.zoom_factor = 1.0
53
  self.current_image = None
54
  self.dicom_data = None
55
  self.display_image = None
56
+ self.marks = []
57
  self.original_image = None
58
  self.original_display = None
 
59
  self.pan_x = 0
60
  self.pan_y = 0
61
  self.max_pan_x = 0
62
  self.max_pan_y = 0
 
63
  self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
64
  print("DicomAnalyzer initialized...")
65
 
66
  def reset_all(self, image):
67
+ self.results = []
68
+ self.marks = []
69
+ self.reset_view()
 
70
  return self.update_display(), "All data has been reset"
71
 
72
  def load_dicom(self, file):
 
80
  dicom_data = pydicom.dcmread(file)
81
 
82
  image = dicom_data.pixel_array.astype(np.float32)
 
 
83
  self.original_image = image.copy()
84
 
 
85
  rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
86
  rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
87
  image = (image * rescale_slope) + rescale_intercept
88
 
89
  self.current_image = image
90
  self.dicom_data = dicom_data
 
91
  self.display_image = self.normalize_image(image)
92
  self.original_display = self.display_image.copy()
93
 
 
94
  self.reset_all(None)
95
  print("DICOM file loaded successfully")
96
 
 
151
  elif key == 'ArrowDown':
152
  self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
153
 
 
 
 
 
 
154
  return self.update_display()
155
  except Exception as e:
156
  print(f"Error handling keyboard input: {str(e)}")
 
176
  height, width = self.original_image.shape[:2]
177
 
178
  Y, X = np.ogrid[:height, :width]
 
179
  radius = self.circle_diameter / 2.0
180
  r_squared = radius * radius
181
 
 
192
  return self.display_image, "Error: No pixels selected"
193
 
194
  pixel_spacing = float(self.dicom_data.PixelSpacing[0])
 
195
  n_pixels = np.sum(mask)
196
  area = n_pixels * (pixel_spacing ** 2)
197
 
 
224
  except Exception as e:
225
  print(f"Error analyzing ROI: {str(e)}")
226
  return self.display_image, f"Error analyzing ROI: {str(e)}"
 
227
  def update_display(self):
228
  try:
229
  if self.original_display is None:
 
279
  print(f"Error updating display: {str(e)}")
280
  return self.original_display
281
 
282
+ def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
283
+ """Add formulas for each row pair and column group"""
284
+ try:
285
+ base_col = col_group[1] # Mean column
286
+ std_col = col_group[2] # StdDev column
287
+
288
+ row1, row2 = row_pair
289
+
290
+ # Formula for first row: =Mean/StdDev
291
+ formula1 = f"={base_col}{row1}/{std_col}{row1}"
292
+ formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
293
+ cell1 = ws[f"{formula_col}{row1}"]
294
+ cell1.value = formula1
295
+ cell1.font = red_font
296
+
297
+ # Formula for second row: =(Mean1-Mean2)/StdDev2
298
+ formula2 = f"=({base_col}{row1}-{base_col}{row2})/{std_col}{row2}"
299
+ cell2 = ws[f"{formula_col}{row2}"]
300
+ cell2.value = formula2
301
+ cell2.font = red_font
302
+
303
+ logger.debug(f"Added formulas for rows {row1},{row2} in column {formula_col}")
304
+ except Exception as e:
305
+ logger.error(f"Error adding formulas: {str(e)}")
306
+
307
+ def save_formatted_results(self, output_path):
308
  try:
309
  if not self.results:
 
310
  return None, "No results to save"
311
 
312
+ wb = openpyxl.Workbook()
313
+ ws = wb.active
314
+
315
+ red_font = openpyxl.styles.Font(color="FF0000")
316
+
317
+ column_groups = [
318
+ ('B', 'C', 'D', 'E', 'F'), # First group
319
+ ('H', 'I', 'J', 'K', 'L'), # Second group
320
+ ('N', 'O', 'P', 'Q', 'R'), # Third group
321
+ ('T', 'U', 'V', 'W', 'X'), # Fourth group
322
+ ('Z', 'AA', 'AB', 'AC', 'AD'), # Fifth group
323
+ ('AF', 'AG', 'AH', 'AI', 'AJ'), # Sixth group
324
+ ('AL', 'AM', 'AN', 'AO', 'AP'), # Seventh group
325
+ ('AR', 'AS', 'AT', 'AU', 'AV'), # Eighth group
326
+ ('AX', 'AY', 'AZ', 'BA', 'BB'), # Ninth group
327
+ ('BD', 'BE', 'BF', 'BG', 'BH'), # Tenth group
328
+ ('BJ', 'BK', 'BL', 'BM', 'BN'), # Eleventh group
329
+ ('BP', 'BQ', 'BR', 'BS', 'BT'), # Twelfth group
330
+ ('BV', 'BW', 'BX', 'BY', 'BZ') # Thirteenth group
331
+ ]
 
 
 
 
332
 
333
+ row_pairs = [
334
+ (2, 3), # 7mm
335
+ (5, 6), # 6.5mm
336
+ (8, 9), # 6mm
337
+ (11, 12), # 5.5mm
338
+ (14, 15), # 5mm
339
+ (17, 18), # 4.5mm
340
+ (20, 21), # 4mm
341
+ (23, 24), # 3.5mm
342
+ (26, 27), # 3mm
343
+ (29, 30) # 2.5mm
344
+ ]
345
 
346
+ phantom_sizes = ['(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)',
347
+ '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)']
348
+ for i, size in enumerate(phantom_sizes):
349
+ header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
350
+ header_cell.font = red_font
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
+ result_idx = 0
353
+ current_col_group = 0
354
+ current_row_pair = 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ while result_idx < len(self.results):
357
+ if current_row_pair >= len(row_pairs):
358
+ break
359
+
360
+ cols = column_groups[current_col_group]
361
+ rows = row_pairs[current_row_pair]
362
+
363
+ if result_idx < len(self.results):
364
+ result = self.results[result_idx]
365
+ self._write_result_to_cells(ws, result, cols, rows[0])
366
+ result_idx += 1
367
+
368
+ if result_idx < len(self.results):
369
+ result = self.results[result_idx]
370
+ self._write_result_to_cells(ws, result, cols, rows[1])
371
+ result_idx += 1
372
+
373
+ self.add_formulas_to_template(ws, rows, cols, red_font)
374
+
375
+ current_col_group += 1
376
+ if current_col_group >= len(column_groups):
377
+ current_col_group = 0
378
+ current_row_pair += 1
379
 
380
+ wb.save(output_path)
381
+ return output_path, f"Results saved successfully ({result_idx} measurements)"
382
 
383
+ except Exception as e:
384
+ logger.error(f"Error saving formatted results: {str(e)}")
385
+ return None, f"Error saving results: {str(e)}"
386
 
387
  def _write_result_to_cells(self, ws, result, cols, row):
388
  """Helper method to write a single result to worksheet cells"""
 
401
  return df.to_string(index=False)
402
 
403
  def add_blank_row(self, image):
404
+ """Add a blank row to the results"""
405
  self.results.append({
406
  'Area (mm²)': '',
407
  'Mean': '',
 
413
  return image, self.format_results()
414
 
415
  def add_zero_row(self, image):
416
+ """Add a row of zeros to the results"""
417
  self.results.append({
418
  'Area (mm²)': '0.000',
419
  'Mean': '0.000',
 
425
  return image, self.format_results()
426
 
427
  def undo_last(self, image):
428
+ """Remove the last measurement"""
429
  if self.results:
430
  self.results.pop()
431
  if self.marks:
432
  self.marks.pop()
433
  return self.update_display(), self.format_results()
 
434
  def create_interface():
435
  print("Creating interface...")
436
  analyzer = DicomAnalyzer()