Source code for crikit.data.spectra

"""
Spectra class and function (very similar to Spetcrum except this deals with
multiple entries)

"""

import numpy as _np
import copy as _copy

from crikit.data.frequency import Frequency as _Frequency
from crikit.data.replicate import Replicate as _Replicate

__all__ = ['Spectrum', 'Spectra', 'Hsi']


[docs]class Spectrum: """ Spectrum class Attributes ---------- data : 1D ndarray [f_pix] Spectrum freq : crikit.data.Frequency instance Frequency [wavelength, wavenumber] object (i.e., the independent \ variable) label : str Spectrum label (i.e., a string describing what the spectrum is) units : str Units of spectrum meta : dict Meta-data dictionary f_pix : int, read-only Size of data. Note: this matches the size of data and does NOT check \ the size of freq.freq_vec. ndim : int, read-only Number of data dimensions shape : tuple, read-only Shape of data size : int, read-only Size of data (i.e., total number of entries) Methods ------- mean : int Mean value. If extent [a,b] is provided, calculate mean over that\ inclusive region. std : int Standard deviation. If extent [a,b] is provided, calculate standard\ deviation over that inclusive region. subtract : 1D ndarray or None Subtract spectrum or object Notes ----- * freq object contains some useful parameters such as op_range* and \ plot_range*, which define spectral regions-of-interest. (It's debatable \ as to whether those parameters should be in Frequency or Spectrum classes) """ # Configurations config = {} config['nd_axis'] = -1 config['nd_fcn'] = _np.mean def __init__(self, data=None, freq=None, label=None, units=None, meta=None): self._data = None self._freq = _Frequency() self._label = None self._units = None self._meta = {} if data is not None: self.data = _copy.deepcopy(data) if freq is not None: self.freq = _copy.deepcopy(freq) else: self.freq = _Frequency() if label is not None: self.label = _copy.deepcopy(label) if units is not None: self.units = _copy.deepcopy(units) if meta is not None: self._meta = _copy.deepcopy(meta) @staticmethod def _mean_axes(ndim, axis): """ Parameters ---------- ndim : int Number of dimensions of input data (target is 1D spectrum) axis : int For ND data, axis is remaining axis Returns ------- Vector that describes what axes to operate (using a mean or similar method) with axis parameter """ if axis < 0: axis2 = ndim + axis else: axis2 = axis return tuple([n for n in range(ndim) if n != axis2]) @property def data(self): return self._data @data.setter def data(self, value): if not isinstance(value, _np.ndarray): raise TypeError('data must be of type ndarray') # If sub-range of operation is defined. Only perform action over op_range_pix if self.freq is not None and self.freq.op_list_pix is not None: if value.shape[self.config['nd_axis']] == self.freq.op_range_pix.size: temp = _np.zeros((self.freq.size), dtype=value.dtype) if value.ndim == 1: temp[self.freq.op_range_pix] = value else: print('Input data is {}-dim. Performing {}'.format(value.ndim, self.config['nd_fcn'].__name__)) nd_ax = self._mean_axes(value.ndim, axis=self.config['nd_axis']) temp[self.freq.op_range_pix] = self.config['nd_fcn'](value, axis=nd_ax) elif value.shape[self.config['nd_axis']] == self.freq.size: temp = _np.zeros((self.freq.size), dtype=value.dtype) if value.ndim == 1: temp[self.freq.op_range_pix] = value[self.freq.op_range_pix] else: print('Input data is {}-dim. Performing {}'.format(value.ndim, self.config['nd_fcn'].__name__)) nd_ax = self._mean_axes(value.ndim, axis=self.config['nd_axis']) temp[self.freq.op_range_pix] = self.config['nd_fcn'](value, axis=nd_ax)[self.freq.op_range_pix] else: raise TypeError('data is of an unrecognized shape: {}'.format(value.shape)) self._data = 1 * temp del temp else: if value.ndim == 1: self._data = value else: print('Input data is {}-dim. Performing {}'.format(value.ndim, self.config['nd_fcn'].__name__)) nd_ax = self._mean_axes(value.ndim, axis=self.config['nd_axis']) self._data = self.config['nd_fcn'](value, axis=nd_ax) @property def freq(self): return self._freq @freq.setter def freq(self, value): if isinstance(value, _Frequency): self._freq = value elif isinstance(value, _np.ndarray): self.freq = _Frequency(data=value) else: raise TypeError('freq must be of type crikit.data.Frequency') @property def f(self): """ Convenience attribute: return frequency vector within operating (op) \ range """ return self.freq.op_range_freq @property def f_full(self): """ Convenience attribute: return full frequency vector """ return self.freq.data @property def units(self): return self._units @units.setter def units(self, value): if isinstance(value, str): self._units = value else: raise TypeError('units must be of type str') @property def label(self): return self._label @label.setter def label(self, value): if isinstance(value, str): self._label = value else: raise TypeError('label must be of type str') @property def meta(self): temp_dict = self._meta.copy() if self.freq.calib is not None: try: calib_dict = {} calib_prefix = 'Calib.' calib_dict[calib_prefix + 'a_vec'] = self.freq.calib['a_vec'] calib_dict[calib_prefix + 'ctr_wl'] = self.freq.calib['ctr_wl'] calib_dict[calib_prefix + 'ctr_wl0'] = self.freq.calib['ctr_wl0'] calib_dict[calib_prefix + 'n_pix'] = self.freq.calib['n_pix'] calib_dict[calib_prefix + 'probe'] = self.freq.calib['probe'] try: # Doesn't really matter if we have the units calib_dict[calib_prefix + 'units'] = self.freq.calib['units'] except Exception: pass except Exception: print('Could not get calibration information') else: temp_dict.update(calib_dict) if self.freq.calib_orig is not None: try: calib_dict = {} calib_prefix = 'CalibOrig.' calib_dict[calib_prefix + 'a_vec'] = self.freq.calib_orig['a_vec'] calib_dict[calib_prefix + 'ctr_wl'] = self.freq.calib_orig['ctr_wl'] calib_dict[calib_prefix + 'ctr_wl0'] = self.freq.calib_orig['ctr_wl0'] calib_dict[calib_prefix + 'n_pix'] = self.freq.calib_orig['n_pix'] calib_dict[calib_prefix + 'probe'] = self.freq.calib_orig['probe'] try: # Doesn't really matter if we have the units calib_dict[calib_prefix + 'units'] = self.freq.calib_orig['units'] except Exception: pass except Exception: print('Could not get calibration information') else: temp_dict.update(calib_dict) # return self._meta return temp_dict @meta.setter def meta(self, value): if isinstance(value, dict): self._meta = value else: raise TypeError('meta must be of type dict') @property def f_pix(self): if self._data is not None: return self._data.shape[-1] @property def ndim(self): if self._data is None: return None elif isinstance(self._data, _np.ndarray): return self._data.ndim else: return len(self._data.shape) @property def shape(self): if self._data is None: return None else: return self._data.shape @property def size(self): if self._data is None: return None else: return self._data.size
[docs] def mean(self, extent=None, over_space=True): """ Return mean spectrum (or mean over extent [list with 2 elements]). If\ over_space is False, returns reps-number of mean spectra """ if self._data is None: return None ndim = len(self._data.shape) if ndim == 1: if isinstance(self._data, _np.ndarray): return self._data.mean() else: return _np.mean(self._data) if ndim > 1: if over_space is True: axes = tuple(_np.arange(ndim - 1)) else: axes = -1 if isinstance(self._data, _np.ndarray): if extent is None: return self._data.mean(axis=axes) else: return self._data[:, extent[0]:extent[1] + 1].mean(axis=axes) else: if extent is None: return _np.mean(self._data, axis=axes) else: return _np.mean(self._data[:, extent[0]:extent[1] + 1], axis=axes)
[docs] def std(self, extent=None, over_space=True): """ Return standard deviation (std) spectrum (or std over extent [list with 2 elements]). If over_space is False, reps (or reps x reps) number of std's. """ if self._data is None: return None ndim = len(self._data.shape) if ndim == 1: if isinstance(self._data, _np.ndarray): return self._data.std() else: return _np.std(self._data) if ndim > 1: if over_space is True: axes = tuple(_np.arange(ndim - 1)) else: axes = -1 if isinstance(self._data, _np.ndarray): if extent is None: return self._data.std(axis=axes) else: return self._data[:, extent[0]:extent[1] + 1].std(axis=axes) else: if extent is None: return _np.std(self._data, axis=axes) else: return _np.std(self._data[:, extent[0]:extent[1] + 1], axis=axes)
[docs] def subtract(self, spectrum, overwrite=True): """ Subtract spectrum from data """ if isinstance(spectrum, Spectrum): if overwrite: self.data -= spectrum.data return None else: return self.data - spectrum.data elif isinstance(spectrum, _np.ndarray): if overwrite: self.data -= spectrum return None else: return self.data - spectrum
def __sub__(self, spectrum): return self.subtract(spectrum, overwrite=False)
[docs]class Hsi(Spectrum): """ Hyperspectral imagery class Parameters ---------- data : 3D ndarray [y_pix, x_pix, f_pix] HSI image mask : 3D ndarray (int) [y_pix, x_pix, f_pix] 0,1 mask with 1 is a usable pixel and 0 is not freq : crikit.data.frequency.Frequency instance Frequency [wavelength, wavenumber] object (i.e., the independent \ variable) label : str Image label (i.e., a string describing what the image is) units : str Units of image (e.g., intensity) x_rep : crikit.data.replicate.Replicate instance, Not implemented yet x-axis spatial object y_rep : crikit.data.replicate.Replicate instance, Not implemented yet x-axis spatial object x : 1D ndarray x-axis spatial vector y : 1D ndarray y-axis spatial vector meta : dict Meta-data dictionary Attributes ---------- shape : tuple, read-only Shape of data size : int, read-only Size of data (i.e., total number of entries) extent : list, read-only Extent of image [xmin, xmax, ymin, ymax] Methods ------- mean : 1D ndarray Mean spectrum. If extent [a,b] is provided, calculate mean over that\ inclusive region. std : 1D ndarray Standard deviation of spectrum. If extent [a,b] is provided, calculate standard\ deviation over that inclusive region. subtract : 3D ndarray or None Subtract spectrum or object Notes ----- * freq object contains some useful parameters such as op_range_* and \ plot_range_*, which define spectral regions-of-interest. (It's debatable \ as to whether those parameters should be in Frequency or Spectrum classes) """ # Configurations config = {} config['nd_axis'] = -1 def __init__(self, data=None, freq=None, x=None, y=None, x_rep=None, y_rep=None, label=None, units=None, meta=None): super().__init__(data, freq, label, units, meta) self._x_rep = _Replicate() self._y_rep = _Replicate() self._mask = None self._x_rep = _Replicate(data=x) self._y_rep = _Replicate(data=y) if x is None and x_rep is not None: self.x_rep = _copy.deepcopy(x_rep) if y is None and y_rep is not None: self.y_rep = _copy.deepcopy(y_rep) @staticmethod def _mean_axes(*args, **kwargs): """ Inhereted from Spectrum """ raise NotImplementedError('Only applicable to Spectrum class.') @staticmethod def _reshape_axes(shape, spectral_axis): """ Parameters ---------- shape : tuple Input data shape spectral_axis : int Spectral axis Returns ------- Reshape vector """ ndim = len(shape) if ndim == 1: out = [1, 1, 1] out[spectral_axis] = shape[0] elif ndim == 2: # ! Super-wonky out = [1, shape[0], shape[1]] elif ndim == 3: out = shape elif ndim > 3: out = [-1, shape[-2], shape[-1]] else: raise ValueError('Shape error') return tuple(out) @property def extent(self): if (self.x is not None) & (self.y is not None): return [self.x.min(), self.x.max(), self.y.min(), self.y.max()] @property def mask(self): return self._mask @property def x_rep(self): return self._x_rep @x_rep.setter def x_rep(self, value): if isinstance(value, _Replicate): self._x_rep = value elif isinstance(value, _np.ndarray): self._x_rep.data = value @property def y_rep(self): return self._y_rep @property def x(self): return self._x_rep.data @x.setter def x(self, value): self._x_rep.data = value @property def y(self): return self._y_rep.data @y.setter def y(self, value): self._y_rep.data = value @y_rep.setter def y_rep(self, value): if isinstance(value, _Replicate): self._y_rep = value elif isinstance(value, _np.ndarray): self._y_rep.data = value @property def data(self): return self._data @data.setter def data(self, value): if not isinstance(value, _np.ndarray): raise TypeError('data must be of type ndarray, not {}'.format(type(value))) ax_rs = self._reshape_axes(value.shape, self.config['nd_axis']) # self._mask = _np.ones(tuple([n for n in range(3) if n != self.config['nd_axis']]), # dtype=int) if self.freq is None or self.freq.op_list_pix is None: self._data = value.reshape(ax_rs) else: if value.shape[self.config['nd_axis']] == self.freq.op_range_pix.size: temp = _np.zeros((self._data.shape), dtype=value.dtype) temp[:, :, self.freq.op_range_pix] = value.reshape(ax_rs) self._data = 1 * temp del temp elif value.shape[self.config['nd_axis']] == self._data.shape[self.config['nd_axis']]: temp = _np.zeros((self._data.shape), dtype=value.dtype) temp[..., self.freq.op_range_pix] = value.reshape(ax_rs)[..., self.freq.op_range_pix] self._data = 1 * temp del temp
[docs] def check(self): """ Check x, y, and freq to make sure the dimensions agree with data """ if self._data is None: print('Hsi check: data is None, not checking') else: if self._x_rep._data is None: self._x_rep._data = _np.arange(self.shape[1]) self._x_rep._label = 'X' self._x_rep._units = 'pix' print('Hsi check: setting x to pixels') elif self._x_rep._data.size != self._data.shape[1]: self._x_rep = _Replicate() self._x_rep._data = _np.arange(self.shape[1]) self._x_rep._label = 'X' self._x_rep._units = 'pix' print('Hsi check: setting x to pixels') if self._y_rep._data is None: self._y_rep._data = _np.arange(self.shape[0]) self._y_rep._label = 'Y' self._y_rep._units = 'pix' print('Hsi check: setting y to pixels') elif self._y_rep._data.size != self._data.shape[0]: self._y_rep = _Replicate() self._y_rep._data = _np.arange(self.shape[0]) self._y_rep._label = 'Y' self._y_rep._units = 'pix' print('Hsi check: setting y to pixels') if self.freq._data is None: self.freq._data = _np.arange(self.shape[-1]) self.freq._label = 'Frequency' self.freq._units = 'pix' print('Hsi check: setting freq to pixels') elif self.freq._data.size != self._data.shape[-1]: self.freq = _Frequency() self.freq._data = _np.arange(self.shape[-1]) print('Hsi check: setting freq to pixels') return None
[docs] def subtract(self, spectra, overwrite=True): """ Subtract spectrum from data """ # Order IS important if isinstance(spectra, Hsi): if overwrite: self.data -= spectra.data return None else: return self.data - spectra.data elif isinstance(spectra, Spectrum): if overwrite: self.data -= spectra.data[None, None, :] return None else: return self.data - spectra.data elif isinstance(spectra, _np.ndarray): if spectra.shape == self.data.shape: if overwrite: self.data -= spectra return None else: return self.data - spectra else: if overwrite: self.data -= spectra[None, None, :] return None else: return self.data - spectra[None, None, :]
[docs] def get_rand_spectra(self, num, pt_sz=1, quads=False, full=False): mlen, nlen, freqlen = self.data.shape if quads: num_spectra = num + 5 else: num_spectra = num if _np.iscomplexobj(self.data): dtype = complex else: dtype = float temp = _np.zeros((num_spectra, self.data.shape[-1]), dtype=dtype) quad_mid_row = int(_np.round(mlen / 2)) quad_mid_col = int(_np.round(nlen / 2)) center_row = (int(_np.round(mlen / 3)), int(_np.round(2 * mlen / 3))) center_col = (int(_np.round(nlen / 3)), int(_np.round(2 * nlen / 3))) start_count = 0 if quads: # QUADS # Bottom-left temp[0, :] = _np.mean(self.data[0:quad_mid_row, 0:quad_mid_col, :], axis=(0, 1)) # Upper-left temp[1, :] = _np.mean(self.data[0:quad_mid_row, quad_mid_col + 1::, :], axis=(0, 1)) # Upper-right temp[2, :] = _np.mean(self.data[quad_mid_row + 1::, quad_mid_col + 1::, :], axis=(0, 1)) # Bottom-right temp[3, :] = _np.mean(self.data[quad_mid_row + 1::, 0:quad_mid_col, :], axis=(0, 1)) # Center temp[4, :] = _np.mean(self.data[center_row[0]:center_row[1], center_col[0]:center_col[1], :], axis=(0, 1)) start_count += 5 else: pass rand_rows = ((mlen - pt_sz - 1) * _np.random.rand(num_spectra)).astype(int) rand_cols = ((nlen - pt_sz - 1) * _np.random.rand(num_spectra)).astype(int) for count in _np.arange(start_count, num_spectra): if pt_sz == 1: temp[count, :] = _np.squeeze(self.data[rand_rows[count - start_count], rand_cols[count - start_count]]) else: rows = [rand_rows[count - start_count] - (pt_sz - 1), rand_rows[count - start_count] + pt_sz] cols = [rand_cols[count - start_count] - (pt_sz - 1), rand_cols[count - start_count] + pt_sz] if rows[0] < 0: rows[0] = 0 if rows[1] >= mlen: rows[1] = mlen - 1 if cols[0] < 0: cols[0] = 0 if cols[1] >= nlen: cols[1] = nlen - 1 if cols[0] == cols[1] or rows[0] == rows[1]: pass else: temp[count, :] = _np.squeeze(_np.mean(self.data[rows[0]:rows[1], cols[0]:cols[1], :], axis=(0, 1))) if (not full) and (self.freq.data is not None): temp = temp[..., self.freq.op_range_pix] return temp
def __sub__(self, spectrum): return self.subtract(spectrum, overwrite=False)
[docs]class Spectra(Spectrum): """ Spectra class Attributes ---------- data : 2D ndarray [n_pix, f_pix] Spectra. Note: input can be a ndarray of any dimension: it will be \ CONVERTED to [n_pix, f_pix] shape, assuming that shape[-1] is the f_pix \ long. freq : crikit.data.frequency.Frequency instance Frequency [wavelength, wavenumber] object (i.e., the independent \ variable) label : str Spectrum label (i.e., a string describing what the spectrum is) units : str Units of spectrum reps : crikit.data.replicate.Replicate instance, Not implemented yet Object describing the meaning of multiple spectra (i.e., the physical \ meaning of n_pix). meta : dict Meta-data dictionary shape : tuple, read-only Shape of data n_pix : int, read-only Size of data's replicate/spectral number axis. Methods ------- mean : 1D ndarray Mean spectrum. If extent [a,b] is provided, calculate mean over that\ inclusive region. std : 1D ndarray Standard deviation of spectrum. If extent [a,b] is provided, calculate standard\ deviation over that inclusive region. subtract : 2D ndarray or None Subtract spectrum or object Notes ----- * freq object contains some useful parameters such as op_range* and \ plot_range*, which define spectral regions-of-interest. (It's debatable \ as to whether those parameters should be in Frequency or Spectrum classes) """ # Configurations config = {} config['nd_axis'] = -1 def __init__(self, data=None, freq=None, label=None, units=None, meta=None): super().__init__(data, freq, label, units, meta) self._reps = _Replicate() @staticmethod def _mean_axes(*args, **kwargs): """ Inhereted from Spectrum """ raise NotImplementedError('Only applicable to Spectrum class.') @staticmethod def _reshape_axes(shape, spectral_axis): """ Parameters ---------- shape : tuple Input data shape spectral_axis : int Spectral axis Returns ------- Reshape vector """ ndim = len(shape) if ndim >= 2: out = [-1, -1] else: out = [1, 1] out[spectral_axis] = shape[spectral_axis] return tuple(out) @property def data(self): return self._data @data.setter def data(self, value): if not isinstance(value, _np.ndarray): raise TypeError('data must be of type ndarray') if self.freq.data is None or self.freq.op_list_pix is None: if value.ndim != 2: print('Spectra: converting data input from {}D to 2D ndarray'.format(value.ndim)) ax_rs = self._reshape_axes(value.shape, spectral_axis=self.config['nd_axis']) self._data = value.reshape(ax_rs) else: if value.ndim != 2: print('Spectra: converting data input from {}D to 2D ndarray'.format(value.ndim)) if value.shape[-1] == self.freq.op_range_pix.size: temp = _np.zeros((self._data.shape), dtype=value.dtype) temp[:, self.freq.op_range_pix] = value self._data = temp elif value.shape[-1] == self.freq.size: temp = _np.zeros((self._data.shape), dtype=value.dtype) temp[..., self.freq.op_range_pix] = value[..., self.freq.op_range_pix].reshape((-1, len(self.freq.op_range_pix))) self._data = temp else: raise TypeError('data is of an unrecognized shape: {}'.format(value.shape)) @property def n_pix(self): return self._data.shape[0] @property def reps(self): return self._reps @reps.setter def reps(self, value): if isinstance(value, _Replicate): self._reps = value elif isinstance(value, _np.ndarray): self._reps.data = value
[docs] def subtract(self, spectra, overwrite=True): """ Subtract spectrum from data """ # Order IS important if isinstance(spectra, Spectra): if overwrite: self.data -= spectra.data return None else: return self.data - spectra.data elif isinstance(spectra, Spectrum): if overwrite: self.data -= spectra.data[None, :] return None else: return self.data - spectra.data[None, :] elif isinstance(spectra, _np.ndarray): if spectra.shape == self.data.shape: if overwrite: self.data -= spectra return None else: return self.data - spectra else: if overwrite: self.data -= spectra[None, :] return None else: return self.data - spectra[None, :]
def __sub__(self, spectrum): return self.subtract(spectrum, overwrite=False)
if __name__ == '__main__': # pragma: no cover sp = Spectra() print(sp.__dict__) print('Subclass? : {}'.format(issubclass(Spectra, Spectrum))) print('Instance of Spectra? : {}'.format(isinstance(sp, Spectra))) print('Instance of Spectrum? : {}'.format(isinstance(sp, Spectrum))) print('Type(sp) == Spectrum? : {}'.format(type(sp) == Spectrum)) print('Type(sp) == Spectra? : {}'.format(type(sp) == Spectra))