Spaces:
Sleeping
Sleeping
| import cv2 | |
| import torch | |
| import numpy as np | |
| import pandas as pd | |
| from collections import Counter, defaultdict | |
| from torch.utils.data import Dataset | |
| from params import LEVELS, LEVELS_ | |
| def get_frames(frame, n_frames, frames_c, stride=1, max_frame=100): | |
| """ | |
| Calculate a sequence of frame indices based on the specified parameters. | |
| If stride is -1, sample n_frames from 0 to max_frame using linear spacing. | |
| Args: | |
| frame (int): The central fr | |
| ame index around which the sequence is generated. | |
| n_frames (int): The number of frames in the sequence. | |
| frames_c (int): The number of frames to be repeated and offset around each frame. | |
| stride (int, optional): The step size between frames. Defaults to 1. | |
| max_frame (int, optional): The maximum frame index allowed. Defaults to 100. | |
| Returns: | |
| numpy.ndarray: An array of frame indices representing the calculated sequence. | |
| """ | |
| frame = int(frame) | |
| if stride == -1: | |
| if max_frame <= frames_c * n_frames * 3 + 3: | |
| frames = np.linspace( | |
| frames_c, max_frame - frames_c, n_frames, endpoint=True, dtype=int | |
| ) | |
| else: | |
| frames = np.linspace( | |
| frames_c, max_frame - frames_c, n_frames + 2, endpoint=True, dtype=int | |
| ) | |
| frames = frames[1:-1] | |
| else: | |
| frames = np.arange(n_frames) * stride | |
| frames = frames - frames[n_frames // 2] + frame | |
| if frames_c: | |
| offset = np.tile(np.arange(-1, 2) * frames_c, len(frames)) | |
| frames = np.repeat(frames, 3) + offset | |
| if frames.min() < 0: | |
| frames -= frames.min() | |
| elif frames.max() > max_frame: | |
| frames += max_frame - frames.max() | |
| frames = np.clip(frames, 0, max_frame) | |
| # print(frames) | |
| return frames | |
| class ImageDataset(Dataset): | |
| """ | |
| Base dataset for loading images and their corresponding targets for classification tasks. | |
| """ | |
| def __init__( | |
| self, | |
| df, | |
| targets="target", | |
| transforms=None, | |
| frames_chanel=0, | |
| n_frames=1, | |
| stride=1, | |
| train=False, | |
| **kwargs, | |
| ): | |
| """ | |
| Constructor for the ImageDataset class. | |
| Args: | |
| df (pandas DataFrame): Metadata containing image paths and targets. | |
| targets (str, optional): Column name for the target values. Defaults to "target". | |
| transforms (callable, optional): Transforms to apply to the images. Defaults to None. | |
| frames_chanel (int, optional): Number of frames for channel stacking. Defaults to 0. | |
| n_frames (int, optional): The number of frames to use. Defaults to 1. | |
| stride (int, optional): The step size between frames. Defaults to 1. | |
| train (bool, optional): Whether the dataset is for training. Defaults to False. | |
| """ | |
| self.df = df | |
| self.targets = df[targets].values | |
| self.img_paths = df["img_path"].values | |
| if "target_aux" in df.columns: | |
| self.targets_aux = df["target_aux"].values | |
| else: | |
| self.targets_aux = np.zeros(len(df)) | |
| self.transforms = transforms | |
| self.frames_chanel = frames_chanel | |
| self.n_frames = n_frames | |
| self.stride = stride | |
| self.train = train | |
| self.coords = None | |
| def __len__(self): | |
| """ | |
| Get the length of the dataset. | |
| Returns: | |
| int: Length of the dataset. | |
| """ | |
| return len(self.df) | |
| def __getitem__(self, idx): | |
| """ | |
| Item accessor. Loads an image and its corresponding target. | |
| Args: | |
| idx (int): Index. | |
| Returns: | |
| torch.Tensor: Image as a tensor. | |
| torch.Tensor: Labels as a tensor. | |
| torch.Tensor: Auxiliary labels as a tensor. | |
| """ | |
| try: | |
| img = np.load(self.img_paths[idx]).astype(np.float32) | |
| # Robust normalization using percentiles | |
| pmin, pmax = np.percentile(img, (1, 99)) | |
| if pmax != pmin: | |
| img = np.clip(img, pmin, pmax) | |
| img = (img - pmin) / (pmax - pmin) * 255 | |
| else: | |
| img = (img - img.min()) / (img.max() - img.min() + 1e-6) * 255 | |
| except Exception as e: | |
| print(f"Error loading image {self.img_paths[idx]}: {e}") | |
| img = np.zeros((1, 512, 512), dtype=np.float32) | |
| img = img.astype(np.uint8) | |
| # Pick frame(s) | |
| try: | |
| xs = self.coords[idx][:, 0] | |
| except Exception: # No coords | |
| xs = [len(img) // 2] | |
| if self.train: | |
| frame = np.random.choice(xs) | |
| frame += np.random.choice([-1, 0, 1]) | |
| else: | |
| frame = Counter(xs).most_common()[0][0] | |
| frames = get_frames( | |
| frame, | |
| self.n_frames, | |
| self.frames_chanel, | |
| stride=self.stride, | |
| max_frame=len(img) - 1, | |
| ) | |
| # Load | |
| image = img[np.array(frames)].transpose(1, 2, 0) | |
| image = image.astype(np.float32) / 255.0 | |
| image = (image - image.min()) / (image.max() - image.min()) | |
| # Augment | |
| if self.transforms: | |
| transformed = self.transforms(image=image) | |
| image = transformed["image"] | |
| y_aux = torch.tensor([self.targets_aux[idx]]) | |
| tgt = self.targets[idx] | |
| if isinstance(self.targets[idx], (int, float, np.int64, np.int32)): | |
| y = torch.zeros(3, dtype=torch.float) | |
| if tgt > -1: | |
| y[tgt] = 1 | |
| else: | |
| y = torch.zeros((len(tgt), 3), dtype=torch.float) | |
| for i in range(len(tgt)): | |
| if tgt[i] > -1: | |
| y[i, tgt[i]] = 1 | |
| # Reshape | |
| if self.frames_chanel: | |
| image = image.view(self.n_frames, 3, image.size(1), image.size(2)) | |
| else: | |
| image = image.unsqueeze(1).repeat(1, 3, 1, 1) | |
| if self.n_frames == 1: | |
| image = image.squeeze(0) | |
| return image, y, y_aux | |
| class CropDataset(ImageDataset): | |
| """ | |
| Dataset for training 2.5D crop classification models. | |
| """ | |
| def __init__( | |
| self, | |
| df, | |
| targets="target", | |
| transforms=None, | |
| frames_chanel=0, | |
| n_frames=1, | |
| stride=1, | |
| train=False, | |
| flip=False, | |
| ): | |
| """ | |
| Constructor for the CropDataset class. | |
| Args: | |
| df (pandas DataFrame): Metadata containing image paths and targets. | |
| targets (str, optional): Column name for the target values. Defaults to "target". | |
| transforms (callable, optional): Transforms to apply to the images. Defaults to None. | |
| frames_chanel (int, optional): Number of frames for channel stacking. Defaults to 0. | |
| n_frames (int, optional): The number of frames to use. Defaults to 1. | |
| stride (int, optional): The step size between frames. Defaults to 1. | |
| train (bool, optional): Whether the dataset is for training. Defaults to False. | |
| flip (bool, optional): Whether to apply flipping augmentation. Defaults to False. | |
| """ | |
| super().__init__( | |
| df, | |
| targets=targets, | |
| transforms=transforms, | |
| frames_chanel=frames_chanel, | |
| n_frames=n_frames, | |
| stride=stride, | |
| train=train, | |
| ) | |
| try: | |
| if isinstance(self.targets[0], list): | |
| self.targets = np.vstack(self.targets) | |
| except Exception: # will not work with PL | |
| pass | |
| try: | |
| if "coords_crops" in df["img_path"][0]: | |
| self.sides = df["side"].map({"Right": 2, "Center": 4, "Left": 6}).values | |
| else: # Cropped with seg | |
| self.sides = df["side"].map({"Right": 1, "Center": 4, "Left": 7}).values | |
| except KeyError: | |
| self.sides = np.ones(len(df)) * 4 # All Center for scs | |
| self.flip = flip | |
| def __getitem__(self, idx): | |
| """ | |
| Item accessor. Loads an image and applies any necessary augmentations. | |
| Args: | |
| idx (int): Index. | |
| Returns: | |
| torch.Tensor: Image as a tensor. | |
| torch.Tensor: Labels as a tensor. | |
| int: Dummy value. | |
| """ | |
| # Load | |
| try: | |
| img = np.load(self.img_paths[idx]).astype(np.float32) | |
| pmin, pmax = np.percentile(img, (1, 99)) | |
| if pmax != pmin: | |
| img = np.clip(img, pmin, pmax) | |
| img = (img - pmin) / (pmax - pmin) * 255 | |
| else: | |
| img = (img - img.min()) / (img.max() - img.min() + 1e-6) * 255 | |
| except Exception as e: | |
| print(f"Error loading crop {self.img_paths[idx]}: {e}") | |
| img = np.zeros((1, 64, 64), dtype=np.float32) | |
| img = img.astype(np.uint8) | |
| # Pick frame(s) | |
| frame = self.sides[idx] * len(img) // 8 | |
| if self.train: | |
| if self.n_frames <= 3: | |
| frame += np.random.choice([-1, 0, 1]) | |
| else: | |
| frame += np.random.choice([-2, -1, 0, 1, 2]) | |
| frames = get_frames( | |
| frame, | |
| self.n_frames, | |
| self.frames_chanel, | |
| stride=self.stride, | |
| max_frame=len(img) - 1, | |
| ) | |
| image = img[np.array(frames)].transpose(1, 2, 0) | |
| image = image.astype(np.float32) / 255.0 | |
| # Augment | |
| if self.transforms: | |
| transformed = self.transforms(image=image) | |
| image = transformed["image"] | |
| tgt = np.array(self.targets[idx]) | |
| if isinstance(self.targets[idx], (int, float, np.int64, np.int32)): | |
| y = torch.zeros(3, dtype=torch.float) | |
| if tgt > -1: | |
| y[tgt] = 1 | |
| elif len(tgt.shape) == 2: # PL - no need to one-hot encode | |
| y = torch.from_numpy(tgt.astype(np.float32)) | |
| else: | |
| y = torch.zeros((len(tgt), 3), dtype=torch.float) | |
| for i in range(len(tgt)): | |
| if tgt[i] > -1: | |
| y[i, tgt[i]] = 1 | |
| # Reshape | |
| if self.frames_chanel: | |
| image = image.view(self.n_frames, 3, image.size(1), image.size(2)) | |
| else: | |
| image = image.unsqueeze(1).repeat(1, 3, 1, 1) | |
| if self.n_frames == 1: | |
| image = image[0] | |
| if np.random.random() < 0.5 and self.flip: | |
| if y.size(0) == 5: | |
| y = y[[0, 2, 1, 4, 3]].contiguous() | |
| image = torch.flip(image, [0]) | |
| return image, y, 0 | |
| class CoordsDataset(Dataset): | |
| """ | |
| Dataset for training coords models. | |
| """ | |
| def __init__( | |
| self, | |
| df, | |
| targets="target", | |
| transforms=None, | |
| train=False, | |
| **kwargs, | |
| ): | |
| """ | |
| Constructor for the CoordsDataset class. | |
| Args: | |
| df (pandas DataFrame): Metadata containing the information. | |
| targets (str, optional): Column name for the target values. Defaults to "target". | |
| transforms (callable, optional): Transforms to apply. Defaults to None. | |
| train (bool, optional): Whether the dataset is for training. Defaults to False. | |
| """ | |
| self.df = df | |
| self.targets = np.array(df[targets].values.tolist()) | |
| self.img_paths = df["img_path"].values | |
| if "target_rel" in df.columns: | |
| self.targets_rel = np.array(df["target_rel"].values.tolist()) | |
| else: | |
| self.targets_rel = np.zeros(len(df)) | |
| self.transforms = transforms | |
| def __len__(self): | |
| """ | |
| Get the length of the dataset. | |
| Returns: | |
| int: Length of the dataset. | |
| """ | |
| return len(self.df) | |
| def __getitem__(self, idx): | |
| """ | |
| Item accessor. Retrieves an image and its corresponding target coordinates. | |
| Args: | |
| idx (int): Index. | |
| Returns: | |
| torch.Tensor: Image as a tensor. | |
| torch.Tensor: Target coordinates as a tensor. | |
| int: Dummy value. | |
| """ | |
| if self.img_paths[idx].endswith('.npy'): | |
| try: | |
| imgs = np.load(self.img_paths[idx]) | |
| image = imgs[len(imgs) // 2].astype(np.float32) | |
| pmin, pmax = np.percentile(image, (1, 99)) | |
| if pmax != pmin: | |
| image = np.clip(image, pmin, pmax) | |
| image = (image - pmin) / (pmax - pmin) | |
| else: | |
| image = (image - image.min()) / (image.max() - image.min() + 1e-6) | |
| except Exception as e: | |
| print(f"Error loading npy for coords {self.img_paths[idx]}: {e}") | |
| image = np.zeros((512, 512), dtype=np.float32) | |
| # Stack to 3 channels to mimic cv2.imread RGB output [H, W, C] | |
| image = np.stack([image] * 3, axis=-1) | |
| else: | |
| image = cv2.imread(self.img_paths[idx]).astype(np.float32) / 255.0 | |
| # Augment | |
| if self.transforms: | |
| y = self.targets[idx].copy() | |
| transformed = self.transforms(image=image, keypoints=y[y.sum(-1) > 0].copy()) | |
| image = transformed["image"] | |
| y = torch.tensor(y).float() | |
| if self.transforms: | |
| y[y.sum(-1) > 0] = torch.tensor(transformed["keypoints"]).float() | |
| # Ensure image is [3, H, W] | |
| if isinstance(image, torch.Tensor): | |
| if image.dim() == 3 and image.shape[0] == 1: | |
| image = image.repeat(3, 1, 1) | |
| elif isinstance(image, np.ndarray): | |
| if image.ndim == 2: | |
| image = np.stack([image] * 3, axis=0) | |
| elif image.ndim == 3 and image.shape[-1] == 3: | |
| image = image.transpose(2, 0, 1) | |
| y[:, 0] /= image.shape[2] if isinstance(image, torch.Tensor) else image.shape[2] | |
| y[:, 1] /= image.shape[1] if isinstance(image, torch.Tensor) else image.shape[1] | |
| y = torch.where(y < 0, -1, y) | |
| y = torch.where(y > 1, -1, y) | |
| return image, y, 0 | |
| class FeatureDataset(Dataset): | |
| """ | |
| Dataset for training level 2 models. | |
| """ | |
| def __init__( | |
| self, | |
| df, | |
| exp_folders, | |
| targets="target", | |
| resize=None, | |
| ): | |
| """ | |
| Constructor for the FeatureDataset class. | |
| Args: | |
| df (pandas DataFrame): Metadata containing image paths and targets. | |
| exp_folders (dict): Dictionary mapping experiment names to folder paths. | |
| targets (str, optional): Column name for the target values. Defaults to "target". | |
| resize (tuple, optional): Dimensions to resize the images to. Defaults to None. | |
| """ | |
| self.df = df | |
| self.targets = df[targets].values | |
| self.resize = resize | |
| self.exp_folders = exp_folders | |
| self.series_dict = self.get_series_dict(df) | |
| self.dummies = { | |
| "scs_crop": np.zeros(3), | |
| "nfn_crop": np.zeros(3), | |
| "ss_crop_": np.zeros((2, 3)), | |
| "crop": np.zeros((5, 3)), | |
| "crop_bi": np.zeros((5, 3)), | |
| "crop_2": np.zeros((5, 3)), | |
| "crop_3": np.zeros((5, 3)), | |
| "crop_4": np.zeros((5, 3)), | |
| "dh": np.zeros((25, 3)), | |
| "ch": np.zeros((25, 3)), | |
| "spinenet": np.zeros((12)), | |
| } | |
| self.fts = {} | |
| for k in self.exp_folders: | |
| if "crop" in k: | |
| self.fts[k] = self.load_fts(self.exp_folders[k]) | |
| elif ("dh" in k) or ("ch" in k): | |
| file = torch.load(self.exp_folders[k]) | |
| self.fts[k] = dict(zip( | |
| file["study_id"].tolist(), | |
| file['logits'].float().cpu().numpy(), | |
| )) | |
| elif "spinenet" in k: | |
| df = pd.read_csv(self.exp_folders[k]).set_index("series_id") | |
| for level in LEVELS_: | |
| df[level] = df[level].fillna('()').apply(eval) | |
| self.fts[k] = df | |
| def get_series_dict(df): | |
| """ | |
| Constructs a dictionary mapping study IDs to their series descriptions and IDs. | |
| Args: | |
| df (pandas DataFrame): DataFrame containing series information. | |
| Returns: | |
| dict: Dictionary mapping study IDs to series information. | |
| """ | |
| series_dict = defaultdict(dict) | |
| df = df[['series_id', 'series_description', "study_id"]] | |
| for study, df_study in df.explode(['series_id', 'series_description']).groupby("study_id"): | |
| series = df_study[ | |
| ["series_id", "series_description"] | |
| ].groupby("series_description").agg(list) | |
| series = series['series_id'].to_dict() | |
| series_dict[study]["scs"] = series.get("Sagittal T2/STIR", []) | |
| series_dict[study]["nfn"] = series.get("Sagittal T1", []) | |
| series_dict[study]["ss"] = series.get("Axial T2", []) | |
| return series_dict | |
| def __len__(self): | |
| """ | |
| Get the length of the dataset. | |
| Returns: | |
| int: Length of the dataset. | |
| """ | |
| return len(self.df) | |
| def load_fts(exp_folder): | |
| """ | |
| Loads feature data from the specified experiment folder. | |
| Args: | |
| exp_folder (str): Path to the experiment folder. | |
| Returns: | |
| dict: Dictionary mapping index keys to feature data. | |
| """ | |
| fts = {} | |
| for fold in range(4): | |
| preds = np.load(exp_folder + f'pred_inf_{fold}.npy') | |
| df = pd.read_csv(exp_folder + f'df_val_{fold}.csv') | |
| if "side" not in df.columns: | |
| df['side'] = 'Center' | |
| df = df[["study_id", "series_id", "level", "side"]].astype(str) | |
| index = ["_".join(row.tolist()) for row in df.values] | |
| fts.update(dict(zip(index, preds))) | |
| return fts | |
| def __getitem__(self, idx): | |
| """ | |
| Retrieves the features and target for a specific index. | |
| Args: | |
| idx (int): Index of the sample. | |
| Returns: | |
| tuple: A tuple containing features, target, and a dummy value. | |
| """ | |
| study = self.df["study_id"][idx] | |
| series = self.series_dict[study] | |
| fts = {} | |
| for exp in self.exp_folders: | |
| series_k = exp.split("_")[0] | |
| series_k = [series_k] if series_k in ["ss", "nfn", "scs"] else ["nfn", "scs"] # "ss" | |
| if "crop" in exp: | |
| sides = ["Left", "Right"] if "nfn" in exp or "ss" in exp else ['Center'] | |
| ft = defaultdict(list) | |
| for lvl in LEVELS: | |
| for side in sides: | |
| for sk in series_k: | |
| ft_ = [] | |
| for s in series[sk]: | |
| ft_k = f'{study}_{s}_{lvl}_{side}' | |
| try: | |
| ft_.append(self.fts[exp][ft_k]) | |
| except KeyError: | |
| # print(exp, sk, ft_k, "missing") | |
| pass | |
| ft_ = np.mean(ft_, 0) if len(ft_) else self.dummies[exp[:8]] | |
| ft[f"{lvl}_{side}"].append(ft_) | |
| ft_ = [] | |
| for k in ft.keys(): | |
| try: | |
| ft_.append(np.concatenate(ft[k], -1)) | |
| except ValueError: | |
| print([x.shape for x in ft[k]]) | |
| ft = np.vstack(ft_) | |
| # Put in the right order | |
| ft = ft.reshape(5, -1, ft.shape[-1]).transpose(1, 0, 2).reshape(-1, ft.shape[-1]) | |
| elif "dh" in exp or "ch" in exp: | |
| ft = self.fts[exp].get(study, self.dummies[exp[:2]]) | |
| else: | |
| raise NotImplementedError | |
| fts[exp] = torch.from_numpy(ft).float().contiguous() | |
| y = torch.from_numpy(self.targets[idx]) | |
| return fts, y, 0 | |