Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regression test for missing frames after exporting a CVAT dataset #8827

Merged
merged 12 commits into from
Dec 19, 2024
2 changes: 2 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@
"python": "${command:python.interpreterPath}",
"module": "pytest",
"args": [
"--verbose",
"--no-cov", // vscode debugger might not work otherwise
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove inline comment in JSON to maintain valid syntax

JSON does not support comments, so including an inline comment // vscode debugger might not work otherwise in the args array will cause a syntax error. Please remove the comment to ensure the launch.json remains valid.

Apply this diff to remove the inline comment:

     "args": [
         "--verbose",
-        "--no-cov", // vscode debugger might not work otherwise
+        "--no-cov",
         "tests/python/rest_api/"
     ],

Consider documenting the reason for including --no-cov in an external README or documentation file if necessary.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"--no-cov", // vscode debugger might not work otherwise
"--no-cov",

"tests/python/rest_api/"
],
"cwd": "${workspaceFolder}",
Expand Down
2 changes: 2 additions & 0 deletions tests/python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ Pillow==10.3.0
python-dateutil==2.8.2
pyyaml==6.0.0
numpy==2.0.0

# TODO: update pytest to 7.0.0 and pytest-timeout to 2.3.1 (better debug in vscode)
71 changes: 69 additions & 2 deletions tests/python/rest_api/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from itertools import chain, groupby, product
from math import ceil
from operator import itemgetter
from pathlib import Path
from pathlib import Path, PurePosixPath
from tempfile import NamedTemporaryFile, TemporaryDirectory
from time import sleep, time
from typing import Any, Callable, ClassVar, Optional, Union
Expand Down Expand Up @@ -66,6 +66,7 @@
from .utils import (
DATUMARO_FORMAT_FOR_DIMENSION,
CollectionSimpleFilterTestBase,
calc_end_frame,
compare_annotations,
create_task,
export_dataset,
Expand Down Expand Up @@ -3111,7 +3112,7 @@ def _compute_annotation_segment_params(self, task_spec: _TaskSpec) -> list[tuple
stop_frame = getattr(task_spec, "stop_frame", None) or (
start_frame + (task_spec.size - 1) * frame_step
)
end_frame = stop_frame - ((stop_frame - start_frame) % frame_step) + frame_step
end_frame = calc_end_frame(start_frame, stop_frame, frame_step)

validation_params = getattr(task_spec, "validation_params", None)
if validation_params and validation_params.mode.value == "gt_pool":
Expand Down Expand Up @@ -6349,3 +6350,69 @@ def check_element_outside_count(track_idx, element_idx, expected_count):
check_element_outside_count(1, 0, 1)
check_element_outside_count(1, 1, 2)
check_element_outside_count(1, 2, 2)


@pytest.mark.usefixtures("restore_db_per_class")
@pytest.mark.usefixtures("restore_redis_ondisk_per_function")
@pytest.mark.usefixtures("restore_redis_ondisk_after_class")
@pytest.mark.usefixtures("restore_redis_inmem_per_function")
class TestPatchExportFrames(TestTaskData):

@fixture(scope="class")
@parametrize("media_type", [_SourceDataType.images, _SourceDataType.video])
@parametrize("step", [5])
@parametrize("frame_count", [20])
@parametrize("start_frame", [None, 3])
def fxt_uploaded_media_task(
self,
request: pytest.FixtureRequest,
media_type: _SourceDataType,
step: int,
frame_count: int,
start_frame: Optional[int],
) -> Generator[tuple[_TaskSpec, Task, str], None, None]:
args = dict(request=request, frame_count=frame_count, step=step, start_frame=start_frame)

if media_type == _SourceDataType.images:
(spec, task_id) = next(self._uploaded_images_task_fxt_base(**args))
else:
(spec, task_id) = next(self._uploaded_video_task_fxt_base(**args))

with make_sdk_client(self._USERNAME) as client:
task = client.tasks.retrieve(task_id)

yield (spec, task, f"CVAT for {media_type} 1.1")

@pytest.mark.usefixtures("restore_redis_ondisk_per_function")
@parametrize("spec, task, format_name", [fixture_ref(fxt_uploaded_media_task)])
def test_export_with_non_default_frame_step(
self, tmp_path: Path, spec: _TaskSpec, task: Task, format_name: str
):

dataset_file = tmp_path / "dataset.zip"
task.export_dataset(format_name, dataset_file, include_images=True)

def get_img_index(zinfo: zipfile.ZipInfo) -> int:
name = PurePosixPath(zinfo.filename)
if name.suffix.lower() not in (".png", ".jpg", ".jpeg"):
return -1
return int(name.stem.rsplit("_", maxsplit=1)[-1])

# get frames and sort them
with zipfile.ZipFile(dataset_file) as dataset:
frames = np.array(
[png_idx for png_idx in map(get_img_index, dataset.filelist) if png_idx != -1]
)
frames.sort()

task_meta = task.get_meta()
(src_start_frame, src_stop_frame, src_frame_step) = (
task_meta["start_frame"],
task_meta["stop_frame"],
spec.frame_step,
)
src_end_frame = calc_end_frame(src_start_frame, src_stop_frame, src_frame_step)
assert len(frames) == spec.size == task_meta["size"], "Some frames were lost"
assert np.all(
frames == np.arange(src_start_frame, src_end_frame, src_frame_step)
), "Some frames are wrong"
4 changes: 4 additions & 0 deletions tests/python/rest_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,3 +601,7 @@ def _exclude_cb(obj, path):

def parse_frame_step(frame_filter: str) -> int:
return int((frame_filter or "step=1").split("=")[1])


def calc_end_frame(start_frame: int, stop_frame: int, frame_step: int) -> int:
return stop_frame - ((stop_frame - start_frame) % frame_step) + frame_step
Loading