Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions samples/scripts/assetStoreImport/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ The script generateSampleData.py creates a folder structure containing:

- Videos: MP4 format, H.264 codec, random duration (5–30 seconds), 1280x720 resolution.
- Image Sequences: Extracted frames from temporary videos, stored as sequential JPGs.
- Annotations: Each video or image sequence is accompanied by either a DIVE track JSON (`.json`) or VIAME CSV (`.csv`) file describing moving or scaling geometric shapes (rectangle, star, circle, diamond) per frame. The format is chosen at random per dataset.
- Annotations: Each video or image sequence is accompanied by a DIVE track JSON (`.json`), VIAME CSV (`.csv`), or COCO JSON file describing moving or scaling geometric shapes (rectangle, star, circle, diamond) per frame. The format is chosen at random per dataset.
- Videos: the annotation file has the same basename as the video, with extension `.json` or `.csv`
- Image Sequences: any `.json` or `.csv` file in the same folder as the frames is imported as annotations
- **VIAME CSV videos**: annotation FPS is a random even subsample of the video FPS (30 → e.g. 1, 5, 10, 15, 30). The CSV `# metadata` `fps` field and frame count match that rate. Filenames include `_annfps{N}` (e.g. `reef_annfps10.mp4` / `reef_annfps10.csv`) so you can verify import respects CSV FPS.

Usage
```bash
Expand All @@ -29,7 +30,14 @@ uv run --script generateSampleData.py
- --folders (-f): Number of top-level folders (default: 3)
- --max-depth (-d): Maximum subfolder depth (default: 2)
- --videos (-v): Maximum videos per folder (default: 2)
- --total (-t): Total number of datasets (videos or image sequences) to create (default: 10)
- --total (-t): Total number of datasets (videos or image sequences) to create (default: 10)
- --annotation-formats: Comma-separated formats (`dive-json`, `viame-csv`, `coco-json`). Default mixes `coco-json,viame-csv`. Use `viame-csv` alone to focus on CSV FPS import testing.

Example focused on CSV FPS testing:
```bash
uv run --script generateSampleData.py --annotation-formats viame-csv --total 8
uv run --script minIOConfig.py
```

The script will randomly generate videos or image sequences with annotations inside the output directory. The output directory defaults to ./sample

Expand Down Expand Up @@ -90,7 +98,7 @@ Example Output
- Check the http://localhost:8010/girder#jobs page to see that importing jobs have completed
10. Finally you can go to the regular DIVE interface (http://localhost:8010) click on the globe in the breadcrumb bar at the top of your user directory and navigate to your collection
- You should be able to open DIVE Datasets and they should have random annotations in them.
- Video Datasets should have an annotation speed relative to the video and image-sequences should default to 1FPS
- Video datasets with VIAME CSV should keep the annotation FPS from the CSV `# metadata` line (see `_annfps{N}` in the filename). Other videos use video FPS. Image sequences should default to 1FPS.


Notes
Expand Down
35 changes: 30 additions & 5 deletions samples/scripts/assetStoreImport/generateSampleData.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
ANNOTATION_FORMATS = ("dive-json", "viame-csv", "coco-json")


def _annotation_fps_choices(video_fps: int = VIDEO_FPS) -> list:
"""FPS values that evenly subsample from ``video_fps`` (including native)."""
return [n for n in range(1, video_fps + 1) if video_fps % n == 0]


def _random_annotation_fps(video_fps: int = VIDEO_FPS) -> int:
"""Pick a random annotation FPS that evenly divides the video FPS."""
return random.choice(_annotation_fps_choices(video_fps))


def generate_random_dataset_info() -> dict:
"""Random per-dataset metadata for CSV/COCO import testing."""
return {
Expand Down Expand Up @@ -417,6 +427,7 @@ def write_annotations(
annotation_format: str,
frame_filenames: list = None,
dataset_name: str = "sample-dataset",
fps: int = VIDEO_FPS,
):
"""Write annotations as DIVE JSON, VIAME CSV, or COCO JSON."""
tracks = build_tracks(num_frames)
Expand All @@ -425,6 +436,7 @@ def write_annotations(
generate_annotation_viame_csv(
tracks,
output_file,
fps=fps,
frame_filenames=frame_filenames,
dataset_info=dataset_info,
)
Expand All @@ -450,26 +462,39 @@ def create_video_content(
total: int,
annotation_formats: tuple,
):
"""Create videos and associated annotation files."""
"""Create videos and associated annotation files.

For VIAME CSV, annotation FPS is a random even subsample of ``VIDEO_FPS``
(e.g. 30 → 1, 2, 3, 5, 6, 10, 15, or 30). Frame count and CSV ``fps``
metadata match that rate so assetStoreImport can be tested against it.
Filenames include ``_annfps{N}`` when CSV is used so expected FPS is obvious.
"""
if counter['count'] >= total:
return
num_videos = random.randint(1, max_videos)
for _ in range(num_videos):
if counter['count'] >= total:
break
duration = random.randint(5, 30)
name = fake.word() + ".mp4"
video_path = base_dir / name
annotation_format = random.choice(annotation_formats)
stem = fake.word()
if annotation_format == "viame-csv":
annotation_fps = _random_annotation_fps(VIDEO_FPS)
stem = f"{stem}_annfps{annotation_fps}"
else:
annotation_fps = VIDEO_FPS

video_path = base_dir / f"{stem}.mp4"
create_random_video(video_path, duration)
counter['count'] += 1

annotation_format = random.choice(annotation_formats)
ext = _annotation_extension(annotation_format)
write_annotations(
duration * VIDEO_FPS,
duration * annotation_fps,
video_path.with_suffix(ext),
annotation_format=annotation_format,
dataset_name=video_path.stem,
fps=annotation_fps,
)

def create_image_sequence_content(
Expand Down
12 changes: 12 additions & 0 deletions server/dive_server/crud_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from dive_server import crud, crud_annotation, crud_dataset
from dive_tasks import tasks
from dive_tasks.utils import choose_annotation_fps
from dive_tasks.multicam_pipeline import is_stereo_or_multicam_pipeline, pipeline_requires_input
from dive_utils import TRUTHY_META_VALUES, asbool, constants, fromMeta, models, types
from dive_utils.constants import TrainingModelExtensions
Expand Down Expand Up @@ -853,6 +854,17 @@ def postprocess(
Folder().save(dsFolder)

aggregate_warnings = process_items(dsFolder, user, additive, additivePrepend, set)
# Image sequences start at fps=-1 (auto). CSV import may have set a value;
# otherwise default to 1. convert_images also resolves, but safe-image folders
# skip that job and need this finalize step.
dsFolder = Folder().load(dsFolder['_id'], force=True)
media_type = fromMeta(dsFolder, constants.TypeMarker)
if media_type in (constants.ImageSequenceType, constants.LargeImageType):
requested_fps = fromMeta(dsFolder, constants.FPSMarker)
new_fps = choose_annotation_fps(requested_fps)
if requested_fps != new_fps:
dsFolder['meta'][constants.FPSMarker] = new_fps
Folder().save(dsFolder)
return {'folder': dsFolder, 'warnings': aggregate_warnings, 'job_ids': created_job_ids}


Expand Down
4 changes: 1 addition & 3 deletions server/dive_server/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,7 @@ def process_assetstore_import(event, meta: dict):
folder["meta"].update(
{
TypeMarker: dataset_type, # Sets to video
FPSMarker: (
-1 if dataset_type == VideoType else 1
), # -1 for video and 1 for image sequence
FPSMarker: -1, # auto: video uses media FPS; image sequence defaults to 1
AssetstoreSourcePathMarker: root,
MarkForPostProcess: True, # skip transcode or transcode if required
**meta,
Expand Down
40 changes: 28 additions & 12 deletions server/dive_tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import shlex
import shutil
import tempfile
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple
from urllib import request
from urllib.parse import urlparse
import zipfile
Expand Down Expand Up @@ -863,6 +863,27 @@ def convert_calibration(self: Task, itemId: str):
gc.delete(f"item/{existing_id}")


def resolve_annotation_fps(
gc: GirderClient,
folder_id: str,
*,
native_fps: Optional[float] = None,
default_fps: float = 1.0,
) -> float:
"""Pick annotation FPS from current folder meta vs media FPS.

Re-reads folder ``fps`` so a concurrent CSV import (assetstore postprocess)
is not overwritten by a stale ``-1`` snapshot from job start.

For video, pass ``native_fps`` from ffprobe. For image sequences, omit it so
``-1`` falls back to ``default_fps`` (1) and any CSV-set value is kept.
"""
requested_fps = fromMeta(gc.getFolder(folder_id), constants.FPSMarker)
return utils.choose_annotation_fps(
requested_fps, native_fps=native_fps, default_fps=default_fps
)


@app.task(bind=True, acks_late=True, ignore_result=True)
def convert_video(
self: Task, folderId: str, itemId: str, user_id: str, user_login: str, skip_transcoding=False
Expand All @@ -874,9 +895,6 @@ def convert_video(
manager.updateStatus(JobStatus.CANCELED)
return

folderData = gc.getFolder(folderId)
requestedFps = fromMeta(folderData, constants.FPSMarker)

with tempfile.TemporaryDirectory() as _working_directory, suppress(utils.CanceledError):
_working_directory_path = Path(_working_directory)
item: GirderModel = gc.getItem(itemId)
Expand Down Expand Up @@ -912,13 +930,6 @@ def convert_video(
# Extract framerate (avg_frame_rate, else r_frame_rate for e.g. MPEG-TS)
originalFpsString, originalFps = utils.fps_from_ffprobe_stream(videostream[0])

if requestedFps == -1:
newAnnotationFps = originalFps
else:
newAnnotationFps = min(requestedFps, originalFps)
if newAnnotationFps < 1:
raise Exception('FPS lower than 1 is not supported')

source_misaligned = False
if skip_transcoding:
source_misaligned = is_frame_misaligned(self, Path(file_name), context, manager)
Expand All @@ -936,6 +947,7 @@ def convert_video(
if can_skip_transcode:
# Now we can update the meta data and push the values
manager.updateStatus(JobStatus.PUSHING_OUTPUT)
newAnnotationFps = resolve_annotation_fps(gc, folderId, native_fps=originalFps)
gc.addMetadataToItem(
itemId,
{
Expand Down Expand Up @@ -1000,6 +1012,7 @@ def convert_video(
misaligned_flag = True

manager.updateStatus(JobStatus.PUSHING_OUTPUT)
newAnnotationFps = resolve_annotation_fps(gc, folderId, native_fps=originalFps)
new_file = gc.uploadFileToFolder(folderId, aligned_file)
gc.addMetadataToItem(
new_file['itemId'],
Expand Down Expand Up @@ -1078,7 +1091,10 @@ def convert_images(self: Task, folderId, user_id: str, user_login: str):

gc.addMetadataToFolder(
str(folderId),
{"annotate": True}, # mark the parent folder as able to annotate.
{
"annotate": True, # mark the parent folder as able to annotate.
constants.FPSMarker: resolve_annotation_fps(gc, folderId),
},
)


Expand Down
23 changes: 23 additions & 0 deletions server/dive_tasks/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ class CanceledError(RuntimeError):
pass


def choose_annotation_fps(
requested_fps,
*,
native_fps: Optional[float] = None,
default_fps: float = 1.0,
) -> float:
"""Resolve annotation FPS from folder meta vs media native rate.

``requested_fps == -1`` means auto: use ``native_fps`` for video, else
``default_fps`` (image sequences). When a concrete folder fps is set (e.g.
CSV import), keep it, capped by ``native_fps`` when provided.
"""
if requested_fps == -1:
annotation_fps = native_fps if native_fps is not None else default_fps
elif native_fps is not None:
annotation_fps = min(requested_fps, native_fps)
else:
annotation_fps = requested_fps
if annotation_fps < 1:
raise Exception('FPS lower than 1 is not supported')
return annotation_fps


def fps_from_ffprobe_stream(video_stream: dict) -> Tuple[str, float]:
"""
Return (fps_string, fps_float) from an ffprobe video stream dict.
Expand Down
Loading