# -*- coding: utf-8 -*-
"""Bunch of functions which help in visualising data
and results
There is a common pattern in the naming of viewing functions.
#. functions starting with 'visualise' include an overlay of
a particular output attribute on top of or with the
the original signal. For example `visualise_sound`
#. functions starting with 'plot' are bare bones
plots with just the attribute on the y and time on the x.
"""
import matplotlib.pyplot as plt
import numpy as np
from itsfm.signal_processing import get_peak_frequency
from itsfm.signal_processing import moving_rms_edge_robust, dB
from itsfm.frequency_tracking import accelaration, speed
make_x_time = lambda X, fs: np.linspace(0, X.size/float(fs), X.size)
[docs]class itsFMInspector:
'''
Handles the output from measure_and_segment calls, and allows plotting
of the outputs.
Parameters
----------
segmeasure_out : tuple
Tuple object containing three other objects which are the output from segment_and_measure_call
1. segmentation_output : tuple
Tuple with the `cf` boolean array, `fm` boolean array and `info` dictioanry
2. audio_parts : dictionary
Dictionary with call part labels and values as selected audio parts as np.arrays
3. measurements : pd.DataFrame
A wide-formate dataframe with one row referring to meaurements done on one call part
eg. if a call has 3 parts (fm1, cf1, fm2), then there will be three columns and
N columns, if N measurements have been done.
whole_audio : np.array
The audio that was analysed.
fs : float>0
Sampling rate in Hz.
Notes
----
* Not all `visualise` methods may be supported. It depends on the segmentation method at hand.
* All `visualise` methods return one/multiple subplots that could be used and embellished further
for your own custom laying over.
'''
def __init__(self, segmeasure_out, whole_audio, fs, **kwargs):
self.seg_details, self.audio_parts, self.measurements = segmeasure_out
self.whole_audio = whole_audio
self.fs = fs
self.kwargs = kwargs
self.cf, self.fm, self.info = self.seg_details
def visualise_audio(self):
w, s = visualise_sound(self.whole_audio, self.fs, **self.kwargs)
return w,s
[docs] def visualise_fmrate(self):
'''
Plots the spectrogram + FM rate profile in a 2 row plot
'''
try:
self.fmrate = self.info['fmrate']
plt.figure()
a = plt.subplot(311)
make_waveform(self.fmrate, self.fs)
plt.ylabel('FM rate, kHz/ms')
b = plt.subplot(312,sharex=a)
make_specgram(self.whole_audio, self.fs, **self.kwargs)
b.set_ylabel('Frequency, Hz', labelpad=-1.5)
c = plt.subplot(313,sharex=a)
make_waveform(self.whole_audio, self.fs)
return a, b, c
except:
raise AttributeError('Cannot make fmrate plot. Check if variable found in the output!')
[docs] def visualise_accelaration(self):
'''
Plots the spectrogram + accelaration of the
frequency profile
in a 2 row plot
'''
try:
self.acc_profile = self.info['acc_profile']
plt.figure()
a = plt.subplot(311)
make_waveform(self.acc_profile, self.fs)
plt.ylabel('Accelaration, $kHz/ms^{2}$')
b = plt.subplot(312, sharex=a)
make_specgram(self.whole_audio, self.fs, **self.kwargs)
c = plt.subplot(313, sharex=a)
make_waveform(self.whole_audio, self.fs)
return a, b, c
except:
raise AttributeError('Cannot make accelaration profile plot')
[docs] def visualise_cffm_segmentation(self):
'''
'''
w,s = visualise_cffm_segmentation(self.cf, self.fm,
self.whole_audio, self.fs,
**self.kwargs)
return w,s
[docs] def visualise_frequency_profiles(self, fp_type='all'):
'''
Visualises either one or all of the frequency profiles that are present in the
info dictionary.
The function relies on picking up all keys in the info dictionary that end with '<>_fp'
pattern.
Parameters
----------
fp_type : str/list with str's
Needs to correspond to a key found in the info dictionary
'''
if fp_type=='all':
all_fps = self._get_fp_keys(self.info)
elif isinstance(fp_type, str):
all_fps = [fp_type]
plt.figure()
a = plt.subplot(211)
make_specgram(self.whole_audio, self.fs, **self.kwargs);
time_axis = make_x_time(self.whole_audio, self.fs)
for each_fp in all_fps:
plt.plot(time_axis, self.info[each_fp], label=each_fp)
plt.legend()
b = plt.subplot(212, sharex=a)
make_waveform(self.whole_audio, self.fs,)
return a,b
[docs] def visualise_pkpctage_profiles(self):
'''
'''
plt.figure()
a = plt.subplot(211)
make_specgram(self.whole_audio, self.fs, **self.kwargs);
a.set_ylabel('Frequency, Hz', labelpad=-1.5)
b = plt.subplot(212, sharex=a)
plot_dbrms_cffmprofiles(self.seg_details,self.fs)
return a,b
[docs] def visualise_geq_signallevel(self):
'''
Some tracking/segmentation methods rely on using only
regions that are above a threshold, the `signal_level`
. A moving dB rms window is pass
ed, and only regions above it are
'''
time_axis = make_x_time(self.whole_audio, self.fs)
above_siglevel = np.zeros(self.whole_audio.size)
plt.figure()
a = plt.subplot(211)
s = make_specgram(self.whole_audio, self.fs, **self.kwargs);
ymin, ymax = a.get_ylim()
for each in self.info['geq_signal_level']:
above_siglevel[each] = 1
plt.plot(time_axis, above_siglevel*ymax*0.5,
label='$\geq$ signal level',
color='C1')
plt.legend()
b = plt.subplot(212, sharex=a)
make_waveform(self.whole_audio, self.fs)
wave_max = np.max(np.abs(self.whole_audio))
plt.plot(time_axis, above_siglevel*wave_max, label='$\geq$ signal level')
return a, b
def _get_fp_keys(self, info_dictionary):
fp_keys = list(filter(lambda x : '_fp' in x ,info_dictionary.keys()))
if len(fp_keys)==0:
raise ValueError("There's no frequency profile (fp) in the output info. Check the output object or method")
return fp_keys
[docs]def check_call_background_segmentation(whole_call, fs, main_call_mask,
**kwargs):
'''Visualises the main call selection
Parameters
----------
whole_call : np.array
Call audio
fs : float>0
Sampling rate in Hz
main_call_mask : np.array
Boolean array where True indicates the sample
is part of the main call, and False that it is not.
Returns
-------
waveform, spec : pyplot.subplots
Notes
-----
The appearance of the two subplots can be further changes by varying the
keyword arguments. For available keyword arguments see the visualise_sound
function.
'''
peak_freq, _ = get_peak_frequency(whole_call, fs)
horizontal_line = peak_freq*1.1
waveform, spec = visualise_sound(whole_call, fs, **kwargs)
waveform.plot(make_x_time(main_call_mask, fs),
main_call_mask*np.max(whole_call),'k')
waveform.plot(make_x_time(main_call_mask, fs),
main_call_mask*np.min(whole_call),'k')
spec.plot(make_x_time(main_call_mask, fs),main_call_mask*horizontal_line,'k')
return waveform, spec
[docs]def show_all_call_parts(only_call, call_parts, fs, **kwargs):
'''
Parameters
----------
only_call : np.array
call_parts : dictionary
Dictionary with keys 'cf' and 'fm'
The entry for 'cf' should only have one audio segment.
The entry for 'fm' can have multiple audio segments.
fs : float>0
Sampling rate in Hz.
Returns
-------
None
Notes
-----
For further keyword arguments to customise the spectrograms
see documentation for make_specgram
This function does not return any output, it only produces a
figure with subplots.
'''
plt.figure(figsize=(6,8))
plt.subplot(421)
make_specgram(only_call, fs, **kwargs);
plt.subplot(423)
make_specgram(call_parts['cf'], fs, **kwargs);
plt.subplot(422);make_waveform(only_call, fs)
plt.subplot(424);make_waveform(call_parts['cf'], fs)
for i,each in enumerate(call_parts['fm']):
try:
plt.subplot(420+i*2+6);make_waveform(each, fs)
plt.subplot(420+i*2+5);make_specgram(each, fs, **kwargs);
except:
pass
def visualise_cffm_segmentation(cf,fm,X,fs, **kwargs):
w,s = visualise_sound(X,fs, **kwargs)
w.plot(make_x_time(cf, fs), cf*np.max(np.abs(X)),'k')
w.plot(make_x_time(fm, fs), fm*np.max(np.abs(X)), 'r')
s.plot(make_x_time(cf, fs), cf*fs*0.5, 'k',label='CF')
s.plot(make_x_time(fm, fs), fm*fs*0.5, 'r',label='FM')
plt.legend()
return w,s
[docs]def visualise_fmrate_profile(X, freq_profile, fs):
'''
'''
def plot_fmrate_profile(X,fs):
speed_profile = speed(X,fs)
t = np.linspace(0,X.size/fs, X.size)
plt.plot(t, speed_profile)
plt.ylabel('Frequency modulation rate, $\\frac{kHz}{ms}$')
plt.xlabel('Time, s')
[docs]def plot_accelaration_profile(X,fs):
'''
Plots the frequency acclearation profile of a frequency
profile
Parameters
----------
X : np.array
The frequency profile with sample-level
estimates of frequency in Hz.
fs : float>0
Returns
-------
A plt.plot which can be used as an independent figure ot
a subplot.
'''
acc_profile = accelaration(X,fs)
t = np.linspace(0,X.size/fs, X.size)
plt.figure()
A = plt.subplot(111)
plt.plot(t, acc_profile)
plt.ylabel('Frequency accelaration, $\\frac{kHz}{ms^{2}}$')
plt.xlabel('Time, s')
return A
[docs]def plot_movingdbrms(X,fs,**kwargs):
'''
'''
m_dbrms = dB(moving_rms_edge_robust(X, **kwargs))
plt.plot(make_x_time(m_dbrms, fs), m_dbrms)
[docs]def visualise_sound(audio, fs, **kwargs):
'''
Parameters
----------
audio
fs
fft_size : integer>0, optional
Returns
-------
a0, a1 : subplots
'''
plt.figure()
a0 = plt.subplot(211)
make_waveform(audio, fs)
a1 = plt.subplot(212, sharex=a0)
make_specgram(audio, fs, **kwargs)
plt.tight_layout()
return a0, a1
[docs]def make_specgram(audio, fs, **kwargs):
'''
'''
fft_size = get_fftsize(fs, **kwargs)
n_overlap = fft_size-1
cmap = kwargs.get('cmap', 'viridis')
vmin = kwargs.get('vmin', -100)
specgram = plt.specgram(audio, Fs=fs,
NFFT=fft_size,
noverlap=n_overlap,
vmin=vmin,
cmap=cmap);
plt.ylabel('Frequency, Hz')
plt.xlabel('Time, s')
return specgram
def make_waveform(audio, fs):
plt.plot(make_x_time(audio,fs), audio)
def time_plot(X, fs):
plt.plot(make_x_time(X,fs), X)
[docs]def get_fftsize(fs, **kwargs):
'''
'''
fft_size_given = not(kwargs.get('fft_size') is None)
freq_resolution_given = not(kwargs.get('freq_resolution') is None)
both_not_given = [False, False] == [fft_size_given, freq_resolution_given]
if freq_resolution_given:
window_size = calculate_window_size(kwargs.get('freq_resolution'), fs)
return window_size
elif fft_size_given:
return kwargs['fft_size']
elif both_not_given:
default_freq_resoln = 1000.0 # Hz
window_size = calculate_window_size(default_freq_resoln, fs)
return window_size
def calculate_window_size(freq_resoln, fs):
return int(fs/freq_resoln)
def plot_fm_measurements(call, fs, measures, subplot, **kwargs):
# check if there's upfm
for fm in ['upfm_','downfm_']:
try:
fm_start, fm_stop = measures[fm+'start'], measures[fm+'end']
subplot.vlines((fm_start, fm_stop),
0,fs*0.5, 'r')
subplot.hlines(measures[fm+'terminal_frequency'],
fm_start, fm_stop, 'b')
except:
pass
return subplot
def plot_cf_measurements(call, fs, measures, subplot, **kwargs):
peak_frequency = measures['peak_frequency']
subplot.hlines(peak_frequency,measures['cf_start'],
measures['cf_end'], 'b')
return subplot
[docs]def plot_dbrms_cffmprofiles(seg_details, fs):
'''
Makes a plot with CF anf FM dB rms profiles. This method only works for
peak-percentage based segmentation.
Parameters
----------
seg_details : tuple
Tuple with 3 entries. The third entry needs to be a dictionary with
at least the following keys : 'cf_re_fm' and 'fm_re_cf'
fs : float>0
Sample rate in Hz
Returns
-------
matplotlib plot
'''
time_axis = np.linspace(0,seg_details[2]['cf_re_fm'].size/fs,seg_details[2]['cf_re_fm'].size)
try:
plt.plot(time_axis, seg_details[2]['cf_re_fm'], label='$high - low$ passed dBrms')
plt.plot(time_axis, seg_details[2]['fm_re_cf'], label='$low - high$ passed dBrms', linestyle='dotted')
plt.legend(frameon=False)
except:
raise KeyError('Unable to find cf_re_fm and fm_re_cf keys in the segmentation output. Are you sure this is output from peak percentage method?')