diff --git a/CHANGELOG.md b/CHANGELOG.md index 217545cb..b6a6eaa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Export of instance masks with holes () +- Changing a label on canvas does not work when 'Show object details' enabled () ### Security diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 52752363..38ddf7a8 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.4.1", + "version": "2.4.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 486a71a7..d6706997 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.4.1", + "version": "2.4.2", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index e84f0044..95e89b7d 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2020 Intel Corporation +// Copyright (C) 2019-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -1175,7 +1175,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } } else if (reason === UpdateReasons.IMAGE_MOVED) { this.moveCanvas(); - } else if ([UpdateReasons.OBJECTS_UPDATED].includes(reason)) { + } else if (reason === UpdateReasons.OBJECTS_UPDATED) { if (this.mode === Mode.GROUP) { this.groupHandler.resetSelectedObjects(); } @@ -1443,6 +1443,7 @@ export class CanvasViewImpl implements CanvasView, Listener { clientID: state.clientID, outside: state.outside, occluded: state.occluded, + source: state.source, hidden: state.hidden, lock: state.lock, shapeType: state.shapeType, @@ -1534,13 +1535,22 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - for (const attrID of Object.keys(state.attributes)) { - if (state.attributes[attrID] !== drawnState.attributes[+attrID]) { - if (text) { - const [span] = (text.node.querySelectorAll(`[attrID="${attrID}"]`) as any) as SVGTSpanElement[]; - if (span && span.textContent) { - const prefix = span.textContent.split(':').slice(0, -1).join(':'); - span.textContent = `${prefix}: ${state.attributes[attrID]}`; + if (drawnState.label.id !== state.label.id) { + // need to remove created text and create it again + if (text) { + text.remove(); + this.svgTexts[state.clientID] = this.addText(state); + } + } else { + // check if there are updates in attributes + for (const attrID of Object.keys(state.attributes)) { + if (state.attributes[attrID] !== drawnState.attributes[+attrID]) { + if (text) { + const [span] = text.node.querySelectorAll(`[attrID="${attrID}"]`); + if (span && span.textContent) { + const prefix = span.textContent.split(':').slice(0, -1).join(':'); + span.textContent = `${prefix}: ${state.attributes[attrID]}`; + } } } } diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 55790f05..9ffa080e 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -41,6 +41,7 @@ export interface DrawnState { occluded?: boolean; hidden?: boolean; lock: boolean; + source: 'AUTO' | 'MANUAL'; shapeType: string; points?: number[]; attributes: Record; @@ -176,5 +177,7 @@ export function scalarProduct(a: Vector2D, b: Vector2D): number { } export function vectorLength(vector: Vector2D): number { - return Math.sqrt((vector.i ** 2) + (vector.j ** 2)); + const sqrI = vector.i ** 2; + const sqrJ = vector.j ** 2; + return Math.sqrt(sqrI + sqrJ); } diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index f4cc563d..cb3424a9 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -13,38 +13,39 @@ } }, "@ant-design/icons": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.5.0.tgz", - "integrity": "sha512-ZAKJcmr4DBV3NWr8wm2dCxNKN4eFrX+qCaPsuFejP6FRsf+m5OKxvCVi9bSp1lmKWeOI5yECAx5s0uFm4QHuPw==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.6.2.tgz", + "integrity": "sha512-QsBG2BxBYU/rxr2eb8b2cZ4rPKAPBpzAR+0v6rrZLp/lnyvflLH3tw1vregK+M7aJauGWjIGNdFmUfpAOtw25A==", "requires": { "@ant-design/colors": "^6.0.0", "@ant-design/icons-svg": "^4.0.0", "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", - "insert-css": "^2.0.0", - "rc-util": "^5.0.1" + "rc-util": "^5.9.4" }, "dependencies": { - "@ant-design/colors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz", - "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==", + "@babel/runtime": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", "requires": { - "@ctrl/tinycolor": "^3.4.0" + "regenerator-runtime": "^0.13.4" } }, - "@babel/runtime": { - "version": "7.13.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.9.tgz", - "integrity": "sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA==", + "rc-util": { + "version": "5.9.8", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.9.8.tgz", + "integrity": "sha512-typLSHYGf5irvGLYQshs0Ra3aze086h0FhzsAkyirMunYZ7b3Te8gKa5PVaanoHaZa9sS6qx98BxgysoRP+6Tw==", "requires": { - "regenerator-runtime": "^0.13.4" + "@babel/runtime": "^7.12.5", + "react-is": "^16.12.0", + "shallowequal": "^1.1.0" } }, - "@ctrl/tinycolor": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz", - "integrity": "sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ==" + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "regenerator-runtime": { "version": "0.13.7", @@ -1301,9 +1302,9 @@ } }, "@types/react-dom": { - "version": "16.9.11", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.11.tgz", - "integrity": "sha512-3UuR4MoWf5spNgrG6cwsmT9DdRghcR4IDFOzNZ6+wcmacxkFykcb5ji0nNVm9ckBT4BCxvCrJJbM4+EYsEEVIg==", + "version": "16.9.12", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.12.tgz", + "integrity": "sha512-i7NPZZpPte3jtVOoW+eLB7G/jsX5OM6GqQnH+lC0nq0rqwlK0x8WcMEvYDgFWqWhWMlTltTimzdMax6wYfZssA==", "requires": { "@types/react": "^16" } @@ -1328,9 +1329,9 @@ } }, "@types/react-router": { - "version": "5.1.12", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.12.tgz", - "integrity": "sha512-0bhXQwHYfMeJlCh7mGhc0VJTRm0Gk+Z8T00aiP4702mDUuLs9SMhnd2DitpjWFjdOecx2UXtICK14H9iMnziGA==", + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.13.tgz", + "integrity": "sha512-ZIuaO9Yrln54X6elg8q2Ivp6iK6p4syPsefEYAhRDAoqNh48C8VYUmB9RkXjKSQAJSJV0mbIFCX7I4vZDcHrjg==", "requires": { "@types/history": "*", "@types/react": "*" @@ -24869,11 +24870,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "insert-css": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz", - "integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ=" - }, "internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -28959,11 +28955,12 @@ "integrity": "sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww==" }, "react-redux": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz", - "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.3.tgz", + "integrity": "sha512-ZhAmQ1lrK+Pyi0ZXNMUZuYxYAZd59wFuVDGUt536kSGdD0ya9Q7BfsE95E3TsFLE3kOSFp5m6G5qbatE+Ic1+w==", "requires": { "@babel/runtime": "^7.12.1", + "@types/react-redux": "^7.1.16", "hoist-non-react-statics": "^3.3.2", "loose-envify": "^1.4.0", "prop-types": "^15.7.2", @@ -28971,9 +28968,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", "requires": { "regenerator-runtime": "^0.13.4" } diff --git a/cvat-ui/package.json b/cvat-ui/package.json index caae6812..e0854271 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -48,14 +48,14 @@ "worker-loader": "^2.0.0" }, "dependencies": { - "@ant-design/icons": "^4.5.0", + "@ant-design/icons": "^4.6.2", "@types/lodash": "^4.14.168", "@types/platform": "^1.3.3", "@types/react": "^16.14.5", "@types/react-color": "^3.0.4", - "@types/react-dom": "^16.9.11", + "@types/react-dom": "^16.9.12", "@types/react-redux": "^7.1.16", - "@types/react-router": "^5.1.12", + "@types/react-router": "^5.1.13", "@types/react-router-dom": "^5.1.7", "@types/react-share": "^3.0.3", "@types/redux-logger": "^3.0.8", @@ -78,7 +78,7 @@ "react-cookie": "^4.0.3", "react-dom": "^16.14.0", "react-moment": "^1.1.1", - "react-redux": "^7.2.2", + "react-redux": "^7.2.3", "react-resizable": "^1.11.1", "@types/react-resizable": "^1.7.2", "react-router": "^5.1.0", diff --git a/cvat/apps/documentation/installation_automatic_annotation.md b/cvat/apps/documentation/installation_automatic_annotation.md index 1410876f..3203f96d 100644 --- a/cvat/apps/documentation/installation_automatic_annotation.md +++ b/cvat/apps/documentation/installation_automatic_annotation.md @@ -60,10 +60,10 @@ ```bash nuctl deploy --project-name cvat \ - --path `pwd`/tensorflow/matterport/mask_rcnn/nuclio \ + --path serverless/tensorflow/matterport/mask_rcnn/nuclio \ --platform local --base-image tensorflow/tensorflow:1.15.5-gpu-py3 \ --desc "GPU based implementation of Mask RCNN on Python 3, Keras, and TensorFlow." \ - --image cvat/tf.matterport.mask_rcnn_gpu + --image cvat/tf.matterport.mask_rcnn_gpu \ --triggers '{"myHttpTrigger": {"maxWorkers": 1}}' \ --resource-limit nvidia.com/gpu=1 ``` diff --git a/cvat/apps/documentation/static/documentation/images/image062.jpg b/cvat/apps/documentation/static/documentation/images/image062.jpg index 22d87bbe..3f4060f2 100644 Binary files a/cvat/apps/documentation/static/documentation/images/image062.jpg and b/cvat/apps/documentation/static/documentation/images/image062.jpg differ diff --git a/cvat/apps/documentation/static/documentation/images/image210.jpg b/cvat/apps/documentation/static/documentation/images/image210.jpg new file mode 100644 index 00000000..9c0edb6f Binary files /dev/null and b/cvat/apps/documentation/static/documentation/images/image210.jpg differ diff --git a/cvat/apps/documentation/static/documentation/images/image211.jpg b/cvat/apps/documentation/static/documentation/images/image211.jpg new file mode 100644 index 00000000..61e2b761 Binary files /dev/null and b/cvat/apps/documentation/static/documentation/images/image211.jpg differ diff --git a/cvat/apps/documentation/user_guide.md b/cvat/apps/documentation/user_guide.md index bc21e2e5..9406756d 100644 --- a/cvat/apps/documentation/user_guide.md +++ b/cvat/apps/documentation/user_guide.md @@ -982,6 +982,20 @@ this way you will change the label color for all jobs in the task. ![](static/documentation/images/image062.jpg) +**Fast label change** +You can change the label of an object using hot keys. In order to do it, you need to assign a number (from 0 to 9) to labels. By default numbers 1,2...0 are assigned to the first ten labels. + To assign a number, click on the button placed at the right of a label name on the sidebar. + +![](static/documentation/images/image210.jpg) + +After that you will be able to assign a corresponding label to an object + by hovering your mouse cursor over it and pressing `Ctrl + Num(0..9)`. + +In case you do not point the cursor to the object, pressing `Ctrl + Num(0..9)` will set a chosen label as default, + so that the next object you create (use `N` key) will automatically have this label assigned. + +![](static/documentation/images/image211.jpg) + --- #### Appearance @@ -1795,6 +1809,7 @@ Many UI elements have shortcut hints. Put your pointer to a required element to | `Ctrl+V` | Paste a shape from internal CVAT clipboard | | Hold `Ctrl` while pasting | When pasting shape from the buffer for multiple pasting. | | `Crtl+B` | Make a copy of the object on the following frames | +| `Ctrl+Num(0..9)` | Сhanges the object label if pressed while the cursor is pointed on the object 
/ changes default label if pressed while the cursor is not pointed on an object| | | _Operations are available only for track_ | | `K` | Change keyframe property for an active track | | `O` | Change outside property for an active track | diff --git a/tests/cypress/integration/actions_tasks_objects/case_72_hotkeys_change_labels.js b/tests/cypress/integration/actions_tasks_objects/case_72_hotkeys_change_labels.js new file mode 100644 index 00000000..ac195698 --- /dev/null +++ b/tests/cypress/integration/actions_tasks_objects/case_72_hotkeys_change_labels.js @@ -0,0 +1,147 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +/// + +context('Hotkeys to change labels feature.', () => { + const caseId = '72'; + const labelName = `Case ${caseId}`; + const taskName = labelName; + const attrName = `Attr for ${labelName}`; + const textDefaultValue = 'Some default value for type Text'; + const imagesCount = 1; + const imageFileName = `image_${labelName.replace(' ', '_').toLowerCase()}`; + const width = 800; + const height = 800; + const posX = 10; + const posY = 10; + const color = 'gray'; + const archiveName = `${imageFileName}.zip`; + const archivePath = `cypress/fixtures/${archiveName}`; + const imagesFolder = `cypress/fixtures/${imageFileName}`; + const directoryToArchive = imagesFolder; + const secondLabel = `Case ${caseId} second` + let firstLabelCurrentVal = ''; + let secondLabelCurrentVal = ''; + + function testCheckingAlwaysShowObjectDetails(check) { + cy.openSettings(); + cy.get('.cvat-settings-modal').within(() => { + cy.contains('Workspace').click(); + cy.get('.cvat-workspace-settings-show-text-always').within(() => { + check + ? cy.get('[type="checkbox"]').check().should('be.checked') + : cy.get('[type="checkbox"]').uncheck().should('not.be.checked'); + }); + }); + cy.closeSettings(); + } + + before(() => { + cy.visit('auth/login'); + cy.login(); + cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); + cy.createZipArchive(directoryToArchive, archivePath); + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName); + cy.openTask(taskName); + cy.addNewLabel(secondLabel); + cy.openJob(); + }); + + after(() => { + cy.goToTaskList(); + cy.deleteTask(taskName); + }); + + describe(`Testing case "${caseId}"`, () => { + // Collect labels text. Since the server can return them in reverse order. + it('Collect label values relative to hotkeys.', () => { + cy.get('.cvat-objects-sidebar-tabs').within(() => { + cy.contains('[role="tab"]', 'Labels').click(); + }); + cy.get('.cvat-objects-sidebar-label-item').then(($objectsSidebarLabelItem) => { + firstLabelCurrentVal = $objectsSidebarLabelItem[0].innerText.slice(0, -2); + secondLabelCurrentVal = $objectsSidebarLabelItem[1].innerText.slice(0, -2); + }); + cy.get('.cvat-objects-sidebar-tabs').within(() => { + cy.contains('[role="tab"]', 'Objects').click(); + }); + }); + + it('Changing a label for a shape using hotkey.', () => { + const createPolygonShape = { + reDraw: false, + type: 'Shape', + labelName: firstLabelCurrentVal, + pointsMap: [ + { x: 200, y: 200 }, + { x: 300, y: 200 }, + { x: 300, y: 300 }, + ], + complete: true, + numberOfPoints: null, + }; + // Set settings "Always show object details" to check issue 3083 + testCheckingAlwaysShowObjectDetails(true); + cy.createPolygon(createPolygonShape); + cy.get('#cvat-objects-sidebar-state-item-1').find('.cvat-objects-sidebar-state-item-label-selector').should('have.text', firstLabelCurrentVal); + cy.get('.cvat-canvas-container').click(270, 260); + cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); + cy.contains('tspan', `${firstLabelCurrentVal} 1 (manual)`).should('be.visible'); + cy.get('body').type('{Ctrl}2') + cy.get('#cvat-objects-sidebar-state-item-1').find('.cvat-objects-sidebar-state-item-label-selector').should('have.text', secondLabelCurrentVal); + cy.contains('tspan', `${secondLabelCurrentVal} 1 (manual)`).should('be.visible'); + // Unset settings "Always show object details" + testCheckingAlwaysShowObjectDetails(); + }); + + it('Changing default label before drawing a shape.', () => { + cy.interactControlButton('draw-rectangle'); + cy.switchLabel(firstLabelCurrentVal, 'draw-rectangle'); + cy.get('.cvat-draw-rectangle-popover-visible').within(() => { + cy.contains('button', 'Shape').click(); + }); + cy.get('body').type('{Ctrl}2'); + cy.contains(`Default label was changed to "${secondLabelCurrentVal}"`).should('exist'); + cy.get('.cvat-canvas-container').click(500, 500).click(600, 600); + cy.get('#cvat-objects-sidebar-state-item-2').find('.cvat-objects-sidebar-state-item-label-selector').should('have.text', secondLabelCurrentVal); + }); + + it('Check changing shortcut for a label.', () => { + // Go to a labels tab + cy.get('.cvat-objects-sidebar-tabs').within(() => { + cy.contains('[role="tab"]', 'Labels').click(); + }); + cy.contains('.cvat-label-item-setup-shortcut-button', '1').click(); + cy.get('.cvat-label-item-setup-shortcut-popover').should('be.visible').within(() => { + cy.get('[type="button"]').then(($btn) => { + expect($btn[0].innerText).to.be.equal(`1:${firstLabelCurrentVal}`); + expect($btn[1].innerText).to.be.equal(`2:${secondLabelCurrentVal}`); + expect($btn[2].innerText).to.be.equal('3:None'); + // Click to "3" button + cy.get($btn[2]).click(); + }); + }); + cy.get('.cvat-label-item-setup-shortcut-popover').should('be.visible').within(() => { + cy.get('[type="button"]').then(($btn) => { + // Buttons 1 and 3 have changed values + expect($btn[0].innerText).to.be.equal('1:None'); + expect($btn[2].innerText).to.be.equal(`3:${firstLabelCurrentVal}`); + }); + }); + cy.contains('.cvat-label-item-setup-shortcut-button', '3').should('exist'); + cy.get('.cvat-canvas-container').click(); // Hide shortcut popover + // Go to "Objects" tab + cy.get('.cvat-objects-sidebar-tabs').within(() => { + cy.contains('[role="tab"]', 'Objects').click(); + }); + // Checking the label change via the new hotkey value + cy.get('.cvat-canvas-container').click(270, 260); + cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); + cy.get('body').type('{Ctrl}3'); + cy.contains('tspan', `${firstLabelCurrentVal} 1 (manual)`).should('be.visible'); + cy.get('#cvat-objects-sidebar-state-item-1').find('.cvat-objects-sidebar-state-item-label-selector').should('have.text', firstLabelCurrentVal); + }); + }); +});