Source code for pitch_garden.metrics.pitch

"""Metrics on the F0 values."""

import numpy as np

from ..utils.units import hz_to_cents
from ._utils import F0Like, prepare, safe_divide


[docs] def gpe( reference: F0Like, estimate: F0Like, mask: np.ndarray | None = None, threshold: float = 0.2, ) -> float: """Calculate GPE (Gross Pitch Error). Parameters ---------- reference : Pitch | np.ndarray The reference F0 in Hz (unvoiced: 0). estimate : Pitch | np.ndarray The estimated F0 in Hz (unvoiced: 0). mask : np.ndarray | None, optional The boolean array that is True for the frames to be evaluated. threshold : float, optional The relative error regarded as gross. Returns ------- out : float The ratio of gross errors among the frames voiced in both. """ ref, est = prepare(reference, estimate, mask) both = (0 < ref) & (0 < est) gross = threshold < np.abs(est[both] / ref[both] - 1) return safe_divide(np.sum(gross), np.sum(both))
[docs] def fpe( reference: F0Like, estimate: F0Like, mask: np.ndarray | None = None, threshold: float = 0.2, ) -> float: """Calculate FPE (Fine Pitch Error). Parameters ---------- reference : Pitch | np.ndarray The reference F0 in Hz (unvoiced: 0). estimate : Pitch | np.ndarray The estimated F0 in Hz (unvoiced: 0). mask : np.ndarray | None, optional The boolean array that is True for the frames to be evaluated. threshold : float, optional The relative error regarded as gross, which is excluded. Returns ------- out : float The standard deviation of the error in cents among the frames voiced in both without gross errors. """ ref, est = prepare(reference, estimate, mask) both = (0 < ref) & (0 < est) fine = both.copy() fine[both] = np.abs(est[both] / ref[both] - 1) <= threshold if not np.any(fine): return float("nan") return float(np.std(hz_to_cents(est[fine], ref[fine])))
def _hits( ref: np.ndarray, est: np.ndarray, tolerance: float, chroma: bool ) -> np.ndarray: """Find the frames whose estimated F0 is within the tolerance.""" both = (0 < ref) & (0 < est) hits = np.zeros_like(both) diff = hz_to_cents(est[both], ref[both]) if chroma: diff = (diff + 600) % 1200 - 600 hits[both] = np.abs(diff) < tolerance return hits
[docs] def rpa( reference: F0Like, estimate: F0Like, mask: np.ndarray | None = None, tolerance: float = 50.0, ) -> float: """Calculate RPA (Raw Pitch Accuracy). Parameters ---------- reference : Pitch | np.ndarray The reference F0 in Hz (unvoiced: 0). estimate : Pitch | np.ndarray The estimated F0 in Hz (unvoiced: 0). mask : np.ndarray | None, optional The boolean array that is True for the frames to be evaluated. tolerance : float, optional The tolerance in cents. Returns ------- out : float The ratio of reference voiced frames estimated within the tolerance. """ ref, est = prepare(reference, estimate, mask) return safe_divide(np.sum(_hits(ref, est, tolerance, False)), np.sum(0 < ref))
[docs] def rca( reference: F0Like, estimate: F0Like, mask: np.ndarray | None = None, tolerance: float = 50.0, ) -> float: """Calculate RCA (Raw Chroma Accuracy), i.e., RPA ignoring octave errors. Parameters ---------- reference : Pitch | np.ndarray The reference F0 in Hz (unvoiced: 0). estimate : Pitch | np.ndarray The estimated F0 in Hz (unvoiced: 0). mask : np.ndarray | None, optional The boolean array that is True for the frames to be evaluated. tolerance : float, optional The tolerance in cents. Returns ------- out : float The ratio of reference voiced frames estimated within the tolerance after octave folding. """ ref, est = prepare(reference, estimate, mask) return safe_divide(np.sum(_hits(ref, est, tolerance, True)), np.sum(0 < ref))
[docs] def oa( reference: F0Like, estimate: F0Like, mask: np.ndarray | None = None, tolerance: float = 50.0, ) -> float: """Calculate OA (Overall Accuracy). Parameters ---------- reference : Pitch | np.ndarray The reference F0 in Hz (unvoiced: 0). estimate : Pitch | np.ndarray The estimated F0 in Hz (unvoiced: 0). mask : np.ndarray | None, optional The boolean array that is True for the frames to be evaluated. tolerance : float, optional The tolerance in cents. Returns ------- out : float The ratio of frames that are correctly unvoiced or voiced in both with the estimated F0 within the tolerance. """ ref, est = prepare(reference, estimate, mask) unvoiced = (ref <= 0) & (est <= 0) correct = unvoiced | _hits(ref, est, tolerance, False) return safe_divide(np.sum(correct), len(ref))
[docs] def f1_score( reference: F0Like, estimate: F0Like, mask: np.ndarray | None = None, tolerance: float = 50.0, ) -> float: """Calculate the pitch F1 score. A frame is a hit if both are voiced and the error is within the tolerance. The precision and recall are the ratios of hits to the estimated and reference voiced frames, respectively. Parameters ---------- reference : Pitch | np.ndarray The reference F0 in Hz (unvoiced: 0). estimate : Pitch | np.ndarray The estimated F0 in Hz (unvoiced: 0). mask : np.ndarray | None, optional The boolean array that is True for the frames to be evaluated. tolerance : float, optional The tolerance in cents. Returns ------- out : float The pitch F1 score. """ ref, est = prepare(reference, estimate, mask) hits = np.sum(_hits(ref, est, tolerance, False)) precision = safe_divide(hits, np.sum(0 < est)) recall = safe_divide(hits, np.sum(0 < ref)) return safe_divide(2 * precision * recall, precision + recall)