" # if applicable
+```
+
+## Running on an object
+
+The tracker can be applied to any polygons and masks. To run the tracker on an object, open the object menu and click
+"Run annotation action".
+
+
+
+Alternatively, you can use a hotkey: select the object and press **Ctrl + E** (default shortcut).
+When the modal opened, in "Select action" list, choose **Segment Anything 2: Tracker**:
+
+
+
+Specify the **target frame** until which you want the object to be tracked,
+then click the **Run** button to start tracking. The process begins and may take some time to complete.
+The duration depends on the inference device, and the number of frames where the object will be tracked.
+
+
+
+Once the process is complete, the modal window closes. You can review how the object was tracked.
+If you notice that the tracked shape deteriorates at some point,
+you can adjust the object coordinates and run the tracker again from that frame.
+
+## Running on multiple objects
+
+Instead of tracking each object individually, you can track multiple objects
+simultaneously. To do this, click the **Menu** button in the annotation view and select the **Run Actions** option:
+
+
+
+Alternatively, you can use a hotkey: just press **Ctrl + E** (default shortcut) when there are no objects selected.
+This opens the actions modal. In this case, the tracker will be applied to all visible objects of suitable types
+(polygons and masks). In the action list of the opened model, select **Segment Anything 2: Tracker**:
+
+
+
+Specify the **target frame** until which you want the objects to be tracked,
+then click the **Run** button to start tracking. The process begins and may take some time to complete.
+The duration depends on the inference device, the number of simultaneously tracked objects,
+and the number of frames where the object will be tracked.
+
+
+
+Once the process finishes, you may close the modal and review how the objects were tracked.
+If you notice that the tracked shapes deteriorate, you can adjust their
+coordinates and run the tracker again from that frame (for a single object or for many objects).
+
+
+## Tracker parameters
+
+- **Target frame**: Objects will be tracked up to this frame. Must be greater than the current frame
+- **Convert polygon shapes to tracks**: When enabled, all visible polygon shapes in the current frame will be converted
+to tracks before tracking begins. Use this option if you need tracks as the final output but started with shapes,
+produced for example by interactors (e.g. SAM2 or another one).
diff --git a/site/content/en/docs/enterprise/shapes-converter.md b/site/content/en/docs/enterprise/shapes-converter.md
index fbceb757c3f1..43caf0e122eb 100644
--- a/site/content/en/docs/enterprise/shapes-converter.md
+++ b/site/content/en/docs/enterprise/shapes-converter.md
@@ -50,7 +50,7 @@ With the following fields:
If unsaved changes are detected, a prompt will advise to save these changes
to avoid any potential loss of data.
-- **Disabу auto-save:** Prior to running the annotation action, disabling the auto-save feature
+- **Disable auto-save:** Prior to running the annotation action, disabling the auto-save feature
is advisable. A notification will suggest this action if auto-save is currently active.
- **Committing changes:** Changes applied during the annotation session
diff --git a/site/content/en/docs/enterprise/social-accounts-configuration.md b/site/content/en/docs/enterprise/social-accounts-configuration.md
index 19e1a6b78367..83b7f463a27e 100644
--- a/site/content/en/docs/enterprise/social-accounts-configuration.md
+++ b/site/content/en/docs/enterprise/social-accounts-configuration.md
@@ -2,7 +2,7 @@
title: 'Social auth configuration'
linkTitle: 'Social auth configuration'
weight: 3
-description: 'Social accounts authentication for Self-Hosted solution'
+description: 'Social accounts authentication for a Self-Hosted solution'
---
> **Note:** This is a paid feature available for [Enterprise clients](https://www.cvat.ai/pricing/on-prem).
@@ -51,7 +51,7 @@ To enable authentication, do the following:
configure: **Application name**, **Authorized JavaScript origins**, **Authorized redirect URIs**.
For example, if you plan to deploy CVAT instance on `https://localhost:8080`, add `https://localhost:8080`
to authorized JS origins and `https://localhost:8080/api/auth/social/goolge/login/callback/` to redirect URIs.
-8. Create conпiguration file in CVAT:
+8. Create configuration file in CVAT:
1. Create the `auth_config.yml` file with the following content:
@@ -81,7 +81,7 @@ There are 2 basic steps to enable GitHub account authentication.
For more information, see [Creating an OAuth App](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app)
3. Fill in the name field, set the homepage URL (for example: `https://localhost:8080`),
and authentication callback URL (for example: `https://localhost:8080/api/auth/social/github/login/callback/`).
-4. Create conпiguration file in CVAT:
+4. Create configuration file in CVAT:
1. Create the `auth_config.yml` file with the following content:
@@ -114,7 +114,7 @@ To enable authentication, do the following:
see [Amazon Cognito user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html)
2. Fill in the name field, set the homepage URL (for example: `https://localhost:8080`),
and authentication callback URL (for example: `https://localhost:8080/api/auth/social/amazon-cognito/login/callback/`).
-3. Create conпiguration file in CVAT:
+3. Create configuration file in CVAT:
1. Create the `auth_config.yml` file with the following content:
diff --git a/site/content/en/docs/faq.md b/site/content/en/docs/faq.md
index 7fef81c88d92..99db9a8b9248 100644
--- a/site/content/en/docs/faq.md
+++ b/site/content/en/docs/faq.md
@@ -84,6 +84,9 @@ services:
cvat_worker_annotation:
volumes:
- cvat_share:/home/django/share:ro
+ cvat_worker_chunks:
+ volumes:
+ - cvat_share:/home/django/share:ro
volumes:
cvat_share:
diff --git a/site/content/en/docs/manual/advanced/analytics-and-monitoring/auto-qa.md b/site/content/en/docs/manual/advanced/analytics-and-monitoring/auto-qa.md
index 19166c79aea5..21ebd2d99087 100644
--- a/site/content/en/docs/manual/advanced/analytics-and-monitoring/auto-qa.md
+++ b/site/content/en/docs/manual/advanced/analytics-and-monitoring/auto-qa.md
@@ -493,9 +493,11 @@ Once the quality estimation is [enabled in a task](#configuring-quality-estimati
and the Ground Truth job is configured, quality analytics becomes available
for the task and its jobs.
-By default, CVAT computes quality metrics automatically at regular intervals.
+When you open the Quality Analytics page, it displays quality metrics from the most recent quality estimation.
+If it's your first time accessing the page, no quality report will be available yet.
+The date of the last computation is shown next to the report download button.
-If you want to refresh quality metrics (e.g. after the settings were changed),
+If you want to request updating of quality metrics in a task (e.g. after the settings were changed),
you can do this by pressing the **Refresh** button on the
task **Quality Management** > **Analytics** page.
diff --git a/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md b/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md
index 047c10605a05..5920c1c76ec8 100644
--- a/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md
+++ b/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md
@@ -7,7 +7,7 @@ weight: 6
### Cutting holes in polygons
Currently, CVAT does not support cutting transparent holes in polygons. However,
-it is poissble to generate holes in exported instance and class masks.
+it is possible to generate holes in exported instance and class masks.
To do this, one needs to define a background class in the task and draw holes
with it as additional shapes above the shapes needed to have holes:
diff --git a/site/content/en/docs/manual/advanced/contextual-images.md b/site/content/en/docs/manual/advanced/contextual-images.md
index 4dcdf6d178ce..fb5eda39b52f 100644
--- a/site/content/en/docs/manual/advanced/contextual-images.md
+++ b/site/content/en/docs/manual/advanced/contextual-images.md
@@ -148,7 +148,7 @@ Each context image has the following elements:
| 1 | **Full screen**. Click to expand the contextual image in to the full screen mode. Click again to revert contextual image to windowed mode. |
| 2 | **Move contextual image**. Hold and move contextual image to the other place on the screen.
![contex_images_3](/images/context_img_03.gif) |
| 3 | **Name**. Unique contextual image name |
-| 4 | **Select contextual image**. Click to open a horisontal listview of all available contextual images.
Click on one to select. |
+| 4 | **Select contextual image**. Click to open a horizontal listview of all available contextual images.
Click on one to select. |
| 5 | **Close**. Click to remove image from contextual images menu. |
| 6 | **Extend** Hold and pull to extend the image. |
diff --git a/site/content/en/docs/manual/advanced/formats/format-coco.md b/site/content/en/docs/manual/advanced/formats/format-coco.md
index 4b467dff3f11..72193d440be5 100644
--- a/site/content/en/docs/manual/advanced/formats/format-coco.md
+++ b/site/content/en/docs/manual/advanced/formats/format-coco.md
@@ -57,7 +57,7 @@ such as `instances`, `panoptic`, `image_info`, `labels`, `captions`, or `stuff`.
## COCO import
-Uplod format: a single unpacked `*.json` or a zip archive with the structure described above or
+Upload format: a single unpacked `*.json` or a zip archive with the structure described above or
[here](https://openvinotoolkit.github.io/datumaro/latest/docs/data-formats/formats/coco.html#import-coco-dataset)
(without images).
diff --git a/site/content/en/docs/manual/advanced/single-shape.md b/site/content/en/docs/manual/advanced/single-shape.md
index e6d02c994976..78fea012f8a8 100644
--- a/site/content/en/docs/manual/advanced/single-shape.md
+++ b/site/content/en/docs/manual/advanced/single-shape.md
@@ -50,7 +50,7 @@ The **Single Shape** annotation mode has the following fields:
| **Skip Button** | Enables moving to the next frame without annotating the current one, particularly useful when the frame does not have anything to be annotated. |
| **List of Hints** | Offers guidance on using the interface effectively, including:
- Click **Skip** for frames without required annotations.
- Hold the **Alt** button to avoid unintentional drawing (e.g. when you want only move the image).
- Use the **Ctrl+Z** combination to undo the last action if needed.
- Use the **Esc** button to completely reset the current drawing progress. |
| **Label selector** | Allows for the selection of different labels (`cat`, or `dog` in our example) for annotation within the interface. |
-| **Label type selector** | A drop-down list to select type of the label (rectangle, ellipce, etc). Only visible when the type of the shape is **Any**. |
+| **Label type selector** | A drop-down list to select type of the label (rectangle, ellipse, etc). Only visible when the type of the shape is **Any**. |
| **Options to Enable or Disable** | Provides configurable options to streamline the annotation process, such as:
- **Automatically go to the next frame**.
- **Automatically save when finish**.
- **Navigate only empty frames**.
- **Predefined number of points** - Specific to polyshape annotations, enabling this option auto-completes a shape once a predefined number of points is reached. Otherwise, pressing **N** is required to finalize the shape. |
| **Number of Points** | Applicable for polyshape annotations, indicating the number of points to use for image annotation. |
diff --git a/site/content/en/docs/manual/basics/CVAT-annotation-Interface/controls-sidebar.md b/site/content/en/docs/manual/basics/CVAT-annotation-Interface/controls-sidebar.md
index 07a7d957b47f..775ab3c5dbaa 100644
--- a/site/content/en/docs/manual/basics/CVAT-annotation-Interface/controls-sidebar.md
+++ b/site/content/en/docs/manual/basics/CVAT-annotation-Interface/controls-sidebar.md
@@ -10,7 +10,7 @@ description: 'Offers tools for navigating within the image, annotation tools, an
**Navigation block** - contains tools for moving and rotating images.
|Icon |Description |
|-- |-- |
-|![](/images/image148.jpg)|`Cursor` (`Esc`)- a basic annotation pedacting tool. |
+|![](/images/image148.jpg)|`Cursor` (`Esc`)- a basic annotation editing tool. |
|![](/images/image149.jpg)|`Move the image`- a tool for moving around the image without
the possibility of editing.|
|![](/images/image102.jpg)|`Rotate`- two buttons to rotate the current frame
a clockwise (`Ctrl+R`) and anticlockwise (`Ctrl+Shift+R`).
You can enable `Rotate all images` in the settings to rotate all the images in the job|
diff --git a/site/content/en/docs/manual/basics/CVAT-annotation-Interface/navbar.md b/site/content/en/docs/manual/basics/CVAT-annotation-Interface/navbar.md
index 74a6af04120b..034f6b9aad3f 100644
--- a/site/content/en/docs/manual/basics/CVAT-annotation-Interface/navbar.md
+++ b/site/content/en/docs/manual/basics/CVAT-annotation-Interface/navbar.md
@@ -82,6 +82,6 @@ toggle between different annotation and QA modes.
| **Fullscreen**
![Fullscreen](/images/image143.jpg) | The fullscreen player mode. The keyboard shortcut is **F11**. |
| **Info**
![Info](/images/image143_2.png) | Open the job info.
![](/images/image144_detrac.png)
Overview:
- **Assignee** - the individual to whom the job is assigned.
- **Reviewer**– the user tasked with conducting the review. For more information, see [**Manual QA**](/docs/manual/advanced/analytics-and-monitoring/manual-qa")
- **Start frame** - the number of the first frame in this job.
- **Stop frame** - the number of the last frame in this job.
- **Frames** - the total number of frames in the job.
**Annotations Statistics** table displays the number of created shapes, categorized by labels (e.g., vehicle, person) and the type of annotation (shape, track), as well as the count of manual and interpolated frames. |
| **Filters**
![](/images/image143_3.png) | Switches on [**Filters**](/docs/manual/advanced/filter/). |
-| **Workplace Switcher** | The drop-down list to swithc between different annotation modes:
![](/images/ui-swithcer.png)
Overview:- **Standart** -- default mode.
- **Attribute** -- annotation with [**Attributes**](docs/manual/advanced/attribute-annotation-mode-advanced/)
- **Single Shape** -- [**Single shape**](/docs/manual/advanced/single-shape/) annotation mode.
- **Tag annotation**- annotation with [Tags](/docs/manual/advanced/annotation-with-tags/)
- **Review** -- [**Manual QA**](/manual/advanced/analytics-and-monitoring/manual-qa/) mode. |
+| **Workplace Switcher** | The drop-down list to switch between different annotation modes:
![](/images/ui-swithcer.png)
Overview:- **Standard** -- default mode.
- **Attribute** -- annotation with [**Attributes**](docs/manual/advanced/attribute-annotation-mode-advanced/)
- **Single Shape** -- [**Single shape**](/docs/manual/advanced/single-shape/) annotation mode.
- **Tag annotation**- annotation with [Tags](/docs/manual/advanced/annotation-with-tags/)
- **Review** -- [**Manual QA**](/manual/advanced/analytics-and-monitoring/manual-qa/) mode. |
diff --git a/site/content/en/docs/manual/basics/attach-cloud-storage.md b/site/content/en/docs/manual/basics/attach-cloud-storage.md
index 4bc3e14e8c16..075b9a2d1a71 100644
--- a/site/content/en/docs/manual/basics/attach-cloud-storage.md
+++ b/site/content/en/docs/manual/basics/attach-cloud-storage.md
@@ -159,7 +159,7 @@ aws s3 cp --recursive
```
4. After copying the files, you can create a manifest file as described in
- {{< ilink "/docs/manual/advanced/dataset_manifest" "preapair manifest file section" >}}:
+ {{< ilink "/docs/manual/advanced/dataset_manifest" "prepare manifest file section" >}}:
```bash
python /utils/dataset_manifest/create.py --output-dir
@@ -329,7 +329,7 @@ To create bucket, do the following:
- **Storage account name**: to access container from CVAT.
- Select a region closest to you.
- - Select **Performance** > **Standart**.
+ - Select **Performance** > **Standard**.
- Select **Local-redundancy storage (LRS)**.
- Click **next: Advanced>**.
@@ -387,7 +387,7 @@ Use the SAS token or connection string to grant secure access to the container.
To configure the credentials:
-1. Go to **Home** > **Resourse groups** > You resource name > Your storage account.
+1. Go to **Home** > **Resource groups** > You resource name > Your storage account.
2. On the left menu, click **Shared access signature**.
3. Change the following fields:
- **Allowed services**: Enable **Blob** . Disable all other fields.
diff --git a/site/content/en/docs/manual/basics/create_an_annotation_task.md b/site/content/en/docs/manual/basics/create_an_annotation_task.md
index b9501a110765..1e4c84a941f3 100644
--- a/site/content/en/docs/manual/basics/create_an_annotation_task.md
+++ b/site/content/en/docs/manual/basics/create_an_annotation_task.md
@@ -260,7 +260,7 @@ The following parameters are available:
| Use zip/video chunks | Use this parameter to divide your video or image dataset for annotation into short video clips a zip file of frames.
Zip files are larger but do not require decoding on the client side, and video clips are smaller but require decoding.
It is recommended to turn off this parameter for video tasks to reduce traffic between the client side and the server. |
| Use cache | Select checkbox, to enable _on-the-fly_ data processing to reduce task creation time and store data in a cache with a policy of
evicting less popular items.
For more information, see {{< ilink "/docs/manual/advanced/data_on_fly" "Data preparation on the fly" >}}. |
| Image Quality | CVAT has two types of data: original quality and compressed. Original quality images are used for dataset export
and automatic annotation. Compressed images are used only for annotations to reduce traffic between the server
and client side.
It is recommended to adjust the compression level only if the images contain small objects that are not
visible in the original quality.
Values range from `5` (highly compressed images) to `100` (not compressed |
-| Overlap Size | Use this parameter to create overlapped segments, making tracking continuous from one segment to another.
**Note** that this functionality only works for bounding boxes.
This parameter has the following options:
**Interpolation task** (video sequence). If you annotate with a bounding box on two adjacent segments, they will be
merged into a single bounding box. In case the overlap is zero or the bounding box is inaccurate (not enclosing the object
properly, misaligned or distorted) on the adjacent segments, it may be difficult to accurately interpole the object's
movement between the segments. As a result, multiple tracks will be created for the same object.
**Annotation task** (independent images). If an object exists on overlapped segments with overlap greater than zero,
and the annotation of these segments is done properly, then the segments will be automatically merged into a single
object. If the overlap is zero or the annotation is inaccurate (not enclosing the object properly, misaligned, distorted) on the
adjacent segments, it may be difficult to accurately track the object. As a result, multiple bounding boxes will be
created for the same object.
If the annotations on different segments (on overlapped frames) are very different, you will have two shapes
for the same object.
To avoid this, accurately annotate the object on the first segment and the same object on the second segment to create a track
between two annotations. |
+| Overlap Size | Use this parameter to create overlapped segments, making tracking continuous from one segment to another.
**Note** that this functionality only works for bounding boxes.
This parameter has the following options:
**Interpolation task** (video sequence). If you annotate with a bounding box on two adjacent segments, they will be
merged into a single bounding box. In case the overlap is zero or the bounding box is inaccurate (not enclosing the object
properly, misaligned or distorted) on the adjacent segments, it may be difficult to accurately interpolate the object's
movement between the segments. As a result, multiple tracks will be created for the same object.
**Annotation task** (independent images). If an object exists on overlapped segments with overlap greater than zero,
and the annotation of these segments is done properly, then the segments will be automatically merged into a single
object. If the overlap is zero or the annotation is inaccurate (not enclosing the object properly, misaligned, distorted) on the
adjacent segments, it may be difficult to accurately track the object. As a result, multiple bounding boxes will be
created for the same object.
If the annotations on different segments (on overlapped frames) are very different, you will have two shapes
for the same object.
To avoid this, accurately annotate the object on the first segment and the same object on the second segment to create a track
between two annotations. |
| Segment size | Use this parameter to divide a dataset into smaller parts. For example, if you want to share a dataset among multiple
annotators, you can split it into smaller sections and assign each section to a separate job.
This allows annotators to work on the same dataset concurrently. |
| Start frame | Defines the first frame of the video. |
| Stop frame | Defines the last frame of the video. |
diff --git a/site/content/en/docs/manual/basics/registration.md b/site/content/en/docs/manual/basics/registration.md
index a56876d79554..8d3a282b1ae7 100644
--- a/site/content/en/docs/manual/basics/registration.md
+++ b/site/content/en/docs/manual/basics/registration.md
@@ -40,7 +40,7 @@ To register, do the following:
A username generates from the email automatically. You can edit it if needed.
-![Usernname generation](/images/filling_email.gif)
+![Username generation](/images/filling_email.gif)
## User registration with social accounts
diff --git a/site/content/en/images/sam2_tracker_run_action.png b/site/content/en/images/sam2_tracker_run_action.png
new file mode 100644
index 000000000000..05d7222699be
Binary files /dev/null and b/site/content/en/images/sam2_tracker_run_action.png differ
diff --git a/site/content/en/images/sam2_tracker_run_action_modal.png b/site/content/en/images/sam2_tracker_run_action_modal.png
new file mode 100644
index 000000000000..689dc8b1e1fb
Binary files /dev/null and b/site/content/en/images/sam2_tracker_run_action_modal.png differ
diff --git a/site/content/en/images/sam2_tracker_run_action_modal_progress.png b/site/content/en/images/sam2_tracker_run_action_modal_progress.png
new file mode 100644
index 000000000000..f2fddc89f978
Binary files /dev/null and b/site/content/en/images/sam2_tracker_run_action_modal_progress.png differ
diff --git a/site/content/en/images/sam2_tracker_run_shape_action.png b/site/content/en/images/sam2_tracker_run_shape_action.png
new file mode 100644
index 000000000000..c13021c3b799
Binary files /dev/null and b/site/content/en/images/sam2_tracker_run_shape_action.png differ
diff --git a/site/content/en/images/sam2_tracker_run_shape_action_modal.png b/site/content/en/images/sam2_tracker_run_shape_action_modal.png
new file mode 100644
index 000000000000..daeb2501ab52
Binary files /dev/null and b/site/content/en/images/sam2_tracker_run_shape_action_modal.png differ
diff --git a/site/content/en/images/sam2_tracker_run_shape_action_modal_progress.png b/site/content/en/images/sam2_tracker_run_shape_action_modal_progress.png
new file mode 100644
index 000000000000..c590a872a634
Binary files /dev/null and b/site/content/en/images/sam2_tracker_run_shape_action_modal_progress.png differ
diff --git a/tests/cypress/e2e/features/requests_page.js b/tests/cypress/e2e/features/requests_page.js
index 8622092b89d8..f78554986356 100644
--- a/tests/cypress/e2e/features/requests_page.js
+++ b/tests/cypress/e2e/features/requests_page.js
@@ -322,9 +322,11 @@ context('Requests page', () => {
cy.getJobIDFromIdx(0).then((jobID) => {
const closeExportNotification = () => {
- cy.contains('Export is finished').should('be.visible');
- cy.contains('Export is finished').parents('.ant-notification-notice')
- .find('span[aria-label="close"]').click();
+ cy.get('.ant-notification-notice').first().within((notification) => {
+ cy.contains('Export is finished').should('be.visible');
+ cy.get('span[aria-label="close"]').click();
+ cy.wrap(notification).should('not.exist');
+ });
};
const exportParams = {
diff --git a/tests/python/README.md b/tests/python/README.md
index 74373153085b..3a7b246c5508 100644
--- a/tests/python/README.md
+++ b/tests/python/README.md
@@ -20,13 +20,15 @@ the server calling REST API directly (as it done by users).
## How to run?
**Initial steps**
+1. On Debian/Ubuntu, make sure that your `$USER` is in `docker` group:
+ ```shell
+ sudo usermod -aG docker $USER
+ ```
1. Follow [this guide](../../site/content/en/docs/api_sdk/sdk/developer-guide.md) to prepare
`cvat-sdk` and `cvat-cli` source code
1. Install all necessary requirements before running REST API tests:
- ```
+ ```shell
pip install -r ./tests/python/requirements.txt
- pip install -e ./cvat-sdk
- pip install -e ./cvat-cli
```
1. Stop any other CVAT containers which you run previously. They keep ports
which are used by containers for the testing system.
diff --git a/tests/python/cli/conftest.py b/tests/python/cli/conftest.py
index c36974b6ca5d..c7a8fe7da4db 100644
--- a/tests/python/cli/conftest.py
+++ b/tests/python/cli/conftest.py
@@ -2,4 +2,4 @@
#
# SPDX-License-Identifier: MIT
-from sdk.fixtures import fxt_client # pylint: disable=unused-import
+from sdk.fixtures import * # pylint: disable=unused-import
diff --git a/tests/python/cli/test_cli_misc.py b/tests/python/cli/test_cli_misc.py
new file mode 100644
index 000000000000..ea4a3f380430
--- /dev/null
+++ b/tests/python/cli/test_cli_misc.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2022-2023 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import json
+import os
+
+import packaging.version as pv
+import pytest
+from cvat_sdk import Client
+from cvat_sdk.api_client import models
+from cvat_sdk.core.proxies.tasks import ResourceType
+
+from .util import TestCliBase, generate_images, https_reverse_proxy, run_cli
+
+
+class TestCliMisc(TestCliBase):
+ def test_can_warn_on_mismatching_server_version(self, monkeypatch, caplog):
+ def mocked_version(_):
+ return pv.Version("0")
+
+ # We don't actually run a separate process in the tests here, so it works
+ monkeypatch.setattr(Client, "get_server_version", mocked_version)
+
+ self.run_cli("task", "ls")
+
+ assert "Server version '0' is not compatible with SDK version" in caplog.text
+
+ @pytest.mark.parametrize("verify", [True, False])
+ def test_can_control_ssl_verification_with_arg(self, verify: bool):
+ with https_reverse_proxy() as proxy_url:
+ if verify:
+ insecure_args = []
+ else:
+ insecure_args = ["--insecure"]
+
+ run_cli(
+ self,
+ f"--auth={self.user}:{self.password}",
+ f"--server-host={proxy_url}",
+ *insecure_args,
+ "task",
+ "ls",
+ expected_code=1 if verify else 0,
+ )
+ stdout = self.stdout.getvalue()
+
+ if not verify:
+ for line in stdout.splitlines():
+ int(line)
+
+ def test_can_control_organization_context(self):
+ org = "cli-test-org"
+ self.client.organizations.create(models.OrganizationWriteRequest(org))
+
+ files = generate_images(self.tmp_path, 1)
+
+ stdout = self.run_cli(
+ "task",
+ "create",
+ "personal_task",
+ ResourceType.LOCAL.name,
+ *map(os.fspath, files),
+ "--labels=" + json.dumps([{"name": "person"}]),
+ "--completion_verification_period=0.01",
+ organization="",
+ )
+
+ personal_task_id = int(stdout.split()[-1])
+
+ stdout = self.run_cli(
+ "task",
+ "create",
+ "org_task",
+ ResourceType.LOCAL.name,
+ *map(os.fspath, files),
+ "--labels=" + json.dumps([{"name": "person"}]),
+ "--completion_verification_period=0.01",
+ organization=org,
+ )
+
+ org_task_id = int(stdout.split()[-1])
+
+ personal_task_ids = list(map(int, self.run_cli("task", "ls", organization="").split()))
+ assert personal_task_id in personal_task_ids
+ assert org_task_id not in personal_task_ids
+
+ org_task_ids = list(map(int, self.run_cli("task", "ls", organization=org).split()))
+ assert personal_task_id not in org_task_ids
+ assert org_task_id in org_task_ids
+
+ all_task_ids = list(map(int, self.run_cli("task", "ls").split()))
+ assert personal_task_id in all_task_ids
+ assert org_task_id in all_task_ids
diff --git a/tests/python/cli/test_cli_projects.py b/tests/python/cli/test_cli_projects.py
new file mode 100644
index 000000000000..032085b52d56
--- /dev/null
+++ b/tests/python/cli/test_cli_projects.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2022-2023 CVAT.ai Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import json
+import os
+
+import pytest
+from cvat_sdk.api_client import exceptions
+from cvat_sdk.core.proxies.projects import Project
+
+from .util import TestCliBase
+
+
+class TestCliProjects(TestCliBase):
+ @pytest.fixture
+ def fxt_new_project(self):
+ project = self.client.projects.create(
+ spec={
+ "name": "test_project",
+ "labels": [{"name": "car"}, {"name": "person"}],
+ },
+ )
+
+ return project
+
+ def test_can_create_project(self):
+ stdout = self.run_cli(
+ "project",
+ "create",
+ "new_project",
+ "--labels",
+ json.dumps([{"name": "car"}, {"name": "person"}]),
+ "--bug_tracker",
+ "https://bugs.example/",
+ )
+
+ project_id = int(stdout.rstrip("\n"))
+ created_project = self.client.projects.retrieve(project_id)
+ assert created_project.name == "new_project"
+ assert created_project.bug_tracker == "https://bugs.example/"
+ assert {label.name for label in created_project.get_labels()} == {"car", "person"}
+
+ def test_can_create_project_from_dataset(self, fxt_coco_dataset):
+ stdout = self.run_cli(
+ "project",
+ "create",
+ "new_project",
+ "--dataset_path",
+ os.fspath(fxt_coco_dataset),
+ "--dataset_format",
+ "COCO 1.0",
+ )
+
+ project_id = int(stdout.rstrip("\n"))
+ created_project = self.client.projects.retrieve(project_id)
+ assert created_project.name == "new_project"
+ assert {label.name for label in created_project.get_labels()} == {"car", "person"}
+ assert created_project.tasks.count == 1
+
+ def test_can_list_projects_in_simple_format(self, fxt_new_project: Project):
+ output = self.run_cli("project", "ls")
+
+ results = output.split("\n")
+ assert any(str(fxt_new_project.id) in r for r in results)
+
+ def test_can_list_project_in_json_format(self, fxt_new_project: Project):
+ output = self.run_cli("project", "ls", "--json")
+
+ results = json.loads(output)
+ assert any(r["id"] == fxt_new_project.id for r in results)
+
+ def test_can_delete_project(self, fxt_new_project: Project):
+ self.run_cli("project", "delete", str(fxt_new_project.id))
+
+ with pytest.raises(exceptions.NotFoundException):
+ fxt_new_project.fetch()
diff --git a/tests/python/cli/test_cli.py b/tests/python/cli/test_cli_tasks.py
similarity index 63%
rename from tests/python/cli/test_cli.py
rename to tests/python/cli/test_cli_tasks.py
index 5a2fb6b0506d..d0af410a7c99 100644
--- a/tests/python/cli/test_cli.py
+++ b/tests/python/cli/test_cli_tasks.py
@@ -2,49 +2,22 @@
#
# SPDX-License-Identifier: MIT
-import io
import json
import os
from pathlib import Path
-from typing import Optional
-import packaging.version as pv
import pytest
-from cvat_sdk import Client, make_client
-from cvat_sdk.api_client import exceptions, models
+from cvat_sdk.api_client import exceptions
from cvat_sdk.core.proxies.tasks import ResourceType, Task
from PIL import Image
from sdk.util import generate_coco_json
-from shared.utils.config import BASE_URL, USER_PASS
from shared.utils.helpers import generate_image_file
-from .util import generate_images, https_reverse_proxy, run_cli
-
-
-class TestCLI:
- @pytest.fixture(autouse=True)
- def setup(
- self,
- restore_db_per_function, # force fixture call order to allow DB setup
- restore_redis_inmem_per_function,
- restore_redis_ondisk_per_function,
- fxt_stdout: io.StringIO,
- tmp_path: Path,
- admin_user: str,
- ):
- self.tmp_path = tmp_path
- self.stdout = fxt_stdout
- self.host, self.port = BASE_URL.rsplit(":", maxsplit=1)
- self.user = admin_user
- self.password = USER_PASS
- self.client = make_client(
- host=self.host, port=self.port, credentials=(self.user, self.password)
- )
- self.client.config.status_check_period = 0.01
+from .util import TestCliBase, generate_images
- yield
+class TestCliTasks(TestCliBase):
@pytest.fixture
def fxt_image_file(self):
img_path = self.tmp_path / "img_0.png"
@@ -86,31 +59,11 @@ def fxt_new_task(self):
return task
- def run_cli(
- self, cmd: str, *args: str, expected_code: int = 0, organization: Optional[str] = None
- ) -> str:
- common_args = [
- f"--auth={self.user}:{self.password}",
- f"--server-host={self.host}",
- f"--server-port={self.port}",
- ]
-
- if organization is not None:
- common_args.append(f"--organization={organization}")
-
- run_cli(
- self,
- *common_args,
- cmd,
- *args,
- expected_code=expected_code,
- )
- return self.stdout.getvalue()
-
def test_can_create_task_from_local_images(self):
files = generate_images(self.tmp_path, 5)
stdout = self.run_cli(
+ "task",
"create",
"test_task",
ResourceType.LOCAL.name,
@@ -121,7 +74,7 @@ def test_can_create_task_from_local_images(self):
"0.01",
)
- task_id = int(stdout.split()[-1])
+ task_id = int(stdout.rstrip("\n"))
assert self.client.tasks.retrieve(task_id).size == 5
def test_can_create_task_from_local_images_with_parameters(self):
@@ -132,6 +85,7 @@ def test_can_create_task_from_local_images_with_parameters(self):
frame_step = 3
stdout = self.run_cli(
+ "task",
"create",
"test_task",
ResourceType.LOCAL.name,
@@ -148,7 +102,7 @@ def test_can_create_task_from_local_images_with_parameters(self):
"http://localhost/bug",
)
- task_id = int(stdout.split()[-1])
+ task_id = int(stdout.rstrip("\n"))
task = self.client.tasks.retrieve(task_id)
frames = task.get_frames_info()
assert [f.name for f in frames] == [
@@ -158,19 +112,19 @@ def test_can_create_task_from_local_images_with_parameters(self):
assert task.bug_tracker == "http://localhost/bug"
def test_can_list_tasks_in_simple_format(self, fxt_new_task: Task):
- output = self.run_cli("ls")
+ output = self.run_cli("task", "ls")
results = output.split("\n")
assert any(str(fxt_new_task.id) in r for r in results)
def test_can_list_tasks_in_json_format(self, fxt_new_task: Task):
- output = self.run_cli("ls", "--json")
+ output = self.run_cli("task", "ls", "--json")
results = json.loads(output)
assert any(r["id"] == fxt_new_task.id for r in results)
def test_can_delete_task(self, fxt_new_task: Task):
- self.run_cli("delete", str(fxt_new_task.id))
+ self.run_cli("task", "delete", str(fxt_new_task.id))
with pytest.raises(exceptions.NotFoundException):
fxt_new_task.fetch()
@@ -178,7 +132,8 @@ def test_can_delete_task(self, fxt_new_task: Task):
def test_can_download_task_annotations(self, fxt_new_task: Task):
filename = self.tmp_path / "task_{fxt_new_task.id}-cvat.zip"
self.run_cli(
- "dump",
+ "task",
+ "export-dataset",
str(fxt_new_task.id),
str(filename),
"--format",
@@ -194,7 +149,8 @@ def test_can_download_task_annotations(self, fxt_new_task: Task):
def test_can_download_task_backup(self, fxt_new_task: Task):
filename = self.tmp_path / "task_{fxt_new_task.id}-cvat.zip"
self.run_cli(
- "export",
+ "task",
+ "backup",
str(fxt_new_task.id),
str(filename),
"--completion_verification_period",
@@ -207,6 +163,7 @@ def test_can_download_task_backup(self, fxt_new_task: Task):
def test_can_download_task_frames(self, fxt_new_task: Task, quality: str):
out_dir = str(self.tmp_path / "downloads")
self.run_cli(
+ "task",
"frames",
str(fxt_new_task.id),
"0",
@@ -222,96 +179,29 @@ def test_can_download_task_frames(self, fxt_new_task: Task, quality: str):
}
def test_can_upload_annotations(self, fxt_new_task: Task, fxt_coco_file: Path):
- self.run_cli("upload", str(fxt_new_task.id), str(fxt_coco_file), "--format", "COCO 1.0")
+ self.run_cli(
+ "task",
+ "import-dataset",
+ str(fxt_new_task.id),
+ str(fxt_coco_file),
+ "--format",
+ "COCO 1.0",
+ )
def test_can_create_from_backup(self, fxt_new_task: Task, fxt_backup_file: Path):
- stdout = self.run_cli("import", str(fxt_backup_file))
+ stdout = self.run_cli("task", "create-from-backup", str(fxt_backup_file))
- task_id = int(stdout.split()[-1])
+ task_id = int(stdout.rstrip("\n"))
assert task_id
assert task_id != fxt_new_task.id
assert self.client.tasks.retrieve(task_id).size == fxt_new_task.size
- def test_can_warn_on_mismatching_server_version(self, monkeypatch, caplog):
- def mocked_version(_):
- return pv.Version("0")
-
- # We don't actually run a separate process in the tests here, so it works
- monkeypatch.setattr(Client, "get_server_version", mocked_version)
-
- self.run_cli("ls")
-
- assert "Server version '0' is not compatible with SDK version" in caplog.text
-
- @pytest.mark.parametrize("verify", [True, False])
- def test_can_control_ssl_verification_with_arg(self, verify: bool):
- with https_reverse_proxy() as proxy_url:
- if verify:
- insecure_args = []
- else:
- insecure_args = ["--insecure"]
-
- run_cli(
- self,
- f"--auth={self.user}:{self.password}",
- f"--server-host={proxy_url}",
- *insecure_args,
- "ls",
- expected_code=1 if verify else 0,
- )
- stdout = self.stdout.getvalue()
-
- if not verify:
- for line in stdout.splitlines():
- int(line)
-
- def test_can_control_organization_context(self):
- org = "cli-test-org"
- self.client.organizations.create(models.OrganizationWriteRequest(org))
-
- files = generate_images(self.tmp_path, 1)
-
- stdout = self.run_cli(
- "create",
- "personal_task",
- ResourceType.LOCAL.name,
- *map(os.fspath, files),
- "--labels=" + json.dumps([{"name": "person"}]),
- "--completion_verification_period=0.01",
- organization="",
- )
-
- personal_task_id = int(stdout.split()[-1])
-
- stdout = self.run_cli(
- "create",
- "org_task",
- ResourceType.LOCAL.name,
- *map(os.fspath, files),
- "--labels=" + json.dumps([{"name": "person"}]),
- "--completion_verification_period=0.01",
- organization=org,
- )
-
- org_task_id = int(stdout.split()[-1])
-
- personal_task_ids = list(map(int, self.run_cli("ls", organization="").split()))
- assert personal_task_id in personal_task_ids
- assert org_task_id not in personal_task_ids
-
- org_task_ids = list(map(int, self.run_cli("ls", organization=org).split()))
- assert personal_task_id not in org_task_ids
- assert org_task_id in org_task_ids
-
- all_task_ids = list(map(int, self.run_cli("ls").split()))
- assert personal_task_id in all_task_ids
- assert org_task_id in all_task_ids
-
def test_auto_annotate_with_module(self, fxt_new_task: Task):
annotations = fxt_new_task.get_annotations()
assert not annotations.shapes
self.run_cli(
+ "task",
"auto-annotate",
str(fxt_new_task.id),
f"--function-module={__package__}.example_function",
@@ -325,6 +215,7 @@ def test_auto_annotate_with_file(self, fxt_new_task: Task):
assert not annotations.shapes
self.run_cli(
+ "task",
"auto-annotate",
str(fxt_new_task.id),
f"--function-file={Path(__file__).with_name('example_function.py')}",
@@ -338,6 +229,7 @@ def test_auto_annotate_with_parameters(self, fxt_new_task: Task):
assert not annotations.shapes
self.run_cli(
+ "task",
"auto-annotate",
str(fxt_new_task.id),
f"--function-module={__package__}.example_parameterized_function",
@@ -355,6 +247,7 @@ def test_auto_annotate_with_threshold(self, fxt_new_task: Task):
assert not annotations.shapes
self.run_cli(
+ "task",
"auto-annotate",
str(fxt_new_task.id),
f"--function-module={__package__}.conf_threshold_function",
@@ -366,6 +259,7 @@ def test_auto_annotate_with_threshold(self, fxt_new_task: Task):
def test_auto_annotate_with_cmtp(self, fxt_new_task: Task):
self.run_cli(
+ "task",
"auto-annotate",
str(fxt_new_task.id),
f"--function-module={__package__}.cmtp_function",
@@ -376,6 +270,7 @@ def test_auto_annotate_with_cmtp(self, fxt_new_task: Task):
assert annotations.shapes[0].type.value == "mask"
self.run_cli(
+ "task",
"auto-annotate",
str(fxt_new_task.id),
f"--function-module={__package__}.cmtp_function",
@@ -385,3 +280,11 @@ def test_auto_annotate_with_cmtp(self, fxt_new_task: Task):
annotations = fxt_new_task.get_annotations()
assert annotations.shapes[0].type.value == "polygon"
+
+ def test_legacy_alias(self, caplog):
+ # All legacy aliases are implemented the same way;
+ # no need to test every single one.
+ self.run_cli("ls")
+
+ assert "deprecated" in caplog.text
+ assert "task ls" in caplog.text
diff --git a/tests/python/cli/util.py b/tests/python/cli/util.py
index d399e695dab8..0a0093475171 100644
--- a/tests/python/cli/util.py
+++ b/tests/python/cli/util.py
@@ -5,16 +5,19 @@
import contextlib
import http.server
+import io
import ssl
import threading
import unittest
from collections.abc import Generator
from pathlib import Path
-from typing import Any, Union
+from typing import Any, Optional, Union
+import pytest
import requests
+from cvat_sdk import make_client
-from shared.utils.config import BASE_URL
+from shared.utils.config import BASE_URL, USER_PASS
from shared.utils.helpers import generate_image_file
@@ -84,3 +87,48 @@ def _translate_response(self, response: requests.Response) -> None:
self.end_headers()
# Need to use raw here to prevent requests from handling Content-Encoding.
self.wfile.write(response.raw.read())
+
+
+class TestCliBase:
+ @pytest.fixture(autouse=True)
+ def setup(
+ self,
+ restore_db_per_function, # force fixture call order to allow DB setup
+ restore_redis_inmem_per_function,
+ restore_redis_ondisk_per_function,
+ fxt_stdout: io.StringIO,
+ tmp_path: Path,
+ admin_user: str,
+ ):
+ self.tmp_path = tmp_path
+ self.stdout = fxt_stdout
+ self.host, self.port = BASE_URL.rsplit(":", maxsplit=1)
+ self.user = admin_user
+ self.password = USER_PASS
+ self.client = make_client(
+ host=self.host, port=self.port, credentials=(self.user, self.password)
+ )
+ self.client.config.status_check_period = 0.01
+
+ yield
+
+ def run_cli(
+ self, cmd: str, *args: str, expected_code: int = 0, organization: Optional[str] = None
+ ) -> str:
+ common_args = [
+ f"--auth={self.user}:{self.password}",
+ f"--server-host={self.host}",
+ f"--server-port={self.port}",
+ ]
+
+ if organization is not None:
+ common_args.append(f"--organization={organization}")
+
+ run_cli(
+ self,
+ *common_args,
+ cmd,
+ *args,
+ expected_code=expected_code,
+ )
+ return self.stdout.getvalue()
diff --git a/tests/python/requirements.txt b/tests/python/requirements.txt
index 5dfad3d6f7fb..d43d9b61d5df 100644
--- a/tests/python/requirements.txt
+++ b/tests/python/requirements.txt
@@ -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)
\ No newline at end of file
diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py
index be49c9d43ca1..15496cc31f73 100644
--- a/tests/python/rest_api/test_tasks.py
+++ b/tests/python/rest_api/test_tasks.py
@@ -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
@@ -66,6 +66,7 @@
from .utils import (
DATUMARO_FORMAT_FOR_DIMENSION,
CollectionSimpleFilterTestBase,
+ calc_end_frame,
compare_annotations,
create_task,
export_dataset,
@@ -84,6 +85,15 @@ def get_cloud_storage_content(username: str, cloud_storage_id: int, manifest: Op
return [f"{f['name']}{'/' if str(f['type']) == 'DIR' else ''}" for f in data["content"]]
+def count_frame_uses(data: Sequence[int], *, included_frames: Sequence[int]) -> dict[int, int]:
+ use_counts = {f: 0 for f in included_frames}
+ for f in data:
+ if f in included_frames:
+ use_counts[f] += 1
+
+ return use_counts
+
+
@pytest.mark.usefixtures("restore_db_per_class")
class TestGetTasks:
def _test_task_list_200(self, user, project_id, data, exclude_paths="", **kwargs):
@@ -2265,6 +2275,15 @@ def test_can_create_task_with_honeypots(
validation_frames
)
+ if frame_selection_method == "random_uniform":
+ # Test distribution
+ validation_frame_counts = {
+ f: annotation_job_frame_counts.get(f, 0) + 1 for f in validation_frames
+ }
+ assert max(validation_frame_counts.values()) <= 1 + min(
+ validation_frame_counts.values()
+ )
+
# each job must have the specified number of validation frames
for job_meta in annotation_job_metas:
assert (
@@ -3102,7 +3121,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":
@@ -4442,6 +4461,15 @@ def test_can_change_honeypot_frames_in_task(
api_client.tasks_api.retrieve_validation_layout(task["id"])[1].data
)
+ api_client.tasks_api.partial_update_validation_layout(
+ task["id"],
+ patched_task_validation_layout_write_request=models.PatchedTaskValidationLayoutWriteRequest(
+ frame_selection_method="manual",
+ honeypot_real_frames=old_validation_layout["honeypot_count"]
+ * [gt_frame_set[0]],
+ ),
+ )
+
params = {"frame_selection_method": frame_selection_method}
if frame_selection_method == "manual":
@@ -4468,6 +4496,15 @@ def test_can_change_honeypot_frames_in_task(
if frame_selection_method == "manual":
assert new_honeypot_real_frames == requested_honeypot_real_frames
+ elif frame_selection_method == "random_uniform":
+ # Test distribution
+ validation_frame_counts = count_frame_uses(
+ new_honeypot_real_frames,
+ included_frames=new_validation_layout["validation_frames"],
+ )
+ assert max(validation_frame_counts.values()) <= 1 + min(
+ validation_frame_counts.values()
+ )
assert (
DeepDiff(
@@ -4495,10 +4532,13 @@ def test_can_change_honeypot_frames_in_task_can_only_select_from_active_validati
gt_frame_set = range(gt_job["start_frame"], gt_job["stop_frame"] + 1)
active_gt_set = gt_frame_set[:honeypots_per_job]
- api_client.jobs_api.partial_update_data_meta(
- gt_job["id"],
- patched_job_data_meta_write_request=models.PatchedJobDataMetaWriteRequest(
- deleted_frames=[f for f in gt_frame_set if f not in active_gt_set]
+ api_client.tasks_api.partial_update_validation_layout(
+ task["id"],
+ patched_task_validation_layout_write_request=models.PatchedTaskValidationLayoutWriteRequest(
+ disabled_frames=[f for f in gt_frame_set if f not in active_gt_set],
+ frame_selection_method="manual",
+ honeypot_real_frames=old_validation_layout["honeypot_count"]
+ * [active_gt_set[0]],
),
)
@@ -4541,7 +4581,7 @@ def test_can_change_honeypot_frames_in_task_can_only_select_from_active_validati
new_honeypot_real_frames = new_validation_layout["honeypot_real_frames"]
assert old_validation_layout["honeypot_count"] == len(new_honeypot_real_frames)
- assert all(f in active_gt_set for f in new_honeypot_real_frames)
+ assert all([f in active_gt_set for f in new_honeypot_real_frames])
if frame_selection_method == "manual":
assert new_honeypot_real_frames == requested_honeypot_real_frames
@@ -4560,6 +4600,97 @@ def test_can_change_honeypot_frames_in_task_can_only_select_from_active_validati
]
), new_honeypot_real_frames
+ # Test distribution
+ validation_frame_counts = count_frame_uses(
+ new_honeypot_real_frames, included_frames=active_gt_set
+ )
+ assert max(validation_frame_counts.values()) <= 1 + min(
+ validation_frame_counts.values()
+ )
+
+ @parametrize("task, gt_job, annotation_jobs", [fixture_ref(fxt_task_with_honeypots)])
+ @parametrize("frame_selection_method", ["manual", "random_uniform"])
+ def test_can_restore_and_change_honeypot_frames_in_task_in_the_same_request(
+ self, admin_user, task, gt_job, annotation_jobs, frame_selection_method: str
+ ):
+ assert gt_job["stop_frame"] - gt_job["start_frame"] + 1 >= 2
+
+ with make_api_client(admin_user) as api_client:
+ old_validation_layout = json.loads(
+ api_client.tasks_api.retrieve_validation_layout(task["id"])[1].data
+ )
+
+ honeypots_per_job = old_validation_layout["frames_per_job_count"]
+
+ gt_frame_set = range(gt_job["start_frame"], gt_job["stop_frame"] + 1)
+ active_gt_set = gt_frame_set[:honeypots_per_job]
+
+ api_client.tasks_api.partial_update_validation_layout(
+ task["id"],
+ patched_task_validation_layout_write_request=models.PatchedTaskValidationLayoutWriteRequest(
+ disabled_frames=[f for f in gt_frame_set if f not in active_gt_set],
+ frame_selection_method="manual",
+ honeypot_real_frames=old_validation_layout["honeypot_count"]
+ * [active_gt_set[0]],
+ ),
+ )
+
+ active_gt_set = gt_frame_set
+
+ params = {
+ "frame_selection_method": frame_selection_method,
+ "disabled_frames": [], # restore all validation frames
+ }
+
+ if frame_selection_method == "manual":
+ requested_honeypot_real_frames = [
+ active_gt_set[(old_real_frame + 1) % len(active_gt_set)]
+ for old_real_frame in old_validation_layout["honeypot_real_frames"]
+ ]
+
+ params["honeypot_real_frames"] = requested_honeypot_real_frames
+
+ new_validation_layout = json.loads(
+ api_client.tasks_api.partial_update_validation_layout(
+ task["id"],
+ patched_task_validation_layout_write_request=(
+ models.PatchedTaskValidationLayoutWriteRequest(**params)
+ ),
+ )[1].data
+ )
+
+ new_honeypot_real_frames = new_validation_layout["honeypot_real_frames"]
+
+ assert old_validation_layout["honeypot_count"] == len(new_honeypot_real_frames)
+ assert sorted(new_validation_layout["disabled_frames"]) == sorted(
+ params["disabled_frames"]
+ )
+
+ if frame_selection_method == "manual":
+ assert new_honeypot_real_frames == requested_honeypot_real_frames
+ else:
+ assert all(
+ [
+ honeypots_per_job
+ == len(
+ set(
+ new_honeypot_real_frames[
+ j * honeypots_per_job : (j + 1) * honeypots_per_job
+ ]
+ )
+ )
+ ]
+ for j in range(len(annotation_jobs))
+ ), new_honeypot_real_frames
+
+ # Test distribution
+ validation_frame_counts = count_frame_uses(
+ new_honeypot_real_frames, included_frames=active_gt_set
+ )
+ assert max(validation_frame_counts.values()) <= 1 + min(
+ validation_frame_counts.values()
+ )
+
@parametrize("task, gt_job, annotation_jobs", [fixture_ref(fxt_task_with_honeypots)])
@parametrize("frame_selection_method", ["manual", "random_uniform"])
def test_can_change_honeypot_frames_in_annotation_jobs(
@@ -6331,3 +6462,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"
diff --git a/tests/python/rest_api/utils.py b/tests/python/rest_api/utils.py
index aa747d169e9d..8d5032998358 100644
--- a/tests/python/rest_api/utils.py
+++ b/tests/python/rest_api/utils.py
@@ -44,7 +44,7 @@ def initialize_export(endpoint: Endpoint, *, expect_forbidden: bool = False, **k
def wait_and_download_v1(
endpoint: Endpoint,
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
download_result: bool = True,
**kwargs,
@@ -75,7 +75,7 @@ def wait_and_download_v1(
def export_v1(
endpoint: Endpoint,
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
expect_forbidden: bool = False,
wait_result: bool = True,
@@ -115,7 +115,7 @@ def wait_and_download_v2(
api_client: ApiClient,
rq_id: str,
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
download_result: bool = True,
) -> Optional[bytes]:
@@ -153,7 +153,7 @@ def wait_and_download_v2(
def export_v2(
endpoint: Endpoint,
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
expect_forbidden: bool = False,
wait_result: bool = True,
@@ -196,7 +196,7 @@ def export_dataset(
], # make this parameter required to be sure that all tests was updated and both API versions are used
*,
save_images: bool,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
format: str = "CVAT for images 1.1", # pylint: disable=redefined-builtin
**kwargs,
@@ -278,7 +278,7 @@ def export_backup(
int, tuple[int]
], # make this parameter required to be sure that all tests was updated and both API versions are used
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
**kwargs,
) -> Optional[bytes]:
@@ -326,7 +326,7 @@ def export_task_backup(
def import_resource(
endpoint: Endpoint,
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
expect_forbidden: bool = False,
wait_result: bool = True,
@@ -372,7 +372,7 @@ def import_resource(
def import_backup(
api: Union[ProjectsApi, TasksApi],
*,
- max_retries: int = 30,
+ max_retries: int = 50,
interval: float = 0.1,
**kwargs,
) -> None:
@@ -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
diff --git a/utils/dataset_manifest/requirements.txt b/utils/dataset_manifest/requirements.txt
index c103c3e79add..6d3ed66aecb1 100644
--- a/utils/dataset_manifest/requirements.txt
+++ b/utils/dataset_manifest/requirements.txt
@@ -13,7 +13,7 @@ numpy==1.22.4
# via opencv-python-headless
opencv-python-headless==4.10.0.84
# via -r utils/dataset_manifest/requirements.in
-pillow==10.4.0
+pillow==11.0.0
# via -r utils/dataset_manifest/requirements.in
-tqdm==4.66.5
+tqdm==4.67.1
# via -r utils/dataset_manifest/requirements.in
diff --git a/yarn.lock b/yarn.lock
index 1f42b7b8dccc..46516547350c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10,13 +10,6 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
-"@ant-design/colors@^6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298"
- integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==
- dependencies:
- "@ctrl/tinycolor" "^3.4.0"
-
"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-7.0.2.tgz#c5c753a467ce8d86ba7ca4736d2c01f599bb5492"
@@ -55,31 +48,19 @@
rc-util "^5.35.0"
stylis "^4.0.13"
-"@ant-design/icons-svg@^4.3.0", "@ant-design/icons-svg@^4.4.0":
+"@ant-design/icons-svg@^4.4.0":
version "4.4.2"
resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz#ed2be7fb4d82ac7e1d45a54a5b06d6cecf8be6f6"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
-"@ant-design/icons@^4.6.3":
- version "4.8.3"
- resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.8.3.tgz#41555408ed5e9b0c3d53f3f24fe6a73abfcf4000"
- integrity sha512-HGlIQZzrEbAhpJR6+IGdzfbPym94Owr6JZkJ2QCCnOkPVIWMO2xgIVcOKnl8YcpijIo39V7l2qQL5fmtw56cMw==
- dependencies:
- "@ant-design/colors" "^6.0.0"
- "@ant-design/icons-svg" "^4.3.0"
- "@babel/runtime" "^7.11.2"
- classnames "^2.2.6"
- lodash "^4.17.15"
- rc-util "^5.9.4"
-
-"@ant-design/icons@^5.3.7":
- version "5.3.7"
- resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.3.7.tgz#d9f3654bf7934ee5faba43f91b5a187f5309ec68"
- integrity sha512-bCPXTAg66f5bdccM4TT21SQBDO1Ek2gho9h3nO9DAKXJP4sq+5VBjrQMSxMVXSB3HyEz+cUbHQ5+6ogxCOpaew==
+"@ant-design/icons@^5.3.7", "@ant-design/icons@^5.5.2":
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.5.2.tgz#c4567943cc2b7c6dbe9cae68c06ffa35f755dc0d"
+ integrity sha512-xc53rjVBl9v2BqFxUjZGti/RfdDeA8/6KYglmInM2PNqSXc/WfuGDTifJI/ZsokJK0aeKvOIbXc9y2g8ILAhEA==
dependencies:
"@ant-design/colors" "^7.0.0"
"@ant-design/icons-svg" "^4.4.0"
- "@babel/runtime" "^7.11.2"
+ "@babel/runtime" "^7.24.8"
classnames "^2.2.6"
rc-util "^5.31.1"
@@ -1134,17 +1115,10 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
- version "7.24.5"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c"
- integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==
- dependencies:
- regenerator-runtime "^0.14.0"
-
-"@babel/runtime@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
- integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.5", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
+ version "7.26.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
+ integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
dependencies:
regenerator-runtime "^0.14.0"
@@ -1481,7 +1455,7 @@
resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-1.0.0.tgz#42f3c213f2fb929324d465684ab9f46a0febd4bb"
integrity sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==
-"@ctrl/tinycolor@^3.4.0", "@ctrl/tinycolor@^3.6.1":
+"@ctrl/tinycolor@^3.6.1":
version "3.6.1"
resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31"
integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
@@ -4236,9 +4210,9 @@ create-react-class@^15.5.3:
object-assign "^4.1.1"
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
@@ -4402,7 +4376,7 @@ custom-error-instance@2.1.1:
three "^0.156.1"
"cvat-canvas@link:./cvat-canvas":
- version "2.20.9"
+ version "2.20.10"
dependencies:
"@types/polylabel" "^1.0.5"
polylabel "^1.1.0"
@@ -4413,7 +4387,7 @@ custom-error-instance@2.1.1:
svg.select.js "3.0.1"
"cvat-core@link:./cvat-core":
- version "15.2.0"
+ version "15.3.1"
dependencies:
axios "^1.7.4"
axios-retry "^4.0.0"
@@ -8802,9 +8776,9 @@ nan@^2.17.0:
integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==
nanoid@^3.3.7:
- version "3.3.7"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
- integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+ version "3.3.8"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
+ integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
natural-compare@^1.4.0:
version "1.4.0"
@@ -10438,7 +10412,7 @@ rc-util@^4.15.3:
react-lifecycles-compat "^3.0.4"
shallowequal "^1.1.0"
-rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.24.5, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.28.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.39.3, rc-util@^5.9.4:
+rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.24.5, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.28.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.39.3:
version "5.39.3"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.39.3.tgz#79c7253cff7c71175b772e8242ca66459c1512eb"
integrity sha512-j9wOELkLQ8gC/NkUg3qg9mHZcJf+5mYYv40JrDHqnaf8VSycji4pCf7kJ5fdTXQPDIF0vr5zpb/T2HdrMs9rWA==