vskode commited on
Commit
c96678c
·
0 Parent(s):

initial commit without binaries or large files for huggingface

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +16 -0
  2. .pre-commit-config.yaml +6 -0
  3. LICENSE +21 -0
  4. README.md +328 -0
  5. acodet/annotate.py +229 -0
  6. acodet/annotation_mappers.json +11 -0
  7. acodet/augmentation.py +185 -0
  8. acodet/combine_annotations.py +293 -0
  9. acodet/create_session_file.py +29 -0
  10. acodet/evaluate.py +253 -0
  11. acodet/front_end/help_strings.py +65 -0
  12. acodet/front_end/st_annotate.py +262 -0
  13. acodet/front_end/st_generate_data.py +115 -0
  14. acodet/front_end/st_train.py +92 -0
  15. acodet/front_end/st_visualization.py +286 -0
  16. acodet/front_end/utils.py +238 -0
  17. acodet/funcs.py +747 -0
  18. acodet/global_config.py +144 -0
  19. acodet/hourly_presence.py +765 -0
  20. acodet/humpback_model_dir/README.md +15 -0
  21. acodet/humpback_model_dir/front_end.py +105 -0
  22. acodet/humpback_model_dir/humpback_model.py +384 -0
  23. acodet/humpback_model_dir/leaf_pcen.py +103 -0
  24. acodet/models.py +318 -0
  25. acodet/plot_utils.py +466 -0
  26. acodet/split_daily_annots.py +39 -0
  27. acodet/src/imgs/annotation_output.png +0 -0
  28. acodet/src/imgs/gui_sequence_limit.png +0 -0
  29. acodet/src/imgs/sequence_limit.png +0 -0
  30. acodet/tfrec.py +434 -0
  31. acodet/train.py +290 -0
  32. advanced_config.yml +106 -0
  33. macM1_requirements/requirements_m1-1.txt +11 -0
  34. macM1_requirements/requirements_m1-2.txt +1 -0
  35. pyproject.toml +3 -0
  36. requirements.txt +16 -0
  37. run.py +80 -0
  38. simple_config.yml +99 -0
  39. streamlit_app.py +94 -0
  40. tests/test.py +132 -0
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
+ ![Annotation Output](acodet/src/imgs/annotation_output.png)
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
+ ![Sequence Limit](acodet/src/imgs/sequence_limit.png)
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
+ ![alt text](acodet/src/imgs/gui_sequence_limit.png)
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