ckadirt's picture
Add files using upload-large-folder tool
c324432 verified
Raw
History Blame Contribute Delete
20.4 kB
import gzip
import json
import os
import random
from fnmatch import fnmatch
from functools import partial
from pathlib import Path
from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, Union
import numpy as np
import torch
import webdataset as wds
from torch.utils.data import IterableDataset
from mae_utils.misc import zscore
shared1000 = np.where(np.load("/weka/proj-medarc/shared/mindeyev2_dataset/shared1000.npy"))[0]
HCP_FLAT_ROOT = "https://huggingface.co/datasets/bold-ai/HCP-Flat/resolve/main"
HCP_NUM_SHARDS = 1803
NSD_NUM_SHARDS = 300
FRAME_SIZE_BYTES = 29859
HCP_MASK_SIZE = 29859
# Tasks and conditions used in prior works (Zhang, 2021; Rastegarnia, 2023)
INCLUDE_TASKS = {
"EMOTION", "LANGUAGE", "MOTOR", "RELATIONAL", "SOCIAL", "WM"
}
INCLUDE_CONDS = {
"fear",
"neut",
"math",
"story",
"lf",
"lh",
"rf",
"rh",
"t",
"match",
"relation",
"mental",
"rnd",
"0bk_body",
"2bk_body",
"0bk_faces",
"2bk_faces",
"0bk_places",
"2bk_places",
"0bk_tools",
"2bk_tools",
}
HCP_TR = {"3T": 0.72, "7T": 1.0}
DEFAULT_DELAY_SECS = 4 * 0.72
##### NSD #####
def create_nsd_flat(
root: Optional[str] = "/weka/proj-medarc/shared/NSD-Flat",
shards: Optional[Union[int, Iterable[int]]] = 300,
frames: int = 16,
shuffle: Optional[bool] = True,
buffer_size_mb: int = 3840,
gsr: Optional[bool] = True,
sub: Optional[str] = None,
run: Optional[str] = None,
mindeye_only: Optional[bool] = False,
only_shared1000: Optional[bool] = False,
mindeye_TR_delay: int = 3,
) -> wds.WebDataset:
"""
Create NSD-Flat dataset. Yields samples of (key, images) where key is the webdataset
sample key and images is shape (C, T, H, W).
"""
urls = get_nsd_flat_urls(root, shards)
clipping = seq_clips(frames, mindeye_only=mindeye_only, only_shared1000=only_shared1000, mindeye_TR_delay=mindeye_TR_delay)
if shuffle:
buffer_size = int(buffer_size_mb * 1024 * 1024 / (frames * FRAME_SIZE_BYTES))
print(f"Shuffle buffer size: {buffer_size}")
else:
buffer_size = 0
if gsr:
dataset = (
wds.WebDataset(
urls,
resampled=shuffle,
shardshuffle=1000 if shuffle else False,
nodesplitter=wds.split_by_node,
select_files=partial(select_files, sub=sub, run=run),
)
.decode()
.map(partial(extract_sample,gsr=gsr))
.compose(clipping)
.shuffle(buffer_size)
.map_tuple(partial(to_tensor, mask=load_nsd_flat_mask()))
)
else:
dataset = (
wds.WebDataset(
urls,
resampled=shuffle,
shardshuffle=1000 if shuffle else False,
nodesplitter=wds.split_by_node,
select_files=partial(select_files, sub=sub, run=run),
)
.decode()
.map(partial(extract_sample,gsr=gsr))
.compose(clipping)
.shuffle(buffer_size)
.map_tuple(partial(to_tensor_gsrFalse, mask=load_nsd_flat_mask()))
)
return dataset
def get_nsd_flat_urls(
root: Optional[str] = None,
shards: Optional[Union[int, Iterable[int]]] = None,
):
if isinstance(shards, int):
shards = range(shards)
assert (
min(shards) >= 0 and max(shards) < NSD_NUM_SHARDS
), f"Invalid shards {shards}; expected in [0, {NSD_NUM_SHARDS})"
urls = [f"{root}/tars/nsd-flat_{shard:06d}.tar" for shard in shards]
return urls
def load_nsd_flat_mask(folder="/weka/proj-medarc/shared/NSD-Flat/") -> torch.Tensor:
mask = np.load(os.path.join(folder, "nsd-flat_mask.npy"))
mask = torch.as_tensor(mask)
return mask
def load_nsd_flat_mask_visual(folder="/weka/proj-medarc/shared/NSD-Flat/") -> torch.Tensor:
mask = np.load(os.path.join(folder, "nsd-flat_mask_visual.npy"))
mask = torch.as_tensor(mask)
return mask
##### HCP #####
def create_hcp_flat(
root: Optional[str] = None,
shards: Optional[Union[int, Iterable[int]]] = None,
sub_list: Optional[Union[Literal["train", "test"], List[str]]] = None,
clip_mode: Literal["seq", "event"] = "seq",
target: Optional[Literal["task", "trial_type"]] = None,
frames: int = 16,
stride: Optional[int] = None,
gsr: bool = True,
shuffle: bool = False,
buffer_size: int = 1024,
) -> wds.WebDataset:
"""
Create HCP-Flat dataset. Yields dict samples with keys "image", "meta", and
optionally "target". The images have shape (C, T, H, W).
References:
https://github.com/webdataset/webdataset/issues/250#issuecomment-1454094496
https://github.com/tmbdev-archive/webdataset-imagenet-2/blob/main/imagenet.py
https://github.com/huggingface/pytorch-image-models/blob/main/timm/data/readers/reader_wds.py
"""
assert (
target != "trial_type" or clip_mode == "event"
), "event clipping required for trial_type targets"
root = root or os.environ.get("HCP_FLAT_ROOT") or HCP_FLAT_ROOT
urls = get_hcp_flat_urls(root, shards)
if sub_list in {"train", "test"}:
sub_list = get_hcp_flat_sub_list(root, split=sub_list)
if shuffle:
# Nb, after undoing gsr the data are float32 rather than uint8, to avoid more
# precision loss
dtype_size_bytes = 4 if not gsr else 1
buffer_size_bytes = buffer_size * frames * HCP_MASK_SIZE * dtype_size_bytes
print(f"Shuffle buffer size (MB): {buffer_size_bytes / 1024 / 1024:.0f}")
if clip_mode == "seq":
clipping = seq_clips_hcp(frames, stride=stride, is_training=shuffle)
elif clip_mode == "event":
# all_events_path = "/weka/proj-medarc/shared/HCP-Flat/all_events.json.gz"
# with gzip.open(all_events_path) as f:
# all_events = json.load(f)
clipping = event_clips_hcp(frames)
# In training, we resample shards with replacement independently in every worker and
# yield batches up to the target number of samples. In test, we iterate over the
# shards in order, with workers getting interleaving shards, and yield batches up to
# the target samples. In a distributed setting with variable size shards, setting a
# fixed number of samples is the easiest way to get balanced batches per worker. In
# training we will still see all data eventually. But in test, it means we cut off
# some data.
# Note that in training this does not do deterministic shuffling, which we would
# need for exact reproducibility. They get determistic shuffling in timm, but it's
# more complicated.
# Nb, in initial pretraining runs we shuffled before generating clips, which
# resulted in less random batches. Tbd whether this makes a difference.
dataset = (
wds.WebDataset(
urls,
resampled=shuffle,
shardshuffle=1000 if shuffle else False,
nodesplitter=wds.split_by_node,
select_files=select_files_hcp(sub_list=sub_list, task_only=clip_mode=="event"),
)
.decode()
.map(extract_sample_hcp)
)
if not gsr:
dataset = dataset.map(ungsr)
dataset = dataset.compose(clipping)
# add an integer "target" key to the sample
# this will also filter out samples without a valid target
if target is not None:
class_map_path = Path(root) / f"{target}_class_map.json"
with class_map_path.open() as f:
class_map = json.load(f)
dataset = dataset.compose(with_targets(target, class_map))
if shuffle:
dataset = dataset.shuffle(buffer_size)
# late conversion to tensor, after buffering to save memory
dataset = dataset.map_dict(
image=partial(to_tensor_hcp, mask=load_hcp_flat_mask(root))
)
return dataset
def get_hcp_flat_urls(
root: Optional[str] = None,
shards: Optional[Union[int, Iterable[int]]] = None,
):
root = root or os.environ.get("HCP_FLAT_ROOT") or HCP_FLAT_ROOT
shards = shards or HCP_NUM_SHARDS
if isinstance(shards, int):
shards = range(shards)
assert (
min(shards) >= 0 and max(shards) < HCP_NUM_SHARDS
), f"Invalid shards {shards}; expected in [0, {HCP_NUM_SHARDS})"
urls = [f"{root}/tars/hcp-flat_{shard:06d}.tar" for shard in shards]
return urls
def load_hcp_flat_mask(folder="/weka/proj-medarc/shared/HCP-Flat/") -> torch.Tensor:
mask = np.load(os.path.join(folder, "hcp-flat_mask.npy"))
mask = torch.as_tensor(mask)
return mask
# def event_clips_hcp(
# all_events: Dict[str, List[Dict[str, Any]]],
# frames: int = 16,
# delay: float = DEFAULT_DELAY_SECS,
# ):
# def _filter(src: IterableDataset[Tuple[np.ndarray, Dict[str, Any]]]):
# for img, meta in src:
# tr = HCP_TR[meta["mag"]]
# events = all_events[meta["key"]]
# if not events or meta["task"] not in INCLUDE_TASKS:
# continue
# for event in events:
# cond = event["trial_type"]
# if cond not in INCLUDE_CONDS:
# continue
# onset = event["onset"]
# duration = event["duration"]
# onset_idx = int((onset + delay) / tr)
# # sometimes the end of the trial is cut off
# offset_idx = min(int((onset + delay + duration) / tr), len(img))
# count = (offset_idx - onset_idx) // frames
# for ii in range(count):
# start = onset_idx + ii * frames
# clip = img[start : start + frames].copy()
# meta = {**meta, "start": start, "trial_type": cond}
# yield clip, meta
def get_hcp_flat_urls(
root: Optional[str] = None,
shards: Optional[Union[int, Iterable[int]]] = None,
):
root = root or os.environ.get("HCP_FLAT_ROOT") or HCP_FLAT_ROOT
shards = shards or HCP_NUM_SHARDS
if isinstance(shards, int):
shards = range(shards)
assert (
min(shards) >= 0 and max(shards) < HCP_NUM_SHARDS
), f"Invalid shards {shards}; expected in [0, {HCP_NUM_SHARDS})"
urls = [f"{root}/tars/hcp-flat_{shard:06d}.tar" for shard in shards]
return urls
def get_hcp_flat_sub_list(
root: Optional[str] = None,
split: Literal["train", "test"] = "train",
):
root = root or os.environ.get("HCP_FLAT_ROOT") or HCP_FLAT_ROOT
sub_list = f"{root}/subjects_{split}.txt"
return sub_list
def select_files_hcp(
sub_list: Optional[Union[List[str], str]] = None,
task_only: bool = False,
exclude_exts: Optional[Tuple[str, ...]] = None,
):
if isinstance(sub_list, str):
sub_list = np.loadtxt(sub_list, dtype=str).tolist()
if sub_list is not None:
sub_list = set(sub_list)
if exclude_exts is not None:
exclude_exts = set(exclude_exts)
def _filter(fname: str):
key, ext = fname.split(".", maxsplit=1)
if exclude_exts and ext in exclude_exts:
return False
ents = dict(kv.split("-") for kv in key.split("_"))
if task_only and not (ents["mod"] == "tfMRI" and ents["mag"] == "3T"):
return False
if sub_list and ents["sub"] not in sub_list:
return False
return True
return _filter
def extract_sample_hcp(sample: Dict[str, Any]):
key = sample["__key__"]
bold = sample["bold.npy"]
meta = sample["meta.json"]
meta = {"key": key, **meta}
events = sample["events.json"]
misc = sample.get("misc.npz")
return {"bold": bold, "meta": meta, "events": events, "misc": misc}
def ungsr(sample: Dict[str, Any]):
bold = sample["bold"]
misc = sample["misc"]
mean = misc["mean"]
std = misc["std"]
offset = misc["offset"]
global_signal = misc["global_signal"]
beta = misc["beta"]
# uint8 to float32 with normal range
bold = bold.astype("float32") / 255.0
bold = (bold - 0.5) / 0.2
# recover timeseries
bold = std * bold + mean
bold = bold + global_signal[:, None] * beta + offset
# re-zscore
bold, _, _ = zscore(bold)
return {**sample, "bold": bold}
def to_tensor_hcp(img: np.ndarray, mask: torch.Tensor):
img = torch.from_numpy(img)
if img.dtype == torch.uint8:
img = img / 255.0
img = (img - 0.5) / 0.2
img = unmask(img, mask)
img = img.unsqueeze(0) # (C, T, H, W)
return img
def seq_clips_hcp(frames: int = 16, stride: Optional[int] = None, is_training: bool = True):
stride = stride or frames
def _filter(src: IterableDataset[Dict[str, Any]]):
for sample in src:
bold = sample["bold"]
meta = sample["meta"]
first_idx = random.randint(0, frames) if is_training else 0
count = len(bold) // frames
for ii in range(count):
start = first_idx + ii * frames
stop = start + frames
if stop > len(bold):
break
# copy to avoid a memory leak due to storing the entire underlying array
# https://github.com/webdataset/webdataset/issues/354
clip = bold[start:stop].copy()
meta = {**meta, "start": start}
yield {"image": clip, "meta": meta}
return _filter
def event_clips_hcp(
frames: int = 16,
delay_secs: float = DEFAULT_DELAY_SECS,
):
def _filter(src: IterableDataset[Dict[str, Any]]):
for sample in src:
bold = sample["bold"]
meta = sample["meta"]
events = sample["events"]
tr = HCP_TR[meta["mag"]]
for event in events:
cond = event["trial_type"]
onset = event["onset"]
duration = event["duration"]
if cond not in INCLUDE_CONDS:
continue
first_idx = int((onset + delay_secs) / tr)
# we extract at least one clip per block, and then as many as fit
count = max(int(duration / tr / frames), 1)
for ii in range(count):
start = first_idx + ii * frames
stop = start + frames
# sometimes the trial extends past the end of the run
if stop > len(bold):
break
clip = bold[start:stop].copy()
meta = {**meta, "start": start, "trial_type": cond}
yield {"image": clip, "meta": meta}
return _filter
def with_targets(key: str, class_id_map: Dict[str, int]):
def _filter(src: IterableDataset[Dict[str, Any]]):
for sample in src:
label = sample["meta"][key]
if label in class_id_map:
target = class_id_map[label]
yield {**sample, "target": target}
return _filter
def load_hcp_flat_mask(root: Path) -> torch.Tensor:
mask = np.load(Path(root) / "hcp-flat_mask.npy")
mask = torch.as_tensor(mask)
return mask
# ALL #
import re
def select_files(fname: str, *,
task_only: bool = False,
sub=None, run=None):
# Define the file suffixes to keep
suffix = ".".join(fname.split(".")[1:])
keep = suffix in {"bold.npy", "meta.json", "events.json", "misc.npz"}
if run is not None:
# Excluding run-14 because it's resting-state; note that run-01 is SOMETIMES resting-state
# https://cvnlab.slite.page/p/vjWTghPTb3/Time-series-data
match = re.search(r"run-(0[1-9]|1[0-3])", fname)
keep = keep and bool(match)
# Additional filtering based on task_only and sub
if task_only:
keep = keep and fnmatch(fname, "*mod-tfMRI*mag-3T*")
elif sub is not None:
keep = keep and fnmatch(fname, f"*{sub}*")
return keep
def extract_sample(sample: Dict[str, Any], gsr=True):
key = sample["__key__"]
img = sample["bold.npy"]
meta = sample["meta.json"]
meta = {"key": key, **meta}
events = sample["events.json"]
misc = sample["misc.npz"]
if not gsr:
mean = misc["mean"]
std = misc["std"]
beta = misc["beta"]
global_signal = misc["global_signal"]
offset = misc["offset"]
img = img / 255.0
img = (img - 0.5) / 0.2
img = mean + std * img
img = img + global_signal[:, None] * beta + offset
session_mean = img.mean(axis=0)
session_std = img.std(axis=0)
img = (img - session_mean[None]) / session_std[None]
return img, meta, events, misc
def to_tensor(img, mask, mask2=None):
img = torch.from_numpy(img) / 255.0
img = (img - 0.5) / 0.2
try:
img = unmask(img, mask)
except:
img = unmask(img, mask2)
img = img.unsqueeze(0) # (C, T, H, W)
return img
def to_tensor_gsrFalse(img, mask, mask2=None):
img = torch.from_numpy(img)
try:
img = unmask(img, mask)
except:
img = unmask(img, mask2)
img = img.unsqueeze(0).float() # (C, T, H, W)
return img
def unmask(img: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
unmasked = torch.zeros(
(img.shape[0], *mask.shape), dtype=img.dtype, device=img.device
)
unmasked[:, mask] = img
return unmasked
def batch_unmask(img: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
# img: shape [B, C, D] -> [32, 16, 30191]
# mask: shape [M, N] -> [144, 320]
B, C, D = img.shape # Batch size, channels, last dimension size
M, N = mask.shape # Mask dimensions
# Ensure the mask is flattened to apply along the last dimension (D) of img
flat_mask = mask.view(-1) # shape: [M * N]
num_unmasked_elements = flat_mask.sum() # The number of true elements in the mask
# Initialize an empty tensor for the unmasked output
unmasked = torch.zeros((B, C, M * N), dtype=img.dtype, device=img.device)
# Use broadcasting and advanced indexing to unmask
idx = flat_mask.nonzero(as_tuple=False).squeeze(-1) # Indices where mask is True
unmasked[:, :, idx] = img[:, :, :num_unmasked_elements]
# Reshape the unmasked tensor to the original shape
unmasked = unmasked.view(B, C, M, N)
return unmasked
def seq_clips(frames: int = 16, mindeye_only=False, mindeye_TR_delay=3, only_shared1000=False):
def _filter(src: IterableDataset[Tuple[np.ndarray, Dict[str, Any]]]):
for ii, (img, meta, events, meanstd) in enumerate(src):
if mindeye_only:
if meta['sub']!=1: # forcing samples to come from only subj01
continue
group = [(s['index'], s['nsd_id']) for s in events]
mindeye_info = np.array(group)
if len(mindeye_info)==0:
continue
image_onsets, image_nsd_id = mindeye_info[:,0], mindeye_info[:,1]
for istart, start in enumerate(image_onsets + mindeye_TR_delay):
nsd_id = image_nsd_id[istart].item() - 1 # because nsd_id is 1-indexed
if only_shared1000:
if nsd_id in shared1000:
clip = img[start : start + frames].copy()
meta = {**meta, "start": start}
yield clip, meta, nsd_id, meanstd['mean'], meanstd['std']
else:
if not (nsd_id in shared1000):
clip = img[start : start + frames].copy()
meta = {**meta, "start": start}
yield clip, meta, nsd_id, meanstd['mean'], meanstd['std']
else:
offsets = np.arange(frames)
for offset in offsets:
for start in range(offset, len(img) - frames, frames):
# copy to avoid a memory leak due to storing the entire underlying array
# https://github.com/webdataset/webdataset/issues/354
clip = img[start : start + frames].copy()
meta = {**meta, "start": start}
yield clip, meta, events, meanstd['mean'], meanstd['std']
return _filter