import numpy as np
[docs]class ion_datacube():
# a class for holding a datacube from an MSI dataset and producing ion images
def __init__(self, step_size=[]): # define mandatory parameters (todo- validator to check all present)
self.xic = [] # 'xtracted ion chromatogram (2d array of intensities(vector of time by mzs))
self.bounding_box = [] # tuple (x,y,w,h) in co-ordinate space
self.coords = [] # co-ordiantes of every pixel
self.mzs = [] # centroids of each xic vector
self.tol = [] # tolerance window around centroids in m/z
if step_size == []:
self.step_size = []
else:
self.step_size = step_size
self.n_im = 0
[docs] def add_xic(self, xic, mz, tol):
# add an eXtracted Ion Chromatogram to the datacube
if len(self.coords) != len(xic):
raise ValueError(
'size of co-ordinates to not match xic (coords:{} xic:{})'.format(len(self.coords), len(xic)))
self.xic.append(xic) # = np.concatenate((self.xic,xic),axis=1)
self.mzs = np.concatenate((self.mzs, mz))
self.tol = np.concatenate((self.tol, tol))
self.n_im += 1
[docs] def remove_xic(self, mz):
# remove an xic and related info
raise NotImplementedError
index_to_remove = self.mz.index(mz)
self.n_im -= 1
[docs] def add_coords(self, coords):
# record spatial co-ordinates for each spectrum
# coords is a ix3 array with x,y,z coords
self.coords = coords
# if len(self.xic)==0:
# self.xic = np.zeros((len(coords),0))
self.calculate_bounding_box()
self.coord_to_index()
[docs] def calculate_bounding_box(self):
self.bounding_box = []
for ii in range(0, 3):
self.bounding_box.append(np.amin(self.coords[:, ii]))
self.bounding_box.append(np.amax(self.coords[:, ii]))
[docs] def coord_to_index(self, transform_type='reg_grid', params=()):
# this function maps coords onto pixel indicies (assuming a grid defined by bounding box and transform type)
# -implement methods such as interp onto grid spaced over coords
# -currently treats coords as grid positions,
if transform_type == 'reg_grid':
# data was collected on a grid
# - coordinates can transformed directly into pixel indiceis
# - subtract smallest value and divide by x & y step size
pixel_indices = np.zeros(len(self.coords))
_coord = np.asarray(self.coords, dtype=float)
_coord = np.around(_coord, 5) # correct for numerical precision
_coord = _coord - np.amin(_coord, axis=0)
if self.step_size == []: # no additional info, guess step size in xyz
self.step_size = np.zeros((3, 1))
for ii in range(0, 3):
self.step_size[ii] = np.mean(np.diff(np.unique(_coord[:, ii])))
self.step_size[np.isnan(self.step_size)] = 1
# coordinate to pixels
_coord = np.asarray(_coord / np.reshape(self.step_size, (3,)),dtype=int)
_coord_max = np.amax(_coord, axis=0)
self.nColumns = _coord_max[1] + 1
self.nRows = _coord_max[0] + 1
pixel_indices = _coord[:, 0] * self.nColumns + _coord[:, 1]
pixel_indices = pixel_indices.astype(np.int32)
else:
raise ValueError('transform type not recognised')
self.pixel_indices = pixel_indices
[docs] def xic_to_image(self, xic_index, padval=-1):
xic = self.xic[xic_index].copy()
# turn xic into an image
img = padval + np.zeros(self.nRows * self.nColumns)
img[self.pixel_indices] = xic
img = np.reshape(img, (self.nRows, self.nColumns))
return img
[docs] def xic_to_image_fixed_size(self, xic_index, out_im_size):
"""
returns an xic formatted as an image. This function returns an image of a pre-specfied size, cropping or padding equally around the image as required.
:param xic_index: index of the xic to turn into an image
:param out_im_size: desired image size
:return:
"""
def calc_pad(px1, px2):
delta = px1-px2
if delta < 0:
return 0, 0
return delta/2, delta/2+delta%2
def calc_crop(px1, px2):
delta = px1 - px2
if delta > 0:
return 0, -1
delta = abs(delta)
return delta/2, -1*delta/2+delta%2
pad_total_pixels = map(calc_pad, out_im_size, (self.nRows, self.nColumns))
crop_total_pixels = map(calc_crop, out_im_size, (self.nRows, self.nColumns))
img = self.xic_to_image(xic_index)
img=np.asarray(img)
img = img[crop_total_pixels[0][0]:crop_total_pixels[0][1], crop_total_pixels[1][0]:crop_total_pixels[1][1]]
img = np.pad(img, ((pad_total_pixels[0][0], pad_total_pixels[0][1]), (pad_total_pixels[1][0], pad_total_pixels[1][1])), 'constant', constant_values=-1)
return img
[docs] def image_to_xic(self, im):
"""
takes an image (presumably generated by xic to image) and returns a vector suitable for insertion into the datacube
:param im:
numpy 2d array
:return:
numpy vector
"""
im = np.reshape(im, self.nRows * self.nColumns)
xic = im[self.pixel_indices]
return xic
[docs] def apply_image_processing(self, smooth_method, **smooth_params):
"""
Function to apply pre-defined image processing methods to ion_datacube
#todo: expose parameters in config
:param ion_datacube:
object from pyImagingMSpec.ion_datacube already containing images
:return:
ion_datacube is updated in place.
None returned
"""
from pyImagingMSpec import smoothing
for ii in range(self.n_im):
im = self.xic_to_image(ii)
# todo: for method in smoothing_methods:
methodToCall = getattr(smoothing, smooth_method)
im_s = methodToCall(im, **smooth_params)
self.xic[ii] = self.image_to_xic(im_s)