diff --git a/cvat/apps/dashboard/static/dashboard/js/dashboard.js b/cvat/apps/dashboard/static/dashboard/js/dashboard.js index 091ed668..d3815969 100644 --- a/cvat/apps/dashboard/static/dashboard/js/dashboard.js +++ b/cvat/apps/dashboard/static/dashboard/js/dashboard.js @@ -524,14 +524,15 @@ function uploadAnnotationRequest() { $.ajax({ url: '/get/task/' + window.cvat.dashboard.taskID, success: function(data) { - let annotationParser = new AnnotationParser({ + let annotationParser = new AnnotationParser( + { start: 0, stop: data.size, image_meta_data: data.image_meta_data, flipped: data.flipped }, new LabelsInfo(data.spec), - new ConstIdGenerator(-1), + new ConstIdGenerator(-1) ); let asyncParse = function() { @@ -559,7 +560,7 @@ function uploadAnnotationRequest() { overlay.remove(); }, }); - } + }; let asyncSaveChunk = function(start) { const CHUNK_SIZE = 100000; diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index b5dc93a0..79f4f126 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -472,6 +472,7 @@ class _Annotation: group_id=box.group_id, boxes=[box0, box1], attributes=box.attributes, + client_id=box.client_id, ) paths.append(path) @@ -491,6 +492,7 @@ class _Annotation: stop_frame=shape.frame + 1, group_id=shape.group_id, shapes=[shape0, shape1], + client_id=shape.client_id, attributes=shape.attributes, ) paths.append(path) @@ -2080,10 +2082,14 @@ class _AnnotationForTask(_Annotation): im_w = im_meta_data['original_size'][0]['width'] im_h = im_meta_data['original_size'][0]['height'] + counter = 0 for shape_type in ["boxes", "polygons", "polylines", "points"]: path_list = paths[shape_type] for path in path_list: + path_id = path.client_id if path.client_id != -1 else counter + counter += 1 dump_dict = OrderedDict([ + ("id", str(path_id)), ("label", path.label.name), ]) if path.group_id: diff --git a/cvat/apps/engine/static/engine/js/player.js b/cvat/apps/engine/static/engine/js/player.js index a3b57c3e..2f445647 100644 --- a/cvat/apps/engine/static/engine/js/player.js +++ b/cvat/apps/engine/static/engine/js/player.js @@ -798,9 +798,14 @@ class PlayerView { this._playerUI.on('contextmenu.playerContextMenu', (e) => { if (!window.cvat.mode) { $('.custom-menu').hide(100); - this._contextMenuUI.finish().show(100).offset({ - top: e.pageY - 10, - left: e.pageX - 10, + this._contextMenuUI.finish().show(100); + let x = Math.min(e.pageX, this._playerUI[0].offsetWidth - + this._contextMenuUI[0].scrollWidth); + let y = Math.min(e.pageY, this._playerUI[0].offsetHeight - + this._contextMenuUI[0].scrollHeight); + this._contextMenuUI.offset({ + left: x, + top: y, }); e.preventDefault(); } diff --git a/cvat/apps/engine/static/engine/js/shapeBuffer.js b/cvat/apps/engine/static/engine/js/shapeBuffer.js index 449456ea..6c554b94 100644 --- a/cvat/apps/engine/static/engine/js/shapeBuffer.js +++ b/cvat/apps/engine/static/engine/js/shapeBuffer.js @@ -50,7 +50,7 @@ class ShapeBufferModel extends Listener { } } - _makeObject(box, points, trackedObj) { + _makeObject(box, points, isTracked) { if (!this._shape.type) { return null; } @@ -75,7 +75,7 @@ class ShapeBufferModel extends Listener { box.frame = window.cvat.player.frames.current; box.z_order = this._collection.zOrder(box.frame).max; - if (trackedObj) { + if (isTracked) { object.shapes = []; object.shapes.push(Object.assign(box, { outside: false, @@ -180,18 +180,49 @@ class ShapeBufferModel extends Listener { let startFrame = window.cvat.player.frames.start; let originalImageSize = imageSizes[object.frame - startFrame] || imageSizes[0]; + // Getting normalized coordinates [0..1] + let normalized = {}; + if (this._shape.type === 'box') { + normalized.xtl = object.xtl / originalImageSize.width; + normalized.ytl = object.ytl / originalImageSize.height; + normalized.xbr = object.xbr / originalImageSize.width; + normalized.ybr = object.ybr / originalImageSize.height; + } + else { + normalized.points = []; + for (let point of PolyShapeModel.convertStringToNumberArray(object.points)) { + normalized.points.push({ + x: point.x / originalImageSize.width, + y: point.y / originalImageSize.height, + }); + } + } + let addedObjects = []; while (numOfFrames > 0 && (object.frame + 1 <= window.cvat.player.frames.stop)) { object.frame ++; numOfFrames --; - // Propagate only for frames with same size + object.z_order = this._collection.zOrder(object.frame).max; let imageSize = imageSizes[object.frame - startFrame] || imageSizes[0]; - if ((imageSize.width != originalImageSize.width) || (imageSize.height != originalImageSize.height)) { - continue; + let position = {}; + if (this._shape.type === 'box') { + position.xtl = normalized.xtl * imageSize.width; + position.ytl = normalized.ytl * imageSize.height; + position.xbr = normalized.xbr * imageSize.width; + position.ybr = normalized.ybr * imageSize.height; } - - object.z_order = this._collection.zOrder(object.frame).max; + else { + position.points = []; + for (let point of normalized.points) { + position.points.push({ + x: point.x * imageSize.width, + y: point.y * imageSize.height, + }); + } + position.points = PolyShapeModel.convertNumberArrayToString(position.points); + } + Object.assign(object, position); this._collection.add(object, `annotation_${this._shape.type}`); addedObjects.push(this._collection.shapes.slice(-1)[0]); } @@ -251,8 +282,24 @@ class ShapeBufferController { let propagateHandler = Logger.shortkeyLogDecorator(function() { if (!propagateDialogShowed) { if (this._model.copyToBuffer()) { + let curFrame = window.cvat.player.frames.current; + let startFrame = window.cvat.player.frames.start; + let endFrame = Math.min(window.cvat.player.frames.stop, curFrame + this._model.propagateFrames); + let imageSizes = window.cvat.job.images.original_size; + + let message = `Propagate up to ${endFrame} frame. `; + let refSize = imageSizes[curFrame - startFrame] || imageSizes[0]; + for (let _frame = curFrame + 1; _frame <= endFrame; _frame ++) { + let size = imageSizes[_frame - startFrame] || imageSizes[0]; + if ((size.width != refSize.width) || (size.height != refSize.height) ) { + message += 'Some covered frames have another resolution. Shapes in them can differ from reference. '; + break; + } + } + message += 'Are you sure?'; + propagateDialogShowed = true; - confirm(`Propagate to ${this._model.propagateFrames} frames. Are you sure?`, () => { + confirm(message, () => { this._model.propagateToFrames(); propagateDialogShowed = false; }, () => propagateDialogShowed = false); @@ -402,7 +449,7 @@ class ShapeBufferView { let area = w * h; let type = this._shape.type; - if (area > AREA_TRESHOLD || type === 'points' || type === 'polyline' && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) { + if (area >= AREA_TRESHOLD || type === 'points' || type === 'polyline' && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) { this._controller.pasteToFrame(e, null, actualPoints); } else { diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index b3c1d8bd..d4d68036 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -1423,6 +1423,7 @@ class ShapeView extends Listener { this._pointContextMenu = $('#pointContextMenu'); this._rightBorderFrame = $('#playerFrame')[0].offsetWidth; + this._bottomBorderFrame = $('#playerFrame')[0].offsetHeight; shapeModel.subscribe(this); } @@ -1541,9 +1542,12 @@ class ShapeView extends Listener { dragPolyItem.addClass('hidden'); } - this._shapeContextMenu.finish().show(100).offset({ - top: e.pageY - 10, - left: e.pageX - 10, + this._shapeContextMenu.finish().show(100); + let x = Math.min(e.pageX, this._rightBorderFrame - this._shapeContextMenu[0].scrollWidth); + let y = Math.min(e.pageY, this._bottomBorderFrame - this._shapeContextMenu[0].scrollHeight); + this._shapeContextMenu.offset({ + left: x, + top: y, }); e.preventDefault(); @@ -2437,7 +2441,7 @@ class ShapeView extends Listener { if (this._uis.text && this._uis.text.node.parentElement) { let revscale = 1 / scale; let shapeBBox = this._uis.shape.node.getBBox(); - let textBBox = this._uis.text.node.getBBox() + let textBBox = this._uis.text.node.getBBox(); let x = shapeBBox.x + shapeBBox.width + TEXT_MARGIN * revscale; let y = shapeBBox.y; @@ -2826,9 +2830,12 @@ class PolyShapeView extends ShapeView { this._pointContextMenu.attr('point_idx', point.index()); this._pointContextMenu.attr('dom_point_id', point.attr('id')); - this._pointContextMenu.finish().show(100).offset({ - top: e.pageY - 20, - left: e.pageX - 20, + this._pointContextMenu.finish().show(100); + let x = Math.min(e.pageX, this._rightBorderFrame - this._pointContextMenu[0].scrollWidth); + let y = Math.min(e.pageY, this._bottomBorderFrame - this._pointContextMenu[0].scrollHeight); + this._pointContextMenu.offset({ + left: x, + top: y, }); e.preventDefault(); @@ -3037,14 +3044,15 @@ class PointsView extends PolyShapeView { return; } - this._uis.points = this._scenes.svg.group().fill(this._appearance.fill || this._appearance.colors.shape) + this._uis.points = this._scenes.svg.group() + .fill(this._appearance.fill || this._appearance.colors.shape) .on('click', () => { this._positionateMenus(); this._controller.click(); - }).attr({ - 'z_order': position.z_order }).addClass('pointTempGroup'); + this._uis.points.node.setAttribute('z_order', position.z_order); + let points = PolyShapeModel.convertStringToNumberArray(position.points); for (let point of points) { let radius = POINT_RADIUS * 2 / window.cvat.player.geometry.scale; @@ -3079,7 +3087,7 @@ class PointsView extends PolyShapeView { let interpolation = this._controller.interpolate(window.cvat.player.frames.current); if (interpolation.position.points) { let points = window.cvat.translate.points.actualToCanvas(interpolation.position.points); - this._drawPointMarkers(Object.assign(interpolation.position.points, {points: points})); + this._drawPointMarkers(Object.assign(interpolation.position, {points: points})); } } } diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 7a735ae9..786dfb03 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -26,6 +26,7 @@ from django.db import transaction from ffmpy import FFmpeg from pyunpack import Archive from distutils.dir_util import copy_tree +from collections import OrderedDict from . import models from .log import slogger @@ -154,7 +155,7 @@ def get(tid): """Get the task as dictionary of attributes""" db_task = models.Task.objects.get(pk=tid) if db_task: - db_labels = db_task.label_set.prefetch_related('attributespec_set').all() + db_labels = db_task.label_set.prefetch_related('attributespec_set').order_by('-pk').all() im_meta_data = get_image_meta_cache(db_task) attributes = {} for db_label in db_labels: @@ -174,7 +175,7 @@ def get(tid): response = { "status": db_task.status, "spec": { - "labels": { db_label.id:db_label.name for db_label in db_labels }, + "labels": OrderedDict((db_label.id, db_label.name) for db_label in db_labels), "attributes": attributes }, "size": db_task.size, @@ -228,7 +229,7 @@ def get_job(jid): if db_task.mode == 'annotation': im_meta_data['original_size'] = im_meta_data['original_size'][db_segment.start_frame:db_segment.stop_frame + 1] - db_labels = db_task.label_set.prefetch_related('attributespec_set').all() + db_labels = db_task.label_set.prefetch_related('attributespec_set').order_by('-pk').all() attributes = {} for db_label in db_labels: attributes[db_label.id] = {} @@ -237,7 +238,7 @@ def get_job(jid): response = { "status": db_job.status, - "labels": { db_label.id:db_label.name for db_label in db_labels }, + "labels": OrderedDict((db_label.id, db_label.name) for db_label in db_labels), "stop": db_segment.stop_frame, "taskid": db_task.id, "slug": db_task.name, @@ -375,7 +376,7 @@ def _get_frame_path(frame, base_dir): return path def _parse_labels(labels): - parsed_labels = {} + parsed_labels = OrderedDict() last_label = "" for token in shlex.split(labels): diff --git a/tests/eslintrc.conf.js b/tests/eslintrc.conf.js index 9e27f60c..fe822dee 100644 --- a/tests/eslintrc.conf.js +++ b/tests/eslintrc.conf.js @@ -52,6 +52,8 @@ module.exports = { 'createExportContainer': true, 'ExportType': true, 'getExportTargetContainer': true, + 'IncrementIdGenerator': true, + 'ConstIdGenerator': true, // from shapeCollection.js 'ShapeCollectionModel': true, 'ShapeCollectionController': true,