Source code for jams.display

#!/usr/bin/env python
r'''
Display
-------

.. autosummary::
    :toctree: generated/

    display
    display_multi
'''

from collections import OrderedDict

import json
import re
import six

import numpy as np

import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredText

import mir_eval.display

from .eval import hierarchy_flatten
from .exceptions import NamespaceError, ParameterError
from .eval import coerce_annotation
from .nsconvert import can_convert


def pprint_jobject(obj, **kwargs):
    '''Pretty-print a jobject.

    Parameters
    ----------
    obj : jams.JObject

    kwargs
        additional parameters to `json.dumps`

    Returns
    -------
    string
        A simplified display of `obj` contents.
    '''

    obj_simple = {k: v for k, v in six.iteritems(obj.__json__) if v}

    string = json.dumps(obj_simple, **kwargs)

    # Suppress braces and quotes
    string = re.sub(r'[{}"]', '', string)

    # Kill trailing commas
    string = re.sub(r',\n', '\n', string)

    # Kill blank lines
    string = re.sub(r'^\s*$', '', string)

    return string


def intervals(annotation, **kwargs):
    '''Plotting wrapper for labeled intervals'''
    times, labels = annotation.to_interval_values()

    return mir_eval.display.labeled_intervals(times, labels, **kwargs)


def hierarchy(annotation, **kwargs):
    '''Plotting wrapper for hierarchical segmentations'''
    htimes, hlabels = hierarchy_flatten(annotation)

    htimes = [np.asarray(_) for _ in htimes]
    return mir_eval.display.hierarchy(htimes, hlabels, **kwargs)


def pitch_contour(annotation, **kwargs):
    '''Plotting wrapper for pitch contours'''
    ax = kwargs.pop('ax', None)

    # If the annotation is empty, we need to construct a new axes
    ax = mir_eval.display.__get_axes(ax=ax)[0]

    times, values = annotation.to_interval_values()

    indices = np.unique([v['index'] for v in values])

    for idx in indices:
        rows = [i for (i, v) in enumerate(values) if v['index'] == idx]
        freqs = np.asarray([values[r]['frequency'] for r in rows])
        unvoiced = ~np.asarray([values[r]['voiced'] for r in rows])
        freqs[unvoiced] *= -1

        ax = mir_eval.display.pitch(times[rows, 0], freqs, unvoiced=True,
                                    ax=ax,
                                    **kwargs)
    return ax


def event(annotation, **kwargs):
    '''Plotting wrapper for events'''

    times, values = annotation.to_interval_values()

    if any(values):
        labels = values
    else:
        labels = None

    return mir_eval.display.events(times, labels=labels, **kwargs)


def beat_position(annotation, **kwargs):
    '''Plotting wrapper for beat-position data'''

    times, values = annotation.to_interval_values()

    labels = [_['position'] for _ in values]

    # TODO: plot time signature, measure number
    return mir_eval.display.events(times, labels=labels, **kwargs)


def piano_roll(annotation, **kwargs):
    '''Plotting wrapper for piano rolls'''
    times, midi = annotation.to_interval_values()

    return mir_eval.display.piano_roll(times, midi=midi, **kwargs)


VIZ_MAPPING = OrderedDict()

VIZ_MAPPING['segment_open'] = intervals
VIZ_MAPPING['chord'] = intervals
VIZ_MAPPING['multi_segment'] = hierarchy
VIZ_MAPPING['pitch_contour'] = pitch_contour
VIZ_MAPPING['beat_position'] = beat_position
VIZ_MAPPING['beat'] = event
VIZ_MAPPING['onset'] = event
VIZ_MAPPING['note_midi'] = piano_roll
VIZ_MAPPING['tag_open'] = intervals


[docs]def display(annotation, meta=True, **kwargs): '''Visualize a jams annotation through mir_eval Parameters ---------- annotation : jams.Annotation The annotation to display meta : bool If `True`, include annotation metadata in the figure kwargs Additional keyword arguments to mir_eval.display functions Returns ------- ax Axis handles for the new display Raises ------ NamespaceError If the annotation cannot be visualized ''' for namespace, func in six.iteritems(VIZ_MAPPING): try: ann = coerce_annotation(annotation, namespace) axes = func(ann, **kwargs) # Title should correspond to original namespace, not the coerced version axes.set_title(annotation.namespace) if meta: description = pprint_jobject(annotation.annotation_metadata, indent=2) anchored_box = AnchoredText(description.strip('\n'), loc=2, frameon=True, bbox_to_anchor=(1.02, 1.0), bbox_transform=axes.transAxes, borderpad=0.0) axes.add_artist(anchored_box) axes.figure.subplots_adjust(right=0.8) return axes except NamespaceError: pass raise NamespaceError('Unable to visualize annotation of namespace="{:s}"' .format(annotation.namespace))
[docs]def display_multi(annotations, fig_kw=None, meta=True, **kwargs): '''Display multiple annotations with shared axes Parameters ---------- annotations : jams.AnnotationArray A collection of annotations to display fig_kw : dict Keyword arguments to `plt.figure` meta : bool If `True`, display annotation metadata for each annotation kwargs Additional keyword arguments to the `mir_eval.display` routines Returns ------- fig The created figure axs List of subplot axes corresponding to each displayed annotation ''' if fig_kw is None: fig_kw = dict() fig_kw.setdefault('sharex', True) fig_kw.setdefault('squeeze', True) # Filter down to coercable annotations first display_annotations = [] for ann in annotations: for namespace in VIZ_MAPPING: if can_convert(ann, namespace): display_annotations.append(ann) break # If there are no displayable annotations, fail here if not len(display_annotations): raise ParameterError('No displayable annotations found') fig, axs = plt.subplots(nrows=len(display_annotations), ncols=1, **fig_kw) # MPL is stupid when making singleton subplots. # We catch this and make it always iterable. if len(display_annotations) == 1: axs = [axs] for ann, ax in zip(display_annotations, axs): kwargs['ax'] = ax display(ann, meta=meta, **kwargs) return fig, axs