Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitignore +4 -0
- README.md +40 -5
- app.py +980 -0
- packages.txt +1 -0
- requirements.txt +5 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
.env
|
README.md
CHANGED
|
@@ -1,12 +1,47 @@
|
|
| 1 |
---
|
| 2 |
-
title: DAVIS
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.9.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: DAVIS-Dataset-Explorer
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.9.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: cc-by-nc-4.0
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# DAVIS Dataset Explorer
|
| 14 |
+
|
| 15 |
+
Interactive browser for the **DAVIS 2017 Video Object Segmentation** benchmark (480p split).
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
| Tab | What it does |
|
| 20 |
+
|-----|-------------|
|
| 21 |
+
| 📋 Browse | Filter & search all 90 sequences; click a row to inspect metadata |
|
| 22 |
+
| 🔍 Viewer | Frame-by-frame scrubber with DAVIS palette mask overlay; single-sequence video playback |
|
| 23 |
+
| 🖼 Gallery | Thumbnail grid of all sequences; click a thumbnail to instantly play it |
|
| 24 |
+
| 📺 Multi-Video | Paged 3×3 video grid — page through all 90 sequences, 9 at a time |
|
| 25 |
+
| ⚖️ Compare | Up to 6 sequences side-by-side |
|
| 26 |
+
| 📊 Statistics | Distribution plots (frames, objects, splits, resolution) |
|
| 27 |
+
|
| 28 |
+
## First-run behaviour
|
| 29 |
+
|
| 30 |
+
On the first startup the app downloads **DAVIS-2017-trainval-480p.zip** (~800 MB)
|
| 31 |
+
from the official ETH Zurich server and extracts it to `DAVIS_ROOT`.
|
| 32 |
+
|
| 33 |
+
With HF Spaces **persistent storage** the data is stored under `/data/DAVIS` and
|
| 34 |
+
the MP4 cache under `/data/DAVIS_explorer_cache` — both survive restarts.
|
| 35 |
+
|
| 36 |
+
## Environment variables
|
| 37 |
+
|
| 38 |
+
| Variable | Default | Description |
|
| 39 |
+
|----------|---------|-------------|
|
| 40 |
+
| `DAVIS_ROOT` | `/data/DAVIS` (Spaces) or local path | Dataset root directory |
|
| 41 |
+
| `DAVIS_CACHE_DIR` | `DAVIS_ROOT/../DAVIS_explorer_cache` | Where encoded MP4s are stored |
|
| 42 |
+
|
| 43 |
+
## Dataset
|
| 44 |
+
|
| 45 |
+
DAVIS 2017 — *The 2017 DAVIS Challenge on Video Object Segmentation*
|
| 46 |
+
Pont-Tuset et al., arXiv:1704.00675
|
| 47 |
+
Licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/).
|
app.py
ADDED
|
@@ -0,0 +1,980 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DAVIS Dataset Explorer
|
| 3 |
+
======================
|
| 4 |
+
Interactive Gradio app for browsing, viewing and analysing the DAVIS 2017
|
| 5 |
+
video object segmentation dataset (480p split).
|
| 6 |
+
|
| 7 |
+
Usage (from repo root):
|
| 8 |
+
python scripts/davis_explorer/app.py
|
| 9 |
+
|
| 10 |
+
# Custom DAVIS root:
|
| 11 |
+
DAVIS_ROOT=/path/to/DAVIS python scripts/davis_explorer/app.py
|
| 12 |
+
|
| 13 |
+
# Public link:
|
| 14 |
+
python scripts/davis_explorer/app.py --share
|
| 15 |
+
|
| 16 |
+
Dataset layout expected:
|
| 17 |
+
<DAVIS_ROOT>/
|
| 18 |
+
JPEGImages/480p/<sequence>/%05d.jpg
|
| 19 |
+
Annotations/480p/<sequence>/%05d.png
|
| 20 |
+
ImageSets/2016/{train,val}.txt
|
| 21 |
+
ImageSets/2017/{train,val}.txt
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
import argparse
|
| 27 |
+
import os
|
| 28 |
+
import shutil
|
| 29 |
+
import subprocess
|
| 30 |
+
import threading
|
| 31 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 32 |
+
from functools import lru_cache
|
| 33 |
+
from pathlib import Path
|
| 34 |
+
|
| 35 |
+
import gradio as gr
|
| 36 |
+
import numpy as np
|
| 37 |
+
import pandas as pd
|
| 38 |
+
import plotly.express as px
|
| 39 |
+
from PIL import Image
|
| 40 |
+
|
| 41 |
+
# ── Configuration ──────────────────────────────────────────────────────────────
|
| 42 |
+
|
| 43 |
+
# Official ETH Zurich download — DAVIS 2017 trainval 480p (~800 MB zipped).
|
| 44 |
+
# The zip extracts to a top-level DAVIS/ directory.
|
| 45 |
+
DAVIS_ZIP_URL = (
|
| 46 |
+
"https://data.vision.ee.ethz.ch/csergi/share/davis/"
|
| 47 |
+
"DAVIS-2017-trainval-480p.zip"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
IS_HF_SPACE = bool(os.environ.get("SPACE_ID"))
|
| 51 |
+
|
| 52 |
+
# Path resolution:
|
| 53 |
+
# • HF Spaces with persistent storage → /data/DAVIS
|
| 54 |
+
# • HF Spaces without persistent storage → /tmp/DAVIS
|
| 55 |
+
# • Local → workspace path (or DAVIS_ROOT env var)
|
| 56 |
+
if IS_HF_SPACE:
|
| 57 |
+
_hf_base = Path("/data") if Path("/data").exists() else Path("/tmp")
|
| 58 |
+
_local_root = _hf_base / "DAVIS"
|
| 59 |
+
else:
|
| 60 |
+
_local_root = Path("/workspace/diffusion-research/data/raw/DAVIS")
|
| 61 |
+
|
| 62 |
+
DAVIS_ROOT = Path(os.environ.get("DAVIS_ROOT", str(_local_root)))
|
| 63 |
+
|
| 64 |
+
IMG_DIR = DAVIS_ROOT / "JPEGImages" / "480p"
|
| 65 |
+
ANN_DIR = DAVIS_ROOT / "Annotations" / "480p"
|
| 66 |
+
SETS_DIR = DAVIS_ROOT / "ImageSets"
|
| 67 |
+
|
| 68 |
+
# Cache lives as a sibling of DAVIS_ROOT so the path is always valid.
|
| 69 |
+
CACHE_DIR = Path(os.environ.get(
|
| 70 |
+
"DAVIS_CACHE_DIR",
|
| 71 |
+
str(DAVIS_ROOT.parent / "DAVIS_explorer_cache"),
|
| 72 |
+
))
|
| 73 |
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
| 74 |
+
|
| 75 |
+
DAVIS_PALETTE = np.array([
|
| 76 |
+
[ 0, 0, 0], [128, 0, 0], [ 0, 128, 0], [128, 128, 0],
|
| 77 |
+
[ 0, 0, 128], [128, 0, 128], [ 0, 128, 128], [128, 128, 128],
|
| 78 |
+
[ 64, 0, 0], [192, 0, 0], [ 64, 128, 0], [192, 128, 0],
|
| 79 |
+
[ 64, 0, 128], [192, 0, 128], [ 64, 128, 128], [192, 128, 128],
|
| 80 |
+
[ 0, 64, 0], [128, 64, 0], [ 0, 192, 0], [128, 192, 0],
|
| 81 |
+
], dtype=np.uint8)
|
| 82 |
+
|
| 83 |
+
DEFAULT_FPS = 24
|
| 84 |
+
DEFAULT_ALPHA = 0.55
|
| 85 |
+
DEFAULT_CRF = 18
|
| 86 |
+
MAX_COMPARE = 6 # slots in the Compare tab
|
| 87 |
+
PAGE_SIZE = 9 # videos per page in Multi-Video tab
|
| 88 |
+
THUMB_W, THUMB_H = 320, 200 # thumbnail dimensions for Gallery
|
| 89 |
+
|
| 90 |
+
# ── Dataset download ───────────────────────────────────────────────────────────
|
| 91 |
+
|
| 92 |
+
def ensure_dataset() -> None:
|
| 93 |
+
"""Download and extract DAVIS 2017 trainval (480p) if not already present.
|
| 94 |
+
|
| 95 |
+
Safe to call every startup — exits immediately when data is found.
|
| 96 |
+
The zip extracts into a top-level ``DAVIS/`` directory, so we extract
|
| 97 |
+
into ``DAVIS_ROOT.parent`` which gives the expected ``DAVIS_ROOT`` layout.
|
| 98 |
+
"""
|
| 99 |
+
if IMG_DIR.exists() and any(IMG_DIR.iterdir()):
|
| 100 |
+
return # data already present
|
| 101 |
+
|
| 102 |
+
import urllib.request
|
| 103 |
+
import zipfile
|
| 104 |
+
|
| 105 |
+
DAVIS_ROOT.mkdir(parents=True, exist_ok=True)
|
| 106 |
+
zip_dst = DAVIS_ROOT.parent / "_davis_download.zip"
|
| 107 |
+
|
| 108 |
+
print(f"DAVIS dataset not found at {DAVIS_ROOT}")
|
| 109 |
+
print(f"Downloading {DAVIS_ZIP_URL} (~800 MB) …")
|
| 110 |
+
|
| 111 |
+
_last_pct: list[int] = [-1]
|
| 112 |
+
|
| 113 |
+
def _progress(count: int, block: int, total: int) -> None:
|
| 114 |
+
pct = min(100, int(count * block / total * 100))
|
| 115 |
+
if pct != _last_pct[0] and pct % 5 == 0:
|
| 116 |
+
bar = "█" * (pct // 5) + "░" * (20 - pct // 5)
|
| 117 |
+
print(f" [{bar}] {pct:3d}%", end="\r", flush=True)
|
| 118 |
+
_last_pct[0] = pct
|
| 119 |
+
|
| 120 |
+
try:
|
| 121 |
+
urllib.request.urlretrieve(DAVIS_ZIP_URL, zip_dst, _progress)
|
| 122 |
+
except Exception as exc:
|
| 123 |
+
zip_dst.unlink(missing_ok=True)
|
| 124 |
+
raise RuntimeError(f"Download failed: {exc}") from exc
|
| 125 |
+
|
| 126 |
+
print(f"\n Download complete ({zip_dst.stat().st_size // 1_048_576} MB). Extracting…")
|
| 127 |
+
|
| 128 |
+
with zipfile.ZipFile(zip_dst, "r") as zf:
|
| 129 |
+
zf.extractall(DAVIS_ROOT.parent)
|
| 130 |
+
|
| 131 |
+
zip_dst.unlink(missing_ok=True)
|
| 132 |
+
|
| 133 |
+
if not IMG_DIR.exists():
|
| 134 |
+
raise RuntimeError(
|
| 135 |
+
f"Extraction failed — expected {IMG_DIR} not found. "
|
| 136 |
+
"Check that the zip contains a top-level DAVIS/ directory."
|
| 137 |
+
)
|
| 138 |
+
print(f" DAVIS dataset ready at {DAVIS_ROOT}")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# ── Dataset loading ─────��──────────────────────────────────────────────────────
|
| 142 |
+
|
| 143 |
+
def _read_split(year: str, split: str) -> list[str]:
|
| 144 |
+
p = SETS_DIR / year / f"{split}.txt"
|
| 145 |
+
return p.read_text().strip().splitlines() if p.exists() else []
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def _count_objects(seq: str) -> int:
|
| 149 |
+
ann_seq = ANN_DIR / seq
|
| 150 |
+
if not ann_seq.exists():
|
| 151 |
+
return 0
|
| 152 |
+
files = sorted(ann_seq.iterdir())
|
| 153 |
+
return int(np.max(np.array(Image.open(files[0])))) if files else 0
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def build_dataframe() -> pd.DataFrame:
|
| 157 |
+
seqs = sorted(d.name for d in IMG_DIR.iterdir() if d.is_dir())
|
| 158 |
+
s16_train = set(_read_split("2016", "train"))
|
| 159 |
+
s16_val = set(_read_split("2016", "val"))
|
| 160 |
+
s17_train = set(_read_split("2017", "train"))
|
| 161 |
+
s17_val = set(_read_split("2017", "val"))
|
| 162 |
+
rows = []
|
| 163 |
+
for seq in seqs:
|
| 164 |
+
imgs = sorted((IMG_DIR / seq).glob("*.jpg"))
|
| 165 |
+
n = len(imgs)
|
| 166 |
+
n_obj = _count_objects(seq)
|
| 167 |
+
w, h = Image.open(imgs[0]).size if imgs else (0, 0)
|
| 168 |
+
in16t, in16v = seq in s16_train, seq in s16_val
|
| 169 |
+
in17t, in17v = seq in s17_train, seq in s17_val
|
| 170 |
+
splits = (["2016-train"] * in16t + ["2016-val"] * in16v +
|
| 171 |
+
["2017-train"] * in17t + ["2017-val"] * in17v)
|
| 172 |
+
rows.append({
|
| 173 |
+
"sequence": seq, "frames": n, "n_objects": n_obj,
|
| 174 |
+
"width": w, "height": h, "resolution": f"{w}×{h}",
|
| 175 |
+
"split": ", ".join(splits) or "unlisted",
|
| 176 |
+
"in_2016": in16t or in16v, "in_2017": in17t or in17v,
|
| 177 |
+
"in_train": in16t or in17t, "in_val": in16v or in17v,
|
| 178 |
+
})
|
| 179 |
+
return pd.DataFrame(rows)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
ensure_dataset()
|
| 183 |
+
print("Loading DAVIS metadata…")
|
| 184 |
+
DF = build_dataframe()
|
| 185 |
+
ALL_SEQUENCES = sorted(DF["sequence"].tolist())
|
| 186 |
+
print(f" {len(DF)} sequences · frames {DF['frames'].min()}–{DF['frames'].max()} "
|
| 187 |
+
f"· objects {DF['n_objects'].min()}–{DF['n_objects'].max()}")
|
| 188 |
+
|
| 189 |
+
DISPLAY_COLS = ["sequence", "frames", "n_objects", "resolution", "split"]
|
| 190 |
+
|
| 191 |
+
# ── Frame helpers ──────────────────────────────────────────────────────────────
|
| 192 |
+
|
| 193 |
+
@lru_cache(maxsize=16)
|
| 194 |
+
def _get_frame_paths(seq: str) -> list[Path]:
|
| 195 |
+
return sorted((IMG_DIR / seq).glob("*.jpg"))
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@lru_cache(maxsize=16)
|
| 199 |
+
def _get_ann_paths(seq: str) -> list[Path]:
|
| 200 |
+
d = ANN_DIR / seq
|
| 201 |
+
return sorted(d.glob("*.png")) if d.exists() else []
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _blend(img_f32: np.ndarray, ann: np.ndarray, alpha: float) -> np.ndarray:
|
| 205 |
+
ov = DAVIS_PALETTE[np.clip(ann, 0, len(DAVIS_PALETTE) - 1)].astype(np.float32)
|
| 206 |
+
a = np.where(ann == 0, 0.0, alpha).astype(np.float32)[:, :, None]
|
| 207 |
+
return (img_f32 * (1 - a) + ov * a).clip(0, 255).astype(np.uint8)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def render_frame(seq: str, idx: int, overlay: bool, alpha: float) -> Image.Image:
|
| 211 |
+
fps = _get_frame_paths(seq)
|
| 212 |
+
if not fps:
|
| 213 |
+
return Image.new("RGB", (854, 480), 20)
|
| 214 |
+
idx = min(max(0, idx), len(fps) - 1)
|
| 215 |
+
arr = np.array(Image.open(fps[idx]).convert("RGB"), dtype=np.float32)
|
| 216 |
+
if overlay:
|
| 217 |
+
anns = _get_ann_paths(seq)
|
| 218 |
+
if idx < len(anns):
|
| 219 |
+
arr = _blend(arr, np.array(Image.open(anns[idx])), alpha).astype(np.float32)
|
| 220 |
+
return Image.fromarray(arr.clip(0, 255).astype(np.uint8))
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def render_mask(seq: str, idx: int) -> Image.Image:
|
| 224 |
+
anns = _get_ann_paths(seq)
|
| 225 |
+
if not anns:
|
| 226 |
+
return Image.new("RGB", (854, 480), 20)
|
| 227 |
+
idx = min(max(0, idx), len(anns) - 1)
|
| 228 |
+
ann = np.array(Image.open(anns[idx]))
|
| 229 |
+
rgb = np.zeros((*ann.shape, 3), dtype=np.uint8)
|
| 230 |
+
for oid in range(1, len(DAVIS_PALETTE)):
|
| 231 |
+
m = ann == oid
|
| 232 |
+
if m.any():
|
| 233 |
+
rgb[m] = DAVIS_PALETTE[oid]
|
| 234 |
+
return Image.fromarray(rgb)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# ── MP4 helpers ────────────────────────────────────────────────────────────────
|
| 238 |
+
|
| 239 |
+
def _mp4_path(seq: str, overlay: bool, alpha: float, fps: int) -> Path:
|
| 240 |
+
tag = f"ov{int(alpha * 100):03d}" if overlay else "raw"
|
| 241 |
+
return CACHE_DIR / f"{seq}_{tag}_{fps}fps.mp4"
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _ffmpeg(pattern: str, out: Path, fps: int) -> None:
|
| 245 |
+
cmd = ["ffmpeg", "-y", "-framerate", str(fps), "-i", pattern,
|
| 246 |
+
"-c:v", "libx264", "-preset", "fast", "-pix_fmt", "yuv420p",
|
| 247 |
+
"-crf", str(DEFAULT_CRF), "-movflags", "+faststart",
|
| 248 |
+
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", str(out)]
|
| 249 |
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
| 250 |
+
if r.returncode != 0:
|
| 251 |
+
raise RuntimeError(r.stderr[-600:])
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def encode_sequence(seq: str, overlay: bool, alpha: float, fps: int) -> Path:
|
| 255 |
+
out = _mp4_path(seq, overlay, round(alpha, 2), fps)
|
| 256 |
+
if out.exists():
|
| 257 |
+
return out
|
| 258 |
+
fps_paths = _get_frame_paths(seq)
|
| 259 |
+
if not fps_paths:
|
| 260 |
+
raise FileNotFoundError(f"No frames for {seq}")
|
| 261 |
+
if not overlay:
|
| 262 |
+
_ffmpeg(str(IMG_DIR / seq / "%05d.jpg"), out, fps)
|
| 263 |
+
return out
|
| 264 |
+
anns = _get_ann_paths(seq)
|
| 265 |
+
tmp = CACHE_DIR / f"_tmp_{seq}_{int(alpha*100):03d}"
|
| 266 |
+
tmp.mkdir(exist_ok=True)
|
| 267 |
+
try:
|
| 268 |
+
for i, fp in enumerate(fps_paths):
|
| 269 |
+
arr = np.array(Image.open(fp).convert("RGB"), dtype=np.float32)
|
| 270 |
+
if i < len(anns):
|
| 271 |
+
arr = _blend(arr, np.array(Image.open(anns[i])), alpha).astype(np.float32)
|
| 272 |
+
Image.fromarray(arr.clip(0, 255).astype(np.uint8)).save(
|
| 273 |
+
tmp / f"{i:05d}.png", optimize=False)
|
| 274 |
+
_ffmpeg(str(tmp / "%05d.png"), out, fps)
|
| 275 |
+
finally:
|
| 276 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 277 |
+
return out
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def get_video(seq: str, overlay: bool, alpha: float, fps: int) -> tuple[str | None, str]:
|
| 281 |
+
if not seq or seq not in ALL_SEQUENCES:
|
| 282 |
+
return None, "No sequence selected."
|
| 283 |
+
try:
|
| 284 |
+
p = encode_sequence(seq, overlay, round(alpha, 2), fps)
|
| 285 |
+
n = int(DF[DF["sequence"] == seq].iloc[0]["frames"])
|
| 286 |
+
size = p.stat().st_size // 1024
|
| 287 |
+
mode = "overlay" if overlay else "raw"
|
| 288 |
+
return str(p), f"✅ **{seq}** · {n} frames · {fps} fps · {mode} · {size} KB"
|
| 289 |
+
except Exception as e:
|
| 290 |
+
return None, f"❌ {e}"
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
# ── Background pre-cache ───────────────────────────────────────────────────────
|
| 294 |
+
|
| 295 |
+
_cache_progress: dict[str, str] = {}
|
| 296 |
+
_cache_lock = threading.Lock()
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def _precache_worker(seq: str, fps: int) -> None:
|
| 300 |
+
with _cache_lock:
|
| 301 |
+
_cache_progress[seq] = "encoding…"
|
| 302 |
+
try:
|
| 303 |
+
encode_sequence(seq, False, DEFAULT_ALPHA, fps)
|
| 304 |
+
encode_sequence(seq, True, DEFAULT_ALPHA, fps)
|
| 305 |
+
with _cache_lock:
|
| 306 |
+
_cache_progress[seq] = "done"
|
| 307 |
+
except Exception as e:
|
| 308 |
+
with _cache_lock:
|
| 309 |
+
_cache_progress[seq] = f"error: {e}"
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def start_precache(fps: int = DEFAULT_FPS, workers: int = 4) -> None:
|
| 313 |
+
missing = [s for s in ALL_SEQUENCES
|
| 314 |
+
if not _mp4_path(s, False, DEFAULT_ALPHA, fps).exists()
|
| 315 |
+
or not _mp4_path(s, True, DEFAULT_ALPHA, fps).exists()]
|
| 316 |
+
if not missing:
|
| 317 |
+
print(f" MP4 cache complete ({len(ALL_SEQUENCES)}×2 already exist)")
|
| 318 |
+
for s in ALL_SEQUENCES:
|
| 319 |
+
_cache_progress[s] = "done"
|
| 320 |
+
return
|
| 321 |
+
print(f" Pre-caching {len(missing)} sequences (workers={workers})…")
|
| 322 |
+
def _run():
|
| 323 |
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
| 324 |
+
futs = {pool.submit(_precache_worker, s, fps): s for s in missing}
|
| 325 |
+
done = 0
|
| 326 |
+
for f in as_completed(futs):
|
| 327 |
+
done += 1
|
| 328 |
+
s = futs[f]
|
| 329 |
+
if done % 10 == 0 or done == len(missing):
|
| 330 |
+
print(f" Cache {done}/{len(missing)} ({s}: {_cache_progress.get(s)})")
|
| 331 |
+
threading.Thread(target=_run, daemon=True).start()
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
# ── Gallery helpers ────────────────────────────────────────────────────────────
|
| 335 |
+
|
| 336 |
+
def _make_thumb(seq: str, overlay: bool = False, alpha: float = 0.0) -> Image.Image:
|
| 337 |
+
fps = _get_frame_paths(seq)
|
| 338 |
+
if not fps:
|
| 339 |
+
return Image.new("RGB", (THUMB_W, THUMB_H), 30)
|
| 340 |
+
img = render_frame(seq, 0, overlay, alpha) if overlay else Image.open(fps[0]).convert("RGB")
|
| 341 |
+
img = img.copy()
|
| 342 |
+
img.thumbnail((THUMB_W, THUMB_H), Image.LANCZOS)
|
| 343 |
+
return img
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def build_gallery_items(seqs: list[str], overlay: bool = False) -> list[tuple]:
|
| 347 |
+
items = []
|
| 348 |
+
for seq in seqs:
|
| 349 |
+
row = DF[DF["sequence"] == seq].iloc[0]
|
| 350 |
+
caption = f"{seq} [{row['frames']}f · {row['n_objects']}obj]"
|
| 351 |
+
items.append((_make_thumb(seq, overlay), caption))
|
| 352 |
+
return items
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
print("Building gallery thumbnails…")
|
| 356 |
+
_ALL_THUMBS: list[tuple] = build_gallery_items(ALL_SEQUENCES)
|
| 357 |
+
print(" Done.")
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
# ── Filter helpers ─────────────────────────────────────────────────────────────
|
| 361 |
+
|
| 362 |
+
def filter_df(year_f, split_f, obj_f, fmin, fmax, search) -> pd.DataFrame:
|
| 363 |
+
d = DF.copy()
|
| 364 |
+
if year_f == "2016 only": d = d[d["in_2016"]]
|
| 365 |
+
elif year_f == "2017 only": d = d[d["in_2017"]]
|
| 366 |
+
if split_f == "Train only": d = d[d["in_train"]]
|
| 367 |
+
elif split_f == "Val only": d = d[d["in_val"]]
|
| 368 |
+
if obj_f == "1 object": d = d[d["n_objects"] == 1]
|
| 369 |
+
elif obj_f == "2 objects": d = d[d["n_objects"] == 2]
|
| 370 |
+
elif obj_f == "3+ objects": d = d[d["n_objects"] >= 3]
|
| 371 |
+
d = d[(d["frames"] >= fmin) & (d["frames"] <= fmax)]
|
| 372 |
+
if search.strip():
|
| 373 |
+
d = d[d["sequence"].str.lower().str.contains(search.strip().lower(), na=False)]
|
| 374 |
+
return d[DISPLAY_COLS].reset_index(drop=True)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def _seq_info(seq: str) -> str:
|
| 378 |
+
if seq not in ALL_SEQUENCES:
|
| 379 |
+
return ""
|
| 380 |
+
r = DF[DF["sequence"] == seq].iloc[0]
|
| 381 |
+
return (f"**{seq}** �� {r['frames']} frames · {r['n_objects']} obj · "
|
| 382 |
+
f"{r['resolution']} · _{r['split']}_")
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def get_legend(seq: str) -> str:
|
| 386 |
+
if seq not in ALL_SEQUENCES:
|
| 387 |
+
return ""
|
| 388 |
+
n = int(DF[DF["sequence"] == seq].iloc[0]["n_objects"])
|
| 389 |
+
if n == 0:
|
| 390 |
+
return "*No annotated objects.*"
|
| 391 |
+
lines = ["**Objects:**"]
|
| 392 |
+
for i in range(1, min(n + 1, len(DAVIS_PALETTE))):
|
| 393 |
+
hx = "#{:02X}{:02X}{:02X}".format(*DAVIS_PALETTE[i])
|
| 394 |
+
lines.append(f"- <span style='color:{hx};font-weight:bold'>■</span> Object {i}")
|
| 395 |
+
return "\n".join(lines)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def cache_status_md() -> str:
|
| 399 |
+
with _cache_lock:
|
| 400 |
+
done = sum(1 for v in _cache_progress.values() if v == "done")
|
| 401 |
+
total = len(ALL_SEQUENCES)
|
| 402 |
+
pct = done / total * 100 if total else 0
|
| 403 |
+
bar = "█" * int(pct / 5) + "░" * (20 - int(pct / 5))
|
| 404 |
+
return f"`[{bar}]` **{done}/{total}** cached ({pct:.0f}%)"
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
# ── Stats plots ────────────────────────────────────────────────────────────────
|
| 408 |
+
|
| 409 |
+
def make_stats_plots():
|
| 410 |
+
d = DF.copy()
|
| 411 |
+
fig_frames = px.histogram(d, x="frames", nbins=30, title="Frame Count Distribution",
|
| 412 |
+
color_discrete_sequence=["#3B82F6"], labels={"frames": "Frames"})
|
| 413 |
+
fig_frames.update_layout(margin=dict(t=45, b=40))
|
| 414 |
+
|
| 415 |
+
oc = d["n_objects"].value_counts().sort_index().reset_index()
|
| 416 |
+
oc.columns = ["n_objects", "count"]
|
| 417 |
+
fig_objs = px.bar(oc, x="n_objects", y="count", title="Sequences by Object Count",
|
| 418 |
+
color="count", color_continuous_scale="Teal",
|
| 419 |
+
labels={"n_objects": "Objects", "count": "# Sequences"})
|
| 420 |
+
fig_objs.update_layout(coloraxis_showscale=False, margin=dict(t=45, b=40))
|
| 421 |
+
fig_objs.update_xaxes(tickmode="linear", dtick=1)
|
| 422 |
+
|
| 423 |
+
sp = {"2016-train": int(d["split"].str.contains("2016-train").sum()),
|
| 424 |
+
"2016-val": int(d["split"].str.contains("2016-val").sum()),
|
| 425 |
+
"2017-train": int(d["split"].str.contains("2017-train").sum()),
|
| 426 |
+
"2017-val": int(d["split"].str.contains("2017-val").sum())}
|
| 427 |
+
fig_splits = px.bar(x=list(sp.keys()), y=list(sp.values()), title="Sequences per Split",
|
| 428 |
+
color=list(sp.keys()),
|
| 429 |
+
color_discrete_sequence=["#3B82F6","#6366F1","#F59E0B","#EF4444"],
|
| 430 |
+
labels={"x": "Split", "y": "# Sequences"})
|
| 431 |
+
fig_splits.update_layout(showlegend=False, margin=dict(t=45, b=40))
|
| 432 |
+
|
| 433 |
+
rc = d["resolution"].value_counts().reset_index()
|
| 434 |
+
rc.columns = ["resolution", "count"]
|
| 435 |
+
fig_res = px.pie(rc, names="resolution", values="count", title="Resolution Distribution",
|
| 436 |
+
color_discrete_sequence=px.colors.qualitative.Pastel)
|
| 437 |
+
fig_res.update_layout(margin=dict(t=45, b=20))
|
| 438 |
+
|
| 439 |
+
fig_scatter = px.scatter(d, x="frames", y="n_objects", text="sequence",
|
| 440 |
+
title="Frames vs. Object Count",
|
| 441 |
+
color="n_objects", color_continuous_scale="Viridis",
|
| 442 |
+
size="frames", size_max=18,
|
| 443 |
+
labels={"frames": "Frames", "n_objects": "Objects"},
|
| 444 |
+
hover_data=["sequence", "frames", "n_objects", "resolution", "split"])
|
| 445 |
+
fig_scatter.update_traces(textposition="top center", textfont_size=8)
|
| 446 |
+
fig_scatter.update_layout(coloraxis_showscale=False, margin=dict(t=45, b=40))
|
| 447 |
+
|
| 448 |
+
return fig_frames, fig_objs, fig_splits, fig_res, fig_scatter
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
# ── Build UI ───────────────────────────────────────────────────────────────────
|
| 452 |
+
|
| 453 |
+
def build_ui():
|
| 454 |
+
figs = make_stats_plots()
|
| 455 |
+
n_multi = int((DF["n_objects"] > 1).sum())
|
| 456 |
+
n_2016 = int(DF["in_2016"].sum())
|
| 457 |
+
n_2017 = int(DF["in_2017"].sum())
|
| 458 |
+
_first = ALL_SEQUENCES[0]
|
| 459 |
+
_first_n = len(_get_frame_paths(_first))
|
| 460 |
+
total_pages = (len(ALL_SEQUENCES) + PAGE_SIZE - 1) // PAGE_SIZE
|
| 461 |
+
|
| 462 |
+
with gr.Blocks(title="DAVIS Dataset Explorer") as demo:
|
| 463 |
+
|
| 464 |
+
gr.Markdown(
|
| 465 |
+
"# 🎬 DAVIS Dataset Explorer\n"
|
| 466 |
+
f"**DAVIS 2017 · 480p** — {len(DF)} sequences · "
|
| 467 |
+
f"frames {DF['frames'].min()}–{DF['frames'].max()} · "
|
| 468 |
+
f"{n_2016} in DAVIS-2016 · {n_2017} in DAVIS-2017 · "
|
| 469 |
+
f"{n_multi} multi-object"
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
with gr.Tabs():
|
| 473 |
+
|
| 474 |
+
# ──────────────────────────────────────────────────────────────
|
| 475 |
+
# Tab 1 · Browse
|
| 476 |
+
# ──────────────────────────────────────────────────────────────
|
| 477 |
+
with gr.TabItem("📋 Browse"):
|
| 478 |
+
with gr.Row():
|
| 479 |
+
dd_year = gr.Dropdown(["All years","2016 only","2017 only"],
|
| 480 |
+
value="All years", label="Year", scale=1)
|
| 481 |
+
dd_split = gr.Dropdown(["All splits","Train only","Val only"],
|
| 482 |
+
value="All splits", label="Split", scale=1)
|
| 483 |
+
dd_obj = gr.Dropdown(["Any # objects","1 object","2 objects","3+ objects"],
|
| 484 |
+
value="Any # objects", label="Objects", scale=1)
|
| 485 |
+
txt_srch = gr.Textbox(placeholder="Search…", label="Search", scale=2)
|
| 486 |
+
with gr.Row():
|
| 487 |
+
fmin_sl = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
|
| 488 |
+
int(DF["frames"].min()), step=1, label="Min frames", scale=3)
|
| 489 |
+
fmax_sl = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
|
| 490 |
+
int(DF["frames"].max()), step=1, label="Max frames", scale=3)
|
| 491 |
+
count_md = gr.Markdown(f"**{len(DF)} sequences** match.")
|
| 492 |
+
with gr.Row(equal_height=False):
|
| 493 |
+
with gr.Column(scale=3):
|
| 494 |
+
tbl = gr.DataFrame(value=DF[DISPLAY_COLS], interactive=False, wrap=False)
|
| 495 |
+
with gr.Column(scale=2):
|
| 496 |
+
detail_md = gr.Markdown("*Select a row to see details.*")
|
| 497 |
+
|
| 498 |
+
filtered_state = gr.State(DF[DISPLAY_COLS].copy())
|
| 499 |
+
selected_seq = gr.State("")
|
| 500 |
+
f_inputs = [dd_year, dd_split, dd_obj, fmin_sl, fmax_sl, txt_srch]
|
| 501 |
+
|
| 502 |
+
def _on_filter(*a):
|
| 503 |
+
df = filter_df(*a)
|
| 504 |
+
return df, f"**{len(df)} sequences** match."
|
| 505 |
+
for inp in f_inputs:
|
| 506 |
+
inp.change(_on_filter, f_inputs, [tbl, count_md])
|
| 507 |
+
inp.change(lambda *a: filter_df(*a), f_inputs, filtered_state)
|
| 508 |
+
|
| 509 |
+
def _on_row(evt: gr.SelectData, fdf):
|
| 510 |
+
if evt is None or fdf is None or len(fdf) == 0:
|
| 511 |
+
return gr.update(), "Select a row."
|
| 512 |
+
seq = fdf.iloc[evt.index[0]]["sequence"]
|
| 513 |
+
r = DF[DF["sequence"] == seq].iloc[0]
|
| 514 |
+
sc = r["split"].replace(", ", "\n• ")
|
| 515 |
+
md = (f"### `{seq}`\n| Field | Value |\n|---|---|\n"
|
| 516 |
+
f"| Frames | **{r['frames']}** |\n"
|
| 517 |
+
f"| Objects | **{r['n_objects']}** |\n"
|
| 518 |
+
f"| Resolution | {r['resolution']} |\n"
|
| 519 |
+
f"| Splits | • {sc} |\n\n"
|
| 520 |
+
f"> Open the **Viewer** or **Gallery** tab to watch.")
|
| 521 |
+
return seq, md
|
| 522 |
+
tbl.select(_on_row, filtered_state, [selected_seq, detail_md])
|
| 523 |
+
|
| 524 |
+
# ──────────────────────────────────────────────────────────────
|
| 525 |
+
# Tab 2 · Viewer (frame scrubber + single video)
|
| 526 |
+
# ──────────────────────────────────────────────────────────────
|
| 527 |
+
with gr.TabItem("🔍 Viewer"):
|
| 528 |
+
with gr.Row():
|
| 529 |
+
seq_dd = gr.Dropdown(ALL_SEQUENCES, value=_first,
|
| 530 |
+
label="Sequence", scale=5)
|
| 531 |
+
seq_info_md = gr.Markdown(_seq_info(_first))
|
| 532 |
+
|
| 533 |
+
gr.Markdown("#### Frame Scrubber")
|
| 534 |
+
with gr.Row():
|
| 535 |
+
ov_cb = gr.Checkbox(value=True, label="Mask overlay")
|
| 536 |
+
alpha_sl = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
|
| 537 |
+
label="Overlay opacity")
|
| 538 |
+
frame_sl = gr.Slider(0, _first_n - 1, 0, step=1,
|
| 539 |
+
label=f"Frame (0 – {_first_n - 1})")
|
| 540 |
+
with gr.Row():
|
| 541 |
+
img_out = gr.Image(label="Frame (+overlay)", type="pil", height=360,
|
| 542 |
+
value=render_frame(_first, 0, True, DEFAULT_ALPHA))
|
| 543 |
+
ann_out = gr.Image(label="Annotation mask", type="pil", height=360,
|
| 544 |
+
value=render_mask(_first, 0))
|
| 545 |
+
legend_md = gr.Markdown(get_legend(_first))
|
| 546 |
+
|
| 547 |
+
gr.Markdown("---\n#### Video Playback")
|
| 548 |
+
gr.Markdown(
|
| 549 |
+
"Raw encodes directly from JPEGs (instant). "
|
| 550 |
+
"Overlay uses vectorised numpy. Both variants are cached permanently."
|
| 551 |
+
)
|
| 552 |
+
with gr.Row():
|
| 553 |
+
v_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1, label="FPS", scale=2)
|
| 554 |
+
v_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
|
| 555 |
+
v_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
|
| 556 |
+
label="Overlay opacity", scale=2)
|
| 557 |
+
with gr.Row():
|
| 558 |
+
btn_play = gr.Button("▶ Generate & Play", variant="primary", scale=1)
|
| 559 |
+
with gr.Column(scale=4):
|
| 560 |
+
v_status = gr.Markdown("*Click Generate & Play.*")
|
| 561 |
+
video_out = gr.Video(label="Playback", height=390, autoplay=True)
|
| 562 |
+
cache_md = gr.Markdown(cache_status_md())
|
| 563 |
+
gr.Button("↻ Refresh cache status", size="sm").click(
|
| 564 |
+
cache_status_md, outputs=cache_md)
|
| 565 |
+
|
| 566 |
+
# wiring
|
| 567 |
+
selected_seq.change(
|
| 568 |
+
lambda s: gr.Dropdown(value=s) if s and s in ALL_SEQUENCES else gr.Dropdown(),
|
| 569 |
+
selected_seq, seq_dd)
|
| 570 |
+
|
| 571 |
+
def _on_seq(seq):
|
| 572 |
+
if seq not in ALL_SEQUENCES:
|
| 573 |
+
return gr.Slider(), None, None, "", ""
|
| 574 |
+
n = len(_get_frame_paths(seq))
|
| 575 |
+
fi = render_frame(seq, 0, True, DEFAULT_ALPHA)
|
| 576 |
+
ai = render_mask(seq, 0)
|
| 577 |
+
sl = gr.Slider(minimum=0, maximum=n-1, value=0, step=1,
|
| 578 |
+
label=f"Frame (0 – {n-1})")
|
| 579 |
+
return sl, fi, ai, _seq_info(seq), get_legend(seq)
|
| 580 |
+
|
| 581 |
+
seq_dd.change(_on_seq, seq_dd,
|
| 582 |
+
[frame_sl, img_out, ann_out, seq_info_md, legend_md])
|
| 583 |
+
seq_dd.change(lambda *_: (None, "*Click Generate & Play.*"),
|
| 584 |
+
seq_dd, [video_out, v_status])
|
| 585 |
+
|
| 586 |
+
def _fr(seq, idx, ov, a):
|
| 587 |
+
return render_frame(seq, int(idx), ov, a), render_mask(seq, int(idx))
|
| 588 |
+
frame_sl.change(_fr, [seq_dd, frame_sl, ov_cb, alpha_sl], [img_out, ann_out])
|
| 589 |
+
ov_cb.change(_fr, [seq_dd, frame_sl, ov_cb, alpha_sl], [img_out, ann_out])
|
| 590 |
+
alpha_sl.change(_fr, [seq_dd, frame_sl, ov_cb, alpha_sl], [img_out, ann_out])
|
| 591 |
+
btn_play.click(get_video, [seq_dd, v_ov, v_a, v_fps], [video_out, v_status])
|
| 592 |
+
|
| 593 |
+
# ──────────────────────────────────────────────────────────────
|
| 594 |
+
# Tab 3 · Gallery (thumbnail grid of all sequences)
|
| 595 |
+
# ──────────────────────────────────────────────────────────────
|
| 596 |
+
with gr.TabItem("🖼 Gallery"):
|
| 597 |
+
gr.Markdown(
|
| 598 |
+
"Thumbnails of all sequences (first frame). "
|
| 599 |
+
"Use the filters to narrow down, then **click any thumbnail** "
|
| 600 |
+
"to instantly play that sequence below."
|
| 601 |
+
)
|
| 602 |
+
with gr.Row():
|
| 603 |
+
g_year = gr.Dropdown(["All years","2016 only","2017 only"],
|
| 604 |
+
value="All years", label="Year", scale=1)
|
| 605 |
+
g_split = gr.Dropdown(["All splits","Train only","Val only"],
|
| 606 |
+
value="All splits", label="Split", scale=1)
|
| 607 |
+
g_obj = gr.Dropdown(["Any # objects","1 object","2 objects","3+ objects"],
|
| 608 |
+
value="Any # objects", label="Objects", scale=1)
|
| 609 |
+
g_srch = gr.Textbox(placeholder="Search…", label="Search", scale=2)
|
| 610 |
+
with gr.Row():
|
| 611 |
+
g_fmin = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
|
| 612 |
+
int(DF["frames"].min()), step=1, label="Min frames", scale=3)
|
| 613 |
+
g_fmax = gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
|
| 614 |
+
int(DF["frames"].max()), step=1, label="Max frames", scale=3)
|
| 615 |
+
with gr.Row():
|
| 616 |
+
g_ov = gr.Checkbox(value=False, label="Show mask overlay on thumbnails")
|
| 617 |
+
|
| 618 |
+
g_count_md = gr.Markdown(f"**{len(ALL_SEQUENCES)} sequences**")
|
| 619 |
+
|
| 620 |
+
# Gallery component
|
| 621 |
+
gallery = gr.Gallery(
|
| 622 |
+
value=_ALL_THUMBS,
|
| 623 |
+
label="Sequences",
|
| 624 |
+
columns=5,
|
| 625 |
+
rows=None,
|
| 626 |
+
height="auto",
|
| 627 |
+
allow_preview=False,
|
| 628 |
+
show_label=False,
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
# State holding the sequence names matching current filter (in gallery order)
|
| 632 |
+
g_seq_state = gr.State(ALL_SEQUENCES.copy())
|
| 633 |
+
|
| 634 |
+
gr.Markdown("---")
|
| 635 |
+
with gr.Row():
|
| 636 |
+
g_info_md = gr.Markdown("*Click a thumbnail to play.*")
|
| 637 |
+
with gr.Row():
|
| 638 |
+
g_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1,
|
| 639 |
+
label="FPS", scale=2)
|
| 640 |
+
g_vid_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
|
| 641 |
+
g_vid_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
|
| 642 |
+
label="Opacity", scale=2)
|
| 643 |
+
g_btn_play = gr.Button("▶ Play selected", variant="primary", scale=1)
|
| 644 |
+
|
| 645 |
+
with gr.Column(scale=5):
|
| 646 |
+
g_vid_status = gr.Markdown("")
|
| 647 |
+
g_video = gr.Video(label="Playback", height=400, autoplay=True)
|
| 648 |
+
g_selected = gr.State("")
|
| 649 |
+
|
| 650 |
+
# Filter → rebuild gallery
|
| 651 |
+
g_f_inputs = [g_year, g_split, g_obj, g_fmin, g_fmax, g_srch]
|
| 652 |
+
|
| 653 |
+
def _on_g_filter(*args):
|
| 654 |
+
ov = args[-1] # last arg is the overlay checkbox
|
| 655 |
+
fargs = args[:-1] # filter args
|
| 656 |
+
fdf = filter_df(*fargs)
|
| 657 |
+
seqs = fdf["sequence"].tolist()
|
| 658 |
+
items = build_gallery_items(seqs, overlay=ov)
|
| 659 |
+
return items, seqs, f"**{len(seqs)} sequences**"
|
| 660 |
+
|
| 661 |
+
for inp in g_f_inputs + [g_ov]:
|
| 662 |
+
inp.change(_on_g_filter, g_f_inputs + [g_ov],
|
| 663 |
+
[gallery, g_seq_state, g_count_md])
|
| 664 |
+
|
| 665 |
+
# Click thumbnail → load info + auto-generate video
|
| 666 |
+
def _on_gallery_click(evt: gr.SelectData, seqs, ov, a, fps):
|
| 667 |
+
if evt is None or not seqs:
|
| 668 |
+
return "", gr.update(), None, ""
|
| 669 |
+
seq = seqs[evt.index]
|
| 670 |
+
info = _seq_info(seq)
|
| 671 |
+
path, status = get_video(seq, ov, a, fps)
|
| 672 |
+
return info, seq, path, status
|
| 673 |
+
|
| 674 |
+
gallery.select(
|
| 675 |
+
_on_gallery_click,
|
| 676 |
+
inputs=[g_seq_state, g_vid_ov, g_vid_a, g_fps],
|
| 677 |
+
outputs=[g_info_md, g_selected, g_video, g_vid_status],
|
| 678 |
+
)
|
| 679 |
+
g_btn_play.click(
|
| 680 |
+
lambda seq, ov, a, fps: get_video(seq, ov, a, fps),
|
| 681 |
+
inputs=[g_selected, g_vid_ov, g_vid_a, g_fps],
|
| 682 |
+
outputs=[g_video, g_vid_status],
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
# ──────────────────────────────────────────────────────────────
|
| 686 |
+
# Tab 4 · Multi-Video (paged 3×3 grid, all as MP4)
|
| 687 |
+
# ──────────────────────────────────────────────────────────────
|
| 688 |
+
with gr.TabItem("📺 Multi-Video"):
|
| 689 |
+
gr.Markdown(
|
| 690 |
+
f"Watch **{PAGE_SIZE} sequences at once** in a 3×3 grid. "
|
| 691 |
+
"Use Prev/Next to page through all {len(ALL_SEQUENCES)} sequences, "
|
| 692 |
+
"or filter first. Videos are encoded once and cached permanently."
|
| 693 |
+
)
|
| 694 |
+
with gr.Row():
|
| 695 |
+
mv_year = gr.Dropdown(["All years","2016 only","2017 only"],
|
| 696 |
+
value="All years", label="Year", scale=1)
|
| 697 |
+
mv_split = gr.Dropdown(["All splits","Train only","Val only"],
|
| 698 |
+
value="All splits", label="Split", scale=1)
|
| 699 |
+
mv_obj = gr.Dropdown(["Any # objects","1 object","2 objects","3+ objects"],
|
| 700 |
+
value="Any # objects", label="Objects", scale=1)
|
| 701 |
+
mv_srch = gr.Textbox(placeholder="Search…", label="Search", scale=2)
|
| 702 |
+
with gr.Row():
|
| 703 |
+
mv_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1, label="FPS", scale=2)
|
| 704 |
+
mv_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
|
| 705 |
+
mv_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
|
| 706 |
+
label="Opacity", scale=2)
|
| 707 |
+
mv_load = gr.Button("▶ Load Page", variant="primary", scale=1)
|
| 708 |
+
|
| 709 |
+
with gr.Row():
|
| 710 |
+
mv_prev = gr.Button("◀ Prev", scale=1)
|
| 711 |
+
with gr.Column(scale=3):
|
| 712 |
+
mv_page_lbl = gr.Markdown(f"**Page 1 / {total_pages}**")
|
| 713 |
+
mv_next = gr.Button("Next ▶", scale=1)
|
| 714 |
+
|
| 715 |
+
mv_status = gr.Markdown("")
|
| 716 |
+
|
| 717 |
+
# 9 fixed video slots, 3 rows × 3 cols
|
| 718 |
+
mv_vids = []
|
| 719 |
+
mv_lbls = []
|
| 720 |
+
for row_i in range(3):
|
| 721 |
+
with gr.Row():
|
| 722 |
+
for col_i in range(3):
|
| 723 |
+
with gr.Column():
|
| 724 |
+
lbl = gr.Markdown("—")
|
| 725 |
+
vid = gr.Video(height=260, autoplay=True, label="")
|
| 726 |
+
mv_lbls.append(lbl)
|
| 727 |
+
mv_vids.append(vid)
|
| 728 |
+
|
| 729 |
+
# State: list of sequences currently matching filter, page index
|
| 730 |
+
mv_seq_state = gr.State(ALL_SEQUENCES.copy())
|
| 731 |
+
mv_page_state = gr.State(0)
|
| 732 |
+
|
| 733 |
+
def _mv_filter(*args):
|
| 734 |
+
fdf = filter_df(*args)
|
| 735 |
+
seqs = fdf["sequence"].tolist()
|
| 736 |
+
tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
|
| 737 |
+
return seqs, 0, f"**Page 1 / {tp}**"
|
| 738 |
+
|
| 739 |
+
mv_f_inputs = [mv_year, mv_split, mv_obj,
|
| 740 |
+
gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
|
| 741 |
+
int(DF["frames"].min()), step=1, label=""),
|
| 742 |
+
gr.Slider(int(DF["frames"].min()), int(DF["frames"].max()),
|
| 743 |
+
int(DF["frames"].max()), step=1, label=""),
|
| 744 |
+
mv_srch]
|
| 745 |
+
|
| 746 |
+
# Simpler: just use the three dropdowns + search for multi-video filter
|
| 747 |
+
def _mv_filter_simple(yr, sp, ob, sr):
|
| 748 |
+
fdf = filter_df(yr, sp, ob,
|
| 749 |
+
int(DF["frames"].min()), int(DF["frames"].max()), sr)
|
| 750 |
+
seqs = fdf["sequence"].tolist()
|
| 751 |
+
tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
|
| 752 |
+
return seqs, 0, f"**Page 1 / {tp}**"
|
| 753 |
+
|
| 754 |
+
mv_simple_f = [mv_year, mv_split, mv_obj, mv_srch]
|
| 755 |
+
for inp in mv_simple_f:
|
| 756 |
+
inp.change(_mv_filter_simple, mv_simple_f,
|
| 757 |
+
[mv_seq_state, mv_page_state, mv_page_lbl])
|
| 758 |
+
|
| 759 |
+
def _load_page(seqs, page, ov, a, fps):
|
| 760 |
+
start = page * PAGE_SIZE
|
| 761 |
+
chunk = seqs[start: start + PAGE_SIZE]
|
| 762 |
+
tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
|
| 763 |
+
pg_lbl = f"**Page {page + 1} / {tp}**"
|
| 764 |
+
vids = []
|
| 765 |
+
labels = []
|
| 766 |
+
|
| 767 |
+
def _enc(seq):
|
| 768 |
+
p, _ = get_video(seq, ov, a, fps)
|
| 769 |
+
return seq, str(p) if p else None
|
| 770 |
+
|
| 771 |
+
with ThreadPoolExecutor(max_workers=PAGE_SIZE) as pool:
|
| 772 |
+
futs = {pool.submit(_enc, s): i for i, s in enumerate(chunk)}
|
| 773 |
+
res = [None] * PAGE_SIZE
|
| 774 |
+
lbs = [""] * PAGE_SIZE
|
| 775 |
+
for fut in as_completed(futs):
|
| 776 |
+
i = futs[fut]
|
| 777 |
+
seq, path = fut.result()
|
| 778 |
+
res[i] = path
|
| 779 |
+
lbs[i] = seq
|
| 780 |
+
|
| 781 |
+
# Pad to PAGE_SIZE
|
| 782 |
+
while len(res) < PAGE_SIZE:
|
| 783 |
+
res.append(None)
|
| 784 |
+
lbs.append("—")
|
| 785 |
+
|
| 786 |
+
n_loaded = sum(1 for r in res if r)
|
| 787 |
+
status = f"✅ Loaded {n_loaded}/{len(chunk)} videos (page {page+1}/{tp})"
|
| 788 |
+
|
| 789 |
+
# Build flat output: [lbl0, vid0, lbl1, vid1, …, status, pg_lbl]
|
| 790 |
+
out = []
|
| 791 |
+
for lb, r in zip(lbs, res):
|
| 792 |
+
out.append(f"**{lb}**" if lb and lb != "—" else "—")
|
| 793 |
+
out.append(r)
|
| 794 |
+
out.append(status)
|
| 795 |
+
out.append(pg_lbl)
|
| 796 |
+
return out
|
| 797 |
+
|
| 798 |
+
mv_page_outputs = []
|
| 799 |
+
for l, v in zip(mv_lbls, mv_vids):
|
| 800 |
+
mv_page_outputs.append(l)
|
| 801 |
+
mv_page_outputs.append(v)
|
| 802 |
+
mv_page_outputs.append(mv_status)
|
| 803 |
+
mv_page_outputs.append(mv_page_lbl)
|
| 804 |
+
|
| 805 |
+
mv_load.click(
|
| 806 |
+
_load_page,
|
| 807 |
+
inputs=[mv_seq_state, mv_page_state, mv_ov, mv_a, mv_fps],
|
| 808 |
+
outputs=mv_page_outputs,
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
+
def _prev(seqs, page):
|
| 812 |
+
new_p = max(0, page - 1)
|
| 813 |
+
tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
|
| 814 |
+
return new_p, f"**Page {new_p+1} / {tp}**"
|
| 815 |
+
|
| 816 |
+
def _next(seqs, page):
|
| 817 |
+
tp = max(1, (len(seqs) + PAGE_SIZE - 1) // PAGE_SIZE)
|
| 818 |
+
new_p = min(tp - 1, page + 1)
|
| 819 |
+
return new_p, f"**Page {new_p+1} / {tp}**"
|
| 820 |
+
|
| 821 |
+
mv_prev.click(_prev, [mv_seq_state, mv_page_state],
|
| 822 |
+
[mv_page_state, mv_page_lbl])
|
| 823 |
+
mv_next.click(_next, [mv_seq_state, mv_page_state],
|
| 824 |
+
[mv_page_state, mv_page_lbl])
|
| 825 |
+
|
| 826 |
+
# ──────────────────────────────────────────────────────────────
|
| 827 |
+
# Tab 5 · Compare (up to 6 side-by-side)
|
| 828 |
+
# ──────────────────────────────────────────────────────────────
|
| 829 |
+
with gr.TabItem("⚖️ Compare"):
|
| 830 |
+
gr.Markdown(
|
| 831 |
+
"Pick up to **6 sequences**, set FPS/overlay, "
|
| 832 |
+
"then **Load All** — encoded in parallel and cached."
|
| 833 |
+
)
|
| 834 |
+
with gr.Row():
|
| 835 |
+
cmp_fps = gr.Slider(1, 30, DEFAULT_FPS, step=1, label="FPS", scale=2)
|
| 836 |
+
cmp_ov = gr.Checkbox(value=True, label="Burn overlay", scale=1)
|
| 837 |
+
cmp_a = gr.Slider(0.1, 1.0, DEFAULT_ALPHA, step=0.05,
|
| 838 |
+
label="Opacity", scale=2)
|
| 839 |
+
cmp_btn = gr.Button("▶ Load All", variant="primary", scale=1)
|
| 840 |
+
|
| 841 |
+
cmp_dds = []
|
| 842 |
+
cmp_vids = []
|
| 843 |
+
cmp_lbls = []
|
| 844 |
+
default_seqs = (ALL_SEQUENCES + [None] * MAX_COMPARE)[:MAX_COMPARE]
|
| 845 |
+
|
| 846 |
+
for row_i in range(2):
|
| 847 |
+
with gr.Row():
|
| 848 |
+
for col_i in range(3):
|
| 849 |
+
si = row_i * 3 + col_i
|
| 850 |
+
with gr.Column():
|
| 851 |
+
dd = gr.Dropdown([""] + ALL_SEQUENCES,
|
| 852 |
+
value=default_seqs[si] or "",
|
| 853 |
+
label=f"Slot {si+1}")
|
| 854 |
+
vid = gr.Video(height=270, autoplay=True, label="")
|
| 855 |
+
lbl = gr.Markdown(
|
| 856 |
+
f"*{default_seqs[si]}*" if default_seqs[si] else "*empty*")
|
| 857 |
+
cmp_dds.append(dd)
|
| 858 |
+
cmp_vids.append(vid)
|
| 859 |
+
cmp_lbls.append(lbl)
|
| 860 |
+
|
| 861 |
+
cmp_status = gr.Markdown("")
|
| 862 |
+
|
| 863 |
+
cmp_outputs = []
|
| 864 |
+
for v, l in zip(cmp_vids, cmp_lbls):
|
| 865 |
+
cmp_outputs.append(v)
|
| 866 |
+
cmp_outputs.append(l)
|
| 867 |
+
cmp_outputs.append(cmp_status)
|
| 868 |
+
|
| 869 |
+
def _load_all(*args):
|
| 870 |
+
ov, a, fps = args[0], args[1], args[2]
|
| 871 |
+
slots = list(args[3:])
|
| 872 |
+
res = [None] * MAX_COMPARE
|
| 873 |
+
lbs = [""] * MAX_COMPARE
|
| 874 |
+
|
| 875 |
+
def _enc(i, seq):
|
| 876 |
+
if seq:
|
| 877 |
+
p, _ = get_video(seq, ov, a, fps)
|
| 878 |
+
res[i] = str(p) if p else None
|
| 879 |
+
lbs[i] = seq
|
| 880 |
+
|
| 881 |
+
with ThreadPoolExecutor(max_workers=MAX_COMPARE) as pool:
|
| 882 |
+
futs = [pool.submit(_enc, i, s) for i, s in enumerate(slots)]
|
| 883 |
+
for f in as_completed(futs):
|
| 884 |
+
f.result()
|
| 885 |
+
|
| 886 |
+
n_ok = sum(1 for r in res if r)
|
| 887 |
+
out = []
|
| 888 |
+
for r, l in zip(res, lbs):
|
| 889 |
+
out.append(r)
|
| 890 |
+
out.append(f"**{l}**" if l else "*empty*")
|
| 891 |
+
out.append(f"✅ Loaded {n_ok}/{len([s for s in slots if s])} slots")
|
| 892 |
+
return out
|
| 893 |
+
|
| 894 |
+
cmp_btn.click(_load_all,
|
| 895 |
+
inputs=[cmp_ov, cmp_a, cmp_fps] + cmp_dds,
|
| 896 |
+
outputs=cmp_outputs)
|
| 897 |
+
|
| 898 |
+
for i, (dd, vid, lbl) in enumerate(zip(cmp_dds, cmp_vids, cmp_lbls)):
|
| 899 |
+
def _mk(idx):
|
| 900 |
+
def _single(seq, ov, a, fps):
|
| 901 |
+
p, _ = get_video(seq, ov, a, fps)
|
| 902 |
+
return p, f"**{seq}**" if seq else "*empty*"
|
| 903 |
+
return _single
|
| 904 |
+
dd.change(_mk(i), [dd, cmp_ov, cmp_a, cmp_fps], [vid, lbl])
|
| 905 |
+
|
| 906 |
+
# ──────────────────────────────────────────────────────────────
|
| 907 |
+
# Tab 6 · Statistics
|
| 908 |
+
# ──────────────────────────────────────────────────────────────
|
| 909 |
+
with gr.TabItem("📊 Statistics"):
|
| 910 |
+
gr.Markdown("### Dataset Overview")
|
| 911 |
+
with gr.Row():
|
| 912 |
+
gr.Plot(value=figs[0], label="Frame count")
|
| 913 |
+
gr.Plot(value=figs[1], label="Object count")
|
| 914 |
+
with gr.Row():
|
| 915 |
+
gr.Plot(value=figs[2], label="Splits")
|
| 916 |
+
gr.Plot(value=figs[3], label="Resolution")
|
| 917 |
+
with gr.Row():
|
| 918 |
+
gr.Plot(value=figs[4], label="Frames vs. Objects")
|
| 919 |
+
gr.Markdown(f"""
|
| 920 |
+
**Quick facts**
|
| 921 |
+
- Total sequences: **{len(DF):,}** | Frame range: **{DF['frames'].min()}–{DF['frames'].max()}** (avg {DF['frames'].mean():.1f})
|
| 922 |
+
- Objects/seq: **{DF['n_objects'].min()}–{DF['n_objects'].max()}** (avg {DF['n_objects'].mean():.2f}) | Single-obj: **{int((DF['n_objects']==1).sum())}** · Multi-obj: **{int((DF['n_objects']>1).sum())}**
|
| 923 |
+
- DAVIS-2016: **{n_2016}** (30 train + 20 val) | DAVIS-2017: **{n_2017}** (60 train + 30 val)
|
| 924 |
+
- MP4 cache: `{CACHE_DIR}`
|
| 925 |
+
""")
|
| 926 |
+
|
| 927 |
+
# ──────────────────────────────────────────────────────────────
|
| 928 |
+
# Tab 7 · About
|
| 929 |
+
# ──────────────────────────────────────────────────────���───────
|
| 930 |
+
with gr.TabItem("ℹ️ About"):
|
| 931 |
+
gr.Markdown(f"""
|
| 932 |
+
## DAVIS — Densely Annotated VIdeo Segmentation
|
| 933 |
+
|
| 934 |
+
| Version | Train | Val | Total |
|
| 935 |
+
|---------|-------|-----|-------|
|
| 936 |
+
| DAVIS-2016 | 30 | 20 | 50 |
|
| 937 |
+
| DAVIS-2017 | 60 | 30 | 90 |
|
| 938 |
+
|
| 939 |
+
### Dataset structure
|
| 940 |
+
```
|
| 941 |
+
DAVIS/
|
| 942 |
+
├── JPEGImages/480p/<seq>/%05d.jpg RGB frames
|
| 943 |
+
├── Annotations/480p/<seq>/%05d.png palette-indexed masks (value = object ID)
|
| 944 |
+
└── ImageSets/2016|2017/train|val.txt
|
| 945 |
+
```
|
| 946 |
+
|
| 947 |
+
### MP4 cache (`{CACHE_DIR}`)
|
| 948 |
+
- `<seq>_raw_<fps>fps.mp4` — raw frames
|
| 949 |
+
- `<seq>_ov055_<fps>fps.mp4` — DAVIS palette overlay @ 55 % opacity
|
| 950 |
+
|
| 951 |
+
### Annotation format
|
| 952 |
+
Pixel value = object ID. Rendered with the official DAVIS 20-colour palette.
|
| 953 |
+
|
| 954 |
+
### Citation
|
| 955 |
+
```bibtex
|
| 956 |
+
@article{{Pont-Tuset_arXiv_2017,
|
| 957 |
+
author = {{Jordi Pont-Tuset et al.}},
|
| 958 |
+
title = {{The 2017 DAVIS Challenge on Video Object Segmentation}},
|
| 959 |
+
journal = {{arXiv:1704.00675}}, year = {{2017}}
|
| 960 |
+
}}
|
| 961 |
+
```
|
| 962 |
+
**Data root:** `{DAVIS_ROOT}`
|
| 963 |
+
""")
|
| 964 |
+
|
| 965 |
+
return demo
|
| 966 |
+
|
| 967 |
+
|
| 968 |
+
# ── Entry point ────────────────────────────────────────────────────────────────
|
| 969 |
+
|
| 970 |
+
demo = build_ui()
|
| 971 |
+
start_precache(fps=DEFAULT_FPS, workers=4)
|
| 972 |
+
|
| 973 |
+
if __name__ == "__main__":
|
| 974 |
+
parser = argparse.ArgumentParser(description="DAVIS Dataset Explorer")
|
| 975 |
+
parser.add_argument("--share", action="store_true")
|
| 976 |
+
parser.add_argument("--port", type=int, default=7860)
|
| 977 |
+
parser.add_argument("--host", default="0.0.0.0")
|
| 978 |
+
args = parser.parse_args()
|
| 979 |
+
demo.launch(server_name=args.host, server_port=args.port,
|
| 980 |
+
share=args.share, theme=gr.themes.Soft())
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ffmpeg
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.0.0
|
| 2 |
+
numpy
|
| 3 |
+
pandas
|
| 4 |
+
pillow
|
| 5 |
+
plotly
|