LIENWEIYING/T1C_OAR_TV/Dice/test/compute_test_dice.py

318 lines
13 KiB
Python
Raw Permalink Normal View History

2026-07-22 06:50:09 +00:00
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))