Skip to content

Commit

Permalink
Prepare UI for attributes configuration (#4)
Browse files Browse the repository at this point in the history
* Prepare UI for attributes configuration

* Add padding for label attributes

* Update attributes inference logic

Check the attributes returned by nuclio function call and reject those that
have either incompatible types or values.

* Update cvat-ui version, CHANGELOG.md

* Enhance automatic annotation BE logic

The code in lambda_manager didn't account for attributes mappings that had
different names thus returning an empty set of attributes because it couldn't
find the correct match. Fix this by getting proper mapping from `attrMapping`
property of the input data.

* Updated CHANGELOG

* Updated changelog

* Adjusted code & feature

* A bit adjusted layout

* Minor refactoring

* Fixed bug when run auto annotation without 'attributes' key

* Fixed a couple of minor issues

* Increased access key id length

* Fixed unit tests

* Merged develop

* Rejected unnecessary change

Co-authored-by: Artem Zhivoderov <[email protected]>
  • Loading branch information
bsekachev and azhiv authored May 30, 2022
1 parent 5820ece commit 2d522c8
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 186 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## \[2.2.0] - Unreleased
### Added
- TDB
- Support of attributes returned by serverless functions (<https://github.com/cvat-ai/cvat/pull/4>) based on (<https://github.com/openvinotoolkit/cvat/pull/4506>)

### Changed
- TDB
Expand Down
49 changes: 33 additions & 16 deletions cvat-core/src/ml-model.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT

/**
* Class representing a machine learning model
* Class representing a serverless function
* @memberof module:API.cvat.classes
*/
class MLModel {
constructor(data) {
this._id = data.id;
this._name = data.name;
this._labels = data.labels;
this._attributes = data.attributes || [];
this._framework = data.framework;
this._description = data.description;
this._type = data.type;
Expand All @@ -28,23 +29,24 @@ class MLModel {
}

/**
* @returns {string}
* @type {string}
* @readonly
*/
get id() {
return this._id;
}

/**
* @returns {string}
* @type {string}
* @readonly
*/
get name() {
return this._name;
}

/**
* @returns {string[]}
* @description labels supported by the model
* @type {string[]}
* @readonly
*/
get labels() {
Expand All @@ -56,31 +58,45 @@ class MLModel {
}

/**
* @returns {string}
* @typedef ModelAttribute
* @property {string} name
* @property {string[]} values
* @property {'select'|'number'|'checkbox'|'radio'|'text'} input_type
*/
/**
* @type {Object<string, ModelAttribute>}
* @readonly
*/
get attributes() {
return { ...this._attributes };
}

/**
* @type {string}
* @readonly
*/
get framework() {
return this._framework;
}

/**
* @returns {string}
* @type {string}
* @readonly
*/
get description() {
return this._description;
}

/**
* @returns {module:API.cvat.enums.ModelType}
* @type {module:API.cvat.enums.ModelType}
* @readonly
*/
get type() {
return this._type;
}

/**
* @returns {object}
* @type {object}
* @readonly
*/
get params() {
Expand All @@ -90,25 +106,26 @@ class MLModel {
}

/**
* @typedef {Object} MlModelTip
* @type {MlModelTip}
* @property {string} message A short message for a user about the model
* @property {string} gif A gif URL to be shawn to a user as an example
* @returns {MlModelTip}
* @property {string} gif A gif URL to be shown to a user as an example
* @readonly
*/
get tip() {
return { ...this._tip };
}

/**
* @callback onRequestStatusChange
* @typedef onRequestStatusChange
* @param {string} event
* @global
*/
*/
/**
* @param {onRequestStatusChange} onRequestStatusChange Set canvas onChangeToolsBlockerState callback
* @param {onRequestStatusChange} onRequestStatusChange
* @instance
* @description Used to set a callback when the tool is blocked in UI
* @returns {void}
*/
*/
set onChangeToolsBlockerState(onChangeToolsBlockerState) {
this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState;
}
Expand Down
5 changes: 3 additions & 2 deletions cvat-core/src/object-state.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -208,7 +208,8 @@ const { Source } = require('./enums');
rotation: {
/**
* @name rotation
* @type {number} angle measured by degrees
* @description angle measured by degrees
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
Expand Down
4 changes: 2 additions & 2 deletions cvat-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.37.1",
"version": "1.38.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState,
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute,
} from 'reducers/interfaces';
import {
interactWithCanvas,
Expand All @@ -37,9 +37,10 @@ import {
updateAnnotationsAsync,
createAnnotationsAsync,
} from 'actions/annotation-actions';
import DetectorRunner from 'components/model-runner-modal/detector-runner';
import DetectorRunner, { DetectorRequestBody } from 'components/model-runner-modal/detector-runner';
import LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip';
import { Attribute, Label } from 'components/labels-editor/common';

import ApproximationAccuracy, {
thresholdFromAccuracy,
Expand Down Expand Up @@ -374,7 +375,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}

setTimeout(() => this.runInteractionRequest(interactionId));
} catch (err) {
} catch (err: any) {
notification.error({
description: err.toString(),
message: 'Interaction error occured',
Expand Down Expand Up @@ -466,7 +467,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {

// update annotations on a canvas
fetchAnnotations();
} catch (err) {
} catch (err: any) {
notification.error({
description: err.toString(),
message: 'Tracking error occured',
Expand Down Expand Up @@ -706,7 +707,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
Array.prototype.push.apply(statefullContainer.states, serverlessStates);
trackingData.statefull[trackerID] = statefullContainer;
delete trackingData.stateless[trackerID];
} catch (error) {
} catch (error: any) {
notification.error({
message: 'Tracker initialization error',
description: error.toString(),
Expand Down Expand Up @@ -757,7 +758,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
trackedShape.shapePoints = shape;
});
}
} catch (error) {
} catch (error: any) {
notification.error({
message: 'Tracking error',
description: error.toString(),
Expand Down Expand Up @@ -1022,41 +1023,106 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
});
});

function checkAttributesCompatibility(
functionAttribute: ModelAttribute | undefined,
dbAttribute: Attribute | undefined,
value: string,
): boolean {
if (!dbAttribute || !functionAttribute) {
return false;
}

const { inputType } = (dbAttribute as any as { inputType: string });
if (functionAttribute.input_type === inputType) {
if (functionAttribute.input_type === 'number') {
const [min, max, step] = dbAttribute.values;
return !Number.isNaN(+value) && +value >= +min && +value <= +max && !(+value % +step);
}

if (functionAttribute.input_type === 'checkbox') {
return ['true', 'false'].includes(value.toLowerCase());
}

if (['select', 'radio'].includes(functionAttribute.input_type)) {
return dbAttribute.values.includes(value);
}

return true;
}

switch (functionAttribute.input_type) {
case 'number':
return dbAttribute.values.includes(value) || inputType === 'text';
case 'text':
return ['select', 'radio'].includes(dbAttribute.input_type) && dbAttribute.values.includes(value);
case 'select':
return (inputType === 'radio' && dbAttribute.values.includes(value)) || inputType === 'text';
case 'radio':
return (inputType === 'select' && dbAttribute.values.includes(value)) || inputType === 'text';
case 'checkbox':
return dbAttribute.values.includes(value) || inputType === 'text';
default:
return false;
}
}

return (
<DetectorRunner
withCleanup={false}
models={detectors}
labels={jobInstance.labels}
dimension={jobInstance.dimension}
runInference={async (model: Model, body: object) => {
runInference={async (model: Model, body: DetectorRequestBody) => {
try {
this.setState({ mode: 'detection', fetching: true });
const result = await core.lambda.call(jobInstance.taskId, model, { ...body, frame });
const states = result.map(
(data: any): any => new core.classes.ObjectState({
shapeType: data.type,
label: jobInstance.labels.filter((label: any): boolean => label.name === data.label)[0],
points: data.points,
objectType: ObjectType.SHAPE,
frame,
occluded: false,
source: 'auto',
attributes: (data.attributes as { name: string, value: string }[])
.reduce((mapping, attr) => {
mapping[attrsMap[data.label][attr.name]] = attr.value;
return mapping;
}, {} as Record<number, string>),
zOrder: curZOrder,
}),
);
(data: any): any => {
const jobLabel = (jobInstance.labels as Label[])
.find((jLabel: Label): boolean => jLabel.name === data.label);
const [modelLabel] = Object.entries(body.mapping)
.find(([, { name }]) => name === data.label) || [];

if (!jobLabel || !modelLabel) return null;

return new core.classes.ObjectState({
shapeType: data.type,
label: jobLabel,
points: data.points,
objectType: ObjectType.SHAPE,
frame,
occluded: false,
source: 'auto',
attributes: (data.attributes as { name: string, value: string }[])
.reduce((acc, attr) => {
const [modelAttr] = Object.entries(body.mapping[modelLabel].attributes)
.find((value: string[]) => value[1] === attr.name) || [];
const areCompatible = checkAttributesCompatibility(
model.attributes[modelLabel].find((mAttr) => mAttr.name === modelAttr),
jobLabel.attributes.find((jobAttr: Attribute) => (
jobAttr.name === attr.name
)),
attr.value,
);

if (areCompatible) {
acc[attrsMap[data.label][attr.name]] = attr.value;
}

return acc;
}, {} as Record<number, string>),
zOrder: curZOrder,
});
},
).filter((state: any) => state);

createAnnotations(jobInstance, frame, states);
const { onSwitchToolsBlockerState } = this.props;
onSwitchToolsBlockerState({ buttonVisible: false });
} catch (error) {
} catch (error: any) {
notification.error({
description: error.toString(),
message: 'Detection error occured',
message: 'Detection error occurred',
});
} finally {
this.setState({ fetching: false });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
const fakeCredentialsData = {
accountName: 'X'.repeat(24),
sessionToken: 'X'.repeat(300),
key: 'X'.repeat(20),
key: 'X'.repeat(128),
secretKey: 'X'.repeat(40),
keyFile: new File([], 'fakeKey.json'),
};
Expand Down Expand Up @@ -332,7 +332,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
{...internalCommonProps}
>
<Input.Password
maxLength={20}
maxLength={128}
visibilityToggle={keyVisibility}
onChange={() => setKeyVisibility(true)}
onFocus={() => onFocusCredentialsItem('key', 'key')}
Expand Down
Loading

0 comments on commit 2d522c8

Please sign in to comment.