Added rotated bounding boxes (#3832)

Co-authored-by: Maxim Zhiltsov <maxim.zhiltsov@intel.com>
main
Boris Sekachev 4 years ago committed by GitHub
parent 14262fa951
commit 7bab58e1a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -193,7 +193,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "jest debug", "name": "jest debug",
"program": "${workspaceFolder}/cvat-core/node_modules/.bin/jest", "program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [ "args": [
"--config", "--config",
"${workspaceFolder}/cvat-core/jest.config.js" "${workspaceFolder}/cvat-core/jest.config.js"

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add LFW format (<https://github.com/openvinotoolkit/cvat/pull/3770>) - Add LFW format (<https://github.com/openvinotoolkit/cvat/pull/3770>)
- Add Cityscapes format (<https://github.com/openvinotoolkit/cvat/pull/3758>) - Add Cityscapes format (<https://github.com/openvinotoolkit/cvat/pull/3758>)
- Add Open Images V6 format (<https://github.com/openvinotoolkit/cvat/pull/3679>) - Add Open Images V6 format (<https://github.com/openvinotoolkit/cvat/pull/3679>)
- Rotated bounding boxes (<https://github.com/openvinotoolkit/cvat/pull/3832>)
### Changed ### Changed
- TDB - TDB

@ -137,6 +137,10 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5; stroke-dasharray: 5;
} }
.svg_select_points_rot {
fill: white;
}
.cvat_canvas_shape .svg_select_points, .cvat_canvas_shape .svg_select_points,
.cvat_canvas_shape .cvat_canvas_cuboid_projections { .cvat_canvas_shape .cvat_canvas_cuboid_projections {
stroke-dasharray: none; stroke-dasharray: none;
@ -166,8 +170,9 @@ polyline.cvat_canvas_shape_splitting {
.cvat_canvas_removable_interaction_point { .cvat_canvas_removable_interaction_point {
cursor: cursor:
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K') url(
10 10, 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
auto; auto;
} }

@ -31,6 +31,7 @@ import {
vectorLength, vectorLength,
ShapeSizeElement, ShapeSizeElement,
DrawnState, DrawnState,
rotate2DPoints,
} from './shared'; } from './shared';
import { import {
CanvasModel, CanvasModel,
@ -85,6 +86,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private interactionHandler: InteractionHandler; private interactionHandler: InteractionHandler;
private activeElement: ActiveElement; private activeElement: ActiveElement;
private configuration: Configuration; private configuration: Configuration;
private snapToAngleResize: number;
private serviceFlags: { private serviceFlags: {
drawHidden: Record<number, boolean>; drawHidden: Record<number, boolean>;
}; };
@ -112,6 +114,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
return points.map((coord: number): number => coord - offset); return points.map((coord: number): number => coord - offset);
} }
private translatePointsFromRotatedShape(shape: SVG.Shape, points: number[]): number[] {
const { rotation } = shape.transform();
// currently shape is rotated and shifted somehow additionally (css transform property)
// let's remove rotation to get correct transformation matrix (element -> screen)
// correct means that we do not consider points to be rotated
// because rotation property is stored separately and already saved
shape.rotate(0);
const result = [];
try {
// get each point and apply a couple of matrix transformation to it
const point = this.content.createSVGPoint();
// matrix to convert from ELEMENT file system to CLIENT coordinate system
const ctm = ((shape.node as any) as SVGRectElement | SVGPolygonElement | SVGPolylineElement).getScreenCTM();
// matrix to convert from CLIENT coordinate system to CANVAS coordinate system
const ctm1 = this.content.getScreenCTM().inverse();
// NOTE: I tried to use element.getCTM(), but this way does not work on firefox
for (let i = 0; i < points.length; i += 2) {
point.x = points[i];
point.y = points[i + 1];
let transformedPoint = point.matrixTransform(ctm);
transformedPoint = transformedPoint.matrixTransform(ctm1);
result.push(transformedPoint.x, transformedPoint.y);
}
} finally {
shape.rotate(rotation);
}
return result;
}
private stringifyToCanvas(points: number[]): string { private stringifyToCanvas(points: number[]): string {
return points.reduce((acc: string, val: number, idx: number): string => { return points.reduce((acc: string, val: number, idx: number): string => {
if (idx % 2) { if (idx % 2) {
@ -199,7 +234,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void { private onDrawDone(data: any | null, duration: number, continueDraw?: boolean): void {
const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden).map((_clientID): number => +_clientID); const hiddenBecauseOfDraw = Object.keys(this.serviceFlags.drawHidden).map((_clientID): number => +_clientID);
if (hiddenBecauseOfDraw.length) { if (hiddenBecauseOfDraw.length) {
for (const hidden of hiddenBecauseOfDraw) { for (const hidden of hiddenBecauseOfDraw) {
@ -256,7 +291,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private onEditDone(state: any, points: number[]): void { private onEditDone(state: any, points: number[], rotation?: number): void {
if (state && points) { if (state && points) {
const event: CustomEvent = new CustomEvent('canvas.edited', { const event: CustomEvent = new CustomEvent('canvas.edited', {
bubbles: false, bubbles: false,
@ -264,6 +299,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
detail: { detail: {
state, state,
points, points,
rotation: typeof rotation === 'number' ? rotation : state.rotation,
}, },
}); });
@ -388,7 +424,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private onFindObject(e: MouseEvent): void { private onFindObject(e: MouseEvent): void {
if (e.which === 1 || e.which === 0) { if (e.button === 0) {
const { offset } = this.controller.geometry; const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]); const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.find', { const event: CustomEvent = new CustomEvent('canvas.find', {
@ -483,7 +519,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / this.geometry.scale}px`); this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / this.geometry.scale}px`);
// Transform all shape points // Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) { for (const element of [
...window.document.getElementsByClassName('svg_select_points'),
...window.document.getElementsByClassName('svg_select_points_rot'),
]) {
element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`); element.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`);
element.setAttribute('r', `${consts.BASE_POINT_SIZE / this.geometry.scale}`); element.setAttribute('r', `${consts.BASE_POINT_SIZE / this.geometry.scale}`);
} }
@ -744,12 +783,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
const pointID = Array.prototype.indexOf.call(
((e.target as HTMLElement).parentElement as HTMLElement).children,
e.target,
);
if (this.activeElement.clientID !== null) { if (this.activeElement.clientID !== null) {
const pointID = Array.prototype.indexOf.call(
((e.target as HTMLElement).parentElement as HTMLElement).children,
e.target,
);
const [state] = this.controller.objects.filter( const [state] = this.controller.objects.filter(
(_state: any): boolean => _state.clientID === this.activeElement.clientID, (_state: any): boolean => _state.clientID === this.activeElement.clientID,
); );
@ -821,13 +859,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault(); e.preventDefault();
}; };
const getGeometry = (): Geometry => this.geometry;
if (value) { if (value) {
const getGeometry = (): Geometry => this.geometry;
(shape as any).selectize(value, { (shape as any).selectize(value, {
deepSelect: true, deepSelect: true,
pointSize: (2 * consts.BASE_POINT_SIZE) / this.geometry.scale, pointSize: (2 * consts.BASE_POINT_SIZE) / this.geometry.scale,
rotationPoint: false, rotationPoint: shape.type === 'rect',
pointType(cx: number, cy: number): SVG.Circle { pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested const circle: SVG.Circle = this.nested
.circle(this.options.pointSize) .circle(this.options.pointSize)
@ -874,8 +911,45 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (handler && handler.nested) { if (handler && handler.nested) {
handler.nested.fill(shape.attr('fill')); handler.nested.fill(shape.attr('fill'));
} }
const [rotationPoint] = window.document.getElementsByClassName('svg_select_points_rot');
if (rotationPoint && !rotationPoint.children.length) {
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
title.textContent = 'Hold Shift to snap angle';
rotationPoint.appendChild(title);
}
} }
private onShiftKeyDown = (e: KeyboardEvent): void => {
if (!e.repeat && e.code.toLowerCase().includes('shift')) {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_SHIFT;
if (this.activeElement) {
const shape = this.svgShapes[this.activeElement.clientID];
if (shape && shape.hasClass('cvat_canvas_shape_activated')) {
(shape as any).resize({ snapToAngle: this.snapToAngleResize });
}
}
}
};
private onShiftKeyUp = (e: KeyboardEvent): void => {
if (e.code.toLowerCase().includes('shift') && this.activeElement) {
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
if (this.activeElement) {
const shape = this.svgShapes[this.activeElement.clientID];
if (shape && shape.hasClass('cvat_canvas_shape_activated')) {
(shape as any).resize({ snapToAngle: this.snapToAngleResize });
}
}
}
};
private onMouseUp = (event: MouseEvent): void => {
if (event.button === 0 || event.button === 1) {
this.controller.disableDrag();
}
};
public constructor(model: CanvasModel & Master, controller: CanvasController) { public constructor(model: CanvasModel & Master, controller: CanvasController) {
this.controller = controller; this.controller = controller;
this.geometry = controller.geometry; this.geometry = controller.geometry;
@ -889,6 +963,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}; };
this.configuration = model.configuration; this.configuration = model.configuration;
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
this.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
this.serviceFlags = { this.serviceFlags = {
drawHidden: {}, drawHidden: {},
}; };
@ -1046,11 +1121,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
}); });
window.document.addEventListener('mouseup', (event): void => { window.document.addEventListener('mouseup', this.onMouseUp);
if (event.which === 1 || event.which === 2) { window.document.addEventListener('keydown', this.onShiftKeyDown);
this.controller.disableDrag(); window.document.addEventListener('keyup', this.onShiftKeyUp);
}
});
this.content.addEventListener('wheel', (event): void => { this.content.addEventListener('wheel', (event): void => {
if (event.ctrlKey) return; if (event.ctrlKey) return;
@ -1365,9 +1438,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
cancelable: true, cancelable: true,
}), }),
); );
// We can't call namespaced svgjs event
// see - https://svgjs.dev/docs/2.7/events/ window.document.removeEventListener('keydown', this.onShiftKeyDown);
this.adoptedContent.fire('destroy'); window.document.removeEventListener('keyup', this.onShiftKeyUp);
window.document.removeEventListener('mouseup', this.onMouseUp);
this.interactionHandler.destroy();
} }
if (model.imageBitmap && [UpdateReasons.IMAGE_CHANGED, UpdateReasons.OBJECTS_UPDATED].includes(reason)) { if (model.imageBitmap && [UpdateReasons.IMAGE_CHANGED, UpdateReasons.OBJECTS_UPDATED].includes(reason)) {
@ -1387,6 +1462,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const states = this.controller.objects; const states = this.controller.objects;
const ctx = this.bitmap.getContext('2d'); const ctx = this.bitmap.getContext('2d');
ctx.imageSmoothingEnabled = false;
if (ctx) { if (ctx) {
ctx.fillStyle = 'black'; ctx.fillStyle = 'black';
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
@ -1394,31 +1470,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (state.hidden || state.outside) continue; if (state.hidden || state.outside) continue;
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) { if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) {
let points = []; let points = [...state.points];
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle') {
points = [ points = rotate2DPoints(
state.points[0], // xtl points[0] + (points[2] - points[0]) / 2,
state.points[1], // ytl points[1] + (points[3] - points[1]) / 2,
state.points[2], // xbr state.rotation,
state.points[1], // ytl [
state.points[2], // xbr points[0], // xtl
state.points[3], // ybr points[1], // ytl
state.points[0], // xtl points[2], // xbr
state.points[3], // ybr points[1], // ytl
]; points[2], // xbr
points[3], // ybr
points[0], // xtl
points[3], // ybr
],
);
} else if (state.shapeType === 'cuboid') { } else if (state.shapeType === 'cuboid') {
points = [ points = [
state.points[0], points[0],
state.points[1], points[1],
state.points[4], points[4],
state.points[5], points[5],
state.points[8], points[8],
state.points[9], points[9],
state.points[12], points[12],
state.points[13], points[13],
]; ];
} else {
points = [...state.points];
} }
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(points[0], points[1]); ctx.moveTo(points[0], points[1]);
@ -1464,6 +1543,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
lock: state.lock, lock: state.lock,
shapeType: state.shapeType, shapeType: state.shapeType,
points: [...state.points], points: [...state.points],
rotation: state.rotation,
attributes: { ...state.attributes }, attributes: { ...state.attributes },
descriptions: [...state.descriptions], descriptions: [...state.descriptions],
zOrder: state.zOrder, zOrder: state.zOrder,
@ -1523,6 +1603,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement); this.activate(activeElement);
} }
if (drawnState.rotation) {
// need to rotate it back before changing points
shape.untransform();
}
if ( if (
state.points.length !== drawnState.points.length || state.points.length !== drawnState.points.length ||
state.points.some((p: number, id: number): boolean => p !== drawnState.points[id]) state.points.some((p: number, id: number): boolean => p !== drawnState.points[id])
@ -1552,6 +1637,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
if (state.rotation) {
// now, when points changed, need to rotate it to new angle
shape.rotate(state.rotation);
}
const stateDescriptions = state.descriptions; const stateDescriptions = state.descriptions;
const drawnStateDescriptions = drawnState.descriptions; const drawnStateDescriptions = drawnState.descriptions;
@ -1802,17 +1892,25 @@ export class CanvasViewImpl implements CanvasView, Listener {
const p1 = e.detail.handler.startPoints.point; const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p; const p2 = e.detail.p;
const delta = 1; const delta = 1;
const { offset } = this.controller.geometry;
const dx2 = (p1.x - p2.x) ** 2; const dx2 = (p1.x - p2.x) ** 2;
const dy2 = (p1.y - p2.y) ** 2; const dy2 = (p1.y - p2.y) ** 2;
if (Math.sqrt(dx2 + dy2) >= delta) { if (Math.sqrt(dx2 + dy2) >= delta) {
const points = pointsToNumberArray( // these points does not take into account possible transformations, applied on the element
// so, if any (like rotation) we need to map them to canvas coordinate space
let points = pointsToNumberArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},` + `${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`,
`${shape.attr('y') + shape.attr('height')}`, );
).map((x: number): number => x - offset);
// let's keep current points, but they could be rewritten in updateObjects
this.drawnStates[clientID].points = this.translateFromCanvas(points);
const { rotation } = shape.transform();
if (rotation) {
points = this.translatePointsFromRotatedShape(shape, points);
}
this.drawnStates[state.clientID].points = points; points = this.translateFromCanvas(points);
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('canvas.dragshape', { new CustomEvent('canvas.dragshape', {
bubbles: false, bubbles: false,
@ -1850,6 +1948,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
(shape as any) (shape as any)
.resize({ .resize({
snapToGrid: 0.1, snapToGrid: 0.1,
snapToAngle: this.snapToAngleResize,
}) })
.on('resizestart', (): void => { .on('resizestart', (): void => {
this.mode = Mode.RESIZE; this.mode = Mode.RESIZE;
@ -1869,6 +1968,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
.on('resizedone', (): void => { .on('resizedone', (): void => {
if (shapeSizeElement) { if (shapeSizeElement) {
shapeSizeElement.rm(); shapeSizeElement.rm();
shapeSizeElement = null;
} }
showDirection(); showDirection();
@ -1877,15 +1977,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
if (resized) { if (resized) {
const { offset } = this.controller.geometry; let rotation = shape.transform().rotation || 0;
// be sure, that rotation in range [0; 360]
while (rotation < 0) rotation += 360;
rotation %= 360;
const points = pointsToNumberArray( // these points does not take into account possible transformations, applied on the element
// so, if any (like rotation) we need to map them to canvas coordinate space
let points = pointsToNumberArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},` + `${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`,
`${shape.attr('y') + shape.attr('height')}`, );
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points; // let's keep current points, but they could be rewritten in updateObjects
this.drawnStates[clientID].points = this.translateFromCanvas(points);
this.drawnStates[clientID].rotation = rotation;
if (rotation) {
points = this.translatePointsFromRotatedShape(shape, points);
}
// points = this.translateFromCanvas(points);
this.canvas.dispatchEvent( this.canvas.dispatchEvent(
new CustomEvent('canvas.resizeshape', { new CustomEvent('canvas.resizeshape', {
bubbles: false, bubbles: false,
@ -1895,7 +2007,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}, },
}), }),
); );
this.onEditDone(state, points); this.onEditDone(state, this.translateFromCanvas(points), rotation);
} }
}); });
@ -1938,6 +2050,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Update text position after corresponding box has been moved, resized, etc. // Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
if (text.node.style.display === 'none') return; // wrong transformation matrix if (text.node.style.display === 'none') return; // wrong transformation matrix
const { rotation } = shape.transform();
let box = (shape.node as any).getBBox(); let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system // Translate the whole box to the client coordinate system
@ -1965,13 +2078,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
// Translate back to text SVG // Translate back to text SVG
const [x, y]: number[] = translateToSVG(this.text, [ const [x, y, cx, cy]: number[] = translateToSVG(this.text, [
clientX + consts.TEXT_MARGIN, clientX + consts.TEXT_MARGIN,
clientY + consts.TEXT_MARGIN, clientY + consts.TEXT_MARGIN,
x1 + (x2 - x1) / 2,
y1 + (y2 - y1) / 2,
]); ]);
// Finally draw a text // Finally draw a text
text.move(x, y); text.move(x, y);
if (rotation) {
text.rotate(rotation, cx, cy);
}
for (const tspan of (text.lines() as any).members) { for (const tspan of (text.lines() as any).members) {
tspan.attr('x', text.attr('x')); tspan.attr('x', text.attr('x'));
} }
@ -2033,6 +2152,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
.move(xtl, ytl) .move(xtl, ytl)
.addClass('cvat_canvas_shape'); .addClass('cvat_canvas_shape');
if (state.rotation) {
rect.rotate(state.rotation);
}
if (state.occluded) { if (state.occluded) {
rect.addClass('cvat_canvas_shape_occluded'); rect.addClass('cvat_canvas_shape_occluded');
} }

@ -17,6 +17,8 @@ const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' + const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' +
'0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z'; '0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
const BASE_PATTERN_SIZE = 5; const BASE_PATTERN_SIZE = 5;
const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1;
const SNAP_TO_ANGLE_RESIZE_SHIFT = 15;
export default { export default {
BASE_STROKE_WIDTH, BASE_STROKE_WIDTH,
@ -33,4 +35,6 @@ export default {
UNDEFINED_ATTRIBUTE_VALUE, UNDEFINED_ATTRIBUTE_VALUE,
ARROW_PATH, ARROW_PATH,
BASE_PATTERN_SIZE, BASE_PATTERN_SIZE,
SNAP_TO_ANGLE_RESIZE_DEFAULT,
SNAP_TO_ANGLE_RESIZE_SHIFT,
}; };

@ -17,38 +17,25 @@ export interface InteractionHandler {
transform(geometry: Geometry): void; transform(geometry: Geometry): void;
interact(interactData: InteractionData): void; interact(interactData: InteractionData): void;
configurate(config: Configuration): void; configurate(config: Configuration): void;
destroy(): void;
cancel(): void; cancel(): void;
} }
export class InteractionHandlerImpl implements InteractionHandler { export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void; private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private configuration: Configuration; private configuration: Configuration;
private geometry: Geometry; private geometry: Geometry;
private canvas: SVG.Container; private canvas: SVG.Container;
private interactionData: InteractionData; private interactionData: InteractionData;
private cursorPosition: { x: number; y: number }; private cursorPosition: { x: number; y: number };
private shapesWereUpdated: boolean; private shapesWereUpdated: boolean;
private interactionShapes: SVG.Shape[]; private interactionShapes: SVG.Shape[];
private currentInteractionShape: SVG.Shape | null; private currentInteractionShape: SVG.Shape | null;
private crosshair: Crosshair; private crosshair: Crosshair;
private threshold: SVG.Rect | null; private threshold: SVG.Rect | null;
private thresholdRectSize: number; private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>; private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape; private drawnIntermediateShape: SVG.Shape;
private thresholdWasModified: boolean; private thresholdWasModified: boolean;
private prepareResult(): InteractionResult[] { private prepareResult(): InteractionResult[] {
@ -473,13 +460,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
} }
}); });
window.addEventListener('keyup', this.onKeyUp); window.document.addEventListener('keyup', this.onKeyUp);
window.addEventListener('keydown', this.onKeyDown); window.document.addEventListener('keydown', this.onKeyDown);
this.canvas.on('destroy.canvas', ():void => {
window.removeEventListener('keyup', this.onKeyUp);
window.removeEventListener('keydown', this.onKeyDown);
});
} }
public transform(geometry: Geometry): void { public transform(geometry: Geometry): void {
@ -560,4 +542,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.release(); this.release();
this.onInteraction(null); this.onInteraction(null);
} }
public destroy(): void {
window.document.removeEventListener('keyup', this.onKeyUp);
window.document.removeEventListener('keydown', this.onKeyDown);
}
} }

@ -44,6 +44,7 @@ export interface DrawnState {
source: 'AUTO' | 'MANUAL'; source: 'AUTO' | 'MANUAL';
shapeType: string; shapeType: string;
points?: number[]; points?: number[];
rotation: number;
attributes: Record<number, string>; attributes: Record<number, string>;
descriptions: string[]; descriptions: string[];
zOrder?: number; zOrder?: number;
@ -95,16 +96,30 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
.fill('white') .fill('white')
.addClass('cvat_canvas_text'), .addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void { update(shape: SVG.Shape): void {
const bbox = shape.bbox(); let text = `${Math.round(shape.width())}x${Math.round(shape.height())}px`;
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`; if (shape.type === 'rect') {
const [x, y]: number[] = translateToSVG( let rotation = shape.transform().rotation || 0;
// be sure, that rotation in range [0; 360]
while (rotation < 0) rotation += 360;
rotation %= 360;
if (rotation) {
text = `${text} ${rotation.toFixed(1)}\u00B0`;
}
}
const [x, y, cx, cy]: number[] = translateToSVG(
(textContainer.node as any) as SVGSVGElement, (textContainer.node as any) as SVGSVGElement,
translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [bbox.x, bbox.y]), translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [
); shape.x(),
shape.y(),
shape.cx(),
shape.cy(),
]),
).map((coord: number): number => Math.round(coord));
this.sizeElement this.sizeElement
.clear() .clear()
.plain(text) .plain(text)
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN); .move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN)
.rotate(shape.transform().rotation, cx, cy);
}, },
rm(): void { rm(): void {
if (this.sizeElement) { if (this.sizeElement) {
@ -117,6 +132,23 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
return shapeSize; return shapeSize;
} }
export function rotate2DPoints(cx: number, cy: number, angle: number, points: number[]): number[] {
const rad = (Math.PI / 180) * angle;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
const result = [];
for (let i = 0; i < points.length; i += 2) {
const x = points[i];
const y = points[i + 1];
result.push(
(x - cx) * cos - (y - cy) * sin + cx,
(y - cy) * cos + (x - cx) * sin + cy,
);
}
return result;
}
export function pointsToNumberArray(points: string | Point[]): number[] { export function pointsToNumberArray(points: string | Point[]): number[] {
if (Array.isArray(points)) { if (Array.isArray(points)) {
return points.reduce((acc: number[], point: Point): number[] => { return points.reduce((acc: number[], point: Point): number[] => {

@ -167,18 +167,23 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
handler = this.remember('_resizeHandler'); handler = this.remember('_resizeHandler');
handler.resize = function (e: any) { handler.resize = function (e: any) {
const { event } = e.detail; const { event } = e.detail;
this.rotationPointPressed = e.type === 'rot';
if ( if (
event.button === 0 && event.button === 0 &&
// ignore shift key for cuboid change perspective // ignore shift key for cuboids (change perspective) and rectangles (precise rotation)
(!event.shiftKey || this.el.parent().hasClass('cvat_canvas_shape_cuboid')) && (!event.shiftKey || (
!event.altKey this.el.parent().hasClass('cvat_canvas_shape_cuboid')
|| this.el.type === 'rect')
) && !event.altKey
) { ) {
return handler.constructor.prototype.resize.call(this, e); return handler.constructor.prototype.resize.call(this, e);
} }
}; };
handler.update = function (e: any) { handler.update = function (e: any) {
this.m = this.el.node.getScreenCTM().inverse(); if (!this.rotationPointPressed) {
return handler.constructor.prototype.update.call(this, e); this.m = this.el.node.getScreenCTM().inverse();
}
handler.constructor.prototype.update.call(this, e);
}; };
} else { } else {
originalResize.call(this, ...args); originalResize.call(this, ...args);

@ -1,12 +1,12 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.17.0", "version": "3.19.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-core", "name": "cvat-core",
"version": "3.17.0", "version": "3.19.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^0.21.4", "axios": "^0.21.4",

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.17.0", "version": "3.19.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {

@ -235,7 +235,7 @@
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof object === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError( throw new ArgumentError(
'The object has not been saved yet. Call ObjectState.put([state]) before you can merge it', 'The object is not in collection yet. Call ObjectState.put([state]) before you can merge it',
); );
} }
return object; return object;
@ -282,6 +282,7 @@
frame: object.frame, frame: object.frame,
points: [...object.points], points: [...object.points],
occluded: object.occluded, occluded: object.occluded,
rotation: object.rotation,
zOrder: object.zOrder, zOrder: object.zOrder,
outside: false, outside: false,
attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => { attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => {
@ -333,6 +334,7 @@
type: shapeType, type: shapeType,
frame: +keyframe, frame: +keyframe,
points: [...shape.points], points: [...shape.points],
rotation: shape.rotation,
occluded: shape.occluded, occluded: shape.occluded,
outside: shape.outside, outside: shape.outside,
zOrder: shape.zOrder, zOrder: shape.zOrder,
@ -442,6 +444,7 @@
const position = { const position = {
type: objectState.shapeType, type: objectState.shapeType,
points: [...objectState.points], points: [...objectState.points],
rotation: objectState.rotation,
occluded: objectState.occluded, occluded: objectState.occluded,
outside: objectState.outside, outside: objectState.outside,
zOrder: objectState.zOrder, zOrder: objectState.zOrder,
@ -481,6 +484,12 @@
return shape; return shape;
}); });
prev.shapes.push(position); prev.shapes.push(position);
// add extra keyframe if no other keyframes before outside
if (!prev.shapes.some((shape) => shape.frame === frame - 1)) {
prev.shapes.push(JSON.parse(JSON.stringify(position)));
prev.shapes[prev.shapes.length - 2].frame -= 1;
}
prev.shapes[prev.shapes.length - 1].outside = true; prev.shapes[prev.shapes.length - 1].outside = true;
let clientID = ++this.count; let clientID = ++this.count;
@ -844,7 +853,7 @@
if (typeof object === 'undefined') { if (typeof object === 'undefined') {
throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before'); throw new ArgumentError('The object has not been saved yet. Call annotations.put([state]) before');
} }
const distance = object.constructor.distance(state.points, x, y); const distance = object.constructor.distance(state.points, x, y, state.rotation);
if (distance !== null && (minimumDistance === null || distance < minimumDistance)) { if (distance !== null && (minimumDistance === null || distance < minimumDistance)) {
minimumDistance = distance; minimumDistance = distance;
minimumState = state; minimumState = state;

@ -84,30 +84,34 @@
return area >= MIN_SHAPE_AREA; return area >= MIN_SHAPE_AREA;
} }
function fitPoints(shapeType, points, maxX, maxY) { function rotatePoint(x, y, angle, cx = 0, cy = 0) {
const fittedPoints = []; const sin = Math.sin((angle * Math.PI) / 180);
const cos = Math.cos((angle * Math.PI) / 180);
for (let i = 0; i < points.length - 1; i += 2) { const rotX = (x - cx) * cos - (y - cy) * sin + cx;
const x = points[i]; const rotY = (y - cy) * cos + (x - cx) * sin + cy;
const y = points[i + 1]; return [rotX, rotY];
}
checkObjectType('coordinate', x, 'number', null); function fitPoints(shapeType, points, rotation, maxX, maxY) {
checkObjectType('coordinate', y, 'number', null); checkObjectType('rotation', rotation, 'number', null);
points.forEach((coordinate) => checkObjectType('coordinate', coordinate, 'number', null));
fittedPoints.push(Math.clamp(x, 0, maxX), Math.clamp(y, 0, maxY)); if (shapeType === ObjectShape.CUBOID || !!rotation) {
// cuboids and rotated bounding boxes cannot be fitted
return points;
} }
return shapeType === ObjectShape.CUBOID ? points : fittedPoints; const fittedPoints = [];
}
function checkOutside(points, width, height) {
let inside = false;
for (let i = 0; i < points.length - 1; i += 2) { for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i); const x = points[i];
inside = inside || (x >= 0 && x <= width && y >= 0 && y <= height); const y = points[i + 1];
const clampedX = Math.clamp(x, 0, maxX);
const clampedY = Math.clamp(y, 0, maxY);
fittedPoints.push(clampedX, clampedY);
} }
return !inside; return fittedPoints;
} }
function validateAttributeValue(value, attr) { function validateAttributeValue(value, attr) {
@ -345,13 +349,13 @@
checkNumberOfPoints(this.shapeType, data.points); checkNumberOfPoints(this.shapeType, data.points);
// cut points // cut points
const { width, height, filename } = this.frameMeta[frame]; const { width, height, filename } = this.frameMeta[frame];
fittedPoints = fitPoints(this.shapeType, data.points, width, height); fittedPoints = fitPoints(this.shapeType, data.points, data.rotation, width, height);
let check = true; let check = true;
if (filename && filename.slice(filename.length - 3) === 'pcd') { if (filename && filename.slice(filename.length - 3) === 'pcd') {
check = false; check = false;
} }
if (check) { if (check) {
if (!checkShapeArea(this.shapeType, fittedPoints) || checkOutside(fittedPoints, width, height)) { if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = []; fittedPoints = [];
} }
} }
@ -492,6 +496,7 @@
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
super(data, clientID, color, injection); super(data, clientID, color, injection);
this.points = data.points; this.points = data.points;
this.rotation = data.rotation || 0;
this.occluded = data.occluded; this.occluded = data.occluded;
this.zOrder = data.z_order; this.zOrder = data.z_order;
} }
@ -504,6 +509,7 @@
occluded: this.occluded, occluded: this.occluded,
z_order: this.zOrder, z_order: this.zOrder,
points: [...this.points], points: [...this.points],
rotation: this.rotation,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => { attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({ attributeAccumulator.push({
spec_id: attrId, spec_id: attrId,
@ -535,6 +541,7 @@
lock: this.lock, lock: this.lock,
zOrder: this.zOrder, zOrder: this.zOrder,
points: [...this.points], points: [...this.points],
rotation: this.rotation,
attributes: { ...this.attributes }, attributes: { ...this.attributes },
descriptions: [...this.descriptions], descriptions: [...this.descriptions],
label: this.label, label: this.label,
@ -548,9 +555,11 @@
}; };
} }
_savePoints(points, frame) { _savePoints(points, rotation, frame) {
const undoPoints = this.points; const undoPoints = this.points;
const undoRotation = this.rotation;
const redoPoints = points; const redoPoints = points;
const redoRotation = rotation;
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
@ -559,11 +568,13 @@
() => { () => {
this.points = undoPoints; this.points = undoPoints;
this.source = undoSource; this.source = undoSource;
this.rotation = undoRotation;
this.updated = Date.now(); this.updated = Date.now();
}, },
() => { () => {
this.points = redoPoints; this.points = redoPoints;
this.source = redoSource; this.source = redoSource;
this.rotation = redoRotation;
this.updated = Date.now(); this.updated = Date.now();
}, },
[this.clientID], [this.clientID],
@ -572,6 +583,7 @@
this.source = Source.MANUAL; this.source = Source.MANUAL;
this.points = points; this.points = points;
this.rotation = rotation;
} }
_saveOccluded(occluded, frame) { _saveOccluded(occluded, frame) {
@ -637,6 +649,7 @@
const updated = data.updateFlags; const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated); const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
// Now when all fields are validated, we can apply them // Now when all fields are validated, we can apply them
if (updated.label) { if (updated.label) {
@ -652,7 +665,7 @@
} }
if (updated.points && fittedPoints.length) { if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, frame); this._savePoints(fittedPoints, rotation, frame);
} }
if (updated.occluded) { if (updated.occluded) {
@ -696,6 +709,7 @@
zOrder: value.z_order, zOrder: value.z_order,
points: value.points, points: value.points,
outside: value.outside, outside: value.outside,
rotation: value.rotation || 0,
attributes: value.attributes.reduce((attributeAccumulator, attr) => { attributes: value.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value; attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator; return attributeAccumulator;
@ -736,6 +750,7 @@
occluded: this.shapes[frame].occluded, occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder, z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points], points: [...this.shapes[frame].points],
rotation: this.shapes[frame].rotation,
outside: this.shapes[frame].outside, outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes).reduce( attributes: Object.keys(this.shapes[frame].attributes).reduce(
(attributeAccumulator, attrId) => { (attributeAccumulator, attrId) => {
@ -1009,22 +1024,21 @@
); );
} }
_savePoints(points, frame) { _savePoints(points, rotation, frame) {
const current = this.get(frame); const current = this.get(frame);
const wasKeyframe = frame in this.shapes; const wasKeyframe = frame in this.shapes;
const undoSource = this.source; const undoSource = this.source;
const redoSource = Source.MANUAL; const redoSource = Source.MANUAL;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined; const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? const redoShape = wasKeyframe ? { ...this.shapes[frame], points, rotation } : {
{ ...this.shapes[frame], points } : frame,
{ points,
frame, rotation,
points, zOrder: current.zOrder,
zOrder: current.zOrder, outside: current.outside,
outside: current.outside, occluded: current.occluded,
occluded: current.occluded, attributes: {},
attributes: {}, };
};
this.shapes[frame] = redoShape; this.shapes[frame] = redoShape;
this.source = Source.MANUAL; this.source = Source.MANUAL;
@ -1049,6 +1063,7 @@
{ {
frame, frame,
outside, outside,
rotation: current.rotation,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
occluded: current.occluded, occluded: current.occluded,
@ -1078,6 +1093,7 @@
{ {
frame, frame,
occluded, occluded,
rotation: current.rotation,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
outside: current.outside, outside: current.outside,
@ -1107,6 +1123,7 @@
{ {
frame, frame,
zOrder, zOrder,
rotation: current.rotation,
occluded: current.occluded, occluded: current.occluded,
points: current.points, points: current.points,
outside: current.outside, outside: current.outside,
@ -1139,6 +1156,7 @@
const redoShape = keyframe ? const redoShape = keyframe ?
{ {
frame, frame,
rotation: current.rotation,
zOrder: current.zOrder, zOrder: current.zOrder,
points: current.points, points: current.points,
outside: current.outside, outside: current.outside,
@ -1172,6 +1190,7 @@
const updated = data.updateFlags; const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated); const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
const { rotation } = data;
if (updated.label) { if (updated.label) {
this._saveLabel(data.label, frame); this._saveLabel(data.label, frame);
@ -1194,7 +1213,7 @@
} }
if (updated.points && fittedPoints.length) { if (updated.points && fittedPoints.length) {
this._savePoints(fittedPoints, frame); this._savePoints(fittedPoints, rotation, frame);
} }
if (updated.outside) { if (updated.outside) {
@ -1246,6 +1265,7 @@
if (leftPosition) { if (leftPosition) {
return { return {
points: [...leftPosition.points], points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1256,10 +1276,11 @@
if (rightPosition) { if (rightPosition) {
return { return {
points: [...rightPosition.points], points: [...rightPosition.points],
rotation: rightPosition.rotation,
occluded: rightPosition.occluded, occluded: rightPosition.occluded,
outside: true,
zOrder: rightPosition.zOrder, zOrder: rightPosition.zOrder,
keyframe: targetFrame in this.shapes, keyframe: targetFrame in this.shapes,
outside: true,
}; };
} }
@ -1356,20 +1377,28 @@
checkNumberOfPoints(this.shapeType, this.points); checkNumberOfPoints(this.shapeType, this.points);
} }
static distance(points, x, y) { static distance(points, x, y, angle) {
const [xtl, ytl, xbr, ybr] = points; const [xtl, ytl, xbr, ybr] = points;
const cx = xtl + (xbr - xtl) / 2;
const cy = ytl + (ybr - ytl) / 2;
const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy);
if (!(x >= xtl && x <= xbr && y >= ytl && y <= ybr)) { if (!(rotX >= xtl && rotX <= xbr && rotY >= ytl && rotY <= ybr)) {
// Cursor is outside of a box // Cursor is outside of a box
return null; return null;
} }
// The shortest distance from point to an edge // The shortest distance from point to an edge
return Math.min.apply(null, [x - xtl, y - ytl, xbr - x, ybr - y]); return Math.min.apply(null, [rotX - xtl, rotY - ytl, xbr - rotX, ybr - rotY]);
} }
} }
class PolyShape extends Shape {} class PolyShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.rotation = 0; // is not supported
}
}
class PolygonShape extends PolyShape { class PolygonShape extends PolyShape {
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
@ -1509,6 +1538,7 @@
class CuboidShape extends Shape { class CuboidShape extends Shape {
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
super(data, clientID, color, injection); super(data, clientID, color, injection);
this.rotation = 0;
this.shapeType = ObjectShape.CUBOID; this.shapeType = ObjectShape.CUBOID;
this.pinned = false; this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points); checkNumberOfPoints(this.shapeType, this.points);
@ -1638,10 +1668,25 @@
} }
interpolatePosition(leftPosition, rightPosition, offset) { interpolatePosition(leftPosition, rightPosition, offset) {
function findAngleDiff(rightAngle, leftAngle) {
let angleDiff = rightAngle - leftAngle;
angleDiff = ((angleDiff + 180) % 360) - 180;
if (Math.abs(angleDiff) >= 180) {
// if the main arc is bigger than 180, go another arc
// to find it, just substract absolute value from 360 and inverse sign
angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1;
}
return angleDiff;
}
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point); const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return { return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset), points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation:
(leftPosition.rotation + findAngleDiff(
rightPosition.rotation, leftPosition.rotation,
) * offset + 360) % 360,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1650,10 +1695,18 @@
} }
class PolyTrack extends Track { class PolyTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
for (const shape of Object.values(this.shapes)) {
shape.rotation = 0; // is not supported
}
}
interpolatePosition(leftPosition, rightPosition, offset) { interpolatePosition(leftPosition, rightPosition, offset) {
if (offset === 0) { if (offset === 0) {
return { return {
points: [...leftPosition.points], points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1900,6 +1953,7 @@
return { return {
points: toArray(reducedPoints), points: toArray(reducedPoints),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1962,6 +2016,7 @@
points: leftPosition.points.map( points: leftPosition.points.map(
(value, index) => value + (rightPosition.points[index] - value) * offset, (value, index) => value + (rightPosition.points[index] - value) * offset,
), ),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1970,6 +2025,7 @@
return { return {
points: [...leftPosition.points], points: [...leftPosition.points],
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,
@ -1984,6 +2040,7 @@
this.pinned = false; this.pinned = false;
for (const shape of Object.values(this.shapes)) { for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points); checkNumberOfPoints(this.shapeType, shape.points);
shape.rotation = 0; // is not supported
} }
} }
@ -1992,6 +2049,7 @@
return { return {
points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset), points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
rotation: leftPosition.rotation,
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: leftPosition.zOrder, zOrder: leftPosition.zOrder,

@ -100,6 +100,7 @@
'occluded', 'occluded',
'z_order', 'z_order',
'points', 'points',
'rotation',
'type', 'type',
'shapes', 'shapes',
'attributes', 'attributes',

@ -28,6 +28,7 @@ const { Source } = require('./enums');
descriptions: [], descriptions: [],
points: null, points: null,
rotation: null,
outside: null, outside: null,
occluded: null, occluded: null,
keyframe: null, keyframe: null,
@ -204,6 +205,28 @@ const { Source } = require('./enums');
} }
}, },
}, },
rotation: {
/**
* @name rotation
* @type {number} angle measured by degrees
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.rotation,
set: (rotation) => {
if (typeof rotation === 'number') {
data.updateFlags.points = true;
data.rotation = rotation;
} else {
throw new ArgumentError(
`Rotation is expected to be a number, but got ${
typeof rotation === 'object' ? rotation.constructor.name : typeof points
}`,
);
}
},
},
group: { group: {
/** /**
* Object with short group info { color, id } * Object with short group info { color, id }
@ -410,6 +433,9 @@ const { Source } = require('./enums');
if (typeof serialized.color === 'string') { if (typeof serialized.color === 'string') {
this.color = serialized.color; this.color = serialized.color;
} }
if (typeof serialized.rotation === 'number') {
this.rotation = serialized.rotation;
}
if (Array.isArray(serialized.points)) { if (Array.isArray(serialized.points)) {
this.points = serialized.points; this.points = serialized.points;
} }

@ -60,6 +60,37 @@ describe('Feature: get annotations', () => {
// TODO: Test filter (hasn't been implemented yet) // TODO: Test filter (hasn't been implemented yet)
}); });
describe('Feature: get interpolated annotations', () => {
test('get interpolated box', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(5);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(1);
const [xtl, ytl, xbr, ybr] = annotations[0].points;
const { rotation } = annotations[0];
expect(rotation).toBe(50);
expect(Math.round(xtl)).toBe(332);
expect(Math.round(ytl)).toBe(519);
expect(Math.round(xbr)).toBe(651);
expect(Math.round(ybr)).toBe(703);
annotations = await task.annotations.get(15);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(2); // there is also a polygon on these frames (up to frame 22)
expect(annotations[1].rotation).toBe(40);
expect(annotations[1].shapeType).toBe('rectangle');
annotations = await task.annotations.get(30);
annotations[0].rotation = 20;
await annotations[0].save();
annotations = await task.annotations.get(25);
expect(annotations[0].rotation).toBe(0);
expect(annotations[0].shapeType).toBe('rectangle');
});
});
describe('Feature: put annotations', () => { describe('Feature: put annotations', () => {
test('put a shape to a task', async () => { test('put a shape to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];

@ -1740,6 +1740,7 @@ const taskAnnotationsDummyData = {
occluded: false, occluded: false,
z_order: 1, z_order: 1,
points: [425.58984375, 540.298828125, 755.9765625, 745.6328125], points: [425.58984375, 540.298828125, 755.9765625, 745.6328125],
rotation: 0,
id: 379, id: 379,
frame: 0, frame: 0,
outside: false, outside: false,
@ -1759,6 +1760,7 @@ const taskAnnotationsDummyData = {
occluded: false, occluded: false,
z_order: 1, z_order: 1,
points: [238.8000000000011, 498.6000000000022, 546.01171875, 660.720703125], points: [238.8000000000011, 498.6000000000022, 546.01171875, 660.720703125],
rotation: 100,
id: 380, id: 380,
frame: 10, frame: 10,
outside: false, outside: false,
@ -1769,6 +1771,7 @@ const taskAnnotationsDummyData = {
occluded: false, occluded: false,
z_order: 1, z_order: 1,
points: [13.3955078125, 447.650390625, 320.6072265624989, 609.7710937499978], points: [13.3955078125, 447.650390625, 320.6072265624989, 609.7710937499978],
rotation: 340,
id: 381, id: 381,
frame: 20, frame: 20,
outside: false, outside: false,

@ -1,12 +1,12 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.25.1", "version": "1.26.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.25.1", "version": "1.26.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.3", "@ant-design/icons": "^4.6.3",

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.25.1", "version": "1.26.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -525,8 +525,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onEditShape(false); onEditShape(false);
const { state, points } = event.detail; const { state, points, rotation } = event.detail;
state.points = points; state.points = points;
state.rotation = rotation;
onUpdateAnnotations([state]); onUpdateAnnotations([state]);
}; };

@ -38,7 +38,7 @@ const defaultState: AnnotationState = {
pointID: null, pointID: null,
clientID: null, clientID: null,
}, },
instance: new Canvas(), instance: null,
ready: false, ready: false,
activeControl: ActiveControl.CURSOR, activeControl: ActiveControl.CURSOR,
}, },
@ -166,7 +166,9 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
activeShapeType = ShapeType.CUBOID; activeShapeType = ShapeType.CUBOID;
} }
state.canvas.instance.destroy(); if (state.canvas.instance) {
state.canvas.instance.destroy();
}
return { return {
...state, ...state,
@ -705,7 +707,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
canvas: { activeControl, instance }, canvas: { activeControl, instance },
} = state; } = state;
if (activeControl !== ActiveControl.CURSOR || instance.mode() !== CanvasMode.IDLE) { if (activeControl !== ActiveControl.CURSOR || (instance as Canvas | Canvas3d).mode() !== CanvasMode.IDLE) {
return state; return state;
} }
@ -929,7 +931,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations, ...state.annotations,
history, history,
states, states,
selectedStatesID: [],
activatedStateID: null, activatedStateID: null,
collapsed: {}, collapsed: {},
}, },
@ -1223,6 +1224,9 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
} }
case AnnotationActionTypes.CLOSE_JOB: case AnnotationActionTypes.CLOSE_JOB:
case AuthActionTypes.LOGOUT_SUCCESS: { case AuthActionTypes.LOGOUT_SUCCESS: {
if (state.canvas.instance) {
state.canvas.instance.destroy();
}
return { ...defaultState }; return { ...defaultState };
} }
default: { default: {

@ -496,7 +496,7 @@ export interface AnnotationState {
pointID: number | null; pointID: number | null;
clientID: number | null; clientID: number | null;
}; };
instance: Canvas | Canvas3d; instance: Canvas | Canvas3d | null;
ready: boolean; ready: boolean;
activeControl: ActiveControl; activeControl: ActiveControl;
}; };

@ -429,14 +429,26 @@ class TrackManager(ObjectManager):
@staticmethod @staticmethod
def get_interpolated_shapes(track, start_frame, end_frame): def get_interpolated_shapes(track, start_frame, end_frame):
def copy_shape(source, frame, points=None): def copy_shape(source, frame, points=None, rotation=None):
copied = deepcopy(source) copied = deepcopy(source)
copied["keyframe"] = False copied["keyframe"] = False
copied["frame"] = frame copied["frame"] = frame
if rotation is not None:
copied["rotation"] = rotation
if points is not None: if points is not None:
copied["points"] = points copied["points"] = points
return copied return copied
def find_angle_diff(right_angle, left_angle):
angle_diff = right_angle - left_angle
angle_diff = ((angle_diff + 180) % 360) - 180
if abs(angle_diff) >= 180:
# if the main arc is bigger than 180, go another arc
# to find it, just substract absolute value from 360 and inverse sign
angle_diff = 360 - abs(angle_diff) * -1 if angle_diff > 0 else 1
return angle_diff
def simple_interpolation(shape0, shape1): def simple_interpolation(shape0, shape1):
shapes = [] shapes = []
distance = shape1["frame"] - shape0["frame"] distance = shape1["frame"] - shape0["frame"]
@ -444,9 +456,12 @@ class TrackManager(ObjectManager):
for frame in range(shape0["frame"] + 1, shape1["frame"]): for frame in range(shape0["frame"] + 1, shape1["frame"]):
offset = (frame - shape0["frame"]) / distance offset = (frame - shape0["frame"]) / distance
rotation = (shape0["rotation"] + find_angle_diff(
shape1["rotation"], shape0["rotation"],
) * offset + 360) % 360
points = shape0["points"] + diff * offset points = shape0["points"] + diff * offset
shapes.append(copy_shape(shape0, frame, points.tolist())) shapes.append(copy_shape(shape0, frame, points.tolist(), rotation))
return shapes return shapes

@ -124,11 +124,11 @@ class InstanceLabelData:
class TaskData(InstanceLabelData): class TaskData(InstanceLabelData):
Shape = namedtuple("Shape", 'id, label_id') # 3d Shape = namedtuple("Shape", 'id, label_id') # 3d
LabeledShape = namedtuple( LabeledShape = namedtuple(
'LabeledShape', 'type, frame, label, points, occluded, attributes, source, group, z_order') 'LabeledShape', 'type, frame, label, points, occluded, attributes, source, rotation, group, z_order')
LabeledShape.__new__.__defaults__ = (0, 0) LabeledShape.__new__.__defaults__ = (0, 0, 0)
TrackedShape = namedtuple( TrackedShape = namedtuple(
'TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, source, group, z_order, label, track_id') 'TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, rotation, source, group, z_order, label, track_id')
TrackedShape.__new__.__defaults__ = ('manual', 0, 0, None, 0) TrackedShape.__new__.__defaults__ = (0, 'manual', 0, 0, None, 0)
Track = namedtuple('Track', 'label, group, source, shapes') Track = namedtuple('Track', 'label, group, source, shapes')
Tag = namedtuple('Tag', 'frame, label, attributes, source, group') Tag = namedtuple('Tag', 'frame, label, attributes, source, group')
Tag.__new__.__defaults__ = (0, ) Tag.__new__.__defaults__ = (0, )
@ -263,6 +263,7 @@ class TaskData(InstanceLabelData):
frame=self.abs_frame_id(shape["frame"]), frame=self.abs_frame_id(shape["frame"]),
label=self._get_label_name(shape["label_id"]), label=self._get_label_name(shape["label_id"]),
points=shape["points"], points=shape["points"],
rotation=shape["rotation"],
occluded=shape["occluded"], occluded=shape["occluded"],
z_order=shape.get("z_order", 0), z_order=shape.get("z_order", 0),
group=shape.get("group", 0), group=shape.get("group", 0),
@ -279,6 +280,7 @@ class TaskData(InstanceLabelData):
label=self._get_label_name(shape["label_id"]), label=self._get_label_name(shape["label_id"]),
frame=self.abs_frame_id(shape["frame"]), frame=self.abs_frame_id(shape["frame"]),
points=shape["points"], points=shape["points"],
rotation=shape["rotation"],
occluded=shape["occluded"], occluded=shape["occluded"],
z_order=shape.get("z_order", 0), z_order=shape.get("z_order", 0),
group=shape.get("group", 0), group=shape.get("group", 0),
@ -508,12 +510,12 @@ class TaskData(InstanceLabelData):
return None return None
class ProjectData(InstanceLabelData): class ProjectData(InstanceLabelData):
LabeledShape = NamedTuple('LabledShape', [('type', str), ('frame', int), ('label', str), ('points', List[float]), ('occluded', bool), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('z_order', int), ('task_id', int)]) LabeledShape = NamedTuple('LabledShape', [('type', str), ('frame', int), ('label', str), ('points', List[float]), ('occluded', bool), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('rotation', float), ('z_order', int), ('task_id', int)])
LabeledShape.__new__.__defaults__ = (0,0) LabeledShape.__new__.__defaults__ = (0, 0, 0)
TrackedShape = NamedTuple('TrackedShape', TrackedShape = NamedTuple('TrackedShape',
[('type', str), ('frame', int), ('points', List[float]), ('occluded', bool), ('outside', bool), ('keyframe', bool), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('z_order', int), ('label', str), ('track_id', int)], [('type', str), ('frame', int), ('points', List[float]), ('occluded', bool), ('outside', bool), ('keyframe', bool), ('attributes', List[InstanceLabelData.Attribute]), ('rotation', float), ('source', str), ('group', int), ('z_order', int), ('label', str), ('track_id', int)],
) )
TrackedShape.__new__.__defaults__ = ('manual', 0, 0, None, 0) TrackedShape.__new__.__defaults__ = (0, 'manual', 0, 0, None, 0)
Track = NamedTuple('Track', [('label', str), ('group', int), ('source', str), ('shapes', List[TrackedShape]), ('task_id', int)]) Track = NamedTuple('Track', [('label', str), ('group', int), ('source', str), ('shapes', List[TrackedShape]), ('task_id', int)])
Tag = NamedTuple('Tag', [('frame', int), ('label', str), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('task_id', int)]) Tag = NamedTuple('Tag', [('frame', int), ('label', str), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('task_id', int)])
Tag.__new__.__defaults__ = (0, ) Tag.__new__.__defaults__ = (0, )
@ -644,6 +646,7 @@ class ProjectData(InstanceLabelData):
frame=self.abs_frame_id(task_id, shape["frame"]), frame=self.abs_frame_id(task_id, shape["frame"]),
label=self._get_label_name(shape["label_id"]), label=self._get_label_name(shape["label_id"]),
points=shape["points"], points=shape["points"],
rotation=shape["rotation"],
occluded=shape["occluded"], occluded=shape["occluded"],
z_order=shape.get("z_order", 0), z_order=shape.get("z_order", 0),
group=shape.get("group", 0), group=shape.get("group", 0),
@ -660,6 +663,7 @@ class ProjectData(InstanceLabelData):
label=self._get_label_name(shape["label_id"]), label=self._get_label_name(shape["label_id"]),
frame=self.abs_frame_id(task_id, shape["frame"]), frame=self.abs_frame_id(task_id, shape["frame"]),
points=shape["points"], points=shape["points"],
rotation=shape["rotation"],
occluded=shape["occluded"], occluded=shape["occluded"],
z_order=shape.get("z_order", 0), z_order=shape.get("z_order", 0),
group=shape.get("group", 0), group=shape.get("group", 0),
@ -1125,6 +1129,8 @@ def convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label, format_name
anno_label = map_label(shape_obj.label) anno_label = map_label(shape_obj.label)
anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes) anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes)
anno_attr['occluded'] = shape_obj.occluded anno_attr['occluded'] = shape_obj.occluded
if shape_obj.type == ShapeType.RECTANGLE:
anno_attr['rotation'] = shape_obj.rotation
if hasattr(shape_obj, 'track_id'): if hasattr(shape_obj, 'track_id'):
anno_attr['track_id'] = shape_obj.track_id anno_attr['track_id'] = shape_obj.track_id

@ -11,6 +11,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -19,6 +20,7 @@ from .utils import make_colormap
def _export(dst_file, instance_data, save_images=False): def _export(dst_file, instance_data, save_images=False):
dataset = Dataset.from_extractors(GetCVATDataExtractor( dataset = Dataset.from_extractors(GetCVATDataExtractor(
instance_data, include_images=save_images), env=dm_env) instance_data, include_images=save_images), env=dm_env)
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('boxes_to_masks') dataset.transform('boxes_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')

@ -13,6 +13,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -21,6 +22,7 @@ from .utils import make_colormap
def _export(dst_file, instance_data, save_images=False): def _export(dst_file, instance_data, save_images=False):
dataset = Dataset.from_extractors(GetCVATDataExtractor( dataset = Dataset.from_extractors(GetCVATDataExtractor(
instance_data, include_images=save_images), env=dm_env) instance_data, include_images=save_images), env=dm_env)
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('boxes_to_masks') dataset.transform('boxes_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')

@ -215,6 +215,11 @@ def dump_as_cvat_annotation(dumper, annotations):
("xbr", "{:.2f}".format(shape.points[2])), ("xbr", "{:.2f}".format(shape.points[2])),
("ybr", "{:.2f}".format(shape.points[3])) ("ybr", "{:.2f}".format(shape.points[3]))
])) ]))
if shape.rotation:
dump_data.update(OrderedDict([
("rotation", "{:.2f}".format(shape.rotation))
]))
elif shape.type == "cuboid": elif shape.type == "cuboid":
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("xtl1", "{:.2f}".format(shape.points[0])), ("xtl1", "{:.2f}".format(shape.points[0])),
@ -338,6 +343,11 @@ def dump_as_cvat_interpolation(dumper, annotations):
("xbr", "{:.2f}".format(shape.points[2])), ("xbr", "{:.2f}".format(shape.points[2])),
("ybr", "{:.2f}".format(shape.points[3])), ("ybr", "{:.2f}".format(shape.points[3])),
])) ]))
if shape.rotation:
dump_data.update(OrderedDict([
("rotation", "{:.2f}".format(shape.rotation))
]))
elif shape.type == "cuboid": elif shape.type == "cuboid":
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("xtl1", "{:.2f}".format(shape.points[0])), ("xtl1", "{:.2f}".format(shape.points[0])),
@ -417,6 +427,7 @@ def dump_as_cvat_interpolation(dumper, annotations):
'shapes': [annotations.TrackedShape( 'shapes': [annotations.TrackedShape(
type=shape.type, type=shape.type,
points=shape.points, points=shape.points,
rotation=shape.rotation,
occluded=shape.occluded, occluded=shape.occluded,
outside=False, outside=False,
keyframe=True, keyframe=True,
@ -428,6 +439,7 @@ def dump_as_cvat_interpolation(dumper, annotations):
[annotations.TrackedShape( [annotations.TrackedShape(
type=shape.type, type=shape.type,
points=shape.points, points=shape.points,
rotation=shape.rotation,
occluded=shape.occluded, occluded=shape.occluded,
outside=True, outside=True,
keyframe=True, keyframe=True,
@ -511,6 +523,7 @@ def load(file_object, annotations):
shape['type'] = 'rectangle' if el.tag == 'box' else el.tag shape['type'] = 'rectangle' if el.tag == 'box' else el.tag
shape['occluded'] = el.attrib['occluded'] == '1' shape['occluded'] = el.attrib['occluded'] == '1'
shape['z_order'] = int(el.attrib.get('z_order', 0)) shape['z_order'] = int(el.attrib.get('z_order', 0))
shape['rotation'] = float(el.attrib.get('rotation', 0))
if el.tag == 'box': if el.tag == 'box':
shape['points'].append(el.attrib['xtl']) shape['points'].append(el.attrib['xtl'])

@ -14,6 +14,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -116,6 +117,7 @@ def _export_segmentation(dst_file, instance_data, save_images=False):
dataset = Dataset.from_extractors(GetCVATDataExtractor( dataset = Dataset.from_extractors(GetCVATDataExtractor(
instance_data, include_images=save_images), env=dm_env) instance_data, include_images=save_images), env=dm_env)
with TemporaryDirectory() as temp_dir: with TemporaryDirectory() as temp_dir:
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('boxes_to_masks') dataset.transform('boxes_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')

@ -13,6 +13,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
ProjectData, import_dm_annotations) ProjectData, import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@ -23,6 +24,7 @@ def _export(dst_file, instance_data, save_images=False):
include_images=save_images), env=dm_env) include_images=save_images), env=dm_env)
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')
dataset.export(tmp_dir, format='kitti', dataset.export(tmp_dir, format='kitti',

@ -11,14 +11,15 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations) import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
from .utils import make_colormap from .utils import make_colormap
@exporter(name='Segmentation mask', ext='ZIP', version='1.1') @exporter(name='Segmentation mask', ext='ZIP', version='1.1')
def _export(dst_file, instance_data, save_images=False): def _export(dst_file, instance_data, save_images=False):
dataset = Dataset.from_extractors(GetCVATDataExtractor( dataset = Dataset.from_extractors(GetCVATDataExtractor(
instance_data, include_images=save_images), env=dm_env) instance_data, include_images=save_images), env=dm_env)
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('boxes_to_masks') dataset.transform('boxes_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')

@ -13,6 +13,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
find_dataset_root, match_dm_item) find_dataset_root, match_dm_item)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -26,6 +27,7 @@ def _export(dst_file, instance_data, save_images=False):
dataset = Dataset.from_extractors(GetCVATDataExtractor( dataset = Dataset.from_extractors(GetCVATDataExtractor(
instance_data, include_images=save_images), env=dm_env) instance_data, include_images=save_images), env=dm_env)
dataset.transform(KeepTracks) # can only export tracks dataset.transform(KeepTracks) # can only export tracks
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('boxes_to_masks') dataset.transform('boxes_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')

@ -15,6 +15,7 @@ from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
find_dataset_root, import_dm_annotations, match_dm_item) find_dataset_root, import_dm_annotations, match_dm_item)
from cvat.apps.dataset_manager.util import make_zip_archive from cvat.apps.dataset_manager.util import make_zip_archive
from .transformations import RotatedBoxesToPolygons
from .registry import dm_env, exporter, importer from .registry import dm_env, exporter, importer
@ -40,6 +41,7 @@ def find_item_ids(path):
def _export(dst_file, task_data, save_images=False): def _export(dst_file, task_data, save_images=False):
dataset = Dataset.from_extractors(GetCVATDataExtractor( dataset = Dataset.from_extractors(GetCVATDataExtractor(
task_data, include_images=save_images), env=dm_env) task_data, include_images=save_images), env=dm_env)
dataset.transform(RotatedBoxesToPolygons)
dataset.transform('polygons_to_masks') dataset.transform('polygons_to_masks')
dataset.transform('merge_instance_segments') dataset.transform('merge_instance_segments')

@ -0,0 +1,34 @@
# Copyright (C) 2021 Intel Corporation
#
# SPDX-License-Identifier: MIT
import math
from itertools import chain
from datumaro.components.extractor import ItemTransform
import datumaro.components.annotation as datum_annotation
class RotatedBoxesToPolygons(ItemTransform):
def _rotate_point(self, p, angle, cx, cy):
[x, y] = p
rx = cx + math.cos(angle) * (x - cx) - math.sin(angle) * (y - cy)
ry = cy + math.sin(angle) * (x - cx) + math.cos(angle) * (y - cy)
return rx, ry
def transform_item(self, item):
annotations = item.annotations[:]
anns = [p for p in annotations if p.type == datum_annotation.AnnotationType.bbox and p.attributes['rotation']]
for ann in anns:
rotation = math.radians(ann.attributes['rotation'])
x0, y0, x1, y1 = ann.points
[cx, cy] = [(x0 + (x1 - x0) / 2), (y0 + (y1 - y0) / 2)]
anno_points = list(chain.from_iterable(
map(lambda p: self._rotate_point(p, rotation, cx, cy), [(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
))
annotations.remove(ann)
annotations.append(datum_annotation.Polygon(anno_points,
label=ann.label, attributes=ann.attributes, group=ann.group,
z_order=ann.z_order))
return item.wrap(annotations=annotations)

@ -420,6 +420,7 @@ class JobAnnotation:
'source', 'source',
'occluded', 'occluded',
'z_order', 'z_order',
'rotation',
'points', 'points',
'labeledshapeattributeval__spec_id', 'labeledshapeattributeval__spec_id',
'labeledshapeattributeval__value', 'labeledshapeattributeval__value',
@ -461,6 +462,7 @@ class JobAnnotation:
"trackedshape__type", "trackedshape__type",
"trackedshape__occluded", "trackedshape__occluded",
"trackedshape__z_order", "trackedshape__z_order",
"trackedshape__rotation",
"trackedshape__points", "trackedshape__points",
"trackedshape__id", "trackedshape__id",
"trackedshape__frame", "trackedshape__frame",
@ -483,6 +485,7 @@ class JobAnnotation:
"trackedshape__occluded", "trackedshape__occluded",
"trackedshape__z_order", "trackedshape__z_order",
"trackedshape__points", "trackedshape__points",
"trackedshape__rotation",
"trackedshape__id", "trackedshape__id",
"trackedshape__frame", "trackedshape__frame",
"trackedshape__outside", "trackedshape__outside",

@ -104,6 +104,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 0, "frame": 0,
"points": [1.0, 2.0, 3.0, 4.0], "points": [1.0, 2.0, 3.0, 4.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": False, "outside": False,
@ -113,6 +114,7 @@ class TrackManagerTest(TestCase):
"frame": 2, "frame": 2,
"attributes": [], "attributes": [],
"points": [3.0, 4.0, 5.0, 6.0], "points": [3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": True "outside": True
@ -121,6 +123,7 @@ class TrackManagerTest(TestCase):
"frame": 4, "frame": 4,
"attributes": [], "attributes": [],
"points": [3.0, 4.0, 5.0, 6.0], "points": [3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": False "outside": False
@ -141,6 +144,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 0, "frame": 0,
"points": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], "points": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "polyline", "type": "polyline",
"occluded": False, "occluded": False,
"outside": False, "outside": False,
@ -150,6 +154,7 @@ class TrackManagerTest(TestCase):
"frame": 2, "frame": 2,
"attributes": [], "attributes": [],
"points": [3.0, 4.0, 5.0, 6.0], "points": [3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "polyline", "type": "polyline",
"occluded": False, "occluded": False,
"outside": True "outside": True
@ -158,6 +163,7 @@ class TrackManagerTest(TestCase):
"frame": 4, "frame": 4,
"attributes": [], "attributes": [],
"points": [3.0, 4.0, 5.0, 6.0], "points": [3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "polyline", "type": "polyline",
"occluded": False, "occluded": False,
"outside": False "outside": False
@ -178,6 +184,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 0, "frame": 0,
"points": [1.0, 2.0, 3.0, 4.0], "points": [1.0, 2.0, 3.0, 4.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": False, "outside": False,
@ -186,6 +193,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 2, "frame": 2,
"points": [3.0, 4.0, 5.0, 6.0], "points": [3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": True, "outside": True,
@ -194,6 +202,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 4, "frame": 4,
"points": [5.0, 6.0, 7.0, 8.0], "points": [5.0, 6.0, 7.0, 8.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": True, "outside": True,
@ -206,6 +215,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 0, "frame": 0,
"points": [1.0, 2.0, 3.0, 4.0], "points": [1.0, 2.0, 3.0, 4.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": False, "outside": False,
@ -215,6 +225,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 1, "frame": 1,
"points": [2.0, 3.0, 4.0, 5.0], "points": [2.0, 3.0, 4.0, 5.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": False, "outside": False,
@ -224,6 +235,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 2, "frame": 2,
"points": [3.0, 4.0, 5.0, 6.0], "points": [3.0, 4.0, 5.0, 6.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": True, "outside": True,
@ -233,6 +245,7 @@ class TrackManagerTest(TestCase):
{ {
"frame": 4, "frame": 4,
"points": [5.0, 6.0, 7.0, 8.0], "points": [5.0, 6.0, 7.0, 8.0],
"rotation": 0,
"type": "rectangle", "type": "rectangle",
"occluded": False, "occluded": False,
"outside": True, "outside": True,

@ -0,0 +1,23 @@
# Generated by Django 3.1.13 on 2021-11-15 08:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('engine', '0043_auto_20211027_0718'),
]
operations = [
migrations.AddField(
model_name='labeledshape',
name='rotation',
field=models.FloatField(default=0),
),
migrations.AddField(
model_name='trackedshape',
name='rotation',
field=models.FloatField(default=0),
),
]

@ -10,6 +10,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.db import models from django.db import models
from django.db.models.fields import FloatField
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from cvat.apps.engine.utils import parse_specific_attributes from cvat.apps.engine.utils import parse_specific_attributes
@ -479,6 +480,7 @@ class Shape(models.Model):
occluded = models.BooleanField(default=False) occluded = models.BooleanField(default=False)
z_order = models.IntegerField(default=0) z_order = models.IntegerField(default=0)
points = FloatArrayField() points = FloatArrayField()
rotation = FloatField(default=0)
class Meta: class Meta:
abstract = True abstract = True

@ -670,6 +670,7 @@ class ShapeSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=models.ShapeType.choices()) type = serializers.ChoiceField(choices=models.ShapeType.choices())
occluded = serializers.BooleanField() occluded = serializers.BooleanField()
z_order = serializers.IntegerField(default=0) z_order = serializers.IntegerField(default=0)
rotation = serializers.FloatField(default=0, min_value=0, max_value=360)
points = serializers.ListField( points = serializers.ListField(
child=serializers.FloatField(), child=serializers.FloatField(),
allow_empty=False, allow_empty=False,

@ -349,6 +349,7 @@ class Task3DTest(_DbTestBase):
"occluded": False, "occluded": False,
"z_order": 0, "z_order": 0,
"points": [0.16, 0.20, -0.26, 0, -0.14, 0, 4.84, 4.48, 4.12, 0, 0, 0, 0, 0, 0, 0], "points": [0.16, 0.20, -0.26, 0, -0.14, 0, 4.84, 4.48, 4.12, 0, 0, 0, 0, 0, 0, 0],
"rotation": 0,
"frame": 0, "frame": 0,
"label_id": None, "label_id": None,
"group": 0, "group": 0,

@ -11,7 +11,7 @@ context('Object make a copy.', () => {
const rectangleShape2Points = { const rectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 100, firstX: 100,
firstY: 100, firstY: 100,
secondX: 150, secondX: 150,
@ -20,7 +20,7 @@ context('Object make a copy.', () => {
const createCuboidShape2Points = { const createCuboidShape2Points = {
points: 'From rectangle', points: 'From rectangle',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 200, firstX: 200,
firstY: 100, firstY: 100,
secondX: 250, secondX: 250,
@ -29,7 +29,7 @@ context('Object make a copy.', () => {
const createPolygonShape = { const createPolygonShape = {
reDraw: false, reDraw: false,
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 300, y: 100 }, { x: 300, y: 100 },
{ x: 350, y: 100 }, { x: 350, y: 100 },
@ -40,7 +40,7 @@ context('Object make a copy.', () => {
}; };
const createPolylinesShape = { const createPolylinesShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 400, y: 100 }, { x: 400, y: 100 },
{ x: 450, y: 100 }, { x: 450, y: 100 },
@ -51,7 +51,7 @@ context('Object make a copy.', () => {
}; };
const createPointsShape = { const createPointsShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [{ x: 500, y: 100 }], pointsMap: [{ x: 500, y: 100 }],
complete: true, complete: true,
numberOfPoints: null, numberOfPoints: null,
@ -97,7 +97,7 @@ context('Object make a copy.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Make a copy via sidebar.', () => { it('Make a copy via sidebar.', () => {
let coordX = 100; let coordX = 100;
let coordY = 300; const coordY = 300;
for (let id = 1; id < countObject + 2; id++) { for (let id = 1; id < countObject + 2; id++) {
cy.get(`#cvat-objects-sidebar-state-item-${id}`).within(() => { cy.get(`#cvat-objects-sidebar-state-item-${id}`).within(() => {
cy.get('[aria-label="more"]').trigger('mouseover').wait(300); // Wait dropdown menu transition cy.get('[aria-label="more"]').trigger('mouseover').wait(300); // Wait dropdown menu transition
@ -112,7 +112,8 @@ context('Object make a copy.', () => {
it('After copying via sidebar, the attributes of the objects are the same.', () => { it('After copying via sidebar, the attributes of the objects are the same.', () => {
checkObjectArrSize(10, 12); checkObjectArrSize(10, 12);
for (let id = 1; id < countObject; id++) { for (let id = 1; id < countObject; id++) {
compareObjectsAttr(`#cvat_canvas_shape_${id}`, `#cvat_canvas_shape_${id + countObject + 1}`); // Parameters id 1 equal patameters id 7, 2 to 8, etc. // Parameters id 1 equal patameters id 7, 2 to 8, etc.
compareObjectsAttr(`#cvat_canvas_shape_${id}`, `#cvat_canvas_shape_${id + countObject + 1}`);
} }
for (let idSidebar = 1; idSidebar < 7; idSidebar++) { for (let idSidebar = 1; idSidebar < 7; idSidebar++) {
compareObjectsSidebarAttr( compareObjectsSidebarAttr(
@ -122,23 +123,17 @@ context('Object make a copy.', () => {
} }
}); });
// Disabled part of the test for the Firefox browser due to possible problems positioning the element and completing the trigger() construct for moving the mouse cursor over the element. // Disabled part of the test for the Firefox browser due to possible problems
// positioning the element and completing the trigger() construct for moving the mouse cursor over the element.
it('Make a copy via object context menu.', { browser: '!firefox' }, () => { it('Make a copy via object context menu.', { browser: '!firefox' }, () => {
let coordX = 100; let coordX = 100;
let coordY = 400; const coordY = 400;
for (let id = 1; id < countObject; id++) { for (let id = 1; id < countObject; id++) {
// Point doesn't have a context menu // Point doesn't have a context menu
if (id === 4) { cy.get(`#cvat_canvas_shape_${id}`)
cy.get(`#cvat_canvas_shape_${id}`) .trigger('mousemove', 'right')
.trigger('mousemove', 'right') .should('have.class', 'cvat_canvas_shape_activated')
.should('have.class', 'cvat_canvas_shape_activated') .rightclick({ force: true });
.rightclick('right'); // When click in the center of polyline: is being covered by another element: <svg xmlns="http://www.w3.org/2000/svg" ...
} else {
cy.get(`#cvat_canvas_shape_${id}`)
.trigger('mousemove', 'right')
.should('have.class', 'cvat_canvas_shape_activated')
.rightclick();
}
cy.get('.cvat-canvas-context-menu') cy.get('.cvat-canvas-context-menu')
.last() .last()
.should('be.visible') .should('be.visible')
@ -158,7 +153,8 @@ context('Object make a copy.', () => {
() => { () => {
checkObjectArrSize(14, 16); // The point and tag was not copied via the object's context menu checkObjectArrSize(14, 16); // The point and tag was not copied via the object's context menu
for (let id = 1; id < countObject; id++) { for (let id = 1; id < countObject; id++) {
compareObjectsAttr(`#cvat_canvas_shape_${id}`, `#cvat_canvas_shape_${id + countObject + 7}`); // Parameters id 1 equal patameters id 13, 2 to 14, etc. // Parameters id 1 equal patameters id 13, 2 to 14, etc.
compareObjectsAttr(`#cvat_canvas_shape_${id}`, `#cvat_canvas_shape_${id + countObject + 7}`);
} }
for (let idSidebar = 1; idSidebar < 6; idSidebar++) { for (let idSidebar = 1; idSidebar < 6; idSidebar++) {
compareObjectsSidebarAttr( compareObjectsSidebarAttr(
@ -198,10 +194,10 @@ context('Object make a copy.', () => {
.should('have.class', 'cvat_canvas_shape_activated'); .should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').type('{ctrl}', { release: false }); // Hold cy.get('body').type('{ctrl}', { release: false }); // Hold
cy.get('body') cy.get('body')
.trigger('keydown', { keyCode: keyCodeC, ctrlKey: true }) .trigger('keydown', { keyCode: keyCodeC, code: 'KeyC', ctrlKey: true })
.trigger('keyup') .trigger('keyup', { keyCode: keyCodeC, code: 'KeyC', ctrlKey: true })
.trigger('keydown', { keyCode: keyCodeV, ctrlKey: true }) .trigger('keydown', { keyCode: keyCodeV, code: 'KeyV', ctrlKey: true })
.trigger('keyup'); .trigger('keyup', { keyCode: keyCodeC, code: 'KeyC', ctrlKey: true });
cy.get('.cvat-canvas-container').click(400, 300); cy.get('.cvat-canvas-container').click(400, 300);
cy.get('.cvat-canvas-container').click(500, 300); cy.get('.cvat-canvas-container').click(500, 300);
cy.get('body').type('{ctrl}'); // Unhold cy.get('body').type('{ctrl}'); // Unhold

@ -11,7 +11,7 @@ context('Redraw feature.', () => {
const createRectangleShape2Points = { const createRectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 150, firstX: 150,
firstY: 350, firstY: 350,
secondX: 250, secondX: 250,
@ -20,7 +20,7 @@ context('Redraw feature.', () => {
const createCuboidShape2Points = { const createCuboidShape2Points = {
points: 'From rectangle', points: 'From rectangle',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 300, firstX: 300,
firstY: 350, firstY: 350,
secondX: 400, secondX: 400,
@ -29,7 +29,7 @@ context('Redraw feature.', () => {
const createPolygonShape = { const createPolygonShape = {
reDraw: false, reDraw: false,
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 450, y: 350 }, { x: 450, y: 350 },
{ x: 550, y: 350 }, { x: 550, y: 350 },
@ -40,7 +40,7 @@ context('Redraw feature.', () => {
}; };
const createPolylinesShape = { const createPolylinesShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 600, y: 350 }, { x: 600, y: 350 },
{ x: 700, y: 350 }, { x: 700, y: 350 },
@ -51,7 +51,7 @@ context('Redraw feature.', () => {
}; };
const createPointsShape = { const createPointsShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [{ x: 750, y: 400 }], pointsMap: [{ x: 750, y: 400 }],
complete: true, complete: true,
numberOfPoints: null, numberOfPoints: null,
@ -67,7 +67,7 @@ context('Redraw feature.', () => {
cy.createRectangle(createRectangleShape2Points); cy.createRectangle(createRectangleShape2Points);
cy.get('.cvat-canvas-container').trigger('mousemove', 200, 400); cy.get('.cvat-canvas-container').trigger('mousemove', 200, 400);
cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the rectangle cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }); // Start redraw the rectangle
cy.get('.cvat-canvas-container') cy.get('.cvat-canvas-container')
.click(createRectangleShape2Points.firstX, createRectangleShape2Points.firstY - 50) .click(createRectangleShape2Points.firstX, createRectangleShape2Points.firstY - 50)
.click(createRectangleShape2Points.secondX, createRectangleShape2Points.secondY - 50); .click(createRectangleShape2Points.secondX, createRectangleShape2Points.secondY - 50);
@ -83,11 +83,12 @@ context('Redraw feature.', () => {
cy.createPolygon(createPolygonShape); cy.createPolygon(createPolygonShape);
cy.get('.cvat-canvas-container').trigger('mousemove', 520, 400); cy.get('.cvat-canvas-container').trigger('mousemove', 520, 400);
cy.get('#cvat_canvas_shape_2').should('have.class', 'cvat_canvas_shape_activated'); cy.get('#cvat_canvas_shape_2').should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the polygon cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }); // Start redraw the polygon
createPolygonShape.pointsMap.forEach((element) => { createPolygonShape.pointsMap.forEach((element) => {
cy.get('.cvat-canvas-container').click(element.x, element.y - 50); cy.get('.cvat-canvas-container').click(element.x, element.y - 50);
}); });
cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
cy.get('.cvat_canvas_shape').then(($shape) => { cy.get('.cvat_canvas_shape').then(($shape) => {
expect($shape.length).to.be.equal(2); expect($shape.length).to.be.equal(2);
}); });
@ -100,11 +101,12 @@ context('Redraw feature.', () => {
cy.createPolyline(createPolylinesShape); cy.createPolyline(createPolylinesShape);
cy.get('.cvat-canvas-container').trigger('mousemove', 700, 400); cy.get('.cvat-canvas-container').trigger('mousemove', 700, 400);
cy.get('#cvat_canvas_shape_3').should('have.class', 'cvat_canvas_shape_activated'); cy.get('#cvat_canvas_shape_3').should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the polyline cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }); // Start redraw the polyline
createPolylinesShape.pointsMap.forEach((element) => { createPolylinesShape.pointsMap.forEach((element) => {
cy.get('.cvat-canvas-container').click(element.x, element.y - 50); cy.get('.cvat-canvas-container').click(element.x, element.y - 50);
}); });
cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
cy.get('.cvat_canvas_shape').then(($shape) => { cy.get('.cvat_canvas_shape').then(($shape) => {
expect($shape.length).to.be.equal(3); expect($shape.length).to.be.equal(3);
}); });
@ -116,11 +118,12 @@ context('Redraw feature.', () => {
it('Draw and redraw a point.', () => { it('Draw and redraw a point.', () => {
cy.createPoint(createPointsShape); cy.createPoint(createPointsShape);
cy.get('.cvat-canvas-container').trigger('mousemove', 750, 400); cy.get('.cvat-canvas-container').trigger('mousemove', 750, 400);
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the point cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }); // Start redraw the point
createPointsShape.pointsMap.forEach((element) => { createPointsShape.pointsMap.forEach((element) => {
cy.get('.cvat-canvas-container').click(element.x, element.y - 50); cy.get('.cvat-canvas-container').click(element.x, element.y - 50);
}); });
cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
cy.get('.cvat_canvas_shape').then(($shape) => { cy.get('.cvat_canvas_shape').then(($shape) => {
expect($shape.length).to.be.equal(4); expect($shape.length).to.be.equal(4);
}); });
@ -133,18 +136,18 @@ context('Redraw feature.', () => {
cy.createCuboid(createCuboidShape2Points); cy.createCuboid(createCuboidShape2Points);
cy.get('.cvat-canvas-container').trigger('mousemove', 350, 400); cy.get('.cvat-canvas-container').trigger('mousemove', 350, 400);
cy.get('#cvat_canvas_shape_5').should('have.class', 'cvat_canvas_shape_activated'); cy.get('#cvat_canvas_shape_5').should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the cuboid cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }); // Start redraw the cuboid
cy.get('.cvat-canvas-container') cy.get('.cvat-canvas-container')
.click(createCuboidShape2Points.firstX, createCuboidShape2Points.firstY - 50) .click(createCuboidShape2Points.firstX, createCuboidShape2Points.firstY - 50)
.click(createCuboidShape2Points.secondX, createCuboidShape2Points.secondY - 50); .click(createCuboidShape2Points.secondX, createCuboidShape2Points.secondY - 50);
// Check issue 3219. Press "N" during the redrawing of the cuboid // Check issue 3219. Press "N" during the redrawing of the cuboid
cy.get('.cvat-canvas-container').trigger('mousemove', 350, 300); cy.get('.cvat-canvas-container').trigger('mousemove', 350, 300);
cy.get('#cvat_canvas_shape_5').should('have.class', 'cvat_canvas_shape_activated'); cy.get('#cvat_canvas_shape_5').should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }); // Start redraw the cuboid cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }); // Start redraw the cuboid
cy.get('.cvat-canvas-container') cy.get('.cvat-canvas-container')
.click(createCuboidShape2Points.firstX, createCuboidShape2Points.firstY - 100) .click(createCuboidShape2Points.firstX, createCuboidShape2Points.firstY - 100)
.trigger('mousemove', createCuboidShape2Points.secondX, createCuboidShape2Points.secondY - 100); .trigger('mousemove', createCuboidShape2Points.secondX, createCuboidShape2Points.secondY - 100);
cy.get('body').trigger('keydown', { keyCode: keyCodeN }); cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' });
cy.get('.cvat_canvas_shape_drawing').should('not.exist'); cy.get('.cvat_canvas_shape_drawing').should('not.exist');
cy.get('.cvat_canvas_shape').then(($shape) => { cy.get('.cvat_canvas_shape').then(($shape) => {
expect($shape.length).to.be.equal(5); expect($shape.length).to.be.equal(5);

@ -11,7 +11,7 @@ context('Repeat draw feature.', () => {
const createRectangleShape2Points = { const createRectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 150, firstX: 150,
firstY: 350, firstY: 350,
secondX: 250, secondX: 250,
@ -20,7 +20,7 @@ context('Repeat draw feature.', () => {
const createCuboidShape2Points = { const createCuboidShape2Points = {
points: 'From rectangle', points: 'From rectangle',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 300, firstX: 300,
firstY: 350, firstY: 350,
secondX: 400, secondX: 400,
@ -29,7 +29,7 @@ context('Repeat draw feature.', () => {
const createPolygonShape = { const createPolygonShape = {
redraw: false, redraw: false,
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 450, y: 350 }, { x: 450, y: 350 },
{ x: 550, y: 350 }, { x: 550, y: 350 },
@ -40,7 +40,7 @@ context('Repeat draw feature.', () => {
}; };
const createPolylinesShape = { const createPolylinesShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 600, y: 350 }, { x: 600, y: 350 },
{ x: 700, y: 350 }, { x: 700, y: 350 },
@ -51,7 +51,7 @@ context('Repeat draw feature.', () => {
}; };
const createPointsShape = { const createPointsShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [{ x: 750, y: 400 }], pointsMap: [{ x: 750, y: 400 }],
complete: true, complete: true,
numberOfPoints: null, numberOfPoints: null,
@ -69,11 +69,12 @@ context('Repeat draw feature.', () => {
} }
function repeatDrawningStart() { function repeatDrawningStart() {
cy.get('body').trigger('keydown', { keyCode: keyCodeN }); cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' });
} }
function repeatDrawningFinish() { function repeatDrawningFinish() {
cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
} }
before(() => { before(() => {

@ -11,7 +11,7 @@ context('Autoborder feature.', () => {
const createRectangleShape2Points = { const createRectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 400, firstX: 400,
firstY: 350, firstY: 350,
secondX: 500, secondX: 500,
@ -21,7 +21,7 @@ context('Autoborder feature.', () => {
const createRectangleShape2PointsSec = { const createRectangleShape2PointsSec = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 600, firstX: 600,
firstY: 350, firstY: 350,
secondX: 700, secondX: 700,
@ -29,10 +29,10 @@ context('Autoborder feature.', () => {
}; };
const keyCodeN = 78; const keyCodeN = 78;
let rectangleSvgJsCircleId = []; const rectangleSvgJsCircleId = [];
let rectangleSvgJsCircleIdSecond = []; const rectangleSvgJsCircleIdSecond = [];
let polygonSvgJsCircleId = []; const polygonSvgJsCircleId = [];
let polylineSvgJsCircleId = []; const polylineSvgJsCircleId = [];
function testCollectCxCircleCoord(arrToPush) { function testCollectCxCircleCoord(arrToPush) {
cy.get('circle').then((circle) => { cy.get('circle').then((circle) => {
@ -81,7 +81,8 @@ context('Autoborder feature.', () => {
cy.get('body').type('{Ctrl}'); // Autoborder activation cy.get('body').type('{Ctrl}'); // Autoborder activation
testAutoborderPointsCount(8); // 8 points at the rectangles testAutoborderPointsCount(8); // 8 points at the rectangles
cy.get('.cvat-canvas-container').click(400, 350).click(450, 250).click(500, 350).click(500, 450); cy.get('.cvat-canvas-container').click(400, 350).click(450, 250).click(500, 350).click(500, 450);
cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
cy.get('.cvat_canvas_autoborder_point').should('not.exist'); cy.get('.cvat_canvas_autoborder_point').should('not.exist');
// Collect the polygon points coordinates // Collect the polygon points coordinates
@ -99,7 +100,8 @@ context('Autoborder feature.', () => {
.click(550, 500) .click(550, 500)
.click(600, 450) .click(600, 450)
.click(600, 350); .click(600, 350);
cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('.cvat-canvas-container').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
cy.get('.cvat_canvas_autoborder_point').should('not.exist'); cy.get('.cvat_canvas_autoborder_point').should('not.exist');
// Collect the polygon points coordinates // Collect the polygon points coordinates
@ -108,9 +110,12 @@ context('Autoborder feature.', () => {
}); });
it('Checking whether the coordinates of the contact points of the shapes match.', () => { it('Checking whether the coordinates of the contact points of the shapes match.', () => {
expect(polygonSvgJsCircleId[0]).to.be.equal(rectangleSvgJsCircleId[0]); // The 1st point of the rect and the 1st polygon point expect(polygonSvgJsCircleId[0]).to
expect(polygonSvgJsCircleId[2]).to.be.equal(rectangleSvgJsCircleId[1]); // The 2nd point of the rect and the 3rd polygon point .be.equal(rectangleSvgJsCircleId[0]); // The 1st point of the rect and the 1st polygon point
expect(polylineSvgJsCircleId[1]).to.be.equal(rectangleSvgJsCircleId[3]); // The 2nd point of the polyline and the 4th point rect expect(polygonSvgJsCircleId[2]).to
.be.equal(rectangleSvgJsCircleId[1]); // The 2nd point of the rect and the 3rd polygon point
expect(polylineSvgJsCircleId[1]).to
.be.equal(rectangleSvgJsCircleId[3]); // The 2nd point of the polyline and the 4th point rect
}); });
}); });
}); });

@ -17,7 +17,7 @@ context('Shortcuts window.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Press "F1" from a task. Shortcuts window be visible. Closing the modal window by button "OK".', () => { it('Press "F1" from a task. Shortcuts window be visible. Closing the modal window by button "OK".', () => {
cy.get('body').trigger('keydown', { keyCode: keyCodeF1 }); cy.get('body').trigger('keydown', { keyCode: keyCodeF1, code: 'F1' });
cy.get('.cvat-shortcuts-modal-window') cy.get('.cvat-shortcuts-modal-window')
.should('exist') .should('exist')
.and('be.visible') .and('be.visible')
@ -36,7 +36,7 @@ context('Shortcuts window.', () => {
it('Open a job. Press "F1". Shortcuts window be visible. Closing the modal window by F1.', () => { it('Open a job. Press "F1". Shortcuts window be visible. Closing the modal window by F1.', () => {
cy.openJob(); cy.openJob();
cy.get('body').trigger('keydown', { keyCode: keyCodeF1 }); cy.get('body').trigger('keydown', { keyCode: keyCodeF1, code: 'F1' });
cy.get('.cvat-shortcuts-modal-window') cy.get('.cvat-shortcuts-modal-window')
.should('exist') .should('exist')
.and('be.visible') .and('be.visible')
@ -49,7 +49,7 @@ context('Shortcuts window.', () => {
}); });
}); });
}); });
cy.get('body').trigger('keydown', { keyCode: keyCodeF1 }); cy.get('body').trigger('keydown', { keyCode: keyCodeF1, code: 'F1' });
cy.get('.cvat-shortcuts-modal-window').should('not.be.visible'); cy.get('.cvat-shortcuts-modal-window').should('not.be.visible');
}); });
}); });

@ -11,7 +11,7 @@ context('OpenCV. Intelligent scissors. Histogram Equalization.', () => {
const caseId = '101'; const caseId = '101';
const newLabel = `Case ${caseId}`; const newLabel = `Case ${caseId}`;
const createOpencvShape = { const createOpencvShape = {
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 200, y: 200 }, { x: 200, y: 200 },
{ x: 250, y: 200 }, { x: 250, y: 200 },
@ -74,9 +74,9 @@ context('OpenCV. Intelligent scissors. Histogram Equalization.', () => {
cy.get('.cvat-approx-poly-threshold-wrapper') cy.get('.cvat-approx-poly-threshold-wrapper')
.find('[role="slider"]') .find('[role="slider"]')
.type(generateString(4, 'rightarrow')); .type(generateString(4, 'rightarrow'));
cy.get('.cvat_canvas_interact_intermediate_shape').then((intermediateShape) => { cy.get('.cvat_canvas_interact_intermediate_shape').then((_intermediateShape) => {
// Get count of points againe // Get count of points againe
const intermediateShapeNumberPointsAfterChange = intermediateShape.attr('points').split(' ').length; const intermediateShapeNumberPointsAfterChange = _intermediateShape.attr('points').split(' ').length;
// expected 7 to be below 10 // expected 7 to be below 10
expect(intermediateShapeNumberPointsBeforeChange).to.be.lt( expect(intermediateShapeNumberPointsBeforeChange).to.be.lt(
intermediateShapeNumberPointsAfterChange, intermediateShapeNumberPointsAfterChange,
@ -152,12 +152,12 @@ context('OpenCV. Intelligent scissors. Histogram Equalization.', () => {
.trigger('mousemove') .trigger('mousemove')
.trigger('mouseover') .trigger('mouseover')
.should('have.class', 'cvat_canvas_shape_activated'); .should('have.class', 'cvat_canvas_shape_activated');
cy.get('body').trigger('keydown', { keyCode: keyCodeN, shiftKey: true }).trigger('keyup'); cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true }).trigger('keyup');
cy.get('.cvat-tools-control').should('have.attr', 'tabindex'); cy.get('.cvat-tools-control').should('have.attr', 'tabindex');
createOpencvShape.pointsMap.forEach((el) => { createOpencvShape.pointsMap.forEach((el) => {
cy.get('.cvat-canvas-container').click(el.x + 150, el.y + 50); cy.get('.cvat-canvas-container').click(el.x + 150, el.y + 50);
}); });
cy.get('body').trigger('keydown', { keyCode: keyCodeN }).trigger('keyup'); cy.get('body').trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' }).trigger('keyup');
}); });
}); });
}); });

@ -6,12 +6,12 @@
import { taskName, labelName } from '../../support/const'; import { taskName, labelName } from '../../support/const';
context("The points of the previous polygon mustn't appear while polygon's interpolation.", () => { context('The points of the previous polygon mustn\'t appear while polygon\'s interpolation.', () => {
const issueId = '1882'; const issueId = '1882';
const createPolygonTrack = { const createPolygonTrack = {
reDraw: false, reDraw: false,
type: 'Track', type: 'Track',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 309, y: 431 }, { x: 309, y: 431 },
{ x: 360, y: 500 }, { x: 360, y: 500 },
@ -23,7 +23,7 @@ context("The points of the previous polygon mustn't appear while polygon's inter
const reDrawPolygonTrack = { const reDrawPolygonTrack = {
reDraw: true, reDraw: true,
type: 'Track', type: 'Track',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 359, y: 431 }, { x: 359, y: 431 },
{ x: 410, y: 500 }, { x: 410, y: 500 },
@ -46,8 +46,10 @@ context("The points of the previous polygon mustn't appear while polygon's inter
const keyCodeN = 78; const keyCodeN = 78;
cy.get('#cvat_canvas_shape_1') cy.get('#cvat_canvas_shape_1')
.trigger('mousemove', { force: true }) .trigger('mousemove', { force: true })
.trigger('keydown', { keyCode: keyCodeN, shiftKey: true }) .trigger('keydown', { keyCode: keyCodeN, code: 'KeyN', shiftKey: true })
.trigger('keyup', { force: true }, { keyCode: keyCodeN, shiftKey: true }); .trigger('keyup', {
force: true, keyCode: keyCodeN, code: 'KeyN', shiftKey: true
});
cy.createPolygon(reDrawPolygonTrack); cy.createPolygon(reDrawPolygonTrack);
}); });
it('Activate auto bordering mode', () => { it('Activate auto bordering mode', () => {

@ -11,7 +11,7 @@ context('Check hide functionality (H)', () => {
const createRectangleShape2Points = { const createRectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 250, firstX: 250,
firstY: 350, firstY: 350,
secondX: 350, secondX: 350,
@ -29,7 +29,7 @@ context('Check hide functionality (H)', () => {
cy.get('#cvat_canvas_shape_1') cy.get('#cvat_canvas_shape_1')
.trigger('mousemove') .trigger('mousemove')
.trigger('mouseover') .trigger('mouseover')
.trigger('keydown', { keyCode: keyCodeH }) .trigger('keydown', { keyCode: keyCodeH, code: 'KeyH' })
.should('be.hidden'); .should('be.hidden');
}); });
}); });

@ -27,7 +27,7 @@ context('Check if the UI not to crash after remove a tag', () => {
.should('contain', '1') .should('contain', '1')
.and('contain', 'TAG') .and('contain', 'TAG')
.trigger('mouseover') .trigger('mouseover')
.trigger('keydown', { keyCode: keyCodeDel }); .trigger('keydown', { keyCode: keyCodeDel, code: 'Delete' });
}); });
it('Page with the error is missing', () => { it('Page with the error is missing', () => {
cy.contains('Oops, something went wrong').should('not.exist'); cy.contains('Oops, something went wrong').should('not.exist');

@ -11,7 +11,7 @@ context('First part of a split track is visible', () => {
const createRectangleTrack2Points = { const createRectangleTrack2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Track', type: 'Track',
labelName: labelName, labelName,
firstX: 250, firstX: 250,
firstY: 350, firstY: 350,
secondX: 350, secondX: 350,
@ -34,7 +34,7 @@ context('First part of a split track is visible', () => {
}); });
it('Split track', () => { it('Split track', () => {
cy.get('body').type('{alt}m'); cy.get('body').type('{alt}m');
cy.get('#cvat_canvas_shape_1').trigger('mousemove', { which: 1 }).trigger('click', { which: 1 }); cy.get('#cvat_canvas_shape_1').trigger('mousemove', { button: 0 }).trigger('click', { button: 0 });
}); });
it('Go to previous frame', () => { it('Go to previous frame', () => {
cy.get('.cvat-player-previous-button').click(); cy.get('.cvat-player-previous-button').click();

@ -19,7 +19,9 @@ Cypress.Commands.add('login', (username = Cypress.env('user'), password = Cypres
cy.url().should('match', /\/tasks$/); cy.url().should('match', /\/tasks$/);
cy.document().then((doc) => { cy.document().then((doc) => {
const loadSettingFailNotice = Array.from(doc.querySelectorAll('.cvat-notification-notice-load-settings-fail')); const loadSettingFailNotice = Array.from(doc.querySelectorAll('.cvat-notification-notice-load-settings-fail'));
loadSettingFailNotice.length > 0 ? cy.closeNotification('.cvat-notification-notice-load-settings-fail') : null; if (loadSettingFailNotice.length > 0) {
cy.closeNotification('.cvat-notification-notice-load-settings-fail');
}
}); });
}); });
@ -56,17 +58,17 @@ Cypress.Commands.add('deletingRegisteredUsers', (accountToDelete) => {
password: Cypress.env('password'), password: Cypress.env('password'),
}, },
}).then((response) => { }).then((response) => {
const authKey = response['body']['key']; const authKey = response.body.key;
cy.request({ cy.request({
url: '/api/v1/users?page_size=all', url: '/api/v1/users?page_size=all',
headers: { headers: {
Authorization: `Token ${authKey}`, Authorization: `Token ${authKey}`,
}, },
}).then((response) => { }).then((_response) => {
const responceResult = response['body']['results']; const responceResult = _response.body.results;
for (const user of responceResult) { for (const user of responceResult) {
const userId = user['id']; const userId = user.id;
const userName = user['username']; const userName = user.username;
for (const account of accountToDelete) { for (const account of accountToDelete) {
if (userName === account) { if (userName === account) {
cy.request({ cy.request({
@ -90,10 +92,10 @@ Cypress.Commands.add('changeUserActiveStatus', (authKey, accountsToChangeActiveS
Authorization: `Token ${authKey}`, Authorization: `Token ${authKey}`,
}, },
}).then((response) => { }).then((response) => {
const responceResult = response['body']['results']; const responceResult = response.body.results;
responceResult.forEach((user) => { responceResult.forEach((user) => {
const userId = user['id']; const userId = user.id;
const userName = user['username']; const userName = user.username;
if (userName.includes(accountsToChangeActiveStatus)) { if (userName.includes(accountsToChangeActiveStatus)) {
cy.request({ cy.request({
method: 'PATCH', method: 'PATCH',
@ -117,12 +119,12 @@ Cypress.Commands.add('checkUserStatuses', (authKey, userName, staffStatus, super
Authorization: `Token ${authKey}`, Authorization: `Token ${authKey}`,
}, },
}).then((response) => { }).then((response) => {
const responceResult = response['body']['results']; const responceResult = response.body.results;
responceResult.forEach((user) => { responceResult.forEach((user) => {
if (user['username'].includes(userName)) { if (user.username.includes(userName)) {
expect(staffStatus).to.be.equal(user['is_staff']); expect(staffStatus).to.be.equal(user.is_staff);
expect(superuserStatus).to.be.equal(user['is_superuser']); expect(superuserStatus).to.be.equal(user.is_superuser);
expect(activeStatus).to.be.equal(user['is_active']); expect(activeStatus).to.be.equal(user.is_active);
} }
}); });
}); });
@ -211,9 +213,7 @@ Cypress.Commands.add('getJobNum', (jobID) => {
.find('td') .find('td')
.eq(0) .eq(0)
.invoke('text') .invoke('text')
.then(($tdText) => { .then(($tdText) => (Number($tdText.match(/\d+/g)) + jobID));
return Number($tdText.match(/\d+/g)) + jobID;
});
}); });
Cypress.Commands.add('openJob', (jobID = 0, removeAnnotations = true, expectedFail = false) => { Cypress.Commands.add('openJob', (jobID = 0, removeAnnotations = true, expectedFail = false) => {
@ -221,9 +221,12 @@ Cypress.Commands.add('openJob', (jobID = 0, removeAnnotations = true, expectedFa
cy.get('.cvat-task-jobs-table-row').contains('a', `Job #${$job}`).click(); cy.get('.cvat-task-jobs-table-row').contains('a', `Job #${$job}`).click();
}); });
cy.url().should('include', '/jobs'); cy.url().should('include', '/jobs');
expectedFail if (expectedFail) {
? cy.get('.cvat-canvas-container').should('not.exist') cy.get('.cvat-canvas-container').should('not.exist');
: cy.get('.cvat-canvas-container').should('exist'); } else {
cy.get('.cvat-canvas-container').should('exist');
}
if (removeAnnotations) { if (removeAnnotations) {
cy.document().then((doc) => { cy.document().then((doc) => {
const objects = Array.from(doc.querySelectorAll('.cvat_canvas_shape')); const objects = Array.from(doc.querySelectorAll('.cvat_canvas_shape'));
@ -284,7 +287,7 @@ Cypress.Commands.add('checkPopoverHidden', (objectType) => {
}); });
Cypress.Commands.add('checkObjectParameters', (objectParameters, objectType) => { Cypress.Commands.add('checkObjectParameters', (objectParameters, objectType) => {
let listCanvasShapeId = []; const listCanvasShapeId = [];
cy.document().then((doc) => { cy.document().then((doc) => {
const listCanvasShape = Array.from(doc.querySelectorAll('.cvat_canvas_shape')); const listCanvasShape = Array.from(doc.querySelectorAll('.cvat_canvas_shape'));
for (let i = 0; i < listCanvasShape.length; i++) { for (let i = 0; i < listCanvasShape.length; i++) {
@ -318,13 +321,11 @@ Cypress.Commands.add('createPoint', (createPointParams) => {
}); });
if (createPointParams.finishWithButton) { if (createPointParams.finishWithButton) {
cy.contains('span', 'Done').click(); cy.contains('span', 'Done').click();
} else { } else if (!createPointParams.numberOfPoints) {
if (!createPointParams.numberOfPoints) { const keyCodeN = 78;
const keyCodeN = 78; cy.get('.cvat-canvas-container')
cy.get('.cvat-canvas-container') .trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keydown', { keyCode: keyCodeN }) .trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
.trigger('keyup', { keyCode: keyCodeN });
}
} }
cy.checkPopoverHidden('draw-points'); cy.checkPopoverHidden('draw-points');
cy.checkObjectParameters(createPointParams, 'POINTS'); cy.checkObjectParameters(createPointParams, 'POINTS');
@ -339,13 +340,13 @@ Cypress.Commands.add('changeAppearance', (colorBy) => {
Cypress.Commands.add('shapeGrouping', (firstX, firstY, lastX, lastY) => { Cypress.Commands.add('shapeGrouping', (firstX, firstY, lastX, lastY) => {
const keyCodeG = 71; const keyCodeG = 71;
cy.get('.cvat-canvas-container') cy.get('.cvat-canvas-container')
.trigger('keydown', { keyCode: keyCodeG }) .trigger('keydown', { keyCode: keyCodeG, code: 'KeyG' })
.trigger('keyup', { keyCode: keyCodeG }) .trigger('keyup', { keyCode: keyCodeG, code: 'KeyG' })
.trigger('mousedown', firstX, firstY, { which: 1 }) .trigger('mousedown', firstX, firstY, { which: 1 })
.trigger('mousemove', lastX, lastY) .trigger('mousemove', lastX, lastY)
.trigger('mouseup', lastX, lastY) .trigger('mouseup', lastX, lastY)
.trigger('keydown', { keyCode: keyCodeG }) .trigger('keydown', { keyCode: keyCodeG, code: 'KeyG' })
.trigger('keyup', { keyCode: keyCodeG }); .trigger('keyup', { keyCode: keyCodeG, code: 'KeyG' });
}); });
Cypress.Commands.add('createPolygon', (createPolygonParams) => { Cypress.Commands.add('createPolygon', (createPolygonParams) => {
@ -367,13 +368,11 @@ Cypress.Commands.add('createPolygon', (createPolygonParams) => {
}); });
if (createPolygonParams.finishWithButton) { if (createPolygonParams.finishWithButton) {
cy.contains('span', 'Done').click(); cy.contains('span', 'Done').click();
} else { } else if (!createPolygonParams.numberOfPoints) {
if (!createPolygonParams.numberOfPoints) { const keyCodeN = 78;
const keyCodeN = 78; cy.get('.cvat-canvas-container')
cy.get('.cvat-canvas-container') .trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keydown', { keyCode: keyCodeN }) .trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
.trigger('keyup', { keyCode: keyCodeN });
}
} }
cy.checkPopoverHidden('draw-polygon'); cy.checkPopoverHidden('draw-polygon');
cy.checkObjectParameters(createPolygonParams, 'POLYGON'); cy.checkObjectParameters(createPolygonParams, 'POLYGON');
@ -444,7 +443,7 @@ Cypress.Commands.add('createCuboid', (createCuboidParams) => {
}); });
Cypress.Commands.add('updateAttributes', (multiAttrParams) => { Cypress.Commands.add('updateAttributes', (multiAttrParams) => {
let cvatAttributeInputsWrapperId = []; const cvatAttributeInputsWrapperId = [];
cy.get('.cvat-new-attribute-button').click(); cy.get('.cvat-new-attribute-button').click();
cy.document().then((doc) => { cy.document().then((doc) => {
const cvatAttributeInputsWrapperList = Array.from(doc.querySelectorAll('.cvat-attribute-inputs-wrapper')); const cvatAttributeInputsWrapperList = Array.from(doc.querySelectorAll('.cvat-attribute-inputs-wrapper'));
@ -513,13 +512,11 @@ Cypress.Commands.add('createPolyline', (createPolylineParams) => {
}); });
if (createPolylineParams.finishWithButton) { if (createPolylineParams.finishWithButton) {
cy.contains('span', 'Done').click(); cy.contains('span', 'Done').click();
} else { } else if (!createPolylineParams.numberOfPoints) {
if (!createPolylineParams.numberOfPoints) { const keyCodeN = 78;
const keyCodeN = 78; cy.get('.cvat-canvas-container')
cy.get('.cvat-canvas-container') .trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keydown', { keyCode: keyCodeN }) .trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
.trigger('keyup', { keyCode: keyCodeN });
}
} }
cy.checkPopoverHidden('draw-polyline'); cy.checkPopoverHidden('draw-polyline');
cy.checkObjectParameters(createPolylineParams, 'POLYLINE'); cy.checkObjectParameters(createPolylineParams, 'POLYLINE');
@ -591,7 +588,7 @@ Cypress.Commands.add('changeColorViaBadge', (labelColor) => {
}); });
Cypress.Commands.add('collectLabelsName', () => { Cypress.Commands.add('collectLabelsName', () => {
let listCvatConstructorViewerItemText = []; const listCvatConstructorViewerItemText = [];
cy.get('.cvat-constructor-viewer').should('exist'); cy.get('.cvat-constructor-viewer').should('exist');
cy.document().then((doc) => { cy.document().then((doc) => {
const labels = Array.from(doc.querySelectorAll('.cvat-constructor-viewer-item')); const labels = Array.from(doc.querySelectorAll('.cvat-constructor-viewer-item'));
@ -661,9 +658,7 @@ Cypress.Commands.add('goToRegisterPage', () => {
Cypress.Commands.add('getScaleValue', () => { Cypress.Commands.add('getScaleValue', () => {
cy.get('#cvat_canvas_background') cy.get('#cvat_canvas_background')
.should('have.attr', 'style') .should('have.attr', 'style')
.then(($styles) => { .then(($styles) => (Number($styles.match(/scale\((\d\.\d+)\)/m)[1])));
return Number($styles.match(/scale\((\d\.\d+)\)/m)[1]);
});
}); });
Cypress.Commands.add('goCheckFrameNumber', (frameNum) => { Cypress.Commands.add('goCheckFrameNumber', (frameNum) => {
@ -713,9 +708,7 @@ Cypress.Commands.add('getObjectIdNumberByLabelName', (labelName) => {
cy.get(stateItemLabelSelectorList[i]) cy.get(stateItemLabelSelectorList[i])
.parents('.cvat-objects-sidebar-state-item') .parents('.cvat-objects-sidebar-state-item')
.should('have.attr', 'id') .should('have.attr', 'id')
.then((id) => { .then((id) => (Number(id.match(/\d+$/))));
return Number(id.match(/\d+$/));
});
} }
} }
}); });
@ -729,7 +722,9 @@ Cypress.Commands.add('closeModalUnsupportedPlatform', () => {
} }
}); });
Cypress.Commands.add('exportTask', ({ as, type, format, archiveCustomeName }) => { Cypress.Commands.add('exportTask', ({
as, type, format, archiveCustomeName,
}) => {
cy.interactMenu('Export task dataset'); cy.interactMenu('Export task dataset');
cy.intercept('GET', `/api/v1/tasks/**/${type}**`).as(as); cy.intercept('GET', `/api/v1/tasks/**/${type}**`).as(as);
cy.get('.cvat-modal-export-task').should('be.visible').find('.cvat-modal-export-select').click(); cy.get('.cvat-modal-export-task').should('be.visible').find('.cvat-modal-export-select').click();

@ -35,15 +35,15 @@ Cypress.Commands.add('opencvCreateShape', (opencvShapeParams) => {
} else { } else {
const keyCodeN = 78; const keyCodeN = 78;
cy.get('.cvat-canvas-container') cy.get('.cvat-canvas-container')
.trigger('keydown', { keyCode: keyCodeN }) .trigger('keydown', { keyCode: keyCodeN, code: 'KeyN' })
.trigger('keyup', { keyCode: keyCodeN }); .trigger('keyup', { keyCode: keyCodeN, code: 'KeyN' });
} }
cy.checkPopoverHidden('opencv-control'); cy.checkPopoverHidden('opencv-control');
cy.opencvCheckObjectParameters('POLYGON'); cy.opencvCheckObjectParameters('POLYGON');
}); });
Cypress.Commands.add('opencvCheckObjectParameters', (objectType) => { Cypress.Commands.add('opencvCheckObjectParameters', (objectType) => {
let listCanvasShapeId = []; const listCanvasShapeId = [];
cy.document().then((doc) => { cy.document().then((doc) => {
const listCanvasShape = Array.from(doc.querySelectorAll('.cvat_canvas_shape')); const listCanvasShape = Array.from(doc.querySelectorAll('.cvat_canvas_shape'));
for (let i = 0; i < listCanvasShape.length; i++) { for (let i = 0; i < listCanvasShape.length; i++) {

2082
tests/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -4,15 +4,13 @@
"cypress:run:firefox": "cypress run --env coverage=false --browser firefox --config-file cypress_cron_type.json", "cypress:run:firefox": "cypress run --env coverage=false --browser firefox --config-file cypress_cron_type.json",
"cypress:run:chrome:canvas3d": "cypress run --headed --browser chrome --env coverage=false --config-file cypress_canvas3d.json" "cypress:run:chrome:canvas3d": "cypress run --headed --browser chrome --env coverage=false --config-file cypress_canvas3d.json"
}, },
"devDependencies": { "dependencies": {
"archiver": "^5.3.0",
"jimp": "^0.16.1",
"@cypress/code-coverage": "^3.9.10", "@cypress/code-coverage": "^3.9.10",
"cypress": "^8.3.1", "cypress": "^8.3.1",
"cypress-file-upload": "^5.0.8", "cypress-file-upload": "^5.0.8",
"cypress-localstorage-commands": "^1.5.0", "cypress-localstorage-commands": "^1.5.0",
"cypress-plugin-tab": "^1.0.5" "cypress-plugin-tab": "^1.0.5"
},
"dependencies": {
"archiver": "^5.3.0",
"jimp": "^0.16.1"
} }
} }

Loading…
Cancel
Save