)
diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index 60d51c369aff..1bf30e34ad9b 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -404,6 +404,32 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.style.cursor = '';
this.mode = Mode.IDLE;
if (state && points) {
+ // we need to store "updated" and set "points" to an empty array
+ // as this information is used to define "updated" objects in diff logic during canvas objects setup
+ // if because of any reason updating was actually rejected somewhere, we must reset view inside this logic
+
+ // there is one more deeper issue:
+ // somewhere canvas updates drawn views and then sends request,
+ // updating internal CVAT state (e.g. drag, resize)
+ // somewhere, however, it just sends request to update internal CVAT state
+ // (e.g. remove point, edit polygon/polyline)
+ // if object view was not changed by canvas and points accepted as is without any changes
+ // the view will not be updated during objects setup if we just set points as is here
+ // that is why we need to set points to an empty array (something that can't normally come from CVAT)
+ // I do not think it can be easily fixed now, hovewer in the future we should refactor code
+ if (Number.isInteger(state.parentID)) {
+ const { elements } = this.drawnStates[state.parentID];
+ const drawnElement = elements.find((el) => el.clientID === state.clientID);
+ drawnElement.updated = 0;
+ drawnElement.points = [];
+
+ this.drawnStates[state.parentID].updated = 0;
+ this.drawnStates[state.parentID].points = [];
+ } else {
+ this.drawnStates[state.clientID].updated = 0;
+ this.drawnStates[state.clientID].points = [];
+ }
+
const event: CustomEvent = new CustomEvent('canvas.edited', {
bubbles: false,
cancelable: true,
diff --git a/cvat-canvas/src/typescript/consts.ts b/cvat-canvas/src/typescript/consts.ts
index 3ea75dbb557d..1e39c1316879 100644
--- a/cvat-canvas/src/typescript/consts.ts
+++ b/cvat-canvas/src/typescript/consts.ts
@@ -1,4 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
+// Copyright (C) CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@@ -6,8 +7,7 @@ const BASE_STROKE_WIDTH = 1.25;
const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 4;
const TEXT_MARGIN = 10;
-const AREA_THRESHOLD = 9;
-const SIZE_THRESHOLD = 3;
+const SIZE_THRESHOLD = 1;
const POINTS_STROKE_WIDTH = 1;
const POINTS_SELECTED_STROKE_WIDTH = 4;
const MIN_EDGE_LENGTH = 3;
@@ -36,7 +36,6 @@ export default {
BASE_GRID_WIDTH,
BASE_POINT_SIZE,
TEXT_MARGIN,
- AREA_THRESHOLD,
SIZE_THRESHOLD,
POINTS_STROKE_WIDTH,
POINTS_SELECTED_STROKE_WIDTH,
diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts
index d54117c72957..ebee54109a04 100644
--- a/cvat-canvas/src/typescript/drawHandler.ts
+++ b/cvat-canvas/src/typescript/drawHandler.ts
@@ -47,16 +47,18 @@ interface FinalCoordinates {
function checkConstraint(shapeType: string, points: number[], box: Box | null = null): boolean {
if (shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = points;
- return (xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD;
+ const [width, height] = [xbr - xtl, ybr - ytl];
+ return width >= consts.SIZE_THRESHOLD && height >= consts.SIZE_THRESHOLD;
}
if (shapeType === 'polygon') {
- return (box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD && points.length >= 3 * 2;
+ const [width, height] = [box.xbr - box.xtl, box.ybr - box.ytl];
+ return (width >= consts.SIZE_THRESHOLD || height > consts.SIZE_THRESHOLD) && points.length >= 3 * 2;
}
if (shapeType === 'polyline') {
- return (box.xbr - box.xtl >= consts.SIZE_THRESHOLD ||
- box.ybr - box.ytl >= consts.SIZE_THRESHOLD) && points.length >= 2 * 2;
+ const [width, height] = [box.xbr - box.xtl, box.ybr - box.ytl];
+ return (width >= consts.SIZE_THRESHOLD || height >= consts.SIZE_THRESHOLD) && points.length >= 2 * 2;
}
if (shapeType === 'points') {
@@ -64,18 +66,22 @@ function checkConstraint(shapeType: string, points: number[], box: Box | null =
}
if (shapeType === 'ellipse') {
- const [rx, ry] = [points[2] - points[0], points[1] - points[3]];
- return rx * ry * Math.PI >= consts.AREA_THRESHOLD;
+ const [width, height] = [(points[2] - points[0]) * 2, (points[1] - points[3]) * 2];
+ return width >= consts.SIZE_THRESHOLD && height > consts.SIZE_THRESHOLD;
}
if (shapeType === 'cuboid') {
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]) >= consts.SIZE_THRESHOLD &&
+ (points[3] - points[1]) >= consts.SIZE_THRESHOLD
+ );
}
if (shapeType === 'skeleton') {
const [xtl, ytl, xbr, ybr] = points;
- return (xbr - xtl >= 1 || ybr - ytl >= 1);
+ const [width, height] = [xbr - xtl, ybr - ytl];
+ return width >= consts.SIZE_THRESHOLD || height >= consts.SIZE_THRESHOLD;
}
return false;
diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts
index 9e210067e7d7..bde8cdbb8671 100644
--- a/cvat-canvas/src/typescript/shared.ts
+++ b/cvat-canvas/src/typescript/shared.ts
@@ -100,7 +100,7 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer:
.fill('white')
.addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void {
- let text = `${Math.round(shape.width())}x${Math.round(shape.height())}px`;
+ let text = `${Math.floor(shape.width())}x${Math.floor(shape.height())}px`;
if (shape.type === 'rect' || shape.type === 'ellipse') {
let rotation = shape.transform().rotation || 0;
// be sure, that rotation in range [0; 360]
diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt
index 2bd32ba664f5..7e75d4e88ec2 100644
--- a/cvat-cli/requirements/base.txt
+++ b/cvat-cli/requirements/base.txt
@@ -1,4 +1,4 @@
-cvat-sdk==2.27.1
+cvat-sdk==2.28.1
attrs>=24.2.0
Pillow>=10.3.0
diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py
index 063100886b02..5515a1a9a1ae 100644
--- a/cvat-cli/src/cvat_cli/version.py
+++ b/cvat-cli/src/cvat_cli/version.py
@@ -1 +1 @@
-VERSION = "2.27.1"
+VERSION = "2.28.1"
diff --git a/cvat-core/src/object-utils.ts b/cvat-core/src/object-utils.ts
index 12712032dca5..06a90a958924 100644
--- a/cvat-core/src/object-utils.ts
+++ b/cvat-core/src/object-utils.ts
@@ -67,45 +67,48 @@ export function findAngleDiff(rightAngle: number, leftAngle: number): number {
}
export function checkShapeArea(shapeType: ShapeType, points: number[]): boolean {
- const MIN_SHAPE_LENGTH = 3;
- const MIN_SHAPE_AREA = 9;
- const MIN_MASK_SHAPE_AREA = 1;
+ const MIN_SHAPE_SIZE = 1;
if (shapeType === ShapeType.POINTS) {
return true;
}
+ let width = 0;
+ let height = 0;
+
if (shapeType === ShapeType.MASK) {
const [left, top, right, bottom] = points.slice(-4);
- const area = (right - left + 1) * (bottom - top + 1);
- return area >= MIN_MASK_SHAPE_AREA;
- }
-
- if (shapeType === ShapeType.ELLIPSE) {
+ [width, height] = [right - left + 1, bottom - top + 1];
+ } else if (shapeType === ShapeType.RECTANGLE) {
+ const [xtl, ytl, xbr, ybr] = points;
+ [width, height] = [xbr - xtl, ybr - ytl];
+ } else if (shapeType === ShapeType.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 xmax = Number.MIN_SAFE_INTEGER;
- let ymin = Number.MAX_SAFE_INTEGER;
- let ymax = Number.MIN_SAFE_INTEGER;
+ [width, height] = [(rightX - cx) * 2, (cy - topY) * 2];
+ } else {
+ // polygon, polyline, cuboid, skeleton
+ let xmin = Number.MAX_SAFE_INTEGER;
+ let xmax = Number.MIN_SAFE_INTEGER;
+ let ymin = Number.MAX_SAFE_INTEGER;
+ let ymax = Number.MIN_SAFE_INTEGER;
+
+ for (let i = 0; i < points.length - 1; i += 2) {
+ xmin = Math.min(xmin, points[i]);
+ xmax = Math.max(xmax, points[i]);
+ ymin = Math.min(ymin, points[i + 1]);
+ ymax = Math.max(ymax, points[i + 1]);
+ }
- for (let i = 0; i < points.length - 1; i += 2) {
- xmin = Math.min(xmin, points[i]);
- xmax = Math.max(xmax, points[i]);
- ymin = Math.min(ymin, points[i + 1]);
- ymax = Math.max(ymax, points[i + 1]);
- }
+ if ([ShapeType.POLYLINE, ShapeType.SKELETON, ShapeType.POLYGON].includes(shapeType)) {
+ // for polyshapes consider at least one dimension
+ // skeleton in corner cases may be a regular polyshape
+ return Math.max(xmax - xmin, ymax - ymin) >= MIN_SHAPE_SIZE;
+ }
- if (shapeType === ShapeType.POLYLINE) {
- const length = Math.max(xmax - xmin, ymax - ymin);
- return length >= MIN_SHAPE_LENGTH;
+ [width, height] = [xmax - xmin, ymax - ymin];
}
- const area = (xmax - xmin) * (ymax - ymin);
- return area >= MIN_SHAPE_AREA;
+ return width >= MIN_SHAPE_SIZE && height >= MIN_SHAPE_SIZE;
}
export function rotatePoint(x: number, y: number, angle: number, cx = 0, cy = 0): number[] {
diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh
index 17106556b638..8e3ed8469198 100755
--- a/cvat-sdk/gen/generate.sh
+++ b/cvat-sdk/gen/generate.sh
@@ -8,7 +8,7 @@ set -e
GENERATOR_VERSION="v6.0.1"
-VERSION="2.27.1"
+VERSION="2.28.1"
LIB_NAME="cvat_sdk"
LAYER1_LIB_NAME="${LIB_NAME}/api_client"
DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)"
diff --git a/cvat-ui/package.json b/cvat-ui/package.json
index 058442d25575..1daa2292e3e9 100644
--- a/cvat-ui/package.json
+++ b/cvat-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
- "version": "2.27.1",
+ "version": "2.28.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
diff --git a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx
index e624e7c4adf0..c659daa8ac0e 100644
--- a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx
+++ b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx
@@ -80,8 +80,8 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element {
const maxAutoSaveInterval = 60;
const minAAMMargin = 0;
const maxAAMMargin = 1000;
- const minControlPointsSize = 4;
- const maxControlPointsSize = 8;
+ const minControlPointsSize = 2;
+ const maxControlPointsSize = 10;
return (
diff --git a/cvat/__init__.py b/cvat/__init__.py
index 0b18ef317830..ced839eace75 100644
--- a/cvat/__init__.py
+++ b/cvat/__init__.py
@@ -4,6 +4,6 @@
from cvat.utils.version import get_version
-VERSION = (2, 27, 1, "alpha", 0)
+VERSION = (2, 28, 1, "alpha", 0)
__version__ = get_version(VERSION)
diff --git a/cvat/schema.yml b/cvat/schema.yml
index a6a33a62aa7a..67f1af715f92 100644
--- a/cvat/schema.yml
+++ b/cvat/schema.yml
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: CVAT REST API
- version: 2.27.1
+ version: 2.28.1
description: REST API for Computer Vision Annotation Tool (CVAT)
termsOfService: https://www.google.com/policies/terms/
contact:
diff --git a/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js b/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js
new file mode 100644
index 000000000000..51a599672aa3
--- /dev/null
+++ b/tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js
@@ -0,0 +1,170 @@
+// Copyright (C) CVAT.ai Corporation
+//
+// SPDX-License-Identifier: MIT
+
+///
+
+const taskName = '5frames';
+const labelName = 'label';
+const attrName = 'attr1';
+const textDefaultValue = 'Some text';
+const issueId = '8952';
+const imagesCount = 5;
+const width = 400;
+const height = 400;
+const posX = 50;
+const posY = 50;
+const color = 'white';
+const imageFileName = `image_${issueId}`;
+const archiveName = `${imageFileName}.zip`;
+const archivePath = `cypress/fixtures/${archiveName}`;
+const imagesFolder = `cypress/fixtures/${imageFileName}`;
+
+const rect = [
+ 30,
+ 30,
+ 30 + 34,
+ 30 + 23,
+];
+
+function translatePoints(points, delta, axis) {
+ if (axis === 'x') {
+ return [
+ points[0] + delta,
+ points[1],
+ points[2] + delta,
+ points[3],
+ ];
+ }
+ if (axis === 'y') {
+ return [
+ points[0],
+ points[1] + delta,
+ points[2],
+ points[3] + delta,
+ ];
+ }
+ return points;
+}
+
+context('Create any track, check if track works correctly after deleting some frames', () => {
+ function readShapeCoords() {
+ return cy.get('.cvat_canvas_shape').then(($shape) => ({
+ x: +$shape.attr('x'),
+ y: +$shape.attr('y'),
+ }));
+ }
+
+ function validateShapeCoords({ x, y }) {
+ const precision = 0.01; // db server precision is 2 digits
+ cy.get('.cvat_canvas_shape').then(($shape) => {
+ const [xVal, yVal] = [
+ +$shape.attr('x'),
+ +$shape.attr('y'),
+ ];
+ expect(xVal).to.be.closeTo(x, precision);
+ expect(yVal).to.be.closeTo(y, precision);
+ });
+ }
+
+ describe('Description: user error, Could not receive frame 43 No one left position or right position was found. Interpolation impossible', () => {
+ let jobID = null;
+ const delta = 300;
+ before(() => {
+ cy.visit('/auth/login');
+ cy.login();
+
+ // Create assets for task using nodeJS
+ cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
+ cy.createZipArchive(imagesFolder, archivePath);
+ cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName);
+
+ cy.goToTaskList();
+ cy.openTaskJob(taskName);
+ cy.url().should('contain', 'jobs').then((url) => {
+ const last = url.lastIndexOf('/');
+ jobID = parseInt(url.slice(last + 1), 10);
+ }).then(() => {
+ // Remove all annotations and draw a track rect
+ const points0 = rect;
+ const points1 = translatePoints(points0, delta, 'x');
+ const points2 = translatePoints(points1, delta, 'y');
+ const track = {
+ shapes: [
+ {
+ frame: 0,
+ type: 'rectangle',
+ points: points0,
+ },
+ {
+ frame: 2,
+ type: 'rectangle',
+ points: points1,
+ },
+ {
+ frame: 4,
+ type: 'rectangle',
+ points: points2,
+ },
+ ],
+ frame: 0,
+ labelName,
+ objectType: 'track',
+ };
+ cy.headlessCreateObjects([track], jobID);
+ });
+ });
+
+ beforeEach(() => {
+ cy.headlessRestoreAllFrames(jobID);
+
+ // Get job meta updates from the server and reload page to bring changes to UI
+ cy.reload();
+
+ cy.saveJob();
+ cy.get('.cvat-player-first-button').click();
+ });
+
+ it('Delete interpolated frames 0, 2, 4. Error should not appear', () => {
+ // Delete frames 0, 2, 4. Watch out for errors
+ cy.get('.cvat-player-first-button').click();
+ cy.checkFrameNum(0);
+ cy.clickDeleteFrameAnnotationView();
+ cy.checkFrameNum(1);
+ cy.goToNextFrame(2);
+ cy.clickDeleteFrameAnnotationView();
+ cy.checkFrameNum(3);
+ cy.goToNextFrame(4);
+ cy.clickDeleteFrameAnnotationView();
+
+ // There should be no objects on the deleted frame
+ cy.get('.cvat_canvas_shape').should('not.exist');
+ cy.clickSaveAnnotationView();
+
+ // Reopening a task with bad metadata might throw an exception that we can catch
+ cy.goToTaskList();
+ cy.openTaskJob(taskName);
+ });
+
+ it('Change track positions on frames 2 and 4. Delete frame. Confirm same shape positions', () => {
+ cy.goCheckFrameNumber(2);
+ cy.clickDeleteFrameAnnotationView();
+ cy.checkFrameNum(3);
+ cy.clickSaveAnnotationView();
+
+ let pos3 = null;
+ readShapeCoords().then((posOnFrame3) => {
+ pos3 = posOnFrame3;
+ cy.goToPreviousFrame(1);
+ });
+ let pos1 = null;
+ readShapeCoords().then((posOnFrame1) => {
+ pos1 = posOnFrame1;
+ });
+ cy.reload().then(() => {
+ cy.goToNextFrame(1).then(() => validateShapeCoords(pos1));
+ cy.goToNextFrame(3).then(() => validateShapeCoords(pos3));
+ });
+ });
+ });
+});
diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js
index ba0c826e4cd6..ee2f3a12b780 100644
--- a/tests/cypress/support/commands.js
+++ b/tests/cypress/support/commands.js
@@ -348,6 +348,17 @@ Cypress.Commands.add('headlessCreateObjects', (objects, jobID) => {
});
});
+Cypress.Commands.add('headlessRestoreAllFrames', (jobID) => {
+ cy.intercept('PATCH', `/api/jobs/${jobID}/data/meta**`).as('patchMeta');
+ cy.window().then(async ($win) => {
+ await $win.cvat.server.request(`/api/jobs/${jobID}/data/meta`, {
+ method: 'PATCH',
+ data: { deleted_frames: [] },
+ });
+ });
+ cy.wait('@patchMeta');
+});
+
Cypress.Commands.add('headlessCreateTask', (taskSpec, dataSpec, extras) => {
cy.window().then(async ($win) => {
const task = new $win.cvat.classes.Task({