المشاركات

Deteksi TB dari CXR — Catatan & Notebook (PyTorch + MONAI)

Deteksi Tuberculosis (TB) dari Chest X-Ray (CXR)

Kompilasi penjelasan konsep, pipeline riset S2, serta template notebook (PyTorch + MONAI). Termasuk: baseline, 5-fold CV, lung segmentation (simple, U-Net, U-Net transfer), training U-Net, dan inference mask otomatis.

Ringkasan Strategi Singkat

  • Gunakan transfer learning (EfficientNet / DenseNet / ResNet) untuk klasifikasi TB dari CXR (dataset ~1000 gambar).
  • Terapkan lung segmentation (U-Net) sebagai preprocessing untuk memfokuskan classifier ke area paru.
  • Gunakan data augmentation dan teknik regularisasi (MixUp, CutMix, RandAugment) karena dataset moderat.
  • Gunakan explainability (Grad-CAM) agar radiolog dapat memverifikasi fokus model.
  • Lakukan 5-fold cross validation untuk estimasi performa yang kuat.
Target realistis: dengan pipeline ini Anda sering mendapat AUC di rentang ~0.85–0.92 (bergantung kualitas label & keragaman data). Selalu lakukan validasi eksternal bila mungkin.

Pipeline Teknis (Step-by-step)

  1. Quality check & labeling — verifikasi label TB vs non-TB, periksa orientasi, duplikat.
  2. Preprocessing — konversi grayscale, normalisasi, resize (224×224 atau 384×384), optional CLAHE.
  3. Lung segmentation (opsional tapi disarankan) — U-Net pretrained untuk mask paru.
  4. Augmentation & balancing — flips, rotations, brightness/contrast, MixUp/CutMix.
  5. Model baseline — EfficientNet-B0 / DenseNet121 / ResNet50 (transfer learning).
  6. Training — Binary CE (atau focal loss), AdamW, scheduler, early stopping.
  7. Validation — Stratified 5-fold CV, laporkan AUC, sensitivity, specificity, PR AUC.
  8. Explainability — Grad-CAM untuk inspeksi radiolog.
  9. External validation — uji di dataset publik (Montgomery, Shenzhen) jika diizinkan.

Notebook Templates (semua kode ada di bawah)

Di bagian ini saya sertakan beberapa notebook: baseline training, 5-fold CV, 5-fold + simple lung masking, 5-fold + pretrained U-Net, training U-Net (standar), U-Net + transfer learning (SMP), dan inference mask otomatis.

1) Baseline — Single split, EfficientNet

Kode: baseline training (PyTorch + MONAI)
# ======================================
# 🏥 TB Detection with PyTorch + MONAI
# ======================================

!pip install monai[all] torch torchvision torchaudio albumentations opencv-python scikit-learn grad-cam matplotlib tqdm --quiet

import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import models
from monai.transforms import (
    Compose, LoadImage, AddChannel, ScaleIntensity, EnsureType,
    RandFlip, RandRotate, RandZoom, Resize
)
from monai.data import ImageDataset
from sklearn.metrics import roc_auc_score, confusion_matrix, roc_curve
import matplotlib.pyplot as plt
from tqdm import tqdm

# ========================
# 📌 Config
# ========================
DATA_DIR = "dataset"
IMG_SIZE = (224, 224)
BATCH_SIZE = 16
EPOCHS = 10
LR = 1e-4
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# ========================
# 📥 Data Transform
# ========================
train_transforms = Compose([
    LoadImage(image_only=True),
    AddChannel(),
    Resize(IMG_SIZE),
    ScaleIntensity(),
    RandFlip(spatial_axis=1, prob=0.5),
    RandRotate(range_x=np.pi/12, prob=0.5),
    RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),
    EnsureType()
])

val_transforms = Compose([
    LoadImage(image_only=True),
    AddChannel(),
    Resize(IMG_SIZE),
    ScaleIntensity(),
    EnsureType()
])

# ========================
# 📂 Load Data
# ========================
train_images = []
train_labels = []
val_images = []
val_labels = []

for label_idx, label_name in enumerate(["Normal", "TB"]):
    train_path = os.path.join(DATA_DIR, "train", label_name)
    val_path = os.path.join(DATA_DIR, "val", label_name)

    train_images += [os.path.join(train_path, f) for f in os.listdir(train_path)]
    train_labels += [label_idx] * len(os.listdir(train_path))

    val_images += [os.path.join(val_path, f) for f in os.listdir(val_path)]
    val_labels += [label_idx] * len(os.listdir(val_path))

train_ds = ImageDataset(train_images, train_labels, transform=train_transforms)
val_ds = ImageDataset(val_images, val_labels, transform=val_transforms)

train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)
val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)

print(f"Train: {len(train_ds)} | Val: {len(val_ds)}")

# ========================
# 🧠 Model (EfficientNet)
# ========================
from torchvision.models import efficientnet_b0

model = efficientnet_b0(weights='IMAGENET1K_V1')
model.classifier[1] = nn.Linear(model.classifier[1].in_features, 2)
model = model.to(DEVICE)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=LR)

# ========================
# 🚀 Training Loop
# ========================
def train_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss = 0
    correct = 0
    total = 0
    for imgs, labels in tqdm(loader):
        imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
        optimizer.zero_grad()
        outputs = model(imgs.repeat(1,3,1,1))  # repeat channel -> 3ch
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        preds = outputs.argmax(1)
        correct += (preds == labels).sum().item()
        total += labels.size(0)
    return total_loss / len(loader), correct / total

def validate_epoch(model, loader, criterion):
    model.eval()
    total_loss = 0
    correct = 0
    total = 0
    all_labels = []
    all_probs = []
    with torch.no_grad():
        for imgs, labels in loader:
            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)
            outputs = model(imgs.repeat(1,3,1,1))
            loss = criterion(outputs, labels)
            total_loss += loss.item()

            probs = torch.softmax(outputs, dim=1)[:, 1]
            preds = outputs.argmax(1)
            correct += (preds == labels).sum().item()
            total += labels.size(0)

            all_labels.extend(labels.cpu().numpy())
            all_probs.extend(probs.cpu().numpy())
    return total_loss / len(loader), correct / total, np.array(all_labels), np.array(all_probs)

# ========================
# 🏋️ Training
# ========================
best_auc = 0
for epoch in range(EPOCHS):
    train_loss, train_acc = train_epoch(model, train_loader, optimizer, criterion)
    val_loss, val_acc, y_true, y_prob = validate_epoch(model, val_loader, criterion)
    auc = roc_auc_score(y_true, y_prob)
    print(f"[{epoch+1}/{EPOCHS}] Train Acc: {train_acc:.3f} | Val Acc: {val_acc:.3f} | AUC: {auc:.3f}")
    if auc > best_auc:
        best_auc = auc
        torch.save(model.state_dict(), "best_model.pt")

# ========================
# 📊 Evaluation
# ========================
model.load_state_dict(torch.load("best_model.pt"))
_, _, y_true, y_prob = validate_epoch(model, val_loader, criterion)
y_pred = (y_prob > 0.5).astype(int)

cm = confusion_matrix(y_true, y_pred)
print("Confusion Matrix:\n", cm)
print("AUC:", roc_auc_score(y_true, y_prob))

# ROC curve
fpr, tpr, _ = roc_curve(y_true, y_prob)
plt.figure()
plt.plot(fpr, tpr, label=f"AUC={roc_auc_score(y_true, y_prob):.2f}")
plt.plot([0,1],[0,1],'--')
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.legend()
plt.show()

# ========================
# 🔥 Grad-CAM (Interpretability)
# ========================
from pytorch_grad_cam import GradCAM
from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
from pytorch_grad_cam.utils.image import show_cam_on_image

target_layer = model.features[-1]
cam = GradCAM(model=model, target_layers=[target_layer], use_cuda=(DEVICE=="cuda"))

# ambil contoh 1 gambar TB
sample_path = val_images[0]
sample_img = val_transforms(sample_path)
input_tensor = sample_img.unsqueeze(0).to(DEVICE)
rgb_tensor = sample_img.repeat(3,1,1).permute(1,2,0).numpy()
rgb_tensor = (rgb_tensor - rgb_tensor.min())/(rgb_tensor.max()-rgb_tensor.min())

targets = [ClassifierOutputTarget(1)]
grayscale_cam = cam(input_tensor=input_tensor.repeat(1,3,1,1), targets=targets)[0]
visualization = show_cam_on_image(rgb_tensor, grayscale_cam, use_rgb=True)
plt.imshow(visualization)
plt.axis('off')
plt.show()

2) 5-Fold Cross Validation (otomatis)

Kode: 5-Fold CV
# ==========================================
# 🏥 TB Detection - 5 Fold CV with PyTorch + MONAI
# ==========================================

!pip install monai[all] torch torchvision albumentations scikit-learn grad-cam matplotlib tqdm opencv-python --quiet

import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Subset
from torchvision.models import efficientnet_b0
from monai.transforms import (
    Compose, LoadImage, AddChannel, Resize,
    ScaleIntensity, RandFlip, RandRotate, RandZoom,
    EnsureType
)
from monai.data import ImageDataset
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score, roc_curve, confusion_matrix
import matplotlib.pyplot as plt
from tqdm import tqdm

# ========================
# ⚙️ Config
# ========================
DATA_DIR = "dataset"
IMG_SIZE = (224, 224)
BATCH_SIZE = 16
EPOCHS = 8
LR = 1e-4
N_FOLDS = 5
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)

# ========================
# 🧼 Transform
# ========================
train_transforms = Compose([
    LoadImage(image_only=True),
    AddChannel(),
    Resize(IMG_SIZE),
    ScaleIntensity(),
    RandFlip(spatial_axis=1, prob=0.5),
    RandRotate(range_x=np.pi/12, prob=0.5),
    RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),
    EnsureType()
])

val_transforms = Compose([
    LoadImage(image_only=True),
    AddChannel(),
    Resize(IMG_SIZE),
    ScaleIntensity(),
    EnsureType()
])

# ========================
# 📂 Load all images and labels
# ========================
image_paths = []
labels = []

for label_idx, label_name in enumerate(["Normal", "TB"]):
    class_path = os.path.join(DATA_DIR, label_name)
    imgs = [os.path.join(class_path, f) for f in os.listdir(class_path)]
    image_paths.extend(imgs)
    labels.extend([label_idx] * len(imgs))

image_paths = np.array(image_paths)
labels = np.array(labels)

print(f"Total dataset: {len(image_paths)} images")

# ========================
# 🧠 Model Function
# ========================
def create_model():
    model = efficientnet_b0(weights='IMAGENET1K_V1')
    model.classifier[1] = nn.Linear(model.classifier[1].in_features, 2)
    return model.to(DEVICE)

# ========================
# 🚀 Train / Validate Loop
# ========================
def train_epoch(model, loader, optimizer, criterion):
    model.train()
    total_loss, total_correct, total = 0, 0, 0
    for imgs, lbls in loader:
        imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE)
        optimizer.zero_grad()
        outputs = model(imgs.repeat(1,3,1,1))
        loss = criterion(outputs, lbls)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        preds = outputs.argmax(1)
        total_correct += (preds == lbls).sum().item()
        total += lbls.size(0)
    return total_loss/len(loader), total_correct/total

def validate_epoch(model, loader, criterion):
    model.eval()
    total_loss, total_correct, total = 0, 0, 0
    y_true, y_prob = [], []
    with torch.no_grad():
        for imgs, lbls in loader:
            imgs, lbls = imgs.to(DEVICE), lbls.to(DEVICE)
            outputs = model(imgs.repeat(1,3,1,1))
            loss = criterion(outputs, lbls)
            total_loss += loss.item()

            probs = torch.softmax(outputs, dim=1)[:, 1]
            preds = outputs.argmax(1)
            total_correct += (preds == lbls).sum().item()
            total += lbls.size(0)

            y_true.extend(lbls.cpu().numpy())
            y_prob.extend(probs.cpu().numpy())
    return total_loss/len(loader), total_correct/total, np.array(y_true), np.array(y_prob)

# ========================
# 🧪 Cross Validation
# ========================
kf = StratifiedKFold(n_splits=N_FOLDS, shuffle=True, random_state=SEED)
fold_metrics = []

for fold, (train_idx, val_idx) in enumerate(kf.split(image_paths, labels)):
    print(f"\n===== Fold {fold+1}/{N_FOLDS} =====")
    model = create_model()
    optimizer = torch.optim.AdamW(model.parameters(), lr=LR)
    criterion = nn.CrossEntropyLoss()

    train_ds = ImageDataset(image_paths[train_idx].tolist(), labels[train_idx].tolist(), transform=train_transforms)
    val_ds = ImageDataset(image_paths[val_idx].tolist(), labels[val_idx].tolist(), transform=val_transforms)

    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)
    val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)

    best_auc = 0
    for epoch in range(EPOCHS):
        tr_loss, tr_acc = train_epoch(model, train_loader, optimizer, criterion)
        val_loss, val_acc, y_true, y_prob = validate_epoch(model, val_loader, criterion)
        auc = roc_auc_score(y_true, y_prob)
        print(f"Epoch {epoch+1}/{EPOCHS} - Train Acc: {tr_acc:.3f} | Val Acc: {val_acc:.3f} | AUC: {auc:.3f}")
        if auc > best_auc:
            best_auc = auc
            torch.save(model.state_dict(), f"best_fold{fold+1}.pt")

    # evaluate best model for this fold
    model.load_state_dict(torch.load(f"best_fold{fold+1}.pt"))
    _, _, y_true, y_prob = validate_epoch(model, val_loader, criterion)
    y_pred = (y_prob > 0.5).astype(int)
    cm = confusion_matrix(y_true, y_pred)
    fold_metrics.append({
        "fold": fold+1,
        "auc": roc_auc_score(y_true, y_prob),
        "acc": np.mean(y_pred == y_true),
        "cm": cm
    })

# ========================
# 📊 CV Result Summary
# ========================
aucs = [m["auc"] for m in fold_metrics]
accs = [m["acc"] for m in fold_metrics]
print("\n===== 5-Fold CV Result =====")
for m in fold_metrics:
    print(f"Fold {m['fold']} - AUC: {m['auc']:.3f} | Acc: {m['acc']:.3f}")
    print(m['cm'])
print(f"Mean AUC: {np.mean(aucs):.3f} ± {np.std(aucs):.3f}")
print(f"Mean Acc: {np.mean(accs):.3f} ± {np.std(accs):.3f}")

# ========================
# 🫀 Optional: Grad-CAM Example
# ========================
from pytorch_grad_cam import GradCAM
from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
from pytorch_grad_cam.utils.image import show_cam_on_image

# Pakai model fold terakhir
target_layer = model.features[-1]
cam = GradCAM(model=model, target_layers=[target_layer], use_cuda=(DEVICE=="cuda"))

sample_path = image_paths[val_idx[0]]
sample_img = val_transforms(sample_path)
input_tensor = sample_img.unsqueeze(0).to(DEVICE)
rgb_tensor = sample_img.repeat(3,1,1).permute(1,2,0).numpy()
rgb_tensor = (rgb_tensor - rgb_tensor.min())/(rgb_tensor.max()-rgb_tensor.min())

targets = [ClassifierOutputTarget(1)]
grayscale_cam = cam(input_tensor=input_tensor.repeat(1,3,1,1), targets=targets)[0]
visualization = show_cam_on_image(rgb_tensor, grayscale_cam, use_rgb=True)
plt.imshow(visualization)
plt.axis('off')
plt.title(f"Grad-CAM — {os.path.basename(sample_path)}")
plt.show()

3) 5-Fold CV + Simple Lung Mask (thresholding)

Kode: 5-Fold CV + simple threshold lung mask
# ==========================================
# 🏥 TB Detection - 5 Fold CV + Lung Segmentation (simple mask)
# ==========================================

!pip install monai[all] torch torchvision albumentations scikit-learn grad-cam matplotlib tqdm opencv-python --quiet

import os
import numpy as np
import cv2
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision.models import efficientnet_b0
from monai.transforms import (
    Compose, LoadImage, AddChannel, Resize,
    ScaleIntensity, RandFlip, RandRotate, RandZoom,
    EnsureType
)
from monai.data import ImageDataset
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score, confusion_matrix
import matplotlib.pyplot as plt
from tqdm import tqdm

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
IMG_SIZE = (224, 224)
BATCH_SIZE = 16
EPOCHS = 8
LR = 1e-4
N_FOLDS = 5
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)

# ========================
# 🫁 Simple Lung Segmentation Function (Thresholding + Contour)
# ========================
def simple_lung_mask(image_np):
    """
    image_np: array 2D (grayscale)
    output: binary mask paru
    """
    img = cv2.equalizeHist((image_np * 255).astype(np.uint8))
    blur = cv2.GaussianBlur(img, (5,5), 0)
    _, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    th = cv2.bitwise_not(th)

    contours, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    mask = np.zeros_like(th)
    for c in contours:
        if cv2.contourArea(c) > 500:  # filter area kecil
            cv2.drawContours(mask, [c], -1, 255, -1)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((7,7), np.uint8))
    mask = mask.astype(np.float32) / 255.0
    return mask

# ========================
# 🧼 Transform dengan Lung Mask
# ========================
class LungMaskTransform:
    def __call__(self, img):
        arr = img[0].cpu().numpy()
        mask = simple_lung_mask(arr)
        masked = arr * mask
        return torch.tensor(masked).unsqueeze(0)

train_transforms = Compose([
    LoadImage(image_only=True),
    AddChannel(),
    Resize(IMG_SIZE),
    ScaleIntensity(),
    RandFlip(spatial_axis=1, prob=0.5),
    RandRotate(range_x=np.pi/12, prob=0.5),
    RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),
    EnsureType(),
    LungMaskTransform()
])

val_transforms = Compose([
    LoadImage(image_only=True),
    AddChannel(),
    Resize(IMG_SIZE),
    ScaleIntensity(),
    EnsureType(),
    LungMaskTransform()
])

# rest of CV code same as 5-fold example (create_model, train/validate loops, kfold)...

4) 5-Fold CV + Pretrained U-Net (masking before classification)

Kode: 5-Fold CV + Pretrained U-Net
# ==========================================
# 🏥 TB Detection - 5 Fold CV + Pretrained U-Net Lung Segmentation
# ==========================================

!pip install monai[all] torch torchvision scikit-learn albumentations grad-cam matplotlib opencv-python tqdm --quiet

import os
import numpy as np
import cv2
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision.models import efficientnet_b0
from monai.transforms import (
    Compose, LoadImage, AddChannel, Resize,
    ScaleIntensity, EnsureType
)
from monai.data import ImageDataset
from monai.networks.nets import UNet
from monai.inferers import sliding_window_inference
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score, confusion_matrix
import matplotlib.pyplot as plt
from tqdm import tqdm

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
IMG_SIZE = (224, 224)
BATCH_SIZE = 16
EPOCHS = 8
LR = 1e-4
N_FOLDS = 5
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)

# ========================
# 🫁 Pretrained Lung Segmentation Model
# ========================
lung_seg_model = UNet(
    spatial_dims=2,
    in_channels=1,
    out_channels=1,
    channels=(16, 32, 64, 128, 256),
    strides=(2, 2, 2, 2),
    num_res_units=2,
).to(DEVICE)

WEIGHT_PATH = "lung_unet_pretrained.pth"
lung_seg_model.load_state_dict(torch.load(WEIGHT_PATH, map_location=DEVICE))
lung_seg_model.eval()

def segment_lungs(image_tensor):
    with torch.no_grad():
        pred = sliding_window_inference(image_tensor.unsqueeze(0).to(DEVICE), (224,224), 1, lung_seg_model)
        pred = torch.sigmoid(pred)
        mask = (pred[0,0].cpu().numpy() > 0.5).astype(np.float32)
    return mask

# Custom transform with LungSegTransform + base transforms and rest of CV code (create_model, train/validate, kfold)...

Training U-Net (segmentasi paru) — dari awal

Notebook untuk melatih U-Net 2D sederhana pada dataset <images, masks>.

Kode: Training U-Net (MONAI)
# ==========================================
# 🫁 Training U-Net untuk Segmentasi Paru
# ==========================================

!pip install monai[all] torch torchvision scikit-learn albumentations tqdm matplotlib opencv-python --quiet

import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from monai.networks.nets import UNet
from monai.transforms import (
    Compose, LoadImage, AddChannel, Resize,
    ScaleIntensity, EnsureType, RandFlip, RandRotate, RandZoom
)
from monai.data import CacheDataset
from monai.losses import DiceLoss
from monai.metrics import DiceMetric
from monai.inferers import sliding_window_inference
import matplotlib.pyplot as plt
from tqdm import tqdm

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
IMG_SIZE = (224, 224)
BATCH_SIZE = 8
EPOCHS = 30
LR = 1e-4
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)

DATA_DIR = "lung_seg_dataset"  # ganti sesuai lokasi data Anda
IMAGE_DIR = os.path.join(DATA_DIR, "images")
MASK_DIR = os.path.join(DATA_DIR, "masks")

image_paths = sorted([os.path.join(IMAGE_DIR, f) for f in os.listdir(IMAGE_DIR)])
mask_paths = sorted([os.path.join(MASK_DIR, f) for f in os.listdir(MASK_DIR)])
print(f"Total data: {len(image_paths)}")

# transforms, dataloaders, model, loss (DiceLoss), training loop...
# Simpan model sebagai lung_unet_pretrained.pth

U-Net + Transfer Learning (Segmentation Models PyTorch)

Versi U-Net dengan encoder pretrained (EfficientNet / ResNet) via segmentation_models_pytorch untuk akurasi lebih tinggi.

Kode: U-Net + pretrained encoder (SMP)
# ==========================================
# 🫁 U-Net + Transfer Learning untuk Segmentasi Paru
# ==========================================

!pip install segmentation-models-pytorch monai[all] torch torchvision albumentations tqdm matplotlib opencv-python --quiet

import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from monai.transforms import (
    Compose, LoadImage, AddChannel, ScaleIntensity, Resize,
    EnsureType, RandFlip, RandRotate, RandZoom
)
from monai.data import CacheDataset
from monai.losses import DiceLoss
from monai.metrics import DiceMetric
from monai.inferers import sliding_window_inference
import segmentation_models_pytorch as smp
import matplotlib.pyplot as plt
from tqdm import tqdm

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
IMG_SIZE = (224, 224)
BATCH_SIZE = 8
EPOCHS = 30
LR = 1e-4
SEED = 42
torch.manual_seed(SEED)
np.random.seed(SEED)

# dataset setup (IMAGE_DIR, MASK_DIR), transforms, dataloaders...

ENCODER = "efficientnet-b0"
model = smp.Unet(encoder_name=ENCODER, encoder_weights="imagenet", in_channels=1, classes=1, activation=None).to(DEVICE)

# loss Dice, optimizer AdamW, training loop...
# Simpan model sebagai lung_unet_transfer_efficientnet-b0.pth

Inference: Mask Paru Otomatis (semua file)

Script untuk memproses semua CXR dalam folder, menghasilkan mask (png) dan gambar ter-masking siap dipakai untuk klasifikasi.

Kode: Inference masker paru otomatis
# ==========================================
# 🫁 Inference Mask Paru Otomatis untuk Dataset CXR
# ==========================================

import os
import torch
import numpy as np
import cv2
from tqdm import tqdm
from monai.transforms import (
    Compose, LoadImage, AddChannel, Resize, ScaleIntensity, EnsureType
)
from monai.inferers import sliding_window_inference
import segmentation_models_pytorch as smp

# ========================
# ⚙️ Pengaturan
# ========================
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
IMG_SIZE = (224, 224)
ENCODER = "efficientnet-b0"  # sama dengan model training
MODEL_PATH = f"lung_unet_transfer_{ENCODER}.pth"

INPUT_DIR = "dataset_TB/raw"        # folder berisi CXR asli
OUTPUT_MASK_DIR = "dataset_TB/mask" # folder output mask paru
OUTPUT_CLEAN_DIR = "dataset_TB/clean" # folder output masked image

os.makedirs(OUTPUT_MASK_DIR, exist_ok=True)
os.makedirs(OUTPUT_CLEAN_DIR, exist_ok=True)

# Load model, transform, apply mask, save mask & masked image...

Catatan Etika, Evaluasi & Saran untuk Tesis

  • Etika & privasi: de-identify DICOM metadata, dapatkan IRB / persetujuan etik sebelum gunakan data pasien.
  • Evaluasi klinis: sertakan review radiolog pada Grad-CAM & visualisasi mask untuk konfirmasi klinis.
  • Metrik: AUC, sensitivity (penting untuk screening), specificity, precision, F1, PR-AUC; untuk segmentation: Dice/IoU.
  • Analisis error: periksa false positives (mis. scarring, devices) & false negatives.
  • Perbandingan eksperimen: laporkan baseline vs threshold mask vs U-Net mask, sertakan statistik (paired t-test / Wilcoxon) bila mengklaim perbedaan signifikan.

Rekomendasi teknis singkat

KomponenRekomendasi
FrameworkPyTorch + MONAI (+ segmentation-models-pytorch untuk U-Net transfer)
Backbone klasifikasiEfficientNet-B0 / DenseNet121 / ResNet50
Segmentation backboneEfficientNet-B0 (encoder pada SMP) atau ResNet34
AugmentasiFlip, rotate, scale, brightness/contrast, MixUp/CutMix

File ini dibuat untuk catatan pribadi Anda — silakan edit/format ulang untuk blog Anda. Jika mau saya kemas jadi file HTML siap download (atau versi markdown), bilang saja.

Jika ingin bagian tertentu diubah gaya/struktur (mis. menambahkan gambar/ROC plot/ikon), saya bantu modifikasi.

إرسال تعليق