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>)
- \[SDK\] Class to represent a project as a PyTorch dataset
(<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)
(<https://github.com/opencv/cvat/pull/5536>)
- \[SDK\] A PyTorch adapter setting to disable cache updates

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

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -290,21 +290,6 @@ g.cvat_canvas_shape_occluded {
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 {
text-rendering: optimizeSpeed;
position: absolute;

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

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

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

@ -238,7 +238,10 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
})
.catch((exception: any): void => {
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) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -107,6 +107,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private cube: CuboidModel;
private isPerspectiveBeingDragged: boolean;
private activatedElementID: number | null;
private isCtrlDown: boolean;
private drawnObjects: Record<number, {
data: DrawnObjectData;
cuboid: CuboidModel;
@ -184,6 +185,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
},
};
this.isCtrlDown = false;
this.action = {
scan: null,
frameCoordinates: {
@ -263,6 +265,20 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const canvasSideView = this.views.side.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 => {
if (this.model.data.activeElement.clientID !== null) {
this.dispatchEvent(
@ -330,6 +346,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasPerspectiveView.addEventListener('mousemove', (event: MouseEvent): void => {
event.preventDefault();
this.isCtrlDown = event.ctrlKey;
if (this.mode === Mode.DRAG_CANVAS) return;
const canvas = this.views.perspective.renderer.domElement;
const rect = canvas.getBoundingClientRect();
@ -539,7 +556,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}
private setDefaultZoom(): void {
if (this.model.data.activeElement === null) {
if (this.model.data.activeElement.clientID === null) {
Object.keys(this.views).forEach((view: string): void => {
const viewType = this.views[view as keyof Views];
if (view !== ViewType.PERSPECTIVE) {
@ -554,7 +571,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasTop.offsetWidth / (bboxtop.max.x - bboxtop.min.x),
canvasTop.offsetHeight / (bboxtop.max.y - bboxtop.min.y),
) * 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.updateMatrix();
this.updateHelperPointsSize(ViewType.TOP);
@ -565,7 +582,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasFront.offsetWidth / (bboxfront.max.y - bboxfront.min.y),
canvasFront.offsetHeight / (bboxfront.max.z - bboxfront.min.z),
) * 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.updateMatrix();
this.updateHelperPointsSize(ViewType.FRONT);
@ -576,7 +593,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
canvasSide.offsetWidth / (bboxside.max.x - bboxside.min.x),
canvasSide.offsetHeight / (bboxside.max.z - bboxside.min.z),
) * 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.updateMatrix();
this.updateHelperPointsSize(ViewType.SIDE);
@ -842,7 +859,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.activatedElementID = +clientID;
this.rotatePlane(null, 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) {
this.deactivateObject();
this.activateObject();
if (this.activatedElementID) {
this.setDefaultZoom();
}
} else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData;
if (Number.isInteger(data.redraw)) {
@ -1385,7 +1406,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const { x, y, z } = intersection.point;
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 intersects = renderer.intersectObjects(this.getAllVisibleCuboids(), false);
if (intersects.length !== 0) {

@ -1,6 +1,6 @@
{
"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",
"main": "src/api.ts",
"scripts": {

@ -3,14 +3,29 @@
//
// SPDX-License-Identifier: MIT
import * as cvatData from 'cvat-data';
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
const frameDataCache = {};
import * as cvatData from 'cvat-data';
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 {
constructor({
@ -23,7 +38,7 @@ export class FrameData {
stopFrame,
decodeForward,
deleted,
has_related_context: hasRelatedContext,
related_files: relatedFiles,
}) {
Object.defineProperties(
this,
@ -48,8 +63,8 @@ export class FrameData {
value: frameNumber,
writable: false,
},
hasRelatedContext: {
value: hasRelatedContext,
relatedFiles: {
value: relatedFiles,
writable: false,
},
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];
let size = null;
if (mode === 'interpolation') {
@ -314,6 +329,7 @@ function getFrameMeta(jobID, frame) {
} else {
throw new DataError(`Invalid mode is specified ${mode}`);
}
return size;
}
@ -329,16 +345,46 @@ class FrameBuffer {
this._jobID = jobID;
}
isContextImageAvailable(frame) {
return frame in this._contextImage;
addContextImage(frame, data): void {
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) {
return this._contextImage[frame] || null;
isContextImageAvailable(frame): boolean {
return frame in this._contextImage;
}
addContextImage(frame, data) {
this._contextImage[frame] = data;
getContextImage(frame): Promise<ImageBitmap[]> {
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() {
@ -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) {
if (+frame < frameNumber || +frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame];
@ -554,11 +600,7 @@ async function getImageContext(jobID, frame) {
// eslint-disable-next-line no-undef
resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(result);
resolve(result);
}
})
.catch((error) => {
@ -572,7 +614,7 @@ export async function getContextImage(jobID, frame) {
return frameDataCache[jobID].frameBuffer.getContextImage(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);
}
@ -600,16 +642,16 @@ export async function getPreview(taskID = null, jobID = null) {
}
export async function getFrame(
jobID,
chunkSize,
chunkType,
mode,
frame,
startFrame,
stopFrame,
isPlaying,
step,
dimension,
jobID: number,
chunkSize: number,
chunkType: 'video' | 'imageset',
mode: 'interpolation' | 'annotation', // todo: obsolete, need to remove
frame: number,
startFrame: number,
stopFrame: number,
isPlaying: boolean,
step: number,
dimension: DimensionType,
) {
if (!(jobID in frameDataCache)) {
const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE;
@ -648,8 +690,9 @@ export async function getFrame(
activeChunkRequest: null,
nextChunkRequest: null,
};
// relevant only for video chunks
const frameMeta = getFrameMeta(jobID, frame);
// actual only for video chunks
frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height);
}

@ -1,5 +1,5 @@
// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -1384,7 +1384,7 @@ async function getImageContext(jid, frame) {
number: frame,
},
proxy: config.proxy,
responseType: 'blob',
responseType: 'arraybuffer',
});
} catch (errorData) {
throw generateError(errorData);
@ -1423,7 +1423,23 @@ async function getData(tid, jid, chunk) {
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;
let response = null;

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

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

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

@ -1,139 +1,235 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import { Mutex } from 'async-mutex';
// eslint-disable-next-line max-classes-per-file
import { MP4Reader, Bytestream } from './3rdparty/mp4';
import ZipDecoder from './unzip_imgs.worker';
import H264Decoder from './3rdparty/Decoder.worker';
export const BlockType = Object.freeze({
MP4VIDEO: 'mp4video',
ARCHIVE: 'archive',
});
export enum BlockType {
MP4VIDEO = 'mp4video',
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({
DIM_3D: '3d',
DIM_2D: '2d',
});
decodeZip.mutex = new Mutex();
interface BlockToDecode {
start: number;
end: number;
block: ArrayBuffer;
resolveCallback: (frame: number) => void;
rejectCallback: (e: ErrorEvent) => void;
}
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(
blockType,
blockSize,
cachedBlockCount,
blockType: BlockType,
blockSize: number,
cachedBlockCount: number,
decodedBlocksCacheSize = 5,
maxWorkerThreadCount = 2,
dimension = DimensionType.DIM_2D,
dimension: DimensionType = DimensionType.DIMENSION_2D,
) {
this._frames = {};
this._cachedBlockCount = Math.max(1, cachedBlockCount); // number of stored blocks
this._decodedBlocksCacheSize = decodedBlocksCacheSize;
this._blocksRanges = [];
this.mutex = new Mutex();
this.blocksRanges = [];
this.frames = {};
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._running = false;
this._blockSize = blockSize;
this._blockType = blockType;
this._currFrame = -1;
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;
this.requestedBlockToDecode = null;
this.blocksAreBeingDecoded = {};
setTimeout(this._checkDecodeRequests.bind(this), 100);
}
async _worker() {
if (this._requestedBlockDecode !== null && this._decodeThreadCount < this._maxWorkerThreadCount) {
await this.startDecode();
_checkDecodeRequests(): void {
if (this.requestedBlockToDecode !== null && this.currentDecodingThreads < this.workerThreadsLimit) {
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) {
return `${start}:${end}` in this._blocksRanges;
isChunkCached(start: number, end: number): boolean {
// 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 */
async _cleanup() {
if (this._blocksRanges.length > this._cachedBlockCount) {
const shifted = this._blocksRanges.shift(); // get the oldest block
async _cleanup(): Promise<void> {
if (this.blocksRanges.length > this.cachedEncodedBlocksLimit) {
const shifted = this.blocksRanges.shift(); // get the oldest block
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++) {
delete this._frames[i];
delete this.frames[i];
}
}
// delete frames whose are not in areas of current frame
const distance = Math.floor(this._decodedBlocksCacheSize / 2);
for (let i = 0; i < this._blocksRanges.length; i++) {
const [start, end] = this._blocksRanges[i].split(':').map((el) => +el);
const distance = Math.floor(this.cachedDecodedBlocksLimit / 2);
for (let i = 0; i < this.blocksRanges.length; i++) {
const [start, end] = this.blocksRanges[i].split(':').map((el) => +el);
if (
end < this._currFrame - distance * this._blockSize ||
start > this._currFrame + distance * this._blockSize
end < this.currentFrame - distance * this.blockSize ||
start > this.currentFrame + distance * this.blockSize
) {
for (let j = start; j <= end; j++) {
delete this._frames[j];
delete this.frames[j];
}
}
}
}
async requestDecodeBlock(block, start, end, resolveCallback, rejectCallback) {
const release = await this._mutex.acquire();
async requestDecodeBlock(
block: ArrayBuffer,
start: number,
end: number,
resolveCallback: () => void,
rejectCallback: () => void,
): Promise<void> {
const release = await this.mutex.acquire();
try {
if (this._requestedBlockDecode !== null) {
if (start === this._requestedBlockDecode.start && end === this._requestedBlockDecode.end) {
this._requestedBlockDecode.resolveCallback = resolveCallback;
this._requestedBlockDecode.rejectCallback = rejectCallback;
} else if (this._requestedBlockDecode.rejectCallback) {
this._requestedBlockDecode.rejectCallback();
if (this.requestedBlockToDecode !== null) {
if (start === this.requestedBlockToDecode.start && end === this.requestedBlockToDecode.end) {
// only rewrite callbacks if the same block was requested again
this.requestedBlockToDecode.resolveCallback = resolveCallback;
this.requestedBlockToDecode.rejectCallback = 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 = {
block: block || this._blocks[Math.floor(start / this._blockSize)],
if (!(`${start}:${end}` in this.blocksAreBeingDecoded)) {
this.requestedBlockToDecode = {
block: block || this._blocks[Math.floor(start / this.blockSize)],
start,
end,
resolveCallback,
rejectCallback,
};
} else {
this._decodingBlocks[`${start}:${end}`].rejectCallback = rejectCallback;
this._decodingBlocks[`${start}:${end}`].resolveCallback = resolveCallback;
this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback = rejectCallback;
this.blocksAreBeingDecoded[`${start}:${end}`].resolveCallback = resolveCallback;
}
} finally {
release();
}
}
isRequestExist() {
return this._requestedBlockDecode !== null;
}
setRenderSize(width, height) {
this._width = width;
this._height = height;
setRenderSize(width: number, height: number): void {
this.renderWidth = width;
this.renderHeight = height;
}
/* Method returns frame from collection. Else method returns 0 */
async frame(frameNumber) {
this._currFrame = frameNumber;
/* Method returns frame from collection. Else method returns null */
async frame(frameNumber: number): Promise<ImageBitmap | ImageData | Blob> {
this.currentFrame = frameNumber;
return new Promise((resolve, reject) => {
if (frameNumber in this._frames) {
if (this._frames[frameNumber] !== null) {
resolve(this._frames[frameNumber]);
if (frameNumber in this.frames) {
if (this.frames[frameNumber] !== null) {
resolve(this.frames[frameNumber]);
} else {
this._promisedFrames[frameNumber] = {
resolve,
reject,
};
this.promisedFrames[frameNumber] = { resolve, reject };
}
} else {
resolve(null);
@ -141,30 +237,24 @@ export class FrameProvider {
});
}
isNextChunkExists(frameNumber) {
const nextChunkNum = Math.floor(frameNumber / this._blockSize) + 1;
if (this._blocks[nextChunkNum] === 'loading') {
return true;
}
isNextChunkExists(frameNumber: number): boolean {
const nextChunkNum = Math.floor(frameNumber / this.blockSize) + 1;
return nextChunkNum in this._blocks;
}
/*
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) {
setReadyToLoading(chunkNumber: number): void {
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) {
return new ImageData(new Uint8ClampedArray(imageBuffer), width, height);
}
@ -189,22 +279,26 @@ export class FrameProvider {
return new ImageData(rgbaInt8Clamped, width, height);
}
async startDecode() {
const release = await this._mutex.acquire();
async startDecode(): Promise<void> {
const release = await this.mutex.acquire();
try {
const height = this._height;
const width = this._width;
const { start, end, block } = this._requestedBlockDecode;
this._blocksRanges.push(`${start}:${end}`);
this._decodingBlocks[`${start}:${end}`] = this._requestedBlockDecode;
this._requestedBlockDecode = null;
this._blocks[Math.floor((start + 1) / this._blockSize)] = block;
const height = this.renderHeight;
const width = this.renderWidth;
const { start, end, block } = this.requestedBlockToDecode;
this.blocksRanges.push(`${start}:${end}`);
this.blocksAreBeingDecoded[`${start}:${end}`] = this.requestedBlockToDecode;
this.requestedBlockToDecode = null;
this._blocks[Math.floor((start + 1) / this.blockSize)] = block;
for (let i = start; i <= end; i++) {
this._frames[i] = null;
this.frames[i] = null;
}
this._cleanup();
if (this._blockType === BlockType.MP4VIDEO) {
this.currentDecodingThreads++;
if (this.blockType === BlockType.MP4VIDEO) {
const worker = new H264Decoder();
let index = start;
@ -214,8 +308,8 @@ export class FrameProvider {
return;
}
const scaleFactor = Math.ceil(this._height / e.data.height);
this._frames[index] = FrameProvider.cropImage(
const scaleFactor = Math.ceil(height / e.data.height);
this.frames[index] = FrameProvider.cropImage(
e.data.buf,
e.data.width,
e.data.height,
@ -225,37 +319,44 @@ export class FrameProvider {
Math.floor(height / scaleFactor),
);
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {
this._decodingBlocks[`${start}:${end}`].resolveCallback(index);
const { resolveCallback } = this.blocksAreBeingDecoded[`${start}:${end}`];
if (resolveCallback) {
resolveCallback(index);
}
if (index in this._promisedFrames) {
this._promisedFrames[index].resolve(this._frames[index]);
delete this._promisedFrames[index];
if (index in this.promisedFrames) {
const { resolve } = this.promisedFrames[index];
delete this.promisedFrames[index];
resolve(this.frames[index]);
}
if (index === end) {
this._decodeThreadCount--;
delete this._decodingBlocks[`${start}:${end}`];
worker.terminate();
this.currentDecodingThreads--;
delete this.blocksAreBeingDecoded[`${start}:${end}`];
}
index++;
};
worker.onerror = (e) => {
worker.onerror = (e: ErrorEvent) => {
worker.terminate();
this._decodeThreadCount--;
this.currentDecodingThreads--;
for (let i = index; i <= end; i++) {
if (i in this._promisedFrames) {
this._promisedFrames[i].reject();
delete this._promisedFrames[i];
// reject all the following frames
if (i in this.promisedFrames) {
const { reject } = this.promisedFrames[i];
delete this.promisedFrames[i];
reject();
}
}
if (this._decodingBlocks[`${start}:${end}`].rejectCallback) {
this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e));
if (this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback) {
this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback(e);
}
delete this._decodingBlocks[`${start}:${end}`];
delete this.blocksAreBeingDecoded[`${start}:${end}`];
};
worker.postMessage({
@ -284,92 +385,69 @@ export class FrameProvider {
worker.postMessage({ buf: nal, offset: 0, length: nal.length });
});
}
this._decodeThreadCount++;
} else {
const worker = new ZipDecoder();
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) => {
if (this._dimension === DimensionType.DIM_2D && event.data.isRaw) {
// 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;
this._frames[event.data.index] = event.data.data;
if (this._decodingBlocks[`${start}:${end}`].resolveCallback) {
this._decodingBlocks[`${start}:${end}`].resolveCallback(event.data.index);
const { resolveCallback } = this.blocksAreBeingDecoded[`${start}:${end}`];
if (resolveCallback) {
resolveCallback(event.data.index);
}
if (event.data.index in this._promisedFrames) {
this._promisedFrames[event.data.index].resolve(this._frames[event.data.index]);
delete this._promisedFrames[event.data.index];
if (event.data.index in this.promisedFrames) {
const { resolve } = this.promisedFrames[event.data.index];
delete this.promisedFrames[event.data.index];
resolve(this.frames[event.data.index]);
}
if (index === end) {
worker.terminate();
delete this._decodingBlocks[`${start}:${end}`];
this._decodeThreadCount--;
this.currentDecodingThreads--;
delete this.blocksAreBeingDecoded[`${start}:${end}`];
}
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({
block,
start,
end,
dimension,
dimension2D: DimensionType.DIM_2D,
dimension: this.dimension,
dimension2D: DimensionType.DIMENSION_2D,
});
this._decodeThreadCount++;
}
} finally {
release();
}
}
get decodeThreadCount() {
return this._decodeThreadCount;
}
get decodedBlocksCacheSize() {
return this._decodedBlocksCacheSize;
get decodedBlocksCacheSize(): number {
return this.cachedDecodedBlocksLimit;
}
/*
Method returns a list of cached ranges
Is an array of strings like "start:end"
*/
get cachedFrames() {
return [...this._blocksRanges].sort((a, b) => a.split(':')[0] - b.split(':')[0]);
get cachedFrames(): string[] {
return [...this.blocksRanges].sort((a, b) => +a.split(':')[0] - +b.split(':')[0]);
}
}

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

@ -13,7 +13,7 @@ const cvatData = {
target: 'web',
mode: 'production',
entry: {
'cvat-data': './src/js/cvat-data.ts',
'cvat-data': './src/ts/cvat-data.ts',
},
output: {
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;

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

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -125,7 +125,6 @@ export enum AnnotationActionTypes {
SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS',
SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS',
SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED',
SAVE_UPDATE_ANNOTATIONS_STATUS = 'SAVE_UPDATE_ANNOTATIONS_STATUS',
SWITCH_PLAY = 'SWITCH_PLAY',
CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY',
DRAG_CANVAS = 'DRAG_CANVAS',
@ -196,10 +195,6 @@ export enum AnnotationActionTypes {
GET_PREDICTIONS = 'GET_PREDICTIONS',
GET_PREDICTIONS_FAILED = 'GET_PREDICTIONS_FAILED',
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',
DELETE_FRAME = 'DELETE_FRAME',
DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS',
@ -700,7 +695,7 @@ export function changeFrameAsync(
number: currentState.annotation.player.frame.number,
data: currentState.annotation.player.frame.data,
filename: currentState.annotation.player.frame.filename,
hasRelatedContext: currentState.annotation.player.frame.hasRelatedContext,
relatedFiles: currentState.annotation.player.frame.relatedFiles,
delay: currentState.annotation.player.frame.delay,
changeTime: currentState.annotation.player.frame.changeTime,
states: currentState.annotation.annotations.states,
@ -767,7 +762,7 @@ export function changeFrameAsync(
number: toFrame,
data,
filename: data.filename,
hasRelatedContext: data.hasRelatedContext,
relatedFiles: data.relatedFiles,
states,
minZ,
maxZ,
@ -1046,7 +1041,7 @@ export function getJobAsync(
states,
frameNumber,
frameFilename: frameData.filename,
frameHasRelatedContext: frameData.hasRelatedContext,
relatedFiles: frameData.relatedFiles,
frameData,
colors,
filters,
@ -1113,19 +1108,8 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
try {
const saveJobEvent = await sessionInstance.logger.log(LogType.saveJob, {}, true);
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
payload: { status: 'Saving frames' },
});
await sessionInstance.frames.save();
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
payload: {
status,
},
});
});
await sessionInstance.annotations.save();
await saveJobEvent.close();
await sessionInstance.logger.log(LogType.sendTaskInfo, await jobInfoGenerator(sessionInstance));
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 {
return {

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -6,7 +7,6 @@ $grid-unit-size: 8px;
$header-height: $grid-unit-size * 6;
$layout-sm-grid-size: $grid-unit-size * 0.5;
$layout-lg-grid-size: $grid-unit-size * 2;
$layout-sm-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) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -139,31 +140,13 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{workspace === Workspace.STANDARD3D && (
<Layout.Content className='cvat-annotation-layout-content'>
<StandardWorkspace3DComponent />
</Layout.Content>
)}
{workspace === Workspace.STANDARD && (
<Layout.Content className='cvat-annotation-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>
)}
<Layout.Content className='cvat-annotation-layout-content'>
{workspace === Workspace.STANDARD3D && <StandardWorkspace3DComponent />}
{workspace === Workspace.STANDARD && <StandardWorkspaceComponent />}
{workspace === Workspace.ATTRIBUTE_ANNOTATION && <AttributeAnnotationWorkspace />}
{workspace === Workspace.TAG_ANNOTATION && <TagAnnotationWorkspace />}
{workspace === Workspace.REVIEW_WORKSPACE && <ReviewAnnotationsWorkspace />}
</Layout.Content>
<FiltersModalComponent />
<StatisticsModalComponent />
</Layout>

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -6,13 +7,13 @@ import './styles.scss';
import React from 'react';
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';
export default function AttributeAnnotationWorkspace(): JSX.Element {
return (
<Layout hasSider className='attribute-annotation-workspace'>
<CanvasWrapperContainer />
<CanvasLayout />
<AttributeAnnotationSidebar />
</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
@import '../../../base.scss';
@import 'base.scss';
.cvat-brush-tools-toolbox {
position: absolute;

@ -1,17 +1,18 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Layout from 'antd/lib/layout';
import { connect } from 'react-redux';
import Slider from 'antd/lib/slider';
import Spin from 'antd/lib/spin';
import Dropdown from 'antd/lib/dropdown';
import { PlusCircleOutlined, UpOutlined } from '@ant-design/icons';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import {
ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType,
ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType, ActiveControl, CombinedState,
} from 'reducers';
import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas-wrapper';
@ -20,16 +21,46 @@ import { getCore } from 'cvat-core-wrapper';
import consts from 'consts';
import CVATTooltip from 'components/common/cvat-tooltip';
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 BrushTools from './brush-tools';
import ContextImage from '../standard-workspace/context-image/context-image';
const cvat = getCore();
const MAX_DISTANCE_TO_OPEN_SHAPE = 50;
interface Props {
sidebarCollapsed: boolean;
interface StateToProps {
canvasInstance: Canvas | Canvas3d | null;
jobInstance: any;
activatedStateID: number | null;
@ -38,7 +69,7 @@ interface Props {
annotations: any[];
frameData: any;
frameAngle: number;
frameFetching: boolean;
canvasIsReady: boolean;
frame: number;
opacity: number;
colorBy: ColorBy;
@ -53,9 +84,6 @@ interface Props {
gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
curZLayer: number;
minZLayer: number;
maxZLayer: number;
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
@ -69,27 +97,32 @@ interface Props {
textContent: string;
showAllInterpolationTracks: boolean;
workspace: Workspace;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean;
intelligentPolygonCrop: boolean;
keyMap: KeyMap;
canvasBackgroundColor: string;
switchableAutomaticBordering: boolean;
keyMap: KeyMap;
showTagsOnFrame: boolean;
onSetupCanvas: () => void;
}
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;
onShapeDrawn: () => void;
onResetCanvas: () => 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;
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;
@ -105,7 +138,210 @@ interface Props {
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 {
const {
automaticBordering,
@ -163,7 +399,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
frameData,
frameAngle,
annotations,
sidebarCollapsed,
activatedStateID,
curZLayer,
resetZoom,
@ -176,7 +411,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
contrastLevel,
saturationLevel,
workspace,
frameFetching,
showObjectsTextAlways,
textFontSize,
controlPointsSize,
@ -186,7 +420,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
automaticBordering,
intelligentPolygonCrop,
showProjections,
canvasBackgroundColor,
colorBy,
onFetchAnnotation,
} = this.props;
@ -230,19 +463,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
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) {
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();
}
@ -365,8 +565,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence);
window.removeEventListener('resize', this.fitCanvas);
}
private onCanvasErrorOccurrence = (event: any): void => {
@ -458,19 +656,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onSplitAnnotations(jobInstance, frame, state);
};
private fitCanvas = (): void => {
const { canvasInstance } = this.props;
if (canvasInstance) {
canvasInstance.fitCanvas();
}
};
private onCanvasMouseDown = (e: MouseEvent): void => {
const { workspace, activatedStateID, onActivateObject } = this.props;
if ((e.target as HTMLElement).tagName === 'svg' && e.button !== 2) {
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
// in this case we do not need to update our state
if (state.clientID === activatedStateID) {
onActivateObject(null);
onActivateObject(null, null);
}
};
@ -557,7 +748,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
private onCanvasEditStart = (): void => {
const { onActivateObject, onEditShape } = this.props;
onActivateObject(null);
onActivateObject(null, null);
onEditShape(true);
};
@ -683,14 +874,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel,
contrastLevel,
saturationLevel,
canvasBackgroundColor,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
// Size
window.addEventListener('resize', this.fitCanvas);
this.fitCanvas();
// Grid
const gridElement = window.document.getElementById('cvat_canvas_grid');
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
@ -707,18 +893,13 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
CSSImageFilter:
`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
canvasInstance.html().addEventListener(
'canvas.setup',
() => {
const { activatedStateID, activatedAttributeID } = this.props;
canvasInstance.fitCanvas();
canvasInstance.fit();
canvasInstance.activate(activatedStateID, activatedAttributeID);
},
@ -763,6 +944,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
switchableAutomaticBordering,
automaticBordering,
showTagsOnFrame,
canvasIsReady,
onSwitchAutomaticBordering,
onSwitchZLayer,
onAddZLayer,
@ -788,13 +970,20 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
};
return (
<Layout.Content style={{ position: 'relative' }}>
<>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
{/*
This element doesn't have any props
So, React isn't going to rerender it
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
className='cvat-canvas-container'
style={{
@ -804,7 +993,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}}
/>
<ContextImage />
<BrushTools />
<Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}>
@ -832,8 +1020,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
<FrameTags />
</div>
) : 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) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.cvat-canvas-container-overflow {
overflow: hidden;
width: 100%;
height: 100%;
}
.cvat-canvas3d-perspective {
height: 100%;
width: 100%;
@ -77,87 +72,20 @@
}
.cvat-canvas3d-fullsize {
position: relative;
width: 100%;
height: 100%;
}
.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;
height: calc(100% - $grid-unit-size * 3);
}
.cvat-canvas3d-header {
height: $grid-unit-size * 4;
height: $grid-unit-size * 3;
width: 100%;
background-color: $background-color-2;
text-align: center;
vertical-align: middle;
}
.cvat-resizable {
position: relative;
}
.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;
.cvat-canvas-container-overflow {
overflow: hidden;
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_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) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -6,8 +7,8 @@ import './styles.scss';
import React from 'react';
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 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 ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu';
@ -17,7 +18,7 @@ export default function ReviewWorkspaceComponent(): JSX.Element {
return (
<Layout hasSider className='cvat-review-workspace'>
<ControlsSideBarContainer />
<CanvasWrapperContainer />
<CanvasLayout />
<ObjectSideBarComponent objectsList={<ObjectsListContainer readonly />} />
<CanvasContextMenuContainer readonly />
<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) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -11,11 +12,8 @@ import Text from 'antd/lib/typography/Text';
import Tabs from 'antd/lib/tabs';
import Layout from 'antd/lib/layout';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { CombinedState, DimensionType } from 'reducers';
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 AppearanceBlock from 'components/annotation-page/appearance-block';
import IssuesListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/issues-list';
@ -26,7 +24,6 @@ interface OwnProps {
interface StateToProps {
sidebarCollapsed: boolean;
canvasInstance: Canvas | Canvas3d;
jobInstance: any;
}
@ -38,14 +35,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
sidebarCollapsed,
canvas: { instance: canvasInstance },
job: { instance: jobInstance },
},
} = state;
return {
sidebarCollapsed,
canvasInstance,
jobInstance,
};
}
@ -60,15 +55,14 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const {
sidebarCollapsed, canvasInstance, collapseSidebar, objectsList, jobInstance,
sidebarCollapsed, collapseSidebar, objectsList, jobInstance,
} = props;
const collapse = (): void => {
const [collapser] = window.document.getElementsByClassName('cvat-objects-sidebar');
const listener = (event: TransitionEvent): void => {
if (event.target && event.propertyName === 'width' && event.target === collapser) {
canvasInstance.fitCanvas();
canvasInstance.fit();
window.dispatchEvent(new Event('resize'));
(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);
}
adjustContextImagePosition(!sidebarCollapsed);
collapseSidebar();
};

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporations
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -7,12 +7,12 @@ import './styles.scss';
import React from 'react';
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 CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu';
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 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 RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm';
import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm';
@ -21,7 +21,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
return (
<Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer />
<CanvasWrapperContainer />
<CanvasLayout />
<ObjectSideBarComponent objectsList={<ObjectsListContainer />} />
<PropagateConfirmComponent />
<CanvasContextMenuContainer />

@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -7,55 +7,11 @@
.cvat-standard-workspace.ant-layout {
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 {
height: $grid-unit-size * 4;
width: 100%;
z-index: 101;
background: rgba(0, 0, 0, 0.2);
position: absolute;
top: 0;
left: 0;
> .ant-layout-content {
overflow-y: hidden;
overflow-x: hidden;
}
> .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 {

@ -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) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
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 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 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 PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm';
@ -19,7 +20,7 @@ export default function StandardWorkspace3DComponent(): JSX.Element {
return (
<Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer />
<CanvasWrapperContainer />
<CanvasLayout type={DimensionType.DIM_3D} />
<ObjectSideBarComponent objectsList={<ObjectsListContainer />} />
<PropagateConfirmComponent />
<CanvasContextMenuContainer />

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

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -6,14 +7,14 @@ import './styles.scss';
import React from 'react';
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 TagAnnotationSidebar from './tag-annotation-sidebar/tag-annotation-sidebar';
export default function TagAnnotationWorkspace(): JSX.Element {
return (
<Layout hasSider className='cvat-tag-annotation-workspace'>
<CanvasWrapperContainer />
<CanvasLayout />
<TagAnnotationSidebar />
<RemoveConfirmComponent />
</Layout>

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

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

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

@ -24,6 +24,10 @@ const LATEST_COMMENTS_SHOWN_QUICK_ISSUE = 3;
const QUICK_ISSUE_INCORRECT_POSITION_TEXT = 'Wrong position';
const QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT = 'Wrong attribute';
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 DEFAULT_AWS_S3_REGIONS: string[][] = [
['us-east-1', 'US East (N. Virginia)'],
@ -114,4 +118,8 @@ export default {
HEALH_CHECK_RETRIES,
HEALTH_CHECK_PERIOD,
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) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -11,7 +11,7 @@ import {
CombinedState, ContextMenuType, ShapeType, Workspace,
} 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 { reviewActions, finishIssueAsync } from 'actions/review-actions';
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) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -56,7 +57,6 @@ interface StateToProps {
playing: boolean;
saving: boolean;
canvasIsReady: boolean;
savingStatuses: string[];
undoAction?: string;
redoAction?: string;
autoSave: boolean;
@ -106,7 +106,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
},
annotations: {
saving: { uploading: saving, statuses: savingStatuses, forceExit },
saving: { uploading: saving, forceExit },
history,
},
job: { instance: jobInstance },
@ -135,7 +135,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
playing,
canvasIsReady,
saving,
savingStatuses,
frameNumber,
frameFilename,
jobInstance,
@ -607,7 +606,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
const {
playing,
saving,
savingStatuses,
jobInstance,
jobInstance: { startFrame, stopFrame },
frameNumber,
@ -751,7 +749,6 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
workspace={workspace}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}

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

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

@ -85,7 +85,6 @@ const defaultState: NotificationsState = {
saving: null,
jobFetching: null,
frameFetching: null,
contextImageFetching: null,
changingLabelColor: null,
updating: 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: {
return {
...state,

@ -3,9 +3,12 @@
# SPDX-License-Identifier: MIT
import os
import io
import zipfile
from io import BytesIO
from datetime import datetime
from tempfile import NamedTemporaryFile
import cv2
import pytz
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,
Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter,
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.cloud_provider import get_cloud_storage_instance, Credentials
from cvat.apps.engine.utils import md5_hash
@ -34,7 +37,8 @@ class MediaCache:
item = cache.get(key)
if not item:
item = create_function()
cache.set(key, item)
if item[0]:
cache.set(key, item)
return item
@ -62,13 +66,20 @@ class MediaCache:
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
def _get_frame_provider():
from cvat.apps.engine.frame_provider import FrameProvider # TODO: remove circular dependency
return FrameProvider
def _prepare_chunk_buff(self, db_data, quality, chunk_number):
FrameProvider = self._get_frame_provider()
writer_classes = {
@ -183,3 +194,25 @@ class MediaCache:
mime_type = mimetypes.guess_type(preview_path)[0]
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) 2022 CVAT.ai Corporation
# Copyright (C) 2022-2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT
@ -924,7 +924,7 @@ class FrameMetaSerializer(serializers.Serializer):
width = serializers.IntegerField()
height = serializers.IntegerField()
name = serializers.CharField(max_length=1024)
has_related_context = serializers.BooleanField()
related_files = serializers.IntegerField()
class PluginsSerializer(serializers.Serializer):
GIT_INTEGRATION = serializers.BooleanField()

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

@ -73,7 +73,7 @@ context('Actions on ellipse.', () => {
it('Ellipse rotation/interpolation.', () => {
Cypress.config('scrollBehavior', false);
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);
// Rotation with shift
cy.shapeRotate('#cvat_canvas_shape_4', '15.0', true);

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -35,8 +36,8 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', ()
labelName,
tracker: 'TrackerMIL',
pointsMap: [
{ x: 440, y: 45 },
{ x: 650, y: 150 },
{ x: 430, y: 40 },
{ 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,
// so we expect shape to be close to real text positions
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) => {
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')
.should('contain', 'RECTANGLE TRACK')

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -45,29 +46,28 @@ context('Settings. Text size/position. Text labels content.', () => {
let textTopPosition = 0;
let getText;
cy.get(shape).then(($shape) => {
shapeLeftPosition = Math.trunc($shape.position().left);
shapeTopPosition = Math.trunc($shape.position().top);
if (shape === '#cvat_canvas_shape_1') {
shapeWidth = $shape.attr('width');
shapeHeight = $shape.attr('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];
}
cy.get(shape).then(([shapeObj]) => {
const shapeBBox = shapeObj.getBoundingClientRect();
shapeLeftPosition = shapeBBox.left;
shapeTopPosition = shapeBBox.top;
shapeWidth = shapeBBox.width;
shapeHeight = shapeBBox.height;
if (shape === '#cvat_canvas_shape_1') {
getText = cy.get('.cvat_canvas_text').first();
} else {
getText = cy.get('.cvat_canvas_text').last();
}
getText.then(($text) => {
textLeftPosition = Math.trunc($text.position().left);
textTopPosition = Math.trunc($text.position().top);
getText.then(([textObj]) => {
const textBBox = textObj.getBoundingClientRect();
textLeftPosition = textBBox.left;
textTopPosition = textBBox.top;
if (expectedPosition === 'outside') {
// Text outside the shape of the right. Slightly below the shape upper edge.
expect(+shapeLeftPosition + +shapeWidth).lessThan(+textLeftPosition);
expect(+textTopPosition).to.be.within(+shapeTopPosition, +shapeTopPosition + 10);
expect(+textTopPosition).to.be.within(+shapeTopPosition, +shapeTopPosition + 15);
} else {
// Text inside the shape
expect(+shapeLeftPosition + +shapeWidth / 2).greaterThan(+textLeftPosition);
@ -108,6 +108,21 @@ context('Settings. Text size/position. Text labels content.', () => {
});
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.', () => {
testTextPosition('#cvat_canvas_shape_1', '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');
});
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.', () => {
cy.openSettings();
cy.get('.cvat-workspace-settings-text-content').within(() => {

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -29,10 +30,11 @@ context('Canvas color feature', () => {
.click()
.should('have.css', 'background-color')
.then((colorPickerBgValue) => {
cy.get('.cvat-canvas-container')
cy.get('.cvat-canvas-grid-root')
.should('have.css', 'background-color')
.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);
});
});

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

@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -11,7 +12,7 @@ context('Attribute annotation mode (AAM) zoom margin feature', () => {
const rectangleShape2Points = {
points: 'By 2 Points',
type: 'Shape',
labelName: labelName,
labelName,
firstX: 100,
firstY: 100,
secondX: 150,
@ -43,6 +44,8 @@ context('Attribute annotation mode (AAM) zoom margin feature', () => {
describe(`Testing case "${caseId}"`, () => {
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.getScaleValue().then((scaleBeforeChangeZoomMargin) => {
changeSettingsZoomMargin(150);

@ -61,7 +61,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => {
.trigger('mousemove')
.trigger('mouseover');
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_shape_1').should('have.attr', 'transform');
cy.document().then((doc) => {

@ -1,4 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -11,36 +12,6 @@ context('Context images for 2D tasks.', () => {
const attrName = `Attr for ${labelName}`;
const textDefaultValue = 'color';
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(() => {
cy.visit('auth/login');
@ -56,65 +27,10 @@ context('Context images for 2D tasks.', () => {
describe(`Testing case "${caseId}"`, () => {
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-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-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) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// 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 firstCuboidCreationParams = {
labelName,
x: 480,
y: 150,
x: 400,
y: 200,
};
const secondCuboidCreationParams = {
labelName,
x: 480,
y: 200,
x: 400,
y: 280,
};
const thirdCuboidCreationParams = {
labelName,
x: 530,
y: 150,
x: 500,
y: 280,
};
const fourthCuboidCreationParams = {
labelName,
x: 530,
x: 500,
y: 200,
};
const yellowHex = 'fcbe03';
@ -64,8 +64,8 @@ context('Canvas 3D functionality. Grouping.', () => {
describe(`Testing case "${caseId}"`, () => {
it('Grouping two cuboids.', () => {
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', 530, 150).click(530, 150);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 400, 280).click(400, 280);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 500, 280).click(500, 280);
cy.get('.cvat-group-control').click();
cy.changeAppearance('Group');
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.', () => {
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_before_reset_group');
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', 530, 150).click(530, 150);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 400, 280).click(400, 280);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 500, 280).click(500, 280);
cy.get('body').type('{Shift}g');
cy.get('#cvat-objects-sidebar-state-item-2').invoke('attr', 'style').then((bgColorItem2) => {
expect(bgColorItem).to.be.equal(bgColorItem2);

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

@ -1,5 +1,5 @@
// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
// Copyright (C) 2022-2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
@ -41,7 +41,7 @@ context('Canvas 3D functionality. Basic actions.', () => {
function testPerspectiveChangeOnWheel(screenshotNameBefore, screenshotNameAfter) {
cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameBefore);
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.compareImagesAndCheckResult(
@ -52,9 +52,12 @@ context('Canvas 3D functionality. Basic actions.', () => {
function testTopSideFrontChangeOnWheel(element, screenshotNameBefore, screenshotNameAfter) {
cy.customScreenshot(element, screenshotNameBefore);
for (let i = 0; i < 3; i++) {
cy.get(element).trigger('wheel', { deltaY: -100 });
}
cy.get(element).within(() => {
for (let i = 0; i < 3; i++) {
cy.get('.cvat-canvas3d-fullsize canvas').trigger('wheel', { deltaY: -100 });
}
});
cy.customScreenshot(element, screenshotNameAfter);
cy.compareImagesAndCheckResult(
`${screenshotsPath}/${screenshotNameBefore}.png`,
@ -63,10 +66,7 @@ context('Canvas 3D functionality. Basic actions.', () => {
}
function testContextImage() {
cy.get('.cvat-context-image-wrapper img').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
cy.get('.cvat-context-image-wrapper canvas').should('exist').and('be.visible');
}
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.', () => {
const caseId = '82';
const screenshotsPath = 'cypress/screenshots/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js';
const cuboidCreationParams = {
labelName,
x: 500,
@ -21,48 +20,48 @@ context('Canvas 3D functionality. Opacity. Outlined borders.', () => {
before(() => {
cy.openTask(taskName);
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.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}"`, () => {
it('Change opacity to 100. To 0.', () => {
cy.get('.cvat-appearance-opacity-slider').click('right');
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.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_perspective_deactivate_cuboid.png`,
`${screenshotsPath}/canvas3d_perspective_opacty_100.png`,
);
cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
expect(getFirstChild(el).material.opacity).to.equal(1);
});
cy.get('.cvat-appearance-opacity-slider').click('left');
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.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_perspective_opacty_100.png`,
`${screenshotsPath}/canvas3d_perspective_opacty_0.png`,
);
cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
expect(getFirstChild(el).material.opacity).to.equal(0);
});
cy.get('body').click();
});
it('Change selected opacity to 100. To 0.', () => {
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('body').click();
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(
`${screenshotsPath}/canvas3d_perspective_opacty_100.png`,
`${screenshotsPath}/canvas3d_perspective_selected_opacty_100.png`,
true, // No diff between the images
);
cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
expect(el.scene.children[0].children[0].material.opacity).to.equal(1);
});
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.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_selected_opacty_0');
cy.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_perspective_opacty_0.png`,
`${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`,
true, // No diff between the images
);
cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
expect(getFirstChild(el).material.opacity).to.equal(0);
});
});
it('Enable/disable outlined borders.', () => {
@ -72,18 +71,14 @@ context('Canvas 3D functionality. Opacity. Outlined borders.', () => {
cy.get('div[title="#ff007c"]').click();
cy.contains('Ok').click();
});
cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_enable_outlined_borders');
cy.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_perspective_enable_outlined_borders.png`,
`${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`,
);
cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
expect({ ...getWireframe(el).material.color }).to.deep.equal({ r: 1, g: 0, b: 0.48627450980392156 });
});
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.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_perspective_disable_outlined_borders.png`,
`${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`,
true, // No diff between the images
);
cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => {
expect({ ...getWireframe(el).material.color }).to.deep.equal({ ...getFirstChild(el).material.color });
});
});
});
});

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

@ -1884,7 +1884,7 @@
resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.0.5.tgz#9262f269de36f1e9248aeb9dee0ee9d10065e043"
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"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb"
integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw==
@ -1917,31 +1917,31 @@
"@types/react" "*"
"@types/reactcss" "*"
"@types/react-dom@^16.9.14":
version "16.9.17"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.17.tgz#29100cbcc422d7b7dba7de24bb906de56680dd34"
integrity sha512-qSRyxEsrm5btPXnowDOs5jSkgT8ldAA0j6Qp+otHUh+xHzy3sXmgNfyhucZjAjkgpdAUw9rJe0QRtX/l+yaS4g==
"@types/react-dom@^16.9.14", "@types/react-dom@^18.0.5":
version "18.0.10"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352"
integrity sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==
dependencies:
"@types/react" "^16"
"@types/react" "*"
"@types/react-redux@^7.1.18":
version "7.1.24"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==
"@types/react-grid-layout@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.2.tgz#9f195666a018a5ae2b773887e3b552cb4378d67f"
integrity sha512-ZzpBEOC1JTQ7MGe1h1cPKSLP4jSWuxc+yvT4TsAlEW9+EFPzAf8nxQfFd7ea9gL17Em7PbwJZAsiwfQQBUklZQ==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-resizable@^3.0.1":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/react-resizable/-/react-resizable-3.0.2.tgz#3c914be6b02c8d6864b82ffb6461b2e8a771fb75"
integrity sha512-4rHjZDQmSpFqRlNzlcnF5tpOG5fBcMuDlvD+qT3XHAJLKGx/FC3iDQ9li9tHW53ecWwZzHTPCGvz5vNWQN+v/Q==
"@types/react-redux@^7.1.18", "@types/react-redux@^7.1.24":
version "7.1.25"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88"
integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@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"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
@ -1965,19 +1965,10 @@
dependencies:
"@types/react" "*"
"@types/react@*":
version "17.0.48"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.48.tgz#a4532a8b91d7b27b8768b6fc0c3bccb760d15a6c"
integrity sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A==
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==
"@types/react@*", "@types/react@^16", "@types/react@^16.14.15", "@types/react@^17.0.30":
version "17.0.52"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b"
integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@ -4107,7 +4098,7 @@ custom-error-instance@2.1.1:
integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==
"cvat-canvas3d@link:./cvat-canvas3d":
version "0.0.6"
version "0.0.7"
dependencies:
"@types/three" "^0.125.3"
camera-controls "^1.25.3"
@ -4115,7 +4106,7 @@ custom-error-instance@2.1.1:
three "^0.126.1"
"cvat-canvas@link:./cvat-canvas":
version "2.16.1"
version "2.16.2"
dependencies:
"@types/fabric" "^4.5.7"
"@types/polylabel" "^1.0.5"
@ -4128,7 +4119,7 @@ custom-error-instance@2.1.1:
svg.select.js "3.0.1"
"cvat-core@link:./cvat-core":
version "7.4.0"
version "8.0.0"
dependencies:
axios "^0.27.2"
browser-or-node "^2.0.0"
@ -4145,7 +4136,7 @@ custom-error-instance@2.1.1:
tus-js-client "^3.0.1"
"cvat-data@link:./cvat-data":
version "1.0.2"
version "1.1.0"
dependencies:
async-mutex "^0.4.0"
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"
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:
version "4.0.6"
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"
scheduler "^0.19.1"
react-draggable@^4.0.3:
react-draggable@^4.0.0, react-draggable@^4.0.3:
version "4.4.5"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g==
@ -10541,6 +10537,17 @@ react-draggable@^4.0.3:
clsx "^1.1.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:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"

Loading…
Cancel
Save