# -*- coding: utf-8 -*-
# ######### COPYRIGHT #########
# Credits
# #######
#
# Copyright(c) 2015-2018
# ----------------------
#
# * `LabEx Archimède <http://labex-archimede.univ-amu.fr/>`_
# * `Laboratoire d'Informatique Fondamentale <http://www.lif.univ-mrs.fr/>`_
# (now `Laboratoire d'Informatique et Systèmes <http://www.lis-lab.fr/>`_)
# * `Institut de Mathématiques de Marseille <http://www.i2m.univ-amu.fr/>`_
# * `Université d'Aix-Marseille <http://www.univ-amu.fr/>`_
#
# This software is a port from LTFAT 2.1.0 :
# Copyright (C) 2005-2018 Peter L. Soendergaard <peter@sonderport.dk>.
#
# Contributors
# ------------
#
# * Denis Arrivault <contact.dev_AT_lis-lab.fr>
# * Florent Jaillet <contact.dev_AT_lis-lab.fr>
#
# Description
# -----------
#
# ltfatpy is a partial Python port of the
# `Large Time/Frequency Analysis Toolbox <http://ltfat.sourceforge.net/>`_,
# a MATLAB®/Octave toolbox for working with time-frequency analysis and
# synthesis.
#
# Version
# -------
#
# * ltfatpy version = 1.0.16
# * LTFAT version = 2.1.0
#
# Licence
# -------
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# ######### COPYRIGHT #########
"""Module of time-frequency plotting
Ported from ltfat_2.1.0/gabor/tfplot.m
.. moduleauthor:: Florent Jaillet
"""
from __future__ import print_function, division
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
[docs]def tfplot(coef, step, yr, fs=None, dynrange=None, normalization='db',
tc=False, clim=None, plottype='image', colorbar=True, display=True,
time='Time', frequency='Frequency', samples='samples',
normalized='normalized'):
"""Plot coefficient matrix on the TF plane
- Input parameters:
:param numpy.ndarray coef: 2D coefficient array
:param float step: Shift in samples between each column of coefficients
:param numpy.ndarray yr: 2 elements vector containing the lowest and
highest normalized frequency
:param float fs: Sampling rate in Hz of the original signal
:param float dynrange: Limit the dynamical range to dynrange by using a
colormap in the interval [chigh-dynrange, chigh], where chigh is the
highest value in the plot. The default value of None means to not limit
the dynamical range. If both **clim** and **dynrange** are specified,
then **clim** takes precedence.
:param str normalization: String specifying the normalization of the plot,
possible values are listed below
:param bool tc: Time centering: if ``True``, move the beginning of the
signal to the middle of the plot. This is usefull for visualizing the
window functions of the toolbox.
:param tuple clim: Use a colormap ranging from clim[0] to clim[1]. If both
**clim** and **dynrange** are specified, then **clim** takes
precedence.
:param str plottype: String specifying the type of plot, possible values
are listed below
:param bool colorbar: If ``True``, display the colorbar (this is the
default)
:param bool display: If ``True``, display the figure (this is the default).
Using ``display=False`` to avoid displaying the figure is usefull if
you only want to obtain the output for further processing.
:param str time: Text customization: the word denoting time
:param str frequency: Text customization: the word denoting frequency
:param str samples: Text customization: the word denoting samples
:param str normalized: Text customization: the word denoting normalized
- Output parameter:
:returns: The processed image data used in the plotting. Inputting this
data directly to :func:`~matplotlib.pyplot.matshow` or similar
functions will create the plot. This is usefull for custom
post-processing of the image data.
:rtype: numpy.ndarray
``tfplot(coef, step, yr)`` will plot a rectangular coefficient array on the
TF-plane.
``tfplot`` is not meant to be called directly. Instead, it is called by
other plotting routines to give a uniform display format.
Possible values for **normalization**:
============ ==========================================================
``'db'`` Apply :math:`20*\log_{10}` to the coefficients. This makes
it possible to see very weak phenomena, but it might show
too much noise. A logarithmic scale is more adapted to
perception of sound. This is the default.
``'dbsq'`` Apply :math:`10*\log_{10}` to the coefficients. Same as
the ``'db'`` option, but assume that the input is already
squared.
``'lin'`` Show the coefficients on a linear scale. This will display
the raw input without any modifications. Only works for
real-valued input.
``'linsq'`` Show the square of the coefficients on a linear scale.
``'linabs'`` Show the absolute value of the coefficients on a linear
scale.
============ ==========================================================
Possible values for **plottype**:
============= ====================================================
``'image'`` Use imshow to display the plot. This is the default.
``'contour'`` Do a contour plot.
``'surf'`` Do a surface plot.
``'pcolor'`` Do a pcolor plot.
============= ====================================================
.. seealso:: :func:`~ltfatpy.gabor.sgram.sgram`,
:func:`~ltfatpy.gabor.plotdgt.plotdgt`,
:func:`~ltfatpy.gabor.plotdgtreal.plotdgtreal`,
:func:`plotwmdct`, :func:`plotdwilt`
"""
if not isinstance(coef, np.ndarray):
raise TypeError('coef must be a 2D numpy.ndarray')
if coef.ndim > 2:
raise ValueError('Input is multidimensional. coef must be a 2D '
'numpy.ndarray')
if coef.ndim < 2:
raise ValueError('coef must be a 2D numpy.ndarray')
M = coef.shape[0]
N = coef.shape[1]
# Apply transformation to coefficients.
if normalization == 'db':
coef = 20. * np.log10(np.abs(coef) + np.finfo(np.float64).tiny)
elif normalization == 'dbsq':
coef = 10. * np.log10(np.abs(coef) + np.finfo(np.float64).tiny)
elif normalization == 'linsq':
coef = np.square(np.abs(coef))
elif normalization == 'linabs':
coef = np.abs(coef)
elif normalization == 'lin':
if not np.isrealobj(coef):
raise ValueError("Complex valued input cannot be plotted using the"
" 'lin' flag. Please use the 'linsq' or 'linabs' "
"flag.")
else:
# coef is returned in the output so we make a copy to avoid
# returning a reference to the data passed in input
coef = coef.copy()
# 'dynrange' parameter is handled by converting it into clim
# clim overrides dynrange, so do nothing if clim is already specified
if dynrange and not clim:
maxclim = np.nanmax(coef)
clim = (maxclim - dynrange, maxclim)
# Handle clim by thresholding and cutting
if clim:
np.clip(coef, clim[0], clim[1], out=coef)
if tc:
xr = np.arange(-np.floor(N/2.), np.floor((N-1)/2)+1) * step
coef = np.fft.fftshift(coef, axes=1)
else:
xr = np.arange(0, N) * step
if display:
if fs:
xr = xr / fs
yr = yr * fs/2
# Convert yr to range of values
yr = np.linspace(yr[0], yr[1], M)
if plottype == 'image':
xstep = xr[1] - xr[0]
ystep = yr[1] - yr[0]
extent = [xr[0] - xstep/2, xr[-1] + xstep/2,
yr[0] - ystep/2, yr[-1] + ystep/2]
if clim:
# Call imshow explicitly with clim. This is necessary for the
# situations where the data is by itself limited (from above
# or below) to within the specified range. Setting clim
# explicitly avoids the colormap moves in the top or bottom.
plt.imshow(coef, extent=extent, aspect='auto',
interpolation='nearest', origin='lower', clim=clim)
else:
plt.imshow(coef, extent=extent, aspect='auto',
interpolation='nearest', origin='lower')
elif plottype == 'contour':
# Note: The matlplotlib contour function doesn't give the exact
# same visual results as in Octave for the same data.
# So we can expect some slight differences when comparing contour
# plots produced by tfplot.
plt.contour(xr, yr, coef, 10)
elif plottype == 'surf':
# Note: The following import is needed to be able to use
# plot_surface
from mpl_toolkits.mplot3d import Axes3D
plt.delaxes()
ax = plt.gcf().add_subplot(111, projection='3d')
ax.azim = -130.
ax.elev = 30.
xgrid, ygrid = np.meshgrid(xr, yr)
ax.plot_surface(xgrid, ygrid, coef, rstride=1, cstride=1,
antialiased=True, linewidth=0, cmap=cm.jet)
# Note: matplotlib doesn't support orthogonal projection,
# so the result doesn't look exactly as in Octave. See:
# http://stackoverflow.com/questions/23840756/ \
# how-to-disable-perspective-in-mplot3d
elif plottype == 'pcolor':
plt.pcolor(xr, yr, coef, edgecolors='k', antialiased=False,
linewidth=1)
plt.axis('tight')
if colorbar:
if plottype == 'surf':
# Note: we can use "plottype in ('contour', 'surf')" in the
# previous test if we want the colorbar in contour plots to
# look more like the one in Octave
mappable = cm.ScalarMappable(cmap=cm.jet)
mappable.set_array(coef)
plt.colorbar(mappable, ax=plt.gca())
else:
plt.colorbar(ax=plt.gca())
if fs:
plt.xlabel(time + ' (s)')
plt.ylabel(frequency + ' (Hz)')
else:
plt.xlabel(time + ' (' + samples + ')')
plt.ylabel(frequency + ' (' + normalized + ')')
return coef