├── screenshots ├── m1.JPG ├── m2.JPG ├── m3.JPG ├── m4.JPG ├── m5.JPG ├── m6.JPG ├── minke_video.mp4 ├── pase_icon.ico ├── pase_icon.png └── minke_video_example.gif ├── to_compile_yourself ├── compilecommands.txt └── pase_compile.py ├── src └── pase │ ├── __init__.py │ └── pase.py ├── license.txt ├── pyproject.toml ├── .github └── workflows │ └── python-publish.yml └── README.md /screenshots/m1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/m1.JPG -------------------------------------------------------------------------------- /screenshots/m2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/m2.JPG -------------------------------------------------------------------------------- /screenshots/m3.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/m3.JPG -------------------------------------------------------------------------------- /screenshots/m4.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/m4.JPG -------------------------------------------------------------------------------- /screenshots/m5.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/m5.JPG -------------------------------------------------------------------------------- /screenshots/m6.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/m6.JPG -------------------------------------------------------------------------------- /screenshots/minke_video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/minke_video.mp4 -------------------------------------------------------------------------------- /screenshots/pase_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/pase_icon.ico -------------------------------------------------------------------------------- /screenshots/pase_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/pase_icon.png -------------------------------------------------------------------------------- /screenshots/minke_video_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sebastianmenze/Python-Audio-Spectrogram-Explorer/HEAD/screenshots/minke_video_example.gif -------------------------------------------------------------------------------- /to_compile_yourself/compilecommands.txt: -------------------------------------------------------------------------------- 1 | cd C:\GitHub\pase_complile 2 | 3 | pyinstaller pase_compile.py --onefile --ico pase_icon.ico 4 | 5 | # py pase_compile.py 6 | -------------------------------------------------------------------------------- /src/pase/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Audio Spectrogram Explorer (PASE) 3 | A GUI tool for marine acoustic analysis and annotation 4 | 5 | Created by Sebastian Menze 6 | Email: sebastian.menze@gmail.com 7 | """ 8 | 9 | __version__ = "1.0.0" 10 | __author__ = "Sebastian Menze" 11 | __email__ = "sebastian.menze@gmail.com" 12 | __license__ = "MIT" 13 | 14 | from .pase import start 15 | 16 | __all__ = ["start"] 17 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | CC BY-NC 4.0 2 | 3 | Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) 4 | This is a human-readable summary of (and not a substitute for) the license. Disclaimer. 5 | 6 | You are free to: 7 | Share — copy and redistribute the material in any medium or format 8 | Adapt — remix, transform, and build upon the material 9 | The licensor cannot revoke these freedoms as long as you follow the license terms. 10 | Under the following terms: 11 | Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 12 | 13 | NonCommercial — You may not use the material for commercial purposes. 14 | 15 | No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 16 | Notices: 17 | You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. 18 | No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools-scm>=8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pase" 7 | dynamic = ["version"] # Version comes from git tags 8 | description = "Python Audio Spectrogram Explorer (PASE) - A GUI tool for marine acoustic analysis" 9 | authors = [ 10 | {name = "Sebastian Menze", email = "sebastian.menze@gmail.com"} 11 | ] 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | license = {text = "MIT"} 15 | keywords = ["audio", "spectrogram", "acoustics", "marine-biology", "bioacoustics", "passive-acoustics"] 16 | 17 | classifiers = [ 18 | "Development Status :: 4 - Beta", 19 | "Intended Audience :: Science/Research", 20 | "Topic :: Scientific/Engineering :: Physics", 21 | "Topic :: Multimedia :: Sound/Audio :: Analysis", 22 | "License :: OSI Approved :: MIT License", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | ] 30 | 31 | dependencies = [ 32 | "PyQt5>=5.15.0", 33 | "matplotlib>=3.3.0", 34 | "scipy>=1.5.0", 35 | "soundfile>=0.10.0", 36 | "numpy>=1.19.0", 37 | "pandas>=1.1.0", 38 | "sounddevice>=0.4.0", 39 | "scikit-image>=0.17.0", 40 | ] 41 | 42 | [project.optional-dependencies] 43 | video = [ 44 | "moviepy>=1.0.3", 45 | "imageio-ffmpeg>=0.4.0", 46 | ] 47 | dev = [ 48 | "pytest>=7.0.0", 49 | "pytest-qt>=4.0.0", 50 | "black>=22.0.0", 51 | "flake8>=4.0.0", 52 | "isort>=5.10.0", 53 | ] 54 | all = [ 55 | "pase[video,dev]", 56 | ] 57 | 58 | [project.urls] 59 | Homepage = "https://github.com/sebastianmenze/pase" 60 | Repository = "https://github.com/sebastianmenze/pase" 61 | Issues = "https://github.com/sebastianmenze/pase/issues" 62 | 63 | [project.scripts] 64 | pase = "pase.pase:start" 65 | 66 | [tool.setuptools_scm] 67 | 68 | [tool.setuptools] 69 | package-dir = {"" = "src"} 70 | 71 | [tool.setuptools.packages.find] 72 | where = ["src"] 73 | 74 | [tool.black] 75 | line-length = 120 76 | target-version = ['py38'] 77 | 78 | [tool.isort] 79 | profile = "black" 80 | line_length = 120 81 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Build release distributions 30 | run: | 31 | # NOTE: put your own distribution build steps here. 32 | python -m pip install build 33 | python -m build 34 | 35 | - name: Upload distributions 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: release-dists 39 | path: dist/ 40 | 41 | pypi-publish: 42 | runs-on: ubuntu-latest 43 | needs: 44 | - release-build 45 | permissions: 46 | # IMPORTANT: this permission is mandatory for trusted publishing 47 | id-token: write 48 | 49 | # Dedicated environments with protections for publishing are strongly recommended. 50 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 51 | environment: 52 | name: pypi 53 | url: https://pypi.org/p/pase 54 | 55 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 56 | # url: https://pypi.org/p/YOURPROJECT 57 | # 58 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 59 | # ALTERNATIVE: exactly, uncomment the following line instead: 60 | # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} 61 | 62 | steps: 63 | - name: Retrieve release distributions 64 | uses: actions/download-artifact@v4 65 | with: 66 | name: release-dists 67 | path: dist/ 68 | 69 | - name: Publish release distributions to PyPI 70 | uses: pypa/gh-action-pypi-publish@release/v1 71 | with: 72 | packages-dir: dist/ 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![pase_icon](screenshots/pase_icon.png) 2 | # Python Audio Spectrogram Explorer (PASE) 3 | [![DOI](https://zenodo.org/badge/403931857.svg)](https://zenodo.org/badge/latestdoi/403931857) 4 | 5 | ### What you can do with this program: 6 | 7 | - Visualize audio files as spectrograms 8 | 9 | - Navigate through the spectrograms and listen in to selected areas in the spectrogram (adjustable playback speeds) 10 | 11 | - Export selected area in the spectrogram as .wav file, .csv table or .mp4 video 12 | 13 | - Annotate areas in the spectrograms with custom labels and log each annotation's time-stamp and frequency 14 | 15 | - Export spectrograms as image files and automatically plot spectrograms for all selected files 16 | 17 | - Draw shapes in the spectrogram and save them as .csv file 18 | 19 | - Automatically detect signals using spectrogram correlation or shapematching 20 | 21 | ![screenshots/s1](screenshots/m1.JPG) 22 | 23 | ## How to install and start the program: 24 | You can either download the windows executable (found here under "Release" and "PASE") or start the program using the python source code. The windows executable is included in this release, it cannot be used to export videos but has all the other functions. 25 | 26 | A platform independent way to start the program is run the source code directly in python. To download PASE use this command: 27 | 28 | ```python 29 | pip install pase 30 | ``` 31 | 32 | Than open a python console and start PASE with these two commands: 33 | 34 | ```python 35 | import pase 36 | pase.start() 37 | ``` 38 | 39 | This program uses PyQT5 as GUI framework and numpy, scipy, pandas and matplotlib to manipulate and visualize the data. The module `simpleaudio` is used to playback sound and `moviepy` to generated videos. In case you are getting an error message due to a missing module, simply copy the module's name and install it using pip, for example `pip install simpleaudio` and `pip install soundfile`. 40 | 41 | ## How to use it: 42 | 43 | ### Open files with or without timestamps 44 | 45 | The currently supported audio file types are: .wav .aif .aiff .aifc .ogg .flac 46 | 47 | To get started, you first have to decide if you want to use real time-stamps (year-month-day hour:minute:seconds) or not. For simply looking at the spectrograms and exploring your audio-files, you do not need the real time-stamps. But as soon as you want to annotate your data, the program needs to know when each .wav file started recording based on the file names. The default is using real time-stamps. 48 | 49 | **Without timestamps:** 50 | 51 | - Delete the content of the field "Timestamp:" 52 | - Press the "Open files" button in the Menu 53 | 54 | **With timestamps:** 55 | 56 | - The start date and time of each recoding should be contained in the audio file name 57 | 58 | - Adjust the "Timestamp:" field so that the program recognizes the correct time-stamp. For example: `aural_%Y_%m_%d_%H_%M_%S.wav` or `%y%m%d_%H%M%S_AU_SO02.wav` Where %Y is year, %m is month, %d is day and so on. Here is a list of the format strings: 59 | 60 | | **Directive** | **Meaning** | **Example** | 61 | | ------------- | ------------------------------------------------------------ | ------------------------ | 62 | | `%a` | Abbreviated weekday name. | Sun, Mon, ... | 63 | | `%A` | Full weekday name. | Sunday, Monday, ... | 64 | | `%w` | Weekday as a decimal number. | 0, 1, ..., 6 | 65 | | `%d` | Day of the month as a zero-padded decimal. | 01, 02, ..., 31 | 66 | | `%-d` | Day of the month as a decimal number. | 1, 2, ..., 30 | 67 | | `%b` | Abbreviated month name. | Jan, Feb, ..., Dec | 68 | | `%B` | Full month name. | January, February, ... | 69 | | `%m` | Month as a zero-padded decimal number. | 01, 02, ..., 12 | 70 | | `%-m` | Month as a decimal number. | 1, 2, ..., 12 | 71 | | `%y` | Year without century as a zero-padded decimal number. | 00, 01, ..., 99 | 72 | | `%-y` | Year without century as a decimal number. | 0, 1, ..., 99 | 73 | | `%Y` | Year with century as a decimal number. | 2013, 2019 etc. | 74 | | `%H` | Hour (24-hour clock) as a zero-padded decimal number. | 00, 01, ..., 23 | 75 | | `%-H` | Hour (24-hour clock) as a decimal number. | 0, 1, ..., 23 | 76 | | `%I` | Hour (12-hour clock) as a zero-padded decimal number. | 01, 02, ..., 12 | 77 | | `%-I` | Hour (12-hour clock) as a decimal number. | 1, 2, ... 12 | 78 | | `%p` | Locale’s AM or PM. | AM, PM | 79 | | `%M` | Minute as a zero-padded decimal number. | 00, 01, ..., 59 | 80 | | `%-M` | Minute as a decimal number. | 0, 1, ..., 59 | 81 | | `%S` | Second as a zero-padded decimal number. | 00, 01, ..., 59 | 82 | | `%-S` | Second as a decimal number. | 0, 1, ..., 59 | 83 | | `%j` | Day of the year as a zero-padded decimal number. | 001, 002, ..., 366 | 84 | | `%-j` | Day of the year as a decimal number. | 1, 2, ..., 366 | 85 | 86 | - Press the "Open files" button and select your audio files with the dialogue. 87 | 88 | ### Plot and browse spectrograms 89 | - Select the spectrogram setting of your choice: 90 | - Minimum and maximum frequency (y-axis) as f_min and f_max 91 | - Linear or logarithmic (default) frequency scale 92 | - The length (x-axis) of each spectrogram in seconds. If the field is left empty the spectrogram will be the length of the entire .wav file. 93 | - The FFT size determines the spectral resolution. The higher it is, the more detail you will see in the lower part of the spectrogram, with less detail in the upper part 94 | - The minimum and maximum dB values for the spectrogram color, will be determined automatically if left empty 95 | - The colormap from a dropdown menu, below are examples for the black and white colormap called "gist_yarg". 96 | 97 | - Press next spectrogram (The Shortkey for this is the right arrow button) 98 | - You can now navigate between the spectrograms using the "next/previous spectrogram" buttons or the left and right arrow keys. The time-stamp or filename of the current audio file is displayed as title. 99 | - You can zoom and pan using the magnifying glass symbol in the matplotlib toolbar, where you can also save the spectrogram as image file (square save button). 100 | - Once you have reached the final spectrogram, the program will display a warning 101 | 102 | ### Play audio and adjust playback speed, export the selected sound as .wav 103 | - Press the "Play/Stop" button or the spacebar to play the current selection of your audio file. 104 | - The program will only play what is visible in the current spectrogram (Sound above and below the frequency limits is filtered out) 105 | - To listen to specific sounds, zoom in using the magnifying glass 106 | - To listen to sound below or above the human hearing range, adjust the playback speed and press the "Play" button again. 107 | - To export the sound you selected as .wav file, press "Export" in the Menu and selected "Spectrogram as .wav file" 108 | 109 | ### Automatically plot spectrograms of multiple .wav files 110 | 111 | - Select your audio files with the "Open files" button 112 | - Select the spectrogram settings of your choice 113 | - Press "Export" in the Menu and select "All files as spectrogram images" 114 | - The spectrograms will be saved as .jpg files with the same filename and location as your .wav files. 115 | 116 | ### Annotate the spectrograms 117 | 118 | - Make sure the "filename key" field contains the correct time-stamp information 119 | 120 | - Now you can either choose to log you annotations in real time or save them later. I recommend using the "real-time logging" option. 121 | 122 | - Press the "real-time logging" check-box. Now the program will look if there are already log files existing for each audio file. Log files are named by adding "_log.csv" to the .wav filename, for example "aural_2017_02_12_22_40_00_log.csv". You can choose to overwrite these log files. If you do not choose to overwrite them, the program ignores audio files that already have an existing log file. This is useful if you want to work on a dataset over several sessions. 123 | 124 | - Now you can choose custom (or preset) labels for your annotations by changing the labels in the row "Annotation labels". If no label is selected (using the check-boxes) an empty string will be used as label. 125 | 126 | - To set an annotation, left-click at any location inside the spectrogram plot and draw a rectangle over the region of interest 127 | 128 | - To remove the last annotation, click the right mouse button. 129 | 130 | - Once a new .wav file is opened, the annotations for the previous .wav file are saved as .csv file, for example as "aural_2017_02_12_22_40_00_log.csv". If no annotations were set an empty table is saved. This indicates you have already screened this .wav file but found nothing to annotate. 131 | 132 | - The "...._log.csv" files are formated like this (t1,f1 and t2,f2 are the corners of the annotation box): 133 | 134 | | | t1 | t2 | f1 | f2 | Label | 135 | | ---- | ------------------ | ---------------|------- | ----|--- | 136 | | 0 | 2016-04-09 19:25:47.49 |2016-04-09 19:25:49.49 | 17.313 | 20.546 | FW_20_Hz | 137 | | 1 | 2016-05-10 17:36:13.94 | 2016-05-10 17:38:13.94 | 27.59109 | 34.57 | BW_Z_call | 138 | 139 | - If you want to save your annotations separately, press "Export" in the menu and choose "Annotations as .csv table" 140 | 141 | ### Remove the background from spectrogram 142 | 143 | This feature can be useful to detect sounds hidden in background noise. It subtracts the average spectrum from the current spectrogram, so that the horizontal noise lines and slope in background noise disappear. To active this function toggle the checkbox called "Remove background". For optimal use, test different dB minimum setting. Here is an example for the spectrogram shown above: 144 | 145 | ![screenshots/s2](screenshots/m2.JPG) 146 | 147 | ### Animated spectrogram videos 148 | 149 | You can use the python console based version of PASE to generate a video (.mp4 files) of the spectrogram with a moving bar. Here is an example: 150 | 151 | ![minke_video_example](screenshots/minke_video_example.gif) 152 | 153 | With sound: https://youtu.be/x3_kFesvw5I 154 | 155 | - zoom into the desired area of the spectrogram and press "Export" in the menu and select "Spectrogram as animated video" 156 | - Wait while the video is generated (might take a few minutes depending on the spectrogram size) 157 | 158 | ### Draw shape and export as .csv file 159 | 160 | ![screenshots/s4](screenshots/m3.JPG) 161 | 162 | - Press the "Draw" button in the menu 163 | 164 | - now you can draw a line by adding points with a double left click and removing them with a right click 165 | 166 | - to save the shape as .csv file and exit the drawing mode press ENTER 167 | 168 | - now you are back in the normal annotation mode 169 | 170 | - the .csv file is structured as follows: 171 | 172 | | | Time_in_s | Frequency_in_Hz | 173 | | ---- | --------- | --------------- | 174 | | 0 | 49.273229 | 171.060302 | 175 | | 1 | 49.224946 | 166.780047 | 176 | | 2 | 49.221929 | 147.346955 | 177 | 178 | ### Automatic signal detection 179 | 180 | You can use two automatic detections methods to detect sounds that are very similar to a selected template: 181 | 182 | **Shapematching** https://github.com/sebastianmenze/Marine-mammal-call-detection-using-spectrogram-shape-matching 183 | 184 | - Draw a template shape based on an example signal and save the .csv file (example for a minke whale calls shown above) 185 | 186 | - Press "Automatic detection" in the menu an select shapematching (on either the current or all audio files) 187 | - load the template (.csv file) of your choice 188 | - select a signal-to-noise dB threshold, usually between 3 and 10 dB, depending on the strength of the signal and amount of noise 189 | - The spectrogram will now display the bounding boxes of detected signals, with the score displayed in the upper left corners 190 | - You can export the automatic detections (including some more metadata) as .csv file in the "Export" menu under "Automatic detections as .csv file" 191 | - To clear the automatic detections, press any of the "Automatic detection" buttons but than press "Cancel" instead of opening a template file 192 | 193 | or **Spectrogram correlation** https://github.com/sebastianmenze/Spectrogram-correlation-tutorial 194 | 195 | - Here the template can either be a shape or a 2-D image table 196 | - To use a shape, use the "draw" mode and save the .csv file (example for a minke whale calls shown above) 197 | 198 | - To use a 2-D table, zoom into the spectrogram so your selection contains only the desired signal and press "Export" in the menu and select "Spectrogram as .csv table" 199 | - To start the automatic detections, press "Automatic detection" in the menu and select spectrogram correlation (on either the current or all audio files) 200 | - Select a template .csv file (shape or table) 201 | - Choose a detection threshold (correlation score r between 0 and 1) 202 | 203 | - The spectrogram will now display the bounding boxes of detected signals, with the score displayed in the upper left corners 204 | - You can export the automatic detections including some more metadata as .csv file using the "Export auto-detec" button 205 | - To clear the automatic detections, press any of the "Automatic detection" buttons but than press "Cancel" instead of opening a template file 206 | 207 | Here is an example for the Ant. Minke whale calls:![autodetect4](screenshots/m6.JPG) 208 | 209 | Example result for the shapematching: 210 | 211 | ![autodetect3](screenshots/m4.JPG) 212 | 213 | Example result for the spectrogram correlation: 214 | 215 | ![autodetect3](screenshots/m5.JPG) 216 | 217 | 218 | -------------------------------------------------------------------------------- /src/pase/pase.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python Audio Spectrogram Explorer (PASE) 4 | Created on Mon Sep 6 17:28:37 2021 5 | 6 | @author: Sebastian Menze, sebastian.menze@gmail.com 7 | """ 8 | import sys 9 | from PyQt5 import QtCore, QtWidgets 10 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar 11 | from matplotlib.figure import Figure 12 | 13 | import scipy.io.wavfile as wav 14 | import soundfile as sf 15 | from scipy import signal 16 | import numpy as np 17 | from matplotlib import pyplot as plt 18 | import pandas as pd 19 | import datetime as dt 20 | import os 21 | 22 | from matplotlib.widgets import RectangleSelector 23 | 24 | # Replace simpleaudio with sounddevice 25 | import sounddevice as sd 26 | 27 | from skimage import filters, measure, morphology 28 | from skimage.morphology import closing, disk 29 | from matplotlib.path import Path 30 | from skimage.transform import resize 31 | 32 | from scipy.signal import find_peaks 33 | from skimage.feature import match_template 34 | 35 | # Optional moviepy import - only needed for video export 36 | try: 37 | from moviepy.editor import VideoClip, AudioFileClip 38 | from moviepy.video.io.bindings import mplfig_to_npimage 39 | MOVIEPY_AVAILABLE = True 40 | except (ImportError, RuntimeError) as e: 41 | MOVIEPY_AVAILABLE = False 42 | print("Warning: moviepy not available. Video export will be disabled.") 43 | print(f"Error: {e}") 44 | print("To enable video export, install ffmpeg: https://ffmpeg.org/download.html") 45 | 46 | 47 | class MplCanvas(FigureCanvasQTAgg): 48 | def __init__(self, parent=None, dpi=150): 49 | self.fig = Figure(figsize=None, dpi=dpi) 50 | super(MplCanvas, self).__init__(self.fig) 51 | 52 | 53 | class gui(QtWidgets.QMainWindow): 54 | def __init__(self, *args, **kwargs): 55 | super(gui, self).__init__(*args, **kwargs) 56 | 57 | self.canvas = MplCanvas(self, dpi=150) 58 | 59 | # Audio playback control 60 | self.audio_stream = None 61 | self.audio_playing = False 62 | 63 | # Initialize UI elements 64 | self.f_min = QtWidgets.QLineEdit(self) 65 | self.f_min.setText('10') 66 | self.f_max = QtWidgets.QLineEdit(self) 67 | self.f_max.setText('16000') 68 | self.t_length = QtWidgets.QLineEdit(self) 69 | self.t_length.setText('120') 70 | self.db_saturation = QtWidgets.QLineEdit(self) 71 | self.db_saturation.setText('155') 72 | self.db_vmin = QtWidgets.QLineEdit(self) 73 | self.db_vmin.setText('30') 74 | self.db_vmax = QtWidgets.QLineEdit(self) 75 | self.db_vmax.setText('') 76 | 77 | self.fft_size = QtWidgets.QComboBox(self) 78 | self.fft_size.addItems(['1024', '2048', '4096', '8192', '16384', '32768', '65536', '131072']) 79 | self.fft_size.setCurrentIndex(4) 80 | 81 | self.colormap_plot = QtWidgets.QComboBox(self) 82 | self.colormap_plot.addItems(['plasma', 'viridis', 'inferno', 'gist_gray', 'gist_yarg']) 83 | self.colormap_plot.setCurrentIndex(2) 84 | 85 | self.checkbox_logscale = QtWidgets.QCheckBox('Log. scale') 86 | self.checkbox_logscale.setChecked(True) 87 | self.checkbox_background = QtWidgets.QCheckBox('Remove background') 88 | self.checkbox_background.setChecked(False) 89 | 90 | self.fft_overlap = QtWidgets.QComboBox(self) 91 | self.fft_overlap.addItems(['0.2', '0.5', '0.7', '0.9']) 92 | self.fft_overlap.setCurrentIndex(3) 93 | 94 | self.filename_timekey = QtWidgets.QLineEdit(self) 95 | 96 | self.playbackspeed = QtWidgets.QComboBox(self) 97 | self.playbackspeed.addItems(['0.5', '1', '2', '5', '10']) 98 | self.playbackspeed.setCurrentIndex(1) 99 | 100 | # Initialize data variables 101 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 102 | self.f = None 103 | self.t = [-1, -1] 104 | self.Sxx = None 105 | self.draw_x = pd.Series(dtype='float') 106 | self.draw_y = pd.Series(dtype='float') 107 | self.cid1 = None 108 | self.cid2 = None 109 | 110 | self.plotwindow_startsecond = float(self.t_length.text()) 111 | self.filecounter = -1 112 | self.filenames = np.array([]) 113 | self.current_audiopath = None 114 | self.detectiondf = pd.DataFrame([]) 115 | 116 | # Connect signals 117 | self.fft_size.currentIndexChanged.connect(self.new_fft_size_selected) 118 | self.colormap_plot.currentIndexChanged.connect(self.plot_spectrogram) 119 | self.checkbox_background.stateChanged.connect(self.plot_spectrogram) 120 | self.checkbox_logscale.stateChanged.connect(self.plot_spectrogram) 121 | 122 | self.checkbox_log = QtWidgets.QCheckBox('Real-time Logging') 123 | self.checkbox_log.toggled.connect(self.func_logging) 124 | 125 | # Create annotation labels 126 | self.checkbox_an_1 = QtWidgets.QCheckBox() 127 | self.an_1 = QtWidgets.QLineEdit(self) 128 | self.checkbox_an_2 = QtWidgets.QCheckBox() 129 | self.an_2 = QtWidgets.QLineEdit(self) 130 | self.checkbox_an_3 = QtWidgets.QCheckBox() 131 | self.an_3 = QtWidgets.QLineEdit(self) 132 | self.checkbox_an_4 = QtWidgets.QCheckBox() 133 | self.an_4 = QtWidgets.QLineEdit(self) 134 | self.checkbox_an_5 = QtWidgets.QCheckBox() 135 | self.an_5 = QtWidgets.QLineEdit(self) 136 | self.checkbox_an_6 = QtWidgets.QCheckBox() 137 | self.an_6 = QtWidgets.QLineEdit(self) 138 | 139 | self.bg = QtWidgets.QButtonGroup() 140 | self.bg.addButton(self.checkbox_an_1, 1) 141 | self.bg.addButton(self.checkbox_an_2, 2) 142 | self.bg.addButton(self.checkbox_an_3, 3) 143 | self.bg.addButton(self.checkbox_an_4, 4) 144 | self.bg.addButton(self.checkbox_an_5, 5) 145 | self.bg.addButton(self.checkbox_an_6, 6) 146 | 147 | # Setup menu bar 148 | self.setup_menubar() 149 | 150 | # Setup layouts 151 | self.setup_layouts() 152 | 153 | # Setup hotkeys 154 | self.msgSc1 = QtWidgets.QShortcut(QtCore.Qt.Key_Right, self) 155 | self.msgSc1.activated.connect(self.plot_next_spectro) 156 | self.msgSc2 = QtWidgets.QShortcut(QtCore.Qt.Key_Left, self) 157 | self.msgSc2.activated.connect(self.plot_previous_spectro) 158 | self.msgSc3 = QtWidgets.QShortcut(QtCore.Qt.Key_Space, self) 159 | self.msgSc3.activated.connect(self.func_playaudio) 160 | 161 | self.show() 162 | 163 | def setup_menubar(self): 164 | """Setup the menu bar""" 165 | menuBar = self.menuBar() 166 | 167 | openMenu = menuBar.addAction("Open files") 168 | openMenu.triggered.connect(self.openfilefunc) 169 | 170 | exportMenu = menuBar.addMenu("Export") 171 | exportMenu.addAction("Spectrogram as .wav file").triggered.connect(self.func_saveaudio) 172 | exportMenu.addAction("Spectrogram as animated video").triggered.connect(self.func_save_video) 173 | exportMenu.addAction("Spectrogram as .csv table").triggered.connect(self.export_zoomed_sgram_as_csv) 174 | exportMenu.addAction("All files as spectrogram images").triggered.connect(self.plot_all_spectrograms) 175 | exportMenu.addAction("Annotations as .csv table").triggered.connect(self.func_savecsv) 176 | exportMenu.addAction("Automatic detections as .csv table").triggered.connect(self.export_automatic_detector) 177 | 178 | drawMenu = menuBar.addAction("Draw") 179 | drawMenu.triggered.connect(self.func_draw_shape) 180 | 181 | autoMenu = menuBar.addMenu("Automatic detection") 182 | autoMenu.addAction("Shapematching on current file").triggered.connect(self.automatic_detector_shapematching) 183 | autoMenu.addAction("Shapematching on all files").triggered.connect(self.automatic_detector_shapematching_allfiles) 184 | autoMenu.addAction("Spectrogram correlation on current file").triggered.connect(self.automatic_detector_specgram_corr) 185 | autoMenu.addAction("Spectrogram correlation on all files").triggered.connect(self.automatic_detector_specgram_corr_allfiles) 186 | autoMenu.addAction("Show regions based on threshold").triggered.connect(self.plot_spectrogram_threshold) 187 | 188 | quitMenu = menuBar.addAction("Quit") 189 | quitMenu.triggered.connect(self.exitfunc) 190 | 191 | def setup_layouts(self): 192 | """Setup all UI layouts""" 193 | outer_layout = QtWidgets.QVBoxLayout() 194 | 195 | # Top layout with settings 196 | top2_layout = QtWidgets.QHBoxLayout() 197 | top2_layout.addWidget(self.checkbox_log) 198 | top2_layout.addWidget(self.checkbox_logscale) 199 | top2_layout.addWidget(self.checkbox_background) 200 | top2_layout.addWidget(QtWidgets.QLabel('Timestamp:')) 201 | top2_layout.addWidget(self.filename_timekey) 202 | top2_layout.addWidget(QtWidgets.QLabel('f_min[Hz]:')) 203 | top2_layout.addWidget(self.f_min) 204 | top2_layout.addWidget(QtWidgets.QLabel('f_max[Hz]:')) 205 | top2_layout.addWidget(self.f_max) 206 | top2_layout.addWidget(QtWidgets.QLabel('Spec. length [sec]:')) 207 | top2_layout.addWidget(self.t_length) 208 | top2_layout.addWidget(QtWidgets.QLabel('Saturation dB:')) 209 | top2_layout.addWidget(self.db_saturation) 210 | top2_layout.addWidget(QtWidgets.QLabel('dB min:')) 211 | top2_layout.addWidget(self.db_vmin) 212 | top2_layout.addWidget(QtWidgets.QLabel('dB max:')) 213 | top2_layout.addWidget(self.db_vmax) 214 | 215 | # Annotation labels layout 216 | top3_layout = QtWidgets.QHBoxLayout() 217 | top3_layout.addWidget(QtWidgets.QLabel('Annotation labels:')) 218 | for i in range(1, 7): 219 | checkbox = getattr(self, f'checkbox_an_{i}') 220 | lineedit = getattr(self, f'an_{i}') 221 | top3_layout.addWidget(checkbox) 222 | top3_layout.addWidget(lineedit) 223 | 224 | # Plot layout 225 | plot_layout = QtWidgets.QVBoxLayout() 226 | tnav = NavigationToolbar(self.canvas, self) 227 | 228 | toolbar = QtWidgets.QToolBar() 229 | 230 | button_plot_prevspectro = QtWidgets.QPushButton('<--Previous spectrogram') 231 | button_plot_prevspectro.clicked.connect(self.plot_previous_spectro) 232 | toolbar.addWidget(button_plot_prevspectro) 233 | 234 | toolbar.addWidget(QtWidgets.QLabel(' ')) 235 | 236 | button_plot_spectro = QtWidgets.QPushButton('Next spectrogram-->') 237 | button_plot_spectro.clicked.connect(self.plot_next_spectro) 238 | toolbar.addWidget(button_plot_spectro) 239 | 240 | toolbar.addWidget(QtWidgets.QLabel(' ')) 241 | 242 | button_play_audio = QtWidgets.QPushButton('Play/Stop [spacebar]') 243 | button_play_audio.clicked.connect(self.func_playaudio) 244 | toolbar.addWidget(button_play_audio) 245 | 246 | toolbar.addWidget(QtWidgets.QLabel(' ')) 247 | toolbar.addWidget(QtWidgets.QLabel('Playback speed:')) 248 | toolbar.addWidget(QtWidgets.QLabel(' ')) 249 | toolbar.addWidget(self.playbackspeed) 250 | toolbar.addWidget(QtWidgets.QLabel(' ')) 251 | 252 | toolbar.addSeparator() 253 | toolbar.addWidget(QtWidgets.QLabel(' ')) 254 | 255 | toolbar.addWidget(QtWidgets.QLabel('fft_size[bits]:')) 256 | toolbar.addWidget(QtWidgets.QLabel(' ')) 257 | toolbar.addWidget(self.fft_size) 258 | toolbar.addWidget(QtWidgets.QLabel(' ')) 259 | toolbar.addWidget(QtWidgets.QLabel('fft_overlap[0-1]:')) 260 | toolbar.addWidget(QtWidgets.QLabel(' ')) 261 | toolbar.addWidget(self.fft_overlap) 262 | toolbar.addWidget(QtWidgets.QLabel(' ')) 263 | toolbar.addWidget(QtWidgets.QLabel('Colormap:')) 264 | toolbar.addWidget(QtWidgets.QLabel(' ')) 265 | toolbar.addWidget(self.colormap_plot) 266 | toolbar.addWidget(QtWidgets.QLabel(' ')) 267 | 268 | toolbar.addSeparator() 269 | toolbar.addWidget(tnav) 270 | 271 | plot_layout.addWidget(toolbar) 272 | plot_layout.addWidget(self.canvas) 273 | 274 | outer_layout.addLayout(top2_layout) 275 | outer_layout.addLayout(top3_layout) 276 | outer_layout.addLayout(plot_layout) 277 | 278 | widget = QtWidgets.QWidget() 279 | widget.setLayout(outer_layout) 280 | self.setCentralWidget(widget) 281 | 282 | def exitfunc(self): 283 | """Exit the application""" 284 | self.stop_audio() 285 | QtWidgets.QApplication.instance().quit() 286 | self.close() 287 | 288 | def stop_audio(self): 289 | """Stop any playing audio""" 290 | if self.audio_playing: 291 | sd.stop() 292 | self.audio_playing = False 293 | 294 | def find_regions(self, db_threshold): 295 | """Find regions in spectrogram above threshold""" 296 | y1 = int(self.f_min.text()) 297 | y2 = int(self.f_max.text()) 298 | if y2 > (self.fs / 2): 299 | y2 = (self.fs / 2) 300 | t1 = self.plotwindow_startsecond 301 | t2 = self.plotwindow_startsecond + self.plotwindow_length 302 | 303 | ix_time = np.where((self.t >= t1) & (self.t < t2))[0] 304 | ix_f = np.where((self.f >= y1) & (self.f < y2))[0] 305 | 306 | t = self.t[ix_time] 307 | minimum_patcharea = 5 308 | 309 | plotsxx = self.Sxx[int(ix_f[0]):int(ix_f[-1]), int(ix_time[0]):int(ix_time[-1])] 310 | spectrog = 10 * np.log10(plotsxx) 311 | 312 | # Filter out background 313 | spec_mean = np.median(spectrog, axis=1) 314 | sxx_background = np.transpose(np.broadcast_to(spec_mean, np.transpose(spectrog).shape)) 315 | z = spectrog - sxx_background 316 | 317 | # Binary image, post-process the binary mask and compute labels 318 | mask = z > db_threshold 319 | mask = morphology.remove_small_objects(mask, 50, connectivity=30) 320 | mask = morphology.remove_small_holes(mask, 50, connectivity=30) 321 | mask = closing(mask, disk(3)) 322 | 323 | labels = measure.label(mask) 324 | 325 | probs = measure.regionprops_table(labels, spectrog, properties=['label', 'area', 'mean_intensity', 326 | 'orientation', 'major_axis_length', 327 | 'minor_axis_length', 'weighted_centroid', 328 | 'bbox']) 329 | df = pd.DataFrame(probs) 330 | 331 | # Get correct f and t 332 | ff = self.f[ix_f[0]:ix_f[-1]] 333 | ix = df['bbox-0'] > len(ff) - 1 334 | df.loc[ix, 'bbox-0'] = len(ff) - 1 335 | ix = df['bbox-2'] > len(ff) - 1 336 | df.loc[ix, 'bbox-2'] = len(ff) - 1 337 | 338 | df['f-1'] = ff[df['bbox-0']] 339 | df['f-2'] = ff[df['bbox-2']] 340 | df['f-width'] = df['f-2'] - df['f-1'] 341 | 342 | ix = df['bbox-1'] > len(t) - 1 343 | df.loc[ix, 'bbox-1'] = len(t) - 1 344 | ix = df['bbox-3'] > len(t) - 1 345 | df.loc[ix, 'bbox-3'] = len(t) - 1 346 | 347 | df['t-1'] = t[df['bbox-1']] 348 | df['t-2'] = t[df['bbox-3']] 349 | df['duration'] = df['t-2'] - df['t-1'] 350 | 351 | indices = np.where((df['area'] < minimum_patcharea) | 352 | (df['bbox-3'] - df['bbox-1'] < 3) | 353 | (df['bbox-2'] - df['bbox-0'] < 3))[0] 354 | df = df.drop(indices) 355 | df = df.reset_index() 356 | 357 | df['id'] = np.arange(len(df)) 358 | 359 | # Get region dict 360 | patches = {} 361 | p_t_dict = {} 362 | p_f_dict = {} 363 | 364 | for ix in range(len(df)): 365 | m = labels == df.loc[ix, 'label'] 366 | ix1 = df.loc[ix, 'bbox-1'] 367 | ix2 = df.loc[ix, 'bbox-3'] 368 | jx1 = df.loc[ix, 'bbox-0'] 369 | jx2 = df.loc[ix, 'bbox-2'] 370 | 371 | patch = m[jx1:jx2, ix1:ix2] 372 | pt = t[ix1:ix2] 373 | pt = pt - pt[0] 374 | pf = ff[jx1:jx2] 375 | 376 | patches[df['id'][ix]] = patch 377 | p_t_dict[df['id'][ix]] = pt 378 | p_f_dict[df['id'][ix]] = pf 379 | 380 | self.detectiondf = df 381 | self.patches = patches 382 | self.p_t_dict = p_t_dict 383 | self.p_f_dict = p_f_dict 384 | self.region_labels = labels 385 | 386 | def match_bbox_and_iou(self, template): 387 | """Match detected regions with template using IoU""" 388 | shape_f = template['Frequency_in_Hz'].values 389 | shape_t = template['Time_in_s'].values 390 | shape_t = shape_t - shape_t.min() 391 | 392 | df = self.detectiondf 393 | patches = self.patches 394 | p_t_dict = self.p_t_dict 395 | p_f_dict = self.p_f_dict 396 | 397 | score_ioubox = [] 398 | smc_rs = [] 399 | 400 | for ix in df.index: 401 | patch = patches[ix] 402 | pf = p_f_dict[ix] 403 | pt = p_t_dict[ix] 404 | pt = pt - pt[0] 405 | 406 | if df.loc[ix, 'f-1'] < shape_f.min(): 407 | f1 = df.loc[ix, 'f-1'] 408 | else: 409 | f1 = shape_f.min() 410 | if df.loc[ix, 'f-2'] > shape_f.max(): 411 | f2 = df.loc[ix, 'f-2'] 412 | else: 413 | f2 = shape_f.max() 414 | 415 | time_step = np.diff(pt)[0] 416 | f_step = np.diff(pf)[0] 417 | k_f = np.arange(f1, f2, f_step) 418 | 419 | if pt.max() > shape_t.max(): 420 | k_t = pt 421 | else: 422 | k_t = np.arange(0, shape_t.max(), time_step) 423 | 424 | # IoU bounding box 425 | iou_kernel = np.zeros([k_f.shape[0], k_t.shape[0]]) 426 | ixp2 = np.where((k_t >= shape_t.min()) & (k_t <= shape_t.max()))[0] 427 | ixp1 = np.where((k_f >= shape_f.min()) & (k_f <= shape_f.max()))[0] 428 | iou_kernel[ixp1[0]:ixp1[-1], ixp2[0]:ixp2[-1]] = 1 429 | 430 | iou_patch = np.zeros([k_f.shape[0], k_t.shape[0]]) 431 | ixp2 = np.where((k_t >= pt[0]) & (k_t <= pt[-1]))[0] 432 | ixp1 = np.where((k_f >= pf[0]) & (k_f <= pf[-1]))[0] 433 | iou_patch[ixp1[0]:ixp1[-1], ixp2[0]:ixp2[-1]] = 1 434 | 435 | intersection = iou_kernel.astype('bool') & iou_patch.astype('bool') 436 | union = iou_kernel.astype('bool') | iou_patch.astype('bool') 437 | iou_bbox = np.sum(intersection) / np.sum(union) 438 | score_ioubox.append(iou_bbox) 439 | 440 | patch_rs = resize(patch, (50, 50)) 441 | n_resize = 50 442 | k_t = np.linspace(0, shape_t.max(), n_resize) 443 | k_f = np.linspace(shape_f.min(), shape_f.max(), n_resize) 444 | kk_t, kk_f = np.meshgrid(k_t, k_f) 445 | x, y = kk_t.flatten(), kk_f.flatten() 446 | points = np.vstack((x, y)).T 447 | p = Path(list(zip(shape_t, shape_f))) 448 | grid = p.contains_points(points) 449 | kernel_rs = grid.reshape(kk_t.shape) 450 | smc_rs.append(np.sum(kernel_rs.astype('bool') == patch_rs.astype('bool')) / len(patch_rs.flatten())) 451 | 452 | smc_rs = np.array(smc_rs) 453 | score_ioubox = np.array(score_ioubox) 454 | 455 | score = score_ioubox * (smc_rs - 0.5) / 0.5 456 | return score 457 | 458 | def automatic_detector_specgram_corr(self): 459 | """Run spectrogram correlation detector on current file""" 460 | self.detectiondf = pd.DataFrame([]) 461 | 462 | templatefiles, ok1 = QtWidgets.QFileDialog.getOpenFileNames( 463 | self, "QFileDialog.getOpenFileNames()", "", "CSV file (*.csv)") 464 | 465 | if not ok1: 466 | return 467 | 468 | templates = [] 469 | for fnam in templatefiles: 470 | template = pd.read_csv(fnam, index_col=0) 471 | templates.append(template) 472 | 473 | corrscore_threshold, ok = QtWidgets.QInputDialog.getDouble( 474 | self, 'Input Dialog', 'Enter correlation threshold in (0-1):', decimals=2) 475 | 476 | if not ok: 477 | return 478 | 479 | corrscore_threshold = max(0, min(1, corrscore_threshold)) 480 | 481 | if templates[0].columns[0] == 'Time_in_s': 482 | self._run_shape_correlation(templates, corrscore_threshold) 483 | else: 484 | self._run_image_correlation(templates, corrscore_threshold) 485 | 486 | print(self.detectiondf) 487 | print('done!!!') 488 | self.plot_spectrogram() 489 | 490 | def _run_shape_correlation(self, templates, corrscore_threshold): 491 | """Run shape-based correlation""" 492 | offset_f = 10 493 | offset_t = 0.5 494 | 495 | shape_f = np.array([]) 496 | shape_t_raw = np.array([]) 497 | for template in templates: 498 | shape_f = np.concatenate([shape_f, template['Frequency_in_Hz'].values]) 499 | shape_t_raw = np.concatenate([shape_t_raw, template['Time_in_s'].values]) 500 | shape_t = shape_t_raw - shape_t_raw.min() 501 | 502 | f_lim = [shape_f.min() - offset_f, shape_f.max() + offset_f] 503 | k_length_seconds = shape_t.max() + offset_t * 2 504 | 505 | time_step = np.diff(self.t)[0] 506 | k_t = np.linspace(0, k_length_seconds, int(k_length_seconds / time_step)) 507 | ix_f = np.where((self.f >= f_lim[0]) & (self.f <= f_lim[1]))[0] 508 | k_f = self.f[ix_f[0]:ix_f[-1]] 509 | 510 | kk_t, kk_f = np.meshgrid(k_t, k_f) 511 | kernel = np.zeros([k_f.shape[0], k_t.shape[0]]) 512 | 513 | x, y = kk_t.flatten(), kk_f.flatten() 514 | points = np.vstack((x, y)).T 515 | 516 | for template in templates: 517 | shf = template['Frequency_in_Hz'].values 518 | st = template['Time_in_s'].values 519 | st = st - shape_t_raw.min() 520 | p = Path(list(zip(st, shf))) 521 | grid = p.contains_points(points) 522 | kern = grid.reshape(kk_t.shape) 523 | kernel[kern > 0] = 1 524 | 525 | ix_f = np.where((self.f >= f_lim[0]) & (self.f <= f_lim[1]))[0] 526 | spectrog = 10 * np.log10(self.Sxx[ix_f[0]:ix_f[-1], :]) 527 | 528 | result = match_template(spectrog, kernel) 529 | corr_score = result[0, :] 530 | t_score = np.linspace(self.t[int(kernel.shape[1] / 2)], 531 | self.t[-int(kernel.shape[1] / 2)], corr_score.shape[0]) 532 | 533 | peaks_indices = find_peaks(corr_score, height=corrscore_threshold)[0] 534 | 535 | if len(peaks_indices) > 0: 536 | t1, t2, f1, f2, score = [], [], [], [], [] 537 | for ixpeak in peaks_indices: 538 | tstar = t_score[ixpeak] - k_length_seconds / 2 - offset_t 539 | tend = t_score[ixpeak] + k_length_seconds / 2 - offset_t 540 | t1.append(tstar) 541 | t2.append(tend) 542 | f1.append(f_lim[0] + offset_f) 543 | f2.append(f_lim[1] - offset_f) 544 | score.append(corr_score[ixpeak]) 545 | 546 | df = pd.DataFrame({'t-1': t1, 't-2': t2, 'f-1': f1, 'f-2': f2, 'score': score}) 547 | self.detectiondf = df.copy() 548 | self.detectiondf['audiofilename'] = self.current_audiopath 549 | self.detectiondf['threshold'] = corrscore_threshold 550 | 551 | def _run_image_correlation(self, templates, corrscore_threshold): 552 | """Run image-based correlation""" 553 | template = templates[0] 554 | 555 | k_length_seconds = float(template.columns[-1]) - float(template.columns[0]) 556 | f_lim = [int(template.index[0]), int(template.index[-1])] 557 | 558 | ix_f = np.where((self.f >= f_lim[0]) & (self.f <= f_lim[1]))[0] 559 | spectrog = 10 * np.log10(self.Sxx[ix_f[0]:ix_f[-1], :]) 560 | specgram_t_step = self.t[1] - self.t[0] 561 | n_f = spectrog.shape[0] 562 | n_t = int(k_length_seconds / specgram_t_step) 563 | 564 | kernel = resize(template.values, [n_f, n_t]) 565 | 566 | result = match_template(spectrog, kernel) 567 | corr_score = result[0, :] 568 | t_score = np.linspace(self.t[int(kernel.shape[1] / 2)], 569 | self.t[-int(kernel.shape[1] / 2)], corr_score.shape[0]) 570 | 571 | peaks_indices = find_peaks(corr_score, height=corrscore_threshold)[0] 572 | 573 | if len(peaks_indices) > 0: 574 | t1, t2, f1, f2, score = [], [], [], [], [] 575 | for ixpeak in peaks_indices: 576 | tstar = t_score[ixpeak] - k_length_seconds / 2 577 | tend = t_score[ixpeak] + k_length_seconds / 2 578 | t1.append(tstar) 579 | t2.append(tend) 580 | f1.append(f_lim[0]) 581 | f2.append(f_lim[1]) 582 | score.append(corr_score[ixpeak]) 583 | 584 | df = pd.DataFrame({'t-1': t1, 't-2': t2, 'f-1': f1, 'f-2': f2, 'score': score}) 585 | self.detectiondf = df.copy() 586 | self.detectiondf['audiofilename'] = self.current_audiopath 587 | self.detectiondf['threshold'] = corrscore_threshold 588 | 589 | def automatic_detector_specgram_corr_allfiles(self): 590 | """Run spectrogram correlation detector on all files""" 591 | msg = QtWidgets.QMessageBox() 592 | msg.setIcon(QtWidgets.QMessageBox.Information) 593 | msg.setText(f"Are you sure you want to run the detector over {self.file_blocks.shape[0]} files?") 594 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 595 | returnValue = msg.exec() 596 | 597 | if returnValue != QtWidgets.QMessageBox.Yes: 598 | return 599 | 600 | templatefiles, ok1 = QtWidgets.QFileDialog.getOpenFileNames( 601 | self, "QFileDialog.getOpenFileNames()", "", "CSV file (*.csv)") 602 | 603 | if not ok1: 604 | return 605 | 606 | templates = [] 607 | for fnam in templatefiles: 608 | template = pd.read_csv(fnam, index_col=0) 609 | templates.append(template) 610 | 611 | corrscore_threshold, ok = QtWidgets.QInputDialog.getDouble( 612 | self, 'Input Dialog', 'Enter correlation threshold in (0-1):', decimals=2) 613 | 614 | if not ok: 615 | return 616 | 617 | corrscore_threshold = max(0, min(1, corrscore_threshold)) 618 | self.detectiondf_all = pd.DataFrame([]) 619 | 620 | for i_block in range(len(self.file_blocks)): 621 | audiopath = self.file_blocks.loc[i_block, 'fname'] 622 | 623 | if self.filename_timekey.text() == '': 624 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 625 | else: 626 | try: 627 | self.time = dt.datetime.strptime(audiopath.split('/')[-1], self.filename_timekey.text()) 628 | except: 629 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 630 | 631 | if self.file_blocks.loc[i_block, 'start'] > 0: 632 | secoffset = self.file_blocks.loc[i_block, 'start'] / self.fs 633 | self.time = self.time + pd.Timedelta(seconds=secoffset) 634 | 635 | self.x, self.fs = sf.read(audiopath, dtype='int16', 636 | start=self.file_blocks.loc[i_block, 'start'], 637 | stop=self.file_blocks.loc[i_block, 'end']) 638 | print(f'Processing file: {audiopath}') 639 | 640 | if len(self.x.shape) > 1: 641 | if np.shape(self.x)[1] > 1: 642 | self.x = self.x[:, 0] 643 | 644 | db_saturation = float(self.db_saturation.text()) 645 | x = self.x / 32767 646 | p = np.power(10, (db_saturation / 20)) * x 647 | 648 | fft_size = int(self.fft_size.currentText()) 649 | fft_overlap = float(self.fft_overlap.currentText()) 650 | 651 | self.f, self.t, self.Sxx = signal.spectrogram( 652 | p, self.fs, window='hamming', nperseg=fft_size, noverlap=int(fft_size * fft_overlap)) 653 | 654 | if self.file_blocks.loc[i_block, 'start'] > 0: 655 | secoffset = self.file_blocks.loc[i_block, 'start'] / self.fs 656 | self.t = self.t + secoffset 657 | 658 | if templates[0].columns[0] == 'Time_in_s': 659 | self._run_shape_correlation(templates, corrscore_threshold) 660 | else: 661 | self._run_image_correlation(templates, corrscore_threshold) 662 | 663 | if hasattr(self, 'detectiondf') and not self.detectiondf.empty: 664 | self.detectiondf_all = pd.concat([self.detectiondf_all, self.detectiondf]) 665 | self.detectiondf_all = self.detectiondf_all.reset_index(drop=True) 666 | 667 | self.detectiondf = self.detectiondf_all 668 | self.read_wav() 669 | self.plot_spectrogram() 670 | print('Done!!!') 671 | 672 | def automatic_detector_shapematching(self): 673 | """Run shape matching detector on current file""" 674 | self.detectiondf = pd.DataFrame([]) 675 | 676 | templatefiles, ok1 = QtWidgets.QFileDialog.getOpenFileNames( 677 | self, "QFileDialog.getOpenFileNames()", "", "CSV file (*.csv)") 678 | 679 | if not ok1: 680 | return 681 | 682 | db_threshold, ok = QtWidgets.QInputDialog.getInt( 683 | self, 'Input Dialog', 'Enter signal-to-noise threshold in dB:') 684 | 685 | if not ok: 686 | return 687 | 688 | print(f'Threshold: {db_threshold} dB') 689 | self.detectiondf = pd.DataFrame([]) 690 | 691 | self.find_regions(db_threshold) 692 | self.detectiondf['score'] = np.zeros(len(self.detectiondf)) 693 | 694 | for fnam in templatefiles: 695 | template = pd.read_csv(fnam, index_col=0) 696 | score_new = self.match_bbox_and_iou(template) 697 | ix_better = score_new > self.detectiondf['score'].values 698 | self.detectiondf.loc[ix_better, 'score'] = score_new[ix_better] 699 | 700 | ixdel = np.where(self.detectiondf['score'] < 0.01)[0] 701 | self.detectiondf = self.detectiondf.drop(ixdel) 702 | self.detectiondf = self.detectiondf.reset_index(drop=True) 703 | self.detectiondf['audiofilename'] = self.current_audiopath 704 | self.detectiondf['threshold'] = db_threshold 705 | 706 | print(self.detectiondf) 707 | self.plot_spectrogram() 708 | 709 | def automatic_detector_shapematching_allfiles(self): 710 | """Run shape matching detector on all files""" 711 | msg = QtWidgets.QMessageBox() 712 | msg.setIcon(QtWidgets.QMessageBox.Information) 713 | msg.setText(f"Are you sure you want to run the detector over {self.filenames.shape[0]} files?") 714 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 715 | returnValue = msg.exec() 716 | 717 | if returnValue != QtWidgets.QMessageBox.Yes: 718 | return 719 | 720 | templatefiles, ok1 = QtWidgets.QFileDialog.getOpenFileNames( 721 | self, "QFileDialog.getOpenFileNames()", "", "CSV file (*.csv)") 722 | 723 | if not ok1: 724 | return 725 | 726 | db_threshold, ok = QtWidgets.QInputDialog.getInt( 727 | self, 'Input Dialog', 'Enter signal-to-noise threshold in dB:') 728 | 729 | if not ok: 730 | return 731 | 732 | self.detectiondf_all = pd.DataFrame([]) 733 | 734 | for i_block in range(len(self.file_blocks)): 735 | audiopath = self.file_blocks.loc[i_block, 'fname'] 736 | 737 | if self.filename_timekey.text() == '': 738 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 739 | else: 740 | try: 741 | self.time = dt.datetime.strptime(audiopath.split('/')[-1], self.filename_timekey.text()) 742 | except: 743 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 744 | 745 | self.x, self.fs = sf.read(audiopath, dtype='int16', 746 | start=self.file_blocks.loc[i_block, 'start'], 747 | stop=self.file_blocks.loc[i_block, 'end']) 748 | print(f'Processing file: {audiopath}') 749 | 750 | if len(self.x.shape) > 1: 751 | if np.shape(self.x)[1] > 1: 752 | self.x = self.x[:, 0] 753 | 754 | db_saturation = float(self.db_saturation.text()) 755 | x = self.x / 32767 756 | p = np.power(10, (db_saturation / 20)) * x 757 | 758 | fft_size = int(self.fft_size.currentText()) 759 | fft_overlap = float(self.fft_overlap.currentText()) 760 | 761 | self.f, self.t, self.Sxx = signal.spectrogram( 762 | p, self.fs, window='hamming', nperseg=fft_size, noverlap=int(fft_size * fft_overlap)) 763 | 764 | if self.file_blocks.loc[i_block, 'start'] > 0: 765 | secoffset = self.file_blocks.loc[i_block, 'start'] / self.fs 766 | self.t = self.t + secoffset 767 | 768 | self.plotwindow_startsecond = 0 769 | self.plotwindow_length = self.t.max() 770 | 771 | self.detectiondf = pd.DataFrame([]) 772 | self.find_regions(db_threshold) 773 | self.detectiondf['score'] = np.zeros(len(self.detectiondf)) 774 | 775 | for fnam in templatefiles: 776 | template = pd.read_csv(fnam, index_col=0) 777 | score_new = self.match_bbox_and_iou(template) 778 | ix_better = score_new > self.detectiondf['score'].values 779 | self.detectiondf.loc[ix_better, 'score'] = score_new[ix_better] 780 | 781 | ixdel = np.where(self.detectiondf['score'] < 0.01)[0] 782 | self.detectiondf = self.detectiondf.drop(ixdel) 783 | self.detectiondf = self.detectiondf.reset_index(drop=True) 784 | self.detectiondf['audiofilename'] = audiopath 785 | self.detectiondf['threshold'] = db_threshold 786 | 787 | self.detectiondf_all = pd.concat([self.detectiondf_all, self.detectiondf]) 788 | self.detectiondf_all = self.detectiondf_all.reset_index(drop=True) 789 | 790 | print(self.detectiondf_all) 791 | 792 | self.detectiondf = self.detectiondf_all 793 | print('Done!!!') 794 | 795 | def export_automatic_detector(self): 796 | """Export automatic detection results to CSV""" 797 | if self.detectiondf.shape[0] > 0: 798 | savename = QtWidgets.QFileDialog.getSaveFileName( 799 | self, "QFileDialog.getSaveFileName()", "", "csv files (*.csv)") 800 | if len(savename[0]) > 0: 801 | self.detectiondf.to_csv(savename[0]) 802 | 803 | def openfilefunc(self): 804 | """Open audio files dialog""" 805 | fname_candidates, ok = QtWidgets.QFileDialog.getOpenFileNames( 806 | self, "QFileDialog.getOpenFileNames()", '', 807 | "Audio Files (*.wav *.aif *.aiff *.aifc *.ogg *.flac)") 808 | 809 | if len(fname_candidates) == 0: 810 | return 811 | 812 | self.filenames = np.array(fname_candidates) 813 | self.filecounter = -1 814 | self.plotwindow_startsecond = float(self.t_length.text()) 815 | 816 | self.annotation = pd.DataFrame({ 817 | 't1': pd.Series(dtype='datetime64[ns]'), 818 | 't2': pd.Series(dtype='datetime64[ns]'), 819 | 'f1': pd.Series(dtype='float'), 820 | 'f2': pd.Series(dtype='float'), 821 | 'label': pd.Series(dtype='object'), 822 | 'audiofilename': pd.Series(dtype='object') 823 | }) 824 | self.detectiondf = pd.DataFrame([]) 825 | 826 | # Create file blocks for large files 827 | fid_names = [] 828 | fid_start = [] 829 | fid_end = [] 830 | max_element_length_sec = 60 * 10 831 | 832 | for fname in self.filenames: 833 | a = sf.info(fname) 834 | 835 | if a.duration < max_element_length_sec: 836 | fid_names.append(fname) 837 | fid_start.append(0) 838 | fid_end.append(a.frames) 839 | else: 840 | s = 0 841 | while s < a.frames: 842 | fid_names.append(fname) 843 | fid_start.append(s) 844 | e = s + max_element_length_sec * a.samplerate 845 | fid_end.append(e) 846 | s = s + max_element_length_sec * a.samplerate 847 | 848 | self.file_blocks = pd.DataFrame({ 849 | 'fname': fid_names, 850 | 'start': fid_start, 851 | 'end': fid_end 852 | }) 853 | 854 | print(self.file_blocks) 855 | self.plotwindow_startsecond = 0 856 | self.plot_next_spectro() 857 | 858 | def read_wav(self): 859 | """Read WAV file and compute spectrogram""" 860 | if self.filecounter < 0: 861 | return 862 | 863 | self.current_audiopath = self.file_blocks.loc[self.filecounter, 'fname'] 864 | 865 | self.x, self.fs = sf.read( 866 | self.current_audiopath, 867 | start=self.file_blocks.loc[self.filecounter, 'start'], 868 | stop=self.file_blocks.loc[self.filecounter, 'end'], 869 | dtype='int16' 870 | ) 871 | 872 | if self.filename_timekey.text() == '': 873 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 874 | else: 875 | try: 876 | self.time = dt.datetime.strptime( 877 | self.current_audiopath.split('/')[-1], 878 | self.filename_timekey.text() 879 | ) 880 | except: 881 | print('Wrong filename format') 882 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 883 | 884 | if self.file_blocks.loc[self.filecounter, 'start'] > 0: 885 | secoffset = self.file_blocks.loc[self.filecounter, 'start'] / self.fs 886 | self.time = self.time + pd.Timedelta(seconds=secoffset) 887 | 888 | print(f'Open new file: {self.current_audiopath}') 889 | print(f'FS: {self.fs}, x: {np.shape(self.x)}') 890 | 891 | if len(self.x.shape) > 1: 892 | if np.shape(self.x)[1] > 1: 893 | self.x = self.x[:, 0] 894 | 895 | db_saturation = float(self.db_saturation.text()) 896 | x = self.x / 32767 897 | p = np.power(10, (db_saturation / 20)) * x 898 | 899 | fft_size = int(self.fft_size.currentText()) 900 | fft_overlap = float(self.fft_overlap.currentText()) 901 | 902 | self.f, self.t, self.Sxx = signal.spectrogram( 903 | p, self.fs, window='hamming', nperseg=fft_size, noverlap=int(fft_size * fft_overlap)) 904 | 905 | if self.file_blocks.loc[self.filecounter, 'start'] > 0: 906 | secoffset = self.file_blocks.loc[self.filecounter, 'start'] / self.fs 907 | self.t = self.t + secoffset 908 | 909 | def plot_annotation_box(self, annotation_row): 910 | """Plot annotation box on spectrogram""" 911 | x1 = annotation_row.iloc[0, 0] 912 | x2 = annotation_row.iloc[0, 1] 913 | 914 | xt = pd.Series([x1, x2]) 915 | tt = xt - np.array(self.time).astype('datetime64[ns]') 916 | xt = tt.dt.seconds + tt.dt.microseconds / 10**6 917 | x1 = xt.iloc[0] 918 | x2 = xt.iloc[1] 919 | 920 | y1 = annotation_row.iloc[0, 2] 921 | y2 = annotation_row.iloc[0, 3] 922 | c_label = annotation_row.iloc[0, 4] 923 | 924 | line_x = [x2, x1, x1, x2, x2] 925 | line_y = [y1, y1, y2, y2, y1] 926 | 927 | xmin = np.min([x1, x2]) 928 | ymax = np.max([y1, y2]) 929 | 930 | self.canvas.axes.plot(line_x, line_y, '-b', linewidth=0.75) 931 | self.canvas.axes.text(xmin, ymax, c_label, size=8) 932 | 933 | def plot_spectrogram(self): 934 | """Plot spectrogram""" 935 | if self.filecounter < 0: 936 | return 937 | 938 | self.canvas.fig.clf() 939 | self.canvas.axes = self.canvas.fig.add_subplot(111) 940 | 941 | if self.t_length.text() == '': 942 | self.plotwindow_length = self.t[-1] 943 | self.plotwindow_startsecond = self.t[0] 944 | else: 945 | self.plotwindow_length = float(self.t_length.text()) 946 | if self.t[-1] < self.plotwindow_length: 947 | self.plotwindow_startsecond = self.t[0] 948 | self.plotwindow_length = self.t[-1] 949 | 950 | y1 = int(self.f_min.text()) 951 | y2 = int(self.f_max.text()) 952 | if y2 > (self.fs / 2): 953 | y2 = (self.fs / 2) 954 | t1 = self.plotwindow_startsecond 955 | t2 = self.plotwindow_startsecond + self.plotwindow_length 956 | 957 | ix_time = np.where((self.t >= t1) & (self.t < t2))[0] 958 | ix_f = np.where((self.f >= y1) & (self.f < y2))[0] 959 | 960 | plotsxx = self.Sxx[int(ix_f[0]):int(ix_f[-1]), int(ix_time[0]):int(ix_time[-1])] 961 | plotsxx_db = 10 * np.log10(plotsxx) 962 | 963 | if self.checkbox_background.isChecked(): 964 | spec_mean = np.median(plotsxx_db, axis=1) 965 | sxx_background = np.transpose(np.broadcast_to(spec_mean, np.transpose(plotsxx_db).shape)) 966 | plotsxx_db = plotsxx_db - sxx_background 967 | plotsxx_db = plotsxx_db - np.min(plotsxx_db.flatten()) 968 | 969 | colormap_plot = self.colormap_plot.currentText() 970 | img = self.canvas.axes.imshow( 971 | plotsxx_db, aspect='auto', cmap=colormap_plot, origin='lower', extent=[t1, t2, y1, y2]) 972 | 973 | self.canvas.axes.set_ylabel('Frequency [Hz]') 974 | self.canvas.axes.set_xlabel('Time [sec]') 975 | 976 | if self.checkbox_logscale.isChecked(): 977 | self.canvas.axes.set_yscale('log') 978 | else: 979 | self.canvas.axes.set_yscale('linear') 980 | 981 | if self.filename_timekey.text() == '': 982 | self.canvas.axes.set_title(self.current_audiopath.split('/')[-1]) 983 | else: 984 | self.canvas.axes.set_title(self.time) 985 | 986 | clims = img.get_clim() 987 | if (self.db_vmin.text() == '') & (self.db_vmax.text() != ''): 988 | img.set_clim([clims[0], float(self.db_vmax.text())]) 989 | if (self.db_vmin.text() != '') & (self.db_vmax.text() == ''): 990 | img.set_clim([float(self.db_vmin.text()), clims[1]]) 991 | if (self.db_vmin.text() != '') & (self.db_vmax.text() != ''): 992 | img.set_clim([float(self.db_vmin.text()), float(self.db_vmax.text())]) 993 | 994 | self.canvas.fig.colorbar(img, label=r'PSD [dB re $1 \mu Pa Hz^{-1}$]') 995 | 996 | # Plot annotations 997 | if self.annotation.shape[0] > 0: 998 | ix = (self.annotation['t1'] > (np.array(self.time).astype('datetime64[ns]') + 999 | pd.Timedelta(self.plotwindow_startsecond, unit="s"))) & \ 1000 | (self.annotation['t1'] < (np.array(self.time).astype('datetime64[ns]') + 1001 | pd.Timedelta(self.plotwindow_startsecond + self.plotwindow_length, unit="s"))) & \ 1002 | (self.annotation['audiofilename'] == self.current_audiopath) 1003 | 1004 | if np.sum(ix) > 0: 1005 | ix = np.where(ix)[0] 1006 | for ix_x in ix: 1007 | a = pd.DataFrame([self.annotation.iloc[ix_x, :]]) 1008 | self.plot_annotation_box(a) 1009 | 1010 | # Plot detections 1011 | cmap = plt.get_cmap('cool') 1012 | if self.detectiondf.shape[0] > 0: 1013 | for i in range(self.detectiondf.shape[0]): 1014 | insidewindow = (self.detectiondf.loc[i, 't-1'] > self.plotwindow_startsecond) & \ 1015 | (self.detectiondf.loc[i, 't-2'] < (self.plotwindow_startsecond + self.plotwindow_length)) & \ 1016 | (self.detectiondf.loc[i, 'audiofilename'] == self.current_audiopath) 1017 | 1018 | scoremin = self.detectiondf['score'].min() 1019 | scoremax = self.detectiondf['score'].max() 1020 | 1021 | if (self.detectiondf.loc[i, 'score'] >= 0.01) & insidewindow: 1022 | xx1 = self.detectiondf.loc[i, 't-1'] 1023 | xx2 = self.detectiondf.loc[i, 't-2'] 1024 | yy1 = self.detectiondf.loc[i, 'f-1'] 1025 | yy2 = self.detectiondf.loc[i, 'f-2'] 1026 | scorelabel = str(np.round(self.detectiondf.loc[i, 'score'], 2)) 1027 | snorm = (self.detectiondf.loc[i, 'score'] - scoremin) / (scoremax - scoremin) 1028 | scorecolor = cmap(snorm) 1029 | 1030 | line_x = [xx2, xx1, xx1, xx2, xx2] 1031 | line_y = [yy1, yy1, yy2, yy2, yy1] 1032 | 1033 | xmin = np.min([xx1, xx2]) 1034 | ymax = np.max([yy1, yy2]) 1035 | self.canvas.axes.plot(line_x, line_y, '-', color=scorecolor, linewidth=0.75) 1036 | self.canvas.axes.text(xmin, ymax, scorelabel, size=8, color=scorecolor) 1037 | 1038 | self.canvas.axes.set_ylim([y1, y2]) 1039 | self.canvas.axes.set_xlim([t1, t2]) 1040 | 1041 | self.canvas.fig.tight_layout() 1042 | self.toggle_selector = RectangleSelector( 1043 | self.canvas.axes, self.box_select_callback, 1044 | useblit=False, button=[1], 1045 | interactive=False, 1046 | props=dict(facecolor="blue", edgecolor="black", alpha=0.1, fill=True)) 1047 | 1048 | self.canvas.draw() 1049 | self.cid1 = self.canvas.fig.canvas.mpl_connect('button_press_event', self.onclick) 1050 | 1051 | def plot_spectrogram_threshold(self): 1052 | """Plot spectrogram with threshold regions""" 1053 | if self.filecounter < 0: 1054 | return 1055 | 1056 | db_threshold, ok = QtWidgets.QInputDialog.getInt( 1057 | self, 'Input Dialog', 'Enter signal-to-noise threshold in dB:') 1058 | 1059 | if not ok: 1060 | return 1061 | 1062 | self.find_regions(db_threshold) 1063 | self.detectiondf = pd.DataFrame([]) 1064 | 1065 | self.canvas.fig.clf() 1066 | self.canvas.axes = self.canvas.fig.add_subplot(111) 1067 | 1068 | self.canvas.axes.set_ylabel('Frequency [Hz]') 1069 | 1070 | if self.checkbox_logscale.isChecked(): 1071 | self.canvas.axes.set_yscale('log') 1072 | else: 1073 | self.canvas.axes.set_yscale('linear') 1074 | 1075 | img = self.canvas.axes.imshow( 1076 | self.region_labels > 0, aspect='auto', cmap='gist_yarg', origin='lower') 1077 | 1078 | self.canvas.fig.colorbar(img) 1079 | self.canvas.fig.tight_layout() 1080 | self.canvas.draw() 1081 | 1082 | def export_zoomed_sgram_as_csv(self): 1083 | """Export zoomed spectrogram as CSV""" 1084 | if self.filecounter < 0: 1085 | return 1086 | 1087 | spectrog = 10 * np.log10(self.Sxx) 1088 | 1089 | msg = QtWidgets.QMessageBox() 1090 | msg.setIcon(QtWidgets.QMessageBox.Information) 1091 | msg.setText("Remove background?") 1092 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 1093 | returnValue = msg.exec() 1094 | 1095 | if returnValue == QtWidgets.QMessageBox.Yes: 1096 | rectime = pd.to_timedelta(self.t, 's') 1097 | spg = pd.DataFrame(np.transpose(spectrog), index=rectime) 1098 | bg = spg.resample('3min').mean().copy() 1099 | bg = bg.resample('1s').interpolate(method='time') 1100 | bg = bg.reindex(rectime, method='nearest') 1101 | background = np.transpose(bg.values) 1102 | z = spectrog - background 1103 | else: 1104 | z = spectrog 1105 | 1106 | self.f_limits = self.canvas.axes.get_ylim() 1107 | self.t_limits = self.canvas.axes.get_xlim() 1108 | y1 = int(self.f_limits[0]) 1109 | y2 = int(self.f_limits[1]) 1110 | t1 = self.t_limits[0] 1111 | t2 = self.t_limits[1] 1112 | 1113 | ix_time = np.where((self.t >= t1) & (self.t < t2))[0] 1114 | ix_f = np.where((self.f >= y1) & (self.f < y2))[0] 1115 | 1116 | plotsxx_db = z[int(ix_f[0]):int(ix_f[-1]), int(ix_time[0]):int(ix_time[-1])] 1117 | 1118 | sgram = pd.DataFrame(data=plotsxx_db, index=self.f[ix_f[:-1]], columns=self.t[ix_time[:-1]]) 1119 | print(sgram) 1120 | 1121 | savename = QtWidgets.QFileDialog.getSaveFileName(self, "", "csv files (*.csv)") 1122 | if len(savename[0]) > 0: 1123 | if savename[0][-4:] != '.csv': 1124 | savename = savename[0] + '.csv' 1125 | else: 1126 | savename = savename[0] 1127 | sgram.to_csv(savename) 1128 | 1129 | def box_select_callback(self, eclick, erelease): 1130 | """Callback for box selection""" 1131 | x1, y1 = eclick.xdata, eclick.ydata 1132 | x2, y2 = erelease.xdata, erelease.ydata 1133 | 1134 | x1 = self.time + pd.to_timedelta(x1, unit='s') 1135 | x2 = self.time + pd.to_timedelta(x2, unit='s') 1136 | 1137 | t1 = np.min([x1, x2]) 1138 | t2 = np.max([x1, x2]) 1139 | f1 = np.min([y1, y2]) 1140 | f2 = np.max([y1, y2]) 1141 | 1142 | if self.bg.checkedId() == -1: 1143 | c_label = '' 1144 | else: 1145 | c_label = eval(f'self.an_{self.bg.checkedId()}.text()') 1146 | 1147 | a = pd.DataFrame({ 1148 | 't1': pd.Series(t1, dtype='datetime64[ns]'), 1149 | 't2': pd.Series(t2, dtype='datetime64[ns]'), 1150 | 'f1': pd.Series(f1, dtype='float'), 1151 | 'f2': pd.Series(f2, dtype='float'), 1152 | 'label': pd.Series(c_label, dtype='object'), 1153 | 'audiofilename': self.current_audiopath 1154 | }) 1155 | 1156 | self.annotation = pd.concat([self.annotation, a], ignore_index=True) 1157 | self.plot_annotation_box(a) 1158 | 1159 | def onclick(self, event): 1160 | """Handle mouse clicks""" 1161 | if event.button == 3: 1162 | self.annotation = self.annotation.head(-1) 1163 | self.plot_spectrogram() 1164 | 1165 | def end_of_filelist_warning(self): 1166 | """Show end of file list warning""" 1167 | msg_listend = QtWidgets.QMessageBox() 1168 | msg_listend.setIcon(QtWidgets.QMessageBox.Information) 1169 | msg_listend.setText("End of file list reached!") 1170 | msg_listend.exec_() 1171 | 1172 | def plot_next_spectro(self): 1173 | """Plot next spectrogram""" 1174 | if len(self.filenames) == 0: 1175 | return 1176 | 1177 | print(f'Old filecounter: {self.filecounter}') 1178 | 1179 | if self.t_length.text() == '' or ((self.filecounter >= 0) and (self.t[-1] < float(self.t_length.text()))): 1180 | self.filecounter = self.filecounter + 1 1181 | if self.filecounter > self.file_blocks.shape[0] - 1: 1182 | self.filecounter = self.file_blocks.shape[0] - 1 1183 | print('That was it') 1184 | self.end_of_filelist_warning() 1185 | self.plotwindow_length = self.t[-1] 1186 | self.plotwindow_startsecond = self.t[0] 1187 | self.read_wav() 1188 | self.plot_spectrogram() 1189 | else: 1190 | self.plotwindow_length = float(self.t_length.text()) 1191 | self.plotwindow_startsecond = self.plotwindow_startsecond + self.plotwindow_length 1192 | 1193 | print([self.plotwindow_startsecond, self.t[0], self.t[-1]]) 1194 | 1195 | if self.plotwindow_startsecond > self.t[-1]: 1196 | # Save log 1197 | if self.checkbox_log.isChecked(): 1198 | tt = self.annotation['t1'] - self.time 1199 | t_in_seconds = np.array(tt.values * 1e-9, dtype='float16') 1200 | reclength = np.array(self.t[-1], dtype='float16') 1201 | 1202 | ix = (t_in_seconds > 0) & (t_in_seconds < reclength) 1203 | 1204 | calldata = self.annotation.iloc[ix, :] 1205 | print(calldata) 1206 | savename = self.current_audiopath 1207 | nn = savename[:-4] + f'_log_sec{int(self.t[0])}_to_sec{int(self.t[-1])}.csv' 1208 | calldata.to_csv(nn) 1209 | print(f'Writing log: {nn}') 1210 | 1211 | # New file 1212 | self.filecounter = self.filecounter + 1 1213 | if self.filecounter >= self.file_blocks.shape[0] - 1: 1214 | self.filecounter = self.file_blocks.shape[0] - 1 1215 | print('That was it') 1216 | self.end_of_filelist_warning() 1217 | self.read_wav() 1218 | self.plotwindow_startsecond = self.t[0] 1219 | self.plot_spectrogram() 1220 | else: 1221 | self.plot_spectrogram() 1222 | 1223 | def plot_previous_spectro(self): 1224 | """Plot previous spectrogram""" 1225 | if len(self.filenames) == 0: 1226 | return 1227 | 1228 | print(f'Old filecounter: {self.filecounter}') 1229 | 1230 | if self.t_length.text() == '' or ((self.filecounter >= 0) and (self.t[-1] < float(self.t_length.text()))): 1231 | self.filecounter = self.filecounter - 1 1232 | if self.filecounter < 0: 1233 | self.filecounter = 0 1234 | print('That was it') 1235 | self.end_of_filelist_warning() 1236 | self.plotwindow_length = self.t[-1] 1237 | self.plotwindow_startsecond = self.t[0] 1238 | self.read_wav() 1239 | self.plot_spectrogram() 1240 | else: 1241 | self.plotwindow_startsecond = self.plotwindow_startsecond - self.plotwindow_length 1242 | print([self.plotwindow_startsecond, self.t[0], self.t[-1]]) 1243 | 1244 | if self.plotwindow_startsecond < self.t[0]: 1245 | self.filecounter = self.filecounter - 1 1246 | 1247 | if self.filecounter < 0: 1248 | self.filecounter = 0 1249 | print('That was it') 1250 | self.end_of_filelist_warning() 1251 | 1252 | self.read_wav() 1253 | self.plot_spectrogram() 1254 | else: 1255 | self.plot_spectrogram() 1256 | 1257 | def new_fft_size_selected(self): 1258 | """Handle FFT size change""" 1259 | self.read_wav() 1260 | self.plot_spectrogram() 1261 | 1262 | def func_savecsv(self): 1263 | """Save annotations to CSV""" 1264 | savename = QtWidgets.QFileDialog.getSaveFileName( 1265 | self, "QFileDialog.getSaveFileName()", "", "csv files (*.csv)") 1266 | print(f'Location: {savename[0]}') 1267 | if len(savename[0]) > 0: 1268 | self.annotation.to_csv(savename[0]) 1269 | 1270 | def func_logging(self): 1271 | """Handle logging checkbox""" 1272 | if self.checkbox_log.isChecked(): 1273 | print('Logging enabled') 1274 | msg = QtWidgets.QMessageBox() 1275 | msg.setIcon(QtWidgets.QMessageBox.Information) 1276 | msg.setText("Overwrite existing log files?") 1277 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 1278 | returnValue = msg.exec() 1279 | 1280 | if returnValue == QtWidgets.QMessageBox.No: 1281 | ix_delete = [] 1282 | for i, fn in enumerate(self.filenames): 1283 | logpath = fn[:-4] + '_log.csv' 1284 | if os.path.isfile(logpath): 1285 | ix_delete.append(i) 1286 | 1287 | self.filenames = np.delete(self.filenames, ix_delete) 1288 | print('Updated filelist:') 1289 | print(self.filenames) 1290 | 1291 | def plot_all_spectrograms(self): 1292 | """Plot all spectrograms and save as images""" 1293 | msg = QtWidgets.QMessageBox() 1294 | msg.setIcon(QtWidgets.QMessageBox.Information) 1295 | msg.setText(f"Are you sure you want to plot {self.filenames.shape[0]} spectrograms?") 1296 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 1297 | returnValue = msg.exec() 1298 | 1299 | if returnValue != QtWidgets.QMessageBox.Yes: 1300 | return 1301 | 1302 | for audiopath in self.filenames: 1303 | if self.filename_timekey.text() == '': 1304 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 1305 | else: 1306 | try: 1307 | self.time = dt.datetime.strptime(audiopath.split('/')[-1], self.filename_timekey.text()) 1308 | except: 1309 | self.time = dt.datetime(2000, 1, 1, 0, 0, 0) 1310 | 1311 | self.x, self.fs = sf.read(audiopath, dtype='int16') 1312 | print(f'Processing file: {audiopath}') 1313 | 1314 | db_saturation = float(self.db_saturation.text()) 1315 | x = self.x / 32767 1316 | p = np.power(10, (db_saturation / 20)) * x 1317 | 1318 | fft_size = int(self.fft_size.currentText()) 1319 | fft_overlap = float(self.fft_overlap.currentText()) 1320 | 1321 | self.f, self.t, self.Sxx = signal.spectrogram( 1322 | p, self.fs, window='hamming', nperseg=fft_size, noverlap=int(fft_size * fft_overlap)) 1323 | 1324 | self.plotwindow_startsecond = 0 1325 | self.plot_spectrogram() 1326 | self.canvas.axes.set_title(audiopath.split('/')[-1]) 1327 | self.canvas.fig.savefig(audiopath[:-4] + '.jpg', dpi=150) 1328 | 1329 | def func_draw_shape_plot(self): 1330 | """Plot spectrogram for shape drawing""" 1331 | if self.filecounter < 0: 1332 | return 1333 | 1334 | self.canvas.fig.clf() 1335 | self.canvas.axes = self.canvas.fig.add_subplot(111) 1336 | 1337 | if self.t_length.text() == '': 1338 | self.plotwindow_length = self.t[-1] 1339 | self.plotwindow_startsecond = 0 1340 | else: 1341 | self.plotwindow_length = float(self.t_length.text()) 1342 | if self.t[-1] < self.plotwindow_length: 1343 | self.plotwindow_startsecond = 0 1344 | self.plotwindow_length = self.t[-1] 1345 | 1346 | y1 = int(self.f_min.text()) 1347 | y2 = int(self.f_max.text()) 1348 | if y2 > (self.fs / 2): 1349 | y2 = (self.fs / 2) 1350 | t1 = self.plotwindow_startsecond 1351 | t2 = self.plotwindow_startsecond + self.plotwindow_length 1352 | 1353 | ix_time = np.where((self.t >= t1) & (self.t < t2))[0] 1354 | ix_f = np.where((self.f >= y1) & (self.f < y2))[0] 1355 | 1356 | plotsxx = self.Sxx[int(ix_f[0]):int(ix_f[-1]), int(ix_time[0]):int(ix_time[-1])] 1357 | plotsxx_db = 10 * np.log10(plotsxx) 1358 | 1359 | if self.checkbox_background.isChecked(): 1360 | spec_mean = np.median(plotsxx_db, axis=1) 1361 | sxx_background = np.transpose(np.broadcast_to(spec_mean, np.transpose(plotsxx_db).shape)) 1362 | plotsxx_db = plotsxx_db - sxx_background 1363 | plotsxx_db = plotsxx_db - np.min(plotsxx_db.flatten()) 1364 | 1365 | colormap_plot = self.colormap_plot.currentText() 1366 | img = self.canvas.axes.imshow( 1367 | plotsxx_db, aspect='auto', cmap=colormap_plot, origin='lower', extent=[t1, t2, y1, y2]) 1368 | 1369 | self.canvas.axes.set_ylabel('Frequency [Hz]') 1370 | self.canvas.axes.set_xlabel('Time [sec]') 1371 | 1372 | if self.checkbox_logscale.isChecked(): 1373 | self.canvas.axes.set_yscale('log') 1374 | else: 1375 | self.canvas.axes.set_yscale('linear') 1376 | 1377 | clims = img.get_clim() 1378 | if (self.db_vmin.text() == '') & (self.db_vmax.text() != ''): 1379 | img.set_clim([clims[0], float(self.db_vmax.text())]) 1380 | if (self.db_vmin.text() != '') & (self.db_vmax.text() == ''): 1381 | img.set_clim([float(self.db_vmin.text()), clims[1]]) 1382 | if (self.db_vmin.text() != '') & (self.db_vmax.text() != ''): 1383 | img.set_clim([float(self.db_vmin.text()), float(self.db_vmax.text())]) 1384 | 1385 | self.canvas.fig.colorbar(img, label=r'PSD [dB re $1 \mu Pa Hz^{-1}$]') 1386 | 1387 | # Plot annotations 1388 | if self.annotation.shape[0] > 0: 1389 | ix = (self.annotation['t1'] > (np.array(self.time).astype('datetime64[ns]') + 1390 | pd.Timedelta(self.plotwindow_startsecond, unit="s"))) & \ 1391 | (self.annotation['t1'] < (np.array(self.time).astype('datetime64[ns]') + 1392 | pd.Timedelta(self.plotwindow_startsecond + self.plotwindow_length, unit="s"))) 1393 | 1394 | if np.sum(ix) > 0: 1395 | ix = np.where(ix)[0] 1396 | for ix_x in ix: 1397 | a = pd.DataFrame([self.annotation.iloc[ix_x, :]]) 1398 | self.plot_annotation_box(a) 1399 | 1400 | if hasattr(self, 't_limits') and self.t_limits is not None: 1401 | self.canvas.axes.set_ylim(self.f_limits) 1402 | self.canvas.axes.set_xlim(self.t_limits) 1403 | else: 1404 | self.canvas.axes.set_ylim([y1, y2]) 1405 | self.canvas.axes.set_xlim([t1, t2]) 1406 | 1407 | self.canvas.fig.tight_layout() 1408 | self.canvas.axes.plot(self.draw_x, self.draw_y, '.-g') 1409 | self.canvas.draw() 1410 | self.cid2 = self.canvas.fig.canvas.mpl_connect('button_press_event', self.onclick_draw) 1411 | 1412 | def onclick_draw(self, event): 1413 | """Handle clicks in draw mode""" 1414 | if event.button == 1 and event.dblclick: 1415 | self.draw_x = pd.concat([self.draw_x, pd.Series([event.xdata])], ignore_index=True) 1416 | self.draw_y = pd.concat([self.draw_y, pd.Series([event.ydata])], ignore_index=True) 1417 | self.f_limits = self.canvas.axes.get_ylim() 1418 | self.t_limits = self.canvas.axes.get_xlim() 1419 | 1420 | line = self.line_2.pop(0) 1421 | line.remove() 1422 | self.line_2 = self.canvas.axes.plot(self.draw_x, self.draw_y, '.-g') 1423 | self.canvas.draw() 1424 | 1425 | if event.button == 3: 1426 | self.draw_x = self.draw_x.head(-1) 1427 | self.draw_y = self.draw_y.head(-1) 1428 | self.f_limits = self.canvas.axes.get_ylim() 1429 | self.t_limits = self.canvas.axes.get_xlim() 1430 | 1431 | line = self.line_2.pop(0) 1432 | line.remove() 1433 | self.line_2 = self.canvas.axes.plot(self.draw_x, self.draw_y, '.-g') 1434 | self.canvas.draw() 1435 | 1436 | def func_draw_shape_exit(self): 1437 | """Exit draw mode and save shape""" 1438 | print(f'Save shape: {self.draw_x.shape}') 1439 | self.canvas.fig.canvas.mpl_disconnect(self.cid2) 1440 | self.plot_spectrogram() 1441 | print('Back to boxes') 1442 | self.drawexitm.setEnabled(False) 1443 | 1444 | if self.draw_x.shape[0] > 0: 1445 | savename = QtWidgets.QFileDialog.getSaveFileName( 1446 | self, "QFileDialog.getSaveFileName()", "csv files (*.csv)") 1447 | 1448 | if len(savename[0]) > 0: 1449 | if savename[0][-4:] != '.csv': 1450 | savename = savename[0] + '.csv' 1451 | else: 1452 | savename = savename[0] 1453 | 1454 | drawcsv = pd.DataFrame({'Time_in_s': self.draw_x, 'Frequency_in_Hz': self.draw_y}) 1455 | drawcsv.to_csv(savename) 1456 | 1457 | def func_draw_shape(self): 1458 | """Enter draw mode""" 1459 | msg = QtWidgets.QMessageBox() 1460 | msg.setIcon(QtWidgets.QMessageBox.Information) 1461 | msg.setText("Add points with double left click.\n" 1462 | "Remove latest point with single right click.\n" 1463 | "Exit draw mode and save CSV by pushing enter") 1464 | msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) 1465 | returnValue = msg.exec() 1466 | 1467 | if returnValue != QtWidgets.QMessageBox.Ok: 1468 | return 1469 | 1470 | print('Drawing mode enabled') 1471 | self.draw_x = pd.Series(dtype='float') 1472 | self.draw_y = pd.Series(dtype='float') 1473 | self.f_limits = self.canvas.axes.get_ylim() 1474 | self.t_limits = self.canvas.axes.get_xlim() 1475 | self.canvas.fig.canvas.mpl_disconnect(self.cid1) 1476 | self.cid2 = self.canvas.fig.canvas.mpl_connect('button_press_event', self.onclick_draw) 1477 | self.line_2 = self.canvas.axes.plot(self.draw_x, self.draw_y, '.-g') 1478 | self.func_draw_shape_plot() 1479 | self.drawexitm = QtWidgets.QShortcut(QtCore.Qt.Key_Return, self) 1480 | self.drawexitm.activated.connect(self.func_draw_shape_exit) 1481 | 1482 | def func_playaudio(self): 1483 | """Play/stop audio""" 1484 | if self.filecounter < 0: 1485 | return 1486 | 1487 | # Stop if already playing 1488 | if self.audio_playing: 1489 | self.stop_audio() 1490 | return 1491 | 1492 | # Get audio parameters 1493 | new_rate = 32000 1494 | t_limits = list(self.canvas.axes.get_xlim()) 1495 | t_limits = np.array(t_limits) - self.file_blocks.loc[self.filecounter, 'start'] / self.fs 1496 | f_limits = list(self.canvas.axes.get_ylim()) 1497 | if f_limits[1] >= (self.fs / 2): 1498 | f_limits[1] = self.fs / 2 - 10 1499 | 1500 | print(f'Time limits: {t_limits}') 1501 | print(f'Frequency limits: {f_limits}') 1502 | 1503 | # Extract and filter audio 1504 | x_select = self.x[int(t_limits[0] * self.fs):int(t_limits[1] * self.fs)] 1505 | 1506 | sos = signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1507 | x_select = signal.sosfilt(sos, x_select) 1508 | 1509 | # Resample 1510 | playback_speed = float(self.playbackspeed.currentText()) 1511 | number_of_samples = round(len(x_select) * (new_rate / playback_speed) / self.fs) 1512 | x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1513 | 1514 | # Normalize 1515 | maximum_x = 32767 * 0.8 1516 | old_max = np.max(np.abs([x_resampled.min(), x_resampled.max()])) 1517 | x_resampled = x_resampled * (maximum_x / old_max) 1518 | x_resampled = x_resampled.astype(np.int16) 1519 | 1520 | print(f'Audio range: [{x_resampled.min()}, {x_resampled.max()}]') 1521 | 1522 | # Play using sounddevice 1523 | self.audio_playing = True 1524 | sd.play(x_resampled, new_rate) 1525 | 1526 | def func_saveaudio(self): 1527 | """Export selected audio to WAV file""" 1528 | if self.filecounter < 0: 1529 | return 1530 | 1531 | savename = QtWidgets.QFileDialog.getSaveFileName( 1532 | self, "QFileDialog.getSaveFileName()", "wav files (*.wav)") 1533 | 1534 | if len(savename[0]) == 0: 1535 | return 1536 | 1537 | savename = savename[0] 1538 | new_rate = 32000 1539 | 1540 | t_limits = self.canvas.axes.get_xlim() 1541 | f_limits = list(self.canvas.axes.get_ylim()) 1542 | if f_limits[1] >= (self.fs / 2): 1543 | f_limits[1] = self.fs / 2 - 10 1544 | 1545 | print(f'Time limits: {t_limits}') 1546 | print(f'Frequency limits: {f_limits}') 1547 | 1548 | x_select = self.x[int(t_limits[0] * self.fs):int(t_limits[1] * self.fs)] 1549 | 1550 | sos = signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1551 | x_select = signal.sosfilt(sos, x_select) 1552 | 1553 | playback_speed = float(self.playbackspeed.currentText()) 1554 | number_of_samples = round(len(x_select) * (new_rate / playback_speed) / self.fs) 1555 | x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1556 | 1557 | # Normalize 1558 | maximum_x = 32767 * 0.8 1559 | old_max = np.max(np.abs([x_resampled.min(), x_resampled.max()])) 1560 | x_resampled = x_resampled * (maximum_x / old_max) 1561 | x_resampled = x_resampled.astype(np.int16) 1562 | 1563 | if savename[-4:] != '.wav': 1564 | savename = savename + '.wav' 1565 | 1566 | wav.write(savename, new_rate, x_resampled) 1567 | print(f'Saved audio to: {savename}') 1568 | 1569 | def func_save_video(self): 1570 | """Export spectrogram as animated video""" 1571 | if self.filecounter < 0: 1572 | return 1573 | 1574 | # Check if moviepy is available 1575 | if not MOVIEPY_AVAILABLE: 1576 | msg = QtWidgets.QMessageBox() 1577 | msg.setIcon(QtWidgets.QMessageBox.Warning) 1578 | msg.setWindowTitle("Video Export Not Available") 1579 | msg.setText("Video export requires ffmpeg to be installed.") 1580 | msg.setInformativeText( 1581 | "To enable video export:\n\n" 1582 | "1. Install ffmpeg from https://ffmpeg.org/download.html\n" 1583 | "2. Or install via conda: conda install -c conda-forge ffmpeg\n" 1584 | "3. Restart this application\n\n" 1585 | "Alternatively, use 'Export > Spectrogram as .wav file' to export audio only." 1586 | ) 1587 | msg.exec_() 1588 | return 1589 | 1590 | savename = QtWidgets.QFileDialog.getSaveFileName( 1591 | self, "QFileDialog.getSaveFileName()", "video files (*.mp4)") 1592 | 1593 | if len(savename[0]) == 0: 1594 | return 1595 | 1596 | savename = savename[0] 1597 | new_rate = 32000 1598 | 1599 | t_limits = self.canvas.axes.get_xlim() 1600 | f_limits = list(self.canvas.axes.get_ylim()) 1601 | if f_limits[1] >= (self.fs / 2): 1602 | f_limits[1] = self.fs / 2 - 10 1603 | 1604 | print(f'Time limits: {t_limits}') 1605 | print(f'Frequency limits: {f_limits}') 1606 | 1607 | x_select = self.x[int(t_limits[0] * self.fs):int(t_limits[1] * self.fs)] 1608 | 1609 | sos = signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1610 | x_select = signal.sosfilt(sos, x_select) 1611 | 1612 | playback_speed = float(self.playbackspeed.currentText()) 1613 | number_of_samples = round(len(x_select) * (new_rate / playback_speed) / self.fs) 1614 | x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1615 | 1616 | # Normalize 1617 | maximum_x = 32767 * 0.8 1618 | old_max = np.max(np.abs([x_resampled.min(), x_resampled.max()])) 1619 | x_resampled = x_resampled * (maximum_x / old_max) 1620 | x_resampled = x_resampled.astype(np.int16) 1621 | 1622 | if savename[-4:] == '.wav': 1623 | savename = savename[:-4] 1624 | if savename[-4:] == '.mp4': 1625 | savename = savename[:-4] 1626 | 1627 | wav.write(savename + '.wav', new_rate, x_resampled) 1628 | 1629 | audioclip = AudioFileClip(savename + '.wav') 1630 | duration = audioclip.duration 1631 | 1632 | self.canvas.axes.set_title(None) 1633 | self.line_2 = self.canvas.axes.plot([t_limits[0], t_limits[0]], f_limits, '-k') 1634 | 1635 | def make_frame(x): 1636 | s = t_limits[1] - t_limits[0] 1637 | xx = x / duration * s + t_limits[0] 1638 | line = self.line_2.pop(0) 1639 | line.remove() 1640 | self.line_2 = self.canvas.axes.plot([xx, xx], f_limits, '-k') 1641 | return mplfig_to_npimage(self.canvas.fig) 1642 | 1643 | animation = VideoClip(make_frame, duration=duration) 1644 | animation = animation.set_audio(audioclip) 1645 | animation.write_videofile(savename + ".mp4", fps=24, preset='fast') 1646 | 1647 | self.plot_spectrogram() 1648 | 1649 | 1650 | def start(): 1651 | """Start the application""" 1652 | app = QtWidgets.QApplication(sys.argv) 1653 | app.setApplicationName("Python Audio Spectrogram Explorer") 1654 | w = gui() 1655 | sys.exit(app.exec_()) 1656 | 1657 | 1658 | if __name__ == "__main__": 1659 | start() 1660 | -------------------------------------------------------------------------------- /to_compile_yourself/pase_compile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Sep 6 17:28:37 2021 4 | 5 | @author: Sebastian Menze, sebastian.menze@gmail.com 6 | """ 7 | 8 | import sys 9 | import matplotlib 10 | # matplotlib.use('Qt5Agg') 11 | 12 | from PyQt5 import QtCore, QtGui, QtWidgets 13 | 14 | # from PyQt5.QtWidgets import QShortcut 15 | # from PyQt5.QtGui import QKeySequence 16 | 17 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar 18 | from matplotlib.figure import Figure 19 | 20 | import scipy.io.wavfile as wav 21 | 22 | import soundfile as sf 23 | 24 | 25 | from scipy import signal 26 | import numpy as np 27 | from matplotlib import pyplot as plt 28 | import pandas as pd 29 | import datetime as dt 30 | import time 31 | import os 32 | 33 | from matplotlib.widgets import RectangleSelector 34 | 35 | 36 | # from pydub import AudioSegment 37 | # from pydub.playback import play 38 | # import threading 39 | 40 | import simpleaudio as sa 41 | 42 | from skimage import data, filters, measure, morphology 43 | from skimage.morphology import (erosion, dilation, opening, closing, # noqa 44 | white_tophat) 45 | from skimage.morphology import disk # noqa 46 | from matplotlib.path import Path 47 | from skimage.transform import rescale, resize, downscale_local_mean 48 | 49 | from scipy.signal import find_peaks 50 | from skimage.feature import match_template 51 | 52 | # from moviepy.editor import VideoClip, AudioFileClip 53 | # from moviepy.video.io.bindings import mplfig_to_npimage 54 | 55 | 56 | 57 | 58 | class MplCanvas(FigureCanvasQTAgg ): 59 | 60 | def __init__(self, parent=None, width=5, height=4, dpi=100): 61 | self.fig = Figure(figsize=(width, height), dpi=dpi) 62 | self.axes = self.fig.add_subplot(111) 63 | super(MplCanvas, self).__init__(self.fig) 64 | 65 | 66 | 67 | class MainWindow(QtWidgets.QMainWindow): 68 | 69 | 70 | def __init__(self, *args, **kwargs): 71 | super(MainWindow, self).__init__(*args, **kwargs) 72 | 73 | self.canvas = MplCanvas(self, width=5, height=4, dpi=150) 74 | 75 | # self.call_time=pd.Series() 76 | # self.call_frec=pd.Series() 77 | 78 | self.f_min = QtWidgets.QLineEdit(self) 79 | self.f_min.setText('10') 80 | self.f_max = QtWidgets.QLineEdit(self) 81 | self.f_max.setText('16000') 82 | self.t_length = QtWidgets.QLineEdit(self) 83 | self.t_length.setText('120') 84 | self.db_saturation=QtWidgets.QLineEdit(self) 85 | self.db_saturation.setText('155') 86 | self.db_vmin=QtWidgets.QLineEdit(self) 87 | self.db_vmin.setText('30') 88 | self.db_vmax=QtWidgets.QLineEdit(self) 89 | self.db_vmax.setText('') 90 | # self.fft_size = QtWidgets.QLineEdit(self) 91 | # self.fft_size.setText('32768') 92 | self.fft_size = QtWidgets.QComboBox(self) 93 | self.fft_size.addItem('1024') 94 | self.fft_size.addItem('2048') 95 | self.fft_size.addItem('4096') 96 | self.fft_size.addItem('8192') 97 | self.fft_size.addItem('16384') 98 | self.fft_size.addItem('32768') 99 | self.fft_size.addItem('65536') 100 | self.fft_size.addItem('131072') 101 | self.fft_size.setCurrentIndex(4) 102 | 103 | 104 | self.colormap_plot = QtWidgets.QComboBox(self) 105 | self.colormap_plot.addItem('plasma') 106 | self.colormap_plot.addItem('viridis') 107 | self.colormap_plot.addItem('inferno') 108 | self.colormap_plot.addItem('gist_gray') 109 | self.colormap_plot.addItem('gist_yarg') 110 | self.colormap_plot.setCurrentIndex(2) 111 | 112 | self.checkbox_logscale=QtWidgets.QCheckBox('Log. scale') 113 | self.checkbox_logscale.setChecked(True) 114 | self.checkbox_background=QtWidgets.QCheckBox('Remove background') 115 | self.checkbox_background.setChecked(False) 116 | 117 | # self.fft_overlap = QtWidgets.QLineEdit(self) 118 | # self.fft_overlap.setText('0.9') 119 | 120 | self.fft_overlap = QtWidgets.QComboBox(self) 121 | self.fft_overlap.addItem('0.2') 122 | self.fft_overlap.addItem('0.5') 123 | self.fft_overlap.addItem('0.7') 124 | self.fft_overlap.addItem('0.9') 125 | self.fft_overlap.setCurrentIndex(3) 126 | 127 | 128 | 129 | self.filename_timekey = QtWidgets.QLineEdit(self) 130 | self.filename_timekey.setText('aural_%Y_%m_%d_%H_%M_%S.wav') 131 | 132 | self.playbackspeed = QtWidgets.QComboBox(self) 133 | self.playbackspeed.addItem('0.5') 134 | self.playbackspeed.addItem('1') 135 | self.playbackspeed.addItem('2') 136 | self.playbackspeed.addItem('5') 137 | self.playbackspeed.addItem('10') 138 | self.playbackspeed.setCurrentIndex(1) 139 | 140 | 141 | self.time= dt.datetime(2000,1,1,0,0,0) 142 | self.f=None 143 | self.t=[-1,-1] 144 | self.Sxx=None 145 | self.draw_x=pd.Series(dtype='float') 146 | self.draw_y=pd.Series(dtype='float') 147 | self.cid1=None 148 | self.cid2=None 149 | 150 | self.plotwindow_startsecond=0 151 | # self.plotwindow_length=120 152 | self.filecounter=-1 153 | self.filenames=np.array( [] ) 154 | self.current_audiopath=None 155 | 156 | self.detectiondf=pd.DataFrame([]) 157 | 158 | 159 | def find_regions(db_threshold): 160 | 161 | y1=int(self.f_min.text()) 162 | y2=int(self.f_max.text()) 163 | if y2>(self.fs/2): 164 | y2=(self.fs/2) 165 | t1=self.plotwindow_startsecond 166 | t2=self.plotwindow_startsecond+self.plotwindow_length 167 | 168 | ix_time=np.where( (self.t>=t1) & (self.t=y1) & (self.f db_threshold 198 | mask = morphology.remove_small_objects(mask, 50,connectivity=30) 199 | mask = morphology.remove_small_holes(mask, 50,connectivity=30) 200 | 201 | mask = closing(mask, disk(3) ) 202 | # op_and_clo = opening(closed, disk(1) ) 203 | 204 | labels = measure.label(mask) 205 | 206 | probs=measure.regionprops_table(labels,spectrog,properties=['label','area','mean_intensity','orientation','major_axis_length','minor_axis_length','weighted_centroid','bbox']) 207 | df=pd.DataFrame(probs) 208 | 209 | # get corect f anf t 210 | ff=self.f[ ix_f[0]:ix_f[-1] ] 211 | ix=df['bbox-0']>len(ff)-1 212 | df.loc[ix,'bbox-0']=len(ff)-1 213 | ix=df['bbox-2']>len(ff)-1 214 | df.loc[ix,'bbox-2']=len(ff)-1 215 | 216 | df['f-1']=ff[df['bbox-0']] 217 | df['f-2']=ff[df['bbox-2']] 218 | df['f-width']=df['f-2']-df['f-1'] 219 | 220 | ix=df['bbox-1']>len(t)-1 221 | df.loc[ix,'bbox-1']=len(t)-1 222 | ix=df['bbox-3']>len(t)-1 223 | df.loc[ix,'bbox-3']=len(t)-1 224 | 225 | df['t-1']=t[df['bbox-1']] 226 | df['t-2']=t[df['bbox-3']] 227 | df['duration']=df['t-2']-df['t-1'] 228 | 229 | indices=np.where( (df['area']=spectrog.shape[1]: ix2=spectrog.shape[1]-1 266 | # sgram[ df['id'][ix] ] = spectrog[:,ix1:ix2] 267 | self.detectiondf = df 268 | self.patches = patches 269 | self.p_t_dict = p_t_dict 270 | self.p_f_dict = p_f_dict 271 | 272 | # return df, patches,p_t_dict,p_f_dict 273 | 274 | def match_bbox_and_iou(template): 275 | 276 | shape_f=template['Frequency_in_Hz'].values 277 | shape_t=template['Time_in_s'].values 278 | shape_t=shape_t-shape_t.min() 279 | 280 | df=self.detectiondf 281 | patches=self.patches 282 | p_t_dict=self.p_t_dict 283 | p_f_dict=self.p_f_dict 284 | 285 | # f_lim=[ shape_f.min()-10 ,shape_f.max()+10 ] 286 | 287 | # score_smc=[] 288 | score_ioubox=[] 289 | smc_rs=[] 290 | 291 | for ix in df.index: 292 | 293 | # breakpoint() 294 | patch=patches[ix] 295 | pf=p_f_dict[ix] 296 | pt=p_t_dict[ix] 297 | pt=pt-pt[0] 298 | 299 | 300 | if df.loc[ix,'f-1'] < shape_f.min(): 301 | f1= df.loc[ix,'f-1'] 302 | else: 303 | f1= shape_f.min() 304 | if df.loc[ix,'f-2'] > shape_f.max(): 305 | f2= df.loc[ix,'f-2'] 306 | else: 307 | f2= shape_f.max() 308 | 309 | # f_lim=[ f1,f2 ] 310 | 311 | time_step=np.diff(pt)[0] 312 | f_step=np.diff(pf)[0] 313 | k_f=np.arange(f1,f2,f_step ) 314 | 315 | if pt.max()>shape_t.max(): 316 | k_t=pt 317 | 318 | else: 319 | k_t=np.arange(0,shape_t.max(),time_step) 320 | 321 | 322 | ### iou bounding box 323 | 324 | iou_kernel=np.zeros( [ k_f.shape[0] ,k_t.shape[0] ] ) 325 | ixp2=np.where((k_t>=shape_t.min()) & (k_t<=shape_t.max()))[0] 326 | ixp1=np.where((k_f>=shape_f.min()) & (k_f<=shape_f.max()))[0] 327 | iou_kernel[ ixp1[0]:ixp1[-1] , ixp2[0]:ixp2[-1] ]=1 328 | 329 | iou_patch=np.zeros( [ k_f.shape[0] ,k_t.shape[0] ] ) 330 | ixp2=np.where((k_t>=pt[0]) & (k_t<=pt[-1]))[0] 331 | ixp1=np.where((k_f>=pf[0]) & (k_f<=pf[-1]))[0] 332 | iou_patch[ ixp1[0]:ixp1[-1] , ixp2[0]:ixp2[-1] ]=1 333 | 334 | intersection= iou_kernel.astype('bool') & iou_patch.astype('bool') 335 | union= iou_kernel.astype('bool') | iou_patch.astype('bool') 336 | iou_bbox = np.sum( intersection ) / np.sum( union ) 337 | score_ioubox.append(iou_bbox) 338 | 339 | patch_rs = resize(patch, (50,50)) 340 | n_resize=50 341 | k_t=np.linspace(0,shape_t.max(),n_resize ) 342 | k_f=np.linspace(shape_f.min(), shape_f.max(),n_resize ) 343 | kk_t,kk_f=np.meshgrid(k_t,k_f) 344 | # kernel=np.zeros( [ k_f.shape[0] ,k_t.shape[0] ] ) 345 | x, y = kk_t.flatten(), kk_f.flatten() 346 | points = np.vstack((x,y)).T 347 | p = Path(list(zip(shape_t, shape_f))) # make a polygon 348 | grid = p.contains_points(points) 349 | kernel_rs = grid.reshape(kk_t.shape) # now you have a mask with points inside a polygon 350 | smc_rs.append( np.sum( kernel_rs.astype('bool') == patch_rs.astype('bool') ) / len( patch_rs.flatten() ) ) 351 | 352 | smc_rs=np.array(smc_rs) 353 | score_ioubox=np.array(score_ioubox) 354 | 355 | df['score'] =score_ioubox * (smc_rs-.5)/.5 356 | 357 | self.detectiondf = df.copy() 358 | 359 | 360 | def automatic_detector_specgram_corr(): 361 | # open template 362 | self.detectiondf=pd.DataFrame([]) 363 | 364 | templatefile, ok1 = QtWidgets.QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileNames()", r"C:\Users","CSV file (*.csv)") 365 | if ok1: 366 | template=pd.read_csv(templatefile,index_col=0) 367 | 368 | corrscore_threshold, ok = QtWidgets.QInputDialog.getDouble(self, 'Input Dialog', 369 | 'Enter correlation threshold in (0-1):',decimals=2) 370 | if corrscore_threshold>1: 371 | corrscore_threshold=1 372 | if corrscore_threshold<0: 373 | corrscore_threshold=0 374 | 375 | if template.columns[0]=='Time_in_s': 376 | 377 | # print(template) 378 | offset_f=10 379 | offset_t=0.5 380 | shape_f=template['Frequency_in_Hz'].values 381 | shape_t=template['Time_in_s'].values 382 | shape_t=shape_t-shape_t.min() 383 | 384 | f_lim=[ shape_f.min() - offset_f , shape_f.max() + offset_f ] 385 | k_length_seconds=shape_t.max()+offset_t*2 386 | 387 | # generate kernel 388 | time_step=np.diff(self.t)[0] 389 | 390 | k_t=np.linspace(0,k_length_seconds,int(k_length_seconds/time_step) ) 391 | ix_f=np.where((self.f>=f_lim[0]) & (self.f<=f_lim[1]))[0] 392 | k_f=self.f[ix_f[0]:ix_f[-1]] 393 | # k_f=np.linspace(f_lim[0],f_lim[1], int( (f_lim[1]-f_lim[0]) /f_step) ) 394 | 395 | kk_t,kk_f=np.meshgrid(k_t,k_f) 396 | kernel_background_db=0 397 | kernel_signal_db=1 398 | kernel=np.ones( [ k_f.shape[0] ,k_t.shape[0] ] ) * kernel_background_db 399 | # find wich grid points are inside the shape 400 | x, y = kk_t.flatten(), kk_f.flatten() 401 | points = np.vstack((x,y)).T 402 | p = Path(list(zip(shape_t, shape_f))) # make a polygon 403 | grid = p.contains_points(points) 404 | mask = grid.reshape(kk_t.shape) # now you have a mask with points inside a polygon 405 | kernel[mask]=kernel_signal_db 406 | 407 | ix_f=np.where((self.f>=f_lim[0]) & (self.f<=f_lim[1]))[0] 408 | spectrog =10*np.log10( self.Sxx[ ix_f[0]:ix_f[-1],: ] ) 409 | 410 | result = match_template(spectrog, kernel) 411 | corr_score=result[0,:] 412 | t_score=np.linspace( self.t[int(kernel.shape[1]/2)] , self.t[-int(kernel.shape[1]/2)], corr_score.shape[0] ) 413 | 414 | peaks_indices = find_peaks(corr_score, height=corrscore_threshold)[0] 415 | 416 | 417 | t1=[] 418 | t2=[] 419 | f1=[] 420 | f2=[] 421 | score=[] 422 | 423 | if len(peaks_indices)>0: 424 | t2_old=0 425 | for ixpeak in peaks_indices: 426 | tstar=t_score[ixpeak] - k_length_seconds/2 - offset_t 427 | tend=t_score[ixpeak] + k_length_seconds/2 - offset_t 428 | # if tstar>t2_old: 429 | t1.append(tstar) 430 | t2.append(tend) 431 | f1.append(f_lim[0]+offset_f) 432 | f2.append(f_lim[1]-offset_f) 433 | score.append(corr_score[ixpeak]) 434 | # t2_old=tend 435 | df=pd.DataFrame() 436 | df['t-1']=t1 437 | df['t-2']=t2 438 | df['f-1']=f1 439 | df['f-2']=f2 440 | df['score']=score 441 | 442 | self.detectiondf = df.copy() 443 | self.detectiondf['audiofilename']= self.current_audiopath 444 | self.detectiondf['threshold']= corrscore_threshold 445 | 446 | plot_spectrogram() 447 | else: # image kernel 448 | 449 | 450 | k_length_seconds= float(template.columns[-1]) -float(template.columns[0]) 451 | 452 | f_lim=[ int(template.index[0]) , int( template.index[-1] )] 453 | ix_f=np.where((self.f>=f_lim[0]) & (self.f<=f_lim[1]))[0] 454 | spectrog =10*np.log10( self.Sxx[ ix_f[0]:ix_f[-1],: ] ) 455 | specgram_t_step= self.t[1] - self.t[0] 456 | n_f=spectrog.shape[0] 457 | n_t= int(k_length_seconds/ specgram_t_step) 458 | 459 | kernel= resize( template.values , [ n_f,n_t] ) 460 | 461 | 462 | result = match_template(spectrog, kernel) 463 | corr_score=result[0,:] 464 | t_score=np.linspace( self.t[int(kernel.shape[1]/2)] , self.t[-int(kernel.shape[1]/2)], corr_score.shape[0] ) 465 | 466 | peaks_indices = find_peaks(corr_score, height=corrscore_threshold)[0] 467 | 468 | # print(corr_score) 469 | 470 | t1=[] 471 | t2=[] 472 | f1=[] 473 | f2=[] 474 | score=[] 475 | 476 | if len(peaks_indices)>0: 477 | t2_old=0 478 | for ixpeak in peaks_indices: 479 | tstar=t_score[ixpeak] - k_length_seconds/2 480 | tend=t_score[ixpeak] + k_length_seconds/2 481 | # if tstar>t2_old: 482 | t1.append(tstar) 483 | t2.append(tend) 484 | f1.append(f_lim[0]) 485 | f2.append(f_lim[1]) 486 | score.append(corr_score[ixpeak]) 487 | t2_old=tend 488 | df=pd.DataFrame() 489 | df['t-1']=t1 490 | df['t-2']=t2 491 | df['f-1']=f1 492 | df['f-2']=f2 493 | df['score']=score 494 | 495 | self.detectiondf = df.copy() 496 | self.detectiondf['audiofilename']= self.current_audiopath 497 | self.detectiondf['threshold']= corrscore_threshold 498 | 499 | plot_spectrogram() 500 | 501 | def automatic_detector_specgram_corr_allfiles(): 502 | msg = QtWidgets.QMessageBox() 503 | msg.setIcon(QtWidgets.QMessageBox.Information) 504 | msg.setText("Are you sure you want to run the detector over "+ str(self.filenames.shape[0]) +" ?") 505 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 506 | returnValue = msg.exec() 507 | 508 | if returnValue == QtWidgets.QMessageBox.Yes: 509 | 510 | templatefile, ok1 = QtWidgets.QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileNames()", r"C:\Users","CSV file (*.csv)") 511 | if ok1: 512 | template=pd.read_csv(templatefile,index_col=0) 513 | 514 | corrscore_threshold, ok = QtWidgets.QInputDialog.getDouble(self, 'Input Dialog', 515 | 'Enter correlation threshold in (0-1):',decimals=2) 516 | if corrscore_threshold>1: 517 | corrscore_threshold=1 518 | if corrscore_threshold<0: 519 | corrscore_threshold=0 520 | 521 | self.detectiondf_all=pd.DataFrame([]) 522 | 523 | for audiopath in self.filenames: 524 | 525 | if self.filename_timekey.text()=='': 526 | self.time= dt.datetime(2000,1,1,0,0,0) 527 | else: 528 | try: 529 | self.time= dt.datetime.strptime( audiopath.split('/')[-1], self.filename_timekey.text() ) 530 | except: 531 | self.time= dt.datetime(2000,1,1,0,0,0) 532 | 533 | self.x,self.fs = sf.read(audiopath,dtype='int16') 534 | print('open new file: '+audiopath) 535 | 536 | db_saturation=float( self.db_saturation.text() ) 537 | x=self.x/32767 538 | p =np.power(10,(db_saturation/20))*x #convert data.signal to uPa 539 | 540 | fft_size=int( self.fft_size.currentText() ) 541 | fft_overlap=float( self.fft_overlap.currentText() ) 542 | 543 | self.f, self.t, self.Sxx = signal.spectrogram(p, self.fs, window='hamming',nperseg=fft_size,noverlap=fft_size*fft_overlap) 544 | 545 | # self.plotwindow_startsecond=0 546 | # self.plotwindow_length = self.t.max() 547 | 548 | if template.columns[0]=='Time_in_s': 549 | 550 | # print(template) 551 | offset_f=10 552 | offset_t=0.5 553 | shape_f=template['Frequency_in_Hz'].values 554 | shape_t=template['Time_in_s'].values 555 | shape_t=shape_t-shape_t.min() 556 | 557 | f_lim=[ shape_f.min() - offset_f , shape_f.max() + offset_f ] 558 | k_length_seconds=shape_t.max()+offset_t*2 559 | 560 | # generate kernel 561 | time_step=np.diff(self.t)[0] 562 | 563 | k_t=np.linspace(0,k_length_seconds,int(k_length_seconds/time_step) ) 564 | ix_f=np.where((self.f>=f_lim[0]) & (self.f<=f_lim[1]))[0] 565 | k_f=self.f[ix_f[0]:ix_f[-1]] 566 | # k_f=np.linspace(f_lim[0],f_lim[1], int( (f_lim[1]-f_lim[0]) /f_step) ) 567 | 568 | kk_t,kk_f=np.meshgrid(k_t,k_f) 569 | kernel_background_db=0 570 | kernel_signal_db=1 571 | kernel=np.ones( [ k_f.shape[0] ,k_t.shape[0] ] ) * kernel_background_db 572 | # find wich grid points are inside the shape 573 | x, y = kk_t.flatten(), kk_f.flatten() 574 | points = np.vstack((x,y)).T 575 | p = Path(list(zip(shape_t, shape_f))) # make a polygon 576 | grid = p.contains_points(points) 577 | mask = grid.reshape(kk_t.shape) # now you have a mask with points inside a polygon 578 | kernel[mask]=kernel_signal_db 579 | 580 | ix_f=np.where((self.f>=f_lim[0]) & (self.f<=f_lim[1]))[0] 581 | spectrog =10*np.log10( self.Sxx[ ix_f[0]:ix_f[-1],: ] ) 582 | 583 | result = match_template(spectrog, kernel) 584 | corr_score=result[0,:] 585 | t_score=np.linspace( self.t[int(kernel.shape[1]/2)] , self.t[-int(kernel.shape[1]/2)], corr_score.shape[0] ) 586 | 587 | peaks_indices = find_peaks(corr_score, height=corrscore_threshold)[0] 588 | 589 | 590 | t1=[] 591 | t2=[] 592 | f1=[] 593 | f2=[] 594 | score=[] 595 | 596 | if len(peaks_indices)>0: 597 | t2_old=0 598 | for ixpeak in peaks_indices: 599 | tstar=t_score[ixpeak] - k_length_seconds/2 - offset_t 600 | tend=t_score[ixpeak] + k_length_seconds/2 - offset_t 601 | # if tstar>t2_old: 602 | t1.append(tstar) 603 | t2.append(tend) 604 | f1.append(f_lim[0]+offset_f) 605 | f2.append(f_lim[1]-offset_f) 606 | score.append(corr_score[ixpeak]) 607 | t2_old=tend 608 | df=pd.DataFrame() 609 | df['t-1']=t1 610 | df['t-2']=t2 611 | df['f-1']=f1 612 | df['f-2']=f2 613 | df['score']=score 614 | 615 | self.detectiondf = df.copy() 616 | self.detectiondf['audiofilename']= audiopath 617 | self.detectiondf['threshold']= corrscore_threshold 618 | else: # image kernel 619 | k_length_seconds= float(template.columns[-1]) -float(template.columns[0]) 620 | 621 | f_lim=[ int(template.index[0]) , int( template.index[-1] )] 622 | ix_f=np.where((self.f>=f_lim[0]) & (self.f<=f_lim[1]))[0] 623 | spectrog =10*np.log10( self.Sxx[ ix_f[0]:ix_f[-1],: ] ) 624 | specgram_t_step= self.t[1] - self.t[0] 625 | n_f=spectrog.shape[0] 626 | n_t= int(k_length_seconds/ specgram_t_step) 627 | 628 | kernel= resize( template.values , [ n_f,n_t] ) 629 | 630 | 631 | result = match_template(spectrog, kernel) 632 | corr_score=result[0,:] 633 | t_score=np.linspace( self.t[int(kernel.shape[1]/2)] , self.t[-int(kernel.shape[1]/2)], corr_score.shape[0] ) 634 | 635 | peaks_indices = find_peaks(corr_score, height=corrscore_threshold)[0] 636 | 637 | # print(corr_score) 638 | 639 | t1=[] 640 | t2=[] 641 | f1=[] 642 | f2=[] 643 | score=[] 644 | 645 | if len(peaks_indices)>0: 646 | t2_old=0 647 | for ixpeak in peaks_indices: 648 | tstar=t_score[ixpeak] - k_length_seconds/2 649 | tend=t_score[ixpeak] + k_length_seconds/2 650 | # if tstar>t2_old: 651 | t1.append(tstar) 652 | t2.append(tend) 653 | f1.append(f_lim[0]) 654 | f2.append(f_lim[1]) 655 | score.append(corr_score[ixpeak]) 656 | t2_old=tend 657 | df=pd.DataFrame() 658 | df['t-1']=t1 659 | df['t-2']=t2 660 | df['f-1']=f1 661 | df['f-2']=f2 662 | df['score']=score 663 | 664 | self.detectiondf = df.copy() 665 | self.detectiondf['audiofilename']= self.current_audiopath 666 | self.detectiondf['threshold']= corrscore_threshold 667 | 668 | self.detectiondf_all=pd.concat([ self.detectiondf_all,self.detectiondf ]) 669 | self.detectiondf_all=self.detectiondf_all.reset_index(drop=True) 670 | 671 | print(self.detectiondf_all) 672 | 673 | 674 | self.detectiondf= self.detectiondf_all 675 | # self.detectiondf=self.detectiondf.reset_index(drop=True) 676 | print('done!!!') 677 | 678 | 679 | def automatic_detector_shapematching(): 680 | # open template 681 | self.detectiondf=pd.DataFrame([]) 682 | 683 | templatefile, ok1 = QtWidgets.QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileNames()", r"C:\Users","CSV file (*.csv)") 684 | if ok1: 685 | template=pd.read_csv(templatefile,index_col=0) 686 | 687 | if template.columns[0]=='Time_in_s': 688 | # print(template) 689 | 690 | # set db threshold 691 | db_threshold, ok = QtWidgets.QInputDialog.getInt(self, 'Input Dialog', 692 | 'Enter signal-to-noise threshold in dB:') 693 | if ok: 694 | print(db_threshold) 695 | self.detectiondf=pd.DataFrame([]) 696 | 697 | find_regions(db_threshold) 698 | match_bbox_and_iou(template) 699 | ixdel=np.where(self.detectiondf['score']<0.01)[0] 700 | self.detectiondf=self.detectiondf.drop(ixdel) 701 | self.detectiondf=self.detectiondf.reset_index(drop=True) 702 | self.detectiondf['audiofilename']= self.current_audiopath 703 | self.detectiondf['threshold']= db_threshold 704 | 705 | print(self.detectiondf) 706 | 707 | # plot results 708 | plot_spectrogram() 709 | else: 710 | msg = QtWidgets.QMessageBox() 711 | msg.setIcon(QtWidgets.QMessageBox.Information) 712 | msg.setText("Wrong template format") 713 | msg.setStandardButtons(QtWidgets.QMessageBox.Ok) 714 | returnValue = msg.exec() 715 | 716 | 717 | def automatic_detector_shapematching_allfiles(): 718 | msg = QtWidgets.QMessageBox() 719 | msg.setIcon(QtWidgets.QMessageBox.Information) 720 | msg.setText("Are you sure you want to run the detector over "+ str(self.filenames.shape[0]) +"files ?") 721 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 722 | returnValue = msg.exec() 723 | 724 | if returnValue == QtWidgets.QMessageBox.Yes: 725 | templatefile, ok1 = QtWidgets.QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileNames()", r"C:\Users","CSV file (*.csv)") 726 | template=pd.read_csv(templatefile,index_col=0) 727 | 728 | self.detectiondf_all=pd.DataFrame([]) 729 | 730 | 731 | if template.columns[0]=='Time_in_s': 732 | 733 | db_threshold, ok = QtWidgets.QInputDialog.getInt(self, 'Input Dialog', 734 | 'Enter signal-to-noise threshold in dB:') 735 | 736 | for audiopath in self.filenames: 737 | 738 | if self.filename_timekey.text()=='': 739 | self.time= dt.datetime(2000,1,1,0,0,0) 740 | else: 741 | try: 742 | self.time= dt.datetime.strptime( audiopath.split('/')[-1], self.filename_timekey.text() ) 743 | except: 744 | self.time= dt.datetime(2000,1,1,0,0,0) 745 | 746 | self.x,self.fs = sf.read(audiopath,dtype='int16') 747 | print('open new file: '+audiopath) 748 | 749 | db_saturation=float( self.db_saturation.text() ) 750 | x=self.x/32767 751 | p =np.power(10,(db_saturation/20))*x #convert data.signal to uPa 752 | 753 | fft_size=int( self.fft_size.currentText() ) 754 | fft_overlap=float( self.fft_overlap.currentText() ) 755 | 756 | self.f, self.t, self.Sxx = signal.spectrogram(p, self.fs, window='hamming',nperseg=fft_size,noverlap=fft_size*fft_overlap) 757 | 758 | self.plotwindow_startsecond=0 759 | self.plotwindow_length = self.t.max() 760 | 761 | self.detectiondf=pd.DataFrame([]) 762 | 763 | find_regions(db_threshold) 764 | match_bbox_and_iou(template) 765 | ixdel=np.where(self.detectiondf['score']<0.01)[0] 766 | self.detectiondf=self.detectiondf.drop(ixdel) 767 | self.detectiondf=self.detectiondf.reset_index(drop=True) 768 | self.detectiondf['audiofilename']= audiopath 769 | self.detectiondf['threshold']= db_threshold 770 | 771 | self.detectiondf_all=pd.concat([ self.detectiondf_all,self.detectiondf ]) 772 | self.detectiondf_all=self.detectiondf_all.reset_index(drop=True) 773 | 774 | print(self.detectiondf_all) 775 | else: 776 | msg = QtWidgets.QMessageBox() 777 | msg.setIcon(QtWidgets.QMessageBox.Information) 778 | msg.setText("Wrong template format") 779 | msg.setStandardButtons(QtWidgets.QMessageBox.Ok) 780 | returnValue = msg.exec() 781 | 782 | self.detectiondf= self.detectiondf_all 783 | # self.detectiondf=self.detectiondf.reset_index(drop=True) 784 | print('done!!!') 785 | 786 | def export_automatic_detector_shapematching(): 787 | if self.detectiondf.shape[0]>0: 788 | savename = QtWidgets.QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()", r"C:\Users", "csv files (*.csv)") 789 | print('location is:' + savename[0]) 790 | if len(savename[0])>0: 791 | self.detectiondf.to_csv(savename[0]) 792 | 793 | openfilebutton=QtWidgets.QPushButton('Open files') 794 | def openfilefunc(): 795 | self.filecounter=-1 796 | self.annotation= pd.DataFrame({'t1': pd.Series(dtype='datetime64[ns]'), 797 | 't2': pd.Series(dtype='datetime64[ns]'), 798 | 'f1': pd.Series(dtype='float'), 799 | 'f2': pd.Series(dtype='float'), 800 | 'label': pd.Series(dtype='object'), 801 | 'audiofilename': pd.Series(dtype='object')}) 802 | 803 | # self.annotation=pd.DataFrame(columns=['t1','t2','f1','f2','label'],dtype=[("t1", "datetime64[ns]"), ("t2", "datetime64[ns]"), ("f1", "float"), ("f2", "float"), ("label", "object")] ) 804 | 805 | # annotation=pd.DataFrame(dtype=[("t1", "datetime64[ns]"), ("t2", "datetime64[ns]"), ("f1", "float"), ("f2", "float"), ("label", "object")] ) 806 | 807 | # ,dtype=('datetime64[ns]','datetime64[ns]','float','float','object')) 808 | # self.call_t_1=pd.Series(dtype='datetime64[ns]') 809 | # self.call_f_1=pd.Series(dtype='float') 810 | # self.call_t_2=pd.Series(dtype='datetime64[ns]') 811 | # self.call_f_2=pd.Series(dtype='float') 812 | # self.call_label=pd.Series(dtype='object') 813 | 814 | options = QtWidgets.QFileDialog.Options() 815 | # options |= QtWidgets.QFileDialog.DontUseNativeDialog 816 | self.filenames, _ = QtWidgets.QFileDialog.getOpenFileNames(self,"QFileDialog.getOpenFileNames()", r"C:\Users\a5278\Documents\passive_acoustics\detector_delevopment\detector_validation_subset","Audio Files (*.wav *.aif *.aiff *.aifc *.ogg *.flac)", options=options) 817 | self.filenames = np.array( self.filenames ) 818 | print(self.filenames) 819 | plot_next_spectro() 820 | openfilebutton.clicked.connect(openfilefunc) 821 | 822 | 823 | def read_wav(): 824 | if self.filecounter>=0: 825 | self.current_audiopath=self.filenames[self.filecounter] 826 | 827 | # if self.filename_timekey.text()=='': 828 | # self.time= dt.datetime(1,1,1,0,0,0) 829 | # else: 830 | # self.time= dt.datetime.strptime( audiopath.split('/')[-1], self.filename_timekey.text() ) 831 | 832 | if self.filename_timekey.text()=='': 833 | self.time= dt.datetime(2000,1,1,0,0,0) 834 | #self.time= dt.datetime.now() 835 | else: 836 | try: 837 | self.time= dt.datetime.strptime( self.current_audiopath.split('/')[-1], self.filename_timekey.text() ) 838 | except: 839 | print('wrongfilename') 840 | 841 | # if audiopath[-4:]=='.wav': 842 | 843 | 844 | self.x,self.fs = sf.read(self.current_audiopath,dtype='int16') 845 | 846 | # if audiopath[-4:]=='.aif' | audiopath[-4:]=='.aiff' | audiopath[-4:]=='.aifc': 847 | # obj = aifc.open(audiopath,'r') 848 | # self.fs, self.x = wav.read(audiopath) 849 | print('open new file: '+self.current_audiopath) 850 | print('FS: '+str(self.fs) +' x: '+str(np.shape(self.x))) 851 | if len(self.x.shape)>1: 852 | if np.shape(self.x)[1]>1: 853 | self.x=self.x[:,0] 854 | 855 | # factor=60 856 | # x=signal.decimate(x,factor,ftype='fir') 857 | 858 | db_saturation=float( self.db_saturation.text() ) 859 | x=self.x/32767 860 | p =np.power(10,(db_saturation/20))*x #convert data.signal to uPa 861 | 862 | fft_size=int( self.fft_size.currentText() ) 863 | fft_overlap=float( self.fft_overlap.currentText() ) 864 | self.f, self.t, self.Sxx = signal.spectrogram(p, self.fs, window='hamming',nperseg=fft_size,noverlap=int(fft_size*fft_overlap)) 865 | # self.t=self.time + pd.to_timedelta( t , unit='s') 866 | 867 | def plot_annotation_box(annotation_row): 868 | print('row:') 869 | # print(annotation_row.dtypes) 870 | x1=annotation_row.iloc[0,0] 871 | x2=annotation_row.iloc[0,1] 872 | 873 | xt=pd.Series([x1,x2]) 874 | print(xt) 875 | print(xt.dtype) 876 | 877 | # print(np.dtype(np.array(self.time).astype('datetime64[ns]') )) 878 | tt=xt - np.array(self.time).astype('datetime64[ns]') 879 | xt=tt.dt.seconds + tt.dt.microseconds/10**6 880 | x1=xt[0] 881 | x2=xt[1] 882 | 883 | # tt=x1 - np.array(self.time).astype('datetime64[ns]') 884 | # x1=tt.dt.seconds + tt.dt.microseconds/10**6 885 | # tt=x2 - np.array(self.time).astype('datetime64[ns]') 886 | # x2=tt.dt.seconds + tt.dt.microseconds/10**6 887 | 888 | y1=annotation_row.iloc[0,2] 889 | y2=annotation_row.iloc[0,3] 890 | c_label=annotation_row.iloc[0,4] 891 | 892 | line_x=[x2,x1,x1,x2,x2] 893 | line_y=[y1,y1,y2,y2,y1] 894 | 895 | xmin=np.min([x1,x2]) 896 | ymax=np.max([y1,y2]) 897 | 898 | self.canvas.axes.plot(line_x,line_y,'-b',linewidth=.75) 899 | self.canvas.axes.text(xmin,ymax,c_label,size=5) 900 | 901 | 902 | 903 | def plot_spectrogram(): 904 | if self.filecounter>=0: 905 | # self.canvas = MplCanvas(self, width=5, height=4, dpi=100) 906 | # self.setCentralWidget(self.canvas) 907 | self.canvas.fig.clf() 908 | self.canvas.axes = self.canvas.fig.add_subplot(111) 909 | # self.canvas.axes.cla() 910 | 911 | if self.t_length.text()=='': 912 | self.plotwindow_length= self.t[-1] 913 | self.plotwindow_startsecond=0 914 | else: 915 | self.plotwindow_length=float( self.t_length.text() ) 916 | if self.t[-1](self.fs/2): 923 | y2=(self.fs/2) 924 | t1=self.plotwindow_startsecond 925 | t2=self.plotwindow_startsecond+self.plotwindow_length 926 | 927 | # if self.t_length.text=='': 928 | # t2=self.t[-1] 929 | # else: 930 | # if self.t[-1]=self.plotwindow_startsecond) & (tt<(self.plotwindow_startsecond+self.plotwindow_length)) 937 | # ix_f=(ff>=y1) & (ff=t1) & (self.t=y1) & (self.f0: 992 | ix=(self.annotation['t1'] > (np.array(self.time).astype('datetime64[ns]')+pd.Timedelta(self.plotwindow_startsecond, unit="s") ) ) & \ 993 | (self.annotation['t1'] < (np.array(self.time).astype('datetime64[ns]')+pd.Timedelta(self.plotwindow_startsecond+self.plotwindow_length, unit="s") ) ) &\ 994 | (self.annotation['audiofilename'] == self.current_audiopath ) 995 | if np.sum(ix)>0: 996 | ix=np.where(ix)[0] 997 | print('ix is') 998 | print(ix) 999 | for ix_x in ix: 1000 | a= pd.DataFrame([self.annotation.iloc[ix_x,:] ]) 1001 | print(a) 1002 | plot_annotation_box(a) 1003 | 1004 | # plot detections 1005 | cmap = plt.cm.get_cmap('cool') 1006 | if self.detectiondf.shape[0]>0: 1007 | for i in range(self.detectiondf.shape[0]): 1008 | 1009 | insidewindow=(self.detectiondf.loc[i,'t-1'] > self.plotwindow_startsecond ) & (self.detectiondf.loc[i,'t-2'] < (self.plotwindow_startsecond+self.plotwindow_length) ) &\ 1010 | (self.detectiondf.loc[i,'audiofilename'] == self.current_audiopath ) 1011 | 1012 | scoremin=self.detectiondf['score'].min() 1013 | scoremax=self.detectiondf['score'].max() 1014 | 1015 | if (self.detectiondf.loc[i,'score']>=0.01) & insidewindow: 1016 | 1017 | xx1=self.detectiondf.loc[i,'t-1'] 1018 | xx2=self.detectiondf.loc[i,'t-2'] 1019 | yy1=self.detectiondf.loc[i,'f-1'] 1020 | yy2=self.detectiondf.loc[i,'f-2'] 1021 | scorelabel=str(np.round(self.detectiondf.loc[i,'score'],2)) 1022 | snorm=(self.detectiondf.loc[i,'score']-scoremin) / (scoremax-scoremin) 1023 | scorecolor = cmap(snorm) 1024 | 1025 | line_x=[xx2,xx1,xx1,xx2,xx2] 1026 | line_y=[yy1,yy1,yy2,yy2,yy1] 1027 | 1028 | xmin=np.min([xx1,xx2]) 1029 | ymax=np.max([yy1,yy2]) 1030 | self.canvas.axes.plot(line_x,line_y,'-',color=scorecolor,linewidth=.75) 1031 | self.canvas.axes.text(xmin,ymax,scorelabel,size=5,color=scorecolor) 1032 | 1033 | 1034 | 1035 | self.canvas.axes.set_ylim([y1,y2]) 1036 | self.canvas.axes.set_xlim([t1,t2]) 1037 | 1038 | 1039 | self.canvas.fig.tight_layout() 1040 | toggle_selector.RS=RectangleSelector(self.canvas.axes, box_select_callback, 1041 | drawtype='box', useblit=False, 1042 | button=[1], # disable middle button 1043 | interactive=False,rectprops=dict(facecolor="blue", edgecolor="black", alpha=0.1, fill=True)) 1044 | 1045 | 1046 | self.canvas.draw() 1047 | self.cid1=self.canvas.fig.canvas.mpl_connect('button_press_event', onclick) 1048 | 1049 | 1050 | def export_zoomed_sgram_as_csv(): 1051 | if self.filecounter>=0: 1052 | 1053 | # filter out background 1054 | spectrog = 10*np.log10(self.Sxx ) 1055 | rectime= pd.to_timedelta( self.t ,'s') 1056 | spg=pd.DataFrame(np.transpose(spectrog),index=rectime) 1057 | bg=spg.resample('3min').mean().copy() 1058 | bg=bg.resample('1s').interpolate(method='time') 1059 | bg= bg.reindex(rectime,method='nearest') 1060 | background=np.transpose(bg.values) 1061 | z=spectrog-background 1062 | 1063 | 1064 | self.f_limits=self.canvas.axes.get_ylim() 1065 | self.t_limits=self.canvas.axes.get_xlim() 1066 | y1=int(self.f_limits[0]) 1067 | y2=int(self.f_limits[1]) 1068 | t1=self.t_limits[0] 1069 | t2=self.t_limits[1] 1070 | 1071 | 1072 | ix_time=np.where( (self.t>=t1) & (self.t=y1) & (self.f0: 1083 | if savename[-4:]!='.csv': 1084 | savename=savename[0]+'.csv' 1085 | sgram.to_csv(savename) 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | def box_select_callback(eclick, erelease): 1092 | 1093 | x1, y1 = eclick.xdata, eclick.ydata 1094 | x2, y2 = erelease.xdata, erelease.ydata 1095 | 1096 | x1 =self.time + pd.to_timedelta( x1 , unit='s') 1097 | x2 =self.time + pd.to_timedelta( x2 , unit='s') 1098 | 1099 | # sort to increasing values 1100 | t1=np.min([x1,x2]) 1101 | t2=np.max([x1,x2]) 1102 | f1=np.min([y1,y2]) 1103 | f2=np.max([y1,y2]) 1104 | 1105 | if self.bg.checkedId()==-1: 1106 | c_label='' 1107 | else: 1108 | c_label=eval( 'self.an_'+str(self.bg.checkedId())+'.text()' ) 1109 | 1110 | # a=pd.DataFrame(columns=['t1','t2','f1','f2','label']) 1111 | # a.iloc[0,:]=np.array([x1,x2,y1,y2,c_label ]) 1112 | a=pd.DataFrame({'t1': pd.Series(t1,dtype='datetime64[ns]'), 1113 | 't2': pd.Series(t2,dtype='datetime64[ns]'), 1114 | 'f1': pd.Series(f1,dtype='float'), 1115 | 'f2': pd.Series(f2,dtype='float'), 1116 | 'label': pd.Series(c_label,dtype='object') , 1117 | 'audiofilename': self.current_audiopath }) 1118 | 1119 | # a=pd.DataFrame(data=[ [x1,x2,y1,y2,c_label ] ],columns=['t1','t2','f1','f2','label']) 1120 | # print('a:') 1121 | # print(a.dtypes) 1122 | # self.annotation.append(a, ignore_index = True) 1123 | self.annotation=pd.concat([ self.annotation ,a ] , ignore_index = True) 1124 | 1125 | # print(self.annotation.dtypes) 1126 | plot_annotation_box(a) 1127 | 1128 | def toggle_selector(event): 1129 | # toggle_selector.RS.set_active(True) 1130 | print('select') 1131 | # if event.key == 't': 1132 | # if toggle_selector.RS.active: 1133 | # print(' RectangleSelector deactivated.') 1134 | # toggle_selector.RS.set_active(False) 1135 | # else: 1136 | # print(' RectangleSelector activated.') 1137 | # toggle_selector.RS.set_active(True) 1138 | 1139 | 1140 | 1141 | def onclick(event): 1142 | if event.button==3: 1143 | self.annotation=self.annotation.head(-1) 1144 | # print(self.annotation) 1145 | plot_spectrogram() 1146 | 1147 | def end_of_filelist_warning(): 1148 | msg_listend = QtWidgets.QMessageBox() 1149 | msg_listend.setIcon(QtWidgets.QMessageBox.Information) 1150 | msg_listend.setText("End of file list reached!") 1151 | msg_listend.exec_() 1152 | 1153 | def plot_next_spectro(): 1154 | if len(self.filenames)>0: 1155 | print('old filecounter is: '+str(self.filecounter)) 1156 | 1157 | if self.t_length.text()=='' or self.t[-1]self.filenames.shape[0]-1: 1160 | self.filecounter=self.filenames.shape[0]-1 1161 | print('That was it') 1162 | end_of_filelist_warning() 1163 | self.plotwindow_length= self.t[-1] 1164 | self.plotwindow_startsecond=0 1165 | # new file 1166 | # self.filecounter=self.filecounter+1 1167 | read_wav() 1168 | plot_spectrogram() 1169 | 1170 | else: 1171 | self.plotwindow_length=float( self.t_length.text() ) 1172 | self.plotwindow_startsecond=self.plotwindow_startsecond + self.plotwindow_length 1173 | 1174 | print( [self.plotwindow_startsecond, self.t[-1] ] ) 1175 | 1176 | if self.plotwindow_startsecond > self.t[-1]: 1177 | #save log 1178 | if checkbox_log.isChecked(): 1179 | 1180 | tt= self.annotation['t1'] - self.time 1181 | t_in_seconds=np.array( tt.values*1e-9 ,dtype='float16') 1182 | reclength=np.array( self.t[-1] ,dtype='float16') 1183 | 1184 | ix=(t_in_seconds>0) & (t_in_seconds=self.filenames.shape[0]-1: 1194 | self.filecounter=self.filenames.shape[0]-1 1195 | print('That was it') 1196 | end_of_filelist_warning() 1197 | read_wav() 1198 | self.plotwindow_startsecond=0 1199 | plot_spectrogram() 1200 | else: 1201 | plot_spectrogram() 1202 | 1203 | 1204 | 1205 | def plot_previous_spectro(): 1206 | if len(self.filenames)>0: 1207 | print('old filecounter is: '+str(self.filecounter)) 1208 | 1209 | if self.t_length.text()=='' or self.t[-1]=self.filenames.shape[0]-1: 1241 | # print('That was it') 1242 | # self.canvas.fig.clf() 1243 | # self.canvas.axes = self.canvas.fig.add_subplot(111) 1244 | # self.canvas.axes.set_title('That was it') 1245 | # self.canvas.draw() 1246 | 1247 | read_wav() 1248 | # self.plotwindow_startsecond=0 1249 | plot_spectrogram() 1250 | else: 1251 | plot_spectrogram() 1252 | 1253 | 1254 | 1255 | def new_fft_size_selected(): 1256 | read_wav() 1257 | plot_spectrogram() 1258 | self.fft_size.currentIndexChanged.connect(new_fft_size_selected) 1259 | 1260 | self.colormap_plot.currentIndexChanged.connect( plot_spectrogram) 1261 | self.checkbox_background.stateChanged.connect(plot_spectrogram ) 1262 | self.checkbox_logscale.stateChanged.connect(plot_spectrogram ) 1263 | 1264 | 1265 | # self.canvas.fig.canvas.mpl_connect('button_press_event', onclick) 1266 | # self.canvas.fig.canvas.mpl_connect('key_press_event', toggle_selector) 1267 | 1268 | 1269 | 1270 | # QtGui.QShortcut(QtCore.Qt.Key_Right, MainWindow, plot_next_spectro()) 1271 | # self.msgSc = QShortcut(QKeySequence(u"\u2192"), self) 1272 | # self.msgSc.activated.connect(plot_next_spectro) 1273 | # button_plot_spectro=QtWidgets.QPushButton('Next spectrogram-->') 1274 | # button_plot_spectro.clicked.connect(plot_next_spectro) 1275 | 1276 | # button_plot_prevspectro=QtWidgets.QPushButton('<--Previous spectrogram') 1277 | # button_plot_prevspectro.clicked.connect(plot_previous_spectro) 1278 | 1279 | button_save=QtWidgets.QPushButton('Save annotation csv') 1280 | def func_savecsv(): 1281 | options = QtWidgets.QFileDialog.Options() 1282 | savename = QtWidgets.QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()", r"C:\Users\a5278\Documents\passive_acoustics\detector_delevopment\detector_validation_subset", "csv files (*.csv)",options=options) 1283 | print('location is:' + savename[0]) 1284 | if len(savename[0])>0: 1285 | self.annotation.to_csv(savename[0]) 1286 | button_save.clicked.connect(func_savecsv) 1287 | 1288 | button_quit=QtWidgets.QPushButton('Quit') 1289 | button_quit.clicked.connect(QtWidgets.QApplication.instance().quit) 1290 | 1291 | 1292 | 1293 | def func_logging(): 1294 | if checkbox_log.isChecked(): 1295 | print('logging') 1296 | msg = QtWidgets.QMessageBox() 1297 | msg.setIcon(QtWidgets.QMessageBox.Information) 1298 | msg.setText("Overwrite existing log files?") 1299 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 1300 | returnValue = msg.exec() 1301 | if returnValue == QtWidgets.QMessageBox.No: 1302 | 1303 | ix_delete=[] 1304 | i=0 1305 | for fn in self.filenames: 1306 | logpath=fn[:-4]+'_log.csv' 1307 | # print(logpath) 1308 | if os.path.isfile( logpath): 1309 | ix_delete.append(i) 1310 | i=i+1 1311 | # print(ix_delete) 1312 | 1313 | self.filenames=np.delete(self.filenames,ix_delete) 1314 | print('Updated filelist:') 1315 | print(self.filenames) 1316 | 1317 | 1318 | 1319 | checkbox_log=QtWidgets.QCheckBox('Real-time Logging') 1320 | checkbox_log.toggled.connect(func_logging) 1321 | 1322 | button_plot_all_spectrograms=QtWidgets.QPushButton('Plot all spectrograms') 1323 | def plot_all_spectrograms(): 1324 | 1325 | msg = QtWidgets.QMessageBox() 1326 | msg.setIcon(QtWidgets.QMessageBox.Information) 1327 | msg.setText("Are you sure you want to plot "+ str(self.filenames.shape[0]) +" spectrograms?") 1328 | msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) 1329 | returnValue = msg.exec() 1330 | 1331 | if returnValue == QtWidgets.QMessageBox.Yes: 1332 | for audiopath in self.filenames: 1333 | 1334 | if self.filename_timekey.text()=='': 1335 | self.time= dt.datetime(2000,1,1,0,0,0) 1336 | else: 1337 | try: 1338 | self.time= dt.datetime.strptime( audiopath.split('/')[-1], self.filename_timekey.text() ) 1339 | except: 1340 | self.time= dt.datetime(2000,1,1,0,0,0) 1341 | 1342 | self.x,self.fs = sf.read(audiopath,dtype='int16') 1343 | print('open new file: '+audiopath) 1344 | 1345 | db_saturation=float( self.db_saturation.text() ) 1346 | x=self.x/32767 1347 | p =np.power(10,(db_saturation/20))*x #convert data.signal to uPa 1348 | 1349 | fft_size=int( self.fft_size.currentText() ) 1350 | fft_overlap=float( self.fft_overlap.currentText() ) 1351 | 1352 | 1353 | self.f, self.t, self.Sxx = signal.spectrogram(p, self.fs, window='hamming',nperseg=fft_size,noverlap=fft_size*fft_overlap) 1354 | 1355 | self.plotwindow_startsecond=0 1356 | 1357 | plot_spectrogram() 1358 | self.canvas.axes.set_title(audiopath.split('/')[-1]) 1359 | self.canvas.fig.savefig( audiopath[:-4]+'.jpg',dpi=150 ) 1360 | 1361 | 1362 | button_plot_all_spectrograms.clicked.connect(plot_all_spectrograms) 1363 | 1364 | button_draw_shape=QtWidgets.QPushButton('Draw shape') 1365 | def func_draw_shape_plot(): 1366 | if self.filecounter>=0: 1367 | self.canvas.fig.clf() 1368 | self.canvas.axes = self.canvas.fig.add_subplot(111) 1369 | if self.t_length.text()=='': 1370 | self.plotwindow_length= self.t[-1] 1371 | self.plotwindow_startsecond=0 1372 | else: 1373 | self.plotwindow_length=float( self.t_length.text() ) 1374 | if self.t[-1](self.fs/2): 1381 | y2=(self.fs/2) 1382 | t1=self.plotwindow_startsecond 1383 | t2=self.plotwindow_startsecond+self.plotwindow_length 1384 | 1385 | ix_time=np.where( (self.t>=t1) & (self.t=y1) & (self.f0: 1424 | ix=(self.annotation['t1'] > (np.array(self.time).astype('datetime64[ns]')+pd.Timedelta(self.plotwindow_startsecond, unit="s") ) ) & (self.annotation['t1'] < (np.array(self.time).astype('datetime64[ns]')+pd.Timedelta(self.plotwindow_startsecond+self.plotwindow_length, unit="s") ) ) 1425 | if np.sum(ix)>0: 1426 | ix=np.where(ix)[0] 1427 | print('ix is') 1428 | print(ix) 1429 | for ix_x in ix: 1430 | a= pd.DataFrame([self.annotation.iloc[ix_x,:] ]) 1431 | print(a) 1432 | plot_annotation_box(a) 1433 | 1434 | if self.t_limits==None: 1435 | self.canvas.axes.set_ylim([y1,y2]) 1436 | self.canvas.axes.set_xlim([t1,t2]) 1437 | else: 1438 | self.canvas.axes.set_ylim(self.f_limits) 1439 | self.canvas.axes.set_xlim(self.t_limits) 1440 | 1441 | 1442 | 1443 | self.canvas.fig.tight_layout() 1444 | 1445 | self.canvas.axes.plot(self.draw_x,self.draw_y,'.-g') 1446 | 1447 | self.canvas.draw() 1448 | self.cid2=self.canvas.fig.canvas.mpl_connect('button_press_event', onclick_draw) 1449 | 1450 | def onclick_draw(event): 1451 | # print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % 1452 | # ('double' if event.dblclick else 'single', event.button, 1453 | # event.x, event.y, event.xdata, event.ydata)) 1454 | if event.button==1 & event.dblclick: 1455 | self.draw_x=self.draw_x.append( pd.Series(event.xdata) ,ignore_index=True ) 1456 | self.draw_y=self.draw_y.append( pd.Series(event.ydata) ,ignore_index=True ) 1457 | self.f_limits=self.canvas.axes.get_ylim() 1458 | self.t_limits=self.canvas.axes.get_xlim() 1459 | 1460 | line = self.line_2.pop(0) 1461 | line.remove() 1462 | self.line_2 =self.canvas.axes.plot(self.draw_x,self.draw_y,'.-g') 1463 | self.canvas.draw() 1464 | 1465 | # func_draw_shape_plot() 1466 | 1467 | if event.button==3: 1468 | self.draw_x=self.draw_x.head(-1) 1469 | self.draw_y=self.draw_y.head(-1) 1470 | self.f_limits=self.canvas.axes.get_ylim() 1471 | self.t_limits=self.canvas.axes.get_xlim() 1472 | # func_draw_shape_plot() 1473 | line = self.line_2.pop(0) 1474 | line.remove() 1475 | self.line_2 =self.canvas.axes.plot(self.draw_x,self.draw_y,'.-g') 1476 | self.canvas.draw() 1477 | 1478 | # func_draw_shape_plot() 1479 | 1480 | def func_draw_shape_exit(): 1481 | print('save shape' + str(self.draw_x.shape)) 1482 | self.canvas.fig.canvas.mpl_disconnect(self.cid2) 1483 | plot_spectrogram() 1484 | print('back to boxes') 1485 | ## deactive shortcut 1486 | self.drawexitm.setEnabled(False) 1487 | 1488 | if self.draw_x.shape[0]>0: 1489 | options = QtWidgets.QFileDialog.Options() 1490 | savename = QtWidgets.QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()", "csv files (*.csv)",options=options) 1491 | if len(savename[0])>0: 1492 | if savename[-4:]!='.csv': 1493 | savename=savename[0]+'.csv' 1494 | # drawcsv=pd.concat([self.draw_x,self.draw_y],axis=1) 1495 | drawcsv=pd.DataFrame(columns=['Time_in_s','Frequency_in_Hz']) 1496 | drawcsv['Time_in_s']=self.draw_x 1497 | drawcsv['Frequency_in_Hz']=self.draw_y 1498 | drawcsv.to_csv(savename) 1499 | 1500 | 1501 | def func_draw_shape(): 1502 | msg = QtWidgets.QMessageBox() 1503 | msg.setIcon(QtWidgets.QMessageBox.Information) 1504 | msg.setText("Add points with double left click.\nRemove latest point with single right click. \nExit draw mode and save CSV by pushing enter") 1505 | msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) 1506 | returnValue = msg.exec() 1507 | if returnValue == QtWidgets.QMessageBox.Ok: 1508 | print('drawing') 1509 | self.draw_x=pd.Series(dtype='float') 1510 | self.draw_y=pd.Series(dtype='float') 1511 | self.f_limits=self.canvas.axes.get_ylim() 1512 | self.t_limits=self.canvas.axes.get_xlim() 1513 | self.canvas.fig.canvas.mpl_disconnect(self.cid1) 1514 | self.cid2=self.canvas.fig.canvas.mpl_connect('button_press_event', onclick_draw) 1515 | self.line_2 =self.canvas.axes.plot(self.draw_x,self.draw_y,'.-g') 1516 | func_draw_shape_plot() 1517 | self.drawexitm = QtWidgets.QShortcut(QtCore.Qt.Key_Return, self) 1518 | self.drawexitm.activated.connect(func_draw_shape_exit) 1519 | 1520 | button_draw_shape.clicked.connect(func_draw_shape) 1521 | 1522 | ####### play audio 1523 | button_play_audio=QtWidgets.QPushButton('Play/Stop [spacebar]') 1524 | def func_playaudio(): 1525 | if self.filecounter>=0: 1526 | if not hasattr(self, "play_obj"): 1527 | new_rate = 32000 1528 | 1529 | t_limits=self.canvas.axes.get_xlim() 1530 | f_limits=list(self.canvas.axes.get_ylim()) 1531 | if f_limits[1]>=(self.fs/2): 1532 | f_limits[1]= self.fs/2-10 1533 | print(t_limits) 1534 | print(f_limits) 1535 | 1536 | x_select=self.x[int(t_limits[0]*self.fs) : int(t_limits[1]*self.fs) ] 1537 | 1538 | sos=signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1539 | x_select = signal.sosfilt(sos, x_select) 1540 | 1541 | number_of_samples = round(len(x_select) * (float(new_rate)/ float(self.playbackspeed.currentText())) / self.fs) 1542 | x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1543 | 1544 | #normalize sound level 1545 | maximum_x=32767*0.8 1546 | old_max=np.max(np.abs([x_resampled.min(),x_resampled.max()])) 1547 | x_resampled=x_resampled * (maximum_x/old_max) 1548 | x_resampled = x_resampled.astype(np.int16) 1549 | 1550 | print( [x_resampled.min(),x_resampled.max()] ) 1551 | wave_obj = sa.WaveObject(x_resampled, 1, 2, new_rate) 1552 | self.play_obj = wave_obj.play() 1553 | else: 1554 | if self.play_obj.is_playing(): 1555 | sa.stop_all() 1556 | else: 1557 | new_rate = 32000 1558 | t_limits=self.canvas.axes.get_xlim() 1559 | f_limits=list(self.canvas.axes.get_ylim()) 1560 | if f_limits[1]>=(self.fs/2): 1561 | f_limits[1]= self.fs/2-10 1562 | print(t_limits) 1563 | print(f_limits) 1564 | 1565 | x_select=self.x[int(t_limits[0]*self.fs) : int(t_limits[1]*self.fs) ] 1566 | sos=signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1567 | x_select = signal.sosfilt(sos, x_select) 1568 | 1569 | # number_of_samples = round(len(x_select) * float(new_rate) / self.fs) 1570 | number_of_samples = round(len(x_select) * (float(new_rate)/ float(self.playbackspeed.currentText())) / self.fs) 1571 | 1572 | x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1573 | #normalize sound level 1574 | maximum_x=32767*0.8 1575 | old_max=np.max(np.abs([x_resampled.min(),x_resampled.max()])) 1576 | x_resampled=x_resampled * (maximum_x/old_max) 1577 | x_resampled = x_resampled.astype(np.int16) 1578 | print( [x_resampled.min(),x_resampled.max()] ) 1579 | wave_obj = sa.WaveObject(x_resampled, 1, 2, new_rate) 1580 | self.play_obj = wave_obj.play() 1581 | 1582 | button_play_audio.clicked.connect(func_playaudio) 1583 | 1584 | button_save_audio=QtWidgets.QPushButton('Export selected audio') 1585 | def func_saveaudio(): 1586 | if self.filecounter>=0: 1587 | options = QtWidgets.QFileDialog.Options() 1588 | savename = QtWidgets.QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()", "wav files (*.wav)",options=options) 1589 | if len(savename[0])>0: 1590 | savename=savename[0] 1591 | new_rate = 32000 1592 | 1593 | t_limits=self.canvas.axes.get_xlim() 1594 | f_limits=list(self.canvas.axes.get_ylim()) 1595 | if f_limits[1]>=(self.fs/2): 1596 | f_limits[1]= self.fs/2-10 1597 | print(t_limits) 1598 | print(f_limits) 1599 | x_select=self.x[int(t_limits[0]*self.fs) : int(t_limits[1]*self.fs) ] 1600 | 1601 | sos=signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1602 | x_select = signal.sosfilt(sos, x_select) 1603 | 1604 | number_of_samples = round(len(x_select) * (float(new_rate)/ float(self.playbackspeed.currentText())) / self.fs) 1605 | x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1606 | #normalize sound level 1607 | maximum_x=32767*0.8 1608 | old_max=np.max(np.abs([x_resampled.min(),x_resampled.max()])) 1609 | x_resampled=x_resampled * (maximum_x/old_max) 1610 | x_resampled = x_resampled.astype(np.int16) 1611 | 1612 | if savename[-4:]!='.wav': 1613 | savename=savename+'.wav' 1614 | wav.write(savename, new_rate, x_resampled) 1615 | button_save_audio.clicked.connect(func_saveaudio) 1616 | 1617 | # button_save_video=QtWidgets.QPushButton('Export video') 1618 | # def func_save_video(): 1619 | # if self.filecounter>=0: 1620 | # savename = QtWidgets.QFileDialog.getSaveFileName(self,"QFileDialog.getSaveFileName()", "video files (*.mp4)") 1621 | # if len(savename[0])>0: 1622 | # savename=savename[0] 1623 | # new_rate = 32000 1624 | 1625 | # t_limits=self.canvas.axes.get_xlim() 1626 | # f_limits=list(self.canvas.axes.get_ylim()) 1627 | # if f_limits[1]>=(self.fs/2): 1628 | # f_limits[1]= self.fs/2-10 1629 | # print(t_limits) 1630 | # print(f_limits) 1631 | # x_select=self.x[int(t_limits[0]*self.fs) : int(t_limits[1]*self.fs) ] 1632 | 1633 | # sos=signal.butter(8, f_limits, 'bandpass', fs=self.fs, output='sos') 1634 | # x_select = signal.sosfilt(sos, x_select) 1635 | 1636 | # number_of_samples = round(len(x_select) * (float(new_rate)/ float(self.playbackspeed.currentText())) / self.fs) 1637 | # x_resampled = np.array(signal.resample(x_select, number_of_samples)).astype('int') 1638 | # #normalize sound level 1639 | # maximum_x=32767*0.8 1640 | # old_max=np.max(np.abs([x_resampled.min(),x_resampled.max()])) 1641 | # x_resampled=x_resampled * (maximum_x/old_max) 1642 | # x_resampled = x_resampled.astype(np.int16) 1643 | 1644 | # if savename[:-4]=='.wav': 1645 | # savename=savename[:-4] 1646 | # if savename[:-4]=='.mp4': 1647 | # savename=savename[:-4] 1648 | # wav.write(savename+'.wav', new_rate, x_resampled) 1649 | 1650 | # # self.f_limits=self.canvas.axes.get_ylim() 1651 | # # self.t_limits=self.canvas.axes.get_xlim() 1652 | 1653 | # audioclip = AudioFileClip(savename+'.wav') 1654 | # duration=audioclip.duration 1655 | # # func_draw_shape_plot() 1656 | 1657 | # self.canvas.axes.set_title(None) 1658 | # # self.canvas.axes.set_ylim(f_limits) 1659 | # # self.canvas.axes.set_xlim(t_limits) 1660 | # self.line_2=self.canvas.axes.plot([t_limits[0] ,t_limits[0] ],f_limits,'-k') 1661 | # def make_frame(x): 1662 | # s=t_limits[1] - t_limits[0] 1663 | # xx= x/duration * s + t_limits[0] 1664 | # line = self.line_2.pop(0) 1665 | # line.remove() 1666 | # self.line_2=self.canvas.axes.plot([xx,xx],f_limits,'-k') 1667 | 1668 | 1669 | # return mplfig_to_npimage(self.canvas.fig) 1670 | 1671 | # animation = VideoClip(make_frame, duration = duration ) 1672 | # animation = animation.set_audio(audioclip) 1673 | # animation.write_videofile(savename+".mp4",fps=24,preset='fast') 1674 | 1675 | # plot_spectrogram() 1676 | # # self.canvas.fig.canvas.mpl_disconnect(self.cid2) 1677 | 1678 | # button_save_video.clicked.connect(func_save_video) 1679 | 1680 | ############# menue 1681 | menuBar = self.menuBar() 1682 | 1683 | # Creating menus using a title 1684 | openMenu = menuBar.addAction("Open files") 1685 | openMenu.triggered.connect(openfilefunc) 1686 | 1687 | 1688 | exportMenu = menuBar.addMenu("Export") 1689 | e1 =exportMenu.addAction("Spectrogram as .wav file") 1690 | e1.triggered.connect(func_saveaudio) 1691 | # e2 =exportMenu.addAction("Spectrogram as animated video") 1692 | # e2.triggered.connect(func_save_video) 1693 | e3 =exportMenu.addAction("Spectrogram as .csv table") 1694 | e3.triggered.connect(export_zoomed_sgram_as_csv) 1695 | e4 =exportMenu.addAction("All files as spectrogram images") 1696 | e4.triggered.connect(plot_all_spectrograms) 1697 | e5 =exportMenu.addAction("Annotations as .csv table") 1698 | e5.triggered.connect(func_savecsv) 1699 | e6 =exportMenu.addAction("Automatic detections as .csv table") 1700 | e6.triggered.connect(export_automatic_detector_shapematching) 1701 | 1702 | drawMenu = menuBar.addAction("Draw") 1703 | drawMenu.triggered.connect(func_draw_shape) 1704 | 1705 | 1706 | autoMenu = menuBar.addMenu("Automatic detection") 1707 | a1 =autoMenu.addAction("Shapematching on current file") 1708 | a1.triggered.connect(automatic_detector_shapematching) 1709 | a2 =autoMenu.addAction("Spectrogram correlation on current file") 1710 | a2.triggered.connect(automatic_detector_specgram_corr) 1711 | a3 =autoMenu.addAction("Shapematching on all files") 1712 | a3.triggered.connect(automatic_detector_shapematching_allfiles) 1713 | a4 =autoMenu.addAction("Spectrogram correlation on all files") 1714 | a4.triggered.connect(automatic_detector_specgram_corr_allfiles) 1715 | 1716 | quitMenu = menuBar.addAction("Quit") 1717 | quitMenu.triggered.connect(QtWidgets.QApplication.instance().quit) 1718 | 1719 | ################# 1720 | 1721 | ######## layout 1722 | outer_layout = QtWidgets.QVBoxLayout() 1723 | 1724 | # top_layout = QtWidgets.QHBoxLayout() 1725 | 1726 | # top_layout.addWidget(openfilebutton) 1727 | 1728 | # top_layout.addWidget(button_plot_prevspectro) 1729 | # top_layout.addWidget(button_plot_spectro) 1730 | # # top_layout.addWidget(button_plot_all_spectrograms) 1731 | # top_layout.addWidget(button_play_audio) 1732 | # top_layout.addWidget(QtWidgets.QLabel('Playback speed:')) 1733 | # top_layout.addWidget(self.playbackspeed) 1734 | # # top_layout.addWidget(button_save_audio) 1735 | # # top_layout.addWidget(button_save_video) 1736 | # # top_layout.addWidget(button_draw_shape) 1737 | 1738 | # top_layout.addWidget(button_save) 1739 | # top_layout.addWidget(button_quit) 1740 | 1741 | top2_layout = QtWidgets.QHBoxLayout() 1742 | 1743 | # self.f_min = QtWidgets.QLineEdit(self) 1744 | # self.f_max = QtWidgets.QLineEdit(self) 1745 | # self.t_start = QtWidgets.QLineEdit(self) 1746 | # self.t_end = QtWidgets.QLineEdit(self) 1747 | # self.fft_size = QtWidgets.QLineEdit(self) 1748 | top2_layout.addWidget(checkbox_log) 1749 | top2_layout.addWidget(self.checkbox_logscale) 1750 | top2_layout.addWidget(self.checkbox_background) 1751 | 1752 | top2_layout.addWidget(QtWidgets.QLabel('Timestamp:')) 1753 | top2_layout.addWidget(self.filename_timekey) 1754 | top2_layout.addWidget(QtWidgets.QLabel('f_min[Hz]:')) 1755 | top2_layout.addWidget(self.f_min) 1756 | top2_layout.addWidget(QtWidgets.QLabel('f_max[Hz]:')) 1757 | top2_layout.addWidget(self.f_max) 1758 | top2_layout.addWidget(QtWidgets.QLabel('Spec. length [sec]:')) 1759 | top2_layout.addWidget(self.t_length) 1760 | 1761 | # top2_layout.addWidget(QtWidgets.QLabel('fft_size[bits]:')) 1762 | # top2_layout.addWidget(self.fft_size) 1763 | # top2_layout.addWidget(QtWidgets.QLabel('fft_overlap[0-1]:')) 1764 | # top2_layout.addWidget(self.fft_overlap) 1765 | 1766 | 1767 | 1768 | # top2_layout.addWidget(QtWidgets.QLabel('Colormap:')) 1769 | # top2_layout.addWidget( self.colormap_plot) 1770 | 1771 | 1772 | top2_layout.addWidget(QtWidgets.QLabel('Saturation dB:')) 1773 | top2_layout.addWidget(self.db_saturation) 1774 | 1775 | top2_layout.addWidget(QtWidgets.QLabel('dB min:')) 1776 | top2_layout.addWidget(self.db_vmin) 1777 | top2_layout.addWidget(QtWidgets.QLabel('dB max:')) 1778 | top2_layout.addWidget(self.db_vmax) 1779 | 1780 | 1781 | 1782 | # annotation label area 1783 | top3_layout = QtWidgets.QHBoxLayout() 1784 | top3_layout.addWidget(QtWidgets.QLabel('Annotation labels:')) 1785 | 1786 | 1787 | self.checkbox_an_1=QtWidgets.QCheckBox() 1788 | top3_layout.addWidget(self.checkbox_an_1) 1789 | self.an_1 = QtWidgets.QLineEdit(self) 1790 | top3_layout.addWidget(self.an_1) 1791 | self.an_1.setText('') 1792 | 1793 | self.checkbox_an_2=QtWidgets.QCheckBox() 1794 | top3_layout.addWidget(self.checkbox_an_2) 1795 | self.an_2 = QtWidgets.QLineEdit(self) 1796 | top3_layout.addWidget(self.an_2) 1797 | self.an_2.setText('') 1798 | 1799 | self.checkbox_an_3=QtWidgets.QCheckBox() 1800 | top3_layout.addWidget(self.checkbox_an_3) 1801 | self.an_3 = QtWidgets.QLineEdit(self) 1802 | top3_layout.addWidget(self.an_3) 1803 | self.an_3.setText('') 1804 | 1805 | self.checkbox_an_4=QtWidgets.QCheckBox() 1806 | top3_layout.addWidget(self.checkbox_an_4) 1807 | self.an_4 = QtWidgets.QLineEdit(self) 1808 | top3_layout.addWidget(self.an_4) 1809 | self.an_4.setText('') 1810 | 1811 | self.checkbox_an_5=QtWidgets.QCheckBox() 1812 | top3_layout.addWidget(self.checkbox_an_5) 1813 | self.an_5 = QtWidgets.QLineEdit(self) 1814 | top3_layout.addWidget(self.an_5) 1815 | self.an_5.setText('') 1816 | 1817 | self.checkbox_an_6=QtWidgets.QCheckBox() 1818 | top3_layout.addWidget(self.checkbox_an_6) 1819 | self.an_6 = QtWidgets.QLineEdit(self) 1820 | top3_layout.addWidget(self.an_6) 1821 | self.an_6.setText('') 1822 | 1823 | # self.checkbox_an_7=QtWidgets.QCheckBox() 1824 | # top3_layout.addWidget(self.checkbox_an_7) 1825 | # self.an_7 = QtWidgets.QLineEdit(self) 1826 | # top3_layout.addWidget(self.an_7) 1827 | # self.an_7.setText('') 1828 | 1829 | 1830 | # button_export_sgramcsv=QtWidgets.QPushButton('Export spectrog. csv') 1831 | # button_export_sgramcsv.clicked.connect(export_zoomed_sgram_as_csv) 1832 | # top3_layout.addWidget(button_export_sgramcsv) 1833 | 1834 | # button_autodetect_shape=QtWidgets.QPushButton('Shapematching') 1835 | # button_autodetect_shape.clicked.connect(automatic_detector_shapematching) 1836 | # top3_layout.addWidget(button_autodetect_shape) 1837 | 1838 | # button_autodetect_corr=QtWidgets.QPushButton('Spectrog. correlation') 1839 | # button_autodetect_corr.clicked.connect(automatic_detector_specgram_corr) 1840 | # top3_layout.addWidget(button_autodetect_corr) 1841 | 1842 | 1843 | # button_saveautodetect=QtWidgets.QPushButton('Export auto-detec.') 1844 | # button_saveautodetect.clicked.connect(export_automatic_detector_shapematching) 1845 | # top3_layout.addWidget(button_saveautodetect) 1846 | 1847 | 1848 | 1849 | # self.checkbox_an_8=QtWidgets.QCheckBox() 1850 | # top3_layout.addWidget(self.checkbox_an_8) 1851 | # self.an_8 = QtWidgets.QLineEdit(self) 1852 | # top3_layout.addWidget(self.an_8) 1853 | # self.an_8.setText('') 1854 | 1855 | self.bg = QtWidgets.QButtonGroup() 1856 | self.bg.addButton(self.checkbox_an_1,1) 1857 | self.bg.addButton(self.checkbox_an_2,2) 1858 | self.bg.addButton(self.checkbox_an_3,3) 1859 | self.bg.addButton(self.checkbox_an_4,4) 1860 | self.bg.addButton(self.checkbox_an_5,5) 1861 | self.bg.addButton(self.checkbox_an_6,6) 1862 | # self.bg.addButton(self.checkbox_an_7,7) 1863 | # self.bg.addButton(self.checkbox_an_8,8) 1864 | 1865 | 1866 | 1867 | 1868 | # combine layouts together 1869 | 1870 | plot_layout = QtWidgets.QVBoxLayout() 1871 | tnav = NavigationToolbar( self.canvas, self) 1872 | 1873 | toolbar = QtWidgets.QToolBar() 1874 | 1875 | 1876 | # toolbar.addAction('test') 1877 | # toolbar.addWidget(button_plot_prevspectro) 1878 | # toolbar.addWidget(button_plot_spectro) 1879 | 1880 | # b1=QtWidgets.QToolButton() 1881 | # b1.setText('<--Previous spectrogram') 1882 | # # b1.setStyleSheet("background-color: yellow; font-size: 18pt") 1883 | # b1.clicked.connect(plot_previous_spectro) 1884 | # toolbar.addWidget(b1) 1885 | 1886 | # b2=QtWidgets.QToolButton() 1887 | # b2.setText('Next spectrogram-->') 1888 | # b2.clicked.connect(plot_next_spectro) 1889 | # toolbar.addWidget(b2) 1890 | 1891 | # b3=QtWidgets.QToolButton() 1892 | # b3.setText('Play/Stop [spacebar]') 1893 | # b3.clicked.connect(func_playaudio) 1894 | # toolbar.addWidget(b3) 1895 | 1896 | button_plot_prevspectro=QtWidgets.QPushButton('<--Previous spectrogram') 1897 | button_plot_prevspectro.clicked.connect(plot_previous_spectro) 1898 | toolbar.addWidget(button_plot_prevspectro) 1899 | 1900 | ss=' ' 1901 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1902 | 1903 | button_plot_spectro=QtWidgets.QPushButton('Next spectrogram-->') 1904 | button_plot_spectro.clicked.connect(plot_next_spectro) 1905 | toolbar.addWidget(button_plot_spectro) 1906 | 1907 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1908 | 1909 | 1910 | toolbar.addWidget(button_play_audio) 1911 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1912 | 1913 | toolbar.addWidget(QtWidgets.QLabel('Playback speed:')) 1914 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1915 | toolbar.addWidget(self.playbackspeed) 1916 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1917 | 1918 | toolbar.addSeparator() 1919 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1920 | 1921 | 1922 | toolbar.addWidget(QtWidgets.QLabel('fft_size[bits]:')) 1923 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1924 | toolbar.addWidget(self.fft_size) 1925 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1926 | toolbar.addWidget(QtWidgets.QLabel('fft_overlap[0-1]:')) 1927 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1928 | toolbar.addWidget(self.fft_overlap) 1929 | 1930 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1931 | 1932 | 1933 | toolbar.addWidget(QtWidgets.QLabel('Colormap:')) 1934 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1935 | toolbar.addWidget( self.colormap_plot) 1936 | toolbar.addWidget(QtWidgets.QLabel(ss)) 1937 | 1938 | toolbar.addSeparator() 1939 | 1940 | toolbar.addWidget(tnav) 1941 | 1942 | 1943 | plot_layout.addWidget(toolbar) 1944 | plot_layout.addWidget(self.canvas) 1945 | 1946 | # outer_layout.addLayout(top_layout) 1947 | outer_layout.addLayout(top2_layout) 1948 | outer_layout.addLayout(top3_layout) 1949 | 1950 | outer_layout.addLayout(plot_layout) 1951 | 1952 | # self.setLayout(outer_layout) 1953 | 1954 | # Create a placeholder widget to hold our toolbar and canvas. 1955 | widget = QtWidgets.QWidget() 1956 | widget.setLayout(outer_layout) 1957 | self.setCentralWidget(widget) 1958 | 1959 | #### hotkeys 1960 | self.msgSc1 = QtWidgets.QShortcut(QtCore.Qt.Key_Right, self) 1961 | self.msgSc1.activated.connect(plot_next_spectro) 1962 | self.msgSc2 = QtWidgets.QShortcut(QtCore.Qt.Key_Left, self) 1963 | self.msgSc2.activated.connect(plot_previous_spectro) 1964 | self.msgSc3 = QtWidgets.QShortcut(QtCore.Qt.Key_Space, self) 1965 | self.msgSc3.activated.connect(func_playaudio) 1966 | 1967 | #### 1968 | # layout = QtWidgets.QVBoxLayout() 1969 | # layout.addWidget(openfilebutton) 1970 | # layout.addWidget(button_plot_spectro) 1971 | # layout.addWidget(button_save) 1972 | # layout.addWidget(button_quit) 1973 | 1974 | 1975 | # layout.addWidget(toolbar) 1976 | # layout.addWidget(self.canvas) 1977 | 1978 | # # Create a placeholder widget to hold our toolbar and canvas. 1979 | # widget = QtWidgets.QWidget() 1980 | # widget.setLayout(layout) 1981 | # self.setCentralWidget(widget) 1982 | 1983 | self.show() 1984 | 1985 | app = QtWidgets.QApplication(sys.argv) 1986 | app.setApplicationName("Python Audio Spectrogram Explorer") 1987 | w = MainWindow() 1988 | sys.exit(app.exec_()) --------------------------------------------------------------------------------