Commit 2432aa7c authored by Oliver Kirsebom's avatar Oliver Kirsebom
Browse files

Merge branch 'shore_in_with_dave' into 'development'

Shore in with dave

See merge request data_analytics_dal/packages/aural-features!1
parents 23adb98b e31c7730
......@@ -13,6 +13,7 @@ class Config:
self.local_km_window_seconds = 0.008
self.filter_n = 100
self.filter_min_hz = 50
self.always_isolate = False
@property
def filter_pad_samples( self ):
......@@ -64,6 +65,16 @@ class Config:
def filter_min_hz( self, f ):
self.__filter_min_hz = f
@property
def always_isolate( self ):
'''Always succeed at isolating a call. Young 6.3 discards data when the KM algorithm does not find the attack peak before the decay peak. This option forces the algorithm to always succeed by first finding the peak of the envelope, then finding the attack peak prior to the envelope peak, and the decay peak after the envelope peak.
'''
return self.__always_isolate
@always_isolate.setter
def always_isolate( self, b ):
self.__always_isolate = b
class IsolationFailed( Exception ):
def __init__( self ):
pass
......@@ -77,9 +88,10 @@ def extract( timeseries, fs, config ):
:return: A tuple of features, or raises IsolationFailed if the vocalization could not be properly isolated.
'''
isolated = young.isolate( timeseries, fs,
config.global_km_window_seconds,
config.filter_pad_samples )
isolate_f = young.forced_isolate if config.always_isolate else young.isolate
isolated = isolate_f( timeseries, fs,
config.global_km_window_seconds,
config.filter_pad_samples )
if not isolated.size: raise IsolationFailed
......
......@@ -19,8 +19,8 @@ def kliewer_mertins( squared_data, N ):
left = np.convolve( yy[:-N-1], kernel )
right = np.convolve( yy[N+1:], kernel )
Fattack = np.log( right / left ) * right
Fdecay = np.log( left / right ) * left
Fattack = np.log10( right / left ) * right
Fdecay = np.log10( left / right ) * left
return Fattack, Fdecay
......
......@@ -21,6 +21,9 @@ class Band( enum.IntEnum ):
def isolate( y, fs, global_km_window_seconds, pad ):
'''This function applies the 'modified' Kliewer-Mertins algorithm to isolate the beginning and end of a call as described in Young 6.3
The isolated call vector is returned with an extra pad samples on both ends to allow for subsequent filter stages' edge effects to be discarded.
:param y: Raw timeseries vector of a detection event
:param fs: Timeseries sample-rate in Hz
:param global_km_window_seconds: The global KM window size in seconds
......@@ -39,20 +42,61 @@ def isolate( y, fs, global_km_window_seconds, pad ):
N=int( fs * global_km_window_seconds )
Fattack, Fdecay = segment.kliewer_mertins( np.square( y ), N )
envelope = abs( scipy.signal.hilbert( y ))
# Search only within x[pad:-pad] so that we have extra padding for filter
# edge effects. This keeps the math simple, and is a safe assumption
# as the (implied) coarse detector will center the detected events.
attack_beg = np.argmax( Fattack[pad:-pad] ) + pad
peak = np.argmax( envelope[pad:-pad] ) + pad
decay_end = np.argmax( Fdecay[pad:-pad] ) + pad
if not attack_beg < peak or not peak < decay_end:
if not attack_beg < decay_end:
return np.array([])
return y[ attack_beg-pad:decay_end+pad ]
def forced_isolate( y, fs, global_km_window_seconds, pad ):
'''This function applies the 'modified' Kliewer-Mertins algorithm to isolate the beginning and end of a call as described in Young 6.3, but with the following modification. It will always find a call by first determining the envelope peak, then searching left of the peak for the maximum attack, and searching right of the peak for the maximum decay.
The isolated call vector is returned with an extra pad samples on both ends to allow for subsequent filter stages' edge effects to be discarded.
:param y: Raw timeseries vector of a detection event
:param fs: Timeseries sample-rate in Hz
:param global_km_window_seconds: The global KM window size in seconds
:param pad: The number extra samples to pad the isolated call with. This padding is later discarded after applying the filters.
:return: An isolated timeseries vector which is a subset of the vector y
'''
# Normalize so that max(abs(y)) == 32768
# This is somewhat arbitrary, but the original Young thesis does this to
# assign "temporary" physical values where 1 == 1uPa, 2 == 2uPa while
# adjusting for loudness. There may be some assumptions built into their
# C and alpha values, so we'll mimic that here.
y = np.asarray( y, 'float32' )
y /= max( abs( y )) / (2**15)
N=int( fs * global_km_window_seconds )
Fattack, Fdecay = segment.kliewer_mertins( np.square( y ), N )
# Search only within x[pad:-pad] so that we have extra padding for filter
# edge effects. This keeps the math simple, and is a safe assumption
# as the (implied) coarse detector will center the detected events.
# First find the envelope peak, leaving room for padding and
# ensuring at least one extra sample to satisfy attack<peak<decay
envelope = abs( scipy.signal.hilbert( y ))
bound = pad+1
peak = np.argmax( envelope[ bound:-bound ] ) + bound
# Search left for attack
attack_beg = np.argmax( Fattack[ pad:peak ] ) + pad
# Search right for decay
offset = peak+1
decay_end = np.argmax( Fdecay[ offset:-pad ] ) + offset
return y[ attack_beg-pad:decay_end+pad ]
def filter( padded_timeseries, pad, fs, FCs ):
'''This function pre-calculates a matrix of filter-bank vectors described in the enum Band. For each center-frequency in FCs, a gammatone filter is applied to the padded_timeseries. This function also calculates squared and hilbert-transformed vectors for convenience. Vectors are then trimmed by pad elements on both ends to discard filtering edge effects.
......
......@@ -26,6 +26,8 @@ parser.add_argument( '-l','--local-km-window-seconds', type=float,
help='Local KM window width in seconds [default=0.008s]',
default=0.008,
metavar='sec' )
parser.add_argument( '-a','--always-isolate', action='store_true',
help='Always succeed in isolating a call, using a modified algorithm which searches left and right from the envelope peak.' )
advanced = parser.add_argument_group( 'advanced' )
advanced.add_argument( '--pad-samples', metavar='N', type=int,
......@@ -52,7 +54,8 @@ for wavefile in wavefiles:
# Isolate, and add padding to the isolated call, which will be accounted
# for in the filtering stage
isolated = young.isolate( y, fs, args.global_km_window_seconds, pad )
isolate_f = young.forced_isolate if args.always_isolate else young.isolate
isolated = isolate_f( y, fs, args.global_km_window_seconds, pad )
if not isolated.size:
print( 'Skipping {}, cannot isolate attack/decay'.format( wavefile ))
continue
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment