Spaces:
Running on Zero
Running on Zero
File size: 6,030 Bytes
f66bbd0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | """
datasets.py — Image/mask/fixation loaders for the two reported benchmarks,
plus a small base class so adding a third dataset is just a few lines.
Expected folder layout (same for both datasets):
<root>/
images/ one image per case (.jpg/.png for Kvasir, .dcm for prostate MRI)
masks/ matching binary mask, same basename, .png
<fixation_csv>
one row per fixation, see gazerefine.gaze for the expected columns.
The IMAGE column must match an images/ filename (Kvasir) or the
DICOM basename + ".jpg" (prostate — fixation collection was run on
JPEG-rendered slices while the model reads the original DICOM).
"""
from __future__ import annotations
import os
import numpy as np
import pandas as pd
import torch
from PIL import Image
from torch.utils.data import Dataset
import torchvision.transforms as T
from .constants import IMG_SIZE, IMG_MEAN, IMG_STD
from .gaze import get_scanpath, IMG_COL
class _BaseGazeDataset(Dataset):
"""Shared image/mask transform + fixation-grouping logic.
Subclasses only need to implement ``_load_image(name)`` and provide the
set of valid image ids that have a matching mask and fixation entries.
"""
def __init__(self, root: str, fixation_csv: str, img_size: int = IMG_SIZE):
self.root = root
self.img_dir = os.path.join(root, "images")
self.mask_dir = os.path.join(root, "masks")
self.img_size = img_size
self.df = pd.read_csv(fixation_csv)
self.df.columns = self.df.columns.str.strip()
self.fix_df = self.df.groupby(IMG_COL)
self.max_len = int(self.df.groupby(IMG_COL).size().max())
print(f"[Dataset] max scanpath length = {self.max_len}")
self.image_ids: list[str] = [] # set by subclass __init__
self.img_tf = T.Compose([
T.Resize((img_size, img_size)),
T.ToTensor(),
T.Normalize(IMG_MEAN, IMG_STD),
])
self.mask_tf = T.Compose([
T.Resize((img_size, img_size), interpolation=T.InterpolationMode.NEAREST),
T.ToTensor(),
])
def __len__(self):
return len(self.image_ids)
def _load_image(self, name: str) -> Image.Image:
raise NotImplementedError
def _mask_path(self, name: str) -> str:
raise NotImplementedError
def _fixation_key(self, name: str) -> str:
"""CSV IMAGE-column key for this sample. Override if it differs from
the on-disk basename (e.g. prostate MRI uses .jpg keys for .dcm files)."""
return name
def __getitem__(self, idx: int):
name = self.image_ids[idx]
image = self.img_tf(self._load_image(name))
mask = self.mask_tf(Image.open(self._mask_path(name)).convert("L"))
mask = (mask > 0.5).float()
fix_rows = self.fix_df.get_group(self._fixation_key(name))
fixation = get_scanpath(fix_rows, self.max_len)
return {"image": image, "fixation": fixation, "mask": mask, "name": name}
class KvasirSEGDataset(_BaseGazeDataset):
"""Kvasir-SEG colonoscopy polyp segmentation. images/masks share filenames
(e.g. ``cju0qkwl35piu0993l0dewei2.jpg`` in both folders)."""
def __init__(self, root: str, fixation_csv: str, img_size: int = IMG_SIZE):
super().__init__(root, fixation_csv, img_size)
img_files = set(os.listdir(self.img_dir))
mask_files = set(os.listdir(self.mask_dir))
csv_imgs = set(self.df[IMG_COL].unique())
self.image_ids = sorted(img_files & mask_files & csv_imgs)
if not self.image_ids:
raise RuntimeError("No overlap between images/, masks/ and the fixation CSV.")
print(f"[Dataset] Kvasir-SEG valid samples = {len(self.image_ids)}")
def _load_image(self, name: str) -> Image.Image:
return Image.open(os.path.join(self.img_dir, name)).convert("RGB")
def _mask_path(self, name: str) -> str:
return os.path.join(self.mask_dir, name)
class ProstateMRIDataset(_BaseGazeDataset):
"""NCI-ISBI prostate MRI. images/ holds DICOM (.dcm), masks/ holds PNG,
and the fixation CSV references each case as ``<basename>.jpg`` (the
format the eye-tracking session was actually rendered/displayed in)."""
def __init__(self, root: str, fixation_csv: str, img_size: int = IMG_SIZE):
super().__init__(root, fixation_csv, img_size)
dcm_basenames = {os.path.splitext(f)[0] for f in os.listdir(self.img_dir) if f.endswith(".dcm")}
png_basenames = {os.path.splitext(f)[0] for f in os.listdir(self.mask_dir) if f.endswith(".png")}
csv_basenames = {os.path.splitext(f)[0] for f in self.df[IMG_COL].unique()}
self.image_ids = sorted(dcm_basenames & png_basenames & csv_basenames)
if not self.image_ids:
raise RuntimeError("No overlap between images/ (.dcm), masks/ (.png) and the fixation CSV.")
print(f"[Dataset] Prostate MRI valid samples = {len(self.image_ids)}")
def _load_image(self, name: str) -> Image.Image:
import pydicom # local import: optional dependency, only needed for DICOM datasets
dcm_path = os.path.join(self.img_dir, f"{name}.dcm")
dicom = pydicom.dcmread(dcm_path)
arr = dicom.pixel_array.astype(np.float32)
if arr.max() > arr.min():
arr = (arr - arr.min()) / (arr.max() - arr.min())
else:
arr = np.zeros_like(arr)
rgb = np.stack([arr, arr, arr], axis=-1) # grayscale -> 3-channel, DINOv3 expects RGB
return Image.fromarray((rgb * 255).astype(np.uint8))
def _mask_path(self, name: str) -> str:
return os.path.join(self.mask_dir, f"{name}.png")
def _fixation_key(self, name: str) -> str:
return f"{name}.jpg"
DATASET_REGISTRY = {
"kvasir": KvasirSEGDataset,
"prostate_mri": ProstateMRIDataset,
}
|