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 |