Coverage for src/thunderfish/fishfinder.py: 0%
635 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-29 16:21 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-29 16:21 +0000
1import sys
2import os
3import warnings
4import argparse
5import numpy as np
6import matplotlib.pyplot as plt
7import matplotlib.mlab as ml
8from audioio import PlayAudio, fade, write_audio
9from thunderlab.configfile import ConfigFile
10from thunderlab.dataloader import DataLoader
11from thunderlab.powerspectrum import nfft, decibel, psd, spectrogram
12from thunderlab.powerspectrum import add_multi_psd_config, multi_psd_args
13from .version import __version__, __year__
14from .harmonics import harmonic_groups, harmonic_groups_args, psd_peak_detection_args
15from .harmonics import add_psd_peak_detection_config, add_harmonic_groups_config, colors_markers
16from .bestwindow import clip_amplitudes, clip_args, best_window_indices
17from .bestwindow import best_window_args
18from .thunderfish import configuration, save_configuration
19# check: import logging https://docs.python.org/2/howto/logging.html#logging-basic-tutorial
22class SignalPlot:
23 def __init__(self, data, samplingrate, unit, filename, channel, verbose, cfg):
24 self.filename = filename
25 self.channel = channel
26 self.samplerate = samplingrate
27 self.data = data
28 self.unit = unit
29 self.cfg = cfg
30 self.verbose = verbose
31 self.tmax = (len(self.data)-1)/self.samplerate
32 self.toffset = 0.0
33 self.twindow = 8.0
34 if self.twindow > self.tmax:
35 self.twindow = np.round(2 ** (np.floor(np.log(self.tmax) / np.log(2.0)) + 1.0))
36 self.ymin = -1.0
37 self.ymax = +1.0
38 self.trace_artist = None
39 self.spectrogram_artist = None
40 self.fmin = 0.0
41 self.fmax = 0.0
42 self.decibel = True
43 self.freq_resolution = self.cfg.value('frequencyResolution')
44 self.deltaf = 1.0
45 self.mains_freq = self.cfg.value('mainsFreq')
46 self.power_label = None
47 self.all_peaks_artis = None
48 self.good_peaks_artist = None
49 self.power_artist = None
50 self.power_frequency_label = None
51 self.peak_artists = []
52 self.legend = True
53 self.legendhandle = None
54 self.help = False
55 self.helptext = []
56 self.allpeaks = []
57 self.fishlist = []
58 self.mains = []
59 self.peak_specmarker = []
60 self.peak_annotation = []
61 self.min_clip = self.cfg.value('minClipAmplitude')
62 self.max_clip = self.cfg.value('maxClipAmplitude')
63 self.colorrange, self.markerrange = colors_markers()
65 # audio output:
66 self.audio = PlayAudio()
68 # set key bindings:
69 plt.rcParams['keymap.fullscreen'] = 'ctrl+f'
70 plt.rcParams['keymap.pan'] = 'ctrl+m'
71 plt.rcParams['keymap.quit'] = 'ctrl+w, alt+q, q'
72 plt.rcParams['keymap.yscale'] = ''
73 plt.rcParams['keymap.xscale'] = ''
74 plt.rcParams['keymap.grid'] = ''
75 #plt.rcParams['keymap.all_axes'] = ''
77 # the figure:
78 plt.ioff()
79 self.fig = plt.figure(figsize=(15, 9))
80 self.fig.canvas.manager.set_window_title(self.filename + ' channel {0:d}'.format(self.channel))
81 self.fig.canvas.mpl_connect('key_press_event', self.keypress)
82 self.fig.canvas.mpl_connect('button_press_event', self.buttonpress)
83 self.fig.canvas.mpl_connect('pick_event', self.onpick)
84 self.fig.canvas.mpl_connect('resize_event', self.resize)
85 # trace plot:
86 self.axt = self.fig.add_axes([0.1, 0.7, 0.87, 0.25])
87 self.axt.set_ylabel('Amplitude [{:s}]'.format(self.unit))
88 ht = self.axt.text(0.98, 0.05, '(ctrl+) page and arrow up, down, home, end: scroll', ha='right',
89 transform=self.axt.transAxes)
90 self.helptext.append(ht)
91 ht = self.axt.text(0.98, 0.15, '+, -, X, x: zoom in/out', ha='right', transform=self.axt.transAxes)
92 self.helptext.append(ht)
93 ht = self.axt.text(0.98, 0.25, 'y,Y,v,V: zoom amplitudes', ha='right', transform=self.axt.transAxes)
94 self.helptext.append(ht)
95 ht = self.axt.text(0.98, 0.35, 'p,P: play audio (display,all)', ha='right', transform=self.axt.transAxes)
96 self.helptext.append(ht)
97 ht = self.axt.text(0.98, 0.45, 'ctrl-f: full screen', ha='right', transform=self.axt.transAxes)
98 self.helptext.append(ht)
99 ht = self.axt.text(0.98, 0.55, 'w: plot waveform into png file', ha='right', transform=self.axt.transAxes)
100 self.helptext.append(ht)
101 ht = self.axt.text(0.98, 0.65, 's: save figure', ha='right', transform=self.axt.transAxes)
102 self.helptext.append(ht)
103 ht = self.axt.text(0.98, 0.75, 'S: save audiosegment', ha='right', transform=self.axt.transAxes)
104 self.helptext.append(ht)
105 ht = self.axt.text(0.98, 0.85, 'q: quit', ha='right', transform=self.axt.transAxes)
106 self.helptext.append(ht)
107 ht = self.axt.text(0.98, 0.95, 'h: toggle this help', ha='right', transform=self.axt.transAxes)
108 self.helptext.append(ht)
109 self.axt.set_xticklabels([])
110 # spectrogram:
111 self.axs = self.fig.add_axes([0.1, 0.45, 0.87, 0.25])
112 self.axs.set_xlabel('Time [seconds]')
113 self.axs.set_ylabel('Frequency [Hz]')
114 # power spectrum:
115 self.axp = self.fig.add_axes([0.1, 0.1, 0.87, 0.25])
116 ht = self.axp.text(0.98, 0.9, 'r, R: frequency resolution', ha='right', transform=self.axp.transAxes)
117 self.helptext.append(ht)
118 ht = self.axp.text(0.98, 0.8, 'f, F: zoom', ha='right', transform=self.axp.transAxes)
119 self.helptext.append(ht)
120 ht = self.axp.text(0.98, 0.7, '(ctrl+) left, right: move', ha='right', transform=self.axp.transAxes)
121 self.helptext.append(ht)
122 ht = self.axp.text(0.98, 0.6, 'l: toggle legend', ha='right', transform=self.axp.transAxes)
123 self.helptext.append(ht)
124 ht = self.axp.text(0.98, 0.5, 'd: toggle decibel', ha='right', transform=self.axp.transAxes)
125 self.helptext.append(ht)
126 ht = self.axp.text(0.98, 0.4, 'm: toggle mains filter', ha='right', transform=self.axp.transAxes)
127 self.helptext.append(ht)
128 ht = self.axp.text(0.98, 0.3, 'left mouse: show peak properties', ha='right', transform=self.axp.transAxes)
129 self.helptext.append(ht)
130 ht = self.axp.text(0.98, 0.2, 'shift/ctrl + left/right mouse: goto previous/next harmonic', ha='right',
131 transform=self.axp.transAxes)
132 self.helptext.append(ht)
133 # plot:
134 for ht in self.helptext:
135 ht.set_visible(self.help)
136 self.update_plots(False)
137 plt.show()
139 def __del__(self):
140 self.audio.close()
142 def remove_peak_annotation(self):
143 for fm in self.peak_specmarker:
144 fm.remove()
145 self.peak_specmarker = []
146 for fa in self.peak_annotation:
147 fa.remove()
148 self.peak_annotation = []
150 def annotate_peak(self, peak, harmonics=-1, inx=-1):
151 # marker:
152 if inx >= 0:
153 m, = self.axs.plot([self.toffset + 0.01 * self.twindow], [peak[0]], linestyle='None',
154 color=self.colorrange[inx % len(self.colorrange)],
155 marker=self.markerrange[inx], ms=10.0, mec=None, mew=0.0, zorder=2)
156 else:
157 m, = self.axs.plot([self.toffset + 0.01 * self.twindow], [peak[0]], linestyle='None',
158 color='k', marker='o', ms=10.0, mec=None, mew=0.0, zorder=2)
159 self.peak_specmarker.append(m)
160 # annotation:
161 fwidth = self.fmax - self.fmin
162 pl = []
163 pl.append(r'$f=$%.1f Hz' % peak[0])
164 pl.append(r'$h=$%d' % harmonics)
165 pl.append(r'$p=$%g' % peak[1])
166 pl.append(r'$c=$%.0f' % peak[2])
167 self.peak_annotation.append(self.axp.annotate('\n'.join(pl), xy=(peak[0], peak[1]),
168 xytext=(peak[0] + 0.03 * fwidth, peak[1]),
169 bbox=dict(boxstyle='round', facecolor='white'),
170 arrowprops=dict(arrowstyle='-')))
172 def annotate_fish(self, fish, inx=-1):
173 self.remove_peak_annotation()
174 for harmonic, freq in enumerate(fish[:, 0]):
175 peak = self.allpeaks[np.abs(self.allpeaks[:, 0] - freq) < 0.8 * self.deltaf, :]
176 if len(peak) > 0:
177 self.annotate_peak(peak[0, :], harmonic, inx)
178 self.fig.canvas.draw()
180 def update_plots(self, draw=True):
181 self.remove_peak_annotation()
182 # trace:
183 self.axt.set_xlim(self.toffset, self.toffset + self.twindow)
184 t0 = int(np.round(self.toffset * self.samplerate))
185 t1 = int(np.round((self.toffset + self.twindow) * self.samplerate))
186 if t1>len(self.data):
187 t1 = len(self.data)
188 time = np.arange(t0, t1) / self.samplerate
189 if self.trace_artist == None:
190 self.trace_artist, = self.axt.plot(time, self.data[t0:t1,self.channel])
191 else:
192 self.trace_artist.set_data(time, self.data[t0:t1,self.channel])
193 self.axt.set_ylim(self.ymin, self.ymax)
195 # compute power spectrum:
196 n_fft = nfft(self.samplerate, self.freq_resolution)
197 t00 = t0
198 t11 = t1
199 w = t11 - t00
200 minw = n_fft * (self.cfg.value('minPSDAverages') + 1) // 2
201 if t11 - t00 < minw:
202 w = minw
203 t11 = t00 + w
204 if t11 >= len(self.data):
205 t11 = len(self.data)
206 t00 = t11 - w
207 if t00 < 0:
208 t00 = 0
209 t11 = w
210 freqs, power = psd(self.data[t00:t11,self.channel], self.samplerate,
211 self.freq_resolution, detrend=ml.detrend_mean)
212 self.deltaf = freqs[1] - freqs[0]
213 # detect fish:
214 h_kwargs = psd_peak_detection_args(self.cfg)
215 h_kwargs.update(harmonic_groups_args(self.cfg))
216 self.fishlist, fzero_harmonics, self.mains, self.allpeaks, peaks, lowth, highth, center = harmonic_groups(freqs, power, verbose=self.verbose, **h_kwargs)
217 highth = center + highth - 0.5 * lowth
218 lowth = center + 0.5 * lowth
220 # spectrogram:
221 t2 = t1 + n_fft
222 freqs, bins, specpower = spectrogram(self.data[t0:t2,self.channel],
223 self.samplerate,
224 self.freq_resolution,
225 detrend=ml.detrend_mean)
226 z = decibel(specpower)
227 z = np.flipud(z)
228 extent = self.toffset, self.toffset + np.amax(bins), freqs[0], freqs[-1]
229 self.axs.set_xlim(self.toffset, self.toffset + self.twindow)
230 if self.spectrogram_artist == None:
231 self.fmax = np.round((freqs[-1] / 4.0) / 100.0) * 100.0
232 min = highth
233 min = np.percentile(z, 70.0)
234 max = np.percentile(z, 99.9) + 30.0
235 # cm = plt.get_cmap( 'hot_r' )
236 cm = plt.get_cmap('jet')
237 self.spectrogram_artist = self.axs.imshow(z, aspect='auto',
238 extent=extent, vmin=min, vmax=max,
239 cmap=cm, zorder=1)
240 else:
241 self.spectrogram_artist.set_data(z)
242 self.spectrogram_artist.set_extent(extent)
243 self.axs.set_ylim(self.fmin, self.fmax)
245 # power spectrum:
246 self.axp.set_xlim(self.fmin, self.fmax)
247 if self.deltaf >= 1000.0:
248 dfs = '%.3gkHz' % 0.001 * self.deltaf
249 else:
250 dfs = '%.3gHz' % self.deltaf
251 tw = float(w) / self.samplerate
252 if tw < 1.0:
253 tws = '%.3gms' % (1000.0 * tw)
254 else:
255 tws = '%.3gs' % (tw)
256 a = 2 * w // n_fft - 1 # number of ffts
257 m = ''
258 if self.cfg.value('mainsFreq') > 0.0:
259 m = ', mains=%.0fHz' % self.cfg.value('mainsFreq')
260 if self.power_frequency_label == None:
261 self.power_frequency_label = self.axp.set_xlabel(
262 r'Frequency [Hz] (nfft={:d}, $\Delta f$={:s}: T={:s}/{:d}{:s})'.format(n_fft, dfs, tws, a, m))
263 else:
264 self.power_frequency_label.set_text(
265 r'Frequency [Hz] (nfft={:d}, $\Delta f$={:s}: T={:s}/{:d}{:s})'.format(n_fft, dfs, tws, a, m))
266 self.axp.set_xlim(self.fmin, self.fmax)
267 if self.power_label == None:
268 self.power_label = self.axp.set_ylabel('Power')
269 if self.decibel:
270 if len(self.allpeaks) > 0:
271 self.allpeaks[:, 1] = decibel(self.allpeaks[:, 1])
272 power = decibel(power)
273 pmin = np.min(power[freqs < self.fmax])
274 pmin = np.floor(pmin / 10.0) * 10.0
275 pmax = np.max(power[freqs < self.fmax])
276 pmax = np.ceil(pmax / 10.0) * 10.0
277 doty = pmax - 5.0
278 self.power_label.set_text('Power [dB]')
279 self.axp.set_ylim(pmin, pmax)
280 else:
281 pmax = np.max(power[freqs < self.fmax])
282 doty = pmax
283 pmax *= 1.1
284 self.power_label.set_text('Power')
285 self.axp.set_ylim(0.0, pmax)
286 if self.all_peaks_artis == None:
287 self.all_peaks_artis, = self.axp.plot(self.allpeaks[:, 0],
288 np.zeros(len(self.allpeaks[:, 0])) + doty,
289 'o', color='#ffffff')
290 self.good_peaks_artist, = self.axp.plot(peaks, np.zeros(len(peaks)) + doty,
291 'o', color='#888888')
292 else:
293 self.all_peaks_artis.set_data(self.allpeaks[:, 0],
294 np.zeros(len(self.allpeaks[:, 0])) + doty)
295 self.good_peaks_artist.set_data(peaks, np.zeros(len(peaks)) + doty)
296 labels = []
297 fsizes = [np.sqrt(np.sum(self.fishlist[k][:, 1])) for k in range(len(self.fishlist))]
298 fmaxsize = np.max(fsizes) if len(fsizes) > 0 else 1.0
299 for k in range(len(self.peak_artists)):
300 self.peak_artists[k].remove()
301 self.peak_artists = []
302 for k in range(len(self.fishlist)):
303 if k >= len(self.markerrange):
304 break
305 fpeaks = self.fishlist[k][:, 0]
306 fpeakinx = [int(np.round(fp / self.deltaf)) for fp in fpeaks if fp < freqs[-1]]
307 fsize = 7.0 + 10.0 * (fsizes[k] / fmaxsize) ** 0.5
308 fishpoints, = self.axp.plot(fpeaks[:len(fpeakinx)], power[fpeakinx], linestyle='None',
309 color=self.colorrange[k % len(self.colorrange)],
310 marker=self.markerrange[k], ms=fsize,
311 mec=None, mew=0.0, zorder=1)
312 self.peak_artists.append(fishpoints)
313 if self.deltaf < 0.1:
314 labels.append('%4.2f Hz' % fpeaks[0])
315 elif self.deltaf < 1.0:
316 labels.append('%4.1f Hz' % fpeaks[0])
317 else:
318 labels.append('%4.0f Hz' % fpeaks[0])
319 if len(self.mains) > 0:
320 fpeaks = self.mains[:, 0]
321 fpeakinx = np.array([np.round(fp / self.deltaf) for fp in fpeaks if fp < freqs[-1]], dtype=int)
322 fishpoints, = self.axp.plot(fpeaks[:len(fpeakinx)],
323 power[fpeakinx], linestyle='None',
324 marker='.', color='k', ms=10, mec=None, mew=0.0, zorder=2)
325 self.peak_artists.append(fishpoints)
326 labels.append('%3.0f Hz mains' % self.cfg.value('mainsFreq'))
327 ncol = (len(labels)-1) // 8 + 1
328 self.legendhandle = self.axs.legend(self.peak_artists[:len(labels)], labels, loc='upper right', ncol=ncol)
329 self.legenddict = dict()
330 for legpoints, (finx, fish) in zip(self.legendhandle.get_lines(), enumerate(self.fishlist)):
331 legpoints.set_picker(8)
332 self.legenddict[legpoints] = [finx, fish]
333 self.legendhandle.set_visible(self.legend)
334 if self.power_artist == None:
335 self.power_artist, = self.axp.plot(freqs, power, 'b', zorder=3)
336 else:
337 self.power_artist.set_data(freqs, power)
338 if draw:
339 self.fig.canvas.draw()
341 def keypress(self, event):
342 # print('pressed', event.key)
343 if event.key in '+=X':
344 if self.twindow * self.samplerate > 20:
345 self.twindow *= 0.5
346 self.update_plots()
347 elif event.key in '-x':
348 if self.twindow < len(self.data) / self.samplerate:
349 self.twindow *= 2.0
350 self.update_plots()
351 elif event.key == 'pagedown':
352 if self.toffset + 0.5 * self.twindow < len(self.data) / self.samplerate:
353 self.toffset += 0.5 * self.twindow
354 self.update_plots()
355 elif event.key == 'pageup':
356 if self.toffset > 0:
357 self.toffset -= 0.5 * self.twindow
358 if self.toffset < 0.0:
359 self.toffset = 0.0
360 self.update_plots()
361 elif event.key == 'a':
362 if self.min_clip == 0.0 or self.max_clip == 0.0:
363 self.min_clip, self.max_clip = clip_amplitudes(
364 self.data[:,self.channel],
365 **clip_args(self.cfg, self.samplerate))
366 try:
367 if self.cfg.value('windowSize') <= 0.0:
368 self.cfg.set('windowSize', (len(self.data)-1)/self.samplerate)
369 idx0, idx1, clipped = best_window_indices(
370 self.data[:,self.channel], self.samplerate,
371 min_clip=self.min_clip, max_clip=self.max_clip,
372 **best_window_args(self.cfg))
373 if idx1 > 0:
374 self.toffset = idx0 / self.samplerate
375 self.twindow = (idx1 - idx0) / self.samplerate
376 self.twindow *= 2.0/(self.cfg.value('numberPSDWindows')+1.0)
377 self.update_plots()
378 except UserWarning as e:
379 if self.verbose > 0:
380 print(str(e))
381 elif event.key == 'ctrl+pagedown':
382 if self.toffset + 5.0 * self.twindow < len(self.data) / self.samplerate:
383 self.toffset += 5.0 * self.twindow
384 self.update_plots()
385 elif event.key == 'ctrl+pageup':
386 if self.toffset > 0:
387 self.toffset -= 5.0 * self.twindow
388 if self.toffset < 0.0:
389 self.toffset = 0.0
390 self.update_plots()
391 elif event.key == 'down':
392 if self.toffset + self.twindow < len(self.data) / self.samplerate:
393 self.toffset += 0.05 * self.twindow
394 self.update_plots()
395 elif event.key == 'up':
396 if self.toffset > 0.0:
397 self.toffset -= 0.05 * self.twindow
398 if self.toffset < 0.0:
399 self.toffset = 0.0
400 self.update_plots()
401 elif event.key == 'home':
402 if self.toffset > 0.0:
403 self.toffset = 0.0
404 self.update_plots()
405 elif event.key == 'end':
406 toffs = np.floor(len(self.data) / self.samplerate / self.twindow) * self.twindow
407 if self.toffset < toffs:
408 self.toffset = toffs
409 self.update_plots()
410 elif event.key == 'y':
411 h = self.ymax - self.ymin
412 c = 0.5 * (self.ymax + self.ymin)
413 self.ymin = c - h
414 self.ymax = c + h
415 self.axt.set_ylim(self.ymin, self.ymax)
416 self.fig.canvas.draw()
417 elif event.key == 'Y':
418 h = 0.25 * (self.ymax - self.ymin)
419 c = 0.5 * (self.ymax + self.ymin)
420 self.ymin = c - h
421 self.ymax = c + h
422 self.axt.set_ylim(self.ymin, self.ymax)
423 self.fig.canvas.draw()
424 elif event.key == 'v':
425 t0 = int(np.round(self.toffset * self.samplerate))
426 t1 = int(np.round((self.toffset + self.twindow) * self.samplerate))
427 min = np.min(self.data[t0:t1,self.channel])
428 max = np.max(self.data[t0:t1,self.channel])
429 h = 0.5 * (max - min)
430 c = 0.5 * (max + min)
431 self.ymin = c - h
432 self.ymax = c + h
433 self.axt.set_ylim(self.ymin, self.ymax)
434 self.fig.canvas.draw()
435 elif event.key == 'V':
436 self.ymin = -1.0
437 self.ymax = +1.0
438 self.axt.set_ylim(self.ymin, self.ymax)
439 self.fig.canvas.draw()
440 elif event.key == 'left':
441 if self.fmin > 0.0:
442 fwidth = self.fmax - self.fmin
443 self.fmin -= 0.5 * fwidth
444 self.fmax -= 0.5 * fwidth
445 if self.fmin < 0.0:
446 self.fmin = 0.0
447 self.fmax = fwidth
448 self.axs.set_ylim(self.fmin, self.fmax)
449 self.axp.set_xlim(self.fmin, self.fmax)
450 self.fig.canvas.draw()
451 elif event.key == 'right':
452 if self.fmax < 0.5 * self.samplerate:
453 fwidth = self.fmax - self.fmin
454 self.fmin += 0.5 * fwidth
455 self.fmax += 0.5 * fwidth
456 self.axs.set_ylim(self.fmin, self.fmax)
457 self.axp.set_xlim(self.fmin, self.fmax)
458 self.fig.canvas.draw()
459 elif event.key == 'ctrl+left':
460 if self.fmin > 0.0:
461 fwidth = self.fmax - self.fmin
462 self.fmin = 0.0
463 self.fmax = fwidth
464 self.axs.set_ylim(self.fmin, self.fmax)
465 self.axp.set_xlim(self.fmin, self.fmax)
466 self.fig.canvas.draw()
467 elif event.key == 'ctrl+right':
468 if self.fmax < 0.5 * self.samplerate:
469 fwidth = self.fmax - self.fmin
470 fm = 0.5 * self.samplerate
471 self.fmax = np.ceil(fm / fwidth) * fwidth
472 self.fmin = self.fmax - fwidth
473 if self.fmin < 0.0:
474 self.fmin = 0.0
475 self.fmax = fwidth
476 self.axs.set_ylim(self.fmin, self.fmax)
477 self.axp.set_xlim(self.fmin, self.fmax)
478 self.fig.canvas.draw()
479 elif event.key in 'f':
480 if self.fmax < 0.5 * self.samplerate or self.fmin > 0.0:
481 fwidth = self.fmax - self.fmin
482 if self.fmax < 0.5 * self.samplerate:
483 self.fmax = self.fmin + 2.0 * fwidth
484 elif self.fmin > 0.0:
485 self.fmin = self.fmax - 2.0 * fwidth
486 if self.fmin < 0.0:
487 self.fmin = 0.0
488 self.fmax = 2.0 * fwidth
489 self.axs.set_ylim(self.fmin, self.fmax)
490 self.axp.set_xlim(self.fmin, self.fmax)
491 self.fig.canvas.draw()
492 elif event.key in 'F':
493 if self.fmax - self.fmin > 1.0:
494 fwidth = self.fmax - self.fmin
495 self.fmax = self.fmin + 0.5 * fwidth
496 self.axs.set_ylim(self.fmin, self.fmax)
497 self.axp.set_xlim(self.fmin, self.fmax)
498 self.fig.canvas.draw()
499 elif event.key in 'r':
500 if self.freq_resolution < 1000.0:
501 self.freq_resolution *= 2.0
502 self.update_plots()
503 elif event.key in 'R':
504 if 1.0 / self.freq_resolution < self.tmax:
505 self.freq_resolution *= 0.5
506 self.update_plots()
507 elif event.key in 'd':
508 self.decibel = not self.decibel
509 self.update_plots()
510 elif event.key in 'm':
511 if self.cfg.value('mainsFreq') == 0.0:
512 self.cfg.set('mainsFreq', self.mains_freq)
513 else:
514 self.cfg.set('mainsFreq', 0.0)
515 self.update_plots()
516 elif event.key in 't':
517 t_diff = self.cfg.value('highThresholdFactor') - self.cfg.value('lowThresholdFactor')
518 self.cfg.set('lowThresholdFactor', self.cfg.value('lowThresholdFactor') - 0.1)
519 if self.cfg.value('lowThresholdFactor') < 0.1:
520 self.cfg.set('lowThresholdFactor', 0.1)
521 self.cfg.set('highThresholdFactor', self.cfg.value('lowThresholdFactor') + t_diff)
522 print('lowThresholdFactor =', self.cfg.value('lowThresholdFactor'))
523 self.update_plots()
524 elif event.key in 'T':
525 t_diff = self.cfg.value('highThresholdFactor') - self.cfg.value('lowThresholdFactor')
526 self.cfg.set('lowThresholdFactor', self.cfg.value('lowThresholdFactor') + 0.1)
527 if self.cfg.value('lowThresholdFactor') > 20.0:
528 self.cfg.set('lowThresholdFactor', 20.0)
529 self.cfg.set('highThresholdFactor', self.cfg.value('lowThresholdFactor') + t_diff)
530 print('lowThresholdFactor =', self.cfg.value('lowThresholdFactor'))
531 self.update_plots()
532 elif event.key == 'escape':
533 self.remove_peak_annotation()
534 self.fig.canvas.draw()
535 elif event.key in 'h':
536 self.help = not self.help
537 for ht in self.helptext:
538 ht.set_visible(self.help)
539 self.fig.canvas.draw()
540 elif event.key in 'l':
541 self.legend = not self.legend
542 self.legendhandle.set_visible(self.legend)
543 self.fig.canvas.draw()
544 elif event.key in 'w':
545 self.plot_waveform()
546 elif event.key in 'p':
547 self.play_segment()
548 elif event.key in 'P':
549 self.play_all()
550 elif event.key in '1' :
551 self.play_tone('c3')
552 elif event.key in '2' :
553 self.play_tone('a3')
554 elif event.key in '3' :
555 self.play_tone('e4')
556 elif event.key in '4' :
557 self.play_tone('a4')
558 elif event.key in '5' :
559 self.play_tone('c5')
560 elif event.key in '6' :
561 self.play_tone('e5')
562 elif event.key in '7' :
563 self.play_tone('g5')
564 elif event.key in '8' :
565 self.play_tone('a5')
566 elif event.key in '9' :
567 self.play_tone('c6')
568 elif event.key in 'S':
569 self.save_segment()
571 def buttonpress( self, event ) :
572 # print('mouse pressed', event.button, event.key, event.step)
573 if event.inaxes == self.axp:
574 if event.key == 'shift' or event.key == 'control':
575 # show next or previous harmonic:
576 if event.key == 'shift':
577 if event.button == 1:
578 ftarget = event.xdata / 2.0
579 elif event.button == 3:
580 ftarget = event.xdata * 2.0
581 else:
582 if event.button == 1:
583 ftarget = event.xdata / 1.5
584 elif event.button == 3:
585 ftarget = event.xdata * 1.5
586 foffs = event.xdata - self.fmin
587 fwidth = self.fmax - self.fmin
588 self.fmin = ftarget - foffs
589 self.fmax = self.fmin + fwidth
590 self.axs.set_ylim(self.fmin, self.fmax)
591 self.axp.set_xlim(self.fmin, self.fmax)
592 self.fig.canvas.draw()
593 else:
594 # put label on peak
595 self.remove_peak_annotation()
596 # find closest peak:
597 fwidth = self.fmax - self.fmin
598 peakdist = np.abs(self.allpeaks[:, 0] - event.xdata)
599 inx = np.argmin(peakdist)
600 if peakdist[inx] < 0.005 * fwidth:
601 peak = self.allpeaks[inx, :]
602 # find fish:
603 foundfish = False
604 for finx, fish in enumerate(self.fishlist):
605 if np.min(np.abs(fish[:, 0] - peak[0])) < 0.8 * self.deltaf:
606 self.annotate_fish(fish, finx)
607 foundfish = True
608 break
609 if not foundfish:
610 self.annotate_peak(peak)
611 self.fig.canvas.draw()
612 else:
613 self.fig.canvas.draw()
615 def onpick(self, event):
616 # print('pick')
617 legendpoint = event.artist
618 finx, fish = self.legenddict[legendpoint]
619 self.annotate_fish(fish, finx)
621 def resize(self, event):
622 # print('resized', event.width, event.height)
623 leftpixel = 80.0
624 rightpixel = 20.0
625 xaxispixel = 50.0
626 toppixel = 20.0
627 timeaxis = 0.42
628 left = leftpixel / event.width
629 width = 1.0 - left - rightpixel / event.width
630 xaxis = xaxispixel / event.height
631 top = toppixel / event.height
632 height = (1.0 - timeaxis - top) / 2.0
633 if left < 0.5 and width < 1.0 and xaxis < 0.3 and top < 0.2:
634 self.axt.set_position([left, timeaxis + height, width, height])
635 self.axs.set_position([left, timeaxis, width, height])
636 self.axp.set_position([left, xaxis, width, timeaxis - 2.0 * xaxis])
638 def plot_waveform(self):
639 fig = plt.figure()
640 ax = fig.add_subplot(1, 1, 1)
641 name = self.filename.split('.')[0]
642 if self.channel > 0:
643 ax.set_title('{filename} channel={channel:d}'.format(
644 filename=self.filename, channel=self.channel))
645 figfile = '{name}-{channel:d}-{time:.4g}s-waveform.png'.format(
646 name=name, channel=self.channel, time=self.toffset)
647 else:
648 ax.set_title(self.filename)
649 figfile = '{name}-{time:.4g}s-waveform.png'.format(
650 name=name, time=self.toffset)
651 t0 = int(np.round(self.toffset * self.samplerate))
652 t1 = int(np.round((self.toffset + self.twindow) * self.samplerate))
653 if t1>len(self.data):
654 t1 = len(self.data)
655 time = np.arange(t0, t1) / self.samplerate
656 if self.twindow < 1.0:
657 ax.set_xlabel('Time [ms]')
658 ax.set_xlim(1000.0 * self.toffset,
659 1000.0 * (self.toffset + self.twindow))
660 ax.plot(1000.0 * time, self.data[t0:t1,self.channel])
661 else:
662 ax.set_xlabel('Time [s]')
663 ax.set_xlim(self.toffset, self.toffset + self.twindow)
664 ax.plot(time, self.data[t0:t1,self.channel])
665 ax.set_ylabel('Amplitude [{:s}]'.format(self.unit))
666 fig.tight_layout()
667 fig.savefig(figfile)
668 fig.clear()
669 plt.close(fig)
670 print('saved waveform figure to', figfile)
672 def play_segment(self):
673 t0 = int(np.round(self.toffset * self.samplerate))
674 t1 = int(np.round((self.toffset + self.twindow) * self.samplerate))
675 playdata = 1.0 * self.data[t0:t1,self.channel]
676 fade(playdata, self.samplerate, 0.1)
677 self.audio.play(playdata, self.samplerate, blocking=False)
679 def save_segment(self):
680 t0s = int(np.round(self.toffset))
681 t1s = int(np.round(self.toffset + self.twindow))
682 t0 = int(np.round(self.toffset * self.samplerate))
683 t1 = int(np.round((self.toffset + self.twindow) * self.samplerate))
684 savedata = 1.0 * self.data[t0:t1,self.channel]
685 filename = self.filename.split('.')[0]
686 segmentfilename = '{name}-{time0:.4g}s-{time1:.4g}s.wav'.format(
687 name=filename, time0=t0s, time1 = t1s)
688 write_audio(segmentfilename, savedata, self.data.samplerate)
689 print('saved segment to: ' , segmentfilename)
691 def play_all(self):
692 self.audio.play(self.data[:,self.channel], self.samplerate,
693 blocking=False)
695 def play_tone( self, frequency ) :
696 self.audio.beep(1.0, frequency)
699def short_user_warning(message, category, filename, lineno, file=None, line=''):
700 if file is None:
701 file = sys.stderr
702 if category == UserWarning:
703 file.write('%s line %d: %s\n' % ('/'.join(filename.split('/')[-2:]), lineno, message))
704 else:
705 s = warnings.formatwarning(message, category, filename, lineno, line)
706 file.write(s)
709def main(cargs=None):
710 warnings.showwarning = short_user_warning
712 # config file name:
713 cfgfile = __package__ + '.cfg'
715 # command line arguments:
716 if cargs is None:
717 cargs = sys.argv[1:]
718 parser = argparse.ArgumentParser(
719 description='Display waveform, and power spectrum with detected fundamental frequencies of EOD recordings.',
720 epilog='version %s by Jan Benda (2015-%s)' % (__version__, __year__))
721 parser.add_argument('--version', action='version', version=__version__)
722 parser.add_argument('-v', action='count', dest='verbose')
723 parser.add_argument('-c', '--save-config', nargs='?', default='', const=cfgfile,
724 type=str, metavar='cfgfile',
725 help='save configuration to file cfgfile (defaults to {0})'.format(cfgfile))
726 parser.add_argument('file', nargs='?', default='', type=str,
727 help='name of the file with the time series data')
728 parser.add_argument('channel', nargs='?', default=0, type=int,
729 help='channel to be displayed')
730 args = parser.parse_args(cargs)
731 filepath = args.file
733 # set verbosity level from command line:
734 verbose = 0
735 if args.verbose != None:
736 verbose = args.verbose
738 if len(args.save_config):
739 # save configuration:
740 cfg = configuration()
741 cfg.load_files(cfgfile, filepath, 4, verbose)
742 save_configuration(cfg, cfgfile)
743 return
744 elif len(filepath) == 0:
745 parser.error('you need to specify a file containing some data')
747 # load configuration:
748 cfg = configuration()
749 cfg.load_files(cfgfile, filepath, 4, verbose-1)
751 # load data:
752 filename = os.path.basename(filepath)
753 channel = args.channel
754 # TODO: add blocksize and backsize as configuration parameter!
755 with DataLoader(filepath, 60.0, 10.0, verbose) as data:
756 SignalPlot(data, data.samplerate, data.unit, filename, channel, verbose, cfg)
759if __name__ == '__main__':
760 main()
763# 50301L02.WAV t=9 bis 9.15 sec
766## 1 fish:
767# simple aptero (clipped):
768# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L14.WAV
769# nice sterno:
770# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L31.WAV
771# sterno (clipped) with a little bit of background:
772# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L26.WAV
773# simple brachy (clipped, with a very small one in the background): still difficult, but great with T=4s
774# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L30.WAV
775# eigenmannia (very nice): EN086.MP3
776# single, very nice brachy, with difficult psd:
777# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L19.WAV
778# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L2[789].WAV
780## 2 fish:
781# 2 aptero:
782# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L10.WAV
783# EN098.MP3 and in particular EN099.MP3 nice 2Hz beat!
784# 2 brachy beat:
785# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L08.WAV
786# >= 2 brachys:
787# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L2[12789].WAV
789## one sterno with weak aptero:
790# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L11.WAV
791# EN144.MP3
793## 2 and 2 fish:
794# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L12.WAV
796## one aptero with brachy:
797# EN148
799## lots of fish:
800# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L07.WAV
801# EN065.MP3 EN066.MP3 EN067.MP3 EN103.MP3 EN104.MP3
802# EN109: 1Hz beat!!!!
803# EN013: doppel detection of 585 Hz
804# EN015,30,31: noise estimate problem
806# EN083.MP3 aptero glitch
807# EN146 sek 4 sterno frequency glitch
809# EN056.MP3 EN080.MP3 difficult low frequencies
810# EN072.MP3 unstable low and high freq
811# EN122.MP3 background fish detection difficulties at low res
813# problems: EN088, EN089, 20140524_RioCanita/EN055 sterno not catched, EN056, EN059