Merge branch 'develop' into dk/point-deletion

main
Dmitry Kalinin 6 years ago
commit 7e7a5a60ed

@ -20,7 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- -
### Fixed ### Fixed
- - File names in LabelMe format export are no longer truncated ([#1259](https://github.com/opencv/cvat/issues/1259))
- `occluded` and `z_order` annotation attributes are now correctly passed to Datumaro ([#1271](https://github.com/opencv/cvat/pull/1271))
- Annotation-less tasks now can be exported as empty datasets in COCO ([#1277](https://github.com/opencv/cvat/issues/1277))
- Frame name matching for video annotations import -
allowed `frame_XXXXXX[.ext]` format ([#1274](https://github.com/opencv/cvat/pull/1274))
### Security ### Security
- Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270)) - Bump acorn from 6.3.0 to 6.4.1 in /cvat-ui ([#1270](https://github.com/opencv/cvat/pull/1270))
@ -48,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- React & Redux & Antd based dashboard - React & Redux & Antd based dashboard
- Yolov3 interpretation script fix and changes to mapping.json - Yolov3 interpretation script fix and changes to mapping.json
- YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151)) - YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151))
- Added support for OpenVINO 2020
### Fixed ### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826) - Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)

@ -37,6 +37,19 @@ Canvas itself handles:
EXTREME_POINTS = 'By 4 points' EXTREME_POINTS = 'By 4 points'
} }
enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
}
interface DrawData { interface DrawData {
enabled: boolean; enabled: boolean;
shapeType?: string; shapeType?: string;
@ -70,6 +83,7 @@ Canvas itself handles:
} }
interface Canvas { interface Canvas {
mode(): Mode;
html(): HTMLDivElement; html(): HTMLDivElement;
setZLayer(zLayer: number | null): void; setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
@ -128,6 +142,10 @@ Standard JS events are used.
- canvas.dragstop - canvas.dragstop
- canvas.zoomstart - canvas.zoomstart
- canvas.zoomstop - canvas.zoomstop
- canvas.zoom
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.resizeshape => {id: number}
``` ```
### WEB ### WEB
@ -135,7 +153,8 @@ Standard JS events are used.
// Create an instance of a canvas // Create an instance of a canvas
const canvas = new window.canvas.Canvas(); const canvas = new window.canvas.Canvas();
console.log('Version', window.canvas.CanvasVersion); console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container // Put canvas to a html container
htmlContainer.appendChild(canvas.html()); htmlContainer.appendChild(canvas.html());

@ -74,7 +74,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode; return this.controller.mode;
} }
private onDrawDone(data: object, continueDraw?: boolean): void { private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
if (data) { if (data) {
const { zLayer } = this.controller; const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', { const event: CustomEvent = new CustomEvent('canvas.drawn', {
@ -87,6 +87,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
zOrder: zLayer || 0, zOrder: zLayer || 0,
}, },
continue: continueDraw, continue: continueDraw,
duration,
}, },
}); });
@ -137,12 +138,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
} }
private onMergeDone(objects: any[]): void { private onMergeDone(objects: any[]| null, duration?: number): void {
if (objects) { if (objects) {
const event: CustomEvent = new CustomEvent('canvas.merged', { const event: CustomEvent = new CustomEvent('canvas.merged', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
detail: { detail: {
duration,
states: objects, states: objects,
}, },
}); });
@ -724,6 +726,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) { } else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas(); this.moveCanvas();
this.transformCanvas(); this.transformCanvas();
if (reason === UpdateReasons.IMAGE_FITTED) {
this.canvas.dispatchEvent(new CustomEvent('canvas.fit', {
bubbles: false,
cancelable: true,
}));
}
} else if (reason === UpdateReasons.IMAGE_MOVED) { } else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas(); this.moveCanvas();
} else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) { } else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
@ -1182,6 +1190,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset); ).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points; this.drawnStates[state.clientID].points = points;
this.canvas.dispatchEvent(new CustomEvent('canvas.dragshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}));
this.onEditDone(state, points); this.onEditDone(state, points);
} }
}); });
@ -1232,6 +1247,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset); ).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points; this.drawnStates[state.clientID].points = points;
this.canvas.dispatchEvent(new CustomEvent('canvas.resizeshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}));
this.onEditDone(state, points); this.onEditDone(state, points);
} }
}); });

@ -31,7 +31,8 @@ export interface DrawHandler {
export class DrawHandlerImpl implements DrawHandler { export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape // callback is used to notify about creating new shape
private onDrawDone: (data: object, continueDraw?: boolean) => void; private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void;
private startTimestamp: number;
private canvas: SVG.Container; private canvas: SVG.Container;
private text: SVG.Container; private text: SVG.Container;
private cursorPosition: { private cursorPosition: {
@ -180,7 +181,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({ this.onDrawDone({
shapeType, shapeType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
}); }, Date.now() - this.startTimestamp);
} }
}).on('drawupdate', (): void => { }).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance); this.shapeSizeElement.update(this.drawInstance);
@ -213,7 +214,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({ this.onDrawDone({
shapeType, shapeType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
}); }, Date.now() - this.startTimestamp);
} }
} }
}).on('undopoint', (): void => { }).on('undopoint', (): void => {
@ -300,7 +301,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({ this.onDrawDone({
shapeType, shapeType,
points, points,
}); }, Date.now() - this.startTimestamp);
} else if (shapeType === 'polyline' } else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD && ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD) || (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
@ -308,13 +309,13 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({ this.onDrawDone({
shapeType, shapeType,
points, points,
}); }, Date.now() - this.startTimestamp);
} else if (shapeType === 'points' } else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') { && (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({ this.onDrawDone({
shapeType, shapeType,
points, points,
}); }, Date.now() - this.startTimestamp);
} }
}); });
} }
@ -365,7 +366,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}, e.detail.originalEvent.ctrlKey); }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
}); });
} }
@ -405,7 +406,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}, e.detail.originalEvent.ctrlKey); }, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
}); });
} }
@ -583,14 +584,16 @@ export class DrawHandlerImpl implements DrawHandler {
this.setupDrawEvents(); this.setupDrawEvents();
} }
this.startTimestamp = Date.now();
this.initialized = true; this.initialized = true;
} }
public constructor( public constructor(
onDrawDone: (data: object, continueDraw?: boolean) => void, onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void,
canvas: SVG.Container, canvas: SVG.Container,
text: SVG.Container, text: SVG.Container,
) { ) {
this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone; this.onDrawDone = onDrawDone;
this.canvas = canvas; this.canvas = canvas;
this.text = text; this.text = text;

@ -15,8 +15,9 @@ export interface MergeHandler {
export class MergeHandlerImpl implements MergeHandler { export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end // callback is used to notify about merging end
private onMergeDone: (objects: any[]) => void; private onMergeDone: (objects: any[] | null, duration?: number) => void;
private onFindObject: (event: MouseEvent) => void; private onFindObject: (event: MouseEvent) => void;
private startTimestamp: number;
private canvas: SVG.Container; private canvas: SVG.Container;
private initialized: boolean; private initialized: boolean;
private statesToBeMerged: any[]; // are being merged private statesToBeMerged: any[]; // are being merged
@ -57,6 +58,7 @@ export class MergeHandlerImpl implements MergeHandler {
private initMerging(): void { private initMerging(): void {
this.canvas.node.addEventListener('click', this.onFindObject); this.canvas.node.addEventListener('click', this.onFindObject);
this.startTimestamp = Date.now();
this.initialized = true; this.initialized = true;
} }
@ -66,7 +68,7 @@ export class MergeHandlerImpl implements MergeHandler {
this.release(); this.release();
if (statesToBeMerged.length > 1) { if (statesToBeMerged.length > 1) {
this.onMergeDone(statesToBeMerged); this.onMergeDone(statesToBeMerged, Date.now() - this.startTimestamp);
} else { } else {
this.onMergeDone(null); this.onMergeDone(null);
// here is a cycle // here is a cycle
@ -77,12 +79,13 @@ export class MergeHandlerImpl implements MergeHandler {
} }
public constructor( public constructor(
onMergeDone: (objects: any[]) => void, onMergeDone: (objects: any[] | null, duration?: number) => void,
onFindObject: (event: MouseEvent) => void, onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container, canvas: SVG.Container,
) { ) {
this.onMergeDone = onMergeDone; this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject; this.onFindObject = onFindObject;
this.startTimestamp = Date.now();
this.canvas = canvas; this.canvas = canvas;
this.statesToBeMerged = []; this.statesToBeMerged = [];
this.highlightedShapes = {}; this.highlightedShapes = {};

@ -14,7 +14,8 @@
function build() { function build() {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const User = require('./user'); const loggerStorage = require('./logger-storage');
const Log = require('./log');
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
@ -41,6 +42,7 @@ function build() {
ServerError, ServerError,
} = require('./exceptions'); } = require('./exceptions');
const User = require('./user');
const pjson = require('../package.json'); const pjson = require('../package.json');
const config = require('./config'); const config = require('./config');
@ -419,6 +421,53 @@ function build() {
return result; return result;
}, },
}, },
/**
* Namespace to working with logs
* @namespace logger
* @memberof module:API.cvat
*/
/**
* Method to logger configuration
* @method configure
* @memberof module:API.cvat.logger
* @param {function} isActiveChecker - callback to know if logger
* should increase working time or not
* @param {object} userActivityCallback - container for a callback <br>
* Logger put here a callback to update user activity timer <br>
* You can call it outside
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Append log to a log collection <br>
* Durable logs will have been added after "close" method is called for them <br>
* Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log
* @memberof module:API.cvat.logger
* @param {module:API.cvat.enums.LogType | string} type - log type
* @param {Object} [payload = {}] - any other data that will be appended to the log
* @param {boolean} [wait = false] - specifies if log is durable
* @returns {module:API.cvat.classes.Log}
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Save accumulated logs on a server
* @method save
* @memberof module:API.cvat.logger
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
*/
logger: loggerStorage,
/** /**
* Namespace contains some changeable configurations * Namespace contains some changeable configurations
* @namespace config * @namespace config
@ -432,12 +481,6 @@ function build() {
* @property {string} proxy Axios proxy settings. * @property {string} proxy Axios proxy settings.
* For more details please read <a href="https://github.com/axios/axios"> here </a> * For more details please read <a href="https://github.com/axios/axios"> here </a>
* @memberof module:API.cvat.config * @memberof module:API.cvat.config
* @property {integer} taskID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} jobID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} clientID read only auto-generated
* value which is displayed in a logs
* @memberof module:API.cvat.config * @memberof module:API.cvat.config
*/ */
get backendAPI() { get backendAPI() {
@ -452,21 +495,6 @@ function build() {
set proxy(value) { set proxy(value) {
config.proxy = value; config.proxy = value;
}, },
get taskID() {
return config.taskID;
},
set taskID(value) {
config.taskID = value;
},
get jobID() {
return config.jobID;
},
set jobID(value) {
config.jobID = value;
},
get clientID() {
return config.clientID;
},
}, },
/** /**
* Namespace contains some library information e.g. api version * Namespace contains some library information e.g. api version
@ -524,6 +552,7 @@ function build() {
Task, Task,
User, User,
Job, Job,
Log,
Attribute, Attribute,
Label, Label,
Statistics, Statistics,

@ -6,7 +6,4 @@
module.exports = { module.exports = {
backendAPI: 'http://localhost:7000/api/v1', backendAPI: 'http://localhost:7000/api/v1',
proxy: false, proxy: false,
taskID: undefined,
jobID: undefined,
clientID: +Date.now().toString().substr(-6),
}; };

@ -103,68 +103,74 @@
}); });
/** /**
* Event types * Logger event types
* @enum {number} * @enum {string}
* @name LogType * @name LogType
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {number} pasteObject 0 * @property {string} loadJob Load job
* @property {number} changeAttribute 1 * @property {string} saveJob Save job
* @property {number} dragObject 2 * @property {string} uploadAnnotations Upload annotations
* @property {number} deleteObject 3 * @property {string} sendUserActivity Send user activity
* @property {number} pressShortcut 4 * @property {string} sendException Send exception
* @property {number} resizeObject 5 * @property {string} sendTaskInfo Send task info
* @property {number} sendLogs 6
* @property {number} saveJob 7 * @property {string} drawObject Draw object
* @property {number} jumpFrame 8 * @property {string} pasteObject Paste object
* @property {number} drawObject 9 * @property {string} copyObject Copy object
* @property {number} changeLabel 10 * @property {string} propagateObject Propagate object
* @property {number} sendTaskInfo 11 * @property {string} dragObject Drag object
* @property {number} loadJob 12 * @property {string} resizeObject Resize object
* @property {number} moveImage 13 * @property {string} deleteObject Delete object
* @property {number} zoomImage 14 * @property {string} lockObject Lock object
* @property {number} lockObject 15 * @property {string} mergeObjects Merge objects
* @property {number} mergeObjects 16 * @property {string} changeAttribute Change attribute
* @property {number} copyObject 17 * @property {string} changeLabel Change label
* @property {number} propagateObject 18
* @property {number} undoAction 19 * @property {string} changeFrame Change frame
* @property {number} redoAction 20 * @property {string} moveImage Move image
* @property {number} sendUserActivity 21 * @property {string} zoomImage Zoom image
* @property {number} sendException 22 * @property {string} fitImage Fit image
* @property {number} changeFrame 23 * @property {string} rotateImage Rotate image
* @property {number} debugInfo 24
* @property {number} fitImage 25 * @property {string} undoAction Undo action
* @property {number} rotateImage 26 * @property {string} redoAction Redo action
* @property {string} pressShortcut Press shortcut
* @property {string} debugInfo Debug info
* @readonly * @readonly
*/ */
const LogType = { const LogType = Object.freeze({
pasteObject: 0, loadJob: 'Load job',
changeAttribute: 1, saveJob: 'Save job',
dragObject: 2, uploadAnnotations: 'Upload annotations',
deleteObject: 3, sendUserActivity: 'Send user activity',
pressShortcut: 4, sendException: 'Send exception',
resizeObject: 5, sendTaskInfo: 'Send task info',
sendLogs: 6,
saveJob: 7, drawObject: 'Draw object',
jumpFrame: 8, pasteObject: 'Paste object',
drawObject: 9, copyObject: 'Copy object',
changeLabel: 10, propagateObject: 'Propagate object',
sendTaskInfo: 11, dragObject: 'Drag object',
loadJob: 12, resizeObject: 'Resize object',
moveImage: 13, deleteObject: 'Delete object',
zoomImage: 14, lockObject: 'Lock object',
lockObject: 15, mergeObjects: 'Merge objects',
mergeObjects: 16, changeAttribute: 'Change attribute',
copyObject: 17, changeLabel: 'Change label',
propagateObject: 18,
undoAction: 19, changeFrame: 'Change frame',
redoAction: 20, moveImage: 'Move image',
sendUserActivity: 21, zoomImage: 'Zoom image',
sendException: 22, fitImage: 'Fit image',
changeFrame: 23, rotateImage: 'Rotate image',
debugInfo: 24,
fitImage: 25, undoAction: 'Undo action',
rotateImage: 26, redoAction: 'Redo action',
};
pressShortcut: 'Press shortcut',
debugInfo: 'Debug info',
});
/** /**
* Types of actions with annotations * Types of actions with annotations
@ -208,7 +214,6 @@
/** /**
* Array of hex colors * Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values
* @name colors * @name colors
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @type {string[]} * @type {string[]}

@ -0,0 +1,209 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
/**
* Class representing a single log
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, payload) {
this.onCloseCallback = null;
this.type = logType;
this.payload = { ...payload };
this.time = new Date();
}
onClose(callback) {
this.onCloseCallback = callback;
}
validatePayload() {
if (typeof (this.payload) !== 'object') {
throw new ArgumentError('Payload must be an object');
}
try {
JSON.stringify(this.payload);
} catch (error) {
const message = `Log payload must be JSON serializable. ${error.toString()}`;
throw new ArgumentError(message);
}
}
dump() {
const payload = { ...this.payload };
const body = {
name: this.type,
time: this.time.toISOString(),
};
for (const field of ['client_id', 'job_id', 'task_id', 'is_active']) {
if (field in payload) {
body[field] = payload[field];
delete payload[field];
}
}
return {
...body,
payload,
};
}
/**
* Method saves a durable log in a storage <br>
* Note then you can call close() multiple times <br>
* Log duration will be computed based on the latest call <br>
* All payloads will be shallowly combined (all top level properties will exist)
* @method close
* @memberof module:API.cvat.classes.Log
* @param {object} [payload] part of payload can be added when close a log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async close(payload = {}) {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close, payload);
return result;
}
}
Log.prototype.close.implementation = function (payload) {
this.payload.duration = Date.now() - this.time.getTime();
this.payload = { ...this.payload, ...payload };
if (this.onCloseCallback) {
this.onCloseCallback();
}
};
class LogWithCount extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
const message = `The field "count" is required for "${this.type}" log`
+ 'It must be a positive integer';
throw new ArgumentError(message);
}
}
}
class LogWithObjectsInfo extends Log {
validatePayload() {
const generateError = (name, range) => {
const message = `The field "${name}" is required for "${this.type}" log. ${range}`;
throw new ArgumentError(message);
};
if (!Number.isInteger(this.payload['track count']) || this.payload['track count'] < 0) {
generateError('track count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['tag count']) || this.payload['tag count'] < 0) {
generateError('tag count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['object count']) || this.payload['object count'] < 0) {
generateError('object count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['frame count']) || this.payload['frame count'] < 1) {
generateError('frame count', 'It must be an integer not less than 1');
}
if (!Number.isInteger(this.payload['box count']) || this.payload['box count'] < 0) {
generateError('box count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['polygon count']) || this.payload['polygon count'] < 0) {
generateError('polygon count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['polyline count']) || this.payload['polyline count'] < 0) {
generateError('polyline count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['points count']) || this.payload['points count'] < 0) {
generateError('points count', 'It must be an integer not less than 0');
}
}
}
class LogWithWorkingTime extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!('working_time' in this.payload)
|| !typeof (this.payload.working_time) === 'number'
|| this.payload.working_time < 0
) {
const message = `The field "working_time" is required for ${this.type} log. `
+ 'It must be a number not less than 0';
throw new ArgumentError(message);
}
}
}
class LogWithExceptionInfo extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (typeof (this.payload.message) !== 'string') {
const message = `The field "message" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
if (typeof (this.payload.filename) !== 'string') {
const message = `The field "filename" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
if (typeof (this.payload.line) !== 'number') {
const message = `The field "line" is required for ${this.type} log. `
+ 'It must be a number';
throw new ArgumentError(message);
}
}
}
function logFactory(logType, payload) {
const logsWithCount = [
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject,
LogType.undoAction, LogType.redoAction,
];
if (logsWithCount.includes(logType)) {
return new LogWithCount(logType, payload);
}
if ([LogType.sendTaskInfo, LogType.loadJob, LogType.uploadAnnotations].includes(logType)) {
return new LogWithObjectsInfo(logType, payload);
}
if (logType === LogType.sendUserActivity) {
return new LogWithWorkingTime(logType, payload);
}
if (logType === LogType.sendException) {
return new LogWithExceptionInfo(logType, payload);
}
return new Log(logType, payload);
}
module.exports = logFactory;

@ -0,0 +1,169 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins');
const server = require('./server-proxy');
const logFactory = require('./log');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
class LoggerStorage {
constructor() {
this.clientID = Date.now().toString().substr(-6);
this.lastLogTime = Date.now();
this.workingTime = 0;
this.collection = [];
this.ignoreRules = {}; // by event
this.isActiveChecker = null;
this.ignoreRules[LogType.zoomImage] = {
lastLog: null,
timeThreshold: 1000,
ignore(previousLog) {
return Date.now() - previousLog.time < this.timeThreshold;
},
};
this.ignoreRules[LogType.changeAttribute] = {
lastLog: null,
ignore(previousLog, currentPayload) {
return currentPayload.object_id === previousLog.payload.object_id
&& currentPayload.id === previousLog.payload.id;
},
};
}
updateWorkingTime() {
if (!this.isActiveChecker || this.isActiveChecker()) {
const lastLogTime = Date.now();
const diff = lastLogTime - this.lastLogTime;
this.workingTime += diff < WORKING_TIME_THRESHOLD ? diff : 0;
this.lastLogTime = lastLogTime;
}
}
async configure(isActiveChecker, activityHelper) {
const result = await PluginRegistry
.apiWrapper.call(
this, LoggerStorage.prototype.configure,
isActiveChecker, activityHelper,
);
return result;
}
async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
return result;
}
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.save);
return result;
}
}
LoggerStorage.prototype.configure.implementation = function (
isActiveChecker,
userActivityCallback,
) {
if (typeof (isActiveChecker) !== 'function') {
throw new ArgumentError('isActiveChecker argument must be callable');
}
if (!Array.isArray(userActivityCallback)) {
throw new ArgumentError('userActivityCallback argument must be an array');
}
this.isActiveChecker = () => !!isActiveChecker();
userActivityCallback.push(this.updateWorkingTime.bind(this));
};
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
if (typeof (payload) !== 'object') {
throw new ArgumentError('Payload must be an object');
}
if (typeof (wait) !== 'boolean') {
throw new ArgumentError('Payload must be an object');
}
if (logType in this.ignoreRules) {
const ignoreRule = this.ignoreRules[logType];
const { lastLog } = ignoreRule;
if (lastLog && ignoreRule.ignore(lastLog, payload)) {
lastLog.payload = {
...lastLog.payload,
...payload,
};
this.updateWorkingTime();
return ignoreRule.lastLog;
}
}
const logPayload = { ...payload };
logPayload.client_id = this.clientID;
if (this.isActiveChecker) {
logPayload.is_active = this.isActiveChecker();
}
const log = logFactory(logType, { ...logPayload });
if (logType in this.ignoreRules) {
this.ignoreRules[logType].lastLog = log;
}
const pushEvent = () => {
this.updateWorkingTime();
log.validatePayload();
log.onClose(null);
this.collection.push(log);
};
if (wait) {
log.onClose(pushEvent);
} else {
pushEvent();
}
return log;
};
LoggerStorage.prototype.save.implementation = async function () {
const collectionToSend = [...this.collection];
const lastLog = this.collection[this.collection.length - 1];
const logPayload = {};
logPayload.client_id = this.clientID;
logPayload.working_time = this.workingTime;
if (this.isActiveChecker) {
logPayload.is_active = this.isActiveChecker();
}
if (lastLog && lastLog.type === LogType.sendTaskInfo) {
logPayload.job_id = lastLog.payload.job_id;
logPayload.task_id = lastLog.payload.task_id;
}
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
collectionToSend.push(userActivityLog);
await server.logs.save(collectionToSend.map((log) => log.dump()));
for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null;
}
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
};
module.exports = new LoggerStorage();

@ -1,42 +0,0 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
/**
* Class describe scheme of a log object
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, continuous, details) {
this.type = logType;
this.continuous = continuous;
this.details = details;
}
/**
* Method closes a continue log
* @method close
* @memberof module:API.cvat.classes.Log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async close() {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close);
return result;
}
}
module.exports = Log;
})();

@ -584,6 +584,21 @@
}); });
} }
async function saveLogs(logs) {
const { backendAPI } = config;
try {
await Axios.post(`${backendAPI}/server/logs`, JSON.stringify(logs), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(this, Object.freeze({ Object.defineProperties(this, Object.freeze({
server: { server: {
value: Object.freeze({ value: Object.freeze({
@ -646,6 +661,13 @@
}), }),
writable: false, writable: false,
}, },
logs: {
value: Object.freeze({
save: saveLogs,
}),
writable: false,
},
})); }));
} }
} }

@ -9,6 +9,7 @@
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { getFrame, getPreview } = require('./frames'); const { getFrame, getPreview } = require('./frames');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
@ -125,16 +126,11 @@
}, },
writable: true, writable: true,
}), }),
logs: Object.freeze({ logger: Object.freeze({
value: { value: {
async put(logType, details) { async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.put, logType, details); .apiWrapper.call(this, prototype.logger.log, logType, payload, wait);
return result;
},
async save(onUpdate) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.save, onUpdate);
return result; return result;
}, },
}, },
@ -436,33 +432,28 @@
/** /**
* Namespace is used for an interaction with logs * Namespace is used for an interaction with logs
* @namespace logs * @namespace logger
* @memberof Session * @memberof Session
*/ */
/** /**
* Append log to a log collection. * Create a log and add it to a log collection <br>
* Continue logs will have been added after "close" method is called * Durable logs will be added after "close" method is called for them <br>
* @method put * The fields "task_id" and "job_id" automatically added when add logs
* @memberof Session.logs * throught a task or a job <br>
* @param {module:API.cvat.enums.LogType} type a type of a log * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* @param {boolean} continuous log is a continuous log * Payload of ignored logs are shallowly combined to previous logs of the same type
* @param {Object} details any others data which will be append to log data * @method log
* @memberof Session.logger
* @param {module:API.cvat.enums.LogType | string} type - log type
* @param {Object} [payload = {}] - any other data that will be appended to the log
* @param {boolean} [wait = false] - specifies if log is durable
* @returns {module:API.cvat.classes.Log} * @returns {module:API.cvat.classes.Log}
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
/**
* Save accumulated logs on a server
* @method save
* @memberof Session.logs
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
*/
/** /**
* Namespace is used for an interaction with actions * Namespace is used for an interaction with actions
@ -702,6 +693,10 @@
get: Object.getPrototypeOf(this).frames.get.bind(this), get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this),
}; };
this.logger = {
log: Object.getPrototypeOf(this).logger.log.bind(this),
};
} }
/** /**
@ -1212,6 +1207,10 @@
get: Object.getPrototypeOf(this).frames.get.bind(this), get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this), preview: Object.getPrototypeOf(this).frames.preview.bind(this),
}; };
this.logger = {
log: Object.getPrototypeOf(this).logger.log.bind(this),
};
} }
/** /**
@ -1452,6 +1451,11 @@
return result; return result;
}; };
Job.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await this.task.logger.log(logType, { ...payload, job_id: this.id }, wait);
return result;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') { if (typeof (this.id) !== 'undefined') {
@ -1663,4 +1667,9 @@
const result = getActions(this); const result = getActions(this);
return result; return result;
}; };
Task.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait);
return result;
};
})(); })();

@ -23,6 +23,7 @@ import {
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas'; import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
@ -89,6 +90,23 @@ function computeZRange(states: any[]): number[] {
return [minZ, maxZ]; return [minZ, maxZ];
} }
async function jobInfoGenerator(job: any): Promise<Record<string, number>> {
const { total } = await job.annotations.statistics();
return {
'frame count': job.stopFrame - job.startFrame + 1,
'track count': total.rectangle.shape + total.rectangle.track
+ total.polygon.shape + total.polygon.track
+ total.polyline.shape + total.polyline.track
+ total.points.shape + total.points.track,
'object count': total.total,
'box count': total.rectangle.shape + total.rectangle.track,
'polygon count': total.polygon.shape + total.polygon.track,
'polyline count': total.polyline.shape + total.polyline.track,
'points count': total.points.shape + total.points.track,
'tag count': total.tags,
};
}
export enum AnnotationActionTypes { export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB', GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS', GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
@ -166,6 +184,28 @@ export enum AnnotationActionTypes {
ADD_Z_LAYER = 'ADD_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED', SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE', CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
}
export function saveLogsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>) => {
try {
await logger.save();
dispatch({
type: AnnotationActionTypes.SAVE_LOGS_SUCCESS,
payload: {},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.SAVE_LOGS_FAILED,
payload: {
error,
},
});
}
};
} }
export function changeWorkspace(workspace: Workspace): AnyAction { export function changeWorkspace(workspace: Workspace): AnyAction {
@ -193,8 +233,7 @@ export function switchZLayer(cur: number): AnyAction {
}; };
} }
export function fetchAnnotationsAsync(): export function fetchAnnotationsAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const { const {
@ -251,14 +290,21 @@ export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization // TODO: use affected IDs as an optimization
const [undoName] = state.annotation.annotations.history.undo.slice(-1);
const undoLog = await sessionInstance.logger.log(LogType.undoAction, {
name: undoName,
count: 1,
}, true);
await sessionInstance.actions.undo(); await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
await undoLog.close();
dispatch({ dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS, type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
@ -284,14 +330,21 @@ export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization // TODO: use affected IDs as an optimization
const [redoName] = state.annotation.annotations.history.redo.slice(-1);
const redoLog = await sessionInstance.logger.log(LogType.redoAction, {
name: redoName,
count: 1,
}, true);
await sessionInstance.actions.redo(); await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
await redoLog.close();
dispatch({ dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS, type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
@ -382,6 +435,12 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
const frame = state.annotation.player.frame.number; const frame = state.annotation.player.frame.number;
await job.annotations.upload(file, loader); await job.annotations.upload(file, loader);
await job.logger.log(
LogType.uploadAnnotations, {
...(await jobInfoGenerator(job)),
},
);
// One more update to escape some problems // One more update to escape some problems
// in canvas when shape with the same // in canvas when shape with the same
// clientID has different type (polygon, rectangle) for example // clientID has different type (polygon, rectangle) for example
@ -508,6 +567,9 @@ export function propagateObjectAsync(
frame: from, frame: from,
}; };
await sessionInstance.logger.log(
LogType.propagateObject, { count: to - from + 1 },
);
const states = []; const states = [];
for (let frame = from; frame <= to; frame++) { for (let frame = from; frame <= to; frame++) {
copy.frame = frame; copy.frame = frame;
@ -558,6 +620,7 @@ export function removeObjectAsync(sessionInstance: any, objectState: any, force:
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
await sessionInstance.logger.log(LogType.deleteObject, { count: 1 });
const removed = await objectState.delete(force); const removed = await objectState.delete(force);
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
@ -593,6 +656,9 @@ export function editShape(enabled: boolean): AnyAction {
} }
export function copyShape(objectState: any): AnyAction { export function copyShape(objectState: any): AnyAction {
const job = getStore().getState().annotation.job.instance;
job.logger.log(LogType.copyObject, { count: 1 });
return { return {
type: AnnotationActionTypes.COPY_SHAPE, type: AnnotationActionTypes.COPY_SHAPE,
payload: { payload: {
@ -696,6 +762,12 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
payload: {}, payload: {},
}); });
await job.logger.log(
LogType.changeFrame, {
from: frame,
to: toFrame,
},
);
const data = await job.frames.get(toFrame); const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters); const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
@ -716,6 +788,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
} }
const delay = Math.max(0, Math.round(1000 / frameSpeed) const delay = Math.max(0, Math.round(1000 / frameSpeed)
- currentTime + (state.annotation.player.frame.changeTime as number)); - currentTime + (state.annotation.player.frame.changeTime as number));
dispatch({ dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: { payload: {
@ -743,14 +816,33 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function rotateCurrentFrame(rotation: Rotation): AnyAction { export function rotateCurrentFrame(rotation: Rotation): AnyAction {
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { number: frameNumber } = state.annotation.player.frame; const {
const { startFrame } = state.annotation.job.instance; annotation: {
const { frameAngles } = state.annotation.player; player: {
const { rotateAll } = state.settings.player; frame: {
number: frameNumber,
},
frameAngles,
},
job: {
instance: job,
instance: {
startFrame,
},
},
},
settings: {
player: {
rotateAll,
},
},
} = state;
const frameAngle = (frameAngles[frameNumber - startFrame] const frameAngle = (frameAngles[frameNumber - startFrame]
+ (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360; + (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360;
job.logger.log(LogType.rotateImage, { angle: frameAngle });
return { return {
type: AnnotationActionTypes.ROTATE_FRAME, type: AnnotationActionTypes.ROTATE_FRAME,
payload: { payload: {
@ -800,11 +892,6 @@ export function getJobAsync(
initialFilters: string[], initialFilters: string[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> { ): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
});
try { try {
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const filters = initialFilters; const filters = initialFilters;
@ -817,6 +904,18 @@ export function getJobAsync(
}); });
} }
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
});
const loadJobEvent = await logger.log(
LogType.loadJob, {
task_id: tid,
job_id: jid,
}, true,
);
// Check state if the task is already there // Check state if the task is already there
let task = state.tasks.current let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid) .filter((_task: Task) => _task.instance.id === tid)
@ -841,6 +940,8 @@ export function getJobAsync(
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors]; const colors = [...cvat.enums.colors];
loadJobEvent.close(await jobInfoGenerator(job));
dispatch({ dispatch({
type: AnnotationActionTypes.GET_JOB_SUCCESS, type: AnnotationActionTypes.GET_JOB_SUCCESS,
payload: { payload: {
@ -874,6 +975,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}); });
try { try {
const saveJobEvent = await sessionInstance.logger.log(
LogType.saveJob, {}, true,
);
await sessionInstance.annotations.save((status: string) => { await sessionInstance.annotations.save((status: string) => {
dispatch({ dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS, type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
@ -883,6 +988,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}); });
}); });
await saveJobEvent.close();
await sessionInstance.logger.log(
LogType.sendTaskInfo,
await jobInfoGenerator(sessionInstance),
);
dispatch(saveLogsAsync());
dispatch({ dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: {}, payload: {},
@ -898,6 +1010,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
// used to reproduce the latest drawing (in case of tags just creating) by using N
export function rememberObject( export function rememberObject(
objectType: ObjectType, objectType: ObjectType,
labelID: number, labelID: number,

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React from 'react'; import React, { useEffect } from 'react';
import { import {
Layout, Layout,
@ -21,18 +21,24 @@ interface Props {
job: any | null | undefined; job: any | null | undefined;
fetching: boolean; fetching: boolean;
getJob(): void; getJob(): void;
saveLogs(): void;
workspace: Workspace; workspace: Workspace;
} }
export default function AnnotationPageComponent(props: Props): JSX.Element { export default function AnnotationPageComponent(props: Props): JSX.Element {
const { const {
job, job,
fetching, fetching,
getJob, getJob,
saveLogs,
workspace, workspace,
} = props; } = props;
useEffect(() => {
saveLogs();
return saveLogs;
}, []);
if (job === null) { if (job === null) {
if (!fetching) { if (!fetching) {
getJob(); getJob();

@ -11,6 +11,7 @@ import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { LogType } from 'cvat-logger';
import { import {
activateObject as activateObjectAction, activateObject as activateObjectAction,
updateAnnotationsAsync, updateAnnotationsAsync,
@ -28,6 +29,7 @@ interface StateToProps {
activatedAttributeID: number | null; activatedAttributeID: number | null;
states: any[]; states: any[];
labels: any[]; labels: any[];
jobInstance: any;
} }
interface DispatchToProps { interface DispatchToProps {
@ -48,12 +50,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
states, states,
}, },
job: { job: {
instance: jobInstance,
labels, labels,
}, },
}, },
} = state; } = state;
return { return {
jobInstance,
labels, labels,
activatedStateID, activatedStateID,
activatedAttributeID, activatedAttributeID,
@ -78,6 +82,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
states, states,
activatedStateID, activatedStateID,
activatedAttributeID, activatedAttributeID,
jobInstance,
updateAnnotations, updateAnnotations,
activateObject, activateObject,
} = props; } = props;
@ -267,6 +272,13 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
currentValue={activeObjectState.attributes[activeAttribute.id]} currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => { onChange={(value: string) => {
const { attributes } = activeObjectState; const { attributes } = activeObjectState;
jobInstance.logger.log(
LogType.changeAttribute, {
id: activeAttribute.id,
object_id: activeObjectState.clientID,
value,
},
);
attributes[activeAttribute.id] = value; attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes; activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]); updateAnnotations([activeObjectState]);

@ -21,6 +21,7 @@ import {
Workspace, Workspace,
ShapeType, ShapeType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas'; import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
@ -223,6 +224,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated); canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved); canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().removeEventListener('canvas.zoom', this.onCanvasZoomChanged);
canvasInstance.html().removeEventListener('canvas.fit', this.onCanvasImageFitted);
canvasInstance.html().removeEventListener('canvas.dragshape', this.onCanvasShapeDragged);
canvasInstance.html().removeEventListener('canvas.resizeshape', this.onCanvasShapeResized);
canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked); canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn); canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged); canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
@ -246,20 +251,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onShapeDrawn(); onShapeDrawn();
} }
const { state } = event.detail; const { state, duration } = event.detail;
if (!state.objectType) { const isDrawnFromScratch = !state.label;
state.objectType = activeObjectType; if (isDrawnFromScratch) {
} jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
} else {
if (!state.label) { jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
[state.label] = jobInstance.task.labels
.filter((label: any) => label.id === activeLabelID);
}
if (typeof (state.occluded) === 'undefined') {
state.occluded = false;
} }
state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.task.labels
.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false;
state.frame = frame; state.frame = frame;
const objectState = new cvat.classes.ObjectState(state); const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]); onCreateAnnotations(jobInstance, frame, [objectState]);
@ -275,7 +278,11 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onMergeObjects(false); onMergeObjects(false);
const { states } = event.detail; const { states, duration } = event.detail;
jobInstance.logger.log(LogType.mergeObjects, {
duration,
count: states.length,
});
onMergeAnnotations(jobInstance, frame, states); onMergeAnnotations(jobInstance, frame, states);
}; };
@ -342,6 +349,28 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
}; };
private onCanvasShapeDragged = (e: any): void => {
const { jobInstance } = this.props;
const { id } = e.detail;
jobInstance.logger.log(LogType.dragObject, { id });
};
private onCanvasShapeResized = (e: any): void => {
const { jobInstance } = this.props;
const { id } = e.detail;
jobInstance.logger.log(LogType.resizeObject, { id });
};
private onCanvasImageFitted = (): void => {
const { jobInstance } = this.props;
jobInstance.logger.log(LogType.fitImage);
};
private onCanvasZoomChanged = (): void => {
const { jobInstance } = this.props;
jobInstance.logger.log(LogType.zoomImage);
};
private onCanvasShapeClicked = (e: any): void => { private onCanvasShapeClicked = (e: any): void => {
const { clientID } = e.detail.state; const { clientID } = e.detail.state;
const sidebarItem = window.document const sidebarItem = window.document
@ -613,6 +642,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated); canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved); canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().addEventListener('canvas.zoom', this.onCanvasZoomChanged);
canvasInstance.html().addEventListener('canvas.fit', this.onCanvasImageFitted);
canvasInstance.html().addEventListener('canvas.dragshape', this.onCanvasShapeDragged);
canvasInstance.html().addEventListener('canvas.resizeshape', this.onCanvasShapeResized);
canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked); canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn); canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged); canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);

@ -27,6 +27,7 @@ import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from 'containers/register-page/register-page'; import RegisterPageContainer from 'containers/register-page/register-page';
import HeaderContainer from 'containers/header/header'; import HeaderContainer from 'containers/header/header';
import getCore from 'cvat-core';
import { NotificationsState } from 'reducers/interfaces'; import { NotificationsState } from 'reducers/interfaces';
interface CVATAppProps { interface CVATAppProps {
@ -56,8 +57,17 @@ interface CVATAppProps {
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> { class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void { public componentDidMount(): void {
const core = getCore();
const { verifyAuthorized } = this.props; const { verifyAuthorized } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false }); configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
// Logger configuration
const userActivityCallback: (() => void)[] = [];
window.addEventListener('click', () => {
userActivityCallback.forEach((handler) => handler());
});
core.logger.configure(() => window.document.hasFocus, userActivityCallback);
verifyAuthorized(); verifyAuthorized();
} }
@ -198,7 +208,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const readyForRender = (userInitialized && user == null) const readyForRender = (userInitialized && user == null)
|| (userInitialized && formatsInitialized || (userInitialized && formatsInitialized
&& pluginsInitialized && usersInitialized && aboutInitialized); && pluginsInitialized && usersInitialized && aboutInitialized);
const withModels = installedAutoAnnotation const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation; || installedTFAnnotation || installedTFSegmentation;
@ -253,15 +263,15 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Route exact path='/tasks/create' component={CreateTaskPageContainer} /> <Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} /> <Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route exact path='/tasks/:tid/jobs/:jid' component={AnnotationPageContainer} /> <Route exact path='/tasks/:tid/jobs/:jid' component={AnnotationPageContainer} />
{ withModels {withModels
&& <Route exact path='/models' component={ModelsPageContainer} /> } && <Route exact path='/models' component={ModelsPageContainer} />}
{ installedAutoAnnotation {installedAutoAnnotation
&& <Route exact path='/models/create' component={CreateModelPageContainer} /> } && <Route exact path='/models/create' component={CreateModelPageContainer} />}
<Redirect push to='/tasks' /> <Redirect push to='/tasks' />
</Switch> </Switch>
</GlobalHotKeys> </GlobalHotKeys>
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/> <a id='downloadAnchor' style={{ display: 'none' }} download />
</Layout.Content> </Layout.Content>
</Layout> </Layout>
); );

@ -7,7 +7,7 @@ import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page'; import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync } from 'actions/annotation-actions'; import { getJobAsync, saveLogsAsync } from 'actions/annotation-actions';
import { CombinedState, Workspace } from 'reducers/interfaces'; import { CombinedState, Workspace } from 'reducers/interfaces';
@ -24,6 +24,7 @@ interface StateToProps {
interface DispatchToProps { interface DispatchToProps {
getJob(): void; getJob(): void;
saveLogs(): void;
} }
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -77,6 +78,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
getJob(): void { getJob(): void {
dispatch(getJobAsync(taskID, jobID, initialFrame, initialFilters)); dispatch(getJobAsync(taskID, jobID, initialFrame, initialFilters));
}, },
saveLogs(): void {
dispatch(saveLogsAsync());
},
}; };
} }

@ -5,6 +5,8 @@
import React from 'react'; import React from 'react';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { LogType } from 'cvat-logger';
import { import {
ActiveControl, ActiveControl,
CombinedState, CombinedState,
@ -292,13 +294,15 @@ class ObjectItemContainer extends React.PureComponent<Props> {
}; };
private lock = (): void => { private lock = (): void => {
const { objectState } = this.props; const { objectState, jobInstance } = this.props;
jobInstance.logger.log(LogType.lockObject, { locked: true });
objectState.lock = true; objectState.lock = true;
this.commit(); this.commit();
}; };
private unlock = (): void => { private unlock = (): void => {
const { objectState } = this.props; const { objectState, jobInstance } = this.props;
jobInstance.logger.log(LogType.lockObject, { locked: false });
objectState.lock = false; objectState.lock = false;
this.commit(); this.commit();
}; };
@ -405,7 +409,12 @@ class ObjectItemContainer extends React.PureComponent<Props> {
}; };
private changeAttribute = (id: number, value: string): void => { private changeAttribute = (id: number, value: string): void => {
const { objectState } = this.props; const { objectState, jobInstance } = this.props;
jobInstance.logger.log(LogType.changeAttribute, {
id,
value,
object_id: objectState.clientID,
});
const attr: Record<number, string> = {}; const attr: Record<number, string> = {};
attr[id] = value; attr[id] = value;
objectState.attributes = attr; objectState.attributes = attr;

@ -0,0 +1,14 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import getCore from 'cvat-core';
const core = getCore();
const { logger } = core;
const { LogType } = core.enums;
export default logger;
export {
LogType,
};

@ -230,6 +230,7 @@ export interface NotificationsState {
undo: null | ErrorState; undo: null | ErrorState;
redo: null | ErrorState; redo: null | ErrorState;
search: null | ErrorState; search: null | ErrorState;
savingLogs: null | ErrorState;
}; };
[index: string]: any; [index: string]: any;

@ -74,6 +74,7 @@ const defaultState: NotificationsState = {
undo: null, undo: null,
redo: null, redo: null,
search: null, search: null,
savingLogs: null,
}, },
}, },
messages: { messages: {
@ -766,6 +767,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
}, },
}; };
} }
case AnnotationActionTypes.SAVE_LOGS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
savingLogs: {
message: 'Could not send logs to the server',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: { case NotificationsActionType.RESET_ERRORS: {
return { return {
...state, ...state,

@ -86,8 +86,8 @@ module.exports = {
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: "./src/index.html", template: "./src/index.html",
inject: false, inject: false,
}), }),
new Dotenv({ new Dotenv({
systemvars: true, systemvars: true,

@ -107,17 +107,17 @@ def dump_frame_anno(frame_annotation):
return ET.tostring(root_elem, encoding='unicode', pretty_print=True) return ET.tostring(root_elem, encoding='unicode', pretty_print=True)
def dump_as_labelme_annotation(file_object, annotations): def dump_as_labelme_annotation(file_object, annotations):
import os.path as osp
from zipfile import ZipFile, ZIP_DEFLATED from zipfile import ZipFile, ZIP_DEFLATED
with ZipFile(file_object, 'w', compression=ZIP_DEFLATED) as output_zip: with ZipFile(file_object, 'w', compression=ZIP_DEFLATED) as output_zip:
for frame_annotation in annotations.group_by_frame(): for frame_annotation in annotations.group_by_frame():
xml_data = dump_frame_anno(frame_annotation) xml_data = dump_frame_anno(frame_annotation)
filename = frame_annotation.name filename = osp.splitext(frame_annotation.name)[0] + '.xml'
filename = filename[ : filename.rfind('.')] + '.xml'
output_zip.writestr(filename, xml_data) output_zip.writestr(filename, xml_data)
def parse_xml_annotations(xml_data, annotations, input_zip): def parse_xml_annotations(xml_data, annotations, input_zip):
from cvat.apps.annotation.coco import mask_to_polygon from datumaro.util.mask_tools import mask_to_polygons
from io import BytesIO from io import BytesIO
from lxml import etree as ET from lxml import etree as ET
import numpy as np import numpy as np
@ -229,7 +229,7 @@ def parse_xml_annotations(xml_data, annotations, input_zip):
mask = input_zip.read(osp.join(_MASKS_DIR, mask_file)) mask = input_zip.read(osp.join(_MASKS_DIR, mask_file))
mask = np.asarray(Image.open(BytesIO(mask)).convert('L')) mask = np.asarray(Image.open(BytesIO(mask)).convert('L'))
mask = (mask != 0) mask = (mask != 0)
polygons = mask_to_polygon(mask) polygons = mask_to_polygons(mask)
for polygon in polygons: for polygon in polygons:
ann_items.append(annotations.LabeledShape( ann_items.append(annotations.LabeledShape(

@ -2,7 +2,7 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from openvino.inference_engine import IENetwork, IEPlugin from openvino.inference_engine import IENetwork, IEPlugin, IECore, get_version
import subprocess import subprocess
import os import os
@ -19,7 +19,20 @@ def _check_instruction(instruction):
) )
def make_plugin(): def make_plugin_or_core():
version = get_version()
use_core_openvino = False
try:
major, minor, reference = [int(x) for x in version.split('.')]
if major >= 2 and minor >= 1 and reference >= 37988:
use_core_openvino = True
except Exception:
pass
if use_core_openvino:
ie = IECore()
return ie
if _IE_PLUGINS_PATH is None: if _IE_PLUGINS_PATH is None:
raise OSError('Inference engine plugin path env not found in the system.') raise OSError('Inference engine plugin path env not found in the system.')

@ -8,25 +8,22 @@ import cv2
import os import os
import numpy as np import numpy as np
from cvat.apps.auto_annotation.inference_engine import make_plugin, make_network from cvat.apps.auto_annotation.inference_engine import make_plugin_or_core, make_network
class ModelLoader(): class ModelLoader():
def __init__(self, model, weights): def __init__(self, model, weights):
self._model = model self._model = model
self._weights = weights self._weights = weights
IE_PLUGINS_PATH = os.getenv("IE_PLUGINS_PATH") core_or_plugin = make_plugin_or_core()
if not IE_PLUGINS_PATH:
raise OSError("Inference engine plugin path env not found in the system.")
plugin = make_plugin()
network = make_network(self._model, self._weights) network = make_network(self._model, self._weights)
supported_layers = plugin.get_supported_layers(network) if getattr(core_or_plugin, 'get_supported_layers', False):
not_supported_layers = [l for l in network.layers.keys() if l not in supported_layers] supported_layers = core_or_plugin.get_supported_layers(network)
if len(not_supported_layers) != 0: not_supported_layers = [l for l in network.layers.keys() if l not in supported_layers]
raise Exception("Following layers are not supported by the plugin for specified device {}:\n {}". if len(not_supported_layers) != 0:
format(plugin.device, ", ".join(not_supported_layers))) raise Exception("Following layers are not supported by the plugin for specified device {}:\n {}".
format(core_or_plugin.device, ", ".join(not_supported_layers)))
iter_inputs = iter(network.inputs) iter_inputs = iter(network.inputs)
self._input_blob_name = next(iter_inputs) self._input_blob_name = next(iter_inputs)
@ -45,7 +42,12 @@ class ModelLoader():
if self._input_blob_name in info_names: if self._input_blob_name in info_names:
self._input_blob_name = next(iter_inputs) self._input_blob_name = next(iter_inputs)
self._net = plugin.load(network=network, num_requests=2) if getattr(core_or_plugin, 'load_network', False):
self._net = core_or_plugin.load_network(network,
"CPU",
num_requests=2)
else:
self._net = core_or_plugin.load(network=network, num_requests=2)
input_type = network.inputs[self._input_blob_name] input_type = network.inputs[self._input_blob_name]
self._input_layout = input_type if isinstance(input_type, list) else input_type.shape self._input_layout = input_type if isinstance(input_type, list) else input_type.shape

@ -91,7 +91,9 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
@staticmethod @staticmethod
def _load_categories(cvat_anno): def _load_categories(cvat_anno):
categories = {} categories = {}
label_categories = datumaro.LabelCategories()
label_categories = datumaro.LabelCategories(
attributes=['occluded', 'z_order'])
for _, label in cvat_anno.meta['task']['labels']: for _, label in cvat_anno.meta['task']['labels']:
label_categories.add(label['name']) label_categories.add(label['name'])
@ -144,6 +146,8 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
anno_group = shape_obj.group anno_group = shape_obj.group
anno_label = map_label(shape_obj.label) anno_label = map_label(shape_obj.label)
anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes) anno_attr = convert_attrs(shape_obj.label, shape_obj.attributes)
anno_attr['occluded'] = shape_obj.occluded
anno_attr['z_order'] = shape_obj.z_order
anno_points = shape_obj.points anno_points = shape_obj.points
if shape_obj.type == ShapeType.POINTS: if shape_obj.type == ShapeType.POINTS:
@ -177,6 +181,8 @@ class CvatTaskExtractor(CvatAnnotationsExtractor):
def match_frame(item, cvat_task_anno): def match_frame(item, cvat_task_anno):
is_video = cvat_task_anno.meta['task']['mode'] == 'interpolation'
frame_number = None frame_number = None
if frame_number is None: if frame_number is None:
try: try:
@ -193,6 +199,8 @@ def match_frame(item, cvat_task_anno):
frame_number = int(item.id) frame_number = int(item.id)
except Exception: except Exception:
pass pass
if frame_number is None and is_video and item.id.startswith('frame_'):
frame_number = int(item.id[len('frame_'):])
if not frame_number in cvat_task_anno.frame_info: if not frame_number in cvat_task_anno.frame_info:
raise Exception("Could not match item id: '%s' with any task frame" % raise Exception("Could not match item id: '%s' with any task frame" %
item.id) item.id)
@ -234,7 +242,7 @@ def import_dm_annotations(dm_dataset, cvat_task_anno):
frame=frame_number, frame=frame_number,
label=label_cat.items[ann.label].name, label=label_cat.items[ann.label].name,
points=ann.points, points=ann.points,
occluded=False, occluded=ann.attributes.get('occluded') == True,
group=group_map.get(ann.group, 0), group=group_map.get(ann.group, 0),
attributes=[cvat_task_anno.Attribute(name=n, value=str(v)) attributes=[cvat_task_anno.Attribute(name=n, value=str(v))
for n, v in ann.attributes.items()], for n, v in ann.attributes.items()],

@ -3,7 +3,7 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from cvat.apps.auto_annotation.inference_engine import make_plugin, make_network from cvat.apps.auto_annotation.inference_engine import make_plugin_or_core, make_network
import os import os
import cv2 import cv2
@ -32,12 +32,15 @@ class DEXTR_HANDLER:
def handle(self, im_path, points): def handle(self, im_path, points):
# Lazy initialization # Lazy initialization
if not self._plugin: if not self._plugin:
self._plugin = make_plugin() self._plugin = make_plugin_or_core()
self._network = make_network(os.path.join(_DEXTR_MODEL_DIR, 'dextr.xml'), self._network = make_network(os.path.join(_DEXTR_MODEL_DIR, 'dextr.xml'),
os.path.join(_DEXTR_MODEL_DIR, 'dextr.bin')) os.path.join(_DEXTR_MODEL_DIR, 'dextr.bin'))
self._input_blob = next(iter(self._network.inputs)) self._input_blob = next(iter(self._network.inputs))
self._output_blob = next(iter(self._network.outputs)) self._output_blob = next(iter(self._network.outputs))
self._exec_network = self._plugin.load(network=self._network) if getattr(self._plugin, 'load_network', False):
self._exec_network = self._plugin.load_network(self._network, 'CPU')
else:
self._exec_network = self._plugin.load(network=self._network)
image = PIL.Image.open(im_path) image = PIL.Image.open(im_path)
numpy_image = np.array(image) numpy_image = np.array(image)

@ -2655,6 +2655,15 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
"points": [20.0, 0.1, 10, 3.22, 4, 7, 10, 30, 1, 2, 4.44, 5.55], "points": [20.0, 0.1, 10, 3.22, 4, 7, 10, 30, 1, 2, 4.44, 5.55],
"type": "polygon", "type": "polygon",
"occluded": True "occluded": True
},
{
"frame": 2,
"label_id": task["labels"][1]["id"],
"group": 1,
"attributes": [],
"points": [4, 7, 10, 30, 4, 5.55],
"type": "polygon",
"occluded": False
}] }]
tags_wo_attrs = [{ tags_wo_attrs = [{
@ -2711,6 +2720,12 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
elif annotation_format == "MOT CSV 1.0": elif annotation_format == "MOT CSV 1.0":
annotations["tracks"] = rectangle_tracks_wo_attrs annotations["tracks"] = rectangle_tracks_wo_attrs
elif annotation_format == "LabelMe ZIP 3.0 for images":
annotations["shapes"] = rectangle_shapes_with_attrs + \
rectangle_shapes_wo_attrs + \
polygon_shapes_wo_attrs + \
polygon_shapes_with_attrs
return annotations return annotations
response = self._get_annotation_formats(annotator) response = self._get_annotation_formats(annotator)

@ -30,7 +30,7 @@ def load_image_into_numpy(image):
def run_inference_engine_annotation(image_list, labels_mapping, treshold): def run_inference_engine_annotation(image_list, labels_mapping, treshold):
from cvat.apps.auto_annotation.inference_engine import make_plugin, make_network from cvat.apps.auto_annotation.inference_engine import make_plugin_or_core, make_network
def _normalize_box(box, w, h, dw, dh): def _normalize_box(box, w, h, dw, dh):
xmin = min(int(box[0] * dw * w), w) xmin = min(int(box[0] * dw * w), w)
@ -44,11 +44,14 @@ def run_inference_engine_annotation(image_list, labels_mapping, treshold):
if MODEL_PATH is None: if MODEL_PATH is None:
raise OSError('Model path env not found in the system.') raise OSError('Model path env not found in the system.')
plugin = make_plugin() core_or_plugin = make_plugin_or_core()
network = make_network('{}.xml'.format(MODEL_PATH), '{}.bin'.format(MODEL_PATH)) network = make_network('{}.xml'.format(MODEL_PATH), '{}.bin'.format(MODEL_PATH))
input_blob_name = next(iter(network.inputs)) input_blob_name = next(iter(network.inputs))
output_blob_name = next(iter(network.outputs)) output_blob_name = next(iter(network.outputs))
executable_network = plugin.load(network=network) if getattr(core_or_plugin, 'load_network', False):
executable_network = core_or_plugin.load_network(network, 'CPU')
else:
executable_network = core_or_plugin.load(network=network)
job = rq.get_current_job() job = rq.get_current_job()
del network del network

@ -329,20 +329,24 @@ class _KeypointsConverter(_InstancesConverter):
label_categories = dataset.categories().get(AnnotationType.label) label_categories = dataset.categories().get(AnnotationType.label)
if label_categories is None: if label_categories is None:
return return
points_categories = dataset.categories().get(AnnotationType.points) point_categories = dataset.categories().get(AnnotationType.points)
if points_categories is None:
return
for idx, kp_cat in points_categories.items.items():
label_cat = label_categories.items[idx]
for idx, label_cat in enumerate(label_categories.items):
cat = { cat = {
'id': 1 + idx, 'id': 1 + idx,
'name': _cast(label_cat.name, str, ''), 'name': _cast(label_cat.name, str, ''),
'supercategory': _cast(label_cat.parent, str, ''), 'supercategory': _cast(label_cat.parent, str, ''),
'keypoints': [str(l) for l in kp_cat.labels], 'keypoints': [],
'skeleton': [int(i) for i in kp_cat.adjacent], 'skeleton': [],
} }
if point_categories is not None:
kp_cat = point_categories.items.get(idx)
if kp_cat is not None:
cat.update({
'keypoints': [str(l) for l in kp_cat.labels],
'skeleton': [int(i) for i in kp_cat.adjacent],
})
self.categories.append(cat) self.categories.append(cat)
def save_annotations(self, item): def save_annotations(self, item):
@ -447,14 +451,19 @@ class _Converter:
def __init__(self, extractor, save_dir, def __init__(self, extractor, save_dir,
tasks=None, save_images=False, segmentation_mode=None, tasks=None, save_images=False, segmentation_mode=None,
crop_covered=False): crop_covered=False):
assert tasks is None or isinstance(tasks, (CocoTask, list)) assert tasks is None or isinstance(tasks, (CocoTask, list, str))
if tasks is None: if tasks is None:
tasks = list(self._TASK_CONVERTER) tasks = list(self._TASK_CONVERTER)
elif isinstance(tasks, CocoTask): elif isinstance(tasks, CocoTask):
tasks = [tasks] tasks = [tasks]
elif isinstance(tasks, str):
tasks = [CocoTask[tasks]]
else: else:
for t in tasks: for i, t in enumerate(tasks):
assert t in CocoTask if isinstance(t, str):
tasks[i] = CocoTask[t]
else:
assert t in CocoTask, t
self._tasks = tasks self._tasks = tasks
self._extractor = extractor self._extractor = extractor
@ -546,9 +555,8 @@ class _Converter:
task_conv.save_annotations(item) task_conv.save_annotations(item)
for task, task_conv in task_converters.items(): for task, task_conv in task_converters.items():
if not task_conv.is_empty(): task_conv.write(osp.join(self._ann_dir,
task_conv.write(osp.join(self._ann_dir, '%s_%s.json' % (task.name, subset_name)))
'%s_%s.json' % (task.name, subset_name)))
class CocoConverter(Converter, CliPlugin): class CocoConverter(Converter, CliPlugin):
@staticmethod @staticmethod

@ -90,7 +90,9 @@ class YoloExtractor(SourceExtractor):
subset = YoloExtractor.Subset(subset_name, self) subset = YoloExtractor.Subset(subset_name, self)
with open(list_path, 'r') as f: with open(list_path, 'r') as f:
subset.items = OrderedDict( subset.items = OrderedDict(
(osp.splitext(osp.basename(p))[0], p.strip()) for p in f) (osp.splitext(osp.basename(p.strip()))[0], p.strip())
for p in f
)
for item_id, image_path in subset.items.items(): for item_id, image_path in subset.items.items():
image_path = self._make_local_path(image_path) image_path = self._make_local_path(image_path)

@ -632,10 +632,13 @@ class CocoConverterTest(TestCase):
def categories(self): def categories(self):
label_cat = LabelCategories() label_cat = LabelCategories()
point_cat = PointsCategories()
for label in range(10): for label in range(10):
label_cat.add('label_' + str(label)) label_cat.add('label_' + str(label))
point_cat.add(label)
return { return {
AnnotationType.label: label_cat, AnnotationType.label: label_cat,
AnnotationType.points: point_cat,
} }
with TestDir() as test_dir: with TestDir() as test_dir:
@ -651,4 +654,4 @@ class CocoConverterTest(TestCase):
with TestDir() as test_dir: with TestDir() as test_dir:
self._test_save_and_load(TestExtractor(), self._test_save_and_load(TestExtractor(),
CocoConverter(), test_dir) CocoConverter(tasks='image_info'), test_dir)
Loading…
Cancel
Save