Update app.py
Browse files
app.py
CHANGED
|
@@ -45,42 +45,6 @@ def debug_decorator(func):
|
|
| 45 |
logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
|
| 46 |
return wrapper
|
| 47 |
|
| 48 |
-
def save_results(self):
|
| 49 |
-
"""
|
| 50 |
-
Basic save function for raw results with improved error handling and logging
|
| 51 |
-
"""
|
| 52 |
-
try:
|
| 53 |
-
if not self.results:
|
| 54 |
-
logger.warning("Attempted to save with no results")
|
| 55 |
-
return None, "No results to save"
|
| 56 |
-
|
| 57 |
-
df = pd.DataFrame(self.results)
|
| 58 |
-
columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
|
| 59 |
-
df = df[columns_order]
|
| 60 |
-
|
| 61 |
-
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 62 |
-
output_file = f"analysis_results_{timestamp}.xlsx"
|
| 63 |
-
|
| 64 |
-
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
| 65 |
-
df.to_excel(writer, index=False, sheet_name='Results')
|
| 66 |
-
|
| 67 |
-
worksheet = writer.sheets['Results']
|
| 68 |
-
for idx, col in enumerate(df.columns):
|
| 69 |
-
max_length = max(
|
| 70 |
-
df[col].astype(str).apply(len).max(),
|
| 71 |
-
len(str(col))
|
| 72 |
-
) + 2
|
| 73 |
-
worksheet.column_dimensions[get_column_letter(idx + 1)].width = max_length
|
| 74 |
-
|
| 75 |
-
logger.info(f"Results saved successfully to {output_file}")
|
| 76 |
-
return output_file, f"Results saved successfully to {output_file}"
|
| 77 |
-
|
| 78 |
-
except Exception as e:
|
| 79 |
-
error_msg = f"Error saving results: {str(e)}"
|
| 80 |
-
logger.error(error_msg)
|
| 81 |
-
logger.error(traceback.format_exc())
|
| 82 |
-
return None, error_msg
|
| 83 |
-
|
| 84 |
|
| 85 |
class DicomAnalyzer:
|
| 86 |
def __init__(self):
|
|
@@ -99,6 +63,43 @@ class DicomAnalyzer:
|
|
| 99 |
self.max_pan_y = 0
|
| 100 |
self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
|
| 101 |
print("DicomAnalyzer initialized...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
def reset_all(self, image):
|
| 103 |
self.results = []
|
| 104 |
self.marks = []
|
|
@@ -198,8 +199,11 @@ class DicomAnalyzer:
|
|
| 198 |
new_height = int(height * self.zoom_factor)
|
| 199 |
new_width = int(width * self.zoom_factor)
|
| 200 |
|
| 201 |
-
zoomed = cv2.resize(
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
|
| 205 |
|
|
@@ -208,24 +212,28 @@ class DicomAnalyzer:
|
|
| 208 |
zoomed_y = int(y * self.zoom_factor)
|
| 209 |
zoomed_radius = int((diameter/2.0) * self.zoom_factor)
|
| 210 |
|
| 211 |
-
cv2.circle(
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
| 217 |
|
| 218 |
num_points = 8
|
| 219 |
for i in range(num_points):
|
| 220 |
angle = 2 * np.pi * i / num_points
|
| 221 |
point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
|
| 222 |
point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
|
| 223 |
-
cv2.circle(
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
| 229 |
|
| 230 |
zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
|
| 231 |
|
|
@@ -243,6 +251,7 @@ class DicomAnalyzer:
|
|
| 243 |
except Exception as e:
|
| 244 |
print(f"Error updating display: {str(e)}")
|
| 245 |
return self.original_display
|
|
|
|
| 246 |
def analyze_roi(self, evt: gr.SelectData):
|
| 247 |
try:
|
| 248 |
if self.current_image is None:
|
|
@@ -318,7 +327,7 @@ class DicomAnalyzer:
|
|
| 318 |
|
| 319 |
row1, row2 = row_pair
|
| 320 |
|
| 321 |
-
# SNR Formula for first row with
|
| 322 |
formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
|
| 323 |
formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
|
| 324 |
cell1 = ws[f"{formula_col}{row1}"]
|
|
@@ -326,7 +335,7 @@ class DicomAnalyzer:
|
|
| 326 |
cell1.font = red_font
|
| 327 |
cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 328 |
|
| 329 |
-
# CNR Formula for second row with
|
| 330 |
formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
|
| 331 |
cell2 = ws[f"{formula_col}{row2}"]
|
| 332 |
cell2.value = formula2
|
|
@@ -372,8 +381,10 @@ class DicomAnalyzer:
|
|
| 372 |
(17, 18), (20, 21), (23, 24), (26, 27), (29, 30)
|
| 373 |
]
|
| 374 |
|
| 375 |
-
phantom_sizes = [
|
| 376 |
-
|
|
|
|
|
|
|
| 377 |
|
| 378 |
for i, size in enumerate(phantom_sizes):
|
| 379 |
header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
|
|
@@ -425,15 +436,15 @@ class DicomAnalyzer:
|
|
| 425 |
stddev_header.alignment = center_alignment
|
| 426 |
current_row += 1
|
| 427 |
|
| 428 |
-
# Calculate StdDev averages for each row pair
|
| 429 |
for i, size in enumerate(phantom_sizes):
|
| 430 |
-
row_number = row_pairs[i][0] # First row
|
| 431 |
stddev_values = []
|
| 432 |
|
| 433 |
for cols in column_groups:
|
| 434 |
stddev_col = cols[2] # StdDev column
|
| 435 |
cell_value = ws[f"{stddev_col}{row_number}"].value
|
| 436 |
-
if cell_value not in [0, None, '']: # Ignore zeros and empty
|
| 437 |
stddev_values.append(float(cell_value))
|
| 438 |
|
| 439 |
if stddev_values:
|
|
@@ -454,23 +465,22 @@ class DicomAnalyzer:
|
|
| 454 |
cnr_header.alignment = center_alignment
|
| 455 |
current_row += 1
|
| 456 |
|
| 457 |
-
# Calculate CNR averages for each row pair
|
| 458 |
for i, size in enumerate(phantom_sizes):
|
| 459 |
-
row_number = row_pairs[i][1] #
|
| 460 |
valid_cnr_refs = []
|
| 461 |
|
| 462 |
-
#
|
| 463 |
for cols in column_groups:
|
| 464 |
formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
|
| 465 |
mean_col = cols[1]
|
| 466 |
std_col = cols[2]
|
| 467 |
|
| 468 |
-
# Check values used in CNR calculation
|
| 469 |
mean1 = ws[f"{mean_col}{row_pairs[i][0]}"].value
|
| 470 |
mean2 = ws[f"{mean_col}{row_pairs[i][1]}"].value
|
| 471 |
std2 = ws[f"{std_col}{row_pairs[i][1]}"].value
|
| 472 |
|
| 473 |
-
# Convert values to float
|
| 474 |
try:
|
| 475 |
mean1 = float(mean1) if mean1 not in [None, ''] else 0
|
| 476 |
mean2 = float(mean2) if mean2 not in [None, ''] else 0
|
|
@@ -478,16 +488,14 @@ class DicomAnalyzer:
|
|
| 478 |
except (ValueError, TypeError):
|
| 479 |
continue
|
| 480 |
|
| 481 |
-
#
|
| 482 |
if not (mean1 == 0 and mean2 == 0 and std2 == 0):
|
| 483 |
valid_cnr_refs.append(f"{formula_col}{row_number}")
|
| 484 |
|
| 485 |
-
# Add row to the averages table
|
| 486 |
size_cell = ws.cell(row=current_row, column=1, value=size)
|
| 487 |
size_cell.alignment = center_alignment
|
| 488 |
|
| 489 |
if valid_cnr_refs:
|
| 490 |
-
# Create array formula for average that handles errors
|
| 491 |
refs = ",".join(valid_cnr_refs)
|
| 492 |
array_formula = f'=IFERROR(AVERAGE(IF(ISNUMBER({refs}),{refs})),"")'
|
| 493 |
|
|
@@ -510,6 +518,7 @@ class DicomAnalyzer:
|
|
| 510 |
except Exception as e:
|
| 511 |
logger.error(f"Error saving formatted results: {str(e)}")
|
| 512 |
return None, f"Error saving results: {str(e)}"
|
|
|
|
| 513 |
def _write_result_to_cells(self, ws, result, cols, row):
|
| 514 |
"""Helper method to write a single result to worksheet cells"""
|
| 515 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
|
@@ -565,6 +574,7 @@ class DicomAnalyzer:
|
|
| 565 |
self.marks.pop()
|
| 566 |
return self.update_display(), self.format_results()
|
| 567 |
|
|
|
|
| 568 |
def create_interface():
|
| 569 |
print("Creating interface...")
|
| 570 |
analyzer = DicomAnalyzer()
|
|
@@ -615,7 +625,7 @@ def create_interface():
|
|
| 615 |
- Use Reset All to clear all measurements
|
| 616 |
- Save Formatted Results will create Excel file with formulas
|
| 617 |
""")
|
| 618 |
-
|
| 619 |
def update_diameter(x):
|
| 620 |
analyzer.circle_diameter = float(x)
|
| 621 |
print(f"Diameter updated to: {x}")
|
|
@@ -739,4 +749,4 @@ if __name__ == "__main__":
|
|
| 739 |
print(f"Error launching application: {str(e)}")
|
| 740 |
logger.error(f"Error launching application: {str(e)}")
|
| 741 |
logger.error(traceback.format_exc())
|
| 742 |
-
raise e
|
|
|
|
| 45 |
logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
|
| 46 |
return wrapper
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
class DicomAnalyzer:
|
| 50 |
def __init__(self):
|
|
|
|
| 63 |
self.max_pan_y = 0
|
| 64 |
self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
|
| 65 |
print("DicomAnalyzer initialized...")
|
| 66 |
+
|
| 67 |
+
def save_results(self):
|
| 68 |
+
"""
|
| 69 |
+
Basic save function for raw results with improved error handling and logging
|
| 70 |
+
"""
|
| 71 |
+
try:
|
| 72 |
+
if not self.results:
|
| 73 |
+
logger.warning("Attempted to save with no results")
|
| 74 |
+
return None, "No results to save"
|
| 75 |
+
|
| 76 |
+
df = pd.DataFrame(self.results)
|
| 77 |
+
columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
|
| 78 |
+
df = df[columns_order]
|
| 79 |
+
|
| 80 |
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 81 |
+
output_file = f"analysis_results_{timestamp}.xlsx"
|
| 82 |
+
|
| 83 |
+
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
| 84 |
+
df.to_excel(writer, index=False, sheet_name='Results')
|
| 85 |
+
|
| 86 |
+
worksheet = writer.sheets['Results']
|
| 87 |
+
for idx, col in enumerate(df.columns):
|
| 88 |
+
max_length = max(
|
| 89 |
+
df[col].astype(str).apply(len).max(),
|
| 90 |
+
len(str(col))
|
| 91 |
+
) + 2
|
| 92 |
+
worksheet.column_dimensions[get_column_letter(idx + 1)].width = max_length
|
| 93 |
+
|
| 94 |
+
logger.info(f"Results saved successfully to {output_file}")
|
| 95 |
+
return output_file, f"Results saved successfully to {output_file}"
|
| 96 |
+
|
| 97 |
+
except Exception as e:
|
| 98 |
+
error_msg = f"Error saving results: {str(e)}"
|
| 99 |
+
logger.error(error_msg)
|
| 100 |
+
logger.error(traceback.format_exc())
|
| 101 |
+
return None, error_msg
|
| 102 |
+
|
| 103 |
def reset_all(self, image):
|
| 104 |
self.results = []
|
| 105 |
self.marks = []
|
|
|
|
| 199 |
new_height = int(height * self.zoom_factor)
|
| 200 |
new_width = int(width * self.zoom_factor)
|
| 201 |
|
| 202 |
+
zoomed = cv2.resize(
|
| 203 |
+
self.original_display,
|
| 204 |
+
(new_width, new_height),
|
| 205 |
+
interpolation=cv2.INTER_CUBIC
|
| 206 |
+
)
|
| 207 |
|
| 208 |
zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
|
| 209 |
|
|
|
|
| 212 |
zoomed_y = int(y * self.zoom_factor)
|
| 213 |
zoomed_radius = int((diameter/2.0) * self.zoom_factor)
|
| 214 |
|
| 215 |
+
cv2.circle(
|
| 216 |
+
zoomed_bgr,
|
| 217 |
+
(zoomed_x, zoomed_y),
|
| 218 |
+
zoomed_radius,
|
| 219 |
+
self.CIRCLE_COLOR,
|
| 220 |
+
1,
|
| 221 |
+
lineType=cv2.LINE_AA
|
| 222 |
+
)
|
| 223 |
|
| 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.CIRCLE_COLOR,
|
| 234 |
+
-1,
|
| 235 |
+
lineType=cv2.LINE_AA
|
| 236 |
+
)
|
| 237 |
|
| 238 |
zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
|
| 239 |
|
|
|
|
| 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:
|
|
|
|
| 327 |
|
| 328 |
row1, row2 = row_pair
|
| 329 |
|
| 330 |
+
# SNR Formula for the first row with IFERROR
|
| 331 |
formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
|
| 332 |
formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
|
| 333 |
cell1 = ws[f"{formula_col}{row1}"]
|
|
|
|
| 335 |
cell1.font = red_font
|
| 336 |
cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 337 |
|
| 338 |
+
# CNR Formula for the second row with IFERROR
|
| 339 |
formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
|
| 340 |
cell2 = ws[f"{formula_col}{row2}"]
|
| 341 |
cell2.value = formula2
|
|
|
|
| 381 |
(17, 18), (20, 21), (23, 24), (26, 27), (29, 30)
|
| 382 |
]
|
| 383 |
|
| 384 |
+
phantom_sizes = [
|
| 385 |
+
'(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)',
|
| 386 |
+
'(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
|
| 387 |
+
]
|
| 388 |
|
| 389 |
for i, size in enumerate(phantom_sizes):
|
| 390 |
header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
|
|
|
|
| 436 |
stddev_header.alignment = center_alignment
|
| 437 |
current_row += 1
|
| 438 |
|
| 439 |
+
# Calculate StdDev averages for each row pair (top row only)
|
| 440 |
for i, size in enumerate(phantom_sizes):
|
| 441 |
+
row_number = row_pairs[i][0] # First row
|
| 442 |
stddev_values = []
|
| 443 |
|
| 444 |
for cols in column_groups:
|
| 445 |
stddev_col = cols[2] # StdDev column
|
| 446 |
cell_value = ws[f"{stddev_col}{row_number}"].value
|
| 447 |
+
if cell_value not in [0, None, '']: # Ignore zeros and empty
|
| 448 |
stddev_values.append(float(cell_value))
|
| 449 |
|
| 450 |
if stddev_values:
|
|
|
|
| 465 |
cnr_header.alignment = center_alignment
|
| 466 |
current_row += 1
|
| 467 |
|
| 468 |
+
# Calculate CNR averages for each row pair (second row)
|
| 469 |
for i, size in enumerate(phantom_sizes):
|
| 470 |
+
row_number = row_pairs[i][1] # second row for CNR
|
| 471 |
valid_cnr_refs = []
|
| 472 |
|
| 473 |
+
# Build references only if Mean and StdDev are not all zero
|
| 474 |
for cols in column_groups:
|
| 475 |
formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
|
| 476 |
mean_col = cols[1]
|
| 477 |
std_col = cols[2]
|
| 478 |
|
|
|
|
| 479 |
mean1 = ws[f"{mean_col}{row_pairs[i][0]}"].value
|
| 480 |
mean2 = ws[f"{mean_col}{row_pairs[i][1]}"].value
|
| 481 |
std2 = ws[f"{std_col}{row_pairs[i][1]}"].value
|
| 482 |
|
| 483 |
+
# Convert potential string values to float
|
| 484 |
try:
|
| 485 |
mean1 = float(mean1) if mean1 not in [None, ''] else 0
|
| 486 |
mean2 = float(mean2) if mean2 not in [None, ''] else 0
|
|
|
|
| 488 |
except (ValueError, TypeError):
|
| 489 |
continue
|
| 490 |
|
| 491 |
+
# If not all zero, we consider the cell for average
|
| 492 |
if not (mean1 == 0 and mean2 == 0 and std2 == 0):
|
| 493 |
valid_cnr_refs.append(f"{formula_col}{row_number}")
|
| 494 |
|
|
|
|
| 495 |
size_cell = ws.cell(row=current_row, column=1, value=size)
|
| 496 |
size_cell.alignment = center_alignment
|
| 497 |
|
| 498 |
if valid_cnr_refs:
|
|
|
|
| 499 |
refs = ",".join(valid_cnr_refs)
|
| 500 |
array_formula = f'=IFERROR(AVERAGE(IF(ISNUMBER({refs}),{refs})),"")'
|
| 501 |
|
|
|
|
| 518 |
except Exception as e:
|
| 519 |
logger.error(f"Error saving formatted results: {str(e)}")
|
| 520 |
return None, f"Error saving results: {str(e)}"
|
| 521 |
+
|
| 522 |
def _write_result_to_cells(self, ws, result, cols, row):
|
| 523 |
"""Helper method to write a single result to worksheet cells"""
|
| 524 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
|
|
|
| 574 |
self.marks.pop()
|
| 575 |
return self.update_display(), self.format_results()
|
| 576 |
|
| 577 |
+
|
| 578 |
def create_interface():
|
| 579 |
print("Creating interface...")
|
| 580 |
analyzer = DicomAnalyzer()
|
|
|
|
| 625 |
- Use Reset All to clear all measurements
|
| 626 |
- Save Formatted Results will create Excel file with formulas
|
| 627 |
""")
|
| 628 |
+
|
| 629 |
def update_diameter(x):
|
| 630 |
analyzer.circle_diameter = float(x)
|
| 631 |
print(f"Diameter updated to: {x}")
|
|
|
|
| 749 |
print(f"Error launching application: {str(e)}")
|
| 750 |
logger.error(f"Error launching application: {str(e)}")
|
| 751 |
logger.error(traceback.format_exc())
|
| 752 |
+
raise e
|