Grid view and multiple context images supported (#5542)

### Motivation and context
<img width="1918" alt="image"
src="https://user-images.githubusercontent.com/40690378/210207552-7a7dcb0b-4f0c-4cb6-a030-9522ff68a710.png">
<img width="1920" alt="image"
src="https://user-images.githubusercontent.com/40690378/210207577-d05503e8-71d5-4e5c-aecd-03e5a762d7b1.png">
main
Boris Sekachev 3 years ago committed by GitHub
parent bc33ba430c
commit fb0b8675e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(<https://github.com/opencv/cvat/pull/5535>) (<https://github.com/opencv/cvat/pull/5535>)
- \[SDK\] Class to represent a project as a PyTorch dataset - \[SDK\] Class to represent a project as a PyTorch dataset
(<https://github.com/opencv/cvat/pull/5523>) (<https://github.com/opencv/cvat/pull/5523>)
- Grid view and multiple context images supported (<https://github.com/opencv/cvat/pull/5542>)
- Support for custom file to job splits in tasks (server API & SDK only) - Support for custom file to job splits in tasks (server API & SDK only)
(<https://github.com/opencv/cvat/pull/5536>) (<https://github.com/opencv/cvat/pull/5536>)
- \[SDK\] A PyTorch adapter setting to disable cache updates - \[SDK\] A PyTorch adapter setting to disable cache updates

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

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -290,21 +290,6 @@ g.cvat_canvas_shape_occluded {
position: relative; position: relative;
} }
#cvat_canvas_loading_animation {
z-index: 1;
position: absolute;
width: 100%;
height: 100%;
}
#cvat_canvas_loading_circle {
fill-opacity: 0;
stroke: #09c;
stroke-width: 3px;
stroke-dasharray: 50;
animation: loadingAnimation 1s linear infinite;
}
#cvat_canvas_text_content { #cvat_canvas_text_content {
text-rendering: optimizeSpeed; text-rendering: optimizeSpeed;
position: absolute; position: absolute;

@ -527,7 +527,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (typeof exception !== 'number' || exception === this.data.imageID) { if (typeof exception !== 'number' || exception === this.data.imageID) {
this.notify(UpdateReasons.DATA_FAILED); this.notify(UpdateReasons.DATA_FAILED);
} }
throw exception;
}); });
} }

@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -66,7 +66,6 @@ export interface CanvasView {
} }
export class CanvasViewImpl implements CanvasView, Listener { export class CanvasViewImpl implements CanvasView, Listener {
private loadingAnimation: SVGSVGElement;
private text: SVGSVGElement; private text: SVGSVGElement;
private adoptedText: SVG.Container; private adoptedText: SVG.Container;
private background: HTMLCanvasElement; private background: HTMLCanvasElement;
@ -1082,7 +1081,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
}; };
// Create HTML elements // Create HTML elements
this.loadingAnimation = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedText = SVG.adopt((this.text as any) as HTMLElement) as SVG.Container; this.adoptedText = SVG.adopt((this.text as any) as HTMLElement) as SVG.Container;
this.background = window.document.createElement('canvas'); this.background = window.document.createElement('canvas');
@ -1101,8 +1099,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas = window.document.createElement('div'); 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 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'); const gridRect: SVGRectElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'rect');
@ -1129,13 +1125,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
patternUnits: 'userSpaceOnUse', patternUnits: 'userSpaceOnUse',
}); });
// Setup loading animation
this.loadingAnimation.setAttribute('id', 'cvat_canvas_loading_animation');
loadingCircle.setAttribute('id', 'cvat_canvas_loading_circle');
loadingCircle.setAttribute('r', '30');
loadingCircle.setAttribute('cx', '50%');
loadingCircle.setAttribute('cy', '50%');
// Setup grid // Setup grid
this.grid.setAttribute('id', 'cvat_canvas_grid'); this.grid.setAttribute('id', 'cvat_canvas_grid');
this.grid.setAttribute('version', '2'); this.grid.setAttribute('version', '2');
@ -1166,14 +1155,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.setAttribute('id', 'cvat_canvas_wrapper'); this.canvas.setAttribute('id', 'cvat_canvas_wrapper');
// Unite created HTML elements together // Unite created HTML elements together
this.loadingAnimation.appendChild(loadingCircle);
this.grid.appendChild(gridDefs); this.grid.appendChild(gridDefs);
this.grid.appendChild(gridRect); this.grid.appendChild(gridRect);
gridDefs.appendChild(this.gridPattern); gridDefs.appendChild(this.gridPattern);
this.gridPattern.appendChild(this.gridPath); this.gridPattern.appendChild(this.gridPath);
this.canvas.appendChild(this.loadingAnimation);
this.canvas.appendChild(this.text); this.canvas.appendChild(this.text);
this.canvas.appendChild(this.background); this.canvas.appendChild(this.background);
this.canvas.appendChild(this.masksContent); this.canvas.appendChild(this.masksContent);
@ -1412,10 +1399,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} else if (reason === UpdateReasons.IMAGE_CHANGED) { } else if (reason === UpdateReasons.IMAGE_CHANGED) {
const { image } = model; const { image } = model;
if (!image) { if (image) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else {
this.loadingAnimation.classList.add('cvat_canvas_hidden');
const ctx = this.background.getContext('2d'); const ctx = this.background.getContext('2d');
this.background.setAttribute('width', `${image.renderWidth}px`); this.background.setAttribute('width', `${image.renderWidth}px`);
this.background.setAttribute('height', `${image.renderHeight}px`); this.background.setAttribute('height', `${image.renderHeight}px`);

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

@ -238,7 +238,10 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
}) })
.catch((exception: any): void => { .catch((exception: any): void => {
this.data.isFrameUpdating = false; this.data.isFrameUpdating = false;
throw exception; // don't notify when the frame is no longer needed
if (typeof exception !== 'number' || exception === this.data.imageID) {
throw exception;
}
}); });
} }

@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -107,6 +107,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private cube: CuboidModel; private cube: CuboidModel;
private isPerspectiveBeingDragged: boolean; private isPerspectiveBeingDragged: boolean;
private activatedElementID: number | null; private activatedElementID: number | null;
private isCtrlDown: boolean;
private drawnObjects: Record<number, { private drawnObjects: Record<number, {
data: DrawnObjectData; data: DrawnObjectData;
cuboid: CuboidModel; cuboid: CuboidModel;
@ -184,6 +185,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}, },
}; };
this.isCtrlDown = false;
this.action = { this.action = {
scan: null, scan: null,
frameCoordinates: { frameCoordinates: {
@ -263,6 +265,20 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const canvasSideView = this.views.side.renderer.domElement; const canvasSideView = this.views.side.renderer.domElement;
const canvasFrontView = this.views.front.renderer.domElement; const canvasFrontView = this.views.front.renderer.domElement;
[
[canvasPerspectiveView, this.views.perspective.scene],
[canvasTopView, this.views.top.scene],
[canvasSideView, this.views.side.scene],
[canvasFrontView, this.views.front.scene],
].forEach(([view, scene]) => {
Object.defineProperty(view, 'scene', {
value: scene,
enumerable: false,
configurable: false,
writable: false,
});
});
canvasPerspectiveView.addEventListener('contextmenu', (e: MouseEvent): void => { canvasPerspectiveView.addEventListener('contextmenu', (e: MouseEvent): void => {
if (this.model.data.activeElement.clientID !== null) { if (this.model.data.activeElement.clientID !== null) {
this.dispatchEvent( this.dispatchEvent(
@ -330,6 +346,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasPerspectiveView.addEventListener('mousemove', (event: MouseEvent): void => { canvasPerspectiveView.addEventListener('mousemove', (event: MouseEvent): void => {
event.preventDefault(); event.preventDefault();
this.isCtrlDown = event.ctrlKey;
if (this.mode === Mode.DRAG_CANVAS) return; if (this.mode === Mode.DRAG_CANVAS) return;
const canvas = this.views.perspective.renderer.domElement; const canvas = this.views.perspective.renderer.domElement;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
@ -539,7 +556,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
} }
private setDefaultZoom(): void { private setDefaultZoom(): void {
if (this.model.data.activeElement === null) { if (this.model.data.activeElement.clientID === null) {
Object.keys(this.views).forEach((view: string): void => { Object.keys(this.views).forEach((view: string): void => {
const viewType = this.views[view as keyof Views]; const viewType = this.views[view as keyof Views];
if (view !== ViewType.PERSPECTIVE) { if (view !== ViewType.PERSPECTIVE) {
@ -554,7 +571,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasTop.offsetWidth / (bboxtop.max.x - bboxtop.min.x), canvasTop.offsetWidth / (bboxtop.max.x - bboxtop.min.x),
canvasTop.offsetHeight / (bboxtop.max.y - bboxtop.min.y), canvasTop.offsetHeight / (bboxtop.max.y - bboxtop.min.y),
) * 0.4; ) * 0.4;
this.views.top.camera.zoom = x1 / 100; this.views.top.camera.zoom = x1 / 50;
this.views.top.camera.updateProjectionMatrix(); this.views.top.camera.updateProjectionMatrix();
this.views.top.camera.updateMatrix(); this.views.top.camera.updateMatrix();
this.updateHelperPointsSize(ViewType.TOP); this.updateHelperPointsSize(ViewType.TOP);
@ -565,7 +582,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasFront.offsetWidth / (bboxfront.max.y - bboxfront.min.y), canvasFront.offsetWidth / (bboxfront.max.y - bboxfront.min.y),
canvasFront.offsetHeight / (bboxfront.max.z - bboxfront.min.z), canvasFront.offsetHeight / (bboxfront.max.z - bboxfront.min.z),
) * 0.4; ) * 0.4;
this.views.front.camera.zoom = x2 / 100; this.views.front.camera.zoom = x2 / 50;
this.views.front.camera.updateProjectionMatrix(); this.views.front.camera.updateProjectionMatrix();
this.views.front.camera.updateMatrix(); this.views.front.camera.updateMatrix();
this.updateHelperPointsSize(ViewType.FRONT); this.updateHelperPointsSize(ViewType.FRONT);
@ -576,7 +593,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasSide.offsetWidth / (bboxside.max.x - bboxside.min.x), canvasSide.offsetWidth / (bboxside.max.x - bboxside.min.x),
canvasSide.offsetHeight / (bboxside.max.z - bboxside.min.z), canvasSide.offsetHeight / (bboxside.max.z - bboxside.min.z),
) * 0.4; ) * 0.4;
this.views.side.camera.zoom = x3 / 100; this.views.side.camera.zoom = x3 / 50;
this.views.side.camera.updateProjectionMatrix(); this.views.side.camera.updateProjectionMatrix();
this.views.side.camera.updateMatrix(); this.views.side.camera.updateMatrix();
this.updateHelperPointsSize(ViewType.SIDE); this.updateHelperPointsSize(ViewType.SIDE);
@ -842,7 +859,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.activatedElementID = +clientID; this.activatedElementID = +clientID;
this.rotatePlane(null, null); this.rotatePlane(null, null);
this.detachCamera(null); this.detachCamera(null);
this.setDefaultZoom(); [ViewType.TOP, ViewType.SIDE, ViewType.FRONT]
.forEach((type) => this.updateHelperPointsSize(type));
} }
} }
@ -1030,6 +1048,9 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) { } else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.deactivateObject(); this.deactivateObject();
this.activateObject(); this.activateObject();
if (this.activatedElementID) {
this.setDefaultZoom();
}
} else if (reason === UpdateReasons.DRAW) { } else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData; const data: DrawData = this.controller.drawData;
if (Number.isInteger(data.redraw)) { if (Number.isInteger(data.redraw)) {
@ -1385,7 +1406,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const { x, y, z } = intersection.point; const { x, y, z } = intersection.point;
object.position.set(x, y, z); object.position.set(x, y, z);
} }
} else if (this.mode === Mode.IDLE && !this.isPerspectiveBeingDragged) { } else if (this.mode === Mode.IDLE && !this.isPerspectiveBeingDragged && !this.isCtrlDown) {
const { renderer } = this.views.perspective.rayCaster; const { renderer } = this.views.perspective.rayCaster;
const intersects = renderer.intersectObjects(this.getAllVisibleCuboids(), false); const intersects = renderer.intersectObjects(this.getAllVisibleCuboids(), false);
if (intersects.length !== 0) { if (intersects.length !== 0) {

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "7.5.0", "version": "8.0.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts", "main": "src/api.ts",
"scripts": { "scripts": {

@ -3,14 +3,29 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import * as cvatData from 'cvat-data';
import { isBrowser, isNode } from 'browser-or-node'; import { isBrowser, isNode } from 'browser-or-node';
import PluginRegistry from './plugins';
import serverProxy from './server-proxy';
import { Exception, ArgumentError, DataError } from './exceptions';
// This is the frames storage import * as cvatData from 'cvat-data';
const frameDataCache = {}; import { DimensionType } from 'enums';
import PluginRegistry from './plugins';
import serverProxy, { FramesMetaData } from './server-proxy';
import {
Exception, ArgumentError, DataError, ServerError,
} from './exceptions';
// frame storage by job id
const frameDataCache: Record<string, {
meta: FramesMetaData;
chunkSize: number;
mode: 'annotation' | 'interpolation';
startFrame: number;
stopFrame: number;
provider: cvatData.FrameProvider;
frameBuffer: FrameBuffer;
decodedBlocksCacheSize: number;
activeChunkRequest: null;
nextChunkRequest: null;
}> = {};
export class FrameData { export class FrameData {
constructor({ constructor({
@ -23,7 +38,7 @@ export class FrameData {
stopFrame, stopFrame,
decodeForward, decodeForward,
deleted, deleted,
has_related_context: hasRelatedContext, related_files: relatedFiles,
}) { }) {
Object.defineProperties( Object.defineProperties(
this, this,
@ -48,8 +63,8 @@ export class FrameData {
value: frameNumber, value: frameNumber,
writable: false, writable: false,
}, },
hasRelatedContext: { relatedFiles: {
value: hasRelatedContext, value: relatedFiles,
writable: false, writable: false,
}, },
startFrame: { startFrame: {
@ -300,7 +315,7 @@ FrameData.prototype.data.implementation = async function (onServerRequest) {
}); });
}; };
function getFrameMeta(jobID, frame) { function getFrameMeta(jobID, frame): FramesMetaData['frames'][0] {
const { meta, mode, startFrame } = frameDataCache[jobID]; const { meta, mode, startFrame } = frameDataCache[jobID];
let size = null; let size = null;
if (mode === 'interpolation') { if (mode === 'interpolation') {
@ -314,6 +329,7 @@ function getFrameMeta(jobID, frame) {
} else { } else {
throw new DataError(`Invalid mode is specified ${mode}`); throw new DataError(`Invalid mode is specified ${mode}`);
} }
return size; return size;
} }
@ -329,16 +345,46 @@ class FrameBuffer {
this._jobID = jobID; this._jobID = jobID;
} }
isContextImageAvailable(frame) { addContextImage(frame, data): void {
return frame in this._contextImage; const promise = new Promise<void>((resolve, reject) => {
data.then((resolvedData) => {
const meta = getFrameMeta(this._jobID, frame);
return cvatData
.decodeZip(resolvedData, 0, meta.related_files, cvatData.DimensionType.DIMENSION_2D);
}).then((decodedData) => {
this._contextImage[frame] = decodedData;
resolve();
}).catch((error: Error) => {
if (error instanceof ServerError && (error as any).code === 404) {
this._contextImage[frame] = {};
resolve();
} else {
reject(error);
}
});
});
this._contextImage[frame] = promise;
} }
getContextImage(frame) { isContextImageAvailable(frame): boolean {
return this._contextImage[frame] || null; return frame in this._contextImage;
} }
addContextImage(frame, data) { getContextImage(frame): Promise<ImageBitmap[]> {
this._contextImage[frame] = data; return new Promise((resolve) => {
if (frame in this._contextImage) {
if (this._contextImage[frame] instanceof Promise) {
this._contextImage[frame].then(() => {
resolve(this.getContextImage(frame));
});
} else {
resolve({ ...this._contextImage[frame] });
}
} else {
resolve([]);
}
});
} }
getFreeBufferSize() { getFreeBufferSize() {
@ -477,7 +523,7 @@ class FrameBuffer {
} }
} }
async require(frameNumber, jobID, fillBuffer, frameStep) { async require(frameNumber: number, jobID: number, fillBuffer: boolean, frameStep: number): FrameData {
for (const frame in this._buffer) { for (const frame in this._buffer) {
if (+frame < frameNumber || +frame >= frameNumber + this._size * frameStep) { if (+frame < frameNumber || +frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame]; delete this._buffer[frame];
@ -554,11 +600,7 @@ async function getImageContext(jobID, frame) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
resolve(global.Buffer.from(result, 'binary').toString('base64')); resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) { } else if (isBrowser) {
const reader = new FileReader(); resolve(result);
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(result);
} }
}) })
.catch((error) => { .catch((error) => {
@ -572,7 +614,7 @@ export async function getContextImage(jobID, frame) {
return frameDataCache[jobID].frameBuffer.getContextImage(frame); return frameDataCache[jobID].frameBuffer.getContextImage(frame);
} }
const response = getImageContext(jobID, frame); const response = getImageContext(jobID, frame);
frameDataCache[jobID].frameBuffer.addContextImage(frame, response); await frameDataCache[jobID].frameBuffer.addContextImage(frame, response);
return frameDataCache[jobID].frameBuffer.getContextImage(frame); return frameDataCache[jobID].frameBuffer.getContextImage(frame);
} }
@ -600,16 +642,16 @@ export async function getPreview(taskID = null, jobID = null) {
} }
export async function getFrame( export async function getFrame(
jobID, jobID: number,
chunkSize, chunkSize: number,
chunkType, chunkType: 'video' | 'imageset',
mode, mode: 'interpolation' | 'annotation', // todo: obsolete, need to remove
frame, frame: number,
startFrame, startFrame: number,
stopFrame, stopFrame: number,
isPlaying, isPlaying: boolean,
step, step: number,
dimension, dimension: DimensionType,
) { ) {
if (!(jobID in frameDataCache)) { if (!(jobID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE; const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE;
@ -648,8 +690,9 @@ export async function getFrame(
activeChunkRequest: null, activeChunkRequest: null,
nextChunkRequest: null, nextChunkRequest: null,
}; };
// relevant only for video chunks
const frameMeta = getFrameMeta(jobID, frame); const frameMeta = getFrameMeta(jobID, frame);
// actual only for video chunks
frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height); frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height);
} }

@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -1384,7 +1384,7 @@ async function getImageContext(jid, frame) {
number: frame, number: frame,
}, },
proxy: config.proxy, proxy: config.proxy,
responseType: 'blob', responseType: 'arraybuffer',
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
@ -1423,7 +1423,23 @@ async function getData(tid, jid, chunk) {
return response; return response;
} }
async function getMeta(session, jid) { export interface FramesMetaData {
chunk_size: number;
deleted_frames: number[];
frame_filter: string;
frames: {
width: number;
height: number;
name: string;
related_files: number;
}[];
image_quality: number;
size: number;
start_frame: number;
stop_frame: number;
}
async function getMeta(session, jid): Promise<FramesMetaData> {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;

@ -2963,22 +2963,22 @@ const frameMetaDummyData = {
width: 2560, width: 2560,
height: 1703, height: 1703,
name: '1598296101_1033667.jpg', name: '1598296101_1033667.jpg',
has_related_context: false related_files: 0
}, { }, {
width: 1600, width: 1600,
height: 1200, height: 1200,
name: '30fdce7f27b9c7b1d50108d7c16d23ef.jpg', name: '30fdce7f27b9c7b1d50108d7c16d23ef.jpg',
has_related_context: false related_files: 0
}, { }, {
width: 2880, width: 2880,
height: 1800, height: 1800,
name: '567362-ily-comedy-drama-1finding-3.jpg', name: '567362-ily-comedy-drama-1finding-3.jpg',
has_related_context: false related_files: 0
}, { }, {
width: 1920, width: 1920,
height: 1080, height: 1080,
name: '730443-under-the-sea-wallpapers-1920x1080-windows-10.jpg', name: '730443-under-the-sea-wallpapers-1920x1080-windows-10.jpg',
has_related_context: false related_files: 0
}], }],
deleted_frames: [] deleted_frames: []
}, },

@ -55,7 +55,7 @@ const webConfig = {
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].min.js', filename: '[name].[contenthash].min.js',
library: 'cvat', library: 'cvat-core.js',
libraryTarget: 'window', libraryTarget: 'window',
}, },
resolve: { resolve: {

@ -1,6 +1,6 @@
{ {
"name": "cvat-data", "name": "cvat-data",
"version": "1.0.2", "version": "1.1.0",
"description": "", "description": "",
"main": "src/ts/cvat-data.ts", "main": "src/ts/cvat-data.ts",
"scripts": { "scripts": {

@ -1,139 +1,235 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
// eslint-disable-next-line max-classes-per-file
import { MP4Reader, Bytestream } from './3rdparty/mp4'; import { MP4Reader, Bytestream } from './3rdparty/mp4';
import ZipDecoder from './unzip_imgs.worker'; import ZipDecoder from './unzip_imgs.worker';
import H264Decoder from './3rdparty/Decoder.worker'; import H264Decoder from './3rdparty/Decoder.worker';
export const BlockType = Object.freeze({ export enum BlockType {
MP4VIDEO: 'mp4video', MP4VIDEO = 'mp4video',
ARCHIVE: 'archive', ARCHIVE = 'archive',
}); }
export enum DimensionType {
DIMENSION_3D = '3d',
DIMENSION_2D = '2d',
}
export function decodeZip(
block: any, start: number, end: number, dimension: any,
): Promise<Record<string, ImageBitmap>> {
return new Promise((resolve, reject) => {
decodeZip.mutex.acquire().then((release) => {
const worker = new ZipDecoder();
const result: Record<string, ImageBitmap> = {};
let decoded = 0;
worker.onerror = (e: ErrorEvent) => {
release();
worker.terminate();
reject(new Error(`Archive can not be decoded. ${e.message}`));
};
worker.onmessage = async (event) => {
const { error, fileName } = event.data;
if (error) {
worker.onerror(new ErrorEvent('error', { message: error.toString() }));
}
const { data } = event.data;
result[fileName.split('.')[0]] = data;
decoded++;
if (decoded === end) {
release();
worker.terminate();
resolve(result);
}
};
worker.postMessage({
block,
start,
end,
dimension,
dimension2D: DimensionType.DIMENSION_2D,
});
});
});
}
export const DimensionType = Object.freeze({ decodeZip.mutex = new Mutex();
DIM_3D: '3d',
DIM_2D: '2d', interface BlockToDecode {
}); start: number;
end: number;
block: ArrayBuffer;
resolveCallback: (frame: number) => void;
rejectCallback: (e: ErrorEvent) => void;
}
export class FrameProvider { export class FrameProvider {
private blocksRanges: string[];
private blockSize: number;
private blockType: BlockType;
/*
ImageBitmap when decode zip chunks
ImageData when decode video chunks
Blob when 3D dimension
null when not decoded yet
*/
private frames: Record<string, ImageBitmap | ImageData | Blob | null>;
private requestedBlockToDecode: null | BlockToDecode;
private blocksAreBeingDecoded: Record<string, BlockToDecode>;
private promisedFrames: Record<string, {
resolve: (data: ImageBitmap | ImageData | Blob) => void;
reject: () => void;
}>;
private currentDecodingThreads: number;
private currentFrame: number;
private mutex: Mutex;
private dimension: DimensionType;
private workerThreadsLimit: number;
private cachedEncodedBlocksLimit: number;
private cachedDecodedBlocksLimit: number;
// used for video chunks to resize after decoding
private renderWidth: number;
private renderHeight: number;
constructor( constructor(
blockType, blockType: BlockType,
blockSize, blockSize: number,
cachedBlockCount, cachedBlockCount: number,
decodedBlocksCacheSize = 5, decodedBlocksCacheSize = 5,
maxWorkerThreadCount = 2, maxWorkerThreadCount = 2,
dimension = DimensionType.DIM_2D, dimension: DimensionType = DimensionType.DIMENSION_2D,
) { ) {
this._frames = {}; this.mutex = new Mutex();
this._cachedBlockCount = Math.max(1, cachedBlockCount); // number of stored blocks this.blocksRanges = [];
this._decodedBlocksCacheSize = decodedBlocksCacheSize; this.frames = {};
this._blocksRanges = []; this.promisedFrames = {};
this.currentDecodingThreads = 0;
this.currentFrame = -1;
this.cachedEncodedBlocksLimit = Math.max(1, cachedBlockCount); // number of stored blocks
this.cachedDecodedBlocksLimit = decodedBlocksCacheSize;
this.workerThreadsLimit = maxWorkerThreadCount;
this.dimension = dimension;
this.renderWidth = 1920;
this.renderHeight = 1080;
this.blockSize = blockSize;
this.blockType = blockType;
// todo: sort out with logic of blocks
this._blocks = {}; this._blocks = {};
this._running = false; this.requestedBlockToDecode = null;
this._blockSize = blockSize; this.blocksAreBeingDecoded = {};
this._blockType = blockType;
this._currFrame = -1; setTimeout(this._checkDecodeRequests.bind(this), 100);
this._requestedBlockDecode = null;
this._width = null;
this._height = null;
this._decodingBlocks = {};
this._decodeThreadCount = 0;
this._timerId = setTimeout(this._worker.bind(this), 100);
this._mutex = new Mutex();
this._promisedFrames = {};
this._maxWorkerThreadCount = maxWorkerThreadCount;
this._dimension = dimension;
} }
async _worker() { _checkDecodeRequests(): void {
if (this._requestedBlockDecode !== null && this._decodeThreadCount < this._maxWorkerThreadCount) { if (this.requestedBlockToDecode !== null && this.currentDecodingThreads < this.workerThreadsLimit) {
await this.startDecode(); this.startDecode().then(() => {
setTimeout(this._checkDecodeRequests.bind(this), 100);
});
} else {
setTimeout(this._checkDecodeRequests.bind(this), 100);
} }
this._timerId = setTimeout(this._worker.bind(this), 100);
} }
isChunkCached(start, end) { isChunkCached(start: number, end: number): boolean {
return `${start}:${end}` in this._blocksRanges; // todo: always returns false because this.blocksRanges is Array, not dictionary
// but if try to correct other errors happens, need to debug..
return `${start}:${end}` in this.blocksRanges;
} }
/* This method removes extra data from a cache when memory overflow */ /* This method removes extra data from a cache when memory overflow */
async _cleanup() { async _cleanup(): Promise<void> {
if (this._blocksRanges.length > this._cachedBlockCount) { if (this.blocksRanges.length > this.cachedEncodedBlocksLimit) {
const shifted = this._blocksRanges.shift(); // get the oldest block const shifted = this.blocksRanges.shift(); // get the oldest block
const [start, end] = shifted.split(':').map((el) => +el); const [start, end] = shifted.split(':').map((el) => +el);
delete this._blocks[start / this._blockSize]; delete this._blocks[Math.floor(start / this.blockSize)];
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
delete this._frames[i]; delete this.frames[i];
} }
} }
// delete frames whose are not in areas of current frame // delete frames whose are not in areas of current frame
const distance = Math.floor(this._decodedBlocksCacheSize / 2); const distance = Math.floor(this.cachedDecodedBlocksLimit / 2);
for (let i = 0; i < this._blocksRanges.length; i++) { for (let i = 0; i < this.blocksRanges.length; i++) {
const [start, end] = this._blocksRanges[i].split(':').map((el) => +el); const [start, end] = this.blocksRanges[i].split(':').map((el) => +el);
if ( if (
end < this._currFrame - distance * this._blockSize || end < this.currentFrame - distance * this.blockSize ||
start > this._currFrame + distance * this._blockSize start > this.currentFrame + distance * this.blockSize
) { ) {
for (let j = start; j <= end; j++) { for (let j = start; j <= end; j++) {
delete this._frames[j]; delete this.frames[j];
} }
} }
} }
} }
async requestDecodeBlock(block, start, end, resolveCallback, rejectCallback) { async requestDecodeBlock(
const release = await this._mutex.acquire(); block: ArrayBuffer,
start: number,
end: number,
resolveCallback: () => void,
rejectCallback: () => void,
): Promise<void> {
const release = await this.mutex.acquire();
try { try {
if (this._requestedBlockDecode !== null) { if (this.requestedBlockToDecode !== null) {
if (start === this._requestedBlockDecode.start && end === this._requestedBlockDecode.end) { if (start === this.requestedBlockToDecode.start && end === this.requestedBlockToDecode.end) {
this._requestedBlockDecode.resolveCallback = resolveCallback; // only rewrite callbacks if the same block was requested again
this._requestedBlockDecode.rejectCallback = rejectCallback; this.requestedBlockToDecode.resolveCallback = resolveCallback;
} else if (this._requestedBlockDecode.rejectCallback) { this.requestedBlockToDecode.rejectCallback = rejectCallback;
this._requestedBlockDecode.rejectCallback();
// todo: should we reject the previous request here?
} else if (this.requestedBlockToDecode.rejectCallback) {
// if another block requested, the previous request should be rejected
this.requestedBlockToDecode.rejectCallback();
} }
} }
if (!(`${start}:${end}` in this._decodingBlocks)) {
this._requestedBlockDecode = { if (!(`${start}:${end}` in this.blocksAreBeingDecoded)) {
block: block || this._blocks[Math.floor(start / this._blockSize)], this.requestedBlockToDecode = {
block: block || this._blocks[Math.floor(start / this.blockSize)],
start, start,
end, end,
resolveCallback, resolveCallback,
rejectCallback, rejectCallback,
}; };
} else { } else {
this._decodingBlocks[`${start}:${end}`].rejectCallback = rejectCallback; this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback = rejectCallback;
this._decodingBlocks[`${start}:${end}`].resolveCallback = resolveCallback; this.blocksAreBeingDecoded[`${start}:${end}`].resolveCallback = resolveCallback;
} }
} finally { } finally {
release(); release();
} }
} }
isRequestExist() { setRenderSize(width: number, height: number): void {
return this._requestedBlockDecode !== null; this.renderWidth = width;
} this.renderHeight = height;
setRenderSize(width, height) {
this._width = width;
this._height = height;
} }
/* Method returns frame from collection. Else method returns 0 */ /* Method returns frame from collection. Else method returns null */
async frame(frameNumber) { async frame(frameNumber: number): Promise<ImageBitmap | ImageData | Blob> {
this._currFrame = frameNumber; this.currentFrame = frameNumber;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (frameNumber in this._frames) { if (frameNumber in this.frames) {
if (this._frames[frameNumber] !== null) { if (this.frames[frameNumber] !== null) {
resolve(this._frames[frameNumber]); resolve(this.frames[frameNumber]);
} else { } else {
this._promisedFrames[frameNumber] = { this.promisedFrames[frameNumber] = { resolve, reject };
resolve,
reject,
};
} }
} else { } else {
resolve(null); resolve(null);
@ -141,30 +237,24 @@ export class FrameProvider {
}); });
} }
isNextChunkExists(frameNumber) { isNextChunkExists(frameNumber: number): boolean {
const nextChunkNum = Math.floor(frameNumber / this._blockSize) + 1; const nextChunkNum = Math.floor(frameNumber / this.blockSize) + 1;
if (this._blocks[nextChunkNum] === 'loading') {
return true;
}
return nextChunkNum in this._blocks; return nextChunkNum in this._blocks;
} }
/* setReadyToLoading(chunkNumber: number): void {
Method start asynchronic decode a block of data
@param block - is a data from a server as is (ts file or archive)
@param start {number} - is the first frame of a block
@param end {number} - is the last frame of a block + 1
@param callback - callback)
*/
setReadyToLoading(chunkNumber) {
this._blocks[chunkNumber] = 'loading'; this._blocks[chunkNumber] = 'loading';
} }
static cropImage(imageBuffer, imageWidth, imageHeight, xOffset, yOffset, width, height) { static cropImage(
imageBuffer: ArrayBuffer,
imageWidth: number,
imageHeight: number,
xOffset: number,
yOffset: number,
width: number,
height: number,
): ImageData {
if (xOffset === 0 && width === imageWidth && yOffset === 0 && height === imageHeight) { if (xOffset === 0 && width === imageWidth && yOffset === 0 && height === imageHeight) {
return new ImageData(new Uint8ClampedArray(imageBuffer), width, height); return new ImageData(new Uint8ClampedArray(imageBuffer), width, height);
} }
@ -189,22 +279,26 @@ export class FrameProvider {
return new ImageData(rgbaInt8Clamped, width, height); return new ImageData(rgbaInt8Clamped, width, height);
} }
async startDecode() { async startDecode(): Promise<void> {
const release = await this._mutex.acquire(); const release = await this.mutex.acquire();
try { try {
const height = this._height; const height = this.renderHeight;
const width = this._width; const width = this.renderWidth;
const { start, end, block } = this._requestedBlockDecode; const { start, end, block } = this.requestedBlockToDecode;
this._blocksRanges.push(`${start}:${end}`); this.blocksRanges.push(`${start}:${end}`);
this._decodingBlocks[`${start}:${end}`] = this._requestedBlockDecode; this.blocksAreBeingDecoded[`${start}:${end}`] = this.requestedBlockToDecode;
this._requestedBlockDecode = null; this.requestedBlockToDecode = null;
this._blocks[Math.floor((start + 1) / this._blockSize)] = block; this._blocks[Math.floor((start + 1) / this.blockSize)] = block;
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i++) {
this._frames[i] = null; this.frames[i] = null;
} }
this._cleanup(); this._cleanup();
if (this._blockType === BlockType.MP4VIDEO) { this.currentDecodingThreads++;
if (this.blockType === BlockType.MP4VIDEO) {
const worker = new H264Decoder(); const worker = new H264Decoder();
let index = start; let index = start;
@ -214,8 +308,8 @@ export class FrameProvider {
return; return;
} }
const scaleFactor = Math.ceil(this._height / e.data.height); const scaleFactor = Math.ceil(height / e.data.height);
this._frames[index] = FrameProvider.cropImage( this.frames[index] = FrameProvider.cropImage(
e.data.buf, e.data.buf,
e.data.width, e.data.width,
e.data.height, e.data.height,
@ -225,37 +319,44 @@ export class FrameProvider {
Math.floor(height / scaleFactor), Math.floor(height / scaleFactor),
); );
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) { const { resolveCallback } = this.blocksAreBeingDecoded[`${start}:${end}`];
this._decodingBlocks[`${start}:${end}`].resolveCallback(index); if (resolveCallback) {
resolveCallback(index);
} }
if (index in this._promisedFrames) { if (index in this.promisedFrames) {
this._promisedFrames[index].resolve(this._frames[index]); const { resolve } = this.promisedFrames[index];
delete this._promisedFrames[index]; delete this.promisedFrames[index];
resolve(this.frames[index]);
} }
if (index === end) { if (index === end) {
this._decodeThreadCount--;
delete this._decodingBlocks[`${start}:${end}`];
worker.terminate(); worker.terminate();
this.currentDecodingThreads--;
delete this.blocksAreBeingDecoded[`${start}:${end}`];
} }
index++; index++;
}; };
worker.onerror = (e) => { worker.onerror = (e: ErrorEvent) => {
worker.terminate(); worker.terminate();
this._decodeThreadCount--; this.currentDecodingThreads--;
for (let i = index; i <= end; i++) { for (let i = index; i <= end; i++) {
if (i in this._promisedFrames) { // reject all the following frames
this._promisedFrames[i].reject(); if (i in this.promisedFrames) {
delete this._promisedFrames[i]; const { reject } = this.promisedFrames[i];
delete this.promisedFrames[i];
reject();
} }
} }
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) { if (this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback) {
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e)); this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback(e);
} }
delete this._decodingBlocks[`${start}:${end}`];
delete this.blocksAreBeingDecoded[`${start}:${end}`];
}; };
worker.postMessage({ worker.postMessage({
@ -284,92 +385,69 @@ export class FrameProvider {
worker.postMessage({ buf: nal, offset: 0, length: nal.length }); worker.postMessage({ buf: nal, offset: 0, length: nal.length });
}); });
} }
this._decodeThreadCount++;
} else { } else {
const worker = new ZipDecoder(); const worker = new ZipDecoder();
let index = start; let index = start;
worker.onerror = (e) => {
for (let i = start; i <= end; i++) {
if (i in this._promisedFrames) {
this._promisedFrames[i].reject();
delete this._promisedFrames[i];
}
}
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) {
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e));
}
this._decodeThreadCount--;
worker.terminate();
};
worker.onmessage = async (event) => { worker.onmessage = async (event) => {
if (this._dimension === DimensionType.DIM_2D && event.data.isRaw) { this.frames[event.data.index] = event.data.data;
// safary doesn't support createImageBitmap
// there is a way to polyfill it with using document.createElement
// but document.createElement doesn't work in worker
// so, we get raw data and decode it here, no other way
const createImageBitmap = async function (blob) {
return new Promise((resolve) => {
const img = document.createElement('img');
img.addEventListener('load', function loadListener() {
resolve(this);
});
img.src = URL.createObjectURL(blob);
});
};
// eslint-disable-next-line
event.data.data = await createImageBitmap(event.data.data);
}
this._frames[event.data.index] = event.data.data; const { resolveCallback } = this.blocksAreBeingDecoded[`${start}:${end}`];
if (resolveCallback) {
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) { resolveCallback(event.data.index);
this._decodingBlocks[`${start}:${end}`].resolveCallback(event.data.index);
} }
if (event.data.index in this._promisedFrames) { if (event.data.index in this.promisedFrames) {
this._promisedFrames[event.data.index].resolve(this._frames[event.data.index]); const { resolve } = this.promisedFrames[event.data.index];
delete this._promisedFrames[event.data.index]; delete this.promisedFrames[event.data.index];
resolve(this.frames[event.data.index]);
} }
if (index === end) { if (index === end) {
worker.terminate(); worker.terminate();
delete this._decodingBlocks[`${start}:${end}`]; this.currentDecodingThreads--;
this._decodeThreadCount--; delete this.blocksAreBeingDecoded[`${start}:${end}`];
} }
index++; index++;
}; };
const dimension = this._dimension;
worker.onerror = (e: ErrorEvent) => {
for (let i = start; i <= end; i++) {
if (i in this.promisedFrames) {
const { reject } = this.promisedFrames[i];
delete this.promisedFrames[i];
reject();
}
}
if (this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback) {
this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback(e);
}
this.currentDecodingThreads--;
worker.terminate();
};
worker.postMessage({ worker.postMessage({
block, block,
start, start,
end, end,
dimension, dimension: this.dimension,
dimension2D: DimensionType.DIM_2D, dimension2D: DimensionType.DIMENSION_2D,
}); });
this._decodeThreadCount++;
} }
} finally { } finally {
release(); release();
} }
} }
get decodeThreadCount() { get decodedBlocksCacheSize(): number {
return this._decodeThreadCount; return this.cachedDecodedBlocksLimit;
}
get decodedBlocksCacheSize() {
return this._decodedBlocksCacheSize;
} }
/* /*
Method returns a list of cached ranges Method returns a list of cached ranges
Is an array of strings like "start:end" Is an array of strings like "start:end"
*/ */
get cachedFrames() { get cachedFrames(): string[] {
return [...this._blocksRanges].sort((a, b) => a.split(':')[0] - b.split(':')[0]); return [...this.blocksRanges].sort((a, b) => +a.split(':')[0] - +b.split(':')[0]);
} }
} }

@ -1,7 +1,9 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// eslint-disable-next-line @typescript-eslint/no-var-requires
const JSZip = require('jszip'); const JSZip = require('jszip');
onmessage = (e) => { onmessage = (e) => {
@ -19,8 +21,7 @@ onmessage = (e) => {
_zip.file(relativePath) _zip.file(relativePath)
.async('blob') .async('blob')
.then((fileData) => { .then((fileData) => {
// eslint-disable-next-line no-restricted-globals if (dimension === dimension2D) {
if (dimension === dimension2D && self.createImageBitmap) {
createImageBitmap(fileData).then((img) => { createImageBitmap(fileData).then((img) => {
postMessage({ postMessage({
fileName: relativePath, fileName: relativePath,
@ -33,12 +34,11 @@ onmessage = (e) => {
fileName: relativePath, fileName: relativePath,
index: fileIndex, index: fileIndex,
data: fileData, data: fileData,
isRaw: true,
}); });
} }
}); });
} }
}); });
}); }).catch((error) => postMessage({ error }));
} }
}; };

@ -13,7 +13,7 @@ const cvatData = {
target: 'web', target: 'web',
mode: 'production', mode: 'production',
entry: { entry: {
'cvat-data': './src/js/cvat-data.ts', 'cvat-data': './src/ts/cvat-data.ts',
}, },
output: { output: {
path: path.resolve(__dirname, 'dist'), path: path.resolve(__dirname, 'dist'),
@ -66,7 +66,7 @@ const cvatData = {
}, },
], ],
}, },
plugins: [new CopyPlugin({ patterns: ['./src/js/3rdparty/avc.wasm'] })], plugins: [new CopyPlugin({ patterns: ['./src/ts/3rdparty/avc.wasm'] })],
}; };
module.exports = cvatData; module.exports = cvatData;

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.46.1", "version": "1.47.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {
@ -26,8 +26,8 @@
"@types/react": "^16.14.15", "@types/react": "^16.14.15",
"@types/react-color": "^3.0.5", "@types/react-color": "^3.0.5",
"@types/react-dom": "^16.9.14", "@types/react-dom": "^16.9.14",
"@types/react-grid-layout": "^1.3.2",
"@types/react-redux": "^7.1.18", "@types/react-redux": "^7.1.18",
"@types/react-resizable": "^3.0.1",
"@types/react-router": "^5.1.16", "@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.9", "@types/react-router-dom": "^5.1.9",
"@types/react-share": "^3.0.3", "@types/react-share": "^3.0.3",
@ -50,10 +50,10 @@
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^4.0.3", "react-cookie": "^4.0.3",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-grid-layout": "^1.3.4",
"react-markdown": "^8.0.4", "react-markdown": "^8.0.4",
"react-moment": "^1.1.1", "react-moment": "^1.1.1",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-resizable": "^3.0.4",
"react-router": "^5.1.0", "react-router": "^5.1.0",
"react-router-dom": "^5.1.0", "react-router-dom": "^5.1.0",
"react-share": "^4.4.0", "react-share": "^4.4.0",

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -125,7 +125,6 @@ export enum AnnotationActionTypes {
SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS', SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS',
SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS', SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS',
SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED', SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED',
SAVE_UPDATE_ANNOTATIONS_STATUS = 'SAVE_UPDATE_ANNOTATIONS_STATUS',
SWITCH_PLAY = 'SWITCH_PLAY', SWITCH_PLAY = 'SWITCH_PLAY',
CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY', CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY',
DRAG_CANVAS = 'DRAG_CANVAS', DRAG_CANVAS = 'DRAG_CANVAS',
@ -196,10 +195,6 @@ export enum AnnotationActionTypes {
GET_PREDICTIONS = 'GET_PREDICTIONS', GET_PREDICTIONS = 'GET_PREDICTIONS',
GET_PREDICTIONS_FAILED = 'GET_PREDICTIONS_FAILED', GET_PREDICTIONS_FAILED = 'GET_PREDICTIONS_FAILED',
GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS', GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS',
HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE_SUCCESS = 'GET_CONTEXT_IMAGE_SUCCESS',
GET_CONTEXT_IMAGE_FAILED = 'GET_CONTEXT_IMAGE_FAILED',
SWITCH_NAVIGATION_BLOCKED = 'SWITCH_NAVIGATION_BLOCKED', SWITCH_NAVIGATION_BLOCKED = 'SWITCH_NAVIGATION_BLOCKED',
DELETE_FRAME = 'DELETE_FRAME', DELETE_FRAME = 'DELETE_FRAME',
DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS', DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS',
@ -700,7 +695,7 @@ export function changeFrameAsync(
number: currentState.annotation.player.frame.number, number: currentState.annotation.player.frame.number,
data: currentState.annotation.player.frame.data, data: currentState.annotation.player.frame.data,
filename: currentState.annotation.player.frame.filename, filename: currentState.annotation.player.frame.filename,
hasRelatedContext: currentState.annotation.player.frame.hasRelatedContext, relatedFiles: currentState.annotation.player.frame.relatedFiles,
delay: currentState.annotation.player.frame.delay, delay: currentState.annotation.player.frame.delay,
changeTime: currentState.annotation.player.frame.changeTime, changeTime: currentState.annotation.player.frame.changeTime,
states: currentState.annotation.annotations.states, states: currentState.annotation.annotations.states,
@ -767,7 +762,7 @@ export function changeFrameAsync(
number: toFrame, number: toFrame,
data, data,
filename: data.filename, filename: data.filename,
hasRelatedContext: data.hasRelatedContext, relatedFiles: data.relatedFiles,
states, states,
minZ, minZ,
maxZ, maxZ,
@ -1046,7 +1041,7 @@ export function getJobAsync(
states, states,
frameNumber, frameNumber,
frameFilename: frameData.filename, frameFilename: frameData.filename,
frameHasRelatedContext: frameData.hasRelatedContext, relatedFiles: frameData.relatedFiles,
frameData, frameData,
colors, colors,
filters, filters,
@ -1113,19 +1108,8 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
try { try {
const saveJobEvent = await sessionInstance.logger.log(LogType.saveJob, {}, true); 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.frames.save();
await sessionInstance.annotations.save((status: string) => { await sessionInstance.annotations.save();
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
payload: {
status,
},
});
});
await saveJobEvent.close(); await saveJobEvent.close();
await sessionInstance.logger.log(LogType.sendTaskInfo, await jobInfoGenerator(sessionInstance)); await sessionInstance.logger.log(LogType.sendTaskInfo, await jobInfoGenerator(sessionInstance));
dispatch(saveLogsAsync()); dispatch(saveLogsAsync());
@ -1667,40 +1651,6 @@ export function switchPredictor(predictorEnabled: boolean): AnyAction {
}, },
}; };
} }
export function hideShowContextImage(hidden: boolean): AnyAction {
return {
type: AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE,
payload: {
hidden,
},
};
}
export function getContextImageAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job;
const { number: frameNumber } = state.annotation.player.frame;
try {
dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE,
payload: {},
});
const contextImageData = await job.frames.contextImage(frameNumber);
dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData },
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED,
payload: { error },
});
}
};
}
export function switchNavigationBlocked(navigationBlocked: boolean): AnyAction { export function switchNavigationBlocked(navigationBlocked: boolean): AnyAction {
return { return {

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -24,7 +24,7 @@ export const boundariesActions = {
openTime: number; openTime: number;
frameNumber: number; frameNumber: number;
frameFilename: string; frameFilename: string;
frameHasRelatedContext: boolean; relatedFiles: boolean;
colors: string[]; colors: string[];
filters: string[]; filters: string[];
frameData: any; frameData: any;
@ -58,7 +58,7 @@ export function resetAfterErrorAsync(): ThunkAction {
openTime: state.annotation.job.openTime || Date.now(), openTime: state.annotation.job.openTime || Date.now(),
frameNumber, frameNumber,
frameFilename: frameData.filename, frameFilename: frameData.filename,
frameHasRelatedContext: frameData.hasRelatedContext, relatedFiles: frameData.relatedFiles,
colors, colors,
filters: [], filters: [],
frameData, frameData,

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,7 +7,6 @@ $grid-unit-size: 8px;
$header-height: $grid-unit-size * 6; $header-height: $grid-unit-size * 6;
$layout-sm-grid-size: $grid-unit-size * 0.5;
$layout-lg-grid-size: $grid-unit-size * 2; $layout-lg-grid-size: $grid-unit-size * 2;
$layout-sm-grid-color: rgba(0, 0, 0, 0.15); $layout-sm-grid-color: rgba(0, 0, 0, 0.15);
$layout-lg-grid-color: rgba(0, 0, 0, 0.15); $layout-lg-grid-color: rgba(0, 0, 0, 0.15);

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -139,31 +140,13 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout.Header className='cvat-annotation-header'> <Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer /> <AnnotationTopBarContainer />
</Layout.Header> </Layout.Header>
{workspace === Workspace.STANDARD3D && ( <Layout.Content className='cvat-annotation-layout-content'>
<Layout.Content className='cvat-annotation-layout-content'> {workspace === Workspace.STANDARD3D && <StandardWorkspace3DComponent />}
<StandardWorkspace3DComponent /> {workspace === Workspace.STANDARD && <StandardWorkspaceComponent />}
</Layout.Content> {workspace === Workspace.ATTRIBUTE_ANNOTATION && <AttributeAnnotationWorkspace />}
)} {workspace === Workspace.TAG_ANNOTATION && <TagAnnotationWorkspace />}
{workspace === Workspace.STANDARD && ( {workspace === Workspace.REVIEW_WORKSPACE && <ReviewAnnotationsWorkspace />}
<Layout.Content className='cvat-annotation-layout-content'> </Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
)}
{workspace === Workspace.ATTRIBUTE_ANNOTATION && (
<Layout.Content className='cvat-annotation-layout-content'>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
{workspace === Workspace.TAG_ANNOTATION && (
<Layout.Content className='cvat-annotation-layout-content'>
<TagAnnotationWorkspace />
</Layout.Content>
)}
{workspace === Workspace.REVIEW_WORKSPACE && (
<Layout.Content className='cvat-annotation-layout-content'>
<ReviewAnnotationsWorkspace />
</Layout.Content>
)}
<FiltersModalComponent /> <FiltersModalComponent />
<StatisticsModalComponent /> <StatisticsModalComponent />
</Layout> </Layout>

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -11,8 +11,6 @@ import Text from 'antd/lib/typography/Text';
import { filterApplicableLabels } from 'utils/filter-applicable-labels'; import { filterApplicableLabels } from 'utils/filter-applicable-labels';
import { Label } from 'cvat-core-wrapper'; import { Label } from 'cvat-core-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { import {
activateObject as activateObjectAction, activateObject as activateObjectAction,
@ -24,7 +22,6 @@ import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { ThunkDispatch } from 'utils/redux'; import { ThunkDispatch } from 'utils/redux';
import AppearanceBlock from 'components/annotation-page/appearance-block'; import AppearanceBlock from 'components/annotation-page/appearance-block';
import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons'; import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons';
import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image';
import { CombinedState, ObjectType } from 'reducers'; import { CombinedState, ObjectType } from 'reducers';
import AttributeEditor from './attribute-editor'; import AttributeEditor from './attribute-editor';
import AttributeSwitcher from './attribute-switcher'; import AttributeSwitcher from './attribute-switcher';
@ -39,7 +36,6 @@ interface StateToProps {
jobInstance: any; jobInstance: any;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas | Canvas3d;
canvasIsReady: boolean; canvasIsReady: boolean;
curZLayer: number; curZLayer: number;
} }
@ -64,7 +60,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
zLayer: { cur }, zLayer: { cur },
}, },
job: { instance: jobInstance, labels }, job: { instance: jobInstance, labels },
canvas: { instance: canvasInstance, ready: canvasIsReady }, canvas: { ready: canvasIsReady },
}, },
shortcuts: { keyMap, normalizedKeyMap }, shortcuts: { keyMap, normalizedKeyMap },
} = state; } = state;
@ -77,7 +73,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
states, states,
keyMap, keyMap,
normalizedKeyMap, normalizedKeyMap,
canvasInstance,
canvasIsReady, canvasIsReady,
curZLayer: cur, curZLayer: cur,
}; };
@ -109,7 +104,6 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
activateObject, activateObject,
keyMap, keyMap,
normalizedKeyMap, normalizedKeyMap,
canvasInstance,
canvasIsReady, canvasIsReady,
curZLayer, curZLayer,
} = props; } = props;
@ -129,8 +123,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
const listener = (event: TransitionEvent): void => { const listener = (event: TransitionEvent): void => {
if (event.target && event.propertyName === 'width' && event.target === collapser) { if (event.target && event.propertyName === 'width' && event.target === collapser) {
canvasInstance.fitCanvas(); window.dispatchEvent(new Event('resize'));
canvasInstance.fit();
(collapser as HTMLElement).removeEventListener('transitionend', listener as any); (collapser as HTMLElement).removeEventListener('transitionend', listener as any);
} }
}; };
@ -139,7 +132,6 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
(collapser as HTMLElement).addEventListener('transitionend', listener as any); (collapser as HTMLElement).addEventListener('transitionend', listener as any);
} }
adjustContextImagePosition(!sidebarCollapsed);
setSidebarCollapsed(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed);
}; };

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,13 +7,13 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout';
import AttributeAnnotationSidebar from './attribute-annotation-sidebar/attribute-annotation-sidebar'; import AttributeAnnotationSidebar from './attribute-annotation-sidebar/attribute-annotation-sidebar';
export default function AttributeAnnotationWorkspace(): JSX.Element { export default function AttributeAnnotationWorkspace(): JSX.Element {
return ( return (
<Layout hasSider className='attribute-annotation-workspace'> <Layout hasSider className='attribute-annotation-workspace'>
<CanvasWrapperContainer /> <CanvasLayout />
<AttributeAnnotationSidebar /> <AttributeAnnotationSidebar />
</Layout> </Layout>
); );

@ -1,586 +0,0 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React, {
ReactElement, SyntheticEvent, useEffect, useReducer, useRef,
} from 'react';
import Layout from 'antd/lib/layout/layout';
import {
ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
} from '@ant-design/icons';
import { ResizableBox } from 'react-resizable';
import {
ColorBy, ContextMenuType, ObjectType, Workspace,
} from 'reducers';
import {
CameraAction, Canvas3d, ViewType, ViewsDOM,
} from 'cvat-canvas3d-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
import ContextImage from 'components/annotation-page/standard-workspace/context-image/context-image';
import CVATTooltip from 'components/common/cvat-tooltip';
import { LogType } from 'cvat-logger';
import { getCore } from 'cvat-core-wrapper';
const cvat = getCore();
interface Props {
opacity: number;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d | Canvas;
jobInstance: any;
frameData: any;
annotations: any[];
contextMenuVisibility: boolean;
activeLabelID: number;
activeObjectType: ObjectType;
activatedStateID: number | null;
onSetupCanvas: () => void;
onGroupObjects: (enabled: boolean) => void;
onResetCanvas(): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onActivateObject(activatedStateID: number | null): void;
onUpdateAnnotations(states: any[]): void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onEditShape: (enabled: boolean) => void;
onDragCanvas: (enabled: boolean) => void;
onShapeDrawn: () => void;
workspace: Workspace;
frame: number;
resetZoom: boolean;
}
interface ViewSize {
fullHeight: number;
fullWidth: number;
vertical: number;
top: number;
side: number;
front: number;
}
function viewSizeReducer(
state: ViewSize,
action: { type: ViewType | 'set' | 'resize'; e?: SyntheticEvent; data?: ViewSize },
): ViewSize {
const event = (action.e as unknown) as MouseEvent;
const canvas3dContainer = document.getElementById('canvas3d-container');
if (canvas3dContainer) {
switch (action.type) {
case ViewType.TOP: {
const width = event.clientX - canvas3dContainer.getBoundingClientRect().left;
const topWidth = state.top;
if (topWidth < width) {
const top = state.top + (width - topWidth);
const side = state.side - (width - topWidth);
return {
...state,
top,
side,
};
}
const top = state.top - (topWidth - width);
const side = state.side + (topWidth - width);
return {
...state,
top,
side,
};
}
case ViewType.SIDE: {
const width = event.clientX - canvas3dContainer.getBoundingClientRect().left;
const topSideWidth = state.top + state.side;
if (topSideWidth < width) {
const side = state.side + (width - topSideWidth);
const front = state.front - (width - topSideWidth);
return {
...state,
side,
front,
};
}
const side = state.side - (topSideWidth - width);
const front = state.front + (topSideWidth - width);
return {
...state,
side,
front,
};
}
case ViewType.PERSPECTIVE:
return {
...state,
vertical: event.clientY - canvas3dContainer.getBoundingClientRect().top,
};
case 'set':
return action.data as ViewSize;
case 'resize': {
const canvasPerspectiveContainer = document.getElementById('cvat-canvas3d-perspective');
let midState = { ...state };
if (canvasPerspectiveContainer) {
if (state.fullHeight !== canvas3dContainer.clientHeight) {
const diff = canvas3dContainer.clientHeight - state.fullHeight;
midState = {
...midState,
fullHeight: canvas3dContainer.clientHeight,
vertical: state.vertical + diff,
};
}
if (state.fullWidth !== canvasPerspectiveContainer.clientWidth) {
const oldWidth = state.fullWidth;
const width = canvasPerspectiveContainer.clientWidth;
midState = {
...midState,
fullWidth: width,
top: (state.top / oldWidth) * width,
side: (state.side / oldWidth) * width,
front: (state.front / oldWidth) * width,
};
}
return midState;
}
return state;
}
default:
throw new Error();
}
}
return state;
}
const CanvasWrapperComponent = (props: Props): ReactElement => {
const animateId = useRef(0);
const [viewSize, setViewSize] = useReducer(viewSizeReducer, {
fullHeight: 0,
fullWidth: 0,
vertical: 0,
top: 0,
side: 0,
front: 0,
});
const perspectiveView = useRef<HTMLDivElement | null>(null);
const topView = useRef<HTMLDivElement | null>(null);
const sideView = useRef<HTMLDivElement | null>(null);
const frontView = useRef<HTMLDivElement | null>(null);
const {
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
contextMenuVisibility,
frameData,
onResetCanvas,
onSetupCanvas,
annotations,
frame,
jobInstance,
activeLabelID,
activatedStateID,
resetZoom,
activeObjectType,
onShapeDrawn,
onCreateAnnotations,
frameFetching,
} = props;
const { canvasInstance } = props as { canvasInstance: Canvas3d };
const onCanvasSetup = (): void => {
onSetupCanvas();
};
const onCanvasDragStart = (): void => {
const { onDragCanvas } = props;
onDragCanvas(true);
};
const onCanvasDragDone = (): void => {
const { onDragCanvas } = props;
onDragCanvas(false);
};
const animateCanvas = (): void => {
canvasInstance.render();
animateId.current = requestAnimationFrame(animateCanvas);
};
const updateCanvas = (): void => {
if (frameData !== null) {
canvasInstance.setup(
frameData,
annotations.filter((e) => e.objectType !== ObjectType.TAG),
);
}
};
const onCanvasCancel = (): void => {
onResetCanvas();
};
const onCanvasShapeDrawn = (event: any): void => {
if (!event.detail.continue) {
onShapeDrawn();
}
const { state, duration } = event.detail;
const isDrawnFromScratch = !state.label;
if (isDrawnFromScratch) {
jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
} else {
jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
}
state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false;
state.frame = frame;
state.zOrder = 0;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
};
const onCanvasClick = (e: MouseEvent): void => {
const { onUpdateContextMenu } = props;
if (contextMenuVisibility) {
onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
}
};
const initialSetup = (): void => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone);
canvasInstance.configure({ resetZoom });
};
const keyControlsKeyDown = (key: KeyboardEvent): void => {
canvasInstance.keyControls(key);
};
const keyControlsKeyUp = (key: KeyboardEvent): void => {
if (key.code === 'ControlLeft') {
canvasInstance.keyControls(key);
}
};
const onCanvasShapeSelected = (event: any): void => {
const { onActivateObject } = props;
const { clientID } = event.detail;
onActivateObject(clientID);
canvasInstance.activate(clientID);
};
const onCanvasEditDone = (event: any): void => {
const { onEditShape, onUpdateAnnotations } = props;
onEditShape(false);
const { state, points } = event.detail;
state.points = points;
onUpdateAnnotations([state]);
};
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html();
if (
perspectiveView &&
perspectiveView.current &&
topView &&
topView.current &&
sideView &&
sideView.current &&
frontView &&
frontView.current
) {
perspectiveView.current.appendChild(canvasInstanceDOM.perspective);
topView.current.appendChild(canvasInstanceDOM.top);
sideView.current.appendChild(canvasInstanceDOM.side);
frontView.current.appendChild(canvasInstanceDOM.front);
const canvas3dContainer = document.getElementById('canvas3d-container');
if (canvas3dContainer) {
const width = canvas3dContainer.clientWidth / 3;
setViewSize({
type: 'set',
data: {
fullHeight: canvas3dContainer.clientHeight,
fullWidth: canvas3dContainer.clientWidth,
vertical: canvas3dContainer.clientHeight / 2,
top: width,
side: width,
front: width,
},
});
}
}
document.addEventListener('keydown', keyControlsKeyDown);
document.addEventListener('keyup', keyControlsKeyUp);
initialSetup();
updateCanvas();
animateCanvas();
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone);
document.removeEventListener('keydown', keyControlsKeyDown);
document.removeEventListener('keyup', keyControlsKeyUp);
cancelAnimationFrame(animateId.current);
};
}, []);
useEffect(() => {
canvasInstance.activate(activatedStateID);
}, [activatedStateID]);
useEffect(() => {
canvasInstance.configure({ resetZoom });
}, [resetZoom]);
const updateShapesView = (): void => {
(canvasInstance as Canvas3d).configureShapes({
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
});
};
const onContextMenu = (event: any): void => {
const { onUpdateContextMenu, onActivateObject } = props;
onActivateObject(event.detail.clientID);
onUpdateContextMenu(
event.detail.clientID !== null,
event.detail.clientX,
event.detail.clientY,
ContextMenuType.CANVAS_SHAPE,
);
};
const onResize = (): void => {
setViewSize({
type: 'resize',
});
};
const onCanvasObjectsGroupped = (event: any): void => {
const { onGroupAnnotations, onGroupObjects } = props;
onGroupObjects(false);
const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states);
};
useEffect(() => {
updateShapesView();
}, [opacity, outlined, outlineColor, selectedOpacity, colorBy]);
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
updateCanvas();
canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.addEventListener('canvas.fit', onResize);
canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped);
window.addEventListener('resize', onResize);
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.removeEventListener('canvas.fit', onResize);
canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped);
window.removeEventListener('resize', onResize);
};
}, [frameData, annotations, activeLabelID, contextMenuVisibility]);
const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
};
const ArrowGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-arrow-directions'>
<CVATTooltip title='Shift+Arrow Up' placement='topRight'>
<button
data-cy='arrow-up'
onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-up'
>
<ArrowUpOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<br />
<CVATTooltip title='Shift+Arrow Left' placement='topRight'>
<button
onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
<button
onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<CVATTooltip title='Shift+Arrow Right' placement='topRight'>
<button
onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowRightOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
</span>
);
const ControlGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-directions'>
<CVATTooltip title='Alt+U' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
U
</button>
</CVATTooltip>
<CVATTooltip title='Alt+I' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
I
</button>
</CVATTooltip>
<CVATTooltip title='Alt+O' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
O
</button>
</CVATTooltip>
<br />
<CVATTooltip title='Alt+J' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
J
</button>
</CVATTooltip>
<CVATTooltip title='Alt+K' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
K
</button>
</CVATTooltip>
<CVATTooltip title='Alt+L' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
L
</button>
</CVATTooltip>
</span>
);
return (
<Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'>
<ContextImage />
<ResizableBox
className='cvat-resizable'
width={Infinity}
height={viewSize.vertical}
axis='y'
handle={<span className='cvat-resizable-handle-horizontal' />}
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })}
>
<>
{frameFetching ? (
<svg id='cvat_canvas_loading_animation'>
<circle id='cvat_canvas_loading_circle' r='30' cx='50%' cy='50%' />
</svg>
) : null}
<div className='cvat-canvas3d-perspective' id='cvat-canvas3d-perspective'>
<div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} />
<ArrowGroup />
<ControlGroup />
</div>
</>
</ResizableBox>
<div
className='cvat-canvas3d-orthographic-views'
style={{ height: viewSize.fullHeight - viewSize.vertical }}
>
<ResizableBox
className='cvat-resizable'
width={viewSize.top}
height={viewSize.fullHeight - viewSize.vertical}
axis='x'
handle={<span className='cvat-resizable-handle-vertical-top' />}
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.TOP, e })}
>
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-topview'>
<div className='cvat-canvas3d-header'>TOP</div>
<div className='cvat-canvas3d-fullsize' ref={topView} />
</div>
</ResizableBox>
<ResizableBox
className='cvat-resizable'
width={viewSize.side}
height={viewSize.fullHeight - viewSize.vertical}
axis='x'
handle={<span className='cvat-resizable-handle-vertical-side' />}
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.SIDE, e })}
>
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-sideview'>
<div className='cvat-canvas3d-header'>SIDE</div>
<div className='cvat-canvas3d-fullsize' ref={sideView} />
</div>
</ResizableBox>
<div
className='cvat-canvas3d-orthographic-view cvat-canvas3d-frontview'
style={{ width: viewSize.front, height: viewSize.fullHeight - viewSize.vertical }}
>
<div className='cvat-canvas3d-header'>FRONT</div>
<div className='cvat-canvas3d-fullsize' ref={frontView} />
</div>
</div>
</Layout.Content>
);
};
export default React.memo(CanvasWrapperComponent);

@ -0,0 +1,139 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
export interface ItemLayout {
viewType: ViewType;
offset: number[];
x: number;
y: number;
w: number;
h: number;
viewIndex?: string;
}
export enum ViewType {
CANVAS = 'canvas',
CANVAS_3D = 'canvas3D',
CANVAS_3D_TOP = 'canvas3DTop',
CANVAS_3D_SIDE = 'canvas3DSide',
CANVAS_3D_FRONT = 'canvas3DFront',
RELATED_IMAGE = 'relatedImage',
}
const defaultLayout: {
'2D': {
[index: string]: ItemLayout[];
};
'3D': {
[index: string]: ItemLayout[];
};
} = { '2D': {}, '3D': {} };
defaultLayout['2D']['0'] = [{
viewType: ViewType.CANVAS,
offset: [0],
x: 0,
y: 0,
w: 12,
h: 12,
}];
defaultLayout['2D']['1'] = [
{ ...defaultLayout['2D']['0'][0], w: 9 }, {
viewType: ViewType.RELATED_IMAGE,
offset: [0, 0],
x: 9,
y: 0,
w: 3,
h: 4,
viewIndex: '0',
},
];
defaultLayout['2D']['2'] = [
...defaultLayout['2D']['1'], {
...defaultLayout['2D']['1'][1],
viewType: ViewType.RELATED_IMAGE,
viewIndex: '1',
offset: [0, 1],
y: 4,
},
];
defaultLayout['2D']['3'] = [
...defaultLayout['2D']['2'], {
...defaultLayout['2D']['2'][2],
viewIndex: '2',
offset: [0, 2],
y: 8,
},
];
defaultLayout['3D']['0'] = [{
viewType: ViewType.CANVAS_3D,
offset: [0],
x: 0,
y: 0,
w: 12,
h: 9,
}, {
viewType: ViewType.CANVAS_3D_TOP,
offset: [0],
x: 0,
y: 9,
w: 4,
h: 3,
}, {
viewType: ViewType.CANVAS_3D_SIDE,
offset: [0],
x: 4,
y: 9,
w: 4,
h: 3,
}, {
viewType: ViewType.CANVAS_3D_FRONT,
offset: [0],
x: 8,
y: 9,
w: 4,
h: 3,
}];
defaultLayout['3D']['1'] = [
{ ...defaultLayout['3D']['0'][0], w: 9 },
{ ...defaultLayout['3D']['0'][1], w: 3 },
{ ...defaultLayout['3D']['0'][2], x: 3, w: 3 },
{ ...defaultLayout['3D']['0'][3], x: 6, w: 3 },
{
viewType: ViewType.RELATED_IMAGE,
offset: [0, 0],
x: 9,
y: 0,
w: 3,
h: 4,
viewIndex: '0',
},
];
defaultLayout['3D']['2'] = [
...defaultLayout['3D']['1'],
{
...defaultLayout['3D']['1'][4],
viewIndex: '1',
offset: [0, 1],
y: 4,
},
];
defaultLayout['3D']['3'] = [
...defaultLayout['3D']['2'],
{
...defaultLayout['3D']['2'][5],
viewIndex: '2',
offset: [0, 2],
y: 8,
},
];
export default defaultLayout;

@ -0,0 +1,365 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import 'react-grid-layout/css/styles.css';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import RGL, { WidthProvider } from 'react-grid-layout';
import PropTypes from 'prop-types';
import { isEqual } from 'lodash';
import Layout from 'antd/lib/layout';
import {
CloseOutlined,
DragOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
PicCenterOutlined,
PlusOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import consts from 'consts';
import { DimensionType, CombinedState } from 'reducers';
import CanvasWrapperComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-wrapper';
import CanvasWrapper3DComponent, {
PerspectiveViewComponent,
TopViewComponent,
SideViewComponent,
FrontViewComponent,
} from 'components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D';
import ContextImage from 'components/annotation-page/canvas/views/context-image/context-image';
import CVATTooltip from 'components/common/cvat-tooltip';
import defaultLayout, { ItemLayout, ViewType } from './canvas-layout.conf';
const ReactGridLayout = WidthProvider(RGL);
const ViewFabric = (itemLayout: ItemLayout): JSX.Element => {
const { viewType: type, offset } = itemLayout;
let component = null;
switch (type) {
case ViewType.CANVAS:
component = <CanvasWrapperComponent />;
break;
case ViewType.CANVAS_3D:
component = <PerspectiveViewComponent />;
break;
case ViewType.RELATED_IMAGE:
component = <ContextImage offset={offset} />;
break;
case ViewType.CANVAS_3D_FRONT:
component = <FrontViewComponent />;
break;
case ViewType.CANVAS_3D_SIDE:
component = <SideViewComponent />;
break;
case ViewType.CANVAS_3D_TOP:
component = <TopViewComponent />;
break;
default:
component = <div> Undefined view </div>;
}
return component;
};
const fitLayout = (type: DimensionType, layoutConfig: ItemLayout[]): ItemLayout[] => {
const updatedLayout: ItemLayout[] = [];
const relatedViews = layoutConfig
.filter((item: ItemLayout) => item.viewType === ViewType.RELATED_IMAGE);
const relatedViewsCols = relatedViews.length > 6 ? 2 : 1;
const height = Math.floor(consts.CANVAS_WORKSPACE_ROWS / (relatedViews.length / relatedViewsCols));
relatedViews.forEach((view: ItemLayout, i: number) => {
updatedLayout.push({
...view,
h: height,
w: relatedViews.length > 6 ? 2 : 3,
x: relatedViewsCols === 1 ? 9 : 8 + (i % 2) * 2,
y: height * i,
});
});
let widthAvail = consts.CANVAS_WORKSPACE_COLS;
if (updatedLayout.length > 0) {
widthAvail -= updatedLayout[0].w * relatedViewsCols;
}
if (type === DimensionType.DIM_2D) {
const canvas = layoutConfig
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS) as ItemLayout;
updatedLayout.push({
...canvas,
x: 0,
y: 0,
w: widthAvail,
h: consts.CANVAS_WORKSPACE_ROWS,
});
} else {
const canvas = layoutConfig
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D) as ItemLayout;
const top = layoutConfig
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_TOP) as ItemLayout;
const side = layoutConfig
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_SIDE) as ItemLayout;
const front = layoutConfig
.find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_FRONT) as ItemLayout;
const helpfulCanvasViewHeight = 3;
updatedLayout.push({
...canvas,
x: 0,
y: 0,
w: widthAvail,
h: consts.CANVAS_WORKSPACE_ROWS - helpfulCanvasViewHeight,
}, {
...top,
x: 0,
y: consts.CANVAS_WORKSPACE_ROWS,
w: Math.ceil(widthAvail / 3),
h: helpfulCanvasViewHeight,
},
{
...side,
x: Math.ceil(widthAvail / 3),
y: consts.CANVAS_WORKSPACE_ROWS,
w: Math.ceil(widthAvail / 3),
h: helpfulCanvasViewHeight,
},
{
...front,
x: Math.ceil(widthAvail / 3) * 2,
y: consts.CANVAS_WORKSPACE_ROWS,
w: Math.floor(widthAvail / 3),
h: helpfulCanvasViewHeight,
});
}
return updatedLayout;
};
function CanvasLayout({ type }: { type?: DimensionType }): JSX.Element {
const relatedFiles = useSelector((state: CombinedState) => state.annotation.player.frame.relatedFiles);
const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance);
const canvasBackgroundColor = useSelector((state: CombinedState) => state.settings.player.canvasBackgroundColor);
const computeRowHeight = (): number => {
const container = window.document.getElementsByClassName('cvat-annotation-header')[0];
let containerHeight = window.innerHeight;
if (container) {
containerHeight = window.innerHeight - container.getBoundingClientRect().bottom;
// https://github.com/react-grid-layout/react-grid-layout/issues/628#issuecomment-1228453084
return Math.floor(
(containerHeight - consts.CANVAS_WORKSPACE_MARGIN * (consts.CANVAS_WORKSPACE_ROWS)) /
consts.CANVAS_WORKSPACE_ROWS,
);
}
return 0;
};
const getLayout = useCallback(() => (
defaultLayout[(type as DimensionType).toUpperCase() as '2D' | '3D'][Math.min(relatedFiles, 3)]
), [type, relatedFiles]);
const [layoutConfig, setLayoutConfig] = useState<ItemLayout[]>(getLayout());
const [rowHeight, setRowHeight] = useState<number>(Math.floor(computeRowHeight()));
const [fullscreenKey, setFullscreenKey] = useState<string>('');
const fitCanvas = useCallback(() => {
if (canvasInstance) {
canvasInstance.fitCanvas();
canvasInstance.fit();
}
}, [canvasInstance]);
useEffect(() => {
const onResize = (): void => {
setRowHeight(computeRowHeight());
fitCanvas();
const [el] = window.document.getElementsByClassName('cvat-canvas-grid-root');
if (el) {
el.addEventListener('transitionend', () => {
fitCanvas();
}, { once: true });
}
};
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}, [fitCanvas]);
useEffect(() => {
setRowHeight(computeRowHeight());
}, []);
useEffect(() => {
window.dispatchEvent(new Event('resize'));
}, [layoutConfig]);
const children = layoutConfig.map((value: ItemLayout) => ViewFabric(value));
const layout = layoutConfig.map((value: ItemLayout) => ({
x: value.x,
y: value.y,
w: value.w,
h: value.h,
i: typeof (value.viewIndex) !== 'undefined' ? `${value.viewType}_${value.viewIndex}` : `${value.viewType}`,
}));
return (
<Layout.Content>
{ !!rowHeight && (
<ReactGridLayout
cols={consts.CANVAS_WORKSPACE_COLS}
maxRows={consts.CANVAS_WORKSPACE_ROWS}
style={{ background: canvasBackgroundColor }}
containerPadding={[consts.CANVAS_WORKSPACE_PADDING, consts.CANVAS_WORKSPACE_PADDING]}
margin={[consts.CANVAS_WORKSPACE_MARGIN, consts.CANVAS_WORKSPACE_MARGIN]}
className='cvat-canvas-grid-root'
rowHeight={rowHeight}
layout={layout}
onLayoutChange={(updatedLayout: RGL.Layout[]) => {
const transformedLayout = layoutConfig.map((itemLayout: ItemLayout, i: number): ItemLayout => ({
...itemLayout,
x: updatedLayout[i].x,
y: updatedLayout[i].y,
w: updatedLayout[i].w,
h: updatedLayout[i].h,
}));
if (!isEqual(layoutConfig, transformedLayout)) {
setLayoutConfig(transformedLayout);
}
}}
resizeHandle={(_: any, ref: React.MutableRefObject<HTMLDivElement>) => (
<div ref={ref} className='cvat-grid-item-resize-handler react-resizable-handle' />
)}
draggableHandle='.cvat-grid-item-drag-handler'
>
{ children.map((child: JSX.Element, idx: number): JSX.Element => {
const { viewType, viewIndex } = layoutConfig[idx];
const key = typeof viewIndex !== 'undefined' ? `${viewType}_${viewIndex}` : `${viewType}`;
return (
<div
style={fullscreenKey === key ? { backgroundColor: canvasBackgroundColor } : {}}
className={fullscreenKey === key ?
'cvat-canvas-grid-item cvat-canvas-grid-fullscreen-item' :
'cvat-canvas-grid-item'}
key={key}
>
<DragOutlined className='cvat-grid-item-drag-handler' />
<CloseOutlined
className='cvat-grid-item-close-button'
style={{
pointerEvents: viewType !== ViewType.RELATED_IMAGE ? 'none' : undefined,
opacity: viewType !== ViewType.RELATED_IMAGE ? 0.2 : undefined,
}}
onClick={() => {
if (viewType === ViewType.RELATED_IMAGE) {
setLayoutConfig(
layoutConfig
.filter((item: ItemLayout) => !(
item.viewType === viewType && item.viewIndex === viewIndex
)),
);
}
}}
/>
{fullscreenKey === key ? (
<FullscreenExitOutlined
className='cvat-grid-item-fullscreen-handler'
onClick={() => {
window.dispatchEvent(new Event('resize'));
setFullscreenKey('');
}}
/>
) : (
<FullscreenOutlined
className='cvat-grid-item-fullscreen-handler'
onClick={() => {
window.dispatchEvent(new Event('resize'));
setFullscreenKey(key);
}}
/>
)}
{ child }
</div>
);
}) }
</ReactGridLayout>
)}
{ type === DimensionType.DIM_3D && <CanvasWrapper3DComponent /> }
<div className='cvat-grid-layout-common-setups'>
<CVATTooltip title='Fit views'>
<PicCenterOutlined
onClick={() => {
setLayoutConfig(fitLayout(type as DimensionType, layoutConfig));
window.dispatchEvent(new Event('resize'));
}}
/>
</CVATTooltip>
<CVATTooltip title='Add context image'>
<PlusOutlined
style={{
pointerEvents: !relatedFiles ? 'none' : undefined,
opacity: !relatedFiles ? 0.2 : undefined,
}}
disabled={!!relatedFiles}
onClick={() => {
const MAXIMUM_RELATED = 12;
const existingRelated = layoutConfig
.filter((configItem: ItemLayout) => configItem.viewType === ViewType.RELATED_IMAGE);
if (existingRelated.length >= MAXIMUM_RELATED) {
return;
}
if (existingRelated.length === 0) {
setLayoutConfig(defaultLayout[type?.toUpperCase() as '2D' | '3D']['1']);
return;
}
const viewIndexes = existingRelated
.map((item: ItemLayout) => +(item.viewIndex as string)).sort();
const max = Math.max(...viewIndexes);
let viewIndex = max + 1;
for (let i = 0; i < max + 1; i++) {
if (!viewIndexes.includes(i)) {
viewIndex = i;
break;
}
}
const latest = existingRelated[existingRelated.length - 1];
const copy = { ...latest, offset: [0, viewIndex], viewIndex: `${viewIndex}` };
setLayoutConfig(fitLayout(type as DimensionType, [...layoutConfig, copy]));
window.dispatchEvent(new Event('resize'));
}}
/>
</CVATTooltip>
<CVATTooltip title='Reload layout'>
<ReloadOutlined onClick={() => {
setLayoutConfig([...getLayout()]);
window.dispatchEvent(new Event('resize'));
}}
/>
</CVATTooltip>
</div>
</Layout.Content>
);
}
CanvasLayout.defaultProps = {
type: DimensionType.DIM_2D,
};
CanvasLayout.PropType = {
type: PropTypes.oneOf(Object.values(DimensionType)),
};
export default React.memo(CanvasLayout);

@ -0,0 +1,102 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.cvat-canvas-grid-root {
position: relative;
}
.cvat-grid-layout-common-setups {
position: absolute;
top: 0;
right: 50%;
transform: translate(0, calc($grid-unit-size * 12 - 1px));
z-index: 1000;
background: $background-color-2;
line-height: $grid-unit-size * 3;
height: calc($grid-unit-size * 3 + 1px);
padding-bottom: $grid-unit-size;
padding-right: $grid-unit-size;
padding-left: $grid-unit-size;
border-radius: 0 0 4px 4px;
border-bottom: 1px solid $border-color-1;
border-right: 1px solid $border-color-1;
border-left: 1px solid $border-color-1;
> span {
margin-right: $grid-unit-size * 2;
&:last-child {
margin-right: 0;
}
}
}
.cvat-canvas-grid-item {
background-color: rgba(241, 241, 241, 0.7);
border-radius: 4px;
&.react-grid-item.cssTransforms {
transition-property: all;
}
&.cvat-canvas-grid-fullscreen-item {
width: 100% !important;
height: 100% !important;
padding-right: $grid-unit-size;
transform: translate(4px, 4px) !important;
z-index: 1;
.cvat-grid-item-resize-handler.react-resizable-handle,
.cvat-grid-item-drag-handler {
visibility: hidden;
}
}
.cvat-grid-item-drag-handler,
.cvat-grid-item-fullscreen-handler,
.cvat-grid-item-close-button {
position: absolute;
top: $grid-unit-size;
z-index: 1000;
font-size: 16px;
background: $header-color;
border-radius: 2px;
opacity: 0.6;
transition: all 200ms;
&:hover {
opacity: 0.9;
}
&.cvat-grid-item-drag-handler {
left: $grid-unit-size * 4;
cursor: move;
}
&.cvat-grid-item-fullscreen-handler {
left: $grid-unit-size;
}
&.cvat-grid-item-close-button {
right: $grid-unit-size;
}
}
.cvat-grid-item-resize-handler.react-resizable-handle {
bottom: -3px;
right: -3px;
cursor: se-resize;
&::after {
bottom: 0;
right: 0;
width: 9px;
height: 10px;
border-right: 2px solid rgba(0, 0, 0, 1);
border-bottom: 2px solid rgba(0, 0, 0, 1);
}
}
}

@ -1,8 +1,8 @@
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@import '../../../base.scss'; @import 'base.scss';
.cvat-brush-tools-toolbox { .cvat-brush-tools-toolbox {
position: absolute; position: absolute;

@ -1,17 +1,18 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import { connect } from 'react-redux';
import Slider from 'antd/lib/slider'; import Slider from 'antd/lib/slider';
import Spin from 'antd/lib/spin';
import Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
import { PlusCircleOutlined, UpOutlined } from '@ant-design/icons'; import { PlusCircleOutlined, UpOutlined } from '@ant-design/icons';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { import {
ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType, ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType, ActiveControl, CombinedState,
} from 'reducers'; } from 'reducers';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
@ -20,16 +21,46 @@ import { getCore } from 'cvat-core-wrapper';
import consts from 'consts'; import consts from 'consts';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags'; import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags';
import {
confirmCanvasReady,
dragCanvas,
zoomCanvas,
resetCanvas,
shapeDrawn,
mergeObjects,
groupObjects,
splitTrack,
editShape,
updateAnnotationsAsync,
createAnnotationsAsync,
mergeAnnotationsAsync,
groupAnnotationsAsync,
splitAnnotationsAsync,
activateObject,
updateCanvasContextMenu,
addZLayer,
switchZLayer,
fetchAnnotationsAsync,
getDataFailed,
} from 'actions/annotation-actions';
import {
switchGrid,
changeGridColor,
changeGridOpacity,
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
switchAutomaticBordering,
} from 'actions/settings-actions';
import { reviewActions } from 'actions/review-actions';
import ImageSetupsContent from './image-setups-content'; import ImageSetupsContent from './image-setups-content';
import BrushTools from './brush-tools'; import BrushTools from './brush-tools';
import ContextImage from '../standard-workspace/context-image/context-image';
const cvat = getCore(); const cvat = getCore();
const MAX_DISTANCE_TO_OPEN_SHAPE = 50; const MAX_DISTANCE_TO_OPEN_SHAPE = 50;
interface Props { interface StateToProps {
sidebarCollapsed: boolean;
canvasInstance: Canvas | Canvas3d | null; canvasInstance: Canvas | Canvas3d | null;
jobInstance: any; jobInstance: any;
activatedStateID: number | null; activatedStateID: number | null;
@ -38,7 +69,7 @@ interface Props {
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frameAngle: number; frameAngle: number;
frameFetching: boolean; canvasIsReady: boolean;
frame: number; frame: number;
opacity: number; opacity: number;
colorBy: ColorBy; colorBy: ColorBy;
@ -53,9 +84,6 @@ interface Props {
gridOpacity: number; gridOpacity: number;
activeLabelID: number; activeLabelID: number;
activeObjectType: ObjectType; activeObjectType: ObjectType;
curZLayer: number;
minZLayer: number;
maxZLayer: number;
brightnessLevel: number; brightnessLevel: number;
contrastLevel: number; contrastLevel: number;
saturationLevel: number; saturationLevel: number;
@ -69,27 +97,32 @@ interface Props {
textContent: string; textContent: string;
showAllInterpolationTracks: boolean; showAllInterpolationTracks: boolean;
workspace: Workspace; workspace: Workspace;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean; automaticBordering: boolean;
intelligentPolygonCrop: boolean; intelligentPolygonCrop: boolean;
keyMap: KeyMap;
canvasBackgroundColor: string;
switchableAutomaticBordering: boolean; switchableAutomaticBordering: boolean;
keyMap: KeyMap;
showTagsOnFrame: boolean; showTagsOnFrame: boolean;
onSetupCanvas: () => void; }
interface DispatchToProps {
onSetupCanvas(): void;
onDragCanvas: (enabled: boolean) => void; onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void;
onResetCanvas: () => void;
onShapeDrawn: () => void;
onMergeObjects: (enabled: boolean) => void; onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void; onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void;
onResetCanvas: () => void;
onUpdateAnnotations(states: any[]): void; onUpdateAnnotations(states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject(activatedStateID: number | null, activatedElementID?: number | null): void; onActivateObject: (activatedStateID: number | null, activatedElementID: number | null) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
onAddZLayer(): void; onAddZLayer(): void;
onSwitchZLayer(cur: number): void; onSwitchZLayer(cur: number): void;
@ -105,7 +138,210 @@ interface Props {
onStartIssue(position: number[]): void; onStartIssue(position: number[]): void;
} }
export default class CanvasWrapperComponent extends React.PureComponent<Props> { function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: { activeControl, instance: canvasInstance, ready: canvasIsReady },
drawing: { activeLabelID, activeObjectType },
job: { instance: jobInstance },
player: {
frame: { data: frameData, number: frame },
frameAngles,
},
annotations: {
states: annotations,
activatedStateID,
activatedElementID,
activatedAttributeID,
zLayer: { cur: curZLayer, min: minZLayer, max: maxZLayer },
},
workspace,
},
settings: {
player: {
grid,
gridSize,
gridColor,
gridOpacity,
brightnessLevel,
contrastLevel,
saturationLevel,
resetZoom,
smoothImage,
},
workspace: {
aamZoomMargin,
showObjectsTextAlways,
showAllInterpolationTracks,
showTagsOnFrame,
automaticBordering,
intelligentPolygonCrop,
textFontSize,
controlPointsSize,
textPosition,
textContent,
},
shapes: {
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
},
},
shortcuts: { keyMap },
} = state;
return {
canvasInstance,
jobInstance,
frameData,
frameAngle: frameAngles[frame - jobInstance.startFrame],
canvasIsReady,
frame,
activatedStateID,
activatedElementID,
activatedAttributeID,
annotations,
opacity: opacity / 100,
colorBy,
selectedOpacity: selectedOpacity / 100,
outlined,
outlineColor,
showBitmap,
showProjections,
grid,
gridSize,
gridColor,
gridOpacity: gridOpacity / 100,
activeLabelID,
activeObjectType,
brightnessLevel: brightnessLevel / 100,
contrastLevel: contrastLevel / 100,
saturationLevel: saturationLevel / 100,
resetZoom,
smoothImage,
aamZoomMargin,
showObjectsTextAlways,
textFontSize,
controlPointsSize,
textPosition,
textContent,
showAllInterpolationTracks,
showTagsOnFrame,
curZLayer,
minZLayer,
maxZLayer,
automaticBordering,
intelligentPolygonCrop,
workspace,
keyMap,
switchableAutomaticBordering:
activeControl === ActiveControl.DRAW_POLYGON ||
activeControl === ActiveControl.DRAW_POLYLINE ||
activeControl === ActiveControl.DRAW_MASK ||
activeControl === ActiveControl.EDIT,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onSetupCanvas(): void {
dispatch(confirmCanvasReady());
},
onDragCanvas(enabled: boolean): void {
dispatch(dragCanvas(enabled));
},
onZoomCanvas(enabled: boolean): void {
dispatch(zoomCanvas(enabled));
},
onResetCanvas(): void {
dispatch(resetCanvas());
},
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onMergeObjects(enabled: boolean): void {
dispatch(mergeObjects(enabled));
},
onGroupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onSplitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled));
},
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(mergeAnnotationsAsync(sessionInstance, frame, states));
},
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
},
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void {
dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
},
onActivateObject(activatedStateID: number | null, activatedElementID: number | null = null): void {
if (activatedStateID === null) {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID, activatedElementID, null));
},
onUpdateContextMenu(
visible: boolean,
left: number,
top: number,
type: ContextMenuType,
pointID?: number,
): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
},
onAddZLayer(): void {
dispatch(addZLayer());
},
onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur));
},
onChangeBrightnessLevel(level: number): void {
dispatch(changeBrightnessLevel(level));
},
onChangeContrastLevel(level: number): void {
dispatch(changeContrastLevel(level));
},
onChangeSaturationLevel(level: number): void {
dispatch(changeSaturationLevel(level));
},
onChangeGridOpacity(opacity: number): void {
dispatch(changeGridOpacity(opacity));
},
onChangeGridColor(color: GridColor): void {
dispatch(changeGridColor(color));
},
onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled));
},
onSwitchAutomaticBordering(enabled: boolean): void {
dispatch(switchAutomaticBordering(enabled));
},
onFetchAnnotation(): void {
dispatch(fetchAnnotationsAsync());
},
onGetDataFailed(error: any): void {
dispatch(getDataFailed(error));
},
onStartIssue(position: number[]): void {
dispatch(reviewActions.startIssue(position));
},
};
}
type Props = StateToProps & DispatchToProps;
class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidMount(): void { public componentDidMount(): void {
const { const {
automaticBordering, automaticBordering,
@ -163,7 +399,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
frameData, frameData,
frameAngle, frameAngle,
annotations, annotations,
sidebarCollapsed,
activatedStateID, activatedStateID,
curZLayer, curZLayer,
resetZoom, resetZoom,
@ -176,7 +411,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
workspace, workspace,
frameFetching,
showObjectsTextAlways, showObjectsTextAlways,
textFontSize, textFontSize,
controlPointsSize, controlPointsSize,
@ -186,7 +420,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
automaticBordering, automaticBordering,
intelligentPolygonCrop, intelligentPolygonCrop,
showProjections, showProjections,
canvasBackgroundColor,
colorBy, colorBy,
onFetchAnnotation, onFetchAnnotation,
} = this.props; } = this.props;
@ -230,19 +463,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onFetchAnnotation(); onFetchAnnotation();
} }
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar');
if (sidebar) {
sidebar.addEventListener(
'transitionend',
() => {
canvasInstance.fitCanvas();
},
{ once: true },
);
}
}
if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) { if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null); canvasInstance.activate(null);
} }
@ -312,26 +532,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
} }
if (frameFetching !== prevProps.frameFetching) {
const loadingAnimation = window.document.getElementById('cvat_canvas_loading_animation');
if (loadingAnimation) {
if (frameFetching) {
loadingAnimation.classList.remove('cvat_canvas_hidden');
} else {
loadingAnimation.classList.add('cvat_canvas_hidden');
}
}
}
if (prevProps.canvasBackgroundColor !== canvasBackgroundColor) {
const canvasWrapperElement = window.document
.getElementsByClassName('cvat-canvas-container')
.item(0) as HTMLElement | null;
if (canvasWrapperElement) {
canvasWrapperElement.style.backgroundColor = canvasBackgroundColor;
}
}
this.activateOnCanvas(); this.activateOnCanvas();
} }
@ -365,8 +565,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence); canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence);
window.removeEventListener('resize', this.fitCanvas);
} }
private onCanvasErrorOccurrence = (event: any): void => { private onCanvasErrorOccurrence = (event: any): void => {
@ -458,19 +656,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onSplitAnnotations(jobInstance, frame, state); onSplitAnnotations(jobInstance, frame, state);
}; };
private fitCanvas = (): void => {
const { canvasInstance } = this.props;
if (canvasInstance) {
canvasInstance.fitCanvas();
}
};
private onCanvasMouseDown = (e: MouseEvent): void => { private onCanvasMouseDown = (e: MouseEvent): void => {
const { workspace, activatedStateID, onActivateObject } = this.props; const { workspace, activatedStateID, onActivateObject } = this.props;
if ((e.target as HTMLElement).tagName === 'svg' && e.button !== 2) { if ((e.target as HTMLElement).tagName === 'svg' && e.button !== 2) {
if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) { if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) {
onActivateObject(null); onActivateObject(null, null);
} }
} }
}; };
@ -526,7 +717,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
// and triggers this event // and triggers this event
// in this case we do not need to update our state // in this case we do not need to update our state
if (state.clientID === activatedStateID) { if (state.clientID === activatedStateID) {
onActivateObject(null); onActivateObject(null, null);
} }
}; };
@ -557,7 +748,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
private onCanvasEditStart = (): void => { private onCanvasEditStart = (): void => {
const { onActivateObject, onEditShape } = this.props; const { onActivateObject, onEditShape } = this.props;
onActivateObject(null); onActivateObject(null, null);
onEditShape(true); onEditShape(true);
}; };
@ -683,14 +874,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
canvasBackgroundColor,
} = this.props; } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas }; const { canvasInstance } = this.props as { canvasInstance: Canvas };
// Size
window.addEventListener('resize', this.fitCanvas);
this.fitCanvas();
// Grid // Grid
const gridElement = window.document.getElementById('cvat_canvas_grid'); const gridElement = window.document.getElementById('cvat_canvas_grid');
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern'); const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
@ -707,18 +893,13 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
CSSImageFilter: CSSImageFilter:
`brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`, `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`,
}); });
const canvasWrapperElement = window.document
.getElementsByClassName('cvat-canvas-container')
.item(0) as HTMLElement | null;
if (canvasWrapperElement) {
canvasWrapperElement.style.backgroundColor = canvasBackgroundColor;
}
// Events // Events
canvasInstance.html().addEventListener( canvasInstance.html().addEventListener(
'canvas.setup', 'canvas.setup',
() => { () => {
const { activatedStateID, activatedAttributeID } = this.props; const { activatedStateID, activatedAttributeID } = this.props;
canvasInstance.fitCanvas();
canvasInstance.fit(); canvasInstance.fit();
canvasInstance.activate(activatedStateID, activatedAttributeID); canvasInstance.activate(activatedStateID, activatedAttributeID);
}, },
@ -763,6 +944,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
switchableAutomaticBordering, switchableAutomaticBordering,
automaticBordering, automaticBordering,
showTagsOnFrame, showTagsOnFrame,
canvasIsReady,
onSwitchAutomaticBordering, onSwitchAutomaticBordering,
onSwitchZLayer, onSwitchZLayer,
onAddZLayer, onAddZLayer,
@ -788,13 +970,20 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
return ( return (
<Layout.Content style={{ position: 'relative' }}> <>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} /> <GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
{/* {/*
This element doesn't have any props This element doesn't have any props
So, React isn't going to rerender it So, React isn't going to rerender it
And it's a reason why cvat-canvas appended in mount function works And it's a reason why cvat-canvas appended in mount function works
*/} */}
{
!canvasIsReady && (
<div className='cvat-spinner-container'>
<Spin className='cvat-spinner' />
</div>
)
}
<div <div
className='cvat-canvas-container' className='cvat-canvas-container'
style={{ style={{
@ -804,7 +993,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}} }}
/> />
<ContextImage />
<BrushTools /> <BrushTools />
<Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}> <Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}>
@ -832,8 +1020,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
<FrameTags /> <FrameTags />
</div> </div>
) : null} ) : null}
; </>
</Layout.Content>
); );
} }
} }
export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent);

@ -0,0 +1,593 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, {
ReactElement, useEffect, useRef,
} from 'react';
import { connect, useSelector } from 'react-redux';
import {
ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined,
} from '@ant-design/icons';
import Spin from 'antd/lib/spin';
import {
activateObject,
confirmCanvasReady,
createAnnotationsAsync,
dragCanvas,
editShape,
groupAnnotationsAsync,
groupObjects,
resetCanvas,
shapeDrawn,
updateAnnotationsAsync,
updateCanvasContextMenu,
} from 'actions/annotation-actions';
import {
ColorBy, CombinedState, ContextMenuType, ObjectType, Workspace,
} from 'reducers';
import { CameraAction, Canvas3d, ViewsDOM } from 'cvat-canvas3d-wrapper';
import CVATTooltip from 'components/common/cvat-tooltip';
import { LogType } from 'cvat-logger';
import { getCore } from 'cvat-core-wrapper';
const cvat = getCore();
interface StateToProps {
opacity: number;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d;
jobInstance: any;
frameData: any;
annotations: any[];
contextMenuVisibility: boolean;
activeLabelID: number | null;
activatedStateID: number | null;
activeObjectType: ObjectType;
workspace: Workspace;
frame: number;
resetZoom: boolean;
}
interface DispatchToProps {
onDragCanvas: (enabled: boolean) => void;
onSetupCanvas(): void;
onGroupObjects: (enabled: boolean) => void;
onResetCanvas(): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onUpdateAnnotations(states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onActivateObject: (activatedStateID: number | null) => void;
onShapeDrawn: () => void;
onEditShape: (enabled: boolean) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: {
instance: canvasInstance,
contextMenu: { visible: contextMenuVisibility },
},
drawing: { activeLabelID, activeObjectType },
job: { instance: jobInstance },
player: {
frame: { data: frameData, number: frame, fetching: frameFetching },
},
annotations: {
states: annotations,
activatedStateID,
},
workspace,
},
settings: {
player: {
resetZoom,
},
shapes: {
opacity, colorBy, selectedOpacity, outlined, outlineColor,
},
},
} = state;
return {
canvasInstance: canvasInstance as Canvas3d,
jobInstance,
frameData,
contextMenuVisibility,
annotations,
frameFetching,
frame,
opacity,
colorBy,
selectedOpacity,
outlined,
outlineColor,
activeLabelID,
activatedStateID,
activeObjectType,
resetZoom,
workspace,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onDragCanvas(enabled: boolean): void {
dispatch(dragCanvas(enabled));
},
onSetupCanvas(): void {
dispatch(confirmCanvasReady());
},
onResetCanvas(): void {
dispatch(resetCanvas());
},
onGroupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
},
onActivateObject(activatedStateID: number | null): void {
if (activatedStateID === null) {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID, null, null));
},
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onUpdateContextMenu(
visible: boolean,
left: number,
top: number,
type: ContextMenuType,
pointID?: number,
): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
},
};
}
type Props = StateToProps & DispatchToProps;
const Spinner = React.memo(() => (
<div className='cvat-spinner-container'>
<Spin className='cvat-spinner' />
</div>
));
export const PerspectiveViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef<HTMLDivElement>(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
canvas.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
};
const ArrowGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-arrow-directions'>
<CVATTooltip title='Shift+Arrow Up' placement='topRight'>
<button
data-cy='arrow-up'
onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-up'
>
<ArrowUpOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<br />
<CVATTooltip title='Shift+Arrow Left' placement='topRight'>
<button
onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
<button
onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
<CVATTooltip title='Shift+Arrow Right' placement='topRight'>
<button
onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
>
<ArrowRightOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button>
</CVATTooltip>
</span>
);
const ControlGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-directions'>
<CVATTooltip title='Alt+U' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
U
</button>
</CVATTooltip>
<CVATTooltip title='Alt+I' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
I
</button>
</CVATTooltip>
<CVATTooltip title='Alt+O' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
O
</button>
</CVATTooltip>
<br />
<CVATTooltip title='Alt+J' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
J
</button>
</CVATTooltip>
<CVATTooltip title='Alt+K' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
K
</button>
</CVATTooltip>
<CVATTooltip title='Alt+L' placement='topLeft'>
<button
onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
type='button'
className='cvat-canvas3d-perspective-directions-icon'
>
L
</button>
</CVATTooltip>
</span>
);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().perspective);
}
}, []);
return (
<div className='cvat-canvas3d-perspective'>
{ !canvasIsReady && <Spinner /> }
<div
className='cvat-canvas-container cvat-canvas-container-overflow'
ref={ref}
/>
<ArrowGroup />
<ControlGroup />
</div>
);
},
);
export const TopViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef<HTMLDivElement>(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().top);
}
}, []);
return (
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-topview'>
{ !canvasIsReady && <Spinner /> }
<div className='cvat-canvas3d-header'>Top</div>
<div
className='cvat-canvas3d-fullsize'
ref={ref}
/>
</div>
);
},
);
export const SideViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef<HTMLDivElement>(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().side);
}
}, []);
return (
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-sideview'>
{ !canvasIsReady && <Spinner /> }
<div className='cvat-canvas3d-header'>Side</div>
<div
className='cvat-canvas3d-fullsize'
ref={ref}
/>
</div>
);
},
);
export const FrontViewComponent = React.memo(
(): JSX.Element => {
const ref = useRef<HTMLDivElement>(null);
const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d);
const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready);
useEffect(() => {
if (ref.current) {
ref.current.appendChild(canvas.html().front);
}
}, []);
return (
<div className='cvat-canvas3d-orthographic-view cvat-canvas3d-frontview'>
{ !canvasIsReady && <Spinner /> }
<div className='cvat-canvas3d-header'>Front</div>
<div
className='cvat-canvas3d-fullsize'
ref={ref}
/>
</div>
);
},
);
const Canvas3DWrapperComponent = React.memo((props: Props): ReactElement => {
const animateId = useRef(0);
const {
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
contextMenuVisibility,
frameData,
onResetCanvas,
onSetupCanvas,
annotations,
frame,
jobInstance,
activeLabelID,
activatedStateID,
resetZoom,
activeObjectType,
onShapeDrawn,
onCreateAnnotations,
} = props;
const { canvasInstance } = props as { canvasInstance: Canvas3d };
const onCanvasSetup = (): void => {
onSetupCanvas();
};
const onCanvasDragStart = (): void => {
const { onDragCanvas } = props;
onDragCanvas(true);
};
const onCanvasDragDone = (): void => {
const { onDragCanvas } = props;
onDragCanvas(false);
};
const animateCanvas = (): void => {
canvasInstance.render();
animateId.current = requestAnimationFrame(animateCanvas);
};
const updateCanvas = (): void => {
if (frameData !== null) {
canvasInstance.setup(
frameData,
annotations.filter((e) => e.objectType !== ObjectType.TAG),
);
}
};
const onCanvasCancel = (): void => {
onResetCanvas();
};
const onCanvasShapeDrawn = (event: any): void => {
if (!event.detail.continue) {
onShapeDrawn();
}
const { state, duration } = event.detail;
const isDrawnFromScratch = !state.label;
if (isDrawnFromScratch) {
jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
} else {
jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
}
state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false;
state.frame = frame;
state.zOrder = 0;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
};
const onCanvasClick = (e: MouseEvent): void => {
const { onUpdateContextMenu } = props;
if (contextMenuVisibility) {
onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE);
}
};
const initialSetup = (): void => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone);
canvasInstance.configure({ resetZoom });
};
const keyControlsKeyDown = (key: KeyboardEvent): void => {
canvasInstance.keyControls(key);
};
const keyControlsKeyUp = (key: KeyboardEvent): void => {
if (key.code === 'ControlLeft') {
canvasInstance.keyControls(key);
}
};
const onCanvasShapeSelected = (event: any): void => {
const { onActivateObject } = props;
const { clientID } = event.detail;
onActivateObject(clientID);
canvasInstance.activate(clientID);
};
const onCanvasEditDone = (event: any): void => {
const { onEditShape, onUpdateAnnotations } = props;
onEditShape(false);
const { state, points } = event.detail;
state.points = points;
onUpdateAnnotations([state]);
};
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html();
document.addEventListener('keydown', keyControlsKeyDown);
document.addEventListener('keyup', keyControlsKeyUp);
initialSetup();
updateCanvas();
animateCanvas();
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup);
canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart);
canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone);
document.removeEventListener('keydown', keyControlsKeyDown);
document.removeEventListener('keyup', keyControlsKeyUp);
cancelAnimationFrame(animateId.current);
};
}, []);
useEffect(() => {
canvasInstance.activate(activatedStateID);
}, [activatedStateID]);
useEffect(() => {
canvasInstance.configure({ resetZoom });
}, [resetZoom]);
const updateShapesView = (): void => {
(canvasInstance as Canvas3d).configureShapes({
opacity,
outlined,
outlineColor,
selectedOpacity,
colorBy,
});
};
const onContextMenu = (event: any): void => {
const { onUpdateContextMenu, onActivateObject } = props;
onActivateObject(event.detail.clientID);
onUpdateContextMenu(
event.detail.clientID !== null,
event.detail.clientX,
event.detail.clientY,
ContextMenuType.CANVAS_SHAPE,
);
};
const onCanvasObjectsGroupped = (event: any): void => {
const { onGroupAnnotations, onGroupObjects } = props;
onGroupObjects(false);
const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states);
};
useEffect(() => {
updateShapesView();
}, [opacity, outlined, outlineColor, selectedOpacity, colorBy]);
useEffect(() => {
const canvasInstanceDOM = canvasInstance.html() as ViewsDOM;
updateCanvas();
canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped);
return () => {
canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn);
canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected);
canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone);
canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu);
canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick);
canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped);
};
}, [frameData, annotations, activeLabelID, contextMenuVisibility]);
return <></>;
});
export default connect(mapStateToProps, mapDispatchToProps)(Canvas3DWrapperComponent);

@ -1,15 +1,10 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@import 'base.scss'; @import 'base.scss';
.cvat-canvas-container-overflow {
overflow: hidden;
width: 100%;
height: 100%;
}
.cvat-canvas3d-perspective { .cvat-canvas3d-perspective {
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -77,87 +72,20 @@
} }
.cvat-canvas3d-fullsize { .cvat-canvas3d-fullsize {
position: relative;
width: 100%; width: 100%;
height: 100%; height: calc(100% - $grid-unit-size * 3);
}
.cvat-canvas3d-view-slider {
position: absolute;
margin-left: auto;
width: $grid-unit-size * 10;
margin-right: auto;
right: 0;
top: 0;
left: 0;
background-color: grey;
height: $grid-unit-size * 0.5;
} }
.cvat-canvas3d-header { .cvat-canvas3d-header {
height: $grid-unit-size * 4; height: $grid-unit-size * 3;
width: 100%; width: 100%;
background-color: $background-color-2; background-color: $background-color-2;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
} }
.cvat-resizable { .cvat-canvas-container-overflow {
position: relative; overflow: hidden;
}
.cvat-resizable-handle-horizontal {
position: absolute;
margin-left: auto;
width: 100%;
margin-right: auto;
right: 0;
bottom: 0;
left: 0;
background-color: grey;
height: $grid-unit-size * 0.5;
cursor: ns-resize;
}
.cvat-resizable-handle-vertical-side {
position: absolute;
width: $grid-unit-size * 0.5;
margin-right: auto;
top: $grid-unit-size * 4.5;
right: 0;
bottom: 0;
background-color: grey;
height: 100%;
cursor: ew-resize;
}
.cvat-resizable-handle-vertical-top {
position: absolute;
width: $grid-unit-size * 0.5;
margin-right: auto;
top: $grid-unit-size * 4.5;
right: 0;
bottom: 0;
background-color: grey;
height: 100%;
cursor: ew-resize;
}
#cvat_canvas_loading_animation {
z-index: 1;
position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#cvat_canvas_loading_circle {
fill-opacity: 0;
stroke: #09c;
stroke-width: 3px;
stroke-dasharray: 50;
animation: loadingAnimation 1s linear infinite;
}
.cvat_canvas_hidden {
display: none;
}

@ -0,0 +1,87 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import Text from 'antd/lib/typography/Text';
import { CloseOutlined } from '@ant-design/icons';
interface Props {
images: Record<string, ImageBitmap>;
offset: number;
onChangeOffset: (offset: number) => void;
onClose: () => void;
}
function CanvasWithRef({
image, isActive, onClick, name,
}: { image: ImageBitmap, name: string, isActive: boolean, onClick: () => void }): JSX.Element {
const ref = useRef<HTMLCanvasElement>(null);
useEffect((): void => {
if (ref.current) {
const context = ref.current.getContext('2d');
if (context) {
ref.current.width = image.width;
ref.current.height = image.height;
context.drawImage(image, 0, 0);
}
}
}, [image, ref]);
return (
<div className={(isActive ? ['cvat-context-image-gallery-item cvat-context-image-gallery-item-current'] : ['cvat-context-image-gallery-item']).join(' ')}>
<Text strong className='cvat-context-image-gallery-item-name'>{name}</Text>
<canvas
ref={ref}
onClick={onClick}
/>
</div>
);
}
function ContextImageSelector(props: Props): React.ReactPortal {
const {
images, offset, onChangeOffset, onClose,
} = props;
const keys = Object.keys(images).sort();
return ReactDOM.createPortal((
<div className='cvat-context-image-overlay'>
<div className='cvat-context-image-gallery'>
<div className='cvat-context-image-gallery-header'>
<Text>
Click the image to display it as a context image
</Text>
<CloseOutlined className='cvat-context-image-close-button' onClick={onClose} />
</div>
<div className='cvat-context-image-gallery-items'>
{ keys.map((key, i: number) => (
<CanvasWithRef
name={key}
image={images[key]}
isActive={offset === i}
onClick={() => {
onChangeOffset(i);
onClose();
}}
key={i}
/>
))}
</div>
</div>
</div>
), window.document.body);
}
ContextImageSelector.PropType = {
images: PropTypes.arrayOf(PropTypes.string),
offset: PropTypes.number,
onChangeOffset: PropTypes.func,
onClose: PropTypes.func,
};
export default React.memo(ContextImageSelector);

@ -0,0 +1,128 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import notification from 'antd/lib/notification';
import Spin from 'antd/lib/spin';
import Text from 'antd/lib/typography/Text';
import { SettingOutlined } from '@ant-design/icons';
import CVATTooltop from 'components/common/cvat-tooltip';
import { CombinedState } from 'reducers';
import ContextImageSelector from './context-image-selector';
interface Props {
offset: number[];
}
function ContextImage(props: Props): JSX.Element {
const { offset } = props;
const defaultFrameOffset = (offset[0] || 0);
const defaultContextImageOffset = (offset[1] || 0);
const canvasRef = useRef<HTMLCanvasElement>(null);
const job = useSelector((state: CombinedState) => state.annotation.job.instance);
const { number: frame, relatedFiles } = useSelector((state: CombinedState) => state.annotation.player.frame);
const frameIndex = frame + defaultFrameOffset;
const [contextImageData, setContextImageData] = useState<Record<string, ImageBitmap>>({});
const [fetching, setFetching] = useState<boolean>(false);
const [contextImageOffset, setContextImageOffset] = useState<number>(
Math.min(defaultContextImageOffset, relatedFiles),
);
const [hasError, setHasError] = useState<boolean>(false);
const [showSelector, setShowSelector] = useState<boolean>(false);
useEffect(() => {
let unmounted = false;
const promise = job.frames.contextImage(frameIndex);
setFetching(true);
promise.then((imageBitmaps: Record<string, ImageBitmap>) => {
if (!unmounted) {
setContextImageData(imageBitmaps);
}
}).catch((error: any) => {
if (!unmounted) {
setHasError(true);
notification.error({
message: `Could not fetch context images. Frame: ${frameIndex}`,
description: error.toString(),
});
}
}).finally(() => {
if (!unmounted) {
setFetching(false);
}
});
return () => {
setContextImageData({});
unmounted = true;
};
}, [frameIndex]);
useEffect(() => {
if (canvasRef.current) {
const sortedKeys = Object.keys(contextImageData).sort();
const key = sortedKeys[contextImageOffset];
const image = contextImageData[key];
const context = canvasRef.current.getContext('2d');
if (context && image) {
canvasRef.current.width = image.width;
canvasRef.current.height = image.height;
context.drawImage(image, 0, 0);
}
}
}, [contextImageData, contextImageOffset, canvasRef]);
const contextImageName = Object.keys(contextImageData).sort()[contextImageOffset];
return (
<div className='cvat-context-image-wrapper'>
<div className='cvat-context-image-header'>
{ relatedFiles > 1 && (
<SettingOutlined
className='cvat-context-image-setup-button'
onClick={() => {
setShowSelector(true);
}}
/>
)}
<div className='cvat-context-image-title'>
<CVATTooltop title={contextImageName}>
<Text>{contextImageName}</Text>
</CVATTooltop>
</div>
</div>
{ (hasError ||
(!fetching && contextImageOffset >= Object.keys(contextImageData).length)) && <Text> No data </Text>}
{ fetching && <Spin size='small' /> }
{
contextImageOffset < Object.keys(contextImageData).length &&
<canvas ref={canvasRef} />
}
{ showSelector && (
<ContextImageSelector
images={contextImageData}
offset={contextImageOffset}
onChangeOffset={(newContextImageOffset: number) => {
setContextImageOffset(newContextImageOffset);
}}
onClose={() => {
setShowSelector(false);
}}
/>
)}
</div>
);
}
ContextImage.PropType = {
offset: PropTypes.arrayOf(PropTypes.number),
};
export default React.memo(ContextImage);

@ -0,0 +1,148 @@
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.cvat-context-image-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
> .ant-spin {
position: absolute;
top: 50%;
transform: translate(0, -50%);
}
> .ant-typography {
top: 50%;
position: absolute;
}
.cvat-context-image-header {
position: absolute;
height: $grid-unit-size * 4;
border-radius: 4px 4px 0 0;
width: 100%;
text-align: center;
z-index: 1;
background: $header-color;
overflow: hidden;
> .cvat-context-image-title {
width: calc(100% - $grid-unit-size * 13);
margin-right: $grid-unit-size * 7;
margin-left: $grid-unit-size * 7;
> span.ant-typography {
font-size: 12px;
line-height: $grid-unit-size * 4;
word-break: break-all;
}
}
> .cvat-context-image-setup-button {
font-size: 16px;
opacity: 0;
transition: all 200ms;
position: absolute;
top: $grid-unit-size;
right: $grid-unit-size * 4;
}
> .cvat-context-image-close-button {
font-size: 16px;
opacity: 0;
transition: all 200ms;
position: absolute;
top: $grid-unit-size;
right: $grid-unit-size;
}
}
> canvas {
object-fit: contain;
position: relative;
top: calc(50% + $grid-unit-size * 2);
transform: translateY(-50%);
width: 100%;
height: calc(100% - $grid-unit-size * 4);
}
&:hover {
> .cvat-context-image-header > .cvat-context-image-setup-button {
opacity: 0.6;
&:hover {
opacity: 0.9;
}
}
}
}
.cvat-context-image-overlay {
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.25);
position: absolute;
justify-content: space-around;
align-items: center;
display: flex;
.cvat-context-image-gallery {
width: 80%;
max-height: 80%;
position: relative;
background: white;
padding: $grid-unit-size;
display: block;
justify-content: space-between;
overflow: hidden;
overflow-y: auto;
.cvat-context-image-gallery-items {
display: block;
.cvat-context-image-gallery-item {
text-align: center;
padding: $grid-unit-size;
opacity: 0.6;
width: 25%;
float: left;
&.cvat-context-image-gallery-item-current {
opacity: 1;
}
&:hover {
opacity: 0.9;
}
> canvas {
width: 100%;
}
}
}
.cvat-context-image-gallery-header {
text-align: center;
.cvat-context-image-close-button {
&:hover {
opacity: 0.9;
}
transition: all 200ms;
opacity: 0.6;
position: absolute;
top: $grid-unit-size;
right: $grid-unit-size;
}
}
}
}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,8 +7,8 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper';
import ControlsSideBarContainer from 'containers/annotation-page/review-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarContainer from 'containers/annotation-page/review-workspace/controls-side-bar/controls-side-bar';
import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout';
import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu';
@ -17,7 +18,7 @@ export default function ReviewWorkspaceComponent(): JSX.Element {
return ( return (
<Layout hasSider className='cvat-review-workspace'> <Layout hasSider className='cvat-review-workspace'>
<ControlsSideBarContainer /> <ControlsSideBarContainer />
<CanvasWrapperContainer /> <CanvasLayout />
<ObjectSideBarComponent objectsList={<ObjectsListContainer readonly />} /> <ObjectSideBarComponent objectsList={<ObjectsListContainer readonly />} />
<CanvasContextMenuContainer readonly /> <CanvasContextMenuContainer readonly />
<IssueAggregatorComponent /> <IssueAggregatorComponent />

@ -1,89 +0,0 @@
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import notification from 'antd/lib/notification';
import { useDispatch, useSelector } from 'react-redux';
import { QuestionCircleOutlined, ShrinkOutlined } from '@ant-design/icons';
import Spin from 'antd/lib/spin';
import Image from 'antd/lib/image';
import { CombinedState } from 'reducers';
import { hideShowContextImage, getContextImageAsync } from 'actions/annotation-actions';
import CVATTooltip from 'components/common/cvat-tooltip';
export function adjustContextImagePosition(sidebarCollapsed: boolean): void {
const element = window.document.getElementsByClassName('cvat-context-image-wrapper')[0] as
| HTMLDivElement
| undefined;
if (element) {
if (sidebarCollapsed) {
element.style.right = '40px';
} else {
element.style.right = '';
}
}
}
function ContextImage(): JSX.Element | null {
const dispatch = useDispatch();
const { number: frame, hasRelatedContext } = useSelector((state: CombinedState) => state.annotation.player.frame);
const { data: contextImageData, hidden: contextImageHidden, fetching: contextImageFetching } = useSelector(
(state: CombinedState) => state.annotation.player.contextImage,
);
const [requested, setRequested] = useState(false);
useEffect(() => {
if (requested) {
setRequested(false);
}
}, [frame, contextImageData]);
useEffect(() => {
if (hasRelatedContext && !contextImageHidden && !requested) {
dispatch(getContextImageAsync());
setRequested(true);
}
}, [contextImageHidden, requested, hasRelatedContext]);
if (!hasRelatedContext) {
return null;
}
return (
<div className='cvat-context-image-wrapper' {...(contextImageHidden ? { style: { width: '32px' } } : {})}>
<div className='cvat-context-image-wrapper-header' />
{contextImageFetching ? <Spin size='small' /> : null}
{contextImageHidden ? (
<CVATTooltip title='A context image is available'>
<QuestionCircleOutlined
className='cvat-context-image-switcher'
onClick={() => dispatch(hideShowContextImage(false))}
/>
</CVATTooltip>
) : (
<>
<ShrinkOutlined
className='cvat-context-image-switcher'
onClick={() => dispatch(hideShowContextImage(true))}
/>
<Image
{...(contextImageData ? { src: contextImageData } : {})}
onError={() => {
notification.error({
message: 'Could not display context image',
description: `Source is ${
contextImageData === null ? 'empty' : contextImageData.slice(0, 100)
}`,
});
}}
className='cvat-context-image'
/>
</>
)}
</div>
);
}
export default React.memo(ContextImage);

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -11,11 +12,8 @@ import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs'; import Tabs from 'antd/lib/tabs';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { CombinedState, DimensionType } from 'reducers'; import { CombinedState, DimensionType } from 'reducers';
import LabelsList from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list'; import LabelsList from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list';
import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image';
import { collapseSidebar as collapseSidebarAction } from 'actions/annotation-actions'; import { collapseSidebar as collapseSidebarAction } from 'actions/annotation-actions';
import AppearanceBlock from 'components/annotation-page/appearance-block'; import AppearanceBlock from 'components/annotation-page/appearance-block';
import IssuesListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/issues-list'; import IssuesListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/issues-list';
@ -26,7 +24,6 @@ interface OwnProps {
interface StateToProps { interface StateToProps {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
canvasInstance: Canvas | Canvas3d;
jobInstance: any; jobInstance: any;
} }
@ -38,14 +35,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
sidebarCollapsed, sidebarCollapsed,
canvas: { instance: canvasInstance },
job: { instance: jobInstance }, job: { instance: jobInstance },
}, },
} = state; } = state;
return { return {
sidebarCollapsed, sidebarCollapsed,
canvasInstance,
jobInstance, jobInstance,
}; };
} }
@ -60,15 +55,14 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { const {
sidebarCollapsed, canvasInstance, collapseSidebar, objectsList, jobInstance, sidebarCollapsed, collapseSidebar, objectsList, jobInstance,
} = props; } = props;
const collapse = (): void => { const collapse = (): void => {
const [collapser] = window.document.getElementsByClassName('cvat-objects-sidebar'); const [collapser] = window.document.getElementsByClassName('cvat-objects-sidebar');
const listener = (event: TransitionEvent): void => { const listener = (event: TransitionEvent): void => {
if (event.target && event.propertyName === 'width' && event.target === collapser) { if (event.target && event.propertyName === 'width' && event.target === collapser) {
canvasInstance.fitCanvas(); window.dispatchEvent(new Event('resize'));
canvasInstance.fit();
(collapser as HTMLElement).removeEventListener('transitionend', listener as any); (collapser as HTMLElement).removeEventListener('transitionend', listener as any);
} }
}; };
@ -77,7 +71,6 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E
(collapser as HTMLElement).addEventListener('transitionend', listener as any); (collapser as HTMLElement).addEventListener('transitionend', listener as any);
} }
adjustContextImagePosition(!sidebarCollapsed);
collapseSidebar(); collapseSidebar();
}; };

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporations // Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -7,12 +7,12 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout';
import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu';
import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/canvas-point-context-menu'; import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu';
import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator'; import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator';
import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm';
import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm';
@ -21,7 +21,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
return ( return (
<Layout hasSider className='cvat-standard-workspace'> <Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer /> <ControlsSideBarContainer />
<CanvasWrapperContainer /> <CanvasLayout />
<ObjectSideBarComponent objectsList={<ObjectsListContainer />} /> <ObjectSideBarComponent objectsList={<ObjectsListContainer />} />
<PropagateConfirmComponent /> <PropagateConfirmComponent />
<CanvasContextMenuContainer /> <CanvasContextMenuContainer />

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -7,55 +7,11 @@
.cvat-standard-workspace.ant-layout { .cvat-standard-workspace.ant-layout {
height: 100%; height: 100%;
}
.cvat-context-image-wrapper {
height: auto;
width: $grid-unit-size * 32;
position: absolute;
top: $grid-unit-size;
right: $grid-unit-size;
z-index: 100;
background: black;
display: flex;
flex-direction: column;
justify-content: space-between;
user-select: none;
> .cvat-context-image-wrapper-header { > .ant-layout-content {
height: $grid-unit-size * 4; overflow-y: hidden;
width: 100%; overflow-x: hidden;
z-index: 101;
background: rgba(0, 0, 0, 0.2);
position: absolute;
top: 0;
left: 0;
} }
> .ant-image {
margin: $grid-unit-size * 0.5;
}
> span {
position: absolute;
font-size: 18px;
top: 7px;
right: 7px;
z-index: 102;
color: white;
&:hover {
> svg {
transform: scale(1.2);
}
}
}
}
.cvat-context-image {
width: 100%;
height: auto;
display: block;
} }
.cvat-objects-sidebar-sider { .cvat-objects-sidebar-sider {

@ -1,38 +0,0 @@
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import CameraIcon from '@ant-design/icons/CameraOutlined';
import CVATTooltip from 'components/common/cvat-tooltip';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
import { ActiveControl } from 'reducers';
interface Props {
canvasInstance: Canvas3d | Canvas;
activeControl: ActiveControl;
hideShowContextImage: (hidden: boolean) => void;
contextImageHide: boolean;
}
function PhotoContextControl(props: Props): JSX.Element {
const { activeControl, contextImageHide, hideShowContextImage } = props;
return (
<CVATTooltip title='Photo context show/hide' placement='right'>
<CameraIcon
className={`cvat-context-image-control
cvat-control-side-bar-icon-size ${
activeControl === ActiveControl.PHOTO_CONTEXT ? 'cvat-active-canvas-control' : ''
}`}
onClick={(): void => {
hideShowContextImage(!contextImageHide);
}}
/>
</CVATTooltip>
);
}
export default React.memo(PhotoContextControl);

@ -1,17 +1,18 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper3D'; import { DimensionType } from 'reducers';
import ControlsSideBarContainer from 'containers/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarContainer from 'containers/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar';
import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu';
import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/canvas-point-context-menu'; import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout';
import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu';
import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm';
import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm';
@ -19,7 +20,7 @@ export default function StandardWorkspace3DComponent(): JSX.Element {
return ( return (
<Layout hasSider className='cvat-standard-workspace'> <Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer /> <ControlsSideBarContainer />
<CanvasWrapperContainer /> <CanvasLayout type={DimensionType.DIM_3D} />
<ObjectSideBarComponent objectsList={<ObjectsListContainer />} /> <ObjectSideBarComponent objectsList={<ObjectsListContainer />} />
<PropagateConfirmComponent /> <PropagateConfirmComponent />
<CanvasContextMenuContainer /> <CanvasContextMenuContainer />

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -181,6 +182,8 @@ button.cvat-predictor-button {
.cvat-player-filename-wrapper { .cvat-player-filename-wrapper {
max-width: $grid-unit-size * 30; max-width: $grid-unit-size * 30;
max-height: $grid-unit-size * 3;
line-height: $grid-unit-size * 3;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
user-select: none; user-select: none;
@ -517,3 +520,9 @@ button.cvat-predictor-button {
} }
} }
} }
.cvat-saving-job-modal {
span.anticon {
margin-left: $grid-unit-size * 2;
}
}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -10,14 +11,14 @@
.cvat-tag-annotation-sidebar:not(.ant-layout-sider-collapsed) { .cvat-tag-annotation-sidebar:not(.ant-layout-sider-collapsed) {
background: $background-color-2; background: $background-color-2;
padding: $grid-unit-size * 0.5; padding: $grid-unit-size;
padding-left: $grid-unit-size * 1.25; padding-left: $grid-unit-size;
overflow-y: auto; overflow-y: auto;
} }
.cvat-tag-annotation-sidebar-label-select { .cvat-tag-annotation-sidebar-label-select {
padding-top: $grid-unit-size * 1.25; padding-top: $grid-unit-size;
padding-bottom: $grid-unit-size * 1.8; padding-bottom: $grid-unit-size * 2;
> .ant-col > .ant-select { > .ant-col > .ant-select {
width: $grid-unit-size * 25; width: $grid-unit-size * 25;
@ -25,16 +26,16 @@
} }
.cvat-tag-annotation-sidebar-shortcut-help { .cvat-tag-annotation-sidebar-shortcut-help {
padding-top: $grid-unit-size * 1.8; padding-top: $grid-unit-size;
text-align: center; text-align: center;
} }
.cvat-tag-annotation-sidebar-checkbox-skip-frame { .cvat-tag-annotation-sidebar-checkbox-skip-frame {
padding-bottom: $grid-unit-size * 1.8; padding-bottom: $grid-unit-size;
} }
.cvat-tag-annotation-label-selects { .cvat-tag-annotation-label-selects {
padding-top: $grid-unit-size * 1.25; padding-top: $grid-unit-size;
.ant-select { .ant-select {
width: $grid-unit-size * 29; width: $grid-unit-size * 29;
@ -42,7 +43,7 @@
} }
.cvat-tag-annotation-shortcut-key { .cvat-tag-annotation-shortcut-key {
margin-left: $grid-unit-size * 1.25; margin-left: $grid-unit-size;
} }
} }
@ -52,14 +53,13 @@
.cvat-frame-tags { .cvat-frame-tags {
.ant-tag { .ant-tag {
margin: $grid-unit-size * 0.25;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.ant-tag-close-icon { .ant-tag-close-icon {
margin-left: $grid-unit-size * 0.5; margin-left: $grid-unit-size;
font-size: $grid-unit-size * 1.5; font-size: 12px;
} }
} }
} }
@ -68,7 +68,7 @@
@extend .cvat-frame-tags; @extend .cvat-frame-tags;
position: absolute; position: absolute;
top: $layout-sm-grid-size; top: $grid-unit-size * 4;
left: $grid-unit-size; left: $grid-unit-size;
z-index: 3; z-index: 3;
@ -78,11 +78,11 @@
} }
.cvat-tag-annotation-sidebar-tag-label { .cvat-tag-annotation-sidebar-tag-label {
margin-top: $grid-unit-size * 1.8; margin-top: $grid-unit-size * 2;
} }
.cvat-add-tag-button { .cvat-add-tag-button {
margin-left: $grid-unit-size * 1.25; margin-left: $grid-unit-size;
width: $grid-unit-size * 3.5; width: $grid-unit-size * 4;
height: $grid-unit-size * 3.5; height: $grid-unit-size * 3;
} }

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -22,12 +22,9 @@ import {
changeFrameAsync, changeFrameAsync,
rememberObject, rememberObject,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { getCore, Label, LabelType } from 'cvat-core-wrapper'; import { getCore, Label, LabelType } from 'cvat-core-wrapper';
import { CombinedState, ObjectType } from 'reducers'; import { CombinedState, ObjectType } from 'reducers';
import { filterApplicableForType } from 'utils/filter-applicable-labels'; import { filterApplicableForType } from 'utils/filter-applicable-labels';
import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image';
import LabelSelector from 'components/label-selector/label-selector'; import LabelSelector from 'components/label-selector/label-selector';
import isAbleToChangeFrame from 'utils/is-able-to-change-frame'; import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
@ -39,7 +36,6 @@ interface StateToProps {
states: any[]; states: any[];
labels: any[]; labels: any[];
jobInstance: any; jobInstance: any;
canvasInstance: Canvas | Canvas3d;
frameNumber: number; frameNumber: number;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
@ -61,7 +57,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
}, },
annotations: { states }, annotations: { states },
job: { instance: jobInstance, labels }, job: { instance: jobInstance, labels },
canvas: { instance: canvasInstance },
}, },
shortcuts: { keyMap, normalizedKeyMap }, shortcuts: { keyMap, normalizedKeyMap },
} = state; } = state;
@ -70,7 +65,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance, jobInstance,
labels, labels,
states, states,
canvasInstance: canvasInstance as Canvas | Canvas3d,
frameNumber, frameNumber,
keyMap, keyMap,
normalizedKeyMap, normalizedKeyMap,
@ -102,7 +96,6 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
removeObject, removeObject,
jobInstance, jobInstance,
changeFrame, changeFrame,
canvasInstance,
frameNumber, frameNumber,
onRememberObject, onRememberObject,
createAnnotations, createAnnotations,
@ -143,8 +136,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
(event as TransitionEvent).propertyName === 'width' && (event as TransitionEvent).propertyName === 'width' &&
((event.target as any).classList as DOMTokenList).contains('cvat-tag-annotation-sidebar') ((event.target as any).classList as DOMTokenList).contains('cvat-tag-annotation-sidebar')
) { ) {
canvasInstance.fitCanvas(); window.dispatchEvent(new Event('resize'));
canvasInstance.fit();
} }
}; };
@ -234,7 +226,6 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
ant-layout-sider-zero-width-trigger ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`} ant-layout-sider-zero-width-trigger-left`}
onClick={() => { onClick={() => {
adjustContextImagePosition(!sidebarCollapsed);
setSidebarCollapsed(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed);
}} }}
> >
@ -256,7 +247,6 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen
ant-layout-sider-zero-width-trigger ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`} ant-layout-sider-zero-width-trigger-left`}
onClick={() => { onClick={() => {
adjustContextImagePosition(!sidebarCollapsed);
setSidebarCollapsed(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed);
}} }}
> >

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -6,14 +7,14 @@ import './styles.scss';
import React from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout';
import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm';
import TagAnnotationSidebar from './tag-annotation-sidebar/tag-annotation-sidebar'; import TagAnnotationSidebar from './tag-annotation-sidebar/tag-annotation-sidebar';
export default function TagAnnotationWorkspace(): JSX.Element { export default function TagAnnotationWorkspace(): JSX.Element {
return ( return (
<Layout hasSider className='cvat-tag-annotation-workspace'> <Layout hasSider className='cvat-tag-annotation-workspace'>
<CanvasWrapperContainer /> <CanvasLayout />
<TagAnnotationSidebar /> <TagAnnotationSidebar />
<RemoveConfirmComponent /> <RemoveConfirmComponent />
</Layout> </Layout>

@ -1,13 +1,14 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { Col } from 'antd/lib/grid'; import { Col } from 'antd/lib/grid';
import Icon, { StopOutlined, CheckCircleOutlined } from '@ant-design/icons'; import Icon, { StopOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Timeline from 'antd/lib/timeline'; import Text from 'antd/lib/typography/Text';
import Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotation-menu'; import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotation-menu';
@ -19,7 +20,6 @@ import CVATTooltip from 'components/common/cvat-tooltip';
interface Props { interface Props {
saving: boolean; saving: boolean;
savingStatuses: string[];
undoAction?: string; undoAction?: string;
redoAction?: string; redoAction?: string;
saveShortcut: string; saveShortcut: string;
@ -39,7 +39,6 @@ interface Props {
function LeftGroup(props: Props): JSX.Element { function LeftGroup(props: Props): JSX.Element {
const { const {
saving, saving,
savingStatuses,
undoAction, undoAction,
redoAction, redoAction,
saveShortcut, saveShortcut,
@ -71,12 +70,9 @@ function LeftGroup(props: Props): JSX.Element {
return ( return (
<> <>
<Modal title='Saving changes on the server' visible={saving} footer={[]} closable={false}> <Modal className='cvat-saving-job-modal' title='Saving changes on the server' visible={saving} footer={[]} closable={false}>
<Timeline pending={savingStatuses[savingStatuses.length - 1] || 'Pending..'}> <Text>CVAT is working to save annotations, please wait </Text>
{savingStatuses.slice(0, -1).map((status: string, id: number) => ( <LoadingOutlined />
<Timeline.Item key={id}>{status}</Timeline.Item>
))}
</Timeline>
</Modal> </Modal>
<Col className='cvat-annotation-header-left-group'> <Col className='cvat-annotation-header-left-group'>
<Dropdown overlay={<AnnotationMenuContainer />}> <Dropdown overlay={<AnnotationMenuContainer />}>

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -17,7 +18,6 @@ import RightGroup from './right-group';
interface Props { interface Props {
playing: boolean; playing: boolean;
saving: boolean; saving: boolean;
savingStatuses: string[];
frameNumber: number; frameNumber: number;
frameFilename: string; frameFilename: string;
frameDeleted: boolean; frameDeleted: boolean;
@ -75,7 +75,6 @@ interface Props {
export default function AnnotationTopBarComponent(props: Props): JSX.Element { export default function AnnotationTopBarComponent(props: Props): JSX.Element {
const { const {
saving, saving,
savingStatuses,
undoAction, undoAction,
redoAction, redoAction,
playing, playing,
@ -135,7 +134,6 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
<Row justify='space-between'> <Row justify='space-between'>
<LeftGroup <LeftGroup
saving={saving} saving={saving}
savingStatuses={savingStatuses}
undoAction={undoAction} undoAction={undoAction}
redoAction={redoAction} redoAction={redoAction}
saveShortcut={saveShortcut} saveShortcut={saveShortcut}

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -18,13 +19,13 @@
} }
&.sm { &.sm {
grid-template-rows: repeat(1000, $layout-sm-grid-size); grid-template-rows: repeat(1000, $grid-unit-size);
grid-template-columns: repeat(1000, $layout-sm-grid-size); grid-template-columns: repeat(1000, $grid-unit-size);
&::before, &::before,
&::after { &::after {
background: linear-gradient(to right, $layout-sm-grid-color 1px, transparent 1px); background: linear-gradient(to right, $layout-sm-grid-color 1px, transparent 1px);
background-size: $layout-sm-grid-size; background-size: $grid-unit-size;
} }
&::after { &::after {

@ -24,6 +24,10 @@ const LATEST_COMMENTS_SHOWN_QUICK_ISSUE = 3;
const QUICK_ISSUE_INCORRECT_POSITION_TEXT = 'Wrong position'; const QUICK_ISSUE_INCORRECT_POSITION_TEXT = 'Wrong position';
const QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT = 'Wrong attribute'; const QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT = 'Wrong attribute';
const DEFAULT_PROJECT_SUBSETS = ['Train', 'Test', 'Validation']; const DEFAULT_PROJECT_SUBSETS = ['Train', 'Test', 'Validation'];
const CANVAS_WORKSPACE_ROWS = 12;
const CANVAS_WORKSPACE_COLS = 12;
const CANVAS_WORKSPACE_MARGIN = 8;
const CANVAS_WORKSPACE_PADDING = CANVAS_WORKSPACE_MARGIN / 2;
const OUTSIDE_PIC_URL = 'https://opencv.github.io/cvat/images/image019.jpg'; const OUTSIDE_PIC_URL = 'https://opencv.github.io/cvat/images/image019.jpg';
const DEFAULT_AWS_S3_REGIONS: string[][] = [ const DEFAULT_AWS_S3_REGIONS: string[][] = [
['us-east-1', 'US East (N. Virginia)'], ['us-east-1', 'US East (N. Virginia)'],
@ -114,4 +118,8 @@ export default {
HEALH_CHECK_RETRIES, HEALH_CHECK_RETRIES,
HEALTH_CHECK_PERIOD, HEALTH_CHECK_PERIOD,
HEALTH_CHECK_REQUEST_TIMEOUT, HEALTH_CHECK_REQUEST_TIMEOUT,
CANVAS_WORKSPACE_ROWS,
CANVAS_WORKSPACE_COLS,
CANVAS_WORKSPACE_MARGIN,
CANVAS_WORKSPACE_PADDING,
}; };

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -11,7 +11,7 @@ import {
CombinedState, ContextMenuType, ShapeType, Workspace, CombinedState, ContextMenuType, ShapeType, Workspace,
} from 'reducers'; } from 'reducers';
import CanvasContextMenuComponent from 'components/annotation-page/canvas/canvas-context-menu'; import CanvasContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-context-menu';
import { updateCanvasContextMenu } from 'actions/annotation-actions'; import { updateCanvasContextMenu } from 'actions/annotation-actions';
import { reviewActions, finishIssueAsync } from 'actions/review-actions'; import { reviewActions, finishIssueAsync } from 'actions/review-actions';
import { ThunkDispatch } from 'utils/redux'; import { ThunkDispatch } from 'utils/redux';

@ -1,339 +0,0 @@
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { connect } from 'react-redux';
import { KeyMap } from 'utils/mousetrap-react';
import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper';
import {
confirmCanvasReady,
dragCanvas,
zoomCanvas,
resetCanvas,
shapeDrawn,
mergeObjects,
groupObjects,
splitTrack,
editShape,
updateAnnotationsAsync,
createAnnotationsAsync,
mergeAnnotationsAsync,
groupAnnotationsAsync,
splitAnnotationsAsync,
activateObject,
updateCanvasContextMenu,
addZLayer,
switchZLayer,
fetchAnnotationsAsync,
getDataFailed,
} from 'actions/annotation-actions';
import {
switchGrid,
changeGridColor,
changeGridOpacity,
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
switchAutomaticBordering,
} from 'actions/settings-actions';
import { reviewActions } from 'actions/review-actions';
import {
ColorBy,
GridColor,
ObjectType,
CombinedState,
ContextMenuType,
Workspace,
ActiveControl,
} from 'reducers';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
interface StateToProps {
sidebarCollapsed: boolean;
canvasInstance: Canvas | Canvas3d | null;
jobInstance: any;
activatedStateID: number | null;
activatedElementID: number | null;
activatedAttributeID: number | null;
annotations: any[];
frameData: any;
frameAngle: number;
frameFetching: boolean;
frame: number;
opacity: number;
colorBy: ColorBy;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
showBitmap: boolean;
showProjections: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
resetZoom: boolean;
smoothImage: boolean;
aamZoomMargin: number;
showObjectsTextAlways: boolean;
textFontSize: number;
controlPointsSize: number;
textPosition: 'auto' | 'center';
textContent: string;
showAllInterpolationTracks: boolean;
workspace: Workspace;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean;
intelligentPolygonCrop: boolean;
switchableAutomaticBordering: boolean;
keyMap: KeyMap;
canvasBackgroundColor: string;
showTagsOnFrame: boolean;
}
interface DispatchToProps {
onSetupCanvas(): void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
onResetCanvas: () => void;
onShapeDrawn: () => void;
onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void;
onUpdateAnnotations(states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null, activatedElementID: number | null) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
onFetchAnnotation(): void;
onGetDataFailed(error: any): void;
onStartIssue(position: number[]): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: { activeControl, instance: canvasInstance },
drawing: { activeLabelID, activeObjectType },
job: { instance: jobInstance },
player: {
frame: { data: frameData, number: frame, fetching: frameFetching },
frameAngles,
},
annotations: {
states: annotations,
activatedStateID,
activatedElementID,
activatedAttributeID,
zLayer: { cur: curZLayer, min: minZLayer, max: maxZLayer },
},
sidebarCollapsed,
workspace,
},
settings: {
player: {
canvasBackgroundColor,
grid,
gridSize,
gridColor,
gridOpacity,
brightnessLevel,
contrastLevel,
saturationLevel,
resetZoom,
smoothImage,
},
workspace: {
aamZoomMargin,
showObjectsTextAlways,
showAllInterpolationTracks,
showTagsOnFrame,
automaticBordering,
intelligentPolygonCrop,
textFontSize,
controlPointsSize,
textPosition,
textContent,
},
shapes: {
opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
},
},
shortcuts: { keyMap },
} = state;
return {
sidebarCollapsed,
canvasInstance,
jobInstance,
frameData,
frameAngle: frameAngles[frame - jobInstance.startFrame],
frameFetching,
frame,
activatedStateID,
activatedElementID,
activatedAttributeID,
annotations,
opacity: opacity / 100,
colorBy,
selectedOpacity: selectedOpacity / 100,
outlined,
outlineColor,
showBitmap,
showProjections,
grid,
gridSize,
gridColor,
gridOpacity: gridOpacity / 100,
activeLabelID,
activeObjectType,
brightnessLevel: brightnessLevel / 100,
contrastLevel: contrastLevel / 100,
saturationLevel: saturationLevel / 100,
resetZoom,
smoothImage,
aamZoomMargin,
showObjectsTextAlways,
textFontSize,
controlPointsSize,
textPosition,
textContent,
showAllInterpolationTracks,
showTagsOnFrame,
curZLayer,
minZLayer,
maxZLayer,
automaticBordering,
intelligentPolygonCrop,
workspace,
keyMap,
canvasBackgroundColor,
switchableAutomaticBordering:
activeControl === ActiveControl.DRAW_POLYGON ||
activeControl === ActiveControl.DRAW_POLYLINE ||
activeControl === ActiveControl.DRAW_MASK ||
activeControl === ActiveControl.EDIT,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onSetupCanvas(): void {
dispatch(confirmCanvasReady());
},
onDragCanvas(enabled: boolean): void {
dispatch(dragCanvas(enabled));
},
onZoomCanvas(enabled: boolean): void {
dispatch(zoomCanvas(enabled));
},
onResetCanvas(): void {
dispatch(resetCanvas());
},
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onMergeObjects(enabled: boolean): void {
dispatch(mergeObjects(enabled));
},
onGroupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onSplitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled));
},
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(mergeAnnotationsAsync(sessionInstance, frame, states));
},
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
},
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void {
dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
},
onActivateObject(activatedStateID: number | null, activatedElementID: number | null = null): void {
if (activatedStateID === null) {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID, activatedElementID, null));
},
onUpdateContextMenu(
visible: boolean,
left: number,
top: number,
type: ContextMenuType,
pointID?: number,
): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
},
onAddZLayer(): void {
dispatch(addZLayer());
},
onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur));
},
onChangeBrightnessLevel(level: number): void {
dispatch(changeBrightnessLevel(level));
},
onChangeContrastLevel(level: number): void {
dispatch(changeContrastLevel(level));
},
onChangeSaturationLevel(level: number): void {
dispatch(changeSaturationLevel(level));
},
onChangeGridOpacity(opacity: number): void {
dispatch(changeGridOpacity(opacity));
},
onChangeGridColor(color: GridColor): void {
dispatch(changeGridColor(color));
},
onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled));
},
onSwitchAutomaticBordering(enabled: boolean): void {
dispatch(switchAutomaticBordering(enabled));
},
onFetchAnnotation(): void {
dispatch(fetchAnnotationsAsync());
},
onGetDataFailed(error: any): void {
dispatch(getDataFailed(error));
},
onStartIssue(position: number[]): void {
dispatch(reviewActions.startIssue(position));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent);

@ -1,165 +0,0 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { connect } from 'react-redux';
import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper3D';
import {
activateObject,
confirmCanvasReady,
createAnnotationsAsync,
dragCanvas,
editShape,
groupAnnotationsAsync,
groupObjects,
resetCanvas,
shapeDrawn,
updateAnnotationsAsync,
updateCanvasContextMenu,
} from 'actions/annotation-actions';
import {
ColorBy,
CombinedState,
ContextMenuType,
ObjectType,
Workspace,
} from 'reducers';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
interface StateToProps {
opacity: number;
selectedOpacity: number;
outlined: boolean;
outlineColor: string;
colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d | Canvas;
jobInstance: any;
frameData: any;
annotations: any[];
contextMenuVisibility: boolean;
activeLabelID: number;
activatedStateID: number | null;
activeObjectType: ObjectType;
workspace: Workspace;
frame: number;
resetZoom: boolean;
}
interface DispatchToProps {
onDragCanvas: (enabled: boolean) => void;
onSetupCanvas(): void;
onGroupObjects: (enabled: boolean) => void;
onResetCanvas(): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onUpdateAnnotations(states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onActivateObject: (activatedStateID: number | null) => void;
onShapeDrawn: () => void;
onEditShape: (enabled: boolean) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: {
instance: canvasInstance,
contextMenu: { visible: contextMenuVisibility },
},
drawing: { activeLabelID, activeObjectType },
job: { instance: jobInstance },
player: {
frame: { data: frameData, number: frame, fetching: frameFetching },
},
annotations: {
states: annotations,
activatedStateID,
},
workspace,
},
settings: {
player: {
resetZoom,
},
shapes: {
opacity, colorBy, selectedOpacity, outlined, outlineColor,
},
},
} = state;
return {
canvasInstance,
jobInstance,
frameData,
contextMenuVisibility,
annotations,
frameFetching,
frame,
opacity,
colorBy,
selectedOpacity,
outlined,
outlineColor,
activeLabelID,
activatedStateID,
activeObjectType,
resetZoom,
workspace,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onDragCanvas(enabled: boolean): void {
dispatch(dragCanvas(enabled));
},
onSetupCanvas(): void {
dispatch(confirmCanvasReady());
},
onResetCanvas(): void {
dispatch(resetCanvas());
},
onGroupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
},
onActivateObject(activatedStateID: number | null): void {
if (activatedStateID === null) {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID, null, null));
},
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onUpdateContextMenu(
visible: boolean,
left: number,
top: number,
type: ContextMenuType,
pointID?: number,
): void {
dispatch(updateCanvasContextMenu(visible, left, top, pointID, type));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent);

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -56,7 +57,6 @@ interface StateToProps {
playing: boolean; playing: boolean;
saving: boolean; saving: boolean;
canvasIsReady: boolean; canvasIsReady: boolean;
savingStatuses: string[];
undoAction?: string; undoAction?: string;
redoAction?: string; redoAction?: string;
autoSave: boolean; autoSave: boolean;
@ -106,7 +106,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
}, },
}, },
annotations: { annotations: {
saving: { uploading: saving, statuses: savingStatuses, forceExit }, saving: { uploading: saving, forceExit },
history, history,
}, },
job: { instance: jobInstance }, job: { instance: jobInstance },
@ -135,7 +135,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
playing, playing,
canvasIsReady, canvasIsReady,
saving, saving,
savingStatuses,
frameNumber, frameNumber,
frameFilename, frameFilename,
jobInstance, jobInstance,
@ -607,7 +606,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
const { const {
playing, playing,
saving, saving,
savingStatuses,
jobInstance, jobInstance,
jobInstance: { startFrame, stopFrame }, jobInstance: { startFrame, stopFrame },
frameNumber, frameNumber,
@ -751,7 +749,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
workspace={workspace} workspace={workspace}
playing={playing} playing={playing}
saving={saving} saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame} startFrame={startFrame}
stopFrame={stopFrame} stopFrame={stopFrame}
frameNumber={frameNumber} frameNumber={frameNumber}

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -65,7 +65,7 @@ const defaultState: AnnotationState = {
number: 0, number: 0,
filename: '', filename: '',
data: null, data: null,
hasRelatedContext: false, relatedFiles: 0,
fetching: false, fetching: false,
delay: 0, delay: 0,
changeTime: null, changeTime: null,
@ -73,11 +73,6 @@ const defaultState: AnnotationState = {
playing: false, playing: false,
frameAngles: [], frameAngles: [],
navigationBlocked: false, navigationBlocked: false,
contextImage: {
fetching: false,
data: null,
hidden: false,
},
}, },
drawing: { drawing: {
activeShapeType: ShapeType.RECTANGLE, activeShapeType: ShapeType.RECTANGLE,
@ -91,7 +86,6 @@ const defaultState: AnnotationState = {
saving: { saving: {
forceExit: false, forceExit: false,
uploading: false, uploading: false,
statuses: [],
}, },
collapsed: {}, collapsed: {},
collapsedAll: true, collapsedAll: true,
@ -160,7 +154,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
openTime, openTime,
frameNumber: number, frameNumber: number,
frameFilename: filename, frameFilename: filename,
frameHasRelatedContext, relatedFiles,
colors, colors,
filters, filters,
frameData: data, frameData: data,
@ -213,7 +207,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
frame: { frame: {
...state.player.frame, ...state.player.frame,
filename, filename,
hasRelatedContext: frameHasRelatedContext, relatedFiles,
number, number,
data, data,
}, },
@ -277,7 +271,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
number, number,
data, data,
filename, filename,
hasRelatedContext, relatedFiles,
states, states,
minZ, minZ,
maxZ, maxZ,
@ -293,16 +287,12 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
frame: { frame: {
data, data,
filename, filename,
hasRelatedContext, relatedFiles,
number, number,
fetching: false, fetching: false,
changeTime, changeTime,
delay, delay,
}, },
contextImage: {
...state.player.contextImage,
...(state.player.frame.number === number ? {} : { data: null }),
},
}, },
annotations: { annotations: {
...state.annotations, ...state.annotations,
@ -351,7 +341,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
saving: { saving: {
...state.annotations.saving, ...state.annotations.saving,
uploading: true, uploading: true,
statuses: [],
}, },
}, },
}; };
@ -385,20 +374,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS: {
const { status } = action.payload;
return {
...state,
annotations: {
...state.annotations,
saving: {
...state.annotations.saving,
statuses: [...state.annotations.saving.statuses, status],
},
},
};
}
case AnnotationActionTypes.SWITCH_PLAY: { case AnnotationActionTypes.SWITCH_PLAY: {
const { playing } = action.payload; const { playing } = action.payload;
@ -1188,58 +1163,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE: {
const { hidden } = action.payload;
return {
...state,
player: {
...state.player,
contextImage: {
...state.player.contextImage,
hidden,
},
},
};
}
case AnnotationActionTypes.GET_CONTEXT_IMAGE: {
return {
...state,
player: {
...state.player,
contextImage: {
...state.player.contextImage,
fetching: true,
},
},
};
}
case AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS: {
const { contextImageData } = action.payload;
return {
...state,
player: {
...state.player,
contextImage: {
...state.player.contextImage,
fetching: false,
data: contextImageData,
},
},
};
}
case AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED: {
return {
...state,
player: {
...state.player,
contextImage: {
...state.player.contextImage,
fetching: false,
},
},
};
}
case AnnotationActionTypes.SWITCH_NAVIGATION_BLOCKED: { case AnnotationActionTypes.SWITCH_NAVIGATION_BLOCKED: {
return { return {
...state, ...state,

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -457,7 +457,6 @@ export interface NotificationsState {
saving: null | ErrorState; saving: null | ErrorState;
jobFetching: null | ErrorState; jobFetching: null | ErrorState;
frameFetching: null | ErrorState; frameFetching: null | ErrorState;
contextImageFetching: null | ErrorState;
changingLabelColor: null | ErrorState; changingLabelColor: null | ErrorState;
updating: null | ErrorState; updating: null | ErrorState;
creating: null | ErrorState; creating: null | ErrorState;
@ -669,7 +668,7 @@ export interface AnnotationState {
frame: { frame: {
number: number; number: number;
filename: string; filename: string;
hasRelatedContext: boolean; relatedFiles: number;
data: any | null; data: any | null;
fetching: boolean; fetching: boolean;
delay: number; delay: number;
@ -678,11 +677,6 @@ export interface AnnotationState {
navigationBlocked: boolean; navigationBlocked: boolean;
playing: boolean; playing: boolean;
frameAngles: number[]; frameAngles: number[];
contextImage: {
fetching: boolean;
data: string | null;
hidden: boolean;
};
}; };
drawing: { drawing: {
activeInteractor?: Model | OpenCVTool; activeInteractor?: Model | OpenCVTool;
@ -710,7 +704,6 @@ export interface AnnotationState {
saving: { saving: {
forceExit: boolean; forceExit: boolean;
uploading: boolean; uploading: boolean;
statuses: string[];
}; };
zLayer: { zLayer: {
min: number; min: number;

@ -85,7 +85,6 @@ const defaultState: NotificationsState = {
saving: null, saving: null,
jobFetching: null, jobFetching: null,
frameFetching: null, frameFetching: null,
contextImageFetching: null,
changingLabelColor: null, changingLabelColor: null,
updating: null, updating: null,
creating: null, creating: null,
@ -835,21 +834,6 @@ export default function (state = defaultState, action: AnyAction): Notifications
}, },
}; };
} }
case AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
contextImageFetching: {
message: 'Could not fetch context image from the server',
reason: action.payload.error,
},
},
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: { case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: {
return { return {
...state, ...state,

@ -3,9 +3,12 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os import os
import io
import zipfile
from io import BytesIO from io import BytesIO
from datetime import datetime from datetime import datetime
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import cv2
import pytz import pytz
from django.core.cache import cache from django.core.cache import cache
@ -16,7 +19,7 @@ from cvat.apps.engine.log import slogger
from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter, from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter,
Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter, Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter,
ImageDatasetManifestReader, VideoDatasetManifestReader) ImageDatasetManifestReader, VideoDatasetManifestReader)
from cvat.apps.engine.models import DataChoice, StorageChoice from cvat.apps.engine.models import DataChoice, StorageChoice, Image
from cvat.apps.engine.models import DimensionType from cvat.apps.engine.models import DimensionType
from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials
from cvat.apps.engine.utils import md5_hash from cvat.apps.engine.utils import md5_hash
@ -34,7 +37,8 @@ class MediaCache:
item = cache.get(key) item = cache.get(key)
if not item: if not item:
item = create_function() item = create_function()
cache.set(key, item) if item[0]:
cache.set(key, item)
return item return item
@ -62,13 +66,20 @@ class MediaCache:
return item return item
def get_frame_context_images(self, db_data, frame_number):
item = self._get_or_set_cache_item(
key=f'context_image_{db_data.id}_{frame_number}',
create_function=lambda: self._prepare_context_image(db_data, frame_number)
)
return item
@staticmethod @staticmethod
def _get_frame_provider(): def _get_frame_provider():
from cvat.apps.engine.frame_provider import FrameProvider # TODO: remove circular dependency from cvat.apps.engine.frame_provider import FrameProvider # TODO: remove circular dependency
return FrameProvider return FrameProvider
def _prepare_chunk_buff(self, db_data, quality, chunk_number): def _prepare_chunk_buff(self, db_data, quality, chunk_number):
FrameProvider = self._get_frame_provider() FrameProvider = self._get_frame_provider()
writer_classes = { writer_classes = {
@ -183,3 +194,25 @@ class MediaCache:
mime_type = mimetypes.guess_type(preview_path)[0] mime_type = mimetypes.guess_type(preview_path)[0]
return buff, mime_type return buff, mime_type
def _prepare_context_image(self, db_data, frame_number):
zip_buffer = io.BytesIO()
try:
image = Image.objects.get(data_id=db_data.id, frame=frame_number)
except Image.DoesNotExist:
return None, None
with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:
if not image.related_files.count():
return None, None
common_path = os.path.commonpath(list(map(lambda x: str(x.path), image.related_files.all())))
for i in image.related_files.all():
path = os.path.realpath(str(i.path))
name = os.path.relpath(str(i.path), common_path)
image = cv2.imread(path)
success, result = cv2.imencode('.JPEG', image)
if not success:
raise Exception('Failed to encode image to ".jpeg" format')
zip_file.writestr(f'{name}.jpg', result.tobytes())
buff = zip_buffer.getvalue()
mime_type = 'application/zip'
return buff, mime_type

@ -1,5 +1,5 @@
# Copyright (C) 2019-2022 Intel Corporation # Copyright (C) 2019-2022 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation # Copyright (C) 2022-2023 CVAT.ai Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -924,7 +924,7 @@ class FrameMetaSerializer(serializers.Serializer):
width = serializers.IntegerField() width = serializers.IntegerField()
height = serializers.IntegerField() height = serializers.IntegerField()
name = serializers.CharField(max_length=1024) name = serializers.CharField(max_length=1024)
has_related_context = serializers.BooleanField() related_files = serializers.IntegerField()
class PluginsSerializer(serializers.Serializer): class PluginsSerializer(serializers.Serializer):
GIT_INTEGRATION = serializers.BooleanField() GIT_INTEGRATION = serializers.BooleanField()

@ -1,5 +1,5 @@
# Copyright (C) 2018-2022 Intel Corporation # Copyright (C) 2018-2022 Intel Corporation
# Copyright (C) 2022 CVAT.ai Corporation # Copyright (C) 2022-2023 CVAT.ai Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
@ -13,7 +13,6 @@ from datetime import datetime
from distutils.util import strtobool from distutils.util import strtobool
from tempfile import mkstemp from tempfile import mkstemp
import cv2
from django.db.models.query import Prefetch from django.db.models.query import Prefetch
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
import django_rq import django_rq
@ -48,7 +47,7 @@ from cvat.apps.engine.frame_provider import FrameProvider
from cvat.apps.engine.media_extractors import get_mime from cvat.apps.engine.media_extractors import get_mime
from cvat.apps.engine.models import ( from cvat.apps.engine.models import (
Job, Task, Project, Issue, Data, Job, Task, Project, Issue, Data,
Comment, StorageMethodChoice, StorageChoice, Image, Comment, StorageMethodChoice, StorageChoice,
CloudProviderChoice, Location CloudProviderChoice, Location
) )
from cvat.apps.engine.models import CloudStorage as CloudStorageModel from cvat.apps.engine.models import CloudStorage as CloudStorageModel
@ -636,7 +635,6 @@ class ProjectViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
return response return response
class DataChunkGetter: class DataChunkGetter:
def __init__(self, data_type, data_num, data_quality, task_dim): def __init__(self, data_type, data_num, data_quality, task_dim):
possible_data_type_values = ('chunk', 'frame', 'preview', 'context_image') possible_data_type_values = ('chunk', 'frame', 'preview', 'context_image')
@ -696,20 +694,14 @@ class DataChunkGetter:
return HttpResponse(buf.getvalue(), content_type=mime) return HttpResponse(buf.getvalue(), content_type=mime)
elif self.type == 'context_image': elif self.type == 'context_image':
if not (start <= self.number <= stop): if start <= self.number <= stop:
raise ValidationError('The frame number should be in ' + cache = MediaCache(self.dimension)
f'[{start}, {stop}] range') buff, mime = cache.get_frame_context_images(db_data, self.number)
if not buff:
image = Image.objects.get(data_id=db_data.id, frame=self.number) return HttpResponseNotFound()
for i in image.related_files.all(): return HttpResponse(io.BytesIO(buff), content_type=mime)
path = os.path.realpath(str(i.path)) raise ValidationError('The frame number should be in ' +
image = cv2.imread(path) f'[{start}, {stop}] range')
success, result = cv2.imencode('.JPEG', image)
if not success:
raise Exception('Failed to encode image to ".jpeg" format')
return HttpResponse(io.BytesIO(result.tobytes()), content_type='image/jpeg')
return Response(data='No context image related to the frame',
status=status.HTTP_404_NOT_FOUND)
else: else:
return Response(data='unknown data type {}.'.format(self.type), return Response(data='unknown data type {}.'.format(self.type),
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
@ -1275,7 +1267,7 @@ class TaskViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
'width': item.width, 'width': item.width,
'height': item.height, 'height': item.height,
'name': item.path, 'name': item.path,
'has_related_context': hasattr(item, 'related_files') and item.related_files.exists() 'related_files': item.related_files.count() if hasattr(item, 'related_files') else 0
} for item in media] } for item in media]
db_data = db_task.data db_data = db_task.data
@ -1747,7 +1739,7 @@ class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
'width': item.width, 'width': item.width,
'height': item.height, 'height': item.height,
'name': item.path, 'name': item.path,
'has_related_context': hasattr(item, 'related_files') and item.related_files.exists() 'related_files': item.related_files.count() if hasattr(item, 'related_files') else 0
} for item in media] } for item in media]
db_data.frames = frame_meta db_data.frames = frame_meta

@ -73,7 +73,7 @@ context('Actions on ellipse.', () => {
it('Ellipse rotation/interpolation.', () => { it('Ellipse rotation/interpolation.', () => {
Cypress.config('scrollBehavior', false); Cypress.config('scrollBehavior', false);
cy.get('.cvat-player-last-button').click(); cy.get('.cvat-player-last-button').click();
cy.shapeRotate('#cvat_canvas_shape_4', '19.5'); cy.shapeRotate('#cvat_canvas_shape_4', '19.7');
testCompareRotate('cvat_canvas_shape_4', 0); testCompareRotate('cvat_canvas_shape_4', 0);
// Rotation with shift // Rotation with shift
cy.shapeRotate('#cvat_canvas_shape_4', '15.0', true); cy.shapeRotate('#cvat_canvas_shape_4', '15.0', true);

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -35,8 +36,8 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', ()
labelName, labelName,
tracker: 'TrackerMIL', tracker: 'TrackerMIL',
pointsMap: [ pointsMap: [
{ x: 440, y: 45 }, { x: 430, y: 40 },
{ x: 650, y: 150 }, { x: 640, y: 145 },
], ],
}; };
@ -211,10 +212,10 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', ()
// On each frame text is moved by 5px on x and y axis, // On each frame text is moved by 5px on x and y axis,
// so we expect shape to be close to real text positions // so we expect shape to be close to real text positions
cy.get('#cvat_canvas_shape_3').invoke('attr', 'x').then((xVal) => { cy.get('#cvat_canvas_shape_3').invoke('attr', 'x').then((xVal) => {
expect(parseFloat(xVal)).to.be.closeTo(x + (i - 1) * 5, 1.0); expect(parseFloat(xVal)).to.be.closeTo(x + (i - 1) * 5, 2.0);
}); });
cy.get('#cvat_canvas_shape_3').invoke('attr', 'y').then((yVal) => { cy.get('#cvat_canvas_shape_3').invoke('attr', 'y').then((yVal) => {
expect(parseFloat(yVal)).to.be.closeTo(y + (i - 1) * 5, 1.0); expect(parseFloat(yVal)).to.be.closeTo(y + (i - 1) * 5, 2.0);
}); });
cy.get('#cvat-objects-sidebar-state-item-3') cy.get('#cvat-objects-sidebar-state-item-3')
.should('contain', 'RECTANGLE TRACK') .should('contain', 'RECTANGLE TRACK')

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -45,29 +46,28 @@ context('Settings. Text size/position. Text labels content.', () => {
let textTopPosition = 0; let textTopPosition = 0;
let getText; let getText;
cy.get(shape).then(($shape) => { cy.get(shape).then(([shapeObj]) => {
shapeLeftPosition = Math.trunc($shape.position().left); const shapeBBox = shapeObj.getBoundingClientRect();
shapeTopPosition = Math.trunc($shape.position().top); shapeLeftPosition = shapeBBox.left;
if (shape === '#cvat_canvas_shape_1') { shapeTopPosition = shapeBBox.top;
shapeWidth = $shape.attr('width'); shapeWidth = shapeBBox.width;
shapeHeight = $shape.attr('height'); shapeHeight = shapeBBox.height;
} else {
const points = $shape.attr('points').split(' ');
shapeWidth = +points[1].split(',')[0] - +points[0].split(',')[0];
shapeHeight = +points[2].split(',')[1] - +points[0].split(',')[1];
}
if (shape === '#cvat_canvas_shape_1') { if (shape === '#cvat_canvas_shape_1') {
getText = cy.get('.cvat_canvas_text').first(); getText = cy.get('.cvat_canvas_text').first();
} else { } else {
getText = cy.get('.cvat_canvas_text').last(); getText = cy.get('.cvat_canvas_text').last();
} }
getText.then(($text) => {
textLeftPosition = Math.trunc($text.position().left); getText.then(([textObj]) => {
textTopPosition = Math.trunc($text.position().top); const textBBox = textObj.getBoundingClientRect();
textLeftPosition = textBBox.left;
textTopPosition = textBBox.top;
if (expectedPosition === 'outside') { if (expectedPosition === 'outside') {
// Text outside the shape of the right. Slightly below the shape upper edge. // Text outside the shape of the right. Slightly below the shape upper edge.
expect(+shapeLeftPosition + +shapeWidth).lessThan(+textLeftPosition); expect(+shapeLeftPosition + +shapeWidth).lessThan(+textLeftPosition);
expect(+textTopPosition).to.be.within(+shapeTopPosition, +shapeTopPosition + 10); expect(+textTopPosition).to.be.within(+shapeTopPosition, +shapeTopPosition + 15);
} else { } else {
// Text inside the shape // Text inside the shape
expect(+shapeLeftPosition + +shapeWidth / 2).greaterThan(+textLeftPosition); expect(+shapeLeftPosition + +shapeWidth / 2).greaterThan(+textLeftPosition);
@ -108,6 +108,21 @@ context('Settings. Text size/position. Text labels content.', () => {
}); });
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Text font size.', () => {
cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 14px;');
cy.openSettings();
// Change the text size to 16
cy.get('.cvat-workspace-settings-text-size')
.find('input')
.should('have.attr', 'value', '14')
.clear()
.type('10')
.should('have.attr', 'value', '10');
cy.closeSettings();
cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 10px;');
});
it('Text position.', () => { it('Text position.', () => {
testTextPosition('#cvat_canvas_shape_1', 'outside'); testTextPosition('#cvat_canvas_shape_1', 'outside');
testTextPosition('#cvat_canvas_shape_2', 'outside'); testTextPosition('#cvat_canvas_shape_2', 'outside');
@ -127,21 +142,6 @@ context('Settings. Text size/position. Text labels content.', () => {
testTextPosition('#cvat_canvas_shape_2', 'inside'); testTextPosition('#cvat_canvas_shape_2', 'inside');
}); });
it('Text font size.', () => {
cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 14px;');
cy.openSettings();
// Change the text size to 16
cy.get('.cvat-workspace-settings-text-size')
.find('input')
.should('have.attr', 'value', '14')
.clear()
.type('16')
.should('have.attr', 'value', '16');
cy.closeSettings();
cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 16px;');
});
it('Text labels content.', () => { it('Text labels content.', () => {
cy.openSettings(); cy.openSettings();
cy.get('.cvat-workspace-settings-text-content').within(() => { cy.get('.cvat-workspace-settings-text-content').within(() => {

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -29,10 +30,11 @@ context('Canvas color feature', () => {
.click() .click()
.should('have.css', 'background-color') .should('have.css', 'background-color')
.then((colorPickerBgValue) => { .then((colorPickerBgValue) => {
cy.get('.cvat-canvas-container') cy.get('.cvat-canvas-grid-root')
.should('have.css', 'background-color') .should('have.css', 'background-color')
.then((canvasBgColor) => { .then((canvasBgColor) => {
//For each color change, compare the value with the css value background-color of .cvat-canvas-container // For each color change compare
// the value with the css value background-color of .cvat-canvas-grid-root
expect(colorPickerBgValue).to.be.equal(canvasBgColor); expect(colorPickerBgValue).to.be.equal(canvasBgColor);
}); });
}); });

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -13,7 +14,7 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi
const createRectangleShape2Points = { const createRectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 250, firstX: 250,
firstY: 350, firstY: 350,
secondX: 350, secondX: 350,
@ -21,11 +22,10 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi
}; };
function checkEqualBackground() { function checkEqualBackground() {
cy.get('#cvat_canvas_background') cy.get('.cvat-canvas-grid-root')
.should('have.css', 'left') .then((el) => {
.and((currentValueLeftBackground) => { expect(el[0].getBoundingClientRect().left)
currentValueLeftBackground = Number(currentValueLeftBackground.match(/\d+/)); .to.be.eq(defaultValueLeftBackground);
expect(currentValueLeftBackground).to.be.eq(defaultValueLeftBackground);
}); });
} }
@ -34,10 +34,9 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi
cy.createRectangle(createRectangleShape2Points); cy.createRectangle(createRectangleShape2Points);
// get default left value from background // get default left value from background
cy.get('#cvat_canvas_background') cy.get('.cvat-canvas-grid-root')
.should('have.css', 'left') .then((el) => {
.then((currentValueLeftBackground) => { defaultValueLeftBackground = el[0].getBoundingClientRect().left;
defaultValueLeftBackground = Number(currentValueLeftBackground.match(/\d+/));
}); });
}); });
@ -46,28 +45,15 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi
// hide sidebar // hide sidebar
cy.get('.cvat-objects-sidebar-sider').click(); cy.get('.cvat-objects-sidebar-sider').click();
cy.get('.cvat-objects-sidebar').should('not.be.visible'); cy.get('.cvat-objects-sidebar').should('not.be.visible');
cy.get('#cvat_canvas_background')
.should('have.css', 'left')
.and((currentValueLeftBackground) => {
currentValueLeftBackground = Number(currentValueLeftBackground.match(/\d+/));
expect(currentValueLeftBackground).to.be.greaterThan(defaultValueLeftBackground);
});
// Check issue 3250
cy.get('#cvat_canvas_content').invoke('attr', 'style').then((canvasContainerStyle) => {
cy.viewport(2999, 2999); // Resize window
cy.get('#cvat_canvas_content').should('have.attr', 'style').and('not.equal', canvasContainerStyle);
cy.viewport(Cypress.config('viewportWidth'), Cypress.config('viewportHeight')); // Return to the original size
cy.get('#cvat_canvas_content').should('have.attr', 'style').and('equal', canvasContainerStyle);
});
// unhide sidebar // unhide sidebar
cy.get('.cvat-objects-sidebar-sider').click(); cy.get('.cvat-objects-sidebar-sider').click();
cy.get('.cvat-objects-sidebar').should('be.visible'); cy.get('.cvat-objects-sidebar').should('be.visible');
checkEqualBackground(); checkEqualBackground();
// Before the issue fix the sidebar item did not appear accordingly it was not possible to activate the shape through the sidebar item // Before the issue fix the sidebar item did not appear accordingly
cy.get(`#cvat-objects-sidebar-state-item-1`).trigger('mouseover'); // it was not possible to activate the shape through the sidebar item
cy.get('#cvat-objects-sidebar-state-item-1').trigger('mouseover');
cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated');
}); });

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -11,7 +12,7 @@ context('Attribute annotation mode (AAM) zoom margin feature', () => {
const rectangleShape2Points = { const rectangleShape2Points = {
points: 'By 2 Points', points: 'By 2 Points',
type: 'Shape', type: 'Shape',
labelName: labelName, labelName,
firstX: 100, firstX: 100,
firstY: 100, firstY: 100,
secondX: 150, secondX: 150,
@ -43,6 +44,8 @@ context('Attribute annotation mode (AAM) zoom margin feature', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Change AAM zoom margin on workspace with rectangle', () => { it('Change AAM zoom margin on workspace with rectangle', () => {
cy.get('.cvat-attribute-annotation-sidebar-object-switcher-right').click();
cy.get('.cvat-attribute-annotation-sidebar-object-switcher-left').click();
cy.get('.cvat-attribute-annotation-sidebar-object-switcher').should('contain', `${labelName} 1 [1/2]`); cy.get('.cvat-attribute-annotation-sidebar-object-switcher').should('contain', `${labelName} 1 [1/2]`);
cy.getScaleValue().then((scaleBeforeChangeZoomMargin) => { cy.getScaleValue().then((scaleBeforeChangeZoomMargin) => {
changeSettingsZoomMargin(150); changeSettingsZoomMargin(150);

@ -61,7 +61,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => {
.trigger('mousemove') .trigger('mousemove')
.trigger('mouseover'); .trigger('mouseover');
cy.get('.svg_select_points_rot').trigger('mousedown', { button: 0 }); cy.get('.svg_select_points_rot').trigger('mousedown', { button: 0 });
cy.get('.cvat-canvas-container').trigger('mousemove', 340, 150); cy.get('.cvat-canvas-container').trigger('mousemove', 345, 150);
cy.get('.cvat-canvas-container').trigger('mouseup'); cy.get('.cvat-canvas-container').trigger('mouseup');
cy.get('#cvat_canvas_shape_1').should('have.attr', 'transform'); cy.get('#cvat_canvas_shape_1').should('have.attr', 'transform');
cy.document().then((doc) => { cy.document().then((doc) => {

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -11,36 +12,6 @@ context('Context images for 2D tasks.', () => {
const attrName = `Attr for ${labelName}`; const attrName = `Attr for ${labelName}`;
const textDefaultValue = 'color'; const textDefaultValue = 'color';
const pathToArchive = `../../${__dirname}/assets/case_90/case_90_context_image.zip`; const pathToArchive = `../../${__dirname}/assets/case_90/case_90_context_image.zip`;
const createRectangleShape2Points = {
points: 'By 2 Points',
type: 'Shape',
labelName: labelName,
firstX: 250,
firstY: 350,
secondX: 350,
secondY: 450,
};
function previewRotate(directionRotation, expectedDeg) {
if (directionRotation === 'right') {
cy.get('[data-icon="rotate-right"]').click();
} else {
cy.get('[data-icon="rotate-left"]').click();
}
cy.get('.ant-image-preview-img').should('have.attr', 'style').and('contain', `rotate(${expectedDeg}deg)`);
}
function previewScaleWheel(zoom, expectedScaleValue) {
cy.get('.ant-image-preview-img')
.trigger('wheel', { deltaY: zoom })
.should('have.attr', 'style')
.and('contain', `scale3d(${expectedScaleValue})`);
}
function previewScaleButton(zoom, expectedScaleValue) {
cy.get(`[data-icon="zoom-${zoom}"]`).click();
cy.get('.ant-image-preview-img').should('have.attr', 'style').and('contain', `scale3d(${expectedScaleValue})`);
}
before(() => { before(() => {
cy.visit('auth/login'); cy.visit('auth/login');
@ -56,65 +27,10 @@ context('Context images for 2D tasks.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Check a context image.', () => { it('Check a context image.', () => {
cy.get('.cvat-context-image').should('exist').and('be.visible'); cy.get('.cvat-context-image-wrapper').should('exist').and('be.visible');
cy.get('.cvat-player-next-button').click(); cy.get('.cvat-player-next-button').click();
cy.get('.cvat-context-image').should('exist').and('be.visible'); // Check a context image on the second frame cy.get('.cvat-context-image-wrapper').should('exist').and('be.visible'); // Check a context image on the second frame
cy.get('.cvat-player-previous-button').click(); cy.get('.cvat-player-previous-button').click();
cy.get('.cvat-context-image-switcher').click(); // Hide a context image
cy.get('.cvat-context-image').should('not.exist');
cy.get('.cvat-context-image-switcher').click(); // Unhide
cy.get('.cvat-context-image').should('exist').and('be.visible');
});
it('Preview a context image. Rotate.', () => {
let degRight = 0;
let degLeft = 360;
cy.contains('Preview').click();
cy.get('.ant-image-preview-mask').should('exist');
for (let numberSpins = 0; numberSpins < 4; numberSpins++) {
degRight += 90;
previewRotate('right', String(degRight));
}
for (let numberSpins = 0; numberSpins < 4; numberSpins++) {
degLeft -= 90;
previewRotate('left', String(degLeft));
}
});
it('Preview a context image. Scale.', () => {
previewScaleWheel(-1, '2, 2, 1');
previewScaleWheel(1, '1, 1, 1');
previewScaleButton('in', '2, 2, 1');
previewScaleButton('out', '1, 1, 1');
});
it('Preview a context image. Move.', () => {
cy.get('.ant-image-preview-img-wrapper')
.should('have.attr', 'style')
.then((translate3d) => {
cy.get('.ant-image-preview-img').trigger('mousedown', { button: 0 });
cy.get('.ant-image-preview-moving').should('exist');
cy.get('.ant-image-preview-wrap').trigger('mousemove', 300, 300);
cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('not.equal', translate3d);
cy.get('.ant-image-preview-img').trigger('mouseup');
cy.get('.ant-image-preview-moving').should('not.exist');
cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('equal', translate3d);
});
});
it('Preview a context image. Cancel preview.', () => {
cy.get('.ant-image-preview-wrap').type('{Esc}');
cy.get('.ant-image-preview-wrap').should('have.attr', 'style').and('contain', 'display: none');
});
it('Checking issue "Context image disappears after undo/redo".', () => {
cy.createRectangle(createRectangleShape2Points);
cy.contains('.cvat-annotation-header-button', 'Undo').click();
cy.get('.cvat-context-image').should('have.attr', 'src');
cy.get('#cvat_canvas_shape_1').should('not.exist');
cy.contains('.cvat-annotation-header-button', 'Redo').click();
cy.get('.cvat-context-image').should('have.attr', 'src');
cy.get('#cvat_canvas_shape_1').should('exist');
}); });
}); });
}); });

@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -14,22 +14,22 @@ context('Canvas 3D functionality. Grouping.', () => {
const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js'; const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js';
const firstCuboidCreationParams = { const firstCuboidCreationParams = {
labelName, labelName,
x: 480, x: 400,
y: 150, y: 200,
}; };
const secondCuboidCreationParams = { const secondCuboidCreationParams = {
labelName, labelName,
x: 480, x: 400,
y: 200, y: 280,
}; };
const thirdCuboidCreationParams = { const thirdCuboidCreationParams = {
labelName, labelName,
x: 530, x: 500,
y: 150, y: 280,
}; };
const fourthCuboidCreationParams = { const fourthCuboidCreationParams = {
labelName, labelName,
x: 530, x: 500,
y: 200, y: 200,
}; };
const yellowHex = 'fcbe03'; const yellowHex = 'fcbe03';
@ -64,8 +64,8 @@ context('Canvas 3D functionality. Grouping.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Grouping two cuboids.', () => { it('Grouping two cuboids.', () => {
cy.get('.cvat-group-control').click(); cy.get('.cvat-group-control').click();
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 480, 200).click(480, 200); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 400, 280).click(400, 280);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 530, 150).click(530, 150); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 500, 280).click(500, 280);
cy.get('.cvat-group-control').click(); cy.get('.cvat-group-control').click();
cy.changeAppearance('Group'); cy.changeAppearance('Group');
cy.get('#cvat-objects-sidebar-state-item-1').invoke('attr', 'style').then((bgColorItem1) => { cy.get('#cvat-objects-sidebar-state-item-1').invoke('attr', 'style').then((bgColorItem1) => {
@ -105,8 +105,8 @@ context('Canvas 3D functionality. Grouping.', () => {
it('Reset group.', () => { it('Reset group.', () => {
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_before_reset_group'); cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_before_reset_group');
cy.get('.cvat-group-control').click(); cy.get('.cvat-group-control').click();
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 480, 200).click(480, 200); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 400, 280).click(400, 280);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 530, 150).click(530, 150); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 500, 280).click(500, 280);
cy.get('body').type('{Shift}g'); cy.get('body').type('{Shift}g');
cy.get('#cvat-objects-sidebar-state-item-2').invoke('attr', 'style').then((bgColorItem2) => { cy.get('#cvat-objects-sidebar-state-item-2').invoke('attr', 'style').then((bgColorItem2) => {
expect(bgColorItem).to.be.equal(bgColorItem2); expect(bgColorItem).to.be.equal(bgColorItem2);

@ -1,7 +1,10 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/* eslint-disable cypress/no-unnecessary-waiting */
/// <reference types="cypress" /> /// <reference types="cypress" />
import { taskName, labelName } from '../../support/const_canvas3d'; import { taskName, labelName } from '../../support/const_canvas3d';
@ -11,8 +14,9 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => {
const screenshotsPath = const screenshotsPath =
'cypress/screenshots/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js'; 'cypress/screenshots/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js';
const cuboidCreationParams = { const cuboidCreationParams = {
labelName: labelName, labelName,
}; };
const waitTime = 2000;
before(() => { before(() => {
cy.openTask(taskName); cy.openTask(taskName);
@ -24,20 +28,14 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Save a job. Reopen the job.', () => { it('Save a job. Reopen the job.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.saveJob('PATCH', 200, 'saveJob'); cy.saveJob('PATCH', 200, 'saveJob');
cy.wait(waitTime);
cy.goToTaskList(); cy.goToTaskList();
cy.wait(waitTime);
cy.openTaskJob(taskName); cy.openTaskJob(taskName);
cy.wait(waitTime); // Waiting for the point cloud to display
cy.get('.cvat-objects-sidebar-state-item').then((sidebarStateItem) => { cy.get('.cvat-objects-sidebar-state-item').then((sidebarStateItem) => {
expect(sidebarStateItem.length).to.be.equal(1); expect(sidebarStateItem.length).to.be.equal(1);
}); });
cy.wait(waitTime); cy.wait(waitTime);
cy.customScreenshot('.cvat-canvas3d-topview', 'canvas3d_topview_after_reopen_job'); cy.customScreenshot('.cvat-canvas3d-topview', 'canvas3d_topview_after_reopen_job');
cy.wait(waitTime);
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_topview_before_all.png`, `${screenshotsPath}/canvas3d_topview_before_all.png`,
`${screenshotsPath}/canvas3d_topview_after_reopen_job.png`, `${screenshotsPath}/canvas3d_topview_after_reopen_job.png`,
@ -49,6 +47,7 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => {
cy.saveJob('PUT'); cy.saveJob('PUT');
cy.contains('Saving changes on the server').should('be.hidden'); cy.contains('Saving changes on the server').should('be.hidden');
cy.get('.cvat-objects-sidebar-state-item').should('not.exist'); cy.get('.cvat-objects-sidebar-state-item').should('not.exist');
cy.wait(waitTime);
cy.customScreenshot('.cvat-canvas3d-topview', 'canvas3d_topview_after_remove_annotations'); cy.customScreenshot('.cvat-canvas3d-topview', 'canvas3d_topview_after_remove_annotations');
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_topview_after_reopen_job.png`, `${screenshotsPath}/canvas3d_topview_after_reopen_job.png`,

@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -41,7 +41,7 @@ context('Canvas 3D functionality. Basic actions.', () => {
function testPerspectiveChangeOnWheel(screenshotNameBefore, screenshotNameAfter) { function testPerspectiveChangeOnWheel(screenshotNameBefore, screenshotNameAfter) {
cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameBefore); cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameBefore);
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
cy.get('.cvat-canvas3d-perspective').trigger('wheel', { deltaY: -50 }); cy.get('.cvat-canvas3d-perspective canvas').trigger('wheel', { deltaY: -50 });
} }
cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameAfter); cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameAfter);
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
@ -52,9 +52,12 @@ context('Canvas 3D functionality. Basic actions.', () => {
function testTopSideFrontChangeOnWheel(element, screenshotNameBefore, screenshotNameAfter) { function testTopSideFrontChangeOnWheel(element, screenshotNameBefore, screenshotNameAfter) {
cy.customScreenshot(element, screenshotNameBefore); cy.customScreenshot(element, screenshotNameBefore);
for (let i = 0; i < 3; i++) { cy.get(element).within(() => {
cy.get(element).trigger('wheel', { deltaY: -100 }); for (let i = 0; i < 3; i++) {
} cy.get('.cvat-canvas3d-fullsize canvas').trigger('wheel', { deltaY: -100 });
}
});
cy.customScreenshot(element, screenshotNameAfter); cy.customScreenshot(element, screenshotNameAfter);
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
`${screenshotsPath}/${screenshotNameBefore}.png`, `${screenshotsPath}/${screenshotNameBefore}.png`,
@ -63,10 +66,7 @@ context('Canvas 3D functionality. Basic actions.', () => {
} }
function testContextImage() { function testContextImage() {
cy.get('.cvat-context-image-wrapper img').should('exist').and('be.visible'); cy.get('.cvat-context-image-wrapper canvas').should('exist').and('be.visible');
cy.get('.cvat-context-image-switcher').click(); // Context image hide
cy.get('.cvat-context-image-wrapper img').should('not.exist');
cy.get('.cvat-context-image-switcher').click(); // Context image show
} }
function testControlButtonTooltip(button, expectedTooltipText) { function testControlButtonTooltip(button, expectedTooltipText) {

@ -1,84 +0,0 @@
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Resize views.', () => {
const caseId = '62';
let widthHeightArrBeforeResize = [];
let widthHeightArrAfterResize = [];
function getViewWidthHeight(element, arrToPush) {
cy.get(element)
.find('canvas')
.invoke('attr', 'width')
.then(($topviewWidth) => {
cy.get(element)
.find('canvas')
.invoke('attr', 'height')
.then(($topviewHeight) => {
arrToPush.push([$topviewWidth, $topviewHeight]);
});
});
}
before(() => {
cy.openTaskJob(taskName);
getViewWidthHeight('.cvat-canvas3d-perspective', widthHeightArrBeforeResize);
getViewWidthHeight('.cvat-canvas3d-topview', widthHeightArrBeforeResize);
getViewWidthHeight('.cvat-canvas3d-sideview', widthHeightArrBeforeResize);
getViewWidthHeight('.cvat-canvas3d-frontview', widthHeightArrBeforeResize);
});
describe(`Testing case "${caseId}"`, () => {
it('Resizing perspective.', () => {
cy.get('.cvat-resizable-handle-horizontal').trigger('mousedown', { button: 0, scrollBehavior: false });
cy.get('.cvat-canvas3d-perspective')
.trigger('mousemove', 600, 300, { scrollBehavior: false })
.trigger('mouseup');
getViewWidthHeight('.cvat-canvas3d-perspective', widthHeightArrAfterResize);
});
it('Resizing topview.', () => {
cy.get('.cvat-resizable-handle-vertical-top').trigger('mousedown', { button: 0, scrollBehavior: false });
cy.get('.cvat-canvas3d-topview')
.trigger('mousemove', 200, 200, { scrollBehavior: false })
.trigger('mouseup');
getViewWidthHeight('.cvat-canvas3d-topview', widthHeightArrAfterResize);
});
it('Resizing sideview.', () => {
cy.get('.cvat-resizable-handle-vertical-side').trigger('mousedown', { button: 0, scrollBehavior: false });
cy.get('.cvat-canvas3d-frontview')
.trigger('mousemove', 200, 200, { scrollBehavior: false })
.trigger('mouseup');
getViewWidthHeight('.cvat-canvas3d-sideview', widthHeightArrAfterResize);
getViewWidthHeight('.cvat-canvas3d-frontview', widthHeightArrAfterResize);
});
it('Checking for elements resizing.', () => {
expect(widthHeightArrBeforeResize[0][0]).to.be.equal(widthHeightArrAfterResize[0][0]); // Width of cvat-canvas3d-perspective before and after didn't change
expect(widthHeightArrBeforeResize[0][1]).not.be.equal(widthHeightArrAfterResize[0][1]); // Height of cvat-canvas3d-perspective changed
expect(widthHeightArrAfterResize[1][1])
.to.be.equal(widthHeightArrAfterResize[2][1])
.to.be.equal(widthHeightArrAfterResize[3][1]); // Top/side/front has equal height after changes
[
[widthHeightArrBeforeResize[1][0], widthHeightArrAfterResize[1][0]],
[widthHeightArrBeforeResize[2][0], widthHeightArrAfterResize[2][0]],
[widthHeightArrBeforeResize[3][0], widthHeightArrAfterResize[3][0]],
].forEach(([widthBefore, widthAfter]) => {
expect(widthBefore).not.be.equal(widthAfter); // Width of top/side/front changed
});
[
[widthHeightArrBeforeResize[1][1], widthHeightArrAfterResize[1][1]],
[widthHeightArrBeforeResize[2][1], widthHeightArrAfterResize[2][1]],
[widthHeightArrBeforeResize[3][1], widthHeightArrAfterResize[3][1]],
].forEach(([heightBefore, heightAfter]) => {
expect(heightBefore).not.be.equal(heightAfter); // Height of top/side/front changed
});
});
});
});

@ -11,7 +11,6 @@ import { taskName, labelName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Opacity. Outlined borders.', () => { context('Canvas 3D functionality. Opacity. Outlined borders.', () => {
const caseId = '82'; const caseId = '82';
const screenshotsPath = 'cypress/screenshots/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js';
const cuboidCreationParams = { const cuboidCreationParams = {
labelName, labelName,
x: 500, x: 500,
@ -21,48 +20,48 @@ context('Canvas 3D functionality. Opacity. Outlined borders.', () => {
before(() => { before(() => {
cy.openTask(taskName); cy.openTask(taskName);
cy.openJob(); cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display cy.wait(2000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams); cy.create3DCuboid(cuboidCreationParams);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove').click(); // Deactivate the cuboiud cy.get('.cvat-canvas3d-perspective').trigger('mousemove').click(); // Deactivate the cuboiud
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_deactivate_cuboid');
}); });
const getScene = (el) => el.scene.children[0];
const getFirstChild = (el) => getScene(el).children[0];
const getWireframe = (el) => getFirstChild(el).children[0];
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Change opacity to 100. To 0.', () => { it('Change opacity to 100. To 0.', () => {
cy.get('.cvat-appearance-opacity-slider').click('right'); cy.get('.cvat-appearance-opacity-slider').click('right');
cy.get('.cvat-appearance-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 100); cy.get('.cvat-appearance-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 100);
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_opacty_100'); cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
cy.compareImagesAndCheckResult( expect(getFirstChild(el).material.opacity).to.equal(1);
`${screenshotsPath}/canvas3d_perspective_deactivate_cuboid.png`, });
`${screenshotsPath}/canvas3d_perspective_opacty_100.png`,
);
cy.get('.cvat-appearance-opacity-slider').click('left'); cy.get('.cvat-appearance-opacity-slider').click('left');
cy.get('.cvat-appearance-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 0); cy.get('.cvat-appearance-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 0);
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_opacty_0'); cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
cy.compareImagesAndCheckResult( expect(getFirstChild(el).material.opacity).to.equal(0);
`${screenshotsPath}/canvas3d_perspective_opacty_100.png`, });
`${screenshotsPath}/canvas3d_perspective_opacty_0.png`,
); cy.get('body').click();
}); });
it('Change selected opacity to 100. To 0.', () => { it('Change selected opacity to 100. To 0.', () => {
cy.get('.cvat-appearance-selected-opacity-slider').click('right'); cy.get('.cvat-appearance-selected-opacity-slider').click('right');
cy.get('.cvat-appearance-selected-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 100); cy.get('.cvat-appearance-selected-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 100);
cy.get('body').click();
cy.get('.cvat-canvas3d-perspective').trigger('mousemove').trigger('mousemove', 500, 250).wait(1000); // Waiting for the cuboid activation cy.get('.cvat-canvas3d-perspective').trigger('mousemove').trigger('mousemove', 500, 250).wait(1000); // Waiting for the cuboid activation
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_selected_opacty_100');
cy.compareImagesAndCheckResult( cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
`${screenshotsPath}/canvas3d_perspective_opacty_100.png`, expect(el.scene.children[0].children[0].material.opacity).to.equal(1);
`${screenshotsPath}/canvas3d_perspective_selected_opacty_100.png`, });
true, // No diff between the images
);
cy.get('.cvat-appearance-selected-opacity-slider').click('left'); cy.get('.cvat-appearance-selected-opacity-slider').click('left');
cy.get('.cvat-appearance-selected-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 0); cy.get('.cvat-appearance-selected-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 0);
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_selected_opacty_0');
cy.compareImagesAndCheckResult( cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
`${screenshotsPath}/canvas3d_perspective_opacty_0.png`, expect(getFirstChild(el).material.opacity).to.equal(0);
`${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`, });
true, // No diff between the images
);
}); });
it('Enable/disable outlined borders.', () => { it('Enable/disable outlined borders.', () => {
@ -72,18 +71,14 @@ context('Canvas 3D functionality. Opacity. Outlined borders.', () => {
cy.get('div[title="#ff007c"]').click(); cy.get('div[title="#ff007c"]').click();
cy.contains('Ok').click(); cy.contains('Ok').click();
}); });
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_enable_outlined_borders'); cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
cy.compareImagesAndCheckResult( expect({ ...getWireframe(el).material.color }).to.deep.equal({ r: 1, g: 0, b: 0.48627450980392156 });
`${screenshotsPath}/canvas3d_perspective_enable_outlined_borders.png`, });
`${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`,
);
cy.get('.cvat-appearance-outlinded-borders-checkbox').find('[type="checkbox"]').uncheck().should('not.be.checked'); cy.get('.cvat-appearance-outlinded-borders-checkbox').find('[type="checkbox"]').uncheck().should('not.be.checked');
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_disable_outlined_borders'); cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
cy.compareImagesAndCheckResult( expect({ ...getWireframe(el).material.color }).to.deep.equal({ ...getFirstChild(el).material.color });
`${screenshotsPath}/canvas3d_perspective_disable_outlined_borders.png`, });
`${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`,
true, // No diff between the images
);
}); });
}); });
}); });

@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation // Copyright (C) 2022-2023 CVAT.ai Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -29,14 +29,13 @@ Cypress.Commands.add('create3DCuboid', (cuboidCreationParams) => {
}); });
Cypress.Commands.add('customScreenshot', (element, screenshotName) => { Cypress.Commands.add('customScreenshot', (element, screenshotName) => {
let getEl; cy.get(`${element} canvas`).then(([$el]) => ($el.getBoundingClientRect())).then((rect) => {
let padding; cy.screenshot(screenshotName, {
if (element.includes('perspective')) { overwrite: true,
getEl = cy.get(element); capture: 'fullPage',
padding = -130; clip: {
} else { x: rect.x, y: rect.y, width: rect.width, height: rect.height,
getEl = cy.get(element).find('.cvat-canvas3d-fullsize'); },
padding = -40; });
} });
getEl.screenshot(screenshotName, { padding });
}); });

@ -1884,7 +1884,7 @@
resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.0.5.tgz#9262f269de36f1e9248aeb9dee0ee9d10065e043" resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.0.5.tgz#9262f269de36f1e9248aeb9dee0ee9d10065e043"
integrity sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w== integrity sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w==
"@types/prettier@^2.0.0", "@types/prettier@^2.1.5": "@types/prettier@2.4.1", "@types/prettier@^2.0.0", "@types/prettier@^2.1.5":
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb"
integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw==
@ -1917,31 +1917,31 @@
"@types/react" "*" "@types/react" "*"
"@types/reactcss" "*" "@types/reactcss" "*"
"@types/react-dom@^16.9.14": "@types/react-dom@^16.9.14", "@types/react-dom@^18.0.5":
version "16.9.17" version "18.0.10"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.17.tgz#29100cbcc422d7b7dba7de24bb906de56680dd34" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352"
integrity sha512-qSRyxEsrm5btPXnowDOs5jSkgT8ldAA0j6Qp+otHUh+xHzy3sXmgNfyhucZjAjkgpdAUw9rJe0QRtX/l+yaS4g== integrity sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==
dependencies: dependencies:
"@types/react" "^16" "@types/react" "*"
"@types/react-redux@^7.1.18": "@types/react-grid-layout@^1.3.2":
version "7.1.24" version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.2.tgz#9f195666a018a5ae2b773887e3b552cb4378d67f"
integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== integrity sha512-ZzpBEOC1JTQ7MGe1h1cPKSLP4jSWuxc+yvT4TsAlEW9+EFPzAf8nxQfFd7ea9gL17Em7PbwJZAsiwfQQBUklZQ==
dependencies: dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*" "@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-resizable@^3.0.1": "@types/react-redux@^7.1.18", "@types/react-redux@^7.1.24":
version "3.0.2" version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-resizable/-/react-resizable-3.0.2.tgz#3c914be6b02c8d6864b82ffb6461b2e8a771fb75" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
integrity sha512-4rHjZDQmSpFqRlNzlcnF5tpOG5fBcMuDlvD+qT3XHAJLKGx/FC3iDQ9li9tHW53ecWwZzHTPCGvz5vNWQN+v/Q== integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
dependencies: dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*" "@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^5.1.9": "@types/react-router-dom@^5.1.9", "@types/react-router-dom@^5.3.3":
version "5.3.3" version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
@ -1965,19 +1965,10 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*": "@types/react@*", "@types/react@^16", "@types/react@^16.14.15", "@types/react@^17.0.30":
version "17.0.48" version "17.0.52"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.48.tgz#a4532a8b91d7b27b8768b6fc0c3bccb760d15a6c" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b"
integrity sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A== integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@^16", "@types/react@^16.14.15":
version "16.14.34"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.34.tgz#d129324ffda312044e1c47aab18696e4ed493282"
integrity sha512-b99nWeGGReLh6aKBppghVqp93dFJtgtDOzc8NXM6hewD8PQ2zZG5kBLgbx+VJr7Q7WBMjHxaIl3dwpwwPIUgyA==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/scheduler" "*" "@types/scheduler" "*"
@ -4107,7 +4098,7 @@ custom-error-instance@2.1.1:
integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg== integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==
"cvat-canvas3d@link:./cvat-canvas3d": "cvat-canvas3d@link:./cvat-canvas3d":
version "0.0.6" version "0.0.7"
dependencies: dependencies:
"@types/three" "^0.125.3" "@types/three" "^0.125.3"
camera-controls "^1.25.3" camera-controls "^1.25.3"
@ -4115,7 +4106,7 @@ custom-error-instance@2.1.1:
three "^0.126.1" three "^0.126.1"
"cvat-canvas@link:./cvat-canvas": "cvat-canvas@link:./cvat-canvas":
version "2.16.1" version "2.16.2"
dependencies: dependencies:
"@types/fabric" "^4.5.7" "@types/fabric" "^4.5.7"
"@types/polylabel" "^1.0.5" "@types/polylabel" "^1.0.5"
@ -4128,7 +4119,7 @@ custom-error-instance@2.1.1:
svg.select.js "3.0.1" svg.select.js "3.0.1"
"cvat-core@link:./cvat-core": "cvat-core@link:./cvat-core":
version "7.4.0" version "8.0.0"
dependencies: dependencies:
axios "^0.27.2" axios "^0.27.2"
browser-or-node "^2.0.0" browser-or-node "^2.0.0"
@ -4145,7 +4136,7 @@ custom-error-instance@2.1.1:
tus-js-client "^3.0.1" tus-js-client "^3.0.1"
"cvat-data@link:./cvat-data": "cvat-data@link:./cvat-data":
version "1.0.2" version "1.1.0"
dependencies: dependencies:
async-mutex "^0.4.0" async-mutex "^0.4.0"
jszip "3.10.1" jszip "3.10.1"
@ -7906,6 +7897,11 @@ lodash.flattendeep@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==
lodash.isequal@^4.0.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
lodash.isplainobject@^4.0.6: lodash.isplainobject@^4.0.6:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@ -10533,7 +10529,7 @@ react-dom@^16.14.0:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.19.1" scheduler "^0.19.1"
react-draggable@^4.0.3: react-draggable@^4.0.0, react-draggable@^4.0.3:
version "4.4.5" version "4.4.5"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g== integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==
@ -10541,6 +10537,17 @@ react-draggable@^4.0.3:
clsx "^1.1.1" clsx "^1.1.1"
prop-types "^15.8.1" prop-types "^15.8.1"
react-grid-layout@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff"
integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==
dependencies:
clsx "^1.1.1"
lodash.isequal "^4.0.0"
prop-types "^15.8.1"
react-draggable "^4.0.0"
react-resizable "^3.0.4"
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.9.0: react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.9.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"

Loading…
Cancel
Save