Chayanat commited on
Commit
ac15a1e
·
verified ·
1 Parent(s): c4cc12f
Files changed (1) hide show
  1. app.py +86 -230
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 measurement - separate left and right lung measurements
173
- # Find widest points of each lung separately
174
- rl_xmin = np.min(RL_corrected[:, 0])
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
- # Create dashed line effect
216
- line_length = np.sqrt((center_end[0] - center_start[0])**2 + (center_end[1] - center_start[1])**2)
217
- dash_length = 10
218
- num_dashes = int(line_length / (dash_length * 2))
 
 
 
 
 
 
219
 
220
- for i in range(num_dashes):
221
- start_ratio = (i * 2 * dash_length) / line_length
222
- end_ratio = ((i * 2 + 1) * dash_length) / line_length
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
- # Add perpendicular lines at lung endpoints
234
- line_length = 30
 
235
 
236
- # Right lung perpendicular lines
237
- rl_dx = rl_end[0] - rl_start[0]
238
- rl_dy = rl_end[1] - rl_start[1]
239
- rl_length = np.sqrt(rl_dx**2 + rl_dy**2)
240
- if rl_length > 0:
241
- rl_perp_x = -rl_dy / rl_length * line_length
242
- rl_perp_y = rl_dx / rl_length * line_length
243
-
244
- # Perpendicular lines at right lung endpoints
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(ll_start[0] + ll_perp_x), int(ll_start[1] + ll_perp_y)),
265
- (int(ll_start[0] - ll_perp_x), int(ll_start[1] - ll_perp_y)),
266
  (0, 0, 1), 2)
 
267
  image = cv2.line(image,
268
- (int(ll_end[0] + ll_perp_x), int(ll_end[1] + ll_perp_y)),
269
- (int(ll_end[0] - ll_perp_x), int(ll_end[1] - ll_perp_y)),
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 calculate_ctr_separate_lungs(landmarks, corrected_landmarks=None):
390
- """Calculate CTR with separate left and right lung measurements (แบบแบ่งครึ่ง)"""
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
- # === การวัดแบบแบ่งครึ่ง (Separate Lung Measurement) ===
419
-
420
- # 1. หาจุดกึ่งกลางของหน้าอก (Midline)
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
- # 4. คำนวณ CTR แบบต่างๆ
438
- # CTR แบบดั้งเดิม (รวม)
439
- ctr_traditional = cardiac_width_total / thoracic_width_total if thoracic_width_total > 0 else 0
 
440
 
441
- # CTR แบบแบ่งครึ่ง (แยกซ้าย-ขวา)
442
- ctr_left_side = cardiac_left_width / left_lung_width if left_lung_width > 0 else 0
443
- ctr_right_side = cardiac_right_width / right_lung_width if right_lung_width > 0 else 0
444
 
445
- # CTR เฉลี่ยจากการวัดแยก
446
- ctr_separate_avg = (ctr_left_side + ctr_right_side) / 2
447
 
448
- # 5. การวัดแบบ Percentile (ลดผลกระทบจากจุดผิดปกติ)
449
  cardiac_x_coords = H[:, 0]
450
- cardiac_width_p95 = np.percentile(cardiac_x_coords, 95) - np.percentile(cardiac_x_coords, 5)
451
 
452
- rl_x_coords = RL[:, 0]
453
- ll_x_coords = LL[:, 0]
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
- ctr_percentile = cardiac_width_p95 / thoracic_width_p if thoracic_width_p > 0 else 0
 
 
 
460
 
461
- # 6. ตรวจสอบความสมมาตรของหัวใจ
462
- cardiac_asymmetry = abs(cardiac_left_width - cardiac_right_width) / cardiac_width_total if cardiac_width_total > 0 else 0
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
- if cardiac_asymmetry > 0.3 or lung_asymmetry > 0.2:
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
- 'measurement_method': 'Separate Lung Analysis'
 
 
 
 
503
  }
504
 
505
  except Exception as e:
506
- print(f"Error in separate lung CTR calculation: {e}")
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
- 'measurement_method': 'Error'
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 with all expected outputs
689
- return None, None, 0, 0, 0, 0, 0, 0, f"Error: {str(e)}"
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 แบบแบ่งครึ่ง (Separate Lung Analysis)
695
- ctr_result = calculate_ctr_separate_lungs(output, corrected_data)
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 CTR"
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 and measurement method
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 (outseg, "tmp/overlap_segmentation.png", ctr_value,
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 with Advanced CTR Analysis
752
 
753
- Demo of the HybridGNet model with enhanced **Separate Lung CTR Measurement** (การวัด CTR แบบแบ่งครึ่ง)
754
 
755
- ## Key Features:
756
- - **Separate Lung Analysis**: วัดปอดซ้าย-ขวาแยกกัน เหมือนในรูปที่คุณแนบมา
757
- - **Tilt Correction**: แก้ไขความเอียงของภาพอัตโนมัติ
758
- - **Asymmetry Detection**: ตรวจหาความไม่สมมาตรของหัวใจและปอด
759
- - **Multiple Measurement Methods**: รวมหลายวิธีการวัดเพื่อความแม่นยำ
760
 
761
- ## Instructions:
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
- **Note**: ระบบจะประมวลผลและแก้ไขความเอียงของภาพอัตโนมัติ
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 (Overall)", precision=3)
787
- ctr_traditional = gr.Number(label="CTR (Traditional)", precision=3)
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()