torch_em.data.datasets.medical.adrenal_acc

The Adrenal-ACC-Ki67-Seg dataset contains annotations for adrenocortical carcinoma segmentation in contrast-enhanced abdominal CT.

It consists of 53 CT volumes (one segmented CT series per patient, the collection also contains 71 additional unsegmented CT series of the same patients) with binary tumor labels (1: adrenal tumor). The CT scans are distributed as DICOM series and the labels as DICOM-SEG objects, which are converted and stored in hdf5 files by this module.

NOTE: This requires the pydicom python package.

The dataset is located at https://www.cancerimagingarchive.net/collection/adrenal-acc-ki67-seg/.

This dataset is from the publication https://doi.org/10.1016/j.crad.2020.01.012. The data was released at https://doi.org/10.7937/1FPG-VM46. Please cite it if you use this dataset in your research.

  1"""The Adrenal-ACC-Ki67-Seg dataset contains annotations for adrenocortical carcinoma segmentation
  2in contrast-enhanced abdominal CT.
  3
  4It consists of 53 CT volumes (one segmented CT series per patient, the collection also contains 71 additional
  5unsegmented CT series of the same patients) with binary tumor labels (1: adrenal tumor). The CT scans are
  6distributed as DICOM series and the labels as DICOM-SEG objects, which are converted and stored in hdf5 files
  7by this module.
  8
  9NOTE: This requires the pydicom python package.
 10
 11The dataset is located at https://www.cancerimagingarchive.net/collection/adrenal-acc-ki67-seg/.
 12
 13This dataset is from the publication https://doi.org/10.1016/j.crad.2020.01.012.
 14The data was released at https://doi.org/10.7937/1FPG-VM46.
 15Please cite it if you use this dataset in your research.
 16"""
 17
 18import os
 19import csv
 20from glob import glob
 21from tqdm import tqdm
 22from natsort import natsorted
 23from typing import Union, Tuple, List
 24
 25import numpy as np
 26
 27from torch.utils.data import Dataset, DataLoader
 28
 29import torch_em
 30
 31from .. import util
 32
 33
 34URL = "https://www.cancerimagingarchive.net/wp-content/uploads/Adrenal-ACC-Ki67-Seg_v1.tcia"
 35
 36# The DICOM series are downloaded individually from TCIA.
 37CHECKSUM = None
 38
 39LABEL_IDS = {"tumor": 1}
 40
 41
 42def _load_dicom_volume(series_dir):
 43    """Stack a DICOM series into a volume with axes (z, y, x) and slices sorted along the slice normal.
 44
 45    Returns the volume in Hounsfield units and the affine matrix that maps voxel indices (z, y, x)
 46    to DICOM patient coordinates.
 47    """
 48    import pydicom
 49
 50    slices = [pydicom.dcmread(dcm_path) for dcm_path in natsorted(glob(os.path.join(series_dir, "*.dcm")))]
 51    orientation = np.array([float(v) for v in slices[0].ImageOrientationPatient])
 52    row_dir, col_dir = orientation[:3], orientation[3:]
 53    normal = np.cross(row_dir, col_dir)
 54    slices.sort(key=lambda dcm: np.dot([float(v) for v in dcm.ImagePositionPatient], normal))
 55
 56    volume = np.stack([dcm.pixel_array for dcm in slices]).astype("float32")
 57    volume = volume * float(slices[0].RescaleSlope) + float(slices[0].RescaleIntercept)
 58    volume = np.round(volume).astype("int16")
 59
 60    positions = np.array([[float(v) for v in dcm.ImagePositionPatient] for dcm in slices])
 61    spacing = [float(v) for v in slices[0].PixelSpacing]  # The spacing between rows and between columns.
 62    affine = np.eye(4)
 63    affine[:3, 0] = (positions[-1] - positions[0]) / (len(slices) - 1)
 64    affine[:3, 1] = col_dir * spacing[0]
 65    affine[:3, 2] = row_dir * spacing[1]
 66    affine[:3, 3] = positions[0]
 67    return volume, affine
 68
 69
 70def _load_dicom_seg(seg_path):
 71    """Load a DICOM-SEG object as a label volume with axes (z, y, x), where the segment number is used as label id.
 72
 73    Returns the label volume and the affine matrix that maps its voxel indices to DICOM patient coordinates.
 74    """
 75    import pydicom
 76
 77    seg = pydicom.dcmread(seg_path)
 78    frames = seg.pixel_array
 79    if frames.ndim == 2:  # A segmentation with a single frame.
 80        frames = frames[None]
 81
 82    shared_group = seg.SharedFunctionalGroupsSequence[0]
 83    orientation = np.array([float(v) for v in shared_group.PlaneOrientationSequence[0].ImageOrientationPatient])
 84    row_dir, col_dir = orientation[:3], orientation[3:]
 85    normal = np.cross(row_dir, col_dir)
 86    pixel_measures = shared_group.PixelMeasuresSequence[0]
 87    spacing = [float(v) for v in pixel_measures.PixelSpacing]  # The spacing between rows and between columns.
 88
 89    # The frames may be stored in arbitrary order and frames without any foreground may be skipped,
 90    # so the position of each frame along the slice normal is derived from its patient position.
 91    frame_groups = seg.PerFrameFunctionalGroupsSequence
 92    positions = np.array([[float(v) for v in g.PlanePositionSequence[0].ImagePositionPatient] for g in frame_groups])
 93    projections = positions @ normal
 94    if "SpacingBetweenSlices" in pixel_measures:
 95        slice_spacing = float(pixel_measures.SpacingBetweenSlices)
 96    elif len(projections) > 1:
 97        slice_spacing = np.min(np.diff(np.unique(np.round(projections, 3))))
 98    else:
 99        slice_spacing = float(pixel_measures.SliceThickness)
100    slice_ids = np.round((projections - projections.min()) / slice_spacing).astype("int")
101
102    labels = np.zeros((slice_ids.max() + 1, seg.Rows, seg.Columns), dtype="uint8")
103    for frame, frame_group, slice_id in zip(frames, frame_groups, slice_ids):
104        segment_number = int(frame_group.SegmentIdentificationSequence[0].ReferencedSegmentNumber)
105        labels[slice_id][frame.astype("bool")] = segment_number
106
107    affine = np.eye(4)
108    affine[:3, 0] = normal * slice_spacing
109    affine[:3, 1] = col_dir * spacing[0]
110    affine[:3, 2] = row_dir * spacing[1]
111    affine[:3, 3] = positions[np.argmin(projections)]
112    return labels, affine
113
114
115def _resample_labels(labels, affine, target_shape, target_affine):
116    """Resample a label volume onto the voxel grid of a reference image with nearest neighbor interpolation.
117
118    This is exact if both volumes are stored on the same grid (e.g. a DICOM-SEG object stored on a cropped
119    grid of the reference image) and downsamples segmentations that are stored on a finer grid.
120    """
121    to_label_index = np.linalg.inv(affine) @ target_affine
122    resampled = np.zeros(target_shape, dtype=labels.dtype)
123    yy, xx = np.meshgrid(np.arange(target_shape[1]), np.arange(target_shape[2]), indexing="ij")
124    for z in range(target_shape[0]):
125        target_indices = np.stack([np.full(yy.size, z), yy.ravel(), xx.ravel(), np.ones(yy.size)])
126        indices = np.round(to_label_index[:3] @ target_indices).astype("int")
127        valid = np.all((indices >= 0) & (indices < np.array(labels.shape)[:, None]), axis=0)
128        resampled[z].flat[valid] = labels[tuple(indices[:, valid])]
129    return resampled
130
131
132def _preprocess_adrenal_acc(dicom_dir, csv_path, preprocessed_dir):
133    import h5py
134    import pydicom
135
136    with open(csv_path, "r") as f:
137        seg_series = {row["Subject ID"]: row["Series UID"] for row in csv.DictReader(f) if row["Modality"] == "SEG"}
138
139    os.makedirs(preprocessed_dir, exist_ok=True)
140    for subject_id, seg_uid in tqdm(sorted(seg_series.items()), desc="Preprocess Adrenal-ACC-Ki67-Seg"):
141        out_path = os.path.join(preprocessed_dir, f"{subject_id}.h5")
142        if os.path.exists(out_path):
143            continue
144
145        # The segmentation references the CT series it was created for.
146        seg_path = glob(os.path.join(dicom_dir, seg_uid, "*.dcm"))[0]
147        ct_uid = pydicom.dcmread(seg_path, stop_before_pixels=True).ReferencedSeriesSequence[0].SeriesInstanceUID
148        volume, affine = _load_dicom_volume(os.path.join(dicom_dir, ct_uid))
149        seg_labels, seg_affine = _load_dicom_seg(seg_path)
150        assert seg_labels.max() == 1, f"Expected a single segment in {seg_path}."
151        labels = _resample_labels(seg_labels, seg_affine, volume.shape, affine) * LABEL_IDS["tumor"]
152
153        with h5py.File(out_path, "w") as f:
154            f.create_dataset("raw", data=volume, compression="gzip")
155            f.create_dataset("labels", data=labels, compression="gzip")
156
157
158def get_adrenal_acc_data(path: Union[os.PathLike, str], download: bool = False) -> str:
159    """Download the Adrenal-ACC-Ki67-Seg dataset.
160
161    Args:
162        path: Filepath to a folder where the data is downloaded for further processing.
163        download: Whether to download the data if it is not present.
164
165    Returns:
166        Filepath where the preprocessed data is stored.
167    """
168    preprocessed_dir = os.path.join(path, "preprocessed")
169    if os.path.exists(preprocessed_dir):
170        return preprocessed_dir
171
172    os.makedirs(path, exist_ok=True)
173
174    # Download the DICOM series (CT and SEG) from the TCIA manifest.
175    dicom_dir = os.path.join(path, "dicom")
176    csv_path = os.path.join(path, "adrenal_acc_series")
177    util.download_source_tcia(
178        path=os.path.join(path, "Adrenal-ACC-Ki67-Seg_v1.tcia"), url=URL, dst=dicom_dir,
179        csv_filename=csv_path, download=download,
180    )
181
182    _preprocess_adrenal_acc(dicom_dir, f"{csv_path}.csv", preprocessed_dir)
183    return preprocessed_dir
184
185
186def get_adrenal_acc_paths(path: Union[os.PathLike, str], download: bool = False) -> List[str]:
187    """Get paths to the Adrenal-ACC-Ki67-Seg data.
188
189    Args:
190        path: Filepath to a folder where the data is downloaded for further processing.
191        download: Whether to download the data if it is not present.
192
193    Returns:
194        List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').
195    """
196    data_dir = get_adrenal_acc_data(path, download)
197    volume_paths = natsorted(glob(os.path.join(data_dir, "*.h5")))
198    return volume_paths
199
200
201def get_adrenal_acc_dataset(
202    path: Union[os.PathLike, str],
203    patch_shape: Tuple[int, ...],
204    resize_inputs: bool = False,
205    download: bool = False,
206    **kwargs
207) -> Dataset:
208    """Get the Adrenal-ACC-Ki67-Seg dataset for adrenal tumor segmentation.
209
210    Args:
211        path: Filepath to a folder where the data is downloaded for further processing.
212        patch_shape: The patch shape to use for training.
213        resize_inputs: Whether to resize inputs to the desired patch shape.
214        download: Whether to download the data if it is not present.
215        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
216
217    Returns:
218        The segmentation dataset.
219    """
220    volume_paths = get_adrenal_acc_paths(path, download)
221
222    if resize_inputs:
223        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
224        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
225            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
226        )
227
228    return torch_em.default_segmentation_dataset(
229        raw_paths=volume_paths,
230        raw_key="raw",
231        label_paths=volume_paths,
232        label_key="labels",
233        patch_shape=patch_shape,
234        is_seg_dataset=True,
235        **kwargs
236    )
237
238
239def get_adrenal_acc_loader(
240    path: Union[os.PathLike, str],
241    batch_size: int,
242    patch_shape: Tuple[int, ...],
243    resize_inputs: bool = False,
244    download: bool = False,
245    **kwargs
246) -> DataLoader:
247    """Get the Adrenal-ACC-Ki67-Seg dataloader for adrenal tumor segmentation.
248
249    Args:
250        path: Filepath to a folder where the data is downloaded for further processing.
251        batch_size: The batch size for training.
252        patch_shape: The patch shape to use for training.
253        resize_inputs: Whether to resize inputs to the desired patch shape.
254        download: Whether to download the data if it is not present.
255        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
256
257    Returns:
258        The DataLoader.
259    """
260    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
261    dataset = get_adrenal_acc_dataset(path, patch_shape, resize_inputs, download, **ds_kwargs)
262    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)
URL = 'https://www.cancerimagingarchive.net/wp-content/uploads/Adrenal-ACC-Ki67-Seg_v1.tcia'
CHECKSUM = None
LABEL_IDS = {'tumor': 1}
def get_adrenal_acc_data(path: Union[os.PathLike, str], download: bool = False) -> str:
159def get_adrenal_acc_data(path: Union[os.PathLike, str], download: bool = False) -> str:
160    """Download the Adrenal-ACC-Ki67-Seg dataset.
161
162    Args:
163        path: Filepath to a folder where the data is downloaded for further processing.
164        download: Whether to download the data if it is not present.
165
166    Returns:
167        Filepath where the preprocessed data is stored.
168    """
169    preprocessed_dir = os.path.join(path, "preprocessed")
170    if os.path.exists(preprocessed_dir):
171        return preprocessed_dir
172
173    os.makedirs(path, exist_ok=True)
174
175    # Download the DICOM series (CT and SEG) from the TCIA manifest.
176    dicom_dir = os.path.join(path, "dicom")
177    csv_path = os.path.join(path, "adrenal_acc_series")
178    util.download_source_tcia(
179        path=os.path.join(path, "Adrenal-ACC-Ki67-Seg_v1.tcia"), url=URL, dst=dicom_dir,
180        csv_filename=csv_path, download=download,
181    )
182
183    _preprocess_adrenal_acc(dicom_dir, f"{csv_path}.csv", preprocessed_dir)
184    return preprocessed_dir

Download the Adrenal-ACC-Ki67-Seg dataset.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • download: Whether to download the data if it is not present.
Returns:

Filepath where the preprocessed data is stored.

def get_adrenal_acc_paths(path: Union[os.PathLike, str], download: bool = False) -> List[str]:
187def get_adrenal_acc_paths(path: Union[os.PathLike, str], download: bool = False) -> List[str]:
188    """Get paths to the Adrenal-ACC-Ki67-Seg data.
189
190    Args:
191        path: Filepath to a folder where the data is downloaded for further processing.
192        download: Whether to download the data if it is not present.
193
194    Returns:
195        List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').
196    """
197    data_dir = get_adrenal_acc_data(path, download)
198    volume_paths = natsorted(glob(os.path.join(data_dir, "*.h5")))
199    return volume_paths

Get paths to the Adrenal-ACC-Ki67-Seg data.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • download: Whether to download the data if it is not present.
Returns:

List of filepaths for the hdf5 files, which contain the image data ('raw') and the label data ('labels').

def get_adrenal_acc_dataset( path: Union[os.PathLike, str], patch_shape: Tuple[int, ...], resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataset.Dataset:
202def get_adrenal_acc_dataset(
203    path: Union[os.PathLike, str],
204    patch_shape: Tuple[int, ...],
205    resize_inputs: bool = False,
206    download: bool = False,
207    **kwargs
208) -> Dataset:
209    """Get the Adrenal-ACC-Ki67-Seg dataset for adrenal tumor segmentation.
210
211    Args:
212        path: Filepath to a folder where the data is downloaded for further processing.
213        patch_shape: The patch shape to use for training.
214        resize_inputs: Whether to resize inputs to the desired patch shape.
215        download: Whether to download the data if it is not present.
216        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset`.
217
218    Returns:
219        The segmentation dataset.
220    """
221    volume_paths = get_adrenal_acc_paths(path, download)
222
223    if resize_inputs:
224        resize_kwargs = {"patch_shape": patch_shape, "is_rgb": False}
225        kwargs, patch_shape = util.update_kwargs_for_resize_trafo(
226            kwargs=kwargs, patch_shape=patch_shape, resize_inputs=resize_inputs, resize_kwargs=resize_kwargs
227        )
228
229    return torch_em.default_segmentation_dataset(
230        raw_paths=volume_paths,
231        raw_key="raw",
232        label_paths=volume_paths,
233        label_key="labels",
234        patch_shape=patch_shape,
235        is_seg_dataset=True,
236        **kwargs
237    )

Get the Adrenal-ACC-Ki67-Seg dataset for adrenal tumor segmentation.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • patch_shape: The patch shape to use for training.
  • resize_inputs: Whether to resize inputs to the desired patch shape.
  • download: Whether to download the data if it is not present.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset.
Returns:

The segmentation dataset.

def get_adrenal_acc_loader( path: Union[os.PathLike, str], batch_size: int, patch_shape: Tuple[int, ...], resize_inputs: bool = False, download: bool = False, **kwargs) -> torch.utils.data.dataloader.DataLoader:
240def get_adrenal_acc_loader(
241    path: Union[os.PathLike, str],
242    batch_size: int,
243    patch_shape: Tuple[int, ...],
244    resize_inputs: bool = False,
245    download: bool = False,
246    **kwargs
247) -> DataLoader:
248    """Get the Adrenal-ACC-Ki67-Seg dataloader for adrenal tumor segmentation.
249
250    Args:
251        path: Filepath to a folder where the data is downloaded for further processing.
252        batch_size: The batch size for training.
253        patch_shape: The patch shape to use for training.
254        resize_inputs: Whether to resize inputs to the desired patch shape.
255        download: Whether to download the data if it is not present.
256        kwargs: Additional keyword arguments for `torch_em.default_segmentation_dataset` or for the PyTorch DataLoader.
257
258    Returns:
259        The DataLoader.
260    """
261    ds_kwargs, loader_kwargs = util.split_kwargs(torch_em.default_segmentation_dataset, **kwargs)
262    dataset = get_adrenal_acc_dataset(path, patch_shape, resize_inputs, download, **ds_kwargs)
263    return torch_em.get_data_loader(dataset, batch_size, **loader_kwargs)

Get the Adrenal-ACC-Ki67-Seg dataloader for adrenal tumor segmentation.

Arguments:
  • path: Filepath to a folder where the data is downloaded for further processing.
  • batch_size: The batch size for training.
  • patch_shape: The patch shape to use for training.
  • resize_inputs: Whether to resize inputs to the desired patch shape.
  • download: Whether to download the data if it is not present.
  • kwargs: Additional keyword arguments for torch_em.default_segmentation_dataset or for the PyTorch DataLoader.
Returns:

The DataLoader.