CVAT-3D milestone6 (#3234)

Added support for Dump annotations, Export Annotations and Upload annotations in PCD and Kitti formats.

Co-authored-by: cdp <cdp123>
Co-authored-by: Jayraj <jayrajsolanki96@gmail.com>
Co-authored-by: dvkruchinin <dvkruchinin@gmail.com>
Co-authored-by: Smirnova Maria <mariax.smirnova@intel.com>
main
manasars 5 years ago committed by GitHub
parent 04f63a4063
commit c58e915c32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -91,6 +91,7 @@ export enum Mode {
INTERACT = 'interact', INTERACT = 'interact',
DRAG_CANVAS = 'drag_canvas', DRAG_CANVAS = 'drag_canvas',
GROUP = 'group', GROUP = 'group',
BUSY = 'busy',
} }
export interface Canvas3dDataModel { export interface Canvas3dDataModel {
@ -102,6 +103,7 @@ export interface Canvas3dDataModel {
imageSize: Size; imageSize: Size;
drawData: DrawData; drawData: DrawData;
mode: Mode; mode: Mode;
objectUpdating: boolean;
exception: Error | null; exception: Error | null;
objects: any[]; objects: any[];
groupedObjects: any[]; groupedObjects: any[];
@ -140,6 +142,7 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
height: 0, height: 0,
width: 0, width: 0,
}, },
objectUpdating: false,
objects: [], objects: [],
groupedObjects: [], groupedObjects: [],
image: null, image: null,
@ -179,13 +182,16 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
} }
if ([Mode.EDIT, Mode.BUSY].includes(this.data.mode)) {
if ([Mode.EDIT].includes(this.data.mode)) {
return; return;
} }
if (frameData.number === this.data.imageID) { if (frameData.number === this.data.imageID) {
if (this.data.objectUpdating) {
return;
}
this.data.objects = objectStates; this.data.objects = objectStates;
this.data.objectUpdating = true;
this.notify(UpdateReasons.OBJECTS_UPDATED); this.notify(UpdateReasons.OBJECTS_UPDATED);
return; return;
} }
@ -228,16 +234,16 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
} }
public isAbleToChangeFrame(): boolean { public isAbleToChangeFrame(): boolean {
const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT].includes(this.data.mode) const isUnable = [Mode.DRAG, Mode.EDIT, Mode.RESIZE, Mode.INTERACT, Mode.BUSY].includes(this.data.mode)
|| (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number'); || (this.data.mode === Mode.DRAW && typeof this.data.drawData.redraw === 'number');
return !isUnable; return !isUnable;
} }
public draw(drawData: DrawData): void { public draw(drawData: DrawData): void {
if (drawData.enabled && this.data.drawData.enabled) { if (drawData.enabled && this.data.drawData.enabled && !drawData.initialState) {
throw new Error('Drawing has been already started'); throw new Error('Drawing has been already started');
} }
if ([Mode.DRAW, Mode.EDIT].includes(this.data.mode)) { if ([Mode.DRAW, Mode.EDIT].includes(this.data.mode) && !drawData.initialState) {
return; return;
} }
this.data.drawData.enabled = drawData.enabled; this.data.drawData.enabled = drawData.enabled;
@ -318,6 +324,9 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel {
} }
public configureShapes(shapeProperties: ShapeProperties): void { public configureShapes(shapeProperties: ShapeProperties): void {
this.data.drawData.enabled = false;
this.data.mode = Mode.IDLE;
this.cancel();
this.data.shapeProperties = { this.data.shapeProperties = {
...shapeProperties, ...shapeProperties,
}; };

@ -111,6 +111,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}, },
}; };
this.action = { this.action = {
loading: false,
oldState: '',
scan: null, scan: null,
selectable: true, selectable: true,
frameCoordinates: { frameCoordinates: {
@ -216,6 +218,35 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}), }),
); );
} }
if (this.model.mode === Mode.DRAW && e.ctrlKey && this.model.data.drawData.initialState) {
const { x, y, z } = this.cube.perspective.position;
const { x: width, y: height, z: depth } = this.cube.perspective.scale;
const { x: rotationX, y: rotationY, z: rotationZ } = this.cube.perspective.rotation;
const points = [x, y, z, rotationX, rotationY, rotationZ, width, height, depth, 0, 0, 0, 0, 0, 0, 0];
const initState = this.model.data.drawData.initialState;
let label;
if (initState) {
({ label } = initState);
}
this.dispatchEvent(
new CustomEvent('canvas.drawn', {
bubbles: false,
cancelable: true,
detail: {
state: {
...initState,
shapeType: 'cuboid',
frame: this.model.data.imageID,
points,
label,
},
continue: true,
duration: 0,
},
}),
);
this.action.oldState = Mode.DRAW;
}
}); });
canvasTopView.addEventListener('mousedown', this.startAction.bind(this, 'top')); canvasTopView.addEventListener('mousedown', this.startAction.bind(this, 'top'));
@ -293,7 +324,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.views.perspective.scene.children[0].children, this.views.perspective.scene.children[0].children,
false, false,
); );
if (intersects.length !== 0) { if (intersects.length !== 0 || this.controller.focused.clientID !== null) {
this.setDefaultZoom(); this.setDefaultZoom();
} else { } else {
const { x, y, z } = this.action.frameCoordinates; const { x, y, z } = this.action.frameCoordinates;
@ -417,6 +448,12 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE; viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE;
} else { } else {
viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement); viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement);
viewType.controls.mouseButtons.left = CameraControls.ACTION.NONE;
viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE;
viewType.controls.mouseButtons.wheel = CameraControls.ACTION.NONE;
viewType.controls.touches.one = CameraControls.ACTION.NONE;
viewType.controls.touches.two = CameraControls.ACTION.NONE;
viewType.controls.touches.three = CameraControls.ACTION.NONE;
} }
viewType.controls.minDistance = CONST.MIN_DISTANCE; viewType.controls.minDistance = CONST.MIN_DISTANCE;
viewType.controls.maxDistance = CONST.MAX_DISTANCE; viewType.controls.maxDistance = CONST.MAX_DISTANCE;
@ -493,6 +530,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private startAction(view: any, event: MouseEvent): void { private startAction(view: any, event: MouseEvent): void {
if (event.detail !== 1) return; if (event.detail !== 1) return;
if (this.model.mode === Mode.DRAG_CANVAS) return;
const { clientID } = this.model.data.activeElement; const { clientID } = this.model.data.activeElement;
if (clientID === 'null') return; if (clientID === 'null') return;
const canvas = this.views[view as keyof Views].renderer.domElement; const canvas = this.views[view as keyof Views].renderer.domElement;
@ -517,6 +555,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private moveAction(view: any, event: MouseEvent): void { private moveAction(view: any, event: MouseEvent): void {
event.preventDefault(); event.preventDefault();
if (this.model.mode === Mode.DRAG_CANVAS) return;
const { clientID } = this.model.data.activeElement; const { clientID } = this.model.data.activeElement;
if (clientID === 'null') return; if (clientID === 'null') return;
const canvas = this.views[view as keyof Views].renderer.domElement; const canvas = this.views[view as keyof Views].renderer.domElement;
@ -571,10 +610,13 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
recentMouseVector: new THREE.Vector2(0, 0), recentMouseVector: new THREE.Vector2(0, 0),
}, },
}; };
this.model.mode = Mode.IDLE;
this.action.selectable = true;
} }
private completeActions(): void { private completeActions(): void {
const { scan, detected } = this.action; const { scan, detected } = this.action;
if (this.model.mode === Mode.DRAG_CANVAS) return;
if (!detected) { if (!detected) {
this.resetActions(); this.resetActions();
return; return;
@ -604,8 +646,6 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.adjustPerspectiveCameras(); this.adjustPerspectiveCameras();
this.translateReferencePlane(new THREE.Vector3(x, y, z)); this.translateReferencePlane(new THREE.Vector3(x, y, z));
this.resetActions(); this.resetActions();
this.model.mode = Mode.IDLE;
this.action.selectable = true;
} }
private onGroupDone(objects?: any[]): void { private onGroupDone(objects?: any[]): void {
@ -718,6 +758,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const object = this.model.data.objects[i]; const object = this.model.data.objects[i];
this.setupObject(object, true); this.setupObject(object, true);
} }
this.action.loading = false;
} }
} }
@ -735,6 +776,13 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
public notify(model: Canvas3dModel & Master, reason: UpdateReasons): void { public notify(model: Canvas3dModel & Master, reason: UpdateReasons): void {
if (reason === UpdateReasons.IMAGE_CHANGED) { if (reason === UpdateReasons.IMAGE_CHANGED) {
if (!model.data.image) return; if (!model.data.image) return;
this.dispatchEvent(new CustomEvent('canvas.canceled'));
if (this.model.mode === Mode.DRAW) {
this.model.data.drawData.enabled = false;
}
this.views.perspective.renderer.dispose();
this.model.mode = Mode.BUSY;
this.action.loading = true;
const loader = new PCDLoader(); const loader = new PCDLoader();
const objectURL = URL.createObjectURL(model.data.image.imageData); const objectURL = URL.createObjectURL(model.data.image.imageData);
this.clearScene(); this.clearScene();
@ -766,12 +814,21 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.setupObjects(); this.setupObjects();
} else if (reason === UpdateReasons.DRAG_CANVAS) { } else if (reason === UpdateReasons.DRAG_CANVAS) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(this.mode === Mode.DRAG_CANVAS ? 'canvas.dragstart' : 'canvas.dragstop', { new CustomEvent(this.model.mode === Mode.DRAG_CANVAS ? 'canvas.dragstart' : 'canvas.dragstop', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
}), }),
); );
this.model.data.activeElement.clientID = 'null'; this.model.data.activeElement.clientID = 'null';
if (this.model.mode === Mode.DRAG_CANVAS) {
const { controls } = this.views.perspective;
controls.mouseButtons.left = CameraControls.ACTION.ROTATE;
controls.mouseButtons.right = CameraControls.ACTION.TRUCK;
controls.mouseButtons.wheel = CameraControls.ACTION.DOLLY;
controls.touches.one = CameraControls.ACTION.TOUCH_ROTATE;
controls.touches.two = CameraControls.ACTION.TOUCH_DOLLY_TRUCK;
controls.touches.three = CameraControls.ACTION.TOUCH_TRUCK;
}
this.setupObjects(); this.setupObjects();
} else if (reason === UpdateReasons.CANCEL) { } else if (reason === UpdateReasons.CANCEL) {
if (this.mode === Mode.DRAW) { if (this.mode === Mode.DRAW) {
@ -783,7 +840,14 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
} }
this.model.data.groupData.grouped = []; this.model.data.groupData.grouped = [];
this.setHelperVisibility(false); this.setHelperVisibility(false);
this.mode = Mode.IDLE; this.model.mode = Mode.IDLE;
const { controls } = this.views.perspective;
controls.mouseButtons.left = CameraControls.ACTION.NONE;
controls.mouseButtons.right = CameraControls.ACTION.NONE;
controls.mouseButtons.wheel = CameraControls.ACTION.NONE;
controls.touches.one = CameraControls.ACTION.NONE;
controls.touches.two = CameraControls.ACTION.NONE;
controls.touches.three = CameraControls.ACTION.NONE;
this.dispatchEvent(new CustomEvent('canvas.canceled')); this.dispatchEvent(new CustomEvent('canvas.canceled'));
} else if (reason === UpdateReasons.FITTED_CANVAS) { } else if (reason === UpdateReasons.FITTED_CANVAS) {
this.dispatchEvent(new CustomEvent('canvas.fit')); this.dispatchEvent(new CustomEvent('canvas.fit'));
@ -1153,6 +1217,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
} }
this.updateRotationHelperPos(); this.updateRotationHelperPos();
this.updateResizeHelperPos(); this.updateResizeHelperPos();
} else {
this.resetActions();
} }
} }
} }
@ -1166,6 +1232,16 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.action.detachCam = false; this.action.detachCam = false;
} }
} }
if (this.model.mode === Mode.BUSY && !this.action.loading) {
if (this.action.oldState !== '') {
this.model.mode = this.action.oldState;
this.action.oldState = '';
} else {
this.model.mode = Mode.IDLE;
}
} else if (this.model.data.objectUpdating && !this.action.loading) {
this.model.data.objectUpdating = false;
}
} }
private adjustPerspectiveCameras(): void { private adjustPerspectiveCameras(): void {
@ -1729,9 +1805,6 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
true, true,
); );
if (intersectsBox.length !== 0) { if (intersectsBox.length !== 0) {
// const [state] = this.model.data.objects.filter(
// (_state: any): boolean => _state.clientID === Number(this.model.data.selected[view].name),
// );
if (state.pinned) return; if (state.pinned) return;
this.action.translation.helper = viewType.rayCaster.mouseVector.clone(); this.action.translation.helper = viewType.rayCaster.mouseVector.clone();
this.action.translation.inverseMatrix = intersectsBox[0].object.parent.matrixWorld.invert(); this.action.translation.inverseMatrix = intersectsBox[0].object.parent.matrixWorld.invert();
@ -1749,26 +1822,24 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
public keyControls(key: any): void { public keyControls(key: any): void {
const { controls } = this.views.perspective; const { controls } = this.views.perspective;
if (!controls) return; if (!controls) return;
switch (key.code) { if (key.shiftKey) {
case CameraAction.ROTATE_RIGHT: switch (key.code) {
controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true); case CameraAction.ROTATE_RIGHT:
break; controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
case CameraAction.ROTATE_LEFT: break;
controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true); case CameraAction.ROTATE_LEFT:
break; controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
case CameraAction.TILT_UP: break;
controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true); case CameraAction.TILT_UP:
break; controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
case CameraAction.TILT_DOWN: break;
controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true); case CameraAction.TILT_DOWN:
break; controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
case 'ControlLeft': break;
this.action.selectable = !key.ctrlKey; default:
break; break;
default: }
break; } else if (key.altKey === true) {
}
if (key.altKey === true) {
switch (key.code) { switch (key.code) {
case CameraAction.ZOOM_IN: case CameraAction.ZOOM_IN:
controls.dolly(CONST.DOLLY_FACTOR, true); controls.dolly(CONST.DOLLY_FACTOR, true);
@ -1791,6 +1862,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
default: default:
break; break;
} }
} else if (key.code === 'ControlLeft') {
this.action.selectable = !key.ctrlKey;
} }
} }

@ -6,7 +6,7 @@ const BASE_GRID_WIDTH = 2;
const MOVEMENT_FACTOR = 200; const MOVEMENT_FACTOR = 200;
const DOLLY_FACTOR = 5; const DOLLY_FACTOR = 5;
const MAX_DISTANCE = 100; const MAX_DISTANCE = 100;
const MIN_DISTANCE = 0; const MIN_DISTANCE = 0.3;
const ZOOM_FACTOR = 7; const ZOOM_FACTOR = 7;
const ROTATION_HELPER_OFFSET = 0.1; const ROTATION_HELPER_OFFSET = 0.1;
const CAMERA_REFERENCE = 'camRef'; const CAMERA_REFERENCE = 'camRef';

@ -30,6 +30,7 @@ interface Props {
outlined: boolean; outlined: boolean;
outlineColor: string; outlineColor: string;
colorBy: ColorBy; colorBy: ColorBy;
frameFetching: boolean;
canvasInstance: Canvas3d | Canvas; canvasInstance: Canvas3d | Canvas;
jobInstance: any; jobInstance: any;
frameData: any; frameData: any;
@ -186,6 +187,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
activeObjectType, activeObjectType,
onShapeDrawn, onShapeDrawn,
onCreateAnnotations, onCreateAnnotations,
frameFetching,
} = props; } = props;
const { canvasInstance } = props as { canvasInstance: Canvas3d }; const { canvasInstance } = props as { canvasInstance: Canvas3d };
@ -398,16 +400,16 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
}; };
}, [frameData, annotations, activeLabelID, contextMenuVisibility]); }, [frameData, annotations, activeLabelID, contextMenuVisibility]);
const screenKeyControl = (code: CameraAction): void => { const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => {
canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey: true })); canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey }));
}; };
const ArrowGroup = (): ReactElement => ( const ArrowGroup = (): ReactElement => (
<span className='cvat-canvas3d-perspective-arrow-directions'> <span className='cvat-canvas3d-perspective-arrow-directions'>
<CVATTooltip title='Arrow Up' placement='topRight'> <CVATTooltip title='Shift+Arrow Up' placement='topRight'>
<button <button
data-cy='arrow-up' data-cy='arrow-up'
onClick={() => screenKeyControl(CameraAction.TILT_UP)} onClick={() => screenKeyControl(CameraAction.TILT_UP, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-up' className='cvat-canvas3d-perspective-arrow-directions-icons-up'
> >
@ -415,27 +417,27 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</button> </button>
</CVATTooltip> </CVATTooltip>
<br /> <br />
<CVATTooltip title='Arrow Left' placement='topRight'> <CVATTooltip title='Shift+Arrow Left' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT)} onClick={() => screenKeyControl(CameraAction.ROTATE_LEFT, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom' className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
> >
<ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' /> <ArrowLeftOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button> </button>
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Arrow Bottom' placement='topRight'> <CVATTooltip title='Shift+Arrow Bottom' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CameraAction.TILT_DOWN)} onClick={() => screenKeyControl(CameraAction.TILT_DOWN, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom' className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
> >
<ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' /> <ArrowDownOutlined className='cvat-canvas3d-perspective-arrow-directions-icons-color' />
</button> </button>
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Arrow Right' placement='topRight'> <CVATTooltip title='Shift+Arrow Right' placement='topRight'>
<button <button
onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT)} onClick={() => screenKeyControl(CameraAction.ROTATE_RIGHT, false, true)}
type='button' type='button'
className='cvat-canvas3d-perspective-arrow-directions-icons-bottom' className='cvat-canvas3d-perspective-arrow-directions-icons-bottom'
> >
@ -449,7 +451,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
<span className='cvat-canvas3d-perspective-directions'> <span className='cvat-canvas3d-perspective-directions'>
<CVATTooltip title='Alt+U' placement='topLeft'> <CVATTooltip title='Alt+U' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CameraAction.MOVE_UP)} onClick={() => screenKeyControl(CameraAction.MOVE_UP, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -458,7 +460,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+I' placement='topLeft'> <CVATTooltip title='Alt+I' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CameraAction.ZOOM_IN)} onClick={() => screenKeyControl(CameraAction.ZOOM_IN, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -467,7 +469,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+O' placement='topLeft'> <CVATTooltip title='Alt+O' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CameraAction.MOVE_DOWN)} onClick={() => screenKeyControl(CameraAction.MOVE_DOWN, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -477,7 +479,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
<br /> <br />
<CVATTooltip title='Alt+J' placement='topLeft'> <CVATTooltip title='Alt+J' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CameraAction.MOVE_LEFT)} onClick={() => screenKeyControl(CameraAction.MOVE_LEFT, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -486,7 +488,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+K' placement='topLeft'> <CVATTooltip title='Alt+K' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CameraAction.ZOOM_OUT)} onClick={() => screenKeyControl(CameraAction.ZOOM_OUT, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -495,7 +497,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
</CVATTooltip> </CVATTooltip>
<CVATTooltip title='Alt+L' placement='topLeft'> <CVATTooltip title='Alt+L' placement='topLeft'>
<button <button
onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT)} onClick={() => screenKeyControl(CameraAction.MOVE_RIGHT, true, false)}
type='button' type='button'
className='cvat-canvas3d-perspective-directions-icon' className='cvat-canvas3d-perspective-directions-icon'
> >
@ -516,6 +518,11 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
handle={<span className='cvat-resizable-handle-horizontal' />} handle={<span className='cvat-resizable-handle-horizontal' />}
onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })} onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })}
> >
{frameFetching ? (
<svg id='cvat_canvas_loading_animation'>
<circle id='cvat_canvas_loading_circle' r='30' cx='50%' cy='50%' />
</svg>
) : null}
<div className='cvat-canvas3d-perspective' id='cvat-canvas3d-perspective'> <div className='cvat-canvas3d-perspective' id='cvat-canvas3d-perspective'>
<div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} /> <div className='cvat-canvas-container cvat-canvas-container-overflow' ref={perspectiveView} />
<ArrowGroup /> <ArrowGroup />

@ -38,7 +38,7 @@ function ContextImage(): JSX.Element | null {
if (requested) { if (requested) {
setRequested(false); setRequested(false);
} }
}, [frame]); }, [frame, contextImageData]);
useEffect(() => { useEffect(() => {
if (hasRelatedContext && !contextImageHidden && !requested) { if (hasRelatedContext && !contextImageHidden && !requested) {

@ -13,7 +13,7 @@ import Layout from 'antd/lib/layout';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { CombinedState } from 'reducers/interfaces'; import { CombinedState, DimensionType } from 'reducers/interfaces';
import LabelsList from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list'; import LabelsList from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list';
import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image'; import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image';
import { collapseSidebar as collapseSidebarAction } from 'actions/annotation-actions'; import { collapseSidebar as collapseSidebarAction } from 'actions/annotation-actions';
@ -27,6 +27,7 @@ interface OwnProps {
interface StateToProps { interface StateToProps {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
canvasInstance: Canvas | Canvas3d; canvasInstance: Canvas | Canvas3d;
jobInstance: any;
} }
interface DispatchToProps { interface DispatchToProps {
@ -38,12 +39,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotation: { annotation: {
sidebarCollapsed, sidebarCollapsed,
canvas: { instance: canvasInstance }, canvas: { instance: canvasInstance },
job: { instance: jobInstance },
}, },
} = state; } = state;
return { return {
sidebarCollapsed, sidebarCollapsed,
canvasInstance, canvasInstance,
jobInstance,
}; };
} }
@ -57,7 +60,11 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { const {
sidebarCollapsed, canvasInstance, collapseSidebar, objectsList, sidebarCollapsed,
canvasInstance,
collapseSidebar,
objectsList,
jobInstance,
} = props; } = props;
const collapse = (): void => { const collapse = (): void => {
@ -78,6 +85,11 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E
collapseSidebar(); collapseSidebar();
}; };
let is2D = true;
if (jobInstance) {
is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
}
return ( return (
<Layout.Sider <Layout.Sider
className='cvat-objects-sidebar' className='cvat-objects-sidebar'
@ -106,9 +118,14 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E
<Tabs.TabPane forceRender tab={<Text strong>Labels</Text>} key='labels'> <Tabs.TabPane forceRender tab={<Text strong>Labels</Text>} key='labels'>
<LabelsList /> <LabelsList />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane tab={<Text strong>Issues</Text>} key='issues'>
<IssuesListComponent /> {is2D ?
</Tabs.TabPane> (
<Tabs.TabPane tab={<Text strong>Issues</Text>} key='issues'>
<IssuesListComponent />
</Tabs.TabPane>
) : null}
</Tabs> </Tabs>
{!sidebarCollapsed && <AppearanceBlock />} {!sidebarCollapsed && <AppearanceBlock />}

@ -142,3 +142,22 @@
height: 100%; height: 100%;
cursor: ew-resize; cursor: ew-resize;
} }
#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_hidden {
display: none;
}

@ -12,6 +12,7 @@ import { CombinedState } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
visible: boolean; visible: boolean;
jobInstance: any;
} }
interface DispatchToProps { interface DispatchToProps {
@ -21,10 +22,14 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { const {
shortcuts: { visibleShortcutsHelp: visible }, shortcuts: { visibleShortcutsHelp: visible },
annotation: {
job: { instance: jobInstance },
},
} = state; } = state;
return { return {
visible, visible,
jobInstance,
}; };
} }
@ -37,7 +42,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
} }
function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | null { function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | null {
const { visible, switchShortcutsDialog } = props; const { visible, switchShortcutsDialog, jobInstance } = props;
const keyMap = getApplicationKeyMap(); const keyMap = getApplicationKeyMap();
const splitToRows = (data: string[]): JSX.Element[] => const splitToRows = (data: string[]): JSX.Element[] =>
@ -76,13 +81,17 @@ function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | n
}, },
]; ];
const dataSource = Object.keys(keyMap).map((key: string, id: number) => ({ const dimensionType = jobInstance ? jobInstance.task.dimension : undefined;
key: id,
name: keyMap[key].name || key, const dataSource = Object.keys(keyMap)
description: keyMap[key].description || '', .filter((key: string) => !dimensionType || keyMap[key].applicable.includes(dimensionType))
shortcut: keyMap[key].sequences, .map((key: string, id: number) => ({
action: [keyMap[key].action], key: id,
})); name: keyMap[key].name || key,
description: keyMap[key].description || '',
shortcut: keyMap[key].sequences,
action: [keyMap[key].action],
}));
return ( return (
<Modal <Modal

@ -287,6 +287,16 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
NEXT_KEY_FRAME: keyMap.NEXT_KEY_FRAME, NEXT_KEY_FRAME: keyMap.NEXT_KEY_FRAME,
PREV_KEY_FRAME: keyMap.PREV_KEY_FRAME, PREV_KEY_FRAME: keyMap.PREV_KEY_FRAME,
CHANGE_OBJECT_COLOR: keyMap.CHANGE_OBJECT_COLOR, CHANGE_OBJECT_COLOR: keyMap.CHANGE_OBJECT_COLOR,
TILT_UP: keyMap.TILT_UP,
TILT_DOWN: keyMap.TILT_DOWN,
ROTATE_LEFT: keyMap.ROTATE_LEFT,
ROTATE_RIGHT: keyMap.ROTATE_RIGHT,
MOVE_UP: keyMap.MOVE_UP,
MOVE_DOWN: keyMap.MOVE_DOWN,
MOVE_LEFT: keyMap.MOVE_LEFT,
MOVE_RIGHT: keyMap.MOVE_RIGHT,
ZOOM_IN: keyMap.ZOOM_IN,
ZOOM_OUT: keyMap.ZOOM_OUT,
}; };
const preventDefault = (event: KeyboardEvent | undefined): void => { const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -308,6 +318,16 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
}; };
const handlers = { const handlers = {
TILT_UP: () => {}, // Handled by CVAT 3D Independently
TILT_DOWN: () => {},
ROTATE_LEFT: () => {},
ROTATE_RIGHT: () => {},
MOVE_UP: () => {},
MOVE_DOWN: () => {},
MOVE_LEFT: () => {},
MOVE_RIGHT: () => {},
ZOOM_IN: () => {},
ZOOM_OUT: () => {},
SWITCH_ALL_LOCK: (event: KeyboardEvent | undefined) => { SWITCH_ALL_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event); preventDefault(event);
this.lockAllStates(!statesLocked); this.lockAllStates(!statesLocked);

@ -30,7 +30,7 @@ import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-ba
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { import {
CombinedState, FrameSpeed, Workspace, PredictorState, CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
@ -220,48 +220,13 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
} }
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
const { const { autoSaveInterval } = this.props;
jobInstance,
frameSpeed,
frameNumber,
frameDelay,
playing,
canvasIsReady,
canvasInstance,
onSwitchPlay,
onChangeFrame,
autoSaveInterval,
} = this.props;
if (autoSaveInterval !== prevProps.autoSaveInterval) { if (autoSaveInterval !== prevProps.autoSaveInterval) {
if (this.autoSaveInterval) window.clearInterval(this.autoSaveInterval); if (this.autoSaveInterval) window.clearInterval(this.autoSaveInterval);
this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval); this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval);
} }
this.play();
if (playing && canvasIsReady) {
if (frameNumber < jobInstance.stopFrame) {
let framesSkipped = 0;
if (frameSpeed === FrameSpeed.Fast && frameNumber + 1 < jobInstance.stopFrame) {
framesSkipped = 1;
}
if (frameSpeed === FrameSpeed.Fastest && frameNumber + 2 < jobInstance.stopFrame) {
framesSkipped = 2;
}
setTimeout(() => {
const { playing: stillPlaying } = this.props;
if (stillPlaying) {
if (canvasInstance.isAbleToChangeFrame()) {
onChangeFrame(frameNumber + 1 + framesSkipped, stillPlaying, framesSkipped + 1);
} else {
onSwitchPlay(false);
}
}
}, frameDelay);
} else {
onSwitchPlay(false);
}
}
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
@ -473,6 +438,47 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
return undefined; return undefined;
}; };
private play(): void {
const {
jobInstance,
frameSpeed,
frameNumber,
frameDelay,
playing,
canvasIsReady,
canvasInstance,
onSwitchPlay,
onChangeFrame,
} = this.props;
if (playing && canvasIsReady) {
if (frameNumber < jobInstance.stopFrame) {
let framesSkipped = 0;
if (frameSpeed === FrameSpeed.Fast && frameNumber + 1 < jobInstance.stopFrame) {
framesSkipped = 1;
}
if (frameSpeed === FrameSpeed.Fastest && frameNumber + 2 < jobInstance.stopFrame) {
framesSkipped = 2;
}
setTimeout(() => {
const { playing: stillPlaying } = this.props;
if (stillPlaying) {
if (canvasInstance.isAbleToChangeFrame()) {
onChangeFrame(frameNumber + 1 + framesSkipped, stillPlaying, framesSkipped + 1);
} else if (jobInstance.task.dimension === DimensionType.DIM_2D) {
onSwitchPlay(false);
} else {
setTimeout(() => this.play(), frameDelay);
}
}
}, frameDelay);
} else {
onSwitchPlay(false);
}
}
}
private autoSave(): void { private autoSave(): void {
const { autoSave, saving } = this.props; const { autoSave, saving } = this.props;

@ -6,7 +6,7 @@ import { BoundariesActions, BoundariesActionTypes } from 'actions/boundaries-act
import { AuthActions, AuthActionTypes } from 'actions/auth-actions'; import { AuthActions, AuthActionTypes } from 'actions/auth-actions';
import { ShortcutsActions, ShortcutsActionsTypes } from 'actions/shortcuts-actions'; import { ShortcutsActions, ShortcutsActionsTypes } from 'actions/shortcuts-actions';
import { KeyMap, KeyMapItem } from 'utils/mousetrap-react'; import { KeyMap, KeyMapItem } from 'utils/mousetrap-react';
import { ShortcutsState } from './interfaces'; import { DimensionType, ShortcutsState } from './interfaces';
function formatShortcuts(shortcuts: KeyMapItem): string { function formatShortcuts(shortcuts: KeyMapItem): string {
const list: string[] = shortcuts.displayedSequences || (shortcuts.sequences as string[]); const list: string[] = shortcuts.displayedSequences || (shortcuts.sequences as string[]);
@ -27,12 +27,14 @@ const defaultKeyMap = ({
description: 'Open/hide the list of available shortcuts', description: 'Open/hide the list of available shortcuts',
sequences: ['f1'], sequences: ['f1'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_SETTINGS: { SWITCH_SETTINGS: {
name: 'Show settings', name: 'Show settings',
description: 'Open/hide settings dialog', description: 'Open/hide settings dialog',
sequences: ['f2'], sequences: ['f2'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_ALL_LOCK: { SWITCH_ALL_LOCK: {
@ -40,84 +42,98 @@ const defaultKeyMap = ({
description: 'Change locked state for all objects in the side bar', description: 'Change locked state for all objects in the side bar',
sequences: ['t l'], sequences: ['t l'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_LOCK: { SWITCH_LOCK: {
name: 'Lock/unlock an object', name: 'Lock/unlock an object',
description: 'Change locked state for an active object', description: 'Change locked state for an active object',
sequences: ['l'], sequences: ['l'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_ALL_HIDDEN: { SWITCH_ALL_HIDDEN: {
name: 'Hide/show all objects', name: 'Hide/show all objects',
description: 'Change hidden state for objects in the side bar', description: 'Change hidden state for objects in the side bar',
sequences: ['t h'], sequences: ['t h'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_HIDDEN: { SWITCH_HIDDEN: {
name: 'Hide/show an object', name: 'Hide/show an object',
description: 'Change hidden state for an active object', description: 'Change hidden state for an active object',
sequences: ['h'], sequences: ['h'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_OCCLUDED: { SWITCH_OCCLUDED: {
name: 'Switch occluded', name: 'Switch occluded',
description: 'Change occluded property for an active object', description: 'Change occluded property for an active object',
sequences: ['q', '/'], sequences: ['q', '/'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_KEYFRAME: { SWITCH_KEYFRAME: {
name: 'Switch keyframe', name: 'Switch keyframe',
description: 'Change keyframe property for an active track', description: 'Change keyframe property for an active track',
sequences: ['k'], sequences: ['k'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
SWITCH_OUTSIDE: { SWITCH_OUTSIDE: {
name: 'Switch outside', name: 'Switch outside',
description: 'Change outside property for an active track', description: 'Change outside property for an active track',
sequences: ['o'], sequences: ['o'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
DELETE_OBJECT: { DELETE_OBJECT: {
name: 'Delete object', name: 'Delete object',
description: 'Delete an active object. Use shift to force delete of locked objects', description: 'Delete an active object. Use shift to force delete of locked objects',
sequences: ['del', 'shift+del'], sequences: ['del', 'shift+del'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
TO_BACKGROUND: { TO_BACKGROUND: {
name: 'To background', name: 'To background',
description: 'Put an active object "farther" from the user (decrease z axis value)', description: 'Put an active object "farther" from the user (decrease z axis value)',
sequences: ['-', '_'], sequences: ['-', '_'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
TO_FOREGROUND: { TO_FOREGROUND: {
name: 'To foreground', name: 'To foreground',
description: 'Put an active object "closer" to the user (increase z axis value)', description: 'Put an active object "closer" to the user (increase z axis value)',
sequences: ['+', '='], sequences: ['+', '='],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
COPY_SHAPE: { COPY_SHAPE: {
name: 'Copy shape', name: 'Copy shape',
description: 'Copy shape to CVAT internal clipboard', description: 'Copy shape to CVAT internal clipboard',
sequences: ['ctrl+c'], sequences: ['ctrl+c'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
PROPAGATE_OBJECT: { PROPAGATE_OBJECT: {
name: 'Propagate object', name: 'Propagate object',
description: 'Make a copy of the object on the following frames', description: 'Make a copy of the object on the following frames',
sequences: ['ctrl+b'], sequences: ['ctrl+b'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
NEXT_KEY_FRAME: { NEXT_KEY_FRAME: {
name: 'Next keyframe', name: 'Next keyframe',
description: 'Go to the next keyframe of an active track', description: 'Go to the next keyframe of an active track',
sequences: ['r'], sequences: ['r'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
PREV_KEY_FRAME: { PREV_KEY_FRAME: {
name: 'Previous keyframe', name: 'Previous keyframe',
description: 'Go to the previous keyframe of an active track', description: 'Go to the previous keyframe of an active track',
sequences: ['e'], sequences: ['e'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
NEXT_ATTRIBUTE: { NEXT_ATTRIBUTE: {
@ -125,24 +141,28 @@ const defaultKeyMap = ({
description: 'Go to the next attribute', description: 'Go to the next attribute',
sequences: ['down'], sequences: ['down'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
PREVIOUS_ATTRIBUTE: { PREVIOUS_ATTRIBUTE: {
name: 'Previous attribute', name: 'Previous attribute',
description: 'Go to the previous attribute', description: 'Go to the previous attribute',
sequences: ['up'], sequences: ['up'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
NEXT_OBJECT: { NEXT_OBJECT: {
name: 'Next object', name: 'Next object',
description: 'Go to the next object', description: 'Go to the next object',
sequences: ['tab'], sequences: ['tab'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
PREVIOUS_OBJECT: { PREVIOUS_OBJECT: {
name: 'Previous object', name: 'Previous object',
description: 'Go to the previous object', description: 'Go to the previous object',
sequences: ['shift+tab'], sequences: ['shift+tab'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
PASTE_SHAPE: { PASTE_SHAPE: {
@ -150,6 +170,7 @@ const defaultKeyMap = ({
description: 'Paste a shape from internal CVAT clipboard', description: 'Paste a shape from internal CVAT clipboard',
sequences: ['ctrl+v'], sequences: ['ctrl+v'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_DRAW_MODE: { SWITCH_DRAW_MODE: {
name: 'Draw mode', name: 'Draw mode',
@ -157,54 +178,63 @@ const defaultKeyMap = ({
'Repeat the latest procedure of drawing with the same parameters (shift to redraw an existing shape)', 'Repeat the latest procedure of drawing with the same parameters (shift to redraw an existing shape)',
sequences: ['shift+n', 'n'], sequences: ['shift+n', 'n'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
OPEN_REVIEW_ISSUE: { OPEN_REVIEW_ISSUE: {
name: 'Open an issue', name: 'Open an issue',
description: 'Create a new issues in the review workspace', description: 'Create a new issues in the review workspace',
sequences: ['n'], sequences: ['n'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
SWITCH_MERGE_MODE: { SWITCH_MERGE_MODE: {
name: 'Merge mode', name: 'Merge mode',
description: 'Activate or deactivate mode to merging shapes', description: 'Activate or deactivate mode to merging shapes',
sequences: ['m'], sequences: ['m'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
SWITCH_SPLIT_MODE: { SWITCH_SPLIT_MODE: {
name: 'Split mode', name: 'Split mode',
description: 'Activate or deactivate mode to splitting shapes', description: 'Activate or deactivate mode to splitting shapes',
sequences: ['alt+m'], sequences: ['alt+m'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
SWITCH_GROUP_MODE: { SWITCH_GROUP_MODE: {
name: 'Group mode', name: 'Group mode',
description: 'Activate or deactivate mode to grouping shapes', description: 'Activate or deactivate mode to grouping shapes',
sequences: ['g'], sequences: ['g'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
RESET_GROUP: { RESET_GROUP: {
name: 'Reset group', name: 'Reset group',
description: 'Reset group for selected shapes (in group mode)', description: 'Reset group for selected shapes (in group mode)',
sequences: ['shift+g'], sequences: ['shift+g'],
action: 'keyup', action: 'keyup',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
CANCEL: { CANCEL: {
name: 'Cancel', name: 'Cancel',
description: 'Cancel any active canvas mode', description: 'Cancel any active canvas mode',
sequences: ['esc'], sequences: ['esc'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
CLOCKWISE_ROTATION: { CLOCKWISE_ROTATION: {
name: 'Rotate clockwise', name: 'Rotate clockwise',
description: 'Change image angle (add 90 degrees)', description: 'Change image angle (add 90 degrees)',
sequences: ['ctrl+r'], sequences: ['ctrl+r'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
ANTICLOCKWISE_ROTATION: { ANTICLOCKWISE_ROTATION: {
name: 'Rotate anticlockwise', name: 'Rotate anticlockwise',
description: 'Change image angle (subtract 90 degrees)', description: 'Change image angle (subtract 90 degrees)',
sequences: ['ctrl+shift+r'], sequences: ['ctrl+shift+r'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
SAVE_JOB: { SAVE_JOB: {
@ -212,60 +242,70 @@ const defaultKeyMap = ({
description: 'Send all changes of annotations to the server', description: 'Send all changes of annotations to the server',
sequences: ['ctrl+s'], sequences: ['ctrl+s'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
UNDO: { UNDO: {
name: 'Undo action', name: 'Undo action',
description: 'Cancel the latest action related with objects', description: 'Cancel the latest action related with objects',
sequences: ['ctrl+z'], sequences: ['ctrl+z'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
REDO: { REDO: {
name: 'Redo action', name: 'Redo action',
description: 'Cancel undo action', description: 'Cancel undo action',
sequences: ['ctrl+shift+z', 'ctrl+y'], sequences: ['ctrl+shift+z', 'ctrl+y'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
NEXT_FRAME: { NEXT_FRAME: {
name: 'Next frame', name: 'Next frame',
description: 'Go to the next frame', description: 'Go to the next frame',
sequences: ['f'], sequences: ['f'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
PREV_FRAME: { PREV_FRAME: {
name: 'Previous frame', name: 'Previous frame',
description: 'Go to the previous frame', description: 'Go to the previous frame',
sequences: ['d'], sequences: ['d'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
FORWARD_FRAME: { FORWARD_FRAME: {
name: 'Forward frame', name: 'Forward frame',
description: 'Go forward with a step', description: 'Go forward with a step',
sequences: ['v'], sequences: ['v'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
BACKWARD_FRAME: { BACKWARD_FRAME: {
name: 'Backward frame', name: 'Backward frame',
description: 'Go backward with a step', description: 'Go backward with a step',
sequences: ['c'], sequences: ['c'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SEARCH_FORWARD: { SEARCH_FORWARD: {
name: 'Search forward', name: 'Search forward',
description: 'Search the next frame that satisfies to the filters', description: 'Search the next frame that satisfies to the filters',
sequences: ['right'], sequences: ['right'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SEARCH_BACKWARD: { SEARCH_BACKWARD: {
name: 'Search backward', name: 'Search backward',
description: 'Search the previous frame that satisfies to the filters', description: 'Search the previous frame that satisfies to the filters',
sequences: ['left'], sequences: ['left'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
PLAY_PAUSE: { PLAY_PAUSE: {
name: 'Play/pause', name: 'Play/pause',
description: 'Start/stop automatic changing frames', description: 'Start/stop automatic changing frames',
sequences: ['space'], sequences: ['space'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
FOCUS_INPUT_FRAME: { FOCUS_INPUT_FRAME: {
name: 'Focus input frame', name: 'Focus input frame',
@ -273,30 +313,105 @@ const defaultKeyMap = ({
sequences: ['`'], sequences: ['`'],
displayedSequences: ['~'], displayedSequences: ['~'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
SWITCH_AUTOMATIC_BORDERING: { SWITCH_AUTOMATIC_BORDERING: {
name: 'Switch automatic bordering', name: 'Switch automatic bordering',
description: 'Switch automatic bordering for polygons and polylines during drawing/editing', description: 'Switch automatic bordering for polygons and polylines during drawing/editing',
sequences: ['ctrl'], sequences: ['ctrl'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
CHANGE_OBJECT_COLOR: { CHANGE_OBJECT_COLOR: {
name: 'Change color', name: 'Change color',
description: 'Set the next color for an activated shape', description: 'Set the next color for an activated shape',
sequences: ['enter'], sequences: ['enter'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
}, },
TOGGLE_LAYOUT_GRID: { TOGGLE_LAYOUT_GRID: {
name: 'Toggle layout grid', name: 'Toggle layout grid',
description: 'The grid is used to UI development', description: 'The grid is used to UI development',
sequences: ['ctrl+alt+enter'], sequences: ['ctrl+alt+enter'],
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D],
}, },
SWITCH_LABEL: { SWITCH_LABEL: {
name: 'Switch label', name: 'Switch label',
description: 'Changes a label for an activated object or for the next drawn object if no objects are activated', description: 'Changes a label for an activated object or for the next drawn object if no objects are activated',
sequences: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'].map((val: string): string => `ctrl+${val}`), sequences: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'].map((val: string): string => `ctrl+${val}`),
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
},
TILT_UP: {
name: 'Camera Roll Angle Up',
description: 'Increases camera roll angle',
sequences: ['shift+arrowup'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
TILT_DOWN: {
name: 'Camera Roll Angle Down',
description: 'Decreases camera roll angle',
sequences: ['shift+arrowdown'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
ROTATE_LEFT: {
name: 'Camera Pitch Angle Left',
description: 'Decreases camera pitch angle',
sequences: ['shift+arrowleft'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
ROTATE_RIGHT: {
name: 'Camera Pitch Angle Right',
description: 'Increases camera pitch angle',
sequences: ['shift+arrowright'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
MOVE_UP: {
name: 'Camera Move Up',
description: 'Move the camera up',
sequences: ['alt+u'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
MOVE_DOWN: {
name: 'Camera Move Down',
description: 'Move the camera down',
sequences: ['alt+o'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
MOVE_LEFT: {
name: 'Camera Move Left',
description: 'Move the camera left',
sequences: ['alt+j'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
MOVE_RIGHT: {
name: 'Camera Move Right',
description: 'Move the camera right',
sequences: ['alt+l'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
ZOOM_IN: {
name: 'Camera Zoom In',
description: 'Performs zoom in',
sequences: ['alt+i'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
},
ZOOM_OUT: {
name: 'Camera Zoom Out',
description: 'Performs zoom out',
sequences: ['alt+k'],
action: 'keydown',
applicable: [DimensionType.DIM_3D],
}, },
} as any) as KeyMap; } as any) as KeyMap;

@ -11,6 +11,7 @@ export interface KeyMapItem {
sequences: string[]; sequences: string[];
displayedSequences?: string[]; displayedSequences?: string[];
action: 'keydown' | 'keyup' | 'keypress'; action: 'keydown' | 'keyup' | 'keypress';
applicable: any[];
} }
export interface KeyMap { export interface KeyMap {

@ -11,7 +11,7 @@ from django.utils import timezone
import datumaro.components.extractor as datumaro import datumaro.components.extractor as datumaro
from cvat.apps.engine.frame_provider import FrameProvider from cvat.apps.engine.frame_provider import FrameProvider
from cvat.apps.engine.models import AttributeType, ShapeType from cvat.apps.engine.models import AttributeType, ShapeType, DimensionType, Image as Img
from datumaro.util import cast from datumaro.util import cast
from datumaro.util.image import ByteImage, Image from datumaro.util.image import ByteImage, Image
@ -20,6 +20,7 @@ from .annotation import AnnotationManager, TrackManager
class TaskData: class TaskData:
Attribute = namedtuple('Attribute', 'name, value') Attribute = namedtuple('Attribute', 'name, value')
Shape = namedtuple("Shape", 'id, label_id') # 3d
LabeledShape = namedtuple( LabeledShape = namedtuple(
'LabeledShape', 'type, frame, label, points, occluded, attributes, source, group, z_order') 'LabeledShape', 'type, frame, label, points, occluded, attributes, source, group, z_order')
LabeledShape.__new__.__defaults__ = (0, 0) LabeledShape.__new__.__defaults__ = (0, 0)
@ -30,7 +31,8 @@ class TaskData:
Tag = namedtuple('Tag', 'frame, label, attributes, source, group') Tag = namedtuple('Tag', 'frame, label, attributes, source, group')
Tag.__new__.__defaults__ = (0, ) Tag.__new__.__defaults__ = (0, )
Frame = namedtuple( Frame = namedtuple(
'Frame', 'idx, frame, name, width, height, labeled_shapes, tags') 'Frame', 'idx, id, frame, name, width, height, labeled_shapes, tags, shapes, labels')
Labels = namedtuple('Label', 'id, name, color')
def __init__(self, annotation_ir, db_task, host='', create_callback=None): def __init__(self, annotation_ir, db_task, host='', create_callback=None):
self._annotation_ir = annotation_ir self._annotation_ir = annotation_ir
@ -122,6 +124,7 @@ class TaskData:
} for frame in range(self._db_task.data.size)} } for frame in range(self._db_task.data.size)}
else: else:
self._frame_info = {self.rel_frame_id(db_image.frame): { self._frame_info = {self.rel_frame_id(db_image.frame): {
"id": db_image.id,
"path": db_image.path, "path": db_image.path,
"width": db_image.width, "width": db_image.width,
"height": db_image.height, "height": db_image.height,
@ -234,6 +237,12 @@ class TaskData:
attributes=self._export_attributes(shape["attributes"]), attributes=self._export_attributes(shape["attributes"]),
) )
def _export_shape(self, shape):
return TaskData.Shape(
id=shape["id"],
label_id=shape["label_id"]
)
def _export_tag(self, tag): def _export_tag(self, tag):
return TaskData.Tag( return TaskData.Tag(
frame=self.abs_frame_id(tag["frame"]), frame=self.abs_frame_id(tag["frame"]),
@ -243,6 +252,14 @@ class TaskData:
attributes=self._export_attributes(tag["attributes"]), attributes=self._export_attributes(tag["attributes"]),
) )
@staticmethod
def _export_label(label):
return TaskData.Labels(
id=label.id,
name=label.name,
color=label.color
)
def group_by_frame(self, include_empty=False): def group_by_frame(self, include_empty=False):
frames = {} frames = {}
def get_frame(idx): def get_frame(idx):
@ -251,12 +268,15 @@ class TaskData:
if frame not in frames: if frame not in frames:
frames[frame] = TaskData.Frame( frames[frame] = TaskData.Frame(
idx=idx, idx=idx,
id=frame_info.get('id',0),
frame=frame, frame=frame,
name=frame_info['path'], name=frame_info['path'],
height=frame_info["height"], height=frame_info["height"],
width=frame_info["width"], width=frame_info["width"],
labeled_shapes=[], labeled_shapes=[],
tags=[], tags=[],
shapes=[],
labels={}
) )
return frames[frame] return frames[frame]
@ -265,6 +285,7 @@ class TaskData:
get_frame(idx) get_frame(idx)
anno_manager = AnnotationManager(self._annotation_ir) anno_manager = AnnotationManager(self._annotation_ir)
shape_data = ''
for shape in sorted(anno_manager.to_shapes(self._db_task.data.size), for shape in sorted(anno_manager.to_shapes(self._db_task.data.size),
key=lambda shape: shape.get("z_order", 0)): key=lambda shape: shape.get("z_order", 0)):
if shape['frame'] not in self._frame_info: if shape['frame'] not in self._frame_info:
@ -278,8 +299,13 @@ class TaskData:
exported_shape = self._export_tracked_shape(shape) exported_shape = self._export_tracked_shape(shape)
else: else:
exported_shape = self._export_labeled_shape(shape) exported_shape = self._export_labeled_shape(shape)
get_frame(shape['frame']).labeled_shapes.append( shape_data = self._export_shape(shape)
exported_shape) get_frame(shape['frame']).labeled_shapes.append(exported_shape)
if shape_data:
get_frame(shape['frame']).shapes.append(shape_data)
for label in self._label_mapping.values():
label = self._export_label(label)
get_frame(shape['frame']).labels.update({label.id: label})
for tag in self._annotation_ir.tags: for tag in self._annotation_ir.tags:
get_frame(tag['frame']).tags.append(self._export_tag(tag)) get_frame(tag['frame']).tags.append(self._export_tag(tag))
@ -457,17 +483,31 @@ class TaskData:
return None return None
class CvatTaskDataExtractor(datumaro.SourceExtractor): class CvatTaskDataExtractor(datumaro.SourceExtractor):
def __init__(self, task_data, include_images=False): def __init__(self, task_data, include_images=False, format_type=None, dimension=DimensionType.DIM_2D):
super().__init__() super().__init__()
self._categories = self._load_categories(task_data) self._categories, self._user = self._load_categories(task_data, dimension=dimension)
self._dimension = dimension
self._format_type = format_type
dm_items = [] dm_items = []
is_video = task_data.meta['task']['mode'] == 'interpolation' is_video = task_data.meta['task']['mode'] == 'interpolation'
ext = '' ext = ''
if is_video: if is_video:
ext = FrameProvider.VIDEO_FRAME_EXT ext = FrameProvider.VIDEO_FRAME_EXT
if include_images:
if dimension == DimensionType.DIM_3D:
def _make_image(image_id, **kwargs):
loader = osp.join(
task_data.db_task.data.get_upload_dirname(), kwargs['path'])
related_images = []
image = Img.objects.get(id=image_id)
for i in image.related_files.all():
path = osp.realpath(str(i.path))
if osp.isfile(path):
related_images.append(path)
return loader, related_images
elif include_images:
frame_provider = FrameProvider(task_data.db_task.data) frame_provider = FrameProvider(task_data.db_task.data)
if is_video: if is_video:
# optimization for videos: use numpy arrays instead of bytes # optimization for videos: use numpy arrays instead of bytes
@ -490,14 +530,36 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
'path': frame_data.name + ext, 'path': frame_data.name + ext,
'size': (frame_data.height, frame_data.width), 'size': (frame_data.height, frame_data.width),
} }
if include_images:
if dimension == DimensionType.DIM_3D:
dm_image = _make_image(frame_data.id, **image_args)
elif include_images:
dm_image = _make_image(frame_data.idx, **image_args) dm_image = _make_image(frame_data.idx, **image_args)
else: else:
dm_image = Image(**image_args) dm_image = Image(**image_args)
dm_anno = self._read_cvat_anno(frame_data, task_data) dm_anno = self._read_cvat_anno(frame_data, task_data)
dm_item = datumaro.DatasetItem(id=osp.splitext(frame_data.name)[0],
annotations=dm_anno, image=dm_image, if dimension == DimensionType.DIM_2D:
attributes={'frame': frame_data.frame}) dm_item = datumaro.DatasetItem(id=osp.splitext(frame_data.name)[0],
annotations=dm_anno, image=dm_image,
attributes={'frame': frame_data.frame})
elif dimension == DimensionType.DIM_3D:
attributes = {'frame': frame_data.frame}
if format_type == "sly_pointcloud":
attributes["name"] = self._user["name"]
attributes["createdAt"] = self._user["createdAt"]
attributes["updatedAt"] = self._user["updatedAt"]
attributes["labels"] = []
index = 0
for _, label in task_data.meta['task']['labels']:
attributes["labels"].append({"label_id": index, "name": label["name"], "color": label["color"]})
attributes["track_id"] = -1
index += 1
dm_item = datumaro.DatasetItem(id=osp.split(frame_data.name)[-1].split('.')[0],
annotations=dm_anno, point_cloud=dm_image[0], related_images=dm_image[1],
attributes=attributes)
dm_items.append(dm_item) dm_items.append(dm_item)
self._items = dm_items self._items = dm_items
@ -513,19 +575,25 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
return self._categories return self._categories
@staticmethod @staticmethod
def _load_categories(cvat_anno): def _load_categories(cvat_anno, dimension):
categories = {} categories = {}
label_categories = datumaro.LabelCategories(attributes=['occluded']) label_categories = datumaro.LabelCategories(attributes=['occluded'])
user_info = {}
if dimension == DimensionType.DIM_3D:
user_info = {"name": cvat_anno.meta['task']['owner']['username'],
"createdAt": cvat_anno.meta['task']['created'],
"updatedAt": cvat_anno.meta['task']['updated']}
for _, label in cvat_anno.meta['task']['labels']: for _, label in cvat_anno.meta['task']['labels']:
label_categories.add(label['name']) label_categories.add(label['name'])
for _, attr in label['attributes']: for _, attr in label['attributes']:
label_categories.attributes.add(attr['name']) label_categories.attributes.add(attr['name'])
categories[datumaro.AnnotationType.label] = label_categories categories[datumaro.AnnotationType.label] = label_categories
return categories return categories, user_info
def _read_cvat_anno(self, cvat_frame_anno, task_data): def _read_cvat_anno(self, cvat_frame_anno, task_data):
item_anno = [] item_anno = []
@ -554,6 +622,9 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
raise Exception( raise Exception(
"Failed to convert attribute '%s'='%s': %s" % "Failed to convert attribute '%s'='%s': %s" %
(a_name, a_value, e)) (a_name, a_value, e))
if self._format_type == "sly_pointcloud" and (a_desc.get('input_type') == 'select' or a_desc.get('input_type') == 'radio'):
dm_attr[f"{a_name}__values"] = a_desc["values"]
return dm_attr return dm_attr
for tag_obj in cvat_frame_anno.tags: for tag_obj in cvat_frame_anno.tags:
@ -565,7 +636,11 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
attributes=anno_attr, group=anno_group) attributes=anno_attr, group=anno_group)
item_anno.append(anno) item_anno.append(anno)
for shape_obj in cvat_frame_anno.labeled_shapes: shapes = []
for shape in cvat_frame_anno.shapes:
shapes.append({"id": shape.id, "label_id": shape.label_id})
for index, shape_obj in enumerate(cvat_frame_anno.labeled_shapes):
anno_group = shape_obj.group or 0 anno_group = shape_obj.group or 0
anno_label = map_label(shape_obj.label) anno_label = map_label(shape_obj.label)
anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes) anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes)
@ -594,7 +669,18 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
label=anno_label, attributes=anno_attr, group=anno_group, label=anno_label, attributes=anno_attr, group=anno_group,
z_order=shape_obj.z_order) z_order=shape_obj.z_order)
elif shape_obj.type == ShapeType.CUBOID: elif shape_obj.type == ShapeType.CUBOID:
continue # Datumaro does not support cuboids if self._dimension == DimensionType.DIM_3D:
if self._format_type == "sly_pointcloud":
anno_id = shapes[index]["id"]
anno_attr["label_id"] = shapes[index]["label_id"]
else:
anno_id = index
position, rotation, scale = anno_points[0:3], anno_points[3:6], anno_points[6:9]
anno = datumaro.Cuboid3d(id=anno_id, position=position, rotation=rotation, scale=scale,
label=anno_label, attributes=anno_attr, group=anno_group
)
else:
continue
else: else:
raise Exception("Unknown shape type '%s'" % shape_obj.type) raise Exception("Unknown shape type '%s'" % shape_obj.type)
@ -645,6 +731,7 @@ def import_dm_annotations(dm_dataset, task_data):
datumaro.AnnotationType.polygon: ShapeType.POLYGON, datumaro.AnnotationType.polygon: ShapeType.POLYGON,
datumaro.AnnotationType.polyline: ShapeType.POLYLINE, datumaro.AnnotationType.polyline: ShapeType.POLYLINE,
datumaro.AnnotationType.points: ShapeType.POINTS, datumaro.AnnotationType.points: ShapeType.POINTS,
datumaro.AnnotationType.cuboid_3d: ShapeType.CUBOID
} }
if len(dm_dataset) == 0: if len(dm_dataset) == 0:
@ -679,11 +766,17 @@ def import_dm_annotations(dm_dataset, task_data):
if hasattr(ann, 'label') and ann.label is None: if hasattr(ann, 'label') and ann.label is None:
raise CvatImportError("annotation has no label") raise CvatImportError("annotation has no label")
if ann.type in shapes: if ann.type in shapes:
if ann.type == datumaro.AnnotationType.cuboid_3d:
try:
ann.points = [*ann.position,*ann.rotation,*ann.scale,0,0,0,0,0,0,0]
except Exception as e:
ann.points = ann.points
ann.z_order = 0
task_data.add_shape(task_data.LabeledShape( task_data.add_shape(task_data.LabeledShape(
type=shapes[ann.type], type=shapes[ann.type],
frame=frame_number, frame=frame_number,
points = ann.points,
label=label_cat.items[ann.label].name, label=label_cat.items[ann.label].name,
points=ann.points,
occluded=ann.attributes.get('occluded') == True, occluded=ann.attributes.get('occluded') == True,
z_order=ann.z_order, z_order=ann.z_order,
group=group_map.get(ann.group, 0), group=group_map.get(ann.group, 0),

@ -0,0 +1,42 @@
# Copyright (C) 2021 Intel Corporation
#
# SPDX-License-Identifier: MIT
import zipfile
from tempfile import TemporaryDirectory
from datumaro.components.dataset import Dataset
from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor,
import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive
from cvat.apps.engine.models import DimensionType
from .registry import dm_env, exporter, importer
@exporter(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _export_images(dst_file, task_data, save_images=False):
dataset = Dataset.from_extractors(CvatTaskDataExtractor(
task_data, include_images=save_images, format_type='sly_pointcloud', dimension=DimensionType.DIM_3D), env=dm_env)
with TemporaryDirectory() as temp_dir:
dataset.export(temp_dir, 'sly_pointcloud', save_images=save_images)
make_zip_archive(temp_dir, dst_file)
@importer(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _import(src_file, task_data):
if zipfile.is_zipfile(src_file):
with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir)
dataset = Dataset.import_from(tmp_dir, 'sly_pointcloud', env=dm_env)
import_dm_annotations(dataset, task_data)
else:
dataset = Dataset.import_from(src_file.name,
'sly_pointcloud', env=dm_env)
import_dm_annotations(dataset, task_data)

@ -107,3 +107,6 @@ import cvat.apps.dataset_manager.formats.widerface
import cvat.apps.dataset_manager.formats.vggface2 import cvat.apps.dataset_manager.formats.vggface2
import cvat.apps.dataset_manager.formats.market1501 import cvat.apps.dataset_manager.formats.market1501
import cvat.apps.dataset_manager.formats.icdar import cvat.apps.dataset_manager.formats.icdar
import cvat.apps.dataset_manager.formats.velodynepoint
import cvat.apps.dataset_manager.formats.pointcloud

@ -0,0 +1,45 @@
# Copyright (C) 2021 Intel Corporation
#
# SPDX-License-Identifier: MIT
import zipfile
from tempfile import TemporaryDirectory
from datumaro.components.dataset import Dataset
from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, \
import_dm_annotations
from .registry import dm_env
from cvat.apps.dataset_manager.util import make_zip_archive
from cvat.apps.engine.models import DimensionType
from .registry import exporter, importer
@exporter(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _export_images(dst_file, task_data, save_images=False):
dataset = Dataset.from_extractors(CvatTaskDataExtractor(
task_data, include_images=save_images, format_type="kitti_raw", dimension=DimensionType.DIM_3D), env=dm_env)
with TemporaryDirectory() as temp_dir:
dataset.export(temp_dir, 'kitti_raw', save_images=save_images, reindex=True)
make_zip_archive(temp_dir, dst_file)
@importer(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _import(src_file, task_data):
if zipfile.is_zipfile(src_file):
with TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(src_file).extractall(tmp_dir)
dataset = Dataset.import_from(
tmp_dir, 'kitti_raw', env=dm_env)
import_dm_annotations(dataset, task_data)
else:
dataset = Dataset.import_from(
src_file.name, 'kitti_raw', env=dm_env)
import_dm_annotations(dataset, task_data)

@ -288,6 +288,9 @@ class TaskExportTest(_DbTestBase):
'ICDAR Recognition 1.0', 'ICDAR Recognition 1.0',
'ICDAR Localization 1.0', 'ICDAR Localization 1.0',
'ICDAR Segmentation 1.0', 'ICDAR Segmentation 1.0',
'Kitti Raw Format 1.0',
'Sly Point Cloud Format 1.0'
}) })
def test_import_formats_query(self): def test_import_formats_query(self):
@ -312,6 +315,8 @@ class TaskExportTest(_DbTestBase):
'ICDAR Recognition 1.0', 'ICDAR Recognition 1.0',
'ICDAR Localization 1.0', 'ICDAR Localization 1.0',
'ICDAR Segmentation 1.0', 'ICDAR Segmentation 1.0',
'Kitti Raw Format 1.0',
'Sly Point Cloud Format 1.0'
}) })
def test_exports(self): def test_exports(self):

@ -323,7 +323,10 @@ class TaskDumpUploadTest(_DbTestBase):
with TestDir() as test_dir: with TestDir() as test_dir:
# Dump annotations with objects type is shape # Dump annotations with objects type is shape
for dump_format in dump_formats: for dump_format in dump_formats:
if not dump_format.ENABLED: if not dump_format.ENABLED or dump_format.DISPLAY_NAME in [
'Kitti Raw Format 1.0', 'Sly Point Cloud Format 1.0'
]:
continue continue
dump_format_name = dump_format.DISPLAY_NAME dump_format_name = dump_format.DISPLAY_NAME
with self.subTest(format=dump_format_name): with self.subTest(format=dump_format_name):
@ -425,7 +428,10 @@ class TaskDumpUploadTest(_DbTestBase):
with TestDir() as test_dir: with TestDir() as test_dir:
# Dump annotations with objects type is track # Dump annotations with objects type is track
for dump_format in dump_formats: for dump_format in dump_formats:
if not dump_format.ENABLED: if not dump_format.ENABLED or dump_format.DISPLAY_NAME in [
'Kitti Raw Format 1.0','Sly Point Cloud Format 1.0'
]:
continue continue
dump_format_name = dump_format.DISPLAY_NAME dump_format_name = dump_format.DISPLAY_NAME
with self.subTest(format=dump_format_name): with self.subTest(format=dump_format_name):
@ -868,7 +874,10 @@ class TaskDumpUploadTest(_DbTestBase):
with self.subTest(format=dump_format_name): with self.subTest(format=dump_format_name):
if dump_format_name in [ if dump_format_name in [
"MOTS PNG 1.0", # issue #2925 and changed points values "MOTS PNG 1.0", # issue #2925 and changed points values
"Datumaro 1.0" # Datumaro 1.0 is not in the list of import format "Datumaro 1.0", # Datumaro 1.0 is not in the list of import format
'Kitti Raw Format 1.0',
'Sly Point Cloud Format 1.0'
]: ]:
self.skipTest("Format is fail") self.skipTest("Format is fail")
images = self._generate_task_images(3) images = self._generate_task_images(3)
@ -975,6 +984,8 @@ class TaskDumpUploadTest(_DbTestBase):
"MOTS PNG 1.0", # changed points values "MOTS PNG 1.0", # changed points values
"Segmentation mask 1.1", # changed points values "Segmentation mask 1.1", # changed points values
"ICDAR Segmentation 1.0", # changed points values "ICDAR Segmentation 1.0", # changed points values
'Kitti Raw Format 1.0',
'Sly Point Cloud Format 1.0'
]: ]:
self.skipTest("Format is fail") self.skipTest("Format is fail")

@ -3370,6 +3370,7 @@ class JobAnnotationAPITestCase(APITestCase):
create_db_users(cls) create_db_users(cls)
def _create_task(self, owner, assignee, annotation_format=""): def _create_task(self, owner, assignee, annotation_format=""):
dimension = DimensionType.DIM_2D
data = { data = {
"name": "my task #1", "name": "my task #1",
"owner_id": owner.id, "owner_id": owner.id,
@ -3461,6 +3462,12 @@ class JobAnnotationAPITestCase(APITestCase):
}, },
] ]
}] }]
elif annotation_format in ['Kitti Raw Format 1.0', 'Sly Point Cloud Format 1.0']:
data["labels"] = [{
"name": "car"},
{"name": "bus"}
]
dimension = DimensionType.DIM_3D
elif annotation_format == "ICDAR Segmentation 1.0": elif annotation_format == "ICDAR Segmentation 1.0":
data["labels"] = [{ data["labels"] = [{
"name": "icdar", "name": "icdar",
@ -3510,6 +3517,15 @@ class JobAnnotationAPITestCase(APITestCase):
"image_quality": 75, "image_quality": 75,
"frame_filter": "step=3", "frame_filter": "step=3",
} }
if dimension == DimensionType.DIM_3D:
images = {
"client_files[0]": open(
os.path.join(os.path.dirname(__file__), 'assets', 'test_pointcloud_pcd.zip'
if annotation_format == 'Sly Point Cloud Format 1.0' else 'test_velodyne_points.zip'),
'rb'),
"image_quality": 100,
}
response = self.client.post("/api/v1/tasks/{}/data".format(tid), data=images) response = self.client.post("/api/v1/tasks/{}/data".format(tid), data=images)
assert response.status_code == status.HTTP_202_ACCEPTED assert response.status_code == status.HTTP_202_ACCEPTED
@ -4471,7 +4487,8 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
def _get_initial_annotation(annotation_format): def _get_initial_annotation(annotation_format):
if annotation_format not in ["Market-1501 1.0", "ICDAR Recognition 1.0", if annotation_format not in ["Market-1501 1.0", "ICDAR Recognition 1.0",
"ICDAR Localization 1.0", "ICDAR Segmentation 1.0"]: "ICDAR Localization 1.0", "ICDAR Segmentation 1.0",
'Kitti Raw Format 1.0', 'Sly Point Cloud Format 1.0']:
rectangle_tracks_with_attrs = [{ rectangle_tracks_with_attrs = [{
"frame": 0, "frame": 0,
"label_id": task["labels"][0]["id"], "label_id": task["labels"][0]["id"],
@ -4815,7 +4832,32 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
], ],
}] }]
annotations["tags"] = tags_with_attrs annotations["tags"] = tags_with_attrs
elif annotation_format in ['Kitti Raw Format 1.0','Sly Point Cloud Format 1.0']:
velodyne_wo_attrs = [{
"frame": 0,
"label_id": task["labels"][0]["id"],
"group": 0,
"source": "manual",
"attributes": [
],
"points": [-3.62, 7.95, -1.03, 0.0, 0.0, 0.0, 1.0, 1.0,
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"type": "cuboid_3d",
"occluded": False,
},
{
"frame": 0,
"label_id": task["labels"][0]["id"],
"group": 0,
"source": "manual",
"attributes": [],
"points": [23.01, 8.34, -0.76, 0.0, 0.0, 0.0, 1.0, 1.0,
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
"type": "cuboid_3d",
"occluded": False,
}
]
annotations["shapes"] = velodyne_wo_attrs
elif annotation_format == "ICDAR Recognition 1.0": elif annotation_format == "ICDAR Recognition 1.0":
tags_with_attrs = [{ tags_with_attrs = [{
"frame": 1, "frame": 1,
@ -5033,6 +5075,7 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
data["version"] += 2 # upload is delete + put data["version"] += 2 # upload is delete + put
self._check_response(response, data) self._check_response(response, data)
break
def _check_dump_content(self, content, task, jobs, data, format_name): def _check_dump_content(self, content, task, jobs, data, format_name):
def etree_to_dict(t): def etree_to_dict(t):
d = {t.tag: {} if t.attrib else None} d = {t.tag: {} if t.attrib else None}
@ -5068,6 +5111,8 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
self.assertTrue(zipfile.is_zipfile(content)) self.assertTrue(zipfile.is_zipfile(content))
elif format_name == "YOLO 1.1": elif format_name == "YOLO 1.1":
self.assertTrue(zipfile.is_zipfile(content)) self.assertTrue(zipfile.is_zipfile(content))
elif format_name in ['Kitti Raw Format 1.0','Sly Point Cloud Format 1.0']:
self.assertTrue(zipfile.is_zipfile(content))
elif format_name == "COCO 1.0": elif format_name == "COCO 1.0":
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(content).extractall(tmp_dir) zipfile.ZipFile(content).extractall(tmp_dir)

@ -0,0 +1,750 @@
# Copyright (C) 2020 Intel Corporation
#
# SPDX-License-Identifier: MIT
import io
import os
import os.path as osp
import tempfile
import xml.etree.ElementTree as ET
import zipfile
from collections import defaultdict
from glob import glob
from io import BytesIO
import copy
from shutil import copyfile
import itertools
from django.contrib.auth.models import Group, User
from rest_framework import status
from rest_framework.test import APIClient, APITestCase
from cvat.apps.engine.media_extractors import ValidateDimension
from cvat.apps.dataset_manager.task import TaskAnnotation
from datumaro.util.test_utils import TestDir
CREATE_ACTION = "create"
UPDATE_ACTION = "update"
DELETE_ACTION = "delete"
class ForceLogin:
def __init__(self, user, client):
self.user = user
self.client = client
def __enter__(self):
if self.user:
self.client.force_login(self.user,
backend='django.contrib.auth.backends.ModelBackend')
return self
def __exit__(self, exception_type, exception_value, traceback):
if self.user:
self.client.logout()
class _DbTestBase(APITestCase):
def setUp(self):
self.client = APIClient()
@classmethod
def setUpTestData(cls):
cls.create_db_users()
@classmethod
def create_db_users(cls):
(group_admin, _) = Group.objects.get_or_create(name="admin")
(group_user, _) = Group.objects.get_or_create(name="user")
user_admin = User.objects.create_superuser(username="admin", email="",
password="admin")
user_admin.groups.add(group_admin)
user_dummy = User.objects.create_user(username="user", password="user")
user_dummy.groups.add(group_user)
cls.admin = user_admin
cls.user = user_dummy
def _put_api_v1_task_id_annotations(self, tid, data):
with ForceLogin(self.admin, self.client):
response = self.client.put("/api/v1/tasks/%s/annotations" % tid,
data=data, format="json")
return response
def _put_api_v1_job_id_annotations(self, jid, data):
with ForceLogin(self.admin, self.client):
response = self.client.put("/api/v1/jobs/%s/annotations" % jid,
data=data, format="json")
return response
def _patch_api_v1_task_id_annotations(self, tid, data, action, user):
with ForceLogin(user, self.client):
response = self.client.patch(
"/api/v1/tasks/{}/annotations?action={}".format(tid, action),
data=data, format="json")
return response
def _patch_api_v1_job_id_annotations(self, jid, data, action, user):
with ForceLogin(user, self.client):
response = self.client.patch(
"/api/v1/jobs/{}/annotations?action={}".format(jid, action),
data=data, format="json")
return response
def _create_task(self, data, image_data):
with ForceLogin(self.user, self.client):
response = self.client.post('/api/v1/tasks', data=data, format="json")
assert response.status_code == status.HTTP_201_CREATED, response.status_code
tid = response.data["id"]
response = self.client.post("/api/v1/tasks/%s/data" % tid,
data=image_data)
assert response.status_code == status.HTTP_202_ACCEPTED, response.status_code
response = self.client.get("/api/v1/tasks/%s" % tid)
task = response.data
return task
@staticmethod
def _get_tmp_annotation(task, annotation):
tmp_annotations = copy.deepcopy(annotation)
for item in tmp_annotations:
if item in ["tags", "shapes", "tracks"]:
for index_elem, _ in enumerate(tmp_annotations[item]):
tmp_annotations[item][index_elem]["label_id"] = task["labels"][0]["id"]
for index_attribute, attribute in enumerate(task["labels"][0]["attributes"]):
spec_id = task["labels"][0]["attributes"][index_attribute]["id"]
value = attribute["default_value"]
if item == "tracks" and attribute["mutable"]:
for index_shape, _ in enumerate(tmp_annotations[item][index_elem]["shapes"]):
tmp_annotations[item][index_elem]["shapes"][index_shape]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
else:
tmp_annotations[item][index_elem]["attributes"].append({
"spec_id": spec_id,
"value": value,
})
return tmp_annotations
def _get_jobs(self, task_id):
with ForceLogin(self.admin, self.client):
response = self.client.get("/api/v1/tasks/{}/jobs".format(task_id))
return response.data
def _get_request(self, path, user):
with ForceLogin(user, self.client):
response = self.client.get(path)
return response
def _get_request_with_data(self, path, data, user):
with ForceLogin(user, self.client):
response = self.client.get(path, data)
return response
def _delete_request(self, path, user):
with ForceLogin(user, self.client):
response = self.client.delete(path)
return response
def _download_file(self, url, data, user, file_name):
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, status.HTTP_200_OK)
content = BytesIO(b"".join(response.streaming_content))
with open(file_name, "wb") as f:
f.write(content.getvalue())
def _upload_file(self, url, data, user):
response = self._put_request_with_data(url, {"annotation_file": data}, user)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
response = self._put_request_with_data(url, {}, user)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def _generate_url_dump_tasks_annotations(self, task_id):
return f"/api/v1/tasks/{task_id}/annotations"
def _generate_url_upload_tasks_annotations(self, task_id, upload_format_name):
return f"/api/v1/tasks/{task_id}/annotations?format={upload_format_name}"
def _generate_url_dump_job_annotations(self, job_id):
return f"/api/v1/jobs/{job_id}/annotations"
def _generate_url_upload_job_annotations(self, job_id, upload_format_name):
return f"/api/v1/jobs/{job_id}/annotations?format={upload_format_name}"
def _generate_url_dump_dataset(self, task_id):
return f"/api/v1/tasks/{task_id}/dataset"
def _remove_annotations(self, tid):
response = self._delete_request(f"/api/v1/tasks/{tid}/annotations", self.admin)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
return response
def _put_request_with_data(self, url, data, user):
with ForceLogin(user, self.client):
response = self.client.put(url, data)
return response
def _delete_task(self, tid):
response = self._delete_request('/api/v1/tasks/{}'.format(tid), self.admin)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
return response
def _check_dump_content(self, content, task_data, format_name, related_files=True):
def etree_to_dict(t):
d = {t.tag: {} if t.attrib else None}
children = list(t)
if children:
dd = defaultdict(list)
for dc in map(etree_to_dict, children):
for k, v in dc.items():
dd[k].append(v)
d = {t.tag: {k: v[0] if len(v) == 1 else v
for k, v in dd.items()}}
if t.attrib:
d[t.tag].update(('@' + k, v)
for k, v in t.attrib.items())
if t.text:
text = t.text.strip()
if children or t.attrib:
if text:
d[t.tag]['#text'] = text
else:
d[t.tag] = text
return d
if format_name == "Kitti Raw Format 1.0":
with tempfile.TemporaryDirectory() as tmp_dir:
zipfile.ZipFile(content).extractall(tmp_dir)
xmls = glob(osp.join(tmp_dir, '**', '*.xml'), recursive=True)
self.assertTrue(xmls)
for xml in xmls:
xmlroot = ET.parse(xml).getroot()
self.assertEqual(xmlroot.tag, "boost_serialization")
items = xmlroot.findall("./tracklets/item")
self.assertEqual(len(items), len(task_data["shapes"]))
elif format_name == "Sly Point Cloud Format 1.0":
with tempfile.TemporaryDirectory() as tmp_dir:
checking_files = [osp.join(tmp_dir, "key_id_map.json"),
osp.join(tmp_dir, "meta.json"),
osp.join(tmp_dir, "ds0", "ann", "000001.pcd.json"),
osp.join(tmp_dir, "ds0", "ann", "000002.pcd.json"),
osp.join(tmp_dir, "ds0", "ann","000003.pcd.json")]
if related_files:
checking_files.extend([osp.join(tmp_dir, "ds0", "related_images", "000001.pcd_pcd", "000001.png.json"),
osp.join(tmp_dir, "ds0", "related_images", "000002.pcd_pcd", "000002.png.json"),
osp.join(tmp_dir, "ds0", "related_images", "000003.pcd_pcd", "000003.png.json")])
zipfile.ZipFile(content).extractall(tmp_dir)
jsons = glob(osp.join(tmp_dir, '**', '*.json'), recursive=True)
self.assertTrue(jsons)
self.assertTrue(set(checking_files).issubset(set(jsons)))
class Task3DTest(_DbTestBase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.format_names = ["Sly Point Cloud Format 1.0", "Kitti Raw Format 1.0"]
cls._image_sizes = {}
cls.pointcloud_pcd_filename = "test_canvas3d.zip"
cls.pointcloud_pcd_path = osp.join(os.path.dirname(__file__), 'assets', cls.pointcloud_pcd_filename)
image_sizes = []
zip_file = zipfile.ZipFile(cls.pointcloud_pcd_path )
for info in zip_file.namelist():
if info.rsplit(".", maxsplit=1)[-1] == "pcd":
with zip_file.open(info, "r") as file:
data = ValidateDimension.get_pcd_properties(file)
image_sizes.append((int(data["WIDTH"]), int(data["HEIGHT"])))
cls.task = {
"name": "main task",
"owner_id": 1,
"assignee_id": 2,
"overlap": 0,
"segment_size": 100,
"labels": [
{"name": "car"},
{"name": "person"},
]
}
cls.task_with_attributes = {
"name": "task with attributes",
"owner_id": 1,
"assignee_id": 2,
"overlap": 0,
"segment_size": 100,
"labels": [
{"name": "car",
"color": "#2080c0",
"attributes": [
{
"name": "radio_name",
"mutable": False,
"input_type": "radio",
"default_value": "x1",
"values": ["x1", "x2", "x3"]
},
{
"name": "check_name",
"mutable": True,
"input_type": "checkbox",
"default_value": "false",
"values": ["false"]
},
{
"name": "text_name",
"mutable": False,
"input_type": "text",
"default_value": "qwerty",
"values": ["qwerty"]
},
{
"name": "number_name",
"mutable": False,
"input_type": "number",
"default_value": "-4.0",
"values": ["-4", "4", "1"]
}
]
},
{"name": "person",
"color": "#c06060",
"attributes": []
},
]
}
cls.task_many_jobs = {
"name": "task several jobs",
"owner_id": 1,
"assignee_id": 2,
"overlap": 3,
"segment_size": 1,
"labels": [
{
"name": "car",
"color": "#c06060",
"id": 1,
"attributes": []
}
]
}
cls.cuboid_example = {
"version": 0,
"tags": [],
"shapes": [
{
"type": "cuboid",
"occluded": False,
"z_order": 0,
"points": [0.16, 0.20, -0.26, 0, -0.14, 0, 4.84, 4.48, 4.12, 0, 0, 0, 0, 0, 0, 0],
"frame": 0,
"label_id": None,
"group": 0,
"source": "manual",
"attributes": []
},
],
"tracks": []
}
cls._image_sizes[cls.pointcloud_pcd_filename] = image_sizes
cls.expected_action = {
cls.admin: {'name': 'admin', 'code': status.HTTP_200_OK, 'annotation_changed': True},
cls.user: {'name': 'user', 'code': status.HTTP_200_OK, 'annotation_changed': True},
None: {'name': 'none', 'code': status.HTTP_401_UNAUTHORIZED, 'annotation_changed': False},
}
cls.expected_dump_upload = {
cls.admin: {'name': 'admin', 'code': status.HTTP_200_OK, 'create code': status.HTTP_201_CREATED,
'accept code': status.HTTP_202_ACCEPTED, 'file_exists': True, 'annotation_loaded': True},
cls.user: {'name': 'user', 'code': status.HTTP_200_OK, 'create code': status.HTTP_201_CREATED,
'accept code': status.HTTP_202_ACCEPTED, 'file_exists': True, 'annotation_loaded': True},
None: {'name': 'none', 'code': status.HTTP_401_UNAUTHORIZED, 'create code': status.HTTP_401_UNAUTHORIZED,
'accept code': status.HTTP_401_UNAUTHORIZED, 'file_exists': False, 'annotation_loaded': False},
}
def copy_pcd_file_and_get_task_data(self, test_dir):
tmp_file = osp.join(test_dir, self.pointcloud_pcd_filename)
copyfile(self.pointcloud_pcd_path, tmp_file)
task_data = {
"client_files[0]": open(tmp_file, 'rb'),
"image_quality": 100,
}
return task_data
def test_api_v1_create_annotation_in_job(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
annotation = self._get_tmp_annotation(task, self.cuboid_example)
for user, edata in list(self.expected_action.items()):
with self.subTest(format=edata["name"]):
response = self._patch_api_v1_task_id_annotations(task_id, annotation, CREATE_ACTION, user)
self.assertEqual(response.status_code, edata["code"])
if edata["annotation_changed"]:
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
task_shape = task_ann.data["shapes"][0]
task_shape.pop("id")
self.assertEqual(task_shape, annotation["shapes"][0])
self._remove_annotations(task_id)
def test_api_v1_update_annotation_in_task(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
annotation = self._get_tmp_annotation(task, self.cuboid_example)
response = self._put_api_v1_task_id_annotations(task_id, annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
for user, edata in list(self.expected_action.items()):
with self.subTest(format=edata["name"]):
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
annotation["shapes"][0]["points"] = [x + 0.1 for x in annotation["shapes"][0]["points"]]
annotation["shapes"][0]["id"] = task_ann_prev.data["shapes"][0]["id"]
response = self._patch_api_v1_task_id_annotations(task_id, annotation, UPDATE_ACTION, user)
self.assertEqual(response.status_code, edata["code"], task_id)
if edata["annotation_changed"]:
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
self.assertEqual(task_ann.data["shapes"], annotation["shapes"])
def test_api_v1_remove_annotation_in_task(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
annotation = self._get_tmp_annotation(task, self.cuboid_example)
for user, edata in list(self.expected_action.items()):
with self.subTest(format=edata["name"]):
response = self._patch_api_v1_task_id_annotations(task_id, annotation, CREATE_ACTION, self.admin)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
annotation["shapes"][0]["id"] = task_ann_prev.data["shapes"][0]["id"]
response = self._patch_api_v1_task_id_annotations(task_id, annotation, DELETE_ACTION, user)
self.assertEqual(response.status_code, edata["code"])
if edata["annotation_changed"]:
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
self.assertTrue(len(task_ann.data["shapes"]) == 0)
def test_api_v1_create_annotation_in_jobs(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
annotation = self._get_tmp_annotation(task, self.cuboid_example)
jobs = self._get_jobs(task_id)
for user, edata in list(self.expected_action.items()):
with self.subTest(format=edata["name"]):
response = self._patch_api_v1_job_id_annotations(jobs[0]["id"], annotation, CREATE_ACTION, user)
self.assertEqual(response.status_code, edata["code"])
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
if len(task_ann.data["shapes"]):
task_shape = task_ann.data["shapes"][0]
task_shape.pop("id")
self.assertEqual(task_shape, annotation["shapes"][0])
self._remove_annotations(task_id)
def test_api_v1_update_annotation_in_job(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
jobs = self._get_jobs(task_id)
annotation = self._get_tmp_annotation(task, self.cuboid_example)
response = self._put_api_v1_task_id_annotations(task_id, annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
for user, edata in list(self.expected_action.items()):
with self.subTest(format=edata["name"]):
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
annotation["shapes"][0]["points"] = [x + 0.1 for x in annotation["shapes"][0]["points"]]
annotation["shapes"][0]["id"] = task_ann_prev.data["shapes"][0]["id"]
response = self._patch_api_v1_job_id_annotations(jobs[0]["id"], annotation, UPDATE_ACTION, user)
self.assertEqual(response.status_code, edata["code"])
if edata["annotation_changed"]:
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
self.assertEqual(task_ann.data["shapes"], annotation["shapes"])
def test_api_v1_remove_annotation_in_job(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
jobs = self._get_jobs(task_id)
annotation = self._get_tmp_annotation(task, self.cuboid_example)
for user, edata in list(self.expected_action.items()):
with self.subTest(format=edata["name"]):
response = self._patch_api_v1_job_id_annotations(jobs[0]["id"], annotation, CREATE_ACTION, self.admin)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
annotation["shapes"][0]["id"] = task_ann_prev.data["shapes"][0]["id"]
response = self._patch_api_v1_job_id_annotations(jobs[0]["id"], annotation, DELETE_ACTION, user)
self.assertEqual(response.status_code, edata["code"])
if edata["annotation_changed"]:
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
self.assertTrue(len(task_ann.data["shapes"]) == 0)
def test_api_v1_dump_and_upload_annotation(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
for format_name in self.format_names:
annotation = self._get_tmp_annotation(task, self.cuboid_example)
response = self._put_api_v1_task_id_annotations(task_id, annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
for user, edata in list(self.expected_dump_upload.items()):
with self.subTest(format=f"{format_name}_{edata['name']}_dump"):
url = self._generate_url_dump_tasks_annotations(task_id)
file_name = osp.join(test_dir, f"{format_name}_{edata['name']}.zip")
data = {
"format": format_name,
}
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, edata['accept code'])
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, edata['create code'])
data = {
"format": format_name,
"action": "download",
}
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, edata['code'])
if response.status_code == status.HTTP_200_OK:
content = io.BytesIO(b"".join(response.streaming_content))
with open(file_name, "wb") as f:
f.write(content.getvalue())
self._check_dump_content(content, task_ann_prev.data, format_name, related_files=False)
self.assertEqual(osp.exists(file_name), edata['file_exists'])
self._remove_annotations(task_id)
with self.subTest(format=f"{format_name}_upload"):
file_name = osp.join(test_dir, f"{format_name}_admin.zip")
url = self._generate_url_upload_tasks_annotations(task_id, format_name)
with open(file_name, 'rb') as binary_file:
self._upload_file(url, binary_file, self.admin)
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
task_ann_prev.data["shapes"][0].pop("id")
task_ann.data["shapes"][0].pop("id")
self.assertEqual(len(task_ann_prev.data["shapes"]), len(task_ann.data["shapes"]))
self.assertEqual(task_ann_prev.data["shapes"], task_ann.data["shapes"])
def test_api_v1_rewrite_annotation(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
for format_name in self.format_names:
with self.subTest(format=f"{format_name}"):
annotation = self._get_tmp_annotation(task, self.cuboid_example)
response = self._put_api_v1_task_id_annotations(task_id, annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
url = self._generate_url_dump_tasks_annotations(task_id)
file_name = osp.join(test_dir, f"{format_name}.zip")
data = {
"format": format_name,
"action": "download",
}
self._download_file(url, data, self.admin, file_name)
self.assertTrue(osp.exists(file_name))
self._remove_annotations(task_id)
# rewrite annotation
annotation_copy = copy.deepcopy(annotation)
annotation_copy["shapes"][0]["points"] = [0] * 16
response = self._put_api_v1_task_id_annotations(task_id, annotation_copy)
self.assertEqual(response.status_code, status.HTTP_200_OK)
file_name = osp.join(test_dir, f"{format_name}.zip")
url = self._generate_url_upload_tasks_annotations(task_id, format_name)
with open(file_name, 'rb') as binary_file:
self._upload_file(url, binary_file, self.admin)
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
task_ann_prev.data["shapes"][0].pop("id")
task_ann.data["shapes"][0].pop("id")
self.assertEqual(len(task_ann_prev.data["shapes"]), len(task_ann.data["shapes"]))
self.assertEqual(task_ann_prev.data["shapes"], task_ann.data["shapes"])
def test_api_v1_dump_and_upload_empty_annotation(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
for format_name in self.format_names:
with self.subTest(format=f"{format_name}"):
url = self._generate_url_dump_tasks_annotations(task_id)
file_name = osp.join(test_dir, f"{format_name}.zip")
data = {
"format": format_name,
"action": "download",
}
self._download_file(url, data, self.admin, file_name)
self.assertTrue(osp.exists(file_name))
file_name = osp.join(test_dir, f"{format_name}.zip")
url = self._generate_url_upload_tasks_annotations(task_id, format_name)
with open(file_name, 'rb') as binary_file:
self._upload_file(url, binary_file, self.admin)
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
self.assertEqual(len(task_ann.data["shapes"]), 0)
self.assertEqual(task_ann_prev.data["shapes"], task_ann.data["shapes"])
def test_api_v1_dump_and_upload_several_jobs(self):
job_test_cases = ["first", "all"]
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task_many_jobs, task_data)
task_id = task["id"]
annotation = self._get_tmp_annotation(task, self.cuboid_example)
for format_name, job_test_case in itertools.product(self.format_names, job_test_cases):
with self.subTest(format=f"{format_name}_{job_test_case}"):
jobs = self._get_jobs(task_id)
if job_test_case == "all":
for job in jobs:
response = self._put_api_v1_job_id_annotations(job["id"], annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
else:
response = self._put_api_v1_job_id_annotations(jobs[1]["id"], annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
url = self._generate_url_dump_tasks_annotations(task_id)
file_name = osp.join(test_dir, f"{format_name}.zip")
data = {
"format": format_name,
"action": "download",
}
self._download_file(url, data, self.admin, file_name)
self._remove_annotations(task_id)
def test_api_v1_upload_annotation_with_attributes(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task_with_attributes, task_data)
task_id = task["id"]
for format_name in self.format_names:
annotation = self._get_tmp_annotation(task, self.cuboid_example)
response = self._put_api_v1_task_id_annotations(task_id, annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
with self.subTest(format=f"{format_name}_dump"):
url = self._generate_url_dump_tasks_annotations(task_id)
file_name = osp.join(test_dir, f"{format_name}.zip")
data = {
"format": format_name,
"action": "download",
}
self._download_file(url, data, self.admin, file_name)
self.assertTrue(osp.exists(file_name))
self._remove_annotations(task_id)
with self.subTest(format=f"{format_name}_upload"):
file_name = osp.join(test_dir, f"{format_name}.zip")
url = self._generate_url_upload_tasks_annotations(task_id, format_name)
with open(file_name, 'rb') as binary_file:
self._upload_file(url, binary_file, self.admin)
task_ann = TaskAnnotation(task_id)
task_ann.init_from_db()
task_ann_prev.data["shapes"][0].pop("id")
task_ann.data["shapes"][0].pop("id")
self.assertEqual(task_ann_prev.data["shapes"][0]["attributes"],
task_ann.data["shapes"][0]["attributes"])
def test_api_v1_export_dataset(self):
with TestDir() as test_dir:
task_data = self.copy_pcd_file_and_get_task_data(test_dir)
task = self._create_task(self.task, task_data)
task_id = task["id"]
for format_name in self.format_names:
annotation = self._get_tmp_annotation(task, self.cuboid_example)
response = self._put_api_v1_task_id_annotations(task_id, annotation)
self.assertEqual(response.status_code, status.HTTP_200_OK)
task_ann_prev = TaskAnnotation(task_id)
task_ann_prev.init_from_db()
for user, edata in list(self.expected_dump_upload.items()):
with self.subTest(format=f"{format_name}_{edata['name']}_export"):
url = self._generate_url_dump_dataset(task_id)
file_name = osp.join(test_dir, f"{format_name}_{edata['name']}.zip")
data = {
"format": format_name,
}
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, edata['accept code'])
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, edata['create code'])
data = {
"format": format_name,
"action": "download",
}
response = self._get_request_with_data(url, data, user)
self.assertEqual(response.status_code, edata['code'])
if response.status_code == status.HTTP_200_OK:
content = io.BytesIO(b"".join(response.streaming_content))
with open(file_name, "wb") as f:
f.write(content.getvalue())
self.assertEqual(osp.exists(file_name), edata['file_exists'])
self._check_dump_content(content, task_ann_prev.data, format_name,related_files=False)

@ -24,7 +24,7 @@ context('Canvas 3D functionality. Basic actions.', () => {
function testPerspectiveChangeOnArrowKeyPress(key, screenshotNameBefore, screenshotNameAfter) { function testPerspectiveChangeOnArrowKeyPress(key, screenshotNameBefore, screenshotNameAfter) {
cy.get('.cvat-canvas3d-perspective').trigger('mouseover').screenshot(screenshotNameBefore); cy.get('.cvat-canvas3d-perspective').trigger('mouseover').screenshot(screenshotNameBefore);
cy.get('body').type(key); cy.get('body').type(`{Shift}${key}`);
cy.get('.cvat-canvas3d-perspective').screenshot(screenshotNameAfter); cy.get('.cvat-canvas3d-perspective').screenshot(screenshotNameAfter);
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
`${screenshotsPath}/${screenshotNameBefore}.png`, `${screenshotsPath}/${screenshotNameBefore}.png`,
@ -123,6 +123,9 @@ context('Canvas 3D functionality. Basic actions.', () => {
].forEach(([button, tooltip]) => { ].forEach(([button, tooltip]) => {
testControlButtonTooltip(button, tooltip); testControlButtonTooltip(button, tooltip);
}); });
cy.get('.cvat-objects-sidebar-tabs').within(() => {
cy.contains('[role="tab"]', 'Issues').should('not.exist');
});
}); });
it('Check workspace selector.', () => { it('Check workspace selector.', () => {
@ -143,6 +146,8 @@ context('Canvas 3D functionality. Basic actions.', () => {
}); });
it('Interaction with the frame change buttons.', () => { it('Interaction with the frame change buttons.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.get('.cvat-player-last-button').click(); cy.get('.cvat-player-last-button').click();
cy.checkFrameNum(2); cy.checkFrameNum(2);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd');
@ -151,22 +156,28 @@ context('Canvas 3D functionality. Basic actions.', () => {
cy.checkFrameNum(0); cy.checkFrameNum(0);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd');
testContextImage(); // Check context image on the first frame testContextImage(); // Check context image on the first frame
cy.wait(waitTime);
cy.get('.cvat-player-forward-button').click(); cy.get('.cvat-player-forward-button').click();
cy.checkFrameNum(2); cy.checkFrameNum(2);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-backward-button').click(); cy.get('.cvat-player-backward-button').click();
cy.checkFrameNum(0); cy.checkFrameNum(0);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-next-button').click(); cy.get('.cvat-player-next-button').click();
cy.checkFrameNum(1); cy.checkFrameNum(1);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000002.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000002.pcd');
testContextImage(); // Check context image on the second frame testContextImage(); // Check context image on the second frame
cy.wait(waitTime);
cy.get('.cvat-player-previous-button').click(); cy.get('.cvat-player-previous-button').click();
cy.checkFrameNum(0); cy.checkFrameNum(0);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-play-button').click(); cy.get('.cvat-player-play-button').click();
cy.checkFrameNum(2); cy.checkFrameNum(2);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd'); cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-first-button').click(); // Return to first frame cy.get('.cvat-player-first-button').click(); // Return to first frame
}); });

@ -14,7 +14,7 @@ context('Canvas 3D functionality. Make a copy.', () => {
}; };
before(() => { before(() => {
cy.openTask(taskName) cy.openTask(taskName);
cy.addNewLabel(secondLabel); cy.addNewLabel(secondLabel);
cy.openJob(); cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display cy.wait(1000); // Waiting for the point cloud to display
@ -27,14 +27,14 @@ context('Canvas 3D functionality. Make a copy.', () => {
.find('.cvat-objects-sidebar-state-item-label-selector') .find('.cvat-objects-sidebar-state-item-label-selector')
.type(`${secondLabel}{Enter}`) .type(`${secondLabel}{Enter}`)
.trigger('mouseout'); .trigger('mouseout');
cy.get('#cvat-objects-sidebar-state-item-1') cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').click();
.find('[aria-label="more"]')
.click();
cy.get('.ant-dropdown-menu').not('.ant-dropdown-menu-hidden').find('[aria-label="copy"]').click(); cy.get('.ant-dropdown-menu').not('.ant-dropdown-menu-hidden').find('[aria-label="copy"]').click();
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 300, 200).dblclick(300, 200); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 300, 200).dblclick(300, 200);
cy.get('#cvat-objects-sidebar-state-item-1').invoke('attr', 'style').then((bgColor) => { cy.get('#cvat-objects-sidebar-state-item-1')
cy.get('#cvat-objects-sidebar-state-item-2').should('have.attr', 'style').and('equal', bgColor); .invoke('attr', 'style')
}); .then((bgColor) => {
cy.get('#cvat-objects-sidebar-state-item-2').should('have.attr', 'style').and('equal', bgColor);
});
}); });
it('Make a copy via hot keys.', () => { it('Make a copy via hot keys.', () => {
@ -44,14 +44,16 @@ context('Canvas 3D functionality. Make a copy.', () => {
cy.get('.cvat-objects-sidebar-state-item').then((sideBarItems) => { cy.get('.cvat-objects-sidebar-state-item').then((sideBarItems) => {
expect(sideBarItems.length).to.be.equal(3); expect(sideBarItems.length).to.be.equal(3);
}); });
cy.get('#cvat-objects-sidebar-state-item-2').invoke('attr', 'style').then((bgColor) => { cy.get('#cvat-objects-sidebar-state-item-2')
cy.get('#cvat-objects-sidebar-state-item-3').should('have.attr', 'style').and('equal', bgColor); .invoke('attr', 'style')
}); .then((bgColor) => {
cy.get('#cvat-objects-sidebar-state-item-3').should('have.attr', 'style').and('equal', bgColor);
});
}); });
it('Copy a cuboid to an another frame.', () => { it('Copy a cuboid to an another frame.', () => {
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 100, 200).trigger('mousemove', 300, 200); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 100, 200).trigger('mousemove', 300, 200);
cy.get('#cvat-objects-sidebar-state-item-2').should('have.class', 'cvat-objects-sidebar-state-active-item') cy.get('#cvat-objects-sidebar-state-item-2').should('have.class', 'cvat-objects-sidebar-state-active-item');
cy.get('body').type('{Ctrl}c'); cy.get('body').type('{Ctrl}c');
cy.get('.cvat-player-next-button').click().wait(1000); cy.get('.cvat-player-next-button').click().wait(1000);
cy.get('body').type('{Ctrl}v'); cy.get('body').type('{Ctrl}v');
@ -61,16 +63,5 @@ context('Canvas 3D functionality. Make a copy.', () => {
}); });
cy.get('.cvat-player-previous-button').click().wait(1000); cy.get('.cvat-player-previous-button').click().wait(1000);
}); });
it('Copy a shape to an another frame after press "Ctrl+V" on the first frame.', () => {
cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 100, 200).trigger('mousemove', 300, 200);
cy.get('#cvat-objects-sidebar-state-item-2').should('have.class', 'cvat-objects-sidebar-state-active-item')
cy.get('body').type('{Ctrl}c').type('{Ctrl}v');
cy.get('.cvat-player-next-button').click().wait(1000);
cy.get('.cvat-canvas3d-perspective').trigger('mousemove').dblclick();
cy.get('.cvat-objects-sidebar-state-item').then((sideBarItems) => {
expect(sideBarItems.length).to.be.equal(2);
});
});
}); });
}); });

@ -13,7 +13,7 @@ context('Canvas 3D functionality. Cuboid propagate.', () => {
}; };
before(() => { before(() => {
cy.openTask(taskName) cy.openTask(taskName);
cy.openJob(); cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams); cy.create3DCuboid(cuboidCreationParams);
@ -21,18 +21,21 @@ context('Canvas 3D functionality. Cuboid propagate.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Cuboid propagate.', () => { it('Cuboid propagate.', () => {
cy.get('#cvat-objects-sidebar-state-item-1') cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').click();
.find('[aria-label="more"]')
.click();
cy.get('.ant-dropdown-menu').not('.ant-dropdown-menu-hidden').find('[aria-label="block"]').click(); cy.get('.ant-dropdown-menu').not('.ant-dropdown-menu-hidden').find('[aria-label="block"]').click();
cy.get('.cvat-propagate-confirm-object-on-frames').should('exist'); cy.get('.cvat-propagate-confirm-object-on-frames').should('exist');
cy.contains('button', 'Yes').click(); cy.contains('button', 'Yes').click();
}); });
it('On a other frames the cuboid should exist.', () => { it('On a other frames the cuboid should exist.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.get('.cvat-player-next-button').click(); cy.get('.cvat-player-next-button').click();
cy.wait(waitTime);
cy.get('#cvat-objects-sidebar-state-item-2').should('exist'); cy.get('#cvat-objects-sidebar-state-item-2').should('exist');
cy.wait(waitTime);
cy.get('.cvat-player-next-button').click(); cy.get('.cvat-player-next-button').click();
cy.wait(waitTime);
cy.get('#cvat-objects-sidebar-state-item-3').should('exist'); cy.get('#cvat-objects-sidebar-state-item-3').should('exist');
}); });
}); });

@ -8,13 +8,14 @@ import { taskName, labelName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Save a job. Remove annotations.', () => { context('Canvas 3D functionality. Save a job. Remove annotations.', () => {
const caseId = '88'; const caseId = '88';
const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js'; const screenshotsPath =
'cypress/screenshots/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js';
const cuboidCreationParams = { const cuboidCreationParams = {
labelName: labelName, labelName: labelName,
}; };
before(() => { before(() => {
cy.openTask(taskName) cy.openTask(taskName);
cy.openJob(); cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display cy.wait(1000); // Waiting for the point cloud to display
cy.get('.cvat-canvas3d-topview').find('.cvat-canvas3d-fullsize').screenshot('canvas3d_topview_before_all'); cy.get('.cvat-canvas3d-topview').find('.cvat-canvas3d-fullsize').screenshot('canvas3d_topview_before_all');
@ -23,14 +24,22 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => {
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {
it('Save a job. Reopen the job.', () => { it('Save a job. Reopen the job.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.saveJob('PATCH', 200, 'saveJob'); cy.saveJob('PATCH', 200, 'saveJob');
cy.wait(waitTime);
cy.goToTaskList(); cy.goToTaskList();
cy.wait(waitTime);
cy.openTaskJob(taskName); cy.openTaskJob(taskName);
cy.wait(1000); // Waiting for the point cloud to display cy.wait(waitTime); // Waiting for the point cloud to display
cy.get('.cvat-objects-sidebar-state-item').then((sidebarStateItem) => { cy.get('.cvat-objects-sidebar-state-item').then((sidebarStateItem) => {
expect(sidebarStateItem.length).to.be.equal(1); expect(sidebarStateItem.length).to.be.equal(1);
}); });
cy.get('.cvat-canvas3d-topview').find('.cvat-canvas3d-fullsize').screenshot('canvas3d_topview_after_reopen_job'); cy.wait(waitTime);
cy.get('.cvat-canvas3d-topview')
.find('.cvat-canvas3d-fullsize')
.screenshot('canvas3d_topview_after_reopen_job');
cy.wait(waitTime);
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_topview_before_all.png`, `${screenshotsPath}/canvas3d_topview_before_all.png`,
`${screenshotsPath}/canvas3d_topview_after_reopen_job.png`, `${screenshotsPath}/canvas3d_topview_after_reopen_job.png`,
@ -42,7 +51,9 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => {
cy.saveJob('PUT'); cy.saveJob('PUT');
cy.contains('Saving changes on the server').should('be.hidden'); cy.contains('Saving changes on the server').should('be.hidden');
cy.get('.cvat-objects-sidebar-state-item').should('not.exist'); cy.get('.cvat-objects-sidebar-state-item').should('not.exist');
cy.get('.cvat-canvas3d-topview').find('.cvat-canvas3d-fullsize').screenshot('canvas3d_topview_after_remove_annotations'); cy.get('.cvat-canvas3d-topview')
.find('.cvat-canvas3d-fullsize')
.screenshot('canvas3d_topview_after_remove_annotations');
cy.compareImagesAndCheckResult( cy.compareImagesAndCheckResult(
`${screenshotsPath}/canvas3d_topview_after_reopen_job.png`, `${screenshotsPath}/canvas3d_topview_after_reopen_job.png`,
`${screenshotsPath}/canvas3d_topview_after_remove_annotations.png`, `${screenshotsPath}/canvas3d_topview_after_remove_annotations.png`,

@ -0,0 +1,114 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName, labelName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', () => {
const caseId = '91';
const cuboidCreationParams = {
labelName: labelName,
};
const dumpTypePC = 'Sly Point Cloud Format';
let annotationPCArchiveName = '';
function confirmUpdate(modalWindowClassName) {
cy.get(modalWindowClassName).within(() => {
cy.contains('button', 'Update').click();
});
}
function uploadToTask(toTaskName) {
cy.contains('.cvat-item-task-name', toTaskName)
.parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon')
.trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover');
cy.readFile('cypress/downloads/' + annotationPCArchiveName, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileName: annotationPCArchiveName,
fileContent: fileContent,
});
});
});
}
before(() => {
cy.openTask(taskName);
cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams);
});
describe(`Testing case "${caseId}"`, () => {
it('Save a job. Dump with "Point Cloud" format.', () => {
cy.saveJob('PATCH', 200, 'saveJob');
cy.intercept('GET', '/api/v1/tasks/**/annotations**').as('dumpAnnotations');
cy.interactMenu('Dump annotations');
cy.get('.cvat-menu-dump-submenu-item').then((subMenu) => {
expect(subMenu.length).to.be.equal(2);
});
cy.get('.cvat-menu-dump-submenu-item').within(() => {
cy.contains(dumpTypePC).click();
});
cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201);
cy.removeAnnotations();
cy.saveJob('PUT');
cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist');
cy.wait(2000); // Waiting for the full download.
cy.task('listFiles', 'cypress/downloads').each((fileName) => {
if (fileName.includes(dumpTypePC.toLowerCase())) {
annotationPCArchiveName = fileName;
}
});
});
it('Upload "Point Cloud" format annotation to job.', () => {
cy.interactMenu('Upload annotations');
cy.readFile('cypress/downloads/' + annotationPCArchiveName, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypePC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileContent: fileContent,
fileName: annotationPCArchiveName,
});
});
});
cy.intercept('PUT', '/api/v1/jobs/**/annotations**').as('uploadAnnotationsPut');
cy.intercept('GET', '/api/v1/jobs/**/annotations**').as('uploadAnnotationsGet');
confirmUpdate('.cvat-modal-content-load-job-annotation');
cy.wait('@uploadAnnotationsPut', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@uploadAnnotationsPut').its('response.statusCode').should('equal', 201);
cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();
cy.get('button').contains('Save').click({ force: true });
});
it('Upload annotation to task.', () => {
cy.goToTaskList();
uploadToTask(taskName);
confirmUpdate('.cvat-modal-content-load-task-annotation');
cy.contains('Annotations have been loaded').should('be.visible');
cy.get('[data-icon="close"]').click();
cy.openTaskJob(taskName);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();
cy.get('button').contains('Save').click({ force: true });
});
});
});

@ -0,0 +1,109 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName, labelName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" format.', () => {
const caseId = '92';
const cuboidCreationParams = {
labelName: labelName,
};
const dumpTypeVC = 'Kitti Raw Format';
let annotationVCArchiveName = '';
function confirmUpdate(modalWindowClassName) {
cy.get(modalWindowClassName).within(() => {
cy.contains('button', 'Update').click();
});
}
function uploadToTask(toTaskName) {
cy.contains('.cvat-item-task-name', toTaskName)
.parents('.cvat-tasks-list-item')
.find('.cvat-menu-icon')
.trigger('mouseover');
cy.contains('Upload annotations').trigger('mouseover');
cy.readFile('cypress/downloads/' + annotationVCArchiveName, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileContent: fileContent,
fileName: annotationVCArchiveName,
});
});
});
}
before(() => {
cy.openTask(taskName);
cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams);
});
describe(`Testing case "${caseId}"`, () => {
it('Save a job. Dump with "Velodyne Points" format.', () => {
cy.saveJob('PATCH', 200, 'saveJob');
cy.intercept('GET', '/api/v1/tasks/**/annotations**').as('dumpAnnotations');
cy.interactMenu('Dump annotations');
cy.get('.cvat-menu-dump-submenu-item').within(() => {
cy.contains(dumpTypeVC).click();
});
cy.wait('@dumpAnnotations', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@dumpAnnotations').its('response.statusCode').should('equal', 201);
cy.removeAnnotations();
cy.saveJob('PUT');
cy.get('#cvat-objects-sidebar-state-item-1').should('not.exist');
cy.wait(2000); // Waiting for the full download.
cy.task('listFiles', 'cypress/downloads').each((fileName) => {
if (fileName.includes(dumpTypeVC.toLowerCase())) {
annotationVCArchiveName = fileName;
}
});
});
it('Upload "Velodyne Points" format annotation to job.', () => {
cy.interactMenu('Upload annotations');
cy.readFile('cypress/downloads/' + annotationVCArchiveName, 'binary')
.then(Cypress.Blob.binaryStringToBlob)
.then((fileContent) => {
cy.contains('.cvat-menu-load-submenu-item', dumpTypeVC.split(' ')[0])
.should('be.visible')
.within(() => {
cy.get('.cvat-menu-load-submenu-item-button').click().get('input[type=file]').attachFile({
fileContent: fileContent,
fileName: annotationVCArchiveName,
});
});
});
cy.intercept('PUT', '/api/v1/jobs/**/annotations**').as('uploadAnnotationsPut');
cy.intercept('GET', '/api/v1/jobs/**/annotations**').as('uploadAnnotationsGet');
confirmUpdate('.cvat-modal-content-load-job-annotation');
cy.wait('@uploadAnnotationsPut', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@uploadAnnotationsPut').its('response.statusCode').should('equal', 201);
cy.wait('@uploadAnnotationsGet').its('response.statusCode').should('equal', 200);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();
cy.get('button').contains('Save').click({ force: true });
});
it('Upload annotation to task.', () => {
cy.goToTaskList();
uploadToTask(taskName);
confirmUpdate('.cvat-modal-content-load-task-annotation');
cy.contains('Annotations have been loaded').should('be.visible');
cy.get('[data-icon="close"]').click();
cy.openTaskJob(taskName);
cy.get('#cvat-objects-sidebar-state-item-1').should('exist');
cy.removeAnnotations();
cy.get('button').contains('Save').click({ force: true });
});
});
});

@ -0,0 +1,49 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
/// <reference types="cypress" />
import { taskName, labelName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Export as a dataset.', () => {
const caseId = '93';
const cuboidCreationParams = {
labelName: labelName,
};
const dumpTypePC = 'Sly Point Cloud Format';
const dumpTypeVC = 'Kitti Raw Format';
before(() => {
cy.openTask(taskName)
cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams);
cy.saveJob();
});
describe(`Testing case "${caseId}"`, () => {
it('Export as a dataset with "Point Cloud" format.', () => {
cy.intercept('GET', '/api/v1/tasks/**/dataset**').as('exportDatasetPC');
cy.interactMenu('Export as a dataset');
cy.get('.cvat-menu-export-submenu-item').within(() => {
cy.contains(dumpTypePC).click();
});
cy.wait('@exportDatasetPC', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@exportDatasetPC').its('response.statusCode').should('equal', 201);
});
it('Export as a dataset with "Velodyne Points" format.', () => {
cy.intercept('GET', '/api/v1/tasks/**/dataset**').as('exportDatasetVC');
cy.interactMenu('Export as a dataset');
cy.get('.cvat-menu-export-submenu-item').within(() => {
cy.contains(dumpTypeVC).click();
});
cy.wait('@exportDatasetVC', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@exportDatasetVC').its('response.statusCode').should('equal', 201);
cy.removeAnnotations();
cy.saveJob('PUT');
});
});
});
Loading…
Cancel
Save