Source code for dicom3d.series

import pydicom
import numpy as np

from .dataset import Dataset
from .data import walk_data

[docs]class Series(): """ Class responsible for managing a volumetric scan, comprised by a list of successive datasets Note: When constructed, this class performs the following operations - sorts the given datasets by *ZLocation* - wraps the datasets in **dicom3d.Dataset** class - checks the series for spacial homogeneity Attention: Homogeneity check is required by this class to make sure the assumptions it makes over the volumetric scan, are correct. These assumptions are related to space contiguity between datasets and consistent density and size among datasets Important: A series is homogeneous if: - all datasets are continuous on the Z axis (no gaps) - each dataset has identical values for these attributes - SliceThickness - PixelSpacing - Rows - Columns - ImagePositionPatient - ImageOrientationPatient """ def __init__(self, datasets, check_homogeneity=True): # wrao datasets in helper class wrapped_datasets = self._wrap(datasets) # sort datasets by SliceLocation self.datasets = sorted(wrapped_datasets, key=lambda ds: ds.ZLocation) # fix index for idx, dataset in enumerate(self.datasets): dataset.index = idx # check for consistency in Z-location, thickness, density etc. if check_homogeneity: self._verify_homogeneity() self._build_mapping() else: self.homogeneous = False self.mapping = None self.pixel_data = None
[docs] @staticmethod def from_directory(path, pattern="*.dcm", check_homogeneity=True): """ Constructs a series from a directory of DICOM files Args: path (str): path to the directory continaing the DICOM files pattern (str, optional): Wildcard pattern of DIICOM files, default is "\*.dcm" check_homogeneity (bool, optional): Homogeneity test, default is **True** Returns: [Series]: a **dicom3d.Series** object """ # enumerate all files with this pattern files = walk_data( path, pattern, search_files=True, search_dirs=False) # read the datasets datasets = [ pydicom.dcmread(image_filename) for image_filename in files ] if len(datasets) == 0: raise Exception("no datasets found to build series from directory '%s'" % ( path )) # construct the series return Series( datasets, check_homogeneity = check_homogeneity)
[docs] @staticmethod def from_dataset(dataset): """ Constructs a series from a single dataset. Helpful if you want to build section images from planar datasets Args: dataset (Dataset): planar DICOM dataset Returns: Series: resulted series object """ series = Series([dataset]) series.homogeneous = True return series
[docs] @staticmethod def fix_z_duplicates(datasets): """ Attempts to curate a list of datasets that has datasets duplicated on the Z axis Args:s datasets (list): list of pydicom.dataset.Dataset objects Important: This code is experimental and is used to fix some series that fail the homogeneity test. """ # construct a set of SliceLocations locations_set = { ds.SliceLocation for ds in datasets } # if the number of locations is lower than the number of datasets # => dataset list has duplicated elements in regards of SliceLocation if len(locations_set) != len(datasets): curated = [] # eliminate those datasets with duplicated SliceLocation for ds in datasets: # if in unique location list, add to curated # if not, it's a duplicate dataset if ds.SliceLocation in locations_set: curated.append(ds) locations_set.remove(ds.SliceLocation) return curated # the original list has no duplicates return datasets
def _wrap(self, datasets): """ wrap each dataset into the 'dataset' class """ if len(datasets) == 0: return [] # already wraped, re-wrap if type(datasets[0]) is Dataset: return [ Dataset(x.dataset, self, idx) for idx,x in enumerate(datasets) ] return [ Dataset(x, self, idx) for idx,x in enumerate(datasets) ] def _build_mapping(self): # avoid using SliceThickness if len(self.datasets) > 1: thick = self.datasets[1].ZLocation - self.datasets[0].ZLocation else: thick = self.datasets[0].SliceThickness start_z = self.first().ZLocation end_z = self.last().ZLocation + thick self.mapping = (start_z, end_z, thick) return True def _ensure_homogeneity(self): if self.homogeneous == False: raise ValueError("series must be homogeneous to use this function") def _verify_homogeneity(self, tolerance=1.e-4): """ checks if the given series of datasets is continuous (i.e. each dataset starts where the previous dataset ends) and also checks if all datasets have the same thinkness """ # single dataset series if len(self.datasets) == 1: self.homogeneous = True return # check continuity prev = self.first() ref_thickness = prev.SliceThickness ref_distance = prev.next().ZLocation - prev.ZLocation ref_Rows = prev.Rows ref_Columns = prev.Columns ref_imop = prev.ImageOrientationPatient ref_density = prev.PixelSpacing if ref_distance == 0: raise Exception("homogeneity test failed: first datasets have the same Z location") for dataset in self.datasets[1:]: # check if a datasets ends where the other one begins diff = dataset.ZLocation - prev.ZLocation - ref_distance if abs(diff) > tolerance: raise Exception("homogeneity test failed: series is not continuous on the Z axis") # check if a dataset has different slice thickness if dataset.SliceThickness != ref_thickness: raise Exception("homogeneity test failed: series has datasets with variable thickness") # check for same rows and columns if dataset.Rows != ref_Rows or dataset.Columns != ref_Columns: raise Exception("homogeneity test failed: series has datasets with variable Rows,Columns configuration") # datasets must have same coordinate system if dataset.ImageOrientationPatient != ref_imop: raise Exception("homogeneity test failed: series has datasets with different orientation (ImageOrientationPatient)") # datasets must have same density if dataset.PixelSpacing != ref_density: raise Exception("homogeneity test failed: series has datasets with different density (PixelSpacing)") prev = dataset self.homogeneous = True return
[docs] def cache(self): """ Builds a three-dimensional numpy array from all pixel data from datasets. Important: Reserved for future use! Returns: numpy.array: numpy array of the pixel data """ if self.pixel_data is not None: return self.pixel_data self._ensure_homogeneity() dataset = self.first() zd, yd, xd = len(self.datasets), dataset.Rows, dataset.Columns pixel_data = np.zeros((zd, yd, xd),dtype=np.long) # populate for zi, dataset in enumerate(self.datasets): pixel_data[zi,:,:] = dataset.pixel_array self.pixel_data = pixel_data return pixel_data
[docs] def first(self): """ Retrives the first dataset from series. The first dataset is located at the bottom of the Z segment, therefore it will have the lowest Z coordinate value. Returns: Dataset: a **dicom3d.Dataset** object """ return self.datasets[0]
[docs] def last(self): """ Retrives the last dataset from series. The last dataset is located at the top of the Z segment, therefore it will have the highest Z coordinate value. Returns: Dataset: a **dicom3d.Dataset** object """ return self.datasets[len(self.datasets) -1]
[docs] def middle(self): """ Retrives the dataset from series that is on the middle of the Z segment. Returns: Dataset: a **dicom3d.Dataset** object """ return self.datasets[len(self.datasets)//2]
[docs] def count(self): """ Returns the number of datasets Returns: int: legnth of datasets list """ return len(self.datasets)
[docs] def z_bounds(self): """ Returns a tuple representing the world Z coordinates of the volumetric scan, including the top dataset and its thickness. Important: If homogeneity test was disabled for this series, this function will raise an exception Returns: tuple: a tuple of float values representing the Z bounds Examples: >>> z_start, z_end = series.z_bounds() >>> print("Series starts from %.1f to %.1f mm on the Z axis" % (z_start, z_end) ) """ self._ensure_homogeneity() start_z, end_z, _ = self.mapping return (start_z, end_z)
[docs] def at_index(self, index): """ Returns the dataset at the corresponding index or None if index is out of bounds. Args: index (int): dataset index in series Returns: Dataset: the **dicom3d.Dataset** object at **index** """ if index < 0 or index > len(self.datasets): return None # exceeds list return self.datasets[index]
[docs] def at_z(self, z_loc): """ Returns the dataset that intersects the given Z location in world coordinates Important: If homogeneity test was disabled for this series, this function will raise an exception Args: z_loc (float): value describing location on the Z axis Returns: Dataset: the corresponding **dicom3d.Dataset** object """ self._ensure_homogeneity() # linear mapping -> calculate index start_z, end_z, thick = self.mapping if z_loc >= end_z or z_loc < start_z: return None # exceeds Z segment idx = int((z_loc - start_z) / thick) return self.datasets[idx]
[docs] def at(self, where): """ This is a wrapper function for ease of use over **at_index** and **at_z** functions. Args: where (int, float, tuple, Point): if **where** is an integer is treated like an index, else is considered to be a Z location Returns: Dataset: the corresponding **dicom3d.Dataset** object Examples: >>> dataset = series.at(1.0) # gets dataset at 0.1 Z coordinate >>> dataset = series.at(1) # gets dataset at index 1 >>> dataset = series.at(Point(5,5,2)) # gets dataset at 2.0 Z coordinate """ if type(where) is int: return self.at_index(where) if type(where) is float: return self.at_z(where) if type(where) is tuple and len(where) == 3 or type(where) is Point: x,y,z = where dataset = self.at_z(z) if dataset is None: return None return dataset if dataset.intersects_xy((x,y)) else None raise ValueError("invalid type for looking up dataset (%s)" % (type(where)))
[docs] def find_dataset(self, z_loc): """ Despite **Series.at_z** method, this one doesn't require homogenuity test to be ran If will effectively search though the list to find the corresponding dataset, instead of calculating the index based on Z-bounds and dataset thickness. Args: z_loc (float): value describing location on the Z axiss Returns: int: index of the corresponding dataset or -1 if not found """ for idx, dataset in enumerate(self.datasets): if z_loc >= dataset.ZLocation and z_loc < dataset.ZLocation + dataset.SliceThickness: return idx return -1