Generated banner for Tao Analyze Gaps Od Map
Generated logo for Tao Analyze Gaps Od Map

Tao Analyze Gaps Od Map

NVIDIAOfficialCreated by NVIDIA Corporation

  • Skill
  • Review and audit
  • Software development
  • Apache-2.0
  • Medium risk
  • Has scripts

From the author

Run TAO Data Services object-detection gap analysis from ground-truth and inference annotations. Use when an object detection workflow needs to identify weak images by comparing model predictions against ground truth using per-class recall, precision, and AP50 thresholds. Use when the user asks to "analyze OD gaps", "find weak OD images", or "run mAP gap analysis".
— the description by NVIDIA Corporation

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The TAO Analyze Gaps OD mAP skill identifies weak images in an object detection dataset by evaluating model performance against ground truth. It computes metrics such as TP, FP, FN, and AP50 on a per-image and per-class basis. By applying configurable thresholds, the skill flags images where model performance is insufficient, providing actionable data for downstream mining and model improvement workflows. It requires pre-existing inference annotations and ground truth data to function.

What it does

  • Compares ground-truth and inference annotations to identify model performance gaps.
  • Calculates per-image and per-class metrics including recall, precision, and AP50.
  • Generates a list of weak images based on user-defined performance thresholds.
  • Produces detailed parquet reports for box-level gaps and image-level metrics.
  • Validates configuration specifications to ensure correct input formats and pathing.
  • Outputs a JSON gap report summarizing FP/FN counts and run settings.

When to use it

  • When you need to identify images where an object detection model is underperforming.
  • When preparing data for active learning or image mining workflows.
  • When you need to audit model predictions against ground truth annotations.
  • When you need to generate a quantitative report on model weaknesses per class.

Advantages

  • Enables targeted data collection by identifying specific weak images.
  • Supports both KITTI and COCO annotation formats.
  • Provides granular metrics for per-class performance analysis.
  • Ensures reproducibility by linking output artifacts with the specific experiment configuration.

Usage examples

“Analyze OD gaps for my current model predictions.”
The agent will guide you to configure the object_detection.yaml spec with your annotation paths and run the gap analysis container to identify weak images.
“Find weak OD images where car recall is below 0.9.”
The agent will help you set the weak_thresholds in your spec to include a car recall threshold of 0.9 and execute the analysis to filter the dataset.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/nvidia/tao-analyze-gaps-od-map/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip tao-analyze-gaps-od-map.zip -d .agents/skills

Documentation by NVIDIA Corporation

TAO Analyze Gaps OD mAP

Use this skill to run TAO Data Services object-detection gap analysis. The skill compares ground-truth and inference annotations, computes per-image per-class TP/FP/FN/AP50 metrics, and identifies weak images where any class metric falls below its threshold. It does not run inference; upstream steps must produce the inference annotations first.

The container entrypoint is:

gap_analysis object_detection -e /absolute/path/to/object_detection.yaml

Inputs

Required spec fields:

FieldMeaning
ground_truth_ann_pathKITTI label directory or COCO .json with ground-truth boxes.
inference_ann_pathKITTI label directory or COCO .json with model predictions.
images_dirRoot image directory. Establishes the full image universe including unannotated images.
results_dirOutput directory for all artifacts.
kpiIdentifier tag written to every output row.
input_formatkitti or coco. Must be declared explicitly; never inferred from the path.

Common optional fields:

FieldDefaultMeaning
iou_threshold0.5IoU at or above which a prediction is accepted as a true positive.
conf_threshold0.0Predictions below this confidence are dropped before matching.
min_area0Boxes whose pixel area (w × h) is strictly below this value are discarded.
class_mapping{}Maps raw annotation label strings to canonical class names. Absent labels are kept as-is.
weak_thresholds{}Per-class thresholds as {class_name: {recall, precision, ap50}}. Absent keys fall back to the default_*_threshold values. Reference ITS defaults: car 0.99, bicycle 0.7, person 0.7 — a strict gate on the abundant, well-learned class and looser gates on the rare ones the loop exists to improve.
default_recall_threshold0.5Fallback recall threshold for classes not listed in weak_thresholds.
default_precision_threshold0.0Fallback precision threshold. Set to 0.0 to disable precision-based weak selection.
default_ap50_threshold0.5Fallback for classes absent from weak_thresholds. Set 0.0 so unlisted classes never mark an image weak — the reference filter had no fallback, and leaving TAO DS's 0.5 in place silently gates every class you did not list.

Do not hand-write the spec. Copy the template and fill in the nulls — every tuning value it already carries is the one this stage wants — then validate:

cp skills/data/tao-analyze-gaps-od-map/assets/default_object_detection.yaml "$SPEC"
# fill ground_truth_ann_path, inference_ann_path, images_dir, results_dir, kpi, input_format
python3 skills/data/tao-analyze-gaps-od-map/scripts/verify_object_detection_spec.py --spec "$SPEC"

verify rejects the spellings that fail — uppercase input_format, relative or missing paths, a weak_thresholds entry that is a bare number rather than a mapping — and reports every gated class plus the default_* fallbacks, so the selection criteria behind a weak set are recoverable from the run's output. It warns when a fallback is above zero, since that gates classes you did not list.

Quick Start

Run from the tao-skill-bank repo root.

Write the spec into the results directory. The run emits four artifacts and does not retain the spec, so a completed gap analysis otherwise cannot tell you which thresholds produced its weak set — and that weak set sizes the mining budget downstream. Keeping them together makes the selection criteria recoverable from the run alone.

RESULTS_DIR=/absolute/path/for/this/run          # results_dir in the spec
SPEC="$RESULTS_DIR/object_detection.yaml"        # spec lives beside its outputs
RUN_ROOT=/absolute/path/that/contains/annotations/images/and/results
GPU_COUNT=1

DS_IMAGE="$(scripts/resolve_versions_key.py images.tao_toolkit.data_services)"

RUNTIME_HOME="$RUN_ROOT/.tao-runtime/home"
mkdir -p "$RUNTIME_HOME/.cache"

docker run --rm --gpus "$GPU_COUNT" --shm-size=8g --network=host \
  --user "$(id -u):$(id -g)" \
  -e USER="$(id -un)" -e LOGNAME="$(id -un)" \
  -e HOME="$RUNTIME_HOME" -e XDG_CACHE_HOME="$RUNTIME_HOME/.cache" \
  -v "$RUN_ROOT:$RUN_ROOT" \
  -w "$RUN_ROOT" \
  "$DS_IMAGE" \
  gap_analysis object_detection -e "$SPEC"

Keep --user and the identity env block above. TAO DS images call getpass.getuser() at startup; without USER/LOGNAME they fail with KeyError: 'getpwuid(): uid not found: <uid>' because the UID has no /etc/passwd entry. Do not work around it by running the container as root; that leaves root-owned outputs. This follows the launch contract in tao-run-on-docker.

Preflight

  1. Verify Docker access:

docker info > /dev/null
  1. Resolve and pull the data-services image if needed:

DS_IMAGE="$(scripts/resolve_versions_key.py images.tao_toolkit.data_services)"
docker image inspect "$DS_IMAGE" > /dev/null || docker pull "$DS_IMAGE"
  1. Confirm RUN_ROOT contains the spec, both annotation sources, and the image directory. Mount RUN_ROOT to the same absolute path inside Docker.

Outputs

ArtifactLocationContents
FP/FN box gapsresults_dir/box_gaps.parquetOne row per unmatched box: kpi, image_id, filepath, class, gap_type (FP/FN), bbox, confidence, best_iou.
Per-image metricsresults_dir/image_metrics.parquetPer-image per-class: tp, fp, fn, precision, recall, ap50.
Weak imagesresults_dir/weak_images.parquetImages where any class metric falls below threshold: filepath, weak_classes, weak_recall, weak_precision, weak_ap50. Feed this into tao-mine-od-images.
Gap reportresults_dir/gap_report.jsonFP/FN counts by type and class, plus run settings.

All four artifacts are always written, even when no gaps are found.

Troubleshooting

The subtask object_detection requires -e/--experiment_spec_file: rerun with gap_analysis object_detection -e "$SPEC".

Input path not found inside Docker: use a RUN_ROOT mount where host and container paths are identical.

input_format error: set input_format: kitti or input_format: coco explicitly — it is never inferred from the path.

weak_images.parquet is empty: all class metrics are above their thresholds. Lower default_recall_threshold / default_ap50_threshold or add per-class entries to weak_thresholds.

Output directory not writable after Docker exits: the container ran without --user, so it wrote as root. Relaunch with the --user and identity env block shown above.

Files

scripts/verify_object_detection_spec.py

#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Validate a TAO Data Services object-detection gap-analysis spec.

Fill `assets/default_object_detection.yaml` — it carries every default this stage
needs — then run this against the result. The template is the only place a
default value lives, so there is nothing that can disagree with it.

Checks the spec against what `gap_analysis object_detection` actually requires:

- `input_format` is `kitti` or `coco`, **lowercase**. `analytics kpi_analyze` in
  the same container wants these uppercase; the two stages spell it differently
  and both are correct for themselves.
- the three input paths and `results_dir` are present and absolute. KITTI passes
  a directory here, COCO a json file, so both are accepted.
- `weak_thresholds` entries are mappings of metric to a number in [0, 1].
- every gated class is reported, since an image is weak if **any** class falls
  below its gate and one forgotten entry silently changes the selection.

The `default_*_threshold` values get their own report line. TAO DS defaults
`default_ap50_threshold` to 0.5, so a spec that lists some classes and says
nothing else still gates every class it did not list — which inflates the weak
set and the mining budget derived from it. A non-zero fallback is legal and
occasionally wanted, so it warns rather than fails.
"""

from __future__ import annotations

import argparse
import math
import sys
from pathlib import Path
from typing import Any

import yaml

VALID_FORMATS = {"kitti", "coco"}
PATH_KEYS = ("ground_truth_ann_path", "inference_ann_path", "images_dir")
METRICS = ("ap50", "recall", "precision")
DEFAULT_KEYS = ("default_ap50_threshold", "default_recall_threshold",
                "default_precision_threshold")


def load_yaml(path: Path) -> dict[str, Any]:
    with path.open("r", encoding="utf-8") as fh:
        data = yaml.safe_load(fh) or {}
    if not isinstance(data, dict):
        raise ValueError(f"{path} must contain a YAML mapping.")
    return data


def validate_config(config: dict[str, Any]) -> list[str]:
    """Raise on anything that would fail or silently mis-select. Returns warnings."""
    warnings: list[str] = []

    fmt = config.get("input_format")
    if not isinstance(fmt, str) or fmt not in VALID_FORMATS:
        raise ValueError(
            f"input_format must be one of {sorted(VALID_FORMATS)} (lowercase); got {fmt!r}. "
            "It is never inferred from the path."
        )

    for key in PATH_KEYS:
        value = config.get(key)
        if not isinstance(value, str) or not value.strip():
            raise ValueError(f"{key} is required.")
        path = Path(str(value)).expanduser()
        if not path.is_absolute():
            raise ValueError(f"{key} must be absolute: {path}")
        if not path.exists():
            raise FileNotFoundError(f"{key} does not exist: {path}")

    results_dir = config.get("results_dir")
    if not isinstance(results_dir, str) or not results_dir.strip():
        raise ValueError("results_dir is required.")
    out = Path(str(results_dir)).expanduser()
    if not out.is_absolute():
        raise ValueError(f"results_dir must be absolute: {out}")
    out.mkdir(parents=True, exist_ok=True)

    if not isinstance(config.get("kpi"), str) or not config["kpi"].strip():
        raise ValueError("kpi is required — it labels the rows in the emitted parquets.")

    iou = config.get("iou_threshold", 0.5)
    if isinstance(iou, bool) or not isinstance(iou, (int, float)) or not math.isfinite(iou):
        raise ValueError("iou_threshold must be a finite number within (0, 1].")
    if not 0.0 < iou <= 1.0:
        raise ValueError(f"iou_threshold must be within (0, 1]; got {iou}")
    conf = config.get("conf_threshold", 0.0)
    if isinstance(conf, bool) or not isinstance(conf, (int, float)) or not math.isfinite(conf):
        raise ValueError("conf_threshold must be a finite number within [0, 1].")
    if not 0.0 <= conf <= 1.0:
        raise ValueError(f"conf_threshold must be within [0, 1]; got {conf}")

    min_area = config.get("min_area", 0)
    if (
        isinstance(min_area, bool)
        or not isinstance(min_area, (int, float))
        or not math.isfinite(min_area)
        or min_area < 0
    ):
        raise ValueError("min_area must be a finite non-negative number.")
    class_mapping = config.get("class_mapping", {})
    if not isinstance(class_mapping, dict):
        raise ValueError("class_mapping must be a mapping.")

    thresholds = config.get("weak_thresholds") or {}
    if not isinstance(thresholds, dict):
        raise ValueError("weak_thresholds must be a mapping of class name to metric gates.")
    if not thresholds:
        warnings.append(
            "weak_thresholds is empty, so no class is gated explicitly and every class "
            "falls back to the default_* values below."
        )
    for name, gates in thresholds.items():
        if not isinstance(gates, dict):
            raise ValueError(
                f"weak_thresholds[{name!r}] must be a mapping such as {{ap50: 0.7}}; got {gates!r}"
            )
        for metric, value in gates.items():
            if metric not in METRICS:
                raise ValueError(
                    f"weak_thresholds[{name!r}] has unknown metric {metric!r}; "
                    f"expected one of {list(METRICS)}"
                )
            if (
                isinstance(value, bool)
                or not isinstance(value, (int, float))
                or not math.isfinite(value)
            ):
                raise ValueError(
                    f"weak_thresholds[{name!r}][{metric!r}] must be a finite number."
                )
            if not 0.0 <= value <= 1.0:
                raise ValueError(
                    f"weak_thresholds[{name!r}][{metric!r}] must be within [0, 1]; got {value}"
                )

    defaults: dict[str, float] = {}
    for key in DEFAULT_KEYS:
        value = config.get(key, 0.0)
        if (
            isinstance(value, bool)
            or not isinstance(value, (int, float))
            or not math.isfinite(value)
            or not 0.0 <= value <= 1.0
        ):
            raise ValueError(f"{key} must be a finite number within [0, 1].")
        defaults[key] = float(value)
    gated = [key for key, value in defaults.items() if value > 0.0]
    if gated:
        warnings.append(
            f"{', '.join(gated)} is above zero, so every class absent from weak_thresholds is "  # nosec B608 - prose warning text, not a SQL query
            "also gated. An image is weak if ANY class falls below its gate, so a class you "
            "did not list can select images on its own — which inflates the weak set and any "
            "mining budget derived from it. Set these to 0.0 unless that is intended."
        )
    return warnings


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--spec", required=True, help="Path to the gap-analysis YAML.")
    return parser.parse_args()


def main() -> int:
    try:
        spec = Path(parse_args().spec).expanduser().resolve()
        config = load_yaml(spec)
        warnings = validate_config(config)
        out = Path(str(config['results_dir'])).expanduser()

        thresholds = config.get("weak_thresholds") or {}
        print(f"OK: gap-analysis spec is valid: {spec}")
        print(f"format: {config['input_format']} | iou={config.get('iou_threshold', 0.5)} "
              f"conf={config.get('conf_threshold', 0.0)} min_area={config.get('min_area', 0)}")
        if thresholds:
            gates = ", ".join(f"{n} {dict(g)}" for n, g in thresholds.items())
            print(f"gated classes ({len(thresholds)}): {gates}")
        print("fallbacks: " + ", ".join(f"{k.replace('default_', '').replace('_threshold', '')}"
                                        f"={config.get(k, 0.0)}" for k in DEFAULT_KEYS))
        print(f"Expected output: {config['results_dir']}/weak_images.parquet")
        # The run writes four artifacts and not the spec, so a spec kept elsewhere
        # leaves the finished results with no record of what selected them.
        if spec.parent != out.resolve():
            warnings.append(
                f"the spec is outside results_dir ({spec.parent} vs {out}). The run does not "
                "copy it, so the finished artifacts will carry no record of the thresholds "
                f"that produced them. Author it at {out / spec.name} instead."
            )
        for w in warnings:
            print(f"WARNING: {w}", file=sys.stderr)
        return 0
    except Exception as exc:  # noqa: BLE001
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Needs a command-line tool, Needs Python

The author says: Requires docker, nvidia-container-toolkit, and the TAO data-services container pinned in versions.yaml.

Security

What our scanner noticed· Medium risk

  • Large base64-encoded blobs
  • References to credential files
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
NVIDIA Corporation
Publisher
NVIDIA (Official)
Source
NVIDIA/skills · skills/tao-analyze-gaps-od-map · commit fce3cca
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE-APACHE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by NVIDIA Corporation or NVIDIA.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download tao-analyze-gaps-od-map.zip