Skip to content

Commit 2d522c8

Browse files
bsekachevazhiv
andauthored
Prepare UI for attributes configuration (#4)
* 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]>
1 parent 5820ece commit 2d522c8

File tree

15 files changed

+473
-186
lines changed

15 files changed

+473
-186
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

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

1212
### Changed
1313
- TDB

cvat-core/src/ml-model.js

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
// Copyright (C) 2019-2021 Intel Corporation
1+
// Copyright (C) 2019-2022 Intel Corporation
22
//
33
// SPDX-License-Identifier: MIT
44

55
/**
6-
* Class representing a machine learning model
6+
* Class representing a serverless function
77
* @memberof module:API.cvat.classes
88
*/
99
class MLModel {
1010
constructor(data) {
1111
this._id = data.id;
1212
this._name = data.name;
1313
this._labels = data.labels;
14+
this._attributes = data.attributes || [];
1415
this._framework = data.framework;
1516
this._description = data.description;
1617
this._type = data.type;
@@ -28,23 +29,24 @@ class MLModel {
2829
}
2930

3031
/**
31-
* @returns {string}
32+
* @type {string}
3233
* @readonly
3334
*/
3435
get id() {
3536
return this._id;
3637
}
3738

3839
/**
39-
* @returns {string}
40+
* @type {string}
4041
* @readonly
4142
*/
4243
get name() {
4344
return this._name;
4445
}
4546

4647
/**
47-
* @returns {string[]}
48+
* @description labels supported by the model
49+
* @type {string[]}
4850
* @readonly
4951
*/
5052
get labels() {
@@ -56,31 +58,45 @@ class MLModel {
5658
}
5759

5860
/**
59-
* @returns {string}
61+
* @typedef ModelAttribute
62+
* @property {string} name
63+
* @property {string[]} values
64+
* @property {'select'|'number'|'checkbox'|'radio'|'text'} input_type
65+
*/
66+
/**
67+
* @type {Object<string, ModelAttribute>}
68+
* @readonly
69+
*/
70+
get attributes() {
71+
return { ...this._attributes };
72+
}
73+
74+
/**
75+
* @type {string}
6076
* @readonly
6177
*/
6278
get framework() {
6379
return this._framework;
6480
}
6581

6682
/**
67-
* @returns {string}
83+
* @type {string}
6884
* @readonly
6985
*/
7086
get description() {
7187
return this._description;
7288
}
7389

7490
/**
75-
* @returns {module:API.cvat.enums.ModelType}
91+
* @type {module:API.cvat.enums.ModelType}
7692
* @readonly
7793
*/
7894
get type() {
7995
return this._type;
8096
}
8197

8298
/**
83-
* @returns {object}
99+
* @type {object}
84100
* @readonly
85101
*/
86102
get params() {
@@ -90,25 +106,26 @@ class MLModel {
90106
}
91107

92108
/**
93-
* @typedef {Object} MlModelTip
109+
* @type {MlModelTip}
94110
* @property {string} message A short message for a user about the model
95-
* @property {string} gif A gif URL to be shawn to a user as an example
96-
* @returns {MlModelTip}
111+
* @property {string} gif A gif URL to be shown to a user as an example
97112
* @readonly
98113
*/
99114
get tip() {
100115
return { ...this._tip };
101116
}
102117

103118
/**
104-
* @callback onRequestStatusChange
119+
* @typedef onRequestStatusChange
105120
* @param {string} event
106121
* @global
107-
*/
122+
*/
108123
/**
109-
* @param {onRequestStatusChange} onRequestStatusChange Set canvas onChangeToolsBlockerState callback
124+
* @param {onRequestStatusChange} onRequestStatusChange
125+
* @instance
126+
* @description Used to set a callback when the tool is blocked in UI
110127
* @returns {void}
111-
*/
128+
*/
112129
set onChangeToolsBlockerState(onChangeToolsBlockerState) {
113130
this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState;
114131
}

cvat-core/src/object-state.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2019-2021 Intel Corporation
1+
// Copyright (C) 2019-2022 Intel Corporation
22
//
33
// SPDX-License-Identifier: MIT
44

@@ -208,7 +208,8 @@ const { Source } = require('./enums');
208208
rotation: {
209209
/**
210210
* @name rotation
211-
* @type {number} angle measured by degrees
211+
* @description angle measured by degrees
212+
* @type {number}
212213
* @memberof module:API.cvat.classes.ObjectState
213214
* @throws {module:API.cvat.exceptions.ArgumentError}
214215
* @instance

cvat-ui/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cvat-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cvat-ui",
3-
"version": "1.37.1",
3+
"version": "1.38.0",
44
"description": "CVAT single-page application",
55
"main": "src/index.tsx",
66
"scripts": {

cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
2828
import getCore from 'cvat-core-wrapper';
2929
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
3030
import {
31-
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState,
31+
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute,
3232
} from 'reducers/interfaces';
3333
import {
3434
interactWithCanvas,
@@ -37,9 +37,10 @@ import {
3737
updateAnnotationsAsync,
3838
createAnnotationsAsync,
3939
} from 'actions/annotation-actions';
40-
import DetectorRunner from 'components/model-runner-modal/detector-runner';
40+
import DetectorRunner, { DetectorRequestBody } from 'components/model-runner-modal/detector-runner';
4141
import LabelSelector from 'components/label-selector/label-selector';
4242
import CVATTooltip from 'components/common/cvat-tooltip';
43+
import { Attribute, Label } from 'components/labels-editor/common';
4344

4445
import ApproximationAccuracy, {
4546
thresholdFromAccuracy,
@@ -374,7 +375,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
374375
}
375376

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

467468
// update annotations on a canvas
468469
fetchAnnotations();
469-
} catch (err) {
470+
} catch (err: any) {
470471
notification.error({
471472
description: err.toString(),
472473
message: 'Tracking error occured',
@@ -706,7 +707,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
706707
Array.prototype.push.apply(statefullContainer.states, serverlessStates);
707708
trackingData.statefull[trackerID] = statefullContainer;
708709
delete trackingData.stateless[trackerID];
709-
} catch (error) {
710+
} catch (error: any) {
710711
notification.error({
711712
message: 'Tracker initialization error',
712713
description: error.toString(),
@@ -757,7 +758,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
757758
trackedShape.shapePoints = shape;
758759
});
759760
}
760-
} catch (error) {
761+
} catch (error: any) {
761762
notification.error({
762763
message: 'Tracking error',
763764
description: error.toString(),
@@ -1022,41 +1023,106 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
10221023
});
10231024
});
10241025

1026+
function checkAttributesCompatibility(
1027+
functionAttribute: ModelAttribute | undefined,
1028+
dbAttribute: Attribute | undefined,
1029+
value: string,
1030+
): boolean {
1031+
if (!dbAttribute || !functionAttribute) {
1032+
return false;
1033+
}
1034+
1035+
const { inputType } = (dbAttribute as any as { inputType: string });
1036+
if (functionAttribute.input_type === inputType) {
1037+
if (functionAttribute.input_type === 'number') {
1038+
const [min, max, step] = dbAttribute.values;
1039+
return !Number.isNaN(+value) && +value >= +min && +value <= +max && !(+value % +step);
1040+
}
1041+
1042+
if (functionAttribute.input_type === 'checkbox') {
1043+
return ['true', 'false'].includes(value.toLowerCase());
1044+
}
1045+
1046+
if (['select', 'radio'].includes(functionAttribute.input_type)) {
1047+
return dbAttribute.values.includes(value);
1048+
}
1049+
1050+
return true;
1051+
}
1052+
1053+
switch (functionAttribute.input_type) {
1054+
case 'number':
1055+
return dbAttribute.values.includes(value) || inputType === 'text';
1056+
case 'text':
1057+
return ['select', 'radio'].includes(dbAttribute.input_type) && dbAttribute.values.includes(value);
1058+
case 'select':
1059+
return (inputType === 'radio' && dbAttribute.values.includes(value)) || inputType === 'text';
1060+
case 'radio':
1061+
return (inputType === 'select' && dbAttribute.values.includes(value)) || inputType === 'text';
1062+
case 'checkbox':
1063+
return dbAttribute.values.includes(value) || inputType === 'text';
1064+
default:
1065+
return false;
1066+
}
1067+
}
1068+
10251069
return (
10261070
<DetectorRunner
10271071
withCleanup={false}
10281072
models={detectors}
10291073
labels={jobInstance.labels}
10301074
dimension={jobInstance.dimension}
1031-
runInference={async (model: Model, body: object) => {
1075+
runInference={async (model: Model, body: DetectorRequestBody) => {
10321076
try {
10331077
this.setState({ mode: 'detection', fetching: true });
10341078
const result = await core.lambda.call(jobInstance.taskId, model, { ...body, frame });
10351079
const states = result.map(
1036-
(data: any): any => new core.classes.ObjectState({
1037-
shapeType: data.type,
1038-
label: jobInstance.labels.filter((label: any): boolean => label.name === data.label)[0],
1039-
points: data.points,
1040-
objectType: ObjectType.SHAPE,
1041-
frame,
1042-
occluded: false,
1043-
source: 'auto',
1044-
attributes: (data.attributes as { name: string, value: string }[])
1045-
.reduce((mapping, attr) => {
1046-
mapping[attrsMap[data.label][attr.name]] = attr.value;
1047-
return mapping;
1048-
}, {} as Record<number, string>),
1049-
zOrder: curZOrder,
1050-
}),
1051-
);
1080+
(data: any): any => {
1081+
const jobLabel = (jobInstance.labels as Label[])
1082+
.find((jLabel: Label): boolean => jLabel.name === data.label);
1083+
const [modelLabel] = Object.entries(body.mapping)
1084+
.find(([, { name }]) => name === data.label) || [];
1085+
1086+
if (!jobLabel || !modelLabel) return null;
1087+
1088+
return new core.classes.ObjectState({
1089+
shapeType: data.type,
1090+
label: jobLabel,
1091+
points: data.points,
1092+
objectType: ObjectType.SHAPE,
1093+
frame,
1094+
occluded: false,
1095+
source: 'auto',
1096+
attributes: (data.attributes as { name: string, value: string }[])
1097+
.reduce((acc, attr) => {
1098+
const [modelAttr] = Object.entries(body.mapping[modelLabel].attributes)
1099+
.find((value: string[]) => value[1] === attr.name) || [];
1100+
const areCompatible = checkAttributesCompatibility(
1101+
model.attributes[modelLabel].find((mAttr) => mAttr.name === modelAttr),
1102+
jobLabel.attributes.find((jobAttr: Attribute) => (
1103+
jobAttr.name === attr.name
1104+
)),
1105+
attr.value,
1106+
);
1107+
1108+
if (areCompatible) {
1109+
acc[attrsMap[data.label][attr.name]] = attr.value;
1110+
}
1111+
1112+
return acc;
1113+
}, {} as Record<number, string>),
1114+
zOrder: curZOrder,
1115+
});
1116+
},
1117+
).filter((state: any) => state);
10521118

10531119
createAnnotations(jobInstance, frame, states);
10541120
const { onSwitchToolsBlockerState } = this.props;
10551121
onSwitchToolsBlockerState({ buttonVisible: false });
1056-
} catch (error) {
1122+
} catch (error: any) {
10571123
notification.error({
10581124
description: error.toString(),
1059-
message: 'Detection error occured',
1125+
message: 'Detection error occurred',
10601126
});
10611127
} finally {
10621128
this.setState({ fetching: false });

cvat-ui/src/components/create-cloud-storage-page/cloud-storage-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
7474
const fakeCredentialsData = {
7575
accountName: 'X'.repeat(24),
7676
sessionToken: 'X'.repeat(300),
77-
key: 'X'.repeat(20),
77+
key: 'X'.repeat(128),
7878
secretKey: 'X'.repeat(40),
7979
keyFile: new File([], 'fakeKey.json'),
8080
};
@@ -332,7 +332,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
332332
{...internalCommonProps}
333333
>
334334
<Input.Password
335-
maxLength={20}
335+
maxLength={128}
336336
visibilityToggle={keyVisibility}
337337
onChange={() => setKeyVisibility(true)}
338338
onFocus={() => onFocusCredentialsItem('key', 'key')}

0 commit comments

Comments
 (0)