318 lines
13 KiB
Python
318 lines
13 KiB
Python
|
|
import json
|
||
|
|
import os
|
||
|
|
import numpy as np
|
||
|
|
import SimpleITK as sitk
|
||
|
|
import pandas as pd
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
import seaborn as sns
|
||
|
|
from tqdm import tqdm
|
||
|
|
import time
|
||
|
|
from datetime import datetime
|
||
|
|
from scipy.ndimage import distance_transform_edt
|
||
|
|
import multiprocessing
|
||
|
|
from joblib import Parallel, delayed
|
||
|
|
|
||
|
|
class IntegratedAnalyzer:
|
||
|
|
def __init__(self, base_path):
|
||
|
|
"""初始化分析器"""
|
||
|
|
self.base_path = base_path
|
||
|
|
self.organ_names = {
|
||
|
|
1: "Brainstem",
|
||
|
|
2: "Right_Eye",
|
||
|
|
3: "Left_Eye",
|
||
|
|
4: "Optic_Chiasm",
|
||
|
|
5: "Right_Optic_Nerve",
|
||
|
|
6: "Left_Optic_Nerve",
|
||
|
|
7: "TV"
|
||
|
|
}
|
||
|
|
self.configurations = {
|
||
|
|
'Dataset012_OAR_TV': {
|
||
|
|
'2d': {
|
||
|
|
'model': 'nnUNetTrainer__nnUNetPlans__2d',
|
||
|
|
'path': '/mnt/1248/onlylian/T1C_OAR_TV/output_predictions/2d',
|
||
|
|
'folds': range(5)
|
||
|
|
},
|
||
|
|
'3d_fullres': {
|
||
|
|
'model': 'nnUNetTrainer__nnUNetPlans__3d_fullres',
|
||
|
|
'path': '/mnt/1248/onlylian/T1C_OAR_TV/output_predictions/3d_fullres',
|
||
|
|
'folds': range(5)
|
||
|
|
},
|
||
|
|
'3d_lowres': {
|
||
|
|
'model': 'nnUNetTrainer__nnUNetPlans__3d_lowres',
|
||
|
|
'path': '/mnt/1248/onlylian/T1C_OAR_TV/output_predictions/3d_lowres',
|
||
|
|
'folds': range(5)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
# 設定並行處理的核心數
|
||
|
|
self.n_jobs = max(1, multiprocessing.cpu_count() - 1)
|
||
|
|
self.setup_paths()
|
||
|
|
self.create_output_folders()
|
||
|
|
|
||
|
|
def setup_paths(self):
|
||
|
|
"""設置所有需要的路徑"""
|
||
|
|
self.dataset_paths = {}
|
||
|
|
for dataset, model_configs in self.configurations.items():
|
||
|
|
self.dataset_paths[dataset] = {
|
||
|
|
'ground_truth': os.path.join(
|
||
|
|
"/mnt/1248/onlylian/nnUNet/nnUNet_raw",
|
||
|
|
dataset,
|
||
|
|
"labelsTs"
|
||
|
|
),
|
||
|
|
'predictions': {}
|
||
|
|
}
|
||
|
|
for model_type, config in model_configs.items():
|
||
|
|
self.dataset_paths[dataset]['predictions'][model_type] = {
|
||
|
|
fold: os.path.join(config['path'], f"fold_{fold}")
|
||
|
|
for fold in config['folds']
|
||
|
|
}
|
||
|
|
|
||
|
|
self.output_path = os.path.join(
|
||
|
|
self.base_path,
|
||
|
|
"analysis_results",
|
||
|
|
datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
|
)
|
||
|
|
|
||
|
|
def create_output_folders(self):
|
||
|
|
"""建立輸出資料夾"""
|
||
|
|
os.makedirs(self.output_path, exist_ok=True)
|
||
|
|
|
||
|
|
def setup_font_configuration(self):
|
||
|
|
"""設置基本字型配置"""
|
||
|
|
plt.rcParams.update(plt.rcParamsDefault)
|
||
|
|
plt.rcParams['font.family'] = 'DejaVu Sans'
|
||
|
|
plt.rcParams['axes.unicode_minus'] = False
|
||
|
|
plt.rcParams['font.size'] = 12
|
||
|
|
plt.rcParams['axes.titlesize'] = 14
|
||
|
|
plt.rcParams['axes.labelsize'] = 12
|
||
|
|
plt.rcParams['xtick.labelsize'] = 10
|
||
|
|
plt.rcParams['ytick.labelsize'] = 10
|
||
|
|
return True
|
||
|
|
|
||
|
|
def load_nifti(self, file_path):
|
||
|
|
"""載入 NIfTI 檔案"""
|
||
|
|
return sitk.GetArrayFromImage(sitk.ReadImage(file_path))
|
||
|
|
|
||
|
|
def separate_structures(self, combined_mask, structure_name=""):
|
||
|
|
"""分離左右結構,改進的版本"""
|
||
|
|
sitk_image = sitk.GetImageFromArray(combined_mask)
|
||
|
|
labeled_components = sitk.ConnectedComponent(sitk_image)
|
||
|
|
labeled_array = sitk.GetArrayFromImage(labeled_components)
|
||
|
|
|
||
|
|
unique, counts = np.unique(labeled_array, return_counts=True)
|
||
|
|
if len(unique) < 3: # 背景+至少兩個區域
|
||
|
|
# 只在實際需要時輸出警告
|
||
|
|
if np.sum(combined_mask) > 0: # 只有當mask中確實有內容時才輸出警告
|
||
|
|
print(f"Warning: Not enough connected regions found for {structure_name}")
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
region_sizes = [(i, count) for i, count in zip(unique[1:], counts[1:])]
|
||
|
|
sorted_regions = sorted(region_sizes, key=lambda x: x[1], reverse=True)
|
||
|
|
|
||
|
|
if len(sorted_regions) < 2:
|
||
|
|
if np.sum(combined_mask) > 0: # 只有當mask中確實有內容時才輸出警告
|
||
|
|
print(f"Warning: Only one major region found for {structure_name}")
|
||
|
|
return None, None
|
||
|
|
|
||
|
|
region1_mask = (labeled_array == sorted_regions[0][0])
|
||
|
|
region2_mask = (labeled_array == sorted_regions[1][0])
|
||
|
|
|
||
|
|
def get_centroid(mask):
|
||
|
|
indices = np.where(mask)
|
||
|
|
return np.mean(indices[2])
|
||
|
|
|
||
|
|
centroid1 = get_centroid(region1_mask)
|
||
|
|
centroid2 = get_centroid(region2_mask)
|
||
|
|
|
||
|
|
if centroid1 > centroid2:
|
||
|
|
right = region1_mask
|
||
|
|
left = region2_mask
|
||
|
|
else:
|
||
|
|
right = region2_mask
|
||
|
|
left = region1_mask
|
||
|
|
|
||
|
|
return right.astype(np.uint8), left.astype(np.uint8)
|
||
|
|
|
||
|
|
def compute_dice(self, pred, gt):
|
||
|
|
"""計算 Dice 係數"""
|
||
|
|
intersection = np.sum(pred & gt)
|
||
|
|
sum_pred = np.sum(pred)
|
||
|
|
sum_gt = np.sum(gt)
|
||
|
|
|
||
|
|
if sum_pred == 0 and sum_gt == 0:
|
||
|
|
return 1.0
|
||
|
|
elif sum_pred == 0 or sum_gt == 0:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
return 2.0 * intersection / (sum_pred + sum_gt)
|
||
|
|
|
||
|
|
def evaluate_case(self, gt_path, pred_path):
|
||
|
|
"""評估單一案例,包含腫瘤距離分析"""
|
||
|
|
gt_img = self.load_nifti(gt_path)
|
||
|
|
pred_img = self.load_nifti(pred_path)
|
||
|
|
|
||
|
|
# 計算腫瘤距離圖
|
||
|
|
tumor_mask = (gt_img == 7) # 7 是腫瘤的標籤
|
||
|
|
tumor_distance = distance_transform_edt(~tumor_mask)
|
||
|
|
|
||
|
|
results = {}
|
||
|
|
distance_results = {}
|
||
|
|
|
||
|
|
# Process eyes
|
||
|
|
combined_gt_eye = ((gt_img == 2) | (gt_img == 3)).astype(np.uint8)
|
||
|
|
combined_pred_eye = ((pred_img == 2) | (pred_img == 3)).astype(np.uint8)
|
||
|
|
|
||
|
|
combined_pred_eye_sitk = sitk.GetImageFromArray(combined_pred_eye)
|
||
|
|
closing_filter = sitk.BinaryMorphologicalClosingImageFilter()
|
||
|
|
closing_filter.SetKernelRadius(2)
|
||
|
|
processed_pred_eye = sitk.GetArrayFromImage(closing_filter.Execute(combined_pred_eye_sitk))
|
||
|
|
|
||
|
|
results['combined_eye'] = self.compute_dice(processed_pred_eye, combined_gt_eye)
|
||
|
|
eye_distances = tumor_distance[combined_gt_eye > 0]
|
||
|
|
if len(eye_distances) > 0:
|
||
|
|
distance_results['combined_eye'] = np.mean(eye_distances)
|
||
|
|
|
||
|
|
right_pred_eye, left_pred_eye = self.separate_structures(processed_pred_eye, "Eyes")
|
||
|
|
right_gt_eye, left_gt_eye = self.separate_structures(combined_gt_eye, "Eyes")
|
||
|
|
|
||
|
|
# Process optic nerves
|
||
|
|
combined_gt_nerve = ((gt_img == 5) | (gt_img == 6)).astype(np.uint8)
|
||
|
|
combined_pred_nerve = ((pred_img == 5) | (pred_img == 6)).astype(np.uint8)
|
||
|
|
|
||
|
|
combined_pred_nerve_sitk = sitk.GetImageFromArray(combined_pred_nerve)
|
||
|
|
processed_pred_nerve = sitk.GetArrayFromImage(closing_filter.Execute(combined_pred_nerve_sitk))
|
||
|
|
|
||
|
|
results['combined_nerve'] = self.compute_dice(processed_pred_nerve, combined_gt_nerve)
|
||
|
|
nerve_distances = tumor_distance[combined_gt_nerve > 0]
|
||
|
|
if len(nerve_distances) > 0:
|
||
|
|
distance_results['combined_nerve'] = np.mean(nerve_distances)
|
||
|
|
|
||
|
|
right_pred_nerve, left_pred_nerve = self.separate_structures(processed_pred_nerve, "Optic Nerves")
|
||
|
|
right_gt_nerve, left_gt_nerve = self.separate_structures(combined_gt_nerve, "Optic Nerves")
|
||
|
|
|
||
|
|
# Process individual structures
|
||
|
|
for label in self.organ_names.keys():
|
||
|
|
if label == 1: # Brainstem
|
||
|
|
gt_binary = (gt_img == label).astype(np.uint8)
|
||
|
|
pred_binary = (pred_img == label).astype(np.uint8)
|
||
|
|
elif label == 2 and right_pred_eye is not None: # Right Eye
|
||
|
|
gt_binary = right_gt_eye
|
||
|
|
pred_binary = right_pred_eye
|
||
|
|
elif label == 3 and left_pred_eye is not None: # Left Eye
|
||
|
|
gt_binary = left_gt_eye
|
||
|
|
pred_binary = left_pred_eye
|
||
|
|
elif label == 4: # Optic Chiasm
|
||
|
|
gt_binary = (gt_img == label).astype(np.uint8)
|
||
|
|
pred_binary = (pred_img == label).astype(np.uint8)
|
||
|
|
elif label == 5 and right_pred_nerve is not None: # Right Optic Nerve
|
||
|
|
gt_binary = right_gt_nerve
|
||
|
|
pred_binary = right_pred_nerve
|
||
|
|
elif label == 6 and left_pred_nerve is not None: # Left Optic Nerve
|
||
|
|
gt_binary = left_gt_nerve
|
||
|
|
pred_binary = left_pred_nerve
|
||
|
|
elif label == 7: # TV (tumor)
|
||
|
|
gt_binary = (gt_img == label).astype(np.uint8)
|
||
|
|
pred_binary = (pred_img == label).astype(np.uint8)
|
||
|
|
else:
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
dice = self.compute_dice(pred_binary, gt_binary)
|
||
|
|
results[label] = dice
|
||
|
|
|
||
|
|
# 計算每個器官到腫瘤的距離
|
||
|
|
if label != 7: # 不計算腫瘤自身的距離
|
||
|
|
organ_distances = tumor_distance[gt_binary > 0]
|
||
|
|
if len(organ_distances) > 0:
|
||
|
|
distance_results[label] = np.mean(organ_distances)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Error processing label {label}: {str(e)}")
|
||
|
|
results[label] = np.nan
|
||
|
|
distance_results[label] = np.nan
|
||
|
|
|
||
|
|
return results, distance_results
|
||
|
|
|
||
|
|
def process_single_case(self, args):
|
||
|
|
"""處理單一案例的包裝函數"""
|
||
|
|
gt_path, pred_path = args
|
||
|
|
return self.evaluate_case(gt_path, pred_path)
|
||
|
|
|
||
|
|
def process_fold(self, model_type, fold, model_config, dataset):
|
||
|
|
"""處理單一fold的函數"""
|
||
|
|
results = []
|
||
|
|
pred_folder = os.path.join(model_config['path'], f"fold_{fold}")
|
||
|
|
gt_folder = self.dataset_paths[dataset]['ground_truth']
|
||
|
|
|
||
|
|
if not os.path.exists(pred_folder):
|
||
|
|
print(f"Warning: Prediction folder does not exist: {pred_folder}")
|
||
|
|
return results
|
||
|
|
|
||
|
|
pred_files = [f for f in os.listdir(pred_folder) if f.endswith('.nii.gz')]
|
||
|
|
|
||
|
|
# 準備參數
|
||
|
|
case_args = [(os.path.join(gt_folder, pred_file),
|
||
|
|
os.path.join(pred_folder, pred_file))
|
||
|
|
for pred_file in pred_files
|
||
|
|
if os.path.exists(os.path.join(gt_folder, pred_file))]
|
||
|
|
|
||
|
|
# 使用 joblib 進行並行處理
|
||
|
|
processed_results = Parallel(n_jobs=self.n_jobs)(
|
||
|
|
delayed(self.process_single_case)(args) for args in tqdm(case_args,
|
||
|
|
desc=f"Processing fold {fold}")
|
||
|
|
)
|
||
|
|
|
||
|
|
# 整理結果
|
||
|
|
for (dice_results, distance_results), (gt_path, pred_path) in zip(processed_results, case_args):
|
||
|
|
pred_file = os.path.basename(pred_path)
|
||
|
|
for key, value in dice_results.items():
|
||
|
|
if isinstance(key, int):
|
||
|
|
result_entry = {
|
||
|
|
'model_type': model_type,
|
||
|
|
'fold': fold,
|
||
|
|
'case_id': pred_file,
|
||
|
|
'structure': self.organ_names[key],
|
||
|
|
'dice_score': value,
|
||
|
|
'distance_to_tumor': distance_results.get(key, np.nan)
|
||
|
|
}
|
||
|
|
results.append(result_entry)
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
def analyze_and_visualize(self):
|
||
|
|
"""分析並視覺化結果,包含距離分析"""
|
||
|
|
self.setup_font_configuration()
|
||
|
|
plots_path = os.path.join(self.output_path, "plots")
|
||
|
|
os.makedirs(plots_path, exist_ok=True)
|
||
|
|
|
||
|
|
for dataset in self.configurations.keys():
|
||
|
|
all_results = []
|
||
|
|
|
||
|
|
for model_type, model_config in self.configurations[dataset].items():
|
||
|
|
print(f"\nProcessing {dataset} - {model_type}")
|
||
|
|
|
||
|
|
# 並行處理每個 fold
|
||
|
|
fold_results = Parallel(n_jobs=self.n_jobs)(
|
||
|
|
delayed(self.process_fold)(model_type, fold, model_config, dataset)
|
||
|
|
for fold in model_config['folds']
|
||
|
|
)
|
||
|
|
|
||
|
|
# 整合所有結果
|
||
|
|
for results in fold_results:
|
||
|
|
all_results.extend(results)
|
||
|
|
|
||
|
|
# 創建DataFrame並視覺化
|
||
|
|
if all_results:
|
||
|
|
df = pd.DataFrame(all_results)
|
||
|
|
|
||
|
|
# 基本的模型比較圖
|
||
|
|
plt.figure(figsize=(12, 6))
|
||
|
|
sns.boxplot(data=df, x='model_type', y='dice_score')
|
||
|
|
plt.title(f'{dataset} - Model Comparison')
|
||
|
|
plt.xlabel('Model Type')
|
||
|
|
plt.ylabel('Dice Score')
|
||
|
|
plt.tight_layout()
|
||
|
|
plt.savefig(os.path.join(plots_path, f'{dataset}_model_comparison.png'))
|
||
|
|
plt.close()
|
||
|
|
|
||
|
|
# 結構wise比較圖
|
||
|
|
plt.figure(figsize=(15, 8))
|