Added ability to delete frames (#10)

Co-authored-by: Dmitry Kalinin <dmitry.kalinin@intel.com>
Co-authored-by: Nikita Manovich <nikita.manovich@gmail.com>
Co-authored-by: Boris Sekachev <b.sekachev@yandex.ru>
main
Boris Sekachev 4 years ago committed by GitHub
parent ff30b776cf
commit f960da9117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -136,7 +136,7 @@ jobs:
-c 'coverage run -a manage.py test cvat/apps utils/cli -k 'tasks_id' -k 'lambda' -k 'share' && mv .coverage ${CONTAINER_COVERAGE_DATA_DIR}'
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml run cvat_ci /bin/bash \
-c 'cd cvat-data && npm ci && cd ../cvat-core && npm ci && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
-c 'cd cvat-data && npm ci --ignore-scripts && cd ../cvat-core && npm ci --ignore-scripts && npm run test && mv ./reports/coverage/lcov.info ${CONTAINER_COVERAGE_DATA_DIR} && chmod a+rwx ${CONTAINER_COVERAGE_DATA_DIR}/lcov.info'
- name: Uploading code coverage results as an artifact
if: github.ref == 'refs/heads/develop'
uses: actions/upload-artifact@v2

@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Advanced filtration and sorting for a list of tasks/projects/cloudstorages (<https://github.com/openvinotoolkit/cvat/pull/4403>)
- Project dataset importing via chunk uploads (<https://github.com/openvinotoolkit/cvat/pull/4485>)
- Support paginated list for job commits (<https://github.com/openvinotoolkit/cvat/pull/4482>)
- Added ability to delete frames from a job (<https://github.com/openvinotoolkit/cvat/pull/4194>)
### Changed
- Added missing geos dependency into Dockerfile (<https://github.com/openvinotoolkit/cvat/pull/4451>)

@ -30,6 +30,7 @@ RUN gem install coveralls-lcov
COPY utils ${HOME}/utils
COPY cvat-core ${HOME}/cvat-core
COPY cvat-data ${HOME}/cvat-data
COPY package*.json ${HOME}/
COPY tests ${HOME}/tests
COPY .coveragerc .

@ -23,7 +23,7 @@ Please provide as much information as possible, including:
- Information on known exploits
- A member of the Intel Product Security Team will review your e-mail and contact you to
collaborate on resolving the issue.
For more information on how Intel works to resolve security issues, see:
[Vulnerability handling guidelines](<https://www.intel.com/content/www/us/en/security-center/vulnerability-handling-guidelines.html>)

@ -62,8 +62,19 @@ Canvas itself handles:
}
interface Configuration {
displayAllText?: boolean;
undefinedAttrValue?: string;
smoothImage?: boolean;
autoborders?: boolean;
displayAllText?: boolean;
textFontSize?: number;
textPosition?: 'auto' | 'center';
textContent?: string;
undefinedAttrValue?: string;
showProjections?: boolean;
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
creationOpacity?: number;
CSSImageFilter?: string;
}
interface DrawData {

@ -1,169 +0,0 @@
{
"name": "cvat-canvas",
"version": "2.13.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas",
"version": "2.13.2",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
"polylabel": "^1.1.0",
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
}
},
"node_modules/@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"node_modules/polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"dependencies": {
"tinyqueue": "^2.0.3"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.draw.js": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg.draw.js/-/svg.draw.js-2.0.4.tgz",
"integrity": "sha512-NMbecB0vg11AP76B0aLfI2cX7g9WurPM8x3yKxuJ9feM1vkI1GVjWZZjWpo3mkEzB1UJ8pKngaPaUCIOGi8uUA==",
"dependencies": {
"svg.js": "2.x.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
},
"dependencies": {
"@types/polylabel": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.0.5.tgz",
"integrity": "sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w=="
},
"polylabel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
"integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
"requires": {
"tinyqueue": "^2.0.3"
}
},
"svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"requires": {
"svg.js": "^2.0.1"
}
},
"svg.draw.js": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/svg.draw.js/-/svg.draw.js-2.0.4.tgz",
"integrity": "sha512-NMbecB0vg11AP76B0aLfI2cX7g9WurPM8x3yKxuJ9feM1vkI1GVjWZZjWpo3mkEzB1UJ8pKngaPaUCIOGi8uUA==",
"requires": {
"svg.js": "2.x.x"
}
},
"svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"requires": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"dependencies": {
"svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"requires": {
"svg.js": "^2.2.5"
}
}
}
},
"svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"requires": {
"svg.js": "^2.6.5"
}
},
"tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}
}
}

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

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -237,6 +237,10 @@ polyline.cvat_canvas_shape_splitting {
-ms-interpolation-mode: nearest-neighbor; /* IE8+ */
}
.cvat_canvas_removed_image {
filter: saturate(0) brightness(1.2) contrast(0.75) !important;
}
#cvat_canvas_wrapper {
width: calc(100% - 10px);
height: calc(100% - 10px);

@ -65,6 +65,7 @@ export interface Configuration {
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
creationOpacity?: number;
CSSImageFilter?: string;
}
export interface DrawData {
@ -171,6 +172,7 @@ export enum Mode {
export interface CanvasModel {
readonly imageBitmap: boolean;
readonly imageIsDeleted: boolean;
readonly image: Image | null;
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
readonly objects: any[];
@ -230,6 +232,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
imageID: number | null;
imageOffset: number;
imageSize: Size;
imageIsDeleted: boolean;
focusData: FocusData;
gridSize: Size;
left: number;
@ -277,6 +280,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0,
width: 0,
},
imageIsDeleted: false,
focusData: {
clientID: 0,
padding: 0,
@ -406,7 +410,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
}
if (frameData.number === this.data.imageID && !this.data.configuration.forceFrameUpdate) {
if (frameData.number === this.data.imageID &&
frameData.deleted === this.data.imageIsDeleted &&
!this.data.configuration.forceFrameUpdate
) {
this.data.zLayer = zLayer;
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
@ -431,6 +438,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
};
this.data.image = data;
this.data.imageIsDeleted = frameData.deleted;
if (this.data.imageIsDeleted) {
this.data.angle = 0;
}
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.zLayer = zLayer;
this.data.objects = objectStates;
@ -476,7 +487,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public rotate(rotationAngle: number): void {
if (this.data.angle !== rotationAngle) {
if (this.data.angle !== rotationAngle && !this.data.imageIsDeleted) {
this.data.angle = (360 + Math.floor(rotationAngle / 90) * 90) % 360;
this.fit();
}
@ -695,6 +706,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.creationOpacity = configuration.creationOpacity;
}
if (typeof configuration.CSSImageFilter === 'string') {
this.data.configuration.CSSImageFilter = configuration.CSSImageFilter;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
}
@ -753,6 +768,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.imageBitmap;
}
public get imageIsDeleted(): boolean {
return this.data.imageIsDeleted;
}
public get image(): Image | null {
return this.data.image;
}

@ -515,7 +515,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
private transformCanvas(): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.content, this.bitmap, this.attachmentBoard]) {
for (const obj of [
this.background, this.grid, this.content, this.bitmap, this.attachmentBoard,
]) {
obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`;
}
@ -1009,11 +1011,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas = window.document.createElement('div');
const loadingCircle: SVGCircleElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'circle');
const gridDefs: SVGDefsElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'defs');
const gridRect: SVGRectElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'rect');
// Setup defs
const contentDefs = this.adoptedContent.defs();
this.issueRegionPattern_1 = contentDefs
.pattern(consts.BASE_PATTERN_SIZE, consts.BASE_PATTERN_SIZE, (add): void => {
add.line(0, 0, 0, 10).stroke('red');
@ -1241,6 +1245,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
if (typeof configuration.CSSImageFilter === 'string') {
this.background.style.filter = configuration.CSSImageFilter;
}
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
@ -1282,6 +1290,28 @@ export class CanvasViewImpl implements CanvasView, Listener {
ctx.drawImage(image.imageData, 0, 0);
}
}
if (model.imageIsDeleted) {
let { width, height } = this.background;
if (image.imageData instanceof ImageData) {
width = image.imageData.width;
height = image.imageData.height;
}
this.background.classList.add('cvat_canvas_removed_image');
const canvasContext = this.background.getContext('2d');
const fontSize = width / 10;
canvasContext.font = `bold ${fontSize}px serif`;
canvasContext.textAlign = 'center';
canvasContext.lineWidth = fontSize / 20;
canvasContext.strokeStyle = 'white';
canvasContext.strokeText('IMAGE REMOVED', width / 2, height / 2);
canvasContext.fillStyle = 'black';
canvasContext.fillText('IMAGE REMOVED', width / 2, height / 2);
} else if (this.background.classList.contains('cvat_canvas_removed_image')) {
this.background.classList.remove('cvat_canvas_removed_image');
}
this.moveCanvas();
this.resizeCanvas();
this.transformCanvas();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -1,55 +0,0 @@
{
"name": "cvat-canvas3d",
"version": "0.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-canvas3d",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@types/three": "^0.125.3",
"camera-controls": "^1.25.3",
"three": "^0.126.1"
},
"devDependencies": {}
},
"node_modules/@types/three": {
"version": "0.125.3",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz",
"integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA=="
},
"node_modules/camera-controls": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.33.0.tgz",
"integrity": "sha512-QTXwz/XbLCPGf7l6u9cWKfR3WwKulnNAahfg+RE+dFOAQ40KKvwTIvBs3Q29kqntJlKvY79ZVsmPUEUA6LoF2A==",
"peerDependencies": {
"three": ">=0.126.1"
}
},
"node_modules/three": {
"version": "0.126.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.126.1.tgz",
"integrity": "sha512-eOEXnZeE1FDV0XgL1u08auIP13jxdN9LQBAEmlErYzMxtIIfuGIAZbijOyookALUhqVzVOx0Tywj6n192VM+nQ=="
}
},
"dependencies": {
"@types/three": {
"version": "0.125.3",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz",
"integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA=="
},
"camera-controls": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.33.0.tgz",
"integrity": "sha512-QTXwz/XbLCPGf7l6u9cWKfR3WwKulnNAahfg+RE+dFOAQ40KKvwTIvBs3Q29kqntJlKvY79ZVsmPUEUA6LoF2A==",
"requires": {}
},
"three": {
"version": "0.126.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.126.1.tgz",
"integrity": "sha512-eOEXnZeE1FDV0XgL1u08auIP13jxdN9LQBAEmlErYzMxtIIfuGIAZbijOyookALUhqVzVOx0Tywj6n192VM+nQ=="
}
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -112,5 +112,5 @@ class Canvas3dImpl implements Canvas3d {
}
export {
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CameraAction, ViewsDOM,
Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CameraAction, ViewsDOM, Mode as CanvasMode,
};

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,6 +12,7 @@ export interface Canvas3dController {
readonly selected: any;
readonly focused: FocusData;
readonly groupData: GroupData;
readonly imageIsDeleted: boolean;
mode: Mode;
group(groupData: GroupData): void;
}
@ -47,6 +48,10 @@ export class Canvas3dControllerImpl implements Canvas3dController {
return this.model.data.focusData;
}
public get imageIsDeleted(): any {
return this.model.imageIsDeleted;
}
public get groupData(): GroupData {
return this.model.groupData;
}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -101,6 +101,7 @@ export interface Canvas3dDataModel {
imageID: number | null;
imageOffset: number;
imageSize: Size;
imageIsDeleted: boolean;
drawData: DrawData;
mode: Mode;
objectUpdating: boolean;
@ -116,6 +117,7 @@ export interface Canvas3dDataModel {
export interface Canvas3dModel {
mode: Mode;
data: Canvas3dDataModel;
readonly imageIsDeleted: boolean;
readonly groupData: GroupData;
setup(frameData: any, objectStates: any[]): void;
isAbleToChangeFrame(): boolean;
@ -153,6 +155,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
height: 0,
width: 0,
},
imageIsDeleted: false,
drawData: {
enabled: false,
initialState: null,
@ -187,7 +190,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
return;
}
if (frameData.number === this.data.imageID) {
if (frameData.number === this.data.imageID && frameData.deleted === this.data.imageIsDeleted) {
if (this.data.objectUpdating) {
return;
}
@ -213,7 +216,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
height: frameData.height as number,
width: frameData.width as number,
};
this.data.imageIsDeleted = frameData.deleted;
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.objects = objectStates;
@ -342,5 +345,9 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
return { ...this.data.groupData };
}
public get imageIsDeleted(): boolean {
return this.data.imageIsDeleted;
}
public destroy(): void {}
}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -782,12 +782,43 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.model.data.drawData.enabled = false;
}
this.views.perspective.renderer.dispose();
this.model.mode = Mode.BUSY;
if (!this.controller.imageIsDeleted) {
this.model.mode = Mode.BUSY;
}
this.action.loading = true;
const loader = new PCDLoader();
const objectURL = URL.createObjectURL(model.data.image.imageData);
this.clearScene();
loader.load(objectURL, this.addScene.bind(this));
if (this.controller.imageIsDeleted) {
this.render();
const [container] = window.document.getElementsByClassName('cvat-canvas-container');
const overlay = window.document.createElement('canvas');
overlay.classList.add('cvat_3d_canvas_deleted_overlay');
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.position = 'absolute';
overlay.style.top = '0px';
overlay.style.left = '0px';
container.appendChild(overlay);
const { clientWidth: width, clientHeight: height } = overlay;
overlay.width = width;
overlay.height = height;
const canvasContext = overlay.getContext('2d');
const fontSize = width / 10;
canvasContext.font = `bold ${fontSize}px serif`;
canvasContext.textAlign = 'center';
canvasContext.lineWidth = fontSize / 20;
canvasContext.strokeStyle = 'white';
canvasContext.strokeText('IMAGE REMOVED', width / 2, height / 2);
canvasContext.fillStyle = 'black';
canvasContext.fillText('IMAGE REMOVED', width / 2, height / 2);
} else {
loader.load(objectURL, this.addScene.bind(this));
const [overlay] = window.document.getElementsByClassName('cvat_3d_canvas_deleted_overlay');
if (overlay) {
overlay.remove();
}
}
URL.revokeObjectURL(objectURL);
this.dispatchEvent(new CustomEvent('canvas.setup'));
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {

File diff suppressed because it is too large Load Diff

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

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -36,12 +36,12 @@ class AnnotationHistory {
this._redo = [];
}
undo(count) {
async undo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._undo.pop();
if (action) {
action.undo();
await action.undo();
this._redo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
@ -52,12 +52,12 @@ class AnnotationHistory {
return affectedObjects;
}
redo(count) {
async redo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._redo.pop();
if (action) {
action.redo();
await action.redo();
this._undo.push(action);
affectedObjects.push(...action.clientIDs);
} else {

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -23,6 +23,7 @@
objectState.__internal = {
save: this.save.bind(this, frame, objectState),
delete: this.delete.bind(this),
context: this,
};
return objectState;
@ -834,6 +835,10 @@
let last = Number.MIN_SAFE_INTEGER;
for (const frame of frames) {
if (frame in this.frameMeta.deleted_frames) {
continue;
}
if (frame < first) {
first = frame;
}

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,6 +12,7 @@
const { Task, Job } = require('./session');
const { Loader } = require('./annotation-formats');
const { ScriptingError, DataError, ArgumentError } = require('./exceptions');
const { getDeletedFrames } = require('./frames');
const jobCache = new WeakMap();
const taskCache = new WeakMap();
@ -42,6 +43,7 @@
for (let i = startFrame; i <= stopFrame; i++) {
frameMeta[i] = await session.frames.get(i);
}
frameMeta.deleted_frames = await getDeletedFrames(sessionType, session.id);
const history = new AnnotationsHistory();
const collection = new Collection({
@ -51,16 +53,11 @@
stopFrame,
frameMeta,
});
// eslint-disable-next-line no-unsanitized/method
collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
cache.set(session, {
collection,
saver,
history,
});
cache.set(session, { collection, saver, history });
}
}
@ -300,7 +297,20 @@
return serverProxy.projects.importDataset(instance.id, format, file, updateStatusCallback);
}
function undoActions(session, count) {
function getHistory(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history;
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
async function undoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -313,7 +323,7 @@
);
}
function redoActions(session, count) {
async function redoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -386,6 +396,7 @@
undoActions,
redoActions,
freezeHistory,
getHistory,
clearActions,
getActions,
closeSession,

@ -280,8 +280,8 @@ function build() {
* @property {string} name Check if name contains this value
* @property {module:API.cvat.enums.ProjectStatus} status
* Check if status contains this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* @property {number} id Check if id equals this value
* @property {number} page Get specific page
* (default REST API returns 20 projects per request.
* In order to get more, it is need to specify next page)
* @property {string} owner Check if owner user contains this value
@ -336,11 +336,11 @@ function build() {
* Check if status contains this value
* @property {module:API.cvat.enums.TaskMode} mode
* Check if mode contains this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* @property {number} id Check if id equals this value
* @property {number} page Get specific page
* (default REST API returns 20 tasks per request.
* In order to get more, it is need to specify next page)
* @property {integer} projectId Check if project_id field contains this value
* @property {number} projectId Check if project_id field contains this value
* @property {string} owner Check if owner user contains this value
* @property {string} assignee Check if assigneed contains this value
* @property {string} search Combined search of contains among all fields
@ -371,8 +371,8 @@ function build() {
/**
* @typedef {Object} JobFilter
* Only one of fields is allowed simultaneously
* @property {integer} taskID filter all jobs of specific task
* @property {integer} jobID filter job with a specific id
* @property {number} taskID filter all jobs of specific task
* @property {number} jobID filter job with a specific id
* @global
*/
@ -775,8 +775,8 @@ function build() {
* @property {string} displayName Check if displayName contains this value
* @property {string} resource Check if resource name contains this value
* @property {module:API.cvat.enums.ProviderType} providerType Check if providerType equal this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* @property {number} id Check if id equals this value
* @property {number} page Get specific page
* (default REST API returns 20 clouds storages per request.
* In order to get more, it is need to specify next page)
* @property {string} owner Check if an owner name contains this value

@ -54,7 +54,7 @@
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -36,7 +36,7 @@ class Comment {
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Comment
* @readonly
* @instance

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -278,6 +278,8 @@
* @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object
* @property {string} REMOVED_FRAME Removed frame
* @property {string} RESTORED_FRAME Restored frame
* @readonly
*/
const HistoryActions = Object.freeze({
@ -298,6 +300,8 @@
GROUPED_OBJECTS: 'Grouped objects',
CREATED_OBJECTS: 'Created objects',
REMOVED_OBJECT: 'Removed object',
REMOVED_FRAME: 'Removed frame',
RESTORED_FRAME: 'Restored frame',
});
/**

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -67,7 +67,7 @@
jobID: {
/**
* @name jobID
* @type {integer}
* @type {number}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
@ -77,7 +77,7 @@
taskID: {
/**
* @name taskID
* @type {integer}
* @type {number}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
@ -87,7 +87,7 @@
projID: {
/**
* @name projID
* @type {integer}
* @type {number}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
@ -97,7 +97,7 @@
clientID: {
/**
* @name clientID
* @type {integer}
* @type {number}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
@ -117,7 +117,7 @@
line: {
/**
* @name line
* @type {integer}
* @type {number}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
@ -127,7 +127,7 @@
column: {
/**
* @name column
* @type {integer}
* @type {number}
* @memberof module:API.cvat.exceptions.Exception
* @readonly
* @instance
@ -235,7 +235,7 @@
class ServerError extends Exception {
/**
* @param {string} message - Exception message
* @param {(string|integer)} code - Response code
* @param {(string|number)} code - Response code
*/
constructor(message, code) {
super(message);
@ -245,7 +245,7 @@
Object.freeze({
/**
* @name code
* @type {(string|integer)}
* @type {(string|number)}
* @memberof module:API.cvat.exceptions.ServerError
* @readonly
* @instance

@ -22,12 +22,12 @@
width,
height,
name,
taskID,
jobID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
deleted,
has_related_context: hasRelatedContext,
}) {
Object.defineProperties(
@ -46,7 +46,7 @@
},
/**
* @name width
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
@ -57,7 +57,7 @@
},
/**
* @name height
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
@ -66,21 +66,9 @@
value: height,
writable: false,
},
/**
* task ID
* @name tid
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
tid: {
value: taskID,
writable: false,
},
/**
* @name jid
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
@ -91,7 +79,7 @@
},
/**
* @name number
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
@ -112,10 +100,26 @@
value: hasRelatedContext,
writable: false,
},
/**
* Start frame of the frame in the job
* @name startFrame
* @type {number}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
startFrame: {
value: startFrame,
writable: false,
},
/**
* Stop frame of the frame in the job
* @name stopFrame
* @type {number}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
stopFrame: {
value: stopFrame,
writable: false,
@ -124,6 +128,18 @@
value: decodeForward,
writable: false,
},
/**
* True if frame was deleted from the task data
* @name deleted
* @type {boolean}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
deleted: {
value: deleted,
writable: false,
},
}),
);
}
@ -170,18 +186,18 @@
return;
}
const { provider } = frameDataCache[this.tid];
const { chunkSize } = frameDataCache[this.tid];
const { provider } = frameDataCache[this.jid];
const { chunkSize } = frameDataCache[this.jid];
const start = parseInt(this.number / chunkSize, 10) * chunkSize;
const stop = Math.min(this.stopFrame, (parseInt(this.number / chunkSize, 10) + 1) * chunkSize - 1);
const chunkNumber = Math.floor(this.number / chunkSize);
const onDecodeAll = async (frameNumber) => {
if (
frameDataCache[this.tid].activeChunkRequest &&
chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber
frameDataCache[this.jid].activeChunkRequest &&
chunkNumber === frameDataCache[this.jid].activeChunkRequest.chunkNumber
) {
const callbackArray = frameDataCache[this.tid].activeChunkRequest.callbacks;
const callbackArray = frameDataCache[this.jid].activeChunkRequest.callbacks;
for (let i = callbackArray.length - 1; i >= 0; --i) {
if (callbackArray[i].frameNumber === frameNumber) {
const callback = callbackArray[i];
@ -190,30 +206,30 @@
}
}
if (callbackArray.length === 0) {
frameDataCache[this.tid].activeChunkRequest = null;
frameDataCache[this.jid].activeChunkRequest = null;
}
}
};
const rejectRequestAll = () => {
if (
frameDataCache[this.tid].activeChunkRequest &&
chunkNumber === frameDataCache[this.tid].activeChunkRequest.chunkNumber
frameDataCache[this.jid].activeChunkRequest &&
chunkNumber === frameDataCache[this.jid].activeChunkRequest.chunkNumber
) {
for (const r of frameDataCache[this.tid].activeChunkRequest.callbacks) {
for (const r of frameDataCache[this.jid].activeChunkRequest.callbacks) {
r.reject(r.frameNumber);
}
frameDataCache[this.tid].activeChunkRequest = null;
frameDataCache[this.jid].activeChunkRequest = null;
}
};
const makeActiveRequest = () => {
const taskDataCache = frameDataCache[this.tid];
const taskDataCache = frameDataCache[this.jid];
const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames
.getData(this.tid, this.jid, activeChunk.chunkNumber)
.getData(null, this.jid, activeChunk.chunkNumber)
.then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true;
frameDataCache[this.jid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) {
provider.requestDecodeBlock(
chunk,
@ -253,7 +269,7 @@
.then((frame) => {
if (frame === null) {
onServerRequest();
const activeRequest = frameDataCache[this.tid].activeChunkRequest;
const activeRequest = frameDataCache[this.jid].activeChunkRequest;
if (!provider.isChunkCached(start, stop)) {
if (
!activeRequest ||
@ -264,7 +280,7 @@
if (activeRequest && activeRequest.rejectRequestAll) {
activeRequest.rejectRequestAll();
}
frameDataCache[this.tid].activeChunkRequest = {
frameDataCache[this.jid].activeChunkRequest = {
request: null,
chunkNumber,
start,
@ -292,13 +308,13 @@
frameNumber: this.number,
});
} else {
if (frameDataCache[this.tid].nextChunkRequest) {
const { callbacks } = frameDataCache[this.tid].nextChunkRequest;
if (frameDataCache[this.jid].nextChunkRequest) {
const { callbacks } = frameDataCache[this.jid].nextChunkRequest;
for (const r of callbacks) {
r.reject(r.frameNumber);
}
}
frameDataCache[this.tid].nextChunkRequest = {
frameDataCache[this.jid].nextChunkRequest = {
request: null,
chunkNumber,
start,
@ -336,8 +352,8 @@
const nextStart = nextChunkNumber * chunkSize;
const nextStop = Math.min(this.stopFrame, (nextChunkNumber + 1) * chunkSize - 1);
if (!provider.isChunkCached(nextStart, nextStop)) {
if (!frameDataCache[this.tid].activeChunkRequest) {
frameDataCache[this.tid].activeChunkRequest = {
if (!frameDataCache[this.jid].activeChunkRequest) {
frameDataCache[this.jid].activeChunkRequest = {
request: null,
chunkNumber: nextChunkNumber,
start: nextStart,
@ -368,8 +384,8 @@
});
};
function getFrameMeta(taskID, frame) {
const { meta, mode } = frameDataCache[taskID];
function getFrameMeta(jobID, frame) {
const { meta, mode, startFrame } = frameDataCache[jobID];
let size = null;
if (mode === 'interpolation') {
[size] = meta.frames;
@ -377,7 +393,7 @@
if (frame >= meta.size) {
throw new ArgumentError(`Meta information about frame ${frame} can't be received from the server`);
} else {
size = meta.frames[frame];
size = meta.frames[frame - startFrame];
}
} else {
throw new DataError(`Invalid mode is specified ${mode}`);
@ -386,7 +402,7 @@
}
class FrameBuffer {
constructor(size, chunkSize, stopFrame, taskID, jobID) {
constructor(size, chunkSize, stopFrame, jobID) {
this._size = size;
this._buffer = {};
this._contextImage = {};
@ -394,7 +410,6 @@
this._chunkSize = chunkSize;
this._stopFrame = stopFrame;
this._activeFillBufferRequest = false;
this._taskID = taskID;
this._jobID = jobID;
}
@ -428,15 +443,15 @@
};
for (const frame of this._requestedChunks[chunkIdx].requestedFrames.entries()) {
const requestedFrame = frame[1];
const frameMeta = getFrameMeta(this._taskID, requestedFrame);
const frameMeta = getFrameMeta(this._jobID, requestedFrame);
const frameData = new FrameData({
...frameMeta,
taskID: this._taskID,
jobID: this._jobID,
frameNumber: requestedFrame,
startFrame: frameDataCache[this._taskID].startFrame,
stopFrame: frameDataCache[this._taskID].stopFrame,
startFrame: frameDataCache[this._jobID].startFrame,
stopFrame: frameDataCache[this._jobID].stopFrame,
decodeForward: false,
deleted: requestedFrame in frameDataCache[this._jobID].meta,
});
frameData
@ -546,7 +561,7 @@
}
}
async require(frameNumber, taskID, jobID, fillBuffer, frameStep) {
async require(frameNumber, jobID, fillBuffer, frameStep) {
for (const frame in this._buffer) {
if (+frame < frameNumber || +frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame];
@ -554,15 +569,15 @@
}
this._required = frameNumber;
const frameMeta = getFrameMeta(taskID, frameNumber);
const frameMeta = getFrameMeta(jobID, frameNumber);
let frame = new FrameData({
...frameMeta,
taskID,
jobID,
frameNumber,
startFrame: frameDataCache[taskID].startFrame,
stopFrame: frameDataCache[taskID].stopFrame,
startFrame: frameDataCache[jobID].startFrame,
stopFrame: frameDataCache[jobID].stopFrame,
decodeForward: !fillBuffer,
deleted: frameNumber in frameDataCache[jobID].meta.deleted_frames,
});
if (frameNumber in this._buffer) {
@ -636,13 +651,13 @@
});
}
async function getContextImage(taskID, jobID, frame) {
if (frameDataCache[taskID].frameBuffer.isContextImageAvailable(frame)) {
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
async function getContextImage(jobID, frame) {
if (frameDataCache[jobID].frameBuffer.isContextImageAvailable(frame)) {
return frameDataCache[jobID].frameBuffer.getContextImage(frame);
}
const response = getImageContext(jobID, frame);
frameDataCache[taskID].frameBuffer.addContextImage(frame, response);
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
frameDataCache[jobID].frameBuffer.addContextImage(frame, response);
return frameDataCache[jobID].frameBuffer.getContextImage(frame);
}
async function getPreview(taskID = null, jobID = null) {
@ -669,7 +684,6 @@
}
async function getFrame(
taskID,
jobID,
chunkSize,
chunkType,
@ -681,10 +695,10 @@
step,
dimension,
) {
if (!(taskID in frameDataCache)) {
if (!(jobID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE;
const meta = await serverProxy.frames.getMeta(taskID);
const meta = await serverProxy.frames.getMeta('job', jobID);
meta.deleted_frames = Object.fromEntries(meta.deleted_frames.map((_frame) => [_frame, true]));
const mean = meta.frames.reduce((a, b) => a + b.width * b.height, 0) / meta.frames.length;
const stdDev = Math.sqrt(
meta.frames.map((x) => (x.width * x.height - mean) ** 2).reduce((a, b) => a + b) /
@ -694,7 +708,7 @@
// limit of decoded frames cache by 2GB
const decodedBlocksCacheSize = Math.floor(2147483648 / (mean + stdDev) / 4 / chunkSize) || 1;
frameDataCache[taskID] = {
frameDataCache[jobID] = {
meta,
chunkSize,
mode,
@ -712,23 +726,93 @@
Math.min(180, decodedBlocksCacheSize * chunkSize),
chunkSize,
stopFrame,
taskID,
jobID,
),
decodedBlocksCacheSize,
activeChunkRequest: null,
nextChunkRequest: null,
};
const frameMeta = getFrameMeta(taskID, frame);
const frameMeta = getFrameMeta(jobID, frame);
// actual only for video chunks
frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height);
frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height);
}
return frameDataCache[jobID].frameBuffer.require(frame, jobID, isPlaying, step);
}
async function getDeletedFrames(sessionType, id) {
if (sessionType === 'job') {
const { meta } = frameDataCache[id];
return meta.deleted_frames;
}
if (sessionType === 'task') {
const meta = await serverProxy.frames.getMeta('job', id);
meta.deleted_frames = Object.fromEntries(meta.deleted_frames.map((_frame) => [_frame, true]));
return meta;
}
throw Exception('getDeletedFrames is not implemented for tasks');
}
function deleteFrame(jobID, frame) {
const { meta } = frameDataCache[jobID];
meta.deleted_frames[frame] = true;
}
function restoreFrame(jobID, frame) {
const { meta } = frameDataCache[jobID];
if (frame in meta.deleted_frames) {
delete meta.deleted_frames[frame];
}
}
async function patchMeta(jobID) {
const { meta } = frameDataCache[jobID];
const newMeta = await serverProxy.frames.saveMeta('job', jobID, {
deleted_frames: Object.keys(meta.deleted_frames),
});
const prevDeletedFrames = meta.deleted_frames;
// it is important do not overwrite the object, it is why we working on keys in two loops below
for (const frame of Object.keys(prevDeletedFrames)) {
delete prevDeletedFrames[frame];
}
for (const frame of newMeta.deleted_frames) {
prevDeletedFrames[frame] = true;
}
frameDataCache[jobID].meta = newMeta;
frameDataCache[jobID].meta.deleted_frames = prevDeletedFrames;
}
async function findNotDeletedFrame(jobID, frameFrom, frameTo, offset) {
let meta;
if (!frameDataCache[jobID]) {
meta = await serverProxy.frames.getMeta('job', jobID);
} else {
meta = frameDataCache[jobID].meta;
}
const sign = Math.sign(frameTo - frameFrom);
const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo;
const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1;
let framesCounter = 0;
let lastUndeletedFrame = null;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
if (!(frame in meta.deleted_frames)) {
lastUndeletedFrame = frame;
framesCounter++;
if (framesCounter === offset) {
return lastUndeletedFrame;
}
}
}
return frameDataCache[taskID].frameBuffer.require(frame, taskID, jobID, isPlaying, step);
return lastUndeletedFrame;
}
function getRanges(taskID) {
if (!(taskID in frameDataCache)) {
function getRanges(jobID) {
if (!(jobID in frameDataCache)) {
return {
decoded: [],
buffered: [],
@ -736,24 +820,29 @@
}
return {
decoded: frameDataCache[taskID].provider.cachedFrames,
buffered: frameDataCache[taskID].frameBuffer.cachedFrames(),
decoded: frameDataCache[jobID].provider.cachedFrames,
buffered: frameDataCache[jobID].frameBuffer.cachedFrames(),
};
}
function clear(taskID) {
if (taskID in frameDataCache) {
frameDataCache[taskID].frameBuffer.clear();
delete frameDataCache[taskID];
function clear(jobID) {
if (jobID in frameDataCache) {
frameDataCache[jobID].frameBuffer.clear();
delete frameDataCache[jobID];
}
}
module.exports = {
FrameData,
getFrame,
getDeletedFrames,
deleteFrame,
restoreFrame,
patchMeta,
getRanges,
getPreview,
clear,
findNotDeletedFrame,
getContextImage,
};
})();

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -49,7 +49,7 @@ class Issue {
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
@ -101,7 +101,7 @@ class Issue {
},
/**
* @name frame
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -43,7 +43,7 @@
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Attribute
* @readonly
* @instance
@ -160,7 +160,7 @@
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance

@ -86,7 +86,7 @@ const { Source } = require('./enums');
frame: {
/**
* @name frame
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
@ -126,7 +126,7 @@ const { Source } = require('./enums');
clientID: {
/**
* @name clientID
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
@ -136,7 +136,7 @@ const { Source } = require('./enums');
serverID: {
/**
* @name serverID
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -63,7 +63,7 @@
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance

@ -1234,12 +1234,27 @@
return response;
}
async function getMeta(tid) {
async function getMeta(session, jid) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/data/meta`, {
response = await Axios.get(`${backendAPI}/${session}s/${jid}/data/meta`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function saveMeta(session, jid, meta) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.patch(`${backendAPI}/${session}s/${jid}/data/meta`, meta, {
proxy: config.proxy,
});
} catch (errorData) {
@ -1932,6 +1947,7 @@
value: Object.freeze({
getData,
getMeta,
saveMeta,
getPreview,
getImageContext,
}),

@ -7,10 +7,21 @@
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
const {
getFrame, getRanges, getPreview, clear: clearFrames, getContextImage,
getFrame,
deleteFrame,
restoreFrame,
getRanges,
getPreview,
clear: clearFrames,
findNotDeletedFrame,
getContextImage,
patchMeta,
getDeletedFrames,
} = require('./frames');
const { ArgumentError, DataError } = require('./exceptions');
const { JobStage, JobState } = require('./enums');
const {
JobStage, JobState, HistoryActions,
} = require('./enums');
const { Label } = require('./labels');
const User = require('./user');
const Issue = require('./issue');
@ -170,6 +181,26 @@
);
return result;
},
async delete(frame) {
await PluginRegistry.apiWrapper.call(
this,
prototype.frames.delete,
frame,
);
},
async restore(frame) {
await PluginRegistry.apiWrapper.call(
this,
prototype.frames.restore,
frame,
);
},
async save() {
await PluginRegistry.apiWrapper.call(
this,
prototype.frames.save,
);
},
async ranges() {
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.ranges);
return result;
@ -178,6 +209,16 @@
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview);
return result;
},
async search(filters, frameFrom, frameTo) {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.frames.search,
filters,
frameFrom,
frameTo,
);
return result;
},
async contextImage(frameId) {
const result = await PluginRegistry.apiWrapper.call(
this,
@ -368,7 +409,7 @@
* <b> If you have double quotes in your query string,
* please escape them using back slash: \" </b>
* @method get
* @param {integer} frame get objects from the frame
* @param {number} frame get objects from the frame
* @param {boolean} allTracks show all tracks
* even if they are outside and not keyframe
* @param {any[]} [filters = []]
@ -386,9 +427,9 @@
* @method search
* @memberof Session.annotations
* @param {ObjectFilter} [filter = []] filter
* @param {integer} from lower bound of a search
* @param {integer} to upper bound of a search
* @returns {integer|null} a frame that contains objects according to the filter
* @param {number} from lower bound of a search
* @param {number} to upper bound of a search
* @returns {number|null} a frame that contains objects according to the filter
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
@ -398,9 +439,9 @@
* Find the nearest empty frame without any annotations
* @method searchEmpty
* @memberof Session.annotations
* @param {integer} from lower bound of a search
* @param {integer} to upper bound of a search
* @returns {integer|null} a frame that contains objects according to the filter
* @param {number} from lower bound of a search
* @param {number} to upper bound of a search
* @returns {number|null} a empty frame according boundaries
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
@ -444,7 +485,7 @@
* @method split
* @memberof Session.annotations
* @param {module:API.cvat.classes.ObjectState} objectState
* @param {integer} frame
* @param {number} frame
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
@ -457,7 +498,7 @@
* @memberof Session.annotations
* @param {module:API.cvat.classes.ObjectState[]} objectStates
* @param {boolean} reset pass "true" to reset group value (set it to 0)
* @returns {integer} an ID of created group
* @returns {number} an ID of created group
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
@ -517,7 +558,7 @@
* Get frame by its number
* @method get
* @memberof Session.frames
* @param {integer} frame number of frame which you want to get
* @param {number} frame number of frame which you want to get
* @returns {module:API.cvat.classes.FrameData}
* @instance
* @async
@ -526,6 +567,51 @@
* @throws {module:API.cvat.exceptions.DataError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* @typedef {Object} FrameSearchFilters
* @property {boolean} notDeleted if true will search for non-deleted frames
* @property {number} offset defines frame step during search
/**
* Find frame that match the condition
* @method search
* @memberof Session.frames
* @param {FrameSearchFilters} filters filters to search frame for
* @param {number} from lower bound of a search
* @param {number} to upper bound of a search
* @returns {number|null} a non-deleted frame according boundaries
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Delete frame from the job
* @method delete
* @memberof Session.frames
* @param {number} frame number of frame which you want to delete
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Restore frame from the job
* @method delete
* @memberof Session.frames
* @param {number} frame number of frame which you want to restore
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Save any changes in frames if some of them were deleted/restored
* @method save
* @memberof Session.frames
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Get the first frame of a task for preview
* @method preview
@ -754,7 +840,7 @@
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
@ -840,7 +926,7 @@
},
/**
* @name startFrame
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
@ -850,7 +936,7 @@
},
/**
* @name stopFrame
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
@ -860,7 +946,7 @@
},
/**
* @name projectId
* @type {integer|null}
* @type {number|null}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
@ -870,7 +956,7 @@
},
/**
* @name taskId
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
@ -900,7 +986,7 @@
},
/**
* @name dataChunkSize
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
@ -985,8 +1071,12 @@
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
delete: Object.getPrototypeOf(this).frames.delete.bind(this),
restore: Object.getPrototypeOf(this).frames.restore.bind(this),
save: Object.getPrototypeOf(this).frames.save.bind(this),
ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
search: Object.getPrototypeOf(this).frames.search.bind(this),
contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this),
};
@ -1107,6 +1197,7 @@
data_chunk_size: undefined,
data_compressed_chunk_type: undefined,
data_original_chunk_type: undefined,
deleted_frames: undefined,
use_zip_chunks: undefined,
use_cache: undefined,
copy_data: undefined,
@ -1176,7 +1267,7 @@
Object.freeze({
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
@ -1203,7 +1294,7 @@
},
/**
* @name projectId
* @type {integer|null}
* @type {number|null}
* @memberof module:API.cvat.classes.Task
* @instance
*/
@ -1230,7 +1321,7 @@
},
/**
* @name size
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
@ -1339,7 +1430,7 @@
},
/**
* @name overlap
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
@ -1355,7 +1446,7 @@
},
/**
* @name segmentSize
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
@ -1371,7 +1462,7 @@
},
/**
* @name imageQuality
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
@ -1562,7 +1653,7 @@
/**
* The first frame of a video to annotation
* @name startFrame
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
@ -1579,7 +1670,7 @@
/**
* The last frame of a video to annotation
* @name stopFrame
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
@ -1698,9 +1789,13 @@
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
delete: Object.getPrototypeOf(this).frames.delete.bind(this),
restore: Object.getPrototypeOf(this).frames.restore.bind(this),
save: Object.getPrototypeOf(this).frames.save.bind(this),
ranges: Object.getPrototypeOf(this).frames.ranges.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
contextImage: Object.getPrototypeOf(this).frames.contextImage.bind(this),
search: Object.getPrototypeOf(this).frames.search.bind(this),
};
this.logger = {
@ -1821,6 +1916,7 @@
clearActions,
getActions,
closeSession,
getHistory,
} = require('./annotations');
buildDuplicatedAPI(Job.prototype);
@ -1866,7 +1962,6 @@
}
const frameData = await getFrame(
this.taskId,
this.id,
this.dataChunkSize,
this.dataChunkType,
@ -1881,8 +1976,62 @@
return frameData;
};
// must be called with task/job context
async function deleteFrameWrapper(jobID, frame) {
const history = getHistory(this);
const redo = async () => {
deleteFrame(jobID, frame);
};
await redo();
history.do(HistoryActions.REMOVED_FRAME, async () => {
restoreFrame(jobID, frame);
}, redo, [], frame);
}
async function restoreFrameWrapper(jobID, frame) {
const history = getHistory(this);
const redo = async () => {
restoreFrame(jobID, frame);
};
await redo();
history.do(HistoryActions.RESTORED_FRAME, async () => {
deleteFrame(jobID, frame);
}, redo, [], frame);
}
Job.prototype.frames.delete.implementation = async function (frame) {
if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`);
}
if (frame < this.startFrame || frame > this.stopFrame) {
throw new Error('The frame is out of the job');
}
await deleteFrameWrapper.call(this, this.id, frame);
};
Job.prototype.frames.restore.implementation = async function (frame) {
if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`);
}
if (frame < this.startFrame || frame > this.stopFrame) {
throw new Error('The frame is out of the job');
}
await restoreFrameWrapper.call(this, this.id, frame);
};
Job.prototype.frames.save.implementation = async function () {
const result = await patchMeta(this.id);
return result;
};
Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.taskId);
const rangesData = await getRanges(this.id);
return rangesData;
};
@ -1895,6 +2044,33 @@
return frameData;
};
Job.prototype.frames.contextImage.implementation = async function (frameId) {
const result = await getContextImage(this.id, frameId);
return result;
};
Job.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) {
if (typeof filters !== 'object') {
throw new ArgumentError('Filters should be an object');
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError('The start and end frames both must be an integer');
}
if (frameFrom < this.startFrame || frameFrom > this.stopFrame) {
throw new ArgumentError('The start frame is out of the job');
}
if (frameTo < this.startFrame || frameTo > this.stopFrame) {
throw new ArgumentError('The stop frame is out of the job');
}
if (filters.notDeleted) {
return findNotDeletedFrame(this.id, frameFrom, frameTo, filters.offset || 1);
}
return null;
};
// TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters)) {
@ -1910,6 +2086,11 @@
}
const annotationsData = await getAnnotations(this, frame, allTracks, filters);
const deletedFrames = await getDeletedFrames('job', this.id);
if (frame in deletedFrames) {
return [];
}
return annotationsData;
};
@ -2018,13 +2199,13 @@
return result;
};
Job.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
Job.prototype.actions.undo.implementation = async function (count) {
const result = await undoActions(this, count);
return result;
};
Job.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
Job.prototype.actions.redo.implementation = async function (count) {
const result = await redoActions(this, count);
return result;
};
@ -2081,20 +2262,15 @@
return result;
};
Job.prototype.frames.contextImage.implementation = async function (frameId) {
const result = await getContextImage(this.taskId, this.id, frameId);
return result;
};
Job.prototype.close.implementation = function closeTask() {
clearFrames(this.taskId);
clearFrames(this.id);
closeSession(this);
return this;
};
Task.prototype.close.implementation = function closeTask() {
clearFrames(this.id);
for (const job of this.jobs) {
clearFrames(job.id);
closeSession(job);
}
@ -2203,15 +2379,16 @@
throw new ArgumentError(`The frame with number ${frame} is out of the task`);
}
const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0];
const result = await getFrame(
this.id,
null,
job.id,
this.dataChunkSize,
this.dataChunkType,
this.mode,
frame,
0,
this.size - 1,
job.startFrame,
job.stopFrame,
isPlaying,
step,
);
@ -2219,7 +2396,15 @@
};
Task.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.id);
const rangesData = {
decoded: [],
buffered: [],
};
for (const job of this.jobs) {
const { decoded, buffered } = await getRanges(job.id);
rangesData.decoded.push(decoded);
rangesData.buffered.push(buffered);
}
return rangesData;
};
@ -2232,6 +2417,76 @@
return frameData;
};
Task.prototype.frames.delete.implementation = async function (frame) {
if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`);
}
if (frame < 0 || frame >= this.size) {
throw new Error('The frame is out of the task');
}
const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0];
if (job) {
await deleteFrameWrapper.call(this, job.id, frame);
}
};
Task.prototype.frames.restore.implementation = async function (frame) {
if (!Number.isInteger(frame)) {
throw new Error(`Frame must be an integer. Got: "${frame}"`);
}
if (frame < 0 || frame >= this.size) {
throw new Error('The frame is out of the task');
}
const job = this.jobs.filter((_job) => _job.startFrame <= frame && _job.stopFrame >= frame)[0];
if (job) {
await restoreFrameWrapper.call(this, job.id, frame);
}
};
Task.prototype.frames.save.implementation = async function () {
return Promise.all(this.jobs.map((job) => patchMeta(job.id)));
};
Task.prototype.frames.search.implementation = async function (filters, frameFrom, frameTo) {
if (typeof filters !== 'object') {
throw new ArgumentError('Filters should be an object');
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError('The start and end frames both must be an integer');
}
if (frameFrom < 0 || frameFrom > this.size) {
throw new ArgumentError('The start frame is out of the task');
}
if (frameTo < 0 || frameTo > this.size) {
throw new ArgumentError('The stop frame is out of the task');
}
const jobs = this.jobs.filter((_job) => (
(frameFrom >= _job.startFrame && frameFrom <= _job.stopFrame) ||
(frameTo >= _job.startFrame && frameTo <= _job.stopFrame) ||
(frameFrom < _job.startFrame && frameTo > _job.stopFrame)
));
if (filters.notDeleted) {
for (const job of jobs) {
const result = await findNotDeletedFrame(
job.id, Math.max(frameFrom, job.startFrame), Math.min(frameTo, job.stopFrame), 1,
);
if (result !== null) return result;
}
}
return null;
};
// TODO: Check filter for annotations
Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof filter !== 'string')) {
@ -2247,6 +2502,11 @@
}
const result = await getAnnotations(this, frame, allTracks, filters);
const deletedFrames = await getDeletedFrames('task', this.id);
if (frame in deletedFrames) {
return [];
}
return result;
};

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -37,7 +37,7 @@
id: {
/**
* @name id
* @type {integer}
* @type {number}
* @memberof module:API.cvat.classes.User
* @readonly
* @instance

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -43,6 +43,48 @@ describe('Feature: get frame meta', () => {
});
});
describe('Feature: delete/restore frame', () => {
test('delete frame from job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
await job.annotations.clear(true);
let frame = await job.frames.get(0);
expect(frame.deleted).toBe(false);
await job.frames.delete(0);
frame = await job.frames.get(0);
expect(frame.deleted).toBe(true);
});
test('restore frame from job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
await job.annotations.clear(true);
let frame = await job.frames.get(8);
expect(frame.deleted).toBe(true);
await job.frames.restore(8);
frame = await job.frames.get(8);
expect(frame.deleted).toBe(false);
});
test('delete frame from task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true);
let frame = await task.frames.get(1);
expect(frame.deleted).toBe(false);
await task.frames.delete(1);
frame = await task.frames.get(1);
expect(frame.deleted).toBe(true);
});
test('restore frame from task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
await task.annotations.clear(true);
let frame = await task.frames.get(7);
expect(frame.deleted).toBe(true);
await task.frames.restore(7);
frame = await task.frames.get(7);
expect(frame.deleted).toBe(false);
});
});
describe('Feature: get frame data', () => {
test('get frame data for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];

@ -2544,6 +2544,7 @@ const frameMetaDummyData = {
start_frame: 0,
stop_frame: 8,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1920,
@ -2590,6 +2591,7 @@ const frameMetaDummyData = {
start_frame: 0,
stop_frame: 74,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1920,
@ -2602,8 +2604,24 @@ const frameMetaDummyData = {
size: 5002,
image_quality: 50,
start_frame: 0,
stop_frame: 4999,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
4: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 4995,
stop_frame: 5001,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
@ -2618,6 +2636,7 @@ const frameMetaDummyData = {
start_frame: 0,
stop_frame: 8,
frame_filter: '',
deleted_frames: [7,8],
frames: [
{
width: 1920,
@ -2662,8 +2681,9 @@ const frameMetaDummyData = {
size: 5002,
image_quality: 50,
start_frame: 0,
stop_frame: 5001,
stop_frame: 499,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
@ -2672,12 +2692,163 @@ const frameMetaDummyData = {
],
},
102: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 495,
stop_frame: 994,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
103: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 990,
stop_frame: 1489,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
104: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 1485,
stop_frame: 1984,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
105: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 1980,
stop_frame: 2479,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
106: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 2475,
stop_frame: 2974,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
107: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 2970,
stop_frame: 3469,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
108: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 3465,
stop_frame: 3964,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
109: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 3960,
stop_frame: 4459,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
110: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 4455,
stop_frame: 4954,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
111: {
chunk_size: 36,
size: 5002,
image_quality: 50,
start_frame: 4950,
stop_frame: 5001,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1888,
height: 1408,
},
],
},
112: {
chunk_size: 36,
size: 1,
image_quality: 50,
start_frame: 0,
stop_frame: 0,
frame_filter: '',
deleted_frames: [],
frames: [
{
width: 1920,

@ -285,8 +285,33 @@ class ServerProxy {
return 'DUMMY_IMAGE';
}
async function getMeta(tid) {
return JSON.parse(JSON.stringify(frameMetaDummyData[tid]));
async function getMeta(session, jid) {
if (session !== 'job') {
throw new Error('not implemented test');
}
return JSON.parse(JSON.stringify(frameMetaDummyData[jid]));
}
async function saveMeta(session, jid, meta) {
if (session !== 'job') {
throw new Error('not implemented test');
}
const object = frameMetaDummyData[jid];
for (const prop in meta) {
if (
Object.prototype.hasOwnProperty.call(meta, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
) {
if (prop === 'labels') {
object[prop] = meta[prop].filter((label) => !label.deleted);
} else {
object[prop] = meta[prop];
}
}
}
return getMeta(jid);
}
async function getAnnotations(session, id) {
@ -442,6 +467,7 @@ class ServerProxy {
value: Object.freeze({
getData,
getMeta,
saveMeta,
getPreview,
}),
writable: false,

@ -1,221 +0,0 @@
{
"name": "cvat-data",
"version": "1.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-data",
"version": "1.0.2",
"license": "MIT",
"dependencies": {
"async-mutex": "^0.3.2",
"jszip": "3.7.1"
},
"devDependencies": {}
},
"node_modules/async-mutex": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
"integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==",
"dependencies": {
"tslib": "^2.3.1"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"node_modules/jszip": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
"integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}
},
"dependencies": {
"async-mutex": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
"integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==",
"requires": {
"tslib": "^2.3.1"
}
},
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"jszip": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
"integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
}
},
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"requires": {
"immediate": "~3.0.5"
}
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}
}
}

File diff suppressed because it is too large Load Diff

@ -7,7 +7,10 @@ import {
} from 'redux';
import { ThunkAction } from 'utils/redux';
import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
import { RectDrawingMethod, CuboidDrawingMethod, Canvas } from 'cvat-canvas-wrapper';
import { CanvasMode as Canvas3DMode } from 'cvat-canvas3d-wrapper';
import {
RectDrawingMethod, CuboidDrawingMethod, Canvas, CanvasMode as Canvas2DMode,
} from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper';
import logger, { LogType } from 'cvat-logger';
import { getCVATStore } from 'cvat-store';
@ -197,6 +200,12 @@ export enum AnnotationActionTypes {
GET_CONTEXT_IMAGE_SUCCESS = 'GET_CONTEXT_IMAGE_SUCCESS',
GET_CONTEXT_IMAGE_FAILED = 'GET_CONTEXT_IMAGE_FAILED',
SWITCH_NAVIGATION_BLOCKED = 'SWITCH_NAVIGATION_BLOCKED',
DELETE_FRAME = 'DELETE_FRAME',
DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS',
DELETE_FRAME_FAILED = 'DELETE_FRAME_FAILED',
RESTORE_FRAME = 'RESTORE_FRAME',
RESTORE_FRAME_SUCCESS = 'RESTORE_FRAME_SUCCESS',
RESTORE_FRAME_FAILED = 'RESTORE_FRAME_FAILED',
}
export function saveLogsAsync(): ThunkAction {
@ -822,6 +831,7 @@ export function undoActionAsync(sessionInstance: any, frame: number): ThunkActio
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, showAllInterpolationTracks, filters);
const frameData = await sessionInstance.frames.get(frame);
const [minZ, maxZ] = computeZRange(states);
await undoLog.close();
@ -832,12 +842,13 @@ export function undoActionAsync(sessionInstance: any, frame: number): ThunkActio
states,
minZ,
maxZ,
frameData,
},
});
const undoOnFrame = undo[1];
if (frame !== undoOnFrame) {
dispatch(changeFrameAsync(undoOnFrame));
if (frame !== undoOnFrame || ['Removed frame', 'Restored frame'].includes(undo[0])) {
dispatch(changeFrameAsync(undoOnFrame, undefined, undefined, true));
}
} catch (error) {
dispatch({
@ -872,8 +883,8 @@ export function redoActionAsync(sessionInstance: any, frame: number): ThunkActio
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
const frameData = await sessionInstance.frames.get(frame);
await redoLog.close();
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: {
@ -881,12 +892,14 @@ export function redoActionAsync(sessionInstance: any, frame: number): ThunkActio
states,
minZ,
maxZ,
frameData,
},
});
const redoOnFrame = redo[1];
if (frame !== redoOnFrame) {
dispatch(changeFrameAsync(redoOnFrame));
if (frame !== redoOnFrame || ['Removed frame', 'Restored frame'].includes(redo[0])) {
dispatch(changeFrameAsync(redoOnFrame, undefined, undefined, true));
}
} catch (error) {
dispatch({
@ -976,7 +989,9 @@ export function closeJob(): ThunkAction {
};
}
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): ThunkAction {
export function getJobAsync(
tid: number, jid: number, initialFrame: number | null, initialFilters: object[],
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
try {
const state = getState();
@ -984,6 +999,7 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
const {
settings: {
workspace: { showAllInterpolationTracks },
player: { showDeletedFrames },
},
} = state;
@ -1017,7 +1033,16 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
[job] = await cvat.jobs.get({ jobID: jid });
}
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
// opening correct first frame according to setup
let frameNumber;
if (initialFrame === null && !showDeletedFrames) {
frameNumber = (await job.frames.search(
{ notDeleted: true }, job.startFrame, job.stopFrame,
)) || job.startFrame;
} else {
frameNumber = Math.max(Math.min(job.stopFrame, initialFrame || 0), job.startFrame);
}
const frameData = await job.frames.get(frameNumber);
// call first getting of frame data before rendering interface
// to load and decode first chunk
@ -1115,6 +1140,11 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
try {
const saveJobEvent = await sessionInstance.logger.log(LogType.saveJob, {}, true);
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
payload: { status: 'Saving frames' },
});
await sessionInstance.frames.save();
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
@ -1387,10 +1417,29 @@ export function changeGroupColorAsync(group: number, color: string): ThunkAction
}
export function searchAnnotationsAsync(sessionInstance: any, frameFrom: number, frameTo: number): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const frame = await sessionInstance.annotations.search(filters, frameFrom, frameTo);
const {
settings: {
player: { showDeletedFrames },
},
annotation: {
annotations: { filters },
},
} = getState();
const sign = Math.sign(frameTo - frameFrom);
let frame = await sessionInstance.annotations.search(filters, frameFrom, frameTo);
while (frame !== null) {
const isDeleted = (await sessionInstance.frames.get(frame)).deleted;
if (!isDeleted || showDeletedFrames) {
break;
} else if (sign > 0 ? frame < frameTo : frame > frameTo) {
frame = await sessionInstance.annotations.search(filters, frame + sign, frameTo);
} else {
frame = null;
}
}
if (frame !== null) {
dispatch(changeFrameAsync(frame));
}
@ -1406,9 +1455,26 @@ export function searchAnnotationsAsync(sessionInstance: any, frameFrom: number,
}
export function searchEmptyFrameAsync(sessionInstance: any, frameFrom: number, frameTo: number): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
try {
const frame = await sessionInstance.annotations.searchEmpty(frameFrom, frameTo);
const {
settings: {
player: { showDeletedFrames },
},
} = getState();
const sign = Math.sign(frameTo - frameFrom);
let frame = await sessionInstance.annotations.searchEmpty(frameFrom, frameTo);
while (frame !== null) {
const isDeleted = (await sessionInstance.frames.get(frame)).deleted;
if (!isDeleted || showDeletedFrames) {
break;
} else if (sign > 0 ? frame < frameTo : frame > frameTo) {
frame = await sessionInstance.annotations.searchEmpty(frame + sign, frameTo);
} else {
frame = null;
}
}
if (frame !== null) {
dispatch(changeFrameAsync(frame));
}
@ -1671,3 +1737,98 @@ export function switchNavigationBlocked(navigationBlocked: boolean): AnyAction {
},
};
}
export function deleteFrameAsync(frame: number): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const {
annotation: {
annotations: { filters },
job: {
instance: jobInstance,
},
canvas: {
instance: canvasInstance,
},
},
settings: {
player: { showDeletedFrames },
workspace: { showAllInterpolationTracks },
},
} = state;
try {
dispatch({ type: AnnotationActionTypes.DELETE_FRAME });
if (canvasInstance &&
canvasInstance.mode() !== Canvas2DMode.IDLE &&
canvasInstance.mode() !== Canvas3DMode.IDLE) {
canvasInstance.cancel();
}
await jobInstance.frames.delete(frame);
dispatch({
type: AnnotationActionTypes.DELETE_FRAME_SUCCESS,
payload: {
data: await jobInstance.frames.get(frame),
history: await jobInstance.actions.get(),
states: await jobInstance.annotations.get(frame, showAllInterpolationTracks, filters),
},
});
if (!showDeletedFrames) {
let notDeletedFrame = await jobInstance.frames.search(
{ notDeleted: true }, frame, jobInstance.stopFrame,
);
if (notDeletedFrame === null && jobInstance.startFrame !== frame) {
notDeletedFrame = await jobInstance.frames.search(
{ notDeleted: true }, frame, jobInstance.startFrame,
);
}
if (notDeletedFrame !== null) {
dispatch(changeFrameAsync(notDeletedFrame));
}
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.DELETE_FRAME_FAILED,
payload: { error },
});
}
};
}
export function restoreFrameAsync(frame: number): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const {
annotation: {
job: {
instance: jobInstance,
},
annotations: { filters },
},
settings: {
workspace: { showAllInterpolationTracks },
},
} = state;
try {
dispatch({ type: AnnotationActionTypes.RESTORE_FRAME });
await jobInstance.frames.restore(frame);
dispatch({
type: AnnotationActionTypes.RESTORE_FRAME_SUCCESS,
payload: {
data: await jobInstance.frames.get(frame),
history: await jobInstance.actions.get(),
states: await jobInstance.annotations.get(frame, showAllInterpolationTracks, filters),
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.RESTORE_FRAME_FAILED,
payload: { error },
});
}
};
}

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -41,6 +41,7 @@ export enum SettingsActionTypes {
SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG',
SET_SETTINGS = 'SET_SETTINGS',
SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE',
SWITCH_SHOWING_DELETED_FRAMES = 'SWITCH_SHOWING_DELETED_FRAMES',
}
export function changeShapesOpacity(opacity: number): AnyAction {
@ -340,3 +341,12 @@ export function setSettings(settings: Partial<SettingsState>): AnyAction {
},
};
}
export function switchShowingDeletedFrames(showDeletedFrames: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_SHOWING_DELETED_FRAMES,
payload: {
showDeletedFrames,
},
};
}

@ -0,0 +1,12 @@
<svg width="1em" height="1em" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13739_233)">
<path d="M17.8932 3.56123C18.0449 3.2359 17.4359 3.34796 17.8932 3.56123L28.9631 8.72321C29.4205 8.93648 29.1148 8.39788 28.9631 8.72321L27.7406 11.345L30.3624 12.5676L31.7208 9.65447C32.3202 8.36905 31.7624 6.83663 30.477 6.23723L18.8245 0.803565C17.5391 0.204164 16.0067 0.761919 15.4073 2.04734L14.0488 4.96047L16.6707 6.18305L17.8932 3.56123ZM35.0234 14.7411L9.38783 2.787C8.7433 2.48646 7.97976 2.76436 7.67921 3.40889L7.13584 4.57414C7.06113 4.73437 7.13109 4.92659 7.29131 5.0013C17.7574 9.88172 32.5151 16.7633 34.6748 17.7704C34.835 17.8451 35.0272 17.7752 35.1019 17.6149L35.6453 16.4497C35.9458 15.8052 35.6679 15.0416 35.0234 14.7411Z" fill="black" fill-opacity="0.65"/>
<path d="M5.31897 17L6.3114 31.6168C6.37569 32.9869 7.50882 34.0677 8.87891 34.0677H27.1199C28.494 34.0677 29.6231 32.9909 29.6874 31.6168L30.6797 17H5.31897ZM26.8105 31.1748H9.18823L8.21594 17H27.7828L26.8105 31.1748Z" fill="black" fill-opacity="0.65"/>
<path d="M15.3406 14.272C12.8752 11.0314 9.20844 9.3291 5.49785 9.3098L5.49159 7.25027C5.49159 7.0411 5.25381 6.92525 5.09738 7.05397L1.09269 10.3042C0.967544 10.4039 0.970673 10.597 1.09269 10.7L5.10051 13.9534C5.26007 14.0821 5.49785 13.9663 5.49472 13.7571V11.7008C5.89831 11.704 6.30504 11.7297 6.70864 11.7812C8.0258 11.9486 9.27727 12.3669 10.4317 13.0266C11.6238 13.7088 12.6594 14.6259 13.5103 15.7394C14.3582 16.856 14.9714 18.111 15.3281 19.4658H17.7157C17.3279 17.6312 16.5409 15.8536 15.3406 14.272Z" fill="black" fill-opacity="0.65"/>
</g>
<defs>
<clipPath id="clip0_13739_233">
<rect width="36" height="36" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -246,11 +246,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
contrastLevel !== prevProps.contrastLevel ||
saturationLevel !== prevProps.saturationLevel
) {
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
backgroundElement.style.filter = filter;
}
canvasInstance.configure({
CSSImageFilter:
`brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`,
});
}
if (
@ -685,7 +684,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
if (frameData !== null && canvasInstance) {
canvasInstance.setup(
frameData,
annotations.filter((e) => e.objectType !== ObjectType.TAG),
frameData.deleted ? [] : annotations.filter((e) => e.objectType !== ObjectType.TAG),
curZLayer,
);
}
@ -720,13 +719,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
canvasInstance.grid(gridSize, gridSize);
// Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
backgroundElement.style.filter = filter;
}
canvasInstance.configure({
CSSImageFilter:
`brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`,
});
const canvasWrapperElement = window.document
.getElementsByClassName('cvat-canvas-container')
.item(0) as HTMLElement | null;

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -21,6 +21,7 @@ interface Props {
activeControl: ActiveControl;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
frameIsDeleted: boolean;
rotateFrame(rotation: Rotation): void;
selectIssuePosition(enabled: boolean): void;
@ -28,9 +29,11 @@ interface Props {
export default function ControlsSideBarComponent(props: Props): JSX.Element {
const {
canvasInstance, activeControl, normalizedKeyMap, keyMap, rotateFrame, selectIssuePosition,
canvasInstance, activeControl, normalizedKeyMap, keyMap, rotateFrame, selectIssuePosition, frameIsDeleted,
} = props;
const controlsDisabled = frameIsDeleted;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
@ -42,26 +45,32 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
OPEN_REVIEW_ISSUE: keyMap.OPEN_REVIEW_ISSUE,
};
const handlers = {
let handlers: any = {
CANCEL: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeControl !== ActiveControl.CURSOR) {
canvasInstance.cancel();
}
},
OPEN_REVIEW_ISSUE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeControl === ActiveControl.OPEN_ISSUE) {
canvasInstance.selectRegion(false);
selectIssuePosition(false);
} else {
canvasInstance.cancel();
canvasInstance.selectRegion(true);
selectIssuePosition(true);
}
},
};
if (!controlsDisabled) {
handlers = {
...handlers,
OPEN_REVIEW_ISSUE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeControl === ActiveControl.OPEN_ISSUE) {
canvasInstance.selectRegion(false);
selectIssuePosition(false);
} else {
canvasInstance.cancel();
canvasInstance.selectRegion(true);
selectIssuePosition(true);
}
},
};
}
return (
<Layout.Sider className='cvat-canvas-controls-sidebar' theme='light' width={44}>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
@ -87,6 +96,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance}
activeControl={activeControl}
selectIssuePosition={selectIssuePosition}
disabled={controlsDisabled}
/>
</Layout.Sider>
);

@ -13,33 +13,40 @@ import CVATTooltip from 'components/common/cvat-tooltip';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
disabled: boolean;
selectIssuePosition(enabled: boolean): void;
}
function CreateIssueControl(props: Props): JSX.Element {
const { activeControl, canvasInstance, selectIssuePosition } = props;
const {
activeControl, canvasInstance, selectIssuePosition, disabled,
} = props;
return (
<CVATTooltip title='Open an issue' placement='right'>
<Icon
component={RectangleIcon}
className={
activeControl === ActiveControl.OPEN_ISSUE ?
'cvat-issue-control cvat-active-canvas-control' :
'cvat-issue-control'
}
onClick={(): void => {
if (activeControl === ActiveControl.OPEN_ISSUE) {
canvasInstance.selectRegion(false);
selectIssuePosition(false);
} else {
canvasInstance.cancel();
canvasInstance.selectRegion(true);
selectIssuePosition(true);
disabled ? (
<Icon component={RectangleIcon} className='cvat-issue-control cvat-disabled-canvas-control' />
) : (
<CVATTooltip title='Open an issue' placement='right'>
<Icon
component={RectangleIcon}
className={
activeControl === ActiveControl.OPEN_ISSUE ?
'cvat-issue-control cvat-active-canvas-control' :
'cvat-issue-control'
}
}}
/>
</CVATTooltip>
onClick={(): void => {
if (activeControl === ActiveControl.OPEN_ISSUE) {
canvasInstance.selectRegion(false);
selectIssuePosition(false);
} else {
canvasInstance.cancel();
canvasInstance.selectRegion(true);
selectIssuePosition(true);
}
}}
/>
</CVATTooltip>
)
);
}

@ -11,6 +11,12 @@
.cvat-issue-control {
font-size: 40px;
&.cvat-disabled-canvas-control {
&::after {
opacity: 0.3;
}
}
&::after {
content: '\FE56';
font-size: 32px;

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -34,6 +34,7 @@ interface Props {
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
labels: any[];
frameData: any;
mergeObjects(enabled: boolean): void;
groupObjects(enabled: boolean): void;
@ -80,8 +81,11 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
pasteShape,
resetGroup,
redrawShape,
frameData,
} = props;
const controlsDisabled = !labels.length || frameData.deleted;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
@ -111,7 +115,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
},
};
if (labels.length) {
if (!controlsDisabled) {
handlers = {
...handlers,
PASTE_SHAPE: (event: KeyboardEvent | undefined) => {
@ -226,34 +230,34 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
<ObservedDrawRectangleControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_RECTANGLE}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedDrawPolygonControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POLYGON}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedDrawPolylineControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POLYLINE}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedDrawPointsControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POINTS}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedDrawEllipseControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_ELLIPSE}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedDrawCuboidControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_CUBOID}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedSetupTagControl canvasInstance={canvasInstance} isDrawing={false} disabled={!labels.length} />
<ObservedSetupTagControl canvasInstance={canvasInstance} isDrawing={false} disabled={controlsDisabled} />
<hr />
@ -262,7 +266,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance}
activeControl={activeControl}
mergeObjects={mergeObjects}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedGroupControl
switchGroupShortcut={normalizedKeyMap.SWITCH_GROUP_MODE}
@ -270,14 +274,14 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance}
activeControl={activeControl}
groupObjects={groupObjects}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ObservedSplitControl
canvasInstance={canvasInstance}
switchSplitShortcut={normalizedKeyMap.SWITCH_SPLIT_MODE}
activeControl={activeControl}
splitTrack={splitTrack}
disabled={!labels.length}
disabled={controlsDisabled}
/>
<ExtraControlsControl />

@ -824,7 +824,9 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
}
public render(): JSX.Element {
const { isActivated, canvasInstance, labels } = this.props;
const {
isActivated, canvasInstance, labels, frameData,
} = this.props;
const { libraryInitialized, approxPolyAccuracy, mode } = this.state;
const dynamcPopoverPros = isActivated ?
{
@ -842,10 +844,10 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
},
} :
{
className: 'cvat-tools-control',
className: 'cvat-opencv-control',
};
return !labels.length ? (
return !labels.length || frameData.deleted ? (
<Icon className='cvat-opencv-control cvat-disabled-canvas-control' component={OpenCVIcon} />
) : (
<>

@ -63,6 +63,7 @@ interface StateToProps {
curZOrder: number;
defaultApproxPolyAccuracy: number;
toolsBlockerState: ToolsBlockerState;
frameIsDeleted: boolean;
}
interface DispatchToProps {
@ -78,29 +79,42 @@ const core = getCore();
const CustomPopover = withVisibilityHandling(Popover, 'tools-control');
function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state;
const { settings } = state;
const { number: frame } = annotation.player.frame;
const { instance: jobInstance } = annotation.job;
const { instance: canvasInstance, activeControl } = annotation.canvas;
const { models } = state;
const { interactors, detectors, trackers } = models;
const { toolsBlockerState } = state.settings.workspace;
const {
annotation: {
job: { instance: jobInstance, labels },
canvas: { instance: canvasInstance, activeControl },
player: {
frame: { number: frame, data: { deleted: frameIsDeleted } },
},
annotations: {
zLayer: { cur: curZOrder },
states,
},
drawing: { activeLabelID },
},
models: {
interactors, detectors, trackers,
},
settings: {
workspace: { toolsBlockerState, defaultApproxPolyAccuracy },
},
} = state;
return {
interactors,
detectors,
trackers,
isActivated: activeControl === ActiveControl.AI_TOOLS,
activeLabelID: annotation.drawing.activeLabelID,
labels: annotation.job.labels,
states: annotation.annotations.states,
activeLabelID,
labels,
states,
canvasInstance: canvasInstance as Canvas,
jobInstance,
frame,
curZOrder: annotation.annotations.zLayer.cur,
defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy,
curZOrder,
defaultApproxPolyAccuracy,
toolsBlockerState,
frameIsDeleted,
};
}
@ -1161,7 +1175,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
public render(): JSX.Element | null {
const {
interactors, detectors, trackers, isActivated, canvasInstance, labels,
interactors, detectors, trackers, isActivated, canvasInstance, labels, frameIsDeleted,
} = this.props;
const {
fetching, approxPolyAccuracy, pointsRecieved, mode, portals,
@ -1188,7 +1202,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
className: 'cvat-tools-control',
};
const showAnyContent = !!labels.length;
const showAnyContent = labels.length && !frameIsDeleted;
const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved;
const showDetectionContent = fetching && mode === 'detection';

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -179,14 +179,16 @@ button.cvat-predictor-button {
}
.cvat-player-filename-wrapper {
max-width: 300px;
max-width: $grid-unit-size * 30;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
word-break: break-all;
}
.cvat-player-frame-url-icon {
.cvat-player-frame-url-icon,
.cvat-player-delete-frame,
.cvat-player-restore-frame {
opacity: 0.7;
color: $objects-bar-icons-color;
@ -199,6 +201,11 @@ button.cvat-predictor-button {
}
}
.cvat-player-delete-frame,
.cvat-player-restore-frame {
margin-left: $grid-unit-size * 2;
}
.cvat-player-frame-selector {
width: 5em;
padding-right: 5px;

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -40,6 +40,7 @@ interface StateToProps {
frameNumber: number;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
frameData: any;
}
interface DispatchToProps {
@ -53,7 +54,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
player: {
frame: { number: frameNumber },
frame: { number: frameNumber, data: frameData },
},
annotations: { states },
job: { instance: jobInstance, labels },
@ -70,6 +71,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameNumber,
keyMap,
normalizedKeyMap,
frameData,
};
}
@ -102,6 +104,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
onRememberObject,
createAnnotations,
keyMap,
frameData,
} = props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -110,6 +113,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
}
};
const controlsDisabled = !labels.length || frameData.deleted;
const defaultLabelID = labels.length ? labels[0].id : null;
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
@ -199,7 +203,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
},
};
return !labels.length ? (
return controlsDisabled ? (
<Layout.Sider {...siderProps}>
{/* eslint-disable-next-line */}
<span
@ -215,7 +219,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
</span>
<Row justify='center' className='labels-tag-annotation-sidebar-not-found-wrapper'>
<Col>
<Text strong>No labels are available.</Text>
<Text strong>Can&apos;t place tag on this frame.</Text>
</Col>
</Row>
</Layout.Sider>

@ -1,42 +1,54 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { Row, Col } from 'antd/lib/grid';
import { LinkOutlined } from '@ant-design/icons';
import Icon, { LinkOutlined, DeleteOutlined } from '@ant-design/icons';
import Slider from 'antd/lib/slider';
import InputNumber from 'antd/lib/input-number';
import Input from 'antd/lib/input';
import Text from 'antd/lib/typography/Text';
import { RestoreIcon } from 'icons';
import CVATTooltip from 'components/common/cvat-tooltip';
import { clamp } from 'utils/math';
import modal from 'antd/lib/modal';
interface Props {
startFrame: number;
stopFrame: number;
playing: boolean;
frameNumber: number;
frameFilename: string;
frameDeleted: boolean;
focusFrameInputShortcut: string;
inputFrameRef: React.RefObject<Input>;
onSliderChange(value: number): void;
onInputChange(value: number): void;
onURLIconClick(): void;
onDeleteFrame(): void;
onRestoreFrame(): void;
switchNavigationBlocked(blocked: boolean): void;
}
function PlayerNavigation(props: Props): JSX.Element {
const {
startFrame,
stopFrame,
playing,
frameNumber,
frameFilename,
frameDeleted,
focusFrameInputShortcut,
inputFrameRef,
onSliderChange,
onInputChange,
onURLIconClick,
onDeleteFrame,
onRestoreFrame,
switchNavigationBlocked,
} = props;
const [frameInputValue, setFrameInputValue] = useState<number>(frameNumber);
@ -47,6 +59,26 @@ function PlayerNavigation(props: Props): JSX.Element {
}
}, [frameNumber]);
const showDeleteFrameDialog = useCallback(() => {
if (!playing) {
switchNavigationBlocked(true);
modal.confirm({
title: `Do you want to delete frame #${frameNumber}?`,
content: 'The frame will not be visible in navigation and exported datasets, but it still can be restored with all the annotations.',
className: 'cvat-modal-delete-frame',
okText: 'Delete',
okType: 'danger',
onOk: () => {
switchNavigationBlocked(false);
onDeleteFrame();
},
afterClose: () => {
switchNavigationBlocked(false);
},
});
}
}, [playing, frameNumber]);
return (
<>
<Col className='cvat-player-controls'>
@ -71,6 +103,15 @@ function PlayerNavigation(props: Props): JSX.Element {
<CVATTooltip title='Create frame URL'>
<LinkOutlined className='cvat-player-frame-url-icon' onClick={onURLIconClick} />
</CVATTooltip>
{ (!frameDeleted) ? (
<CVATTooltip title='Delete the frame'>
<DeleteOutlined className='cvat-player-delete-frame' onClick={showDeleteFrameDialog} />
</CVATTooltip>
) : (
<CVATTooltip title='Restore the frame'>
<Icon className='cvat-player-restore-frame' onClick={onRestoreFrame} component={RestoreIcon} />
</CVATTooltip>
)}
</Col>
</Row>
</Col>

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -20,6 +20,7 @@ interface Props {
savingStatuses: string[];
frameNumber: number;
frameFilename: string;
frameDeleted: boolean;
inputFrameRef: React.RefObject<Input>;
startFrame: number;
stopFrame: number;
@ -64,6 +65,9 @@ interface Props {
onRedoClick(): void;
onFinishDraw(): void;
onSwitchToolsBlockerState(): void;
onDeleteFrame(): void;
onRestoreFrame(): void;
switchNavigationBlocked(blocked: boolean): void;
jobInstance: any;
}
@ -76,6 +80,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
playing,
frameNumber,
frameFilename,
frameDeleted,
inputFrameRef,
startFrame,
stopFrame,
@ -117,6 +122,9 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
onRedoClick,
onFinishDraw,
onSwitchToolsBlockerState,
onDeleteFrame,
onRestoreFrame,
switchNavigationBlocked,
jobInstance,
isTrainingActive,
} = props;
@ -165,13 +173,18 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
<PlayerNavigation
startFrame={startFrame}
stopFrame={stopFrame}
playing={playing}
frameNumber={frameNumber}
frameFilename={frameFilename}
frameDeleted={frameDeleted}
focusFrameInputShortcut={focusFrameInputShortcut}
inputFrameRef={inputFrameRef}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
onURLIconClick={onURLIconClick}
onDeleteFrame={onDeleteFrame}
onRestoreFrame={onRestoreFrame}
switchNavigationBlocked={switchNavigationBlocked}
/>
</Row>
</Col>

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { useHistory, useLocation } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
@ -169,7 +169,7 @@ function HeaderContainer(props: Props): JSX.Element {
const history = useHistory();
const location = useLocation();
function showAboutModal(): void {
const showAboutModal = useCallback((): void => {
Modal.info({
title: `${tool.name}`,
content: (
@ -222,7 +222,7 @@ function HeaderContainer(props: Props): JSX.Element {
},
},
});
}
}, [tool]);
const resetOrganization = (): void => {
localStorage.removeItem('currentOrganization');

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -25,6 +25,7 @@ interface Props {
resetZoom: boolean;
rotateAll: boolean;
smoothImage: boolean;
showDeletedFrames: boolean;
canvasBackgroundColor: string;
onChangeFrameStep(step: number): void;
onChangeFrameSpeed(speed: FrameSpeed): void;
@ -32,6 +33,7 @@ interface Props {
onSwitchRotateAll(rotateAll: boolean): void;
onChangeCanvasBackgroundColor(color: string): void;
onSwitchSmoothImage(enabled: boolean): void;
onSwitchShowingDeletedFrames(enabled: boolean): void;
}
export default function PlayerSettingsComponent(props: Props): JSX.Element {
@ -41,6 +43,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
resetZoom,
rotateAll,
smoothImage,
showDeletedFrames,
canvasBackgroundColor,
onChangeFrameStep,
onChangeFrameSpeed,
@ -48,6 +51,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
onSwitchRotateAll,
onSwitchSmoothImage,
onChangeCanvasBackgroundColor,
onSwitchShowingDeletedFrames,
} = props;
const minFrameStep = 2;
@ -199,6 +203,22 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
</Col>
</Row>
</Col>
<Col span={7} offset={5} className='cvat-workspace-settings-show-deleted'>
<Row>
<Checkbox
className='cvat-text-color'
checked={showDeletedFrames}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchShowingDeletedFrames(event.target.checked);
}}
>
Show deleted frames
</Checkbox>
</Row>
<Row>
<Text type='secondary'>You will be able to navigate and restore deleted frames</Text>
</Row>
</Col>
</Row>
</div>
);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -29,6 +29,7 @@
.cvat-workspace-settings-intelligent-polygon-cropping,
.cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-interpolated,
.cvat-workspace-settings-show-deleted,
.cvat-workspace-settings-approx-poly-threshold,
.cvat-workspace-settings-aam-zoom-margin,
.cvat-workspace-settings-text-settings {

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -107,20 +107,22 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element {
<Text type='secondary'> minutes </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-show-interpolated'>
<Col span={24}>
<Checkbox
className='cvat-text-color'
checked={showAllInterpolationTracks}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchShowingInterpolatedTracks(event.target.checked);
}}
>
Show all interpolation tracks
</Checkbox>
</Col>
<Col span={24}>
<Text type='secondary'> Show hidden interpolated objects in the side panel </Text>
<Row>
<Col span={12} className='cvat-workspace-settings-show-interpolated'>
<Row>
<Checkbox
className='cvat-text-color'
checked={showAllInterpolationTracks}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchShowingInterpolatedTracks(event.target.checked);
}}
>
Show all interpolation tracks
</Checkbox>
</Row>
<Row>
<Text type='secondary'> Show hidden interpolated objects in the side panel</Text>
</Row>
</Col>
</Row>
<Row className='cvat-workspace-settings-show-text-always'>

@ -31,7 +31,7 @@
.cvat-job-page-list-item {
width: 25%;
border-width: 4px;
border-width: $grid-unit-size * 0.5;
display: flex;
flex-direction: column;

@ -62,7 +62,7 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
const jobID = +params.jid;
const searchParams = new URLSearchParams(window.location.search);
const initialFilters: object[] = [];
let initialFrame = 0;
let initialFrame: number | null = null;
if (searchParams.has('frame')) {
const searchFrame = +(searchParams.get('frame') as string);

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -16,6 +16,7 @@ interface StateToProps {
activeControl: ActiveControl;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
frameIsDeleted: boolean;
}
interface DispatchToProps {
@ -27,6 +28,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: { instance: canvasInstance, activeControl },
player: { frame: { data: { deleted: frameIsDeleted } } },
},
settings: {
player: { rotateAll },
@ -40,6 +42,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
activeControl,
normalizedKeyMap,
keyMap,
frameIsDeleted,
};
}

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -26,6 +26,7 @@ interface StateToProps {
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
labels: any[];
frameData: any;
}
interface DispatchToProps {
@ -44,6 +45,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotation: {
canvas: { instance: canvasInstance, activeControl },
job: { labels },
player: {
frame: { data: frameData },
},
},
settings: {
player: { rotateAll },
@ -58,6 +62,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
labels,
normalizedKeyMap,
keyMap,
frameData,
};
}

@ -25,6 +25,9 @@ import {
showStatistics as showStatisticsAction,
switchPlay,
undoActionAsync,
deleteFrameAsync,
restoreFrameAsync,
switchNavigationBlocked as switchNavigationBlockedAction,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { Canvas } from 'cvat-canvas-wrapper';
@ -44,6 +47,7 @@ import { switchToolsBlockerState } from 'actions/settings-actions';
interface StateToProps {
jobInstance: any;
frameIsDeleted: boolean;
frameNumber: number;
frameFilename: string;
frameStep: number;
@ -59,6 +63,7 @@ interface StateToProps {
autoSave: boolean;
autoSaveInterval: number;
toolsBlockerState: ToolsBlockerState;
showDeletedFrames: boolean;
workspace: Workspace;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
@ -83,6 +88,9 @@ interface DispatchToProps {
changeWorkspace(workspace: Workspace): void;
switchPredictor(predictorEnabled: boolean): void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void;
deleteFrame(frame: number): void;
restoreFrame(frame: number): void;
switchNavigationBlocked(blocked: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -91,7 +99,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
player: {
playing,
frame: {
filename: frameFilename, number: frameNumber, delay: frameDelay, fetching: frameFetching,
data: { deleted: frameIsDeleted },
filename: frameFilename,
number: frameNumber,
delay: frameDelay,
fetching: frameFetching,
},
},
annotations: {
@ -104,14 +116,19 @@ function mapStateToProps(state: CombinedState): StateToProps {
predictor,
},
settings: {
player: { frameSpeed, frameStep },
workspace: { autoSave, autoSaveInterval, toolsBlockerState },
player: { frameSpeed, frameStep, showDeletedFrames },
workspace: {
autoSave,
autoSaveInterval,
toolsBlockerState,
},
},
shortcuts: { keyMap, normalizedKeyMap },
plugins: { list },
} = state;
return {
frameIsDeleted,
frameStep,
frameSpeed,
frameDelay,
@ -128,6 +145,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
autoSave,
autoSaveInterval,
toolsBlockerState,
showDeletedFrames,
workspace,
keyMap,
normalizedKeyMap,
@ -185,6 +203,15 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void {
dispatch(switchToolsBlockerState(toolsBlockerState));
},
deleteFrame(frame: number): void {
dispatch(deleteFrameAsync(frame));
},
restoreFrame(frame: number): void {
dispatch(restoreFrameAsync(frame));
},
switchNavigationBlocked(blocked: boolean): void {
dispatch(switchNavigationBlockedAction(blocked));
},
};
}
@ -272,13 +299,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
private showStatistics = (): void => {
const { jobInstance, showStatistics } = this.props;
showStatistics(jobInstance);
};
private showFilters = (): void => {
const { jobInstance, showFilters } = this.props;
showFilters(jobInstance);
};
@ -294,13 +319,14 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}
};
private onFirstFrame = (): void => {
private onFirstFrame = async (): Promise<void> => {
const {
frameNumber, jobInstance, playing, onSwitchPlay,
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const newFrame = jobInstance.startFrame;
if (newFrame !== frameNumber) {
const newFrame = showDeletedFrames ? jobInstance.startFrame :
await jobInstance.frames.search({ notDeleted: true }, jobInstance.startFrame, frameNumber);
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
@ -308,13 +334,19 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}
};
private onBackward = (): void => {
private onBackward = async (): Promise<void> => {
const {
frameNumber, frameStep, jobInstance, playing, onSwitchPlay,
frameNumber, frameStep, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const newFrame = Math.max(jobInstance.startFrame, frameNumber - frameStep);
if (newFrame !== frameNumber) {
let newFrame = Math.max(jobInstance.startFrame, frameNumber - frameStep);
if (!showDeletedFrames) {
newFrame = await jobInstance.frames.search(
{ notDeleted: true, offset: frameStep }, frameNumber - 1, jobInstance.startFrame,
);
}
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
@ -322,15 +354,17 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}
};
private onPrevFrame = (): void => {
private onPrevFrame = async (): Promise<void> => {
const { prevButtonType } = this.state;
const {
frameNumber, jobInstance, playing, onSwitchPlay,
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const { startFrame } = jobInstance;
const newFrame = Math.max(jobInstance.startFrame, frameNumber - 1);
if (newFrame !== frameNumber) {
const frameFrom = Math.max(jobInstance.startFrame, frameNumber - 1);
const newFrame = showDeletedFrames ? frameFrom :
await jobInstance.frames.search({ notDeleted: true }, frameFrom, jobInstance.startFrame);
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
@ -338,22 +372,24 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (prevButtonType === 'regular') {
this.changeFrame(newFrame);
} else if (prevButtonType === 'filtered') {
this.searchAnnotations(frameNumber - 1, startFrame);
this.searchAnnotations(newFrame, startFrame);
} else {
this.searchEmptyFrame(frameNumber - 1, startFrame);
this.searchEmptyFrame(newFrame, startFrame);
}
}
};
private onNextFrame = (): void => {
private onNextFrame = async (): Promise<void> => {
const { nextButtonType } = this.state;
const {
frameNumber, jobInstance, playing, onSwitchPlay,
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const { stopFrame } = jobInstance;
const newFrame = Math.min(jobInstance.stopFrame, frameNumber + 1);
if (newFrame !== frameNumber) {
const frameFrom = Math.min(jobInstance.stopFrame, frameNumber + 1);
const newFrame = showDeletedFrames ? frameFrom :
await jobInstance.frames.search({ notDeleted: true }, frameFrom, jobInstance.stopFrame);
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
@ -361,20 +397,25 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (nextButtonType === 'regular') {
this.changeFrame(newFrame);
} else if (nextButtonType === 'filtered') {
this.searchAnnotations(frameNumber + 1, stopFrame);
this.searchAnnotations(newFrame, stopFrame);
} else {
this.searchEmptyFrame(frameNumber + 1, stopFrame);
this.searchEmptyFrame(newFrame, stopFrame);
}
}
};
private onForward = (): void => {
private onForward = async (): Promise<void> => {
const {
frameNumber, frameStep, jobInstance, playing, onSwitchPlay,
frameNumber, frameStep, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
let newFrame = Math.min(jobInstance.stopFrame, frameNumber + frameStep);
if (!showDeletedFrames) {
newFrame = await jobInstance.frames.search(
{ notDeleted: true, offset: frameStep }, frameNumber + 1, jobInstance.stopFrame,
);
}
const newFrame = Math.min(jobInstance.stopFrame, frameNumber + frameStep);
if (newFrame !== frameNumber) {
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
@ -382,13 +423,14 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}
};
private onLastFrame = (): void => {
private onLastFrame = async (): Promise<void> => {
const {
frameNumber, jobInstance, playing, onSwitchPlay,
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const newFrame = jobInstance.stopFrame;
if (newFrame !== frameNumber) {
const newFrame = showDeletedFrames ? jobInstance.stopFrame :
await jobInstance.frames.search({ notDeleted: true }, jobInstance.stopFrame, frameNumber);
if (newFrame !== frameNumber && frameNumber !== null) {
if (playing) {
onSwitchPlay(false);
}
@ -418,11 +460,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(value as number);
this.changeFrame(value);
};
private onChangePlayerInputValue = (value: number): void => {
const { onSwitchPlay, playing, frameNumber } = this.props;
const { frameNumber, onSwitchPlay, playing } = this.props;
if (value !== frameNumber) {
if (playing) {
onSwitchPlay(false);
@ -467,6 +509,16 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
copy(url);
};
private onDeleteFrame = (): void => {
const { deleteFrame, frameNumber } = this.props;
deleteFrame(frameNumber);
};
private onRestoreFrame = (): void => {
const { restoreFrame, frameNumber } = this.props;
restoreFrame(frameNumber);
};
private beforeUnloadCallback = (event: BeforeUnloadEvent): string | undefined => {
const { jobInstance, forceExit, setForceExitAnnotationFlag } = this.props;
if (jobInstance.annotations.hasUnsavedChanges() && !forceExit) {
@ -562,6 +614,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
jobInstance: { startFrame, stopFrame },
frameNumber,
frameFilename,
frameIsDeleted,
undoAction,
redoAction,
workspace,
@ -574,6 +627,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
searchAnnotations,
changeWorkspace,
switchPredictor,
switchNavigationBlocked,
toolsBlockerState,
} = this.props;
@ -683,8 +737,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
onDeleteFrame={this.onDeleteFrame}
onRestoreFrame={this.onRestoreFrame}
changeWorkspace={changeWorkspace}
switchPredictor={switchPredictor}
switchNavigationBlocked={switchNavigationBlocked}
predictor={predictor}
workspace={workspace}
playing={playing}
@ -694,6 +751,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
stopFrame={stopFrame}
frameNumber={frameNumber}
frameFilename={frameFilename}
frameDeleted={frameIsDeleted}
inputFrameRef={this.inputFrameRef}
undoAction={undoAction}
redoAction={redoAction}

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,6 +12,7 @@ import {
switchRotateAll,
changeCanvasBackgroundColor,
switchSmoothImage,
switchShowingDeletedFrames,
} from 'actions/settings-actions';
import { CombinedState, FrameSpeed } from 'reducers/interfaces';
@ -22,6 +23,7 @@ interface StateToProps {
rotateAll: boolean;
smoothImage: boolean;
canvasBackgroundColor: string;
showDeletedFrames: boolean;
}
interface DispatchToProps {
@ -31,6 +33,7 @@ interface DispatchToProps {
onSwitchRotateAll(rotateAll: boolean): void;
onChangeCanvasBackgroundColor(color: string): void;
onSwitchSmoothImage(enabled: boolean): void;
onSwitchShowingDeletedFrames(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -61,6 +64,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchSmoothImage(enabled: boolean): void {
dispatch(switchSmoothImage(enabled));
},
onSwitchShowingDeletedFrames(enabled: boolean): void {
dispatch(switchShowingDeletedFrames(enabled));
},
};
}

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -9,8 +9,9 @@ import {
ViewType,
CameraAction,
ViewsDOM,
CanvasMode,
} from 'cvat-canvas3d/src/typescript/canvas3d';
export {
Canvas3d, Canvas3dVersion, MouseInteraction, ViewType, CameraAction, ViewsDOM,
Canvas3d, Canvas3dVersion, MouseInteraction, ViewType, CameraAction, ViewsDOM, CanvasMode,
};

@ -51,6 +51,7 @@ import SVGCVATAzureProvider from './assets/vscode-icons_file-type-azure.svg';
import SVGCVATS3Provider from './assets/S3.svg';
import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg';
import SVGOpenVINO from './assets/openvino.svg';
import SVGRestoreIcon from './assets/restore-icon.svg';
export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />);
@ -99,3 +100,4 @@ export const AzureProvider = React.memo((): JSX.Element => <SVGCVATAzureProvider
export const S3Provider = React.memo((): JSX.Element => <SVGCVATS3Provider />);
export const GoogleCloudProvider = React.memo((): JSX.Element => <SVGCVATGoogleCloudProvider />);
export const OpenVINOIcon = React.memo((): JSX.Element => <SVGOpenVINO />);
export const RestoreIcon = React.memo((): JSX.Element => <SVGRestoreIcon />);

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -1207,6 +1207,63 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.DELETE_FRAME:
case AnnotationActionTypes.RESTORE_FRAME: {
return {
...state,
player: {
...state.player,
frame: {
...state.player.frame,
fetching: true,
},
},
canvas: {
...state.canvas,
ready: false,
},
};
}
case AnnotationActionTypes.DELETE_FRAME_FAILED:
case AnnotationActionTypes.RESTORE_FRAME_FAILED: {
return {
...state,
player: {
...state.player,
frame: {
...state.player.frame,
fetching: false,
},
},
canvas: {
...state.canvas,
ready: true,
},
};
}
case AnnotationActionTypes.DELETE_FRAME_SUCCESS:
case AnnotationActionTypes.RESTORE_FRAME_SUCCESS: {
return {
...state,
player: {
...state.player,
frame: {
...state.player.frame,
data: action.payload.data,
fetching: false,
},
},
annotations: {
...state.annotations,
history: action.payload.history,
states: action.payload.states,
},
canvas: {
...state.canvas,
ready: true,
},
};
}
case AnnotationActionTypes.CLOSE_JOB:
case AuthActionTypes.LOGOUT_SUCCESS: {
if (state.canvas.instance) {

@ -411,6 +411,8 @@ export interface NotificationsState {
redo: null | ErrorState;
search: null | ErrorState;
searchEmptyFrame: null | ErrorState;
deleteFrame: null | ErrorState;
restoreFrame: null | ErrorState;
savingLogs: null | ErrorState;
};
boundaries: {
@ -677,6 +679,7 @@ export interface PlayerSettingsState {
resetZoom: boolean;
rotateAll: boolean;
smoothImage: boolean;
showDeletedFrames: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;

@ -104,6 +104,8 @@ const defaultState: NotificationsState = {
redo: null,
search: null,
searchEmptyFrame: null,
deleteFrame: null,
restoreFrame: null,
savingLogs: null,
},
boundaries: {
@ -1299,6 +1301,36 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.DELETE_FRAME_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
deleteFrame: {
message: 'Could not delete frame',
reason: action.payload.error,
},
},
},
};
}
case AnnotationActionTypes.RESTORE_FRAME_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
restoreFrame: {
message: 'Could not restore frame',
reason: action.payload.error,
},
},
},
};
}
case CloudStorageActionTypes.GET_CLOUD_STORAGE_FAILED: {
return {
...state,

@ -42,6 +42,12 @@ export default function (state: ReviewState = defaultState, action: any): Review
newIssuePosition: null,
};
}
case AnnotationActionTypes.DELETE_FRAME_SUCCESS: {
return {
...state,
newIssuePosition: null,
};
}
case ReviewActionTypes.SUBMIT_REVIEW: {
const { jobId } = action.payload;
return {

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -47,6 +47,7 @@ const defaultState: SettingsState = {
resetZoom: false,
rotateAll: false,
smoothImage: true,
showDeletedFrames: false,
grid: false,
gridSize: 100,
gridColor: GridColor.White,
@ -353,6 +354,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
...action.payload.settings,
};
}
case SettingsActionTypes.SWITCH_SHOWING_DELETED_FRAMES: {
return {
...state,
player: {
...state.player,
showDeletedFrames: action.payload.showDeletedFrames,
},
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AnnotationActionTypes.GET_JOB_SUCCESS: {
const { job } = action.payload;

@ -10,6 +10,7 @@ export default function isAbleToChangeFrame(): boolean {
const state: CombinedState = store.getState();
const { instance } = state.annotation.canvas;
return !!instance && instance.isAbleToChangeFrame() &&
!state.annotation.player.navigationBlocked;
}

@ -1,5 +1,5 @@
# Copyright (C) 2019-2021 Intel Corporation
# Copyright (C) 2019-2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -206,6 +206,7 @@ class TaskData(InstanceLabelData):
return d
def _init_frame_info(self):
self._deleted_frames = { k: True for k in self._db_task.data.deleted_frames }
if hasattr(self._db_task.data, 'video'):
self._frame_info = {frame: {
"path": "frame_{:06d}".format(self.abs_frame_id(frame)),
@ -375,16 +376,18 @@ class TaskData(InstanceLabelData):
if include_empty:
for idx in self._frame_info:
get_frame(idx)
if idx not in self._deleted_frames:
get_frame(idx)
anno_manager = AnnotationManager(self._annotation_ir)
shape_data = ''
for shape in sorted(anno_manager.to_shapes(self._db_task.data.size),
key=lambda shape: shape.get("z_order", 0)):
if shape['frame'] not in self._frame_info:
if shape['frame'] not in self._frame_info or shape['frame'] in self._deleted_frames:
# After interpolation there can be a finishing frame
# outside of the task boundaries. Filter it out to avoid errors.
# https://github.com/openvinotoolkit/cvat/issues/2827
# Also we skipped deleted frames here
continue
if 'track_id' in shape:
if shape['outside']:
@ -401,6 +404,8 @@ class TaskData(InstanceLabelData):
get_frame(shape['frame']).labels.update({label.id: label})
for tag in self._annotation_ir.tags:
if tag['frame'] not in self._frame_info or shape['frame'] in self._deleted_frames:
continue
get_frame(tag['frame']).tags.append(self._export_tag(tag))
return iter(frames.values())
@ -408,11 +413,13 @@ class TaskData(InstanceLabelData):
@property
def shapes(self):
for shape in self._annotation_ir.shapes:
yield self._export_labeled_shape(shape)
if shape["frame"] not in self._deleted_frames:
yield self._export_labeled_shape(shape)
@property
def tracks(self):
for idx, track in enumerate(self._annotation_ir.tracks):
track['shapes'] = list(filter(lambda x: x['frame'] not in self._deleted_frames, track['shapes']))
tracked_shapes = TrackManager.get_interpolated_shapes(
track, 0, self._db_task.data.size)
for tracked_shape in tracked_shapes:
@ -427,13 +434,14 @@ class TaskData(InstanceLabelData):
group=track["group"],
source=track["source"],
shapes=[self._export_tracked_shape(shape)
for shape in tracked_shapes],
for shape in tracked_shapes if shape["frame"] not in self._deleted_frames],
)
@property
def tags(self):
for tag in self._annotation_ir.tags:
yield self._export_tag(tag)
if tag["frame"] not in self._deleted_frames:
yield self._export_tag(tag)
@property
def meta(self):
@ -538,6 +546,10 @@ class TaskData(InstanceLabelData):
def frame_info(self):
return self._frame_info
@property
def deleted_frames(self):
return self._deleted_frames
@property
def frame_step(self):
return self._frame_step
@ -700,6 +712,7 @@ class ProjectData(InstanceLabelData):
def _init_frame_info(self):
self._frame_info = dict()
self._deleted_frames = { (task.id, frame): True for task in self._db_tasks.values() for frame in task.data.deleted_frames }
original_names = DefaultDict[Tuple[str, str], int](int)
for task in self._db_tasks.values():
defaulted_subset = get_defaulted_subset(task.subset, self._subsets)
@ -832,13 +845,14 @@ class ProjectData(InstanceLabelData):
if include_empty:
for ident in self._frame_info:
get_frame(*ident)
if ident not in self._deleted_frames:
get_frame(*ident)
for task in self._db_tasks.values():
anno_manager = AnnotationManager(self._annotation_irs[task.id])
for shape in sorted(anno_manager.to_shapes(task.data.size),
key=lambda shape: shape.get("z_order", 0)):
if (task.id, shape['frame']) not in self._frame_info:
if (task.id, shape['frame']) not in self._frame_info or (task.id, shape['frame']) in self._deleted_frames:
continue
if 'track_id' in shape:
if shape['outside']:
@ -849,6 +863,8 @@ class ProjectData(InstanceLabelData):
get_frame(task.id, shape['frame']).labeled_shapes.append(exported_shape)
for tag in self._annotation_irs[task.id].tags:
if (task.id, tag['frame']) not in self._frame_info:
continue
get_frame(task.id, tag['frame']).tags.append(self._export_tag(tag, task.id))
return iter(frames.values())
@ -857,13 +873,15 @@ class ProjectData(InstanceLabelData):
def shapes(self):
for task in self._db_tasks.values():
for shape in self._annotation_irs[task.id].shapes:
yield self._export_labeled_shape(shape, task.id)
if (task.id, shape['frame']) not in self._deleted_frames:
yield self._export_labeled_shape(shape, task.id)
@property
def tracks(self):
idx = 0
for task in self._db_tasks.values():
for track in self._annotation_irs[task.id].tracks:
track['shapes'] = list(filter(lambda x: (task.id, x['frame']) not in self._deleted_frames, track['shapes']))
tracked_shapes = TrackManager.get_interpolated_shapes(
track, 0, task.data.size
)
@ -877,8 +895,8 @@ class ProjectData(InstanceLabelData):
label=self._get_label_name(track["label_id"]),
group=track["group"],
source=track["source"],
shapes=[self._export_tracked_shape(shape, task.id)
for shape in tracked_shapes],
shapes=[self._export_tracked_shape(shape, task.id) for shape in tracked_shapes
if (task.id, shape["frame"]) not in self._deleted_frames],
task_id=task.id
)
idx+=1
@ -887,7 +905,8 @@ class ProjectData(InstanceLabelData):
def tags(self):
for task in self._db_tasks.values():
for tag in self._annotation_irs[task.id].tags:
yield self._export_tag(tag, task.id)
if (task.id, tag['frame']) not in self._deleted_frames:
yield self._export_tag(tag, task.id)
@property
def meta(self):
@ -901,6 +920,10 @@ class ProjectData(InstanceLabelData):
def frame_info(self):
return self._frame_info
@property
def deleted_frames(self):
return self._deleted_frames
@property
def frame_step(self):
return self._frame_steps

@ -1034,6 +1034,9 @@ def dump_media_files(task_data: TaskData, img_dir: str, project_data: ProjectDat
frame_provider.Quality.ORIGINAL,
frame_provider.Type.BUFFER)
for frame_id, (frame_data, _) in enumerate(frames):
if (project_data is not None and (task_data.db_task.id, frame_id) in project_data.deleted_frames) \
or frame_id in task_data.deleted_frames:
continue
frame_name = task_data.frame_info[frame_id]['path'] if project_data is None \
else project_data.frame_info[(task_data.db_task.id, frame_id)]['path']
img_path = osp.join(img_dir, frame_name + ext)

@ -113,6 +113,7 @@ class _TaskBackupBase(_BackupBase):
'storage_method',
'storage',
'sorting_method',
'deleted_frames',
}
self._prepare_meta(allowed_fields, data)
@ -310,6 +311,8 @@ class TaskExporter(_ExporterBase, _TaskBackupBase):
data_serializer = DataSerializer(self._db_data)
data = data_serializer.data
data['chunk_type'] = data.pop('compressed_chunk_type')
# There are no deleted frames in DataSerializer so we need to pick it
data['deleted_frames'] = self._db_data.deleted_frames
return self._prepare_data_meta(data)
task = serialize_task()
@ -511,8 +514,9 @@ class TaskImporter(_ImporterBase, _TaskBackupBase):
db_data.start_frame = data['start_frame']
db_data.stop_frame = data['stop_frame']
db_data.frame_filter = data['frame_filter']
db_data.deleted_frames = data_serializer.initial_data.get('deleted_frames', [])
db_data.storage = StorageChoice.LOCAL
db_data.save(update_fields=['start_frame', 'stop_frame', 'frame_filter', 'storage'])
db_data.save(update_fields=['start_frame', 'stop_frame', 'frame_filter', 'storage', 'deleted_frames'])
for db_job, job in zip(self._get_db_jobs(), jobs):
db_job.status = job['status']

@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2022-05-20 09:21
import cvat.apps.engine.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('engine', '0052_alter_cloudstorage_specific_attributes'),
]
operations = [
migrations.AddField(
model_name='data',
name='deleted_frames',
field=cvat.apps.engine.models.IntArrayField(default=''),
),
]

@ -1,4 +1,4 @@
# Copyright (C) 2018-2019 Intel Corporation
# Copyright (C) 2018-2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -6,6 +6,7 @@ import os
import re
import shutil
from enum import Enum
from typing import Optional
from django.conf import settings
from django.contrib.auth.models import User
@ -124,6 +125,41 @@ class SortingMethod(str, Enum):
def __str__(self):
return self.value
class AbstractArrayField(models.TextField):
separator = ","
converter = lambda x: x
def __init__(self, *args, store_sorted:Optional[bool]=False, unique_values:Optional[bool]=False, **kwargs):
self._store_sorted = store_sorted
self._unique_values = unique_values
super().__init__(*args,**{'default': '', **kwargs})
def from_db_value(self, value, expression, connection):
if not value:
return []
if value.startswith('[') and value.endswith(']'):
value = value[1:-1]
return [self.converter(v) for v in value.split(self.separator)]
def to_python(self, value):
if isinstance(value, list):
return value
return self.from_db_value(value, None, None)
def get_prep_value(self, value):
if self._unique_values:
value = list(dict.fromkeys(value))
if self._store_sorted:
value = sorted(value)
return self.separator.join(map(str, value))
class FloatArrayField(AbstractArrayField):
converter = float
class IntArrayField(AbstractArrayField):
converter = int
class Data(models.Model):
chunk_size = models.PositiveIntegerField(null=True)
size = models.PositiveIntegerField(default=0)
@ -139,6 +175,7 @@ class Data(models.Model):
storage = models.CharField(max_length=15, choices=StorageChoice.choices(), default=StorageChoice.LOCAL)
cloud_storage = models.ForeignKey('CloudStorage', on_delete=models.SET_NULL, null=True, related_name='data')
sorting_method = models.CharField(max_length=15, choices=SortingMethod.choices(), default=SortingMethod.LEXICOGRAPHICAL)
deleted_frames = IntArrayField(store_sorted=True, unique_values=True)
class Meta:
default_permissions = ()
@ -529,25 +566,6 @@ class Commit(models.Model):
class JobCommit(Commit):
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="commits")
class FloatArrayField(models.TextField):
separator = ","
def from_db_value(self, value, expression, connection):
if not value:
return value
if value.startswith('[') and value.endswith(']'):
value = value[1:-1]
return [float(v) for v in value.split(self.separator)]
def to_python(self, value):
if isinstance(value, list):
return value
return self.from_db_value(value, None, None)
def get_prep_value(self, value):
return self.separator.join(map(str, value))
class Shape(models.Model):
type = models.CharField(max_length=16, choices=ShapeType.choices())
occluded = models.BooleanField(default=False)

@ -1,4 +1,4 @@
# Copyright (C) 2019-2021 Intel Corporation
# Copyright (C) 2019-2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
@ -404,6 +404,7 @@ class DataSerializer(serializers.ModelSerializer):
remote_file = models.RemoteFile(data=instance, **f)
remote_file.save()
class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
labels = LabelSerializer(many=True, source='label_set', partial=True, required=False)
segments = SegmentSerializer(many=True, source='segment_set', read_only=True)
@ -681,9 +682,10 @@ class PluginsSerializer(serializers.Serializer):
MODELS = serializers.BooleanField()
PREDICT = serializers.BooleanField()
class DataMetaSerializer(serializers.ModelSerializer):
class DataMetaReadSerializer(serializers.ModelSerializer):
frames = FrameMetaSerializer(many=True, allow_null=True)
image_quality = serializers.IntegerField(min_value=0, max_value=100)
deleted_frames = serializers.ListField(child=serializers.IntegerField(min_value=0))
class Meta:
model = models.Data
@ -695,16 +697,16 @@ class DataMetaSerializer(serializers.ModelSerializer):
'stop_frame',
'frame_filter',
'frames',
'deleted_frames',
)
read_only_fields = (
'chunk_size',
'size',
'image_quality',
'start_frame',
'stop_frame',
'frame_filter',
'frames',
)
read_only_fields = fields
class DataMetaWriteSerializer(serializers.ModelSerializer):
deleted_frames = serializers.ListField(child=serializers.IntegerField(min_value=0))
class Meta:
model = models.Data
fields = ('deleted_frames',)
class AttributeValSerializer(serializers.Serializer):
spec_id = serializers.IntegerField()

@ -399,6 +399,49 @@ class JobPartialUpdateAPITestCase(JobUpdateAPITestCase):
response = self._run_api_v2_jobs_id(self.job.id, self.owner, data)
self._check_request(response, data)
class JobDataMetaPartialUpdateAPITestCase(APITestCase):
def setUp(self):
self.client = APIClient()
self.task = create_dummy_db_tasks(self)[0]
self.job = Job.objects.filter(segment__task_id=self.task.id).first()
self.job.assignee = self.annotator
self.job.save()
@classmethod
def setUpTestData(cls):
create_db_users(cls)
def _run_api_v1_jobs_data_meta_id(self, jid, user, data):
with ForceLogin(user, self.client):
response = self.client.patch('/api/jobs/{}/data/meta'.format(jid), data=data, format='json')
return response
def _check_response(self, response, db_data, data):
self.assertEqual(response.status_code, status.HTTP_200_OK)
deleted_frames = data.get("deleted_frames", db_data.deleted_frames)
self.assertEqual(response.data["deleted_frames"], deleted_frames)
def _check_api_v1_jobs_data_meta_id(self, user, data):
response = self._run_api_v1_jobs_data_meta_id(self.job.id, user, data)
if user is None:
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
elif user == self.job.segment.task.owner or user == self.job.segment.task.assignee or user == self.job.assignee or user.is_superuser:
self._check_response(response, self.job.segment.task.data, data)
else:
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_api_v1_jobss_data_meta(self):
data = {
"deleted_frames": [1,2,3]
}
self._check_api_v1_jobs_data_meta_id(self.admin, data)
data = {
"deleted_frames": []
}
self._check_api_v1_jobs_data_meta_id(self.admin, data)
class ServerAboutAPITestCase(APITestCase):
ACCEPT_HEADER_TEMPLATE = 'application/vnd.cvat+json; version={}'
@ -2112,6 +2155,49 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
}
self._check_api_v2_tasks_id(None, data)
class TaskDataMetaPartialUpdateAPITestCase(APITestCase):
def setUp(self):
self.client = APIClient()
@classmethod
def setUpTestData(cls):
create_db_users(cls)
cls.tasks = create_dummy_db_tasks(cls)
def _run_api_v1_task_data_meta_id(self, tid, user, data):
with ForceLogin(user, self.client):
response = self.client.patch('/api/tasks/{}/data/meta'.format(tid),
data=data, format="json")
return response
def _check_response(self, response, db_data, data):
self.assertEqual(response.status_code, status.HTTP_200_OK)
deleted_frames = data.get("deleted_frames", db_data.deleted_frames)
self.assertEqual(response.data["deleted_frames"], deleted_frames)
def _check_api_v1_task_data_id(self, user, data):
for db_task in self.tasks:
response = self._run_api_v1_task_data_meta_id(db_task.id, user, data)
if user is None:
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
elif user == db_task.owner or user == db_task.assignee or user.is_superuser:
self._check_response(response, db_task.data, data)
else:
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_api_v1_tasks_data_meta(self):
data = {
"deleted_frames": [1,2,3]
}
self._check_api_v1_task_data_id(self.user, data)
data = {
"deleted_frames": []
}
self._check_api_v1_task_data_id(self.user, data)
class TaskUpdateLabelsAPITestCase(UpdateLabelsAPITestCase):
@classmethod
def setUpTestData(cls):

@ -54,7 +54,7 @@ from cvat.apps.engine.models import (
from cvat.apps.engine.models import CloudStorage as CloudStorageModel
from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaSerializer, DataSerializer, ExceptionSerializer,
DataMetaReadSerializer, DataMetaWriteSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobReadSerializer, JobWriteSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer, PluginsSerializer, IssueReadSerializer,
@ -941,20 +941,29 @@ class TaskViewSet(UploadMixin, viewsets.ModelViewSet):
return response
@staticmethod
@extend_schema(summary='Method provides a meta information about media files which are related with the task',
responses={
'200': DataMetaSerializer,
'200': DataMetaReadSerializer,
})
@extend_schema(methods=['PATCH'], summary='Method performs an update of data meta fields (deleted frames)',
responses={
'200': DataMetaReadSerializer,
})
@action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer,
@action(detail=True, methods=['GET', 'PATCH'], serializer_class=DataMetaReadSerializer,
url_path='data/meta')
def data_info(request, pk):
def metadata(self, request, pk):
self.get_object() #force to call check_object_permissions
db_task = models.Task.objects.prefetch_related(
Prefetch('data', queryset=models.Data.objects.select_related('video').prefetch_related(
Prefetch('images', queryset=models.Image.objects.prefetch_related('related_files').order_by('frame'))
))
).get(pk=pk)
if request.method == 'PATCH':
serializer = DataMetaWriteSerializer(instance=db_task.data, data=request.data)
if serializer.is_valid(raise_exception=True):
db_task.data = serializer.save()
if hasattr(db_task.data, 'video'):
media = [db_task.data.video]
else:
@ -970,7 +979,7 @@ class TaskViewSet(UploadMixin, viewsets.ModelViewSet):
db_data = db_task.data
db_data.frames = frame_meta
serializer = DataMetaSerializer(db_data)
serializer = DataMetaReadSerializer(db_data)
return Response(serializer.data)
@extend_schema(summary='Export task as a dataset in a specific format',
@ -1201,6 +1210,76 @@ class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return data_getter(request, db_job.segment.start_frame,
db_job.segment.stop_frame, db_job.segment.task.data)
@extend_schema(summary='Method provides a meta information about media files which are related with the job',
responses={
'200': DataMetaReadSerializer,
})
@extend_schema(methods=['PATCH'], summary='Method performs an update of data meta fields (deleted frames)',
responses={
'200': DataMetaReadSerializer,
}, tags=['tasks'], versions=['2.0'])
@action(detail=True, methods=['GET', 'PATCH'], serializer_class=DataMetaReadSerializer,
url_path='data/meta')
def metadata(self, request, pk):
self.get_object() #force to call check_object_permissions
db_job = models.Job.objects.prefetch_related(
'segment',
'segment__task',
Prefetch('segment__task__data', queryset=models.Data.objects.select_related('video').prefetch_related(
Prefetch('images', queryset=models.Image.objects.prefetch_related('related_files').order_by('frame'))
))
).get(pk=pk)
db_data = db_job.segment.task.data
start_frame = db_job.segment.start_frame
stop_frame = db_job.segment.stop_frame
data_start_frame = db_data.start_frame + start_frame * db_data.get_frame_step()
data_stop_frame = db_data.start_frame + stop_frame * db_data.get_frame_step()
if request.method == 'PATCH':
serializer = DataMetaWriteSerializer(instance=db_data, data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.validated_data['deleted_frames'] = list(filter(
lambda frame: frame >= start_frame and frame <= stop_frame,
serializer.validated_data['deleted_frames']
)) + list(filter(
lambda frame: frame < start_frame and frame > stop_frame,
db_data.deleted_frames,
))
db_data = serializer.save()
db_job.segment.task.save()
if db_job.segment.task.project:
db_job.segment.task.project.save()
if hasattr(db_data, 'video'):
media = [db_data.video]
else:
media = list(db_data.images.filter(
frame__gte=data_start_frame,
frame__lte=data_stop_frame,
).all())
# Filter data with segment size
# Should data.size also be cropped by segment size?
db_data.deleted_frames = filter(
lambda frame: frame >= start_frame and frame <= stop_frame,
db_data.deleted_frames,
)
db_data.start_frame = data_start_frame
db_data.stop_frame = data_stop_frame
frame_meta = [{
'width': item.width,
'height': item.height,
'name': item.path,
'has_related_context': hasattr(item, 'related_files') and item.related_files.exists()
} for item in media]
db_data.frames = frame_meta
serializer = DataMetaReadSerializer(db_data)
return Response(serializer.data)
@extend_schema(summary='The action returns the list of tracked '
'changes for the job', responses={
'200': JobCommitSerializer(many=True),

@ -651,8 +651,9 @@ class TaskPermission(OpenPolicyAgentPermission):
('append_annotations_chunk', 'PATCH'): 'update:annotations',
('append_annotations_chunk', 'HEAD'): 'update:annotations',
('dataset_export', 'GET'): 'export:dataset',
('metadata', 'GET'): 'view:metadata',
('metadata', 'PATCH'): 'update:metadata',
('data', 'GET'): 'view:data',
('data_info', 'GET'): 'view:data',
('data', 'POST'): 'upload:data',
('append_data_chunk', 'PATCH'): 'upload:data',
('append_data_chunk', 'HEAD'): 'upload:data',
@ -801,6 +802,8 @@ class JobPermission(OpenPolicyAgentPermission):
('append_annotations_chunk', 'PATCH'): 'update:annotations',
('append_annotations_chunk', 'HEAD'): 'update:annotations',
('data', 'GET'): 'view:data',
('metadata','GET'): 'view:metadata',
('metadata','PATCH'): 'update:metadata',
('issues', 'GET'): 'view',
('commits', 'GET'): 'view:commits'
}.get((view.action, request.method))
@ -1051,7 +1054,8 @@ class PolicyEnforcer(BasePermission):
@staticmethod
def is_metadata_request(request, view):
return request.method == 'OPTIONS' or view.action == 'metadata'
return request.method == 'OPTIONS' \
or (request.method == 'POST' and view.action == 'metadata' and len(request.data) == 0)
class IsMemberInOrganization(BasePermission):
message = 'You should be an active member in the organization.'

@ -33,6 +33,14 @@ view:data,Job,Sandbox,None,,GET,/jobs/{id}/data,Admin,N/A
view:data,Job,Sandbox,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,GET,/jobs/{id}/data,None,N/A
view:data,Job,Organization,None,,GET,/jobs/{id}/data,User,Maintainer
view:data,Job,Organization,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,GET,/jobs/{id}/data,None,Worker
view:metadata,Job,Sandbox,None,,GET,/jobs/{id}/data/meta,Admin,N/A
view:metadata,Job,Sandbox,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,GET,/jobs/{id}/data/meta,None,N/A
view:metadata,Job,Organization,None,,GET,/jobs/{id}/data/meta,User,Maintainer
view:metadata,Job,Organization,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,GET,/jobs/{id}/data/meta,None,Worker
update:metadata,Job,Sandbox,None,,PATCH,/jobs/{id}/data/meta,Admin,N/A
update:metadata,Job,Sandbox,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,PATCH,/jobs/{id}/data/meta,Worker,N/A
update:metadata,Job,Organization,None,,PATCH,/jobs/{id}/data/meta,User,Maintainer
update:metadata,Job,Organization,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,PATCH,/jobs/{id}/data/meta,Worker,Worker
view:commits,Job,Sandbox,None,,GET,/jobs/{id}/commits,Admin,N/A
view:commits,Job,Sandbox,"Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee",,GET,/jobs/{id}/commits,None,N/A
view:commits,Job,Organization,None,,GET,/jobs/{id}/commits,User,Maintainer

1 Scope Resource Context Ownership Limit Method URL Privilege Membership
33 view:data Job Sandbox Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee GET /jobs/{id}/data None N/A
34 view:data Job Organization None GET /jobs/{id}/data User Maintainer
35 view:data Job Organization Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee GET /jobs/{id}/data None Worker
36 view:metadata Job Sandbox None GET /jobs/{id}/data/meta Admin N/A
37 view:metadata Job Sandbox Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee GET /jobs/{id}/data/meta None N/A
38 view:metadata Job Organization None GET /jobs/{id}/data/meta User Maintainer
39 view:metadata Job Organization Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee GET /jobs/{id}/data/meta None Worker
40 update:metadata Job Sandbox None PATCH /jobs/{id}/data/meta Admin N/A
41 update:metadata Job Sandbox Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee PATCH /jobs/{id}/data/meta Worker N/A
42 update:metadata Job Organization None PATCH /jobs/{id}/data/meta User Maintainer
43 update:metadata Job Organization Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee PATCH /jobs/{id}/data/meta Worker Worker
44 view:commits Job Sandbox None GET /jobs/{id}/commits Admin N/A
45 view:commits Job Sandbox Project:owner, Project:assignee, Task:owner, Task:assignee, Assignee GET /jobs/{id}/commits None N/A
46 view:commits Job Organization None GET /jobs/{id}/commits User Maintainer

@ -139,20 +139,20 @@ filter = [] { # Django Q object to filter list of entries
}
allow {
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.VIEW_DATA, utils.VIEW_COMMITS }[input.scope]
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.VIEW_DATA, utils.VIEW_METADATA, utils.VIEW_COMMITS }[input.scope]
utils.is_sandbox
is_job_staff
}
allow {
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.VIEW_DATA, utils.VIEW_COMMITS }[input.scope]
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.VIEW_DATA, utils.VIEW_METADATA, utils.VIEW_COMMITS }[input.scope]
input.auth.organization.id == input.resource.organization.id
utils.has_perm(utils.USER)
organizations.has_perm(organizations.MAINTAINER)
}
allow {
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.VIEW_DATA, utils.VIEW_COMMITS }[input.scope]
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.VIEW_DATA, utils.VIEW_METADATA, utils.VIEW_COMMITS }[input.scope]
input.auth.organization.id == input.resource.organization.id
organizations.has_perm(organizations.WORKER)
is_job_staff
@ -160,7 +160,7 @@ allow {
allow {
{ utils.UPDATE_STATE, utils.UPDATE_ANNOTATIONS, utils.DELETE_ANNOTATIONS,
utils.IMPORT_ANNOTATIONS }[input.scope]
utils.IMPORT_ANNOTATIONS, utils.UPDATE_METADATA }[input.scope]
utils.is_sandbox
utils.has_perm(utils.WORKER)
is_job_staff
@ -168,7 +168,7 @@ allow {
allow {
{ utils.UPDATE_STATE, utils.UPDATE_ANNOTATIONS, utils.DELETE_ANNOTATIONS,
utils.IMPORT_ANNOTATIONS }[input.scope]
utils.IMPORT_ANNOTATIONS, utils.UPDATE_METADATA }[input.scope]
input.auth.organization.id == input.resource.organization.id
utils.has_perm(utils.USER)
organizations.has_perm(organizations.MAINTAINER)
@ -176,7 +176,7 @@ allow {
allow {
{ utils.UPDATE_STATE, utils.UPDATE_ANNOTATIONS, utils.DELETE_ANNOTATIONS,
utils.IMPORT_ANNOTATIONS }[input.scope]
utils.IMPORT_ANNOTATIONS, utils.UPDATE_METADATA }[input.scope]
input.auth.organization.id == input.resource.organization.id
utils.has_perm(utils.WORKER)
organizations.has_perm(organizations.WORKER)

File diff suppressed because it is too large Load Diff

@ -56,10 +56,18 @@ export:dataset,Task,Sandbox,None,,GET,/tasks/{id}/dataset?format=,Admin,N/A
export:dataset,Task,Sandbox,"Owner, Project:owner, Assignee, Project:assignee",,GET,/tasks/{id}/dataset?format=,None,N/A
export:dataset,Task,Organization,None,,GET,/tasks/{id}/dataset?format=,User,Maintainer
export:dataset,Task,Organization,"Owner, Project:owner, Assignee, Project:assignee",,GET,/tasks/{id}/dataset?format=,None,Worker
view:data,Task,Sandbox,None,,GET,"/tasks/{id}/data, /tasks/{id}/data/meta",Admin,N/A
view:data,Task,Sandbox,"Owner, Project:owner, Assignee, Project:assignee",,GET,"/tasks/{id}/data, /tasks/{id}/data/meta",None,N/A
view:data,Task,Organization,None,,GET,"/tasks/{id}/data, /tasks/{id}/data/meta",User,Maintainer
view:data,Task,Organization,"Owner, Project:owner, Assignee, Project:assignee",,GET,"/tasks/{id}/data, /tasks/{id}/data/meta",None,Worker
view:data,Task,Sandbox,None,,GET,/tasks/{id}/data,Admin,N/A
view:data,Task,Sandbox,"Owner, Project:owner, Assignee, Project:assignee",,GET,/tasks/{id}/data,None,N/A
view:data,Task,Organization,None,,GET,/tasks/{id}/data,User,Maintainer
view:data,Task,Organization,"Owner, Project:owner, Assignee, Project:assignee",,GET,/tasks/{id}/data,None,Worker
view:metadata,Task,Sandbox,None,,GET,/tasks/{id}/data/meta,Admin,N/A
view:metadata,Task,Sandbox,"Owner, Project:owner, Assignee, Project:assignee",,GET,/tasks/{id}/data/meta,None,N/A
view:metadata,Task,Organization,None,,GET,/tasks/{id}/data/meta,User,Maintainer
view:metadata,Task,Organization,"Owner, Project:owner, Assignee, Project:assignee",,GET,/tasks/{id}/data/meta,None,Worker
update:metadata,Task,Sandbox,None,,PATCH,/tasks/{id}/data/meta,Admin,N/A
update:metadata,Task,Sandbox,"Owner, Project:owner, Assignee, Project:assignee",,PATCH,/tasks/{id}/data/meta,Worker,N/A
update:metadata,Task,Organization,None,,PATCH,/tasks/{id}/data/meta,User,Maintainer
update:metadata,Task,Organization,"Owner, Project:owner, Assignee, Project:assignee",,PATCH,/tasks/{id}/data/meta,Worker,Worker
upload:data,Task,Sandbox,None,,POST,/tasks/{id}/data,Admin,N/A
upload:data,Task,Sandbox,"Owner, Project:owner, Assignee, Project:assignee",,POST,/tasks/{id}/data,Worker,N/A
upload:data,Task,Organization,None,,POST,/tasks/{id}/data,User,Maintainer

1 Scope Resource Context Ownership Limit Method URL Privilege Membership
56 export:dataset Task Sandbox Owner, Project:owner, Assignee, Project:assignee GET /tasks/{id}/dataset?format= None N/A
57 export:dataset Task Organization None GET /tasks/{id}/dataset?format= User Maintainer
58 export:dataset Task Organization Owner, Project:owner, Assignee, Project:assignee GET /tasks/{id}/dataset?format= None Worker
59 view:data Task Sandbox None GET /tasks/{id}/data, /tasks/{id}/data/meta /tasks/{id}/data Admin N/A
60 view:data Task Sandbox Owner, Project:owner, Assignee, Project:assignee GET /tasks/{id}/data, /tasks/{id}/data/meta /tasks/{id}/data None N/A
61 view:data Task Organization None GET /tasks/{id}/data, /tasks/{id}/data/meta /tasks/{id}/data User Maintainer
62 view:data Task Organization Owner, Project:owner, Assignee, Project:assignee GET /tasks/{id}/data, /tasks/{id}/data/meta /tasks/{id}/data None Worker
63 view:metadata Task Sandbox None GET /tasks/{id}/data/meta Admin N/A
64 view:metadata Task Sandbox Owner, Project:owner, Assignee, Project:assignee GET /tasks/{id}/data/meta None N/A
65 view:metadata Task Organization None GET /tasks/{id}/data/meta User Maintainer
66 view:metadata Task Organization Owner, Project:owner, Assignee, Project:assignee GET /tasks/{id}/data/meta None Worker
67 update:metadata Task Sandbox None PATCH /tasks/{id}/data/meta Admin N/A
68 update:metadata Task Sandbox Owner, Project:owner, Assignee, Project:assignee PATCH /tasks/{id}/data/meta Worker N/A
69 update:metadata Task Organization None PATCH /tasks/{id}/data/meta User Maintainer
70 update:metadata Task Organization Owner, Project:owner, Assignee, Project:assignee PATCH /tasks/{id}/data/meta Worker Worker
71 upload:data Task Sandbox None POST /tasks/{id}/data Admin N/A
72 upload:data Task Sandbox Owner, Project:owner, Assignee, Project:assignee POST /tasks/{id}/data Worker N/A
73 upload:data Task Organization None POST /tasks/{id}/data User Maintainer

@ -195,14 +195,14 @@ filter = [] { # Django Q object to filter list of entries
}
allow {
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.EXPORT_DATASET,
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.EXPORT_DATASET, utils.VIEW_METADATA,
utils.VIEW_DATA, utils.EXPORT_ANNOTATIONS, utils.EXPORT_BACKUP }[input.scope]
utils.is_sandbox
is_task_staff
}
allow {
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.EXPORT_DATASET,
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.EXPORT_DATASET, utils.VIEW_METADATA,
utils.VIEW_DATA, utils.EXPORT_ANNOTATIONS, utils.EXPORT_BACKUP }[input.scope]
input.auth.organization.id == input.resource.organization.id
utils.has_perm(utils.USER)
@ -210,7 +210,7 @@ allow {
}
allow {
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.EXPORT_DATASET,
{ utils.VIEW, utils.VIEW_ANNOTATIONS, utils.EXPORT_DATASET, utils.VIEW_METADATA,
utils.VIEW_DATA, utils.EXPORT_ANNOTATIONS, utils.EXPORT_BACKUP }[input.scope]
input.auth.organization.id == input.resource.organization.id
organizations.has_perm(organizations.WORKER)
@ -219,7 +219,7 @@ allow {
allow {
{ utils.UPDATE_DESC, utils.UPDATE_ANNOTATIONS, utils.DELETE_ANNOTATIONS,
utils.UPLOAD_DATA, utils.IMPORT_ANNOTATIONS }[input.scope]
utils.UPLOAD_DATA, utils.UPDATE_METADATA, utils.IMPORT_ANNOTATIONS }[input.scope]
utils.is_sandbox
is_task_staff
utils.has_perm(utils.WORKER)
@ -227,7 +227,7 @@ allow {
allow {
{ utils.UPDATE_DESC, utils.UPDATE_ANNOTATIONS, utils.DELETE_ANNOTATIONS,
utils.UPLOAD_DATA, utils.IMPORT_ANNOTATIONS }[input.scope]
utils.UPLOAD_DATA, utils.UPDATE_METADATA, utils.IMPORT_ANNOTATIONS }[input.scope]
input.auth.organization.id == input.resource.organization.id
utils.has_perm(utils.USER)
organizations.has_perm(organizations.MAINTAINER)
@ -235,7 +235,7 @@ allow {
allow {
{ utils.UPDATE_DESC, utils.UPDATE_ANNOTATIONS, utils.DELETE_ANNOTATIONS,
utils.UPLOAD_DATA, utils.IMPORT_ANNOTATIONS }[input.scope]
utils.UPLOAD_DATA, utils.UPDATE_METADATA, utils.IMPORT_ANNOTATIONS }[input.scope]
is_task_staff
input.auth.organization.id == input.resource.organization.id
utils.has_perm(utils.WORKER)

File diff suppressed because it is too large Load Diff

@ -42,6 +42,8 @@ DELETE_ANNOTATIONS := "delete:annotations"
VIEW_DATA := "view:data"
VIEW_COMMITS := "view:commits"
UPLOAD_DATA := "upload:data"
VIEW_METADATA := "view:metadata"
UPDATE_METADATA := "update:metadata"
IMPORT_ANNOTATIONS := "import:annotations"
UPDATE_STATE := "update:state"
UPDATE_STAGE := "update:stage"

@ -473,6 +473,8 @@ class LambdaJob:
results = Results(db_task.id)
for frame in range(db_task.data.size):
if frame in db_task.data.deleted_frames:
continue
annotations = function.invoke(db_task, data={
"frame": frame, "quality": quality, "mapping": mapping,
"threshold": threshold })

129
package-lock.json generated

@ -91,7 +91,7 @@
}
},
"cvat-canvas": {
"version": "2.13.2",
"version": "2.14.0",
"license": "MIT",
"dependencies": {
"@types/polylabel": "^1.0.5",
@ -114,7 +114,7 @@
"devDependencies": {}
},
"cvat-core": {
"version": "5.0.2",
"version": "5.1.0",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",
@ -253,7 +253,7 @@
"devDependencies": {}
},
"cvat-ui": {
"version": "1.37.1",
"version": "1.38.0",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",
@ -5249,16 +5249,6 @@
"node": ">=8"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -5741,13 +5731,19 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001272",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz",
"integrity": "sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
}
"version": "1.0.30001342",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001342.tgz",
"integrity": "sha512-bn6sOCu7L7jcbBbyNhLg0qzXdJ/PMbybZTH/BA6Roet9wxYRm6Tr9D0s0uhLkOZ6MSG+QU6txUgdpr3MXIVqjA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
}
]
},
"node_modules/capture-exit": {
"version": "2.0.0",
@ -9428,13 +9424,6 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -9740,19 +9729,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -15320,13 +15296,6 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"node_modules/nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -24624,25 +24593,6 @@
"node": ">=6"
}
},
"node_modules/webpack-dev-server/node_modules/fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"dependencies": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/webpack-dev-server/node_modules/glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
@ -29233,16 +29183,6 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -29629,9 +29569,9 @@
"requires": {}
},
"caniuse-lite": {
"version": "1.0.30001272",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz",
"integrity": "sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw=="
"version": "1.0.30001342",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001342.tgz",
"integrity": "sha512-bn6sOCu7L7jcbBbyNhLg0qzXdJ/PMbybZTH/BA6Roet9wxYRm6Tr9D0s0uhLkOZ6MSG+QU6txUgdpr3MXIVqjA=="
},
"capture-exit": {
"version": "2.0.0",
@ -32664,13 +32604,6 @@
"flat-cache": "^3.0.4"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@ -32896,12 +32829,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -37141,13 +37068,6 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -44425,17 +44345,6 @@
"locate-path": "^3.0.0"
}
},
"fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",

@ -1,7 +1,7 @@
---
title: 'Analytics Monitoring'
linkTitle: 'Analytics Monitoring'
weight: 27
weight: 28
description: 'Using Analytics to monitor usage statistics.'
---

@ -1,7 +1,7 @@
---
title: 'Command line interface (CLI)'
linkTitle: 'CLI'
weight: 28
weight: 29
description: 'Guide to working with CVAT tasks in the command line interface. This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/utils/cli).'
---

@ -1,7 +1,7 @@
---
title: 'Context images for 2d task'
linkTitle: 'Context images'
weight: 25
weight: 26
description: 'Adding additional contextual images to a task.'
---

@ -1,7 +1,7 @@
---
title: 'Data preparation on the fly'
linkTitle: 'Data preparation on the fly'
weight: 30
weight: 31
---
<!--lint disable heading-style-->

@ -4,7 +4,7 @@
title: 'Simple command line to prepare dataset manifest file'
linkTitle: 'Dataset manifest'
weight: 29
weight: 30
description: This section on [GitHub](https://github.com/openvinotoolkit/cvat/tree/develop/utils/dataset_manifest)
---

@ -0,0 +1,57 @@
---
title: 'Frame deleting'
linkTitle: 'Delete frame from task'
weight: 18
description: 'This section explains how delete and restore frame from a task.'
---
# Delete frame
You can delete current frame from a task.
This frame will not be presented either in the UI or in the exported annotation.
Thus, it is possible to mark corrupted frames that are not subject to annotation.
1. Go to the Job annotation view and than click on the `Delete frame` button.
![](/images/image245.jpg)
1. After that you will be asked to confirm frame deleting.
**Important note:** all annotations from that frame will be deleted, unsaved annotations
will be saved and the frame will be invisible in the annotation view (Until you make it visible in the settings).
If there is some overlap in the task and the deleted frame falls within this interval,
then this will cause this frame to become unavailable in another job as well.
1. When you delete a frame in a job with tracks, you may need adjust some tracks manually. Common adjustments are:
- Add keyframes at the edges of the deleted interval in order for the interpolation to look correct;
- Move keyframe start or end keyframe to the correct side of the deleted interval.
# Configurate deleted frames visability and navigation
If you need to enable showing the deleted frames, you can do it in the settings.
1. Go to the settings and chose `Player` settings.
![](/images/image246.jpg)
1. Click on the `Show deleted frames` checkbox. And close the settings dialog.
![](/images/image247.jpg)
1. Then you will be able to navigate through deleted frames.
But annotation tools will be unavailable. Deleted frames differ in the corresponding overlay.
1. There are view ways to navigate through deleted frames without enabling this option:
- Go to the frame via direct navigation methods: navigation slider or frame input field,
- Go to the frame via direct link.
1. Navigation with step will not count deleted frames.
# Restore deleted frame
You can also restore deleted frames in the task.
1. Turn on deleted frames visability, as it was told in the previous part,
and go to the deleted frame you want to restore.
![](/images/image248.jpg)
2. Click on the `Restore` icon. The frame will be restored immediately.

@ -1,7 +1,7 @@
---
title: 'Export/import datasets and upload annotation'
linkTitle: 'Export/import datasets'
weight: 18
weight: 19
description: 'This section explains how to download and upload datasets
(including annotation, images, and metadata) of projects, tasks, and jobs.'
---

@ -1,7 +1,7 @@
---
title: 'Filter'
linkTitle: 'Filter'
weight: 23
weight: 24
description: 'Guide to using the Filter feature in CVAT.'
---

@ -1,7 +1,7 @@
---
title: 'Review'
linkTitle: 'Review'
weight: 24
weight: 25
description: 'Guide to using the Review mode for task validation.'
---

@ -1,7 +1,7 @@
---
title: 'Serverless tutorial'
linkTitle: 'Serverless tutorial'
weight: 31
weight: 32
---
## Introduction

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save