diff --git a/CHANGELOG.md b/CHANGELOG.md index deab2c3f..69b313da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Installation guide +- Linear interpolation for a single point ### Changed -- +- Outside and keyframe buttons in the side panel for all interpolation shapes (they were only for boxes before) ### Deprecated - @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed incorrect width of shapes borders in some cases - Fixed annotation parser for tracks with a start frame less than the first segment frame +- Fixed interpolation on the server near outside frames ### Security - diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index ef1d0bcc..7500eb12 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -1090,12 +1090,19 @@ class TrackManager(ObjectManager): step = np.subtract(shape1["points"], shape0["points"]) / distance for frame in range(shape0["frame"] + 1, shape1["frame"]): off = frame - shape0["frame"] - points = shape0["points"] + step * off + if shape1["outside"]: + points = np.asarray(shape0["points"]).reshape(-1, 2) + else: + points = (shape0["points"] + step * off).reshape(-1, 2) shape = copy.deepcopy(shape0) - broken_line = geometry.LineString(points.reshape(-1, 2)).simplify(0.05, False) + if len(points) == 1: + shape["points"] = points.flatten() + else: + broken_line = geometry.LineString(points).simplify(0.05, False) + shape["points"] = [x for p in broken_line.coords for x in p] + shape["keyframe"] = False shape["frame"] = frame - shape["points"] = [x for p in broken_line.coords for x in p] shapes.append(shape) return shapes diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js index 76f7ec55..78d3b155 100644 --- a/cvat/apps/engine/static/engine/js/shapeCreator.js +++ b/cvat/apps/engine/static/engine/js/shapeCreator.js @@ -54,12 +54,12 @@ class ShapeCreatorModel extends Listener { }); } - if (this._defaultMode === 'interpolation' && this._defaultType === 'box') { + // FIXME: In the future we have to make some generic solution + if (this._defaultMode === 'interpolation' && ['box', 'points'].includes(this._defaultType)) { data.shapes = []; data.shapes.push(Object.assign({}, result, data)); - this._shapeCollection.add(data, `interpolation_box`); - } - else { + this._shapeCollection.add(data, `interpolation_${this._defaultType}`); + } else { Object.assign(data, result); this._shapeCollection.add(data, `annotation_${this._defaultType}`); } @@ -213,11 +213,14 @@ class ShapeCreatorView { } this._typeSelector.on('change', (e) => { - let type = $(e.target).prop('value'); - if (type != 'box' && this._modeSelector.prop('value') != 'annotation') { + // FIXME: In the future we have to make some generic solution + const mode = this._modeSelector.prop('value'); + const type = $(e.target).prop('value'); + if (type !== 'box' && !(type === 'points' && this._polyShapeSize === 1) + && mode !== 'annotation') { this._modeSelector.prop('value', 'annotation'); this._controller.setDefaultShapeMode('annotation'); - showMessage('Poly shapes available only like annotation shapes'); + showMessage('Only the annotation mode allowed for the shape'); } this._controller.setDefaultShapeType(type); }).trigger('change'); @@ -227,20 +230,30 @@ class ShapeCreatorView { }).trigger('change'); this._modeSelector.on('change', (e) => { - let mode = $(e.target).prop('value'); - if (mode != 'annotation' && this._typeSelector.prop('value') != 'box') { + // FIXME: In the future we have to make some generic solution + const mode = $(e.target).prop('value'); + const type = this._typeSelector.prop('value'); + if (mode !== 'annotation' && !(type === 'points' && this._polyShapeSize === 1) + && type !== 'box') { this._typeSelector.prop('value', 'box'); this._controller.setDefaultShapeType('box'); - showMessage('Only boxes available like interpolation shapes'); + showMessage('Only boxes and single point allowed in the interpolation mode'); } this._controller.setDefaultShapeMode(mode); }).trigger('change'); this._polyShapeSizeInput.on('change', (e) => { e.stopPropagation(); - let size = + e.target.value; + let size = +e.target.value; if (size < 0) size = 0; if (size > 100) size = 0; + const mode = this._modeSelector.prop('value'); + const type = this._typeSelector.prop('value'); + if (mode === 'interpolation' && type === 'points' && size !== 1) { + showMessage('Only single point allowed in the interpolation mode'); + size = 1; + } + e.target.value = size || ''; this._polyShapeSize = size; }).trigger('change'); @@ -265,6 +278,7 @@ class ShapeCreatorView { let size = this._polyShapeSize; let sizeDecrement = function() { if (!--size) { + numberOfPoints = this._polyShapeSize; this._drawInstance.draw('done'); } }.bind(this); @@ -323,7 +337,7 @@ class ShapeCreatorView { this._drawInstance.draw('point', e); lastPoint = { x: e.clientX, - y: e.clientY + y: e.clientY, }; } } diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index e0373aaf..6763abd3 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -341,8 +341,8 @@ class ShapeModel extends Listener { } switchOutside(frame) { - // Only for interpolation boxes - if (this._type != 'interpolation_box') { + // Only for interpolation shapes + if (this._type.split('_')[0] !== 'interpolation') { return; } @@ -379,7 +379,7 @@ class ShapeModel extends Listener { if (frame < this._frame) { if (this._frame in this._attributes.mutable) { this._attributes.mutable[frame] = this._attributes.mutable[this._frame]; - delete(this._attributes.mutable[this._frame]); + delete (this._attributes.mutable[this._frame]); } this._frame = frame; } @@ -388,17 +388,17 @@ class ShapeModel extends Listener { } switchKeyFrame(frame) { - // Only for interpolation boxes - if (this._type != 'interpolation_box') { + // Only for interpolation shapes + if (this._type.split('_')[0] !== 'interpolation') { return; } // Undo/redo code - let oldPos = Object.assign({}, this._positions[frame]); + const oldPos = Object.assign({}, this._positions[frame]); window.cvat.addAction('Change Keyframe', () => { this.switchKeyFrame(frame); - if (Object.keys(oldPos).length && oldPos.outside) { - this.switchOutside(frame); + if (frame in this._positions) { + this.updatePosition(frame, oldPos); } }, () => { this.switchKeyFrame(frame); @@ -411,19 +411,18 @@ class ShapeModel extends Listener { this._frame = Object.keys(this._positions).map((el) => +el).sort((a,b) => a - b)[1]; if (frame in this._attributes.mutable) { this._attributes.mutable[this._frame] = this._attributes.mutable[frame]; - delete(this._attributes.mutable[frame]); + delete (this._attributes.mutable[frame]); } } - delete(this._positions[frame]); - } - else { + delete (this._positions[frame]); + } else { let position = this._interpolatePosition(frame); this.updatePosition(frame, position, true); if (frame < this._frame) { if (this._frame in this._attributes.mutable) { this._attributes.mutable[frame] = this._attributes.mutable[this._frame]; - delete(this._attributes.mutable[this._frame]); + delete (this._attributes.mutable[this._frame]); } this._frame = frame; } @@ -917,7 +916,7 @@ class PolyShapeModel extends ShapeModel { } return Object.assign({}, leftPos, { - outside: leftFrame != frame, + outside: leftPos.outside || leftFrame !== frame, }); } @@ -1145,6 +1144,60 @@ class PointsModel extends PolyShapeModel { this._minPoints = 1; } + _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) { + leftFrame = frame; + } + + 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, + }); + } + + return { + outside: true, + }; + } + + if (frame === leftFrame || leftPos.outside || !rightPos || rightPos.outside) { + return Object.assign({}, leftPos); + } + + const rightPoints = PolyShapeModel.convertStringToNumberArray(rightPos.points); + const leftPoints = PolyShapeModel.convertStringToNumberArray(leftPos.points); + + if (rightPoints.length === leftPoints.length && leftPoints.length === 1) { + const moveCoeff = (frame - leftFrame) / (rightFrame - leftFrame); + const interpolatedPoints = [{ + x: leftPoints[0].x + (rightPoints[0].x - leftPoints[0].x) * moveCoeff, + y: leftPoints[0].y + (rightPoints[0].y - leftPoints[0].y) * moveCoeff, + }]; + + return Object.assign({}, leftPos, { + points: PolyShapeModel.convertNumberArrayToString(interpolatedPoints), + }); + } + + return Object.assign({}, leftPos, { + outside: true, + }); + } + distance(mousePos, frame) { let pos = this._interpolatePosition(frame); if (pos.outside) return Number.MAX_SAFE_INTEGER; @@ -1958,19 +2011,17 @@ class ShapeView extends Listener { if (type.split('_')[0] == 'interpolation') { let interpolationCenter = document.createElement('center'); - if (type.split('_')[1] == 'box') { - let outsideButton = document.createElement('button'); - outsideButton.classList.add('graphicButton', 'outsideButton'); + let outsideButton = document.createElement('button'); + outsideButton.classList.add('graphicButton', 'outsideButton'); - let keyframeButton = document.createElement('button'); - keyframeButton.classList.add('graphicButton', 'keyFrameButton'); + let keyframeButton = document.createElement('button'); + keyframeButton.classList.add('graphicButton', 'keyFrameButton'); - interpolationCenter.appendChild(outsideButton); - interpolationCenter.appendChild(keyframeButton); + interpolationCenter.appendChild(outsideButton); + interpolationCenter.appendChild(keyframeButton); - this._uis.buttons['outside'] = outsideButton; - this._uis.buttons['keyframe'] = keyframeButton; - } + this._uis.buttons['outside'] = outsideButton; + this._uis.buttons['keyframe'] = keyframeButton; let prevKeyFrameButton = document.createElement('button'); prevKeyFrameButton.classList.add('graphicButton', 'prevKeyFrameButton'); @@ -3125,7 +3176,7 @@ class PointsView extends PolyShapeView { _drawPointMarkers(position) { - if (this._uis.points) { + if (this._uis.points || position.outside) { return; }