diff --git a/PyHa/FG_BG_sep/utils.py b/PyHa/FG_BG_sep/utils.py
new file mode 100644
index 00000000..05097a2f
--- /dev/null
+++ b/PyHa/FG_BG_sep/utils.py
@@ -0,0 +1,178 @@
+import librosa
+import numpy as np
+import scipy.signal as scipy_signal
+from scipy import ndimage
+
+def perform_stft(SIGNAL, SAMPLE_RATE=44100):
+ """
+ Function that's main purpose is for reverse-engineering the birdnet FG-BG separation technique
+ SIGNAL (list, np.ndarray)
+ - Audio Signal the STFT is being performed on
+ SAMPLE_RATE (int)
+ - Nyquist sample rate to load the clip in as
+
+ returns:
+ - floating point value that is a ratio between the length of the clip and the length of the x-axis of the spectrogram
+ - Numpy array representing the normalized magnitude stft of the clip from clip_path
+ """
+
+ assert isinstance(SIGNAL, list) or isinstance(SIGNAL, np.ndarray)
+ assert isinstance(SAMPLE_RATE, int)
+ assert SAMPLE_RATE > 0
+
+ # parameters set by "Audio Based Bird Species Identification using Deep Learning Techniques"
+ window_size = 512
+ overlap_size = int(window_size*0.75)
+ f,t,z = scipy_signal.stft(SIGNAL,fs=SAMPLE_RATE,window=np.hanning(window_size),noverlap=overlap_size,nperseg=window_size)
+ # normalizing [0,1]
+ z = np.abs(z)
+ z = z/np.max(z)
+ clip_stft_time_ratio = len(SIGNAL)/z.shape[1]
+ return clip_stft_time_ratio, z
+
+def calculate_medians(stft):
+ """
+ Function that computes the frequency and temporal medians of a 2D stft spectrogram.
+ Used in binary thresholding for FG-BG separation
+ stft (ndarray)
+ - numpy array of spectrogram being processed
+ returns:
+ - median values of each spectrogram column (time medians)
+ - median values of each spectrogram row (frequency medians)
+ """
+ assert isinstance(stft,np.ndarray)
+
+ freq_medians = np.median(stft,axis=1)
+ time_medians = np.median(stft,axis=0)
+
+ return time_medians, freq_medians
+
+def binary_thresholding(stft, time_medians, freq_medians, multiplier_treshold=3.0):
+ """
+ Primary Foreground-background separation step used in BirdNET.
+ stft (ndarray)
+ - numpy array of spectrogram being processed
+ time_medians (ndarray)
+ - vector of medians wrt time of stft
+ freq_medians (ndarray)
+ - vector of medians wrt frequency of stft
+ multiplier_threshold (int, float)
+ - default = 3.0
+ - a constant that is multiplied by both the time and frequency medians to decide
+ whether or not a pixel is foreground or not
+ returns:
+ - binary ndarray same size as stft that contains 1's for foreground and 0's for background
+
+ """
+
+ assert isinstance(stft, np.ndarray)
+ assert isinstance(time_medians, np.ndarray)
+ assert isinstance(freq_medians, np.ndarray)
+ assert isinstance(multiplier_treshold, float) or isinstance(multiplier_treshold, int)
+ assert multiplier_treshold > 0
+
+ binary_mask_time = np.zeros(stft.shape)
+ binary_mask_freq = np.zeros(stft.shape)
+
+ # building time mask
+ for column in range(stft.shape[1]):
+ binary_mask_time[:,column] = stft[:,column] >= multiplier_treshold*time_medians[column]
+
+ # building frequency mask
+ for row in range(stft.shape[0]):
+ binary_mask_freq[row,:] = stft[row,:] >= multiplier_treshold*freq_medians[row]
+
+
+ # performing a element-wise and operation
+ return (binary_mask_freq*binary_mask_time).astype(np.uint8)
+
+def binary_morph_opening(binary_stft, kernel_size=4):
+ """
+ Function that performs the binary morphological and followed by an or operation, commonly referred to
+ as erosion and dilation respectively. Called an opening operation to people familiar with image processing
+
+ binary_stft (ndarray)
+ - foreground (high power) pixels represented as 1, background (lower power) represented as 0.
+ kernel_shape (int)
+ - defines the dimensions of the 2D binary morph kernel.
+ returns:
+ - binary stft image after a binary morphological opening operation determined by the kernel shape
+ """
+
+ assert isinstance(binary_stft, np.ndarray)
+ assert isinstance(kernel_size, int)
+ assert kernel_size > 0
+
+ kernel = np.ones( (kernel_size, kernel_size), np.uint8)
+
+ erode = ndimage.binary_erosion(binary_stft, kernel, iterations=1)
+ dilate = ndimage.binary_dilation(erode, kernel, iterations=1)
+
+ return dilate.astype(np.uint8)
+
+
+def temporal_thresholding(opened_binary_stft):
+ """
+ Function that converts the 2D binary thresholded stft into a temporal indicator vector
+
+ opened_binary_stft (ndarray)
+ - binary foreground-background separated stft
+ returns:
+ - binary temporal indicator vector that signifies the temporal components with high power
+ """
+ time_axis_sum = np.sum(opened_binary_stft, axis=0)
+ indicator_vector = time_axis_sum > 0
+ return indicator_vector.astype(np.uint8)
+
+def indicator_vector_processing(indicator_vector, kernel_size=4):
+ """
+ Function that performs additional dilations to the temporal indicator vector, expands on smaller relevant high-power sections
+
+ indicator_vector (ndarray)
+ - Numpy binary vector indicating high power temporal regions from the STFT
+ kernel_size (int)
+ - default: 4
+ - determines the length of the kernel that performs the dilation (1, kernel_size)
+ returns:
+ - indicator vector that has been subjected to 2 binary morphological dilation (or) operations based on 1D kernel
+ """
+ assert isinstance(indicator_vector, np.ndarray)
+ assert isinstance(kernel_size, int)
+ assert kernel_size > 0
+
+ kernel = np.ones((1, kernel_size), np.uint8)
+ dilate = ndimage.binary_dilation(indicator_vector.reshape((1,indicator_vector.shape[0])), kernel, iterations=2)
+
+ return dilate.astype(np.uint8)
+
+
+def FG_BG_local_score_arr(SIGNAL, isolation_parameters, normalized_sample_rate):
+ """
+ Function that reverse-engineers that uses the BirdNET Signal-to-noise-ratio technique to build local score arrays out of audio clips
+
+ SIGNAL (list, np.ndarray)
+ - Audio Signal the STFT is being performed on
+ SAMPLE_RATE (int)
+ - Nyquist sampling rate at which to process the audio clip
+ returns:
+ - ratio between the length of the audio clip and the stft time axis
+ - Numpy array of the local score array derived from median thresholding
+ """
+ assert isinstance(SIGNAL, list) or isinstance(SIGNAL, np.ndarray)
+ assert isinstance(normalized_sample_rate, int)
+
+ time_ratio, stft = perform_stft(SIGNAL, normalized_sample_rate)
+ time_medians, freq_medians = calculate_medians(stft)
+ binary_stft = binary_thresholding(stft, time_medians, freq_medians, isolation_parameters["power_threshold"])
+ opened_binary_stft = binary_morph_opening(binary_stft, isolation_parameters["kernel_size"])
+ temporal_indicator_vector = temporal_thresholding(opened_binary_stft)
+ dilated_indicator_vector = indicator_vector_processing(temporal_indicator_vector, isolation_parameters["kernel_size"])
+
+ return time_ratio, dilated_indicator_vector.reshape((dilated_indicator_vector.shape[1],))
+
+
+
+# sanity check
+#x = np.array([0,1,1,1,1,1,0]).reshape((1,7))
+#print(x)
+#print(indicator_vector_processing(x))
\ No newline at end of file
diff --git a/PyHa/IsoAutio.py b/PyHa/IsoAutio.py
index 732e79d9..2c28f729 100644
--- a/PyHa/IsoAutio.py
+++ b/PyHa/IsoAutio.py
@@ -1,8 +1,14 @@
+from audioop import mul
+from unicodedata import normalize
+from unittest.mock import Base
from .birdnet_lite.analyze import analyze
from .microfaune_package.microfaune.detection import RNNDetector
from .microfaune_package.microfaune import audio
from .tweetynet_package.tweetynet.TweetyNetModel import TweetyNetModel
from .tweetynet_package.tweetynet.Load_data_functions import compute_features, predictions_to_kaleidoscope
+from .FG_BG_sep.utils import FG_BG_local_score_arr
+from .template_matching.utils import filter, butter_bandpass, generate_specgram, template_matching_local_score_arr
+
import os
import torch
import librosa
@@ -28,7 +34,7 @@ def checkVerbose(
- Python Dictionary that controls the various label creation
techniques.
"""
- assert isinstance(errorMessage,str)
+ #assert isinstance(errorMessage,str)
assert isinstance(isolation_parameters,dict)
assert 'verbose' in isolation_parameters.keys()
@@ -119,6 +125,36 @@ def build_isolation_parameters_microfaune(
return isolation_parameters
+def write_confidence(local_score_arr, automated_labels_df):
+ """
+ Function that adds a new column to a clip dataframe that has had automated labels generated.
+ Goes through all of the annotations and adding to said row a confidence metric based on the
+ maximum value of said annotation.
+
+ Args:
+ local_score_arr (np.ndarray or list of floats)
+ - Array of small predictions of bird presence
+ automated_labels_df (pd.DataFrame)
+ - labels derived from the local_score_arr from the def isolate() method for the "IN FILE"
+ column clip
+ returns:
+ Pandas DataFrame with an additional column of the confidence scores from the local score array
+ """
+ assert isinstance(local_score_arr, np.ndarray) or isinstance(local_score_arr, list)
+ assert isinstance(automated_labels_df, pd.DataFrame)
+ assert len(automated_labels_df) > 0
+
+ time_ratio = len(local_score_arr)/automated_labels_df["CLIP LENGTH"][0]
+ confidences = []
+ for row in automated_labels_df.index:
+ start_ndx = int(automated_labels_df["OFFSET"][row] * time_ratio)
+ end_ndx = start_ndx + int(automated_labels_df["DURATION"][row] * time_ratio)
+ cur_confidence = np.max(local_score_arr[start_ndx:end_ndx])
+ confidences.append(cur_confidence)
+
+ automated_labels_df["CONFIDENCE"] = confidences
+ return automated_labels_df
+
def isolate(
local_scores,
@@ -172,7 +208,6 @@ def isolate(
assert "technique" in dict.fromkeys(isolation_parameters)
potential_isolation_techniques = {"simple","steinberg","stack","chunk"}
assert isolation_parameters["technique"] in potential_isolation_techniques
-
# normalize the local scores so that the max value is 1.
#if normalize_local_scores:
# local_scores_max = max(local_scores)
@@ -220,6 +255,10 @@ def isolate(
filename,
isolation_parameters,
manual_id=manual_id)
+
+ if "write_confidence" in isolation_parameters.keys():
+ if isolation_parameters["write_confidence"]:
+ isolation_df = write_confidence(local_scores, isolation_df)
return isolation_df
@@ -249,7 +288,7 @@ def threshold(local_scores, isolation_parameters):
assert isinstance(local_scores,np.ndarray)
assert isinstance(isolation_parameters,dict)
- potential_threshold_types = {"median","mean","standard deviation","threshold_const"}
+ potential_threshold_types = {"median","mean","standard deviation","pure"}
assert isolation_parameters["threshold_type"] in potential_threshold_types
@@ -350,9 +389,11 @@ def steinberg_isolate(
thresh = threshold(local_scores, isolation_parameters)
# how many samples one local score represents
samples_per_score = len(SIGNAL) // len(local_scores)
-
+ threshold_min = 0
+ if "threshold_min" in isolation_parameters.keys():
+ threshold_min = isolation_parameters["threshold_min"]
# Calculating local scores that are at or above threshold
- thresh_scores = local_scores >= max(thresh, isolation_parameters["threshold_min"])
+ thresh_scores = local_scores >= max(thresh, threshold_min)
# if statement to check if window size is smaller than time between two local scores
# (as a safeguard against problems that can occur)
@@ -506,7 +547,7 @@ def simple_isolate(
time_per_score = samples_per_score / SAMPLE_RATE
# Calculating local scores that are at or above threshold
- thresh_scores = local_scores >= max(thresh, isolation_parameters["threshold_min"])
+ thresh_scores = local_scores >= max(thresh, threshold_min)
# Set up to find the starts and ends of clips
thresh_scores = np.append(thresh_scores, [0])
@@ -618,7 +659,7 @@ def stack_isolate(
time_per_score = samples_per_score / SAMPLE_RATE
# Calculating local scores that are at or above threshold
- thresh_scores = local_scores >= max(thresh, isolation_parameters["threshold_min"])
+ thresh_scores = local_scores >= max(thresh, threshold_min)
# Set up to find the starts and ends of clips
thresh_scores = np.append(thresh_scores, [0])
@@ -777,7 +818,7 @@ def chunk_isolate(
chunked_scores = np.array(list(map(np.amax, np.split(local_scores, chunk_starts))))
# Finds which chunks are above threshold, and creates indices based on that
- thresh_scores = chunked_scores >= max(thresh, isolation_parameters["threshold_min"])
+ thresh_scores = chunked_scores >= max(thresh, threshold_min)
chunk_indices = np.where(thresh_scores == 1)[0]
# Assigns offset values based on float values of the starts
@@ -1143,6 +1184,234 @@ def generate_automated_labels_tweetynet(
return annotations
+
+def generate_automated_labels_FG_BG_separation(
+ audio_dir,
+ isolation_parameters,
+ manual_id="foreground",
+ normalized_sample_rate=44100):
+ """
+ Function that reverse-engineers the approach to foreground-background separation deployed by BirdNET:
+ https://www.sciencedirect.com/science/article/pii/S1574954121000273
+ The technique is more specifically described in this paper:
+ https://www.semanticscholar.org/paper/Audio-Based-Bird-Species-Identification-using-Deep-Sprengel-Jaggi/42ffd303b6a8373300a965da4327439575d23131
+ The algorith goes:
+ 1. Compute the STFT with a hanning window of length 512, with 75% overlap
+ 2. Take the absolute value of the STFT
+ 3. Normalize [0,1] by dividing by the maximum value
+ 4. Compute the median for every row and column of the Normalized STFT
+ 5. Construct Binary Mask
+ For every pixel in the Normalized STFT
+ i. if (pixel > 3*row_median) and (pixel > 3*column_median) : set to 1
+ else : set to 0
+ 6. Apply Binary Morphology Opening Operation with 4x4 square kernel of 1's
+ i. Apply erosion (nested 2D AND operation) with kernel
+ ii. Apply dilation (nested 2D OR operation) with kernel
+ 7. Convert "Opened" binary mask to Time Indicator Vector (binary local-score array)
+ i. Compute the sum of each column (creating a vector of said sums)
+ ii. if val in vector >= 1 : set to 1
+ 8. Apply dilation two times on Time Indicator Vector with 4x1 kernel
+ i. Note that this is similar to a convolution operation where the kernel is flipped,
+ so, a 1x4 kernel will end up actually being applied to the indicator vector
+ 9. In the paper, they would then apply what we call the "simple" isolation with a threshold of 0.99.
+
+ audio_dir (string)
+ - Path to directory with audio files.
+
+ isolation_parameters (dict)
+ - Python Dictionary that controls the various label creation
+ techniques.
+
+ manual_id (string)
+ - controls the name of the class written to the pandas dataframe.
+ - default: "foreground"
+
+ normalized_sample_rate (int)
+ - Sampling rate that the audio files should all be normalized to.
+
+ Returns:
+ Dataframe of automated labels for the audio clips in audio_dir.
+ """
+
+ logger = logging.getLogger("Foreground-Background Separation Autogenerated Labels")
+ assert isinstance(audio_dir, str)
+ assert isinstance(isolation_parameters, dict)
+ assert isinstance(manual_id, str)
+ assert isinstance(normalized_sample_rate, int)
+ assert normalized_sample_rate > 0
+
+ # initialize annotations dataframe
+ annotations = pd.DataFrame()
+
+ # looping through the folder
+ for audio_file in os.listdir(audio_dir):
+ # skip directories
+ if os.path.isdir(audio_dir + audio_file):
+ continue
+ # loading in the audio clip
+ try:
+ SIGNAL, SAMPLE_RATE = librosa.load(os.path.join(audio_dir, audio_file), sr=normalized_sample_rate, mono=True)
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException:
+ checkVerbose("Failed to load " + audio_file, isolation_parameters)
+ continue
+
+ # generating local score array from clip
+ try:
+ time_ratio, local_score_arr = FG_BG_local_score_arr(SIGNAL,
+ isolation_parameters,
+ SAMPLE_RATE)
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException:
+ checkVerbose("Failed to collect local score array of " + audio_file, isolation_parameters)
+ continue
+
+ # passing through isolation technique
+ try:
+ new_entry = isolate(
+ local_score_arr,
+ SIGNAL,
+ SAMPLE_RATE,
+ audio_dir,
+ audio_file,
+ isolation_parameters,
+ manual_id=manual_id,
+ )
+ if annotations.empty:
+ annotations = new_entry
+ else:
+ annotations = pd.concat([annotations, new_entry])
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException as e:
+ checkVerbose(e, isolation_parameters)
+ checkVerbose("Error in isolating bird calls from " + audio_file, isolation_parameters)
+ continue
+
+ annotations.reset_index(inplace=True, drop=True)
+ return annotations
+
+def generate_automated_labels_template_matching(
+ audio_dir,
+ isolation_parameters,
+ manual_id="template",
+ normalized_sample_rate=44100):
+ """
+
+
+ audio_dir (string)
+ - Path to directory with audio files.
+
+ isolation_parameters (dict)
+ - Python Dictionary that controls the various label creation
+ techniques.
+
+ manual_id (string)
+ - controls the name of the class written to the pandas dataframe.
+ - default: "template"
+
+ normalized_sample_rate (int)
+ - Sampling rate that the audio files should all be normalized to.
+
+ Returns:
+ Dataframe of automated labels for the audio clips in audio_dir.
+ """
+
+ logger = logging.getLogger("Template Matching Autogenerated Labels")
+ assert isinstance(audio_dir, str)
+ assert isinstance(isolation_parameters, dict)
+ assert isinstance(manual_id, str)
+ assert isinstance(normalized_sample_rate, int)
+ assert normalized_sample_rate > 0
+ bandpass = False
+ b = None
+ a = None
+ if "cutoff_freq_low" in isolation_parameters.keys() and "cutoff_freq_high" in isolation_parameters.keys():
+ bandpass = True
+ assert isinstance(isolation_parameters["cutoff_freq_low"], int)
+ assert isinstance(isolation_parameters["cutoff_freq_high"], int)
+ assert isolation_parameters["cutoff_freq_low"] > 0 and isolation_parameters["cutoff_freq_high"] > 0
+ assert isolation_parameters["cutoff_freq_high"] > isolation_parameters["cutoff_freq_low"]
+ assert isolation_parameters["cutoff_freq_high"] <= int(0.5*normalized_sample_rate)
+
+ # initialize annotations dataframe
+ annotations = pd.DataFrame()
+
+ # processing the template clip
+ try:
+ # loading the template signal
+ TEMPLATE, SAMPLE_RATE = librosa.load(isolation_parameters["template_path"], sr=normalized_sample_rate, mono=True)
+ if bandpass:
+ b, a = butter_bandpass(isolation_parameters["cutoff_freq_low"], isolation_parameters["cutoff_freq_high"], SAMPLE_RATE)
+ TEMPLATE = filter(TEMPLATE, b, a)
+
+ TEMPLATE_spec = generate_specgram(TEMPLATE, SAMPLE_RATE)
+ TEMPLATE_mean = np.mean(TEMPLATE_spec)
+
+ TEMPLATE_std_dev = np.std(TEMPLATE_spec)
+ TEMPLATE_spec -= TEMPLATE_mean
+ n = TEMPLATE_spec.shape[0] * TEMPLATE_spec.shape[1]
+
+
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException:
+ checkVerbose("Failed to load and process template " + isolation_parameters["template_path"], isolation_parameters)
+ exit("Can't do template matching without a template")
+
+ # looping through the clips to process
+ for audio_file in os.listdir(audio_dir):
+ # skip directories
+ if os.path.isdir(audio_dir + audio_file):
+ continue
+ # loading in the audio clip
+ try:
+ SIGNAL, SAMPLE_RATE = librosa.load(os.path.join(audio_dir, audio_file), sr=normalized_sample_rate, mono=True)
+ if bandpass:
+ SIGNAL = filter(SIGNAL, b, a)
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException:
+ checkVerbose("Failed to load " + audio_file, isolation_parameters)
+ continue
+
+ # generating local score array from clip
+ try:
+ local_score_arr = template_matching_local_score_arr(SIGNAL, SAMPLE_RATE, TEMPLATE_spec, n, TEMPLATE_std_dev)
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException:
+ checkVerbose("Failed to collect local score array of " + audio_file, isolation_parameters)
+ continue
+
+ # passing through isolation technique
+ try:
+ new_entry = isolate(
+ local_score_arr,
+ SIGNAL,
+ SAMPLE_RATE,
+ audio_dir,
+ audio_file,
+ isolation_parameters,
+ manual_id=manual_id,
+ )
+ if annotations.empty:
+ annotations = new_entry
+ else:
+ annotations = pd.concat([annotations, new_entry])
+ except KeyboardInterrupt:
+ exit("Keyboard Interrupt")
+ except BaseException as e:
+ checkVerbose(e, isolation_parameters)
+ checkVerbose("Error in isolating bird calls from " + audio_file, isolation_parameters)
+ continue
+
+ annotations.reset_index(inplace=True, drop=True)
+ return annotations
+
+
def generate_automated_labels(
audio_dir,
isolation_parameters,
@@ -1204,7 +1473,8 @@ def generate_automated_labels(
keys_to_delete = ['model', 'technique', 'threshold_type',
'threshold_const', 'chunk_size']
for key in keys_to_delete:
- birdnet_parameters.pop(key, None)
+ if key in birdnet_parameters.keys():
+ birdnet_parameters.pop(key, None)
annotations = generate_automated_labels_birdnet(
audio_dir, birdnet_parameters)
elif(isolation_parameters['model'] == 'tweetynet'):
@@ -1215,6 +1485,20 @@ def generate_automated_labels(
weight_path=weight_path,
normalized_sample_rate=normalized_sample_rate,
normalize_local_scores=normalize_local_scores)
+ elif(isolation_parameters["model"]=='fg_bg_dsp_sep'):
+ annotations = generate_automated_labels_FG_BG_separation(
+ audio_dir=audio_dir,
+ isolation_parameters=isolation_parameters,
+ manual_id=manual_id,
+ normalized_sample_rate=normalized_sample_rate
+ )
+ elif (isolation_parameters["model"]=="template_matching"):
+ annotations = generate_automated_labels_template_matching(
+ audio_dir=audio_dir,
+ isolation_parameters=isolation_parameters,
+ manual_id=manual_id,
+ normalized_sample_rate=normalized_sample_rate
+ )
else:
# print("{model_name} model does not exist"\
# .format(model_name=isolation_parameters["model"]))
diff --git a/PyHa/annotation_post_processing.py b/PyHa/annotation_post_processing.py
index 1f1e7041..fa2ac856 100644
--- a/PyHa/annotation_post_processing.py
+++ b/PyHa/annotation_post_processing.py
@@ -15,17 +15,23 @@ def annotation_chunker(kaleidoscope_df, chunk_length):
kaleidoscope_df (Dataframe)
- Dataframe of annotations in kaleidoscope format
- chunk_length (int)
+ chunk_length (int, float)
- duration to set all annotation chunks
Returns:
Dataframe of labels with chunk_length duration
(elements in "OFFSET" are divisible by chunk_length).
"""
-
+ assert isinstance(kaleidoscope_df, pd.DataFrame)
+ assert isinstance(chunk_length, int) or isinstance(chunk_length, float)
+ assert chunk_length > 0
#Init list of clips to cycle through and output dataframe
clips = kaleidoscope_df["IN FILE"].unique()
df_columns = {'IN FILE' :'str', 'CLIP LENGTH' : 'float64', 'CHANNEL' : 'int64', 'OFFSET' : 'float64',
'DURATION' : 'float64', 'SAMPLE RATE' : 'int64','MANUAL ID' : 'str'}
+ set_confidence = False
+ if "CONFIDENCE" in kaleidoscope_df.keys():
+ df_columns["CONFIDENCE"] = 'float64'
+ set_confidence = True
output_df = pd.DataFrame({c: pd.Series(dtype=t) for c, t in df_columns.items()})
# going through each clip
@@ -57,14 +63,18 @@ def annotation_chunker(kaleidoscope_df, chunk_length):
1000,
0))
# Placing the label relative to the clip
- human_arr[minval:maxval] = 1
+ if set_confidence:
+ human_arr[minval:maxval] = species_df["CONFIDENCE"][annotation]
+ else:
+ human_arr[minval:maxval] = 1
# performing the chunk isolation technique on the human array
for index in range(potential_annotation_count):
chunk_start = index * (chunk_length*1000)
chunk_end = min((index+1)*chunk_length*1000,arr_len)
chunk = human_arr[int(chunk_start):int(chunk_end)]
- if max(chunk) >= 0.5:
+ chunk_max = max(chunk)
+ if chunk_max > 1e-4:
row = pd.DataFrame(index = [0])
annotation_start = chunk_start / 1000
#updating the dictionary
@@ -75,5 +85,7 @@ def annotation_chunker(kaleidoscope_df, chunk_length):
row["SAMPLE RATE"] = sr
row["MANUAL ID"] = bird
row["CHANNEL"] = 0
+ if set_confidence:
+ row["CONFIDENCE"] = chunk_max
output_df = pd.concat([output_df,row], ignore_index=True)
return output_df
\ No newline at end of file
diff --git a/PyHa/template_matching/utils.py b/PyHa/template_matching/utils.py
new file mode 100644
index 00000000..8d1d4084
--- /dev/null
+++ b/PyHa/template_matching/utils.py
@@ -0,0 +1,81 @@
+import librosa
+from scipy.signal import butter, lfilter, stft
+import numpy as np
+
+
+def generate_specgram(SIGNAL, SAMPLE_RATE):
+ """
+ Generates a magnitude stft spectrogram normalized [0,1] for the sake of template matching
+ SIGNAL (ndarray)
+ - Audio signal of which the stft is performed
+ SAMPLE_RATE (int)
+ - rate at which the audio signal was sampled at
+ returns:
+ - 2D numpy array representing the stft of the signal using a window length of 1024 and 50% overlap
+ """
+ assert isinstance(SIGNAL, np.ndarray)
+ assert isinstance(SAMPLE_RATE, int)
+ assert SAMPLE_RATE > 0
+
+ window_len = 1024
+ noverlap = 512
+ nsperseg = 1024
+ f, t, SIGNAL_stft = stft(SIGNAL, fs=SAMPLE_RATE, window=np.hanning(window_len), noverlap=noverlap, nperseg=nsperseg)
+ SIGNAL_stft_mag = np.abs(SIGNAL_stft)
+ output = SIGNAL_stft_mag/np.max(SIGNAL_stft_mag)
+ return output
+
+def butter_bandpass(lowcut, highcut, fs, order=5):
+ """
+
+ """
+ nyq = 0.5 * fs
+ low = lowcut / nyq
+ high = highcut / nyq
+ b, a = butter(order, [low, high], btype='band')
+ return b, a
+
+def filter(data, b, a):
+ return lfilter(b, a, data)
+
+def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
+ b, a = butter_bandpass(lowcut, highcut, fs, order=order)
+ y = filter(b, a, data)
+ return y
+
+
+def template_matching_local_score_arr(SIGNAL, SAMPLE_RATE, template_spec, n, template_std_dev):
+ assert isinstance(SIGNAL, np.ndarray)
+ assert isinstance(SAMPLE_RATE, int)
+ assert isinstance(template_spec, np.ndarray)
+ signal_spec = generate_specgram(SIGNAL, SAMPLE_RATE)
+
+ assert signal_spec.shape[0] == template_spec.shape[0]
+ padded_signal = np.zeros((signal_spec.shape[0], signal_spec.shape[1] + 2 * template_spec.shape[1]))
+ start_ndx = template_spec.shape[1]
+ end_ndx = start_ndx + signal_spec.shape[1]
+ padded_signal[0:signal_spec.shape[0],start_ndx:end_ndx] = signal_spec
+ local_score_arr = np.zeros((signal_spec.shape[1],))
+ local_score_ndx = 0
+ for ndx in range(start_ndx, end_ndx):
+ # handling even and odd template cases
+ if template_spec.shape[1] % 2 == 1:
+ active_window = padded_signal[:, (ndx-template_spec.shape[1]//2)-1:ndx+template_spec.shape[1]//2]
+ else:
+ active_window = padded_signal[:, (ndx-template_spec.shape[1]//2):ndx+template_spec.shape[1]//2]
+ active_window_mean = np.mean(active_window)
+ active_window -= active_window_mean
+ active_window_std = np.std(active_window)
+ std_dev_product = template_std_dev*active_window_std
+ if std_dev_product == 0:
+ local_score_arr[local_score_ndx] = 0
+ local_score_ndx += 1
+ continue
+ #print("STD DEV PRODUCT", std_dev_product)
+ product = active_window * template_spec * (1/std_dev_product)
+ product_sum = np.sum(product)/n
+ local_score_arr[local_score_ndx] = product_sum
+
+ local_score_ndx += 1
+
+ return local_score_arr
diff --git a/PyHa/visualizations.py b/PyHa/visualizations.py
index 7653c195..2cd114d3 100644
--- a/PyHa/visualizations.py
+++ b/PyHa/visualizations.py
@@ -2,6 +2,8 @@
from .microfaune_package.microfaune import audio
from .tweetynet_package.tweetynet.TweetyNetModel import TweetyNetModel
from .tweetynet_package.tweetynet.Load_data_functions import compute_features
+from .FG_BG_sep.utils import FG_BG_local_score_arr
+from .template_matching.utils import filter, butter_bandpass, generate_specgram, template_matching_local_score_arr
import torch
import librosa
import matplotlib.pyplot as plt
@@ -185,7 +187,7 @@ def local_line_graph(
None
"""
- assert isinstance(local_scores,list)
+ assert isinstance(local_scores,list) or isinstance(local_scores, np.ndarray)
assert isinstance(clip_name,str)
assert isinstance(sample_rate,int)
assert sample_rate > 0
@@ -226,7 +228,7 @@ def local_line_graph(
if log_scale:
axs[0].set_yscale('log')
else:
- axs[0].set_ylim(0, 1)
+ axs[0].set_ylim(0, 1.05)
axs[0].grid(which='major', linestyle='-')
# Adding in the optional automated labels from a Pandas DataFrame
# if automated_df is not None:
@@ -382,6 +384,7 @@ def spectrogram_visualization(
# Running the Mel Spectrogram through the RNN
global_score, local_score = detector.predict(microfaune_features)
local_scores = local_score[0].tolist()
+
except BaseException:
checkVerbose(
"Skipping " +
@@ -405,6 +408,35 @@ def spectrogram_visualization(
clip_path +
" due to error in TweetyNet Prediction", verbose)
return None
+ elif (isolation_parameters["model"] == "fg_bg_dsp_sep"):
+ time_ratio, local_scores = FG_BG_local_score_arr(SIGNAL, isolation_parameters, normalized_sample_rate=SAMPLE_RATE)
+ elif (isolation_parameters["model"]=="template_matching"):
+ bandpass = False
+ b = None
+ a = None
+ if "cutoff_freq_low" in isolation_parameters.keys() and "cutoff_freq_high" in isolation_parameters.keys():
+ bandpass = True
+ assert isinstance(isolation_parameters["cutoff_freq_low"], int)
+ assert isinstance(isolation_parameters["cutoff_freq_high"], int)
+ assert isolation_parameters["cutoff_freq_low"] > 0 and isolation_parameters["cutoff_freq_high"] > 0
+ assert isolation_parameters["cutoff_freq_high"] > isolation_parameters["cutoff_freq_low"]
+ assert isolation_parameters["cutoff_freq_high"] <= int(0.5*SAMPLE_RATE)
+
+ TEMPLATE, _ = librosa.load(isolation_parameters["template_path"], sr=SAMPLE_RATE, mono=True)
+ if bandpass:
+ b, a = butter_bandpass(isolation_parameters["cutoff_freq_low"], isolation_parameters["cutoff_freq_high"], SAMPLE_RATE)
+ TEMPLATE = filter(TEMPLATE, b, a)
+
+ TEMPLATE_spec = generate_specgram(TEMPLATE, SAMPLE_RATE)
+ TEMPLATE_mean = np.mean(TEMPLATE_spec)
+ TEMPLATE_spec -= TEMPLATE_mean
+ TEMPLATE_std_dev = np.std(TEMPLATE_spec)
+ n = TEMPLATE_spec.shape[0] * TEMPLATE_spec.shape[1]
+
+ SIGNAL, SAMPLE_RATE = librosa.load(clip_path, sr=SAMPLE_RATE, mono=True)
+ if bandpass:
+ SIGNAL = filter(SIGNAL, b, a)
+ local_scores = template_matching_local_score_arr(SIGNAL, SAMPLE_RATE, TEMPLATE_spec, n, TEMPLATE_std_dev)
# In the case where the user wants to look at automated bird labels
if premade_annotations_df is None:
@@ -429,8 +461,10 @@ def spectrogram_visualization(
SAMPLE_RATE)
# Isolation techniques
else:
+ if isinstance(local_scores,list):
+ local_scores = np.array(local_scores)
automated_df = isolate(
- local_score[0],
+ local_scores,
SIGNAL,
SAMPLE_RATE,
audio_dir = "",
diff --git a/PyHa_Tutorial.ipynb b/PyHa_Tutorial.ipynb
index 0b1df87c..63e94179 100644
--- a/PyHa_Tutorial.ipynb
+++ b/PyHa_Tutorial.ipynb
@@ -66,7 +66,8 @@
" \"threshold_min\" : 0.0,\n",
" \"window_size\" : 2.0,\n",
" \"chunk_size\" : 5.0,\n",
- " \"verbose\" : True\n",
+ " \"verbose\" : True,\n",
+ " \"write_confidence\" : True\n",
"}\n",
"\n",
"# Example parameters for TweetyNET\n",
@@ -79,7 +80,34 @@
"# \"threshold_min\" : 0.0,\n",
"# \"window_size\" : 2.0,\n",
"# \"chunk_size\" : 5.0\n",
- "#}"
+ "#}\n",
+ "\n",
+ "# Example parameters for FG-BG Separation\n",
+ "# isolation_parameters = {\n",
+ "# \"model\" : \"fg_bg_dsp_sep\",\n",
+ "# \"technique\" : \"simple\",\n",
+ "# \"threshold_type\" : \"pure\",\n",
+ "# \"threshold_const\" : 0.5,\n",
+ "# \"verbose\" : True,\n",
+ "# \"kernel_size\" : 4,\n",
+ "# \"power_threshold\" : 3.0,\n",
+ "# \"threshold_min\" : 0.0\n",
+ "# }\n",
+ "\n",
+ "# Example parameters for Template Matching\n",
+ "# isolation_parameters = {\n",
+ "# \"model\" : \"template_matching\",\n",
+ "# \"template_path\" : \"./TEST/templates/piha.wav\",\n",
+ "# \"technique\" : \"steinberg\",\n",
+ "# # ideally this is the length of the template in seconds\n",
+ "# \"window_size\" : 4.2,\n",
+ "# \"threshold_type\" : \"pure\",\n",
+ "# \"threshold_const\" : 0.3,\n",
+ "# \"cutoff_freq_low\" : 850,\n",
+ "# \"cutoff_freq_high\" : 5600,\n",
+ "# \"verbose\" : True,\n",
+ "# \"write_confidence\" : True\n",
+ "# }"
]
},
{
@@ -95,7 +123,25 @@
"metadata": {
"scrolled": true
},
- "outputs": [],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1/1 [==============================] - 1s 567ms/step\n",
+ "1/1 [==============================] - 1s 575ms/step\n",
+ "1/1 [==============================] - 0s 421ms/step\n",
+ "1/1 [==============================] - 0s 279ms/step\n",
+ "1/1 [==============================] - 0s 341ms/step\n",
+ "1/1 [==============================] - 0s 126ms/step\n",
+ "1/1 [==============================] - 0s 302ms/step\n",
+ "1/1 [==============================] - 0s 337ms/step\n",
+ "1/1 [==============================] - 1s 574ms/step\n",
+ "1/1 [==============================] - 0s 456ms/step\n",
+ "1/1 [==============================] - 0s 174ms/step\n"
+ ]
+ }
+ ],
"source": [
"automated_df = generate_automated_labels(path,isolation_parameters);"
]
@@ -112,6 +158,14 @@
"execution_count": 5,
"metadata": {},
"outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/tzhang04/Desktop/e4e_ASID/PyHa/PyHa/statistics.py:49: FutureWarning: Unlike other reduction functions (e.g. `skew`, `kurtosis`), the default behavior of `mode` typically preserves the axis it acts along. In SciPy 1.11.0, this behavior will change: the default value of `keepdims` will become False, the `axis` over which the statistic is taken will be eliminated, and the value None will no longer be accepted. Set `keepdims` to True or False to avoid this warning.\n",
+ " 'MODE': stats.mode(np.round(annotation_lengths, 2))[0][0],\n"
+ ]
+ },
{
"data": {
"text/html": [
@@ -218,58 +272,64 @@
"
DURATION | \n",
" SAMPLE RATE | \n",
" MANUAL ID | \n",
+ " CONFIDENCE | \n",
" \n",
" \n",
" \n",
" \n",
" 0 | \n",
- " ScreamingPiha7.wav | \n",
- " 133.590204 | \n",
+ " ScreamingPiha9.wav | \n",
+ " 37.302857 | \n",
" 0 | \n",
" 0.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.559960 | \n",
"
\n",
" \n",
" 1 | \n",
- " ScreamingPiha7.wav | \n",
- " 133.590204 | \n",
+ " ScreamingPiha9.wav | \n",
+ " 37.302857 | \n",
" 0 | \n",
" 3.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.799775 | \n",
"
\n",
" \n",
" 2 | \n",
- " ScreamingPiha7.wav | \n",
- " 133.590204 | \n",
+ " ScreamingPiha9.wav | \n",
+ " 37.302857 | \n",
" 0 | \n",
" 6.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.799775 | \n",
"
\n",
" \n",
" 3 | \n",
- " ScreamingPiha7.wav | \n",
- " 133.590204 | \n",
+ " ScreamingPiha9.wav | \n",
+ " 37.302857 | \n",
" 0 | \n",
" 9.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.799775 | \n",
"
\n",
" \n",
" 4 | \n",
- " ScreamingPiha7.wav | \n",
- " 133.590204 | \n",
+ " ScreamingPiha9.wav | \n",
+ " 37.302857 | \n",
" 0 | \n",
" 12.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.799775 | \n",
"
\n",
" \n",
" ... | \n",
@@ -280,90 +340,96 @@
" ... | \n",
" ... | \n",
" ... | \n",
+ " ... | \n",
"
\n",
" \n",
" 180 | \n",
- " ScreamingPiha9.wav | \n",
- " 37.302857 | \n",
+ " ScreamingPiha5.wav | \n",
+ " 54.177959 | \n",
" 0 | \n",
- " 27.0 | \n",
+ " 51.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.037936 | \n",
"
\n",
" \n",
" 181 | \n",
- " ScreamingPiha9.wav | \n",
- " 37.302857 | \n",
+ " ScreamingPiha4.wav | \n",
+ " 13.557551 | \n",
" 0 | \n",
- " 30.0 | \n",
+ " 0.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.818890 | \n",
"
\n",
" \n",
" 182 | \n",
- " ScreamingPiha9.wav | \n",
- " 37.302857 | \n",
+ " ScreamingPiha4.wav | \n",
+ " 13.557551 | \n",
" 0 | \n",
- " 33.0 | \n",
+ " 3.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.818890 | \n",
"
\n",
" \n",
" 183 | \n",
- " ScreamingPiha3.wav | \n",
- " 6.844082 | \n",
+ " ScreamingPiha4.wav | \n",
+ " 13.557551 | \n",
" 0 | \n",
- " 0.0 | \n",
+ " 6.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.818890 | \n",
"
\n",
" \n",
" 184 | \n",
- " ScreamingPiha3.wav | \n",
- " 6.844082 | \n",
+ " ScreamingPiha4.wav | \n",
+ " 13.557551 | \n",
" 0 | \n",
- " 3.0 | \n",
+ " 9.0 | \n",
" 3.0 | \n",
" 44100 | \n",
" bird | \n",
+ " 0.818890 | \n",
"
\n",
" \n",
"\n",
- "185 rows × 7 columns
\n",
+ "185 rows × 8 columns
\n",
""
],
"text/plain": [
" IN FILE CLIP LENGTH CHANNEL OFFSET DURATION SAMPLE RATE \\\n",
- "0 ScreamingPiha7.wav 133.590204 0 0.0 3.0 44100 \n",
- "1 ScreamingPiha7.wav 133.590204 0 3.0 3.0 44100 \n",
- "2 ScreamingPiha7.wav 133.590204 0 6.0 3.0 44100 \n",
- "3 ScreamingPiha7.wav 133.590204 0 9.0 3.0 44100 \n",
- "4 ScreamingPiha7.wav 133.590204 0 12.0 3.0 44100 \n",
+ "0 ScreamingPiha9.wav 37.302857 0 0.0 3.0 44100 \n",
+ "1 ScreamingPiha9.wav 37.302857 0 3.0 3.0 44100 \n",
+ "2 ScreamingPiha9.wav 37.302857 0 6.0 3.0 44100 \n",
+ "3 ScreamingPiha9.wav 37.302857 0 9.0 3.0 44100 \n",
+ "4 ScreamingPiha9.wav 37.302857 0 12.0 3.0 44100 \n",
".. ... ... ... ... ... ... \n",
- "180 ScreamingPiha9.wav 37.302857 0 27.0 3.0 44100 \n",
- "181 ScreamingPiha9.wav 37.302857 0 30.0 3.0 44100 \n",
- "182 ScreamingPiha9.wav 37.302857 0 33.0 3.0 44100 \n",
- "183 ScreamingPiha3.wav 6.844082 0 0.0 3.0 44100 \n",
- "184 ScreamingPiha3.wav 6.844082 0 3.0 3.0 44100 \n",
+ "180 ScreamingPiha5.wav 54.177959 0 51.0 3.0 44100 \n",
+ "181 ScreamingPiha4.wav 13.557551 0 0.0 3.0 44100 \n",
+ "182 ScreamingPiha4.wav 13.557551 0 3.0 3.0 44100 \n",
+ "183 ScreamingPiha4.wav 13.557551 0 6.0 3.0 44100 \n",
+ "184 ScreamingPiha4.wav 13.557551 0 9.0 3.0 44100 \n",
"\n",
- " MANUAL ID \n",
- "0 bird \n",
- "1 bird \n",
- "2 bird \n",
- "3 bird \n",
- "4 bird \n",
- ".. ... \n",
- "180 bird \n",
- "181 bird \n",
- "182 bird \n",
- "183 bird \n",
- "184 bird \n",
+ " MANUAL ID CONFIDENCE \n",
+ "0 bird 0.559960 \n",
+ "1 bird 0.799775 \n",
+ "2 bird 0.799775 \n",
+ "3 bird 0.799775 \n",
+ "4 bird 0.799775 \n",
+ ".. ... ... \n",
+ "180 bird 0.037936 \n",
+ "181 bird 0.818890 \n",
+ "182 bird 0.818890 \n",
+ "183 bird 0.818890 \n",
+ "184 bird 0.818890 \n",
"\n",
- "[185 rows x 7 columns]"
+ "[185 rows x 8 columns]"
]
},
"execution_count": 6,
@@ -591,6 +657,14 @@
"execution_count": 8,
"metadata": {},
"outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/tzhang04/Desktop/e4e_ASID/PyHa/PyHa/statistics.py:49: FutureWarning: Unlike other reduction functions (e.g. `skew`, `kurtosis`), the default behavior of `mode` typically preserves the axis it acts along. In SciPy 1.11.0, this behavior will change: the default value of `keepdims` will become False, the `axis` over which the statistic is taken will be eliminated, and the value None will no longer be accepted. Set `keepdims` to True or False to avoid this warning.\n",
+ " 'MODE': stats.mode(np.round(annotation_lengths, 2))[0][0],\n"
+ ]
+ },
{
"data": {
"text/html": [
@@ -671,14 +745,12 @@
"outputs": [
{
"data": {
- "image/png": "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\n",
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAHHCAYAAACRAnNyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/OQEPoAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA460lEQVR4nO3deXxU1f3/8fcYkkkIEIIJJIFAICA7SMNSwA0BEQJuaJFiC1apC4uA+q1YK8RSoVoRlM2FglUQRUQtsgjIYosUiCBBJQUMgohGFBKIkEDy+f3BL1OGBEhCYA7yej4e9/Hgnnvmns+cGZJ37jLjMTMTAACAgy4JdAEAAACnQlABAADOIqgAAABnEVQAAICzCCoAAMBZBBUAAOAsggoAAHAWQQUAADiLoAIAAJxFUAEuUgkJCRowYECgy7ggzJw5Ux6PRzt37gx0KReMlStXyuPx6K233gp0KbjAEVRwQZgyZYo8Ho/atWt31vtauHChRo8effZFnQcu1XrgwAGFhobK4/Hoiy++CHQ5RZTHXD355JN65513yqWe8pKQkKCePXsGuoxTmj17tiZMmBDoMvAzRlDBBWHWrFlKSEjQunXrtH379rPa18KFC5WSklJOlZ1bLtU6d+5ceTwexcTEaNasWYEup4jymKtTBZXf/OY3Onz4sOrUqXNW+/85IqjgXCOowHkZGRlas2aNxo8fr+joaCd/SV4MXnvtNfXo0UN9+/bV7NmzA13OeRUUFOQ7mgTg/CKowHmzZs1SZGSkkpOTdeuttxYbVArPh69cudKvfefOnfJ4PJo5c6YkacCAAZo8ebIkyePx+JZCOTk5evDBBxUfHy+v16uGDRvqb3/7m07+knGPx6PBgwdr7ty5atKkicLCwtS+fXulpaVJkl544QXVr19foaGhuuaaa4pc2/DRRx/ptttuU+3ateX1ehUfH6/hw4fr8OHDvj5nqrWgoEATJkxQ06ZNFRoaqho1auiee+7R/v37/cYyM40ZM0a1atVSxYoV1alTJ3322WclmPn/2bVrlz766CPdfvvtuv32233h8WTXXHONmjVrps8//1ydOnVSxYoVVbNmTT311FN+/QpfrzfffFN/+ctfVKtWLYWGhqpz587FHjGbO3eukpKSFBYWpqioKN1xxx3as2dPiefqb3/7mzp06KBLL71UYWFhSkpKKnLthMfjUU5Ojl555RXf4wuv4TnVNSpTpkxR06ZN5fV6FRcXp0GDBunAgQNlmpOz9dprr/nmqFq1arr99tu1e/fuMtfy1Vdf6YYbblB4eLiqV6+u4cOHa8mSJX7/z6655hq9//77+uqrr3xzlpCQ4LefgoKCM77G27ZtU+/evRUTE6PQ0FDVqlVLt99+u7Kyssp1jnCBMsBxjRo1srvuusvMzFavXm2SbN26dX59VqxYYZJsxYoVfu0ZGRkmyWbMmGFmZmvWrLGuXbuaJHv11Vd9i5lZQUGBXXvttebxeOzuu++2SZMmWa9evUySDRs2zG+/kqxFixYWHx9v48aNs3HjxllERITVrl3bJk2aZE2aNLFnnnnGHnvsMQsJCbFOnTr5PX7IkCHWo0cPe/LJJ+2FF16wu+66y4KCguzWW2/19TldrWZmd999t1WoUMEGDhxo06ZNsz/84Q8WHh5ubdq0sby8PF+/xx57zCRZjx49bNKkSfa73/3O4uLiLCoqyvr371+i12DcuHFWqVIl++mnn8zMLDEx0e6///4i/a6++mqLi4uz+Ph4e+CBB2zKlCl27bXXmiRbuHBhkderVatWlpSUZM8++6yNHj3aKlasaG3btvXb54wZM0yStWnTxp599ll75JFHLCwszBISEmz//v0lmqtatWrZ/fffb5MmTbLx48db27ZtTZItWLDA1+fVV181r9drV155pe/xa9as8ashIyPD13/UqFEmybp06WLPP/+8DR482IKCgorMf0nn5FTq1KljycnJp+0zZswY83g81qdPH5syZYqlpKRYVFSU3xyVppZDhw5ZvXr1LCwszB555BGbMGGCtW3b1lq2bOn3/+yDDz6wyy+/3KKionxzNn/+/FK9xrm5uVa3bl2Li4uzMWPG2Msvv2wpKSnWpk0b27lz5xnnBz9/BBU4bcOGDSbJli5dambHw0StWrXsgQce8OtX0qBiZjZo0CArLqO/8847JsnGjBnj137rrbeax+Ox7du3+9okmdfr9fvF9cILL5gki4mJsezsbF/7yJEji/ySK/yFf6KxY8eax+Oxr7766oy1fvTRRybJZs2a5de+ePFiv/bMzEwLCQmx5ORkKygo8PV79NFHTVKJg0rz5s2tX79+fo+Pioqyo0eP+vW7+uqrTZL94x//8LXl5uZaTEyM9e7d29dW+Ho1btzYcnNzfe0TJ040SZaWlmZmZnl5eVa9enVr1qyZHT582NdvwYIFJskef/zxM86VWdH5zsvLs2bNmtm1117r1x4eHl7snJwcVArn9brrrrP8/Hxfv0mTJpkk+/vf/17qOTmVMwWVnTt3WlBQkP3lL3/xa09LS7MKFSr4tZe0lmeeecYk2TvvvONrO3z4sDVq1KjI/7Pk5GSrU6dOkbpK+hpv3LjRJNncuXPPOBe4OHHqB06bNWuWatSooU6dOkk6fni+T58+mjNnjvLz88t1rIULFyooKEhDhw71a3/wwQdlZlq0aJFfe+fOnf0OcxfekdS7d29Vrly5SPuXX37pawsLC/P9OycnR/v27VOHDh1kZtq4ceMZa507d64iIiLUtWtX7du3z7ckJSWpUqVKWrFihSRp2bJlysvL05AhQ/xOhQwbNuyMYxTavHmz0tLS1LdvX19b3759tW/fPi1ZsqRI/0qVKumOO+7wrYeEhKht27Z+z7/QnXfeqZCQEN/6lVdeKel/c7VhwwZlZmbq/vvvV2hoqK9fcnKyGjVqpPfff79Ez+HE+d6/f7+ysrJ05ZVX6pNPPinR409WOK/Dhg3TJZf878fowIEDVaVKlSJ1lWZOSuvtt99WQUGBfvWrX/m9F2JiYtSgQQPfe6E0tSxevFg1a9bUDTfc4GsLDQ3VwIEDS13fmV7jiIgISdKSJUv0008/lXr/+PkjqMBZ+fn5mjNnjjp16qSMjAxt375d27dvV7t27fTdd99p+fLl5TreV199pbi4OL+QIUmNGzf2bT9R7dq1/dYLf+DGx8cX237itSO7du3SgAEDVK1aNVWqVEnR0dG6+uqrJalE5+W3bdumrKwsVa9eXdHR0X7LoUOHlJmZ6VdzgwYN/B4fHR2tyMjIM44jHb/2ITw8XPXq1fO9BqGhoUpISCj2eqFatWoVueg0MjKyyLUzUtE5LKypsG9h/Q0bNizy2EaNGhV5TU5lwYIF+uUvf6nQ0FBVq1ZN0dHRmjp1apmvgThVXSEhIapXr16RukozJ6W1bds2mZkaNGhQ5L3wxRdf+N4Lpanlq6++UmJiYpF+9evXL3V9Z3qN69atqxEjRujll19WVFSUunXrpsmTJ3N9CnwqBLoA4FQ+/PBD7d27V3PmzNGcOXOKbJ81a5auu+46STrl3RjlfdTlREFBQaVqt/9/QW5+fr66du2qH3/8UX/4wx/UqFEjhYeHa8+ePRowYIAKCgrOOHZBQYGqV69+yjugoqOjS/gsTs/M9PrrrysnJ0dNmjQpsj0zM1OHDh1SpUqVfG1nev4nKk3fsvroo490ww036KqrrtKUKVMUGxur4OBgzZgx47zdvXQun2dBQYE8Ho8WLVpU7DgnvjbnupbilGS8Z555RgMGDNC7776rDz74QEOHDtXYsWO1du1a1apV65zUhQsHQQXOmjVrlqpXr+67m+NEb7/9tubPn69p06YpLCzM91fayXdcFPcX96lCTZ06dbRs2TIdPHjQ76jK1q1bfdvLQ1pamv773//qlVde0W9/+1tf+9KlS0tca2JiopYtW6aOHTv6ndY4WWHN27ZtU7169Xzt33//fYn+ml+1apW+/vprPfHEE74jS4X279+v3//+93rnnXf8TiWUp8L609PTde211/ptS09P93tNTjVX8+bNU2hoqJYsWSKv1+trnzFjRpG+Jb39+MS6TpzXvLw8ZWRkqEuXLiXaT3lITEyUmalu3bq67LLLymWfderU0eeffy4z85uT4u7IKq9btps3b67mzZvrscce05o1a9SxY0dNmzZNY8aMKZf948LFqR846fDhw3r77bfVs2dP3XrrrUWWwYMH6+DBg3rvvfckHf/BGhQUpNWrV/vtZ8qUKUX2HR4eLqloqOnRo4fy8/M1adIkv/Znn31WHo9H3bt3L5fnVvgX5ol/UZqZJk6cWOJaf/WrXyk/P19//vOfizzm2LFjvv5dunRRcHCwnn/+eb/xSvoBXYWnfR5++OEir8HAgQPVoEGDc/q5Nq1bt1b16tU1bdo05ebm+toXLVqkL774QsnJyb62U81VUFCQPB6P39G1nTt3FvvBbuHh4UUeX5wuXbooJCREzz33nN+8Tp8+XVlZWX51nWu33HKLgoKClJKSUuSoiJnphx9+KPU+u3Xrpj179vj+f0nSkSNH9NJLLxXpGx4eflanabKzs3Xs2DG/tubNm+uSSy7xe81x8eKICpz03nvv6eDBg34X853ol7/8pe/D3/r06aOIiAjddtttev755+XxeJSYmKgFCxYUOT8vSUlJSZKkoUOHqlu3bgoKCtLtt9+uXr16qVOnTvrjH/+onTt3qmXLlvrggw/07rvvatiwYUpMTCyX59aoUSMlJibqoYce0p49e1SlShXNmzev2CMcp6r16quv1j333KOxY8dq06ZNuu666xQcHKxt27Zp7ty5mjhxom699VZFR0froYce0tixY9WzZ0/16NFDGzdu1KJFixQVFXXaOnNzczVv3jx17drV70LWE91www2aOHGiMjMzVb169bOfnJMEBwfrr3/9q+68805dffXV6tu3r7777jtNnDhRCQkJGj58uK/vqeYqOTlZ48eP1/XXX69f//rXyszM1OTJk1W/fn1t3rzZb7ykpCQtW7ZM48ePV1xcnOrWrVvs1zZER0dr5MiRSklJ0fXXX68bbrhB6enpmjJlitq0aVPuR5i2b99e7JGFVq1aKTk5WWPGjNHIkSO1c+dO3XTTTapcubIyMjI0f/58/f73v9dDDz1UqvHuueceTZo0SX379tUDDzyg2NhYzZo1y/c+OPEoSlJSkt544w2NGDFCbdq0UaVKldSrV68Sj/Xhhx9q8ODBuu2223TZZZfp2LFjevXVVxUUFKTevXuXqm78TJ3v24yAkujVq5eFhoZaTk7OKfsMGDDAgoODbd++fWZm9v3331vv3r2tYsWKFhkZaffcc49t2bKlyO3Jx44dsyFDhlh0dLR5PB6/W1oPHjxow4cPt7i4OAsODrYGDRrY008/7Xdrr9nx25MHDRrk11Z4K/TTTz/t1154m+aJt19+/vnn1qVLF6tUqZJFRUXZwIED7dNPPy1VrWZmL774oiUlJVlYWJhVrlzZmjdvbv/3f/9n33zzja9Pfn6+paSkWGxsrIWFhdk111xjW7ZssTp16pz29uR58+aZJJs+ffop+6xcudIk2cSJE83s+O2vTZs2LdKvf//+frewFjcnJ87hiXNgZvbGG29Yq1atzOv1WrVq1axfv3729ddf+/U53VxNnz7dGjRoYF6v1xo1amQzZszwfQ7KibZu3WpXXXWVhYWF+d2+XdznqJgdvx25UaNGFhwcbDVq1LD77rvP73NLSjMnp1KnTh2TVOxS+PlCZsdfryuuuMLCw8MtPDzcGjVqZIMGDbL09PQy1fLll19acnKyhYWFWXR0tD344IO+98TatWt9/Q4dOmS//vWvrWrVqibJt5+SvsZffvml/e53v7PExEQLDQ21atWqWadOnWzZsmVnnBtcHDxm5+gKKgDAz8qECRM0fPhwff3116pZs2agy8FFgqACACji8OHDfhdqHzlyRK1atVJ+fr7+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ZcaMGeUy+NSpUzV16lTt3LlTktS0aVM9/vjj6t69e7nsHwAAXNjKFFQKpaam6osvvpB0PGS0atWqVI+vVauWxo0bpwYNGsjM9Morr+jGG2/Uxo0b1bRp07MpDQAA/AyUKahkZmbq9ttv18qVK1W1alVJ0oEDB9SpUyfNmTNH0dHRJdpPr169/Nb/8pe/aOrUqVq7di1BBQAAlO2unyFDhujgwYP67LPP9OOPP+rHH3/Uli1blJ2draFDh5apkPz8fM2ZM0c5OTlq3759mfYBAAB+Xsp0RGXx4sVatmyZGjdu7Gtr0qSJJk+eXOqLadPS0tS+fXsdOXJElSpV0vz589WkSZNi++bm5io3N9e3np2dXZbyy03BsaNKS0vza0tKSpLX6/Wt5+bmKjU11beelpamgoLT7/fkx5xq32eqpyRjFfcc8vLyJEkhISElHhsAgHOhTEGloKBAwcHBRdqDg4NVcKbfjCdp2LChNm3apKysLL311lvq37+/Vq1aVWxYGTt2rFJSUspS8jmR/d0uPb/zsGK+PP69Rwf27NBz90sdOnTw9UlNTdXQKe+qas1ESdLXmz5SZP2k0+735Mecat9nqqckY538mOOPW60Klaoppn6zEo8NAMC5UKagcu211+qBBx7Q66+/rri4OEnSnj17NHz4cHXu3LlU+woJCVH9+vUlHf+rff369Zo4caJeeOGFIn1HjhypESNG+Nazs7MVHx9flqdQbirH1FVUYvPT9qlaM9HX58CeHSXa74mPKWs9JR3r5OdwYM8OBUfElGl8AADKU5mCyqRJk3TDDTcoISHBFxR2796tZs2a6bXXXjurggoKCvxO75zI6/Vy+gEAgItImYJKfHy8PvnkEy1btkxbt26VJDVu3FhdunQp1X5Gjhyp7t27q3bt2jp48KBmz56tlStXasmSJWUpCwAA/MyUKqh8+OGHGjx4sNauXasqVaqoa9eu6tq1qyQpKytLTZs21bRp03TllVeWaH+ZmZn67W9/q7179yoiIkItWrTQkiVLfPsEAAAXt1IFlQkTJmjgwIGqUqVKkW0RERG65557NH78+BIHlenTp5dmeAAAcJEp1eeofPrpp7r++utPuf26664r9rZaAACAsihVUPnuu++KvS25UIUKFfT999+fdVEAAABSKYNKzZo1tWXLllNu37x5s2JjY8+6KAAAAKmU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N97QiBEjNGrUKH3yySdq2bKlunXrpszMzECX5rScnBy1bNlSkydPLnb7U089peeee07Tpk3Tf/7zH4WHh6tbt246cuTIea7UfatWrdKgQYO0du1aLV26VEePHtV1113n98Vyw4cP1z//+U/NnTtXq1at0jfffKNbbrklgFW7qVatWho3bpxSU1O1YcMGXXvttbrxxhv12WefSWIey2L9+vV64YUX1KJFC7925rJ0mjZtqr179/qWf/3rX75tzGUZlMu3C14g2rZta4MGDfKt5+fnW1xcnI0dOzaAVV1YJNn8+fN96wUFBRYTE2NPP/20r+3AgQPm9Xrt9ddfD0CFF5bMzEyTZKtWrTKz43MXHBxsc+fO9fX54osvTJJ9/PHHgSrzghEZGWkvv/wy81gGBw8etAYNGtjSpUvt6quvtgceeMDMeE+W1qhRo6xly5bFbmMuy+aiOaKSl5en1NRUdenSxdd2ySWXqEuXLvr4448DWNmFLSMjQ99++63fvEZERKhdu3bMawlkZWVJkqpVqyZJSk1N1dGjR/3ms1GjRqpduzbzeRr5+fmaM2eOcnJy1L59e+axDAYNGqTk5GS/OZN4T5bFtm3bFBcXp3r16qlfv37atWuXJOayrJz4ZNrzYd++fcrPzy/y0fw1atTQ1q1bA1TVhe/bb7+VpGLntXAbildQUKBhw4apY8eOatasmaTj8xkSElLkyzaZz+KlpaWpffv2OnLkiCpVqqT58+erSZMm2rRpE/NYCnPmzNEnn3yi9evXF9nGe7J02rVrp5kzZ6phw4bau3evUlJSdOWVV2rLli3MZRldNEEFcM2gQYO0ZcsWv/PXKJ2GDRtq06ZNysrK0ltvvaX+/ftr1apVgS7rgrJ792498MADWrp0qUJDQwNdzgWve/fuvn+3aNFC7dq1U506dfTmm28qLCwsgJVduC6aUz9RUVEKCgoqcnX1d999p5iYmABVdeErnDvmtXQGDx6sBQsWaMWKFapVq5avPSYmRnl5eTpw4IBff+azeCEhIapfv76SkpI0duxYtWzZUhMnTmQeSyE1NVWZmZn6xS9+oQoVKqhChQpatWqVnnvuOVWoUEE1atRgLs9C1apVddlll2n79u28L8voogkqISEhSkpK0vLly31tBQUFWr58udq3bx/Ayi5sdevWVUxMjN+8Zmdn6z//+Q/zWgwz0+DBgzV//nx9+OGHqlu3rt/2pKQkBQcH+81nenq6du3axXyWQEFBgXJzc5nHUujcubPS0tK0adMm39K6dWv169fP92/msuwOHTqkHTt2KDY2lvdlWQX6at7zac6cOeb1em3mzJn2+eef2+9//3urWrWqffvtt4EuzWkHDx60jRs32saNG02SjR8/3jZu3GhfffWVmZmNGzfOqlatau+++65t3rzZbrzxRqtbt64dPnw4wJW757777rOIiAhbuXKl7d2717f89NNPvj733nuv1a5d2z788EPbsGGDtW/f3tq3bx/Aqt30yCOP2KpVqywjI8M2b95sjzzyiHk8Hvvggw/MjHk8Gyfe9WPGXJbGgw8+aCtXrrSMjAz797//bV26dLGoqCjLzMw0M+ayLC6qoGJm9vzzz1vt2rUtJCTE2rZta2vXrg10Sc5bsWKFSSqy9O/f38yO36L8pz/9yWrUqGFer9c6d+5s6enpgS3aUcXNoySbMWOGr8/hw4ft/vvvt8jISKtYsaLdfPPNtnfv3sAV7ajf/e53VqdOHQsJCbHo6Gjr3LmzL6SYMY9n4+SgwlyWXJ8+fSw2NtZCQkKsZs2a1qdPH9u+fbtvO3NZeh4zs8AcywEAADi9i+YaFQAAcOEhqAAAAGcRVAAAgLMIKgAAwFkEFQAA4CyCCgAAcBZBBQAAOIugAqBcDRgwQDfddFOgywgoj8ejd955p9SPS09PV0xMjA4ePHjGvp9//rlq1aqlnJycMlQIXDgIKkAAffzxxwoKClJycnLAali5cqU8Hk+RL0o7k507d8rj8WjTpk1+7RMnTtTMmTPLrb5TKWsYKE+jR4/W5ZdfXm77GzlypIYMGaLKlSufsW+TJk30y1/+UuPHjy+38QEXEVSAAJo+fbqGDBmi1atX65tvvgl0OeUiIiJCVatWDXQZF5xdu3ZpwYIFGjBgQIkfc+edd2rq1Kk6duzYuSsMCDCCChAghw4d0htvvKH77rtPycnJRY5CFB7pWL58uVq3bq2KFSuqQ4cOSk9P9/Up/Iv+1VdfVUJCgiIiInT77bf7nTrIzc3V0KFDVb16dYWGhuqKK67Q+vXrJR0/KtKpUydJUmRkpDwej+8X5eLFi3XFFVeoatWquvTSS9WzZ0/t2LHDt9/Cb35u1aqVPB6PrrnmGklFT/2cbvySPs+yePnll9W4cWOFhoaqUaNGmjJlim9b4dGgt99+W506dVLFihXVsmVLffzxx377eOmllxQfH6+KFSvq5ptv1vjx430hbObMmUpJSdGnn34qj8cjj8fj9xru27dPN998sypWrKgGDRrovffeO229b775plq2bKmaNWv62r766iv16tVLkZGRCg8PV9OmTbVw4ULf9q5du+rHH3/UqlWrzmKmAMcF+suGgIvV9OnTrXXr1mZm9s9//tMSExOtoKDAt73wyyDbtWtnK1eutM8++8yuvPJK69Chg6/PqFGjrFKlSnbLLbdYWlqarV692mJiYuzRRx/19Rk6dKjFxcXZwoUL7bPPPrP+/ftbZGSk/fDDD3bs2DGbN2+eSbL09HTbu3evHThwwMzM3nrrLZs3b55t27bNNm7caL169bLmzZtbfn6+mZmtW7fOJNmyZcts79699sMPP5iZWf/+/e3GG28s0fglfZ7FkWTz588vdttrr71msbGxNm/ePPvyyy9t3rx5Vq1aNZs5c6aZmWVkZJgka9SokS1YsMDS09Pt1ltvtTp16tjRo0fNzOxf//qXXXLJJfb0009benq6TZ482apVq2YRERFmZvbTTz/Zgw8+aE2bNi3yLdiSrFatWjZ79mzbtm2bDR061CpVquR7zsW54YYb7N577/VrS05Otq5du9rmzZttx44d9s9//tNWrVrl16ddu3Y2atSo084VcCEjqAAB0qFDB5swYYKZmR09etSioqJsxYoVvu2Fv8CXLVvma3v//fdNkh0+fNjMjgeVihUrWnZ2tq/Pww8/bO3atTMzs0OHDllwcLDNmjXLtz0vL8/i4uLsqaee8htn//79p633+++/N0mWlpZmZv/7Zb9x40a/ficGldKMf7rnWZzTBZXExESbPXu2X9uf//xna9++vV/tL7/8sm/7Z599ZpLsiy++MLPj34KbnJzst49+/fr5gorZ8flv2bJlsbU99thjvvVDhw6ZJFu0aNEpn0/Lli3tiSee8Gtr3ry5jR49+pSPMTO7+eabbcCAAaftA1zIOPUDBEB6errWrVunvn37SpIqVKigPn36aPr06UX6tmjRwvfv2NhYSVJmZqavLSEhwe/iy9jYWN/2HTt26OjRo+rYsaNve3BwsNq2basvvvjitDVu27ZNffv2Vb169VSlShUlJCRIOn4tRUmVZvwzPc+SysnJ0Y4dO3TXXXepUqVKvmXMmDF+p67ONGZ6erratm3r1//k9dM5cd/h4eGqUqXKaZ/P4cOHFRoa6tc2dOhQjRkzRh07dtSoUaO0efPmIo8LCwvTTz/9VOK6gAtNhUAXAFyMpk+frmPHjikuLs7XZmbyer2aNGmSIiIifO3BwcG+f3s8HklSQUFBsdsL+5y4vax69eqlOnXq6KWXXlJcXJwKCgrUrFkz5eXlnfW+i3Om51lShw4dknT8+pJ27dr5bQsKCjonYxantK9LVFSU9u/f79d29913q1u3bnr//ff1wQcfaOzYsXrmmWc0ZMgQX58ff/xRiYmJ5VIz4CKOqADn2bFjx/SPf/xDzzzzjDZt2uRbPv30U8XFxen1118vt7ESExMVEhKif//73762o0ePav369WrSpIkkKSQkRJKUn5/v6/PDDz8oPT1djz32mDp37qzGjRsX+SVa3OPKMn55q1GjhuLi4vTll1+qfv36fkvhBcAl0bBhQ7+LfiUVWQ8JCTnt8y+NVq1a6fPPPy/SHh8fr3vvvVdvv/22HnzwQb300kt+27ds2aJWrVqVSw2AiziiApxnCxYs0P79+3XXXXf5HTmRpN69e2v69Om69957y2Ws8PBw3XfffXr44YdVrVo11a5dW0899ZR++ukn3XXXXZKkOnXqyOPxaMGCBerRo4fCwsIUGRmpSy+9VC+++KJiY2O1a9cuPfLII377rl69usLCwrR48WLVqlVLoaGhRZ5PScY/GxkZGUU+x6VBgwZKSUnR0KFDFRERoeuvv165ubnasGGD9u/frxEjRpRo30OGDNFVV12l8ePHq1evXvrwww+1aNEi35EX6fhpt8IaatWqpcqVK8vr9ZbpuXTr1k1333238vPzfUd+hg0bpu7du+uyyy7T/v37tWLFCjVu3Nj3mJ07d2rPnj3q0qVLmcYELgiBvkgGuNj07NnTevToUey2//znPybJPv3002Ivct24caNJsoyMDDMr/mLOZ5991urUqeNbP3z4sA0ZMsSioqLM6/Vax44dbd26dX6PeeKJJywmJsY8Ho/179/fzMyWLl1qjRs3Nq/Xay1atLCVK1cWuYD1pZdesvj4eLvkkkvs6quvNrOid/2cafySPM/iSCp2+eijj8zMbNasWXb55ZdbSEiIRUZG2lVXXWVvv/22mRV/IfD+/ftNkt8FzS+++KLVrFnTwsLC7KabbrIxY8ZYTEyMb/uRI0esd+/eVrVqVZNkM2bM8NV28oW+ERERvu3FOXr0qMXFxdnixYt9bYMHD7bExETzer0WHR1tv/nNb2zfvn2+7U8++aR169btlPsEfg48ZmbnPx4BwIVn4MCB2rp1qz766KNzsv/Jkyfrvffe05IlS87YNy8vTw0aNNDs2bP9LlYGfm449QMAp/C3v/1NXbt2VXh4uBYtWqRXXnnF74Pjyts999yjAwcO6ODBg2f8GP1du3bp0UcfJaTgZ48jKgBwCr/61a+0cuVKHTx4UPXq1dOQIUPK7fohACVDUAEAAM7i9mQAAOAsggoAAHAWQQUAADiLoAIAAJxFUAEAAM4iqAAAAGcRVAAAgLMIKgAAwFkEFQAA4Kz/ByYKBik1EccGAAAAAElFTkSuQmCC",
"text/plain": [
- "