diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b271c00b7ea..6097e3648355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### 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) ### Changed - Propagation setup has been moved from settings to bottom player panel diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index cccf01417374..c351f4c5e614 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -653,13 +653,11 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player } -function drawBoxSize(scene, box) { - let scale = window.cvat.player.geometry.scale; - let width = +box.getAttribute('width'); - let height = +box.getAttribute('height'); - let text = `${width.toFixed(1)}x${height.toFixed(1)}`; +function drawBoxSize(boxScene, textScene, box) { + let clientBox = window.cvat.translate.box.canvasToClient(boxScene.node, box); + let text = `${box.width.toFixed(1)}x${box.height.toFixed(1)}`; let obj = this && this.textUI && this.rm ? this : { - textUI: scene.text('').font({ + textUI: textScene.text('').font({ weight: 'bolder' }).fill('white'), @@ -670,16 +668,11 @@ function drawBoxSize(scene, box) { } }; - obj.textUI.clear().plain(text); - - obj.textUI.font({ - size: 20 / scale, - }).style({ - stroke: 'black', - 'stroke-width': 1 / scale - }); + let textPoint = window.cvat.translate.point.clientToCanvas(textScene.node, clientBox.x, clientBox.y); - obj.textUI.move(+box.getAttribute('x'), +box.getAttribute('y')); + obj.textUI.clear().plain(text); + obj.textUI.addClass("shapeText"); + obj.textUI.move(textPoint.x, textPoint.y); return obj; } diff --git a/cvat/apps/engine/static/engine/js/coordinateTranslator.js b/cvat/apps/engine/static/engine/js/coordinateTranslator.js index 087974019338..6315f0e1dca8 100644 --- a/cvat/apps/engine/static/engine/js/coordinateTranslator.js +++ b/cvat/apps/engine/static/engine/js/coordinateTranslator.js @@ -37,6 +37,30 @@ class CoordinateTranslator { return this._convert(actualBox, -1); }, + + canvasToClient: function(sourceCanvas, canvasBox) { + let points = [ + window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x, canvasBox.y), + window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x + canvasBox.width, canvasBox.y), + window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x, canvasBox.y + canvasBox.height), + window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x + canvasBox.width, canvasBox.y + canvasBox.height), + ]; + + let xes = points.map((el) => el.x); + let yes = points.map((el) => el.y); + + let xmin = Math.min(...xes); + let xmax = Math.max(...xes); + let ymin = Math.min(...yes); + let ymax = Math.max(...yes); + + return { + x: xmin, + y: ymin, + width: xmax - xmin, + height: ymax - ymin + }; + }, }; this._pointsTranslator = { @@ -70,6 +94,7 @@ class CoordinateTranslator { }, this._pointTranslator = { + _rotation: 0, clientToCanvas: function(targetCanvas, clientX, clientY) { let pt = targetCanvas.createSVGPoint(); pt.x = clientX; @@ -83,6 +108,19 @@ class CoordinateTranslator { pt.y = canvasY; pt = pt.matrixTransform(sourceCanvas.getScreenCTM()); return pt; + }, + rotate(x, y, cx, cy) { + cx = (typeof cx === "undefined" ? 0 : cx); + cy = (typeof cy === "undefined" ? 0 : cy); + + let radians = (Math.PI / 180) * window.cvat.player.rotation; + let cos = Math.cos(radians); + let sin = Math.sin(radians); + + return { + x: (cos * (x - cx)) + (sin * (y - cy)) + cx, + y: (cos * (y - cy)) - (sin * (x - cx)) + cy + } } }; } @@ -103,4 +141,8 @@ class CoordinateTranslator { this._boxTranslator._playerOffset = value; this._pointsTranslator._playerOffset = value; } + + set rotation(value) { + this._pointTranslator._rotation = value; + } } diff --git a/cvat/apps/engine/static/engine/js/logger.js b/cvat/apps/engine/static/engine/js/logger.js index 3060a7160220..1f86bf5d663d 100644 --- a/cvat/apps/engine/static/engine/js/logger.js +++ b/cvat/apps/engine/static/engine/js/logger.js @@ -363,6 +363,8 @@ var Logger = { debugInfo: 24, // dumped as "Fit image". There are no additional required fields. fitImage: 25, + // dumped as "Rotate image". There are no additional required fields. + rotateImage: 26, }, /** @@ -526,6 +528,7 @@ var Logger = { case this.EventType.changeFrame: return 'Change frame'; case this.EventType.debugInfo: return 'Debug info'; case this.EventType.fitImage: return 'Fit image'; + case this.EventType.rotateImage: return 'Rotate image'; default: return 'Unknown'; } }, diff --git a/cvat/apps/engine/static/engine/js/player.js b/cvat/apps/engine/static/engine/js/player.js index 1002e0d48bc7..c231e57e18bc 100644 --- a/cvat/apps/engine/static/engine/js/player.js +++ b/cvat/apps/engine/static/engine/js/player.js @@ -146,6 +146,7 @@ class PlayerModel extends Listener { this._settings = { multipleStep: 10, fps: 25, + rotateAll: job.mode === 'interpolation', resetZoom: job.mode === 'annotation' }; @@ -162,13 +163,16 @@ class PlayerModel extends Listener { width: playerSize.width, height: playerSize.height, frameOffset: 0, + rotation: 0, }; + this._framewiseRotation = {}; 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; + window.cvat.player.rotation = this._geometry.rotation; this._frameProvider.subscribe(this); } @@ -183,7 +187,10 @@ class PlayerModel extends Listener { } get geometry() { - return Object.assign({}, this._geometry); + let copy = Object.assign({}, this._geometry); + copy.rotation = this._settings.rotateAll ? this._geometry.rotation : + this._framewiseRotation[this._frame.current] || 0; + return copy; } get playing() { @@ -202,6 +209,20 @@ class PlayerModel extends Listener { return this._settings.multipleStep; } + get rotateAll() { + return this._settings.rotateAll; + } + + set rotateAll(value) { + this._settings.rotateAll = value; + + if (!value) { + this._geometry.rotation = 0; + } else { + this._framewiseRotation = {}; + } + } + set fps(value) { this._settings.fps = value; } @@ -304,7 +325,8 @@ class PlayerModel extends Listener { }); let changed = this._frame.previous != this._frame.current; - if (this._settings.resetZoom || this._frame.previous === null) { // fit in annotation mode or once in interpolation mode + // fit if tool is in the annotation mode or frame loading is first in the interpolation mode + if (this._settings.resetZoom || !this._settings.rotateAll || this._frame.previous === null) { this._frame.previous = this._frame.current; this.fit(); // notify() inside the fit() } @@ -319,10 +341,22 @@ class PlayerModel extends Listener { fit() { let img = this._frameProvider.require(this._frame.current); if (!img) return; - this._geometry.scale = Math.min(this._geometry.width / img.width, this._geometry.height / img.height); + + let rotation = this.geometry.rotation; + + if ((rotation / 90) % 2) { + // 90, 270, .. + this._geometry.scale = Math.min(this._geometry.width / img.height, this._geometry.height / img.width); + } + else { + // 0, 180, .. + this._geometry.scale = Math.min(this._geometry.width / img.width, this._geometry.height / img.height); + } + this._geometry.top = (this._geometry.height - img.height * this._geometry.scale) / 2; this._geometry.left = (this._geometry.width - img.width * this._geometry.scale ) / 2; + window.cvat.player.rotation = rotation; window.cvat.player.geometry.scale = this._geometry.scale; this.notify(); } @@ -352,28 +386,19 @@ class PlayerModel extends Listener { window.cvat.player.geometry.scale = this._geometry.scale; this._frame.previous = this._frame.current; // fix infinite loop via playerUpdate->collectionUpdate*->AAMUpdate->playerUpdate->... this.notify(); - } - scale(x, y, value) { + scale(point, value) { if (!this._frameProvider.require(this._frame.current)) return; - let currentCenter = { - x: (x - this._geometry.left) / this._geometry.scale, - y: (y - this._geometry.top) / this._geometry.scale - }; - - this._geometry.scale = value > 0 ? this._geometry.scale * 6/5 : this._geometry.scale * 5/6; - this._geometry.scale = Math.min(this._geometry.scale, MAX_PLAYER_SCALE); - this._geometry.scale = Math.max(this._geometry.scale, MIN_PLAYER_SCALE); - - let newCenter = { - x: (x - this._geometry.left) / this._geometry.scale, - y: (y - this._geometry.top) / this._geometry.scale - }; + let oldScale = this._geometry.scale; + this._geometry.scale = Math.clamp( + value > 0 ? this._geometry.scale * 6/5 : this._geometry.scale * 5/6, + MIN_PLAYER_SCALE, MAX_PLAYER_SCALE + ); - this._geometry.left += (newCenter.x - currentCenter.x) * this._geometry.scale; - this._geometry.top += (newCenter.y - currentCenter.y) * this._geometry.scale; + this._geometry.left += (point.x * (oldScale / this._geometry.scale - 1)) * this._geometry.scale; + this._geometry.top += (point.y * (oldScale / this._geometry.scale - 1)) * this._geometry.scale; window.cvat.player.geometry.scale = this._geometry.scale; this.notify(); @@ -384,6 +409,26 @@ class PlayerModel extends Listener { this._geometry.left += leftOffset; this.notify(); } + + rotate(angle) { + if (['resize', 'drag'].indexOf(window.cvat.mode) != -1) { + return false; + } + + if (this._settings.rotateAll) { + this._geometry.rotation += angle; + this._geometry.rotation %= 360; + } else { + if (typeof(this._framewiseRotation[this._frame.current]) === 'undefined') { + this._framewiseRotation[this._frame.current] = angle; + } else { + this._framewiseRotation[this._frame.current] += angle; + this._framewiseRotation[this._frame.current] %= 360; + } + } + + this.fit(); + } } @@ -483,19 +528,27 @@ class PlayerController { Mousetrap.bind(shortkeys["forward_frame"].value, forwardHandler, 'keydown'); Mousetrap.bind(shortkeys["backward_frame"].value, backwardHandler, 'keydown'); Mousetrap.bind(shortkeys["play_pause"].value, playPauseHandler, 'keydown'); + Mousetrap.bind(shortkeys['clockwise_rotation'].value, (e) => { + e.preventDefault(); + this.rotate(90); + }, 'keydown'); + Mousetrap.bind(shortkeys['counter_clockwise_rotation'].value, (e) => { + e.preventDefault(); + this.rotate(-90); + }, 'keydown'); } } - zoom(e) { - let x = e.originalEvent.pageX - this._leftOffset; - let y = e.originalEvent.pageY - this._topOffset; + zoom(e, canvas) { + let point = window.cvat.translate.point.clientToCanvas(canvas, e.clientX, e.clientY); let zoomImageEvent = Logger.addContinuedEvent(Logger.EventType.zoomImage); + if (e.originalEvent.deltaY < 0) { - this._model.scale(x, y, 1); + this._model.scale(point, 1); } else { - this._model.scale(x, y, -1); + this._model.scale(point, -1); } zoomImageEvent.close(); e.preventDefault(); @@ -509,8 +562,11 @@ class PlayerController { frameMouseDown(e) { if ((e.which === 1 && !window.cvat.mode) || (e.which === 2)) { this._moving = true; - this._lastClickX = e.clientX; - this._lastClickY = e.clientY; + + let p = window.cvat.translate.point.rotate(e.clientX, e.clientY); + + this._lastClickX = p.x; + this._lastClickY = p.y; } } @@ -528,11 +584,11 @@ class PlayerController { this._events.move = Logger.addContinuedEvent(Logger.EventType.moveImage); } - let topOffset = e.clientY - this._lastClickY; - let leftOffset = e.clientX - this._lastClickX; - this._lastClickX = e.clientX; - this._lastClickY = e.clientY; - + let p = window.cvat.translate.point.rotate(e.clientX, e.clientY); + let topOffset = p.y - this._lastClickY; + let leftOffset = p.x - this._lastClickX; + this._lastClickX = p.x; + this._lastClickY = p.y; this._model.move(topOffset, leftOffset); } } @@ -634,6 +690,19 @@ class PlayerController { seek(frame) { this._model.shift(frame, true); } + + rotate(angle) { + Logger.addEvent(Logger.EventType.rotateImage); + this._model.rotate(angle); + } + + get rotateAll() { + return this._model.rotateAll; + } + + set rotateAll(value) { + this._model.rotateAll = value; + } } @@ -644,6 +713,7 @@ class PlayerView { this._playerBackgroundUI = $('#frameBackground'); this._playerContentUI = $('#frameContent'); this._playerGridUI = $('#frameGrid'); + this._playerTextUI = $('#frameText'); this._progressUI = $('#playerProgress'); this._loadingUI = $('#frameLoadingAnim'); this._playButtonUI = $('#playButton'); @@ -661,6 +731,23 @@ class PlayerView { this._playerGridPattern = $('#playerGridPattern'); this._playerGridPath = $('#playerGridPath'); this._contextMenuUI = $('#playerContextMenu'); + this._clockwiseRotationButtonUI = $('#clockwiseRotation'); + this._counterClockwiseRotationButtonUI = $('#counterClockwiseRotation'); + this._rotationWrapperUI = $('#rotationWrapper'); + this._rotatateAllImagesUI = $('#rotateAllImages'); + + this._clockwiseRotationButtonUI.on('click', () => { + this._controller.rotate(90); + }); + + this._counterClockwiseRotationButtonUI.on('click', () => { + this._controller.rotate(-90); + }); + + this._rotatateAllImagesUI.prop("checked", this._controller.rotateAll); + this._rotatateAllImagesUI.on("change", (e) => { + this._controller.rotateAll = e.target.checked; + }); $('*').on('mouseup.player', () => this._controller.frameMouseUp()); this._playerContentUI.on('mousedown', (e) => { @@ -673,9 +760,9 @@ class PlayerView { e.preventDefault(); }); - this._playerUI.on('wheel', (e) => this._controller.zoom(e)); - this._playerUI.on('dblclick', () => this._controller.fit()); - this._playerUI.on('mousemove', (e) => this._controller.frameMouseMove(e)); + this._playerContentUI.on('wheel', (e) => this._controller.zoom(e, this._playerBackgroundUI[0])); + this._playerContentUI.on('dblclick', () => this._controller.fit()); + this._playerContentUI.on('mousemove', (e) => this._controller.frameMouseMove(e)); this._progressUI.on('mousedown', (e) => this._controller.progressMouseDown(e)); this._progressUI.on('mouseup', () => this._controller.progressMouseUp()); this._progressUI.on('mousemove', (e) => this._controller.progressMouseMove(e)); @@ -699,6 +786,12 @@ class PlayerView { }); let shortkeys = window.cvat.config.shortkeys; + + this._clockwiseRotationButtonUI.attr('title', ` + ${shortkeys['clockwise_rotation'].view_value} - ${shortkeys['clockwise_rotation'].description}`); + this._counterClockwiseRotationButtonUI.attr('title', ` + ${shortkeys['counter_clockwise_rotation'].view_value} - ${shortkeys['counter_clockwise_rotation'].description}`); + let playerGridOpacityInput = $('#playerGridOpacityInput'); playerGridOpacityInput.on('input', (e) => { let value = Math.clamp(+e.target.value, +e.target.min, +e.target.max); @@ -879,6 +972,8 @@ class PlayerView { this._progressUI['0'].value = frames.current - frames.start; + this._rotationWrapperUI.css("transform", `rotate(${geometry.rotation}deg)`); + for (let obj of [this._playerBackgroundUI, this._playerGridUI]) { obj.css('width', image.width); obj.css('height', image.height); @@ -887,12 +982,15 @@ 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 + ')'); + for (let obj of [this._playerContentUI, this._playerTextUI]) { + obj.css('width', image.width + geometry.frameOffset * 2); + obj.css('height', image.height + geometry.frameOffset * 2); + obj.css('top', geometry.top - geometry.frameOffset * geometry.scale); + obj.css('left', geometry.left - geometry.frameOffset * geometry.scale); + } + this._playerContentUI.css('transform', 'scale(' + geometry.scale + ')'); + this._playerTextUI.css('transform', `scale(10) rotate(${-geometry.rotation}deg)`); this._playerGridPath.attr('stroke-width', 2 / geometry.scale); this._frameNumber.prop('value', frames.current); } diff --git a/cvat/apps/engine/static/engine/js/qunitTests.js b/cvat/apps/engine/static/engine/js/qunitTests.js index 4b7bf2bb1745..3aeb14adfdd4 100644 --- a/cvat/apps/engine/static/engine/js/qunitTests.js +++ b/cvat/apps/engine/static/engine/js/qunitTests.js @@ -8,7 +8,8 @@ let qUnitTests = []; window.cvat = { - translate: {} + translate: {}, + player: {}, }; // Run all tests diff --git a/cvat/apps/engine/static/engine/js/shapeCollection.js b/cvat/apps/engine/static/engine/js/shapeCollection.js index 39222fba253c..7acde4bb94f5 100644 --- a/cvat/apps/engine/static/engine/js/shapeCollection.js +++ b/cvat/apps/engine/static/engine/js/shapeCollection.js @@ -1123,6 +1123,7 @@ class ShapeCollectionView { this._controller = collectionController; this._frameBackground = $('#frameBackground'); this._frameContent = SVG.adopt($('#frameContent')[0]); + this._textContent = SVG.adopt($('#frameText')[0]); this._UIContent = $('#uiContent'); this._labelsContent = $('#labelsContent'); this._showAllInterpolationBox = $('#showAllInterBox'); @@ -1141,6 +1142,7 @@ class ShapeCollectionView { this._activeShapeUI = null; this._scale = 1; + this._rotation = 0; this._colorSettings = { "fill-opacity": 0 }; @@ -1495,7 +1497,7 @@ class ShapeCollectionView { this._updateLabelUIs(); function drawView(shape, model) { - let view = buildShapeView(model, buildShapeController(model), this._frameContent, this._UIContent); + let view = buildShapeView(model, buildShapeController(model), this._frameContent, this._UIContent, this._textContent); view.draw(shape.interpolation); view.updateColorSettings(this._colorSettings); model.subscribe(view); @@ -1509,7 +1511,13 @@ class ShapeCollectionView { if (!player.ready()) this._frameContent.addClass('hidden'); else this._frameContent.removeClass('hidden'); - if (this._scale === player.geometry.scale) return; + let geometry = player.geometry; + if (this._rotation != geometry.rotation) { + this._rotation = geometry.rotation; + this._controller.resetActive(); + } + + if (this._scale === geometry.scale) return; this._scale = player.geometry.scale; let scaledR = POINT_RADIUS / this._scale; diff --git a/cvat/apps/engine/static/engine/js/shapeCreator.js b/cvat/apps/engine/static/engine/js/shapeCreator.js index e63cade5a445..a93dcc77e40a 100644 --- a/cvat/apps/engine/static/engine/js/shapeCreator.js +++ b/cvat/apps/engine/static/engine/js/shapeCreator.js @@ -184,6 +184,7 @@ class ShapeCreatorView { this._typeSelector = $('#shapeTypeSelector'); this._polyShapeSizeInput = $('#polyShapeSize'); this._frameContent = SVG.adopt($('#frameContent')[0]); + this._frameText = SVG.adopt($("#frameText")[0]); this._playerFrame = $('#playerFrame'); this._createButton.on('click', () => this._controller.switchCreateMode(false)); this._drawInstance = null; @@ -406,7 +407,7 @@ class ShapeCreatorView { this._controller.switchCreateMode(true); }.bind(this)).on('drawupdate', (e) => { - sizeUI = drawBoxSize.call(sizeUI, this._frameContent, e.target); + sizeUI = drawBoxSize.call(sizeUI, this._frameContent, this._frameText, e.target.getBBox()); }).on('drawcancel', () => { if (sizeUI) { sizeUI.rm(); diff --git a/cvat/apps/engine/static/engine/js/shapes.js b/cvat/apps/engine/static/engine/js/shapes.js index 8e9f2b542b75..c273640be331 100644 --- a/cvat/apps/engine/static/engine/js/shapes.js +++ b/cvat/apps/engine/static/engine/js/shapes.js @@ -1399,7 +1399,7 @@ class PolygonController extends PolyShapeController { /******************************** SHAPE VIEWS ********************************/ class ShapeView extends Listener { - constructor(shapeModel, shapeController, svgScene, menusScene) { + constructor(shapeModel, shapeController, svgScene, menusScene, textsScene) { super('onShapeViewUpdate', () => this); this._uis = { menu: null, @@ -1412,7 +1412,8 @@ class ShapeView extends Listener { this._scenes = { svg: svgScene, - menus: menusScene + menus: menusScene, + texts: textsScene }; this._appearance = { @@ -1512,6 +1513,23 @@ class ShapeView extends Listener { this.notify('resize'); }); + let centers = ['t', 'r', 'b', 'l']; + let corners = ['lt', 'rt', 'rb', 'lb']; + let elements = {}; + for (let i = 0; i < 4; ++i) { + elements[centers[i]] = $(`.svg_select_points_${centers[i]}`); + elements[corners[i]] = $(`.svg_select_points_${corners[i]}`); + } + + let angle = window.cvat.player.rotation; + let offset = angle / 90 < 0 ? angle / 90 + centers.length : angle / 90; + + for (let i = 0; i < 4; ++i) { + elements[centers[i]].removeClass(`svg_select_points_${centers[i]}`) + .addClass(`svg_select_points_${centers[(i+offset) % centers.length]}`); + elements[corners[i]].removeClass(`svg_select_points_${corners[i]}`) + .addClass(`svg_select_points_${corners[(i+offset) % centers.length]}`); + } this._updateColorForDots(); let self = this; @@ -1678,7 +1696,7 @@ class ShapeView extends Listener { _hideShapeText() { if (this._uis.text && this._uis.text.node.parentElement) { - this._scenes.svg.node.removeChild(this._uis.text.node); + this._scenes.texts.node.removeChild(this._uis.text.node); } } @@ -1689,7 +1707,7 @@ class ShapeView extends Listener { this._drawShapeText(this._controller.interpolate(frame).attributes); } else if (!this._uis.text.node.parentElement) { - this._scenes.svg.node.appendChild(this._uis.text.node); + this._scenes.texts.node.appendChild(this._uis.text.node); } this.updateShapeTextPosition(); @@ -1701,18 +1719,16 @@ class ShapeView extends Listener { if (this._uis.shape) { let id = this._controller.id; let label = ShapeView.labels()[this._controller.label]; - let bbox = this._uis.shape.node.getBBox(); - let x = bbox.x + bbox.width + TEXT_MARGIN; - this._uis.text = this._scenes.svg.text((add) => { - add.tspan(`${label.normalize()} ${id}`).addClass('bold'); + this._uis.text = this._scenes.texts.text((add) => { + add.tspan(`${label.normalize()} ${id}`).style("text-transform", "uppercase"); for (let attrId in attributes) { let value = attributes[attrId].value != AAMUndefinedKeyword ? attributes[attrId].value : ''; let name = attributes[attrId].name; - add.tspan(`${name}: ${value}`).attr({ dy: '1em', x: x, attrId: attrId}); + add.tspan(`${name}: ${value}`).attr({ dy: '1em', x: 0, attrId: attrId}); } - }).move(x, bbox.y).addClass('shapeText regular'); + }).move(0, 0).addClass('shapeText bold'); } } @@ -2461,21 +2477,29 @@ 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 shapeBBox = window.cvat.translate.box.canvasToClient(this._scenes.svg.node, this._uis.shape.node.getBBox()); let textBBox = this._uis.text.node.getBBox(); - let x = shapeBBox.x + shapeBBox.width + TEXT_MARGIN * revscale; - let y = shapeBBox.y; + let drawPoint = { + x: shapeBBox.x + shapeBBox.width + TEXT_MARGIN, + y: shapeBBox.y + }; - 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; + const textContentScale = 10; + if ((drawPoint.x + textBBox.width * textContentScale) > this._rightBorderFrame) { + drawPoint = { + x: shapeBBox.x + TEXT_MARGIN, + y: shapeBBox.y + }; } - this._uis.text.move(x / revscale, y / revscale); - this._uis.text.attr('transform', `scale(${revscale})`); + let textPoint = window.cvat.translate.point.clientToCanvas( + this._scenes.texts.node, + drawPoint.x, + drawPoint.y + ); + + this._uis.text.move(textPoint.x, textPoint.y); for (let tspan of this._uis.text.lines().members) { tspan.attr('x', this._uis.text.attr('x')); @@ -2758,8 +2782,8 @@ ShapeView.labels = function() { class BoxView extends ShapeView { - constructor(boxModel, boxController, svgScene, menusScene) { - super(boxModel, boxController, svgScene, menusScene); + constructor(boxModel, boxController, svgScene, menusScene, textsScene) { + super(boxModel, boxController, svgScene, menusScene, textsScene); this._uis.boxSize = null; } @@ -2774,9 +2798,9 @@ class BoxView extends ShapeView { this._uis.boxSize = null; } - this._uis.boxSize = drawBoxSize(this._scenes.svg, e.target); + this._uis.boxSize = drawBoxSize(this._scenes.svg, this._scenes.texts, e.target.getBBox()); }).on('resizing', (e) => { - this._uis.boxSize = drawBoxSize.call(this._uis.boxSize, this._scenes.svg, e.target); + this._uis.boxSize = drawBoxSize.call(this._uis.boxSize, this._scenes.svg, this._scenes.texts, e.target.getBBox()); }).on('resizedone', () => { this._uis.boxSize.rm(); }); @@ -2827,8 +2851,8 @@ class BoxView extends ShapeView { class PolyShapeView extends ShapeView { - constructor(polyShapeModel, polyShapeController, svgScene, menusScene) { - super(polyShapeModel, polyShapeController, svgScene, menusScene); + constructor(polyShapeModel, polyShapeController, svgScene, menusScene, textsScene) { + super(polyShapeModel, polyShapeController, svgScene, menusScene, textsScene); } @@ -2918,8 +2942,8 @@ class PolyShapeView extends ShapeView { class PolygonView extends PolyShapeView { - constructor(polygonModel, polygonController, svgContent, UIContent) { - super(polygonModel, polygonController, svgContent, UIContent); + constructor(polygonModel, polygonController, svgContent, UIContent, textsScene) { + super(polygonModel, polygonController, svgContent, UIContent, textsScene); } _drawShapeUI(position) { @@ -2958,8 +2982,8 @@ class PolygonView extends PolyShapeView { class PolylineView extends PolyShapeView { - constructor(polylineModel, polylineController, svgScene, menusScene) { - super(polylineModel, polylineController, svgScene, menusScene); + constructor(polylineModel, polylineController, svgScene, menusScene, textsScene) { + super(polylineModel, polylineController, svgScene, menusScene, textsScene); } @@ -3031,8 +3055,8 @@ class PolylineView extends PolyShapeView { class PointsView extends PolyShapeView { - constructor(pointsModel, pointsController, svgScene, menusScene) { - super(pointsModel, pointsController, svgScene, menusScene); + constructor(pointsModel, pointsController, svgScene, menusScene, textsScene) { + super(pointsModel, pointsController, svgScene, menusScene, textsScene); this._uis.points = null; } @@ -3220,20 +3244,20 @@ function buildShapeController(shapeModel) { } -function buildShapeView(shapeModel, shapeController, svgContent, UIContent) { +function buildShapeView(shapeModel, shapeController, svgContent, UIContent, textsContent) { switch (shapeModel.type) { case 'interpolation_box': case 'annotation_box': - return new BoxView(shapeModel, shapeController, svgContent, UIContent); + return new BoxView(shapeModel, shapeController, svgContent, UIContent, textsContent); case 'interpolation_points': case 'annotation_points': - return new PointsView(shapeModel, shapeController, svgContent, UIContent); + return new PointsView(shapeModel, shapeController, svgContent, UIContent, textsContent); case 'interpolation_polyline': case 'annotation_polyline': - return new PolylineView(shapeModel, shapeController, svgContent, UIContent); + return new PolylineView(shapeModel, shapeController, svgContent, UIContent, textsContent); case 'interpolation_polygon': case 'annotation_polygon': - return new PolygonView(shapeModel, shapeController, svgContent, UIContent); + return new PolygonView(shapeModel, shapeController, svgContent, UIContent, textsContent); } throw Error('Unreacheable code was reached.'); } diff --git a/cvat/apps/engine/static/engine/js/userConfig.js b/cvat/apps/engine/static/engine/js/userConfig.js index 25feb1242d5a..3e0329bd22a0 100644 --- a/cvat/apps/engine/static/engine/js/userConfig.js +++ b/cvat/apps/engine/static/engine/js/userConfig.js @@ -286,7 +286,19 @@ class Config { value: 'esc', view_value: "Esc", description: "cancel active mode" - } + }, + + clockwise_rotation: { + value: 'ctrl+r', + view_value: 'Ctrl + R', + description: 'clockwise image rotation' + }, + + counter_clockwise_rotation: { + value: 'ctrl+shift+r', + view_value: 'Ctrl + Shift + R', + description: 'counter clockwise image rotation' + }, }; if (window.cvat && window.cvat.job && window.cvat.job.z_order) { diff --git a/cvat/apps/engine/static/engine/stylesheet.css b/cvat/apps/engine/static/engine/stylesheet.css index 570aad45499d..beaa6fd38c80 100644 --- a/cvat/apps/engine/static/engine/stylesheet.css +++ b/cvat/apps/engine/static/engine/stylesheet.css @@ -225,11 +225,11 @@ } .shapeText { - font-size: 1.2em; + font-size: 0.12em; fill: white; - text-shadow: 0px 0px 3px black; + stroke:black; + stroke-width: 0.05; cursor: default; - pointer-events: none; } .highlightedShape { @@ -424,19 +424,30 @@ position: relative; } +#rotationWrapper { + width: 100%; + height: 100%; + transform-origin: center center; +} + #frameContent { position: absolute; - z-index: 1; + z-index: 2; outline: 10px solid black; - -moz-transform-origin: top left; - -webkit-transform-origin: top left; + transform-origin: top left; +} + +#frameText { + position: absolute; + z-index: 3; + transform-origin: center center; + pointer-events: none; } #frameGrid { position: absolute; z-index: 2; - -moz-transform-origin: top left; - -webkit-transform-origin: top left; + transform-origin: top left; pointer-events: none; } @@ -444,8 +455,7 @@ position: absolute; z-index: 0; background-repeat: no-repeat; - -moz-transform-origin: top left; - -webkit-transform-origin: top left; + transform-origin: top left; } #frameLoadingAnimation { diff --git a/cvat/apps/engine/templates/engine/annotation.html b/cvat/apps/engine/templates/engine/annotation.html index d93bac65b258..e8034a7fbc74 100644 --- a/cvat/apps/engine/templates/engine/annotation.html +++ b/cvat/apps/engine/templates/engine/annotation.html @@ -66,39 +66,42 @@