Skip to content

Add MOT and CVAT video annotation import/export support#678

Open
deanp70 wants to merge 10 commits intomainfrom
mot_cvat_video_converter
Open

Add MOT and CVAT video annotation import/export support#678
deanp70 wants to merge 10 commits intomainfrom
mot_cvat_video_converter

Conversation

@deanp70
Copy link
Copy Markdown
Member

@deanp70 deanp70 commented Mar 25, 2026

Based on changes to dagshub-annotation-converter to the same effect: DagsHub/dagshub-annotation-converter#25

Implementation Details

The PR extends annotation support through a multi-layered approach:

Importer Module Enhancements (importer.py):

  • Extended AnnotationType literal to include "mot", and "cvat_video"
  • Introduced is_video_format property to identify video-based annotation types
  • Added video-specific annotation handling via _flatten_video_annotations to convert per-frame annotations into single video-entry dictionaries
  • Implemented video conversion routing in convert_to_ls_tasks for Label Studio video task generation
  • Adjusted remap_annotations to handle cases where video format annotations lack explicit filenames
  • New internal helpers: _is_video_annotation_dict, _flatten_cvat_fs_annotations, _flatten_mot_fs_annotations

Metadata Annotations Support (metadata.py):

  • Added VideoRectangleAnnotation support in Label Studio task lookup registry

Query Result Export API (query_result.py):

  • Two new export methods:
    • export_as_mot() - supports single/multi-video export with optional dimension parameters
    • export_as_cvat_video() - handles video CVAT XML generation with video name customization
  • Added video annotation helpers: _get_all_video_annotations, _prepare_video_file_for_export, _get_annotation_filename
  • Introduced _resolve_annotation_field to auto-select available annotation field with alphabetical fallback

Test Coverage:

  • test_mot.py (383 lines): Video annotation flattening, directory/zip import, MOT export structure, dimension handling
  • test_cvat_video.py (276 lines): CVAT video XML import/export, track/box element generation, multi-datapoint support

Code Quality and Testing Infrastructure:

  • Enhanced ruff.toml linting configuration with comprehensive rule set including:
    • Security checks (flake8-bandit)
    • Bug detection (flake8-bugbear)
    • Try/except pattern validation (tryceratops)
    • Unused argument detection and other design pattern checks
    • Per-file exceptions for __init__.py and conftest.py
  • Added DatasourceType.REPOSITORY assignment in test fixtures (tests/data_engine/conftest.py)
  • Added id property to MockRepoAPI test utility
  • Minor refactor in token_auth.py (no behavioral change - logical equivalence in boolean expression)

Documentation Note: Per PR comments, the supported formats list at dagshub.com/docs/use_cases/annotation/#supported-formats-for-annotation_imports requires manual update to reflect MOT, CVAT Video support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Warning

Rate limit exceeded

@deanp70 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 19 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 6 minutes and 19 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d54f0fae-999e-46e1-9e91-759e613ffb9d

📥 Commits

Reviewing files that changed from the base of the PR and between ad1c465 and 7a76372.

📒 Files selected for processing (1)
  • tests/data_engine/annotation_import/test_cvat_video.py
📝 Walkthrough

Walkthrough

Adds first-class video annotation support (MOT and CVAT video): importer detection/flattening and conversion, Label Studio video parsing/serialization, QueryResult exports for MOT and CVAT video, video-sequence builder, tests, a small token-auth condition tweak, and a package version bump.

Changes

Cohort / File(s) Summary
Version & Auth
dagshub/__init__.py, dagshub/auth/token_auth.py
Bumped package __version__ 0.6.9 → 0.6.10; simplified token-type conditional expression (is not vs not ... is).
Annotation Importer
dagshub/data_engine/annotation/importer.py
Added mot and cvat_video types, is_video_format detection, _is_video_annotation, flattening helpers for CVAT/MOT, loaders for FS/zip/xml, routing for video formats, and _convert_to_ls_video_tasks.
Label Studio metadata
dagshub/data_engine/annotation/metadata.py
Registered videorectangle handling; MetadataAnnotations now serialize video tracks to task.data["video"], compute framesCount, convert IR video tracks to LS video annotations, and copy extra metadata when present.
Video helpers
dagshub/data_engine/annotation/video.py
Added build_video_sequence_from_annotations to group frame annotations into tracks and construct a resolved IRVideoSequence (propagates filenames and resolves dims/length).
QueryResult exports
dagshub/data_engine/model/query_result.py
Refactored annotation collectors to include video, added helpers to build sequences and prepare video files, refactored export_as_yolo field resolution, and added export_as_mot and export_as_cvat_video export flows (single- and multi-source outputs).
Tests: video & MOT
tests/data_engine/annotation_import/test_cvat_video.py, tests/data_engine/annotation_import/test_mot.py, tests/data_engine/annotation_import/test_annotation_parsing.py
New/expanded tests covering CVAT video and MOT import/export, detection/flattening behavior, Label Studio video task serialization, sequence building, export artifacts, and error cases.
Test infra updates
tests/data_engine/conftest.py, tests/mocks/repo_api.py
Mock datasource source_type set to DatasourceType.REPOSITORY; MockRepoAPI.id property added returning 1.
Packaging
setup.py
Replaced PyPI version constraint for dagshub-annotation-converter with a Git direct reference to the video_converters ref (@ git+https://github.com/DagsHub/dagshub-annotation-converter@video_converters#egg=dagshub-annotation-converter).

Sequence Diagram

sequenceDiagram
    participant User
    participant Importer as AnnotationImporter
    participant Metadata as MetadataAnnotations
    participant Query as QueryResult
    participant Exporter as Export Converter
    participant Storage

    User->>Importer: import_annotations(type="mot"/"cvat_video", source)
    activate Importer
    Importer->>Importer: is_video_format? / _is_video_annotation
    Importer->>Metadata: parse -> IRVideoBBoxFrameAnnotation / IRVideoSequence
    Metadata-->>Importer: video-aware annotations
    Importer->>Importer: _flatten_* => per-video sequences
    Importer-->>User: flattened annotations
    deactivate Importer

    User->>Query: export_as_mot()/export_as_cvat_video()
    activate Query
    Query->>Query: _get_all_video_annotations() / _annotations_to_sequences()
    Query->>Exporter: pass sequences (+optional video_file)
    Exporter->>Storage: write MOT dirs or CVAT ZIP(s)
    Exporter-->>Query: export path(s)
    Query-->>User: return export path(s)
    deactivate Query
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • kbolashev

Poem

🐰 Hops through frames in a moonlit row,
MOT and CVAT videos learn to flow.
Tracks stitched tight, frames counted true,
Zips and seqs spring bright and new.
Version bumped — the burrow grew!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add MOT and CVAT video annotation import/export support' directly and clearly describes the main objective of the changeset, which extends annotation support for MOT and CVAT video formats.
Description check ✅ Passed The PR description provides comprehensive details about the implementation, including enhancements to importer, metadata, and query result modules, plus test coverage and tooling improvements, all directly related to the MOT and CVAT video annotation support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mot_cvat_video_converter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Mar 25, 2026
@deanp70 deanp70 requested a review from kbolashev March 25, 2026 11:38
@deanp70 deanp70 self-assigned this Mar 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
tests/data_engine/conftest.py (1)

27-30: Consider keeping the shared datasource fixture configurable.

Hardcoding DatasourceType.REPOSITORY in the common helper means every test that reuses ds/other_ds now bypasses the BUCKET/storage branches in DatasourceState and loader code. A repo-specific fixture or a source_type parameter would keep the new video tests deterministic without narrowing coverage for the rest of tests/data_engine/.

dagshub/data_engine/model/query_result.py (1)

1086-1091: Consider documenting the multiple-key strategy for video_files dict.

The code adds three different keys for the same video file (full path, basename, and stem). While this appears intentional to handle different lookup patterns from the converter, a brief comment explaining this would improve maintainability.

💡 Suggested comment
                     if local_video is None:
                         raise FileNotFoundError(
                             f"Could not find local downloaded video file for '{ann_filename}' "
                             f"under '{local_download_root}'."
                         )
                     ann_path = Path(ann_filename)
+                    # Register multiple keys so the converter can look up by full path, basename, or stem
                     video_files[ann_filename] = local_video
                     video_files[ann_path.name] = local_video
                     video_files[ann_path.stem] = local_video

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 32d9e47a-3ec9-4069-ae90-95375126deb1

📥 Commits

Reviewing files that changed from the base of the PR and between af5fe5d and 3096e28.

📒 Files selected for processing (10)
  • dagshub/__init__.py
  • dagshub/auth/token_auth.py
  • dagshub/data_engine/annotation/importer.py
  • dagshub/data_engine/annotation/metadata.py
  • dagshub/data_engine/annotation/video.py
  • dagshub/data_engine/model/query_result.py
  • tests/data_engine/annotation_import/test_cvat_video.py
  • tests/data_engine/annotation_import/test_mot.py
  • tests/data_engine/conftest.py
  • tests/mocks/repo_api.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build (3.10)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.13)
  • GitHub Check: build (3.9)
🧰 Additional context used
🪛 GitHub Check: Flake8
dagshub/data_engine/annotation/importer.py

[failure] 165-165: dagshub/data_engine/annotation/importer.py#L165
Line too long (136 > 120 characters) (E501)

dagshub/data_engine/model/query_result.py

[failure] 793-793: dagshub/data_engine/model/query_result.py#L793
Undefined name 'IRVideoSequence' (F821)

🪛 Ruff (0.15.6)
tests/data_engine/annotation_import/test_cvat_video.py

[warning] 37-37: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)


[warning] 236-236: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

tests/data_engine/annotation_import/test_mot.py

[warning] 144-144: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)


[warning] 158-158: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)

dagshub/data_engine/model/query_result.py

[error] 793-793: Undefined name IRVideoSequence

(F821)

🔇 Additional comments (8)
dagshub/auth/token_auth.py (1)

40-40: Readability improvement is correct.

Using is not here keeps the same behavior and reads more clearly.

dagshub/data_engine/model/query_result.py (7)

18-25: LGTM!

The new imports for CVAT and MOT video export functionality are well-organized and correctly placed alongside existing annotation converter imports.


806-828: LGTM!

The helper methods _prepare_video_file_for_export and _get_annotation_filename correctly handle edge cases including path resolution with/without datasource prefix and various filename formats (None, list, tuple, string).


830-838: LGTM!

Good refactoring to extract the annotation field resolution logic into a reusable helper method. The alphabetical sorting ensures deterministic behavior when no explicit field is provided.


953-964: Potential edge case: source_names could be empty while video_annotations is non-empty.

If all video annotations have None filenames, source_names will be empty, but the code proceeds as has_multiple_sources = False. This leads to Line 1014 accessing source_names[0] which would raise an IndexError.

However, I see Line 1014 has a fallback: Path(source_names[0]).stem if source_names else "sequence", so this is handled correctly.


974-1019: LGTM!

The export logic properly handles both single and multi-source scenarios with appropriate error handling for missing video files. The conditional video download for dimension probing is a good optimization.


1103-1132: LGTM!

The single-source CVAT export path correctly handles video file resolution and passes appropriate parameters to the converter. The error handling is comprehensive.


865-865: LGTM!

Good refactoring to use the new _resolve_annotation_field helper, which consolidates the annotation field selection logic and improves code reuse across export methods.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
dagshub/data_engine/model/query_result.py (1)

790-793: ⚠️ Potential issue | 🟠 Major

Import IRVideoSequence for the return annotation.

Line 793 references IRVideoSequence, but this module only imports IRVideoBBoxFrameAnnotation. Ruff/Pyflakes still treat that forward ref as undefined here, so linting will keep failing until the symbol is imported.

🐛 Proposed fix
-from dagshub_annotation_converter.ir.video import IRVideoBBoxFrameAnnotation
+from dagshub_annotation_converter.ir.video import IRVideoBBoxFrameAnnotation, IRVideoSequence
🧹 Nitpick comments (1)
tests/data_engine/annotation_import/test_mot.py (1)

295-329: Add a regression for ambiguous video identifiers.

This only exercises unique filenames with ann.filename present on every frame. It won’t catch cam_a/video.mp4 + cam_b/video.mp4, or multiple datapoints that rely on the datapoint path because filename is missing, which are the cases the new grouping code is most likely to break. A small regression case here would lock down the intended behavior.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d642dc65-5715-4c7d-bb0c-50ea13f132b4

📥 Commits

Reviewing files that changed from the base of the PR and between 3096e28 and 6c68a98.

📒 Files selected for processing (3)
  • dagshub/data_engine/annotation/importer.py
  • dagshub/data_engine/model/query_result.py
  • tests/data_engine/annotation_import/test_mot.py
✅ Files skipped from review due to trivial changes (1)
  • dagshub/data_engine/annotation/importer.py

@coderabbitai coderabbitai bot added the auto-review-done Review loop by AI finished label Apr 7, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
dagshub/data_engine/model/query_result.py (4)

803-807: ⚠️ Potential issue | 🟠 Major

Fix unresolved IRVideoSequence type reference at Line 806.

List["IRVideoSequence"] is currently flagged as undefined (F821). Add a type-checking import for IRVideoSequence in this module.

Suggested fix
 if TYPE_CHECKING:
+    from dagshub_annotation_converter.ir.video import IRVideoSequence
     import datasets as hf_ds
#!/bin/bash
# Verify the unresolved symbol and import presence
rg -n 'IRVideoSequence|TYPE_CHECKING' dagshub/data_engine/model/query_result.py

809-816: ⚠️ Potential issue | 🔴 Critical

Do not group sequences by optional filename only.

When filename is missing, the fallback "" merges annotations from different datapoints into a single sequence. Preserve a stable per-datapoint source key through grouping.


985-993: ⚠️ Potential issue | 🔴 Critical

Preserve full source identity; basename/stem introduces collisions.

Using .name / .stem can collapse distinct videos (cam_a/video.mp4 vs cam_b/video.mp4, or video.mp4 vs video.mov). Use full repo-relative identifiers (or sequence keys) for source detection and video_files mapping.

Also applies to: 1062-1069, 1093-1097


994-999: ⚠️ Potential issue | 🟠 Major

Avoid unconditional video downloads in export paths.

Both exporters download videos eagerly even when dimensions are already available from annotations or explicit args. Gate downloads/probing to only sequences that still need width/height inference.

Also applies to: 1074-1080


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 740caa4c-cfd8-423b-acb9-379c7e0d707d

📥 Commits

Reviewing files that changed from the base of the PR and between 6c68a98 and 30ac943.

📒 Files selected for processing (8)
  • dagshub/data_engine/annotation/importer.py
  • dagshub/data_engine/annotation/metadata.py
  • dagshub/data_engine/annotation/video.py
  • dagshub/data_engine/model/query_result.py
  • setup.py
  • tests/data_engine/annotation_import/test_annotation_parsing.py
  • tests/data_engine/annotation_import/test_cvat_video.py
  • tests/data_engine/annotation_import/test_mot.py
✅ Files skipped from review due to trivial changes (1)
  • setup.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • dagshub/data_engine/annotation/metadata.py
  • dagshub/data_engine/annotation/video.py
  • dagshub/data_engine/annotation/importer.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build (3.13)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.9)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.10)
🧰 Additional context used
🪛 GitHub Check: Flake8
tests/data_engine/annotation_import/test_mot.py

[failure] 10-10: tests/data_engine/annotation_import/test_mot.py#L10
'dagshub_annotation_converter.ir.video.IRVideoSequence' imported but unused (F401)

dagshub/data_engine/model/query_result.py

[failure] 806-806: dagshub/data_engine/model/query_result.py#L806
Undefined name 'IRVideoSequence' (F821)

🪛 Ruff (0.15.9)
tests/data_engine/annotation_import/test_mot.py

[warning] 205-205: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)


[warning] 219-219: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)

dagshub/data_engine/model/query_result.py

[error] 806-806: Undefined name IRVideoSequence

(F821)

tests/data_engine/annotation_import/test_cvat_video.py

[warning] 37-37: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)


[warning] 276-276: Pattern passed to match= contains metacharacters but is neither escaped nor raw

(RUF043)

🔇 Additional comments (1)
tests/data_engine/annotation_import/test_annotation_parsing.py (1)

175-213: Good regression coverage for LS video task serialization.

This test validates the key contract (data.video, videorectangle, and framesCount consistency) on the new video-track path.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/data_engine/annotation_import/test_mot.py (2)

205-205: Consider using next(iter(...)) for clarity.

Static analysis suggests next(iter(result.values())) instead of slicing. While the performance difference is negligible in tests, the iterator approach is more idiomatic when you only need the first item.

♻️ Proposed refactor
-    anns = list(result.values())[0]
+    anns = next(iter(result.values()))

219-219: Consider using next(iter(...)) for clarity.

Same suggestion as line 205: next(iter(result.values())) is more idiomatic when extracting the first value.

♻️ Proposed refactor
-    assert len(list(result.values())[0]) == 2
+    assert len(next(iter(result.values()))) == 2

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 885a4f8d-69aa-4620-957e-005d11a8e97f

📥 Commits

Reviewing files that changed from the base of the PR and between 30ac943 and ad1c465.

📒 Files selected for processing (3)
  • dagshub/data_engine/model/query_result.py
  • setup.py
  • tests/data_engine/annotation_import/test_mot.py
✅ Files skipped from review due to trivial changes (1)
  • dagshub/data_engine/model/query_result.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • setup.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build (3.12)
  • GitHub Check: build (3.9)
  • GitHub Check: build (3.13)
  • GitHub Check: build (3.11)
  • GitHub Check: build (3.10)
🧰 Additional context used
🪛 Ruff (0.15.9)
tests/data_engine/annotation_import/test_mot.py

[warning] 205-205: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)


[warning] 219-219: Prefer next(iter(result.values())) over single element slice

Replace with next(iter(result.values()))

(RUF015)

🔇 Additional comments (9)
tests/data_engine/annotation_import/test_mot.py (9)

21-24: LGTM: Clean fixture design.

The autouse fixture provides a sensible default while individual tests can override it as needed.


27-53: LGTM: Comprehensive edge case coverage.

The tests cover all relevant scenarios for video annotation detection, including edge cases like empty dicts and mixed key types.


58-73: LGTM: Well-structured parametrized test.

The parametrized approach efficiently verifies video format detection for all supported annotation types.


79-133: LGTM: Thorough flattening logic validation.

The tests comprehensively verify video annotation flattening across different input formats and naming scenarios, including proper handling of nested paths.


136-156: LGTM: Critical dimension propagation test.

Verifies that video dimensions are correctly propagated from frame-level annotations to the sequence object, which is essential for proper MOT export.


158-192: LGTM: Export path computation validation.

The tests ensure correct directory structure computation across various datasource prefix scenarios, preventing path duplication and ensuring proper layout.


262-275: LGTM: Label Studio conversion validation.

The tests verify proper JSON task generation and confirm that empty annotations are correctly filtered out.


281-440: LGTM: Comprehensive MOT export validation.

The export tests thoroughly cover:

  • Directory structure and file creation
  • Dimension handling (explicit and fallback)
  • Error cases (missing annotations)
  • Multi-video scenarios
  • Path resolution with proper mocking

The tests properly isolate dependencies and verify key behaviors across diverse scenarios.


446-498: LGTM: Well-designed test utilities.

The helper functions effectively reduce duplication and create realistic test fixtures with sensible defaults, improving overall test maintainability.

@deanp70 deanp70 requested a review from kbolashev April 7, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review-done Review loop by AI finished enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants