Coverage for src / thunderfish / fishfinder.py: 0%

635 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-15 17:50 +0000

1import sys 

2import os 

3import warnings 

4import argparse 

5import numpy as np 

6import matplotlib.pyplot as plt 

7import matplotlib.mlab as ml 

8 

9from audioio import PlayAudio, fade, write_audio 

10from thunderlab.configfile import ConfigFile 

11from thunderlab.dataloader import DataLoader 

12from thunderlab.powerspectrum import nfft, decibel, psd, spectrogram 

13from thunderlab.powerspectrum import add_multi_psd_config, multi_psd_args 

14 

15from .version import __version__, __year__ 

16from .harmonics import harmonic_groups, harmonic_groups_args, psd_peak_detection_args 

17from .harmonics import add_psd_peak_detection_config, add_harmonic_groups_config, colors_markers 

18from .bestwindow import clip_amplitudes, clip_args, best_window_indices 

19from .bestwindow import best_window_args 

20from .thunderfish import configuration, save_configuration 

21# check: import logging https://docs.python.org/2/howto/logging.html#logging-basic-tutorial 

22 

23 

24class SignalPlot: 

25 def __init__(self, data, samplingrate, unit, filename, channel, verbose, cfg): 

26 self.filename = filename 

27 self.channel = channel 

28 self.rate = samplingrate 

29 self.data = data 

30 self.unit = unit 

31 self.cfg = cfg 

32 self.verbose = verbose 

33 self.tmax = (len(self.data)-1)/self.rate 

34 self.toffset = 0.0 

35 self.twindow = 8.0 

36 if self.twindow > self.tmax: 

37 self.twindow = np.round(2 ** (np.floor(np.log(self.tmax) / np.log(2.0)) + 1.0)) 

38 self.ymin = -1.0 

39 self.ymax = +1.0 

40 self.trace_artist = None 

41 self.spectrogram_artist = None 

42 self.fmin = 0.0 

43 self.fmax = 0.0 

44 self.decibel = True 

45 self.freq_resolution = self.cfg.value('frequencyResolution') 

46 self.deltaf = 1.0 

47 self.mains_freq = self.cfg.value('mainsFreq') 

48 self.power_label = None 

49 self.all_peaks_artis = None 

50 self.good_peaks_artist = None 

51 self.power_artist = None 

52 self.power_frequency_label = None 

53 self.peak_artists = [] 

54 self.legend = True 

55 self.legendhandle = None 

56 self.help = False 

57 self.helptext = [] 

58 self.allpeaks = [] 

59 self.fishlist = [] 

60 self.mains = [] 

61 self.peak_specmarker = [] 

62 self.peak_annotation = [] 

63 self.min_clip = self.cfg.value('minClipAmplitude') 

64 self.max_clip = self.cfg.value('maxClipAmplitude') 

65 self.colorrange, self.markerrange = colors_markers() 

66 

67 # audio output: 

68 self.audio = PlayAudio() 

69 

70 # set key bindings: 

71 plt.rcParams['keymap.fullscreen'] = 'ctrl+f' 

72 plt.rcParams['keymap.pan'] = 'ctrl+m' 

73 plt.rcParams['keymap.quit'] = 'ctrl+w, alt+q, q' 

74 plt.rcParams['keymap.yscale'] = '' 

75 plt.rcParams['keymap.xscale'] = '' 

76 plt.rcParams['keymap.grid'] = '' 

77 #plt.rcParams['keymap.all_axes'] = '' 

78 

79 # the figure: 

80 plt.ioff() 

81 self.fig = plt.figure(figsize=(15, 9)) 

82 self.fig.canvas.manager.set_window_title(self.filename + ' channel {0:d}'.format(self.channel)) 

83 self.fig.canvas.mpl_connect('key_press_event', self.keypress) 

84 self.fig.canvas.mpl_connect('button_press_event', self.buttonpress) 

85 self.fig.canvas.mpl_connect('pick_event', self.onpick) 

86 self.fig.canvas.mpl_connect('resize_event', self.resize) 

87 # trace plot: 

88 self.axt = self.fig.add_axes([0.1, 0.7, 0.87, 0.25]) 

89 self.axt.set_ylabel('Amplitude [{:s}]'.format(self.unit)) 

90 ht = self.axt.text(0.98, 0.05, '(ctrl+) page and arrow up, down, home, end: scroll', ha='right', 

91 transform=self.axt.transAxes) 

92 self.helptext.append(ht) 

93 ht = self.axt.text(0.98, 0.15, '+, -, X, x: zoom in/out', ha='right', transform=self.axt.transAxes) 

94 self.helptext.append(ht) 

95 ht = self.axt.text(0.98, 0.25, 'y,Y,v,V: zoom amplitudes', ha='right', transform=self.axt.transAxes) 

96 self.helptext.append(ht) 

97 ht = self.axt.text(0.98, 0.35, 'p,P: play audio (display,all)', ha='right', transform=self.axt.transAxes) 

98 self.helptext.append(ht) 

99 ht = self.axt.text(0.98, 0.45, 'ctrl-f: full screen', ha='right', transform=self.axt.transAxes) 

100 self.helptext.append(ht) 

101 ht = self.axt.text(0.98, 0.55, 'w: plot waveform into png file', ha='right', transform=self.axt.transAxes) 

102 self.helptext.append(ht) 

103 ht = self.axt.text(0.98, 0.65, 's: save figure', ha='right', transform=self.axt.transAxes) 

104 self.helptext.append(ht) 

105 ht = self.axt.text(0.98, 0.75, 'S: save audiosegment', ha='right', transform=self.axt.transAxes) 

106 self.helptext.append(ht) 

107 ht = self.axt.text(0.98, 0.85, 'q: quit', ha='right', transform=self.axt.transAxes) 

108 self.helptext.append(ht) 

109 ht = self.axt.text(0.98, 0.95, 'h: toggle this help', ha='right', transform=self.axt.transAxes) 

110 self.helptext.append(ht) 

111 self.axt.set_xticklabels([]) 

112 # spectrogram: 

113 self.axs = self.fig.add_axes([0.1, 0.45, 0.87, 0.25]) 

114 self.axs.set_xlabel('Time [seconds]') 

115 self.axs.set_ylabel('Frequency [Hz]') 

116 # power spectrum: 

117 self.axp = self.fig.add_axes([0.1, 0.1, 0.87, 0.25]) 

118 ht = self.axp.text(0.98, 0.9, 'r, R: frequency resolution', ha='right', transform=self.axp.transAxes) 

119 self.helptext.append(ht) 

120 ht = self.axp.text(0.98, 0.8, 'f, F: zoom', ha='right', transform=self.axp.transAxes) 

121 self.helptext.append(ht) 

122 ht = self.axp.text(0.98, 0.7, '(ctrl+) left, right: move', ha='right', transform=self.axp.transAxes) 

123 self.helptext.append(ht) 

124 ht = self.axp.text(0.98, 0.6, 'l: toggle legend', ha='right', transform=self.axp.transAxes) 

125 self.helptext.append(ht) 

126 ht = self.axp.text(0.98, 0.5, 'd: toggle decibel', ha='right', transform=self.axp.transAxes) 

127 self.helptext.append(ht) 

128 ht = self.axp.text(0.98, 0.4, 'm: toggle mains filter', ha='right', transform=self.axp.transAxes) 

129 self.helptext.append(ht) 

130 ht = self.axp.text(0.98, 0.3, 'left mouse: show peak properties', ha='right', transform=self.axp.transAxes) 

131 self.helptext.append(ht) 

132 ht = self.axp.text(0.98, 0.2, 'shift/ctrl + left/right mouse: goto previous/next harmonic', ha='right', 

133 transform=self.axp.transAxes) 

134 self.helptext.append(ht) 

135 # plot: 

136 for ht in self.helptext: 

137 ht.set_visible(self.help) 

138 self.update_plots(False) 

139 plt.show() 

140 

141 def __del__(self): 

142 self.audio.close() 

143 

144 def remove_peak_annotation(self): 

145 for fm in self.peak_specmarker: 

146 fm.remove() 

147 self.peak_specmarker = [] 

148 for fa in self.peak_annotation: 

149 fa.remove() 

150 self.peak_annotation = [] 

151 

152 def annotate_peak(self, peak, harmonics=-1, inx=-1): 

153 # marker: 

154 if inx >= 0: 

155 m, = self.axs.plot([self.toffset + 0.01 * self.twindow], [peak[0]], linestyle='None', 

156 color=self.colorrange[inx % len(self.colorrange)], 

157 marker=self.markerrange[inx], ms=10.0, mec=None, mew=0.0, zorder=2) 

158 else: 

159 m, = self.axs.plot([self.toffset + 0.01 * self.twindow], [peak[0]], linestyle='None', 

160 color='k', marker='o', ms=10.0, mec=None, mew=0.0, zorder=2) 

161 self.peak_specmarker.append(m) 

162 # annotation: 

163 fwidth = self.fmax - self.fmin 

164 pl = [] 

165 pl.append(r'$f=$%.1f Hz' % peak[0]) 

166 pl.append(r'$h=$%d' % harmonics) 

167 pl.append(r'$p=$%g' % peak[1]) 

168 pl.append(r'$c=$%.0f' % peak[2]) 

169 self.peak_annotation.append(self.axp.annotate('\n'.join(pl), xy=(peak[0], peak[1]), 

170 xytext=(peak[0] + 0.03 * fwidth, peak[1]), 

171 bbox=dict(boxstyle='round', facecolor='white'), 

172 arrowprops=dict(arrowstyle='-'))) 

173 

174 def annotate_fish(self, fish, inx=-1): 

175 self.remove_peak_annotation() 

176 for harmonic, freq in enumerate(fish[:, 0]): 

177 peak = self.allpeaks[np.abs(self.allpeaks[:, 0] - freq) < 0.8 * self.deltaf, :] 

178 if len(peak) > 0: 

179 self.annotate_peak(peak[0, :], harmonic, inx) 

180 self.fig.canvas.draw() 

181 

182 def update_plots(self, draw=True): 

183 self.remove_peak_annotation() 

184 # trace: 

185 self.axt.set_xlim(self.toffset, self.toffset + self.twindow) 

186 t0 = int(np.round(self.toffset * self.rate)) 

187 t1 = int(np.round((self.toffset + self.twindow) * self.rate)) 

188 if t1>len(self.data): 

189 t1 = len(self.data) 

190 time = np.arange(t0, t1) / self.rate 

191 if self.trace_artist == None: 

192 self.trace_artist, = self.axt.plot(time, self.data[t0:t1,self.channel]) 

193 else: 

194 self.trace_artist.set_data(time, self.data[t0:t1,self.channel]) 

195 self.axt.set_ylim(self.ymin, self.ymax) 

196 

197 # compute power spectrum: 

198 n_fft = nfft(self.rate, self.freq_resolution) 

199 t00 = t0 

200 t11 = t1 

201 w = t11 - t00 

202 minw = n_fft * (self.cfg.value('minPSDAverages') + 1) // 2 

203 if t11 - t00 < minw: 

204 w = minw 

205 t11 = t00 + w 

206 if t11 >= len(self.data): 

207 t11 = len(self.data) 

208 t00 = t11 - w 

209 if t00 < 0: 

210 t00 = 0 

211 t11 = w 

212 freqs, power = psd(self.data[t00:t11,self.channel], self.rate, 

213 self.freq_resolution, detrend=ml.detrend_mean) 

214 self.deltaf = freqs[1] - freqs[0] 

215 # detect fish: 

216 h_kwargs = psd_peak_detection_args(self.cfg) 

217 h_kwargs.update(harmonic_groups_args(self.cfg)) 

218 self.fishlist, fzero_harmonics, self.mains, self.allpeaks, peaks, lowth, highth, center = harmonic_groups(freqs, power, verbose=self.verbose, **h_kwargs) 

219 highth = center + highth - 0.5 * lowth 

220 lowth = center + 0.5 * lowth 

221 

222 # spectrogram: 

223 t2 = t1 + n_fft 

224 freqs, bins, specpower = spectrogram(self.data[t0:t2,self.channel], 

225 self.rate, 

226 self.freq_resolution, 

227 detrend=ml.detrend_mean) 

228 z = decibel(specpower) 

229 z = np.flipud(z) 

230 extent = self.toffset, self.toffset + np.amax(bins), freqs[0], freqs[-1] 

231 self.axs.set_xlim(self.toffset, self.toffset + self.twindow) 

232 if self.spectrogram_artist == None: 

233 self.fmax = np.round((freqs[-1] / 4.0) / 100.0) * 100.0 

234 min = highth 

235 min = np.percentile(z, 70.0) 

236 max = np.percentile(z, 99.9) + 30.0 

237 # cm = plt.get_cmap( 'hot_r' ) 

238 cm = plt.get_cmap('jet') 

239 self.spectrogram_artist = self.axs.imshow(z, aspect='auto', 

240 extent=extent, vmin=min, vmax=max, 

241 cmap=cm, zorder=1) 

242 else: 

243 self.spectrogram_artist.set_data(z) 

244 self.spectrogram_artist.set_extent(extent) 

245 self.axs.set_ylim(self.fmin, self.fmax) 

246 

247 # power spectrum: 

248 self.axp.set_xlim(self.fmin, self.fmax) 

249 if self.deltaf >= 1000.0: 

250 dfs = '%.3gkHz' % 0.001 * self.deltaf 

251 else: 

252 dfs = '%.3gHz' % self.deltaf 

253 tw = float(w) / self.rate 

254 if tw < 1.0: 

255 tws = '%.3gms' % (1000.0 * tw) 

256 else: 

257 tws = '%.3gs' % (tw) 

258 a = 2 * w // n_fft - 1 # number of ffts 

259 m = '' 

260 if self.cfg.value('mainsFreq') > 0.0: 

261 m = ', mains=%.0fHz' % self.cfg.value('mainsFreq') 

262 if self.power_frequency_label == None: 

263 self.power_frequency_label = self.axp.set_xlabel( 

264 r'Frequency [Hz] (nfft={:d}, $\Delta f$={:s}: T={:s}/{:d}{:s})'.format(n_fft, dfs, tws, a, m)) 

265 else: 

266 self.power_frequency_label.set_text( 

267 r'Frequency [Hz] (nfft={:d}, $\Delta f$={:s}: T={:s}/{:d}{:s})'.format(n_fft, dfs, tws, a, m)) 

268 self.axp.set_xlim(self.fmin, self.fmax) 

269 if self.power_label == None: 

270 self.power_label = self.axp.set_ylabel('Power') 

271 if self.decibel: 

272 if len(self.allpeaks) > 0: 

273 self.allpeaks[:, 1] = decibel(self.allpeaks[:, 1]) 

274 power = decibel(power) 

275 pmin = np.min(power[freqs < self.fmax]) 

276 pmin = np.floor(pmin / 10.0) * 10.0 

277 pmax = np.max(power[freqs < self.fmax]) 

278 pmax = np.ceil(pmax / 10.0) * 10.0 

279 doty = pmax - 5.0 

280 self.power_label.set_text('Power [dB]') 

281 self.axp.set_ylim(pmin, pmax) 

282 else: 

283 pmax = np.max(power[freqs < self.fmax]) 

284 doty = pmax 

285 pmax *= 1.1 

286 self.power_label.set_text('Power') 

287 self.axp.set_ylim(0.0, pmax) 

288 if self.all_peaks_artis == None: 

289 self.all_peaks_artis, = self.axp.plot(self.allpeaks[:, 0], 

290 np.zeros(len(self.allpeaks[:, 0])) + doty, 

291 'o', color='#ffffff') 

292 self.good_peaks_artist, = self.axp.plot(peaks, np.zeros(len(peaks)) + doty, 

293 'o', color='#888888') 

294 else: 

295 self.all_peaks_artis.set_data(self.allpeaks[:, 0], 

296 np.zeros(len(self.allpeaks[:, 0])) + doty) 

297 self.good_peaks_artist.set_data(peaks, np.zeros(len(peaks)) + doty) 

298 labels = [] 

299 fsizes = [np.sqrt(np.sum(self.fishlist[k][:, 1])) for k in range(len(self.fishlist))] 

300 fmaxsize = np.max(fsizes) if len(fsizes) > 0 else 1.0 

301 for k in range(len(self.peak_artists)): 

302 self.peak_artists[k].remove() 

303 self.peak_artists = [] 

304 for k in range(len(self.fishlist)): 

305 if k >= len(self.markerrange): 

306 break 

307 fpeaks = self.fishlist[k][:, 0] 

308 fpeakinx = [int(np.round(fp / self.deltaf)) for fp in fpeaks if fp < freqs[-1]] 

309 fsize = 7.0 + 10.0 * (fsizes[k] / fmaxsize) ** 0.5 

310 fishpoints, = self.axp.plot(fpeaks[:len(fpeakinx)], power[fpeakinx], linestyle='None', 

311 color=self.colorrange[k % len(self.colorrange)], 

312 marker=self.markerrange[k], ms=fsize, 

313 mec=None, mew=0.0, zorder=1) 

314 self.peak_artists.append(fishpoints) 

315 if self.deltaf < 0.1: 

316 labels.append('%4.2f Hz' % fpeaks[0]) 

317 elif self.deltaf < 1.0: 

318 labels.append('%4.1f Hz' % fpeaks[0]) 

319 else: 

320 labels.append('%4.0f Hz' % fpeaks[0]) 

321 if len(self.mains) > 0: 

322 fpeaks = self.mains[:, 0] 

323 fpeakinx = np.array([np.round(fp / self.deltaf) for fp in fpeaks if fp < freqs[-1]], dtype=int) 

324 fishpoints, = self.axp.plot(fpeaks[:len(fpeakinx)], 

325 power[fpeakinx], linestyle='None', 

326 marker='.', color='k', ms=10, mec=None, mew=0.0, zorder=2) 

327 self.peak_artists.append(fishpoints) 

328 labels.append('%3.0f Hz mains' % self.cfg.value('mainsFreq')) 

329 ncol = (len(labels)-1) // 8 + 1 

330 self.legendhandle = self.axs.legend(self.peak_artists[:len(labels)], labels, loc='upper right', ncol=ncol) 

331 self.legenddict = dict() 

332 for legpoints, (finx, fish) in zip(self.legendhandle.get_lines(), enumerate(self.fishlist)): 

333 legpoints.set_picker(8) 

334 self.legenddict[legpoints] = [finx, fish] 

335 self.legendhandle.set_visible(self.legend) 

336 if self.power_artist == None: 

337 self.power_artist, = self.axp.plot(freqs, power, 'b', zorder=3) 

338 else: 

339 self.power_artist.set_data(freqs, power) 

340 if draw: 

341 self.fig.canvas.draw() 

342 

343 def keypress(self, event): 

344 # print('pressed', event.key) 

345 if event.key in '+=X': 

346 if self.twindow * self.rate > 20: 

347 self.twindow *= 0.5 

348 self.update_plots() 

349 elif event.key in '-x': 

350 if self.twindow < len(self.data) / self.rate: 

351 self.twindow *= 2.0 

352 self.update_plots() 

353 elif event.key == 'pagedown': 

354 if self.toffset + 0.5 * self.twindow < len(self.data) / self.rate: 

355 self.toffset += 0.5 * self.twindow 

356 self.update_plots() 

357 elif event.key == 'pageup': 

358 if self.toffset > 0: 

359 self.toffset -= 0.5 * self.twindow 

360 if self.toffset < 0.0: 

361 self.toffset = 0.0 

362 self.update_plots() 

363 elif event.key == 'a': 

364 if self.min_clip == 0.0 or self.max_clip == 0.0: 

365 self.min_clip, self.max_clip = clip_amplitudes( 

366 self.data[:,self.channel], 

367 **clip_args(self.cfg, self.rate)) 

368 try: 

369 if self.cfg.value('windowSize') <= 0.0: 

370 self.cfg.set('windowSize', (len(self.data)-1)/self.rate) 

371 idx0, idx1, clipped = best_window_indices( 

372 self.data[:,self.channel], self.rate, 

373 min_clip=self.min_clip, max_clip=self.max_clip, 

374 **best_window_args(self.cfg)) 

375 if idx1 > 0: 

376 self.toffset = idx0 / self.rate 

377 self.twindow = (idx1 - idx0) / self.rate 

378 self.twindow *= 2.0/(self.cfg.value('numberPSDWindows')+1.0) 

379 self.update_plots() 

380 except UserWarning as e: 

381 if self.verbose > 0: 

382 print(str(e)) 

383 elif event.key == 'ctrl+pagedown': 

384 if self.toffset + 5.0 * self.twindow < len(self.data) / self.rate: 

385 self.toffset += 5.0 * self.twindow 

386 self.update_plots() 

387 elif event.key == 'ctrl+pageup': 

388 if self.toffset > 0: 

389 self.toffset -= 5.0 * self.twindow 

390 if self.toffset < 0.0: 

391 self.toffset = 0.0 

392 self.update_plots() 

393 elif event.key == 'down': 

394 if self.toffset + self.twindow < len(self.data) / self.rate: 

395 self.toffset += 0.05 * self.twindow 

396 self.update_plots() 

397 elif event.key == 'up': 

398 if self.toffset > 0.0: 

399 self.toffset -= 0.05 * self.twindow 

400 if self.toffset < 0.0: 

401 self.toffset = 0.0 

402 self.update_plots() 

403 elif event.key == 'home': 

404 if self.toffset > 0.0: 

405 self.toffset = 0.0 

406 self.update_plots() 

407 elif event.key == 'end': 

408 toffs = np.floor(len(self.data) / self.rate / self.twindow) * self.twindow 

409 if self.toffset < toffs: 

410 self.toffset = toffs 

411 self.update_plots() 

412 elif event.key == 'y': 

413 h = self.ymax - self.ymin 

414 c = 0.5 * (self.ymax + self.ymin) 

415 self.ymin = c - h 

416 self.ymax = c + h 

417 self.axt.set_ylim(self.ymin, self.ymax) 

418 self.fig.canvas.draw() 

419 elif event.key == 'Y': 

420 h = 0.25 * (self.ymax - self.ymin) 

421 c = 0.5 * (self.ymax + self.ymin) 

422 self.ymin = c - h 

423 self.ymax = c + h 

424 self.axt.set_ylim(self.ymin, self.ymax) 

425 self.fig.canvas.draw() 

426 elif event.key == 'v': 

427 t0 = int(np.round(self.toffset * self.rate)) 

428 t1 = int(np.round((self.toffset + self.twindow) * self.rate)) 

429 min = np.min(self.data[t0:t1,self.channel]) 

430 max = np.max(self.data[t0:t1,self.channel]) 

431 h = 0.5 * (max - min) 

432 c = 0.5 * (max + min) 

433 self.ymin = c - h 

434 self.ymax = c + h 

435 self.axt.set_ylim(self.ymin, self.ymax) 

436 self.fig.canvas.draw() 

437 elif event.key == 'V': 

438 self.ymin = -1.0 

439 self.ymax = +1.0 

440 self.axt.set_ylim(self.ymin, self.ymax) 

441 self.fig.canvas.draw() 

442 elif event.key == 'left': 

443 if self.fmin > 0.0: 

444 fwidth = self.fmax - self.fmin 

445 self.fmin -= 0.5 * fwidth 

446 self.fmax -= 0.5 * fwidth 

447 if self.fmin < 0.0: 

448 self.fmin = 0.0 

449 self.fmax = fwidth 

450 self.axs.set_ylim(self.fmin, self.fmax) 

451 self.axp.set_xlim(self.fmin, self.fmax) 

452 self.fig.canvas.draw() 

453 elif event.key == 'right': 

454 if self.fmax < 0.5 * self.rate: 

455 fwidth = self.fmax - self.fmin 

456 self.fmin += 0.5 * fwidth 

457 self.fmax += 0.5 * fwidth 

458 self.axs.set_ylim(self.fmin, self.fmax) 

459 self.axp.set_xlim(self.fmin, self.fmax) 

460 self.fig.canvas.draw() 

461 elif event.key == 'ctrl+left': 

462 if self.fmin > 0.0: 

463 fwidth = self.fmax - self.fmin 

464 self.fmin = 0.0 

465 self.fmax = fwidth 

466 self.axs.set_ylim(self.fmin, self.fmax) 

467 self.axp.set_xlim(self.fmin, self.fmax) 

468 self.fig.canvas.draw() 

469 elif event.key == 'ctrl+right': 

470 if self.fmax < 0.5 * self.rate: 

471 fwidth = self.fmax - self.fmin 

472 fm = 0.5 * self.rate 

473 self.fmax = np.ceil(fm / fwidth) * fwidth 

474 self.fmin = self.fmax - fwidth 

475 if self.fmin < 0.0: 

476 self.fmin = 0.0 

477 self.fmax = fwidth 

478 self.axs.set_ylim(self.fmin, self.fmax) 

479 self.axp.set_xlim(self.fmin, self.fmax) 

480 self.fig.canvas.draw() 

481 elif event.key in 'f': 

482 if self.fmax < 0.5 * self.rate or self.fmin > 0.0: 

483 fwidth = self.fmax - self.fmin 

484 if self.fmax < 0.5 * self.rate: 

485 self.fmax = self.fmin + 2.0 * fwidth 

486 elif self.fmin > 0.0: 

487 self.fmin = self.fmax - 2.0 * fwidth 

488 if self.fmin < 0.0: 

489 self.fmin = 0.0 

490 self.fmax = 2.0 * fwidth 

491 self.axs.set_ylim(self.fmin, self.fmax) 

492 self.axp.set_xlim(self.fmin, self.fmax) 

493 self.fig.canvas.draw() 

494 elif event.key in 'F': 

495 if self.fmax - self.fmin > 1.0: 

496 fwidth = self.fmax - self.fmin 

497 self.fmax = self.fmin + 0.5 * fwidth 

498 self.axs.set_ylim(self.fmin, self.fmax) 

499 self.axp.set_xlim(self.fmin, self.fmax) 

500 self.fig.canvas.draw() 

501 elif event.key in 'r': 

502 if self.freq_resolution < 1000.0: 

503 self.freq_resolution *= 2.0 

504 self.update_plots() 

505 elif event.key in 'R': 

506 if 1.0 / self.freq_resolution < self.tmax: 

507 self.freq_resolution *= 0.5 

508 self.update_plots() 

509 elif event.key in 'd': 

510 self.decibel = not self.decibel 

511 self.update_plots() 

512 elif event.key in 'm': 

513 if self.cfg.value('mainsFreq') == 0.0: 

514 self.cfg.set('mainsFreq', self.mains_freq) 

515 else: 

516 self.cfg.set('mainsFreq', 0.0) 

517 self.update_plots() 

518 elif event.key in 't': 

519 t_diff = self.cfg.value('highThresholdFactor') - self.cfg.value('lowThresholdFactor') 

520 self.cfg.set('lowThresholdFactor', self.cfg.value('lowThresholdFactor') - 0.1) 

521 if self.cfg.value('lowThresholdFactor') < 0.1: 

522 self.cfg.set('lowThresholdFactor', 0.1) 

523 self.cfg.set('highThresholdFactor', self.cfg.value('lowThresholdFactor') + t_diff) 

524 print('lowThresholdFactor =', self.cfg.value('lowThresholdFactor')) 

525 self.update_plots() 

526 elif event.key in 'T': 

527 t_diff = self.cfg.value('highThresholdFactor') - self.cfg.value('lowThresholdFactor') 

528 self.cfg.set('lowThresholdFactor', self.cfg.value('lowThresholdFactor') + 0.1) 

529 if self.cfg.value('lowThresholdFactor') > 20.0: 

530 self.cfg.set('lowThresholdFactor', 20.0) 

531 self.cfg.set('highThresholdFactor', self.cfg.value('lowThresholdFactor') + t_diff) 

532 print('lowThresholdFactor =', self.cfg.value('lowThresholdFactor')) 

533 self.update_plots() 

534 elif event.key == 'escape': 

535 self.remove_peak_annotation() 

536 self.fig.canvas.draw() 

537 elif event.key in 'h': 

538 self.help = not self.help 

539 for ht in self.helptext: 

540 ht.set_visible(self.help) 

541 self.fig.canvas.draw() 

542 elif event.key in 'l': 

543 self.legend = not self.legend 

544 self.legendhandle.set_visible(self.legend) 

545 self.fig.canvas.draw() 

546 elif event.key in 'w': 

547 self.plot_waveform() 

548 elif event.key in 'p': 

549 self.play_segment() 

550 elif event.key in 'P': 

551 self.play_all() 

552 elif event.key in '1' : 

553 self.play_tone('c3') 

554 elif event.key in '2' : 

555 self.play_tone('a3') 

556 elif event.key in '3' : 

557 self.play_tone('e4') 

558 elif event.key in '4' : 

559 self.play_tone('a4') 

560 elif event.key in '5' : 

561 self.play_tone('c5') 

562 elif event.key in '6' : 

563 self.play_tone('e5') 

564 elif event.key in '7' : 

565 self.play_tone('g5') 

566 elif event.key in '8' : 

567 self.play_tone('a5') 

568 elif event.key in '9' : 

569 self.play_tone('c6') 

570 elif event.key in 'S': 

571 self.save_segment() 

572 

573 def buttonpress( self, event ) : 

574 # print('mouse pressed', event.button, event.key, event.step) 

575 if event.inaxes == self.axp: 

576 if event.key == 'shift' or event.key == 'control': 

577 # show next or previous harmonic: 

578 if event.key == 'shift': 

579 if event.button == 1: 

580 ftarget = event.xdata / 2.0 

581 elif event.button == 3: 

582 ftarget = event.xdata * 2.0 

583 else: 

584 if event.button == 1: 

585 ftarget = event.xdata / 1.5 

586 elif event.button == 3: 

587 ftarget = event.xdata * 1.5 

588 foffs = event.xdata - self.fmin 

589 fwidth = self.fmax - self.fmin 

590 self.fmin = ftarget - foffs 

591 self.fmax = self.fmin + fwidth 

592 self.axs.set_ylim(self.fmin, self.fmax) 

593 self.axp.set_xlim(self.fmin, self.fmax) 

594 self.fig.canvas.draw() 

595 else: 

596 # put label on peak 

597 self.remove_peak_annotation() 

598 # find closest peak: 

599 fwidth = self.fmax - self.fmin 

600 peakdist = np.abs(self.allpeaks[:, 0] - event.xdata) 

601 inx = np.argmin(peakdist) 

602 if peakdist[inx] < 0.005 * fwidth: 

603 peak = self.allpeaks[inx, :] 

604 # find fish: 

605 foundfish = False 

606 for finx, fish in enumerate(self.fishlist): 

607 if np.min(np.abs(fish[:, 0] - peak[0])) < 0.8 * self.deltaf: 

608 self.annotate_fish(fish, finx) 

609 foundfish = True 

610 break 

611 if not foundfish: 

612 self.annotate_peak(peak) 

613 self.fig.canvas.draw() 

614 else: 

615 self.fig.canvas.draw() 

616 

617 def onpick(self, event): 

618 # print('pick') 

619 legendpoint = event.artist 

620 finx, fish = self.legenddict[legendpoint] 

621 self.annotate_fish(fish, finx) 

622 

623 def resize(self, event): 

624 # print('resized', event.width, event.height) 

625 leftpixel = 80.0 

626 rightpixel = 20.0 

627 xaxispixel = 50.0 

628 toppixel = 20.0 

629 timeaxis = 0.42 

630 left = leftpixel / event.width 

631 width = 1.0 - left - rightpixel / event.width 

632 xaxis = xaxispixel / event.height 

633 top = toppixel / event.height 

634 height = (1.0 - timeaxis - top) / 2.0 

635 if left < 0.5 and width < 1.0 and xaxis < 0.3 and top < 0.2: 

636 self.axt.set_position([left, timeaxis + height, width, height]) 

637 self.axs.set_position([left, timeaxis, width, height]) 

638 self.axp.set_position([left, xaxis, width, timeaxis - 2.0 * xaxis]) 

639 

640 def plot_waveform(self): 

641 fig = plt.figure() 

642 ax = fig.add_subplot(1, 1, 1) 

643 name = self.filename.split('.')[0] 

644 if self.channel > 0: 

645 ax.set_title('{filename} channel={channel:d}'.format( 

646 filename=self.filename, channel=self.channel)) 

647 figfile = '{name}-{channel:d}-{time:.4g}s-waveform.png'.format( 

648 name=name, channel=self.channel, time=self.toffset) 

649 else: 

650 ax.set_title(self.filename) 

651 figfile = '{name}-{time:.4g}s-waveform.png'.format( 

652 name=name, time=self.toffset) 

653 t0 = int(np.round(self.toffset * self.rate)) 

654 t1 = int(np.round((self.toffset + self.twindow) * self.rate)) 

655 if t1>len(self.data): 

656 t1 = len(self.data) 

657 time = np.arange(t0, t1) / self.rate 

658 if self.twindow < 1.0: 

659 ax.set_xlabel('Time [ms]') 

660 ax.set_xlim(1000.0 * self.toffset, 

661 1000.0 * (self.toffset + self.twindow)) 

662 ax.plot(1000.0 * time, self.data[t0:t1,self.channel]) 

663 else: 

664 ax.set_xlabel('Time [s]') 

665 ax.set_xlim(self.toffset, self.toffset + self.twindow) 

666 ax.plot(time, self.data[t0:t1,self.channel]) 

667 ax.set_ylabel('Amplitude [{:s}]'.format(self.unit)) 

668 fig.tight_layout() 

669 fig.savefig(figfile) 

670 fig.clear() 

671 plt.close(fig) 

672 print('saved waveform figure to', figfile) 

673 

674 def play_segment(self): 

675 t0 = int(np.round(self.toffset * self.rate)) 

676 t1 = int(np.round((self.toffset + self.twindow) * self.rate)) 

677 playdata = 1.0 * self.data[t0:t1,self.channel] 

678 fade(playdata, self.rate, 0.1) 

679 self.audio.play(playdata, self.rate, blocking=False) 

680 

681 def save_segment(self): 

682 t0s = int(np.round(self.toffset)) 

683 t1s = int(np.round(self.toffset + self.twindow)) 

684 t0 = int(np.round(self.toffset * self.rate)) 

685 t1 = int(np.round((self.toffset + self.twindow) * self.rate)) 

686 savedata = 1.0 * self.data[t0:t1,self.channel] 

687 filename = self.filename.split('.')[0] 

688 segmentfilename = '{name}-{time0:.4g}s-{time1:.4g}s.wav'.format( 

689 name=filename, time0=t0s, time1 = t1s) 

690 write_audio(segmentfilename, savedata, self.data.rate) 

691 print('saved segment to: ' , segmentfilename) 

692 

693 def play_all(self): 

694 self.audio.play(self.data[:,self.channel], self.rate, 

695 blocking=False) 

696 

697 def play_tone( self, frequency ) : 

698 self.audio.beep(1.0, frequency) 

699 

700 

701def short_user_warning(message, category, filename, lineno, file=None, line=''): 

702 if file is None: 

703 file = sys.stderr 

704 if category == UserWarning: 

705 file.write('%s line %d: %s\n' % ('/'.join(filename.split('/')[-2:]), lineno, message)) 

706 else: 

707 s = warnings.formatwarning(message, category, filename, lineno, line) 

708 file.write(s) 

709 

710 

711def main(cargs=None): 

712 warnings.showwarning = short_user_warning 

713 

714 # config file name: 

715 cfgfile = __package__ + '.cfg' 

716 

717 # command line arguments: 

718 if cargs is None: 

719 cargs = sys.argv[1:] 

720 parser = argparse.ArgumentParser( 

721 description='Display waveform, and power spectrum with detected fundamental frequencies of EOD recordings.', 

722 epilog='version %s by Jan Benda (2015-%s)' % (__version__, __year__)) 

723 parser.add_argument('--version', action='version', version=__version__) 

724 parser.add_argument('-v', action='count', dest='verbose') 

725 parser.add_argument('-c', '--save-config', nargs='?', default='', const=cfgfile, 

726 type=str, metavar='cfgfile', 

727 help='save configuration to file cfgfile (defaults to {0})'.format(cfgfile)) 

728 parser.add_argument('file', nargs='?', default='', type=str, 

729 help='name of the file with the time series data') 

730 parser.add_argument('channel', nargs='?', default=0, type=int, 

731 help='channel to be displayed') 

732 args = parser.parse_args(cargs) 

733 filepath = args.file 

734 

735 # set verbosity level from command line: 

736 verbose = 0 

737 if args.verbose != None: 

738 verbose = args.verbose 

739 

740 if len(args.save_config): 

741 # save configuration: 

742 cfg = configuration() 

743 cfg.load_files(cfgfile, filepath, 4, verbose) 

744 save_configuration(cfg, cfgfile) 

745 return 

746 elif len(filepath) == 0: 

747 parser.error('you need to specify a file containing some data') 

748 

749 # load configuration: 

750 cfg = configuration() 

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

752 

753 # load data: 

754 filename = os.path.basename(filepath) 

755 channel = args.channel 

756 # TODO: add blocksize and backsize as configuration parameter! 

757 with DataLoader(filepath, 60.0, 10.0, verbose) as data: 

758 SignalPlot(data, data.rate, data.unit, filename, channel, verbose, cfg) 

759 

760 

761if __name__ == '__main__': 

762 main() 

763 

764 

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

766 

767 

768## 1 fish: 

769# simple aptero (clipped): 

770# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L14.WAV 

771# nice sterno: 

772# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L31.WAV 

773# sterno (clipped) with a little bit of background: 

774# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L26.WAV 

775# simple brachy (clipped, with a very small one in the background): still difficult, but great with T=4s 

776# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L30.WAV 

777# eigenmannia (very nice): EN086.MP3 

778# single, very nice brachy, with difficult psd: 

779# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L19.WAV 

780# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L2[789].WAV 

781 

782## 2 fish: 

783# 2 aptero: 

784# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L10.WAV 

785# EN098.MP3 and in particular EN099.MP3 nice 2Hz beat! 

786# 2 brachy beat: 

787# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L08.WAV 

788# >= 2 brachys: 

789# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L2[12789].WAV 

790 

791## one sterno with weak aptero: 

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

793# EN144.MP3 

794 

795## 2 and 2 fish: 

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

797 

798## one aptero with brachy: 

799# EN148 

800 

801## lots of fish: 

802# python fishfinder.py ~/data/fishgrid/Panama2014/MP3_1/20140517_RioCanita/40517L07.WAV 

803# EN065.MP3 EN066.MP3 EN067.MP3 EN103.MP3 EN104.MP3 

804# EN109: 1Hz beat!!!! 

805# EN013: doppel detection of 585 Hz 

806# EN015,30,31: noise estimate problem 

807 

808# EN083.MP3 aptero glitch 

809# EN146 sek 4 sterno frequency glitch 

810 

811# EN056.MP3 EN080.MP3 difficult low frequencies 

812# EN072.MP3 unstable low and high freq 

813# EN122.MP3 background fish detection difficulties at low res 

814 

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