Source code for libfmp.b.b_plot

"""
Module: libfmp.b.plot
Author: Frank Zalkow, Meinard Mueller
License: The MIT license, https://opensource.org/licenses/MIT

This file is part of the FMP Notebooks (https://www.audiolabs-erlangen.de/FMP)
"""

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import matplotlib.gridspec as gridspec
import matplotlib.patches as mpatch


FMP_COLORMAPS = {
    'FMP_1': np.array([[1.0, 0.5, 0.0], [0.33, 0.75, 0.96], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 1.0],
                       [1.0, 0.0, 1.0],  [0.99, 0.51, 0.71], [0.53, 0.0, 0.46], [0.56, 0.93, 0.72], [0, 0, 0.9]])
}


[docs]def plot_signal(x, Fs=1, T_coef=None, ax=None, figsize=(6, 2), xlabel='Time (seconds)', ylabel='', title='', dpi=72, ylim=True, **kwargs): """Line plot visualization of a signal, e.g. a waveform or a novelty function. Args: x: Input signal Fs: Sample rate (Default value = 1) T_coef: Time coeffients. If None, will be computed, based on Fs. (Default value = None) ax: The Axes instance to plot on. If None, will create a figure and axes. (Default value = None) figsize: Width, height in inches (Default value = (6, 2)) xlabel: Label for x axis (Default value = 'Time (seconds)') ylabel: Label for y axis (Default value = '') title: Title for plot (Default value = '') dpi: Dots per inch (Default value = 72) ylim: True or False (auto adjust ylim or nnot) or tuple with actual ylim (Default value = True) **kwargs: Keyword arguments for matplotlib.pyplot.plot Returns: fig: The created matplotlib figure or None if ax was given. ax: The used axes. line: The line plot """ fig = None if ax is None: fig = plt.figure(figsize=figsize, dpi=dpi) ax = plt.subplot(1, 1, 1) if T_coef is None: T_coef = np.arange(x.shape[0]) / Fs if 'color' not in kwargs: kwargs['color'] = 'gray' line = ax.plot(T_coef, x, **kwargs) ax.set_xlim([T_coef[0], T_coef[-1]]) if ylim is True: ylim_x = x[np.isfinite(x)] x_min, x_max = ylim_x.min(), ylim_x.max() if x_max == x_min: x_max = x_max + 1 ax.set_ylim([min(1.1 * x_min, 0.9 * x_min), max(1.1 * x_max, 0.9 * x_max)]) elif ylim not in [True, False, None]: ax.set_ylim(ylim) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) ax.set_title(title) if fig is not None: plt.tight_layout() return fig, ax, line
[docs]def plot_matrix(X, Fs=1, Fs_F=1, T_coef=None, F_coef=None, xlabel='Time (seconds)', ylabel='Frequency (Hz)', xlim=None, ylim=None, clim=None, title='', dpi=72, colorbar=True, colorbar_aspect=20.0, cbar_label='', ax=None, figsize=(6, 3), **kwargs): """2D raster visualization of a matrix, e.g. a spectrogram or a tempogram. Args: X: The matrix Fs: Sample rate for axis 1 (Default value = 1) Fs_F: Sample rate for axis 0 (Default value = 1) T_coef: Time coeffients. If None, will be computed, based on Fs. (Default value = None) F_coef: Frequency coeffients. If None, will be computed, based on Fs_F. (Default value = None) xlabel: Label for x-axis (Default value = 'Time (seconds)') ylabel: Label for y-axis (Default value = 'Frequency (Hz)') xlim: Limits for x-axis (Default value = None) ylim: Limits for y-axis (Default value = None) clim: Color limits (Default value = None) title: Title for plot (Default value = '') dpi: Dots per inch (Default value = 72) colorbar: Create a colorbar. (Default value = True) colorbar_aspect: Aspect used for colorbar, in case only a single axes is used. (Default value = 20.0) cbar_label: Label for colorbar (Default value = '') ax: Either (1.) a list of two axes (first used for matrix, second for colorbar), or (2.) a list with a single axes (used for matrix), or (3.) None (an axes will be created). (Default value = None) figsize: Width, height in inches (Default value = (6, 3)) **kwargs: Keyword arguments for matplotlib.pyplot.imshow Returns: fig: The created matplotlib figure or None if ax was given. ax: The used axes. im: The image plot """ fig = None if ax is None: fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi) ax = [ax] if T_coef is None: T_coef = np.arange(X.shape[1]) / Fs if F_coef is None: F_coef = np.arange(X.shape[0]) / Fs_F if 'extent' not in kwargs: x_ext1 = (T_coef[1] - T_coef[0]) / 2 x_ext2 = (T_coef[-1] - T_coef[-2]) / 2 y_ext1 = (F_coef[1] - F_coef[0]) / 2 y_ext2 = (F_coef[-1] - F_coef[-2]) / 2 kwargs['extent'] = [T_coef[0] - x_ext1, T_coef[-1] + x_ext2, F_coef[0] - y_ext1, F_coef[-1] + y_ext2] if 'cmap' not in kwargs: kwargs['cmap'] = 'gray_r' if 'aspect' not in kwargs: kwargs['aspect'] = 'auto' if 'origin' not in kwargs: kwargs['origin'] = 'lower' if 'interpolation' not in kwargs: kwargs['interpolation'] = 'nearest' im = ax[0].imshow(X, **kwargs) if len(ax) == 2 and colorbar: cbar = plt.colorbar(im, cax=ax[1]) cbar.set_label(cbar_label) elif len(ax) == 2 and not colorbar: ax[1].set_axis_off() elif len(ax) == 1 and colorbar: plt.sca(ax[0]) cbar = plt.colorbar(im, aspect=colorbar_aspect) cbar.set_label(cbar_label) ax[0].set_xlabel(xlabel) ax[0].set_ylabel(ylabel) ax[0].set_title(title) if xlim is not None: ax[0].set_xlim(xlim) if ylim is not None: ax[0].set_ylim(ylim) if clim is not None: im.set_clim(clim) if fig is not None: plt.tight_layout() return fig, ax, im
[docs]def plot_chromagram(*args, chroma_yticks=np.arange(12), **kwargs): """Call libfmp.b.plot_matrix and sets chroma labels. See :func:`libfmp.b.b_plot.plot_matrix` for parameters and return values. """ if 'ylabel' not in kwargs: kwargs['ylabel'] = 'Chroma' fig, ax, im = plot_matrix(*args, **kwargs) chroma_names = 'C C# D D# E F F# G G# A A# B'.split() ax[0].set_yticks(np.array(chroma_yticks)) ax[0].set_yticklabels([chroma_names[i] for i in chroma_yticks]) return fig, ax, im
[docs]def compressed_gray_cmap(alpha=5, N=256, reverse=False): """Create a logarithmically or exponentially compressed grayscale colormap. Args: alpha (float): The compression factor. If alpha > 0, it performs log compression (enhancing black colors). If alpha < 0, it performs exp compression (enhancing white colors). Raises an error if alpha = 0. (Default value = 5) N (int): The number of rgb quantization levels (usually 256 in matplotlib) (Default value = 256) reverse (bool): If False then "white to black", if True then "black to white" (Default value = False) Returns: color_wb (mpl.colors.LinearSegmentedColormap): The colormap """ assert alpha != 0 gray_values = np.log(1 + abs(alpha) * np.linspace(0, 1, N)) gray_values /= gray_values.max() if alpha > 0: gray_values = 1 - gray_values else: gray_values = gray_values[::-1] if reverse: gray_values = gray_values[::-1] gray_values_rgb = np.repeat(gray_values.reshape(N, 1), 3, axis=1) color_wb = LinearSegmentedColormap.from_list('color_wb', gray_values_rgb, N=N) return color_wb
[docs]class MultiplePlotsWithColorbar(): """Two-column layout plot, where the first column is for user-given plots and the second column is for colorbars if the corresponding row needs a colorbar. Attributes: axes: A list of axes for the first column. cbar_axes: A list of axes for the second column. num_plots: Number of rows, as given to init method. """ def __init__(self, num_plots, figsize=(8, 4), dpi=72, cbar_ratio=0.1, height_ratios=None): """Create an instance of the MultiplePlotsWithColorbar class Args: num_plots: Number of plots (also number of rows) figsize: Figure size in dpi dpi: Dots per inch cbar_ratio: Width ratio of color bar height_ratios: Height ratio for rows """ if height_ratios is None: height_ratios = [1] * num_plots plt.figure(figsize=figsize, dpi=dpi) gs = gridspec.GridSpec(num_plots, 2, width_ratios=[1, cbar_ratio], height_ratios=height_ratios) self.num_plots = num_plots self.axes = [] self.cbar_axes = [] for i in range(self.num_plots): self.axes.append(plt.subplot(gs[i, 0])) self.cbar_axes.append(plt.subplot(gs[i, 1]))
[docs] def make_colorbars(self): """Create colorbars if the corresponding row needs a colorbar, or hides the axis in other cases.""" for i in range(self.num_plots): ax_img = self.axes[i].get_images() if len(ax_img) == 0: self.cbar_axes[i].set_axis_off() else: plt.colorbar(ax_img[0], cax=self.cbar_axes[i]) plt.tight_layout()
[docs]def color_argument_to_dict(colors, labels_set, default='gray'): """Create a dictionary that maps labels to colors. Args: colors: Several options: 1. string of ``FMP_COLORMAPS``, 2. string of matplotlib colormap, 3. list or np.ndarray of matplotlib color specifications, 4. dict that assigns labels to colors labels_set: List of all labels default: Default color, used for labels that are in labels_set, but not in colors Returns: color_dict: Dictionary that maps labels to colors """ if isinstance(colors, str): # FMP colormap if colors in FMP_COLORMAPS: color_dict = {l: c for l, c in zip(labels_set, FMP_COLORMAPS[colors])} # matplotlib colormap else: cm = plt.get_cmap(colors) num_labels = len(labels_set) colors = [cm(i / (num_labels + 1)) for i in range(num_labels)] color_dict = {l: c for l, c in zip(labels_set, colors)} # list/np.ndarray of colors elif isinstance(colors, (list, np.ndarray, tuple)): color_dict = {l: c for l, c in zip(labels_set, colors)} # is already a dict, nothing to do elif isinstance(colors, dict): color_dict = colors else: raise ValueError('`colors` must be str, list, np.ndarray, or dict') for key in labels_set: if key not in color_dict: color_dict[key] = default return color_dict
[docs]def check_line_annotations(annot, default_label=''): """Check a list of annotations with time positions and labels. If label is missing, adds an default label. Args: annot: A List of the form of ``[(time_position, label), ...]``, or ``[(time_position, ), ...]``, or ``[time_position, ...]`` default_label: The default label used if label is missing Returns: annot: A List of tuples in the form of ``[(time_position, label), ...]`` """ if isinstance(annot[0], (list, np.ndarray, tuple)): len_annot = len(annot[0]) assert all(len(a) == len_annot for a in annot) if len_annot == 1: annot = [(a[0], default_label) for a in annot] else: assert isinstance(annot[0], (int, float, complex)) or np.isscalar(annot[0]) annot = [(a, default_label) for a in annot] return annot
[docs]def check_segment_annotations(annot, default_label=''): """Check a list of segment annotations. If label is missing, adds an default label. Args: annot: A List of the form of ``[(start_position, end_position, label), ...]``, or ``[(start_position, end_position), ...]`` default_label: The default label used if label is missing Returns: annot: A List of tuples in the form of ``[(start_position, end_position, label), ...]`` """ assert isinstance(annot[0], (list, np.ndarray, tuple)) len_annot = len(annot[0]) assert all(len(a) == len_annot for a in annot) if len_annot == 2: annot = [(a[0], a[1], default_label) for a in annot] return annot
[docs]def plot_annotation_line(annot, ax=None, label_keys=None, colors='FMP_1', figsize=(6, 1), direction='horizontal', time_min=None, time_max=None, time_axis=True, nontime_axis=False, swap_time_ticks=False, axis_off=False, dpi=72): """Create a line plot visualization for a list of annotations with time positions and labels. Args: annot: A List of tuples in the form of ``[(time_position, label), ...]`` ax: The Axes instance to plot on. If None, will create a figure and axes. (Default value = None) label_keys: A dict, where the keys are the labels used in `annot`. The values are dicts, which are used as keyword arguments for matplotlib.pyplot.axvline or matplotlib.pyplot.axhline. (Default value = None) colors: Several options: 1. string of ``FMP_COLORMAPS``, 2. string of matplotlib colormap, 3. list or np.ndarray of matplotlib color specifications, 4. dict that assigns labels to colors (Default value = 'FMP_1') figsize: Width, height in inches (Default value = (6, 1) direction: 'vertical' or 'horizontal' (Default value = 'horizontal') time_min: Minimal limit for time axis. If None, will be min annotation. (Default value = None) time_max: Maximal limit for time axis. If None, will be max from annotation. (Default value = None) time_axis: Display time axis ticks or not (Default value = True) nontime_axis: Display non-time axis ticks or not (Default value = False) swap_time_ticks: For horizontal: xticks up; for vertical: yticks left (Default value = False) axis_off: Calls ax.axis('off') (Default value = False) dpi: Dots per inch (Default value = 72) Returns: fig: The created matplotlib figure or None if ax was given. ax: The used axes. """ assert direction in ['vertical', 'horizontal'] annot = check_line_annotations(annot) fig = None if ax is None: fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi) labels_set = sorted(set([label for pos, label in annot])) colors = color_argument_to_dict(colors, labels_set) if label_keys is None: label_keys = {} for key, value in colors.items(): if key not in label_keys: label_keys[key] = {} if 'color' not in label_keys[key]: label_keys[key]['color'] = value for pos, label in annot: if direction == 'horizontal': ax.axvline(pos, **label_keys[label]) else: ax.axhline(pos, **label_keys[label]) if time_min is None: time_min = min(pos for pos, label in annot) if time_max is None: time_max = max(pos for pos, label in annot) if direction == 'horizontal': ax.set_xlim([time_min, time_max]) if not time_axis: ax.set_xticks([]) if not nontime_axis: ax.set_yticks([]) if swap_time_ticks: ax.xaxis.tick_top() else: ax.set_ylim([time_min, time_max]) if not time_axis: ax.set_yticks([]) if not nontime_axis: ax.set_xticks([]) if swap_time_ticks: ax.yaxis.tick_right() if axis_off: ax.axis('off') if fig is not None: plt.tight_layout() return fig, ax
[docs]def plot_annotation_line_overlay(*args, **kwargs): """Plot segment annotations as overlay. See :func:`libfmp.b.b_plot.plot_annotation_line` for parameters and return values. """ assert 'nontime_axis' not in kwargs kwargs['nontime_axis'] = True return plot_annotation_line(*args, **kwargs)
[docs]def plot_annotation_multiline(annot, ax=None, label_keys=None, colors='FMP_1', figsize=(6, 1.5), direction='horizontal', sort_labels=None, time_min=None, time_max=None, time_axis=True, swap_time_ticks=False, axis_off=False, dpi=72): """Create a multi-line plot visualization for a list of annotations with time positions and labels. The multi-line plot visualization comprises a separate line plot visualization for each label category stacked horizontally or vertically (depending on the ``direction`` keyword). Args: annot: A List of tuples in the form of ``[(time_position, label), ...]`` ax: The Axes instance to plot on. If None, will create a figure and axes. (Default value = None) label_keys: A dict, where the keys are the labels used in ``annot``. The values are dicts, which are used as keyword arguments for matplotlib.pyplot.axvline or matplotlib.pyplot.axhline. (Default value = None) colors: Several options: 1. string of ``FMP_COLORMAPS``, 2. string of matplotlib colormap, 3. list or np.ndarray of matplotlib color specifications, 4. dict that assigns labels to colors (Default value = 'FMP_1') figsize: Width, height in inches (Default value = (6, 1.5) direction: 'vertical' or 'horizontal' (Default value = 'horizontal') sort_labels: List of labels used for sorting the line plots (Default value = None) time_min: Minimal limit for time axis. If None, will be min annotation. (Default value = None) time_max: Maximal limit for time axis. If None, will be max from annotation. (Default value = None) time_axis: Display time axis ticks or not (Default value = True) swap_time_ticks: For horizontal: xticks up; for vertical: yticks left (Default value = False) axis_off: Calls ax.axis('off') (Default value = False) dpi: Dots per inch (Default value = 72) Returns: fig: The created matplotlib figure or None if ax was given. ax: The used axes. """ assert direction in ['vertical', 'horizontal'] annot = check_line_annotations(annot) fig = None if ax is None: fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi) labels_set = sorted(set([label for pos, label in annot])) colors = color_argument_to_dict(colors, labels_set) if label_keys is None: label_keys = {} for key, value in colors.items(): if key not in label_keys: label_keys[key] = {} if 'color' not in label_keys[key]: label_keys[key]['color'] = value if sort_labels: sort_func = lambda x: sort_labels.index(x) if x in sort_labels else 0 else: sort_func = None all_labels = sorted(set(label for pos, label in annot), key=sort_func) for i, cur_label in enumerate(all_labels): cur_pos = [pos for pos, label in annot if cur_label == label] if direction == 'horizontal': ax.vlines(cur_pos, i, i+1, **label_keys[cur_label]) else: ax.hlines(cur_pos, i, i+1, **label_keys[cur_label]) if time_min is None: time_min = min(pos for pos, label in annot) if time_max is None: time_max = max(pos for pos, label in annot) if direction == 'horizontal': ax.set_ylim([0, len(all_labels)]) ax.set_xlim([time_min, time_max]) for seperator in range(1, len(all_labels)): ax.axhline(seperator, color='k') ax.set_yticks(np.arange(len(all_labels)) + 0.5) ax.set_yticklabels(all_labels) if not time_axis: ax.set_xticks([]) if swap_time_ticks: ax.xaxis.tick_top() else: ax.set_xlim([0, len(all_labels)]) ax.set_ylim([time_min, time_max]) for seperator in range(1, len(all_labels)): ax.axvline(seperator, color='k') ax.set_xticks(np.arange(len(all_labels)) + 0.5) ax.set_xticklabels(all_labels, rotation=90) if not time_axis: ax.set_yticks([]) if swap_time_ticks: ax.yaxis.tick_right() if axis_off: ax.axis('off') if fig is not None: plt.tight_layout() return fig, ax
[docs]def plot_segments(annot, ax=None, figsize=(6, 1), direction='horizontal', colors='FMP_1', time_min=None, time_max=None, nontime_min=0, nontime_max=1, time_axis=True, nontime_axis=False, time_label=None, swap_time_ticks=False, edgecolor='k', axis_off=False, dpi=72, adjust_time_axislim=True, adjust_nontime_axislim=True, alpha=None, print_labels=True, label_ticks=False, **kwargs): """Create a segment plot visualization for a list of annotations with start time positions, end time positions, and labels. The segment plot visualization shows a colored box for each segment. Args: annot: A List of tuples in the form of ``[(start_position, end_position, label), ...]`` ax: The Axes instance to plot on. If None, will create a figure and axes. (Default value = None) figsize: Width, height in inches (Default value = (6, 1) direction: 'vertical' or 'horizontal' (Default value = 'horizontal') colors: Several options: 1. string of ``FMP_COLORMAPS``, 2. string of matplotlib colormap, 3. list or np.ndarray of matplotlib color specifications, 4. dict that assigns labels to colors (Default value = 'FMP_1') time_min: Minimal limit for time axis. If None, will be min annotation. (Default value = None) time_max: Maximal limit for time axis. If None, will be max from annotation. (Default value = None) nontime_min: Minimal limit for non-time axis. (Default value = 0) nontime_max: Maximal limit for non-time axis. (Default value = 1) time_axis: Display time axis ticks or not (Default value = True) nontime_axis: Display non-time axis ticks or not (Default value = False) time_label: Label for time axes (Default value = None) swap_time_ticks: For horizontal: xticks up; for vertical: yticks left (Default value = False) edgecolor: Color for edgelines of segment box (Default value = 'k') axis_off: Calls ax.axis('off') (Default value = False) dpi: Dots per inch (Default value = 72) adjust_time_axislim: Adjust time-axis. Usually True for plotting on standalone axes and False for overlay plotting (Default value = True) adjust_nontime_axislim: Adjust non-time-axis. Usually True for plotting on standalone axes and False for overlay plotting (Default value = True) alpha: Alpha value for rectangle (Default value = None) print_labels: Print labels inside Rectangles (Default value = True) label_ticks: Print labels as ticks (Default value = False) **kwargs: Returns: fig: The created matplotlib figure or None if ax was given. ax: The used axes. """ assert direction in ['vertical', 'horizontal'] annot = check_segment_annotations(annot) if 'color' not in kwargs: kwargs['color'] = 'k' if 'weight' not in kwargs: kwargs['weight'] = 'bold' # kwargs['weight'] = 'normal' if 'fontsize' not in kwargs: kwargs['fontsize'] = 12 if 'ha' not in kwargs: kwargs['ha'] = 'center' if 'va' not in kwargs: kwargs['va'] = 'center' if colors is None: colors = 'FMP_1' fig = None if ax is None: fig, ax = plt.subplots(1, 1, figsize=figsize, dpi=dpi) labels_set = sorted(set([label for start, end, label in annot])) colors = color_argument_to_dict(colors, labels_set) nontime_width = nontime_max - nontime_min nontime_middle = nontime_min + nontime_width / 2 all_time_middles = [] for start, end, label in annot: time_width = end - start time_middle = start + time_width / 2 all_time_middles.append(time_middle) if direction == 'horizontal': rect = mpatch.Rectangle((start, nontime_min), time_width, nontime_width, facecolor=colors[label], edgecolor=edgecolor, alpha=alpha) ax.add_patch(rect) if print_labels: ax.annotate(label, (time_middle, nontime_middle), **kwargs) else: rect = mpatch.Rectangle((nontime_min, start), nontime_width, time_width, facecolor=colors[label], edgecolor=edgecolor, alpha=alpha) ax.add_patch(rect) if print_labels: ax.annotate(label, (nontime_middle, time_middle), **kwargs) if time_min is None: time_min = min(start for start, end, label in annot) if time_max is None: time_max = max(end for start, end, label in annot) if direction == 'horizontal': if adjust_time_axislim: ax.set_xlim([time_min, time_max]) if adjust_nontime_axislim: ax.set_ylim([nontime_min, nontime_max]) if not nontime_axis: ax.set_yticks([]) if not time_axis: ax.set_xticks([]) if swap_time_ticks: ax.xaxis.tick_top() if time_label: ax.set_xlabel(time_label) if label_ticks: ax.set_xticks(all_time_middles) ax.set_xticklabels([label for start, end, label in annot]) else: if adjust_time_axislim: ax.set_ylim([time_min, time_max]) if adjust_nontime_axislim: ax.set_xlim([nontime_min, nontime_max]) if not nontime_axis: ax.set_xticks([]) if not time_axis: ax.set_yticks([]) if swap_time_ticks: ax.yaxis.tick_right() if time_label: ax.set_ylabel(time_label) if label_ticks: ax.set_yticks(all_time_middles) ax.set_yticklabels([label for start, end, label in annot]) if axis_off: ax.axis('off') if fig is not None: plt.tight_layout() return fig, ax
[docs]def plot_segments_overlay(*args, **kwargs): """Plot segment annotations as overlay. See :func:`libfmp.b.b_plot.plot_segments` for parameters and return values. """ assert 'ax' in kwargs ax = kwargs['ax'] if 'adjust_time_axislim' not in kwargs: kwargs['adjust_time_axislim'] = False if 'adjust_nontime_axislim' not in kwargs: kwargs['adjust_nontime_axislim'] = False if 'alpha' not in kwargs: kwargs['alpha'] = 0.3 if 'edgecolor' not in kwargs: kwargs['edgecolor'] = None if 'nontime_axis' not in kwargs: kwargs['nontime_axis'] = True if 'direction' in kwargs and kwargs['direction'] == 'vertical': kwargs['nontime_min'], kwargs['nontime_max'] = ax.get_xlim() else: kwargs['nontime_min'], kwargs['nontime_max'] = ax.get_ylim() fig, ax = plot_segments(*args, **kwargs) return fig, ax