Spaces:
Runtime error
Runtime error
Default
Browse files
app.py
CHANGED
|
@@ -111,10 +111,6 @@ def drawOnTop(img, landmarks, original_shape):
|
|
| 111 |
tilt_text = f"Tilt: {tilt_angle:.1f} degrees"
|
| 112 |
cv2.putText(image, tilt_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 1, 0), 2)
|
| 113 |
|
| 114 |
-
# Add measurement method text
|
| 115 |
-
method_text = "Separate Lung Measurement"
|
| 116 |
-
cv2.putText(image, method_text, (10, h-30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0.8, 1), 2)
|
| 117 |
-
|
| 118 |
# Correct landmarks for tilt
|
| 119 |
if abs(tilt_angle) > 2: # Only correct if tilt is significant
|
| 120 |
RL_corrected = rotate_points(RL, tilt_angle, image_center)
|
|
@@ -169,104 +165,46 @@ def drawOnTop(img, landmarks, original_shape):
|
|
| 169 |
(int(heart_end[0] - perp_x), int(heart_end[1] - perp_y)),
|
| 170 |
(1, 0, 0), 2)
|
| 171 |
|
| 172 |
-
# Thorax
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
rl_xmax = np.max(RL_corrected[:, 0])
|
| 176 |
-
ll_xmin = np.min(LL_corrected[:, 0])
|
| 177 |
-
ll_xmax = np.max(LL_corrected[:, 0])
|
| 178 |
-
|
| 179 |
-
# Find center line (approximate midline of chest)
|
| 180 |
-
chest_center_x = (rl_xmin + rl_xmax + ll_xmin + ll_xmax) / 4
|
| 181 |
-
|
| 182 |
-
# Right lung line: from rightmost point to center
|
| 183 |
-
rl_right_point = RL_corrected[np.argmax(RL_corrected[:, 0])]
|
| 184 |
-
rl_center_point = np.array([chest_center_x, rl_right_point[1]])
|
| 185 |
-
|
| 186 |
-
# Left lung line: from leftmost point to center
|
| 187 |
-
ll_left_point = LL_corrected[np.argmin(LL_corrected[:, 0])]
|
| 188 |
-
ll_center_point = np.array([chest_center_x, ll_left_point[1]])
|
| 189 |
-
|
| 190 |
-
# Draw right lung measurement line (blue)
|
| 191 |
-
rl_points_corrected = np.array([rl_right_point, rl_center_point])
|
| 192 |
-
rl_points_display = rotate_points(rl_points_corrected, -tilt_angle, image_center)
|
| 193 |
-
rl_start = (int(rl_points_display[0, 0]), int(rl_points_display[0, 1]))
|
| 194 |
-
rl_end = (int(rl_points_display[1, 0]), int(rl_points_display[1, 1]))
|
| 195 |
-
image = cv2.line(image, rl_start, rl_end, (0, 0, 1), 2)
|
| 196 |
-
|
| 197 |
-
# Draw left lung measurement line (blue)
|
| 198 |
-
ll_points_corrected = np.array([ll_left_point, ll_center_point])
|
| 199 |
-
ll_points_display = rotate_points(ll_points_corrected, -tilt_angle, image_center)
|
| 200 |
-
ll_start = (int(ll_points_display[0, 0]), int(ll_points_display[0, 1]))
|
| 201 |
-
ll_end = (int(ll_points_display[1, 0]), int(ll_points_display[1, 1]))
|
| 202 |
-
image = cv2.line(image, ll_start, ll_end, (0, 0, 1), 2)
|
| 203 |
-
|
| 204 |
-
# Draw center line (dashed blue)
|
| 205 |
-
center_y_top = min(rl_right_point[1], ll_left_point[1]) - 50
|
| 206 |
-
center_y_bottom = max(rl_right_point[1], ll_left_point[1]) + 50
|
| 207 |
-
center_top = np.array([chest_center_x, center_y_top])
|
| 208 |
-
center_bottom = np.array([chest_center_x, center_y_bottom])
|
| 209 |
-
center_points_display = rotate_points(np.array([center_top, center_bottom]), -tilt_angle, image_center)
|
| 210 |
-
|
| 211 |
-
# Draw dashed center line
|
| 212 |
-
center_start = (int(center_points_display[0, 0]), int(center_points_display[0, 1]))
|
| 213 |
-
center_end = (int(center_points_display[1, 0]), int(center_points_display[1, 1]))
|
| 214 |
|
| 215 |
-
#
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
if end_ratio > 1:
|
| 224 |
-
end_ratio = 1
|
| 225 |
-
|
| 226 |
-
dash_start_x = int(center_start[0] + (center_end[0] - center_start[0]) * start_ratio)
|
| 227 |
-
dash_start_y = int(center_start[1] + (center_end[1] - center_start[1]) * start_ratio)
|
| 228 |
-
dash_end_x = int(center_start[0] + (center_end[0] - center_start[0]) * end_ratio)
|
| 229 |
-
dash_end_y = int(center_start[1] + (center_end[1] - center_start[1]) * end_ratio)
|
| 230 |
-
|
| 231 |
-
image = cv2.line(image, (dash_start_x, dash_start_y), (dash_end_x, dash_end_y), (0, 0.5, 1), 1)
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
|
|
|
| 235 |
|
| 236 |
-
#
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
if
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
# Perpendicular lines at
|
| 245 |
-
image = cv2.line(image,
|
| 246 |
-
(int(rl_start[0] + rl_perp_x), int(rl_start[1] + rl_perp_y)),
|
| 247 |
-
(int(rl_start[0] - rl_perp_x), int(rl_start[1] - rl_perp_y)),
|
| 248 |
-
(0, 0, 1), 2)
|
| 249 |
-
image = cv2.line(image,
|
| 250 |
-
(int(rl_end[0] + rl_perp_x), int(rl_end[1] + rl_perp_y)),
|
| 251 |
-
(int(rl_end[0] - rl_perp_x), int(rl_end[1] - rl_perp_y)),
|
| 252 |
-
(0, 0, 1), 2)
|
| 253 |
-
|
| 254 |
-
# Left lung perpendicular lines
|
| 255 |
-
ll_dx = ll_end[0] - ll_start[0]
|
| 256 |
-
ll_dy = ll_end[1] - ll_start[1]
|
| 257 |
-
ll_length = np.sqrt(ll_dx**2 + ll_dy**2)
|
| 258 |
-
if ll_length > 0:
|
| 259 |
-
ll_perp_x = -ll_dy / ll_length * line_length
|
| 260 |
-
ll_perp_y = ll_dx / ll_length * line_length
|
| 261 |
-
|
| 262 |
-
# Perpendicular lines at left lung endpoints
|
| 263 |
image = cv2.line(image,
|
| 264 |
-
(int(
|
| 265 |
-
(int(
|
| 266 |
(0, 0, 1), 2)
|
|
|
|
| 267 |
image = cv2.line(image,
|
| 268 |
-
(int(
|
| 269 |
-
(int(
|
| 270 |
(0, 0, 1), 2)
|
| 271 |
|
| 272 |
# Store corrected landmarks for CTR calculation
|
|
@@ -386,8 +324,8 @@ def validate_landmarks_consistency(landmarks, original_landmarks, threshold=0.05
|
|
| 386 |
print(f"Error in landmark validation: {e}")
|
| 387 |
return False
|
| 388 |
|
| 389 |
-
def
|
| 390 |
-
"""Calculate CTR with
|
| 391 |
try:
|
| 392 |
original_landmarks = landmarks.copy()
|
| 393 |
|
|
@@ -415,114 +353,70 @@ def calculate_ctr_separate_lungs(landmarks, corrected_landmarks=None):
|
|
| 415 |
tilt_angle = 0
|
| 416 |
correction_applied = False
|
| 417 |
|
| 418 |
-
#
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
chest_center_x = (np.min(RL[:, 0]) + np.max(RL[:, 0]) + np.min(LL[:, 0]) + np.max(LL[:, 0])) / 4
|
| 422 |
-
|
| 423 |
-
# 2. การวัดความกว้างของหัวใจ (Cardiac Width)
|
| 424 |
-
cardiac_left = np.min(H[:, 0]) # ขอบซ้ายของหัวใจ
|
| 425 |
-
cardiac_right = np.max(H[:, 0]) # ขอบขวาของหัวใจ
|
| 426 |
-
cardiac_width_total = cardiac_right - cardiac_left
|
| 427 |
-
|
| 428 |
-
# แบ่งความกว้างหัวใจออกเป็นซ้าย-ขวา
|
| 429 |
-
cardiac_left_width = chest_center_x - cardiac_left # ความกว้างหัวใจฝั่งซ้าย
|
| 430 |
-
cardiac_right_width = cardiac_right - chest_center_x # ความกว้างหัวใจฝั่งขวา
|
| 431 |
-
|
| 432 |
-
# 3. การวัดความกว้างของปอด (Lung Width) แยกซ้าย-ขวา
|
| 433 |
-
right_lung_width = np.max(RL[:, 0]) - chest_center_x # ความกว้างปอดขวา (จากกึ่งกลางไปขอบขวา)
|
| 434 |
-
left_lung_width = chest_center_x - np.min(LL[:, 0]) # ความกว้างปอดซ้าย (จากขอบซ้ายมาก��่งกลาง)
|
| 435 |
-
thoracic_width_total = right_lung_width + left_lung_width
|
| 436 |
|
| 437 |
-
#
|
| 438 |
-
|
| 439 |
-
|
|
|
|
| 440 |
|
| 441 |
-
#
|
| 442 |
-
|
| 443 |
-
|
| 444 |
|
| 445 |
-
|
| 446 |
-
ctr_separate_avg = (ctr_left_side + ctr_right_side) / 2
|
| 447 |
|
| 448 |
-
#
|
| 449 |
cardiac_x_coords = H[:, 0]
|
| 450 |
-
|
| 451 |
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
chest_center_x_p = (np.percentile(rl_x_coords, 50) + np.percentile(ll_x_coords, 50)) / 2
|
| 455 |
-
right_lung_width_p = np.percentile(rl_x_coords, 95) - chest_center_x_p
|
| 456 |
-
left_lung_width_p = chest_center_x_p - np.percentile(ll_x_coords, 5)
|
| 457 |
-
thoracic_width_p = right_lung_width_p + left_lung_width_p
|
| 458 |
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
| 460 |
|
| 461 |
-
#
|
| 462 |
-
|
| 463 |
-
lung_asymmetry = abs(left_lung_width - right_lung_width) / thoracic_width_total if thoracic_width_total > 0 else 0
|
| 464 |
-
|
| 465 |
-
# 7. กำหนดค่าความเชื่อมั่น
|
| 466 |
-
ctr_values = [ctr_traditional, ctr_separate_avg, ctr_percentile]
|
| 467 |
ctr_std = np.std(ctr_values)
|
| 468 |
|
| 469 |
-
if ctr_std > 0.05:
|
|
|
|
| 470 |
confidence = "Low"
|
| 471 |
elif ctr_std > 0.02:
|
| 472 |
-
confidence = "Medium"
|
| 473 |
else:
|
| 474 |
confidence = "High"
|
| 475 |
|
| 476 |
-
#
|
| 477 |
-
|
| 478 |
-
confidence = "Low" if confidence == "High" else confidence
|
| 479 |
-
|
| 480 |
-
# 8. ใช้ค่าเฉลี่ยถ่วงน้ำหนักสำหรับผลลัพธ์สุดท้าย
|
| 481 |
-
# ให้น้ำหนักมากกับ CTR แบบแบ่งครึ่งเพราะแม่นยำกว่า
|
| 482 |
-
final_ctr = (ctr_traditional * 0.3 + ctr_separate_avg * 0.5 + ctr_percentile * 0.2)
|
| 483 |
|
| 484 |
return {
|
| 485 |
'ctr': round(final_ctr, 3),
|
| 486 |
-
'ctr_traditional': round(ctr_traditional, 3),
|
| 487 |
-
'ctr_separate_avg': round(ctr_separate_avg, 3),
|
| 488 |
-
'ctr_left_side': round(ctr_left_side, 3),
|
| 489 |
-
'ctr_right_side': round(ctr_right_side, 3),
|
| 490 |
-
'cardiac_width_total': round(cardiac_width_total, 1),
|
| 491 |
-
'cardiac_left_width': round(cardiac_left_width, 1),
|
| 492 |
-
'cardiac_right_width': round(cardiac_right_width, 1),
|
| 493 |
-
'thoracic_width_total': round(thoracic_width_total, 1),
|
| 494 |
-
'left_lung_width': round(left_lung_width, 1),
|
| 495 |
-
'right_lung_width': round(right_lung_width, 1),
|
| 496 |
-
'cardiac_asymmetry': round(cardiac_asymmetry, 3),
|
| 497 |
-
'lung_asymmetry': round(lung_asymmetry, 3),
|
| 498 |
'tilt_angle': abs(tilt_angle),
|
| 499 |
'correction_applied': correction_applied,
|
| 500 |
'confidence': confidence,
|
| 501 |
'method_variance': round(ctr_std, 4),
|
| 502 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
}
|
| 504 |
|
| 505 |
except Exception as e:
|
| 506 |
-
print(f"Error in
|
| 507 |
return {
|
| 508 |
'ctr': 0,
|
| 509 |
-
'ctr_traditional': 0,
|
| 510 |
-
'ctr_separate_avg': 0,
|
| 511 |
-
'ctr_left_side': 0,
|
| 512 |
-
'ctr_right_side': 0,
|
| 513 |
-
'cardiac_width_total': 0,
|
| 514 |
-
'cardiac_left_width': 0,
|
| 515 |
-
'cardiac_right_width': 0,
|
| 516 |
-
'thoracic_width_total': 0,
|
| 517 |
-
'left_lung_width': 0,
|
| 518 |
-
'right_lung_width': 0,
|
| 519 |
-
'cardiac_asymmetry': 0,
|
| 520 |
-
'lung_asymmetry': 0,
|
| 521 |
'tilt_angle': 0,
|
| 522 |
'correction_applied': False,
|
| 523 |
'confidence': 'Error',
|
| 524 |
'method_variance': 0,
|
| 525 |
-
'
|
| 526 |
}
|
| 527 |
|
| 528 |
|
|
@@ -685,23 +579,23 @@ def segment(input_img):
|
|
| 685 |
|
| 686 |
except Exception as e:
|
| 687 |
print(f"Error in segmentation: {e}")
|
| 688 |
-
# Return a basic error response
|
| 689 |
-
return None, None, 0,
|
| 690 |
|
| 691 |
seg_to_save = (outseg.copy() * 255).astype('uint8')
|
| 692 |
cv2.imwrite("tmp/overlap_segmentation.png", cv2.cvtColor(seg_to_save, cv2.COLOR_RGB2BGR))
|
| 693 |
|
| 694 |
-
# Step 9: CTR calculation
|
| 695 |
-
ctr_result =
|
| 696 |
ctr_value = ctr_result['ctr']
|
| 697 |
tilt_angle = ctr_result['tilt_angle']
|
| 698 |
|
| 699 |
-
#
|
| 700 |
interpretation_parts = []
|
| 701 |
|
| 702 |
# CTR interpretation
|
| 703 |
if ctr_value < 0.5:
|
| 704 |
-
base_interpretation = "Normal
|
| 705 |
elif 0.50 <= ctr_value <= 0.55:
|
| 706 |
base_interpretation = "Mild Cardiomegaly (CTR 50-55%)"
|
| 707 |
elif 0.56 <= ctr_value <= 0.60:
|
|
@@ -713,17 +607,6 @@ def segment(input_img):
|
|
| 713 |
|
| 714 |
interpretation_parts.append(base_interpretation)
|
| 715 |
|
| 716 |
-
# เพิ่มข้อมูลการวัดแยกซ้าย-ขวา
|
| 717 |
-
if ctr_result['ctr_left_side'] > 0 and ctr_result['ctr_right_side'] > 0:
|
| 718 |
-
interpretation_parts.append(f"Left: {ctr_result['ctr_left_side']:.3f}, Right: {ctr_result['ctr_right_side']:.3f}")
|
| 719 |
-
|
| 720 |
-
# ตรวจสอบความไม่สมมาตร
|
| 721 |
-
if ctr_result['cardiac_asymmetry'] > 0.2:
|
| 722 |
-
interpretation_parts.append(f"Cardiac asymmetry detected ({ctr_result['cardiac_asymmetry']:.2f})")
|
| 723 |
-
|
| 724 |
-
if ctr_result['lung_asymmetry'] > 0.15:
|
| 725 |
-
interpretation_parts.append(f"Lung asymmetry detected ({ctr_result['lung_asymmetry']:.2f})")
|
| 726 |
-
|
| 727 |
# Add quality indicators
|
| 728 |
if was_rotated:
|
| 729 |
interpretation_parts.append(f"Image rotation corrected ({detected_rotation:.1f}°)")
|
|
@@ -733,37 +616,28 @@ def segment(input_img):
|
|
| 733 |
elif tilt_angle > 3:
|
| 734 |
interpretation_parts.append(f"Residual tilt detected ({tilt_angle:.1f}°)")
|
| 735 |
|
| 736 |
-
# Add confidence indicator
|
| 737 |
-
interpretation_parts.append(f"Method: {ctr_result['measurement_method']}")
|
| 738 |
interpretation_parts.append(f"Confidence: {ctr_result['confidence']}")
|
| 739 |
|
| 740 |
final_interpretation = " | ".join(interpretation_parts)
|
| 741 |
|
| 742 |
-
return
|
| 743 |
-
ctr_result['ctr_traditional'], ctr_result['ctr_left_side'],
|
| 744 |
-
ctr_result['ctr_right_side'], ctr_result['cardiac_width_total'],
|
| 745 |
-
ctr_result['thoracic_width_total'], final_interpretation)
|
| 746 |
|
| 747 |
|
| 748 |
if __name__ == "__main__":
|
| 749 |
with gr.Blocks() as demo:
|
| 750 |
gr.Markdown("""
|
| 751 |
-
# Chest X-ray HybridGNet Segmentation
|
| 752 |
|
| 753 |
-
Demo of the HybridGNet model
|
| 754 |
|
| 755 |
-
|
| 756 |
-
-
|
| 757 |
-
|
| 758 |
-
- **Asymmetry Detection**: ตรวจหาความไม่สมมาตรของหัวใจและปอด
|
| 759 |
-
- **Multiple Measurement Methods**: รวมหลายวิธีการวัดเพื่อความแม่นยำ
|
| 760 |
|
| 761 |
-
|
| 762 |
-
1. Upload a chest X-ray image (PA or AP) in PNG or JPEG format
|
| 763 |
-
2. Click on "Segment Image"
|
| 764 |
-
3. ดูผลการวัด CTR แบบแยกซ้าย-ขวา พร้อมข้อมูลละเอียด
|
| 765 |
|
| 766 |
-
|
| 767 |
""")
|
| 768 |
|
| 769 |
with gr.Tab("Segment Image"):
|
|
@@ -783,18 +657,8 @@ if __name__ == "__main__":
|
|
| 783 |
image_output = gr.Image(type="filepath", height=750)
|
| 784 |
|
| 785 |
with gr.Row():
|
| 786 |
-
ctr_output = gr.Number(label="CTR (
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
with gr.Row():
|
| 790 |
-
ctr_left = gr.Number(label="CTR Left Side", precision=3)
|
| 791 |
-
ctr_right = gr.Number(label="CTR Right Side", precision=3)
|
| 792 |
-
|
| 793 |
-
with gr.Row():
|
| 794 |
-
cardiac_width = gr.Number(label="Cardiac Width (pixels)", precision=1)
|
| 795 |
-
thoracic_width = gr.Number(label="Thoracic Width (pixels)", precision=1)
|
| 796 |
-
|
| 797 |
-
ctr_interpretation = gr.Textbox(label="Detailed Analysis", interactive=False, lines=3)
|
| 798 |
|
| 799 |
results = gr.File()
|
| 800 |
|
|
@@ -842,16 +706,8 @@ if __name__ == "__main__":
|
|
| 842 |
clear_button.click(lambda: None, None, image_input, queue=False)
|
| 843 |
clear_button.click(lambda: None, None, image_output, queue=False)
|
| 844 |
clear_button.click(lambda: None, None, ctr_output, queue=False)
|
| 845 |
-
clear_button.click(lambda: None, None, ctr_traditional, queue=False)
|
| 846 |
-
clear_button.click(lambda: None, None, ctr_left, queue=False)
|
| 847 |
-
clear_button.click(lambda: None, None, ctr_right, queue=False)
|
| 848 |
-
clear_button.click(lambda: None, None, cardiac_width, queue=False)
|
| 849 |
-
clear_button.click(lambda: None, None, thoracic_width, queue=False)
|
| 850 |
clear_button.click(lambda: None, None, ctr_interpretation, queue=False)
|
| 851 |
|
| 852 |
-
image_button.click(segment, inputs=image_input,
|
| 853 |
-
outputs=[image_output, results, ctr_output, ctr_traditional,
|
| 854 |
-
ctr_left, ctr_right, cardiac_width, thoracic_width, ctr_interpretation],
|
| 855 |
-
queue=False)
|
| 856 |
|
| 857 |
demo.launch()
|
|
|
|
| 111 |
tilt_text = f"Tilt: {tilt_angle:.1f} degrees"
|
| 112 |
cv2.putText(image, tilt_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 1, 0), 2)
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
# Correct landmarks for tilt
|
| 115 |
if abs(tilt_angle) > 2: # Only correct if tilt is significant
|
| 116 |
RL_corrected = rotate_points(RL, tilt_angle, image_center)
|
|
|
|
| 165 |
(int(heart_end[0] - perp_x), int(heart_end[1] - perp_y)),
|
| 166 |
(1, 0, 0), 2)
|
| 167 |
|
| 168 |
+
# Thorax (blue line) - calculate positions from corrected coordinates
|
| 169 |
+
thorax_xmin_corrected = min(np.min(RL_corrected[:, 0]), np.min(LL_corrected[:, 0]))
|
| 170 |
+
thorax_xmax_corrected = max(np.max(RL_corrected[:, 0]), np.max(LL_corrected[:, 0]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
# Find y at leftmost and rightmost points (corrected)
|
| 173 |
+
if np.min(RL_corrected[:, 0]) < np.min(LL_corrected[:, 0]):
|
| 174 |
+
thorax_ymin_corrected = RL_corrected[np.argmin(RL_corrected[:, 0]), 1]
|
| 175 |
+
else:
|
| 176 |
+
thorax_ymin_corrected = LL_corrected[np.argmin(LL_corrected[:, 0]), 1]
|
| 177 |
+
if np.max(RL_corrected[:, 0]) > np.max(LL_corrected[:, 0]):
|
| 178 |
+
thorax_ymax_corrected = RL_corrected[np.argmax(RL_corrected[:, 0]), 1]
|
| 179 |
+
else:
|
| 180 |
+
thorax_ymax_corrected = LL_corrected[np.argmax(LL_corrected[:, 0]), 1]
|
| 181 |
+
thorax_y_corrected = np.mean([thorax_ymin_corrected, thorax_ymax_corrected])
|
| 182 |
|
| 183 |
+
# Rotate back to match the tilted image for display
|
| 184 |
+
thorax_points_corrected = np.array([[thorax_xmin_corrected, thorax_y_corrected], [thorax_xmax_corrected, thorax_y_corrected]])
|
| 185 |
+
thorax_points_display = rotate_points(thorax_points_corrected, -tilt_angle, image_center) # Rotate back for display
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
+
thorax_start = (int(thorax_points_display[0, 0]), int(thorax_points_display[0, 1]))
|
| 188 |
+
thorax_end = (int(thorax_points_display[1, 0]), int(thorax_points_display[1, 1]))
|
| 189 |
+
image = cv2.line(image, thorax_start, thorax_end, (0, 0, 1), 2)
|
| 190 |
|
| 191 |
+
# Add perpendicular lines at thorax endpoints
|
| 192 |
+
thorax_dx = thorax_end[0] - thorax_start[0]
|
| 193 |
+
thorax_dy = thorax_end[1] - thorax_start[1]
|
| 194 |
+
thorax_length = np.sqrt(thorax_dx**2 + thorax_dy**2)
|
| 195 |
+
if thorax_length > 0:
|
| 196 |
+
perp_x = -thorax_dy / thorax_length * line_length
|
| 197 |
+
perp_y = thorax_dx / thorax_length * line_length
|
| 198 |
+
|
| 199 |
+
# Perpendicular lines at start point
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
image = cv2.line(image,
|
| 201 |
+
(int(thorax_start[0] + perp_x), int(thorax_start[1] + perp_y)),
|
| 202 |
+
(int(thorax_start[0] - perp_x), int(thorax_start[1] - perp_y)),
|
| 203 |
(0, 0, 1), 2)
|
| 204 |
+
# Perpendicular lines at end point
|
| 205 |
image = cv2.line(image,
|
| 206 |
+
(int(thorax_end[0] + perp_x), int(thorax_end[1] + perp_y)),
|
| 207 |
+
(int(thorax_end[0] - perp_x), int(thorax_end[1] - perp_y)),
|
| 208 |
(0, 0, 1), 2)
|
| 209 |
|
| 210 |
# Store corrected landmarks for CTR calculation
|
|
|
|
| 324 |
print(f"Error in landmark validation: {e}")
|
| 325 |
return False
|
| 326 |
|
| 327 |
+
def calculate_ctr_robust(landmarks, corrected_landmarks=None):
|
| 328 |
+
"""Calculate CTR with multiple validation steps"""
|
| 329 |
try:
|
| 330 |
original_landmarks = landmarks.copy()
|
| 331 |
|
|
|
|
| 353 |
tilt_angle = 0
|
| 354 |
correction_applied = False
|
| 355 |
|
| 356 |
+
# Method 1: Traditional width measurement
|
| 357 |
+
cardiac_width_1 = np.max(H[:, 0]) - np.min(H[:, 0])
|
| 358 |
+
thoracic_width_1 = max(np.max(RL[:, 0]), np.max(LL[:, 0])) - min(np.min(RL[:, 0]), np.min(LL[:, 0]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
+
# Method 2: Centroid-based measurement (more robust to outliers)
|
| 361 |
+
h_centroid = np.mean(H, axis=0)
|
| 362 |
+
rl_centroid = np.mean(RL, axis=0)
|
| 363 |
+
ll_centroid = np.mean(LL, axis=0)
|
| 364 |
|
| 365 |
+
# Find widest points from centroids
|
| 366 |
+
h_distances = np.linalg.norm(H - h_centroid, axis=1)
|
| 367 |
+
cardiac_width_2 = 2 * np.max(h_distances)
|
| 368 |
|
| 369 |
+
thoracic_width_2 = max(np.max(RL[:, 0]), np.max(LL[:, 0])) - min(np.min(RL[:, 0]), np.min(LL[:, 0]))
|
|
|
|
| 370 |
|
| 371 |
+
# Method 3: Percentile-based measurement (removes extreme outliers)
|
| 372 |
cardiac_x_coords = H[:, 0]
|
| 373 |
+
cardiac_width_3 = np.percentile(cardiac_x_coords, 95) - np.percentile(cardiac_x_coords, 5)
|
| 374 |
|
| 375 |
+
lung_x_coords = np.concatenate([RL[:, 0], LL[:, 0]])
|
| 376 |
+
thoracic_width_3 = np.percentile(lung_x_coords, 95) - np.percentile(lung_x_coords, 5)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
+
# Calculate CTR for each method
|
| 379 |
+
ctr_1 = cardiac_width_1 / thoracic_width_1 if thoracic_width_1 > 0 else 0
|
| 380 |
+
ctr_2 = cardiac_width_2 / thoracic_width_2 if thoracic_width_2 > 0 else 0
|
| 381 |
+
ctr_3 = cardiac_width_3 / thoracic_width_3 if thoracic_width_3 > 0 else 0
|
| 382 |
|
| 383 |
+
# Validate consistency between methods
|
| 384 |
+
ctr_values = [ctr_1, ctr_2, ctr_3]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
ctr_std = np.std(ctr_values)
|
| 386 |
|
| 387 |
+
if ctr_std > 0.05: # High variance between methods
|
| 388 |
+
print(f"Warning: CTR calculation methods show high variance (std: {ctr_std:.3f})")
|
| 389 |
confidence = "Low"
|
| 390 |
elif ctr_std > 0.02:
|
| 391 |
+
confidence = "Medium"
|
| 392 |
else:
|
| 393 |
confidence = "High"
|
| 394 |
|
| 395 |
+
# Use median of methods for final result
|
| 396 |
+
final_ctr = np.median(ctr_values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
return {
|
| 399 |
'ctr': round(final_ctr, 3),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
'tilt_angle': abs(tilt_angle),
|
| 401 |
'correction_applied': correction_applied,
|
| 402 |
'confidence': confidence,
|
| 403 |
'method_variance': round(ctr_std, 4),
|
| 404 |
+
'individual_results': {
|
| 405 |
+
'traditional': round(ctr_1, 3),
|
| 406 |
+
'centroid': round(ctr_2, 3),
|
| 407 |
+
'percentile': round(ctr_3, 3)
|
| 408 |
+
}
|
| 409 |
}
|
| 410 |
|
| 411 |
except Exception as e:
|
| 412 |
+
print(f"Error in robust CTR calculation: {e}")
|
| 413 |
return {
|
| 414 |
'ctr': 0,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
'tilt_angle': 0,
|
| 416 |
'correction_applied': False,
|
| 417 |
'confidence': 'Error',
|
| 418 |
'method_variance': 0,
|
| 419 |
+
'individual_results': {}
|
| 420 |
}
|
| 421 |
|
| 422 |
|
|
|
|
| 579 |
|
| 580 |
except Exception as e:
|
| 581 |
print(f"Error in segmentation: {e}")
|
| 582 |
+
# Return a basic error response
|
| 583 |
+
return None, None, 0, f"Error: {str(e)}"
|
| 584 |
|
| 585 |
seg_to_save = (outseg.copy() * 255).astype('uint8')
|
| 586 |
cv2.imwrite("tmp/overlap_segmentation.png", cv2.cvtColor(seg_to_save, cv2.COLOR_RGB2BGR))
|
| 587 |
|
| 588 |
+
# Step 9: Robust CTR calculation
|
| 589 |
+
ctr_result = calculate_ctr_robust(output, corrected_data)
|
| 590 |
ctr_value = ctr_result['ctr']
|
| 591 |
tilt_angle = ctr_result['tilt_angle']
|
| 592 |
|
| 593 |
+
# Enhanced interpretation with quality indicators
|
| 594 |
interpretation_parts = []
|
| 595 |
|
| 596 |
# CTR interpretation
|
| 597 |
if ctr_value < 0.5:
|
| 598 |
+
base_interpretation = "Normal"
|
| 599 |
elif 0.50 <= ctr_value <= 0.55:
|
| 600 |
base_interpretation = "Mild Cardiomegaly (CTR 50-55%)"
|
| 601 |
elif 0.56 <= ctr_value <= 0.60:
|
|
|
|
| 607 |
|
| 608 |
interpretation_parts.append(base_interpretation)
|
| 609 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
# Add quality indicators
|
| 611 |
if was_rotated:
|
| 612 |
interpretation_parts.append(f"Image rotation corrected ({detected_rotation:.1f}°)")
|
|
|
|
| 616 |
elif tilt_angle > 3:
|
| 617 |
interpretation_parts.append(f"Residual tilt detected ({tilt_angle:.1f}°)")
|
| 618 |
|
| 619 |
+
# Add confidence indicator
|
|
|
|
| 620 |
interpretation_parts.append(f"Confidence: {ctr_result['confidence']}")
|
| 621 |
|
| 622 |
final_interpretation = " | ".join(interpretation_parts)
|
| 623 |
|
| 624 |
+
return outseg, "tmp/overlap_segmentation.png", ctr_value, final_interpretation
|
|
|
|
|
|
|
|
|
|
| 625 |
|
| 626 |
|
| 627 |
if __name__ == "__main__":
|
| 628 |
with gr.Blocks() as demo:
|
| 629 |
gr.Markdown("""
|
| 630 |
+
# Chest X-ray HybridGNet Segmentation.
|
| 631 |
|
| 632 |
+
Demo of the HybridGNet model introduced in "Improving anatomical plausibility in medical image segmentation via hybrid graph neural networks: applications to chest x-ray analysis."
|
| 633 |
|
| 634 |
+
Instructions:
|
| 635 |
+
1. Upload a chest X-ray image (PA or AP) in PNG or JPEG format.
|
| 636 |
+
2. Click on "Segment Image".
|
|
|
|
|
|
|
| 637 |
|
| 638 |
+
Note: Pre-processing is not needed, it will be done automatically and removed after the segmentation.
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
+
Please check citations below.
|
| 641 |
""")
|
| 642 |
|
| 643 |
with gr.Tab("Segment Image"):
|
|
|
|
| 657 |
image_output = gr.Image(type="filepath", height=750)
|
| 658 |
|
| 659 |
with gr.Row():
|
| 660 |
+
ctr_output = gr.Number(label="CTR (Cardiothoracic Ratio)")
|
| 661 |
+
ctr_interpretation = gr.Textbox(label="Interpretation", interactive=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
results = gr.File()
|
| 664 |
|
|
|
|
| 706 |
clear_button.click(lambda: None, None, image_input, queue=False)
|
| 707 |
clear_button.click(lambda: None, None, image_output, queue=False)
|
| 708 |
clear_button.click(lambda: None, None, ctr_output, queue=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
clear_button.click(lambda: None, None, ctr_interpretation, queue=False)
|
| 710 |
|
| 711 |
+
image_button.click(segment, inputs=image_input, outputs=[image_output, results, ctr_output, ctr_interpretation], queue=False)
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
demo.launch()
|