diff --git a/CHANGELOG.md b/CHANGELOG.md index 0062678f502cbbd7f4f8067e4b6214ed4ee645a4..afb997fece2158f7fc5424748873190f2df19ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Data sorting option () - Options to change font size & position of text labels on the canvas () - Add "tag" return type for automatic annotation in Nuclio () +- User is able to customize information that text labels show () ### Changed - TDB diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 87500d87bfad68cc11e6646af55476ea71c93286..94ea84c02604b20fed2a5c7ae6d382c23c5643f2 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-canvas", - "version": "2.10.2", + "version": "2.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-canvas", - "version": "2.10.2", + "version": "2.11.0", "license": "MIT", "dependencies": { "@types/polylabel": "^1.0.5", diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 9b18633fb20b83e07044c5e6342310a653d02f97..1654fabdddfe62a0cedf875eeafea7669ae3ef24 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.10.2", + "version": "2.11.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index daf8f935b71683e27e685b7fc0d11e166c6241b1..bb391f582bd6d44f90ab2b4dd9e35ab2ec39b79f 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: MIT +import consts from './consts'; import { MasterImpl } from './master'; export interface Size { @@ -57,6 +58,7 @@ export interface Configuration { displayAllText?: boolean; textFontSize?: number; textPosition?: 'auto' | 'center'; + textContent?: string; undefinedAttrValue?: string; showProjections?: boolean; forceDisableEditing?: boolean; @@ -263,6 +265,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { displayAllText: false, autoborders: false, undefinedAttrValue: '', + textContent: 'id,label,attributes,source,descriptions', + textPosition: 'auto', + textFontSize: consts.DEFAULT_SHAPE_TEXT_SIZE, }, imageBitmap: false, image: null, @@ -649,7 +654,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.configuration.displayAllText = configuration.displayAllText; } - if (typeof configuration.textFontSize === 'number') { + if (typeof configuration.textFontSize === 'number' && configuration.textFontSize >= consts.MINIMUM_TEXT_FONT_SIZE) { this.data.configuration.textFontSize = configuration.textFontSize; } @@ -657,6 +662,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.configuration.textPosition = configuration.textPosition; } + if (typeof configuration.textContent === 'string') { + const splitted = configuration.textContent.split(',').filter((entry: string) => !!entry); + if (splitted.every((entry: string) => ['id', 'label', 'attributes', 'source', 'descriptions'].includes(entry))) { + this.data.configuration.textContent = configuration.textContent; + } + } + if (typeof configuration.showProjections === 'boolean') { this.data.configuration.showProjections = configuration.showProjections; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4d04bfd7cbea313ca00d044b8cd550016c2c8fa9..220a82100eef5df867bab7cfa042b33ea85908b6 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1189,9 +1189,11 @@ export class CanvasViewImpl implements CanvasView, Listener { } } + const recreateText = configuration.textContent !== this.configuration.textContent; const updateTextPosition = configuration.displayAllText !== this.configuration.displayAllText || configuration.textFontSize !== this.configuration.textFontSize || - configuration.textPosition !== this.configuration.textPosition; + configuration.textPosition !== this.configuration.textPosition || + recreateText; if (configuration.smoothImage === true) { this.background.classList.remove('cvat_canvas_pixelized'); @@ -1200,6 +1202,19 @@ export class CanvasViewImpl implements CanvasView, Listener { } this.configuration = configuration; + if (recreateText) { + const states = this.controller.objects; + for (const key of Object.keys(this.drawnStates)) { + const clientID = +key; + const [state] = states.filter((_state: any) => _state.clientID === clientID); + if (clientID in this.svgTexts) { + this.svgTexts[clientID].remove(); + delete this.svgTexts[clientID]; + if (state) this.svgTexts[clientID] = this.addText(state); + } + } + } + if (updateTextPosition) { for (const i in this.drawnStates) { if (i in this.svgTexts) { @@ -2071,8 +2086,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // Update text position after corresponding box has been moved, resized, etc. private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { if (text.node.style.display === 'none') return; // wrong transformation matrix - const textFontSize = this.configuration.textFontSize || consts.DEFAULT_SHAPE_TEXT_SIZE; - const textPosition = this.configuration.textPosition || 'auto'; + const { textFontSize, textPosition } = this.configuration; text.untransform(); text.style({ 'font-size': `${textFontSize}px` }); @@ -2131,8 +2145,8 @@ export class CanvasViewImpl implements CanvasView, Listener { // Translate found coordinates to text SVG const [x, y, rotX, rotY]: number[] = translateToSVG(this.text, [ - clientX + consts.TEXT_MARGIN, - clientY + consts.TEXT_MARGIN, + clientX + (textPosition === 'auto' ? consts.TEXT_MARGIN : 0), + clientY + (textPosition === 'auto' ? consts.TEXT_MARGIN : 0), clientCX, clientCY, ]); @@ -2156,6 +2170,13 @@ export class CanvasViewImpl implements CanvasView, Listener { private addText(state: any): SVG.Text { const { undefinedAttrValue } = this.configuration; + const content = this.configuration.textContent; + const withID = content.includes('id'); + const withAttr = content.includes('attributes'); + const withLabel = content.includes('label'); + const withSource = content.includes('source'); + const withDescriptions = content.includes('descriptions'); + const textFontSize = this.configuration.textFontSize || 12; const { label, clientID, attributes, source, descriptions, @@ -2167,28 +2188,32 @@ export class CanvasViewImpl implements CanvasView, Listener { return this.adoptedText .text((block): void => { - block.tspan(`${label.name} ${clientID} (${source})`).style({ + block.tspan(`${withLabel ? label.name : ''} ${withID ? clientID : ''} ${withSource ? `(${source})` : ''}`).style({ 'text-transform': 'uppercase', }); - for (const desc of descriptions) { - block - .tspan(`${desc}`) - .attr({ - dy: '1em', - x: 0, - }) - .addClass('cvat_canvas_text_description'); + if (withDescriptions) { + for (const desc of descriptions) { + block + .tspan(`${desc}`) + .attr({ + dy: '1em', + x: 0, + }) + .addClass('cvat_canvas_text_description'); + } } - for (const attrID of Object.keys(attributes)) { - const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]; - block - .tspan(`${attrNames[attrID]}: ${value}`) - .attr({ - attrID, - dy: '1em', - x: 0, - }) - .addClass('cvat_canvas_text_attribute'); + if (withAttr) { + for (const attrID of Object.keys(attributes)) { + const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]; + block + .tspan(`${attrNames[attrID]}: ${value}`) + .attr({ + attrID, + dy: '1em', + x: 0, + }) + .addClass('cvat_canvas_text_attribute'); + } } }) .move(0, 0) diff --git a/cvat-canvas/src/typescript/consts.ts b/cvat-canvas/src/typescript/consts.ts index a2b42f177929ba1daee94857e3a9ae105fbaa875..e382496f5a44faf0712706e4cfb7fbc39fa044c0 100644 --- a/cvat-canvas/src/typescript/consts.ts +++ b/cvat-canvas/src/typescript/consts.ts @@ -20,6 +20,7 @@ const BASE_PATTERN_SIZE = 5; const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1; const SNAP_TO_ANGLE_RESIZE_SHIFT = 15; const DEFAULT_SHAPE_TEXT_SIZE = 12; +const MINIMUM_TEXT_FONT_SIZE = 8; export default { BASE_STROKE_WIDTH, @@ -39,4 +40,5 @@ export default { SNAP_TO_ANGLE_RESIZE_DEFAULT, SNAP_TO_ANGLE_RESIZE_SHIFT, DEFAULT_SHAPE_TEXT_SIZE, + MINIMUM_TEXT_FONT_SIZE, }; diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index f9bad34c0436449195ebe8637d93450d79b0489b..ebe48e3d0a2b148d571765efa718f0f1724a5b1d 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-ui", - "version": "1.28.2", + "version": "1.29.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-ui", - "version": "1.28.2", + "version": "1.29.0", "license": "MIT", "dependencies": { "@ant-design/icons": "^4.6.3", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index fa806bb75039bef491d0c529cf4ac6fb026f8917..5030d6b13afc8157118b068664579200458c2d94 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.28.2", + "version": "1.29.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index d7d42097bc5c06b6c2bbf78d865ad4daf66c6118..56430553b14d5ceacfb43ac78570df9b8a04a3c1 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -25,6 +25,7 @@ export enum SettingsActionTypes { SWITCH_SMOOTH_IMAGE = 'SWITCH_SMOOTH_IMAGE', SWITCH_TEXT_FONT_SIZE = 'SWITCH_TEXT_FONT_SIZE', SWITCH_TEXT_POSITION = 'SWITCH_TEXT_POSITION', + SWITCH_TEXT_CONTENT = 'SWITCH_TEXT_CONTENT', CHANGE_BRIGHTNESS_LEVEL = 'CHANGE_BRIGHTNESS_LEVEL', CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL', CHANGE_SATURATION_LEVEL = 'CHANGE_SATURATION_LEVEL', @@ -196,6 +197,15 @@ export function switchTextPosition(position: 'auto' | 'center'): AnyAction { }; } +export function switchTextContent(textContent: string): AnyAction { + return { + type: SettingsActionTypes.SWITCH_TEXT_CONTENT, + payload: { + textContent, + }, + }; +} + export function changeBrightnessLevel(level: number): AnyAction { return { type: SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL, diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx index d6e53601f75f494a418e7fc6f1cc2dfd28f751df..4fb018cb107a8908afdbecd2a32e36910844746f 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx @@ -62,6 +62,7 @@ interface Props { showObjectsTextAlways: boolean; textFontSize: number; textPosition: 'auto' | 'center'; + textContent: string; showAllInterpolationTracks: boolean; workspace: Workspace; automaticBordering: boolean; @@ -111,6 +112,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { smoothImage, textFontSize, textPosition, + textContent, } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas }; @@ -130,6 +132,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { creationOpacity: selectedOpacity, textFontSize, textPosition, + textContent, }); this.initialSetup(); @@ -166,6 +169,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { showObjectsTextAlways, textFontSize, textPosition, + textContent, showAllInterpolationTracks, automaticBordering, intelligentPolygonCrop, @@ -182,7 +186,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { prevProps.selectedOpacity !== selectedOpacity || prevProps.smoothImage !== smoothImage || prevProps.textFontSize !== textFontSize || - prevProps.textPosition !== textPosition + prevProps.textPosition !== textPosition || + prevProps.textContent !== textContent ) { canvasInstance.configure({ undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, @@ -194,6 +199,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { smoothImage, textFontSize, textPosition, + textContent, }); } diff --git a/cvat-ui/src/components/header/settings-modal/styles.scss b/cvat-ui/src/components/header/settings-modal/styles.scss index 1b898e592893b2ea0e8acd79518ea110d87431d1..e8754ed6849da43226c4aa86503656f07310fe9a 100644 --- a/cvat-ui/src/components/header/settings-modal/styles.scss +++ b/cvat-ui/src/components/header/settings-modal/styles.scss @@ -38,6 +38,10 @@ } } +.cvat-workspace-settings-text-content { + width: 100%; +} + .cvat-workspace-settings-approx-poly-threshold { user-select: none; } diff --git a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx index aa721746e3a5458d0560cdc4fd1becd6f2d64602..7facb20ec1eab9a407efcbdef823e7919e0ceecc 100644 --- a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx +++ b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx @@ -9,13 +9,13 @@ import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; import InputNumber from 'antd/lib/input-number'; import Text from 'antd/lib/typography/Text'; import Slider from 'antd/lib/slider'; +import Select from 'antd/lib/select'; import { MAX_ACCURACY, marks, } from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy'; import { clamp } from 'utils/math'; -import { Select } from 'antd'; interface Props { autoSave: boolean; @@ -28,6 +28,7 @@ interface Props { defaultApproxPolyAccuracy: number; textFontSize: number; textPosition: 'center' | 'auto'; + textContent: string; onSwitchAutoSave(enabled: boolean): void; onChangeAutoSaveInterval(interval: number): void; onChangeAAMZoomMargin(margin: number): void; @@ -38,6 +39,7 @@ interface Props { onSwitchIntelligentPolygonCrop(enabled: boolean): void; onChangeTextFontSize(fontSize: number): void; onChangeTextPosition(position: 'auto' | 'center'): void; + onChangeTextContent(textContent: string[]): void; } function WorkspaceSettingsComponent(props: Props): JSX.Element { @@ -52,6 +54,7 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element { defaultApproxPolyAccuracy, textFontSize, textPosition, + textContent, onSwitchAutoSave, onChangeAutoSaveInterval, onChangeAAMZoomMargin, @@ -62,6 +65,7 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element { onChangeDefaultApproxPolyAccuracy, onChangeTextFontSize, onChangeTextPosition, + onChangeTextContent, } = props; const minAutoSaveInterval = 1; @@ -137,6 +141,25 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element { + + + Content of a text + + + + + Position of a text diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx index 3b0df0ce57699b7b176e9af075676252ce08b6b5..802bb4eaa11e4c4708c38cb3664370daf1dec359 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper.tsx @@ -85,6 +85,7 @@ interface StateToProps { showObjectsTextAlways: boolean; textFontSize: number; textPosition: 'auto' | 'center'; + textContent: string; showAllInterpolationTracks: boolean; workspace: Workspace; minZLayer: number; @@ -168,6 +169,7 @@ function mapStateToProps(state: CombinedState): StateToProps { intelligentPolygonCrop, textFontSize, textPosition, + textContent, }, shapes: { opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections, @@ -216,6 +218,7 @@ function mapStateToProps(state: CombinedState): StateToProps { showObjectsTextAlways, textFontSize, textPosition, + textContent, showAllInterpolationTracks, curZLayer, minZLayer, diff --git a/cvat-ui/src/containers/header/settings-modal/workspace-settings.tsx b/cvat-ui/src/containers/header/settings-modal/workspace-settings.tsx index b78036784edd048a29f0136df36d9b29143a6467..dbf26e8b739e206a4e4198e5c73cc1bfe7246547 100644 --- a/cvat-ui/src/containers/header/settings-modal/workspace-settings.tsx +++ b/cvat-ui/src/containers/header/settings-modal/workspace-settings.tsx @@ -16,6 +16,7 @@ import { changeDefaultApproxPolyAccuracy, switchTextFontSize, switchTextPosition, + switchTextContent, } from 'actions/settings-actions'; import { CombinedState } from 'reducers/interfaces'; @@ -33,6 +34,7 @@ interface StateToProps { intelligentPolygonCrop: boolean; textFontSize: number; textPosition: 'auto' | 'center'; + textContent: string; } interface DispatchToProps { @@ -46,6 +48,7 @@ interface DispatchToProps { onChangeDefaultApproxPolyAccuracy(approxPolyAccuracy: number): void; onChangeTextFontSize(fontSize: number): void; onChangeTextPosition(position: 'auto' | 'center'): void; + onChangeTextContent(textContent: string[]): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -61,6 +64,7 @@ function mapStateToProps(state: CombinedState): StateToProps { defaultApproxPolyAccuracy, textFontSize, textPosition, + textContent, } = workspace; return { @@ -74,6 +78,7 @@ function mapStateToProps(state: CombinedState): StateToProps { defaultApproxPolyAccuracy, textFontSize, textPosition, + textContent, }; } @@ -109,6 +114,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onChangeTextPosition(position: 'auto' | 'center'): void { dispatch(switchTextPosition(position)); }, + onChangeTextContent(textContent: string[]): void { + const serialized = textContent.join(','); + dispatch(switchTextContent(serialized)); + }, }; } diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index aeea45220fea2de35ed2f24db7017a364f6b59c3..ff83618b789850ce1dfa3fb4a9360ad22343191b 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -642,6 +642,7 @@ export interface WorkspaceSettingsState { toolsBlockerState: ToolsBlockerState; textFontSize: number; textPosition: 'auto' | 'center'; + textContent: string; } export interface ShapesSettingsState { diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index acac204e8aa4ab5d1e47d11f92f5d125f8f298d8..83eeb833c3344227427a0a95d6c6c95299e56ad1 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -34,6 +34,7 @@ const defaultState: SettingsState = { defaultApproxPolyAccuracy: 9, textFontSize: 14, textPosition: 'auto', + textContent: 'id,source,label,attributes,descriptions', toolsBlockerState: { algorithmsLocked: false, buttonVisible: false, @@ -213,6 +214,16 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case SettingsActionTypes.SWITCH_TEXT_CONTENT: { + const { textContent } = action.payload; + return { + ...state, + workspace: { + ...state.workspace, + textContent, + }, + }; + } case SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL: { return { ...state, diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 898b5d1dd68d75449b02282a5ee90fe96cb85898..c642a40c2c17bf5701b3ff2f214a1bd1286940fd 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -678,12 +678,12 @@ Cypress.Commands.add('checkFrameNum', (frameNum) => { }); Cypress.Commands.add('goToNextFrame', (expectedFrameNum) => { - cy.get('.cvat-player-next-button').click(); + cy.get('.cvat-player-next-button').click().trigger('mouseout'); cy.checkFrameNum(expectedFrameNum); }); Cypress.Commands.add('goToPreviousFrame', (expectedFrameNum) => { - cy.get('.cvat-player-previous-button').click(); + cy.get('.cvat-player-previous-button').click().trigger('mouseout'); cy.checkFrameNum(expectedFrameNum); });