Module thunderhopper.labeltools
Functions
def label_along_axis(array, axis=0)
-
Expand source code
def label_along_axis(array, axis=0): """ Wrapper to scipy.ndimage.label() that connects only along a given axis. Marks segments of consecutive non-zero elements along axis of the input array with unique integer labels. Does not distinguish between non-zero elements of different values. Neighbouring non-zero elements along any dimension other than axis are not connected. Individual segments along axis must be separated by at least one zero element. Parameters ---------- array : ND array of floats, ints, or bools (any shape) Input array in which to mark segments. axis : int, optional Target dimension of array along which to connect non-zero elements. The default is 0. Returns ------- marked_segments : ND array of ints (array.shape) Labeled array with unique label values for each non-zero segment. Label values are in [1, n_segments]. Other elements have a value of 0. n_segments : int Total number of non-zero segments in marked_segments. """ # Equilateral connectivity matrix: structure = np.zeros((3,) * array.ndim) # Connect only along axis: connect_ind = [1] * array.ndim connect_ind[axis] = slice(None) # Insert centered connector bar: structure[tuple(connect_ind)] = 1 return label(array, structure)
Wrapper to scipy.ndimage.label() that connects only along a given axis. Marks segments of consecutive non-zero elements along axis of the input array with unique integer labels. Does not distinguish between non-zero elements of different values. Neighbouring non-zero elements along any dimension other than axis are not connected. Individual segments along axis must be separated by at least one zero element.
Parameters
array
:ND array
offloats, ints,
orbools (any shape)
- Input array in which to mark segments.
axis
:int
, optional- Target dimension of array along which to connect non-zero elements. The default is 0.
Returns
marked_segments
:ND array
ofints (array.shape)
- Labeled array with unique label values for each non-zero segment. Label values are in [1, n_segments]. Other elements have a value of 0.
n_segments
:int
- Total number of non-zero segments in marked_segments.
def bundle_inds(array)
-
Expand source code
def bundle_inds(array): """ Finds separate segments of consecutive non-zero data points in array. Individual segments must be separated by at least one zero value. Different consecutive non-zero values are treated as a single segment. Values of zero or False are not considered in the returned indices. Parameters ---------- array : 1D array of ints, floats, or bools (m,) Array in which to find and bundle segment indices. Must contain some values of zero or False against which to identify non-zero segments. Returns ------- inds : list (n,) of 1D arrays of ints (p,) Indices of each non-zero segment in array. Returns an empty list if no non-zero segments are found (that is, if array is all zero). """ marked_segs, n_segs = label(array.ravel()) inds = [np.nonzero(marked_segs == i)[0] for i in range(1, n_segs + 1)] return inds
Finds separate segments of consecutive non-zero data points in array. Individual segments must be separated by at least one zero value. Different consecutive non-zero values are treated as a single segment. Values of zero or False are not considered in the returned indices.
Parameters
array
:1D array
ofints, floats,
orbools (m,)
- Array in which to find and bundle segment indices. Must contain some values of zero or False against which to identify non-zero segments.
Returns
inds
:list (n,)
of1D arrays
ofints (p,)
- Indices of each non-zero segment in array. Returns an empty list if no non-zero segments are found (that is, if array is all zero).
def bundle_songs(spec_code, spec_list, do_noise=False, file_code=None)
-
Expand source code
def bundle_songs(spec_code, spec_list, do_noise=False, file_code=None): """ Finds species-specific song segments (and noise) in spec_code. Applies bundle_inds() to song labels as tagged by encoder(). Parameters ---------- spec_code : 1D array of ints (m,) Species-specific song labels for each datapoint, indicating the position of the associated species among all species in spec_list. Expected tag values are in the range [1, n] and 0 for noise. spec_list : list of str (n,) Names of all included species. do_noise : bool, optional If True, returns indices of noise segments in addition to songs. Noise across file boundaries is treated as a single segment, except when file_code is specified. The default is False. file_code : 1D array of ints, optional File tag for each datapoint, indicating the position of the associated file among all files. Expected tag values start at 1 and end with the total number of files. If specified, returns indices of noise segments even if do_noise is False. Noise segments are separated at file boundaries. The default is None. Returns ------- songs : dict of lists (p,) of 1D arrays of ints (q,) Indices of individual song segments for each species (keys). May contain empty lists if no song segments are found for a species. noise : list (r,) of 1D arrays of ints (s,) Indices of noise segments between songs. Only returned if do_noise is True or file_code is specified. """ # Indices of species-specific song segments: tags = range(1, len(spec_list) + 1) songs = {s: bundle_inds(spec_code == i) for s, i in zip(spec_list, tags)} # Indices of noise segments between songs: if do_noise or (file_code is not None): ind = spec_code == 0 if file_code is not None: # Separate noise at file boundaries: tags = range(1, np.max(file_code) + 1) noise = [bundle_inds((file_code == tag) & ind) for tag in tags] else: # Noise segments across files: noise = bundle_inds(ind) return songs, noise return songs
Finds species-specific song segments (and noise) in spec_code. Applies bundle_inds() to song labels as tagged by encoder().
Parameters
spec_code
:1D array
ofints (m,)
- Species-specific song labels for each datapoint, indicating the position of the associated species among all species in spec_list. Expected tag values are in the range [1, n] and 0 for noise.
spec_list
:list
ofstr (n,)
- Names of all included species.
do_noise
:bool
, optional- If True, returns indices of noise segments in addition to songs. Noise across file boundaries is treated as a single segment, except when file_code is specified. The default is False.
file_code
:1D array
ofints
, optional- File tag for each datapoint, indicating the position of the associated file among all files. Expected tag values start at 1 and end with the total number of files. If specified, returns indices of noise segments even if do_noise is False. Noise segments are separated at file boundaries. The default is None.
Returns
songs
:dict
oflists (p,)
of1D arrays
ofints (q,)
- Indices of individual song segments for each species (keys). May contain empty lists if no song segments are found for a species.
noise
:list (r,)
of1D arrays
ofints (s,)
- Indices of noise segments between songs. Only returned if do_noise is True or file_code is specified.
def bundle_detections(predict)
-
Expand source code
def bundle_detections(predict): """ Finds detected segments for each species-specific model in predict. Applies bundle_inds() to binary model predictions. Input format determines output format. Parameters ---------- predict : dict of 1D arrays of floats (m,) or 2D array of floats (m, n) Predictions of n models over m datapoints (1 = detected, 0 = rejected). Returns ------- inds : dict or list (n,) of lists (p,) of 1D arrays of ints (q,) Indices of model-specific detection segments. If input is dict, output is dict of lists with the same keys. If input is array, output is list of lists, each of which corresponds to a column in the input array. """ if isinstance(predict, dict): # Model detections for saving and printing: inds = {key: bundle_inds(predict[key]) for key in predict.keys()} elif isinstance(predict, np.ndarray): # Model detections in less explicit compact format: inds = [bundle_inds(predict[:, i]) for i in range(predict.shape[1])] return inds
Finds detected segments for each species-specific model in predict. Applies bundle_inds() to binary model predictions. Input format determines output format.
Parameters
predict
:dict
of1D arrays
offloats (m,)
or2D array
offloats (m, n)
- Predictions of n models over m datapoints (1 = detected, 0 = rejected).
Returns
inds
:dict
orlist (n,)
oflists (p,)
of1D arrays
ofints (q,)
- Indices of model-specific detection segments. If input is dict, output is dict of lists with the same keys. If input is array, output is list of lists, each of which corresponds to a column in the input array.
def find_segment(array, target='max', index=None, eval_func='any')
-
Expand source code
def find_segment(array, target='max', index=None, eval_func='any'): """ Selects one or multiple segments meeting the specified criterion. Applies bundle_inds() to array to find separate segment indices of consecutive non-zero data points, then selects matching segments from the list. Can target the longest or shortest segment, or any segment(s) containing specific target indices. Parameters ---------- array : 1D array of ints, floats, or bools (m,) Array in which to find and bundle segment indices for selection. Must contain some values of zero or False against which to identify non-zero segments. target : str, optional Criterion for selecting target segments. Options are 'max' (longest), 'min' (shortest), or 'ind' (containing target indices). If 'ind', the index (required) and eval_func keyword arguments control selection fine-tuning. The default is 'max'. index : int or 1D array or list (n,) of ints, optional Target index or indices for segment selection. Required if target is 'ind', else ignored. For multiple targets, eval_func controls whether any or all of them must be present in a segment. The default is None. eval_func : str, optional Operation to evaluate presence of target indices in each segment (if target is 'ind', else ignored). If 'all', all targets must be contained within a single segment, which is returned immediately once found. If 'any', a segment must contain at least one target to be selected, resulting in up to len(index) returned segments. The default is 'any'. Returns ------- segments : 1D array (p,) of ints or list (q,) of 1D arrays of ints Identified segment indices in array that match the specified criterion. If target is 'max', 'min', or 'ind' with eval_func 'all', returns the selected segment as a single array of indices. If target is 'ind' with eval_func 'any' (enables multiple matches), returns a list of arrays, even if only a single segment is selected. Raises ------ ValueError Breaks if no segments could be found for the given target criterion. """ # Bundle consecutive indices: ind_segments = bundle_inds(array) # Target segment(s) by included indices: if index is not None and target == 'ind': # Assert array format: index = ensure_array(var=index, dtype=int, shape=(-1,)) segments = [] # Check for target indices: for segment in ind_segments: # Find targets contained in current segment: intersection = np.intersect1d(segment, index) if len(intersection) == len(index): # Early exit if one segment contains all indices: return segment if eval_func == 'all' else [segment] elif eval_func == 'any' and len(intersection): # Log and continue: segments.append(segment) if not len(segments): # Report failure under given match criterion: ind = 'indices' if len(index) > 1 else 'index' s = 's' if (eval_func == 'any') and (len(index) > 1) else '' raise ValueError(f'No segment{s} found for target {ind} {index}.') return segments # Target segment by number of data points: lengths = [len(seg) for seg in ind_segments] if target == 'max': # Find longest segment: return ind_segments[np.argmax(lengths)] elif target == 'min': # Find shortest segment: return ind_segments[np.argmin(lengths)]
Selects one or multiple segments meeting the specified criterion. Applies bundle_inds() to array to find separate segment indices of consecutive non-zero data points, then selects matching segments from the list. Can target the longest or shortest segment, or any segment(s) containing specific target indices.
Parameters
array
:1D array
ofints, floats,
orbools (m,)
- Array in which to find and bundle segment indices for selection. Must contain some values of zero or False against which to identify non-zero segments.
target
:str
, optional- Criterion for selecting target segments. Options are 'max' (longest), 'min' (shortest), or 'ind' (containing target indices). If 'ind', the index (required) and eval_func keyword arguments control selection fine-tuning. The default is 'max'.
index
:int
or1D array
orlist (n,)
ofints
, optional- Target index or indices for segment selection. Required if target is 'ind', else ignored. For multiple targets, eval_func controls whether any or all of them must be present in a segment. The default is None.
eval_func
:str
, optional- Operation to evaluate presence of target indices in each segment (if target is 'ind', else ignored). If 'all', all targets must be contained within a single segment, which is returned immediately once found. If 'any', a segment must contain at least one target to be selected, resulting in up to len(index) returned segments. The default is 'any'.
Returns
segments
:1D array (p,)
ofints
orlist (q,)
of1D arrays
ofints
- Identified segment indices in array that match the specified criterion. If target is 'max', 'min', or 'ind' with eval_func 'all', returns the selected segment as a single array of indices. If target is 'ind' with eval_func 'any' (enables multiple matches), returns a list of arrays, even if only a single segment is selected.
Raises
ValueError
- Breaks if no segments could be found for the given target criterion.
def edge_to_binary(edges, rate, n, dtype=builtins.bool)
-
Expand source code
def edge_to_binary(edges, rate, n, dtype=bool): """ Converts time intervals into a binary label array with given sampling. Time points within each labeled segment are set to 1, else 0. Allows to adapt label information to the rate and size of some reference array. Parameters ---------- edges : 2D array (m, 2) or list (m,) of lists (2,) of floats or ints Start and end time points of each labeled segment in s. rate : float or int Sampling rate of the reference array and the returned labels in Hz. n : int Number of samples in the reference array and the returned labels. dtype : type, optional Numpy data type of the returned label array. The default is bool. Returns ------- labels : 1D array of dtype (n,) Binary label array corresponding to the reference array. Overlapping time intervals are merged into a single continuous segment. """ labels = np.zeros(n, dtype=dtype) time = np.arange(n) / rate for edge in edges: labels[(time >= edge[0]) & (time <= edge[1])] = 1 return labels
Converts time intervals into a binary label array with given sampling. Time points within each labeled segment are set to 1, else 0. Allows to adapt label information to the rate and size of some reference array.
Parameters
edges
:2D array (m, 2)
orlist (m,)
oflists (2,)
offloats
orints
- Start and end time points of each labeled segment in s.
rate
:float
orint
- Sampling rate of the reference array and the returned labels in Hz.
n
:int
- Number of samples in the reference array and the returned labels.
dtype
:type
, optional- Numpy data type of the returned label array. The default is bool.
Returns
labels
:1D array
ofdtype (n,)
- Binary label array corresponding to the reference array. Overlapping time intervals are merged into a single continuous segment.
def edge_to_inds(edges, rate, n)
-
Expand source code
def edge_to_inds(edges, rate, n): """ Converts time intervals into indices along some reference array. First creates a binary label array of rate and size of the reference, then bundles the indices of time points within each labeled segment. Parameters ---------- edges : 2D array (m, 2) or list (m,) of lists (2,) of floats or ints Start and end time points of each labeled segment in s. rate : float or int Sampling rate of the reference array in Hz. n : int Number of samples in the reference array. Returns ------- inds : list (m,) of 1D arrays (n,) of ints Indices of time points within each labeled segment along the reference. Segment order corresponds to temporal order, not to the order in edges. Overlapping time intervals are merged into a single continuous segment. """ return bundle_inds(edge_to_binary(edges, rate, n))
Converts time intervals into indices along some reference array. First creates a binary label array of rate and size of the reference, then bundles the indices of time points within each labeled segment.
Parameters
edges
:2D array (m, 2)
orlist (m,)
oflists (2,)
offloats
orints
- Start and end time points of each labeled segment in s.
rate
:float
orint
- Sampling rate of the reference array in Hz.
n
:int
- Number of samples in the reference array.
Returns
inds
:list (m,)
of1D arrays (n,)
ofints
- Indices of time points within each labeled segment along the reference. Segment order corresponds to temporal order, not to the order in edges. Overlapping time intervals are merged into a single continuous segment.
def defragment(array, tol, rel_tol=None, replace=0, as_ind=False, skip_single=False)
-
Expand source code
def defragment(array, tol, rel_tol=None, replace=0, as_ind=False, skip_single=False): """ Removes non-zero segments if their length is below tolerance. Applies bundle_inds() to array to identify individual segments. Parameters ---------- array : 1D array of ints, floats, or bools (m,) Array to defragment. Values of zero or False are not considered during the defragmentation. tol : int or None Minimum tolerated length of a single non-zero segment. If None, immediately returns array without changes. rel_tol : float, optional If specified, overrides tol with a flexible criterion relative to the length of the largest non-zero segment in array. The default is None. replace : int, float, or bool, optional Sets gap segments below tolerance to the given value. Can be used to specify a desired datatype should the insertion of value into array fail unexpectedly. The default is 0. as_ind : bool, optional If True, returns indices of remaining non-zero segments in the defragmented array instead of the array itself. If replace is non-zero, the result is identical to calling np.nonzero() on the original array. The default is False. skip_single : bool, optional If True, leaves array unchanged if it contains only a single non-zero segment. The default is False. Returns ------- data : 1D array of ints, floats, or bools (m,) or 1D array of ints (n,) Defragmented array, or indices of remaining non-zero segments if as_ind is True. Returns unchanged array if tol is None, array contains no segments, or array contains a single segment and skip_single is True. """ # Silent skip: if tol is None: return array # Find segments and handle other skips: segments = bundle_inds(array) if len(segments) == 0: # Nothing labeled: print('WARNING: No non-zero segments found for defragmentation.') return array elif skip_single and len(segments) == 1: return array # Segment lengths and flexible tolerance: len_segments = [len(segment) for segment in segments] if rel_tol is not None: tol = int(rel_tol * np.max(len_segments)) data = array.copy() # Apply tolerance criterion: for segment, n in zip(segments, len_segments): if n < tol: data[segment] = replace return np.nonzero(data)[0] if as_ind else data
Removes non-zero segments if their length is below tolerance. Applies bundle_inds() to array to identify individual segments.
Parameters
array
:1D array
ofints, floats,
orbools (m,)
- Array to defragment. Values of zero or False are not considered during the defragmentation.
tol
:int
orNone
- Minimum tolerated length of a single non-zero segment. If None, immediately returns array without changes.
rel_tol
:float
, optional- If specified, overrides tol with a flexible criterion relative to the length of the largest non-zero segment in array. The default is None.
replace
:int, float,
orbool
, optional- Sets gap segments below tolerance to the given value. Can be used to specify a desired datatype should the insertion of value into array fail unexpectedly. The default is 0.
as_ind
:bool
, optional- If True, returns indices of remaining non-zero segments in the defragmented array instead of the array itself. If replace is non-zero, the result is identical to calling np.nonzero() on the original array. The default is False.
skip_single
:bool
, optional- If True, leaves array unchanged if it contains only a single non-zero segment. The default is False.
Returns
data
:1D array
ofints, floats,
orbools (m,)
or1D array
ofints (n,)
- Defragmented array, or indices of remaining non-zero segments if as_ind is True. Returns unchanged array if tol is None, array contains no segments, or array contains a single segment and skip_single is True.
def desegment(array, tol, rel_tol=None, replace=1, as_ind=False, with_edges=False)
-
Expand source code
def desegment(array, tol, rel_tol=None, replace=1, as_ind=False, with_edges=False): """ Fills gaps between non-zero segments if gap length is below tolerance. Applies bundle_inds() to array to identify individual gap segments. Parameters ---------- array : 1D array of ints, floats, or bools (m,) Array to desegment. Non-zero values are not considered during the desegmentation. tol : int or None Minimum tolerated length of a single gap segment. If None, immediately returns array without changes. rel_tol : float, optional If specified, overrides tol with a flexible criterion relative to the length of the largest gap segment in array. The default is None. replace : ints, float, or bool, optional Sets gap segments below tolerance to the given value. Can be used to specify a desired datatype should the insertion of value into array fail unexpectedly. The default is 1. as_ind : bool, optional If True, returns indices of merged non-zero segments in the desegmented array instead of the array itself. If replace is not non-zero, the result is identical to calling np.nonzero() on the original array. The default is False. with_edges : bool, optional If True, applies tolerance criterion to gap segments that include the first or last index of array. Else, omits these edge gaps from desegmentation. The default is False. Returns ------- data : 1D array of ints, floats, or bools (m,) or 1D array of ints (n,) Desegmented array, or indices of merged non-zero segments if as_ind is True. Returns unchanged array if tol is None or array contains no gap segments. """ # Silent skip: if tol is None: return array # Find segments and handle other skips: segments = bundle_inds(array == 0) if len(segments) == 0: print('WARNING: No unlabeled segments found for desegmentation.') return array # Handle edge cases: if not with_edges: # Omit gap at start: if segments[0][0] == 0: segments = segments[1:] # Omit gap at end: if segments[-1][-1] == len(array) - 1: segments = segments[:-1] # Segment lengths and flexible tolerance: len_segments = [len(segment) for segment in segments] if rel_tol is not None: tol = int(rel_tol * np.max(len_segments)) data = array.copy() # Apply tolerance criterion: for segment, n in zip(segments, len_segments): if n < tol: data[segment] = replace return np.nonzero(data)[0] if as_ind else data
Fills gaps between non-zero segments if gap length is below tolerance. Applies bundle_inds() to array to identify individual gap segments.
Parameters
array
:1D array
ofints, floats,
orbools (m,)
- Array to desegment. Non-zero values are not considered during the desegmentation.
tol
:int
orNone
- Minimum tolerated length of a single gap segment. If None, immediately returns array without changes.
rel_tol
:float
, optional- If specified, overrides tol with a flexible criterion relative to the length of the largest gap segment in array. The default is None.
replace
:ints, float,
orbool
, optional- Sets gap segments below tolerance to the given value. Can be used to specify a desired datatype should the insertion of value into array fail unexpectedly. The default is 1.
as_ind
:bool
, optional- If True, returns indices of merged non-zero segments in the desegmented array instead of the array itself. If replace is not non-zero, the result is identical to calling np.nonzero() on the original array. The default is False.
with_edges
:bool
, optional- If True, applies tolerance criterion to gap segments that include the first or last index of array. Else, omits these edge gaps from desegmentation. The default is False.
Returns
data
:1D array
ofints, floats,
orbools (m,)
or1D array
ofints (n,)
- Desegmented array, or indices of merged non-zero segments if as_ind is True. Returns unchanged array if tol is None or array contains no gap segments.
def label_songs(rate,
threshold=None,
norm=None,
features=None,
channels=None,
ref_channel=None,
global_ref=False,
wrap=False,
config=None)-
Expand source code
def label_songs(rate, threshold=None, norm=None, features=None, channels=None, ref_channel=None, global_ref=False, wrap=False, config=None): """ Identifies song events by thresholding the norm of the feature set. Accepts either the norm itself or the feature set to compute the norm. Can process a single or multiple channels. The applied threshold is relative to the channel-wise or global maximum of the feature norm, or to the maximum of a specified reference channel. Returns start and end time of each identified song segment to render the label information independent of the sampling rate. Parameters ---------- rate : float or int Sampling rate of the feature set and the corresponding norm in Hz. threshold : float or 1D array (m,) of floats Threshold to apply relative to the maximum of the feature norm. Scalars are applied to all target channels. Arrays must have a length matching the number of specified target channels. The default is None. norm : 1D array (t,) or 2D array (t, c) of floats, optional Pre-computed norm over the kernel axis of the feature set (could be any time-series to be labeled). First axis must be time. If 1D, expects a single channel. If 2D, expects several channels. Ignored if features is specified. The default is None. features : 2D array (t, k) or 3D array (t, k, c) of floats, optional If specified, the feature set to compute the norm from. First axis must be time, second axis must be kernels. Calls np.linalg.norm(axis=1) to condense the features into the default Frobenius norm. If 2D, expects a single channel. If 3D, expects several channels. Overrides norm if specified. The default is None. channels : int or 1D array (m,) of ints, optional For multi-channel data (2D norm or 3D features), specifies a subset of one or more target channels for labeling. If not specified, labels all channels. Ignored for single-channel data. The default is None. ref_channel : int, optional For multiple target channels, specifies a single channel whose maximum norm is used as reference for thresholding, overriding global_ref. Must be one of the specified target channels. The default is None. global_ref : bool, optional If True and ref_channel is None, labels multiple target channels using the global maximum across channels, else relative to the maximum of each channel. Ignored for single target channels. The default is True. wrap : bool, optional For single target channels, returns the array of edge times wrapped in a tuple to match multi-channel output format. The default is False. config : dict, optional Top-level parameter configuration in the format of configuration() that replaces all keyword arguments except 'rate', 'norm', 'features', and 'wrap' if specified. The default is None. Returns ------- edges : 2D array (n, 2) or tuple (m,) of 2D arrays (n, 2) of floats Edge times (start, end) of identified song events in seconds, one row per segment. Returns one array for each target channel. Raises ------ ValueError Breaks if both norm and features are None, or if norm is not 1D or 2D. """ # Input interpretation: if features is not None: norm = np.linalg.norm(features, axis=1) elif norm is None: raise ValueError('Either norm or features must be specified.') elif norm.ndim not in (1, 2): raise ValueError('Norm must be a 1D or 2D array.') if config is not None: threshold = config['label_thresh'] channels = config['label_channels'] ref_channel = config['label_ref'] global_ref = config['global_ref'] # Channel subset selection: if channels is not None and norm.ndim == 2: norm = norm[:, channels] # Single channel: if norm.ndim == 1: # Identify consecutive supra-threshold elements: segments = bundle_inds(norm >= threshold * norm.max()) # Convert to start/end time points: edges = np.zeros((len(segments), 2)) for i, segment in enumerate(segments): edges[i] = [segment[0], segment[-1]] return (edges / rate,) if wrap else edges / rate # Multi-channel labeling: reference = norm.max(axis=0) if ref_channel is not None: # Relative to maximum of single reference channel: reference = reference[np.searchsorted(channel, ref_channel)] elif global_ref: # Relative to global maximum: reference = reference.max() # Mark consecutive supra-threshold elements in individual channels: marked, n_segs = label_along_axis(norm >= threshold * reference, axis=0) edges = [[] for _ in range(norm.shape[1])] for i in range(1, n_segs + 1): # Get segment indices along both dimensions: segment, channel = np.nonzero(marked == i) # Assign edge elements to corresponding channel: edges[channel[0]].append([segment[0], segment[-1]]) # Convert to start/end time points: return tuple(np.array(edge) / rate for edge in edges)
Identifies song events by thresholding the norm of the feature set. Accepts either the norm itself or the feature set to compute the norm. Can process a single or multiple channels. The applied threshold is relative to the channel-wise or global maximum of the feature norm, or to the maximum of a specified reference channel. Returns start and end time of each identified song segment to render the label information independent of the sampling rate.
Parameters
rate
:float
orint
- Sampling rate of the feature set and the corresponding norm in Hz.
threshold
:float
or1D array (m,)
offloats
- Threshold to apply relative to the maximum of the feature norm. Scalars are applied to all target channels. Arrays must have a length matching the number of specified target channels. The default is None.
norm
:1D array (t,)
or2D array (t, c)
offloats
, optional- Pre-computed norm over the kernel axis of the feature set (could be any time-series to be labeled). First axis must be time. If 1D, expects a single channel. If 2D, expects several channels. Ignored if features is specified. The default is None.
features
:2D array (t, k)
or3D array (t, k, c)
offloats
, optional- If specified, the feature set to compute the norm from. First axis must be time, second axis must be kernels. Calls np.linalg.norm(axis=1) to condense the features into the default Frobenius norm. If 2D, expects a single channel. If 3D, expects several channels. Overrides norm if specified. The default is None.
channels
:int
or1D array (m,)
ofints
, optional- For multi-channel data (2D norm or 3D features), specifies a subset of one or more target channels for labeling. If not specified, labels all channels. Ignored for single-channel data. The default is None.
ref_channel
:int
, optional- For multiple target channels, specifies a single channel whose maximum norm is used as reference for thresholding, overriding global_ref. Must be one of the specified target channels. The default is None.
global_ref
:bool
, optional- If True and ref_channel is None, labels multiple target channels using the global maximum across channels, else relative to the maximum of each channel. Ignored for single target channels. The default is True.
wrap
:bool
, optional- For single target channels, returns the array of edge times wrapped in a tuple to match multi-channel output format. The default is False.
config
:dict
, optional- Top-level parameter configuration in the format of configuration() that replaces all keyword arguments except 'rate', 'norm', 'features', and 'wrap' if specified. The default is None.
Returns
edges
:2D array (n, 2)
ortuple (m,)
of2D arrays (n, 2)
offloats
- Edge times (start, end) of identified song events in seconds, one row per segment. Returns one array for each target channel.
Raises
ValueError
- Breaks if both norm and features are None, or if norm is not 1D or 2D.
def encoder(labels, path, paths, species_list=None)
-
Expand source code
def encoder(labels, path, paths, species_list=None): """ Tag datapoints in labels to indicate corresponding file and species. Used to identify specific segments within a larger appended dataset. Parameters ---------- labels : 1D array of floats or ints or bools (m,) Song labels for each datapoint (1 = song, 0 = no song/noise). Any other values are considered buffer zones and treated as unlabeled (0). If zero-array, skips species encoding even if spec_list is not None. path : str Path to the current file. Current species is determined by correspondence between path and entries of species_list. Format of species name must be consistent between path and spec_list. paths : list of str (n,) Paths to all included files. File tag is index of path in paths + 1. species_list : list of str (p,), optional Names of all included species. Species tag is index of current species (that matches path) in spec_list + 1. If not specified, skips species encoding even if labels is no zero-array. The default is None. Returns ------- file_code : 1D array of ints (m,) File tag for each datapoint in labels, indicating the position of the current file among all files in paths. Possible tag values are in the range [1, n]. species_code : 1D array of ints (m,) Species-specific song labels for each datapoint in labels, indicating the position of the current species among all species in species_list. If species_list is None, labels is a zero-array, or path contains "noise", the returned array is a zero-array of fitting length. Possible tag values are in the range [1, p]. """ # Encode file number: file_code = np.zeros(len(labels), dtype=int) + (paths.index(path) + 1) # Encode species-specific song labels: if species_list is None or 'noise' in path or (np.sum(labels) == 0): # Disabled species code/Empty labels/Noise file: species_code = np.zeros(len(labels), dtype=int) else: # Treat buffer values as unlabeled (0): species_code = (labels == 1).astype(int) # Identify species corresponding to current file: tag = [i for i, species in enumerate(species_list) if species in path] species_code *= np.array(tag) + 1 return file_code, species_code
Tag datapoints in labels to indicate corresponding file and species. Used to identify specific segments within a larger appended dataset.
Parameters
labels
:1D array
offloats
orints
orbools (m,)
- Song labels for each datapoint (1 = song, 0 = no song/noise). Any other values are considered buffer zones and treated as unlabeled (0). If zero-array, skips species encoding even if spec_list is not None.
path
:str
- Path to the current file. Current species is determined by correspondence between path and entries of species_list. Format of species name must be consistent between path and spec_list.
paths
:list
ofstr (n,)
- Paths to all included files. File tag is index of path in paths + 1.
species_list
:list
ofstr (p,)
, optional- Names of all included species. Species tag is index of current species (that matches path) in spec_list + 1. If not specified, skips species encoding even if labels is no zero-array. The default is None.
Returns
file_code
:1D array
ofints (m,)
- File tag for each datapoint in labels, indicating the position of the current file among all files in paths. Possible tag values are in the range [1, n].
species_code
:1D array
ofints (m,)
- Species-specific song labels for each datapoint in labels, indicating the position of the current species among all species in species_list. If species_list is None, labels is a zero-array, or path contains "noise", the returned array is a zero-array of fitting length. Possible tag values are in the range [1, p].
def buffer(labels, buff_value=0.0, start_out=0, start_in=0, end_in=0, end_out=0, config=None)
-
Expand source code
def buffer(labels, buff_value=0., start_out=0, start_in=0, end_in=0, end_out=0, config=None): """ Applies buffer zones around edges of labeled song segments. Buffer zones can be used to omit data from classifier training. Four possible buffer zones per segment: Inward and outward at segment start and end, respectively. Inward buffers are limited to datapoints of the labeled segment, while outward buffers are limited to the unlabeled datapoints between neighbouring segments (independent of the specified extents.) Parameters ---------- labels : 1D array of floats (m,) Song labels for each datapoint (1 = song, 0 = no song/noise). Any other non-zero values are treated as song labels. buff_value : float, optional Sets datapoints in buffer zones to the given value. May not be 1.0, which is the tag value to encode song segments! The default is 0.0, so that any noise segments are also interpreted as buffer zones during modification of training data by debuffer(). start_out : int or float, optional Outward buffer zone at the start of each segment, excluding the first index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0. start_in : int or float, optional Inward buffer zone at the start of each segment, including the first index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0. end_in : int or float, optional Inward buffer zone at the end of each segment, including the last index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0. end_out : int or float, optional Outward buffer zone at the end of each segment, excluding the last index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0. config : dict, optional Top-level parameter configuration in the format of configuration() that replaces all keyword arguments if specified. The default is None. Returns ------- buff_labels : 1D array of floats (m,) Buffered song labels for each datapoint (1 = song, 0 = no song/noise, buff_value = buffer zone). If all specified buffer extents are 0, returns the unchanged labels. """ # Input interpretation: if config is not None: buff_value = config['buff_value'] n_buff = config['n_buff'] else: n_buff = [start_out, start_in, end_in, end_out] # Skip if no buffers are requested: if not any(n_buff): return labels # Find labeled segments: buff_labels = labels.copy() segments = bundle_inds(labels) for i, segment in enumerate(segments): # Segment extent: start = segment[0] end = segment[-1] length = len(segment) # Length and boundaries of each of the four buffer zones in points: n = [int(np.ceil(n*length)) if type(n) == float else n for n in n_buff] edges = np.array([start - n[0], start + n[1], end - n[2], end + n[3]]) # Assert valid indices: edges[edges < 0] = 0 edges[edges > len(labels) - 1] = len(labels) - 1 # Assert inward buffers in segment: edges[1] = min(edges[1], end) edges[2] = max(edges[2], start) # Assert outward buffers not in nearby segments: if i > 0: edges[0] = max(edges[0], segments[i - 1][-1] + 1) if i < len(segments) - 1: edges[3] = min(edges[3], segments[i + 1][0] - 1) # Apply buffer zones to labels: buff_labels[edges[0]:edges[1]] = buff_value buff_labels[edges[2] + 1:edges[3] + 1] = buff_value return buff_labels
Applies buffer zones around edges of labeled song segments. Buffer zones can be used to omit data from classifier training. Four possible buffer zones per segment: Inward and outward at segment start and end, respectively. Inward buffers are limited to datapoints of the labeled segment, while outward buffers are limited to the unlabeled datapoints between neighbouring segments (independent of the specified extents.)
Parameters
labels
:1D array
offloats (m,)
- Song labels for each datapoint (1 = song, 0 = no song/noise). Any other non-zero values are treated as song labels.
buff_value
:float
, optional- Sets datapoints in buffer zones to the given value. May not be 1.0, which is the tag value to encode song segments! The default is 0.0, so that any noise segments are also interpreted as buffer zones during modification of training data by debuffer().
start_out
:int
orfloat
, optional- Outward buffer zone at the start of each segment, excluding the first index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0.
start_in
:int
orfloat
, optional- Inward buffer zone at the start of each segment, including the first index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0.
end_in
:int
orfloat
, optional- Inward buffer zone at the end of each segment, including the last index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0.
end_out
:int
orfloat
, optional- Outward buffer zone at the end of each segment, excluding the last index of the segment. If int, the buffer extent in points. If float, the buffer extent as a proportion of the segment length (at least one point). The default is 0.
config
:dict
, optional- Top-level parameter configuration in the format of configuration() that replaces all keyword arguments if specified. The default is None.
Returns
buff_labels
:1D array
offloats (m,)
- Buffered song labels for each datapoint (1 = song, 0 = no song/noise, buff_value = buffer zone). If all specified buffer extents are 0, returns the unchanged labels.
def debuffer(buff_labels, buff_value=0.0, learn_ind=None, mod_nolearn=True)
-
Expand source code
def debuffer(buff_labels, buff_value=0.0, learn_ind=None, mod_nolearn=True): """ Omits buffers from training data before it is shown to classifier. Modifications depend on the combination of chosen buffer extents and buff_value applied by buffer() as well as learn_ind and mod_nolearn: 1) Buffer extents determine if data is modified at all: If no buffer zones were applied, use full data (do not call this function in any case!). 2) buff_value determines which data is treated as buffers: If non-zero, omit explicitly declared buffer zones, keep noise. If zero, omit buffer zones plus all noise (use pure song data). 3) learn_ind determines interpretation of appended data: If unspecified, assume pure learn files (target songs + noise). If given, assume combination of learn and nolearn files (no-target songs + noise). Target songs and no-target songs require different class labels for classifier training. Buffers are omitted according to 2) in both cases. 4) mod_nolearn determines if learn files (target songs + noise) and nolearn files (no-target songs + noise) are modified differently: If unspecified, omit any buffers everywhere in the data. If given, omit buffers in learn files, preserve nolearn files. Always requires learn_ind for the creation of correct class labels. Buffers are omitted according to 2) in both cases. TARGET SONGS ARE ALWAYS LABELED AS 1, NO-TARGET SONGS AND NOISE AS 0. Parameters ---------- buff_labels : 1D array of floats (m,) Buffered song labels for each datapoint (1 = song, 0 = no song/noise, buff_value = buffer zones). Can also handle other song labels, as long as they are not the same as buff_value. If learn_ind is given, can also handle species-specific song labels in the style of encoder(). buff_value : float, optional Tag value that encodes buffer zones in buff_labels. The default is 0.0, so that any noise segments are also interpreted as buffer zones. learn_ind : 1D array of bools (m,), optional If specified, indicates portions of the data that contain target songs. Obligate to create correct class labels if buff_labels contains song segments of multiple species (learn + nolearn files). Else, assumes that all song segments are target songs (pure learn files). The default is None. mod_nolearn : bool, optional If True, omits buffers in learn files as indicated by learn_ind, while sparing nolearn files from any modifications. If False, omits buffers in entire data. Ignored if learn_ind is None. The default is True. Returns ------- train_labels : 1D array of floats (n,) Portions of buff_labels that are used for classifier training. All buffers are omitted according to the specified conditions. Target songs are always labeled as 1, no-target songs and noise as 0. ind : 1D array of bools (m,) Indeces of remaining datapoints in buff_labels after buffer omission. Can be used to select corresponding portions of feature data. """ train_labels = buff_labels.copy() if learn_ind is None: # Assume pure learn files, omit buffers everywhere: ind = (train_labels != buff_value) # Adjust training class labels: train_labels[ind & (train_labels != 0)] = 1 else: # Assume both learn and nolearn files: nolearn_ind = np.invert(learn_ind) if mod_nolearn: # Omit buffers everywhere: ind = (train_labels != buff_value) else: # Omit buffers in learn files only: ind = ((train_labels != buff_value) & learn_ind) | nolearn_ind # Adjust training class labels: train_labels[ind & learn_ind & (train_labels != 0)] = 1 train_labels[ind & nolearn_ind] = 0 return train_labels[ind], ind
Omits buffers from training data before it is shown to classifier. Modifications depend on the combination of chosen buffer extents and buff_value applied by buffer() as well as learn_ind and mod_nolearn:
1) Buffer extents determine if data is modified at all: If no buffer zones were applied, use full data (do not call this function in any case!). 2) buff_value determines which data is treated as buffers: If non-zero, omit explicitly declared buffer zones, keep noise. If zero, omit buffer zones plus all noise (use pure song data). 3) learn_ind determines interpretation of appended data: If unspecified, assume pure learn files (target songs + noise). If given, assume combination of learn and nolearn files (no-target songs + noise). Target songs and no-target songs require different class labels for classifier training. Buffers are omitted according to 2) in both cases. 4) mod_nolearn determines if learn files (target songs + noise) and nolearn files (no-target songs + noise) are modified differently: If unspecified, omit any buffers everywhere in the data. If given, omit buffers in learn files, preserve nolearn files. Always requires learn_ind for the creation of correct class labels. Buffers are omitted according to 2) in both cases. TARGET SONGS ARE ALWAYS LABELED AS 1, NO-TARGET SONGS AND NOISE AS 0.
Parameters
buff_labels
:1D array
offloats (m,)
- Buffered song labels for each datapoint (1 = song, 0 = no song/noise, buff_value = buffer zones). Can also handle other song labels, as long as they are not the same as buff_value. If learn_ind is given, can also handle species-specific song labels in the style of encoder().
buff_value
:float
, optional- Tag value that encodes buffer zones in buff_labels. The default is 0.0, so that any noise segments are also interpreted as buffer zones.
learn_ind
:1D array
ofbools (m,)
, optional- If specified, indicates portions of the data that contain target songs. Obligate to create correct class labels if buff_labels contains song segments of multiple species (learn + nolearn files). Else, assumes that all song segments are target songs (pure learn files). The default is None.
mod_nolearn
:bool
, optional- If True, omits buffers in learn files as indicated by learn_ind, while sparing nolearn files from any modifications. If False, omits buffers in entire data. Ignored if learn_ind is None. The default is True.
Returns
train_labels
:1D array
offloats (n,)
- Portions of buff_labels that are used for classifier training. All buffers are omitted according to the specified conditions. Target songs are always labeled as 1, no-target songs and noise as 0.
ind
:1D array
ofbools (m,)
- Indeces of remaining datapoints in buff_labels after buffer omission. Can be used to select corresponding portions of feature data.
def label_gui(signal, rate, edges=None, labels=None, fullscreen=True, spec_kwargs={}, **plot_kwargs)
-
Expand source code
def label_gui(signal, rate, edges=None, labels=None, fullscreen=True, spec_kwargs={}, **plot_kwargs): """ Simple matplotlib GUI for labeling segments of a time series signal. Consists of a line plot of the signal with optional spectrogram. Each label is represented by a pair of edge times and indicated as a patch that ranges from one edge to the other. Accepts pre-computed labels as either edge times or a binary label array. Mouse clicks can be used to place new edges and remove existing labels. Keys can be used to add new labels, to exit or restart the GUI, or move along the working history. Working history logs all instances of added and removed labels, which can then be undone or redone. While down the history, further adding or removing of labels will clear the "future" history branch. MOUSE INTERACTIONS: - primary (left): place 1st edge - secondary (right): place 2nd edge - tertiary (wheel): remove label KEY BINDINGS: - space: add label from 1st to 2nd edge - escape: exit GUI and return label edges - r: restart GUI with initial input arguments - arrow down: go back in working history - arrow up: go forward in working history - o/p: auto-toggle mouse interactivity Parameters ---------- signal : 1D array (m,) of floats or ints Time series signal used as reference for labeling. rate : float or int Sampling rate of the signal in Hz. edges : 2D array (n, 2) or list (n,) of lists (2,) of floats, optional If specified, edge times (start, end) of each initial label in seconds. The default is None. labels : 1D array (m,) of bools or floats or ints, optional If specified, binary array that indicates initial labels as segments of non-zero values. Must be sampled with the same rate as signal. Ignored if edges is specified. The default is None. fullscreen : bool, optional If True, opens the GUI in full-screen mode. The default is True. spec_kwargs : dict, optional If not empty, adds a spectrogram subplot to the GUI and passes the specified keyword arguments to the spectrogram() wrapper function to compute the frequency spectrum of the signal. The default is {}. **plot_kwargs : dict, optional Keyword arguments passed to plot_spectrogram() for displaying the spectrogram, balancing rendering speed and visual quality. May contain any of 'quick_render', 'f_resample', and 't_resample', as well as any keyword argument accepted by plt.pcolormesh() if quick_render=False, or plt.imshow() if quick_render=True. Ignored if spec_kwargs is empty. Returns ------- edges : 2D array (n, 2) of floats or empty 1D array Edge times (start, end) of each labeled segment in seconds. Returns an empty array if the GUI was exited without adding any labels. """ # Input interpretation: if edges is None and labels is not None: # Find consecutive non-zero elements: segments = bundle_inds(labels) # Convert and bundle start and end time of each segment: edges = [[seg[0] / rate, seg[-1] / rate] for seg in segments] input_edges = None elif isinstance(edges, np.ndarray): # Ensure simple mutable: edges = edges.tolist() input_edges = edges.copy() elif edges is None: # No pre-existing labels: edges, input_edges = [], [] else: # Retain original input: input_edges = edges.copy() plt.ion() # Initialize interface: n = 2 if spec_kwargs else 1 fig, axes = plt.subplots(n, 1, figsize=(16, 9), sharex=True, squeeze=False) axes = axes[:, 0].tolist() if fullscreen: # Optionally opening in full-screen mode: fig.canvas.manager.full_screen_toggle() # Prepare signal subplot variables: time = np.arange(signal.shape[0]) / rate signal_limits = np.array([signal.min(), signal.max()]) signal_limits += np.array([-1, 1]) * np.diff(signal_limits) * 0.1 patch_alpha = [0.5] # Plot signal trace: axes[0].plot(time, signal, 'k', lw=0.5) axes[0].set_xlim(time[0], time[-1]) axes[0].set_ylim(signal_limits) if signal_limits[0] < 0 < signal_limits[1]: # Add optional zero line: axes[0].axhline(0, color='k', lw=0.5, ls='dotted') if spec_kwargs: # Compute and plot spectrogram: f, _, _ = plot_spectrogram(signal, rate, axes[1], spec_kwargs, **plot_kwargs) axes[1].set_ylim(f[0], f[-1]) patch_alpha.append(0.25) # Prepare persistent variables: mouse = [True] # Enables GUI-specific mouse events patches = [[] for _ in range(n)] # Subplot-specific label patches lines = [[None, None] for _ in range(n)] # Subplot-specific temporary lines current_times = [None, None] # Currently registered 1st and 2nd edge time history = [] # Chronology of add/remove events [1/0, edges, [patches]] state_ind = [-1] # Current position along history retry = [False] # Flag for recursive GUI recall # Indicate any initial labels: for segment_edges in edges: for i, ax in enumerate(axes): patches[i].append(ax.axvspan(*segment_edges, fc='r', ec='k', alpha=patch_alpha[i], lw=1, picker=10, rasterized=True)) # Interactivity: def on_key(event): # Toggle mouse interactivity: if event.key in ['o', 'p']: mouse[0] = not mouse[0] return None # Exit and return results: elif event.key == 'escape': plt.close(fig) print('Finished labeling.') return None # Abort and start over: elif event.key == 'r': plt.close(fig) retry[0] = True print('Aborted labeling. Retrying...') return None # Turn registered edge times into a new label: elif event.key == ' ' and all(current_times): # Ensure correct temporal order: if current_times[0] > current_times[1]: current_times.reverse() # Indicate in interface: for i, ax in enumerate(axes): # Wipe temporary lines and reset: lines[i][0].remove(), lines[i][1].remove() lines[i][0], lines[i][1] = None, None # Replace with new patch from edge to edge: patches[i].append(ax.axvspan(*current_times, fc='r', ec='k', alpha=patch_alpha[i], lw=1, picker=10, rasterized=True)) print(f'Added {current_times[0]:.4f} - {current_times[1]:.4f}') # Log edge times and reset: edges.append(current_times.copy()) current_times[0], current_times[1] = None, None # Update history: if state_ind[0] < -1: # Clear "future" branch (later than current state): [history.pop() for _ in range(-1 - state_ind[0])] state_ind[0] = -1 # Remember add-event as new most recent history state: history.append([1, edges[-1], [patches[i][-1] for i in range(n)]]) # Update interface: fig.canvas.draw() return None # Go back in history: elif event.key == 'down': # Ignore if at beginning of history: if state_ind[0] == -len(history) - 1: return None state = history[state_ind[0]] if state[0]: # Undo add event: edges.remove(state[1]) for i, state_patch in enumerate(state[2]): patches[i].remove(state_patch) state_patch.remove() elif not state[0]: # Undo remove event: edges.append(state[1]) for i, ax in enumerate(axes): patches[i].append(state[2][i]) ax.add_patch(state[2][i]) # To earlier state: state_ind[0] -= 1 # Update interface: fig.canvas.draw() return None # Go forward in history: elif event.key == 'up': if state_ind[0] == -1: return None # To later state: state_ind[0] += 1 state = history[state_ind[0]] if state[0]: # Redo add event: edges.append(state[1]) for i, ax in enumerate(axes): patches[i].append(state[2][i]) ax.add_patch(state[2][i]) elif not state[0]: # Redo remove event: edges.remove(state[1]) for i, state_patch in enumerate(state[2]): patches[i].remove(state_patch) state_patch.remove() # Update interface: fig.canvas.draw() return None # Edge registration: def on_click(event): # Ignore suspended inputs, out-of-focus, or wrong button: if not mouse[0] or event.inaxes is None or event.button not in [1, 3]: return None # Decide between 1st and 2nd edge: ind = {1: 0, 3: 1}[event.button] # Register selected time point: current_times[ind] = event.xdata # Indicate in interface: for i, ax in enumerate(axes): if lines[i][ind]: # Wipe previous line: lines[i][ind].remove() # Replace with new temporary line at edge time: lines[i][ind] = ax.axvline(current_times[ind], color='r', lw=1) # Update interface: fig.canvas.draw() return None # Label removal: def on_pick(event): # Ignore suspended inputs or wrong button: if not mouse[0] or event.mouseevent.button != 2: return None # Identify label from selected patch: ax_ind = axes.index(event.artist.axes) label_ind = patches[ax_ind].index(event.artist) # Update history: if state_ind[0] < -1: # Clear "future" branch (later than current state): [history.pop() for _ in range(-1 - state_ind[0])] state_ind[0] = -1 # Remember remove-event as new most recent history state: history_patches = [patches[i][label_ind] for i in range(n)] history.append([0, edges[label_ind], history_patches]) # Wipe from interface: for i in range(len(axes)): patches[i][label_ind].remove() patches[i].pop(label_ind) print(f'Wiped {edges[label_ind][0]:.4f} - {edges[label_ind][1]:.4f}') # Unlog edge times: edges.pop(label_ind) # Update interface: fig.canvas.draw() return None # Establish interactivity: plt.connect('key_press_event', on_key) plt.connect('button_press_event', on_click) plt.connect('pick_event', on_pick) plt.ioff() plt.show() if retry[0]: # Recursive recall with unmodified initial input arguments: return label_gui(signal, rate, input_edges, labels, fullscreen, spec_kwargs, **plot_kwargs) # Return options: edges = np.array(edges) if edges.ndim == 2: # Return chronologically sorted: return edges[np.argsort(edges[:, 0]), :] # Return empty: return edges
Simple matplotlib GUI for labeling segments of a time series signal. Consists of a line plot of the signal with optional spectrogram. Each label is represented by a pair of edge times and indicated as a patch that ranges from one edge to the other. Accepts pre-computed labels as either edge times or a binary label array. Mouse clicks can be used to place new edges and remove existing labels. Keys can be used to add new labels, to exit or restart the GUI, or move along the working history. Working history logs all instances of added and removed labels, which can then be undone or redone. While down the history, further adding or removing of labels will clear the "future" history branch.
MOUSE INTERACTIONS: - primary (left): place 1st edge - secondary (right): place 2nd edge - tertiary (wheel): remove label KEY BINDINGS: - space: add label from 1st to 2nd edge - escape: exit GUI and return label edges - r: restart GUI with initial input arguments - arrow down: go back in working history - arrow up: go forward in working history - o/p: auto-toggle mouse interactivity
Parameters
signal
:1D array (m,)
offloats
orints
- Time series signal used as reference for labeling.
rate
:float
orint
- Sampling rate of the signal in Hz.
edges
:2D array (n, 2)
orlist (n,)
oflists (2,)
offloats
, optional- If specified, edge times (start, end) of each initial label in seconds. The default is None.
labels
:1D array (m,)
ofbools
orfloats
orints
, optional- If specified, binary array that indicates initial labels as segments of non-zero values. Must be sampled with the same rate as signal. Ignored if edges is specified. The default is None.
fullscreen
:bool
, optional- If True, opens the GUI in full-screen mode. The default is True.
spec_kwargs
:dict
, optional- If not empty, adds a spectrogram subplot to the GUI and passes the specified keyword arguments to the spectrogram() wrapper function to compute the frequency spectrum of the signal. The default is {}.
**plot_kwargs
:dict
, optional- Keyword arguments passed to plot_spectrogram() for displaying the spectrogram, balancing rendering speed and visual quality. May contain any of 'quick_render', 'f_resample', and 't_resample', as well as any keyword argument accepted by plt.pcolormesh() if quick_render=False, or plt.imshow() if quick_render=True. Ignored if spec_kwargs is empty.
Returns
edges
:2D array (n, 2)
offloats
orempty 1D array
- Edge times (start, end) of each labeled segment in seconds. Returns an empty array if the GUI was exited without adding any labels.