--- name: fiftyone-dataset-curation description: "End-to-end dataset curation for FiftyOne: inspect schema and quality, audit annotations, analyze class distributions, explore embeddings, find duplicates, create curated subsets, and build train/val/test splits. Works with any computer vision dataset type." --- # FiftyOne Dataset Curation End-to-end curation pipeline for any FiftyOne dataset: images, video, point clouds, grouped multimodal. Run all phases sequentially or jump directly to any single phase. ## Key Directives **ALWAYS follow these rules — no exceptions:** ### 1. Check for a loaded dataset first ``` list_datasets() ``` If none exists, offer to delegate to `fiftyone-dataset-import` skill. ### 2. Set context before any operation ``` set_context(dataset_name="") ``` ### 3. Launch App before any brain operator ``` launch_app() ``` Wait 5–10 seconds for initialization before executing delegated operators. ### 4. Discover schema before referencing any field ``` dataset_summary() get_field_schema(flat=True) ``` Never hardcode field names. Adapt all field references from schema discovery results. ### 5. Discover operators dynamically ``` list_operators(builtin_only=False) get_operator_schema(operator_uri="") ``` Always call `list_operators()` before any `execute_operator()`. Confirm the operator exists. ### 6. Confirm before mutating Before tagging, adding fields, or deleting — present findings to the user and ask for confirmation. ### 7. Keep App open for interactive review Do NOT call `close_app()` automatically. Leave the App running so the user can explore results. ### 8. Check delegated service before brain operations Before running any brain operator with `delegate=True`, verify the delegated service is running: ```bash fiftyone delegated list ``` If no services are running: ```bash fiftyone delegated launch & ``` Wait ~5 seconds for initialization. Without this service, delegated operators will queue but never execute. ### 9. Always discover plugin names dynamically Never assume a plugin's exact name or operator URI from documentation. Always verify: ``` list_plugins() ``` Use the exact plugin name from the output. Then: ``` list_operators(builtin_only=False) ``` Use the exact operator URI from the output. Documentation names may differ from installed names. ### 10. Generate interactive insights after each phase After computing any metric (uniqueness, quality, similarity, etc.): 1. Calculate counts of flagged/notable samples 2. Create named saved views for the findings 3. Load the view in the App automatically 4. Present a narrative insight to the user 5. Offer to continue to the next phase See **Insight Generation Pattern** at the bottom of this file. --- ## Phase -1 — Curation Plan **Before doing anything else**, present a complete curation plan to the user. This sets expectations and allows them to choose which phases to run. ``` list_datasets() dataset_summary() get_field_schema(flat=True) ``` Present the plan: ``` ## Curation Plan for Dataset: | samples | Label fields: Existing brain runs: ### Phases I will run: Phase 1 — Dataset Inspection (schema, tags, class distribution) Phase 2 — Data Quality Audit (metadata, resolution, file size, image quality via jacobmarks/image_issues plugin) Phase 3 — Near-Duplicate Detection (via fiftyone-find-duplicates skill) Phase 4 — Class Distribution (imbalance, coverage gaps) Phase 5 — Embedding Exploration (CLIP UMAP, gap detection) Phase 6 — Annotation Audit (mistakenness — requires predictions field) Phase 7 — Curated Subset & Splits (clean view, train/val/test) Phase 8 — Data Q&A (on-demand questions) ### Phases I will SKIP (and why): Phase 6 — No predictions field found. Run fiftyone-dataset-inference first. ### What you'll get: - Named saved views for each key finding (loadable in App) - Interactive insights at each step - A clean_ view with quality filters applied Ready to start? (yes / skip to phase X / select specific phases) ``` Wait for user confirmation before proceeding. If they want to skip phases or run only specific ones, adapt accordingly. --- ## Phase 0 — Dataset Loading (Optional) Run this phase only if no dataset is loaded yet. ``` list_datasets() ``` - If a dataset exists → `set_context(dataset_name="")` and proceed to Phase 1. - If no dataset exists → delegate to `fiftyone-dataset-import` skill. After loading: ``` dataset_summary() ``` Confirm with user: - Media type (image, video, point-cloud, group) - Sample count - Whether `persistent=True` is set --- ## Phase 1 — Dataset Inspection **MCP tools:** `dataset_summary`, `get_field_schema`, `count_values`, `distinct` ### Steps ``` dataset_summary() get_field_schema(flat=True) count_values("tags") ``` From the schema, identify label fields (Detections, Classifications, Keypoints, Segmentation, Polylines, etc.) and run: ``` distinct(".label") ``` Adapt the field path from the schema — never assume a field name. ### Report to user Present a summary with: - Media type and sample count - All field names and their types - Existing tags and counts - Unique class names per label field - Any existing brain runs (embeddings, similarity indexes, mistakenness scores) - Missing or empty fields (flag these) - **Label type awareness**: Note whether label fields are Classifications vs. Detections — this affects which export formats are valid later ### Export format note After identifying label types, inform the user: ``` Label type: Classification → supports Image Classification Directory Tree export Label type: Detection → supports COCO, YOLO, VOC export For COCO export with Classification labels → must run inference first (fiftyone-dataset-inference) to generate Detection labels ``` ### Grouped dataset detection If `dataset_summary()` reports `media_type == "group"`: 1. List available group slices from the schema 2. Recommend operating on individual sensor slices for all curation phases: ```python # Python SDK equivalent (use for user guidance) rgb_view = dataset.select_group_slices(["rgb_center"]) dataset.save_view("rgb_view", rgb_view) ``` 3. Apply all subsequent brain operations to the slice view, not the full grouped dataset 4. Use sensor-prefixed brain keys (e.g., `rgb_clip_umap`, `ir_clip_umap`) ### Existing brain runs If brain runs are reported in `dataset_summary()`: - List them and their types (similarity, visualization, mistakenness, uniqueness, etc.) - Offer to reuse existing runs instead of recomputing - Ask user which phases to run and whether to skip phases with existing results --- ## Phase 2 — Data Quality Audit See `QUALITY-CHECKS.md` for the full deep-dive workflow. **MCP tools:** `execute_operator`, `bounds`, `histogram_values`, `mean`, `std`, `count_values`, `set_view`, `list_plugins`, `download_plugin`, `enable_plugin` ### Minimum steps **Step 1: Compute metadata** ``` list_operators(builtin_only=False) get_operator_schema(operator_uri="@voxel51/utils/compute_metadata") execute_operator(operator_uri="@voxel51/utils/compute_metadata", params={}) ``` **Step 2: Check for corruption (null metadata)** ``` set_view(filters={"metadata": null}) ``` If samples appear → report count; offer to tag them as `"corrupted"` after confirmation. **Step 3: Resolution analysis** ``` bounds("metadata.width") bounds("metadata.height") histogram_values("metadata.width", bins=20) histogram_values("metadata.height", bins=20) ``` **Step 4: File size analysis** ``` bounds("metadata.size_bytes") mean("metadata.size_bytes") std("metadata.size_bytes") ``` **Step 5: Aspect ratio** Report wide/tall outliers using ViewField pattern (see QUALITY-CHECKS.md). **Step 6: Image quality via jacobmarks plugin (REQUIRED for image datasets)** This is the primary method for detecting blur, brightness issues, contrast, saturation, and aspect ratio anomalies. Do NOT use custom cv2 code. Follow the discovery workflow: ``` list_plugins() ``` Find the entry matching "image_issues" or "image-quality". Note the exact plugin name from the output — **do not assume it matches the documentation name**. If not installed: ``` download_plugin(plugin_name="jacobmarks/image-quality-issues") ``` Then run `list_plugins()` again to confirm the installed name. Enable if disabled: ``` enable_plugin(plugin_name="") ``` Discover operator URIs: ``` list_operators(builtin_only=False) ``` Find operators matching `find_issues`, `compute_blurriness`, `compute_brightness`, etc. Use exact URIs. Run each issue check (delegated, one at a time). Use the exact operator URI from `list_operators()`: ``` execute_operator( operator_uri="", # e.g. @jacobmarks/image_issues/find_issues params={"issue_mode": "SINGLE", "issue": "blurry"}, delegate=True ) ``` Wait for completion via `list_delegated_operations()` before running the next issue check. Repeat for: `low_contrast`, `low_saturation`, `weird_aspect_ratio`, `dark`, `bright` (see note on `bright` in QUALITY-CHECKS.md). After all checks complete: ``` get_field_schema(flat=True) ``` Identify the boolean flag fields added (e.g., `blurry`, `low_contrast`, `low_saturation`, `weird_aspect_ratio`). Generate interactive insights — see **Insight Generation Pattern** below. --- ## Phase 3 — Near-Duplicate Detection **Delegate to `fiftyone-find-duplicates` skill.** Tell the user: > "For near-duplicate detection, I'll use the `fiftyone-find-duplicates` skill." Recommended settings to suggest: - Model: `mobilenet-v2-imagenet-torch` (fast) or `clip-vit-base32-torch` (semantic) - Brain key: `img_sim` - Default threshold: 0.3 After duplicates are identified and optionally removed, return here for Phase 4. --- ## Phase 4 — Class Distribution & Imbalance Analysis **MCP tools:** `count_values`, `histogram_values`, `distinct`, `bounds`, `mean`, `std` For each label field discovered in Phase 1: ### Classification datasets ``` count_values(".label") ``` ### Detection datasets ``` count_values(".detections.label") histogram_values(".detections.confidence", bins=20) bounds(".detections.bounding_box") mean(".detections.confidence") ``` ### Analysis to surface - **Class imbalance**: Classes with < 5% of total samples are underrepresented — flag them - **Confidence spread**: Bimodal distribution suggests uncertain predictions - **BBox size range**: Very small bboxes (< 1% of image area) may be low-quality annotations - **Metadata distributions**: If metadata fields exist (time of day, weather, sensor type, etc.), run `count_values` on them to surface coverage gaps: ``` count_values("") ``` Adapt field paths from schema — do not assume any specific metadata fields exist. ### Coverage gaps Report under-represented attribute combinations. Example guidance: - Few night-scene samples → recommend semantic search augmentation (Phase 5) - Rare class < 20 samples → flag for annotation priority --- ## Phase 5 — Embedding Exploration & Gap Detection **Delegate to `fiftyone-embeddings-visualization` skill.** Tell the user: > "For embedding visualization and gap detection, I'll use the `fiftyone-embeddings-visualization` skill." ### Critical parameter: num_dims When calling `compute_visualization`, always include `num_dims`: ``` execute_operator( operator_uri="@voxel51/brain/compute_visualization", params={ "brain_key": "", "model": "clip-vit-base32-torch", "method": "umap", "num_dims": 2 # REQUIRED — operator fails without this }, delegate=True ) ``` ### UMAP dependency `umap-learn` must be installed in the same Python environment as FiftyOne. If it fails: ```bash # Find which python FiftyOne uses python3 -c "import fiftyone; print(fiftyone.__file__)" # Install to the correct environment /path/to/correct/python -m pip install umap-learn ``` ### Recommended models - **CLIP** (`clip-vit-base32-torch`): Semantic similarity, supports text search → best for gap detection - **DINOv2** (`dinov2-vits14-torch`): Visual similarity → best for cluster exploration ### Patch-level embeddings (detection datasets) For detection datasets, suggest patch-level embeddings after schema confirms a detections field: ``` get_operator_schema(operator_uri="@voxel51/brain/compute_visualization") execute_operator( operator_uri="@voxel51/brain/compute_visualization", params={ "patches_field": "", # from schema discovery "model": "clip-vit-base32-torch", "method": "umap", "num_dims": 2, "brain_key": "_clip_umap" }, delegate=True ) ``` ### Uniqueness computation After UMAP/similarity completes, compute uniqueness to find redundant samples: ``` execute_operator( operator_uri="@voxel51/brain/compute_uniqueness", params={}, delegate=True ) ``` After completion, generate interactive insights: ``` bounds("uniqueness") histogram_values("uniqueness", bins=20) ``` Build named views: ```python # Python SDK guidance — present these commands to user # Most redundant: bottom uniqueness scores dataset.save_view("redundant_samples", dataset.sort_by("uniqueness").limit(N)) # Most diverse: top uniqueness scores dataset.save_view("most_unique", dataset.sort_by("uniqueness", reverse=True).limit(M)) ``` Then load the insight view: ``` execute_operator("@voxel51/operators/load_saved_view", params={"name": "most_unique"}) ``` ### Semantic text search for subset discovery After CLIP similarity index exists: ``` execute_operator( operator_uri="@voxel51/brain/sort_by_similarity", params={"query": "foggy day", "k": 50, "brain_key": ""} ) tag_samples(["foggy"]) ``` Use text queries that match domain gaps found in Phase 4. --- ## Phase 6 — Annotation Audit See `ANNOTATION-AUDIT.md` for the full deep-dive workflow. **MCP tools:** `execute_operator`, `get_field_schema`, `set_view`, `tag_samples` ### Prerequisite check Check if a predictions field exists: ``` get_field_schema(flat=True) ``` If no predictions field: > "Annotation audit (mistakenness) requires a predictions field. Delegate to `fiftyone-dataset-inference` skill first, then return here." > Note: If the dataset has Classification labels and you want COCO-compatible predictions, use `yolov8n-coco-torch` from the Zoo — it generates actual Detection bounding boxes. ### Steps **Step 1: Compute mistakenness** ``` list_operators(builtin_only=False) get_operator_schema(operator_uri="@voxel51/brain/compute_mistakenness") execute_operator( operator_uri="@voxel51/brain/compute_mistakenness", params={ "pred_field": "", # from schema "label_field": "", # from schema "mistakenness_field": "mistakenness" } ) ``` **Step 2: Review high-mistakenness samples** ``` set_view(sort_by="mistakenness", reverse=True, limit=50) ``` Direct user to the FiftyOne App (use get_session_info() to get the URL) for visual review. **Step 3: Tag suspicious samples** (after user confirmation) ``` tag_samples(["annotation_review"]) ``` **Step 4: Check possible missing annotations** ``` set_view(filters={"possible_missing": {"$gt": 0}}) ``` Full annotation audit workflow in `ANNOTATION-AUDIT.md`. --- ## Phase 7 — Curated Subset Creation & Splits See `SUBSET-CREATION.md` for the full deep-dive workflow. **MCP tools:** `execute_operator`, `tag_samples`, `untag_samples`, `set_view` ### Building the clean view After quality checks and duplicate detection, construct a clean view by combining all quality filters. Use `get_field_schema(flat=True)` to discover which boolean flag fields were added by the quality plugin — never hardcode field names. Example (adapt field names from schema): ```python # Python SDK guidance — adapt field names from your schema from fiftyone import ViewField as F # Discover which quality flag fields exist via get_field_schema() # then build the filter expression from those fields only quality_filters = (F("blurry") == False) & (F("low_contrast") == False) # Add uniqueness filter if computed if "uniqueness" in field_schema: quality_filters = quality_filters & (F("uniqueness") >= 0.3) # tune threshold clean_view = dataset.match(quality_filters) dataset.save_view("clean_", clean_view) ``` Then load in App: ``` execute_operator("@voxel51/operators/load_saved_view", params={"name": "clean_"}) ``` Report to user: - Total samples before cleaning - Samples removed per quality filter - Final clean sample count and percentage ### Option A: Uniqueness-based diverse subset ``` list_operators(builtin_only=False) get_operator_schema(operator_uri="@voxel51/brain/compute_uniqueness") execute_operator(operator_uri="@voxel51/brain/compute_uniqueness", params={}) set_view(sort_by="uniqueness", reverse=True, limit=) tag_samples(["diverse"]) ``` ### Option B: Representativeness-based coreset ``` list_operators(builtin_only=False) get_operator_schema(operator_uri="@voxel51/brain/compute_representativeness") execute_operator(operator_uri="@voxel51/brain/compute_representativeness", params={}) set_view(sort_by="representativeness", reverse=True, limit=) tag_samples(["coreset"]) ``` ### Option C: Train/val/test splits ``` # Clear any existing split tags first untag_samples(["train", "val", "test"]) # Python SDK guidance for user: # import fiftyone.utils.random as four # four.random_split(dataset, {"train": 0.70, "val": 0.15, "test": 0.15}) ``` ### Saving and cloning ``` # Python SDK guidance: # dataset.save_view("curated_subset", view) # view.clone(name="_curated", persistent=True) ``` Full subset workflows in `SUBSET-CREATION.md`. --- ## Phase 8 — Data Q&A Interface Answer natural language questions about the dataset using aggregation MCP tools. **MCP tools:** `count_values`, `bounds`, `mean`, `std`, `distinct`, `histogram_values`, `dataset_summary`, `count_sample_tags` ### Question → Tool mapping | User question | MCP tool | |---------------|----------| | "How many samples have no annotations?" | `set_view(exists=[""])` then count | | "What is the class distribution?" | `count_values(".label")` | | "What % of samples are night scenes?" | `count_values(".label")` | | "What is the average bbox size?" | `mean(".detections.bounding_box")` | | "How many images are untagged?" | `count_sample_tags()` | | "What is the resolution range?" | `bounds("metadata.width")` + `bounds("metadata.height")` | | "How many duplicate samples were removed?" | `count_values("tags")` for relevant tag | | "What brain runs have been computed?" | `dataset_summary()` | | "How confident are the predictions?" | `histogram_values(".detections.confidence", bins=20)` | | "Are there any corrupted files?" | `set_view(filters={"metadata": null})` | Always adapt field paths from schema discovery — never assume field names exist. --- ## Insight Generation Pattern After any significant computation, generate and share interactive insights. This pattern applies after every phase. ### Template 1. **Compute aggregations** on the results: ``` bounds("") histogram_values("", bins=20) mean("") ``` 2. **Calculate counts** for notable subsets: ``` # Use set_view to count samples above/below thresholds set_view(filters={"": True}) # Note the count from the response ``` 3. **Create named saved views** via Python SDK (present as guidance to user): ```python # Present as Python SDK guidance dataset.save_view("insight_", ) ``` 4. **Load the view in App** using the load_saved_view operator: ``` execute_operator("@voxel51/operators/load_saved_view", params={"name": "insight_"}) ``` 5. **Present narrative insight** with counts and percentages: ``` ## Insight: Found samples () flagged as . Saved view "" is now loaded in the App. → Visit the FiftyOne App (use get_session_info() to get the URL) to inspect these samples. Would you like to: - Tag these samples as "" - Move to the next phase - Dig deeper into this finding ``` ### Example: After uniqueness computation ``` ## Insight: Dataset Redundancy Uniqueness range: (mean: ) Bottom 10% by uniqueness ( samples) = highly redundant images — visually similar to many others. View "redundant_samples" is now loaded in the App. Top 20% by uniqueness ( samples) = maximally diverse images — best candidates for a training subset. View "most_unique" is now loaded in the App. Next up: Image quality checks (Phase 2). Continue? ``` --- ## Delegation Map | Sub-task | Delegated skill | |----------|----------------| | Import dataset if none loaded | `fiftyone-dataset-import` | | Near-duplicate detection | `fiftyone-find-duplicates` | | Embedding visualization | `fiftyone-embeddings-visualization` | | Predictions for mistakenness | `fiftyone-dataset-inference` | --- ## Production Patterns (Multimodal / Grouped Datasets) ### Grouped dataset curation ```python # Select sensor slice before brain ops (Python SDK guidance) rgb_view = dataset.select_group_slices(["rgb_center"]) dataset.save_view("rgb_view", rgb_view) # Run brain ops on slice with sensor-prefixed keys # fob.compute_similarity(rgb_view, model="clip-vit-base32-torch", brain_key="rgb_img_sim") ``` ### Check if similarity index supports text search ```python # Python SDK guidance info = dataset.get_brain_info("rgb_img_sim") supports_text = info.config.supports_prompts # True for CLIP-based ``` ### Always save after field mutations ```python # Python SDK guidance view.apply_model(model, "predictions") view.save() # critical — never skip ``` ### Thumbnail generation for large datasets (> 10k samples) ```python # Python SDK guidance — improves App performance # foui.transform_images(view, size=(-1, 128), output_field="thumbnail_path", output_dir=...) # dataset.app_config.media_fields = ["filepath", "thumbnail_path"] # dataset.app_config.grid_media_field = "thumbnail_path" # dataset.save() ``` ### ViewField patterns for quality filters ```python from fiftyone import ViewField as F # Resolution F("metadata.width") > min_width F("metadata.height") > min_height # Color channels F("metadata.num_channels") == 3 # Aspect ratio filters long_filter = F("metadata.width") > 2 * F("metadata.height") tall_filter = F("metadata.height") > 2 * F("metadata.width") normal_aspect = (~long_filter) & (~tall_filter) # BBox area (relative) rel_bbox_area = F("bounding_box")[2] * F("bounding_box")[3] # Annotation quality (after mistakenness computation) F("mistakenness") > 0.95 F("possible_missing") > 0 F("max_iou") > 0.75 ``` --- ## Reference Files - `QUALITY-CHECKS.md` — Image quality filtering: metadata, blur, brightness, resolution, aspect ratio, CLIPScore - `ANNOTATION-AUDIT.md` — Annotation error detection: mistakenness, hardness, IoU dedup, side-by-side review - `SUBSET-CREATION.md` — Subset and split workflows: coreset, uniqueness, random split, saved views, cloning