from __future__ import annotations
import logging
import os
from dataclasses import dataclass, field
from typing import Iterable
import numpy as np
from sample_id import util
logger = logging.getLogger(__name__)
[docs]def from_file(audio_path, id, sr, hop_length=512, feature="sift", dedupe=False, **kwargs) -> Fingerprint:
"""Generate a fingerprint from an audio file."""
if feature == "sift":
from . import sift
fp = sift.from_file(audio_path, id, sr, hop_length=hop_length, **kwargs)
if dedupe:
fp.remove_similar_keypoints()
return fp
else:
raise NotImplementedError
[docs]def load(filepath: str) -> Fingerprint:
"""Load a fingerprint from file."""
with np.load(filepath) as data:
constructor_args = Fingerprint.__init__.__code__.co_varnames[1:]
# TODO: replace this hack to be backwards compatible with misnamed hop arg
if "hop" in data.keys():
constructor_args = ["hop" if arg == "hop_length" else arg for arg in constructor_args]
arg_data = tuple(data.get(arg) for arg in constructor_args)
arg_values = [arg if arg is None or arg.shape else arg.item() for arg in arg_data]
return Fingerprint(*arg_values)
[docs]class Fingerprint:
spectrogram = NotImplemented
def __init__(self, keypoints, descriptors, id, sr, hop_length, is_deduped=False, octave_bins=None):
self.keypoints = keypoints
self.descriptors = descriptors
self.id = id
self.sr = sr
self.hop_length = hop_length
self.is_deduped = is_deduped
self.size = len(keypoints)
self.octave_bins = octave_bins
[docs] def remove_similar_keypoints(self):
if len(self.descriptors) > 0:
logger.info(f"{self.id}: Removing duplicate/similar keypoints...")
a = np.array(self.descriptors)
rounding_factor = 10
b = np.ascontiguousarray((a // rounding_factor) * rounding_factor).view(
np.dtype((np.void, a.dtype.itemsize * a.shape[1]))
)
_, idx = np.unique(b, return_index=True)
desc = a[sorted(idx)]
kp = np.array([k for i, k in enumerate(self.keypoints) if i in idx])
logger.info(f"{self.id}: Removed {a.shape[0] - idx.shape[0]} duplicate keypoints")
self.keypoints = kp
self.descriptors = desc
self.is_deduped = True
[docs] def keypoint_ms(self, kp) -> int:
return int(kp[0] * self.hop_length * 1000.0 / self.sr)
[docs] def keypoint_index_ids(self):
return np.repeat(self.id, self.keypoints.shape[0])
[docs] def keypoint_index_ms(self):
return np.array([self.keypoint_ms(kp) for kp in self.keypoints], dtype=np.uint32)
[docs] def save_to_dir(self, dir: str, compress: bool = True):
filepath = os.path.join(dir, self.id)
self.save(filepath, compress=compress)
[docs] def save(self, filepath: str, compress: bool = True):
save_fn = np.savez_compressed if compress else np.savez
# save all attributes used in constructor
constructor_arg_names = self.__init__.__code__.co_varnames[1:]
constructor_kwargs = {name: getattr(self, name, None) for name in constructor_arg_names}
constructor_kwargs = {key: value for key, value in constructor_kwargs.items() if value is not None}
save_fn(filepath, **constructor_kwargs)
def __repr__(self):
return util.class_repr(self)
[docs]def save_fingerprints(fingerprints: Iterable[Fingerprint], filepath: str, compress=True):
# TODO: try structured arrays: https://docs.scipy.org/doc/numpy-1.13.0/user/basics.rec.html
keypoints = np.vstack([fp.keypoints for fp in fingerprints])
descriptors = np.vstack([fp.descriptors for fp in fingerprints])
index_to_id = np.hstack([fp.keypoint_index_ids() for fp in fingerprints])
# index_to_ms = np.hstack([fp.keypoint_index_ms() for fp in fingerprints])
sr = next(fp.sr for fp in fingerprints)
hop_length = next(fp.hop_length for fp in fingerprints)
save_fn = np.savez_compressed if compress else np.savez
save_fn(
filepath,
keypoints=keypoints,
descriptors=descriptors,
index_to_id=index_to_id,
# index_to_ms=index_to_ms,
sr=sr,
hop=hop_length,
)
[docs]def load_fingerprints(filepath: str) -> Fingerprints:
with np.load(filepath) as data:
return Fingerprints(data["keypoints"], data["descriptors"], data["index_to_id"], data["index_to_ms"])
[docs]class Fingerprints:
def __init__(self, keypoints, descriptors, index_to_id, index_to_ms):
self.keypoints = keypoints
self.descriptors = descriptors
self.index_to_id = index_to_id
self.index_to_ms = index_to_ms
[docs]class LazyFingerprints(Fingerprints):
def __init__(self, npz_filepath: str):
self.data = np.load(npz_filepath, mmap_mode="r")
@property
def keypoints(self):
return self.data["keypoints"]
@property
def descriptors(self):
return self.data["descriptors"]
@property
def index_to_id(self):
return self.data["index_to_id"]
@property
def index_to_ms(self):
return self.data["index_to_ms"]
[docs]@dataclass(unsafe_hash=True)
class Keypoint:
"""A fingerprint keypoint."""
kp: np.ndarray[np.float32] = field(repr=False, compare=False)
x: float = field(init=False)
y: float = field(init=False)
scale: float = field(init=False)
orientation: float = field(init=False)
def __post_init__(self):
self.x = self.kp[0].item()
self.y = self.kp[1].item()
self.scale = self.kp[2].item()
self.orientation = self.kp[3].item()