Skip to content
Merged
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
6 changes: 1 addition & 5 deletions birdnet_analyzer/gui/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,7 @@ def run_analysis(
lat = lat if species_list_choice == gu._PREDICT_SPECIES else None # ty:ignore[invalid-assignment]
lon = lon if species_list_choice == gu._PREDICT_SPECIES else None # ty:ignore[invalid-assignment]
week = None if use_yearlong else week # ty:ignore[invalid-assignment]
audio_speed = (
max(0.1, 1.0 / (audio_speed * -1))
if audio_speed < 0
else max(1.0, float(audio_speed))
)
audio_speed = gu.slider_to_value(audio_speed)
on_update = partial(on_progress, progress=progress) if callable(progress) else None

if selected_model == gu._CUSTOM_CLASSIFIER and custom_classifier_file is None:
Expand Down
50 changes: 40 additions & 10 deletions birdnet_analyzer/gui/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ class ProcessorState(typing.NamedTuple):
prediction_dir: str


def _parse_recording_duration(text: str | None) -> float | None:
text = (text or "").strip()

return float(text) if text else None


def _require_built_recording_duration(proc_state: ProcessorState, text: str | None):
built_with = proc_state.processor.recording_duration

try:
matches = _parse_recording_duration(text) == built_with
except ValueError:
matches = False

if not matches:
raise gr.Error(loc.localize("eval-tab-error-no-valid-recording-duration"))


def build_evaluation_tab() -> gu.TAB_BUILDER_RESULT:
state = TabState("evaluation")

Expand Down Expand Up @@ -139,11 +157,16 @@ def download_results_table(
f"{loc.localize('eval-tab-error-saving-results-table')} {e}"
) from e

def download_data_table(processor_state: ProcessorState):
def download_data_table(
processor_state: ProcessorState, recording_duration_value: str
):
if processor_state is None:
raise gr.Error(
loc.localize("eval-tab-error-calc-metrics-first"), print_exception=False
)

_require_built_recording_duration(processor_state, recording_duration_value)

try:
file_location = gu.save_file_dialog(
state_key="eval-data-table",
Expand Down Expand Up @@ -205,13 +228,14 @@ def build_processor(
if not annotation_dir or not prediction_dir:
return None

# Refreshed on every keystroke, so a duration the processor would reject is
# left out here; the handlers using the processor refuse the mismatch.
try:
rec_dur = (
float(recording_duration_value.strip())
if recording_duration_value
else None
)
except (ValueError, TypeError):
rec_dur = _parse_recording_duration(recording_duration_value)
except ValueError:
rec_dur = None

if rec_dur is not None and not 0 < (sample_duration_value or 0) <= rec_dur:
rec_dur = None

cols_ann = {
Expand Down Expand Up @@ -680,7 +704,9 @@ def select_directory(current_files, current_dir):
averaging_radio,
],
)
download_data_button.click(fn=download_data_table, inputs=[processor_state])
download_data_button.click(
fn=download_data_table, inputs=[processor_state, recording_duration]
)

def download_plot_guarded(plot, plot_name):
if plot is None:
Expand Down Expand Up @@ -917,6 +943,7 @@ def calculate_metrics(
class_wise_value,
averaging_value,
score_unannotated_value,
recording_duration_value,
selected_classes_list,
selected_recordings_list,
*metrics_checkbox_values,
Expand All @@ -927,6 +954,8 @@ def calculate_metrics(
print_exception=False,
)

_require_built_recording_duration(proc_state, recording_duration_value)

metric_ids = tuple(
metric_id
for value, metric_id in zip(
Expand All @@ -935,7 +964,7 @@ def calculate_metrics(
if value
)
if not metric_ids:
metric_ids = ("precision", "recall", "f1")
raise gr.Error(loc.localize("eval-tab-error-no-metric-selected"))

if not selected_classes_list:
selected_classes_list = list(proc_state.processor.classes)
Expand Down Expand Up @@ -989,6 +1018,7 @@ def calculate_metrics(
class_wise,
averaging_radio,
score_unannotated,
recording_duration,
select_classes_checkboxgroup,
select_recordings_checkboxgroup,
*metrics_checkboxes.values(),
Expand Down Expand Up @@ -1020,7 +1050,7 @@ def recompute_overall(

return gr.update(value=overall_row, column_widths=widths)

averaging_radio.input(
averaging_radio.change(
recompute_overall,
inputs=[pa_state, predictions_state, labels_state, averaging_radio],
outputs=[overall_table],
Expand Down
40 changes: 18 additions & 22 deletions birdnet_analyzer/gui/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))


def _confidence(path: str) -> float | None:
try:
value = float(os.path.basename(path).split("_", 1)[0])
except ValueError:
return None

return value if 0 <= value <= 1 else None


def build_review_tab() -> gu.TAB_BUILDER_RESULT:
state = TabState("review")

Expand Down Expand Up @@ -76,17 +85,13 @@ def create_log_plot(positives, negatives, fig_num=None):
x_vals = []
y_val = []

for fl in positives + negatives:
try:
x_val = float(os.path.basename(fl).split("_", 1)[0])

if 0 > x_val > 1:
continue
for label, files in ((1, positives), (0, negatives)):
for fl in files:
x_val = _confidence(fl)

x_vals.append([x_val])
y_val.append(1 if fl in positives else 0)
except ValueError:
pass
if x_val is not None:
x_vals.append([x_val])
y_val.append(label)

if (len(positives) + len(negatives)) >= 2 and len(set(y_val)) > 1:
log_model = linear_model.LogisticRegression(C=55)
Expand Down Expand Up @@ -348,17 +353,6 @@ def start_review(next_review_state):

return {review_state: next_review_state}

def try_confidence(filename):
try:
val = float(os.path.basename(filename).split("_", 1)[0])

if 0 > val > 1:
return 0

return val
except ValueError:
return 0

def update_review(next_review_state: dict, selected_species: str | None = None):
next_review_state["history"] = []
next_review_state["skipped"] = []
Expand All @@ -381,7 +375,9 @@ def update_review(next_review_state: dict, selected_species: str | None = None):
else next_review_state["input_directory"]
)

todo_files = sorted(todo_files, key=try_confidence, reverse=True)
todo_files = sorted(
todo_files, key=lambda f: _confidence(f) or 0, reverse=True
)

next_review_state |= {
"files": todo_files,
Expand Down
2 changes: 1 addition & 1 deletion birdnet_analyzer/gui/segments.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def on_update(info):
min_conf=min_conf,
max_conf=max_conf,
max_segments=num_seq,
audio_speed=audio_speed,
audio_speed=gu.slider_to_value(audio_speed),
seg_length=seq_length,
threads=threads,
collection_mode=collection_mode,
Expand Down
2 changes: 1 addition & 1 deletion birdnet_analyzer/gui/single_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def download_kaleidoscope(prediction_state):
file_location = gu.save_file_dialog(
state_key="single-file-table",
default_filename=cfg.OUTPUT_KALEIDOSCOPE_FILENAME,
filetypes=("Kaleidoscope (*.txt)",),
filetypes=("Kaleidoscope (*.csv)",),
)

if file_location:
Expand Down
61 changes: 46 additions & 15 deletions birdnet_analyzer/gui/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ def start_training(
classifier_name: Name of the custom classifier.
model_save_mode: Save mode for the model (replace or append).
birdnet_version: The BirdNET version to train on.
cache_mode: Cache mode for training data (load, save, or None).
cache_file: Path to the cache file.
cache_file_name: Name of the cache file.
cache_mode: Cache mode for training data (none, load or save).
cache_file: The cache file to train from in "load" mode, or the directory to
write the cache to in "save" mode.
cache_file_name: Name of the cache file written in "save" mode.
autotune: Whether to use hyperparameter autotuning.
autotune_trials: Number of trials for autotuning.
autotune_folds: Number of folds for autotuning.
Expand Down Expand Up @@ -179,9 +180,15 @@ def start_training(
(0, epochs), desc=loc.localize("progress-build-classifier"), unit="epochs"
)

if cache_mode == "load" and not os.path.isfile(cache_file):
if cache_mode == "load" and not (cache_file and os.path.isfile(cache_file)):
raise gr.Error(loc.localize("validation-no-cache-file-selected"))

if cache_mode == "save" and not (cache_file and os.path.isdir(cache_file)):
raise gr.Error(loc.localize("validation-no-directory-selected"))

if cache_mode == "save" and not (cache_file_name or "").strip():
raise gr.Error(loc.localize("validation-no-valid-cache-file-name"))

if model_save_mode == "append" and "detached" in model_formats:
gr.Warning(loc.localize("training-tab-warning-detached-classifier-append"))

Expand Down Expand Up @@ -239,8 +246,8 @@ def trial_progression(trial):
use_focal_loss=focal_loss,
focal_loss_gamma=max(0.0, float(focal_loss_gamma)),
focal_loss_alpha=max(0.0, min(1.0, float(focal_loss_alpha))),
fmin=max(0, min(15000, int(fmin))),
fmax=max(0, min(15000, int(fmax))),
fmin=int(fmin),
fmax=int(fmax),
model_save_mode=model_save_mode,
birdnet=birdnet_version,
save_cache_to=os.path.join(cache_file, cache_file_name)
Expand All @@ -254,9 +261,7 @@ def trial_progression(trial):
on_epoch_end=epoch_progression,
on_trial_result=trial_progression,
on_data_load_end=data_load_progression,
audio_speed=max(0.1, 1.0 / (audio_speed * -1))
if audio_speed < 0
else max(1.0, float(audio_speed)),
audio_speed=gu.slider_to_value(audio_speed),
autotune=autotune,
autotune_trials=int(autotune_trials),
autotune_n_splits=int(autotune_folds),
Expand Down Expand Up @@ -603,9 +608,29 @@ def on_cache_mode_change(value, data_dir, test_data_dir):
gr.update(interactive=value != "load"),
gr.update(interactive=value != "load"),
gr.update(interactive=value != "load"),
gr.update(interactive=value != "load"),
)

def clear_cache_location():
return (
None,
"",
gr.update(visible=False),
"",
gr.update(value=None, visible=False),
)

cache_mode.change(
clear_cache_location,
outputs=[
cache_file_state,
selected_cache_dir_textbox,
cache_file_name,
selected_cache_file_textbox,
cache_file_input,
],
show_progress="hidden",
)

with (
gr.Group(),
gr.Accordion(
Expand Down Expand Up @@ -702,13 +727,20 @@ def on_cache_mode_change(value, data_dir, test_data_dir):
info=loc.localize("training-tab-threads-number-info"),
)

def on_crop_select(new_crop_mode):
def on_crop_or_cache_mode_change(new_cache_mode, new_crop_mode):
visible = new_crop_mode in ("segments", "smart")

return gr.update(
visible=new_crop_mode in ["segments", "smart"],
interactive=new_crop_mode in ["segments", "smart"],
visible=visible, interactive=visible and new_cache_mode != "load"
)

crop_mode.change(on_crop_select, inputs=crop_mode, outputs=crop_overlap)
gr.on(
[cache_mode.change, crop_mode.change],
on_crop_or_cache_mode_change,
inputs=[cache_mode, crop_mode],
outputs=crop_overlap,
show_progress="hidden",
)

cache_mode.change(
on_cache_mode_change,
Expand All @@ -724,7 +756,6 @@ def on_crop_select(new_crop_mode):
fmax_number,
audio_speed_slider,
crop_mode,
crop_overlap,
threads_number,
base_model_radio,
],
Expand Down
Loading
Loading