diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 12540742..e62493b8 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -822,7 +822,7 @@ class _AnnotationForJob(_Annotation): start_frame=db_path.frame, stop_frame= self.stop_frame, group_id=db_path.group_id, - client_id=db_path.id, + client_id=db_path.client_id, ) for db_attr in db_path.attributes: spec = self.db_attributes[db_attr.spec_id] diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 1cbd9079..041bbd00 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -/* exported callAnnotationUI translateSVGPos blurAllElements drawBoxSize copyToClipboard */ +/* exported callAnnotationUI blurAllElements drawBoxSize copyToClipboard */ "use strict"; function callAnnotationUI(jid) { @@ -40,6 +40,7 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) { // Setup some API window.cvat = { labelsInfo: new LabelsInfo(job), + translate: new CoordinateTranslator(), player: { geometry: { scale: 1, @@ -53,7 +54,8 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) { mode: null, job: { z_order: job.z_order, - id: job.jobid + id: job.jobid, + images: job.image_meta_data, }, search: { value: window.location.search, @@ -140,6 +142,8 @@ function buildAnnotationUI(job, shapeData, loadJobEvent) { let aamModel = new AAMModel(shapeCollectionModel, (xtl, xbr, ytl, ybr) => { playerModel.focus(xtl, xbr, ytl, ybr); + }, () => { + playerModel.fit(); }); let aamController = new AAMController(aamModel); new AAMView(aamModel, aamController); @@ -730,25 +734,6 @@ function saveAnnotation(shapeCollectionModel, job) { }); } -function translateSVGPos(svgCanvas, clientX, clientY) { - let pt = svgCanvas.createSVGPoint(); - pt.x = clientX; - pt.y = clientY; - pt = pt.matrixTransform(svgCanvas.getScreenCTM().inverse()); - - let pos = { - x: pt.x, - y: pt.y - }; - - if (platform.name.toLowerCase() == 'firefox') { - pos.x /= window.cvat.player.geometry.scale; - pos.y /= window.cvat.player.geometry.scale; - } - - return pos; -} - function blurAllElements() { document.activeElement.blur(); diff --git a/cvat/apps/engine/static/engine/js/attributeAnnotationMode.js b/cvat/apps/engine/static/engine/js/attributeAnnotationMode.js index e0e8d6cc..f0ae3cdb 100644 --- a/cvat/apps/engine/static/engine/js/attributeAnnotationMode.js +++ b/cvat/apps/engine/static/engine/js/attributeAnnotationMode.js @@ -10,10 +10,11 @@ const AAMUndefinedKeyword = '__undefined__'; class AAMModel extends Listener { - constructor(shapeCollection, focus) { + constructor(shapeCollection, focus, fit) { super('onAAMUpdate', () => this); this._shapeCollection = shapeCollection; this._focus = focus; + this._fit = fit; this._activeAAM = false; this._activeIdx = null; this._active = null; @@ -167,6 +168,7 @@ class AAMModel extends Listener { // Notify for remove aam UI this.notify(); + this._fit(); } } diff --git a/cvat/apps/engine/static/engine/js/coordinateTranslator.js b/cvat/apps/engine/static/engine/js/coordinateTranslator.js new file mode 100644 index 00000000..08797401 --- /dev/null +++ b/cvat/apps/engine/static/engine/js/coordinateTranslator.js @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 Intel Corporation + * + * SPDX-License-Identifier: MIT + */ + +/* exported CoordinateTranslator */ +"use strict"; + +class CoordinateTranslator { + constructor() { + this._boxTranslator = { + _playerOffset: 0, + _convert: function(box, sign) { + for (let prop of ["xtl", "ytl", "xbr", "ybr", "x", "y"]) { + if (prop in box) { + box[prop] += this._playerOffset * sign; + } + } + + return box; + }, + actualToCanvas: function(actualBox) { + let canvasBox = {}; + for (let key in actualBox) { + canvasBox[key] = actualBox[key]; + } + + return this._convert(canvasBox, 1); + }, + + canvasToActual: function(canvasBox) { + let actualBox = {}; + for (let key in canvasBox) { + actualBox[key] = canvasBox[key]; + } + + return this._convert(actualBox, -1); + }, + }; + + this._pointsTranslator = { + _playerOffset: 0, + _convert: function(points, sign) { + if (typeof(points) === 'string') { + return points.split(' ').map((coord) => coord.split(',') + .map((x) => +x + this._playerOffset * sign).join(',')).join(' '); + } + else if (typeof(points) === 'object') { + let result = []; + for (let point of points) { + result.push({ + x: point.x + this._playerOffset * sign, + y: point.y + this._playerOffset * sign, + }); + } + return result; + } + else { + throw Error('Unknown points type was found'); + } + }, + actualToCanvas: function(actualPoints) { + return this._convert(actualPoints, 1); + }, + + canvasToActual: function(canvasPoints) { + return this._convert(canvasPoints, -1); + } + }, + + this._pointTranslator = { + clientToCanvas: function(targetCanvas, clientX, clientY) { + let pt = targetCanvas.createSVGPoint(); + pt.x = clientX; + pt.y = clientY; + pt = pt.matrixTransform(targetCanvas.getScreenCTM().inverse()); + return pt; + }, + canvasToClient: function(sourceCanvas, canvasX, canvasY) { + let pt = sourceCanvas.createSVGPoint(); + pt.x = canvasX; + pt.y = canvasY; + pt = pt.matrixTransform(sourceCanvas.getScreenCTM()); + return pt; + } + }; + } + + get box() { + return this._boxTranslator; + } + + get points() { + return this._pointsTranslator; + } + + get point() { + return this._pointTranslator; + } + + set playerOffset(value) { + this._boxTranslator._playerOffset = value; + this._pointsTranslator._playerOffset = value; + } +} diff --git a/cvat/apps/engine/static/engine/js/player.js b/cvat/apps/engine/static/engine/js/player.js index bc298d51..a3b57c3e 100644 --- a/cvat/apps/engine/static/engine/js/player.js +++ b/cvat/apps/engine/static/engine/js/player.js @@ -152,8 +152,15 @@ class PlayerModel extends Listener { top: 0, width: playerSize.width, height: playerSize.height, + frameOffset: 0, }; + this._geometry.frameOffset = Math.floor(Math.max( + (playerSize.height - MIN_PLAYER_SCALE) / MIN_PLAYER_SCALE, + (playerSize.width - MIN_PLAYER_SCALE) / MIN_PLAYER_SCALE + )); + window.cvat.translate.playerOffset = this._geometry.frameOffset; + this._frameProvider.subscribe(this); } @@ -167,11 +174,7 @@ class PlayerModel extends Listener { } get geometry() { - return { - scale: this._geometry.scale, - top: this._geometry.top, - left: this._geometry.left - }; + return Object.assign({}, this._geometry); } get playing() { @@ -498,7 +501,6 @@ class PlayerController { this._moving = true; this._lastClickX = e.clientX; this._lastClickY = e.clientY; - e.preventDefault(); } } @@ -520,6 +522,7 @@ class PlayerController { let leftOffset = e.clientX - this._lastClickX; this._lastClickX = e.clientX; this._lastClickY = e.clientY; + this._model.move(topOffset, leftOffset); } } @@ -649,10 +652,19 @@ class PlayerView { this._playerGridPath = $('#playerGridPath'); this._contextMenuUI = $('#playerContextMenu'); - $('*').on('mouseup', () => this._controller.frameMouseUp()); + $('*').on('mouseup.player', () => this._controller.frameMouseUp()); + this._playerContentUI.on('mousedown', (e) => { + let pos = window.cvat.translate.point.clientToCanvas(this._playerBackgroundUI[0], e.clientX, e.clientY); + let frameWidth = window.cvat.player.geometry.frameWidth; + let frameHeight = window.cvat.player.geometry.frameHeight; + if (pos.x >= 0 && pos.y >= 0 && pos.x <= frameWidth && pos.y <= frameHeight) { + this._controller.frameMouseDown(e); + } + e.preventDefault(); + }); + this._playerUI.on('wheel', (e) => this._controller.zoom(e)); this._playerUI.on('dblclick', () => this._controller.fit()); - this._playerContentUI.on('mousedown', (e) => this._controller.frameMouseDown(e)); this._playerUI.on('mousemove', (e) => this._controller.frameMouseMove(e)); this._progressUI.on('mousedown', (e) => this._controller.progressMouseDown(e)); this._progressUI.on('mouseup', () => this._controller.progressMouseUp()); @@ -852,7 +864,7 @@ class PlayerView { this._progressUI['0'].value = frames.current - frames.start; - for (let obj of [this._playerBackgroundUI, this._playerContentUI, this._playerGridUI]) { + for (let obj of [this._playerBackgroundUI, this._playerGridUI]) { obj.css('width', image.width); obj.css('height', image.height); obj.css('top', geometry.top); @@ -860,6 +872,12 @@ class PlayerView { obj.css('transform', 'scale(' + geometry.scale + ')'); } + this._playerContentUI.css('width', image.width + geometry.frameOffset * 2); + this._playerContentUI.css('height', image.height + geometry.frameOffset * 2); + this._playerContentUI.css('top', geometry.top - geometry.frameOffset * geometry.scale); + this._playerContentUI.css('left', geometry.left - geometry.frameOffset * geometry.scale); + this._playerContentUI.css('transform', 'scale(' + geometry.scale + ')'); + this._playerGridPath.attr('stroke-width', 2 / geometry.scale); this._frameNumber.prop('value', frames.current); } diff --git a/cvat/apps/engine/static/engine/js/shapeBuffer.js b/cvat/apps/engine/static/engine/js/shapeBuffer.js index 42ef6bf5..449456ea 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(bbRect, polyPoints, trackedObj) { + _makeObject(box, points, trackedObj) { if (!this._shape.type) { return null; } @@ -71,17 +71,10 @@ class ShapeBufferModel extends Listener { object.attributes = attributes; if (this._shape.type === 'box') { - let box = {}; - - box.xtl = Math.max(bbRect.x, 0); - box.ytl = Math.max(bbRect.y, 0); - box.xbr = Math.min(bbRect.x + bbRect.width, window.cvat.player.geometry.frameWidth); - box.ybr = Math.min(bbRect.y + bbRect.height, window.cvat.player.geometry.frameHeight); box.occluded = this._shape.position.occluded; box.frame = window.cvat.player.frames.current; box.z_order = this._collection.zOrder(box.frame).max; - if (trackedObj) { object.shapes = []; object.shapes.push(Object.assign(box, { @@ -95,8 +88,7 @@ class ShapeBufferModel extends Listener { } else { let position = {}; - - position.points = polyPoints; + position.points = points; position.occluded = this._shape.position.occluded; position.frame = window.cvat.player.frames.current; position.z_order = this._collection.zOrder(position.frame).max; @@ -135,78 +127,91 @@ class ShapeBufferModel extends Listener { return false; } - pasteToFrame(bbRect, polyPoints) { - if (!this._shape.type) { - return; - } + pasteToFrame(box, polyPoints) { + let object = this._makeObject(box, polyPoints, this._shape.mode === 'interpolation'); - Logger.addEvent(Logger.EventType.pasteObject); - let object = this._makeObject(bbRect, polyPoints, this._shape.mode === 'interpolation'); - if (this._shape.type === 'box') { - this._collection.add(object, `${this._shape.mode}_${this._shape.type}`); - } - else { - this._collection.add(object, `annotation_${this._shape.type}`); - } + if (object) { + Logger.addEvent(Logger.EventType.pasteObject); + if (this._shape.type === 'box') { + this._collection.add(object, `${this._shape.mode}_${this._shape.type}`); + } + else { + this._collection.add(object, `annotation_${this._shape.type}`); + } - // Undo/redo code - let model = this._collection.shapes.slice(-1)[0]; - window.cvat.addAction('Paste Object', () => { - model.removed = true; - model.unsubscribe(this._collection); - }, () => { - model.subscribe(this._collection); - model.removed = false; - }, window.cvat.player.frames.current); - // End of undo/redo code - - this._collection.update(); + // Undo/redo code + let model = this._collection.shapes.slice(-1)[0]; + window.cvat.addAction('Paste Object', () => { + model.removed = true; + model.unsubscribe(this._collection); + }, () => { + model.subscribe(this._collection); + model.removed = false; + }, window.cvat.player.frames.current); + // End of undo/redo code + + this._collection.update(); + } } propagateToFrames() { let numOfFrames = this._propagateFrames; if (this._shape.type && Number.isInteger(numOfFrames)) { - let bbRect = null; - let polyPoints = null; + let object = null; if (this._shape.type === 'box') { - bbRect = { - x: this._shape.position.xtl, - y: this._shape.position.ytl, - height: this._shape.position.ybr - this._shape.position.ytl, - width: this._shape.position.xbr - this._shape.position.xtl, + let box = { + xtl: this._shape.position.xtl, + ytl: this._shape.position.ytl, + xbr: this._shape.position.xbr, + ybr: this._shape.position.ybr, }; + object = this._makeObject(box, null, false); } else { - polyPoints = this._shape.position.points; + object = this._makeObject(null, this._shape.position.points, false); } - let object = this._makeObject(bbRect, polyPoints, false); - Logger.addEvent(Logger.EventType.propagateObject, { - count: numOfFrames, - }); + if (object) { + Logger.addEvent(Logger.EventType.propagateObject, { + count: numOfFrames, + }); - let addedObjects = []; - while (numOfFrames > 0 && (object.frame + 1 <= window.cvat.player.frames.stop)) { - object.frame ++; - object.z_order = this._collection.zOrder(object.frame).max; - this._collection.add(object, `annotation_${this._shape.type}`); - addedObjects.push(this._collection.shapes.slice(-1)[0]); - numOfFrames --; - } + let imageSizes = window.cvat.job.images.original_size; + let startFrame = window.cvat.player.frames.start; + let originalImageSize = imageSizes[object.frame - startFrame] || imageSizes[0]; - // Undo/redo code - window.cvat.addAction('Propagate Object', () => { - for (let object of addedObjects) { - object.removed = true; - object.unsubscribe(this._collection); + let addedObjects = []; + while (numOfFrames > 0 && (object.frame + 1 <= window.cvat.player.frames.stop)) { + object.frame ++; + numOfFrames --; + + // Propagate only for frames with same size + let imageSize = imageSizes[object.frame - startFrame] || imageSizes[0]; + if ((imageSize.width != originalImageSize.width) || (imageSize.height != originalImageSize.height)) { + continue; + } + + object.z_order = this._collection.zOrder(object.frame).max; + this._collection.add(object, `annotation_${this._shape.type}`); + addedObjects.push(this._collection.shapes.slice(-1)[0]); } - }, () => { - for (let object of addedObjects) { - object.removed = false; - object.subscribe(this._collection); + + if (addedObjects.length) { + // Undo/redo code + window.cvat.addAction('Propagate Object', () => { + for (let object of addedObjects) { + object.removed = true; + object.unsubscribe(this._collection); + } + }, () => { + for (let object of addedObjects) { + object.removed = false; + object.subscribe(this._collection); + } + }, window.cvat.player.frames.current); + // End of undo/redo code } - }, window.cvat.player.frames.current); - // End of undo/redo code + } } } @@ -264,7 +269,10 @@ class ShapeBufferController { pasteToFrame(e, bbRect, polyPoints) { if (this._model.pasteMode) { - this._model.pasteToFrame(bbRect, polyPoints); + if (bbRect || polyPoints) { + this._model.pasteToFrame(bbRect, polyPoints); + } + if (!e.ctrlKey) { this._model.switchPaste(); } @@ -298,35 +306,37 @@ class ShapeBufferView { _drawShapeView() { let scale = window.cvat.player.geometry.scale; + let points = this._shape.position.points ? + window.cvat.translate.points.actualToCanvas(this._shape.position.points) : null; switch (this._shape.type) { case 'box': { let width = this._shape.position.xbr - this._shape.position.xtl; let height = this._shape.position.ybr - this._shape.position.ytl; + this._shape.position = window.cvat.translate.box.actualToCanvas(this._shape.position); this._shapeView = this._frameContent.rect(width, height) - .move(this._shape.position.xtl, this._shape.position.ytl) - .addClass('shapeCreation').attr({ + .move(this._shape.position.xtl, this._shape.position.ytl).addClass('shapeCreation').attr({ 'stroke-width': STROKE_WIDTH / scale, }); break; } case 'polygon': - this._shapeView = this._frameContent.polygon(this._shape.position.points).addClass('shapeCreation').attr({ + this._shapeView = this._frameContent.polygon(points).addClass('shapeCreation').attr({ 'stroke-width': STROKE_WIDTH / scale, }); break; case 'polyline': - this._shapeView = this._frameContent.polyline(this._shape.position.points).addClass('shapeCreation').attr({ + this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({ 'stroke-width': STROKE_WIDTH / scale, }); break; case 'points': - this._shapeView = this._frameContent.polyline(this._shape.position.points).addClass('shapeCreation').attr({ + this._shapeView = this._frameContent.polyline(points).addClass('shapeCreation').attr({ 'stroke-width': 0, }); this._shapeViewGroup = this._frameContent.group(); - for (let point of PolyShapeModel.convertStringToNumberArray(this._shape.position.points)) { + for (let point of PolyShapeModel.convertStringToNumberArray(points)) { let radius = POINT_RADIUS * 2 / window.cvat.player.geometry.scale; let scaledStroke = STROKE_WIDTH / window.cvat.player.geometry.scale; this._shapeViewGroup.circle(radius).move(point.x - radius / 2, point.y - radius / 2) @@ -344,6 +354,7 @@ class ShapeBufferView { _moveShapeView(pos) { let rect = this._shapeView.node.getBBox(); + this._shapeView.move(pos.x - rect.width / 2, pos.y - rect.height / 2); if (this._shapeViewGroup) { let rect = this._shapeViewGroup.node.getBBox(); @@ -362,25 +373,58 @@ class ShapeBufferView { _enableEvents() { this._frameContent.on('mousemove.buffer', (e) => { - let pos = translateSVGPos(this._frameContent.node, e.clientX, e.clientY); + let pos = window.cvat.translate.point.clientToCanvas(this._frameContent.node, e.clientX, e.clientY); this._shapeView.style('visibility', ''); this._moveShapeView(pos); }); this._frameContent.on('mousedown.buffer', (e) => { if (e.which != 1) return; - let rect = this._shapeView.node.getBBox(); if (this._shape.type != 'box') { - let points = PolyShapeModel.convertStringToNumberArray(this._shapeView.attr('points')); - for (let point of points) { - point.x = Math.clamp(point.x, 0, window.cvat.player.geometry.frameWidth); - point.y = Math.clamp(point.y, 0, window.cvat.player.geometry.frameHeight); + let actualPoints = window.cvat.translate.points.canvasToActual(this._shapeView.attr('points')); + let frameWidth = window.cvat.player.geometry.frameWidth; + let frameHeight = window.cvat.player.geometry.frameHeight; + + actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints); + for (let point of actualPoints) { + point.x = Math.clamp(point.x, 0, frameWidth); + point.y = Math.clamp(point.y, 0, frameHeight); + } + actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints); + + // Set clamped points to a view in order to get an updated bounding box for a poly shape + this._shapeView.attr('points', window.cvat.translate.points.actualToCanvas(actualPoints)); + + // Get an updated bounding box for check it area + let polybox = this._shapeView.node.getBBox(); + let w = polybox.width; + let h = polybox.height; + let area = w * h; + let type = this._shape.type; + + if (area > AREA_TRESHOLD || type === 'points' || type === 'polyline' && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) { + this._controller.pasteToFrame(e, null, actualPoints); + } + else { + this._controller.pasteToFrame(e, null, null); } - points = PolyShapeModel.convertNumberArrayToString(points); - this._controller.pasteToFrame(e, rect, points); } else { - this._controller.pasteToFrame(e, rect); + let frameWidth = window.cvat.player.geometry.frameWidth; + let frameHeight = window.cvat.player.geometry.frameHeight; + let rect = window.cvat.translate.box.canvasToActual(this._shapeView.node.getBBox()); + let box = {}; + box.xtl = Math.clamp(rect.x, 0, frameWidth); + box.ytl = Math.clamp(rect.y, 0, frameHeight); + box.xbr = Math.clamp(rect.x + rect.width, 0, frameWidth); + box.ybr = Math.clamp(rect.y + rect.height, 0, frameHeight); + + if ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= AREA_TRESHOLD) { + this._controller.pasteToFrame(e, box, null); + } + else { + this._controller.pasteToFrame(e, null, null); + } } }); diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index e8efe268..299fe3b8 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -1116,6 +1116,7 @@ class ShapeCollectionView { constructor(collectionModel, collectionController) { collectionModel.subscribe(this); this._controller = collectionController; + this._frameBackground = $('#frameBackground'); this._frameContent = SVG.adopt($('#frameContent')[0]); this._UIContent = $('#uiContent'); this._labelsContent = $('#labelsContent'); @@ -1229,12 +1230,16 @@ class ShapeCollectionView { return; } - let pos = translateSVGPos(this._frameContent.node, e.clientX, e.clientY); - if (!window.cvat.mode) { - this._controller.selectShape(pos, false); - } + let frameHeight = window.cvat.player.geometry.frameHeight; + let frameWidth = window.cvat.player.geometry.frameWidth; + let pos = window.cvat.translate.point.clientToCanvas(this._frameBackground[0], e.clientX, e.clientY); + if (pos.x >= 0 && pos.y >= 0 && pos.x <= frameWidth && pos.y <= frameHeight) { + if (!window.cvat.mode) { + this._controller.selectShape(pos, false); + } - this._controller.setLastPosition(pos); + this._controller.setLastPosition(pos); + } }.bind(this)); $('#shapeContextMenu li').click((e) => { diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js index 7cd91213..53408080 100644 --- a/cvat/apps/engine/static/engine/js/shapeCreator.js +++ b/cvat/apps/engine/static/engine/js/shapeCreator.js @@ -234,22 +234,6 @@ class ShapeCreatorView { this._polyShapeSizeInput.on('keydown', function(e) { e.stopPropagation(); }); - - this._playerFrame.on('mousemove', function(e) { - // Save last coordinates in order to draw aim - this._aimCoord = translateSVGPos(this._frameContent.node, e.clientX, e.clientY); - if (this._aim) { - this._aim.x.attr({ - y1: this._aimCoord.y, - y2: this._aimCoord.y, - }); - - this._aim.y.attr({ - x1: this._aimCoord.x, - x2: this._aimCoord.x, - }); - } - }.bind(this)); } @@ -329,29 +313,36 @@ class ShapeCreatorView { }); // Also we need callback on drawdone event for get points this._drawInstance.on('drawdone', function(e) { - let points = PolyShapeModel.convertStringToNumberArray(e.target.getAttribute('points')); - for (let point of points) { - point.x = Math.clamp(point.x, 0, window.cvat.player.geometry.frameWidth); - point.y = Math.clamp(point.y, 0, window.cvat.player.geometry.frameHeight); - } + let actualPoints = window.cvat.translate.points.canvasToActual(e.target.getAttribute('points')); + actualPoints = PolyShapeModel.convertStringToNumberArray(actualPoints); // Min 2 points for polyline and 3 points for polygon - if (points.length) { - if (this._type === 'polyline' && points.length < 2) { + if (actualPoints.length) { + if (this._type === 'polyline' && actualPoints.length < 2) { showMessage("Min 2 points must be for polyline drawing."); } - else if (this._type === 'polygon' && points.length < 3) { + else if (this._type === 'polygon' && actualPoints.length < 3) { showMessage("Min 3 points must be for polygon drawing."); } else { - points = PolyShapeModel.convertNumberArrayToString(points); - - // Update points in view in order to get updated box - e.target.setAttribute('points', points); - let box = e.target.getBBox(); - if (box.width * box.height >= AREA_TRESHOLD || this._type === 'points' || - this._type === 'polyline' && (box.width >= AREA_TRESHOLD || box.height >= AREA_TRESHOLD)) { - this._controller.finish({points: e.target.getAttribute('points')}, this._type); + let frameWidth = window.cvat.player.geometry.frameWidth; + let frameHeight = window.cvat.player.geometry.frameHeight; + for (let point of actualPoints) { + point.x = Math.clamp(point.x, 0, frameWidth); + point.y = Math.clamp(point.y, 0, frameHeight); + } + actualPoints = PolyShapeModel.convertNumberArrayToString(actualPoints); + + // Update points in a view in order to get an updated box + e.target.setAttribute('points', window.cvat.translate.points.actualToCanvas(actualPoints)); + let polybox = e.target.getBBox(); + let w = polybox.width; + let h = polybox.height; + let area = w * h; + let type = this.type; + + if (area >= AREA_TRESHOLD || type === 'points' || type === 'polyline' && (w >= AREA_TRESHOLD || h >= AREA_TRESHOLD)) { + this._controller.finish({points: actualPoints}, type); } } } @@ -373,19 +364,22 @@ class ShapeCreatorView { sizeUI.rm(); sizeUI = null; } - let result = { - xtl: Math.max(0, +e.target.getAttribute('x')), - ytl: Math.max(0, +e.target.getAttribute('y')), - xbr: Math.min(window.cvat.player.geometry.frameWidth, +e.target.getAttribute('x') + +e.target.getAttribute('width')), - ybr: Math.min(window.cvat.player.geometry.frameHeight, +e.target.getAttribute('y') + +e.target.getAttribute('height')), - }; + + let frameWidth = window.cvat.player.geometry.frameWidth; + let frameHeight = window.cvat.player.geometry.frameHeight; + let rect = window.cvat.translate.box.canvasToActual(e.target.getBBox()); + let box = {}; + box.xtl = Math.clamp(rect.x, 0, frameWidth); + box.ytl = Math.clamp(rect.y, 0, frameHeight); + box.xbr = Math.clamp(rect.x + rect.width, 0, frameWidth); + box.ybr = Math.clamp(rect.y + rect.height, 0, frameHeight); if (this._mode === 'interpolation') { - result.outside = false; + box.outside = false; } - if ((result.ybr - result.ytl) * (result.xbr - result.xtl) >= AREA_TRESHOLD) { - this._controller.finish(result, this._type); + if ((box.ybr - box.ytl) * (box.xbr - box.xtl) >= AREA_TRESHOLD) { + this._controller.finish(box, this._type); } this._controller.switchCreateMode(true); @@ -434,37 +428,6 @@ class ShapeCreatorView { throw Error(`Bad type found ${this._type}`); } - this._playerFrame.on('click.shapeCreation', (e) => { - if (e.target === this._playerFrame[0]) { - let original = e.originalEvent; - Object.defineProperty(original, 'clientX', { - value: original.clientX, - writable: true, - }); - - Object.defineProperty(original, 'clientY', { - value: original.clientY, - writable: true, - }); - - let svgNodePos = this._frameContent.node.getBoundingClientRect(); - - original.clientX = Math.clamp(original.clientX, svgNodePos.left, svgNodePos.right); - original.clientY = Math.clamp(original.clientY, svgNodePos.top, svgNodePos.bottom); - - if (this._type === 'box') { - this._drawInstance.draw(original); - } - else { - for (let point of this._drawInstance.array().value) { - point[0] = Math.clamp(point[0], 0, window.cvat.player.geometry.frameWidth); - point[1] = Math.clamp(point[1], 0, window.cvat.player.geometry.frameHeight); - } - this._drawInstance.draw('point', original); - } - } - }); - this._drawInstance.attr({ 'z_order': Number.MAX_SAFE_INTEGER, }); @@ -480,13 +443,13 @@ class ShapeCreatorView { _drawAim() { if (!(this._aim)) { this._aim = { - x: this._frameContent.line(0, this._aimCoord.y, window.cvat.player.geometry.frameWidth, this._aimCoord.y) + x: this._frameContent.line(0, this._aimCoord.y, this._frameContent.node.clientWidth, this._aimCoord.y) .attr({ 'stroke-width': STROKE_WIDTH / this._scale, 'stroke': 'red', 'z_order': Number.MAX_SAFE_INTEGER, }).addClass('aim'), - y: this._frameContent.line(this._aimCoord.x, 0, this._aimCoord.x, window.cvat.player.geometry.frameHeight) + y: this._frameContent.line(this._aimCoord.x, 0, this._aimCoord.x, this._frameContent.node.clientHeight) .attr({ 'stroke-width': STROKE_WIDTH / this._scale, 'stroke': 'red', @@ -512,6 +475,20 @@ class ShapeCreatorView { if (!['polygon', 'polyline', 'points'].includes(this._type)) { this._drawAim(); + this._playerFrame.on('mousemove.shapeCreatorAIM', (e) => { + this._aimCoord = window.cvat.translate.point.clientToCanvas(this._frameContent.node, e.clientX, e.clientY); + if (this._aim) { + this._aim.x.attr({ + y1: this._aimCoord.y, + y2: this._aimCoord.y, + }); + + this._aim.y.attr({ + x1: this._aimCoord.x, + x2: this._aimCoord.x, + }); + } + }); } this._createButton.text("Stop Creation"); @@ -519,18 +496,19 @@ class ShapeCreatorView { this._create(); } else { + this._playerFrame.off('mousemove.shapeCreatorAIM'); this._removeAim(); + this._aimCoord = { + x: 0, + y: 0 + }; this._cancel = true; this._createButton.text("Create Shape"); document.oncontextmenu = null; - this._playerFrame.off('click.shapeCreation'); if (this._drawInstance) { - // if need save current result for poly shape, do it. - // drawInstance and env will clean in the future when - // drawdone handler will call switchCreateMode with force argument - // also need draw min one point. Otherwise errors occur in SVG.draw.js on done event + // We save current result for poly shape if it's need + // drawInstance will be removed after save when drawdone handler calls switchCreateMode with force argument if (model.saveCurrent && this._type != 'box') { - // FIXME: Error occured in svg.draw.js if no points was drawed and done, cancel or stop action applied this._drawInstance.draw('done'); } else { diff --git a/cvat/apps/engine/static/engine/js/shapeGrouper.js b/cvat/apps/engine/static/engine/js/shapeGrouper.js index 7a0dccbf..a7cbf9ee 100644 --- a/cvat/apps/engine/static/engine/js/shapeGrouper.js +++ b/cvat/apps/engine/static/engine/js/shapeGrouper.js @@ -186,11 +186,12 @@ class ShapeGrouperView { _enableEvents() { this._frameContent.on('mousedown.grouper', (e) => { - this._initPoint = translateSVGPos(this._frameContent[0], e.clientX, e.clientY); + this._initPoint = window.cvat.translate.point.clientToCanvas(this._frameContent[0], e.clientX, e.clientY); }); this._frameContent.on('mousemove.grouper', (e) => { - let currentPoint = translateSVGPos(this._frameContent[0], e.clientX, e.clientY); + let currentPoint = window.cvat.translate.point.clientToCanvas(this._frameContent[0], e.clientX, e.clientY); + if (this._initPoint) { if (!this._rectSelector) { this._rectSelector = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect')); diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 4dc4567a..4d6c79d7 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -609,7 +609,7 @@ class BoxModel extends ShapeModel { return Object.assign({}, this._positions[this._frame], { - outside: false + outside: this._frame != frame } ); } @@ -868,16 +868,42 @@ class PolyShapeModel extends ShapeModel { } _interpolatePosition(frame) { + if (this._type.startsWith('annotation')) { + return Object.assign({}, + this._positions[this._frame], + { + outside: this._frame != frame + } + ); + } + + let [leftFrame, rightFrame] = this._neighboringFrames(frame); if (frame in this._positions) { - return Object.assign({}, this._positions[frame], { - outside: false - }); + leftFrame = frame; } - else { - return { - outside: true - }; + + let leftPos = null; + let rightPos = null; + + if (leftFrame != null) leftPos = this._positions[leftFrame]; + if (rightFrame != null) rightPos = this._positions[rightFrame]; + + if (!leftPos) { + if (rightPos) { + return Object.assign({}, rightPos, { + outside: true, + }); + } + else { + return { + outside: true + }; + } } + + return Object.assign({}, leftPos, { + outside: leftFrame != frame, + }); } updatePosition(frame, position, silent) { @@ -1405,6 +1431,8 @@ class ShapeView extends Listener { this._shapeContextMenu = $('#shapeContextMenu'); this._pointContextMenu = $('#pointContextMenu'); + this._rightBorderFrame = $('#playerFrame')[0].offsetWidth; + shapeModel.subscribe(this); } @@ -2415,24 +2443,18 @@ class ShapeView extends Listener { this._uis.shape.attr('stroke-width', STROKE_WIDTH / scale); } - 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; + let x = shapeBBox.x + shapeBBox.width + TEXT_MARGIN * revscale; let y = shapeBBox.y; - if (x + textBBox.width * revscale > window.cvat.player.geometry.frameWidth) { - x = shapeBBox.x - TEXT_MARGIN - textBBox.width * revscale; - if (x < 0) { - x = shapeBBox.x + TEXT_MARGIN; - } - } - - if (y + textBBox.height * revscale > window.cvat.player.geometry.frameHeight) { - y = Math.max(0, window.cvat.player.geometry.frameHeight - textBBox.height * revscale); + let transl = window.cvat.translate.point; + let canvas = this._scenes.svg.node; + if (transl.canvasToClient(canvas, x + textBBox.width * revscale, 0).x > this._rightBorderFrame) { + x = shapeBBox.x + TEXT_MARGIN * revscale; } this._uis.text.move(x / revscale, y / revscale); @@ -2752,7 +2774,7 @@ class BoxView extends ShapeView { _buildPosition() { let shape = this._uis.shape.node; - return { + return window.cvat.translate.box.canvasToActual({ xtl: +shape.getAttribute('x'), ytl: +shape.getAttribute('y'), xbr: +shape.getAttribute('x') + +shape.getAttribute('width'), @@ -2760,13 +2782,12 @@ class BoxView extends ShapeView { occluded: this._uis.shape.hasClass('occludedShape'), outside: false, // if drag or resize possible, track is not outside z_order: +shape.getAttribute('z_order'), - }; + }); } _drawShapeUI(position) { - let xtl = position.xtl; - let ytl = position.ytl; + position = window.cvat.translate.box.actualToCanvas(position); let width = position.xbr - position.xtl; let height = position.ybr - position.ytl; @@ -2776,7 +2797,7 @@ class BoxView extends ShapeView { 'stroke-width': STROKE_WIDTH / window.cvat.player.geometry.scale, 'z_order': position.z_order, 'fill-opacity': this._appearance.fillOpacity - }).move(xtl, ytl).addClass('shape'); + }).move(position.xtl, position.ytl).addClass('shape'); ShapeView.prototype._drawShapeUI.call(this); } @@ -2791,12 +2812,14 @@ class BoxView extends ShapeView { oldMask.remove(); } - let size = { + let size = window.cvat.translate.box.actualToCanvas({ x: 0, y: 0, width: window.cvat.player.geometry.frameWidth, height: window.cvat.player.geometry.frameHeight - }; + }); + + pos = window.cvat.translate.box.actualToCanvas(pos); let excludeField = this._scenes.svg.rect(size.width, size.height).move(size.x, size.y).fill('#666'); let includeField = this._scenes.svg.rect(pos.xbr - pos.xtl, pos.ybr - pos.ytl).move(pos.xtl, pos.ytl); @@ -2822,7 +2845,7 @@ class PolyShapeView extends ShapeView { _buildPosition() { return { - points: this._uis.shape.node.getAttribute('points'), + points: window.cvat.translate.points.canvasToActual(this._uis.shape.node.getAttribute('points')), occluded: this._uis.shape.hasClass('occludedShape'), outside: false, z_order: +this._uis.shape.node.getAttribute('z_order'), @@ -2840,15 +2863,17 @@ class PolyShapeView extends ShapeView { oldMask.remove(); } - let size = { + let size = window.cvat.translate.box.actualToCanvas({ x: 0, y: 0, width: window.cvat.player.geometry.frameWidth, height: window.cvat.player.geometry.frameHeight - }; + }); + + let points = window.cvat.translate.points.actualToCanvas(pos.points); let excludeField = this._scenes.svg.rect(size.width, size.height).move(size.x, size.y).fill('#666'); - let includeField = this._scenes.svg.polygon(pos.points); + let includeField = this._scenes.svg.polygon(points); this._scenes.svg.mask().add(excludeField).add(includeField).fill('black').attr('id', 'outsideMask'); this._scenes.svg.rect(size.width, size.height).move(size.x, size.y).attr({ mask: 'url(#outsideMask)', @@ -2879,6 +2904,7 @@ class PolyShapeView extends ShapeView { }); e.preventDefault(); + e.stopPropagation(); }); point.on('dblclick.polyshapeEditor', (e) => { @@ -2940,7 +2966,8 @@ class PolygonView extends PolyShapeView { } _drawShapeUI(position) { - this._uis.shape = this._scenes.svg.polygon(position.points).fill(this._appearance.colors.shape).attr({ + let points = window.cvat.translate.points.actualToCanvas(position.points); + this._uis.shape = this._scenes.svg.polygon(points).fill(this._appearance.colors.shape).attr({ 'fill': this._appearance.fill || this._appearance.colors.shape, 'stroke': this._appearance.stroke || this._appearance.colors.shape, 'stroke-width': STROKE_WIDTH / window.cvat.player.geometry.scale, @@ -2980,7 +3007,8 @@ class PolylineView extends PolyShapeView { _drawShapeUI(position) { - this._uis.shape = this._scenes.svg.polyline(position.points).fill(this._appearance.colors.shape).attr({ + let points = window.cvat.translate.points.actualToCanvas(position.points); + this._uis.shape = this._scenes.svg.polyline(points).fill(this._appearance.colors.shape).attr({ 'stroke': this._appearance.stroke || this._appearance.colors.shape, 'stroke-width': STROKE_WIDTH / window.cvat.player.geometry.scale, 'z_order': position.z_order, @@ -3122,17 +3150,19 @@ class PointsView extends PolyShapeView { if (!this._controller.hiddenShape) { let interpolation = this._controller.interpolate(window.cvat.player.frames.current); if (interpolation.position.points) { - this._drawPointMarkers(interpolation.position); + let points = window.cvat.translate.points.actualToCanvas(interpolation.position.points); + this._drawPointMarkers(Object.assign(interpolation.position.points, {points: points})); } } } _drawShapeUI(position) { - this._uis.shape = this._scenes.svg.polyline(position.points).addClass('shape points').attr({ + let points = window.cvat.translate.points.actualToCanvas(position.points); + this._uis.shape = this._scenes.svg.polyline(points).addClass('shape points').attr({ 'z_order': position.z_order, }); - this._drawPointMarkers(position); + this._drawPointMarkers(Object.assign(position, {points: points})); ShapeView.prototype._drawShapeUI.call(this); } diff --git a/cvat/apps/engine/static/engine/stylesheet.css b/cvat/apps/engine/static/engine/stylesheet.css index 6b89d57b..1a5484a8 100644 --- a/cvat/apps/engine/static/engine/stylesheet.css +++ b/cvat/apps/engine/static/engine/stylesheet.css @@ -263,6 +263,7 @@ fill: white; text-shadow: 0px 0px 3px black; cursor: default; + pointer-events: none; } .highlightedShape { @@ -460,6 +461,7 @@ #frameContent { position: absolute; z-index: 1; + outline: 10px solid black; -moz-transform-origin: top left; -webkit-transform-origin: top left; } diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index a72bd4db..b5d612ff 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -35,6 +35,7 @@ + diff --git a/tests/eslintrc.conf.js b/tests/eslintrc.conf.js index 487df365..9e27f60c 100644 --- a/tests/eslintrc.conf.js +++ b/tests/eslintrc.conf.js @@ -41,7 +41,6 @@ module.exports = { 'AnnotationParser': true, // from annotationUI.js 'callAnnotationUI': true, - 'translateSVGPos': true, 'blurAllElements': true, 'drawBoxSize': true, 'copyToClipboard': true, @@ -123,5 +122,7 @@ module.exports = { 'PolyshapeEditorModel': true, 'PolyshapeEditorController': true, 'PolyshapeEditorView': true, + // from coordinateTranslator + 'CoordinateTranslator': true, }, };