diff --git a/docs/source/conf.py b/docs/source/conf.py index bb834ca..075dac5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,6 +41,8 @@ html_theme = "sphinx_book_theme" html_static_path = ["_static"] html_theme_options = { "home_page_in_toc": True, + "show_navbar_depth": 2, + "show_toc_level": 2, } intersphinx_mapping = { @@ -58,7 +60,7 @@ intersphinx_mapping = { } # -- Options for autodoc ------------------------------------------------------ -autosummary_generate = True +autosummary_generate = False autosummary_imported_members = True autodoc_default_options = { @@ -70,3 +72,7 @@ autodoc_default_options = { "show-inheritance": True, "module-first": True, } + +numpydoc_show_class_members = False +numpydoc_show_inherited_class_members = False +numpydoc_class_members_toctree = False diff --git a/docs/source/getting_started.md b/docs/source/getting_started.md index 077a6c8..a197522 100644 --- a/docs/source/getting_started.md +++ b/docs/source/getting_started.md @@ -80,4 +80,4 @@ pip install batdetect2 - Run your first detection workflow: {doc}`tutorials/run-inference-on-folder` - For practical task recipes, go to {doc}`how_to/index` -- For command and option details, go to {doc}`reference/cli` +- For command and option details, go to {doc}`reference/cli/index` diff --git a/docs/source/how_to/run-batch-predictions.md b/docs/source/how_to/run-batch-predictions.md index 7b95080..4af7826 100644 --- a/docs/source/how_to/run-batch-predictions.md +++ b/docs/source/how_to/run-batch-predictions.md @@ -27,4 +27,4 @@ batdetect2 predict file_list \ - `--workers` to set data-loading parallelism. - `--format` to select output format. -For complete option details, see {doc}`../reference/cli`. +For complete option details, see {doc}`../reference/cli/index`. diff --git a/docs/source/reference/cli.rst b/docs/source/reference/cli.rst deleted file mode 100644 index 3e60369..0000000 --- a/docs/source/reference/cli.rst +++ /dev/null @@ -1,7 +0,0 @@ -CLI reference -============= - -.. click:: batdetect2.cli:cli - :prog: batdetect2 - :nested: full - diff --git a/docs/source/reference/cli/base.rst b/docs/source/reference/cli/base.rst new file mode 100644 index 0000000..d270aa3 --- /dev/null +++ b/docs/source/reference/cli/base.rst @@ -0,0 +1,8 @@ +Base command +============ + +The options on this page apply to all subcommands. + +.. click:: batdetect2.cli:cli + :prog: batdetect2 + :nested: none diff --git a/docs/source/reference/cli/data.rst b/docs/source/reference/cli/data.rst new file mode 100644 index 0000000..506f254 --- /dev/null +++ b/docs/source/reference/cli/data.rst @@ -0,0 +1,8 @@ +Data command +============ + +Inspect and convert dataset config files. + +.. click:: batdetect2.cli.data:data + :prog: batdetect2 data + :nested: full diff --git a/docs/source/reference/cli/detect_legacy.rst b/docs/source/reference/cli/detect_legacy.rst new file mode 100644 index 0000000..e1a4002 --- /dev/null +++ b/docs/source/reference/cli/detect_legacy.rst @@ -0,0 +1,17 @@ +Legacy detect command +===================== + +.. warning:: + + ``batdetect2 detect`` is a legacy compatibility command. + Prefer ``batdetect2 predict directory`` for new workflows. + +Migration at a glance +--------------------- + +- Legacy: ``batdetect2 detect AUDIO_DIR ANN_DIR DETECTION_THRESHOLD`` +- Current: ``batdetect2 predict directory MODEL_PATH AUDIO_DIR OUTPUT_PATH`` + +.. click:: batdetect2.cli.compat:detect + :prog: batdetect2 detect + :nested: none diff --git a/docs/source/reference/cli/evaluate.rst b/docs/source/reference/cli/evaluate.rst new file mode 100644 index 0000000..0ac4a73 --- /dev/null +++ b/docs/source/reference/cli/evaluate.rst @@ -0,0 +1,8 @@ +Evaluate command +================ + +Evaluate a checkpoint against a configured test dataset. + +.. click:: batdetect2.cli.evaluate:evaluate_command + :prog: batdetect2 evaluate + :nested: none diff --git a/docs/source/reference/cli/index.md b/docs/source/reference/cli/index.md new file mode 100644 index 0000000..ac738f0 --- /dev/null +++ b/docs/source/reference/cli/index.md @@ -0,0 +1,18 @@ +# CLI reference + +Use this section like this: + +1. Check `base` for global options shared by all commands. +2. Pick the command page you need (`predict`, `data`, `train`, or `evaluate`). +3. Use `detect_legacy` only if you are maintaining old workflows. + +```{toctree} +:maxdepth: 1 + +base +predict +data +train +evaluate +detect_legacy +``` diff --git a/docs/source/reference/cli/predict.rst b/docs/source/reference/cli/predict.rst new file mode 100644 index 0000000..7ff6359 --- /dev/null +++ b/docs/source/reference/cli/predict.rst @@ -0,0 +1,8 @@ +Predict command +=============== + +Run model inference from a directory, a file list, or a dataset. + +.. click:: batdetect2.cli.inference:predict + :prog: batdetect2 predict + :nested: full diff --git a/docs/source/reference/cli/train.rst b/docs/source/reference/cli/train.rst new file mode 100644 index 0000000..6c416cb --- /dev/null +++ b/docs/source/reference/cli/train.rst @@ -0,0 +1,8 @@ +Train command +============= + +Train a model from dataset configs or fine-tune from a checkpoint. + +.. click:: batdetect2.cli.train:train_command + :prog: batdetect2 train + :nested: none diff --git a/docs/source/reference/configs.md b/docs/source/reference/configs.md deleted file mode 100644 index 476850d..0000000 --- a/docs/source/reference/configs.md +++ /dev/null @@ -1,7 +0,0 @@ -# Config Reference - -```{eval-rst} -.. automodule:: batdetect2.config - :members: - :inherited-members: pydantic.BaseModel -``` diff --git a/docs/source/reference/configs.rst b/docs/source/reference/configs.rst new file mode 100644 index 0000000..733159a --- /dev/null +++ b/docs/source/reference/configs.rst @@ -0,0 +1,5 @@ +Config reference +================ + +.. automodule:: batdetect2.config + :members: diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md index 9bd30f4..6b7fe8f 100644 --- a/docs/source/reference/index.md +++ b/docs/source/reference/index.md @@ -6,7 +6,7 @@ configuration, and data structures. ```{toctree} :maxdepth: 1 -cli +cli/index configs targets ``` diff --git a/docs/source/reference/targets.md b/docs/source/reference/targets.md deleted file mode 100644 index b6607a0..0000000 --- a/docs/source/reference/targets.md +++ /dev/null @@ -1,6 +0,0 @@ -# Targets Reference - -```{eval-rst} -.. automodule:: batdetect2.targets - :members: -``` diff --git a/docs/source/reference/targets.rst b/docs/source/reference/targets.rst new file mode 100644 index 0000000..eeb1bae --- /dev/null +++ b/docs/source/reference/targets.rst @@ -0,0 +1,5 @@ +Targets reference +================= + +.. automodule:: batdetect2.targets + :members: diff --git a/docs/source/targets/rois.md b/docs/source/targets/rois.md index bbf406e..3b3e292 100644 --- a/docs/source/targets/rois.md +++ b/docs/source/targets/rois.md @@ -1,6 +1,6 @@ -## Defining Target Geometry: Mapping Sound Event Regions +# Defining Target Geometry: Mapping Sound Event Regions -### Introduction +## Introduction In the previous steps of defining targets, we focused on determining _which_ sound events are relevant (`filtering`), _what_ descriptive tags they should have (`transform`), and _which category_ they belong to (`classes`). However, for the model to learn effectively, it also needs to know **where** in the spectrogram each sound event is located and approximately **how large** it is. @@ -10,7 +10,7 @@ This ROI contains detailed spatial information (start/end time, low/high frequen This section explains how BatDetect2 converts the geometric ROI from your annotations into the specific positional and size information used as targets during model training. -### From ROI to Model Targets: Position & Size +## From ROI to Model Targets: Position & Size BatDetect2 does not directly predict a full bounding box. Instead, it is trained to predict: @@ -21,7 +21,7 @@ Instead, it is trained to predict: This step defines _how_ BatDetect2 calculates this specific reference point and these numerical size values from the original annotation's bounding box. It also handles the reverse process – converting predicted positions and sizes back into bounding boxes for visualization or analysis. -### Configuring the ROI Mapping +## Configuring the ROI Mapping You can control how this conversion happens through settings in your configuration file (e.g., your main `.yaml` file). These settings are usually placed within the main `targets:` configuration block, under a specific `roi:` key. @@ -74,12 +74,12 @@ targets: # Top-level key for target definition frequency_scale: 0.00116 # e.g., Model predicts height relative to ~860Hz (or other model-specific scaling) ``` -### Decoding Size Predictions +## Decoding Size Predictions These scaling factors (`time_scale`, `frequency_scale`) are also essential for interpreting the model's output correctly. When the model predicts numerical values for width and height, BatDetect2 uses these same scales (in reverse) to convert those numbers back into physically meaningful durations (seconds) and bandwidths (Hz/kHz) when reconstructing bounding boxes from predictions. -### Outcome +## Outcome By configuring the `roi` settings, you ensure that BatDetect2 consistently translates the geometric information from your annotations into the specific reference points and scaled size values required for training the model. Using consistent scales that are appropriate for your data and potentially beneficial for training stability allows the model to effectively learn not just _what_ sound is present, but also _where_ it is located and _how large_ it is, and enables meaningful interpretation of the model's spatial and size predictions. diff --git a/docs/source/targets/use.md b/docs/source/targets/use.md index a242363..d413b63 100644 --- a/docs/source/targets/use.md +++ b/docs/source/targets/use.md @@ -1,6 +1,6 @@ -## Bringing It All Together: The `Targets` Object +# Bringing It All Together: The `Targets` Object -### Recap: Defining Your Target Strategy +## Recap: Defining Your Target Strategy In the previous sections, we covered the sequential steps to precisely define what your BatDetect2 model should learn, specified within your configuration file: @@ -12,7 +12,7 @@ In the previous sections, we covered the sequential steps to precisely define wh You define all these aspects within your configuration file (e.g., YAML), which holds the complete specification for your target definition strategy, typically under a main `targets:` key. -### What is the `Targets` Object? +## What is the `Targets` Object? While the configuration file specifies _what_ you want to happen, BatDetect2 needs an active component to actually _perform_ these steps. This is the role of the `Targets` object. @@ -21,7 +21,7 @@ The `Targets` is an organized container that holds all the specific functions an It's created directly from your configuration and provides methods to apply the **filtering**, **transformation**, **ROI mapping** (geometry to position/size and back), **class encoding**, and **class decoding** steps you defined. It effectively bundles together all the target definition logic determined by your settings into a single, usable object. -### How is it Created and Used? +## How is it Created and Used? For most standard training workflows, you typically won't need to create or interact with the `Targets` object directly in Python code. BatDetect2 usually handles its creation automatically when you provide your main configuration file during training setup. @@ -50,7 +50,7 @@ targets_processor: TargetProtocol = load_targets(target_config_file) # to be used internally by the training pipeline or for prediction processing. ``` -### What Does the `Targets` Object Do? (Its Role) +## What Does the `Targets` Object Do? (Its Role) Once created, the `targets_processor` object plays several vital roles within the BatDetect2 system: @@ -71,7 +71,7 @@ Once created, the `targets_processor` object plays several vital roles within th - `targets_processor.generic_class_tags`: The tags representing the generic class. - `targets_processor.dimension_names`: The names used for the size dimensions (e.g., `['width', 'height']`). -### Why is Understanding This Important? +## Why is Understanding This Important? As a researcher using BatDetect2, your primary interaction is typically through the **configuration file**. The `Targets` object is the component that materializes your configurations. @@ -84,7 +84,7 @@ Understanding its role can be important: While standard training runs handle this object internally, the underlying functions for filtering, transforming, encoding, decoding, and ROI mapping are accessible or can be built individually. This modular design provides the **flexibility to use or customize specific parts of the target definition workflow programmatically** for advanced analyses, integration tasks, or specialized data processing pipelines, should you need to go beyond the standard configuration-driven approach. -### Summary +## Summary The `Targets` object encapsulates the entire configured target definition logic specified in your `TargetConfig` file. It acts as the central component within BatDetect2 for applying filtering, tag transformation, ROI mapping (geometry to/from position/size), class encoding (for training preparation), and class/ROI decoding (for interpreting predictions). diff --git a/docs/source/tutorials/run-inference-on-folder.md b/docs/source/tutorials/run-inference-on-folder.md index 838626c..5820394 100644 --- a/docs/source/tutorials/run-inference-on-folder.md +++ b/docs/source/tutorials/run-inference-on-folder.md @@ -25,7 +25,7 @@ batdetect2 predict directory \ ## What to do next - Use {doc}`../how_to/tune-detection-threshold` to tune sensitivity. -- Use {doc}`../reference/cli` for full command options. +- Use {doc}`../reference/cli/index` for full command options. Note: this is the initial Phase 1 scaffold and will be expanded with a full, validated end-to-end walkthrough. diff --git a/src/batdetect2/cli/base.py b/src/batdetect2/cli/base.py index d0fd5fc..7209dd7 100644 --- a/src/batdetect2/cli/base.py +++ b/src/batdetect2/cli/base.py @@ -27,7 +27,11 @@ BatDetect2 - Detection and Classification help="Increase verbosity. -v for INFO, -vv for DEBUG.", ) def cli(verbose: int = 0): - """BatDetect2 - Bat Call Detection and Classification.""" + """Run the BatDetect2 CLI. + + This command initializes logging and exposes subcommands for prediction, + training, evaluation, and dataset utilities. + """ click.echo(INFO_STR) enable_logging(verbose) diff --git a/src/batdetect2/cli/compat.py b/src/batdetect2/cli/compat.py index 856be3a..c8934fe 100644 --- a/src/batdetect2/cli/compat.py +++ b/src/batdetect2/cli/compat.py @@ -12,7 +12,13 @@ DEFAULT_MODEL_PATH = os.path.join( ) -@cli.command() +@cli.command( + short_help="Legacy detection command.", + epilog=( + "Deprecated workflow. Prefer `batdetect2 predict directory` for " + "new analyses." + ), +) @click.argument( "audio_dir", type=click.Path(exists=True), @@ -68,7 +74,10 @@ def detect( time_expansion_factor: int, **args, ): - """Detect bat calls in files in AUDIO_DIR and save predictions to ANN_DIR. + """Legacy detection command for directory-based inference. + + Detect bat calls in files in `AUDIO_DIR` and save predictions to + `ANN_DIR`. DETECTION_THRESHOLD is the detection threshold. All predictions with a score below this threshold will be discarded. Values between 0 and 1. @@ -78,6 +87,11 @@ def detect( Spaces in the input paths will throw an error. Wrap in quotes. Input files should be short in duration e.g. < 30 seconds. + + Note + ---- + This command is kept for backwards compatibility. Prefer + `batdetect2 predict directory` for new workflows. """ from batdetect2 import api from batdetect2.utils.detector_utils import save_results_to_file @@ -132,7 +146,7 @@ def detect( def print_config(config): - """Print the processing configuration.""" + """Print the processing configuration values.""" click.echo("\nProcessing Configuration:") click.echo(f"Time Expansion Factor: {config.get('time_expansion')}") click.echo(f"Detection Threshold: {config.get('detection_threshold')}") diff --git a/src/batdetect2/cli/data.py b/src/batdetect2/cli/data.py index 31dd7b3..a772cb5 100644 --- a/src/batdetect2/cli/data.py +++ b/src/batdetect2/cli/data.py @@ -7,12 +7,12 @@ from batdetect2.cli.base import cli __all__ = ["data"] -@cli.group() +@cli.group(short_help="Inspect and convert datasets.") def data(): """Inspect and convert dataset configuration files.""" -@data.command() +@data.command(short_help="Print dataset summary information.") @click.argument( "dataset_config", type=click.Path(exists=True), @@ -20,17 +20,27 @@ def data(): @click.option( "--field", type=str, - help="If the dataset info is in a nested field please specify here.", + help=( + "Nested field name that contains dataset configuration. " + "Use this when the config is wrapped in a larger file." + ), ) @click.option( "--targets", "targets_path", type=click.Path(exists=True), + help=( + "Path to targets config file. If provided, a per-class summary " + "table is printed." + ), ) @click.option( "--base-dir", type=click.Path(exists=True), - help="The base directory to which all recording and annotations paths are relative to.", + help=( + "Base directory used to resolve relative recording and annotation " + "paths in the dataset config." + ), ) def summary( dataset_config: Path, @@ -38,7 +48,11 @@ def summary( targets_path: Path | None = None, base_dir: Path | None = None, ): - """Show annotation counts and optional class summary.""" + """Show dataset size and optional class summary. + + Prints the number of annotated clips. If `--targets` is provided, it also + prints a per-class summary table based on the configured targets. + """ from batdetect2.data import compute_class_summary, load_dataset_from_config from batdetect2.targets import load_targets @@ -62,7 +76,7 @@ def summary( print(summary.to_markdown()) -@data.command() +@data.command(short_help="Convert dataset config to annotation set.") @click.argument( "dataset_config", type=click.Path(exists=True), @@ -70,7 +84,10 @@ def summary( @click.option( "--field", type=str, - help="If the dataset info is in a nested field please specify here.", + help=( + "Nested field name that contains dataset configuration. " + "Use this when the config is wrapped in a larger file." + ), ) @click.option( "--output", @@ -80,12 +97,18 @@ def summary( @click.option( "--base-dir", type=click.Path(exists=True), - help="The base directory to which all recording and annotations paths are relative to.", + help=( + "Base directory used to resolve relative recording and annotation " + "paths in the dataset config." + ), ) @click.option( "--audio-dir", type=click.Path(exists=True), - help="The directory containing the audio files. All paths will be relative to this directory.", + help=( + "Directory containing audio files. Output annotation paths are " + "made relative to this directory." + ), ) def convert( dataset_config: Path, @@ -94,7 +117,11 @@ def convert( base_dir: Path | None = None, audio_dir: Path | None = None, ): - """Convert a dataset config file to soundevent format.""" + """Convert a dataset config into soundevent annotation-set format. + + Writes a single annotation-set file that can be used by downstream tools. + Use `--audio-dir` to control relative audio path handling in the output. + """ from soundevent import data, io from batdetect2.data import load_dataset, load_dataset_config diff --git a/src/batdetect2/cli/evaluate.py b/src/batdetect2/cli/evaluate.py index c1dbd1f..5acffdf 100644 --- a/src/batdetect2/cli/evaluate.py +++ b/src/batdetect2/cli/evaluate.py @@ -11,20 +11,73 @@ __all__ = ["evaluate_command"] DEFAULT_OUTPUT_DIR = Path("outputs") / "evaluation" -@cli.command(name="evaluate") +@cli.command(name="evaluate", short_help="Evaluate a model checkpoint.") @click.argument("model_path", type=click.Path(exists=True)) @click.argument("test_dataset", type=click.Path(exists=True)) -@click.option("--targets", "targets_config", type=click.Path(exists=True)) -@click.option("--audio-config", type=click.Path(exists=True)) -@click.option("--evaluation-config", type=click.Path(exists=True)) -@click.option("--inference-config", type=click.Path(exists=True)) -@click.option("--outputs-config", type=click.Path(exists=True)) -@click.option("--logging-config", type=click.Path(exists=True)) -@click.option("--base-dir", type=click.Path(), default=Path.cwd()) -@click.option("--output-dir", type=click.Path(), default=DEFAULT_OUTPUT_DIR) -@click.option("--experiment-name", type=str) -@click.option("--run-name", type=str) -@click.option("--workers", "num_workers", type=int) +@click.option( + "--targets", + "targets_config", + type=click.Path(exists=True), + help="Path to targets config file.", +) +@click.option( + "--audio-config", + type=click.Path(exists=True), + help="Path to audio config file.", +) +@click.option( + "--evaluation-config", + type=click.Path(exists=True), + help="Path to evaluation config file.", +) +@click.option( + "--inference-config", + type=click.Path(exists=True), + help="Path to inference config file.", +) +@click.option( + "--outputs-config", + type=click.Path(exists=True), + help="Path to outputs config file.", +) +@click.option( + "--logging-config", + type=click.Path(exists=True), + help="Path to logging config file.", +) +@click.option( + "--base-dir", + type=click.Path(), + default=Path.cwd(), + show_default=True, + help=( + "Base directory used to resolve relative paths in the dataset " + "configuration." + ), +) +@click.option( + "--output-dir", + type=click.Path(), + default=DEFAULT_OUTPUT_DIR, + show_default=True, + help="Directory where evaluation outputs are written.", +) +@click.option( + "--experiment-name", + type=str, + help="Experiment name used for logging backends.", +) +@click.option( + "--run-name", + type=str, + help="Run name used for logging backends.", +) +@click.option( + "--workers", + "num_workers", + type=int, + help="Number of worker processes for dataset loading.", +) def evaluate_command( model_path: Path, test_dataset: Path, @@ -40,7 +93,11 @@ def evaluate_command( experiment_name: str | None = None, run_name: str | None = None, ): - """Evaluate a checkpoint against a configured test dataset.""" + """Evaluate a checkpoint against a test dataset. + + Loads model and optional override configs, runs evaluation on + `test_dataset`, and writes metrics/artifacts to `output_dir`. + """ from batdetect2.api_v2 import BatDetect2API from batdetect2.audio import AudioConfig from batdetect2.data import load_dataset_from_config diff --git a/src/batdetect2/cli/inference.py b/src/batdetect2/cli/inference.py index 26a770d..dac9ab5 100644 --- a/src/batdetect2/cli/inference.py +++ b/src/batdetect2/cli/inference.py @@ -1,4 +1,6 @@ +from functools import wraps from pathlib import Path +from typing import TYPE_CHECKING import click from loguru import logger @@ -7,12 +9,89 @@ from soundevent.audio.files import get_audio_files from batdetect2.cli.base import cli +if TYPE_CHECKING: + from batdetect2.api_v2 import BatDetect2API + from batdetect2.audio import AudioConfig + from batdetect2.inference import InferenceConfig + from batdetect2.outputs import OutputsConfig + __all__ = ["predict"] -@cli.group(name="predict") +@cli.group(name="predict", short_help="Run prediction workflows.") def predict() -> None: - """Run model inference on audio using API v2.""" + """Run model inference on audio files. + + Use one of the subcommands to select inputs from a directory, a text file + list, or an annotation dataset. + """ + + +def common_predict_options(func): + """Attach options shared by all `predict` subcommands.""" + + @click.option( + "--audio-config", + type=click.Path(exists=True), + help=( + "Path to an audio config file. Use this to override audio " + "loading and preprocessing-related settings." + ), + ) + @click.option( + "--inference-config", + type=click.Path(exists=True), + help=( + "Path to an inference config file. Use this to override " + "prediction-time thresholds and behavior." + ), + ) + @click.option( + "--outputs-config", + type=click.Path(exists=True), + help=( + "Path to an outputs config file. Use this to control the " + "prediction fields written to disk." + ), + ) + @click.option( + "--logging-config", + type=click.Path(exists=True), + help=( + "Path to a logging config file. Use this to customize logging " + "format and levels." + ), + ) + @click.option( + "--batch-size", + type=int, + help=( + "Batch size for inference. If omitted, the value from the " + "loaded config is used." + ), + ) + @click.option( + "--workers", + "num_workers", + type=int, + default=0, + show_default=True, + help="Number of worker processes for audio loading.", + ) + @click.option( + "--format", + "format_name", + type=str, + help=( + "Output format name used by the prediction writer. If omitted, " + "the default output format is used." + ), + ) + @wraps(func) + def wrapped(*args, **kwargs): + return func(*args, **kwargs) + + return wrapped def _build_api( @@ -21,7 +100,7 @@ def _build_api( inference_config: Path | None, outputs_config: Path | None, logging_config: Path | None, -): +) -> "tuple[BatDetect2API, AudioConfig | None, InferenceConfig | None, OutputsConfig | None]": from batdetect2.api_v2 import BatDetect2API from batdetect2.audio import AudioConfig from batdetect2.inference import InferenceConfig @@ -103,17 +182,14 @@ def _run_prediction( ) -@predict.command(name="directory") +@predict.command( + name="directory", + short_help="Predict on audio files in a directory.", +) @click.argument("model_path", type=click.Path(exists=True)) @click.argument("audio_dir", type=click.Path(exists=True)) @click.argument("output_path", type=click.Path()) -@click.option("--audio-config", type=click.Path(exists=True)) -@click.option("--inference-config", type=click.Path(exists=True)) -@click.option("--outputs-config", type=click.Path(exists=True)) -@click.option("--logging-config", type=click.Path(exists=True)) -@click.option("--batch-size", type=int) -@click.option("--workers", "num_workers", type=int, default=0) -@click.option("--format", "format_name", type=str) +@common_predict_options def predict_directory_command( model_path: Path, audio_dir: Path, @@ -126,7 +202,11 @@ def predict_directory_command( num_workers: int, format_name: str | None, ) -> None: - """Predict on all audio files in a directory.""" + """Predict on all audio files in a directory. + + Loads a checkpoint, scans `audio_dir` for supported audio files, runs + inference, and saves predictions to `output_path`. + """ audio_files = list(get_audio_files(audio_dir)) _run_prediction( model_path=model_path, @@ -142,17 +222,14 @@ def predict_directory_command( ) -@predict.command(name="file_list") +@predict.command( + name="file_list", + short_help="Predict on paths listed in a text file.", +) @click.argument("model_path", type=click.Path(exists=True)) @click.argument("file_list", type=click.Path(exists=True)) @click.argument("output_path", type=click.Path()) -@click.option("--audio-config", type=click.Path(exists=True)) -@click.option("--inference-config", type=click.Path(exists=True)) -@click.option("--outputs-config", type=click.Path(exists=True)) -@click.option("--logging-config", type=click.Path(exists=True)) -@click.option("--batch-size", type=int) -@click.option("--workers", "num_workers", type=int, default=0) -@click.option("--format", "format_name", type=str) +@common_predict_options def predict_file_list_command( model_path: Path, file_list: Path, @@ -165,7 +242,11 @@ def predict_file_list_command( num_workers: int, format_name: str | None, ) -> None: - """Predict on audio files listed in a text file.""" + """Predict on audio files listed in a text file. + + The list file should contain one audio path per line. Empty lines are + ignored. + """ file_list = Path(file_list) audio_files = [ Path(line.strip()) @@ -187,17 +268,14 @@ def predict_file_list_command( ) -@predict.command(name="dataset") +@predict.command( + name="dataset", + short_help="Predict on recordings from a dataset config.", +) @click.argument("model_path", type=click.Path(exists=True)) @click.argument("dataset_path", type=click.Path(exists=True)) @click.argument("output_path", type=click.Path()) -@click.option("--audio-config", type=click.Path(exists=True)) -@click.option("--inference-config", type=click.Path(exists=True)) -@click.option("--outputs-config", type=click.Path(exists=True)) -@click.option("--logging-config", type=click.Path(exists=True)) -@click.option("--batch-size", type=int) -@click.option("--workers", "num_workers", type=int, default=0) -@click.option("--format", "format_name", type=str) +@common_predict_options def predict_dataset_command( model_path: Path, dataset_path: Path, @@ -210,7 +288,11 @@ def predict_dataset_command( num_workers: int, format_name: str | None, ) -> None: - """Predict on recordings referenced in an annotation dataset.""" + """Predict on recordings referenced in an annotation dataset. + + The dataset is read as a soundevent annotation set and unique recording + paths are extracted before inference. + """ dataset_path = Path(dataset_path) dataset = io.load(dataset_path, type="annotation_set") audio_files = sorted( diff --git a/src/batdetect2/cli/train.py b/src/batdetect2/cli/train.py index 0f630d7..8a6c8ef 100644 --- a/src/batdetect2/cli/train.py +++ b/src/batdetect2/cli/train.py @@ -8,26 +8,103 @@ from batdetect2.cli.base import cli __all__ = ["train_command"] -@cli.command(name="train") +@cli.command(name="train", short_help="Train or fine-tune a model.") @click.argument("train_dataset", type=click.Path(exists=True)) -@click.option("--val-dataset", type=click.Path(exists=True)) -@click.option("--model", "model_path", type=click.Path(exists=True)) -@click.option("--targets", "targets_config", type=click.Path(exists=True)) -@click.option("--model-config", type=click.Path(exists=True)) -@click.option("--training-config", type=click.Path(exists=True)) -@click.option("--audio-config", type=click.Path(exists=True)) -@click.option("--evaluation-config", type=click.Path(exists=True)) -@click.option("--inference-config", type=click.Path(exists=True)) -@click.option("--outputs-config", type=click.Path(exists=True)) -@click.option("--logging-config", type=click.Path(exists=True)) -@click.option("--ckpt-dir", type=click.Path(exists=True)) -@click.option("--log-dir", type=click.Path(exists=True)) -@click.option("--train-workers", type=int) -@click.option("--val-workers", type=int) -@click.option("--num-epochs", type=int) -@click.option("--experiment-name", type=str) -@click.option("--run-name", type=str) -@click.option("--seed", type=int) +@click.option( + "--val-dataset", + type=click.Path(exists=True), + help="Path to validation dataset config file.", +) +@click.option( + "--model", + "model_path", + type=click.Path(exists=True), + help=( + "Path to a checkpoint to continue training from. If omitted, " + "training starts from a fresh model config." + ), +) +@click.option( + "--targets", + "targets_config", + type=click.Path(exists=True), + help="Path to targets config file.", +) +@click.option( + "--model-config", + type=click.Path(exists=True), + help=("Path to model config file. Cannot be used together with --model."), +) +@click.option( + "--training-config", + type=click.Path(exists=True), + help="Path to training config file.", +) +@click.option( + "--audio-config", + type=click.Path(exists=True), + help="Path to audio config file.", +) +@click.option( + "--evaluation-config", + type=click.Path(exists=True), + help="Path to evaluation config file.", +) +@click.option( + "--inference-config", + type=click.Path(exists=True), + help="Path to inference config file.", +) +@click.option( + "--outputs-config", + type=click.Path(exists=True), + help="Path to outputs config file.", +) +@click.option( + "--logging-config", + type=click.Path(exists=True), + help="Path to logging config file.", +) +@click.option( + "--ckpt-dir", + type=click.Path(exists=True), + help="Directory where checkpoints are saved.", +) +@click.option( + "--log-dir", + type=click.Path(exists=True), + help="Directory where logs are written.", +) +@click.option( + "--train-workers", + type=int, + help="Number of worker processes for training data loading.", +) +@click.option( + "--val-workers", + type=int, + help="Number of worker processes for validation data loading.", +) +@click.option( + "--num-epochs", + type=int, + help="Maximum number of training epochs.", +) +@click.option( + "--experiment-name", + type=str, + help="Experiment name used for logging backends.", +) +@click.option( + "--run-name", + type=str, + help="Run name used for logging backends.", +) +@click.option( + "--seed", + type=int, + help="Random seed used for reproducibility.", +) def train_command( train_dataset: Path, val_dataset: Path | None = None, @@ -49,7 +126,12 @@ def train_command( experiment_name: str | None = None, run_name: str | None = None, ): - """Train a model from dataset configs or a checkpoint.""" + """Train a BatDetect2 model. + + Train either from a fresh config (`--model-config`) or by fine-tuning an + existing checkpoint (`--model`). Training data are loaded from + `train_dataset`, with optional validation data from `--val-dataset`. + """ from batdetect2.api_v2 import BatDetect2API from batdetect2.audio import AudioConfig from batdetect2.config import BatDetect2Config diff --git a/src/batdetect2/postprocess/__init__.py b/src/batdetect2/postprocess/__init__.py index 69070a5..35cc61d 100644 --- a/src/batdetect2/postprocess/__init__.py +++ b/src/batdetect2/postprocess/__init__.py @@ -1,6 +1,10 @@ """Main entry point for the BatDetect2 Postprocessing pipeline.""" -from batdetect2.postprocess.config import PostprocessConfig +from batdetect2.postprocess.config import ( + DEFAULT_CLASSIFICATION_THRESHOLD, + DEFAULT_DETECTION_THRESHOLD, + PostprocessConfig, +) from batdetect2.postprocess.nms import non_max_suppression from batdetect2.postprocess.postprocessor import ( Postprocessor, @@ -28,4 +32,6 @@ __all__ = [ "PostprocessorProtocol", "build_postprocessor", "non_max_suppression", + "DEFAULT_CLASSIFICATION_THRESHOLD", + "DEFAULT_DETECTION_THRESHOLD", ] diff --git a/src/batdetect2/targets/rois.py b/src/batdetect2/targets/rois.py index 215b99e..e0e8504 100644 --- a/src/batdetect2/targets/rois.py +++ b/src/batdetect2/targets/rois.py @@ -1,23 +1,19 @@ -"""Handles mapping between geometric ROIs and target representations. +"""Map geometric ROIs to target representations and back. This module defines a standardized interface (`ROITargetMapper`) for converting -a sound event's Region of Interest (ROI) into a target representation suitable -for machine learning models, and for decoding model outputs back into geometric -ROIs. +a sound event ROI into a target representation for model training and decoding +model outputs back into approximate geometries. -The core operations are: -1. **Encoding**: A `soundevent.data.SoundEvent` is mapped to a reference - `Position` (time, frequency) and a `Size` array. The method for - determining the position and size varies by the mapper implementation - (e.g., using a bounding box anchor or the point of peak energy). -2. **Decoding**: A `Position` and `Size` array are mapped back to an - approximate `soundevent.data.Geometry` (typically a `BoundingBox`). +Core operations: -This logic is encapsulated within specific mapper classes. Configuration for -each mapper (e.g., anchor point, scaling factors) is managed by a corresponding -Pydantic config object. The `ROIMapperConfig` type allows for flexibly -selecting and configuring the desired mapper. This module separates the -*geometric* aspect of target definition from *semantic* classification. +- Encode a `soundevent.data.SoundEvent` into a reference `Position` + `(time, frequency)` and a `Size` array. +- Decode a `Position` and `Size` array into an approximate + `soundevent.data.Geometry` (usually a `BoundingBox`). + +The specific mapping depends on the selected mapper implementation. Config +objects provide mapper-specific parameters such as anchor choice and scaling. +This module focuses on the geometric part of target definition. """ from typing import Annotated, Literal @@ -131,12 +127,12 @@ class AnchorBBoxMapper(ROITargetMapper): This class implements the `ROITargetMapper` protocol for `BoundingBox` geometries. - **Encoding**: The `position` is a fixed anchor point on the bounding box - (e.g., "bottom-left"). The `size` is a 2-element array containing the - scaled width and height of the box. + Encoding uses a fixed anchor point on the bounding box for `position` + (for example, ``bottom-left``). The `size` is a 2-element array with + scaled width and height. - **Decoding**: Reconstructs a `BoundingBox` from an anchor point and - scaled width/height. + Decoding reconstructs a `BoundingBox` from anchor position and scaled + width/height. Attributes ---------- @@ -300,13 +296,12 @@ class PeakEnergyBBoxMapper(ROITargetMapper): This class implements the `ROITargetMapper` protocol. - **Encoding**: The `position` is the (time, frequency) coordinate of the - point with the highest energy within the sound event's bounding box. The - `size` is a 4-element array representing the scaled distances from this - peak energy point to the left, bottom, right, and top edges of the box. + Encoding sets `position` to the (time, frequency) coordinate of peak energy + inside the sound event bounding box. The `size` is a 4-element array with + scaled distances from the peak point to left, bottom, right, and top edges. - **Decoding**: Reconstructs a `BoundingBox` by adding/subtracting the - un-scaled distances from the peak energy point. + Decoding reconstructs a `BoundingBox` by applying the unscaled distances to + the peak-energy position. Attributes ----------