diff --git a/README.md b/README.md index 824bfbf53..080d1f275 100644 --- a/README.md +++ b/README.md @@ -1,235 +1,9 @@ -

Review the license!!

-

You may not use BirdNET-Pi to develop a commercial product!!!!

-

- BirdNET-Pi -

-

-A realtime acoustic bird classification system for the Raspberry Pi 5, 4B, 400, 3B+, and 0W2 -

-

- -

-

-Icon made by Freepik from www.flaticon.com -

- -## About this fork: -I've been building on [mcguirepr89's](https://github.com/mcguirepr89/BirdNET-Pi) most excellent work to further update and improve BirdNET-Pi. Maybe someone will find it useful. - -Changes include: - - - Web ui is much more responsive - - Daily charts now include all species, not just top/bottom 10 - - Bump apprise version, so more notification type are possible - - Swipe events on Daily Charts (by @croisez) - - Support for 'Species range model V2.4 - V2' - - Bookworm support - - Experimental support for writing transient files to tmpfs - - Rework analysis to consolidate analysis/server/extraction. Should make analysis more robust and slightly more efficient, especially on installations with a large number of recordings - - Bump tflite_runtime to 2.11.0, it is faster - - Rework daily_plot.py (chart_viewer) to run as a daemon to avoid the very expensive startup - - Lots of fixes & cleanups - -!! note: see 'Migrating' on how to migrate from mcguirepr89 - -## Introduction -BirdNET-Pi is built on the [BirdNET framework](https://github.com/kahst/BirdNET-Analyzer) by [**@kahst**](https://github.com/kahst) using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world. - -Check out birds from around the world -- [BirdWeather](https://app.birdweather.com)
- -Currently listening in these countries . . . that I know of . . . -- The United States -- Germany -- South Africa -- France -- Austria -- Sweden -- Scotland -- Norway -- England -- Italy -- Finland -- Australia -- Canada -- Switzerland -- Romania -- Spain -- New Zealand -- Russia -- Croatia -- Belgium -- Israel -- Ireland -- Denmark -- Costa Rica -- The Philippines -- Hungary -- South Sudan -- Argentina -- Brazil -- Thailand -- Colombia -- Estonia -- Tasmania -- Luxembourg -- Crete -- Rwanda -- Oman -- Belarus -- Czech Republic -- Japan - -## Features -* **24/7 recording and automatic identification** of bird songs, chirps, and peeps using BirdNET machine learning -* **Automatic extraction and cataloguing** of bird clips from full-length recordings -* **Tools to visualize your recorded bird data** and analyze trends -* **Live audio stream and spectrogram** -* **Automatic disk space management** that periodically purges old audio files -* [BirdWeather](https://app.birdweather.com) integration -- you can request a BirdWeather ID from BirdNET-Pi's "Tools" > "Settings" page -* Web interface access to all data and logs provided by [Caddy](https://caddyserver.com) -* [GoTTY](https://github.com/yudai/gotty) and [GoTTY x86](https://github.com/sorenisanerd/gotty) Web Terminal -* [Tiny File Manager](https://tinyfilemanager.github.io/) -* FTP server included -* SQLite3 Database -* [Adminer](https://www.adminer.org/) database maintenance -* [phpSysInfo](https://github.com/phpsysinfo/phpsysinfo) -* [Apprise Notifications](https://github.com/caronc/apprise) supporting 90+ notification platforms -* Localization supported - -## Requirements -* A Raspberry Pi 5, Raspberry 4B, Raspberry Pi 400, Raspberry Pi 3B+, or Raspberry Pi 0W2 (The 3B+ and 0W2 must run on RaspiOS-ARM64-**Lite**) -* An SD Card with the **_64-bit version of RaspiOS_** installed (please use Bookworm) -- Lite is recommended, but the installation works on RaspiOS-ARM64-Full as well. Downloads available within the [Raspberry Pi Imager](https://www.raspberrypi.com/software/). -* A USB Microphone or Sound Card - -## Installation -[A comprehensive installation guide is available here](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Installation-Guide). This guide is slightly out-dated: make sure to pick Bookworm, also the curl command is still pointing to mcguirepr89's repo. - -Please note that installing BirdNET-Pi on top of other servers is not supported. If this is something that you require, please open a discussion for your idea and inquire about how to contribute to development. - -[Raspberry Pi 3B[+] and 0W2 installation guide available here](https://github.com/mcguirepr89/BirdNET-Pi/wiki/RPi0W2-Installation-Guide) - -The system can be installed with: -``` -curl -s https://raw.githubusercontent.com/Nachtzuster/BirdNET-Pi/main/newinstaller.sh | bash -``` -The installer takes care of any and all necessary updates, so you can run that as the very first command upon the first boot, if you'd like. - -The installation creates a log in `$HOME/installation-$(date "+%F").txt`. -## Access -The BirdNET-Pi can be accessed from any web browser on the same network: -- http://birdnetpi.local OR your Pi's IP address -- Default Basic Authentication Username: birdnet -- Password is empty by default. Set this in "Tools" > "Settings" > "Advanced Settings" - -Please take a look at the [wiki](https://github.com/mcguirepr89/BirdNET-Pi/wiki) and [discussions](https://github.com/mcguirepr89/BirdNET-Pi/discussions) for information on -- [BirdNET-Pi's Deep Convolutional Neural Network(s)](https://github.com/mcguirepr89/BirdNET-Pi/wiki/BirdNET-Pi:-some-theory-on-classification-&-some-practical-hints) -- [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi) -- [backing up and restoring your database](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Backup-and-Restore-the-Database) -- [adjusting your sound card settings](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Adjusting-your-sound-card) -- [suggested USB microphones](https://github.com/mcguirepr89/BirdNET-Pi/discussions/39) -- [building your own microphone](https://github.com/DD4WH/SASS/wiki/Stereo--(Mono)-recording-low-noise-low-cost-system) -- [privacy concerns and options](https://github.com/mcguirepr89/BirdNET-Pi/discussions/166) -- [beta testing](https://github.com/mcguirepr89/BirdNET-Pi/discussions/11) -- [and more!](https://github.com/mcguirepr89/BirdNET-Pi/discussions) - - -## Updating - -Use the web interface and go to "Tools" > "System Controls" > "Update." If you encounter any issues with that, or suspect that the update did not work for some reason, please save its output and post it in an issue where we can help. - -## Backup and Restore - -This script is primary meant for migrating your data for one system to another. Since the time required to create or restore a backup depends on the size of the data set and the speed of the storage, this could take quite a while. -These examples assume the backup medium is mounted on `/mnt` - -To backup: -```commandline -./scripts/backup_data.sh -a backup -f /mnt/birds/backup-2024-07-09.tar -``` -To restore: -```commandline -./scripts/backup_data.sh -a restore -f /mnt/birds/backup-2024-07-09.tar -``` - -## Uninstallation -``` -/usr/local/bin/uninstall.sh && cd ~ && rm -drf BirdNET-Pi -``` -## Migrating -Before switching, make sure your installation is fully up-to-date. Also make sure to have a backup, that is also the only way to get back to the original BirdNET-Pi. -Please note that upgrading your underlying OS to Bookworm is not going to work. Please stick to Bullseye. If you do want Bookworm, you need to start from a fresh install and copy back your data. (remember the backup!) - -Run these commands to migrate to this repo: -``` -git remote remove origin -git remote add origin https://github.com/Nachtzuster/BirdNET-Pi.git -./scripts/update_birdnet.sh -``` -## Troubleshooting and Ideas -*Hint: A lot of weird problems can be solved by simply restarting the core services. Do this from the web interface "Tools" > "Services" > "Restart Core Services" -Having trouble or have an idea? *Submit an issue for trouble* and a *discussion for ideas*. Please do *not* submit an issue as a discussion -- the issue tracker solicits information that is needed for anyone to help -- discussions are *not for issues*. - -PLEASE search the repo for your issue before creating a new one. This repo has nothing to do with the validity of the detection results, so please do not start any issues around "False positives." - -## Sharing -Please join a Discussion!! and please join [BirdWeather!!](https://app.birdweather.com) -I hope that if you find BirdNET-Pi has been worth your time, you will share your setup, results, customizations, etc. [HERE](https://github.com/mcguirepr89/BirdNET-Pi/discussions/69) and will consider [making your installation public](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi). - -## Homeassistant addon - -BirdNET-Pi can also be run as a [Homeassistant](https://www.home-assistant.io/) addon through docker. -For more information : https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi/README.md - -## Cool Links - -- [Marie Lelouche's Out of Spaces](https://www.lestanneries.fr/exposition/marie-lelouche-out-of-spaces/) using BirdNET-Pi in post-sculpture VR! [Press Kit](https://github.com/mcguirepr89/BirdNET-Pi-assets/blob/main/dp_out_of_spaces_marie_lelouche_digital_05_01_22.pdf) -- [Research on noded BirdNET-Pi networks for farming](https://github.com/mcguirepr89/BirdNET-Pi-assets/blob/main/G23_Report_ModelBasedSysEngineering_FarmMarkBirdDetector_V1__Copy_.pdf) -- [PixCams Build Guide](https://pixcams.com/building-a-birdnet-pi-real-time-acoustic-bird-id-station/) -- [Core-Electronics](https://core-electronics.com.au/projects/bird-calls-raspberry-pi) Build Article -- [RaspberryPi.com Blog Post](https://www.raspberrypi.com/news/classify-birds-acoustically-with-birdnet-pi/) -- [MagPi Issue 119 Showcase Article](https://magpi.raspberrypi.com/issues/119/pdf) - - -### Internationalization: -The bird names are in English by default, but other localized versions are available thanks to the wonderful efforts of [@patlevin](https://github.com/patlevin). Use the web interface's "Tools" > "Settings" and select your "Database Language" to have the detections in your language. - -Current database languages include the list below: -| Language | Missing Species out of 6,362 | Missing labels (%) | -| -------- | ------- | ------ | -| Afrikaans | 5774 | 90.76% | -| Catalan | 544 | 8.55% | -| Chinese | 264 | 4.15% | -| Croatian | 370 | 5.82% | -| Czech | 683 | 10.74% | -| Danish | 460 | 7.23% | -| Dutch | 264 | 4.15% | -| Estonian | 3171 | 49.84% | -| Finnish | 518 | 8.14% | -| French | 264 | 4.15% | -| German | 264 | 4.15% | -| Hungarian | 2688 | 42.25% | -| Icelandic | 5588 | 87.83% | -| Indonesian | 5550 | 87.24% | -| Italian | 524 | 8.24% | -| Japanese | 640 | 10.06% | -| Latvian | 4821 | 75.78% | -| Lithuanian | 597 | 9.38% | -| Norwegian | 325 | 5.11% | -| Polish | 265 | 4.17% | -| Portuguese | 2742 | 43.10% | -| Russian | 808 | 12.70% | -| Slovak | 264 | 4.15% | -| Slovenian | 5532 | 86.95% | -| Spanish | 348 | 5.47% | -| Swedish | 264 | 4.15% | -| Thai | 5580 | 87.71% | -| Ukrainian | 646 | 10.15% | - -## Screenshots -![Overview](docs/overview.png) -![chrome_HNMJKSPwV0](https://user-images.githubusercontent.com/103586016/217896322-aee3ecc4-e40e-40df-ade1-79f05ded21f2.png) - - -## :thinking: -Are you a lucky ducky with a spare Raspberry Pi? [Try Folding@home!](https://foldingathome.org/) +Additional PR vs Nachtzuster, used for HA addon +- New graph : https://github.com/alexbelgium/BirdNET-Pi/tree/new_daily_graph +- Confirmed species : https://github.com/alexbelgium/BirdNET-Pi/tree/confirmed_species_feature +- SNR : https://github.com/alexbelgium/BirdNET-Pi/tree/SNR +- New species on top : https://github.com/alexbelgium/BirdNET-Pi/tree/new_species + +To do : +- observation.org upload +- High/low pass diff --git a/homepage/images/check.svg b/homepage/images/check.svg new file mode 100644 index 000000000..a12292f51 --- /dev/null +++ b/homepage/images/check.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/homepage/images/question.svg b/homepage/images/question.svg new file mode 100644 index 000000000..f88ab1e38 --- /dev/null +++ b/homepage/images/question.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/newinstaller.sh b/newinstaller.sh index fb0cac89a..7484a2e85 100755 --- a/newinstaller.sh +++ b/newinstaller.sh @@ -45,7 +45,7 @@ if [[ ! -z $PACKAGES_MISSING ]] ; then fi branch=main -git clone -b $branch --depth=1 https://github.com/Nachtzuster/BirdNET-Pi.git ${HOME}/BirdNET-Pi && +git clone -b $branch --depth=1 https://github.com/alexbelgium/BirdNET-Pi.git ${HOME}/BirdNET-Pi && $HOME/BirdNET-Pi/scripts/install_birdnet.sh if [ ${PIPESTATUS[0]} -eq 0 ];then diff --git a/scripts/advanced.php b/scripts/advanced.php index 765016cb2..a60931269 100644 --- a/scripts/advanced.php +++ b/scripts/advanced.php @@ -196,6 +196,15 @@ $contents = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=0", $contents); } + if(isset($_GET["confirm_species"])) { + $confirm_species = 1; + if(strcmp($CONFIRM_SPECIES,$config['CONFIRM_SPECIES']) !== 0) { + $contents = preg_replace("/CONFIRM_SPECIES=.*/", "CONFIRM_SPECIES=$confirm_species", $contents); + } + } else { + $contents = preg_replace("/CONFIRM_SPECIES=.*/", "CONFIRM_SPECIES=0", $contents); + } + if(isset($_GET["custom_image"])) { $custom_image = $_GET["custom_image"]; if(strcmp($custom_image,$config['CUSTOM_IMAGE']) !== 0) { @@ -428,12 +437,19 @@ function collectrtspUrls() {

Options

- >
-

This allows you to quiet the display of how many commits your installation is behind by relative to the Github repo. This number appears next to "Tools" when you're 50 or more commits behind.

+ > +

This allows you to quiet the display of how many commits your installation is behind by relative to the Github repo. This number appears next to "Tools" when you're 50 or more commits behind.


- >
-

This allows you to remove the axes and labels of the spectrograms that are generated by Sox for each detection for a cleaner appearance.

+ > +

This allows you to remove the axes and labels of the spectrograms that are generated by Sox for each detection for a cleaner appearance.


+

+ +
+

Option : Confirmed Species

+ + > +

This allows to visually mark species that were manually confirmed as existing in the area. A new question mark appears next to species names in the Recordings page. Clicking it changes the icon to a checkmark, and add the species to the file confirmed_species_list.txt


fetchArray(SQLITE3_ASSOC)) { $name = $results['Com_Name']; $dir_name = str_replace("'", '', $name); + if(isset($_GET['only_confirmed']) && in_array(str_replace("'", "", $results['Sci_Name'] . "_" . $results['Com_Name']), $confirmed_species)) { + continue; + } if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_", $dir_name)) !== false){ $birds[] = $name; + $birds_sciname_name[] = $results['Sci_Name'] . "_" . $name; if ($_GET['sort'] == "confidence") { $confidence[] = ' (' . round($results['MaxConfidence'] * 100) . '%)'; } @@ -503,7 +574,13 @@ function changeDetection(filename,copylink=false) { if ($index < count($birds)) { ?>
diff --git a/scripts/clear_all_data.sh b/scripts/clear_all_data.sh index ee9d88d02..3a418ee08 100755 --- a/scripts/clear_all_data.sh +++ b/scripts/clear_all_data.sh @@ -25,6 +25,7 @@ echo "Re-creating necessary directories" sudo -u ${USER} ln -fs $(dirname $my_dir)/exclude_species_list.txt $my_dir sudo -u ${USER} ln -fs $(dirname $my_dir)/include_species_list.txt $my_dir sudo -u ${USER} ln -fs $(dirname $my_dir)/whitelist_species_list.txt $my_dir +sudo -u ${USER} ln -fs $(dirname $my_dir)/confirmed_species_list.txt $my_dir sudo -u ${USER} ln -fs $(dirname $my_dir)/homepage/* ${EXTRACTED} sudo -u ${USER} ln -fs $(dirname $my_dir)/model/labels.txt ${my_dir} sudo -u ${USER} ln -fs $my_dir ${EXTRACTED} diff --git a/scripts/daily_plot.py b/scripts/daily_plot.py index cce51b16f..8aedaa1fd 100755 --- a/scripts/daily_plot.py +++ b/scripts/daily_plot.py @@ -1,228 +1,250 @@ +#=============================================================================== +#=== daily_plot.py (adjusted version @jmtmp) ========================================== +#=============================================================================== +#=== 2024-04-19: new version +#=== 2024-04-28: new custom formatting for millions (my_int_fmt function) +#=== new formatting of total occurence in semi-monthly plot +#=== 2024-09-01: updated suptitle and xlabels formatting +#=== 2024-09-05: Daemon implementing +#=== 2024-09-26: transparent first column +#=== 2024-10-02: code refactor +#=============================================================================== + import argparse -import os import sqlite3 -import textwrap -from datetime import datetime -from time import sleep - -import matplotlib.font_manager as font_manager -import matplotlib.pyplot as plt -import numpy as np +import os import pandas as pd import seaborn as sns +import matplotlib.pyplot as plt +import matplotlib.font_manager as font_manager from matplotlib import rcParams -from matplotlib.colors import LogNorm - +from matplotlib.colors import LogNorm, TwoSlopeNorm +from matplotlib.ticker import FormatStrFormatter +from datetime import datetime +from time import sleep +from functools import lru_cache from utils.helpers import DB_PATH, get_settings - -def get_data(now=None): - conn = sqlite3.connect(DB_PATH) - if now is None: - now = datetime.now() - df = pd.read_sql_query(f"SELECT * from detections WHERE Date = DATE('{now.strftime('%Y-%m-%d')}')", - conn) - - # Convert Date and Time Fields to Panda's format - df['Date'] = pd.to_datetime(df['Date']) - df['Time'] = pd.to_datetime(df['Time'], unit='ns') - - # Add round hours to dataframe - df['Hour of Day'] = [r.hour for r in df.Time] - - return df, now - - -# Function to show value on bars - from https://stackoverflow.com/questions/43214978/seaborn-barplot-displaying-values -def show_values_on_bars(ax, label): - conf = get_settings() - - for i, p in enumerate(ax.patches): - x = p.get_x() + p.get_width() * 0.9 - y = p.get_y() + p.get_height() / 2 - # Species confidence - # value = '{:.0%}'.format(label.iloc[i]) - # Species Count Total - value = '{:n}'.format(p.get_width()) - bbox = {'facecolor': 'lightgrey', 'edgecolor': 'none', 'pad': 1.0} - if conf['COLOR_SCHEME'] == "dark": - color = 'black' - else: - color = 'darkgreen' - - ax.text(x, y, value, bbox=bbox, ha='center', va='center', size=9, color=color) - - -def wrap_width(txt): - # try to estimate wrap width - w = 16 - for c in txt: - if c in ['M', 'm', 'W', 'w']: - w -= 0.33 - if c in ['I', 'i', 'j', 'l']: - w += 0.33 - return round(w) - - -def create_plot(df_plt_today, now, is_top=None): - if is_top is not None: - readings = 10 - if is_top: - plt_selection_today = (df_plt_today['Com_Name'].value_counts()[:readings]) - else: - plt_selection_today = (df_plt_today['Com_Name'].value_counts()[-readings:]) - else: - plt_selection_today = df_plt_today['Com_Name'].value_counts() - readings = len(df_plt_today['Com_Name'].value_counts()) - - df_plt_selection_today = df_plt_today[df_plt_today.Com_Name.isin(plt_selection_today.index)] - - conf = get_settings() - - # Set up plot axes and titles - height = max(readings / 3, 0) + 1.06 - if conf['COLOR_SCHEME'] == "dark": - facecolor = 'darkgrey' - else: - facecolor = 'none' - - f, axs = plt.subplots(1, 2, figsize=(10, height), gridspec_kw=dict(width_ratios=[3, 6]), facecolor=facecolor) - - # generate y-axis order for all figures based on frequency - freq_order = df_plt_selection_today['Com_Name'].value_counts().index - - # make color for max confidence --> this groups by name and calculates max conf - confmax = df_plt_selection_today.groupby('Com_Name')['Confidence'].max() - # reorder confmax to detection frequency order - confmax = confmax.reindex(freq_order) - - # norm values for color palette - norm = plt.Normalize(confmax.values.min(), confmax.values.max()) - if is_top or is_top is None: - # Set Palette for graphics - if conf['COLOR_SCHEME'] == "dark": - pal = "Greys" - colors = plt.cm.Greys(norm(confmax)).tolist() - else: - pal = "Greens" - colors = plt.cm.Greens(norm(confmax)).tolist() - if is_top: - plot_type = "Top" - else: - plot_type = 'All' - name = "Combo" - else: - # Set Palette for graphics - pal = "Reds" - colors = plt.cm.Reds(norm(confmax)).tolist() - plot_type = "Bottom" - name = "Combo2" - - # Generate frequency plot - plot = sns.countplot(y='Com_Name', hue='Com_Name', legend=False, data=df_plt_selection_today, - palette=colors, order=freq_order, ax=axs[0], edgecolor='lightgrey') - - # Prints Max Confidence on bars - show_values_on_bars(axs[0], confmax) - - # Try plot grid lines between bars - problem at the moment plots grid lines on bars - want between bars - yticklabels = ['\n'.join(textwrap.wrap(ticklabel.get_text(), wrap_width(ticklabel.get_text()))) for ticklabel in plot.get_yticklabels()] - # Next two lines avoid a UserWarning on set_ticklabels() requesting a fixed number of ticks - yticks = plot.get_yticks() - plot.set_yticks(yticks) - plot.set_yticklabels(yticklabels, fontsize=10) - plot.set(ylabel=None) - plot.set(xlabel="Detections") - - # Generate crosstab matrix for heatmap plot - heat = pd.crosstab(df_plt_selection_today['Com_Name'], df_plt_selection_today['Hour of Day']) - - # Order heatmap Birds by frequency of occurrance - heat.index = pd.CategoricalIndex(heat.index, categories=freq_order) - heat.sort_index(level=0, inplace=True) - - hours_in_day = pd.Series(data=range(0, 24)) - heat_frame = pd.DataFrame(data=0, index=heat.index, columns=hours_in_day) - heat = (heat+heat_frame).fillna(0) - # mask out zeros, so they do not show up in the final plot. this happens when max count/h is one - heat[heat == 0] = np.nan - - # Generatie heatmap plot - plot = sns.heatmap(heat, norm=LogNorm(), annot=True, annot_kws={"fontsize": 7}, fmt="g", cmap=pal, square=False, - cbar=False, linewidths=0.5, linecolor="Grey", ax=axs[1], yticklabels=False) - - # Set color and weight of tick label for current hour - for label in plot.get_xticklabels(): - if int(label.get_text()) == now.hour: - if conf['COLOR_SCHEME'] == "dark": - label.set_color('white') - else: - label.set_color('yellow') - - plot.set_xticklabels(plot.get_xticklabels(), rotation=0, size=8) - - # Set heatmap border - for _, spine in plot.spines.items(): - spine.set_visible(True) - - plot.set(ylabel=None) - plot.set(xlabel="Hour of Day") - # Set combined plot layout and titles - y = 1 - 8 / (height * 100) - plt.suptitle(f"{plot_type} {readings} Last Updated: {now.strftime('%Y-%m-%d %H:%M')}", y=y) - f.tight_layout() - top = 1 - 40 / (height * 100) - f.subplots_adjust(left=0.125, right=0.9, top=top, wspace=0) - - # Save combined plot - save_name = os.path.expanduser(f"~/BirdSongs/Extracted/Charts/{name}-{now.strftime('%Y-%m-%d')}.png") - plt.savefig(save_name) - plt.show() - plt.close() - +# Cache the settings to avoid redundant calls +@lru_cache(maxsize=None) +def get_settings_cached(): + return get_settings() def load_fonts(): - conf = get_settings() # Add every font at the specified location font_dir = [os.path.expanduser('~/BirdNET-Pi/homepage/static')] for font in font_manager.findSystemFonts(font_dir, fontext='ttf'): font_manager.fontManager.addfont(font) # Set font family globally - if conf['DATABASE_LANG'] in ['ja', 'zh']: + lang = get_settings_cached()['DATABASE_LANG'] + if lang in ['ja', 'zh']: rcParams['font.family'] = 'Noto Sans JP' - elif conf['DATABASE_LANG'] == 'th': + elif lang == 'th': rcParams['font.family'] = 'Noto Sans Thai' else: rcParams['font.family'] = 'Roboto Flex' +def my_int_fmt(number, converthundreds=False): + try: + number = float(number) + except (ValueError, TypeError): + return str(number) + if number >= 9_500_000: + return f"{round(number / 1_000_000)}M" + elif number >= 950: + return f"{round(number / 1_000)}k" + elif converthundreds and number >= 100: + return f".{round(number / 100)}k" + else: + return str(int(number)) + +def clr_plot_facecolor(): + # Update colors according to color scheme + if get_settings_cached()['COLOR_SCHEME'] == "dark": + return 'darkgrey' + else: + return 'none' + +def clr_current_ticklabel(): + # Update colors according to color scheme + if get_settings_cached()['COLOR_SCHEME'] == "dark": + return 'white' + else: + return 'red' + +def my_heatmap(axis, crosstable, clrmap, clrnorm, annotfmt='', annotsize='medium'): + # annotsize: float or {'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'} + hm_axes = sns.heatmap(crosstable, cmap=clrmap, norm=clrnorm, cbar=False, linewidths=0.5, linecolor="Silver", ax=axis, + annot=(annotfmt != ''), fmt=annotfmt, annot_kws={"fontsize": annotsize}) + # Set border + for _, spine in hm_axes.spines.items(): + spine.set_visible(True) + return hm_axes + +def get_daily_plot_data(conn, now): + sql_fields = "COUNT(DISTINCT Com_Name) as Species, COUNT(Com_Name) as Detections, COUNT(DISTINCT Date) as Days" + db_entire = pd.read_sql_query(f"SELECT {sql_fields} FROM detections", conn) + db_today = pd.read_sql_query(f"SELECT {sql_fields} FROM detections WHERE Date = DATE('now')", conn) + # Prepare suptitle + avg_daily_detections = round(int(db_entire.Detections[0]) / int(db_entire.Days[0])) + plot_suptitle = f"Hourly overview updated at {now.strftime('%Y-%m-%d %H:%M:%S')}\n" + plot_suptitle += f"({db_today.Species[0]} species today, {db_entire.Species[0]} in total; " + plot_suptitle += f"{db_today.Detections[0]} detections today, {avg_daily_detections} on average)" + # Prepare dataset + sql = """ + SELECT Date, CAST(strftime('%H', Time) AS INTEGER) AS Hour, Com_Name AS Bird, + COUNT(Com_Name) AS Count, MAX(Confidence) AS Conf + FROM detections + WHERE Date = DATE('now', 'localtime') + GROUP BY Hour, Bird + """ + plot_dataframe = pd.read_sql_query(sql, conn) + return plot_suptitle, plot_dataframe + +def get_yearly_plot_data(conn, now): + sql_fields = "COUNT(DISTINCT Com_Name) as Species, COUNT(Com_Name) as Detections, COUNT(DISTINCT Date) as Days" + db_entire = pd.read_sql_query(f"SELECT {sql_fields} FROM detections", conn) + db_ytd = pd.read_sql_query(f"SELECT {sql_fields} FROM detections WHERE Date >= DATE('now','start of year')", conn) + # Prepare suptitle + plot_suptitle = (f"Semi-monthly overview updated at {now.strftime('%Y-%m-%d %H:%M:%S')} " + f"({db_ytd.Species[0]} species this year, {db_entire.Species[0]} in total)") + # Prepare dataset + sql = """ + SELECT 2 * (CAST(strftime('%m', Date) AS INTEGER) - 1) + + CASE WHEN CAST(strftime('%d', Date) AS INTEGER) < 16 THEN 0 ELSE 1 END AS Period, + strftime('%Y', Date) AS Year, Com_Name AS Bird, + COUNT(Com_Name) AS Count, MAX(Confidence) AS Conf + FROM detections + WHERE Date >= DATE('now','start of year') + GROUP BY Period, Bird + """ + plot_dataframe = pd.read_sql_query(sql, conn) + return plot_suptitle, plot_dataframe + +def create_plot(chart_name, chart_suptitle, df_birds, now, time_unit, period_col, xlabel, xtick_labels): + # Common code for data preparation + df_birds_summary = df_birds.groupby('Bird').agg({'Count': 'sum', 'Conf': 'max'}) + df_birds_ordered = df_birds_summary.sort_values(by=['Count', 'Conf'], ascending=[False, False]) + df_birds['Bird'] = pd.Categorical(df_birds['Bird'], ordered=True, categories=df_birds_ordered.index) + no_of_rows = df_birds_summary.shape[0] + total_recordings = df_birds['Count'].sum() + if no_of_rows == 0: + print("No data available for plotting.") + return + + # Prepare crosstables + df_confidences = pd.crosstab(index=df_birds['Bird'], columns=df_birds[time_unit], values=df_birds['Conf'], aggfunc='max') + df_detections = pd.crosstab(index=df_birds['Bird'], columns=df_birds[time_unit], values=df_birds['Count'], aggfunc='sum') + df_perioddata = pd.crosstab(index=df_birds['Bird'], columns=df_birds[period_col], values=df_birds['Count'], aggfunc='sum') + + # Prepare empty matrix for periods + df_empty_matrix = pd.DataFrame(data=0, index=df_perioddata.index, columns=pd.Series(data=range(len(xtick_labels)))) + df_perioddata = (df_empty_matrix + df_perioddata).fillna(0) + + # Color palettes + color_scheme = get_settings_cached()['COLOR_SCHEME'] + cmap_confi = 'PiYG' if color_scheme != "dark" else 'Greys' + cmap_count = 'Blues' if color_scheme != "dark" else 'Greys' + norm_confi = TwoSlopeNorm(vmin=0.25, vmax=1.25, vcenter=0.75) + norm_count = LogNorm(vmin=1, vmax=total_recordings) + + # Plot dimensions + row_height = 0.28 + fig_height = row_height * (no_of_rows + 4) + row_space = row_height / fig_height + + # Plot setup + f, axs = plt.subplots(1, 4, figsize=(10, fig_height), width_ratios=[5, 2, 2, 18], facecolor=clr_plot_facecolor()) + plt.subplots_adjust(left=0.02, right=0.98, top=(1 - 2 * row_space), bottom=(0 + 2 * row_space), wspace=0, hspace=0) + plt.suptitle(chart_suptitle, y=0.99) + + # Bird name column + axs[0].set_xlim(0, 1) + axs[0].set_ylim(0, len(df_confidences.index)) + axs[0].axis('off') + + # Confidence column + hm_confi = my_heatmap(axs[1], df_confidences, cmap_confi, norm_confi, annotfmt=".0%") + hm_confi.tick_params(bottom=True, left=False, labelbottom=True, labeltop=False, labelleft=True, labelrotation=0) + hm_confi.set(xlabel=None, ylabel=None, xticklabels=['max\nconfidence']) + + # Occurrence column + hm_count = my_heatmap(axs[2], df_detections, cmap_count, norm_count, annotfmt="g") + hm_count.tick_params(bottom=True, left=False, labelbottom=True, labeltop=False, labelleft=False) + hm_count.set(xlabel=None, ylabel=None, xticklabels=['total\ndetections']) + + # Apply custom annotation format + for t in hm_count.texts: + if len(t.get_text()) > 3: + t.set_text(my_int_fmt(t.get_text())) + + # Occurrence heatmap + hm_data = my_heatmap(axs[3], df_perioddata, cmap_count, norm_count, annotfmt="g", annotsize=9) + hm_data.tick_params(bottom=True, top=False, left=False, labelbottom=True, labeltop=False, + labelleft=False, labelrotation=0) + hm_data.set(xlabel=None, ylabel=None) + hm_data.set_xlabel(xlabel, labelpad=1) + hm_data.xaxis.set_major_formatter(FormatStrFormatter('%d')) + hm_data.set_xticklabels(xtick_labels) + + # Apply custom annotation format + for t in hm_data.texts: + if len(t.get_text()) > 2: + t.set_text(my_int_fmt(t.get_text(), converthundreds=True)) + + # Set tick label for current period + for idx, label in enumerate(hm_data.get_xticklabels()): + if period_col == 'Hour': + if int(label.get_text()) == now.hour: + label.set_color(clr_current_ticklabel()) + elif period_col == 'Period': + # Map current period to index + current_period = 2 * (now.month - 1) + (0 if now.day < 16 else 1) + if idx == current_period: + label.set_color(clr_current_ticklabel()) + + # Save the plot + plt.savefig(os.path.expanduser(f'~/BirdSongs/Extracted/Charts/{chart_name}.png')) + plt.show() + plt.close() def main(daemon, sleep_m): load_fonts() - last_run = None while True: - now = datetime.now() - # now = datetime.strptime('2023-12-13T23:59:59', "%Y-%m-%dT%H:%M:%S") - # now = datetime.strptime('2024-01-02T23:59:59', "%Y-%m-%dT%H:%M:%S") - # now = datetime.strptime('2024-02-26T23:59:59', "%Y-%m-%dT%H:%M:%S") - # now = datetime.strptime('2024-04-03T23:59:59', "%Y-%m-%dT%H:%M:%S") - # now = datetime.strptime('2024-04-07T23:59:59', "%Y-%m-%dT%H:%M:%S") - if last_run and now.day != last_run.day: - print("getting yesterday's dataset") - yesterday = last_run.replace(hour=23, minute=59) - data, time = get_data(yesterday) - else: - data, time = get_data(now) - if not data.empty: - create_plot(data, time) - else: - print('empty dataset') + with sqlite3.connect(DB_PATH) as conn: + now = datetime.now() + + suptitle, dataframe = get_daily_plot_data(conn, now) + create_plot( + chart_name='Combo-' + now.strftime("%Y-%m-%d"), + chart_suptitle=suptitle, + df_birds=dataframe, + now=now, + time_unit='Date', + period_col='Hour', + xlabel='hourly detections', + xtick_labels=list(range(24)) + ) + + suptitle, dataframe = get_yearly_plot_data(conn, now) + month_labels = ['Jan', '', 'Feb', '', 'Mar', '', 'Apr', '', 'May', '', 'Jun', '', 'Jul', '', + 'Aug', '', 'Sep', '', 'Oct', '', 'Nov', '', 'Dec', ''] + create_plot( + chart_name='Combo2-' + now.strftime("%Y-%m-%d"), + chart_suptitle=suptitle, + df_birds=dataframe, + now=now, + time_unit='Year', + period_col='Period', + xlabel='semi-monthly detections', + xtick_labels=month_labels + ) + if daemon: - last_run = now sleep(60 * sleep_m) else: break - if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--daemon', action='store_true') diff --git a/scripts/install_config.sh b/scripts/install_config.sh index 5fdf4521d..60408fc83 100755 --- a/scripts/install_config.sh +++ b/scripts/install_config.sh @@ -270,6 +270,10 @@ RAW_SPECTROGRAM=0 CUSTOM_IMAGE= CUSTOM_IMAGE_TITLE="" +## CONFIRM_SPECIES adds an icon next to species in the Recordings tab to keep track which species are manually confirmed +## It generates a confirmed_species_list.txt file, and allows to better visualize species that could be false positives +CONFIRM_SPECIES=0 + ## These are just for debugging LAST_RUN= THIS_RUN= diff --git a/scripts/install_services.sh b/scripts/install_services.sh index 7c297e508..e6f6a3b91 100755 --- a/scripts/install_services.sh +++ b/scripts/install_services.sh @@ -67,6 +67,7 @@ create_necessary_dirs() { sudo -u ${USER} ln -fs $my_dir/exclude_species_list.txt $my_dir/scripts sudo -u ${USER} ln -fs $my_dir/include_species_list.txt $my_dir/scripts sudo -u ${USER} ln -fs $my_dir/whitelist_species_list.txt $my_dir/scripts + sudo -u ${USER} ln -fs $my_dir/confirmed_species_list.txt $my_dir/scripts sudo -u ${USER} ln -fs $my_dir/homepage/* ${EXTRACTED} sudo -u ${USER} ln -fs $my_dir/model/labels.txt ${my_dir}/scripts sudo -u ${USER} ln -fs $my_dir/scripts ${EXTRACTED} diff --git a/scripts/overview.php b/scripts/overview.php index 29bfee391..95b5174ea 100644 --- a/scripts/overview.php +++ b/scripts/overview.php @@ -250,6 +250,49 @@ function get_chart_data($db, $force_regen = false) { die(); } +if(isset($_GET['ajax_center_chart']) && $_GET['ajax_center_chart'] == "true") { + +$statement = $db->prepare('SELECT COUNT(*) FROM detections'); +ensure_db_ok($statement); +$result = $statement->execute(); +$totalcount = $result->fetchArray(SQLITE3_ASSOC); + +$statement3 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == Date(\'now\', \'localtime\') AND TIME >= TIME(\'now\', \'localtime\', \'-1 hour\')'); +ensure_db_ok($statement3); +$result3 = $statement3->execute(); +$hourcount = $result3->fetchArray(SQLITE3_ASSOC); + +$statement5 = $db->prepare('SELECT COUNT(DISTINCT(Com_Name)) FROM detections WHERE Date == Date(\'now\',\'localtime\')'); +ensure_db_ok($statement5); +$result5 = $statement5->execute(); +$speciestally = $result5->fetchArray(SQLITE3_ASSOC); + +$statement6 = $db->prepare('SELECT COUNT(DISTINCT(Com_Name)) FROM detections'); +ensure_db_ok($statement6); +$result6 = $statement6->execute(); +$totalspeciestally = $result6->fetchArray(SQLITE3_ASSOC); + +?> + + + + + + + + + + + + + + +
TotalTodayLast HourSpecies TotalSpecies Today
+ + @@ -312,6 +355,89 @@ function setModalText(iter, title, text, authorlink, photolink, licenseurl) {
+prepare('SELECT Com_Name, Sci_Name, Date, Time, Confidence, File_Name, MAX(Confidence) as MaxConfidence FROM detections WHERE Date = DATE(\'now\', \'localtime\') AND Com_Name NOT IN (SELECT Com_Name FROM detections WHERE Date < DATE(\'now\', \'localtime\', \'-1 day\')) GROUP BY Com_Name'); +ensure_db_ok($statement7); +$result7 = $statement7->execute(); +$new_species = []; +while ($row = $result7->fetchArray(SQLITE3_ASSOC)) { + $new_species[] = $row; +} +$newspeciescount = count($new_species); + +if ($newspeciescount > 0): ?> +
+

new species detected today!

+ 5): ?> +
+ + + get_uid_from_db()['uid']) { + unset($_SESSION['images']); + $_SESSION["FLICKR_FILTER_EMAIL"] = $flickr->get_uid_from_db()['uid']; + } + + // Check if the Flickr image has been cached in the session + $key = array_search($comname, array_column($_SESSION['images'], 0)); + if ($key !== false) { + $image = $_SESSION['images'][$key]; + } else { + // Retrieve the image from Flickr API and cache it + $flickr_cache = $flickr->get_image($todaytable['Sci_Name']); + array_push($_SESSION["images"], array($comname, $flickr_cache["image_url"], $flickr_cache["title"], $flickr_cache["photos_url"], $flickr_cache["author_url"], $flickr_cache["license_url"])); + $image = $_SESSION['images'][count($_SESSION['images']) - 1]; + } + $image_url = $image[1] ?? ""; // Get the image URL if available + } + ?> + + + + + + + +

+ + +
+ + +

+ + + + +

Confidence:
+ +
+
busyTimeout(1000); +$confirmedspecies_filename = $home."/BirdNET-Pi/scripts/confirmed_species_list.txt"; +if (!file_exists($confirmedspecies_filename) || filesize($confirmedspecies_filename) == 0) { + file_put_contents($confirmedspecies_filename, "# List of confirmed species\n"); +} +$fp = @fopen($confirmedspecies_filename, 'r'); +if ($fp) { + $confirmed_species = explode("\n", fread($fp, filesize($confirmedspecies_filename))); +} else { + $confirmed_species = []; +} + if(isset($_GET['deletefile'])) { ensure_authenticated('You must be authenticated to delete files.'); if (preg_match('~^.*(\.\.\/).+$~', $_GET['deletefile'])) { @@ -68,6 +80,25 @@ } } +if(isset($_GET['confirmspecies'])) { + if(isset($_GET['confirm_add'])) { + $myfile = fopen($home."/BirdNET-Pi/scripts/confirmed_species_list.txt", "a") or die("Unable to open file!"); + $txt = $_GET['confirmspecies']; + fwrite($myfile, $txt."\n"); + fclose($myfile); + echo "OK"; + die(); + } else { + $search = $_GET['confirmspecies']; + $lines = array_filter($confirmed_species, function($line) use ($search) { + return stripos($line, $search) === false; + }); + file_put_contents($home."/BirdNET-Pi/scripts/confirmed_species_list.txt", implode("\n", $lines)); + echo "OK"; + die(); + } +} + if(isset($_GET['getlabels'])) { $labels = file('./scripts/labels.txt', FILE_IGNORE_NEW_LINES); echo json_encode($labels); @@ -137,11 +168,11 @@ session_start(); $_SESSION['date'] = $date; if(isset($_GET['sort']) && $_GET['sort'] == "occurrences") { - $statement = $db->prepare("SELECT DISTINCT(Com_Name) FROM detections WHERE Date == \"$date\" GROUP BY Com_Name ORDER BY COUNT(*) DESC"); + $statement = $db->prepare("SELECT DISTINCT(Com_Name), Sci_Name FROM detections WHERE Date == \"$date\" GROUP BY Com_Name ORDER BY COUNT(Com_Name) DESC"); } elseif(isset($_GET['sort']) && $_GET['sort'] == "confidence") { $statement = $db->prepare("SELECT Com_Name, Sci_Name, MAX(Confidence) as MaxConfidence FROM detections WHERE Date == \"$date\" GROUP BY Com_Name ORDER BY MaxConfidence DESC"); } else { - $statement = $db->prepare("SELECT DISTINCT(Com_Name) FROM detections WHERE Date == \"$date\" ORDER BY Com_Name"); + $statement = $db->prepare("SELECT DISTINCT(Com_Name), Sci_Name FROM detections WHERE Date == \"$date\" ORDER BY Com_Name"); } ensure_db_ok($statement); $result = $statement->execute(); @@ -150,11 +181,11 @@ #By Species } elseif(isset($_GET['byspecies'])) { if(isset($_GET['sort']) && $_GET['sort'] == "occurrences") { - $statement = $db->prepare('SELECT DISTINCT(Com_Name) FROM detections GROUP BY Com_Name ORDER BY COUNT(*) DESC'); + $statement = $db->prepare('SELECT DISTINCT(Com_Name), Sci_Name FROM detections GROUP BY Com_Name ORDER BY COUNT(Com_Name) DESC'); } elseif(isset($_GET['sort']) && $_GET['sort'] == "confidence") { $statement = $db->prepare('SELECT Com_Name, Sci_Name, MAX(Confidence) as MaxConfidence FROM detections GROUP BY Com_Name ORDER BY MaxConfidence DESC'); } else { - $statement = $db->prepare('SELECT DISTINCT(Com_Name) FROM detections ORDER BY Com_Name ASC'); + $statement = $db->prepare('SELECT DISTINCT(Com_Name), Sci_Name FROM detections ORDER BY Com_Name ASC'); } session_start(); ensure_db_ok($statement); @@ -210,6 +241,22 @@ function deleteDetection(filename,copylink=false) { } } +function confirmspecies(species, type) { + const xhttp = new XMLHttpRequest(); + xhttp.onload = function() { + if(this.responseText == "OK"){ + location.reload(); + } + } + if(type == "add") { + xhttp.open("GET", "play.php?confirmspecies="+species+"&confirm_add=true", true); + } else { + xhttp.open("GET", "play.php?confirmspecies="+species+"&confirm_del=true", true); + } + xhttp.send(); + elem.setAttribute("src","images/spinner.gif"); +} + function toggleLock(filename, type, elem) { const xhttp = new XMLHttpRequest(); xhttp.onload = function() { @@ -416,8 +463,16 @@ function changeDetection(filename,copylink=false) { - +

+ +

@@ -436,13 +491,18 @@ function changeDetection(filename,copylink=false) { #By Species } elseif($view == "byspecies") { $birds = array(); + $birds_sciname_name = array(); $confidence = array(); while($results=$result->fetchArray(SQLITE3_ASSOC)) { + if(isset($_GET['only_confirmed']) && in_array(str_replace("'", "", $results['Sci_Name'] . "_" . $results['Com_Name']), $confirmed_species)) { + continue; + } $name = $results['Com_Name']; $birds[] = $name; + $birds_sciname_name[] = $results['Sci_Name'] . "_" . $name; if ($_GET['sort'] == "confidence") { - $confidence[] = ' (' . round($results['MaxConfidence'] * 100) . '%)'; + $confidence[] = ' (' . round($results['MaxConfidence'] * 100) . '%)'; } } @@ -462,7 +522,13 @@ function changeDetection(filename,copylink=false) { if ($index < count($birds)) { ?>
- + - +
- "; @@ -667,14 +751,22 @@ function changeDetection(filename,copylink=false) { $result2 = $statement2->execute(); $comname = str_replace("_", " ", strtok($name, '-')); $sciname = get_sci_name($comname); + $sciname_name = $sciname . '_' . $comname; $info_url = get_info_url($sciname); $url = $info_url['URL']; echo "
$name
- $sciname
+
$name +
$sciname

- "; + "; while($results=$result2->fetchArray(SQLITE3_ASSOC)) { $comname = preg_replace('/ /', '_', $results['Com_Name']); diff --git a/scripts/server.py b/scripts/server.py index d4558a70d..64fe7b7b2 100644 --- a/scripts/server.py +++ b/scripts/server.py @@ -9,6 +9,7 @@ import numpy as np from utils.helpers import get_settings, Detection +from scipy.signal import butter, sosfilt os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ['CUDA_VISIBLE_DEVICES'] = '' @@ -241,6 +242,39 @@ def predict(sample, sensitivity): return p_sorted[:human_cutoff] +def calculate_snr(audio_signal, sample_rate=48000, bands=[(200, 500), (500, 1000), (1000, 8000)], percentile=20): + # Calculate the SNR by selecting the best frequency band and comparing it against a complementary noise band. + def bandpass_filter(signal, low_freq, high_freq): + sos = butter(4, [low_freq, high_freq], btype='bandpass', fs=sample_rate, output='sos') + return sosfilt(sos, signal) + def estimate_modulation(signal): + return np.std(signal) + np.max(np.abs(signal)) + # Normalize the audio signal + audio_signal = audio_signal / np.max(np.abs(audio_signal)) + # 1. Select the best frequency band based on modulation + modulation_metrics = {} + for band in bands: + filtered_signal = bandpass_filter(audio_signal, band[0], band[1]) + modulation_metrics[band] = estimate_modulation(filtered_signal) + # Identify the band with the highest modulation + best_band = max(modulation_metrics, key=modulation_metrics.get) + # 2. Choose a noise band different from the selected signal band + remaining_bands = [b for b in bands if b != best_band] + noise_band = remaining_bands[0] if remaining_bands else bands[-1] # Fallback to any band if needed + # 3. Apply bandpass filters to both the selected signal band and the noise band + filtered_signal = bandpass_filter(audio_signal, best_band[0], best_band[1]) + filtered_noise = bandpass_filter(audio_signal, noise_band[0], noise_band[1]) + # 4. Calculate power of the signal and noise + signal_power = np.mean(filtered_signal ** 2) + quiet_threshold = np.percentile(np.abs(filtered_noise), percentile) + quiet_section_noise = filtered_noise[np.abs(filtered_noise) < quiet_threshold] + # Use fallback noise power if the quiet section is sparse + noise_power = np.mean(quiet_section_noise ** 2) if len(quiet_section_noise) > 0 else signal_power * 0.1 + # 5. Compute SNR + snr = 10 * np.log10(signal_power / noise_power) + return round(snr, 6) + + def analyzeAudioData(chunks, lat, lon, week, sens, overlap,): global INTERPRETER @@ -327,6 +361,10 @@ def run_analysis(file): raw_detections = analyzeAudioData(audio_data, conf.getfloat('LATITUDE'), conf.getfloat('LONGITUDE'), file.week, conf.getfloat('SENSITIVITY'), conf.getfloat('OVERLAP')) confident_detections = [] + if audio_data: + global_snr = calculate_snr(np.concatenate(audio_data)) + else: + global_snr = 0 for time_slot, entries in raw_detections.items(): log.info('%s-%s', time_slot, entries[0]) for entry in entries: @@ -338,6 +376,6 @@ def run_analysis(file): elif entry[0] not in PREDICTED_SPECIES_LIST and len(PREDICTED_SPECIES_LIST) != 0: log.warning("Excluded as below Species Occurrence Frequency Threshold: %s", entry[0]) else: - d = Detection(time_slot.split(';')[0], time_slot.split(';')[1], entry[0], entry[1]) + d = Detection(time_slot.split(';')[0], time_slot.split(';')[1], entry[0], entry[1], global_snr) confident_detections.append(d) return confident_detections diff --git a/scripts/stats.php b/scripts/stats.php index 06fff9ddb..d05563631 100644 --- a/scripts/stats.php +++ b/scripts/stats.php @@ -22,6 +22,16 @@ $statement2 = $db->prepare('SELECT Date, Time, File_Name, Com_Name, COUNT(*), MAX(Confidence) FROM detections GROUP BY Com_Name ORDER BY COUNT(*) DESC'); ensure_db_ok($statement2); $result2 = $statement2->execute(); + +} else if(isset($_GET['sort']) && $_GET['sort'] == "confidence") { + $statement = $db->prepare('SELECT Date, Time, File_Name, Com_Name, COUNT(*), MAX(Confidence) FROM detections GROUP BY Com_Name ORDER BY MAX(Confidence) DESC'); + ensure_db_ok($statement); + $result = $statement->execute(); + + $statement2 = $db->prepare('SELECT Date, Time, File_Name, Com_Name, COUNT(*), MAX(Confidence) FROM detections GROUP BY Com_Name ORDER BY MAX(Confidence) DESC'); + ensure_db_ok($statement2); + $result2 = $statement2->execute(); + } else { $statement = $db->prepare('SELECT Date, Time, File_Name, Com_Name, COUNT(*), MAX(Confidence) FROM detections GROUP BY Com_Name ORDER BY Com_Name ASC'); @@ -33,8 +43,6 @@ $result2 = $statement2->execute(); } - - if(isset($_GET['species'])){ $selection = htmlspecialchars_decode($_GET['species'], ENT_QUOTES); $statement3 = $db->prepare("SELECT Com_Name, Sci_Name, COUNT(*), MAX(Confidence), File_Name, Date, Time from detections WHERE Com_Name = \"$selection\""); @@ -71,6 +79,9 @@ +
@@ -80,12 +91,17 @@
$name
- $sciname
- - -
".$name." +
+ ".$sciname."
+ + +
fetchArray(SQLITE3_ASSOC)) { $comname = preg_replace('/ /', '_', $results['Com_Name']); $comname = preg_replace('/\'/', '', $comname); $filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name']; $birds[] = $results['Com_Name']; + if ($_GET['sort'] == "confidence") { + $confidence[] = ' (' . round($results['MAX(Confidence)'] * 100) . '%)'; + } } if(count($birds) > 45) { @@ -93,6 +109,7 @@ } else { $num_cols = 1; } + $num_rows = ceil(count($birds) / $num_cols); for ($row = 0; $row < $num_rows; $row++) { @@ -104,7 +121,7 @@ if ($index < count($birds)) { ?> +

+ +

Choose a species to load images from Flickr.

diff --git a/scripts/update_birdnet_snippets.sh b/scripts/update_birdnet_snippets.sh index 72fbe2a2c..84a1ee045 100755 --- a/scripts/update_birdnet_snippets.sh +++ b/scripts/update_birdnet_snippets.sh @@ -98,6 +98,12 @@ if ! grep -E '^MAX_FILES_SPECIES=' /etc/birdnet/birdnet.conf &>/dev/null;then echo "MAX_FILES_SPECIES=\"0\"" >> /etc/birdnet/birdnet.conf fi +if ! grep -E '^CONFIRM_SPECIES=' /etc/birdnet/birdnet.conf &>/dev/null;then + echo "## CONFIRM_SPECIES adds an icon next to species in the Recordings tab to keep track which species are manually confirmed" >> /etc/birdnet/birdnet.conf + echo "## It generates a confirmed_species_list.txt file, and allows to better visualize species that could be false positives" >> /etc/birdnet/birdnet.conf + echo "CONFIRM_SPECIES=0" >> /etc/birdnet/birdnet.conf +fi + [ -d $RECS_DIR/StreamData ] || sudo_with_user mkdir -p $RECS_DIR/StreamData [ -L ${EXTRACTED}/spectrogram.png ] || sudo_with_user ln -sf ${RECS_DIR}/StreamData/spectrogram.png ${EXTRACTED}/spectrogram.png diff --git a/scripts/utils/helpers.py b/scripts/utils/helpers.py index cb75d584e..1a129c84d 100644 --- a/scripts/utils/helpers.py +++ b/scripts/utils/helpers.py @@ -42,7 +42,7 @@ def get_settings(settings_path='/etc/birdnet/birdnet.conf', force_reload=False): class Detection: - def __init__(self, start_time, stop_time, species, confidence): + def __init__(self, start_time, stop_time, species, confidence, snr): self.start = float(start_time) self.stop = float(stop_time) self.confidence = round(float(confidence), 4) @@ -52,6 +52,7 @@ def __init__(self, start_time, stop_time, species, confidence): self.common_name = species.split('_')[1] self.common_name_safe = self.common_name.replace("'", "").replace(" ", "_") self.file_name_extr = None + self.snr = float(snr) class ParseFileName: diff --git a/scripts/utils/reporting.py b/scripts/utils/reporting.py index 56bc23c85..342aa377c 100644 --- a/scripts/utils/reporting.py +++ b/scripts/utils/reporting.py @@ -107,7 +107,7 @@ def summary(file: ParseFileName, detection: Detection): s = (f'{file.date};{file.time};{detection.scientific_name};{detection.common_name};' f'{detection.confidence};' f'{conf["LATITUDE"]};{conf["LONGITUDE"]};{conf["CONFIDENCE"]};{file.week};{conf["SENSITIVITY"]};' - f'{conf["OVERLAP"]}') + f'{conf["OVERLAP"]};{detection.snr}') return s
- +