未验证 提交 fb0b8675 编写于 作者: B Boris Sekachev 提交者: GitHub

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">
上级 bc33ba43
......@@ -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
......
{
"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": {
......
// 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;
});
}
......
// 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`);
......
{
"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;
}
});
}
......
// 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) {
......
{
"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);
}
......
// 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: {
......
{
"name": "cvat-data",
"version": "1.0.2",
"version": "1.1.0",
"description": "",
"main": "src/ts/cvat-data.ts",
"scripts": {
......
此差异已折叠。
// 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;
{
"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",
......
// 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 {
......
// 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,
......
// 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);
......
// 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>
......
// 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);
};
......
// 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>
);
......
// 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;
// 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);
// 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);
}
}
}
// 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;
......
// 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);
// 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;
}
// 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);
// 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);
// 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;
}
}
}
}
// 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 />
......
// 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);
// 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();
};
......
// 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 />
......
// 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 {
......
// 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);
// 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 />
......
// 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;
}
}
// 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;
}
// 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);
}}
>
......
// 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>
......
// 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 />}>
......
// 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}
......
// 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,
};
// 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';
......
// 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);
// 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);
此差异已折叠。
此差异已折叠。
# 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()
......
此差异已折叠。
......@@ -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);
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册