Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions data_exporters/label_studio_exporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,19 @@ NOTE: Save the project_id from the URL of the project
If keeping the data local on the instance, try to keep the file structure the same as is the audio file from your ML machine. For example, if some dataset is located at `mnt/datasets/audio_dataset_cool/AB/1/audio.wav` then you may want to make the path on label studio something like `label_studio_path/audio_dataset_cool/AB/1/audio.wav` for the easiest intergrations. Otherwise some minor file changes will be needed.

3) Run the script to apply annotations, see demo.py in this folder


# Importing BirdNET annotations into Label Studio

*Assumes BirdNET analyzer has been run over data, and there exists an input directory of wavs and an output file with concatinated results.

create python env
pip install whoot
whoot labelstudio_import
nano yaml
edit yaml with labelstudio project data, links, paths to the audio/birdnet labels
whoot run_import (it will automatically use the yaml)




51 changes: 51 additions & 0 deletions data_exporters/label_studio_exporter/expand_birdnet_detections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Expand birdnet detections into longer clips for validation.

This script takes in t combined table of birdnet detections for a given
group of wav files. It checks for overlap, and then creates an expanded clip
in order to provide acoustic context while validating the birdnet detections.
It also outputs a pkl file containing a dictionary of audio info including
the offset and duration of the label, as well as the label for each clip.

"""
import argparse
from whoot import check_overlap_dict
import pandas as pd
import pickle


if __name__ == "__main__":
PARSER = argparse.ArgumentParser(
description='Input config path'
)
PARSER.add_argument('-results', type=str,
help='Path to Birdnet concatenated results file.')
PARSER.add_argument('-output', type=str,
help='Path to output dir for metadata and segments.')
ARGS = PARSER.parse_args()
results = pd.read_csv(ARGS.results)
out_dir = ARGS.output

results['File'] = results['File'].str.replace('Volumes/BUOW', 'mnt/restorage')

all_data = {
"audio": [],
"labels": [],
}

metadata = []

# creates shortened segments that combine overlaps into 1, provides results in dataframe
for file_path, detections in results.groupby("File"):
metadata_dict, metadata_list = check_overlap_dict(file_path, detections, out_dir)

all_data["audio"].extend(metadata_dict["audio"])
all_data["labels"].extend(metadata_dict["labels"])

metadata.extend(metadata_list)

dataframe = pd.DataFrame(metadata)

with open(f"{out_dir}/output.pkl", "wb") as file:
pickle.dump(all_data, file)

dataframe.to_csv(f"{out_dir}/metadata.csv", index=False)
10 changes: 10 additions & 0 deletions labelstudio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ are uploaded to labelstudio with the intention to validate these
predictions. The labeler will mark whether a prediction is
correct or incorrect, and if incorrect, will be shown options to select
if it is a different vocalization type, something else, or they're not sure.


### buow_acoustic_labeling.xml

This template is used to strongly label acoustic events of interest for
burrowing owl calls. It was created to validate birdnet detections, so
segments were thought to have a burrowing owl in them already. But this template
could also be used to label calls for the first time as well, without having been
preprocessed with birdnet. It automatically loads the spectrogram and waveform in
large sizes.
16 changes: 16 additions & 0 deletions labelstudio/buow_acoustic_labeling.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<View>
<Audio name="audio" value="$audio" spectrogram="true" height="300" decode="true"/>
<Labels name="label" toName="audio">
<Label value="twitter" />
<Label value="rasp" />
<Label value="cluck" />
<Label value="coocoo" />
<Label value="eep" />
<Label value="alarm" />
<Label value="rattle" />
</Labels>
<Choices name="correct" toName="audio"
choice="single">
<Choice value="No BUOW" />
</Choices>
</View>
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ data-exporters = [
cu128 = "https://download.pytorch.org/whl/cu128"

[tool.setuptools]
packages = ["make_model", "assess_birdnet", "whoot_model_training"]
packages = ["make_model", "assess_birdnet", "whoot_model_training", "whoot"]

[tool.uv.sources]
pyha-analyzer = { git = "https://github.com/UCSD-E4E/pyha-analyzer-2.0.git", branch = "support_whoot" }
Expand Down
2 changes: 1 addition & 1 deletion whoot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = "0.1.1.dev0"

from .audio_utils import expand_window
from .audio_utils import expand_window, check_overlap_dict
97 changes: 97 additions & 0 deletions whoot/audio_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""
import random
from pydub import AudioSegment
from pathlib import Path
import pandas as pd


def expand_window(audio, start_time, end_time, length=3000, randomize=False):
Expand Down Expand Up @@ -64,3 +66,98 @@ def expand_window(audio, start_time, end_time, length=3000, randomize=False):
start_offset = start_time - new_start
return audio[new_start:clip_length], start_offset
return audio[int(expanded_start):int(expanded_end)], half_diff


def check_overlap_dict(file_path, detections, output_dir):
"""Check for overlap with other detections before expanding window
and create a dictionary with the audio, the new path, the duration and
offset of the detection within the newly expanded window.

Args:
file_path:
detections:
output_dir:

Returns:
dict: A dictionary containing the clip path, offset/duration and label.
"""

audio = AudioSegment.from_wav(file_path)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)

detections = detections.sort_values("Start (s)")

groups = []

for _, row in detections.iterrows():
start = int(row["Start (s)"] * 1000)
end = int(row["End (s)"] * 1000)

detection = {
"start": start,
"end": end,
"label": row["Common name"],
}


if groups and start - 3500 <= groups[-1]["end"] + 3500:
groups[-1]["end"] = max(groups[-1]["end"], end)
groups[-1]["detections"].append(detection)
else:
groups.append({
"start": start,
"end": end,
"detections": [detection],
})

metadata_dict = {
"audio": [],
"labels": [],
}

dataframe_list = []

for i, group in enumerate(groups):
group_start = group["start"]
group_end = group["end"]
length = (group_end - group_start) + 7000

clip, group_offset = expand_window(
audio,
group_start,
group_end,
length,
randomize=False,
)

segment_name = f"{Path(file_path).stem}_{i}.wav"
output_path = output_dir / segment_name
clip.export(output_path, format="wav")
dataframe_dict = {
"ls_filename": str(segment_name),
"original_file_path": str(file_path),
"offset": group_start,
"duration": length
}
dataframe_list.append(dataframe_dict)

for detection in group["detections"]:
detection_offset = (
group_offset
+ detection["start"]
- group_start
)

metadata_dict["audio"].append({
"bytes": None,
"path": str(output_path),
"offset": detection_offset / 1000,
"duration": (
detection["end"] - detection["start"]
) / 1000,
})

metadata_dict["labels"].append(detection["label"])

return metadata_dict, dataframe_list