def __init__(self, image, bbox=None, mask=False, grid_spacing=None, interpolation_order=3): """ Creates a new SampledVolumeSlicer Parameters ---------- image : a NIPY Image The image to slice bbox : iterable (optional) The {x,y,z} limits of the enrounding volume box. If None, then slices planes in the natural box of the image. This argument is useful for overlaying an image onto another image's volume box mask : bool or ndarray (optional) A binary mask, with same shape as image, with unmasked points marked as True (opposite of MaskedArray convention) grid_spacing : iterable (optional) New grid spacing for the sliced planes. If None, then the natural voxel spacing is used. """ xyz_image = ni_api.Image( np.asarray(image), image.coordmap.reordered_range(xipy_ras) ) ## reorder_output(image.coordmap, 'xyz')) self.coordmap = xyz_image.coordmap self.raw_image = xyz_image nvox = np.product(self.raw_image.shape) # if the volume array of the map is more than 100mb in memory, # better use a memmap self._use_mmap = nvox*8 > 100e6 self.interpolator = ImageInterpolator(xyz_image, order=interpolation_order, use_mmap=self._use_mmap) ## if mask is True: ## mask = compute_mask(np.asarray(self.raw_image), cc=0, m=.1, M=.999) if type(mask) is np.ndarray: self.update_mask(mask, positive_mask=True) else: self._masking = False self.raw_mask = None if grid_spacing is None: self.grid_spacing = list(vu.voxel_size(image.affine)) else: self.grid_spacing = grid_spacing if bbox is None: self._nominal_bbox = vu.world_limits(xyz_image) else: self._nominal_bbox = bbox # defines {x,y,z}shape and {x,y,z}grid self._define_grids()
def update_mask(self, mask, positive_mask=True): cmap = self.coordmap if not positive_mask: # IE, if this is a MaskedArray type mask mask = np.logical_not(mask) ## fmask = np.array([ndimage.binary_fill_holes(m) for m in mask], 'd') self.m_interpolator = ImageInterpolator(ni_api.Image(mask, cmap), order=3, use_mmap=self._use_mmap) self.raw_mask = mask self._masking = True
def resample(image, target, mapping, shape, order=3, **interp_kws): """ Resample an image to a target CoordinateMap with a "world-to-world" mapping and spline interpolation of a given order. Here, "world-to-world" refers to the fact that mapping should be a callable that takes a physical coordinate in "target" and gives a physical coordinate in "image". Parameters ---------- image : Image instance that is to be resampled target :target CoordinateMap for output image mapping : transformation from target.function_range to image.coordmap.function_range, i.e. 'world-to-world mapping' Can be specified in three ways: a callable, a tuple (A, b) representing the mapping y=dot(A,x)+b or a representation of this in homogeneous coordinates. shape : shape of output array, in target.function_domain order : what order of interpolation to use in `scipy.ndimage` interp_kws : keyword arguments for ndimage interpolator routine Returns ------- output : Image instance with interpolated data and output.coordmap == target """ if not callable(mapping): if type(mapping) is type(()): A, b = mapping ndimout = b.shape[0] ndimin = A.shape[1] mapping = np.zeros((ndimout+1, ndimin+1)) mapping[:ndimout,:ndimin] = A mapping[:ndimout,-1] = b mapping[-1,-1] = 1. # image world to target world mapping TW2IW = AffineTransform(target.function_range, image.coordmap.function_range, mapping) else: TW2IW = CoordinateMap(mapping, target.function_range, image.coordmap.function_range) function_domain = target.function_domain function_range = image.coordmap.function_range # target voxel to image world mapping TV2IW = compose(TW2IW, target) # CoordinateMap describing mapping from target voxel to # image world coordinates if not isinstance(TV2IW, AffineTransform): # interpolator evaluates image at values image.coordmap.function_range, # i.e. physical coordinates rather than voxel coordinates grid = ArrayCoordMap.from_shape(TV2IW, shape) interp = ImageInterpolator(image, order=order) idata = interp.evaluate(grid.transposed_values, **interp_kws) del(interp) else: TV2IV = compose(image.coordmap.inverse(), TV2IW) if isinstance(TV2IV, AffineTransform): A, b = affines.to_matrix_vector(TV2IV.affine) data = np.asarray(image) idata = affine_transform(data, A, offset=b, output_shape=shape, output=data.dtype, order=order, **interp_kws) else: interp = ImageInterpolator(image, order=order) grid = ArrayCoordMap.from_shape(TV2IV, shape) idata = interp.evaluate(grid.values, **interp_kws) del(interp) return Image(idata, target)
class SampledVolumeSlicer(VolumeSlicerInterface): """ This object cuts up an image along the axes defined by its CoordinateMap target space. The SampledVolumeSlicer provides slices through an image such that the cut planes extend across the three {x,y,z} planes of the target space. Each plane is sampled from the original image voxel array by spline interpolation. """ def __init__(self, image, bbox=None, mask=False, grid_spacing=None, interpolation_order=3): """ Creates a new SampledVolumeSlicer Parameters ---------- image : a NIPY Image The image to slice bbox : iterable (optional) The {x,y,z} limits of the enrounding volume box. If None, then slices planes in the natural box of the image. This argument is useful for overlaying an image onto another image's volume box mask : bool or ndarray (optional) A binary mask, with same shape as image, with unmasked points marked as True (opposite of MaskedArray convention) grid_spacing : iterable (optional) New grid spacing for the sliced planes. If None, then the natural voxel spacing is used. """ xyz_image = ni_api.Image( np.asarray(image), image.coordmap.reordered_range(xipy_ras) ) ## reorder_output(image.coordmap, 'xyz')) self.coordmap = xyz_image.coordmap self.raw_image = xyz_image nvox = np.product(self.raw_image.shape) # if the volume array of the map is more than 100mb in memory, # better use a memmap self._use_mmap = nvox*8 > 100e6 self.interpolator = ImageInterpolator(xyz_image, order=interpolation_order, use_mmap=self._use_mmap) ## if mask is True: ## mask = compute_mask(np.asarray(self.raw_image), cc=0, m=.1, M=.999) if type(mask) is np.ndarray: self.update_mask(mask, positive_mask=True) else: self._masking = False self.raw_mask = None if grid_spacing is None: self.grid_spacing = list(vu.voxel_size(image.affine)) else: self.grid_spacing = grid_spacing if bbox is None: self._nominal_bbox = vu.world_limits(xyz_image) else: self._nominal_bbox = bbox # defines {x,y,z}shape and {x,y,z}grid self._define_grids() def _define_grids(self): dx, dy, dz = self.grid_spacing xbox, ybox, zbox = map( lambda x: np.array(x).reshape(2,2), vu.limits_to_extents(self._nominal_bbox) ) # xbox is [ <ylims> , <zlims> ], so xgrid is [ypts.T, zpts.T] xr = np.diff(xbox)[:,0] xspacing = np.array([dy, dz]) self.xshape = (xr / xspacing).astype('i') aff = ni_api.AffineTransform.from_start_step('ij', 'xy', xbox[:,0], xspacing) self.xgrid = ni_api.ArrayCoordMap(aff, self.xshape).values # ybox is [ <xlims>, <zlims> ], so ygrid is [xpts.T, zpts.T] yr = np.diff(ybox)[:,0] yspacing = np.array([dx, dz]) self.yshape = (yr / yspacing).astype('i') aff = ni_api.AffineTransform.from_start_step('ij', 'xy', ybox[:,0], yspacing) self.ygrid = ni_api.ArrayCoordMap(aff, self.yshape).values # zbox is [ <xlims>, <ylims> ], so zgrid is [xpts.T, ypts.T] zr = np.diff(zbox)[:,0] zspacing = np.array([dx, dy]) self.zshape = (zr / zspacing).astype('i') aff = ni_api.AffineTransform.from_start_step('ij', 'xy', zbox[:,0], zspacing) self.zgrid = ni_api.ArrayCoordMap(aff, self.zshape).values bb_min = [b[0] for b in self._nominal_bbox] lx = self.yshape[0] ly = self.xshape[0] lz = self.yshape[1] bb_max = [bb_min[0] + dx*lx, bb_min[1] + dy*ly, bb_min[2] + dz*lz] self.bbox = zip(bb_min, bb_max) def update_mask(self, mask, positive_mask=True): cmap = self.coordmap if not positive_mask: # IE, if this is a MaskedArray type mask mask = np.logical_not(mask) ## fmask = np.array([ndimage.binary_fill_holes(m) for m in mask], 'd') self.m_interpolator = ImageInterpolator(ni_api.Image(mask, cmap), order=3, use_mmap=self._use_mmap) self.raw_mask = mask self._masking = True def _update_grid_spacing(self, grid_spacing): self.grid_spacing = grid_spacing self._define_grids() ## def update_mask_crit(self, crit, thresh): ## self._masking = True ## self._mcrit = crit ## self._thresh = thresh def _closest_grid_pt(self, coord, ax): """For a given coordinate value on an axis, find the closest fixed grid point as defined on the complementary grid planes. This is to ensure consistency at the intersection of interpolated planes """ ax_min = np.asarray(self.bbox)[ax,0] ax_step = self.grid_spacing[ax] # ax_min + n*ax_step = coord new_coord = round ( (coord-ax_min) / ax_step ) * ax_step + ax_min return new_coord def _cut_plane(self, ax, coord, **interp_kw): """ For a given axis in {SAG, COR, AXI}, make a plane cut in the volume at the coordinate value. Parameters ---------- ax : int axis label in {SAG, COR, AXI} (defined in xipy.slicing) coord : float coordinate value along this axis interp_kw : dict Keyword args for the interpolating machinery (ie, ndimage.map_coordinates keyword args) Returns _______ plane : ndarray The transverse plane sampled at the grid points and fixed axis coordinate for the given args """ # a little hokey grid_lookup = {SAG: ('xshape', 'xgrid'), COR: ('yshape', 'ygrid'), AXI: ('zshape', 'zgrid')} sname, gname= grid_lookup[ax] ii, jj = transverse_plane_lookup(ax) grid = getattr(self, gname) shape = getattr(self, sname) coords = np.empty((3, grid.shape[0]), 'd') coords[ax] = self._closest_grid_pt(coord, ax) coords[ii] = grid[:,0]; coords[jj] = grid[:,1] pln = self.interpolator.evaluate(coords, **interp_kw).reshape(shape) if self._masking: m_pln = self.m_interpolator.evaluate(coords, mode='constant', cval=-10).reshape(shape) return np.ma.masked_where(m_pln < 0.5, pln) return pln def update_target_space(self, coreg_image): raise NotImplementedError('not sure how to do this yet')