diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb69400e..483feb90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,7 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
-
### 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
- 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
- Yolov3 interpretation script fix and changes to mapping.json
- YOLO format support ([#1151](https://github.com/opencv/cvat/pull/1151))
+- Added support for OpenVINO 2020
### Fixed
- Exception in Git plugin [#826](https://github.com/opencv/cvat/issues/826)
diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md
index 2b9f9201..7fe60491 100644
--- a/cvat-canvas/README.md
+++ b/cvat-canvas/README.md
@@ -37,6 +37,19 @@ Canvas itself handles:
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 {
enabled: boolean;
shapeType?: string;
@@ -70,6 +83,7 @@ Canvas itself handles:
}
interface Canvas {
+ mode(): Mode;
html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
@@ -128,6 +142,10 @@ Standard JS events are used.
- canvas.dragstop
- canvas.zoomstart
- canvas.zoomstop
+ - canvas.zoom
+ - canvas.fit
+ - canvas.dragshape => {id: number}
+ - canvas.resizeshape => {id: number}
```
### WEB
@@ -135,7 +153,8 @@ Standard JS events are used.
// Create an instance of a 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
htmlContainer.appendChild(canvas.html());
diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index 9068f32d..297ab2c1 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -74,7 +74,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode;
}
- private onDrawDone(data: object, continueDraw?: boolean): void {
+ private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
if (data) {
const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', {
@@ -87,6 +87,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
zOrder: zLayer || 0,
},
continue: continueDraw,
+ duration,
},
});
@@ -137,12 +138,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE;
}
- private onMergeDone(objects: any[]): void {
+ private onMergeDone(objects: any[]| null, duration?: number): void {
if (objects) {
const event: CustomEvent = new CustomEvent('canvas.merged', {
bubbles: false,
cancelable: true,
detail: {
+ duration,
states: objects,
},
});
@@ -724,6 +726,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas();
this.transformCanvas();
+ if (reason === UpdateReasons.IMAGE_FITTED) {
+ this.canvas.dispatchEvent(new CustomEvent('canvas.fit', {
+ bubbles: false,
+ cancelable: true,
+ }));
+ }
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
} 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);
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);
}
});
@@ -1232,6 +1247,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset);
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);
}
});
diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts
index 60745f9f..4afe75ec 100644
--- a/cvat-canvas/src/typescript/drawHandler.ts
+++ b/cvat-canvas/src/typescript/drawHandler.ts
@@ -31,7 +31,8 @@ export interface DrawHandler {
export class DrawHandlerImpl implements DrawHandler {
// 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 text: SVG.Container;
private cursorPosition: {
@@ -180,7 +181,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
- });
+ }, Date.now() - this.startTimestamp);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
@@ -213,7 +214,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
- });
+ }, Date.now() - this.startTimestamp);
}
}
}).on('undopoint', (): void => {
@@ -300,7 +301,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points,
- });
+ }, Date.now() - this.startTimestamp);
} else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
@@ -308,13 +309,13 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points,
- });
+ }, Date.now() - this.startTimestamp);
} else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
shapeType,
points,
- });
+ }, Date.now() - this.startTimestamp);
}
});
}
@@ -365,7 +366,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
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 },
label: this.drawData.initialState.label,
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.startTimestamp = Date.now();
this.initialized = true;
}
public constructor(
- onDrawDone: (data: object, continueDraw?: boolean) => void,
+ onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void,
canvas: SVG.Container,
text: SVG.Container,
) {
+ this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.text = text;
diff --git a/cvat-canvas/src/typescript/mergeHandler.ts b/cvat-canvas/src/typescript/mergeHandler.ts
index efaa4ac0..cfb3f78c 100644
--- a/cvat-canvas/src/typescript/mergeHandler.ts
+++ b/cvat-canvas/src/typescript/mergeHandler.ts
@@ -15,8 +15,9 @@ export interface MergeHandler {
export class MergeHandlerImpl implements MergeHandler {
// 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 startTimestamp: number;
private canvas: SVG.Container;
private initialized: boolean;
private statesToBeMerged: any[]; // are being merged
@@ -57,6 +58,7 @@ export class MergeHandlerImpl implements MergeHandler {
private initMerging(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
+ this.startTimestamp = Date.now();
this.initialized = true;
}
@@ -66,7 +68,7 @@ export class MergeHandlerImpl implements MergeHandler {
this.release();
if (statesToBeMerged.length > 1) {
- this.onMergeDone(statesToBeMerged);
+ this.onMergeDone(statesToBeMerged, Date.now() - this.startTimestamp);
} else {
this.onMergeDone(null);
// here is a cycle
@@ -77,12 +79,13 @@ export class MergeHandlerImpl implements MergeHandler {
}
public constructor(
- onMergeDone: (objects: any[]) => void,
+ onMergeDone: (objects: any[] | null, duration?: number) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject;
+ this.startTimestamp = Date.now();
this.canvas = canvas;
this.statesToBeMerged = [];
this.highlightedShapes = {};
diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js
index 7f9ad9c7..4eb4e99a 100644
--- a/cvat-core/src/api.js
+++ b/cvat-core/src/api.js
@@ -14,7 +14,8 @@
function build() {
const PluginRegistry = require('./plugins');
- const User = require('./user');
+ const loggerStorage = require('./logger-storage');
+ const Log = require('./log');
const ObjectState = require('./object-state');
const Statistics = require('./statistics');
const { Job, Task } = require('./session');
@@ -41,6 +42,7 @@ function build() {
ServerError,
} = require('./exceptions');
+ const User = require('./user');
const pjson = require('../package.json');
const config = require('./config');
@@ -419,6 +421,53 @@ function build() {
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
+ * Logger put here a callback to update user activity timer
+ * 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
+ * Durable logs will have been added after "close" method is called for them
+ * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
+ * 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 config
@@ -432,12 +481,6 @@ function build() {
* @property {string} proxy Axios proxy settings.
* For more details please read here
* @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
*/
get backendAPI() {
@@ -452,21 +495,6 @@ function build() {
set 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
@@ -524,6 +552,7 @@ function build() {
Task,
User,
Job,
+ Log,
Attribute,
Label,
Statistics,
diff --git a/cvat-core/src/config.js b/cvat-core/src/config.js
index e940a214..3b9eade8 100644
--- a/cvat-core/src/config.js
+++ b/cvat-core/src/config.js
@@ -6,7 +6,4 @@
module.exports = {
backendAPI: 'http://localhost:7000/api/v1',
proxy: false,
- taskID: undefined,
- jobID: undefined,
- clientID: +Date.now().toString().substr(-6),
};
diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js
index 2c342908..8b6c86fc 100644
--- a/cvat-core/src/enums.js
+++ b/cvat-core/src/enums.js
@@ -103,68 +103,74 @@
});
/**
- * Event types
- * @enum {number}
+ * Logger event types
+ * @enum {string}
* @name LogType
* @memberof module:API.cvat.enums
- * @property {number} pasteObject 0
- * @property {number} changeAttribute 1
- * @property {number} dragObject 2
- * @property {number} deleteObject 3
- * @property {number} pressShortcut 4
- * @property {number} resizeObject 5
- * @property {number} sendLogs 6
- * @property {number} saveJob 7
- * @property {number} jumpFrame 8
- * @property {number} drawObject 9
- * @property {number} changeLabel 10
- * @property {number} sendTaskInfo 11
- * @property {number} loadJob 12
- * @property {number} moveImage 13
- * @property {number} zoomImage 14
- * @property {number} lockObject 15
- * @property {number} mergeObjects 16
- * @property {number} copyObject 17
- * @property {number} propagateObject 18
- * @property {number} undoAction 19
- * @property {number} redoAction 20
- * @property {number} sendUserActivity 21
- * @property {number} sendException 22
- * @property {number} changeFrame 23
- * @property {number} debugInfo 24
- * @property {number} fitImage 25
- * @property {number} rotateImage 26
+ * @property {string} loadJob Load job
+ * @property {string} saveJob Save job
+ * @property {string} uploadAnnotations Upload annotations
+ * @property {string} sendUserActivity Send user activity
+ * @property {string} sendException Send exception
+ * @property {string} sendTaskInfo Send task info
+
+ * @property {string} drawObject Draw object
+ * @property {string} pasteObject Paste object
+ * @property {string} copyObject Copy object
+ * @property {string} propagateObject Propagate object
+ * @property {string} dragObject Drag object
+ * @property {string} resizeObject Resize object
+ * @property {string} deleteObject Delete object
+ * @property {string} lockObject Lock object
+ * @property {string} mergeObjects Merge objects
+ * @property {string} changeAttribute Change attribute
+ * @property {string} changeLabel Change label
+
+ * @property {string} changeFrame Change frame
+ * @property {string} moveImage Move image
+ * @property {string} zoomImage Zoom image
+ * @property {string} fitImage Fit image
+ * @property {string} rotateImage Rotate image
+
+ * @property {string} undoAction Undo action
+ * @property {string} redoAction Redo action
+
+ * @property {string} pressShortcut Press shortcut
+ * @property {string} debugInfo Debug info
* @readonly
*/
- const LogType = {
- pasteObject: 0,
- changeAttribute: 1,
- dragObject: 2,
- deleteObject: 3,
- pressShortcut: 4,
- resizeObject: 5,
- sendLogs: 6,
- saveJob: 7,
- jumpFrame: 8,
- drawObject: 9,
- changeLabel: 10,
- sendTaskInfo: 11,
- loadJob: 12,
- moveImage: 13,
- zoomImage: 14,
- lockObject: 15,
- mergeObjects: 16,
- copyObject: 17,
- propagateObject: 18,
- undoAction: 19,
- redoAction: 20,
- sendUserActivity: 21,
- sendException: 22,
- changeFrame: 23,
- debugInfo: 24,
- fitImage: 25,
- rotateImage: 26,
- };
+ const LogType = Object.freeze({
+ loadJob: 'Load job',
+ saveJob: 'Save job',
+ uploadAnnotations: 'Upload annotations',
+ sendUserActivity: 'Send user activity',
+ sendException: 'Send exception',
+ sendTaskInfo: 'Send task info',
+
+ drawObject: 'Draw object',
+ pasteObject: 'Paste object',
+ copyObject: 'Copy object',
+ propagateObject: 'Propagate object',
+ dragObject: 'Drag object',
+ resizeObject: 'Resize object',
+ deleteObject: 'Delete object',
+ lockObject: 'Lock object',
+ mergeObjects: 'Merge objects',
+ changeAttribute: 'Change attribute',
+ changeLabel: 'Change label',
+
+ changeFrame: 'Change frame',
+ moveImage: 'Move image',
+ zoomImage: 'Zoom image',
+ fitImage: 'Fit image',
+ rotateImage: 'Rotate image',
+
+ undoAction: 'Undo action',
+ redoAction: 'Redo action',
+
+ pressShortcut: 'Press shortcut',
+ debugInfo: 'Debug info',
+ });
/**
* Types of actions with annotations
@@ -208,7 +214,6 @@
/**
* Array of hex colors
- * @type {module:API.cvat.classes.Loader[]} values
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}
diff --git a/cvat-core/src/log.js b/cvat-core/src/log.js
new file mode 100644
index 00000000..56d9592d
--- /dev/null
+++ b/cvat-core/src/log.js
@@ -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
+ * Note then you can call close() multiple times
+ * Log duration will be computed based on the latest call
+ * 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;
diff --git a/cvat-core/src/logger-storage.js b/cvat-core/src/logger-storage.js
new file mode 100644
index 00000000..e8e24d7d
--- /dev/null
+++ b/cvat-core/src/logger-storage.js
@@ -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();
diff --git a/cvat-core/src/logging.js b/cvat-core/src/logging.js
deleted file mode 100644
index f6b52c4e..00000000
--- a/cvat-core/src/logging.js
+++ /dev/null
@@ -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;
-})();
diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js
index 25f10182..7a833fb1 100644
--- a/cvat-core/src/server-proxy.js
+++ b/cvat-core/src/server-proxy.js
@@ -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({
server: {
value: Object.freeze({
@@ -646,6 +661,13 @@
}),
writable: false,
},
+
+ logs: {
+ value: Object.freeze({
+ save: saveLogs,
+ }),
+ writable: false,
+ },
}));
}
}
diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js
index 32643dd2..edd5efc8 100644
--- a/cvat-core/src/session.js
+++ b/cvat-core/src/session.js
@@ -9,6 +9,7 @@
(() => {
const PluginRegistry = require('./plugins');
+ const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
const { getFrame, getPreview } = require('./frames');
const { ArgumentError } = require('./exceptions');
@@ -125,16 +126,11 @@
},
writable: true,
}),
- logs: Object.freeze({
+ logger: Object.freeze({
value: {
- async put(logType, details) {
+ async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
- .apiWrapper.call(this, prototype.logs.put, logType, details);
- return result;
- },
- async save(onUpdate) {
- const result = await PluginRegistry
- .apiWrapper.call(this, prototype.logs.save, onUpdate);
+ .apiWrapper.call(this, prototype.logger.log, logType, payload, wait);
return result;
},
},
@@ -436,33 +432,28 @@
/**
* Namespace is used for an interaction with logs
- * @namespace logs
+ * @namespace logger
* @memberof Session
*/
/**
- * Append log to a log collection.
- * Continue logs will have been added after "close" method is called
- * @method put
- * @memberof Session.logs
- * @param {module:API.cvat.enums.LogType} type a type of a log
- * @param {boolean} continuous log is a continuous log
- * @param {Object} details any others data which will be append to log data
+ * Create a log and add it to a log collection
+ * Durable logs will be added after "close" method is called for them
+ * The fields "task_id" and "job_id" automatically added when add logs
+ * throught a task or a job
+ * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
+ * Payload of ignored logs are shallowly combined to previous logs of the same type
+ * @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}
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @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
@@ -702,6 +693,10 @@
get: Object.getPrototypeOf(this).frames.get.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),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
+
+ this.logger = {
+ log: Object.getPrototypeOf(this).logger.log.bind(this),
+ };
}
/**
@@ -1452,6 +1451,11 @@
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) {
// TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') {
@@ -1663,4 +1667,9 @@
const result = getActions(this);
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;
+ };
})();
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index 4041973d..79ebaaa0 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -23,6 +23,7 @@ import {
} from 'reducers/interfaces';
import getCore from 'cvat-core';
+import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store';
@@ -89,6 +90,23 @@ function computeZRange(states: any[]): number[] {
return [minZ, maxZ];
}
+async function jobInfoGenerator(job: any): Promise> {
+ 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 {
GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
@@ -166,6 +184,28 @@ export enum AnnotationActionTypes {
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
+ SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
+ SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
+}
+
+export function saveLogsAsync():
+ThunkAction, {}, {}, AnyAction> {
+ return async (dispatch: ActionCreator) => {
+ 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 {
@@ -193,8 +233,7 @@ export function switchZLayer(cur: number): AnyAction {
};
}
-export function fetchAnnotationsAsync():
-ThunkAction, {}, {}, AnyAction> {
+export function fetchAnnotationsAsync(): ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
try {
const {
@@ -251,14 +290,21 @@ export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
try {
+ const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// 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();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
+ await undoLog.close();
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
@@ -284,14 +330,21 @@ export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
try {
+ const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// 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();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
+ await redoLog.close();
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
@@ -382,6 +435,12 @@ ThunkAction, {}, {}, AnyAction> {
const frame = state.annotation.player.frame.number;
await job.annotations.upload(file, loader);
+ await job.logger.log(
+ LogType.uploadAnnotations, {
+ ...(await jobInfoGenerator(job)),
+ },
+ );
+
// One more update to escape some problems
// in canvas when shape with the same
// clientID has different type (polygon, rectangle) for example
@@ -508,6 +567,9 @@ export function propagateObjectAsync(
frame: from,
};
+ await sessionInstance.logger.log(
+ LogType.propagateObject, { count: to - from + 1 },
+ );
const states = [];
for (let frame = from; frame <= to; frame++) {
copy.frame = frame;
@@ -558,6 +620,7 @@ export function removeObjectAsync(sessionInstance: any, objectState: any, force:
ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
try {
+ await sessionInstance.logger.log(LogType.deleteObject, { count: 1 });
const removed = await objectState.delete(force);
const history = await sessionInstance.actions.get();
@@ -593,6 +656,9 @@ export function editShape(enabled: boolean): AnyAction {
}
export function copyShape(objectState: any): AnyAction {
+ const job = getStore().getState().annotation.job.instance;
+ job.logger.log(LogType.copyObject, { count: 1 });
+
return {
type: AnnotationActionTypes.COPY_SHAPE,
payload: {
@@ -696,6 +762,12 @@ ThunkAction, {}, {}, AnyAction> {
payload: {},
});
+ await job.logger.log(
+ LogType.changeFrame, {
+ from: frame,
+ to: toFrame,
+ },
+ );
const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
@@ -716,6 +788,7 @@ ThunkAction, {}, {}, AnyAction> {
}
const delay = Math.max(0, Math.round(1000 / frameSpeed)
- currentTime + (state.annotation.player.frame.changeTime as number));
+
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
@@ -743,14 +816,33 @@ ThunkAction, {}, {}, AnyAction> {
export function rotateCurrentFrame(rotation: Rotation): AnyAction {
const state: CombinedState = getStore().getState();
- const { number: frameNumber } = state.annotation.player.frame;
- const { startFrame } = state.annotation.job.instance;
- const { frameAngles } = state.annotation.player;
- const { rotateAll } = state.settings.player;
+ const {
+ annotation: {
+ player: {
+ frame: {
+ number: frameNumber,
+ },
+ frameAngles,
+ },
+ job: {
+ instance: job,
+ instance: {
+ startFrame,
+ },
+ },
+ },
+ settings: {
+ player: {
+ rotateAll,
+ },
+ },
+ } = state;
const frameAngle = (frameAngles[frameNumber - startFrame]
+ (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360;
+ job.logger.log(LogType.rotateImage, { angle: frameAngle });
+
return {
type: AnnotationActionTypes.ROTATE_FRAME,
payload: {
@@ -800,11 +892,6 @@ export function getJobAsync(
initialFilters: string[],
): ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
- dispatch({
- type: AnnotationActionTypes.GET_JOB,
- payload: {},
- });
-
try {
const state: CombinedState = getStore().getState();
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
let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
@@ -841,6 +940,8 @@ export function getJobAsync(
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
+ loadJobEvent.close(await jobInfoGenerator(job));
+
dispatch({
type: AnnotationActionTypes.GET_JOB_SUCCESS,
payload: {
@@ -874,6 +975,10 @@ ThunkAction, {}, {}, AnyAction> {
});
try {
+ const saveJobEvent = await sessionInstance.logger.log(
+ LogType.saveJob, {}, true,
+ );
+
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
@@ -883,6 +988,13 @@ ThunkAction, {}, {}, AnyAction> {
});
});
+ await saveJobEvent.close();
+ await sessionInstance.logger.log(
+ LogType.sendTaskInfo,
+ await jobInfoGenerator(sessionInstance),
+ );
+ dispatch(saveLogsAsync());
+
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: {},
@@ -898,6 +1010,7 @@ ThunkAction, {}, {}, AnyAction> {
};
}
+// used to reproduce the latest drawing (in case of tags just creating) by using N
export function rememberObject(
objectType: ObjectType,
labelID: number,
diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx
index a096afd2..db5b6500 100644
--- a/cvat-ui/src/components/annotation-page/annotation-page.tsx
+++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
-import React from 'react';
+import React, { useEffect } from 'react';
import {
Layout,
@@ -21,18 +21,24 @@ interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
+ saveLogs(): void;
workspace: Workspace;
}
-
export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
job,
fetching,
getJob,
+ saveLogs,
workspace,
} = props;
+ useEffect(() => {
+ saveLogs();
+ return saveLogs;
+ }, []);
+
if (job === null) {
if (!fetching) {
getJob();
diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx
index d19b94ef..49d3b499 100644
--- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx
+++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx
@@ -11,6 +11,7 @@ import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
+import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
@@ -28,6 +29,7 @@ interface StateToProps {
activatedAttributeID: number | null;
states: any[];
labels: any[];
+ jobInstance: any;
}
interface DispatchToProps {
@@ -48,12 +50,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
states,
},
job: {
+ instance: jobInstance,
labels,
},
},
} = state;
return {
+ jobInstance,
labels,
activatedStateID,
activatedAttributeID,
@@ -78,6 +82,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
states,
activatedStateID,
activatedAttributeID,
+ jobInstance,
updateAnnotations,
activateObject,
} = props;
@@ -267,6 +272,13 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
+ jobInstance.logger.log(
+ LogType.changeAttribute, {
+ id: activeAttribute.id,
+ object_id: activeObjectState.clientID,
+ value,
+ },
+ );
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
index 41d8acc4..4922a6d1 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
@@ -21,6 +21,7 @@ import {
Workspace,
ShapeType,
} from 'reducers/interfaces';
+import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
@@ -223,6 +224,10 @@ export default class CanvasWrapperComponent extends React.PureComponent {
canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
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.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
@@ -246,20 +251,18 @@ export default class CanvasWrapperComponent extends React.PureComponent {
onShapeDrawn();
}
- const { state } = event.detail;
- if (!state.objectType) {
- state.objectType = activeObjectType;
- }
-
- if (!state.label) {
- [state.label] = jobInstance.task.labels
- .filter((label: any) => label.id === activeLabelID);
- }
-
- if (typeof (state.occluded) === 'undefined') {
- state.occluded = false;
+ const { state, duration } = event.detail;
+ const isDrawnFromScratch = !state.label;
+ if (isDrawnFromScratch) {
+ jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
+ } else {
+ jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
}
+ state.objectType = state.objectType || activeObjectType;
+ state.label = state.label || jobInstance.task.labels
+ .filter((label: any) => label.id === activeLabelID)[0];
+ state.occluded = state.occluded || false;
state.frame = frame;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
@@ -275,7 +278,11 @@ export default class CanvasWrapperComponent extends React.PureComponent {
onMergeObjects(false);
- const { states } = event.detail;
+ const { states, duration } = event.detail;
+ jobInstance.logger.log(LogType.mergeObjects, {
+ duration,
+ count: states.length,
+ });
onMergeAnnotations(jobInstance, frame, states);
};
@@ -342,6 +349,28 @@ export default class CanvasWrapperComponent extends React.PureComponent {
}
};
+ 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 => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
@@ -613,6 +642,10 @@ export default class CanvasWrapperComponent extends React.PureComponent {
canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
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.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);
diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx
index 91795797..bab5887a 100644
--- a/cvat-ui/src/components/cvat-app.tsx
+++ b/cvat-ui/src/components/cvat-app.tsx
@@ -27,6 +27,7 @@ import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from 'containers/register-page/register-page';
import HeaderContainer from 'containers/header/header';
+import getCore from 'cvat-core';
import { NotificationsState } from 'reducers/interfaces';
interface CVATAppProps {
@@ -56,8 +57,17 @@ interface CVATAppProps {
class CVATApplication extends React.PureComponent {
public componentDidMount(): void {
+ const core = getCore();
const { verifyAuthorized } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
+
+ // Logger configuration
+ const userActivityCallback: (() => void)[] = [];
+ window.addEventListener('click', () => {
+ userActivityCallback.forEach((handler) => handler());
+ });
+ core.logger.configure(() => window.document.hasFocus, userActivityCallback);
+
verifyAuthorized();
}
@@ -198,7 +208,7 @@ class CVATApplication extends React.PureComponent
- { withModels
- && }
- { installedAutoAnnotation
- && }
+ {withModels
+ && }
+ {installedAutoAnnotation
+ && }
{/* eslint-disable-next-line */}
-
+
);
diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx
index 264d1d94..8d2972ba 100644
--- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx
+++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx
@@ -7,7 +7,7 @@ import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
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';
@@ -24,6 +24,7 @@ interface StateToProps {
interface DispatchToProps {
getJob(): void;
+ saveLogs(): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@@ -77,6 +78,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
getJob(): void {
dispatch(getJobAsync(taskID, jobID, initialFrame, initialFilters));
},
+ saveLogs(): void {
+ dispatch(saveLogsAsync());
+ },
};
}
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index fb11fe1d..4efc8803 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -5,6 +5,8 @@
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
+
+import { LogType } from 'cvat-logger';
import {
ActiveControl,
CombinedState,
@@ -292,13 +294,15 @@ class ObjectItemContainer extends React.PureComponent {
};
private lock = (): void => {
- const { objectState } = this.props;
+ const { objectState, jobInstance } = this.props;
+ jobInstance.logger.log(LogType.lockObject, { locked: true });
objectState.lock = true;
this.commit();
};
private unlock = (): void => {
- const { objectState } = this.props;
+ const { objectState, jobInstance } = this.props;
+ jobInstance.logger.log(LogType.lockObject, { locked: false });
objectState.lock = false;
this.commit();
};
@@ -405,7 +409,12 @@ class ObjectItemContainer extends React.PureComponent {
};
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 = {};
attr[id] = value;
objectState.attributes = attr;
diff --git a/cvat-ui/src/cvat-logger.ts b/cvat-ui/src/cvat-logger.ts
new file mode 100644
index 00000000..f1277ff2
--- /dev/null
+++ b/cvat-ui/src/cvat-logger.ts
@@ -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,
+};
diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts
index 1b888405..3eb393e3 100644
--- a/cvat-ui/src/reducers/interfaces.ts
+++ b/cvat-ui/src/reducers/interfaces.ts
@@ -230,6 +230,7 @@ export interface NotificationsState {
undo: null | ErrorState;
redo: null | ErrorState;
search: null | ErrorState;
+ savingLogs: null | ErrorState;
};
[index: string]: any;
diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts
index 9ca0240d..668e70f2 100644
--- a/cvat-ui/src/reducers/notifications-reducer.ts
+++ b/cvat-ui/src/reducers/notifications-reducer.ts
@@ -74,6 +74,7 @@ const defaultState: NotificationsState = {
undo: null,
redo: null,
search: null,
+ savingLogs: null,
},
},
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: {
return {
...state,
diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js
index e298954d..a172632a 100644
--- a/cvat-ui/webpack.config.js
+++ b/cvat-ui/webpack.config.js
@@ -86,8 +86,8 @@ module.exports = {
},
plugins: [
new HtmlWebpackPlugin({
- template: "./src/index.html",
- inject: false,
+ template: "./src/index.html",
+ inject: false,
}),
new Dotenv({
systemvars: true,
diff --git a/cvat/apps/annotation/labelme.py b/cvat/apps/annotation/labelme.py
index 0128ca73..baacb388 100644
--- a/cvat/apps/annotation/labelme.py
+++ b/cvat/apps/annotation/labelme.py
@@ -107,17 +107,17 @@ def dump_frame_anno(frame_annotation):
return ET.tostring(root_elem, encoding='unicode', pretty_print=True)
def dump_as_labelme_annotation(file_object, annotations):
+ import os.path as osp
from zipfile import ZipFile, ZIP_DEFLATED
with ZipFile(file_object, 'w', compression=ZIP_DEFLATED) as output_zip:
for frame_annotation in annotations.group_by_frame():
xml_data = dump_frame_anno(frame_annotation)
- filename = frame_annotation.name
- filename = filename[ : filename.rfind('.')] + '.xml'
+ filename = osp.splitext(frame_annotation.name)[0] + '.xml'
output_zip.writestr(filename, xml_data)
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 lxml import etree as ET
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 = np.asarray(Image.open(BytesIO(mask)).convert('L'))
mask = (mask != 0)
- polygons = mask_to_polygon(mask)
+ polygons = mask_to_polygons(mask)
for polygon in polygons:
ann_items.append(annotations.LabeledShape(
diff --git a/cvat/apps/auto_annotation/inference_engine.py b/cvat/apps/auto_annotation/inference_engine.py
index 310e78c4..fb6b543d 100644
--- a/cvat/apps/auto_annotation/inference_engine.py
+++ b/cvat/apps/auto_annotation/inference_engine.py
@@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: MIT
-from openvino.inference_engine import IENetwork, IEPlugin
+from openvino.inference_engine import IENetwork, IEPlugin, IECore, get_version
import subprocess
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:
raise OSError('Inference engine plugin path env not found in the system.')
diff --git a/cvat/apps/auto_annotation/model_loader.py b/cvat/apps/auto_annotation/model_loader.py
index cb923a9c..e48d5c8e 100644
--- a/cvat/apps/auto_annotation/model_loader.py
+++ b/cvat/apps/auto_annotation/model_loader.py
@@ -8,25 +8,22 @@ import cv2
import os
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():
def __init__(self, model, weights):
self._model = model
self._weights = weights
- IE_PLUGINS_PATH = os.getenv("IE_PLUGINS_PATH")
- if not IE_PLUGINS_PATH:
- raise OSError("Inference engine plugin path env not found in the system.")
-
- plugin = make_plugin()
+ core_or_plugin = make_plugin_or_core()
network = make_network(self._model, self._weights)
- supported_layers = plugin.get_supported_layers(network)
- not_supported_layers = [l for l in network.layers.keys() if l not in supported_layers]
- if len(not_supported_layers) != 0:
- raise Exception("Following layers are not supported by the plugin for specified device {}:\n {}".
- format(plugin.device, ", ".join(not_supported_layers)))
+ if getattr(core_or_plugin, 'get_supported_layers', False):
+ supported_layers = core_or_plugin.get_supported_layers(network)
+ not_supported_layers = [l for l in network.layers.keys() if l not in supported_layers]
+ if len(not_supported_layers) != 0:
+ 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)
self._input_blob_name = next(iter_inputs)
@@ -45,7 +42,12 @@ class ModelLoader():
if self._input_blob_name in info_names:
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]
self._input_layout = input_type if isinstance(input_type, list) else input_type.shape
diff --git a/cvat/apps/dataset_manager/bindings.py b/cvat/apps/dataset_manager/bindings.py
index da37a304..d1d98af2 100644
--- a/cvat/apps/dataset_manager/bindings.py
+++ b/cvat/apps/dataset_manager/bindings.py
@@ -91,7 +91,9 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
@staticmethod
def _load_categories(cvat_anno):
categories = {}
- label_categories = datumaro.LabelCategories()
+
+ label_categories = datumaro.LabelCategories(
+ attributes=['occluded', 'z_order'])
for _, label in cvat_anno.meta['task']['labels']:
label_categories.add(label['name'])
@@ -144,6 +146,8 @@ class CvatAnnotationsExtractor(datumaro.Extractor):
anno_group = shape_obj.group
anno_label = map_label(shape_obj.label)
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
if shape_obj.type == ShapeType.POINTS:
@@ -177,6 +181,8 @@ class CvatTaskExtractor(CvatAnnotationsExtractor):
def match_frame(item, cvat_task_anno):
+ is_video = cvat_task_anno.meta['task']['mode'] == 'interpolation'
+
frame_number = None
if frame_number is None:
try:
@@ -193,6 +199,8 @@ def match_frame(item, cvat_task_anno):
frame_number = int(item.id)
except Exception:
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:
raise Exception("Could not match item id: '%s' with any task frame" %
item.id)
@@ -234,7 +242,7 @@ def import_dm_annotations(dm_dataset, cvat_task_anno):
frame=frame_number,
label=label_cat.items[ann.label].name,
points=ann.points,
- occluded=False,
+ occluded=ann.attributes.get('occluded') == True,
group=group_map.get(ann.group, 0),
attributes=[cvat_task_anno.Attribute(name=n, value=str(v))
for n, v in ann.attributes.items()],
diff --git a/cvat/apps/dextr_segmentation/dextr.py b/cvat/apps/dextr_segmentation/dextr.py
index 703c6d08..628961ff 100644
--- a/cvat/apps/dextr_segmentation/dextr.py
+++ b/cvat/apps/dextr_segmentation/dextr.py
@@ -3,7 +3,7 @@
#
# 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 cv2
@@ -32,12 +32,15 @@ class DEXTR_HANDLER:
def handle(self, im_path, points):
# Lazy initialization
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'),
os.path.join(_DEXTR_MODEL_DIR, 'dextr.bin'))
self._input_blob = next(iter(self._network.inputs))
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)
numpy_image = np.array(image)
diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py
index d959f71c..f3da0410 100644
--- a/cvat/apps/engine/tests/test_rest_api.py
+++ b/cvat/apps/engine/tests/test_rest_api.py
@@ -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],
"type": "polygon",
"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 = [{
@@ -2711,6 +2720,12 @@ class TaskAnnotationAPITestCase(JobAnnotationAPITestCase):
elif annotation_format == "MOT CSV 1.0":
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
response = self._get_annotation_formats(annotator)
diff --git a/cvat/apps/tf_annotation/views.py b/cvat/apps/tf_annotation/views.py
index 2a70c9db..4aa0589c 100644
--- a/cvat/apps/tf_annotation/views.py
+++ b/cvat/apps/tf_annotation/views.py
@@ -30,7 +30,7 @@ def load_image_into_numpy(image):
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):
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:
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))
input_blob_name = next(iter(network.inputs))
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()
del network
diff --git a/datumaro/datumaro/plugins/coco_format/converter.py b/datumaro/datumaro/plugins/coco_format/converter.py
index 39fe7b15..403a6a83 100644
--- a/datumaro/datumaro/plugins/coco_format/converter.py
+++ b/datumaro/datumaro/plugins/coco_format/converter.py
@@ -329,20 +329,24 @@ class _KeypointsConverter(_InstancesConverter):
label_categories = dataset.categories().get(AnnotationType.label)
if label_categories is None:
return
- points_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]
+ point_categories = dataset.categories().get(AnnotationType.points)
+ for idx, label_cat in enumerate(label_categories.items):
cat = {
'id': 1 + idx,
'name': _cast(label_cat.name, str, ''),
'supercategory': _cast(label_cat.parent, str, ''),
- 'keypoints': [str(l) for l in kp_cat.labels],
- 'skeleton': [int(i) for i in kp_cat.adjacent],
+ 'keypoints': [],
+ '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)
def save_annotations(self, item):
@@ -447,14 +451,19 @@ class _Converter:
def __init__(self, extractor, save_dir,
tasks=None, save_images=False, segmentation_mode=None,
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:
tasks = list(self._TASK_CONVERTER)
elif isinstance(tasks, CocoTask):
tasks = [tasks]
+ elif isinstance(tasks, str):
+ tasks = [CocoTask[tasks]]
else:
- for t in tasks:
- assert t in CocoTask
+ for i, t in enumerate(tasks):
+ if isinstance(t, str):
+ tasks[i] = CocoTask[t]
+ else:
+ assert t in CocoTask, t
self._tasks = tasks
self._extractor = extractor
@@ -546,9 +555,8 @@ class _Converter:
task_conv.save_annotations(item)
for task, task_conv in task_converters.items():
- if not task_conv.is_empty():
- task_conv.write(osp.join(self._ann_dir,
- '%s_%s.json' % (task.name, subset_name)))
+ task_conv.write(osp.join(self._ann_dir,
+ '%s_%s.json' % (task.name, subset_name)))
class CocoConverter(Converter, CliPlugin):
@staticmethod
diff --git a/datumaro/datumaro/plugins/yolo_format/extractor.py b/datumaro/datumaro/plugins/yolo_format/extractor.py
index 7840b26c..11e829d4 100644
--- a/datumaro/datumaro/plugins/yolo_format/extractor.py
+++ b/datumaro/datumaro/plugins/yolo_format/extractor.py
@@ -90,7 +90,9 @@ class YoloExtractor(SourceExtractor):
subset = YoloExtractor.Subset(subset_name, self)
with open(list_path, 'r') as f:
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():
image_path = self._make_local_path(image_path)
diff --git a/datumaro/tests/test_coco_format.py b/datumaro/tests/test_coco_format.py
index 724fdc5a..f9340b65 100644
--- a/datumaro/tests/test_coco_format.py
+++ b/datumaro/tests/test_coco_format.py
@@ -632,10 +632,13 @@ class CocoConverterTest(TestCase):
def categories(self):
label_cat = LabelCategories()
+ point_cat = PointsCategories()
for label in range(10):
label_cat.add('label_' + str(label))
+ point_cat.add(label)
return {
AnnotationType.label: label_cat,
+ AnnotationType.points: point_cat,
}
with TestDir() as test_dir:
@@ -651,4 +654,4 @@ class CocoConverterTest(TestCase):
with TestDir() as test_dir:
self._test_save_and_load(TestExtractor(),
- CocoConverter(), test_dir)
\ No newline at end of file
+ CocoConverter(tasks='image_info'), test_dir)
\ No newline at end of file