Added support of ellipses (#4062)

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

@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- User is able to customize information that text labels show (<https://github.com/openvinotoolkit/cvat/pull/4029>) - User is able to customize information that text labels show (<https://github.com/openvinotoolkit/cvat/pull/4029>)
- Support for uploading manifest with any name (<https://github.com/openvinotoolkit/cvat/pull/4041>) - Support for uploading manifest with any name (<https://github.com/openvinotoolkit/cvat/pull/4041>)
- Added information about OpenVINO toolkit to login page (<https://github.com/openvinotoolkit/cvat/pull/4077>) - Added information about OpenVINO toolkit to login page (<https://github.com/openvinotoolkit/cvat/pull/4077>)
- Support for working with ellipses (<https://github.com/openvinotoolkit/cvat/pull/4062>)
### Changed ### Changed
- Users don't have access to a task object anymore if they are assigneed only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>) - Users don't have access to a task object anymore if they are assigneed only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>)

@ -1,12 +1,12 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.11.1", "version": "2.12.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.11.1", "version": "2.12.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/polylabel": "^1.0.5", "@types/polylabel": "^1.0.5",

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.11.1", "version": "2.12.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {

@ -253,6 +253,10 @@ export class AutoborderHandlerImpl implements AutoborderHandler {
let points = ''; let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') { if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points'); points = shape.getAttribute('points');
} else if (shape.tagName === 'ellipse') {
const cx = +shape.getAttribute('cx');
const cy = +shape.getAttribute('cy');
points = `${cx},${cy}`;
} else if (shape.tagName === 'rect') { } else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x'); const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y'); const y = +shape.getAttribute('y');

@ -25,6 +25,7 @@ import {
translateToSVG, translateToSVG,
translateFromSVG, translateFromSVG,
translateToCanvas, translateToCanvas,
translateFromCanvas,
pointsToNumberArray, pointsToNumberArray,
parsePoints, parsePoints,
displayShapeSize, displayShapeSize,
@ -33,6 +34,7 @@ import {
ShapeSizeElement, ShapeSizeElement,
DrawnState, DrawnState,
rotate2DPoints, rotate2DPoints,
readPointsFromShape,
} from './shared'; } from './shared';
import { import {
CanvasModel, CanvasModel,
@ -88,7 +90,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private activeElement: ActiveElement; private activeElement: ActiveElement;
private configuration: Configuration; private configuration: Configuration;
private snapToAngleResize: number; private snapToAngleResize: number;
private serviceFlags: { private innerObjectsFlags: {
drawHidden: Record<number, boolean>; drawHidden: Record<number, boolean>;
}; };
@ -112,7 +114,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private translateFromCanvas(points: number[]): number[] { private translateFromCanvas(points: number[]): number[] {
const { offset } = this.controller.geometry; const { offset } = this.controller.geometry;
return points.map((coord: number): number => coord - offset); return translateFromCanvas(offset, points);
} }
private translatePointsFromRotatedShape(shape: SVG.Shape, points: number[]): number[] { private translatePointsFromRotatedShape(shape: SVG.Shape, points: number[]): number[] {
@ -158,12 +160,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
}, ''); }, '');
} }
private isServiceHidden(clientID: number): boolean { private isInnerHidden(clientID: number): boolean {
return this.serviceFlags.drawHidden[clientID] || false; return this.innerObjectsFlags.drawHidden[clientID] || false;
} }
private setupServiceHidden(clientID: number, value: boolean): void { private setupInnerFlags(clientID: number, path: 'drawHidden', value: boolean): void {
this.serviceFlags.drawHidden[clientID] = value; this.innerObjectsFlags[path][clientID] = value;
const shape = this.svgShapes[clientID]; const shape = this.svgShapes[clientID];
const text = this.svgTexts[clientID]; const text = this.svgTexts[clientID];
const state = this.drawnStates[clientID]; const state = this.drawnStates[clientID];
@ -179,7 +181,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
text.addClass('cvat_canvas_hidden'); text.addClass('cvat_canvas_hidden');
} }
} else { } else {
delete this.serviceFlags.drawHidden[clientID]; delete this.innerObjectsFlags[path][clientID];
if (state) { if (state) {
if (!state.outside && !state.hidden) { if (!state.outside && !state.hidden) {
@ -236,10 +238,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private onDrawDone(data: any | 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.innerObjectsFlags.drawHidden)
.map((_clientID): number => +_clientID);
if (hiddenBecauseOfDraw.length) { if (hiddenBecauseOfDraw.length) {
for (const hidden of hiddenBecauseOfDraw) { for (const hidden of hiddenBecauseOfDraw) {
this.setupServiceHidden(hidden, false); this.setupInnerFlags(hidden, 'drawHidden', false);
} }
} }
@ -867,7 +870,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
(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: shape.type === 'rect', rotationPoint: shape.type === 'rect' || shape.type === 'ellipse',
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)
@ -967,7 +970,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.snapToAngleResize = consts.SNAP_TO_ANGLE_RESIZE_DEFAULT;
this.serviceFlags = { this.innerObjectsFlags = {
drawHidden: {}, drawHidden: {},
}; };
@ -1367,7 +1370,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.style.cursor = 'crosshair'; this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW; this.mode = Mode.DRAW;
if (typeof data.redraw === 'number') { if (typeof data.redraw === 'number') {
this.setupServiceHidden(data.redraw, true); this.setupInnerFlags(data.redraw, 'drawHidden', true);
} }
this.drawHandler.draw(data, this.geometry); this.drawHandler.draw(data, this.geometry);
} else { } else {
@ -1544,6 +1547,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
ctx.fill(); ctx.fill();
} }
if (state.shapeType === 'ellipse') {
const [cx, cy, rightX, topY] = state.points;
ctx.beginPath();
ctx.ellipse(cx, cy, rightX - cx, cy - topY, (state.rotation * Math.PI) / 180.0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
}
if (state.shapeType === 'cuboid') { if (state.shapeType === 'cuboid') {
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const points = [ const points = [
@ -1596,7 +1607,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const drawnState = this.drawnStates[clientID]; const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[state.clientID]; const shape = this.svgShapes[state.clientID];
const text = this.svgTexts[state.clientID]; const text = this.svgTexts[state.clientID];
const isInvisible = state.hidden || state.outside || this.isServiceHidden(state.clientID); const isInvisible = state.hidden || state.outside || this.isInnerHidden(state.clientID);
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) { if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
if (isInvisible) { if (isInvisible) {
@ -1659,6 +1670,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
width: xbr - xtl, width: xbr - xtl,
height: ybr - ytl, height: ybr - ytl,
}); });
} else if (state.shapeType === 'ellipse') {
const [cx, cy] = translatedPoints;
const [rx, ry] = [translatedPoints[2] - cx, cy - translatedPoints[3]];
shape.attr({
cx, cy, rx, ry,
});
} else { } else {
const stringified = this.stringifyToCanvas(translatedPoints); const stringified = this.stringifyToCanvas(translatedPoints);
if (state.shapeType !== 'cuboid') { if (state.shapeType !== 'cuboid') {
@ -1728,6 +1745,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.svgShapes[state.clientID] = this.addPolyline(stringified, state); this.svgShapes[state.clientID] = this.addPolyline(stringified, state);
} else if (state.shapeType === 'points') { } else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this.addPoints(stringified, state); this.svgShapes[state.clientID] = this.addPoints(stringified, state);
} else if (state.shapeType === 'ellipse') {
this.svgShapes[state.clientID] = this.addEllipse(stringified, state);
} else if (state.shapeType === 'cuboid') { } else if (state.shapeType === 'cuboid') {
this.svgShapes[state.clientID] = this.addCuboid(stringified, state); this.svgShapes[state.clientID] = this.addCuboid(stringified, state);
} else { } else {
@ -1933,10 +1952,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (Math.sqrt(dx2 + dy2) >= delta) { if (Math.sqrt(dx2 + dy2) >= delta) {
// these points does not take into account possible transformations, applied on the element // 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 // so, if any (like rotation) we need to map them to canvas coordinate space
let points = pointsToNumberArray( let points = readPointsFromShape(shape);
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`,
);
// let's keep current points, but they could be rewritten in updateObjects // let's keep current points, but they could be rewritten in updateObjects
this.drawnStates[clientID].points = this.translateFromCanvas(points); this.drawnStates[clientID].points = this.translateFromCanvas(points);
@ -2021,10 +2037,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// these points does not take into account possible transformations, applied on the element // 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 // so, if any (like rotation) we need to map them to canvas coordinate space
let points = pointsToNumberArray( let points = readPointsFromShape(shape);
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` +
`${shape.attr('x') + shape.attr('width')},${shape.attr('y') + shape.attr('height')}`,
);
// let's keep current points, but they could be rewritten in updateObjects // let's keep current points, but they could be rewritten in updateObjects
this.drawnStates[clientID].points = this.translateFromCanvas(points); this.drawnStates[clientID].points = this.translateFromCanvas(points);
@ -2101,6 +2114,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
// for rectangle finding a center is simple // for rectangle finding a center is simple
cx = +shape.attr('x') + +shape.attr('width') / 2; cx = +shape.attr('x') + +shape.attr('width') / 2;
cy = +shape.attr('y') + +shape.attr('height') / 2; cy = +shape.attr('y') + +shape.attr('height') / 2;
} else if (shape.type === 'ellipse') {
// even simpler for ellipses
cx = +shape.attr('cx');
cy = +shape.attr('cy');
} else { } else {
// for polyshapes we use special algorithm // for polyshapes we use special algorithm
const points = parsePoints(pointsToNumberArray(shape.attr('points'))); const points = parsePoints(pointsToNumberArray(shape.attr('points')));
@ -2247,7 +2264,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
rect.addClass('cvat_canvas_shape_occluded'); rect.addClass('cvat_canvas_shape_occluded');
} }
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) { if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
rect.addClass('cvat_canvas_hidden'); rect.addClass('cvat_canvas_hidden');
} }
@ -2273,7 +2290,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
polygon.addClass('cvat_canvas_shape_occluded'); polygon.addClass('cvat_canvas_shape_occluded');
} }
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) { if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
polygon.addClass('cvat_canvas_hidden'); polygon.addClass('cvat_canvas_hidden');
} }
@ -2299,7 +2316,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
polyline.addClass('cvat_canvas_shape_occluded'); polyline.addClass('cvat_canvas_shape_occluded');
} }
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) { if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
polyline.addClass('cvat_canvas_hidden'); polyline.addClass('cvat_canvas_hidden');
} }
@ -2326,7 +2343,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
cube.addClass('cvat_canvas_shape_occluded'); cube.addClass('cvat_canvas_shape_occluded');
} }
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) { if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
cube.addClass('cvat_canvas_hidden'); cube.addClass('cvat_canvas_hidden');
} }
@ -2359,6 +2376,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
return group; return group;
} }
private addEllipse(points: string, state: any): SVG.Rect {
const [cx, cy, rightX, topY] = points.split(/[/,\s]/g).map((coord) => +coord);
const [rx, ry] = [rightX - cx, cy - topY];
const rect = this.adoptedContent
.ellipse(rx * 2, ry * 2)
.attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'data-z-order': state.zOrder,
})
.center(cx, cy)
.addClass('cvat_canvas_shape');
if (state.rotation) {
rect.rotate(state.rotation);
}
if (state.occluded) {
rect.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
rect.addClass('cvat_canvas_hidden');
}
return rect;
}
private addPoints(points: string, state: any): SVG.PolyLine { private addPoints(points: string, state: any): SVG.PolyLine {
const shape = this.adoptedContent const shape = this.adoptedContent
.polyline(points) .polyline(points)
@ -2375,7 +2425,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = this.setupPoints(shape, state); const group = this.setupPoints(shape, state);
if (state.hidden || state.outside || this.isServiceHidden(state.clientID)) { if (state.hidden || state.outside || this.isInnerHidden(state.clientID)) {
group.addClass('cvat_canvas_hidden'); group.addClass('cvat_canvas_hidden');
} }

@ -16,6 +16,7 @@ import {
Box, Box,
Point, Point,
readPointsFromShape, readPointsFromShape,
clamp,
} from './shared'; } from './shared';
import Crosshair from './crosshair'; import Crosshair from './crosshair';
import consts from './consts'; import consts from './consts';
@ -56,6 +57,11 @@ function checkConstraint(shapeType: string, points: number[], box: Box | null =
return points.length > 2 || (points.length === 2 && points[0] !== 0 && points[1] !== 0); return points.length > 2 || (points.length === 2 && points[0] !== 0 && points[1] !== 0);
} }
if (shapeType === 'ellipse') {
const [rx, ry] = [points[2] - points[0], points[1] - points[3]];
return rx * ry * Math.PI >= consts.AREA_THRESHOLD;
}
if (shapeType === 'cuboid') { if (shapeType === 'cuboid') {
return points.length === 4 * 2 || points.length === 8 * 2 || return points.length === 4 * 2 || points.length === 8 * 2 ||
(points.length === 2 * 2 && (points[2] - points[0]) * (points[3] - points[1]) >= consts.AREA_THRESHOLD); (points.length === 2 * 2 && (points[2] - points[0]) * (points[3] - points[1]) >= consts.AREA_THRESHOLD);
@ -89,6 +95,19 @@ export class DrawHandlerImpl implements DrawHandler {
private pointsGroup: SVG.G | null; private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement; private shapeSizeElement: ShapeSizeElement;
private getFinalEllipseCoordinates(points: number[], fitIntoFrame: boolean): number[] {
const { offset } = this.geometry;
const [cx, cy, rightX, topY] = points.map((coord: number) => coord - offset);
const [rx, ry] = [rightX - cx, cy - topY];
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const [fitCX, fitCY] = fitIntoFrame ?
[clamp(cx, 0, frameWidth), clamp(cy, 0, frameHeight)] : [cx, cy];
const [fitRX, fitRY] = fitIntoFrame ?
[Math.min(rx, frameWidth - cx, cx), Math.min(ry, frameHeight - cy, cy)] : [rx, ry];
return [fitCX, fitCY, fitCX + fitRX, fitCY - fitRY];
}
private getFinalRectCoordinates(points: number[], fitIntoFrame: boolean): number[] { private getFinalRectCoordinates(points: number[], fitIntoFrame: boolean): number[] {
const frameWidth = this.geometry.image.width; const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height; const frameHeight = this.geometry.image.height;
@ -328,7 +347,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.initialized = false; this.initialized = false;
this.canvas.off('mousedown.draw'); this.canvas.off('mousedown.draw');
this.canvas.off('mousemove.draw'); this.canvas.off('mousemove.draw');
this.canvas.off('click.draw');
if (this.pointsGroup) { if (this.pointsGroup) {
this.pointsGroup.remove(); this.pointsGroup.remove();
@ -403,6 +421,58 @@ export class DrawHandlerImpl implements DrawHandler {
}); });
} }
private drawEllipse(): void {
this.drawInstance = (this.canvas as any).ellipse()
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
const initialPoint: {
x: number;
y: number;
} = {
x: null,
y: null,
};
this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
if (initialPoint.x === null || initialPoint.y === null) {
const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]);
[initialPoint.x, initialPoint.y] = translated;
} else {
const points = this.getFinalEllipseCoordinates(readPointsFromShape(this.drawInstance), false);
const { shapeType, redraw: clientID } = this.drawData;
this.release();
if (this.canceled) return;
if (checkConstraint('ellipse', points)) {
this.onDrawDone(
{
clientID,
shapeType,
points,
},
Date.now() - this.startTimestamp,
);
}
}
});
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
if (initialPoint.x !== null && initialPoint.y !== null) {
const translated = translateToSVG(this.canvas.node as any as SVGSVGElement, [e.clientX, e.clientY]);
const rx = Math.abs(translated[0] - initialPoint.x) / 2;
const ry = Math.abs(translated[1] - initialPoint.y) / 2;
const cx = initialPoint.x + rx * Math.sign(translated[0] - initialPoint.x);
const cy = initialPoint.y + ry * Math.sign(translated[1] - initialPoint.y);
this.drawInstance.center(cx, cy);
this.drawInstance.radius(rx, ry);
}
});
}
private drawBoxBy4Points(): void { private drawBoxBy4Points(): void {
let numberOfPoints = 0; let numberOfPoints = 0;
this.drawInstance = (this.canvas as any) this.drawInstance = (this.canvas as any)
@ -505,7 +575,7 @@ export class DrawHandlerImpl implements DrawHandler {
} }
}); });
// We need scale just drawn points // We need to scale points that have been just drawn
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => { this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry); this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX; lastDrawnPoint.x = e.detail.event.clientX;
@ -704,6 +774,45 @@ export class DrawHandlerImpl implements DrawHandler {
}); });
} }
private pasteEllipse([cx, cy, rx, ry]: number[], rotation: number): void {
this.drawInstance = (this.canvas as any)
.ellipse(rx * 2, ry * 2)
.center(cx, cy)
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
}).rotate(rotation);
this.pasteShape();
this.drawInstance.on('done', (e: CustomEvent): void => {
const points = this.getFinalEllipseCoordinates(
readPointsFromShape((e.target as any as { instance: SVG.Ellipse }).instance), false,
);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
if (checkConstraint('ellipse', points)) {
this.onDrawDone(
{
shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
rotation: this.drawData.initialState.rotation,
},
Date.now() - this.startTimestamp,
e.detail.originalEvent.ctrlKey,
);
}
});
}
private pastePolygon(points: string): void { private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any) this.drawInstance = (this.canvas as any)
.polygon(points) .polygon(points)
@ -819,6 +928,12 @@ export class DrawHandlerImpl implements DrawHandler {
width: xbr - xtl, width: xbr - xtl,
height: ybr - ytl, height: ybr - ytl,
}, this.drawData.initialState.rotation); }, this.drawData.initialState.rotation);
} else if (this.drawData.shapeType === 'ellipse') {
const [cx, cy, rightX, topY] = this.drawData.initialState.points.map(
(coord: number): number => coord + offset,
);
this.pasteEllipse([cx, cy, rightX - cx, cy - topY], this.drawData.initialState.rotation);
} else { } else {
const points = this.drawData.initialState.points.map((coord: number): number => coord + offset); const points = this.drawData.initialState.points.map((coord: number): number => coord + offset);
const stringifiedPoints = stringifyPoints(points); const stringifiedPoints = stringifyPoints(points);
@ -837,12 +952,10 @@ export class DrawHandlerImpl implements DrawHandler {
} else { } else {
if (this.drawData.shapeType === 'rectangle') { if (this.drawData.shapeType === 'rectangle') {
if (this.drawData.rectDrawingMethod === RectDrawingMethod.EXTREME_POINTS) { if (this.drawData.rectDrawingMethod === RectDrawingMethod.EXTREME_POINTS) {
// draw box by extreme clicking this.drawBoxBy4Points(); // draw box by extreme clicking
this.drawBoxBy4Points();
} else { } else {
// default box drawing this.drawBox(); // default box drawing
this.drawBox(); // draw instance was initialized after drawBox();
// Draw instance was initialized after drawBox();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text); this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
} }
} else if (this.drawData.shapeType === 'polygon') { } else if (this.drawData.shapeType === 'polygon') {
@ -851,6 +964,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyline(); this.drawPolyline();
} else if (this.drawData.shapeType === 'points') { } else if (this.drawData.shapeType === 'points') {
this.drawPoints(); this.drawPoints();
} else if (this.drawData.shapeType === 'ellipse') {
this.drawEllipse();
} else if (this.drawData.shapeType === 'cuboid') { } else if (this.drawData.shapeType === 'cuboid') {
if (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS) { if (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CORNER_POINTS) {
this.drawCuboidBy4Points(); this.drawCuboidBy4Points();
@ -859,7 +974,10 @@ export class DrawHandlerImpl implements DrawHandler {
this.shapeSizeElement = displayShapeSize(this.canvas, this.text); this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
} }
} }
this.setupDrawEvents();
if (this.drawData.shapeType !== 'ellipse') {
this.setupDrawEvents();
}
} }
this.startTimestamp = Date.now(); this.startTimestamp = Date.now();

@ -235,4 +235,8 @@ export function translateToCanvas(offset: number, points: number[]): number[] {
return points.map((coord: number): number => coord + offset); return points.map((coord: number): number => coord + offset);
} }
export function translateFromCanvas(offset: number, points: number[]): number[] {
return points.map((coord: number): number => coord - offset);
}
export type PropType<T, Prop extends keyof T> = T[Prop]; export type PropType<T, Prop extends keyof T> = T[Prop];

@ -1,12 +1,12 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "4.0.1", "version": "4.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-core", "name": "cvat-core",
"version": "4.0.1", "version": "4.1.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": "4.0.1", "version": "4.1.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": {

@ -8,11 +8,13 @@
PolygonShape, PolygonShape,
PolylineShape, PolylineShape,
PointsShape, PointsShape,
EllipseShape,
CuboidShape, CuboidShape,
RectangleTrack, RectangleTrack,
PolygonTrack, PolygonTrack,
PolylineTrack, PolylineTrack,
PointsTrack, PointsTrack,
EllipseTrack,
CuboidTrack, CuboidTrack,
Track, Track,
Shape, Shape,
@ -48,6 +50,9 @@
case 'points': case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection); shapeModel = new PointsShape(shapeData, clientID, color, injection);
break; break;
case 'ellipse':
shapeModel = new EllipseShape(shapeData, clientID, color, injection);
break;
case 'cuboid': case 'cuboid':
shapeModel = new CuboidShape(shapeData, clientID, color, injection); shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break; break;
@ -77,6 +82,9 @@
case 'points': case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection); trackModel = new PointsTrack(trackData, clientID, color, injection);
break; break;
case 'ellipse':
trackModel = new EllipseTrack(trackData, clientID, color, injection);
break;
case 'cuboid': case 'cuboid':
trackModel = new CuboidTrack(trackData, clientID, color, injection); trackModel = new CuboidTrack(trackData, clientID, color, injection);
break; break;
@ -615,6 +623,10 @@
shape: 0, shape: 0,
track: 0, track: 0,
}, },
ellipse: {
shape: 0,
track: 0,
},
cuboid: { cuboid: {
shape: 0, shape: 0,
track: 0, track: 0,

@ -47,13 +47,28 @@
} }
} else if (shapeType === ObjectShape.CUBOID) { } else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) { if (points.length / 2 !== 8) {
throw new DataError(`Points must have exact 8 points, but got ${points.length / 2}`); throw new DataError(`Cuboid must have 8 points, but got ${points.length / 2}`);
}
} else if (shapeType === ObjectShape.ELLIPSE) {
if (points.length / 2 !== 2) {
throw new DataError(`Ellipse must have 1 point, rx and ry but got ${points.toString()}`);
} }
} else { } else {
throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`); throw new ArgumentError(`Unknown value of shapeType has been received ${shapeType}`);
} }
} }
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;
}
function checkShapeArea(shapeType, points) { function checkShapeArea(shapeType, points) {
const MIN_SHAPE_LENGTH = 3; const MIN_SHAPE_LENGTH = 3;
const MIN_SHAPE_AREA = 9; const MIN_SHAPE_AREA = 9;
@ -62,6 +77,12 @@
return true; return true;
} }
if (shapeType === ObjectShape.ELLIPSE) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
return rx * ry * Math.PI > MIN_SHAPE_AREA;
}
let xmin = Number.MAX_SAFE_INTEGER; let xmin = Number.MAX_SAFE_INTEGER;
let xmax = Number.MIN_SAFE_INTEGER; let xmax = Number.MIN_SAFE_INTEGER;
let ymin = Number.MAX_SAFE_INTEGER; let ymin = Number.MAX_SAFE_INTEGER;
@ -76,7 +97,6 @@
if (shapeType === ObjectShape.POLYLINE) { if (shapeType === ObjectShape.POLYLINE) {
const length = Math.max(xmax - xmin, ymax - ymin); const length = Math.max(xmax - xmin, ymax - ymin);
return length >= MIN_SHAPE_LENGTH; return length >= MIN_SHAPE_LENGTH;
} }
@ -96,7 +116,7 @@
checkObjectType('rotation', rotation, 'number', null); checkObjectType('rotation', rotation, 'number', null);
points.forEach((coordinate) => checkObjectType('coordinate', coordinate, 'number', null)); points.forEach((coordinate) => checkObjectType('coordinate', coordinate, 'number', null));
if (shapeType === ObjectShape.CUBOID || !!rotation) { if (shapeType === ObjectShape.CUBOID || shapeType === ObjectShape.ELLIPSE || !!rotation) {
// cuboids and rotated bounding boxes cannot be fitted // cuboids and rotated bounding boxes cannot be fitted
return points; return points;
} }
@ -1393,6 +1413,62 @@
} }
} }
class EllipseShape extends Shape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.ELLIPSE;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static distance(points, x, y, angle) {
const [cx, cy, rightX, topY] = points;
const [rx, ry] = [rightX - cx, cy - topY];
const [rotX, rotY] = rotatePoint(x, y, -angle, cx, cy);
// https://math.stackexchange.com/questions/76457/check-if-a-point-is-within-an-ellipse
const pointWithinEllipse = (_x, _y) => (
((_x - cx) ** 2) / rx ** 2) + (((_y - cy) ** 2) / ry ** 2
) <= 1;
if (!pointWithinEllipse(rotX, rotY)) {
// Cursor is outside of an ellipse
return null;
}
if (Math.abs(x - cx) < Number.EPSILON && Math.abs(y - cy) < Number.EPSILON) {
// cursor is near to the center, just return minimum of height, width
return Math.min(rx, ry);
}
// ellipse equation is x^2/rx^2 + y^2/ry^2 = 1
// from this equation:
// x^2 = ((rx * ry)^2 - (y * rx)^2) / ry^2
// y^2 = ((rx * ry)^2 - (x * ry)^2) / rx^2
// we have one point inside the ellipse, let's build two lines (horizontal and vertical) through the point
// and find their interception with ellipse
const x2Equation = (_y) => (((rx * ry) ** 2) - ((_y * rx) ** 2)) / (ry ** 2);
const y2Equation = (_x) => (((rx * ry) ** 2) - ((_x * ry) ** 2)) / (rx ** 2);
// shift x,y to the ellipse coordinate system to compute equation correctly
// y axis is inverted
const [shiftedX, shiftedY] = [x - cx, cy - y];
const [x1, x2] = [Math.sqrt(x2Equation(shiftedY)), -Math.sqrt(x2Equation(shiftedY))];
const [y1, y2] = [Math.sqrt(y2Equation(shiftedX)), -Math.sqrt(y2Equation(shiftedX))];
// found two points on ellipse edge
const ellipseP1X = shiftedX >= 0 ? x1 : x2; // ellipseP1Y is shiftedY
const ellipseP2Y = shiftedY >= 0 ? y1 : y2; // ellipseP1X is shiftedX
// found diffs between two points on edges and target point
const diff1X = ellipseP1X - shiftedX;
const diff2Y = ellipseP2Y - shiftedY;
// return minimum, get absolute value because we need distance, not diff
return Math.min(Math.abs(diff1X), Math.abs(diff2Y));
}
}
class PolyShape extends Shape { class PolyShape extends Shape {
constructor(data, clientID, color, injection) { constructor(data, clientID, color, injection) {
super(data, clientID, color, injection); super(data, clientID, color, injection);
@ -1668,17 +1744,31 @@
} }
interpolatePosition(leftPosition, rightPosition, offset) { interpolatePosition(leftPosition, rightPosition, offset) {
function findAngleDiff(rightAngle, leftAngle) { const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
let angleDiff = rightAngle - leftAngle; return {
angleDiff = ((angleDiff + 180) % 360) - 180; points: leftPosition.points.map((point, index) => point + positionOffset[index] * offset),
if (Math.abs(angleDiff) >= 180) { rotation:
// if the main arc is bigger than 180, go another arc (leftPosition.rotation + findAngleDiff(
// to find it, just substract absolute value from 360 and inverse sign rightPosition.rotation, leftPosition.rotation,
angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1; ) * offset + 360) % 360,
} occluded: leftPosition.occluded,
return angleDiff; outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
};
}
}
class EllipseTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.ELLIPSE;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
} }
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point); const positionOffset = leftPosition.points.map((point, index) => rightPosition.points[index] - point);
return { return {
@ -2061,6 +2151,7 @@
PolygonTrack.distance = PolygonShape.distance; PolygonTrack.distance = PolygonShape.distance;
PolylineTrack.distance = PolylineShape.distance; PolylineTrack.distance = PolylineShape.distance;
PointsTrack.distance = PointsShape.distance; PointsTrack.distance = PointsShape.distance;
EllipseTrack.distance = EllipseShape.distance;
CuboidTrack.distance = CuboidShape.distance; CuboidTrack.distance = CuboidShape.distance;
module.exports = { module.exports = {
@ -2068,11 +2159,13 @@
PolygonShape, PolygonShape,
PolylineShape, PolylineShape,
PointsShape, PointsShape,
EllipseShape,
CuboidShape, CuboidShape,
RectangleTrack, RectangleTrack,
PolygonTrack, PolygonTrack,
PolylineTrack, PolylineTrack,
PointsTrack, PointsTrack,
EllipseTrack,
CuboidTrack, CuboidTrack,
Track, Track,
Shape, Shape,

@ -168,6 +168,7 @@
POLYGON: 'polygon', POLYGON: 'polygon',
POLYLINE: 'polyline', POLYLINE: 'polyline',
POINTS: 'points', POINTS: 'points',
ELLIPSE: 'ellipse',
CUBOID: 'cuboid', CUBOID: 'cuboid',
}); });

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -14,80 +14,88 @@
this, this,
Object.freeze({ Object.freeze({
/** /**
* Statistics by labels with a structure: * Statistics by labels with a structure:
* @example * @example
* { * {
* label: { * label: {
* boxes: { * boxes: {
* tracks: 10, * tracks: 10,
* shapes: 11, * shapes: 11,
* }, * },
* polygons: { * polygons: {
* tracks: 13, * tracks: 13,
* shapes: 14, * shapes: 14,
* }, * },
* polylines: { * polylines: {
* tracks: 16, * tracks: 16,
* shapes: 17, * shapes: 17,
* }, * },
* points: { * points: {
* tracks: 19, * tracks: 19,
* shapes: 20, * shapes: 20,
* }, * },
* cuboids: { * ellipse: {
* tracks: 21, * tracks: 13,
* shapes: 22, * shapes: 15,
* }, * },
* tags: 66, * cuboids: {
* manually: 186, * tracks: 21,
* interpolated: 500, * shapes: 22,
* total: 608, * },
* } * tags: 66,
* } * manually: 186,
* @name label * interpolated: 500,
* @type {Object} * total: 608,
* @memberof module:API.cvat.classes.Statistics * }
* @readonly * }
* @instance * @name label
*/ * @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
label: { label: {
get: () => JSON.parse(JSON.stringify(label)), get: () => JSON.parse(JSON.stringify(label)),
}, },
/** /**
* Total statistics (covers all labels) with a structure: * Total statistics (covers all labels) with a structure:
* @example * @example
* { * {
* boxes: { * boxes: {
* tracks: 10, * tracks: 10,
* shapes: 11, * shapes: 11,
* }, * },
* polygons: { * polygons: {
* tracks: 13, * tracks: 13,
* shapes: 14, * shapes: 14,
* }, * },
* polylines: { * polylines: {
* tracks: 16, * tracks: 16,
* shapes: 17, * shapes: 17,
* }, * },
* points: { * points: {
* tracks: 19, * tracks: 19,
* shapes: 20, * shapes: 20,
* }, * },
* cuboids: { * ellipse: {
* tracks: 21, * tracks: 13,
* shapes: 22, * shapes: 15,
* }, * },
* tags: 66, * cuboids: {
* manually: 186, * tracks: 21,
* interpolated: 500, * shapes: 22,
* total: 608, * },
* } * tags: 66,
* @name total * manually: 186,
* @type {Object} * interpolated: 500,
* @memberof module:API.cvat.classes.Statistics * total: 608,
* @readonly * }
* @instance * @name total
*/ * @type {Object}
* @memberof module:API.cvat.classes.Statistics
* @readonly
* @instance
*/
total: { total: {
get: () => JSON.parse(JSON.stringify(total)), get: () => JSON.parse(JSON.stringify(total)),
}, },

@ -30,8 +30,8 @@ describe('Feature: get annotations', () => {
const annotations10 = await job.annotations.get(10); const annotations10 = await job.annotations.get(10);
expect(Array.isArray(annotations0)).toBeTruthy(); expect(Array.isArray(annotations0)).toBeTruthy();
expect(Array.isArray(annotations10)).toBeTruthy(); expect(Array.isArray(annotations10)).toBeTruthy();
expect(annotations0).toHaveLength(1); expect(annotations0).toHaveLength(2);
expect(annotations10).toHaveLength(2); expect(annotations10).toHaveLength(3);
for (const state of annotations0.concat(annotations10)) { for (const state of annotations0.concat(annotations10)) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState); expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
} }
@ -57,7 +57,13 @@ describe('Feature: get annotations', () => {
expect(job.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError); expect(job.annotations.get(-1)).rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
// TODO: Test filter (hasn't been implemented yet) test('get only ellipses', async () => {
const job = (await window.cvat.jobs.get({ jobID: 101 }))[0];
const annotations = await job.annotations.get(5, false, JSON.parse('[{"and":[{"==":[{"var":"shape"},"ellipse"]}]}]'));
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(1);
expect(annotations[0].shapeType).toBe('ellipse');
});
}); });
describe('Feature: get interpolated annotations', () => { describe('Feature: get interpolated annotations', () => {
@ -65,7 +71,7 @@ describe('Feature: get interpolated annotations', () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(5); let annotations = await task.annotations.get(5);
expect(Array.isArray(annotations)).toBeTruthy(); expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(1); expect(annotations).toHaveLength(2);
const [xtl, ytl, xbr, ybr] = annotations[0].points; const [xtl, ytl, xbr, ybr] = annotations[0].points;
const { rotation } = annotations[0]; const { rotation } = annotations[0];
@ -78,7 +84,7 @@ describe('Feature: get interpolated annotations', () => {
annotations = await task.annotations.get(15); annotations = await task.annotations.get(15);
expect(Array.isArray(annotations)).toBeTruthy(); expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(2); // there is also a polygon on these frames (up to frame 22) expect(annotations).toHaveLength(3);
expect(annotations[1].rotation).toBe(40); expect(annotations[1].rotation).toBe(40);
expect(annotations[1].shapeType).toBe('rectangle'); expect(annotations[1].shapeType).toBe('rectangle');
@ -89,6 +95,19 @@ describe('Feature: get interpolated annotations', () => {
expect(annotations[0].rotation).toBe(0); expect(annotations[0].rotation).toBe(0);
expect(annotations[0].shapeType).toBe('rectangle'); expect(annotations[0].shapeType).toBe('rectangle');
}); });
test('get interpolated ellipse', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(5);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(2);
expect(annotations[1].shapeType).toBe('ellipse');
const [cx, cy, rightX, topY] = annotations[1].points;
expect(Math.round(cx)).toBe(550);
expect(Math.round(cy)).toBe(550);
expect(Math.round(rightX)).toBe(900);
expect(Math.round(topY)).toBe(150);
});
}); });
describe('Feature: put annotations', () => { describe('Feature: put annotations', () => {
@ -136,6 +155,28 @@ describe('Feature: put annotations', () => {
expect(annotations).toHaveLength(length + 1); expect(annotations).toHaveLength(length + 1);
}); });
test('put an ellipse shape to a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
let annotations = await job.annotations.get(5);
const { length } = annotations;
const state = new window.cvat.classes.ObjectState({
frame: 5,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.ELLIPSE,
points: [500, 500, 800, 100],
occluded: true,
label: job.labels[0],
zOrder: 0,
});
const indexes = await job.annotations.put([state]);
expect(indexes).toBeInstanceOf(Array);
expect(indexes).toHaveLength(1);
annotations = await job.annotations.get(5);
expect(annotations).toHaveLength(length + 1);
});
test('put a track to a task', async () => { test('put a track to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
let annotations = await task.annotations.get(1); let annotations = await task.annotations.get(1);
@ -593,7 +634,7 @@ describe('Feature: split annotations', () => {
await task.annotations.split(annotations5[0], 5); await task.annotations.split(annotations5[0], 5);
const splitted4 = await task.annotations.get(4); const splitted4 = await task.annotations.get(4);
const splitted5 = (await task.annotations.get(5)).filter((state) => !state.outside); const splitted5 = (await task.annotations.get(5)).filter((state) => !state.outside);
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID); expect(splitted4[1].clientID).not.toBe(splitted5[1].clientID);
}); });
test('split annotations in a job', async () => { test('split annotations in a job', async () => {
@ -605,7 +646,7 @@ describe('Feature: split annotations', () => {
await job.annotations.split(annotations5[0], 5); await job.annotations.split(annotations5[0], 5);
const splitted4 = await job.annotations.get(4); const splitted4 = await job.annotations.get(4);
const splitted5 = (await job.annotations.get(5)).filter((state) => !state.outside); const splitted5 = (await job.annotations.get(5)).filter((state) => !state.outside);
expect(splitted4[0].clientID).not.toBe(splitted5[0].clientID); expect(splitted4[1].clientID).not.toBe(splitted5[1].clientID);
}); });
test('split on a bad frame', async () => { test('split on a bad frame', async () => {
@ -733,7 +774,7 @@ describe('Feature: get statistics', () => {
await job.annotations.clear(true); await job.annotations.clear(true);
const statistics = await job.annotations.statistics(); const statistics = await job.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics); expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
expect(statistics.total.total).toBe(512); expect(statistics.total.total).toBe(1012);
}); });
}); });

@ -1830,6 +1830,38 @@ const taskAnnotationsDummyData = {
}, },
], ],
}, },
{
id: 61,
frame: 0,
label_id: 19,
group: 0,
shapes: [
{
type: 'ellipse',
occluded: false,
z_order: 1,
points: [500, 500, 800, 100],
rotation: 0,
id: 611,
frame: 0,
outside: false,
attributes: [],
},
{
type: 'ellipse',
occluded: false,
z_order: 1,
points: [600, 600, 1000, 200],
rotation: 0,
id: 612,
frame: 10,
outside: false,
attributes: [],
},
],
attributes: [],
},
], ],
}, },
100: { 100: {

@ -1,12 +1,12 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.32.3", "version": "1.33.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.32.3", "version": "1.33.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.32.3", "version": "1.33.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -1515,7 +1515,10 @@ export function repeatDrawShapeAsync(): ThunkAction {
activeControl = ActiveControl.DRAW_POLYLINE; activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.CUBOID) { } else if (activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID; activeControl = ActiveControl.DRAW_CUBOID;
} else if (activeShapeType === ShapeType.ELLIPSE) {
activeControl = ActiveControl.DRAW_ELLIPSE;
} }
dispatch({ dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE, type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: { payload: {
@ -1533,14 +1536,14 @@ export function repeatDrawShapeAsync(): ThunkAction {
frame: frameNumber, frame: frameNumber,
}); });
dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState])); dispatch(createAnnotationsAsync(jobInstance, frameNumber, [objectState]));
} else { } else if (canvasInstance) {
canvasInstance.draw({ canvasInstance.draw({
enabled: true, enabled: true,
rectDrawingMethod: activeRectDrawingMethod, rectDrawingMethod: activeRectDrawingMethod,
cuboidDrawingMethod: activeCuboidDrawingMethod, cuboidDrawingMethod: activeCuboidDrawingMethod,
numberOfPoints: activeNumOfPoints, numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType, shapeType: activeShapeType,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(activeShapeType), crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID, ShapeType.ELLIPSE].includes(activeShapeType),
}); });
} }
}; };
@ -1582,7 +1585,7 @@ export function redrawShapeAsync(): ThunkAction {
enabled: true, enabled: true,
redraw: activatedStateID, redraw: activatedStateID,
shapeType: state.shapeType, shapeType: state.shapeType,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(state.shapeType), crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID, ShapeType.ELLIPSE].includes(state.shapeType),
}); });
} }
} }

@ -0,0 +1,22 @@
<!-- Downloaded from: https://www.svgrepo.com/svg/253277/vector-ellipse, CC0 License -->
<svg width="40" height="40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M503.47,231.292h-19.628c-22.657-62.638-105.58-108.515-210.691-116.371V94.806c0-4.709-3.822-8.53-8.53-8.53h-51.182
c-4.709,0-8.53,3.822-8.53,8.53v21.138C87.138,128.714,0,193.264,0,269.678c0,86.046,111.014,156.046,247.465,156.046
c127.776,0,221.943-50.389,237.766-126.189h18.238c4.709,0,8.53-3.822,8.53-8.53v-51.182
C512,235.113,508.187,231.292,503.47,231.292z M221.968,103.337h34.121v34.121h-34.121V103.337z M247.465,408.663
c-127.051,0-230.405-62.348-230.405-138.985c0-67.236,79.742-124.355,187.847-136.571v12.881c0,4.709,3.822,8.53,8.53,8.53h51.182
c4.709,0,8.53-3.822,8.53-8.53v-13.964c94.508,7.328,169.336,46.072,192.556,99.268h-13.41c-4.709,0-8.53,3.822-8.53,8.53v51.182
c0,4.709,3.822,8.53,8.53,8.53h15.235C451.025,364.246,362.821,408.663,247.465,408.663z M494.939,282.474h-34.121v-34.121h34.121
V282.474z"/>
</g>
</g>
<g>
<g>
<path d="M256.09,256.883h-8.53v-8.53c0-4.709-3.813-8.53-8.53-8.53c-4.709,0-8.53,3.822-8.53,8.53v8.53h-8.53
c-4.709,0-8.53,3.822-8.53,8.53c0,4.709,3.822,8.53,8.53,8.53h8.53v8.53c0,4.709,3.822,8.53,8.53,8.53s8.53-3.822,8.53-8.53v-8.53
h8.53c4.709,0,8.53-3.822,8.53-8.53C264.62,260.704,260.798,256.883,256.09,256.883z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -21,6 +21,7 @@ import DrawRectangleControl, { Props as DrawRectangleControlProps } from './draw
import DrawPolygonControl, { Props as DrawPolygonControlProps } from './draw-polygon-control'; import DrawPolygonControl, { Props as DrawPolygonControlProps } from './draw-polygon-control';
import DrawPolylineControl, { Props as DrawPolylineControlProps } from './draw-polyline-control'; import DrawPolylineControl, { Props as DrawPolylineControlProps } from './draw-polyline-control';
import DrawPointsControl, { Props as DrawPointsControlProps } from './draw-points-control'; import DrawPointsControl, { Props as DrawPointsControlProps } from './draw-points-control';
import DrawEllipseControl, { Props as DrawEllipseControlProps } from './draw-ellipse-control';
import DrawCuboidControl, { Props as DrawCuboidControlProps } from './draw-cuboid-control'; import DrawCuboidControl, { Props as DrawCuboidControlProps } from './draw-cuboid-control';
import SetupTagControl, { Props as SetupTagControlProps } from './setup-tag-control'; import SetupTagControl, { Props as SetupTagControlProps } from './setup-tag-control';
import MergeControl, { Props as MergeControlProps } from './merge-control'; import MergeControl, { Props as MergeControlProps } from './merge-control';
@ -57,6 +58,7 @@ const ObservedDrawRectangleControl = ControlVisibilityObserver<DrawRectangleCont
const ObservedDrawPolygonControl = ControlVisibilityObserver<DrawPolygonControlProps>(DrawPolygonControl); const ObservedDrawPolygonControl = ControlVisibilityObserver<DrawPolygonControlProps>(DrawPolygonControl);
const ObservedDrawPolylineControl = ControlVisibilityObserver<DrawPolylineControlProps>(DrawPolylineControl); const ObservedDrawPolylineControl = ControlVisibilityObserver<DrawPolylineControlProps>(DrawPolylineControl);
const ObservedDrawPointsControl = ControlVisibilityObserver<DrawPointsControlProps>(DrawPointsControl); const ObservedDrawPointsControl = ControlVisibilityObserver<DrawPointsControlProps>(DrawPointsControl);
const ObservedDrawEllipseControl = ControlVisibilityObserver<DrawEllipseControlProps>(DrawEllipseControl);
const ObservedDrawCuboidControl = ControlVisibilityObserver<DrawCuboidControlProps>(DrawCuboidControl); const ObservedDrawCuboidControl = ControlVisibilityObserver<DrawCuboidControlProps>(DrawCuboidControl);
const ObservedSetupTagControl = ControlVisibilityObserver<SetupTagControlProps>(SetupTagControl); const ObservedSetupTagControl = ControlVisibilityObserver<SetupTagControlProps>(SetupTagControl);
const ObservedMergeControl = ControlVisibilityObserver<MergeControlProps>(MergeControl); const ObservedMergeControl = ControlVisibilityObserver<MergeControlProps>(MergeControl);
@ -241,6 +243,11 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
isDrawing={activeControl === ActiveControl.DRAW_POINTS} isDrawing={activeControl === ActiveControl.DRAW_POINTS}
disabled={!labels.length} disabled={!labels.length}
/> />
<ObservedDrawEllipseControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_ELLIPSE}
disabled={!labels.length}
/>
<ObservedDrawCuboidControl <ObservedDrawCuboidControl
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_CUBOID} isDrawing={activeControl === ActiveControl.DRAW_CUBOID}

@ -24,30 +24,26 @@ export interface Props {
const CustomPopover = withVisibilityHandling(Popover, 'draw-cuboid'); const CustomPopover = withVisibilityHandling(Popover, 'draw-cuboid');
function DrawPolygonControl(props: Props): JSX.Element { function DrawPolygonControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamicPopoverProps = isDrawing ? {
{ overlayStyle: {
overlayStyle: { display: 'none',
display: 'none', },
}, } : {};
} :
{}; const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-cuboid-control cvat-active-canvas-control',
const dynamicIconProps = isDrawing ? onClick: (): void => {
{ canvasInstance.draw({ enabled: false });
className: 'cvat-draw-cuboid-control cvat-active-canvas-control', },
onClick: (): void => { } : {
canvasInstance.draw({ enabled: false }); className: 'cvat-draw-cuboid-control',
}, };
} :
{
className: 'cvat-draw-cuboid-control',
};
return disabled ? ( return disabled ? (
<Icon className='cvat-draw-cuboid-control cvat-disabled-canvas-control' component={CubeIcon} /> <Icon className='cvat-draw-cuboid-control cvat-disabled-canvas-control' component={CubeIcon} />
) : ( ) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'
placement='right' placement='right'
content={<DrawShapePopoverContainer shapeType={ShapeType.CUBOID} />} content={<DrawShapePopoverContainer shapeType={ShapeType.CUBOID} />}

@ -0,0 +1,54 @@
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Popover from 'antd/lib/popover';
import Icon from '@ant-design/icons';
import { Canvas } from 'cvat-canvas-wrapper';
import { EllipseIcon } from 'icons';
import { ShapeType } from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
import withVisibilityHandling from './handle-popover-visibility';
export interface Props {
canvasInstance: Canvas;
isDrawing: boolean;
disabled?: boolean;
}
const CustomPopover = withVisibilityHandling(Popover, 'draw-ellipse');
function DrawPointsControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props;
const dynamicPopoverProps = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-ellipse-control cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {
className: 'cvat-draw-ellipse-control',
};
return disabled ? (
<Icon className='cvat-draw-ellipse-control cvat-disabled-canvas-control' component={EllipseIcon} />
) : (
<CustomPopover
{...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={<DrawShapePopoverContainer shapeType={ShapeType.ELLIPSE} />}
>
<Icon {...dynamicIconProps} component={EllipseIcon} />
</CustomPopover>
);
}
export default React.memo(DrawPointsControl);

@ -22,30 +22,26 @@ export interface Props {
const CustomPopover = withVisibilityHandling(Popover, 'draw-points'); const CustomPopover = withVisibilityHandling(Popover, 'draw-points');
function DrawPointsControl(props: Props): JSX.Element { function DrawPointsControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamicPopoverProps = isDrawing ? {
{ overlayStyle: {
overlayStyle: { display: 'none',
display: 'none', },
}, } : {};
} :
{}; const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-points-control cvat-active-canvas-control',
const dynamicIconProps = isDrawing ? onClick: (): void => {
{ canvasInstance.draw({ enabled: false });
className: 'cvat-draw-points-control cvat-active-canvas-control', },
onClick: (): void => { } : {
canvasInstance.draw({ enabled: false }); className: 'cvat-draw-points-control',
}, };
} :
{
className: 'cvat-draw-points-control',
};
return disabled ? ( return disabled ? (
<Icon className='cvat-draw-points-control cvat-disabled-canvas-control' component={PointIcon} /> <Icon className='cvat-draw-points-control cvat-disabled-canvas-control' component={PointIcon} />
) : ( ) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'
placement='right' placement='right'
content={<DrawShapePopoverContainer shapeType={ShapeType.POINTS} />} content={<DrawShapePopoverContainer shapeType={ShapeType.POINTS} />}

@ -22,30 +22,26 @@ export interface Props {
const CustomPopover = withVisibilityHandling(Popover, 'draw-polygon'); const CustomPopover = withVisibilityHandling(Popover, 'draw-polygon');
function DrawPolygonControl(props: Props): JSX.Element { function DrawPolygonControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamicPopoverProps = isDrawing ? {
{ overlayStyle: {
overlayStyle: { display: 'none',
display: 'none', },
}, } : {};
} :
{}; const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-polygon-control cvat-active-canvas-control',
const dynamicIconProps = isDrawing ? onClick: (): void => {
{ canvasInstance.draw({ enabled: false });
className: 'cvat-draw-polygon-control cvat-active-canvas-control', },
onClick: (): void => { } : {
canvasInstance.draw({ enabled: false }); className: 'cvat-draw-polygon-control',
}, };
} :
{
className: 'cvat-draw-polygon-control',
};
return disabled ? ( return disabled ? (
<Icon className='cvat-draw-polygon-control cvat-disabled-canvas-control' component={PolygonIcon} /> <Icon className='cvat-draw-polygon-control cvat-disabled-canvas-control' component={PolygonIcon} />
) : ( ) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'
placement='right' placement='right'
content={<DrawShapePopoverContainer shapeType={ShapeType.POLYGON} />} content={<DrawShapePopoverContainer shapeType={ShapeType.POLYGON} />}

@ -22,30 +22,26 @@ export interface Props {
const CustomPopover = withVisibilityHandling(Popover, 'draw-polyline'); const CustomPopover = withVisibilityHandling(Popover, 'draw-polyline');
function DrawPolylineControl(props: Props): JSX.Element { function DrawPolylineControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamicPopoverProps = isDrawing ? {
{ overlayStyle: {
overlayStyle: { display: 'none',
display: 'none', },
}, } : {};
} :
{}; const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-polyline-control cvat-active-canvas-control',
const dynamicIconProps = isDrawing ? onClick: (): void => {
{ canvasInstance.draw({ enabled: false });
className: 'cvat-draw-polyline-control cvat-active-canvas-control', },
onClick: (): void => { } : {
canvasInstance.draw({ enabled: false }); className: 'cvat-draw-polyline-control',
}, };
} :
{
className: 'cvat-draw-polyline-control',
};
return disabled ? ( return disabled ? (
<Icon className='cvat-draw-polyline-control cvat-disabled-canvas-control' component={PolylineIcon} /> <Icon className='cvat-draw-polyline-control cvat-disabled-canvas-control' component={PolylineIcon} />
) : ( ) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'
placement='right' placement='right'
content={<DrawShapePopoverContainer shapeType={ShapeType.POLYLINE} />} content={<DrawShapePopoverContainer shapeType={ShapeType.POLYLINE} />}

@ -22,30 +22,26 @@ export interface Props {
const CustomPopover = withVisibilityHandling(Popover, 'draw-rectangle'); const CustomPopover = withVisibilityHandling(Popover, 'draw-rectangle');
function DrawRectangleControl(props: Props): JSX.Element { function DrawRectangleControl(props: Props): JSX.Element {
const { canvasInstance, isDrawing, disabled } = props; const { canvasInstance, isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamicPopoverProps = isDrawing ? {
{ overlayStyle: {
overlayStyle: { display: 'none',
display: 'none', },
}, } : {};
} :
{}; const dynamicIconProps = isDrawing ? {
className: 'cvat-draw-rectangle-control cvat-active-canvas-control',
const dynamicIconProps = isDrawing ? onClick: (): void => {
{ canvasInstance.draw({ enabled: false });
className: 'cvat-draw-rectangle-control cvat-active-canvas-control', },
onClick: (): void => { } : {
canvasInstance.draw({ enabled: false }); className: 'cvat-draw-rectangle-control',
}, };
} :
{
className: 'cvat-draw-rectangle-control',
};
return disabled ? ( return disabled ? (
<Icon className='cvat-draw-rectangle-control cvat-disabled-canvas-control' component={RectangleIcon} /> <Icon className='cvat-draw-rectangle-control cvat-disabled-canvas-control' component={RectangleIcon} />
) : ( ) : (
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamicPopoverProps}
overlayClassName='cvat-draw-shape-popover' overlayClassName='cvat-draw-shape-popover'
placement='right' placement='right'
content={<DrawShapePopoverContainer shapeType={ShapeType.RECTANGLE} />} content={<DrawShapePopoverContainer shapeType={ShapeType.RECTANGLE} />}

@ -126,7 +126,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
</Row> </Row>
</> </>
)} )}
{is2D && shapeType !== ShapeType.RECTANGLE && shapeType !== ShapeType.CUBOID && ( {is2D && ![ShapeType.RECTANGLE, ShapeType.CUBOID, ShapeType.ELLIPSE].includes(shapeType) ? (
<Row justify='space-around' align='middle'> <Row justify='space-around' align='middle'>
<Col span={14}> <Col span={14}>
<Text className='cvat-text-color'> Number of points: </Text> <Text className='cvat-text-color'> Number of points: </Text>
@ -147,7 +147,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
/> />
</Col> </Col>
</Row> </Row>
)} ) : null}
<Row justify='space-around'> <Row justify='space-around'>
<Col span={12}> <Col span={12}>
<CVATTooltip title={`Press ${repeatShapeShortcut} to draw again`}> <CVATTooltip title={`Press ${repeatShapeShortcut} to draw again`}>

@ -298,8 +298,9 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
throw new Error('Canvas context is empty'); throw new Error('Canvas context is empty');
} }
const imageData = context.getImageData(0, 0, width, height); const imageData = context.getImageData(0, 0, width, height);
const newImageData = activeImageModifiers.reduce((oldImageData, activeImageModifier) => const newImageData = activeImageModifiers.reduce((oldImageData, activeImageModifier) => (
activeImageModifier.modifier.processImage(oldImageData, frame), imageData); activeImageModifier.modifier.processImage(oldImageData, frame)
), imageData);
const imageBitmap = await createImageBitmap(newImageData); const imageBitmap = await createImageBitmap(newImageData);
frameData.imageData = imageBitmap; frameData.imageData = imageBitmap;
canvasInstance.setup(frameData, states, curZOrder); canvasInstance.setup(frameData, states, curZOrder);
@ -346,7 +347,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
return points; return points;
} }
private imageModifier(alias: string): ImageProcessing|null { private imageModifier(alias: string): ImageProcessing | null {
const { activeImageModifiers } = this.state; const { activeImageModifiers } = this.state;
return activeImageModifiers.find((imageModifier) => imageModifier.alias === alias)?.modifier || null; return activeImageModifiers.find((imageModifier) => imageModifier.alias === alias)?.modifier || null;
} }
@ -362,7 +363,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
} }
} }
private enableImageModifier(modifier: ImageProcessing, alias: string): void{ private enableImageModifier(modifier: ImageProcessing, alias: string): void {
this.setState((prev: State) => ({ this.setState((prev: State) => ({
...prev, ...prev,
activeImageModifiers: [...prev.activeImageModifiers, { modifier, alias }], activeImageModifiers: [...prev.activeImageModifiers, { modifier, alias }],
@ -371,13 +372,13 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
}); });
} }
private enableCanvasForceUpdate():void{ private enableCanvasForceUpdate():void {
const { canvasInstance } = this.props; const { canvasInstance } = this.props;
canvasInstance.configure({ forceFrameUpdate: true }); canvasInstance.configure({ forceFrameUpdate: true });
this.canvasForceUpdateWasEnabled = true; this.canvasForceUpdateWasEnabled = true;
} }
private disableCanvasForceUpdate():void{ private disableCanvasForceUpdate():void {
if (this.canvasForceUpdateWasEnabled) { if (this.canvasForceUpdateWasEnabled) {
const { canvasInstance } = this.props; const { canvasInstance } = this.props;
canvasInstance.configure({ forceFrameUpdate: false }); canvasInstance.configure({ forceFrameUpdate: false });
@ -528,7 +529,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public render(): JSX.Element { public render(): JSX.Element {
const { isActivated, canvasInstance, labels } = this.props; const { isActivated, canvasInstance, labels } = this.props;
const { libraryInitialized, approxPolyAccuracy } = this.state; const { libraryInitialized, approxPolyAccuracy } = this.state;
const dynamcPopoverPros = isActivated ? const dynamicPopoverProps = isActivated ?
{ {
overlayStyle: { overlayStyle: {
display: 'none', display: 'none',
@ -552,7 +553,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
) : ( ) : (
<> <>
<CustomPopover <CustomPopover
{...dynamcPopoverPros} {...dynamicPopoverProps}
placement='right' placement='right'
overlayClassName='cvat-opencv-control-popover' overlayClassName='cvat-opencv-control-popover'
content={this.renderContent()} content={this.renderContent()}

@ -21,7 +21,7 @@ export interface Props {
const CustomPopover = withVisibilityHandling(Popover, 'setup-tag'); const CustomPopover = withVisibilityHandling(Popover, 'setup-tag');
function SetupTagControl(props: Props): JSX.Element { function SetupTagControl(props: Props): JSX.Element {
const { isDrawing, disabled } = props; const { isDrawing, disabled } = props;
const dynamcPopoverPros = isDrawing ? const dynamicPopoverProps = isDrawing ?
{ {
overlayStyle: { overlayStyle: {
display: 'none', display: 'none',
@ -32,7 +32,7 @@ function SetupTagControl(props: Props): JSX.Element {
return disabled ? ( return disabled ? (
<Icon className='cvat-setup-tag-control cvat-disabled-canvas-control' component={TagIcon} /> <Icon className='cvat-setup-tag-control cvat-disabled-canvas-control' component={TagIcon} />
) : ( ) : (
<CustomPopover {...dynamcPopoverPros} placement='right' content={<SetupTagPopoverContainer />}> <CustomPopover {...dynamicPopoverProps} placement='right' content={<SetupTagPopoverContainer />}>
<Icon className='cvat-setup-tag-control' component={TagIcon} /> <Icon className='cvat-setup-tag-control' component={TagIcon} />
</CustomPopover> </CustomPopover>
); );

@ -1092,7 +1092,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
if (![...interactors, ...detectors, ...trackers].length) return null; if (![...interactors, ...detectors, ...trackers].length) return null;
const dynamcPopoverPros = isActivated ? const dynamicPopoverProps = isActivated ?
{ {
overlayStyle: { overlayStyle: {
display: 'none', display: 'none',
@ -1142,7 +1142,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
return showAnyContent ? ( return showAnyContent ? (
<> <>
<CustomPopover {...dynamcPopoverPros} placement='right' content={this.renderPopoverContent()}> <CustomPopover {...dynamicPopoverProps} placement='right' content={this.renderPopoverContent()}>
<Icon {...dynamicIconProps} component={AIToolsIcon} /> <Icon {...dynamicIconProps} component={AIToolsIcon} />
</CustomPopover> </CustomPopover>
{interactionContent} {interactionContent}

@ -114,6 +114,7 @@
.cvat-draw-polygon-control, .cvat-draw-polygon-control,
.cvat-draw-polyline-control, .cvat-draw-polyline-control,
.cvat-draw-points-control, .cvat-draw-points-control,
.cvat-draw-ellipse-control,
.cvat-draw-cuboid-control, .cvat-draw-cuboid-control,
.cvat-setup-tag-control, .cvat-setup-tag-control,
.cvat-merge-control, .cvat-merge-control,

@ -109,7 +109,8 @@ function FiltersModalComponent(): JSX.Element {
{ value: 'points', title: 'Points' }, { value: 'points', title: 'Points' },
{ value: 'polyline', title: 'Polyline' }, { value: 'polyline', title: 'Polyline' },
{ value: 'polygon', title: 'Polygon' }, { value: 'polygon', title: 'Polygon' },
{ value: 'cuboids', title: 'Cuboids' }, { value: 'cuboid', title: 'Cuboid' },
{ value: 'ellipse', title: 'Ellipse' },
], ],
}, },
}, },

@ -111,6 +111,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
polygon: `${data.label[key].polygon.shape} / ${data.label[key].polygon.track}`, polygon: `${data.label[key].polygon.shape} / ${data.label[key].polygon.track}`,
polyline: `${data.label[key].polyline.shape} / ${data.label[key].polyline.track}`, polyline: `${data.label[key].polyline.shape} / ${data.label[key].polyline.track}`,
points: `${data.label[key].points.shape} / ${data.label[key].points.track}`, points: `${data.label[key].points.shape} / ${data.label[key].points.track}`,
ellipse: `${data.label[key].ellipse.shape} / ${data.label[key].ellipse.track}`,
cuboid: `${data.label[key].cuboid.shape} / ${data.label[key].cuboid.track}`, cuboid: `${data.label[key].cuboid.shape} / ${data.label[key].cuboid.track}`,
tags: data.label[key].tags, tags: data.label[key].tags,
manually: data.label[key].manually, manually: data.label[key].manually,
@ -125,6 +126,7 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
polygon: `${data.total.polygon.shape} / ${data.total.polygon.track}`, polygon: `${data.total.polygon.shape} / ${data.total.polygon.track}`,
polyline: `${data.total.polyline.shape} / ${data.total.polyline.track}`, polyline: `${data.total.polyline.shape} / ${data.total.polyline.track}`,
points: `${data.total.points.shape} / ${data.total.points.track}`, points: `${data.total.points.shape} / ${data.total.points.track}`,
ellipse: `${data.total.ellipse.shape} / ${data.total.ellipse.track}`,
cuboid: `${data.total.cuboid.shape} / ${data.total.cuboid.track}`, cuboid: `${data.total.cuboid.shape} / ${data.total.cuboid.track}`,
tags: data.total.tags, tags: data.total.tags,
manually: data.total.manually, manually: data.total.manually,
@ -167,6 +169,11 @@ function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.El
dataIndex: 'points', dataIndex: 'points',
key: 'points', key: 'points',
}, },
{
title: makeShapesTracksTitle('Ellipse'),
dataIndex: 'ellipse',
key: 'ellipse',
},
{ {
title: makeShapesTracksTitle('Cuboids'), title: makeShapesTracksTitle('Cuboids'),
dataIndex: 'cuboid', dataIndex: 'cuboid',

@ -53,7 +53,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
return { return {
rotateAll, rotateAll,
canvasInstance, canvasInstance: canvasInstance as Canvas,
activeControl, activeControl,
labels, labels,
normalizedKeyMap, normalizedKeyMap,

@ -9,7 +9,6 @@ import { RadioChangeEvent } from 'antd/lib/radio';
import { CombinedState, ShapeType, ObjectType } from 'reducers/interfaces'; import { CombinedState, ShapeType, ObjectType } from 'reducers/interfaces';
import { rememberObject } from 'actions/annotation-actions'; import { rememberObject } from 'actions/annotation-actions';
import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface OwnProps { interface OwnProps {
@ -29,7 +28,7 @@ interface DispatchToProps {
interface StateToProps { interface StateToProps {
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas | Canvas3d; canvasInstance: Canvas;
shapeType: ShapeType; shapeType: ShapeType;
labels: any[]; labels: any[];
jobInstance: any; jobInstance: any;
@ -70,7 +69,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
return { return {
...own, ...own,
canvasInstance, canvasInstance: canvasInstance as Canvas,
labels, labels,
normalizedKeyMap, normalizedKeyMap,
jobInstance, jobInstance,
@ -104,11 +103,9 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
if (shapeType === ShapeType.POLYGON) { if (shapeType === ShapeType.POLYGON) {
this.minimumPoints = 3; this.minimumPoints = 3;
} } else if (shapeType === ShapeType.POLYLINE) {
if (shapeType === ShapeType.POLYLINE) {
this.minimumPoints = 2; this.minimumPoints = 2;
} } else if (shapeType === ShapeType.POINTS) {
if (shapeType === ShapeType.POINTS) {
this.minimumPoints = 1; this.minimumPoints = 1;
} }
} }
@ -127,7 +124,7 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
cuboidDrawingMethod, cuboidDrawingMethod,
numberOfPoints, numberOfPoints,
shapeType, shapeType,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(shapeType), crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID, ShapeType.ELLIPSE].includes(shapeType),
}); });
onDrawStart(shapeType, selectedLabelID, objectType, numberOfPoints, rectDrawingMethod, cuboidDrawingMethod); onDrawStart(shapeType, selectedLabelID, objectType, numberOfPoints, rectDrawingMethod, cuboidDrawingMethod);

@ -15,6 +15,7 @@ import SVGZoomIcon from './assets/zoom-icon.svg';
import SVGRectangleIcon from './assets/rectangle-icon.svg'; import SVGRectangleIcon from './assets/rectangle-icon.svg';
import SVGPolygonIcon from './assets/polygon-icon.svg'; import SVGPolygonIcon from './assets/polygon-icon.svg';
import SVGPointIcon from './assets/point-icon.svg'; import SVGPointIcon from './assets/point-icon.svg';
import SVGEllipseIcon from './assets/ellipse-icon.svg';
import SVGPolylineIcon from './assets/polyline-icon.svg'; import SVGPolylineIcon from './assets/polyline-icon.svg';
import SVGTagIcon from './assets/tag-icon.svg'; import SVGTagIcon from './assets/tag-icon.svg';
import SVGMergeIcon from './assets/merge-icon.svg'; import SVGMergeIcon from './assets/merge-icon.svg';
@ -65,6 +66,7 @@ export const ZoomIcon = React.memo((): JSX.Element => <SVGZoomIcon />);
export const RectangleIcon = React.memo((): JSX.Element => <SVGRectangleIcon />); export const RectangleIcon = React.memo((): JSX.Element => <SVGRectangleIcon />);
export const PolygonIcon = React.memo((): JSX.Element => <SVGPolygonIcon />); export const PolygonIcon = React.memo((): JSX.Element => <SVGPolygonIcon />);
export const PointIcon = React.memo((): JSX.Element => <SVGPointIcon />); export const PointIcon = React.memo((): JSX.Element => <SVGPointIcon />);
export const EllipseIcon = React.memo((): JSX.Element => <SVGEllipseIcon />);
export const PolylineIcon = React.memo((): JSX.Element => <SVGPolylineIcon />); export const PolylineIcon = React.memo((): JSX.Element => <SVGPolylineIcon />);
export const TagIcon = React.memo((): JSX.Element => <SVGTagIcon />); export const TagIcon = React.memo((): JSX.Element => <SVGTagIcon />);
export const MergeIcon = React.memo((): JSX.Element => <SVGMergeIcon />); export const MergeIcon = React.memo((): JSX.Element => <SVGMergeIcon />);

@ -478,6 +478,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
activeControl = ActiveControl.DRAW_POLYLINE; activeControl = ActiveControl.DRAW_POLYLINE;
} else if (payload.activeShapeType === ShapeType.POINTS) { } else if (payload.activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS; activeControl = ActiveControl.DRAW_POINTS;
} else if (payload.activeShapeType === ShapeType.ELLIPSE) {
activeControl = ActiveControl.DRAW_ELLIPSE;
} else if (payload.activeShapeType === ShapeType.CUBOID) { } else if (payload.activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID; activeControl = ActiveControl.DRAW_CUBOID;
} else if (payload.activeObjectType === ObjectType.TAG) { } else if (payload.activeObjectType === ObjectType.TAG) {

@ -473,6 +473,7 @@ export enum ActiveControl {
DRAW_POLYGON = 'draw_polygon', DRAW_POLYGON = 'draw_polygon',
DRAW_POLYLINE = 'draw_polyline', DRAW_POLYLINE = 'draw_polyline',
DRAW_POINTS = 'draw_points', DRAW_POINTS = 'draw_points',
DRAW_ELLIPSE = 'draw_ellipse',
DRAW_CUBOID = 'draw_cuboid', DRAW_CUBOID = 'draw_cuboid',
MERGE = 'merge', MERGE = 'merge',
GROUP = 'group', GROUP = 'group',
@ -489,6 +490,7 @@ export enum ShapeType {
POLYGON = 'polygon', POLYGON = 'polygon',
POLYLINE = 'polyline', POLYLINE = 'polyline',
POINTS = 'points', POINTS = 'points',
ELLIPSE = 'ellipse',
CUBOID = 'cuboid', CUBOID = 'cuboid',
} }

@ -696,6 +696,7 @@ class TrackManager(ObjectManager):
def interpolate(shape0, shape1): def interpolate(shape0, shape1):
is_same_type = shape0["type"] == shape1["type"] is_same_type = shape0["type"] == shape1["type"]
is_rectangle = shape0["type"] == ShapeType.RECTANGLE is_rectangle = shape0["type"] == ShapeType.RECTANGLE
is_ellipse = shape0["type"] == ShapeType.ELLIPSE
is_cuboid = shape0["type"] == ShapeType.CUBOID is_cuboid = shape0["type"] == ShapeType.CUBOID
is_polygon = shape0["type"] == ShapeType.POLYGON is_polygon = shape0["type"] == ShapeType.POLYGON
is_polyline = shape0["type"] == ShapeType.POLYLINE is_polyline = shape0["type"] == ShapeType.POLYLINE
@ -705,7 +706,7 @@ class TrackManager(ObjectManager):
raise NotImplementedError() raise NotImplementedError()
shapes = [] shapes = []
if is_rectangle or is_cuboid: if is_rectangle or is_cuboid or is_ellipse:
shapes = simple_interpolation(shape0, shape1) shapes = simple_interpolation(shape0, shape1)
elif is_points: elif is_points:
shapes = points_interpolation(shape0, shape1) shapes = points_interpolation(shape0, shape1)

@ -8,6 +8,7 @@ import rq
import os.path as osp import os.path as osp
from attr import attrib, attrs from attr import attrib, attrs
from collections import namedtuple from collections import namedtuple
from types import SimpleNamespace
from pathlib import Path from pathlib import Path
from typing import (Any, Callable, DefaultDict, Dict, List, Literal, Mapping, from typing import (Any, Callable, DefaultDict, Dict, List, Literal, Mapping,
NamedTuple, OrderedDict, Tuple, Union, Set) NamedTuple, OrderedDict, Tuple, Union, Set)
@ -26,7 +27,7 @@ from cvat.apps.engine.models import Label, Project, ShapeType, Task
from cvat.apps.dataset_manager.formats.utils import get_label_color from cvat.apps.dataset_manager.formats.utils import get_label_color
from .annotation import AnnotationIR, AnnotationManager, TrackManager from .annotation import AnnotationIR, AnnotationManager, TrackManager
from .formats.transformations import EllipsesToMasks
CVAT_INTERNAL_ATTRIBUTES = {'occluded', 'outside', 'keyframe', 'track_id', 'rotation'} CVAT_INTERNAL_ATTRIBUTES = {'occluded', 'outside', 'keyframe', 'track_id', 'rotation'}
@ -1325,6 +1326,18 @@ def convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label, format_name
anno = datum_annotation.Points(anno_points, anno = datum_annotation.Points(anno_points,
label=anno_label, attributes=anno_attr, group=anno_group, label=anno_label, attributes=anno_attr, group=anno_group,
z_order=shape_obj.z_order) z_order=shape_obj.z_order)
elif shape_obj.type == ShapeType.ELLIPSE:
# TODO: for now Datumaro does not support ellipses
# so, we convert an ellipse to RLE mask here
# instead of applying transformation in directly in formats
anno = EllipsesToMasks.convert_ellipse(SimpleNamespace(**{
"points": shape_obj.points,
"label": anno_label,
"z_order": shape_obj.z_order,
"rotation": shape_obj.rotation,
"group": anno_group,
"attributes": anno_attr,
}), cvat_frame_anno.height, cvat_frame_anno.width)
elif shape_obj.type == ShapeType.POLYLINE: elif shape_obj.type == ShapeType.POLYLINE:
anno = datum_annotation.PolyLine(anno_points, anno = datum_annotation.PolyLine(anno_points,
label=anno_label, attributes=anno_attr, group=anno_group, label=anno_label, attributes=anno_attr, group=anno_group,

@ -496,6 +496,11 @@ def create_xml_dumper(file_object):
self.xmlgen.startElement("box", box) self.xmlgen.startElement("box", box)
self._level += 1 self._level += 1
def open_ellipse(self, ellipse):
self._indent()
self.xmlgen.startElement("ellipse", ellipse)
self._level += 1
def open_polygon(self, polygon): def open_polygon(self, polygon):
self._indent() self._indent()
self.xmlgen.startElement("polygon", polygon) self.xmlgen.startElement("polygon", polygon)
@ -532,6 +537,11 @@ def create_xml_dumper(file_object):
self._indent() self._indent()
self.xmlgen.endElement("box") self.xmlgen.endElement("box")
def close_ellipse(self):
self._level -= 1
self._indent()
self.xmlgen.endElement("ellipse")
def close_polygon(self): def close_polygon(self):
self._level -= 1 self._level -= 1
self._indent() self._indent()
@ -615,6 +625,18 @@ def dump_as_cvat_annotation(dumper, annotations):
("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 == "ellipse":
dump_data.update(OrderedDict([
("cx", "{:.2f}".format(shape.points[0])),
("cy", "{:.2f}".format(shape.points[1])),
("rx", "{:.2f}".format(shape.points[2] - shape.points[0])),
("ry", "{:.2f}".format(shape.points[1] - shape.points[3]))
]))
if shape.rotation: if shape.rotation:
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("rotation", "{:.2f}".format(shape.rotation)) ("rotation", "{:.2f}".format(shape.rotation))
@ -652,9 +674,10 @@ def dump_as_cvat_annotation(dumper, annotations):
if shape.group: if shape.group:
dump_data['group_id'] = str(shape.group) dump_data['group_id'] = str(shape.group)
if shape.type == "rectangle": if shape.type == "rectangle":
dumper.open_box(dump_data) dumper.open_box(dump_data)
elif shape.type == "ellipse":
dumper.open_ellipse(dump_data)
elif shape.type == "polygon": elif shape.type == "polygon":
dumper.open_polygon(dump_data) dumper.open_polygon(dump_data)
elif shape.type == "polyline": elif shape.type == "polyline":
@ -674,6 +697,8 @@ def dump_as_cvat_annotation(dumper, annotations):
if shape.type == "rectangle": if shape.type == "rectangle":
dumper.close_box() dumper.close_box()
elif shape.type == "ellipse":
dumper.close_ellipse()
elif shape.type == "polygon": elif shape.type == "polygon":
dumper.close_polygon() dumper.close_polygon()
elif shape.type == "polyline": elif shape.type == "polyline":
@ -743,6 +768,18 @@ def dump_as_cvat_interpolation(dumper, annotations):
("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 == "ellipse":
dump_data.update(OrderedDict([
("cx", "{:.2f}".format(shape.points[0])),
("cy", "{:.2f}".format(shape.points[1])),
("rx", "{:.2f}".format(shape.points[2] - shape.points[0])),
("ry", "{:.2f}".format(shape.points[1] - shape.points[3]))
]))
if shape.rotation: if shape.rotation:
dump_data.update(OrderedDict([ dump_data.update(OrderedDict([
("rotation", "{:.2f}".format(shape.rotation)) ("rotation", "{:.2f}".format(shape.rotation))
@ -776,6 +813,8 @@ def dump_as_cvat_interpolation(dumper, annotations):
if shape.type == "rectangle": if shape.type == "rectangle":
dumper.open_box(dump_data) dumper.open_box(dump_data)
elif shape.type == "ellipse":
dumper.open_ellipse(dump_data)
elif shape.type == "polygon": elif shape.type == "polygon":
dumper.open_polygon(dump_data) dumper.open_polygon(dump_data)
elif shape.type == "polyline": elif shape.type == "polyline":
@ -795,6 +834,8 @@ def dump_as_cvat_interpolation(dumper, annotations):
if shape.type == "rectangle": if shape.type == "rectangle":
dumper.close_box() dumper.close_box()
elif shape.type == "ellipse":
dumper.close_ellipse()
elif shape.type == "polygon": elif shape.type == "polygon":
dumper.close_polygon() dumper.close_polygon()
elif shape.type == "polyline": elif shape.type == "polyline":
@ -857,7 +898,7 @@ def dump_as_cvat_interpolation(dumper, annotations):
dumper.close_root() dumper.close_root()
def load_anno(file_object, annotations): def load_anno(file_object, annotations):
supported_shapes = ('box', 'polygon', 'polyline', 'points', 'cuboid') supported_shapes = ('box', 'ellipse', 'polygon', 'polyline', 'points', 'cuboid')
context = ElementTree.iterparse(file_object, events=("start", "end")) context = ElementTree.iterparse(file_object, events=("start", "end"))
context = iter(context) context = iter(context)
next(context) next(context)
@ -927,6 +968,11 @@ def load_anno(file_object, annotations):
shape['points'].append(el.attrib['ytl']) shape['points'].append(el.attrib['ytl'])
shape['points'].append(el.attrib['xbr']) shape['points'].append(el.attrib['xbr'])
shape['points'].append(el.attrib['ybr']) shape['points'].append(el.attrib['ybr'])
elif el.tag == 'ellipse':
shape['points'].append(el.attrib['cx'])
shape['points'].append(el.attrib['cy'])
shape['points'].append("{:.2f}".format(float(el.attrib['cx']) + float(el.attrib['rx'])))
shape['points'].append("{:.2f}".format(float(el.attrib['cy']) - float(el.attrib['ry'])))
elif el.tag == 'cuboid': elif el.tag == 'cuboid':
shape['points'].append(el.attrib['xtl1']) shape['points'].append(el.attrib['xtl1'])
shape['points'].append(el.attrib['ytl1']) shape['points'].append(el.attrib['ytl1'])

@ -3,7 +3,10 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import math import math
import cv2
import numpy as np
from itertools import chain from itertools import chain
from pycocotools import mask as mask_utils
from datumaro.components.extractor import ItemTransform from datumaro.components.extractor import ItemTransform
import datumaro.components.annotation as datum_annotation import datumaro.components.annotation as datum_annotation
@ -32,3 +35,18 @@ class RotatedBoxesToPolygons(ItemTransform):
z_order=ann.z_order)) z_order=ann.z_order))
return item.wrap(annotations=annotations) return item.wrap(annotations=annotations)
class EllipsesToMasks:
@staticmethod
def convert_ellipse(ellipse, img_h, img_w):
cx, cy, rightX, topY = ellipse.points
rx = rightX - cx
ry = cy - topY
center = (round(cx), round(cy))
axis = (round(rx), round(ry))
angle = ellipse.rotation
mat = np.zeros((img_h, img_w), dtype=np.uint8)
cv2.ellipse(mat, center, axis, angle, 0, 360, 255, thickness=-1)
rle = mask_utils.encode(np.asfortranarray(mat))
return datum_annotation.RleMask(rle=rle, label=ellipse.label, z_order=ellipse.z_order,
attributes=ellipse.attributes, group=ellipse.group)

@ -480,6 +480,7 @@ class ShapeType(str, Enum):
POLYGON = 'polygon' # (x0, y0, ..., xn, yn) POLYGON = 'polygon' # (x0, y0, ..., xn, yn)
POLYLINE = 'polyline' # (x0, y0, ..., xn, yn) POLYLINE = 'polyline' # (x0, y0, ..., xn, yn)
POINTS = 'points' # (x0, y0, ..., xn, yn) POINTS = 'points' # (x0, y0, ..., xn, yn)
ELLIPSE = 'ellipse' # (cx, cy, rx, ty)
CUBOID = 'cuboid' # (x0, y0, ..., x7, y7) CUBOID = 'cuboid' # (x0, y0, ..., x7, y7)
@classmethod @classmethod

@ -12,7 +12,7 @@ context('Annotations statistics.', () => {
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,
@ -21,16 +21,32 @@ context('Annotations statistics.', () => {
const createRectangleTrack2Points = { const createRectangleTrack2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Track', type: 'Track',
labelName: labelName, labelName,
firstX: createRectangleShape2Points.firstX, firstX: createRectangleShape2Points.firstX,
firstY: createRectangleShape2Points.firstY - 150, firstY: createRectangleShape2Points.firstY - 150,
secondX: createRectangleShape2Points.secondX, secondX: createRectangleShape2Points.secondX,
secondY: createRectangleShape2Points.secondY - 150, secondY: createRectangleShape2Points.secondY - 150,
}; };
const createEllipseShape = {
type: 'Shape',
labelName,
cx: 400,
cy: 400,
rightX: 500,
topY: 350,
};
const createEllipseTrack = {
type: 'Track',
labelName,
cx: createEllipseShape.cx,
cy: createEllipseShape.cy - 150,
rightX: createEllipseShape.rightX,
topY: createEllipseShape.topY - 150,
};
const createCuboidShape2Points = { const createCuboidShape2Points = {
points: 'From rectangle', points: 'From rectangle',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 250, firstX: 250,
firstY: 350, firstY: 350,
secondX: 350, secondX: 350,
@ -39,7 +55,7 @@ context('Annotations statistics.', () => {
const createCuboidTrack2Points = { const createCuboidTrack2Points = {
points: 'From rectangle', points: 'From rectangle',
type: 'Track', type: 'Track',
labelName: labelName, labelName,
firstX: createCuboidShape2Points.firstX, firstX: createCuboidShape2Points.firstX,
firstY: createCuboidShape2Points.firstY + 150, firstY: createCuboidShape2Points.firstY + 150,
secondX: createCuboidShape2Points.secondX, secondX: createCuboidShape2Points.secondX,
@ -48,7 +64,7 @@ context('Annotations statistics.', () => {
const createPolygonShape = { const createPolygonShape = {
reDraw: false, reDraw: false,
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 100, y: 100 }, { x: 100, y: 100 },
{ x: 150, y: 100 }, { x: 150, y: 100 },
@ -60,7 +76,7 @@ context('Annotations statistics.', () => {
const createPolygonTrack = { const createPolygonTrack = {
reDraw: false, reDraw: false,
type: 'Track', type: 'Track',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 200, y: 100 }, { x: 200, y: 100 },
{ x: 250, y: 100 }, { x: 250, y: 100 },
@ -71,7 +87,7 @@ context('Annotations statistics.', () => {
}; };
const createPolylinesShape = { const createPolylinesShape = {
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 },
@ -82,7 +98,7 @@ context('Annotations statistics.', () => {
}; };
const createPolylinesTrack = { const createPolylinesTrack = {
type: 'Track', type: 'Track',
labelName: labelName, labelName,
pointsMap: [ pointsMap: [
{ x: 400, y: 100 }, { x: 400, y: 100 },
{ x: 450, y: 100 }, { x: 450, y: 100 },
@ -93,14 +109,14 @@ context('Annotations statistics.', () => {
}; };
const createPointsShape = { const createPointsShape = {
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
pointsMap: [{ x: 200, y: 400 }], pointsMap: [{ x: 200, y: 400 }],
complete: true, complete: true,
numberOfPoints: null, numberOfPoints: null,
}; };
const createPointsTrack = { const createPointsTrack = {
type: 'Track', type: 'Track',
labelName: labelName, labelName,
pointsMap: [{ x: 300, y: 400 }], pointsMap: [{ x: 300, y: 400 }],
complete: true, complete: true,
numberOfPoints: null, numberOfPoints: null,
@ -123,6 +139,9 @@ context('Annotations statistics.', () => {
cy.goToNextFrame(4); cy.goToNextFrame(4);
cy.createPoint(createPointsShape); cy.createPoint(createPointsShape);
cy.createPoint(createPointsTrack); cy.createPoint(createPointsTrack);
cy.goToNextFrame(5);
cy.createEllipse(createEllipseShape);
cy.createEllipse(createEllipseTrack);
}); });
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
@ -142,12 +161,12 @@ context('Annotations statistics.', () => {
cy.get(jobInfoTableHeader) cy.get(jobInfoTableHeader)
.find('th') .find('th')
.then((jobInfoTableHeaderColumns) => { .then((jobInfoTableHeaderColumns) => {
jobInfoTableHeaderColumns = Array.from(jobInfoTableHeaderColumns); const elTextContent = Array.from(jobInfoTableHeaderColumns).map((el) => (
const elTextContent = jobInfoTableHeaderColumns.map((el) => el.textContent.replace(/\s/g, '')
el.textContent.replace(/\s/g, ''), )); // Removing spaces. For example: " Tags ". In Firefox, this causes an error.
); // Removing spaces. For example: " Tags ". In Firefox, this causes an error.
for (let i = 0; i < objectTypes.length; i++) { for (let i = 0; i < objectTypes.length; i++) {
expect(elTextContent).to.include(objectTypes[i]); // expected [ Array(11) ] to include Cuboids, etc. expect(elTextContent)
.to.include(objectTypes[i]); // expected [ Array(11) ] to include Cuboids, etc.
} }
}); });
}); });
@ -162,16 +181,15 @@ context('Annotations statistics.', () => {
.parents('tr') .parents('tr')
.find('td') .find('td')
.then((tableBodyFirstRowThs) => { .then((tableBodyFirstRowThs) => {
tableBodyFirstRowThs = Array.from(tableBodyFirstRowThs); const elTextContent = Array.from(tableBodyFirstRowThs).map((el) => el.textContent);
const elTextContent = tableBodyFirstRowThs.map((el) => el.textContent);
expect(elTextContent[0]).to.be.equal(labelName); expect(elTextContent[0]).to.be.equal(labelName);
for (let i = 1; i < 6; i++) { for (let i = 1; i < 7; i++) {
expect(elTextContent[i]).to.be.equal('1 / 1'); // Rectangle, Polygon, Polyline, Points, Cuboids expect(elTextContent[i]).to.be.equal('1 / 1'); // Rectangle, Polygon, Polyline, Points, Cuboids, Ellipses
} }
expect(elTextContent[6]).to.be.equal('1'); // Tags expect(elTextContent[7]).to.be.equal('1'); // Tags
expect(elTextContent[7]).to.be.equal('11'); // Manually expect(elTextContent[8]).to.be.equal('13'); // Manually
expect(elTextContent[8]).to.be.equal('35'); // Interpolated expect(elTextContent[9]).to.be.equal('39'); // Interpolated
expect(elTextContent[9]).to.be.equal('46'); // Total expect(elTextContent[10]).to.be.equal('52'); // Total
}); });
}); });
cy.contains('[type="button"]', 'OK').click(); cy.contains('[type="button"]', 'OK').click();

@ -334,6 +334,22 @@ Cypress.Commands.add('createPoint', (createPointParams) => {
cy.checkObjectParameters(createPointParams, 'POINTS'); cy.checkObjectParameters(createPointParams, 'POINTS');
}); });
Cypress.Commands.add('createEllipse', (createEllipseParams) => {
cy.interactControlButton('draw-ellipse');
cy.switchLabel(createEllipseParams.labelName, 'draw-ellipse');
cy.get('.cvat-draw-ellipse-popover').within(() => {
cy.get('.ant-select-selection-item').then(($labelValue) => {
selectedValueGlobal = $labelValue.text();
});
cy.contains('button', createEllipseParams.type).click();
});
cy.get('.cvat-canvas-container')
.click(createEllipseParams.cx, createEllipseParams.cy)
.click(createEllipseParams.rightX, createEllipseParams.topY);
cy.checkPopoverHidden('draw-ellipse');
cy.checkObjectParameters(createEllipseParams, 'ELLIPSE');
});
Cypress.Commands.add('changeAppearance', (colorBy) => { Cypress.Commands.add('changeAppearance', (colorBy) => {
cy.get('.cvat-appearance-color-by-radio-group').within(() => { cy.get('.cvat-appearance-color-by-radio-group').within(() => {
cy.get('[type="radio"]').check(colorBy, { force: true }); cy.get('[type="radio"]').check(colorBy, { force: true });

Loading…
Cancel
Save