diff --git a/cvat/apps/auto_annotation/README.md b/cvat/apps/auto_annotation/README.md
index 3b618995..23e4aaa5 100644
--- a/cvat/apps/auto_annotation/README.md
+++ b/cvat/apps/auto_annotation/README.md
@@ -2,8 +2,9 @@
### Description
-The application will be enabled automatically if OpenVINO™ component is
-installed. It allows to use custom models for auto annotation. Only models in
+The application will be enabled automatically if
+[OpenVINO™ component](../../../components/openvino)
+is installed. It allows to use custom models for auto annotation. Only models in
OpenVINO™ toolkit format are supported. If you would like to annotate a
task with a custom model please convert it to the intermediate representation
(IR) format via the model optimizer tool. See [OpenVINO documentation](https://software.intel.com/en-us/articles/OpenVINO-InferEngine) for details.
@@ -14,9 +15,7 @@ To annotate a task with a custom model you need to prepare 4 files:
1. __Model config__ (*.xml) - a text file with network configuration.
1. __Model weights__ (*.bin) - a binary file with trained weights.
1. __Label map__ (*.json) - a simple json file with `label_map` dictionary like
-object with string values for label numbers. Values in `label_map` should be
-exactly equal to labels for the annotation task, otherwise objects with mismatched
-labels will be ignored.
+object with string values for label numbers.
Example:
```json
{
diff --git a/cvat/apps/auto_annotation/__init__.py b/cvat/apps/auto_annotation/__init__.py
index a78eca36..d6d5de7d 100644
--- a/cvat/apps/auto_annotation/__init__.py
+++ b/cvat/apps/auto_annotation/__init__.py
@@ -3,6 +3,10 @@
#
# SPDX-License-Identifier: MIT
-from cvat.settings.base import JS_3RDPARTY
+from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
-JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['auto_annotation/js/auto_annotation.js']
+default_app_config = 'cvat.apps.auto_annotation.apps.AutoAnnotationConfig'
+
+JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['auto_annotation/js/dashboardPlugin.js']
+
+CSS_3RDPARTY['dashboard'] = CSS_3RDPARTY.get('dashboard', []) + ['auto_annotation/stylesheet.css']
diff --git a/cvat/apps/auto_annotation/apps.py b/cvat/apps/auto_annotation/apps.py
index f421e132..cea75abf 100644
--- a/cvat/apps/auto_annotation/apps.py
+++ b/cvat/apps/auto_annotation/apps.py
@@ -7,5 +7,9 @@ from django.apps import AppConfig
class AutoAnnotationConfig(AppConfig):
- name = "auto_annotation"
+ name = "cvat.apps.auto_annotation"
+ def ready(self):
+ from .permissions import setup_permissions
+
+ setup_permissions()
diff --git a/cvat/apps/auto_annotation/migrations/0001_initial.py b/cvat/apps/auto_annotation/migrations/0001_initial.py
new file mode 100644
index 00000000..ebd8a6e1
--- /dev/null
+++ b/cvat/apps/auto_annotation/migrations/0001_initial.py
@@ -0,0 +1,39 @@
+# Generated by Django 2.1.3 on 2019-01-24 14:05
+
+import cvat.apps.auto_annotation.models
+from django.conf import settings
+import django.core.files.storage
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AnnotationModel',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', cvat.apps.auto_annotation.models.SafeCharField(max_length=256)),
+ ('created_date', models.DateTimeField(auto_now_add=True)),
+ ('updated_date', models.DateTimeField(auto_now_add=True)),
+ ('model_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(), upload_to=cvat.apps.auto_annotation.models.upload_path_handler)),
+ ('weights_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(), upload_to=cvat.apps.auto_annotation.models.upload_path_handler)),
+ ('labelmap_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(), upload_to=cvat.apps.auto_annotation.models.upload_path_handler)),
+ ('interpretation_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(), upload_to=cvat.apps.auto_annotation.models.upload_path_handler)),
+ ('shared', models.BooleanField(default=False)),
+ ('primary', models.BooleanField(default=False)),
+ ('framework', models.CharField(default=cvat.apps.auto_annotation.models.FrameworkChoice('openvino'), max_length=32)),
+ ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'default_permissions': (),
+ },
+ ),
+ ]
diff --git a/cvat/apps/auto_annotation/model_manager.py b/cvat/apps/auto_annotation/model_manager.py
new file mode 100644
index 00000000..a25c153f
--- /dev/null
+++ b/cvat/apps/auto_annotation/model_manager.py
@@ -0,0 +1,143 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import django_rq
+import os
+import rq
+import shutil
+
+from django.db import transaction
+from django.utils import timezone
+from django.conf import settings
+
+from .models import AnnotationModel, FrameworkChoice
+
+def _remove_old_file(model_file_field):
+ if model_file_field and os.path.exists(model_file_field.name):
+ os.remove(model_file_field.name)
+
+@transaction.atomic
+def _update_dl_model_thread(dl_model_id, model_file, weights_file, labelmap_file, interpretation_file, run_tests):
+ def _get_file_content(filename):
+ return os.path.basename(filename), open(filename, "rb")
+
+ job = rq.get_current_job()
+ job.meta["progress"] = "Saving data"
+ job.save_meta()
+
+ dl_model = AnnotationModel.objects.select_for_update().get(pk=dl_model_id)
+
+ #save files in case of files should be uploaded from share
+ if model_file:
+ _remove_old_file(dl_model.model_file)
+ dl_model.model_file.save(*_get_file_content(model_file))
+ if weights_file:
+ _remove_old_file(dl_model.weights_file)
+ dl_model.weights_file.save(*_get_file_content(weights_file))
+ if labelmap_file:
+ _remove_old_file(dl_model.labelmap_file)
+ dl_model.labelmap_file.save(*_get_file_content(labelmap_file))
+ if interpretation_file:
+ _remove_old_file(dl_model.interpretation_file)
+ dl_model.interpretation_file.save(*_get_file_content(interpretation_file))
+
+ if run_tests:
+ #only for testing
+ import time
+ time.sleep(3)
+ job.meta["progress"] = "Test started"
+ job.save_meta()
+ time.sleep(5)
+ job.meta["progress"] = "Test finished"
+
+@transaction.atomic
+def update_model(dl_model_id, name, model_file, weights_file, labelmap_file, interpretation_file, storage, is_shared):
+
+ def get_abs_path(share_path):
+ if not share_path:
+ return share_path
+ share_root = settings.SHARE_ROOT
+ relpath = os.path.normpath(share_path).lstrip('/')
+ if '..' in relpath.split(os.path.sep):
+ raise Exception('Permission denied')
+ abspath = os.path.abspath(os.path.join(share_root, relpath))
+ if os.path.commonprefix([share_root, abspath]) != share_root:
+ raise Exception('Bad file path on share: ' + abspath)
+ return abspath
+
+ dl_model = AnnotationModel.objects.select_for_update().get(pk=dl_model_id)
+
+ if name:
+ dl_model.name = name
+
+ if is_shared != None:
+ dl_model.shared = is_shared
+
+ run_tests = bool(model_file or weights_file or labelmap_file or interpretation_file)
+ if storage != "local":
+ model_file = get_abs_path(model_file)
+ weights_file = get_abs_path(weights_file)
+ labelmap_file = get_abs_path(labelmap_file)
+ interpretation_file = get_abs_path(interpretation_file)
+ else:
+ if model_file:
+ _remove_old_file(dl_model.model_file)
+ dl_model.model_file = model_file
+ model_file = None
+ if weights_file:
+ _remove_old_file(dl_model.weights_file)
+ dl_model.weights_file = weights_file
+ weights_file = None
+ if labelmap_file:
+ _remove_old_file(dl_model.labelmap_file)
+ dl_model.labelmap_file = labelmap_file
+ labelmap_file = None
+ if interpretation_file:
+ _remove_old_file(dl_model.interpretation_file)
+ dl_model.interpretation_file = interpretation_file
+ interpretation_file = None
+
+ dl_model.updated_date = timezone.now()
+ dl_model.save()
+
+ rq_id = "auto_annotation.create.{}".format(dl_model_id)
+ queue = django_rq.get_queue('default')
+ queue.enqueue_call(
+ func = _update_dl_model_thread,
+ args = (dl_model_id,
+ model_file,
+ weights_file,
+ labelmap_file,
+ interpretation_file,
+ run_tests,
+ ),
+ job_id = rq_id
+ )
+
+ return rq_id
+
+def create_empty(owner, framework=FrameworkChoice.OPENVINO):
+ db_model = AnnotationModel(
+ owner=owner,
+ )
+ db_model.save()
+
+ model_path = db_model.get_dirname()
+ if os.path.isdir(model_path):
+ shutil.rmtree(model_path)
+ os.mkdir(model_path)
+
+ return db_model.id
+
+@transaction.atomic
+def delete(dl_model_id):
+ dl_model = AnnotationModel.objects.select_for_update().get(pk=dl_model_id)
+ if dl_model:
+ if dl_model.primary:
+ raise Exception("Can not delete primary model {}".format(dl_model_id))
+
+ dl_model.delete()
+ shutil.rmtree(dl_model.get_dirname(), ignore_errors=True)
+ else:
+ raise Exception("Requested DL model {} doesn't exist".format(dl_model_id))
diff --git a/cvat/apps/auto_annotation/models.py b/cvat/apps/auto_annotation/models.py
index a59acdef..72513b10 100644
--- a/cvat/apps/auto_annotation/models.py
+++ b/cvat/apps/auto_annotation/models.py
@@ -2,3 +2,54 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
+
+from enum import Enum
+
+from django.db import models
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.files.storage import FileSystemStorage
+
+fs = FileSystemStorage()
+
+def upload_path_handler(instance, filename):
+ return "{models_root}/{id}/{file}".format(models_root=settings.MODELS_ROOT, id=instance.id, file=filename)
+
+class FrameworkChoice(Enum):
+ OPENVINO = 'openvino'
+ TENSORFLOW = 'tensorflow'
+ PYTORCH = 'pytorch'
+
+ def __str__(self):
+ return self.value
+
+
+class SafeCharField(models.CharField):
+ def get_prep_value(self, value):
+ value = super().get_prep_value(value)
+ if value:
+ return value[:self.max_length]
+ return value
+
+class AnnotationModel(models.Model):
+ name = SafeCharField(max_length=256)
+ owner = models.ForeignKey(User, null=True, blank=True,
+ on_delete=models.SET_NULL)
+ created_date = models.DateTimeField(auto_now_add=True)
+ updated_date = models.DateTimeField(auto_now_add=True)
+ model_file = models.FileField(upload_to=upload_path_handler, storage=fs)
+ weights_file = models.FileField(upload_to=upload_path_handler, storage=fs)
+ labelmap_file = models.FileField(upload_to=upload_path_handler, storage=fs)
+ interpretation_file = models.FileField(upload_to=upload_path_handler, storage=fs)
+ shared = models.BooleanField(default=False)
+ primary = models.BooleanField(default=False)
+ framework = models.CharField(max_length=32, default=FrameworkChoice.OPENVINO)
+
+ class Meta:
+ default_permissions = ()
+
+ def get_dirname(self):
+ return "{models_root}/{id}".format(models_root=settings.MODELS_ROOT, id=self.id)
+
+ def __str__(self):
+ return self.name
diff --git a/cvat/apps/auto_annotation/permissions.py b/cvat/apps/auto_annotation/permissions.py
new file mode 100644
index 00000000..ede8d611
--- /dev/null
+++ b/cvat/apps/auto_annotation/permissions.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import rules
+
+from cvat.apps.authentication.auth import has_admin_role, has_user_role
+
+@rules.predicate
+def is_model_owner(db_user, db_dl_model):
+ return db_dl_model.owner == db_user
+
+@rules.predicate
+def is_shared_model(_, db_dl_model):
+ return db_dl_model.shared
+
+@rules.predicate
+def is_primary_model(_, db_dl_model):
+ return db_dl_model.primary
+
+def setup_permissions():
+ rules.add_perm('auto_annotation.model.create', has_admin_role | has_user_role)
+
+ rules.add_perm('auto_annotation.model.update', (has_admin_role | is_model_owner) & ~is_primary_model)
+
+ rules.add_perm('auto_annotation.model.delete', (has_admin_role | is_model_owner) & ~is_primary_model)
+
+ rules.add_perm('auto_annotation.model.access', has_admin_role | is_model_owner |
+ is_shared_model | is_primary_model)
diff --git a/cvat/apps/auto_annotation/static/auto_annotation/js/auto_annotation.js b/cvat/apps/auto_annotation/static/auto_annotation/js/auto_annotation.js
deleted file mode 100644
index 558268b9..00000000
--- a/cvat/apps/auto_annotation/static/auto_annotation/js/auto_annotation.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2018 Intel Corporation
- *
- * SPDX-License-Identifier: MIT
-*/
-
-/* global
- showMessage:false
-*/
-
-"use strict";
-
-window.cvat = window.cvat || {};
-window.cvat.dashboard = window.cvat.dashboard || {};
-window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
-window.cvat.dashboard.uiCallbacks.push(function(newElements) {
- let tids = [];
- for (let el of newElements) {
- tids.push(el.id.split("_")[1]);
- }
-
- $.ajax({
- type: "POST",
- url: "/auto_annotation/meta/get",
- data: JSON.stringify(tids),
- contentType: "application/json; charset=utf-8",
- success: (data) => {
- newElements.each(function() {
- let elem = $(this);
- let tid = +elem.attr("id").split("_")[1];
-
- const autoAnnoButton = $("").addClass("regular dashboardButtonUI dashboardAutoAnno");
- autoAnnoButton.appendTo(elem.find("div.dashboardButtonsUI")[0]);
-
- if (tid in data && data[tid].active) {
- autoAnnoButton.text("Cancel auto annotation");
- autoAnnoButton.addClass("autoAnnotationProcess");
- window.cvat.autoAnnotation.checkAutoAnnotationRequest(tid, autoAnnoButton);
- }
-
- autoAnnoButton.on("click", () => {
- if (autoAnnoButton.hasClass("autoAnnotationProcess")) {
- $.post(`/auto_annotation/cancel/task/${tid}`).fail( (data) => {
- let message = `Error during cancel auto annotation request. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
- showMessage(message);
- throw Error(message);
- });
- }
- else {
- let dialogWindow = $(`#${window.cvat.autoAnnotation.modalWindowId}`);
- dialogWindow.attr("current_tid", tid);
- dialogWindow.removeClass("hidden");
- }
- });
- });
- },
- error: (data) => {
- let message = `Can not get auto annotation meta info. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
- window.cvat.autoAnnotation.badResponse(message);
- }
- });
-});
-
-window.cvat.autoAnnotation = {
- modalWindowId: "autoAnnotationWindow",
- autoAnnoFromId: "autoAnnotationForm",
- autoAnnoModelFieldId: "autoAnnotationModelField",
- autoAnnoWeightsFieldId: "autoAnnotationWeightsField",
- autoAnnoConfigFieldId: "autoAnnotationConfigField",
- autoAnnoConvertFieldId: "autoAnnotationConvertField",
- autoAnnoCloseButtonId: "autoAnnoCloseButton",
- autoAnnoSubmitButtonId: "autoAnnoSubmitButton",
-
- checkAutoAnnotationRequest: (tid, autoAnnoButton) => {
- function timeoutCallback() {
- $.get(`/auto_annotation/check/task/${tid}`).done((data) => {
- if (data.status === "started" || data.status === "queued") {
- let progress = Math.round(data.progress) || 0;
- autoAnnoButton.text(`Cancel auto annotation (${progress}%)`);
- setTimeout(timeoutCallback, 1000);
- }
- else {
- autoAnnoButton.text("Run auto annotation");
- autoAnnoButton.removeClass("autoAnnotationProcess");
- }
- }).fail((data) => {
- let message = "Error was occurred during check annotation status. " +
- `Code: ${data.status}, text: ${data.responseText || data.statusText}`;
- window.cvat.autoAnnotation.badResponse(message);
- });
- }
- setTimeout(timeoutCallback, 1000);
- },
-
- badResponse(message) {
- showMessage(message);
- throw Error(message);
- },
-};
-
-function submitButtonOnClick() {
- const annoWindow = $(`#${window.cvat.autoAnnotation.modalWindowId}`);
- const tid = annoWindow.attr("current_tid");
- const modelInput = $(`#${window.cvat.autoAnnotation.autoAnnoModelFieldId}`);
- const weightsInput = $(`#${window.cvat.autoAnnotation.autoAnnoWeightsFieldId}`);
- const configInput = $(`#${window.cvat.autoAnnotation.autoAnnoConfigFieldId}`);
- const convFileInput = $(`#${window.cvat.autoAnnotation.autoAnnoConvertFieldId}`);
-
- const modelFile = modelInput.prop("files")[0];
- const weightsFile = weightsInput.prop("files")[0];
- const configFile = configInput.prop("files")[0];
- const convFile = convFileInput.prop("files")[0];
-
- if (!modelFile || !weightsFile || !configFile || !convFile) {
- showMessage("All files must be selected");
- return;
- }
-
- let taskData = new FormData();
- taskData.append("model", modelFile);
- taskData.append("weights", weightsFile);
- taskData.append("config", configFile);
- taskData.append("conv_script", convFile);
-
- $.ajax({
- url: `/auto_annotation/create/task/${tid}`,
- type: "POST",
- data: taskData,
- contentType: false,
- processData: false,
- }).done(() => {
- annoWindow.addClass("hidden");
- const autoAnnoButton = $(`#dashboardTask_${tid} div.dashboardButtonsUI button.dashboardAutoAnno`);
- autoAnnoButton.addClass("autoAnnotationProcess");
- window.cvat.autoAnnotation.checkAutoAnnotationRequest(tid, autoAnnoButton);
- }).fail((data) => {
- let message = "Error was occurred during run annotation request. " +
- `Code: ${data.status}, text: ${data.responseText || data.statusText}`;
- window.cvat.autoAnnotation.badResponse(message);
- });
-}
-
-document.addEventListener("DOMContentLoaded", () => {
- $(`
`).appendTo("body");
-
- const annoWindow = $(`#${window.cvat.autoAnnotation.modalWindowId}`);
- const closeWindowButton = $(`#${window.cvat.autoAnnotation.autoAnnoCloseButtonId}`);
- const submitButton = $(`#${window.cvat.autoAnnotation.autoAnnoSubmitButtonId}`);
-
-
- closeWindowButton.on("click", () => {
- annoWindow.addClass("hidden");
- });
-
- submitButton.on("click", () => {
- submitButtonOnClick();
- });
-});
diff --git a/cvat/apps/auto_annotation/static/auto_annotation/js/dashboardPlugin.js b/cvat/apps/auto_annotation/static/auto_annotation/js/dashboardPlugin.js
new file mode 100644
index 00000000..82e3a6f2
--- /dev/null
+++ b/cvat/apps/auto_annotation/static/auto_annotation/js/dashboardPlugin.js
@@ -0,0 +1,792 @@
+/*
+ * Copyright (C) 2018 Intel Corporation
+ *
+ * SPDX-License-Identifier: MIT
+*/
+
+/* global
+ showMessage
+ showOverlay
+ userConfirm
+*/
+
+window.cvat = window.cvat || {};
+window.cvat.dashboard = window.cvat.dashboard || {};
+window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
+
+const AutoAnnotationServer = {
+ start(modelId, taskId, data, success, error, progress, check) {
+ $.ajax({
+ url: `/auto_annotation/start/${modelId}/${taskId}`,
+ type: 'POST',
+ data: JSON.stringify(data),
+ contentType: 'application/json',
+ success: (responseData) => {
+ check(responseData.id, success, error, progress);
+ },
+ error: (responseData) => {
+ const message = `Starting request has been failed. Code: ${responseData.status}. Message: ${responseData.responseText || responseData.statusText}`;
+ error(message);
+ },
+ });
+ },
+
+ update(data, success, error, progress, check, modelId) {
+ let url = '';
+ if (modelId === null) {
+ url = '/auto_annotation/create';
+ } else {
+ url = `/auto_annotation/update/${modelId}`;
+ }
+
+ $.ajax({
+ url,
+ type: 'POST',
+ data,
+ contentType: false,
+ processData: false,
+ success: (responseData) => {
+ check(responseData.id, success, error, progress);
+ },
+ error: (responseData) => {
+ const message = `Creating request has been failed. Code: ${responseData.status}. Message: ${responseData.responseText || responseData.statusText}`;
+ error(message);
+ },
+ });
+ },
+
+ delete(modelId, success, error) {
+ $.ajax({
+ url: `/auto_annotation/delete/${modelId}`,
+ type: 'DELETE',
+ success,
+ error: (data) => {
+ const message = `Deleting request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
+ error(message);
+ },
+ });
+ },
+
+ check(workerId, success, error, progress) {
+ function updateProgress(data) {
+ if (data.progress && progress) {
+ progress(data.progress);
+ }
+ }
+
+ function checkCallback() {
+ $.ajax({
+ url: `/auto_annotation/check/${workerId}`,
+ type: 'GET',
+ success: (data) => {
+ updateProgress(data, progress);
+
+ switch (data.status) {
+ case 'failed':
+ error(`Checking request has returned the "${data.status}" status. Message: ${data.error}`);
+ break;
+
+ case 'unknown':
+ error(`Checking request has returned the "${data.status}" status.`);
+ break;
+
+ case 'finished':
+ success();
+ break;
+
+ default:
+ setTimeout(checkCallback, 1000);
+ }
+ },
+ error: (data) => {
+ const message = `Checking request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
+ error(message);
+ },
+ });
+ }
+
+ setTimeout(checkCallback, 1000);
+ },
+
+ meta(tids, success, error) {
+ $.ajax({
+ url: '/auto_annotation/meta/get',
+ type: 'POST',
+ data: JSON.stringify(tids),
+ contentType: 'application/json',
+ success,
+ error: (data) => {
+ const message = `Getting meta request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
+ error(message);
+ },
+ });
+ },
+
+ cancel(tid, success, error) {
+ $.ajax({
+ url: `/auto_annotation/cancel/${tid}`,
+ type: 'GET',
+ success,
+ error: (data) => {
+ const message = `Getting meta request has been failed. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
+ error(message);
+ },
+ });
+ },
+};
+
+
+class AutoAnnotationModelManagerView {
+ constructor() {
+ const html = `
+
+
+
+
+
+
+
+
+
+ | Name |
+ Upload Date |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+ this.el = $(html);
+
+ this.table = this.el.find(`#${window.cvat.autoAnnotation.managerUploadedModelsId}`);
+ this.globallyBlock = this.el.find(`#${window.cvat.autoAnnotation.uploadGloballyBlockId}`);
+ this.uploadTitle = this.el.find(`#${window.cvat.autoAnnotation.uploadTitleId}`);
+ this.uploadNameInput = this.el.find(`#${window.cvat.autoAnnotation.uploadNameInputId}`);
+ this.uploadMessage = this.el.find(`#${window.cvat.autoAnnotation.uploadMessageId}`);
+ this.selectedFilesLabel = this.el.find(`#${window.cvat.autoAnnotation.selectedFilesId}`);
+ this.modelNameInput = this.el.find(`#${window.cvat.autoAnnotation.uploadNameInputId}`);
+ this.localSource = this.el.find(`#${window.cvat.autoAnnotation.uploadLocalSourceId}`);
+ this.shareSource = this.el.find(`#${window.cvat.autoAnnotation.uploadShareSourceId}`);
+ this.cancelButton = this.el.find(`#${window.cvat.autoAnnotation.cancelUploadButtonId}`);
+ this.submitButton = this.el.find(`#${window.cvat.autoAnnotation.submitUploadButtonId}`);
+ this.globallyBox = this.el.find(`#${window.cvat.autoAnnotation.uploadGloballyId}`);
+ this.selectButton = this.el.find(`#${window.cvat.autoAnnotation.selectFilesButtonId}`);
+ this.localSelector = this.el.find(`#${window.cvat.autoAnnotation.localFileSelectorId}`);
+ this.shareSelector = $('#dashboardShareBrowseModal');
+ this.shareBrowseTree = $('#dashboardShareBrowser');
+ this.submitShare = $('#dashboardSubmitBrowseServer');
+
+ this.id = null;
+ this.source = this.localSource.prop('checked') ? 'local' : 'shared';
+ this.files = [];
+
+ function filesLabel(source, files) {
+ const fileLabels = source === 'local' ? [...files].map(el => el.name) : files;
+ if (fileLabels.length) {
+ const labelStr = fileLabels.join(', ');
+ if (labelStr.length > 30) {
+ return `${labelStr.substr(0, 30)}..`;
+ }
+
+ return labelStr;
+ }
+
+ return 'No Files';
+ }
+
+ function extractFiles(extensions, files, source) {
+ const extractedFiles = {};
+ function getExt(file) {
+ return source === 'local' ? file.name.split('.').pop() : file.split('.').pop();
+ }
+
+ function addFile(file, extention) {
+ if (extention in files) {
+ throw Error(`More than one file with the extension .${extention} have been found`);
+ }
+
+ extractedFiles[extention] = file;
+ }
+
+ files.forEach((file) => {
+ const fileExt = getExt(file);
+ if (extensions.includes(fileExt)) {
+ addFile(file, fileExt);
+ }
+ });
+
+ return extractedFiles;
+ }
+
+ function validateFiles(isUpdate, files, source) {
+ const extensions = ['xml', 'bin', 'py', 'json'];
+ const extractedFiles = extractFiles(extensions, files, source);
+
+ if (!isUpdate) {
+ extensions.forEach((extension) => {
+ if (!(extension in extractedFiles)) {
+ throw Error(`Please specify a .${extension} file`);
+ }
+ });
+ }
+
+ return extractedFiles;
+ }
+
+ this.localSource.on('click', () => {
+ if (this.source !== 'local') {
+ this.source = 'local';
+ this.files = [];
+ this.selectedFilesLabel.text(filesLabel(this.source, this.files));
+ }
+ });
+
+ this.shareSource.on('click', () => {
+ if (this.source !== 'shared') {
+ this.source = 'shared';
+ this.files = [];
+ this.selectedFilesLabel.text(filesLabel(this.source, this.files));
+ }
+ });
+
+ this.selectButton.on('click', () => {
+ if (this.source === 'local') {
+ this.localSelector.click();
+ } else {
+ this.shareSelector.appendTo('body');
+ this.shareBrowseTree.jstree('refresh');
+ this.shareSelector.removeClass('hidden');
+ this.shareBrowseTree.jstree({
+ core: {
+ data: {
+ url: 'get_share_nodes',
+ data: node => ({ id: node.id }),
+ },
+ },
+ plugins: ['checkbox', 'sort'],
+ });
+ }
+ });
+
+ this.submitShare.on('click', () => {
+ if (!this.el.hasClass('hidden')) {
+ this.shareSelector.addClass('hidden');
+ this.files = this.shareBrowseTree.jstree(true).get_selected();
+ this.selectedFilesLabel.text(filesLabel(this.source, this.files));
+ }
+ });
+
+ this.localSelector.on('change', (e) => {
+ this.files = Array.from(e.target.files);
+ this.selectedFilesLabel.text(filesLabel(this.source, this.files));
+ });
+
+ this.cancelButton.on('click', () => this.el.addClass('hidden'));
+ this.submitButton.on('click', () => {
+ try {
+ this.submitButton.prop('disabled', true);
+
+ const name = $.trim(this.modelNameInput.prop('value'));
+ if (!name.length) {
+ this.uploadMessage.css('color', 'red');
+ this.uploadMessage.text('Please specify a model name');
+ return;
+ }
+
+ let validatedFiles = {};
+ try {
+ validatedFiles = validateFiles(this.id !== null, this.files, this.source);
+ } catch (err) {
+ this.uploadMessage.css('color', 'red');
+ this.uploadMessage.text(err);
+ return;
+ }
+
+ const modelData = new FormData();
+ modelData.append('name', name);
+ modelData.append('storage', this.source);
+ modelData.append('shared', this.globallyBox.prop('checked'));
+
+ ['xml', 'bin', 'json', 'py'].filter(e => e in validatedFiles).forEach((ext) => {
+ modelData.append(ext, validatedFiles[ext]);
+ });
+
+ this.uploadMessage.text('');
+ const overlay = showOverlay('Send request to the server..');
+ window.cvat.autoAnnotation.server.update(modelData, () => {
+ window.location.reload();
+ }, (message) => {
+ overlay.remove();
+ showMessage(message);
+ }, (progress) => {
+ overlay.setMessage(progress);
+ }, window.cvat.autoAnnotation.server.check, this.id);
+ } finally {
+ this.submitButton.prop('disabled', false);
+ }
+ });
+ }
+
+ reset() {
+ const setBlocked = () => {
+ if (window.cvat.autoAnnotation.data.admin) {
+ this.globallyBlock.removeClass('hidden');
+ } else {
+ this.globallyBlock.addClass('hidden');
+ }
+ };
+
+ setBlocked();
+ this.uploadTitle.text('Create Model');
+ this.uploadNameInput.prop('value', '');
+ this.uploadMessage.css('color', '');
+ this.uploadMessage.text('');
+ this.selectedFilesLabel.text('No Files');
+ this.localSource.prop('checked', true);
+ this.globallyBox.prop('checked', false);
+ this.table.empty();
+
+ this.id = null;
+ this.source = this.localSource.prop('checked') ? 'local' : 'share';
+ this.files = [];
+
+ const updateButtonClickHandler = (event) => {
+ this.reset();
+
+ this.uploadTitle.text('Update Model');
+ this.uploadNameInput.prop('value', `${event.data.model.name}`);
+ this.id = event.data.model.id;
+ };
+
+ const deleteButtonClickHandler = (event) => {
+ userConfirm(`Do you actually want to delete the "${event.data.model.name}" model. Are you sure?`, () => {
+ window.cvat.autoAnnotation.server.delete(event.data.model.id, () => {
+ const filtered = window.cvat.autoAnnotation.data.models.filter(
+ item => item !== event.data.model,
+ );
+ window.cvat.autoAnnotation.data.models = filtered;
+ this.reset();
+ }, (message) => {
+ showMessage(message);
+ });
+ });
+ };
+
+ const getModelModifyButtons = (model) => {
+ if (model.primary) {
+ return '
| ';
+ }
+
+ const updateButtonHtml = '
';
+ const deleteButtonHtml = '
';
+
+ return $('
| ').append(
+ $(updateButtonHtml).on('click', { model }, updateButtonClickHandler),
+ $(deleteButtonHtml).on('click', { model }, deleteButtonClickHandler),
+ );
+ };
+
+ window.cvat.autoAnnotation.data.models.forEach((model) => {
+ const rowHtml = `
+ | ${model.name} |
+ ${model.uploadDate} |
+
`;
+
+ this.table.append(
+ $(rowHtml).append(getModelModifyButtons(model)),
+ );
+ });
+
+ return this;
+ }
+
+ show() {
+ this.el.removeClass('hidden');
+ return this;
+ }
+
+ get element() {
+ return this.el;
+ }
+}
+
+
+class AutoAnnotationModelRunnerView {
+ constructor() {
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Task Label |
+ DL Model Label |
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+
+ this.el = $(html);
+ this.id = null;
+ this.tid = null;
+ this.initButton = null;
+ this.modelsTable = this.el.find(`#${window.cvat.autoAnnotation.runnerUploadedModelsId}`);
+ this.labelsTable = this.el.find(`#${window.cvat.autoAnnotation.annotationLabelsId}`);
+ this.active = null;
+
+ this.el.find(`#${window.cvat.autoAnnotation.cancelAnnotationId}`).on('click', () => {
+ this.el.addClass('hidden');
+ });
+
+ this.el.find(`#${window.cvat.autoAnnotation.submitAnnotationId}`).on('click', () => {
+ try {
+ if (this.id === null) {
+ throw Error('Please specify a model for an annotation process');
+ }
+
+ const mapping = {};
+ $('.annotatorMappingRow').each((_, element) => {
+ const dlModelLabel = $(element).find('.annotatorDlLabelSelector')[0].value;
+ const taskLabel = $(element).find('.annotatorTaskLabelSelector')[0].value;
+ if (dlModelLabel in mapping) {
+ throw Error(`The label "${dlModelLabel}" has been specified twice or more`);
+ }
+ mapping[dlModelLabel] = taskLabel;
+ });
+
+ if (!Object.keys(mapping).length) {
+ throw Error('Labels for an annotation process haven\'t been found');
+ }
+
+ const overlay = showOverlay('Request has been sent');
+ window.cvat.autoAnnotation.server.start(this.id, this.tid, {
+ reset: $(`#${window.cvat.autoAnnotation.removeCurrentAnnotationId}`).prop('checked'),
+ labels: mapping,
+ }, () => {
+ overlay.remove();
+ this.initButton[0].setupRun();
+ window.cvat.autoAnnotation.runner.hide();
+ }, (message) => {
+ overlay.remove();
+ this.initButton[0].setupRun();
+ showMessage(message);
+ }, () => {
+ window.location.reload();
+ }, window.cvat.autoAnnotation.server.check);
+ } catch (error) {
+ showMessage(error);
+ }
+ });
+ }
+
+ reset(data, initButton) {
+ function labelsSelect(labels, elClass) {
+ const select = $(`
`);
+ labels.forEach(label => select.append($(`
`)));
+ select.prop('value', null);
+
+ return select;
+ }
+
+ function makeCreator(dlSelect, taskSelect, callback) {
+ let dlIsFilled = false;
+ let taskIsFilled = false;
+ const creator = $('
').append(
+ $('
| ').append(taskSelect),
+ $('
| ').append(dlSelect),
+ );
+
+ const onSelectHandler = () => {
+ $('
| ').append(
+ $('
').css('top', '0px').on('click', (e) => {
+ $(e.target.parentNode.parentNode).remove();
+ }),
+ ).appendTo(creator);
+
+ creator.addClass('annotatorMappingRow');
+ callback();
+ };
+
+ dlSelect.on('change', (e) => {
+ if (e.target.value && taskIsFilled) {
+ dlSelect.off('change');
+ taskSelect.off('change');
+ onSelectHandler();
+ }
+ dlIsFilled = Boolean(e.target.value);
+ });
+
+ taskSelect.on('change', (e) => {
+ if (e.target.value && dlIsFilled) {
+ dlSelect.off('change');
+ taskSelect.off('change');
+ onSelectHandler();
+ }
+
+ taskIsFilled = Boolean(e.target.value);
+ });
+
+ return creator;
+ }
+
+ this.id = null;
+ this.initButton = initButton;
+ this.tid = data.taskid;
+ this.modelsTable.empty();
+ this.labelsTable.empty();
+ this.active = null;
+
+ const modelItemClickHandler = (event) => {
+ if (this.active) {
+ this.active.style.color = '';
+ }
+
+ this.id = event.data.model.id;
+ this.active = event.target;
+ this.active.style.color = 'darkblue';
+
+ this.labelsTable.empty();
+ const labels = Object.values(event.data.data.spec.labels);
+ const intersection = labels.filter(el => event.data.model.labels.indexOf(el) !== -1);
+ intersection.forEach((label) => {
+ const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector');
+ dlSelect.prop('value', label);
+ const taskSelect = labelsSelect(labels, 'annotatorTaskLabelSelector');
+ taskSelect.prop('value', label);
+ $('
').append(
+ $('
| ').append(taskSelect),
+ $('
| ').append(dlSelect),
+ $('
| ').append(
+ $('
').css('top', '0px').on('click', (e) => {
+ $(e.target.parentNode.parentNode).remove();
+ }),
+ ),
+ ).appendTo(this.labelsTable);
+ });
+
+ const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector');
+ const taskSelect = labelsSelect(labels, 'annotatorTaskLabelSelector');
+
+ const callback = () => {
+ makeCreator(
+ labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector'),
+ labelsSelect(labels, 'annotatorTaskLabelSelector'),
+ callback,
+ ).appendTo(this.labelsTable);
+ };
+
+ makeCreator(dlSelect, taskSelect, callback).appendTo(this.labelsTable);
+ };
+
+ window.cvat.autoAnnotation.data.models.forEach((model) => {
+ this.modelsTable.append(
+ $(`
| |
`).on(
+ 'click', { model, data }, modelItemClickHandler,
+ ),
+ );
+ });
+
+ return this;
+ }
+
+ show() {
+ this.el.removeClass('hidden');
+ return this;
+ }
+
+ hide() {
+ this.el.addClass('hidden');
+ return this;
+ }
+
+ get element() {
+ return this.el;
+ }
+}
+
+window.cvat.autoAnnotation = {
+ managerWindowId: 'annotatorManagerWindow',
+ managerContentId: 'annotatorManagerContent',
+ managerUploadedModelsId: 'annotatorManagerUploadedModels',
+ uploadContentId: 'annotatorManagerUploadModel',
+ uploadTitleId: 'annotatorManagerUploadTitle',
+ uploadNameInputId: 'annotatorManagerUploadNameInput',
+ uploadLocalSourceId: 'annotatorManagerUploadLocalSource',
+ uploadShareSourceId: 'annotatorManagerUploadShareSource',
+ uploadGloballyId: 'annotatorManagerUploadGlobally',
+ uploadGloballyBlockId: 'annotatorManagerUploadGloballyblock',
+ selectFilesButtonId: 'annotatorManagerUploadSelector',
+ selectedFilesId: 'annotatorManagerUploadSelectedFiles',
+ localFileSelectorId: 'annotatorManagerUploadLocalSelector',
+ shareFileSelectorId: 'annotatorManagerUploadShareSelector',
+ submitUploadButtonId: 'annotatorManagerSubmitUploadButton',
+ cancelUploadButtonId: 'annotatorManagerCancelUploadButton',
+ uploadMessageId: 'annotatorUploadStatusMessage',
+
+ runnerWindowId: 'annotatorRunnerWindow',
+ runnerContentId: 'annotatorRunnerContent',
+ runnerUploadedModelsId: 'annotatorRunnerUploadedModels',
+ removeCurrentAnnotationId: 'annotatorRunnerRemoveCurrentAnnotationBox',
+ annotationLabelsId: 'annotatorRunnerAnnotationLabels',
+ submitAnnotationId: 'annotatorRunnerSubmitAnnotationButton',
+ cancelAnnotationId: 'annotatorRunnerCancelAnnotationButton',
+
+ managerButtonId: 'annotatorManagerButton',
+};
+
+window.cvat.dashboard.uiCallbacks.push((newElements) => {
+ window.cvat.autoAnnotation.server = AutoAnnotationServer;
+ window.cvat.autoAnnotation.manager = new AutoAnnotationModelManagerView();
+ window.cvat.autoAnnotation.runner = new AutoAnnotationModelRunnerView();
+
+ const tids = Array.from(newElements, el => el.id.split('_')[1]);
+
+ window.cvat.autoAnnotation.server.meta(tids, (data) => {
+ window.cvat.autoAnnotation.data = data;
+ $('body').append(window.cvat.autoAnnotation.manager.element, window.cvat.autoAnnotation.runner.element);
+ $(`
`)
+ .on('click', () => {
+ const overlay = showOverlay('The manager are being setup..');
+ window.cvat.autoAnnotation.manager.reset().show();
+ overlay.remove();
+ }).appendTo('#dashboardManageButtons');
+
+ newElements.each((_, element) => {
+ const elem = $(element);
+ const tid = +elem.attr('id').split('_')[1];
+
+ const button = $('
').addClass('regular dashboardButtonUI');
+ button[0].setupRun = function setupRun() {
+ const self = $(this);
+ self.text('Run Auto Annotation').off('click').on('click', () => {
+ const overlay = showOverlay('Task date are being recieved from the server..');
+ $.ajax({
+ url: `/get/task/${tid}`,
+ dataType: 'json',
+ success: (responseData) => {
+ overlay.setMessage('The model runner are being setup..');
+ window.cvat.autoAnnotation.runner.reset(responseData, self).show();
+ overlay.remove();
+ },
+ error: (responseData) => {
+ showMessage(`Can't get task data. Code: ${responseData.status}. Message: ${responseData.responseText || responseData.statusText}`);
+ },
+ complete: () => {
+ overlay.remove();
+ },
+ });
+ });
+ };
+
+ button[0].setupCancel = function setupCancel() {
+ const self = $(this);
+ self.off('click').text('Cancel Auto Annotation').on('click', () => {
+ userConfirm('Process will be canceled. Are you sure?', () => {
+ window.cvat.autoAnnotation.server.cancel(tid, () => {
+ this.setupRun();
+ }, (message) => {
+ showMessage(message);
+ });
+ });
+ });
+
+ window.cvat.autoAnnotation.server.check(
+ window.cvat.autoAnnotation.data.run[tid].rq_id,
+ () => {
+ this.setupRun();
+ },
+ (error) => {
+ button[0].setupRun();
+ button.text('Annotation has failed');
+ button.title(error);
+ },
+ (progress) => {
+ button.text(`Cancel Auto Annotation (${progress.toString().slice(0, 4)})%`);
+ },
+ );
+ };
+
+ const taskStatus = window.cvat.autoAnnotation.data.run[tid];
+ if (taskStatus && ['queued', 'started'].includes(taskStatus.status)) {
+ button[0].setupCancel();
+ } else {
+ button[0].setupRun();
+ }
+
+ button.appendTo(elem.find('div.dashboardButtonsUI')[0]);
+ });
+ }, (error) => {
+ showMessage(`Cannot get models meta information: ${error}`);
+ });
+});
diff --git a/cvat/apps/auto_annotation/static/auto_annotation/stylesheet.css b/cvat/apps/auto_annotation/static/auto_annotation/stylesheet.css
new file mode 100644
index 00000000..78828bdf
--- /dev/null
+++ b/cvat/apps/auto_annotation/static/auto_annotation/stylesheet.css
@@ -0,0 +1,83 @@
+#annotatorManagerContent, #annotatorRunnerContent {
+ width: 800px;
+ height: 300px;
+}
+
+#annotatorManagerButton {
+ padding: 7px;
+ margin-left: 4px;
+}
+
+.modelsTable {
+ width: 100%;
+ color:#666;
+ text-shadow: 1px 1px 0px #fff;
+ background:#D2D3D4;
+ border:#ccc 1px solid;
+ border-radius: 3px;
+ box-shadow: 0 1px 2px black;
+}
+
+.modelsTable th {
+ border-top: 1px solid #fafafa;
+ border-bottom: 1px solid #e0e0e0;
+ background: #ededed;
+}
+
+.modelsTable th:first-child {
+ text-align: left;
+ padding-left:20px;
+}
+
+.modelsTable tr:first-child th:first-child {
+ border-top-left-radius:3px;
+}
+
+.modelsTable tr:first-child th:last-child {
+ border-top-right-radius:3px;
+}
+
+.modelsTable tr {
+ text-align: center;
+ padding-left: 20px;
+}
+
+.modelsTable td:first-child {
+ text-align: left;
+ padding-left: 20px;
+ border-left: 0;
+}
+
+.modelsTable td {
+ padding: 18px;
+ border-top: 1px solid #ffffff;
+ border-bottom:1px solid #e0e0e0;
+ border-left: 1px solid #e0e0e0;
+ background: #fafafa;
+}
+
+.modelsTable tr.even td {
+ background: #f6f6f6;
+}
+
+.modelsTable tr:last-child td {
+ border-bottom:0;
+}
+
+.modelsTable tr:last-child td:first-child {
+ border-bottom-left-radius:3px;
+}
+
+.modelsTable tr:last-child td:last-child {
+ border-bottom-right-radius:3px;
+}
+
+.modelsTable tr:hover td {
+ background: #f2f2f2;
+}
+
+#annotatorManagerUploadModel {
+ float: left;
+ padding-left: 3%;
+ width: 40%;
+}
diff --git a/cvat/apps/auto_annotation/urls.py b/cvat/apps/auto_annotation/urls.py
index 87131757..2aa75c5e 100644
--- a/cvat/apps/auto_annotation/urls.py
+++ b/cvat/apps/auto_annotation/urls.py
@@ -7,8 +7,13 @@ from django.urls import path
from . import views
urlpatterns = [
- path("create/task/
", views.create),
- path("check/task/", views.check),
- path("cancel/task/", views.cancel),
+ path("create", views.create_model),
+ path("update/", views.update_model),
+ path("delete/", views.delete_model),
+
+ path("start//", views.start_annotation),
+ path("check/", views.check),
+ path("cancel/", views.cancel),
+
path("meta/get", views.get_meta_info),
]
diff --git a/cvat/apps/auto_annotation/views.py b/cvat/apps/auto_annotation/views.py
index e7577a9e..dd43e3f7 100644
--- a/cvat/apps/auto_annotation/views.py
+++ b/cvat/apps/auto_annotation/views.py
@@ -3,32 +3,35 @@
#
# SPDX-License-Identifier: MIT
-from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
-from rules.contrib.views import permission_required, objectgetter
-from cvat.apps.authentication.decorators import login_required
-from cvat.apps.engine.models import Task as TaskModel
-from cvat.apps.engine import annotation
-
import django_rq
import fnmatch
import json
import os
import rq
+from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
+from django.db.models import Q
+from rules.contrib.views import permission_required, objectgetter
+
+from cvat.apps.authentication.decorators import login_required
+from cvat.apps.engine.models import Task as TaskModel
+from cvat.apps.engine import annotation
+from cvat.apps.authentication.auth import has_admin_role
from cvat.apps.engine.log import slogger
from .model_loader import ModelLoader, load_label_map
from .image_loader import ImageLoader
-import os.path as osp
+from . import model_manager
+from .models import AnnotationModel
def get_image_data(path_to_data):
def get_image_key(item):
- return int(osp.splitext(osp.basename(item))[0])
+ return int(os.path.splitext(os.path.basename(item))[0])
image_list = []
for root, _, filenames in os.walk(path_to_data):
for filename in fnmatch.filter(filenames, "*.jpg"):
- image_list.append(osp.join(root, filename))
+ image_list.append(os.path.join(root, filename))
image_list.sort(key=get_image_key)
return ImageLoader(image_list)
@@ -202,7 +205,6 @@ def run_inference_engine_annotation(path_to_data, model_file, weights_file,
frame_counter += 1
if not update_progress(job, frame_counter * 100 / data_len):
return None
-
processed_detections = process_detections(detections, convertation_file)
add_boxes(processed_detections.get_boxes(), result["create"]["boxes"])
@@ -222,7 +224,7 @@ def update_progress(job, progress):
job.save_meta()
return True
-def create_thread(tid, model_file, weights_file, labels_mapping, attributes, convertation_file):
+def create_thread(tid, model_file, weights_file, labels_mapping, attributes, convertation_file, reset):
try:
job = rq.get_current_job()
job.meta["progress"] = 0
@@ -246,85 +248,205 @@ def create_thread(tid, model_file, weights_file, labels_mapping, attributes, con
slogger.glob.info("auto annotation for task {} canceled by user".format(tid))
return
- annotation.clear_task(tid)
+ if reset:
+ annotation.clear_task(tid)
annotation.save_task(tid, result)
slogger.glob.info("auto annotation for task {} done".format(tid))
- except Exception:
+ except Exception as e:
try:
slogger.task[tid].exception("exception was occurred during auto annotation of the task", exc_info=True)
except Exception as ex:
slogger.glob.exception("exception was occurred during auto annotation of the task {}: {}".format(tid, str(ex)), exc_info=True)
+ raise ex
+
+ raise e
+
+@login_required
+@permission_required(perm=["engine.task.change"],
+ fn=objectgetter(TaskModel, "tid"), raise_exception=True)
+def cancel(request, tid):
+ try:
+ queue = django_rq.get_queue("low")
+ job = queue.fetch_job("auto_annotation.run.{}".format(tid))
+ if job is None or job.is_finished or job.is_failed:
+ raise Exception("Task is not being annotated currently")
+ elif "cancel" not in job.meta:
+ job.meta["cancel"] = True
+ job.save()
+
+ except Exception as ex:
+ try:
+ slogger.task[tid].exception("cannot cancel auto annotation for task #{}".format(tid), exc_info=True)
+ except Exception as logger_ex:
+ slogger.glob.exception("exception was occured during cancel auto annotation request for task {}: {}".format(tid, str(logger_ex)), exc_info=True)
+ return HttpResponseBadRequest(str(ex))
+
+ return HttpResponse()
+
+
+@login_required
+@permission_required(perm=["auto_annotation.model.create"], raise_exception=True)
+def create_model(request):
+ if request.method != 'POST':
+ return HttpResponseBadRequest("Only POST requests are accepted")
+
+ try:
+ params = request.POST
+ storage = params["storage"]
+ name = params["name"]
+ is_shared = params["shared"].lower() == "true"
+ if is_shared and not has_admin_role(request.user):
+ raise Exception("Only admin can create shared models")
+
+ files = request.FILES if storage == "local" else params
+ model = files["xml"]
+ weights = files["bin"]
+ labelmap = files["json"]
+ interpretation_script = files["py"]
+ owner = request.user
+
+ dl_model_id = model_manager.create_empty(owner=owner)
+ rq_id = model_manager.update_model(
+ dl_model_id=dl_model_id,
+ name=name,
+ model_file=model,
+ weights_file=weights,
+ labelmap_file=labelmap,
+ interpretation_file=interpretation_script,
+ storage=storage,
+ is_shared=is_shared,
+ )
+
+ return JsonResponse({"id": rq_id})
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
+
+@login_required
+@permission_required(perm=["auto_annotation.model.update"],
+ fn=objectgetter(AnnotationModel, "mid"), raise_exception=True)
+def update_model(request, mid):
+ if request.method != 'POST':
+ return HttpResponseBadRequest("Only POST requests are accepted")
+
+ try:
+ params = request.POST
+ storage = params["storage"]
+ name = params.get("name")
+ is_shared = params.get("shared")
+ is_shared = is_shared.lower() == "true" if is_shared else None
+ if is_shared and not has_admin_role(request.user):
+ raise Exception("Only admin can create shared models")
+ files = request.FILES
+ model = files.get("xml")
+ weights = files.get("bin")
+ labelmap = files.get("json")
+ interpretation_script = files.get("py")
+
+ rq_id = model_manager.update_model(
+ dl_model_id=mid,
+ name=name,
+ model_file=model,
+ weights_file=weights,
+ labelmap_file=labelmap,
+ interpretation_file=interpretation_script,
+ storage=storage,
+ is_shared=is_shared,
+ )
+
+ return JsonResponse({"id": rq_id})
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
+
+@login_required
+@permission_required(perm=["auto_annotation.model.delete"],
+ fn=objectgetter(AnnotationModel, "mid"), raise_exception=True)
+def delete_model(request, mid):
+ if request.method != 'DELETE':
+ return HttpResponseBadRequest("Only DELETE requests are accepted")
+ model_manager.delete(mid)
+ return HttpResponse()
@login_required
def get_meta_info(request):
try:
+ tids = json.loads(request.body.decode('utf-8'))
+ response = {
+ "admin": has_admin_role(request.user),
+ "models": [],
+ "run": {},
+ }
+ dl_model_list = list(AnnotationModel.objects.filter(Q(owner=request.user) | Q(primary=True) | Q(shared=True)).order_by('-created_date'))
+ for dl_model in dl_model_list:
+ labels = []
+ if dl_model.labelmap_file and os.path.exists(dl_model.labelmap_file.name):
+ with dl_model.labelmap_file.open('r') as f:
+ labels = list(json.load(f)["label_map"].values())
+
+ response["models"].append({
+ "id": dl_model.id,
+ "name": dl_model.name,
+ "primary": dl_model.primary,
+ "uploadDate": dl_model.created_date,
+ "updateDate": dl_model.updated_date,
+ "labels": labels,
+ })
+
queue = django_rq.get_queue("low")
- tids = json.loads(request.body.decode("utf-8"))
- result = {}
for tid in tids:
- job = queue.fetch_job("auto_annotation.create/{}".format(tid))
+ rq_id = "auto_annotation.run.{}".format(tid)
+ job = queue.fetch_job(rq_id)
if job is not None:
- result[tid] = {
- "active": job.is_queued or job.is_started,
- "success": not job.is_failed
+ response["run"][tid] = {
+ "status": job.status,
+ "rq_id": rq_id,
}
- return JsonResponse(result)
- except Exception as ex:
- slogger.glob.exception("exception was occurred during auto annotation meta request", exc_info=True)
- return HttpResponseBadRequest(str(ex))
+ return JsonResponse(response)
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
@login_required
@permission_required(perm=["engine.task.change"],
fn=objectgetter(TaskModel, "tid"), raise_exception=True)
-def create(request, tid):
- slogger.glob.info("auto annotation create request for task {}".format(tid))
-
- def write_file(path, file_obj):
- with open(path, "wb") as upload_file:
- for chunk in file_obj.chunks():
- upload_file.write(chunk)
-
+@permission_required(perm=["auto_annotation.model.access"],
+ fn=objectgetter(AnnotationModel, "mid"), raise_exception=True)
+def start_annotation(request, mid, tid):
+ slogger.glob.info("auto annotation create request for task {} via DL model {}".format(tid, mid))
try:
db_task = TaskModel.objects.get(pk=tid)
- upload_dir = db_task.get_upload_dirname()
queue = django_rq.get_queue("low")
- job = queue.fetch_job("auto_annotation.create/{}".format(tid))
+ job = queue.fetch_job("auto_annotation.run.{}".format(tid))
if job is not None and (job.is_started or job.is_queued):
raise Exception("The process is already running")
- model_file = request.FILES["model"]
- model_file_path = os.path.join(upload_dir, model_file.name)
- write_file(model_file_path, model_file)
+ data = json.loads(request.body.decode('utf-8'))
- weights_file = request.FILES["weights"]
- weights_file_path = os.path.join(upload_dir, weights_file.name)
- write_file(weights_file_path, weights_file)
+ should_reset = data["reset"]
+ user_defined_labels_mapping = data["labels"]
- config_file = request.FILES["config"]
- config_file_path = os.path.join(upload_dir, config_file.name)
- write_file(config_file_path, config_file)
+ dl_model = AnnotationModel.objects.get(pk=mid)
- convertation_file = request.FILES["conv_script"]
- convertation_file_path = os.path.join(upload_dir, convertation_file.name)
- write_file(convertation_file_path, convertation_file)
+ model_file_path = dl_model.model_file.name
+ weights_file_path = dl_model.weights_file.name
+ labelmap_file = dl_model.labelmap_file.name
+ convertation_file_path = dl_model.interpretation_file.name
db_labels = db_task.label_set.prefetch_related("attributespec_set").all()
db_attributes = {db_label.id:
{db_attr.get_name(): db_attr.id for db_attr in db_label.attributespec_set.all()} for db_label in db_labels}
- db_labels = {db_label.id:db_label.name for db_label in db_labels}
+ db_labels = {db_label.name:db_label.id for db_label in db_labels}
- class_names = load_label_map(config_file_path)
+ model_labels = {value: key for key, value in load_label_map(labelmap_file).items()}
labels_mapping = {}
- for db_key, db_label in db_labels.items():
- for key, label in class_names.items():
- if label == db_label:
- labels_mapping[int(key)] = db_key
+ for user_model_label, user_db_label in user_defined_labels_mapping.items():
+ if user_model_label in model_labels and user_db_label in db_labels:
+ labels_mapping[int(model_labels[user_model_label])] = db_labels[user_db_label]
if not labels_mapping:
raise Exception("No labels found for annotation")
+ rq_id="auto_annotation.run.{}".format(tid)
queue.enqueue_call(func=create_thread,
args=(
tid,
@@ -332,8 +454,10 @@ def create(request, tid):
weights_file_path,
labels_mapping,
db_attributes,
- convertation_file_path),
- job_id="auto_annotation.create/{}".format(tid),
+ convertation_file_path,
+ should_reset,
+ ),
+ job_id = rq_id,
timeout=604800) # 7 days
slogger.task[tid].info("auto annotation job enqueued")
@@ -345,15 +469,14 @@ def create(request, tid):
slogger.glob.exception("exception was occurred during create auto annotation request for task {}: {}".format(tid, str(logger_ex)), exc_info=True)
return HttpResponseBadRequest(str(ex))
- return HttpResponse()
+ return JsonResponse({"id": rq_id})
@login_required
-@permission_required(perm=["engine.task.access"],
- fn=objectgetter(TaskModel, "tid"), raise_exception=True)
-def check(request, tid):
+def check(request, rq_id):
try:
- queue = django_rq.get_queue("low")
- job = queue.fetch_job("auto_annotation.create/{}".format(tid))
+ target_queue = "low" if "auto_annotation.run" in rq_id else "default"
+ queue = django_rq.get_queue(target_queue)
+ job = queue.fetch_job(rq_id)
if job is not None and "cancel" in job.meta:
return JsonResponse({"status": "finished"})
data = {}
@@ -363,39 +486,16 @@ def check(request, tid):
data["status"] = "queued"
elif job.is_started:
data["status"] = "started"
- data["progress"] = job.meta["progress"]
+ data["progress"] = job.meta["progress"] if "progress" in job.meta else ""
elif job.is_finished:
data["status"] = "finished"
job.delete()
else:
data["status"] = "failed"
- data["stderr"] = job.exc_info
+ data["error"] = job.exc_info
job.delete()
except Exception:
data["status"] = "unknown"
return JsonResponse(data)
-
-
-@login_required
-@permission_required(perm=["engine.task.change"],
- fn=objectgetter(TaskModel, "tid"), raise_exception=True)
-def cancel(request, tid):
- try:
- queue = django_rq.get_queue("low")
- job = queue.fetch_job("auto_annotation.create/{}".format(tid))
- if job is None or job.is_finished or job.is_failed:
- raise Exception("Task is not being annotated currently")
- elif "cancel" not in job.meta:
- job.meta["cancel"] = True
- job.save()
-
- except Exception as ex:
- try:
- slogger.task[tid].exception("cannot cancel auto annotation for task #{}".format(tid), exc_info=True)
- except Exception as logger_ex:
- slogger.glob.exception("exception was occured during cancel auto annotation request for task {}: {}".format(tid, str(logger_ex)), exc_info=True)
- return HttpResponseBadRequest(str(ex))
-
- return HttpResponse()
diff --git a/cvat/apps/dashboard/static/dashboard/js/dashboard.js b/cvat/apps/dashboard/static/dashboard/js/dashboard.js
index a7a62f33..d12668c0 100644
--- a/cvat/apps/dashboard/static/dashboard/js/dashboard.js
+++ b/cvat/apps/dashboard/static/dashboard/js/dashboard.js
@@ -7,7 +7,7 @@
/* global
AnnotationParser:false
Config:false
- confirm:false
+ userConfirm:false
ConstIdGenerator:false
createExportContainer:false
dumpAnnotationRequest:false
@@ -97,7 +97,7 @@ function updateTaskRequest(labels) {
function removeTaskRequest() {
- confirm("The action can not be undone. Are you sure?", confirmCallback);
+ userConfirm("The action can not be undone. Are you sure?", confirmCallback);
function confirmCallback() {
$.ajax ({
@@ -266,7 +266,7 @@ window.cvat.dashboard.uiCallbacks.push(function(elements) {
uploadButton.on("click", function() {
window.cvat.dashboard.taskID = taskID;
window.cvat.dashboard.taskName = taskName;
- confirm("The current annotation will be lost. Are you sure?", uploadAnnotationRequest);
+ userConfirm("The current annotation will be lost. Are you sure?", uploadAnnotationRequest);
});
updateButton.on("click", function() {
@@ -400,9 +400,11 @@ function setupTaskCreator() {
cancelBrowseServer.on("click", () => shareFileSelector.addClass("hidden"));
submitBrowseServer.on("click", function() {
- files = shareBrowseTree.jstree(true).get_selected();
- cancelBrowseServer.click();
- updateSelectedFiles();
+ if (!createModal.hasClass("hidden")) {
+ files = shareBrowseTree.jstree(true).get_selected();
+ cancelBrowseServer.click();
+ updateSelectedFiles();
+ }
});
flipImagesBox.on("click", (e) => {
diff --git a/cvat/apps/dashboard/static/dashboard/stylesheet.css b/cvat/apps/dashboard/static/dashboard/stylesheet.css
index a5a047fc..4ae41b89 100644
--- a/cvat/apps/dashboard/static/dashboard/stylesheet.css
+++ b/cvat/apps/dashboard/static/dashboard/stylesheet.css
@@ -65,7 +65,7 @@
padding: 10px;
border: 1px solid grey;
float: left;
- width: 70%;
+ width: 76%;
background: #f1f1f1;
outline: 0;
}
@@ -75,7 +75,6 @@
width: 20%;
padding: 10px;
background: #0b7dda;
- color: white;
border: 1px solid grey;
border-left: none;
cursor: pointer;
@@ -90,10 +89,6 @@
background: #0b7fff;
}
-#dashboardCreateTaskButton {
- font-size: 2em;
-}
-
#dashboardCreateContent {
width: 500px;
display: table;
diff --git a/cvat/apps/dashboard/templates/dashboard/dashboard.html b/cvat/apps/dashboard/templates/dashboard/dashboard.html
index 253fa4e3..18ddd088 100644
--- a/cvat/apps/dashboard/templates/dashboard/dashboard.html
+++ b/cvat/apps/dashboard/templates/dashboard/dashboard.html
@@ -15,6 +15,9 @@
{{ block.super }}
+ {% for css_file in css_3rdparty %}
+
+ {% endfor %}
{% endblock %}
{% block head_js_3rdparty %}
@@ -41,19 +44,19 @@
{% block content %}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
{% autopaginate data %}
diff --git a/cvat/apps/dashboard/views.py b/cvat/apps/dashboard/views.py
index 0341cc4d..1b2f81dc 100644
--- a/cvat/apps/dashboard/views.py
+++ b/cvat/apps/dashboard/views.py
@@ -10,7 +10,7 @@ from django.conf import settings
from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel, Job as JobModel
-from cvat.settings.base import JS_3RDPARTY
+from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
import os
@@ -77,4 +77,5 @@ def DashboardView(request):
'base_url': "{0}://{1}/".format(request.scheme, request.get_host()),
'share_path': os.getenv('CVAT_SHARE_URL', default=r'${cvat_root}/share'),
'js_3rdparty': JS_3RDPARTY.get('dashboard', []),
+ 'css_3rdparty': CSS_3RDPARTY.get('dashboard', []),
})
diff --git a/cvat/apps/engine/static/engine/base.css b/cvat/apps/engine/static/engine/base.css
index b694d06e..3d33c454 100644
--- a/cvat/apps/engine/static/engine/base.css
+++ b/cvat/apps/engine/static/engine/base.css
@@ -85,6 +85,36 @@ html {
width: 80%;
}
+.close {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ width: 32px;
+ height: 32px;
+ opacity: 0.3;
+ }
+
+ .close:hover {
+ opacity: 1;
+ }
+
+ .close:before, .close:after {
+ position: absolute;
+ left: 15px;
+ content: ' ';
+ height: 22px;
+ width: 2px;
+ background-color: #333;
+ }
+
+ .close:before {
+ transform: rotate(45deg);
+ }
+
+ .close:after {
+ transform: rotate(-45deg);
+ }
+
.tab {
overflow: hidden;
border: 1px solid black;
diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js
index 1aaf1583..cccf0141 100644
--- a/cvat/apps/engine/static/engine/js/annotationUI.js
+++ b/cvat/apps/engine/static/engine/js/annotationUI.js
@@ -12,7 +12,7 @@
AAMView:false
AnnotationParser:false
Config:false
- confirm:false
+ userConfirm:false
CoordinateTranslator:false
dumpAnnotationRequest:false
HistoryController:false
@@ -604,7 +604,7 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player
$('#uploadAnnotationButton').on('click', () => {
hide();
- confirm('Current annotation will be removed from the client. Continue?',
+ userConfirm('Current annotation will be removed from the client. Continue?',
() => {
uploadAnnotation(shapeCollectionModel, historyModel, annotationParser, $('#uploadAnnotationButton'));
}
@@ -614,7 +614,7 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player
$('#removeAnnotationButton').on('click', () => {
if (!window.cvat.mode) {
hide();
- confirm('Do you want to remove all annotations? The action cannot be undone!',
+ userConfirm('Do you want to remove all annotations? The action cannot be undone!',
() => {
historyModel.empty();
shapeCollectionModel.empty();
diff --git a/cvat/apps/engine/static/engine/js/base.js b/cvat/apps/engine/static/engine/js/base.js
index 6bc158e1..1674b358 100644
--- a/cvat/apps/engine/static/engine/js/base.js
+++ b/cvat/apps/engine/static/engine/js/base.js
@@ -5,7 +5,7 @@
*/
/* exported
- confirm
+ userConfirm
createExportContainer
dumpAnnotationRequest
ExportType
@@ -25,7 +25,7 @@ Math.clamp = function(x, min, max) {
};
-function confirm(message, onagree, ondisagree) {
+function userConfirm(message, onagree, ondisagree) {
let template = $('#confirmTemplate');
let confirmWindow = $(template.html()).css('display', 'block');
diff --git a/cvat/apps/engine/static/engine/js/shapeBuffer.js b/cvat/apps/engine/static/engine/js/shapeBuffer.js
index ea0f491d..15ab15da 100644
--- a/cvat/apps/engine/static/engine/js/shapeBuffer.js
+++ b/cvat/apps/engine/static/engine/js/shapeBuffer.js
@@ -8,7 +8,7 @@
/* global
AREA_TRESHOLD:false
- confirm:false
+ userConfirm:false
Listener:false
Logger:false
Mousetrap:false
@@ -313,7 +313,7 @@ class ShapeBufferController {
message += 'Are you sure?';
propagateDialogShowed = true;
- confirm(message, () => {
+ userConfirm(message, () => {
this._model.propagateToFrames();
propagateDialogShowed = false;
}, () => propagateDialogShowed = false);
diff --git a/cvat/apps/engine/static/engine/stylesheet.css b/cvat/apps/engine/static/engine/stylesheet.css
index 1a5484a8..570aad45 100644
--- a/cvat/apps/engine/static/engine/stylesheet.css
+++ b/cvat/apps/engine/static/engine/stylesheet.css
@@ -29,40 +29,6 @@
stroke: grey !important;
}
-
-
-.close {
- position: absolute;
- right: 5px;
- top: 5px;
- width: 32px;
- height: 32px;
- opacity: 0.3;
- }
-
- .close:hover {
- opacity: 1;
- }
-
- .close:before, .close:after {
- position: absolute;
- left: 15px;
- content: ' ';
- height: 22px;
- width: 2px;
- background-color: #333;
- }
-
- .close:before {
- transform: rotate(45deg);
- }
-
- .close:after {
- transform: rotate(-45deg);
- }
-
-
-
.graphicButton {
opacity: 0.6;
width: 20px;
diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html
index 3fd53602..0b9ab7c3 100644
--- a/cvat/apps/engine/templates/engine/annotation.html
+++ b/cvat/apps/engine/templates/engine/annotation.html
@@ -9,6 +9,9 @@
{% block head_css %}
{{ block.super }}
+ {% for css_file in css_3rdparty %}
+
+ {% endfor %}
{% endblock %}
diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py
index 190885c7..6941c9bb 100644
--- a/cvat/apps/engine/views.py
+++ b/cvat/apps/engine/views.py
@@ -15,7 +15,7 @@ from django.views.decorators.gzip import gzip_page
from sendfile import sendfile
from . import annotation, task, models
-from cvat.settings.base import JS_3RDPARTY
+from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
from cvat.apps.authentication.decorators import login_required
from requests.exceptions import RequestException
import logging
@@ -38,6 +38,7 @@ def dispatch_request(request):
"""An entry point to dispatch legacy requests"""
if request.method == 'GET' and 'id' in request.GET:
return render(request, 'engine/annotation.html', {
+ 'css_3rdparty': CSS_3RDPARTY.get('engine', []),
'js_3rdparty': JS_3RDPARTY.get('engine', []),
'status_list': [str(i) for i in StatusChoice]
})
diff --git a/cvat/apps/tf_annotation/static/tf_annotation/js/tf_annotation.js b/cvat/apps/tf_annotation/static/tf_annotation/js/tf_annotation.js
index 420301a0..84ad318d 100644
--- a/cvat/apps/tf_annotation/static/tf_annotation/js/tf_annotation.js
+++ b/cvat/apps/tf_annotation/static/tf_annotation/js/tf_annotation.js
@@ -5,7 +5,7 @@
*/
/* global
- confirm:false
+ userConfirm:false
showMessage:false
*/
@@ -85,10 +85,10 @@ function onTFAnnotationClick() {
window.cvat.dashboard.taskName = taskName;
if (button.hasClass("tfAnnotationProcess")) {
- confirm('The process will be canceled. Continue?', CancelTFAnnotationRequest.bind(button));
+ userConfirm('The process will be canceled. Continue?', CancelTFAnnotationRequest.bind(button));
}
else {
- confirm('The current annotation will be lost. Are you sure?', RunTFAnnotationRequest.bind(button));
+ userConfirm('The current annotation will be lost. Are you sure?', RunTFAnnotationRequest.bind(button));
}
}
diff --git a/cvat/settings/base.py b/cvat/settings/base.py
index 12df3625..aba1d56e 100644
--- a/cvat/settings/base.py
+++ b/cvat/settings/base.py
@@ -78,6 +78,7 @@ except Exception:
# Application definition
JS_3RDPARTY = {}
+CSS_3RDPARTY = {}
INSTALLED_APPS = [
'django.contrib.admin',
@@ -308,6 +309,8 @@ DATA_ROOT = os.path.join(BASE_DIR, 'data')
os.makedirs(DATA_ROOT, exist_ok=True)
SHARE_ROOT = os.path.join(BASE_DIR, 'share')
os.makedirs(SHARE_ROOT, exist_ok=True)
+MODELS_ROOT=os.path.join(BASE_DIR, 'models')
+os.makedirs(MODELS_ROOT, exist_ok=True)
DATA_UPLOAD_MAX_MEMORY_SIZE = 100 * 1024 * 1024 # 100 MB
DATA_UPLOAD_MAX_NUMBER_FIELDS = None # this django check disabled
diff --git a/docker-compose.yml b/docker-compose.yml
index 7e55bd3f..2f521520 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -55,9 +55,11 @@ services:
- cvat_data:/home/django/data
- cvat_keys:/home/django/keys
- cvat_logs:/home/django/logs
+ - cvat_models:/home/django/models
volumes:
cvat_db:
cvat_data:
cvat_keys:
cvat_logs:
+ cvat_models: