Skip to content

Commit

Permalink
Added support for performing operations on a collection of patients, …
Browse files Browse the repository at this point in the history
…and implemented (basic) cropping mechanism

- Added 'PatientCollection' class to provide an interface to work with a set of patients.
- Implemented a (naive) cropping technique (#17 )
- Visualized/analysed some global (across patients) heuristics (including #11 ).
- Minor updates to make the utilities compatible with test data directories (for example - test data does not have landmarks)
  • Loading branch information
MrinalJain17 committed Oct 14, 2020
1 parent 0193f03 commit 7dca68e
Show file tree
Hide file tree
Showing 5 changed files with 751 additions and 313 deletions.
319 changes: 319 additions & 0 deletions notebooks/miccai_batch_exploration.ipynb

Large diffs are not rendered by default.

305 changes: 0 additions & 305 deletions notebooks/miccai_patient_exploration.ipynb

This file was deleted.

328 changes: 328 additions & 0 deletions notebooks/miccai_per_patient_exploration.ipynb

Large diffs are not rendered by default.

106 changes: 99 additions & 7 deletions utils/miccai.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import functools
from pathlib import Path
from typing import Union
from typing import Callable, Tuple, Union

import nrrd
import numpy as np
import pandas as pd
import torch
from torchvision.utils import make_grid
from tqdm import tqdm

from .utils import AttrDict

Expand Down Expand Up @@ -59,7 +60,7 @@ def data(self) -> torch.Tensor:
return self._data

@data.setter
def data(self, arr) -> None:
def data(self, arr: Union[np.ndarray, torch.Tensor]) -> None:
arr = self._check_data(arr)
self._data = arr
self._is_data_modified = True
Expand Down Expand Up @@ -89,6 +90,18 @@ def _check_data(self, data: Union[np.ndarray, torch.Tensor]) -> torch.Tensor:

return data

def _crop_data(
self, min_z: int, max_z: int, min_x: int, max_x: int, min_y: int, max_y: int
) -> None:
"""
Only intended to be used internally. This function performs no checks,
and updates the data according to the given crop information (and holds
no reference to the 'old' data).
All coordinates are expected to be integers.
"""
self.data = self.data[:, min_z:max_z, min_x:max_x, min_y:max_y]

def as_numpy(self, reverse_dims: bool = False) -> np.ndarray:
arr = self.data.numpy()
if reverse_dims:
Expand Down Expand Up @@ -117,9 +130,13 @@ def __init__(self, patient_dir: str):

self._image = Volume(self.meta_data["image"])
self._structures = self._load_structures()
self._landmarks = pd.read_csv(
self.meta_data["landmarks"], comment="#", names=LANDMARK_COLS
) # TODO: Figure out how landmarks help
if self.meta_data["landmarks"] is not None:
self._landmarks = pd.read_csv(
self.meta_data["landmarks"], comment="#", names=LANDMARK_COLS
)
else: # No landmarks for test data
self._landmarks = None
self._is_cropped = False

def __repr__(self):
return f"Patient(patient_dir={self.patient_dir})"
Expand All @@ -137,7 +154,7 @@ def num_slides(self) -> int:
return self.image.data.shape[1]

@property
def landmarks(self) -> pd.DataFrame:
def landmarks(self) -> Union[pd.DataFrame, None]:
return self._landmarks

@property
Expand All @@ -153,7 +170,10 @@ def _store_meta_data(self) -> dict:
directory = Path(self.patient_dir)

meta_data["image"] = (directory / "img.nrrd").as_posix()
meta_data["landmarks"] = (list(directory.glob("*.fcsv"))[0]).as_posix()
try:
meta_data["landmarks"] = (list(directory.glob("*.fcsv"))[0]).as_posix()
except IndexError: # No landmarks for test data
meta_data["landmarks"] = None

for structure_path in (directory / "structures").iterdir():
meta_data["structures"][structure_path.stem] = structure_path.as_posix()
Expand All @@ -170,6 +190,42 @@ def _load_structures(self) -> AttrDict:

return temp

def crop_data(
self,
boundary_x: Tuple[int, int] = (120, 400),
boundary_y: Tuple[int, int] = (55, 335),
boundary_z: Tuple[float, float] = (0.32, 0.99),
):
assert np.all(
[isinstance(i, tuple) for i in (boundary_x, boundary_y, boundary_z)]
), "Cropping boundary is expected to be a tuple for each axis"

min_x, max_x = boundary_x
min_y, max_y = boundary_y
min_z, max_z = boundary_z

min_z = np.math.ceil(min_z * self.num_slides)
max_z = np.math.ceil(max_z * self.num_slides)

assert np.all(
[isinstance(i, int) for i in (min_z, max_z, min_x, max_x, min_y, max_y)]
), (
"'x' and 'y' coordinates are expected to be integers, and 'z' "
"should be float between 0 and 1"
)
assert min_x < max_x, "Invalid x-axis boundaries"
assert min_y < max_y, "Invalid y-axis boundaries"
assert min_z < max_z, "Invalid z-axis boundaries"

self.image._crop_data(min_z, max_z, min_x, max_x, min_y, max_y)
for structure in STRUCTURES:
if self.structures[structure] is not None:
self.structures[structure]._crop_data(
min_z, max_z, min_x, max_x, min_y, max_y
)

self._is_cropped = True

def combine_segmentation_masks(self, structure_list: list) -> np.ndarray:
"""
This is used as a workaround for overlaying multiple segmentation masks
Expand All @@ -191,6 +247,42 @@ def combine_segmentation_masks(self, structure_list: list) -> np.ndarray:
return combined


class PatientCollection(object):
def __init__(self, path: str):
self._path = path
self._patient_paths = {
directory.name: directory.as_posix()
for directory in Path(path).glob("0522c*")
}
assert (
len(self._patient_paths) > 0
), "No patients found at the specified location: {path}"

@property
def patient_paths(self) -> dict:
return self._patient_paths

def apply_function(
self, func: Callable, disable_progress: bool = False, **kwargs
) -> dict:
"""
Applies the callable to each patient, and stores the result in a dictionary.
Any extra keyword arguments will be passed to the callable.
The callable should be of the following form:
def func(patient: Patient, **kwargs):
...
"""
iterator = tqdm(self.patient_paths.items(), disable=disable_progress)

collected_results = {
name: func(Patient(path), **kwargs) for (name, path) in iterator
}

return collected_results


def load_nrrd_as_tensor(path: str) -> torch.Tensor:
"""
Headers are returned without any changes. Should be kept in mind if used with the
Expand Down
6 changes: 5 additions & 1 deletion utils/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ def plot_slide(patient: Patient, index: int = 0, region=None):
region = [region]

if len(region) == 1:
region_array = patient.structures[region[0]].as_numpy()
region_array = patient.structures[region[0]]
if region_array is not None:
region_array = region_array.as_numpy()
else:
region_array = np.expand_dims(np.zeros_like(volume), 0)
else:
# More than one region requested --> combine
region_array = patient.combine_segmentation_masks(structure_list=region)
Expand Down

0 comments on commit 7dca68e

Please sign in to comment.