Skip to content

Commit

Permalink
Merge pull request #74 from bmcfee/segment-label-agreement
Browse files Browse the repository at this point in the history
added structure agreement task
  • Loading branch information
bmcfee committed Jun 11, 2017
2 parents 11fc3d7 + 2c28666 commit b87b665
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 1 deletion.
7 changes: 7 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changes
-------

0.2.3
=====
- `#74`_ Implemented segmentation agreement task

.. _#74: https://github.com/bmcfee/pumpp/pull/74


0.2.2
=====

Expand Down
2 changes: 2 additions & 0 deletions pumpp/task/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
VectorTransformer
DynamicLabelTransformer
StaticLabelTransformer
StructureTransformer
'''

from .base import *
from .chord import *
from .event import *
from .regression import *
from .tags import *
from .structure import *
2 changes: 1 addition & 1 deletion pumpp/task/chord.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class ChordTransformer(BaseTaskTransformer):
See Also
--------
SimpleTransformer
SimpleChordTransformer
'''
def __init__(self, name='chord', sr=22050, hop_length=512, sparse=False):
'''Initialize a chord task transformer'''
Expand Down
80 changes: 80 additions & 0 deletions pumpp/task/structure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''Segment and structure tasks'''

import numpy as np
from mir_eval.util import intervals_to_samples, index_labels, adjust_intervals

from .base import BaseTaskTransformer

__all__ = ['StructureTransformer']


class StructureTransformer(BaseTaskTransformer):
'''Structure agreement transformer.
This transformer maps a labeled, flat structural segmentation
to an `n*n` boolean matrix indicating whether two frames
belong to a similarly labeled segment or not.
Attributes
----------
name : str
The name of this transformer
sr : number > 0
The audio sampling rate
hop_length : int > 0
The number of samples between each annotation frame
'''

def __init__(self, name='structure', sr=22050, hop_length=512):
'''Initialize a structure agreement transformer'''

super(StructureTransformer, self).__init__(name=name,
namespace='segment_open',
sr=sr,
hop_length=hop_length)

self.register('agree', [None, None], np.bool)

def empty(self, duration):
ann = super(StructureTransformer, self).empty(duration)
ann.append(time=0, duration=duration, value='none', confidence=0)
return ann

def transform_annotation(self, ann, duration):
'''Apply the structure agreement transformation.
Parameters
----------
ann : jams.Annotation
The segment annotation
duration : number > 0
The target duration
Returns
-------
data : dict
data['agree'] : np.ndarray, shape=(n, n), dtype=bool
'''

intervals, values = ann.to_interval_values()

intervals, values = adjust_intervals(intervals, values,
t_min=0, t_max=duration)
# Re-index the labels
ids, _ = index_labels(values)

rate = float(self.hop_length) / self.sr
# Sample segment labels on our frame grid
_, labels = intervals_to_samples(intervals, ids, sample_size=rate)

# Make the agreement matrix
return {'agree': np.equal.outer(labels, labels)}

def inverse(self, agree, duration=None):

raise NotImplementedError('Segment agreement cannot be inverted')
28 changes: 28 additions & 0 deletions tests/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ def ann_chord():
return ann


@pytest.fixture()
def ann_segment():

ann = jams.Annotation(namespace='segment_open', duration=5)

for t, c in [(0, 'A'),
(1, 'B'),
(2, 'A'),
(3, 'B'),
(4, 'C')]:
ann.append(time=t, duration=1, value=c)

return ann


def test_decode_tags_dynamic_hard(sr, hop_length, ann_tag):

# This test encodes an annotation, decodes it, and then re-encodes it
Expand Down Expand Up @@ -320,3 +335,16 @@ def test_decode_chordtag_soft_dense_sparse(sr, hop_length, ann_chord):
dense_positions = np.where(data['chord'])[1]
sparse_positions = data2['chord'][:, 0]
assert np.allclose(dense_positions, sparse_positions)


@pytest.mark.xfail(raises=NotImplementedError)
def test_decode_structure(sr, hop_length, ann_segment):

tc = pumpp.task.StructureTransformer('struct', sr=sr,
hop_length=hop_length)

data = tc.transform_annotation(ann_segment, ann_segment.duration)
inverse = tc.inverse(data['agree'], duration=ann_segment.duration)
data2 = tc.transform_annotation(inverse, ann_segment.duration)

assert np.allclose(data['agree'], data2['agree'])
66 changes: 66 additions & 0 deletions tests/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,72 @@ def test_task_beat_absent(SR, HOP_LENGTH):
assert type_match(output[key].dtype, trans.fields[key].dtype)


def test_task_structure_fields():

trans = pumpp.task.StructureTransformer(name='struct')

assert set(trans.fields.keys()) == set(['struct/agree'])

assert trans.fields['struct/agree'].shape == (None, None)
assert trans.fields['struct/agree'].dtype is np.bool


def test_task_structure_present(SR, HOP_LENGTH):
jam = jams.JAMS(file_metadata=dict(duration=4.0))

ann = jams.Annotation(namespace='segment_open')

ann.append(time=0, duration=1.0, value='alpha')
ann.append(time=1.0, duration=0.5, value='beta')
ann.append(time=1.5, duration=1.5, value='alpha')
ann.append(time=3, duration=1.0, value='gamma')

jam.annotations.append(ann)
trans = pumpp.task.StructureTransformer(name='struct',
sr=SR,
hop_length=HOP_LENGTH)

output = trans.transform(jam)

# Mask should be true
assert np.all(output['struct/_valid'] == [0, 4 * trans.sr //
trans.hop_length])

y = output['struct/agree']

# Check the shape
assert y.shape == (1, 4 * (SR // HOP_LENGTH), 4 * (SR // HOP_LENGTH))

for key in trans.fields:
assert shape_match(output[key].shape[1:], trans.fields[key].shape)
assert type_match(output[key].dtype, trans.fields[key].dtype)


def test_task_structure_absent(SR, HOP_LENGTH):
jam = jams.JAMS(file_metadata=dict(duration=4.0))

trans = pumpp.task.StructureTransformer(name='struct',
sr=SR,
hop_length=HOP_LENGTH)

output = trans.transform(jam)

# Mask should be true
assert not np.any(output['struct/_valid'])

y = output['struct/agree']

# Check the shape
assert y.shape == (1, 4 * (SR // HOP_LENGTH), 4 * (SR // HOP_LENGTH))

# With a null structure annotation, all frames are similar
assert np.all(y)

for key in trans.fields:
assert shape_match(output[key].shape[1:], trans.fields[key].shape)
assert type_match(output[key].dtype, trans.fields[key].dtype)


def test_transform_noprefix():

labels = ['foo', 'bar', 'baz']
Expand Down

0 comments on commit b87b665

Please sign in to comment.