From 3c17a2337166245de8df778fe174aad997e14e8f Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Wed, 25 Jan 2023 19:17:38 +0000 Subject: [PATCH 1/2] Formatted code with Black and isort --- .ropeproject/autoimport.db | Bin 0 -> 1056768 bytes app.py | 163 +++-- bat_detect/detector/compute_features.py | 128 ++-- bat_detect/detector/model_helpers.py | 131 +++- bat_detect/detector/models.py | 328 +++++++--- bat_detect/detector/parameters.py | 204 ++++--- bat_detect/detector/post_process.py | 92 ++- bat_detect/evaluate/evaluate_models.py | 710 ++++++++++++++-------- bat_detect/finetune/finetune_model.py | 368 +++++++---- bat_detect/finetune/prep_data_finetune.py | 207 ++++--- bat_detect/train/audio_dataloader.py | 453 +++++++++----- bat_detect/train/evaluate.py | 263 +++++--- bat_detect/train/losses.py | 27 +- bat_detect/train/train_model.py | 555 +++++++++++------ bat_detect/train/train_split.py | 483 ++++++++++----- bat_detect/train/train_utils.py | 152 +++-- bat_detect/utils/audio_utils.py | 184 ++++-- bat_detect/utils/detector_utils.py | 360 +++++++---- bat_detect/utils/plot_utils.py | 458 +++++++++----- bat_detect/utils/visualize.py | 195 ++++-- bat_detect/utils/wavfile.py | 122 ++-- run_batdetect.py | 118 ++-- scripts/gen_dataset_summary_image.py | 107 ++-- scripts/gen_spec_image.py | 216 +++++-- scripts/gen_spec_video.py | 255 +++++--- scripts/viz_helpers.py | 197 ++++-- 26 files changed, 4357 insertions(+), 2119 deletions(-) create mode 100644 .ropeproject/autoimport.db diff --git a/.ropeproject/autoimport.db b/.ropeproject/autoimport.db new file mode 100644 index 0000000000000000000000000000000000000000..585d38353baaf7751fd73e1326f04f53133c2c45 GIT binary patch literal 1056768 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 NTn*VMjG`RVy8wmAS0?}f literal 0 HcmV?d00001 diff --git a/app.py b/app.py index ae44690..1c884f0 100644 --- a/app.py +++ b/app.py @@ -1,84 +1,121 @@ -import gradio as gr import os + +import gradio as gr import matplotlib.pyplot as plt -import pandas as pd import numpy as np +import pandas as pd -import bat_detect.utils.detector_utils as du import bat_detect.utils.audio_utils as au +import bat_detect.utils.detector_utils as du import bat_detect.utils.plot_utils as viz - # setup the arguments args = {} args = du.get_default_bd_args() -args['detection_threshold'] = 0.3 -args['time_expansion_factor'] = 1 -args['model_path'] = 'models/Net2DFast_UK_same.pth.tar' +args["detection_threshold"] = 0.3 +args["time_expansion_factor"] = 1 +args["model_path"] = "models/Net2DFast_UK_same.pth.tar" max_duration = 2.0 # load the model -model, params = du.load_model(args['model_path']) +model, params = du.load_model(args["model_path"]) -df = gr.Dataframe( - headers=["species", "time", "detection_prob", "species_prob"], - datatype=["str", "str", "str", "str"], - row_count=1, - col_count=(4, "fixed"), - label='Predictions' - ) - -examples = [['example_data/audio/20170701_213954-MYOMYS-LR_0_0.5.wav', 0.3], - ['example_data/audio/20180530_213516-EPTSER-LR_0_0.5.wav', 0.3], - ['example_data/audio/20180627_215323-RHIFER-LR_0_0.5.wav', 0.3]] +df = gr.Dataframe( + headers=["species", "time", "detection_prob", "species_prob"], + datatype=["str", "str", "str", "str"], + row_count=1, + col_count=(4, "fixed"), + label="Predictions", +) + +examples = [ + ["example_data/audio/20170701_213954-MYOMYS-LR_0_0.5.wav", 0.3], + ["example_data/audio/20180530_213516-EPTSER-LR_0_0.5.wav", 0.3], + ["example_data/audio/20180627_215323-RHIFER-LR_0_0.5.wav", 0.3], +] def make_prediction(file_name=None, detection_threshold=0.3): - + if file_name is not None: audio_file = file_name else: return "You must provide an input audio file." - - if detection_threshold is not None and detection_threshold != '': - args['detection_threshold'] = float(detection_threshold) - + + if detection_threshold is not None and detection_threshold != "": + args["detection_threshold"] = float(detection_threshold) + # process the file to generate predictions - results = du.process_file(audio_file, model, params, args, max_duration=max_duration) - - anns = [ann for ann in results['pred_dict']['annotation']] - clss = [aa['class'] for aa in anns] - st_time = [aa['start_time'] for aa in anns] - cls_prob = [aa['class_prob'] for aa in anns] - det_prob = [aa['det_prob'] for aa in anns] - data = {'species': clss, 'time': st_time, 'detection_prob': det_prob, 'species_prob': cls_prob} - + results = du.process_file( + audio_file, model, params, args, max_duration=max_duration + ) + + anns = [ann for ann in results["pred_dict"]["annotation"]] + clss = [aa["class"] for aa in anns] + st_time = [aa["start_time"] for aa in anns] + cls_prob = [aa["class_prob"] for aa in anns] + det_prob = [aa["det_prob"] for aa in anns] + data = { + "species": clss, + "time": st_time, + "detection_prob": det_prob, + "species_prob": cls_prob, + } + df = pd.DataFrame(data=data) im = generate_results_image(audio_file, anns) - + return [df, im] -def generate_results_image(audio_file, anns): - +def generate_results_image(audio_file, anns): + # load audio - sampling_rate, audio = au.load_audio_file(audio_file, args['time_expansion_factor'], - params['target_samp_rate'], params['scale_raw_audio'], max_duration=max_duration) + sampling_rate, audio = au.load_audio_file( + audio_file, + args["time_expansion_factor"], + params["target_samp_rate"], + params["scale_raw_audio"], + max_duration=max_duration, + ) duration = audio.shape[0] / sampling_rate - + # generate spec - spec, spec_viz = au.generate_spectrogram(audio, sampling_rate, params, True, False) + spec, spec_viz = au.generate_spectrogram( + audio, sampling_rate, params, True, False + ) # create fig - plt.close('all') - fig = plt.figure(1, figsize=(spec.shape[1]/100, spec.shape[0]/100), dpi=100, frameon=False) - spec_duration = au.x_coords_to_time(spec.shape[1], sampling_rate, params['fft_win_length'], params['fft_overlap']) - viz.create_box_image(spec, fig, anns, 0, spec_duration, spec_duration, params, spec.max()*1.1, False, True) - plt.ylabel('Freq - kHz') - plt.xlabel('Time - secs') + plt.close("all") + fig = plt.figure( + 1, + figsize=(spec.shape[1] / 100, spec.shape[0] / 100), + dpi=100, + frameon=False, + ) + spec_duration = au.x_coords_to_time( + spec.shape[1], + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + ) + viz.create_box_image( + spec, + fig, + anns, + 0, + spec_duration, + spec_duration, + params, + spec.max() * 1.1, + False, + True, + ) + plt.ylabel("Freq - kHz") + plt.xlabel("Time - secs") plt.tight_layout() - + # convert fig to image fig.canvas.draw() data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) @@ -88,21 +125,23 @@ def generate_results_image(audio_file, anns): return im -descr_txt = "Demo of BatDetect2 deep learning-based bat echolocation call detection. " \ - "
This model is only trained on bat species from the UK. If the input " \ - "file is longer than 2 seconds, only the first 2 seconds will be processed." \ - "
Check out the paper [here](https://www.biorxiv.org/content/10.1101/2022.12.14.520490v1)." +descr_txt = ( + "Demo of BatDetect2 deep learning-based bat echolocation call detection. " + "
This model is only trained on bat species from the UK. If the input " + "file is longer than 2 seconds, only the first 2 seconds will be processed." + "
Check out the paper [here](https://www.biorxiv.org/content/10.1101/2022.12.14.520490v1)." +) gr.Interface( - fn = make_prediction, - inputs = [gr.Audio(source="upload", type="filepath", optional=True), - gr.Dropdown([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])], - outputs = [df, gr.Image(label="Visualisation")], - theme = "huggingface", - title = "BatDetect2 Demo", - description = descr_txt, - examples = examples, - allow_flagging = 'never', + fn=make_prediction, + inputs=[ + gr.Audio(source="upload", type="filepath", optional=True), + gr.Dropdown([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]), + ], + outputs=[df, gr.Image(label="Visualisation")], + theme="huggingface", + title="BatDetect2 Demo", + description=descr_txt, + examples=examples, + allow_flagging="never", ).launch() - - diff --git a/bat_detect/detector/compute_features.py b/bat_detect/detector/compute_features.py index b6f4b8b..368c2db 100644 --- a/bat_detect/detector/compute_features.py +++ b/bat_detect/detector/compute_features.py @@ -2,8 +2,10 @@ import numpy as np def convert_int_to_freq(spec_ind, spec_height, min_freq, max_freq): - spec_ind = spec_height-spec_ind - return round((spec_ind / float(spec_height)) * (max_freq - min_freq) + min_freq, 2) + spec_ind = spec_height - spec_ind + return round( + (spec_ind / float(spec_height)) * (max_freq - min_freq) + min_freq, 2 + ) def extract_spec_slices(spec, pred_nms, params): @@ -11,28 +13,40 @@ def extract_spec_slices(spec, pred_nms, params): Extracts spectrogram slices from spectrogram based on detected call locations. """ - x_pos = pred_nms['x_pos'] - y_pos = pred_nms['y_pos'] - bb_width = pred_nms['bb_width'] - bb_height = pred_nms['bb_height'] - slices = [] + x_pos = pred_nms["x_pos"] + y_pos = pred_nms["y_pos"] + bb_width = pred_nms["bb_width"] + bb_height = pred_nms["bb_height"] + slices = [] # add 20% padding either side of call - pad = bb_width*0.2 - x_pos_pad = x_pos - pad - bb_width_pad = bb_width + 2*pad + pad = bb_width * 0.2 + x_pos_pad = x_pos - pad + bb_width_pad = bb_width + 2 * pad - for ff in range(len(pred_nms['det_probs'])): + for ff in range(len(pred_nms["det_probs"])): x_start = int(np.maximum(0, x_pos_pad[ff])) - x_end = int(np.minimum(spec.shape[1]-1, np.round(x_pos_pad[ff] + bb_width_pad[ff]))) + x_end = int( + np.minimum( + spec.shape[1] - 1, np.round(x_pos_pad[ff] + bb_width_pad[ff]) + ) + ) slices.append(spec[:, x_start:x_end].astype(np.float16)) return slices def get_feature_names(): - feature_names = ['duration', 'low_freq_bb', 'high_freq_bb', 'bandwidth', - 'max_power_bb', 'max_power', 'max_power_first', - 'max_power_second', 'call_interval'] + feature_names = [ + "duration", + "low_freq_bb", + "high_freq_bb", + "bandwidth", + "max_power_bb", + "max_power", + "max_power_first", + "max_power_second", + "call_interval", + ] return feature_names @@ -45,40 +59,76 @@ def get_feats(spec, pred_nms, params): https://github.com/YvesBas/Tadarida-D/blob/master/Manual_Tadarida-D.odt """ - x_pos = pred_nms['x_pos'] - y_pos = pred_nms['y_pos'] - bb_width = pred_nms['bb_width'] - bb_height = pred_nms['bb_height'] + x_pos = pred_nms["x_pos"] + y_pos = pred_nms["y_pos"] + bb_width = pred_nms["bb_width"] + bb_height = pred_nms["bb_height"] - feature_names = get_feature_names() - num_detections = len(pred_nms['det_probs']) - features = np.ones((num_detections, len(feature_names)), dtype=np.float32)*-1 + feature_names = get_feature_names() + num_detections = len(pred_nms["det_probs"]) + features = ( + np.ones((num_detections, len(feature_names)), dtype=np.float32) * -1 + ) for ff in range(num_detections): x_start = int(np.maximum(0, x_pos[ff])) - x_end = int(np.minimum(spec.shape[1]-1, np.round(x_pos[ff] + bb_width[ff]))) + x_end = int( + np.minimum(spec.shape[1] - 1, np.round(x_pos[ff] + bb_width[ff])) + ) # y low is the lowest freq but it will have a higher value due to array starting at 0 at top - y_low = int(np.minimum(spec.shape[0]-1, y_pos[ff])) - y_high = int(np.maximum(0, np.round(y_pos[ff] - bb_height[ff]))) + y_low = int(np.minimum(spec.shape[0] - 1, y_pos[ff])) + y_high = int(np.maximum(0, np.round(y_pos[ff] - bb_height[ff]))) spec_slice = spec[:, x_start:x_end] if spec_slice.shape[1] > 1: - features[ff, 0] = round(pred_nms['end_times'][ff] - pred_nms['start_times'][ff], 5) - features[ff, 1] = int(pred_nms['low_freqs'][ff]) - features[ff, 2] = int(pred_nms['high_freqs'][ff]) - features[ff, 3] = int(pred_nms['high_freqs'][ff] - pred_nms['low_freqs'][ff]) - features[ff, 4] = int(convert_int_to_freq(y_high+spec_slice[y_high:y_low, :].sum(1).argmax(), - spec.shape[0], params['min_freq'], params['max_freq'])) - features[ff, 5] = int(convert_int_to_freq(spec_slice.sum(1).argmax(), - spec.shape[0], params['min_freq'], params['max_freq'])) - hlf_val = spec_slice.shape[1]//2 + features[ff, 0] = round( + pred_nms["end_times"][ff] - pred_nms["start_times"][ff], 5 + ) + features[ff, 1] = int(pred_nms["low_freqs"][ff]) + features[ff, 2] = int(pred_nms["high_freqs"][ff]) + features[ff, 3] = int( + pred_nms["high_freqs"][ff] - pred_nms["low_freqs"][ff] + ) + features[ff, 4] = int( + convert_int_to_freq( + y_high + spec_slice[y_high:y_low, :].sum(1).argmax(), + spec.shape[0], + params["min_freq"], + params["max_freq"], + ) + ) + features[ff, 5] = int( + convert_int_to_freq( + spec_slice.sum(1).argmax(), + spec.shape[0], + params["min_freq"], + params["max_freq"], + ) + ) + hlf_val = spec_slice.shape[1] // 2 - features[ff, 6] = int(convert_int_to_freq(spec_slice[:, :hlf_val].sum(1).argmax(), - spec.shape[0], params['min_freq'], params['max_freq'])) - features[ff, 7] = int(convert_int_to_freq(spec_slice[:, hlf_val:].sum(1).argmax(), - spec.shape[0], params['min_freq'], params['max_freq'])) + features[ff, 6] = int( + convert_int_to_freq( + spec_slice[:, :hlf_val].sum(1).argmax(), + spec.shape[0], + params["min_freq"], + params["max_freq"], + ) + ) + features[ff, 7] = int( + convert_int_to_freq( + spec_slice[:, hlf_val:].sum(1).argmax(), + spec.shape[0], + params["min_freq"], + params["max_freq"], + ) + ) if ff > 0: - features[ff, 8] = round(pred_nms['start_times'][ff] - pred_nms['start_times'][ff-1], 5) + features[ff, 8] = round( + pred_nms["start_times"][ff] + - pred_nms["start_times"][ff - 1], + 5, + ) return features diff --git a/bat_detect/detector/model_helpers.py b/bat_detect/detector/model_helpers.py index c91ef04..e237f7c 100644 --- a/bat_detect/detector/model_helpers.py +++ b/bat_detect/detector/model_helpers.py @@ -1,47 +1,71 @@ -import torch.nn as nn -import torch -import torch.nn.functional as F -import numpy as np import math +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + class SelfAttention(nn.Module): def __init__(self, ip_dim, att_dim): super(SelfAttention, self).__init__() # Note, does not encode position information (absolute or realtive) self.temperature = 1.0 - self.att_dim = att_dim + self.att_dim = att_dim self.key_fun = nn.Linear(ip_dim, att_dim) self.val_fun = nn.Linear(ip_dim, att_dim) self.que_fun = nn.Linear(ip_dim, att_dim) self.pro_fun = nn.Linear(att_dim, ip_dim) def forward(self, x): - x = x.squeeze(2).permute(0,2,1) + x = x.squeeze(2).permute(0, 2, 1) - kk = torch.matmul(x, self.key_fun.weight.T) + self.key_fun.bias.unsqueeze(0).unsqueeze(0) - qq = torch.matmul(x, self.que_fun.weight.T) + self.que_fun.bias.unsqueeze(0).unsqueeze(0) - vv = torch.matmul(x, self.val_fun.weight.T) + self.val_fun.bias.unsqueeze(0).unsqueeze(0) + kk = torch.matmul( + x, self.key_fun.weight.T + ) + self.key_fun.bias.unsqueeze(0).unsqueeze(0) + qq = torch.matmul( + x, self.que_fun.weight.T + ) + self.que_fun.bias.unsqueeze(0).unsqueeze(0) + vv = torch.matmul( + x, self.val_fun.weight.T + ) + self.val_fun.bias.unsqueeze(0).unsqueeze(0) - kk_qq = torch.bmm(kk, qq.permute(0,2,1)) / (self.temperature*self.att_dim) - att_weights = F.softmax(kk_qq, 1) # each col of each attention matrix sums to 1 - att = torch.bmm(vv.permute(0,2,1), att_weights) + kk_qq = torch.bmm(kk, qq.permute(0, 2, 1)) / ( + self.temperature * self.att_dim + ) + att_weights = F.softmax( + kk_qq, 1 + ) # each col of each attention matrix sums to 1 + att = torch.bmm(vv.permute(0, 2, 1), att_weights) - op = torch.matmul(att.permute(0,2,1), self.pro_fun.weight.T) + self.pro_fun.bias.unsqueeze(0).unsqueeze(0) - op = op.permute(0,2,1).unsqueeze(2) + op = torch.matmul( + att.permute(0, 2, 1), self.pro_fun.weight.T + ) + self.pro_fun.bias.unsqueeze(0).unsqueeze(0) + op = op.permute(0, 2, 1).unsqueeze(2) return op class ConvBlockDownCoordF(nn.Module): - def __init__(self, in_chn, out_chn, ip_height, k_size=3, pad_size=1, stride=1): + def __init__( + self, in_chn, out_chn, ip_height, k_size=3, pad_size=1, stride=1 + ): super(ConvBlockDownCoordF, self).__init__() - self.coords = nn.Parameter(torch.linspace(-1, 1, ip_height)[None, None, ..., None], requires_grad=False) - self.conv = nn.Conv2d(in_chn+1, out_chn, kernel_size=k_size, padding=pad_size, stride=stride) + self.coords = nn.Parameter( + torch.linspace(-1, 1, ip_height)[None, None, ..., None], + requires_grad=False, + ) + self.conv = nn.Conv2d( + in_chn + 1, + out_chn, + kernel_size=k_size, + padding=pad_size, + stride=stride, + ) self.conv_bn = nn.BatchNorm2d(out_chn) def forward(self, x): - freq_info = self.coords.repeat(x.shape[0],1,1,x.shape[3]) + freq_info = self.coords.repeat(x.shape[0], 1, 1, x.shape[3]) x = torch.cat((x, freq_info), 1) x = F.max_pool2d(self.conv(x), 2, 2) x = F.relu(self.conv_bn(x), inplace=True) @@ -49,9 +73,17 @@ class ConvBlockDownCoordF(nn.Module): class ConvBlockDownStandard(nn.Module): - def __init__(self, in_chn, out_chn, ip_height=None, k_size=3, pad_size=1, stride=1): + def __init__( + self, in_chn, out_chn, ip_height=None, k_size=3, pad_size=1, stride=1 + ): super(ConvBlockDownStandard, self).__init__() - self.conv = nn.Conv2d(in_chn, out_chn, kernel_size=k_size, padding=pad_size, stride=stride) + self.conv = nn.Conv2d( + in_chn, + out_chn, + kernel_size=k_size, + padding=pad_size, + stride=stride, + ) self.conv_bn = nn.BatchNorm2d(out_chn) def forward(self, x): @@ -61,17 +93,41 @@ class ConvBlockDownStandard(nn.Module): class ConvBlockUpF(nn.Module): - def __init__(self, in_chn, out_chn, ip_height, k_size=3, pad_size=1, up_mode='bilinear', up_scale=(2,2)): + def __init__( + self, + in_chn, + out_chn, + ip_height, + k_size=3, + pad_size=1, + up_mode="bilinear", + up_scale=(2, 2), + ): super(ConvBlockUpF, self).__init__() self.up_scale = up_scale self.up_mode = up_mode - self.coords = nn.Parameter(torch.linspace(-1, 1, ip_height*up_scale[0])[None, None, ..., None], requires_grad=False) - self.conv = nn.Conv2d(in_chn+1, out_chn, kernel_size=k_size, padding=pad_size) + self.coords = nn.Parameter( + torch.linspace(-1, 1, ip_height * up_scale[0])[ + None, None, ..., None + ], + requires_grad=False, + ) + self.conv = nn.Conv2d( + in_chn + 1, out_chn, kernel_size=k_size, padding=pad_size + ) self.conv_bn = nn.BatchNorm2d(out_chn) def forward(self, x): - op = F.interpolate(x, size=(x.shape[-2]*self.up_scale[0], x.shape[-1]*self.up_scale[1]), mode=self.up_mode, align_corners=False) - freq_info = self.coords.repeat(op.shape[0],1,1,op.shape[3]) + op = F.interpolate( + x, + size=( + x.shape[-2] * self.up_scale[0], + x.shape[-1] * self.up_scale[1], + ), + mode=self.up_mode, + align_corners=False, + ) + freq_info = self.coords.repeat(op.shape[0], 1, 1, op.shape[3]) op = torch.cat((op, freq_info), 1) op = self.conv(op) op = F.relu(self.conv_bn(op), inplace=True) @@ -79,15 +135,34 @@ class ConvBlockUpF(nn.Module): class ConvBlockUpStandard(nn.Module): - def __init__(self, in_chn, out_chn, ip_height=None, k_size=3, pad_size=1, up_mode='bilinear', up_scale=(2,2)): + def __init__( + self, + in_chn, + out_chn, + ip_height=None, + k_size=3, + pad_size=1, + up_mode="bilinear", + up_scale=(2, 2), + ): super(ConvBlockUpStandard, self).__init__() self.up_scale = up_scale self.up_mode = up_mode - self.conv = nn.Conv2d(in_chn, out_chn, kernel_size=k_size, padding=pad_size) + self.conv = nn.Conv2d( + in_chn, out_chn, kernel_size=k_size, padding=pad_size + ) self.conv_bn = nn.BatchNorm2d(out_chn) def forward(self, x): - op = F.interpolate(x, size=(x.shape[-2]*self.up_scale[0], x.shape[-1]*self.up_scale[1]), mode=self.up_mode, align_corners=False) + op = F.interpolate( + x, + size=( + x.shape[-2] * self.up_scale[0], + x.shape[-1] * self.up_scale[1], + ), + mode=self.up_mode, + align_corners=False, + ) op = self.conv(op) op = F.relu(self.conv_bn(op), inplace=True) return op diff --git a/bat_detect/detector/models.py b/bat_detect/detector/models.py index fc7b5b4..b39cbf4 100644 --- a/bat_detect/detector/models.py +++ b/bat_detect/detector/models.py @@ -1,52 +1,97 @@ -import torch.nn as nn -import torch -import torch.nn.functional as F import numpy as np -from .model_helpers import * - -import torchvision - +import torch import torch.fft +import torch.nn as nn +import torch.nn.functional as F +import torchvision from torch import nn +from .model_helpers import * + class Net2DFast(nn.Module): - def __init__(self, num_filts, num_classes=0, emb_dim=0, ip_height=128, resize_factor=0.5): + def __init__( + self, + num_filts, + num_classes=0, + emb_dim=0, + ip_height=128, + resize_factor=0.5, + ): super(Net2DFast, self).__init__() self.num_classes = num_classes self.emb_dim = emb_dim self.num_filts = num_filts self.resize_factor = resize_factor self.ip_height_rs = ip_height - self.bneck_height = self.ip_height_rs//32 + self.bneck_height = self.ip_height_rs // 32 # encoder - self.conv_dn_0 = ConvBlockDownCoordF(1, num_filts//4, self.ip_height_rs, k_size=3, pad_size=1, stride=1) - self.conv_dn_1 = ConvBlockDownCoordF(num_filts//4, num_filts//2, self.ip_height_rs//2, k_size=3, pad_size=1, stride=1) - self.conv_dn_2 = ConvBlockDownCoordF(num_filts//2, num_filts, self.ip_height_rs//4, k_size=3, pad_size=1, stride=1) - self.conv_dn_3 = nn.Conv2d(num_filts, num_filts*2, 3, padding=1) - self.conv_dn_3_bn = nn.BatchNorm2d(num_filts*2) + self.conv_dn_0 = ConvBlockDownCoordF( + 1, + num_filts // 4, + self.ip_height_rs, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_1 = ConvBlockDownCoordF( + num_filts // 4, + num_filts // 2, + self.ip_height_rs // 2, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_2 = ConvBlockDownCoordF( + num_filts // 2, + num_filts, + self.ip_height_rs // 4, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_3 = nn.Conv2d(num_filts, num_filts * 2, 3, padding=1) + self.conv_dn_3_bn = nn.BatchNorm2d(num_filts * 2) # bottleneck - self.conv_1d = nn.Conv2d(num_filts*2, num_filts*2, (self.ip_height_rs//8,1), padding=0) - self.conv_1d_bn = nn.BatchNorm2d(num_filts*2) - self.att = SelfAttention(num_filts*2, num_filts*2) + self.conv_1d = nn.Conv2d( + num_filts * 2, + num_filts * 2, + (self.ip_height_rs // 8, 1), + padding=0, + ) + self.conv_1d_bn = nn.BatchNorm2d(num_filts * 2) + self.att = SelfAttention(num_filts * 2, num_filts * 2) # decoder - self.conv_up_2 = ConvBlockUpF(num_filts*2, num_filts//2, self.ip_height_rs//8) - self.conv_up_3 = ConvBlockUpF(num_filts//2, num_filts//4, self.ip_height_rs//4) - self.conv_up_4 = ConvBlockUpF(num_filts//4, num_filts//4, self.ip_height_rs//2) + self.conv_up_2 = ConvBlockUpF( + num_filts * 2, num_filts // 2, self.ip_height_rs // 8 + ) + self.conv_up_3 = ConvBlockUpF( + num_filts // 2, num_filts // 4, self.ip_height_rs // 4 + ) + self.conv_up_4 = ConvBlockUpF( + num_filts // 4, num_filts // 4, self.ip_height_rs // 2 + ) # output # +1 to include background class for class output - self.conv_op = nn.Conv2d(num_filts//4, num_filts//4, kernel_size=3, padding=1) - self.conv_op_bn = nn.BatchNorm2d(num_filts//4) - self.conv_size_op = nn.Conv2d(num_filts//4, 2, kernel_size=1, padding=0) - self.conv_classes_op = nn.Conv2d(num_filts//4, self.num_classes+1, kernel_size=1, padding=0) + self.conv_op = nn.Conv2d( + num_filts // 4, num_filts // 4, kernel_size=3, padding=1 + ) + self.conv_op_bn = nn.BatchNorm2d(num_filts // 4) + self.conv_size_op = nn.Conv2d( + num_filts // 4, 2, kernel_size=1, padding=0 + ) + self.conv_classes_op = nn.Conv2d( + num_filts // 4, self.num_classes + 1, kernel_size=1, padding=0 + ) if self.emb_dim > 0: - self.conv_emb = nn.Conv2d(num_filts, self.emb_dim, kernel_size=1, padding=0) - + self.conv_emb = nn.Conv2d( + num_filts, self.emb_dim, kernel_size=1, padding=0 + ) def forward(self, ip, return_feats=False): @@ -59,33 +104,40 @@ class Net2DFast(nn.Module): # bottleneck x = F.relu(self.conv_1d_bn(self.conv_1d(x3)), inplace=True) x = self.att(x) - x = x.repeat([1,1,self.bneck_height*4,1]) + x = x.repeat([1, 1, self.bneck_height * 4, 1]) # decoder - x = self.conv_up_2(x+x3) - x = self.conv_up_3(x+x2) - x = self.conv_up_4(x+x1) + x = self.conv_up_2(x + x3) + x = self.conv_up_3(x + x2) + x = self.conv_up_4(x + x1) # output x = F.relu(self.conv_op_bn(self.conv_op(x)), inplace=True) - cls = self.conv_classes_op(x) + cls = self.conv_classes_op(x) comb = torch.softmax(cls, 1) op = {} - op['pred_det'] = comb[:,:-1, :, :].sum(1).unsqueeze(1) - op['pred_size'] = F.relu(self.conv_size_op(x), inplace=True) - op['pred_class'] = comb - op['pred_class_un_norm'] = cls + op["pred_det"] = comb[:, :-1, :, :].sum(1).unsqueeze(1) + op["pred_size"] = F.relu(self.conv_size_op(x), inplace=True) + op["pred_class"] = comb + op["pred_class_un_norm"] = cls if self.emb_dim > 0: - op['pred_emb'] = self.conv_emb(x) + op["pred_emb"] = self.conv_emb(x) if return_feats: - op['features'] = x + op["features"] = x return op class Net2DFastNoAttn(nn.Module): - def __init__(self, num_filts, num_classes=0, emb_dim=0, ip_height=128, resize_factor=0.5): + def __init__( + self, + num_filts, + num_classes=0, + emb_dim=0, + ip_height=128, + resize_factor=0.5, + ): super(Net2DFastNoAttn, self).__init__() self.num_classes = num_classes @@ -93,31 +145,70 @@ class Net2DFastNoAttn(nn.Module): self.num_filts = num_filts self.resize_factor = resize_factor self.ip_height_rs = ip_height - self.bneck_height = self.ip_height_rs//32 + self.bneck_height = self.ip_height_rs // 32 - self.conv_dn_0 = ConvBlockDownCoordF(1, num_filts//4, self.ip_height_rs, k_size=3, pad_size=1, stride=1) - self.conv_dn_1 = ConvBlockDownCoordF(num_filts//4, num_filts//2, self.ip_height_rs//2, k_size=3, pad_size=1, stride=1) - self.conv_dn_2 = ConvBlockDownCoordF(num_filts//2, num_filts, self.ip_height_rs//4, k_size=3, pad_size=1, stride=1) - self.conv_dn_3 = nn.Conv2d(num_filts, num_filts*2, 3, padding=1) - self.conv_dn_3_bn = nn.BatchNorm2d(num_filts*2) + self.conv_dn_0 = ConvBlockDownCoordF( + 1, + num_filts // 4, + self.ip_height_rs, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_1 = ConvBlockDownCoordF( + num_filts // 4, + num_filts // 2, + self.ip_height_rs // 2, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_2 = ConvBlockDownCoordF( + num_filts // 2, + num_filts, + self.ip_height_rs // 4, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_3 = nn.Conv2d(num_filts, num_filts * 2, 3, padding=1) + self.conv_dn_3_bn = nn.BatchNorm2d(num_filts * 2) - self.conv_1d = nn.Conv2d(num_filts*2, num_filts*2, (self.ip_height_rs//8,1), padding=0) - self.conv_1d_bn = nn.BatchNorm2d(num_filts*2) + self.conv_1d = nn.Conv2d( + num_filts * 2, + num_filts * 2, + (self.ip_height_rs // 8, 1), + padding=0, + ) + self.conv_1d_bn = nn.BatchNorm2d(num_filts * 2) - - self.conv_up_2 = ConvBlockUpF(num_filts*2, num_filts//2, self.ip_height_rs//8) - self.conv_up_3 = ConvBlockUpF(num_filts//2, num_filts//4, self.ip_height_rs//4) - self.conv_up_4 = ConvBlockUpF(num_filts//4, num_filts//4, self.ip_height_rs//2) + self.conv_up_2 = ConvBlockUpF( + num_filts * 2, num_filts // 2, self.ip_height_rs // 8 + ) + self.conv_up_3 = ConvBlockUpF( + num_filts // 2, num_filts // 4, self.ip_height_rs // 4 + ) + self.conv_up_4 = ConvBlockUpF( + num_filts // 4, num_filts // 4, self.ip_height_rs // 2 + ) # output # +1 to include background class for class output - self.conv_op = nn.Conv2d(num_filts//4, num_filts//4, kernel_size=3, padding=1) - self.conv_op_bn = nn.BatchNorm2d(num_filts//4) - self.conv_size_op = nn.Conv2d(num_filts//4, 2, kernel_size=1, padding=0) - self.conv_classes_op = nn.Conv2d(num_filts//4, self.num_classes+1, kernel_size=1, padding=0) + self.conv_op = nn.Conv2d( + num_filts // 4, num_filts // 4, kernel_size=3, padding=1 + ) + self.conv_op_bn = nn.BatchNorm2d(num_filts // 4) + self.conv_size_op = nn.Conv2d( + num_filts // 4, 2, kernel_size=1, padding=0 + ) + self.conv_classes_op = nn.Conv2d( + num_filts // 4, self.num_classes + 1, kernel_size=1, padding=0 + ) if self.emb_dim > 0: - self.conv_emb = nn.Conv2d(num_filts, self.emb_dim, kernel_size=1, padding=0) + self.conv_emb = nn.Conv2d( + num_filts, self.emb_dim, kernel_size=1, padding=0 + ) def forward(self, ip, return_feats=False): @@ -127,31 +218,38 @@ class Net2DFastNoAttn(nn.Module): x3 = F.relu(self.conv_dn_3_bn(self.conv_dn_3(x3)), inplace=True) x = F.relu(self.conv_1d_bn(self.conv_1d(x3)), inplace=True) - x = x.repeat([1,1,self.bneck_height*4,1]) + x = x.repeat([1, 1, self.bneck_height * 4, 1]) - x = self.conv_up_2(x+x3) - x = self.conv_up_3(x+x2) - x = self.conv_up_4(x+x1) + x = self.conv_up_2(x + x3) + x = self.conv_up_3(x + x2) + x = self.conv_up_4(x + x1) x = F.relu(self.conv_op_bn(self.conv_op(x)), inplace=True) - cls = self.conv_classes_op(x) + cls = self.conv_classes_op(x) comb = torch.softmax(cls, 1) op = {} - op['pred_det'] = comb[:,:-1, :, :].sum(1).unsqueeze(1) - op['pred_size'] = F.relu(self.conv_size_op(x), inplace=True) - op['pred_class'] = comb - op['pred_class_un_norm'] = cls + op["pred_det"] = comb[:, :-1, :, :].sum(1).unsqueeze(1) + op["pred_size"] = F.relu(self.conv_size_op(x), inplace=True) + op["pred_class"] = comb + op["pred_class_un_norm"] = cls if self.emb_dim > 0: - op['pred_emb'] = self.conv_emb(x) + op["pred_emb"] = self.conv_emb(x) if return_feats: - op['features'] = x + op["features"] = x return op class Net2DFastNoCoordConv(nn.Module): - def __init__(self, num_filts, num_classes=0, emb_dim=0, ip_height=128, resize_factor=0.5): + def __init__( + self, + num_filts, + num_classes=0, + emb_dim=0, + ip_height=128, + resize_factor=0.5, + ): super(Net2DFastNoCoordConv, self).__init__() self.num_classes = num_classes @@ -159,32 +257,72 @@ class Net2DFastNoCoordConv(nn.Module): self.num_filts = num_filts self.resize_factor = resize_factor self.ip_height_rs = ip_height - self.bneck_height = self.ip_height_rs//32 + self.bneck_height = self.ip_height_rs // 32 - self.conv_dn_0 = ConvBlockDownStandard(1, num_filts//4, self.ip_height_rs, k_size=3, pad_size=1, stride=1) - self.conv_dn_1 = ConvBlockDownStandard(num_filts//4, num_filts//2, self.ip_height_rs//2, k_size=3, pad_size=1, stride=1) - self.conv_dn_2 = ConvBlockDownStandard(num_filts//2, num_filts, self.ip_height_rs//4, k_size=3, pad_size=1, stride=1) - self.conv_dn_3 = nn.Conv2d(num_filts, num_filts*2, 3, padding=1) - self.conv_dn_3_bn = nn.BatchNorm2d(num_filts*2) + self.conv_dn_0 = ConvBlockDownStandard( + 1, + num_filts // 4, + self.ip_height_rs, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_1 = ConvBlockDownStandard( + num_filts // 4, + num_filts // 2, + self.ip_height_rs // 2, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_2 = ConvBlockDownStandard( + num_filts // 2, + num_filts, + self.ip_height_rs // 4, + k_size=3, + pad_size=1, + stride=1, + ) + self.conv_dn_3 = nn.Conv2d(num_filts, num_filts * 2, 3, padding=1) + self.conv_dn_3_bn = nn.BatchNorm2d(num_filts * 2) - self.conv_1d = nn.Conv2d(num_filts*2, num_filts*2, (self.ip_height_rs//8,1), padding=0) - self.conv_1d_bn = nn.BatchNorm2d(num_filts*2) + self.conv_1d = nn.Conv2d( + num_filts * 2, + num_filts * 2, + (self.ip_height_rs // 8, 1), + padding=0, + ) + self.conv_1d_bn = nn.BatchNorm2d(num_filts * 2) - self.att = SelfAttention(num_filts*2, num_filts*2) + self.att = SelfAttention(num_filts * 2, num_filts * 2) - self.conv_up_2 = ConvBlockUpStandard(num_filts*2, num_filts//2, self.ip_height_rs//8) - self.conv_up_3 = ConvBlockUpStandard(num_filts//2, num_filts//4, self.ip_height_rs//4) - self.conv_up_4 = ConvBlockUpStandard(num_filts//4, num_filts//4, self.ip_height_rs//2) + self.conv_up_2 = ConvBlockUpStandard( + num_filts * 2, num_filts // 2, self.ip_height_rs // 8 + ) + self.conv_up_3 = ConvBlockUpStandard( + num_filts // 2, num_filts // 4, self.ip_height_rs // 4 + ) + self.conv_up_4 = ConvBlockUpStandard( + num_filts // 4, num_filts // 4, self.ip_height_rs // 2 + ) # output # +1 to include background class for class output - self.conv_op = nn.Conv2d(num_filts//4, num_filts//4, kernel_size=3, padding=1) - self.conv_op_bn = nn.BatchNorm2d(num_filts//4) - self.conv_size_op = nn.Conv2d(num_filts//4, 2, kernel_size=1, padding=0) - self.conv_classes_op = nn.Conv2d(num_filts//4, self.num_classes+1, kernel_size=1, padding=0) + self.conv_op = nn.Conv2d( + num_filts // 4, num_filts // 4, kernel_size=3, padding=1 + ) + self.conv_op_bn = nn.BatchNorm2d(num_filts // 4) + self.conv_size_op = nn.Conv2d( + num_filts // 4, 2, kernel_size=1, padding=0 + ) + self.conv_classes_op = nn.Conv2d( + num_filts // 4, self.num_classes + 1, kernel_size=1, padding=0 + ) if self.emb_dim > 0: - self.conv_emb = nn.Conv2d(num_filts, self.emb_dim, kernel_size=1, padding=0) + self.conv_emb = nn.Conv2d( + num_filts, self.emb_dim, kernel_size=1, padding=0 + ) def forward(self, ip, return_feats=False): @@ -195,24 +333,24 @@ class Net2DFastNoCoordConv(nn.Module): x = F.relu(self.conv_1d_bn(self.conv_1d(x3)), inplace=True) x = self.att(x) - x = x.repeat([1,1,self.bneck_height*4,1]) + x = x.repeat([1, 1, self.bneck_height * 4, 1]) - x = self.conv_up_2(x+x3) - x = self.conv_up_3(x+x2) - x = self.conv_up_4(x+x1) + x = self.conv_up_2(x + x3) + x = self.conv_up_3(x + x2) + x = self.conv_up_4(x + x1) x = F.relu(self.conv_op_bn(self.conv_op(x)), inplace=True) - cls = self.conv_classes_op(x) + cls = self.conv_classes_op(x) comb = torch.softmax(cls, 1) op = {} - op['pred_det'] = comb[:,:-1, :, :].sum(1).unsqueeze(1) - op['pred_size'] = F.relu(self.conv_size_op(x), inplace=True) - op['pred_class'] = comb - op['pred_class_un_norm'] = cls + op["pred_det"] = comb[:, :-1, :, :].sum(1).unsqueeze(1) + op["pred_size"] = F.relu(self.conv_size_op(x), inplace=True) + op["pred_class"] = comb + op["pred_class_un_norm"] = cls if self.emb_dim > 0: - op['pred_emb'] = self.conv_emb(x) + op["pred_emb"] = self.conv_emb(x) if return_feats: - op['features'] = x + op["features"] = x return op diff --git a/bat_detect/detector/parameters.py b/bat_detect/detector/parameters.py index 10276eb..d93ac8c 100644 --- a/bat_detect/detector/parameters.py +++ b/bat_detect/detector/parameters.py @@ -1,108 +1,164 @@ -import numpy as np -import os import datetime +import os + +import numpy as np def mk_dir(path): if not os.path.isdir(path): os.makedirs(path) - - -def get_params(make_dirs=False, exps_dir='../../experiments/'): + + +def get_params(make_dirs=False, exps_dir="../../experiments/"): params = {} - params['model_name'] = 'Net2DFast' # Net2DFast, Net2DSkip, Net2DSimple, Net2DSkipDS, Net2DRN - params['num_filters'] = 128 + params[ + "model_name" + ] = "Net2DFast" # Net2DFast, Net2DSkip, Net2DSimple, Net2DSkipDS, Net2DRN + params["num_filters"] = 128 now_str = datetime.datetime.now().strftime("%Y_%m_%d__%H_%M_%S") - model_name = now_str + '.pth.tar' - params['experiment'] = os.path.join(exps_dir, now_str, '') - params['model_file_name'] = os.path.join(params['experiment'], model_name) - params['op_im_dir'] = os.path.join(params['experiment'], 'op_ims', '') - params['op_im_dir_test'] = os.path.join(params['experiment'], 'op_ims_test', '') - #params['notes'] = '' # can save notes about an experiment here - + model_name = now_str + ".pth.tar" + params["experiment"] = os.path.join(exps_dir, now_str, "") + params["model_file_name"] = os.path.join(params["experiment"], model_name) + params["op_im_dir"] = os.path.join(params["experiment"], "op_ims", "") + params["op_im_dir_test"] = os.path.join( + params["experiment"], "op_ims_test", "" + ) + # params['notes'] = '' # can save notes about an experiment here # spec parameters - params['target_samp_rate'] = 256000 # resamples all audio so that it is at this rate - params['fft_win_length'] = 512 / 256000.0 # in milliseconds, amount of time per stft time step - params['fft_overlap'] = 0.75 # stft window overlap + params[ + "target_samp_rate" + ] = 256000 # resamples all audio so that it is at this rate + params["fft_win_length"] = ( + 512 / 256000.0 + ) # in milliseconds, amount of time per stft time step + params["fft_overlap"] = 0.75 # stft window overlap - params['max_freq'] = 120000 # in Hz, everything above this will be discarded - params['min_freq'] = 10000 # in Hz, everything below this will be discarded + params[ + "max_freq" + ] = 120000 # in Hz, everything above this will be discarded + params[ + "min_freq" + ] = 10000 # in Hz, everything below this will be discarded - params['resize_factor'] = 0.5 # resize so the spectrogram at the input of the network - params['spec_height'] = 256 # units are number of frequency bins (before resizing is performed) - params['spec_train_width'] = 512 # units are number of time steps (before resizing is performed) - params['spec_divide_factor'] = 32 # spectrogram should be divisible by this amount in width and height + params[ + "resize_factor" + ] = 0.5 # resize so the spectrogram at the input of the network + params[ + "spec_height" + ] = 256 # units are number of frequency bins (before resizing is performed) + params[ + "spec_train_width" + ] = 512 # units are number of time steps (before resizing is performed) + params[ + "spec_divide_factor" + ] = 32 # spectrogram should be divisible by this amount in width and height # spec processing params - params['denoise_spec_avg'] = True # removes the mean for each frequency band - params['scale_raw_audio'] = False # scales the raw audio to [-1, 1] - params['max_scale_spec'] = False # scales the spectrogram so that it is max 1 - params['spec_scale'] = 'pcen' # 'log', 'pcen', 'none' + params[ + "denoise_spec_avg" + ] = True # removes the mean for each frequency band + params["scale_raw_audio"] = False # scales the raw audio to [-1, 1] + params[ + "max_scale_spec" + ] = False # scales the spectrogram so that it is max 1 + params["spec_scale"] = "pcen" # 'log', 'pcen', 'none' # detection params - params['detection_overlap'] = 0.01 # has to be within this number of ms to count as detection - params['ignore_start_end'] = 0.01 # if start of GT calls are within this time from the start/end of file ignore - params['detection_threshold'] = 0.01 # the smaller this is the better the recall will be - params['nms_kernel_size'] = 9 - params['nms_top_k_per_sec'] = 200 # keep top K highest predictions per second of audio - params['target_sigma'] = 2.0 + params[ + "detection_overlap" + ] = 0.01 # has to be within this number of ms to count as detection + params[ + "ignore_start_end" + ] = 0.01 # if start of GT calls are within this time from the start/end of file ignore + params[ + "detection_threshold" + ] = 0.01 # the smaller this is the better the recall will be + params["nms_kernel_size"] = 9 + params[ + "nms_top_k_per_sec" + ] = 200 # keep top K highest predictions per second of audio + params["target_sigma"] = 2.0 # augmentation params - params['aug_prob'] = 0.20 # augmentations will be performed with this probability - params['augment_at_train'] = True - params['augment_at_train_combine'] = True - params['echo_max_delay'] = 0.005 # simulate echo by adding copy of raw audio - params['stretch_squeeze_delta'] = 0.04 # stretch or squeeze spec - params['mask_max_time_perc'] = 0.05 # max mask size - here percentage, not ideal - params['mask_max_freq_perc'] = 0.10 # max mask size - here percentage, not ideal - params['spec_amp_scaling'] = 2.0 # multiply the "volume" by 0:X times current amount - params['aug_sampling_rates'] = [220500, 256000, 300000, 312500, 384000, 441000, 500000] + params[ + "aug_prob" + ] = 0.20 # augmentations will be performed with this probability + params["augment_at_train"] = True + params["augment_at_train_combine"] = True + params[ + "echo_max_delay" + ] = 0.005 # simulate echo by adding copy of raw audio + params["stretch_squeeze_delta"] = 0.04 # stretch or squeeze spec + params[ + "mask_max_time_perc" + ] = 0.05 # max mask size - here percentage, not ideal + params[ + "mask_max_freq_perc" + ] = 0.10 # max mask size - here percentage, not ideal + params[ + "spec_amp_scaling" + ] = 2.0 # multiply the "volume" by 0:X times current amount + params["aug_sampling_rates"] = [ + 220500, + 256000, + 300000, + 312500, + 384000, + 441000, + 500000, + ] # loss params - params['train_loss'] = 'focal' # mse or focal - params['det_loss_weight'] = 1.0 # weight for the detection part of the loss - params['size_loss_weight'] = 0.1 # weight for the bbox size loss - params['class_loss_weight'] = 2.0 # weight for the classification loss - params['individual_loss_weight'] = 0.0 # not used - if params['individual_loss_weight'] == 0.0: - params['emb_dim'] = 0 # number of dimensions used for individual id embedding + params["train_loss"] = "focal" # mse or focal + params[ + "det_loss_weight" + ] = 1.0 # weight for the detection part of the loss + params["size_loss_weight"] = 0.1 # weight for the bbox size loss + params["class_loss_weight"] = 2.0 # weight for the classification loss + params["individual_loss_weight"] = 0.0 # not used + if params["individual_loss_weight"] == 0.0: + params[ + "emb_dim" + ] = 0 # number of dimensions used for individual id embedding else: - params['emb_dim'] = 3 + params["emb_dim"] = 3 # train params - params['lr'] = 0.001 - params['batch_size'] = 8 - params['num_workers'] = 4 - params['num_epochs'] = 200 - params['num_eval_epochs'] = 5 # run evaluation every X epochs - params['device'] = 'cuda' - params['save_test_image_during_train'] = False - params['save_test_image_after_train'] = True + params["lr"] = 0.001 + params["batch_size"] = 8 + params["num_workers"] = 4 + params["num_epochs"] = 200 + params["num_eval_epochs"] = 5 # run evaluation every X epochs + params["device"] = "cuda" + params["save_test_image_during_train"] = False + params["save_test_image_after_train"] = True - params['convert_to_genus'] = False - params['genus_mapping'] = [] - params['class_names'] = [] - params['classes_to_ignore'] = ['', ' ', 'Unknown', 'Not Bat'] - params['generic_class'] = ['Bat'] - params['events_of_interest'] = ['Echolocation'] # will ignore all other types of events e.g. social calls + params["convert_to_genus"] = False + params["genus_mapping"] = [] + params["class_names"] = [] + params["classes_to_ignore"] = ["", " ", "Unknown", "Not Bat"] + params["generic_class"] = ["Bat"] + params["events_of_interest"] = [ + "Echolocation" + ] # will ignore all other types of events e.g. social calls # the classes in this list are standardized during training so that the same low and high freq are used - params['standardize_classs_names'] = [] + params["standardize_classs_names"] = [] # create directories if make_dirs: - print('Model name : ' + params['model_name']) - print('Model file : ' + params['model_file_name']) - print('Experiment : ' + params['experiment']) + print("Model name : " + params["model_name"]) + print("Model file : " + params["model_file_name"]) + print("Experiment : " + params["experiment"]) - mk_dir(params['experiment']) - if params['save_test_image_during_train']: - mk_dir(params['op_im_dir']) - if params['save_test_image_after_train']: - mk_dir(params['op_im_dir_test']) - mk_dir(os.path.dirname(params['model_file_name'])) + mk_dir(params["experiment"]) + if params["save_test_image_during_train"]: + mk_dir(params["op_im_dir"]) + if params["save_test_image_after_train"]: + mk_dir(params["op_im_dir_test"]) + mk_dir(os.path.dirname(params["model_file_name"])) return params diff --git a/bat_detect/detector/post_process.py b/bat_detect/detector/post_process.py index 757831f..2745cdf 100644 --- a/bat_detect/detector/post_process.py +++ b/bat_detect/detector/post_process.py @@ -1,35 +1,42 @@ +import numpy as np import torch import torch.nn as nn import torch.nn.functional as F -import numpy as np -np.seterr(divide='ignore', invalid='ignore') + +np.seterr(divide="ignore", invalid="ignore") def x_coords_to_time(x_pos, sampling_rate, fft_win_length, fft_overlap): - nfft = int(fft_win_length*sampling_rate) - noverlap = int(fft_overlap*nfft) - return ((x_pos*(nfft - noverlap)) + noverlap) / sampling_rate - #return (1.0 - fft_overlap) * fft_win_length * (x_pos + 0.5) # 0.5 is for center of temporal window + nfft = int(fft_win_length * sampling_rate) + noverlap = int(fft_overlap * nfft) + return ((x_pos * (nfft - noverlap)) + noverlap) / sampling_rate + # return (1.0 - fft_overlap) * fft_win_length * (x_pos + 0.5) # 0.5 is for center of temporal window def overall_class_pred(det_prob, class_prob): - weighted_pred = (class_prob*det_prob).sum(1) + weighted_pred = (class_prob * det_prob).sum(1) return weighted_pred / weighted_pred.sum() def run_nms(outputs, params, sampling_rate): - pred_det = outputs['pred_det'] # probability of box - pred_size = outputs['pred_size'] # box size + pred_det = outputs["pred_det"] # probability of box + pred_size = outputs["pred_size"] # box size - pred_det_nms = non_max_suppression(pred_det, params['nms_kernel_size']) - freq_rescale = (params['max_freq'] - params['min_freq']) /pred_det.shape[-2] + pred_det_nms = non_max_suppression(pred_det, params["nms_kernel_size"]) + freq_rescale = (params["max_freq"] - params["min_freq"]) / pred_det.shape[ + -2 + ] # NOTE there will be small differences depending on which sampling rate is chosen # as we are choosing the same sampling rate for the entire batch - duration = x_coords_to_time(pred_det.shape[-1], sampling_rate[0].item(), - params['fft_win_length'], params['fft_overlap']) - top_k = int(duration * params['nms_top_k_per_sec']) + duration = x_coords_to_time( + pred_det.shape[-1], + sampling_rate[0].item(), + params["fft_win_length"], + params["fft_overlap"], + ) + top_k = int(duration * params["nms_top_k_per_sec"]) scores, y_pos, x_pos = get_topk_scores(pred_det_nms, top_k) # loop over batch to save outputs @@ -38,30 +45,47 @@ def run_nms(outputs, params, sampling_rate): for ii in range(pred_det_nms.shape[0]): # get valid indices inds_ord = torch.argsort(x_pos[ii, :]) - valid_inds = scores[ii, inds_ord] > params['detection_threshold'] + valid_inds = scores[ii, inds_ord] > params["detection_threshold"] valid_inds = inds_ord[valid_inds] # create result dictionary pred = {} - pred['det_probs'] = scores[ii, valid_inds] - pred['x_pos'] = x_pos[ii, valid_inds] - pred['y_pos'] = y_pos[ii, valid_inds] - pred['bb_width'] = pred_size[ii, 0, pred['y_pos'], pred['x_pos']] - pred['bb_height'] = pred_size[ii, 1, pred['y_pos'], pred['x_pos']] - pred['start_times'] = x_coords_to_time(pred['x_pos'].float() / params['resize_factor'], - sampling_rate[ii].item(), params['fft_win_length'], params['fft_overlap']) - pred['end_times'] = x_coords_to_time((pred['x_pos'].float()+pred['bb_width']) / params['resize_factor'], - sampling_rate[ii].item(), params['fft_win_length'], params['fft_overlap']) - pred['low_freqs'] = (pred_size[ii].shape[1] - pred['y_pos'].float())*freq_rescale + params['min_freq'] - pred['high_freqs'] = pred['low_freqs'] + pred['bb_height']*freq_rescale + pred["det_probs"] = scores[ii, valid_inds] + pred["x_pos"] = x_pos[ii, valid_inds] + pred["y_pos"] = y_pos[ii, valid_inds] + pred["bb_width"] = pred_size[ii, 0, pred["y_pos"], pred["x_pos"]] + pred["bb_height"] = pred_size[ii, 1, pred["y_pos"], pred["x_pos"]] + pred["start_times"] = x_coords_to_time( + pred["x_pos"].float() / params["resize_factor"], + sampling_rate[ii].item(), + params["fft_win_length"], + params["fft_overlap"], + ) + pred["end_times"] = x_coords_to_time( + (pred["x_pos"].float() + pred["bb_width"]) + / params["resize_factor"], + sampling_rate[ii].item(), + params["fft_win_length"], + params["fft_overlap"], + ) + pred["low_freqs"] = ( + pred_size[ii].shape[1] - pred["y_pos"].float() + ) * freq_rescale + params["min_freq"] + pred["high_freqs"] = ( + pred["low_freqs"] + pred["bb_height"] * freq_rescale + ) # extract the per class votes - if 'pred_class' in outputs: - pred['class_probs'] = outputs['pred_class'][ii, :, y_pos[ii, valid_inds], x_pos[ii, valid_inds]] + if "pred_class" in outputs: + pred["class_probs"] = outputs["pred_class"][ + ii, :, y_pos[ii, valid_inds], x_pos[ii, valid_inds] + ] # extract the model features - if 'features' in outputs: - feat = outputs['features'][ii, :, y_pos[ii, valid_inds], x_pos[ii, valid_inds]].transpose(0, 1) + if "features" in outputs: + feat = outputs["features"][ + ii, :, y_pos[ii, valid_inds], x_pos[ii, valid_inds] + ].transpose(0, 1) feat = feat.cpu().numpy().astype(np.float32) feats.append(feat) @@ -82,7 +106,9 @@ def non_max_suppression(heat, kernel_size): pad_h = (kernel_size_h - 1) // 2 pad_w = (kernel_size_w - 1) // 2 - hmax = nn.functional.max_pool2d(heat, (kernel_size_h, kernel_size_w), stride=1, padding=(pad_h, pad_w)) + hmax = nn.functional.max_pool2d( + heat, (kernel_size_h, kernel_size_w), stride=1, padding=(pad_h, pad_w) + ) keep = (hmax == heat).float() return heat * keep @@ -94,7 +120,7 @@ def get_topk_scores(scores, K): topk_scores, topk_inds = torch.topk(scores.view(batch, -1), K) topk_inds = topk_inds % (height * width) - topk_ys = torch.div(topk_inds, width, rounding_mode='floor').long() - topk_xs = (topk_inds % width).long() + topk_ys = torch.div(topk_inds, width, rounding_mode="floor").long() + topk_xs = (topk_inds % width).long() return topk_scores, topk_ys, topk_xs diff --git a/bat_detect/evaluate/evaluate_models.py b/bat_detect/evaluate/evaluate_models.py index 0fc8ae9..6b7c460 100644 --- a/bat_detect/evaluate/evaluate_models.py +++ b/bat_detect/evaluate/evaluate_models.py @@ -2,67 +2,76 @@ Evaluates trained model on test set and generates plots. """ -import numpy as np -import sys -import os +import argparse import copy import json +import os +import sys + +import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier -import argparse -sys.path.append('../../') -import bat_detect.utils.detector_utils as du -import bat_detect.train.train_utils as tu +sys.path.append("../../") import bat_detect.detector.parameters as parameters import bat_detect.train.evaluate as evl +import bat_detect.train.train_utils as tu +import bat_detect.utils.detector_utils as du import bat_detect.utils.plot_utils as pu def get_blank_annotation(ip_str): res = {} - res['class_name'] = '' - res['duration'] = -1 - res['id'] = ''# fileName - res['issues'] = False - res['notes'] = ip_str - res['time_exp'] = 1 - res['annotated'] = False - res['annotation'] = [] + res["class_name"] = "" + res["duration"] = -1 + res["id"] = "" # fileName + res["issues"] = False + res["notes"] = ip_str + res["time_exp"] = 1 + res["annotated"] = False + res["annotation"] = [] ann = {} - ann['class'] = '' - ann['event'] = 'Echolocation' - ann['individual'] = -1 - ann['start_time'] = -1 - ann['end_time'] = -1 - ann['low_freq'] = -1 - ann['high_freq'] = -1 - ann['confidence'] = -1 + ann["class"] = "" + ann["event"] = "Echolocation" + ann["individual"] = -1 + ann["start_time"] = -1 + ann["end_time"] = -1 + ann["low_freq"] = -1 + ann["high_freq"] = -1 + ann["confidence"] = -1 return copy.deepcopy(res), copy.deepcopy(ann) def create_genus_mapping(gt_test, preds, class_names): # rolls the per class predictions and ground truth back up to genus level - class_names_genus, cls_to_genus = np.unique([cc.split(' ')[0] for cc in class_names], return_inverse=True) - genus_to_cls_map = [np.where(np.array(cls_to_genus) == cc)[0] for cc in range(len(class_names_genus))] + class_names_genus, cls_to_genus = np.unique( + [cc.split(" ")[0] for cc in class_names], return_inverse=True + ) + genus_to_cls_map = [ + np.where(np.array(cls_to_genus) == cc)[0] + for cc in range(len(class_names_genus)) + ] gt_test_g = [] for gg in gt_test: gg_g = copy.deepcopy(gg) - inds = np.where(gg_g['class_ids']!=-1)[0] - gg_g['class_ids'][inds] = cls_to_genus[gg_g['class_ids'][inds]] + inds = np.where(gg_g["class_ids"] != -1)[0] + gg_g["class_ids"][inds] = cls_to_genus[gg_g["class_ids"][inds]] gt_test_g.append(gg_g) # note, will have entries geater than one as we are summing across the respective classes preds_g = [] for pp in preds: pp_g = copy.deepcopy(pp) - pp_g['class_probs'] = np.zeros((len(class_names_genus), pp_g['class_probs'].shape[1]), dtype=np.float32) + pp_g["class_probs"] = np.zeros( + (len(class_names_genus), pp_g["class_probs"].shape[1]), + dtype=np.float32, + ) for cc, inds in enumerate(genus_to_cls_map): - pp_g['class_probs'][cc, :] = pp['class_probs'][inds, :].sum(0) + pp_g["class_probs"][cc, :] = pp["class_probs"][inds, :].sum(0) preds_g.append(pp_g) return class_names_genus, preds_g, gt_test_g @@ -70,56 +79,70 @@ def create_genus_mapping(gt_test, preds, class_names): def load_tadarida_pred(ip_dir, dataset, file_of_interest): - res, ann = get_blank_annotation('Generated by Tadarida') + res, ann = get_blank_annotation("Generated by Tadarida") # create the annotations in the correct format - da_c = pd.read_csv(ip_dir + dataset + '/' + file_of_interest.replace('.wav', '.ta').replace('.WAV', '.ta'), sep='\t') + da_c = pd.read_csv( + ip_dir + + dataset + + "/" + + file_of_interest.replace(".wav", ".ta").replace(".WAV", ".ta"), + sep="\t", + ) res_c = copy.deepcopy(res) - res_c['id'] = file_of_interest - res_c['dataset'] = dataset - res_c['feats'] = da_c.iloc[:, 6:].values.astype(np.float32) + res_c["id"] = file_of_interest + res_c["dataset"] = dataset + res_c["feats"] = da_c.iloc[:, 6:].values.astype(np.float32) if da_c.shape[0] > 0: - res_c['class_name'] = '' - res_c['class_prob'] = 0.0 + res_c["class_name"] = "" + res_c["class_prob"] = 0.0 for aa in range(da_c.shape[0]): ann_c = copy.deepcopy(ann) - ann_c['class'] = 'Not Bat' # will assign to class later - ann_c['start_time'] = np.round(da_c.iloc[aa]['StTime']/1000.0 ,5) - ann_c['end_time'] = np.round((da_c.iloc[aa]['StTime'] + da_c.iloc[aa]['Dur'])/1000.0, 5) - ann_c['low_freq'] = np.round(da_c.iloc[aa]['Fmin'] * 1000.0, 2) - ann_c['high_freq'] = np.round(da_c.iloc[aa]['Fmax'] * 1000.0, 2) - ann_c['det_prob'] = 0.0 - res_c['annotation'].append(ann_c) + ann_c["class"] = "Not Bat" # will assign to class later + ann_c["start_time"] = np.round(da_c.iloc[aa]["StTime"] / 1000.0, 5) + ann_c["end_time"] = np.round( + (da_c.iloc[aa]["StTime"] + da_c.iloc[aa]["Dur"]) / 1000.0, 5 + ) + ann_c["low_freq"] = np.round(da_c.iloc[aa]["Fmin"] * 1000.0, 2) + ann_c["high_freq"] = np.round(da_c.iloc[aa]["Fmax"] * 1000.0, 2) + ann_c["det_prob"] = 0.0 + res_c["annotation"].append(ann_c) return res_c -def load_sonobat_meta(ip_dir, datasets, region_classifier, class_names, only_accepted_species=True): +def load_sonobat_meta( + ip_dir, + datasets, + region_classifier, + class_names, + only_accepted_species=True, +): sp_dict = {} for ss in class_names: - sp_key = ss.split(' ')[0][:3] + ss.split(' ')[1][:3] + sp_key = ss.split(" ")[0][:3] + ss.split(" ")[1][:3] sp_dict[sp_key] = ss - sp_dict['x'] = '' # not bat - sp_dict['Bat'] = 'Bat' + sp_dict["x"] = "" # not bat + sp_dict["Bat"] = "Bat" sonobat_meta = {} for tt in datasets: - dataset = tt['dataset_name'] - sb_ip_dir = ip_dir + dataset + '/' + region_classifier + '/' + dataset = tt["dataset_name"] + sb_ip_dir = ip_dir + dataset + "/" + region_classifier + "/" # load the call level predictions - ip_file_p = sb_ip_dir + dataset + '_Parameters_v4.5.0.txt' - #ip_file_p = sb_ip_dir + 'audio_SonoBatch_v30.0 beta.txt' - da = pd.read_csv(ip_file_p, sep='\t') + ip_file_p = sb_ip_dir + dataset + "_Parameters_v4.5.0.txt" + # ip_file_p = sb_ip_dir + 'audio_SonoBatch_v30.0 beta.txt' + da = pd.read_csv(ip_file_p, sep="\t") # load the file level predictions - ip_file_b = sb_ip_dir + dataset + '_SonoBatch_v4.5.0.txt' - #ip_file_b = sb_ip_dir + 'audio_CumulativeParameters_v30.0 beta.txt' + ip_file_b = sb_ip_dir + dataset + "_SonoBatch_v4.5.0.txt" + # ip_file_b = sb_ip_dir + 'audio_CumulativeParameters_v30.0 beta.txt' with open(ip_file_b) as f: lines = f.readlines() @@ -129,7 +152,7 @@ def load_sonobat_meta(ip_dir, datasets, region_classifier, class_names, only_acc file_res = {} for ll in lines: # note this does not seem to parse the file very well - ll_data = ll.split('\t') + ll_data = ll.split("\t") # there are sometimes many different species names per file if only_accepted_species: @@ -137,20 +160,24 @@ def load_sonobat_meta(ip_dir, datasets, region_classifier, class_names, only_acc ind = 4 else: # choosing ""~Spp" if "SppAccp" does not exist - if ll_data[4] != 'x': - ind = 4 # choosing "SppAccp", along with "Prob" here + if ll_data[4] != "x": + ind = 4 # choosing "SppAccp", along with "Prob" here else: ind = 8 # choosing "~Spp", along with "~Prob" here sp_name_1 = sp_dict[ll_data[ind]] - prob_1 = ll_data[ind+1] - if prob_1 == 'x': + prob_1 = ll_data[ind + 1] + if prob_1 == "x": prob_1 = 0.0 - file_res[ll_data[1]] = {'id':ll_data[1], 'species_1':sp_name_1, 'prob_1':prob_1} + file_res[ll_data[1]] = { + "id": ll_data[1], + "species_1": sp_name_1, + "prob_1": prob_1, + } sonobat_meta[dataset] = {} - sonobat_meta[dataset]['file_res'] = file_res - sonobat_meta[dataset]['call_info'] = da + sonobat_meta[dataset]["file_res"] = file_res + sonobat_meta[dataset]["call_info"] = da return sonobat_meta @@ -158,34 +185,38 @@ def load_sonobat_meta(ip_dir, datasets, region_classifier, class_names, only_acc def load_sonobat_preds(dataset, id, sb_meta, set_class_name=None): # create the annotations in the correct format - res, ann = get_blank_annotation('Generated by Sonobat') + res, ann = get_blank_annotation("Generated by Sonobat") res_c = copy.deepcopy(res) - res_c['id'] = id - res_c['dataset'] = dataset + res_c["id"] = id + res_c["dataset"] = dataset - da = sb_meta[dataset]['call_info'] - da_c = da[da['Filename'] == id] + da = sb_meta[dataset]["call_info"] + da_c = da[da["Filename"] == id] - file_res = sb_meta[dataset]['file_res'] - res_c['feats'] = np.zeros((0,0)) + file_res = sb_meta[dataset]["file_res"] + res_c["feats"] = np.zeros((0, 0)) if da_c.shape[0] > 0: - res_c['class_name'] = file_res[id]['species_1'] - res_c['class_prob'] = file_res[id]['prob_1'] - res_c['feats'] = da_c.iloc[:, 3:105].values.astype(np.float32) + res_c["class_name"] = file_res[id]["species_1"] + res_c["class_prob"] = file_res[id]["prob_1"] + res_c["feats"] = da_c.iloc[:, 3:105].values.astype(np.float32) for aa in range(da_c.shape[0]): ann_c = copy.deepcopy(ann) if set_class_name is None: - ann_c['class'] = file_res[id]['species_1'] + ann_c["class"] = file_res[id]["species_1"] else: - ann_c['class'] = set_class_name - ann_c['start_time'] = np.round(da_c.iloc[aa]['TimeInFile'] / 1000.0 ,5) - ann_c['end_time'] = np.round(ann_c['start_time'] + da_c.iloc[aa]['CallDuration']/1000.0, 5) - ann_c['low_freq'] = np.round(da_c.iloc[aa]['LowFreq'] * 1000.0, 2) - ann_c['high_freq'] = np.round(da_c.iloc[aa]['HiFreq'] * 1000.0, 2) - ann_c['det_prob'] = np.round(da_c.iloc[aa]['Quality'], 3) - res_c['annotation'].append(ann_c) + ann_c["class"] = set_class_name + ann_c["start_time"] = np.round( + da_c.iloc[aa]["TimeInFile"] / 1000.0, 5 + ) + ann_c["end_time"] = np.round( + ann_c["start_time"] + da_c.iloc[aa]["CallDuration"] / 1000.0, 5 + ) + ann_c["low_freq"] = np.round(da_c.iloc[aa]["LowFreq"] * 1000.0, 2) + ann_c["high_freq"] = np.round(da_c.iloc[aa]["HiFreq"] * 1000.0, 2) + ann_c["det_prob"] = np.round(da_c.iloc[aa]["Quality"], 3) + res_c["annotation"].append(ann_c) return res_c @@ -193,8 +224,18 @@ def load_sonobat_preds(dataset, id, sb_meta, set_class_name=None): def bb_overlap(bb_g_in, bb_p_in): freq_scale = 10000000.0 # ensure that both axis are roughly the same range - bb_g = [bb_g_in['start_time'], bb_g_in['low_freq']/freq_scale, bb_g_in['end_time'], bb_g_in['high_freq']/freq_scale] - bb_p = [bb_p_in['start_time'], bb_p_in['low_freq']/freq_scale, bb_p_in['end_time'], bb_p_in['high_freq']/freq_scale] + bb_g = [ + bb_g_in["start_time"], + bb_g_in["low_freq"] / freq_scale, + bb_g_in["end_time"], + bb_g_in["high_freq"] / freq_scale, + ] + bb_p = [ + bb_p_in["start_time"], + bb_p_in["low_freq"] / freq_scale, + bb_p_in["end_time"], + bb_p_in["high_freq"] / freq_scale, + ] xA = max(bb_g[0], bb_p[0]) yA = max(bb_g[1], bb_p[1]) @@ -220,13 +261,15 @@ def bb_overlap(bb_g_in, bb_p_in): def assign_to_gt(gt, pred, iou_thresh): # this will edit pred in place - num_preds = len(pred['annotation']) - num_gts = len(gt['annotation']) + num_preds = len(pred["annotation"]) + num_gts = len(gt["annotation"]) if num_preds > 0 and num_gts > 0: iou_m = np.zeros((num_preds, num_gts)) for ii in range(num_preds): for jj in range(num_gts): - iou_m[ii, jj] = bb_overlap(gt['annotation'][jj], pred['annotation'][ii]) + iou_m[ii, jj] = bb_overlap( + gt["annotation"][jj], pred["annotation"][ii] + ) # greedily assign detections to ground truths # needs to be greater than some threshold and we cannot assign GT @@ -235,7 +278,9 @@ def assign_to_gt(gt, pred, iou_thresh): for jj in range(num_gts): max_iou = np.argmax(iou_m[:, jj]) if iou_m[max_iou, jj] > iou_thresh: - pred['annotation'][max_iou]['class'] = gt['annotation'][jj]['class'] + pred["annotation"][max_iou]["class"] = gt["annotation"][jj][ + "class" + ] iou_m[max_iou, :] = -1.0 return pred @@ -244,27 +289,39 @@ def assign_to_gt(gt, pred, iou_thresh): def parse_data(data, class_names, non_event_classes, is_pred=False): class_names_all = class_names + non_event_classes - data['class_names'] = np.array([aa['class'] for aa in data['annotation']]) - data['start_times'] = np.array([aa['start_time'] for aa in data['annotation']]) - data['end_times'] = np.array([aa['end_time'] for aa in data['annotation']]) - data['high_freqs'] = np.array([float(aa['high_freq']) for aa in data['annotation']]) - data['low_freqs'] = np.array([float(aa['low_freq']) for aa in data['annotation']]) + data["class_names"] = np.array([aa["class"] for aa in data["annotation"]]) + data["start_times"] = np.array( + [aa["start_time"] for aa in data["annotation"]] + ) + data["end_times"] = np.array([aa["end_time"] for aa in data["annotation"]]) + data["high_freqs"] = np.array( + [float(aa["high_freq"]) for aa in data["annotation"]] + ) + data["low_freqs"] = np.array( + [float(aa["low_freq"]) for aa in data["annotation"]] + ) if is_pred: # when loading predictions - data['det_probs'] = np.array([float(aa['det_prob']) for aa in data['annotation']]) - data['class_probs'] = np.zeros((len(class_names)+1, len(data['annotation']))) - data['class_ids'] = np.array([class_names_all.index(aa['class']) for aa in data['annotation']]).astype(np.int32) + data["det_probs"] = np.array( + [float(aa["det_prob"]) for aa in data["annotation"]] + ) + data["class_probs"] = np.zeros( + (len(class_names) + 1, len(data["annotation"])) + ) + data["class_ids"] = np.array( + [class_names_all.index(aa["class"]) for aa in data["annotation"]] + ).astype(np.int32) else: # when loading ground truth # if the class label is not in the set of interest then set to -1 labels = [] - for aa in data['annotation']: - if aa['class'] in class_names: - labels.append(class_names_all.index(aa['class'])) + for aa in data["annotation"]: + if aa["class"] in class_names: + labels.append(class_names_all.index(aa["class"])) else: labels.append(-1) - data['class_ids'] = np.array(labels).astype(np.int32) + data["class_ids"] = np.array(labels).astype(np.int32) return data @@ -272,12 +329,17 @@ def parse_data(data, class_names, non_event_classes, is_pred=False): def load_gt_data(datasets, events_of_interest, class_names, classes_to_ignore): gt_data = [] for dd in datasets: - print('\n' + dd['dataset_name']) - gt_dataset = tu.load_set_of_anns([dd], events_of_interest=events_of_interest, verbose=True) - gt_dataset = [parse_data(gg, class_names, classes_to_ignore, False) for gg in gt_dataset] + print("\n" + dd["dataset_name"]) + gt_dataset = tu.load_set_of_anns( + [dd], events_of_interest=events_of_interest, verbose=True + ) + gt_dataset = [ + parse_data(gg, class_names, classes_to_ignore, False) + for gg in gt_dataset + ] for gt in gt_dataset: - gt['dataset_name'] = dd['dataset_name'] + gt["dataset_name"] = dd["dataset_name"] gt_data.extend(gt_dataset) @@ -300,69 +362,103 @@ def train_rf_model(x_train, y_train, num_classes, seed=2001): clf = RandomForestClassifier(random_state=seed, n_jobs=-1) clf.fit(x_train, y_train) y_pred = clf.predict(x_train) - tr_acc = (y_pred==y_train).mean() - #print('Train acc', round(tr_acc*100, 2)) + tr_acc = (y_pred == y_train).mean() + # print('Train acc', round(tr_acc*100, 2)) return clf, un_train_class def eval_rf_model(clf, pred, un_train_class, num_classes): # stores the prediction in place - if pred['feats'].shape[0] > 0: - pred['class_probs'] = np.zeros((num_classes, pred['feats'].shape[0])) - pred['class_probs'][un_train_class, :] = clf.predict_proba(pred['feats']).T - pred['det_probs'] = pred['class_probs'][:-1, :].sum(0) + if pred["feats"].shape[0] > 0: + pred["class_probs"] = np.zeros((num_classes, pred["feats"].shape[0])) + pred["class_probs"][un_train_class, :] = clf.predict_proba( + pred["feats"] + ).T + pred["det_probs"] = pred["class_probs"][:-1, :].sum(0) else: - pred['class_probs'] = np.zeros((num_classes, 0)) - pred['det_probs'] = np.zeros(0) + pred["class_probs"] = np.zeros((num_classes, 0)) + pred["det_probs"] = np.zeros(0) return pred def save_summary_to_json(op_dir, mod_name, results): op = {} - op['avg_prec'] = round(results['avg_prec'], 3) - op['avg_prec_class'] = round(results['avg_prec_class'], 3) - op['top_class'] = round(results['top_class']['avg_prec'], 3) - op['file_acc'] = round(results['file_acc'], 3) - op['model'] = mod_name + op["avg_prec"] = round(results["avg_prec"], 3) + op["avg_prec_class"] = round(results["avg_prec_class"], 3) + op["top_class"] = round(results["top_class"]["avg_prec"], 3) + op["file_acc"] = round(results["file_acc"], 3) + op["model"] = mod_name - op['per_class'] = {} - for cc in results['class_pr']: - op['per_class'][cc['name']] = cc['avg_prec'] + op["per_class"] = {} + for cc in results["class_pr"]: + op["per_class"][cc["name"]] = cc["avg_prec"] - op_file_name = os.path.join(op_dir, mod_name + '_results.json') - with open(op_file_name, 'w') as da: + op_file_name = os.path.join(op_dir, mod_name + "_results.json") + with open(op_file_name, "w") as da: json.dump(op, da, indent=2) -def print_results(model_name, mod_str, results, op_dir, class_names, file_type, title_text=''): - print('\nResults - ' + model_name) - print('avg_prec ', round(results['avg_prec'], 3)) - print('avg_prec_class', round(results['avg_prec_class'], 3)) - print('top_class ', round(results['top_class']['avg_prec'], 3)) - print('file_acc ', round(results['file_acc'], 3)) +def print_results( + model_name, mod_str, results, op_dir, class_names, file_type, title_text="" +): + print("\nResults - " + model_name) + print("avg_prec ", round(results["avg_prec"], 3)) + print("avg_prec_class", round(results["avg_prec_class"], 3)) + print("top_class ", round(results["top_class"]["avg_prec"], 3)) + print("file_acc ", round(results["file_acc"], 3)) - print('\nSaving ' + model_name + ' results to: ' + op_dir) + print("\nSaving " + model_name + " results to: " + op_dir) save_summary_to_json(op_dir, mod_str, results) - pu.plot_pr_curve(op_dir, mod_str+'_test_all_det', mod_str+'_test_all_det', results, file_type, title_text + 'Detection PR') - pu.plot_pr_curve(op_dir, mod_str+'_test_all_top_class', mod_str+'_test_all_top_class', results['top_class'], file_type, title_text + 'Top Class') - pu.plot_pr_curve_class(op_dir, mod_str+'_test_all_class', mod_str+'_test_all_class', results, file_type, title_text + 'Per-Class PR') - pu.plot_confusion_matrix(op_dir, mod_str+'_confusion', results['gt_valid_file'], results['pred_valid_file'], - results['file_acc'], class_names, True, file_type, title_text + 'Confusion Matrix') + pu.plot_pr_curve( + op_dir, + mod_str + "_test_all_det", + mod_str + "_test_all_det", + results, + file_type, + title_text + "Detection PR", + ) + pu.plot_pr_curve( + op_dir, + mod_str + "_test_all_top_class", + mod_str + "_test_all_top_class", + results["top_class"], + file_type, + title_text + "Top Class", + ) + pu.plot_pr_curve_class( + op_dir, + mod_str + "_test_all_class", + mod_str + "_test_all_class", + results, + file_type, + title_text + "Per-Class PR", + ) + pu.plot_confusion_matrix( + op_dir, + mod_str + "_confusion", + results["gt_valid_file"], + results["pred_valid_file"], + results["file_acc"], + class_names, + True, + file_type, + title_text + "Confusion Matrix", + ) def add_root_path_back(data_sets, ann_path, wav_path): for dd in data_sets: - dd['ann_path'] = os.path.join(ann_path, dd['ann_path']) - dd['wav_path'] = os.path.join(wav_path, dd['wav_path']) + dd["ann_path"] = os.path.join(ann_path, dd["ann_path"]) + dd["wav_path"] = os.path.join(wav_path, dd["wav_path"]) return data_sets def check_classes_in_train(gt_list, class_names): - num_gt_total = np.sum([gg['start_times'].shape[0] for gg in gt_list]) + num_gt_total = np.sum([gg["start_times"].shape[0] for gg in gt_list]) num_with_no_class = 0 for gt in gt_list: - for cc in gt['class_names']: + for cc in gt["class_names"]: if cc not in class_names: num_with_no_class += 1 return num_with_no_class @@ -371,195 +467,335 @@ def check_classes_in_train(gt_list, class_names): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('op_dir', type=str, default='plots/results_compare/', - help='Output directory for plots') - parser.add_argument('data_dir', type=str, - help='Path to root of datasets') - parser.add_argument('ann_dir', type=str, - help='Path to extracted annotations') - parser.add_argument('bd_model_path', type=str, - help='Path to BatDetect model') - parser.add_argument('--test_file', type=str, default='', - help='Path to json file used for evaluation.') - parser.add_argument('--sb_ip_dir', type=str, default='', - help='Path to sonobat predictions') - parser.add_argument('--sb_region_classifier', type=str, default='south', - help='Path to sonobat predictions') - parser.add_argument('--td_ip_dir', type=str, default='', - help='Path to tadarida_D predictions') - parser.add_argument('--iou_thresh', type=float, default=0.01, - help='IOU threshold for assigning predictions to ground truth') - parser.add_argument('--file_type', type=str, default='png', - help='Type of image to save - png or pdf') - parser.add_argument('--title_text', type=str, default='', - help='Text to add as title of plots') - parser.add_argument('--rand_seed', type=int, default=2001, - help='Random seed') + parser.add_argument( + "op_dir", + type=str, + default="plots/results_compare/", + help="Output directory for plots", + ) + parser.add_argument("data_dir", type=str, help="Path to root of datasets") + parser.add_argument( + "ann_dir", type=str, help="Path to extracted annotations" + ) + parser.add_argument( + "bd_model_path", type=str, help="Path to BatDetect model" + ) + parser.add_argument( + "--test_file", + type=str, + default="", + help="Path to json file used for evaluation.", + ) + parser.add_argument( + "--sb_ip_dir", type=str, default="", help="Path to sonobat predictions" + ) + parser.add_argument( + "--sb_region_classifier", + type=str, + default="south", + help="Path to sonobat predictions", + ) + parser.add_argument( + "--td_ip_dir", + type=str, + default="", + help="Path to tadarida_D predictions", + ) + parser.add_argument( + "--iou_thresh", + type=float, + default=0.01, + help="IOU threshold for assigning predictions to ground truth", + ) + parser.add_argument( + "--file_type", + type=str, + default="png", + help="Type of image to save - png or pdf", + ) + parser.add_argument( + "--title_text", + type=str, + default="", + help="Text to add as title of plots", + ) + parser.add_argument( + "--rand_seed", type=int, default=2001, help="Random seed" + ) args = vars(parser.parse_args()) - np.random.seed(args['rand_seed']) - - if not os.path.isdir(args['op_dir']): - os.makedirs(args['op_dir']) + np.random.seed(args["rand_seed"]) + if not os.path.isdir(args["op_dir"]): + os.makedirs(args["op_dir"]) # load the model params_eval = parameters.get_params(False) - _, params_bd = du.load_model(args['bd_model_path']) + _, params_bd = du.load_model(args["bd_model_path"]) - class_names = params_bd['class_names'] + class_names = params_bd["class_names"] num_classes = len(class_names) + 1 # num classes plus background class - classes_to_ignore = ['Not Bat', 'Bat', 'Unknown'] - events_of_interest = ['Echolocation'] + classes_to_ignore = ["Not Bat", "Bat", "Unknown"] + events_of_interest = ["Echolocation"] # load test data - if args['test_file'] == '': + if args["test_file"] == "": # load the test files of interest from the trained model - test_sets = add_root_path_back(params_bd['test_sets'], args['ann_dir'], args['data_dir']) - test_sets = [dd for dd in test_sets if not dd['is_binary']] # exclude bat/not datasets + test_sets = add_root_path_back( + params_bd["test_sets"], args["ann_dir"], args["data_dir"] + ) + test_sets = [ + dd for dd in test_sets if not dd["is_binary"] + ] # exclude bat/not datasets else: # user specified annotation file to evaluate test_dict = {} - test_dict['dataset_name'] = args['test_file'].replace('.json', '') - test_dict['is_test'] = True - test_dict['is_binary'] = True - test_dict['ann_path'] = os.path.join(args['ann_dir'], args['test_file']) - test_dict['wav_path'] = args['data_dir'] + test_dict["dataset_name"] = args["test_file"].replace(".json", "") + test_dict["is_test"] = True + test_dict["is_binary"] = True + test_dict["ann_path"] = os.path.join( + args["ann_dir"], args["test_file"] + ) + test_dict["wav_path"] = args["data_dir"] test_sets = [test_dict] # load the gt for the test set - gt_test = load_gt_data(test_sets, events_of_interest, class_names, classes_to_ignore) - total_num_calls = np.sum([gg['start_times'].shape[0] for gg in gt_test]) - print('\nTotal number of test files:', len(gt_test)) - print('Total number of test calls:', np.sum([gg['start_times'].shape[0] for gg in gt_test])) + gt_test = load_gt_data( + test_sets, events_of_interest, class_names, classes_to_ignore + ) + total_num_calls = np.sum([gg["start_times"].shape[0] for gg in gt_test]) + print("\nTotal number of test files:", len(gt_test)) + print( + "Total number of test calls:", + np.sum([gg["start_times"].shape[0] for gg in gt_test]), + ) # check if test contains classes not in the train set num_with_no_class = check_classes_in_train(gt_test, class_names) if total_num_calls == num_with_no_class: - print('Classes from the test set are not in the train set.') + print("Classes from the test set are not in the train set.") assert False # only need the train data if evaluating Sonobat or Tadarida - if args['sb_ip_dir'] != '' or args['td_ip_dir'] != '': - train_sets = add_root_path_back(params_bd['train_sets'], args['ann_dir'], args['data_dir']) - train_sets = [dd for dd in train_sets if not dd['is_binary']] # exclude bat/not datasets - gt_train = load_gt_data(train_sets, events_of_interest, class_names, classes_to_ignore) - + if args["sb_ip_dir"] != "" or args["td_ip_dir"] != "": + train_sets = add_root_path_back( + params_bd["train_sets"], args["ann_dir"], args["data_dir"] + ) + train_sets = [ + dd for dd in train_sets if not dd["is_binary"] + ] # exclude bat/not datasets + gt_train = load_gt_data( + train_sets, events_of_interest, class_names, classes_to_ignore + ) # # evaluate Sonobat by training random forest classifier # # NOTE: Sonobat may only make predictions for a subset of the files # - if args['sb_ip_dir'] != '': - sb_meta = load_sonobat_meta(args['sb_ip_dir'], train_sets + test_sets, args['sb_region_classifier'], class_names) + if args["sb_ip_dir"] != "": + sb_meta = load_sonobat_meta( + args["sb_ip_dir"], + train_sets + test_sets, + args["sb_region_classifier"], + class_names, + ) preds_sb = [] keep_inds_sb = [] for ii, gt in enumerate(gt_test): - sb_pred = load_sonobat_preds(gt['dataset_name'], gt['id'], sb_meta) - if sb_pred['class_name'] != '': - sb_pred = parse_data(sb_pred, class_names, classes_to_ignore, True) - sb_pred['class_probs'][sb_pred['class_ids'], np.arange(sb_pred['class_probs'].shape[1])] = sb_pred['det_probs'] + sb_pred = load_sonobat_preds(gt["dataset_name"], gt["id"], sb_meta) + if sb_pred["class_name"] != "": + sb_pred = parse_data( + sb_pred, class_names, classes_to_ignore, True + ) + sb_pred["class_probs"][ + sb_pred["class_ids"], + np.arange(sb_pred["class_probs"].shape[1]), + ] = sb_pred["det_probs"] preds_sb.append(sb_pred) keep_inds_sb.append(ii) - results_sb = evl.evaluate_predictions([gt_test[ii] for ii in keep_inds_sb], preds_sb, class_names, - params_eval['detection_overlap'], params_eval['ignore_start_end']) - print_results('Sonobat', 'sb', results_sb, args['op_dir'], class_names, - args['file_type'], args['title_text'] + ' - Species - ') - print('Only reporting results for', len(keep_inds_sb), 'files, out of', len(gt_test)) - + results_sb = evl.evaluate_predictions( + [gt_test[ii] for ii in keep_inds_sb], + preds_sb, + class_names, + params_eval["detection_overlap"], + params_eval["ignore_start_end"], + ) + print_results( + "Sonobat", + "sb", + results_sb, + args["op_dir"], + class_names, + args["file_type"], + args["title_text"] + " - Species - ", + ) + print( + "Only reporting results for", + len(keep_inds_sb), + "files, out of", + len(gt_test), + ) # train our own random forest on sonobat features x_train = [] y_train = [] for gt in gt_train: - pred = load_sonobat_preds(gt['dataset_name'], gt['id'], sb_meta, 'Not Bat') + pred = load_sonobat_preds( + gt["dataset_name"], gt["id"], sb_meta, "Not Bat" + ) - if len(pred['annotation']) > 0: + if len(pred["annotation"]) > 0: # compute detection overlap with ground truth to determine which are the TP detections - assign_to_gt(gt, pred, args['iou_thresh']) + assign_to_gt(gt, pred, args["iou_thresh"]) pred = parse_data(pred, class_names, classes_to_ignore, True) - x_train.append(pred['feats']) - y_train.append(pred['class_ids']) + x_train.append(pred["feats"]) + y_train.append(pred["class_ids"]) # train random forest on tadarida predictions - clf_sb, un_train_class = train_rf_model(x_train, y_train, num_classes, args['rand_seed']) + clf_sb, un_train_class = train_rf_model( + x_train, y_train, num_classes, args["rand_seed"] + ) # run the model on the test set preds_sb_rf = [] for gt in gt_test: - pred = load_sonobat_preds(gt['dataset_name'], gt['id'], sb_meta, 'Not Bat') + pred = load_sonobat_preds( + gt["dataset_name"], gt["id"], sb_meta, "Not Bat" + ) pred = parse_data(pred, class_names, classes_to_ignore, True) pred = eval_rf_model(clf_sb, pred, un_train_class, num_classes) preds_sb_rf.append(pred) - results_sb_rf = evl.evaluate_predictions(gt_test, preds_sb_rf, class_names, - params_eval['detection_overlap'], params_eval['ignore_start_end']) - print_results('Sonobat RF', 'sb_rf', results_sb_rf, args['op_dir'], class_names, - args['file_type'], args['title_text'] + ' - Species - ') - print('\n\nWARNING\nThis is evaluating on the full test set, but there is only dections for a subset of files\n\n') - + results_sb_rf = evl.evaluate_predictions( + gt_test, + preds_sb_rf, + class_names, + params_eval["detection_overlap"], + params_eval["ignore_start_end"], + ) + print_results( + "Sonobat RF", + "sb_rf", + results_sb_rf, + args["op_dir"], + class_names, + args["file_type"], + args["title_text"] + " - Species - ", + ) + print( + "\n\nWARNING\nThis is evaluating on the full test set, but there is only dections for a subset of files\n\n" + ) # # evaluate Tadarida-D by training random forest classifier # - if args['td_ip_dir'] != '': + if args["td_ip_dir"] != "": x_train = [] y_train = [] for gt in gt_train: - pred = load_tadarida_pred(args['td_ip_dir'], gt['dataset_name'], gt['id']) + pred = load_tadarida_pred( + args["td_ip_dir"], gt["dataset_name"], gt["id"] + ) # compute detection overlap with ground truth to determine which are the TP detections - assign_to_gt(gt, pred, args['iou_thresh']) + assign_to_gt(gt, pred, args["iou_thresh"]) pred = parse_data(pred, class_names, classes_to_ignore, True) - x_train.append(pred['feats']) - y_train.append(pred['class_ids']) + x_train.append(pred["feats"]) + y_train.append(pred["class_ids"]) # train random forest on Tadarida-D predictions - clf_td, un_train_class = train_rf_model(x_train, y_train, num_classes, args['rand_seed']) + clf_td, un_train_class = train_rf_model( + x_train, y_train, num_classes, args["rand_seed"] + ) # run the model on the test set preds_td = [] for gt in gt_test: - pred = load_tadarida_pred(args['td_ip_dir'], gt['dataset_name'], gt['id']) + pred = load_tadarida_pred( + args["td_ip_dir"], gt["dataset_name"], gt["id"] + ) pred = parse_data(pred, class_names, classes_to_ignore, True) pred = eval_rf_model(clf_td, pred, un_train_class, num_classes) preds_td.append(pred) - results_td = evl.evaluate_predictions(gt_test, preds_td, class_names, - params_eval['detection_overlap'], params_eval['ignore_start_end']) - print_results('Tadarida', 'td_rf', results_td, args['op_dir'], class_names, - args['file_type'], args['title_text'] + ' - Species - ') - + results_td = evl.evaluate_predictions( + gt_test, + preds_td, + class_names, + params_eval["detection_overlap"], + params_eval["ignore_start_end"], + ) + print_results( + "Tadarida", + "td_rf", + results_td, + args["op_dir"], + class_names, + args["file_type"], + args["title_text"] + " - Species - ", + ) # # evaluate BatDetect # - if args['bd_model_path'] != '': + if args["bd_model_path"] != "": # load model bd_args = du.get_default_bd_args() - model, params_bd = du.load_model(args['bd_model_path']) + model, params_bd = du.load_model(args["bd_model_path"]) # check if the class names are the same - if params_bd['class_names'] != class_names: - print('Warning: Class names are not the same as the trained model') + if params_bd["class_names"] != class_names: + print("Warning: Class names are not the same as the trained model") assert False preds_bd = [] for ii, gg in enumerate(gt_test): - pred = du.process_file(gg['file_path'], model, params_bd, bd_args, return_raw_preds=True) + pred = du.process_file( + gg["file_path"], + model, + params_bd, + bd_args, + return_raw_preds=True, + ) preds_bd.append(pred) - results_bd = evl.evaluate_predictions(gt_test, preds_bd, class_names, - params_eval['detection_overlap'], params_eval['ignore_start_end']) - print_results('BatDetect', 'bd', results_bd, args['op_dir'], - class_names, args['file_type'], args['title_text'] + ' - Species - ') + results_bd = evl.evaluate_predictions( + gt_test, + preds_bd, + class_names, + params_eval["detection_overlap"], + params_eval["ignore_start_end"], + ) + print_results( + "BatDetect", + "bd", + results_bd, + args["op_dir"], + class_names, + args["file_type"], + args["title_text"] + " - Species - ", + ) # evaluate genus level - class_names_genus, preds_bd_g, gt_test_g = create_genus_mapping(gt_test, preds_bd, class_names) - results_bd_genus = evl.evaluate_predictions(gt_test_g, preds_bd_g, class_names_genus, - params_eval['detection_overlap'], params_eval['ignore_start_end']) - print_results('BatDetect Genus', 'bd_genus', results_bd_genus, args['op_dir'], - class_names_genus, args['file_type'], args['title_text'] + ' - Genus - ') + class_names_genus, preds_bd_g, gt_test_g = create_genus_mapping( + gt_test, preds_bd, class_names + ) + results_bd_genus = evl.evaluate_predictions( + gt_test_g, + preds_bd_g, + class_names_genus, + params_eval["detection_overlap"], + params_eval["ignore_start_end"], + ) + print_results( + "BatDetect Genus", + "bd_genus", + results_bd_genus, + args["op_dir"], + class_names_genus, + args["file_type"], + args["title_text"] + " - Genus - ", + ) diff --git a/bat_detect/finetune/finetune_model.py b/bat_detect/finetune/finetune_model.py index 4fecc48..8c20e22 100644 --- a/bat_detect/finetune/finetune_model.py +++ b/bat_detect/finetune/finetune_model.py @@ -1,183 +1,325 @@ -import numpy as np -import matplotlib.pyplot as plt +import argparse +import glob +import json import os +import sys + +import matplotlib.pyplot as plt +import numpy as np import torch import torch.nn.functional as F from torch.optim.lr_scheduler import CosineAnnealingLR -import json -import argparse -import glob -import sys -sys.path.append(os.path.join('..', '..')) -import bat_detect.train.train_model as tm +sys.path.append(os.path.join("..", "..")) +import bat_detect.detector.models as models +import bat_detect.detector.parameters as parameters +import bat_detect.detector.post_process as pp import bat_detect.train.audio_dataloader as adl import bat_detect.train.evaluate as evl -import bat_detect.train.train_utils as tu import bat_detect.train.losses as losses - -import bat_detect.detector.parameters as parameters -import bat_detect.detector.models as models -import bat_detect.detector.post_process as pp -import bat_detect.utils.plot_utils as pu +import bat_detect.train.train_model as tm +import bat_detect.train.train_utils as tu import bat_detect.utils.detector_utils as du - +import bat_detect.utils.plot_utils as pu if __name__ == "__main__": - info_str = '\nBatDetect - Finetune Model\n' + info_str = "\nBatDetect - Finetune Model\n" print(info_str) parser = argparse.ArgumentParser() - parser.add_argument('audio_path', type=str, help='Input directory for audio') - parser.add_argument('train_ann_path', type=str, - help='Path to where train annotation file is stored') - parser.add_argument('test_ann_path', type=str, - help='Path to where test annotation file is stored') - parser.add_argument('model_path', type=str, - help='Path to pretrained model') - parser.add_argument('--op_model_name', type=str, default='', - help='Path and name for finetuned model') - parser.add_argument('--num_epochs', type=int, default=200, dest='num_epochs', - help='Number of finetuning epochs') - parser.add_argument('--finetune_only_last_layer', action='store_true', - help='Only train final layers') - parser.add_argument('--train_from_scratch', action='store_true', - help='Do not use pretrained weights') - parser.add_argument('--do_not_save_images', action='store_false', - help='Do not save images at the end of training') - parser.add_argument('--notes', type=str, default='', - help='Notes to save in text file') + parser.add_argument( + "audio_path", type=str, help="Input directory for audio" + ) + parser.add_argument( + "train_ann_path", + type=str, + help="Path to where train annotation file is stored", + ) + parser.add_argument( + "test_ann_path", + type=str, + help="Path to where test annotation file is stored", + ) + parser.add_argument( + "model_path", type=str, help="Path to pretrained model" + ) + parser.add_argument( + "--op_model_name", + type=str, + default="", + help="Path and name for finetuned model", + ) + parser.add_argument( + "--num_epochs", + type=int, + default=200, + dest="num_epochs", + help="Number of finetuning epochs", + ) + parser.add_argument( + "--finetune_only_last_layer", + action="store_true", + help="Only train final layers", + ) + parser.add_argument( + "--train_from_scratch", + action="store_true", + help="Do not use pretrained weights", + ) + parser.add_argument( + "--do_not_save_images", + action="store_false", + help="Do not save images at the end of training", + ) + parser.add_argument( + "--notes", type=str, default="", help="Notes to save in text file" + ) args = vars(parser.parse_args()) - params = parameters.get_params(True, '../../experiments/') + params = parameters.get_params(True, "../../experiments/") if torch.cuda.is_available(): - params['device'] = 'cuda' + params["device"] = "cuda" else: - params['device'] = 'cpu' - print('\nNote, this will be a lot faster if you use computer with a GPU.\n') + params["device"] = "cpu" + print( + "\nNote, this will be a lot faster if you use computer with a GPU.\n" + ) - print('\nAudio directory: ' + args['audio_path']) - print('Train file: ' + args['train_ann_path']) - print('Test file: ' + args['test_ann_path']) - print('Loading model: ' + args['model_path']) + print("\nAudio directory: " + args["audio_path"]) + print("Train file: " + args["train_ann_path"]) + print("Test file: " + args["test_ann_path"]) + print("Loading model: " + args["model_path"]) - dataset_name = os.path.basename(args['train_ann_path']).replace('.json', '').replace('_TRAIN', '') + dataset_name = ( + os.path.basename(args["train_ann_path"]) + .replace(".json", "") + .replace("_TRAIN", "") + ) - if args['train_from_scratch']: - print('\nTraining model from scratch i.e. not using pretrained weights') - model, params_train = du.load_model(args['model_path'], False) + if args["train_from_scratch"]: + print( + "\nTraining model from scratch i.e. not using pretrained weights" + ) + model, params_train = du.load_model(args["model_path"], False) else: - model, params_train = du.load_model(args['model_path'], True) - model.to(params['device']) + model, params_train = du.load_model(args["model_path"], True) + model.to(params["device"]) - params['num_epochs'] = args['num_epochs'] - if args['op_model_name'] != '': - params['model_file_name'] = args['op_model_name'] - classes_to_ignore = params['classes_to_ignore']+params['generic_class'] + params["num_epochs"] = args["num_epochs"] + if args["op_model_name"] != "": + params["model_file_name"] = args["op_model_name"] + classes_to_ignore = params["classes_to_ignore"] + params["generic_class"] # save notes file - params['notes'] = args['notes'] - if args['notes'] != '': - tu.write_notes_file(params['experiment'] + 'notes.txt', args['notes']) - + params["notes"] = args["notes"] + if args["notes"] != "": + tu.write_notes_file(params["experiment"] + "notes.txt", args["notes"]) # load train annotations train_sets = [] - train_sets.append(tu.get_blank_dataset_dict(dataset_name, False, args['train_ann_path'], args['audio_path'])) - params['train_sets'] = [tu.get_blank_dataset_dict(dataset_name, False, os.path.basename(args['train_ann_path']), args['audio_path'])] + train_sets.append( + tu.get_blank_dataset_dict( + dataset_name, False, args["train_ann_path"], args["audio_path"] + ) + ) + params["train_sets"] = [ + tu.get_blank_dataset_dict( + dataset_name, + False, + os.path.basename(args["train_ann_path"]), + args["audio_path"], + ) + ] - print('\nTrain set:') - data_train, params['class_names'], params['class_inv_freq'] = \ - tu.load_set_of_anns(train_sets, classes_to_ignore, params['events_of_interest']) - print('Number of files', len(data_train)) + print("\nTrain set:") + ( + data_train, + params["class_names"], + params["class_inv_freq"], + ) = tu.load_set_of_anns( + train_sets, classes_to_ignore, params["events_of_interest"] + ) + print("Number of files", len(data_train)) - params['genus_names'], params['genus_mapping'] = tu.get_genus_mapping(params['class_names']) - params['class_names_short'] = tu.get_short_class_names(params['class_names']) + params["genus_names"], params["genus_mapping"] = tu.get_genus_mapping( + params["class_names"] + ) + params["class_names_short"] = tu.get_short_class_names( + params["class_names"] + ) # load test annotations test_sets = [] - test_sets.append(tu.get_blank_dataset_dict(dataset_name, True, args['test_ann_path'], args['audio_path'])) - params['test_sets'] = [tu.get_blank_dataset_dict(dataset_name, True, os.path.basename(args['test_ann_path']), args['audio_path'])] + test_sets.append( + tu.get_blank_dataset_dict( + dataset_name, True, args["test_ann_path"], args["audio_path"] + ) + ) + params["test_sets"] = [ + tu.get_blank_dataset_dict( + dataset_name, + True, + os.path.basename(args["test_ann_path"]), + args["audio_path"], + ) + ] - print('\nTest set:') - data_test, _, _ = tu.load_set_of_anns(test_sets, classes_to_ignore, params['events_of_interest']) - print('Number of files', len(data_test)) + print("\nTest set:") + data_test, _, _ = tu.load_set_of_anns( + test_sets, classes_to_ignore, params["events_of_interest"] + ) + print("Number of files", len(data_test)) # train loader train_dataset = adl.AudioLoader(data_train, params, is_train=True) - train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=params['batch_size'], - shuffle=True, num_workers=params['num_workers'], pin_memory=True) + train_loader = torch.utils.data.DataLoader( + train_dataset, + batch_size=params["batch_size"], + shuffle=True, + num_workers=params["num_workers"], + pin_memory=True, + ) # test loader - batch size of one because of variable file length test_dataset = adl.AudioLoader(data_test, params, is_train=False) - test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, - shuffle=False, num_workers=params['num_workers'], pin_memory=True) + test_loader = torch.utils.data.DataLoader( + test_dataset, + batch_size=1, + shuffle=False, + num_workers=params["num_workers"], + pin_memory=True, + ) inputs_train = next(iter(train_loader)) - params['ip_height'] = inputs_train['spec'].shape[2] - print('\ntrain batch size :', inputs_train['spec'].shape) + params["ip_height"] = inputs_train["spec"].shape[2] + print("\ntrain batch size :", inputs_train["spec"].shape) - assert(params_train['model_name'] == 'Net2DFast') - print('\n\nSOME hyperparams need to be the same as the loaded model (e.g. FFT) - currently they are getting overwritten.\n\n') + assert params_train["model_name"] == "Net2DFast" + print( + "\n\nSOME hyperparams need to be the same as the loaded model (e.g. FFT) - currently they are getting overwritten.\n\n" + ) # set the number of output classes num_filts = model.conv_classes_op.in_channels k_size = model.conv_classes_op.kernel_size pad = model.conv_classes_op.padding - model.conv_classes_op = torch.nn.Conv2d(num_filts, len(params['class_names'])+1, kernel_size=k_size, padding=pad) - model.conv_classes_op.to(params['device']) + model.conv_classes_op = torch.nn.Conv2d( + num_filts, + len(params["class_names"]) + 1, + kernel_size=k_size, + padding=pad, + ) + model.conv_classes_op.to(params["device"]) - if args['finetune_only_last_layer']: - print('\nOnly finetuning the final layers.\n') - train_layers_i = ['conv_classes', 'conv_classes_op', 'conv_size', 'conv_size_op'] - train_layers = [tt + '.weight' for tt in train_layers_i] + [tt + '.bias' for tt in train_layers_i] + if args["finetune_only_last_layer"]: + print("\nOnly finetuning the final layers.\n") + train_layers_i = [ + "conv_classes", + "conv_classes_op", + "conv_size", + "conv_size_op", + ] + train_layers = [tt + ".weight" for tt in train_layers_i] + [ + tt + ".bias" for tt in train_layers_i + ] for name, param in model.named_parameters(): if name in train_layers: param.requires_grad = True else: param.requires_grad = False - optimizer = torch.optim.Adam(model.parameters(), lr=params['lr']) - scheduler = CosineAnnealingLR(optimizer, params['num_epochs'] * len(train_loader)) - if params['train_loss'] == 'mse': + optimizer = torch.optim.Adam(model.parameters(), lr=params["lr"]) + scheduler = CosineAnnealingLR( + optimizer, params["num_epochs"] * len(train_loader) + ) + if params["train_loss"] == "mse": det_criterion = losses.mse_loss - elif params['train_loss'] == 'focal': + elif params["train_loss"] == "focal": det_criterion = losses.focal_loss # plotting - train_plt_ls = pu.LossPlotter(params['experiment'] + 'train_loss.png', params['num_epochs']+1, - ['train_loss'], None, None, ['epoch', 'train_loss'], logy=True) - test_plt_ls = pu.LossPlotter(params['experiment'] + 'test_loss.png', params['num_epochs']+1, - ['test_loss'], None, None, ['epoch', 'test_loss'], logy=True) - test_plt = pu.LossPlotter(params['experiment'] + 'test.png', params['num_epochs']+1, - ['avg_prec', 'rec_at_x', 'avg_prec_class', 'file_acc', 'top_class'], [0,1], None, ['epoch', '']) - test_plt_class = pu.LossPlotter(params['experiment'] + 'test_avg_prec.png', params['num_epochs']+1, - params['class_names_short'], [0,1], params['class_names_short'], ['epoch', 'avg_prec']) + train_plt_ls = pu.LossPlotter( + params["experiment"] + "train_loss.png", + params["num_epochs"] + 1, + ["train_loss"], + None, + None, + ["epoch", "train_loss"], + logy=True, + ) + test_plt_ls = pu.LossPlotter( + params["experiment"] + "test_loss.png", + params["num_epochs"] + 1, + ["test_loss"], + None, + None, + ["epoch", "test_loss"], + logy=True, + ) + test_plt = pu.LossPlotter( + params["experiment"] + "test.png", + params["num_epochs"] + 1, + ["avg_prec", "rec_at_x", "avg_prec_class", "file_acc", "top_class"], + [0, 1], + None, + ["epoch", ""], + ) + test_plt_class = pu.LossPlotter( + params["experiment"] + "test_avg_prec.png", + params["num_epochs"] + 1, + params["class_names_short"], + [0, 1], + params["class_names_short"], + ["epoch", "avg_prec"], + ) # main train loop - for epoch in range(0, params['num_epochs']+1): + for epoch in range(0, params["num_epochs"] + 1): - train_loss = tm.train(model, epoch, train_loader, det_criterion, optimizer, scheduler, params) - train_plt_ls.update_and_save(epoch, [train_loss['train_loss']]) + train_loss = tm.train( + model, + epoch, + train_loader, + det_criterion, + optimizer, + scheduler, + params, + ) + train_plt_ls.update_and_save(epoch, [train_loss["train_loss"]]) - if epoch % params['num_eval_epochs'] == 0: + if epoch % params["num_eval_epochs"] == 0: # detection accuracy on test set - test_res, test_loss = tm.test(model, epoch, test_loader, det_criterion, params) - test_plt_ls.update_and_save(epoch, [test_loss['test_loss']]) - test_plt.update_and_save(epoch, [test_res['avg_prec'], test_res['rec_at_x'], - test_res['avg_prec_class'], test_res['file_acc'], test_res['top_class']['avg_prec']]) - test_plt_class.update_and_save(epoch, [rs['avg_prec'] for rs in test_res['class_pr']]) - pu.plot_pr_curve_class(params['experiment'] , 'test_pr', 'test_pr', test_res) + test_res, test_loss = tm.test( + model, epoch, test_loader, det_criterion, params + ) + test_plt_ls.update_and_save(epoch, [test_loss["test_loss"]]) + test_plt.update_and_save( + epoch, + [ + test_res["avg_prec"], + test_res["rec_at_x"], + test_res["avg_prec_class"], + test_res["file_acc"], + test_res["top_class"]["avg_prec"], + ], + ) + test_plt_class.update_and_save( + epoch, [rs["avg_prec"] for rs in test_res["class_pr"]] + ) + pu.plot_pr_curve_class( + params["experiment"], "test_pr", "test_pr", test_res + ) # save finetuned model - print('saving model to: ' + params['model_file_name']) - op_state = {'epoch': epoch + 1, - 'state_dict': model.state_dict(), - 'params' : params} - torch.save(op_state, params['model_file_name']) - + print("saving model to: " + params["model_file_name"]) + op_state = { + "epoch": epoch + 1, + "state_dict": model.state_dict(), + "params": params, + } + torch.save(op_state, params["model_file_name"]) # save an image with associated prediction for each batch in the test set - if not args['do_not_save_images']: + if not args["do_not_save_images"]: tm.save_images_batch(model, test_loader, params) diff --git a/bat_detect/finetune/prep_data_finetune.py b/bat_detect/finetune/prep_data_finetune.py index 3e86cd4..bf86e97 100644 --- a/bat_detect/finetune/prep_data_finetune.py +++ b/bat_detect/finetune/prep_data_finetune.py @@ -1,32 +1,33 @@ -import numpy as np import argparse -import os import json - +import os import sys -sys.path.append(os.path.join('..', '..')) + +import numpy as np + +sys.path.append(os.path.join("..", "..")) import bat_detect.train.train_utils as tu def print_dataset_stats(data, split_name, classes_to_ignore): - print('\nSplit:', split_name) - print('Num files:', len(data)) + print("\nSplit:", split_name) + print("Num files:", len(data)) class_cnts = {} for dd in data: - for aa in dd['annotation']: - if aa['class'] not in classes_to_ignore: - if aa['class'] in class_cnts: - class_cnts[aa['class']] += 1 + for aa in dd["annotation"]: + if aa["class"] not in classes_to_ignore: + if aa["class"] in class_cnts: + class_cnts[aa["class"]] += 1 else: - class_cnts[aa['class']] = 1 + class_cnts[aa["class"]] = 1 if len(class_cnts) == 0: class_names = [] else: class_names = np.sort([*class_cnts]).tolist() - print('Class count:') + print("Class count:") str_len = np.max([len(cc) for cc in class_names]) + 5 for ii, cc in enumerate(class_names): @@ -41,111 +42,169 @@ def load_file_names(file_name): with open(file_name) as da: files = [line.rstrip() for line in da.readlines()] for ff in files: - if ff.lower()[-3:] != 'wav': - print('Error: Filenames need to end in .wav - ', ff) - assert(False) + if ff.lower()[-3:] != "wav": + print("Error: Filenames need to end in .wav - ", ff) + assert False else: - print('Error: Input file not found - ', file_name) - assert(False) + print("Error: Input file not found - ", file_name) + assert False return files if __name__ == "__main__": - info_str = '\nBatDetect - Prepare Data for Finetuning\n' + info_str = "\nBatDetect - Prepare Data for Finetuning\n" print(info_str) parser = argparse.ArgumentParser() - parser.add_argument('dataset_name', type=str, help='Name to call your dataset') - parser.add_argument('audio_dir', type=str, help='Input directory for audio') - parser.add_argument('ann_dir', type=str, help='Input directory for where the audio annotations are stored') - parser.add_argument('op_dir', type=str, help='Path where the train and test splits will be stored') - parser.add_argument('--percent_val', type=float, default=0.20, - help='Hold out this much data for validation. Should be number between 0 and 1') - parser.add_argument('--rand_seed', type=int, default=2001, - help='Random seed used for creating the validation split') - parser.add_argument('--train_file', type=str, default='', - help='Text file where each line is a wav file in train split') - parser.add_argument('--test_file', type=str, default='', - help='Text file where each line is a wav file in test split') - parser.add_argument('--input_class_names', type=str, default='', - help='Specify names of classes that you want to change. Separate with ";"') - parser.add_argument('--output_class_names', type=str, default='', - help='New class names to use instead. One to one mapping with "--input_class_names". \ - Separate with ";"') + parser.add_argument( + "dataset_name", type=str, help="Name to call your dataset" + ) + parser.add_argument( + "audio_dir", type=str, help="Input directory for audio" + ) + parser.add_argument( + "ann_dir", + type=str, + help="Input directory for where the audio annotations are stored", + ) + parser.add_argument( + "op_dir", + type=str, + help="Path where the train and test splits will be stored", + ) + parser.add_argument( + "--percent_val", + type=float, + default=0.20, + help="Hold out this much data for validation. Should be number between 0 and 1", + ) + parser.add_argument( + "--rand_seed", + type=int, + default=2001, + help="Random seed used for creating the validation split", + ) + parser.add_argument( + "--train_file", + type=str, + default="", + help="Text file where each line is a wav file in train split", + ) + parser.add_argument( + "--test_file", + type=str, + default="", + help="Text file where each line is a wav file in test split", + ) + parser.add_argument( + "--input_class_names", + type=str, + default="", + help='Specify names of classes that you want to change. Separate with ";"', + ) + parser.add_argument( + "--output_class_names", + type=str, + default="", + help='New class names to use instead. One to one mapping with "--input_class_names". \ + Separate with ";"', + ) args = vars(parser.parse_args()) + np.random.seed(args["rand_seed"]) - np.random.seed(args['rand_seed']) + classes_to_ignore = ["", " ", "Unknown", "Not Bat"] + generic_class = ["Bat"] + events_of_interest = ["Echolocation"] - classes_to_ignore = ['', ' ', 'Unknown', 'Not Bat'] - generic_class = ['Bat'] - events_of_interest = ['Echolocation'] - - if args['input_class_names'] != '' and args['output_class_names'] != '': + if args["input_class_names"] != "" and args["output_class_names"] != "": # change the names of the classes - ip_names = args['input_class_names'].split(';') - op_names = args['output_class_names'].split(';') + ip_names = args["input_class_names"].split(";") + op_names = args["output_class_names"].split(";") name_dict = dict(zip(ip_names, op_names)) else: name_dict = False # load annotations - data_all, _, _ = tu.load_set_of_anns({'ann_path': args['ann_dir'], 'wav_path': args['audio_dir']}, - classes_to_ignore, events_of_interest, False, False, - list_of_anns=True, filter_issues=True, name_replace=name_dict) + data_all, _, _ = tu.load_set_of_anns( + {"ann_path": args["ann_dir"], "wav_path": args["audio_dir"]}, + classes_to_ignore, + events_of_interest, + False, + False, + list_of_anns=True, + filter_issues=True, + name_replace=name_dict, + ) - print('Dataset name: ' + args['dataset_name']) - print('Audio directory: ' + args['audio_dir']) - print('Annotation directory: ' + args['ann_dir']) - print('Ouput directory: ' + args['op_dir']) - print('Num annotated files: ' + str(len(data_all))) + print("Dataset name: " + args["dataset_name"]) + print("Audio directory: " + args["audio_dir"]) + print("Annotation directory: " + args["ann_dir"]) + print("Ouput directory: " + args["op_dir"]) + print("Num annotated files: " + str(len(data_all))) - if args['train_file'] != '' and args['test_file'] != '': + if args["train_file"] != "" and args["test_file"] != "": # user has specifed the train / test split - train_files = load_file_names(args['train_file']) - test_files = load_file_names(args['test_file']) - file_names_all = [dd['id'] for dd in data_all] - train_inds = [file_names_all.index(ff) for ff in train_files if ff in file_names_all] - test_inds = [file_names_all.index(ff) for ff in test_files if ff in file_names_all] + train_files = load_file_names(args["train_file"]) + test_files = load_file_names(args["test_file"]) + file_names_all = [dd["id"] for dd in data_all] + train_inds = [ + file_names_all.index(ff) + for ff in train_files + if ff in file_names_all + ] + test_inds = [ + file_names_all.index(ff) + for ff in test_files + if ff in file_names_all + ] else: # split the data into train and test at the file level num_exs = len(data_all) - test_inds = np.random.choice(np.arange(num_exs), int(num_exs*args['percent_val']), replace=False) + test_inds = np.random.choice( + np.arange(num_exs), + int(num_exs * args["percent_val"]), + replace=False, + ) test_inds = np.sort(test_inds) train_inds = np.setdiff1d(np.arange(num_exs), test_inds) data_train = [data_all[ii] for ii in train_inds] data_test = [data_all[ii] for ii in test_inds] - if not os.path.isdir(args['op_dir']): - os.makedirs(args['op_dir']) - op_name = os.path.join(args['op_dir'], args['dataset_name']) - op_name_train = op_name + '_TRAIN.json' - op_name_test = op_name + '_TEST.json' + if not os.path.isdir(args["op_dir"]): + os.makedirs(args["op_dir"]) + op_name = os.path.join(args["op_dir"], args["dataset_name"]) + op_name_train = op_name + "_TRAIN.json" + op_name_test = op_name + "_TEST.json" - class_un_train = print_dataset_stats(data_train, 'Train', classes_to_ignore) - class_un_test = print_dataset_stats(data_test, 'Test', classes_to_ignore) + class_un_train = print_dataset_stats( + data_train, "Train", classes_to_ignore + ) + class_un_test = print_dataset_stats(data_test, "Test", classes_to_ignore) if len(data_train) > 0 and len(data_test) > 0: if class_un_train != class_un_test: - print('\nError: some classes are not in both the training and test sets.\ - \nTry a different random seed "--rand_seed".') + print( + '\nError: some classes are not in both the training and test sets.\ + \nTry a different random seed "--rand_seed".' + ) assert False - print('\n') + print("\n") if len(data_train) == 0: - print('No train annotations to save') + print("No train annotations to save") else: - print('Saving: ', op_name_train) - with open(op_name_train, 'w') as da: + print("Saving: ", op_name_train) + with open(op_name_train, "w") as da: json.dump(data_train, da, indent=2) if len(data_test) == 0: - print('No test annotations to save') + print("No test annotations to save") else: - print('Saving: ', op_name_test) - with open(op_name_test, 'w') as da: + print("Saving: ", op_name_test) + with open(op_name_test, "w") as da: json.dump(data_test, da, indent=2) diff --git a/bat_detect/train/audio_dataloader.py b/bat_detect/train/audio_dataloader.py index a36ec0b..ffd8086 100644 --- a/bat_detect/train/audio_dataloader.py +++ b/bat_detect/train/audio_dataloader.py @@ -1,71 +1,101 @@ -import torch -import random -import numpy as np import copy +import os +import random +import sys + import librosa +import numpy as np +import torch import torch.nn.functional as F import torchaudio -import os -import sys -sys.path.append(os.path.join('..', '..')) +sys.path.append(os.path.join("..", "..")) import bat_detect.utils.audio_utils as au def generate_gt_heatmaps(spec_op_shape, sampling_rate, ann, params): # spec may be resized on input into the network - num_classes = len(params['class_names']) - op_height = spec_op_shape[0] - op_width = spec_op_shape[1] - freq_per_bin = (params['max_freq'] - params['min_freq']) / op_height + num_classes = len(params["class_names"]) + op_height = spec_op_shape[0] + op_width = spec_op_shape[1] + freq_per_bin = (params["max_freq"] - params["min_freq"]) / op_height # start and end times - x_pos_start = au.time_to_x_coords(ann['start_times'], sampling_rate, - params['fft_win_length'], params['fft_overlap']) - x_pos_start = (params['resize_factor']*x_pos_start).astype(np.int) - x_pos_end = au.time_to_x_coords(ann['end_times'], sampling_rate, - params['fft_win_length'], params['fft_overlap']) - x_pos_end = (params['resize_factor']*x_pos_end).astype(np.int) + x_pos_start = au.time_to_x_coords( + ann["start_times"], + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + ) + x_pos_start = (params["resize_factor"] * x_pos_start).astype(np.int) + x_pos_end = au.time_to_x_coords( + ann["end_times"], + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + ) + x_pos_end = (params["resize_factor"] * x_pos_end).astype(np.int) # location on y axis i.e. frequency - y_pos_low = (ann['low_freqs'] - params['min_freq']) / freq_per_bin - y_pos_low = (op_height - y_pos_low).astype(np.int) - y_pos_high = (ann['high_freqs'] - params['min_freq']) / freq_per_bin + y_pos_low = (ann["low_freqs"] - params["min_freq"]) / freq_per_bin + y_pos_low = (op_height - y_pos_low).astype(np.int) + y_pos_high = (ann["high_freqs"] - params["min_freq"]) / freq_per_bin y_pos_high = (op_height - y_pos_high).astype(np.int) - bb_widths = x_pos_end - x_pos_start - bb_heights = (y_pos_low - y_pos_high) + bb_widths = x_pos_end - x_pos_start + bb_heights = y_pos_low - y_pos_high - valid_inds = np.where((x_pos_start >= 0) & (x_pos_start < op_width) & - (y_pos_low >= 0) & (y_pos_low < (op_height-1)))[0] + valid_inds = np.where( + (x_pos_start >= 0) + & (x_pos_start < op_width) + & (y_pos_low >= 0) + & (y_pos_low < (op_height - 1)) + )[0] ann_aug = {} - ann_aug['x_inds'] = x_pos_start[valid_inds] - ann_aug['y_inds'] = y_pos_low[valid_inds] - keys = ['start_times', 'end_times', 'high_freqs', 'low_freqs', 'class_ids', 'individual_ids'] + ann_aug["x_inds"] = x_pos_start[valid_inds] + ann_aug["y_inds"] = y_pos_low[valid_inds] + keys = [ + "start_times", + "end_times", + "high_freqs", + "low_freqs", + "class_ids", + "individual_ids", + ] for kk in keys: ann_aug[kk] = ann[kk][valid_inds] # if the number of calls is only 1, then it is unique # TODO would be better if we found these unique calls at the merging stage - if len(ann_aug['individual_ids']) == 1: - ann_aug['individual_ids'][0] = 0 + if len(ann_aug["individual_ids"]) == 1: + ann_aug["individual_ids"][0] = 0 - y_2d_det = np.zeros((1, op_height, op_width), dtype=np.float32) + y_2d_det = np.zeros((1, op_height, op_width), dtype=np.float32) y_2d_size = np.zeros((2, op_height, op_width), dtype=np.float32) # num classes and "background" class - y_2d_classes = np.zeros((num_classes+1, op_height, op_width), dtype=np.float32) + y_2d_classes = np.zeros( + (num_classes + 1, op_height, op_width), dtype=np.float32 + ) # create 2D ground truth heatmaps for ii in valid_inds: - draw_gaussian(y_2d_det[0,:], (x_pos_start[ii], y_pos_low[ii]), params['target_sigma']) - #draw_gaussian(y_2d_det[0,:], (x_pos_start[ii], y_pos_low[ii]), params['target_sigma'], params['target_sigma']*2) + draw_gaussian( + y_2d_det[0, :], + (x_pos_start[ii], y_pos_low[ii]), + params["target_sigma"], + ) + # draw_gaussian(y_2d_det[0,:], (x_pos_start[ii], y_pos_low[ii]), params['target_sigma'], params['target_sigma']*2) y_2d_size[0, y_pos_low[ii], x_pos_start[ii]] = bb_widths[ii] y_2d_size[1, y_pos_low[ii], x_pos_start[ii]] = bb_heights[ii] - cls_id = ann['class_ids'][ii] + cls_id = ann["class_ids"][ii] if cls_id > -1: - draw_gaussian(y_2d_classes[cls_id, :], (x_pos_start[ii], y_pos_low[ii]), params['target_sigma']) - #draw_gaussian(y_2d_classes[cls_id, :], (x_pos_start[ii], y_pos_low[ii]), params['target_sigma'], params['target_sigma']*2) + draw_gaussian( + y_2d_classes[cls_id, :], + (x_pos_start[ii], y_pos_low[ii]), + params["target_sigma"], + ) + # draw_gaussian(y_2d_classes[cls_id, :], (x_pos_start[ii], y_pos_low[ii]), params['target_sigma'], params['target_sigma']*2) # be careful as this will have a 1.0 places where we have event but dont know gt class # this will be masked in training anyway @@ -96,20 +126,24 @@ def draw_gaussian(heatmap, center, sigmax, sigmay=None): x = np.arange(0, size, 1, np.float32) y = x[:, np.newaxis] x0 = y0 = size // 2 - #g = np.exp(- ((x - x0) ** 2 + (y - y0) ** 2) / (2 * sigma ** 2)) - g = np.exp(- ((x - x0) ** 2)/(2 * sigmax ** 2) - ((y - y0) ** 2)/(2 * sigmay ** 2)) + # g = np.exp(- ((x - x0) ** 2 + (y - y0) ** 2) / (2 * sigma ** 2)) + g = np.exp( + -((x - x0) ** 2) / (2 * sigmax**2) + - ((y - y0) ** 2) / (2 * sigmay**2) + ) g_x = max(0, -ul[0]), min(br[0], h) - ul[0] g_y = max(0, -ul[1]), min(br[1], w) - ul[1] img_x = max(0, ul[0]), min(br[0], h) img_y = max(0, ul[1]), min(br[1], w) - heatmap[img_y[0]:img_y[1], img_x[0]:img_x[1]] = np.maximum( - heatmap[img_y[0]:img_y[1], img_x[0]:img_x[1]], - g[g_y[0]:g_y[1], g_x[0]:g_x[1]]) + heatmap[img_y[0] : img_y[1], img_x[0] : img_x[1]] = np.maximum( + heatmap[img_y[0] : img_y[1], img_x[0] : img_x[1]], + g[g_y[0] : g_y[1], g_x[0] : g_x[1]], + ) return True def pad_aray(ip_array, pad_size): - return np.hstack((ip_array, np.ones(pad_size, dtype=np.int)*-1)) + return np.hstack((ip_array, np.ones(pad_size, dtype=np.int) * -1)) def warp_spec_aug(spec, ann, return_spec_for_viz, params): @@ -121,24 +155,37 @@ def warp_spec_aug(spec, ann, return_spec_for_viz, params): if return_spec_for_viz: assert False - delta = params['stretch_squeeze_delta'] + delta = params["stretch_squeeze_delta"] op_size = (spec.shape[1], spec.shape[2]) - resize_fract_r = np.random.rand()*delta*2 - delta + 1.0 - resize_amt = int(spec.shape[2]*resize_fract_r) + resize_fract_r = np.random.rand() * delta * 2 - delta + 1.0 + resize_amt = int(spec.shape[2] * resize_fract_r) if resize_amt >= spec.shape[2]: - spec_r = torch.cat((spec, torch.zeros((1, spec.shape[1], resize_amt-spec.shape[2]), dtype=spec.dtype)), 2) + spec_r = torch.cat( + ( + spec, + torch.zeros( + (1, spec.shape[1], resize_amt - spec.shape[2]), + dtype=spec.dtype, + ), + ), + 2, + ) else: spec_r = spec[:, :, :resize_amt] - spec = F.interpolate(spec_r.unsqueeze(0), size=op_size, mode='bilinear', align_corners=False).squeeze(0) - ann['start_times'] *= (1.0/resize_fract_r) - ann['end_times'] *= (1.0/resize_fract_r) + spec = F.interpolate( + spec_r.unsqueeze(0), size=op_size, mode="bilinear", align_corners=False + ).squeeze(0) + ann["start_times"] *= 1.0 / resize_fract_r + ann["end_times"] *= 1.0 / resize_fract_r return spec def mask_time_aug(spec, params): # Mask out a random block of time - repeat up to 3 times # SpecAugment: A Simple Data Augmentation Methodfor Automatic Speech Recognition - fm = torchaudio.transforms.TimeMasking(int(spec.shape[1]*params['mask_max_time_perc'])) + fm = torchaudio.transforms.TimeMasking( + int(spec.shape[1] * params["mask_max_time_perc"]) + ) for ii in range(np.random.randint(1, 4)): spec = fm(spec) return spec @@ -147,40 +194,59 @@ def mask_time_aug(spec, params): def mask_freq_aug(spec, params): # Mask out a random frequncy range - repeat up to 3 times # SpecAugment: A Simple Data Augmentation Method for Automatic Speech Recognition - fm = torchaudio.transforms.FrequencyMasking(int(spec.shape[1]*params['mask_max_freq_perc'])) + fm = torchaudio.transforms.FrequencyMasking( + int(spec.shape[1] * params["mask_max_freq_perc"]) + ) for ii in range(np.random.randint(1, 4)): spec = fm(spec) return spec def scale_vol_aug(spec, params): - return spec * np.random.random()*params['spec_amp_scaling'] + return spec * np.random.random() * params["spec_amp_scaling"] def echo_aug(audio, sampling_rate, params): - sample_offset = int(params['echo_max_delay']*np.random.random()*sampling_rate) + 1 - audio[:-sample_offset] += np.random.random()*audio[sample_offset:] + sample_offset = ( + int(params["echo_max_delay"] * np.random.random() * sampling_rate) + 1 + ) + audio[:-sample_offset] += np.random.random() * audio[sample_offset:] return audio def resample_aug(audio, sampling_rate, params): sampling_rate_old = sampling_rate - sampling_rate = np.random.choice(params['aug_sampling_rates']) - audio = librosa.resample(audio, sampling_rate_old, sampling_rate, res_type='polyphase') + sampling_rate = np.random.choice(params["aug_sampling_rates"]) + audio = librosa.resample( + audio, sampling_rate_old, sampling_rate, res_type="polyphase" + ) - audio = au.pad_audio(audio, sampling_rate, params['fft_win_length'], - params['fft_overlap'], params['resize_factor'], - params['spec_divide_factor'], params['spec_train_width']) + audio = au.pad_audio( + audio, + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + params["resize_factor"], + params["spec_divide_factor"], + params["spec_train_width"], + ) duration = audio.shape[0] / float(sampling_rate) return audio, sampling_rate, duration def resample_audio(num_samples, sampling_rate, audio2, sampling_rate2): if sampling_rate != sampling_rate2: - audio2 = librosa.resample(audio2, sampling_rate2, sampling_rate, res_type='polyphase') + audio2 = librosa.resample( + audio2, sampling_rate2, sampling_rate, res_type="polyphase" + ) sampling_rate2 = sampling_rate if audio2.shape[0] < num_samples: - audio2 = np.hstack((audio2, np.zeros((num_samples-audio2.shape[0]), dtype=audio2.dtype))) + audio2 = np.hstack( + ( + audio2, + np.zeros((num_samples - audio2.shape[0]), dtype=audio2.dtype), + ) + ) elif audio2.shape[0] > num_samples: audio2 = audio2[:num_samples] return audio2, sampling_rate2 @@ -189,33 +255,43 @@ def resample_audio(num_samples, sampling_rate, audio2, sampling_rate2): def combine_audio_aug(audio, sampling_rate, ann, audio2, sampling_rate2, ann2): # resample so they are the same - audio2, sampling_rate2 = resample_audio(audio.shape[0], sampling_rate, audio2, sampling_rate2) + audio2, sampling_rate2 = resample_audio( + audio.shape[0], sampling_rate, audio2, sampling_rate2 + ) # # set mean and std to be the same # audio2 = (audio2 - audio2.mean()) # audio2 = (audio2/audio2.std())*audio.std() # audio2 = audio2 + audio.mean() - if ann['annotated'] and (ann2['annotated']) and \ - (sampling_rate2 == sampling_rate) and (audio.shape[0] == audio2.shape[0]): - comb_weight = 0.3 + np.random.random()*0.4 - audio = comb_weight*audio + (1-comb_weight)*audio2 - inds = np.argsort(np.hstack((ann['start_times'], ann2['start_times']))) + if ( + ann["annotated"] + and (ann2["annotated"]) + and (sampling_rate2 == sampling_rate) + and (audio.shape[0] == audio2.shape[0]) + ): + comb_weight = 0.3 + np.random.random() * 0.4 + audio = comb_weight * audio + (1 - comb_weight) * audio2 + inds = np.argsort(np.hstack((ann["start_times"], ann2["start_times"]))) for kk in ann.keys(): # when combining calls from different files, assume they come from different individuals - if kk == 'individual_ids': - if (ann[kk]>-1).sum() > 0: - ann2[kk][ann2[kk]>-1] += np.max(ann[kk][ann[kk]>-1]) + 1 + if kk == "individual_ids": + if (ann[kk] > -1).sum() > 0: + ann2[kk][ann2[kk] > -1] += ( + np.max(ann[kk][ann[kk] > -1]) + 1 + ) - if (kk != 'class_id_file') and (kk != 'annotated'): + if (kk != "class_id_file") and (kk != "annotated"): ann[kk] = np.hstack((ann[kk], ann2[kk]))[inds] return audio, ann class AudioLoader(torch.utils.data.Dataset): - def __init__(self, data_anns_ip, params, dataset_name=None, is_train=False): + def __init__( + self, data_anns_ip, params, dataset_name=None, is_train=False + ): self.data_anns = [] self.is_train = is_train @@ -227,53 +303,70 @@ class AudioLoader(torch.utils.data.Dataset): # filter out unused annotation here filtered_annotations = [] - for ii, aa in enumerate(dd['annotation']): + for ii, aa in enumerate(dd["annotation"]): - if 'individual' in aa.keys(): - aa['individual'] = int(aa['individual']) + if "individual" in aa.keys(): + aa["individual"] = int(aa["individual"]) # if only one call labeled it has to be from the same individual - if len(dd['annotation']) == 1: - aa['individual'] = 0 + if len(dd["annotation"]) == 1: + aa["individual"] = 0 # convert class name into class label - if aa['class'] in self.params['class_names']: - aa['class_id'] = self.params['class_names'].index(aa['class']) + if aa["class"] in self.params["class_names"]: + aa["class_id"] = self.params["class_names"].index( + aa["class"] + ) else: - aa['class_id'] = -1 + aa["class_id"] = -1 - if aa['class'] not in self.params['classes_to_ignore']: + if aa["class"] not in self.params["classes_to_ignore"]: filtered_annotations.append(aa) - dd['annotation'] = filtered_annotations - dd['start_times'] = np.array([aa['start_time'] for aa in dd['annotation']]) - dd['end_times'] = np.array([aa['end_time'] for aa in dd['annotation']]) - dd['high_freqs'] = np.array([float(aa['high_freq']) for aa in dd['annotation']]) - dd['low_freqs'] = np.array([float(aa['low_freq']) for aa in dd['annotation']]) - dd['class_ids'] = np.array([aa['class_id'] for aa in dd['annotation']]).astype(np.int) - dd['individual_ids'] = np.array([aa['individual'] for aa in dd['annotation']]).astype(np.int) + dd["annotation"] = filtered_annotations + dd["start_times"] = np.array( + [aa["start_time"] for aa in dd["annotation"]] + ) + dd["end_times"] = np.array( + [aa["end_time"] for aa in dd["annotation"]] + ) + dd["high_freqs"] = np.array( + [float(aa["high_freq"]) for aa in dd["annotation"]] + ) + dd["low_freqs"] = np.array( + [float(aa["low_freq"]) for aa in dd["annotation"]] + ) + dd["class_ids"] = np.array( + [aa["class_id"] for aa in dd["annotation"]] + ).astype(np.int) + dd["individual_ids"] = np.array( + [aa["individual"] for aa in dd["annotation"]] + ).astype(np.int) # file level class name - dd['class_id_file'] = -1 - if 'class_name' in dd.keys(): - if dd['class_name'] in self.params['class_names']: - dd['class_id_file'] = self.params['class_names'].index(dd['class_name']) + dd["class_id_file"] = -1 + if "class_name" in dd.keys(): + if dd["class_name"] in self.params["class_names"]: + dd["class_id_file"] = self.params["class_names"].index( + dd["class_name"] + ) self.data_anns.append(dd) - ann_cnt = [len(aa['annotation']) for aa in self.data_anns] - self.max_num_anns = 2*np.max(ann_cnt) # x2 because we may be combining files during training + ann_cnt = [len(aa["annotation"]) for aa in self.data_anns] + self.max_num_anns = 2 * np.max( + ann_cnt + ) # x2 because we may be combining files during training - print('\n') + print("\n") if dataset_name is not None: - print('Dataset : ' + dataset_name) + print("Dataset : " + dataset_name) if self.is_train: - print('Split type : train') + print("Split type : train") else: - print('Split type : test') - print('Num files : ' + str(len(self.data_anns))) - print('Num calls : ' + str(np.sum(ann_cnt))) - + print("Split type : test") + print("Num files : " + str(len(self.data_anns))) + print("Num calls : " + str(np.sum(ann_cnt))) def get_file_and_anns(self, index=None): @@ -281,110 +374,171 @@ class AudioLoader(torch.utils.data.Dataset): if index == None: index = np.random.randint(0, len(self.data_anns)) - audio_file = self.data_anns[index]['file_path'] - sampling_rate, audio_raw = au.load_audio_file(audio_file, self.data_anns[index]['time_exp'], - self.params['target_samp_rate'], self.params['scale_raw_audio']) + audio_file = self.data_anns[index]["file_path"] + sampling_rate, audio_raw = au.load_audio_file( + audio_file, + self.data_anns[index]["time_exp"], + self.params["target_samp_rate"], + self.params["scale_raw_audio"], + ) # copy annotation ann = {} - ann['annotated'] = self.data_anns[index]['annotated'] - ann['class_id_file'] = self.data_anns[index]['class_id_file'] - keys = ['start_times', 'end_times', 'high_freqs', 'low_freqs', 'class_ids', 'individual_ids'] + ann["annotated"] = self.data_anns[index]["annotated"] + ann["class_id_file"] = self.data_anns[index]["class_id_file"] + keys = [ + "start_times", + "end_times", + "high_freqs", + "low_freqs", + "class_ids", + "individual_ids", + ] for kk in keys: ann[kk] = self.data_anns[index][kk].copy() # if train then grab a random crop if self.is_train: - nfft = int(self.params['fft_win_length']*sampling_rate) - noverlap = int(self.params['fft_overlap']*nfft) - length_samples = self.params['spec_train_width']*(nfft - noverlap) + noverlap + nfft = int(self.params["fft_win_length"] * sampling_rate) + noverlap = int(self.params["fft_overlap"] * nfft) + length_samples = ( + self.params["spec_train_width"] * (nfft - noverlap) + noverlap + ) if audio_raw.shape[0] - length_samples > 0: - sample_crop = np.random.randint(audio_raw.shape[0] - length_samples) + sample_crop = np.random.randint( + audio_raw.shape[0] - length_samples + ) else: sample_crop = 0 - audio_raw = audio_raw[sample_crop:sample_crop+length_samples] - ann['start_times'] = ann['start_times'] - sample_crop/float(sampling_rate) - ann['end_times'] = ann['end_times'] - sample_crop/float(sampling_rate) + audio_raw = audio_raw[sample_crop : sample_crop + length_samples] + ann["start_times"] = ann["start_times"] - sample_crop / float( + sampling_rate + ) + ann["end_times"] = ann["end_times"] - sample_crop / float( + sampling_rate + ) # pad audio if self.is_train: - op_spec_target_size = self.params['spec_train_width'] + op_spec_target_size = self.params["spec_train_width"] else: op_spec_target_size = None - audio_raw = au.pad_audio(audio_raw, sampling_rate, self.params['fft_win_length'], - self.params['fft_overlap'], self.params['resize_factor'], - self.params['spec_divide_factor'], op_spec_target_size) + audio_raw = au.pad_audio( + audio_raw, + sampling_rate, + self.params["fft_win_length"], + self.params["fft_overlap"], + self.params["resize_factor"], + self.params["spec_divide_factor"], + op_spec_target_size, + ) duration = audio_raw.shape[0] / float(sampling_rate) # sort based on time - inds = np.argsort(ann['start_times']) + inds = np.argsort(ann["start_times"]) for kk in ann.keys(): - if (kk != 'class_id_file') and (kk != 'annotated'): + if (kk != "class_id_file") and (kk != "annotated"): ann[kk] = ann[kk][inds] return audio_raw, sampling_rate, duration, ann - def __getitem__(self, index): # load audio file audio, sampling_rate, duration, ann = self.get_file_and_anns(index) # augment on raw audio - if self.is_train and self.params['augment_at_train']: + if self.is_train and self.params["augment_at_train"]: # augment - combine with random audio file - if self.params['augment_at_train_combine'] and np.random.random() < self.params['aug_prob']: - audio2, sampling_rate2, duration2, ann2 = self.get_file_and_anns() - audio, ann = combine_audio_aug(audio, sampling_rate, ann, audio2, sampling_rate2, ann2) + if ( + self.params["augment_at_train_combine"] + and np.random.random() < self.params["aug_prob"] + ): + ( + audio2, + sampling_rate2, + duration2, + ann2, + ) = self.get_file_and_anns() + audio, ann = combine_audio_aug( + audio, sampling_rate, ann, audio2, sampling_rate2, ann2 + ) # simulate echo by adding delayed copy of the file - if np.random.random() < self.params['aug_prob']: + if np.random.random() < self.params["aug_prob"]: audio = echo_aug(audio, sampling_rate, self.params) # resample the audio - #if np.random.random() < self.params['aug_prob']: + # if np.random.random() < self.params['aug_prob']: # audio, sampling_rate, duration = resample_aug(audio, sampling_rate, self.params) # create spectrogram - spec, spec_for_viz = au.generate_spectrogram(audio, sampling_rate, self.params, self.return_spec_for_viz) - rsf = self.params['resize_factor'] - spec_op_shape = (int(self.params['spec_height']*rsf), int(spec.shape[1]*rsf)) + spec, spec_for_viz = au.generate_spectrogram( + audio, sampling_rate, self.params, self.return_spec_for_viz + ) + rsf = self.params["resize_factor"] + spec_op_shape = ( + int(self.params["spec_height"] * rsf), + int(spec.shape[1] * rsf), + ) # resize the spec spec = torch.from_numpy(spec).unsqueeze(0).unsqueeze(0) - spec = F.interpolate(spec, size=spec_op_shape, mode='bilinear', align_corners=False).squeeze(0) + spec = F.interpolate( + spec, size=spec_op_shape, mode="bilinear", align_corners=False + ).squeeze(0) # augment spectrogram - if self.is_train and self.params['augment_at_train']: + if self.is_train and self.params["augment_at_train"]: - if np.random.random() < self.params['aug_prob']: + if np.random.random() < self.params["aug_prob"]: spec = scale_vol_aug(spec, self.params) - if np.random.random() < self.params['aug_prob']: - spec = warp_spec_aug(spec, ann, self.return_spec_for_viz, self.params) + if np.random.random() < self.params["aug_prob"]: + spec = warp_spec_aug( + spec, ann, self.return_spec_for_viz, self.params + ) - if np.random.random() < self.params['aug_prob']: + if np.random.random() < self.params["aug_prob"]: spec = mask_time_aug(spec, self.params) - if np.random.random() < self.params['aug_prob']: + if np.random.random() < self.params["aug_prob"]: spec = mask_freq_aug(spec, self.params) outputs = {} - outputs['spec'] = spec + outputs["spec"] = spec if self.return_spec_for_viz: - outputs['spec_for_viz'] = torch.from_numpy(spec_for_viz).unsqueeze(0) + outputs["spec_for_viz"] = torch.from_numpy(spec_for_viz).unsqueeze( + 0 + ) # create ground truth heatmaps - outputs['y_2d_det'], outputs['y_2d_size'], outputs['y_2d_classes'], ann_aug =\ - generate_gt_heatmaps(spec_op_shape, sampling_rate, ann, self.params) + ( + outputs["y_2d_det"], + outputs["y_2d_size"], + outputs["y_2d_classes"], + ann_aug, + ) = generate_gt_heatmaps( + spec_op_shape, sampling_rate, ann, self.params + ) # hack to get around requirement that all vectors are the same length in # the output batch - pad_size = self.max_num_anns-len(ann_aug['individual_ids']) - outputs['is_valid'] = pad_aray(np.ones(len(ann_aug['individual_ids'])), pad_size) - keys = ['class_ids', 'individual_ids', 'x_inds', 'y_inds', - 'start_times', 'end_times', 'low_freqs', 'high_freqs'] + pad_size = self.max_num_anns - len(ann_aug["individual_ids"]) + outputs["is_valid"] = pad_aray( + np.ones(len(ann_aug["individual_ids"])), pad_size + ) + keys = [ + "class_ids", + "individual_ids", + "x_inds", + "y_inds", + "start_times", + "end_times", + "low_freqs", + "high_freqs", + ] for kk in keys: outputs[kk] = pad_aray(ann_aug[kk], pad_size) @@ -394,14 +548,13 @@ class AudioLoader(torch.utils.data.Dataset): outputs[kk] = torch.from_numpy(outputs[kk]) # scalars - outputs['class_id_file'] = ann['class_id_file'] - outputs['annotated'] = ann['annotated'] - outputs['duration'] = duration - outputs['sampling_rate'] = sampling_rate - outputs['file_id'] = index + outputs["class_id_file"] = ann["class_id_file"] + outputs["annotated"] = ann["annotated"] + outputs["duration"] = duration + outputs["sampling_rate"] = sampling_rate + outputs["file_id"] = index return outputs - def __len__(self): return len(self.data_anns) diff --git a/bat_detect/train/evaluate.py b/bat_detect/train/evaluate.py index b88719f..a926fbb 100755 --- a/bat_detect/train/evaluate.py +++ b/bat_detect/train/evaluate.py @@ -1,6 +1,10 @@ import numpy as np -from sklearn.metrics import roc_curve, auc -from sklearn.metrics import accuracy_score, balanced_accuracy_score +from sklearn.metrics import ( + accuracy_score, + auc, + balanced_accuracy_score, + roc_curve, +) def compute_error_auc(op_str, gt, pred, prob): @@ -13,8 +17,11 @@ def compute_error_auc(op_str, gt, pred, prob): fpr, tpr, thresholds = roc_curve(gt, pred) roc_auc = auc(fpr, tpr) - print(op_str + ", class acc = {:.3f}, ROC AUC = {:.3f}".format(class_acc, roc_auc)) - #return class_acc, roc_auc + print( + op_str + + ", class acc = {:.3f}, ROC AUC = {:.3f}".format(class_acc, roc_auc) + ) + # return class_acc, roc_auc def calc_average_precision(recall, precision): @@ -25,10 +32,10 @@ def calc_average_precision(recall, precision): # pascal 12 way mprec = np.hstack((0, precision, 0)) mrec = np.hstack((0, recall, 1)) - for ii in range(mprec.shape[0]-2, -1,-1): - mprec[ii] = np.maximum(mprec[ii], mprec[ii+1]) - inds = np.where(np.not_equal(mrec[1:], mrec[:-1]))[0]+1 - ave_prec = ((mrec[inds] - mrec[inds-1])*mprec[inds]).sum() + for ii in range(mprec.shape[0] - 2, -1, -1): + mprec[ii] = np.maximum(mprec[ii], mprec[ii + 1]) + inds = np.where(np.not_equal(mrec[1:], mrec[:-1]))[0] + 1 + ave_prec = ((mrec[inds] - mrec[inds - 1]) * mprec[inds]).sum() return float(ave_prec) @@ -37,7 +44,7 @@ def calc_recall_at_x(recall, precision, x=0.95): precision[np.isnan(precision)] = 0 recall[np.isnan(recall)] = 0 - inds = np.where(precision[::-1]>x)[0] + inds = np.where(precision[::-1] > x)[0] if len(inds) > 0: return float(recall[::-1][inds[0]]) else: @@ -51,7 +58,15 @@ def compute_affinity_1d(pred_box, gt_boxes, threshold): return valid_detection, np.argmin(score) -def compute_pre_rec(gts, preds, eval_mode, class_of_interest, num_classes, threshold, ignore_start_end): +def compute_pre_rec( + gts, + preds, + eval_mode, + class_of_interest, + num_classes, + threshold, + ignore_start_end, +): """ Computes precision and recall. Assumes that each file has been exhaustively annotated. Will not count predicted detection with a start time that is within @@ -78,26 +93,40 @@ def compute_pre_rec(gts, preds, eval_mode, class_of_interest, num_classes, thres for pid, pp in enumerate(preds): # filter predicted calls that are too near the start or end of the file - file_dur = gts[pid]['duration'] - valid_inds = (pp['start_times'] >= ignore_start_end) & (pp['start_times'] <= (file_dur - ignore_start_end)) + file_dur = gts[pid]["duration"] + valid_inds = (pp["start_times"] >= ignore_start_end) & ( + pp["start_times"] <= (file_dur - ignore_start_end) + ) - pred_boxes.append(np.vstack((pp['start_times'][valid_inds], pp['end_times'][valid_inds], - pp['low_freqs'][valid_inds], pp['high_freqs'][valid_inds])).T) + pred_boxes.append( + np.vstack( + ( + pp["start_times"][valid_inds], + pp["end_times"][valid_inds], + pp["low_freqs"][valid_inds], + pp["high_freqs"][valid_inds], + ) + ).T + ) - if eval_mode == 'detection': + if eval_mode == "detection": # overall detection - confidence.append(pp['det_probs'][valid_inds]) - elif eval_mode == 'per_class': + confidence.append(pp["det_probs"][valid_inds]) + elif eval_mode == "per_class": # per class - confidence.append(pp['class_probs'].T[valid_inds, class_of_interest]) - elif eval_mode == 'top_class': + confidence.append( + pp["class_probs"].T[valid_inds, class_of_interest] + ) + elif eval_mode == "top_class": # per class - note that sometimes 'class_probs' can be num_classes+1 in size - top_class = np.argmax(pp['class_probs'].T[valid_inds, :num_classes], 1) - confidence.append(pp['class_probs'].T[valid_inds, top_class]) + top_class = np.argmax( + pp["class_probs"].T[valid_inds, :num_classes], 1 + ) + confidence.append(pp["class_probs"].T[valid_inds, top_class]) pred_class.append(top_class) # be careful, assuming the order in the list is same as GT - file_ids.append([pid]*valid_inds.sum()) + file_ids.append([pid] * valid_inds.sum()) confidence = np.hstack(confidence) file_ids = np.hstack(file_ids).astype(np.int) @@ -105,7 +134,6 @@ def compute_pre_rec(gts, preds, eval_mode, class_of_interest, num_classes, thres if len(pred_class) > 0: pred_class = np.hstack(pred_class) - # extract relevant ground truth boxes gt_boxes = [] gt_assigned = [] @@ -115,32 +143,42 @@ def compute_pre_rec(gts, preds, eval_mode, class_of_interest, num_classes, thres for gg in gts: # filter ground truth calls that are too near the start or end of the file - file_dur = gg['duration'] - valid_inds = (gg['start_times'] >= ignore_start_end) & (gg['start_times'] <= (file_dur - ignore_start_end)) + file_dur = gg["duration"] + valid_inds = (gg["start_times"] >= ignore_start_end) & ( + gg["start_times"] <= (file_dur - ignore_start_end) + ) # note, files with the incorrect duration will cause a problem - if (gg['start_times'] > file_dur).sum() > 0: - print('Error: file duration incorrect for', gg['id']) - assert(False) + if (gg["start_times"] > file_dur).sum() > 0: + print("Error: file duration incorrect for", gg["id"]) + assert False - boxes = np.vstack((gg['start_times'][valid_inds], gg['end_times'][valid_inds], - gg['low_freqs'][valid_inds], gg['high_freqs'][valid_inds])).T - gen_class = gg['class_ids'][valid_inds] == -1 - class_ids = gg['class_ids'][valid_inds] + boxes = np.vstack( + ( + gg["start_times"][valid_inds], + gg["end_times"][valid_inds], + gg["low_freqs"][valid_inds], + gg["high_freqs"][valid_inds], + ) + ).T + gen_class = gg["class_ids"][valid_inds] == -1 + class_ids = gg["class_ids"][valid_inds] # keep track of the number of relevant ground truth calls - if eval_mode == 'detection': + if eval_mode == "detection": # all valid ones - num_positives += len(gg['start_times'][valid_inds]) - elif eval_mode == 'per_class': + num_positives += len(gg["start_times"][valid_inds]) + elif eval_mode == "per_class": # all valid ones with class of interest - num_positives += (gg['class_ids'][valid_inds] == class_of_interest).sum() - elif eval_mode == 'top_class': + num_positives += ( + gg["class_ids"][valid_inds] == class_of_interest + ).sum() + elif eval_mode == "top_class": # all valid ones with non generic class - num_positives += (gg['class_ids'][valid_inds] > -1).sum() + num_positives += (gg["class_ids"][valid_inds] > -1).sum() # find relevant classes (i.e. class_of_interest) and events without known class (i.e. generic class, -1) - if eval_mode == 'per_class': + if eval_mode == "per_class": class_inds = (class_ids == class_of_interest) | (class_ids == -1) boxes = boxes[class_inds, :] gen_class = gen_class[class_inds] @@ -151,25 +189,27 @@ def compute_pre_rec(gts, preds, eval_mode, class_of_interest, num_classes, thres gt_generic_class.append(gen_class) gt_class.append(class_ids) - # loop through detections and keep track of those that have been assigned - true_pos = np.zeros(confidence.shape[0]) - valid_inds = np.ones(confidence.shape[0]) == 1 # intialize to True - sorted_inds = np.argsort(confidence)[::-1] # sort high to low + true_pos = np.zeros(confidence.shape[0]) + valid_inds = np.ones(confidence.shape[0]) == 1 # intialize to True + sorted_inds = np.argsort(confidence)[::-1] # sort high to low for ii, ind in enumerate(sorted_inds): gt_id = file_ids[ind] valid_det = False if gt_boxes[gt_id].shape[0] > 0: # compute overlap - valid_det, det_ind = compute_affinity_1d(pred_boxes[ind], gt_boxes[gt_id], - threshold) + valid_det, det_ind = compute_affinity_1d( + pred_boxes[ind], gt_boxes[gt_id], threshold + ) # valid detection that has not already been assigned if valid_det and (gt_assigned[gt_id][det_ind] == 0): count_as_true_pos = True - if eval_mode == 'top_class' and (gt_class[gt_id][det_ind] != pred_class[ind]): + if eval_mode == "top_class" and ( + gt_class[gt_id][det_ind] != pred_class[ind] + ): # needs to be the same class count_as_true_pos = False @@ -181,40 +221,43 @@ def compute_pre_rec(gts, preds, eval_mode, class_of_interest, num_classes, thres # if event is generic class (i.e. gt_generic_class[gt_id][det_ind] is True) # and eval_mode != 'detection', then ignore it if gt_generic_class[gt_id][det_ind]: - if eval_mode == 'per_class' or eval_mode == 'top_class': + if eval_mode == "per_class" or eval_mode == "top_class": valid_inds[ii] = False - # store threshold values - used for plotting conf_sorted = np.sort(confidence)[::-1][valid_inds] thresholds = np.linspace(0.1, 0.9, 9) thresholds_inds = np.zeros(len(thresholds), dtype=np.int) for ii, tt in enumerate(thresholds): thresholds_inds[ii] = np.argmin(conf_sorted > tt) - thresholds_inds[thresholds_inds==0] = -1 + thresholds_inds[thresholds_inds == 0] = -1 # compute precision and recall - true_pos = true_pos[valid_inds] - false_pos_c = np.cumsum(1-true_pos) - true_pos_c = np.cumsum(true_pos) + true_pos = true_pos[valid_inds] + false_pos_c = np.cumsum(1 - true_pos) + true_pos_c = np.cumsum(true_pos) recall = true_pos_c / num_positives - precision = true_pos_c / np.maximum(true_pos_c + false_pos_c, np.finfo(np.float64).eps) + precision = true_pos_c / np.maximum( + true_pos_c + false_pos_c, np.finfo(np.float64).eps + ) results = {} - results['recall'] = recall - results['precision'] = precision - results['num_gt'] = num_positives + results["recall"] = recall + results["precision"] = precision + results["num_gt"] = num_positives - results['thresholds'] = thresholds - results['thresholds_inds'] = thresholds_inds + results["thresholds"] = thresholds + results["thresholds_inds"] = thresholds_inds if num_positives == 0: - results['avg_prec'] = np.nan - results['rec_at_x'] = np.nan + results["avg_prec"] = np.nan + results["rec_at_x"] = np.nan else: - results['avg_prec'] = np.round(calc_average_precision(recall, precision), 5) - results['rec_at_x'] = np.round(calc_recall_at_x(recall, precision), 5) + results["avg_prec"] = np.round( + calc_average_precision(recall, precision), 5 + ) + results["rec_at_x"] = np.round(calc_recall_at_x(recall, precision), 5) return results @@ -230,19 +273,19 @@ def compute_file_accuracy_simple(gts, preds, num_classes): gt_valid = [] pred_valid = [] for ii in range(len(gts)): - gt_class = np.unique(gts[ii]['class_ids']) + gt_class = np.unique(gts[ii]["class_ids"]) if len(gt_class) == 1 and gt_class[0] != -1: gt_valid.append(gt_class[0]) - pred = preds[ii]['class_probs'][:num_classes, :].T + pred = preds[ii]["class_probs"][:num_classes, :].T pred_valid.append(np.argmax(pred.mean(0))) acc = (np.array(gt_valid) == np.array(pred_valid)).mean() res = {} - res['num_valid_files'] = len(gt_valid) - res['num_total_files'] = len(gts) - res['gt_valid_file'] = gt_valid - res['pred_valid_file'] = pred_valid - res['file_acc'] = np.round(acc, 5) + res["num_valid_files"] = len(gt_valid) + res["num_total_files"] = len(gts) + res["gt_valid_file"] = gt_valid + res["pred_valid_file"] = pred_valid + res["file_acc"] = np.round(acc, 5) return res @@ -256,12 +299,20 @@ def compute_file_accuracy(gts, preds, num_classes): # compute min and max scoring range - then threshold min_val = 0 - mins = [pp['class_probs'].min() for pp in preds if pp['class_probs'].shape[1] > 0] + mins = [ + pp["class_probs"].min() + for pp in preds + if pp["class_probs"].shape[1] > 0 + ] if len(mins) > 0: min_val = np.min(mins) max_val = 1.0 - maxes = [pp['class_probs'].max() for pp in preds if pp['class_probs'].shape[1] > 0] + maxes = [ + pp["class_probs"].max() + for pp in preds + if pp["class_probs"].shape[1] > 0 + ] if len(maxes) > 0: max_val = np.max(maxes) @@ -272,33 +323,37 @@ def compute_file_accuracy(gts, preds, num_classes): gt_valid = [] pred_valid_all = [] for ii in range(len(gts)): - gt_class = np.unique(gts[ii]['class_ids']) + gt_class = np.unique(gts[ii]["class_ids"]) if len(gt_class) == 1 and gt_class[0] != -1: gt_valid.append(gt_class[0]) - pred = preds[ii]['class_probs'][:num_classes, :].T + pred = preds[ii]["class_probs"][:num_classes, :].T p_class = np.zeros(len(thresh)) for tt in range(len(thresh)): - p_class[tt] = (pred*(pred>=thresh[tt])).sum(0).argmax() + p_class[tt] = (pred * (pred >= thresh[tt])).sum(0).argmax() pred_valid_all.append(p_class) # pick the result corresponding to the overall best threshold pred_valid_all = np.vstack(pred_valid_all) - acc_per_thresh = (np.array(gt_valid)[..., np.newaxis] == pred_valid_all).mean(0) + acc_per_thresh = ( + np.array(gt_valid)[..., np.newaxis] == pred_valid_all + ).mean(0) best_thresh = np.argmax(acc_per_thresh) best_acc = acc_per_thresh[best_thresh] pred_valid = pred_valid_all[:, best_thresh].astype(np.int).tolist() res = {} - res['num_valid_files'] = len(gt_valid) - res['num_total_files'] = len(gts) - res['gt_valid_file'] = gt_valid - res['pred_valid_file'] = pred_valid - res['file_acc'] = np.round(best_acc, 5) + res["num_valid_files"] = len(gt_valid) + res["num_total_files"] = len(gts) + res["gt_valid_file"] = gt_valid + res["pred_valid_file"] = pred_valid + res["file_acc"] = np.round(best_acc, 5) return res -def evaluate_predictions(gts, preds, class_names, detection_overlap, ignore_start_end=0.0): +def evaluate_predictions( + gts, preds, class_names, detection_overlap, ignore_start_end=0.0 +): """ Computes metrics derived from the precision and recall. Assumes that gts and preds are both lists of the same lengths, with ground @@ -307,24 +362,50 @@ def evaluate_predictions(gts, preds, class_names, detection_overlap, ignore_star Returns the overall detection results, and per class results """ - assert(len(gts) == len(preds)) + assert len(gts) == len(preds) num_classes = len(class_names) # evaluate detection on its own i.e. ignoring class - det_results = compute_pre_rec(gts, preds, 'detection', None, num_classes, detection_overlap, ignore_start_end) - top_class = compute_pre_rec(gts, preds, 'top_class', None, num_classes, detection_overlap, ignore_start_end) - det_results['top_class'] = top_class + det_results = compute_pre_rec( + gts, + preds, + "detection", + None, + num_classes, + detection_overlap, + ignore_start_end, + ) + top_class = compute_pre_rec( + gts, + preds, + "top_class", + None, + num_classes, + detection_overlap, + ignore_start_end, + ) + det_results["top_class"] = top_class # per class evaluation - det_results['class_pr'] = [] + det_results["class_pr"] = [] for cc in range(num_classes): - res = compute_pre_rec(gts, preds, 'per_class', cc, num_classes, detection_overlap, ignore_start_end) - res['name'] = class_names[cc] - det_results['class_pr'].append(res) + res = compute_pre_rec( + gts, + preds, + "per_class", + cc, + num_classes, + detection_overlap, + ignore_start_end, + ) + res["name"] = class_names[cc] + det_results["class_pr"].append(res) # ignores classes that are not present in the test set - det_results['avg_prec_class'] = np.mean([rs['avg_prec'] for rs in det_results['class_pr'] if rs['num_gt'] > 0]) - det_results['avg_prec_class'] = np.round(det_results['avg_prec_class'], 5) + det_results["avg_prec_class"] = np.mean( + [rs["avg_prec"] for rs in det_results["class_pr"] if rs["num_gt"] > 0] + ) + det_results["avg_prec_class"] = np.round(det_results["avg_prec_class"], 5) # file level evaluation res_file = compute_file_accuracy(gts, preds, num_classes) diff --git a/bat_detect/train/losses.py b/bat_detect/train/losses.py index aaef2c4..02bfdd6 100644 --- a/bat_detect/train/losses.py +++ b/bat_detect/train/losses.py @@ -7,7 +7,9 @@ def bbox_size_loss(pred_size, gt_size): Bounding box size loss. Only compute loss where there is a bounding box. """ gt_size_mask = (gt_size > 0).float() - return (F.l1_loss(pred_size*gt_size_mask, gt_size, reduction='sum') / (gt_size_mask.sum() + 1e-5)) + return F.l1_loss(pred_size * gt_size_mask, gt_size, reduction="sum") / ( + gt_size_mask.sum() + 1e-5 + ) def focal_loss(pred, gt, weights=None, valid_mask=None): @@ -24,20 +26,25 @@ def focal_loss(pred, gt, weights=None, valid_mask=None): neg_inds = gt.lt(1).float() pos_loss = torch.log(pred + eps) * torch.pow(1 - pred, alpha) * pos_inds - neg_loss = torch.log(1 - pred + eps) * torch.pow(pred, alpha) * torch.pow(1 - gt, beta) * neg_inds + neg_loss = ( + torch.log(1 - pred + eps) + * torch.pow(pred, alpha) + * torch.pow(1 - gt, beta) + * neg_inds + ) if weights is not None: - pos_loss = pos_loss*weights - #neg_loss = neg_loss*weights + pos_loss = pos_loss * weights + # neg_loss = neg_loss*weights if valid_mask is not None: - pos_loss = pos_loss*valid_mask - neg_loss = neg_loss*valid_mask + pos_loss = pos_loss * valid_mask + neg_loss = neg_loss * valid_mask pos_loss = pos_loss.sum() neg_loss = neg_loss.sum() - num_pos = pos_inds.float().sum() + num_pos = pos_inds.float().sum() if num_pos == 0: loss = -neg_loss else: @@ -47,10 +54,10 @@ def focal_loss(pred, gt, weights=None, valid_mask=None): def mse_loss(pred, gt, weights=None, valid_mask=None): """ - Mean squared error loss. + Mean squared error loss. """ if valid_mask is None: - op = ((gt-pred)**2).mean() + op = ((gt - pred) ** 2).mean() else: - op = (valid_mask*((gt-pred)**2)).sum() / valid_mask.sum() + op = (valid_mask * ((gt - pred) ** 2)).sum() / valid_mask.sum() return op diff --git a/bat_detect/train/train_model.py b/bat_detect/train/train_model.py index d955216..2fd33fe 100644 --- a/bat_detect/train/train_model.py +++ b/bat_detect/train/train_model.py @@ -1,32 +1,33 @@ -import numpy as np -import matplotlib.pyplot as plt +import argparse +import json import os +import sys + +import matplotlib.pyplot as plt +import numpy as np import torch import torch.nn.functional as F from torch.optim.lr_scheduler import CosineAnnealingLR -import json -import argparse -import sys -sys.path.append(os.path.join('..', '..')) - -import bat_detect.detector.parameters as parameters -import bat_detect.detector.models as models -import bat_detect.detector.post_process as pp -import bat_detect.utils.plot_utils as pu - -import bat_detect.train.audio_dataloader as adl -import bat_detect.train.evaluate as evl -import bat_detect.train.train_utils as tu -import bat_detect.train.train_split as ts -import bat_detect.train.losses as losses +sys.path.append(os.path.join("..", "..")) import warnings + +import bat_detect.detector.models as models +import bat_detect.detector.parameters as parameters +import bat_detect.detector.post_process as pp +import bat_detect.train.audio_dataloader as adl +import bat_detect.train.evaluate as evl +import bat_detect.train.losses as losses +import bat_detect.train.train_split as ts +import bat_detect.train.train_utils as tu +import bat_detect.utils.plot_utils as pu + warnings.filterwarnings("ignore", category=UserWarning) def save_images_batch(model, data_loader, params): - print('\nsaving images ...') + print("\nsaving images ...") is_train_state = data_loader.dataset.is_train data_loader.dataset.is_train = False @@ -36,67 +37,112 @@ def save_images_batch(model, data_loader, params): ind = 0 # first image in each batch with torch.no_grad(): for batch_idx, inputs in enumerate(data_loader): - data = inputs['spec'].to(params['device']) + data = inputs["spec"].to(params["device"]) outputs = model(data) - spec_viz = inputs['spec_for_viz'].data.cpu().numpy() - orig_index = inputs['file_id'][ind] - plot_title = data_loader.dataset.data_anns[orig_index]['id'] - op_file_name = params['op_im_dir_test'] + data_loader.dataset.data_anns[orig_index]['id'] + '.jpg' - save_image(spec_viz, outputs, ind, inputs, params, op_file_name, plot_title) + spec_viz = inputs["spec_for_viz"].data.cpu().numpy() + orig_index = inputs["file_id"][ind] + plot_title = data_loader.dataset.data_anns[orig_index]["id"] + op_file_name = ( + params["op_im_dir_test"] + + data_loader.dataset.data_anns[orig_index]["id"] + + ".jpg" + ) + save_image( + spec_viz, + outputs, + ind, + inputs, + params, + op_file_name, + plot_title, + ) data_loader.dataset.is_train = is_train_state data_loader.dataset.return_spec_for_viz = False -def save_image(spec_viz, outputs, ind, inputs, params, op_file_name, plot_title): - pred_nms, _ = pp.run_nms(outputs, params, inputs['sampling_rate'].float()) - pred_hm = outputs['pred_det'][ind, 0, :].data.cpu().numpy() +def save_image( + spec_viz, outputs, ind, inputs, params, op_file_name, plot_title +): + pred_nms, _ = pp.run_nms(outputs, params, inputs["sampling_rate"].float()) + pred_hm = outputs["pred_det"][ind, 0, :].data.cpu().numpy() spec_viz = spec_viz[ind, 0, :] - gt = parse_gt_data(inputs)[ind] - sampling_rate = inputs['sampling_rate'][ind].item() - duration = inputs['duration'][ind].item() + gt = parse_gt_data(inputs)[ind] + sampling_rate = inputs["sampling_rate"][ind].item() + duration = inputs["duration"][ind].item() - pu.plot_spec(spec_viz, sampling_rate, duration, gt, pred_nms[ind], - params, plot_title, op_file_name, pred_hm, plot_boxes=True, fixed_aspect=False) + pu.plot_spec( + spec_viz, + sampling_rate, + duration, + gt, + pred_nms[ind], + params, + plot_title, + op_file_name, + pred_hm, + plot_boxes=True, + fixed_aspect=False, + ) -def loss_fun(outputs, gt_det, gt_size, gt_class, det_criterion, params, class_inv_freq): +def loss_fun( + outputs, gt_det, gt_size, gt_class, det_criterion, params, class_inv_freq +): # detection loss - loss = params['det_loss_weight']*det_criterion(outputs['pred_det'], gt_det) + loss = params["det_loss_weight"] * det_criterion( + outputs["pred_det"], gt_det + ) # bounding box size loss - loss += params['size_loss_weight']*losses.bbox_size_loss(outputs['pred_size'], gt_size) + loss += params["size_loss_weight"] * losses.bbox_size_loss( + outputs["pred_size"], gt_size + ) # classification loss valid_mask = (gt_class[:, :-1, :, :].sum(1) > 0).float().unsqueeze(1) - p_class = outputs['pred_class'][:, :-1, :] - loss += params['class_loss_weight']*det_criterion(p_class, gt_class[:, :-1, :], valid_mask=valid_mask) + p_class = outputs["pred_class"][:, :-1, :] + loss += params["class_loss_weight"] * det_criterion( + p_class, gt_class[:, :-1, :], valid_mask=valid_mask + ) return loss -def train(model, epoch, data_loader, det_criterion, optimizer, scheduler, params): +def train( + model, epoch, data_loader, det_criterion, optimizer, scheduler, params +): model.train() train_loss = tu.AverageMeter() - class_inv_freq = torch.from_numpy(np.array(params['class_inv_freq'], dtype=np.float32)).to(params['device']) + class_inv_freq = torch.from_numpy( + np.array(params["class_inv_freq"], dtype=np.float32) + ).to(params["device"]) class_inv_freq = class_inv_freq.unsqueeze(0).unsqueeze(2).unsqueeze(2) - print('\nEpoch', epoch) + print("\nEpoch", epoch) for batch_idx, inputs in enumerate(data_loader): - data = inputs['spec'].to(params['device']) - gt_det = inputs['y_2d_det'].to(params['device']) - gt_size = inputs['y_2d_size'].to(params['device']) - gt_class = inputs['y_2d_classes'].to(params['device']) + data = inputs["spec"].to(params["device"]) + gt_det = inputs["y_2d_det"].to(params["device"]) + gt_size = inputs["y_2d_size"].to(params["device"]) + gt_class = inputs["y_2d_classes"].to(params["device"]) optimizer.zero_grad() outputs = model(data) - loss = loss_fun(outputs, gt_det, gt_size, gt_class, det_criterion, params, class_inv_freq) + loss = loss_fun( + outputs, + gt_det, + gt_size, + gt_class, + det_criterion, + params, + class_inv_freq, + ) train_loss.update(loss.item(), data.shape[0]) loss.backward() @@ -104,13 +150,18 @@ def train(model, epoch, data_loader, det_criterion, optimizer, scheduler, params scheduler.step() if batch_idx % 50 == 0 and batch_idx != 0: - print('[{}/{}]\tLoss: {:.4f}'.format( - batch_idx * len(data), len(data_loader.dataset), train_loss.avg)) + print( + "[{}/{}]\tLoss: {:.4f}".format( + batch_idx * len(data), + len(data_loader.dataset), + train_loss.avg, + ) + ) - print('Train loss : {:.4f}'.format(train_loss.avg)) + print("Train loss : {:.4f}".format(train_loss.avg)) res = {} - res['train_loss'] = float(train_loss.avg) + res["train_loss"] = float(train_loss.avg) return res @@ -120,16 +171,18 @@ def test(model, epoch, data_loader, det_criterion, params): ground_truths = [] test_loss = tu.AverageMeter() - class_inv_freq = torch.from_numpy(np.array(params['class_inv_freq'], dtype=np.float32)).to(params['device']) + class_inv_freq = torch.from_numpy( + np.array(params["class_inv_freq"], dtype=np.float32) + ).to(params["device"]) class_inv_freq = class_inv_freq.unsqueeze(0).unsqueeze(2).unsqueeze(2) with torch.no_grad(): for batch_idx, inputs in enumerate(data_loader): - data = inputs['spec'].to(params['device']) - gt_det = inputs['y_2d_det'].to(params['device']) - gt_size = inputs['y_2d_size'].to(params['device']) - gt_class = inputs['y_2d_classes'].to(params['device']) + data = inputs["spec"].to(params["device"]) + gt_det = inputs["y_2d_det"].to(params["device"]) + gt_size = inputs["y_2d_size"].to(params["device"]) + gt_class = inputs["y_2d_classes"].to(params["device"]) outputs = model(data) @@ -139,41 +192,79 @@ def test(model, epoch, data_loader, det_criterion, params): # for kk in ['pred_det', 'pred_size', 'pred_class']: # outputs[kk] = torch.cat([oo for oo in outputs[kk]], 2).unsqueeze(0) - if params['save_test_image_during_train'] and batch_idx == 0: + if params["save_test_image_during_train"] and batch_idx == 0: # for visualization - save the first prediction ind = 0 - orig_index = inputs['file_id'][ind] - plot_title = data_loader.dataset.data_anns[orig_index]['id'] - op_file_name = params['op_im_dir'] + str(orig_index.item()).zfill(4) + '_' + str(epoch).zfill(4) + '_pred.jpg' - save_image(data, outputs, ind, inputs, params, op_file_name, plot_title) + orig_index = inputs["file_id"][ind] + plot_title = data_loader.dataset.data_anns[orig_index]["id"] + op_file_name = ( + params["op_im_dir"] + + str(orig_index.item()).zfill(4) + + "_" + + str(epoch).zfill(4) + + "_pred.jpg" + ) + save_image( + data, + outputs, + ind, + inputs, + params, + op_file_name, + plot_title, + ) - loss = loss_fun(outputs, gt_det, gt_size, gt_class, det_criterion, params, class_inv_freq) + loss = loss_fun( + outputs, + gt_det, + gt_size, + gt_class, + det_criterion, + params, + class_inv_freq, + ) test_loss.update(loss.item(), data.shape[0]) # do NMS - pred_nms, _ = pp.run_nms(outputs, params, inputs['sampling_rate'].float()) + pred_nms, _ = pp.run_nms( + outputs, params, inputs["sampling_rate"].float() + ) predictions.extend(pred_nms) ground_truths.extend(parse_gt_data(inputs)) - res_det = evl.evaluate_predictions(ground_truths, predictions, params['class_names'], - params['detection_overlap'], params['ignore_start_end']) + res_det = evl.evaluate_predictions( + ground_truths, + predictions, + params["class_names"], + params["detection_overlap"], + params["ignore_start_end"], + ) - print('\nTest loss : {:.4f}'.format(test_loss.avg)) - print('Rec at 0.95 (det) : {:.4f}'.format(res_det['rec_at_x'])) - print('Avg prec (cls) : {:.4f}'.format(res_det['avg_prec'])) - print('File acc (cls) : {:.2f} - for {} out of {}'.format(res_det['file_acc'], - res_det['num_valid_files'], res_det['num_total_files'])) - print('Cls Avg prec (cls) : {:.4f}'.format(res_det['avg_prec_class'])) + print("\nTest loss : {:.4f}".format(test_loss.avg)) + print("Rec at 0.95 (det) : {:.4f}".format(res_det["rec_at_x"])) + print("Avg prec (cls) : {:.4f}".format(res_det["avg_prec"])) + print( + "File acc (cls) : {:.2f} - for {} out of {}".format( + res_det["file_acc"], + res_det["num_valid_files"], + res_det["num_total_files"], + ) + ) + print("Cls Avg prec (cls) : {:.4f}".format(res_det["avg_prec_class"])) - print('\nPer class average precision') - str_len = np.max([len(rs['name']) for rs in res_det['class_pr']]) + 5 - for cc, rs in enumerate(res_det['class_pr']): - if rs['num_gt'] > 0: - print(str(cc).ljust(5) + rs['name'].ljust(str_len) + '{:.4f}'.format(rs['avg_prec'])) + print("\nPer class average precision") + str_len = np.max([len(rs["name"]) for rs in res_det["class_pr"]]) + 5 + for cc, rs in enumerate(res_det["class_pr"]): + if rs["num_gt"] > 0: + print( + str(cc).ljust(5) + + rs["name"].ljust(str_len) + + "{:.4f}".format(rs["avg_prec"]) + ) res = {} - res['test_loss'] = float(test_loss.avg) + res["test_loss"] = float(test_loss.avg) return res_det, res @@ -181,176 +272,288 @@ def test(model, epoch, data_loader, det_criterion, params): def parse_gt_data(inputs): # reads the torch arrays into a dictionary of numpy arrays, taking care to # remove padding data i.e. not valid ones - keys = ['start_times', 'end_times', 'low_freqs', 'high_freqs', 'class_ids', 'individual_ids'] + keys = [ + "start_times", + "end_times", + "low_freqs", + "high_freqs", + "class_ids", + "individual_ids", + ] batch_data = [] - for ind in range(inputs['start_times'].shape[0]): - is_valid = inputs['is_valid'][ind]==1 + for ind in range(inputs["start_times"].shape[0]): + is_valid = inputs["is_valid"][ind] == 1 gt = {} for kk in keys: gt[kk] = inputs[kk][ind][is_valid].numpy().astype(np.float32) - gt['duration'] = inputs['duration'][ind].item() - gt['file_id'] = inputs['file_id'][ind].item() - gt['class_id_file'] = inputs['class_id_file'][ind].item() + gt["duration"] = inputs["duration"][ind].item() + gt["file_id"] = inputs["file_id"][ind].item() + gt["class_id_file"] = inputs["class_id_file"][ind].item() batch_data.append(gt) return batch_data def select_model(params): - num_classes = len(params['class_names']) - if params['model_name'] == 'Net2DFast': - model = models.Net2DFast(params['num_filters'], num_classes=num_classes, - emb_dim=params['emb_dim'], ip_height=params['ip_height'], - resize_factor=params['resize_factor']) - elif params['model_name'] == 'Net2DFastNoAttn': - model = models.Net2DFastNoAttn(params['num_filters'], num_classes=num_classes, - emb_dim=params['emb_dim'], ip_height=params['ip_height'], - resize_factor=params['resize_factor']) - elif params['model_name'] == 'Net2DFastNoCoordConv': - model = models.Net2DFastNoCoordConv(params['num_filters'], num_classes=num_classes, - emb_dim=params['emb_dim'], ip_height=params['ip_height'], - resize_factor=params['resize_factor']) + num_classes = len(params["class_names"]) + if params["model_name"] == "Net2DFast": + model = models.Net2DFast( + params["num_filters"], + num_classes=num_classes, + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + elif params["model_name"] == "Net2DFastNoAttn": + model = models.Net2DFastNoAttn( + params["num_filters"], + num_classes=num_classes, + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + elif params["model_name"] == "Net2DFastNoCoordConv": + model = models.Net2DFastNoCoordConv( + params["num_filters"], + num_classes=num_classes, + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) else: - print('No valid network specified') + print("No valid network specified") return model if __name__ == "__main__": - plt.close('all') + plt.close("all") params = parameters.get_params(True) if torch.cuda.is_available(): - params['device'] = 'cuda' + params["device"] = "cuda" else: - params['device'] = 'cpu' + params["device"] = "cpu" # setup arg parser and populate it with exiting parameters - will not work with lists parser = argparse.ArgumentParser() - parser.add_argument('data_dir', type=str, - help='Path to root of datasets') - parser.add_argument('ann_dir', type=str, - help='Path to extracted annotations') - parser.add_argument('--train_split', type=str, default='diff', # diff, same - help='Which train split to use') - parser.add_argument('--notes', type=str, default='', - help='Notes to save in text file') - parser.add_argument('--do_not_save_images', action='store_false', - help='Do not save images at the end of training') - parser.add_argument('--standardize_classs_names_ip', type=str, - default='Rhinolophus ferrumequinum;Rhinolophus hipposideros', - help='Will set low and high frequency the same for these classes. Separate names with ";"') + parser.add_argument("data_dir", type=str, help="Path to root of datasets") + parser.add_argument( + "ann_dir", type=str, help="Path to extracted annotations" + ) + parser.add_argument( + "--train_split", + type=str, + default="diff", # diff, same + help="Which train split to use", + ) + parser.add_argument( + "--notes", type=str, default="", help="Notes to save in text file" + ) + parser.add_argument( + "--do_not_save_images", + action="store_false", + help="Do not save images at the end of training", + ) + parser.add_argument( + "--standardize_classs_names_ip", + type=str, + default="Rhinolophus ferrumequinum;Rhinolophus hipposideros", + help='Will set low and high frequency the same for these classes. Separate names with ";"', + ) for key, val in params.items(): - parser.add_argument('--'+key, type=type(val), default=val) + parser.add_argument("--" + key, type=type(val), default=val) params = vars(parser.parse_args()) # save notes file - if params['notes'] != '': - tu.write_notes_file(params['experiment'] + 'notes.txt', params['notes']) + if params["notes"] != "": + tu.write_notes_file( + params["experiment"] + "notes.txt", params["notes"] + ) # load the training and test meta data - there are different splits defined - train_sets, test_sets = ts.get_train_test_data(params['ann_dir'], params['data_dir'], params['train_split']) - train_sets_no_path, test_sets_no_path = ts.get_train_test_data('', '', params['train_split']) + train_sets, test_sets = ts.get_train_test_data( + params["ann_dir"], params["data_dir"], params["train_split"] + ) + train_sets_no_path, test_sets_no_path = ts.get_train_test_data( + "", "", params["train_split"] + ) # keep track of what we have trained on - params['train_sets'] = train_sets_no_path - params['test_sets'] = test_sets_no_path + params["train_sets"] = train_sets_no_path + params["test_sets"] = test_sets_no_path # load train annotations - merge them all together - print('\nTraining on:') + print("\nTraining on:") for tt in train_sets: - print(tt['ann_path']) - classes_to_ignore = params['classes_to_ignore']+params['generic_class'] - data_train, params['class_names'], params['class_inv_freq'] = \ - tu.load_set_of_anns(train_sets, classes_to_ignore, params['events_of_interest'], params['convert_to_genus']) - params['genus_names'], params['genus_mapping'] = tu.get_genus_mapping(params['class_names']) - params['class_names_short'] = tu.get_short_class_names(params['class_names']) + print(tt["ann_path"]) + classes_to_ignore = params["classes_to_ignore"] + params["generic_class"] + ( + data_train, + params["class_names"], + params["class_inv_freq"], + ) = tu.load_set_of_anns( + train_sets, + classes_to_ignore, + params["events_of_interest"], + params["convert_to_genus"], + ) + params["genus_names"], params["genus_mapping"] = tu.get_genus_mapping( + params["class_names"] + ) + params["class_names_short"] = tu.get_short_class_names( + params["class_names"] + ) # standardize the low and high frequency value for specified classes - params['standardize_classs_names'] = params['standardize_classs_names_ip'].split(';') - for cc in params['standardize_classs_names']: - if cc in params['class_names']: + params["standardize_classs_names"] = params[ + "standardize_classs_names_ip" + ].split(";") + for cc in params["standardize_classs_names"]: + if cc in params["class_names"]: data_train = tu.standardize_low_freq(data_train, cc) else: - print(cc, 'not found') + print(cc, "not found") # train loader train_dataset = adl.AudioLoader(data_train, params, is_train=True) - train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=params['batch_size'], - shuffle=True, num_workers=params['num_workers'], pin_memory=True) - + train_loader = torch.utils.data.DataLoader( + train_dataset, + batch_size=params["batch_size"], + shuffle=True, + num_workers=params["num_workers"], + pin_memory=True, + ) # test set - print('\nTesting on:') + print("\nTesting on:") for tt in test_sets: - print(tt['ann_path']) - data_test, _, _ = tu.load_set_of_anns(test_sets, classes_to_ignore, params['events_of_interest'], params['convert_to_genus']) + print(tt["ann_path"]) + data_test, _, _ = tu.load_set_of_anns( + test_sets, + classes_to_ignore, + params["events_of_interest"], + params["convert_to_genus"], + ) data_train = tu.remove_dupes(data_train, data_test) test_dataset = adl.AudioLoader(data_test, params, is_train=False) # batch size of 1 because of variable file length - test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1, - shuffle=False, num_workers=params['num_workers'], pin_memory=True) - + test_loader = torch.utils.data.DataLoader( + test_dataset, + batch_size=1, + shuffle=False, + num_workers=params["num_workers"], + pin_memory=True, + ) inputs_train = next(iter(train_loader)) # TODO remove params['ip_height'], this is just legacy - params['ip_height'] = int(params['spec_height']*params['resize_factor']) - print('\ntrain batch spec size :', inputs_train['spec'].shape) - print('class target size :', inputs_train['y_2d_classes'].shape) + params["ip_height"] = int(params["spec_height"] * params["resize_factor"]) + print("\ntrain batch spec size :", inputs_train["spec"].shape) + print("class target size :", inputs_train["y_2d_classes"].shape) # select network model = select_model(params) - model = model.to(params['device']) + model = model.to(params["device"]) - optimizer = torch.optim.Adam(model.parameters(), lr=params['lr']) - #optimizer = torch.optim.SGD(model.parameters(), lr=params['lr'], momentum=0.9) - scheduler = CosineAnnealingLR(optimizer, params['num_epochs'] * len(train_loader)) - if params['train_loss'] == 'mse': + optimizer = torch.optim.Adam(model.parameters(), lr=params["lr"]) + # optimizer = torch.optim.SGD(model.parameters(), lr=params['lr'], momentum=0.9) + scheduler = CosineAnnealingLR( + optimizer, params["num_epochs"] * len(train_loader) + ) + if params["train_loss"] == "mse": det_criterion = losses.mse_loss - elif params['train_loss'] == 'focal': + elif params["train_loss"] == "focal": det_criterion = losses.focal_loss # save parameters to file - with open(params['experiment'] + 'params.json', 'w') as da: + with open(params["experiment"] + "params.json", "w") as da: json.dump(params, da, indent=2, sort_keys=True) # plotting - train_plt_ls = pu.LossPlotter(params['experiment'] + 'train_loss.png', params['num_epochs']+1, - ['train_loss'], None, None, ['epoch', 'train_loss'], logy=True) - test_plt_ls = pu.LossPlotter(params['experiment'] + 'test_loss.png', params['num_epochs']+1, - ['test_loss'], None, None, ['epoch', 'test_loss'], logy=True) - test_plt = pu.LossPlotter(params['experiment'] + 'test.png', params['num_epochs']+1, - ['avg_prec', 'rec_at_x', 'avg_prec_class', 'file_acc', 'top_class'], [0,1], None, ['epoch', '']) - test_plt_class = pu.LossPlotter(params['experiment'] + 'test_avg_prec.png', params['num_epochs']+1, - params['class_names_short'], [0,1], params['class_names_short'], ['epoch', 'avg_prec']) - + train_plt_ls = pu.LossPlotter( + params["experiment"] + "train_loss.png", + params["num_epochs"] + 1, + ["train_loss"], + None, + None, + ["epoch", "train_loss"], + logy=True, + ) + test_plt_ls = pu.LossPlotter( + params["experiment"] + "test_loss.png", + params["num_epochs"] + 1, + ["test_loss"], + None, + None, + ["epoch", "test_loss"], + logy=True, + ) + test_plt = pu.LossPlotter( + params["experiment"] + "test.png", + params["num_epochs"] + 1, + ["avg_prec", "rec_at_x", "avg_prec_class", "file_acc", "top_class"], + [0, 1], + None, + ["epoch", ""], + ) + test_plt_class = pu.LossPlotter( + params["experiment"] + "test_avg_prec.png", + params["num_epochs"] + 1, + params["class_names_short"], + [0, 1], + params["class_names_short"], + ["epoch", "avg_prec"], + ) # # main train loop - for epoch in range(0, params['num_epochs']+1): + for epoch in range(0, params["num_epochs"] + 1): - train_loss = train(model, epoch, train_loader, det_criterion, optimizer, scheduler, params) - train_plt_ls.update_and_save(epoch, [train_loss['train_loss']]) + train_loss = train( + model, + epoch, + train_loader, + det_criterion, + optimizer, + scheduler, + params, + ) + train_plt_ls.update_and_save(epoch, [train_loss["train_loss"]]) - if epoch % params['num_eval_epochs'] == 0: + if epoch % params["num_eval_epochs"] == 0: # detection accuracy on test set - test_res, test_loss = test(model, epoch, test_loader, det_criterion, params) - test_plt_ls.update_and_save(epoch, [test_loss['test_loss']]) - test_plt.update_and_save(epoch, [test_res['avg_prec'], test_res['rec_at_x'], - test_res['avg_prec_class'], test_res['file_acc'], test_res['top_class']['avg_prec']]) - test_plt_class.update_and_save(epoch, [rs['avg_prec'] for rs in test_res['class_pr']]) - pu.plot_pr_curve_class(params['experiment'] , 'test_pr', 'test_pr', test_res) - + test_res, test_loss = test( + model, epoch, test_loader, det_criterion, params + ) + test_plt_ls.update_and_save(epoch, [test_loss["test_loss"]]) + test_plt.update_and_save( + epoch, + [ + test_res["avg_prec"], + test_res["rec_at_x"], + test_res["avg_prec_class"], + test_res["file_acc"], + test_res["top_class"]["avg_prec"], + ], + ) + test_plt_class.update_and_save( + epoch, [rs["avg_prec"] for rs in test_res["class_pr"]] + ) + pu.plot_pr_curve_class( + params["experiment"], "test_pr", "test_pr", test_res + ) # save trained model - print('saving model to: ' + params['model_file_name']) - op_state = {'epoch': epoch + 1, - 'state_dict': model.state_dict(), - #'optimizer' : optimizer.state_dict(), - 'params' : params} - torch.save(op_state, params['model_file_name']) - + print("saving model to: " + params["model_file_name"]) + op_state = { + "epoch": epoch + 1, + "state_dict": model.state_dict(), + #'optimizer' : optimizer.state_dict(), + "params": params, + } + torch.save(op_state, params["model_file_name"]) # save an image with associated prediction for each batch in the test set - if not args['do_not_save_images']: + if not args["do_not_save_images"]: save_images_batch(model, test_loader, params) diff --git a/bat_detect/train/train_split.py b/bat_detect/train/train_split.py index 20972bd..01b5c03 100644 --- a/bat_detect/train/train_split.py +++ b/bat_detect/train/train_split.py @@ -2,13 +2,14 @@ Run scripts/extract_anns.py to generate these json files. """ + def get_train_test_data(ann_dir, wav_dir, split_name, load_extra=True): - if split_name == 'diff': + if split_name == "diff": train_sets, test_sets = split_diff(ann_dir, wav_dir, load_extra) - elif split_name == 'same': + elif split_name == "same": train_sets, test_sets = split_same(ann_dir, wav_dir, load_extra) else: - print('Split not defined') + print("Split not defined") assert False return train_sets, test_sets @@ -18,73 +19,126 @@ def split_diff(ann_dir, wav_dir, load_extra=True): train_sets = [] if load_extra: - train_sets.append({'dataset_name': 'BatDetective', - 'is_test': False, - 'is_binary': True, # just a bat / not bat dataset ie no classes - 'ann_path': ann_dir + 'train_set_bulgaria_batdetective_with_bbs.json', - 'wav_path': wav_dir + 'bat_detective/audio/'}) - train_sets.append({'dataset_name': 'bat_logger_qeop_empty', - 'is_test': False, - 'is_binary': True, - 'ann_path': ann_dir + 'bat_logger_qeop_empty.json', - 'wav_path': wav_dir + 'bat_logger_qeop_empty/audio/'}) - train_sets.append({'dataset_name': 'bat_logger_2016_empty', - 'is_test': False, - 'is_binary': True, - 'ann_path': ann_dir + 'train_set_bat_logger_2016_empty.json', - 'wav_path': wav_dir + 'bat_logger_2016/audio/'}) + train_sets.append( + { + "dataset_name": "BatDetective", + "is_test": False, + "is_binary": True, # just a bat / not bat dataset ie no classes + "ann_path": ann_dir + + "train_set_bulgaria_batdetective_with_bbs.json", + "wav_path": wav_dir + "bat_detective/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_logger_qeop_empty", + "is_test": False, + "is_binary": True, + "ann_path": ann_dir + "bat_logger_qeop_empty.json", + "wav_path": wav_dir + "bat_logger_qeop_empty/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_logger_2016_empty", + "is_test": False, + "is_binary": True, + "ann_path": ann_dir + "train_set_bat_logger_2016_empty.json", + "wav_path": wav_dir + "bat_logger_2016/audio/", + } + ) # train_sets.append({'dataset_name': 'brazil_data_binary', # 'is_test': False, # 'ann_path': ann_dir + 'brazil_data_binary.json', # 'wav_path': wav_dir + 'brazil_data/audio/'}) - train_sets.append({'dataset_name': 'echobank', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'Echobank_train_expert.json', - 'wav_path': wav_dir + 'echobank/audio/'}) - train_sets.append({'dataset_name': 'sn_scot_nor', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'sn_scot_nor_0.5_expert.json', - 'wav_path': wav_dir + 'sn_scot_nor/audio/'}) - train_sets.append({'dataset_name': 'BCT_1_sec', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'BCT_1_sec_train_expert.json', - 'wav_path': wav_dir + 'BCT_1_sec/audio/'}) - train_sets.append({'dataset_name': 'bcireland', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'bcireland_expert.json', - 'wav_path': wav_dir + 'bcireland/audio/'}) - train_sets.append({'dataset_name': 'rhinolophus_steve_BCT', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'rhinolophus_steve_BCT_expert.json', - 'wav_path': wav_dir + 'rhinolophus_steve_BCT/audio/'}) + train_sets.append( + { + "dataset_name": "echobank", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "Echobank_train_expert.json", + "wav_path": wav_dir + "echobank/audio/", + } + ) + train_sets.append( + { + "dataset_name": "sn_scot_nor", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "sn_scot_nor_0.5_expert.json", + "wav_path": wav_dir + "sn_scot_nor/audio/", + } + ) + train_sets.append( + { + "dataset_name": "BCT_1_sec", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "BCT_1_sec_train_expert.json", + "wav_path": wav_dir + "BCT_1_sec/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bcireland", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "bcireland_expert.json", + "wav_path": wav_dir + "bcireland/audio/", + } + ) + train_sets.append( + { + "dataset_name": "rhinolophus_steve_BCT", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "rhinolophus_steve_BCT_expert.json", + "wav_path": wav_dir + "rhinolophus_steve_BCT/audio/", + } + ) test_sets = [] - test_sets.append({'dataset_name': 'bat_data_martyn_2018', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2018_1_sec_train_expert.json', - 'wav_path': wav_dir + 'bat_data_martyn_2018/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2018_test', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2018_1_sec_test_expert.json', - 'wav_path': wav_dir + 'bat_data_martyn_2018_test/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2019', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2019_1_sec_train_expert.json', - 'wav_path': wav_dir + 'bat_data_martyn_2019/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2019_test', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2019_1_sec_test_expert.json', - 'wav_path': wav_dir + 'bat_data_martyn_2019_test/audio/'}) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2018", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2018_1_sec_train_expert.json", + "wav_path": wav_dir + "bat_data_martyn_2018/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2018_test", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2018_1_sec_test_expert.json", + "wav_path": wav_dir + "bat_data_martyn_2018_test/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2019", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2019_1_sec_train_expert.json", + "wav_path": wav_dir + "bat_data_martyn_2019/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2019_test", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2019_1_sec_test_expert.json", + "wav_path": wav_dir + "bat_data_martyn_2019_test/audio/", + } + ) return train_sets, test_sets @@ -93,71 +147,124 @@ def split_same(ann_dir, wav_dir, load_extra=True): train_sets = [] if load_extra: - train_sets.append({'dataset_name': 'BatDetective', - 'is_test': False, - 'is_binary': True, - 'ann_path': ann_dir + 'train_set_bulgaria_batdetective_with_bbs.json', - 'wav_path': wav_dir + 'bat_detective/audio/'}) - train_sets.append({'dataset_name': 'bat_logger_qeop_empty', - 'is_test': False, - 'is_binary': True, - 'ann_path': ann_dir + 'bat_logger_qeop_empty.json', - 'wav_path': wav_dir + 'bat_logger_qeop_empty/audio/'}) - train_sets.append({'dataset_name': 'bat_logger_2016_empty', - 'is_test': False, - 'is_binary': True, - 'ann_path': ann_dir + 'train_set_bat_logger_2016_empty.json', - 'wav_path': wav_dir + 'bat_logger_2016/audio/'}) + train_sets.append( + { + "dataset_name": "BatDetective", + "is_test": False, + "is_binary": True, + "ann_path": ann_dir + + "train_set_bulgaria_batdetective_with_bbs.json", + "wav_path": wav_dir + "bat_detective/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_logger_qeop_empty", + "is_test": False, + "is_binary": True, + "ann_path": ann_dir + "bat_logger_qeop_empty.json", + "wav_path": wav_dir + "bat_logger_qeop_empty/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_logger_2016_empty", + "is_test": False, + "is_binary": True, + "ann_path": ann_dir + "train_set_bat_logger_2016_empty.json", + "wav_path": wav_dir + "bat_logger_2016/audio/", + } + ) # train_sets.append({'dataset_name': 'brazil_data_binary', # 'is_test': False, # 'ann_path': ann_dir + 'brazil_data_binary.json', # 'wav_path': wav_dir + 'brazil_data/audio/'}) - train_sets.append({'dataset_name': 'echobank', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'Echobank_train_expert_TRAIN.json', - 'wav_path': wav_dir + 'echobank/audio/'}) - train_sets.append({'dataset_name': 'sn_scot_nor', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'sn_scot_nor_0.5_expert_TRAIN.json', - 'wav_path': wav_dir + 'sn_scot_nor/audio/'}) - train_sets.append({'dataset_name': 'BCT_1_sec', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'BCT_1_sec_train_expert_TRAIN.json', - 'wav_path': wav_dir + 'BCT_1_sec/audio/'}) - train_sets.append({'dataset_name': 'bcireland', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'bcireland_expert_TRAIN.json', - 'wav_path': wav_dir + 'bcireland/audio/'}) - train_sets.append({'dataset_name': 'rhinolophus_steve_BCT', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'rhinolophus_steve_BCT_expert_TRAIN.json', - 'wav_path': wav_dir + 'rhinolophus_steve_BCT/audio/'}) - train_sets.append({'dataset_name': 'bat_data_martyn_2018', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2018_1_sec_train_expert_TRAIN.json', - 'wav_path': wav_dir + 'bat_data_martyn_2018/audio/'}) - train_sets.append({'dataset_name': 'bat_data_martyn_2018_test', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2018_1_sec_test_expert_TRAIN.json', - 'wav_path': wav_dir + 'bat_data_martyn_2018_test/audio/'}) - train_sets.append({'dataset_name': 'bat_data_martyn_2019', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2019_1_sec_train_expert_TRAIN.json', - 'wav_path': wav_dir + 'bat_data_martyn_2019/audio/'}) - train_sets.append({'dataset_name': 'bat_data_martyn_2019_test', - 'is_test': False, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2019_1_sec_test_expert_TRAIN.json', - 'wav_path': wav_dir + 'bat_data_martyn_2019_test/audio/'}) + train_sets.append( + { + "dataset_name": "echobank", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "Echobank_train_expert_TRAIN.json", + "wav_path": wav_dir + "echobank/audio/", + } + ) + train_sets.append( + { + "dataset_name": "sn_scot_nor", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "sn_scot_nor_0.5_expert_TRAIN.json", + "wav_path": wav_dir + "sn_scot_nor/audio/", + } + ) + train_sets.append( + { + "dataset_name": "BCT_1_sec", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "BCT_1_sec_train_expert_TRAIN.json", + "wav_path": wav_dir + "BCT_1_sec/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bcireland", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "bcireland_expert_TRAIN.json", + "wav_path": wav_dir + "bcireland/audio/", + } + ) + train_sets.append( + { + "dataset_name": "rhinolophus_steve_BCT", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + "rhinolophus_steve_BCT_expert_TRAIN.json", + "wav_path": wav_dir + "rhinolophus_steve_BCT/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_data_martyn_2018", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2018_1_sec_train_expert_TRAIN.json", + "wav_path": wav_dir + "bat_data_martyn_2018/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_data_martyn_2018_test", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2018_1_sec_test_expert_TRAIN.json", + "wav_path": wav_dir + "bat_data_martyn_2018_test/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_data_martyn_2019", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2019_1_sec_train_expert_TRAIN.json", + "wav_path": wav_dir + "bat_data_martyn_2019/audio/", + } + ) + train_sets.append( + { + "dataset_name": "bat_data_martyn_2019_test", + "is_test": False, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2019_1_sec_test_expert_TRAIN.json", + "wav_path": wav_dir + "bat_data_martyn_2019_test/audio/", + } + ) # train_sets.append({'dataset_name': 'bat_data_martyn_2021_train', # 'is_test': False, @@ -171,51 +278,91 @@ def split_same(ann_dir, wav_dir, load_extra=True): # 'wav_path': wav_dir + 'volunteers_2021/audio/'}) test_sets = [] - test_sets.append({'dataset_name': 'echobank', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'Echobank_train_expert_TEST.json', - 'wav_path': wav_dir + 'echobank/audio/'}) - test_sets.append({'dataset_name': 'sn_scot_nor', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'sn_scot_nor_0.5_expert_TEST.json', - 'wav_path': wav_dir + 'sn_scot_nor/audio/'}) - test_sets.append({'dataset_name': 'BCT_1_sec', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BCT_1_sec_train_expert_TEST.json', - 'wav_path': wav_dir + 'BCT_1_sec/audio/'}) - test_sets.append({'dataset_name': 'bcireland', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'bcireland_expert_TEST.json', - 'wav_path': wav_dir + 'bcireland/audio/'}) - test_sets.append({'dataset_name': 'rhinolophus_steve_BCT', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'rhinolophus_steve_BCT_expert_TEST.json', - 'wav_path': wav_dir + 'rhinolophus_steve_BCT/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2018', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2018_1_sec_train_expert_TEST.json', - 'wav_path': wav_dir + 'bat_data_martyn_2018/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2018_test', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2018_1_sec_test_expert_TEST.json', - 'wav_path': wav_dir + 'bat_data_martyn_2018_test/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2019', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2019_1_sec_train_expert_TEST.json', - 'wav_path': wav_dir + 'bat_data_martyn_2019/audio/'}) - test_sets.append({'dataset_name': 'bat_data_martyn_2019_test', - 'is_test': True, - 'is_binary': False, - 'ann_path': ann_dir + 'BritishBatCalls_MartynCooke_2019_1_sec_test_expert_TEST.json', - 'wav_path': wav_dir + 'bat_data_martyn_2019_test/audio/'}) + test_sets.append( + { + "dataset_name": "echobank", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + "Echobank_train_expert_TEST.json", + "wav_path": wav_dir + "echobank/audio/", + } + ) + test_sets.append( + { + "dataset_name": "sn_scot_nor", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + "sn_scot_nor_0.5_expert_TEST.json", + "wav_path": wav_dir + "sn_scot_nor/audio/", + } + ) + test_sets.append( + { + "dataset_name": "BCT_1_sec", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + "BCT_1_sec_train_expert_TEST.json", + "wav_path": wav_dir + "BCT_1_sec/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bcireland", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + "bcireland_expert_TEST.json", + "wav_path": wav_dir + "bcireland/audio/", + } + ) + test_sets.append( + { + "dataset_name": "rhinolophus_steve_BCT", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + "rhinolophus_steve_BCT_expert_TEST.json", + "wav_path": wav_dir + "rhinolophus_steve_BCT/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2018", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2018_1_sec_train_expert_TEST.json", + "wav_path": wav_dir + "bat_data_martyn_2018/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2018_test", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2018_1_sec_test_expert_TEST.json", + "wav_path": wav_dir + "bat_data_martyn_2018_test/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2019", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2019_1_sec_train_expert_TEST.json", + "wav_path": wav_dir + "bat_data_martyn_2019/audio/", + } + ) + test_sets.append( + { + "dataset_name": "bat_data_martyn_2019_test", + "is_test": True, + "is_binary": False, + "ann_path": ann_dir + + "BritishBatCalls_MartynCooke_2019_1_sec_test_expert_TEST.json", + "wav_path": wav_dir + "bat_data_martyn_2019_test/audio/", + } + ) # test_sets.append({'dataset_name': 'bat_data_martyn_2021_test', # 'is_test': True, diff --git a/bat_detect/train/train_utils.py b/bat_detect/train/train_utils.py index cff92e4..62441a7 100644 --- a/bat_detect/train/train_utils.py +++ b/bat_detect/train/train_utils.py @@ -1,42 +1,52 @@ -import numpy as np -import random -import os import glob import json +import os +import random + +import numpy as np def write_notes_file(file_name, text): - with open(file_name, 'a') as da: - da.write(text + '\n') + with open(file_name, "a") as da: + da.write(text + "\n") def get_blank_dataset_dict(dataset_name, is_test, ann_path, wav_path): - ddict = {'dataset_name': dataset_name, 'is_test': is_test, 'is_binary': False, - 'ann_path': ann_path, 'wav_path': wav_path} + ddict = { + "dataset_name": dataset_name, + "is_test": is_test, + "is_binary": False, + "ann_path": ann_path, + "wav_path": wav_path, + } return ddict def get_short_class_names(class_names, str_len=3): class_names_short = [] for cc in class_names: - class_names_short.append(' '.join([sp[:str_len] for sp in cc.split(' ')])) + class_names_short.append( + " ".join([sp[:str_len] for sp in cc.split(" ")]) + ) return class_names_short def remove_dupes(data_train, data_test): - test_ids = [dd['id'] for dd in data_test] + test_ids = [dd["id"] for dd in data_test] data_train_prune = [] for aa in data_train: - if aa['id'] not in test_ids: + if aa["id"] not in test_ids: data_train_prune.append(aa) diff = len(data_train) - len(data_train_prune) if diff != 0: - print(diff, 'items removed from train set') + print(diff, "items removed from train set") return data_train_prune def get_genus_mapping(class_names): - genus_names, genus_mapping = np.unique([cc.split(' ')[0] for cc in class_names], return_inverse=True) + genus_names, genus_mapping = np.unique( + [cc.split(" ")[0] for cc in class_names], return_inverse=True + ) return genus_names.tolist(), genus_mapping.tolist() @@ -47,97 +57,110 @@ def standardize_low_freq(data, class_of_interest): low_freqs = [] high_freqs = [] for dd in data: - for aa in dd['annotation']: - if aa['class'] == class_of_interest: - low_freqs.append(aa['low_freq']) - high_freqs.append(aa['high_freq']) + for aa in dd["annotation"]: + if aa["class"] == class_of_interest: + low_freqs.append(aa["low_freq"]) + high_freqs.append(aa["high_freq"]) low_mean = np.mean(low_freqs) high_mean = np.mean(high_freqs) - assert(low_mean < high_mean) + assert low_mean < high_mean - print('\nStandardizing low and high frequency for:') + print("\nStandardizing low and high frequency for:") print(class_of_interest) - print('low: ', round(low_mean, 2)) - print('high: ', round(high_mean, 2)) + print("low: ", round(low_mean, 2)) + print("high: ", round(high_mean, 2)) # only set the low freq, high stays the same # assumes that low_mean < high_mean for dd in data: - for aa in dd['annotation']: - if aa['class'] == class_of_interest: - aa['low_freq'] = low_mean - if aa['high_freq'] < low_mean: - aa['high_freq'] = high_mean + for aa in dd["annotation"]: + if aa["class"] == class_of_interest: + aa["low_freq"] = low_mean + if aa["high_freq"] < low_mean: + aa["high_freq"] = high_mean return data -def load_set_of_anns(data, classes_to_ignore=[], events_of_interest=None, - convert_to_genus=False, verbose=True, list_of_anns=False, - filter_issues=False, name_replace=False): +def load_set_of_anns( + data, + classes_to_ignore=[], + events_of_interest=None, + convert_to_genus=False, + verbose=True, + list_of_anns=False, + filter_issues=False, + name_replace=False, +): # load the annotations anns = [] if list_of_anns: # path to list of individual json files - anns.extend(load_anns_from_path(data['ann_path'], data['wav_path'])) + anns.extend(load_anns_from_path(data["ann_path"], data["wav_path"])) else: # dictionary of datasets for dd in data: - anns.extend(load_anns(dd['ann_path'], dd['wav_path'])) + anns.extend(load_anns(dd["ann_path"], dd["wav_path"])) # discarding unannoated files - anns = [aa for aa in anns if aa['annotated'] is True] + anns = [aa for aa in anns if aa["annotated"] is True] # filter files that have annotation issues - is the input is a dictionary of # datasets, this will lilely have already been done if filter_issues: - anns = [aa for aa in anns if aa['issues'] is False] + anns = [aa for aa in anns if aa["issues"] is False] # check for some basic formatting errors with class names for ann in anns: - for aa in ann['annotation']: - aa['class'] = aa['class'].strip() + for aa in ann["annotation"]: + aa["class"] = aa["class"].strip() # only load specified events - i.e. types of calls if events_of_interest is not None: for ann in anns: filtered_events = [] - for aa in ann['annotation']: - if aa['event'] in events_of_interest: + for aa in ann["annotation"]: + if aa["event"] in events_of_interest: filtered_events.append(aa) - ann['annotation'] = filtered_events + ann["annotation"] = filtered_events # change class names # replace_names will be a dictionary mapping input name to output if type(name_replace) is dict: for ann in anns: - for aa in ann['annotation']: - if aa['class'] in name_replace: - aa['class'] = name_replace[aa['class']] + for aa in ann["annotation"]: + if aa["class"] in name_replace: + aa["class"] = name_replace[aa["class"]] # convert everything to genus name if convert_to_genus: for ann in anns: - for aa in ann['annotation']: - aa['class'] = aa['class'].split(' ')[0] + for aa in ann["annotation"]: + aa["class"] = aa["class"].split(" ")[0] # get unique class names class_names_all = [] for ann in anns: - for aa in ann['annotation']: - if aa['class'] not in classes_to_ignore: - class_names_all.append(aa['class']) + for aa in ann["annotation"]: + if aa["class"] not in classes_to_ignore: + class_names_all.append(aa["class"]) class_names, class_cnts = np.unique(class_names_all, return_counts=True) - class_inv_freq = (class_cnts.sum() / (len(class_names) * class_cnts.astype(np.float32))) + class_inv_freq = class_cnts.sum() / ( + len(class_names) * class_cnts.astype(np.float32) + ) if verbose: - print('Class count:') + print("Class count:") str_len = np.max([len(cc) for cc in class_names]) + 5 for cc in range(len(class_names)): - print(str(cc).ljust(5) + class_names[cc].ljust(str_len) + str(class_cnts[cc])) + print( + str(cc).ljust(5) + + class_names[cc].ljust(str_len) + + str(class_cnts[cc]) + ) if len(classes_to_ignore) == 0: return anns @@ -150,36 +173,37 @@ def load_anns(ann_file_name, raw_audio_dir): anns = json.load(da) for aa in anns: - aa['file_path'] = raw_audio_dir + aa['id'] + aa["file_path"] = raw_audio_dir + aa["id"] return anns def load_anns_from_path(ann_file_dir, raw_audio_dir): - files = glob.glob(ann_file_dir + '*.json') + files = glob.glob(ann_file_dir + "*.json") anns = [] for ff in files: with open(ff) as da: ann = json.load(da) - ann['file_path'] = raw_audio_dir + ann['id'] + ann["file_path"] = raw_audio_dir + ann["id"] anns.append(ann) return anns class AverageMeter(object): - """Computes and stores the average and current value""" - def __init__(self): - self.reset() + """Computes and stores the average and current value""" - def reset(self): - self.val = 0 - self.avg = 0 - self.sum = 0 - self.count = 0 + def __init__(self): + self.reset() - def update(self, val, n=1): - self.val = val - self.sum += val * n - self.count += n - self.avg = self.sum / self.count + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count diff --git a/bat_detect/utils/audio_utils.py b/bat_detect/utils/audio_utils.py index 4a18d74..3ad648b 100644 --- a/bat_detect/utils/audio_utils.py +++ b/bat_detect/utils/audio_utils.py @@ -1,89 +1,142 @@ -import numpy as np -from . import wavfile import warnings -import torch + import librosa +import numpy as np +import torch + +from . import wavfile def time_to_x_coords(time_in_file, sampling_rate, fft_win_length, fft_overlap): - nfft = np.floor(fft_win_length*sampling_rate) # int() uses floor - noverlap = np.floor(fft_overlap*nfft) - return (time_in_file*sampling_rate-noverlap) / (nfft - noverlap) + nfft = np.floor(fft_win_length * sampling_rate) # int() uses floor + noverlap = np.floor(fft_overlap * nfft) + return (time_in_file * sampling_rate - noverlap) / (nfft - noverlap) # NOTE this is also defined in post_process def x_coords_to_time(x_pos, sampling_rate, fft_win_length, fft_overlap): - nfft = np.floor(fft_win_length*sampling_rate) - noverlap = np.floor(fft_overlap*nfft) - return ((x_pos*(nfft - noverlap)) + noverlap) / sampling_rate - #return (1.0 - fft_overlap) * fft_win_length * (x_pos + 0.5) # 0.5 is for center of temporal window + nfft = np.floor(fft_win_length * sampling_rate) + noverlap = np.floor(fft_overlap * nfft) + return ((x_pos * (nfft - noverlap)) + noverlap) / sampling_rate + # return (1.0 - fft_overlap) * fft_win_length * (x_pos + 0.5) # 0.5 is for center of temporal window -def generate_spectrogram(audio, sampling_rate, params, return_spec_for_viz=False, check_spec_size=True): +def generate_spectrogram( + audio, + sampling_rate, + params, + return_spec_for_viz=False, + check_spec_size=True, +): # generate spectrogram - spec = gen_mag_spectrogram(audio, sampling_rate, params['fft_win_length'], params['fft_overlap']) + spec = gen_mag_spectrogram( + audio, sampling_rate, params["fft_win_length"], params["fft_overlap"] + ) # crop to min/max freq - max_freq = round(params['max_freq']*params['fft_win_length']) - min_freq = round(params['min_freq']*params['fft_win_length']) + max_freq = round(params["max_freq"] * params["fft_win_length"]) + min_freq = round(params["min_freq"] * params["fft_win_length"]) if spec.shape[0] < max_freq: freq_pad = max_freq - spec.shape[0] - spec = np.vstack((np.zeros((freq_pad, spec.shape[1]), dtype=spec.dtype), spec)) - spec_cropped = spec[-max_freq:spec.shape[0]-min_freq, :] + spec = np.vstack( + (np.zeros((freq_pad, spec.shape[1]), dtype=spec.dtype), spec) + ) + spec_cropped = spec[-max_freq : spec.shape[0] - min_freq, :] - if params['spec_scale'] == 'log': - log_scaling = 2.0 * (1.0 / sampling_rate) * (1.0/(np.abs(np.hanning(int(params['fft_win_length']*sampling_rate)))**2).sum()) - #log_scaling = (1.0 / sampling_rate)*0.1 - #log_scaling = (1.0 / sampling_rate)*10e4 - spec = np.log1p(log_scaling*spec_cropped) - elif params['spec_scale'] == 'pcen': + if params["spec_scale"] == "log": + log_scaling = ( + 2.0 + * (1.0 / sampling_rate) + * ( + 1.0 + / ( + np.abs( + np.hanning( + int(params["fft_win_length"] * sampling_rate) + ) + ) + ** 2 + ).sum() + ) + ) + # log_scaling = (1.0 / sampling_rate)*0.1 + # log_scaling = (1.0 / sampling_rate)*10e4 + spec = np.log1p(log_scaling * spec_cropped) + elif params["spec_scale"] == "pcen": spec = pcen(spec_cropped, sampling_rate) - elif params['spec_scale'] == 'none': + elif params["spec_scale"] == "none": pass - if params['denoise_spec_avg']: + if params["denoise_spec_avg"]: spec = spec - np.mean(spec, 1)[:, np.newaxis] spec.clip(min=0, out=spec) - if params['max_scale_spec']: + if params["max_scale_spec"]: spec = spec / (spec.max() + 10e-6) # needs to be divisible by specific factor - if not it should have been padded - #if check_spec_size: - #assert((int(spec.shape[0]*params['resize_factor']) % params['spec_divide_factor']) == 0) - #assert((int(spec.shape[1]*params['resize_factor']) % params['spec_divide_factor']) == 0) + # if check_spec_size: + # assert((int(spec.shape[0]*params['resize_factor']) % params['spec_divide_factor']) == 0) + # assert((int(spec.shape[1]*params['resize_factor']) % params['spec_divide_factor']) == 0) # for visualization purposes - use log scaled spectrogram if return_spec_for_viz: - log_scaling = 2.0 * (1.0 / sampling_rate) * (1.0/(np.abs(np.hanning(int(params['fft_win_length']*sampling_rate)))**2).sum()) - spec_for_viz = np.log1p(log_scaling*spec_cropped).astype(np.float32) + log_scaling = ( + 2.0 + * (1.0 / sampling_rate) + * ( + 1.0 + / ( + np.abs( + np.hanning( + int(params["fft_win_length"] * sampling_rate) + ) + ) + ** 2 + ).sum() + ) + ) + spec_for_viz = np.log1p(log_scaling * spec_cropped).astype(np.float32) else: spec_for_viz = None return spec, spec_for_viz -def load_audio_file(audio_file, time_exp_fact, target_samp_rate, scale=False, max_duration=False): +def load_audio_file( + audio_file, + time_exp_fact, + target_samp_rate, + scale=False, + max_duration=False, +): with warnings.catch_warnings(): - warnings.filterwarnings('ignore', category=wavfile.WavFileWarning) - #sampling_rate, audio_raw = wavfile.read(audio_file) + warnings.filterwarnings("ignore", category=wavfile.WavFileWarning) + # sampling_rate, audio_raw = wavfile.read(audio_file) audio_raw, sampling_rate = librosa.load(audio_file, sr=None) if len(audio_raw.shape) > 1: - raise Exception('Currently does not handle stereo files') + raise Exception("Currently does not handle stereo files") sampling_rate = sampling_rate * time_exp_fact # resample - need to do this after correcting for time expansion sampling_rate_old = sampling_rate sampling_rate = target_samp_rate - audio_raw = librosa.resample(audio_raw, orig_sr=sampling_rate_old, target_sr=sampling_rate, res_type='polyphase') + audio_raw = librosa.resample( + audio_raw, + orig_sr=sampling_rate_old, + target_sr=sampling_rate, + res_type="polyphase", + ) # clipping maximum duration if max_duration is not False: - max_duration = np.minimum(int(sampling_rate*max_duration), audio_raw.shape[0]) + max_duration = np.minimum( + int(sampling_rate * max_duration), audio_raw.shape[0] + ) audio_raw = audio_raw[:max_duration] - + # convert to float32 and scale audio_raw = audio_raw.astype(np.float32) if scale: @@ -93,38 +146,53 @@ def load_audio_file(audio_file, time_exp_fact, target_samp_rate, scale=False, ma return sampling_rate, audio_raw -def pad_audio(audio_raw, fs, ms, overlap_perc, resize_factor, divide_factor, fixed_width=None): +def pad_audio( + audio_raw, + fs, + ms, + overlap_perc, + resize_factor, + divide_factor, + fixed_width=None, +): # Adds zeros to the end of the raw data so that the generated sepctrogram # will be evenly divisible by `divide_factor` # Also deals with very short audio clips and fixed_width during training # This code could be clearer, clean up - nfft = int(ms*fs) - noverlap = int(overlap_perc*nfft) + nfft = int(ms * fs) + noverlap = int(overlap_perc * nfft) step = nfft - noverlap - min_size = int(divide_factor*(1.0/resize_factor)) - spec_width = ((audio_raw.shape[0]-noverlap)//step) + min_size = int(divide_factor * (1.0 / resize_factor)) + spec_width = (audio_raw.shape[0] - noverlap) // step spec_width_rs = spec_width * resize_factor if fixed_width is not None and spec_width < fixed_width: # too small # used during training to ensure all the batches are the same size - diff = fixed_width*step + noverlap - audio_raw.shape[0] - audio_raw = np.hstack((audio_raw, np.zeros(diff, dtype=audio_raw.dtype))) + diff = fixed_width * step + noverlap - audio_raw.shape[0] + audio_raw = np.hstack( + (audio_raw, np.zeros(diff, dtype=audio_raw.dtype)) + ) elif fixed_width is not None and spec_width > fixed_width: # too big # used during training to ensure all the batches are the same size - diff = fixed_width*step + noverlap - audio_raw.shape[0] + diff = fixed_width * step + noverlap - audio_raw.shape[0] audio_raw = audio_raw[:diff] - elif spec_width_rs < min_size or (np.floor(spec_width_rs) % divide_factor) != 0: + elif ( + spec_width_rs < min_size + or (np.floor(spec_width_rs) % divide_factor) != 0 + ): # need to be at least min_size div_amt = np.ceil(spec_width_rs / float(divide_factor)) div_amt = np.maximum(1, div_amt) - target_size = int(div_amt*divide_factor*(1.0/resize_factor)) - diff = target_size*step + noverlap - audio_raw.shape[0] - audio_raw = np.hstack((audio_raw, np.zeros(diff, dtype=audio_raw.dtype))) + target_size = int(div_amt * divide_factor * (1.0 / resize_factor)) + diff = target_size * step + noverlap - audio_raw.shape[0] + audio_raw = np.hstack( + (audio_raw, np.zeros(diff, dtype=audio_raw.dtype)) + ) return audio_raw @@ -133,14 +201,16 @@ def gen_mag_spectrogram(x, fs, ms, overlap_perc): # Computes magnitude spectrogram by specifying time. x = x.astype(np.float32) - nfft = int(ms*fs) - noverlap = int(overlap_perc*nfft) + nfft = int(ms * fs) + noverlap = int(overlap_perc * nfft) # window data step = nfft - noverlap # compute spec - spec, _ = librosa.core.spectrum._spectrogram(y=x, power=1, n_fft=nfft, hop_length=step, center=False) + spec, _ = librosa.core.spectrum._spectrogram( + y=x, power=1, n_fft=nfft, hop_length=step, center=False + ) # remove DC component and flip vertical orientation spec = np.flipud(spec[1:, :]) @@ -149,8 +219,8 @@ def gen_mag_spectrogram(x, fs, ms, overlap_perc): def gen_mag_spectrogram_pt(x, fs, ms, overlap_perc): - nfft = int(ms*fs) - nstep = round((1.0-overlap_perc)*nfft) + nfft = int(ms * fs) + nstep = round((1.0 - overlap_perc) * nfft) han_win = torch.hann_window(nfft, periodic=False).to(x.device) @@ -158,12 +228,14 @@ def gen_mag_spectrogram_pt(x, fs, ms, overlap_perc): spec = complex_spec.pow(2.0).sum(-1) # remove DC component and flip vertically - spec = torch.flipud(spec[0, 1:,:]) + spec = torch.flipud(spec[0, 1:, :]) return spec def pcen(spec_cropped, sampling_rate): # TODO should be passing hop_length too i.e. step - spec = librosa.pcen(spec_cropped * (2**31), sr=sampling_rate/10).astype(np.float32) + spec = librosa.pcen( + spec_cropped * (2**31), sr=sampling_rate / 10 + ).astype(np.float32) return spec diff --git a/bat_detect/utils/detector_utils.py b/bat_detect/utils/detector_utils.py index fef9828..7d2470f 100644 --- a/bat_detect/utils/detector_utils.py +++ b/bat_detect/utils/detector_utils.py @@ -1,39 +1,40 @@ -import torch -import torch.nn.functional as F -import os -import numpy as np -import pandas as pd import json +import os import sys -from bat_detect.detector import models +import numpy as np +import pandas as pd +import torch +import torch.nn.functional as F + import bat_detect.detector.compute_features as feats import bat_detect.detector.post_process as pp import bat_detect.utils.audio_utils as au +from bat_detect.detector import models def get_default_bd_args(): args = {} - args['detection_threshold'] = 0.001 - args['time_expansion_factor'] = 1 - args['audio_dir'] = '' - args['ann_dir'] = '' - args['spec_slices'] = False - args['chunk_size'] = 3 - args['spec_features'] = False - args['cnn_features'] = False - args['quiet'] = True - args['save_preds_if_empty'] = True - args['ann_dir'] = os.path.join(args['ann_dir'], '') + args["detection_threshold"] = 0.001 + args["time_expansion_factor"] = 1 + args["audio_dir"] = "" + args["ann_dir"] = "" + args["spec_slices"] = False + args["chunk_size"] = 3 + args["spec_features"] = False + args["cnn_features"] = False + args["quiet"] = True + args["save_preds_if_empty"] = True + args["ann_dir"] = os.path.join(args["ann_dir"], "") return args - - + + def get_audio_files(ip_dir): matches = [] for root, dirnames, filenames in os.walk(ip_dir): for filename in filenames: - if filename.lower().endswith('.wav'): + if filename.lower().endswith(".wav"): matches.append(os.path.join(root, filename)) return matches @@ -41,35 +42,47 @@ def get_audio_files(ip_dir): def load_model(model_path, load_weights=True): # load model - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") if os.path.isfile(model_path): net_params = torch.load(model_path, map_location=device) else: - print('Error: model not found.') + print("Error: model not found.") sys.exit(1) - params = net_params['params'] - params['device'] = device + params = net_params["params"] + params["device"] = device - if params['model_name'] == 'Net2DFast': - model = models.Net2DFast(params['num_filters'], num_classes=len(params['class_names']), - emb_dim=params['emb_dim'], ip_height=params['ip_height'], - resize_factor=params['resize_factor']) - elif params['model_name'] == 'Net2DFastNoAttn': - model = models.Net2DFastNoAttn(params['num_filters'], num_classes=len(params['class_names']), - emb_dim=params['emb_dim'], ip_height=params['ip_height'], - resize_factor=params['resize_factor']) - elif params['model_name'] == 'Net2DFastNoCoordConv': - model = models.Net2DFastNoCoordConv(params['num_filters'], num_classes=len(params['class_names']), - emb_dim=params['emb_dim'], ip_height=params['ip_height'], - resize_factor=params['resize_factor']) + if params["model_name"] == "Net2DFast": + model = models.Net2DFast( + params["num_filters"], + num_classes=len(params["class_names"]), + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + elif params["model_name"] == "Net2DFastNoAttn": + model = models.Net2DFastNoAttn( + params["num_filters"], + num_classes=len(params["class_names"]), + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) + elif params["model_name"] == "Net2DFastNoCoordConv": + model = models.Net2DFastNoCoordConv( + params["num_filters"], + num_classes=len(params["class_names"]), + emb_dim=params["emb_dim"], + ip_height=params["ip_height"], + resize_factor=params["resize_factor"], + ) else: - print('Error: unknown model.') + print("Error: unknown model.") if load_weights: - model.load_state_dict(net_params['state_dict']) + model.load_state_dict(net_params["state_dict"]) - model = model.to(params['device']) + model = model.to(params["device"]) model.eval() return model, params @@ -78,11 +91,13 @@ def load_model(model_path, load_weights=True): def merge_results(predictions, spec_feats, cnn_feats, spec_slices): predictions_m = {} - num_preds = np.sum([len(pp['det_probs']) for pp in predictions]) + num_preds = np.sum([len(pp["det_probs"]) for pp in predictions]) if num_preds > 0: for kk in predictions[0].keys(): - predictions_m[kk] = np.hstack([pp[kk] for pp in predictions if pp['det_probs'].shape[0] > 0]) + predictions_m[kk] = np.hstack( + [pp[kk] for pp in predictions if pp["det_probs"].shape[0] > 0] + ) else: # hack in case where no detected calls as we need some of the key names in dict predictions_m = predictions[0] @@ -94,47 +109,60 @@ def merge_results(predictions, spec_feats, cnn_feats, spec_slices): return predictions_m, spec_feats, cnn_feats, spec_slices -def convert_results(file_id, time_exp, duration, params, predictions, spec_feats, cnn_feats, spec_slices): +def convert_results( + file_id, + time_exp, + duration, + params, + predictions, + spec_feats, + cnn_feats, + spec_slices, +): # create a single dictionary - this is the format used by the annotation tool pred_dict = {} - pred_dict['id'] = file_id - pred_dict['annotated'] = False - pred_dict['issues'] = False - pred_dict['notes'] = 'Automatically generated.' - pred_dict['time_exp'] = time_exp - pred_dict['duration'] = round(duration, 4) - pred_dict['annotation'] = [] + pred_dict["id"] = file_id + pred_dict["annotated"] = False + pred_dict["issues"] = False + pred_dict["notes"] = "Automatically generated." + pred_dict["time_exp"] = time_exp + pred_dict["duration"] = round(duration, 4) + pred_dict["annotation"] = [] - class_prob_best = predictions['class_probs'].max(0) - class_ind_best = predictions['class_probs'].argmax(0) - class_overall = pp.overall_class_pred(predictions['det_probs'], predictions['class_probs']) - pred_dict['class_name'] = params['class_names'][np.argmax(class_overall)] + class_prob_best = predictions["class_probs"].max(0) + class_ind_best = predictions["class_probs"].argmax(0) + class_overall = pp.overall_class_pred( + predictions["det_probs"], predictions["class_probs"] + ) + pred_dict["class_name"] = params["class_names"][np.argmax(class_overall)] - for ii in range(predictions['det_probs'].shape[0]): + for ii in range(predictions["det_probs"].shape[0]): res = {} - res['start_time'] = round(float(predictions['start_times'][ii]), 4) - res['end_time'] = round(float(predictions['end_times'][ii]), 4) - res['low_freq'] = int(predictions['low_freqs'][ii]) - res['high_freq'] = int(predictions['high_freqs'][ii]) - res['class'] = str(params['class_names'][int(class_ind_best[ii])]) - res['class_prob'] = round(float(class_prob_best[ii]), 3) - res['det_prob'] = round(float(predictions['det_probs'][ii]), 3) - res['individual'] = '-1' - res['event'] = 'Echolocation' - pred_dict['annotation'].append(res) + res["start_time"] = round(float(predictions["start_times"][ii]), 4) + res["end_time"] = round(float(predictions["end_times"][ii]), 4) + res["low_freq"] = int(predictions["low_freqs"][ii]) + res["high_freq"] = int(predictions["high_freqs"][ii]) + res["class"] = str(params["class_names"][int(class_ind_best[ii])]) + res["class_prob"] = round(float(class_prob_best[ii]), 3) + res["det_prob"] = round(float(predictions["det_probs"][ii]), 3) + res["individual"] = "-1" + res["event"] = "Echolocation" + pred_dict["annotation"].append(res) # combine into final results dictionary results = {} - results['pred_dict'] = pred_dict + results["pred_dict"] = pred_dict if len(spec_feats) > 0: - results['spec_feats'] = spec_feats - results['spec_feat_names'] = feats.get_feature_names() + results["spec_feats"] = spec_feats + results["spec_feat_names"] = feats.get_feature_names() if len(cnn_feats) > 0: - results['cnn_feats'] = cnn_feats - results['cnn_feat_names'] = [str(ii) for ii in range(cnn_feats.shape[1])] + results["cnn_feats"] = cnn_feats + results["cnn_feat_names"] = [ + str(ii) for ii in range(cnn_feats.shape[1]) + ] if len(spec_slices) > 0: - results['spec_slices'] = spec_slices + results["spec_slices"] = spec_slices return results @@ -146,144 +174,214 @@ def save_results_to_file(results, op_path): os.makedirs(os.path.dirname(op_path)) # save csv file - if there are predictions - result_list = [res for res in results['pred_dict']['annotation']] + result_list = [res for res in results["pred_dict"]["annotation"]] df = pd.DataFrame(result_list) - df['file_name'] = [results['pred_dict']['id']]*len(result_list) - df.index.name = 'id' - if 'class_prob' in df.columns: - df = df[['det_prob', 'start_time', 'end_time', 'high_freq', - 'low_freq', 'class', 'class_prob']] - df.to_csv(op_path + '.csv', sep=',') + df["file_name"] = [results["pred_dict"]["id"]] * len(result_list) + df.index.name = "id" + if "class_prob" in df.columns: + df = df[ + [ + "det_prob", + "start_time", + "end_time", + "high_freq", + "low_freq", + "class", + "class_prob", + ] + ] + df.to_csv(op_path + ".csv", sep=",") # save features - if 'spec_feats' in results.keys(): - df = pd.DataFrame(results['spec_feats'], columns=results['spec_feat_names']) - df.to_csv(op_path + '_spec_features.csv', sep=',', index=False, float_format='%.5f') + if "spec_feats" in results.keys(): + df = pd.DataFrame( + results["spec_feats"], columns=results["spec_feat_names"] + ) + df.to_csv( + op_path + "_spec_features.csv", + sep=",", + index=False, + float_format="%.5f", + ) - if 'cnn_feats' in results.keys(): - df = pd.DataFrame(results['cnn_feats'], columns=results['cnn_feat_names']) - df.to_csv(op_path + '_cnn_features.csv', sep=',', index=False, float_format='%.5f') + if "cnn_feats" in results.keys(): + df = pd.DataFrame( + results["cnn_feats"], columns=results["cnn_feat_names"] + ) + df.to_csv( + op_path + "_cnn_features.csv", + sep=",", + index=False, + float_format="%.5f", + ) # save json file - with open(op_path + '.json', 'w') as da: - json.dump(results['pred_dict'], da, indent=2, sort_keys=True) + with open(op_path + ".json", "w") as da: + json.dump(results["pred_dict"], da, indent=2, sort_keys=True) def compute_spectrogram(audio, sampling_rate, params, return_np=False): # pad audio so it is evenly divisible by downsampling factors duration = audio.shape[0] / float(sampling_rate) - audio = au.pad_audio(audio, sampling_rate, params['fft_win_length'], - params['fft_overlap'], params['resize_factor'], - params['spec_divide_factor']) + audio = au.pad_audio( + audio, + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + params["resize_factor"], + params["spec_divide_factor"], + ) # generate spectrogram spec, _ = au.generate_spectrogram(audio, sampling_rate, params) # convert to pytorch - spec = torch.from_numpy(spec).to(params['device']) + spec = torch.from_numpy(spec).to(params["device"]) spec = spec.unsqueeze(0).unsqueeze(0) # resize the spec - rs = params['resize_factor'] - spec_op_shape = (int(params['spec_height']*rs), int(spec.shape[-1]*rs)) - spec = F.interpolate(spec, size=spec_op_shape, mode='bilinear', align_corners=False) + rs = params["resize_factor"] + spec_op_shape = (int(params["spec_height"] * rs), int(spec.shape[-1] * rs)) + spec = F.interpolate( + spec, size=spec_op_shape, mode="bilinear", align_corners=False + ) if return_np: - spec_np = spec[0,0,:].cpu().data.numpy() + spec_np = spec[0, 0, :].cpu().data.numpy() else: spec_np = None return duration, spec, spec_np -def process_file(audio_file, model, params, args, time_exp=None, top_n=5, return_raw_preds=False, max_duration=False): +def process_file( + audio_file, + model, + params, + args, + time_exp=None, + top_n=5, + return_raw_preds=False, + max_duration=False, +): # store temporary results here predictions = [] - spec_feats = [] - cnn_feats = [] + spec_feats = [] + cnn_feats = [] spec_slices = [] # get time expansion factor if time_exp is None: - time_exp = args['time_expansion_factor'] + time_exp = args["time_expansion_factor"] - params['detection_threshold'] = args['detection_threshold'] + params["detection_threshold"] = args["detection_threshold"] # load audio file - sampling_rate, audio_full = au.load_audio_file(audio_file, time_exp, - params['target_samp_rate'], params['scale_raw_audio']) + sampling_rate, audio_full = au.load_audio_file( + audio_file, + time_exp, + params["target_samp_rate"], + params["scale_raw_audio"], + ) # clipping maximum duration if max_duration is not False: - max_duration = np.minimum(int(sampling_rate*max_duration), audio_full.shape[0]) + max_duration = np.minimum( + int(sampling_rate * max_duration), audio_full.shape[0] + ) audio_full = audio_full[:max_duration] - + duration_full = audio_full.shape[0] / float(sampling_rate) - return_np_spec = args['spec_features'] or args['spec_slices'] + return_np_spec = args["spec_features"] or args["spec_slices"] # loop through larger file and split into chunks # TODO fix so that it overlaps correctly and takes care of duplicate detections at borders - num_chunks = int(np.ceil(duration_full/args['chunk_size'])) + num_chunks = int(np.ceil(duration_full / args["chunk_size"])) for chunk_id in range(num_chunks): # chunk - chunk_time = args['chunk_size']*chunk_id - chunk_length = int(sampling_rate*args['chunk_size']) - start_sample = chunk_id*chunk_length - end_sample = np.minimum((chunk_id+1)*chunk_length, audio_full.shape[0]) + chunk_time = args["chunk_size"] * chunk_id + chunk_length = int(sampling_rate * args["chunk_size"]) + start_sample = chunk_id * chunk_length + end_sample = np.minimum( + (chunk_id + 1) * chunk_length, audio_full.shape[0] + ) audio = audio_full[start_sample:end_sample] # load audio file and compute spectrogram - duration, spec, spec_np = compute_spectrogram(audio, sampling_rate, params, return_np_spec) + duration, spec, spec_np = compute_spectrogram( + audio, sampling_rate, params, return_np_spec + ) # evaluate model with torch.no_grad(): - outputs = model(spec, return_feats=args['cnn_features']) + outputs = model(spec, return_feats=args["cnn_features"]) # run non-max suppression - pred_nms, features = pp.run_nms(outputs, params, np.array([float(sampling_rate)])) + pred_nms, features = pp.run_nms( + outputs, params, np.array([float(sampling_rate)]) + ) pred_nms = pred_nms[0] - pred_nms['start_times'] += chunk_time - pred_nms['end_times'] += chunk_time + pred_nms["start_times"] += chunk_time + pred_nms["end_times"] += chunk_time # if we have a background class - if pred_nms['class_probs'].shape[0] > len(params['class_names']): - pred_nms['class_probs'] = pred_nms['class_probs'][:-1, :] + if pred_nms["class_probs"].shape[0] > len(params["class_names"]): + pred_nms["class_probs"] = pred_nms["class_probs"][:-1, :] predictions.append(pred_nms) # extract features - if there are any calls detected - if (pred_nms['det_probs'].shape[0] > 0): - if args['spec_features']: + if pred_nms["det_probs"].shape[0] > 0: + if args["spec_features"]: spec_feats.append(feats.get_feats(spec_np, pred_nms, params)) - if args['cnn_features']: + if args["cnn_features"]: cnn_feats.append(features[0]) - if args['spec_slices']: - spec_slices.extend(feats.extract_spec_slices(spec_np, pred_nms, params)) + if args["spec_slices"]: + spec_slices.extend( + feats.extract_spec_slices(spec_np, pred_nms, params) + ) # convert the predictions into output dictionary file_id = os.path.basename(audio_file) - predictions, spec_feats, cnn_feats, spec_slices =\ - merge_results(predictions, spec_feats, cnn_feats, spec_slices) - results = convert_results(file_id, time_exp, duration_full, params, - predictions, spec_feats, cnn_feats, spec_slices) + predictions, spec_feats, cnn_feats, spec_slices = merge_results( + predictions, spec_feats, cnn_feats, spec_slices + ) + results = convert_results( + file_id, + time_exp, + duration_full, + params, + predictions, + spec_feats, + cnn_feats, + spec_slices, + ) # summarize results - if not args['quiet']: - num_detections = len(results['pred_dict']['annotation']) - print('{}'.format(num_detections) + ' call(s) detected above the threshold.') + if not args["quiet"]: + num_detections = len(results["pred_dict"]["annotation"]) + print( + "{}".format(num_detections) + + " call(s) detected above the threshold." + ) # print results for top n classes - if not args['quiet'] and (num_detections > 0): - class_overall = pp.overall_class_pred(predictions['det_probs'], predictions['class_probs']) - print('species name'.ljust(30) + 'probablity present') + if not args["quiet"] and (num_detections > 0): + class_overall = pp.overall_class_pred( + predictions["det_probs"], predictions["class_probs"] + ) + print("species name".ljust(30) + "probablity present") for cc in np.argsort(class_overall)[::-1][:top_n]: - print(params['class_names'][cc].ljust(30) + str(round(class_overall[cc], 3))) + print( + params["class_names"][cc].ljust(30) + + str(round(class_overall[cc], 3)) + ) if return_raw_preds: return predictions diff --git a/bat_detect/utils/plot_utils.py b/bat_detect/utils/plot_utils.py index 5b38f65..ce88375 100644 --- a/bat_detect/utils/plot_utils.py +++ b/bat_detect/utils/plot_utils.py @@ -1,63 +1,109 @@ -import numpy as np -import matplotlib.pyplot as plt import json -from sklearn.metrics import confusion_matrix + +import matplotlib.pyplot as plt +import numpy as np from matplotlib import patches from matplotlib.collections import PatchCollection +from sklearn.metrics import confusion_matrix from . import audio_utils as au -def create_box_image(spec, fig, detections_ip, start_time, end_time, duration, params, max_val, hide_axis=True, plot_class_names=False): +def create_box_image( + spec, + fig, + detections_ip, + start_time, + end_time, + duration, + params, + max_val, + hide_axis=True, + plot_class_names=False, +): # filter detections stop_time = start_time + duration detections = [] for bb in detections_ip: - if (bb['start_time'] >= start_time) and (bb['start_time'] < stop_time-0.02): #(bb['end_time'] < end_time): + if (bb["start_time"] >= start_time) and ( + bb["start_time"] < stop_time - 0.02 + ): # (bb['end_time'] < end_time): detections.append(bb) # create figure freq_scale = 1000 # turn Hz to kHz - min_freq = params['min_freq']//freq_scale - max_freq = params['max_freq']//freq_scale + min_freq = params["min_freq"] // freq_scale + max_freq = params["max_freq"] // freq_scale y_extent = [0, duration, min_freq, max_freq] if hide_axis: - ax = plt.Axes(fig, [0., 0., 1., 1.]) + ax = plt.Axes(fig, [0.0, 0.0, 1.0, 1.0]) ax.set_axis_off() fig.add_axes(ax) else: ax = plt.gca() - plt.imshow(spec, aspect='auto', cmap='plasma', extent=y_extent, vmin=0, vmax=max_val) + plt.imshow( + spec, + aspect="auto", + cmap="plasma", + extent=y_extent, + vmin=0, + vmax=max_val, + ) boxes = plot_bounding_box_patch_ann(detections, freq_scale, start_time) ax.add_collection(PatchCollection(boxes, match_original=True)) plt.grid(False) if plot_class_names: for ii, bb in enumerate(boxes): - txt = ' '.join([sp[:3] for sp in detections_ip[ii]['class'].split(' ')]) - font_info = {'color': 'white', 'size': 10, 'weight': 'bold', 'alpha': bb.get_alpha()} + txt = " ".join( + [sp[:3] for sp in detections_ip[ii]["class"].split(" ")] + ) + font_info = { + "color": "white", + "size": 10, + "weight": "bold", + "alpha": bb.get_alpha(), + } y_pos = bb.get_xy()[1] + bb.get_height() if y_pos > (max_freq - 10): y_pos = max_freq - 10 plt.gca().text(bb.get_xy()[0], y_pos, txt, fontdict=font_info) -def save_ann_spec(op_path, spec, min_freq, max_freq, duration, start_time, title_text='', anns=None): +def save_ann_spec( + op_path, + spec, + min_freq, + max_freq, + duration, + start_time, + title_text="", + anns=None, +): # create figure and plot boxes freq_scale = 1000 # turn Hz to kHz - min_freq = min_freq//freq_scale - max_freq = max_freq//freq_scale + min_freq = min_freq // freq_scale + max_freq = max_freq // freq_scale y_extent = [0, duration, min_freq, max_freq] - plt.close('all') - fig = plt.figure(0, figsize=(spec.shape[1]/100, spec.shape[0]/100), dpi=100) - plt.imshow(spec, aspect='auto', cmap='plasma', extent=y_extent, vmin=0, vmax=spec.max()*1.1) + plt.close("all") + fig = plt.figure( + 0, figsize=(spec.shape[1] / 100, spec.shape[0] / 100), dpi=100 + ) + plt.imshow( + spec, + aspect="auto", + cmap="plasma", + extent=y_extent, + vmin=0, + vmax=spec.max() * 1.1, + ) - plt.ylabel('Freq - kHz') - plt.xlabel('Time - secs') - if title_text != '': + plt.ylabel("Freq - kHz") + plt.xlabel("Time - secs") + if title_text != "": plt.title(title_text) plt.tight_layout() @@ -66,122 +112,185 @@ def save_ann_spec(op_path, spec, min_freq, max_freq, duration, start_time, title boxes = plot_bounding_box_patch_ann(anns, freq_scale, start_time) plt.gca().add_collection(PatchCollection(boxes, match_original=True)) for ii, bb in enumerate(boxes): - txt = ' '.join([sp[:3] for sp in anns[ii]['class'].split(' ')]) - font_info = {'color': 'white', 'size': 10, 'weight': 'bold', 'alpha': bb.get_alpha()} + txt = " ".join([sp[:3] for sp in anns[ii]["class"].split(" ")]) + font_info = { + "color": "white", + "size": 10, + "weight": "bold", + "alpha": bb.get_alpha(), + } y_pos = bb.get_xy()[1] + bb.get_height() if y_pos > (max_freq - 10): y_pos = max_freq - 10 plt.gca().text(bb.get_xy()[0], y_pos, txt, fontdict=font_info) - print('Saving figure to:', op_path) + print("Saving figure to:", op_path) plt.savefig(op_path) -def plot_pts(fig_id, feats, class_names, colors, marker_size=4.0, plot_legend=False): +def plot_pts( + fig_id, feats, class_names, colors, marker_size=4.0, plot_legend=False +): plt.figure(fig_id) un_class, labels = np.unique(class_names, return_inverse=True) un_labels = np.unique(labels) if un_labels.shape[0] > len(colors): - colors = [plt.cm.jet(float(ii)/un_labels.shape[0]) for ii in un_labels] + colors = [ + plt.cm.jet(float(ii) / un_labels.shape[0]) for ii in un_labels + ] for ii, u in enumerate(un_labels): - inds = np.where(labels==u)[0] - plt.scatter(feats[inds, 0], feats[inds, 1], c=colors[ii], label=str(un_class[ii]), s=marker_size) + inds = np.where(labels == u)[0] + plt.scatter( + feats[inds, 0], + feats[inds, 1], + c=colors[ii], + label=str(un_class[ii]), + s=marker_size, + ) if plot_legend: plt.legend() plt.xticks([]) plt.yticks([]) - plt.title('downsampled features') + plt.title("downsampled features") -def plot_bounding_box_patch(pred, freq_scale, ecolor='w'): +def plot_bounding_box_patch(pred, freq_scale, ecolor="w"): patch_collect = [] - for bb in range(len(pred['start_times'])): - xx = pred['start_times'][bb] - ww = pred['end_times'][bb] - pred['start_times'][bb] - yy = pred['low_freqs'][bb] / freq_scale - hh = (pred['high_freqs'][bb] - pred['low_freqs'][bb]) / freq_scale + for bb in range(len(pred["start_times"])): + xx = pred["start_times"][bb] + ww = pred["end_times"][bb] - pred["start_times"][bb] + yy = pred["low_freqs"][bb] / freq_scale + hh = (pred["high_freqs"][bb] - pred["low_freqs"][bb]) / freq_scale - if 'det_probs' in pred.keys(): - alpha_val = pred['det_probs'][bb] + if "det_probs" in pred.keys(): + alpha_val = pred["det_probs"][bb] else: alpha_val = 1.0 - patch_collect.append(patches.Rectangle((xx, yy), ww, hh, linewidth=1, - edgecolor=ecolor, facecolor='none', alpha=alpha_val)) + patch_collect.append( + patches.Rectangle( + (xx, yy), + ww, + hh, + linewidth=1, + edgecolor=ecolor, + facecolor="none", + alpha=alpha_val, + ) + ) return patch_collect def plot_bounding_box_patch_ann(anns, freq_scale, start_time): patch_collect = [] for aa in range(len(anns)): - xx = anns[aa]['start_time'] - start_time - ww = anns[aa]['end_time'] - anns[aa]['start_time'] - yy = anns[aa]['low_freq'] / freq_scale - hh = (anns[aa]['high_freq'] - anns[aa]['low_freq']) / freq_scale - if 'det_prob' in anns[aa]: - alpha = anns[aa]['det_prob'] + xx = anns[aa]["start_time"] - start_time + ww = anns[aa]["end_time"] - anns[aa]["start_time"] + yy = anns[aa]["low_freq"] / freq_scale + hh = (anns[aa]["high_freq"] - anns[aa]["low_freq"]) / freq_scale + if "det_prob" in anns[aa]: + alpha = anns[aa]["det_prob"] else: alpha = 1.0 - patch_collect.append(patches.Rectangle((xx,yy), ww, hh, linewidth=1, - edgecolor='w', facecolor='none', alpha=alpha)) + patch_collect.append( + patches.Rectangle( + (xx, yy), + ww, + hh, + linewidth=1, + edgecolor="w", + facecolor="none", + alpha=alpha, + ) + ) return patch_collect -def plot_spec(spec, sampling_rate, duration, gt, pred, params, plot_title, - op_file_name, pred_2d_hm, plot_boxes=True, fixed_aspect=True): +def plot_spec( + spec, + sampling_rate, + duration, + gt, + pred, + params, + plot_title, + op_file_name, + pred_2d_hm, + plot_boxes=True, + fixed_aspect=True, +): if fixed_aspect: # ouptut image will be this width irrespective of the duration of the audio file width = 12 else: - width = 12*duration + width = 12 * duration fig = plt.figure(1, figsize=(width, 8)) - ax0 = plt.axes([0.05, 0.65, 0.9, 0.30]) # l b w h + ax0 = plt.axes([0.05, 0.65, 0.9, 0.30]) # l b w h ax1 = plt.axes([0.05, 0.33, 0.9, 0.30]) ax2 = plt.axes([0.05, 0.01, 0.9, 0.30]) freq_scale = 1000 # turn Hz in kHz - #duration = au.x_coords_to_time(spec.shape[1], sampling_rate, params['fft_win_length'], params['fft_overlap']) - y_extent = [0, duration, params['min_freq']//freq_scale, params['max_freq']//freq_scale] + # duration = au.x_coords_to_time(spec.shape[1], sampling_rate, params['fft_win_length'], params['fft_overlap']) + y_extent = [ + 0, + duration, + params["min_freq"] // freq_scale, + params["max_freq"] // freq_scale, + ] # plot gt boxes - ax0.imshow(spec, aspect='auto', cmap='plasma', extent=y_extent) + ax0.imshow(spec, aspect="auto", cmap="plasma", extent=y_extent) ax0.xaxis.set_ticklabels([]) - font_info = {'color': 'white', 'size': 12, 'weight': 'bold'} - ax0.text(0, params['min_freq']//freq_scale, 'Ground Truth', fontdict=font_info) + font_info = {"color": "white", "size": 12, "weight": "bold"} + ax0.text( + 0, params["min_freq"] // freq_scale, "Ground Truth", fontdict=font_info + ) plt.grid(False) if plot_boxes: boxes = plot_bounding_box_patch(gt, freq_scale) ax0.add_collection(PatchCollection(boxes, match_original=True)) for ii, bb in enumerate(boxes): - class_id = int(gt['class_ids'][ii]) + class_id = int(gt["class_ids"][ii]) if class_id < 0: - txt = params['generic_class'][0] + txt = params["generic_class"][0] else: - txt = params['class_names_short'][class_id] - font_info = {'color': 'white', 'size': 10, 'weight': 'bold', 'alpha': bb.get_alpha()} + txt = params["class_names_short"][class_id] + font_info = { + "color": "white", + "size": 10, + "weight": "bold", + "alpha": bb.get_alpha(), + } y_pos = bb.get_xy()[1] + bb.get_height() ax0.text(bb.get_xy()[0], y_pos, txt, fontdict=font_info) # plot predicted boxes - ax1.imshow(spec, aspect='auto', cmap='plasma', extent=y_extent) + ax1.imshow(spec, aspect="auto", cmap="plasma", extent=y_extent) ax1.xaxis.set_ticklabels([]) - font_info = {'color': 'white', 'size': 12, 'weight': 'bold'} - ax1.text(0, params['min_freq']//freq_scale, 'Prediction', fontdict=font_info) + font_info = {"color": "white", "size": 12, "weight": "bold"} + ax1.text( + 0, params["min_freq"] // freq_scale, "Prediction", fontdict=font_info + ) plt.grid(False) if plot_boxes: boxes = plot_bounding_box_patch(pred, freq_scale) ax1.add_collection(PatchCollection(boxes, match_original=True)) for ii, bb in enumerate(boxes): - if pred['class_probs'].shape[0] > len(params['class_names_short']): - class_id = pred['class_probs'][:-1, ii].argmax() + if pred["class_probs"].shape[0] > len(params["class_names_short"]): + class_id = pred["class_probs"][:-1, ii].argmax() else: - class_id = pred['class_probs'][:, ii].argmax() - txt = params['class_names_short'][class_id] - font_info = {'color': 'white', 'size': 10, 'weight': 'bold', 'alpha': bb.get_alpha()} + class_id = pred["class_probs"][:, ii].argmax() + txt = params["class_names_short"][class_id] + font_info = { + "color": "white", + "size": 10, + "weight": "bold", + "alpha": bb.get_alpha(), + } y_pos = bb.get_xy()[1] + bb.get_height() ax1.text(bb.get_xy()[0], y_pos, txt, fontdict=font_info) @@ -190,10 +299,18 @@ def plot_spec(spec, sampling_rate, duration, gt, pred, params, plot_title, min_val = 0.0 if pred_2d_hm.min() > 0.0 else pred_2d_hm.min() max_val = 1.0 if pred_2d_hm.max() < 1.0 else pred_2d_hm.max() - ax2.imshow(pred_2d_hm, aspect='auto', cmap='plasma', extent=y_extent, clim=[min_val, max_val]) - #ax2.xaxis.set_ticklabels([]) - font_info = {'color': 'white', 'size': 12, 'weight': 'bold'} - ax2.text(0, params['min_freq']//freq_scale, 'Heatmap', fontdict=font_info) + ax2.imshow( + pred_2d_hm, + aspect="auto", + cmap="plasma", + extent=y_extent, + clim=[min_val, max_val], + ) + # ax2.xaxis.set_ticklabels([]) + font_info = {"color": "white", "size": 12, "weight": "bold"} + ax2.text( + 0, params["min_freq"] // freq_scale, "Heatmap", fontdict=font_info + ) plt.grid(False) @@ -204,107 +321,151 @@ def plot_spec(spec, sampling_rate, duration, gt, pred, params, plot_title, plt.close(1) -def plot_pr_curve(op_dir, plt_title, file_name, results, file_type='png', title_text=''): - precision = results['precision'] - recall = results['recall'] - avg_prec = results['avg_prec'] +def plot_pr_curve( + op_dir, plt_title, file_name, results, file_type="png", title_text="" +): + precision = results["precision"] + recall = results["recall"] + avg_prec = results["avg_prec"] - plt.figure(0, figsize=(10,8)) + plt.figure(0, figsize=(10, 8)) plt.plot(recall, precision) - plt.ylabel('Precision', fontsize=20) - plt.xlabel('Recall', fontsize=20) - if title_text != '': - plt.title(title_text, fontdict={'fontsize': 28}) + plt.ylabel("Precision", fontsize=20) + plt.xlabel("Recall", fontsize=20) + if title_text != "": + plt.title(title_text, fontdict={"fontsize": 28}) else: - plt.title(plt_title + ' {:.3f}\n'.format(avg_prec)) - plt.xlim(0,1.02) - plt.ylim(0,1.02) + plt.title(plt_title + " {:.3f}\n".format(avg_prec)) + plt.xlim(0, 1.02) + plt.ylim(0, 1.02) plt.grid(True) plt.tight_layout() - plt.savefig(op_dir + file_name + '.' + file_type) + plt.savefig(op_dir + file_name + "." + file_type) plt.close(0) -def plot_pr_curve_class(op_dir, plt_title, file_name, results, file_type='png', title_text=''): - plt.figure(0, figsize=(10,8)) - plt.ylabel('Precision', fontsize=20) - plt.xlabel('Recall', fontsize=20) - plt.xlim(0,1.02) - plt.ylim(0,1.02) +def plot_pr_curve_class( + op_dir, plt_title, file_name, results, file_type="png", title_text="" +): + plt.figure(0, figsize=(10, 8)) + plt.ylabel("Precision", fontsize=20) + plt.xlabel("Recall", fontsize=20) + plt.xlim(0, 1.02) + plt.ylim(0, 1.02) plt.grid(True) - linestyles = ['-', ':', '--'] - markers = ['o', 'v', '>', '^', '<', 's', 'P', 'X', '*'] - colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] + linestyles = ["-", ":", "--"] + markers = ["o", "v", ">", "^", "<", "s", "P", "X", "*"] + colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] # plot the PR curves - for ii, rr in enumerate(results['class_pr']): - class_name = ' '.join([sp[:3] for sp in rr['name'].split(' ')]) - cur_color = colors[int(ii%10)] - plt.plot(rr['recall'], rr['precision'], label=class_name, color=cur_color, - linestyle=linestyles[int(ii//10)], lw=2.5) + for ii, rr in enumerate(results["class_pr"]): + class_name = " ".join([sp[:3] for sp in rr["name"].split(" ")]) + cur_color = colors[int(ii % 10)] + plt.plot( + rr["recall"], + rr["precision"], + label=class_name, + color=cur_color, + linestyle=linestyles[int(ii // 10)], + lw=2.5, + ) - #print(class_name) + # print(class_name) # plot the location of the confidence threshold values - for jj, tt in enumerate(rr['thresholds']): - ind = rr['thresholds_inds'][jj] + for jj, tt in enumerate(rr["thresholds"]): + ind = rr["thresholds_inds"][jj] if ind > -1: - plt.plot(rr['recall'][ind], rr['precision'][ind], markers[jj], - color=cur_color, ms=10) - #print(np.round(tt,2), np.round(rr['recall'][ind],3), np.round(rr['precision'][ind],3)) + plt.plot( + rr["recall"][ind], + rr["precision"][ind], + markers[jj], + color=cur_color, + ms=10, + ) + # print(np.round(tt,2), np.round(rr['recall'][ind],3), np.round(rr['precision'][ind],3)) - if title_text != '': - plt.title(title_text, fontdict={'fontsize': 28}) + if title_text != "": + plt.title(title_text, fontdict={"fontsize": 28}) else: - plt.title(plt_title + ' {:.3f}\n'.format(results['avg_prec_class'])) - plt.legend(loc='lower left', prop={'size': 14}) + plt.title(plt_title + " {:.3f}\n".format(results["avg_prec_class"])) + plt.legend(loc="lower left", prop={"size": 14}) plt.tight_layout() - plt.savefig(op_dir + file_name + '.' + file_type) + plt.savefig(op_dir + file_name + "." + file_type) plt.close(0) -def plot_confusion_matrix(op_dir, op_file, gt, pred, file_acc, class_names_long, verbose=False, file_type='png', title_text=''): +def plot_confusion_matrix( + op_dir, + op_file, + gt, + pred, + file_acc, + class_names_long, + verbose=False, + file_type="png", + title_text="", +): # shorten the class names for plotting class_names = [] for cc in class_names_long: - class_name_sm = ''.join([cc_sm[:3] + ' ' for cc_sm in cc.split(' ')])[:-1] + class_name_sm = "".join([cc_sm[:3] + " " for cc_sm in cc.split(" ")])[ + :-1 + ] class_names.append(class_name_sm) num_classes = len(class_names) - cm = confusion_matrix(gt, pred, labels=np.arange(num_classes)).astype(np.float32) + cm = confusion_matrix(gt, pred, labels=np.arange(num_classes)).astype( + np.float32 + ) cm_norm = cm.sum(1) valid_inds = np.where(cm_norm > 0)[0] - cm[valid_inds, :] = cm[valid_inds, :] / cm_norm[valid_inds][..., np.newaxis] - cm[np.where(cm_norm ==- 0)[0], :] = np.nan + cm[valid_inds, :] = ( + cm[valid_inds, :] / cm_norm[valid_inds][..., np.newaxis] + ) + cm[np.where(cm_norm == -0)[0], :] = np.nan if verbose: - print('Per class accuracy:') + print("Per class accuracy:") str_len = np.max([len(cc) for cc in class_names_long]) + 5 accs = np.diag(cm) for ii, cc in enumerate(class_names_long): if np.isnan(accs[ii]): print(str(ii).ljust(5) + cc.ljust(str_len)) else: - print(str(ii).ljust(5) + cc.ljust(str_len) + '{:.2f}'.format(accs[ii]*100)) + print( + str(ii).ljust(5) + + cc.ljust(str_len) + + "{:.2f}".format(accs[ii] * 100) + ) - plt.figure(0, figsize=(10,8)) - plt.imshow(cm, vmin=0, vmax=1, cmap='plasma') + plt.figure(0, figsize=(10, 8)) + plt.imshow(cm, vmin=0, vmax=1, cmap="plasma") plt.colorbar() - plt.xticks(np.arange(cm.shape[1]), class_names, rotation='vertical') + plt.xticks(np.arange(cm.shape[1]), class_names, rotation="vertical") plt.yticks(np.arange(cm.shape[0]), class_names) - plt.xlabel('Predicted', fontsize=20) - plt.ylabel('Ground Truth', fontsize=20) - if title_text != '': - plt.title(title_text, fontdict={'fontsize': 28}) + plt.xlabel("Predicted", fontsize=20) + plt.ylabel("Ground Truth", fontsize=20) + if title_text != "": + plt.title(title_text, fontdict={"fontsize": 28}) else: - plt.title(op_file + ' {:.3f}\n'.format(file_acc)) + plt.title(op_file + " {:.3f}\n".format(file_acc)) plt.tight_layout() - plt.savefig(op_dir + op_file + '.' + file_type) - plt.close('all') + plt.savefig(op_dir + op_file + "." + file_type) + plt.close("all") class LossPlotter(object): - def __init__(self, op_file_name, duration, labels, ylim, class_names, axis_labels=None, logy=False): + def __init__( + self, + op_file_name, + duration, + labels, + ylim, + class_names, + axis_labels=None, + logy=False, + ): self.reset() self.op_file_name = op_file_name self.duration = duration # length of x axis @@ -327,11 +488,16 @@ class LossPlotter(object): self.save_confusion_matrix(gt, pred) def save_plot(self): - linestyles = ['-', ':', '--'] - plt.figure(0, figsize=(8,5)) + linestyles = ["-", ":", "--"] + plt.figure(0, figsize=(8, 5)) for ii in range(len(self.vals[0])): l_vals = [vv[ii] for vv in self.vals] - plt.plot(self.epochs, l_vals, label=self.labels[ii], linestyle=linestyles[int(ii//10)]) + plt.plot( + self.epochs, + l_vals, + label=self.labels[ii], + linestyle=linestyles[int(ii // 10)], + ) plt.xlim(0, np.maximum(self.duration, len(self.vals))) if self.ylim is not None: plt.ylim(self.ylim[0], self.ylim[1]) @@ -339,33 +505,41 @@ class LossPlotter(object): plt.xlabel(self.axis_labels[0]) plt.ylabel(self.axis_labels[1]) if self.logy: - plt.gca().set_yscale('log') + plt.gca().set_yscale("log") plt.grid(True) - plt.legend(bbox_to_anchor=(1.01, 1), loc='upper left', borderaxespad=0.0) + plt.legend( + bbox_to_anchor=(1.01, 1), loc="upper left", borderaxespad=0.0 + ) plt.tight_layout() plt.savefig(self.op_file_name) plt.close(0) def save_json(self): data = {} - data['epochs'] = self.epochs + data["epochs"] = self.epochs for ii in range(len(self.vals[0])): - data[self.labels[ii]] = [round(vv[ii],4) for vv in self.vals] - with open(self.op_file_name[:-4] + '.json', 'w') as da: + data[self.labels[ii]] = [round(vv[ii], 4) for vv in self.vals] + with open(self.op_file_name[:-4] + ".json", "w") as da: json.dump(data, da, indent=2) def save_confusion_matrix(self, gt, pred): plt.figure(0) - cm = confusion_matrix(gt, pred, np.arange(len(self.class_names))).astype(np.float32) + cm = confusion_matrix( + gt, pred, np.arange(len(self.class_names)) + ).astype(np.float32) cm_norm = cm.sum(1) valid_inds = np.where(cm_norm > 0)[0] - cm[valid_inds, :] = cm[valid_inds, :] / cm_norm[valid_inds][..., np.newaxis] - plt.imshow(cm, vmin=0, vmax=1, cmap='plasma') + cm[valid_inds, :] = ( + cm[valid_inds, :] / cm_norm[valid_inds][..., np.newaxis] + ) + plt.imshow(cm, vmin=0, vmax=1, cmap="plasma") plt.colorbar() - plt.xticks(np.arange(cm.shape[1]), self.class_names, rotation='vertical') + plt.xticks( + np.arange(cm.shape[1]), self.class_names, rotation="vertical" + ) plt.yticks(np.arange(cm.shape[0]), self.class_names) - plt.xlabel('Predicted') - plt.ylabel('Ground Truth') + plt.xlabel("Predicted") + plt.ylabel("Ground Truth") plt.tight_layout() - plt.savefig(self.op_file_name[:-4] + '_cm.png') + plt.savefig(self.op_file_name[:-4] + "_cm.png") plt.close(0) diff --git a/bat_detect/utils/visualize.py b/bat_detect/utils/visualize.py index bea7f6b..54be1df 100644 --- a/bat_detect/utils/visualize.py +++ b/bat_detect/utils/visualize.py @@ -1,19 +1,46 @@ -import numpy as np import matplotlib.pyplot as plt +import numpy as np from matplotlib import patches -from sklearn.svm import LinearSVC from matplotlib.axes._axes import _log as matplotlib_axes_logger -matplotlib_axes_logger.setLevel('ERROR') +from sklearn.svm import LinearSVC + +matplotlib_axes_logger.setLevel("ERROR") -colors = ['#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', - '#42d4f4', '#f032e6', '#bfef45', '#fabebe', '#469990', '#e6beff', - '#9A6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', - '#000075', '#a9a9a9'] +colors = [ + "#e6194B", + "#3cb44b", + "#ffe119", + "#4363d8", + "#f58231", + "#911eb4", + "#42d4f4", + "#f032e6", + "#bfef45", + "#fabebe", + "#469990", + "#e6beff", + "#9A6324", + "#fffac8", + "#800000", + "#aaffc3", + "#808000", + "#ffd8b1", + "#000075", + "#a9a9a9", +] class InteractivePlotter: - def __init__(self, feats_ds, feats, spec_slices, call_info, freq_lims, allow_training): + def __init__( + self, + feats_ds, + feats, + spec_slices, + call_info, + freq_lims, + allow_training, + ): """ Plots 2D low dimensional features on left and corresponding spectgrams on the right. @@ -24,78 +51,123 @@ class InteractivePlotter: self.spec_slices = spec_slices self.call_info = call_info - #_, self.labels = np.unique([cc['class'] for cc in call_info], return_inverse=True) + # _, self.labels = np.unique([cc['class'] for cc in call_info], return_inverse=True) self.labels = np.zeros(len(call_info), dtype=np.int) - self.annotated = np.zeros(self.labels.shape[0], dtype=np.int) # can populate this with 1's where we have labels - self.labels_cols = [colors[self.labels[ii]] for ii in range(len(self.labels))] + self.annotated = np.zeros( + self.labels.shape[0], dtype=np.int + ) # can populate this with 1's where we have labels + self.labels_cols = [ + colors[self.labels[ii]] for ii in range(len(self.labels)) + ] self.freq_lims = freq_lims self.allow_training = allow_training self.pt_size = 5.0 - self.spec_pad = 0.2 # this much padding has been applied to the spec slices + self.spec_pad = ( + 0.2 # this much padding has been applied to the spec slices + ) self.fig_width = 12 self.fig_height = 8 self.current_id = 0 max_ind = np.argmax([ss.shape[1] for ss in self.spec_slices]) self.max_width = self.spec_slices[max_ind].shape[1] - self.blank_spec = np.zeros((self.spec_slices[0].shape[0], self.max_width)) - + self.blank_spec = np.zeros( + (self.spec_slices[0].shape[0], self.max_width) + ) def plot(self, fig_id): - self.fig, self.ax = plt.subplots(nrows=1, ncols=2, num=fig_id, figsize=(self.fig_width, self.fig_height), - gridspec_kw={'width_ratios': [2, 1]}) + self.fig, self.ax = plt.subplots( + nrows=1, + ncols=2, + num=fig_id, + figsize=(self.fig_width, self.fig_height), + gridspec_kw={"width_ratios": [2, 1]}, + ) plt.tight_layout() # plot 2D TNSE features - self.low_dim_plt = self.ax[0].scatter(self.feats_ds[:, 0], self.feats_ds[:, 1], - c=self.labels_cols, s=self.pt_size, picker=5) - self.ax[0].set_title('TSNE of Call Features') + self.low_dim_plt = self.ax[0].scatter( + self.feats_ds[:, 0], + self.feats_ds[:, 1], + c=self.labels_cols, + s=self.pt_size, + picker=5, + ) + self.ax[0].set_title("TSNE of Call Features") self.ax[0].set_xticks([]) self.ax[0].set_yticks([]) # plot clip from spectrogram - spec_min_max = (0, self.blank_spec.shape[1], self.freq_lims[0], self.freq_lims[1]) - self.ax[1].imshow(self.blank_spec, extent=spec_min_max, cmap='plasma', aspect='auto') + spec_min_max = ( + 0, + self.blank_spec.shape[1], + self.freq_lims[0], + self.freq_lims[1], + ) + self.ax[1].imshow( + self.blank_spec, extent=spec_min_max, cmap="plasma", aspect="auto" + ) self.spec_im = self.ax[1].get_images()[0] - self.ax[1].set_title('Spectrogram') - self.ax[1].grid(color='w', linewidth=0.5) + self.ax[1].set_title("Spectrogram") + self.ax[1].grid(color="w", linewidth=0.5) self.ax[1].set_xticks([]) - self.ax[1].set_ylabel('kHz') + self.ax[1].set_ylabel("kHz") - bbox_orig = patches.Rectangle((0,0),0,0, edgecolor='w', linewidth=0, fill=False) + bbox_orig = patches.Rectangle( + (0, 0), 0, 0, edgecolor="w", linewidth=0, fill=False + ) self.ax[1].add_patch(bbox_orig) - self.annot = self.ax[0].annotate('', xy=(0,0), xytext=(20,20),textcoords='offset points', - bbox=dict(boxstyle='round', fc='w'), arrowprops=dict(arrowstyle='->')) + self.annot = self.ax[0].annotate( + "", + xy=(0, 0), + xytext=(20, 20), + textcoords="offset points", + bbox=dict(boxstyle="round", fc="w"), + arrowprops=dict(arrowstyle="->"), + ) self.annot.set_visible(False) - self.fig.canvas.mpl_connect('motion_notify_event', self.mouse_hover) - self.fig.canvas.mpl_connect('key_press_event', self.key_press) - + self.fig.canvas.mpl_connect("motion_notify_event", self.mouse_hover) + self.fig.canvas.mpl_connect("key_press_event", self.key_press) def mouse_hover(self, event): vis = self.annot.get_visible() if event.inaxes == self.ax[0]: cont, ind = self.low_dim_plt.contains(event) if cont: - self.current_id = ind['ind'][0] + self.current_id = ind["ind"][0] # copy spec into full window - probably a better way of doing this new_spec = self.blank_spec.copy() - w_diff = (self.blank_spec.shape[1] - self.spec_slices[self.current_id].shape[1])//2 - new_spec[:, w_diff:self.spec_slices[self.current_id].shape[1]+w_diff] = self.spec_slices[self.current_id] + w_diff = ( + self.blank_spec.shape[1] + - self.spec_slices[self.current_id].shape[1] + ) // 2 + new_spec[ + :, + w_diff : self.spec_slices[self.current_id].shape[1] + + w_diff, + ] = self.spec_slices[self.current_id] self.spec_im.set_data(new_spec) self.spec_im.set_clim(vmin=0, vmax=new_spec.max()) # draw bounding box around call self.ax[1].patches[0].remove() - spec_width_orig = self.spec_slices[self.current_id].shape[1]/(1.0+2.0*self.spec_pad) - xx = w_diff + self.spec_pad*spec_width_orig + spec_width_orig = self.spec_slices[self.current_id].shape[ + 1 + ] / (1.0 + 2.0 * self.spec_pad) + xx = w_diff + self.spec_pad * spec_width_orig ww = spec_width_orig - yy = self.call_info[self.current_id]['low_freq']/1000 - hh = (self.call_info[self.current_id]['high_freq']-self.call_info[self.current_id]['low_freq'])/1000 - bbox = patches.Rectangle((xx,yy),ww,hh, edgecolor='r', linewidth=0.5, fill=False) + yy = self.call_info[self.current_id]["low_freq"] / 1000 + hh = ( + self.call_info[self.current_id]["high_freq"] + - self.call_info[self.current_id]["low_freq"] + ) / 1000 + bbox = patches.Rectangle( + (xx, yy), ww, hh, edgecolor="r", linewidth=0.5, fill=False + ) self.ax[1].add_patch(bbox) # update annotation arrow @@ -104,38 +176,54 @@ class InteractivePlotter: self.annot.set_visible(True) # write call info - info_str = self.call_info[self.current_id]['file_name'] + ', time=' \ - + str(round(self.call_info[self.current_id]['start_time'],3)) \ - + ', prob=' + str(round(self.call_info[self.current_id]['det_prob'],3)) + info_str = ( + self.call_info[self.current_id]["file_name"] + + ", time=" + + str( + round(self.call_info[self.current_id]["start_time"], 3) + ) + + ", prob=" + + str( + round(self.call_info[self.current_id]["det_prob"], 3) + ) + ) self.ax[0].set_xlabel(info_str) # redraw self.fig.canvas.draw_idle() - def key_press(self, event): if event.key.isdigit(): self.labels_cols[self.current_id] = colors[int(event.key)] self.labels[self.current_id] = int(event.key) self.annotated[self.current_id] = 1 - elif event.key == 'enter' and self.allow_training: + elif event.key == "enter" and self.allow_training: self.train_classifier() - elif event.key == 'x' and self.allow_training: + elif event.key == "x" and self.allow_training: self.get_classifier_params() - self.ax[0].scatter(self.feats_ds[:, 0], self.feats_ds[:, 1], - c=self.labels_cols, s=self.pt_size) + self.ax[0].scatter( + self.feats_ds[:, 0], + self.feats_ds[:, 1], + c=self.labels_cols, + s=self.pt_size, + ) self.fig.canvas.draw_idle() - def train_classifier(self): # TODO maybe it's better to classify in 2D space - but then can't be linear ... inds = np.where(self.annotated == 1)[0] labs_un, labs_inds = np.unique(self.labels[inds], return_inverse=True) if labs_un.shape[0] > 1: # needs at least 2 classes - self.clf = LinearSVC(C=1.0, penalty='l2', loss='squared_hinge', tol=0.0001, - intercept_scaling=1.0, max_iter=2000) + self.clf = LinearSVC( + C=1.0, + penalty="l2", + loss="squared_hinge", + tol=0.0001, + intercept_scaling=1.0, + max_iter=2000, + ) self.clf.fit(self.feats[inds, :], self.labels[inds]) @@ -145,14 +233,13 @@ class InteractivePlotter: for ii in inds_unlab: self.labels_cols[ii] = colors[self.labels[ii]] else: - print('Not enough data - please label more classes.') - + print("Not enough data - please label more classes.") def get_classifier_params(self): res = {} if self.clf is None: - print('Model not trained!') + print("Model not trained!") else: - res['weights'] = self.clf.coef_.astype(np.float32) - res['biases'] = self.clf.intercept_.astype(np.float32) + res["weights"] = self.clf.coef_.astype(np.float32) + res["biases"] = self.clf.intercept_.astype(np.float32) return res diff --git a/bat_detect/utils/wavfile.py b/bat_detect/utils/wavfile.py index a6715b0..7fee660 100644 --- a/bat_detect/utils/wavfile.py +++ b/bat_detect/utils/wavfile.py @@ -8,23 +8,25 @@ Functions `write`: Write a numpy array as a WAV file. """ -from __future__ import division, print_function, absolute_import +from __future__ import absolute_import, division, print_function -import sys -import numpy -import struct -import warnings import os +import struct +import sys +import warnings + +import numpy class WavFileWarning(UserWarning): pass + _big_endian = False WAVE_FORMAT_PCM = 0x0001 WAVE_FORMAT_IEEE_FLOAT = 0x0003 -WAVE_FORMAT_EXTENSIBLE = 0xfffe +WAVE_FORMAT_EXTENSIBLE = 0xFFFE KNOWN_WAVE_FORMATS = (WAVE_FORMAT_PCM, WAVE_FORMAT_IEEE_FLOAT) # assumes file pointer is immediately @@ -33,10 +35,10 @@ KNOWN_WAVE_FORMATS = (WAVE_FORMAT_PCM, WAVE_FORMAT_IEEE_FLOAT) def _read_fmt_chunk(fid): if _big_endian: - fmt = '>' + fmt = ">" else: - fmt = '<' - res = struct.unpack(fmt+'iHHIIHH',fid.read(20)) + fmt = "<" + res = struct.unpack(fmt + "iHHIIHH", fid.read(20)) size, comp, noc, rate, sbytes, ba, bits = res if comp not in KNOWN_WAVE_FORMATS or size > 16: comp = WAVE_FORMAT_PCM @@ -51,41 +53,42 @@ def _read_fmt_chunk(fid): # after the 'data' id def _read_data_chunk(fid, comp, noc, bits, mmap=False): if _big_endian: - fmt = '>i' + fmt = ">i" else: - fmt = ' 1: - data = data.reshape(-1,noc) + data = data.reshape(-1, noc) return data def _skip_unknown_chunk(fid): if _big_endian: - fmt = '>i' + fmt = ">i" else: - fmt = '' or (data.dtype.byteorder == '=' and sys.byteorder == 'big'): + fid.write(b"data") + fid.write(struct.pack("" or ( + data.dtype.byteorder == "=" and sys.byteorder == "big" + ): data = data.byteswap() _array_tofile(fid, data) @@ -273,19 +286,22 @@ def write(filename, rate, data): # position at start of the file (replacing the 4 bytes of zeros) size = fid.tell() fid.seek(4) - fid.write(struct.pack('= 3: + def _array_tofile(fid, data): # ravel gives a c-contiguous buffer - fid.write(data.ravel().view('b').data) + fid.write(data.ravel().view("b").data) + else: + def _array_tofile(fid, data): fid.write(data.tostring()) diff --git a/run_batdetect.py b/run_batdetect.py index 9655d45..f9d96ab 100644 --- a/run_batdetect.py +++ b/run_batdetect.py @@ -1,67 +1,115 @@ -import os import argparse +import os + import bat_detect.utils.detector_utils as du def main(args): - print('Loading model: ' + args['model_path']) - model, params = du.load_model(args['model_path']) + print("Loading model: " + args["model_path"]) + model, params = du.load_model(args["model_path"]) - print('\nInput directory: ' + args['audio_dir']) - files = du.get_audio_files(args['audio_dir']) - print('Number of audio files: {}'.format(len(files))) - print('\nSaving results to: ' + args['ann_dir']) + print("\nInput directory: " + args["audio_dir"]) + files = du.get_audio_files(args["audio_dir"]) + print("Number of audio files: {}".format(len(files))) + print("\nSaving results to: " + args["ann_dir"]) # process files error_files = [] for ii, audio_file in enumerate(files): - print('\n' + str(ii).ljust(6) + os.path.basename(audio_file)) + print("\n" + str(ii).ljust(6) + os.path.basename(audio_file)) try: results = du.process_file(audio_file, model, params, args) - if args['save_preds_if_empty'] or (len(results['pred_dict']['annotation']) > 0): - results_path = audio_file.replace(args['audio_dir'], args['ann_dir']) + if args["save_preds_if_empty"] or ( + len(results["pred_dict"]["annotation"]) > 0 + ): + results_path = audio_file.replace( + args["audio_dir"], args["ann_dir"] + ) du.save_results_to_file(results, results_path) except: error_files.append(audio_file) print("Error processing file!") - print('\nResults saved to: ' + args['ann_dir']) + print("\nResults saved to: " + args["ann_dir"]) if len(error_files) > 0: - print('\nUnable to process the follow files:') + print("\nUnable to process the follow files:") for err in error_files: - print(' ' + err) + print(" " + err) if __name__ == "__main__": - info_str = '\nBatDetect2 - Detection and Classification\n' + \ - ' Assumes audio files are mono, not stereo.\n' + \ - ' Spaces in the input paths will throw an error. Wrap in quotes "".\n' + \ - ' Input files should be short in duration e.g. < 30 seconds.\n' + info_str = ( + "\nBatDetect2 - Detection and Classification\n" + + " Assumes audio files are mono, not stereo.\n" + + ' Spaces in the input paths will throw an error. Wrap in quotes "".\n' + + " Input files should be short in duration e.g. < 30 seconds.\n" + ) print(info_str) parser = argparse.ArgumentParser() - parser.add_argument('audio_dir', type=str, help='Input directory for audio') - parser.add_argument('ann_dir', type=str, help='Output directory for where the predictions will be stored') - parser.add_argument('detection_threshold', type=float, help='Cut-off probability for detector e.g. 0.1') - parser.add_argument('--cnn_features', action='store_true', default=False, dest='cnn_features', - help='Extracts CNN call features') - parser.add_argument('--spec_features', action='store_true', default=False, dest='spec_features', - help='Extracts low level call features') - parser.add_argument('--time_expansion_factor', type=int, default=1, dest='time_expansion_factor', - help='The time expansion factor used for all files (default is 1)') - parser.add_argument('--quiet', action='store_true', default=False, dest='quiet', - help='Minimize output printing') - parser.add_argument('--save_preds_if_empty', action='store_true', default=False, dest='save_preds_if_empty', - help='Save empty annotation file if no detections made.') - parser.add_argument('--model_path', type=str, default='models/Net2DFast_UK_same.pth.tar', - help='Path to trained BatDetect2 model') + parser.add_argument( + "audio_dir", type=str, help="Input directory for audio" + ) + parser.add_argument( + "ann_dir", + type=str, + help="Output directory for where the predictions will be stored", + ) + parser.add_argument( + "detection_threshold", + type=float, + help="Cut-off probability for detector e.g. 0.1", + ) + parser.add_argument( + "--cnn_features", + action="store_true", + default=False, + dest="cnn_features", + help="Extracts CNN call features", + ) + parser.add_argument( + "--spec_features", + action="store_true", + default=False, + dest="spec_features", + help="Extracts low level call features", + ) + parser.add_argument( + "--time_expansion_factor", + type=int, + default=1, + dest="time_expansion_factor", + help="The time expansion factor used for all files (default is 1)", + ) + parser.add_argument( + "--quiet", + action="store_true", + default=False, + dest="quiet", + help="Minimize output printing", + ) + parser.add_argument( + "--save_preds_if_empty", + action="store_true", + default=False, + dest="save_preds_if_empty", + help="Save empty annotation file if no detections made.", + ) + parser.add_argument( + "--model_path", + type=str, + default="models/Net2DFast_UK_same.pth.tar", + help="Path to trained BatDetect2 model", + ) args = vars(parser.parse_args()) - args['spec_slices'] = False # used for visualization - args['chunk_size'] = 2 # if files greater than this amount (seconds) they will be broken down into small chunks - args['ann_dir'] = os.path.join(args['ann_dir'], '') + args["spec_slices"] = False # used for visualization + args[ + "chunk_size" + ] = 2 # if files greater than this amount (seconds) they will be broken down into small chunks + args["ann_dir"] = os.path.join(args["ann_dir"], "") main(args) diff --git a/scripts/gen_dataset_summary_image.py b/scripts/gen_dataset_summary_image.py index b789584..cb823d6 100644 --- a/scripts/gen_dataset_summary_image.py +++ b/scripts/gen_dataset_summary_image.py @@ -3,62 +3,97 @@ Loads a set of annotations corresponding to a dataset and saves an image which is the mean spectrogram for each class. """ +import argparse +import os +import sys + import matplotlib.pyplot as plt import numpy as np -import os -import argparse -import sys import viz_helpers as vz -sys.path.append(os.path.join('..')) -import bat_detect.train.train_utils as tu +sys.path.append(os.path.join("..")) import bat_detect.detector.parameters as parameters -import bat_detect.utils.audio_utils as au import bat_detect.train.train_split as ts - +import bat_detect.train.train_utils as tu +import bat_detect.utils.audio_utils as au if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('audio_path', type=str, help='Input directory for audio') - parser.add_argument('op_dir', type=str, - help='Path to where single annotation json file is stored') - parser.add_argument('--ann_file', type=str, - help='Path to where single annotation json file is stored') - parser.add_argument('--uk_split', type=str, default='', - help='Set as: diff or same') - parser.add_argument('--file_type', type=str, default='png', - help='Type of image to save png or pdf') + parser.add_argument( + "audio_path", type=str, help="Input directory for audio" + ) + parser.add_argument( + "op_dir", + type=str, + help="Path to where single annotation json file is stored", + ) + parser.add_argument( + "--ann_file", + type=str, + help="Path to where single annotation json file is stored", + ) + parser.add_argument( + "--uk_split", type=str, default="", help="Set as: diff or same" + ) + parser.add_argument( + "--file_type", + type=str, + default="png", + help="Type of image to save png or pdf", + ) args = vars(parser.parse_args()) - if not os.path.isdir(args['op_dir']): - os.makedirs(args['op_dir']) + if not os.path.isdir(args["op_dir"]): + os.makedirs(args["op_dir"]) params = parameters.get_params(False) - params['smooth_spec'] = False - params['spec_width'] = 48 - params['norm_type'] = 'log' # log, pcen - params['aud_pad'] = 0.005 - classes_to_ignore = params['classes_to_ignore'] + params['generic_class'] - + params["smooth_spec"] = False + params["spec_width"] = 48 + params["norm_type"] = "log" # log, pcen + params["aud_pad"] = 0.005 + classes_to_ignore = params["classes_to_ignore"] + params["generic_class"] # load train annotations - if args['uk_split'] == '': - print('\nLoading:', args['ann_file'], '\n') - dataset_name = os.path.basename(args['ann_file']).replace('.json', '') + if args["uk_split"] == "": + print("\nLoading:", args["ann_file"], "\n") + dataset_name = os.path.basename(args["ann_file"]).replace(".json", "") datasets = [] - datasets.append(tu.get_blank_dataset_dict(dataset_name, False, args['ann_file'], args['audio_path'])) + datasets.append( + tu.get_blank_dataset_dict( + dataset_name, False, args["ann_file"], args["audio_path"] + ) + ) else: # load uk data - special case - print('\nLoading:', args['uk_split'], '\n') - dataset_name = 'uk_' + args['uk_split'] # should be uk_diff, or uk_same - datasets, _ = ts.get_train_test_data(args['ann_file'], args['audio_path'], args['uk_split'], load_extra=False) + print("\nLoading:", args["uk_split"], "\n") + dataset_name = ( + "uk_" + args["uk_split"] + ) # should be uk_diff, or uk_same + datasets, _ = ts.get_train_test_data( + args["ann_file"], + args["audio_path"], + args["uk_split"], + load_extra=False, + ) - anns, class_names, _ = tu.load_set_of_anns(datasets, classes_to_ignore, params['events_of_interest']) + anns, class_names, _ = tu.load_set_of_anns( + datasets, classes_to_ignore, params["events_of_interest"] + ) class_names_order = range(len(class_names)) - x_train, y_train = vz.load_data(anns, params, class_names, smooth_spec=params['smooth_spec'], norm_type=params['norm_type']) + x_train, y_train = vz.load_data( + anns, + params, + class_names, + smooth_spec=params["smooth_spec"], + norm_type=params["norm_type"], + ) - op_file_name = os.path.join(args['op_dir'], dataset_name + '.' + args['file_type']) - vz.save_summary_image(x_train, y_train, class_names, params, op_file_name, class_names_order) - print('\nImage saved to:', op_file_name) + op_file_name = os.path.join( + args["op_dir"], dataset_name + "." + args["file_type"] + ) + vz.save_summary_image( + x_train, y_train, class_names, params, op_file_name, class_names_order + ) + print("\nImage saved to:", op_file_name) diff --git a/scripts/gen_spec_image.py b/scripts/gen_spec_image.py index 11f76de..3d4cffa 100644 --- a/scripts/gen_spec_image.py +++ b/scripts/gen_spec_image.py @@ -7,24 +7,27 @@ Will save images with: 3) spectrogram with predicted boxes """ -import numpy as np -import sys -import os import argparse -import matplotlib.pyplot as plt import json +import os +import sys -sys.path.append(os.path.join('..')) +import matplotlib.pyplot as plt +import numpy as np + +sys.path.append(os.path.join("..")) import bat_detect.evaluate.evaluate_models as evlm +import bat_detect.utils.audio_utils as au import bat_detect.utils.detector_utils as du import bat_detect.utils.plot_utils as viz -import bat_detect.utils.audio_utils as au def filter_anns(anns, start_time, stop_time): anns_op = [] for aa in anns: - if (aa['start_time'] >= start_time) and (aa['start_time'] < stop_time-0.02): + if (aa["start_time"] >= start_time) and ( + aa["start_time"] < stop_time - 0.02 + ): anns_op.append(aa) return anns_op @@ -32,85 +35,172 @@ def filter_anns(anns, start_time, stop_time): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('audio_file', type=str, help='Path to audio file') - parser.add_argument('model_path', type=str, help='Path to BatDetect model') - parser.add_argument('--ann_file', type=str, default='', help='Path to annotation file') - parser.add_argument('--op_dir', type=str, default='plots/', - help='Output directory for plots') - parser.add_argument('--file_type', type=str, default='png', - help='Type of image to save png or pdf') - parser.add_argument('--title_text', type=str, default='', - help='Text to add as title of plots') - parser.add_argument('--detection_threshold', type=float, default=0.2, - help='Threshold for output detections') - parser.add_argument('--start_time', type=float, default=0.0, - help='Start time for cropped file') - parser.add_argument('--stop_time', type=float, default=0.5, - help='End time for cropped file') - parser.add_argument('--time_expansion_factor', type=int, default=1, - help='Time expansion factor') - + parser.add_argument("audio_file", type=str, help="Path to audio file") + parser.add_argument("model_path", type=str, help="Path to BatDetect model") + parser.add_argument( + "--ann_file", type=str, default="", help="Path to annotation file" + ) + parser.add_argument( + "--op_dir", + type=str, + default="plots/", + help="Output directory for plots", + ) + parser.add_argument( + "--file_type", + type=str, + default="png", + help="Type of image to save png or pdf", + ) + parser.add_argument( + "--title_text", + type=str, + default="", + help="Text to add as title of plots", + ) + parser.add_argument( + "--detection_threshold", + type=float, + default=0.2, + help="Threshold for output detections", + ) + parser.add_argument( + "--start_time", + type=float, + default=0.0, + help="Start time for cropped file", + ) + parser.add_argument( + "--stop_time", + type=float, + default=0.5, + help="End time for cropped file", + ) + parser.add_argument( + "--time_expansion_factor", + type=int, + default=1, + help="Time expansion factor", + ) + args_cmd = vars(parser.parse_args()) - - # load the model + + # load the model bd_args = du.get_default_bd_args() - model, params_bd = du.load_model(args_cmd['model_path']) - bd_args['detection_threshold'] = args_cmd['detection_threshold'] - bd_args['time_expansion_factor'] = args_cmd['time_expansion_factor'] - + model, params_bd = du.load_model(args_cmd["model_path"]) + bd_args["detection_threshold"] = args_cmd["detection_threshold"] + bd_args["time_expansion_factor"] = args_cmd["time_expansion_factor"] + # load the annotation if it exists gt_present = False - if args_cmd['ann_file'] != '': - if os.path.isfile(args_cmd['ann_file']): - with open(args_cmd['ann_file']) as da: + if args_cmd["ann_file"] != "": + if os.path.isfile(args_cmd["ann_file"]): + with open(args_cmd["ann_file"]) as da: gt_anns = json.load(da) - gt_anns = filter_anns(gt_anns['annotation'], args_cmd['start_time'], args_cmd['stop_time']) + gt_anns = filter_anns( + gt_anns["annotation"], + args_cmd["start_time"], + args_cmd["stop_time"], + ) gt_present = True else: - print('Annotation file not found: ', args_cmd['ann_file']) + print("Annotation file not found: ", args_cmd["ann_file"]) # load the audio file - if not os.path.isfile(args_cmd['audio_file']): - print('Audio file not found: ', args_cmd['audio_file']) + if not os.path.isfile(args_cmd["audio_file"]): + print("Audio file not found: ", args_cmd["audio_file"]) sys.exit() - + # load audio and crop - print('\nProcessing: ' + os.path.basename(args_cmd['audio_file'])) - print('\nOutput directory: ' + args_cmd['op_dir']) - sampling_rate, audio = au.load_audio_file(args_cmd['audio_file'], args_cmd['time_exp'], - params_bd['target_samp_rate'], params_bd['scale_raw_audio']) - st_samp = int(sampling_rate*args_cmd['start_time']) - en_samp = int(sampling_rate*args_cmd['stop_time']) + print("\nProcessing: " + os.path.basename(args_cmd["audio_file"])) + print("\nOutput directory: " + args_cmd["op_dir"]) + sampling_rate, audio = au.load_audio_file( + args_cmd["audio_file"], + args_cmd["time_exp"], + params_bd["target_samp_rate"], + params_bd["scale_raw_audio"], + ) + st_samp = int(sampling_rate * args_cmd["start_time"]) + en_samp = int(sampling_rate * args_cmd["stop_time"]) if en_samp > audio.shape[0]: - audio = np.hstack((audio, np.zeros((en_samp) - audio.shape[0], dtype=audio.dtype))) + audio = np.hstack( + (audio, np.zeros((en_samp) - audio.shape[0], dtype=audio.dtype)) + ) audio = audio[st_samp:en_samp] duration = audio.shape[0] / sampling_rate - print('File duration: {} seconds'.format(duration)) + print("File duration: {} seconds".format(duration)) # create spec for viz - spec, _ = au.generate_spectrogram(audio, sampling_rate, params_bd, True, False) + spec, _ = au.generate_spectrogram( + audio, sampling_rate, params_bd, True, False + ) # run model and filter detections so only keep ones in relevant time range - results = du.process_file(args_cmd['audio_file'], model, params_bd, bd_args) - pred_anns = filter_anns(results['pred_dict']['annotation'], args_cmd['start_time'], args_cmd['stop_time']) - print(len(pred_anns), 'Detections') + results = du.process_file( + args_cmd["audio_file"], model, params_bd, bd_args + ) + pred_anns = filter_anns( + results["pred_dict"]["annotation"], + args_cmd["start_time"], + args_cmd["stop_time"], + ) + print(len(pred_anns), "Detections") # save output - if not os.path.isdir(args_cmd['op_dir']): - os.makedirs(args_cmd['op_dir']) - + if not os.path.isdir(args_cmd["op_dir"]): + os.makedirs(args_cmd["op_dir"]) + # create output file names - op_path_clean = os.path.basename(args_cmd['audio_file'])[:-4] + '_clean.' + args_cmd['file_type'] - op_path_clean = os.path.join(args_cmd['op_dir'], op_path_clean) - op_path_pred = os.path.basename(args_cmd['audio_file'])[:-4] + '_pred.' + args_cmd['file_type'] - op_path_pred = os.path.join(args_cmd['op_dir'], op_path_pred) + op_path_clean = ( + os.path.basename(args_cmd["audio_file"])[:-4] + + "_clean." + + args_cmd["file_type"] + ) + op_path_clean = os.path.join(args_cmd["op_dir"], op_path_clean) + op_path_pred = ( + os.path.basename(args_cmd["audio_file"])[:-4] + + "_pred." + + args_cmd["file_type"] + ) + op_path_pred = os.path.join(args_cmd["op_dir"], op_path_pred) # create and save iamges - viz.save_ann_spec(op_path_clean, spec, params_bd['min_freq'], params_bd['max_freq'], duration, args_cmd['start_time'], '', None) - viz.save_ann_spec(op_path_pred, spec, params_bd['min_freq'], params_bd['max_freq'], duration, args_cmd['start_time'], '', pred_anns) + viz.save_ann_spec( + op_path_clean, + spec, + params_bd["min_freq"], + params_bd["max_freq"], + duration, + args_cmd["start_time"], + "", + None, + ) + viz.save_ann_spec( + op_path_pred, + spec, + params_bd["min_freq"], + params_bd["max_freq"], + duration, + args_cmd["start_time"], + "", + pred_anns, + ) if gt_present: - op_path_gt = os.path.basename(args_cmd['audio_file'])[:-4] + '_gt.' + args_cmd['file_type'] - op_path_gt = os.path.join(args_cmd['op_dir'], op_path_gt) - viz.save_ann_spec(op_path_gt, spec, params_bd['min_freq'], params_bd['max_freq'], duration, args_cmd['start_time'], '', gt_anns) + op_path_gt = ( + os.path.basename(args_cmd["audio_file"])[:-4] + + "_gt." + + args_cmd["file_type"] + ) + op_path_gt = os.path.join(args_cmd["op_dir"], op_path_gt) + viz.save_ann_spec( + op_path_gt, + spec, + params_bd["min_freq"], + params_bd["max_freq"], + duration, + args_cmd["start_time"], + "", + gt_anns, + ) diff --git a/scripts/gen_spec_video.py b/scripts/gen_spec_video.py index cccfcf8..3c055ec 100644 --- a/scripts/gen_spec_video.py +++ b/scripts/gen_spec_video.py @@ -8,57 +8,83 @@ Notes: Best to use system one - see ffmpeg_path. """ -from scipy.io import wavfile +import argparse import os import shutil +import sys + import matplotlib.pyplot as plt import numpy as np -import argparse +from scipy.io import wavfile -import sys -sys.path.append(os.path.join('..')) +sys.path.append(os.path.join("..")) import bat_detect.detector.parameters as parameters import bat_detect.utils.audio_utils as au -import bat_detect.utils.plot_utils as viz import bat_detect.utils.detector_utils as du - +import bat_detect.utils.plot_utils as viz if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('audio_file', type=str, help='Path to input audio file') - parser.add_argument('model_path', type=str, help='Path to trained BatDetect model') - parser.add_argument('--op_dir', type=str, default='generated_vids/', help='Path to output directory') - parser.add_argument('--no_detector', action='store_true', help='Do not run detector') - parser.add_argument('--plot_class_names_off', action='store_true', help='Do not plot class names') - parser.add_argument('--disable_axis', action='store_true', help='Do not plot axis') - parser.add_argument('--detection_threshold', type=float, default=0.2, help='Cut-off probability for detector') - parser.add_argument('--time_expansion_factor', type=int, default=1, dest='time_expansion_factor', - help='The time expansion factor used for all files (default is 1)') + parser.add_argument( + "audio_file", type=str, help="Path to input audio file" + ) + parser.add_argument( + "model_path", type=str, help="Path to trained BatDetect model" + ) + parser.add_argument( + "--op_dir", + type=str, + default="generated_vids/", + help="Path to output directory", + ) + parser.add_argument( + "--no_detector", action="store_true", help="Do not run detector" + ) + parser.add_argument( + "--plot_class_names_off", + action="store_true", + help="Do not plot class names", + ) + parser.add_argument( + "--disable_axis", action="store_true", help="Do not plot axis" + ) + parser.add_argument( + "--detection_threshold", + type=float, + default=0.2, + help="Cut-off probability for detector", + ) + parser.add_argument( + "--time_expansion_factor", + type=int, + default=1, + dest="time_expansion_factor", + help="The time expansion factor used for all files (default is 1)", + ) args_cmd = vars(parser.parse_args()) # file of interest - audio_file = args_cmd['audio_file'] - op_dir = args_cmd['op_dir'] - op_str = '_output' - ffmpeg_path = '/usr/bin/' + audio_file = args_cmd["audio_file"] + op_dir = args_cmd["op_dir"] + op_str = "_output" + ffmpeg_path = "/usr/bin/" if not os.path.isfile(audio_file): - print('Audio file not found: ', audio_file) + print("Audio file not found: ", audio_file) sys.exit() - if not os.path.isfile(args_cmd['model_path']): - print('Model not found: ', model_path) + if not os.path.isfile(args_cmd["model_path"]): + print("Model not found: ", model_path) sys.exit() - start_time = 0.0 duration = 0.5 reveal_boxes = True # makes the boxes appear one at a time fps = 24 dpi = 100 - op_dir_tmp = os.path.join(op_dir, 'op_tmp_vids', '') + op_dir_tmp = os.path.join(op_dir, "op_tmp_vids", "") if not os.path.isdir(op_dir_tmp): os.makedirs(op_dir_tmp) if not os.path.isdir(op_dir): @@ -66,105 +92,176 @@ if __name__ == "__main__": params = parameters.get_params(False) args = du.get_default_bd_args() - args['time_expansion_factor'] = args_cmd['time_expansion_factor'] - args['detection_threshold'] = args_cmd['detection_threshold'] - + args["time_expansion_factor"] = args_cmd["time_expansion_factor"] + args["detection_threshold"] = args_cmd["detection_threshold"] # load audio file - print('\nProcessing: ' + os.path.basename(audio_file)) - print('\nOutput directory: ' + op_dir) - sampling_rate, audio = au.load_audio_file(audio_file, args['time_expansion_factor'], params['target_samp_rate']) - audio = audio[int(sampling_rate*start_time):int(sampling_rate*start_time + sampling_rate*duration)] + print("\nProcessing: " + os.path.basename(audio_file)) + print("\nOutput directory: " + op_dir) + sampling_rate, audio = au.load_audio_file( + audio_file, args["time_expansion_factor"], params["target_samp_rate"] + ) + audio = audio[ + int(sampling_rate * start_time) : int( + sampling_rate * start_time + sampling_rate * duration + ) + ] audio_orig = audio.copy() - audio = au.pad_audio(audio, sampling_rate, params['fft_win_length'], - params['fft_overlap'], params['resize_factor'], - params['spec_divide_factor']) + audio = au.pad_audio( + audio, + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + params["resize_factor"], + params["spec_divide_factor"], + ) # generate spectrogram spec, _ = au.generate_spectrogram(audio, sampling_rate, params, True) - max_val = spec.max()*1.1 + max_val = spec.max() * 1.1 - - if not args_cmd['no_detector']: - print(' Loading model and running detector on entire file ...') - model, det_params = du.load_model(args_cmd['model_path']) - det_params['detection_threshold'] = args['detection_threshold'] + if not args_cmd["no_detector"]: + print(" Loading model and running detector on entire file ...") + model, det_params = du.load_model(args_cmd["model_path"]) + det_params["detection_threshold"] = args["detection_threshold"] results = du.process_file(audio_file, model, det_params, args) - print(' Processing detections and plotting ...') + print(" Processing detections and plotting ...") detections = [] - for bb in results['pred_dict']['annotation']: - if (bb['start_time'] >= start_time) and (bb['end_time'] < start_time+duration): + for bb in results["pred_dict"]["annotation"]: + if (bb["start_time"] >= start_time) and ( + bb["end_time"] < start_time + duration + ): detections.append(bb) # plot boxes - fig = plt.figure(1, figsize=(spec.shape[1]/dpi, spec.shape[0]/dpi), dpi=dpi) - duration = au.x_coords_to_time(spec.shape[1], sampling_rate, params['fft_win_length'], params['fft_overlap']) - viz.create_box_image(spec, fig, detections, start_time, start_time+duration, duration, params, max_val, - plot_class_names=not args_cmd['plot_class_names_off']) - op_im_file_boxes = os.path.join(op_dir, os.path.basename(audio_file)[:-4] + op_str + '_boxes.png') + fig = plt.figure( + 1, figsize=(spec.shape[1] / dpi, spec.shape[0] / dpi), dpi=dpi + ) + duration = au.x_coords_to_time( + spec.shape[1], + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + ) + viz.create_box_image( + spec, + fig, + detections, + start_time, + start_time + duration, + duration, + params, + max_val, + plot_class_names=not args_cmd["plot_class_names_off"], + ) + op_im_file_boxes = os.path.join( + op_dir, os.path.basename(audio_file)[:-4] + op_str + "_boxes.png" + ) fig.savefig(op_im_file_boxes, dpi=dpi) plt.close(1) spec_with_boxes = plt.imread(op_im_file_boxes) - - print(' Saving audio file ...') - if args['time_expansion_factor']==1: - sampling_rate_op = int(sampling_rate/10.0) + print(" Saving audio file ...") + if args["time_expansion_factor"] == 1: + sampling_rate_op = int(sampling_rate / 10.0) else: sampling_rate_op = sampling_rate - op_audio_file = os.path.join(op_dir, os.path.basename(audio_file)[:-4] + op_str + '.wav') + op_audio_file = os.path.join( + op_dir, os.path.basename(audio_file)[:-4] + op_str + ".wav" + ) wavfile.write(op_audio_file, sampling_rate_op, audio_orig) - - print(' Saving image ...') - op_im_file = os.path.join(op_dir, os.path.basename(audio_file)[:-4] + op_str + '.png') - plt.imsave(op_im_file, spec, vmin=0, vmax=max_val, cmap='plasma') + print(" Saving image ...") + op_im_file = os.path.join( + op_dir, os.path.basename(audio_file)[:-4] + op_str + ".png" + ) + plt.imsave(op_im_file, spec, vmin=0, vmax=max_val, cmap="plasma") spec_blank = plt.imread(op_im_file) # create figure freq_scale = 1000 # turn Hz to kHz - min_freq = params['min_freq']//freq_scale - max_freq = params['max_freq']//freq_scale + min_freq = params["min_freq"] // freq_scale + max_freq = params["max_freq"] // freq_scale y_extent = [0, duration, min_freq, max_freq] - print(' Saving video frames ...') + print(" Saving video frames ...") # save images that will be combined into video # will either plot with or without boxes - for ii, col in enumerate(np.linspace(0, spec.shape[1]-1, int(fps*duration*10))): - if not args_cmd['no_detector']: + for ii, col in enumerate( + np.linspace(0, spec.shape[1] - 1, int(fps * duration * 10)) + ): + if not args_cmd["no_detector"]: spec_op = spec_with_boxes.copy() if ii > 0: spec_op[:, int(col), :] = 1.0 if reveal_boxes: - spec_op[:, int(col)+1:, :] = spec_blank[:, int(col)+1:, :] + spec_op[:, int(col) + 1 :, :] = spec_blank[ + :, int(col) + 1 :, : + ] elif ii == 0 and reveal_boxes: spec_op = spec_blank - if not args_cmd['disable_axis']: - plt.close('all') - fig = plt.figure(ii, figsize=(1.2*(spec_op.shape[1]/dpi), 1.5*(spec_op.shape[0]/dpi)), dpi=dpi) - plt.xlabel('Time - seconds') - plt.ylabel('Frequency - kHz') - plt.imshow(spec_op, vmin=0, vmax=1.0, cmap='plasma', extent=y_extent, aspect='auto') + if not args_cmd["disable_axis"]: + plt.close("all") + fig = plt.figure( + ii, + figsize=( + 1.2 * (spec_op.shape[1] / dpi), + 1.5 * (spec_op.shape[0] / dpi), + ), + dpi=dpi, + ) + plt.xlabel("Time - seconds") + plt.ylabel("Frequency - kHz") + plt.imshow( + spec_op, + vmin=0, + vmax=1.0, + cmap="plasma", + extent=y_extent, + aspect="auto", + ) plt.tight_layout() - fig.savefig(op_dir_tmp + str(ii).zfill(4) + '.png', dpi=dpi) + fig.savefig(op_dir_tmp + str(ii).zfill(4) + ".png", dpi=dpi) else: - plt.imsave(op_dir_tmp + str(ii).zfill(4) + '.png', spec_op, vmin=0, vmax=1.0, cmap='plasma') + plt.imsave( + op_dir_tmp + str(ii).zfill(4) + ".png", + spec_op, + vmin=0, + vmax=1.0, + cmap="plasma", + ) else: spec_op = spec.copy() if ii > 0: spec_op[:, int(col)] = max_val - plt.imsave(op_dir_tmp + str(ii).zfill(4) + '.png', spec_op, vmin=0, vmax=max_val, cmap='plasma') + plt.imsave( + op_dir_tmp + str(ii).zfill(4) + ".png", + spec_op, + vmin=0, + vmax=max_val, + cmap="plasma", + ) - - print(' Creating video ...') - op_vid_file = os.path.join(op_dir, os.path.basename(audio_file)[:-4] + op_str + '.avi') - ffmpeg_cmd = 'ffmpeg -hide_banner -loglevel panic -y -r {} -f image2 -s {}x{} -i {}%04d.png -i {} -vcodec libx264 ' \ - '-crf 25 -pix_fmt yuv420p -acodec copy {}'.format(fps, spec.shape[1], spec.shape[0], op_dir_tmp, op_audio_file, op_vid_file) + print(" Creating video ...") + op_vid_file = os.path.join( + op_dir, os.path.basename(audio_file)[:-4] + op_str + ".avi" + ) + ffmpeg_cmd = ( + "ffmpeg -hide_banner -loglevel panic -y -r {} -f image2 -s {}x{} -i {}%04d.png -i {} -vcodec libx264 " + "-crf 25 -pix_fmt yuv420p -acodec copy {}".format( + fps, + spec.shape[1], + spec.shape[0], + op_dir_tmp, + op_audio_file, + op_vid_file, + ) + ) ffmpeg_cmd = ffmpeg_path + ffmpeg_cmd os.system(ffmpeg_cmd) - print(' Deleting temporary files ...') + print(" Deleting temporary files ...") if os.path.isdir(op_dir_tmp): - shutil.rmtree(op_dir_tmp) + shutil.rmtree(op_dir_tmp) diff --git a/scripts/viz_helpers.py b/scripts/viz_helpers.py index 2f55836..667bb9c 100644 --- a/scripts/viz_helpers.py +++ b/scripts/viz_helpers.py @@ -1,41 +1,70 @@ -import numpy as np -import matplotlib.pyplot as plt -from scipy import ndimage import os import sys -sys.path.append(os.path.join('..')) + +import matplotlib.pyplot as plt +import numpy as np +from scipy import ndimage + +sys.path.append(os.path.join("..")) import bat_detect.utils.audio_utils as au -def generate_spectrogram_data(audio, sampling_rate, params, norm_type='log', smooth_spec=False): - max_freq = round(params['max_freq']*params['fft_win_length']) - min_freq = round(params['min_freq']*params['fft_win_length']) +def generate_spectrogram_data( + audio, sampling_rate, params, norm_type="log", smooth_spec=False +): + max_freq = round(params["max_freq"] * params["fft_win_length"]) + min_freq = round(params["min_freq"] * params["fft_win_length"]) # create spectrogram - numpy - spec = au.gen_mag_spectrogram(audio, sampling_rate, params['fft_win_length'], params['fft_overlap']) - #spec = au.gen_mag_spectrogram_pt(audio, sampling_rate, params['fft_win_length'], params['fft_overlap']).numpy() + spec = au.gen_mag_spectrogram( + audio, sampling_rate, params["fft_win_length"], params["fft_overlap"] + ) + # spec = au.gen_mag_spectrogram_pt(audio, sampling_rate, params['fft_win_length'], params['fft_overlap']).numpy() if spec.shape[0] < max_freq: freq_pad = max_freq - spec.shape[0] - spec = np.vstack((np.zeros((freq_pad, spec.shape[1]), dtype=np.float32), spec)) - spec = spec[-max_freq:spec.shape[0]-min_freq, :] + spec = np.vstack( + (np.zeros((freq_pad, spec.shape[1]), dtype=np.float32), spec) + ) + spec = spec[-max_freq : spec.shape[0] - min_freq, :] - if norm_type == 'log': - log_scaling = 2.0 * (1.0 / sampling_rate) * (1.0/(np.abs(np.hanning(int(params['fft_win_length']*sampling_rate)))**2).sum()) + if norm_type == "log": + log_scaling = ( + 2.0 + * (1.0 / sampling_rate) + * ( + 1.0 + / ( + np.abs( + np.hanning( + int(params["fft_win_length"] * sampling_rate) + ) + ) + ** 2 + ).sum() + ) + ) ##log_scaling = 0.01 - spec = np.log(1.0 + log_scaling*spec).astype(np.float32) - elif norm_type == 'pcen': + spec = np.log(1.0 + log_scaling * spec).astype(np.float32) + elif norm_type == "pcen": spec = au.pcen(spec, sampling_rate) else: pass if smooth_spec: - spec = ndimage.gaussian_filter(spec, 1) + spec = ndimage.gaussian_filter(spec, 1) return spec -def load_data(anns, params, class_names, smooth_spec=False, norm_type='log', extract_bg=False): +def load_data( + anns, + params, + class_names, + smooth_spec=False, + norm_type="log", + extract_bg=False, +): specs = [] labels = [] coords = [] @@ -43,67 +72,106 @@ def load_data(anns, params, class_names, smooth_spec=False, norm_type='log', ext sampling_rates = [] file_names = [] for cur_file in anns: - sampling_rate, audio_orig = au.load_audio_file(cur_file['file_path'], cur_file['time_exp'], - params['target_samp_rate'], params['scale_raw_audio']) + sampling_rate, audio_orig = au.load_audio_file( + cur_file["file_path"], + cur_file["time_exp"], + params["target_samp_rate"], + params["scale_raw_audio"], + ) - for ann in cur_file['annotation']: - if ann['class'] not in params['classes_to_ignore'] and ann['class'] in class_names: + for ann in cur_file["annotation"]: + if ( + ann["class"] not in params["classes_to_ignore"] + and ann["class"] in class_names + ): # clip out of bounds - if ann['low_freq'] < params['min_freq']: - ann['low_freq'] = params['min_freq'] - if ann['high_freq'] > params['max_freq']: - ann['high_freq'] = params['max_freq'] + if ann["low_freq"] < params["min_freq"]: + ann["low_freq"] = params["min_freq"] + if ann["high_freq"] > params["max_freq"]: + ann["high_freq"] = params["max_freq"] # load cropped audio - start_samp_diff = int(sampling_rate*ann['start_time']) - int(sampling_rate*params['aud_pad']) + start_samp_diff = int(sampling_rate * ann["start_time"]) - int( + sampling_rate * params["aud_pad"] + ) start_samp = np.maximum(0, start_samp_diff) - end_samp = np.minimum(audio_orig.shape[0], int(sampling_rate*ann['end_time'])*2 + int(sampling_rate*params['aud_pad'])) + end_samp = np.minimum( + audio_orig.shape[0], + int(sampling_rate * ann["end_time"]) * 2 + + int(sampling_rate * params["aud_pad"]), + ) audio = audio_orig[start_samp:end_samp] if start_samp_diff < 0: # need to pad at start if the call is at the very begining - audio = np.hstack((np.zeros(-start_samp_diff, dtype=np.float32), audio)) + audio = np.hstack( + (np.zeros(-start_samp_diff, dtype=np.float32), audio) + ) - nfft = int(params['fft_win_length']*sampling_rate) - noverlap = int(params['fft_overlap']*nfft) - max_samps = params['spec_width']*(nfft - noverlap) + noverlap + nfft = int(params["fft_win_length"] * sampling_rate) + noverlap = int(params["fft_overlap"] * nfft) + max_samps = params["spec_width"] * (nfft - noverlap) + noverlap if max_samps > audio.shape[0]: - audio = np.hstack((audio, np.zeros(max_samps - audio.shape[0]))) + audio = np.hstack( + (audio, np.zeros(max_samps - audio.shape[0])) + ) audio = audio[:max_samps].astype(np.float32) - audio = au.pad_audio(audio, sampling_rate, params['fft_win_length'], - params['fft_overlap'], params['resize_factor'], - params['spec_divide_factor']) + audio = au.pad_audio( + audio, + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + params["resize_factor"], + params["spec_divide_factor"], + ) # generate spectrogram - spec = generate_spectrogram_data(audio, sampling_rate, params, norm_type, smooth_spec)[:, :params['spec_width']] + spec = generate_spectrogram_data( + audio, sampling_rate, params, norm_type, smooth_spec + )[:, : params["spec_width"]] specs.append(spec[np.newaxis, ...]) - labels.append(ann['class']) + labels.append(ann["class"]) audios.append(audio) sampling_rates.append(sampling_rate) - file_names.append(cur_file['file_path']) + file_names.append(cur_file["file_path"]) # position in crop - x1 = int(au.time_to_x_coords(np.array(params['aud_pad']), sampling_rate, params['fft_win_length'], params['fft_overlap'])) - y1 = (ann['low_freq'] - params['min_freq']) * params['fft_win_length'] + x1 = int( + au.time_to_x_coords( + np.array(params["aud_pad"]), + sampling_rate, + params["fft_win_length"], + params["fft_overlap"], + ) + ) + y1 = (ann["low_freq"] - params["min_freq"]) * params[ + "fft_win_length" + ] coords.append((y1, x1)) - _, file_ids = np.unique(file_names, return_inverse=True) labels = np.array([class_names.index(ll) for ll in labels]) - #return np.vstack(specs), labels, coords, audios, sampling_rates, file_ids, file_names + # return np.vstack(specs), labels, coords, audios, sampling_rates, file_ids, file_names return np.vstack(specs), labels -def save_summary_image(specs, labels, species_names, params, op_file_name='plots/all_species.png', order=None): +def save_summary_image( + specs, + labels, + species_names, + params, + op_file_name="plots/all_species.png", + order=None, +): # takes the mean for each class and plots it on a grid mean_specs = [] max_band = [] for ii in range(len(species_names)): - inds = np.where(labels==ii)[0] + inds = np.where(labels == ii)[0] mu = specs[inds, :].mean(0) max_band.append(np.argmax(mu.sum(1))) mean_specs.append(mu) @@ -113,11 +181,21 @@ def save_summary_image(specs, labels, species_names, params, op_file_name='plots order = np.arange(len(species_names)) max_cols = 6 - nrows = int(np.ceil(len(species_names)/max_cols)) + nrows = int(np.ceil(len(species_names) / max_cols)) ncols = np.minimum(len(species_names), max_cols) - fig, ax = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*3.3, nrows*6), gridspec_kw = {'wspace':0, 'hspace':0.2}) - spec_min_max = (0, mean_specs[0].shape[1], params['min_freq']/1000, params['max_freq']/1000) + fig, ax = plt.subplots( + nrows=nrows, + ncols=ncols, + figsize=(ncols * 3.3, nrows * 6), + gridspec_kw={"wspace": 0, "hspace": 0.2}, + ) + spec_min_max = ( + 0, + mean_specs[0].shape[1], + params["min_freq"] / 1000, + params["max_freq"] / 1000, + ) ii = 0 for row in ax: @@ -126,17 +204,24 @@ def save_summary_image(specs, labels, species_names, params, op_file_name='plots for col in row: if ii >= len(species_names): - col.axis('off') + col.axis("off") else: - inds = np.where(labels==order[ii])[0] - col.imshow(mean_specs[order[ii]], extent=spec_min_max, cmap='plasma', aspect='equal') - col.grid(color='w', alpha=0.3, linewidth=0.3) + inds = np.where(labels == order[ii])[0] + col.imshow( + mean_specs[order[ii]], + extent=spec_min_max, + cmap="plasma", + aspect="equal", + ) + col.grid(color="w", alpha=0.3, linewidth=0.3) col.set_xticks([]) - col.title.set_text(str(ii+1) + ' ' + species_names[order[ii]]) - col.tick_params(axis='both', which='major', labelsize=7) + col.title.set_text( + str(ii + 1) + " " + species_names[order[ii]] + ) + col.tick_params(axis="both", which="major", labelsize=7) ii += 1 - #plt.tight_layout() - #plt.show() + # plt.tight_layout() + # plt.show() plt.savefig(op_file_name) - plt.close('all') + plt.close("all") From 4d6bf5e9e38e32312861776e2181013ef83af0e6 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Wed, 25 Jan 2023 19:19:28 +0000 Subject: [PATCH 2/2] Added formatting commit to ignoreRevs file --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..0fd091c --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Format code with Black and isort +3c17a2337166245de8df778fe174aad997e14e8f