sandy45's picture
Deploy trained ChestViT Space
37fbec9
Raw
History Blame Contribute Delete
6.41 kB
"""
data/dataset.py
---------------
PyTorch Dataset for NIH ChestX-ray14.
Key design decisions:
- Uses Hugging Face `datasets` for streaming from `BahaaEldin0/NIH-Chest-Xray-14`.
- Avoids 45GB local download, scales natively up to 112k images.
- Multi-hot label encoding for 14 simultaneous disease labels.
- CLAHE preprocessing baked into the transform pipeline.
"""
import torch
from torch.utils.data import IterableDataset, DataLoader
from datasets import load_dataset
import numpy as np
import logging
import cv2
from PIL import Image
from data.preprocessing import get_train_transforms, get_val_transforms, apply_clahe
logger = logging.getLogger(__name__)
# ── 14 NIH disease labels ─────────────────────────────────────────────────────
DISEASE_LABELS = [
"Atelectasis",
"Cardiomegaly",
"Effusion",
"Infiltration",
"Mass",
"Nodule",
"Pneumonia",
"Pneumothorax",
"Consolidation",
"Edema",
"Emphysema",
"Fibrosis",
"Pleural_Thickening",
"Hernia",
]
NUM_CLASSES = len(DISEASE_LABELS)
def encode_labels(labels) -> np.ndarray:
"""
Convert a list of string labels (or a pipe separated string) into a 14-dim multi-hot vector.
"""
if isinstance(labels, str):
if labels == "No Finding":
labels_list = []
else:
labels_list = labels.split("|")
else:
labels_list = labels
label_vec = np.zeros(NUM_CLASSES, dtype=np.float32)
for disease in labels_list:
disease = disease.strip()
if disease in DISEASE_LABELS:
label_vec[DISEASE_LABELS.index(disease)] = 1.0
return label_vec
class HFStreamingChestXrayDataset(IterableDataset):
"""
Wrapper around Hugging Face IterableDataset for PyTorch DataLoader compatibility.
"""
def __init__(
self,
hf_iterable,
image_size: int = 224,
transform=None,
clip_limit: float = 2.0,
tile_size: int = 8,
take_limit: int = None
):
self.hf_iterable = hf_iterable
self.image_size = image_size
self.transform = transform
self.clip_limit = clip_limit
self.tile_size = tile_size
self.take_limit = take_limit
def __iter__(self):
iterable = self.hf_iterable
if self.take_limit is not None:
iterable = iterable.take(self.take_limit)
for idx, item in enumerate(iterable):
image = item.get('image') or item.get('Image')
if image is None:
continue
# Ensure image is PIL Image and convert to grayscale numpy array
if not isinstance(image, Image.Image):
continue
img_gray = np.array(image.convert("L"))
# CLAHE
img_clahe = apply_clahe(img_gray, clip_limit=self.clip_limit, tile_size=self.tile_size)
# Resize BEFORE stacking to save memory/computation
img_resized = cv2.resize(img_clahe, (self.image_size, self.image_size), interpolation=cv2.INTER_AREA)
# Convert to RGB numpy array for Albumentations
img_rgb = np.stack([img_resized, img_resized, img_resized], axis=-1)
# Transform
if self.transform:
augmented = self.transform(image=img_rgb)
img_tensor = augmented["image"]
else:
img_tensor = torch.from_numpy(img_rgb.transpose(2, 0, 1)).float() / 255.0
# Labels
label_str_list = item.get('label', [])
label_vec = encode_labels(label_str_list)
label_tensor = torch.tensor(label_vec, dtype=torch.float32)
# Yield path as Patient ID or fallback to str index to satisfy interface
patient_id = str(item.get('Patient ID', idx))
yield img_tensor, label_tensor, patient_id
def build_dataloaders(
images_dir=None,
labels_csv=None,
train_list_txt=None,
test_list_txt=None,
image_size: int = 224,
batch_size: int = 8,
num_workers: int = 0,
pin_memory: bool = True,
val_split: float = 0.1,
train_fraction: float = 1.0,
seed: int = 42,
train_take_limit: int = 20000,
val_take_limit: int = 2000,
):
"""
Build train, validation, and test DataLoaders directly from the HF Stream.
Uses generic fallback args for compatibility with old code calls.
"""
print(f" [Dataset] Loading HF Stream BahaaEldin0/NIH-Chest-Xray-14...")
# In order to stream properly we load each split
ds = load_dataset('BahaaEldin0/NIH-Chest-Xray-14', streaming=True)
train_transform = get_train_transforms(image_size)
val_transform = get_val_transforms(image_size)
# Note: BahaaEldin0 dataset has 'train', 'valid', 'test' splits
train_stream = ds['train']
val_stream = ds['valid']
test_stream = ds['test']
# Shuffle the training stream slightly for better randomness (buffer_size=1000)
train_stream = train_stream.shuffle(buffer_size=1000, seed=seed)
# If train_fraction < 1.0, we can adjust take limits accordingly.
if train_fraction < 1.0 and train_take_limit is not None:
train_take_limit = int(train_take_limit * train_fraction)
train_ds = HFStreamingChestXrayDataset(train_stream, image_size=image_size, transform=train_transform, take_limit=train_take_limit)
val_ds = HFStreamingChestXrayDataset(val_stream, image_size=image_size, transform=val_transform, take_limit=val_take_limit)
test_ds = HFStreamingChestXrayDataset(test_stream, image_size=image_size, transform=val_transform, take_limit=val_take_limit)
dl_kwargs = dict(
batch_size=batch_size,
num_workers=num_workers,
pin_memory=pin_memory,
)
train_loader = DataLoader(train_ds, **dl_kwargs)
val_loader = DataLoader(val_ds, **dl_kwargs)
test_loader = DataLoader(test_ds, **dl_kwargs)
# Calculate uniform weights (streaming mode makes exact calculation slow)
class_weights = torch.ones(NUM_CLASSES, dtype=torch.float32)
print(f"\n DataLoaders ready:")
print(f" Train limit: {train_take_limit if train_take_limit else 'Full'}")
print(f" Val/Test limit: {val_take_limit if val_take_limit else 'Full'}")
return train_loader, val_loader, test_loader, class_weights