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

@ -111,6 +111,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
},
};
this.action = {
loading: false,
oldState: '',
scan: null,
selectable: true,
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'));
@ -293,7 +324,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.views.perspective.scene.children[0].children,
false,
);
if (intersects.length !== 0) {
if (intersects.length !== 0 || this.controller.focused.clientID !== null) {
this.setDefaultZoom();
} else {
const { x, y, z } = this.action.frameCoordinates;
@ -417,6 +448,12 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE;
} else {
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.maxDistance = CONST.MAX_DISTANCE;
@ -493,6 +530,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
private startAction(view: any, event: MouseEvent): void {
if (event.detail !== 1) return;
if (this.model.mode === Mode.DRAG_CANVAS) return;
const { clientID } = this.model.data.activeElement;
if (clientID === 'null') return;
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 {
event.preventDefault();
if (this.model.mode === Mode.DRAG_CANVAS) return;
const { clientID } = this.model.data.activeElement;
if (clientID === 'null') return;
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),
},
};
this.model.mode = Mode.IDLE;
this.action.selectable = true;
}
private completeActions(): void {
const { scan, detected } = this.action;
if (this.model.mode === Mode.DRAG_CANVAS) return;
if (!detected) {
this.resetActions();
return;
@ -604,8 +646,6 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.adjustPerspectiveCameras();
this.translateReferencePlane(new THREE.Vector3(x, y, z));
this.resetActions();
this.model.mode = Mode.IDLE;
this.action.selectable = true;
}
private onGroupDone(objects?: any[]): void {
@ -718,6 +758,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
const object = this.model.data.objects[i];
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 {
if (reason === UpdateReasons.IMAGE_CHANGED) {
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 objectURL = URL.createObjectURL(model.data.image.imageData);
this.clearScene();
@ -766,12 +814,21 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
this.setupObjects();
} else if (reason === UpdateReasons.DRAG_CANVAS) {
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,
cancelable: true,
}),
);
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();
} else if (reason === UpdateReasons.CANCEL) {
if (this.mode === Mode.DRAW) {
@ -783,7 +840,14 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}
this.model.data.groupData.grouped = [];
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'));
} else if (reason === UpdateReasons.FITTED_CANVAS) {
this.dispatchEvent(new CustomEvent('canvas.fit'));
@ -1153,6 +1217,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
}
this.updateRotationHelperPos();
this.updateResizeHelperPos();
} else {
this.resetActions();
}
}
}
@ -1166,6 +1232,16 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
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 {
@ -1729,9 +1805,6 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
true,
);
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;
this.action.translation.helper = viewType.rayCaster.mouseVector.clone();
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 {
const { controls } = this.views.perspective;
if (!controls) return;
switch (key.code) {
case CameraAction.ROTATE_RIGHT:
controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
break;
case CameraAction.ROTATE_LEFT:
controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
break;
case CameraAction.TILT_UP:
controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
break;
case CameraAction.TILT_DOWN:
controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
break;
case 'ControlLeft':
this.action.selectable = !key.ctrlKey;
break;
default:
break;
}
if (key.altKey === true) {
if (key.shiftKey) {
switch (key.code) {
case CameraAction.ROTATE_RIGHT:
controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
break;
case CameraAction.ROTATE_LEFT:
controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true);
break;
case CameraAction.TILT_UP:
controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
break;
case CameraAction.TILT_DOWN:
controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true);
break;
default:
break;
}
} else if (key.altKey === true) {
switch (key.code) {
case CameraAction.ZOOM_IN:
controls.dolly(CONST.DOLLY_FACTOR, true);
@ -1791,6 +1862,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener {
default:
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 DOLLY_FACTOR = 5;
const MAX_DISTANCE = 100;
const MIN_DISTANCE = 0;
const MIN_DISTANCE = 0.3;
const ZOOM_FACTOR = 7;
const ROTATION_HELPER_OFFSET = 0.1;
const CAMERA_REFERENCE = 'camRef';

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

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

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

@ -142,3 +142,22 @@
height: 100%;
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 {
visible: boolean;
jobInstance: any;
}
interface DispatchToProps {
@ -21,10 +22,14 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState): StateToProps {
const {
shortcuts: { visibleShortcutsHelp: visible },
annotation: {
job: { instance: jobInstance },
},
} = state;
return {
visible,
jobInstance,
};
}
@ -37,7 +42,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
}
function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | null {
const { visible, switchShortcutsDialog } = props;
const { visible, switchShortcutsDialog, jobInstance } = props;
const keyMap = getApplicationKeyMap();
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) => ({
key: id,
name: keyMap[key].name || key,
description: keyMap[key].description || '',
shortcut: keyMap[key].sequences,
action: [keyMap[key].action],
}));
const dimensionType = jobInstance ? jobInstance.task.dimension : undefined;
const dataSource = Object.keys(keyMap)
.filter((key: string) => !dimensionType || keyMap[key].applicable.includes(dimensionType))
.map((key: string, id: number) => ({
key: id,
name: keyMap[key].name || key,
description: keyMap[key].description || '',
shortcut: keyMap[key].sequences,
action: [keyMap[key].action],
}));
return (
<Modal

@ -287,6 +287,16 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
NEXT_KEY_FRAME: keyMap.NEXT_KEY_FRAME,
PREV_KEY_FRAME: keyMap.PREV_KEY_FRAME,
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 => {
@ -308,6 +318,16 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
};
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) => {
preventDefault(event);
this.lockAllStates(!statesLocked);

@ -30,7 +30,7 @@ import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-ba
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import {
CombinedState, FrameSpeed, Workspace, PredictorState,
CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType,
} from 'reducers/interfaces';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
@ -220,48 +220,13 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}
public componentDidUpdate(prevProps: Props): void {
const {
jobInstance,
frameSpeed,
frameNumber,
frameDelay,
playing,
canvasIsReady,
canvasInstance,
onSwitchPlay,
onChangeFrame,
autoSaveInterval,
} = this.props;
const { autoSaveInterval } = this.props;
if (autoSaveInterval !== prevProps.autoSaveInterval) {
if (this.autoSaveInterval) window.clearInterval(this.autoSaveInterval);
this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval);
}
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);
}
}
this.play();
}
public componentWillUnmount(): void {
@ -473,6 +438,47 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
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 {
const { autoSave, saving } = this.props;

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

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

@ -11,7 +11,7 @@ from django.utils import timezone
import datumaro.components.extractor as datumaro
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.image import ByteImage, Image
@ -20,6 +20,7 @@ from .annotation import AnnotationManager, TrackManager
class TaskData:
Attribute = namedtuple('Attribute', 'name, value')
Shape = namedtuple("Shape", 'id, label_id') # 3d
LabeledShape = namedtuple(
'LabeledShape', 'type, frame, label, points, occluded, attributes, source, group, z_order')
LabeledShape.__new__.__defaults__ = (0, 0)
@ -30,7 +31,8 @@ class TaskData:
Tag = namedtuple('Tag', 'frame, label, attributes, source, group')
Tag.__new__.__defaults__ = (0, )
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):
self._annotation_ir = annotation_ir
@ -122,6 +124,7 @@ class TaskData:
} for frame in range(self._db_task.data.size)}
else:
self._frame_info = {self.rel_frame_id(db_image.frame): {
"id": db_image.id,
"path": db_image.path,
"width": db_image.width,
"height": db_image.height,
@ -234,6 +237,12 @@ class TaskData:
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):
return TaskData.Tag(
frame=self.abs_frame_id(tag["frame"]),
@ -243,6 +252,14 @@ class TaskData:
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):
frames = {}
def get_frame(idx):
@ -251,12 +268,15 @@ class TaskData:
if frame not in frames:
frames[frame] = TaskData.Frame(
idx=idx,
id=frame_info.get('id',0),
frame=frame,
name=frame_info['path'],
height=frame_info["height"],
width=frame_info["width"],
labeled_shapes=[],
tags=[],
shapes=[],
labels={}
)
return frames[frame]
@ -265,6 +285,7 @@ class TaskData:
get_frame(idx)
anno_manager = AnnotationManager(self._annotation_ir)
shape_data = ''
for shape in sorted(anno_manager.to_shapes(self._db_task.data.size),
key=lambda shape: shape.get("z_order", 0)):
if shape['frame'] not in self._frame_info:
@ -278,8 +299,13 @@ class TaskData:
exported_shape = self._export_tracked_shape(shape)
else:
exported_shape = self._export_labeled_shape(shape)
get_frame(shape['frame']).labeled_shapes.append(
exported_shape)
shape_data = self._export_shape(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:
get_frame(tag['frame']).tags.append(self._export_tag(tag))
@ -457,17 +483,31 @@ class TaskData:
return None
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__()
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 = []
is_video = task_data.meta['task']['mode'] == 'interpolation'
ext = ''
if is_video:
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)
if is_video:
# optimization for videos: use numpy arrays instead of bytes
@ -490,14 +530,36 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
'path': frame_data.name + ext,
'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)
else:
dm_image = Image(**image_args)
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,
attributes={'frame': frame_data.frame})
if dimension == DimensionType.DIM_2D:
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)
self._items = dm_items
@ -513,19 +575,25 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
return self._categories
@staticmethod
def _load_categories(cvat_anno):
def _load_categories(cvat_anno, dimension):
categories = {}
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']:
label_categories.add(label['name'])
for _, attr in label['attributes']:
label_categories.attributes.add(attr['name'])
categories[datumaro.AnnotationType.label] = label_categories
return categories
return categories, user_info
def _read_cvat_anno(self, cvat_frame_anno, task_data):
item_anno = []
@ -554,6 +622,9 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
raise Exception(
"Failed to convert attribute '%s'='%s': %s" %
(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
for tag_obj in cvat_frame_anno.tags:
@ -565,7 +636,11 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor):
attributes=anno_attr, group=anno_group)
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_label = map_label(shape_obj.label)
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,
z_order=shape_obj.z_order)
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:
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.polyline: ShapeType.POLYLINE,
datumaro.AnnotationType.points: ShapeType.POINTS,
datumaro.AnnotationType.cuboid_3d: ShapeType.CUBOID
}
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:
raise CvatImportError("annotation has no label")
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(
type=shapes[ann.type],
frame=frame_number,
points = ann.points,
label=label_cat.items[ann.label].name,
points=ann.points,
occluded=ann.attributes.get('occluded') == True,
z_order=ann.z_order,
group=group_map.get(ann.group, 0),
@ -702,4 +795,4 @@ def import_dm_annotations(dm_dataset, task_data):
))
except Exception as e:
raise CvatImportError("Image {}: can't import annotation "
"#{} ({}): {}".format(item.id, idx, ann.type.name, e))
"#{} ({}): {}".format(item.id, idx, ann.type.name, e))

@ -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.market1501
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 Localization 1.0',
'ICDAR Segmentation 1.0',
'Kitti Raw Format 1.0',
'Sly Point Cloud Format 1.0'
})
def test_import_formats_query(self):
@ -312,6 +315,8 @@ class TaskExportTest(_DbTestBase):
'ICDAR Recognition 1.0',
'ICDAR Localization 1.0',
'ICDAR Segmentation 1.0',
'Kitti Raw Format 1.0',
'Sly Point Cloud Format 1.0'
})
def test_exports(self):
@ -910,4 +915,4 @@ class TaskAnnotationsImportTest(_DbTestBase):
if not f.ENABLED:
self.skipTest("Format is disabled")
self._test_can_import_annotations(task, format_name)
self._test_can_import_annotations(task, format_name)

@ -323,7 +323,10 @@ class TaskDumpUploadTest(_DbTestBase):
with TestDir() as test_dir:
# Dump annotations with objects type is shape
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
dump_format_name = dump_format.DISPLAY_NAME
with self.subTest(format=dump_format_name):
@ -425,7 +428,10 @@ class TaskDumpUploadTest(_DbTestBase):
with TestDir() as test_dir:
# Dump annotations with objects type is track
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
dump_format_name = dump_format.DISPLAY_NAME
with self.subTest(format=dump_format_name):
@ -868,7 +874,10 @@ class TaskDumpUploadTest(_DbTestBase):
with self.subTest(format=dump_format_name):
if dump_format_name in [
"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")
images = self._generate_task_images(3)
@ -975,6 +984,8 @@ class TaskDumpUploadTest(_DbTestBase):
"MOTS PNG 1.0", # changed points values
"Segmentation mask 1.1", # 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")

@ -3370,6 +3370,7 @@ class JobAnnotationAPITestCase(APITestCase):
create_db_users(cls)
def _create_task(self, owner, assignee, annotation_format=""):
dimension = DimensionType.DIM_2D
data = {
"name": "my task #1",
"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":
data["labels"] = [{
"name": "icdar",
@ -3510,6 +3517,15 @@ class JobAnnotationAPITestCase(APITestCase):
"image_quality": 75,
"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)
assert response.status_code == status.HTTP_202_ACCEPTED
@ -4471,7 +4487,8 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
def _get_initial_annotation(annotation_format):
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 = [{
"frame": 0,
"label_id": task["labels"][0]["id"],
@ -4815,7 +4832,32 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
],
}]
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":
tags_with_attrs = [{
"frame": 1,
@ -5033,6 +5075,7 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
data["version"] += 2 # upload is delete + put
self._check_response(response, data)
break
def _check_dump_content(self, content, task, jobs, data, format_name):
def etree_to_dict(t):
d = {t.tag: {} if t.attrib else None}
@ -5068,6 +5111,8 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
self.assertTrue(zipfile.is_zipfile(content))
elif format_name == "YOLO 1.1":
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":
with tempfile.TemporaryDirectory() as 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) {
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.compareImagesAndCheckResult(
`${screenshotsPath}/${screenshotNameBefore}.png`,
@ -123,6 +123,9 @@ context('Canvas 3D functionality. Basic actions.', () => {
].forEach(([button, tooltip]) => {
testControlButtonTooltip(button, tooltip);
});
cy.get('.cvat-objects-sidebar-tabs').within(() => {
cy.contains('[role="tab"]', 'Issues').should('not.exist');
});
});
it('Check workspace selector.', () => {
@ -143,6 +146,8 @@ context('Canvas 3D functionality. Basic actions.', () => {
});
it('Interaction with the frame change buttons.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.get('.cvat-player-last-button').click();
cy.checkFrameNum(2);
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.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd');
testContextImage(); // Check context image on the first frame
cy.wait(waitTime);
cy.get('.cvat-player-forward-button').click();
cy.checkFrameNum(2);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000003.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-backward-button').click();
cy.checkFrameNum(0);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-next-button').click();
cy.checkFrameNum(1);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000002.pcd');
testContextImage(); // Check context image on the second frame
cy.wait(waitTime);
cy.get('.cvat-player-previous-button').click();
cy.checkFrameNum(0);
cy.get('.cvat-player-filename-wrapper').should('contain.text', '000001.pcd');
cy.wait(waitTime);
cy.get('.cvat-player-play-button').click();
cy.checkFrameNum(2);
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
});

@ -14,7 +14,7 @@ context('Canvas 3D functionality. Make a copy.', () => {
};
before(() => {
cy.openTask(taskName)
cy.openTask(taskName);
cy.addNewLabel(secondLabel);
cy.openJob();
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')
.type(`${secondLabel}{Enter}`)
.trigger('mouseout');
cy.get('#cvat-objects-sidebar-state-item-1')
.find('[aria-label="more"]')
.click();
cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').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-objects-sidebar-state-item-1').invoke('attr', 'style').then((bgColor) => {
cy.get('#cvat-objects-sidebar-state-item-2').should('have.attr', 'style').and('equal', bgColor);
});
cy.get('#cvat-objects-sidebar-state-item-1')
.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.', () => {
@ -44,14 +44,16 @@ context('Canvas 3D functionality. Make a copy.', () => {
cy.get('.cvat-objects-sidebar-state-item').then((sideBarItems) => {
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-3').should('have.attr', 'style').and('equal', bgColor);
});
cy.get('#cvat-objects-sidebar-state-item-2')
.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.', () => {
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('.cvat-player-next-button').click().wait(1000);
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);
});
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(() => {
cy.openTask(taskName)
cy.openTask(taskName);
cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display
cy.create3DCuboid(cuboidCreationParams);
@ -21,18 +21,21 @@ context('Canvas 3D functionality. Cuboid propagate.', () => {
describe(`Testing case "${caseId}"`, () => {
it('Cuboid propagate.', () => {
cy.get('#cvat-objects-sidebar-state-item-1')
.find('[aria-label="more"]')
.click();
cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').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.contains('button', 'Yes').click();
});
it('On a other frames the cuboid should exist.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.get('.cvat-player-next-button').click();
cy.wait(waitTime);
cy.get('#cvat-objects-sidebar-state-item-2').should('exist');
cy.wait(waitTime);
cy.get('.cvat-player-next-button').click();
cy.wait(waitTime);
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.', () => {
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 = {
labelName: labelName,
};
before(() => {
cy.openTask(taskName)
cy.openTask(taskName);
cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display
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}"`, () => {
it('Save a job. Reopen the job.', () => {
const waitTime = 1000;
cy.wait(waitTime);
cy.saveJob('PATCH', 200, 'saveJob');
cy.wait(waitTime);
cy.goToTaskList();
cy.wait(waitTime);
cy.openTaskJob(taskName);
cy.wait(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) => {
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(
`${screenshotsPath}/canvas3d_topview_before_all.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.contains('Saving changes on the server').should('be.hidden');
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(
`${screenshotsPath}/canvas3d_topview_after_reopen_job.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