Source code for pitch_garden.voters.median

"""A module for the median voter."""

from collections.abc import Mapping

import numpy as np
from numpy.lib.stride_tricks import sliding_window_view

from ..interfaces.types import Pitch
from .base import BaseVoter


[docs] class MedianVoter(BaseVoter): """A voter taking the median of log F0 across trackers and neighboring frames. This is compatible with `medfilt -w 1 -magic` of SPTK. For each frame, the values of all trackers within the temporal window are collected. The frame is unvoiced if the unvoiced values outnumber the voiced ones; otherwise, the median of the voiced values in the log domain is taken. """ def __init__(self, filter_length: int = 3) -> None: """Initialize the median voter. Parameters ---------- filter_length : int, optional The length of the temporal window in frames. The window is truncated at the edges. Raises ------ ValueError If the filter length is not positive. """ if filter_length < 1: raise ValueError("filter_length must be a positive integer.") self.filter_length = filter_length
[docs] def vote(self, pitches: Mapping[str, Pitch]) -> Pitch: """Integrate the outputs of the trackers. Parameters ---------- pitches : Mapping[str, Pitch] The outputs of the trackers keyed by the tracker names. Returns ------- out : Pitch The integrated F0 contour. """ f0 = np.stack([p.f0 for p in pitches.values()], axis=1) # (N, K) voted = self._vote(f0) if len(f0) else np.zeros(0) first = next(iter(pitches.values())) return Pitch( f0=voted, frame_period=first.frame_period, sample_rate=first.sample_rate, source="median", )
def _vote(self, f0: np.ndarray) -> np.ndarray: """Vote the F0 of the trackers with shape (N, K), where N is positive.""" num_frames, num_trackers = f0.shape order = self.filter_length - 1 future = order // 2 past = order - future # The unvoiced values are NaN, and the padded values out of the contour are # excluded from the counts, which truncates the window at the edges. log_f0 = np.full_like(f0, np.nan) np.log(f0, out=log_f0, where=0 < f0) padding = ((past, future), (0, 0)) values = np.pad(log_f0, padding, constant_values=np.nan) inside = np.pad(np.ones_like(f0, dtype=bool), padding) shape = (num_frames, num_trackers * self.filter_length) values = sliding_window_view(values, self.filter_length, axis=0).reshape(shape) inside = sliding_window_view(inside, self.filter_length, axis=0).reshape(shape) num_voiced = np.sum(~np.isnan(values), axis=1) num_unvoiced = np.sum(inside, axis=1) - num_voiced voiced = num_unvoiced <= num_voiced voted = np.zeros(num_frames) if np.any(voiced): voted[voiced] = np.exp(np.nanmedian(values[voiced], axis=1)) return voted