From 0161b845d5e8c9e4e0609a960a8c4dbd69b0dafc Mon Sep 17 00:00:00 2001 From: "stephane.chavin" <stephanechvn@gmail.com> Date: Wed, 17 Jul 2024 13:49:23 +0200 Subject: [PATCH] V2 --- README.md | 367 ++++++++----- get_spectrogram.py | 150 +++--- get_time_freq_detection.py | 97 ++-- get_train_annot.py | 260 +++++++++ get_train_val.py | 129 +++++ labelme2yolo.py | 80 +-- requirements.txt | 79 ++- utils.py | 508 ++++++++++++++++++ yolo2labelme.py | 35 ++ yolov5/benchmarks.py | 0 yolov5/data/Argoverse.yaml | 0 yolov5/data/GlobalWheat2020.yaml | 0 yolov5/data/ImageNet.yaml | 0 yolov5/data/Objects365.yaml | 0 yolov5/data/SKU-110K.yaml | 0 yolov5/data/VOC.yaml | 0 yolov5/data/VisDrone.yaml | 0 yolov5/data/coco.yaml | 0 yolov5/data/coco128-seg.yaml | 0 yolov5/data/coco128.yaml | 0 yolov5/data/hyps/hyp.Objects365.yaml | 0 yolov5/data/hyps/hyp.VOC.yaml | 0 yolov5/data/hyps/hyp.no-augmentation.yaml | 0 yolov5/data/hyps/hyp.scratch-high.yaml | 0 yolov5/data/hyps/hyp.scratch-low.yaml | 0 yolov5/data/hyps/hyp.scratch-med.yaml | 0 yolov5/data/images/bus.jpg | Bin yolov5/data/images/zidane.jpg | Bin yolov5/data/scripts/download_weights.sh | 0 yolov5/data/scripts/get_coco.sh | 0 yolov5/data/scripts/get_coco128.sh | 0 yolov5/data/scripts/get_imagenet.sh | 0 yolov5/data/xView.yaml | 0 yolov5/detect.py | 24 +- yolov5/export.py | 0 yolov5/hubconf.py | 0 yolov5/models/__init__.py | 0 yolov5/models/common.py | 0 yolov5/models/experimental.py | 0 yolov5/models/hub/anchors.yaml | 0 yolov5/models/hub/yolov3-spp.yaml | 0 yolov5/models/hub/yolov3-tiny.yaml | 0 yolov5/models/hub/yolov3.yaml | 0 yolov5/models/hub/yolov5-bifpn.yaml | 0 yolov5/models/hub/yolov5-fpn.yaml | 0 yolov5/models/hub/yolov5-p2.yaml | 0 yolov5/models/hub/yolov5-p34.yaml | 0 yolov5/models/hub/yolov5-p6.yaml | 0 yolov5/models/hub/yolov5-p7.yaml | 0 yolov5/models/hub/yolov5-panet.yaml | 0 yolov5/models/hub/yolov5l6.yaml | 0 yolov5/models/hub/yolov5m6.yaml | 0 yolov5/models/hub/yolov5n6.yaml | 0 yolov5/models/hub/yolov5s-LeakyReLU.yaml | 0 yolov5/models/hub/yolov5s-ghost.yaml | 0 yolov5/models/hub/yolov5s-transformer.yaml | 0 yolov5/models/hub/yolov5s6.yaml | 0 yolov5/models/hub/yolov5x6.yaml | 0 yolov5/models/segment/yolov5l-seg.yaml | 0 yolov5/models/segment/yolov5m-seg.yaml | 0 yolov5/models/segment/yolov5n-seg.yaml | 0 yolov5/models/segment/yolov5s-seg.yaml | 0 yolov5/models/segment/yolov5x-seg.yaml | 0 yolov5/models/tf.py | 0 yolov5/models/yolo.py | 0 yolov5/models/yolov5l.yaml | 0 yolov5/models/yolov5m.yaml | 0 yolov5/models/yolov5n.yaml | 0 yolov5/models/yolov5s.yaml | 0 yolov5/models/yolov5x.yaml | 0 yolov5/train.py | 11 +- yolov5/utils/__init__.py | 0 yolov5/utils/activations.py | 0 yolov5/utils/augmentations.py | 0 yolov5/utils/autoanchor.py | 0 yolov5/utils/autobatch.py | 0 yolov5/utils/aws/__init__.py | 0 yolov5/utils/aws/mime.sh | 0 yolov5/utils/aws/resume.py | 0 yolov5/utils/aws/userdata.sh | 0 yolov5/utils/callbacks.py | 0 yolov5/utils/dataloaders.py | 15 +- yolov5/utils/docker/Dockerfile | 0 yolov5/utils/docker/Dockerfile-arm64 | 0 yolov5/utils/docker/Dockerfile-cpu | 0 yolov5/utils/downloads.py | 0 yolov5/utils/flask_rest_api/README.md | 0 .../utils/flask_rest_api/example_request.py | 0 yolov5/utils/flask_rest_api/restapi.py | 0 yolov5/utils/general.py | 0 yolov5/utils/google_app_engine/Dockerfile | 0 .../additional_requirements.txt | 0 yolov5/utils/google_app_engine/app.yaml | 0 yolov5/utils/loggers/__init__.py | 0 yolov5/utils/loggers/clearml/.gitkeep | 0 yolov5/utils/loggers/clearml/README.md | 0 yolov5/utils/loggers/clearml/__init__.py | 0 yolov5/utils/loggers/clearml/clearml_utils.py | 0 yolov5/utils/loggers/clearml/hpo.py | 0 yolov5/utils/loggers/comet/.gitkeep | 0 yolov5/utils/loggers/comet/README.md | 0 yolov5/utils/loggers/comet/__init__.py | 0 yolov5/utils/loggers/comet/comet_utils.py | 0 yolov5/utils/loggers/comet/hpo.py | 0 .../utils/loggers/comet/optimizer_config.json | 0 yolov5/utils/loggers/wandb/__init__.py | 0 yolov5/utils/loggers/wandb/wandb_utils.py | 0 yolov5/utils/loss.py | 0 yolov5/utils/metrics.py | 0 yolov5/utils/plots.py | 0 yolov5/utils/segment/__init__.py | 0 yolov5/utils/segment/augmentations.py | 0 yolov5/utils/segment/dataloaders.py | 0 yolov5/utils/segment/general.py | 0 yolov5/utils/segment/loss.py | 0 yolov5/utils/segment/metrics.py | 0 yolov5/utils/segment/plots.py | 0 yolov5/utils/torch_utils.py | 0 yolov5/utils/triton.py | 0 yolov5/val.py | 0 120 files changed, 1390 insertions(+), 365 deletions(-) mode change 100644 => 100755 README.md mode change 100644 => 100755 get_spectrogram.py mode change 100644 => 100755 get_time_freq_detection.py create mode 100755 get_train_annot.py create mode 100755 get_train_val.py mode change 100644 => 100755 labelme2yolo.py create mode 100755 utils.py create mode 100755 yolo2labelme.py mode change 100644 => 100755 yolov5/benchmarks.py mode change 100644 => 100755 yolov5/data/Argoverse.yaml mode change 100644 => 100755 yolov5/data/GlobalWheat2020.yaml mode change 100644 => 100755 yolov5/data/ImageNet.yaml mode change 100644 => 100755 yolov5/data/Objects365.yaml mode change 100644 => 100755 yolov5/data/SKU-110K.yaml mode change 100644 => 100755 yolov5/data/VOC.yaml mode change 100644 => 100755 yolov5/data/VisDrone.yaml mode change 100644 => 100755 yolov5/data/coco.yaml mode change 100644 => 100755 yolov5/data/coco128-seg.yaml mode change 100644 => 100755 yolov5/data/coco128.yaml mode change 100644 => 100755 yolov5/data/hyps/hyp.Objects365.yaml mode change 100644 => 100755 yolov5/data/hyps/hyp.VOC.yaml mode change 100644 => 100755 yolov5/data/hyps/hyp.no-augmentation.yaml mode change 100644 => 100755 yolov5/data/hyps/hyp.scratch-high.yaml mode change 100644 => 100755 yolov5/data/hyps/hyp.scratch-low.yaml mode change 100644 => 100755 yolov5/data/hyps/hyp.scratch-med.yaml mode change 100644 => 100755 yolov5/data/images/bus.jpg mode change 100644 => 100755 yolov5/data/images/zidane.jpg mode change 100644 => 100755 yolov5/data/scripts/download_weights.sh mode change 100644 => 100755 yolov5/data/scripts/get_coco.sh mode change 100644 => 100755 yolov5/data/scripts/get_coco128.sh mode change 100644 => 100755 yolov5/data/scripts/get_imagenet.sh mode change 100644 => 100755 yolov5/data/xView.yaml mode change 100644 => 100755 yolov5/detect.py mode change 100644 => 100755 yolov5/export.py mode change 100644 => 100755 yolov5/hubconf.py mode change 100644 => 100755 yolov5/models/__init__.py mode change 100644 => 100755 yolov5/models/common.py mode change 100644 => 100755 yolov5/models/experimental.py mode change 100644 => 100755 yolov5/models/hub/anchors.yaml mode change 100644 => 100755 yolov5/models/hub/yolov3-spp.yaml mode change 100644 => 100755 yolov5/models/hub/yolov3-tiny.yaml mode change 100644 => 100755 yolov5/models/hub/yolov3.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-bifpn.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-fpn.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-p2.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-p34.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-p6.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-p7.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5-panet.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5l6.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5m6.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5n6.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5s-LeakyReLU.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5s-ghost.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5s-transformer.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5s6.yaml mode change 100644 => 100755 yolov5/models/hub/yolov5x6.yaml mode change 100644 => 100755 yolov5/models/segment/yolov5l-seg.yaml mode change 100644 => 100755 yolov5/models/segment/yolov5m-seg.yaml mode change 100644 => 100755 yolov5/models/segment/yolov5n-seg.yaml mode change 100644 => 100755 yolov5/models/segment/yolov5s-seg.yaml mode change 100644 => 100755 yolov5/models/segment/yolov5x-seg.yaml mode change 100644 => 100755 yolov5/models/tf.py mode change 100644 => 100755 yolov5/models/yolo.py mode change 100644 => 100755 yolov5/models/yolov5l.yaml mode change 100644 => 100755 yolov5/models/yolov5m.yaml mode change 100644 => 100755 yolov5/models/yolov5n.yaml mode change 100644 => 100755 yolov5/models/yolov5s.yaml mode change 100644 => 100755 yolov5/models/yolov5x.yaml mode change 100644 => 100755 yolov5/train.py mode change 100644 => 100755 yolov5/utils/__init__.py mode change 100644 => 100755 yolov5/utils/activations.py mode change 100644 => 100755 yolov5/utils/augmentations.py mode change 100644 => 100755 yolov5/utils/autoanchor.py mode change 100644 => 100755 yolov5/utils/autobatch.py mode change 100644 => 100755 yolov5/utils/aws/__init__.py mode change 100644 => 100755 yolov5/utils/aws/mime.sh mode change 100644 => 100755 yolov5/utils/aws/resume.py mode change 100644 => 100755 yolov5/utils/aws/userdata.sh mode change 100644 => 100755 yolov5/utils/callbacks.py mode change 100644 => 100755 yolov5/utils/dataloaders.py mode change 100644 => 100755 yolov5/utils/docker/Dockerfile mode change 100644 => 100755 yolov5/utils/docker/Dockerfile-arm64 mode change 100644 => 100755 yolov5/utils/docker/Dockerfile-cpu mode change 100644 => 100755 yolov5/utils/downloads.py mode change 100644 => 100755 yolov5/utils/flask_rest_api/README.md mode change 100644 => 100755 yolov5/utils/flask_rest_api/example_request.py mode change 100644 => 100755 yolov5/utils/flask_rest_api/restapi.py mode change 100644 => 100755 yolov5/utils/general.py mode change 100644 => 100755 yolov5/utils/google_app_engine/Dockerfile mode change 100644 => 100755 yolov5/utils/google_app_engine/additional_requirements.txt mode change 100644 => 100755 yolov5/utils/google_app_engine/app.yaml mode change 100644 => 100755 yolov5/utils/loggers/__init__.py mode change 100644 => 100755 yolov5/utils/loggers/clearml/.gitkeep mode change 100644 => 100755 yolov5/utils/loggers/clearml/README.md mode change 100644 => 100755 yolov5/utils/loggers/clearml/__init__.py mode change 100644 => 100755 yolov5/utils/loggers/clearml/clearml_utils.py mode change 100644 => 100755 yolov5/utils/loggers/clearml/hpo.py mode change 100644 => 100755 yolov5/utils/loggers/comet/.gitkeep mode change 100644 => 100755 yolov5/utils/loggers/comet/README.md mode change 100644 => 100755 yolov5/utils/loggers/comet/__init__.py mode change 100644 => 100755 yolov5/utils/loggers/comet/comet_utils.py mode change 100644 => 100755 yolov5/utils/loggers/comet/hpo.py mode change 100644 => 100755 yolov5/utils/loggers/comet/optimizer_config.json mode change 100644 => 100755 yolov5/utils/loggers/wandb/__init__.py mode change 100644 => 100755 yolov5/utils/loggers/wandb/wandb_utils.py mode change 100644 => 100755 yolov5/utils/loss.py mode change 100644 => 100755 yolov5/utils/metrics.py mode change 100644 => 100755 yolov5/utils/plots.py mode change 100644 => 100755 yolov5/utils/segment/__init__.py mode change 100644 => 100755 yolov5/utils/segment/augmentations.py mode change 100644 => 100755 yolov5/utils/segment/dataloaders.py mode change 100644 => 100755 yolov5/utils/segment/general.py mode change 100644 => 100755 yolov5/utils/segment/loss.py mode change 100644 => 100755 yolov5/utils/segment/metrics.py mode change 100644 => 100755 yolov5/utils/segment/plots.py mode change 100644 => 100755 yolov5/utils/torch_utils.py mode change 100644 => 100755 yolov5/utils/triton.py mode change 100644 => 100755 yolov5/val.py diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 93a68b9..d2f736f --- a/README.md +++ b/README.md @@ -1,247 +1,320 @@ -# YOLO-DYNI +<h1 align="center"><strong>Raven2YOLO</strong></h1> -Ce git a été créé avec comme objectif une prise en main de YOLOV5 plus facile. -Il contient notamment un script permettant d'extraire les spectrogrammes de plusieurs enregistrements ([get_spectrogram.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_spectrogram.py)), un script nécessaire à la conversion des annotations LabelMe vers YOLO ([labelme2yolo.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/labelme2yolo.py)), un script pour convertir des annotations d'un dataframe vers YOLO ([get_train_annot_YOLO.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_train_annot_YOLO.py/)), un script permettant de séparer le train et la validation de manière équilibré ([get_train_val_YOLO.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_train_val_YOLO.py)) et un script qui permet de compiler les détections, d'un modèle entrainé, dans un dataframe ([get_yolo_detection.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_yolo_detection.py)). +This GitHub repository was created to simplify learning YOLOv5 and to adapt it for bioacoustics. It supports a paper analyzing Humpback Whale (<i>Megaptera novaeangliae</i>) vocalizations through the automatic detection and classification of 28 units. -## Install +See : [Publication]() -```bash -git clone https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni.git +--- -pip install -r requirements.txt +This repository includes essential scripts for adapting YOLOv5 to bioacoustics research. Key scripts provided are: -``` +* [get_spectrogram.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_spectrogram.py) : Extracts spectrograms from multiple recordings. +* [labelme2yolo.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/labelme2yolo.py) : Converts LabelMe annotations to YOLO format. +* [yolo2labelme.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/yolo2labelme.py) : Converts YOLO detections to LabelMe format. +* [get_train_annot.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_train_annot.py) : Converts Raven format dataframe annotations to YOLO format. +* [get_train_val.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_train_val.py) : Separates training and validation datasets in a balanced manner. +* [get_time_freq_detection.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_time_freq_detection.py) : Compiles detections from a trained model into a dataframe and/or into Raven annotation format (.txt). -## Annoter des images : -<details> -<summary>Extraction des spectrogrammes</summary> -<p> -</p> +<br> -```bash -python3 get_spectrogram.py -f NAME_OF_YOUR_FILE.csv -p PATH_TO_THE_DATA -d DIRECTION_OF_SAVED_DATA -m {unique or multiple} -n NAME_OF_THE_COLUMNS_THAT_CONTAIN_PATH -i {file or folder} -``` -* Le mode unique est utilisé lorsque les enregistrements font quelques secondes et que l'on veut qu'un seul spectrogramme par enregistrement *(mode __unique__, DURATION = durée de l'enregistrement et NB_IMG_PER_REC = 1)* -* Le mode multiple permet de découper l'enregistrements en plusieurs spectrogrammes de quelques secondes -* -i file est à utiliser lorsque les noms de fichiers à traiter sont dans un .csv, tandis que -i folder est à utiliser pour traiter tous les .wav d'un dossier +--- -**WARNING** : Il est important de modifier la valeur de DURATION, OFFSET et NB_IMG_PER_REC dans le code (ligne 35, 36 et 39), la fréquence d'échantillonage des spectrogrammes est celle du fichier .wav +<br> -```python -folder = 'Spectrogram/' -DURATION = 5 -OFFSET = 2 +To use the scripts with Raven annotation software ([Raven Lite](https://www.ravensoundsoftware.com/software/raven-lite/)), export annotations in the recommended format and follow these steps: -if args.mode == 'multiple': - NB_IMG_PER_REC = 10 -``` - -</details> -<details> -<summary>LabelMe</summary> -<p> -</p> +* go into the folder that contains the scripts +* Run get_train_annot.py +* Launch training -* Installation de LabelMe +<br> -```bash -pip install labelme -``` +--- -* Conversion des annotations LabelMe vers YOLO +<br> +To use the scripts without Raven annotation software, you can follow these steps: -```bash -python3 labelme2yolo.py -p PATH_TO_THE_DATA -d DIRECTION_OF_THE_CONVERTED_FILES/FOLDER -``` +* Run get_spectrogram.py +* Install Labelme (pip install labelme) and annotate the spectrograms +* Run labelme2yolo.py +* Run get_train_val.py +* Launch training -**WARNING** : Les annotations *.json* et les images *.jpg* doivent être dans le même dossier (**PATH_TO_THE_DATA/**) +<br> -* Préparation à la séparation train/val +--- -Il est important de déplacer les images .jpg dans un dossier **images/all/** et que les labels .txt soient dans le dossier **labels/** +<br> +The [get_time_freq_detection.py](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/get_time_freq_detection.py) script compiles detections into a NetCDF (.nc) file, detailing minimum, mid-range, and maximum frequency, duration, detection position, and model confidence. -```bash -##go to the .jpg direction## +Additional scripts may be added over time to automate other processes. -mkdir images #create a new folder -mkdir images/all/ #create the folder all in the new folder +<br /> -mv *.jpg images/all/. #move all the images in the all folder - -mv images ../. #place the folder images in the main direction -``` +[<a href="Open in Colab"><img src="https://colab.research.google.com/assets/colab-badge.svg" height="auto" width="120" ></a>](https://colab.research.google.com/drive/1NSEaJyhnAV8BUsSxbWr54EdjhUUODJpt?usp=sharing) -On se retrouve donc avec deux dossiers, un portant le nom **images**, contenant les .jpg, et un second portant le nom **labels/** contenant les .txt -<p> -</p> +<br /> - +--- +<br /> -</details> +* For proper citation when using this methodology, please refer to the provided [CITATION.cff](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/CITATION.cff) file. +<br /> +--- +<br /> -## Créer un dataset à partir d'annotations sur un dataframe : +</p> -* Exemple de dataframe : +# <strong>Install</strong> -| |Path | label | annotation_initial_time |annotation_final_time |duree |min_frequency |max_frequency |avg_frequency | -| :-----: |:---: | :---: | :---: | :---: | :---:|:---: |:---: |:---: | -| 0 | /nfs/NAS7/QUEBEC2/Montmagny_A/142_111_F01/2017/WAV... | btbw | 0.903 | 2.871 | 1.967 | 3263 |6570 |4917 | -| 1 | /nfs/NAS7/QUEBEC2/La_Vallee_de_la_Gatineau_F/124_86_H02/2018/WAV... | coye | 3.678 | 5.734 | 2.056 | 1852 |6262 |4057 | -| 2 | /nfs/NAS7/QUEBEC2/La_Vallee_de_la_Gatineau_F/124_86_H01/2018/WAV... | alfl | 0.238 | 0.998 | 0.76 | 1631 |3836 |2734 | -| ... | ... | ... | ... | ... | ... | ... |... |... | +```bash +git clone https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni.git -**see :** [example.csv](https://gitlab.lis-lab.fr/stephane.chavin/yolo-dyni/-/blob/main/example.csv) +pip install -r requirements.txt +``` +<br /> <details> -<summary>Conversion des annotations</summary> -<p> -</p> +<summary><strong>Spectrogram Extraction Script</strong></summary> +<br /> -* Conversion des annotations (**temps fréquence**) au format YOLO (**label x y w h**) +## Description -**WARNING** : Ne pas oublier de modifier la valeur de **DURATION** et **NB_CLASS** (ligne 28 et 29) +This script extracts spectrograms from `.wav` files. It allows you to specify various parameters such as duration, window size, hop ratio, high and low pass filters, overlap, resampling frequency, and CPU usage to optimize the process. +<br /> +# Usage -```bash -python3 get_train_annot_YOLO.py -f PATH_TO_THE_FILE.csv -p PATH_TO_DATA -d DIRECTION_OF_THE_TXT_AND_IMG -m {uniform or personalized} -u {unique or multiple} -c NAME_OF_THE_COLUMNS_CONTAINS_PATH -``` - -* Vérifier le bon placement des bounding box : +To run the script, use the following command: ```bash -python3 get_train_annot_YOLO.py -f PATH_TO_THE_FILE.csv -p PATH_TO_DATA -d DIRECTION_OF_THE_TXT_AND_IMG -m {uniform or personalized} -u {unique or multiple} -c NAME_OF_THE_COLUMNS_CONTAINS_PATH --export +python get_spectrogram.py <path> <directory> [options] ``` +# Arguments +## Positional Arguments +- path: Path to the folder or file that contains the recordings. +- directory: Directory where the extracted spectrograms will be stored. -*Ajouter **--export** permet d'exporter les spectrogrammes avec les bounding box placées par dessus* -**WARNING** : Le mode uniforme permet d'extraire des bounding box avec y = 0.5 et h = 0.8; En utilisant -u unique seul le début des enregistrements est traité tandis que multiple permet de traiter plusieurs spectrogrammes; la fonction -c correspond au nom de la colonne qui contient le path des fichiers +## Optional Arguments +``` +--duration (int): Duration for each spectrogram. Default is 8. +--window (int): Window size for the Fourier Transform. Default is 1024. +--hop (float): Ratio of hop in window. 50%% corresponds to 0.5. Default is 0.5. +--high (int): High Pass Filter value in Hz. Default is 10. +--low (int): Low Pass Filter value in Hz. Default is None. +--overlap (int): Overlap in seconds between two spectrograms. Default is 0. +--rf (int): Resampling Frequency of the signal. If not provided, the original frequency sampling of the recording will be used. Default is None. +--cpu (int): Number of CPUs to use for processing to speed up the process. Provide 2 or more. Default is 1. +--test (flag): If provided, sets the test flag to 1, otherwise it is None. +``` </details> +<br /> <details> -<summary>Séparation train/val</summary> -<p> -</p> +<summary><strong>LabelMe to YOLO Annotation Converter</strong></summary> +<br /> -```bash -python3 get_train_val_YOLO.py -r RATIO -p PATH_TO_DATA -d DIRECTION_OF_THE_RESULT -``` +## Description + +This script converts annotations from the LabelMe format to a YOLO compatible format. It allows you to specify the path to the LabelMe annotations and the directory where the converted YOLO annotations will be stored. -**WARNING** : Le script get_train_annot_YOLO.py exporte les .txt et .jpg dans deux dossiers nommés **labels_X_Y/** et **images_X_Y/** (X : Jour et Y : Mois). Pour faire tourner le code get_train_val_YOLO.py, il faut d'abord supprimer les 4 derniers caractères du nom de ces dossiers pour obtenir uniquement **images/** et **labels/** +# Usage -```python -##CHANGE TO MAKE## +To run the script, use the following command: -path = str(args.path_to_data) -direction = str(args.direction) -NB_CLASS = 50 #CHOOSE THE X MOST REPRESENTED CLASSES +```bash +python labelme2yolo.py <path_to_data> <directory> ``` +# Arguments +## Positional Arguments +- path_to_data: Path to the folder that contains the LabelMe annotations. +- directory: Directory where the YOLO annotations will be stored. </details> +<br /> + +<details> +<summary><strong>YOLO to JSON/LabelMe Annotation Converter</strong></summary> +<br /> + +## Description -## Entrainement et Détection YOLO +This script converts annotations from the YOLO format (stored in `.txt` files) to JSON files. It allows you to specify the path to the folder containing the `.txt` files, the path to the folder containing the images, and optionally the directory where the modified JSON files will be stored. -* Si besoin : YOLOV5 (une version de YOLOv5 est déjà présente dans yolo-dyni/yolov5) +# Usage +To run the script, use the following command: ```bash -cd yolo-dyni -git clone https://github.com/lamipaul/yolov5 +python yolo2labelme.py <path_to_txt> <path_to_img> [options] ``` +# Arguments +## Positional Arguments + +- path_to_txt: Path to the folder containing the .txt files. +- path_to_img: Path to the folder containing the .jpg images. + +## Optional Arguments + +-d, --directory (str): Directory where the modified JSON files will be stored. If not provided, the directory will be the same as path_to_txt. +</details> +<br /> + <details> -<summary>Entrainement</summary> -<p> -</p> +<summary><strong>Data Splitting and Storage Script</strong></summary> +<br /> + +## Description + +This script splits data into training, validation, and optionally test sets based on a specified ratio. It allows you to specify the path to the folder containing the `.txt` files, the directory where the spectrogram and `.txt` files will be stored, and an optional test flag to include a test split. + +# Usage + +To run the script, use the following command: ```bash -python3 train.py --img IMG_SIZE --batch BATCH_SIZE --epochs NB_EPOCHS --data DIRECTION_OF_THE_RESULT/custom_data.yaml --weights yolov5/weights/yolov5s.pt --cache +python get_train_val.py.py <path_to_data> <directory> [options] ``` +# Arguments +## Positional Arguments +- path_to_data: Path to the folder that contains the .txt files (should end with labels/). +- directory: Directory where the spectrogram and .txt files will be stored (should be different from <path_to_data>). + </details> +<br /> <details> -<summary>Détection</summary> -<p> -</p> +<summary><strong>CSV to Spectrogram and Annotation Converter</strong></summary> +<br /> -* Lancer un entrainement sur de l'audio +## Description -```bash -python3 detect.py --weights yolov5/runs/train/EXP_NB/weights/best.pt --img IMG_SIZE --conf 0.X --source PATH_TO_FOLDER_THAT_CONTAIN_WAV --save-txt --sound --sr X --sampleDur Y -``` +This script creates `.txt` and `.jpg` files for each annotation from a CSV file. It takes in the path to the CSV file containing annotations, the path to the folder containing the recordings, and the directory where the spectrograms and `.txt` files will be stored. The script includes options for setting the duration and overlap of the spectrograms, frequency resampling, window size, hop ratio, CPU usage, and an optional test flag to include a test split. + +# Usage + +To run the script, use the following command: -* Sauvegarde les annotations en .txt ainsi que les images avec les bounding box dessus ```bash -python3 detect.py --weights yolov5/runs/train/EXP_NB/weights/best.pt --img IMG_SIZE --conf 0.X --source PATH_TO_SPECTROGRAM_TO_DETECT --save-txt +python get_train_annot.py <filename_path> <path_to_data> <directory> [options] ``` -**WARNING** : Il faut adapter EXP_NB, qui correspond au numéro de l'entrainement *(exp1 pour le premier entrainement)* ---conf correspond à la confiance toléré par YOLO, c'est-à-dire à partir de quelle confiance d'une détection cette dernière est conservée, il faut donc modifier la valeur de X pour faire varier cette tolérence *(minimum : 0.0, maximum : 1)* - -* Sauvegarde les annotations en .txt seulement avec la confiance de chaque détections +# Arguments +## Positional Arguments +- filename_path: Path/name of the folder/file containing the annotations. If a file, use Raven format and add a Path column with the path to the .wav files. +- path_to_data: Path of the folder that contains the recordings. +- directory: Directory where the spectrograms and .txt files will be stored. -```bash -python3 detect.py --weights yolov5/runs/train/EXP_NB/weights/best.pt --img IMG_SIZE --conf 0.X --source PATH_TO_SPECTROGRAM_TO_DETECT --save-txt --nosave --save-conf -``` +## Optional Arguments +``` +--duration (int): Duration for each spectrogram. Default is 8. +--overlap (int): Overlap in seconds between two spectrograms. Default is 2. +--rf (int): Frequency resampling. Default is None. +--window (int): Window size for the Fourier Transform. Default is 1024. +--hop (float): Ratio of hop in window (e.g., 50% = 0.5). Default is 0.5. +--cpu (int): Number of CPUs to speed up the process. Default is 1. +--test (flag): If provided, splits the data into train/test/validation sets with the ratio 1 - Ratio / 2 for test and the same for validation. If not provided, only train and validation splits are created. +``` </details> +<br /> + <details> -<summary>Compilation des détections</summary> -<p> -</p> +<summary><strong>Detection Collector and DataFrame Generator</strong></summary> +<br /> + +## Description + +This script collects detections from `.txt` files and returns a complete dataframe. It takes in the path to the folder containing the `.txt` files, the directory where the dataframe will be stored, and the path to the `YOLOv5` `custom_data.yaml` file. Additionally, it allows specifying the sampling rate and the duration of the spectrogram. + +# Usage + +To run the script, use the following command: ```bash -cd ../ -python3 get_yolo_detection.py -p PATH_TO_THE_TXT -d DIRECTION_OF_THE_RESULT #PATH_TO_THE_TXT ~ yolov5/runs/detect/expX/labels +python get_time_freq_detection.py <path_to_data> <directory> <names> [options] ``` -**WARNING** : Il est important d'ajouter sa propre liste de classes (ligne 39) +# Arguments +## Positional Arguments +- path_to_data: Path to the folder that contains the .txt files. +- directory: Directory where the dataframe will be stored. +- names: Path to the YOLOv5 custom_data.yaml file. -```python -#put the classes here -names = [] +## Optional Arguments +``` +-s, --sr (int): Sampling rate of the spectrogram. This argument is required. +--duration (int): Duration of the spectrogram. Default is 8. ``` +</details> +<br /> + +# Training a YOLOv5 model + +<br /> +For this project, we adapt the YOLOv5 DataLoader to compute detection on a folder that contains .WAV files. +If you need more informations about YOLOv5, see: +<a href="https://github.com/ultralytics/yolov5" target="_blank">https://github.com/ultralytics/yolov5</a> + +<br /> -* Calcul des détections en secondes +* Jocher Glenn (2020), "YOLOv5 by Ultralytics", doi: 10.5281/zenodo.3908559, license: AGPL-3.0 + +<br /> ```bash -cd ../ -python3 get_time_freq_detection.py -p PATH_TO_THE_TXT -s SR -t DURATION -d DIRECTION_OF_THE_RESULT #PATH_TO_THE_TXT ~ yolov5/runs/detect/expX/labels +python yolov5/train.py --imgsz <IMG_SIZE> --batch <BATCH_SIZE> --epochs <NB_EPOCHS> --data <custom_data.yaml> --weights yolov5/weights/yolov5l.pt --hyp <custom_hyp.yaml> --cache ``` -**WARNING** : Il est important d'ajouter sa propre liste de classes (ligne 39) +<br /> + +# Detection +<br /> -```python -DUREE_SPECTRO = args.duration -SR = args.SR +- Detect on audio files -#put the classes here -names = [] +```bash +python detect.py --weights yolov5/runs/train/<EXP_NB>/weights/best.pt --imgsz <imgsz> --conf <conf> --source <PATH_TO_FOLDER_THAT_CONTAIN_WAV> --save-txt --sound --sr <SR> --sampleDur <SampleDur> --window <window> --hop <hop> --save-conf ``` -</details> +<br /> -<details> -<summary>Conversion des détection YOLO vers LabelMe</summary> -<p> -</p> +- Detect on audio files without saving the detection images ```bash -pip install globox - -globox convert yolov5/runs/detect/exp/labels/ DIRECTORY_TO_WICH_DATA_WILL_BE_STORED --format yolo --save_fmt labelme --img_folder PATH_TO_IMAGES +python detect.py --weights yolov5/runs/train/<EXP_NB>/weights/best.pt --imgsz <imgsz> --conf <conf> --source <PATH_TO_FOLDER_THAT_CONTAIN_WAV> --save-txt --sound --sr <SR> --sampleDur <SampleDur> --window <window> --hop <hop> --save-conf --nosave ``` -* Ce script permet de convertir les *.txt* en *.json* pour les ouvrir dans LabelMe mais il ne prend pas en compte le paramètre 'imageData'. Le script **get_json_file_YOLO.py** permet donc de récupérer ces données et les ajouter aux fichiers *.json*. +<br /> -```bash -python3 get_json_file_YOLO.py -p PATH_TO_STORED_DATA -i PATH_TO_IMAGES -d DIRECTORY_TO_WICH_DATA_WILL_BE_STORED +## Arguments : + +``` +--imgsz: Inference size height and width. Default is 640. +--sampleDur: Duration for each spectrogram for detection. Default is 8. +--sr: Samplerate for each spectrogram for detection. Default is 22050. +--window: Window size for each spectrogram for detection. Default is 1024. +--hop: Hop length for each spectrogram for detection. Default is 50% of window = 512. +--sound: Enable sound. Default is False. Action 'store_true'. ``` +<br /> -</details> +# Contact + +If you have any questions, please contact me at the following e-mail address : stephane.chavin@univ-tln.fr + +<br /> + +# Contributors + +<br /> -## Contact +[<a href="@stephane.chavin"><img src="https://gitlab.lis-lab.fr/uploads/-/system/user/avatar/700/avatar.png" height="auto" width="50" style="border-radius:50%"></a>](https://gitlab.lis-lab.fr/stephane.chavin) [<a href="@paul.best"><img src="https://secure.gravatar.com/avatar/9476891fe83ae7102b73819cc74a89a76fb1f59e9fc7f96bcf4afbc60d734934?s=80&d=identicon" height="auto" width="50" style="border-radius:50%"></a>](https://gitlab.lis-lab.fr/paul.best) [<a href="@lisa.ferre"><img src="https://secure.gravatar.com/avatar/714493b1b540404b05e08795c1c17abdfc9db8dd1cafec2876051e00117bdc21?s=80&d=identicon" height="auto" width="50" style="border-radius:50%"></a>](https://gitlab.lis-lab.fr/lisa.ferre) [<a href="@nicolas.deloustal"><img src="https://secure.gravatar.com/avatar/32367421a8619488135bddb50ea0cfa9b89c9b9e915cd83501345867d79e3666?s=80&d=identicon&width=96" height="auto" width="50" style="border-radius:50%"></a>](https://gitlab.lis-lab.fr/nicolas.deloustal) -Pour toute question, veuillez me contacter à l'adresse mail suivante : stephanechvn@gmail.com +<br /> +--- \ No newline at end of file diff --git a/get_spectrogram.py b/get_spectrogram.py old mode 100644 new mode 100755 index be398af..8067074 --- a/get_spectrogram.py +++ b/get_spectrogram.py @@ -1,80 +1,104 @@ +"""Extracts spectrograms from multiple recordings""" + import os -import librosa -import glob import argparse import numpy as np -import pandas as pd -import matplotlib.pyplot as plt +import soundfile from p_tqdm import p_map +from tqdm import tqdm +import utils +import pandas as pd -import warnings -warnings.filterwarnings('ignore') - -def arg_directory(path): - if os.path.isdir(path): - return path - else: - raise argparse.ArgumentTypeError(f'`{path}` is not a valid path') - -def create_spectrogram(y, directory, filename, offset, duration): - window_size = 1024 - window = np.hanning(window_size) - stft = librosa.core.spectrum.stft(y, n_fft=window_size, hop_length=512, window=window) - plt.close() - plt.figure() +def main(data, arguments): + """ + Load the data and compute n spectrograms with a specific duration and save it into a folder. + :param data (DataFrame): DataFrame containing the path of each file to process. + :param arguments (args): Arguments for signal processing and directory. + """ + _, (i) = data + filename = str(i[0]) # Store the filename of the recording + try: + info = soundfile.info(filename) # Collection recording information + file_duration, fs = info.duration, info.samplerate + except Exception as error: + print(f'`{filename}` cannot be open : : {error}') + return - log_stft = np.log10(np.abs(stft)) - vmin, vmax = log_stft.min(), log_stft.max() + # Create the list of all possible offset to compute spectrogram + offset_list = np.arange(0, file_duration, arguments.duration - arguments.overlap) - plt.imshow(log_stft[::-1], aspect="auto", interpolation=None, cmap='jet', vmin=vmin, vmax=vmax) - plt.subplots_adjust(top=1, bottom=0, left=0, right=1) + for offset in offset_list: + file = filename.replace('/', '_').split('.', maxsplit=1)[0] + try: + sig, fs = soundfile.read(filename, start=int( + offset*fs), stop=int((offset+arguments.duration)*fs), + always_2d=True) # Load the signal + sig = sig[:, 0] # Only take channel 0 + # Apply resample and low/high pass filter + sig = utils.signal_processing( + sig, rf=arguments.rf, fs=fs, high=arguments.high, low=arguments.low) - name = os.path.join(directory, 'Spectrogram', f"{filename.replace('/', '_').split('.')[0]}_{offset}") - - try: - plt.savefig(name + '.jpg') - except FileNotFoundError: - os.makedirs(os.path.join(directory, 'Spectrogram'), exist_ok=True) - plt.savefig(name + '.jpg') + # Check if empty .txt annotation file is needed + if arguments.background: + folder = 'background' + name = os.path.join(arguments.directory, folder, f'{file}_{offset}') + empty_dataframe = pd.DataFrame(columns=['specie', 'x', 'y', 'w', 'h']) + empty_dataframe.to_csv(str(name+'.txt'), sep=' ', index=False, header=False) + else: + folder = 'spectrograms' + name = os.path.join(arguments.directory, folder, f'{file}_{offset}') -def process_recordings(args): - _, (i) = args - duration = 8 - overlap = 2 + utils.create_spectrogram( + sig, arguments.directory, name, window_size=arguments.window, + overlap=arguments.hop) + return folder + except Exception as error: + print(f'`{filename}` cannot be open : {error}') - for count in range(args.img_per_rec): - offset = count * (duration - overlap) - filename = str(i[0]) - - try: - y, _ = librosa.load(filename, offset=offset, duration=duration, sr=None) - create_spectrogram(y, args.directory, filename, offset, duration) - except Exception: - print(filename) if __name__ == "__main__": - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='TODO') - parser.add_argument('-f', '--file', type=str, help='Name of the file that contains the recording to print') - parser.add_argument('-p', '--path_to_data', type=arg_directory, help='Path of the folder that contains the recordings', required=True) - parser.add_argument('-d', '--directory', type=arg_directory, help='Directory to which spectrograms will be stored', required=True) - parser.add_argument('-m', '--mode', type=str, choices=['unique', 'multiple'], help='Direction of the saved spectrogram') - parser.add_argument('-n', '--columns_name', type=str, help='Name of the columns that contain the path of the .wav') - parser.add_argument('-i', '--input', type=str, choices=['file', 'folder'], help='Choose "file" if you have a .csv file or "folder" to export spectrogram from all the .wav of a folder') + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Extract spectrogram for each .wav file') + parser.add_argument('path', type=utils.arg_directory, + help='Path of the folder/file that contains the recordings') + parser.add_argument('directory', type=utils.arg_directory, + help='Directory to which spectrograms will be stored') + parser.add_argument('--duration', type=int, + help='Duration for each spectrogram', default=8) + parser.add_argument( + '--window', type=int, help='Window size for the Fourier Transform', default=1024) + parser.add_argument('--hop', type=float, + help='Ratio of hop in window : 50%% = 0.5', default=0.5) + parser.add_argument('--high', type=int, + help='High Pass Filter value in Hz', default=10) + parser.add_argument('--low', type=int, + help='Low Pass Filter value in Hz', default=None) + parser.add_argument('--overlap', type=int, + help='Overlap in secondes between 2 spectrograms', default=0) + parser.add_argument('--rf', type=int, help='Resampling Frequency of the signal. If no argument,' + ' will be original frequency sampling of the recording', default=None) + parser.add_argument( + '--cpu', type=int, help='To speed up the process, write 2 or more', default=1) + parser.add_argument( + '--background', action='store_const', help='If in arguments, will save an empty .txt file', + const=1, default=None) args = parser.parse_args() - if args.mode == 'multiple': - img_per_rec = 30 - elif args.mode == 'unique': - img_per_rec = 1 - - path_to_data = args.path_to_data + # Load the data and put it into a DataFrame + df = utils.open_file(args.path) - if args.input == 'file': - df = pd.read_csv(args.file, low_memory=False) - df['Path'] = df[args.columns_name] - elif args.input == 'folder': - df = pd.DataFrame(glob.glob(os.path.join(path_to_data, '*'), recursive=True), columns=['Path']) + if args.cpu == 1: # If no multiprocessing, then loop processing + for num, row in tqdm(df.iterrows(), total=len(df), + desc="Processing", ascii='░▒▓█'): + dest = main([num, [row.Path]], args) + directory = args.directory + else: + args = [args]*len(df.groupby('Path')) + dest = p_map(main, enumerate(df.groupby('Path')), args, + num_cpus=args[0].cpu, total=len(df.groupby('Path'))) + directory = args[0].directory - p_map(process_recordings, enumerate(df.groupby('Path'), img_per_rec=img_per_rec), num_cpus=1, total=len(df.groupby('Path'))) - print(f'Saved to {args.directory}/Spectrogram') + final_dest = os.path.join(directory, dest[0] if len(dest) is list else dest) + print(f'Saved to {final_dest}') diff --git a/get_time_freq_detection.py b/get_time_freq_detection.py old mode 100644 new mode 100755 index ea709d0..29655be --- a/get_time_freq_detection.py +++ b/get_time_freq_detection.py @@ -1,53 +1,64 @@ -import pandas as pd -import os -import argparse -from datetime import date -from tqdm import tqdm - -def arg_directory(path): - if os.path.isdir(path): - return path - else: - raise argparse.ArgumentTypeError(f'`{path}` is not a valid path') - -def process_annotations(annotations_folder, duration, sr): - today = date.today() - out_file = f'YOLO_detection_{today.day}_{today.month}' +"""Compiles detections into a dataframe and/or into Raven annotation format (.txt)""" - df_list = [] - names = [] # Add your class names here - - for file_name in tqdm(os.listdir(annotations_folder)): - if file_name.endswith('.txt'): - file_path = os.path.join(annotations_folder, file_name) - annotation_df = pd.read_csv(file_path, sep=' ', names=['espece', 'x', 'y', 'w', 'h']) +import argparse +import os +import yaml +import utils +import xarray as xr +import pandas as pd - annotation_df['file'] = file_name - annotation_df['idx'] = annotation_df['file'].str.split('_').str[-1].str.split('.').str[0] - annotation_df['file'] = annotation_df['file'].str.rsplit('.', 1).str[0] + '.wav' - annotation_df['annot'] = annotation_df['espece'].apply(lambda x: names[x]) +def main(arguments): + """ + Load all the informations to concatenate detection and get time/frequency informations + and save them into a full file and multiple raven annotation file if --raven + :param arguments (args): All the arguments of the script + """ + with open(arguments.names, 'r', encoding='utf-8') as file: + data = yaml.safe_load(file) + names = data['names'] + df, dir_path = utils.detection2time_freq(annotations_folder=arguments.path_to_data, + duration=arguments.duration, + outdir=arguments.directory, + sr=arguments.sr, + names=names, + wav=args.path_to_wav, + raven=args.raven) + # Convert DataFrame to xarray Dataset + ds = xr.Dataset.from_dataframe(df) - annotation_df['midl'] = (annotation_df['x'] * duration) + annotation_df['idx'].astype(int) - annotation_df['freq_center'] = (1 - annotation_df['y']) * (sr / 2) - annotation_df['freq_min'] = annotation_df['freq_center'] - (annotation_df['h'] * (sr / 2)) / 2 - annotation_df['freq_max'] = annotation_df['freq_center'] + (annotation_df['h'] * (sr / 2)) / 2 - annotation_df['start'] = annotation_df['midl'] - (annotation_df['w'] * duration) / 2 - annotation_df['stop'] = annotation_df['midl'] + (annotation_df['w'] * duration) / 2 - annotation_df['duration'] = annotation_df['stop'] - annotation_df['start'] + # Add metadata attributes + ds.attrs['title'] = 'YOLO detection final table' + ds.attrs['summary'] = 'This dataset contains the YOLO detection with parameters.' + ds.attrs['description'] = str('The data includes positions : "pos", frequencies :' + ' "low freq (hz)"; "high freq (hz)", and timings : ' + '"begin time (s)"; "end time (s)" and the species detected' + ' in the given files.') + ds.attrs['date_created'] = pd.Timestamp.now().isoformat() + ds.attrs['creator_name'] = os.getlogin() - df_list.append(annotation_df) + # Save Dataset to NetCDF file + ds.to_netcdf(dir_path) + print(f'Saved as {dir_path}') - result_df = pd.concat(df_list, ignore_index=True) - result_df.to_csv(os.path.join(outdir, f'{out_file}.csv'), index=False) - print(f'Saved as {os.path.join(outdir, f"{out_file}.csv")}') if __name__ == "__main__": - parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='TODO') - parser.add_argument('-p', '--path_to_data', type=arg_directory, help='Path of the folder that contains the .txt files', required=True) - parser.add_argument('-d', '--directory', type=arg_directory, help='Directory where the dataframe will be stored', required=True) - parser.add_argument('-t', '--duration', type=int, help='Duration of the spectrogram', required=True) - parser.add_argument('-s', '--SR', type=int, help='Sampling Rate of the spectrogram') + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Collect detections and return a complete dataframe') + parser.add_argument('path_to_data', type=utils.arg_directory, + help='Path of the folder that contains the .txt files') + parser.add_argument('directory', type=utils.arg_directory, + help='Directory where the dataframe will be stored') + parser.add_argument('names', type=str, + help='path to YOLOv5 custom_data.yaml file') + parser.add_argument('-s', '--sr', type=int, + help='Sampling Rate of the spectrogram', required=True) + parser.add_argument('--duration', type=int, + help='Duration of the spectrogram', default=8) + parser.add_argument('--path_to_wav', type=utils.arg_directory, + help='Path of the folder that contains the .wav files', required=True) + parser.add_argument('--raven', action='store_const', const=1, default=None, + help='Export the .txt per .WAV file to vizualize on Raven') args = parser.parse_args() - process_annotations(args.path_to_data, args.duration, args.SR) + main(args) diff --git a/get_train_annot.py b/get_train_annot.py new file mode 100755 index 0000000..1c7835f --- /dev/null +++ b/get_train_annot.py @@ -0,0 +1,260 @@ +"""Converts Raven format dataframe annotations to YOLO format""" + +import argparse +import random +import os +import sys +import cv2 +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import soundfile as sf + +import utils + +from p_tqdm import p_map +from tqdm import tqdm + + +def process(entry, arguments, species_list, colors): + """ + Precess the annotation to get the .jpg spectrogram and the .txt annotation file + :param x (tuple): Enumerate number, [filename, group] per file + :param arguments (args): Arguments + :param species_list (list): List with corresponding species + """ + _, (filename, grp) = entry + + try: + info = sf.info(filename) # Collection recording information + file_duration, fs = info.duration, info.samplerate + except Exception as e: + print(f'`{filename}` cannot be open... : {e}') + return + + # create the time list between 0 and 1000 * duration + # Create the list of all possible offset to compute spectrogram + offset_list = np.arange( + 0, file_duration, arguments.duration - arguments.overlap) + new_pos = utils.split_annotations( + grp[['start', 'stop']], arguments.duration) + grp = pd.merge(grp, new_pos) + while len(grp) != 0: + # collect all the data between the offset and duration-overlap + if len(offset_list) >= 1: + table = grp[grp.start <= offset_list[0] + arguments.duration] + else: + continue + + # create an empty dataframe + annotation = pd.DataFrame(columns=['id', 'x', 'y', 'width', 'height']) + + # if no data for this period, go to the next one + if len(table) == 0: + offset_list = offset_list[1:] + continue + + offset = offset_list[0] # take initial time for offset + + name = str(grp.iloc[0].Path.replace( + '/', '_').replace('.', '_') + '_' + str(offset)) + + sig, fs = sf.read(filename, start=int( + offset*fs), stop=int((offset+arguments.duration)*fs), always_2d=True) # Load the signal + sig = sig[:, 0] # Only take channel 0 + if arguments.rf is None: + arguments.rf = fs + # Apply resample and low/high pass filter + sig = utils.signal_processing( + sig, rf=arguments.rf, fs=fs, high=arguments.high, low=arguments.low) + _ = utils.create_spectrogram( + sig, arguments.directory, names=None, + window_size=arguments.window, + overlap=arguments.hop) + + for _, row in table.iterrows(): + specie = row.species + x_pxl = (row.midl - offset) / \ + arguments.duration # take x value in pixels + width_pxl = (row.stop - row.start) / \ + arguments.duration # take width value in pixels + + # take y value in pixels + y_pxl = 1 - (row.midl_y / (arguments.rf / 2)) + height_pxl = (row.max_freq - row.min_freq) / \ + (arguments.rf / 2) # take height value in pixels + if height_pxl > 1: + height_pxl = 1 + elif height_pxl > y_pxl * 2: + y_pxl = y_pxl + 0.5 * (height_pxl - y_pxl * 2) + # Store the annotation in a DataFrame + new_table = pd.DataFrame([[str(species_list[species_list.species == specie].index[0]), + x_pxl, y_pxl, width_pxl, height_pxl]], + columns=['id', 'x', 'y', 'width', 'height']) + + annotation = annotation.dropna(axis=1, how='all') + new_table = new_table.dropna(axis=1, how='all') + annotation = pd.concat([annotation, new_table]) + + grp = grp.drop(table.index) + + name_file = os.path.join(arguments.directory, + 'labels', f'{name}.txt') + # Create all the folder + for folder in ['images', 'labels', 'images/all', 'annotated_images']: + utils.create_directory(os.path.join( + arguments.directory, folder)) + for specie_num in species_list: + utils.create_directory(os.path.join( + arguments.directory, 'images', str(specie_num))) + # Save the images and annotation + plt.savefig(os.path.join(arguments.directory, + 'images', str(species_list[species_list.species == + specie].index[0]), f'{name}.jpg')) + annotation.to_csv(name_file, sep=' ', header=False, index=False) + plt.savefig(os.path.join(arguments.directory, 'images', 'all', + f'{name}.jpg')) + plt.close() + + # Add annotation to the images in another folder + image = cv2.imread( + os.path.join(arguments.directory, 'images', 'all', f'{name}.jpg')) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + # For each annotation in a spectrogram + for num, annot in annotation.iterrows(): + shp1, shape1, shp4, shape4 = utils.get_box_shape( + [annot, num], image) + + text_shape = shp1[0], shp1[1] - 5 + label = annot['id'] + + # Add the annotation into the images as a rectangle + cv2.rectangle(image, pt1=shape1, pt2=shape4, + color=colors[species_list[species_list.species == + specie].index[0]], + thickness=1) + cv2.rectangle( + image, pt1=shp1, pt2=shp4, + color=colors[species_list[species_list.species == + specie].index[0]], + thickness=-1) + # Add the label associated + cv2.putText(image, label, text_shape, + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1) + plt.imshow(image) + plt.subplots_adjust(top=1, bottom=0, left=0, + right=1) # Remove the border + plt.savefig( + os.path.join(arguments.directory, 'annotated_images', f'{name}.jpg')) + plt.close() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Create .txt and .jpg to each annotation ' + 'from a csv') + parser.add_argument('filename_path', type=str, + help='Path/name of the folder/file containing the annotations. If a file ' + 'use Raven format and add a Path columns with the path to the ' + '.wav files') + parser.add_argument('path_to_data', type=utils.arg_directory, + help='Path of the folder that contains the recordings') + parser.add_argument('directory', type=utils.arg_directory, + help='Directory to which spectrograms and .txt files will be stored') + parser.add_argument('--duration', type=int, + help='Duration for each spectrogram', default=8) + parser.add_argument('--overlap', type=int, + help='Overlap in seconds between 2 spectrograms', default=0) + parser.add_argument( + '--rf', type=int, help='Frequency Resampling ', default=None) + parser.add_argument( + '--window', type=int, help='Window size for the Fourier Transform', + default=1024) + parser.add_argument( + '--hop', type=float, help='Ratio of hop in window : 50%% = 0.5', default=.5) + parser.add_argument( + '--cpu', type=int, help='To speed up the process, write 2 or more', + default=1) + parser.add_argument('--high', type=int, + help='High Pass Filter value in Hz', default=10) + parser.add_argument('--low', type=int, + help='Low Pass Filter value in Hz', default=None) + parser.add_argument('--test', action='store_const', const=1, + help='Split into train/test/val. 1 - Ratio / 2 for test and' + ' same for validation', default=None) + args = parser.parse_args() + + # Load the data and put it into a DataFrame + df = utils.open_file(args.filename_path) + suffix = input('Which suffix for your recording data? [wav, WAV, Wav, flac, mp3] : ') + if len(df.columns) == 1: + final = [] + for file, _ in df.groupby('Path'): + new_df = utils.open_file(file) + if len(new_df) >= 1: + new_df['Path'] = os.path.join(args.path_to_data, + str(file.split('.Table')[0]+f'.{suffix}')) + final.append(new_df) + else: + continue + df = pd.concat(final) + elif 'Path' not in df.columns: + df['Path'] = os.path.join(args.path_to_data, + args.filename_path.split('/')[-1].split('.Table')[0]+f'.{suffix}') + df, species = utils.prepare_dataframe(df, args) + + list_colors = [(random.randint(0, 255), random.randint(0, 255), + random.randint(0, 255)) for _ in range(len(species))] + + species.to_csv(os.path.join( + args.directory, 'species_list.csv'), index=False) + + if args.cpu == 1: + for i in tqdm(enumerate(df.groupby('Path')), total=len(df.groupby('Path')), + desc="Processing", ascii='░▒▓█'): + process(i, args, species, list_colors) + else: + args = [args for _ in range(len(df.groupby('Path')))] + species = [species for _ in range(len(df.groupby('Path')))] + list_colors = [list_colors for _ in range(len(df.groupby('Path')))] + p_map(process, enumerate(df.groupby('Path')), args, + species, list_colors, num_cpus=args[0].cpu, total=len(df.groupby('Path'))) + args = args[0] + print('saved to', args.directory) + + if not args.test: + # Ask user if the script split the data or not + SPLIT = input( + 'Do you want to split your data into a random train/test/val ? [Y/N] : ') + else: + SPLIT = 'Y' + + if SPLIT == 'Y': + print('The train set will be 70%, val set 15% and test set 15%') + path = os.getcwd() + # Get the current path to find the split script + script = os.path.join(path, 'get_train_val.py') + data_path = os.path.join(path, args.directory, 'labels') + directory_path = os.path.join(path, args.directory, 'set') + # Create the directory path if not exists + utils.create_directory(directory_path) + try : + # Run the split command + os.system(f'{sys.executable} {script} {data_path} {directory_path} -r 0.7 --test') + print(f'Train saved in {directory_path}\n') + print('To train your model, use the following command : \n') + + yolo_path = os.path.join(path, 'yolov5/train.py') + data_path = os.path.join(directory_path, 'custom_data.yaml') + weights_path = os.path.join(path, 'yolov5/weights/yolov5l.pt') + hyp_path = os.path.join(path, 'custom_hyp.yaml') + + command = f'python {yolo_path} --data {data_path} --imgsz 640 --epochs 100 --weights {weights_path} --hyp {hyp_path} --cache' + print(command,'\n') + print('\u26A0\uFE0F Be aware that it is recommended to have background images that', + ' represents 10% of your dataset. To do so, please use the script "get_spectrogram.py"', + ' with --background arguments. Comptue on recordings that contains multiple', + ' type of noise...') + + except Exception as error: + print(error) diff --git a/get_train_val.py b/get_train_val.py new file mode 100755 index 0000000..de6d9a6 --- /dev/null +++ b/get_train_val.py @@ -0,0 +1,129 @@ +"""Separates training and validation datasets in a balanced manner""" + +import argparse +import os +import pandas as pd +import utils + +from tqdm import tqdm + + +def export_split(entry, path, directory): + """ + Export the annotations' sets + :param entry (list): Minor set = [0], Major set = [1] + :param path (str): Path to take the labels files + :param directory (str): Directory to save the data + """ + train_set = entry[0] + val_set = entry[1] + + for folder in ['images', 'labels', 'images/train', 'images/val', 'images/test', + 'labels/train', 'labels/val', 'labels/test']: + utils.create_directory(os.path.join(directory, folder)) + + if args.test: + test_set = entry[2] + test_set.file = ['.'.join(x.split('.')[:-1]) + for num, x in enumerate(test.file)] + + utils.copy_files_to_directory(test_set.file, path, os.path.join( + directory, 'labels/test'), 'txt') + utils.copy_files_to_directory(test_set.file, os.path.join( + path, '../images/all'), os.path.join(directory, 'images/test'), 'jpg') + + val_set.file = ['.'.join(x.split('.')[:-1]) + for _, x in enumerate(val.file)] + train_set.file = ['.'.join(x.split('.')[:-1]) + for _, x in enumerate(train_set.file)] + + # Copy the validation set into the folder + utils.copy_files_to_directory(val_set.file, path, os.path.join( + directory, 'labels/val'), 'txt') + utils.copy_files_to_directory(val_set.file, os.path.join( + path, '../images/all'), os.path.join(directory, 'images/val'), 'jpg') + # Copy the trainning set into the folder + utils.copy_files_to_directory(train_set.file, path, os.path.join( + directory, 'labels/train'), 'txt') + utils.copy_files_to_directory(train_set.file, os.path.join( + path, '../images/all'), os.path.join(directory, 'images/train'), 'jpg') + + try: + species_list = pd.read_csv(os.path.join(path, '../species_list.csv')) + except FileNotFoundError: + print('No species list detected, please add it to : ', + os.path.join(directory, 'custom_data.yaml')) + + with open(os.path.join(directory, 'custom_data.yaml'), 'w', encoding='utf-8') as f: + if args.test == 1: + f.write(f'test: {os.path.join(directory, "images/test")}\n') + f.write(f'train: {os.path.join(directory, "images/train")}\n') + f.write(f'val: {os.path.join(directory, "images/val")}\n') + f.write(f'nc: {len(species_list)}\n') + f.write(f'names: {species_list.species.tolist()}') + + +def prepare_data(arguments): + """ + Prepare the annotation before getting splited + :param args (args): Argument + :return detection (DataFrame): DataFrame with all the annotation to split + """ + detections = pd.concat({f: pd.read_csv(os.path.join(arguments.path_to_data, f), sep=' ', + names=['species', 'x', 'y', 'w', 'h']) + for f in tqdm(os.listdir(arguments.path_to_data), + desc="Processing", ascii='░▒▓█')}, + names=['file']) + + detections = detections.reset_index() + detections.species = detections.species.astype(float) + return detections + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Split the annotation into train, val and test if needed') + parser.add_argument('path_to_data', type=utils.arg_directory, + help='Path of the folder that contains the .txt (ending with labels/)') + parser.add_argument('directory', type=utils.arg_directory, + help='Directory to which spectrogram and .txt files will be' + 'stored (different from -p)') + parser.add_argument('-r', '--ratio', type=float, + default=0.7, help='Train Ratio (val = 1 - ratio)') + parser.add_argument('--test', action='store_const', const=1, + help='Split into train/test/val. 1 - Ratio / 2 ' + ' for test and same for validation', default=None) + args = parser.parse_args() + + df = prepare_data(args) + train, val = utils.split(df, 'train', args.ratio) + + if args.test: + val, test = utils.split(val, 'val', 0.5) + export_split([train, val, test], args.path_to_data, args.directory) + state, proposition = utils.get_set_info([train, val, test]) + else: + export_split([train, val], args.path_to_data, args.directory) + state, proposition = utils.get_set_info([train, val]) + + print(f'\nYour dataset is {state} {proposition}') + + print(f'Train saved in {args.directory}\n') + print('To train your model, use the following command : \n') + + path = os.getcwd() + + directory_path = os.path.join(path, args.directory, 'set') + + yolo_path = os.path.join(path, 'yolov5/train.py') + data_path = os.path.join(directory_path, 'custom_data.yaml') + weights_path = os.path.join(path, 'yolov5/weights/yolov5l.pt') + hyp_path = os.path.join(path, 'custom_hyp.yaml') + + command = f'python {yolo_path} --data {data_path} --imgsz 640 --epochs 100 --weights {weights_path} --hyp {hyp_path} --cache' + print(command,'\n') + print('\u26A0\uFE0F Be aware that it is recommended to have background images that', + 'represents 10% of your dataset. To do so, please use the script "get_spectrogram.py"', + 'with --background arguments. Comptue on recordings that contains multiple type of noise...') + \ No newline at end of file diff --git a/labelme2yolo.py b/labelme2yolo.py old mode 100644 new mode 100755 index 5ba5fa2..8c6a4c1 --- a/labelme2yolo.py +++ b/labelme2yolo.py @@ -1,72 +1,44 @@ +"""Converts LabelMe annotations to YOLO format""" + import os import json -import base64 -import shutil import argparse from glob import glob -from pathlib import Path - -def arg_directory(path): - if os.path.isdir(path): - return path - else: - raise argparse.ArgumentTypeError(f'`{path}` is not a valid path') - -def convert_labelme_to_yolo(labelme_annotation_path, yolo_directory): - # Load LabelMe annotation - image_id = Path(labelme_annotation_path).stem - with open(labelme_annotation_path, 'r') as labelme_annotation_file: - labelme_annotation = json.load(labelme_annotation_file) - - # YOLO annotation and image paths - yolo_annotation_path = os.path.join(yolo_directory, 'labels', f'{image_id}.txt') - yolo_image_path = os.path.join(yolo_directory, 'images', f'{image_id}.jpg') - - with open(yolo_annotation_path, 'w') as yolo_annotation_file: - yolo_image_data = base64.b64decode(labelme_annotation['imageData']) - - # Write YOLO image - with open(yolo_image_path, 'wb') as yolo_image_file: - yolo_image_file.write(yolo_image_data) +import utils - # Write YOLO image annotation - for shape in labelme_annotation['shapes']: - if shape['shape_type'] != 'rectangle': - print(f'Invalid type `{shape["shape_type"]}` in annotation `{labelme_annotation_path}`') - continue - label = shape['label'] - x1, y1 = shape['points'][0] - x2, y2 = shape['points'][1] - width = x2 - x1 - height = y2 - y1 - x_center = (x1 + x2) / 2 - y_center = (y1 + y2) / 2 - - annotation_line = f'{label} {x_center} {y_center} {width} {height}\n' - yolo_annotation_file.write(annotation_line) - -def main(args): +def main(arguments): + """ + Launch the processing on each .txt. + :param arguments (args): Argument + """ yolo_names = set() - - for labelme_annotation_path in glob(f'{args.path_to_data}/*.json'): - convert_labelme_to_yolo(labelme_annotation_path, args.directory) - - with open(labelme_annotation_path, 'r') as labelme_annotation_file: + for folder in ['images', 'labels', 'images/all']: # Create saving directory + utils.create_directory(os.path.join(arguments.directory, folder)) + + # For each annotation, convert into YOLO format + for labelme_annotation_path in glob(f'{arguments.path_to_data}/*.json'): + utils.labelme2yolo(labelme_annotation_path, arguments.directory) + # Add all the information in the .txt file + with open(labelme_annotation_path, 'r', encoding='utf-8') as labelme_annotation_file: labelme_annotation = json.load(labelme_annotation_file) for shape in labelme_annotation['shapes']: yolo_names.add(shape['label']) - # Write YOLO names - yolo_names_path = os.path.join(args.directory, 'custom.names') - with open(yolo_names_path, 'w') as yolo_names_file: + # Write YOLO names and save it + yolo_names_path = os.path.join(arguments.directory, 'custom.names') + with open(yolo_names_path, 'w', encoding='utf-8') as yolo_names_file: yolo_names_file.write('\n'.join(yolo_names)) + if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Convert LabelMe annotations to YOLO compatible') - parser.add_argument('-p', '--path_to_data', type=arg_directory, help='Path to LabelMe annotations', required=True) - parser.add_argument('-d', '--directory', type=arg_directory, help='Directory to which YOLO annotations will be stored', required=True) + parser = argparse.ArgumentParser( + description='Convert LabelMe annotations to YOLO compatible') + parser.add_argument('path_to_data', type=utils.arg_directory, + help='Path to LabelMe annotations') + parser.add_argument('directory', type=utils.arg_directory, + help='Directory to which YOLO annotations will be stored') args = parser.parse_args() main(args) diff --git a/requirements.txt b/requirements.txt index 1ac3daf..9cdf446 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,51 +1,42 @@ -# YOLOv5 requirements +# Raven2YOLO requirements # Usage: pip install -r requirements.txt # Base ------------------------------------------------------------------------ -gitpython>=3.1.30 -matplotlib>=3.2.2 -numpy>=1.18.5 -opencv-python>=4.1.1 -Pillow>=7.1.2 -psutil # system resources -PyYAML>=5.3.1 -requests>=2.23.0 -scipy>=1.4.1 -thop>=0.1.1 # FLOPs computation -torch>=1.7.0 # see https://pytorch.org/get-started/locally (recommended) -torchvision>=0.8.1 -tqdm>=4.64.0 -librosa==0.9.2 -# protobuf<=3.20.1 # https://github.com/ultralytics/yolov5/issues/8012 - -# Logging --------------------------------------------------------------------- -tensorboard>=2.4.1 -# clearml>=1.2.0 -# comet + +argparse==1.4.0 # Command-line argument parsing +matplotlib>=3.2.2 # Plotting library +numpy>=1.18.5 # Numerical computing library +opencv-python>=4.1.1 # Computer vision library +Pillow>=7.1.2 # Python Imaging Library +PyYAML>=5.3.1 # YAML parser and emitter +os>=0.11.0 # Operating system interface +pandas>=2.2.2 # Data manipulation and analysis +tqdm>=4.64.0 # Progress bar library # Plotting -------------------------------------------------------------------- -pandas>=1.1.4 -seaborn>=0.11.0 - -# Export ---------------------------------------------------------------------- -# coremltools>=6.0 # CoreML export -# onnx>=1.12.0 # ONNX export -# onnx-simplifier>=0.4.1 # ONNX simplifier -# nvidia-pyindex # TensorRT export -# nvidia-tensorrt # TensorRT export -# scikit-learn<=1.1.2 # CoreML quantization -# tensorflow>=2.4.1 # TF exports (-cpu, -aarch64, -macos) -# tensorflowjs>=3.9.0 # TF.js export -# openvino-dev # OpenVINO export - -# Deploy ---------------------------------------------------------------------- -setuptools>=65.5.1 # Snyk vulnerability fix -# tritonclient[all]~=2.24.0 + +matplotlib>=3.7.1 # Plotting library +seaborn>=0.11.0 # Statistical data visualization # Extras ---------------------------------------------------------------------- -# ipython # interactive notebook -# mss # screenshots -# albumentations>=1.0.3 -# pycocotools>=2.0.6 # COCO mAP -# roboflow -# ultralytics # HUB https://hub.ultralytics.com + +ipdb>=0.13.9 # IPython debugger +ipython>=8.1.1 # Interactive Python shell + +# Additional Packages --------------------------------------------------------- + +glob>=1.3.0 # Unix-style pathname pattern expansion +shutil>=1.0.0 # High-level file operations +base64>=1.2.0 # Base16, Base32, Base64, Base85 data encoding +json>=1.0.0 # JSON encoder/decoder +datetime>=1.0.0 # Basic date and time types +pathlib>=1.0.0 # Object-oriented filesystem paths +librosa>=0.9.2 # Audio and music processing library +scipy>=1.8.0 # Scientific library for numerical computations +cv2>=1.0.0 # OpenCV computer vision library +soundfile>=0.11.0 # Sound library +p_tqdm==1.4.0 # Parallel tqdm (progress bar) for Python +yaml==6.0 # YAML parser and emitter +torch==1.12.0 # PyTorch deep learning framework +albumentations>=1.0.3 # Image augmentation library +scikit-learn<=1.1.2 # Machine learning library diff --git a/utils.py b/utils.py new file mode 100755 index 0000000..947e5bb --- /dev/null +++ b/utils.py @@ -0,0 +1,508 @@ +"""Define all the function that are used in the repository""" + +import argparse +import glob +import shutil +import os +import base64 +import json +from datetime import date +from pathlib import Path +import librosa + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import xarray as xr +from scipy import signal +from tqdm import tqdm + + +def arg_directory(path): + """ + Check if the path given in args is a real directory or not. + :param path (str): The path to a folder. + :return str: Return the path if correct, error if doesn't exist. + """ + if os.path.isdir(path): + return path + raise argparse.ArgumentTypeError(f'{path} is not a valid path') + + +def copy_files_to_directory(file_list, source, directory, suffix): + """ + Copy files from a directory to another one + :param file_list (str): List of the file to copy. + :param source_dir (str): Directory of the original files. + :param directory (str): Directory to copy the new files. + :param suffix (str): Suffix of the files + """ + for filename in file_list: + source_file = os.path.join(source, f'{filename}.{suffix}') + destination_path = os.path.join(directory, f'{filename}.{suffix}') + shutil.copy2(source_file, destination_path) + + +def create_directory(directory): + """ + Create a directory if not exists. + :param directory (str): Directory to create. + """ + # Check if directory exists + if not os.path.exists(directory): + os.mkdir(directory) + print(f'`{directory}` has been created') + + +def signal_processing(sig, rf, fs, high=None, low=None): + """ + Resample the signal and apply high pass and low pass filter. + + :param sig (array): Signal. + :param rf (int): Resampling Frequency. + :param fs (int): Original Sampling Frequency. + :param high (int): High pass filter value (default None). + :param low (int): Low pass filter value (default None). + :return array: Processed signal. + """ + # Check if resampling frequency is different than sampling frequency + if rf == None: + rf = fs + if rf != fs: + sig = signal.resample(sig, int(len(sig) * rf / fs) + ) # Resample the signal + + # Apply high pass filter if specified + if high: + # Create high pass filter + high_pass = signal.butter(2, high / (rf / 2), 'hp', output='sos') + sig = signal.sosfilt(high_pass, sig) # Apply high pass filter + + # Apply low pass filter if specified + if low: + # Create low pass filter + low_pass = signal.butter(1, low / (rf / 2), 'lp', output='sos') + sig = signal.sosfilt(low_pass, sig) # Apply low pass filter + + return sig + + +def create_spectrogram(sig, directory, names, window_size=1024, overlap=.5): + """ + Create a spectrogram STFT with hanning window and save it into a directory + + :param sig (array): Signal to process. + :param window_size (int): Number of sample / STFT window. + :param overlap (float): Ratio of overlapping samples between each window (default 50%). + :param directory (str): Path to save the spectrogram. + :param filename (str): Name of the final spectrogram. + """ + overlap_size = int(window_size * overlap) + + stft = librosa.stft(sig, n_fft=window_size, + hop_length=overlap_size, window='hann') # Compute the STFT + stft = np.log10(np.abs(stft)) # Adapt the Complex-valued matrix + fig = plt.figure() + # plot the spectrogram + plt.imshow(stft[::-1], aspect='auto', + interpolation=None, cmap='viridis', vmin=stft.mean()) + # Remove all the borders around the plot + plt.subplots_adjust(top=1, bottom=0, left=0, right=1) + if names: + folder = names.split('/')[-2] + create_directory(os.path.join(directory, folder)) + plt.savefig(f'{names}.jpg') + plt.close() # Close the figure + return + else : + return fig # Return the figure + + +def split(df, method, ratio=0.7): + """ + Split an annotation dataframe into 2 groups with a ratio + :param df (DataFrame): DataFrame containing the annotation with 2 columns : + 'species' : number between 0 and n; and 'file' : Path of the file that contain the annotation. + :param ratio (float): Ratio of the annotation in major instead of minor + :return major_df: DataFrame containing the major part of the annotations. + :return minor_df: DataFrame containing the minor part of the annotations. + """ + classes = df.species.unique() + n_class = classes.size + # Initialize 2 counters + major_count = pd.DataFrame(np.zeros((n_class, 1)), index=classes) + minor_count = major_count.copy() + # Initialize 2 DataFrame + major_df = pd.DataFrame() + minor_df = pd.DataFrame() + # Go throught the differents classes + for _, specie in enumerate(classes): + try: + data = df.groupby('species').get_group(specie) + except KeyError: + print( + f"Warning: The species '{specie}' was not found in the DataFrame.") + continue + except Exception as error: + print( + f"An unexpected error occurred while processing the species '{specie}': {error}") + continue + # Add a first annotation in both major and minor DataFrame + if major_count.loc[specie].iloc[0] == 0: + # Random sampling of 1 annotation + annotation = data.sample(1).file.iloc[0] + mask = df.file == annotation + major_count = major_count.add( + df[mask].species.value_counts(), axis=0).fillna(0) + major_df = pd.concat([major_df, df[mask]]) + # Removing the annotation from the original DataFrame + df = df[~mask] + if minor_count.loc[specie].iloc[0] == 0: + # Random sampling of 1 annotation + annotation = data.sample(1).file.iloc[0] + mask = df.file == annotation + minor_count = minor_count.add( + df[mask].species.value_counts(), axis=0).fillna(0) + minor_df = pd.concat([minor_df, df[mask]]) + # Removing the annotation from the original DataFrame + df = df[~mask] + # Go throught df to do the split until no data left in df + while len(df): + # find the least common species in the DataFrame + min_esp = df.groupby('species').count().file.idxmin() + # find all the data of this species + data = df.groupby('species').get_group(min_esp) + # Random sampling of 1 annotation + annotation = data.sample(1).file.iloc[0] + # Check the actual ratio + if (major_count.loc[min_esp]/(minor_count.loc[min_esp] + + major_count.loc[min_esp]))[0] > ratio: + # between major and minor + minor_count.loc[min_esp] += df[df.file == + annotation].groupby('species').count().iloc[0].file + minor_df = pd.concat([minor_df, df[df.file == annotation]]) + else: + major_count.loc[min_esp] += df[df.file == + annotation].groupby('species').count().iloc[0].file + major_df = pd.concat([major_df, df[df.file == annotation]]) + # Removing the annotation from the original DataFrame + df = df[df.file != annotation] + res = major_count/(minor_count + major_count) + res.columns = [f'{method} ratio'] + if method == 'train': + other = 'val' + else: + other = 'test' + res[f'{other} ratio'] = 1 - res[res.columns[0]] + res = res.reset_index().rename(columns={'index': 'class'}) + print('\n', res) + return major_df, minor_df + + +def open_file(path): + """ + Open a file with a path without knowing if suffix is .pkl or .csv + :param path (str): Path to the file to open or the folder that + contains all the files to conactenate + :return df: DataFrame. + """ + suffix = path.split('.')[-1] # Extract the suffix of the file + if suffix == 'pkl': + print('Try to load as pickle...') + df = pd.read_pickle(path) + elif suffix == 'csv': + if path.split('/')[-1] == 'species_list.csv': + return pd.DataFrame() + else: + print('Try to load as csv...') + try: + df = pd.read_csv(path) + except Exception: + df = pd.read_csv(path, sep=';') + elif suffix == 'nc': + print('Try to load as netcdf...') + ds = xr.load_dataset(path) + df = ds.to_dataframe() + elif suffix == 'txt': + print('Try to load as txt...') + df = pd.read_csv(path, sep='\t') + elif suffix == 'wav' or suffix == 'WAV' or suffix == 'Wav': + print("Wav files can't be load...") + return pd.DataFrame() + else: + print('Collect all files on a folder...') + df = pd.DataFrame(glob.glob(os.path.join(path, '*'), + recursive=True), columns=['Path']) + return df + + +def process_json_files(json_dir, img_dir, directory): + """ + Process json annotation and add data information + :param json_dir (str): Path to the .json files + :param img (str): Path to the .jpg files + :param directory (str): Directory to save the results + """ + json_files = [f for f in os.listdir(json_dir) if f.endswith( + '.json')] # Collect all the .json file int the path + + for json_file in json_files: # Process each file one by one + json_path = os.path.join(json_dir, json_file) + img_path = os.path.join(img_dir, json_file.replace('.json', '.jpg')) + + if not os.path.exists(img_path): + continue + try: + with open(img_path, 'rb') as img_file: # Load the images + image_data = base64.b64encode(img_file.read()).decode( + 'utf-8') # Collect the images pixels information + # and encode into the correct format + except FileNotFoundError: + continue + + with open(json_path, 'r', encoding='utf-8') as f: + json_data = json.load(f) + + json_data['imageData'] = image_data + json_data['imagePath'] = img_path + + output_path = os.path.join(directory, json_file) + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(json_data, f, indent=4) + + +def labelme2yolo(labelme_annotation_path, yolo_directory): + """ + Process json annotation and convert to labelme format + :param labelme_annotation_path (str): Path to the .json files + :param yolo_directory (str): Directory to save the .txt files + """ + # Load LabelMe annotation + image_id = Path(labelme_annotation_path).stem + with open(labelme_annotation_path, 'r', encoding='utf-8') as labelme_annotation_file: + labelme_annotation = json.load(labelme_annotation_file) + + # YOLO annotation and image paths + yolo_annotation_path = os.path.join( + yolo_directory, 'labels', f'{image_id}.txt') + yolo_image_path = os.path.join( + yolo_directory, 'images/all', f'{image_id}.jpg') + + with open(yolo_annotation_path, 'w', encoding='utf-8') as yolo_annotation_file: + yolo_image_data = base64.b64decode(labelme_annotation['imageData']) + + # Write YOLO image + with open(yolo_image_path, 'wb') as yolo_image_file: + yolo_image_file.write(yolo_image_data) + + # Write YOLO image annotation + for shape in labelme_annotation['shapes']: + if shape['shape_type'] != 'rectangle': + print( + f'Invalid type `{shape["shape_type"]}` in annotation `{labelme_annotation_path}`' + ) + continue + + label = shape['label'] + + # shape['points'] format : [[x1,y1],[x2,y2]...] # + scale_width = 1.0 / labelme_annotation['imageWidth'] + scale_height = 1.0 / labelme_annotation['imageHeight'] + width = abs(shape['points'][1][0] - + shape['points'][0][0]) * scale_width + height = abs(shape['points'][1][1] - + shape['points'][0][1]) * scale_height + + x = min(shape['points'][0][0], shape['points'] + [1][0]) * scale_width + width / 2 + y = min(shape['points'][0][1], shape['points'] + [1][1]) * scale_height + height / 2 + if x+width/2 > 1 or y+height/2 > 1: + print( + f'Error with bounding box values over 1 in file {yolo_image_file}') + annotation_line = f'{label} {x} {y} {width} {height}\n' + yolo_annotation_file.write(annotation_line) + + +def prepare_dataframe(df, args): + """ + Prepare the annotation in time frequency + :param df (DataFrame): DataFrame that contains the annotations' informations + :param args (args): Argument + :return df (DataFrame): Prepared DataFrame + :return colors (list): Color for each class + :return species_list (list): List of each class + """ + df.rename(columns={'Begin Time (s)': 'start', 'End Time (s)': 'stop', + 'Low Freq (Hz)': 'min_freq', 'High Freq (Hz)': 'max_freq', 'Annotation' : 'species'}, + inplace=True) + + species_list = df.groupby('species').size().sort_values( + ascending=False).reset_index() + + df['d_annot'] = df.stop - df.start + df['midl'] = (df.stop + df.start) / 2 + df['midl_y'] = (df.min_freq+df.max_freq)/2 + + df = df[df.d_annot < args.duration] + df = df.reset_index() + + return df, species_list + + +def detection2time_freq(annotations_folder, duration, outdir, sr, names, wav, raven): + """ + Collect all .txt detection and get time and frequency informations + :param annotations_folder (str): Path to the .json files + :param duration (int): Directory to save the .txt files + :param outfir (str): Directory to save the .txt files + :param sr (int): Directory to save the .txt files + :param names (str): Directory to save the .txt files + """ + today = date.today() + out_file = f'YOLO_detection_{today.day}_{today.month}_freq_{sr}_duration_{duration}.nc' + + # Load and process data + df = pd.concat({f: pd.read_csv(os.path.join(annotations_folder, f), + sep=' ', names=['class', 'x', 'y', 'w', 'h', 'conf']) + for f in tqdm(os.listdir(annotations_folder), + desc="Processing", ascii='░▒▓█')}, + names=['file']) + + df = df.reset_index(level=[0]) + df = df.reset_index(drop=True) + # Collect start time of the spectrogram + df['offset'] = df.file.str.split('_').str[-1].str.split('.').str[0] + # Remove all the path to keep the file name + df.file = ['.'.join(x.file.split('.')[:-1]) + + '.WAV' for i, x in df.iterrows()] + + if len(names) == 0: + total = len(df.groupby('species').size()) - 1 + print( + f'Consider that no names has been put into : {names} list, so it will automatically be from 0 to {total}') + names = np.arange(0, total+1).tolist() + df['species'] = df['class'].apply(lambda x: names[int(x)]) + + df['pos'] = (df['x'] * duration) + df['class'].astype(int) + df['Low Freq (Hz)'] = (1 - df['y']) * (sr / 2) - (df['h'] * (sr / 2)) / 2 + df['High Freq (Hz)'] = (1 - df['y']) * (sr / 2) + (df['h'] * (sr / 2)) / 2 + df['Begin Time (s)'] = df['pos'] - (df['w'] * duration) / 2 + df['End Time (s)'] = df['pos'] + (df['w'] * duration) / 2 + df['duration'] = df['End Time (s)'] - df['Begin Time (s)'] + + # Extract the annotation of each file and save them into a .txt file + if raven: + folder = 'Raven_annotation' + create_directory(os.path.join(outdir, folder)) + # Collect all the original filename + files = pd.DataFrame(os.listdir(wav), columns = ['file_origin']) + files['file'] = files.file_origin.str.split('.').str[0] + # Remove the time information in the detection filename + df['filename'] = ['_'.join(file.split('_')[:-1]) for file in df.file] + print('\nSaving the Raven Annotations files...\n') + for file, grp in tqdm(df.groupby('filename'), + total=len(df.groupby('filename').size()), + desc="Processing", ascii='░▒▓█'): + # Check if the filename match with an original file .wav + for _, row in files.iterrows(): + if row.file in file: + file = row['file_origin'] + + file = '.'.join(file.split('.')[:-1]) + filename_raven = f'{file}.Table1.txt' + dir_raven = os.path.join(outdir, folder, filename_raven) + grp.to_csv(dir_raven, sep='\t', index=False) + print(f'Annotation saved in <{outdir}> as {folder}') + + dir_path = os.path.join(outdir, out_file) + return df, dir_path + + +def split_annotations(df, duration=8): + """ + Split the annotations into multiple segments if they span across different spectrograms. + :param df (DataFrame): DataFrame containing the annotations with 'start' and 'stop' columns. + :param duration (int): Duration of a single spectrogram. + :return (DataFrame): DataFrame containing the split annotations. + """ + splited_annotations = [] + + for _, row in df.iterrows(): + start = row['start'] + end = row['stop'] + + while start < end: + # Calculate the end of the current spectrogram chunk + current_chunk_end = (start // duration + 1) * duration + + if end > current_chunk_end: + if (current_chunk_end - start) >= (end - start) * 0.5: + # Split the annotation + splited_annotations.append( + {'start': start, 'stop': current_chunk_end}) + start = current_chunk_end + else: + # If the remaining segment is less than half of the annotation + # only keep the longest part + splited_annotations.append( + {'start': current_chunk_end, 'stop': end}) + break + else: + # This annotation fits within the current chunk + splited_annotations.append({'start': start, 'stop': end}) + break + + return pd.DataFrame(splited_annotations) + + +def get_box_shape(info, im): + """ + Get the pixels information to place the bounding box in the image. + :param im (array): Image array in cv2.cvtColor(im, cv2.COLOR_BGR2RGB) format + :param annotation (DataFrame): DataFrame with x, y, width, and height information + :return shp1 (tuple): Positions in x and y of the top left point in pixels + :return shape1 (tuple): Positions in x and y of the top left point in ratio + :return shp4 (tuple): Positions in x and y of the bottom right point in pixels + :return shape4 (tuple): Positions in x and y of the bottom right point in ratio + """ + annotation, _ = info + H, W = im.shape[0], im.shape[1] + x, y, w, h = annotation.x * W, annotation.y * \ + H, annotation.width * W, annotation.height * H + + shape1 = (int(x - 0.5 * w), int(y + 0.5 * h)) + shape4 = (int(x + 0.5 * w), int(y - 0.5 * h)) + + shp1 = (shape4[0], shape4[1]) + shp4 = (shape4[0], shape4[1]) + + return shp1, shape1, shp4, shape4 + + +def get_set_info(entry): + """ + Check if the dataset is balanced + :param entry (list): List containing train, val and test dataset + :return state (str): State of the dataset : balanced or unbalanced + :return proposition (str): If balance, just str, else a str + list of weights + """ + # Check entry size + if len(entry) == 2: + train, val = entry[0], entry[1] + dataset = pd.concat([train, val]) # Concat the datasets into one + else: + train, val, test = entry[0], entry[1], entry[2] + dataset = pd.concat([train, val, test]) # Concat the datasets into one + + # Check whether the minor class is under-represented + if dataset.groupby('species').size().min() < dataset.groupby('species').size().max()*.3: + state = 'unbalanced' + # Calculate the multiple factor to get a balanced dataset + weights_list = (dataset.groupby('species').size().max() / + dataset.groupby('species').size()).tolist() + proposition = f'\u274C you should use positive class weights in the custom_hyp.yaml cls_pw, add this {weights_list}' + else: + state = 'balanced' + proposition = '\u2705 this is good' + return state, proposition diff --git a/yolo2labelme.py b/yolo2labelme.py new file mode 100755 index 0000000..7937b03 --- /dev/null +++ b/yolo2labelme.py @@ -0,0 +1,35 @@ +"""Converts YOLO detections to LabelMe format""" + +import argparse +import os +import utils + + +def main(arguments): + """ + Launch the processing on each .json. + :param arguments (args): Argument + """ + if arguments.directory is None: + arguments.directory = arguments.path_to_txt + utils.process_json_files( + arguments.path_to_txt, arguments.path_to_img, + arguments.directory) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Convert YOLO annotation to .json file') + parser.add_argument('path_to_txt', type=utils.arg_directory, + help='Path to the folder containing the .txt files') + parser.add_argument('path_to_img', type=utils.arg_directory, + help='Path to the folder containing the .jpg images') + parser.add_argument('-d', '--directory', type=utils.arg_directory, help='Directory to which modified .json files will be stored' + 'if no argument, directory will be same as path_to_txt', required=False, + default=None) + args = parser.parse_args() + + # Convert the .json file into .txt in the labelme format + os.system( + f'globox convert {args.path_to_txt} {args.directory} --format yolo --save_fmt labelme --img_folder {args.path_to_img}') + main(args) diff --git a/yolov5/benchmarks.py b/yolov5/benchmarks.py old mode 100644 new mode 100755 diff --git a/yolov5/data/Argoverse.yaml b/yolov5/data/Argoverse.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/GlobalWheat2020.yaml b/yolov5/data/GlobalWheat2020.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/ImageNet.yaml b/yolov5/data/ImageNet.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/Objects365.yaml b/yolov5/data/Objects365.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/SKU-110K.yaml b/yolov5/data/SKU-110K.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/VOC.yaml b/yolov5/data/VOC.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/VisDrone.yaml b/yolov5/data/VisDrone.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/coco.yaml b/yolov5/data/coco.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/coco128-seg.yaml b/yolov5/data/coco128-seg.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/coco128.yaml b/yolov5/data/coco128.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/hyps/hyp.Objects365.yaml b/yolov5/data/hyps/hyp.Objects365.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/hyps/hyp.VOC.yaml b/yolov5/data/hyps/hyp.VOC.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/hyps/hyp.no-augmentation.yaml b/yolov5/data/hyps/hyp.no-augmentation.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/hyps/hyp.scratch-high.yaml b/yolov5/data/hyps/hyp.scratch-high.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/hyps/hyp.scratch-low.yaml b/yolov5/data/hyps/hyp.scratch-low.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/hyps/hyp.scratch-med.yaml b/yolov5/data/hyps/hyp.scratch-med.yaml old mode 100644 new mode 100755 diff --git a/yolov5/data/images/bus.jpg b/yolov5/data/images/bus.jpg old mode 100644 new mode 100755 diff --git a/yolov5/data/images/zidane.jpg b/yolov5/data/images/zidane.jpg old mode 100644 new mode 100755 diff --git a/yolov5/data/scripts/download_weights.sh b/yolov5/data/scripts/download_weights.sh old mode 100644 new mode 100755 diff --git a/yolov5/data/scripts/get_coco.sh b/yolov5/data/scripts/get_coco.sh old mode 100644 new mode 100755 diff --git a/yolov5/data/scripts/get_coco128.sh b/yolov5/data/scripts/get_coco128.sh old mode 100644 new mode 100755 diff --git a/yolov5/data/scripts/get_imagenet.sh b/yolov5/data/scripts/get_imagenet.sh old mode 100644 new mode 100755 diff --git a/yolov5/data/xView.yaml b/yolov5/data/xView.yaml old mode 100644 new mode 100755 diff --git a/yolov5/detect.py b/yolov5/detect.py old mode 100644 new mode 100755 index 30ce41f..6c67e76 --- a/yolov5/detect.py +++ b/yolov5/detect.py @@ -33,6 +33,7 @@ import os import platform import sys from pathlib import Path +from datetime import date import torch @@ -70,7 +71,9 @@ def run( classes=None, # filter by class: --class 0, or --class 0 2 3 agnostic_nms=False, # class-agnostic NMS augment=False, # augmented inference - sr=22050, + rf=22050, + window=1024, + hop=0.5, visualize=False, # visualize features update=False, # update all models project=ROOT / 'runs/detect', # save results to project/name @@ -92,6 +95,14 @@ def run( if is_url and is_file: source = check_file(source) # download + project_name = input('Please enter the name of your project : ') + date_now = date.today().strftime("%Y%m%d") + if sound: + name = '_'.join([project_name, 'detect', date_now, weights[0].split('/')[-3], 'conf', str(conf_thres).replace('.','_'), str(rf), str(sampleDur), str(window), str(int(window*hop)),'']) + else: + name = '_'.join([project_name, 'detect', date_now, weights[0].split('/')[-3], 'conf', str(conf_thres).replace('.','_'),'']) + print(f'\nYour train results will be saved as {name}\n') + # Directories save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir @@ -111,7 +122,8 @@ def run( elif screenshot: dataset = LoadScreenshots(source, img_size=imgsz, stride=stride, auto=pt) elif sound: - dataset = LoadSpectros(source, sampleDur, sr, img_size=imgsz, stride=stride, auto=pt) + hop = window * hop + dataset = LoadSpectros(source, sampleDur, rf, window, hop, img_size=imgsz, stride=stride, auto=pt) else: dataset = LoadImages(source, img_size=imgsz, stride=stride, auto=pt, vid_stride=vid_stride) vid_path, vid_writer = [None] * bs, [None] * bs @@ -246,11 +258,13 @@ def parse_opt(): parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)') parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels') - parser.add_argument('--sampleDur', default=8, type=int) - parser.add_argument('--sr', default=22050, type=int) + parser.add_argument('--sampleDur', default=8, help="Duration for each spectrogram for detection",type=int) + parser.add_argument('--rf', default=22050, help="Resampling Frequency",type=int) + parser.add_argument('--window', default=1024, help="Window size for each spectrogram for detection",type=int) + parser.add_argument('--hop', default=0.5, help="Hop lenght for each spectrogram for detection",type=float) parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences') parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') - parser.add_argument('--sound', default=False, action='store_true') + parser.add_argument('--sound', default=True, action='store_true') parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride') opt = parser.parse_args() diff --git a/yolov5/export.py b/yolov5/export.py old mode 100644 new mode 100755 diff --git a/yolov5/hubconf.py b/yolov5/hubconf.py old mode 100644 new mode 100755 diff --git a/yolov5/models/__init__.py b/yolov5/models/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/models/common.py b/yolov5/models/common.py old mode 100644 new mode 100755 diff --git a/yolov5/models/experimental.py b/yolov5/models/experimental.py old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/anchors.yaml b/yolov5/models/hub/anchors.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov3-spp.yaml b/yolov5/models/hub/yolov3-spp.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov3-tiny.yaml b/yolov5/models/hub/yolov3-tiny.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov3.yaml b/yolov5/models/hub/yolov3.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-bifpn.yaml b/yolov5/models/hub/yolov5-bifpn.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-fpn.yaml b/yolov5/models/hub/yolov5-fpn.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-p2.yaml b/yolov5/models/hub/yolov5-p2.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-p34.yaml b/yolov5/models/hub/yolov5-p34.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-p6.yaml b/yolov5/models/hub/yolov5-p6.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-p7.yaml b/yolov5/models/hub/yolov5-p7.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5-panet.yaml b/yolov5/models/hub/yolov5-panet.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5l6.yaml b/yolov5/models/hub/yolov5l6.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5m6.yaml b/yolov5/models/hub/yolov5m6.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5n6.yaml b/yolov5/models/hub/yolov5n6.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5s-LeakyReLU.yaml b/yolov5/models/hub/yolov5s-LeakyReLU.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5s-ghost.yaml b/yolov5/models/hub/yolov5s-ghost.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5s-transformer.yaml b/yolov5/models/hub/yolov5s-transformer.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5s6.yaml b/yolov5/models/hub/yolov5s6.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/hub/yolov5x6.yaml b/yolov5/models/hub/yolov5x6.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/segment/yolov5l-seg.yaml b/yolov5/models/segment/yolov5l-seg.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/segment/yolov5m-seg.yaml b/yolov5/models/segment/yolov5m-seg.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/segment/yolov5n-seg.yaml b/yolov5/models/segment/yolov5n-seg.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/segment/yolov5s-seg.yaml b/yolov5/models/segment/yolov5s-seg.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/segment/yolov5x-seg.yaml b/yolov5/models/segment/yolov5x-seg.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/tf.py b/yolov5/models/tf.py old mode 100644 new mode 100755 diff --git a/yolov5/models/yolo.py b/yolov5/models/yolo.py old mode 100644 new mode 100755 diff --git a/yolov5/models/yolov5l.yaml b/yolov5/models/yolov5l.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/yolov5m.yaml b/yolov5/models/yolov5m.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/yolov5n.yaml b/yolov5/models/yolov5n.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/yolov5s.yaml b/yolov5/models/yolov5s.yaml old mode 100644 new mode 100755 diff --git a/yolov5/models/yolov5x.yaml b/yolov5/models/yolov5x.yaml old mode 100644 new mode 100755 diff --git a/yolov5/train.py b/yolov5/train.py old mode 100644 new mode 100755 index c4e3aac..ce470f6 --- a/yolov5/train.py +++ b/yolov5/train.py @@ -23,7 +23,7 @@ import subprocess import sys import time from copy import deepcopy -from datetime import datetime +from datetime import datetime, date from pathlib import Path import numpy as np @@ -468,6 +468,8 @@ def parse_opt(known=False): parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') + parser.add_argument('--sr', type=str, default='', help='If sound, give the samplerate of the spectrogram') + parser.add_argument('--duration', type=str, default='', help='If sound, give the duration of the spectrogram') # Logger arguments parser.add_argument('--entity', default=None, help='Entity') @@ -509,7 +511,12 @@ def main(opt, callbacks=Callbacks()): opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume if opt.name == 'cfg': opt.name = Path(opt.cfg).stem # use model.yaml as name - opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) + project_name = input('Please enter the name of your project : ') + date_now = date.today().strftime("%Y%m%d") + folder_name = '_'.join([project_name, date_now, opt.weights[-4:-3], str(opt.imgsz), opt.optimizer, opt.sr, opt.duration, 'YOLOV5',]) + print(f'Your train results will be saved in {folder_name}') + + opt.save_dir = str(increment_path(Path(opt.project) / folder_name, exist_ok=opt.exist_ok)) # DDP mode device = select_device(opt.device, batch_size=opt.batch_size) diff --git a/yolov5/utils/__init__.py b/yolov5/utils/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/activations.py b/yolov5/utils/activations.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/augmentations.py b/yolov5/utils/augmentations.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/autoanchor.py b/yolov5/utils/autoanchor.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/autobatch.py b/yolov5/utils/autobatch.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/aws/__init__.py b/yolov5/utils/aws/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/aws/mime.sh b/yolov5/utils/aws/mime.sh old mode 100644 new mode 100755 diff --git a/yolov5/utils/aws/resume.py b/yolov5/utils/aws/resume.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/aws/userdata.sh b/yolov5/utils/aws/userdata.sh old mode 100644 new mode 100755 diff --git a/yolov5/utils/callbacks.py b/yolov5/utils/callbacks.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/dataloaders.py b/yolov5/utils/dataloaders.py old mode 100644 new mode 100755 index 83f0ae2..e0b047d --- a/yolov5/utils/dataloaders.py +++ b/yolov5/utils/dataloaders.py @@ -238,8 +238,8 @@ class LoadScreenshots: return str(self.screen), im, im0, None, s # screen, img, original img, im0s, s class LoadSpectros: - def __init__(self, folder, sampleDur, sr, img_size, stride=32, auto=True): - self.folder, self.sampleDur, self.sr, self.img_size, self.stride, self.auto = folder, sampleDur, sr, img_size, stride, auto + def __init__(self, folder, sampleDur, rf, window, hop, img_size, stride=32, auto=True): + self.folder, self.sampleDur, self.rf, self.window, self.hop, self.img_size, self.stride, self.auto = folder, sampleDur, rf, window, hop, img_size, stride, auto self.files = os.listdir(folder) self.mode = 'image' self.samples = [] @@ -249,7 +249,7 @@ class LoadSpectros: duration, fs = info.duration, info.samplerate self.samples.extend([{'fn':fn, 'offset':offset, 'fs':fs} for offset in np.arange(0, duration+.01 - sampleDur, sampleDur)]) except: - print('failed with '+fn) + print(f'failed with {fn}') continue self.nf = len(self.samples) @@ -268,12 +268,13 @@ class LoadSpectros: self.count += 1 sig, fs = sf.read(os.path.join(self.folder, row['fn']), start=int(row['offset']*row['fs']), stop=int((row['offset']+self.sampleDur)*row['fs']), always_2d=True) sig = sig[:,0] - if fs != self.sr: - sig = signal.resample(sig, int(len(sig)*self.sr/fs)) + if fs != self.rf: + sig = signal.resample(sig, int(len(sig)*self.rf/fs)) fig = plt.figure() - f, t, stft = signal.spectrogram(sig, nperseg=1024, noverlap=512) + stft = librosa.stft(sig, n_fft=self.window, + hop_length=self.hop, window='hann') # Compute the STFT stft = np.log10(np.abs(stft)) - axim = plt.imshow(stft, aspect = "auto", interpolation = None, cmap = 'jet', vmin=np.mean(stft)) + axim = plt.imshow(stft, aspect = "auto", interpolation = None, cmap = 'viridis', vmin=np.mean(stft)) plt.subplots_adjust(top=1, bottom=0, left=0, right=1) im0 = axim.make_image(fig.canvas)[0][:,:,:-1][:,:,::-1] cv2.imwrite(path, im0) diff --git a/yolov5/utils/docker/Dockerfile b/yolov5/utils/docker/Dockerfile old mode 100644 new mode 100755 diff --git a/yolov5/utils/docker/Dockerfile-arm64 b/yolov5/utils/docker/Dockerfile-arm64 old mode 100644 new mode 100755 diff --git a/yolov5/utils/docker/Dockerfile-cpu b/yolov5/utils/docker/Dockerfile-cpu old mode 100644 new mode 100755 diff --git a/yolov5/utils/downloads.py b/yolov5/utils/downloads.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/flask_rest_api/README.md b/yolov5/utils/flask_rest_api/README.md old mode 100644 new mode 100755 diff --git a/yolov5/utils/flask_rest_api/example_request.py b/yolov5/utils/flask_rest_api/example_request.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/flask_rest_api/restapi.py b/yolov5/utils/flask_rest_api/restapi.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/general.py b/yolov5/utils/general.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/google_app_engine/Dockerfile b/yolov5/utils/google_app_engine/Dockerfile old mode 100644 new mode 100755 diff --git a/yolov5/utils/google_app_engine/additional_requirements.txt b/yolov5/utils/google_app_engine/additional_requirements.txt old mode 100644 new mode 100755 diff --git a/yolov5/utils/google_app_engine/app.yaml b/yolov5/utils/google_app_engine/app.yaml old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/__init__.py b/yolov5/utils/loggers/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/clearml/.gitkeep b/yolov5/utils/loggers/clearml/.gitkeep old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/clearml/README.md b/yolov5/utils/loggers/clearml/README.md old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/clearml/__init__.py b/yolov5/utils/loggers/clearml/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/clearml/clearml_utils.py b/yolov5/utils/loggers/clearml/clearml_utils.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/clearml/hpo.py b/yolov5/utils/loggers/clearml/hpo.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/comet/.gitkeep b/yolov5/utils/loggers/comet/.gitkeep old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/comet/README.md b/yolov5/utils/loggers/comet/README.md old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/comet/__init__.py b/yolov5/utils/loggers/comet/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/comet/comet_utils.py b/yolov5/utils/loggers/comet/comet_utils.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/comet/hpo.py b/yolov5/utils/loggers/comet/hpo.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/comet/optimizer_config.json b/yolov5/utils/loggers/comet/optimizer_config.json old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/wandb/__init__.py b/yolov5/utils/loggers/wandb/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loggers/wandb/wandb_utils.py b/yolov5/utils/loggers/wandb/wandb_utils.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/loss.py b/yolov5/utils/loss.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/metrics.py b/yolov5/utils/metrics.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/plots.py b/yolov5/utils/plots.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/__init__.py b/yolov5/utils/segment/__init__.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/augmentations.py b/yolov5/utils/segment/augmentations.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/dataloaders.py b/yolov5/utils/segment/dataloaders.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/general.py b/yolov5/utils/segment/general.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/loss.py b/yolov5/utils/segment/loss.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/metrics.py b/yolov5/utils/segment/metrics.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/segment/plots.py b/yolov5/utils/segment/plots.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/torch_utils.py b/yolov5/utils/torch_utils.py old mode 100644 new mode 100755 diff --git a/yolov5/utils/triton.py b/yolov5/utils/triton.py old mode 100644 new mode 100755 diff --git a/yolov5/val.py b/yolov5/val.py old mode 100644 new mode 100755 -- GitLab