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.
Pipeline Teknis (Step-by-step)
- Quality check & labeling — verifikasi label TB vs non-TB, periksa orientasi, duplikat.
- Preprocessing — konversi grayscale, normalisasi, resize (224×224 atau 384×384), optional CLAHE.
- Lung segmentation (opsional tapi disarankan) — U-Net pretrained untuk mask paru.
- Augmentation & balancing — flips, rotations, brightness/contrast, MixUp/CutMix.
- Model baseline — EfficientNet-B0 / DenseNet121 / ResNet50 (transfer learning).
- Training — Binary CE (atau focal loss), AdamW, scheduler, early stopping.
- Validation — Stratified 5-fold CV, laporkan AUC, sensitivity, specificity, PR AUC.
- Explainability — Grad-CAM untuk inspeksi radiolog.
- 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
# ======================================
# 🏥 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)
# ==========================================
# 🏥 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)
# ==========================================
# 🏥 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)
# ==========================================
# 🏥 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>.
# ==========================================
# 🫁 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.
# ==========================================
# 🫁 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.
# ==========================================
# 🫁 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
| Komponen | Rekomendasi |
|---|---|
| Framework | PyTorch + MONAI (+ segmentation-models-pytorch untuk U-Net transfer) |
| Backbone klasifikasi | EfficientNet-B0 / DenseNet121 / ResNet50 |
| Segmentation backbone | EfficientNet-B0 (encoder pada SMP) atau ResNet34 |
| Augmentasi | Flip, rotate, scale, brightness/contrast, MixUp/CutMix |