From 6833b377c69fc48b004d32e0882eeb0d708c9dc4 Mon Sep 17 00:00:00 2001
From: Mariia Acoca <39969264+mdacoca@users.noreply.github.com>
Date: Tue, 31 Oct 2023 14:02:29 +0100
Subject: [PATCH 01/10] Documentation retouch: cloud storages and mot data
format (#7071)
- Added videos to Cloud Storages
- Updated wording in Cloud Storages
- Updated data format for MOT Import
---
.../manual/advanced/formats/format-mot.md | 7 +-
.../manual/basics/attach-cloud-storage.md | 65 ++++++++++++++-----
2 files changed, 52 insertions(+), 20 deletions(-)
diff --git a/site/content/en/docs/manual/advanced/formats/format-mot.md b/site/content/en/docs/manual/advanced/formats/format-mot.md
index 18882dce69a3..cd9815d59a09 100644
--- a/site/content/en/docs/manual/advanced/formats/format-mot.md
+++ b/site/content/en/docs/manual/advanced/formats/format-mot.md
@@ -53,9 +53,10 @@ person
Uploaded file: a zip archive of the structure above or:
```bash
-taskname.zip/
-├── labels.txt # optional, mandatory for non-official labels
-└── gt.txt
+archive.zip/
+└── gt/
+ └── gt.txt
+ └── labels.txt # optional, mandatory for non-official labels
```
- supported annotations: Rectangle tracks
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 3c40e26f746c..83c027c23d8b 100644
--- a/site/content/en/docs/manual/basics/attach-cloud-storage.md
+++ b/site/content/en/docs/manual/basics/attach-cloud-storage.md
@@ -5,11 +5,10 @@ weight: 23
description: 'Instructions on how to attach cloud storage using UI'
---
-In CVAT you can use **AWS S3**, **Azure Blob Container**
-and **Google Cloud** storages to import and export
+In CVAT you can use **AWS S3**, **Azure Blob Storage**
+and **Google Cloud Storage** storages to import and export
image datasets for your tasks.
-
See:
- [AWS S3](#aws-s3)
@@ -19,21 +18,24 @@ See:
- [Authorized access](#authorized-access)
- [Anonymous access](#anonymous-access)
- [Attach AWS S3 storage](#attach-aws-s3-storage)
- - [AWS manifest file](#aws-manifest-file)
-- [Google Cloud](#google-cloud)
+ - [AWS S3 manifest file](#aws-s3-manifest-file)
+ - [Video tutorial: Add AWS S3 as Cloud Storage in CVAT](#video-tutorial-add-aws-s3-as-cloud-storage-in-cvat)
+- [Google Cloud Storage](#google-cloud-storage)
- [Create a bucket](#create-a-bucket-1)
- [Upload data](#upload-data-1)
- [Access permissions](#access-permissions-1)
- [Authorized access](#authorized-access-1)
- [Anonymous access](#anonymous-access-1)
- - [Attach Google Cloud storage](#attach-google-cloud-storage)
-- [Microsoft Azure](#microsoft-azure)
+ - [Attach Google Cloud Storage](#attach-google-cloud-storage)
+ - [Video tutorial: Add Google Cloud Storage as Cloud Storage in CVAT](#video-tutorial-add-google-cloud-storage-as-cloud-storage-in-cvat)
+- [Microsoft Azure Blob Storage](#microsoft-azure-blob-storage)
- [Create a bucket](#create-a-bucket-2)
- [Create a container](#create-a-container)
- [Upload data](#upload-data-2)
- [SAS token and connection string](#sas-token-and-connection-string)
- [Personal use](#personal-use)
- - [Attach Azure Blob Container](#attach-azure-blob-container)
+ - [Attach Azure Blob Storage](#attach-azure-blob-storage)
+ - [Video tutorial: Add Microsoft Azure Blob Storage as Cloud Storage in CVAT](#video-tutorial-add-microsoft-azure-blob-storage-as-cloud-storage-in-cvat)
- [Prepare the dataset](#prepare-the-dataset)
## AWS S3
@@ -56,6 +58,8 @@ A new bucket will appear on the list of buckets.
### Upload data
+> **Note**: manifest file is optional.
+
You need to upload data for annotation and the `manifest.jsonl` file.
1. Prepare data.
@@ -129,11 +133,13 @@ Fill in the following fields:
After filling in all the fields, click **Submit**.
-### AWS manifest file
+### AWS S3 manifest file
+
+> **Note**: manifest file is optional.
To prepare the manifest file, do the following:
-1. Go to [**AWS cli**](https://aws.amazon.com/cli/) and run
+1. Go to [**AWS CLI**](https://aws.amazon.com/cli/) and run
[script for prepare manifest file](https://github.com/cvat-ai/cvat/tree/develop/utils/dataset_manifest).
2. Perform the installation, following the [**aws-shell manual**](https://github.com/awslabs/aws-shell),
You can configure credentials by running `aws configure`.
@@ -170,7 +176,14 @@ aws s3 cp /manifest.jsonl
![](/images/aws-s3_tutorial_5.jpg)
-## Google Cloud
+### Video tutorial: Add AWS S3 as Cloud Storage in CVAT
+
+
+
+
+
+
+## Google Cloud Storage
### Create a bucket
@@ -194,6 +207,8 @@ You will be forwarded to the bucket.
### Upload data
+> **Note**: manifest file is optional.
+
You need to upload data for annotation and the `manifest.jsonl` file.
1. Prepare data.
@@ -218,7 +233,7 @@ For authorized access you need to create a service account and key file.
To create a service account:
-1. In Google Cloud platform, go to **IAM & Admin** > **Service Accounts** and click **+Create Service Account**.
+1. On the Google Cloud platform, go to **IAM & Admin** > **Service Accounts** and click **+Create Service Account**.
2. Enter your account name and click **Create And Continue**.
3. Select a role, for example **Basic** > **Viewer**, and click **Continue**.
4. (Optional) Give access rights to the service account.
@@ -249,9 +264,9 @@ To configure anonymous access:
![](/images/google_cloud_storage_tutorial4.jpg)
-Now you can attach new Azure Blob container into CVAT.
+Now you can attach the Google Cloud Storage bucket to CVAT.
-### Attach Google Cloud storage
+### Attach Google Cloud Storage
To attach storage, do the following:
@@ -264,7 +279,7 @@ Fill in the following fields:
-| CVAT | Google Cloud |
+| CVAT | Google Cloud Storage |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Display name** | Preferred display name for your storage. |
| **Description** | (Optional) Add description of storage. |
@@ -280,7 +295,15 @@ Fill in the following fields:
After filling in all the fields, click **Submit**.
-## Microsoft Azure
+### Video tutorial: Add Google Cloud Storage as Cloud Storage in CVAT
+
+
+
+
+
+
+
+## Microsoft Azure Blob Storage
### Create a bucket
@@ -387,7 +410,7 @@ To get the **Access Key**:
![](/images/azure_blob_container_tutorial8.jpg)
-### Attach Azure Blob Container
+### Attach Azure Blob Storage
To attach storage, do the following:
@@ -412,6 +435,14 @@ Fill in the following fields:
After filling in all the fields, click **Submit**.
+### Video tutorial: Add Microsoft Azure Blob Storage as Cloud Storage in CVAT
+
+
+
+
+
+
+
## Prepare the dataset
For example, the dataset is [The Oxford-IIIT Pet Dataset](https://www.robots.ox.ac.uk/~vgg/data/pets/):
From e1fe140d05244b80fb21f0839b44fa275ca2b513 Mon Sep 17 00:00:00 2001
From: Nikita Manovich
Date: Wed, 1 Nov 2023 13:45:53 +0200
Subject: [PATCH 02/10] [Snyk] Security upgrade urllib3 from 1.26.17 to 1.26.18
(#7027)
Co-authored-by: snyk-bot
---
cvat/requirements/base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt
index b3685af4a2aa..f7b69b9f0e56 100644
--- a/cvat/requirements/base.txt
+++ b/cvat/requirements/base.txt
@@ -382,7 +382,7 @@ uritemplate==4.1.1
# via
# coreapi
# drf-spectacular
-urllib3==1.26.17
+urllib3==1.26.18
# via
# botocore
# clickhouse-connect
From 623476e2596044fcdad7bd52507387e9254e8f69 Mon Sep 17 00:00:00 2001
From: Maria Khrustaleva
Date: Thu, 2 Nov 2023 15:27:23 +0100
Subject: [PATCH 03/10] [AWS S3] Use local session (#7067)
Try to fix the issue described in cart 322
```
File \"/opt/venv/lib/python3.10/site-packages/botocore/session.py\", line 941, in get_component\n del self._deferred[name]\nKeyError: 'endpoint_resolver'\n","status_code":500
```
Generally, this approach increases the time of creating clients but it
does not affect us much because we create a client once or twice times
for some operations (cloud storage creating, task creating with cloud
storage data, retrieving a chunk, etc)
![image](https://github.com/opencv/cvat/assets/49038720/5adb4434-4d05-4882-b70c-ea760852b367)
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html#multithreading-or-multiprocessing-with-resources
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/session.html#multithreading-or-multiprocessing-with-sessions
Co-authored-by: Andrey Zhavoronkov
---
...llel_downloading_of_cloud_storage_files.md | 4 ++
cvat/apps/engine/cloud_provider.py | 49 +++++++++++--------
2 files changed, 33 insertions(+), 20 deletions(-)
create mode 100644 changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md
diff --git a/changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md b/changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md
new file mode 100644
index 000000000000..97b8dcb81a90
--- /dev/null
+++ b/changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md
@@ -0,0 +1,4 @@
+### Changed
+
+- Create a local session for AWS S3 client instead of using the default global one
+ ()
diff --git a/cvat/apps/engine/cloud_provider.py b/cvat/apps/engine/cloud_provider.py
index 52e29ccd703e..0992139d9bdb 100644
--- a/cvat/apps/engine/cloud_provider.py
+++ b/cvat/apps/engine/cloud_provider.py
@@ -381,29 +381,38 @@ def __init__(self,
prefix: Optional[str] = None,
):
super().__init__(prefix=prefix)
- if all([access_key_id, secret_key, session_token]):
- self._s3 = boto3.resource(
- 's3',
- aws_access_key_id=access_key_id,
- aws_secret_access_key=secret_key,
- aws_session_token=session_token,
- region_name=region,
- endpoint_url=endpoint_url
+ if (
+ sum(
+ 1
+ for credential in (access_key_id, secret_key, session_token)
+ if credential
)
- elif access_key_id and secret_key:
- self._s3 = boto3.resource(
- 's3',
- aws_access_key_id=access_key_id,
- aws_secret_access_key=secret_key,
- region_name=region,
- endpoint_url=endpoint_url
- )
- elif any([access_key_id, secret_key, session_token]):
- raise Exception('Insufficient data for authorization')
+ == 1
+ ):
+ raise Exception("Insufficient data for authorization")
+
+ kwargs = dict()
+ for key, arg_v in zip(
+ (
+ "aws_access_key_id",
+ "aws_secret_access_key",
+ "aws_session_token",
+ "region_name",
+ ),
+ (access_key_id, secret_key, session_token, region),
+ ):
+ if arg_v:
+ kwargs[key] = arg_v
+
+ session = boto3.Session(**kwargs)
+ self._s3 = session.resource("s3", endpoint_url=endpoint_url)
+
# anonymous access
if not any([access_key_id, secret_key, session_token]):
- self._s3 = boto3.resource('s3', region_name=region, endpoint_url=endpoint_url)
- self._s3.meta.client.meta.events.register('choose-signer.s3.*', disable_signing)
+ self._s3.meta.client.meta.events.register(
+ "choose-signer.s3.*", disable_signing
+ )
+
self._client = self._s3.meta.client
self._bucket = self._s3.Bucket(bucket)
self.region = region
From d2b5f3de4361bd47af13199be6f8ded33c31e190 Mon Sep 17 00:00:00 2001
From: Kirill Lakhov
Date: Thu, 2 Nov 2023 18:37:11 +0300
Subject: [PATCH 04/10] Compress `changeFrame` events (#7048)
We have too many event records of `changeFrame` and `zoomImage` events.
They are not really informative. This pr compresses `changeFrame` events
and increases ignore events timeouts
Co-authored-by: Andrey Zhavoronkov
---
...5752_klakhov_ignore_change_frame_events.md | 4 ++
cvat-core/src/log.ts | 1 +
cvat-core/src/logger-storage.ts | 58 +++++++++++++++----
cvat-ui/src/actions/annotation-actions.ts | 9 ++-
cvat-ui/src/cvat-core-wrapper.ts | 2 +
cvat-ui/src/reducers/annotation-reducer.ts | 4 ++
cvat-ui/src/reducers/index.ts | 3 +-
cvat/apps/events/serializers.py | 50 ++++++++++------
8 files changed, 98 insertions(+), 33 deletions(-)
create mode 100644 changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md
diff --git a/changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md b/changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md
new file mode 100644
index 000000000000..1a4b28351783
--- /dev/null
+++ b/changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md
@@ -0,0 +1,4 @@
+### Changed
+
+- Compressed sequental `change:frame` events into one
+ ()
diff --git a/cvat-core/src/log.ts b/cvat-core/src/log.ts
index cdc5ee1e3b9d..a2476e810865 100644
--- a/cvat-core/src/log.ts
+++ b/cvat-core/src/log.ts
@@ -168,6 +168,7 @@ export default function logFactory(logType: LogType, payload: any): EventLogger
LogType.copyObject,
LogType.undoAction,
LogType.redoAction,
+ LogType.changeFrame,
];
if (logsWithCount.includes(logType)) {
diff --git a/cvat-core/src/logger-storage.ts b/cvat-core/src/logger-storage.ts
index 507aaeff1b5f..33eb7a0d7bc9 100644
--- a/cvat-core/src/logger-storage.ts
+++ b/cvat-core/src/logger-storage.ts
@@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
-// Copyright (C) 2022 CVAT.ai Corporation
+// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@@ -15,40 +15,70 @@ function sleep(ms): Promise {
});
}
+function defaultUpdate(previousLog: EventLogger, currentPayload: any): object {
+ return {
+ ...previousLog.payload,
+ ...currentPayload,
+ };
+}
+
interface IgnoreRule {
lastLog: EventLogger | null;
timeThreshold?: number;
ignore: (previousLog: EventLogger, currentPayload: any) => boolean;
+ update: (previousLog: EventLogger, currentPayload: any) => object;
}
+type IgnoredRules = LogType.zoomImage | LogType.changeAttribute | LogType.changeFrame;
+
class LoggerStorage {
public clientID: string;
public collection: Array;
- public ignoreRules: Record;
+ public ignoreRules: Record;
public isActiveChecker: (() => boolean) | null;
public saving: boolean;
+ public compressedLogs: Array;
constructor() {
this.clientID = Date.now().toString().substr(-6);
this.collection = [];
this.isActiveChecker = null;
this.saving = false;
+ this.compressedLogs = [LogType.changeFrame];
this.ignoreRules = {
[LogType.zoomImage]: {
lastLog: null,
- timeThreshold: 1000,
- ignore(previousLog: EventLogger) {
+ timeThreshold: 4000,
+ ignore(previousLog: EventLogger): boolean {
return (Date.now() - previousLog.time.getTime()) < this.timeThreshold;
},
+ update: defaultUpdate,
},
[LogType.changeAttribute]: {
lastLog: null,
- ignore(previousLog: EventLogger, currentPayload: any) {
+ ignore(previousLog: EventLogger, currentPayload: any): boolean {
return (
currentPayload.object_id === previousLog.payload.object_id &&
currentPayload.id === previousLog.payload.id
);
},
+ update: defaultUpdate,
+ },
+ [LogType.changeFrame]: {
+ lastLog: null,
+ ignore(previousLog: EventLogger, currentPayload: any): boolean {
+ return (
+ currentPayload.job_id === previousLog.payload.job_id &&
+ currentPayload.step === previousLog.payload.step
+ );
+ },
+ update(previousLog: EventLogger, currentPayload: any): object {
+ return {
+ ...previousLog.payload,
+ to: currentPayload.to,
+ count: previousLog.payload.count + 1,
+ };
+ },
},
};
}
@@ -105,14 +135,17 @@ Object.defineProperties(LoggerStorage.prototype.log, {
throw new ArgumentError('Wait must be boolean');
}
+ if (!this.compressedLogs.includes(logType)) {
+ this.compressedLogs.forEach((compressedType: LogType) => {
+ this.ignoreRules[compressedType].lastLog = null;
+ });
+ }
+
if (logType in this.ignoreRules) {
const ignoreRule = this.ignoreRules[logType];
const { lastLog } = ignoreRule;
if (lastLog && ignoreRule.ignore(lastLog, payload)) {
- lastLog.payload = {
- ...lastLog.payload,
- ...payload,
- };
+ lastLog.payload = ignoreRule.update(lastLog, payload);
return ignoreRule.lastLog;
}
@@ -125,14 +158,15 @@ Object.defineProperties(LoggerStorage.prototype.log, {
}
const log = logFactory(logType, { ...logPayload });
- if (logType in this.ignoreRules) {
- this.ignoreRules[logType].lastLog = log;
- }
const pushEvent = (): void => {
log.validatePayload();
log.onClose(null);
this.collection.push(log);
+
+ if (logType in this.ignoreRules) {
+ this.ignoreRules[logType].lastLog = log;
+ }
};
if (log.scope === LogType.exception) {
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index 6953ef5ff5fe..133949791bf1 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -593,6 +593,7 @@ export function confirmCanvasReadyAsync(): ThunkAction {
try {
const state: CombinedState = getState();
const { instance: job } = state.annotation.job;
+ const { changeFrameLog } = state.annotation.player.frame;
const chunks = await job.frames.cachedChunks() as number[];
const { startFrame, stopFrame, dataChunkSize } = job;
@@ -612,6 +613,7 @@ export function confirmCanvasReadyAsync(): ThunkAction {
}, []).map(([start, end]) => `${start}:${end}`).join(';');
dispatch(confirmCanvasReady(ranges));
+ await changeFrameLog?.close();
} catch (error) {
// even if error happens here, do not need to notify the users
dispatch(confirmCanvasReady());
@@ -662,10 +664,12 @@ export function changeFrameAsync(
// commit the latest job frame to local storage
localStorage.setItem(`Job_${job.id}_frame`, `${toFrame}`);
- await job.logger.log(LogType.changeFrame, {
+ const changeFrameLog = await job.logger.log(LogType.changeFrame, {
from: frame,
to: toFrame,
- });
+ step: toFrame - frame,
+ count: 1,
+ }, true);
const [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime();
@@ -701,6 +705,7 @@ export function changeFrameAsync(
curZ: maxZ,
changeTime: currentTime + delay,
delay,
+ changeFrameLog,
},
});
} catch (error) {
diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts
index 2efdc36f51c7..00df7b61c1c1 100644
--- a/cvat-ui/src/cvat-core-wrapper.ts
+++ b/cvat-ui/src/cvat-core-wrapper.ts
@@ -32,6 +32,7 @@ import Organization, { Membership, Invitation } from 'cvat-core/src/organization
import AnnotationGuide from 'cvat-core/src/guide';
import AnalyticsReport, { AnalyticsEntryViewType, AnalyticsEntry } from 'cvat-core/src/analytics-report';
import { Dumper } from 'cvat-core/src/annotation-formats';
+import { EventLogger } from 'cvat-core/src/log';
import { APIWrapperEnterOptions } from 'cvat-core/src/plugins';
const cvat: any = _cvat;
@@ -87,6 +88,7 @@ export {
AnalyticsEntry,
AnalyticsEntryViewType,
ServerError,
+ EventLogger,
};
export type {
diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts
index ebe4c833d8da..b9784f45295d 100644
--- a/cvat-ui/src/reducers/annotation-reducer.ts
+++ b/cvat-ui/src/reducers/annotation-reducer.ts
@@ -72,6 +72,7 @@ const defaultState: AnnotationState = {
fetching: false,
delay: 0,
changeTime: null,
+ changeFrameLog: null,
},
ranges: '',
playing: false,
@@ -287,6 +288,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
curZ,
delay,
changeTime,
+ changeFrameLog,
} = action.payload;
return {
...state,
@@ -300,6 +302,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
fetching: false,
changeTime,
delay,
+ changeFrameLog,
},
},
annotations: {
@@ -323,6 +326,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
frame: {
...state.player.frame,
fetching: false,
+ changeFrameLog: null,
},
},
};
diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts
index 7597b357fc68..db531abd3d44 100644
--- a/cvat-ui/src/reducers/index.ts
+++ b/cvat-ui/src/reducers/index.ts
@@ -7,7 +7,7 @@ import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d';
import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper';
import {
Webhook, MLModel, ModelProvider, Organization,
- QualityReport, QualityConflict, QualitySettings, FramesMetaData, RQStatus,
+ QualityReport, QualityConflict, QualitySettings, FramesMetaData, RQStatus, EventLogger,
} from 'cvat-core-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import { KeyMap } from 'utils/mousetrap-react';
@@ -693,6 +693,7 @@ export interface AnnotationState {
fetching: boolean;
delay: number;
changeTime: number | null;
+ changeFrameLog: EventLogger | null;
};
ranges: string;
navigationBlocked: boolean;
diff --git a/cvat/apps/events/serializers.py b/cvat/apps/events/serializers.py
index 283d6ef7478b..635e5e601fd1 100644
--- a/cvat/apps/events/serializers.py
+++ b/cvat/apps/events/serializers.py
@@ -32,6 +32,7 @@ class ClientEventsSerializer(serializers.Serializer):
timestamp = serializers.DateTimeField()
_TIME_THRESHOLD = datetime.timedelta(seconds=100)
_WORKING_TIME_RESOLUTION = datetime.timedelta(milliseconds=1)
+ _COLLAPSED_EVENT_SCOPES = frozenset(("change:frame",))
def to_internal_value(self, data):
request = self.context.get("request")
@@ -42,28 +43,41 @@ def to_internal_value(self, data):
send_time = datetime_parser.isoparse(data["timestamp"])
receive_time = datetime.datetime.now(datetime.timezone.utc)
time_correction = receive_time - send_time
- last_timestamp = None
+ last_timestamp = datetime.datetime(datetime.MINYEAR, 1, 1, tzinfo=datetime.timezone.utc)
+ zero_t_delta = datetime.timedelta()
for event in data["events"]:
- timestamp = datetime_parser.isoparse(event['timestamp'])
- if last_timestamp:
- t_diff = timestamp - last_timestamp
+ timestamp = datetime_parser.isoparse(event["timestamp"])
+ working_time = datetime.timedelta()
+ event_duration = datetime.timedelta()
+ t_diff = timestamp - last_timestamp
+
+ payload = json.loads(event.get("payload", "{}"))
+
+ if t_diff >= zero_t_delta:
+ if event["scope"] in self._COLLAPSED_EVENT_SCOPES:
+ event_duration += datetime.timedelta(milliseconds=event["duration"])
+ working_time += event_duration
+
if t_diff < self._TIME_THRESHOLD:
- payload = event.get('payload', {})
- if payload:
- payload = json.loads(payload)
+ working_time += t_diff
+
+ payload.update({
+ "working_time": working_time // self._WORKING_TIME_RESOLUTION,
+ "username": request.user.username,
+ })
- payload['working_time'] = t_diff // self._WORKING_TIME_RESOLUTION
- payload['username'] = request.user.username
- event['payload'] = json.dumps(payload)
+ event.update({
+ "timestamp": str((timestamp + time_correction).timestamp()),
+ "source": "client",
+ "org_id": org_id,
+ "org_slug": org_slug,
+ "user_id": request.user.id,
+ "user_name": request.user.username,
+ "user_email": request.user.email,
+ "payload": json.dumps(payload),
+ })
- last_timestamp = timestamp
- event['timestamp'] = str((timestamp + time_correction).timestamp())
- event['source'] = 'client'
- event['org_id'] = org_id
- event['org_slug'] = org_slug
- event['user_id'] = request.user.id
- event['user_name'] = request.user.username
- event['user_email'] = request.user.email
+ last_timestamp = timestamp + event_duration
return data
From 1f8d5d3f5359e4d02d1dba32bb497481f90cbbab Mon Sep 17 00:00:00 2001
From: Maria Khrustaleva
Date: Thu, 2 Nov 2023 16:51:22 +0100
Subject: [PATCH 05/10] REST API tests for default bucket prefix (#7079)
This PR contains REST API tests for https://github.com/opencv/cvat/pull/6943
---
cvat/apps/engine/cloud_provider.py | 2 +-
tests/python/rest_api/test_cloud_storages.py | 150 ++++++++++++++++++-
tests/python/rest_api/test_tasks.py | 49 ++++++
utils/dataset_manifest/core.py | 2 +-
4 files changed, 200 insertions(+), 3 deletions(-)
diff --git a/cvat/apps/engine/cloud_provider.py b/cvat/apps/engine/cloud_provider.py
index 0992139d9bdb..8a7e3bd3a8ab 100644
--- a/cvat/apps/engine/cloud_provider.py
+++ b/cvat/apps/engine/cloud_provider.py
@@ -256,7 +256,7 @@ def list_files_on_one_page(
search_prefix = prefix
if self.prefix and (len(prefix) < len(self.prefix)):
- if '/' in self.prefix[len(prefix):]:
+ if prefix and '/' in self.prefix[len(prefix):]:
next_layer_and_tail = self.prefix[prefix.find('/') + 1:].split(
"/", maxsplit=1
)
diff --git a/tests/python/rest_api/test_cloud_storages.py b/tests/python/rest_api/test_cloud_storages.py
index 1c99143585b8..8c61b0b03690 100644
--- a/tests/python/rest_api/test_cloud_storages.py
+++ b/tests/python/rest_api/test_cloud_storages.py
@@ -445,6 +445,7 @@ def test_org_user_get_cloud_storage_preview(
self._test_cannot_see(username, storage_id)
+@pytest.mark.usefixtures("restore_db_per_function")
class TestGetCloudStorageContent:
USER = "admin1"
@@ -477,13 +478,14 @@ def _test_get_cloud_storage_content(
@pytest.mark.parametrize("cloud_storage_id", [2])
@pytest.mark.parametrize(
- "version, manifest, prefix, page_size, expected_content",
+ "version, manifest, prefix, default_bucket_prefix, page_size, expected_content",
[
(
SUPPORTED_VERSIONS.V1, # [v1] list all bucket content
"sub/manifest.jsonl",
None,
None,
+ None,
["sub/image_case_65_1.png", "sub/image_case_65_2.png"],
),
(
@@ -491,6 +493,7 @@ def _test_get_cloud_storage_content(
"sub/manifest.jsonl",
None,
None,
+ None,
[FileInfo(mime_type="DIR", name="sub", type="DIR")],
),
(
@@ -498,6 +501,7 @@ def _test_get_cloud_storage_content(
"sub/manifest.jsonl",
"sub/image_case_65_1",
None,
+ None,
[
FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
],
@@ -507,6 +511,7 @@ def _test_get_cloud_storage_content(
"sub/manifest.jsonl",
"sub/",
None,
+ None,
[
FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
FileInfo(mime_type="image", name="image_case_65_2.png", type="REG"),
@@ -517,12 +522,14 @@ def _test_get_cloud_storage_content(
None,
None,
None,
+ None,
[FileInfo(mime_type="DIR", name="sub", type="DIR")],
),
(
SUPPORTED_VERSIONS.V2, # [v2] list the second layer (directory "sub") of real bucket content
None,
"sub/",
+ None,
2,
[
FileInfo(mime_type="unknown", name="demo_manifest.jsonl", type="REG"),
@@ -534,6 +541,83 @@ def _test_get_cloud_storage_content(
None,
"/sub/", # cover case: API is identical to share point API
None,
+ None,
+ [
+ FileInfo(mime_type="unknown", name="demo_manifest.jsonl", type="REG"),
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ FileInfo(mime_type="image", name="image_case_65_2.png", type="REG"),
+ FileInfo(mime_type="unknown", name="manifest.jsonl", type="REG"),
+ FileInfo(mime_type="unknown", name="manifest_1.jsonl", type="REG"),
+ FileInfo(mime_type="unknown", name="manifest_2.jsonl", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list bucket content based on manifest when default bucket prefix is set to directory
+ "sub/manifest.jsonl",
+ None,
+ "sub/",
+ None,
+ [
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ FileInfo(mime_type="image", name="image_case_65_2.png", type="REG"),
+ ],
+ ),
+ (
+ # [v2] list bucket content based on manifest when default bucket prefix
+ # is set to template from which the files should start
+ SUPPORTED_VERSIONS.V2,
+ "sub/manifest.jsonl",
+ None,
+ "sub/image_case_65_1",
+ None,
+ [
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list bucket content based on manifest when specified prefix is stricter than default bucket prefix
+ "sub/manifest.jsonl",
+ "sub/image_case_65_1",
+ "sub/image_case",
+ None,
+ [
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list bucket content based on manifest when default bucket prefix is stricter than specified prefix
+ "sub/manifest.jsonl",
+ "sub/image_case",
+ "sub/image_case_65_1",
+ None,
+ [
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list bucket content based on manifest when default bucket prefix and specified prefix have no intersection
+ "sub/manifest.jsonl",
+ "sub/image_case_65_1",
+ "sub/image_case_65_2",
+ None,
+ [],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list bucket content based on manifest when default bucket prefix contains dirs and prefix starts with it
+ "sub/manifest.jsonl",
+ "s",
+ "sub/",
+ None,
+ [
+ FileInfo(mime_type="DIR", name="sub", type="DIR"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list real bucket content when default bucket prefix is set to directory
+ None,
+ None,
+ "sub/",
+ None,
[
FileInfo(mime_type="unknown", name="demo_manifest.jsonl", type="REG"),
FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
@@ -543,6 +627,56 @@ def _test_get_cloud_storage_content(
FileInfo(mime_type="unknown", name="manifest_2.jsonl", type="REG"),
],
),
+ (
+ # [v2] list real bucket content when default bucket prefix
+ # is set to template from which the files should start
+ SUPPORTED_VERSIONS.V2,
+ None,
+ None,
+ "sub/demo",
+ None,
+ [
+ FileInfo(mime_type="unknown", name="demo_manifest.jsonl", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list real bucket content when specified prefix is stricter than default bucket prefix
+ None,
+ "sub/image_case_65_1",
+ "sub/image_case",
+ None,
+ [
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list real bucket content when default bucket prefix is stricter than specified prefix
+ None,
+ "sub/image_case",
+ "sub/image_case_65_1",
+ None,
+ [
+ FileInfo(mime_type="image", name="image_case_65_1.png", type="REG"),
+ ],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list real bucket content when default bucket prefix and specified prefix have no intersection
+ None,
+ "sub/image_case_65_1",
+ "sub/image_case_65_2",
+ None,
+ [],
+ ),
+ (
+ SUPPORTED_VERSIONS.V2, # [v2] list real bucket content when default bucket prefix contains dirs and prefix starts with it
+ None,
+ "s",
+ "sub/",
+ None,
+ [
+ FileInfo(mime_type="DIR", name="sub", type="DIR"),
+ ],
+ ),
],
)
def test_get_cloud_storage_content(
@@ -551,9 +685,23 @@ def test_get_cloud_storage_content(
version: SUPPORTED_VERSIONS,
manifest: Optional[str],
prefix: Optional[str],
+ default_bucket_prefix: Optional[str],
page_size: Optional[int],
expected_content: Optional[Any],
+ cloud_storages,
):
+ if default_bucket_prefix:
+ cloud_storage = cloud_storages[cloud_storage_id]
+
+ with make_api_client(self.USER) as api_client:
+ (_, response) = api_client.cloudstorages_api.partial_update(
+ cloud_storage_id,
+ patched_cloud_storage_write_request={
+ "specific_attributes": f'{cloud_storage["specific_attributes"]}&prefix={default_bucket_prefix}'
+ },
+ )
+ assert response.status == HTTPStatus.OK
+
result = self._test_get_cloud_storage_content(
cloud_storage_id, version, manifest, prefix=prefix, page_size=page_size
)
diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py
index e93f76ff43ce..f433101f6082 100644
--- a/tests/python/rest_api/test_tasks.py
+++ b/tests/python/rest_api/test_tasks.py
@@ -1620,6 +1620,55 @@ def test_cannot_create_task_with_same_skeleton_sublabels(self):
response = get_method(self._USERNAME, "tasks")
assert response.status_code == HTTPStatus.OK
+ @pytest.mark.with_external_services
+ @pytest.mark.parametrize("cloud_storage_id", [2])
+ @pytest.mark.parametrize("use_manifest", [True, False])
+ @pytest.mark.parametrize("server_files", [["test/"]])
+ @pytest.mark.parametrize(
+ "default_prefix, expected_task_size",
+ [
+ (
+ "test/sub_1/img_0",
+ 1,
+ ),
+ (
+ "test/sub_1/",
+ 3,
+ ),
+ ],
+ )
+ @pytest.mark.parametrize("org", [""])
+ def test_create_task_with_cloud_storage_directories_and_default_bucket_prefix(
+ self,
+ cloud_storage_id: int,
+ use_manifest: bool,
+ server_files: List[str],
+ default_prefix: str,
+ expected_task_size: int,
+ org: str,
+ cloud_storages,
+ request,
+ ):
+ cloud_storage = cloud_storages[cloud_storage_id]
+
+ with make_api_client(self._USERNAME) as api_client:
+ (_, response) = api_client.cloudstorages_api.partial_update(
+ cloud_storage_id,
+ patched_cloud_storage_write_request={
+ "specific_attributes": f'{cloud_storage["specific_attributes"]}&prefix={default_prefix}'
+ },
+ )
+ assert response.status == HTTPStatus.OK
+
+ task_id, _ = self._create_task_with_cloud_data(
+ request, cloud_storage, use_manifest, server_files, org=org
+ )
+
+ with make_api_client(self._USERNAME) as api_client:
+ (task, response) = api_client.tasks_api.retrieve(task_id)
+ assert response.status == HTTPStatus.OK
+ assert task.size == expected_task_size
+
@pytest.mark.usefixtures("restore_db_per_function")
class TestPatchTaskLabel:
diff --git a/utils/dataset_manifest/core.py b/utils/dataset_manifest/core.py
index 2fd2fedd7fa0..dc050687b390 100644
--- a/utils/dataset_manifest/core.py
+++ b/utils/dataset_manifest/core.py
@@ -622,7 +622,7 @@ def emulate_hierarchical_structure(
search_prefix = prefix
if default_prefix and (len(prefix) < len(default_prefix)):
- if '/' in self.prefix[len(prefix):]:
+ if prefix and '/' in default_prefix[len(prefix):]:
next_layer_and_tail = default_prefix[prefix.find('/') + 1:].split(
"/", maxsplit=1
)
From 0535d452dd426ba62d6d312766906ddd1230c7eb Mon Sep 17 00:00:00 2001
From: Andrey Zhavoronkov
Date: Thu, 2 Nov 2023 17:59:39 +0200
Subject: [PATCH 06/10] Chunk preparation optimization (#7081)
This PR speeds up the preparation of chunks by:
1. loading images once instead of twice in each writer,
2. as well as by allowing simultaneous preparation of more than 1 chunk
using multithreading.
This allows to reduce the time for preparation of chunks for 4895 images
from 0:04:36 to 0:01:20 in case of preparation of 3 chunks in parallel
and 0:02:46 in case of 1 chunk in my environment.
Co-authored-by: Maria Khrustaleva
---
...2_andrey_optimization_creation_of_tasks.md | 4 +
cvat/apps/engine/cache.py | 9 ++-
cvat/apps/engine/media_extractors.py | 71 ++++++++--------
cvat/apps/engine/task.py | 80 ++++++++++++++-----
cvat/apps/engine/utils.py | 10 ++-
cvat/settings/base.py | 3 +
6 files changed, 119 insertions(+), 58 deletions(-)
create mode 100644 changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md
diff --git a/changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md b/changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md
new file mode 100644
index 000000000000..44fd5f67cef9
--- /dev/null
+++ b/changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md
@@ -0,0 +1,4 @@
+### Changed
+
+- Improved performance of chunk preparation when creating tasks
+ ()
diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py
index 6f88ed51290c..a1139b4bf16e 100644
--- a/cvat/apps/engine/cache.py
+++ b/cvat/apps/engine/cache.py
@@ -33,7 +33,7 @@
from cvat.apps.engine.mime_types import mimetypes
from cvat.apps.engine.models import (DataChoice, DimensionType, Job, Image,
StorageChoice, CloudStorage)
-from cvat.apps.engine.utils import md5_hash
+from cvat.apps.engine.utils import md5_hash, preload_images
from utils.dataset_manifest import ImageManifestManager
slogger = ServerLogManager(__name__)
@@ -117,7 +117,7 @@ def _get_frame_provider_class():
@staticmethod
@contextmanager
- def _get_images(db_data, chunk_number):
+ def _get_images(db_data, chunk_number, dimension):
images = []
tmp_dir = None
upload_dir = {
@@ -168,6 +168,7 @@ def _get_images(db_data, chunk_number):
images.append((fs_filename, fs_filename, None))
cloud_storage_instance.bulk_download_to_dir(files=files_to_download, upload_dir=tmp_dir)
+ images = preload_images(images)
for checksum, (_, fs_filename, _) in zip(checksums, images):
if checksum and not md5_hash(fs_filename) == checksum:
@@ -176,6 +177,8 @@ def _get_images(db_data, chunk_number):
for item in reader:
source_path = os.path.join(upload_dir, f"{item['name']}{item['extension']}")
images.append((source_path, source_path, None))
+ if dimension == DimensionType.DIM_2D:
+ images = preload_images(images)
yield images
finally:
@@ -199,7 +202,7 @@ def _prepare_task_chunk(self, db_data, quality, chunk_number):
writer = writer_classes[quality](image_quality, **kwargs)
buff = BytesIO()
- with self._get_images(db_data, chunk_number) as images:
+ with self._get_images(db_data, chunk_number, self._dimension) as images:
writer.save_as_chunk(images, buff)
buff.seek(0)
diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py
index 5f64a93f3024..3de0c0829d4d 100644
--- a/cvat/apps/engine/media_extractors.py
+++ b/cvat/apps/engine/media_extractors.py
@@ -12,6 +12,7 @@
from enum import IntEnum
from abc import ABC, abstractmethod
from contextlib import closing
+from typing import Iterable
import av
import numpy as np
@@ -587,12 +588,17 @@ def __init__(self, quality, dimension=DimensionType.DIM_2D):
self._dimension = dimension
@staticmethod
- def _compress_image(image_path, quality):
- if isinstance(image_path, av.VideoFrame):
- image = image_path.to_image()
- else:
- with Image.open(image_path) as source_image:
- image = ImageOps.exif_transpose(source_image)
+ def _compress_image(source_image: av.VideoFrame | io.IOBase | Image.Image, quality: int) -> tuple[int, int, io.BytesIO]:
+ image = None
+ if isinstance(source_image, av.VideoFrame):
+ image = source_image.to_image()
+ elif isinstance(source_image, io.IOBase):
+ with Image.open(source_image) as _img:
+ image = ImageOps.exif_transpose(_img)
+ elif isinstance(source_image, Image.Image):
+ image = source_image
+
+ assert image is not None
# Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion
if image.mode == "I":
@@ -619,7 +625,7 @@ def _compress_image(image_path, quality):
image = ImageOps.equalize(image) # The Images need equalization. High resolution with 16-bit but only small range that actually contains information
converted_image = image.convert('RGB')
- image.close()
+
try:
buf = io.BytesIO()
converted_image.save(buf, format='JPEG', quality=quality, optimize=True)
@@ -637,7 +643,7 @@ class ZipChunkWriter(IChunkWriter):
IMAGE_EXT = 'jpeg'
POINT_CLOUD_EXT = 'pcd'
- def _write_pcd_file(self, image):
+ def _write_pcd_file(self, image: str|io.BytesIO) -> tuple[io.BytesIO, str, int, int]:
image_buf = open(image, "rb") if isinstance(image, str) else image
try:
properties = ValidateDimension.get_pcd_properties(image_buf)
@@ -648,33 +654,32 @@ def _write_pcd_file(self, image):
if isinstance(image, str):
image_buf.close()
- def save_as_chunk(self, images, chunk_path):
+ def save_as_chunk(self, images: Iterable[tuple[Image.Image|io.IOBase|str, str, str]], chunk_path: str):
with zipfile.ZipFile(chunk_path, 'x') as zip_chunk:
for idx, (image, path, _) in enumerate(images):
ext = os.path.splitext(path)[1].replace('.', '')
output = io.BytesIO()
if self._dimension == DimensionType.DIM_2D:
- with Image.open(image) as pil_image:
- if has_exif_rotation(pil_image):
- rot_image = ImageOps.exif_transpose(pil_image)
- try:
- if rot_image.format == 'TIFF':
- # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
- # use loseless lzw compression for tiff images
- rot_image.save(output, format='TIFF', compression='tiff_lzw')
- else:
- rot_image.save(
- output,
- format=rot_image.format if rot_image.format else self.IMAGE_EXT,
- quality=100,
- subsampling=0
- )
- finally:
- rot_image.close()
- else:
- output = image
+ if has_exif_rotation(image):
+ rot_image = ImageOps.exif_transpose(image)
+ try:
+ if rot_image.format == 'TIFF':
+ # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
+ # use loseless lzw compression for tiff images
+ rot_image.save(output, format='TIFF', compression='tiff_lzw')
+ else:
+ rot_image.save(
+ output,
+ format=rot_image.format if rot_image.format else self.IMAGE_EXT,
+ quality=100,
+ subsampling=0
+ )
+ finally:
+ rot_image.close()
+ else:
+ output = path
else:
- output, ext = self._write_pcd_file(image)[0:2]
+ output, ext = self._write_pcd_file(path)[0:2]
arcname = '{:06d}.{}'.format(idx, ext)
if isinstance(output, io.BytesIO):
@@ -687,11 +692,13 @@ def save_as_chunk(self, images, chunk_path):
class ZipCompressedChunkWriter(ZipChunkWriter):
def save_as_chunk(
- self, images, chunk_path, *, compress_frames: bool = True, zip_compress_level: int = 0
+ self,
+ images: Iterable[tuple[Image.Image|io.IOBase|str, str, str]],
+ chunk_path: str, *, compress_frames: bool = True, zip_compress_level: int = 0
):
image_sizes = []
with zipfile.ZipFile(chunk_path, 'x', compresslevel=zip_compress_level) as zip_chunk:
- for idx, (image, _, _) in enumerate(images):
+ for idx, (image, path, _) in enumerate(images):
if self._dimension == DimensionType.DIM_2D:
if compress_frames:
w, h, image_buf = self._compress_image(image, self._image_quality)
@@ -702,7 +709,7 @@ def save_as_chunk(
w, h = img.size
extension = self.IMAGE_EXT
else:
- image_buf, extension, w, h = self._write_pcd_file(image)
+ image_buf, extension, w, h = self._write_pcd_file(path)
image_sizes.append((w, h))
arcname = '{:06d}.{}'.format(idx, extension)
zip_chunk.writestr(arcname, image_buf.getvalue())
diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py
index f9956e7a4e77..a1194c62c2ac 100644
--- a/cvat/apps/engine/task.py
+++ b/cvat/apps/engine/task.py
@@ -7,7 +7,7 @@
import fnmatch
import os
import sys
-from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Union
+from typing import Any, Dict, Iterator, List, NamedTuple, Optional, Union, Iterable
from rest_framework.serializers import ValidationError
import rq
import re
@@ -17,6 +17,8 @@
from urllib import request as urlrequest
import django_rq
import pytz
+import concurrent.futures
+import queue
from django.conf import settings
from django.db import transaction
@@ -27,7 +29,7 @@
from cvat.apps.engine.log import ServerLogManager
from cvat.apps.engine.media_extractors import (MEDIA_TYPES, ImageListReader, Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter,
ValidateDimension, ZipChunkWriter, ZipCompressedChunkWriter, get_mime, sort)
-from cvat.apps.engine.utils import av_scan_paths,get_rq_job_meta, define_dependent_job, get_rq_lock_by_user
+from cvat.apps.engine.utils import av_scan_paths,get_rq_job_meta, define_dependent_job, get_rq_lock_by_user, preload_images
from cvat.utils.http import make_requests_session, PROXIES_FOR_UNTRUSTED_URLS
from utils.dataset_manifest import ImageManifestManager, VideoManifestManager, is_manifest
from utils.dataset_manifest.core import VideoManifestValidator, is_dataset_manifest
@@ -1025,37 +1027,71 @@ def _update_status(msg):
frame=frame, width=w, height=h)
for (path, frame), (w, h) in zip(chunk_paths, img_sizes)
])
-
if db_data.storage_method == models.StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE:
counter = itertools.count()
- generator = itertools.groupby(extractor, lambda x: next(counter) // db_data.chunk_size)
- for chunk_idx, chunk_data in generator:
- chunk_data = list(chunk_data)
- original_chunk_path = db_data.get_original_chunk_path(chunk_idx)
- original_chunk_writer.save_as_chunk(chunk_data, original_chunk_path)
+ generator = itertools.groupby(extractor, lambda _: next(counter) // db_data.chunk_size)
+ generator = ((idx, list(chunk_data)) for idx, chunk_data in generator)
+
+ def save_chunks(
+ executor: concurrent.futures.ThreadPoolExecutor,
+ chunk_idx: int,
+ chunk_data: Iterable[tuple[str, str, str]]) -> list[tuple[str, int, tuple[int, int]]]:
+ nonlocal db_data, db_task, extractor, original_chunk_writer, compressed_chunk_writer
+ if (db_task.dimension == models.DimensionType.DIM_2D and
+ isinstance(extractor, (
+ MEDIA_TYPES['image']['extractor'],
+ MEDIA_TYPES['zip']['extractor'],
+ MEDIA_TYPES['pdf']['extractor'],
+ MEDIA_TYPES['archive']['extractor'],
+ ))):
+ chunk_data = preload_images(chunk_data)
+
+ fs_original = executor.submit(
+ original_chunk_writer.save_as_chunk,
+ images=chunk_data,
+ chunk_path=db_data.get_original_chunk_path(chunk_idx)
+ )
+ fs_compressed = executor.submit(
+ compressed_chunk_writer.save_as_chunk,
+ images=chunk_data,
+ chunk_path=db_data.get_compressed_chunk_path(chunk_idx),
+ )
+ fs_original.result()
+ image_sizes = fs_compressed.result()
+
+ # (path, frame, size)
+ return list((i[0][1], i[0][2], i[1]) for i in zip(chunk_data, image_sizes))
- compressed_chunk_path = db_data.get_compressed_chunk_path(chunk_idx)
- img_sizes = compressed_chunk_writer.save_as_chunk(chunk_data, compressed_chunk_path)
+ def process_results(img_meta: list[tuple[str, int, tuple[int, int]]]):
+ nonlocal db_images, db_data, video_path, video_size
if db_task.mode == 'annotation':
- db_images.extend([
+ db_images.extend(
models.Image(
data=db_data,
- path=os.path.relpath(data[1], upload_dir),
- frame=data[2],
- width=size[0],
- height=size[1])
-
- for data, size in zip(chunk_data, img_sizes)
- ])
+ path=os.path.relpath(frame_path, upload_dir),
+ frame=frame_number,
+ width=frame_size[0],
+ height=frame_size[1])
+ for frame_path, frame_number, frame_size in img_meta)
else:
- video_size = img_sizes[0]
- video_path = chunk_data[0][1]
+ video_size = img_meta[0][2]
+ video_path = img_meta[0][0]
- db_data.size += len(chunk_data)
- progress = extractor.get_progress(chunk_data[-1][2])
+ progress = extractor.get_progress(img_meta[-1][1])
update_progress(progress)
+ futures = queue.Queue(maxsize=settings.CVAT_CONCURRENT_CHUNK_PROCESSING)
+ with concurrent.futures.ThreadPoolExecutor(max_workers=2*settings.CVAT_CONCURRENT_CHUNK_PROCESSING) as executor:
+ for chunk_idx, chunk_data in generator:
+ db_data.size += len(chunk_data)
+ if futures.full():
+ process_results(futures.get().result())
+ futures.put(executor.submit(save_chunks, executor, chunk_idx, chunk_data))
+
+ while not futures.empty():
+ process_results(futures.get().result())
+
if db_task.mode == 'annotation':
models.Image.objects.bulk_create(db_images)
created_images = models.Image.objects.filter(data_id=db_data.id)
diff --git a/cvat/apps/engine/utils.py b/cvat/apps/engine/utils.py
index 0e17b24dd788..0a1b29801907 100644
--- a/cvat/apps/engine/utils.py
+++ b/cvat/apps/engine/utils.py
@@ -10,7 +10,7 @@
import sys
import traceback
from contextlib import suppress, nullcontext
-from typing import Any, Dict, Optional, Callable, Union
+from typing import Any, Dict, Optional, Callable, Union, Iterable
import subprocess
import os
import urllib.parse
@@ -375,3 +375,11 @@ def sendfile(
attachment_filename = make_attachment_file_name(attachment_filename)
return _sendfile(request, filename, attachment, attachment_filename, mimetype, encoding)
+
+def preload_image(image: tuple[str, str, str])-> tuple[Image.Image, str, str]:
+ pil_img = Image.open(image[0])
+ pil_img.load()
+ return pil_img, image[1], image[2]
+
+def preload_images(images: Iterable[tuple[str, str, str]]) -> list[tuple[Image.Image, str, str]]:
+ return list(map(preload_image, images))
diff --git a/cvat/settings/base.py b/cvat/settings/base.py
index 24eefc23fca9..89f5f1e2c1b8 100644
--- a/cvat/settings/base.py
+++ b/cvat/settings/base.py
@@ -720,3 +720,6 @@ class CVAT_QUEUES(Enum):
EMAIL_BACKEND = None
ONE_RUNNING_JOB_IN_QUEUE_PER_USER = strtobool(os.getenv('ONE_RUNNING_JOB_IN_QUEUE_PER_USER', 'false'))
+
+# How many chunks can be prepared simultaneously during task creation in case the cache is not used
+CVAT_CONCURRENT_CHUNK_PROCESSING = int(os.getenv('CVAT_CONCURRENT_CHUNK_PROCESSING', 1))
From 9819e6d69ec84b66dc4ba20064c969cbf2094628 Mon Sep 17 00:00:00 2001
From: Kirill Lakhov
Date: Fri, 3 Nov 2023 18:56:55 +0300
Subject: [PATCH 07/10] Fixed user email auto verification on accepting
organization invitation (#7073)
There is a couple of UX bugs in invite user to organization feature.
This pr fixes:
- Email is auto-verified after accepting invitation
- Stuff can view unaccepted invitations
- Stuff can edit unaccepted memberships
- User email is now used as username
---
.../src/components/organization-page/member-item.tsx | 2 +-
cvat/apps/iam/rules/memberships.rego | 12 ++++++++++--
.../tests/generators/memberships_test.gen.rego.py | 10 ++++++++++
cvat/apps/organizations/serializers.py | 11 +++++++++--
4 files changed, 30 insertions(+), 5 deletions(-)
diff --git a/cvat-ui/src/components/organization-page/member-item.tsx b/cvat-ui/src/components/organization-page/member-item.tsx
index a52d9661233f..506bc75ad0f8 100644
--- a/cvat-ui/src/components/organization-page/member-item.tsx
+++ b/cvat-ui/src/components/organization-page/member-item.tsx
@@ -41,7 +41,7 @@ function MemberItem(props: Props): JSX.Element {
const { username, firstName, lastName } = user;
const { username: selfUserName } = useSelector((state: CombinedState) => state.auth.user);
- const invitationActionsMenu = (
+ const invitationActionsMenu = invitation && (
{
if (action.key === MenuKeys.RESEND_INVITATION) {
diff --git a/cvat/apps/iam/rules/memberships.rego b/cvat/apps/iam/rules/memberships.rego
index ce0ba9225fb1..497b6fe58ebe 100644
--- a/cvat/apps/iam/rules/memberships.rego
+++ b/cvat/apps/iam/rules/memberships.rego
@@ -53,6 +53,10 @@ filter := [] { # Django Q object to filter list of entries
utils.is_admin
org_id := input.auth.organization.id
qobject := [ {"organization": org_id} ]
+} else := qobject {
+ organizations.is_staff
+ org_id := input.auth.organization.id
+ qobject := [ {"organization": org_id} ]
} else := qobject {
org_id := input.auth.organization.id
qobject := [ {"organization": org_id}, {"is_active": true}, "&" ]
@@ -65,6 +69,12 @@ allow {
input.resource.user.id == input.auth.user.id
}
+allow {
+ input.scope == utils.VIEW
+ organizations.is_staff
+ input.resource.organization.id == input.auth.organization.id
+}
+
allow {
input.scope == utils.VIEW
input.resource.is_active
@@ -76,7 +86,6 @@ allow {
# himself/another maintainer/owner
allow {
{ utils.CHANGE_ROLE, utils.DELETE }[input.scope]
- input.resource.is_active
input.resource.organization.id == input.auth.organization.id
utils.has_perm(utils.USER)
organizations.is_maintainer
@@ -91,7 +100,6 @@ allow {
# owner of the organization can change the role of any member and remove any member except himself
allow {
{ utils.CHANGE_ROLE, utils.DELETE }[input.scope]
- input.resource.is_active
input.resource.organization.id == input.auth.organization.id
utils.has_perm(utils.USER)
organizations.is_owner
diff --git a/cvat/apps/iam/rules/tests/generators/memberships_test.gen.rego.py b/cvat/apps/iam/rules/tests/generators/memberships_test.gen.rego.py
index 6510084dcd6c..7cf9cfca255e 100644
--- a/cvat/apps/iam/rules/tests/generators/memberships_test.gen.rego.py
+++ b/cvat/apps/iam/rules/tests/generators/memberships_test.gen.rego.py
@@ -98,6 +98,16 @@ def eval_rule(scope, context, ownership, privilege, membership, data):
return False
if scope != "create" and not data["resource"]["is_active"]:
+ is_staff = membership == "owner" or membership == 'maintainer'
+ if is_staff:
+ if scope != 'view':
+ if ORG_ROLES.index(membership) >= ORG_ROLES.index(resource["role"]):
+ return False
+ if GROUPS.index(privilege) > GROUPS.index("user"):
+ return False
+ if resource["user"]['id'] == data["auth"]["user"]['id']:
+ return False
+ return True
return False
return bool(rules)
diff --git a/cvat/apps/organizations/serializers.py b/cvat/apps/organizations/serializers.py
index 25b37e0a84cb..b88d6e2f3d12 100644
--- a/cvat/apps/organizations/serializers.py
+++ b/cvat/apps/organizations/serializers.py
@@ -4,10 +4,13 @@
# SPDX-License-Identifier: MIT
from django.contrib.auth import get_user_model
+from allauth.account.models import EmailAddress
+from allauth.account.adapter import get_adapter
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.contrib.auth.models import User
from django.utils.crypto import get_random_string
+from django.db import transaction
from rest_framework import serializers
from distutils.util import strtobool
@@ -78,6 +81,7 @@ class Meta:
fields = ['key', 'created_date', 'owner', 'role', 'organization', 'email']
read_only_fields = ['key', 'created_date', 'owner', 'organization']
+ @transaction.atomic
def create(self, validated_data):
membership_data = validated_data.pop('membership')
organization = validated_data.pop('organization')
@@ -87,11 +91,12 @@ def create(self, validated_data):
del membership_data['user']
except ObjectDoesNotExist:
user_email = membership_data['user']['email']
- username = user_email.split("@")[0]
- user = User.objects.create_user(username=username, password=get_random_string(length=32),
+ user = User.objects.create_user(username=user_email, password=get_random_string(length=32),
email=user_email, is_active=False)
user.set_unusable_password()
+ email = EmailAddress.objects.create(user=user, email=user_email, primary=True, verified=False)
user.save()
+ email.save()
del membership_data['user']
membership, created = Membership.objects.get_or_create(
defaults=membership_data,
@@ -151,6 +156,8 @@ def save(self, request, invitation):
self.cleaned_data = self.get_cleaned_data()
user = invitation.membership.user
user.is_active = True
+ email = EmailAddress.objects.get(email=user.email)
+ get_adapter(request).confirm_email(request, email)
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
user.username = self.cleaned_data['username']
From 20892eceec8ec0c581a9a243c0dccf8be0ff048e Mon Sep 17 00:00:00 2001
From: "cvat-bot[bot]" <147643061+cvat-bot[bot]@users.noreply.github.com>
Date: Fri, 3 Nov 2023 15:58:34 +0000
Subject: [PATCH 08/10] Prepare release v2.8.1
---
CHANGELOG.md | 53 +++++++++++++++++++
..._103807_maria_add_aws_s3_prefix_support.md | 6 ---
...18_224126_andrey_bulk_save_server_files.md | 5 --
...ia_limit_one_user_to_one_task_at_a_time.md | 16 ------
...54559_klakhov_tracker_mil_optimizations.md | 4 --
.../20231024_105610_boris_fix_detectron.md | 4 --
...190737_roman_docker_compose_external_db.md | 4 --
.../20231025_101044_boris_keypoints.md | 4 --
...ekachev.bs_additional_org_receive_check.md | 4 --
...5752_klakhov_ignore_change_frame_events.md | 4 --
...llel_downloading_of_cloud_storage_files.md | 4 --
...2_andrey_optimization_creation_of_tasks.md | 4 --
cvat-cli/requirements/base.txt | 2 +-
cvat-cli/src/cvat_cli/version.py | 2 +-
cvat-sdk/gen/generate.sh | 2 +-
cvat/__init__.py | 2 +-
cvat/schema.yml | 2 +-
docker-compose.yml | 18 +++----
helm-chart/values.yaml | 4 +-
19 files changed, 69 insertions(+), 75 deletions(-)
delete mode 100644 changelog.d/20231009_103807_maria_add_aws_s3_prefix_support.md
delete mode 100644 changelog.d/20231018_224126_andrey_bulk_save_server_files.md
delete mode 100644 changelog.d/20231023_132746_maria_limit_one_user_to_one_task_at_a_time.md
delete mode 100644 changelog.d/20231023_154559_klakhov_tracker_mil_optimizations.md
delete mode 100644 changelog.d/20231024_105610_boris_fix_detectron.md
delete mode 100644 changelog.d/20231024_190737_roman_docker_compose_external_db.md
delete mode 100644 changelog.d/20231025_101044_boris_keypoints.md
delete mode 100644 changelog.d/20231026_141231_sekachev.bs_additional_org_receive_check.md
delete mode 100644 changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md
delete mode 100644 changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md
delete mode 100644 changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 015e063a8bf0..882b1f2e2ddb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
+
+## \[2.8.1\] - 2023-11-03
+
+### Added
+
+- Support for default bucket prefix
+ ()
+- Search for cloud storage and share files
+ ()
+
+- Ability to limit one user to one task at a time
+ ()
+
+- Support for using an external database in a Docker Compose-based deployment
+ ()
+
+### Changed
+
+- Migrated to rq 1.15.1
+ ()
+
+- Compressed sequental `change:frame` events into one
+ ()
+
+- Create a local session for AWS S3 client instead of using the default global one
+ ()
+
+- Improved performance of chunk preparation when creating tasks
+ ()
+
+### Fixed
+
+- Race condition in a task data upload request, which may lead to problems with task creation in some specific cases,
+ such as multiple identical data requests at the same time
+ ()
+
+- Bug with viewing dependent RQ jobs for downloading resources from
+cloud storage when file path contains sub-directories.
+This is relevant for admins that can view detailed information about RQ queues.
+ ()
+
+- OpenCV.js memory leak with TrackerMIL
+ ()
+
+- Can't deploy detectron serverless function
+ ()
+
+- A mask becomes visible even if hidden after changing opacity level
+ ()
+
+- There is no switcher to personal workspace if an organization request failed
+ ()
+
## \[2.8.0\] - 2023-10-23
diff --git a/changelog.d/20231009_103807_maria_add_aws_s3_prefix_support.md b/changelog.d/20231009_103807_maria_add_aws_s3_prefix_support.md
deleted file mode 100644
index f12f565d5a6a..000000000000
--- a/changelog.d/20231009_103807_maria_add_aws_s3_prefix_support.md
+++ /dev/null
@@ -1,6 +0,0 @@
-### Added
-
-- Support for default bucket prefix
- ()
-- Search for cloud storage and share files
- ()
diff --git a/changelog.d/20231018_224126_andrey_bulk_save_server_files.md b/changelog.d/20231018_224126_andrey_bulk_save_server_files.md
deleted file mode 100644
index de4437155375..000000000000
--- a/changelog.d/20231018_224126_andrey_bulk_save_server_files.md
+++ /dev/null
@@ -1,5 +0,0 @@
-### Fixed
-
-- Race condition in a task data upload request, which may lead to problems with task creation in some specific cases,
- such as multiple identical data requests at the same time
- ()
diff --git a/changelog.d/20231023_132746_maria_limit_one_user_to_one_task_at_a_time.md b/changelog.d/20231023_132746_maria_limit_one_user_to_one_task_at_a_time.md
deleted file mode 100644
index 5c72f957438d..000000000000
--- a/changelog.d/20231023_132746_maria_limit_one_user_to_one_task_at_a_time.md
+++ /dev/null
@@ -1,16 +0,0 @@
-### Added
-
-- Ability to limit one user to one task at a time
- ()
-
-### Fixed
-
-- Bug with viewing dependent RQ jobs for downloading resources from
-cloud storage when file path contains sub-directories.
-This is relevant for admins that can view detailed information about RQ queues.
- ()
-
-### Changed
-
-- Migrated to rq 1.15.1
- ()
diff --git a/changelog.d/20231023_154559_klakhov_tracker_mil_optimizations.md b/changelog.d/20231023_154559_klakhov_tracker_mil_optimizations.md
deleted file mode 100644
index 3a8c3c759a0f..000000000000
--- a/changelog.d/20231023_154559_klakhov_tracker_mil_optimizations.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Fixed
-
-- OpenCV.js memory leak with TrackerMIL
- ()
diff --git a/changelog.d/20231024_105610_boris_fix_detectron.md b/changelog.d/20231024_105610_boris_fix_detectron.md
deleted file mode 100644
index 4b2900d7696d..000000000000
--- a/changelog.d/20231024_105610_boris_fix_detectron.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Fixed
-
-- Can't deploy detectron serverless function
- ()
diff --git a/changelog.d/20231024_190737_roman_docker_compose_external_db.md b/changelog.d/20231024_190737_roman_docker_compose_external_db.md
deleted file mode 100644
index eff196a3f9d6..000000000000
--- a/changelog.d/20231024_190737_roman_docker_compose_external_db.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Added
-
-- Support for using an external database in a Docker Compose-based deployment
- ()
diff --git a/changelog.d/20231025_101044_boris_keypoints.md b/changelog.d/20231025_101044_boris_keypoints.md
deleted file mode 100644
index 7f13392ac55f..000000000000
--- a/changelog.d/20231025_101044_boris_keypoints.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Fixed
-
-- A mask becomes visible even if hidden after changing opacity level
- ()
diff --git a/changelog.d/20231026_141231_sekachev.bs_additional_org_receive_check.md b/changelog.d/20231026_141231_sekachev.bs_additional_org_receive_check.md
deleted file mode 100644
index 0468945b2eaa..000000000000
--- a/changelog.d/20231026_141231_sekachev.bs_additional_org_receive_check.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Fixed
-
-- There is no switcher to personal workspace if an organization request failed
- ()
diff --git a/changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md b/changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md
deleted file mode 100644
index 1a4b28351783..000000000000
--- a/changelog.d/20231101_115752_klakhov_ignore_change_frame_events.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Changed
-
-- Compressed sequental `change:frame` events into one
- ()
diff --git a/changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md b/changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md
deleted file mode 100644
index 97b8dcb81a90..000000000000
--- a/changelog.d/20231101_122729_maria_fix_parallel_downloading_of_cloud_storage_files.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Changed
-
-- Create a local session for AWS S3 client instead of using the default global one
- ()
diff --git a/changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md b/changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md
deleted file mode 100644
index 44fd5f67cef9..000000000000
--- a/changelog.d/20231102_105602_andrey_optimization_creation_of_tasks.md
+++ /dev/null
@@ -1,4 +0,0 @@
-### Changed
-
-- Improved performance of chunk preparation when creating tasks
- ()
diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt
index 022f624fcdb7..9076821f4852 100644
--- a/cvat-cli/requirements/base.txt
+++ b/cvat-cli/requirements/base.txt
@@ -1,3 +1,3 @@
-cvat-sdk~=2.9.0
+cvat-sdk~=2.8.1
Pillow>=10.0.1
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py
index 481582eb75bb..c3bf080aaaf0 100644
--- a/cvat-cli/src/cvat_cli/version.py
+++ b/cvat-cli/src/cvat_cli/version.py
@@ -1 +1 @@
-VERSION = "2.9.0"
+VERSION = "2.8.1"
diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh
index e6985656e74c..6a7918ca573d 100755
--- a/cvat-sdk/gen/generate.sh
+++ b/cvat-sdk/gen/generate.sh
@@ -8,7 +8,7 @@ set -e
GENERATOR_VERSION="v6.0.1"
-VERSION="2.9.0"
+VERSION="2.8.1"
LIB_NAME="cvat_sdk"
LAYER1_LIB_NAME="${LIB_NAME}/api_client"
DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)"
diff --git a/cvat/__init__.py b/cvat/__init__.py
index 9ba067504a42..23afdda4220b 100644
--- a/cvat/__init__.py
+++ b/cvat/__init__.py
@@ -4,6 +4,6 @@
from cvat.utils.version import get_version
-VERSION = (2, 9, 0, 'alpha', 0)
+VERSION = (2, 8, 1, 'final', 0)
__version__ = get_version(VERSION)
diff --git a/cvat/schema.yml b/cvat/schema.yml
index 98d761e69ae0..bb21b929f29a 100644
--- a/cvat/schema.yml
+++ b/cvat/schema.yml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: CVAT REST API
- version: 2.9.0
+ version: 2.8.1
description: REST API for Computer Vision Annotation Tool (CVAT)
termsOfService: https://www.google.com/policies/terms/
contact:
diff --git a/docker-compose.yml b/docker-compose.yml
index 65909fd1682a..fac11f237c0c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -43,7 +43,7 @@ services:
cvat_server:
container_name: cvat_server
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -77,7 +77,7 @@ services:
cvat_utils:
container_name: cvat_utils
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -97,7 +97,7 @@ services:
cvat_worker_import:
container_name: cvat_worker_import
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -115,7 +115,7 @@ services:
cvat_worker_export:
container_name: cvat_worker_export
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -133,7 +133,7 @@ services:
cvat_worker_annotation:
container_name: cvat_worker_annotation
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -152,7 +152,7 @@ services:
cvat_worker_webhooks:
container_name: cvat_worker_webhooks
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -171,7 +171,7 @@ services:
cvat_worker_quality_reports:
container_name: cvat_worker_quality_reports
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -189,7 +189,7 @@ services:
cvat_worker_analytics_reports:
container_name: cvat_worker_analytics_reports
- image: cvat/server:${CVAT_VERSION:-dev}
+ image: cvat/server:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_redis
@@ -207,7 +207,7 @@ services:
cvat_ui:
container_name: cvat_ui
- image: cvat/ui:${CVAT_VERSION:-dev}
+ image: cvat/ui:${CVAT_VERSION:-v2.8.1}
restart: always
depends_on:
- cvat_server
diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml
index b9986e7bc24e..52eef9140794 100644
--- a/helm-chart/values.yaml
+++ b/helm-chart/values.yaml
@@ -104,7 +104,7 @@ cvat:
additionalVolumeMounts: []
replicas: 1
image: cvat/server
- tag: dev
+ tag: v2.8.1
imagePullPolicy: Always
permissionFix:
enabled: true
@@ -128,7 +128,7 @@ cvat:
frontend:
replicas: 1
image: cvat/ui
- tag: dev
+ tag: v2.8.1
imagePullPolicy: Always
labels: {}
# test: test
From 77262814f607e0b28b4d34726e39b5488bc23698 Mon Sep 17 00:00:00 2001
From: "cvat-bot[bot]" <147643061+cvat-bot[bot]@users.noreply.github.com>
Date: Sun, 5 Nov 2023 08:50:06 +0000
Subject: [PATCH 09/10] Update develop after v2.8.1
---
cvat-cli/requirements/base.txt | 2 +-
cvat-cli/src/cvat_cli/version.py | 2 +-
cvat-sdk/gen/generate.sh | 2 +-
cvat/__init__.py | 2 +-
cvat/schema.yml | 2 +-
docker-compose.yml | 18 +++++++++---------
helm-chart/values.yaml | 4 ++--
7 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt
index 9076821f4852..022f624fcdb7 100644
--- a/cvat-cli/requirements/base.txt
+++ b/cvat-cli/requirements/base.txt
@@ -1,3 +1,3 @@
-cvat-sdk~=2.8.1
+cvat-sdk~=2.9.0
Pillow>=10.0.1
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py
index c3bf080aaaf0..481582eb75bb 100644
--- a/cvat-cli/src/cvat_cli/version.py
+++ b/cvat-cli/src/cvat_cli/version.py
@@ -1 +1 @@
-VERSION = "2.8.1"
+VERSION = "2.9.0"
diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh
index 6a7918ca573d..e6985656e74c 100755
--- a/cvat-sdk/gen/generate.sh
+++ b/cvat-sdk/gen/generate.sh
@@ -8,7 +8,7 @@ set -e
GENERATOR_VERSION="v6.0.1"
-VERSION="2.8.1"
+VERSION="2.9.0"
LIB_NAME="cvat_sdk"
LAYER1_LIB_NAME="${LIB_NAME}/api_client"
DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)"
diff --git a/cvat/__init__.py b/cvat/__init__.py
index 23afdda4220b..9ba067504a42 100644
--- a/cvat/__init__.py
+++ b/cvat/__init__.py
@@ -4,6 +4,6 @@
from cvat.utils.version import get_version
-VERSION = (2, 8, 1, 'final', 0)
+VERSION = (2, 9, 0, 'alpha', 0)
__version__ = get_version(VERSION)
diff --git a/cvat/schema.yml b/cvat/schema.yml
index bb21b929f29a..98d761e69ae0 100644
--- a/cvat/schema.yml
+++ b/cvat/schema.yml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: CVAT REST API
- version: 2.8.1
+ version: 2.9.0
description: REST API for Computer Vision Annotation Tool (CVAT)
termsOfService: https://www.google.com/policies/terms/
contact:
diff --git a/docker-compose.yml b/docker-compose.yml
index fac11f237c0c..65909fd1682a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -43,7 +43,7 @@ services:
cvat_server:
container_name: cvat_server
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -77,7 +77,7 @@ services:
cvat_utils:
container_name: cvat_utils
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -97,7 +97,7 @@ services:
cvat_worker_import:
container_name: cvat_worker_import
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -115,7 +115,7 @@ services:
cvat_worker_export:
container_name: cvat_worker_export
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -133,7 +133,7 @@ services:
cvat_worker_annotation:
container_name: cvat_worker_annotation
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -152,7 +152,7 @@ services:
cvat_worker_webhooks:
container_name: cvat_worker_webhooks
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -171,7 +171,7 @@ services:
cvat_worker_quality_reports:
container_name: cvat_worker_quality_reports
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -189,7 +189,7 @@ services:
cvat_worker_analytics_reports:
container_name: cvat_worker_analytics_reports
- image: cvat/server:${CVAT_VERSION:-v2.8.1}
+ image: cvat/server:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_redis
@@ -207,7 +207,7 @@ services:
cvat_ui:
container_name: cvat_ui
- image: cvat/ui:${CVAT_VERSION:-v2.8.1}
+ image: cvat/ui:${CVAT_VERSION:-dev}
restart: always
depends_on:
- cvat_server
diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml
index 52eef9140794..b9986e7bc24e 100644
--- a/helm-chart/values.yaml
+++ b/helm-chart/values.yaml
@@ -104,7 +104,7 @@ cvat:
additionalVolumeMounts: []
replicas: 1
image: cvat/server
- tag: v2.8.1
+ tag: dev
imagePullPolicy: Always
permissionFix:
enabled: true
@@ -128,7 +128,7 @@ cvat:
frontend:
replicas: 1
image: cvat/ui
- tag: v2.8.1
+ tag: dev
imagePullPolicy: Always
labels: {}
# test: test
From 506c96be013a9ad702862ab83340d788c127160e Mon Sep 17 00:00:00 2001
From: Boris Sekachev
Date: Mon, 6 Nov 2023 15:08:45 +0200
Subject: [PATCH 10/10] Fixed opencv runtime initialization (#7101)
---
...31106_112434_boris_fixed_opencv_runtime_init.md | 4 ++++
.../controls-side-bar/opencv-control.tsx | 2 ++
cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts | 14 ++++++++++----
3 files changed, 16 insertions(+), 4 deletions(-)
create mode 100644 changelog.d/20231106_112434_boris_fixed_opencv_runtime_init.md
diff --git a/changelog.d/20231106_112434_boris_fixed_opencv_runtime_init.md b/changelog.d/20231106_112434_boris_fixed_opencv_runtime_init.md
new file mode 100644
index 000000000000..aa0b2a5bcc08
--- /dev/null
+++ b/changelog.d/20231106_112434_boris_fixed_opencv_runtime_init.md
@@ -0,0 +1,4 @@
+### Fixed
+
+- OpenCV runtime initialization
+ ()
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx
index fe6eebf79e28..83ba8e1f0c01 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx
@@ -811,6 +811,8 @@ class OpenCVControlComponent extends React.PureComponent {
+ runtimeInitialized = true;
+ delete (window as any).Module;
+ },
+ };
// Inject opencv to DOM
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const OpenCVConstructor = new Function(decodedScript);
OpenCVConstructor();
-
- const global = window as any;
-
- this.cv = global.cv;
+ this.cv = (window as any).cv;
+ await waitFor(2, () => runtimeInitialized);
}
public async initialize(onProgress: (percent: number) => void): Promise {