Source code for protoclass.extraction.spatial_extraction

"""Spatial feature extraction from standalone modality."""
from __future__ import division

import numpy as np

from scipy.spatial import cKDTree
from scipy.ndimage.measurements import center_of_mass

from skimage.measure import find_contours

from sklearn.preprocessing import MinMaxScaler

from .standalone_extraction import StandaloneExtraction

KIND_KNOWN = ('position', 'distance')
COORD_SYSTEM_KNOWN = ('euclidean', 'cylindrical')
REFERENCE = ('centre', 'nn-contour-point')


[docs]class SpatialExtraction(StandaloneExtraction): """Spatial extraction from standalone modality. Parameters ---------- base_modality : object The base modality on which the normalization will be applied. The base modality should inherate from StandaloneModality class. kind : str, optional (default='position') The type of spatial feature to extract. Can be either 'position' or 'distance'. coord_system : str, optional (default='euclidean') The type of coordinate system to use. Can be either 'euclidean' or 'cylindrical'. reference : str, optional (default='centre') If type is 'distance', the reference can be either 'centre' or 'nn-contour-point' Attributes ---------- base_modality_ : object The base modality on which the normalization will be applied. The base modality should inherate from StandaloneModality class. roi_data_ : ndarray, shape flexible Corresponds to the index to consider in order to fit the data. data_ : ndarray, Containing the data of all the slices according to the desired spatial information """
[docs] def __init__(self, base_modality, kind='position', coord_system='euclidean', reference='centre'): super(SpatialExtraction, self).__init__(base_modality) self.kind = kind self.coord_system = coord_system self.reference = reference self.data_ = None
[docs] def fit(self, modality, ground_truth=None, cat=None): """Compute the images images. Parameters ---------- modality : object of type TemporalModality The modality object of interest. ground-truth : object of type GTModality or None The ground-truth of GTModality. If None, the whole data will be considered. cat : str or None String corresponding at the ground-truth of interest. Cannot be None if ground-truth is not None. Return ------ self : object Return self. """ super(SpatialExtraction, self).fit(modality=modality, ground_truth=ground_truth, cat=cat) if ground_truth is None: raise ValueError('It is not possible to compute relative position' ' without a segmentation of the prostate organ.') if self.kind == 'distance' and self.coord_system == 'cylindrical': raise ValueError('The distance cannot be expressed in cylindrical' ' coordinate.') # Compute the affine matrix to compute the real world coordinate direc_mat = np.reshape(modality.metadata_['direction'], (3, 3)) orig_mat = np.array(modality.metadata_['origin']) spac_mat = np.array(modality.metadata_['spacing']) affine_mat = np.matrix([[direc_mat[0, 0] * spac_mat[0], direc_mat[0, 1] * spac_mat[1], direc_mat[0, 2] * spac_mat[2], orig_mat[0]], [direc_mat[1, 0] * spac_mat[0], direc_mat[1, 1] * spac_mat[1], direc_mat[1, 2] * spac_mat[2], orig_mat[1]], [direc_mat[2, 0] * spac_mat[0], direc_mat[2, 1] * spac_mat[1], direc_mat[2, 2] * spac_mat[2], orig_mat[2]], [0, 0, 0, 1]]) # Build all the possible coordinate of the image in the real world sz_mat = modality.metadata_['size'] X, Y, Z = np.meshgrid(range(sz_mat[0]), range(sz_mat[1]), range(sz_mat[2])) # Linearize the indexes X = X.reshape(-1) Y = Y.reshape(-1) Z = Z.reshape(-1) # Build a huge matrix with all the coordinate coord_voxel = np.matrix([X, Y, Z, np.array([1] * X.size)]) # Compute the projection in real world coord_real = affine_mat * coord_voxel # Remove the last row which is not useful to compute the distances coord_real = coord_real[:-1, :] # We need to make the computation regarding the reference to consider if self.kind == 'distance' and self.reference == 'nn-contour-point': # Create a ground-truth volume gt = np.zeros(modality.data_.shape) gt[self.roi_data_] = 255. contour_idx = np.empty([0, 3]) # We need to compute the contour of the ground-truth for sl in range(gt.shape[2]): # Check that this is a slice with some ground-truth if np.unique(gt[:, :, sl]).size == 1: continue slice_idx = find_contours(gt[:, :, sl], 254.) if len(slice_idx) != 1: raise ValueError('There is a problem. More than one' ' contour have been found.') # We need to concatenate the slice position slice_idx = np.concatenate((slice_idx[0], np.atleast_2d( [sl] * slice_idx[0].shape[0]).T), axis=1) # Good to be concatenated contour_idx = np.concatenate((contour_idx, slice_idx), axis=0) # We need to swap X and Y to be sure that everything will go fine contour_idx[:, 0], contour_idx[:, 1] = (contour_idx[:, 1], contour_idx[:, 0].copy()) # Transform to homogeneous coordinates contour_idx = np.concatenate((contour_idx, np.atleast_2d( [1] * contour_idx.shape[0]).T), axis=1) # Transform the data in the real world coordinate contour_idx = (affine_mat * contour_idx.T).T # Remove the last row which is not useful to compute the distance # later contour_idx = contour_idx[:, :-1] # Fit a tree to find the nearest neighbours contour point later on self.tree = cKDTree(contour_idx) # Compute the distances for all the data dist, _ = self.tree.query(coord_real.T, k=1, n_jobs=-1) # We need to reshape the distance such that it corresponds to the # original size and we need to swap Y and X back self.data_ = np.swapaxes(np.reshape(dist, sz_mat), 0, 1) elif self.reference == 'centre': # Create a ground-truth volume gt = np.zeros(modality.data_.shape) gt[self.roi_data_] = 1. # Compute the center of mass of the ground-truth centre = center_of_mass(gt) # We need to change X and Y and express in homogeneous coordinate centre = np.atleast_2d((centre[1], centre[0], centre[2], 1)).T # Compute the coordinate of the centre in the real world # coordinate system centre = affine_mat * centre # Remove the last row self.centre = centre[:-1, :] # Compute the relative position in the euclidean space which will # be used afterwards coord_real -= self.centre # Reshape the coordinate properly X = np.squeeze(np.array(coord_real[0, :])) Y = np.squeeze(np.array(coord_real[1, :])) Z = np.squeeze(np.array(coord_real[2, :])) self.data_ = np.array([Y.reshape(modality.data_.shape), X.reshape(modality.data_.shape), Z.reshape(modality.data_.shape)]) # We need to extract either the position or the distance if self.kind == 'position' and self.coord_system == 'cylindrical': # Convert the euclidean coordinate to cylindrical coordinate # Copy the data temporary eucl_data = self.data_.copy() self.data_[0] = np.sqrt(eucl_data[0] ** 2 + eucl_data[1] ** 2) self.data_[1] = np.arctan2(eucl_data[0], eucl_data[1]) elif self.kind == 'distance': # Compute the euclidean distance self.data_ = np.sum(self.data_ ** 2, axis=0) return self
[docs] def transform(self, modality, ground_truth=None, cat=None): """Extract the data from the given modality. Parameters ---------- modality : object of type StandaloneModality The modality object of interest. ground-truth : object of type GTModality or None The ground-truth of GTModality. If None, the whole data will be considered. cat : str or None String corresponding at the ground-truth of interest. Cannot be None if ground-truth is not None. Returns ------ data : ndarray, shape (n_sample, n_feature) A matrix containing the features extracted. The number of samples is equal to the number of positive label in the ground-truth. If distance, a single feature is returned. If position, 3 features are returned. """ super(SpatialExtraction, self).transform( modality=modality, ground_truth=ground_truth, cat=cat) # Check that we fitted the data if self.data_ is None: raise RuntimeError('Fit the data before to extract anything.') if self.kind == 'position': # Allocate the data data = np.zeros((self.roi_data_[0].size, 3)) # Extract the data for each feature for feat_dim in range(3): feat_data = self.data_[feat_dim] data[:, feat_dim] = feat_data[self.roi_data_] # We need to normalize the data if self.coord_system == 'euclidean': # Define an object for the normalization norm_obj = MinMaxScaler() data = norm_obj.fit_transform(data) elif self.coord_system == 'cyclindrical': # We need to normalize r and z between -1 and 1 norm_obj = MinMaxScaler(feature_range=(-1, 1)) # r component data[:, 0] = MinMaxScaler(data[:, 0]) # Z component data[:, 2] = MinMaxScaler(data[:, 2]) elif self.kind == 'distance': data = self.data_[self.roi_data_] # Normalize the distance with the max data = data / np.max(data) return data