Instructions to use leminhhung0101/BrainModel with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use leminhhung0101/BrainModel with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://leminhhung0101/BrainModel") - Notebooks
- Google Colab
- Kaggle
| """ | |
| 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) | |
| # ============================================================ | |
| 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) | |
| 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) | |
| 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 | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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 | |
| 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) | |
| def generalized_dice_loss(y_true, y_pred, smooth=1e-6): | |
| return 1.0 - generalized_dice_coef(y_true, y_pred, smooth) | |
| 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)) | |
| 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) |