cvat-ai--cvat
394 行
15 KiB
JavaScript
394 行
15 KiB
JavaScript
// Copyright (C) CVAT.ai Corporation
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
/// <reference types="cypress" />
|
|
|
|
import * as allure from 'allure-js-commons';
|
|
import { taskName, labelName } from '../../support/const';
|
|
import { getShapeCoord, toggleAutoSimplify } from '../../support/utils.cy';
|
|
import { translatePoint } from '../../support/utils';
|
|
|
|
context('Simplify polygons feature', { scrollBehavior: false }, () => {
|
|
const polygonCenter = { x: 510, y: 324 };
|
|
const detailedPolygonPoints = [
|
|
// a jagged shape with redundant points
|
|
{ x: 340, y: 185 },
|
|
{ x: 360, y: 197.5 },
|
|
{ x: 380, y: 210 },
|
|
{ x: 400.5, y: 194 },
|
|
{ x: 421, y: 178 },
|
|
{ x: 438, y: 196 },
|
|
{ x: 455, y: 214 },
|
|
{ x: 476.5, y: 195 },
|
|
{ x: 498, y: 176 },
|
|
{ x: 518, y: 195.5 },
|
|
{ x: 538, y: 215 },
|
|
{ x: 558, y: 198.5 },
|
|
{ x: 578, y: 182 },
|
|
{ x: 595.5, y: 201 },
|
|
{ x: 613, y: 220 },
|
|
{ x: 634, y: 207.5 },
|
|
{ x: 655, y: 195 },
|
|
{ x: 645.5, y: 220 },
|
|
{ x: 636, y: 245 },
|
|
{ x: 668, y: 257.5 },
|
|
{ x: 700, y: 270 },
|
|
{ x: 667.5, y: 281 },
|
|
{ x: 635, y: 292 },
|
|
{ x: 665, y: 312 },
|
|
{ x: 695, y: 332 },
|
|
{ x: 663, y: 340 },
|
|
{ x: 631, y: 348 },
|
|
{ x: 652, y: 375 },
|
|
{ x: 673, y: 402 },
|
|
{ x: 642.5, y: 394 },
|
|
{ x: 612, y: 386 },
|
|
{ x: 599, y: 423 },
|
|
{ x: 586, y: 460 },
|
|
{ x: 567, y: 431 },
|
|
{ x: 548, y: 402 },
|
|
{ x: 526.5, y: 437 },
|
|
{ x: 505, y: 472 },
|
|
{ x: 487.5, y: 437 },
|
|
{ x: 470, y: 402 },
|
|
{ x: 447, y: 429 },
|
|
{ x: 424, y: 456 },
|
|
{ x: 413, y: 419 },
|
|
{ x: 402, y: 382 },
|
|
{ x: 372, y: 396 },
|
|
{ x: 342, y: 410 },
|
|
{ x: 362, y: 379 },
|
|
{ x: 382, y: 348 },
|
|
{ x: 351, y: 337 },
|
|
{ x: 320, y: 326 },
|
|
{ x: 352, y: 309 },
|
|
{ x: 384, y: 292 },
|
|
{ x: 353.5, y: 276 },
|
|
{ x: 323, y: 260 },
|
|
{ x: 356.5, y: 253 },
|
|
{ x: 390, y: 246 },
|
|
{ x: 375, y: 225.5 },
|
|
{ x: 360, y: 205 },
|
|
{ x: 343, y: 215 },
|
|
{ x: 326, y: 225 },
|
|
];
|
|
const polygonPointsCount = detailedPolygonPoints.length;
|
|
const createDetailedPolygon = {
|
|
type: 'Shape',
|
|
labelName,
|
|
pointsMap: detailedPolygonPoints,
|
|
numberOfPoints: null,
|
|
};
|
|
const referenceObjectId = 1;
|
|
const firstSimplifiedObjectId = 2;
|
|
const simplificationCases = [
|
|
{
|
|
accuracy: 0,
|
|
message: 'Aggressively simplified polygon',
|
|
shouldHaveLessPointsThanBaseline: true,
|
|
},
|
|
{
|
|
accuracy: 2,
|
|
message: 'Middle-accuracy simplified polygon',
|
|
shouldHaveMorePointsThanPrevious: true,
|
|
},
|
|
{
|
|
accuracy: 13,
|
|
message: 'High-accuracy simplified polygon',
|
|
shouldHaveAtLeastPreviousPoints: true,
|
|
shouldHaveAtLeastOriginalPoints: true,
|
|
shouldKeepBaselineArea: true,
|
|
},
|
|
];
|
|
const originalObjectId = 1;
|
|
const copyIds = [2, 3, 4];
|
|
const resultIds = [5, 6, 7];
|
|
|
|
function parsePolygonPoints(rawPoints) {
|
|
return rawPoints
|
|
.filter(Boolean)
|
|
.map((point) => point.split(',').map(Number));
|
|
}
|
|
|
|
function shoelaceArea(points) {
|
|
// Gauss's shoelace formula to find area of a polygon by its points only
|
|
|
|
if (points.length < 3) {
|
|
return 0;
|
|
}
|
|
const signedArea = points.reduce((acc, point, index) => {
|
|
const next = points[(index + 1) % points.length];
|
|
return acc + point[0] * next[1] - next[0] * point[1];
|
|
}, 0);
|
|
return Math.abs(signedArea) / 2;
|
|
}
|
|
|
|
function getPolygonPoints(objectId) {
|
|
return getShapeCoord('polygon', `#cvat_canvas_shape_${objectId}`).then(parsePolygonPoints);
|
|
}
|
|
|
|
function getPolygonStats(objectId) {
|
|
return getPolygonPoints(objectId).then((points) => ({
|
|
objectId,
|
|
area: shoelaceArea(points),
|
|
pointsCount: points.length,
|
|
}));
|
|
}
|
|
|
|
function logPolygonMetrics(message, metrics) {
|
|
const {
|
|
pointsCount, area, objectId,
|
|
} = metrics;
|
|
const logEntry = {
|
|
objectId,
|
|
message,
|
|
points: pointsCount,
|
|
area: area.toFixed(1),
|
|
};
|
|
cy.task('log', logEntry);
|
|
}
|
|
|
|
function setSimplifyAccuracy(value) {
|
|
const sliderSelector = '.cvat-approx-poly-threshold-wrapper [role="slider"]';
|
|
|
|
cy.get(sliderSelector).click();
|
|
cy.get(sliderSelector).type('{home}');
|
|
|
|
if (value > 0) {
|
|
cy.get(sliderSelector).type('{rightarrow}'.repeat(value));
|
|
}
|
|
|
|
cy.get(sliderSelector).should('have.attr', 'aria-valuenow', `${value}`);
|
|
}
|
|
|
|
function makeCopy(expectedObjectId, canvasPosition = polygonCenter) {
|
|
cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Make a copy');
|
|
cy.get('.cvat-canvas-container').click(canvasPosition.x, canvasPosition.y);
|
|
cy.get(`#cvat_canvas_shape_${expectedObjectId}`).should('exist').and('be.visible');
|
|
return cy.get(`#cvat-objects-sidebar-state-item-${expectedObjectId}`).should('exist').and('be.visible');
|
|
}
|
|
|
|
function simplifyPolygon({ objectId, accuracy }) {
|
|
cy.interactAnnotationObjectMenu(`#cvat-objects-sidebar-state-item-${objectId}`, 'Simplify');
|
|
setSimplifyAccuracy(accuracy);
|
|
cy.get('.cvat-approx-poly-threshold-wrapper .ant-btn-primary').click({ force: true });
|
|
cy.get('.cvat-approx-poly-threshold-wrapper').should('not.exist');
|
|
return false;
|
|
}
|
|
|
|
function expectCopiedPolygonMetrics(metrics, baselineStats) {
|
|
expect(metrics.pointsCount).to.be.equal(baselineStats.pointsCount);
|
|
expect(metrics.area).to.be.closeTo(baselineStats.area, 1);
|
|
}
|
|
|
|
function runSimplificationCases(cases) {
|
|
cases.forEach((simplificationCase, index) => {
|
|
const objectId = firstSimplifiedObjectId + index;
|
|
|
|
if (index > 0) {
|
|
makeCopy(objectId);
|
|
}
|
|
|
|
const isNewShape = simplifyPolygon({ objectId, ...simplificationCase });
|
|
getPolygonStats(objectId + +isNewShape).then((metrics) => {
|
|
logPolygonMetrics(simplificationCase.message, metrics);
|
|
});
|
|
});
|
|
}
|
|
function runSimplifyAction(distance) {
|
|
cy.selectAnnotationsAction('Simplify polygons and polylines');
|
|
cy.setAnnotationActionParameter('Distance', 'input', distance);
|
|
// Run action
|
|
cy.get('.cvat-action-runner-run-btn').click();
|
|
cy.get('.cvat-action-runner-run-btn.ant-btn-loading').should('exist');
|
|
}
|
|
function simplifyAction({ objectId, distance }) {
|
|
cy.get(`#cvat-objects-sidebar-state-item-${objectId}`)
|
|
.find('.cvat-object-item-menu-button').click();
|
|
cy.get('.cvat-object-item-menu')
|
|
.contains('button', 'Run annotation action').click();
|
|
|
|
runSimplifyAction(distance);
|
|
// Wait for modal to disappear
|
|
cy.get('.cvat-action-runner-content').should('not.exist');
|
|
return true;
|
|
}
|
|
function simplifyBulkAction(distance) {
|
|
cy.openAnnotationsActionsModal();
|
|
runSimplifyAction(distance);
|
|
cy.closeAnnotationsActionsModal();
|
|
}
|
|
function approveSimplify() {
|
|
cy.get('.cvat-approx-poly-threshold-wrapper').find('.anticon-check').click();
|
|
cy.get('.cvat-approx-poly-threshold-wrapper').should('not.exist');
|
|
}
|
|
function checkLessPointsThan(objectId, refPoints) {
|
|
cy.get(`#cvat_canvas_shape_${objectId}`)
|
|
.should(($shape) => {
|
|
// retry until works
|
|
const pointsRaw = $shape.attr('points');
|
|
const points = parsePolygonPoints([pointsRaw]);
|
|
expect(points.length).to.be.lessThan(refPoints.length);
|
|
});
|
|
}
|
|
|
|
before(() => {
|
|
cy.prepareUserSession();
|
|
cy.openTaskJob(taskName);
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.removeAnnotations();
|
|
});
|
|
|
|
context('Simplify one polygon', () => {
|
|
it("'Simplify' removes points, decreases areas. Higher accuracy restores shape", () => {
|
|
cy.createPolygon(createDetailedPolygon, null, 'shiftHover');
|
|
makeCopy(firstSimplifiedObjectId, polygonCenter);
|
|
getPolygonStats(referenceObjectId).then((baselineStats) => {
|
|
expect(baselineStats.pointsCount).to.be.at.least(polygonPointsCount);
|
|
expect(baselineStats.area).to.be.greaterThan(0);
|
|
|
|
getPolygonStats(firstSimplifiedObjectId).then((metrics) => {
|
|
logPolygonMetrics('Copied polygon', metrics);
|
|
expectCopiedPolygonMetrics(metrics, baselineStats);
|
|
});
|
|
|
|
runSimplificationCases(simplificationCases);
|
|
|
|
getPolygonStats(referenceObjectId).then((originalStats) => {
|
|
expect(originalStats.pointsCount).to.equal(baselineStats.pointsCount);
|
|
expect(originalStats.area).to.be.closeTo(baselineStats.area, 1);
|
|
});
|
|
});
|
|
});
|
|
|
|
context('Auto-simplify', () => {
|
|
before(() => {
|
|
cy.interactControlButton('draw-polyline');
|
|
toggleAutoSimplify(true, 'polyline');
|
|
cy.interactControlButton('draw-polyline');
|
|
cy.interactControlButton('draw-polygon');
|
|
toggleAutoSimplify(true, 'polygon');
|
|
cy.interactControlButton('draw-polygon');
|
|
});
|
|
after(() => {
|
|
cy.interactControlButton('draw-polygon');
|
|
toggleAutoSimplify(false, 'polygon');
|
|
cy.interactControlButton('draw-polygon');
|
|
cy.get('.cvat-polygon-popover').should('not.exist');
|
|
cy.interactControlButton('draw-polyline');
|
|
toggleAutoSimplify(false, 'polyline');
|
|
cy.interactControlButton('draw-polyline');
|
|
cy.get('.cvat-polyline-popover').should('not.exist');
|
|
});
|
|
|
|
it('Auto-simplify when drawing a polyline', () => {
|
|
cy.createPolyline({ ...createDetailedPolygon }, null, 'shiftHover');
|
|
checkLessPointsThan(referenceObjectId, detailedPolygonPoints);
|
|
approveSimplify();
|
|
});
|
|
it('Auto-simplify when drawing a polygon', () => {
|
|
allure.issue('https://github.com/cvat-ai/cvat/pull/10568', 'Auto simplify initialized on first use');
|
|
cy.createPolygon({ ...createDetailedPolygon }, null, 'shiftHover');
|
|
checkLessPointsThan(referenceObjectId, detailedPolygonPoints);
|
|
approveSimplify();
|
|
});
|
|
});
|
|
});
|
|
|
|
context("'Simplify' can be invoked as annotations action", () => {
|
|
const distances = [20, 40, 64];
|
|
let originalArea;
|
|
|
|
function checkAreaProgression(refObjectArea, refObjectId, [stats1, stats2, stats3]) {
|
|
// Verify area progression: copy1Area < copy2Area < copy3Area
|
|
expect(stats1.area).to.be.lessThan(stats2.area);
|
|
expect(stats2.area).to.be.lessThan(stats3.area);
|
|
|
|
// Verify original polygon unchanged
|
|
return getPolygonStats(refObjectId).then((finalOriginalStats) => {
|
|
expect(finalOriginalStats.area).to.be.closeTo(refObjectArea, 1);
|
|
});
|
|
}
|
|
function checkAreasEqual(refObjectId, [stats1, stats2, stats3]) {
|
|
expect(stats1.area).to.be.closeTo(stats2.area, 1, 'area 1 not close to area 2');
|
|
expect(stats2.area).to.be.closeTo(stats3.area, 1, 'area 2 not close to area 3');
|
|
expect(stats3.area).to.be.closeTo(stats1.area, 1, 'area 3 not close to area 1');
|
|
|
|
return getPolygonStats(refObjectId).then((finalOriginalStats) => {
|
|
expect(finalOriginalStats.area).to.be.closeTo(stats1.area, 1, 'ref object is different from the others');
|
|
});
|
|
}
|
|
function logSimplifyStats(msg, stats, distance, objectId) {
|
|
const entry = {
|
|
objectId,
|
|
message: `${msg} (distance=${distance})`,
|
|
points: stats.pointsCount,
|
|
area: stats.area.toFixed(1),
|
|
};
|
|
cy.task('log', entry);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
cy.createPolygon(createDetailedPolygon, null, 'shiftHover');
|
|
// Make 3 copies, place them around cavnas for visual
|
|
makeCopy(copyIds[0], translatePoint({ a: -200 }, polygonCenter));
|
|
makeCopy(copyIds[1], translatePoint({ b: 200 }, polygonCenter));
|
|
makeCopy(copyIds[2], translatePoint({ a: 200 }, polygonCenter));
|
|
});
|
|
|
|
it("Call 'simplify' on each object", () => {
|
|
getPolygonStats(originalObjectId).then((originalStats) => {
|
|
originalArea = originalStats.area;
|
|
expect(originalStats.area).to.be.greaterThan(0);
|
|
|
|
// Simplify each copy with different distance parameters
|
|
copyIds.forEach((copyId, index) => {
|
|
simplifyAction({ objectId: copyId, distance: distances[index] });
|
|
cy.get(`#cvat_canvas_shape_${resultIds[index]}`).should('exist');
|
|
});
|
|
|
|
// Collect stats
|
|
const allStats = [];
|
|
cy.wrap(resultIds).each((id) => {
|
|
getPolygonStats(id).then((stats) => {
|
|
allStats.push(stats);
|
|
});
|
|
});
|
|
cy.then(() => {
|
|
const [stats1, stats2, stats3] = allStats;
|
|
logSimplifyStats('Aggressive simplification', stats1, distances[0]);
|
|
logSimplifyStats('Middle simplification', stats2, distances[1]);
|
|
logSimplifyStats('High-distance simplification', stats3, distances[2]);
|
|
|
|
return checkAreaProgression(originalArea, originalObjectId, allStats);
|
|
});
|
|
});
|
|
});
|
|
|
|
it("Call 'simplify' on all shapes as a bulk action", () => {
|
|
const distance = distances[2];
|
|
const allStats = [];
|
|
|
|
getPolygonStats(originalObjectId).then((originalStats) => {
|
|
originalArea = originalStats.area;
|
|
expect(originalStats.area).to.be.greaterThan(0);
|
|
simplifyBulkAction(distance);
|
|
cy.wrap(resultIds).each((id) => {
|
|
getPolygonStats(id).then((stats) => {
|
|
allStats.push(stats);
|
|
});
|
|
});
|
|
cy.then(() => {
|
|
allStats.forEach((stats, objectId) => {
|
|
logSimplifyStats('Aggressive simplification', stats, distance, objectId);
|
|
});
|
|
return checkAreasEqual(resultIds[0], allStats);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|