vskode commited on
Commit ·
c96678c
0
Parent(s):
initial commit without binaries or large files for huggingface
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +16 -0
- .pre-commit-config.yaml +6 -0
- LICENSE +21 -0
- README.md +328 -0
- acodet/annotate.py +229 -0
- acodet/annotation_mappers.json +11 -0
- acodet/augmentation.py +185 -0
- acodet/combine_annotations.py +293 -0
- acodet/create_session_file.py +29 -0
- acodet/evaluate.py +253 -0
- acodet/front_end/help_strings.py +65 -0
- acodet/front_end/st_annotate.py +262 -0
- acodet/front_end/st_generate_data.py +115 -0
- acodet/front_end/st_train.py +92 -0
- acodet/front_end/st_visualization.py +286 -0
- acodet/front_end/utils.py +238 -0
- acodet/funcs.py +747 -0
- acodet/global_config.py +144 -0
- acodet/hourly_presence.py +765 -0
- acodet/humpback_model_dir/README.md +15 -0
- acodet/humpback_model_dir/front_end.py +105 -0
- acodet/humpback_model_dir/humpback_model.py +384 -0
- acodet/humpback_model_dir/leaf_pcen.py +103 -0
- acodet/models.py +318 -0
- acodet/plot_utils.py +466 -0
- acodet/split_daily_annots.py +39 -0
- acodet/src/imgs/annotation_output.png +0 -0
- acodet/src/imgs/gui_sequence_limit.png +0 -0
- acodet/src/imgs/sequence_limit.png +0 -0
- acodet/tfrec.py +434 -0
- acodet/train.py +290 -0
- advanced_config.yml +106 -0
- macM1_requirements/requirements_m1-1.txt +11 -0
- macM1_requirements/requirements_m1-2.txt +1 -0
- pyproject.toml +3 -0
- requirements.txt +16 -0
- run.py +80 -0
- simple_config.yml +99 -0
- streamlit_app.py +94 -0
- tests/test.py +132 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_00-00-05_annot_2022-11-30_01.txt +28 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_00-15-55_annot_2022-11-30_01.txt +20 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_01-00-05_annot_2022-11-30_01.txt +25 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_01-21-14_annot_2022-11-30_01.txt +6 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_02-00-05_annot_2022-11-30_01.txt +60 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_03-00-05_annot_2022-11-30_01.txt +83 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_04-00-05_annot_2022-11-30_01.txt +5 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_04-05-20_annot_2022-11-30_01.txt +75 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_05-00-05_annot_2022-11-30_01.txt +44 -0
- tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_05-10-39_annot_2022-11-30_01.txt +73 -0
.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.wav
|
| 2 |
+
*.zip
|
| 3 |
+
utils/__*
|
| 4 |
+
*__pycache__*
|
| 5 |
+
acodet/_*
|
| 6 |
+
acodet/src/tmp_*
|
| 7 |
+
acodet/src/models/Humpback_20221130/
|
| 8 |
+
_*
|
| 9 |
+
.vscode/*
|
| 10 |
+
tests/test_files/analysi*
|
| 11 |
+
tests/test_files/test_annoted_files/thresh_0.5/N1/analysi*
|
| 12 |
+
generated*
|
| 13 |
+
tests/test_files/test_gen*
|
| 14 |
+
tests/test_files/test_com*
|
| 15 |
+
tests/test_files/test_tfr*
|
| 16 |
+
simple_test*
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/psf/black
|
| 3 |
+
rev: 23.7.0
|
| 4 |
+
hooks:
|
| 5 |
+
- id: black
|
| 6 |
+
language_version: python3.9
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) [2022] [Vincent Kather]
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **acodet** - **Aco**ustic **Det**ector
|
| 2 |
+
## Framework for the **usage** and **training** of acoustic species detectors based on CNN models
|
| 3 |
+
|
| 4 |
+
### Highlights
|
| 5 |
+
- **Integrated graphical user interface (GUI), so no coding required!**
|
| 6 |
+
- Supports Raven table format
|
| 7 |
+
- resulting spreadsheets can be directly imported into raven to view annotations
|
| 8 |
+
- automatic generation of presence/absence visualizations
|
| 9 |
+
- GUI supports interactive visualizations, allowing you to adjust model thresholds and instantly view the results
|
| 10 |
+
- headless version included for those that prefer command line tools
|
| 11 |
+
- interactive post-processing methods to reduce false-positives
|
| 12 |
+
---------------------------------------------------
|
| 13 |
+
sample output:
|
| 14 |
+
|
| 15 |
+

|
| 16 |
+
|
| 17 |
+
Play around with the GUI in the __Online Demo__ here:
|
| 18 |
+
https://acodet-web.streamlit.app/
|
| 19 |
+
(the program will look identical when executed on your computer)
|
| 20 |
+
|
| 21 |
+
__Video tutorials__ for installation and usage of the AcoDet GUI:
|
| 22 |
+
https://www.youtube.com/watch?v=bJf4d8qf9h0&list=PLQOW4PvEYW-GNWIYRehs2-4-sa9T20c8A
|
| 23 |
+
|
| 24 |
+
The corresponding paper to acodet can be found here:
|
| 25 |
+
https://doi.org/10.1121/10.0025275
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
## Table of Contents
|
| 29 |
+
- [Installation](#installation)
|
| 30 |
+
- [Installation on Windows](#installation-on-windows)
|
| 31 |
+
- [Installation on Mac](#installation-on-mac)
|
| 32 |
+
- [Installation on Linux](#installation-on-linux)
|
| 33 |
+
- [Usage](#usage)
|
| 34 |
+
- [acodet Usage with GUI](#acodet-usage-with-gui)
|
| 35 |
+
- [Usecase 1: Generating annotations (GUI)](#usecase-1-generating-annotations-gui)
|
| 36 |
+
- [Usecase 2: Generating new training data (GUI)](#usecase-2-generating-new-training-data-gui)
|
| 37 |
+
- [Usecase 3: Training (GUI)](#usecase-3-training-gui)
|
| 38 |
+
- [acodet Usage headless](#acodet-usage-headless)
|
| 39 |
+
- [Usecase 1: Generating annotations](#usecase-1-generating-annotations)
|
| 40 |
+
- [Usecase 2: Generating new training data](#usecase-2-generating-new-training-data)
|
| 41 |
+
- [Usecase 3: Training](#usecase-3-training)
|
| 42 |
+
- [Explanation of Sequence limit](#explanation-of-sequence-limit)
|
| 43 |
+
- [Citation](#citation)
|
| 44 |
+
- [FAQ](#faq)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
----------------------------------------------------
|
| 48 |
+
# Installation
|
| 49 |
+
## Installation on Windows
|
| 50 |
+
### Preliminary software installations:
|
| 51 |
+
- install python 3.8: (standard install, no admin privileges needed)
|
| 52 |
+
<https://www.python.org/ftp/python/3.8.0/python-3.8.0-amd64.exe>
|
| 53 |
+
- install git bash: (default install)
|
| 54 |
+
<https://github.com/git-for-windows/git/releases/download/v2.38.1.windows.1/Git-2.38.1-64-bit.exe>
|
| 55 |
+
|
| 56 |
+
### Installation instructions
|
| 57 |
+
- create project directory in location of your choice
|
| 58 |
+
- open git bash in project directory (right click, Git Bash here)
|
| 59 |
+
- clone the repository:
|
| 60 |
+
|
| 61 |
+
`git clone https://github.com/vskode/acodet.git`
|
| 62 |
+
- Install virtualenv (copy and paste in Git Bash console):
|
| 63 |
+
|
| 64 |
+
`"$HOME/AppData/Local/Programs/Python/Python38/python" -m pip install virtualenv`
|
| 65 |
+
|
| 66 |
+
- Create a new virtual environment (default name env_acodet can be changed):
|
| 67 |
+
|
| 68 |
+
`"$HOME/AppData/Local/Programs/Python/Python38/python" -m virtualenv env_acodet`
|
| 69 |
+
|
| 70 |
+
- activate newly created virtual environment (change env_acodet if necessary):
|
| 71 |
+
|
| 72 |
+
`source env_acodet/Scripts/activate`
|
| 73 |
+
|
| 74 |
+
- Install required packages:
|
| 75 |
+
|
| 76 |
+
`pip install -r acodet/requirements.txt`
|
| 77 |
+
|
| 78 |
+
-------------------------
|
| 79 |
+
|
| 80 |
+
## Installation on Mac
|
| 81 |
+
### Preliminary software installations:
|
| 82 |
+
- install python 3.8: (standard install, no admin privileges needed)
|
| 83 |
+
<https://www.python.org/ftp/python/3.8.7/python-3.8.7-macosx10.9.pkg>
|
| 84 |
+
- install git: (default install)
|
| 85 |
+
- simply type `git` into the terminal and follow the installation instructions
|
| 86 |
+
|
| 87 |
+
### Installation instructions
|
| 88 |
+
- create project directory in location of your choice
|
| 89 |
+
- open a terminal in the project directory
|
| 90 |
+
- clone the repository:
|
| 91 |
+
|
| 92 |
+
`git clone https://github.com/vskode/acodet.git`
|
| 93 |
+
- Install virtualenv (copy and paste in Git Bash console):
|
| 94 |
+
|
| 95 |
+
`/usr/bin/python/Python38/python -m pip install virtualenv`
|
| 96 |
+
|
| 97 |
+
- Create a new virtual environment (default name env_acodet can be changed):
|
| 98 |
+
|
| 99 |
+
`/usr/bin/python/Python38/python -m virtualenv env_acodet`
|
| 100 |
+
|
| 101 |
+
- activate newly created virtual environment (change env_acodet if necessary):
|
| 102 |
+
|
| 103 |
+
`source env_acodet/bin/activate`
|
| 104 |
+
|
| 105 |
+
- Install required packages:
|
| 106 |
+
- if you have a M1 chip in your mac, run:
|
| 107 |
+
|
| 108 |
+
`pip install -r acodet/macM1_requirements/requirements_m1-1.txt`
|
| 109 |
+
- then run
|
| 110 |
+
|
| 111 |
+
`pip install -r acodet/macM1_requirements/requirements_m1-1.txt`
|
| 112 |
+
|
| 113 |
+
- if you have an older mac, run:
|
| 114 |
+
|
| 115 |
+
`pip install -r acodet/requirements.txt`
|
| 116 |
+
|
| 117 |
+
--------------------------------------------
|
| 118 |
+
## Installation on Linux
|
| 119 |
+
### Preliminary software installations:
|
| 120 |
+
- install python 3.8: (standard install, no admin privileges needed)
|
| 121 |
+
<https://www.python.org/ftp/python/3.8.0/python-3.8.0-amd64.exe>
|
| 122 |
+
- install git bash: (default install)
|
| 123 |
+
<https://github.com/git-for-windows/git/releases/download/v2.38.1.windows.1/Git-2.38.1-64-bit.exe>
|
| 124 |
+
|
| 125 |
+
### Installation instructions
|
| 126 |
+
- create project directory in location of your choice
|
| 127 |
+
- open a terminal in the project directory
|
| 128 |
+
- clone the repository:
|
| 129 |
+
|
| 130 |
+
`git clone https://github.com/vskode/acodet.git`
|
| 131 |
+
- Install virtualenv (copy and paste in Git Bash console):
|
| 132 |
+
|
| 133 |
+
`/usr/bin/python/Python38/python -m pip install virtualenv`
|
| 134 |
+
|
| 135 |
+
- Create a new virtual environment (default name env_acodet can be changed):
|
| 136 |
+
|
| 137 |
+
`/usr/bin/python/Python38/python -m virtualenv env_acodet`
|
| 138 |
+
|
| 139 |
+
- activate newly created virtual environment (change env_acodet if necessary):
|
| 140 |
+
|
| 141 |
+
`source env_acodet/bin/activate`
|
| 142 |
+
|
| 143 |
+
- Install required packages:
|
| 144 |
+
|
| 145 |
+
`pip install -r acodet/requirements.txt`
|
| 146 |
+
|
| 147 |
+
# Usage
|
| 148 |
+
## AcoDet usage with GUI
|
| 149 |
+
|
| 150 |
+
AcoDet provides a graphical user interface (GUI) for users to intuitively use the program. All inputs and outputs are handled through the GUI. To run the gui, run (while in acodet directory):
|
| 151 |
+
|
| 152 |
+
`streamlit run streamlit_app.py`
|
| 153 |
+
|
| 154 |
+
This should start a new tab in a web browser which runs the interface that you can interact with. It is important that your virtual environment where you have installed the required packages is active, for that see the Installation sections. To activate the environment run
|
| 155 |
+
|
| 156 |
+
`source ../env_acodet/Scripts/activate` (on Windows)
|
| 157 |
+
or
|
| 158 |
+
|
| 159 |
+
`source ../env_acodet/bin/activate` (on Mac/Linux)
|
| 160 |
+
|
| 161 |
+
while your terminal directory is inside **acodet**.
|
| 162 |
+
|
| 163 |
+
### Usecase 1: generating annotations (GUI)
|
| 164 |
+
|
| 165 |
+
- Choose the 1 - Inference option from the first drop-down menu
|
| 166 |
+
- Choose between the predefined Settings
|
| 167 |
+
0. run all of the steps
|
| 168 |
+
1. generating new annotations
|
| 169 |
+
2. filterin existing annotations
|
| 170 |
+
3. generating hourly predictions
|
| 171 |
+
- click Next
|
| 172 |
+
- Depending on your choice you will be prompted to enter the path leading to either your sound files our existing annotation files
|
| 173 |
+
- Enter a path in the text field that is one directory above the folder you would like to use
|
| 174 |
+
- In the dropdown menu you will be presented with all the folders inside the specified path. Choose the one you would like to work with
|
| 175 |
+
- **Important**: time stamps are required within the file names of the source files for steps 0. and 3.
|
| 176 |
+
- If required, choose a Model threshold
|
| 177 |
+
- click Run computation
|
| 178 |
+
- A progress bar should show the progress of your computations
|
| 179 |
+
- click Show results
|
| 180 |
+
- The Output section will provide you with information of the location of the files, and depending on your choice of predifines Settings will show different tabs.
|
| 181 |
+
- the "Stats" is an overview of all processed files, with timestamp and number of predictions
|
| 182 |
+
- the "Annot. Files" gives you a dropdown menu where you can look into prediction values for each vocalization within each source file. By default the threshold for this will be at 0.5, meaning that all sections with prediction values below that will be discarded.
|
| 183 |
+
- the "Filtered Files" shows the same as the previous tab, however, it only shows sections with previously defined values exceeding the predefined threshold.
|
| 184 |
+
- the "Annotaion Plots" shows you a visualization revealing the number of anotations per hour in your dataset. Choose your dataset from the dropdown (in some cases there is only one dataset inside your previously defined folder).
|
| 185 |
+
- The calculations behind this visualization is explained in detail in the corresponding journal paper that is currenlty under review and will be linked here as soon as published.
|
| 186 |
+
- You can choose between a "Simple limit" and a "Sequence limit"
|
| 187 |
+
- the main distinction is whether consecute vocalizations are required for them to be counted (this should help reduce false positives)
|
| 188 |
+
- the "Simple limit" will compute much faster than the "Sequence limit"
|
| 189 |
+
- You can also change the threshold of the model predictions which will then allo you to update the visualization. If the "Sequence limit" is chose, the number limit can also be changed, which will change the required number of consecutive vocalizations for them to be counted. (Try it out)
|
| 190 |
+
- All visualizations can be exported as .png files by clicking on the small camera icon in the top right.
|
| 191 |
+
- the "Presence Plots" shows a similar visualization as the previous section, however, only showing binary presence.
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
### Usecase 2: generating new training data (GUI)
|
| 195 |
+
|
| 196 |
+
This feature is currently not integrated in the gui.
|
| 197 |
+
### Usecase 3: training (GUI)
|
| 198 |
+
|
| 199 |
+
This feature is currently not integrated in the gui.
|
| 200 |
+
|
| 201 |
+
## AcoDet usage headless
|
| 202 |
+
Users only need to change the files **simple_congif.yml** and **advanced_config.yml** to use AcoDet. Once the config files are changed, users can run the program by running the command `python run.py` inside the **acodet** directory.
|
| 203 |
+
|
| 204 |
+
### Usecase 1: generating annotations
|
| 205 |
+
To generate annotations:
|
| 206 |
+
- open the file **simple_config.yml** in any Editor (default is Notepad).
|
| 207 |
+
- change `run_config` to `1`
|
| 208 |
+
- change `predefined_settings` to one of the following:
|
| 209 |
+
- `1` for generating annotations with a threshold of 0.5
|
| 210 |
+
- `2` for generating annotations with a custom threshold
|
| 211 |
+
- specify threshold (**thresh**) value in **simple_config.yml** (defaults to 0.9)
|
| 212 |
+
- `3` for generating hourly counts and presence spreadsheets and visualizations (using the sequence criterion and the simple limit)
|
| 213 |
+
- _simple limit_ and _sequence criterion_ are accumulation metrics aiming to deliver hourly presence information, while filtering out false positives
|
| 214 |
+
- _simple limit_ -> only consider annotations if the number of annotations exceeding the **thresh** value is higher than the value for **simple_limit** in **simple_config.yml** (in a given hour in the dataset)
|
| 215 |
+
- _sequence criterion_ -> only consider annotations if the number of consecutive annotations within **sc_con_win** number of windows exceeding the **sc_thresh** value is higher than **sc_limit** (in a given hour in the dataset)
|
| 216 |
+
- hourly counts gives the number of annotations according to the accumulation metrics
|
| 217 |
+
- hourly presence gives a binary (0 -> no whale; 1 -> whale) corresponding to whether the accumulation metrics are satisfied
|
| 218 |
+
- `4` for generating hourly counts and presence spreadsheets and visualizations (using only the simple limit)
|
| 219 |
+
- or `0` to run all of the above in sequece
|
| 220 |
+
- change `sound_files_source` to the top level directory containing the dataset(s) you want to annotate
|
| 221 |
+
|
| 222 |
+
- once finished, save the **simple_config.yml** file
|
| 223 |
+
|
| 224 |
+
To start the program:
|
| 225 |
+
- activate the virtual environment again:
|
| 226 |
+
|
| 227 |
+
`source env_acodet/Scripts/activate`
|
| 228 |
+
|
| 229 |
+
- run the run.py script:
|
| 230 |
+
|
| 231 |
+
`python acodet/run.py`
|
| 232 |
+
|
| 233 |
+
### Output
|
| 234 |
+
|
| 235 |
+
The software will now run thorugh your dataset and gerate annotations for every (readable) soundifle within the dataset. While running, a spreadsheet, called stats.csv is continuously updated showing information on the annotations for every file (do not open while program is still running, because the program wont be able to access it).
|
| 236 |
+
|
| 237 |
+
The program will create a directory called `generated_annotatoins` in the project directory. It will then create a directory corresponding to the date and time that you started the annotation process. Within that directory you will find a directory `thresh_0.5` corresponding to all annotations with a threshold of 0.5. Furthermore you will find the `stats.csv` spreadsheet.
|
| 238 |
+
|
| 239 |
+
If you have chosen option 2 (or 0) you will also find a directory `thresh_0.x` where the x stands for the custom threshold you specified in the **simple_config.yml** file. Within the `thresh` directories you will find the name of your dataset.
|
| 240 |
+
|
| 241 |
+
If you have chosen option 3, 4 or 0 you will find a directory `analysis` within the dataset directory. In that directory you will find spreadsheets for hourly presence and hourly counts, as well as visualizations of the hourly presence and hourly counts.
|
| 242 |
+
|
| 243 |
+
### Usecase 2: generating new training data
|
| 244 |
+
|
| 245 |
+
Either use manually created annotations -> option 2, or create new annotations by reviewing the automatically generated annotations -> option 1.
|
| 246 |
+
|
| 247 |
+
For option 1, use Raven to open sound files alongside their automatically generated annotations. Edit the column `Predictions/Comments` by writing `n` for noise, `c` for call, or `u` for undefined. If the majority of the shown windows are calls, add the suffix `_allcalls` before the `.txt` ending so that the program will automatically label all of the windows as calls, unless specified as `n`, `c`, or `u`. The suffix `_allnoise` will do the same for noise. The suffix `_annotated` will label all unchanged windows as undefined - thereby essentially ignoring them for the created dataset.
|
| 248 |
+
|
| 249 |
+
Once finished, insert the top-level directory path to the `reviewed_annotation_source` variable in **simple_config.yml**.
|
| 250 |
+
|
| 251 |
+
To generate new training data:
|
| 252 |
+
- open the file **simple_config.yml** in any Editor (default is Notepad).
|
| 253 |
+
- change `run_config` to `2`
|
| 254 |
+
- change `predefined_settings` to one of the following:
|
| 255 |
+
- `1` for generating training data from reviewed annotations
|
| 256 |
+
- `2` for generating training data from manually created training data (space in between annotations will be interpretted as noise)
|
| 257 |
+
- change `sound_files_source` to the top level directory containing the dataset(s) containing the sound files
|
| 258 |
+
|
| 259 |
+
- once finished, save the **simple_config.yml** file
|
| 260 |
+
|
| 261 |
+
To start the program:
|
| 262 |
+
- activate the virtual environment again:
|
| 263 |
+
|
| 264 |
+
`source env_acodet/Scripts/activate`
|
| 265 |
+
|
| 266 |
+
- run the run.py script:
|
| 267 |
+
|
| 268 |
+
`python acodet/run.py`
|
| 269 |
+
|
| 270 |
+
### Usecase 3: training
|
| 271 |
+
|
| 272 |
+
To train the model:
|
| 273 |
+
- open the file **simple_config.yml** in any Editor (default is Notepad).
|
| 274 |
+
- change `run_config` to `3`
|
| 275 |
+
- change `predefined_settings` to one of the following:
|
| 276 |
+
- `1` for generating training data from reviewed annotations
|
| 277 |
+
|
| 278 |
+
- once finished, save the **simple_config.yml** file
|
| 279 |
+
- more adcanced changes for model parameters can be done in **advanced_config.yml**
|
| 280 |
+
|
| 281 |
+
To start the program:
|
| 282 |
+
- activate the virtual environment again:
|
| 283 |
+
|
| 284 |
+
`source env_acodet/Scripts/activate`
|
| 285 |
+
|
| 286 |
+
- run the run.py script:
|
| 287 |
+
|
| 288 |
+
`python acodet/run.py`
|
| 289 |
+
|
| 290 |
+
# Explanation of Sequence limit
|
| 291 |
+
Besides a simple thresholding (simple limit) the sequence limit can be used to distinguish repeating vocalizations from other noise sources. For humpback whales this vastly reduces the number of generated false positives.
|
| 292 |
+
|
| 293 |
+
To briefly explain:
|
| 294 |
+
In a stream of 20 consecutive windows (of approx. 4 s length) you can set limit and threshold. After applying the limit, predictions are only kept if they exceed the threshold and occur in thre frequency set by the limit. This is especially convenient for __hourly presence__ annotations. The below image shows an example of 20 consecutive windows with their given model prediction values. The highlighted values exceed the threshold and since they occur in the required frequency (Limit=3), the hourly presence yield the value 1.
|
| 295 |
+
|
| 296 |
+

|
| 297 |
+
|
| 298 |
+
The sequence limit can be useful in noisy environments, where vocalizations are masked by noise and their repetitiveness can be used to distinguish them from irregular background noise.
|
| 299 |
+
|
| 300 |
+
Threshold and limit can be set interactively and their effect on the data can be analyzed right away. As this is only post-processing the existing annotations, computing time is very fast. The following image shows a screenshot of the sequence limit in the acodet GUI.
|
| 301 |
+
|
| 302 |
+

|
| 303 |
+
|
| 304 |
+
# Citation
|
| 305 |
+
|
| 306 |
+
If you used acodet in your work, please reference the following:
|
| 307 |
+
|
| 308 |
+
Vincent Kather, Fabian Seipel, Benoit Berges, Genevieve Davis, Catherine Gibson, Matt Harvey, Lea-Anne Henry, Andrew Stevenson, Denise Risch; Development of a machine learning detector for North Atlantic humpback whale song. J. Acoust. Soc. Am. 1 March 2024; 155 (3): 2050–2064.
|
| 309 |
+
|
| 310 |
+
For bibtex:
|
| 311 |
+
```bibtex
|
| 312 |
+
@article{10.1121/10.0025275,
|
| 313 |
+
author = {Kather, Vincent and Seipel, Fabian and Berges, Benoit and Davis, Genevieve and Gibson, Catherine and Harvey, Matt and Henry, Lea-Anne and Stevenson, Andrew and Risch, Denise},
|
| 314 |
+
title = "{Development of a machine learning detector for North Atlantic humpback whale song}",
|
| 315 |
+
journal = {The Journal of the Acoustical Society of America},
|
| 316 |
+
volume = {155},
|
| 317 |
+
number = {3},
|
| 318 |
+
pages = {2050-2064},
|
| 319 |
+
year = {2024},
|
| 320 |
+
month = {03},
|
| 321 |
+
issn = {0001-4966},
|
| 322 |
+
doi = {10.1121/10.0025275}
|
| 323 |
+
}
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
# FAQ
|
| 327 |
+
|
| 328 |
+
At the moment the generation of new training data and the training are not yet supported in the graphical user interface.
|
acodet/annotate.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from acodet import models
|
| 3 |
+
from acodet.funcs import (
|
| 4 |
+
get_files,
|
| 5 |
+
gen_annotations,
|
| 6 |
+
get_dt_filename,
|
| 7 |
+
remove_str_flags_from_predictions,
|
| 8 |
+
)
|
| 9 |
+
from acodet import global_config as conf
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class MetaData:
|
| 16 |
+
def __init__(self):
|
| 17 |
+
"""
|
| 18 |
+
Initialize the MetaData class with the columns that will be used to
|
| 19 |
+
store the metadata of the generated annotations.
|
| 20 |
+
"""
|
| 21 |
+
self.filename = "filename"
|
| 22 |
+
self.f_dt = "date from timestamp"
|
| 23 |
+
self.n_pred_col = "number of predictions"
|
| 24 |
+
self.avg_pred_col = "average prediction value"
|
| 25 |
+
self.n_pred08_col = "number of predictions with thresh>0.8"
|
| 26 |
+
self.n_pred09_col = "number of predictions with thresh>0.9"
|
| 27 |
+
self.time_per_file = "computing time [s]"
|
| 28 |
+
if not "timestamp_folder" in conf.session:
|
| 29 |
+
self.df = pd.DataFrame(
|
| 30 |
+
columns=[
|
| 31 |
+
self.filename,
|
| 32 |
+
self.f_dt,
|
| 33 |
+
self.n_pred_col,
|
| 34 |
+
self.avg_pred_col,
|
| 35 |
+
self.n_pred08_col,
|
| 36 |
+
self.n_pred09_col,
|
| 37 |
+
]
|
| 38 |
+
)
|
| 39 |
+
else:
|
| 40 |
+
self.df = pd.read_csv(
|
| 41 |
+
conf.session["timestamp_folder"].parent.parent.joinpath(
|
| 42 |
+
"stats.csv"
|
| 43 |
+
)
|
| 44 |
+
)
|
| 45 |
+
self.df.pop("Unnamed: 0")
|
| 46 |
+
|
| 47 |
+
def append_and_save_meta_file(
|
| 48 |
+
self,
|
| 49 |
+
file: Path,
|
| 50 |
+
annot: pd.DataFrame,
|
| 51 |
+
f_ind: int,
|
| 52 |
+
timestamp_foldername: str,
|
| 53 |
+
relativ_path: str = conf.SOUND_FILES_SOURCE,
|
| 54 |
+
computing_time: str = "not calculated",
|
| 55 |
+
**kwargs,
|
| 56 |
+
):
|
| 57 |
+
"""
|
| 58 |
+
Append the metadata of the generated annotations to the dataframe and
|
| 59 |
+
save it to a csv file.
|
| 60 |
+
|
| 61 |
+
Parameters
|
| 62 |
+
----------
|
| 63 |
+
file : Path
|
| 64 |
+
Path to the file that was annotated.
|
| 65 |
+
annot : pd.DataFrame
|
| 66 |
+
Dataframe containing the annotations.
|
| 67 |
+
f_ind : int
|
| 68 |
+
Index of the file.
|
| 69 |
+
timestamp_foldername : str
|
| 70 |
+
Timestamp of the annotation run for folder name.
|
| 71 |
+
relativ_path : str, optional
|
| 72 |
+
Path of folder containing files , by default conf.SOUND_FILES_SOURCE
|
| 73 |
+
computing_time : str, optional
|
| 74 |
+
Amount of time that prediction took, by default "not calculated"
|
| 75 |
+
"""
|
| 76 |
+
self.df.loc[f_ind, self.f_dt] = str(get_dt_filename(file).date())
|
| 77 |
+
self.df.loc[f_ind, self.filename] = Path(file).relative_to(
|
| 78 |
+
relativ_path
|
| 79 |
+
)
|
| 80 |
+
# TODO relative_path muss noch dauerhaft geändert werden
|
| 81 |
+
self.df.loc[f_ind, self.n_pred_col] = len(annot)
|
| 82 |
+
df_clean = remove_str_flags_from_predictions(annot)
|
| 83 |
+
self.df.loc[f_ind, self.avg_pred_col] = np.mean(
|
| 84 |
+
df_clean[conf.ANNOTATION_COLUMN]
|
| 85 |
+
)
|
| 86 |
+
self.df.loc[f_ind, self.n_pred08_col] = len(
|
| 87 |
+
df_clean.loc[df_clean[conf.ANNOTATION_COLUMN] > 0.8]
|
| 88 |
+
)
|
| 89 |
+
self.df.loc[f_ind, self.n_pred09_col] = len(
|
| 90 |
+
df_clean.loc[df_clean[conf.ANNOTATION_COLUMN] > 0.9]
|
| 91 |
+
)
|
| 92 |
+
self.df.loc[f_ind, self.time_per_file] = computing_time
|
| 93 |
+
self.df.to_csv(
|
| 94 |
+
Path(conf.GEN_ANNOTS_DIR)
|
| 95 |
+
.joinpath(timestamp_foldername)
|
| 96 |
+
.joinpath("stats.csv")
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def run_annotation(train_date=None, **kwargs):
|
| 101 |
+
files = get_files(location=conf.SOUND_FILES_SOURCE, search_str="**/*")
|
| 102 |
+
if not "timestamp_folder" in conf.session:
|
| 103 |
+
timestamp_foldername = time.strftime(
|
| 104 |
+
"%Y-%m-%d_%H-%M-%S", time.gmtime()
|
| 105 |
+
)
|
| 106 |
+
timestamp_foldername += conf.ANNOTS_TIMESTAMP_FOLDER
|
| 107 |
+
mdf = MetaData()
|
| 108 |
+
f_ind = 0
|
| 109 |
+
|
| 110 |
+
else:
|
| 111 |
+
timestamp_foldername = conf.session[
|
| 112 |
+
"timestamp_folder"
|
| 113 |
+
].parent.parent.stem
|
| 114 |
+
last_annotated_file = list(
|
| 115 |
+
conf.session["timestamp_folder"].rglob("*.txt")
|
| 116 |
+
)[-1]
|
| 117 |
+
file_stems = [f.stem for f in files]
|
| 118 |
+
file_idx = np.where(
|
| 119 |
+
np.array(file_stems) == last_annotated_file.stem.split("_annot")[0]
|
| 120 |
+
)[0][0]
|
| 121 |
+
files = files[file_idx:]
|
| 122 |
+
mdf = MetaData()
|
| 123 |
+
f_ind = file_idx - 1
|
| 124 |
+
|
| 125 |
+
if not train_date:
|
| 126 |
+
model = models.init_model()
|
| 127 |
+
mod_label = conf.MODEL_NAME
|
| 128 |
+
else:
|
| 129 |
+
df = pd.read_csv("../trainings/20221124_meta_trainings.csv")
|
| 130 |
+
row = df.loc[df["training_date"] == train_date]
|
| 131 |
+
model_name = row.Model.values[0]
|
| 132 |
+
keras_mod_name = row.keras_mod_name.values[0]
|
| 133 |
+
|
| 134 |
+
model = models.init_model(
|
| 135 |
+
model_instance=model_name,
|
| 136 |
+
checkpoint_dir=f"../trainings/{train_date}/unfreeze_no-TF",
|
| 137 |
+
keras_mod_name=keras_mod_name,
|
| 138 |
+
)
|
| 139 |
+
mod_label = train_date
|
| 140 |
+
|
| 141 |
+
if conf.STREAMLIT:
|
| 142 |
+
import streamlit as st
|
| 143 |
+
|
| 144 |
+
st.session_state.progbar1 = 0
|
| 145 |
+
for i, file in enumerate(files):
|
| 146 |
+
if file.is_dir():
|
| 147 |
+
continue
|
| 148 |
+
|
| 149 |
+
if conf.STREAMLIT:
|
| 150 |
+
import streamlit as st
|
| 151 |
+
|
| 152 |
+
st.session_state.progbar1 += 1
|
| 153 |
+
f_ind += 1
|
| 154 |
+
start = time.time()
|
| 155 |
+
annot = gen_annotations(
|
| 156 |
+
file,
|
| 157 |
+
model,
|
| 158 |
+
mod_label=mod_label,
|
| 159 |
+
timestamp_foldername=timestamp_foldername,
|
| 160 |
+
num_of_files=len(files),
|
| 161 |
+
**kwargs,
|
| 162 |
+
)
|
| 163 |
+
computing_time = time.time() - start
|
| 164 |
+
mdf.append_and_save_meta_file(
|
| 165 |
+
file,
|
| 166 |
+
annot,
|
| 167 |
+
f_ind,
|
| 168 |
+
timestamp_foldername,
|
| 169 |
+
computing_time=computing_time,
|
| 170 |
+
**kwargs,
|
| 171 |
+
)
|
| 172 |
+
return timestamp_foldername
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def check_for_multiple_time_dirs_error(path):
|
| 176 |
+
if not path.joinpath(conf.THRESH_LABEL).exists():
|
| 177 |
+
subdirs = [l for l in path.iterdir() if l.is_dir()]
|
| 178 |
+
path = path.joinpath(subdirs[-1].stem)
|
| 179 |
+
return path
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def filter_annots_by_thresh(time_dir=None, **kwargs):
|
| 183 |
+
if not time_dir:
|
| 184 |
+
path = Path(conf.GEN_ANNOT_SRC)
|
| 185 |
+
else:
|
| 186 |
+
path = Path(conf.GEN_ANNOTS_DIR).joinpath(time_dir)
|
| 187 |
+
files = get_files(location=path, search_str="**/*txt")
|
| 188 |
+
files = [f for f in files if conf.THRESH_LABEL in str(f.parent)]
|
| 189 |
+
path = check_for_multiple_time_dirs_error(path)
|
| 190 |
+
for i, file in enumerate(files):
|
| 191 |
+
try:
|
| 192 |
+
annot = pd.read_csv(file, sep="\t")
|
| 193 |
+
except Exception as e:
|
| 194 |
+
print(
|
| 195 |
+
"Could not process file, maybe not an annotation file?",
|
| 196 |
+
"Error: ",
|
| 197 |
+
e,
|
| 198 |
+
)
|
| 199 |
+
annot = annot.loc[annot[conf.ANNOTATION_COLUMN] >= conf.THRESH]
|
| 200 |
+
save_dir = (
|
| 201 |
+
path.joinpath(f"thresh_{conf.THRESH}")
|
| 202 |
+
.joinpath(file.relative_to(path.joinpath(conf.THRESH_LABEL)))
|
| 203 |
+
.parent
|
| 204 |
+
)
|
| 205 |
+
save_dir.mkdir(exist_ok=True, parents=True)
|
| 206 |
+
|
| 207 |
+
if "Selection" in annot.columns:
|
| 208 |
+
annot.index.name = "Selection"
|
| 209 |
+
annot = annot.drop(columns=["Selection"])
|
| 210 |
+
else:
|
| 211 |
+
annot.index = np.arange(1, len(annot) + 1)
|
| 212 |
+
annot.index.name = "Selection"
|
| 213 |
+
annot.to_csv(save_dir.joinpath(file.stem + file.suffix), sep="\t")
|
| 214 |
+
if conf.STREAMLIT and "progbar1" in kwargs.keys():
|
| 215 |
+
kwargs["progbar1"].progress((i + 1) / len(files), text="Progress")
|
| 216 |
+
else:
|
| 217 |
+
print(f"Writing file {i+1}/{len(files)}")
|
| 218 |
+
if conf.STREAMLIT:
|
| 219 |
+
return path
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
if __name__ == "__main__":
|
| 223 |
+
train_dates = ["2022-11-30_01"]
|
| 224 |
+
|
| 225 |
+
for train_date in train_dates:
|
| 226 |
+
start = time.time()
|
| 227 |
+
run_annotation(train_date)
|
| 228 |
+
end = time.time()
|
| 229 |
+
print(end - start)
|
acodet/annotation_mappers.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"default_mapper": {
|
| 3 |
+
"Begin Time (s)": "start",
|
| 4 |
+
"End Time (s)": "end",
|
| 5 |
+
"Low Freq (Hz)" : "freq_min",
|
| 6 |
+
"High Freq (Hz)" : "freq_max"},
|
| 7 |
+
"file_offset_mapper": {
|
| 8 |
+
"File Offset (s)": "start",
|
| 9 |
+
"Low Freq (Hz)" : "freq_min",
|
| 10 |
+
"High Freq (Hz)" : "freq_max"}
|
| 11 |
+
}
|
acodet/augmentation.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
from keras_cv.layers import BaseImageAugmentationLayer
|
| 3 |
+
from acodet.plot_utils import plot_sample_spectrograms
|
| 4 |
+
import tensorflow_io as tfio
|
| 5 |
+
|
| 6 |
+
AUTOTUNE = tf.data.AUTOTUNE
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class CropAndFill(BaseImageAugmentationLayer):
|
| 10 |
+
def __init__(self, height: int, width: int, seed: int = None) -> None:
|
| 11 |
+
"""
|
| 12 |
+
Augmentation class inheriting from keras' Augmentation Base class.
|
| 13 |
+
This class takes images, cuts them at a random x position and then
|
| 14 |
+
appends the first section to the second section. It is intended for
|
| 15 |
+
spectrograms of labelled bioacoustics data. This way the vocalization
|
| 16 |
+
in the spectrogram is time shifted and potentially cut. All of which
|
| 17 |
+
is possible to occur due to a windowing of a recording file that is
|
| 18 |
+
intended for inference.
|
| 19 |
+
It is essentially a time shift augmentation whilst preserving window
|
| 20 |
+
length and not requiring reloading data from the source file.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
height (int): height of image
|
| 24 |
+
width (int): width of image
|
| 25 |
+
seed (int, optional): create randomization seed. Defaults to None.
|
| 26 |
+
"""
|
| 27 |
+
super().__init__()
|
| 28 |
+
self.height = height
|
| 29 |
+
self.width = width
|
| 30 |
+
self.seed = seed
|
| 31 |
+
tf.random.set_seed(self.seed)
|
| 32 |
+
|
| 33 |
+
def call(self, audio: tf.Tensor):
|
| 34 |
+
"""
|
| 35 |
+
Compute time shift augmentation by creating a random slicing
|
| 36 |
+
position and then returning the reordered image.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
audio (tf.Tensor): input image
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
tf.Tensor: reordered image
|
| 43 |
+
"""
|
| 44 |
+
beg = tf.random.uniform(
|
| 45 |
+
shape=[], maxval=self.width // 2, dtype=tf.int32
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# for debugging purposes
|
| 49 |
+
# tf.print('time shift augmentation computed)
|
| 50 |
+
|
| 51 |
+
return tf.roll(audio, shift=[beg], axis=[0])
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def time_shift():
|
| 55 |
+
return tf.keras.Sequential([CropAndFill(64, 128)])
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def m_test(ds1, ds2, alpha=0.4):
|
| 59 |
+
call, lab = ds1
|
| 60 |
+
noise, l = ds2
|
| 61 |
+
noise_alpha = alpha * tf.math.reduce_max(noise)
|
| 62 |
+
train_alpha = (1 - alpha) * tf.math.reduce_max(call)
|
| 63 |
+
# tf.print('performing mixup')
|
| 64 |
+
return (call * train_alpha + noise * noise_alpha, lab)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def time_mask(x, y, spec_param=10):
|
| 68 |
+
# tf.print('performing time_mask')
|
| 69 |
+
return (tfio.audio.time_mask(x, param=spec_param), y)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def freq_mask(x, y, spec_param=10):
|
| 73 |
+
# tf.print('performing freq_mask')
|
| 74 |
+
return (tfio.audio.freq_mask(x, param=spec_param), y)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
##############################################################################
|
| 78 |
+
##############################################################################
|
| 79 |
+
##############################################################################
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def run_augment_pipeline(
|
| 83 |
+
ds,
|
| 84 |
+
noise_data,
|
| 85 |
+
noise_set_size,
|
| 86 |
+
train_set_size,
|
| 87 |
+
time_augs,
|
| 88 |
+
mixup_augs,
|
| 89 |
+
seed=None,
|
| 90 |
+
plot=False,
|
| 91 |
+
time_start=None,
|
| 92 |
+
spec_aug=False,
|
| 93 |
+
spec_param=10,
|
| 94 |
+
**kwargs,
|
| 95 |
+
):
|
| 96 |
+
T = time_shift()
|
| 97 |
+
if plot:
|
| 98 |
+
plot_sample_spectrograms(
|
| 99 |
+
ds,
|
| 100 |
+
dir=time_start,
|
| 101 |
+
name="train",
|
| 102 |
+
seed=seed,
|
| 103 |
+
ds_size=train_set_size,
|
| 104 |
+
**kwargs,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
if mixup_augs:
|
| 108 |
+
ds_n = noise_data.repeat(train_set_size // noise_set_size + 1)
|
| 109 |
+
if plot:
|
| 110 |
+
plot_sample_spectrograms(
|
| 111 |
+
ds_n,
|
| 112 |
+
dir=time_start,
|
| 113 |
+
name=f"noise",
|
| 114 |
+
seed=seed,
|
| 115 |
+
ds_size=train_set_size,
|
| 116 |
+
**kwargs,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
if plot:
|
| 120 |
+
dss = tf.data.Dataset.zip((ds, ds_n))
|
| 121 |
+
ds_mu = dss.map(
|
| 122 |
+
lambda x, y: m_test(x, y), num_parallel_calls=AUTOTUNE
|
| 123 |
+
)
|
| 124 |
+
plot_sample_spectrograms(
|
| 125 |
+
ds_mu,
|
| 126 |
+
dir=time_start,
|
| 127 |
+
name=f"augment_0-MixUp",
|
| 128 |
+
seed=seed,
|
| 129 |
+
ds_size=train_set_size,
|
| 130 |
+
**kwargs,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
ds_n = ds_n.shuffle(train_set_size // noise_set_size + 1)
|
| 134 |
+
dss = tf.data.Dataset.zip((ds, ds_n))
|
| 135 |
+
ds_mu = dss.map(lambda x, y: m_test(x, y), num_parallel_calls=AUTOTUNE)
|
| 136 |
+
ds_mu_n = ds_mu.concatenate(noise_data)
|
| 137 |
+
# ds = ds.concatenate(ds_mu_n)
|
| 138 |
+
|
| 139 |
+
if time_augs:
|
| 140 |
+
ds_t = ds.map(
|
| 141 |
+
lambda x, y: (T(x, training=True), y), num_parallel_calls=AUTOTUNE
|
| 142 |
+
)
|
| 143 |
+
if plot:
|
| 144 |
+
plot_sample_spectrograms(
|
| 145 |
+
ds_t,
|
| 146 |
+
dir=time_start,
|
| 147 |
+
name=f"augment_0-TimeShift",
|
| 148 |
+
seed=seed,
|
| 149 |
+
ds_size=train_set_size,
|
| 150 |
+
**kwargs,
|
| 151 |
+
)
|
| 152 |
+
# ds = ds.concatenate(ds_t)
|
| 153 |
+
|
| 154 |
+
if spec_aug:
|
| 155 |
+
ds_tm = ds.map(time_mask)
|
| 156 |
+
# ds = ds.concatenate(ds_tm)
|
| 157 |
+
if plot:
|
| 158 |
+
plot_sample_spectrograms(
|
| 159 |
+
ds_tm,
|
| 160 |
+
dir=time_start,
|
| 161 |
+
name=f"augment_0-TimeMask",
|
| 162 |
+
seed=seed,
|
| 163 |
+
ds_size=train_set_size,
|
| 164 |
+
**kwargs,
|
| 165 |
+
)
|
| 166 |
+
ds_fm = ds.map(freq_mask)
|
| 167 |
+
# ds = ds.concatenate(ds_fm)
|
| 168 |
+
if plot:
|
| 169 |
+
plot_sample_spectrograms(
|
| 170 |
+
ds_fm,
|
| 171 |
+
dir=time_start,
|
| 172 |
+
name=f"augment_0-TFreqMask",
|
| 173 |
+
seed=seed,
|
| 174 |
+
ds_size=train_set_size,
|
| 175 |
+
**kwargs,
|
| 176 |
+
)
|
| 177 |
+
if mixup_augs:
|
| 178 |
+
ds = ds.concatenate(ds_mu_n)
|
| 179 |
+
if time_augs:
|
| 180 |
+
ds = ds.concatenate(ds_t)
|
| 181 |
+
if spec_aug:
|
| 182 |
+
ds = ds.concatenate(ds_tm)
|
| 183 |
+
ds = ds.concatenate(ds_fm)
|
| 184 |
+
|
| 185 |
+
return ds
|
acodet/combine_annotations.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
|
| 3 |
+
pd.options.mode.chained_assignment = None # default='warn'
|
| 4 |
+
import glob
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from acodet.funcs import remove_str_flags_from_predictions, get_files
|
| 7 |
+
import os
|
| 8 |
+
import numpy as np
|
| 9 |
+
import acodet.global_config as conf
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
with open("acodet/annotation_mappers.json", "r") as m:
|
| 13 |
+
mappers = json.load(m)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# TODO aufraeumen
|
| 17 |
+
# annotation_files = Path(r'/mnt/f/Daten/20221019-Benoit/').glob('**/*.txt')
|
| 18 |
+
# annotation_files = Path(r'generated_annotations/2022-11-04_12/').glob('ch*.txt')
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def compensate_for_naming_inconsistencies(hard_drive_path, file):
|
| 22 |
+
split_file = file.stem.split("Table")[0]
|
| 23 |
+
file_path = glob.glob(
|
| 24 |
+
f"{hard_drive_path}/**/{split_file}*wav", recursive=True
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
if not file_path:
|
| 28 |
+
file_tolsta = "336097327." + split_file[6:].replace(
|
| 29 |
+
"_000", ""
|
| 30 |
+
).replace("_", "")
|
| 31 |
+
file_path = glob.glob(
|
| 32 |
+
f"{hard_drive_path}/**/{file_tolsta}*wav", recursive=True
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
if not file_path:
|
| 36 |
+
file_tolsta = "335564853." + split_file[6:].replace(
|
| 37 |
+
"5_000", "4"
|
| 38 |
+
).replace("_", "")
|
| 39 |
+
file_path = glob.glob(
|
| 40 |
+
f"{hard_drive_path}/**/{file_tolsta}*wav", recursive=True
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
if not file_path:
|
| 44 |
+
file_new_annot = file.stem.split("_annot")[0]
|
| 45 |
+
file_path = glob.glob(
|
| 46 |
+
f"{hard_drive_path}/**/{file_new_annot}*", recursive=True
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
if not file_path:
|
| 50 |
+
split_file_underscore = file.stem.split("_")[0]
|
| 51 |
+
file_path = glob.glob(
|
| 52 |
+
f"{hard_drive_path}/**/{file_new_annot}*wav", recursive=True
|
| 53 |
+
)
|
| 54 |
+
if not file_path:
|
| 55 |
+
file_new_annot = split_file_underscore.split(".")[-1]
|
| 56 |
+
file_path = glob.glob(
|
| 57 |
+
f"{hard_drive_path}/**/*{file_new_annot}*wav", recursive=True
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
if not file_path:
|
| 61 |
+
print("sound file could not be found, continuing with next file")
|
| 62 |
+
return False
|
| 63 |
+
return file_path
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def get_corresponding_sound_file(file):
|
| 67 |
+
hard_drive_path = conf.SOUND_FILES_SOURCE
|
| 68 |
+
file_path = glob.glob(
|
| 69 |
+
f"{hard_drive_path}/**/{file.stem}*wav", recursive=True
|
| 70 |
+
)
|
| 71 |
+
if not file_path:
|
| 72 |
+
file_path = compensate_for_naming_inconsistencies(
|
| 73 |
+
hard_drive_path, file
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if not file_path:
|
| 77 |
+
# TODO fehler raisen, dass bringt so einfach nichts
|
| 78 |
+
return "empty"
|
| 79 |
+
|
| 80 |
+
if len(file_path) > 1:
|
| 81 |
+
p_dir = list(file.relative_to(conf.REV_ANNOT_SRC).parents)[-2]
|
| 82 |
+
p_dir_main = str(p_dir).split("_")[0]
|
| 83 |
+
for path in file_path:
|
| 84 |
+
if p_dir_main in path:
|
| 85 |
+
file_path = path
|
| 86 |
+
else:
|
| 87 |
+
file_path = file_path[0]
|
| 88 |
+
|
| 89 |
+
if isinstance(file_path, list) and len(file_path) > 1:
|
| 90 |
+
file_path = file_path[0]
|
| 91 |
+
print(
|
| 92 |
+
"WARNING: Multiple sound files for annotations file found."
|
| 93 |
+
" Because pattern could not be resolved, first file is chosen."
|
| 94 |
+
f"\nannotations file name: \n{file}\n"
|
| 95 |
+
f"sound file name: \n{file_path}\n"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
return file_path
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def seperate_long_annotations(df):
|
| 102 |
+
bool_long_annot = df["End Time (s)"] - df["Begin Time (s)"] > round(
|
| 103 |
+
conf.CONTEXT_WIN / conf.SR
|
| 104 |
+
)
|
| 105 |
+
for i, row in df.loc[bool_long_annot].iterrows():
|
| 106 |
+
n_new_annots = int(
|
| 107 |
+
(row["End Time (s)"] - row["Begin Time (s)"])
|
| 108 |
+
/ (conf.CONTEXT_WIN / conf.SR)
|
| 109 |
+
)
|
| 110 |
+
begins = row["Begin Time (s)"] + np.arange(n_new_annots) * (
|
| 111 |
+
conf.CONTEXT_WIN / conf.SR
|
| 112 |
+
)
|
| 113 |
+
ends = begins + (conf.CONTEXT_WIN / conf.SR)
|
| 114 |
+
n_df = pd.DataFrame()
|
| 115 |
+
for col in row.keys():
|
| 116 |
+
n_df[col] = [row[col]] * n_new_annots
|
| 117 |
+
n_df["Begin Time (s)"] = begins
|
| 118 |
+
n_df["End Time (s)"] = ends
|
| 119 |
+
n_df["Selection"] = np.arange(n_new_annots) + row["Selection"]
|
| 120 |
+
df = pd.concat(
|
| 121 |
+
[df.drop(row.name), n_df]
|
| 122 |
+
) # delete long annotation from df
|
| 123 |
+
return df
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def label_explicit_noise(df):
|
| 127 |
+
df_clean = remove_str_flags_from_predictions(df)
|
| 128 |
+
|
| 129 |
+
expl_noise_crit_idx = np.where(df_clean[conf.ANNOTATION_COLUMN] > 0.9)[0]
|
| 130 |
+
df.loc[expl_noise_crit_idx, "label"] = "explicit 0"
|
| 131 |
+
return df
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def differentiate_label_flags(df, flag=None):
|
| 135 |
+
df.loc[:, conf.ANNOTATION_COLUMN].fillna(value="c", inplace=True)
|
| 136 |
+
df.loc[df[conf.ANNOTATION_COLUMN] == "c", "label"] = 1
|
| 137 |
+
df.loc[df[conf.ANNOTATION_COLUMN] == "n", "label"] = "explicit 0"
|
| 138 |
+
df_std = seperate_long_annotations(df)
|
| 139 |
+
|
| 140 |
+
df_std = df_std.drop(
|
| 141 |
+
df_std.loc[df_std[conf.ANNOTATION_COLUMN] == "u"].index
|
| 142 |
+
)
|
| 143 |
+
df_std.index = pd.RangeIndex(0, len(df_std))
|
| 144 |
+
if flag == "noise":
|
| 145 |
+
df_std = label_explicit_noise(df_std)
|
| 146 |
+
|
| 147 |
+
return df_std
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def get_labels(file, df, active_learning=False, **kwargs):
|
| 151 |
+
if not active_learning:
|
| 152 |
+
df["label"] = 1
|
| 153 |
+
else:
|
| 154 |
+
noise_flag, annotated_flag, calls_flag = [
|
| 155 |
+
"_allnoise",
|
| 156 |
+
"_annotated",
|
| 157 |
+
"_allcalls",
|
| 158 |
+
]
|
| 159 |
+
df = df.iloc[df.Selection.drop_duplicates().index]
|
| 160 |
+
if calls_flag in file.stem:
|
| 161 |
+
df["label"] = 1
|
| 162 |
+
df = differentiate_label_flags(df, flag="calls")
|
| 163 |
+
elif noise_flag in file.stem:
|
| 164 |
+
df["label"] = 0
|
| 165 |
+
df = differentiate_label_flags(df, flag="noise")
|
| 166 |
+
elif annotated_flag in file.stem:
|
| 167 |
+
df_clean = remove_str_flags_from_predictions(df)
|
| 168 |
+
df.loc[df_clean.index, conf.ANNOTATION_COLUMN] = "u"
|
| 169 |
+
df = differentiate_label_flags(df)
|
| 170 |
+
return df
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def standardize(
|
| 174 |
+
df, *, mapper, filename_col="filename", selection_col="Selection"
|
| 175 |
+
):
|
| 176 |
+
keep_cols = ["label", "start", "end", "freq_min", "freq_max"]
|
| 177 |
+
df = df.rename(columns=mapper)
|
| 178 |
+
if not "end" in df.columns:
|
| 179 |
+
df["end"] = df.start + (df["End Time (s)"] - df["Begin Time (s)"])
|
| 180 |
+
out_df = df[keep_cols]
|
| 181 |
+
out_df.index = pd.MultiIndex.from_arrays(
|
| 182 |
+
arrays=(df[filename_col], df[selection_col])
|
| 183 |
+
)
|
| 184 |
+
return out_df.astype(dtype=np.float64)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def filter_out_high_freq_and_high_transient(df):
|
| 188 |
+
df = df.loc[df["High Freq (Hz)"] <= 2000]
|
| 189 |
+
df = df.loc[df["End Time (s)"] - df["Begin Time (s)"] >= 0.4]
|
| 190 |
+
return df
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def finalize_annotation(file, freq_time_crit=False, **kwargs):
|
| 194 |
+
ann = pd.read_csv(file, sep="\t")
|
| 195 |
+
|
| 196 |
+
ann["filename"] = get_corresponding_sound_file(file)
|
| 197 |
+
# if not ann['filename']:
|
| 198 |
+
# print(f'corresponding sound file for annotations file: {file} not found')
|
| 199 |
+
|
| 200 |
+
ann = get_labels(file, ann, **kwargs)
|
| 201 |
+
if "File Offset (s)" in ann.columns:
|
| 202 |
+
mapper = mappers["file_offset_mapper"]
|
| 203 |
+
else:
|
| 204 |
+
mapper = mappers["default_mapper"]
|
| 205 |
+
|
| 206 |
+
if freq_time_crit:
|
| 207 |
+
ann = filter_out_high_freq_and_high_transient(ann)
|
| 208 |
+
|
| 209 |
+
ann_explicit_noise = ann.loc[ann["label"] == "explicit 0", :]
|
| 210 |
+
ann_explicit_noise["label"] = 0
|
| 211 |
+
ann = ann.drop(ann.loc[ann["label"] == "explicit 0"].index)
|
| 212 |
+
std_annot_train = standardize(ann, mapper=mapper)
|
| 213 |
+
std_annot_enoise = standardize(ann_explicit_noise, mapper=mapper)
|
| 214 |
+
|
| 215 |
+
return std_annot_train, std_annot_enoise
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def leading_underscore_in_parent_dirs(file):
|
| 219 |
+
return "_" in [f.stem[0] for f in list(file.parents)[:-1]]
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def get_active_learning_files(files):
|
| 223 |
+
cases = ["_allnoise", "_annotated", "_allcalls"]
|
| 224 |
+
cleaned_files = [f for f in files if [True for c in cases if c in f.stem]]
|
| 225 |
+
drop_cases = ["_tobechecked"]
|
| 226 |
+
final_cleanup = [
|
| 227 |
+
f
|
| 228 |
+
for f in cleaned_files
|
| 229 |
+
if not [True for d in drop_cases if d in f.stem]
|
| 230 |
+
]
|
| 231 |
+
return final_cleanup
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def generate_final_annotations(
|
| 235 |
+
annotation_files=None, active_learning=True, **kwargs
|
| 236 |
+
):
|
| 237 |
+
if not annotation_files:
|
| 238 |
+
annotation_files = get_files(
|
| 239 |
+
location=conf.REV_ANNOT_SRC, search_str="**/*.txt"
|
| 240 |
+
)
|
| 241 |
+
files = list(annotation_files)
|
| 242 |
+
if active_learning:
|
| 243 |
+
files = get_active_learning_files(files)
|
| 244 |
+
folders, counts = np.unique(
|
| 245 |
+
[list(f.relative_to(conf.REV_ANNOT_SRC).parents) for f in files],
|
| 246 |
+
return_counts=True,
|
| 247 |
+
)
|
| 248 |
+
if len(folders) > 1:
|
| 249 |
+
folders, counts = np.unique(
|
| 250 |
+
[
|
| 251 |
+
list(f.relative_to(conf.REV_ANNOT_SRC).parents)[-2]
|
| 252 |
+
for f in files
|
| 253 |
+
],
|
| 254 |
+
return_counts=True,
|
| 255 |
+
)
|
| 256 |
+
files.sort()
|
| 257 |
+
ind = 0
|
| 258 |
+
for i, folder in enumerate(folders):
|
| 259 |
+
df_t, df_n = pd.DataFrame(), pd.DataFrame()
|
| 260 |
+
for _ in range(counts[i]):
|
| 261 |
+
if leading_underscore_in_parent_dirs(files[ind]):
|
| 262 |
+
print(
|
| 263 |
+
files[ind],
|
| 264 |
+
" skipped due to leading underscore in parent dir.",
|
| 265 |
+
)
|
| 266 |
+
ind += 1
|
| 267 |
+
continue
|
| 268 |
+
df_train, df_enoise = finalize_annotation(
|
| 269 |
+
files[ind],
|
| 270 |
+
all_noise=False,
|
| 271 |
+
active_learning=active_learning,
|
| 272 |
+
**kwargs,
|
| 273 |
+
)
|
| 274 |
+
df_t = pd.concat([df_t, df_train])
|
| 275 |
+
df_n = pd.concat([df_n, df_enoise])
|
| 276 |
+
print(f"Completed file {ind}/{len(files)}.", end="\r")
|
| 277 |
+
ind += 1
|
| 278 |
+
|
| 279 |
+
# TODO include date in path by default
|
| 280 |
+
save_dir = Path(conf.ANNOT_DEST).joinpath(folder)
|
| 281 |
+
save_dir.mkdir(exist_ok=True, parents=True)
|
| 282 |
+
df_t.to_csv(save_dir.joinpath("combined_annotations.csv"))
|
| 283 |
+
df_n.to_csv(save_dir.joinpath("explicit_noise.csv"))
|
| 284 |
+
# save_ket_annot_only_existing_paths(df)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
if __name__ == "__main__":
|
| 288 |
+
annotation_files = list(Path(conf.REV_ANNOT_SRC).glob("**/*.txt"))
|
| 289 |
+
if len(annotation_files) == 0:
|
| 290 |
+
annotation_files = list(Path(conf.REV_ANNOT_SRC).glob("*.txt"))
|
| 291 |
+
generate_final_annotations(
|
| 292 |
+
annotation_files, active_learning=True, freq_time_crit=False
|
| 293 |
+
)
|
acodet/create_session_file.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
import json
|
| 3 |
+
import streamlit as st
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def create_session_file():
|
| 7 |
+
with open("simple_config.yml", "r") as f:
|
| 8 |
+
simple = yaml.safe_load(f)
|
| 9 |
+
|
| 10 |
+
with open("advanced_config.yml", "r") as f:
|
| 11 |
+
advanced = yaml.safe_load(f)
|
| 12 |
+
|
| 13 |
+
session = {**simple, **advanced}
|
| 14 |
+
|
| 15 |
+
if "session_started" in st.session_state:
|
| 16 |
+
for k, v in session.items():
|
| 17 |
+
setattr(st.session_state, k, v)
|
| 18 |
+
else:
|
| 19 |
+
with open("acodet/src/tmp_session.json", "w") as f:
|
| 20 |
+
json.dump(session, f)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def read_session_file():
|
| 24 |
+
if "session_started" in st.session_state:
|
| 25 |
+
session = {**st.session_state}
|
| 26 |
+
else:
|
| 27 |
+
with open("acodet/src/tmp_session.json", "r") as f:
|
| 28 |
+
session = json.load(f)
|
| 29 |
+
return session
|
acodet/evaluate.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from acodet.plot_utils import (
|
| 2 |
+
plot_evaluation_metric,
|
| 3 |
+
plot_model_results,
|
| 4 |
+
plot_sample_spectrograms,
|
| 5 |
+
plot_pr_curve,
|
| 6 |
+
)
|
| 7 |
+
from acodet import models
|
| 8 |
+
from acodet.models import get_labels_and_preds
|
| 9 |
+
from acodet.tfrec import run_data_pipeline, spec
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
import matplotlib.colors as mcolors
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import time
|
| 15 |
+
import numpy as np
|
| 16 |
+
from acodet.humpback_model_dir import front_end
|
| 17 |
+
import acodet.global_config as conf
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_info(date):
|
| 21 |
+
keys = [
|
| 22 |
+
"data_path",
|
| 23 |
+
"batch_size",
|
| 24 |
+
"epochs",
|
| 25 |
+
"Model",
|
| 26 |
+
"keras_mod_name",
|
| 27 |
+
"load_weights",
|
| 28 |
+
"training_date",
|
| 29 |
+
"steps_per_epoch",
|
| 30 |
+
"f_score_beta",
|
| 31 |
+
"f_score_thresh",
|
| 32 |
+
"bool_SpecAug",
|
| 33 |
+
"bool_time_shift",
|
| 34 |
+
"bool_MixUps",
|
| 35 |
+
"weight_clipping",
|
| 36 |
+
"init_lr",
|
| 37 |
+
"final_lr",
|
| 38 |
+
"unfreezes",
|
| 39 |
+
"preproc blocks",
|
| 40 |
+
]
|
| 41 |
+
path = Path(f"../trainings/{date}")
|
| 42 |
+
f = pd.read_csv(path.joinpath("training_info.txt"), sep="\t")
|
| 43 |
+
l, found = [], 0
|
| 44 |
+
for key in keys:
|
| 45 |
+
found = 0
|
| 46 |
+
for s in f.values:
|
| 47 |
+
if key in s[0]:
|
| 48 |
+
l.append(s[0])
|
| 49 |
+
found = 1
|
| 50 |
+
if found == 0:
|
| 51 |
+
l.append(f"{key}= nan")
|
| 52 |
+
return {key: s.split("= ")[-1] for s, key in zip(l, keys)}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def write_trainings_csv():
|
| 56 |
+
trains = list(Path("../trainings").iterdir())
|
| 57 |
+
try:
|
| 58 |
+
df = pd.read_csv("../trainings/20221124_meta_trainings.csv")
|
| 59 |
+
new = [t for t in trains if t.stem not in df["training_date"].values]
|
| 60 |
+
i = len(trains) - len(new) + 1
|
| 61 |
+
trains = new
|
| 62 |
+
except Exception as e:
|
| 63 |
+
print("file not found", e)
|
| 64 |
+
df = pd.DataFrame()
|
| 65 |
+
i = 0
|
| 66 |
+
for path in trains:
|
| 67 |
+
try:
|
| 68 |
+
f = pd.read_csv(path.joinpath("training_info.txt"), sep="\t")
|
| 69 |
+
for s in f.values:
|
| 70 |
+
if "=" in s[0]:
|
| 71 |
+
df.loc[i, s[0].split("= ")[0].replace(" ", "")] = s[
|
| 72 |
+
0
|
| 73 |
+
].split("= ")[-1]
|
| 74 |
+
df.loc[i, "training_date"] = path.stem
|
| 75 |
+
i += 1
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(e)
|
| 78 |
+
df.to_csv("../trainings/20230207_meta_trainings.csv")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def create_overview_plot(
|
| 82 |
+
train_dates=[],
|
| 83 |
+
val_set=None,
|
| 84 |
+
display_keys=["Model"],
|
| 85 |
+
plot_metrics=False,
|
| 86 |
+
titles=None,
|
| 87 |
+
):
|
| 88 |
+
if not train_dates:
|
| 89 |
+
train_dates = "2022-11-30_01"
|
| 90 |
+
if not isinstance(train_dates, list):
|
| 91 |
+
train_dates = [train_dates]
|
| 92 |
+
|
| 93 |
+
df = pd.read_csv("../trainings/20230207_meta_trainings.csv")
|
| 94 |
+
df.index = df["training_date"]
|
| 95 |
+
|
| 96 |
+
if not val_set:
|
| 97 |
+
val_set = list(Path(conf.TFREC_DESTINATION).iterdir())
|
| 98 |
+
if "dataset_meta_train" in [f.stem for f in val_set]:
|
| 99 |
+
val_set = val_set[0].parent
|
| 100 |
+
|
| 101 |
+
if isinstance(val_set, list):
|
| 102 |
+
val_label = "all"
|
| 103 |
+
else:
|
| 104 |
+
val_label = Path(val_set).stem
|
| 105 |
+
|
| 106 |
+
string = str("Model:{}; " f"val: {val_label}")
|
| 107 |
+
if conf.THRESH != 0.5:
|
| 108 |
+
string += f" thr: {conf.THRESH}"
|
| 109 |
+
|
| 110 |
+
if not train_dates:
|
| 111 |
+
labels = None
|
| 112 |
+
else:
|
| 113 |
+
labels = [
|
| 114 |
+
string.format(
|
| 115 |
+
*df.loc[df["training_date"] == d, display_keys].values[0]
|
| 116 |
+
)
|
| 117 |
+
for d in train_dates
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
training_runs = []
|
| 121 |
+
for i, train in enumerate(train_dates):
|
| 122 |
+
training_runs += [Path(f"../trainings/{train}")]
|
| 123 |
+
for _ in range(
|
| 124 |
+
len(list(Path(f"../trainings/{train}").glob("unfreeze*")))
|
| 125 |
+
):
|
| 126 |
+
labels += labels[i]
|
| 127 |
+
val_data = run_data_pipeline(
|
| 128 |
+
val_set, "val", return_spec=False, return_meta=True
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
model_name = [
|
| 132 |
+
df.loc[df["training_date"] == d, "Model"].values[0]
|
| 133 |
+
for d in train_dates
|
| 134 |
+
]
|
| 135 |
+
keras_mod_name = [
|
| 136 |
+
df.loc[df["training_date"] == d, "keras_mod_name"].values[0]
|
| 137 |
+
for d in train_dates
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
time_start = time.strftime("%Y%m%d_%H%M%S", time.gmtime())
|
| 141 |
+
|
| 142 |
+
if plot_metrics:
|
| 143 |
+
fig = plt.figure(constrained_layout=True, figsize=(6, 6))
|
| 144 |
+
subfigs = fig.subfigures(2, 1) # , wspace=0.07, width_ratios=[1, 1])
|
| 145 |
+
plot_model_results(
|
| 146 |
+
train_dates, labels, fig=subfigs[0], legend=False
|
| 147 |
+
) # , **info_dict)
|
| 148 |
+
eval_fig = subfigs[1]
|
| 149 |
+
else:
|
| 150 |
+
fig = plt.figure(constrained_layout=True, figsize=(5, 5))
|
| 151 |
+
eval_fig = fig
|
| 152 |
+
display_keys = ["keras_mod_name"]
|
| 153 |
+
table_df = df.loc[train_dates, display_keys]
|
| 154 |
+
if not len(table_df) == 0:
|
| 155 |
+
table_df.iloc[-1] = "GoogleModel"
|
| 156 |
+
plot_evaluation_metric(
|
| 157 |
+
model_name,
|
| 158 |
+
training_runs,
|
| 159 |
+
val_data,
|
| 160 |
+
plot_labels=labels,
|
| 161 |
+
fig=eval_fig,
|
| 162 |
+
plot_pr=True,
|
| 163 |
+
plot_cm=False,
|
| 164 |
+
titles=titles,
|
| 165 |
+
train_dates=train_dates,
|
| 166 |
+
label=None,
|
| 167 |
+
legend=False,
|
| 168 |
+
keras_mod_name=keras_mod_name,
|
| 169 |
+
)
|
| 170 |
+
fig.savefig(
|
| 171 |
+
f"../trainings/2022-11-30_01/{time_start}_results_combo.png", dpi=150
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def create_incorrect_prd_plot(
|
| 176 |
+
model_instance, train_date, val_data_path, **kwargs
|
| 177 |
+
):
|
| 178 |
+
training_run = Path(f"../trainings/{train_date}").glob("unfreeze*")
|
| 179 |
+
val_data = run_data_pipeline(val_data_path, "val", return_spec=False)
|
| 180 |
+
labels, preds = get_labels_and_preds(
|
| 181 |
+
model_instance, training_run, val_data, **kwargs
|
| 182 |
+
)
|
| 183 |
+
preds = preds.reshape([len(preds)])
|
| 184 |
+
bin_preds = list(map(lambda x: 1 if x >= conf.THRESH else 0, preds))
|
| 185 |
+
false_pos, false_neg = [], []
|
| 186 |
+
for i in range(len(preds)):
|
| 187 |
+
if bin_preds[i] == 0 and labels[i] == 1:
|
| 188 |
+
false_neg.append(i)
|
| 189 |
+
if bin_preds[i] == 1 and labels[i] == 0:
|
| 190 |
+
false_pos.append(i)
|
| 191 |
+
|
| 192 |
+
offset = min([false_neg[0], false_pos[0]])
|
| 193 |
+
val_data = run_data_pipeline(
|
| 194 |
+
val_data_path, "val", return_spec=False, return_meta=True
|
| 195 |
+
)
|
| 196 |
+
val_data = val_data.batch(1)
|
| 197 |
+
val_data = val_data.map(lambda x, y, z, w: (spec()(x), y, z, w))
|
| 198 |
+
val_data = val_data.unbatch()
|
| 199 |
+
data = list(val_data.skip(offset))
|
| 200 |
+
fp = [data[i - offset] for i in false_pos]
|
| 201 |
+
fn = [data[i - offset] for i in false_neg]
|
| 202 |
+
plot_sample_spectrograms(
|
| 203 |
+
fn, dir=train_date, name=f"False_Negative", plot_meta=True, **kwargs
|
| 204 |
+
)
|
| 205 |
+
plot_sample_spectrograms(
|
| 206 |
+
fp, dir=train_date, name=f"False_Positive", plot_meta=True, **kwargs
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def create_table_plot():
|
| 211 |
+
time_start = time.strftime("%Y%m%d_%H%M%S", time.gmtime())
|
| 212 |
+
df = pd.read_csv("../trainings/20221124_meta_trainings.csv")
|
| 213 |
+
df.index = df["training_date"]
|
| 214 |
+
display_keys = ["keras_mod_name"]
|
| 215 |
+
col_labels = ["model name"]
|
| 216 |
+
table_df = df.loc[train_dates, display_keys]
|
| 217 |
+
table_df.iloc[-1] = "GoogleModel"
|
| 218 |
+
f, ax_tb = plt.subplots()
|
| 219 |
+
bbox = [0, 0, 1, 1]
|
| 220 |
+
ax_tb.axis("off")
|
| 221 |
+
font_size = 20
|
| 222 |
+
import seaborn as sns
|
| 223 |
+
|
| 224 |
+
color = list(sns.color_palette())
|
| 225 |
+
mpl_table = ax_tb.table(
|
| 226 |
+
cellText=table_df.values,
|
| 227 |
+
rowLabels=[" "] * len(table_df),
|
| 228 |
+
bbox=bbox,
|
| 229 |
+
colLabels=col_labels,
|
| 230 |
+
rowColours=color,
|
| 231 |
+
)
|
| 232 |
+
mpl_table.auto_set_font_size(False)
|
| 233 |
+
mpl_table.set_fontsize(font_size)
|
| 234 |
+
f.tight_layout()
|
| 235 |
+
f.savefig(
|
| 236 |
+
f"../trainings/{train_dates[-1]}/{time_start}_results_table.png",
|
| 237 |
+
dpi=150,
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
if __name__ == "__main__":
|
| 242 |
+
tfrec_path = list(Path(conf.TFREC_DESTINATION).iterdir())
|
| 243 |
+
train_dates = ["2022-11-30_01"]
|
| 244 |
+
|
| 245 |
+
display_keys = ["Model", "keras_mod_name", "epochs", "init_lr", "final_lr"]
|
| 246 |
+
|
| 247 |
+
create_overview_plot(
|
| 248 |
+
train_dates,
|
| 249 |
+
tfrec_path,
|
| 250 |
+
display_keys,
|
| 251 |
+
plot_metrics=False,
|
| 252 |
+
titles=["all_data"],
|
| 253 |
+
)
|
acodet/front_end/help_strings.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
RUN_OPTION = (
|
| 2 |
+
"Choose what option to run (currently only Inference is supported)."
|
| 3 |
+
)
|
| 4 |
+
SELECT_PRESET = "Based on the preset, different computations will run."
|
| 5 |
+
SAMPLE_RATE = """
|
| 6 |
+
If you need to change this, make sure that the sample rate
|
| 7 |
+
you set is below the sample rate of your audio files
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
ENTER_PATH = """
|
| 11 |
+
Enter the path to the directory containing the dataset(s) (so one above it).
|
| 12 |
+
The dropdown below will then allow you to choose the dataset(s) you would like to annotate on.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
CHOOSE_FOLDER = """
|
| 16 |
+
Choose the folder containing the dataset(s) you would like to annotate on.
|
| 17 |
+
"""
|
| 18 |
+
THRESHOLD = "The threshold will be used to filter the predictions."
|
| 19 |
+
ANNOTATIONS_DEFAULT_LOCATION = """
|
| 20 |
+
The annotations are stored in this folder by default. If you want to specify another location, choose "No".
|
| 21 |
+
"""
|
| 22 |
+
ANNOTATIONS_TIMESTAMP_FOLDER = """
|
| 23 |
+
Specify custom string to append to timestamp for foldername.\n
|
| 24 |
+
Example: 2024-01-27_22-24-23___Custom-Dataset-1"""
|
| 25 |
+
ANNOTATIONS_TIMESTAMP_RADIO = """
|
| 26 |
+
By default the annotations folder is named according to the timestamp when it was created.
|
| 27 |
+
By clicking Yes you can add a custom string to make it more specific.
|
| 28 |
+
"""
|
| 29 |
+
CHOOSE_TIMESTAMP_FOLDER = """
|
| 30 |
+
These are the time stamps corresponding to computations that
|
| 31 |
+
have been performed on the machine previously. They all contain annotations files
|
| 32 |
+
and can be used to filter the annotations with different thresholds or to generate
|
| 33 |
+
hourly predictions.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
MULTI_DATA = """
|
| 37 |
+
Are there multiple datasets located in the selected folder and would you
|
| 38 |
+
like for all of them to be processed? If so select yes, if not, please only
|
| 39 |
+
select the desired folder.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
SAVE_SELECTION_BTN = """
|
| 43 |
+
By clicking, the selection tables of the chosen datasets will be
|
| 44 |
+
recomputed with the limit settings chosen above and saved in the same location
|
| 45 |
+
with a name corresponding to the limit name and threshold."""
|
| 46 |
+
|
| 47 |
+
LIMIT = """
|
| 48 |
+
Choose between Simple and Sequence limit. Simple limit will only count the
|
| 49 |
+
annotations that are above the threshold. Sequence limit will only count the
|
| 50 |
+
annotations that are above the threshold and exceed the limit within 20
|
| 51 |
+
consecutive sections.
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
ANNOT_FILES_DROPDOWN = """
|
| 55 |
+
Choose the annotation file you would like to inspect.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
SC_LIMIT = """
|
| 59 |
+
The limit will be used to filter the predictions. The limit is the number of
|
| 60 |
+
vocalizations within 20 consecutive sections that need to exceed the threshold.
|
| 61 |
+
Higher limits mean less false positives but more false negatives.
|
| 62 |
+
Play around withit and see how the plot changes.
|
| 63 |
+
The idea behind this is to be able to tune the sensitivity of the model
|
| 64 |
+
to the noise environment within the dataset.
|
| 65 |
+
"""
|
acodet/front_end/st_annotate.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from acodet.front_end import utils
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from acodet import create_session_file
|
| 5 |
+
from acodet.front_end import help_strings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
session_config = create_session_file.read_session_file()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def initial_dropdown(key):
|
| 12 |
+
"""
|
| 13 |
+
Show dropdown menu to select what preset to run.
|
| 14 |
+
|
| 15 |
+
Parameters
|
| 16 |
+
----------
|
| 17 |
+
key : string
|
| 18 |
+
unique identifier for streamlit objects
|
| 19 |
+
|
| 20 |
+
Returns
|
| 21 |
+
-------
|
| 22 |
+
int
|
| 23 |
+
preset number
|
| 24 |
+
"""
|
| 25 |
+
return int(
|
| 26 |
+
st.selectbox(
|
| 27 |
+
"What predefined Settings would you like to run?",
|
| 28 |
+
(
|
| 29 |
+
"1 - generate new annotations",
|
| 30 |
+
"2 - filter existing annotations with different threshold",
|
| 31 |
+
"3 - generate hourly predictions",
|
| 32 |
+
"0 - all of the above",
|
| 33 |
+
),
|
| 34 |
+
key=key,
|
| 35 |
+
help=help_strings.SELECT_PRESET,
|
| 36 |
+
)[0]
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class PresetInterfaceSettings:
|
| 41 |
+
def __init__(self, config, key) -> None:
|
| 42 |
+
"""
|
| 43 |
+
Preset settings class. All methods are relevant for displaying
|
| 44 |
+
streamlit objects that will then be used to run the computations.
|
| 45 |
+
|
| 46 |
+
Parameters
|
| 47 |
+
----------
|
| 48 |
+
config : dict
|
| 49 |
+
config dictionsary
|
| 50 |
+
key : string
|
| 51 |
+
unique identifier
|
| 52 |
+
"""
|
| 53 |
+
self.config = config
|
| 54 |
+
self.key = key
|
| 55 |
+
|
| 56 |
+
def custom_timestamp_dialog(self):
|
| 57 |
+
"""
|
| 58 |
+
Show radio buttons asking for custom folder names. If Yes is selected
|
| 59 |
+
allow user input that will be appended to timestamp for uses to give
|
| 60 |
+
custom names to annotation sessions.
|
| 61 |
+
"""
|
| 62 |
+
timestamp_radio = st.radio(
|
| 63 |
+
f"Would you like to customize the annotaitons folder?",
|
| 64 |
+
("No", "Yes"),
|
| 65 |
+
key="radio_" + self.key,
|
| 66 |
+
horizontal=True,
|
| 67 |
+
help=help_strings.ANNOTATIONS_TIMESTAMP_RADIO,
|
| 68 |
+
)
|
| 69 |
+
if timestamp_radio == "Yes":
|
| 70 |
+
self.config["annots_timestamp_folder"] = "___" + utils.user_input(
|
| 71 |
+
"Custom Folder string:",
|
| 72 |
+
"",
|
| 73 |
+
help=help_strings.ANNOTATIONS_TIMESTAMP_FOLDER,
|
| 74 |
+
)
|
| 75 |
+
elif timestamp_radio == "No":
|
| 76 |
+
self.config["annots_timestamp_folder"] = ""
|
| 77 |
+
|
| 78 |
+
def ask_to_continue_incomplete_inference(self):
|
| 79 |
+
continue_radio = st.radio(
|
| 80 |
+
f"Would you like to continue a cancelled session?",
|
| 81 |
+
("No", "Yes"),
|
| 82 |
+
key="radio_continue_session_" + self.key,
|
| 83 |
+
horizontal=True,
|
| 84 |
+
help=help_strings.ANNOTATIONS_TIMESTAMP_RADIO,
|
| 85 |
+
)
|
| 86 |
+
if continue_radio == "Yes":
|
| 87 |
+
past_sessions = list(
|
| 88 |
+
Path(session_config["generated_annotations_folder"]).rglob(
|
| 89 |
+
Path(self.config["sound_files_source"]).stem
|
| 90 |
+
)
|
| 91 |
+
)
|
| 92 |
+
if len(past_sessions) == 0:
|
| 93 |
+
st.text(
|
| 94 |
+
f"""Sorry, but no annotations have been found in
|
| 95 |
+
`{session_config['generated_annotations_folder']}` on the currently
|
| 96 |
+
selected dataset (`{self.config['sound_files_source']}`)."""
|
| 97 |
+
)
|
| 98 |
+
else:
|
| 99 |
+
previous_session = st.selectbox(
|
| 100 |
+
"Which previous session would you like to continue?",
|
| 101 |
+
past_sessions,
|
| 102 |
+
key="continue_session_" + self.key,
|
| 103 |
+
help=help_strings.SELECT_PRESET,
|
| 104 |
+
)
|
| 105 |
+
self.config["timestamp_folder"] = previous_session
|
| 106 |
+
else:
|
| 107 |
+
return True
|
| 108 |
+
|
| 109 |
+
def perform_inference(self):
|
| 110 |
+
"""
|
| 111 |
+
Interface options when inference is selected, i.e. preset options 0 or 1.
|
| 112 |
+
"""
|
| 113 |
+
path = st.text_input(
|
| 114 |
+
"Enter the path to your sound data:",
|
| 115 |
+
"tests/test_files",
|
| 116 |
+
help=help_strings.ENTER_PATH,
|
| 117 |
+
)
|
| 118 |
+
self.config["sound_files_source"] = utils.open_folder_dialogue(
|
| 119 |
+
path, key="folder_" + self.key, help=help_strings.CHOOSE_FOLDER
|
| 120 |
+
)
|
| 121 |
+
self.config["thresh"] = utils.validate_float(
|
| 122 |
+
utils.user_input(
|
| 123 |
+
"Model threshold:", "0.9", help=help_strings.THRESHOLD
|
| 124 |
+
)
|
| 125 |
+
)
|
| 126 |
+
self.advanced_settings()
|
| 127 |
+
|
| 128 |
+
def advanced_settings(self):
|
| 129 |
+
"""
|
| 130 |
+
Expandable section showing advanced settings options.
|
| 131 |
+
"""
|
| 132 |
+
with st.expander(r"**Advanced Settings**"):
|
| 133 |
+
continue_session = self.ask_to_continue_incomplete_inference()
|
| 134 |
+
|
| 135 |
+
if continue_session:
|
| 136 |
+
self.custom_timestamp_dialog()
|
| 137 |
+
|
| 138 |
+
self.ask_for_multiple_datasets()
|
| 139 |
+
|
| 140 |
+
def ask_for_multiple_datasets(self):
|
| 141 |
+
multiple_datasets = st.radio(
|
| 142 |
+
"Would you like to process multiple datasets in this session?",
|
| 143 |
+
("No", "Yes"),
|
| 144 |
+
key=f"multi_datasets_{self.key}",
|
| 145 |
+
horizontal=True,
|
| 146 |
+
help=help_strings.MULTI_DATA,
|
| 147 |
+
)
|
| 148 |
+
if multiple_datasets == "Yes":
|
| 149 |
+
self.config["multi_datasets"] = True
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def rerun_annotations(self):
|
| 153 |
+
"""
|
| 154 |
+
Show options for rerunning annotations and saving the
|
| 155 |
+
selection tables with a different limit.
|
| 156 |
+
"""
|
| 157 |
+
self.select_annotation_source_directory()
|
| 158 |
+
self.limit = st.radio(
|
| 159 |
+
"What limit would you like to set?",
|
| 160 |
+
("Simple limit", "Sequence limit"),
|
| 161 |
+
key=f"limit_selec_{self.key}",
|
| 162 |
+
help=help_strings.LIMIT,
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
self.lim_obj = utils.Limits(self.limit, self.key)
|
| 166 |
+
self.lim_obj.create_limit_sliders()
|
| 167 |
+
self.lim_obj.save_selection_tables_with_limit_settings()
|
| 168 |
+
|
| 169 |
+
def select_annotation_source_directory(self):
|
| 170 |
+
"""
|
| 171 |
+
Streamlit objects for preset options 2 and 3.
|
| 172 |
+
"""
|
| 173 |
+
default_path = st.radio(
|
| 174 |
+
f"""The annotations I would like to filter are located in
|
| 175 |
+
`{Path(session_config['generated_annotations_folder']).resolve()}`:""",
|
| 176 |
+
("Yes", "No"),
|
| 177 |
+
key="radio_" + self.key,
|
| 178 |
+
horizontal=True,
|
| 179 |
+
help=help_strings.ANNOTATIONS_DEFAULT_LOCATION,
|
| 180 |
+
)
|
| 181 |
+
if default_path == "Yes":
|
| 182 |
+
self.config[
|
| 183 |
+
"generated_annotation_source"
|
| 184 |
+
] = utils.open_folder_dialogue(
|
| 185 |
+
key="folder_default_" + self.key,
|
| 186 |
+
label="From the timestamps folders, choose the one you would like to work on.",
|
| 187 |
+
help=help_strings.CHOOSE_TIMESTAMP_FOLDER,
|
| 188 |
+
filter_existing_annotations=True,
|
| 189 |
+
)
|
| 190 |
+
elif default_path == "No":
|
| 191 |
+
path = st.text_input(
|
| 192 |
+
"Enter the path to your annotation data:",
|
| 193 |
+
"tests/test_files",
|
| 194 |
+
help=help_strings.ENTER_PATH,
|
| 195 |
+
)
|
| 196 |
+
self.config[
|
| 197 |
+
"generated_annotation_source"
|
| 198 |
+
] = utils.open_folder_dialogue(
|
| 199 |
+
path,
|
| 200 |
+
key="folder_" + self.key,
|
| 201 |
+
label="From the timestamps folders, choose the one you would like to work on.",
|
| 202 |
+
help=help_strings.CHOOSE_TIMESTAMP_FOLDER,
|
| 203 |
+
filter_existing_annotations=True,
|
| 204 |
+
)
|
| 205 |
+
if (
|
| 206 |
+
Path(self.config["generated_annotation_source"]).stem
|
| 207 |
+
+ Path(self.config["generated_annotation_source"]).suffix
|
| 208 |
+
== f"thresh_{session_config['default_threshold']}"
|
| 209 |
+
):
|
| 210 |
+
st.write(
|
| 211 |
+
"""
|
| 212 |
+
<p style="color:red; font-size:14px;">
|
| 213 |
+
Please choose the top-level folder (usually a timestamp) instead.
|
| 214 |
+
</p>""",
|
| 215 |
+
unsafe_allow_html=True,
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def annotate_options(key="annot"):
|
| 220 |
+
"""
|
| 221 |
+
Caller function for inference settings. Calls all necessary components
|
| 222 |
+
to show streamlit objects where users can choose what settings to run.
|
| 223 |
+
|
| 224 |
+
Parameters
|
| 225 |
+
----------
|
| 226 |
+
key : str, optional
|
| 227 |
+
unique identifier for streamlit objects, by default "annot"
|
| 228 |
+
|
| 229 |
+
Returns
|
| 230 |
+
-------
|
| 231 |
+
boolean
|
| 232 |
+
True once all settings have been entered
|
| 233 |
+
"""
|
| 234 |
+
preset_option = initial_dropdown(key)
|
| 235 |
+
|
| 236 |
+
st.session_state.preset_option = preset_option
|
| 237 |
+
utils.make_nested_btn_false_if_dropdown_changed(1, preset_option, 1)
|
| 238 |
+
utils.make_nested_btn_false_if_dropdown_changed(
|
| 239 |
+
run_id=1, preset_id=preset_option, btn_id=4
|
| 240 |
+
)
|
| 241 |
+
utils.next_button(id=1)
|
| 242 |
+
|
| 243 |
+
if not st.session_state.b1:
|
| 244 |
+
pass
|
| 245 |
+
else:
|
| 246 |
+
config = dict()
|
| 247 |
+
config["predefined_settings"] = preset_option
|
| 248 |
+
interface_settings = PresetInterfaceSettings(config, key)
|
| 249 |
+
|
| 250 |
+
if preset_option == 1 or preset_option == 0:
|
| 251 |
+
interface_settings.perform_inference()
|
| 252 |
+
|
| 253 |
+
elif preset_option == 2:
|
| 254 |
+
interface_settings.rerun_annotations()
|
| 255 |
+
|
| 256 |
+
elif preset_option == 3:
|
| 257 |
+
interface_settings.select_annotation_source_directory()
|
| 258 |
+
interface_settings.ask_for_multiple_datasets()
|
| 259 |
+
|
| 260 |
+
for k, v in interface_settings.config.items():
|
| 261 |
+
utils.write_to_session_file(k, v)
|
| 262 |
+
return True
|
acodet/front_end/st_generate_data.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from acodet.front_end import utils
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def generate_data_options(key="gen_data"):
|
| 6 |
+
preset_option = int(
|
| 7 |
+
st.selectbox(
|
| 8 |
+
"How would you like run the program?",
|
| 9 |
+
(
|
| 10 |
+
"1 - generate new training data from reviewed annotations",
|
| 11 |
+
"2 - generate new training data from reviewed annotations "
|
| 12 |
+
"and fill space between annotations with noise annotations",
|
| 13 |
+
),
|
| 14 |
+
key=key,
|
| 15 |
+
)[0]
|
| 16 |
+
)
|
| 17 |
+
st.session_state.preset_option = preset_option
|
| 18 |
+
utils.make_nested_btn_false_if_dropdown_changed(2, preset_option, 2)
|
| 19 |
+
utils.make_nested_btn_false_if_dropdown_changed(
|
| 20 |
+
run_id=1, preset_id=preset_option, btn_id=4
|
| 21 |
+
)
|
| 22 |
+
utils.next_button(id=2)
|
| 23 |
+
if not st.session_state.b2:
|
| 24 |
+
pass
|
| 25 |
+
else:
|
| 26 |
+
config = dict()
|
| 27 |
+
config["predefined_settings"] = preset_option
|
| 28 |
+
|
| 29 |
+
if preset_option == 1:
|
| 30 |
+
path1_sound = st.text_input(
|
| 31 |
+
"Enter the path to your sound data:", "."
|
| 32 |
+
)
|
| 33 |
+
config["sound_files_source"] = utils.open_folder_dialogue(
|
| 34 |
+
path1_sound, key="source_folder_" + key
|
| 35 |
+
)
|
| 36 |
+
path2_annots = st.text_input(
|
| 37 |
+
"Enter the path to your reviewed annotations:", "."
|
| 38 |
+
)
|
| 39 |
+
config["reviewed_annotation_source"] = utils.open_folder_dialogue(
|
| 40 |
+
path2_annots, key="reviewed_annotations_folder" + key
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
st.markdown("### Settings")
|
| 44 |
+
st.markdown("#### Audio preprocessing")
|
| 45 |
+
config["sample_rate"] = utils.validate_int(
|
| 46 |
+
utils.user_input("sample rate [Hz]", "2000")
|
| 47 |
+
)
|
| 48 |
+
config["context_window_in_seconds"] = utils.validate_float(
|
| 49 |
+
utils.user_input("context window length [s]", "3.9")
|
| 50 |
+
)
|
| 51 |
+
st.markdown("#### Spectrogram settings")
|
| 52 |
+
config["stft_frame_len"] = utils.validate_int(
|
| 53 |
+
utils.user_input("STFT frame length [samples]", "1024")
|
| 54 |
+
)
|
| 55 |
+
config["number_of_time_bins"] = utils.validate_int(
|
| 56 |
+
utils.user_input("number of time bins", "128")
|
| 57 |
+
)
|
| 58 |
+
st.markdown("#### TFRecord creationg settings")
|
| 59 |
+
config["tfrecs_limit_per_file"] = utils.validate_int(
|
| 60 |
+
utils.user_input(
|
| 61 |
+
"limit of context windows per tfrecord file", "600"
|
| 62 |
+
)
|
| 63 |
+
)
|
| 64 |
+
config["train_ratio"] = utils.validate_float(
|
| 65 |
+
utils.user_input("trainratio", "0.7")
|
| 66 |
+
)
|
| 67 |
+
config["test_val_ratio"] = utils.validate_float(
|
| 68 |
+
utils.user_input("test validation ratio", "0.7")
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
elif preset_option == 2:
|
| 72 |
+
path1_sound = st.text_input(
|
| 73 |
+
"Enter the path to your sound data:", "."
|
| 74 |
+
)
|
| 75 |
+
config["sound_files_source"] = utils.open_folder_dialogue(
|
| 76 |
+
path1_sound, key="source_folder_" + key
|
| 77 |
+
)
|
| 78 |
+
path2_annots = st.text_input(
|
| 79 |
+
"Enter the path to your reviewed annotations:", "."
|
| 80 |
+
)
|
| 81 |
+
config["reviewed_annotation_source"] = utils.open_folder_dialogue(
|
| 82 |
+
path2_annots, key="reviewed_annotations_folder" + key
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
st.markdown("### Settings")
|
| 86 |
+
st.markdown("#### Audio preprocessing")
|
| 87 |
+
config["sample_rate"] = utils.validate_int(
|
| 88 |
+
utils.user_input("sample rate [Hz]", "2000")
|
| 89 |
+
)
|
| 90 |
+
config["context_window_in_seconds"] = utils.validate_float(
|
| 91 |
+
utils.user_input("context window length [s]", "3.9")
|
| 92 |
+
)
|
| 93 |
+
st.markdown("#### Spectrogram settings")
|
| 94 |
+
config["stft_frame_len"] = utils.validate_int(
|
| 95 |
+
utils.user_input("STFT frame length [samples]", "1024")
|
| 96 |
+
)
|
| 97 |
+
config["number_of_time_bins"] = utils.validate_int(
|
| 98 |
+
utils.user_input("number of time bins", "128")
|
| 99 |
+
)
|
| 100 |
+
st.markdown("#### TFRecord creationg settings")
|
| 101 |
+
config["tfrecs_limit_per_file"] = utils.validate_int(
|
| 102 |
+
utils.user_input(
|
| 103 |
+
"limit of context windows per tfrecord file", "600"
|
| 104 |
+
)
|
| 105 |
+
)
|
| 106 |
+
config["train_ratio"] = utils.validate_float(
|
| 107 |
+
utils.user_input("trainratio", "0.7")
|
| 108 |
+
)
|
| 109 |
+
config["test_val_ratio"] = utils.validate_float(
|
| 110 |
+
utils.user_input("test validation ratio", "0.7")
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
for k, v in config.items():
|
| 114 |
+
utils.write_to_session_file(k, v)
|
| 115 |
+
return True
|
acodet/front_end/st_train.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from acodet.front_end import utils
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def train_options(key="train"):
|
| 6 |
+
preset_option = int(
|
| 7 |
+
st.selectbox(
|
| 8 |
+
"How would you like run the program?",
|
| 9 |
+
(
|
| 10 |
+
"1 - train new model",
|
| 11 |
+
"2 - continue training on existing model and save model in the end",
|
| 12 |
+
"3 - evaluate saved model",
|
| 13 |
+
"4 - evaluate model checkpoint",
|
| 14 |
+
"5 - save model specified in advanced config",
|
| 15 |
+
),
|
| 16 |
+
key=key,
|
| 17 |
+
)[0]
|
| 18 |
+
)
|
| 19 |
+
st.session_state.preset_option = preset_option
|
| 20 |
+
utils.make_nested_btn_false_if_dropdown_changed(3, preset_option, 3)
|
| 21 |
+
utils.make_nested_btn_false_if_dropdown_changed(
|
| 22 |
+
run_id=1, preset_id=preset_option, btn_id=4
|
| 23 |
+
)
|
| 24 |
+
utils.next_button(id=3)
|
| 25 |
+
if not st.session_state.b3:
|
| 26 |
+
pass
|
| 27 |
+
else:
|
| 28 |
+
config = dict()
|
| 29 |
+
config["predefined_settings"] = preset_option
|
| 30 |
+
|
| 31 |
+
if preset_option == 1:
|
| 32 |
+
st.markdown("### Model training settings")
|
| 33 |
+
st.markdown("#### Model architecture")
|
| 34 |
+
model_architecture = utils.user_dropdown(
|
| 35 |
+
"Which model architecture would you like to use?",
|
| 36 |
+
(
|
| 37 |
+
"HumpBackNorthAtlantic",
|
| 38 |
+
"ResNet50",
|
| 39 |
+
"ResNet101",
|
| 40 |
+
"ResNet152",
|
| 41 |
+
"MobileNet",
|
| 42 |
+
"DenseNet169",
|
| 43 |
+
"DenseNet201",
|
| 44 |
+
"EfficientNet0",
|
| 45 |
+
"EfficientNet1",
|
| 46 |
+
"EfficientNet2",
|
| 47 |
+
"EfficientNet3",
|
| 48 |
+
"EfficientNet4",
|
| 49 |
+
"EfficientNet5",
|
| 50 |
+
"EfficientNet6",
|
| 51 |
+
"EfficientNet7",
|
| 52 |
+
),
|
| 53 |
+
)
|
| 54 |
+
config["ModelClassName"] = model_architecture
|
| 55 |
+
if not model_architecture == "HumpBackNorthAtlantic":
|
| 56 |
+
config["keras_mod_name"] = True
|
| 57 |
+
st.markdown("#### Hyperparameters")
|
| 58 |
+
config["batch_size"] = utils.validate_int(
|
| 59 |
+
utils.user_input("batch size", "32")
|
| 60 |
+
)
|
| 61 |
+
config["epochs"] = utils.validate_int(
|
| 62 |
+
utils.user_input("epochs", "50")
|
| 63 |
+
)
|
| 64 |
+
config["steps_per_epoch"] = utils.validate_int(
|
| 65 |
+
utils.user_input("steps per epoch", "1000")
|
| 66 |
+
)
|
| 67 |
+
config["init_lr"] = utils.validate_float(
|
| 68 |
+
utils.user_input("initial learning rate", "0.0005")
|
| 69 |
+
)
|
| 70 |
+
config["final_lr"] = utils.validate_float(
|
| 71 |
+
utils.user_input("final learning rate", "0.000005")
|
| 72 |
+
)
|
| 73 |
+
st.markdown("#### Augmentations")
|
| 74 |
+
config["time_augs"] = utils.user_dropdown(
|
| 75 |
+
"Use time-shift augmentation", ("True", "False")
|
| 76 |
+
)
|
| 77 |
+
config["mixup_augs"] = utils.user_dropdown(
|
| 78 |
+
"Use mixup augmentation", ("True", "False")
|
| 79 |
+
)
|
| 80 |
+
config["spec_aug"] = utils.user_dropdown(
|
| 81 |
+
"Use specaugment augmentation", ("True", "False")
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
for key in ["time_augs", "mixup_augs", "spec_aug"]:
|
| 85 |
+
if config[key] == "False":
|
| 86 |
+
config[key] = False
|
| 87 |
+
else:
|
| 88 |
+
config[key] = True
|
| 89 |
+
|
| 90 |
+
for k, v in config.items():
|
| 91 |
+
utils.write_to_session_file(k, v)
|
| 92 |
+
return True
|
acodet/front_end/st_visualization.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from acodet.front_end import utils
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
from acodet.create_session_file import read_session_file
|
| 8 |
+
from acodet.front_end import help_strings
|
| 9 |
+
|
| 10 |
+
conf = read_session_file()
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def output():
|
| 14 |
+
conf = read_session_file()
|
| 15 |
+
if st.session_state.run_option == 1:
|
| 16 |
+
if st.session_state.preset_option == 0:
|
| 17 |
+
disp = ShowAnnotationPredictions()
|
| 18 |
+
disp.show_annotation_predictions()
|
| 19 |
+
disp.create_tabs(
|
| 20 |
+
additional_headings=[
|
| 21 |
+
"Filtered Files",
|
| 22 |
+
"Annot. Plots",
|
| 23 |
+
"Presence Plots",
|
| 24 |
+
]
|
| 25 |
+
)
|
| 26 |
+
disp.show_stats()
|
| 27 |
+
disp.show_individual_files()
|
| 28 |
+
disp.show_individual_files(
|
| 29 |
+
tab_number=2, thresh_path=f"thresh_{conf['thresh']}"
|
| 30 |
+
)
|
| 31 |
+
plot_tabs = Results(disp)
|
| 32 |
+
plot_tabs.create_tabs()
|
| 33 |
+
|
| 34 |
+
elif st.session_state.preset_option == 1:
|
| 35 |
+
disp = ShowAnnotationPredictions()
|
| 36 |
+
disp.show_annotation_predictions()
|
| 37 |
+
disp.create_tabs()
|
| 38 |
+
disp.show_stats()
|
| 39 |
+
disp.show_individual_files()
|
| 40 |
+
|
| 41 |
+
elif st.session_state.preset_option == 2:
|
| 42 |
+
conf = read_session_file()
|
| 43 |
+
disp = ShowAnnotationPredictions()
|
| 44 |
+
disp.show_annotation_predictions()
|
| 45 |
+
disp.create_tabs()
|
| 46 |
+
disp.show_stats()
|
| 47 |
+
disp.show_individual_files(thresh_path=f"thresh_{conf['thresh']}")
|
| 48 |
+
|
| 49 |
+
elif st.session_state.preset_option == 3:
|
| 50 |
+
disp = ShowAnnotationPredictions()
|
| 51 |
+
disp.show_annotation_predictions()
|
| 52 |
+
disp.create_tabs(
|
| 53 |
+
additional_headings=[
|
| 54 |
+
"Annot. Plots",
|
| 55 |
+
"Presence Plots",
|
| 56 |
+
]
|
| 57 |
+
)
|
| 58 |
+
disp.show_stats()
|
| 59 |
+
disp.show_individual_files()
|
| 60 |
+
plot_tabs = Results(disp, tab_number=2)
|
| 61 |
+
plot_tabs.create_tabs()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class ShowAnnotationPredictions:
|
| 65 |
+
def show_annotation_predictions(self):
|
| 66 |
+
saved_annots_dir = Path(st.session_state.save_dir)
|
| 67 |
+
if len(list(saved_annots_dir.parents)) > 1:
|
| 68 |
+
self.annots_path = saved_annots_dir
|
| 69 |
+
else:
|
| 70 |
+
self.annots_path = Path(
|
| 71 |
+
conf["generated_annotations_folder"]
|
| 72 |
+
).joinpath(saved_annots_dir)
|
| 73 |
+
st.markdown(
|
| 74 |
+
f"""Your annotations are saved in the folder:
|
| 75 |
+
`{self.annots_path.resolve().as_posix()}`
|
| 76 |
+
"""
|
| 77 |
+
)
|
| 78 |
+
utils.write_to_session_file(
|
| 79 |
+
"generated_annotation_source", str(self.annots_path)
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
def create_tabs(self, additional_headings=[]):
|
| 83 |
+
"""
|
| 84 |
+
Create the tabs to display the respective results.
|
| 85 |
+
|
| 86 |
+
Parameters
|
| 87 |
+
----------
|
| 88 |
+
additional_headings : list, optional
|
| 89 |
+
list of additional headings, by default []
|
| 90 |
+
"""
|
| 91 |
+
tabs = st.tabs(
|
| 92 |
+
[
|
| 93 |
+
"Stats",
|
| 94 |
+
"Annot. Files",
|
| 95 |
+
*additional_headings,
|
| 96 |
+
]
|
| 97 |
+
)
|
| 98 |
+
for i, tab in enumerate(tabs):
|
| 99 |
+
setattr(self, f"tab{i}", tab)
|
| 100 |
+
|
| 101 |
+
def show_stats(self):
|
| 102 |
+
"""
|
| 103 |
+
Show stats file as pandas.DataFrame in a table.
|
| 104 |
+
"""
|
| 105 |
+
with self.tab0:
|
| 106 |
+
try:
|
| 107 |
+
df = pd.read_csv(self.annots_path.joinpath("stats.csv"))
|
| 108 |
+
if "Unnamed: 0" in df.columns:
|
| 109 |
+
df = df.drop(columns=["Unnamed: 0"])
|
| 110 |
+
st.dataframe(df, hide_index=True)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
print(e)
|
| 113 |
+
st.write(
|
| 114 |
+
"""No stats.csv file found. Please run predefined settings 0, or 1 first
|
| 115 |
+
to view this tab."""
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
def show_individual_files(
|
| 119 |
+
self, tab_number=1, thresh_path=conf["thresh_label"]
|
| 120 |
+
):
|
| 121 |
+
with getattr(self, f"tab{tab_number}"):
|
| 122 |
+
path = self.annots_path.joinpath(thresh_path)
|
| 123 |
+
annot_files = [l for l in path.rglob("*.txt")]
|
| 124 |
+
display_annots = [
|
| 125 |
+
f.relative_to(path).as_posix() for f in annot_files
|
| 126 |
+
]
|
| 127 |
+
chosen_file = st.selectbox(
|
| 128 |
+
label=f"""Choose a generated annotations file from
|
| 129 |
+
`{path.resolve()}`""",
|
| 130 |
+
options=display_annots,
|
| 131 |
+
key=f"file_selec_{tab_number}",
|
| 132 |
+
help=help_strings.ANNOT_FILES_DROPDOWN,
|
| 133 |
+
)
|
| 134 |
+
st.write("All of these files can be imported into Raven directly.")
|
| 135 |
+
df = pd.read_csv(path.joinpath(chosen_file), sep="\t")
|
| 136 |
+
st.dataframe(df, hide_index=True)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class Results(utils.Limits):
|
| 140 |
+
def __init__(self, disp_obj, tab_number=3) -> None:
|
| 141 |
+
"""
|
| 142 |
+
Results class containing all of the data processings to prepare
|
| 143 |
+
data for plots and tables.
|
| 144 |
+
|
| 145 |
+
Parameters
|
| 146 |
+
----------
|
| 147 |
+
disp_obj : object
|
| 148 |
+
ShowAnnotationPredictions object to link processing to
|
| 149 |
+
the respective streamlit widget
|
| 150 |
+
tab_number : int, optional
|
| 151 |
+
number of tab to show results in, by default 3
|
| 152 |
+
"""
|
| 153 |
+
self.plots_paths = [
|
| 154 |
+
[d for d in p.iterdir() if d.is_dir()]
|
| 155 |
+
for p in disp_obj.annots_path.rglob("*analysis*")
|
| 156 |
+
][0]
|
| 157 |
+
if not self.plots_paths:
|
| 158 |
+
st.write(
|
| 159 |
+
"No analysis files found for this dataset. "
|
| 160 |
+
"Please run predefined settings 0, or 1 first."
|
| 161 |
+
)
|
| 162 |
+
st.stop()
|
| 163 |
+
self.disp_obj = disp_obj
|
| 164 |
+
self.tab_number = tab_number
|
| 165 |
+
|
| 166 |
+
def create_tabs(self):
|
| 167 |
+
"""
|
| 168 |
+
Create tabs for plots.
|
| 169 |
+
"""
|
| 170 |
+
self.tabs = {
|
| 171 |
+
"binary": getattr(self.disp_obj, f"tab{self.tab_number}"),
|
| 172 |
+
"presence": getattr(self.disp_obj, f"tab{self.tab_number+1}"),
|
| 173 |
+
}
|
| 174 |
+
for key, tab in self.tabs.items():
|
| 175 |
+
self.init_tab(tab=tab, key=key)
|
| 176 |
+
|
| 177 |
+
def init_tab(self, tab, key):
|
| 178 |
+
with tab:
|
| 179 |
+
datasets = [l.stem for l in self.plots_paths]
|
| 180 |
+
|
| 181 |
+
chosen_dataset = st.selectbox(
|
| 182 |
+
label=f"""Choose a dataset:""",
|
| 183 |
+
options=datasets,
|
| 184 |
+
key=f"dataset_selec_{key}",
|
| 185 |
+
)
|
| 186 |
+
self.chosen_dataset = (
|
| 187 |
+
self.disp_obj.annots_path.joinpath(conf["thresh_label"])
|
| 188 |
+
.joinpath("analysis")
|
| 189 |
+
.joinpath(chosen_dataset)
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
limit = st.radio(
|
| 193 |
+
"What limit would you like to set?",
|
| 194 |
+
("Simple limit", "Sequence limit"),
|
| 195 |
+
key=f"limit_selec_{key}",
|
| 196 |
+
help=help_strings.LIMIT,
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
super(Results, self).__init__(limit, key)
|
| 200 |
+
|
| 201 |
+
results = PlotDisplay(self.chosen_dataset, tab, key)
|
| 202 |
+
results.plot_df(self.limit_label)
|
| 203 |
+
|
| 204 |
+
self.create_limit_sliders()
|
| 205 |
+
self.rerun_computation_btn()
|
| 206 |
+
|
| 207 |
+
self.save_selection_tables_with_limit_settings()
|
| 208 |
+
|
| 209 |
+
def rerun_computation_btn(self):
|
| 210 |
+
"""
|
| 211 |
+
Show rerun computation button after limits have been set and
|
| 212 |
+
execute run.main.
|
| 213 |
+
"""
|
| 214 |
+
rerun = st.button("Rerun computation", key=f"update_plot_{self.key}")
|
| 215 |
+
st.session_state.progbar_update = st.progress(0, text="Updating plot")
|
| 216 |
+
if rerun:
|
| 217 |
+
utils.write_to_session_file(self.thresh_label, self.thresh)
|
| 218 |
+
if self.sc:
|
| 219 |
+
utils.write_to_session_file(self.limit_label, self.limit)
|
| 220 |
+
|
| 221 |
+
import run
|
| 222 |
+
|
| 223 |
+
run.main(
|
| 224 |
+
dont_save_plot=True,
|
| 225 |
+
sc=self.sc,
|
| 226 |
+
fetch_config_again=True,
|
| 227 |
+
preset=3,
|
| 228 |
+
update_plot=True,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class PlotDisplay:
|
| 233 |
+
def __init__(self, chosen_dataset, tab, key) -> None:
|
| 234 |
+
self.chosen_dataset = chosen_dataset
|
| 235 |
+
self.tab = tab
|
| 236 |
+
self.key = key
|
| 237 |
+
|
| 238 |
+
if key == "binary":
|
| 239 |
+
self.path_prefix = "hourly_annotation"
|
| 240 |
+
self.cbar_label = "Number of annotations"
|
| 241 |
+
self.c_range = [0, conf["max_annots_per_hour"]]
|
| 242 |
+
elif key == "presence":
|
| 243 |
+
self.path_prefix = "hourly_presence"
|
| 244 |
+
self.cbar_label = "Presence"
|
| 245 |
+
self.c_range = [0, 1]
|
| 246 |
+
|
| 247 |
+
def plot_df(self, limit_label):
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
Plot dataframe showing either hourly presence or annotation count
|
| 251 |
+
in an interactive plotly visualization.
|
| 252 |
+
|
| 253 |
+
TODO onclick display of scrollable spectrogram would be really sick.
|
| 254 |
+
|
| 255 |
+
Parameters
|
| 256 |
+
----------
|
| 257 |
+
limit_label : string
|
| 258 |
+
key of config dict to acces simple or sequence limit
|
| 259 |
+
"""
|
| 260 |
+
df = pd.read_csv(
|
| 261 |
+
self.chosen_dataset.joinpath(
|
| 262 |
+
f"{self.path_prefix}_{limit_label}.csv"
|
| 263 |
+
)
|
| 264 |
+
)
|
| 265 |
+
df.index = pd.DatetimeIndex(df.Date)
|
| 266 |
+
df = df.reindex(
|
| 267 |
+
pd.date_range(df.index[0], df.index[-1]), fill_value=np.nan
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
h_of_day_str = ["%.2i:00" % i for i in range(24)]
|
| 271 |
+
h_pres = df.loc[:, h_of_day_str]
|
| 272 |
+
|
| 273 |
+
fig = px.imshow(
|
| 274 |
+
h_pres.T,
|
| 275 |
+
labels=dict(x="Date", y="Time of Day", color=self.cbar_label),
|
| 276 |
+
x=df.index,
|
| 277 |
+
y=h_of_day_str,
|
| 278 |
+
range_color=self.c_range,
|
| 279 |
+
color_continuous_scale="blugrn",
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
fig.update_xaxes(showgrid=False)
|
| 283 |
+
fig.update_yaxes(showgrid=False)
|
| 284 |
+
fig.update_layout(hovermode="x")
|
| 285 |
+
|
| 286 |
+
st.plotly_chart(fig)
|
acodet/front_end/utils.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from acodet import create_session_file
|
| 4 |
+
from . import help_strings
|
| 5 |
+
|
| 6 |
+
conf = create_session_file.read_session_file()
|
| 7 |
+
import json
|
| 8 |
+
import keras
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def open_folder_dialogue(
|
| 12 |
+
path=conf["generated_annotations_folder"],
|
| 13 |
+
key="folder",
|
| 14 |
+
label="Choose a folder",
|
| 15 |
+
filter_existing_annotations=False,
|
| 16 |
+
**kwargs,
|
| 17 |
+
):
|
| 18 |
+
try:
|
| 19 |
+
if not filter_existing_annotations:
|
| 20 |
+
foldernames_list = [
|
| 21 |
+
f"{x.stem}{x.suffix}"
|
| 22 |
+
for x in Path(path).iterdir()
|
| 23 |
+
if x.is_dir()
|
| 24 |
+
]
|
| 25 |
+
if f"thresh_{conf['default_threshold']}" in foldernames_list:
|
| 26 |
+
foldernames_list = [f"thresh_{conf['default_threshold']}"]
|
| 27 |
+
else:
|
| 28 |
+
foldernames_list = [
|
| 29 |
+
f"{x.stem}{x.suffix}"
|
| 30 |
+
for x in Path(path).iterdir()
|
| 31 |
+
if x.is_dir()
|
| 32 |
+
]
|
| 33 |
+
foldernames_list.sort()
|
| 34 |
+
foldernames_list.reverse()
|
| 35 |
+
# create selectbox with the foldernames
|
| 36 |
+
chosen_folder = st.selectbox(
|
| 37 |
+
label=label, options=foldernames_list, key=key, **kwargs
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# set the full path to be able to refer to it
|
| 41 |
+
directory_path = Path(path).joinpath(chosen_folder)
|
| 42 |
+
return str(directory_path)
|
| 43 |
+
|
| 44 |
+
except FileNotFoundError:
|
| 45 |
+
st.write("Folder does not exist, retrying.")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def next_button(id, text="Next", **kwargs):
|
| 49 |
+
if f"b{id}" not in st.session_state:
|
| 50 |
+
setattr(st.session_state, f"b{id}", False)
|
| 51 |
+
val = st.button(text, key=f"button_{id}", **kwargs)
|
| 52 |
+
if val:
|
| 53 |
+
setattr(st.session_state, f"b{id}", True)
|
| 54 |
+
make_nested_btns_false_on_click(id)
|
| 55 |
+
if text in ["Run computations", "Next"]:
|
| 56 |
+
st.session_state.run_finished = False
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def user_input(label, val, **input_params):
|
| 60 |
+
c1, c2 = st.columns(2)
|
| 61 |
+
c1.markdown("##")
|
| 62 |
+
input_params.setdefault("key", label)
|
| 63 |
+
c1.markdown(label)
|
| 64 |
+
return c2.text_input(" ", val, **input_params)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def user_dropdown(label, vals, **input_params):
|
| 68 |
+
c1, c2 = st.columns(2)
|
| 69 |
+
c1.markdown("##")
|
| 70 |
+
input_params.setdefault("key", label)
|
| 71 |
+
c1.markdown(label)
|
| 72 |
+
return c2.selectbox(" ", vals, **input_params)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def write_to_session_file(key, value):
|
| 76 |
+
if "session_started" in st.session_state:
|
| 77 |
+
setattr(st.session_state, key, value)
|
| 78 |
+
else:
|
| 79 |
+
with open("acodet/src/tmp_session.json", "r") as f:
|
| 80 |
+
session = json.load(f)
|
| 81 |
+
session[key] = value
|
| 82 |
+
with open("acodet/src/tmp_session.json", "w") as f:
|
| 83 |
+
json.dump(session, f)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def validate_float(input):
|
| 87 |
+
try:
|
| 88 |
+
return float(input)
|
| 89 |
+
except ValueError:
|
| 90 |
+
st.write(
|
| 91 |
+
'<p style="color:red; font-size:10px;">The value you entered is not a number.</p>',
|
| 92 |
+
unsafe_allow_html=True,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def validate_int(input):
|
| 97 |
+
try:
|
| 98 |
+
return int(input)
|
| 99 |
+
except ValueError:
|
| 100 |
+
st.write(
|
| 101 |
+
'<p style="color:red; font-size:12px;">The value you entered is not a number.</p>',
|
| 102 |
+
unsafe_allow_html=True,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def make_nested_btn_false_if_dropdown_changed(run_id, preset_id, btn_id):
|
| 107 |
+
if "session_started" in st.session_state:
|
| 108 |
+
session = {**st.session_state}
|
| 109 |
+
else:
|
| 110 |
+
with open("acodet/src/tmp_session.json", "r") as f:
|
| 111 |
+
session = json.load(f)
|
| 112 |
+
if not (
|
| 113 |
+
session["run_config"] == run_id
|
| 114 |
+
and session["predefined_settings"] == preset_id
|
| 115 |
+
):
|
| 116 |
+
setattr(st.session_state, f"b{btn_id}", False)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def make_nested_btns_false_on_click(btn_id):
|
| 120 |
+
btns = [i for i in range(btn_id + 1, 6)]
|
| 121 |
+
for btn in btns:
|
| 122 |
+
setattr(st.session_state, f"b{btn}", False)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def prepare_run():
|
| 126 |
+
if st.session_state.run_option == 1:
|
| 127 |
+
st.markdown("""---""")
|
| 128 |
+
st.markdown("## Computation started, please wait.")
|
| 129 |
+
if st.session_state.preset_option in [0, 1]:
|
| 130 |
+
kwargs = {
|
| 131 |
+
"callbacks": TFPredictProgressBar,
|
| 132 |
+
"progbar1": st.progress(0, text="Current file"),
|
| 133 |
+
"progbar2": st.progress(0, text="Overall progress"),
|
| 134 |
+
}
|
| 135 |
+
else:
|
| 136 |
+
kwargs = {"progbar1": st.progress(0, text="Progress")}
|
| 137 |
+
return kwargs
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class Limits:
|
| 141 |
+
def __init__(self, limit, key):
|
| 142 |
+
"""
|
| 143 |
+
A simple class to contain all methods revolving around the limit sliders
|
| 144 |
+
for simple and sequence limit.
|
| 145 |
+
|
| 146 |
+
Parameters
|
| 147 |
+
----------
|
| 148 |
+
limit : string
|
| 149 |
+
either simple or sequence limit, from radio btn
|
| 150 |
+
key : string
|
| 151 |
+
unique identifier for streamlit options
|
| 152 |
+
"""
|
| 153 |
+
self.key = "limit_" + key
|
| 154 |
+
self.save_btn = False
|
| 155 |
+
if limit == "Simple limit":
|
| 156 |
+
self.limit_label = "simple_limit"
|
| 157 |
+
self.thresh_label = "thresh"
|
| 158 |
+
self.sc = False
|
| 159 |
+
self.limit_max = 50
|
| 160 |
+
elif limit == "Sequence limit":
|
| 161 |
+
self.limit_label = "sequence_limit"
|
| 162 |
+
self.thresh_label = "sequence_thresh"
|
| 163 |
+
self.sc = True
|
| 164 |
+
self.limit_max = 20
|
| 165 |
+
|
| 166 |
+
def create_limit_sliders(self):
|
| 167 |
+
"""
|
| 168 |
+
Show sliders for simple and sequence limit, depending on the selection
|
| 169 |
+
of the radio btn.
|
| 170 |
+
"""
|
| 171 |
+
self.thresh = st.slider(
|
| 172 |
+
"Threshold",
|
| 173 |
+
0.35,
|
| 174 |
+
0.99,
|
| 175 |
+
conf[self.thresh_label],
|
| 176 |
+
0.01,
|
| 177 |
+
key=f"thresh_slider_{self.key}",
|
| 178 |
+
help=help_strings.THRESHOLD,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
if self.sc:
|
| 182 |
+
self.limit = st.slider(
|
| 183 |
+
"Limit",
|
| 184 |
+
1,
|
| 185 |
+
self.limit_max,
|
| 186 |
+
conf[self.limit_label],
|
| 187 |
+
1,
|
| 188 |
+
key=f"limit_slider_{self.key}",
|
| 189 |
+
help=help_strings.SC_LIMIT,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
def show_save_selection_tables_btn(self):
|
| 193 |
+
"""Show save selection tables btn."""
|
| 194 |
+
self.save_btn = st.button(
|
| 195 |
+
"Save tables", self.key, help=help_strings.SAVE_SELECTION_BTN
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def save_selection_tables_with_limit_settings(self):
|
| 199 |
+
"""
|
| 200 |
+
Save the selection tables of the chosen dataset again with
|
| 201 |
+
the selected settings of the respective limit.
|
| 202 |
+
"""
|
| 203 |
+
self.show_save_selection_tables_btn()
|
| 204 |
+
if self.save_btn:
|
| 205 |
+
st.session_state.progbar_update = st.progress(0, text="Progress")
|
| 206 |
+
write_to_session_file(self.thresh_label, self.thresh)
|
| 207 |
+
if self.sc:
|
| 208 |
+
write_to_session_file(self.limit_label, self.limit)
|
| 209 |
+
|
| 210 |
+
import run
|
| 211 |
+
|
| 212 |
+
run.main(
|
| 213 |
+
dont_save_plot=True,
|
| 214 |
+
sc=self.sc,
|
| 215 |
+
fetch_config_again=True,
|
| 216 |
+
preset=3,
|
| 217 |
+
save_filtered_selection_tables=True,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class TFPredictProgressBar(keras.callbacks.Callback):
|
| 222 |
+
def __init__(self, num_of_files, progbar1, progbar2, **kwargs):
|
| 223 |
+
self.num_of_files = num_of_files
|
| 224 |
+
self.pr_bar1 = progbar1
|
| 225 |
+
self.pr_bar2 = progbar2
|
| 226 |
+
|
| 227 |
+
def on_predict_end(self, logs=None):
|
| 228 |
+
self.pr_bar2.progress(
|
| 229 |
+
st.session_state.progbar1 / self.num_of_files,
|
| 230 |
+
text="Overall progress",
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
def on_predict_batch_begin(self, batch, logs=None):
|
| 234 |
+
if self.params["steps"] == 1:
|
| 235 |
+
denominator = 1
|
| 236 |
+
else:
|
| 237 |
+
denominator = self.params["steps"] - 1
|
| 238 |
+
self.pr_bar1.progress(batch / denominator, text="Current file")
|
acodet/funcs.py
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from email import generator
|
| 2 |
+
import re
|
| 3 |
+
import zipfile
|
| 4 |
+
import datetime as dt
|
| 5 |
+
import json
|
| 6 |
+
import tensorflow as tf
|
| 7 |
+
import numpy as np
|
| 8 |
+
import librosa as lb
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from . import global_config as conf
|
| 12 |
+
|
| 13 |
+
############# ANNOTATION helpers ############################################
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def remove_str_flags_from_predictions(df):
|
| 17 |
+
# TODO wenn annotation_column nicht in columns ist fehler raisen
|
| 18 |
+
n = df.loc[df[conf.ANNOTATION_COLUMN] == "n"].index
|
| 19 |
+
n_ = df.loc[df[conf.ANNOTATION_COLUMN] == "n "].index
|
| 20 |
+
u = df.loc[df[conf.ANNOTATION_COLUMN] == "u"].index
|
| 21 |
+
u_ = df.loc[df[conf.ANNOTATION_COLUMN] == "u "].index
|
| 22 |
+
c = df.loc[df[conf.ANNOTATION_COLUMN] == "c"].index
|
| 23 |
+
c_ = df.loc[df[conf.ANNOTATION_COLUMN] == "c "].index
|
| 24 |
+
|
| 25 |
+
clean = df.drop([*n, *u, *c, *n_, *u_, *c_])
|
| 26 |
+
clean.loc[:, conf.ANNOTATION_COLUMN] = clean[
|
| 27 |
+
conf.ANNOTATION_COLUMN
|
| 28 |
+
].astype(float)
|
| 29 |
+
return clean
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
############# TFRECORDS helpers #############################################
|
| 33 |
+
def get_annots_for_file(annots: pd.DataFrame, file: str) -> pd.DataFrame:
|
| 34 |
+
"""
|
| 35 |
+
Get annotations for a file and sort by the start time of the annotated
|
| 36 |
+
call.
|
| 37 |
+
|
| 38 |
+
Parameters
|
| 39 |
+
----------
|
| 40 |
+
annots : pd.DataFrame
|
| 41 |
+
global annotations dataframe
|
| 42 |
+
file : str
|
| 43 |
+
file path
|
| 44 |
+
|
| 45 |
+
Returns
|
| 46 |
+
-------
|
| 47 |
+
pd.DataFrame
|
| 48 |
+
filtered annotations dataframe
|
| 49 |
+
"""
|
| 50 |
+
return annots[annots.filename == file].sort_values("start")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def get_dt_filename(file):
|
| 54 |
+
if isinstance(file, Path):
|
| 55 |
+
stem = file.stem
|
| 56 |
+
else:
|
| 57 |
+
stem = file
|
| 58 |
+
|
| 59 |
+
if "_annot_" in stem:
|
| 60 |
+
stem = stem.split("_annot_")[0]
|
| 61 |
+
|
| 62 |
+
numbs = re.findall("[0-9]+", stem)
|
| 63 |
+
numbs = [n for n in numbs if len(n) % 2 == 0]
|
| 64 |
+
|
| 65 |
+
i, datetime = 1, ""
|
| 66 |
+
while len(datetime) < 12:
|
| 67 |
+
if i > 1000:
|
| 68 |
+
raise NameError(
|
| 69 |
+
"""Time stamp Error: time stamp in filename
|
| 70 |
+
doesn't fit any known pattern. If you would
|
| 71 |
+
like to predict this file anyway, please
|
| 72 |
+
choose predefined settings option 1.
|
| 73 |
+
"""
|
| 74 |
+
)
|
| 75 |
+
datetime = "".join(numbs[-i:])
|
| 76 |
+
i += 1
|
| 77 |
+
|
| 78 |
+
i = 1
|
| 79 |
+
while 12 <= len(datetime) > 14:
|
| 80 |
+
datetime = datetime[:-i]
|
| 81 |
+
|
| 82 |
+
for _ in range(2):
|
| 83 |
+
try:
|
| 84 |
+
if len(datetime) == 12:
|
| 85 |
+
file_date = dt.datetime.strptime(datetime, "%y%m%d%H%M%S")
|
| 86 |
+
elif len(datetime) == 14:
|
| 87 |
+
file_date = dt.datetime.strptime(datetime, "%Y%m%d%H%M%S")
|
| 88 |
+
except:
|
| 89 |
+
i = 1
|
| 90 |
+
while len(datetime) > 12:
|
| 91 |
+
datetime = datetime[:-i]
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
# print(file_date)
|
| 95 |
+
return file_date
|
| 96 |
+
except Exception as e:
|
| 97 |
+
print(
|
| 98 |
+
"File date naming not understood.\n",
|
| 99 |
+
"This will be prevent hourly prediction computation.\n",
|
| 100 |
+
e,
|
| 101 |
+
)
|
| 102 |
+
return "ERROR"
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def get_channel(dir):
|
| 106 |
+
if "_CH" in str(dir)[-5:]:
|
| 107 |
+
channel = int(re.findall("[0-9]+", str(dir)[-5:])[0]) - 1
|
| 108 |
+
else:
|
| 109 |
+
channel = 0
|
| 110 |
+
return channel
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def load_audio(file, channel=0, **kwargs) -> np.ndarray:
|
| 114 |
+
"""
|
| 115 |
+
Load audio file, print error if file is corrupted. If the sample rate
|
| 116 |
+
specified in the config file is not the same as the downsample sample
|
| 117 |
+
rate, resample the audio file accordingly.
|
| 118 |
+
|
| 119 |
+
Parameters
|
| 120 |
+
----------
|
| 121 |
+
file : str or pathlib.Path
|
| 122 |
+
file path
|
| 123 |
+
|
| 124 |
+
Returns
|
| 125 |
+
-------
|
| 126 |
+
audio_flat: np.ndarray
|
| 127 |
+
audio array
|
| 128 |
+
"""
|
| 129 |
+
try:
|
| 130 |
+
if conf.DOWNSAMPLE_SR and conf.SR != conf.DOWNSAMPLE_SR:
|
| 131 |
+
with open(file, "rb") as f:
|
| 132 |
+
audio_flat, _ = lb.load(
|
| 133 |
+
f, sr=conf.DOWNSAMPLE_SR, mono=False, **kwargs
|
| 134 |
+
)[channel]
|
| 135 |
+
if len(audio_flat.shape) > 1:
|
| 136 |
+
audio_flat = audio_flat[channel]
|
| 137 |
+
|
| 138 |
+
audio_flat = lb.resample(
|
| 139 |
+
audio_flat, orig_sr=conf.DOWNSAMPLE_SR, target_sr=conf.SR
|
| 140 |
+
)
|
| 141 |
+
else:
|
| 142 |
+
with open(file, "rb") as f:
|
| 143 |
+
audio_flat, _ = lb.load(f, sr=conf.SR, mono=False, **kwargs)
|
| 144 |
+
if len(audio_flat.shape) > 1:
|
| 145 |
+
audio_flat = audio_flat[channel]
|
| 146 |
+
|
| 147 |
+
if len(audio_flat) == 0:
|
| 148 |
+
return
|
| 149 |
+
return audio_flat
|
| 150 |
+
except:
|
| 151 |
+
print("File is corrputed and can't be loaded.")
|
| 152 |
+
return
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def return_windowed_file(file) -> tuple([np.ndarray, np.ndarray]):
|
| 156 |
+
"""
|
| 157 |
+
Load audio file and turn the 1D array into a 2D array. The rows length
|
| 158 |
+
corresponds to the window length specified by the config file. The
|
| 159 |
+
number of columns results from the division of the 1D array length
|
| 160 |
+
by the context window length. The incomplete remainder is discarded.
|
| 161 |
+
Along with the window, a 1D array of times is returned, corresponding
|
| 162 |
+
to the beginning in seconds of each context window within the original
|
| 163 |
+
file.
|
| 164 |
+
|
| 165 |
+
Parameters
|
| 166 |
+
----------
|
| 167 |
+
file : str or pathlib.Path
|
| 168 |
+
file path
|
| 169 |
+
|
| 170 |
+
Returns
|
| 171 |
+
-------
|
| 172 |
+
audio_arr: np.ndarray
|
| 173 |
+
2D audio array
|
| 174 |
+
times: np.ndarray
|
| 175 |
+
start times of the context windows
|
| 176 |
+
"""
|
| 177 |
+
audio = load_audio(file)
|
| 178 |
+
audio = audio[: len(audio) // conf.CONTEXT_WIN * conf.CONTEXT_WIN]
|
| 179 |
+
audio_arr = audio.reshape(
|
| 180 |
+
[len(audio) // conf.CONTEXT_WIN, conf.CONTEXT_WIN]
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
times = np.arange(
|
| 184 |
+
0,
|
| 185 |
+
audio_arr.shape[0] * conf.CONTEXT_WIN / conf.SR,
|
| 186 |
+
conf.CONTEXT_WIN / conf.SR,
|
| 187 |
+
)
|
| 188 |
+
return audio_arr, times
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def cntxt_wndw_arr(
|
| 192 |
+
annotations: pd.DataFrame, file, inbetween_noise: bool = True, **kwargs
|
| 193 |
+
) -> tuple:
|
| 194 |
+
"""
|
| 195 |
+
Load an audio file, with the duration given by the time difference
|
| 196 |
+
between the first and last annotation. Iterate through the annotations
|
| 197 |
+
and extract 1D segments from the audio array based on the annotations,
|
| 198 |
+
the sample rate and the context window length, all specified in the config
|
| 199 |
+
file. The resulting context window is appended to a list, yielding a 2D
|
| 200 |
+
array with context windows corresponding to the annotated sections.
|
| 201 |
+
|
| 202 |
+
To be able to find the section in the original audio file, another list
|
| 203 |
+
is filled with all the start times noted in the annotations multiplied
|
| 204 |
+
by the sample rate. The times list contains the beginning time of each
|
| 205 |
+
context window in samples.
|
| 206 |
+
|
| 207 |
+
Finally if the argument 'inbetween_noise' is False, the nosie arrays in between
|
| 208 |
+
the calls are collected and all 4 arrays are returned.
|
| 209 |
+
|
| 210 |
+
Parameters
|
| 211 |
+
----------
|
| 212 |
+
annotations : pd.DataFrame
|
| 213 |
+
annotations of vocalizations in file
|
| 214 |
+
file : str or pathlib.Path
|
| 215 |
+
file path
|
| 216 |
+
inbetween_noise : bool, defaults to True
|
| 217 |
+
decide if in between noise should be collected as well or, in case
|
| 218 |
+
the argument is True, all the annotations are already all noise, in
|
| 219 |
+
which case no in between noise is returned
|
| 220 |
+
|
| 221 |
+
Returns
|
| 222 |
+
-------
|
| 223 |
+
seg_ar: np.ndarray
|
| 224 |
+
segment array containing the 2D audio array
|
| 225 |
+
noise_ar: np.ndarray
|
| 226 |
+
2D audio array of noise
|
| 227 |
+
times_c
|
| 228 |
+
time list for calls
|
| 229 |
+
times_n
|
| 230 |
+
time list for noise
|
| 231 |
+
"""
|
| 232 |
+
duration = annotations["end"].iloc[-1] + conf.CONTEXT_WIN / conf.SR
|
| 233 |
+
audio = load_audio(file, duration=duration)
|
| 234 |
+
|
| 235 |
+
segs, times = [], []
|
| 236 |
+
for _, row in annotations.iterrows():
|
| 237 |
+
num_windows = round(
|
| 238 |
+
(row.end - row.start) / (conf.CONTEXT_WIN / conf.SR) - 1
|
| 239 |
+
)
|
| 240 |
+
num_windows = num_windows or 1
|
| 241 |
+
for i in range(
|
| 242 |
+
num_windows
|
| 243 |
+
): # TODO fuer grosse annotationen mehrere fenster erzeugen
|
| 244 |
+
start = row.start + i * (conf.CONTEXT_WIN / conf.SR)
|
| 245 |
+
beg = int(start * conf.SR)
|
| 246 |
+
end = int(start * conf.SR + conf.CONTEXT_WIN)
|
| 247 |
+
|
| 248 |
+
if len(audio[beg:end]) == conf.CONTEXT_WIN:
|
| 249 |
+
segs.append(audio[beg:end])
|
| 250 |
+
times.append(beg)
|
| 251 |
+
else:
|
| 252 |
+
end = len(audio)
|
| 253 |
+
beg = end - conf.CONTEXT_WIN
|
| 254 |
+
segs.append(audio[beg:end])
|
| 255 |
+
times.append(beg)
|
| 256 |
+
break
|
| 257 |
+
|
| 258 |
+
segs = np.array(segs, dtype="float32")
|
| 259 |
+
times = np.array(times, dtype="float32")
|
| 260 |
+
|
| 261 |
+
# TODO docstrings aufraumen
|
| 262 |
+
if len(segs) - len(annotations) < 0:
|
| 263 |
+
annotations = annotations.drop(
|
| 264 |
+
annotations.index[len(segs) - len(annotations) :]
|
| 265 |
+
)
|
| 266 |
+
if (
|
| 267 |
+
not inbetween_noise
|
| 268 |
+
): # TODO mismatch in lengths, allow longer active learning annots
|
| 269 |
+
seg_ar = np.array(segs[annotations["label"] == 1], dtype="float32")
|
| 270 |
+
times_c = np.array(times[annotations["label"] == 1], dtype="float32")
|
| 271 |
+
else:
|
| 272 |
+
seg_ar = segs
|
| 273 |
+
times_c = times
|
| 274 |
+
if inbetween_noise:
|
| 275 |
+
noise_ar, times_n = return_inbetween_noise_arrays(audio, annotations)
|
| 276 |
+
elif len(annotations.loc[annotations["label"] == 0]) > 0:
|
| 277 |
+
noise_ar = np.array(segs[annotations["label"] == 0], dtype="float32")
|
| 278 |
+
times_n = np.array(times[annotations["label"] == 0], dtype="float32")
|
| 279 |
+
else:
|
| 280 |
+
noise_ar, times_n = np.array([]), np.array([])
|
| 281 |
+
|
| 282 |
+
return seg_ar, noise_ar, times_c, times_n
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def wins_bet_calls(annotations: pd.DataFrame) -> list:
|
| 286 |
+
"""
|
| 287 |
+
Returns a list of ints, corresponding to the number of context windows
|
| 288 |
+
that fit between calls. The indexing is crucial, so that each start time
|
| 289 |
+
is subtracted from the previous end time, thereby yielding the gap length.
|
| 290 |
+
|
| 291 |
+
Parameters
|
| 292 |
+
----------
|
| 293 |
+
annotations : pd.DataFrame
|
| 294 |
+
annotations
|
| 295 |
+
|
| 296 |
+
Returns
|
| 297 |
+
-------
|
| 298 |
+
list
|
| 299 |
+
number of context windows that fit between the start of one and the end of
|
| 300 |
+
the previous annotation
|
| 301 |
+
"""
|
| 302 |
+
beg_min_start = annotations.start[1:].values - annotations.end[:-1].values
|
| 303 |
+
return (beg_min_start // (conf.CONTEXT_WIN / conf.SR)).astype(int)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def return_inbetween_noise_arrays(
|
| 307 |
+
audio: np.ndarray, annotations: pd.DataFrame
|
| 308 |
+
) -> tuple:
|
| 309 |
+
"""
|
| 310 |
+
Collect audio arrays based on the gaps between vocalizations.
|
| 311 |
+
Based on the number of context windows that fit inbetween two
|
| 312 |
+
subsequent annotations, the resulting amount of segments are
|
| 313 |
+
extracted from the audio file.
|
| 314 |
+
Again, a list containing the start time of each array is also
|
| 315 |
+
retrieved.
|
| 316 |
+
If no entire context window fits between two annotations, no
|
| 317 |
+
noise sample is generated.
|
| 318 |
+
The resulting 2D noise array is returned along with the times.
|
| 319 |
+
|
| 320 |
+
Parameters
|
| 321 |
+
----------
|
| 322 |
+
audio : np.ndarray
|
| 323 |
+
flat 1D audio array
|
| 324 |
+
annotations : pd.DataFrame
|
| 325 |
+
annotations of vocalizations
|
| 326 |
+
|
| 327 |
+
Returns
|
| 328 |
+
-------
|
| 329 |
+
np.ndarray
|
| 330 |
+
2D audio array of noise
|
| 331 |
+
times: list
|
| 332 |
+
start times of each context window
|
| 333 |
+
"""
|
| 334 |
+
noise_ar, times = list(), list()
|
| 335 |
+
for ind, num_wndws in enumerate(wins_bet_calls(annotations)):
|
| 336 |
+
if num_wndws < 1:
|
| 337 |
+
continue
|
| 338 |
+
|
| 339 |
+
for window_ind in range(num_wndws):
|
| 340 |
+
beg = (
|
| 341 |
+
int(annotations.end.iloc[ind] * conf.SR)
|
| 342 |
+
+ conf.CONTEXT_WIN * window_ind
|
| 343 |
+
)
|
| 344 |
+
end = beg + conf.CONTEXT_WIN
|
| 345 |
+
noise_ar.append(audio[beg:end])
|
| 346 |
+
times.append(beg)
|
| 347 |
+
|
| 348 |
+
return np.array(noise_ar, dtype="float32"), times
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def get_train_set_size(tfrec_path):
|
| 352 |
+
if not isinstance(tfrec_path, list):
|
| 353 |
+
tfrec_path = [tfrec_path]
|
| 354 |
+
train_set_size, noise_set_size = 0, 0
|
| 355 |
+
for dataset_dir in tfrec_path:
|
| 356 |
+
try:
|
| 357 |
+
for dic in Path(dataset_dir).glob("**/*dataset*.json"):
|
| 358 |
+
with open(dic, "r") as f:
|
| 359 |
+
data_dict = json.load(f)
|
| 360 |
+
if "noise" in str(dic):
|
| 361 |
+
noise_set_size += data_dict["dataset"]["size"]["train"]
|
| 362 |
+
elif "train" in data_dict["dataset"]["size"]:
|
| 363 |
+
train_set_size += data_dict["dataset"]["size"]["train"]
|
| 364 |
+
except:
|
| 365 |
+
print(
|
| 366 |
+
"No dataset dictionary found, estimating dataset size."
|
| 367 |
+
"WARNING: This might lead to incorrect learning rates!"
|
| 368 |
+
)
|
| 369 |
+
train_set_size += 5000
|
| 370 |
+
noise_set_size += 100
|
| 371 |
+
return train_set_size, noise_set_size
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
################ Plotting helpers ###########################################
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def get_time(time: float) -> str:
|
| 378 |
+
"""
|
| 379 |
+
Return time in readable string format m:s.ms.
|
| 380 |
+
|
| 381 |
+
Parameters
|
| 382 |
+
----------
|
| 383 |
+
time : float
|
| 384 |
+
time in seconds
|
| 385 |
+
|
| 386 |
+
Returns
|
| 387 |
+
-------
|
| 388 |
+
str
|
| 389 |
+
time in minutes:seconds.miliseconds
|
| 390 |
+
"""
|
| 391 |
+
return f"{int(time/60)}:{np.mod(time, 60):.1f}s"
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
################ Model Training helpers #####################################
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
def save_model_results(ckpt_dir: str, result: dict):
|
| 398 |
+
"""
|
| 399 |
+
Format the results dict so that no error occurrs when saving the json.
|
| 400 |
+
|
| 401 |
+
Parameters
|
| 402 |
+
----------
|
| 403 |
+
ckpt_dir : str
|
| 404 |
+
checkpoint path
|
| 405 |
+
result : dict
|
| 406 |
+
training results
|
| 407 |
+
"""
|
| 408 |
+
result["fbeta"] = [float(n) for n in result["fbeta"]]
|
| 409 |
+
result["val_fbeta"] = [float(n) for n in result["val_fbeta"]]
|
| 410 |
+
result["fbeta1"] = [float(n) for n in result["fbeta1"]]
|
| 411 |
+
result["val_fbeta1"] = [float(n) for n in result["val_fbeta1"]]
|
| 412 |
+
with open(f"{ckpt_dir}/results.json", "w") as f:
|
| 413 |
+
json.dump(result, f)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def get_val_labels(
|
| 417 |
+
val_data: tf.data.Dataset, num_of_samples: int
|
| 418 |
+
) -> np.ndarray:
|
| 419 |
+
"""
|
| 420 |
+
Return all validation set labels. The dataset is batched with the dataset
|
| 421 |
+
size, thus creating one batch from the entire dataset. This batched
|
| 422 |
+
dataset is then converted to a list and its numpy attribute is returned.
|
| 423 |
+
|
| 424 |
+
Parameters
|
| 425 |
+
----------
|
| 426 |
+
val_data : tf.data.Dataset
|
| 427 |
+
validation set
|
| 428 |
+
num_of_samples : int
|
| 429 |
+
length of dataset
|
| 430 |
+
|
| 431 |
+
Returns
|
| 432 |
+
-------
|
| 433 |
+
np.ndarray
|
| 434 |
+
array of all validation set labels
|
| 435 |
+
"""
|
| 436 |
+
return list(val_data.batch(num_of_samples))[0][1].numpy()
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
############### Model Evaluation helpers ####################################
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def print_evaluation(
|
| 443 |
+
val_data: tf.data.Dataset, model: tf.keras.Sequential, batch_size: int
|
| 444 |
+
):
|
| 445 |
+
"""
|
| 446 |
+
Print evaluation results.
|
| 447 |
+
|
| 448 |
+
Parameters
|
| 449 |
+
----------
|
| 450 |
+
val_data : tf.data.Dataset
|
| 451 |
+
validation data set
|
| 452 |
+
model : tf.keras.Sequential
|
| 453 |
+
keras model
|
| 454 |
+
batch_size : int
|
| 455 |
+
batch size
|
| 456 |
+
"""
|
| 457 |
+
model.evaluate(val_data, batch_size=batch_size, verbose=2)
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def get_pr_arrays(
|
| 461 |
+
labels: np.ndarray, preds: np.ndarray, metric: str, **kwargs
|
| 462 |
+
) -> np.ndarray:
|
| 463 |
+
"""
|
| 464 |
+
Compute Precision or Recall on given set of labels and predictions.
|
| 465 |
+
Threshold values are created with 0.01 increments.
|
| 466 |
+
|
| 467 |
+
Parameters
|
| 468 |
+
----------
|
| 469 |
+
labels : np.ndarray
|
| 470 |
+
labels
|
| 471 |
+
preds : np.ndarray
|
| 472 |
+
predictions
|
| 473 |
+
metric : str
|
| 474 |
+
Metric to calculate i.e. Recall or Precision
|
| 475 |
+
|
| 476 |
+
Returns
|
| 477 |
+
-------
|
| 478 |
+
np.ndarray
|
| 479 |
+
resulting values
|
| 480 |
+
"""
|
| 481 |
+
r = getattr(tf.keras.metrics, metric)(**kwargs)
|
| 482 |
+
r.update_state(labels, preds.reshape(len(preds)))
|
| 483 |
+
return r.result().numpy()
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
############## Generate Model Annotations helpers ############################
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def get_files(
|
| 490 |
+
*, location: str = f"{conf.GEN_ANNOTS_DIR}", search_str: str = "*.wav"
|
| 491 |
+
) -> list:
|
| 492 |
+
"""
|
| 493 |
+
Find all files corresponding to given search string within a specified
|
| 494 |
+
location.
|
| 495 |
+
|
| 496 |
+
Parameters
|
| 497 |
+
----------
|
| 498 |
+
location : str, optional
|
| 499 |
+
root directory of files, by default 'generated_annotations/src'
|
| 500 |
+
search_str : str, optional
|
| 501 |
+
search string containing search pattern, for example '*.wav',
|
| 502 |
+
by default '*.wav'
|
| 503 |
+
|
| 504 |
+
Returns
|
| 505 |
+
-------
|
| 506 |
+
generator
|
| 507 |
+
list containing pathlib.Path objects of all files fitting
|
| 508 |
+
the pattern
|
| 509 |
+
"""
|
| 510 |
+
folder = Path(location)
|
| 511 |
+
return list(folder.glob(search_str))
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def window_data_for_prediction(audio: np.ndarray) -> tf.Tensor:
|
| 515 |
+
"""
|
| 516 |
+
Compute predictions based on spectrograms. First the number of context
|
| 517 |
+
windows that fit into the audio array are calculated. The result is an
|
| 518 |
+
integer unless the last section is reached, in that case the audio is
|
| 519 |
+
zero padded to fit the length of a multiple of the context window length.
|
| 520 |
+
The array is then zero padded to fit a integer multiple of the context
|
| 521 |
+
window.
|
| 522 |
+
|
| 523 |
+
Parameters
|
| 524 |
+
----------
|
| 525 |
+
audio : np.ndarray
|
| 526 |
+
1D audio array
|
| 527 |
+
|
| 528 |
+
Returns
|
| 529 |
+
-------
|
| 530 |
+
tf.Tensor
|
| 531 |
+
2D audio tensor with shape [context window length, number of windows]
|
| 532 |
+
"""
|
| 533 |
+
num = np.ceil(len(audio) / conf.CONTEXT_WIN)
|
| 534 |
+
# zero pad in case the end is reached
|
| 535 |
+
audio = [*audio, *np.zeros([int(num * conf.CONTEXT_WIN - len(audio))])]
|
| 536 |
+
wins = np.array(audio).reshape([int(num), conf.CONTEXT_WIN])
|
| 537 |
+
|
| 538 |
+
return tf.convert_to_tensor(wins)
|
| 539 |
+
|
| 540 |
+
|
| 541 |
+
def create_Raven_annotation_df(preds: np.ndarray, ind: int) -> pd.DataFrame:
|
| 542 |
+
"""
|
| 543 |
+
Create a DataFrame with column names according to the Raven annotation
|
| 544 |
+
format. The DataFrame is then filled with the corresponding values.
|
| 545 |
+
Beginning and end times for each context window, high and low frequency
|
| 546 |
+
(from config), and the prediction values. Based on the predicted values,
|
| 547 |
+
the sections with predicted labels of less than the threshold are
|
| 548 |
+
discarded.
|
| 549 |
+
|
| 550 |
+
Parameters
|
| 551 |
+
----------
|
| 552 |
+
preds : np.ndarray
|
| 553 |
+
predictions
|
| 554 |
+
ind : int
|
| 555 |
+
batch of current predictions (in case predictions are more than
|
| 556 |
+
the specified limitation for predictions)
|
| 557 |
+
|
| 558 |
+
Returns
|
| 559 |
+
-------
|
| 560 |
+
pd.DataFrame
|
| 561 |
+
annotation dataframe for current batch, filtered by threshold
|
| 562 |
+
"""
|
| 563 |
+
df = pd.DataFrame(
|
| 564 |
+
columns=[
|
| 565 |
+
"Begin Time (s)",
|
| 566 |
+
"End Time (s)",
|
| 567 |
+
"High Freq (Hz)",
|
| 568 |
+
"Low Freq (Hz)",
|
| 569 |
+
]
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
df["Begin Time (s)"] = (
|
| 573 |
+
np.arange(0, len(preds)) * conf.CONTEXT_WIN
|
| 574 |
+
) / conf.SR
|
| 575 |
+
df["End Time (s)"] = df["Begin Time (s)"] + conf.CONTEXT_WIN / conf.SR
|
| 576 |
+
|
| 577 |
+
df["Begin Time (s)"] += (ind * conf.PRED_BATCH_SIZE) / conf.SR
|
| 578 |
+
df["End Time (s)"] += (ind * conf.PRED_BATCH_SIZE) / conf.SR
|
| 579 |
+
|
| 580 |
+
df["High Freq (Hz)"] = conf.ANNOTATION_DF_FMAX
|
| 581 |
+
df["Low Freq (Hz)"] = conf.ANNOTATION_DF_FMIN
|
| 582 |
+
df[conf.ANNOTATION_COLUMN] = preds
|
| 583 |
+
|
| 584 |
+
return df.iloc[preds.reshape([len(preds)]) > conf.DEFAULT_THRESH]
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
def create_annotation_df(
|
| 588 |
+
audio_batches: np.ndarray,
|
| 589 |
+
model: tf.keras.Sequential,
|
| 590 |
+
callbacks: None = None,
|
| 591 |
+
**kwargs,
|
| 592 |
+
) -> pd.DataFrame:
|
| 593 |
+
"""
|
| 594 |
+
Create a annotation dataframe containing all necessary information to
|
| 595 |
+
be imported into a annotation program. The loaded audio batches are
|
| 596 |
+
iterated over and used to predict labels. All information is then used
|
| 597 |
+
to fill a DataFrame. After having gone through all batches, the index
|
| 598 |
+
column is set to a increasing integers named 'Selection' (convention).
|
| 599 |
+
|
| 600 |
+
Parameters
|
| 601 |
+
----------
|
| 602 |
+
audio_batches : np.ndarray
|
| 603 |
+
audio batches
|
| 604 |
+
model : tf.keras.Sequential
|
| 605 |
+
model instance to predict values
|
| 606 |
+
|
| 607 |
+
Returns
|
| 608 |
+
-------
|
| 609 |
+
pd.DataFrame
|
| 610 |
+
annotation dataframe
|
| 611 |
+
"""
|
| 612 |
+
annots = pd.DataFrame()
|
| 613 |
+
for ind, audio in enumerate(audio_batches):
|
| 614 |
+
if callbacks is not None and ind == 0:
|
| 615 |
+
callbacks = callbacks(**kwargs)
|
| 616 |
+
preds = model.predict(
|
| 617 |
+
window_data_for_prediction(audio), callbacks=callbacks
|
| 618 |
+
)
|
| 619 |
+
df = create_Raven_annotation_df(preds, ind)
|
| 620 |
+
annots = pd.concat([annots, df], ignore_index=True)
|
| 621 |
+
|
| 622 |
+
annots.index = np.arange(1, len(annots) + 1)
|
| 623 |
+
annots.index.name = "Selection"
|
| 624 |
+
return annots
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
def batch_audio(audio_flat: np.ndarray) -> np.ndarray:
|
| 628 |
+
"""
|
| 629 |
+
Divide 1D audio array into batches depending on the config parameter
|
| 630 |
+
pred_batch_size (predictions batch size) i.e. the number of windows
|
| 631 |
+
that are being simultaneously predicted.
|
| 632 |
+
|
| 633 |
+
Parameters
|
| 634 |
+
----------
|
| 635 |
+
audio_flat : np.ndarray
|
| 636 |
+
1D audio array
|
| 637 |
+
|
| 638 |
+
Returns
|
| 639 |
+
-------
|
| 640 |
+
np.ndarray
|
| 641 |
+
batched audio array
|
| 642 |
+
"""
|
| 643 |
+
if len(audio_flat) < conf.PRED_BATCH_SIZE:
|
| 644 |
+
audio_batches = [audio_flat]
|
| 645 |
+
else:
|
| 646 |
+
n = conf.PRED_BATCH_SIZE
|
| 647 |
+
audio_batches = [
|
| 648 |
+
audio_flat[i : i + n]
|
| 649 |
+
for i in range(0, len(audio_flat), conf.PRED_BATCH_SIZE)
|
| 650 |
+
]
|
| 651 |
+
return audio_batches
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
def get_directory_structure_relative_to_config_path(file):
|
| 655 |
+
return file.relative_to(conf.SOUND_FILES_SOURCE).parent
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
def get_top_dir_name_if_only_one_parent_dir(file, parent_dirs):
|
| 659 |
+
if str(parent_dirs) == ".":
|
| 660 |
+
parent_dirs = file.parent.stem
|
| 661 |
+
return parent_dirs
|
| 662 |
+
|
| 663 |
+
|
| 664 |
+
def check_top_dir_crit(parent_dirs):
|
| 665 |
+
return Path(parent_dirs).parts[0] != Path(conf.SOUND_FILES_SOURCE).stem
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
def check_no_subdir_crit(parent_dirs):
|
| 669 |
+
return len(list(Path(parent_dirs).parents)) == 1
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def check_top_dir_is_conf_top_dir():
|
| 673 |
+
return not Path(conf.SOUND_FILES_SOURCE).stem == conf.TOP_DIR_NAME
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
def manage_dir_structure(file):
|
| 677 |
+
parent_dirs = get_directory_structure_relative_to_config_path(file)
|
| 678 |
+
parent_dirs = get_top_dir_name_if_only_one_parent_dir(file, parent_dirs)
|
| 679 |
+
|
| 680 |
+
bool_top_dir_crit = check_top_dir_crit(parent_dirs)
|
| 681 |
+
bool_no_subdir = check_no_subdir_crit(parent_dirs)
|
| 682 |
+
bool_top_dir_is_conf = check_top_dir_is_conf_top_dir()
|
| 683 |
+
|
| 684 |
+
if (bool_top_dir_crit and bool_no_subdir) and bool_top_dir_is_conf:
|
| 685 |
+
parent_dirs = Path(Path(conf.SOUND_FILES_SOURCE).stem).joinpath(
|
| 686 |
+
parent_dirs
|
| 687 |
+
)
|
| 688 |
+
return parent_dirs
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def get_top_dir(parent_dirs):
|
| 692 |
+
return str(parent_dirs).split("/")[0]
|
| 693 |
+
|
| 694 |
+
|
| 695 |
+
def gen_annotations(
|
| 696 |
+
file,
|
| 697 |
+
model: tf.keras.Model,
|
| 698 |
+
mod_label: str,
|
| 699 |
+
timestamp_foldername: str,
|
| 700 |
+
**kwargs,
|
| 701 |
+
):
|
| 702 |
+
"""
|
| 703 |
+
Load audio file, instantiate model, use it to predict labels, fill a
|
| 704 |
+
dataframe with the predicted labels as well as necessary information to
|
| 705 |
+
import the annotations in a annotation program (like Raven). Finally the
|
| 706 |
+
annotations are saved as a single text file in directories corresponding
|
| 707 |
+
to the model checkpoint name within the generated annotations directory.
|
| 708 |
+
|
| 709 |
+
Parameters
|
| 710 |
+
----------
|
| 711 |
+
file : str or pathlib.Path object
|
| 712 |
+
file path
|
| 713 |
+
model : tf.keras.Model
|
| 714 |
+
tensorflow model
|
| 715 |
+
mod_label : str
|
| 716 |
+
label to clarify which model was used
|
| 717 |
+
timestamp_foldername : str
|
| 718 |
+
date time string foldername corresponding to the time the annotations were
|
| 719 |
+
computed
|
| 720 |
+
"""
|
| 721 |
+
parent_dirs = manage_dir_structure(file)
|
| 722 |
+
|
| 723 |
+
channel = get_channel(get_top_dir(parent_dirs))
|
| 724 |
+
|
| 725 |
+
audio = load_audio(file, channel)
|
| 726 |
+
if audio is None:
|
| 727 |
+
raise ImportError(
|
| 728 |
+
f"The audio file `{str(file)}` cannot be loaded. Check if file has "
|
| 729 |
+
"one of the supported endings "
|
| 730 |
+
"(wav, mp3, flac, etc.)) and is not empty."
|
| 731 |
+
)
|
| 732 |
+
audio_batches = batch_audio(audio)
|
| 733 |
+
|
| 734 |
+
annotation_df = create_annotation_df(audio_batches, model, **kwargs)
|
| 735 |
+
|
| 736 |
+
save_path = (
|
| 737 |
+
Path(conf.GEN_ANNOTS_DIR)
|
| 738 |
+
.joinpath(timestamp_foldername)
|
| 739 |
+
.joinpath(conf.THRESH_LABEL)
|
| 740 |
+
.joinpath(parent_dirs)
|
| 741 |
+
)
|
| 742 |
+
save_path.mkdir(exist_ok=True, parents=True)
|
| 743 |
+
annotation_df.to_csv(
|
| 744 |
+
save_path.joinpath(f"{file.stem}_annot_{mod_label}.txt"), sep="\t"
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
return annotation_df
|
acodet/global_config.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
##############################################################################
|
| 2 |
+
|
| 3 |
+
# THIS FILE IS ONLY MEANT TO BE EDITED IF YOU ARE SURE!
|
| 4 |
+
|
| 5 |
+
# PROGRAM FAILURE IS LIKELY TO OCCUR IF YOU ARE UNSURE OF THE
|
| 6 |
+
# CONSEQUENCES OF YOUR CHANGES.
|
| 7 |
+
|
| 8 |
+
# IF YOU HAVE CHANGED VALUES AND ARE ENCOUNTERING ERRORS,
|
| 9 |
+
# STASH YOUR CHANGES ('git stash' in a git bash console in acodet directory)
|
| 10 |
+
# AND THEN PULL AGAIN ('git pull' in a git bash console in acodet directory)
|
| 11 |
+
|
| 12 |
+
##############################################################################
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import streamlit as st
|
| 16 |
+
|
| 17 |
+
if "session_started" in st.session_state:
|
| 18 |
+
session = {**st.session_state}
|
| 19 |
+
else:
|
| 20 |
+
with open("acodet/src/tmp_session.json", "r") as f:
|
| 21 |
+
session = json.load(f)
|
| 22 |
+
|
| 23 |
+
#################### AUDIO PROCESSING PARAMETERS ###########################
|
| 24 |
+
## GLOBAL AUDIO PROCESSING PARAMETERS
|
| 25 |
+
SR = session["sample_rate"]
|
| 26 |
+
## MEL-SPECTROGRAM PARAMETERS
|
| 27 |
+
|
| 28 |
+
# FFT window length
|
| 29 |
+
STFT_FRAME_LEN = session["stft_frame_len"]
|
| 30 |
+
|
| 31 |
+
# number of time bins for mel spectrogram
|
| 32 |
+
N_TIME_BINS = session["number_of_time_bins"]
|
| 33 |
+
|
| 34 |
+
## CALCULATION OF CONTEXT WINDOW LENGTH
|
| 35 |
+
# calculation of context window in seconds to fit stft frame length
|
| 36 |
+
# and number of freq bins. From the set time length, the stft frame length
|
| 37 |
+
# is subtracted, as it stays constant. The remainder gets used to calculate
|
| 38 |
+
# the module of that value and the freuqency bins - 1 -> this then gives the
|
| 39 |
+
# correct number of samples per context window excluding the stft length,
|
| 40 |
+
# which is added subsequently.
|
| 41 |
+
set_length_samples = session["context_window_in_seconds"] * SR
|
| 42 |
+
set_length_without_stft_frame = set_length_samples - STFT_FRAME_LEN
|
| 43 |
+
set_length_fixed = set_length_without_stft_frame % (N_TIME_BINS - 1)
|
| 44 |
+
|
| 45 |
+
CONTEXT_WIN = int(
|
| 46 |
+
set_length_without_stft_frame - set_length_fixed + STFT_FRAME_LEN
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
CONTEXT_WIN_S_CORRECTED = CONTEXT_WIN / SR
|
| 50 |
+
|
| 51 |
+
# downsample every audio file to this frame rate to ensure comparability
|
| 52 |
+
DOWNSAMPLE_SR = False
|
| 53 |
+
|
| 54 |
+
## Settings for Creation of Tfrecord Dataset
|
| 55 |
+
# limit of context windows in a tfrecords file
|
| 56 |
+
TFRECS_LIM = session["tfrecs_limit_per_file"]
|
| 57 |
+
# train/test split
|
| 58 |
+
TRAIN_RATIO = session["train_ratio"]
|
| 59 |
+
# test/val split
|
| 60 |
+
TEST_VAL_RATIO = session["test_val_ratio"]
|
| 61 |
+
|
| 62 |
+
## Model Parameters
|
| 63 |
+
# threshold for predictions
|
| 64 |
+
THRESH = session["thresh"]
|
| 65 |
+
# simple limit for hourly presence
|
| 66 |
+
SIMPLE_LIMIT = session["simple_limit"]
|
| 67 |
+
# sequence criterion threshold
|
| 68 |
+
SEQUENCE_THRESH = session["sequence_thresh"]
|
| 69 |
+
# sequence criterion limit
|
| 70 |
+
SEQUENCE_LIMIT = session["sequence_limit"]
|
| 71 |
+
# number of consecutive winodws for sequence criterion
|
| 72 |
+
SEQUENCE_CON_WIN = session["sequence_con_win"]
|
| 73 |
+
# limit for colorbar for hourly annotations
|
| 74 |
+
HR_CNTS_VMAX = session["max_annots_per_hour"]
|
| 75 |
+
# prediction window limit
|
| 76 |
+
PRED_WIN_LIM = session["prediction_window_limit"]
|
| 77 |
+
|
| 78 |
+
# calculated global variables
|
| 79 |
+
FFT_HOP = (CONTEXT_WIN - STFT_FRAME_LEN) // (N_TIME_BINS - 1)
|
| 80 |
+
PRED_BATCH_SIZE = PRED_WIN_LIM * CONTEXT_WIN
|
| 81 |
+
|
| 82 |
+
## Paths
|
| 83 |
+
TFREC_DESTINATION = session["tfrecords_destination_folder"]
|
| 84 |
+
ANNOT_DEST = session["annotation_destination"]
|
| 85 |
+
REV_ANNOT_SRC = session["reviewed_annotation_source"]
|
| 86 |
+
GEN_ANNOT_SRC = session["generated_annotation_source"]
|
| 87 |
+
SOUND_FILES_SOURCE = session["sound_files_source"]
|
| 88 |
+
GEN_ANNOTS_DIR = session["generated_annotations_folder"]
|
| 89 |
+
ANNOTS_TIMESTAMP_FOLDER = session["annots_timestamp_folder"]
|
| 90 |
+
# model directory
|
| 91 |
+
MODEL_DIR = "acodet/src/models"
|
| 92 |
+
# model name
|
| 93 |
+
MODEL_NAME = session["model_name"]
|
| 94 |
+
TOP_DIR_NAME = session["top_dir_name"]
|
| 95 |
+
THRESH_LABEL = session["thresh_label"]
|
| 96 |
+
|
| 97 |
+
############# ANNOTATIONS #####################################
|
| 98 |
+
DEFAULT_THRESH = session["default_threshold"]
|
| 99 |
+
ANNOTATION_DF_FMIN = session["annotation_df_fmin"]
|
| 100 |
+
ANNOTATION_DF_FMAX = session["annotation_df_fmax"]
|
| 101 |
+
## Column Names
|
| 102 |
+
# column name for annotation prediction values
|
| 103 |
+
ANNOTATION_COLUMN = "Prediction/Comments"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
#################### RUN CONFIGURATION ######################################
|
| 107 |
+
RUN_CONFIG = session["run_config"]
|
| 108 |
+
PRESET = session["predefined_settings"]
|
| 109 |
+
|
| 110 |
+
#################### TRAINING CONFIG ########################################
|
| 111 |
+
|
| 112 |
+
MODELCLASSNAME = session["ModelClassName"]
|
| 113 |
+
BATCH_SIZE = session["batch_size"]
|
| 114 |
+
EPOCHS = session["epochs"]
|
| 115 |
+
LOAD_CKPT_PATH = session["load_ckpt_path"]
|
| 116 |
+
LOAD_G_CKPT = session["load_g_ckpt"]
|
| 117 |
+
KERAS_MOD_NAME = session["keras_mod_name"]
|
| 118 |
+
STEPS_PER_EPOCH = session["steps_per_epoch"]
|
| 119 |
+
TIME_AUGS = session["time_augs"]
|
| 120 |
+
MIXUP_AUGS = session["mixup_augs"]
|
| 121 |
+
SPEC_AUG = session["spec_aug"]
|
| 122 |
+
DATA_DESCRIPTION = session["data_description"]
|
| 123 |
+
INIT_LR = float(session["init_lr"])
|
| 124 |
+
FINAL_LR = float(session["final_lr"])
|
| 125 |
+
PRE_BLOCKS = session["pre_blocks"]
|
| 126 |
+
F_SCORE_BETA = session["f_score_beta"]
|
| 127 |
+
F_SCORE_THRESH = session["f_score_thresh"]
|
| 128 |
+
UNFREEZE = session["unfreeze"]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
##################### HOURLY PRESENCE DIR AND FILE NAMES #####################
|
| 132 |
+
|
| 133 |
+
HR_CNTS_SL = "hourly_annotation_simple_limit"
|
| 134 |
+
HR_PRS_SL = "hourly_presence_simple_limit"
|
| 135 |
+
HR_CNTS_SC = "hourly_annotation_sequence_limit"
|
| 136 |
+
HR_PRS_SC = "hourly_presence_sequence_limit"
|
| 137 |
+
HR_VAL_PATH = session["hourly_presence_validation_path"]
|
| 138 |
+
|
| 139 |
+
# column name for daily annotations (cumulative counts)
|
| 140 |
+
HR_DA_COL = "daily_annotations"
|
| 141 |
+
# column name for daily presence (binary)
|
| 142 |
+
HR_DP_COL = "Daily_Presence"
|
| 143 |
+
|
| 144 |
+
STREAMLIT = session["streamlit"]
|
acodet/hourly_presence.py
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from acodet.funcs import get_files, get_dt_filename
|
| 4 |
+
import acodet.global_config as conf
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import datetime as dt
|
| 8 |
+
import seaborn as sns
|
| 9 |
+
|
| 10 |
+
sns.set_theme()
|
| 11 |
+
sns.set_style("white")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def hourly_prs(df: pd.DataFrame, lim: int = 10):
|
| 15 |
+
"""
|
| 16 |
+
Compute hourly presence.
|
| 17 |
+
|
| 18 |
+
Parameters
|
| 19 |
+
----------
|
| 20 |
+
df : pd.DataFrame
|
| 21 |
+
dataframe containing annotations
|
| 22 |
+
lim : int, optional
|
| 23 |
+
limit for binary presence judgement, by default 10
|
| 24 |
+
|
| 25 |
+
Returns
|
| 26 |
+
-------
|
| 27 |
+
int
|
| 28 |
+
either 0 or 1 - 0 if less than lim annotations are present, 1 if more
|
| 29 |
+
"""
|
| 30 |
+
if len(df) > lim:
|
| 31 |
+
return 1
|
| 32 |
+
else:
|
| 33 |
+
return 0
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def daily_prs(df: pd.DataFrame):
|
| 37 |
+
"""
|
| 38 |
+
Compute daily presence. If at least one hour is present, the day is
|
| 39 |
+
considered present.
|
| 40 |
+
|
| 41 |
+
Parameters
|
| 42 |
+
----------
|
| 43 |
+
df : pd.Dataframe
|
| 44 |
+
dataframe containing annotations
|
| 45 |
+
|
| 46 |
+
Returns
|
| 47 |
+
-------
|
| 48 |
+
int
|
| 49 |
+
0 or 1 - 0 if no hour is present, 1 if at least one hour is present
|
| 50 |
+
"""
|
| 51 |
+
if 1 in df.loc[len(df), h_of_day_str()].values:
|
| 52 |
+
return 1
|
| 53 |
+
else:
|
| 54 |
+
return 0
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def get_val(path: str or Path):
|
| 58 |
+
"""
|
| 59 |
+
Get validation dataframe.
|
| 60 |
+
|
| 61 |
+
Parameters
|
| 62 |
+
----------
|
| 63 |
+
path : str or Path
|
| 64 |
+
path to validation dataframe
|
| 65 |
+
|
| 66 |
+
Returns
|
| 67 |
+
-------
|
| 68 |
+
pd.Dataframe
|
| 69 |
+
validation dataframe
|
| 70 |
+
"""
|
| 71 |
+
return pd.read_csv(path)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def h_of_day_str():
|
| 75 |
+
return ["%.2i:00" % i for i in np.arange(24)]
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def find_thresh05_path_in_dir(time_dir):
|
| 79 |
+
"""
|
| 80 |
+
Get corrects paths leading to thresh_0.5 directory contatining annotations.
|
| 81 |
+
Correct for incorrect paths, that already contain the thresh_0.5 path.
|
| 82 |
+
|
| 83 |
+
Parameters
|
| 84 |
+
----------
|
| 85 |
+
time_dir : str
|
| 86 |
+
if run.py is run directly, a path to a specific timestamp can be passed
|
| 87 |
+
|
| 88 |
+
Returns
|
| 89 |
+
-------
|
| 90 |
+
pathlib.Path
|
| 91 |
+
correct path leading to thresh_0.5 directory
|
| 92 |
+
"""
|
| 93 |
+
root = Path(conf.GEN_ANNOT_SRC)
|
| 94 |
+
if root.parts[-1] == conf.THRESH_LABEL:
|
| 95 |
+
root = root.parent
|
| 96 |
+
elif root.parts[-1] == "thresh_0.9":
|
| 97 |
+
root = root.parent
|
| 98 |
+
|
| 99 |
+
if not time_dir:
|
| 100 |
+
if root.joinpath(conf.THRESH_LABEL).exists():
|
| 101 |
+
path = root.joinpath(conf.THRESH_LABEL)
|
| 102 |
+
else:
|
| 103 |
+
path = root
|
| 104 |
+
else:
|
| 105 |
+
path = (
|
| 106 |
+
Path(conf.GEN_ANNOTS_DIR)
|
| 107 |
+
.joinpath(time_dir)
|
| 108 |
+
.joinpath(conf.THRESH_LABEL)
|
| 109 |
+
)
|
| 110 |
+
return path
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def init_date_tuple(files):
|
| 114 |
+
dates = list(
|
| 115 |
+
map(lambda x: get_dt_filename(x.stem.split("_annot")[0]), files)
|
| 116 |
+
)
|
| 117 |
+
date_hour_tuple = list(
|
| 118 |
+
map(lambda x: (str(x.date()), "%.2i:00" % int(x.hour)), dates)
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
return np.unique(date_hour_tuple, axis=0, return_counts=True)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def compute_hourly_pres(
|
| 125 |
+
time_dir=None,
|
| 126 |
+
thresh=conf.THRESH,
|
| 127 |
+
lim=conf.SIMPLE_LIMIT,
|
| 128 |
+
thresh_sc=conf.SEQUENCE_THRESH,
|
| 129 |
+
lim_sc=conf.SEQUENCE_LIMIT,
|
| 130 |
+
sc=False,
|
| 131 |
+
fetch_config_again=False,
|
| 132 |
+
**kwargs,
|
| 133 |
+
):
|
| 134 |
+
if fetch_config_again:
|
| 135 |
+
import importlib
|
| 136 |
+
|
| 137 |
+
importlib.reload(conf)
|
| 138 |
+
thresh = conf.THRESH
|
| 139 |
+
lim = conf.SIMPLE_LIMIT
|
| 140 |
+
thresh_sc = conf.SEQUENCE_THRESH
|
| 141 |
+
lim_sc = conf.SEQUENCE_LIMIT
|
| 142 |
+
|
| 143 |
+
path = find_thresh05_path_in_dir(time_dir)
|
| 144 |
+
|
| 145 |
+
if "multi_datasets" in conf.session:
|
| 146 |
+
directories = [
|
| 147 |
+
[d for d in p.iterdir() if d.is_dir()]
|
| 148 |
+
for p in path.iterdir()
|
| 149 |
+
if p.is_dir()
|
| 150 |
+
][0]
|
| 151 |
+
else:
|
| 152 |
+
directories = [p for p in path.iterdir() if p.is_dir()]
|
| 153 |
+
directories = [d for d in directories if not d.stem == "analysis"]
|
| 154 |
+
|
| 155 |
+
for ind, fold in enumerate(directories):
|
| 156 |
+
files = get_files(location=fold, search_str="**/*txt")
|
| 157 |
+
files.sort()
|
| 158 |
+
|
| 159 |
+
annots = return_hourly_pres_df(
|
| 160 |
+
files,
|
| 161 |
+
thresh,
|
| 162 |
+
thresh_sc,
|
| 163 |
+
lim,
|
| 164 |
+
lim_sc,
|
| 165 |
+
sc,
|
| 166 |
+
fold,
|
| 167 |
+
dir_ind=ind,
|
| 168 |
+
total_dirs=len(directories),
|
| 169 |
+
**kwargs,
|
| 170 |
+
)
|
| 171 |
+
if "save_filtered_selection_tables" in kwargs:
|
| 172 |
+
top_dir_path = path.parent.joinpath(conf.THRESH_LABEL).joinpath(
|
| 173 |
+
fold.stem
|
| 174 |
+
)
|
| 175 |
+
else:
|
| 176 |
+
top_dir_path = path.joinpath(fold.stem)
|
| 177 |
+
|
| 178 |
+
annots.df.to_csv(get_path(top_dir_path, conf.HR_PRS_SL))
|
| 179 |
+
annots.df_counts.to_csv(get_path(top_dir_path, conf.HR_CNTS_SL))
|
| 180 |
+
if not "dont_save_plot" in kwargs.keys():
|
| 181 |
+
for metric in (conf.HR_CNTS_SL, conf.HR_PRS_SL):
|
| 182 |
+
plot_hp(top_dir_path, lim, thresh, metric)
|
| 183 |
+
|
| 184 |
+
if sc:
|
| 185 |
+
annots.df_sc.to_csv(get_path(top_dir_path, conf.HR_PRS_SC))
|
| 186 |
+
annots.df_sc_cnt.to_csv(get_path(top_dir_path, conf.HR_CNTS_SC))
|
| 187 |
+
if not "dont_save_plot" in kwargs.keys():
|
| 188 |
+
for metric in (conf.HR_CNTS_SC, conf.HR_PRS_SC):
|
| 189 |
+
plot_hp(top_dir_path, lim_sc, thresh_sc, metric)
|
| 190 |
+
print("\n")
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def get_end_of_last_annotation(annotations):
|
| 194 |
+
"""
|
| 195 |
+
Get number of seconds from beginning to the end of the last annotation.
|
| 196 |
+
|
| 197 |
+
Parameters
|
| 198 |
+
----------
|
| 199 |
+
annotations : pd.DataFrame
|
| 200 |
+
annotation dataframe
|
| 201 |
+
|
| 202 |
+
Returns
|
| 203 |
+
-------
|
| 204 |
+
int or bool
|
| 205 |
+
False or number of seconds until last annotation
|
| 206 |
+
"""
|
| 207 |
+
if len(annotations) == 0:
|
| 208 |
+
return False
|
| 209 |
+
else:
|
| 210 |
+
return int(annotations["End Time (s)"].iloc[-1])
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def init_new_dt_if_exceeding_3600_s(h, date, hour):
|
| 214 |
+
"""
|
| 215 |
+
Return new date and hour string if annotations exceed an hour. This
|
| 216 |
+
ensures that hour presence is still computed even if a recording
|
| 217 |
+
exceeds an hour.
|
| 218 |
+
|
| 219 |
+
Parameters
|
| 220 |
+
----------
|
| 221 |
+
h : int
|
| 222 |
+
number of hours
|
| 223 |
+
date : str
|
| 224 |
+
date string
|
| 225 |
+
hour : str
|
| 226 |
+
hour string
|
| 227 |
+
|
| 228 |
+
Returns
|
| 229 |
+
-------
|
| 230 |
+
tuple
|
| 231 |
+
date and hour string
|
| 232 |
+
"""
|
| 233 |
+
if h > 0:
|
| 234 |
+
new_dt = dt.datetime.strptime(
|
| 235 |
+
date + hour, "%Y-%m-%d%H:00"
|
| 236 |
+
) + dt.timedelta(hours=1)
|
| 237 |
+
date = str(new_dt.date())
|
| 238 |
+
hour = "%.2i:00" % new_dt.hour
|
| 239 |
+
return date, hour
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class ProcessLimits:
|
| 243 |
+
def __init__(
|
| 244 |
+
self,
|
| 245 |
+
files,
|
| 246 |
+
thresh,
|
| 247 |
+
thresh_sc,
|
| 248 |
+
lim,
|
| 249 |
+
lim_sc,
|
| 250 |
+
sc,
|
| 251 |
+
dir_ind,
|
| 252 |
+
total_dirs,
|
| 253 |
+
return_counts,
|
| 254 |
+
):
|
| 255 |
+
"""
|
| 256 |
+
Handle processing of hourly and daily annotations counts and presence.
|
| 257 |
+
A class object is created and used to go through all files and calculate
|
| 258 |
+
the presence and annotation count metrics for a given hour within the
|
| 259 |
+
file. If more than one hour exists within a file, the metrics are counted
|
| 260 |
+
for every hour. If multiple files make up one hour they are concatenated
|
| 261 |
+
before processing. Simple limit and sequence limit processing is handled
|
| 262 |
+
by this class.
|
| 263 |
+
|
| 264 |
+
Parameters
|
| 265 |
+
----------
|
| 266 |
+
files : list
|
| 267 |
+
pathlib.Path objects linking to file path
|
| 268 |
+
thresh : float
|
| 269 |
+
threshold
|
| 270 |
+
thresh_sc : float
|
| 271 |
+
threshold for sequence limit
|
| 272 |
+
lim : int
|
| 273 |
+
limit for simple limit presence
|
| 274 |
+
lim_sc : int
|
| 275 |
+
limit for sequence limit
|
| 276 |
+
sc : bool
|
| 277 |
+
sequence limit yes or no
|
| 278 |
+
dir_ind : int
|
| 279 |
+
directory index, in case multiple dirs are processed
|
| 280 |
+
total_dirs : int
|
| 281 |
+
number of total directories
|
| 282 |
+
return_counts : bool
|
| 283 |
+
return annotation counts or only binary presence
|
| 284 |
+
"""
|
| 285 |
+
self.df = pd.DataFrame(
|
| 286 |
+
columns=["Date", conf.HR_DP_COL, *h_of_day_str()]
|
| 287 |
+
)
|
| 288 |
+
self.df_sc = self.df.copy()
|
| 289 |
+
self.df_counts = pd.DataFrame(
|
| 290 |
+
columns=["Date", conf.HR_DA_COL, *h_of_day_str()]
|
| 291 |
+
)
|
| 292 |
+
self.df_sc_cnt = self.df_counts.copy()
|
| 293 |
+
self.files = files
|
| 294 |
+
self.thresh = thresh
|
| 295 |
+
self.thresh_sc = thresh_sc
|
| 296 |
+
self.sc = sc
|
| 297 |
+
self.lim_sc = lim_sc
|
| 298 |
+
self.lim = lim
|
| 299 |
+
self.dir_ind = dir_ind
|
| 300 |
+
self.total_dirs = total_dirs
|
| 301 |
+
self.return_counts = return_counts
|
| 302 |
+
|
| 303 |
+
self.file_ind = 0
|
| 304 |
+
self.row = 0
|
| 305 |
+
self.n_prec_preds = conf.SEQUENCE_CON_WIN
|
| 306 |
+
self.n_exceed_thresh = 4
|
| 307 |
+
|
| 308 |
+
def concat_files_within_hour(self, count):
|
| 309 |
+
"""
|
| 310 |
+
Concatenate files within one hour. Relevant if multiple files make
|
| 311 |
+
up one hour.
|
| 312 |
+
|
| 313 |
+
Parameters
|
| 314 |
+
----------
|
| 315 |
+
count : int
|
| 316 |
+
number of files making up given hour
|
| 317 |
+
"""
|
| 318 |
+
self.annot_all = pd.DataFrame()
|
| 319 |
+
self.filtered_annots = pd.DataFrame()
|
| 320 |
+
for _ in range(count):
|
| 321 |
+
self.annot_all = pd.concat(
|
| 322 |
+
[
|
| 323 |
+
self.annot_all,
|
| 324 |
+
pd.read_csv(self.files[self.file_ind], sep="\t"),
|
| 325 |
+
]
|
| 326 |
+
)
|
| 327 |
+
self.file_ind += 1
|
| 328 |
+
|
| 329 |
+
def seq_crit(self, annot):
|
| 330 |
+
"""
|
| 331 |
+
Sequence limit calculation. Initially all predictions are thresholded.
|
| 332 |
+
After that two boolean arrays are created. Using the AND operator for
|
| 333 |
+
these two arrays only returns the values that pass the sequence limit.
|
| 334 |
+
This means that within the number of consecutive windows
|
| 335 |
+
(self.n_prec_anns) more than the self.lim_sc number of windows have to
|
| 336 |
+
exceed the value of self.thresh_sc. Depending on the settings this
|
| 337 |
+
function is cancelled early if only binary presence is of interest.
|
| 338 |
+
A filtered annotations dataframe is saved that only has the predictions
|
| 339 |
+
that passed the filtering process.
|
| 340 |
+
|
| 341 |
+
Parameters
|
| 342 |
+
----------
|
| 343 |
+
annot : pandas.DataFrame
|
| 344 |
+
annotations of the given hour
|
| 345 |
+
|
| 346 |
+
Returns
|
| 347 |
+
-------
|
| 348 |
+
int
|
| 349 |
+
either 1 if only binary presence is relevant or the number of
|
| 350 |
+
annotations that passed the sequence limit in the given hour
|
| 351 |
+
"""
|
| 352 |
+
sequ_crit = 0
|
| 353 |
+
annot = annot.loc[annot[conf.ANNOTATION_COLUMN] >= self.thresh_sc]
|
| 354 |
+
for i, row in annot.iterrows():
|
| 355 |
+
bool1 = 0 < (row["Begin Time (s)"] - annot["Begin Time (s)"])
|
| 356 |
+
bool2 = (
|
| 357 |
+
row["Begin Time (s)"] - annot["Begin Time (s)"]
|
| 358 |
+
) < self.n_prec_preds * conf.CONTEXT_WIN / conf.SR
|
| 359 |
+
self.prec_anns = annot.loc[bool1 * bool2]
|
| 360 |
+
if len(self.prec_anns) > self.n_exceed_thresh:
|
| 361 |
+
sequ_crit += 1
|
| 362 |
+
self.filtered_annots = pd.concat(
|
| 363 |
+
[self.filtered_annots, self.prec_anns.iloc[-1:]]
|
| 364 |
+
)
|
| 365 |
+
# this stops the function as soon as the limit is met once
|
| 366 |
+
if not self.return_counts:
|
| 367 |
+
return 1
|
| 368 |
+
return sequ_crit
|
| 369 |
+
|
| 370 |
+
def get_end_of_last_annotation(self):
|
| 371 |
+
"""
|
| 372 |
+
Get the time corresponding to the last annotation in current file.
|
| 373 |
+
"""
|
| 374 |
+
if len(self.annot_all) == 0:
|
| 375 |
+
self.end = False
|
| 376 |
+
else:
|
| 377 |
+
self.end = int(self.annot_all["End Time (s)"].iloc[-1])
|
| 378 |
+
|
| 379 |
+
def filter_files_of_hour_by_limit(self, date, hour):
|
| 380 |
+
"""
|
| 381 |
+
Process the annotation counts and binary presence for a given
|
| 382 |
+
hour in the dataset. This function is quite cryptic because there
|
| 383 |
+
are several dataframes that have to be updated to insert the
|
| 384 |
+
correct number of annotations and the binary presence value
|
| 385 |
+
for the given hour and date.
|
| 386 |
+
|
| 387 |
+
Parameters
|
| 388 |
+
----------
|
| 389 |
+
date : string
|
| 390 |
+
date string
|
| 391 |
+
hour : string
|
| 392 |
+
hour string
|
| 393 |
+
"""
|
| 394 |
+
for h in range(0, self.end or 1, 3600):
|
| 395 |
+
fil_h_ann = self.annot_all.loc[
|
| 396 |
+
(h < self.annot_all["Begin Time (s)"])
|
| 397 |
+
& (self.annot_all["Begin Time (s)"] < h + 3600)
|
| 398 |
+
]
|
| 399 |
+
date, hour = init_new_dt_if_exceeding_3600_s(h, date, hour)
|
| 400 |
+
|
| 401 |
+
fil_h_ann = fil_h_ann.loc[
|
| 402 |
+
fil_h_ann[conf.ANNOTATION_COLUMN] >= self.thresh
|
| 403 |
+
]
|
| 404 |
+
if not date in self.df["Date"].values:
|
| 405 |
+
if not self.row == 0:
|
| 406 |
+
self.df.loc[self.row, conf.HR_DP_COL] = daily_prs(self.df)
|
| 407 |
+
self.df_counts.loc[self.row, conf.HR_DA_COL] = sum(
|
| 408 |
+
self.df_counts.loc[
|
| 409 |
+
len(self.df_counts), h_of_day_str()
|
| 410 |
+
].values
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
if self.sc:
|
| 414 |
+
self.df_sc.loc[self.row, conf.HR_DP_COL] = daily_prs(
|
| 415 |
+
self.df_sc
|
| 416 |
+
)
|
| 417 |
+
self.df_sc_cnt.loc[self.row, conf.HR_DA_COL] = sum(
|
| 418 |
+
self.df_sc_cnt.loc[
|
| 419 |
+
len(self.df_sc_cnt), h_of_day_str()
|
| 420 |
+
].values
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
self.row += 1
|
| 424 |
+
self.df.loc[self.row, "Date"] = date
|
| 425 |
+
self.df_counts.loc[self.row, "Date"] = date
|
| 426 |
+
if self.sc:
|
| 427 |
+
self.df_sc.loc[self.row, "Date"] = date
|
| 428 |
+
self.df_sc_cnt.loc[self.row, "Date"] = date
|
| 429 |
+
|
| 430 |
+
self.df.loc[self.row, hour] = hourly_prs(fil_h_ann, lim=self.lim)
|
| 431 |
+
self.df_counts.loc[self.row, hour] = len(fil_h_ann)
|
| 432 |
+
|
| 433 |
+
if self.file_ind == len(self.files):
|
| 434 |
+
self.df.loc[self.row, conf.HR_DP_COL] = daily_prs(self.df)
|
| 435 |
+
self.df_counts.loc[self.row, conf.HR_DA_COL] = sum(
|
| 436 |
+
self.df_counts.loc[
|
| 437 |
+
len(self.df_counts), h_of_day_str()
|
| 438 |
+
].values
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
if self.sc:
|
| 442 |
+
self.df_sc.loc[self.row, conf.HR_DP_COL] = daily_prs(
|
| 443 |
+
self.df_sc
|
| 444 |
+
)
|
| 445 |
+
self.df_sc_cnt.loc[self.row, conf.HR_DA_COL] = sum(
|
| 446 |
+
self.df_sc_cnt.loc[
|
| 447 |
+
len(self.df_sc_cnt), h_of_day_str()
|
| 448 |
+
].values
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
if self.sc:
|
| 452 |
+
self.df_sc_cnt.loc[self.row, hour] = self.seq_crit(fil_h_ann)
|
| 453 |
+
self.df_sc.loc[self.row, hour] = int(
|
| 454 |
+
bool(self.df_sc_cnt.loc[self.row, hour])
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
def save_filtered_selection_tables(self, dataset_path):
|
| 458 |
+
"""
|
| 459 |
+
Save the selection tables under a new directory with the
|
| 460 |
+
chosen filter settings. Depending if sequence limit is chosen
|
| 461 |
+
or not a directory name is chosen and saved in the parent
|
| 462 |
+
timestamp foldername of the current inference session.
|
| 463 |
+
|
| 464 |
+
Parameters
|
| 465 |
+
----------
|
| 466 |
+
dataset_path : pathlib.Path
|
| 467 |
+
path to dataset in current annotation timestamp folder
|
| 468 |
+
"""
|
| 469 |
+
if self.sc:
|
| 470 |
+
thresh_label = f"thresh_{self.thresh_sc}_seq_{self.lim_sc}"
|
| 471 |
+
else:
|
| 472 |
+
thresh_label = f"thresh_{self.thresh}_sim"
|
| 473 |
+
conf.THRESH_LABEL = thresh_label
|
| 474 |
+
new_thresh_path = Path(conf.GEN_ANNOT_SRC).joinpath(thresh_label)
|
| 475 |
+
new_thresh_path = new_thresh_path.joinpath(
|
| 476 |
+
self.files[self.file_ind - 1]
|
| 477 |
+
.relative_to(dataset_path.parent)
|
| 478 |
+
.parent
|
| 479 |
+
)
|
| 480 |
+
new_thresh_path.mkdir(exist_ok=True, parents=True)
|
| 481 |
+
file_path = new_thresh_path.joinpath(
|
| 482 |
+
self.files[self.file_ind - 1].stem
|
| 483 |
+
+ self.files[self.file_ind - 1].suffix
|
| 484 |
+
)
|
| 485 |
+
if not self.sc:
|
| 486 |
+
self.filtered_annots = self.annot_all.loc[
|
| 487 |
+
self.annot_all[conf.ANNOTATION_COLUMN] >= self.thresh
|
| 488 |
+
]
|
| 489 |
+
if len(self.filtered_annots) > 0:
|
| 490 |
+
self.filtered_annots.index = self.filtered_annots.Selection
|
| 491 |
+
self.filtered_annots.pop("Selection")
|
| 492 |
+
self.filtered_annots.to_csv(file_path, sep="\t")
|
| 493 |
+
|
| 494 |
+
def update_annotation_progbar(self, **kwargs):
|
| 495 |
+
"""
|
| 496 |
+
Update the annotation progbar in the corresponding streamlit widget.
|
| 497 |
+
"""
|
| 498 |
+
import streamlit as st
|
| 499 |
+
|
| 500 |
+
inner_counter = self.file_ind / len(self.files)
|
| 501 |
+
outer_couter = self.dir_ind / self.total_dirs
|
| 502 |
+
counter = inner_counter * 1 / self.total_dirs + outer_couter
|
| 503 |
+
|
| 504 |
+
if "preset" in kwargs:
|
| 505 |
+
st.session_state.progbar_update.progress(
|
| 506 |
+
counter,
|
| 507 |
+
text="Progress",
|
| 508 |
+
)
|
| 509 |
+
if counter == 1 and "update_plot" in kwargs:
|
| 510 |
+
st.write("Plot updated")
|
| 511 |
+
st.button("Update plot")
|
| 512 |
+
elif conf.PRESET == 3:
|
| 513 |
+
kwargs["progbar1"].progress(
|
| 514 |
+
counter,
|
| 515 |
+
text="Progress",
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def return_hourly_pres_df(
|
| 520 |
+
files,
|
| 521 |
+
thresh,
|
| 522 |
+
thresh_sc,
|
| 523 |
+
lim,
|
| 524 |
+
lim_sc,
|
| 525 |
+
sc,
|
| 526 |
+
path,
|
| 527 |
+
total_dirs,
|
| 528 |
+
dir_ind,
|
| 529 |
+
return_counts=True,
|
| 530 |
+
save_filtered_selection_tables=False,
|
| 531 |
+
**kwargs,
|
| 532 |
+
):
|
| 533 |
+
"""
|
| 534 |
+
Return the hourly presence and hourly annotation counts for all files
|
| 535 |
+
within the chosen dataset. Processing is handled by the ProcessLimits class
|
| 536 |
+
this is the caller function.
|
| 537 |
+
|
| 538 |
+
Parameters
|
| 539 |
+
----------
|
| 540 |
+
files : list
|
| 541 |
+
pathlib.Path objects linking to files
|
| 542 |
+
thresh : float
|
| 543 |
+
threshold
|
| 544 |
+
thresh_sc : float
|
| 545 |
+
threshold for sequence limit
|
| 546 |
+
lim : int
|
| 547 |
+
limit of simple limit for binary presence
|
| 548 |
+
lim_sc : int
|
| 549 |
+
limit for sequence limit
|
| 550 |
+
sc : bool
|
| 551 |
+
sequence limit yes or no
|
| 552 |
+
path : pathlib.Path
|
| 553 |
+
path to current dataset in annotations folder
|
| 554 |
+
total_dirs : int
|
| 555 |
+
number of directories to be annotated
|
| 556 |
+
dir_ind : int
|
| 557 |
+
index of directory
|
| 558 |
+
return_counts : bool, optional
|
| 559 |
+
only binary presence or hourly counts, by default True
|
| 560 |
+
save_filtered_selection_tables : bool, optional
|
| 561 |
+
whether to save the filtered selection tables or not, by default False
|
| 562 |
+
|
| 563 |
+
Returns
|
| 564 |
+
-------
|
| 565 |
+
ProcessLimits object
|
| 566 |
+
contains all dataframes with the hourly and daily metrics
|
| 567 |
+
"""
|
| 568 |
+
if not isinstance(path, Path):
|
| 569 |
+
path = Path(path)
|
| 570 |
+
|
| 571 |
+
tup, counts = init_date_tuple(files)
|
| 572 |
+
filt_annots = ProcessLimits(
|
| 573 |
+
files,
|
| 574 |
+
thresh,
|
| 575 |
+
thresh_sc,
|
| 576 |
+
lim,
|
| 577 |
+
lim_sc,
|
| 578 |
+
sc,
|
| 579 |
+
dir_ind,
|
| 580 |
+
total_dirs,
|
| 581 |
+
return_counts,
|
| 582 |
+
)
|
| 583 |
+
for (date, hour), count in zip(tup, counts):
|
| 584 |
+
filt_annots.concat_files_within_hour(count)
|
| 585 |
+
|
| 586 |
+
filt_annots.get_end_of_last_annotation()
|
| 587 |
+
|
| 588 |
+
filt_annots.filter_files_of_hour_by_limit(date, hour)
|
| 589 |
+
|
| 590 |
+
if save_filtered_selection_tables:
|
| 591 |
+
filt_annots.save_filtered_selection_tables(path)
|
| 592 |
+
|
| 593 |
+
print(
|
| 594 |
+
f"Computing files in {path.stem}: "
|
| 595 |
+
f"{filt_annots.file_ind}/{len(files)}",
|
| 596 |
+
end="\r",
|
| 597 |
+
)
|
| 598 |
+
if "preset" in kwargs or conf.PRESET == 3 and conf.STREAMLIT:
|
| 599 |
+
filt_annots.update_annotation_progbar(**kwargs)
|
| 600 |
+
|
| 601 |
+
return filt_annots
|
| 602 |
+
|
| 603 |
+
|
| 604 |
+
def get_path(path, metric):
|
| 605 |
+
if not path.stem == "analysis":
|
| 606 |
+
save_path = path.parent.joinpath("analysis").joinpath(path.stem)
|
| 607 |
+
else:
|
| 608 |
+
save_path = path
|
| 609 |
+
save_path.mkdir(exist_ok=True, parents=True)
|
| 610 |
+
return save_path.joinpath(f"{metric}.csv")
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def get_title(metric):
|
| 614 |
+
if "annotation" in metric:
|
| 615 |
+
return "Annotation counts for each hour"
|
| 616 |
+
elif "presence" in metric:
|
| 617 |
+
return "Hourly presence"
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def plot_hp(path, lim, thresh, metric):
|
| 621 |
+
path = get_path(path, metric)
|
| 622 |
+
df = pd.read_csv(path)
|
| 623 |
+
h_pres = df.loc[:, h_of_day_str()]
|
| 624 |
+
h_pres.index = df["Date"]
|
| 625 |
+
plt.figure(figsize=[8, 6])
|
| 626 |
+
plt.title(
|
| 627 |
+
f"{get_title(metric)}, limit={lim:.0f}, " f"threshold={thresh:.2f}"
|
| 628 |
+
)
|
| 629 |
+
if "presence" in metric:
|
| 630 |
+
d = {"vmin": 0, "vmax": 1}
|
| 631 |
+
else:
|
| 632 |
+
d = {"vmax": conf.HR_CNTS_VMAX}
|
| 633 |
+
sns.heatmap(h_pres.T, cmap="crest", **d)
|
| 634 |
+
plt.ylabel("hour of day")
|
| 635 |
+
plt.tight_layout()
|
| 636 |
+
plt.savefig(
|
| 637 |
+
path.parent.joinpath(f"{metric}_{thresh:.2f}_{lim:.0f}.png"), dpi=150
|
| 638 |
+
)
|
| 639 |
+
plt.close()
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
def calc_val_diff(
|
| 643 |
+
time_dir=None,
|
| 644 |
+
thresh=conf.THRESH,
|
| 645 |
+
lim=conf.SIMPLE_LIMIT,
|
| 646 |
+
thresh_sc=conf.SEQUENCE_THRESH,
|
| 647 |
+
lim_sc=conf.SEQUENCE_LIMIT,
|
| 648 |
+
sc=True,
|
| 649 |
+
**kwargs,
|
| 650 |
+
):
|
| 651 |
+
path = find_thresh05_path_in_dir(time_dir)
|
| 652 |
+
for ind, fold in enumerate(path.iterdir()):
|
| 653 |
+
if not fold.joinpath("analysis").joinpath(conf.HR_VAL_PATH).exists():
|
| 654 |
+
continue
|
| 655 |
+
|
| 656 |
+
df_val = get_val(fold.joinpath("analysis").joinpath(conf.HR_VAL_PATH))
|
| 657 |
+
hours_of_day = ["%.2i:00" % i for i in np.arange(24)]
|
| 658 |
+
files = get_files(
|
| 659 |
+
location=path.joinpath(fold.stem), search_str="**/*txt"
|
| 660 |
+
)
|
| 661 |
+
files.sort()
|
| 662 |
+
|
| 663 |
+
annots = return_hourly_pres_df(
|
| 664 |
+
files,
|
| 665 |
+
thresh,
|
| 666 |
+
thresh_sc,
|
| 667 |
+
lim,
|
| 668 |
+
lim_sc,
|
| 669 |
+
sc,
|
| 670 |
+
fold,
|
| 671 |
+
total_dirs=len(list(path.iterdir())),
|
| 672 |
+
dir_ind=ind + 1,
|
| 673 |
+
return_counts=False,
|
| 674 |
+
**kwargs,
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
d, incorrect, df_diff = dict(), dict(), dict()
|
| 678 |
+
for agg_met, df_metric in zip(("sl", "sq"), (annots.df, annots.df_sc)):
|
| 679 |
+
df_val.index = df_metric.index
|
| 680 |
+
df_diff.update(
|
| 681 |
+
{
|
| 682 |
+
agg_met: df_val.loc[:, hours_of_day]
|
| 683 |
+
- df_metric.loc[:, hours_of_day]
|
| 684 |
+
}
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
results = np.unique(df_diff[agg_met])
|
| 688 |
+
d.update(
|
| 689 |
+
{agg_met: dict({"true": 0, "false_pos": 0, "false_neg": 0})}
|
| 690 |
+
)
|
| 691 |
+
for met, val in zip(d[agg_met].keys(), (0, -1, 1)):
|
| 692 |
+
if val in results:
|
| 693 |
+
d[agg_met][met] = len(np.where(df_diff[agg_met] == val)[0])
|
| 694 |
+
incorrect.update(
|
| 695 |
+
{agg_met: d[agg_met]["false_pos"] + d[agg_met]["false_neg"]}
|
| 696 |
+
)
|
| 697 |
+
perf_df = pd.DataFrame(d)
|
| 698 |
+
|
| 699 |
+
print(
|
| 700 |
+
"\n",
|
| 701 |
+
"l:",
|
| 702 |
+
lim,
|
| 703 |
+
"th:",
|
| 704 |
+
thresh,
|
| 705 |
+
"incorrect:",
|
| 706 |
+
incorrect["sl"],
|
| 707 |
+
"%.2f" % (incorrect["sl"] / (len(df_diff["sl"]) * 24) * 100),
|
| 708 |
+
)
|
| 709 |
+
print(
|
| 710 |
+
"l:",
|
| 711 |
+
lim_sc,
|
| 712 |
+
"th:",
|
| 713 |
+
thresh_sc,
|
| 714 |
+
"sc_incorrect:",
|
| 715 |
+
incorrect["sq"],
|
| 716 |
+
"%.2f" % (incorrect["sq"] / (len(df_diff["sl"]) * 24) * 100),
|
| 717 |
+
)
|
| 718 |
+
|
| 719 |
+
annots.df.to_csv(
|
| 720 |
+
Path(fold)
|
| 721 |
+
.joinpath("analysis")
|
| 722 |
+
.joinpath(f"th{thresh}_l{lim}_hourly_presence.csv")
|
| 723 |
+
)
|
| 724 |
+
annots.df_sc.to_csv(
|
| 725 |
+
Path(fold)
|
| 726 |
+
.joinpath("analysis")
|
| 727 |
+
.joinpath(f"th{thresh_sc}_l{lim_sc}_hourly_pres_sequ_crit.csv")
|
| 728 |
+
)
|
| 729 |
+
df_diff["sl"].to_csv(
|
| 730 |
+
Path(fold)
|
| 731 |
+
.joinpath("analysis")
|
| 732 |
+
.joinpath(f"th{thresh}_l{lim}_diff_hourly_presence.csv")
|
| 733 |
+
)
|
| 734 |
+
df_diff["sq"].to_csv(
|
| 735 |
+
Path(fold)
|
| 736 |
+
.joinpath("analysis")
|
| 737 |
+
.joinpath(
|
| 738 |
+
f"th{thresh_sc}_l{lim_sc}_diff_hourly_pres_sequ_crit.csv"
|
| 739 |
+
)
|
| 740 |
+
)
|
| 741 |
+
perf_df.to_csv(
|
| 742 |
+
Path(fold)
|
| 743 |
+
.joinpath("analysis")
|
| 744 |
+
.joinpath(f"th{thresh_sc}_l{lim_sc}_diff_performance.csv")
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
def plot_varying_limits(annotations_path=conf.ANNOT_DEST):
|
| 749 |
+
thresh_sl, thresh_sc = 0.9, 0.9
|
| 750 |
+
for lim_sl, lim_sc in zip(np.linspace(10, 48, 20), np.linspace(1, 20, 20)):
|
| 751 |
+
for lim, thresh in zip((lim_sl, thresh_sl), (lim_sc, thresh_sc)):
|
| 752 |
+
compute_hourly_pres(
|
| 753 |
+
annotations_path,
|
| 754 |
+
thresh_sc=thresh_sc,
|
| 755 |
+
lim_sc=lim_sc,
|
| 756 |
+
thresh=thresh,
|
| 757 |
+
lim=lim,
|
| 758 |
+
)
|
| 759 |
+
for metric in (
|
| 760 |
+
conf.HR_CNTS_SC,
|
| 761 |
+
conf.HR_CNTS_SL,
|
| 762 |
+
conf.HR_PRS_SC,
|
| 763 |
+
conf.HR_PRS_SL,
|
| 764 |
+
):
|
| 765 |
+
plot_hp(annotations_path, lim, thresh, metric)
|
acodet/humpback_model_dir/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Humpback-specific model Python code
|
| 2 |
+
|
| 3 |
+
This is not part of the multispecies\_whale\_detection package proper but rather
|
| 4 |
+
related prior work being made available for reference to users who want to make
|
| 5 |
+
fine-grained modifications using the weights from the SavedModel released at
|
| 6 |
+
|
| 7 |
+
https://tfhub.dev/google/humpback\_whale/1
|
| 8 |
+
|
| 9 |
+
The best way to learn details is to read the comments in the Python source
|
| 10 |
+
files. Very basic usage:
|
| 11 |
+
|
| 12 |
+
```
|
| 13 |
+
import humpback_model
|
| 14 |
+
|
| 15 |
+
model = humpback_model.Model.load_from_tf_hub()
|
acodet/humpback_model_dir/front_end.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Functions for representing raw audio as images.
|
| 15 |
+
|
| 16 |
+
Having these in the graph allows hyperparameters involved in this representation
|
| 17 |
+
to be fixed at training time instead of needing to materialize extracted
|
| 18 |
+
features on disk.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from __future__ import absolute_import
|
| 22 |
+
from __future__ import division
|
| 23 |
+
from __future__ import print_function
|
| 24 |
+
|
| 25 |
+
import collections
|
| 26 |
+
|
| 27 |
+
import tensorflow as tf
|
| 28 |
+
from acodet import global_config as conf
|
| 29 |
+
|
| 30 |
+
Config = collections.namedtuple(
|
| 31 |
+
"Config",
|
| 32 |
+
[
|
| 33 |
+
"stft_frame_length",
|
| 34 |
+
"stft_frame_step",
|
| 35 |
+
"freq_bins",
|
| 36 |
+
"sample_rate",
|
| 37 |
+
"lower_f",
|
| 38 |
+
"upper_f",
|
| 39 |
+
],
|
| 40 |
+
)
|
| 41 |
+
"""Configuration for the front end.
|
| 42 |
+
|
| 43 |
+
Attributes:
|
| 44 |
+
stft_frame_length: The window length for the STFT in samples.
|
| 45 |
+
stft_frame_step: The number of samples from the start of one STFT snapshot to
|
| 46 |
+
the next.
|
| 47 |
+
freq_bins: The number of mel bins in the spectrogram.
|
| 48 |
+
lower_f: Lower boundary of mel bins in Hz.
|
| 49 |
+
upper_f: Upper boundary of mel bins in Hz.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
Config.__new__.__defaults__ = (
|
| 53 |
+
conf.STFT_FRAME_LEN,
|
| 54 |
+
conf.FFT_HOP,
|
| 55 |
+
64,
|
| 56 |
+
conf.SR,
|
| 57 |
+
0.0,
|
| 58 |
+
conf.SR / 2,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class MelSpectrogram(tf.keras.layers.Layer):
|
| 63 |
+
"""Keras layer that converts a waveform to an amplitude mel spectrogram."""
|
| 64 |
+
|
| 65 |
+
def __init__(self, config=None, name="mel_spectrogram"):
|
| 66 |
+
super(MelSpectrogram, self).__init__(name=name)
|
| 67 |
+
if config is None:
|
| 68 |
+
config = Config()
|
| 69 |
+
self.config = config
|
| 70 |
+
|
| 71 |
+
def get_config(self):
|
| 72 |
+
config = super().get_config()
|
| 73 |
+
config.update({key: val for key, val in config.items()})
|
| 74 |
+
return config
|
| 75 |
+
|
| 76 |
+
def build(self, input_shape):
|
| 77 |
+
self._stft = tf.keras.layers.Lambda(
|
| 78 |
+
lambda t: tf.signal.stft(
|
| 79 |
+
tf.squeeze(t, 2),
|
| 80 |
+
frame_length=self.config.stft_frame_length,
|
| 81 |
+
frame_step=self.config.stft_frame_step,
|
| 82 |
+
),
|
| 83 |
+
name="stft",
|
| 84 |
+
)
|
| 85 |
+
num_spectrogram_bins = self._stft.compute_output_shape(input_shape)[-1]
|
| 86 |
+
self._bin = tf.keras.layers.Lambda(
|
| 87 |
+
lambda t: tf.square(
|
| 88 |
+
tf.tensordot(
|
| 89 |
+
tf.abs(t),
|
| 90 |
+
tf.signal.linear_to_mel_weight_matrix(
|
| 91 |
+
num_mel_bins=self.config.freq_bins,
|
| 92 |
+
num_spectrogram_bins=num_spectrogram_bins,
|
| 93 |
+
sample_rate=self.config.sample_rate,
|
| 94 |
+
lower_edge_hertz=self.config.lower_f,
|
| 95 |
+
upper_edge_hertz=self.config.upper_f,
|
| 96 |
+
name="matrix",
|
| 97 |
+
),
|
| 98 |
+
1,
|
| 99 |
+
)
|
| 100 |
+
),
|
| 101 |
+
name="mel_bins",
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
def call(self, inputs):
|
| 105 |
+
return self._bin(self._stft(inputs))
|
acodet/humpback_model_dir/humpback_model.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""Keras implementation of the NOAA PIFSC humpback whale detection model.
|
| 15 |
+
|
| 16 |
+
The released SavedModel is available at
|
| 17 |
+
|
| 18 |
+
https://tfhub.dev/google/humpback_whale/1
|
| 19 |
+
|
| 20 |
+
Model.load_from_tf_hub is a convenience method that downloads the SavedModel and
|
| 21 |
+
initializes the Model defined here with the weights from the SavedModel.
|
| 22 |
+
|
| 23 |
+
To save redownloading, assuming a local copy is in model_dir, use:
|
| 24 |
+
|
| 25 |
+
model = humpback_model.Model()
|
| 26 |
+
model.load_weights(model_dir)
|
| 27 |
+
|
| 28 |
+
This is the Python code that was used to export that model. We release it to
|
| 29 |
+
enable users who want to do finer-grained fine-tuning than what is possible
|
| 30 |
+
with the SavedModel alone, for example freezing or removing layers.
|
| 31 |
+
|
| 32 |
+
The code implements a standard ResNet50 in Keras. Although off-the-shelf
|
| 33 |
+
implementations exist, this reimplementation was necessary to have exact control
|
| 34 |
+
over variable names, to load weights from a model that had been trained in TF1.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
import shutil
|
| 38 |
+
import tarfile
|
| 39 |
+
import tempfile
|
| 40 |
+
import time
|
| 41 |
+
from urllib import request
|
| 42 |
+
|
| 43 |
+
import tensorflow as tf
|
| 44 |
+
|
| 45 |
+
from . import front_end
|
| 46 |
+
from . import leaf_pcen
|
| 47 |
+
|
| 48 |
+
TF_HUB_URL = (
|
| 49 |
+
"https://tfhub.dev/google/humpback_whale/1?tf-hub-format=compressed"
|
| 50 |
+
)
|
| 51 |
+
NUM_CLASSES = 1
|
| 52 |
+
NUM_AUDIO_CHANNELS = 1
|
| 53 |
+
CONTEXT_WAVEFORM_SHAPE = [None, 39124, NUM_AUDIO_CHANNELS]
|
| 54 |
+
CONTEXT_SPECTROGRAM_SHAPE = [None, 128, 64]
|
| 55 |
+
EMBEDDING_DIMENSION = 2048
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def BatchNormalization(name=None):
|
| 59 |
+
"""Defaults some of the arguments of Keras' BatchNormalization layer."""
|
| 60 |
+
return tf.keras.layers.BatchNormalization(
|
| 61 |
+
epsilon=1e-4,
|
| 62 |
+
momentum=0.9997,
|
| 63 |
+
scale=False,
|
| 64 |
+
center=True,
|
| 65 |
+
name=name,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def Conv2D(filters, kernel_size, strides=(1, 1), padding="VALID", name=None):
|
| 70 |
+
"""Defaults some of the arguments of Keras' Conv2D layer."""
|
| 71 |
+
return tf.keras.layers.Conv2D(
|
| 72 |
+
filters,
|
| 73 |
+
kernel_size,
|
| 74 |
+
strides,
|
| 75 |
+
padding=padding,
|
| 76 |
+
activation=None,
|
| 77 |
+
use_bias=False,
|
| 78 |
+
name=name,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _call_layers(layers, inputs):
|
| 83 |
+
"""Applies the function composition of layers to inputs, like Sequential."""
|
| 84 |
+
t = inputs
|
| 85 |
+
for layer in layers:
|
| 86 |
+
t = layer(t)
|
| 87 |
+
return t
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class ResidualPath(tf.keras.layers.Layer):
|
| 91 |
+
"""Layer for the residual "skip" connection in a ResNet block."""
|
| 92 |
+
|
| 93 |
+
def __init__(self, num_output_channels, input_stride):
|
| 94 |
+
super(ResidualPath, self).__init__(name="residual_path")
|
| 95 |
+
self.num_output_channels = num_output_channels
|
| 96 |
+
self.input_stride = input_stride
|
| 97 |
+
|
| 98 |
+
def build(self, input_shape):
|
| 99 |
+
num_input_channels = input_shape[-1]
|
| 100 |
+
if num_input_channels != self.num_output_channels:
|
| 101 |
+
self._layers = [
|
| 102 |
+
Conv2D(
|
| 103 |
+
self.num_output_channels,
|
| 104 |
+
kernel_size=1,
|
| 105 |
+
strides=self.input_stride,
|
| 106 |
+
padding="VALID",
|
| 107 |
+
name="conv_residual",
|
| 108 |
+
),
|
| 109 |
+
BatchNormalization(name="batch_normalization_residual"),
|
| 110 |
+
]
|
| 111 |
+
else:
|
| 112 |
+
self._layers = []
|
| 113 |
+
|
| 114 |
+
def call(self, inputs):
|
| 115 |
+
return _call_layers(self._layers, inputs)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class MainPath(tf.keras.layers.Layer):
|
| 119 |
+
"""Layer for the bottleneck-and-convolutional "core" of a ResNet block."""
|
| 120 |
+
|
| 121 |
+
def __init__(self, num_inner_channels, num_output_channels, input_stride):
|
| 122 |
+
super(MainPath, self).__init__(name="main_path")
|
| 123 |
+
self.num_inner_channels = num_inner_channels
|
| 124 |
+
self.num_output_channels = num_output_channels
|
| 125 |
+
self.input_stride = input_stride
|
| 126 |
+
|
| 127 |
+
def build(self, input_shape):
|
| 128 |
+
num_input_channels = input_shape[-1]
|
| 129 |
+
if num_input_channels != self.num_output_channels:
|
| 130 |
+
bottleneck_padding = "VALID"
|
| 131 |
+
else:
|
| 132 |
+
bottleneck_padding = "SAME"
|
| 133 |
+
self._layers = [
|
| 134 |
+
Conv2D(
|
| 135 |
+
self.num_inner_channels,
|
| 136 |
+
kernel_size=1,
|
| 137 |
+
strides=self.input_stride,
|
| 138 |
+
padding=bottleneck_padding,
|
| 139 |
+
name="conv_bottleneck",
|
| 140 |
+
),
|
| 141 |
+
BatchNormalization(name="batch_normalization_bottleneck"),
|
| 142 |
+
tf.keras.layers.ReLU(name="relu_bottleneck"),
|
| 143 |
+
Conv2D(
|
| 144 |
+
self.num_inner_channels,
|
| 145 |
+
kernel_size=3,
|
| 146 |
+
strides=1,
|
| 147 |
+
padding="SAME",
|
| 148 |
+
name="conv",
|
| 149 |
+
),
|
| 150 |
+
BatchNormalization(name="batch_normalization"),
|
| 151 |
+
tf.keras.layers.ReLU(name="relu"),
|
| 152 |
+
Conv2D(
|
| 153 |
+
self.num_output_channels,
|
| 154 |
+
kernel_size=1,
|
| 155 |
+
strides=1,
|
| 156 |
+
padding="SAME",
|
| 157 |
+
name="conv_output",
|
| 158 |
+
),
|
| 159 |
+
BatchNormalization(name="batch_normalization_output"),
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
def call(self, inputs):
|
| 163 |
+
return _call_layers(self._layers, inputs)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
class Block(tf.keras.layers.Layer):
|
| 167 |
+
"""Layer for a ResNet block."""
|
| 168 |
+
|
| 169 |
+
def __init__(
|
| 170 |
+
self,
|
| 171 |
+
num_inner_channels,
|
| 172 |
+
num_output_channels,
|
| 173 |
+
input_stride=1,
|
| 174 |
+
name="block",
|
| 175 |
+
):
|
| 176 |
+
super(Block, self).__init__(name=name)
|
| 177 |
+
self.num_inner_channels = num_inner_channels
|
| 178 |
+
self.num_output_channels = num_output_channels
|
| 179 |
+
self.input_stride = input_stride
|
| 180 |
+
|
| 181 |
+
def build(self, input_shape):
|
| 182 |
+
self._residual_path = ResidualPath(
|
| 183 |
+
self.num_output_channels, self.input_stride
|
| 184 |
+
)
|
| 185 |
+
self._main_path = MainPath(
|
| 186 |
+
self.num_inner_channels,
|
| 187 |
+
self.num_output_channels,
|
| 188 |
+
self.input_stride,
|
| 189 |
+
)
|
| 190 |
+
self._activation = tf.keras.layers.ReLU(name="relu_output")
|
| 191 |
+
|
| 192 |
+
def call(self, features):
|
| 193 |
+
return self._activation(
|
| 194 |
+
self._residual_path(features) + self._main_path(features)
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class Group(tf.keras.layers.Layer):
|
| 199 |
+
"""Layer for a group of ResNet blocks with common inner and outer depths."""
|
| 200 |
+
|
| 201 |
+
def __init__(
|
| 202 |
+
self, repeats, inner_channels, output_channels, input_stride, name
|
| 203 |
+
):
|
| 204 |
+
super(Group, self).__init__(name=name)
|
| 205 |
+
assert repeats >= 1
|
| 206 |
+
self.repeats = repeats
|
| 207 |
+
self.inner_channels = inner_channels
|
| 208 |
+
self.output_channels = output_channels
|
| 209 |
+
self.input_stride = input_stride
|
| 210 |
+
|
| 211 |
+
def build(self, input_shape):
|
| 212 |
+
self._layers = [
|
| 213 |
+
Block(
|
| 214 |
+
self.inner_channels,
|
| 215 |
+
self.output_channels,
|
| 216 |
+
input_stride=self.input_stride,
|
| 217 |
+
name="block_widen",
|
| 218 |
+
)
|
| 219 |
+
]
|
| 220 |
+
for i in range(1, self.repeats):
|
| 221 |
+
self._layers.append(
|
| 222 |
+
Block(
|
| 223 |
+
self.inner_channels,
|
| 224 |
+
self.output_channels,
|
| 225 |
+
input_stride=1,
|
| 226 |
+
name=("block_" + str(i)),
|
| 227 |
+
)
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
def call(self, inputs):
|
| 231 |
+
return _call_layers(self._layers, inputs)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
class PreBlocks(tf.keras.layers.Layer):
|
| 235 |
+
"""Layer with first-layer convolutional filters."""
|
| 236 |
+
|
| 237 |
+
def build(self, input_shape):
|
| 238 |
+
self._layers = [
|
| 239 |
+
tf.keras.layers.Lambda(
|
| 240 |
+
lambda t: tf.expand_dims(t, 3), name="make_depth_one"
|
| 241 |
+
),
|
| 242 |
+
Conv2D(64, 7, padding="SAME", name="conv"),
|
| 243 |
+
BatchNormalization(name="batch_normalization"),
|
| 244 |
+
tf.keras.layers.ReLU(name="relu"),
|
| 245 |
+
tf.keras.layers.MaxPool2D(3, 2, padding="SAME", name="pool"),
|
| 246 |
+
]
|
| 247 |
+
|
| 248 |
+
def call(self, inputs):
|
| 249 |
+
return _call_layers(self._layers, inputs)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
class Embed(tf.keras.layers.Layer):
|
| 253 |
+
"""Composition of layers transforming sectrogram to embedding vector.
|
| 254 |
+
|
| 255 |
+
The spectrogram has [128, 64] [time, frequency] bins. When the weights from TF
|
| 256 |
+
Hub are used, frequency whould be on a mel scale and PCEN applied, as in the
|
| 257 |
+
implementation of Model.__init__, later.
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
def build(self, input_shape):
|
| 261 |
+
self._layers = [
|
| 262 |
+
tf.keras.layers.InputLayer(input_shape=[128, 64]),
|
| 263 |
+
PreBlocks(),
|
| 264 |
+
Group(3, 64, 256, input_stride=1, name="group"),
|
| 265 |
+
Group(4, 128, 512, input_stride=2, name="group_1"),
|
| 266 |
+
Group(6, 256, 1024, input_stride=2, name="group_2"),
|
| 267 |
+
Group(3, 512, 2048, input_stride=2, name="group_3"),
|
| 268 |
+
tf.keras.layers.GlobalAveragePooling2D(name="pool"),
|
| 269 |
+
]
|
| 270 |
+
|
| 271 |
+
def call(self, inputs):
|
| 272 |
+
return _call_layers(self._layers, inputs)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
class Model(tf.keras.Sequential):
|
| 276 |
+
"""Full humpback detection Keras model with supplementary signatures.
|
| 277 |
+
|
| 278 |
+
See "Advanced Usage" on https://tfhub.dev/google/humpback_whale/1 for details
|
| 279 |
+
on the reusable SavedModels attributes (front_end, features, logits).
|
| 280 |
+
|
| 281 |
+
The "score" method is provided for variable-length inputs.
|
| 282 |
+
"""
|
| 283 |
+
|
| 284 |
+
@classmethod
|
| 285 |
+
def load_from_tf_hub(cls):
|
| 286 |
+
"""Downloads the SavedModel and initialized this class using its weights."""
|
| 287 |
+
with tempfile.NamedTemporaryFile() as temp_file:
|
| 288 |
+
with request.urlopen(TF_HUB_URL) as response:
|
| 289 |
+
shutil.copyfileobj(response, temp_file)
|
| 290 |
+
temp_file.flush()
|
| 291 |
+
temp_file.seek(0)
|
| 292 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 293 |
+
with tarfile.open(temp_file.name, "r:gz") as tar:
|
| 294 |
+
tar.extractall(path=temp_dir)
|
| 295 |
+
model = cls()
|
| 296 |
+
model.load_weights(temp_dir)
|
| 297 |
+
return model
|
| 298 |
+
|
| 299 |
+
def __init__(self):
|
| 300 |
+
super(Model, self).__init__(
|
| 301 |
+
layers=[
|
| 302 |
+
front_end.MelSpectrogram(),
|
| 303 |
+
leaf_pcen.PCEN(
|
| 304 |
+
alpha=0.98,
|
| 305 |
+
delta=2.0,
|
| 306 |
+
root=2.0,
|
| 307 |
+
smooth_coef=0.025,
|
| 308 |
+
floor=1e-6,
|
| 309 |
+
trainable=True,
|
| 310 |
+
name="pcen",
|
| 311 |
+
),
|
| 312 |
+
Embed(),
|
| 313 |
+
tf.keras.layers.Dense(NUM_CLASSES),
|
| 314 |
+
]
|
| 315 |
+
)
|
| 316 |
+
front_end_layers = self.layers[:2]
|
| 317 |
+
self._spectrogram, self._pcen = front_end_layers
|
| 318 |
+
|
| 319 |
+
# Parts exposed through Reusable SavedModels interface.
|
| 320 |
+
self.front_end = tf.keras.Sequential(
|
| 321 |
+
[tf.keras.layers.InputLayer([None, 1])] + front_end_layers
|
| 322 |
+
)
|
| 323 |
+
self.features = tf.keras.Sequential(
|
| 324 |
+
[tf.keras.layers.InputLayer([128, 64]), self.layers[2]]
|
| 325 |
+
)
|
| 326 |
+
self.logits = tf.keras.Sequential(
|
| 327 |
+
[tf.keras.layers.InputLayer([128, 64])] + self.layers[2:]
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
@tf.function(
|
| 331 |
+
input_signature=[
|
| 332 |
+
tf.TensorSpec(shape=(None, None, 1), dtype=tf.float32),
|
| 333 |
+
tf.TensorSpec(shape=tuple(), dtype=tf.int64),
|
| 334 |
+
]
|
| 335 |
+
)
|
| 336 |
+
def score(self, waveform, context_step_samples):
|
| 337 |
+
"""Scores each context window in an arbitrary-length waveform.
|
| 338 |
+
|
| 339 |
+
This is the clip-level version of __call__. It slices out short waveform
|
| 340 |
+
context windows of the duration expected by __call__, scores them as a
|
| 341 |
+
batch, and returns the corresponding scores.
|
| 342 |
+
|
| 343 |
+
Args:
|
| 344 |
+
waveform: [batch, samples, channels == 1] Tensor of PCM audio.
|
| 345 |
+
context_step_samples: Difference between the starts of two consecutive
|
| 346 |
+
context windows, in samples.
|
| 347 |
+
|
| 348 |
+
Returns:
|
| 349 |
+
Dict {'scores': [batch, num_windows, 1]} Tensor of per-context-window
|
| 350 |
+
model outputs. (Post-sigmoid, in [0, 1].)
|
| 351 |
+
"""
|
| 352 |
+
batch_size = tf.shape(waveform)[0]
|
| 353 |
+
stft_frame_step_samples = 300
|
| 354 |
+
context_step_frames = tf.cast(
|
| 355 |
+
context_step_samples // stft_frame_step_samples, tf.dtypes.int32
|
| 356 |
+
)
|
| 357 |
+
mel_spectrogram = self._spectrogram(waveform)
|
| 358 |
+
context_duration_frames = self.features.input_shape[1]
|
| 359 |
+
context_windows = tf.signal.frame(
|
| 360 |
+
mel_spectrogram,
|
| 361 |
+
context_duration_frames,
|
| 362 |
+
context_step_frames,
|
| 363 |
+
axis=1,
|
| 364 |
+
)
|
| 365 |
+
num_windows = tf.shape(context_windows)[1]
|
| 366 |
+
windows_in_batch = tf.reshape(
|
| 367 |
+
context_windows, (-1,) + self.features.input_shape[1:]
|
| 368 |
+
)
|
| 369 |
+
per_window_pcen = self._pcen(windows_in_batch)
|
| 370 |
+
scores = tf.nn.sigmoid(self.logits(per_window_pcen))
|
| 371 |
+
return {"scores": tf.reshape(scores, [batch_size, num_windows, 1])}
|
| 372 |
+
|
| 373 |
+
@tf.function(input_signature=[])
|
| 374 |
+
def metadata(self):
|
| 375 |
+
config = self._spectrogram.config
|
| 376 |
+
return {
|
| 377 |
+
"input_sample_rate": tf.cast(config.sample_rate, tf.int64),
|
| 378 |
+
"context_width_samples": tf.cast(
|
| 379 |
+
config.stft_frame_step * (self.features.input_shape[1] - 1)
|
| 380 |
+
+ config.stft_frame_length,
|
| 381 |
+
tf.int64,
|
| 382 |
+
),
|
| 383 |
+
"class_names": tf.constant(["Mn"]),
|
| 384 |
+
}
|
acodet/humpback_model_dir/leaf_pcen.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2022 Google LLC.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
"""PCEN implementation, forked from google-research/leaf-audio."""
|
| 15 |
+
|
| 16 |
+
import tensorflow as tf
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class PCEN(tf.keras.layers.Layer):
|
| 20 |
+
"""Per-Channel Energy Normalization.
|
| 21 |
+
|
| 22 |
+
This applies a fixed or learnable normalization by an exponential moving
|
| 23 |
+
average smoother, and a compression.
|
| 24 |
+
This implementation replicates the computation of fixed_pcen and
|
| 25 |
+
trainable_pcen defined in
|
| 26 |
+
google3/audio/hearing/tts/tensorflow/python/ops/pcen_ops.py.
|
| 27 |
+
See https://arxiv.org/abs/1607.05666 for more details.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def __init__(
|
| 31 |
+
self,
|
| 32 |
+
alpha: float,
|
| 33 |
+
smooth_coef: float,
|
| 34 |
+
delta: float = 2.0,
|
| 35 |
+
root: float = 2.0,
|
| 36 |
+
floor: float = 1e-6,
|
| 37 |
+
trainable: bool = False,
|
| 38 |
+
name="PCEN",
|
| 39 |
+
):
|
| 40 |
+
"""PCEN constructor.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
alpha: float, exponent of EMA smoother
|
| 44 |
+
smooth_coef: float, smoothing coefficient of EMA
|
| 45 |
+
delta: float, bias added before compression
|
| 46 |
+
root: float, one over exponent applied for compression (r in the paper)
|
| 47 |
+
floor: float, offset added to EMA smoother
|
| 48 |
+
trainable: bool, False means fixed_pcen, True is trainable_pcen
|
| 49 |
+
name: str, name of the layer
|
| 50 |
+
"""
|
| 51 |
+
super().__init__(name=name)
|
| 52 |
+
self._alpha_init = alpha
|
| 53 |
+
self._delta_init = delta
|
| 54 |
+
self._root_init = root
|
| 55 |
+
self._smooth_coef = smooth_coef
|
| 56 |
+
self._floor = floor
|
| 57 |
+
self._trainable = trainable
|
| 58 |
+
|
| 59 |
+
def build(self, input_shape):
|
| 60 |
+
num_channels = input_shape[-1]
|
| 61 |
+
self.alpha = self.add_weight(
|
| 62 |
+
name="alpha",
|
| 63 |
+
shape=[num_channels],
|
| 64 |
+
initializer=tf.keras.initializers.Constant(self._alpha_init),
|
| 65 |
+
trainable=self._trainable,
|
| 66 |
+
)
|
| 67 |
+
self.delta = self.add_weight(
|
| 68 |
+
name="delta",
|
| 69 |
+
shape=[num_channels],
|
| 70 |
+
initializer=tf.keras.initializers.Constant(self._delta_init),
|
| 71 |
+
trainable=self._trainable,
|
| 72 |
+
)
|
| 73 |
+
self.root = self.add_weight(
|
| 74 |
+
name="root",
|
| 75 |
+
shape=[num_channels],
|
| 76 |
+
initializer=tf.keras.initializers.Constant(self._root_init),
|
| 77 |
+
trainable=self._trainable,
|
| 78 |
+
)
|
| 79 |
+
self.ema = tf.keras.layers.SimpleRNN(
|
| 80 |
+
units=num_channels,
|
| 81 |
+
activation=None,
|
| 82 |
+
use_bias=False,
|
| 83 |
+
kernel_initializer=tf.keras.initializers.Identity(
|
| 84 |
+
gain=self._smooth_coef
|
| 85 |
+
),
|
| 86 |
+
recurrent_initializer=tf.keras.initializers.Identity(
|
| 87 |
+
gain=1.0 - self._smooth_coef
|
| 88 |
+
),
|
| 89 |
+
return_sequences=True,
|
| 90 |
+
trainable=False,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
def call(self, inputs):
|
| 94 |
+
alpha = tf.math.minimum(self.alpha, 1.0)
|
| 95 |
+
root = tf.math.maximum(self.root, 1.0)
|
| 96 |
+
ema_smoother = self.ema(
|
| 97 |
+
inputs, initial_state=tf.gather(inputs, 0, axis=1)
|
| 98 |
+
)
|
| 99 |
+
one_over_root = 1.0 / root
|
| 100 |
+
output = (
|
| 101 |
+
inputs / (self._floor + ema_smoother) ** alpha + self.delta
|
| 102 |
+
) ** one_over_root - self.delta**one_over_root
|
| 103 |
+
return output
|
acodet/models.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
from tensorflow_addons import metrics
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import zipfile
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
from acodet.funcs import get_val_labels
|
| 8 |
+
from . import global_config as conf
|
| 9 |
+
from .humpback_model_dir import humpback_model
|
| 10 |
+
from .humpback_model_dir import front_end
|
| 11 |
+
from .humpback_model_dir import leaf_pcen
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ModelHelper:
|
| 15 |
+
"""
|
| 16 |
+
Helper class to provide checkpoint loading and changing of input shape.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def load_ckpt(self, ckpt_path, ckpt_name="last"):
|
| 20 |
+
if isinstance(ckpt_path, Path):
|
| 21 |
+
ckpt_path = ckpt_path.stem
|
| 22 |
+
ckpt_path = (
|
| 23 |
+
Path("../trainings").joinpath(ckpt_path).joinpath("unfreeze_no-TF")
|
| 24 |
+
) # TODO namen ändern
|
| 25 |
+
try:
|
| 26 |
+
file_path = ckpt_path.joinpath(f"cp-{ckpt_name}.ckpt.index")
|
| 27 |
+
if not file_path.exists():
|
| 28 |
+
ckpts = list(ckpt_path.glob("cp-*.ckpt*"))
|
| 29 |
+
ckpts.sort()
|
| 30 |
+
ckpt = ckpts[-1]
|
| 31 |
+
else:
|
| 32 |
+
ckpt = file_path
|
| 33 |
+
self.model.load_weights(
|
| 34 |
+
str(ckpt).replace(".index", "")
|
| 35 |
+
).expect_partial()
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print("Checkpoint not found.", e)
|
| 38 |
+
|
| 39 |
+
def change_input_to_array(self):
|
| 40 |
+
"""
|
| 41 |
+
change input layers of model after loading checkpoint so that a file
|
| 42 |
+
can be predicted based on arrays rather than spectrograms, i.e.
|
| 43 |
+
reintegrate the spectrogram creation into the model.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
model (tf.keras.Sequential): keras model
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
tf.keras.Sequential: model with new arrays as inputs
|
| 50 |
+
"""
|
| 51 |
+
model_list = self.model.layers
|
| 52 |
+
model_list.insert(0, tf.keras.layers.Input([conf.CONTEXT_WIN]))
|
| 53 |
+
model_list.insert(
|
| 54 |
+
1, tf.keras.layers.Lambda(lambda t: tf.expand_dims(t, -1))
|
| 55 |
+
)
|
| 56 |
+
model_list.insert(2, front_end.MelSpectrogram())
|
| 57 |
+
self.model = tf.keras.Sequential(
|
| 58 |
+
layers=[layer for layer in model_list]
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class HumpBackNorthAtlantic(ModelHelper):
|
| 63 |
+
"""
|
| 64 |
+
Defualt class for North Atlantic Humpback Whale Song detection. If no new
|
| 65 |
+
training is performed this class is the default class. The model is
|
| 66 |
+
currently included in the repository. The model will be extracted
|
| 67 |
+
and made ready for use.
|
| 68 |
+
|
| 69 |
+
Parameters
|
| 70 |
+
----------
|
| 71 |
+
ModelHelper : class
|
| 72 |
+
helper class providing necessary functionalities
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
def __init__(self, **kwargs):
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
def load_model(self, **kwargs):
|
| 79 |
+
if not Path(conf.MODEL_DIR).joinpath(conf.MODEL_NAME).exists():
|
| 80 |
+
for model_path in Path(conf.MODEL_DIR).iterdir():
|
| 81 |
+
if not model_path.suffix == ".zip":
|
| 82 |
+
continue
|
| 83 |
+
else:
|
| 84 |
+
with zipfile.ZipFile(model_path, "r") as model_zip:
|
| 85 |
+
model_zip.extractall(conf.MODEL_DIR)
|
| 86 |
+
self.model = tf.keras.models.load_model(
|
| 87 |
+
Path(conf.MODEL_DIR).joinpath(conf.MODEL_NAME),
|
| 88 |
+
custom_objects={"FBetaScote": metrics.FBetaScore},
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class GoogleMod(ModelHelper): # TODO change name
|
| 93 |
+
def __init__(self, **params) -> None:
|
| 94 |
+
"""
|
| 95 |
+
This class is the framework to load and flatten the model created
|
| 96 |
+
by Matthew Harvey in collaboration with Ann Allen for the
|
| 97 |
+
PIFSC HARP data
|
| 98 |
+
(https://www.frontiersin.org/article/10.3389/fmars.2021.607321).
|
| 99 |
+
|
| 100 |
+
Args:
|
| 101 |
+
params (dict): model parameters
|
| 102 |
+
"""
|
| 103 |
+
self.load_google_new(**params)
|
| 104 |
+
self.load_flat_model(**params)
|
| 105 |
+
|
| 106 |
+
def load_google_new(self, load_g_ckpt=conf.LOAD_G_CKPT, **_):
|
| 107 |
+
"""
|
| 108 |
+
Load google model architecture. By default the model weights are
|
| 109 |
+
initiated with the pretrained weights from the google checkpoint.
|
| 110 |
+
|
| 111 |
+
Args:
|
| 112 |
+
load_g_ckpt (bool, optional): Initialize model weights with Google
|
| 113 |
+
pretrained weights. Defaults to True.
|
| 114 |
+
"""
|
| 115 |
+
self.model = humpback_model.Model()
|
| 116 |
+
if load_g_ckpt:
|
| 117 |
+
self.model = self.model.load_from_tf_hub()
|
| 118 |
+
|
| 119 |
+
def load_flat_model(self, input_tensors="spectrograms", **_):
|
| 120 |
+
"""
|
| 121 |
+
Take nested model architecture from Harvey Matthew and flatten it for
|
| 122 |
+
ease of use. This way trainability of layers can be iteratively
|
| 123 |
+
defined. The model still has a nested structure. The ResNet blocks are
|
| 124 |
+
combined into layers of type Block, but because their trainability would
|
| 125 |
+
only be changed on the block level, this degree of nesting shouldn't
|
| 126 |
+
complicate the usage of the model.
|
| 127 |
+
By default the model is itiated with spectrograms of shape [128, 64] as
|
| 128 |
+
inputs. This means that spectrograms have to be precomputed.
|
| 129 |
+
Alternatively if the argument input_tensors is set to something else,
|
| 130 |
+
inputs are assumed to be audio arrays of 39124 samples length.
|
| 131 |
+
As this process is very specific to the model ascertained from
|
| 132 |
+
Mr. Harvey, layer indices are hard coded.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
input_tensors (str): input type, if not spectrograms, they are
|
| 136 |
+
assumed to be audio arrays of 39124 samples length.
|
| 137 |
+
Defaults to 'spectrograms'.
|
| 138 |
+
"""
|
| 139 |
+
# create list which will contain all the layers
|
| 140 |
+
model_list = []
|
| 141 |
+
if input_tensors == "spectrograms":
|
| 142 |
+
# add Input layer
|
| 143 |
+
model_list.append(tf.keras.layers.Input([128, 64]))
|
| 144 |
+
else:
|
| 145 |
+
# add MelSpectrogram layer
|
| 146 |
+
model_list.append(tf.keras.layers.Input([conf.CONTEXT_WIN]))
|
| 147 |
+
model_list.append(
|
| 148 |
+
tf.keras.layers.Lambda(lambda t: tf.expand_dims(t, -1))
|
| 149 |
+
)
|
| 150 |
+
model_list.append(self.model.layers[0])
|
| 151 |
+
|
| 152 |
+
# add PCEN layer
|
| 153 |
+
model_list.append(self.model.layers[1])
|
| 154 |
+
# iterate through PreBlocks
|
| 155 |
+
model_list.append(self.model.layers[2]._layers[0])
|
| 156 |
+
for layer in self.model.layers[2]._layers[1]._layers:
|
| 157 |
+
model_list.append(layer)
|
| 158 |
+
# change name, to make sure every layer has a unique name
|
| 159 |
+
num_preproc_layers = len(model_list)
|
| 160 |
+
model_list[num_preproc_layers - 1]._name = "pool_pre_resnet"
|
| 161 |
+
# iterate over ResNet blocks
|
| 162 |
+
c = 0
|
| 163 |
+
for i, high_layer in enumerate(self.model.layers[2]._layers[2:6]):
|
| 164 |
+
for j, layer in enumerate(high_layer._layers):
|
| 165 |
+
c += 1
|
| 166 |
+
model_list.append(layer)
|
| 167 |
+
model_list[num_preproc_layers - 1 + c]._name += f"_{i}"
|
| 168 |
+
# add final Dense layers
|
| 169 |
+
model_list.append(self.model.layers[2]._layers[-1])
|
| 170 |
+
model_list.append(self.model.layers[-1])
|
| 171 |
+
# normalize results between 0 and 1
|
| 172 |
+
model_list.append(tf.keras.layers.Activation("sigmoid"))
|
| 173 |
+
|
| 174 |
+
# generate new model
|
| 175 |
+
self.model = tf.keras.Sequential(
|
| 176 |
+
layers=[layer for layer in model_list]
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class KerasAppModel(ModelHelper):
|
| 181 |
+
"""
|
| 182 |
+
Class providing functionalities for usage of standard keras application
|
| 183 |
+
models like EfficientNet. The keras application name is passed and
|
| 184 |
+
the helper class is then used in case existing checkpoints need to be
|
| 185 |
+
loaded or the shape of the input array needs change.
|
| 186 |
+
|
| 187 |
+
Parameters
|
| 188 |
+
----------
|
| 189 |
+
ModelHelper : class
|
| 190 |
+
helper class providing necessary functionalities
|
| 191 |
+
"""
|
| 192 |
+
|
| 193 |
+
def __init__(self, keras_mod_name="EfficientNetB0", **params) -> None:
|
| 194 |
+
keras_model = getattr(tf.keras.applications, keras_mod_name)(
|
| 195 |
+
include_top=True,
|
| 196 |
+
weights=None,
|
| 197 |
+
input_tensor=None,
|
| 198 |
+
input_shape=[128, 64, 3],
|
| 199 |
+
pooling=None,
|
| 200 |
+
classes=1,
|
| 201 |
+
classifier_activation="sigmoid",
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
self.model = tf.keras.Sequential(
|
| 205 |
+
[
|
| 206 |
+
tf.keras.layers.Input([128, 64]),
|
| 207 |
+
leaf_pcen.PCEN(
|
| 208 |
+
alpha=0.98,
|
| 209 |
+
delta=2.0,
|
| 210 |
+
root=2.0,
|
| 211 |
+
smooth_coef=0.025,
|
| 212 |
+
floor=1e-6,
|
| 213 |
+
trainable=True,
|
| 214 |
+
name="pcen",
|
| 215 |
+
),
|
| 216 |
+
# tf.keras.layers.Lambda(lambda t: 255. *t /tf.math.reduce_max(t)),
|
| 217 |
+
tf.keras.layers.Lambda(
|
| 218 |
+
lambda t: tf.tile(
|
| 219 |
+
tf.expand_dims(t, -1), [1 for _ in range(3)] + [3]
|
| 220 |
+
)
|
| 221 |
+
),
|
| 222 |
+
keras_model,
|
| 223 |
+
]
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def init_model(
|
| 228 |
+
model_name: str = conf.MODELCLASSNAME,
|
| 229 |
+
training_path: str = conf.LOAD_CKPT_PATH,
|
| 230 |
+
input_specs=False,
|
| 231 |
+
**kwargs,
|
| 232 |
+
) -> tf.keras.Sequential:
|
| 233 |
+
"""
|
| 234 |
+
Initiate model instance, load weights. As the model is trained on
|
| 235 |
+
spectrogram tensors but will now be used for inference on audio files
|
| 236 |
+
containing continuous audio arrays, the input shape of the model is
|
| 237 |
+
changed after the model weights have been loaded.
|
| 238 |
+
|
| 239 |
+
Parameters
|
| 240 |
+
----------
|
| 241 |
+
model_instance : type
|
| 242 |
+
callable class to create model object
|
| 243 |
+
training_path : str
|
| 244 |
+
checkpoint path
|
| 245 |
+
|
| 246 |
+
Returns
|
| 247 |
+
-------
|
| 248 |
+
tf.keras.Sequential
|
| 249 |
+
the sequential model with pretrained weights
|
| 250 |
+
"""
|
| 251 |
+
model_class = getattr(sys.modules[__name__], model_name)
|
| 252 |
+
mod_obj = model_class(**kwargs)
|
| 253 |
+
if conf.MODEL_NAME == "FlatHBNA":
|
| 254 |
+
input_specs = True
|
| 255 |
+
if model_class == HumpBackNorthAtlantic:
|
| 256 |
+
mod_obj.load_model()
|
| 257 |
+
else:
|
| 258 |
+
mod_obj.load_ckpt(training_path)
|
| 259 |
+
if not input_specs:
|
| 260 |
+
mod_obj.change_input_to_array()
|
| 261 |
+
return mod_obj.model
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def get_labels_and_preds(
|
| 265 |
+
model_name: str, training_path: str, val_data: tf.data.Dataset, **kwArgs
|
| 266 |
+
) -> tuple:
|
| 267 |
+
"""
|
| 268 |
+
Retrieve labels and predictions of validation set and given model
|
| 269 |
+
checkpoint.
|
| 270 |
+
|
| 271 |
+
Parameters
|
| 272 |
+
----------
|
| 273 |
+
model_instance : type
|
| 274 |
+
model class
|
| 275 |
+
training_path : str
|
| 276 |
+
path to checkpoint
|
| 277 |
+
val_data : tf.data.Dataset
|
| 278 |
+
validation dataset
|
| 279 |
+
|
| 280 |
+
Returns
|
| 281 |
+
-------
|
| 282 |
+
labels: np.ndarray
|
| 283 |
+
labels
|
| 284 |
+
preds: mp.ndarray
|
| 285 |
+
predictions
|
| 286 |
+
"""
|
| 287 |
+
model = init_model(
|
| 288 |
+
load_from_ckpt=True,
|
| 289 |
+
model_name=model_name,
|
| 290 |
+
training_path=training_path,
|
| 291 |
+
**kwArgs,
|
| 292 |
+
)
|
| 293 |
+
preds = model.predict(x=prep_ds_4_preds(val_data))
|
| 294 |
+
labels = get_val_labels(val_data, len(preds))
|
| 295 |
+
return labels, preds
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def prep_ds_4_preds(dataset):
|
| 299 |
+
"""
|
| 300 |
+
Prepare dataset for prediction, by batching and ensuring that only
|
| 301 |
+
arrays and corresponding labels are passed (necessary because if
|
| 302 |
+
data about the origin of the array is passed, i.e. the file path and
|
| 303 |
+
start time of the array within that file, model.predict fails.).
|
| 304 |
+
|
| 305 |
+
Parameters
|
| 306 |
+
----------
|
| 307 |
+
dataset : TfRecordDataset
|
| 308 |
+
dataset
|
| 309 |
+
|
| 310 |
+
Returns
|
| 311 |
+
-------
|
| 312 |
+
TFRecordDataset
|
| 313 |
+
prepared dataset
|
| 314 |
+
"""
|
| 315 |
+
if len(list(dataset.take(1))[0]) > 2:
|
| 316 |
+
return dataset.map(lambda x, y, z, w: (x, y)).batch(batch_size=32)
|
| 317 |
+
else:
|
| 318 |
+
return dataset.batch(batch_size=32)
|
acodet/plot_utils.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import matplotlib.pyplot as plt
|
| 3 |
+
import matplotlib.colors as mcolors
|
| 4 |
+
from matplotlib.gridspec import GridSpec
|
| 5 |
+
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import seaborn as sns
|
| 8 |
+
import json
|
| 9 |
+
import librosa as lb
|
| 10 |
+
from . import global_config as conf
|
| 11 |
+
from . import funcs
|
| 12 |
+
from . import tfrec
|
| 13 |
+
from . import models
|
| 14 |
+
import tensorflow as tf
|
| 15 |
+
|
| 16 |
+
drop_keyz = {"fbeta", "val_fbeta"}
|
| 17 |
+
sns.set_theme()
|
| 18 |
+
sns.set_style("white")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def plot_model_results(
|
| 22 |
+
datetimes, labels=None, fig=None, legend=True, **kwargs
|
| 23 |
+
):
|
| 24 |
+
plt.rc("axes", labelsize=20)
|
| 25 |
+
plt.rc("axes", titlesize=20)
|
| 26 |
+
plt.rc("xtick", labelsize=14)
|
| 27 |
+
plt.rc("ytick", labelsize=14)
|
| 28 |
+
if fig == None:
|
| 29 |
+
fig = plt.figure(figsize=(15, 8))
|
| 30 |
+
savefig = True
|
| 31 |
+
else:
|
| 32 |
+
savefig = False
|
| 33 |
+
|
| 34 |
+
if not isinstance(datetimes, list):
|
| 35 |
+
datetimes = [datetimes]
|
| 36 |
+
|
| 37 |
+
checkpoint_paths = []
|
| 38 |
+
for datetime in datetimes:
|
| 39 |
+
checkpoint_paths += list(
|
| 40 |
+
Path(f"../trainings/{datetime}").glob("unfreeze_*")
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
r, c = 2, 5
|
| 44 |
+
for j, checkpoint_path in enumerate(checkpoint_paths):
|
| 45 |
+
unfreeze = checkpoint_path.stem.split("_")[-1]
|
| 46 |
+
|
| 47 |
+
if not Path(f"{checkpoint_path}/results.json").exists():
|
| 48 |
+
if j == 0:
|
| 49 |
+
ax = fig.subplots(ncols=c, nrows=r)
|
| 50 |
+
continue
|
| 51 |
+
with open(f"{checkpoint_path}/results.json", "r") as f:
|
| 52 |
+
results = json.load(f)
|
| 53 |
+
for k in drop_keyz:
|
| 54 |
+
del results[k]
|
| 55 |
+
|
| 56 |
+
if j == 0:
|
| 57 |
+
c = len(list(results.keys())) // 2
|
| 58 |
+
ax = fig.subplots(ncols=c, nrows=r)
|
| 59 |
+
|
| 60 |
+
if not labels is None:
|
| 61 |
+
label = labels[j]
|
| 62 |
+
else:
|
| 63 |
+
label = f"{checkpoint_path.parent.stem}_{unfreeze}"
|
| 64 |
+
|
| 65 |
+
for i, key in enumerate(results.keys()):
|
| 66 |
+
if "val_" in key:
|
| 67 |
+
row = 1
|
| 68 |
+
else:
|
| 69 |
+
row = 0
|
| 70 |
+
if "loss" in key or i == 0:
|
| 71 |
+
col = 0
|
| 72 |
+
else:
|
| 73 |
+
col += 1
|
| 74 |
+
|
| 75 |
+
ax[row, col].plot(results[key], label=label)
|
| 76 |
+
if not col == 0:
|
| 77 |
+
ax[row, col].set_ylim([0.7, 1.01])
|
| 78 |
+
|
| 79 |
+
# axis handling depending on subplot index
|
| 80 |
+
if row == 1 and col == 0:
|
| 81 |
+
ax[row, col].set_ylim([0, 1.01])
|
| 82 |
+
if row == 0 and j == 0:
|
| 83 |
+
ax[row, col].set_title(f"{key}")
|
| 84 |
+
if row == col == 0:
|
| 85 |
+
ax[row, col].set_ylim([0, 0.5])
|
| 86 |
+
ax[row, col].set_ylabel("training")
|
| 87 |
+
elif row == 1 and col == 0:
|
| 88 |
+
ax[row, col].set_ylabel("val")
|
| 89 |
+
if legend and row == 1 and col == c - 1:
|
| 90 |
+
ax[row, col].legend(loc="center left", bbox_to_anchor=(1, 0.5))
|
| 91 |
+
|
| 92 |
+
info_string = ""
|
| 93 |
+
for key, val in kwargs.items():
|
| 94 |
+
info_string += f" | {key}: {val}"
|
| 95 |
+
|
| 96 |
+
ref_time = time.strftime("%Y%m%d", time.gmtime())
|
| 97 |
+
if savefig:
|
| 98 |
+
fig.tight_layout()
|
| 99 |
+
fig.savefig(f"../trainings/{datetime}/model_results_{ref_time}.png")
|
| 100 |
+
else:
|
| 101 |
+
return fig
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def plot_spec_from_file(file, start, sr, cntxt_wn_sz=39124, **kwArgs):
|
| 105 |
+
audio, sr = lb.load(
|
| 106 |
+
file, sr=sr, offset=start / sr, duration=cntxt_wn_sz / sr
|
| 107 |
+
)
|
| 108 |
+
return simple_spec(audio, sr=sr, cntxt_wn_sz=cntxt_wn_sz, **kwArgs)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def plot_sample_spectrograms(
|
| 112 |
+
dataset,
|
| 113 |
+
*,
|
| 114 |
+
dir,
|
| 115 |
+
name,
|
| 116 |
+
ds_size=None,
|
| 117 |
+
random=True,
|
| 118 |
+
seed=None,
|
| 119 |
+
sr=conf.SR,
|
| 120 |
+
rows=4,
|
| 121 |
+
cols=4,
|
| 122 |
+
plot_meta=False,
|
| 123 |
+
**kwargs,
|
| 124 |
+
):
|
| 125 |
+
r, c = rows, cols
|
| 126 |
+
if isinstance(dataset, tf.data.Dataset):
|
| 127 |
+
if random:
|
| 128 |
+
if ds_size is None:
|
| 129 |
+
ds_size = sum(1 for _ in dataset)
|
| 130 |
+
np.random.seed(seed)
|
| 131 |
+
rand_skip = np.random.randint(ds_size)
|
| 132 |
+
sample = dataset.skip(rand_skip).take(r * c)
|
| 133 |
+
else:
|
| 134 |
+
sample = dataset.take(r * c)
|
| 135 |
+
elif isinstance(dataset, list):
|
| 136 |
+
sample = dataset
|
| 137 |
+
|
| 138 |
+
max_freq_bin = 128 // (conf.SR // 2000)
|
| 139 |
+
|
| 140 |
+
fmin = sr / 2 / next(iter(sample))[0].numpy().shape[0]
|
| 141 |
+
fmax = sr / 2 / next(iter(sample))[0].numpy().shape[0] * max_freq_bin
|
| 142 |
+
fig, axes = plt.subplots(nrows=r, ncols=c, figsize=[12, 10])
|
| 143 |
+
|
| 144 |
+
for i, (aud, *lab) in enumerate(sample):
|
| 145 |
+
if i == r * c:
|
| 146 |
+
break
|
| 147 |
+
ar = aud.numpy()[:, 1:max_freq_bin].T
|
| 148 |
+
axes[i // r][i % c].imshow(
|
| 149 |
+
ar, origin="lower", interpolation="nearest", aspect="auto"
|
| 150 |
+
)
|
| 151 |
+
if len(lab) == 1:
|
| 152 |
+
axes[i // r][i % c].set_title(f"label: {lab[0]}")
|
| 153 |
+
elif len(lab) == 3:
|
| 154 |
+
label, file, t = (v.numpy() for v in lab)
|
| 155 |
+
axes[i // r][i % c].set_title(
|
| 156 |
+
f"label: {label}; t in f: {funcs.get_time(t)}\n"
|
| 157 |
+
f"file: {Path(file.decode()).stem}"
|
| 158 |
+
)
|
| 159 |
+
if i // r == r - 1 and i % c == 0:
|
| 160 |
+
axes[i // r][i % c].set_xticks(np.linspace(0, ar.shape[1], 5))
|
| 161 |
+
xlabs = np.linspace(0, 3.9, 5).astype(str)
|
| 162 |
+
axes[i // r][i % c].set_xticklabels(xlabs)
|
| 163 |
+
axes[i // r][i % c].set_xlabel("time in s")
|
| 164 |
+
axes[i // r][i % c].set_yticks(np.linspace(0, ar.shape[0] - 1, 7))
|
| 165 |
+
ylabs = np.linspace(fmin, fmax, 7).astype(int).astype(str)
|
| 166 |
+
axes[i // r][i % c].set_yticklabels(ylabs)
|
| 167 |
+
axes[i // r][i % c].set_ylabel("freq in Hz")
|
| 168 |
+
else:
|
| 169 |
+
axes[i // r][i % c].set_xticks([])
|
| 170 |
+
axes[i // r][i % c].set_xticklabels([])
|
| 171 |
+
axes[i // r][i % c].set_yticks([])
|
| 172 |
+
axes[i // r][i % c].set_yticklabels([])
|
| 173 |
+
|
| 174 |
+
fig.suptitle(f"{name} sample of 16 spectrograms. random={random}")
|
| 175 |
+
fig.savefig(f"../trainings/{dir}/{name}_sample.png")
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def simple_spec(
|
| 179 |
+
signal,
|
| 180 |
+
ax=None,
|
| 181 |
+
fft_window_length=2**11,
|
| 182 |
+
sr=10000,
|
| 183 |
+
cntxt_wn_sz=39124,
|
| 184 |
+
fig=None,
|
| 185 |
+
colorbar=True,
|
| 186 |
+
):
|
| 187 |
+
S = np.abs(lb.stft(signal, win_length=fft_window_length))
|
| 188 |
+
if not ax:
|
| 189 |
+
fig_new, ax = plt.subplots()
|
| 190 |
+
if fig:
|
| 191 |
+
fig_new = fig
|
| 192 |
+
# limit S first dimension from [10:256], thatway isolating frequencies
|
| 193 |
+
# (sr/2)/1025*10 = 48.78 to (sr/2)/1025*266 = 1297.56 for visualization
|
| 194 |
+
fmin = sr / 2 / S.shape[0] * 10
|
| 195 |
+
fmax = sr / 2 / S.shape[0] * 266
|
| 196 |
+
S_dB = lb.amplitude_to_db(S[10:266, :], ref=np.max)
|
| 197 |
+
img = lb.display.specshow(
|
| 198 |
+
S_dB,
|
| 199 |
+
x_axis="s",
|
| 200 |
+
y_axis="linear",
|
| 201 |
+
sr=sr,
|
| 202 |
+
win_length=fft_window_length,
|
| 203 |
+
ax=ax,
|
| 204 |
+
x_coords=np.linspace(0, cntxt_wn_sz / sr, S_dB.shape[1]),
|
| 205 |
+
y_coords=np.linspace(fmin, fmax, 2**8),
|
| 206 |
+
vmin=-60,
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
if colorbar:
|
| 210 |
+
fig_new.colorbar(img, ax=ax, format="%+2.0f dB")
|
| 211 |
+
return fig_new, ax
|
| 212 |
+
else:
|
| 213 |
+
return ax
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def plot_conf_matr(labels, preds, ax, iteration, title, **kwargs):
|
| 217 |
+
plt.rc("axes", titlesize=40)
|
| 218 |
+
plt.rc("font", size=32)
|
| 219 |
+
bin_preds = list(map(lambda x: 1 if x >= conf.THRESH else 0, preds))
|
| 220 |
+
heat = tf.math.confusion_matrix(labels, bin_preds).numpy()
|
| 221 |
+
rearrange = lambda x: np.array([[x[1, 1], x[1, 0]], [x[0, 1], x[0, 0]]])
|
| 222 |
+
# rearranged_head = rearrange(heat)
|
| 223 |
+
value_string = "{}\n{:.0f}%"
|
| 224 |
+
heat_annot = [[[], []], [[], []]]
|
| 225 |
+
heat_perc = [[[], []], [[], []]]
|
| 226 |
+
for row in range(2):
|
| 227 |
+
for col in range(2):
|
| 228 |
+
heat_annot[row][col] = value_string.format(
|
| 229 |
+
heat[row, col], heat[row, col] / np.sum(heat[row]) * 100
|
| 230 |
+
)
|
| 231 |
+
heat_perc[row][col] = heat[row, col] / np.sum(heat[row]) * 100
|
| 232 |
+
rearranged_annot = rearrange(np.array(heat_annot))
|
| 233 |
+
rearranged_heat = rearrange(np.array(heat_perc))
|
| 234 |
+
|
| 235 |
+
ax = sns.heatmap(
|
| 236 |
+
rearranged_heat,
|
| 237 |
+
annot=rearranged_annot,
|
| 238 |
+
fmt="",
|
| 239 |
+
cbar=False,
|
| 240 |
+
ax=ax,
|
| 241 |
+
xticklabels=False,
|
| 242 |
+
yticklabels=False,
|
| 243 |
+
)
|
| 244 |
+
ax.set_yticks([0.5, 1.5], labels=["TP", "TN"], fontsize=32)
|
| 245 |
+
ax.set_xticks([1.5, 0.5], labels=["pred. N", "pred.P"], fontsize=32)
|
| 246 |
+
color = list(sns.color_palette())[iteration]
|
| 247 |
+
ax.set_title(title, color=color)
|
| 248 |
+
return ax
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def plot_pr_curve(
|
| 252 |
+
labels, preds, ax, training_path, iteration=0, legend=True, **kwargs
|
| 253 |
+
):
|
| 254 |
+
m = dict()
|
| 255 |
+
for met in ("Recall", "Precision"):
|
| 256 |
+
threshs = list(np.linspace(0, 1, num=200)[:-1])
|
| 257 |
+
m.update(
|
| 258 |
+
{met: funcs.get_pr_arrays(labels, preds, met, thresholds=threshs)}
|
| 259 |
+
)
|
| 260 |
+
for curve in ("ROC", "PR"):
|
| 261 |
+
m.update(
|
| 262 |
+
{curve: funcs.get_pr_arrays(labels, preds, "AUC", curve=curve)}
|
| 263 |
+
)
|
| 264 |
+
perform_str = f"; AUC_PR:{m['PR']:.2f}; AUC_ROC:{m['ROC']:.2f}"
|
| 265 |
+
print(
|
| 266 |
+
"p_.5: ",
|
| 267 |
+
m["Precision"][100],
|
| 268 |
+
"\nr_.5: ",
|
| 269 |
+
m["Recall"][100],
|
| 270 |
+
"\nAUC-PR: ",
|
| 271 |
+
m["PR"],
|
| 272 |
+
"\nAUC-ROC: ",
|
| 273 |
+
m["ROC"],
|
| 274 |
+
)
|
| 275 |
+
for k, i in m.items():
|
| 276 |
+
m[k] = i.astype(float)
|
| 277 |
+
m["Recall"] = list(m["Recall"])
|
| 278 |
+
m["Precision"] = list(m["Precision"])
|
| 279 |
+
with open(f"../perform_metrics_{training_path.stem}.json", "w") as f:
|
| 280 |
+
json.dump(m, f)
|
| 281 |
+
print(perform_str)
|
| 282 |
+
if "plot_labels" in kwargs:
|
| 283 |
+
if isinstance(kwargs["plot_labels"], list):
|
| 284 |
+
label = kwargs["plot_labels"][iteration] + perform_str
|
| 285 |
+
else:
|
| 286 |
+
label = kwargs["plot_labels"] + perform_str
|
| 287 |
+
elif "load_untrained_model" in kwargs:
|
| 288 |
+
label = "untrained_model" + perform_str
|
| 289 |
+
else:
|
| 290 |
+
label = str(
|
| 291 |
+
training_path.parent.stem
|
| 292 |
+
+ training_path.stem.split("_")[-1]
|
| 293 |
+
+ perform_str
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
ax.plot(m["Recall"], m["Precision"], label=label)
|
| 297 |
+
|
| 298 |
+
ax.set_ylabel("precision", fontsize=32)
|
| 299 |
+
ax.set_xlabel("recall", fontsize=32)
|
| 300 |
+
ax.set_ylim([0, 1])
|
| 301 |
+
ax.set_xlim([0, 1])
|
| 302 |
+
ax.set_xticks([0, 0.3, 0.7, 1])
|
| 303 |
+
ax.set_yticks([0, 0.3, 0.7, 1])
|
| 304 |
+
ax.set_xticklabels(["0", "0.3", "0.7", "1"], fontsize=24)
|
| 305 |
+
ax.set_yticklabels(["0", "0.3", "0.7", "1"], fontsize=24)
|
| 306 |
+
if legend:
|
| 307 |
+
ax.legend()
|
| 308 |
+
ax.grid(True)
|
| 309 |
+
ax.set_title("PR Curve", fontsize=32)
|
| 310 |
+
return ax
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def plot_evaluation_metric(
|
| 314 |
+
model_name,
|
| 315 |
+
training_runs,
|
| 316 |
+
val_data,
|
| 317 |
+
fig,
|
| 318 |
+
plot_pr=True,
|
| 319 |
+
plot_cm=False,
|
| 320 |
+
plot_untrained=False,
|
| 321 |
+
titles=None,
|
| 322 |
+
keras_mod_name=False,
|
| 323 |
+
**kwargs,
|
| 324 |
+
):
|
| 325 |
+
r = plot_cm + plot_pr
|
| 326 |
+
c = len(training_runs)
|
| 327 |
+
if c < 1:
|
| 328 |
+
c = 1
|
| 329 |
+
gs = GridSpec(r, c, figure=fig)
|
| 330 |
+
if plot_pr:
|
| 331 |
+
ax_pr = fig.add_subplot(gs[0, :])
|
| 332 |
+
|
| 333 |
+
for i, run in enumerate(training_runs):
|
| 334 |
+
if not isinstance(model_name, list):
|
| 335 |
+
model_name = [model_name]
|
| 336 |
+
if not isinstance(keras_mod_name, list):
|
| 337 |
+
keras_mod_name = [keras_mod_name]
|
| 338 |
+
if not isinstance(titles, list):
|
| 339 |
+
title = Path(run).parent.stem + Path(run).stem.split("_")[-1]
|
| 340 |
+
else:
|
| 341 |
+
title = titles[i]
|
| 342 |
+
labels, preds = models.get_labels_and_preds(
|
| 343 |
+
model_name[i],
|
| 344 |
+
run,
|
| 345 |
+
val_data,
|
| 346 |
+
keras_mod_name=keras_mod_name[i],
|
| 347 |
+
**kwargs,
|
| 348 |
+
)
|
| 349 |
+
if not plot_pr:
|
| 350 |
+
plot_conf_matr(
|
| 351 |
+
labels,
|
| 352 |
+
preds,
|
| 353 |
+
fig.add_subplot(gs[-1, i]),
|
| 354 |
+
title=title,
|
| 355 |
+
iteration=i,
|
| 356 |
+
)
|
| 357 |
+
else:
|
| 358 |
+
ax_pr = plot_pr_curve(
|
| 359 |
+
labels, preds, ax_pr, run, iteration=i, **kwargs
|
| 360 |
+
)
|
| 361 |
+
if plot_untrained:
|
| 362 |
+
ax_pr = plot_pr_curve(
|
| 363 |
+
labels,
|
| 364 |
+
preds,
|
| 365 |
+
ax_pr,
|
| 366 |
+
run,
|
| 367 |
+
load_untrained_model=True,
|
| 368 |
+
iteration=i,
|
| 369 |
+
**kwargs,
|
| 370 |
+
)
|
| 371 |
+
if plot_cm:
|
| 372 |
+
plot_conf_matr(
|
| 373 |
+
labels,
|
| 374 |
+
preds,
|
| 375 |
+
fig.add_subplot(gs[-1, i]),
|
| 376 |
+
title=title,
|
| 377 |
+
iteration=i,
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
print("creating pr curve for ", run.stem)
|
| 381 |
+
|
| 382 |
+
if "legend" in kwargs and kwargs["legend"]:
|
| 383 |
+
ax_pr.legend(loc="center left", bbox_to_anchor=(1, 0.5))
|
| 384 |
+
return fig
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
def create_and_save_figure(
|
| 388 |
+
model_name,
|
| 389 |
+
tfrec_path,
|
| 390 |
+
batch_size,
|
| 391 |
+
train_date,
|
| 392 |
+
debug=False,
|
| 393 |
+
plot_pr=True,
|
| 394 |
+
plot_cm=False,
|
| 395 |
+
**kwargs,
|
| 396 |
+
):
|
| 397 |
+
training_runs = list(Path(f"../trainings/{train_date}").glob("unfreeze*"))
|
| 398 |
+
val_data = tfrec.run_data_pipeline(tfrec_path, "val", return_spec=False)
|
| 399 |
+
|
| 400 |
+
fig = plt.figure(constrained_layout=True)
|
| 401 |
+
|
| 402 |
+
plot_evaluation_metric(
|
| 403 |
+
model_name,
|
| 404 |
+
training_runs,
|
| 405 |
+
val_data,
|
| 406 |
+
fig=fig,
|
| 407 |
+
plot_pr=plot_pr,
|
| 408 |
+
plot_cm=plot_cm,
|
| 409 |
+
**kwargs,
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
info_string = ""
|
| 413 |
+
for key, val in kwargs.items():
|
| 414 |
+
info_string += f" | {key}: {val}"
|
| 415 |
+
fig.suptitle(f"Evaluation Metrics{info_string}")
|
| 416 |
+
|
| 417 |
+
fig.savefig(f"{training_runs[0].parent}/eval_metrics.png")
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def plot_pre_training_spectrograms(
|
| 421 |
+
train_data, test_data, augmented_data, time_start, seed
|
| 422 |
+
):
|
| 423 |
+
plot_sample_spectrograms(
|
| 424 |
+
train_data, dir=time_start, name="train_all", seed=seed
|
| 425 |
+
)
|
| 426 |
+
for i, (augmentation, aug_name) in enumerate(augmented_data):
|
| 427 |
+
plot_sample_spectrograms(
|
| 428 |
+
augmentation,
|
| 429 |
+
dir=time_start,
|
| 430 |
+
name=f"augment_{i}-{aug_name}",
|
| 431 |
+
seed=seed,
|
| 432 |
+
)
|
| 433 |
+
plot_sample_spectrograms(test_data, dir=time_start, name="test")
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def compare_random_spectrogram(filenames, dataset_size=conf.TFRECS_LIM):
|
| 437 |
+
r = np.random.randint(dataset_size)
|
| 438 |
+
dataset = (
|
| 439 |
+
tf.data.TFRecordDataset(filenames)
|
| 440 |
+
.map(tfrec.parse_tfrecord_fn)
|
| 441 |
+
.skip(r)
|
| 442 |
+
.take(1)
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
sample = next(iter(dataset))
|
| 446 |
+
aud, file, lab, time = (sample[k].numpy() for k in list(sample.keys()))
|
| 447 |
+
file = file.decode()
|
| 448 |
+
|
| 449 |
+
fig, ax = plt.subplots(ncols=2, figsize=[12, 8])
|
| 450 |
+
ax[0] = funcs.simple_spec(
|
| 451 |
+
aud, fft_window_length=512, sr=10000, ax=ax[0], colorbar=False
|
| 452 |
+
)
|
| 453 |
+
_, ax[1] = funcs.plot_spec_from_file(
|
| 454 |
+
file, ax=ax[1], start=time, fft_window_length=512, sr=10000, fig=fig
|
| 455 |
+
)
|
| 456 |
+
ax[0].set_title(
|
| 457 |
+
f"Spec of audio sample from \ntfrecords array nr. {r}"
|
| 458 |
+
f" | label: {lab}"
|
| 459 |
+
)
|
| 460 |
+
ax[1].set_title(
|
| 461 |
+
f"Spec of audio sample from file: \n{Path(file).stem}"
|
| 462 |
+
f" | time in file: {funcs.get_time(time/10000)}"
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
fig.suptitle("Comparison between tfrecord audio and file audio")
|
| 466 |
+
fig.savefig(f"{tfrec.TFRECORDS_DIR}_check_imgs/comp_{Path(file).stem}.png")
|
acodet/split_daily_annots.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from hbdet.funcs import get_dt_filename
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def create_day_dir(file_path):
|
| 8 |
+
file_path.parent.joinpath(f.stem).mkdir(exist_ok=True)
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
main_path = Path("../Annais/Blue_whales/Annotations_bluewhales")
|
| 12 |
+
|
| 13 |
+
files = list(main_path.rglob("2*.txt"))
|
| 14 |
+
counter = 0
|
| 15 |
+
for f in files:
|
| 16 |
+
data = pd.read_csv(f, sep="\t")
|
| 17 |
+
data = data[data["Comments"] == "S"]
|
| 18 |
+
if not "Begin File" in data.columns:
|
| 19 |
+
continue
|
| 20 |
+
else:
|
| 21 |
+
create_day_dir(f)
|
| 22 |
+
for beg_f in np.unique(data["Begin File"]):
|
| 23 |
+
hour_data = data[data["Begin File"] == beg_f]
|
| 24 |
+
hour = get_dt_filename(beg_f).hour
|
| 25 |
+
new_data = hour_data.copy()
|
| 26 |
+
new_data["Begin Time (s)"] -= hour * 1500
|
| 27 |
+
new_data["End Time (s)"] -= hour * 1500
|
| 28 |
+
save_str = f.parent.joinpath(f.stem).joinpath(
|
| 29 |
+
Path(beg_f).stem + "_annotated.txt"
|
| 30 |
+
)
|
| 31 |
+
new_data.to_csv(save_str, sep="\t")
|
| 32 |
+
counter += 1
|
| 33 |
+
print(
|
| 34 |
+
"most recent file:",
|
| 35 |
+
f.stem,
|
| 36 |
+
"files written: ",
|
| 37 |
+
counter,
|
| 38 |
+
end="\r",
|
| 39 |
+
)
|
acodet/src/imgs/annotation_output.png
ADDED
|
acodet/src/imgs/gui_sequence_limit.png
ADDED
|
acodet/src/imgs/sequence_limit.png
ADDED
|
acodet/tfrec.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tokenize import Intnumber
|
| 2 |
+
from functools import partial
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import tensorflow as tf
|
| 6 |
+
from . import funcs
|
| 7 |
+
import random
|
| 8 |
+
from .humpback_model_dir import front_end
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import json
|
| 11 |
+
from . import global_config as conf
|
| 12 |
+
|
| 13 |
+
########################################################
|
| 14 |
+
################# WRITING ###########################
|
| 15 |
+
########################################################
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def exclude_files_from_dataset(annots):
|
| 19 |
+
"""
|
| 20 |
+
Because some of the calls are very faint, a number of files are rexcluded
|
| 21 |
+
from the dataset to make sure that the model performance isn't obscured
|
| 22 |
+
by poor data.
|
| 23 |
+
|
| 24 |
+
Args:
|
| 25 |
+
annots (pd.DataFrame): annotations
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
pd.DataFrame: cleaned annotation dataframe
|
| 29 |
+
"""
|
| 30 |
+
exclude_files = [
|
| 31 |
+
"180324160003",
|
| 32 |
+
"PAM_20180323",
|
| 33 |
+
"PAM_20180324",
|
| 34 |
+
"PAM_20180325_0",
|
| 35 |
+
"PAM_20180325_17",
|
| 36 |
+
"PAM_20180326",
|
| 37 |
+
"PAM_20180327",
|
| 38 |
+
"PAM_20180329",
|
| 39 |
+
"PAM_20180318",
|
| 40 |
+
"PAM_20190321",
|
| 41 |
+
"20022315",
|
| 42 |
+
"20022621",
|
| 43 |
+
"210318",
|
| 44 |
+
"210319",
|
| 45 |
+
"210327",
|
| 46 |
+
]
|
| 47 |
+
drop_files = []
|
| 48 |
+
for file in np.unique(annots.filename):
|
| 49 |
+
for exclude in exclude_files:
|
| 50 |
+
if exclude in file:
|
| 51 |
+
drop_files.append(file)
|
| 52 |
+
annots.index = annots.filename
|
| 53 |
+
|
| 54 |
+
return annots.drop(annots.loc[drop_files].index), annots.loc[drop_files]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def audio_feature(list_of_floats):
|
| 58 |
+
"""
|
| 59 |
+
Returns a list of floats.
|
| 60 |
+
|
| 61 |
+
Args:
|
| 62 |
+
list_of_floats (list): list of floats
|
| 63 |
+
|
| 64 |
+
Returns:
|
| 65 |
+
tf.train.Feature: tensorflow feature containing a list of floats
|
| 66 |
+
"""
|
| 67 |
+
return tf.train.Feature(
|
| 68 |
+
float_list=tf.train.FloatList(value=list_of_floats)
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def int_feature(value):
|
| 73 |
+
"""
|
| 74 |
+
Returns a int value.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
value (int): label value
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
tf.train.Feature: tensorflow feature containing a int
|
| 81 |
+
"""
|
| 82 |
+
return tf.train.Feature(int64_list=tf.train.Int64List(value=[int(value)]))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def string_feature(value):
|
| 86 |
+
"""
|
| 87 |
+
Returns a bytes array.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
value (string): path to file
|
| 91 |
+
|
| 92 |
+
Returns:
|
| 93 |
+
tf.train.Feature: tensorflow feature containing a bytes array
|
| 94 |
+
"""
|
| 95 |
+
return tf.train.Feature(
|
| 96 |
+
bytes_list=tf.train.BytesList(value=[bytes(value, "utf-8")])
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def create_example(audio, label, file, time):
|
| 101 |
+
"""
|
| 102 |
+
Create a tensorflow Example object containing the tf Features that will
|
| 103 |
+
be saved in one file. The file will contain the audio data, corresponding
|
| 104 |
+
label, the file path corresponding to the audio, and the time in samples
|
| 105 |
+
where in the file that audio data begins. The audio data is saved
|
| 106 |
+
according to the frame rate in params, for the Google model 10 kHz.
|
| 107 |
+
The label is binary, either 0 for noise or 1 for call.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
audio (list): raw audio data of length params['cntxt_wn_sz']
|
| 111 |
+
label (int): either 1 for call or 0 for noise
|
| 112 |
+
file (string): path of file corresponding to audio data
|
| 113 |
+
time (int): time in samples when audio data begins within file
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
tf.train.Example: Example object containing all data
|
| 117 |
+
"""
|
| 118 |
+
feature = {
|
| 119 |
+
"audio": audio_feature(audio),
|
| 120 |
+
"label": int_feature(label),
|
| 121 |
+
"file": string_feature(file),
|
| 122 |
+
"time": int_feature(time),
|
| 123 |
+
}
|
| 124 |
+
return tf.train.Example(features=tf.train.Features(feature=feature))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def read_raw_file(file, annots, **kwargs):
|
| 128 |
+
"""
|
| 129 |
+
Load annotations for file, correct annotation starting times to make sure
|
| 130 |
+
that the signal is in the window center.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
file (string): path to file
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
tuple: audio segment arrays, label arrays and time arrays
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
file_annots = funcs.get_annots_for_file(annots, file)
|
| 140 |
+
|
| 141 |
+
x_call, x_noise, times_c, times_n = funcs.cntxt_wndw_arr(
|
| 142 |
+
file_annots, file, **kwargs
|
| 143 |
+
)
|
| 144 |
+
y_call = np.ones(len(x_call), dtype="float32")
|
| 145 |
+
y_noise = np.zeros(len(x_noise), dtype="float32")
|
| 146 |
+
|
| 147 |
+
return (x_call, y_call, times_c), (x_noise, y_noise, times_n)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def write_tfrecords(annots, save_dir, inbetween_noise=True, **kwargs):
|
| 151 |
+
"""
|
| 152 |
+
Write tfrecords files from wav files.
|
| 153 |
+
First the files are imported and the noise files are generated. After that
|
| 154 |
+
a loop iterates through the tuples containing the audio arrays, labels, and
|
| 155 |
+
startint times of the audio arrays within the given file. tfrecord files
|
| 156 |
+
contain no more than 600 audio arrays. The respective audio segments,
|
| 157 |
+
labels, starting times, and file paths are saved in the files.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
files (list): list of file paths to the audio files
|
| 161 |
+
"""
|
| 162 |
+
files = np.unique(annots.filename)
|
| 163 |
+
|
| 164 |
+
if len(files) == 0:
|
| 165 |
+
return
|
| 166 |
+
|
| 167 |
+
random.shuffle(files)
|
| 168 |
+
|
| 169 |
+
split_mode = "within_file"
|
| 170 |
+
specs = ["size", "noise", "calls"]
|
| 171 |
+
folders = ["train", "test", "val"]
|
| 172 |
+
train_file_index = int(len(files) * conf.TRAIN_RATIO)
|
| 173 |
+
test_file_index = int(
|
| 174 |
+
len(files) * (1 - conf.TRAIN_RATIO) * conf.TEST_VAL_RATIO
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
dataset = {k: {k1: 0 for k1 in folders} for k in specs}
|
| 178 |
+
data_meta_dict = dict({"data_split": split_mode})
|
| 179 |
+
files_dict = {}
|
| 180 |
+
tfrec_num = 0
|
| 181 |
+
for i, file in enumerate(files):
|
| 182 |
+
print(
|
| 183 |
+
"writing tf records files, progress:" f"{i/len(files)*100:.0f} %"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
if i < train_file_index:
|
| 187 |
+
folder = folders[0]
|
| 188 |
+
elif i < train_file_index + test_file_index:
|
| 189 |
+
folder = folders[1]
|
| 190 |
+
else:
|
| 191 |
+
folder = folders[2]
|
| 192 |
+
|
| 193 |
+
call_tup, noise_tup = read_raw_file(
|
| 194 |
+
file, annots, inbetween_noise=inbetween_noise, **kwargs
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
calls = randomize_arrays(call_tup, file)
|
| 198 |
+
noise = randomize_arrays(noise_tup, file)
|
| 199 |
+
samples = [*calls, *noise]
|
| 200 |
+
random.shuffle(samples)
|
| 201 |
+
data = dict()
|
| 202 |
+
|
| 203 |
+
end_tr, end_te = map(
|
| 204 |
+
lambda x: int(x * len(samples)),
|
| 205 |
+
(
|
| 206 |
+
conf.TRAIN_RATIO,
|
| 207 |
+
(1 - conf.TRAIN_RATIO) * conf.TEST_VAL_RATIO
|
| 208 |
+
+ conf.TRAIN_RATIO,
|
| 209 |
+
),
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
data["train"] = samples[:end_tr]
|
| 213 |
+
data["test"] = samples[end_tr:end_te]
|
| 214 |
+
data["val"] = samples[end_tr:-1]
|
| 215 |
+
|
| 216 |
+
for folder, samples in data.items():
|
| 217 |
+
split_by_max_length = [
|
| 218 |
+
samples[j * conf.TFRECS_LIM : (j + 1) * conf.TFRECS_LIM]
|
| 219 |
+
for j in range(len(samples) // conf.TFRECS_LIM + 1)
|
| 220 |
+
]
|
| 221 |
+
for samps in split_by_max_length:
|
| 222 |
+
tfrec_num += 1
|
| 223 |
+
writer = get_tfrecords_writer(
|
| 224 |
+
tfrec_num, folder, save_dir, **kwargs
|
| 225 |
+
)
|
| 226 |
+
files_dict, dataset = update_dict(
|
| 227 |
+
samps, files_dict, dataset, folder, tfrec_num
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
for audio, label, file, time in samps:
|
| 231 |
+
examples = create_example(audio, label, file, time)
|
| 232 |
+
writer.write(examples.SerializeToString())
|
| 233 |
+
|
| 234 |
+
# TODO automatisch die noise sachen miterstellen
|
| 235 |
+
data_meta_dict.update({"dataset": dataset})
|
| 236 |
+
data_meta_dict.update({"files": files_dict})
|
| 237 |
+
path = add_child_dirs(save_dir, **kwargs)
|
| 238 |
+
with open(path.joinpath(f"dataset_meta_{folders[0]}.json"), "w") as f:
|
| 239 |
+
json.dump(data_meta_dict, f)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def randomize_arrays(tup, file):
|
| 243 |
+
x, y, times = tup
|
| 244 |
+
|
| 245 |
+
rand = np.arange(len(x))
|
| 246 |
+
random.shuffle(rand)
|
| 247 |
+
|
| 248 |
+
return zip(x[rand], y[rand], [file] * len(x), np.array(times)[rand])
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def update_dict(samples, d, dataset_dict, folder, tfrec_num):
|
| 252 |
+
calls = sum(1 for i in samples if i[1] == 1)
|
| 253 |
+
noise = sum(1 for i in samples if i[1] == 0)
|
| 254 |
+
size = noise + calls
|
| 255 |
+
d.update(
|
| 256 |
+
{f"file_%.2i_{folder}" % tfrec_num: k for k in [size, noise, calls]}
|
| 257 |
+
)
|
| 258 |
+
for l, k in zip(("size", "calls", "noise"), (size, calls, noise)):
|
| 259 |
+
dataset_dict[l][folder] += k
|
| 260 |
+
return d, dataset_dict
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def add_child_dirs(save_dir, alt_subdir="", all_noise=False, **kwargs):
|
| 264 |
+
path = save_dir.joinpath(alt_subdir)
|
| 265 |
+
if all_noise:
|
| 266 |
+
path = path.joinpath("noise")
|
| 267 |
+
return path
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def get_tfrecords_writer(num, fold, save_dir, **kwargs):
|
| 271 |
+
"""
|
| 272 |
+
Return TFRecordWriter object to write file.
|
| 273 |
+
|
| 274 |
+
Args:
|
| 275 |
+
num (int): file number
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
TFRecordWriter object: file handle
|
| 279 |
+
"""
|
| 280 |
+
path = add_child_dirs(save_dir, **kwargs)
|
| 281 |
+
Path(path.joinpath(fold)).mkdir(parents=True, exist_ok=True)
|
| 282 |
+
return tf.io.TFRecordWriter(
|
| 283 |
+
str(path.joinpath(fold).joinpath("file_%.2i.tfrec" % num))
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def get_src_dir_structure(file, annot_dir):
|
| 288 |
+
"""
|
| 289 |
+
Return the directory structure of a file relative to the annotation
|
| 290 |
+
directory. This enables the caller to build a directory structure
|
| 291 |
+
identical to the directory structure of the source files.
|
| 292 |
+
|
| 293 |
+
Parameters
|
| 294 |
+
----------
|
| 295 |
+
file : pathlib.Path
|
| 296 |
+
file path
|
| 297 |
+
annot_dir : str
|
| 298 |
+
path to annotation directory
|
| 299 |
+
|
| 300 |
+
Returns
|
| 301 |
+
-------
|
| 302 |
+
pathlike
|
| 303 |
+
directory structure
|
| 304 |
+
"""
|
| 305 |
+
if len(list(file.relative_to(annot_dir).parents)) == 1:
|
| 306 |
+
return file.relative_to(annot_dir).parent
|
| 307 |
+
else:
|
| 308 |
+
return list(file.relative_to(annot_dir).parents)[-2]
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def write_tfrec_dataset(annot_dir=conf.ANNOT_DEST, active_learning=True):
|
| 312 |
+
annotation_files = list(Path(annot_dir).glob("**/*.csv"))
|
| 313 |
+
if active_learning:
|
| 314 |
+
inbetween_noise = False
|
| 315 |
+
else:
|
| 316 |
+
inbetween_noise = True
|
| 317 |
+
|
| 318 |
+
for file in annotation_files:
|
| 319 |
+
annots = pd.read_csv(file)
|
| 320 |
+
if "explicit_noise" in file.stem:
|
| 321 |
+
all_noise = True
|
| 322 |
+
else:
|
| 323 |
+
all_noise = False
|
| 324 |
+
|
| 325 |
+
save_dir = Path(conf.TFREC_DESTINATION).joinpath(
|
| 326 |
+
get_src_dir_structure(file, annot_dir)
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
write_tfrecords(
|
| 330 |
+
annots,
|
| 331 |
+
save_dir,
|
| 332 |
+
all_noise=all_noise,
|
| 333 |
+
inbetween_noise=inbetween_noise,
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
########################################################
|
| 338 |
+
################# READING ###########################
|
| 339 |
+
########################################################
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def parse_tfrecord_fn(example):
|
| 343 |
+
"""
|
| 344 |
+
Parser for tfrecords files.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
example (tf.train.Example instance): file containing 4 features
|
| 348 |
+
|
| 349 |
+
Returns:
|
| 350 |
+
tf.io object: tensorflow object containg the data
|
| 351 |
+
"""
|
| 352 |
+
feature_description = {
|
| 353 |
+
"audio": tf.io.FixedLenFeature([conf.CONTEXT_WIN], tf.float32),
|
| 354 |
+
"label": tf.io.FixedLenFeature([], tf.int64),
|
| 355 |
+
"file": tf.io.FixedLenFeature([], tf.string),
|
| 356 |
+
"time": tf.io.FixedLenFeature([], tf.int64),
|
| 357 |
+
}
|
| 358 |
+
return tf.io.parse_single_example(example, feature_description)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def prepare_sample(features, return_meta=False, **kwargs):
|
| 362 |
+
if not return_meta:
|
| 363 |
+
return features["audio"], features["label"]
|
| 364 |
+
else:
|
| 365 |
+
return (
|
| 366 |
+
features["audio"],
|
| 367 |
+
features["label"],
|
| 368 |
+
features["file"],
|
| 369 |
+
features["time"],
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def get_dataset(filenames, AUTOTUNE=None, **kwargs):
|
| 374 |
+
dataset = (
|
| 375 |
+
tf.data.TFRecordDataset(filenames, num_parallel_reads=AUTOTUNE)
|
| 376 |
+
.map(parse_tfrecord_fn, num_parallel_calls=AUTOTUNE)
|
| 377 |
+
.map(partial(prepare_sample, **kwargs), num_parallel_calls=AUTOTUNE)
|
| 378 |
+
)
|
| 379 |
+
return dataset
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
def run_data_pipeline(
|
| 383 |
+
root_dir, data_dir, AUTOTUNE=None, return_spec=True, **kwargs
|
| 384 |
+
):
|
| 385 |
+
if not isinstance(root_dir, list):
|
| 386 |
+
root_dir = [root_dir]
|
| 387 |
+
files = []
|
| 388 |
+
for root in root_dir:
|
| 389 |
+
if data_dir == "train":
|
| 390 |
+
files += tf.io.gfile.glob(f"{root}/{data_dir}/*.tfrec")
|
| 391 |
+
elif data_dir == "noise":
|
| 392 |
+
files += tf.io.gfile.glob(f"{root}/{data_dir}/*.tfrec")
|
| 393 |
+
files += tf.io.gfile.glob(f"{root}/noise/train/*.tfrec")
|
| 394 |
+
else:
|
| 395 |
+
try:
|
| 396 |
+
files += tf.io.gfile.glob(f"{root}/noise/{data_dir}/*.tfrec")
|
| 397 |
+
except:
|
| 398 |
+
print("No explicit noise for ", root)
|
| 399 |
+
files += tf.io.gfile.glob(f"{root}/{data_dir}/*.tfrec")
|
| 400 |
+
dataset = get_dataset(files, AUTOTUNE=AUTOTUNE, **kwargs)
|
| 401 |
+
if return_spec:
|
| 402 |
+
return make_spec_tensor(dataset)
|
| 403 |
+
else:
|
| 404 |
+
return dataset
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def spec():
|
| 408 |
+
return tf.keras.Sequential(
|
| 409 |
+
[
|
| 410 |
+
tf.keras.layers.Input([conf.CONTEXT_WIN]),
|
| 411 |
+
tf.keras.layers.Lambda(lambda t: tf.expand_dims(t, -1)),
|
| 412 |
+
front_end.MelSpectrogram(),
|
| 413 |
+
]
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def prepare(
|
| 418 |
+
ds,
|
| 419 |
+
batch_size,
|
| 420 |
+
shuffle=False,
|
| 421 |
+
shuffle_buffer=750,
|
| 422 |
+
augmented_data=None,
|
| 423 |
+
AUTOTUNE=None,
|
| 424 |
+
):
|
| 425 |
+
if shuffle:
|
| 426 |
+
ds = ds.shuffle(shuffle_buffer)
|
| 427 |
+
ds = ds.batch(batch_size)
|
| 428 |
+
return ds.prefetch(buffer_size=AUTOTUNE)
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def make_spec_tensor(ds, AUTOTUNE=None):
|
| 432 |
+
ds = ds.batch(1)
|
| 433 |
+
ds = ds.map(lambda x, *y: (spec()(x), *y), num_parallel_calls=AUTOTUNE)
|
| 434 |
+
return ds.unbatch()
|
acodet/train.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import numpy as np
|
| 5 |
+
import tensorflow as tf
|
| 6 |
+
import tensorflow_addons as tfa
|
| 7 |
+
|
| 8 |
+
from acodet.funcs import save_model_results, get_train_set_size
|
| 9 |
+
from acodet import models
|
| 10 |
+
from acodet.plot_utils import plot_model_results, create_and_save_figure
|
| 11 |
+
from acodet.tfrec import run_data_pipeline, prepare
|
| 12 |
+
from acodet.augmentation import run_augment_pipeline
|
| 13 |
+
from acodet import global_config as conf
|
| 14 |
+
|
| 15 |
+
AUTOTUNE = tf.data.AUTOTUNE
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def run_training(
|
| 19 |
+
ModelClassName=conf.MODELCLASSNAME,
|
| 20 |
+
data_dir=conf.TFREC_DESTINATION,
|
| 21 |
+
# TODO trennen destination und standardpfad - oder doch nicht?
|
| 22 |
+
batch_size=conf.BATCH_SIZE,
|
| 23 |
+
epochs=conf.EPOCHS,
|
| 24 |
+
load_ckpt_path=conf.LOAD_CKPT_PATH,
|
| 25 |
+
load_g_ckpt=conf.LOAD_G_CKPT,
|
| 26 |
+
keras_mod_name=conf.KERAS_MOD_NAME,
|
| 27 |
+
steps_per_epoch=conf.STEPS_PER_EPOCH,
|
| 28 |
+
time_augs=conf.TIME_AUGS,
|
| 29 |
+
mixup_augs=conf.MIXUP_AUGS,
|
| 30 |
+
spec_aug=conf.SPEC_AUG,
|
| 31 |
+
data_description=conf.DATA_DESCRIPTION,
|
| 32 |
+
init_lr=conf.INIT_LR,
|
| 33 |
+
final_lr=conf.FINAL_LR,
|
| 34 |
+
pre_blocks=conf.PRE_BLOCKS,
|
| 35 |
+
f_score_beta=conf.F_SCORE_BETA,
|
| 36 |
+
f_score_thresh=conf.F_SCORE_THRESH,
|
| 37 |
+
unfreeze=conf.UNFREEZE,
|
| 38 |
+
):
|
| 39 |
+
info_text = f"""Model run INFO:
|
| 40 |
+
|
| 41 |
+
model: untrained model
|
| 42 |
+
dataset: {data_description}
|
| 43 |
+
comments: implemented proper on-the-fly augmentation
|
| 44 |
+
|
| 45 |
+
VARS:
|
| 46 |
+
data_path = {data_dir}
|
| 47 |
+
batch_size = {batch_size}
|
| 48 |
+
Model = {ModelClassName}
|
| 49 |
+
keras_mod_name = {keras_mod_name}
|
| 50 |
+
epochs = {epochs}
|
| 51 |
+
load_ckpt_path = {load_ckpt_path}
|
| 52 |
+
load_g_ckpt = {load_g_ckpt}
|
| 53 |
+
steps_per_epoch = {steps_per_epoch}
|
| 54 |
+
f_score_beta = {f_score_beta}
|
| 55 |
+
f_score_thresh = {f_score_thresh}
|
| 56 |
+
bool_time_shift = {time_augs}
|
| 57 |
+
bool_MixUps = {mixup_augs}
|
| 58 |
+
bool_SpecAug = {spec_aug}
|
| 59 |
+
init_lr = {init_lr}
|
| 60 |
+
final_lr = {final_lr}
|
| 61 |
+
unfreeze = {unfreeze}
|
| 62 |
+
preproc blocks = {pre_blocks}
|
| 63 |
+
"""
|
| 64 |
+
|
| 65 |
+
#############################################################################
|
| 66 |
+
############################# RUN #########################################
|
| 67 |
+
#############################################################################
|
| 68 |
+
data_dir = list(Path(data_dir).iterdir())
|
| 69 |
+
|
| 70 |
+
########### INIT TRAINING RUN AND DIRECTORIES ###############################
|
| 71 |
+
time_start = time.strftime("%Y-%m-%d_%H", time.gmtime())
|
| 72 |
+
Path(f"../trainings/{time_start}").mkdir(exist_ok=True, parents=True)
|
| 73 |
+
|
| 74 |
+
n_train, n_noise = get_train_set_size(data_dir)
|
| 75 |
+
n_train_set = n_train * (
|
| 76 |
+
1 + time_augs + mixup_augs + spec_aug * 2
|
| 77 |
+
) # // batch_size
|
| 78 |
+
print(
|
| 79 |
+
"Train set size = {}. Epoch should correspond to this amount of steps.".format(
|
| 80 |
+
n_train_set
|
| 81 |
+
),
|
| 82 |
+
"\n",
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
seed = np.random.randint(100)
|
| 86 |
+
open(f"../trainings/{time_start}/training_info.txt", "w").write(info_text)
|
| 87 |
+
|
| 88 |
+
###################### DATA PREPROC PIPELINE ################################
|
| 89 |
+
|
| 90 |
+
train_data = run_data_pipeline(
|
| 91 |
+
data_dir, data_dir="train", AUTOTUNE=AUTOTUNE
|
| 92 |
+
)
|
| 93 |
+
test_data = run_data_pipeline(data_dir, data_dir="test", AUTOTUNE=AUTOTUNE)
|
| 94 |
+
noise_data = run_data_pipeline(
|
| 95 |
+
data_dir, data_dir="noise", AUTOTUNE=AUTOTUNE
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
train_data = run_augment_pipeline(
|
| 99 |
+
train_data,
|
| 100 |
+
noise_data,
|
| 101 |
+
n_noise,
|
| 102 |
+
n_train,
|
| 103 |
+
time_augs,
|
| 104 |
+
mixup_augs,
|
| 105 |
+
seed,
|
| 106 |
+
spec_aug=spec_aug,
|
| 107 |
+
time_start=time_start,
|
| 108 |
+
plot=False,
|
| 109 |
+
random=False,
|
| 110 |
+
)
|
| 111 |
+
train_data = prepare(
|
| 112 |
+
train_data, batch_size, shuffle=True, shuffle_buffer=n_train_set * 3
|
| 113 |
+
)
|
| 114 |
+
if (
|
| 115 |
+
steps_per_epoch
|
| 116 |
+
and n_train_set // batch_size < epochs * steps_per_epoch
|
| 117 |
+
):
|
| 118 |
+
train_data = train_data.repeat(
|
| 119 |
+
epochs * steps_per_epoch // (n_train_set // batch_size) + 1
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
test_data = prepare(test_data, batch_size)
|
| 123 |
+
|
| 124 |
+
#############################################################################
|
| 125 |
+
######################### TRAINING ##########################################
|
| 126 |
+
#############################################################################
|
| 127 |
+
|
| 128 |
+
lr = tf.keras.optimizers.schedules.ExponentialDecay(
|
| 129 |
+
init_lr,
|
| 130 |
+
decay_steps=steps_per_epoch or n_train_set // batch_size,
|
| 131 |
+
decay_rate=(final_lr / init_lr) ** (1 / epochs),
|
| 132 |
+
staircase=True,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
model = models.init_model(
|
| 136 |
+
model_instance=ModelClassName,
|
| 137 |
+
checkpoint_dir=f"../trainings/{load_ckpt_path}/unfreeze_no-TF",
|
| 138 |
+
keras_mod_name=keras_mod_name,
|
| 139 |
+
input_specs=True,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
model.compile(
|
| 143 |
+
optimizer=tf.keras.optimizers.legacy.Adam(learning_rate=lr),
|
| 144 |
+
loss=tf.keras.losses.BinaryCrossentropy(),
|
| 145 |
+
metrics=[
|
| 146 |
+
tf.keras.metrics.BinaryAccuracy(),
|
| 147 |
+
tf.keras.metrics.Precision(),
|
| 148 |
+
tf.keras.metrics.Recall(),
|
| 149 |
+
tfa.metrics.FBetaScore(
|
| 150 |
+
num_classes=1,
|
| 151 |
+
beta=f_score_beta,
|
| 152 |
+
threshold=f_score_thresh,
|
| 153 |
+
name="fbeta",
|
| 154 |
+
),
|
| 155 |
+
tfa.metrics.FBetaScore(
|
| 156 |
+
num_classes=1,
|
| 157 |
+
beta=1.0,
|
| 158 |
+
threshold=f_score_thresh,
|
| 159 |
+
name="fbeta1",
|
| 160 |
+
),
|
| 161 |
+
],
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
if unfreeze:
|
| 165 |
+
for layer in model.layers[pre_blocks:-unfreeze]:
|
| 166 |
+
layer.trainable = False
|
| 167 |
+
|
| 168 |
+
checkpoint_path = (
|
| 169 |
+
f"../trainings/{time_start}/unfreeze_{unfreeze}" + "/cp-last.ckpt"
|
| 170 |
+
)
|
| 171 |
+
checkpoint_dir = os.path.dirname(checkpoint_path)
|
| 172 |
+
|
| 173 |
+
cp_callback = tf.keras.callbacks.ModelCheckpoint(
|
| 174 |
+
filepath=checkpoint_path,
|
| 175 |
+
mode="min",
|
| 176 |
+
verbose=1,
|
| 177 |
+
save_weights_only=True,
|
| 178 |
+
save_freq="epoch",
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
model.save_weights(checkpoint_path)
|
| 182 |
+
hist = model.fit(
|
| 183 |
+
train_data,
|
| 184 |
+
epochs=epochs,
|
| 185 |
+
steps_per_epoch=steps_per_epoch,
|
| 186 |
+
validation_data=test_data,
|
| 187 |
+
callbacks=[cp_callback],
|
| 188 |
+
)
|
| 189 |
+
result = hist.history
|
| 190 |
+
save_model_results(checkpoint_dir, result)
|
| 191 |
+
|
| 192 |
+
############## PLOT TRAINING PROGRESS & MODEL EVALUTAIONS ###################
|
| 193 |
+
|
| 194 |
+
plot_model_results(
|
| 195 |
+
time_start, data=data_description, init_lr=init_lr, final_lr=final_lr
|
| 196 |
+
)
|
| 197 |
+
ModelClass = getattr(models, ModelClassName)
|
| 198 |
+
create_and_save_figure(
|
| 199 |
+
ModelClass,
|
| 200 |
+
data_dir,
|
| 201 |
+
batch_size,
|
| 202 |
+
time_start,
|
| 203 |
+
plot_cm=True,
|
| 204 |
+
data=data_description,
|
| 205 |
+
keras_mod_name=keras_mod_name,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def save_model(
|
| 210 |
+
string,
|
| 211 |
+
model,
|
| 212 |
+
lr=5e-4,
|
| 213 |
+
weight_clip=None,
|
| 214 |
+
f_score_beta=0.5,
|
| 215 |
+
f_score_thresh=0.5,
|
| 216 |
+
):
|
| 217 |
+
model.compile(
|
| 218 |
+
optimizer=tf.keras.optimizers.Adam(learning_rate=lr),
|
| 219 |
+
loss=tf.keras.losses.BinaryCrossentropy(),
|
| 220 |
+
metrics=[
|
| 221 |
+
tf.keras.metrics.BinaryAccuracy(),
|
| 222 |
+
tf.keras.metrics.Precision(),
|
| 223 |
+
tf.keras.metrics.Recall(),
|
| 224 |
+
tfa.metrics.FBetaScore(
|
| 225 |
+
num_classes=1,
|
| 226 |
+
beta=f_score_beta,
|
| 227 |
+
threshold=f_score_thresh,
|
| 228 |
+
name="fbeta",
|
| 229 |
+
),
|
| 230 |
+
tfa.metrics.FBetaScore(
|
| 231 |
+
num_classes=1,
|
| 232 |
+
beta=1.0,
|
| 233 |
+
threshold=f_score_thresh,
|
| 234 |
+
name="fbeta1",
|
| 235 |
+
),
|
| 236 |
+
],
|
| 237 |
+
)
|
| 238 |
+
model.save(f"acodet/src/models/{string}")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
##############################################################################
|
| 242 |
+
##############################################################################
|
| 243 |
+
####################### CONFIGURE TRAINING ###################################
|
| 244 |
+
##############################################################################
|
| 245 |
+
##############################################################################
|
| 246 |
+
|
| 247 |
+
if __name__ == "__main__":
|
| 248 |
+
data_dir = list(Path(conf.TFREC_DESTINATION).iterdir())
|
| 249 |
+
|
| 250 |
+
epochs = [*[43] * 5, 100]
|
| 251 |
+
batch_size = [32] * 6
|
| 252 |
+
time_augs = [True]
|
| 253 |
+
mixup_augs = [True]
|
| 254 |
+
spec_aug = [True]
|
| 255 |
+
init_lr = [*[4e-4] * 5, 1e-5]
|
| 256 |
+
final_lr = [3e-6] * 6
|
| 257 |
+
weight_clip = [None] * 6
|
| 258 |
+
ModelClassName = ["GoogleMod"] * 6
|
| 259 |
+
keras_mod_name = [None] * 6
|
| 260 |
+
load_ckpt_path = [*[False] * 5, "2022-11-30_01"]
|
| 261 |
+
load_g_weights = [False]
|
| 262 |
+
steps_per_epoch = [1000]
|
| 263 |
+
data_description = [data_dir]
|
| 264 |
+
pre_blocks = [9]
|
| 265 |
+
f_score_beta = [0.5]
|
| 266 |
+
f_score_thresh = [0.5]
|
| 267 |
+
unfreeze = ["no-TF"]
|
| 268 |
+
|
| 269 |
+
for i in range(len(time_augs)):
|
| 270 |
+
run_training(
|
| 271 |
+
data_dir=data_dir,
|
| 272 |
+
epochs=epochs[i],
|
| 273 |
+
batch_size=batch_size[i],
|
| 274 |
+
time_augs=time_augs[i],
|
| 275 |
+
mixup_augs=mixup_augs[i],
|
| 276 |
+
spec_aug=spec_aug[i],
|
| 277 |
+
init_lr=init_lr[i],
|
| 278 |
+
final_lr=final_lr[i],
|
| 279 |
+
# weight_clip=weight_clip[i],
|
| 280 |
+
ModelClassName=ModelClassName[i],
|
| 281 |
+
keras_mod_name=keras_mod_name[i],
|
| 282 |
+
load_ckpt_path=load_ckpt_path[i],
|
| 283 |
+
# load_g_weights=load_g_weights[i],
|
| 284 |
+
steps_per_epoch=steps_per_epoch[i],
|
| 285 |
+
data_description=data_description[i],
|
| 286 |
+
pre_blocks=pre_blocks[i],
|
| 287 |
+
f_score_beta=f_score_beta[i],
|
| 288 |
+
f_score_thresh=f_score_thresh[i],
|
| 289 |
+
unfreeze=unfreeze[i],
|
| 290 |
+
)
|
advanced_config.yml
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
##############################################################################
|
| 2 |
+
|
| 3 |
+
# THIS FILE IS ONLY MEANT TO BE EDITED IF YOU ARE SURE!
|
| 4 |
+
|
| 5 |
+
# PROGRAM FAILURE IS LIKELY TO OCCUR IF YOU ARE UNSURE OF THE
|
| 6 |
+
# CONSEQUENCES OF YOUR CHANGES.
|
| 7 |
+
|
| 8 |
+
# IF YOU HAVE CHANGED VALUES AND ARE ENCOUNTERING ERRORS,
|
| 9 |
+
# STASH YOUR CHANGES ('git stash' in a git bash console in acodet directory)
|
| 10 |
+
|
| 11 |
+
##############################################################################
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
#################### AUDIO PROCESSING PARAMETERS ###########################
|
| 15 |
+
#!!! CHANGING THESE PARAMETERS WILL RESULT IN MODEL FAILURE, ONLY TO !!!#
|
| 16 |
+
#!!! BE CHANGED FOR TRAINING OF NEW MODEL ESPECIALY FOR DIFFERENT SPECIES !!!#
|
| 17 |
+
## Global audio processing parameters
|
| 18 |
+
# sample rate
|
| 19 |
+
sample_rate: 2000
|
| 20 |
+
# length of context window in seconds
|
| 21 |
+
# this is the audio segment length that the model is trained on
|
| 22 |
+
context_window_in_seconds: 3.9
|
| 23 |
+
|
| 24 |
+
## Mel-Spectrogram parameters
|
| 25 |
+
# FFT window length
|
| 26 |
+
stft_frame_len: 1024
|
| 27 |
+
# number of time bins for mel spectrogram
|
| 28 |
+
number_of_time_bins: 128
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
################### TFRECORD CREATION PARAMETERS ###########################
|
| 32 |
+
## Settings for Creation of Tfrecord Dataset
|
| 33 |
+
# limit of context windows in a tfrecords file
|
| 34 |
+
tfrecs_limit_per_file: 600
|
| 35 |
+
# train/test split
|
| 36 |
+
train_ratio: 0.7
|
| 37 |
+
# test/val split
|
| 38 |
+
test_val_ratio: 0.7
|
| 39 |
+
|
| 40 |
+
######################## ANNOTATIONS ########################################
|
| 41 |
+
default_threshold: 0.5
|
| 42 |
+
# minimum frequency of annotation boxes
|
| 43 |
+
annotation_df_fmin: 50
|
| 44 |
+
# maximum frequency of annotation boxes
|
| 45 |
+
annotation_df_fmax: 1000
|
| 46 |
+
|
| 47 |
+
######################## PATHS #############################################
|
| 48 |
+
# dataset destination directory - save tfrecord dataset to this directory
|
| 49 |
+
# only change when really necessary
|
| 50 |
+
tfrecords_destination_folder: '../Data/Datasets'
|
| 51 |
+
# default folder to store newly created annotations
|
| 52 |
+
generated_annotations_folder: '../generated_annotations'
|
| 53 |
+
# name of current North Atlantic humpback whale song model
|
| 54 |
+
model_name: 'Humpback_20221130'
|
| 55 |
+
# name of top level directory when annotating multiple datasets
|
| 56 |
+
top_dir_name: 'main'
|
| 57 |
+
# custom string to add to timestamp for directory name
|
| 58 |
+
# of created annotations
|
| 59 |
+
annots_timestamp_folder: ''
|
| 60 |
+
# default threshold folder name
|
| 61 |
+
thresh_label: 'thresh_0.5'
|
| 62 |
+
|
| 63 |
+
####################### TRAINING ###########################################
|
| 64 |
+
|
| 65 |
+
# Name of Model class, default is HumpBackNorthAtlantic, possible other classes
|
| 66 |
+
# are GoogleMod for the modified ResNet-50 architecture, or KerasAppModel for
|
| 67 |
+
# any of the Keras application models (name will get specified under keras_mod_name)
|
| 68 |
+
ModelClassName: 'HumpBackNorthAtlantic'
|
| 69 |
+
# batch size for training and evaluating purposes
|
| 70 |
+
batch_size: 32
|
| 71 |
+
# number of epochs to run the model
|
| 72 |
+
epochs: 50
|
| 73 |
+
# specify the path to your training checkpoint here to load a pretrained model
|
| 74 |
+
load_ckpt_path: False
|
| 75 |
+
# to run the google model, select True
|
| 76 |
+
load_g_ckpt: False
|
| 77 |
+
# specify the name of the keras application model that you want to run - select the
|
| 78 |
+
# ModelClassName KerasAppModel for this
|
| 79 |
+
keras_mod_name: False
|
| 80 |
+
# number of steps per epoch
|
| 81 |
+
steps_per_epoch: 1000
|
| 82 |
+
# select True if you want your training data to be time shift augmented (recommended)
|
| 83 |
+
time_augs: True
|
| 84 |
+
# select True if you want your training data to be MixUp augmented (recommended)
|
| 85 |
+
mixup_augs: True
|
| 86 |
+
# select True if you want your training data to be
|
| 87 |
+
# time and frequency masked augmented (recommended)
|
| 88 |
+
spec_aug: True
|
| 89 |
+
# specify a string to describe the dataset used for this model run (to later be able
|
| 90 |
+
# to understand what was significant about this model training)
|
| 91 |
+
data_description: 'describe your dataset'
|
| 92 |
+
# starting learning rate
|
| 93 |
+
init_lr: 5e-4
|
| 94 |
+
# final learning rate
|
| 95 |
+
final_lr: 5e-6
|
| 96 |
+
# number of preliminary blocks in model (are kept frozen)
|
| 97 |
+
pre_blocks: 9
|
| 98 |
+
# threshold for f score beta
|
| 99 |
+
f_score_beta: 0.5
|
| 100 |
+
f_score_thresh: 0.5
|
| 101 |
+
# number of layers to unfreeze, if False, the entire model is trainable
|
| 102 |
+
unfreeze: False
|
| 103 |
+
|
| 104 |
+
######################## Streamlit #########################################
|
| 105 |
+
# select True if you want to run the streamlit app
|
| 106 |
+
streamlit: False
|
macM1_requirements/requirements_m1-1.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
keras-cv==0.5.0
|
| 2 |
+
librosa==0.9.1
|
| 3 |
+
matplotlib==3.5.1
|
| 4 |
+
numpy==1.22.0
|
| 5 |
+
pandas==1.4.2
|
| 6 |
+
PyYAML==6.0.1
|
| 7 |
+
seaborn==0.12.1
|
| 8 |
+
streamlit==1.25.0
|
| 9 |
+
tensorflow==2.13.0rc0
|
| 10 |
+
tensorflow_io==0.31.0
|
| 11 |
+
tensorflow-addons==0.20.0
|
macM1_requirements/requirements_m1-2.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
keras==2.12
|
pyproject.toml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool.black]
|
| 2 |
+
line-length = 79
|
| 3 |
+
include = '\.pyi?$'
|
requirements.txt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
keras_cv==0.5.0
|
| 2 |
+
librosa==0.9.1
|
| 3 |
+
matplotlib==3.5.1
|
| 4 |
+
numpy==1.22.0
|
| 5 |
+
pandas==1.4.2
|
| 6 |
+
PyYAML==6.0.1
|
| 7 |
+
seaborn==0.12.2
|
| 8 |
+
streamlit==1.25.0
|
| 9 |
+
tensorflow==2.12.0
|
| 10 |
+
tensorflow_addons==0.20.0
|
| 11 |
+
tensorflow_intel
|
| 12 |
+
tensorflow_io==0.31.0
|
| 13 |
+
plotly==5.16.1
|
| 14 |
+
black==23.7.0
|
| 15 |
+
pre-commit==3.3.3
|
| 16 |
+
pyqt5==5.15.6
|
run.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main(sc=True, **kwargs):
|
| 2 |
+
"""
|
| 3 |
+
Main function to run the whole pipeline. The function is called from the
|
| 4 |
+
streamlit app. The function is called with the preset option as an argument.
|
| 5 |
+
The preset option is used to determine which function should be called.
|
| 6 |
+
The preset option is set in the config file.
|
| 7 |
+
|
| 8 |
+
Parameters
|
| 9 |
+
----------
|
| 10 |
+
sc : bool, optional
|
| 11 |
+
Decide if meta analysis should be done using sequence limit, by default True
|
| 12 |
+
|
| 13 |
+
Returns
|
| 14 |
+
-------
|
| 15 |
+
str or None
|
| 16 |
+
depending on the preset option, the function returns either the time
|
| 17 |
+
when the annotation was started or None
|
| 18 |
+
"""
|
| 19 |
+
from acodet.annotate import run_annotation, filter_annots_by_thresh
|
| 20 |
+
from acodet.train import run_training, save_model
|
| 21 |
+
from acodet.tfrec import write_tfrec_dataset
|
| 22 |
+
from acodet.hourly_presence import compute_hourly_pres, calc_val_diff
|
| 23 |
+
from acodet.evaluate import create_overview_plot
|
| 24 |
+
from acodet.combine_annotations import generate_final_annotations
|
| 25 |
+
from acodet.models import init_model
|
| 26 |
+
import acodet.global_config as conf
|
| 27 |
+
|
| 28 |
+
if "fetch_config_again" in kwargs:
|
| 29 |
+
import importlib
|
| 30 |
+
|
| 31 |
+
importlib.reload(conf)
|
| 32 |
+
kwargs["relativ_path"] = conf.SOUND_FILES_SOURCE
|
| 33 |
+
if "preset" in kwargs:
|
| 34 |
+
preset = kwargs["preset"]
|
| 35 |
+
else:
|
| 36 |
+
preset = conf.PRESET
|
| 37 |
+
|
| 38 |
+
if conf.RUN_CONFIG == 1:
|
| 39 |
+
if preset == 1:
|
| 40 |
+
timestamp_foldername = run_annotation(**kwargs)
|
| 41 |
+
return timestamp_foldername
|
| 42 |
+
elif preset == 2:
|
| 43 |
+
new_thresh = filter_annots_by_thresh(**kwargs)
|
| 44 |
+
return new_thresh
|
| 45 |
+
elif preset == 3:
|
| 46 |
+
compute_hourly_pres(sc=sc, **kwargs)
|
| 47 |
+
elif preset == 4:
|
| 48 |
+
compute_hourly_pres(**kwargs)
|
| 49 |
+
elif preset == 6:
|
| 50 |
+
calc_val_diff(**kwargs)
|
| 51 |
+
elif preset == 0:
|
| 52 |
+
timestamp_foldername = run_annotation(**kwargs)
|
| 53 |
+
filter_annots_by_thresh(timestamp_foldername, **kwargs)
|
| 54 |
+
compute_hourly_pres(timestamp_foldername, sc=sc, **kwargs)
|
| 55 |
+
return timestamp_foldername
|
| 56 |
+
|
| 57 |
+
elif conf.RUN_CONFIG == 2:
|
| 58 |
+
if preset == 1:
|
| 59 |
+
generate_final_annotations(**kwargs)
|
| 60 |
+
write_tfrec_dataset(**kwargs)
|
| 61 |
+
elif preset == 2:
|
| 62 |
+
generate_final_annotations(active_learning=False, **kwargs)
|
| 63 |
+
write_tfrec_dataset(active_learning=False, **kwargs)
|
| 64 |
+
|
| 65 |
+
elif conf.RUN_CONFIG == 3:
|
| 66 |
+
if preset == 1:
|
| 67 |
+
run_training(**kwargs)
|
| 68 |
+
elif preset == 2:
|
| 69 |
+
create_overview_plot(**kwargs)
|
| 70 |
+
elif preset == 3:
|
| 71 |
+
create_overview_plot("2022-05-00_00", **kwargs)
|
| 72 |
+
elif preset == 4:
|
| 73 |
+
save_model("FlatHBNA", init_model(), **kwargs)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
from acodet.create_session_file import create_session_file
|
| 78 |
+
|
| 79 |
+
create_session_file()
|
| 80 |
+
main()
|
simple_config.yml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
##############################################################################
|
| 2 |
+
|
| 3 |
+
# This file is for you to edit the paths corresponding to the following:
|
| 4 |
+
# - source path of annotation files
|
| 5 |
+
# - destination path of your annotation files (leave unchanged if possible)
|
| 6 |
+
# - source path of sound files (.wav of .aif) (top most directory)
|
| 7 |
+
# - destination path of any plots or spreadsheets
|
| 8 |
+
|
| 9 |
+
# This file is also for you to edit the threshold value of the detector, to
|
| 10 |
+
# make the detector more sensitive or less sensitive.
|
| 11 |
+
# - Higher threshold will decrease number of false positives but
|
| 12 |
+
# at the cost of overlooking vocalizations.
|
| 13 |
+
# - Lower threshold values will increase number of false positives
|
| 14 |
+
# but more likely also detect false positives.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
##############################################################################
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
###################### 1. DEFINE YOUR RUN ####################################
|
| 23 |
+
|
| 24 |
+
# what would you like to do?
|
| 25 |
+
# chose the run configuration:
|
| 26 |
+
# - 1 generate annotations
|
| 27 |
+
# - 2 generate new training data from reviewed annotations
|
| 28 |
+
# - 3 train (and evaluate)
|
| 29 |
+
run_config: 1
|
| 30 |
+
|
| 31 |
+
# depending on the main task, chose your predefined settings:
|
| 32 |
+
# for generation of annotations, chose:
|
| 33 |
+
# - 1 generate new annotations
|
| 34 |
+
# - 2 filter existing annotations with different threshold
|
| 35 |
+
# - 3 generate hourly predictions (simple limit and sequence criterion)
|
| 36 |
+
# - 4 generate hourly predictions (only simple limit)
|
| 37 |
+
# - 5 generate hourly predictions with varying limits - n.i.
|
| 38 |
+
# - 0 all of the above
|
| 39 |
+
# for generation of new training data, chose:
|
| 40 |
+
# - 1 generate new training data from reviewed annotations
|
| 41 |
+
# - 2 generate new training data from reviewed annotations
|
| 42 |
+
# and fill space between annotations with noise annotations
|
| 43 |
+
# for training, chose:
|
| 44 |
+
# - 1 continue training on existing model and save model in the end
|
| 45 |
+
# - 2 evaluate saved model
|
| 46 |
+
# - 3 evaluate model checkpoint
|
| 47 |
+
# - 4 save model specified in advanced config
|
| 48 |
+
predefined_settings: 0
|
| 49 |
+
|
| 50 |
+
####################### 2. DEFINCE YOUR PATHS ###############################
|
| 51 |
+
|
| 52 |
+
## Paths
|
| 53 |
+
# source path for your sound files (top most directory)
|
| 54 |
+
# relevant for generation of new annotations (option run_config: 1)
|
| 55 |
+
sound_files_source: 'path to your sound files'
|
| 56 |
+
|
| 57 |
+
# source path for automatically generated annotations
|
| 58 |
+
# relevant for generation of hourly or daily presence, or recomputing
|
| 59 |
+
# with a different threshold
|
| 60 |
+
# (options run_config: 1 and predifined_settings: 2 and 4)
|
| 61 |
+
generated_annotation_source: '../Cathy/HBW_detector'
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# source path for annotations created or reviewed by you
|
| 65 |
+
# relevant for creation of new dataset (option run_config: 2)
|
| 66 |
+
# -> might be easier to just copy annotation files to default location
|
| 67 |
+
reviewed_annotation_source: '../annotations'
|
| 68 |
+
|
| 69 |
+
# source path for automatically generated combined annotations
|
| 70 |
+
# Only relevant for creation of new dataset (option run_config: 2)
|
| 71 |
+
# if unsure, leave unchanged.
|
| 72 |
+
annotation_destination: '../combined_annotations'
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
####################### 3. DEFINE YOUR PARAMETERS ###########################
|
| 78 |
+
|
| 79 |
+
## Model Parameters
|
| 80 |
+
# threshold for predictions
|
| 81 |
+
thresh: 0.9
|
| 82 |
+
# number of annotations above threshold for hourly presence (default = 15)
|
| 83 |
+
simple_limit: 15
|
| 84 |
+
# threshold for sequence criterion (default = 0.9)
|
| 85 |
+
sequence_thresh: 0.9
|
| 86 |
+
# number of annotations above threshold within 20 consecutive windows
|
| 87 |
+
# for hourly presence (default = 3)
|
| 88 |
+
sequence_limit: 3
|
| 89 |
+
# number of consecutive windows that sc_limit has to occur in (default = 20)
|
| 90 |
+
sequence_con_win: 20
|
| 91 |
+
# number of annotations that correspond to the upper limit of color bar in
|
| 92 |
+
# hourly annotations plots
|
| 93 |
+
max_annots_per_hour: 150
|
| 94 |
+
# path to validation file of hourly presence dataset
|
| 95 |
+
hourly_presence_validation_path: 'validation.csv'
|
| 96 |
+
|
| 97 |
+
# number of predictions that get computed at once - should be in [20, 2000]
|
| 98 |
+
# worth testing out different values to find whats fastest for your machine
|
| 99 |
+
prediction_window_limit: 1000
|
streamlit_app.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from acodet.create_session_file import create_session_file, read_session_file
|
| 3 |
+
from acodet.front_end import help_strings
|
| 4 |
+
|
| 5 |
+
if not "session_started" in st.session_state:
|
| 6 |
+
st.session_state.session_started = True
|
| 7 |
+
create_session_file()
|
| 8 |
+
from acodet.front_end import (
|
| 9 |
+
utils,
|
| 10 |
+
st_annotate,
|
| 11 |
+
st_generate_data,
|
| 12 |
+
st_train,
|
| 13 |
+
st_visualization,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
utils.write_to_session_file("streamlit", True)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def select_preset():
|
| 20 |
+
utils.write_to_session_file("run_config", st.session_state.run_option)
|
| 21 |
+
show_run_btn = False
|
| 22 |
+
|
| 23 |
+
if st.session_state.run_option == 1:
|
| 24 |
+
show_run_btn = st_annotate.annotate_options()
|
| 25 |
+
elif st.session_state.run_option == 2:
|
| 26 |
+
show_run_btn = st_generate_data.generate_data_options()
|
| 27 |
+
elif st.session_state.run_option == 3:
|
| 28 |
+
show_run_btn = st_train.train_options()
|
| 29 |
+
if show_run_btn:
|
| 30 |
+
run_computions()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def run_computions(**kwargs):
|
| 34 |
+
utils.next_button(id=4, text="Run computations")
|
| 35 |
+
if st.session_state.b4:
|
| 36 |
+
display_not_implemented_text()
|
| 37 |
+
kwargs = utils.prepare_run()
|
| 38 |
+
if not st.session_state.run_finished:
|
| 39 |
+
import run
|
| 40 |
+
|
| 41 |
+
st.session_state.save_dir = run.main(
|
| 42 |
+
fetch_config_again=True, **kwargs
|
| 43 |
+
)
|
| 44 |
+
st.session_state.run_finished = True
|
| 45 |
+
|
| 46 |
+
if st.session_state.run_finished:
|
| 47 |
+
if not st.session_state.preset_option == 3:
|
| 48 |
+
st.write("Computation finished")
|
| 49 |
+
utils.next_button(id=5, text="Show results")
|
| 50 |
+
st.markdown("""---""")
|
| 51 |
+
else:
|
| 52 |
+
conf = read_session_file()
|
| 53 |
+
st.session_state.b5 = True
|
| 54 |
+
st.session_state.save_dir = conf["generated_annotation_source"]
|
| 55 |
+
|
| 56 |
+
if not st.session_state.b5:
|
| 57 |
+
pass
|
| 58 |
+
else:
|
| 59 |
+
st_visualization.output()
|
| 60 |
+
st.stop()
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def display_not_implemented_text():
|
| 64 |
+
if not st.session_state.run_option == 1:
|
| 65 |
+
st.write(
|
| 66 |
+
"""This option is not yet implemented for usage
|
| 67 |
+
with the user interface. A headless version is
|
| 68 |
+
available at https://github.com/vskode/acodet."""
|
| 69 |
+
)
|
| 70 |
+
st.stop()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
if __name__ == "__main__":
|
| 74 |
+
|
| 75 |
+
st.markdown(
|
| 76 |
+
"""
|
| 77 |
+
# Welcome to AcoDet - Acoustic Detection of Animal Vocalizations :loud_sound:
|
| 78 |
+
### This program is currently equipped with a humpback whale song detector for the North Atlantic :whale2:
|
| 79 |
+
For more information, please visit https://github.com/vskode/acodet
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
"""
|
| 83 |
+
)
|
| 84 |
+
run_option = int(
|
| 85 |
+
st.selectbox(
|
| 86 |
+
"How would you like run the program?",
|
| 87 |
+
("1 - Inference", "2 - Generate new training data", "3 - Train"),
|
| 88 |
+
key="main",
|
| 89 |
+
help=help_strings.RUN_OPTION,
|
| 90 |
+
)[0]
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
st.session_state.run_option = run_option
|
| 94 |
+
select_preset()
|
tests/test.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, sys
|
| 2 |
+
import unittest
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
sys.path.insert(0, os.path.abspath("."))
|
| 7 |
+
|
| 8 |
+
########### MODIFY SESSION SETTINGS BEFORE GLOBAL CONFIG IS IMPORTED #########
|
| 9 |
+
from acodet.create_session_file import create_session_file
|
| 10 |
+
|
| 11 |
+
create_session_file()
|
| 12 |
+
import json
|
| 13 |
+
|
| 14 |
+
with open("acodet/src/tmp_session.json", "r") as f:
|
| 15 |
+
session = json.load(f)
|
| 16 |
+
session["sound_files_source"] = "tests/test_files/test_audio_files"
|
| 17 |
+
session[
|
| 18 |
+
"generated_annotation_source"
|
| 19 |
+
] = "tests/test_files/test_generated_annotations"
|
| 20 |
+
session[
|
| 21 |
+
"annotation_destination"
|
| 22 |
+
] = "tests/test_files/test_combined_annotations"
|
| 23 |
+
session[
|
| 24 |
+
"generated_annotations_folder"
|
| 25 |
+
] = "tests/test_files/test_generated_annotations"
|
| 26 |
+
|
| 27 |
+
session[
|
| 28 |
+
"reviewed_annotation_source"
|
| 29 |
+
] = "tests/test_files/test_generated_annotations"
|
| 30 |
+
session["tfrecords_destination_folder"] = "tests/test_files/test_tfrecords"
|
| 31 |
+
|
| 32 |
+
with open("acodet/src/tmp_session.json", "w") as f:
|
| 33 |
+
json.dump(session, f)
|
| 34 |
+
##############################################################################
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
from acodet.annotate import run_annotation, filter_annots_by_thresh
|
| 38 |
+
from acodet.funcs import return_windowed_file, get_train_set_size
|
| 39 |
+
from acodet.models import GoogleMod
|
| 40 |
+
from acodet.combine_annotations import generate_final_annotations
|
| 41 |
+
from acodet.tfrec import write_tfrec_dataset
|
| 42 |
+
from acodet.train import run_training
|
| 43 |
+
from acodet import global_config as conf
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class TestDetection(unittest.TestCase):
|
| 48 |
+
def test_annotation(self):
|
| 49 |
+
self.time_stamp = run_annotation()
|
| 50 |
+
df = pd.read_csv(
|
| 51 |
+
(
|
| 52 |
+
Path(conf.GEN_ANNOTS_DIR)
|
| 53 |
+
.joinpath(self.time_stamp)
|
| 54 |
+
.joinpath("stats.csv")
|
| 55 |
+
)
|
| 56 |
+
)
|
| 57 |
+
self.assertEqual(
|
| 58 |
+
df["number of predictions with thresh>0.8"][0],
|
| 59 |
+
326,
|
| 60 |
+
"Number of predictions is not what it should be.",
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
filter_annots_by_thresh(self.time_stamp)
|
| 64 |
+
file = list(
|
| 65 |
+
Path(conf.GEN_ANNOT_SRC)
|
| 66 |
+
.joinpath(self.time_stamp)
|
| 67 |
+
.joinpath(f"thresh_{conf.THRESH}")
|
| 68 |
+
.glob("**/*.txt")
|
| 69 |
+
)[0]
|
| 70 |
+
df = pd.read_csv(file)
|
| 71 |
+
self.assertEqual(
|
| 72 |
+
len(df),
|
| 73 |
+
309,
|
| 74 |
+
"Number of predictions from filtered thresholds " "is incorrect.",
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class TestTraining(unittest.TestCase):
|
| 79 |
+
def test_model_load(self):
|
| 80 |
+
model = GoogleMod(load_g_ckpt=False).model
|
| 81 |
+
self.assertGreater(len(model.layers), 15)
|
| 82 |
+
|
| 83 |
+
# def test_tfrecord_loading(self):
|
| 84 |
+
# data_dir = list(Path(conf.TFREC_DESTINATION).iterdir())
|
| 85 |
+
# n_train, n_noise = get_train_set_size(data_dir)
|
| 86 |
+
# self.assertEqual(n_train, 517)
|
| 87 |
+
# self.assertEqual(n_noise, 42)
|
| 88 |
+
|
| 89 |
+
class TestTFRecordCreation(unittest.TestCase):
|
| 90 |
+
def test_tfrecord(self):
|
| 91 |
+
time_stamp = list(Path(conf.ANNOT_DEST).iterdir())[-1]
|
| 92 |
+
write_tfrec_dataset(annot_dir=time_stamp, active_learning=False)
|
| 93 |
+
metadata_file_path = Path(conf.TFREC_DESTINATION).joinpath(
|
| 94 |
+
"dataset_meta_train.json"
|
| 95 |
+
)
|
| 96 |
+
self.assertEqual(
|
| 97 |
+
metadata_file_path.exists(),
|
| 98 |
+
1,
|
| 99 |
+
"TFRecords metadata file was not created.",
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
with open(metadata_file_path, "r") as f:
|
| 103 |
+
data = json.load(f)
|
| 104 |
+
self.assertEqual(
|
| 105 |
+
data["dataset"]["size"]["train"],
|
| 106 |
+
517,
|
| 107 |
+
"TFRecords files has wrong number of datapoints.",
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
def test_combined_annotation(self):
|
| 111 |
+
generate_final_annotations(active_learning=False)
|
| 112 |
+
time_stamp = list(Path(conf.GEN_ANNOTS_DIR).iterdir())[-1].stem
|
| 113 |
+
combined_annots_path = (
|
| 114 |
+
Path(conf.ANNOT_DEST)
|
| 115 |
+
.joinpath(time_stamp)
|
| 116 |
+
.joinpath("combined_annotations.csv")
|
| 117 |
+
)
|
| 118 |
+
self.assertEqual(
|
| 119 |
+
combined_annots_path.exists(),
|
| 120 |
+
1,
|
| 121 |
+
"csv file containing combined_annotations does not exist.",
|
| 122 |
+
)
|
| 123 |
+
df = pd.read_csv(combined_annots_path)
|
| 124 |
+
self.assertEqual(
|
| 125 |
+
df.start.iloc[-1],
|
| 126 |
+
1795.2825,
|
| 127 |
+
"The annotations in combined_annotations.csv don't seem to be identical",
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
if __name__ == "__main__":
|
| 132 |
+
unittest.main()
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_00-00-05_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 3.8775 7.755 1000 50 0.72177804
|
| 3 |
+
2 65.9175 69.795 1000 50 0.5281566
|
| 4 |
+
3 108.57 112.44749999999999 1000 50 0.76347905
|
| 5 |
+
4 124.08 127.9575 1000 50 0.64476347
|
| 6 |
+
5 127.9575 131.835 1000 50 0.6341658
|
| 7 |
+
6 139.59 143.4675 1000 50 0.60823405
|
| 8 |
+
7 178.365 182.2425 1000 50 0.6451596
|
| 9 |
+
8 186.12 189.9975 1000 50 0.82998466
|
| 10 |
+
9 201.63 205.5075 1000 50 0.75589406
|
| 11 |
+
10 213.2625 217.14 1000 50 0.6491813
|
| 12 |
+
11 217.14 221.01749999999998 1000 50 0.7016125
|
| 13 |
+
12 275.3025 279.18 1000 50 0.8662185
|
| 14 |
+
13 290.8125 294.69 1000 50 0.5251316
|
| 15 |
+
14 321.8325 325.71 1000 50 0.64809465
|
| 16 |
+
15 407.1375 411.015 1000 50 0.5973742
|
| 17 |
+
16 411.015 414.8925 1000 50 0.9908187
|
| 18 |
+
17 476.9325 480.81 1000 50 0.85746086
|
| 19 |
+
18 480.81 484.6875 1000 50 0.71086293
|
| 20 |
+
19 507.9525 511.83 1000 50 0.79394937
|
| 21 |
+
20 511.83 515.7075 1000 50 0.91436124
|
| 22 |
+
21 546.7275 550.605 1000 50 0.6516418
|
| 23 |
+
22 601.0125 604.8900000000001 1000 50 0.5256015
|
| 24 |
+
23 616.5225 620.4000000000001 1000 50 0.8155822
|
| 25 |
+
24 647.5425 651.4200000000001 1000 50 0.6215045
|
| 26 |
+
25 659.175 663.0525 1000 50 0.7368213
|
| 27 |
+
26 713.46 717.3375000000001 1000 50 0.57750905
|
| 28 |
+
27 725.0925 728.97 1000 50 0.6525679
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_00-15-55_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 3.8775 7.755 1000 50 0.5005236
|
| 3 |
+
2 50.4075 54.285 1000 50 0.50911117
|
| 4 |
+
3 139.59 143.4675 1000 50 0.59164804
|
| 5 |
+
4 162.855 166.7325 1000 50 0.54786545
|
| 6 |
+
5 178.365 182.2425 1000 50 0.53832465
|
| 7 |
+
6 201.63 205.5075 1000 50 0.759787
|
| 8 |
+
7 213.2625 217.14 1000 50 0.7325302
|
| 9 |
+
8 232.65 236.5275 1000 50 0.73861456
|
| 10 |
+
9 244.2825 248.16 1000 50 0.5488199
|
| 11 |
+
10 248.16 252.0375 1000 50 0.5221725
|
| 12 |
+
11 275.3025 279.18 1000 50 0.604062
|
| 13 |
+
12 290.8125 294.69 1000 50 0.54666525
|
| 14 |
+
13 306.3225 310.2 1000 50 0.78042346
|
| 15 |
+
14 414.8925 418.77 1000 50 0.61935645
|
| 16 |
+
15 430.4025 434.28 1000 50 0.5925473
|
| 17 |
+
16 445.9125 449.79 1000 50 0.57290447
|
| 18 |
+
17 476.9325 480.81 1000 50 0.57334906
|
| 19 |
+
18 496.32 500.1975 1000 50 0.75015646
|
| 20 |
+
19 523.4625 527.34 1000 50 0.50864077
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_01-00-05_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 34.8975 38.775 1000 50 0.6350528
|
| 3 |
+
2 89.1825 93.06 1000 50 0.7238582
|
| 4 |
+
3 151.2225 155.1 1000 50 0.7220111
|
| 5 |
+
4 217.14 221.01749999999998 1000 50 0.59247476
|
| 6 |
+
5 221.0175 224.895 1000 50 0.58241534
|
| 7 |
+
6 232.65 236.5275 1000 50 0.9417606
|
| 8 |
+
7 244.2825 248.16 1000 50 0.5242218
|
| 9 |
+
8 279.18 283.0575 1000 50 0.50423527
|
| 10 |
+
9 294.69 298.5675 1000 50 0.5808482
|
| 11 |
+
10 337.3425 341.21999999999997 1000 50 0.5212356
|
| 12 |
+
11 352.8525 356.73 1000 50 0.7410249
|
| 13 |
+
12 356.73 360.6075 1000 50 0.8129122
|
| 14 |
+
13 372.24 376.1175 1000 50 0.55076885
|
| 15 |
+
14 403.26 407.1375 1000 50 0.5803523
|
| 16 |
+
15 457.545 461.4225 1000 50 0.5226226
|
| 17 |
+
16 527.34 531.2175000000001 1000 50 0.69937265
|
| 18 |
+
17 569.9925 573.87 1000 50 0.53878176
|
| 19 |
+
18 682.44 686.3175000000001 1000 50 0.5675298
|
| 20 |
+
19 725.0925 728.97 1000 50 0.66011316
|
| 21 |
+
20 767.745 771.6225000000001 1000 50 0.8282536
|
| 22 |
+
21 969.375 973.2525 1000 50 0.53780836
|
| 23 |
+
22 981.0075 984.8850000000001 1000 50 0.714791
|
| 24 |
+
23 1019.7825 1023.6600000000001 1000 50 0.65036213
|
| 25 |
+
24 1031.415 1035.2925 1000 50 0.58890146
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_01-21-14_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 34.8975 38.775 1000 50 0.5121363
|
| 3 |
+
2 69.795 73.6725 1000 50 0.8324235
|
| 4 |
+
3 135.7125 139.59 1000 50 0.5703977
|
| 5 |
+
4 201.63 205.5075 1000 50 0.94431615
|
| 6 |
+
5 224.895 228.7725 1000 50 0.661064
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_02-00-05_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 38.775 42.652499999999996 1000 50 0.737386
|
| 3 |
+
2 100.815 104.6925 1000 50 0.72394115
|
| 4 |
+
3 108.57 112.44749999999999 1000 50 0.5832078
|
| 5 |
+
4 116.325 120.2025 1000 50 0.5326732
|
| 6 |
+
5 139.59 143.4675 1000 50 0.6070674
|
| 7 |
+
6 143.4675 147.345 1000 50 0.868954
|
| 8 |
+
7 174.4875 178.365 1000 50 0.8059232
|
| 9 |
+
8 213.2625 217.14 1000 50 0.6078309
|
| 10 |
+
9 224.895 228.7725 1000 50 0.6188046
|
| 11 |
+
10 275.3025 279.18 1000 50 0.7676495
|
| 12 |
+
11 286.935 290.8125 1000 50 0.63999677
|
| 13 |
+
12 294.69 298.5675 1000 50 0.69478
|
| 14 |
+
13 306.3225 310.2 1000 50 0.5553063
|
| 15 |
+
14 314.0775 317.955 1000 50 0.5727619
|
| 16 |
+
15 356.73 360.6075 1000 50 0.5218362
|
| 17 |
+
16 395.505 399.3825 1000 50 0.5227227
|
| 18 |
+
17 399.3825 403.26 1000 50 0.79591954
|
| 19 |
+
18 411.015 414.8925 1000 50 0.69747454
|
| 20 |
+
19 422.6475 426.525 1000 50 0.5296574
|
| 21 |
+
20 438.1575 442.035 1000 50 0.5216314
|
| 22 |
+
21 442.035 445.9125 1000 50 0.80506706
|
| 23 |
+
22 515.7075 519.585 1000 50 0.72778624
|
| 24 |
+
23 531.2175 535.095 1000 50 0.6537169
|
| 25 |
+
24 535.095 538.9725000000001 1000 50 0.6939367
|
| 26 |
+
25 566.115 569.9925000000001 1000 50 0.54583424
|
| 27 |
+
26 612.645 616.5225 1000 50 0.5596068
|
| 28 |
+
27 635.91 639.7875 1000 50 0.87055826
|
| 29 |
+
28 647.5425 651.4200000000001 1000 50 0.5198072
|
| 30 |
+
29 690.195 694.0725000000001 1000 50 0.6586496
|
| 31 |
+
30 697.95 701.8275000000001 1000 50 0.59695214
|
| 32 |
+
31 752.235 756.1125000000001 1000 50 0.5564991
|
| 33 |
+
32 759.99 763.8675000000001 1000 50 0.8687321
|
| 34 |
+
33 771.6225 775.5 1000 50 0.745041
|
| 35 |
+
34 794.8875 798.7650000000001 1000 50 0.63954085
|
| 36 |
+
35 829.785 833.6625 1000 50 0.58957016
|
| 37 |
+
36 841.4175 845.2950000000001 1000 50 0.70007896
|
| 38 |
+
37 845.295 849.1725 1000 50 0.5040522
|
| 39 |
+
38 856.9275 860.8050000000001 1000 50 0.50218743
|
| 40 |
+
39 899.58 903.4575000000001 1000 50 0.64664465
|
| 41 |
+
40 926.7225 930.6 1000 50 0.61015755
|
| 42 |
+
41 934.4775 938.355 1000 50 0.6043967
|
| 43 |
+
42 965.4975 969.375 1000 50 0.592605
|
| 44 |
+
43 1004.2725 1008.1500000000001 1000 50 0.5635989
|
| 45 |
+
44 1019.7825 1023.6600000000001 1000 50 0.7166601
|
| 46 |
+
45 1023.66 1027.5375 1000 50 0.603185
|
| 47 |
+
46 1043.0475 1046.925 1000 50 0.6931752
|
| 48 |
+
47 1077.945 1081.8225 1000 50 0.64394623
|
| 49 |
+
48 1089.5775 1093.4550000000002 1000 50 0.54892826
|
| 50 |
+
49 1159.3725 1163.25 1000 50 0.65110403
|
| 51 |
+
50 1209.78 1213.6575 1000 50 0.52652115
|
| 52 |
+
51 1213.6575 1217.535 1000 50 0.59059477
|
| 53 |
+
52 1217.535 1221.4125000000001 1000 50 0.53762406
|
| 54 |
+
53 1233.045 1236.9225000000001 1000 50 0.77236694
|
| 55 |
+
54 1267.9425 1271.8200000000002 1000 50 0.65751004
|
| 56 |
+
55 1279.575 1283.4525 1000 50 0.8536242
|
| 57 |
+
56 1318.35 1322.2275 1000 50 0.7538498
|
| 58 |
+
57 1349.37 1353.2475 1000 50 0.57182205
|
| 59 |
+
58 1361.0025 1364.88 1000 50 0.6505617
|
| 60 |
+
59 1368.7575 1372.635 1000 50 0.64853406
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_03-00-05_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 7.755 11.6325 1000 50 0.75583506
|
| 3 |
+
2 15.51 19.3875 1000 50 0.5251516
|
| 4 |
+
3 27.1425 31.02 1000 50 0.55041605
|
| 5 |
+
4 31.02 34.8975 1000 50 0.85942334
|
| 6 |
+
5 42.6525 46.53 1000 50 0.9165694
|
| 7 |
+
6 46.53 50.4075 1000 50 0.7389612
|
| 8 |
+
7 50.4075 54.285 1000 50 0.8961259
|
| 9 |
+
8 58.1625 62.04 1000 50 0.58524656
|
| 10 |
+
9 69.795 73.6725 1000 50 0.7369542
|
| 11 |
+
10 77.55 81.4275 1000 50 0.73338825
|
| 12 |
+
11 155.1 158.9775 1000 50 0.63714325
|
| 13 |
+
12 209.385 213.2625 1000 50 0.57494605
|
| 14 |
+
13 259.7925 263.67 1000 50 0.77267367
|
| 15 |
+
14 263.67 267.5475 1000 50 0.5608314
|
| 16 |
+
15 275.3025 279.18 1000 50 0.56708634
|
| 17 |
+
16 286.935 290.8125 1000 50 0.53724974
|
| 18 |
+
17 310.2 314.0775 1000 50 0.6503988
|
| 19 |
+
18 376.1175 379.995 1000 50 0.5667686
|
| 20 |
+
19 379.995 383.8725 1000 50 0.6167431
|
| 21 |
+
20 387.75 391.6275 1000 50 0.69172144
|
| 22 |
+
21 395.505 399.3825 1000 50 0.6661956
|
| 23 |
+
22 399.3825 403.26 1000 50 0.6705925
|
| 24 |
+
23 449.79 453.6675 1000 50 0.76984024
|
| 25 |
+
24 496.32 500.1975 1000 50 0.6549141
|
| 26 |
+
25 535.095 538.9725000000001 1000 50 0.7988651
|
| 27 |
+
26 546.7275 550.605 1000 50 0.98777056
|
| 28 |
+
27 558.36 562.2375000000001 1000 50 0.5757415
|
| 29 |
+
28 581.625 585.5025 1000 50 0.9520807
|
| 30 |
+
29 608.7675 612.6450000000001 1000 50 0.7059972
|
| 31 |
+
30 651.42 655.2975 1000 50 0.58196646
|
| 32 |
+
31 659.175 663.0525 1000 50 0.50375336
|
| 33 |
+
32 663.0525 666.9300000000001 1000 50 0.7096761
|
| 34 |
+
33 666.93 670.8075 1000 50 0.6709009
|
| 35 |
+
34 674.685 678.5625 1000 50 0.6068424
|
| 36 |
+
35 705.705 709.5825000000001 1000 50 0.79320824
|
| 37 |
+
36 709.5825 713.46 1000 50 0.7026795
|
| 38 |
+
37 717.3375 721.215 1000 50 0.59928995
|
| 39 |
+
38 721.215 725.0925000000001 1000 50 0.8359426
|
| 40 |
+
39 736.725 740.6025000000001 1000 50 0.52542675
|
| 41 |
+
40 740.6025 744.48 1000 50 0.56663805
|
| 42 |
+
41 756.1125 759.99 1000 50 0.51278627
|
| 43 |
+
42 775.5 779.3775 1000 50 0.5195257
|
| 44 |
+
43 779.3775 783.2550000000001 1000 50 0.9383165
|
| 45 |
+
44 806.52 810.3975 1000 50 0.74511904
|
| 46 |
+
45 814.275 818.1525 1000 50 0.72356
|
| 47 |
+
46 841.4175 845.2950000000001 1000 50 0.50421375
|
| 48 |
+
47 853.05 856.9275 1000 50 0.57719857
|
| 49 |
+
48 868.56 872.4375 1000 50 0.5337666
|
| 50 |
+
49 903.4575 907.335 1000 50 0.6433379
|
| 51 |
+
50 918.9675 922.845 1000 50 0.7503527
|
| 52 |
+
51 934.4775 938.355 1000 50 0.5536823
|
| 53 |
+
52 946.11 949.9875000000001 1000 50 0.79101586
|
| 54 |
+
53 957.7425 961.62 1000 50 0.6153837
|
| 55 |
+
54 965.4975 969.375 1000 50 0.63526756
|
| 56 |
+
55 969.375 973.2525 1000 50 0.6462273
|
| 57 |
+
56 973.2525 977.1300000000001 1000 50 0.6215711
|
| 58 |
+
57 984.885 988.7625 1000 50 0.5670401
|
| 59 |
+
58 1012.0275 1015.9050000000001 1000 50 0.6619715
|
| 60 |
+
59 1015.905 1019.7825 1000 50 0.6685599
|
| 61 |
+
60 1039.17 1043.0475000000001 1000 50 0.755065
|
| 62 |
+
61 1046.925 1050.8025 1000 50 0.51247126
|
| 63 |
+
62 1050.8025 1054.68 1000 50 0.7799243
|
| 64 |
+
63 1074.0675 1077.9450000000002 1000 50 0.798874
|
| 65 |
+
64 1136.1075 1139.9850000000001 1000 50 0.8805855
|
| 66 |
+
65 1143.8625 1147.74 1000 50 0.50031465
|
| 67 |
+
66 1182.6375 1186.515 1000 50 0.6415266
|
| 68 |
+
67 1190.3925 1194.27 1000 50 0.77716064
|
| 69 |
+
68 1198.1475 1202.025 1000 50 0.6745678
|
| 70 |
+
69 1209.78 1213.6575 1000 50 0.5046
|
| 71 |
+
70 1217.535 1221.4125000000001 1000 50 0.51890004
|
| 72 |
+
71 1310.595 1314.4725 1000 50 0.76617485
|
| 73 |
+
72 1326.105 1329.9825 1000 50 0.5635491
|
| 74 |
+
73 1337.7375 1341.615 1000 50 0.6052231
|
| 75 |
+
74 1361.0025 1364.88 1000 50 0.6599168
|
| 76 |
+
75 1364.88 1368.7575000000002 1000 50 0.6918338
|
| 77 |
+
76 1368.7575 1372.635 1000 50 0.58110696
|
| 78 |
+
77 1384.2675 1388.145 1000 50 0.80470777
|
| 79 |
+
78 1388.145 1392.0225 1000 50 0.772092
|
| 80 |
+
79 1399.7775 1403.655 1000 50 0.5838019
|
| 81 |
+
80 1415.2875 1419.165 1000 50 0.5703714
|
| 82 |
+
81 1450.185 1454.0625 1000 50 0.75559676
|
| 83 |
+
82 1492.8375 1496.7150000000001 1000 50 0.5252385
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_04-00-05_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 0.0 3.8775 1000 50 0.7659342
|
| 3 |
+
2 50.4075 54.285 1000 50 0.5111502
|
| 4 |
+
3 73.6725 77.55 1000 50 0.5522041
|
| 5 |
+
4 89.1825 93.06 1000 50 0.6636806
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_04-05-20_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 34.8975 38.775 1000 50 0.71619654
|
| 3 |
+
2 46.53 50.4075 1000 50 0.55028105
|
| 4 |
+
3 50.4075 54.285 1000 50 0.5685571
|
| 5 |
+
4 62.04 65.9175 1000 50 0.5583923
|
| 6 |
+
5 73.6725 77.55 1000 50 0.61653876
|
| 7 |
+
6 100.815 104.6925 1000 50 0.5019687
|
| 8 |
+
7 104.6925 108.57 1000 50 0.5498319
|
| 9 |
+
8 112.4475 116.325 1000 50 0.523319
|
| 10 |
+
9 147.345 151.2225 1000 50 0.5715398
|
| 11 |
+
10 166.7325 170.60999999999999 1000 50 0.58472294
|
| 12 |
+
11 221.0175 224.895 1000 50 0.6042095
|
| 13 |
+
12 228.7725 232.65 1000 50 0.5261942
|
| 14 |
+
13 279.18 283.0575 1000 50 0.6790548
|
| 15 |
+
14 294.69 298.5675 1000 50 0.88658756
|
| 16 |
+
15 302.445 306.3225 1000 50 0.5844379
|
| 17 |
+
16 314.0775 317.955 1000 50 0.5711364
|
| 18 |
+
17 325.71 329.5875 1000 50 0.672497
|
| 19 |
+
18 337.3425 341.21999999999997 1000 50 0.6838482
|
| 20 |
+
19 356.73 360.6075 1000 50 0.506336
|
| 21 |
+
20 360.6075 364.485 1000 50 0.68574035
|
| 22 |
+
21 403.26 407.1375 1000 50 0.51428175
|
| 23 |
+
22 449.79 453.6675 1000 50 0.8651263
|
| 24 |
+
23 465.3 469.1775 1000 50 0.5493179
|
| 25 |
+
24 476.9325 480.81 1000 50 0.5709537
|
| 26 |
+
25 507.9525 511.83 1000 50 0.54585946
|
| 27 |
+
26 519.585 523.4625000000001 1000 50 0.5220416
|
| 28 |
+
27 527.34 531.2175000000001 1000 50 0.5410059
|
| 29 |
+
28 535.095 538.9725000000001 1000 50 0.72252417
|
| 30 |
+
29 546.7275 550.605 1000 50 0.74286765
|
| 31 |
+
30 550.605 554.4825000000001 1000 50 0.82613313
|
| 32 |
+
31 554.4825 558.36 1000 50 0.5101495
|
| 33 |
+
32 601.0125 604.8900000000001 1000 50 0.5304
|
| 34 |
+
33 635.91 639.7875 1000 50 0.60296184
|
| 35 |
+
34 639.7875 643.6650000000001 1000 50 0.5382861
|
| 36 |
+
35 655.2975 659.1750000000001 1000 50 0.5297241
|
| 37 |
+
36 666.93 670.8075 1000 50 0.621523
|
| 38 |
+
37 686.3175 690.195 1000 50 0.75933427
|
| 39 |
+
38 697.95 701.8275000000001 1000 50 0.7845717
|
| 40 |
+
39 709.5825 713.46 1000 50 0.6805964
|
| 41 |
+
40 736.725 740.6025000000001 1000 50 0.73043954
|
| 42 |
+
41 744.48 748.3575000000001 1000 50 0.51064
|
| 43 |
+
42 756.1125 759.99 1000 50 0.8632106
|
| 44 |
+
43 763.8675 767.745 1000 50 0.5147054
|
| 45 |
+
44 775.5 779.3775 1000 50 0.71989703
|
| 46 |
+
45 779.3775 783.2550000000001 1000 50 0.50403214
|
| 47 |
+
46 783.255 787.1325 1000 50 0.5747235
|
| 48 |
+
47 806.52 810.3975 1000 50 0.7938242
|
| 49 |
+
48 814.275 818.1525 1000 50 0.5156269
|
| 50 |
+
49 818.1525 822.0300000000001 1000 50 0.6604109
|
| 51 |
+
50 841.4175 845.2950000000001 1000 50 0.54860944
|
| 52 |
+
51 845.295 849.1725 1000 50 0.5326691
|
| 53 |
+
52 849.1725 853.0500000000001 1000 50 0.96912056
|
| 54 |
+
53 860.805 864.6825 1000 50 0.5358853
|
| 55 |
+
54 872.4375 876.315 1000 50 0.5902473
|
| 56 |
+
55 922.845 926.7225000000001 1000 50 0.52094966
|
| 57 |
+
56 930.6 934.4775000000001 1000 50 0.95947707
|
| 58 |
+
57 942.2325 946.11 1000 50 0.5568109
|
| 59 |
+
58 961.62 965.4975000000001 1000 50 0.6030713
|
| 60 |
+
59 973.2525 977.1300000000001 1000 50 0.5722379
|
| 61 |
+
60 992.64 996.5175 1000 50 0.60539764
|
| 62 |
+
61 1046.925 1050.8025 1000 50 0.6535185
|
| 63 |
+
62 1054.68 1058.5575000000001 1000 50 0.5753429
|
| 64 |
+
63 1062.435 1066.3125 1000 50 0.6893821
|
| 65 |
+
64 1074.0675 1077.9450000000002 1000 50 0.61925906
|
| 66 |
+
65 1077.945 1081.8225 1000 50 0.6268375
|
| 67 |
+
66 1081.8225 1085.7 1000 50 0.784415
|
| 68 |
+
67 1085.7 1089.5775 1000 50 0.50539637
|
| 69 |
+
68 1097.3325 1101.21 1000 50 0.76099944
|
| 70 |
+
69 1139.985 1143.8625 1000 50 0.54786134
|
| 71 |
+
70 1147.74 1151.6175 1000 50 0.538391
|
| 72 |
+
71 1159.3725 1163.25 1000 50 0.5029607
|
| 73 |
+
72 1167.1275 1171.005 1000 50 0.8406276
|
| 74 |
+
73 1174.8825 1178.76 1000 50 0.5073458
|
| 75 |
+
74 1178.76 1182.6375 1000 50 0.6157743
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_05-00-05_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 7.755 11.6325 1000 50 0.8519168
|
| 3 |
+
2 19.3875 23.265 1000 50 0.7065482
|
| 4 |
+
3 27.1425 31.02 1000 50 0.5228868
|
| 5 |
+
4 31.02 34.8975 1000 50 0.66215074
|
| 6 |
+
5 34.8975 38.775 1000 50 0.694213
|
| 7 |
+
6 69.795 73.6725 1000 50 0.7757755
|
| 8 |
+
7 77.55 81.4275 1000 50 0.7832104
|
| 9 |
+
8 89.1825 93.06 1000 50 0.6566331
|
| 10 |
+
9 93.06 96.9375 1000 50 0.58944553
|
| 11 |
+
10 96.9375 100.815 1000 50 0.55878145
|
| 12 |
+
11 104.6925 108.57 1000 50 0.7410829
|
| 13 |
+
12 127.9575 131.835 1000 50 0.69165194
|
| 14 |
+
13 139.59 143.4675 1000 50 0.79746383
|
| 15 |
+
14 143.4675 147.345 1000 50 0.9918664
|
| 16 |
+
15 166.7325 170.60999999999999 1000 50 0.989512
|
| 17 |
+
16 170.61 174.4875 1000 50 0.6965571
|
| 18 |
+
17 178.365 182.2425 1000 50 0.83577895
|
| 19 |
+
18 189.9975 193.875 1000 50 0.51267475
|
| 20 |
+
19 193.875 197.7525 1000 50 0.5540345
|
| 21 |
+
20 205.5075 209.385 1000 50 0.6198408
|
| 22 |
+
21 209.385 213.2625 1000 50 0.9075451
|
| 23 |
+
22 228.7725 232.65 1000 50 0.7090942
|
| 24 |
+
23 232.65 236.5275 1000 50 0.546338
|
| 25 |
+
24 240.405 244.2825 1000 50 0.7075751
|
| 26 |
+
25 244.2825 248.16 1000 50 0.92911464
|
| 27 |
+
26 252.0375 255.915 1000 50 0.9545463
|
| 28 |
+
27 255.915 259.7925 1000 50 0.87419856
|
| 29 |
+
28 267.5475 271.425 1000 50 0.74750215
|
| 30 |
+
29 271.425 275.3025 1000 50 0.7619019
|
| 31 |
+
30 279.18 283.0575 1000 50 0.5040308
|
| 32 |
+
31 286.935 290.8125 1000 50 0.6829701
|
| 33 |
+
32 294.69 298.5675 1000 50 0.7550749
|
| 34 |
+
33 310.2 314.0775 1000 50 0.53570014
|
| 35 |
+
34 325.71 329.5875 1000 50 0.51625097
|
| 36 |
+
35 337.3425 341.21999999999997 1000 50 0.558227
|
| 37 |
+
36 352.8525 356.73 1000 50 0.542528
|
| 38 |
+
37 356.73 360.6075 1000 50 0.732914
|
| 39 |
+
38 360.6075 364.485 1000 50 0.6010917
|
| 40 |
+
39 364.485 368.3625 1000 50 0.53385454
|
| 41 |
+
40 391.6275 395.505 1000 50 0.7137319
|
| 42 |
+
41 395.505 399.3825 1000 50 0.9997557
|
| 43 |
+
42 399.3825 403.26 1000 50 0.95312613
|
| 44 |
+
43 403.26 407.1375 1000 50 0.9119531
|
tests/test_files/test_annoted_files/thresh_0.5/N1/2021-03-10_2kHz/channelA_2021-03-10_05-10-39_annot_2022-11-30_01.txt
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Selection Begin Time (s) End Time (s) High Freq (Hz) Low Freq (Hz) Prediction/Comments
|
| 2 |
+
1 19.3875 23.265 1000 50 0.7046817
|
| 3 |
+
2 62.04 65.9175 1000 50 0.89541245
|
| 4 |
+
3 77.55 81.4275 1000 50 0.5546575
|
| 5 |
+
4 81.4275 85.30499999999999 1000 50 0.67314935
|
| 6 |
+
5 89.1825 93.06 1000 50 0.6299111
|
| 7 |
+
6 93.06 96.9375 1000 50 0.9997956
|
| 8 |
+
7 108.57 112.44749999999999 1000 50 0.56945264
|
| 9 |
+
8 120.2025 124.08 1000 50 0.5600914
|
| 10 |
+
9 124.08 127.9575 1000 50 0.99446577
|
| 11 |
+
10 139.59 143.4675 1000 50 0.6193613
|
| 12 |
+
11 143.4675 147.345 1000 50 0.51982135
|
| 13 |
+
12 147.345 151.2225 1000 50 0.9659394
|
| 14 |
+
13 162.855 166.7325 1000 50 0.9561064
|
| 15 |
+
14 170.61 174.4875 1000 50 0.7320249
|
| 16 |
+
15 197.7525 201.63 1000 50 0.9880733
|
| 17 |
+
16 224.895 228.7725 1000 50 0.639691
|
| 18 |
+
17 236.5275 240.405 1000 50 0.68663204
|
| 19 |
+
18 244.2825 248.16 1000 50 0.5863455
|
| 20 |
+
19 255.915 259.7925 1000 50 0.98858786
|
| 21 |
+
20 263.67 267.5475 1000 50 0.5719061
|
| 22 |
+
21 286.935 290.8125 1000 50 0.50176364
|
| 23 |
+
22 290.8125 294.69 1000 50 0.71817106
|
| 24 |
+
23 314.0775 317.955 1000 50 0.6869853
|
| 25 |
+
24 325.71 329.5875 1000 50 0.5520363
|
| 26 |
+
25 333.465 337.3425 1000 50 0.6743646
|
| 27 |
+
26 368.3625 372.24 1000 50 0.508152
|
| 28 |
+
27 383.8725 387.75 1000 50 0.76186216
|
| 29 |
+
28 391.6275 395.505 1000 50 0.6509082
|
| 30 |
+
29 395.505 399.3825 1000 50 0.5752171
|
| 31 |
+
30 403.26 407.1375 1000 50 0.72370225
|
| 32 |
+
31 461.4225 465.3 1000 50 0.50161684
|
| 33 |
+
32 465.3 469.1775 1000 50 0.566452
|
| 34 |
+
33 476.9325 480.81 1000 50 0.6763864
|
| 35 |
+
34 488.565 492.4425 1000 50 0.5415367
|
| 36 |
+
35 492.4425 496.32 1000 50 0.628716
|
| 37 |
+
36 507.9525 511.83 1000 50 0.9948579
|
| 38 |
+
37 515.7075 519.585 1000 50 0.98039323
|
| 39 |
+
38 531.2175 535.095 1000 50 0.59561306
|
| 40 |
+
39 535.095 538.9725000000001 1000 50 0.74321645
|
| 41 |
+
40 538.9725 542.85 1000 50 0.7436308
|
| 42 |
+
41 542.85 546.7275000000001 1000 50 0.7321441
|
| 43 |
+
42 550.605 554.4825000000001 1000 50 0.6311483
|
| 44 |
+
43 562.2375 566.115 1000 50 0.68326
|
| 45 |
+
44 573.87 577.7475000000001 1000 50 0.7051835
|
| 46 |
+
45 585.5025 589.3800000000001 1000 50 0.6416048
|
| 47 |
+
46 589.38 593.2575 1000 50 0.8965987
|
| 48 |
+
47 597.135 601.0125 1000 50 0.5137316
|
| 49 |
+
48 604.89 608.7675 1000 50 0.6676253
|
| 50 |
+
49 620.4 624.2775 1000 50 0.65750295
|
| 51 |
+
50 624.2775 628.1550000000001 1000 50 0.76009196
|
| 52 |
+
51 628.155 632.0325 1000 50 0.6924573
|
| 53 |
+
52 639.7875 643.6650000000001 1000 50 0.91235334
|
| 54 |
+
53 647.5425 651.4200000000001 1000 50 0.74130136
|
| 55 |
+
54 651.42 655.2975 1000 50 0.5356798
|
| 56 |
+
55 663.0525 666.9300000000001 1000 50 0.6730036
|
| 57 |
+
56 666.93 670.8075 1000 50 0.78788316
|
| 58 |
+
57 678.5625 682.44 1000 50 0.9130233
|
| 59 |
+
58 701.8275 705.705 1000 50 0.7925505
|
| 60 |
+
59 705.705 709.5825000000001 1000 50 0.67838407
|
| 61 |
+
60 744.48 748.3575000000001 1000 50 0.6167371
|
| 62 |
+
61 759.99 763.8675000000001 1000 50 0.59807014
|
| 63 |
+
62 771.6225 775.5 1000 50 0.52345234
|
| 64 |
+
63 787.1325 791.0100000000001 1000 50 0.7457069
|
| 65 |
+
64 794.8875 798.7650000000001 1000 50 0.5075276
|
| 66 |
+
65 798.765 802.6425 1000 50 0.6309759
|
| 67 |
+
66 806.52 810.3975 1000 50 0.6761872
|
| 68 |
+
67 814.275 818.1525 1000 50 0.786669
|
| 69 |
+
68 818.1525 822.0300000000001 1000 50 0.5443038
|
| 70 |
+
69 825.9075 829.7850000000001 1000 50 0.9998741
|
| 71 |
+
70 837.54 841.4175 1000 50 0.8319586
|
| 72 |
+
71 841.4175 845.2950000000001 1000 50 0.5379799
|
| 73 |
+
72 849.1725 853.0500000000001 1000 50 0.6535889
|