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 {