Merge branch 'develop' into dk/cvat-ui-tags

main
Dmitry Kalinin 6 years ago
commit 5c009f5d88

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
import {
Mode,
DrawData,
MergeData,
SplitData,
@ -51,6 +52,7 @@ interface Canvas {
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
mode(): void;
cancel(): void;
}
@ -132,6 +134,10 @@ class CanvasImpl implements Canvas {
this.model.select(objectState);
}
public mode(): Mode {
return this.model.mode;
}
public cancel(): void {
this.model.cancel();
}
@ -141,4 +147,5 @@ export {
CanvasImpl as Canvas,
CanvasVersion,
RectDrawingMethod,
Mode as CanvasMode,
};

@ -472,21 +472,21 @@ export class CanvasViewImpl implements CanvasView, Listener {
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('mouseenter', (): void => {
circle.on('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('dblclick', dblClickHandler);
circle.on('dblclick', dblClickHandler);
circle.addClass('cvat_canvas_selected_point');
});
circle.node.addEventListener('mouseleave', (): void => {
circle.on('mouseleave', (): void => {
circle.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.removeEventListener('dblclick', dblClickHandler);
circle.off('dblclick', dblClickHandler);
circle.removeClass('cvat_canvas_selected_point');
});
@ -632,7 +632,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (![Mode.ZOOM_CANVAS, Mode.GROUP].includes(this.mode) || event.which === 2) {
self.controller.enableDrag(event.clientX, event.clientY);
}
event.preventDefault();
}
});
@ -1340,13 +1339,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
private setupPoints(basicPolyline: SVG.PolyLine, state: any): any {
this.selectize(true, basicPolyline);
const group = basicPolyline.remember('_selectHandler').nested
const group: SVG.G = basicPolyline.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
id: `cvat_canvas_shape_${state.clientID}`,
'data-z-order': state.zOrder,
});
group.on('click.canvas', (event: MouseEvent): void => {
// Need to redispatch the event on another element
basicPolyline.fire(new MouseEvent('click', event));
});
group.bbox = basicPolyline.bbox.bind(basicPolyline);
group.clone = basicPolyline.clone.bind(basicPolyline);

@ -34,6 +34,10 @@ export class DrawHandlerImpl implements DrawHandler {
private onDrawDone: (data: object, continueDraw?: boolean) => void;
private canvas: SVG.Container;
private text: SVG.Container;
private cursorPosition: {
x: number;
y: number;
};
private crosshair: {
x: SVG.Line;
y: SVG.Line;
@ -96,12 +100,13 @@ export class DrawHandlerImpl implements DrawHandler {
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = {
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({
x: this.canvas.line(0, y, this.canvas.node.clientWidth, y).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({
y: this.canvas.line(x, 0, x, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
@ -181,7 +186,6 @@ export class DrawHandlerImpl implements DrawHandler {
this.shapeSizeElement.update(this.drawInstance);
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
z_order: Number.MAX_SAFE_INTEGER,
});
}
@ -222,10 +226,6 @@ export class DrawHandlerImpl implements DrawHandler {
}
private drawPolyshape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
@ -371,18 +371,17 @@ export class DrawHandlerImpl implements DrawHandler {
// Common settings for rectangle and polyshapes
private pasteShape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
function moveShape(shape: SVG.Shape, x: number, y: number): void {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
}
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const { x: initialX, y: initialY } = this.cursorPosition;
moveShape(this.drawInstance, initialX, initialY);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(this.drawInstance, x, y);
});
}
@ -429,45 +428,53 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePolyshape();
}
private pastePoints(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
private pastePoints(initialPoints: string): void {
function moveShape(
shape: SVG.PolyLine,
group: SVG.G,
x: number,
y: number,
scale: number,
): void {
const bbox = shape.bbox();
shape.move(x - bbox.width / 2, y - bbox.height / 2);
const points = shape.attr('points').split(' ');
const radius = consts.BASE_POINT_SIZE / scale;
group.children().forEach((child: SVG.Element, idx: number): void => {
const [px, py] = points[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
const { x: initialX, y: initialY } = this.cursorPosition;
this.pointsGroup = this.canvas.group();
this.drawInstance = (this.canvas as any).polyline(initialPoints)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
this.pointsGroup = this.canvas.group();
for (const point of points.split(' ')) {
let numOfPoints = initialPoints.split(' ').length;
while (numOfPoints) {
numOfPoints--;
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
const [x, y] = point.split(',').map((coord: string): number => +coord);
this.pointsGroup.circle().move(x - radius / 2, y - radius / 2)
.fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
this.pointsGroup.circle().fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
}
this.pointsGroup.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
moveShape(
this.drawInstance, this.pointsGroup, initialX, initialY, this.geometry.scale,
);
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
this.canvas.on('mousemove.draw', (): void => {
const { x, y } = this.cursorPosition; // was computer in another callback
moveShape(
this.drawInstance, this.pointsGroup, x, y, this.geometry.scale,
);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const newPoints = this.drawInstance.attr('points').split(' ');
if (this.pointsGroup) {
this.pointsGroup.children()
.forEach((child: SVG.Element, idx: number): void => {
const [px, py] = newPoints[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
});
this.pastePolyshape();
@ -593,23 +600,20 @@ export class DrawHandlerImpl implements DrawHandler {
this.crosshair = null;
this.drawInstance = null;
this.pointsGroup = null;
this.cursorPosition = {
x: 0,
y: 0,
};
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.cursorPosition = { x, y };
if (this.crosshair) {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.crosshair.x.attr({
y1: y,
y2: y,
});
this.crosshair.y.attr({
x1: x,
x2: x,
});
this.crosshair.x.attr({ y1: y, y2: y });
this.crosshair.y.attr({ x1: x, x2: x });
}
});
}

@ -84,7 +84,7 @@ export class EditHandlerImpl implements EditHandler {
}).draw(dummyEvent, { snapToGrid: 0.1 });
if (this.editData.state.shapeType === 'points') {
this.editLine.style('stroke-width', 0);
this.editLine.attr('stroke-width', 0);
(this.editLine as any).draw('undo');
}
@ -168,7 +168,7 @@ export class EditHandlerImpl implements EditHandler {
for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
.style('fill-opacity', '0.5')
.attr('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'));
}
@ -340,10 +340,16 @@ export class EditHandlerImpl implements EditHandler {
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.editedShape) {
this.editedShape.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
if (this.editLine) {
(this.editLine as any).draw('transform');
if (this.editData.state.shapeType !== 'points') {
this.editLine.style({
this.editLine.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
@ -351,7 +357,7 @@ export class EditHandlerImpl implements EditHandler {
const paintHandler = this.editLine.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.style(
point.attr(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);

@ -864,7 +864,7 @@
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame
// Consider all shapes, tags, and tracks that have keyframe here
// Consider all shapes, tags, and not outside tracks that have keyframe here
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
@ -878,7 +878,10 @@
|| frame === frameFrom
|| frame === frameTo
));
statesData.push(...tracks.map((track) => track.get(frame)));
statesData.push(
...tracks.map((track) => track.get(frame))
.filter((state) => !state.outside),
);
// Nothing to filtering, go to the next iteration
if (!statesData.length) {

@ -339,6 +339,11 @@
if (updated.keyframe) {
checkObjectType('keyframe', data.keyframe, 'boolean', null);
if (!this.shapes || (Object.keys(this.shapes).length === 1 && !data.keyframe)) {
throw new ArgumentError(
'Can not remove the latest keyframe of an object. Consider removing the object instead',
);
}
}
return fittedPoints;
@ -964,7 +969,8 @@
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) {
if ((keyframe && wasKeyframe)
|| (!keyframe && !wasKeyframe)) {
return;
}
@ -1088,7 +1094,7 @@
throw new DataError(
'No one left position or right position was found. '
+ `Interpolation impossible. Client ID: ${this.id}`,
+ `Interpolation impossible. Client ID: ${this.clientID}`,
);
}
}

@ -39,7 +39,7 @@
color: null,
hidden: null,
pinned: null,
keyframes: null,
keyframes: serialized.keyframes,
group: serialized.group,
updated: serialized.updated,

@ -1343,7 +1343,7 @@
return annotationsData;
};
Job.prototype.annotations.search.implementation = async function (filters, frameFrom, frameTo) {
Job.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
@ -1555,7 +1555,7 @@
return result;
};
Job.prototype.annotations.search.implementation = async function (filters, frameFrom, frameTo) {
Task.prototype.annotations.search.implementation = function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "0.1.0",
"version": "0.5.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -9734,6 +9734,14 @@
"scheduler": "^0.17.0"
}
},
"react-hotkeys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz",
"integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==",
"requires": {
"prop-types": "^15.6.1"
}
},
"react-is": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz",

@ -61,6 +61,7 @@
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-hotkeys": "^2.0.0",
"react-redux": "^7.1.1",
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",

@ -82,8 +82,10 @@ export enum AnnotationActionTypes {
GROUP_OBJECTS = 'GROUP_OBJECTS',
SPLIT_TRACK = 'SPLIT_TRACK',
COPY_SHAPE = 'COPY_SHAPE',
PASTE_SHAPE = 'PASTE_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE',
REPEAT_DRAW_SHAPE = 'REPEAT_DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
@ -92,6 +94,8 @@ export enum AnnotationActionTypes {
CREATE_ANNOTATIONS_FAILED = 'CREATE_ANNOTATIONS_FAILED',
MERGE_ANNOTATIONS_SUCCESS = 'MERGE_ANNOTATIONS_SUCCESS',
MERGE_ANNOTATIONS_FAILED = 'MERGE_ANNOTATIONS_FAILED',
RESET_ANNOTATIONS_GROUP = 'RESET_ANNOTATIONS_GROUP',
GROUP_ANNOTATIONS = 'GROUP_ANNOTATIONS',
GROUP_ANNOTATIONS_SUCCESS = 'GROUP_ANNOTATIONS_SUCCESS',
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
@ -133,6 +137,7 @@ export enum AnnotationActionTypes {
ROTATE_FRAME = 'ROTATE_FRAME',
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
}
export function addZLayer(): AnyAction {
@ -179,10 +184,22 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
export function changeAnnotationsFilters(filters: string[]): AnyAction {
const state: CombinedState = getStore().getState();
const { filtersHistory, filters: oldFilters } = state.annotation.annotations;
filters.forEach((element: string) => {
if (!(filtersHistory.includes(element) || oldFilters.includes(element))) {
filtersHistory.push(element);
}
});
window.localStorage.setItem('filtersHistory', JSON.stringify(filtersHistory.slice(-10)));
return {
type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS,
payload: {
filters,
filtersHistory: filtersHistory.slice(-10),
},
};
}
@ -914,6 +931,11 @@ export function updateAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
}
const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
@ -1000,12 +1022,30 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function groupAnnotationsAsync(sessionInstance: any, frame: number, statesToGroup: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function resetAnnotationsGroup(): AnyAction {
return {
type: AnnotationActionTypes.RESET_ANNOTATIONS_GROUP,
payload: {},
};
}
export function groupAnnotationsAsync(
sessionInstance: any,
frame: number,
statesToGroup: any[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
await sessionInstance.annotations.group(statesToGroup);
const reset = getStore().getState().annotation.annotations.resetGroupFlag;
// The action below set resetFlag to false
dispatch({
type: AnnotationActionTypes.GROUP_ANNOTATIONS,
payload: {},
});
await sessionInstance.annotations.group(statesToGroup, reset);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
@ -1108,3 +1148,94 @@ export function changeGroupColorAsync(
}
};
}
export function searchAnnotationsAsync(
sessionInstance: any,
frameFrom: number,
frameTo: number,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const frame = await sessionInstance.annotations.search(filters, frameFrom, frameTo);
if (frame !== null) {
dispatch(changeFrameAsync(frame));
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.SEARCH_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const initialState = getStore().getState().annotation.drawing.activeInitialState;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
if (initialState) {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (initialState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (initialState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (initialState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
}
dispatch({
type: AnnotationActionTypes.PASTE_SHAPE,
payload: {
activeControl,
},
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
initialState,
});
}
};
}
export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
activeShapeType,
activeNumOfPoints,
activeRectDrawingMethod,
} = getStore().getState().annotation.drawing;
const { instance: canvasInstance } = getStore().getState().annotation.canvas;
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (activeShapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
}
dispatch({
type: AnnotationActionTypes.REPEAT_DRAW_SHAPE,
payload: {
activeControl,
},
});
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
rectDrawingMethod: activeRectDrawingMethod,
numberOfPoints: activeNumOfPoints,
shapeType: activeShapeType,
crosshair: activeShapeType === ShapeType.RECTANGLE,
});
};
}

@ -2,17 +2,20 @@
//
// SPDX-License-Identifier: MIT
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import getCore from 'cvat-core';
import { getCVATStore } from 'cvat-store';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import {
Model,
ModelType,
ModelFiles,
ActiveInference,
CombinedState,
} from '../reducers/interfaces';
} from 'reducers/interfaces';
import getCore from 'cvat-core';
export enum PreinstalledModels {
RCNN = 'RCNN Object Detector',
MaskRCNN = 'Mask RCNN Object Detector',
}
export enum ModelsActionTypes {
GET_MODELS = 'GET_MODELS',
@ -25,66 +28,101 @@ export enum ModelsActionTypes {
CREATE_MODEL_SUCCESS = 'CREATE_MODEL_SUCCESS',
CREATE_MODEL_FAILED = 'CREATE_MODEL_FAILED',
CREATE_MODEL_STATUS_UPDATED = 'CREATE_MODEL_STATUS_UPDATED',
INFER_MODEL = 'INFER_MODEL',
INFER_MODEL_SUCCESS = 'INFER_MODEL_SUCCESS',
INFER_MODEL_FAILED = 'INFER_MODEL_FAILED',
FETCH_META_FAILED = 'FETCH_META_FAILED',
GET_INFERENCE_STATUS = 'GET_INFERENCE_STATUS',
START_INFERENCE_FAILED = 'START_INFERENCE_FAILED',
GET_INFERENCE_STATUS_SUCCESS = 'GET_INFERENCE_STATUS_SUCCESS',
GET_INFERENCE_STATUS_FAILED = 'GET_INFERENCE_STATUS_FAILED',
FETCH_META_FAILED = 'FETCH_META_FAILED',
SHOW_RUN_MODEL_DIALOG = 'SHOW_RUN_MODEL_DIALOG',
CLOSE_RUN_MODEL_DIALOG = 'CLOSE_RUN_MODEL_DIALOG',
CANCEL_INFERENCE_SUCCESS = 'CANCEL_INFERENCE_SUCCESS',
CANCEL_INFERENCE_FAILED = 'CANCEL_INFERENCE_FAILED',
}
export enum PreinstalledModels {
RCNN = 'RCNN Object Detector',
MaskRCNN = 'Mask RCNN Object Detector',
}
const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7);
function getModels(): AnyAction {
const action = {
type: ModelsActionTypes.GET_MODELS,
payload: {},
};
return action;
}
function getModelsSuccess(models: Model[]): AnyAction {
const action = {
type: ModelsActionTypes.GET_MODELS_SUCCESS,
payload: {
export const modelsActions = {
getModels: () => createAction(ModelsActionTypes.GET_MODELS),
getModelsSuccess: (models: Model[]) => createAction(
ModelsActionTypes.GET_MODELS_SUCCESS, {
models,
},
};
return action;
}
function getModelsFailed(error: any): AnyAction {
const action = {
type: ModelsActionTypes.GET_MODELS_FAILED,
payload: {
),
getModelsFailed: (error: any) => createAction(
ModelsActionTypes.GET_MODELS_FAILED, {
error,
},
};
),
deleteModelSuccess: (id: number) => createAction(
ModelsActionTypes.DELETE_MODEL_SUCCESS, {
id,
},
),
deleteModelFailed: (id: number, error: any) => createAction(
ModelsActionTypes.DELETE_MODEL_FAILED, {
error, id,
},
),
createModel: () => createAction(ModelsActionTypes.CREATE_MODEL),
createModelSuccess: () => createAction(ModelsActionTypes.CREATE_MODEL_SUCCESS),
createModelFailed: (error: any) => createAction(
ModelsActionTypes.CREATE_MODEL_FAILED, {
error,
},
),
createModelUpdateStatus: (status: string) => createAction(
ModelsActionTypes.CREATE_MODEL_STATUS_UPDATED, {
status,
},
),
fetchMetaFailed: (error: any) => createAction(ModelsActionTypes.FETCH_META_FAILED, { error }),
getInferenceStatusSuccess: (taskID: number, activeInference: ActiveInference) => createAction(
ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS, {
taskID,
activeInference,
},
),
getInferenceStatusFailed: (taskID: number, error: any) => createAction(
ModelsActionTypes.GET_INFERENCE_STATUS_FAILED, {
taskID,
error,
},
),
startInferenceFailed: (taskID: number, error: any) => createAction(
ModelsActionTypes.START_INFERENCE_FAILED, {
taskID,
error,
},
),
cancelInferenceSuccess: (taskID: number) => createAction(
ModelsActionTypes.CANCEL_INFERENCE_SUCCESS, {
taskID,
},
),
cancelInferenceFaild: (taskID: number, error: any) => createAction(
ModelsActionTypes.CANCEL_INFERENCE_FAILED, {
taskID,
error,
},
),
closeRunModelDialog: () => createAction(ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG),
showRunModelDialog: (taskInstance: any) => createAction(
ModelsActionTypes.SHOW_RUN_MODEL_DIALOG, {
taskInstance,
},
),
};
return action;
}
export type ModelsActions = ActionUnion<typeof modelsActions>;
export function getModelsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const store = getCVATStore();
const state: CombinedState = store.getState();
const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7);
export function getModelsAsync(): ThunkAction {
return async (dispatch, getState): Promise<void> => {
const state: CombinedState = getState();
const OpenVINO = state.plugins.list.AUTO_ANNOTATION;
const RCNN = state.plugins.list.TF_ANNOTATION;
const MaskRCNN = state.plugins.list.TF_SEGMENTATION;
dispatch(getModels());
dispatch(modelsActions.getModels());
const models: Model[] = [];
try {
@ -170,108 +208,31 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
}
} catch (error) {
dispatch(getModelsFailed(error));
dispatch(modelsActions.getModelsFailed(error));
return;
}
dispatch(getModelsSuccess(models));
dispatch(modelsActions.getModelsSuccess(models));
};
}
function deleteModel(id: number): AnyAction {
const action = {
type: ModelsActionTypes.DELETE_MODEL,
payload: {
id,
},
};
return action;
}
function deleteModelSuccess(id: number): AnyAction {
const action = {
type: ModelsActionTypes.DELETE_MODEL_SUCCESS,
payload: {
id,
},
};
return action;
}
function deleteModelFailed(id: number, error: any): AnyAction {
const action = {
type: ModelsActionTypes.DELETE_MODEL_FAILED,
payload: {
error,
id,
},
};
return action;
}
export function deleteModelAsync(id: number): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(deleteModel(id));
export function deleteModelAsync(id: number): ThunkAction {
return async (dispatch): Promise<void> => {
try {
await core.server.request(`${baseURL}/auto_annotation/delete/${id}`, {
method: 'DELETE',
});
} catch (error) {
dispatch(deleteModelFailed(id, error));
dispatch(modelsActions.deleteModelFailed(id, error));
return;
}
dispatch(deleteModelSuccess(id));
};
}
function createModel(): AnyAction {
const action = {
type: ModelsActionTypes.CREATE_MODEL,
payload: {},
};
return action;
}
function createModelSuccess(): AnyAction {
const action = {
type: ModelsActionTypes.CREATE_MODEL_SUCCESS,
payload: {},
dispatch(modelsActions.deleteModelSuccess(id));
};
return action;
}
function createModelFailed(error: any): AnyAction {
const action = {
type: ModelsActionTypes.CREATE_MODEL_FAILED,
payload: {
error,
},
};
return action;
}
function createModelUpdateStatus(status: string): AnyAction {
const action = {
type: ModelsActionTypes.CREATE_MODEL_STATUS_UPDATED,
payload: {
status,
},
};
return action;
}
export function createModelAsync(name: string, files: ModelFiles, global: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
export function createModelAsync(name: string, files: ModelFiles, global: boolean): ThunkAction {
return async (dispatch): Promise<void> => {
async function checkCallback(id: string): Promise<void> {
try {
const data = await core.server.request(
@ -282,30 +243,30 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
switch (data.status) {
case 'failed':
dispatch(createModelFailed(
dispatch(modelsActions.createModelFailed(
`Checking request has returned the "${data.status}" status. Message: ${data.error}`,
));
break;
case 'unknown':
dispatch(createModelFailed(
dispatch(modelsActions.createModelFailed(
`Checking request has returned the "${data.status}" status.`,
));
break;
case 'finished':
dispatch(createModelSuccess());
dispatch(modelsActions.createModelSuccess());
break;
default:
if ('progress' in data) {
createModelUpdateStatus(data.progress);
modelsActions.createModelUpdateStatus(data.progress);
}
setTimeout(checkCallback.bind(null, id), 1000);
}
} catch (error) {
dispatch(createModelFailed(error));
dispatch(modelsActions.createModelFailed(error));
}
}
dispatch(createModel());
dispatch(modelsActions.createModel());
const data = new FormData();
data.append('name', name);
data.append('storage', typeof files.bin === 'string' ? 'shared' : 'local');
@ -316,7 +277,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}, data);
try {
dispatch(createModelUpdateStatus('Request is beign sent..'));
dispatch(modelsActions.createModelUpdateStatus('Request is beign sent..'));
const response = await core.server.request(
`${baseURL}/auto_annotation/create`, {
method: 'POST',
@ -326,56 +287,19 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
);
dispatch(createModelUpdateStatus('Request is being processed..'));
dispatch(modelsActions.createModelUpdateStatus('Request is being processed..'));
setTimeout(checkCallback.bind(null, response.id), 1000);
} catch (error) {
dispatch(createModelFailed(error));
dispatch(modelsActions.createModelFailed(error));
}
};
}
function fetchMetaFailed(error: any): AnyAction {
const action = {
type: ModelsActionTypes.FETCH_META_FAILED,
payload: {
error,
},
};
return action;
}
function getInferenceStatusSuccess(
taskID: number,
activeInference: ActiveInference,
): AnyAction {
const action = {
type: ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS,
payload: {
taskID,
activeInference,
},
};
return action;
}
function getInferenceStatusFailed(taskID: number, error: any): AnyAction {
const action = {
type: ModelsActionTypes.GET_INFERENCE_STATUS_FAILED,
payload: {
taskID,
error,
},
};
return action;
}
interface InferenceMeta {
active: boolean;
taskID: number;
requestID: string;
modelType: ModelType;
}
const timers: any = {};
@ -383,7 +307,8 @@ const timers: any = {};
async function timeoutCallback(
url: string,
taskID: number,
dispatch: ActionCreator<Dispatch>,
modelType: ModelType,
dispatch: (action: ModelsActions) => void,
): Promise<void> {
try {
delete timers[taskID];
@ -396,11 +321,12 @@ async function timeoutCallback(
status: response.status,
progress: +response.progress || 0,
error: response.error || response.stderr || '',
modelType,
};
if (activeInference.status === 'unknown') {
dispatch(getInferenceStatusFailed(
dispatch(modelsActions.getInferenceStatusFailed(
taskID,
new Error(
`Inference status for the task ${taskID} is unknown.`,
@ -411,7 +337,7 @@ async function timeoutCallback(
}
if (activeInference.status === 'failed') {
dispatch(getInferenceStatusFailed(
dispatch(modelsActions.getInferenceStatusFailed(
taskID,
new Error(
`Inference status for the task ${taskID} is failed. ${activeInference.error}`,
@ -427,55 +353,67 @@ async function timeoutCallback(
null,
url,
taskID,
modelType,
dispatch,
), 3000,
);
}
dispatch(getInferenceStatusSuccess(taskID, activeInference));
dispatch(modelsActions.getInferenceStatusSuccess(taskID, activeInference));
} catch (error) {
dispatch(getInferenceStatusFailed(taskID, new Error(
dispatch(modelsActions.getInferenceStatusFailed(taskID, new Error(
`Server request for the task ${taskID} was failed`,
)));
}
}
function subscribe(
urlPath: string,
inferenceMeta: InferenceMeta,
dispatch: ActionCreator<Dispatch>,
dispatch: (action: ModelsActions) => void,
): void {
if (!(inferenceMeta.taskID in timers)) {
const requestURL = `${baseURL}/${urlPath}/${inferenceMeta.requestID}`;
let requestURL = `${baseURL}`;
if (inferenceMeta.modelType === ModelType.OPENVINO) {
requestURL = `${requestURL}/auto_annotation/check`;
} else if (inferenceMeta.modelType === ModelType.RCNN) {
requestURL = `${requestURL}/tensorflow/annotation/check/task`;
} else if (inferenceMeta.modelType === ModelType.MASK_RCNN) {
requestURL = `${requestURL}/tensorflow/segmentation/check/task`;
}
requestURL = `${requestURL}/${inferenceMeta.requestID}`;
timers[inferenceMeta.taskID] = setTimeout(
timeoutCallback.bind(
null,
requestURL,
inferenceMeta.taskID,
inferenceMeta.modelType,
dispatch,
),
);
}
}
export function getInferenceStatusAsync(tasks: number[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
function parse(response: any): InferenceMeta[] {
export function getInferenceStatusAsync(tasks: number[]): ThunkAction {
return async (dispatch, getState): Promise<void> => {
function parse(response: any, modelType: ModelType): InferenceMeta[] {
return Object.keys(response).map((key: string): InferenceMeta => ({
taskID: +key,
requestID: response[key].rq_id || key,
active: typeof (response[key].active) === 'undefined' ? ['queued', 'started']
.includes(response[key].status.toLowerCase()) : response[key].active,
modelType,
}));
}
const store = getCVATStore();
const state: CombinedState = store.getState();
const state: CombinedState = getState();
const OpenVINO = state.plugins.list.AUTO_ANNOTATION;
const RCNN = state.plugins.list.TF_ANNOTATION;
const MaskRCNN = state.plugins.list.TF_SEGMENTATION;
const dispatchCallback = (action: ModelsActions): void => {
dispatch(action);
};
try {
if (OpenVINO) {
const response = await core.server.request(
@ -488,10 +426,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
);
parse(response.run)
parse(response.run, ModelType.OPENVINO)
.filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active)
.forEach((inferenceMeta: InferenceMeta): void => {
subscribe('auto_annotation/check', inferenceMeta, dispatch);
subscribe(inferenceMeta, dispatchCallback);
});
}
@ -506,10 +444,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
);
parse(response)
parse(response, ModelType.RCNN)
.filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active)
.forEach((inferenceMeta: InferenceMeta): void => {
subscribe('tensorflow/annotation/check/task', inferenceMeta, dispatch);
subscribe(inferenceMeta, dispatchCallback);
});
}
@ -524,60 +462,27 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
);
parse(response)
parse(response, ModelType.MASK_RCNN)
.filter((inferenceMeta: InferenceMeta): boolean => inferenceMeta.active)
.forEach((inferenceMeta: InferenceMeta): void => {
subscribe('tensorflow/segmentation/check/task', inferenceMeta, dispatch);
subscribe(inferenceMeta, dispatchCallback);
});
}
} catch (error) {
dispatch(fetchMetaFailed(error));
dispatch(modelsActions.fetchMetaFailed(error));
}
};
}
function inferModel(): AnyAction {
const action = {
type: ModelsActionTypes.INFER_MODEL,
payload: {},
};
return action;
}
function inferModelSuccess(): AnyAction {
const action = {
type: ModelsActionTypes.INFER_MODEL_SUCCESS,
payload: {},
};
return action;
}
function inferModelFailed(error: any, taskID: number): AnyAction {
const action = {
type: ModelsActionTypes.INFER_MODEL_FAILED,
payload: {
taskID,
error,
},
};
return action;
}
export function inferModelAsync(
export function startInferenceAsync(
taskInstance: any,
model: Model,
mapping: {
[index: string]: string;
},
cleanOut: boolean,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(inferModel());
): ThunkAction {
return async (dispatch): Promise<void> => {
try {
if (model.name === PreinstalledModels.RCNN) {
await core.server.request(
@ -604,30 +509,39 @@ export function inferModelAsync(
dispatch(getInferenceStatusAsync([taskInstance.id]));
} catch (error) {
dispatch(inferModelFailed(error, taskInstance.id));
return;
dispatch(modelsActions.startInferenceFailed(taskInstance.id, error));
}
dispatch(inferModelSuccess());
};
}
export function closeRunModelDialog(): AnyAction {
const action = {
type: ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG,
payload: {},
};
export function cancelInferenceAsync(taskID: number): ThunkAction {
return async (dispatch, getState): Promise<void> => {
try {
const inference = getState().models.inferences[taskID];
if (inference) {
if (inference.modelType === ModelType.OPENVINO) {
await core.server.request(
`${baseURL}/auto_annotation/cancel/${taskID}`,
);
} else if (inference.modelType === ModelType.RCNN) {
await core.server.request(
`${baseURL}/tensorflow/annotation/cancel/task/${taskID}`,
);
} else if (inference.modelType === ModelType.MASK_RCNN) {
await core.server.request(
`${baseURL}/tensorflow/segmentation/cancel/task/${taskID}`,
);
}
return action;
}
if (timers[taskID]) {
clearTimeout(timers[taskID]);
delete timers[taskID];
}
}
export function showRunModelDialog(taskInstance: any): AnyAction {
const action = {
type: ModelsActionTypes.SHOW_RUN_MODEL_DIALOG,
payload: {
taskInstance,
},
dispatch(modelsActions.cancelInferenceSuccess(taskID));
} catch (error) {
dispatch(modelsActions.cancelInferenceFaild(taskID, error));
}
};
return action;
}

@ -0,0 +1,11 @@
import { ActionUnion, createAction } from 'utils/redux';
export enum ShortcutsActionsTypes {
SWITCH_SHORTCUT_DIALOG = 'SWITCH_SHORTCUT_DIALOG',
}
export const shortcutsActions = {
switchShortcutsDialog: () => createAction(ShortcutsActionsTypes.SWITCH_SHORTCUT_DIALOG),
};
export type ShortcutsActions = ActionUnion<typeof shortcutsActions>;

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import {
Layout,
@ -12,17 +13,8 @@ import {
} from 'antd';
import { SliderValue } from 'antd/lib//slider';
import {
ColorBy,
GridColor,
ObjectType,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
import { ColorBy, GridColor, ObjectType } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
const cvat = getCore();
@ -75,6 +67,12 @@ interface Props {
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -109,6 +107,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
activatedStateID,
curZLayer,
resetZoom,
grid,
gridOpacity,
gridColor,
brightnessLevel,
contrastLevel,
saturationLevel,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -132,6 +136,31 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (gridOpacity !== prevProps.gridOpacity
|| gridColor !== prevProps.gridColor
|| grid !== prevProps.grid) {
const gridElement = window.document.getElementById('cvat_canvas_grid');
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridElement) {
gridElement.style.display = grid ? 'block' : 'none';
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
}
}
if (brightnessLevel !== prevProps.brightnessLevel
|| contrastLevel !== prevProps.contrastLevel
|| saturationLevel !== prevProps.saturationLevel) {
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter = `brightness(${brightnessLevel / 100})`
+ `contrast(${contrastLevel / 100})`
+ `saturate(${saturationLevel / 100})`;
}
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas();
}
@ -360,7 +389,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
// Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter = `brightness(${brightnessLevel / 100}) contrast(${contrastLevel / 100}) saturate(${saturationLevel / 100})`;
backgroundElement.style.filter = `brightness(${brightnessLevel / 100})`
+ `contrast(${contrastLevel / 100})`
+ `saturate(${saturationLevel / 100})`;
}
// Events
@ -374,6 +405,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
});
canvasInstance.html().addEventListener('click', (): void => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
}
});
canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const {
activatedStateID,
@ -488,10 +525,162 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
minZLayer,
onSwitchZLayer,
onAddZLayer,
brightnessLevel,
contrastLevel,
saturationLevel,
grid,
gridColor,
gridOpacity,
onChangeBrightnessLevel,
onChangeSaturationLevel,
onChangeContrastLevel,
onChangeGridColor,
onChangeGridOpacity,
onSwitchGrid,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const keyMap = {
INCREASE_BRIGHTNESS: {
name: 'Brightness+',
description: 'Increase brightness level for the image',
sequence: 'shift+b+=',
action: 'keypress',
},
DECREASE_BRIGHTNESS: {
name: 'Brightness-',
description: 'Decrease brightness level for the image',
sequence: 'shift+b+-',
action: 'keydown',
},
INCREASE_CONTRAST: {
name: 'Contrast+',
description: 'Increase contrast level for the image',
sequence: 'shift+c+=',
action: 'keydown',
},
DECREASE_CONTRAST: {
name: 'Contrast-',
description: 'Decrease contrast level for the image',
sequence: 'shift+c+-',
action: 'keydown',
},
INCREASE_SATURATION: {
name: 'Saturation+',
description: 'Increase saturation level for the image',
sequence: 'shift+s+=',
action: 'keydown',
},
DECREASE_SATURATION: {
name: 'Saturation-',
description: 'Increase contrast level for the image',
sequence: 'shift+s+-',
action: 'keydown',
},
INCREASE_GRID_OPACITY: {
name: 'Grid opacity+',
description: 'Make the grid more visible',
sequence: 'shift+g+=',
action: 'keydown',
},
DECREASE_GRID_OPACITY: {
name: 'Grid opacity-',
description: 'Make the grid less visible',
sequences: 'shift+g+-',
action: 'keydown',
},
CHANGE_GRID_COLOR: {
name: 'Grid color',
description: 'Set another color for the image grid',
sequence: 'shift+g+enter',
action: 'keydown',
},
};
const step = 10;
const handlers = {
INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 200;
if (brightnessLevel < maxLevel) {
onChangeBrightnessLevel(Math.min(brightnessLevel + step, maxLevel));
}
},
DECREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 50;
if (brightnessLevel > minLevel) {
onChangeBrightnessLevel(Math.max(brightnessLevel - step, minLevel));
}
},
INCREASE_CONTRAST: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 200;
if (contrastLevel < maxLevel) {
onChangeContrastLevel(Math.min(contrastLevel + step, maxLevel));
}
},
DECREASE_CONTRAST: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 50;
if (contrastLevel > minLevel) {
onChangeContrastLevel(Math.max(contrastLevel - step, minLevel));
}
},
INCREASE_SATURATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 300;
if (saturationLevel < maxLevel) {
onChangeSaturationLevel(Math.min(saturationLevel + step, maxLevel));
}
},
DECREASE_SATURATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 0;
if (saturationLevel > minLevel) {
onChangeSaturationLevel(Math.max(saturationLevel - step, minLevel));
}
},
INCREASE_GRID_OPACITY: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const maxLevel = 100;
if (!grid) {
onSwitchGrid(true);
}
if (gridOpacity < maxLevel) {
onChangeGridOpacity(Math.min(gridOpacity + step, maxLevel));
}
},
DECREASE_GRID_OPACITY: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const minLevel = 0;
if (gridOpacity - step <= minLevel) {
onSwitchGrid(false);
}
if (gridOpacity > minLevel) {
onChangeGridOpacity(Math.max(gridOpacity - step, minLevel));
}
},
CHANGE_GRID_COLOR: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const colors = [GridColor.Black, GridColor.Blue,
GridColor.Green, GridColor.Red, GridColor.White];
const indexOf = colors.indexOf(gridColor) + 1;
const color = colors[indexOf >= colors.length ? 0 : indexOf];
onChangeGridColor(color);
},
};
return (
<Layout.Content style={{ position: 'relative' }}>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
{/*
This element doesn't have any props
So, React isn't going to rerender it

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import {
Layout,
@ -39,6 +40,9 @@ interface Props {
groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void;
rotateFrame(rotation: Rotation): void;
repeatDrawShape(): void;
pasteShape(): void;
resetGroup(): void;
}
export default function ControlsSideBarComponent(props: Props): JSX.Element {
@ -50,14 +54,140 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
groupObjects,
splitTrack,
rotateFrame,
repeatDrawShape,
pasteShape,
resetGroup,
} = props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const keyMap = {
PASTE_SHAPE: {
name: 'Paste shape',
description: 'Paste a shape from internal CVAT clipboard',
sequence: 'ctrl+v',
action: 'keydown',
},
SWITCH_DRAW_MODE: {
name: 'Draw mode',
description: 'Repeat the latest procedure of drawing with the same parameters',
sequence: 'n',
action: 'keydown',
},
SWITCH_MERGE_MODE: {
name: 'Merge mode',
description: 'Activate or deactivate mode to merging shapes',
sequence: 'm',
action: 'keydown',
},
SWITCH_GROUP_MODE: {
name: 'Group mode',
description: 'Activate or deactivate mode to grouping shapes',
sequence: 'g',
action: 'keydown',
},
RESET_GROUP: {
name: 'Reset group',
description: 'Reset group for selected shapes (in group mode)',
sequence: 'shift+g',
action: 'keyup',
},
CANCEL: {
name: 'Cancel',
description: 'Cancel any active canvas mode',
sequence: 'esc',
action: 'keydown',
},
CLOCKWISE_ROTATION: {
name: 'Rotate clockwise',
description: 'Change image angle (add 90 degrees)',
sequence: 'ctrl+r',
action: 'keydown',
},
ANTICLOCKWISE_ROTATION: {
name: 'Rotate anticlockwise',
description: 'Change image angle (substract 90 degrees)',
sequence: 'ctrl+shift+r',
action: 'keydown',
},
};
const handlers = {
PASTE_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
canvasInstance.cancel();
pasteShape();
},
SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE].includes(activeControl);
if (!drawing) {
canvasInstance.cancel();
// repeateDrawShapes gets all the latest parameters
// and calls canvasInstance.draw() with them
repeatDrawShape();
} else {
canvasInstance.draw({ enabled: false });
}
},
SWITCH_MERGE_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const merging = activeControl === ActiveControl.MERGE;
if (!merging) {
canvasInstance.cancel();
}
canvasInstance.merge({ enabled: !merging });
mergeObjects(!merging);
},
SWITCH_GROUP_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
canvasInstance.cancel();
}
canvasInstance.group({ enabled: !grouping });
groupObjects(!grouping);
},
RESET_GROUP: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const grouping = activeControl === ActiveControl.GROUP;
if (!grouping) {
return;
}
resetGroup();
canvasInstance.group({ enabled: false });
groupObjects(false);
},
CANCEL: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeControl !== ActiveControl.CURSOR) {
canvasInstance.cancel();
}
},
CLOCKWISE_ROTATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
rotateFrame(Rotation.CLOCKWISE90);
},
ANTICLOCKWISE_ROTATION: (event: KeyboardEvent | undefined) => {
preventDefault(event);
rotateFrame(Rotation.ANTICLOCKWISE90);
},
};
return (
<Layout.Sider
className='cvat-canvas-controls-sidebar'
theme='light'
width={44}
>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<CursorControl canvasInstance={canvasInstance} activeControl={activeControl} />
<MoveControl canvasInstance={canvasInstance} activeControl={activeControl} />
<RotateControl rotateFrame={rotateFrame} />

@ -63,6 +63,7 @@ interface Props {
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
@ -76,6 +77,7 @@ interface Props {
function ObjectListHeader(props: Props): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
statesHidden,
statesLocked,
statesCollapsed,
@ -105,9 +107,12 @@ function ObjectListHeader(props: Props): JSX.Element {
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
dropdownStyle={{ display: 'none' }}
onChange={changeAnnotationsFilters}
/>
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
</Col>
</Row>
<Row type='flex' justify='space-between' align='middle'>

@ -18,6 +18,7 @@ interface Props {
statesOrdering: StatesOrdering;
sortedStatesID: number[];
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
@ -37,6 +38,7 @@ function ObjectListComponent(props: Props): JSX.Element {
statesOrdering,
sortedStatesID,
annotationsFilters,
annotationsFiltersHistory,
changeStatesOrdering,
changeAnnotationsFilters,
lockAllStates,
@ -63,6 +65,7 @@ function ObjectListComponent(props: Props): JSX.Element {
expandAllStates={expandAllStates}
hideAllStates={hideAllStates}
showAllStates={showAllStates}
annotationsFiltersHistory={annotationsFiltersHistory}
/>
<div className='cvat-objects-sidebar-states-list'>
{ sortedStatesID.map((id: number): JSX.Element => (

@ -20,6 +20,7 @@ interface Props {
startFrame: number;
stopFrame: number;
frameNumber: number;
inputFrameRef: React.RefObject<InputNumber>;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
onURLIconClick(): void;
@ -30,6 +31,7 @@ function PlayerNavigation(props: Props): JSX.Element {
startFrame,
stopFrame,
frameNumber,
inputFrameRef,
onSliderChange,
onInputChange,
onURLIconClick,
@ -69,6 +71,7 @@ function PlayerNavigation(props: Props): JSX.Element {
value={frameNumber || 0}
// https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput
onChange={onInputChange}
ref={inputFrameRef}
/>
</Col>
</>

@ -8,6 +8,7 @@ import {
Row,
Col,
Layout,
InputNumber,
} from 'antd';
import { SliderValue } from 'antd/lib/slider';
@ -22,6 +23,7 @@ interface Props {
saving: boolean;
savingStatuses: string[];
frameNumber: number;
inputFrameRef: React.RefObject<InputNumber>;
startFrame: number;
stopFrame: number;
undoAction?: string;
@ -42,7 +44,7 @@ interface Props {
onRedoClick(): void;
}
function AnnotationTopBarComponent(props: Props): JSX.Element {
export default function AnnotationTopBarComponent(props: Props): JSX.Element {
const {
saving,
savingStatuses,
@ -50,6 +52,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
redoAction,
playing,
frameNumber,
inputFrameRef,
startFrame,
stopFrame,
showStatistics,
@ -96,6 +99,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
inputFrameRef={inputFrameRef}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
onURLIconClick={onURLIconClick}
@ -107,5 +111,3 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
</Layout.Header>
);
}
export default React.memo(AnnotationTopBarComponent);

@ -5,18 +5,17 @@
import 'antd/dist/antd.less';
import '../styles.scss';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import {
Switch,
Route,
Redirect,
} from 'react-router';
import { Switch, Route, Redirect } from 'react-router';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { GlobalHotKeys, KeyMap, configure } from 'react-hotkeys';
import {
Spin,
Layout,
notification,
} from 'antd';
import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import SettingsPageContainer from 'containers/settings-page/settings-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
@ -30,7 +29,7 @@ import HeaderContainer from 'containers/header/header';
import { NotificationsState } from 'reducers/interfaces';
type CVATAppProps = {
interface CVATAppProps {
loadFormats: () => void;
loadUsers: () => void;
loadAbout: () => void;
@ -38,6 +37,7 @@ type CVATAppProps = {
initPlugins: () => void;
resetErrors: () => void;
resetMessages: () => void;
switchShortcutsDialog: () => void;
userInitialized: boolean;
pluginsInitialized: boolean;
pluginsFetching: boolean;
@ -52,11 +52,12 @@ type CVATAppProps = {
installedTFSegmentation: boolean;
notifications: NotificationsState;
user: any;
};
}
export default class CVATApplication extends React.PureComponent<CVATAppProps> {
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void {
const { verifyAuthorized } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
verifyAuthorized();
}
@ -190,7 +191,9 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
switchShortcutsDialog,
user,
history,
} = this.props;
const readyForRender = (userInitialized && user == null)
@ -200,13 +203,50 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
const keyMap = {
SWITCH_SHORTCUTS: {
name: 'Show shortcuts',
description: 'Open/hide the list of available shortcuts',
sequence: 'f1',
action: 'keydown',
},
OPEN_SETTINGS: {
name: 'Open settings',
description: 'Go to the settings page or go back',
sequence: 'f2',
action: 'keydown',
},
};
const handlers = {
SWITCH_SHORTCUTS: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
switchShortcutsDialog();
},
OPEN_SETTINGS: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
if (history.location.pathname.endsWith('settings')) {
history.goBack();
} else {
history.push('/settings');
}
},
};
if (readyForRender) {
if (user) {
return (
<BrowserRouter>
<Layout>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<Layout>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<ShorcutsDialog />
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers}>
<Switch>
<Route exact path='/settings' component={SettingsPageContainer} />
<Route exact path='/tasks' component={TasksPageContainer} />
@ -219,22 +259,20 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
&& <Route exact path='/models/create' component={CreateModelPageContainer} /> }
<Redirect push to='/tasks' />
</Switch>
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout.Content>
</Layout>
</BrowserRouter>
</GlobalHotKeys>
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout.Content>
</Layout>
);
}
return (
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Redirect to='/auth/login' />
</Switch>
</BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Redirect to='/auth/login' />
</Switch>
);
}
@ -243,3 +281,5 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
);
}
}
export default withRouter(CVATApplication);

@ -0,0 +1,97 @@
import React from 'react';
import { getApplicationKeyMap } from 'react-hotkeys';
import { Modal, Table } from 'antd';
import { connect } from 'react-redux';
import { shortcutsActions } from 'actions/shortcuts-actions';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
visible: boolean;
}
interface DispatchToProps {
switchShortcutsDialog(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
shortcuts: {
visibleShortcutsHelp: visible,
},
} = state;
return {
visible,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
switchShortcutsDialog(): void {
dispatch(shortcutsActions.switchShortcutsDialog());
},
};
}
function ShorcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | null {
const { visible, switchShortcutsDialog } = props;
const keyMap = getApplicationKeyMap();
const splitToRows = (data: string[]): JSX.Element[] => (
data.map((item: string, id: number): JSX.Element => (
// eslint-disable-next-line react/no-array-index-key
<span key={id}>
{item}
<br />
</span>
))
);
const columns = [{
title: 'Name',
dataIndex: 'name',
key: 'name',
}, {
title: 'Shortcut',
dataIndex: 'shortcut',
key: 'shortcut',
render: splitToRows,
}, {
title: 'Action',
dataIndex: 'action',
key: 'action',
render: splitToRows,
}, {
title: 'Description',
dataIndex: 'description',
key: 'description',
}];
const dataSource = Object.keys(keyMap).map((key: string, id: number) => ({
key: id,
name: keyMap[key].name || key,
description: keyMap[key].description || '',
shortcut: keyMap[key].sequences.map((value) => value.sequence),
action: keyMap[key].sequences.map((value) => value.action || 'keydown'),
}));
return (
<Modal
title='Active list of shortcuts'
visible={visible}
closable={false}
width={800}
onOk={switchShortcutsDialog}
cancelButtonProps={{ style: { display: 'none' } }}
zIndex={1001} /* default antd is 1000 */
>
<Table dataSource={dataSource} columns={columns} size='small' />
</Modal>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ShorcutsDialog);

@ -87,37 +87,11 @@
padding-top: 20px;
background: $background-color-1;
/* description */
> div:nth-child(2) {
word-break: break-all;
max-height: 100%;
overflow: hidden;
}
/* open, actions */
div:nth-child(4) {
> div {
margin-right: 20px;
}
/* actions */
> div:nth-child(2) {
margin-right: 5px;
margin-top: 10px;
> div {
display: flex;
align-items: center;
}
}
}
&:hover {
border: 1px solid $border-color-hover;
}
}
.cvat-task-item-preview-wrapper {
display: flex;
justify-content: center;
@ -131,6 +105,12 @@
}
}
.cvat-task-item-description {
word-break: break-all;
max-height: 100%;
overflow: hidden;
}
.cvat-task-progress {
width: 100%;
}
@ -159,6 +139,26 @@
margin-right: 5px;
}
.close-auto-annotation-icon {
color: $danger-icon-color;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.cvat-item-open-task-actions {
margin-right: 5px;
margin-top: 10px;
display: flex;
align-items: center;
}
.cvat-item-open-task-button {
margin-right: 20px;
}
#cvat-create-task-button {
padding: 0 30px;
}

@ -14,6 +14,8 @@ import {
Icon,
Progress,
Dropdown,
Tooltip,
Modal,
} from 'antd';
import moment from 'moment';
@ -28,6 +30,7 @@ export interface TaskItemProps {
deleted: boolean;
hidden: boolean;
activeInference: ActiveInference | null;
cancelAutoAnnotation(): void;
}
class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteComponentProps> {
@ -54,7 +57,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
const name = `${taskInstance.name.substring(0, 70)}${taskInstance.name.length > 70 ? '...' : ''}`;
return (
<Col span={10}>
<Col span={10} className='cvat-task-item-description'>
<Text strong type='secondary'>{`#${id}: `}</Text>
<Text strong className='cvat-text-color'>{name}</Text>
<br />
@ -76,6 +79,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
const {
taskInstance,
activeInference,
cancelAutoAnnotation,
} = this.props;
// Count number of jobs and performed jobs
const numOfJobs = taskInstance.jobs.length;
@ -132,8 +136,8 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
<Text strong>Automatic annotation</Text>
</Col>
</Row>
<Row>
<Col>
<Row type='flex' justify='space-between'>
<Col span={22}>
<Progress
percent={Math.floor(activeInference.progress)}
strokeColor={{
@ -145,6 +149,23 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
size='small'
/>
</Col>
<Col span={1} className='close-auto-annotation-icon'>
<Tooltip title='Cancel automatic annotation'>
<Icon
type='close'
onClick={() => {
Modal.confirm({
title: 'You are going to cancel automatic annotation?',
content: 'Reached progress will be lost. Continue?',
okType: 'danger',
onOk() {
cancelAutoAnnotation();
},
});
}}
/>
</Tooltip>
</Col>
</Row>
</>
)}
@ -164,6 +185,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
<Row type='flex' justify='end'>
<Col>
<Button
className='cvat-item-open-task-button'
type='primary'
size='large'
ghost
@ -174,7 +196,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
</Col>
</Row>
<Row type='flex' justify='end'>
<Col>
<Col className='cvat-item-open-task-actions'>
<Text className='cvat-text-color'>Actions</Text>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Icon className='cvat-menu-icon' component={MenuIcon} />

@ -10,7 +10,7 @@ import {
CombinedState,
} from 'reducers/interfaces';
import { showRunModelDialog } from 'actions/models-actions';
import { modelsActions } from 'actions/models-actions';
import {
dumpAnnotationsAsync,
loadAnnotationsAsync,
@ -99,7 +99,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(deleteTaskAsync(taskInstance));
},
openRunModelWindow: (taskInstance: any): void => {
dispatch(showRunModelDialog(taskInstance));
dispatch(modelsActions.showRunModelDialog(taskInstance));
},
};
}

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
@ -80,15 +79,10 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
};
}
function AnnotationPageContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<AnnotationPageComponent {...props} />
);
}
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationPageContainer),
)(AnnotationPageComponent),
);

@ -2,11 +2,9 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import CanvasWrapperComponent from 'components/annotation-page/standard-workspace/canvas-wrapper';
import {
confirmCanvasReady,
dragCanvas,
@ -28,6 +26,14 @@ import {
addZLayer,
switchZLayer,
} from 'actions/annotation-actions';
import {
switchGrid,
changeGridColor,
changeGridOpacity,
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
} from 'actions/settings-actions';
import {
ColorBy,
GridColor,
@ -86,6 +92,12 @@ interface DispatchToProps {
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -233,16 +245,28 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur));
},
onChangeBrightnessLevel(level: number): void {
dispatch(changeBrightnessLevel(level));
},
onChangeContrastLevel(level: number): void {
dispatch(changeContrastLevel(level));
},
onChangeSaturationLevel(level: number): void {
dispatch(changeSaturationLevel(level));
},
onChangeGridOpacity(opacity: number): void {
dispatch(changeGridOpacity(opacity));
},
onChangeGridColor(color: GridColor): void {
dispatch(changeGridColor(color));
},
onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled));
},
};
}
function CanvasWrapperContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CanvasWrapperComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CanvasWrapperContainer);
)(CanvasWrapperComponent);

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from 'cvat-canvas';
@ -12,6 +11,9 @@ import {
groupObjects,
splitTrack,
rotateCurrentFrame,
repeatDrawShapeAsync,
pasteShapeAsync,
resetAnnotationsGroup,
} from 'actions/annotation-actions';
import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import {
@ -31,6 +33,9 @@ interface DispatchToProps {
groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void;
rotateFrame(angle: Rotation): void;
resetGroup(): void;
repeatDrawShape(): void;
pasteShape(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -69,16 +74,19 @@ function dispatchToProps(dispatch: any): DispatchToProps {
rotateFrame(rotation: Rotation): void {
dispatch(rotateCurrentFrame(rotation));
},
repeatDrawShape(): void {
dispatch(repeatDrawShapeAsync());
},
pasteShape(): void {
dispatch(pasteShapeAsync());
},
resetGroup(): void {
dispatch(resetAnnotationsGroup());
},
};
}
function ControlsSideBarContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<ControlsSideBarComponent {...props} />
);
}
export default connect(
mapStateToProps,
dispatchToProps,
)(ControlsSideBarContainer);
)(ControlsSideBarComponent);

@ -115,8 +115,11 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
let statesLocked = true;
ownObjectStates.forEach((objectState: any) => {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
const { lock } = objectState;
if (!lock) {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
}
});
return {

@ -2,7 +2,6 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import LabelsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list';
@ -29,12 +28,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
};
}
function LabelsListContainer(props: StateToProps): JSX.Element {
return (
<LabelsListComponent {...props} />
);
}
export default connect(
mapStateToProps,
)(LabelsListContainer);
)(LabelsListComponent);

@ -22,6 +22,7 @@ import {
copyShape as copyShapeAction,
activateObject as activateObjectAction,
propagateObject as propagateObjectAction,
pasteShapeAsync,
} from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
@ -143,6 +144,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
dispatch(pasteShapeAsync());
},
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));

@ -4,6 +4,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { SelectValue } from 'antd/lib/select';
@ -11,13 +12,18 @@ import ObjectsListComponent from 'components/annotation-page/standard-workspace/
import {
updateAnnotationsAsync,
fetchAnnotationsAsync,
removeObjectAsync,
changeFrameAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems,
copyShape as copyShapeAction,
propagateObject as propagateObjectAction,
} from 'actions/annotation-actions';
import {
CombinedState,
StatesOrdering,
ObjectType,
} from 'reducers/interfaces';
interface StateToProps {
@ -29,12 +35,20 @@ interface StateToProps {
statesCollapsed: boolean;
objectStates: any[];
annotationsFilters: string[];
activatedStateID: number | null;
minZLayer: number;
maxZLayer: number;
annotationsFiltersHistory: string[];
}
interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
collapseStates(states: any[], value: boolean): void;
removeObject: (sessionInstance: any, objectState: any, force: boolean) => void;
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
changeFrame(frame: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -43,7 +57,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotations: {
states: objectStates,
filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory,
collapsed,
activatedStateID,
zLayer: {
min: minZLayer,
max: maxZLayer,
},
},
job: {
instance: jobInstance,
@ -62,9 +82,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
let statesCollapsed = true;
objectStates.forEach((objectState: any) => {
const { clientID } = objectState;
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
const { clientID, lock } = objectState;
if (!lock) {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
}
const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true;
statesCollapsed = statesCollapsed && stateCollapsed;
});
@ -78,6 +100,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameNumber,
jobInstance,
annotationsFilters,
activatedStateID,
minZLayer,
maxZLayer,
annotationsFiltersHistory,
};
}
@ -96,6 +122,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance));
},
removeObject(sessionInstance: any, objectState: any, force: boolean): void {
dispatch(removeObjectAsync(sessionInstance, objectState, force));
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
},
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));
},
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
};
}
@ -221,27 +259,263 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
}
public render(): JSX.Element {
const { annotationsFilters } = this.props;
const {
annotationsFilters,
statesHidden,
statesLocked,
activatedStateID,
objectStates,
frameNumber,
jobInstance,
updateAnnotations,
removeObject,
copyShape,
propagateObject,
changeFrame,
maxZLayer,
minZLayer,
annotationsFiltersHistory,
} = this.props;
const {
sortedStatesID,
statesOrdering,
} = this.state;
const keyMap = {
SWITCH_ALL_LOCK: {
name: 'Lock/unlock all objects',
description: 'Change locked state for all objects in the side bar',
sequence: 't+l',
action: 'keydown',
},
SWITCH_LOCK: {
name: 'Lock/unlock an object',
description: 'Change locked state for an active object',
sequence: 'l',
action: 'keydown',
},
SWITCH_ALL_HIDDEN: {
name: 'Hide/show all objects',
description: 'Change hidden state for objects in the side bar',
sequence: 't+h',
action: 'keydown',
},
SWITCH_HIDDEN: {
name: 'Hide/show an object',
description: 'Change hidden state for an active object',
sequence: 'h',
action: 'keydown',
},
SWITCH_OCCLUDED: {
name: 'Switch occluded',
description: 'Change occluded property for an active object',
sequences: ['q', '/'],
action: 'keydown',
},
SWITCH_KEYFRAME: {
name: 'Switch keyframe',
description: 'Change keyframe property for an active track',
sequence: 'k',
action: 'keydown',
},
SWITCH_OUTSIDE: {
name: 'Switch outside',
description: 'Change outside property for an active track',
sequence: 'o',
action: 'keydown',
},
DELETE_OBJECT: {
name: 'Delete object',
description: 'Delete an active object. Use shift to force delete of locked objects',
sequences: ['del', 'shift+del'],
action: 'keydown',
},
TO_BACKGROUND: {
name: 'To background',
description: 'Put an active object "farther" from the user (decrease z axis value)',
sequences: ['-', '_'],
action: 'keydown',
},
TO_FOREGROUND: {
name: 'To foreground',
description: 'Put an active object "closer" to the user (increase z axis value)',
sequences: ['+', '='],
action: 'keydown',
},
COPY_SHAPE: {
name: 'Copy shape',
description: 'Copy shape to CVAT internal clipboard',
sequence: 'ctrl+c',
action: 'keydown',
},
PROPAGATE_OBJECT: {
name: 'Propagate object',
description: 'Make a copy of the object on the following frames',
sequence: 'ctrl+b',
action: 'keydown',
},
NEXT_KEY_FRAME: {
name: 'Next keyframe',
description: 'Go to the next keyframe of an active track',
sequence: 'r',
action: 'keydown',
},
PREV_KEY_FRAME: {
name: 'Previous keyframe',
description: 'Go to the previous keyframe of an active track',
sequence: 'e',
action: 'keydown',
},
};
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const activatedStated = (): any | null => {
if (activatedStateID !== null) {
const [state] = objectStates
.filter((objectState: any): boolean => (
objectState.clientID === activatedStateID
));
return state || null;
}
return null;
};
const handlers = {
SWITCH_ALL_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.lockAllStates(!statesLocked);
},
SWITCH_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state) {
state.lock = !state.lock;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.hideAllStates(!statesHidden);
},
SWITCH_HIDDEN: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state) {
state.hidden = !state.hidden;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.occluded = !state.occluded;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
state.keyframe = !state.keyframe;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
state.outside = !state.outside;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
DELETE_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state) {
removeObject(jobInstance, state, event ? event.shiftKey : false);
}
},
TO_BACKGROUND: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = minZLayer - 1;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
TO_FOREGROUND: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = maxZLayer + 1;
updateAnnotations(jobInstance, frameNumber, [state]);
}
},
COPY_SHAPE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
copyShape(state);
}
},
PROPAGATE_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
propagateObject(state);
}
},
NEXT_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
const frame = typeof (state.keyframes.next) === 'number'
? state.keyframes.next : null;
if (frame !== null) {
changeFrame(frame);
}
}
},
PREV_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
const frame = typeof (state.keyframes.prev) === 'number'
? state.keyframes.prev : null;
if (frame !== null) {
changeFrame(frame);
}
}
},
};
return (
<ObjectsListComponent
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}
expandAllStates={this.onExpandAllStates}
hideAllStates={this.onHideAllStates}
showAllStates={this.onShowAllStates}
/>
<>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<ObjectsListComponent
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
annotationsFiltersHistory={annotationsFiltersHistory}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}
expandAllStates={this.onExpandAllStates}
hideAllStates={this.onHideAllStates}
showAllStates={this.onShowAllStates}
/>
</>
);
}
}

@ -8,7 +8,9 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { RouteComponentProps } from 'react-router-dom';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { InputNumber } from 'antd';
import { SliderValue } from 'antd/lib/slider';
import {
@ -19,6 +21,7 @@ import {
showStatistics as showStatisticsAction,
undoActionAsync,
redoActionAsync,
searchAnnotationsAsync,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -47,6 +50,7 @@ interface DispatchToProps {
showStatistics(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -123,14 +127,23 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber));
},
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
},
};
}
type Props = StateToProps & DispatchToProps & RouteComponentProps;
class AnnotationTopBarContainer extends React.PureComponent<Props> {
private inputFrameRef: React.RefObject<InputNumber>;
private autoSaveInterval: number | undefined;
private unblock: any;
constructor(props: Props) {
super(props);
this.inputFrameRef = React.createRef<InputNumber>();
}
public componentDidMount(): void {
const {
autoSave,
@ -421,6 +434,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
playing,
saving,
savingStatuses,
jobInstance,
jobInstance: {
startFrame,
stopFrame,
@ -428,33 +442,179 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
frameNumber,
undoAction,
redoAction,
searchAnnotations,
canvasIsReady,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const keyMap = {
SAVE_JOB: {
name: 'Save the job',
description: 'Send all changes of annotations to the server',
sequence: 'ctrl+s',
action: 'keydown',
},
UNDO: {
name: 'Undo action',
description: 'Cancel the latest action related with objects',
sequence: 'ctrl+z',
action: 'keydown',
},
REDO: {
name: 'Redo action',
description: 'Cancel undo action',
sequences: ['ctrl+shift+z', 'ctrl+y'],
action: 'keydown',
},
NEXT_FRAME: {
name: 'Next frame',
description: 'Go to the next frame',
sequence: 'f',
action: 'keydown',
},
PREV_FRAME: {
name: 'Previous frame',
description: 'Go to the previous frame',
sequence: 'd',
action: 'keydown',
},
FORWARD_FRAME: {
name: 'Forward frame',
description: 'Go forward with a step',
sequence: 'v',
action: 'keydown',
},
BACKWARD_FRAME: {
name: 'Backward frame',
description: 'Go backward with a step',
sequence: 'c',
action: 'keydown',
},
SEARCH_FORWARD: {
name: 'Search forward',
description: 'Search the next frame that satisfies to the filters',
sequence: 'right',
action: 'keydown',
},
SEARCH_BACKWARD: {
name: 'Search backward',
description: 'Search the previous frame that satisfies to the filters',
sequence: 'left',
action: 'keydown',
},
PLAY_PAUSE: {
name: 'Play/pause',
description: 'Start/stop automatic changing frames',
sequence: 'space',
action: 'keydown',
},
FOCUS_INPUT_FRAME: {
name: 'Focus input frame',
description: 'Focus on the element to change the current frame',
sequences: ['`', '~'],
action: 'keydown',
},
};
const handlers = {
UNDO: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (undoAction) {
this.undo();
}
},
REDO: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (redoAction) {
this.redo();
}
},
SAVE_JOB: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.onSaveAnnotation();
},
NEXT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onNextFrame();
}
},
PREV_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onPrevFrame();
}
},
FORWARD_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onForward();
}
},
BACKWARD_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onBackward();
}
},
SEARCH_FORWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber + 1 <= stopFrame && canvasIsReady) {
searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
}
},
SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber - 1 >= startFrame && canvasIsReady) {
searchAnnotations(jobInstance, frameNumber - 1, startFrame);
}
},
PLAY_PAUSE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.onSwitchPlay();
},
FOCUS_INPUT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (this.inputFrameRef.current) {
this.inputFrameRef.current.focus();
}
},
};
return (
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}
onNextFrame={this.onNextFrame}
onForward={this.onForward}
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/>
<>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}
onNextFrame={this.onNextFrame}
onForward={this.onForward}
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
inputFrameRef={this.inputFrameRef}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/>
</>
);
}
}

@ -12,8 +12,8 @@ import {
} from 'reducers/interfaces';
import {
getModelsAsync,
inferModelAsync,
closeRunModelDialog,
startInferenceAsync,
modelsActions,
} from 'actions/models-actions';
@ -64,13 +64,13 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
},
cleanOut: boolean,
): void {
dispatch(inferModelAsync(taskInstance, model, mapping, cleanOut));
dispatch(startInferenceAsync(taskInstance, model, mapping, cleanOut));
},
getModels(): void {
dispatch(getModelsAsync());
},
closeDialog(): void {
dispatch(closeRunModelDialog());
dispatch(modelsActions.closeRunModelDialog());
},
});
}

@ -13,9 +13,8 @@ import {
import TaskItemComponent from 'components/tasks-page/task-item';
import {
getTasksAsync,
} from 'actions/tasks-actions';
import { getTasksAsync } from 'actions/tasks-actions';
import { cancelInferenceAsync } from 'actions/models-actions';
interface StateToProps {
deleted: boolean;
@ -26,7 +25,8 @@ interface StateToProps {
}
interface DispatchToProps {
getTasks: (query: TasksQuery) => void;
getTasks(query: TasksQuery): void;
cancelAutoAnnotation(): void;
}
interface OwnProps {
@ -48,23 +48,18 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
return {
getTasks: (query: TasksQuery): void => {
getTasks(query: TasksQuery): void {
dispatch(getTasksAsync(query));
},
cancelAutoAnnotation(): void {
dispatch(cancelInferenceAsync(own.taskID));
},
};
}
type TasksItemContainerProps = StateToProps & DispatchToProps & OwnProps;
function TaskItemContainer(props: TasksItemContainerProps): JSX.Element {
return (
<TaskItemComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TaskItemContainer);
)(TaskItemComponent);

@ -4,12 +4,14 @@
import {
Canvas,
CanvasMode,
CanvasVersion,
RectDrawingMethod,
} from '../../cvat-canvas/src/typescript/canvas';
export {
Canvas,
CanvasMode,
CanvasVersion,
RectDrawingMethod,
};

@ -5,6 +5,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import CVATApplication from './components/cvat-app';
@ -16,6 +17,7 @@ import { getFormatsAsync } from './actions/formats-actions';
import { checkPluginsAsync } from './actions/plugins-actions';
import { getUsersAsync } from './actions/users-actions';
import { getAboutAsync } from './actions/about-actions';
import { shortcutsActions } from './actions/shortcuts-actions';
import {
resetErrors,
resetMessages,
@ -54,6 +56,7 @@ interface DispatchToProps {
initPlugins: () => void;
resetErrors: () => void;
resetMessages: () => void;
switchShortcutsDialog: () => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -90,24 +93,21 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadAbout: (): void => dispatch(getAboutAsync()),
resetErrors: (): void => dispatch(resetErrors()),
resetMessages: (): void => dispatch(resetMessages()),
switchShortcutsDialog: (): void => dispatch(shortcutsActions.switchShortcutsDialog()),
};
}
function reduxAppWrapper(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CVATApplication {...props} />
);
}
const ReduxAppWrapper = connect(
mapStateToProps,
mapDispatchToProps,
)(reduxAppWrapper);
)(CVATApplication);
ReactDOM.render(
(
<Provider store={cvatStore}>
<ReduxAppWrapper />
<BrowserRouter>
<ReduxAppWrapper />
</BrowserRouter>
</Provider>
),
document.getElementById('root'),

@ -4,7 +4,7 @@
import { AnyAction } from 'redux';
import { Canvas } from 'cvat-canvas';
import { Canvas, CanvasMode } from 'cvat-canvas';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { AuthActionTypes } from 'actions/auth-actions';
import {
@ -61,6 +61,10 @@ const defaultState: AnnotationState = {
collapsed: {},
states: [],
filters: [],
filtersHistory: JSON.parse(
window.localStorage.getItem('filtersHistory') || '[]',
),
resetGroupFlag: false,
history: {
undo: [],
redo: [],
@ -442,6 +446,21 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.REPEAT_DRAW_SHAPE: {
const { activeControl } = action.payload;
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.MERGE_OBJECTS: {
const { enabled } = action.payload;
const activeControl = enabled
@ -577,6 +596,24 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.RESET_ANNOTATIONS_GROUP: {
return {
...state,
annotations: {
...state.annotations,
resetGroupFlag: true,
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS: {
return {
...state,
annotations: {
...state.annotations,
resetGroupFlag: false,
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: {
const {
states,
@ -633,9 +670,17 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.ACTIVATE_OBJECT: {
const { activatedStateID } = action.payload;
const {
activatedStateID,
} = action.payload;
canvas: {
activeControl,
instance,
},
} = state;
if (activeControl !== ActiveControl.CURSOR || instance.mode() !== CanvasMode.IDLE) {
return state;
}
return {
...state,
@ -677,10 +722,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.COPY_SHAPE: {
const {
activeControl,
} = action.payload;
case AnnotationActionTypes.PASTE_SHAPE: {
const { activeControl } = action.payload;
return {
...state,
@ -694,6 +737,19 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.COPY_SHAPE: {
const {
objectState,
} = action.payload;
return {
...state,
drawing: {
...state.drawing,
activeInitialState: objectState,
},
};
}
case AnnotationActionTypes.EDIT_SHAPE: {
const { enabled } = action.payload;
const activeControl = enabled
@ -955,11 +1011,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS: {
const { filters } = action.payload;
const { filters, filtersHistory } = action.payload;
return {
...state,
annotations: {
...state.annotations,
filtersHistory,
filters,
},
};

@ -134,10 +134,17 @@ export enum RQStatus {
failed = 'failed',
}
export enum ModelType {
OPENVINO = 'openvino',
RCNN = 'rcnn',
MASK_RCNN = 'mask_rcnn',
}
export interface ActiveInference {
status: RQStatus;
progress: number;
error: string;
modelType: ModelType;
}
export interface ModelsState {
@ -199,6 +206,7 @@ export interface NotificationsState {
starting: null | ErrorState;
deleting: null | ErrorState;
fetching: null | ErrorState;
canceling: null | ErrorState;
metaFetching: null | ErrorState;
inferenceStatusFetching: null | ErrorState;
};
@ -221,6 +229,7 @@ export interface NotificationsState {
fetchingAnnotations: null | ErrorState;
undo: null | ErrorState;
redo: null | ErrorState;
search: null | ErrorState;
};
[index: string]: any;
@ -321,6 +330,7 @@ export interface AnnotationState {
activeNumOfPoints?: number;
activeLabelID: number;
activeObjectType: ObjectType;
activeInitialState?: any;
};
annotations: {
selectedStatesID: number[];
@ -328,6 +338,8 @@ export interface AnnotationState {
collapsed: Record<number, boolean>;
states: any[];
filters: string[];
filtersHistory: string[];
resetGroupFlag: boolean;
history: {
undo: string[];
redo: string[];
@ -414,6 +426,10 @@ export interface SettingsState {
player: PlayerSettingsState;
}
export interface ShortcutsState {
visibleShortcutsHelp: boolean;
}
export interface CombinedState {
auth: AuthState;
tasks: TasksState;
@ -426,4 +442,5 @@ export interface CombinedState {
notifications: NotificationsState;
annotation: AnnotationState;
settings: SettingsState;
shortcuts: ShortcutsState;
}

@ -2,10 +2,8 @@
//
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
import { ModelsActionTypes } from 'actions/models-actions';
import { AuthActionTypes } from 'actions/auth-actions';
import { ModelsActionTypes, ModelsActions } from 'actions/models-actions';
import { AuthActionTypes, AuthActions } from 'actions/auth-actions';
import { ModelsState } from './interfaces';
const defaultState: ModelsState = {
@ -18,7 +16,7 @@ const defaultState: ModelsState = {
inferences: {},
};
export default function (state = defaultState, action: AnyAction): ModelsState {
export default function (state = defaultState, action: ModelsActions | AuthActions): ModelsState {
switch (action.type) {
case ModelsActionTypes.GET_MODELS: {
return {
@ -90,7 +88,7 @@ export default function (state = defaultState, action: AnyAction): ModelsState {
};
}
case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: {
const inferences = { ...state.inferences };
const { inferences } = state;
if (action.payload.activeInference.status === 'finished') {
delete inferences[action.payload.taskID];
} else {
@ -99,16 +97,25 @@ export default function (state = defaultState, action: AnyAction): ModelsState {
return {
...state,
inferences,
inferences: { ...inferences },
};
}
case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: {
const inferences = { ...state.inferences };
const { inferences } = state;
delete inferences[action.payload.taskID];
return {
...state,
inferences: { ...inferences },
};
}
case ModelsActionTypes.CANCEL_INFERENCE_SUCCESS: {
const { inferences } = state;
delete inferences[action.payload.taskID];
return {
...state,
inferences,
inferences: { ...inferences },
};
}
case AuthActionTypes.LOGOUT_SUCCESS: {

@ -50,6 +50,7 @@ const defaultState: NotificationsState = {
starting: null,
deleting: null,
fetching: null,
canceling: null,
metaFetching: null,
inferenceStatusFetching: null,
},
@ -72,6 +73,7 @@ const defaultState: NotificationsState = {
fetchingAnnotations: null,
undo: null,
redo: null,
search: null,
},
},
messages: {
@ -432,7 +434,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case ModelsActionTypes.INFER_MODEL_FAILED: {
case ModelsActionTypes.START_INFERENCE_FAILED: {
const { taskID } = action.payload;
return {
...state,
@ -449,6 +451,23 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case ModelsActionTypes.CANCEL_INFERENCE_FAILED: {
const { taskID } = action.payload;
return {
...state,
errors: {
...state.errors,
models: {
...state.errors.models,
canceling: {
message: 'Could not cancel model inference for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.GET_JOB_FAILED: {
return {
...state,
@ -732,6 +751,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.SEARCH_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
search: {
message: 'Could not execute search annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,

@ -14,6 +14,7 @@ import modelsReducer from './models-reducer';
import notificationsReducer from './notifications-reducer';
import annotationReducer from './annotation-reducer';
import settingsReducer from './settings-reducer';
import shortcutsReducer from './shortcuts-reducer';
export default function createRootReducer(): Reducer {
return combineReducers({
@ -28,5 +29,6 @@ export default function createRootReducer(): Reducer {
notifications: notificationsReducer,
annotation: annotationReducer,
settings: settingsReducer,
shortcuts: shortcutsReducer,
});
}

@ -0,0 +1,20 @@
import { ShortcutsActions, ShortcutsActionsTypes } from 'actions/shortcuts-actions';
import { ShortcutsState } from './interfaces';
const defaultState: ShortcutsState = {
visibleShortcutsHelp: false,
};
export default (state = defaultState, action: ShortcutsActions): ShortcutsState => {
switch (action.type) {
case ShortcutsActionsTypes.SWITCH_SHORTCUT_DIALOG: {
return {
...state,
visibleShortcutsHelp: !state.visibleShortcutsHelp,
};
}
default: {
return state;
}
}
};

@ -120,6 +120,7 @@ class Annotation:
self._MAX_ANNO_SIZE=30000
self._frame_info = {}
self._frame_mapping = {}
self._frame_step = db_task.get_frame_step()
db_labels = self._db_task.label_set.all().prefetch_related('attributespec_set').order_by('pk')
@ -270,7 +271,7 @@ class Annotation:
def _export_tracked_shape(self, shape):
return Annotation.TrackedShape(
type=shape["type"],
frame=self._db_task.start_frame + shape["frame"] * self._db_task.get_frame_step(),
frame=self._db_task.start_frame + shape["frame"] * self._frame_step,
points=shape["points"],
occluded=shape["occluded"],
outside=shape.get("outside", False),
@ -283,7 +284,7 @@ class Annotation:
return Annotation.LabeledShape(
type=shape["type"],
label=self._get_label_name(shape["label_id"]),
frame=self._db_task.start_frame + shape["frame"] * self._db_task.get_frame_step(),
frame=self._db_task.start_frame + shape["frame"] * self._frame_step,
points=shape["points"],
occluded=shape["occluded"],
z_order=shape.get("z_order", 0),
@ -293,7 +294,7 @@ class Annotation:
def _export_tag(self, tag):
return Annotation.Tag(
frame=self._db_task.start_frame + tag["frame"] * self._db_task.get_frame_step(),
frame=self._db_task.start_frame + tag["frame"] * self._frame_step,
label=self._get_label_name(tag["label_id"]),
group=tag.get("group", 0),
attributes=self._export_attributes(tag["attributes"]),
@ -302,7 +303,7 @@ class Annotation:
def group_by_frame(self):
def _get_frame(annotations, shape):
db_image = self._frame_info[shape["frame"]]
frame = self._db_task.start_frame + shape["frame"] * self._db_task.get_frame_step()
frame = self._db_task.start_frame + shape["frame"] * self._frame_step
rpath = db_image['path'].split(os.path.sep)
if len(rpath) != 1:
rpath = os.path.sep.join(rpath[rpath.index(".upload")+1:])
@ -359,6 +360,7 @@ class Annotation:
def _import_tag(self, tag):
_tag = tag._asdict()
label_id = self._get_label_id(_tag.pop('label'))
_tag['frame'] = (int(_tag['frame']) - self._db_task.start_frame) // self._frame_step
_tag['label_id'] = label_id
_tag['attributes'] = [self._import_attribute(label_id, attrib) for attrib in _tag['attributes']
if self._get_attribute_id(label_id, attrib.name)]
@ -373,6 +375,7 @@ class Annotation:
def _import_shape(self, shape):
_shape = shape._asdict()
label_id = self._get_label_id(_shape.pop('label'))
_shape['frame'] = (int(_shape['frame']) - self._db_task.start_frame) // self._frame_step
_shape['label_id'] = label_id
_shape['attributes'] = [self._import_attribute(label_id, attrib) for attrib in _shape['attributes']
if self._get_attribute_id(label_id, attrib.name)]
@ -381,11 +384,13 @@ class Annotation:
def _import_track(self, track):
_track = track._asdict()
label_id = self._get_label_id(_track.pop('label'))
_track['frame'] = min(shape.frame for shape in _track['shapes'])
_track['frame'] = (min(int(shape.frame) for shape in _track['shapes']) - \
self._db_task.start_frame) // self._frame_step
_track['label_id'] = label_id
_track['attributes'] = []
_track['shapes'] = [shape._asdict() for shape in _track['shapes']]
for shape in _track['shapes']:
shape['frame'] = (int(shape['frame']) - self._db_task.start_frame) // self._frame_step
_track['attributes'] = [self._import_attribute(label_id, attrib) for attrib in shape['attributes']
if self._get_immutable_attribute_id(label_id, attrib.name)]
shape['attributes'] = [self._import_attribute(label_id, attrib) for attrib in shape['attributes']
@ -431,6 +436,10 @@ class Annotation:
def frame_info(self):
return self._frame_info
@property
def frame_step(self):
return self._frame_step
@staticmethod
def _get_filename(path):
return os.path.splitext(os.path.basename(path))[0]

@ -415,7 +415,7 @@ def dump_as_cvat_interpolation(file_object, annotations):
outside=True,
keyframe=True,
z_order=shape.z_order,
frame=shape.frame + 1,
frame=shape.frame + annotations.frame_step,
attributes=shape.attributes,
),
],
@ -466,7 +466,7 @@ def load(file_object, annotations):
if el.tag == 'attribute' and attributes is not None:
attributes.append(annotations.Attribute(
name=el.attrib['name'],
value=el.text,
value=el.text or "",
))
if el.tag in supported_shapes:
if track is not None:

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

@ -28,6 +28,7 @@
- [Annotation with box by 4 points](#annotation-with-box-by-4-points)
- [Annotation with polygons](#annotation-with-polygons)
- [Annotation with polylines](#annotation-with-polylines)
- [Annotation with cuboids](#annotation-with-cuboids)
- [Annotation with points](#annotation-with-points)
- [Points in annotation mode](#points-in-annotation-mode)
- [Linear interpolation with one point](#linear-interpolation-with-one-point)
@ -1017,6 +1018,62 @@ automatically. You can adjust the polyline after it has been drawn.
![](static/documentation/images/image039.jpg)
## Annotation with cuboids
It is used to annotate 3 dimensional objects such as cars, boxes, etc...
Currently the feature supports one point perspective and has the contraint
where the vertical edges are exactly parallel to the sides.
### Creating the cuboid
Before starting, you have to be sure that ``Cuboid`` is selected.
Press ``N`` for entering drawing mode. There are many ways to draw a cuboid.
You may draw the cuboid by placing 4 points, after which the drawing completes automatically.
The first 3 points will represent a plane of the cuboid
while the last point represents the depth of that plane.
For the first 3 points, it is recomended to only draw the 2 closest side faces,
as well as the top and bottom face.
A few examples:
![](static/documentation/images/CuboidDrawing1.gif)
![](static/documentation/images/CuboidDrawing2.gif)
![](static/documentation/images/CuboidDrawing3.gif)
### Editing the cuboid
The cuboid can be edited in multiple ways, by dragging points or by dragging certain faces.
First notice that there is a face that is painted with pink lines only, let us call it the front face.
The cuboid can be moved by simply dragging the shape as normal.
The cuboid can be extended by dragging on the point in the middle of the edges.
The cuboid can also be extended up and down by dragging the point at the vertices.
![](static/documentation/images/CuboidEditing1.gif)
To draw with perpective effects it is assumed that the front face is the closest to the camera.
To begin simply drag the points on the vertices that are not on the pink/front face while holding ``Shift``.
The cuboid can then be edited as usual.
![](static/documentation/images/EditingPerspective.gif)
If you wish to reset perspective effects, you may right click on cuboid,
and select ``Reset Perspective`` to return to a regular cuboid.
The location of the pink face can be swapped with the adjacent visible side face.
This is done by right clicking on the cuboid and selecting ``Switch Perspective Orientation``.
Note that this will also reset the perspective effects.
![](static/documentation/images/ResetPerspective.gif)
Certain faces of the cuboid can also be edited,
these faces are the left, right and dorsal faces, relative to the pink face.
Simply drag the faces to move them independently from the rest of the cuboid.
![](static/documentation/images/CuboidEditing2.gif)
## Annotation with points
### Points in annotation mode

@ -200,14 +200,12 @@ class Mask(Annotation):
return self._z_order
def as_class_mask(self, label_id=None):
from datumaro.util.mask_tools import make_index_mask
if label_id is None:
label_id = self.label
return make_index_mask(self.image, label_id)
return self.image * label_id
def as_instance_mask(self, instance_id):
from datumaro.util.mask_tools import make_index_mask
return make_index_mask(self.image, instance_id)
return self.image * instance_id
def get_area(self):
return np.count_nonzero(self.image)

@ -634,6 +634,8 @@ class ProjectDataset(Dataset):
return self._sources
def _save_branch_project(self, extractor, save_dir=None):
extractor = Dataset.from_extractors(extractor) # apply lazy transforms
# NOTE: probably this function should be in the ViewModel layer
save_dir = osp.abspath(save_dir)
if save_dir:

@ -278,7 +278,10 @@ class _InstancesConverter(_TaskConverter):
is_crowd = mask is not None
if is_crowd:
segmentation = mask
segmentation = {
'counts': list(int(c) for c in mask['counts']),
'size': list(int(c) for c in mask['size'])
}
else:
segmentation = [list(map(float, p)) for p in polygons]

@ -6,16 +6,18 @@
# pylint: disable=no-self-use
import json
import numpy as np
import os
import os.path as osp
from datumaro.components.converter import Converter
from datumaro.components.extractor import (
DEFAULT_SUBSET_NAME, Annotation,
Label, Mask, Points, Polygon, PolyLine, Bbox, Caption,
Label, Mask, RleMask, Points, Polygon, PolyLine, Bbox, Caption,
LabelCategories, MaskCategories, PointsCategories
)
from datumaro.util.image import save_image
import pycocotools.mask as mask_utils
from datumaro.components.cli_plugin import CliPlugin
from .format import DatumaroPath
@ -40,8 +42,6 @@ class _SubsetWriter:
'items': [],
}
self._next_mask_id = 1
@property
def categories(self):
return self._data['categories']
@ -123,33 +123,22 @@ class _SubsetWriter:
})
return converted
def _save_mask(self, mask):
mask_id = None
if mask is None:
return mask_id
mask_id = self._next_mask_id
self._next_mask_id += 1
filename = '%d%s' % (mask_id, DatumaroPath.MASK_EXT)
masks_dir = osp.join(self._context._annotations_dir,
DatumaroPath.MASKS_DIR)
os.makedirs(masks_dir, exist_ok=True)
path = osp.join(masks_dir, filename)
save_image(path, mask)
return mask_id
def _convert_mask_object(self, obj):
converted = self._convert_annotation(obj)
mask = obj.image
mask_id = None
if mask is not None:
mask_id = self._save_mask(mask)
if isinstance(obj, RleMask):
rle = obj.rle
else:
rle = mask_utils.encode(
np.require(obj.image, dtype=np.uint8, requirements='F'))
converted.update({
'label_id': _cast(obj.label, int),
'mask_id': _cast(mask_id, int),
'rle': {
# serialize as compressed COCO mask
'counts': rle['counts'].decode('ascii'),
'size': list(int(c) for c in rle['size']),
}
})
return converted
@ -289,6 +278,7 @@ class _Converter:
class DatumaroConverter(Converter, CliPlugin):
@classmethod
def build_cmdline_parser(cls, **kwargs):
parser = super().build_cmdline_parser(**kwargs)
parser.add_argument('--save-images', action='store_true',
help="Save images (default: %(default)s)")
return parser

@ -4,16 +4,14 @@
# SPDX-License-Identifier: MIT
import json
import logging as log
import os.path as osp
from datumaro.components.extractor import (SourceExtractor,
DEFAULT_SUBSET_NAME, DatasetItem,
AnnotationType, Label, Mask, Points, Polygon, PolyLine, Bbox, Caption,
AnnotationType, Label, RleMask, Points, Polygon, PolyLine, Bbox, Caption,
LabelCategories, MaskCategories, PointsCategories
)
from datumaro.util.image import Image
from datumaro.util.mask_tools import lazy_mask
from .format import DatumaroPath
@ -127,19 +125,9 @@ class DatumaroExtractor(SourceExtractor):
elif ann_type == AnnotationType.mask:
label_id = ann.get('label_id')
mask_id = str(ann.get('mask_id'))
mask_path = osp.join(self._path, DatumaroPath.ANNOTATIONS_DIR,
DatumaroPath.MASKS_DIR, mask_id + DatumaroPath.MASK_EXT)
mask = None
if osp.isfile(mask_path):
mask = lazy_mask(mask_path)
else:
log.warn("Not found mask image file '%s', skipped." % \
mask_path)
loaded.append(Mask(label=label_id, image=mask,
rle = ann['rle']
rle['counts'] = rle['counts'].encode('ascii')
loaded.append(RleMask(rle=rle, label=label_id,
id=ann_id, attributes=attributes, group=group))
elif ann_type == AnnotationType.polyline:

@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: MIT
from enum import Enum
import logging as log
import os.path as osp
import random
@ -10,7 +11,9 @@ import random
import pycocotools.mask as mask_utils
from datumaro.components.extractor import (Transform, AnnotationType,
RleMask, Polygon, Bbox)
RleMask, Polygon, Bbox,
LabelCategories, MaskCategories, PointsCategories
)
from datumaro.components.cli_plugin import CliPlugin
import datumaro.util.mask_tools as mask_tools
from datumaro.util.annotation_tools import find_group_leader, find_instances
@ -46,7 +49,7 @@ class CropCoveredSegments(Transform, CliPlugin):
segments.append(s.points)
elif s.type == AnnotationType.mask:
if isinstance(s, RleMask):
rle = s._rle
rle = s.rle
else:
rle = mask_tools.mask_to_rle(s.image)
segments.append(rle)
@ -365,3 +368,116 @@ class IdFromImageName(Transform, CliPlugin):
if item.has_image and item.image.filename:
name = osp.splitext(item.image.filename)[0]
return self.wrap_item(item, id=name)
class RemapLabels(Transform, CliPlugin):
DefaultAction = Enum('DefaultAction', ['keep', 'delete'])
@staticmethod
def _split_arg(s):
parts = s.split(':')
if len(parts) != 2:
import argparse
raise argparse.ArgumentTypeError()
return (parts[0], parts[1])
@classmethod
def build_cmdline_parser(cls, **kwargs):
parser = super().build_cmdline_parser(**kwargs)
parser.add_argument('-l', '--label', action='append',
type=cls._split_arg, dest='mapping',
help="Label in the form of: '<src>:<dst>' (repeatable)")
parser.add_argument('--default',
choices=[a.name for a in cls.DefaultAction],
default=cls.DefaultAction.keep.name,
help="Action for unspecified labels")
return parser
def __init__(self, extractor, mapping, default=None):
super().__init__(extractor)
assert isinstance(default, (str, self.DefaultAction))
if isinstance(default, str):
default = self.DefaultAction[default]
assert isinstance(mapping, (dict, list))
if isinstance(mapping, list):
mapping = dict(mapping)
self._categories = {}
src_label_cat = self._extractor.categories().get(AnnotationType.label)
if src_label_cat is not None:
self._make_label_id_map(src_label_cat, mapping, default)
src_mask_cat = self._extractor.categories().get(AnnotationType.mask)
if src_mask_cat is not None:
assert src_label_cat is not None
dst_mask_cat = MaskCategories(attributes=src_mask_cat.attributes)
dst_mask_cat.colormap = {
id: src_mask_cat.colormap[id]
for id, _ in enumerate(src_label_cat.items)
if self._map_id(id) or id == 0
}
self._categories[AnnotationType.mask] = dst_mask_cat
src_points_cat = self._extractor.categories().get(AnnotationType.points)
if src_points_cat is not None:
assert src_label_cat is not None
dst_points_cat = PointsCategories(attributes=src_points_cat.attributes)
dst_points_cat.items = {
id: src_points_cat.items[id]
for id, item in enumerate(src_label_cat.items)
if self._map_id(id) or id == 0
}
self._categories[AnnotationType.points] = dst_points_cat
def _make_label_id_map(self, src_label_cat, label_mapping, default_action):
dst_label_cat = LabelCategories(attributes=src_label_cat.attributes)
id_mapping = {}
for src_index, src_label in enumerate(src_label_cat.items):
dst_label = label_mapping.get(src_label.name)
if not dst_label and default_action == self.DefaultAction.keep:
dst_label = src_label.name # keep unspecified as is
if not dst_label:
continue
dst_index = dst_label_cat.find(dst_label)[0]
if dst_index is None:
dst_label_cat.add(dst_label,
src_label.parent, src_label.attributes)
dst_index = dst_label_cat.find(dst_label)[0]
id_mapping[src_index] = dst_index
if log.getLogger().isEnabledFor(log.DEBUG):
log.debug("Label mapping:")
for src_id, src_label in enumerate(src_label_cat.items):
if id_mapping.get(src_id):
log.debug("#%s '%s' -> #%s '%s'",
src_id, src_label.name, id_mapping[src_id],
dst_label_cat.items[id_mapping[src_id]].name
)
else:
log.debug("#%s '%s' -> <deleted>", src_id, src_label.name)
self._map_id = lambda src_id: id_mapping.get(src_id, None)
self._categories[AnnotationType.label] = dst_label_cat
def categories(self):
return self._categories
def transform_item(self, item):
# TODO: provide non-inplace version
annotations = []
for ann in item.annotations:
if ann.type in { AnnotationType.label, AnnotationType.mask,
AnnotationType.points, AnnotationType.polygon,
AnnotationType.polyline, AnnotationType.bbox
} and ann.label is not None:
conv_label = self._map_id(ann.label)
if conv_label is not None:
ann._label = conv_label
annotations.append(ann)
else:
annotations.append(ann)
item._annotations = annotations
return item

@ -53,14 +53,13 @@ LabelmapType = Enum('LabelmapType', ['voc', 'source', 'guess'])
class _Converter:
def __init__(self, extractor, save_dir,
tasks=None, apply_colormap=True, save_images=False, label_map=None):
assert tasks is None or isinstance(tasks, (VocTask, list))
assert tasks is None or isinstance(tasks, (VocTask, list, set))
if tasks is None:
tasks = list(VocTask)
tasks = set(VocTask)
elif isinstance(tasks, VocTask):
tasks = [tasks]
tasks = {tasks}
else:
tasks = [t if t in VocTask else VocTask[t] for t in tasks]
tasks = set(t if t in VocTask else VocTask[t] for t in tasks)
self._tasks = tasks
self._extractor = extractor
@ -259,10 +258,10 @@ class _Converter:
if len(actions_elem) != 0:
obj_elem.append(actions_elem)
if set(self._tasks) & set([None,
if self._tasks & {None,
VocTask.detection,
VocTask.person_layout,
VocTask.action_classification]):
VocTask.action_classification}:
with open(osp.join(self._ann_dir, item.id + '.xml'), 'w') as f:
f.write(ET.tostring(root_elem,
encoding='unicode', pretty_print=True))
@ -302,19 +301,19 @@ class _Converter:
action_list[item.id] = None
segm_list[item.id] = None
if set(self._tasks) & set([None,
if self._tasks & {None,
VocTask.classification,
VocTask.detection,
VocTask.action_classification,
VocTask.person_layout]):
VocTask.person_layout}:
self.save_clsdet_lists(subset_name, clsdet_list)
if set(self._tasks) & set([None, VocTask.classification]):
if self._tasks & {None, VocTask.classification}:
self.save_class_lists(subset_name, class_lists)
if set(self._tasks) & set([None, VocTask.action_classification]):
if self._tasks & {None, VocTask.action_classification}:
self.save_action_lists(subset_name, action_list)
if set(self._tasks) & set([None, VocTask.person_layout]):
if self._tasks & {None, VocTask.person_layout}:
self.save_layout_lists(subset_name, layout_list)
if set(self._tasks) & set([None, VocTask.segmentation]):
if self._tasks & {None, VocTask.segmentation}:
self.save_segm_lists(subset_name, segm_list)
def save_action_lists(self, subset_name, action_list):

@ -111,15 +111,20 @@ def load_mask(path, inverse_colormap=None):
def lazy_mask(path, inverse_colormap=None):
return lazy_image(path, lambda path: load_mask(path, inverse_colormap))
def mask_to_rle(binary_mask):
counts = []
for i, (value, elements) in enumerate(
groupby(binary_mask.ravel(order='F'))):
# decoding starts from 0
if i == 0 and value == 1:
counts.append(0)
counts.append(len(list(elements)))
# walk in row-major order as COCO format specifies
bounded = binary_mask.ravel(order='F')
# add borders to sequence
# find boundary positions for sequences and compute their lengths
difs = np.diff(bounded, prepend=[1 - bounded[0]], append=[1 - bounded[-1]])
counts, = np.where(difs != 0)
# start RLE encoding from 0 as COCO format specifies
if bounded[0] != 0:
counts = np.diff(counts, prepend=[0])
else:
counts = np.diff(counts)
return {
'counts': counts,
@ -267,7 +272,7 @@ def find_mask_bbox(mask):
def merge_masks(masks):
"""
Merges masks into one, mask order is resposible for z order.
Merges masks into one, mask order is responsible for z order.
"""
if not masks:
return None

@ -68,15 +68,7 @@ class PolygonConversionsTest(TestCase):
self.assertTrue(np.array_equal(e_mask, c_mask),
'#%s: %s\n%s\n' % (i, e_mask, c_mask))
def test_mask_to_rle(self):
source_mask = np.array([
[0, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
])
def _test_mask_to_rle(self, source_mask):
rle_uncompressed = mask_tools.mask_to_rle(source_mask)
from pycocotools import mask as mask_utils
@ -87,6 +79,43 @@ class PolygonConversionsTest(TestCase):
self.assertTrue(np.array_equal(source_mask, resulting_mask),
'%s\n%s\n' % (source_mask, resulting_mask))
def test_mask_to_rle_multi(self):
cases = [
np.array([
[0, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 0, 1, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
]),
np.array([
[0]
]),
np.array([
[1]
]),
np.array([
[1, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
[1, 0, 1, 0, 1, 1, 1, 0, 0, 0],
[1, 1, 0, 1, 0, 1, 1, 1, 1, 0],
[1, 0, 1, 0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 0, 0, 1, 1, 0, 0, 0, 1],
[0, 0, 1, 0, 0, 0, 1, 1, 1, 1],
[1, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[1, 1, 1, 1, 1, 0, 1, 0, 1, 0],
[0, 1, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 0, 0, 1, 0],
[1, 1, 0, 1, 0, 0, 1, 1, 1, 1],
])
]
for case in cases:
self._test_mask_to_rle(case)
class ColormapOperationsTest(TestCase):
def test_can_paint_mask(self):
mask = np.zeros((1, 3), dtype=np.uint8)

@ -3,10 +3,12 @@ import numpy as np
from unittest import TestCase
from datumaro.components.extractor import (Extractor, DatasetItem,
Mask, Polygon, PolyLine, Points, Bbox
Mask, Polygon, PolyLine, Points, Bbox, Label,
LabelCategories, MaskCategories, AnnotationType
)
from datumaro.util.test_utils import compare_datasets
import datumaro.util.mask_tools as mask_tools
import datumaro.plugins.transforms as transforms
from datumaro.util.test_utils import compare_datasets
class TransformsTest(TestCase):
@ -361,3 +363,95 @@ class TransformsTest(TestCase):
('train', -0.5),
('test', 1.5),
])
def test_remap_labels(self):
class SrcExtractor(Extractor):
def __iter__(self):
return iter([
DatasetItem(id=1, annotations=[
# Should be remapped
Label(1),
Bbox(1, 2, 3, 4, label=2),
Mask(image=np.array([1]), label=3),
# Should be kept
Polygon([1, 1, 2, 2, 3, 4], label=4),
PolyLine([1, 3, 4, 2, 5, 6], label=None)
]),
])
def categories(self):
label_cat = LabelCategories()
label_cat.add('label0')
label_cat.add('label1')
label_cat.add('label2')
label_cat.add('label3')
label_cat.add('label4')
mask_cat = MaskCategories(
colormap=mask_tools.generate_colormap(5))
return {
AnnotationType.label: label_cat,
AnnotationType.mask: mask_cat,
}
class DstExtractor(Extractor):
def __iter__(self):
return iter([
DatasetItem(id=1, annotations=[
Label(1),
Bbox(1, 2, 3, 4, label=0),
Mask(image=np.array([1]), label=1),
Polygon([1, 1, 2, 2, 3, 4], label=2),
PolyLine([1, 3, 4, 2, 5, 6], label=None)
]),
])
def categories(self):
label_cat = LabelCategories()
label_cat.add('label0')
label_cat.add('label9')
label_cat.add('label4')
mask_cat = MaskCategories(colormap={
k: v for k, v in mask_tools.generate_colormap(5).items()
if k in { 0, 1, 3, 4 }
})
return {
AnnotationType.label: label_cat,
AnnotationType.mask: mask_cat,
}
actual = transforms.RemapLabels(SrcExtractor(), mapping={
'label1': 'label9',
'label2': 'label0',
'label3': 'label9',
}, default='keep')
compare_datasets(self, DstExtractor(), actual)
def test_remap_labels_delete_unspecified(self):
class SrcExtractor(Extractor):
def __iter__(self):
return iter([ DatasetItem(id=1, annotations=[ Label(0) ]) ])
def categories(self):
label_cat = LabelCategories()
label_cat.add('label0')
return { AnnotationType.label: label_cat }
class DstExtractor(Extractor):
def __iter__(self):
return iter([ DatasetItem(id=1, annotations=[]) ])
def categories(self):
return { AnnotationType.label: LabelCategories() }
actual = transforms.RemapLabels(SrcExtractor(),
mapping={}, default='delete')
compare_datasets(self, DstExtractor(), actual)

Loading…
Cancel
Save