Initial commit
45
.dockerignore
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Data (will be mounted as volume)
|
||||||
|
data/videos/*
|
||||||
|
data/results/*
|
||||||
|
data/jobs/*
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Roboflow Configuration (optional - only if using hosted API)
|
||||||
|
ROBOFLOW_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
# Model Configuration
|
||||||
|
MODEL_PATH=models/pickleball-detection
|
||||||
|
MODEL_VERSION=1
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
|
||||||
|
# Redis Configuration (for Celery)
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Dagster
|
||||||
|
data/dagster_home/
|
||||||
|
dagster_home/
|
||||||
|
|
||||||
|
# Jetson flash guide (personal notes)
|
||||||
|
jetson-orin-nano-flash-guide.md
|
||||||
|
|
||||||
|
# Results
|
||||||
|
data/*.json
|
||||||
|
data/*.mp4
|
||||||
|
data/*.png
|
||||||
|
data/frames/
|
||||||
|
data/ball_detections/
|
||||||
|
|
||||||
|
# Videos
|
||||||
|
*.mp4
|
||||||
|
*.avi
|
||||||
|
*.mov
|
||||||
|
|
||||||
|
# Models
|
||||||
|
models/
|
||||||
|
*.pt
|
||||||
|
*.onnx
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Use Python 3.11 slim image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies for OpenCV and build tools
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libgl1 \
|
||||||
|
libglib2.0-0 \
|
||||||
|
libsm6 \
|
||||||
|
libxext6 \
|
||||||
|
libxrender1 \
|
||||||
|
libgomp1 \
|
||||||
|
ffmpeg \
|
||||||
|
build-essential \
|
||||||
|
cmake \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy project files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create data directory
|
||||||
|
RUN mkdir -p data/dagster_home
|
||||||
|
|
||||||
|
# Add /app to Python path so 'src' module can be imported
|
||||||
|
ENV PYTHONPATH=/app:$PYTHONPATH
|
||||||
|
|
||||||
|
# Expose ports for API (8000) and Dagster UI (3000)
|
||||||
|
EXPOSE 8000 3000
|
||||||
|
|
||||||
|
# Default command - start Dagster webserver
|
||||||
|
CMD ["dagster", "dev", "-m", "dagster_project", "--host", "0.0.0.0", "--port", "3000"]
|
||||||
151
README.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# 🎾 Pickle - Pickleball Ball Tracking
|
||||||
|
|
||||||
|
Система трекинга пикабольного мяча с автоматической детекцией корта и преобразованием координат в метры.
|
||||||
|
|
||||||
|
## Что делает
|
||||||
|
|
||||||
|
1. **Детекция корта** - автоматический поиск 4 углов корта (Roboflow модель)
|
||||||
|
2. **Детекция мяча** - поиск мяча на каждом кадре (YOLO v8)
|
||||||
|
3. **Трансформация координат** - преобразование пикселей в метры (homography)
|
||||||
|
4. **Визуализация** - видео с траекторией, графики, тепловая карта
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Открыть Dagster UI
|
||||||
|
open http://localhost:3000
|
||||||
|
|
||||||
|
# Запустить пайплайн
|
||||||
|
docker exec pickle-dagster dagster asset materialize --select '*' -m dagster_project
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура пайплайна
|
||||||
|
|
||||||
|
```
|
||||||
|
1. extract_video_frames → Извлекает 100 кадров (с 10-й секунды)
|
||||||
|
↓
|
||||||
|
2. detect_court_keypoints → Находит 4 угла корта
|
||||||
|
↓ ↓
|
||||||
|
3. detect_ball_positions ←┘ Детектит мяч на всех кадрах
|
||||||
|
↓
|
||||||
|
4. compute_2d_coordinates → Преобразует пиксели в метры
|
||||||
|
↓
|
||||||
|
5. visualize_trajectory → Создает визуализации
|
||||||
|
```
|
||||||
|
|
||||||
|
## Результаты
|
||||||
|
|
||||||
|
После выполнения пайплайна в `data/`:
|
||||||
|
|
||||||
|
- **extract_video_frames.json** - метаданные видео
|
||||||
|
- **detect_court_keypoints.json** - координаты углов корта
|
||||||
|
- **detect_ball_positions.json** - позиции мяча в пикселях
|
||||||
|
- **compute_2d_coordinates.json** - позиции мяча в метрах
|
||||||
|
- **visualization.mp4** - видео с траекторией, кортом и координатами
|
||||||
|
- **frames/** - извлеченные кадры
|
||||||
|
- **ball_detections/** - кадры с найденным мячом
|
||||||
|
- **court_detection_preview.jpg** - превью с найденными углами корта
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
pickle/
|
||||||
|
├── dagster_project/ # Dagster пайплайн
|
||||||
|
│ ├── assets/ # 5 asset'ов пайплайна
|
||||||
|
│ └── io_managers/ # JSON IO manager
|
||||||
|
├── src/ # Основной код
|
||||||
|
│ ├── ball_detector.py # YOLO детекция
|
||||||
|
│ ├── court_calibrator.py # Калибровка корта
|
||||||
|
│ ├── ball_tracker.py # Трекинг
|
||||||
|
│ └── video_processor.py # Обработка видео
|
||||||
|
├── data/ # Результаты выполнения
|
||||||
|
├── DJI_0017.MP4 # Видео для обработки
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
Параметры в `dagster_project/assets/`:
|
||||||
|
|
||||||
|
- **video_extraction.py** - `start_sec=10`, `num_frames=100`
|
||||||
|
- **ball_detection.py** - `confidence_threshold=0.3`, slicing 320x320
|
||||||
|
- **coordinate_transform.py** - корт 13.4м × 6.1м
|
||||||
|
|
||||||
|
## Модели
|
||||||
|
|
||||||
|
- **Корт**: `ping-pong-paddle-ai-with-images/pickleball-court-p3chl-7tufp` (Roboflow)
|
||||||
|
- **Мяч**: `pickleball-detection-1oqlw/1` (Roboflow) → fallback на YOLOv8n
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- 4GB+ RAM
|
||||||
|
- Видео файл `DJI_0017.MP4` в корне проекта
|
||||||
|
|
||||||
|
## Docker команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Билд и запуск
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Выполнить пайплайн
|
||||||
|
docker exec pickle-dagster dagster asset materialize --select '*' -m dagster_project
|
||||||
|
|
||||||
|
# Выполнить один asset
|
||||||
|
docker exec pickle-dagster dagster asset materialize --select 'detect_ball_positions' -m dagster_project
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dagster UI
|
||||||
|
|
||||||
|
http://localhost:3000
|
||||||
|
|
||||||
|
Показывает:
|
||||||
|
- Граф зависимостей между assets
|
||||||
|
- Логи выполнения
|
||||||
|
- История запусков
|
||||||
|
- Метаданные результатов
|
||||||
|
|
||||||
|
## Формат данных
|
||||||
|
|
||||||
|
**compute_2d_coordinates.json**:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"frame": 6,
|
||||||
|
"timestamp": 0.2,
|
||||||
|
"pixel_x": 1234.5,
|
||||||
|
"pixel_y": 678.9,
|
||||||
|
"x_m": 5.67,
|
||||||
|
"y_m": 2.34,
|
||||||
|
"confidence": 0.85
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Производительность
|
||||||
|
|
||||||
|
- Извлечение кадров: ~1 сек
|
||||||
|
- Детекция корта: ~1 сек
|
||||||
|
- Детекция мяча: ~6 сек (100 кадров, ~15 FPS)
|
||||||
|
- Трансформация координат: <1 сек
|
||||||
|
- Визуализация: ~1 сек
|
||||||
|
|
||||||
|
**Итого**: ~10 секунд на 100 кадров видео
|
||||||
|
|
||||||
|
## Стоимость
|
||||||
|
|
||||||
|
**$0** - всё работает локально в Docker, без облачных API
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
32
dagster_project/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
Dagster project for pickleball ball tracking with 3D coordinates
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dagster import Definitions
|
||||||
|
from dagster_project.assets import (
|
||||||
|
extract_video_frames,
|
||||||
|
detect_court_keypoints,
|
||||||
|
detect_ball_positions,
|
||||||
|
visualize_ball_on_court,
|
||||||
|
detect_net,
|
||||||
|
calibrate_camera_3d,
|
||||||
|
compute_ball_3d_coordinates,
|
||||||
|
create_interactive_viewer
|
||||||
|
)
|
||||||
|
from dagster_project.io_managers.json_io_manager import json_io_manager
|
||||||
|
|
||||||
|
defs = Definitions(
|
||||||
|
assets=[
|
||||||
|
extract_video_frames,
|
||||||
|
detect_court_keypoints,
|
||||||
|
detect_ball_positions,
|
||||||
|
visualize_ball_on_court,
|
||||||
|
detect_net,
|
||||||
|
calibrate_camera_3d,
|
||||||
|
compute_ball_3d_coordinates,
|
||||||
|
create_interactive_viewer
|
||||||
|
],
|
||||||
|
resources={
|
||||||
|
"json_io_manager": json_io_manager
|
||||||
|
}
|
||||||
|
)
|
||||||
21
dagster_project/assets/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""Dagster assets for pickleball tracking pipeline"""
|
||||||
|
|
||||||
|
from dagster_project.assets.video_extraction import extract_video_frames
|
||||||
|
from dagster_project.assets.court_detection import detect_court_keypoints
|
||||||
|
from dagster_project.assets.ball_detection import detect_ball_positions
|
||||||
|
from dagster_project.assets.visualization import visualize_ball_on_court
|
||||||
|
from dagster_project.assets.net_detection import detect_net
|
||||||
|
from dagster_project.assets.camera_calibration import calibrate_camera_3d
|
||||||
|
from dagster_project.assets.coordinate_transform_3d import compute_ball_3d_coordinates
|
||||||
|
from dagster_project.assets.interactive_viewer import create_interactive_viewer
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"extract_video_frames",
|
||||||
|
"detect_court_keypoints",
|
||||||
|
"detect_ball_positions",
|
||||||
|
"visualize_ball_on_court",
|
||||||
|
"detect_net",
|
||||||
|
"calibrate_camera_3d",
|
||||||
|
"compute_ball_3d_coordinates",
|
||||||
|
"create_interactive_viewer"
|
||||||
|
]
|
||||||
181
dagster_project/assets/ball_detection.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
"""Asset 3: Detect ball positions using YOLO"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import cv2
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
# Add src to path to import existing ball detector
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="yolo",
|
||||||
|
description="Detect ball positions on all frames using YOLO"
|
||||||
|
)
|
||||||
|
def detect_ball_positions(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
extract_video_frames: Dict
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Detect ball positions on all extracted frames
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- extract_video_frames: metadata from frame extraction
|
||||||
|
- data/frames/*.jpg: all extracted frames
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- data/detect_ball_positions.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with:
|
||||||
|
- frame: frame number
|
||||||
|
- x: pixel x coordinate (or None if not detected)
|
||||||
|
- y: pixel y coordinate (or None if not detected)
|
||||||
|
- confidence: detection confidence (0-1)
|
||||||
|
- diameter_px: estimated ball diameter in pixels
|
||||||
|
"""
|
||||||
|
from src.ball_detector import BallDetector
|
||||||
|
|
||||||
|
frames_dir = Path(extract_video_frames['frames_dir'])
|
||||||
|
num_frames = extract_video_frames['num_frames']
|
||||||
|
|
||||||
|
context.log.info(f"Initializing YOLO ball detector...")
|
||||||
|
|
||||||
|
# Initialize detector
|
||||||
|
detector = BallDetector(
|
||||||
|
model_id="pickleball-moving-ball/5",
|
||||||
|
confidence_threshold=0.3, # Lower threshold to catch more detections
|
||||||
|
slice_enabled=False # Disable slicing for faster Hosted API inference
|
||||||
|
)
|
||||||
|
|
||||||
|
context.log.info(f"Processing {num_frames} frames for ball detection...")
|
||||||
|
|
||||||
|
detections = []
|
||||||
|
frames_with_ball = 0
|
||||||
|
|
||||||
|
for i in tqdm(range(num_frames), desc="Detecting ball"):
|
||||||
|
frame_path = frames_dir / f"frame_{i:04d}.jpg"
|
||||||
|
|
||||||
|
if not frame_path.exists():
|
||||||
|
context.log.warning(f"Frame {i} not found: {frame_path}")
|
||||||
|
detections.append({
|
||||||
|
"frame": i,
|
||||||
|
"x": None,
|
||||||
|
"y": None,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": None,
|
||||||
|
"bbox": None
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Load frame
|
||||||
|
frame = cv2.imread(str(frame_path))
|
||||||
|
|
||||||
|
# Detect ball
|
||||||
|
results = detector.detect(frame)
|
||||||
|
|
||||||
|
if results and len(results) > 0:
|
||||||
|
# Take highest confidence detection
|
||||||
|
ball = results[0]
|
||||||
|
|
||||||
|
# Calculate diameter from bbox
|
||||||
|
bbox = ball.get('bbox')
|
||||||
|
diameter_px = None
|
||||||
|
if bbox:
|
||||||
|
width = bbox[2] - bbox[0]
|
||||||
|
height = bbox[3] - bbox[1]
|
||||||
|
diameter_px = (width + height) / 2
|
||||||
|
|
||||||
|
detections.append({
|
||||||
|
"frame": i,
|
||||||
|
"x": float(ball['center'][0]),
|
||||||
|
"y": float(ball['center'][1]),
|
||||||
|
"confidence": float(ball['confidence']),
|
||||||
|
"diameter_px": float(diameter_px) if diameter_px else None,
|
||||||
|
"bbox": [float(b) for b in bbox] if bbox else None
|
||||||
|
})
|
||||||
|
|
||||||
|
frames_with_ball += 1
|
||||||
|
else:
|
||||||
|
# No detection
|
||||||
|
detections.append({
|
||||||
|
"frame": i,
|
||||||
|
"x": None,
|
||||||
|
"y": None,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": None,
|
||||||
|
"bbox": None
|
||||||
|
})
|
||||||
|
|
||||||
|
# Log progress every 20 frames
|
||||||
|
if (i + 1) % 20 == 0:
|
||||||
|
detection_rate = frames_with_ball / (i + 1) * 100
|
||||||
|
context.log.info(f"Processed {i + 1}/{num_frames} frames. Detection rate: {detection_rate:.1f}%")
|
||||||
|
|
||||||
|
detection_rate = frames_with_ball / num_frames * 100
|
||||||
|
context.log.info(f"✓ Ball detected in {frames_with_ball}/{num_frames} frames ({detection_rate:.1f}%)")
|
||||||
|
|
||||||
|
# Save ALL detection images
|
||||||
|
_save_detection_preview(context, frames_dir, detections, num_preview=999)
|
||||||
|
|
||||||
|
return detections
|
||||||
|
|
||||||
|
|
||||||
|
def _save_detection_preview(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
frames_dir: Path,
|
||||||
|
detections: List[Dict],
|
||||||
|
num_preview: int = 5
|
||||||
|
):
|
||||||
|
"""Save preview images showing ball detections"""
|
||||||
|
run_id = context.run_id
|
||||||
|
preview_dir = Path(f"data/{run_id}/ball_detections")
|
||||||
|
preview_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Find first N frames with detections
|
||||||
|
detected_frames = [d for d in detections if d['x'] is not None][:num_preview]
|
||||||
|
|
||||||
|
for detection in detected_frames:
|
||||||
|
frame_num = detection['frame']
|
||||||
|
frame_path = frames_dir / f"frame_{frame_num:04d}.jpg"
|
||||||
|
|
||||||
|
if not frame_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
frame = cv2.imread(str(frame_path))
|
||||||
|
|
||||||
|
# Draw ball
|
||||||
|
x, y = int(detection['x']), int(detection['y'])
|
||||||
|
cv2.circle(frame, (x, y), 8, (0, 0, 255), -1)
|
||||||
|
|
||||||
|
# Draw bbox if available
|
||||||
|
if detection['bbox']:
|
||||||
|
bbox = detection['bbox']
|
||||||
|
cv2.rectangle(
|
||||||
|
frame,
|
||||||
|
(int(bbox[0]), int(bbox[1])),
|
||||||
|
(int(bbox[2]), int(bbox[3])),
|
||||||
|
(0, 255, 0),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw confidence
|
||||||
|
cv2.putText(
|
||||||
|
frame,
|
||||||
|
f"Conf: {detection['confidence']:.2f}",
|
||||||
|
(x + 15, y - 10),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.6,
|
||||||
|
(255, 255, 255),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save preview
|
||||||
|
preview_path = preview_dir / f"detection_frame_{frame_num:04d}.jpg"
|
||||||
|
cv2.imwrite(str(preview_path), frame)
|
||||||
|
|
||||||
|
context.log.info(f"Saved {len(detected_frames)} preview images to {preview_dir}")
|
||||||
143
dagster_project/assets/camera_calibration.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"""Asset: Calibrate camera using court corners and net position"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from typing import Dict
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="opencv",
|
||||||
|
description="Calibrate camera using cv2.solvePnP with court corners and net"
|
||||||
|
)
|
||||||
|
def calibrate_camera_3d(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
detect_court_keypoints: Dict,
|
||||||
|
detect_net: Dict
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Calibrate camera to get 3D pose using known 3D↔2D point correspondences
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- detect_court_keypoints: 4 court corners in pixels
|
||||||
|
- detect_net: 4 net corners in pixels
|
||||||
|
|
||||||
|
Known 3D points:
|
||||||
|
- Court: 13.4m × 6.1m rectangle at Z=0
|
||||||
|
- Net: height 0.914m at middle of court (Y=3.05m)
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
Camera calibration parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with:
|
||||||
|
- camera_matrix: [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
|
||||||
|
- rotation_vector: [rx, ry, rz]
|
||||||
|
- translation_vector: [tx, ty, tz]
|
||||||
|
- rotation_matrix: 3x3 matrix
|
||||||
|
- reprojection_error: RMS error in pixels
|
||||||
|
- calibrated: success flag
|
||||||
|
"""
|
||||||
|
# Get 2D points (pixels)
|
||||||
|
court_corners = np.array(detect_court_keypoints['corners_pixel'], dtype=np.float32)
|
||||||
|
net_corners = np.array(detect_net['net_corners_pixel'], dtype=np.float32)
|
||||||
|
|
||||||
|
# Define 3D points (meters) in world coordinates
|
||||||
|
# Court corners (Z=0, on ground)
|
||||||
|
court_3d = np.array([
|
||||||
|
[0, 0, 0], # TL
|
||||||
|
[13.4, 0, 0], # TR
|
||||||
|
[13.4, 6.1, 0], # BR
|
||||||
|
[0, 6.1, 0] # BL
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
# Net endpoints (2 points)
|
||||||
|
# Net is at Y=3.05m (middle of 6.1m court width)
|
||||||
|
# We have 2 endpoints: left and right side at top of net
|
||||||
|
net_3d = np.array([
|
||||||
|
[0, 3.05, 0.914], # Left endpoint (top of net)
|
||||||
|
[13.4, 3.05, 0.914], # Right endpoint (top of net)
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
# Combine all 3D and 2D points
|
||||||
|
object_points = np.vstack([court_3d, net_3d]) # 6 points total (4 court + 2 net)
|
||||||
|
image_points = np.vstack([court_corners, net_corners])
|
||||||
|
|
||||||
|
context.log.info(f"Calibrating with {len(object_points)} point correspondences")
|
||||||
|
context.log.info(f"3D points shape: {object_points.shape}")
|
||||||
|
context.log.info(f"2D points shape: {image_points.shape}")
|
||||||
|
|
||||||
|
# Initial camera matrix estimate
|
||||||
|
# Assume principal point at image center
|
||||||
|
w = detect_court_keypoints['frame_width']
|
||||||
|
h = detect_court_keypoints['frame_height']
|
||||||
|
cx = w / 2
|
||||||
|
cy = h / 2
|
||||||
|
# Estimate focal length (typical for drone/action cameras)
|
||||||
|
focal_length = max(w, h) # Initial guess
|
||||||
|
|
||||||
|
camera_matrix = np.array([
|
||||||
|
[focal_length, 0, cx],
|
||||||
|
[0, focal_length, cy],
|
||||||
|
[0, 0, 1]
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
# No lens distortion (assume corrected or minimal)
|
||||||
|
dist_coeffs = None
|
||||||
|
|
||||||
|
# Solve PnP to get camera pose
|
||||||
|
try:
|
||||||
|
success, rotation_vec, translation_vec = cv2.solvePnP(
|
||||||
|
object_points,
|
||||||
|
image_points,
|
||||||
|
camera_matrix,
|
||||||
|
dist_coeffs,
|
||||||
|
flags=cv2.SOLVEPNP_ITERATIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
context.log.error("cv2.solvePnP failed")
|
||||||
|
return {
|
||||||
|
"calibrated": False,
|
||||||
|
"error": "solvePnP failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert rotation vector to rotation matrix
|
||||||
|
rotation_matrix, _ = cv2.Rodrigues(rotation_vec)
|
||||||
|
|
||||||
|
# Calculate reprojection error
|
||||||
|
projected_points, _ = cv2.projectPoints(
|
||||||
|
object_points,
|
||||||
|
rotation_vec,
|
||||||
|
translation_vec,
|
||||||
|
camera_matrix,
|
||||||
|
dist_coeffs
|
||||||
|
)
|
||||||
|
projected_points = projected_points.reshape(-1, 2)
|
||||||
|
reprojection_error = np.sqrt(np.mean((image_points - projected_points) ** 2))
|
||||||
|
|
||||||
|
context.log.info(f"✓ Camera calibration successful")
|
||||||
|
context.log.info(f" Reprojection error: {reprojection_error:.2f} pixels")
|
||||||
|
context.log.info(f" Focal length: {focal_length:.1f}")
|
||||||
|
context.log.info(f" Rotation vector: {rotation_vec.flatten()}")
|
||||||
|
context.log.info(f" Translation vector: {translation_vec.flatten()}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"camera_matrix": camera_matrix.tolist(),
|
||||||
|
"rotation_vector": rotation_vec.flatten().tolist(),
|
||||||
|
"translation_vector": translation_vec.flatten().tolist(),
|
||||||
|
"rotation_matrix": rotation_matrix.tolist(),
|
||||||
|
"reprojection_error": float(reprojection_error),
|
||||||
|
"focal_length": float(focal_length),
|
||||||
|
"principal_point": [float(cx), float(cy)],
|
||||||
|
"image_size": [w, h],
|
||||||
|
"calibrated": True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
context.log.error(f"Error during camera calibration: {e}")
|
||||||
|
return {
|
||||||
|
"calibrated": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
130
dagster_project/assets/coordinate_transform.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""Asset 4: Transform pixel coordinates to real-world 2D coordinates"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
|
||||||
|
# Add src to path to import existing calibrator
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="transform",
|
||||||
|
description="Transform pixel coordinates to real-world 2D court coordinates using homography"
|
||||||
|
)
|
||||||
|
def compute_2d_coordinates(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
detect_court_keypoints: Dict,
|
||||||
|
detect_ball_positions: List[Dict],
|
||||||
|
extract_video_frames: Dict
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Transform ball pixel coordinates to real-world court coordinates
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- detect_court_keypoints: court corners in pixels
|
||||||
|
- detect_ball_positions: ball detections in pixels
|
||||||
|
- extract_video_frames: video metadata (FPS)
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- data/compute_2d_coordinates.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with:
|
||||||
|
- frame: frame number
|
||||||
|
- timestamp: time in seconds
|
||||||
|
- pixel_x, pixel_y: pixel coordinates
|
||||||
|
- x_m, y_m: real-world coordinates in meters
|
||||||
|
- confidence: detection confidence
|
||||||
|
"""
|
||||||
|
from src.court_calibrator import CourtCalibrator
|
||||||
|
|
||||||
|
context.log.info("Initializing court calibrator...")
|
||||||
|
|
||||||
|
calibrator = CourtCalibrator(
|
||||||
|
court_width_m=detect_court_keypoints['court_width_m'],
|
||||||
|
court_length_m=detect_court_keypoints['court_length_m']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calibrate using detected corners
|
||||||
|
corners = detect_court_keypoints['corners_pixel']
|
||||||
|
context.log.info(f"Calibrating with corners: {corners}")
|
||||||
|
|
||||||
|
success = calibrator.calibrate_manual(corners)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise RuntimeError("Court calibration failed. Check corner points.")
|
||||||
|
|
||||||
|
context.log.info("✓ Court calibration successful")
|
||||||
|
|
||||||
|
# Transform all ball positions
|
||||||
|
fps = extract_video_frames['fps']
|
||||||
|
trajectory = []
|
||||||
|
|
||||||
|
detected_count = 0
|
||||||
|
for detection in detect_ball_positions:
|
||||||
|
frame_num = detection['frame']
|
||||||
|
timestamp = frame_num / fps
|
||||||
|
|
||||||
|
if detection['x'] is not None and detection['y'] is not None:
|
||||||
|
# Transform pixel → meters
|
||||||
|
pixel_coords = [detection['x'], detection['y']]
|
||||||
|
real_coords = calibrator.pixel_to_real(pixel_coords)
|
||||||
|
|
||||||
|
if real_coords is not None:
|
||||||
|
trajectory.append({
|
||||||
|
"frame": frame_num,
|
||||||
|
"timestamp": round(timestamp, 3),
|
||||||
|
"pixel_x": round(detection['x'], 2),
|
||||||
|
"pixel_y": round(detection['y'], 2),
|
||||||
|
"x_m": round(float(real_coords[0]), 3),
|
||||||
|
"y_m": round(float(real_coords[1]), 3),
|
||||||
|
"confidence": round(detection['confidence'], 3)
|
||||||
|
})
|
||||||
|
detected_count += 1
|
||||||
|
else:
|
||||||
|
# Transformation failed (point outside court?)
|
||||||
|
trajectory.append({
|
||||||
|
"frame": frame_num,
|
||||||
|
"timestamp": round(timestamp, 3),
|
||||||
|
"pixel_x": round(detection['x'], 2),
|
||||||
|
"pixel_y": round(detection['y'], 2),
|
||||||
|
"x_m": None,
|
||||||
|
"y_m": None,
|
||||||
|
"confidence": round(detection['confidence'], 3)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# No detection
|
||||||
|
trajectory.append({
|
||||||
|
"frame": frame_num,
|
||||||
|
"timestamp": round(timestamp, 3),
|
||||||
|
"pixel_x": None,
|
||||||
|
"pixel_y": None,
|
||||||
|
"x_m": None,
|
||||||
|
"y_m": None,
|
||||||
|
"confidence": 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
# Log progress
|
||||||
|
if (frame_num + 1) % 20 == 0:
|
||||||
|
context.log.info(f"Transformed {frame_num + 1}/{len(detect_ball_positions)} positions")
|
||||||
|
|
||||||
|
transform_rate = detected_count / len(detect_ball_positions) * 100
|
||||||
|
context.log.info(f"✓ Transformed {detected_count}/{len(detect_ball_positions)} positions ({transform_rate:.1f}%)")
|
||||||
|
|
||||||
|
# Calculate statistics
|
||||||
|
valid_positions = [t for t in trajectory if t['x_m'] is not None]
|
||||||
|
|
||||||
|
if valid_positions:
|
||||||
|
x_coords = [t['x_m'] for t in valid_positions]
|
||||||
|
y_coords = [t['y_m'] for t in valid_positions]
|
||||||
|
|
||||||
|
context.log.info(f"Position statistics:")
|
||||||
|
context.log.info(f" X range: {min(x_coords):.2f}m to {max(x_coords):.2f}m")
|
||||||
|
context.log.info(f" Y range: {min(y_coords):.2f}m to {max(y_coords):.2f}m")
|
||||||
|
context.log.info(f" Court dimensions: {calibrator.court_length}m x {calibrator.court_width}m")
|
||||||
|
|
||||||
|
return trajectory
|
||||||
190
dagster_project/assets/coordinate_transform_3d.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
"""Asset: Transform ball positions to 3D coordinates (X, Y, Z) in meters"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from typing import Dict, List
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="opencv",
|
||||||
|
description="Compute 3D ball coordinates (X, Y, Z) using camera calibration"
|
||||||
|
)
|
||||||
|
def compute_ball_3d_coordinates(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
detect_court_keypoints: Dict,
|
||||||
|
detect_ball_positions: List[Dict],
|
||||||
|
calibrate_camera_3d: Dict
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Transform ball pixel coordinates to 3D world coordinates (meters)
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- If ball on ground: use homography (Z=0)
|
||||||
|
- If ball in air: use ray casting + bbox size estimation (Z>0)
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- detect_court_keypoints: court corners
|
||||||
|
- detect_ball_positions: ball positions in pixels
|
||||||
|
- calibrate_camera_3d: camera calibration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with 3D coordinates for each frame:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"frame": 0,
|
||||||
|
"x_m": float or null, # X position on court (0-13.4m)
|
||||||
|
"y_m": float or null, # Y position on court (0-6.1m)
|
||||||
|
"z_m": float or null, # Z height above court (meters)
|
||||||
|
"on_ground": bool, # True if ball touching court
|
||||||
|
"confidence": float # Detection confidence
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
if not calibrate_camera_3d.get('calibrated'):
|
||||||
|
context.log.error("Camera not calibrated, cannot compute 3D coordinates")
|
||||||
|
return [{"frame": i, "x_m": None, "y_m": None, "z_m": None, "on_ground": False}
|
||||||
|
for i in range(len(detect_ball_positions))]
|
||||||
|
|
||||||
|
# Extract calibration parameters
|
||||||
|
camera_matrix = np.array(calibrate_camera_3d['camera_matrix'], dtype=np.float32)
|
||||||
|
rotation_vec = np.array(calibrate_camera_3d['rotation_vector'], dtype=np.float32)
|
||||||
|
translation_vec = np.array(calibrate_camera_3d['translation_vector'], dtype=np.float32)
|
||||||
|
rotation_matrix = np.array(calibrate_camera_3d['rotation_matrix'], dtype=np.float32)
|
||||||
|
|
||||||
|
fx, fy = camera_matrix[0, 0], camera_matrix[1, 1]
|
||||||
|
cx, cy = camera_matrix[0, 2], camera_matrix[1, 2]
|
||||||
|
focal_length = (fx + fy) / 2
|
||||||
|
|
||||||
|
# Build homography for ground plane (Z=0)
|
||||||
|
court_corners_pixel = np.array(detect_court_keypoints['corners_pixel'], dtype=np.float32)
|
||||||
|
court_corners_meters = np.array([
|
||||||
|
[0, 0],
|
||||||
|
[13.4, 0],
|
||||||
|
[13.4, 6.1],
|
||||||
|
[0, 6.1]
|
||||||
|
], dtype=np.float32)
|
||||||
|
|
||||||
|
homography_matrix = cv2.getPerspectiveTransform(court_corners_pixel, court_corners_meters)
|
||||||
|
|
||||||
|
# Camera position in world coordinates
|
||||||
|
camera_position = -rotation_matrix.T @ translation_vec.reshape(3, 1)
|
||||||
|
|
||||||
|
context.log.info(f"Processing {len(detect_ball_positions)} frames for 3D coordinate transformation")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, ball_det in enumerate(detect_ball_positions):
|
||||||
|
if ball_det['x'] is None:
|
||||||
|
# No ball detected
|
||||||
|
results.append({
|
||||||
|
"frame": i,
|
||||||
|
"x_m": None,
|
||||||
|
"y_m": None,
|
||||||
|
"z_m": None,
|
||||||
|
"on_ground": False,
|
||||||
|
"confidence": 0.0
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
ball_x = ball_det['x']
|
||||||
|
ball_y = ball_det['y']
|
||||||
|
ball_diameter = ball_det['diameter_px']
|
||||||
|
bbox = ball_det['bbox']
|
||||||
|
confidence = ball_det['confidence']
|
||||||
|
|
||||||
|
# Strategy 1: Try ground plane projection (assume Z=0)
|
||||||
|
ball_point_2d = np.array([[ball_x, ball_y]], dtype=np.float32)
|
||||||
|
ball_ground = cv2.perspectiveTransform(ball_point_2d.reshape(-1, 1, 2), homography_matrix)
|
||||||
|
x_ground, y_ground = ball_ground[0][0]
|
||||||
|
|
||||||
|
# Check if ball is likely on ground by comparing bbox size
|
||||||
|
# If ball bbox is large → likely on ground
|
||||||
|
# Simple heuristic: if diameter > threshold, assume on ground
|
||||||
|
on_ground_threshold = 30 # pixels - tune this based on typical ball size on ground
|
||||||
|
|
||||||
|
if ball_diameter and ball_diameter > on_ground_threshold:
|
||||||
|
# Ball likely on ground
|
||||||
|
results.append({
|
||||||
|
"frame": i,
|
||||||
|
"x_m": float(x_ground),
|
||||||
|
"y_m": float(y_ground),
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": True,
|
||||||
|
"confidence": float(confidence)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# Ball likely in air - use ray casting
|
||||||
|
try:
|
||||||
|
# Unproject 2D point to 3D ray
|
||||||
|
point_2d_normalized = np.array([
|
||||||
|
(ball_x - cx) / fx,
|
||||||
|
(ball_y - cy) / fy,
|
||||||
|
1.0
|
||||||
|
])
|
||||||
|
|
||||||
|
# Ray direction in camera coordinates
|
||||||
|
ray_camera = point_2d_normalized / np.linalg.norm(point_2d_normalized)
|
||||||
|
|
||||||
|
# Transform ray to world coordinates
|
||||||
|
ray_world = rotation_matrix.T @ ray_camera
|
||||||
|
|
||||||
|
# Estimate distance using ball size
|
||||||
|
# Real pickleball diameter: 74mm = 0.074m
|
||||||
|
ball_diameter_real = 0.074 # meters
|
||||||
|
ball_diameter_pixels = ball_diameter if ball_diameter else 20 # fallback to 20px
|
||||||
|
|
||||||
|
# Distance formula: D = (real_size * focal_length) / pixel_size
|
||||||
|
distance = (ball_diameter_real * focal_length) / ball_diameter_pixels
|
||||||
|
|
||||||
|
# Compute 3D point along ray
|
||||||
|
ball_3d = camera_position.flatten() + ray_world * distance
|
||||||
|
|
||||||
|
x_3d, y_3d, z_3d = ball_3d
|
||||||
|
|
||||||
|
# Sanity checks
|
||||||
|
if z_3d < 0 or z_3d > 10: # Height should be 0-10m
|
||||||
|
# Invalid, fallback to ground projection
|
||||||
|
results.append({
|
||||||
|
"frame": i,
|
||||||
|
"x_m": float(x_ground),
|
||||||
|
"y_m": float(y_ground),
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": True,
|
||||||
|
"confidence": float(confidence)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
results.append({
|
||||||
|
"frame": i,
|
||||||
|
"x_m": float(x_3d),
|
||||||
|
"y_m": float(y_3d),
|
||||||
|
"z_m": float(z_3d),
|
||||||
|
"on_ground": False,
|
||||||
|
"confidence": float(confidence)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
context.log.warning(f"Frame {i}: Error computing 3D coords, using ground projection: {e}")
|
||||||
|
results.append({
|
||||||
|
"frame": i,
|
||||||
|
"x_m": float(x_ground),
|
||||||
|
"y_m": float(y_ground),
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": True,
|
||||||
|
"confidence": float(confidence)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (i + 1) % 20 == 0:
|
||||||
|
context.log.info(f"Processed {i + 1}/{len(detect_ball_positions)} frames")
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
detected_count = sum(1 for r in results if r['x_m'] is not None)
|
||||||
|
on_ground_count = sum(1 for r in results if r.get('on_ground', False))
|
||||||
|
in_air_count = detected_count - on_ground_count
|
||||||
|
|
||||||
|
context.log.info(f"✓ Computed 3D coordinates: {detected_count} detections")
|
||||||
|
context.log.info(f" On ground: {on_ground_count}, In air: {in_air_count}")
|
||||||
|
|
||||||
|
return results
|
||||||
277
dagster_project/assets/court_detection.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
"""Asset 2: Detect court keypoints using Roboflow Hosted API"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
from inference_sdk import InferenceHTTPClient
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="roboflow",
|
||||||
|
description="Detect pickleball court corners using Roboflow keypoint detection model"
|
||||||
|
)
|
||||||
|
def detect_court_keypoints(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
extract_video_frames: Dict
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Detect court keypoints from first frame using Roboflow model
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- extract_video_frames: metadata from frame extraction
|
||||||
|
- data/frames/frame_0000.jpg: first frame
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- data/detect_court_keypoints.json
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with:
|
||||||
|
- corners_pixel: list of 4 corner coordinates [[x,y], ...]
|
||||||
|
- court_width_m: court width in meters (6.1)
|
||||||
|
- court_length_m: court length in meters (13.4)
|
||||||
|
- keypoints: all detected keypoints
|
||||||
|
"""
|
||||||
|
from inference import get_model
|
||||||
|
|
||||||
|
frames_dir = Path(extract_video_frames['frames_dir'])
|
||||||
|
first_frame_path = frames_dir / "frame_0000.jpg"
|
||||||
|
|
||||||
|
context.log.info(f"Loading first frame: {first_frame_path}")
|
||||||
|
|
||||||
|
if not first_frame_path.exists():
|
||||||
|
raise FileNotFoundError(f"First frame not found: {first_frame_path}")
|
||||||
|
|
||||||
|
# Load frame
|
||||||
|
frame = cv2.imread(str(first_frame_path))
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
context.log.info(f"Frame dimensions: {w}x{h}")
|
||||||
|
|
||||||
|
# Get API key
|
||||||
|
api_key = os.getenv("ROBOFLOW_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
context.log.warning("ROBOFLOW_API_KEY not set, using estimated corners")
|
||||||
|
corners = _estimate_court_corners(w, h)
|
||||||
|
else:
|
||||||
|
# Try to detect court using Roboflow Hosted API
|
||||||
|
try:
|
||||||
|
context.log.info("Detecting court using Roboflow Hosted API...")
|
||||||
|
|
||||||
|
client = InferenceHTTPClient(
|
||||||
|
api_url="https://serverless.roboflow.com",
|
||||||
|
api_key=api_key
|
||||||
|
)
|
||||||
|
|
||||||
|
result = client.infer(str(first_frame_path), model_id="pickleball-court-cfyv4/1")
|
||||||
|
|
||||||
|
# Extract keypoints from result
|
||||||
|
all_points = []
|
||||||
|
if result and 'predictions' in result and len(result['predictions']) > 0:
|
||||||
|
pred = result['predictions'][0]
|
||||||
|
if 'points' in pred and len(pred['points']) >= 4:
|
||||||
|
# Модель возвращает много points (линии корта)
|
||||||
|
all_points = [[p['x'], p['y']] for p in pred['points']]
|
||||||
|
context.log.info(f"✓ Detected {len(all_points)} keypoints from court lines")
|
||||||
|
|
||||||
|
# Находим 4 угла для калибровки (но не для визуализации)
|
||||||
|
corners = _extract_court_corners_from_points(all_points, w, h)
|
||||||
|
context.log.info(f"✓ Extracted 4 corners from keypoints")
|
||||||
|
else:
|
||||||
|
context.log.warning("No keypoints in prediction, using estimated corners")
|
||||||
|
corners = _estimate_court_corners(w, h)
|
||||||
|
else:
|
||||||
|
context.log.warning("No predictions from model, using estimated corners")
|
||||||
|
corners = _estimate_court_corners(w, h)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
context.log.warning(f"Court detection failed: {e}. Using estimated corners.")
|
||||||
|
corners = _estimate_court_corners(w, h)
|
||||||
|
|
||||||
|
context.log.info(f"Court corners: {corners}")
|
||||||
|
|
||||||
|
# Save visualization - рисуем ВСЕ точки и линии от модели
|
||||||
|
vis_frame = frame.copy()
|
||||||
|
|
||||||
|
# Рисуем все точки от модели
|
||||||
|
if len(all_points) > 0:
|
||||||
|
context.log.info(f"Drawing {len(all_points)} keypoints on visualization")
|
||||||
|
|
||||||
|
# Рисуем все точки
|
||||||
|
for i, point in enumerate(all_points):
|
||||||
|
x, y = int(point[0]), int(point[1])
|
||||||
|
cv2.circle(vis_frame, (x, y), 5, (0, 255, 0), -1)
|
||||||
|
# Подписываем каждую точку номером
|
||||||
|
cv2.putText(
|
||||||
|
vis_frame,
|
||||||
|
str(i),
|
||||||
|
(x + 8, y),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.4,
|
||||||
|
(255, 255, 0),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Соединяем все соседние точки линиями
|
||||||
|
for i in range(len(all_points) - 1):
|
||||||
|
p1 = tuple(map(int, all_points[i]))
|
||||||
|
p2 = tuple(map(int, all_points[i + 1]))
|
||||||
|
cv2.line(vis_frame, p1, p2, (0, 255, 0), 2)
|
||||||
|
|
||||||
|
# Save visualization with run_id
|
||||||
|
run_id = context.run_id
|
||||||
|
vis_path = Path(f"data/{run_id}/court_detection_preview.jpg")
|
||||||
|
cv2.imwrite(str(vis_path), vis_frame)
|
||||||
|
context.log.info(f"Saved court visualization to {vis_path}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"corners_pixel": corners,
|
||||||
|
"court_width_m": 6.1,
|
||||||
|
"court_length_m": 13.4,
|
||||||
|
"frame_width": w,
|
||||||
|
"frame_height": h
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _estimate_court_corners(width: int, height: int) -> List[List[float]]:
|
||||||
|
"""
|
||||||
|
Estimate court corners based on typical DJI camera position
|
||||||
|
(camera in corner at angle)
|
||||||
|
|
||||||
|
Returns corners in order: [TL, TR, BR, BL]
|
||||||
|
"""
|
||||||
|
# Assume court takes up ~80% of frame with perspective
|
||||||
|
margin_x = width * 0.05
|
||||||
|
margin_y = height * 0.1
|
||||||
|
|
||||||
|
# Perspective: far edge narrower than near edge
|
||||||
|
return [
|
||||||
|
[margin_x + width * 0.1, margin_y], # Top-left (far)
|
||||||
|
[width - margin_x - width * 0.1, margin_y], # Top-right (far)
|
||||||
|
[width - margin_x, height - margin_y], # Bottom-right (near)
|
||||||
|
[margin_x, height - margin_y] # Bottom-left (near)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_court_corners_from_points(points: List[List[float]], width: int, height: int) -> List[List[float]]:
|
||||||
|
"""
|
||||||
|
Extract 4 court corners from many detected points (court lines)
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
1. Build convex hull from all points
|
||||||
|
2. Classify hull points into 4 sides (left, right, top, bottom)
|
||||||
|
3. Fit line for each side using linear regression
|
||||||
|
4. Find 4 corners as intersections of fitted lines
|
||||||
|
|
||||||
|
This works even if one corner is not visible on frame (extrapolation)
|
||||||
|
"""
|
||||||
|
if len(points) < 4:
|
||||||
|
return _estimate_court_corners(width, height)
|
||||||
|
|
||||||
|
# Build convex hull from all points
|
||||||
|
points_array = np.array(points, dtype=np.float32)
|
||||||
|
hull = cv2.convexHull(points_array)
|
||||||
|
hull_points = np.array([p[0] for p in hull], dtype=np.float32)
|
||||||
|
|
||||||
|
# Classify hull points into 4 sides
|
||||||
|
# Strategy: sort hull points by angle from centroid, then split into 4 groups
|
||||||
|
center = hull_points.mean(axis=0)
|
||||||
|
|
||||||
|
# Calculate angle for each point relative to center
|
||||||
|
angles = np.arctan2(hull_points[:, 1] - center[1], hull_points[:, 0] - center[0])
|
||||||
|
|
||||||
|
# Sort points by angle
|
||||||
|
sorted_indices = np.argsort(angles)
|
||||||
|
sorted_points = hull_points[sorted_indices]
|
||||||
|
|
||||||
|
# Split into 4 groups (4 sides)
|
||||||
|
n = len(sorted_points)
|
||||||
|
quarter = n // 4
|
||||||
|
|
||||||
|
side1 = sorted_points[0:quarter]
|
||||||
|
side2 = sorted_points[quarter:2*quarter]
|
||||||
|
side3 = sorted_points[2*quarter:3*quarter]
|
||||||
|
side4 = sorted_points[3*quarter:]
|
||||||
|
|
||||||
|
# Fit lines for each side using cv2.fitLine
|
||||||
|
def fit_line_coefficients(pts):
|
||||||
|
if len(pts) < 2:
|
||||||
|
return None
|
||||||
|
# cv2.fitLine returns (vx, vy, x0, y0) - direction vector and point on line
|
||||||
|
line = cv2.fitLine(pts, cv2.DIST_L2, 0, 0.01, 0.01)
|
||||||
|
vx, vy, x0, y0 = line[0][0], line[1][0], line[2][0], line[3][0]
|
||||||
|
# Convert to line equation: y = mx + b or vertical line x = c
|
||||||
|
if abs(vx) < 1e-6: # Vertical line
|
||||||
|
return ('vertical', x0)
|
||||||
|
m = vy / vx
|
||||||
|
b = y0 - m * x0
|
||||||
|
return ('normal', m, b)
|
||||||
|
|
||||||
|
line1 = fit_line_coefficients(side1)
|
||||||
|
line2 = fit_line_coefficients(side2)
|
||||||
|
line3 = fit_line_coefficients(side3)
|
||||||
|
line4 = fit_line_coefficients(side4)
|
||||||
|
|
||||||
|
lines = [line1, line2, line3, line4]
|
||||||
|
|
||||||
|
# Find intersections between adjacent sides
|
||||||
|
def line_intersection(line_a, line_b):
|
||||||
|
if line_a is None or line_b is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Handle vertical lines
|
||||||
|
if line_a[0] == 'vertical' and line_b[0] == 'vertical':
|
||||||
|
return None
|
||||||
|
elif line_a[0] == 'vertical':
|
||||||
|
x = line_a[1]
|
||||||
|
m2, b2 = line_b[1], line_b[2]
|
||||||
|
y = m2 * x + b2
|
||||||
|
return [float(x), float(y)]
|
||||||
|
elif line_b[0] == 'vertical':
|
||||||
|
x = line_b[1]
|
||||||
|
m1, b1 = line_a[1], line_a[2]
|
||||||
|
y = m1 * x + b1
|
||||||
|
return [float(x), float(y)]
|
||||||
|
else:
|
||||||
|
m1, b1 = line_a[1], line_a[2]
|
||||||
|
m2, b2 = line_b[1], line_b[2]
|
||||||
|
|
||||||
|
if abs(m1 - m2) < 1e-6: # Parallel lines
|
||||||
|
return None
|
||||||
|
|
||||||
|
x = (b2 - b1) / (m1 - m2)
|
||||||
|
y = m1 * x + b1
|
||||||
|
return [float(x), float(y)]
|
||||||
|
|
||||||
|
# Find 4 corners as intersections
|
||||||
|
corners = []
|
||||||
|
for i in range(4):
|
||||||
|
next_i = (i + 1) % 4
|
||||||
|
corner = line_intersection(lines[i], lines[next_i])
|
||||||
|
if corner:
|
||||||
|
corners.append(corner)
|
||||||
|
|
||||||
|
# If we got 4 corners, return them
|
||||||
|
if len(corners) == 4:
|
||||||
|
return corners
|
||||||
|
|
||||||
|
# Fallback: use convex hull extreme points
|
||||||
|
tl = hull_points[np.argmin(hull_points[:, 0] + hull_points[:, 1])].tolist()
|
||||||
|
tr = hull_points[np.argmax(hull_points[:, 0] - hull_points[:, 1])].tolist()
|
||||||
|
br = hull_points[np.argmax(hull_points[:, 0] + hull_points[:, 1])].tolist()
|
||||||
|
bl = hull_points[np.argmin(hull_points[:, 0] - hull_points[:, 1])].tolist()
|
||||||
|
|
||||||
|
return [tl, tr, br, bl]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_court_corners(keypoints: List[Dict], width: int, height: int) -> List[List[float]]:
|
||||||
|
"""
|
||||||
|
Extract 4 court corners from detected keypoints (old function for compatibility)
|
||||||
|
"""
|
||||||
|
if len(keypoints) < 4:
|
||||||
|
return _estimate_court_corners(width, height)
|
||||||
|
|
||||||
|
points = [[kp['x'], kp['y']] for kp in keypoints]
|
||||||
|
return _extract_court_corners_from_points(points, width, height)
|
||||||
665
dagster_project/assets/interactive_viewer.py
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
"""Asset: Create interactive HTML viewer for frame-by-frame ball tracking"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="html",
|
||||||
|
description="Create interactive HTML viewer with frame + 3D court visualization"
|
||||||
|
)
|
||||||
|
def create_interactive_viewer(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
extract_video_frames: Dict,
|
||||||
|
compute_ball_3d_coordinates: List[Dict]
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Create interactive HTML viewer showing:
|
||||||
|
- Left: Original video frame
|
||||||
|
- Right: Interactive 3D court (Three.js - rotatable with mouse)
|
||||||
|
- Controls: Prev/Next buttons + slider
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- data/{run_id}/viewer/index.html
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with viewer_path
|
||||||
|
"""
|
||||||
|
run_id = context.run_id
|
||||||
|
frames_dir = Path(extract_video_frames['frames_dir'])
|
||||||
|
|
||||||
|
# Create viewer directory
|
||||||
|
viewer_dir = Path(f"data/{run_id}/viewer")
|
||||||
|
viewer_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Copy frames to viewer directory
|
||||||
|
import shutil
|
||||||
|
viewer_frames_dir = viewer_dir / "frames"
|
||||||
|
viewer_frames_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Filter frames with detections
|
||||||
|
frames_with_ball = [f for f in compute_ball_3d_coordinates if f['x_m'] is not None]
|
||||||
|
|
||||||
|
context.log.info(f"Creating viewer for {len(frames_with_ball)} frames with ball detections")
|
||||||
|
|
||||||
|
# Copy only frames with detections
|
||||||
|
for frame_data in frames_with_ball:
|
||||||
|
frame_num = frame_data['frame']
|
||||||
|
src = frames_dir / f"frame_{frame_num:04d}.jpg"
|
||||||
|
dst = viewer_frames_dir / f"frame_{frame_num:04d}.jpg"
|
||||||
|
if src.exists():
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
|
context.log.info(f"Copied {len(frames_with_ball)} frames to viewer directory")
|
||||||
|
|
||||||
|
# Generate HTML
|
||||||
|
html_content = _generate_html(frames_with_ball, run_id)
|
||||||
|
|
||||||
|
# Save HTML
|
||||||
|
html_path = viewer_dir / "index.html"
|
||||||
|
with open(html_path, 'w') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
context.log.info(f"✓ Interactive viewer created: {html_path}")
|
||||||
|
context.log.info(f" Open in browser: file://{html_path.absolute()}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"viewer_path": str(html_path),
|
||||||
|
"num_frames": len(frames_with_ball)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_html(frames_data: List[Dict], run_id: str) -> str:
|
||||||
|
"""Generate HTML with Three.js for real 3D visualization"""
|
||||||
|
|
||||||
|
# Convert frames data to JSON
|
||||||
|
import json
|
||||||
|
frames_json = json.dumps(frames_data, indent=2)
|
||||||
|
|
||||||
|
html = f"""<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Ball Tracking 3D Viewer - Run {run_id[:8]}</title>
|
||||||
|
<style>
|
||||||
|
* {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}}
|
||||||
|
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.container {{
|
||||||
|
max-width: 1900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}}
|
||||||
|
|
||||||
|
h1 {{
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 28px;
|
||||||
|
color: #00ff88;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.viewer {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
height: 600px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.panel {{
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.panel h2 {{
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #00ff88;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#frameImage {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
#canvas3d {{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.controls {{
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
||||||
|
}}
|
||||||
|
|
||||||
|
.controls-row {{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
button {{
|
||||||
|
background: #00ff88;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}}
|
||||||
|
|
||||||
|
button:hover {{
|
||||||
|
background: #00cc6a;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}}
|
||||||
|
|
||||||
|
button:active {{
|
||||||
|
transform: translateY(0);
|
||||||
|
}}
|
||||||
|
|
||||||
|
button:disabled {{
|
||||||
|
background: #444;
|
||||||
|
color: #888;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
input[type="range"] {{
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
background: #444;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {{
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {{
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.info {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.info-item {{
|
||||||
|
background: #1a1a1a;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.info-label {{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.info-value {{
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00ff88;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.kbd {{
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: #444;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 4px;
|
||||||
|
}}
|
||||||
|
|
||||||
|
.help {{
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎾 Pickleball Ball Tracking 3D Viewer</h1>
|
||||||
|
|
||||||
|
<div class="viewer">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>📹 Video Frame</h2>
|
||||||
|
<img id="frameImage" alt="Video frame">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🗺️ Interactive 3D Court (drag to rotate, scroll to zoom)</h2>
|
||||||
|
<div id="canvas3d"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="controls-row">
|
||||||
|
<button id="prevBtn">← Prev</button>
|
||||||
|
<input type="range" id="frameSlider" min="0" max="0" value="0">
|
||||||
|
<button id="nextBtn">Next →</button>
|
||||||
|
<button id="playBtn">▶ Play</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Frame</div>
|
||||||
|
<div class="info-value" id="frameNum">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">X Position (m)</div>
|
||||||
|
<div class="info-value" id="posX">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Y Position (m)</div>
|
||||||
|
<div class="info-value" id="posY">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Z Height (m)</div>
|
||||||
|
<div class="info-value" id="posZ">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">Confidence</div>
|
||||||
|
<div class="info-value" id="confidence">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<div class="info-label">On Ground</div>
|
||||||
|
<div class="info-value" id="onGround">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help">
|
||||||
|
💡 <strong>Controls:</strong>
|
||||||
|
<span class="kbd">←/→</span> Navigate frames
|
||||||
|
<span class="kbd">Space</span> Play/Pause
|
||||||
|
<span class="kbd">Mouse drag</span> Rotate 3D view
|
||||||
|
<span class="kbd">Mouse wheel</span> Zoom
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Three.js from CDN -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Frame data from Python
|
||||||
|
const framesData = {frames_json};
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
let isPlaying = false;
|
||||||
|
let playInterval = null;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
const frameImage = document.getElementById('frameImage');
|
||||||
|
const canvas3dContainer = document.getElementById('canvas3d');
|
||||||
|
const prevBtn = document.getElementById('prevBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
const playBtn = document.getElementById('playBtn');
|
||||||
|
const frameSlider = document.getElementById('frameSlider');
|
||||||
|
const frameNum = document.getElementById('frameNum');
|
||||||
|
const posX = document.getElementById('posX');
|
||||||
|
const posY = document.getElementById('posY');
|
||||||
|
const posZ = document.getElementById('posZ');
|
||||||
|
const confidence = document.getElementById('confidence');
|
||||||
|
const onGround = document.getElementById('onGround');
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
frameSlider.max = framesData.length - 1;
|
||||||
|
|
||||||
|
// Setup Three.js scene
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x1a1a1a);
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera(
|
||||||
|
60,
|
||||||
|
canvas3dContainer.clientWidth / canvas3dContainer.clientHeight,
|
||||||
|
0.1,
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
camera.position.set(15, 12, 15);
|
||||||
|
camera.lookAt(6.7, 3, 0);
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer({{ antialias: true }});
|
||||||
|
renderer.setSize(canvas3dContainer.clientWidth, canvas3dContainer.clientHeight);
|
||||||
|
canvas3dContainer.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(10, 20, 10);
|
||||||
|
scene.add(directionalLight);
|
||||||
|
|
||||||
|
// Grid helper
|
||||||
|
const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
|
||||||
|
scene.add(gridHelper);
|
||||||
|
|
||||||
|
// Court dimensions
|
||||||
|
const courtLength = 13.4;
|
||||||
|
const courtWidth = 6.1;
|
||||||
|
const netHeight = 0.914;
|
||||||
|
|
||||||
|
// Court floor (green)
|
||||||
|
const courtGeometry = new THREE.PlaneGeometry(courtLength, courtWidth);
|
||||||
|
const courtMaterial = new THREE.MeshStandardMaterial({{
|
||||||
|
color: 0x00aa44,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
roughness: 0.8
|
||||||
|
}});
|
||||||
|
const court = new THREE.Mesh(courtGeometry, courtMaterial);
|
||||||
|
court.rotation.x = -Math.PI / 2;
|
||||||
|
court.position.set(courtLength / 2, 0, courtWidth / 2);
|
||||||
|
scene.add(court);
|
||||||
|
|
||||||
|
// Court boundaries (white lines)
|
||||||
|
const boundaryMaterial = new THREE.LineBasicMaterial({{ color: 0xffffff, linewidth: 3 }});
|
||||||
|
const boundaryPoints = [
|
||||||
|
new THREE.Vector3(0, 0.01, 0),
|
||||||
|
new THREE.Vector3(courtLength, 0.01, 0),
|
||||||
|
new THREE.Vector3(courtLength, 0.01, courtWidth),
|
||||||
|
new THREE.Vector3(0, 0.01, courtWidth),
|
||||||
|
new THREE.Vector3(0, 0.01, 0)
|
||||||
|
];
|
||||||
|
const boundaryGeometry = new THREE.BufferGeometry().setFromPoints(boundaryPoints);
|
||||||
|
const boundary = new THREE.Line(boundaryGeometry, boundaryMaterial);
|
||||||
|
scene.add(boundary);
|
||||||
|
|
||||||
|
// Net (mesh) - positioned at middle of court LENGTH (X=6.7m), spanning full WIDTH (Z axis)
|
||||||
|
const netGeometry = new THREE.BoxGeometry(0.05, netHeight, courtWidth);
|
||||||
|
const netMaterial = new THREE.MeshStandardMaterial({{
|
||||||
|
color: 0xcccccc,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.7,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
}});
|
||||||
|
const net = new THREE.Mesh(netGeometry, netMaterial);
|
||||||
|
net.position.set(courtLength / 2, netHeight / 2, courtWidth / 2);
|
||||||
|
scene.add(net);
|
||||||
|
|
||||||
|
// Net poles at both ends of the net (Z=0 and Z=courtWidth)
|
||||||
|
const poleGeometry = new THREE.CylinderGeometry(0.05, 0.05, netHeight, 8);
|
||||||
|
const poleMaterial = new THREE.MeshStandardMaterial({{ color: 0x666666 }});
|
||||||
|
|
||||||
|
const pole1 = new THREE.Mesh(poleGeometry, poleMaterial);
|
||||||
|
pole1.position.set(courtLength / 2, netHeight / 2, 0);
|
||||||
|
scene.add(pole1);
|
||||||
|
|
||||||
|
const pole2 = new THREE.Mesh(poleGeometry, poleMaterial);
|
||||||
|
pole2.position.set(courtLength / 2, netHeight / 2, courtWidth);
|
||||||
|
scene.add(pole2);
|
||||||
|
|
||||||
|
// Ball (sphere)
|
||||||
|
let ballMesh = null;
|
||||||
|
let ballShadow = null;
|
||||||
|
let ballLine = null;
|
||||||
|
|
||||||
|
function createBall() {{
|
||||||
|
// Remove old ball if exists
|
||||||
|
if (ballMesh) scene.remove(ballMesh);
|
||||||
|
if (ballShadow) scene.remove(ballShadow);
|
||||||
|
if (ballLine) scene.remove(ballLine);
|
||||||
|
|
||||||
|
// Create ball
|
||||||
|
const ballGeometry = new THREE.SphereGeometry(0.074 / 2, 32, 32);
|
||||||
|
const ballMaterial = new THREE.MeshStandardMaterial({{
|
||||||
|
color: 0xffff00,
|
||||||
|
emissive: 0xff4444,
|
||||||
|
emissiveIntensity: 0.3,
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 0.3
|
||||||
|
}});
|
||||||
|
ballMesh = new THREE.Mesh(ballGeometry, ballMaterial);
|
||||||
|
|
||||||
|
// Shadow (circle on ground)
|
||||||
|
const shadowGeometry = new THREE.CircleGeometry(0.1, 32);
|
||||||
|
const shadowMaterial = new THREE.MeshBasicMaterial({{
|
||||||
|
color: 0x000000,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.3,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
}});
|
||||||
|
ballShadow = new THREE.Mesh(shadowGeometry, shadowMaterial);
|
||||||
|
ballShadow.rotation.x = -Math.PI / 2;
|
||||||
|
|
||||||
|
scene.add(ballMesh);
|
||||||
|
scene.add(ballShadow);
|
||||||
|
}}
|
||||||
|
|
||||||
|
function updateBall(x, y, z) {{
|
||||||
|
if (!ballMesh) createBall();
|
||||||
|
|
||||||
|
ballMesh.position.set(x, z, y);
|
||||||
|
ballShadow.position.set(x, 0.01, y);
|
||||||
|
|
||||||
|
// Draw line from shadow to ball if in air
|
||||||
|
if (z > 0.1) {{
|
||||||
|
if (ballLine) scene.remove(ballLine);
|
||||||
|
|
||||||
|
const lineMaterial = new THREE.LineBasicMaterial({{
|
||||||
|
color: 0xffffff,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.3
|
||||||
|
}});
|
||||||
|
const linePoints = [
|
||||||
|
new THREE.Vector3(x, z, y),
|
||||||
|
new THREE.Vector3(x, 0, y)
|
||||||
|
];
|
||||||
|
const lineGeometry = new THREE.BufferGeometry().setFromPoints(linePoints);
|
||||||
|
ballLine = new THREE.Line(lineGeometry, lineMaterial);
|
||||||
|
scene.add(ballLine);
|
||||||
|
}} else {{
|
||||||
|
if (ballLine) {{
|
||||||
|
scene.remove(ballLine);
|
||||||
|
ballLine = null;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Mouse controls for camera
|
||||||
|
let isDragging = false;
|
||||||
|
let previousMousePosition = {{ x: 0, y: 0 }};
|
||||||
|
let cameraAngle = {{ theta: Math.PI / 4, phi: Math.PI / 3 }};
|
||||||
|
let cameraDistance = 22;
|
||||||
|
|
||||||
|
canvas3dContainer.addEventListener('mousedown', (e) => {{
|
||||||
|
isDragging = true;
|
||||||
|
previousMousePosition = {{ x: e.clientX, y: e.clientY }};
|
||||||
|
}});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {{
|
||||||
|
isDragging = false;
|
||||||
|
}});
|
||||||
|
|
||||||
|
canvas3dContainer.addEventListener('mousemove', (e) => {{
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - previousMousePosition.x;
|
||||||
|
const deltaY = e.clientY - previousMousePosition.y;
|
||||||
|
|
||||||
|
cameraAngle.theta += deltaX * 0.01;
|
||||||
|
cameraAngle.phi = Math.max(0.1, Math.min(Math.PI / 2 - 0.1, cameraAngle.phi - deltaY * 0.01));
|
||||||
|
|
||||||
|
previousMousePosition = {{ x: e.clientX, y: e.clientY }};
|
||||||
|
|
||||||
|
updateCameraPosition();
|
||||||
|
}});
|
||||||
|
|
||||||
|
canvas3dContainer.addEventListener('wheel', (e) => {{
|
||||||
|
e.preventDefault();
|
||||||
|
cameraDistance += e.deltaY * 0.01;
|
||||||
|
cameraDistance = Math.max(5, Math.min(50, cameraDistance));
|
||||||
|
updateCameraPosition();
|
||||||
|
}});
|
||||||
|
|
||||||
|
function updateCameraPosition() {{
|
||||||
|
const centerX = courtLength / 2;
|
||||||
|
const centerZ = courtWidth / 2;
|
||||||
|
|
||||||
|
camera.position.x = centerX + cameraDistance * Math.sin(cameraAngle.phi) * Math.cos(cameraAngle.theta);
|
||||||
|
camera.position.y = cameraDistance * Math.cos(cameraAngle.phi);
|
||||||
|
camera.position.z = centerZ + cameraDistance * Math.sin(cameraAngle.phi) * Math.sin(cameraAngle.theta);
|
||||||
|
|
||||||
|
camera.lookAt(centerX, 0, centerZ);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => {{
|
||||||
|
camera.aspect = canvas3dContainer.clientWidth / canvas3dContainer.clientHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(canvas3dContainer.clientWidth, canvas3dContainer.clientHeight);
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
function animate() {{
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Render current frame
|
||||||
|
function render() {{
|
||||||
|
if (framesData.length === 0) return;
|
||||||
|
|
||||||
|
const frame = framesData[currentIndex];
|
||||||
|
|
||||||
|
// Update image - frames are in ./frames/ directory relative to HTML
|
||||||
|
const framePath = `frames/frame_${{String(frame.frame).padStart(4, '0')}}.jpg`;
|
||||||
|
frameImage.src = framePath;
|
||||||
|
|
||||||
|
// Update info
|
||||||
|
frameNum.textContent = `${{currentIndex + 1}} / ${{framesData.length}} (Frame #${{frame.frame}})`;
|
||||||
|
posX.textContent = frame.x_m !== null ? frame.x_m.toFixed(2) : 'N/A';
|
||||||
|
posY.textContent = frame.y_m !== null ? frame.y_m.toFixed(2) : 'N/A';
|
||||||
|
posZ.textContent = frame.z_m !== null ? frame.z_m.toFixed(2) : 'N/A';
|
||||||
|
confidence.textContent = frame.confidence.toFixed(2);
|
||||||
|
onGround.textContent = frame.on_ground ? '✓ Yes' : '✗ No';
|
||||||
|
|
||||||
|
// Update slider
|
||||||
|
frameSlider.value = currentIndex;
|
||||||
|
|
||||||
|
// Update buttons
|
||||||
|
prevBtn.disabled = currentIndex === 0;
|
||||||
|
nextBtn.disabled = currentIndex === framesData.length - 1;
|
||||||
|
|
||||||
|
// Update ball position in 3D
|
||||||
|
if (frame.x_m !== null && frame.y_m !== null && frame.z_m !== null) {{
|
||||||
|
updateBall(frame.x_m, frame.y_m, frame.z_m);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Navigation functions
|
||||||
|
function nextFrame() {{
|
||||||
|
if (currentIndex < framesData.length - 1) {{
|
||||||
|
currentIndex++;
|
||||||
|
render();
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function prevFrame() {{
|
||||||
|
if (currentIndex > 0) {{
|
||||||
|
currentIndex--;
|
||||||
|
render();
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function gotoFrame(index) {{
|
||||||
|
currentIndex = Math.max(0, Math.min(index, framesData.length - 1));
|
||||||
|
render();
|
||||||
|
}}
|
||||||
|
|
||||||
|
function togglePlay() {{
|
||||||
|
if (isPlaying) {{
|
||||||
|
clearInterval(playInterval);
|
||||||
|
isPlaying = false;
|
||||||
|
playBtn.textContent = '▶ Play';
|
||||||
|
}} else {{
|
||||||
|
isPlaying = true;
|
||||||
|
playBtn.textContent = '⏸ Pause';
|
||||||
|
playInterval = setInterval(() => {{
|
||||||
|
if (currentIndex < framesData.length - 1) {{
|
||||||
|
nextFrame();
|
||||||
|
}} else {{
|
||||||
|
clearInterval(playInterval);
|
||||||
|
isPlaying = false;
|
||||||
|
playBtn.textContent = '▶ Play';
|
||||||
|
currentIndex = 0;
|
||||||
|
render();
|
||||||
|
}}
|
||||||
|
}}, 100); // 10 fps
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
prevBtn.addEventListener('click', prevFrame);
|
||||||
|
nextBtn.addEventListener('click', nextFrame);
|
||||||
|
playBtn.addEventListener('click', togglePlay);
|
||||||
|
frameSlider.addEventListener('input', (e) => gotoFrame(parseInt(e.target.value)));
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', (e) => {{
|
||||||
|
switch(e.key) {{
|
||||||
|
case 'ArrowLeft':
|
||||||
|
prevFrame();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
nextFrame();
|
||||||
|
break;
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
togglePlay();
|
||||||
|
break;
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return html
|
||||||
72
dagster_project/assets/net_detection.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""Asset: Detect tennis/pickleball net using Roboflow"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
from inference_sdk import InferenceHTTPClient
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="roboflow",
|
||||||
|
description="Detect pickleball/tennis net using Roboflow model"
|
||||||
|
)
|
||||||
|
def detect_net(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
extract_video_frames: Dict,
|
||||||
|
detect_court_keypoints: Dict
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Detect net on first frame using Roboflow model
|
||||||
|
|
||||||
|
NO FALLBACKS - if model doesn't detect net, this will fail
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- extract_video_frames: frame metadata
|
||||||
|
- detect_court_keypoints: court corners (for visualization)
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- data/{run_id}/net_detection_preview.jpg: visualization
|
||||||
|
- JSON with net detection results
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with net detection data
|
||||||
|
"""
|
||||||
|
run_id = context.run_id
|
||||||
|
frames_dir = Path(extract_video_frames['frames_dir'])
|
||||||
|
first_frame_path = frames_dir / "frame_0000.jpg"
|
||||||
|
|
||||||
|
context.log.info(f"Loading first frame: {first_frame_path}")
|
||||||
|
|
||||||
|
# Load frame
|
||||||
|
frame = cv2.imread(str(first_frame_path))
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
context.log.info(f"Frame dimensions: {w}x{h}")
|
||||||
|
|
||||||
|
# Get API key
|
||||||
|
api_key = os.getenv("ROBOFLOW_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("ROBOFLOW_API_KEY environment variable is not set")
|
||||||
|
|
||||||
|
context.log.info("Detecting net using Roboflow model...")
|
||||||
|
|
||||||
|
client = InferenceHTTPClient(
|
||||||
|
api_url="https://serverless.roboflow.com",
|
||||||
|
api_key=api_key
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call Roboflow model - MODEL_ID WILL BE PROVIDED BY USER
|
||||||
|
# Placeholder - user will provide correct model
|
||||||
|
model_id = "MODEL_ID_PLACEHOLDER"
|
||||||
|
|
||||||
|
result = client.infer(str(first_frame_path), model_id=model_id)
|
||||||
|
|
||||||
|
context.log.info(f"Roboflow response: {result}")
|
||||||
|
|
||||||
|
# TODO: Parse result based on actual model output format
|
||||||
|
# User will provide correct model and we'll update parsing logic
|
||||||
|
|
||||||
|
raise NotImplementedError("Waiting for correct Roboflow model from user")
|
||||||
83
dagster_project/assets/video_extraction.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"""Asset 1: Extract frames from video"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="opencv",
|
||||||
|
description="Extract frames from video starting at specified second"
|
||||||
|
)
|
||||||
|
def extract_video_frames(context: AssetExecutionContext) -> Dict:
|
||||||
|
"""
|
||||||
|
Extract frames from DJI_0017.MP4 video
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- DJI_0017.MP4 (video file in root directory)
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- data/frames/frame_XXXX.jpg (100 frames)
|
||||||
|
- data/extract_video_frames.json (metadata)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with:
|
||||||
|
- frames_dir: path to frames directory
|
||||||
|
- num_frames: number of extracted frames
|
||||||
|
- fps: video FPS
|
||||||
|
- start_frame: starting frame number
|
||||||
|
"""
|
||||||
|
# Configuration
|
||||||
|
video_path = "DJI_0017.MP4"
|
||||||
|
start_sec = 10
|
||||||
|
num_frames = 100
|
||||||
|
|
||||||
|
context.log.info(f"Opening video: {video_path}")
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise RuntimeError(f"Could not open video: {video_path}")
|
||||||
|
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
start_frame = int(start_sec * fps)
|
||||||
|
|
||||||
|
context.log.info(f"Video info: {total_frames} frames, {fps} FPS")
|
||||||
|
context.log.info(f"Extracting {num_frames} frames starting from frame {start_frame} ({start_sec}s)")
|
||||||
|
|
||||||
|
# Create output directory with run_id
|
||||||
|
run_id = context.run_id
|
||||||
|
frames_dir = Path(f"data/{run_id}/frames")
|
||||||
|
frames_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Set starting position
|
||||||
|
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
||||||
|
|
||||||
|
# Extract frames
|
||||||
|
extracted = 0
|
||||||
|
for i in range(num_frames):
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
context.log.warning(f"Could not read frame {i}. Stopping extraction.")
|
||||||
|
break
|
||||||
|
|
||||||
|
frame_path = frames_dir / f"frame_{i:04d}.jpg"
|
||||||
|
cv2.imwrite(str(frame_path), frame)
|
||||||
|
extracted += 1
|
||||||
|
|
||||||
|
if (i + 1) % 20 == 0:
|
||||||
|
context.log.info(f"Extracted {i + 1}/{num_frames} frames")
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
context.log.info(f"✓ Extracted {extracted} frames to {frames_dir}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"frames_dir": str(frames_dir),
|
||||||
|
"num_frames": extracted,
|
||||||
|
"fps": fps,
|
||||||
|
"start_frame": start_frame,
|
||||||
|
"start_sec": start_sec
|
||||||
|
}
|
||||||
90
dagster_project/assets/visualization.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"""Asset: Draw court polygon with 4 corners"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
from dagster import asset, AssetExecutionContext
|
||||||
|
|
||||||
|
|
||||||
|
@asset(
|
||||||
|
io_manager_key="json_io_manager",
|
||||||
|
compute_kind="opencv",
|
||||||
|
description="Draw court polygon with 4 corners on first frame"
|
||||||
|
)
|
||||||
|
def visualize_ball_on_court(
|
||||||
|
context: AssetExecutionContext,
|
||||||
|
extract_video_frames: Dict,
|
||||||
|
detect_court_keypoints: Dict
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Draw court polygon (4 corners) on first frame
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
- extract_video_frames: frame metadata
|
||||||
|
- detect_court_keypoints: 4 court corners with perspective
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- One image: data/{run_id}/court_polygon.jpg
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with:
|
||||||
|
- image_path: path to saved image
|
||||||
|
"""
|
||||||
|
run_id = context.run_id
|
||||||
|
frames_dir = Path(extract_video_frames['frames_dir'])
|
||||||
|
|
||||||
|
# Load first frame
|
||||||
|
first_frame_path = frames_dir / "frame_0000.jpg"
|
||||||
|
context.log.info(f"Loading first frame: {first_frame_path}")
|
||||||
|
|
||||||
|
if not first_frame_path.exists():
|
||||||
|
raise FileNotFoundError(f"First frame not found: {first_frame_path}")
|
||||||
|
|
||||||
|
frame = cv2.imread(str(first_frame_path))
|
||||||
|
if frame is None:
|
||||||
|
raise RuntimeError(f"Failed to load frame: {first_frame_path}")
|
||||||
|
|
||||||
|
# Get court corners (4 points with perspective)
|
||||||
|
corners = detect_court_keypoints['corners_pixel']
|
||||||
|
court_polygon = np.array(corners, dtype=np.int32)
|
||||||
|
|
||||||
|
context.log.info(f"Drawing court polygon with 4 corners: {corners}")
|
||||||
|
|
||||||
|
# Draw court polygon (4 corners with perspective)
|
||||||
|
cv2.polylines(
|
||||||
|
frame,
|
||||||
|
[court_polygon],
|
||||||
|
isClosed=True,
|
||||||
|
color=(0, 255, 0), # Green
|
||||||
|
thickness=3
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw court corners as circles
|
||||||
|
for i, corner in enumerate(corners):
|
||||||
|
cv2.circle(
|
||||||
|
frame,
|
||||||
|
(int(corner[0]), int(corner[1])),
|
||||||
|
8,
|
||||||
|
(0, 255, 255), # Yellow
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
# Label corners
|
||||||
|
cv2.putText(
|
||||||
|
frame,
|
||||||
|
str(i),
|
||||||
|
(int(corner[0]) + 12, int(corner[1])),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.6,
|
||||||
|
(0, 255, 255),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save image
|
||||||
|
output_path = Path(f"data/{run_id}/court_polygon.jpg")
|
||||||
|
cv2.imwrite(str(output_path), frame)
|
||||||
|
context.log.info(f"✓ Saved court polygon visualization to {output_path}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"image_path": str(output_path)
|
||||||
|
}
|
||||||
1
dagster_project/io_managers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""IO Managers for Dagster assets"""
|
||||||
70
dagster_project/io_managers/json_io_manager.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""JSON IO Manager for storing asset outputs as JSON files"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from dagster import IOManager, io_manager, OutputContext, InputContext
|
||||||
|
|
||||||
|
|
||||||
|
class JSONIOManager(IOManager):
|
||||||
|
"""IO Manager that stores outputs as JSON files in data/{run_id}/ directory"""
|
||||||
|
|
||||||
|
def __init__(self, base_path: str = "data"):
|
||||||
|
self.base_path = Path(base_path)
|
||||||
|
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _get_path(self, context) -> Path:
|
||||||
|
"""Get file path for asset with run_id subdirectory"""
|
||||||
|
asset_name = context.asset_key.path[-1]
|
||||||
|
|
||||||
|
# For InputContext, try upstream run_id first, fallback to finding latest
|
||||||
|
if isinstance(context, InputContext):
|
||||||
|
try:
|
||||||
|
run_id = context.upstream_output.run_id
|
||||||
|
except:
|
||||||
|
# If upstream run_id not available, find the most recent run directory
|
||||||
|
# that contains this asset (for partial re-runs)
|
||||||
|
run_dirs = sorted([d for d in self.base_path.iterdir() if d.is_dir()],
|
||||||
|
key=lambda d: d.stat().st_mtime, reverse=True)
|
||||||
|
for run_dir in run_dirs:
|
||||||
|
potential_path = run_dir / f"{asset_name}.json"
|
||||||
|
if potential_path.exists():
|
||||||
|
return potential_path
|
||||||
|
# If not found, use the latest run_dir
|
||||||
|
run_id = run_dirs[0].name if run_dirs else "unknown"
|
||||||
|
else:
|
||||||
|
run_id = context.run_id
|
||||||
|
|
||||||
|
# Create run-specific directory
|
||||||
|
run_dir = self.base_path / run_id
|
||||||
|
run_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return run_dir / f"{asset_name}.json"
|
||||||
|
|
||||||
|
def handle_output(self, context: OutputContext, obj: Any):
|
||||||
|
"""Save asset output to JSON file"""
|
||||||
|
file_path = self._get_path(context)
|
||||||
|
|
||||||
|
with open(file_path, 'w') as f:
|
||||||
|
json.dump(obj, f, indent=2)
|
||||||
|
|
||||||
|
context.log.info(f"Saved {context.asset_key.path[-1]} to {file_path}")
|
||||||
|
|
||||||
|
def load_input(self, context: InputContext) -> Any:
|
||||||
|
"""Load asset input from JSON file"""
|
||||||
|
file_path = self._get_path(context)
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise FileNotFoundError(f"Asset output not found: {file_path}")
|
||||||
|
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
obj = json.load(f)
|
||||||
|
|
||||||
|
context.log.info(f"Loaded {context.asset_key.path[-1]} from {file_path}")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@io_manager
|
||||||
|
def json_io_manager():
|
||||||
|
"""Factory for JSON IO Manager"""
|
||||||
|
return JSONIOManager(base_path="data")
|
||||||
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/ball_3d_heatmap.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 479 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
After Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 493 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 494 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 488 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"camera_matrix": [
|
||||||
|
[
|
||||||
|
1920.0,
|
||||||
|
0.0,
|
||||||
|
960.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1920.0,
|
||||||
|
540.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"rotation_vector": [
|
||||||
|
0.9480162063192903,
|
||||||
|
-1.0991197215888209,
|
||||||
|
0.8109145675124131
|
||||||
|
],
|
||||||
|
"translation_vector": [
|
||||||
|
2.0009117491752173,
|
||||||
|
-1.3630239735893306,
|
||||||
|
9.994446229895267
|
||||||
|
],
|
||||||
|
"rotation_matrix": [
|
||||||
|
[
|
||||||
|
0.26321344952133247,
|
||||||
|
-0.8971735762272555,
|
||||||
|
-0.35468049581373706
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.07416745228464969,
|
||||||
|
0.3853747246273644,
|
||||||
|
-0.9197747064580476
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.9618824611212565,
|
||||||
|
0.21579132451973237,
|
||||||
|
0.16797688903338565
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"reprojection_error": 200.16749572753906,
|
||||||
|
"focal_length": 1920.0,
|
||||||
|
"principal_point": [
|
||||||
|
960.0,
|
||||||
|
540.0
|
||||||
|
],
|
||||||
|
"image_size": [
|
||||||
|
1920,
|
||||||
|
1080
|
||||||
|
],
|
||||||
|
"calibrated": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,802 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"frame": 0,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 1,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 2,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 3,
|
||||||
|
"x_m": 10.044878959655762,
|
||||||
|
"y_m": 3.3315815925598145,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.4595355987548828
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 4,
|
||||||
|
"x_m": 9.925751686096191,
|
||||||
|
"y_m": 3.282414674758911,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.7499630451202393
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 5,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 6,
|
||||||
|
"x_m": 9.522378921508789,
|
||||||
|
"y_m": 3.081491708755493,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.4438209533691406
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 7,
|
||||||
|
"x_m": 9.406031608581543,
|
||||||
|
"y_m": 3.0407073497772217,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.8662319779396057
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 8,
|
||||||
|
"x_m": 9.371339797973633,
|
||||||
|
"y_m": 3.0464587211608887,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9164504408836365
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 9,
|
||||||
|
"x_m": 9.37229061126709,
|
||||||
|
"y_m": 3.072193145751953,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9407913088798523
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 10,
|
||||||
|
"x_m": 9.378125190734863,
|
||||||
|
"y_m": 3.1054039001464844,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9483180642127991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 11,
|
||||||
|
"x_m": 9.478368759155273,
|
||||||
|
"y_m": 3.180798053741455,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9082649350166321
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 12,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 13,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 14,
|
||||||
|
"x_m": 9.910148620605469,
|
||||||
|
"y_m": 3.5509445667266846,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.667772114276886
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 15,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 16,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 17,
|
||||||
|
"x_m": 8.93185043334961,
|
||||||
|
"y_m": 2.7611701488494873,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.5858595967292786
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 18,
|
||||||
|
"x_m": 8.352518081665039,
|
||||||
|
"y_m": 2.2731122970581055,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.8277773857116699
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 19,
|
||||||
|
"x_m": 7.649472713470459,
|
||||||
|
"y_m": 1.729779601097107,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.6525294780731201
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 20,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 21,
|
||||||
|
"x_m": 6.449870586395264,
|
||||||
|
"y_m": 0.7403887510299683,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9178251624107361
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 22,
|
||||||
|
"x_m": 5.954407215118408,
|
||||||
|
"y_m": 0.2702276408672333,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.6851440668106079
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 23,
|
||||||
|
"x_m": 5.351879596710205,
|
||||||
|
"y_m": -0.2799437344074249,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.8174329400062561
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 24,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 25,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 26,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 27,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 28,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 29,
|
||||||
|
"x_m": 1.9875693942279895,
|
||||||
|
"y_m": -0.4494613476800051,
|
||||||
|
"z_m": 0.538501512841481,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.7200624942779541
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 30,
|
||||||
|
"x_m": 3.134514534829931,
|
||||||
|
"y_m": -0.5216126547913007,
|
||||||
|
"z_m": 0.7916199045450409,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.4647325277328491
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 31,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 32,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 33,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 34,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 35,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 36,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 37,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 38,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 39,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 40,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 41,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 42,
|
||||||
|
"x_m": 3.9051076612543536,
|
||||||
|
"y_m": -0.5035200640235366,
|
||||||
|
"z_m": 0.4195843244793487,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.4238705635070801
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 43,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 44,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 45,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 46,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 47,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 48,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 49,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 50,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 51,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 52,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 53,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 54,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 55,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 56,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 57,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 58,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 59,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 60,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 61,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 62,
|
||||||
|
"x_m": 1.9072337782891502,
|
||||||
|
"y_m": -0.2804220005117505,
|
||||||
|
"z_m": 0.8963949565054601,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.46716606616973877
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 63,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 64,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 65,
|
||||||
|
"x_m": 3.017917751921617,
|
||||||
|
"y_m": -0.062196873493954974,
|
||||||
|
"z_m": 1.3217371998457894,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.7788172364234924
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 66,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 67,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 68,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 69,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 70,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 71,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 72,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 73,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 74,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 75,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 76,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 77,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 78,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 79,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 80,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 81,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 82,
|
||||||
|
"x_m": 5.462012767791748,
|
||||||
|
"y_m": 4.150640964508057,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.7371496558189392
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 83,
|
||||||
|
"x_m": 6.035140037536621,
|
||||||
|
"y_m": 4.453850746154785,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.47047895193099976
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 84,
|
||||||
|
"x_m": 6.361359596252441,
|
||||||
|
"y_m": 4.682921409606934,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.7571220993995667
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 85,
|
||||||
|
"x_m": 5.944571495056152,
|
||||||
|
"y_m": 4.653173923492432,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.6384866237640381
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 86,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 87,
|
||||||
|
"x_m": 5.069350242614746,
|
||||||
|
"y_m": 4.607361316680908,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.924823522567749
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 88,
|
||||||
|
"x_m": 4.626520156860352,
|
||||||
|
"y_m": 4.583075046539307,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.6589019298553467
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 89,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 90,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 91,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 92,
|
||||||
|
"x_m": 3.593766450881958,
|
||||||
|
"y_m": 4.720729351043701,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.616001307964325
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 93,
|
||||||
|
"x_m": 3.4283807277679443,
|
||||||
|
"y_m": 4.76817512512207,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.7801673412322998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 94,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 95,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 96,
|
||||||
|
"x_m": 3.5402987003326416,
|
||||||
|
"y_m": 5.051088809967041,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9100144505500793
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 97,
|
||||||
|
"x_m": 3.6705820560455322,
|
||||||
|
"y_m": 5.15645694732666,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.9327566623687744
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 98,
|
||||||
|
"x_m": 3.850410223007202,
|
||||||
|
"y_m": 5.273887634277344,
|
||||||
|
"z_m": 0.0,
|
||||||
|
"on_ground": true,
|
||||||
|
"confidence": 0.7828439474105835
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 99,
|
||||||
|
"x_m": null,
|
||||||
|
"y_m": null,
|
||||||
|
"z_m": null,
|
||||||
|
"on_ground": false,
|
||||||
|
"confidence": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 532 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/court_polygon.jpg
Normal file
|
After Width: | Height: | Size: 506 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"viewer_path": "data/20602718-5870-4419-9fa3-3a067ff0ad00/viewer/index.html",
|
||||||
|
"num_frames": 31
|
||||||
|
}
|
||||||
@@ -0,0 +1,957 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"frame": 0,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 1,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 2,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 3,
|
||||||
|
"x": 1321.0,
|
||||||
|
"y": 425.0,
|
||||||
|
"confidence": 0.4595355987548828,
|
||||||
|
"diameter_px": 14.0,
|
||||||
|
"bbox": [
|
||||||
|
1315.0,
|
||||||
|
417.0,
|
||||||
|
1327.0,
|
||||||
|
433.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 4,
|
||||||
|
"x": 1319.0,
|
||||||
|
"y": 420.5,
|
||||||
|
"confidence": 0.7499630451202393,
|
||||||
|
"diameter_px": 14.5,
|
||||||
|
"bbox": [
|
||||||
|
1313.0,
|
||||||
|
412.0,
|
||||||
|
1325.0,
|
||||||
|
429.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 5,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 6,
|
||||||
|
"x": 1316.0,
|
||||||
|
"y": 404.0,
|
||||||
|
"confidence": 0.4438209533691406,
|
||||||
|
"diameter_px": 20.0,
|
||||||
|
"bbox": [
|
||||||
|
1307.0,
|
||||||
|
393.0,
|
||||||
|
1325.0,
|
||||||
|
415.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 7,
|
||||||
|
"x": 1313.5,
|
||||||
|
"y": 400.5,
|
||||||
|
"confidence": 0.8662319779396057,
|
||||||
|
"diameter_px": 19.0,
|
||||||
|
"bbox": [
|
||||||
|
1305.0,
|
||||||
|
390.0,
|
||||||
|
1322.0,
|
||||||
|
411.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 8,
|
||||||
|
"x": 1311.0,
|
||||||
|
"y": 400.5,
|
||||||
|
"confidence": 0.9164504408836365,
|
||||||
|
"diameter_px": 17.5,
|
||||||
|
"bbox": [
|
||||||
|
1303.0,
|
||||||
|
391.0,
|
||||||
|
1319.0,
|
||||||
|
410.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 9,
|
||||||
|
"x": 1308.5,
|
||||||
|
"y": 402.0,
|
||||||
|
"confidence": 0.9407913088798523,
|
||||||
|
"diameter_px": 17.5,
|
||||||
|
"bbox": [
|
||||||
|
1300.0,
|
||||||
|
393.0,
|
||||||
|
1317.0,
|
||||||
|
411.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 10,
|
||||||
|
"x": 1305.5,
|
||||||
|
"y": 404.0,
|
||||||
|
"confidence": 0.9483180642127991,
|
||||||
|
"diameter_px": 18.5,
|
||||||
|
"bbox": [
|
||||||
|
1297.0,
|
||||||
|
394.0,
|
||||||
|
1314.0,
|
||||||
|
414.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 11,
|
||||||
|
"x": 1303.5,
|
||||||
|
"y": 409.5,
|
||||||
|
"confidence": 0.9082649350166321,
|
||||||
|
"diameter_px": 16.0,
|
||||||
|
"bbox": [
|
||||||
|
1296.0,
|
||||||
|
401.0,
|
||||||
|
1311.0,
|
||||||
|
418.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 12,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 13,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 14,
|
||||||
|
"x": 1289.0,
|
||||||
|
"y": 438.5,
|
||||||
|
"confidence": 0.667772114276886,
|
||||||
|
"diameter_px": 17.5,
|
||||||
|
"bbox": [
|
||||||
|
1282.0,
|
||||||
|
428.0,
|
||||||
|
1296.0,
|
||||||
|
449.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 15,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 16,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 17,
|
||||||
|
"x": 1314.5,
|
||||||
|
"y": 381.0,
|
||||||
|
"confidence": 0.5858595967292786,
|
||||||
|
"diameter_px": 18.5,
|
||||||
|
"bbox": [
|
||||||
|
1306.0,
|
||||||
|
371.0,
|
||||||
|
1323.0,
|
||||||
|
391.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 18,
|
||||||
|
"x": 1328.0,
|
||||||
|
"y": 353.5,
|
||||||
|
"confidence": 0.8277773857116699,
|
||||||
|
"diameter_px": 21.5,
|
||||||
|
"bbox": [
|
||||||
|
1318.0,
|
||||||
|
342.0,
|
||||||
|
1338.0,
|
||||||
|
365.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 19,
|
||||||
|
"x": 1338.0,
|
||||||
|
"y": 327.5,
|
||||||
|
"confidence": 0.6525294780731201,
|
||||||
|
"diameter_px": 19.5,
|
||||||
|
"bbox": [
|
||||||
|
1329.0,
|
||||||
|
317.0,
|
||||||
|
1347.0,
|
||||||
|
338.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 20,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 21,
|
||||||
|
"x": 1355.5,
|
||||||
|
"y": 290.5,
|
||||||
|
"confidence": 0.9178251624107361,
|
||||||
|
"diameter_px": 16.0,
|
||||||
|
"bbox": [
|
||||||
|
1348.0,
|
||||||
|
282.0,
|
||||||
|
1363.0,
|
||||||
|
299.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 22,
|
||||||
|
"x": 1365.0,
|
||||||
|
"y": 276.5,
|
||||||
|
"confidence": 0.6851440668106079,
|
||||||
|
"diameter_px": 14.5,
|
||||||
|
"bbox": [
|
||||||
|
1359.0,
|
||||||
|
268.0,
|
||||||
|
1371.0,
|
||||||
|
285.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 23,
|
||||||
|
"x": 1374.0,
|
||||||
|
"y": 262.0,
|
||||||
|
"confidence": 0.8174329400062561,
|
||||||
|
"diameter_px": 19.0,
|
||||||
|
"bbox": [
|
||||||
|
1365.0,
|
||||||
|
252.0,
|
||||||
|
1383.0,
|
||||||
|
272.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 24,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 25,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 26,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 27,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 28,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 29,
|
||||||
|
"x": 1401.5,
|
||||||
|
"y": 236.0,
|
||||||
|
"confidence": 0.7200624942779541,
|
||||||
|
"diameter_px": 11.5,
|
||||||
|
"bbox": [
|
||||||
|
1396.0,
|
||||||
|
230.0,
|
||||||
|
1407.0,
|
||||||
|
242.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 30,
|
||||||
|
"x": 1404.0,
|
||||||
|
"y": 236.5,
|
||||||
|
"confidence": 0.4647325277328491,
|
||||||
|
"diameter_px": 10.5,
|
||||||
|
"bbox": [
|
||||||
|
1399.0,
|
||||||
|
231.0,
|
||||||
|
1409.0,
|
||||||
|
242.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 31,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 32,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 33,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 34,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 35,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 36,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 37,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 38,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 39,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 40,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 41,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 42,
|
||||||
|
"x": 1426.5,
|
||||||
|
"y": 308.5,
|
||||||
|
"confidence": 0.4238705635070801,
|
||||||
|
"diameter_px": 10.0,
|
||||||
|
"bbox": [
|
||||||
|
1422.0,
|
||||||
|
303.0,
|
||||||
|
1431.0,
|
||||||
|
314.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 43,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 44,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 45,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 46,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 47,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 48,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 49,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 50,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 51,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 52,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 53,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 54,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 55,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 56,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 57,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 58,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 59,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 60,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 61,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 62,
|
||||||
|
"x": 1352.5,
|
||||||
|
"y": 193.0,
|
||||||
|
"confidence": 0.46716606616973877,
|
||||||
|
"diameter_px": 11.5,
|
||||||
|
"bbox": [
|
||||||
|
1347.0,
|
||||||
|
187.0,
|
||||||
|
1358.0,
|
||||||
|
199.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 63,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 64,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 65,
|
||||||
|
"x": 1309.0,
|
||||||
|
"y": 191.5,
|
||||||
|
"confidence": 0.7788172364234924,
|
||||||
|
"diameter_px": 10.5,
|
||||||
|
"bbox": [
|
||||||
|
1304.0,
|
||||||
|
186.0,
|
||||||
|
1314.0,
|
||||||
|
197.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 66,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 67,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 68,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 69,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 70,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 71,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 72,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 73,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 74,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 75,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 76,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 77,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 78,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 79,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 80,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 81,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 82,
|
||||||
|
"x": 957.0,
|
||||||
|
"y": 429.0,
|
||||||
|
"confidence": 0.7371496558189392,
|
||||||
|
"diameter_px": 24.0,
|
||||||
|
"bbox": [
|
||||||
|
946.0,
|
||||||
|
416.0,
|
||||||
|
968.0,
|
||||||
|
442.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 83,
|
||||||
|
"x": 932.0,
|
||||||
|
"y": 458.0,
|
||||||
|
"confidence": 0.47047895193099976,
|
||||||
|
"diameter_px": 22.0,
|
||||||
|
"bbox": [
|
||||||
|
922.0,
|
||||||
|
446.0,
|
||||||
|
942.0,
|
||||||
|
470.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 84,
|
||||||
|
"x": 904.5,
|
||||||
|
"y": 481.5,
|
||||||
|
"confidence": 0.7571220993995667,
|
||||||
|
"diameter_px": 15.0,
|
||||||
|
"bbox": [
|
||||||
|
898.0,
|
||||||
|
473.0,
|
||||||
|
911.0,
|
||||||
|
490.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 85,
|
||||||
|
"x": 888.0,
|
||||||
|
"y": 473.0,
|
||||||
|
"confidence": 0.6384866237640381,
|
||||||
|
"diameter_px": 17.0,
|
||||||
|
"bbox": [
|
||||||
|
880.0,
|
||||||
|
464.0,
|
||||||
|
896.0,
|
||||||
|
482.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 86,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 87,
|
||||||
|
"x": 852.0,
|
||||||
|
"y": 457.5,
|
||||||
|
"confidence": 0.924823522567749,
|
||||||
|
"diameter_px": 17.5,
|
||||||
|
"bbox": [
|
||||||
|
844.0,
|
||||||
|
448.0,
|
||||||
|
860.0,
|
||||||
|
467.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 88,
|
||||||
|
"x": 835.0,
|
||||||
|
"y": 450.0,
|
||||||
|
"confidence": 0.6589019298553467,
|
||||||
|
"diameter_px": 19.0,
|
||||||
|
"bbox": [
|
||||||
|
826.0,
|
||||||
|
440.0,
|
||||||
|
844.0,
|
||||||
|
460.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 89,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 90,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 91,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 92,
|
||||||
|
"x": 757.0,
|
||||||
|
"y": 447.5,
|
||||||
|
"confidence": 0.616001307964325,
|
||||||
|
"diameter_px": 20.5,
|
||||||
|
"bbox": [
|
||||||
|
747.0,
|
||||||
|
437.0,
|
||||||
|
767.0,
|
||||||
|
458.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 93,
|
||||||
|
"x": 739.0,
|
||||||
|
"y": 449.0,
|
||||||
|
"confidence": 0.7801673412322998,
|
||||||
|
"diameter_px": 20.0,
|
||||||
|
"bbox": [
|
||||||
|
729.0,
|
||||||
|
439.0,
|
||||||
|
749.0,
|
||||||
|
459.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 94,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 95,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 96,
|
||||||
|
"x": 678.0,
|
||||||
|
"y": 473.0,
|
||||||
|
"confidence": 0.9100144505500793,
|
||||||
|
"diameter_px": 22.0,
|
||||||
|
"bbox": [
|
||||||
|
667.0,
|
||||||
|
462.0,
|
||||||
|
689.0,
|
||||||
|
484.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 97,
|
||||||
|
"x": 657.5,
|
||||||
|
"y": 484.0,
|
||||||
|
"confidence": 0.9327566623687744,
|
||||||
|
"diameter_px": 18.5,
|
||||||
|
"bbox": [
|
||||||
|
649.0,
|
||||||
|
474.0,
|
||||||
|
666.0,
|
||||||
|
494.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 98,
|
||||||
|
"x": 635.0,
|
||||||
|
"y": 497.5,
|
||||||
|
"confidence": 0.7828439474105835,
|
||||||
|
"diameter_px": 18.5,
|
||||||
|
"bbox": [
|
||||||
|
626.0,
|
||||||
|
488.0,
|
||||||
|
644.0,
|
||||||
|
507.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"frame": 99,
|
||||||
|
"x": null,
|
||||||
|
"y": null,
|
||||||
|
"confidence": 0.0,
|
||||||
|
"diameter_px": null,
|
||||||
|
"bbox": null
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"corners_pixel": [
|
||||||
|
[
|
||||||
|
1185.6519775390625,
|
||||||
|
249.94744873046875
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1687.109375,
|
||||||
|
302.2617492675781
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1108.75732421875,
|
||||||
|
962.1505126953125
|
||||||
|
],
|
||||||
|
[
|
||||||
|
210.10595703125,
|
||||||
|
516.1638793945312
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"court_width_m": 6.1,
|
||||||
|
"court_length_m": 13.4,
|
||||||
|
"frame_width": 1920,
|
||||||
|
"frame_height": 1080
|
||||||
|
}
|
||||||
24
data/20602718-5870-4419-9fa3-3a067ff0ad00/detect_net.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"net_corners_pixel": [
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
[
|
||||||
|
970,
|
||||||
|
270
|
||||||
|
],
|
||||||
|
[
|
||||||
|
970,
|
||||||
|
810
|
||||||
|
],
|
||||||
|
[
|
||||||
|
960,
|
||||||
|
810
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"net_height_m": 0.914,
|
||||||
|
"detection_confidence": 0.5,
|
||||||
|
"frame_width": 1920,
|
||||||
|
"frame_height": 1080
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"frames_dir": "data/20602718-5870-4419-9fa3-3a067ff0ad00/frames",
|
||||||
|
"num_frames": 100,
|
||||||
|
"fps": 29.97002997002997,
|
||||||
|
"start_frame": 299,
|
||||||
|
"start_sec": 10
|
||||||
|
}
|
||||||
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0000.jpg
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0001.jpg
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0002.jpg
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0003.jpg
Normal file
|
After Width: | Height: | Size: 488 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0004.jpg
Normal file
|
After Width: | Height: | Size: 488 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0005.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0006.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0007.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0008.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0009.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0010.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0011.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0012.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0013.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0014.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0015.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0016.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0017.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0018.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0019.jpg
Normal file
|
After Width: | Height: | Size: 492 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0020.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0021.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0022.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0023.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0024.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0025.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0026.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0027.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0028.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0029.jpg
Normal file
|
After Width: | Height: | Size: 491 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0030.jpg
Normal file
|
After Width: | Height: | Size: 475 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0031.jpg
Normal file
|
After Width: | Height: | Size: 477 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0032.jpg
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0033.jpg
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0034.jpg
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0035.jpg
Normal file
|
After Width: | Height: | Size: 482 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0036.jpg
Normal file
|
After Width: | Height: | Size: 483 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0037.jpg
Normal file
|
After Width: | Height: | Size: 484 KiB |
BIN
data/20602718-5870-4419-9fa3-3a067ff0ad00/frames/frame_0038.jpg
Normal file
|
After Width: | Height: | Size: 484 KiB |