Shape2Force / experiments /manifest_data.py
kaveh's picture
experiments: dataset audit, evaluation, baselines and ablations
1d4bac8
Raw History Blame Contribute Delete
4.47 kB
"""
Manifest-based data loading. Uses exactly the preprocessing of the S2F package
(``data.cell_dataset.load_image`` -> grayscale, resize to 1024, /255; ``ImageDataset``;
``AdvancedAugmentations``), so results are comparable with the released models.
substrate_mode (single-cell only) decides which substrate setting builds the two substrate channels:
'legacy' what the original training code fed (derived from the all_tune folder name) - default,
reproduces the conditions under which the released model was trained
'nominal' the true Young's modulus / TFM pixel-size setting of each sample
'reference' the 6 kPa reference setting (fibroblasts_PDMS) for every sample
'none' no metadata (models without substrate channels)
input_mode:
'bf' bright-field image (default)
'mask' binary cell mask (file cell_mask.tif/png in the sample folder) - mask-only baseline
"""
import os
import numpy as np
import torch
from torch.utils.data import DataLoader
from data.cell_dataset import ImageDataset, load_image
from experiments.layout import DEFAULT_DATASET_ROOT, MASK_NAMES, manifest_path, read_manifest
def _find(folder, suffix=None, names=None):
for f in sorted(os.listdir(folder)):
if (suffix and f.endswith(suffix)) or (names and f in names):
return os.path.join(folder, f)
return None
def load_manifest_pairs(manifest, dataset_root=DEFAULT_DATASET_ROOT, img_size=1024,
substrate_mode="legacy", input_mode="bf"):
rows = read_manifest(manifest if os.path.isfile(manifest) else manifest_path(manifest))
pairs = []
for r in rows:
d = os.path.join(dataset_root, r["sample_dir"])
bf = _find(d, suffix="BF_001.tif")
hm = _find(d, suffix="_gray.jpg")
if bf is None or hm is None:
raise FileNotFoundError(f"missing BF/heatmap in {d}")
if input_mode == "mask":
mp = _find(d, names=MASK_NAMES)
if mp is None:
raise FileNotFoundError(f"input_mode='mask' but no {MASK_NAMES} in {d}")
x = (load_image(mp, (img_size, img_size)) > 0.5).astype(np.float32)
else:
x = load_image(bf, (img_size, img_size))
y = load_image(hm, (img_size, img_size))
if substrate_mode == "none":
pairs.append((x, y, (0, 0)))
continue
sub = {"legacy": r.get("substrate_legacy"), "nominal": r.get("substrate_nominal"),
"reference": "fibroblasts_PDMS"}[substrate_mode] or "fibroblasts_PDMS"
pairs.append((x, y, (0, 0), {"folder_name": os.path.basename(d), "substrate": sub,
"root_folder": os.path.dirname(r["sample_dir"]), "set": r.get("set", "")}))
return pairs, rows
def _worker_init(worker_id):
"""Distinct, reproducible numpy seed per worker (augmentations use np.random). Module-level for Windows spawn."""
np.random.seed(torch.initial_seed() % 2 ** 32)
class _MaskDataset(ImageDataset):
"""Re-binarise the input after augmentation (intensity augmentations are meaningless for masks)."""
def __getitem__(self, idx):
out = list(super().__getitem__(idx))
out[0] = (out[0] > 0.5).float()
return tuple(out)
def make_loader(manifest, dataset_root=DEFAULT_DATASET_ROOT, batch_size=2, img_size=1024, augment=False,
shuffle=False, substrate_mode="legacy", input_mode="bf", seed=None, num_workers=0):
pairs, rows = load_manifest_pairs(manifest, dataset_root, img_size, substrate_mode, input_mode)
transform = None
if augment:
from data.augmentations import AdvancedAugmentations
transform = AdvancedAugmentations((img_size, img_size))
cls = _MaskDataset if input_mode == "mask" else ImageDataset
ds = cls(pairs, transform=transform, channel_first=True, threshold=0.0,
return_metadata=substrate_mode != "none")
ds.rows = rows
g = None
if seed is not None:
g = torch.Generator(); g.manual_seed(seed)
if num_workers > 0: # workers run the augmentations so the GPU is not left waiting
return DataLoader(ds, batch_size=batch_size, shuffle=shuffle, generator=g, num_workers=num_workers,
persistent_workers=True, pin_memory=torch.cuda.is_available(),
worker_init_fn=_worker_init)
return DataLoader(ds, batch_size=batch_size, shuffle=shuffle, generator=g, num_workers=0)