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

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 

20 

21 

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() 

64 

65 # audio output: 

66 self.audio = PlayAudio() 

67 

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'] = '' 

76 

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() 

138 

139 def __del__(self): 

140 self.audio.close() 

141 

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 = [] 

149 

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='-'))) 

171 

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() 

179 

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) 

194 

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 

219 

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) 

244 

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() 

340 

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() 

570 

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() 

614 

615 def onpick(self, event): 

616 # print('pick') 

617 legendpoint = event.artist 

618 finx, fish = self.legenddict[legendpoint] 

619 self.annotate_fish(fish, finx) 

620 

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]) 

637 

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) 

671 

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) 

678 

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) 

690 

691 def play_all(self): 

692 self.audio.play(self.data[:,self.channel], self.samplerate, 

693 blocking=False) 

694 

695 def play_tone( self, frequency ) : 

696 self.audio.beep(1.0, frequency) 

697 

698 

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) 

707 

708 

709def main(cargs=None): 

710 warnings.showwarning = short_user_warning 

711 

712 # config file name: 

713 cfgfile = __package__ + '.cfg' 

714 

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 

732 

733 # set verbosity level from command line: 

734 verbose = 0 

735 if args.verbose != None: 

736 verbose = args.verbose 

737 

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') 

746 

747 # load configuration: 

748 cfg = configuration() 

749 cfg.load_files(cfgfile, filepath, 4, verbose-1) 

750 

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) 

757 

758 

759if __name__ == '__main__': 

760 main() 

761 

762 

763# 50301L02.WAV t=9 bis 9.15 sec 

764 

765 

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 

779 

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 

788 

789## one sterno with weak aptero: 

790# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L11.WAV 

791# EN144.MP3 

792 

793## 2 and 2 fish: 

794# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L12.WAV 

795 

796## one aptero with brachy: 

797# EN148 

798 

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 

805 

806# EN083.MP3 aptero glitch 

807# EN146 sek 4 sterno frequency glitch 

808 

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 

812 

813# problems: EN088, EN089, 20140524_RioCanita/EN055 sterno not catched, EN056, EN059