Skip to content

geotrax.utils.config_utils

Loading the pipeline config and applying command-line overrides to it, plus resolving the detector model and its class names. Every key of the config is described in the config reference.

Loading configs

load_config

load_config(cfg_filepath: str | Path, logger: Logger, args: Namespace | None = None) -> dict

Load a pipeline config file as a nested dict.

A file that is missing, unreadable, malformed or not a mapping stops the run with a named error here, instead of an opaque traceback later from whichever stage first reads a missing section. A key the file leaves out takes its value from the bundled default.yaml, so a config may hold only the keys it changes, and a copy made before an upgrade keeps working with the keys added since. Without that, a missing config-only key failed with a KeyError midway through a run.

Parameters:

Name Type Description Default
cfg_filepath str | Path

Config path or bundled preset name, resolved with resolve_config_path.

required
logger Logger

Receives the errors and the applied overrides.

required
args Namespace | None

Parsed CLI arguments whose --set overrides are applied with apply_cli_overrides; omit it when there are none.

None

Returns:

Type Description
dict

The config, completed from the bundled defaults, with the overrides applied.

Raises:

Type Description
SystemExit

The file cannot be loaded as a config, or an override is invalid.

Examples:

cfg = load_config("confident", logger)
conf = cfg["ultralytics"]["conf"]
Source code in geotrax/utils/config_utils.py
def load_config(cfg_filepath: str | Path, logger: logging.Logger,
                args: argparse.Namespace | None = None) -> dict:
    """Load a pipeline config file as a nested dict.

    A file that is missing, unreadable, malformed or not a mapping stops the run with a named error
    here, instead of an opaque traceback later from whichever stage first reads a missing section.
    A key the file leaves out takes its value from the bundled `default.yaml`, so a config may hold
    only the keys it changes, and a copy made before an upgrade keeps working with the keys added
    since. Without that, a missing config-only key failed with a `KeyError` midway through a run.

    Args:
        cfg_filepath: Config path or bundled preset name, resolved with
            [`resolve_config_path`][geotrax.utils.config_utils.resolve_config_path].
        logger: Receives the errors and the applied overrides.
        args: Parsed CLI arguments whose `--set` overrides are applied with
            [`apply_cli_overrides`][geotrax.utils.config_utils.apply_cli_overrides]; omit it when
            there are none.

    Returns:
        The config, completed from the bundled defaults, with the overrides applied.

    Raises:
        SystemExit: The file cannot be loaded as a config, or an override is invalid.

    Examples:
        ```python
        cfg = load_config("confident", logger)
        conf = cfg["ultralytics"]["conf"]
        ```
    """
    resolved_filepath = resolve_config_path(cfg_filepath)
    try:
        with open(resolved_filepath) as f:
            kwargs = yaml.safe_load(f)
    except FileNotFoundError:
        logger.critical(f"Configuration file '{cfg_filepath}' not found.")
        sys.exit(1)
    except OSError as exc:
        logger.critical(f"Configuration file '{resolved_filepath}' could not be read: {exc}")
        sys.exit(1)
    except yaml.YAMLError as exc:
        logger.critical(f"Configuration file '{resolved_filepath}' is not valid YAML: {exc}")
        sys.exit(1)

    if not isinstance(kwargs, dict):
        found = 'nothing' if kwargs is None else f'a {type(kwargs).__name__}'
        logger.critical(
            f"Configuration file '{resolved_filepath}' does not contain a top-level mapping of "
            f"sections (found {found}). Run 'geotrax config copy' for a valid starting point."
        )
        sys.exit(1)

    filled = _fill_from_defaults(kwargs, _bundled_defaults())
    if filled and resolved_filepath not in _completed_configs_reported:
        _completed_configs_reported.add(resolved_filepath)
        listed = ', '.join(filled[:5]) + (f' and {len(filled) - 5} more' if len(filled) > 5 else '')
        logger.info(f"Configuration file '{resolved_filepath}' leaves out {len(filled)} key(s), which take their "
                    f"values from the bundled default.yaml: {listed}.")

    if args is not None:
        apply_cli_overrides(kwargs, getattr(args, 'set', None), args, logger)
    return kwargs

load_config_all

load_config_all(args: Namespace, logger: Logger, needs_model: bool = True) -> dict

Load the pipeline config for a stage, with every CLI override applied, and split it for its consumers.

The overrides are applied before the split, so every consumer and the saved run metadata see the configuration the run actually used: --set first, then the dedicated flags through sync_args_with_config. A scalar ultralytics.classes is wrapped in a list, so consumers can always iterate it (or test it for None), and ultralytics keys that Ultralytics has removed, such as keras in an older copied config, are left out with a warning.

With needs_model=True, the model reference is resolved (and downloaded if needed), the class names are resolved, and the active tracker block is written to a temporary YAML file, since Ultralytics takes the tracker config as a file path. The file is removed at interpreter exit.

Parameters:

Name Type Description Default
args Namespace

Parsed CLI arguments; args.cfg names the config file.

required
logger Logger

A logger from setup_logger.

required
needs_model bool

Set to False for stages that never use the detector (e.g. georeferencing), so an unavailable model cannot abort them.

True

Returns:

Type Description
dict

A dict with the keys 'stabilo', 'ultralytics' and 'georef', holding those sections, and 'main', holding the remaining sections plus class_names, class_names_source, model_configured, tracker_active, tracker_params and args. With needs_model=True, 'ultralytics' also holds the resolved model and the tracker file path; with False, the model and tracker entries of 'main' are empty or None.

Raises:

Type Description
SystemExit

The config cannot be loaded, an override is invalid, or the model or tracker cannot be resolved.

Source code in geotrax/utils/config_utils.py
def load_config_all(args: argparse.Namespace, logger: logging.Logger, needs_model: bool = True) -> dict:
    """Load the pipeline config for a stage, with every CLI override applied, and split it for its consumers.

    The overrides are applied before the split, so every consumer and the saved run metadata see the
    configuration the run actually used: `--set` first, then the dedicated flags through
    [`sync_args_with_config`][geotrax.utils.config_utils.sync_args_with_config]. A scalar
    `ultralytics.classes` is wrapped in a list, so consumers can always iterate it (or test it for
    `None`), and `ultralytics` keys that Ultralytics has removed, such as `keras` in an older copied
    config, are left out with a warning.

    With `needs_model=True`, the model reference is resolved (and downloaded if needed), the class
    names are resolved, and the active tracker block is written to a temporary YAML file, since
    Ultralytics takes the tracker config as a file path. The file is removed at interpreter exit.

    Args:
        args: Parsed CLI arguments; `args.cfg` names the config file.
        logger: A logger from [`setup_logger`][geotrax.utils.logging_utils.setup_logger].
        needs_model: Set to `False` for stages that never use the detector (e.g. georeferencing),
            so an unavailable model cannot abort them.

    Returns:
        A dict with the keys `'stabilo'`, `'ultralytics'` and `'georef'`, holding those sections, and
            `'main'`, holding the remaining sections plus `class_names`, `class_names_source`,
            `model_configured`, `tracker_active`, `tracker_params` and `args`. With `needs_model=True`,
            `'ultralytics'` also holds the resolved `model` and the `tracker` file path; with `False`,
            the model and tracker entries of `'main'` are empty or `None`.

    Raises:
        SystemExit: The config cannot be loaded, an override is invalid, or the model or tracker
            cannot be resolved.
    """
    full = load_config(args.cfg, logger, args)
    sync_args_with_config(args, full, logger)

    kwargs_tracker     = full.get('tracker', {})
    kwargs_stabilo     = full.get('stabilo', {})
    kwargs_ultralytics = dict(full.get('ultralytics', {}))
    classes = kwargs_ultralytics.get('classes')
    if isinstance(classes, int) and not isinstance(classes, bool):
        kwargs_ultralytics['classes'] = [classes]
    elif isinstance(classes, tuple):
        kwargs_ultralytics['classes'] = list(classes)
    _drop_obsolete_ultralytics_keys(kwargs_ultralytics, args.cfg, logger)
    kwargs_georef      = full.get('georef', {})
    kwargs_main        = {k: v for k, v in full.items()
                          if k not in ('tracker', 'stabilo', 'ultralytics', 'georef')}

    if needs_model:
        kwargs_ultralytics['tracker'] = str(_write_tracker_yaml(kwargs_tracker, args.cfg, logger))
        # The model and class-rename mapping live in the 'extraction:' section (the 'ultralytics:'
        # section keeps only a pointer comment). A CLI --model override takes precedence over the
        # config value, then the reference (local path or hf:// auto-download) is resolved to a
        # concrete local file for Ultralytics.
        extraction_cfg = full.get('extraction', {})
        raw_model = getattr(args, 'model', None)
        if isinstance(raw_model, list):
            raw_model = ' '.join(raw_model)
        model_ref = raw_model or extraction_cfg.get('model') or kwargs_ultralytics.get('model')
        kwargs_main['model_configured'] = str(model_ref)
        kwargs_ultralytics['model'] = str(resolve_model_path(model_ref, logger))
        kwargs_main['class_names'], kwargs_main['class_names_source'] = resolve_class_names(
            Path(kwargs_ultralytics['model']),
            getattr(args, 'class_names', None),
            extraction_cfg.get('class_rename'),
            kwargs_ultralytics.get('classes'),
            logger,
        )
        active = kwargs_tracker.get('active')
        kwargs_main['tracker_active'] = active
        kwargs_main['tracker_params'] = kwargs_tracker.get(active, {}) if active else {}
    else:
        kwargs_main['class_names'] = {}
        kwargs_main['class_names_source'] = None
        kwargs_main['model_configured'] = None
        kwargs_main['tracker_active'] = None
        kwargs_main['tracker_params'] = {}

    kwargs_main['args'] = args

    logger.info(f"Pipeline configuration loaded from: '{args.cfg}'.")

    return {
        'main': kwargs_main,
        'stabilo': kwargs_stabilo,
        'ultralytics': kwargs_ultralytics,
        'georef': kwargs_georef,
    }

resolve_config_path

resolve_config_path(cfg_filepath: str | Path) -> Path

Resolve a -c value to a config file.

Tries, in order, the path as given, the path relative to ROOT_DIR, and the bundled geotrax/cfg/ folder. A missing .yaml suffix and a legacy leading cfg/ are tolerated, so confident, cfg/default.yaml and lenient.yaml all name bundled presets.

Parameters:

Name Type Description Default
cfg_filepath str | Path

Config path or bundled preset name.

required

Returns:

Type Description
Path

The first candidate that is a file, or cfg_filepath unchanged when none is, leaving the error to load_config.

Source code in geotrax/utils/config_utils.py
def resolve_config_path(cfg_filepath: str | Path) -> Path:
    """Resolve a `-c` value to a config file.

    Tries, in order, the path as given, the path relative to
    [`ROOT_DIR`][geotrax.utils.config_utils.ROOT_DIR], and the bundled `geotrax/cfg/` folder. A
    missing `.yaml` suffix and a legacy leading `cfg/` are tolerated, so `confident`,
    `cfg/default.yaml` and `lenient.yaml` all name bundled presets.

    Args:
        cfg_filepath: Config path or bundled preset name.

    Returns:
        The first candidate that is a file, or `cfg_filepath` unchanged when none is, leaving the
            error to [`load_config`][geotrax.utils.config_utils.load_config].
    """
    path = Path(cfg_filepath)
    if not path.suffix and path.name:
        path = path.with_suffix('.yaml')

    candidates = [path]
    if not path.is_absolute():
        bundled = Path(*path.parts[1:]) if path.parts[:1] == ('cfg',) else path
        candidates += [ROOT_DIR / path, CFG_DIR / bundled]

    for candidate in candidates:
        if candidate.is_file():
            return candidate
    return Path(cfg_filepath)

merge_bundled_defaults

merge_bundled_defaults(section: dict | None, path: str) -> dict

Fill the keys missing from a config block with the values from the bundled default.yaml.

load_config already fills every missing key this way; this function gives the same fallback to a block read from a config dict that did not come from it, such as extraction.sahi, which is read as a whole.

Parameters:

Name Type Description Default
section dict | None

The block from the loaded config, possibly None.

required
path str

Dotted path of the block, e.g. 'extraction.sahi'.

required

Returns:

Type Description
dict

A new dict with the bundled defaults overlaid by section.

Source code in geotrax/utils/config_utils.py
def merge_bundled_defaults(section: dict | None, path: str) -> dict:
    """Fill the keys missing from a config block with the values from the bundled `default.yaml`.

    [`load_config`][geotrax.utils.config_utils.load_config] already fills every missing key this
    way; this function gives the same fallback to a block read from a config dict that did not come
    from it, such as `extraction.sahi`, which is read as a whole.

    Args:
        section: The block from the loaded config, possibly `None`.
        path: Dotted path of the block, e.g. `'extraction.sahi'`.

    Returns:
        A new dict with the bundled defaults overlaid by `section`.
    """
    defaults = _get_by_path(_bundled_defaults(), path, default={}) or {}
    return {**defaults, **(section or {})}

Overrides

apply_cli_overrides

apply_cli_overrides(cfg: dict, overrides: list | None, args: Namespace | None, logger: Logger) -> None

Apply --set KEY=VALUE overrides to a loaded config, in place.

KEY is matched against the loaded config, so a custom config with extra keys works too. It is a full dotted path (ultralytics.conf) or any unambiguous tail of one (conf, matching.gpu). VALUE is parsed as YAML and must be compatible with the type of the current value; ints and floats are interchangeable, 0/1 are accepted for booleans, and a null key takes anything. Nothing is written until every override is valid, so a rejected command line leaves the config untouched.

Parameters:

Name Type Description Default
cfg dict

Full loaded config, updated in place.

required
overrides list | None

The KEY=VALUE strings; None or empty does nothing.

required
args Namespace | None

Parsed CLI arguments. When they carry the parser's _cfg_paths, a key also set by its dedicated flag on the same command line is a conflict.

required
logger Logger

A logger from setup_logger; each override is announced at its notice level.

required

Raises:

Type Description
SystemExit

A key is unknown (close matches are suggested), ambiguous, a whole section, or also set by its dedicated flag, or a value has the wrong type.

Examples:

cfg = load_config("default", logger)
apply_cli_overrides(cfg, ["conf=0.35", "matching.gpu=true"], None, logger)
Source code in geotrax/utils/config_utils.py
def apply_cli_overrides(cfg: dict, overrides: list | None, args: argparse.Namespace | None,
                        logger: logging.Logger) -> None:
    """Apply `--set KEY=VALUE` overrides to a loaded config, in place.

    KEY is matched against the loaded config, so a custom config with extra keys works too. It is
    a full dotted path (`ultralytics.conf`) or any unambiguous tail of one (`conf`, `matching.gpu`).
    VALUE is parsed as YAML and must be compatible with the type of the current value; ints and
    floats are interchangeable, `0`/`1` are accepted for booleans, and a `null` key takes anything.
    Nothing is written until every override is valid, so a rejected command line leaves the config
    untouched.

    Args:
        cfg: Full loaded config, updated in place.
        overrides: The `KEY=VALUE` strings; `None` or empty does nothing.
        args: Parsed CLI arguments. When they carry the parser's `_cfg_paths`, a key also set by its
            dedicated flag on the same command line is a conflict.
        logger: A logger from [`setup_logger`][geotrax.utils.logging_utils.setup_logger]; each
            override is announced at its `notice` level.

    Raises:
        SystemExit: A key is unknown (close matches are suggested), ambiguous, a whole section, or
            also set by its dedicated flag, or a value has the wrong type.

    Examples:
        ```python
        cfg = load_config("default", logger)
        apply_cli_overrides(cfg, ["conf=0.35", "matching.gpu=true"], None, logger)
        ```
    """
    if not overrides:
        return

    leaf_paths = list(_iter_leaf_paths(cfg))
    flag_for_path = _dedicated_flags_in_use(args)
    resolved = []

    for token in overrides:
        if '=' not in token:
            logger.critical(f"Invalid --set entry '{token}'. Expected KEY=VALUE (e.g. --set conf=0.35).")
            sys.exit(1)
        key, raw_value = token.split('=', 1)
        key = key.strip()
        path = _resolve_override_key(key, leaf_paths, cfg, logger)

        if path in flag_for_path:
            logger.critical(
                f"Conflicting overrides for '{path}':\n"
                f"  --set {key}={raw_value}\n"
                f"  {flag_for_path[path]}\n"
                "Pass only one."
            )
            sys.exit(1)

        if raw_value == '':
            value = ''  # '--set tracks_postfix=' clears a string; YAML would read it as null
        else:
            try:
                value = yaml.safe_load(raw_value)
            except yaml.YAMLError:
                value = raw_value  # an unquoted string that is not valid YAML is still a string
        current = _get_by_path(cfg, path)
        value = _coerce_override_value(path, value, current, raw_value, logger)
        resolved.append((path, value, current))

    # The config is re-loaded for every stage and, under 'batch', for every video; announce each
    # override once at NOTICE (visible without --verbose) and drop to INFO on the repeats.
    first_time = not getattr(args, '_overrides_announced', False)
    for path, value, current in resolved:
        _set_by_path(cfg, path, value)
        message = f"CLI override: {path} = {value!r} (was {current!r})."
        logger.notice(message) if first_time else logger.info(message)
    if args is not None:
        args._overrides_announced = True

sync_args_with_config

sync_args_with_config(args: Namespace, cfg: dict, logger: Logger) -> None

Reconcile the parsed CLI arguments with the config, in both directions.

Every config-backed flag defaults to None, so for each entry of the args._cfg_paths map that the parser recorded:

  • an argument that is still None is filled from the config, or from the bundled default.yaml when a custom config lacks the key;
  • an argument that was given is written into the config.

The second direction makes the config dict authoritative: everything downstream, the saved run-metadata YAML included, sees the configuration the run actually used. Flags marked no_sync are skipped, since their own resolvers handle precedence. A dest listed in args._cfg_pinned is always written, even when None; that is how batch clears cut_frame_right in directory mode without the next stage reading it back from the config.

Parameters:

Name Type Description Default
args Namespace

Parsed CLI arguments, as returned by the stage's parse_cli_args().

required
cfg dict

Full loaded config, updated in place.

required
logger Logger

Receives a line for each argument written into the config.

required
Source code in geotrax/utils/config_utils.py
def sync_args_with_config(args: argparse.Namespace, cfg: dict, logger: logging.Logger) -> None:
    """Reconcile the parsed CLI arguments with the config, in both directions.

    Every config-backed flag defaults to `None`, so for each entry of the `args._cfg_paths` map
    that the parser recorded:

    - an argument that is still `None` is filled from the config, or from the bundled `default.yaml`
      when a custom config lacks the key;
    - an argument that was given is written into the config.

    The second direction makes the config dict authoritative: everything downstream, the saved
    run-metadata YAML included, sees the configuration the run actually used. Flags marked
    `no_sync` are skipped, since their own resolvers handle precedence. A dest listed in
    `args._cfg_pinned` is always written, even when `None`; that is how `batch` clears
    `cut_frame_right` in directory mode without the next stage reading it back from the config.

    Args:
        args: Parsed CLI arguments, as returned by the stage's `parse_cli_args()`.
        cfg: Full loaded config, updated in place.
        logger: Receives a line for each argument written into the config.
    """
    pinned = getattr(args, '_cfg_pinned', ())
    for dest, spec in getattr(args, '_cfg_paths', {}).items():
        if spec.no_sync or not hasattr(args, dest):
            continue
        value = getattr(args, dest)
        if value is None and dest not in pinned:
            config_value = _get_by_path(cfg, spec.path)
            if config_value is _MISSING:
                config_value = _get_by_path(_bundled_defaults(), spec.path)
                if config_value is _MISSING:
                    continue
            if spec.invert:
                config_value = not config_value
            elif spec.coerce is not None and config_value is not None:
                config_value = spec.coerce(config_value)
            setattr(args, dest, config_value)
        else:
            _set_by_path(cfg, spec.path, not value if spec.invert and value is not None else value)
            if dest in pinned:  # the caller suppressed the key; it did not come from the CLI
                continue
            provided = getattr(args, '_cli_provided', None)
            if provided is None or dest in provided:  # a pulled value pushed back is not news
                logger.info(f"CLI argument applied to the configuration: {spec.path} = {value}.")

backfill_args_from_config

backfill_args_from_config(args: Namespace, mapping: dict) -> None

Fill every argument that was not given on the command line from a mapping.

Used by the tools/ scripts. The pipeline stages use sync_args_with_config instead, which also writes the given arguments back into the config.

Parameters:

Name Type Description Default
args Namespace

Parsed CLI arguments; an argument still None was not given.

required
mapping dict

Argument name to fallback value, usually read from a config section.

required
Source code in geotrax/utils/config_utils.py
def backfill_args_from_config(args: argparse.Namespace, mapping: dict) -> None:
    """Fill every argument that was not given on the command line from a mapping.

    Used by the `tools/` scripts. The pipeline stages use
    [`sync_args_with_config`][geotrax.utils.config_utils.sync_args_with_config] instead, which
    also writes the given arguments back into the config.

    Args:
        args: Parsed CLI arguments; an argument still `None` was not given.
        mapping: Argument name to fallback value, usually read from a config section.
    """
    for arg_name, config_value in mapping.items():
        if getattr(args, arg_name) is None:
            setattr(args, arg_name, config_value)

Models and class names

resolve_model_path

resolve_model_path(model_ref: str | Path, logger: Logger) -> Path

Resolve an extraction.model / --model value to a local weights file, downloading it if needed.

A Hugging Face reference is downloaded once into the standard Hub cache (~/.cache/huggingface/hub, relocatable with HF_HOME or HF_HUB_CACHE) and revalidated by etag on later runs, so it is not downloaded again. A local path is resolved with resolve_asset_path and never downloaded.

Parameters:

Name Type Description Default
model_ref str | Path

hf://<org>/<repo>/<path/to/file>, optionally pinned to a revision (commit SHA, tag or branch) as hf://<org>/<repo>@<revision>/<path/to/file>, or a local path. Without a revision the repo's current main is used. The weights are unpickled when loaded, so pinning a commit SHA ties a run to a known file.

required
logger Logger

A logger from setup_logger, which provides the notice level used to announce a download.

required

Returns:

Type Description
Path

Local path of the weights.

Raises:

Type Description
SystemExit

A malformed reference, a failed download, or huggingface_hub missing.

Examples:

resolve_model_path("hf://rfonod/geo-trax/geotrax_hbb_yolov8s_1920_v1.pt", logger)
resolve_model_path("hf://rfonod/geo-trax@v1/geotrax_hbb_yolov8s_1920_v1.pt", logger)
resolve_model_path("models/custom.pt", logger)
Source code in geotrax/utils/config_utils.py
def resolve_model_path(model_ref: str | Path, logger: logging.Logger) -> Path:
    """Resolve an `extraction.model` / `--model` value to a local weights file, downloading it if needed.

    A Hugging Face reference is downloaded once into the standard Hub cache
    (`~/.cache/huggingface/hub`, relocatable with `HF_HOME` or `HF_HUB_CACHE`) and revalidated by
    etag on later runs, so it is not downloaded again. A local path is resolved with
    [`resolve_asset_path`][geotrax.utils.config_utils.resolve_asset_path] and never downloaded.

    Args:
        model_ref: `hf://<org>/<repo>/<path/to/file>`, optionally pinned to a revision (commit SHA,
            tag or branch) as `hf://<org>/<repo>@<revision>/<path/to/file>`, or a local path.
            Without a revision the repo's current `main` is used. The weights are unpickled when
            loaded, so pinning a commit SHA ties a run to a known file.
        logger: A logger from [`setup_logger`][geotrax.utils.logging_utils.setup_logger], which
            provides the `notice` level used to announce a download.

    Returns:
        Local path of the weights.

    Raises:
        SystemExit: A malformed reference, a failed download, or `huggingface_hub` missing.

    Examples:
        ```python
        resolve_model_path("hf://rfonod/geo-trax/geotrax_hbb_yolov8s_1920_v1.pt", logger)
        resolve_model_path("hf://rfonod/geo-trax@v1/geotrax_hbb_yolov8s_1920_v1.pt", logger)
        resolve_model_path("models/custom.pt", logger)
        ```
    """
    model_str = str(model_ref).strip()
    if model_str.startswith('hf download '):
        model_str = model_str[len('hf download '):].strip()
    if not model_str.startswith(HF_PREFIX):
        return resolve_asset_path(model_str)

    if hf_hub_download is None:
        logger.critical(
            f"Model '{model_str}' is a Hugging Face reference but 'huggingface_hub' is not installed. "
            "Install it (it is a core dependency: `python -m pip install -e .`) or point the config "
            "`extraction -> model` (or --model) at a local weights file."
        )
        sys.exit(1)

    parts = model_str[len(HF_PREFIX):].split('/')
    if len(parts) < 3:
        logger.critical(
            f"Malformed Hugging Face model reference '{model_str}'. Expected "
            f"'{HF_PREFIX}<org>/<repo>/<path/to/file>' (e.g. '{HF_PREFIX}rfonod/geo-trax/geotrax_hbb_yolov8s_1920_v1.pt')."
        )
        sys.exit(1)

    repo_name, _, revision = parts[1].partition('@')
    repo_id = f'{parts[0]}/{repo_name}'
    revision = revision or None
    filename = '/'.join(parts[2:])
    cached = (
        try_to_load_from_cache(repo_id=repo_id, filename=filename, revision=revision) if try_to_load_from_cache else None
    )
    is_cached = isinstance(cached, str)
    if not is_cached:
        cache_hint = str(_HF_HUB_CACHE) if _HF_HUB_CACHE else '~/.cache/huggingface/hub'
        logger.notice(
            f"Downloading '{filename}' from Hugging Face (repo: '{repo_id}') → "
            f"cache: '{cache_hint}' (override via HF_HOME or HF_HUB_CACHE) ..."
        )
    try:
        local_path = hf_hub_download(repo_id=repo_id, filename=filename, revision=revision)
    except Exception as e:
        logger.critical(f"Failed to download model '{filename}' from Hugging Face repo '{repo_id}': {e}")
        sys.exit(1)
    if is_cached:
        logger.info(f"Model '{filename}' loaded from cache: '{local_path}'.")
    return Path(local_path)

resolve_class_names

resolve_class_names(model_path: Path, cli_value: list | str | None, cfg_value: dict | str | None, classes: list | None, logger: Logger) -> tuple

Resolve the class-id to name mapping by precedence: CLI, config, model, then integer labels.

An override that cannot be read is logged and skipped, falling through to the next source.

Parameters:

Name Type Description Default
model_path Path

Local weights file, read when neither override is given.

required
cli_value list | str | None

--class-names value: ID=NAME tokens or a one-element list holding a .yaml/.json file path.

required
cfg_value dict | str | None

extraction.class_rename value: an inline mapping or a .yaml/.json file path.

required
classes list | None

The ultralytics.classes ids, labelled when falling back to integer labels (range(100) when None).

required
logger Logger

Receives the chosen source, or a warning on the integer fallback.

required

Returns:

Type Description
tuple

(mapping, source), where source is 'cli', 'config', 'model' or 'fallback'.

Source code in geotrax/utils/config_utils.py
def resolve_class_names(
    model_path: Path,
    cli_value: list | str | None,
    cfg_value: dict | str | None,
    classes: list | None,
    logger: logging.Logger,
) -> tuple:
    """Resolve the class-id to name mapping by precedence: CLI, config, model, then integer labels.

    An override that cannot be read is logged and skipped, falling through to the next source.

    Args:
        model_path: Local weights file, read when neither override is given.
        cli_value: `--class-names` value: `ID=NAME` tokens or a one-element list holding a
            `.yaml`/`.json` file path.
        cfg_value: `extraction.class_rename` value: an inline mapping or a `.yaml`/`.json` file path.
        classes: The `ultralytics.classes` ids, labelled when falling back to integer labels
            (`range(100)` when `None`).
        logger: Receives the chosen source, or a warning on the integer fallback.

    Returns:
        `(mapping, source)`, where `source` is `'cli'`, `'config'`, `'model'` or `'fallback'`.
    """
    for source_label, log_tag, value in (
        ('cli', '--class-names', cli_value),
        ('config', 'config extraction.class_rename', cfg_value),
    ):
        if value is not None:
            mapping = _load_class_names_mapping(value, logger)
            if mapping is not None:
                logger.info(f"Class names taken from {log_tag}: {mapping}.")
                return mapping, source_label

    model_names = load_class_names_from_model(model_path, logger)
    if model_names:
        return model_names, 'model'

    ids = classes if classes else range(100)
    logger.warning(
        "No class-name mapping found (CLI, config, or model); falling back to integer class IDs. "
        "Provide one via cfg -> extraction -> class_rename or --class-names to label classes."
    )
    return {int(i): str(int(i)) for i in ids}, 'fallback'

load_class_names_from_model

load_class_names_from_model(model_path: Path, logger: Logger) -> dict | None

Read the class names embedded in a YOLO weights file.

Parameters:

Name Type Description Default
model_path Path

Local weights file.

required
logger Logger

Receives the outcome.

required

Returns:

Type Description
dict | None

Class id to name, or None when Ultralytics is missing or the model fails to load.

Source code in geotrax/utils/config_utils.py
def load_class_names_from_model(model_path: Path, logger: logging.Logger) -> dict | None:
    """Read the class names embedded in a YOLO weights file.

    Args:
        model_path: Local weights file.
        logger: Receives the outcome.

    Returns:
        Class id to name, or `None` when Ultralytics is missing or the model fails to load.
    """
    if YOLO is None:
        logger.error("ultralytics is not installed; cannot load class names from model.")
        return None
    try:
        names = YOLO(str(model_path)).names
        logger.info(f"Class names loaded from model: '{model_path}'.")
        return names
    except Exception as e:
        logger.error(f"Failed to load class names from '{model_path}': {e}.")
        return None

HF_PREFIX module-attribute

HF_PREFIX = 'hf://'

Scheme of a Hugging Face Hub model reference, e.g. hf://rfonod/geo-trax/geotrax_hbb_yolov8s_1920_v1.pt.

Paths

resolve_asset_path

resolve_asset_path(filepath: str | Path) -> Path

Resolve a relative asset path, such as local model weights, against the working directory and ROOT_DIR.

Parameters:

Name Type Description Default
filepath str | Path

Asset path.

required

Returns:

Type Description
Path

The path relative to ROOT_DIR when only that one exists, otherwise filepath unchanged, leaving the error to the caller.

Source code in geotrax/utils/config_utils.py
def resolve_asset_path(filepath: str | Path) -> Path:
    """Resolve a relative asset path, such as local model weights, against the working directory and `ROOT_DIR`.

    Args:
        filepath: Asset path.

    Returns:
        The path relative to [`ROOT_DIR`][geotrax.utils.config_utils.ROOT_DIR] when only that one
            exists, otherwise `filepath` unchanged, leaving the error to the caller.
    """
    path = Path(filepath)
    if not path.is_absolute() and not path.is_file() and (ROOT_DIR / path).is_file():
        return ROOT_DIR / path
    return path

ROOT_DIR module-attribute

ROOT_DIR = PACKAGE_DIR.parent

Parent of the geotrax package: the repository root in a source checkout, site-packages when installed.