BrainModel / SegmentationTest.py
leminhhung0101's picture
Create SegmentationTest.py
bc52876 verified
Raw
History Blame Contribute Delete
15.3 kB
"""
Script doc lap de test model phan doan (segmentation) khoi u nao
(DeepLabV3+ ResNet101V2) da train (.keras).
Khong can toan bo pipeline training - chi can model + 1 file de test.
Input: file .h5 co dataset 'image' (va 'mask' neu muon so sanh voi ground truth).
Cach dung:
# Chi du doan, luu anh overlay ra file
python test_segmentation.py --model best_model.keras --input slice.h5 --output result.png
# Du doan + tinh metric neu file h5 co san mask ground truth
python test_segmentation.py --model best_model.keras --input slice.h5 --output result.png
"""
import argparse
import numpy as np
import cv2
import h5py
import tensorflow as tf
import matplotlib.pyplot as plt
from scipy.ndimage import label, binary_closing, binary_dilation
IMG_SIZE = (256, 256)
NUM_SLICES = 1
PIXEL_THRESHOLD = 0.01
# ============================================================
# LOSS / METRIC FUNCTIONS - BAT BUOC PHAI CO DE load_model() HOAT DONG
# (copy y nguyen tu script training goc)
# ============================================================
@tf.function
def dice_coef(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
y_true_f = tf.reshape(y_true, [-1])
y_pred_f = tf.reshape(y_pred, [-1])
intersection = tf.reduce_sum(y_true_f * y_pred_f)
union = tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f)
return (2.0 * intersection + smooth) / (union + smooth)
@tf.function
def generalized_dice_coef(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
w_fg = 1.0 / (tf.reduce_sum(y_true) ** 2 + smooth)
w_bg = 1.0 / (tf.reduce_sum(1.0 - y_true) ** 2 + smooth)
intersection_fg = tf.reduce_sum(y_true * y_pred)
union_fg = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred)
intersection_bg = tf.reduce_sum((1.0 - y_true) * (1.0 - y_pred))
union_bg = tf.reduce_sum(1.0 - y_true) + tf.reduce_sum(1.0 - y_pred)
numerator = w_fg * intersection_fg + w_bg * intersection_bg
denominator = w_fg * union_fg + w_bg * union_bg
return 2.0 * (numerator + smooth) / (denominator + smooth)
@tf.function
def weighted_dice_coef(y_true, y_pred, weight_fg=0.75, weight_bg=0.25, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
intersection_fg = tf.reduce_sum(y_true * y_pred)
union_fg = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred)
dice_fg = (2.0 * intersection_fg + smooth) / (union_fg + smooth)
intersection_bg = tf.reduce_sum((1.0 - y_true) * (1.0 - y_pred))
union_bg = tf.reduce_sum(1.0 - y_true) + tf.reduce_sum(1.0 - y_pred)
dice_bg = (2.0 * intersection_bg + smooth) / (union_bg + smooth)
return weight_fg * dice_fg + weight_bg * dice_bg
@tf.function
def iou(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
y_true_f = tf.reshape(y_true, [-1])
y_pred_f = tf.reshape(y_pred, [-1])
intersection = tf.reduce_sum(y_true_f * y_pred_f)
union = tf.reduce_sum(y_true_f) + tf.reduce_sum(y_pred_f) - intersection
return (intersection + smooth) / (union + smooth)
@tf.function
def boundary_iou(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
def erosion(x):
return -tf.nn.max_pool2d(-x, ksize=3, strides=1, padding='SAME')
y_true_eroded = erosion(y_true)
y_pred_eroded = erosion(y_pred)
y_true_boundary = tf.abs(y_true - y_true_eroded)
y_pred_boundary = tf.abs(y_pred - y_pred_eroded)
intersection = tf.reduce_sum(y_true_boundary * y_pred_boundary)
union = tf.reduce_sum(y_true_boundary) + tf.reduce_sum(y_pred_boundary) - intersection
return (intersection + smooth) / (union + smooth)
@tf.function
def sensitivity(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred > 0.5, tf.float32)
true_pos = tf.reduce_sum(y_true * y_pred)
false_neg = tf.reduce_sum(y_true * (1.0 - y_pred))
return (true_pos + smooth) / (true_pos + false_neg + smooth)
@tf.function
def specificity(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred > 0.5, tf.float32)
true_neg = tf.reduce_sum((1.0 - y_true) * (1.0 - y_pred))
false_pos = tf.reduce_sum((1.0 - y_true) * y_pred)
return (true_neg + smooth) / (true_neg + false_pos + smooth)
@tf.function
def focal_loss(y_true, y_pred, alpha=0.75, gamma=2.0):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
y_pred = tf.clip_by_value(y_pred, 1e-7, 1.0 - 1e-7)
pt_1 = tf.where(tf.equal(y_true, 1), y_pred, tf.ones_like(y_pred))
focal_pos = -alpha * tf.pow(1.0 - pt_1, gamma) * tf.math.log(pt_1)
pt_0 = tf.where(tf.equal(y_true, 0), y_pred, tf.zeros_like(y_pred))
focal_neg = -(1 - alpha) * tf.pow(pt_0, gamma) * tf.math.log(1.0 - pt_0)
return tf.reduce_mean(focal_pos + focal_neg)
@tf.function
def tversky_loss(y_true, y_pred, alpha=0.8, beta=0.2, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
y_true_f = tf.reshape(y_true, [-1])
y_pred_f = tf.reshape(y_pred, [-1])
true_pos = tf.reduce_sum(y_true_f * y_pred_f)
false_neg = tf.reduce_sum(y_true_f * (1 - y_pred_f))
false_pos = tf.reduce_sum((1 - y_true_f) * y_pred_f)
tversky_index = (true_pos + smooth) / (true_pos + alpha * false_neg + beta * false_pos + smooth)
return 1.0 - tversky_index
@tf.function
def focal_tversky_loss(y_true, y_pred, alpha=0.8, beta=0.2, gamma=0.75, smooth=1e-6):
tversky = tversky_loss(y_true, y_pred, alpha, beta, smooth)
return tf.pow(tversky, gamma)
@tf.function
def generalized_dice_loss(y_true, y_pred, smooth=1e-6):
return 1.0 - generalized_dice_coef(y_true, y_pred, smooth)
@tf.function
def boundary_loss(y_true, y_pred, smooth=1e-6):
y_true = tf.cast(y_true, tf.float32)
y_pred = tf.cast(y_pred, tf.float32)
if len(y_true.shape) == 3:
y_true = tf.expand_dims(y_true, axis=-1)
if len(y_pred.shape) == 3:
y_pred = tf.expand_dims(y_pred, axis=-1)
sobel_x = tf.reshape(tf.constant([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=tf.float32), [3, 3, 1, 1])
sobel_y = tf.reshape(tf.constant([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=tf.float32), [3, 3, 1, 1])
edges_true_x = tf.nn.conv2d(y_true, sobel_x, strides=[1, 1, 1, 1], padding='SAME')
edges_true_y = tf.nn.conv2d(y_true, sobel_y, strides=[1, 1, 1, 1], padding='SAME')
edges_true = tf.sqrt(edges_true_x ** 2 + edges_true_y ** 2 + smooth)
edges_pred_x = tf.nn.conv2d(y_pred, sobel_x, strides=[1, 1, 1, 1], padding='SAME')
edges_pred_y = tf.nn.conv2d(y_pred, sobel_y, strides=[1, 1, 1, 1], padding='SAME')
edges_pred = tf.sqrt(edges_pred_x ** 2 + edges_pred_y ** 2 + smooth)
return tf.reduce_mean(tf.square(edges_true - edges_pred))
@tf.function
def hybrid_loss_optimized(y_true, y_pred, w_ft=0.5, w_dice=0.35, w_boundary=0.1, w_focal=0.05):
ft_loss = focal_tversky_loss(y_true, y_pred, alpha=0.8, beta=0.2, gamma=0.75)
d_loss = generalized_dice_loss(y_true, y_pred)
b_loss = boundary_loss(y_true, y_pred)
f_loss = focal_loss(y_true, y_pred, alpha=0.75, gamma=2.0)
return w_ft * ft_loss + w_dice * d_loss + w_boundary * b_loss + w_focal * f_loss
CUSTOM_OBJECTS = {
'dice_coef': dice_coef,
'generalized_dice_coef': generalized_dice_coef,
'weighted_dice_coef': weighted_dice_coef,
'iou': iou,
'boundary_iou': boundary_iou,
'sensitivity': sensitivity,
'specificity': specificity,
'focal_loss': focal_loss,
'tversky_loss': tversky_loss,
'focal_tversky_loss': focal_tversky_loss,
'generalized_dice_loss': generalized_dice_loss,
'boundary_loss': boundary_loss,
'hybrid_loss_optimized': hybrid_loss_optimized,
}
# ============================================================
# TIEN XU LY - GIONG HET LOGIC TRONG ImprovedNiftiGenerator BAN GOC
# ============================================================
def load_image_and_mask_from_h5(path):
"""Doc anh (va mask neu co) tu file .h5."""
with h5py.File(path, 'r') as f:
if 'image' not in f:
raise ValueError(f"File h5 khong co dataset 'image': {path}")
img = f['image'][:].astype(np.float32)
if img.ndim == 3:
img = img[..., 0]
mask = None
if 'mask' in f:
mask = f['mask'][:].astype(np.float32)
if mask.ndim == 3:
mask = mask[..., 0]
return img, mask
def preprocess_image(img, mask=None):
"""Resize + normalize percentile (2, 98) - dung y nhu _load_slice va
_normalize_batch trong ImprovedNiftiGenerator ban goc."""
if img.shape != IMG_SIZE:
img = cv2.resize(img, IMG_SIZE, interpolation=cv2.INTER_LINEAR)
if mask is not None and mask.shape != IMG_SIZE:
mask = cv2.resize(mask, IMG_SIZE, interpolation=cv2.INTER_NEAREST)
if mask is not None:
mask = (mask > 0).astype(np.float32)
p2, p98 = np.percentile(img, (2, 98))
img_norm = np.clip(img, p2, p98)
img_norm = (img_norm - p2) / (p98 - p2 + 1e-8)
X = img_norm[np.newaxis, ..., np.newaxis].astype(np.float32) # (1, 256, 256, 1)
return X, img, mask
def advanced_post_process(pred, threshold=0.25, min_size=100):
"""Hau xu ly du doan de loai bo noise va lam min (copy tu ban goc)."""
binary_pred = (pred > threshold).astype(np.uint8)
kernel_close = np.ones((3, 3), np.uint8)
binary_pred = binary_closing(binary_pred, structure=kernel_close).astype(np.uint8)
labeled, num_features = label(binary_pred)
if num_features > 0:
sizes = np.bincount(labeled.flat)[1:]
for i, size in enumerate(sizes, 1):
if size < min_size:
binary_pred[labeled == i] = 0
kernel_dilate = np.ones((2, 2), np.uint8)
binary_pred = binary_dilation(binary_pred, structure=kernel_dilate).astype(np.uint8)
return binary_pred
# ============================================================
# CHAY INFERENCE
# ============================================================
def run_inference(model_path, input_path, output_path=None, threshold=0.25, min_size=100):
print(f"Dang load model: {model_path}")
model = tf.keras.models.load_model(model_path, custom_objects=CUSTOM_OBJECTS)
print(f"Dang doc va tien xu ly file: {input_path}")
raw_img, raw_mask = load_image_and_mask_from_h5(input_path)
X, img_resized, mask_resized = preprocess_image(raw_img, raw_mask)
print("Dang du doan...")
pred_raw = model.predict(X, verbose=0)[0, ..., 0]
pred_processed = advanced_post_process(pred_raw, threshold=threshold, min_size=min_size)
tumor_pixels = int(pred_processed.sum())
total_pixels = pred_processed.size
print("\n" + "=" * 50)
print("KET QUA DU DOAN (SEGMENTATION)")
print("=" * 50)
print(f"So pixel duoc du doan la tumor: {tumor_pixels} / {total_pixels} "
f"({100 * tumor_pixels / total_pixels:.2f}%)")
print(f"Xac suat trung binh (raw) vung tumor: {pred_raw.mean():.4f}")
print(f"Xac suat max (raw): {pred_raw.max():.4f}")
# Neu file h5 co san ground truth mask -> tinh metric so sanh
if mask_resized is not None:
y_true = tf.constant(mask_resized[..., np.newaxis], dtype=tf.float32)
y_pred = tf.constant(pred_processed[..., np.newaxis].astype(np.float32), dtype=tf.float32)
dice = dice_coef(y_true, y_pred).numpy()
iou_score = iou(y_true, y_pred).numpy()
sens = sensitivity(y_true, y_pred).numpy()
spec = specificity(y_true, y_pred).numpy()
true_pos = np.sum((mask_resized == 1) & (pred_processed == 1))
false_pos = np.sum((mask_resized == 0) & (pred_processed == 1))
precision = true_pos / (true_pos + false_pos + 1e-7)
f1 = 2 * precision * sens / (precision + sens + 1e-7)
print("\nSo sanh voi ground truth mask co san trong file h5:")
print(f" Dice coefficient : {dice:.4f}")
print(f" IoU : {iou_score:.4f}")
print(f" Sensitivity : {sens:.4f}")
print(f" Specificity : {spec:.4f}")
print(f" Precision : {precision:.4f}")
print(f" F1 Score : {f1:.4f}")
else:
print("\n(File h5 khong co dataset 'mask' nen khong tinh duoc metric so sanh)")
print("=" * 50)
# Xuat anh minh hoa
if output_path:
n_panels = 4 if mask_resized is not None else 3
fig, axes = plt.subplots(1, n_panels, figsize=(4 * n_panels, 4))
axes[0].imshow(img_resized, cmap='gray')
axes[0].set_title('Anh dau vao')
axes[0].axis('off')
idx = 1
if mask_resized is not None:
axes[idx].imshow(mask_resized, cmap='jet', alpha=0.8)
axes[idx].set_title('Ground Truth')
axes[idx].axis('off')
idx += 1
axes[idx].imshow(pred_processed, cmap='jet', alpha=0.8)
axes[idx].set_title('Du doan')
axes[idx].axis('off')
idx += 1
axes[idx].imshow(img_resized, cmap='gray')
axes[idx].imshow(pred_processed, cmap='jet', alpha=0.5)
axes[idx].set_title('Overlay')
axes[idx].axis('off')
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close()
print(f"\nDa luu anh minh hoa tai: {output_path}")
return pred_raw, pred_processed
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Test model segmentation khoi u nao voi 1 file .h5")
parser.add_argument("--model", required=True, help="Duong dan file model .keras")
parser.add_argument("--input", required=True, help="Duong dan file .h5 can test (co dataset 'image', 'mask' neu co)")
parser.add_argument("--output", default=None, help="Duong dan file .png de luu anh minh hoa (tuy chon)")
parser.add_argument("--threshold", type=float, default=0.25, help="Nguong nhi phan hoa du doan (mac dinh 0.25)")
parser.add_argument("--min_size", type=int, default=100, help="Kich thuoc vung nho nhat giu lai, tinh bang pixel (mac dinh 100)")
args = parser.parse_args()
run_inference(args.model, args.input, args.output, args.threshold, args.min_size)