def __init__(self, camera, parent=None, fcn=None): # Initialize PrepareData : PrepareData.__init__(self, axis=0) # Keep camera : self._camera = camera self._rect = (0., 0., 0., 0.) self._fcn = fcn # Time-frequency map self.tf = TFmapsMesh(parent=parent) # Spectrogram self.mesh = scene.visuals.Image(np.zeros((2, 2)), parent=parent, name='Fourier transform') self.mesh.transform = vist.STTransform()
def __init__(self, time, sf, sh, axis, form='line', line_rendering='gl', parent=None): """Init.""" self.form = form self._time = time self._sf = sf self._axis = axis self._index = 0 # selected index of the 3-d array self.rect = (0., 0., 1., 1.) self._prep = PrepareData(way='filtfilt') # Build navigation index : if len(sh) in [2, 3]: sh = list(sh) del sh[axis] self._navidx = list(product(*tuple(range(k) for k in sh))) else: self._navidx = [[]] # Create visuals : pos = np.random.rand(2, 2) posh = np.random.rand(2, ) self._th = scene.visuals.Line(pos=pos, name='threshold', parent=parent) self._line = scene.visuals.Line(pos=pos, name='line', parent=parent, method=line_rendering) self._mark = scene.visuals.Markers(pos=pos, name='marker', parent=parent) self._hist = scene.visuals.Histogram(data=posh, orientation='h', parent=parent, name='histogram') self._tf = TFmapsMesh(parent=parent) self._th.visible = False # Initialize annotations : SignalAnnotations.__init__(self, parent)
class SignalVisual(SignalAnnotations): """1-D signal class creation. Parameters ---------- time : array_like Time vector of shape (N,) sf : float The sampling frequency. sh : tuple Shape of the 2-D array. form : {'line', 'marker', 'histogram'} Plotting type. color : array_like/string/tuple | 'black' Color of the plot. lw : float | 2. Line width (form='line'). symbol : string | 'o' Marker symbol (form='marker'). size : float | 10. Marker size (form='marker'). nbins : int | 10 Number of bins for the histogram (form='histogram') line_rendering : {'gl', 'agg'} Specify the line rendering method. Use 'gl' for a fast but lower quality lines and 'agg', looks better but slower. parent : VisPy.parent | None Parent of the mesh. """ def __init__(self, time, sf, sh, axis, form='line', line_rendering='gl', parent=None): """Init.""" self.form = form self._time = time self._sf = sf self._axis = axis self._index = 0 # selected index of the 3-d array self.rect = (0., 0., 1., 1.) self._prep = PrepareData(way='filtfilt') # Build navigation index : if len(sh) in [2, 3]: sh = list(sh) del sh[axis] self._navidx = list(product(*tuple(range(k) for k in sh))) else: self._navidx = [[]] # Create visuals : pos = np.random.rand(2, 2) posh = np.random.rand(2, ) self._th = scene.visuals.Line(pos=pos, name='threshold', parent=parent) self._line = scene.visuals.Line(pos=pos, name='line', parent=parent, method=line_rendering) self._mark = scene.visuals.Markers(pos=pos, name='marker', parent=parent) self._hist = scene.visuals.Histogram(data=posh, orientation='h', parent=parent, name='histogram') self._tf = TFmapsMesh(parent=parent) self._th.visible = False # Initialize annotations : SignalAnnotations.__init__(self, parent) def __str__(self): """String representation of the currently selected index.""" lst = [str(k) for k in self._navidx[self._index]] lst.insert(self._axis, ':') return '(' + ', '.join(lst) + ')' def set_data(self, data, index, color='black', lw=2., nbins=10, symbol='disc', size=10., form='line', th=None, norm=None, window=None, overlap=0., baseline=None, clim=None, cmap='viridis', interpolation='gaussian', nperseg=256, noverlap=128): """Set data to the plot. Parameters ---------- data : array_like Raw data vector of shape (N,) index : int | 0 Index of the 3-d array. color : array_like/string/tuple | None Color of the plot. lw : float | None Line width (form='line'). symbol : string | None Marker symbol (form='marker'). size : float | None Marker size (form='marker'). nbins : int | None Number of bins for the histogram (form='histogram') form : {'line', 'marker', 'histogram', 'tf'} Plotting type. th : tuple | None Tuple of floats for line thresholding. norm : int | None Normalization method for (form='tf'). window : tuple | None Averaging window (form='tf'). overlap : float | 0. Overlap between successive windows (form='tf'). baseline : tuple | None Baseline period for the normalization (form='tf'). """ # Update variable : self.form = form self._index = index color = color2vb(color) # Get data index : if data.ndim == 1: idx = slice(None) elif data.ndim in [2, 3]: idx = list(self._navidx[index]) idx.insert(self._axis, slice(None)) idx = tuple(idx) # Convert data to be compatible with VisPy and prepare data : data_c = vispy_array(data[idx]).copy() _data = self._prep._prepare_data(self._sf, data_c, self._time) # Set data : if form in ['line', 'marker', 'psd', 'butterfly']: # line and marker # Get position array : pos = np.c_[self._time, _data] # Send position : if form in ['line', 'psd']: if form == 'psd': fmax = self._sf / 4. f, pxx = welch(_data, self._sf, nperseg=nperseg, noverlap=noverlap) f_sf4 = abs(f - fmax) f_1 = abs(f - 1.) fidx_sf4 = np.where(f_sf4 == f_sf4.min())[0][0] fidx_1 = np.where(f_1 == f_1.min())[0][0] pos = np.c_[f[fidx_1:-fidx_sf4], pxx[fidx_1:-fidx_sf4]] # Threshold : is_th = isinstance(th, (tuple, list, np.ndarray)) col = color2vb(color, length=pos.shape[0]) if is_th: # Build threshold segments : t_min, t_max = self._time.min(), self._time.max() pos_th = np.vstack( ([t_min, th[0]], [t_max, th[0]], [t_min, th[1]], [t_max, th[1]])) self._th.set_data(pos_th, connect='segments', color=color2vb('#ab4642')) # Build line color : col = color2vb(color, length=len(_data)) cond = np.logical_or(_data < th[0], _data > th[1]) col[cond, :] = color2vb('#ab4642') self._th.visible = is_th self._line.set_data(pos, width=lw, color=col) self._line.update() elif form == 'marker': self._mark.set_data(pos, face_color=color, symbol=symbol, size=size, edge_width=0.) self._mark.update() elif form == 'butterfly': # Get soe shape related variables : n, m = len(self._time), int(np.prod(data.shape)) n_rep = int(m / n) data = vispy_array(data) # Build position : pos = np.c_[np.tile(self._time.ravel(), n_rep), data.ravel()] # Disconnect some points : connect = np.c_[np.arange(m - 1), np.arange(1, m)] to_delete = np.linspace(n - 1, m - 1, n_rep) connect = np.delete(connect, to_delete, axis=0) # Build color : col = color2vb(color, length=m) # singcol = np.random.uniform(size=(n_rep, 3), low=.2, # high=.8).astype(np.float32) # col = np.repeat(singcol, n, 0) # Send data : self._line.set_data(pos, width=lw, color=col, connect=connect) self._line.update() # Get camera rectangle : t_min, t_max = pos[:, 0].min(), pos[:, 0].max() d_min, d_max = pos[:, 1].min(), pos[:, 1].max() off = .05 * (d_max - d_min) self.rect = (t_min, d_min - off, t_max - t_min, d_max - d_min + 2 * off) elif form == 'histogram': # histogram # Compute the mesh : mesh = scene.visuals.Histogram(_data, nbins) # Get the vertices and faces of the mesh : vert = mesh.mesh_data.get_vertices() faces = mesh.mesh_data.get_faces() # Pass vertices and faces to the histogram : self._hist.set_data(vert, faces, color=color) # Compute the histogram : raw, xvec = np.histogram(_data, nbins) # Get camera rectangle : t_min, t_max = xvec.min(), xvec.max() d_min, d_max = 0.9 * raw[np.nonzero(raw)].min(), 1.01 * raw.max() self.rect = (t_min, d_min, t_max - t_min, d_max - d_min) # Update object : self._hist.update() elif form == 'tf': # time-frequency map self._tf.set_data(_data, self._sf, cmap=cmap, contrast=.5, norm=norm, baseline=baseline, n_window=window, overlap=overlap, window='hanning', clim=clim) self._tf.interpolation = interpolation self.rect = self._tf.rect # Hide non form elements : self._visibility() # Update annotations : self.update_annotations(str(self)) def update_annotations(self, name): """Update annotations.""" is_annotated = self.is_event_annotated(name) if is_annotated: c = self.get_event_coord(name) z = np.full((c.shape[0], ), -10.) c = np.c_[c, z].astype(np.float32) msize = self._annot_mark._data['a_size'][0] mcolor = self._annot_mark._data['a_bg_color'][0, :] self._annot_mark.set_data(c, face_color=mcolor, edge_width=0., size=msize) self._annot_mark.visible = is_annotated self._annot_text.visible = is_annotated def clean_annotations(self): """Clean annotations.""" self._annot_mark.set_data(pos=np.random.rand(2, 2)) self._annotations = {} self._annotations_txt = {} def _get_signal_index(self, signal): """Get index of a signal. This method turn a '(k, n, :)' string into an index used by the instance _navidx. """ # Process signal : signal = signal.replace(', :', '').replace(':, ', '')[1:-1] # Find index : idx = tuple(int(k) for k in signal.split(', ')) return self._navidx.index(idx) def _visibility(self): self._line.visible = self.form in ['line', 'psd', 'butterfly'] self._mark.visible = self.form == 'marker' self._hist.visible = self.form == 'histogram' self._tf.visible = self.form == 'tf'
class Spectrogram(PrepareData): """Create and manage a Spectrogram object. After object creation, use the set_data() method to pass new data, new color, new frequency / time range, new settings... """ def __init__(self, camera, parent=None, fcn=None): # Initialize PrepareData : PrepareData.__init__(self, axis=0) # Keep camera : self._camera = camera self._rect = (0., 0., 0., 0.) self._fcn = fcn # Time-frequency map self.tf = TFmapsMesh(parent=parent) # Spectrogram self.mesh = scene.visuals.Image(np.zeros((2, 2)), parent=parent, name='Fourier transform') self.mesh.transform = vist.STTransform() def set_data(self, sf, data, time, method='Fourier transform', cmap='rainbow', nfft=30., overlap=0., fstart=.5, fend=20., contrast=.5, interp='nearest', norm=0): """Set data to the spectrogram. Use this method to change data, colormap, spectrogram settings, the starting and ending frequencies. Parameters ---------- sf: float The sampling frequency. data: array_like The data to use for the spectrogram. Must be a row vector. time: array_like The time vector. method: string | 'Fourier transform' Computation method. cmap : string | 'viridis' The matplotlib colormap to use. nfft : float | 30. Number of fft points for the spectrogram (in seconds). overlap : float | .5 Ovelap proprotion (0 <= overlap <1). fstart : float | .5 Frequency from which the spectrogram have to start. fend : float | 20. Frequency from which the spectrogram have to finish. contrast : float | .5 Contrast of the colormap. interp : string | 'nearest' Interpolation method. norm : int | 0 Normalization method for TF. """ # =================== PREPARE DATA =================== # Prepare data (only if needed) if self: data = self._prepare_data(sf, data.copy(), time) nperseg = int(round(nfft * sf)) # =================== TF // SPECTRO =================== if method == 'Wavelet': self.tf.set_data(data, sf, f_min=fstart, f_max=fend, cmap=cmap, contrast=contrast, n_window=nperseg, overlap=overlap, window='hamming', norm=norm) self.tf._image.interpolation = interp self.rect = self.tf.rect self.freq = self.tf.freqs else: # =================== CONVERSION =================== overlap = int(round(overlap * nperseg)) if method == 'Multitaper': from lspopt import spectrogram_lspopt freq, _, mesh = spectrogram_lspopt(data, fs=sf, nperseg=nperseg, c_parameter=20, noverlap=overlap) elif method == 'Fourier transform': freq, _, mesh = scpsig.spectrogram(data, fs=sf, nperseg=nperseg, noverlap=overlap, window='hamming') mesh = 20 * np.log10(mesh) # =================== FREQUENCY SELECTION =================== # Find where freq is [fstart, fend] : f = [0., 0.] f[0] = np.abs(freq - fstart).argmin() if fstart else 0 f[1] = np.abs(freq - fend).argmin() if fend else len(freq) # Build slicing and select frequency vector : sls = slice(f[0], f[1] + 1) freq = freq[sls] self._fstart, self._fend = freq[0], freq[-1] # =================== COLOR =================== # Get clim : _mesh = mesh[sls, :] contrast = 1. if contrast is None else contrast clim = (contrast * _mesh.min(), contrast * _mesh.max()) # Turn mesh into color array for selected frequencies: self.mesh.set_data(_mesh) _min, _max = _mesh.min(), _mesh.max() _cmap = cmap_to_glsl(limits=(_min, _max), clim=clim, cmap=cmap) self.mesh.cmap = _cmap self.mesh.clim = 'auto' self.mesh.interpolation = interp # =================== TRANSFORM =================== tm, th = time.min(), time.max() # Re-scale the mesh for fitting in time / frequency : fact = (freq.max() - freq.min()) / len(freq) sc = (th / mesh.shape[1], fact, 1) tr = [0., freq.min(), 0.] self.mesh.transform.translate = tr self.mesh.transform.scale = sc # Update object : self.mesh.update() # Get camera rectangle : self.rect = (tm, freq.min(), th - tm, freq.max() - freq.min()) self.freq = freq # Visibility : self.mesh.visible = 0 if method == 'Wavelet' else 1 self.tf.visible = 1 if method == 'Wavelet' else 0 def clean(self): """Clean indicators.""" pos = np.zeros((3, 4), dtype=np.float32) self.mesh.set_data(pos) self.mesh.parent = None self.mesh = None # ----------- RECT ----------- @property def rect(self): """Get the rect value.""" return self._rect @rect.setter def rect(self, value): """Set rect value.""" self._rect = value self._camera.rect = value # ----------- INTERP ----------- @property def interp(self): """Get the interp value.""" return self._interp @interp.setter def interp(self, value): """Set interp value.""" self._interp = value self.mesh.interpolation = value self.mesh.update() self.tf.interpolation = value self.tf.update()
class Spectrogram(PrepareData): """Create and manage a Spectrogram object. After object creation, use the set_data() method to pass new data, new color, new frequency / time range, new settings... """ def __init__(self, camera, parent=None, fcn=None): # Initialize PrepareData : PrepareData.__init__(self, axis=0) # Keep camera : self._camera = camera self._rect = (0., 0., 0., 0.) self._fcn = fcn # Time-frequency map self.tf = TFmapsMesh(parent=parent) # Spectrogram self.mesh = scene.visuals.Image(np.zeros((2, 2)), parent=parent, name='Fourier transform') self.mesh.transform = vist.STTransform() def set_data(self, sf, data, time, method='Fourier transform', cmap='rainbow', nfft=30., overlap=0., fstart=.5, fend=20., contrast=.5, interp='nearest', norm=0): """Set data to the spectrogram. Use this method to change data, colormap, spectrogram settings, the starting and ending frequencies. Parameters ---------- sf: float The sampling frequency. data: array_like The data to use for the spectrogram. Must be a row vector. time: array_like The time vector. method: string | 'Fourier transform' Computation method. cmap : string | 'viridis' The matplotlib colormap to use. nfft : float | 30. Number of fft points for the spectrogram (in seconds). overlap : float | .5 Ovelap proprotion (0 <= overlap <1). fstart : float | .5 Frequency from which the spectrogram have to start. fend : float | 20. Frequency from which the spectrogram have to finish. contrast : float | .5 Contrast of the colormap. interp : string | 'nearest' Interpolation method. norm : int | 0 Normalization method for TF. """ # =================== PREPARE DATA =================== # Prepare data (only if needed) if self: data = self._prepare_data(sf, data.copy(), time) nperseg = int(round(nfft * sf)) # =================== TF // SPECTRO =================== if method == 'Wavelet': self.tf.set_data(data, sf, f_min=fstart, f_max=fend, cmap=cmap, contrast=contrast, n_window=nperseg, overlap=overlap, window='hamming', norm=norm) self.tf._image.interpolation = interp self.rect = self.tf.rect self.freq = self.tf.freqs else: # =================== CONVERSION =================== overlap = int(round(overlap * nperseg)) if method == 'Multitaper': from lspopt import spectrogram_lspopt freq, _, mesh = spectrogram_lspopt(data, fs=sf, nperseg=nperseg, c_parameter=20, noverlap=overlap) elif method == 'Fourier transform': freq, _, mesh = scpsig.spectrogram(data, fs=sf, nperseg=nperseg, noverlap=overlap, window='hamming') mesh = 20 * np.log10(mesh) # =================== FREQUENCY SELECTION =================== # Find where freq is [fstart, fend] : f = [0., 0.] f[0] = np.abs(freq - fstart).argmin() if fstart else 0 f[1] = np.abs(freq - fend).argmin() if fend else len(freq) # Build slicing and select frequency vector : sls = slice(f[0], f[1] + 1) freq = freq[sls] self._fstart, self._fend = freq[0], freq[-1] # =================== COLOR =================== # Get clim : _mesh = mesh[sls, :] is_finite = np.isfinite(_mesh) _mesh[~is_finite] = np.percentile(_mesh[is_finite], 5) contrast = 1. if contrast is None else contrast clim = (contrast * _mesh.min(), contrast * _mesh.max()) # Turn mesh into color array for selected frequencies: self.mesh.set_data(_mesh) _min, _max = _mesh.min(), _mesh.max() _cmap = cmap_to_glsl(limits=(_min, _max), clim=clim, cmap=cmap) self.mesh.cmap = _cmap self.mesh.clim = 'auto' self.mesh.interpolation = interp # =================== TRANSFORM =================== tm, th = time.min(), time.max() # Re-scale the mesh for fitting in time / frequency : fact = (freq.max() - freq.min()) / len(freq) sc = (th / mesh.shape[1], fact, 1) tr = [0., freq.min(), 0.] self.mesh.transform.translate = tr self.mesh.transform.scale = sc # Update object : self.mesh.update() # Get camera rectangle : self.rect = (tm, freq.min(), th - tm, freq.max() - freq.min()) self.freq = freq # Visibility : self.mesh.visible = 0 if method == 'Wavelet' else 1 self.tf.visible = 1 if method == 'Wavelet' else 0 def clean(self): """Clean indicators.""" pos = np.zeros((3, 4), dtype=np.float32) self.mesh.set_data(pos) self.mesh.parent = None self.mesh = None # ----------- RECT ----------- @property def rect(self): """Get the rect value.""" return self._rect @rect.setter def rect(self, value): """Set rect value.""" self._rect = value self._camera.rect = value # ----------- INTERP ----------- @property def interp(self): """Get the interp value.""" return self._interp @interp.setter def interp(self, value): """Set interp value.""" self._interp = value self.mesh.interpolation = value self.mesh.update() self.tf.interpolation = value self.tf.update()