Update app.py
Browse files
app.py
CHANGED
|
@@ -61,7 +61,10 @@ class DicomAnalyzer:
|
|
| 61 |
self.pan_y = 0
|
| 62 |
self.max_pan_x = 0
|
| 63 |
self.max_pan_y = 0
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
print("DicomAnalyzer initialized...")
|
| 66 |
|
| 67 |
def save_results(self):
|
|
@@ -173,12 +176,12 @@ class DicomAnalyzer:
|
|
| 173 |
|
| 174 |
def handle_keyboard(self, key):
|
| 175 |
"""
|
| 176 |
-
|
| 177 |
-
key is expected to be 'ArrowLeft','ArrowRight','ArrowUp','ArrowDown'.
|
| 178 |
"""
|
| 179 |
try:
|
| 180 |
print(f"Handling key press: {key}")
|
| 181 |
-
|
|
|
|
| 182 |
|
| 183 |
if key == 'ArrowLeft':
|
| 184 |
self.pan_x = max(0, self.pan_x - pan_amount)
|
|
@@ -196,7 +199,8 @@ class DicomAnalyzer:
|
|
| 196 |
|
| 197 |
def update_display(self):
|
| 198 |
"""
|
| 199 |
-
Updates the displayed image according to the current zoom/pan and draws the
|
|
|
|
| 200 |
"""
|
| 201 |
try:
|
| 202 |
if self.original_display is None:
|
|
@@ -217,8 +221,9 @@ class DicomAnalyzer:
|
|
| 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(
|
| 223 |
zoomed_bgr,
|
| 224 |
(zoomed_x, zoomed_y),
|
|
@@ -228,7 +233,7 @@ class DicomAnalyzer:
|
|
| 228 |
lineType=cv2.LINE_AA
|
| 229 |
)
|
| 230 |
|
| 231 |
-
# Draw small
|
| 232 |
num_points = 8
|
| 233 |
for i in range(num_points):
|
| 234 |
angle = 2 * np.pi * i / num_points
|
|
@@ -238,7 +243,7 @@ class DicomAnalyzer:
|
|
| 238 |
zoomed_bgr,
|
| 239 |
(point_x, point_y),
|
| 240 |
1,
|
| 241 |
-
self.
|
| 242 |
-1,
|
| 243 |
lineType=cv2.LINE_AA
|
| 244 |
)
|
|
@@ -262,8 +267,7 @@ class DicomAnalyzer:
|
|
| 262 |
|
| 263 |
def analyze_roi(self, evt: gr.SelectData):
|
| 264 |
"""
|
| 265 |
-
Called when user clicks on the displayed image.
|
| 266 |
-
Extracts the ROI of diameter circle_diameter and computes stats.
|
| 267 |
"""
|
| 268 |
try:
|
| 269 |
if self.current_image is None:
|
|
@@ -272,11 +276,11 @@ class DicomAnalyzer:
|
|
| 272 |
clicked_x = evt.index[0]
|
| 273 |
clicked_y = evt.index[1]
|
| 274 |
|
| 275 |
-
# Adjust by
|
| 276 |
x = clicked_x + self.pan_x
|
| 277 |
y = clicked_y + self.pan_y
|
| 278 |
|
| 279 |
-
#
|
| 280 |
if self.zoom_factor != 1.0:
|
| 281 |
x = x / self.zoom_factor
|
| 282 |
y = y / self.zoom_factor
|
|
@@ -291,7 +295,7 @@ class DicomAnalyzer:
|
|
| 291 |
|
| 292 |
dx = X - x
|
| 293 |
dy = Y - y
|
| 294 |
-
dist_squared = dx*dx + dy*dy
|
| 295 |
|
| 296 |
mask = np.zeros((height, width), dtype=bool)
|
| 297 |
mask[dist_squared <= r_squared] = True
|
|
@@ -345,7 +349,7 @@ class DicomAnalyzer:
|
|
| 345 |
|
| 346 |
row1, row2 = row_pair
|
| 347 |
|
| 348 |
-
# SNR
|
| 349 |
formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
|
| 350 |
formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
|
| 351 |
cell1 = ws[f"{formula_col}{row1}"]
|
|
@@ -353,7 +357,7 @@ class DicomAnalyzer:
|
|
| 353 |
cell1.font = red_font
|
| 354 |
cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 355 |
|
| 356 |
-
# CNR
|
| 357 |
formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
|
| 358 |
cell2 = ws[f"{formula_col}{row2}"]
|
| 359 |
cell2.value = formula2
|
|
@@ -367,10 +371,7 @@ class DicomAnalyzer:
|
|
| 367 |
def save_formatted_results(self, output_path):
|
| 368 |
"""
|
| 369 |
Creates an Excel file with results in a formatted table,
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
NOTE: CNR Averages uses AVERAGE() and we skip any cell references where
|
| 373 |
-
Mean1, Mean2, StdDev2 are all zero. So we avoid AVERAGEIF complexities.
|
| 374 |
"""
|
| 375 |
try:
|
| 376 |
if not self.results:
|
|
@@ -381,7 +382,7 @@ class DicomAnalyzer:
|
|
| 381 |
red_font = openpyxl.styles.Font(color="FF0000")
|
| 382 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 383 |
|
| 384 |
-
#
|
| 385 |
headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
|
| 386 |
|
| 387 |
column_groups = [
|
|
@@ -394,7 +395,6 @@ class DicomAnalyzer:
|
|
| 394 |
('BV', 'BW', 'BX', 'BY', 'BZ')
|
| 395 |
]
|
| 396 |
|
| 397 |
-
# Add headers for each column group
|
| 398 |
for cols in column_groups:
|
| 399 |
for i, header in enumerate(headers):
|
| 400 |
cell = ws[f"{cols[i]}1"]
|
|
@@ -411,13 +411,13 @@ class DicomAnalyzer:
|
|
| 411 |
'(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
|
| 412 |
]
|
| 413 |
|
| 414 |
-
#
|
| 415 |
for i, size in enumerate(phantom_sizes):
|
| 416 |
header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
|
| 417 |
header_cell.font = red_font
|
| 418 |
header_cell.alignment = center_alignment
|
| 419 |
|
| 420 |
-
# Fill data from self.results
|
| 421 |
result_idx = 0
|
| 422 |
current_col_group = 0
|
| 423 |
current_row_pair = 0
|
|
@@ -429,19 +429,19 @@ class DicomAnalyzer:
|
|
| 429 |
cols = column_groups[current_col_group]
|
| 430 |
rows = row_pairs[current_row_pair]
|
| 431 |
|
| 432 |
-
# First row
|
| 433 |
if result_idx < len(self.results):
|
| 434 |
result = self.results[result_idx]
|
| 435 |
self._write_result_to_cells(ws, result, cols, rows[0])
|
| 436 |
result_idx += 1
|
| 437 |
|
| 438 |
-
# Second row
|
| 439 |
if result_idx < len(self.results):
|
| 440 |
result = self.results[result_idx]
|
| 441 |
self._write_result_to_cells(ws, result, cols, rows[1])
|
| 442 |
result_idx += 1
|
| 443 |
|
| 444 |
-
#
|
| 445 |
self.add_formulas_to_template(ws, rows, cols, red_font)
|
| 446 |
|
| 447 |
current_col_group += 1
|
|
@@ -457,23 +457,23 @@ class DicomAnalyzer:
|
|
| 457 |
if cell.value is not None:
|
| 458 |
cell.alignment = center_alignment
|
| 459 |
|
| 460 |
-
#
|
| 461 |
current_row = 32
|
| 462 |
|
| 463 |
-
#
|
| 464 |
stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages")
|
| 465 |
stddev_header.font = red_font
|
| 466 |
stddev_header.alignment = center_alignment
|
| 467 |
current_row += 1
|
| 468 |
|
| 469 |
for i, size in enumerate(phantom_sizes):
|
| 470 |
-
row_number = row_pairs[i][0]
|
| 471 |
stddev_values = []
|
| 472 |
|
| 473 |
for cols in column_groups:
|
| 474 |
-
stddev_col = cols[2] # StdDev column
|
| 475 |
cell_value = ws[f"{stddev_col}{row_number}"].value
|
| 476 |
-
if cell_value not in [0, None, '']:
|
| 477 |
stddev_values.append(float(cell_value))
|
| 478 |
|
| 479 |
size_cell = ws.cell(row=current_row, column=1, value=size)
|
|
@@ -486,15 +486,14 @@ class DicomAnalyzer:
|
|
| 486 |
avg_cell.alignment = center_alignment
|
| 487 |
current_row += 1
|
| 488 |
|
| 489 |
-
current_row += 2
|
| 490 |
|
| 491 |
-
#
|
| 492 |
cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages")
|
| 493 |
cnr_header.font = red_font
|
| 494 |
cnr_header.alignment = center_alignment
|
| 495 |
current_row += 1
|
| 496 |
|
| 497 |
-
# حساب متوسط CNR للصف الثاني في كل pair باستخدام AVERAGE فقط
|
| 498 |
for i, size in enumerate(phantom_sizes):
|
| 499 |
row_number = row_pairs[i][1]
|
| 500 |
cnr_cells = []
|
|
@@ -503,7 +502,7 @@ class DicomAnalyzer:
|
|
| 503 |
formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
|
| 504 |
cnr_cell_ref = f"{formula_col}{row_number}"
|
| 505 |
|
| 506 |
-
#
|
| 507 |
mean_col = cols[1]
|
| 508 |
std_col = cols[2]
|
| 509 |
|
|
@@ -518,7 +517,7 @@ class DicomAnalyzer:
|
|
| 518 |
except:
|
| 519 |
mean1_val, mean2_val, std2_val = 0, 0, 0
|
| 520 |
|
| 521 |
-
#
|
| 522 |
if not (mean1_val == 0 and mean2_val == 0 and std2_val == 0):
|
| 523 |
cnr_cells.append(cnr_cell_ref)
|
| 524 |
|
|
@@ -526,7 +525,7 @@ class DicomAnalyzer:
|
|
| 526 |
size_cell.alignment = center_alignment
|
| 527 |
|
| 528 |
if cnr_cells:
|
| 529 |
-
#
|
| 530 |
average_formula = f'=IFERROR(AVERAGE({",".join(cnr_cells)}), "")'
|
| 531 |
|
| 532 |
avg_cell = ws.cell(row=current_row, column=2)
|
|
@@ -536,7 +535,7 @@ class DicomAnalyzer:
|
|
| 536 |
|
| 537 |
current_row += 1
|
| 538 |
|
| 539 |
-
#
|
| 540 |
for row in range(32, current_row):
|
| 541 |
for col in range(1, 3):
|
| 542 |
cell = ws.cell(row=row, column=col)
|
|
@@ -550,7 +549,7 @@ class DicomAnalyzer:
|
|
| 550 |
return None, f"Error saving results: {str(e)}"
|
| 551 |
|
| 552 |
def _write_result_to_cells(self, ws, result, cols, row):
|
| 553 |
-
"""Helper method to write a single result
|
| 554 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 555 |
|
| 556 |
value_mapping = {
|
|
@@ -564,7 +563,6 @@ class DicomAnalyzer:
|
|
| 564 |
for i, (header, key) in enumerate(value_mapping.items()):
|
| 565 |
cell = ws[f"{cols[i]}{row}"]
|
| 566 |
val = result[key]
|
| 567 |
-
# Handle empty strings
|
| 568 |
cell.value = float(val) if val not in ['', None] else ''
|
| 569 |
cell.alignment = center_alignment
|
| 570 |
|
|
@@ -577,18 +575,10 @@ class DicomAnalyzer:
|
|
| 577 |
df = df[columns_order]
|
| 578 |
return df.to_string(index=False)
|
| 579 |
|
| 580 |
-
def add_blank_row(self, image):
|
| 581 |
-
self.results.append({
|
| 582 |
-
'Area (mm²)': '',
|
| 583 |
-
'Mean': '',
|
| 584 |
-
'StdDev': '',
|
| 585 |
-
'Min': '',
|
| 586 |
-
'Max': '',
|
| 587 |
-
'Point': ''
|
| 588 |
-
})
|
| 589 |
-
return image, self.format_results()
|
| 590 |
-
|
| 591 |
def add_zero_row(self, image):
|
|
|
|
|
|
|
|
|
|
| 592 |
self.results.append({
|
| 593 |
'Area (mm²)': '0.000',
|
| 594 |
'Mean': '0.000',
|
|
@@ -601,7 +591,7 @@ class DicomAnalyzer:
|
|
| 601 |
|
| 602 |
def add_two_zero_rows(self, image):
|
| 603 |
"""
|
| 604 |
-
Adds two consecutive rows
|
| 605 |
"""
|
| 606 |
for _ in range(2):
|
| 607 |
self.results.append({
|
|
@@ -653,10 +643,10 @@ def create_interface():
|
|
| 653 |
elem_id="image_display"
|
| 654 |
)
|
| 655 |
|
|
|
|
| 656 |
with gr.Row():
|
| 657 |
-
blank_btn = gr.Button("Add Blank Row")
|
| 658 |
zero_btn = gr.Button("Add Zero Row")
|
| 659 |
-
zero2_btn = gr.Button("Add Two Zero Rows")
|
| 660 |
undo_btn = gr.Button("Undo Last")
|
| 661 |
save_btn = gr.Button("Save Results")
|
| 662 |
save_formatted_btn = gr.Button("Save Formatted Results")
|
|
@@ -667,26 +657,24 @@ def create_interface():
|
|
| 667 |
|
| 668 |
gr.Markdown("""
|
| 669 |
### Controls:
|
| 670 |
-
- Use arrow keys to pan when zoomed in
|
| 671 |
- Click points to measure ROI.
|
| 672 |
- Use Zoom In/Out buttons or Reset View to adjust zoom level.
|
| 673 |
- Use Reset All to clear all measurements.
|
| 674 |
-
- "Save Results"
|
| 675 |
-
- "Save Formatted Results"
|
| 676 |
""")
|
| 677 |
|
| 678 |
-
# Update diameter callback
|
| 679 |
def update_diameter(x):
|
| 680 |
analyzer.circle_diameter = float(x)
|
| 681 |
print(f"Diameter updated to: {x}")
|
| 682 |
return f"Diameter set to {x} pixels"
|
| 683 |
|
| 684 |
-
# Save formatted callback
|
| 685 |
def save_formatted():
|
| 686 |
output_path = "analysis_results_formatted.xlsx"
|
| 687 |
return analyzer.save_formatted_results(output_path)
|
| 688 |
|
| 689 |
-
#
|
| 690 |
file_input.change(
|
| 691 |
fn=analyzer.load_dicom,
|
| 692 |
inputs=file_input,
|
|
@@ -735,19 +723,13 @@ def create_interface():
|
|
| 735 |
outputs=image_display
|
| 736 |
)
|
| 737 |
|
| 738 |
-
blank_btn
|
| 739 |
-
fn=analyzer.add_blank_row,
|
| 740 |
-
inputs=image_display,
|
| 741 |
-
outputs=[image_display, results_display]
|
| 742 |
-
)
|
| 743 |
-
|
| 744 |
zero_btn.click(
|
| 745 |
fn=analyzer.add_zero_row,
|
| 746 |
inputs=image_display,
|
| 747 |
outputs=[image_display, results_display]
|
| 748 |
)
|
| 749 |
-
|
| 750 |
-
# زر إضافة صفين صفر
|
| 751 |
zero2_btn.click(
|
| 752 |
fn=analyzer.add_two_zero_rows,
|
| 753 |
inputs=image_display,
|
|
@@ -771,7 +753,6 @@ def create_interface():
|
|
| 771 |
)
|
| 772 |
|
| 773 |
# JavaScript snippet to allow multiple arrow key presses
|
| 774 |
-
# by resetting the value in key_press.
|
| 775 |
js = """
|
| 776 |
<script>
|
| 777 |
document.addEventListener('keydown', function(e) {
|
|
@@ -779,11 +760,8 @@ def create_interface():
|
|
| 779 |
e.preventDefault();
|
| 780 |
const keyPressElement = document.querySelector('#key_press textarea');
|
| 781 |
if (keyPressElement) {
|
| 782 |
-
// Set the value to the pressed key and dispatch
|
| 783 |
keyPressElement.value = e.key;
|
| 784 |
keyPressElement.dispatchEvent(new Event('input'));
|
| 785 |
-
|
| 786 |
-
// Clear value after short delay so next press is recognized
|
| 787 |
setTimeout(() => {
|
| 788 |
keyPressElement.value = '';
|
| 789 |
keyPressElement.dispatchEvent(new Event('input'));
|
|
|
|
| 61 |
self.pan_y = 0
|
| 62 |
self.max_pan_x = 0
|
| 63 |
self.max_pan_y = 0
|
| 64 |
+
# Main circle color remains yellow:
|
| 65 |
+
self.CIRCLE_COLOR = (0, 255, 255) # BGR format
|
| 66 |
+
# Small circles inside the main circle will be black:
|
| 67 |
+
self.SMALL_CIRCLES_COLOR = (0, 0, 0) # BGR black
|
| 68 |
print("DicomAnalyzer initialized...")
|
| 69 |
|
| 70 |
def save_results(self):
|
|
|
|
| 176 |
|
| 177 |
def handle_keyboard(self, key):
|
| 178 |
"""
|
| 179 |
+
Handle arrow keys. Pan movement is increased to be more noticeable.
|
|
|
|
| 180 |
"""
|
| 181 |
try:
|
| 182 |
print(f"Handling key press: {key}")
|
| 183 |
+
# Increase pan step for bigger movement:
|
| 184 |
+
pan_amount = int(10 * self.zoom_factor)
|
| 185 |
|
| 186 |
if key == 'ArrowLeft':
|
| 187 |
self.pan_x = max(0, self.pan_x - pan_amount)
|
|
|
|
| 199 |
|
| 200 |
def update_display(self):
|
| 201 |
"""
|
| 202 |
+
Updates the displayed image according to the current zoom/pan and draws the circles.
|
| 203 |
+
The big circle is in CIRCLE_COLOR, and the small circles inside are in SMALL_CIRCLES_COLOR.
|
| 204 |
"""
|
| 205 |
try:
|
| 206 |
if self.original_display is None:
|
|
|
|
| 221 |
for x, y, diameter in self.marks:
|
| 222 |
zoomed_x = int(x * self.zoom_factor)
|
| 223 |
zoomed_y = int(y * self.zoom_factor)
|
| 224 |
+
zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
|
| 225 |
|
| 226 |
+
# Draw the main yellow circle
|
| 227 |
cv2.circle(
|
| 228 |
zoomed_bgr,
|
| 229 |
(zoomed_x, zoomed_y),
|
|
|
|
| 233 |
lineType=cv2.LINE_AA
|
| 234 |
)
|
| 235 |
|
| 236 |
+
# Draw 8 small black circles around
|
| 237 |
num_points = 8
|
| 238 |
for i in range(num_points):
|
| 239 |
angle = 2 * np.pi * i / num_points
|
|
|
|
| 243 |
zoomed_bgr,
|
| 244 |
(point_x, point_y),
|
| 245 |
1,
|
| 246 |
+
self.SMALL_CIRCLES_COLOR,
|
| 247 |
-1,
|
| 248 |
lineType=cv2.LINE_AA
|
| 249 |
)
|
|
|
|
| 267 |
|
| 268 |
def analyze_roi(self, evt: gr.SelectData):
|
| 269 |
"""
|
| 270 |
+
Called when user clicks on the displayed image to measure an ROI.
|
|
|
|
| 271 |
"""
|
| 272 |
try:
|
| 273 |
if self.current_image is None:
|
|
|
|
| 276 |
clicked_x = evt.index[0]
|
| 277 |
clicked_y = evt.index[1]
|
| 278 |
|
| 279 |
+
# Adjust by pan
|
| 280 |
x = clicked_x + self.pan_x
|
| 281 |
y = clicked_y + self.pan_y
|
| 282 |
|
| 283 |
+
# Adjust by zoom
|
| 284 |
if self.zoom_factor != 1.0:
|
| 285 |
x = x / self.zoom_factor
|
| 286 |
y = y / self.zoom_factor
|
|
|
|
| 295 |
|
| 296 |
dx = X - x
|
| 297 |
dy = Y - y
|
| 298 |
+
dist_squared = dx * dx + dy * dy
|
| 299 |
|
| 300 |
mask = np.zeros((height, width), dtype=bool)
|
| 301 |
mask[dist_squared <= r_squared] = True
|
|
|
|
| 349 |
|
| 350 |
row1, row2 = row_pair
|
| 351 |
|
| 352 |
+
# SNR formula
|
| 353 |
formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
|
| 354 |
formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
|
| 355 |
cell1 = ws[f"{formula_col}{row1}"]
|
|
|
|
| 357 |
cell1.font = red_font
|
| 358 |
cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 359 |
|
| 360 |
+
# CNR formula
|
| 361 |
formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
|
| 362 |
cell2 = ws[f"{formula_col}{row2}"]
|
| 363 |
cell2.value = formula2
|
|
|
|
| 371 |
def save_formatted_results(self, output_path):
|
| 372 |
"""
|
| 373 |
Creates an Excel file with results in a formatted table,
|
| 374 |
+
including SNR/CNR formulas and average calculations.
|
|
|
|
|
|
|
|
|
|
| 375 |
"""
|
| 376 |
try:
|
| 377 |
if not self.results:
|
|
|
|
| 382 |
red_font = openpyxl.styles.Font(color="FF0000")
|
| 383 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 384 |
|
| 385 |
+
# Column group headers
|
| 386 |
headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
|
| 387 |
|
| 388 |
column_groups = [
|
|
|
|
| 395 |
('BV', 'BW', 'BX', 'BY', 'BZ')
|
| 396 |
]
|
| 397 |
|
|
|
|
| 398 |
for cols in column_groups:
|
| 399 |
for i, header in enumerate(headers):
|
| 400 |
cell = ws[f"{cols[i]}1"]
|
|
|
|
| 411 |
'(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
|
| 412 |
]
|
| 413 |
|
| 414 |
+
# Set the phantom size row labels
|
| 415 |
for i, size in enumerate(phantom_sizes):
|
| 416 |
header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
|
| 417 |
header_cell.font = red_font
|
| 418 |
header_cell.alignment = center_alignment
|
| 419 |
|
| 420 |
+
# Fill the data from self.results
|
| 421 |
result_idx = 0
|
| 422 |
current_col_group = 0
|
| 423 |
current_row_pair = 0
|
|
|
|
| 429 |
cols = column_groups[current_col_group]
|
| 430 |
rows = row_pairs[current_row_pair]
|
| 431 |
|
| 432 |
+
# First row
|
| 433 |
if result_idx < len(self.results):
|
| 434 |
result = self.results[result_idx]
|
| 435 |
self._write_result_to_cells(ws, result, cols, rows[0])
|
| 436 |
result_idx += 1
|
| 437 |
|
| 438 |
+
# Second row
|
| 439 |
if result_idx < len(self.results):
|
| 440 |
result = self.results[result_idx]
|
| 441 |
self._write_result_to_cells(ws, result, cols, rows[1])
|
| 442 |
result_idx += 1
|
| 443 |
|
| 444 |
+
# Add SNR/CNR formulas
|
| 445 |
self.add_formulas_to_template(ws, rows, cols, red_font)
|
| 446 |
|
| 447 |
current_col_group += 1
|
|
|
|
| 457 |
if cell.value is not None:
|
| 458 |
cell.alignment = center_alignment
|
| 459 |
|
| 460 |
+
# Additional tables: StdDev Averages and CNR Averages
|
| 461 |
current_row = 32
|
| 462 |
|
| 463 |
+
# StdDev
|
| 464 |
stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages")
|
| 465 |
stddev_header.font = red_font
|
| 466 |
stddev_header.alignment = center_alignment
|
| 467 |
current_row += 1
|
| 468 |
|
| 469 |
for i, size in enumerate(phantom_sizes):
|
| 470 |
+
row_number = row_pairs[i][0]
|
| 471 |
stddev_values = []
|
| 472 |
|
| 473 |
for cols in column_groups:
|
| 474 |
+
stddev_col = cols[2] # The StdDev column
|
| 475 |
cell_value = ws[f"{stddev_col}{row_number}"].value
|
| 476 |
+
if cell_value not in [0, None, '']:
|
| 477 |
stddev_values.append(float(cell_value))
|
| 478 |
|
| 479 |
size_cell = ws.cell(row=current_row, column=1, value=size)
|
|
|
|
| 486 |
avg_cell.alignment = center_alignment
|
| 487 |
current_row += 1
|
| 488 |
|
| 489 |
+
current_row += 2
|
| 490 |
|
| 491 |
+
# CNR
|
| 492 |
cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages")
|
| 493 |
cnr_header.font = red_font
|
| 494 |
cnr_header.alignment = center_alignment
|
| 495 |
current_row += 1
|
| 496 |
|
|
|
|
| 497 |
for i, size in enumerate(phantom_sizes):
|
| 498 |
row_number = row_pairs[i][1]
|
| 499 |
cnr_cells = []
|
|
|
|
| 502 |
formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
|
| 503 |
cnr_cell_ref = f"{formula_col}{row_number}"
|
| 504 |
|
| 505 |
+
# Read Mean1, Mean2, Std2 to skip zeros
|
| 506 |
mean_col = cols[1]
|
| 507 |
std_col = cols[2]
|
| 508 |
|
|
|
|
| 517 |
except:
|
| 518 |
mean1_val, mean2_val, std2_val = 0, 0, 0
|
| 519 |
|
| 520 |
+
# If not all zero, add the cell reference
|
| 521 |
if not (mean1_val == 0 and mean2_val == 0 and std2_val == 0):
|
| 522 |
cnr_cells.append(cnr_cell_ref)
|
| 523 |
|
|
|
|
| 525 |
size_cell.alignment = center_alignment
|
| 526 |
|
| 527 |
if cnr_cells:
|
| 528 |
+
# Using AVERAGE(...) instead of AVERAGEIF
|
| 529 |
average_formula = f'=IFERROR(AVERAGE({",".join(cnr_cells)}), "")'
|
| 530 |
|
| 531 |
avg_cell = ws.cell(row=current_row, column=2)
|
|
|
|
| 535 |
|
| 536 |
current_row += 1
|
| 537 |
|
| 538 |
+
# Align the extra rows
|
| 539 |
for row in range(32, current_row):
|
| 540 |
for col in range(1, 3):
|
| 541 |
cell = ws.cell(row=row, column=col)
|
|
|
|
| 549 |
return None, f"Error saving results: {str(e)}"
|
| 550 |
|
| 551 |
def _write_result_to_cells(self, ws, result, cols, row):
|
| 552 |
+
"""Helper method to write a single result to worksheet cells."""
|
| 553 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 554 |
|
| 555 |
value_mapping = {
|
|
|
|
| 563 |
for i, (header, key) in enumerate(value_mapping.items()):
|
| 564 |
cell = ws[f"{cols[i]}{row}"]
|
| 565 |
val = result[key]
|
|
|
|
| 566 |
cell.value = float(val) if val not in ['', None] else ''
|
| 567 |
cell.alignment = center_alignment
|
| 568 |
|
|
|
|
| 575 |
df = df[columns_order]
|
| 576 |
return df.to_string(index=False)
|
| 577 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
def add_zero_row(self, image):
|
| 579 |
+
"""
|
| 580 |
+
Adds one row with zero-values.
|
| 581 |
+
"""
|
| 582 |
self.results.append({
|
| 583 |
'Area (mm²)': '0.000',
|
| 584 |
'Mean': '0.000',
|
|
|
|
| 591 |
|
| 592 |
def add_two_zero_rows(self, image):
|
| 593 |
"""
|
| 594 |
+
Adds two consecutive rows with zero-values.
|
| 595 |
"""
|
| 596 |
for _ in range(2):
|
| 597 |
self.results.append({
|
|
|
|
| 643 |
elem_id="image_display"
|
| 644 |
)
|
| 645 |
|
| 646 |
+
# Removed the "Add Blank Row" button
|
| 647 |
with gr.Row():
|
|
|
|
| 648 |
zero_btn = gr.Button("Add Zero Row")
|
| 649 |
+
zero2_btn = gr.Button("Add Two Zero Rows")
|
| 650 |
undo_btn = gr.Button("Undo Last")
|
| 651 |
save_btn = gr.Button("Save Results")
|
| 652 |
save_formatted_btn = gr.Button("Save Formatted Results")
|
|
|
|
| 657 |
|
| 658 |
gr.Markdown("""
|
| 659 |
### Controls:
|
| 660 |
+
- Use arrow keys to pan when zoomed in. Movement is now larger.
|
| 661 |
- Click points to measure ROI.
|
| 662 |
- Use Zoom In/Out buttons or Reset View to adjust zoom level.
|
| 663 |
- Use Reset All to clear all measurements.
|
| 664 |
+
- "Save Results": basic Excel with raw data.
|
| 665 |
+
- "Save Formatted Results": Excel with advanced formatting & formulas.
|
| 666 |
""")
|
| 667 |
|
|
|
|
| 668 |
def update_diameter(x):
|
| 669 |
analyzer.circle_diameter = float(x)
|
| 670 |
print(f"Diameter updated to: {x}")
|
| 671 |
return f"Diameter set to {x} pixels"
|
| 672 |
|
|
|
|
| 673 |
def save_formatted():
|
| 674 |
output_path = "analysis_results_formatted.xlsx"
|
| 675 |
return analyzer.save_formatted_results(output_path)
|
| 676 |
|
| 677 |
+
# Handlers
|
| 678 |
file_input.change(
|
| 679 |
fn=analyzer.load_dicom,
|
| 680 |
inputs=file_input,
|
|
|
|
| 723 |
outputs=image_display
|
| 724 |
)
|
| 725 |
|
| 726 |
+
# Removed blank_btn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
zero_btn.click(
|
| 728 |
fn=analyzer.add_zero_row,
|
| 729 |
inputs=image_display,
|
| 730 |
outputs=[image_display, results_display]
|
| 731 |
)
|
| 732 |
+
|
|
|
|
| 733 |
zero2_btn.click(
|
| 734 |
fn=analyzer.add_two_zero_rows,
|
| 735 |
inputs=image_display,
|
|
|
|
| 753 |
)
|
| 754 |
|
| 755 |
# JavaScript snippet to allow multiple arrow key presses
|
|
|
|
| 756 |
js = """
|
| 757 |
<script>
|
| 758 |
document.addEventListener('keydown', function(e) {
|
|
|
|
| 760 |
e.preventDefault();
|
| 761 |
const keyPressElement = document.querySelector('#key_press textarea');
|
| 762 |
if (keyPressElement) {
|
|
|
|
| 763 |
keyPressElement.value = e.key;
|
| 764 |
keyPressElement.dispatchEvent(new Event('input'));
|
|
|
|
|
|
|
| 765 |
setTimeout(() => {
|
| 766 |
keyPressElement.value = '';
|
| 767 |
keyPressElement.dispatchEvent(new Event('input'));
|