Added options to switch text font size and position (#3972)

main
Boris Sekachev 4 years ago committed by GitHub
parent c79a4c1584
commit ab351c2aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Google Cloud Storage support in UI (<https://github.com/openvinotoolkit/cvat/pull/3919>)
- Add project tasks paginations (<https://github.com/openvinotoolkit/cvat/pull/3910>)
- Add remove issue button (<https://github.com/openvinotoolkit/cvat/pull/3952>)
- Options to change font size & position of text labels on the canvas (<https://github.com/openvinotoolkit/cvat/pull/3972>)
### Changed
- TDB

@ -1,21 +1,35 @@
{
"name": "cvat-canvas",
"version": "2.10.0",
"version": "2.10.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas",
"version": "2.10.0",
"version": "2.10.1",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
},
"devDependencies": {}
}
},
"node_modules/@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"node_modules/polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"dependencies": {
"tinyqueue": "^2.0.3"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
@ -77,9 +91,27 @@
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
},
"dependencies": {
"@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"requires": {
"tinyqueue": "^2.0.3"
}
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
@ -127,6 +159,11 @@
"requires": {
"svg.js": "^2.6.5"
}
},
"tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
}
}

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.10.0",
"version": "2.10.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
@ -15,8 +15,9 @@
"not IE 11",
"> 2%"
],
"devDependencies": {},
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",

@ -37,7 +37,6 @@ polyline.cvat_shape_drawing_opacity {
.cvat_canvas_text {
font-weight: bold;
font-size: 1.2em;
fill: white;
cursor: default;
font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif;
@ -47,7 +46,6 @@ polyline.cvat_shape_drawing_opacity {
}
.cvat_canvas_text_description {
font-size: 14px;
fill: yellow;
font-style: oblique 40deg;
}

@ -55,6 +55,8 @@ export interface Configuration {
smoothImage?: boolean;
autoborders?: boolean;
displayAllText?: boolean;
textFontSize?: number;
textPosition?: 'auto' | 'center';
undefinedAttrValue?: string;
showProjections?: boolean;
forceDisableEditing?: boolean;
@ -647,6 +649,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof configuration.textFontSize === 'number') {
this.data.configuration.textFontSize = configuration.textFontSize;
}
if (['auto', 'center'].includes(configuration.textPosition)) {
this.data.configuration.textPosition = configuration.textPosition;
}
if (typeof configuration.showProjections === 'boolean') {
this.data.configuration.showProjections = configuration.showProjections;
}

@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: MIT
import polylabel from 'polylabel';
import * as SVG from 'svg.js';
import 'svg.draggable.js';
@ -683,11 +684,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
delete this.svgTexts[state.clientID];
}
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
delete this.svgShapes[state.clientID];
}
this.addObjects(created);
@ -1175,7 +1178,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const i in this.drawnStates) {
if (!(i in this.svgTexts)) {
this.svgTexts[i] = this.addText(this.drawnStates[i]);
this.updateTextPosition(this.svgTexts[i], this.svgShapes[i]);
}
}
} else if (configuration.displayAllText === false && this.configuration.displayAllText) {
@ -1187,15 +1189,25 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
if ('smoothImage' in configuration) {
if (configuration.smoothImage) {
this.background.classList.remove('cvat_canvas_pixelized');
} else {
this.background.classList.add('cvat_canvas_pixelized');
}
const updateTextPosition = configuration.displayAllText !== this.configuration.displayAllText ||
configuration.textFontSize !== this.configuration.textFontSize ||
configuration.textPosition !== this.configuration.textPosition;
if (configuration.smoothImage === true) {
this.background.classList.remove('cvat_canvas_pixelized');
} else if (configuration.smoothImage === false) {
this.background.classList.add('cvat_canvas_pixelized');
}
this.configuration = configuration;
if (updateTextPosition) {
for (const i in this.drawnStates) {
if (i in this.svgTexts) {
this.updateTextPosition(this.svgTexts[i], this.svgShapes[i]);
}
}
}
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
@ -2059,45 +2071,82 @@ 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 { rotation } = shape.transform();
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
const [x1, y1, x2, y2]: number[] = translateFromSVG(this.content, [
box.x,
box.y,
box.x + box.width,
box.y + box.height,
]);
const textFontSize = this.configuration.textFontSize || consts.DEFAULT_SHAPE_TEXT_SIZE;
const textPosition = this.configuration.textPosition || 'auto';
box = {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
text.untransform();
text.style({ 'font-size': textFontSize });
const { rotation } = shape.transform();
// Find the best place for a text
let [clientX, clientY]: number[] = [box.x + box.width, box.y];
if (
clientX + ((text.node as any) as SVGTextElement)
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth
) {
[clientX, clientY] = [box.x, box.y];
let [clientX, clientY, clientCX, clientCY]: number[] = [0, 0, 0, 0];
if (textPosition === 'center') {
let cx = 0;
let cy = 0;
if (shape.type === 'rect') {
// for rectangle finding a center is simple
cx = +shape.attr('x') + +shape.attr('width') / 2;
cy = +shape.attr('y') + +shape.attr('height') / 2;
} else {
// for polyshapes we use special algorithm
const points = parsePoints(pointsToNumberArray(shape.attr('points')));
[cx, cy] = polylabel([points.map((point) => [point.x, point.y])]);
}
[clientX, clientY] = translateFromSVG(this.content, [cx, cy]);
// center is exactly clientX, clientY
clientCX = clientX;
clientCY = clientY;
} else {
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
const [x1, y1, x2, y2]: number[] = translateFromSVG(this.content, [
box.x,
box.y,
box.x + box.width,
box.y + box.height,
]);
clientCX = x1 + (x2 - x1) / 2;
clientCY = y1 + (y2 - y1) / 2;
box = {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
// first try to put to the top right corner
[clientX, clientY] = [box.x + box.width, box.y];
if (
clientX + ((text.node as any) as SVGTextElement)
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth
) {
// if out of visible area, try to put text to top left corner
[clientX, clientY] = [box.x, box.y];
}
}
// Translate back to text SVG
const [x, y, cx, cy]: number[] = translateToSVG(this.text, [
// Translate found coordinates to text SVG
const [x, y, rotX, rotY]: number[] = translateToSVG(this.text, [
clientX + consts.TEXT_MARGIN,
clientY + consts.TEXT_MARGIN,
x1 + (x2 - x1) / 2,
y1 + (y2 - y1) / 2,
clientCX,
clientCY,
]);
const textBBox = ((text.node as any) as SVGTextElement).getBBox();
// Finally draw a text
text.move(x, y);
if (textPosition === 'center') {
text.move(x - textBBox.width / 2, y - textBBox.height / 2);
} else {
text.move(x, y);
}
if (rotation) {
text.rotate(rotation, cx, cy);
text.rotate(rotation, rotX, rotY);
}
for (const tspan of (text.lines() as any).members) {
@ -2107,6 +2156,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private addText(state: any): SVG.Text {
const { undefinedAttrValue } = this.configuration;
const textFontSize = this.configuration.textFontSize || 12;
const {
label, clientID, attributes, source, descriptions,
} = state;
@ -2117,7 +2167,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.adoptedText
.text((block): void => {
block.tspan(`${label.name} ${clientID} (${source})`).style('text-transform', 'uppercase');
block.tspan(`${label.name} ${clientID} (${source})`).style({
'text-transform': 'uppercase',
});
for (const desc of descriptions) {
block
.tspan(`${desc}`)
@ -2140,6 +2192,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
})
.move(0, 0)
.style({ 'font-size': textFontSize })
.addClass('cvat_canvas_text');
}

@ -19,6 +19,7 @@ const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' +
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;
export default {
BASE_STROKE_WIDTH,
@ -37,4 +38,5 @@ export default {
BASE_PATTERN_SIZE,
SNAP_TO_ANGLE_RESIZE_DEFAULT,
SNAP_TO_ANGLE_RESIZE_SHIFT,
DEFAULT_SHAPE_TEXT_SIZE,
};

@ -23,6 +23,8 @@ export enum SettingsActionTypes {
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM',
SWITCH_SMOOTH_IMAGE = 'SWITCH_SMOOTH_IMAGE',
SWITCH_TEXT_FONT_SIZE = 'SWITCH_TEXT_FONT_SIZE',
SWITCH_TEXT_POSITION = 'SWITCH_TEXT_POSITION',
CHANGE_BRIGHTNESS_LEVEL = 'CHANGE_BRIGHTNESS_LEVEL',
CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL',
CHANGE_SATURATION_LEVEL = 'CHANGE_SATURATION_LEVEL',
@ -176,6 +178,24 @@ export function switchSmoothImage(enabled: boolean): AnyAction {
};
}
export function switchTextFontSize(fontSize: number): AnyAction {
return {
type: SettingsActionTypes.SWITCH_TEXT_FONT_SIZE,
payload: {
fontSize,
},
};
}
export function switchTextPosition(position: 'auto' | 'center'): AnyAction {
return {
type: SettingsActionTypes.SWITCH_TEXT_POSITION,
payload: {
position,
},
};
}
export function changeBrightnessLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL,

@ -60,6 +60,8 @@ interface Props {
smoothImage: boolean;
aamZoomMargin: number;
showObjectsTextAlways: boolean;
textFontSize: number;
textPosition: 'auto' | 'center';
showAllInterpolationTracks: boolean;
workspace: Workspace;
automaticBordering: boolean;
@ -107,6 +109,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
showProjections,
selectedOpacity,
smoothImage,
textFontSize,
textPosition,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
@ -124,6 +128,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
intelligentPolygonCrop,
showProjections,
creationOpacity: selectedOpacity,
textFontSize,
textPosition,
});
this.initialSetup();
@ -158,6 +164,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
workspace,
frameFetching,
showObjectsTextAlways,
textFontSize,
textPosition,
showAllInterpolationTracks,
automaticBordering,
intelligentPolygonCrop,
@ -172,7 +180,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
prevProps.showProjections !== showProjections ||
prevProps.intelligentPolygonCrop !== intelligentPolygonCrop ||
prevProps.selectedOpacity !== selectedOpacity ||
prevProps.smoothImage !== smoothImage
prevProps.smoothImage !== smoothImage ||
prevProps.textFontSize !== textFontSize ||
prevProps.textPosition !== textPosition
) {
canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
@ -182,6 +192,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
intelligentPolygonCrop,
creationOpacity: selectedOpacity,
smoothImage,
textFontSize,
textPosition,
});
}

@ -29,11 +29,12 @@
.cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-interpolated,
.cvat-workspace-settings-approx-poly-threshold,
.cvat-workspace-settings-aam-zoom-margin {
margin-bottom: 25px;
.cvat-workspace-settings-aam-zoom-margin,
.cvat-workspace-settings-text-settings {
margin-bottom: $grid-unit-size * 3;
> div:first-child {
margin-bottom: 10px;
margin-bottom: $grid-unit-size;
}
}
@ -48,7 +49,7 @@
.cvat-player-settings-canvas-background,
.cvat-workspace-settings-aam-zoom-margin,
.cvat-workspace-settings-auto-save-interval {
margin-bottom: 25px;
margin-bottom: $grid-unit-size * 3;
}
.cvat-player-settings-step,

@ -15,6 +15,7 @@ import {
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;
@ -25,6 +26,8 @@ interface Props {
automaticBordering: boolean;
intelligentPolygonCrop: boolean;
defaultApproxPolyAccuracy: number;
textFontSize: number;
textPosition: 'center' | 'auto';
onSwitchAutoSave(enabled: boolean): void;
onChangeAutoSaveInterval(interval: number): void;
onChangeAAMZoomMargin(margin: number): void;
@ -33,6 +36,8 @@ interface Props {
onSwitchShowingObjectsTextAlways(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
onSwitchIntelligentPolygonCrop(enabled: boolean): void;
onChangeTextFontSize(fontSize: number): void;
onChangeTextPosition(position: 'auto' | 'center'): void;
}
function WorkspaceSettingsComponent(props: Props): JSX.Element {
@ -45,6 +50,8 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element {
automaticBordering,
intelligentPolygonCrop,
defaultApproxPolyAccuracy,
textFontSize,
textPosition,
onSwitchAutoSave,
onChangeAutoSaveInterval,
onChangeAAMZoomMargin,
@ -53,6 +60,8 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element {
onSwitchAutomaticBordering,
onSwitchIntelligentPolygonCrop,
onChangeDefaultApproxPolyAccuracy,
onChangeTextFontSize,
onChangeTextPosition,
} = props;
const minAutoSaveInterval = 1;
@ -128,6 +137,23 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-text-settings'>
<Col span={12}>
<Text>Position of a text</Text>
</Col>
<Col span={12}>
<Text>Font size of a text</Text>
</Col>
<Col span={12}>
<Select value={textPosition} onChange={onChangeTextPosition}>
<Select.Option value='auto'>Auto</Select.Option>
<Select.Option value='center'>Center</Select.Option>
</Select>
</Col>
<Col span={12}>
<InputNumber onChange={onChangeTextFontSize} min={8} max={20} value={textFontSize} />
</Col>
</Row>
<Row className='cvat-workspace-settings-autoborders'>
<Col span={24}>
<Checkbox

@ -83,6 +83,8 @@ interface StateToProps {
smoothImage: boolean;
aamZoomMargin: number;
showObjectsTextAlways: boolean;
textFontSize: number;
textPosition: 'auto' | 'center';
showAllInterpolationTracks: boolean;
workspace: Workspace;
minZLayer: number;
@ -164,6 +166,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
showAllInterpolationTracks,
automaticBordering,
intelligentPolygonCrop,
textFontSize,
textPosition,
},
shapes: {
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
@ -210,6 +214,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
smoothImage,
aamZoomMargin,
showObjectsTextAlways,
textFontSize,
textPosition,
showAllInterpolationTracks,
curZLayer,
minZLayer,

@ -14,6 +14,8 @@ import {
switchAutomaticBordering,
switchIntelligentPolygonCrop,
changeDefaultApproxPolyAccuracy,
switchTextFontSize,
switchTextPosition,
} from 'actions/settings-actions';
import { CombinedState } from 'reducers/interfaces';
@ -29,6 +31,8 @@ interface StateToProps {
defaultApproxPolyAccuracy: number;
automaticBordering: boolean;
intelligentPolygonCrop: boolean;
textFontSize: number;
textPosition: 'auto' | 'center';
}
interface DispatchToProps {
@ -40,6 +44,8 @@ interface DispatchToProps {
onSwitchAutomaticBordering(enabled: boolean): void;
onSwitchIntelligentPolygonCrop(enabled: boolean): void;
onChangeDefaultApproxPolyAccuracy(approxPolyAccuracy: number): void;
onChangeTextFontSize(fontSize: number): void;
onChangeTextPosition(position: 'auto' | 'center'): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -53,6 +59,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
automaticBordering,
intelligentPolygonCrop,
defaultApproxPolyAccuracy,
textFontSize,
textPosition,
} = workspace;
return {
@ -64,6 +72,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
automaticBordering,
intelligentPolygonCrop,
defaultApproxPolyAccuracy,
textFontSize,
textPosition,
};
}
@ -93,6 +103,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onChangeDefaultApproxPolyAccuracy(threshold: number): void {
dispatch(changeDefaultApproxPolyAccuracy(threshold));
},
onChangeTextFontSize(fontSize: number): void {
dispatch(switchTextFontSize(fontSize));
},
onChangeTextPosition(position: 'auto' | 'center'): void {
dispatch(switchTextPosition(position));
},
};
}

@ -640,6 +640,8 @@ export interface WorkspaceSettingsState {
intelligentPolygonCrop: boolean;
defaultApproxPolyAccuracy: number;
toolsBlockerState: ToolsBlockerState;
textFontSize: number;
textPosition: 'auto' | 'center';
}
export interface ShapesSettingsState {

@ -32,6 +32,8 @@ const defaultState: SettingsState = {
showAllInterpolationTracks: false,
intelligentPolygonCrop: true,
defaultApproxPolyAccuracy: 9,
textFontSize: 14,
textPosition: 'auto',
toolsBlockerState: {
algorithmsLocked: false,
buttonVisible: false,
@ -193,6 +195,24 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.SWITCH_TEXT_FONT_SIZE: {
return {
...state,
workspace: {
...state.workspace,
textFontSize: action.payload.fontSize,
},
};
}
case SettingsActionTypes.SWITCH_TEXT_POSITION: {
return {
...state,
workspace: {
...state.workspace,
textPosition: action.payload.position,
},
};
}
case SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL: {
return {
...state,

Loading…
Cancel
Save