diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9825bcf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.p text eol=lf +*.npy text eol=lf diff --git a/bat_eval/cnn_helpers.py b/bat_eval/cnn_helpers.py index 183c59b..d4f7901 100755 --- a/bat_eval/cnn_helpers.py +++ b/bat_eval/cnn_helpers.py @@ -11,10 +11,11 @@ def aligned_malloc(shape, dtype, alignment=16): alignment is required memory alignment in bytes """ itemsize = np.dtype(dtype).itemsize - extra = alignment / itemsize + extra = alignment // itemsize size = np.prod(shape) + #print(size, type(size), extra, type(extra)) buf = np.empty(size + extra, dtype=dtype) - ofs = (-buf.ctypes.data % alignment) / itemsize + ofs = (-buf.ctypes.data % alignment) // itemsize aa = buf[ofs:ofs+size].reshape(shape) assert (aa.ctypes.data % alignment) == 0 assert (aa.flags['C_CONTIGUOUS']) == True @@ -120,7 +121,7 @@ def fully_connected_as_corr(ip, filters, bias): """ # create DxHxsliding_width views of input - similar to corr2d - sliding_width = filters.shape[0] / np.prod(ip.shape[:2]) + sliding_width = filters.shape[0] // np.prod(ip.shape[:2]) op = view_as_windows(ip, (ip.shape[0],ip.shape[1],sliding_width)) op = op.reshape((np.prod(op.shape[:3]), np.prod(op.shape[3:]))) diff --git a/bat_eval/cpu_detection.py b/bat_eval/cpu_detection.py index 8a34452..6250cce 100755 --- a/bat_eval/cpu_detection.py +++ b/bat_eval/cpu_detection.py @@ -1,13 +1,12 @@ +import pickle +import warnings import numpy as np from scipy.ndimage import zoom from scipy.ndimage.filters import gaussian_filter1d -import cPickle as pickle -import time from spectrogram import Spectrogram import cnn_helpers as ch -import warnings warnings.simplefilter("ignore", UserWarning) try: @@ -32,8 +31,8 @@ def __init__(self, weight_file, params_file): params_file is the path to the network parameters """ - self.weights = np.load(weight_file) - if not all([weight.dtype==np.float32 for weight in self.weights]): + self.weights = np.load(weight_file, encoding='bytes', allow_pickle=True) + if not all([weight.dtype == np.float32 for weight in self.weights]): for i in range(self.weights.shape[0]): self.weights[i] = self.weights[i].astype(np.float32) @@ -99,10 +98,15 @@ def create_spec(self, audio, sampling_rate): max_freq=self.max_freq, min_freq=self.min_freq) hspec = self.sp.process_spectrogram(hspec, denoise_spec=self.denoise, smooth_spec=self.smooth_spec) - nsize = (np.ceil(hspec.shape[0]/2.0).astype(int), np.ceil(hspec.shape[1]/2.0).astype(int)) - spec = ch.aligned_malloc(nsize, np.float32) - - zoom(hspec, 0.5, output=spec, order=1) + #nsize = (np.ceil(hspec.shape[0]/2.0).astype(int), + # np.ceil(hspec.shape[1]/2.0).astype(int)) + nsize = zoom(hspec, 0.5, order=1).shape #dm edit + spec = ch.aligned_malloc(nsize, np.float32) + try: + zoom(hspec, 0.5, output=spec, order=1) + except Exception as e: + print(e, hspec.shape, nsize) + #print(spec, hspec, nsize,e) return spec @@ -138,7 +142,7 @@ def eval_network_1_dense(self, ip): prob = prob - np.amax(prob, axis=1, keepdims=True) prob = np.exp(prob) prob = prob[:, 1] / prob.sum(1) - prob = np.hstack((prob, np.zeros((ip.shape[1]/4)-prob.shape[0], dtype=np.float32))) + prob = np.hstack((prob, np.zeros((ip.shape[1]//4)-prob.shape[0], dtype=np.float32))) return prob @@ -169,7 +173,6 @@ def eval_network_2_dense(self, ip): prob = prob - np.amax(prob, axis=1, keepdims=True) prob = np.exp(prob) prob = prob[:, 1] / prob.sum(1) - prob = np.hstack((prob, np.zeros((ip.shape[1]/4)-prob.shape[0], dtype=np.float32))) + prob = np.hstack((prob, np.zeros((ip.shape[1]//4)-prob.shape[0], dtype=np.float32))) return prob - diff --git a/bat_eval/models/detector.npy b/bat_eval/models/detector.npy index 11315ee..57619a8 100644 Binary files a/bat_eval/models/detector.npy and b/bat_eval/models/detector.npy differ diff --git a/bat_eval/myskimage.py b/bat_eval/myskimage.py index 03d07c5..60559c7 100644 --- a/bat_eval/myskimage.py +++ b/bat_eval/myskimage.py @@ -6,7 +6,6 @@ were also copied to as dependencies of "gaussian" function. """ from __future__ import division -import numbers import collections as coll import numpy as np from scipy import ndimage as ndi @@ -41,7 +40,7 @@ def warn(msg): def img_as_float(image): - dtype=np.float32 + dtype = np.float32 force_copy = False """ diff --git a/bat_eval/nms_slow.py b/bat_eval/nms_slow.py index 12eb2a3..e2ce940 100644 --- a/bat_eval/nms_slow.py +++ b/bat_eval/nms_slow.py @@ -59,8 +59,8 @@ def test_nms(): pos, prob = nms_1d(y, win_size, y.shape[0]) pos_f, prob_f = nms_fast.nms_1d(y, win_size, y.shape[0]) - print 'diff between implementations =', 1-np.isclose(prob_f, prob).mean() - print 'diff between implementations =', 1-np.isclose(pos_f, pos).mean() + print( 'diff between implementations =', 1-np.isclose(prob_f, prob).mean()) + print( 'diff between implementations =', 1-np.isclose(pos_f, pos).mean()) plt.close('all') plt.plot(y) diff --git a/bat_eval/run_detector.py b/bat_eval/run_detector.py index faf8ee1..4024f1e 100755 --- a/bat_eval/run_detector.py +++ b/bat_eval/run_detector.py @@ -14,15 +14,15 @@ def read_audio(file_name, do_time_expansion, chunk_size, win_size): try: samp_rate_orig, audio = mywavfile.read(file_name) except: - print ' Error reading file' + print( ' Error reading file') return True, None, None, None, None # convert to mono if stereo if len(audio.shape) == 2: - print ' Warning: stereo file. Just taking left channel.' + print(' Warning: stereo file. Just taking left channel.') audio = audio[:, 0] file_dur = audio.shape[0] / float(samp_rate_orig) - print ' dur', round(file_dur,3), '(secs) , fs', samp_rate_orig + print(' dur', round(file_dur,3), '(secs) , fs', samp_rate_orig) # original model is trained on time expanded data samp_rate = samp_rate_orig @@ -75,15 +75,45 @@ def run_model(det, audio, file_dur, samp_rate, detection_thresh, max_num_calls=0 if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description='Process some bat files.') + parser.add_argument('--threshold', type=float, default=0.9, + help='detection threshold') + parser.add_argument('--datadir', + help='root directory for output') + parser.add_argument('--resultsdir', default='results', + help='directory in which to place the results') + parser.add_argument('--resfile', default='op_file.csv', + help='results file name') + parser.add_argument('--timeexp', default=True, + help='Apply time expansion') + parser.add_argument('--saveres', default=True, + help='Save results files') + + args = parser.parse_args() + print(args) # params - detection_thresh = 0.95 # make this smaller if you want more calls - do_time_expansion = True # if audio is already time expanded set this to False - save_res = True + detection_thresh = 0.9 + if 'threshold' in args: + detection_thresh =args.threshold # make this smaller if you want more calls + do_time_expansion = args.timeexp # if audio is already time expanded set this to False + save_res = args.saveres # load data - data_dir = 'wavs/' # this is the path to your audio files - op_ann_dir = 'results/' # this where your results will be saved - op_file_name_total = op_ann_dir + 'op_file.csv' + data_dir = 'C:\\Users/David/Documents/Bat recordings/Audiomoth/moth4/Stonebarrow/' # this is the path to your audio files + if 'datadir' in args: + data_dir=args.datadir + outdir = 'results3/' + if 'resultsdir' in args: + outdir = args.resultsdir + op_ann_dir = os.path.join(data_dir , outdir) # this where your results will be saved + + os.makedirs(op_ann_dir, exist_ok=True) + outfile = 'op_file.csv' + if 'resfile' in args: + outfile = args.resfile + op_file_name_total = os.path.join(op_ann_dir, outfile) if not os.path.isdir(op_ann_dir): os.makedirs(op_ann_dir) @@ -100,7 +130,7 @@ def run_model(det, audio, file_dur, samp_rate, detection_thresh, max_num_calls=0 for file_cnt, file_name in enumerate(audio_files): file_name_root = file_name[len(data_dir):] - print '\n', file_cnt+1, 'of', len(audio_files), '\t', file_name_root + print( '\n', file_cnt+1, 'of', len(audio_files), '\t', file_name_root) # read audio file - skip file if can't read it read_fail, audio, file_dur, samp_rate, samp_rate_orig = read_audio(file_name, @@ -114,9 +144,9 @@ def run_model(det, audio, file_dur, samp_rate, detection_thresh, max_num_calls=0 detection_thresh) toc = time.time() - print ' detection time', round(toc-tic, 3), '(secs)' + print( ' detection time', round(toc-tic, 3), '(secs)') num_calls = len(det_time) - print ' ' + str(num_calls) + ' calls found' + print( ' ' + str(num_calls) + ' calls found') # save results if save_res: @@ -133,8 +163,8 @@ def run_model(det, audio, file_dur, samp_rate, detection_thresh, max_num_calls=0 # save results for all files to large csv if save_res and (len(results) > 0): - print '\nsaving results to', op_file_name_total + print( '\nsaving results to', op_file_name_total) wo.save_to_txt(op_file_name_total, results) else: - print 'no detections to save' + print( 'no detections to save') diff --git a/bat_train/classifier.py b/bat_train/classifier.py index 5419564..a490673 100755 --- a/bat_train/classifier.py +++ b/bat_train/classifier.py @@ -18,7 +18,7 @@ def __init__(self, params_): elif self.params.classification_model == 'segment': self.model = seg.SegmentAudio(self.params) else: - print 'Invalid model specified' + print('Invalid model specified') def save_features(self, files): self.model.save_features(files) @@ -34,9 +34,9 @@ def train(self, files, gt_pos, durations): # hard negative mining if self.params.num_hard_negative_mining > 0 and self.params.classification_model != 'segment': - print '\nhard negative mining' + print( '\nhard negative mining') for hn in range(self.params.num_hard_negative_mining): - print '\thmn round', hn + print( '\thmn round', hn) positions, class_labels = self.do_hnm(files, gt_pos, durations, positions, class_labels) self.model.train(positions, class_labels, files, durations) diff --git a/bat_train/cls_audio_forest.py b/bat_train/cls_audio_forest.py index 817e404..ae4cd09 100755 --- a/bat_train/cls_audio_forest.py +++ b/bat_train/cls_audio_forest.py @@ -37,7 +37,7 @@ def train(self, positions, class_labels, files, durations): # flatten list of lists and set to correct output features = np.vstack(feats) labels = np.vstack(labs) - print 'train size', features.shape + print( 'train size', features.shape) self.forest.train(features, labels, False) def test(self, file_name=None, file_duration=None, audio_samples=None, sampling_rate=None): @@ -67,7 +67,7 @@ def create_or_load_features(self, file_name=None, audio_samples=None, sampling_r features = compute_features(audio_samples, sampling_rate, self.params) else: if self.params.load_features_from_file: - features = np.load(self.params.feature_dir + file_name + '.npy') + features = np.load(self.params.feature_dir + file_name + '.npy', encoding='bytes') else: sampling_rate, audio_samples = wavfile.read(self.params.audio_dir + file_name + '.wav') features = compute_features(audio_samples, sampling_rate, self.params) diff --git a/bat_train/cls_cnn.py b/bat_train/cls_cnn.py index 0494d63..60bc0ed 100755 --- a/bat_train/cls_cnn.py +++ b/bat_train/cls_cnn.py @@ -38,7 +38,7 @@ def train(self, positions, class_labels, files, durations): # flatten list of lists and set to correct output size features = np.vstack(feats) labels = np.vstack(labs).astype(np.uint8)[:,0] - print 'train size', features.shape + print( 'train size', features.shape) # train network input_var = theano.tensor.tensor4('inputs') @@ -90,7 +90,7 @@ def create_or_load_features(self, file_name=None, audio_samples=None, sampling_r features = compute_features(audio_samples, sampling_rate, self.params) else: if self.params.load_features_from_file: - features = np.load(self.params.feature_dir + file_name + '.npy') + features = np.load(self.params.feature_dir + file_name + '.npy', encoding='bytes') else: sampling_rate, audio_samples = wavfile.read(self.params.audio_dir + file_name + '.wav') features = compute_features(audio_samples, sampling_rate, self.params) @@ -120,7 +120,7 @@ def build_cnn(ip_size, input_var, net_type): elif net_type == 'small': net = network_sm(ip_size, input_var) else: - print 'Error: network not defined' + print( 'Error: network not defined') return net def network_big(ip_size, input_var): diff --git a/bat_train/create_results.py b/bat_train/create_results.py index f164630..fa60044 100755 --- a/bat_train/create_results.py +++ b/bat_train/create_results.py @@ -11,7 +11,7 @@ def plot_prec_recall(alg_name, recall, precision, nms_prob=None): # average precision ave_prec = evl.calc_average_precision(recall, precision) - print 'average precision (area) = %.3f ' % ave_prec + print( 'average precision (area) = %.3f ' % ave_prec) # recall at 95% precision desired_precision = 0.95 @@ -20,7 +20,7 @@ def plot_prec_recall(alg_name, recall, precision, nms_prob=None): else: recall_at_precision = 0 - print 'recall at', int(desired_precision*100), '% precision = ', "%.3f" % recall_at_precision + print( 'recall at', int(desired_precision*100), '% precision = ', "%.3f" % recall_at_precision) plt.plot([0, 1.02], [desired_precision, desired_precision], 'b:', linewidth=1) plt.plot([recall_at_precision, recall_at_precision], [0, desired_precision], 'b:', linewidth=1) diff --git a/bat_train/evaluate.py b/bat_train/evaluate.py index 0a85751..76ec1a4 100755 --- a/bat_train/evaluate.py +++ b/bat_train/evaluate.py @@ -12,7 +12,7 @@ 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' % (class_acc, roc_auc) + print( op_str, ', class acc = %.3f, ROC AUC = %.3f' % (class_acc, roc_auc)) #return class_acc, roc_auc diff --git a/bat_train/export_detector_weights.py b/bat_train/export_detector_weights.py index 12440e2..7fee407 100755 --- a/bat_train/export_detector_weights.py +++ b/bat_train/export_detector_weights.py @@ -3,7 +3,7 @@ can use it. """ -import cPickle as pickle +import pickle from lasagne.layers.helper import get_all_param_values, get_output_shape, set_all_param_values import numpy as np import lasagne @@ -13,16 +13,16 @@ save_detector = False -print 'saving detector' +print( 'saving detector') model_dir = 'results/models/' model_file = model_dir + 'test_set_norfolk.mod' -print model_file +print( model_file) mod = pickle.load(open(model_file)) weights = get_all_param_values(mod.model.network['prob']) np.save(model_file[:-4], weights) -print 'weights shape', len(weights) +print( 'weights shape', len(weights)) # save detection params mod_params = {'win_size':0, 'chunk_size':0, 'max_freq':0, 'min_freq':0, diff --git a/bat_train/nms_slow.py b/bat_train/nms_slow.py index d1dfd91..27437f1 100644 --- a/bat_train/nms_slow.py +++ b/bat_train/nms_slow.py @@ -59,8 +59,8 @@ def test_nms(): pos, prob = nms_1d(y, win_size, y.shape[0]) pos_f, prob_f = nms_fast.nms_1d(y, win_size, y.shape[0]) - print 'diff between implementations =', 1-np.isclose(prob_f, prob).mean() - print 'diff between implementations =', 1-np.isclose(pos_f, pos).mean() + print( 'diff between implementations =', 1-np.isclose(prob_f, prob).mean()) + print( 'diff between implementations =', 1-np.isclose(pos_f, pos).mean()) plt.close('all') plt.plot(y) diff --git a/bat_train/random_forest.py b/bat_train/random_forest.py index b021ed7..151b055 100755 --- a/bat_train/random_forest.py +++ b/bat_train/random_forest.py @@ -254,7 +254,7 @@ def optimize_node(self, x_local, y_local, node): ## Parallel training helper - used to train trees in parallel def train_forest_helper(t_id, X, Y, params, seed): - #print 'tree', t_id + #print( 'tree', t_id) np.random.seed(seed) tree = Tree(t_id, params) tree.train(X, Y) @@ -277,9 +277,9 @@ def train(self, X, Y, delete_old_trees): self.trees.extend(Parallel(n_jobs=-1)(delayed(train_forest_helper)(t_id, X, Y, self.params, seeds[t_id]) for t_id in range(self.params.num_trees))) else: - #print 'Standard training' + #print( 'Standard training') for t_id in range(self.params.num_trees): - print 'tree', t_id + print( 'tree', t_id) tree = Tree(t_id, self.params) tree.train(X, Y) self.trees.append(tree) diff --git a/bat_train/run_comparison.py b/bat_train/run_comparison.py index 10cf38a..8a42a42 100755 --- a/bat_train/run_comparison.py +++ b/bat_train/run_comparison.py @@ -39,11 +39,11 @@ def read_baseline_res(baseline_file_name, test_files): os.mkdir(result_dir) if not os.path.isdir(model_dir): os.mkdir(model_dir) - print 'test set:', test_set + print( 'test set:', test_set) plt.close('all') # train and test_pos are in units of seconds - loaded_data_tr = np.load(data_set) + loaded_data_tr = np.load(data_set, encoding='bytes') train_pos = loaded_data_tr['train_pos'] train_files = loaded_data_tr['train_files'] train_durations = loaded_data_tr['train_durations'] @@ -57,7 +57,7 @@ def read_baseline_res(baseline_file_name, test_files): # # CNN - print '\ncnn' + print( '\ncnn') params.classification_model = 'cnn' model = clss.Classifier(params) # train and test @@ -71,7 +71,7 @@ def read_baseline_res(baseline_file_name, test_files): # # random forest - print '\nrandom forest' + print( '\nrandom forest') params.classification_model = 'rf_vanilla' model = clss.Classifier(params) # train and test @@ -83,7 +83,7 @@ def read_baseline_res(baseline_file_name, test_files): # # segment - print '\nsegment' + print( '\nsegment') params.classification_model = 'segment' model = clss.Classifier(params) # train and test @@ -97,7 +97,7 @@ def read_baseline_res(baseline_file_name, test_files): # scanr scanr_bat_results = base_line_dir + 'scanr/test_set_'+ test_set +'_scanr.csv' if os.path.isfile(scanr_bat_results): - print '\nscanr' + print( '\nscanr') scanr_pos, scanr_prob = read_baseline_res(scanr_bat_results, test_files) precision_scanr, recall_scanr = evl.prec_recall_1d(scanr_pos, scanr_prob, test_pos, test_durations, params.detection_overlap, params.window_size) res.plot_prec_recall('scanr', recall_scanr, precision_scanr) @@ -106,7 +106,7 @@ def read_baseline_res(baseline_file_name, test_files): # sonobat sono_bat_results = base_line_dir + 'sonobat/test_set_'+ test_set +'_sono.csv' if os.path.isfile(sono_bat_results): - print '\nsonobat' + print( '\nsonobat') sono_pos, sono_prob = read_baseline_res(sono_bat_results, test_files) precision_sono, recall_sono = evl.prec_recall_1d(sono_pos, sono_prob, test_pos, test_durations, params.detection_overlap, params.window_size) res.plot_prec_recall('sonobat', recall_sono, precision_sono) @@ -115,7 +115,7 @@ def read_baseline_res(baseline_file_name, test_files): # kaleidoscope kal_bat_results = base_line_dir + 'kaleidoscope/test_set_'+ test_set +'_kaleidoscope.csv' if os.path.isfile(kal_bat_results): - print '\nkaleidoscope' + print( '\nkaleidoscope') kal_pos, kal_prob = read_baseline_res(kal_bat_results, test_files) precision_kal, recall_kal = evl.prec_recall_1d(kal_pos, kal_prob, test_pos, test_durations, params.detection_overlap, params.window_size) res.plot_prec_recall('kaleidoscope', recall_kal, precision_kal) diff --git a/bat_train/run_detector.py b/bat_train/run_detector.py index f318cb8..53b7547 100755 --- a/bat_train/run_detector.py +++ b/bat_train/run_detector.py @@ -14,15 +14,15 @@ def read_audio(file_name, do_time_expansion, chunk_size, win_size): try: samp_rate_orig, audio = wavfile.read(file_name) except: - print ' Error reading file' + print( ' Error reading file') return True, None, None, None, None # convert to mono if stereo if len(audio.shape) == 2: - print ' Warning: stereo file. Just taking right channel.' + print( ' Warning: stereo file. Just taking right channel.') audio = audio[:, 1] file_dur = audio.shape[0] / float(samp_rate_orig) - print ' dur', round(file_dur,3), '(secs) , fs', samp_rate_orig + print( ' dur', round(file_dur,3), '(secs) , fs', samp_rate_orig) # original model is trained on time expanded data samp_rate = samp_rate_orig @@ -113,7 +113,7 @@ def run_detector(det, audio, file_dur, samp_rate, detection_thresh): for file_cnt, file_name in enumerate(audio_files): file_name_root = file_name[len(data_dir):] - print '\n', file_cnt+1, 'of', len(audio_files), '\t', file_name_root + print( '\n', file_cnt+1, 'of', len(audio_files), '\t', file_name_root) # read audio file - skip file if cannot read read_fail, audio, file_dur, samp_rate, samp_rate_orig = read_audio(file_name, @@ -127,9 +127,9 @@ def run_detector(det, audio, file_dur, samp_rate, detection_thresh): detection_thresh) toc = time.time() - print ' detection time', round(toc-tic, 3), '(secs)' + print( ' detection time', round(toc-tic, 3), '(secs)') num_calls = len(det_time) - print ' ' + str(num_calls) + ' calls found' + print( ' ' + str(num_calls) + ' calls found') # save results if save_res: @@ -152,7 +152,7 @@ def run_detector(det, audio, file_dur, samp_rate, detection_thresh): # save to large csv if save_res and (len(results) > 0): - print '\nsaving results to', op_file_name_total + print( '\nsaving results to', op_file_name_total) wo.save_to_txt(op_file_name_total, results, np.asarray(['bat'])) else: - print 'no detections to save' + print( 'no detections to save') diff --git a/bat_view/batviewer.py b/bat_view/batviewer.py new file mode 100644 index 0000000..28e7107 --- /dev/null +++ b/bat_view/batviewer.py @@ -0,0 +1,562 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Jun 13 11:51:05 2020 +Simple viewer for Batdetect data. +@author: David +""" +import os,struct +import wave, numpy,io,csv +import matplotlib.pyplot as plt +import scipy.signal as sig, numpy as np +import tkinter as tk +import tkinter.filedialog as tkfd +from PIL import Image, ImageTk + +class SpecViewer (tk.Frame): + def __init__(self, master=None): + tk.Frame.__init__(self, master) + self.master=master + self.pack(fill="both", expand=True) + self.observations=None + self.createWidgets() + self.redrawing=0 + self.colormap =plt.cm.seismic + + def createWidgets(self): + menu = tk.Menu(self.master) + self.master.config(menu=menu) + file=tk.Menu(menu) + file.add_command(label="Exit", command=self.client_exit) + menu.add_cascade(label='File', menu=file) + settings=tk.Menu(menu) + settings.add_command(label="Audio Folder", command=self.set_AudioFolder) + settings.add_command(label="Analysis Folder", command=self.set_AnalysisFolder) + settings.add_command(label="Spectrogram Settings", command=self.set_SpectrogramSettings) + settings.add_command(label="show parameters", command = self.showparam) + menu.add_cascade(label="Settings", menu=settings) + #self.menubar = tk.Frame(self, bg='#333') + + #self.quitButton = tk.Button(self.menubar, text='Quit', command=self.quit) + #self.settingsButton(self.menubar, text="Settings", command=self.doSettings) + #self.loadButton = tk.Button(self.menubar, text="Load", command = self.loadSpectrogram) + #self.quitButton.grid() + #self.loadButton.grid(column=1,row=0) + #self.menubar.grid() + self.audioframe=tk.Frame(self,bg='#fff') + self.audioframe.pack(fill="x", side='top') +# self.audioframe.grid(sticky=tk.W+tk.E) + self.audiofiles_lb = tk.Listbox(self.audioframe) + self.audiofiles_lb.grid(row = 0, column=1, rowspan=4, sticky=tk.N+tk.S) + self.audiofiles_sb = tk.Scrollbar(self.audioframe) + self.audiofiles_sb.grid(row = 0, column=2, rowspan=4, sticky=tk.N+tk.S) + self.audiofiles_lb.config( yscrollcommand = self.audiofiles_sb.set) + self.audiofiles_lb.bind('<>',self.selectAudio) + self.audiofiles_sb.config(command=self.audiofiles_lb.yview) + self.audioframe.grid_columnconfigure(0, weight=1) + self.lbl_temp = tk.Label(self.audioframe, text='No audio file selected') + self.lbl_temp.grid(row=0, column=0, sticky=tk.E+tk.W) + self.audiofiles_lb.insert(tk.END, "No audio files loaded") + # analysis summary. + self.analysisblock= tk.Frame(self.audioframe) + self.analysisblock.grid(sticky=tk.N+tk.S+tk.W+tk.E, row=3) + + self.analysissummary = tk.Label(self.analysisblock,text='No analysis file loaded') + self.analysissummary.grid(row=0, column=0, columnspan=6, sticky=tk.E+tk.W) + self.analysisthreshold = tk.Label(self.analysisblock, text='Threshold: 0.5') + self.analysisthreshold.grid(row=1, column=0) + self.timestart_lbl = tk.Label(self.analysisblock, text="Start time") + self.timestart_lbl.grid(row=1,column =1, sticky=tk.E+tk.W) + self.timestart_sld = tk.Scale(self.analysisblock , from_=0, to=15, resolution=0.5, orient=tk.HORIZONTAL, command=self.settimestart) + self.timestart_sld.set(0) + self.timestart_sld.grid(row=2,column =1, sticky=tk.E+tk.W) + self.timezoom_lbl = tk.Label(self.analysisblock, text="Time window") + self.timezoom_lbl.grid(row=1, column =2, sticky=tk.E+tk.W) + self.timezoom_sld = tk.Scale(self.analysisblock , from_=1, to=15, resolution=0.5, orient=tk.HORIZONTAL, command=self.settimezoom) + self.timezoom_sld.set(15) + self.timezoom_sld.grid(row=2,column =2, sticky=tk.E+tk.W) + self.freqstart_lbl = tk.Label(self.analysisblock, text="Start frequency") + self.freqstart_lbl.grid(row=1,column =3, sticky=tk.E+tk.W) + self.freqstart_sld = tk.Scale(self.analysisblock , from_=0, to=0, resolution=1000, orient=tk.HORIZONTAL, command=self.setfreqstart) + self.freqstart_sld.set(0) + self.freqstart_sld.grid(row=2,column =3, sticky=tk.E+tk.W) + self.freqzoom_lbl = tk.Label(self.analysisblock, text="Frequency window") + self.freqzoom_lbl.grid(row=1,column =4, sticky=tk.E+tk.W) + self.freqzoom_sld = tk.Scale(self.analysisblock , from_=0, to=96000, resolution=1000, orient=tk.HORIZONTAL, command=self.setfreqzoom) + self.freqzoom_sld.set(96000) + self.freqzoom_sld.grid(row=2,column =4, sticky=tk.E+tk.W) + self.imagedraw_btn = tk.Button(self.analysisblock, text="Update Plot", command=self.replot_image) + self.imagedraw_btn.grid(column=5, row=1, rowspan=2,sticky=tk.E+tk.W+tk.N+tk.S) + self.colormap_lbl = tk.Label(self.analysisblock, text="Spectrum colours") + self.colormap_lbl.grid(columnspan=2, column=6, row=1) + self.colormapchooser = tk.Listbox(self.analysisblock, height=1) + for m in dir(plt.cm): + if type(getattr(plt.cm,m)).__name__.endswith('Colormap'): + self.colormapchooser.insert(tk.END, m) + self.colormap_sb = tk.Scrollbar(self.analysisblock ) + self.colormapchooser.config(yscrollcommand = self.colormap_sb.set) + self.colormap_sb.config(command=self.colormapchooser.yview) + self.colormapchooser.grid(row=2,column=6) + self.colormap_sb.grid(row=2,column=7) + self.thresholdslider = tk.Scale(self.analysisblock,from_=0.0, to=1.0, + resolution=0.01, orient=tk.HORIZONTAL, command=self.setthreshold) + self.thresholdslider.set(0.8) + self.thresholdslider.grid(row=2, column=0, sticky=tk.E+tk.W) + for n in range(5): + self.analysisblock.columnconfigure(n,weight=1) + + self.imageframe=tk.Frame(self,bg='#fff') +# self.imageframe.grid(sticky=tk.N+tk.S+tk.W+tk.E) + self.imageframe.pack(fill="both", expand=True) + self.imageframe.bind('', self.resize_image) + self.sview=tk.Canvas(self.imageframe, bg='#f00', height=400, width=800) +# self.sview.grid(sticky=tk.N+tk.S+tk.W+tk.E)# + self.sview.pack(fill="both", expand=True) + + def replot_image(self): + cm=self.colormapchooser.get(self.colormapchooser.curselection()) + self.colormap = getattr(plt.cm, cm) + self.resize_image(1) + + def settimestart (self, value): + ''' + Sets the start of the time window. + + Parameters + ---------- + value : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + if hasattr(self, 'wavfilename'): + width=float(self.timezoom_sld.get()) + value =min(float(value), self.limits[1] - width) + self.plotlimits[0]=value + self.plotlimits[1]=value+width + #self.resize_image(1) + + + def settimezoom (self, value): + ''' + Sets the size of the time window. + + Parameters + ---------- + value : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + value=float(value) + if hasattr(self, 'wavfilename'): + self.plotlimits[0] = min(self.plotlimits[0], self.limits[1]-value) + self.plotlimits[1]=self.plotlimits[0]+value + self.timestart_sld.config(to=self.limits[1]-value) + if self.plotlimits[0] != self.timestart_sld.get(): + self.timestart_sld.set(self.plotlimits[0]) + #self.resize_image(1) + def setcolormap(self, value): + ''' + Set the colormap for the spectrogram + + Parameters + ---------- + value : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + self.colormap = getattr(plt.cm, value) + def setfreqstart (self, value): + ''' + Sets the start of the frequency window. + + Parameters + ---------- + value : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + if hasattr(self, 'wavfilename'): + width=int(self.freqzoom_sld.get()) + value =min(float(value), self.limits[3] - width) + self.plotlimits[2]=value + self.plotlimits[3]=value+width + #self.resize_image(1) + + def setfreqzoom (self, value): + ''' + Sets the size of the frequency window. + + Parameters + ---------- + value : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + value=int(value) + if hasattr(self, 'wavfilename'): + self.plotlimits[2] = min(self.plotlimits[2], self.limits[3]-value) + self.plotlimits[3]=self.plotlimits[1]+value + self.freqstart_sld.config(to=self.limits[3]-value) + if self.plotlimits[2] != self.freqstart_sld.get(): + self.freqstart_sld.set(self.plotlimits[2]) + #self.resize_image(1) + + def setthreshold(self,evt): + self.analysisthreshold.config(text="Threshold: {}".format(evt)) + if hasattr(self,'wavfilename'): + self.setThresholdSummary() + + def selectAudio (self,evt): + Audiofile = str(self.audiofiles_lb.get(self.audiofiles_lb.curselection())) + if Audiofile in self.audiofiles: + self.loadSpectrogram(Audiofile) + + def showparam (self): + ''' + Prints the height and width of the image window canvas + Returns + ------- + None. + + ''' + self.master.update() + print('Imagesize height {} width {}'.format(self.sview.winfo_height(), self.sview.winfo_width())) + def getspecsize(self): + ''' + Calculates the size of the image for matplotlib to produce + + Returns + ------- + None. + + ''' + self.master.update() + return (self.sview.winfo_width(), self.sview.winfo_height()) + + def client_exit(self): + self.master.destroy() + #exit() + + def set_AudioFolder(self): + ''' + Generates a file dialogue to set the audio folder + + Returns + ------- + None. + + ''' + gafd = tkfd.askdirectory(title="Folder containing Audio files") + self.audiodirectory = gafd + self.audiofiles={} + self.audiofiles_lb.delete(0,"end") + for file in os.listdir(self.audiodirectory): + if file.upper()[-4:]=='.WAV': + self.audiofiles[file[:-4]] ={'audio': file} + self.audiofiles_lb.insert(tk.END,file[:-4]) + + print(self.audiodirectory) + self.lbl_temp.config(text='{} audio files found'.format(len(self.audiofiles))) + + def set_AnalysisFolder(self): + ''' + Generates a file dialogue to set the analysis folder + + Returns + ------- + None. + + ''' + gafd = tkfd.askdirectory(title="Folder containing batdetect analysis (.csv) output files") + self.analysisdirectory = gafd + count = 0 + for file in os.listdir(self.analysisdirectory): + if file[-4:]=='.csv': + if file[:-14] in self.audiofiles: + self.audiofiles[file[:-14]]['analysis'] = file + count += 1 + print(self.analysisdirectory) + self.lbl_temp.config(text='{} audio files found. {} analysis files found and linked.'.format(len(self.audiofiles),count)) + print("{} analysis files found and linked".format(count)) + + def set_SpectrogramSettings(): + ''' + Launches a settings window + + Returns + ------- + None. + + ''' + def resize_image(self,event): + ''' + Method bound to the event of the outer frame + + Returns + ------- + None. + + ''' + if hasattr(self, 'wavfilename'): + self.sview.delete('all') + self.draw_spectrogram() + self.clear_analysis() + self.plot_analysis() + dims = self.lbl_comment.winfo_width() + self.lbl_comment.config(wraplength=int(dims*0.9)) + self.lbl_comment.grid() + + def loadSpectrogram(self, fileroot): + ''' + Uses a load dialogue to get an audio file, check if spectrogram exists and create if not. + Produces a spectrogram image + Overlays boxes for the Batdetect identifications. + + Returns + ------- + None. +https://stackoverflow.com/questions/8598673/how-to-save-a-pylab-figure-into-in-memory-file-which-can-be-read-into-pil-image + ''' + if fileroot in self.audiofiles: + self.loadaudiofile(os.path.join(self.audiodirectory, self.audiofiles[fileroot]['audio'])) + self.displayaudioinfo() + self.draw_spectrogram() + self.clear_analysis() + if 'analysis' in self.audiofiles[fileroot]: + print('found analysis') + self.load_analysis(fileroot) + else: + self.observations=None + self.plot_analysis() + + + def loadaudiofile(self, filename): + ''' + Opens filename as a WAV file and retrieves the relevant information + + Parameters + ---------- + filename : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + self.wavfilename = filename + self.wavfile = wave.open(filename) + self.audioparams=self.wavfile.getparams() + self.limits=[0, self.audioparams.nframes/self.audioparams.framerate, 0, self.audioparams.framerate/2] + self.plotlimits = self.limits[:] + self.timestart_sld.set(0) + self.timezoom_sld.config(to=self.limits[1]) + self.timezoom_sld.set(self.limits[1]) + self.freqstart_sld.set(0) + self.freqzoom_sld.config(to=self.limits[3]) + self.freqzoom_sld.set(self.limits[3]) + + data = self.wavfile.readframes(self.audioparams.nframes) + self.audio = struct.unpack('{}h'.format(self.audioparams.nframes),data) + fh = open(self.wavfilename, 'rb') + head = fh.read(500) + self.audiocomment = '' + try: + info = head.find(b'INFOICMT')+12 + iart = head.find(b'IART ') + if info > 12: + self.audiocomment =head[info:iart].replace(b'\x00', b'').decode('utf-8') + # add some re here to extract the pertinent information from the comment + except Exception as e: + print('Error extracting header', e) + # create spectrogram + + + def displayaudioinfo(self): + ''' + Utility method to package audio information for display. + + Returns + ------- + None. + + ''' + self.lbl_temp.grid_forget() + self.lbl_filename = tk.Label(self.audioframe, text=self.wavfilename) + self.lbl_filename.grid(sticky=tk.W+tk.E, column=0, row=0) + paramtxt = "; ".join(["{}: {}".format(p, getattr(self.audioparams, p)) for p in ('nchannels', 'sampwidth', 'framerate', 'nframes')]) + + self.lbl_params = tk.Label(self.audioframe, text=paramtxt) + self.lbl_params.grid(sticky=tk.E+tk.W, column=0, row=1) + self.lbl_comment = tk.Label(self.audioframe, text=self.audiocomment, justify=tk.LEFT) + self.lbl_comment.grid(sticky=tk.E+tk.W, column=0, row=2) + + def draw_spectrogram(self): + #get size for the canvas + print('foo') + dims = self.getspecsize() + fig=plt.figure(dpi=100, figsize=((dims[0]-10)/100, (dims[1]-10)/100)) + ax=fig.add_subplot(1,1,1) + if hasattr(self, 'specimg'): + self.sview.delete(self.specimg) + txt=self.sview.create_text(dims[0]/2, dims[1]/2, text='Generating Spectrogram. Please wait ... ') + self.sview.pack() + + p,freqs,bins,im = ax.specgram(self.audio, NFFT=1024, Fs=self.audioparams.framerate, cmap=self.colormap,noverlap=512) + print('foobar') + self.imbounds=im.get_window_extent().extents + + ax.set_xlim(self.plotlimits[0],self.plotlimits[1]) + ax.set_ylim(self.plotlimits[2],self.plotlimits[3]) + ax.set_ylabel('Frequency (Hz)') + ax.set_xlabel('Time (seconds)') + self.fullrange=im.get_window_extent().extents + buf = io.BytesIO() + print('foobar2') + fig.savefig(buf, format='png') + buf.seek(0) + self.specim = Image.open(buf) + print(self.specim) + #self.specim.show() + self.specpim=ImageTk.PhotoImage(self.specim) + print(self.specpim) + + self.specimg =self.sview.create_image(dims[0]//2,dims[1]//2,image=self.specpim) + print(self.specimg) + self.sview.delete(txt) + self.sview.pack() + + def clear_analysis(self): + ''' + Removes all analysis objects from spectrogram + + Returns + ------- + None. + + ''' + print('clearing analysis') + self.sview.delete('analysis') + self.markers=[] + self.sview.pack() + + def load_analysis(self, fileroot): + ''' + Loads csv file with output from batdetect and overlays on the spectrogram + + Parameters + ---------- + fileroot : TYPE + DESCRIPTION. + + Returns + ------- + None. + + ''' + print('loading analysis') + cr = csv.reader(open(os.path.join(app.analysisdirectory, app.audiofiles[fileroot]['analysis']), newline='')) + header=next(cr) + dr =csv.DictReader(open(os.path.join(app.analysisdirectory, app.audiofiles[fileroot]['analysis']), newline=''),fieldnames=header) + next(dr) + self.observations=[] + for obs in dr: + self.observations.append({'start':float(obs['LabelStartTime_Seconds']),'end':float(obs['LabelEndTime_Seconds']),'score':float(obs['DetectorConfidence'])}) + self.setThresholdSummary() + + def setThresholdSummary(self): + if self.observations is not None: + self.analysissummary.config(text='{} bat calls detected ({} above threshold)'.format(len(self.observations), + len([x for x in self.observations if x['score']>=self.thresholdslider.get()]))) + self.clear_analysis() + self.plot_analysis() + else: + if hasattr(self, 'wavfilename'): + self.analysissummary.config(text='No analysis available for {}'.format(self.wavfilename)) + else: + self.analysissummary.config(text='No soundfile loaded') + + def plot_analysis(self): + ''' + Plot the observed calls as boxes for those that are above the threshold + + Returns + ------- + None. + + ''' + + # work through all calls to determine if they are plottable. + # range of data is in self.limits + # area plottable is in self.imbounds (x0,y0,x1,y1) + # virtual plot is in self.fullrange + if not hasattr(self, 'observations'): + return + #example to show boundaries + px = int(self.imbounds[0])+5 #harddoded the offset for the image sizeas it is 10px smaller than the canvas. need to get the padding + py = int(self.imbounds[1])+5 + pw = int(self.imbounds[2]-self.imbounds[0]) + ph = int(self.imbounds[3]-self.imbounds[1]) + print(px,py,pw,ph) + minx = self.limits[0] + maxx = self.limits[1] + print('drawing analysis') + #self.sview.create_rectangle(px,py, px+100, py+100, outline="#ff0" ) + #self.sview.pack() + fw = int(self.fullrange[2] -self.fullrange[0]) + fx = int(self.fullrange[0]) + foffset = int(self.imbounds[0] - self.fullrange[0]) + scale = fw/pw + + pb = py+ph -10 + + #for any point x coordinate is foffset + scale*(x-minx)/(maxx-minx) + for o in self.observations: + col = '#f00' + if o['score'] px and xm