From e225c380920dc4b31f876934bc4b041f07bf60c3 Mon Sep 17 00:00:00 2001
From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Date: Fri, 8 Feb 2019 15:59:10 +0300
Subject: [PATCH 1/4] Re-Identification application (#299)
---
.remarkrc.js | 16 ++
CHANGELOG.md | 1 +
Dockerfile | 15 +-
.../engine/static/engine/js/annotationUI.js | 9 +-
cvat/apps/reid/README.md | 22 ++
cvat/apps/reid/__init__.py | 9 +
cvat/apps/reid/apps.py | 8 +
cvat/apps/reid/reid.py | 226 ++++++++++++++++++
cvat/apps/reid/static/reid/js/enginePlugin.js | 170 +++++++++++++
cvat/apps/reid/urls.py | 12 +
cvat/apps/reid/views.py | 96 ++++++++
cvat/settings/base.py | 3 +
cvat/urls.py | 3 +
13 files changed, 584 insertions(+), 6 deletions(-)
create mode 100644 .remarkrc.js
create mode 100644 cvat/apps/reid/README.md
create mode 100644 cvat/apps/reid/__init__.py
create mode 100644 cvat/apps/reid/apps.py
create mode 100644 cvat/apps/reid/reid.py
create mode 100644 cvat/apps/reid/static/reid/js/enginePlugin.js
create mode 100644 cvat/apps/reid/urls.py
create mode 100644 cvat/apps/reid/views.py
diff --git a/.remarkrc.js b/.remarkrc.js
new file mode 100644
index 000000000000..23862ca44441
--- /dev/null
+++ b/.remarkrc.js
@@ -0,0 +1,16 @@
+exports.settings = {bullet: '*', paddedTable: false}
+
+exports.plugins = [
+ require('remark-preset-lint-recommended'),
+ require('remark-preset-lint-consistent'),
+ require('remark-validate-links'),
+ [require("remark-lint-no-dead-urls"), { skipOffline: true }],
+ [require("remark-lint-maximum-line-length"), 120],
+ [require("remark-lint-maximum-heading-length"), 120],
+ [require("remark-lint-list-item-indent"), "tab-size"],
+ [require("remark-lint-list-item-spacing"), false],
+ [require("remark-lint-strong-marker"), "*"],
+ [require("remark-lint-emphasis-marker"), "_"],
+ [require("remark-lint-unordered-list-marker-style"), "-"],
+ [require("remark-lint-ordered-list-marker-style"), "."],
+]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6097e3648355..d2bf4c9ba669 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically.
- Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305)
+- The ReID application for automatic bounding box merging has been added (#299)
### Changed
- Propagation setup has been moved from settings to bottom player panel
diff --git a/Dockerfile b/Dockerfile
index 97f84c3f0f43..48dbe177843d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -103,14 +103,11 @@ RUN if [ "$WITH_TESTS" = "yes" ]; then \
COPY cvat/requirements/ /tmp/requirements/
COPY supervisord.conf mod_wsgi.conf wait-for-it.sh manage.py ${HOME}/
RUN pip3 install --no-cache-dir -r /tmp/requirements/${DJANGO_CONFIGURATION}.txt
-COPY cvat/ ${HOME}/cvat
-
-COPY ssh ${HOME}/.ssh
# Install git application dependencies
RUN apt-get update && \
apt-get install -y ssh netcat-openbsd git curl zip && \
- curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
+ wget -qO /dev/stdout https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash && \
apt-get install -y git-lfs && \
git lfs install && \
rm -rf /var/lib/apt/lists/* && \
@@ -120,6 +117,16 @@ RUN apt-get update && \
echo export "GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ProxyCommand='nc -X 5 -x ${socks_proxy} %h %p'\"" >> ${HOME}/.bashrc; \
fi
+# Download model for re-identification app
+ENV REID_MODEL_DIR=${HOME}/reid
+RUN if [ "$OPENVINO_TOOLKIT" = "yes" ]; then \
+ mkdir ${HOME}/reid && \
+ wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.xml -O reid/reid.xml && \
+ wget https://download.01.org/openvinotoolkit/2018_R5/open_model_zoo/person-reidentification-retail-0079/FP32/person-reidentification-retail-0079.bin -O reid/reid.bin; \
+ fi
+
+COPY ssh ${HOME}/.ssh
+COPY cvat/ ${HOME}/cvat
COPY tests ${HOME}/tests
RUN patch -p1 < ${HOME}/cvat/apps/engine/static/engine/js/3rdparty.patch
RUN chown -R ${USER}:${USER} .
diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js
index c351f4c5e614..58b306c1e2d4 100644
--- a/cvat/apps/engine/static/engine/js/annotationUI.js
+++ b/cvat/apps/engine/static/engine/js/annotationUI.js
@@ -159,7 +159,12 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) {
window.cvat.data = {
get: () => shapeCollectionModel.exportAll(),
set: (data) => {
- shapeCollectionModel.empty();
+ for (let type in data) {
+ for (let shape of data[type]) {
+ shape.id = idGenerator.next();
+ }
+ }
+
shapeCollectionModel.import(data, false);
shapeCollectionModel.update();
},
@@ -248,7 +253,7 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) {
$(window).on('click', function(event) {
Logger.updateUserActivityTimer();
- if (event.target.classList.contains('modal')) {
+ if (event.target.classList.contains('modal') && !event.target.classList.contains('force-modal')) {
event.target.classList.add('hidden');
}
});
diff --git a/cvat/apps/reid/README.md b/cvat/apps/reid/README.md
new file mode 100644
index 000000000000..8cf29c503781
--- /dev/null
+++ b/cvat/apps/reid/README.md
@@ -0,0 +1,22 @@
+# Re-Identification Application
+
+## About the application
+
+The ReID application uses deep learning model to perform an automatic bbox merging between neighbor frames.
+You can use "Merge" and "Split" functionality to edit automatically generated annotation.
+
+## Installation
+
+This application will be installed automatically with the [OpenVINO](https://github.com/opencv/cvat/blob/develop/components/openvino/README.md) component.
+
+## Running
+
+For starting the ReID merge process:
+
+- Open an annotation job
+- Open the menu
+- Click the "Run ReID Merge" button
+- Click the "Submit" button. Also here you can experiment with values of model threshold or maximum distance.
+ - Model threshold is maximum cosine distance between objects embeddings.
+ - Maximum distance defines a maximum radius that an object can diverge between neightbor frames.
+- The process will be run. You can cancel it in the menu.
diff --git a/cvat/apps/reid/__init__.py b/cvat/apps/reid/__init__.py
new file mode 100644
index 000000000000..4a9759cc68f5
--- /dev/null
+++ b/cvat/apps/reid/__init__.py
@@ -0,0 +1,9 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+from cvat.settings.base import JS_3RDPARTY
+
+default_app_config = 'cvat.apps.reid.apps.ReidConfig'
+
+JS_3RDPARTY['engine'] = JS_3RDPARTY.get('engine', []) + ['reid/js/enginePlugin.js']
diff --git a/cvat/apps/reid/apps.py b/cvat/apps/reid/apps.py
new file mode 100644
index 000000000000..f6aa66e1a5f9
--- /dev/null
+++ b/cvat/apps/reid/apps.py
@@ -0,0 +1,8 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+from django.apps import AppConfig
+
+class ReidConfig(AppConfig):
+ name = 'cvat.apps.reid'
diff --git a/cvat/apps/reid/reid.py b/cvat/apps/reid/reid.py
new file mode 100644
index 000000000000..915359b3e11a
--- /dev/null
+++ b/cvat/apps/reid/reid.py
@@ -0,0 +1,226 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+import os
+import rq
+import cv2
+import math
+import numpy
+import fnmatch
+
+from openvino.inference_engine import IENetwork, IEPlugin
+from scipy.optimize import linear_sum_assignment
+from scipy.spatial.distance import euclidean, cosine
+
+from cvat.apps.engine.models import Job
+
+
+class ReID:
+ __threshold = None
+ __max_distance = None
+ __frame_urls = None
+ __frame_boxes = None
+ __stop_frame = None
+ __plugin = None
+ __executable_network = None
+ __input_blob_name = None
+ __output_blob_name = None
+ __input_height = None
+ __input_width = None
+
+
+ def __init__(self, jid, data):
+ self.__threshold = data["threshold"]
+ self.__max_distance = data["maxDistance"]
+ self.__frame_urls = {}
+ self.__frame_boxes = {}
+
+ db_job = Job.objects.select_related('segment__task').get(pk = jid)
+ db_segment = db_job.segment
+ db_task = db_segment.task
+
+ self.__stop_frame = db_segment.stop_frame
+
+ for root, _, filenames in os.walk(db_task.get_data_dirname()):
+ for filename in fnmatch.filter(filenames, '*.jpg'):
+ frame = int(os.path.splitext(filename)[0])
+ if frame >= db_segment.start_frame and frame <= db_segment.stop_frame:
+ self.__frame_urls[frame] = os.path.join(root, filename)
+
+ for frame in self.__frame_urls:
+ self.__frame_boxes[frame] = [box for box in data["boxes"] if box["frame"] == frame]
+
+ IE_PLUGINS_PATH = os.getenv('IE_PLUGINS_PATH', None)
+ REID_MODEL_DIR = os.getenv('REID_MODEL_DIR', None)
+
+ if not IE_PLUGINS_PATH:
+ raise Exception("Environment variable 'IE_PLUGINS_PATH' isn't defined")
+ if not REID_MODEL_DIR:
+ raise Exception("Environment variable 'REID_MODEL_DIR' isn't defined")
+
+ REID_XML = os.path.join(REID_MODEL_DIR, "reid.xml")
+ REID_BIN = os.path.join(REID_MODEL_DIR, "reid.bin")
+
+ self.__plugin = IEPlugin(device="CPU", plugin_dirs=[IE_PLUGINS_PATH])
+ network = IENetwork.from_ir(model=REID_XML, weights=REID_BIN)
+ self.__input_blob_name = next(iter(network.inputs))
+ self.__output_blob_name = next(iter(network.outputs))
+ self.__input_height, self.__input_width = network.inputs[self.__input_blob_name].shape[-2:]
+ self.__executable_network = self.__plugin.load(network=network)
+ del network
+
+
+ def __del__(self):
+ if self.__executable_network:
+ del self.__executable_network
+ self.__executable_network = None
+
+ if self.__plugin:
+ del self.__plugin
+ self.__plugin = None
+
+
+ def __boxes_are_compatible(self, cur_box, next_box):
+ cur_c_x = (cur_box["xtl"] + cur_box["xbr"]) / 2
+ cur_c_y = (cur_box["ytl"] + cur_box["ybr"]) / 2
+ next_c_x = (next_box["xtl"] + next_box["xbr"]) / 2
+ next_c_y = (next_box["ytl"] + next_box["ybr"]) / 2
+ compatible_distance = euclidean([cur_c_x, cur_c_y], [next_c_x, next_c_y]) <= self.__max_distance
+ compatible_label = cur_box["label_id"] == next_box["label_id"]
+ return compatible_distance and compatible_label and "path_id" not in next_box
+
+
+ def __compute_difference(self, image_1, image_2):
+ image_1 = cv2.resize(image_1, (self.__input_width, self.__input_height)).transpose((2,0,1))
+ image_2 = cv2.resize(image_2, (self.__input_width, self.__input_height)).transpose((2,0,1))
+
+ input_1 = {
+ self.__input_blob_name: image_1[numpy.newaxis, ...]
+ }
+
+ input_2 = {
+ self.__input_blob_name: image_2[numpy.newaxis, ...]
+ }
+
+ embedding_1 = self.__executable_network.infer(inputs = input_1)[self.__output_blob_name]
+ embedding_2 = self.__executable_network.infer(inputs = input_2)[self.__output_blob_name]
+
+ embedding_1 = embedding_1.reshape(embedding_1.size)
+ embedding_2 = embedding_2.reshape(embedding_2.size)
+
+ return cosine(embedding_1, embedding_2)
+
+
+ def __compute_difference_matrix(self, cur_boxes, next_boxes, cur_image, next_image):
+ def _int(number, upper):
+ return math.floor(numpy.clip(number, 0, upper - 1))
+
+ default_mat_value = 1000.0
+
+ matrix = numpy.full([len(cur_boxes), len(next_boxes)], default_mat_value, dtype=float)
+ for row, cur_box in enumerate(cur_boxes):
+ cur_width = cur_image.shape[1]
+ cur_height = cur_image.shape[0]
+ cur_xtl, cur_xbr, cur_ytl, cur_ybr = (
+ _int(cur_box["xtl"], cur_width), _int(cur_box["xbr"], cur_width),
+ _int(cur_box["ytl"], cur_height), _int(cur_box["ybr"], cur_height)
+ )
+
+ for col, next_box in enumerate(next_boxes):
+ next_box = next_boxes[col]
+ next_width = next_image.shape[1]
+ next_height = next_image.shape[0]
+ next_xtl, next_xbr, next_ytl, next_ybr = (
+ _int(next_box["xtl"], next_width), _int(next_box["xbr"], next_width),
+ _int(next_box["ytl"], next_height), _int(next_box["ybr"], next_height)
+ )
+
+ if not self.__boxes_are_compatible(cur_box, next_box):
+ continue
+
+ crop_1 = cur_image[cur_ytl:cur_ybr, cur_xtl:cur_xbr]
+ crop_2 = next_image[next_ytl:next_ybr, next_xtl:next_xbr]
+ matrix[row][col] = self.__compute_difference(crop_1, crop_2)
+
+ return matrix
+
+
+ def __apply_matching(self):
+ frames = sorted(list(self.__frame_boxes.keys()))
+ job = rq.get_current_job()
+ box_paths = {}
+
+ for idx, (cur_frame, next_frame) in enumerate(list(zip(frames[:-1], frames[1:]))):
+ job.refresh()
+ if "cancel" in job.meta:
+ return None
+
+ job.meta["progress"] = idx * 100.0 / len(frames)
+ job.save_meta()
+
+ cur_boxes = self.__frame_boxes[cur_frame]
+ next_boxes = self.__frame_boxes[next_frame]
+
+ for box in cur_boxes:
+ if "path_id" not in box:
+ path_id = len(box_paths)
+ box_paths[path_id] = [box]
+ box["path_id"] = path_id
+
+ if not (len(cur_boxes) and len(next_boxes)):
+ continue
+
+ cur_image = cv2.imread(self.__frame_urls[cur_frame], cv2.IMREAD_COLOR)
+ next_image = cv2.imread(self.__frame_urls[next_frame], cv2.IMREAD_COLOR)
+ difference_matrix = self.__compute_difference_matrix(cur_boxes, next_boxes, cur_image, next_image)
+ cur_idxs, next_idxs = linear_sum_assignment(difference_matrix)
+ for idx, cur_idx in enumerate(cur_idxs):
+ if (difference_matrix[cur_idx][next_idxs[idx]]) <= self.__threshold:
+ cur_box = cur_boxes[cur_idx]
+ next_box = next_boxes[next_idxs[idx]]
+ next_box["path_id"] = cur_box["path_id"]
+ box_paths[cur_box["path_id"]].append(next_box)
+
+ for box in self.__frame_boxes[frames[-1]]:
+ if "path_id" not in box:
+ path_id = len(box_paths)
+ box["path_id"] = path_id
+ box_paths[path_id] = [box]
+
+ return box_paths
+
+
+ def run(self):
+ box_paths = self.__apply_matching()
+ output = []
+
+ # ReID process has been canceled
+ if box_paths is None:
+ return
+
+ for path_id in box_paths:
+ output.append({
+ "label_id": box_paths[path_id][0]["label_id"],
+ "group_id": 0,
+ "attributes": [],
+ "frame": box_paths[path_id][0]["frame"],
+ "shapes": box_paths[path_id]
+ })
+
+ for box in output[-1]["shapes"]:
+ del box["id"]
+ del box["path_id"]
+ del box["group_id"]
+ del box["label_id"]
+ box["outside"] = False
+ box["attributes"] = []
+
+ for path in output:
+ if path["shapes"][-1]["frame"] != self.__stop_frame:
+ copy = path["shapes"][-1].copy()
+ copy["outside"] = True
+ copy["frame"] += 1
+ path["shapes"].append(copy)
+
+ return output
diff --git a/cvat/apps/reid/static/reid/js/enginePlugin.js b/cvat/apps/reid/static/reid/js/enginePlugin.js
new file mode 100644
index 000000000000..6bf14377cf5a
--- /dev/null
+++ b/cvat/apps/reid/static/reid/js/enginePlugin.js
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2018 Intel Corporation
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/* global showMessage userConfirm */
+
+
+document.addEventListener('DOMContentLoaded', () => {
+ function run(overlay, cancelButton, thresholdInput, distanceInput) {
+ const collection = window.cvat.data.get();
+ const data = {
+ threshold: +thresholdInput.prop('value'),
+ maxDistance: +distanceInput.prop('value'),
+ boxes: collection.boxes,
+ };
+
+ overlay.removeClass('hidden');
+ cancelButton.prop('disabled', true);
+ $.ajax({
+ url: `reid/start/job/${window.cvat.job.id}`,
+ type: 'POST',
+ data: JSON.stringify(data),
+ contentType: 'application/json',
+ success: () => {
+ function checkCallback() {
+ $.ajax({
+ url: `/reid/check/${window.cvat.job.id}`,
+ type: 'GET',
+ success: (jobData) => {
+ if (jobData.progress) {
+ cancelButton.text(`Cancel ReID Merge (${jobData.progress.toString().slice(0, 4)}%)`);
+ }
+
+ if (['queued', 'started'].includes(jobData.status)) {
+ setTimeout(checkCallback, 1000);
+ } else {
+ overlay.addClass('hidden');
+
+ if (jobData.status === 'finished') {
+ if (jobData.result) {
+ collection.boxes = [];
+ collection.box_paths = collection.box_paths
+ .concat(JSON.parse(jobData.result));
+ window.cvat.data.clear();
+ window.cvat.data.set(collection);
+ showMessage('ReID merge has done.');
+ } else {
+ showMessage('ReID merge been canceled.');
+ }
+ } else if (jobData.status === 'failed') {
+ const message = `ReID merge has fallen. Error: '${jobData.stderr}'`;
+ showMessage(message);
+ } else {
+ let message = `Check request returned "${jobData.status}" status.`;
+ if (jobData.stderr) {
+ message += ` Error: ${jobData.stderr}`;
+ }
+ showMessage(message);
+ }
+ }
+ },
+ error: (errorData) => {
+ overlay.addClass('hidden');
+ const message = `Can not check ReID merge. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
+ showMessage(message);
+ },
+ });
+ }
+
+ setTimeout(checkCallback, 1000);
+ },
+ error: (errorData) => {
+ overlay.addClass('hidden');
+ const message = `Can not start ReID merge. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
+ showMessage(message);
+ },
+ complete: () => {
+ cancelButton.prop('disabled', false);
+ },
+ });
+ }
+
+ function cancel(overlay, cancelButton) {
+ cancelButton.prop('disabled', true);
+ $.ajax({
+ url: `/reid/cancel/${window.cvat.job.id}`,
+ type: 'GET',
+ success: () => {
+ overlay.addClass('hidden');
+ cancelButton.text('Cancel ReID Merge (0%)');
+ },
+ error: (errorData) => {
+ const message = `Can not cancel ReID process. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
+ showMessage(message);
+ },
+ complete: () => {
+ cancelButton.prop('disabled', false);
+ }
+ });
+ }
+
+ const buttonsUI = $('#engineMenuButtons');
+ const reidWindowId = 'reidSubmitWindow';
+ const reidThresholdValueId = 'reidThresholdValue';
+ const reidDistanceValueId = 'reidDistanceValue';
+ const reidCancelMergeId = 'reidCancelMerge';
+ const reidSubmitMergeId = 'reidSubmitMerge';
+ const reidCancelButtonId = 'reidCancelReID';
+ const reidOverlay = 'reidOverlay';
+
+ $('').on('click', () => {
+ $('#annotationMenu').addClass('hidden');
+ $(`#${reidWindowId}`).removeClass('hidden');
+ }).addClass('menuButton semiBold h2').prependTo(buttonsUI);
+
+ $(`
+
+ `).appendTo('body');
+
+ $(`
+
+
+
+
+
+
+
+
+ `).appendTo('body');
+
+ $(`#${reidCancelMergeId}`).on('click', () => {
+ $(`#${reidWindowId}`).addClass('hidden');
+ });
+
+ $(`#${reidCancelButtonId}`).on('click', () => {
+ userConfirm('ReID process will be canceld. Are you sure?', () => {
+ cancel($(`#${reidOverlay}`), $(`#${reidCancelButtonId}`));
+ });
+ });
+
+ $(`#${reidSubmitMergeId}`).on('click', () => {
+ $(`#${reidWindowId}`).addClass('hidden');
+ run($(`#${reidOverlay}`), $(`#${reidCancelButtonId}`),
+ $(`#${reidThresholdValueId}`), $(`#${reidDistanceValueId}`));
+ });
+});
diff --git a/cvat/apps/reid/urls.py b/cvat/apps/reid/urls.py
new file mode 100644
index 000000000000..4decc5cf7467
--- /dev/null
+++ b/cvat/apps/reid/urls.py
@@ -0,0 +1,12 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path('start/job/', views.start),
+ path('cancel/', views.cancel),
+ path('check/', views.check),
+]
diff --git a/cvat/apps/reid/views.py b/cvat/apps/reid/views.py
new file mode 100644
index 000000000000..d0753e46d664
--- /dev/null
+++ b/cvat/apps/reid/views.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2018 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
+from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
+from cvat.apps.authentication.decorators import login_required
+from rules.contrib.views import permission_required, objectgetter
+
+from cvat.apps.engine.models import Job
+from cvat.apps.reid.reid import ReID
+
+import django_rq
+import json
+import rq
+
+
+def _create_thread(jid, data):
+ job = rq.get_current_job()
+ reid_obj = ReID(jid, data)
+ job.meta["result"] = json.dumps(reid_obj.run())
+ job.save_meta()
+
+
+@login_required
+@permission_required(perm=["engine.job.change"],
+ fn=objectgetter(Job, 'jid'), raise_exception=True)
+def start(request, jid):
+ try:
+ data = json.loads(request.body.decode('utf-8'))
+ queue = django_rq.get_queue("low")
+ job_id = "reid.create.{}".format(jid)
+ job = queue.fetch_job(job_id)
+ if job is not None and (job.is_started or job.is_queued):
+ raise Exception('ReID process has been already started')
+ queue.enqueue_call(func=_create_thread, args=(jid, data), job_id=job_id, timeout=7200)
+ job = queue.fetch_job(job_id)
+ job.meta = {}
+ job.save_meta()
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
+
+ return HttpResponse()
+
+
+@login_required
+@permission_required(perm=["engine.job.change"],
+ fn=objectgetter(Job, 'jid'), raise_exception=True)
+def check(request, jid):
+ try:
+ queue = django_rq.get_queue("low")
+ rq_id = "reid.create.{}".format(jid)
+ job = queue.fetch_job(rq_id)
+ if job is not None and "cancel" in job.meta:
+ return JsonResponse({"status": "finished"})
+ data = {}
+ if job is None:
+ data["status"] = "unknown"
+ elif job.is_queued:
+ data["status"] = "queued"
+ elif job.is_started:
+ data["status"] = "started"
+ if "progress" in job.meta:
+ data["progress"] = job.meta["progress"]
+ elif job.is_finished:
+ data["status"] = "finished"
+ data["result"] = job.meta["result"]
+ job.delete()
+ else:
+ data["status"] = "failed"
+ data["stderr"] = job.exc_info
+ job.delete()
+
+ except Exception as ex:
+ data["stderr"] = str(ex)
+ data["status"] = "unknown"
+
+ return JsonResponse(data)
+
+
+@login_required
+@permission_required(perm=["engine.job.change"],
+ fn=objectgetter(Job, 'jid'), raise_exception=True)
+def cancel(request, jid):
+ try:
+ queue = django_rq.get_queue("low")
+ rq_id = "reid.create.{}".format(jid)
+ job = queue.fetch_job(rq_id)
+ 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_meta()
+ except Exception as e:
+ return HttpResponseBadRequest(str(e))
+
+ return HttpResponse()
diff --git a/cvat/settings/base.py b/cvat/settings/base.py
index aba1d56e3a41..5c70bb710665 100644
--- a/cvat/settings/base.py
+++ b/cvat/settings/base.py
@@ -107,6 +107,9 @@ def generate_ssh_keys():
if 'yes' == os.environ.get('OPENVINO_TOOLKIT', 'no'):
INSTALLED_APPS += ['cvat.apps.auto_annotation']
+if 'yes' == os.environ.get('OPENVINO_TOOLKIT', 'no'):
+ INSTALLED_APPS += ['cvat.apps.reid']
+
if os.getenv('DJANGO_LOG_VIEWER_HOST'):
INSTALLED_APPS += ['cvat.apps.log_viewer']
diff --git a/cvat/urls.py b/cvat/urls.py
index 3680a012ff8c..a0e2769cbf93 100644
--- a/cvat/urls.py
+++ b/cvat/urls.py
@@ -40,6 +40,9 @@
if apps.is_installed('cvat.apps.git'):
urlpatterns.append(path('git/repository/', include('cvat.apps.git.urls')))
+if apps.is_installed('cvat.apps.reid'):
+ urlpatterns.append(path('reid/', include('cvat.apps.reid.urls')))
+
if apps.is_installed('cvat.apps.auto_annotation'):
urlpatterns.append(path('auto_annotation/', include('cvat.apps.auto_annotation.urls')))
From cdc34f6993c7250895bd418ef2512d7c82feb82b Mon Sep 17 00:00:00 2001
From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Date: Fri, 8 Feb 2019 16:01:57 +0300
Subject: [PATCH 2/4] Fixed 400 coda after undo/redo (#315)
---
cvat/apps/engine/static/engine/js/annotationUI.js | 2 +-
cvat/apps/engine/static/engine/js/history.js | 11 ++++++++---
cvat/apps/engine/static/engine/js/shapeBuffer.js | 6 ++++--
cvat/apps/engine/static/engine/js/shapeCollection.js | 6 ++++--
cvat/apps/engine/static/engine/js/shapeCreator.js | 3 ++-
cvat/apps/engine/static/engine/js/shapeMerger.js | 6 ++++--
cvat/apps/engine/static/engine/js/shapes.js | 8 ++++++--
7 files changed, 29 insertions(+), 13 deletions(-)
diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js
index 58b306c1e2d4..1c1471f225c0 100644
--- a/cvat/apps/engine/static/engine/js/annotationUI.js
+++ b/cvat/apps/engine/static/engine/js/annotationUI.js
@@ -224,7 +224,7 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) {
}), job);
new PlayerView(playerModel, playerController, job);
- let historyModel = new HistoryModel(playerModel);
+ let historyModel = new HistoryModel(playerModel, idGenerator);
let historyController = new HistoryController(historyModel);
new HistoryView(historyController, historyModel);
diff --git a/cvat/apps/engine/static/engine/js/history.js b/cvat/apps/engine/static/engine/js/history.js
index cca15672e1a4..3a05b6826bfd 100644
--- a/cvat/apps/engine/static/engine/js/history.js
+++ b/cvat/apps/engine/static/engine/js/history.js
@@ -14,7 +14,7 @@
"use strict";
class HistoryModel extends Listener {
- constructor(playerModel) {
+ constructor(playerModel, idGenerator) {
super('onHistoryUpdate', () => this );
this._deep = 128;
@@ -23,10 +23,15 @@ class HistoryModel extends Listener {
this._redo_stack = [];
this._locked = false;
this._player = playerModel;
+ this._idGenerator = idGenerator;
window.cvat.addAction = (name, undo, redo, frame) => this.addAction(name, undo, redo, frame);
}
+ generateId() {
+ return this._idGenerator.next();
+ }
+
undo() {
let frame = window.cvat.player.frames.current;
let undo = this._undo_stack.pop();
@@ -42,7 +47,7 @@ class HistoryModel extends Listener {
this._player.shift(undo.frame, true);
}
this._locked = true;
- undo.undo();
+ undo.undo(this);
}
catch(err) {
this.notify();
@@ -73,7 +78,7 @@ class HistoryModel extends Listener {
this._player.shift(redo.frame, true);
}
this._locked = true;
- redo.redo();
+ redo.redo(this);
}
catch(err) {
this.notify();
diff --git a/cvat/apps/engine/static/engine/js/shapeBuffer.js b/cvat/apps/engine/static/engine/js/shapeBuffer.js
index 15ab15dae905..bd793dcc8876 100644
--- a/cvat/apps/engine/static/engine/js/shapeBuffer.js
+++ b/cvat/apps/engine/static/engine/js/shapeBuffer.js
@@ -157,8 +157,9 @@ class ShapeBufferModel extends Listener {
window.cvat.addAction('Paste Object', () => {
model.removed = true;
model.unsubscribe(this._collection);
- }, () => {
+ }, (self) => {
model.subscribe(this._collection);
+ model.id = self.generateId();
model.removed = false;
}, window.cvat.player.frames.current);
// End of undo/redo code
@@ -247,8 +248,9 @@ class ShapeBufferModel extends Listener {
object.removed = true;
object.unsubscribe(this._collection);
}
- }, () => {
+ }, (self) => {
for (let object of addedObjects) {
+ object.id = self.generateId();
object.removed = false;
object.subscribe(this._collection);
}
diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js
index 7acde4bb94f5..df0cc61a7c75 100644
--- a/cvat/apps/engine/static/engine/js/shapeCollection.js
+++ b/cvat/apps/engine/static/engine/js/shapeCollection.js
@@ -814,14 +814,16 @@ class ShapeCollectionModel extends Listener {
// Undo/redo code
let newShapes = this._shapes.slice(-list.length);
let originalShape = this._activeShape;
- window.cvat.addAction('Split Object', () => {
+ window.cvat.addAction('Split Object', (self) => {
for (let shape of newShapes) {
shape.removed = true;
shape.unsubscribe(this);
}
+ originalShape.id = self.generateId();
originalShape.removed = false;
- }, () => {
+ }, (self) => {
for (let shape of newShapes) {
+ shape.id = self.generateId();
shape.removed = false;
shape.subscribe(this);
}
diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js
index a93dcc77e40a..bebb7bb17f88 100644
--- a/cvat/apps/engine/static/engine/js/shapeCreator.js
+++ b/cvat/apps/engine/static/engine/js/shapeCreator.js
@@ -70,8 +70,9 @@ class ShapeCreatorModel extends Listener {
window.cvat.addAction('Draw Object', () => {
model.removed = true;
model.unsubscribe(this._shapeCollection);
- }, () => {
+ }, (self) => {
model.subscribe(this._shapeCollection);
+ model.id = self.generateId();
model.removed = false;
}, window.cvat.player.frames.current);
// End of undo/redo code
diff --git a/cvat/apps/engine/static/engine/js/shapeMerger.js b/cvat/apps/engine/static/engine/js/shapeMerger.js
index 8223be05eaee..cd11936652ca 100644
--- a/cvat/apps/engine/static/engine/js/shapeMerger.js
+++ b/cvat/apps/engine/static/engine/js/shapeMerger.js
@@ -167,20 +167,22 @@ class ShapeMergerModel extends Listener {
let shapes = this._shapesForMerge;
// Undo/redo code
- window.cvat.addAction('Merge Objects', () => {
+ window.cvat.addAction('Merge Objects', (self) => {
model.unsubscribe(this._collectionModel);
model.removed = true;
for (let shape of shapes) {
+ shape.id = self.generateId();
shape.removed = false;
shape.subscribe(this._collectionModel);
}
this._collectionModel.update();
- }, () => {
+ }, (self) => {
for (let shape of shapes) {
shape.removed = true;
shape.unsubscribe(this._collectionModel);
}
model.subscribe(this._collectionModel);
+ model.id = self.generateId();
model.removed = false;
}, window.cvat.player.frames.current);
// End of undo/redo code
diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js
index c273640be331..85101f557776 100644
--- a/cvat/apps/engine/static/engine/js/shapes.js
+++ b/cvat/apps/engine/static/engine/js/shapes.js
@@ -476,11 +476,11 @@ class ShapeModel extends Listener {
this.removed = true;
// Undo/redo code
- window.cvat.addAction('Remove Object', () => {
+ window.cvat.addAction('Remove Object', (self) => {
+ this.id = self.generateId();
this.removed = false;
}, () => {
this.removed = true;
-
}, window.cvat.player.frames.current);
// End of undo/redo code
}
@@ -574,6 +574,10 @@ class ShapeModel extends Listener {
return this._id;
}
+ set id(value) {
+ this._id = value;
+ }
+
get frame() {
return this._frame;
}
From 19573c81b70b73b1394a55707bcc221d2b0d1025 Mon Sep 17 00:00:00 2001
From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Date: Fri, 8 Feb 2019 16:13:06 +0300
Subject: [PATCH 3/4] Keyboard shortcut for change shape type (Alt + 1,2,3,4)
(#316)
---
CHANGELOG.md | 2 ++
.../static/engine/js/shapeCollection.js | 27 +++++++++++++++++++
.../engine/static/engine/js/userConfig.js | 12 +++++++++
3 files changed, 41 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2bf4c9ba669..d577d5ab6364 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- OpenVINO auto annotation: it is possible to upload a custom model and annotate images automatically.
- Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305)
- The ReID application for automatic bounding box merging has been added (#299)
+- Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) [Alt + <, Alt + >] (#316)
+
### Changed
- Propagation setup has been moved from settings to bottom player panel
diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js
index df0cc61a7c75..14a2027bc955 100644
--- a/cvat/apps/engine/static/engine/js/shapeCollection.js
+++ b/cvat/apps/engine/static/engine/js/shapeCollection.js
@@ -965,6 +965,30 @@ class ShapeCollectionController {
}
}.bind(this));
+ let nextShapeType = Logger.shortkeyLogDecorator(function(e) {
+ if (window.cvat.mode === null) {
+ let next = $('#shapeTypeSelector option:selected').next();
+ if (!next.length) {
+ next = $('#shapeTypeSelector option').first();
+ }
+
+ next.prop('selected', true);
+ next.trigger('change');
+ }
+ }.bind(this));
+
+ let prevShapeType = Logger.shortkeyLogDecorator(function(e) {
+ if (window.cvat.mode === null) {
+ let prev = $('#shapeTypeSelector option:selected').prev();
+ if (!prev.length) {
+ prev = $('#shapeTypeSelector option').last();
+ }
+
+ prev.prop('selected', true);
+ prev.trigger('change');
+ }
+ }.bind(this));
+
let shortkeys = window.cvat.config.shortkeys;
Mousetrap.bind(shortkeys["switch_lock_property"].value, switchLockHandler.bind(this), 'keydown');
Mousetrap.bind(shortkeys["switch_all_lock_property"].value, switchAllLockHandler.bind(this), 'keydown');
@@ -977,6 +1001,9 @@ class ShapeCollectionController {
Mousetrap.bind(shortkeys["change_shape_label"].value, switchLabelHandler.bind(this), 'keydown');
Mousetrap.bind(shortkeys["delete_shape"].value, removeActiveHandler.bind(this), 'keydown');
Mousetrap.bind(shortkeys["change_shape_color"].value, changeShapeColorHandler.bind(this), 'keydown');
+ Mousetrap.bind(shortkeys['next_shape_type'].value, nextShapeType.bind(this), 'keydown');
+ Mousetrap.bind(shortkeys['prev_shape_type'].value, prevShapeType.bind(this), 'keydown');
+
if (window.cvat.job.z_order) {
Mousetrap.bind(shortkeys["inc_z"].value, incZHandler.bind(this), 'keydown');
diff --git a/cvat/apps/engine/static/engine/js/userConfig.js b/cvat/apps/engine/static/engine/js/userConfig.js
index 3e0329bd22a0..a7dec874fe6c 100644
--- a/cvat/apps/engine/static/engine/js/userConfig.js
+++ b/cvat/apps/engine/static/engine/js/userConfig.js
@@ -299,6 +299,18 @@ class Config {
view_value: 'Ctrl + Shift + R',
description: 'counter clockwise image rotation'
},
+
+ next_shape_type: {
+ value: ['alt+.'],
+ view_value: 'Alt + >',
+ description: 'switch next default shape type'
+ },
+
+ prev_shape_type: {
+ value: ['alt+,'],
+ view_value: 'Alt + <',
+ description: 'switch previous default shape type'
+ },
};
if (window.cvat && window.cvat.job && window.cvat.job.z_order) {
From df10fe19e26d5311100473a1ea633b8a44ddadda Mon Sep 17 00:00:00 2001
From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Date: Fri, 8 Feb 2019 16:29:24 +0300
Subject: [PATCH 4/4] Optional LFS. LFS name mask for an entire repository
(#314)
---
CHANGELOG.md | 4 ++-
cvat/apps/git/git.py | 32 +++++++++++++++----
cvat/apps/git/migrations/0003_gitdata_lfs.py | 18 +++++++++++
cvat/apps/git/models.py | 1 +
.../apps/git/static/git/js/dashboardPlugin.js | 6 ++++
5 files changed, 53 insertions(+), 8 deletions(-)
create mode 100644 cvat/apps/git/migrations/0003_gitdata_lfs.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d577d5ab6364..549d87e2ca22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,15 +15,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Propagation setup has been moved from settings to bottom player panel
- Additional events like "Debug Info" or "Fit Image" have been added for analitics
+- Optional using LFS for git annotation storages (#314)
### Deprecated
--
+- "Flip images" flag in the create task dialog will be removed. Rotation functionality in client part have been added instead.
### Removed
-
### Fixed
- Django 2.1.5 (security fix, https://nvd.nist.gov/vuln/detail/CVE-2019-3498)
+- Several scenarious which cause code 400 after undo/redo/save have been fixed (#315)
### Security
-
diff --git a/cvat/apps/git/git.py b/cvat/apps/git/git.py
index ab2f8d0a0410..4085041e2374 100644
--- a/cvat/apps/git/git.py
+++ b/cvat/apps/git/git.py
@@ -50,6 +50,7 @@ class Git:
__diffs_dir = None
__annotation_file = None
__sync_date = None
+ __lfs = None
def __init__(self, db_git, tid, user):
self.__db_git = db_git
@@ -66,6 +67,7 @@ def __init__(self, db_git, tid, user):
self.__branch_name = 'cvat_{}_{}'.format(tid, self.__task_name)
self.__annotation_file = os.path.join(self.__cwd, self.__path)
self.__sync_date = db_git.sync_date
+ self.__lfs = db_git.lfs
# Method parses an got URL.
@@ -256,6 +258,28 @@ def _accumulate(source, target, target_key):
if os.path.exists(self.__annotation_file):
os.remove(self.__annotation_file)
+ # Initialize LFS if need
+ if self.__lfs:
+ updated = False
+ lfs_settings = ["*.xml\tfilter=lfs diff=lfs merge=lfs -text\n", "*.zip\tfilter=lfs diff=lfs merge=lfs -text\n"]
+ if not os.path.isfile(os.path.join(self.__cwd, ".gitattributes")):
+ with open(os.path.join(self.__cwd, ".gitattributes"), "w") as gitattributes:
+ gitattributes.writelines(lfs_settings)
+ updated = True
+ else:
+ with open(os.path.join(self.__cwd, ".gitattributes"), "r+") as gitattributes:
+ lines = gitattributes.readlines()
+ for setting in lfs_settings:
+ if setting not in lines:
+ updated = True
+ lines.append(setting)
+ gitattributes.seek(0)
+ gitattributes.writelines(lines)
+ gitattributes.truncate()
+
+ if updated:
+ self.__rep.git.add(['.gitattributes'])
+
# Dump an annotation
dump(self.__tid, format, scheme, host, OrderedDict())
dump_name = Task.objects.get(pk = self.__tid).get_dump_path()
@@ -268,8 +292,6 @@ def _accumulate(source, target, target_key):
else:
raise Exception("Got unknown annotation file type")
- # Setup LFS for *.zip files
- self.__rep.git.lfs("track", self.__path)
self.__rep.git.add(self.__annotation_file)
# Merge diffs
@@ -279,12 +301,7 @@ def _accumulate(source, target, target_key):
diff = json.loads(f.read())
_accumulate(diff, summary_diff, None)
- # Commit and push
- self.__rep.index.add([
- '.gitattributes',
- ])
self.__rep.index.commit("CVAT Annotation updated by {}. Summary: {}".format(self.__user["name"], str(summary_diff)))
-
self.__rep.git.push("origin", self.__branch_name, "--force")
shutil.rmtree(self.__diffs_dir, True)
@@ -344,6 +361,7 @@ def _initial_create(tid, params):
db_git.url = git_path
db_git.path = path
db_git.task = db_task
+ db_git.lfs = params["use_lfs"].lower() == "true"
try:
_git = Git(db_git, tid, user)
diff --git a/cvat/apps/git/migrations/0003_gitdata_lfs.py b/cvat/apps/git/migrations/0003_gitdata_lfs.py
new file mode 100644
index 000000000000..84da1c7ba721
--- /dev/null
+++ b/cvat/apps/git/migrations/0003_gitdata_lfs.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.3 on 2019-02-05 17:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('git', '0002_auto_20190123_1305'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='gitdata',
+ name='lfs',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/cvat/apps/git/models.py b/cvat/apps/git/models.py
index d72c921c057e..07a6ece6d50d 100644
--- a/cvat/apps/git/models.py
+++ b/cvat/apps/git/models.py
@@ -22,3 +22,4 @@ class GitData(models.Model):
path = models.CharField(max_length=256)
sync_date = models.DateTimeField(auto_now_add=True)
status = models.CharField(max_length=20, default=GitStatusChoice.NON_SYNCED)
+ lfs = models.BooleanField(default=True)
diff --git a/cvat/apps/git/static/git/js/dashboardPlugin.js b/cvat/apps/git/static/git/js/dashboardPlugin.js
index eff27e10a213..9380ce7396e8 100644
--- a/cvat/apps/git/static/git/js/dashboardPlugin.js
+++ b/cvat/apps/git/static/git/js/dashboardPlugin.js
@@ -60,6 +60,7 @@ window.cvat.git = {
labelStatusId: "gitReposLabelStatus",
labelMessageId: "gitReposLabelMessage",
createURLInputTextId: "gitCreateURLInputText",
+ lfsCheckboxId: "gitLFSCheckbox",
updateState: () => {
let gitWindow = $(`#${window.cvat.git.reposWindowId}`);
@@ -136,6 +137,10 @@ document.addEventListener("DOMContentLoaded", () => {
`style="width: 90%", placeholder="github.com/user/repos [annotation/.zip]" ` +
`title = "Field for a repository URL and a relative path inside the repository. Default repository path is 'annotation/.zip'. There are .zip or .xml extenstions are supported."/>` +
`
+
+
+ |
+
|
`
).insertAfter($("#dashboardBugTrackerInput").parent().parent());
@@ -145,6 +150,7 @@ document.addEventListener("DOMContentLoaded", () => {
let gitPath = $(`#${window.cvat.git.createURLInputTextId}`).prop("value").replace(/\s/g, "");
if (gitPath.length) {
oData.append("git_path", gitPath);
+ oData.append("use_lfs", $(`#${window.cvat.git.lfsCheckboxId}`).prop("checked"));
}
originalCreateTaskRequest(oData, onSuccessRequest, onSuccessCreate, onError, onComplete, onUpdateStatus);
};