Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test for interpolation issues after deleting keyframes #9031

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
"run",
"--headless",
"--browser",
"chrome"
"chrome",
],
"outputCapture": "std",
"console": "internalConsole"
Expand Down
179 changes: 179 additions & 0 deletions tests/cypress/e2e/issues_prs2/issue_8952_interpolation_impossible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright (C) 2025 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

/// <reference types="cypress" />

const taskName = '5frames';
const labelName = 'label';
const issueId = 'xxxx';
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 dirToArchive = imagesFolder;

const [fX, fY] = [30, 30];
const [w, h] = [34, 23];
const createRectangleShape2Points = {
points: 'By 2 Points',
type: 'Track',
labelName,
firstX: fX,
firstY: fY,
secondX: (fX + w),
secondY: (fY + h),
};

function translateShape(shape, delta, axis) {
if (axis === 'x') {
return {
...shape,
firstX: shape.firstX + delta,
secondX: shape.secondX + delta,
};
}
if (axis === 'y') {
return {
...shape,
firstY: shape.firstY + delta,
secondY: shape.secondY + delta,
};
}
return null;
}

function shapeToPayload(shape, frame, shapeType) {
return {
frame,
type: shapeType,
points: [shape.firstX, shape.firstY, shape.secondX, shape.secondY],
};
}

function makeTrack(shapePayloads, frame0, trackLabel) {
return {
shapes: shapePayloads,
frame: frame0,
labelName: trackLabel,
objectType: 'track',
};
}

context('Create any track, check if track works correctly after deleting some frames', () => {
const precision = 0.01; // db server precision is 2 digits
const stash = {};
function storeShape(num) {
cy.get('.cvat_canvas_shape').invoke('attr', 'x').then((x) => {
cy.get('.cvat_canvas_shape').invoke('attr', 'y').then((y) => {
stash[num] = { x: parseFloat(x), y: parseFloat(y) };
});
});
}
function compareShape(num) {
const { x, y } = stash[num];
cy.get('.cvat_canvas_shape').invoke('attr', 'x').then((xVal) => {
expect(parseFloat(xVal)).to.be.closeTo(x, precision);
});
cy.get('.cvat_canvas_shape').invoke('attr', 'y').then((yVal) => {
expect(parseFloat(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(dirToArchive, archivePath);
cy.createTaskFromArchive(taskName, labelName, 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 wrap = (shape, frame) => shapeToPayload(shape, frame, 'rectangle');
const shape0 = createRectangleShape2Points;
const shape1 = translateShape(shape0, delta, 'x');
const shape2 = translateShape(shape1, delta, 'y'); // TODO: fix coords, rect flies away
const track = makeTrack([
wrap(shape0, 0),
wrap(shape1, 2),
wrap(shape2, 4),
], 0, labelName);
cy.headlessCreateObjects([track], jobID);
});
});

beforeEach(() => {
// Restore all frames by patching deleted_frames to []
cy.intercept('PATCH', '/api/jobs/**/data/meta**').as('patchMeta');
cy.headlessRestoreAllFrames(jobID);
cy.wait('@patchMeta');

// Get job meta updates from the server and reload page to bring changes to UI
cy.intercept('GET', '/api/jobs/**/data/meta**').as('getMeta');
cy.reload();
cy.wait('@getMeta');

cy.saveJob();
cy.clickFirstFrame();
});
it('Delete interpolated frames 0, 2, 4. Error should not appear', () => {
cy.on('uncaught:exception', (err) => {
const expectedMsg = 'No one left position or right position was found. Interpolation impossible. Client ID:';
expect(err.message).to.include(expectedMsg); // Exclude familiar error
throw err;
});

// Delete frames 0, 2, 4. Watch out for errors
cy.clickFirstFrame();
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();

// This might pop up after deleting or saving (on firefox e.g.)
cy.get('.ant-notification-notice-error').should('not.exist');

// Reopening a task with bad metadata might throw an exception that we can catch
cy.goToTaskList();
cy.openTaskJob(taskName);
cy.get('.ant-notification-notice-error').should('not.exist');
});
it('Change track positions on frames 2 and 4. Delete frame. Confirm same shape positions', () => {
cy.goToNextFrame(1);
cy.goToNextFrame(2);
cy.clickDeleteFrameAnnotationView();
cy.clickSaveAnnotationView();
cy.checkFrameNum(3);
storeShape(3);
cy.goToPreviousFrame(1);
storeShape(1);
cy.reload({ forceReload: false }).then(() => {
cy.goToNextFrame(1).then(() => compareShape(1));
cy.goToNextFrame(3).then(() => compareShape(3));
});
});
});
});
56 changes: 56 additions & 0 deletions tests/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,32 @@ Cypress.Commands.add('headlessCreateObjects', (objects, jobID) => {
});
});

Cypress.Commands.add('headlessRemoveAnnotations', (jobID) => {
cy.window().then(async ($win) => {
const job = (await $win.cvat.jobs.get({ jobID }))[0];
await job.annotations.clear({ reload: true });
await job.annotations.save();
return cy.wrap();
});
});

Cypress.Commands.add('headlessRestoreAllFrames', (jobID) => {
cy.window().then(async ($win) => {
await $win.cvat.server.request(`/api/jobs/${jobID}/data/meta`, {
method: 'PATCH',
data: { deleted_frames: [] },
});
});
});

Cypress.Commands.add('headlessGetJobMeta', (jobID) => {
cy.window().then(async ($win) => {
await $win.cvat.server.request(`/api/jobs/${jobID}/data/meta`, {
method: 'GET',
});
});
});

Cypress.Commands.add('headlessCreateTask', (taskSpec, dataSpec, extras) => {
cy.window().then(async ($win) => {
const task = new $win.cvat.classes.Task({
Expand Down Expand Up @@ -1767,6 +1793,28 @@ Cypress.Commands.overwrite('reload', (orig, options) => {
cy.closeModalUnsupportedPlatform();
});

Cypress.Commands.add('createTaskFromArchive', (taskName, labelName, archiveName, projectName = null) => {
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
cy.get('[id="name"]').clear();
cy.get('[id="name"]').type(taskName);
if (projectName) {
cy.get('.cvat-project-search-field').first().within(() => {
cy.get('[type="search"]').type(projectName);
});
}
cy.get('.cvat-constructor-viewer-new-item').click();
cy.get('[placeholder="Label name"]').type(labelName);
cy.contains('button', 'Continue').click();
// Archive name is relative to cypress/fixtures
cy.get('input[type="file"]').attachFile(archiveName, { subjectType: 'drag-n-drop' });
cy.contains('button', 'Submit & Continue').click();
cy.get('.cvat-notification-create-task-success').should('exist').within(() => {
cy.get('.anticon-close').click();
});
cy.get('.cvat-notification-create-task-fail').should('not.exist');
});

Cypress.Commands.add('clickDeleteFrameAnnotationView', () => {
cy.get('.cvat-player-delete-frame').click();
cy.get('.cvat-modal-delete-frame').within(() => {
Expand All @@ -1778,3 +1826,11 @@ Cypress.Commands.add('clickSaveAnnotationView', () => {
cy.get('button').contains('Save').click();
cy.get('button').contains('Save').trigger('mouseout');
});

Cypress.Commands.add('clickUndo', () => {
cy.contains('.cvat-annotation-header-button', 'Undo').click();
});

Cypress.Commands.add('clickFirstFrame', () => {
cy.get('.cvat-player-first-button').click();
});
Loading