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 <artemz@retailnext.net>
main
Boris Sekachev 4 years ago committed by GitHub
parent 5820ece3f0
commit 2d522c8781
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## \[2.2.0] - Unreleased ## \[2.2.0] - Unreleased
### Added ### 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 ### Changed
- TDB - TDB

@ -1,9 +1,9 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/** /**
* Class representing a machine learning model * Class representing a serverless function
* @memberof module:API.cvat.classes * @memberof module:API.cvat.classes
*/ */
class MLModel { class MLModel {
@ -11,6 +11,7 @@ class MLModel {
this._id = data.id; this._id = data.id;
this._name = data.name; this._name = data.name;
this._labels = data.labels; this._labels = data.labels;
this._attributes = data.attributes || [];
this._framework = data.framework; this._framework = data.framework;
this._description = data.description; this._description = data.description;
this._type = data.type; this._type = data.type;
@ -28,7 +29,7 @@ class MLModel {
} }
/** /**
* @returns {string} * @type {string}
* @readonly * @readonly
*/ */
get id() { get id() {
@ -36,7 +37,7 @@ class MLModel {
} }
/** /**
* @returns {string} * @type {string}
* @readonly * @readonly
*/ */
get name() { get name() {
@ -44,7 +45,8 @@ class MLModel {
} }
/** /**
* @returns {string[]} * @description labels supported by the model
* @type {string[]}
* @readonly * @readonly
*/ */
get labels() { get labels() {
@ -56,7 +58,21 @@ 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 * @readonly
*/ */
get framework() { get framework() {
@ -64,7 +80,7 @@ class MLModel {
} }
/** /**
* @returns {string} * @type {string}
* @readonly * @readonly
*/ */
get description() { get description() {
@ -72,7 +88,7 @@ class MLModel {
} }
/** /**
* @returns {module:API.cvat.enums.ModelType} * @type {module:API.cvat.enums.ModelType}
* @readonly * @readonly
*/ */
get type() { get type() {
@ -80,7 +96,7 @@ class MLModel {
} }
/** /**
* @returns {object} * @type {object}
* @readonly * @readonly
*/ */
get params() { get params() {
@ -90,10 +106,9 @@ class MLModel {
} }
/** /**
* @typedef {Object} MlModelTip * @type {MlModelTip}
* @property {string} message A short message for a user about the model * @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 * @property {string} gif A gif URL to be shown to a user as an example
* @returns {MlModelTip}
* @readonly * @readonly
*/ */
get tip() { get tip() {
@ -101,14 +116,16 @@ class MLModel {
} }
/** /**
* @callback onRequestStatusChange * @typedef onRequestStatusChange
* @param {string} event * @param {string} event
* @global * @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} * @returns {void}
*/ */
set onChangeToolsBlockerState(onChangeToolsBlockerState) { set onChangeToolsBlockerState(onChangeToolsBlockerState) {
this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState; this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState;
} }

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -208,7 +208,8 @@ const { Source } = require('./enums');
rotation: { rotation: {
/** /**
* @name rotation * @name rotation
* @type {number} angle measured by degrees * @description angle measured by degrees
* @type {number}
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
* @instance * @instance

@ -1,12 +1,12 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.37.1", "version": "1.38.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.37.1", "version": "1.38.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.3", "@ant-design/icons": "^4.6.3",

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.37.1", "version": "1.38.0",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -28,7 +28,7 @@ import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper'; import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState, ModelAttribute,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
interactWithCanvas, interactWithCanvas,
@ -37,9 +37,10 @@ import {
updateAnnotationsAsync, updateAnnotationsAsync,
createAnnotationsAsync, createAnnotationsAsync,
} from 'actions/annotation-actions'; } 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 LabelSelector from 'components/label-selector/label-selector';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import { Attribute, Label } from 'components/labels-editor/common';
import ApproximationAccuracy, { import ApproximationAccuracy, {
thresholdFromAccuracy, thresholdFromAccuracy,
@ -374,7 +375,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
setTimeout(() => this.runInteractionRequest(interactionId)); setTimeout(() => this.runInteractionRequest(interactionId));
} catch (err) { } catch (err: any) {
notification.error({ notification.error({
description: err.toString(), description: err.toString(),
message: 'Interaction error occured', message: 'Interaction error occured',
@ -466,7 +467,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
// update annotations on a canvas // update annotations on a canvas
fetchAnnotations(); fetchAnnotations();
} catch (err) { } catch (err: any) {
notification.error({ notification.error({
description: err.toString(), description: err.toString(),
message: 'Tracking error occured', message: 'Tracking error occured',
@ -706,7 +707,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
Array.prototype.push.apply(statefullContainer.states, serverlessStates); Array.prototype.push.apply(statefullContainer.states, serverlessStates);
trackingData.statefull[trackerID] = statefullContainer; trackingData.statefull[trackerID] = statefullContainer;
delete trackingData.stateless[trackerID]; delete trackingData.stateless[trackerID];
} catch (error) { } catch (error: any) {
notification.error({ notification.error({
message: 'Tracker initialization error', message: 'Tracker initialization error',
description: error.toString(), description: error.toString(),
@ -757,7 +758,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
trackedShape.shapePoints = shape; trackedShape.shapePoints = shape;
}); });
} }
} catch (error) { } catch (error: any) {
notification.error({ notification.error({
message: 'Tracking error', message: 'Tracking error',
description: error.toString(), description: error.toString(),
@ -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 ( return (
<DetectorRunner <DetectorRunner
withCleanup={false} withCleanup={false}
models={detectors} models={detectors}
labels={jobInstance.labels} labels={jobInstance.labels}
dimension={jobInstance.dimension} dimension={jobInstance.dimension}
runInference={async (model: Model, body: object) => { runInference={async (model: Model, body: DetectorRequestBody) => {
try { try {
this.setState({ mode: 'detection', fetching: true }); this.setState({ mode: 'detection', fetching: true });
const result = await core.lambda.call(jobInstance.taskId, model, { ...body, frame }); const result = await core.lambda.call(jobInstance.taskId, model, { ...body, frame });
const states = result.map( const states = result.map(
(data: any): any => new core.classes.ObjectState({ (data: any): any => {
shapeType: data.type, const jobLabel = (jobInstance.labels as Label[])
label: jobInstance.labels.filter((label: any): boolean => label.name === data.label)[0], .find((jLabel: Label): boolean => jLabel.name === data.label);
points: data.points, const [modelLabel] = Object.entries(body.mapping)
objectType: ObjectType.SHAPE, .find(([, { name }]) => name === data.label) || [];
frame,
occluded: false, if (!jobLabel || !modelLabel) return null;
source: 'auto',
attributes: (data.attributes as { name: string, value: string }[]) return new core.classes.ObjectState({
.reduce((mapping, attr) => { shapeType: data.type,
mapping[attrsMap[data.label][attr.name]] = attr.value; label: jobLabel,
return mapping; points: data.points,
}, {} as Record<number, string>), objectType: ObjectType.SHAPE,
zOrder: curZOrder, 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); createAnnotations(jobInstance, frame, states);
const { onSwitchToolsBlockerState } = this.props; const { onSwitchToolsBlockerState } = this.props;
onSwitchToolsBlockerState({ buttonVisible: false }); onSwitchToolsBlockerState({ buttonVisible: false });
} catch (error) { } catch (error: any) {
notification.error({ notification.error({
description: error.toString(), description: error.toString(),
message: 'Detection error occured', message: 'Detection error occurred',
}); });
} finally { } finally {
this.setState({ fetching: false }); this.setState({ fetching: false });

@ -74,7 +74,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
const fakeCredentialsData = { const fakeCredentialsData = {
accountName: 'X'.repeat(24), accountName: 'X'.repeat(24),
sessionToken: 'X'.repeat(300), sessionToken: 'X'.repeat(300),
key: 'X'.repeat(20), key: 'X'.repeat(128),
secretKey: 'X'.repeat(40), secretKey: 'X'.repeat(40),
keyFile: new File([], 'fakeKey.json'), keyFile: new File([], 'fakeKey.json'),
}; };
@ -332,7 +332,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
{...internalCommonProps} {...internalCommonProps}
> >
<Input.Password <Input.Password
maxLength={20} maxLength={128}
visibilityToggle={keyVisibility} visibilityToggle={keyVisibility}
onChange={() => setKeyVisibility(true)} onChange={() => setKeyVisibility(true)}
onFocus={() => onFocusCredentialsItem('key', 'key')} onFocus={() => onFocusCredentialsItem('key', 'key')}

@ -14,8 +14,10 @@ import InputNumber from 'antd/lib/input-number';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import { Model, StringObject } from 'reducers/interfaces'; import { Model, ModelAttribute, StringObject } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import { Label as LabelInterface } from 'components/labels-editor/common';
import { clamp } from 'utils/math'; import { clamp } from 'utils/math';
import consts from 'consts'; import consts from 'consts';
import { DimensionType } from '../../reducers/interfaces'; import { DimensionType } from '../../reducers/interfaces';
@ -23,28 +25,40 @@ import { DimensionType } from '../../reducers/interfaces';
interface Props { interface Props {
withCleanup: boolean; withCleanup: boolean;
models: Model[]; models: Model[];
labels: any[]; labels: LabelInterface[];
dimension: DimensionType; dimension: DimensionType;
runInference(model: Model, body: object): void; runInference(model: Model, body: object): void;
} }
interface MappedLabel {
name: string;
attributes: StringObject;
}
type MappedLabelsList = Record<string, MappedLabel>;
export interface DetectorRequestBody {
mapping: MappedLabelsList;
cleanup: boolean;
}
interface Match {
model: string | null;
task: string | null;
}
function DetectorRunner(props: Props): JSX.Element { function DetectorRunner(props: Props): JSX.Element {
const { const {
models, withCleanup, labels, dimension, runInference, models, withCleanup, labels, dimension, runInference,
} = props; } = props;
const [modelID, setModelID] = useState<string | null>(null); const [modelID, setModelID] = useState<string | null>(null);
const [mapping, setMapping] = useState<StringObject>({}); const [mapping, setMapping] = useState<MappedLabelsList>({});
const [threshold, setThreshold] = useState<number>(0.5); const [threshold, setThreshold] = useState<number>(0.5);
const [distance, setDistance] = useState<number>(50); const [distance, setDistance] = useState<number>(50);
const [cleanup, setCleanup] = useState<boolean>(false); const [cleanup, setCleanup] = useState<boolean>(false);
const [match, setMatch] = useState<{ const [match, setMatch] = useState<Match>({ model: null, task: null });
model: string | null; const [attrMatches, setAttrMatch] = useState<Record<string, Match>>({});
task: string | null;
}>({
model: null,
task: null,
});
const model = models.filter((_model): boolean => _model.id === modelID)[0]; const model = models.filter((_model): boolean => _model.id === modelID)[0];
const isDetector = model && model.type === 'detector'; const isDetector = model && model.type === 'detector';
@ -57,24 +71,47 @@ function DetectorRunner(props: Props): JSX.Element {
if (model && model.type !== 'reid' && !model.labels.length) { if (model && model.type !== 'reid' && !model.labels.length) {
notification.warning({ notification.warning({
message: 'The selected model does not include any lables', message: 'The selected model does not include any labels',
}); });
} }
function matchAttributes(
labelAttributes: LabelInterface['attributes'],
modelAttributes: ModelAttribute[],
): StringObject {
if (Array.isArray(labelAttributes) && Array.isArray(modelAttributes)) {
return labelAttributes
.reduce((attrAcc: StringObject, attr: any): StringObject => {
if (modelAttributes.some((mAttr) => mAttr.name === attr.name)) {
attrAcc[attr.name] = attr.name;
}
return attrAcc;
}, {});
}
return {};
}
function updateMatch(modelLabel: string | null, taskLabel: string | null): void { function updateMatch(modelLabel: string | null, taskLabel: string | null): void {
if (match.model && taskLabel) { function addMatch(modelLbl: string, taskLbl: string): void {
const newmatch: { [index: string]: string } = {}; const newMatch: MappedLabelsList = {};
newmatch[match.model] = taskLabel; const label = labels.find((l) => l.name === taskLbl) as LabelInterface;
setMapping({ ...mapping, ...newmatch }); const currentModel = models.filter((_model): boolean => _model.id === modelID)[0];
const attributes = matchAttributes(label.attributes, currentModel.attributes[modelLbl]);
newMatch[modelLbl] = { name: taskLbl, attributes };
setMapping({ ...mapping, ...newMatch });
setMatch({ model: null, task: null }); setMatch({ model: null, task: null });
}
if (match.model && taskLabel) {
addMatch(match.model, taskLabel);
return; return;
} }
if (match.task && modelLabel) { if (match.task && modelLabel) {
const newmatch: { [index: string]: string } = {}; addMatch(modelLabel, match.task);
newmatch[modelLabel] = match.task;
setMapping({ ...mapping, ...newmatch });
setMatch({ model: null, task: null });
return; return;
} }
@ -84,14 +121,72 @@ function DetectorRunner(props: Props): JSX.Element {
}); });
} }
function updateAttrMatch(modelLabel: string, modelAttrLabel: string | null, taskAttrLabel: string | null): void {
function addAttributeMatch(modelAttr: string, attrLabel: string): void {
const newMatch: StringObject = {};
newMatch[modelAttr] = attrLabel;
mapping[modelLabel].attributes = { ...mapping[modelLabel].attributes, ...newMatch };
delete attrMatches[modelLabel];
setAttrMatch({ ...attrMatches });
}
const modelAttr = attrMatches[modelLabel]?.model;
if (modelAttr && taskAttrLabel) {
addAttributeMatch(modelAttr, taskAttrLabel);
return;
}
const taskAttrModel = attrMatches[modelLabel]?.task;
if (taskAttrModel && modelAttrLabel) {
addAttributeMatch(modelAttrLabel, taskAttrModel);
return;
}
attrMatches[modelLabel] = {
model: modelAttrLabel,
task: taskAttrLabel,
};
setAttrMatch({ ...attrMatches });
}
function renderMappingRow(
color: string,
leftLabel: string,
rightLabel: string,
removalTitle: string,
onClick: () => void,
className = '',
): JSX.Element {
return (
<Row key={leftLabel} justify='start' align='middle'>
<Col span={10} className={className}>
<Tag color={color}>{leftLabel}</Tag>
</Col>
<Col span={10} offset={1} className={className}>
<Tag color={color}>{rightLabel}</Tag>
</Col>
<Col offset={1}>
<CVATTooltip title={removalTitle}>
<DeleteOutlined
className='cvat-danger-circle-icon'
onClick={onClick}
/>
</CVATTooltip>
</Col>
</Row>
);
}
function renderSelector( function renderSelector(
value: string, value: string,
tooltip: string, tooltip: string,
labelsToRender: string[], labelsToRender: string[],
onChange: (label: string) => void, onChange: (label: string) => void,
className = '',
): JSX.Element { ): JSX.Element {
return ( return (
<CVATTooltip title={tooltip}> <CVATTooltip title={tooltip} className={className}>
<Select <Select
value={value} value={value}
onChange={onChange} onChange={onChange}
@ -130,16 +225,24 @@ function DetectorRunner(props: Props): JSX.Element {
disabled={dimension !== DimensionType.DIM_2D} disabled={dimension !== DimensionType.DIM_2D}
style={{ width: '100%' }} style={{ width: '100%' }}
onChange={(_modelID: string): void => { onChange={(_modelID: string): void => {
const newmodel = models.filter((_model): boolean => _model.id === _modelID)[0]; const chosenModel = models.filter((_model): boolean => _model.id === _modelID)[0];
const newmapping = labels.reduce((acc: StringObject, label: any): StringObject => { const defaultMapping = labels.reduce(
if (newmodel.labels.includes(label.name)) { (acc: MappedLabelsList, label: LabelInterface): MappedLabelsList => {
acc[label.name] = label.name; if (chosenModel.labels.includes(label.name)) {
} acc[label.name] = {
return acc; name: label.name,
}, {}); attributes: matchAttributes(
label.attributes, chosenModel.attributes[label.name],
setMapping(newmapping); ),
};
}
return acc;
}, {},
);
setMapping(defaultMapping);
setMatch({ model: null, task: null }); setMatch({ model: null, task: null });
setAttrMatch({});
setModelID(_modelID); setModelID(_modelID);
}} }}
> >
@ -154,45 +257,92 @@ function DetectorRunner(props: Props): JSX.Element {
</Col> </Col>
</Row> </Row>
{isDetector && {isDetector &&
!!Object.keys(mapping).length && Object.keys(mapping).length ?
Object.keys(mapping).map((modelLabel: string) => { Object.keys(mapping).map((modelLabel: string) => {
const label = labels.filter((_label: any): boolean => _label.name === mapping[modelLabel])[0]; const label = labels
.find((_label: LabelInterface): boolean => (
_label.name === mapping[modelLabel].name)) as LabelInterface;
const color = label ? label.color : consts.NEW_LABEL_COLOR; const color = label ? label.color : consts.NEW_LABEL_COLOR;
const notMatchedModelAttributes = model.attributes[modelLabel]
.filter((_attribute: ModelAttribute): boolean => (
!(_attribute.name in (mapping[modelLabel].attributes || {}))
));
const taskAttributes = label.attributes.map((_attrLabel: any): string => _attrLabel.name);
return ( return (
<Row key={modelLabel} justify='start' align='middle'> <React.Fragment key={modelLabel}>
<Col span={10}> {
<Tag color={color}>{modelLabel}</Tag> renderMappingRow(color,
</Col> modelLabel,
<Col span={10} offset={1}> label.name,
<Tag color={color}>{mapping[modelLabel]}</Tag> 'Remove the mapped label',
</Col> (): void => {
<Col offset={1}> const newMapping = { ...mapping };
<CVATTooltip title='Remove the mapped values'> delete newMapping[modelLabel];
<DeleteOutlined setMapping(newMapping);
className='cvat-danger-circle-icon'
onClick={(): void => { const newAttrMatches = { ...attrMatches };
const newmapping = { ...mapping }; delete newAttrMatches[modelLabel];
delete newmapping[modelLabel]; setAttrMatch({ ...newAttrMatches });
setMapping(newmapping); })
}} }
/> {
</CVATTooltip> Object.keys(mapping[modelLabel].attributes || {})
</Col> .map((mappedModelAttr: string) => (
</Row> renderMappingRow(
consts.NEW_LABEL_COLOR,
mappedModelAttr,
mapping[modelLabel].attributes[mappedModelAttr],
'Remove the mapped attribute',
(): void => {
const newMapping = { ...mapping };
delete mapping[modelLabel].attributes[mappedModelAttr];
setMapping(newMapping);
},
'cvat-run-model-label-attribute-block',
)
))
}
{notMatchedModelAttributes.length && taskAttributes.length ? (
<Row justify='start' align='middle'>
<Col span={10}>
{renderSelector(
attrMatches[modelLabel]?.model || '',
'Model attr labels', notMatchedModelAttributes.map((l) => l.name),
(modelAttrLabel: string) => updateAttrMatch(
modelLabel, modelAttrLabel, null,
),
'cvat-run-model-label-attribute-block',
)}
</Col>
<Col span={10} offset={1}>
{renderSelector(
attrMatches[modelLabel]?.task || '',
'Task attr labels', taskAttributes,
(taskAttrLabel: string) => updateAttrMatch(
modelLabel, null, taskAttrLabel,
),
'cvat-run-model-label-attribute-block',
)}
</Col>
<Col span={1} offset={1}>
<CVATTooltip title='Specify an attribute mapping between model label and task label attributes'>
<QuestionCircleOutlined className='cvat-info-circle-icon' />
</CVATTooltip>
</Col>
</Row>
) : null}
</React.Fragment>
); );
})} }) : null}
{isDetector && !!taskLabels.length && !!modelLabels.length && ( {isDetector && !!taskLabels.length && !!modelLabels.length ? (
<> <>
<Row justify='start' align='middle'> <Row justify='start' align='middle'>
<Col span={10}> <Col span={10}>
{renderSelector( {renderSelector(match.model || '', 'Model labels', modelLabels, (modelLabel: string) => updateMatch(modelLabel, null))}
match.model || '', 'Model labels', modelLabels, (modelLabel: string) => updateMatch(modelLabel, null),
)}
</Col> </Col>
<Col span={10} offset={1}> <Col span={10} offset={1}>
{renderSelector( {renderSelector(match.task || '', 'Task labels', taskLabels, (taskLabel: string) => updateMatch(null, taskLabel))}
match.task || '', 'Task labels', taskLabels, (taskLabel: string) => updateMatch(null, taskLabel),
)}
</Col> </Col>
<Col span={1} offset={1}> <Col span={1} offset={1}>
<CVATTooltip title='Specify a label mapping between model labels and task labels'> <CVATTooltip title='Specify a label mapping between model labels and task labels'>
@ -201,8 +351,8 @@ function DetectorRunner(props: Props): JSX.Element {
</Col> </Col>
</Row> </Row>
</> </>
)} ) : null}
{isDetector && withCleanup && ( {isDetector && withCleanup ? (
<div> <div>
<Checkbox <Checkbox
checked={cleanup} checked={cleanup}
@ -211,8 +361,8 @@ function DetectorRunner(props: Props): JSX.Element {
Clean old annotations Clean old annotations
</Checkbox> </Checkbox>
</div> </div>
)} ) : null}
{isReId && ( {isReId ? (
<div> <div>
<Row align='middle' justify='start'> <Row align='middle' justify='start'>
<Col> <Col>
@ -254,18 +404,25 @@ function DetectorRunner(props: Props): JSX.Element {
</Col> </Col>
</Row> </Row>
</div> </div>
)} ) : null}
<Row align='middle' justify='end'> <Row align='middle' justify='end'>
<Col> <Col>
<Button <Button
disabled={!buttonEnabled} disabled={!buttonEnabled}
type='primary' type='primary'
onClick={() => { onClick={() => {
runInference(model, model.type === 'detector' ? const detectorRequestBody: DetectorRequestBody = {
{ mapping, cleanup } : { mapping,
cleanup,
};
runInference(
model,
model.type === 'detector' ? detectorRequestBody : {
threshold, threshold,
max_distance: distance, max_distance: distance,
}); },
);
}} }}
> >
Annotate Annotate

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -11,3 +11,7 @@
.cvat-run-model-content-remove-mapping-icon { .cvat-run-model-content-remove-mapping-icon {
color: $danger-icon-color; color: $danger-icon-color;
} }
.cvat-run-model-label-attribute-block {
padding-left: $grid-unit-size * 4;
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -8,6 +8,7 @@ import Tag from 'antd/lib/tag';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Model } from 'reducers/interfaces'; import { Model } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip';
interface Props { interface Props {
model: Model; model: Model;
@ -22,15 +23,19 @@ export default function DeployedModelItem(props: Props): JSX.Element {
<Tag color='purple'>{model.framework}</Tag> <Tag color='purple'>{model.framework}</Tag>
</Col> </Col>
<Col span={3}> <Col span={3}>
<Text className='cvat-text-color'>{model.name}</Text> <CVATTooltip overlay={model.name}>
<Text className='cvat-text-color'>{model.name}</Text>
</CVATTooltip>
</Col> </Col>
<Col span={3}> <Col span={3} offset={1}>
<Tag color='orange'>{model.type}</Tag> <Tag color='orange'>{model.type}</Tag>
</Col> </Col>
<Col span={10}> <Col span={8}>
<Text style={{ whiteSpace: 'normal', height: 'auto' }}>{model.description}</Text> <CVATTooltip overlay={model.description}>
<Text style={{ whiteSpace: 'normal', height: 'auto' }}>{model.description}</Text>
</CVATTooltip>
</Col> </Col>
<Col span={5}> <Col span={5} offset={1}>
<Select showSearch placeholder='Supported labels' style={{ width: '90%' }} value='Supported labels'> <Select showSearch placeholder='Supported labels' style={{ width: '90%' }} value='Supported labels'>
{model.labels.map( {model.labels.map(
(label): JSX.Element => ( (label): JSX.Element => (

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -29,13 +29,13 @@ export default function DeployedModelsListComponent(props: Props): JSX.Element {
<Col span={3}> <Col span={3}>
<Text strong>Name</Text> <Text strong>Name</Text>
</Col> </Col>
<Col span={3}> <Col span={3} offset={1}>
<Text strong>Type</Text> <Text strong>Type</Text>
</Col> </Col>
<Col span={10}> <Col span={8}>
<Text strong>Description</Text> <Text strong>Description</Text>
</Col> </Col>
<Col span={5}> <Col span={5} offset={1}>
<Text strong>Labels</Text> <Text strong>Labels</Text>
</Col> </Col>
</Row> </Row>

@ -255,10 +255,17 @@ export interface ShareState {
root: ShareItem; root: ShareItem;
} }
export interface ModelAttribute {
name: string;
values: string[];
input_type: 'select' | 'number' | 'checkbox' | 'radio' | 'text';
}
export interface Model { export interface Model {
id: string; id: string;
name: string; name: string;
labels: string[]; labels: string[];
attributes: Record<string, ModelAttribute[]>;
framework: string; framework: string;
description: string; description: string;
type: string; type: string;

@ -95,6 +95,7 @@ def rotate_within_exif(img: Image):
ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED ,ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED ,ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED,
]: ]:
img = img.transpose(Image.FLIP_LEFT_RIGHT) img = img.transpose(Image.FLIP_LEFT_RIGHT)
return img return img
class IMediaReader(ABC): class IMediaReader(ABC):
@ -125,8 +126,8 @@ class IMediaReader(ABC):
preview = Image.open(obj) preview = Image.open(obj)
else: else:
preview = obj preview = obj
preview.thumbnail(PREVIEW_SIZE)
preview = rotate_within_exif(preview) preview = rotate_within_exif(preview)
preview.thumbnail(PREVIEW_SIZE)
return preview.convert('RGB') return preview.convert('RGB')

@ -324,7 +324,7 @@ class LambdaTestCase(APITestCase):
"threshold": 55, "threshold": 55,
"quality": "original", "quality": "original",
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data_main_task) response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data_main_task)
@ -364,7 +364,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f'{LAMBDA_REQUESTS_PATH}', self.admin, data) response = self._post_request(f'{LAMBDA_REQUESTS_PATH}', self.admin, data)
@ -404,7 +404,7 @@ class LambdaTestCase(APITestCase):
"threshold": 55, "threshold": 55,
"quality": "original", "quality": "original",
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
data_assigneed_to_user_task = { data_assigneed_to_user_task = {
@ -414,7 +414,7 @@ class LambdaTestCase(APITestCase):
"quality": "compressed", "quality": "compressed",
"max_distance": 70, "max_distance": 70,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -442,7 +442,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -461,7 +461,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data) response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data)
@ -474,7 +474,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data) response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data)
@ -488,7 +488,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data) self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data)
@ -514,7 +514,7 @@ class LambdaTestCase(APITestCase):
"function": id_function_detector, "function": id_function_detector,
"task": self.main_task["id"], "task": self.main_task["id"],
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data) response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data)
@ -540,7 +540,7 @@ class LambdaTestCase(APITestCase):
"function": id_function_detector, "function": id_function_detector,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data) response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data)
@ -553,7 +553,7 @@ class LambdaTestCase(APITestCase):
"task": 12345, "task": 12345,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data) response = self._post_request(LAMBDA_REQUESTS_PATH, self.admin, data)
@ -569,7 +569,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -584,7 +584,7 @@ class LambdaTestCase(APITestCase):
"cleanup": True, "cleanup": True,
"threshold": 0.55, "threshold": 0.55,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
data_assigneed_to_user_task = { data_assigneed_to_user_task = {
@ -592,7 +592,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -612,7 +612,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.user, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.user, data)
@ -753,7 +753,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -767,7 +767,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -781,7 +781,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -796,7 +796,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -814,7 +814,7 @@ class LambdaTestCase(APITestCase):
"cleanup": True, "cleanup": True,
"quality": quality, "quality": quality,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -827,7 +827,7 @@ class LambdaTestCase(APITestCase):
"cleanup": True, "cleanup": True,
"quality": "test-error-quality", "quality": "test-error-quality",
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
@ -857,7 +857,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"frame": 0, "frame": 0,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data)
@ -879,7 +879,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data)
@ -891,7 +891,7 @@ class LambdaTestCase(APITestCase):
"task": self.main_task["id"], "task": self.main_task["id"],
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data)
@ -904,7 +904,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/test-functions-wrong-id", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/test-functions-wrong-id", self.admin, data)
@ -917,7 +917,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data)
@ -931,7 +931,7 @@ class LambdaTestCase(APITestCase):
"frame": 12345, "frame": 12345,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data)
@ -945,7 +945,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data) self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_detector}", self.admin, data)
@ -959,7 +959,7 @@ class LambdaTestCase(APITestCase):
"frame": 0, "frame": 0,
"cleanup": True, "cleanup": True,
"mapping": { "mapping": {
"car": "car", "car": { "name": "car" },
}, },
} }
response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_state_building}", self.admin, data) response = self._post_request(f"{LAMBDA_FUNCTIONS_PATH}/{id_function_state_building}", self.admin, data)

@ -163,21 +163,24 @@ class LambdaFunction:
def invoke(self, db_task, data): def invoke(self, db_task, data):
try: try:
payload = {} payload = {}
data = {k: v for k,v in data.items() if v is not None}
threshold = data.get("threshold") threshold = data.get("threshold")
if threshold: if threshold:
payload.update({ payload.update({ "threshold": threshold })
"threshold": threshold,
})
quality = data.get("quality") quality = data.get("quality")
mapping = data.get("mapping", {}) mapping = data.get("mapping", {})
mapping_by_default = {}
task_attributes = {} task_attributes = {}
mapping_by_default = {}
for db_label in (db_task.project.label_set if db_task.project_id else db_task.label_set).prefetch_related("attributespec_set").all(): for db_label in (db_task.project.label_set if db_task.project_id else db_task.label_set).prefetch_related("attributespec_set").all():
mapping_by_default[db_label.name] = db_label.name mapping_by_default[db_label.name] = {
'name': db_label.name,
'attributes': {}
}
task_attributes[db_label.name] = {} task_attributes[db_label.name] = {}
for attribute in db_label.attributespec_set.all(): for attribute in db_label.attributespec_set.all():
task_attributes[db_label.name][attribute.name] = { task_attributes[db_label.name][attribute.name] = {
'input_rype': attribute.input_type, 'input_type': attribute.input_type,
'values': attribute.values.split('\n') 'values': attribute.values.split('\n')
} }
if not mapping: if not mapping:
@ -186,15 +189,27 @@ class LambdaFunction:
mapping = mapping_by_default mapping = mapping_by_default
else: else:
# filter labels in mapping which don't exist in the task # filter labels in mapping which don't exist in the task
mapping = {k:v for k,v in mapping.items() if v in mapping_by_default} mapping = {k:v for k,v in mapping.items() if v['name'] in mapping_by_default}
attr_mapping = { label: mapping[label]['attributes'] if 'attributes' in mapping[label] else {} for label in mapping }
mapping = { modelLabel: mapping[modelLabel]['name'] for modelLabel in mapping }
supported_attrs = {} supported_attrs = {}
for func_label, func_attrs in self.func_attributes.items(): for func_label, func_attrs in self.func_attributes.items():
if func_label in mapping: if func_label not in mapping:
supported_attrs[func_label] = {} continue
task_attr_names = [task_attr for task_attr in task_attributes[mapping[func_label]]]
mapped_label = mapping[func_label]
mapped_attributes = attr_mapping.get(func_label, {})
supported_attrs[func_label] = {}
if mapped_attributes:
task_attr_names = [task_attr for task_attr in task_attributes[mapped_label]]
for attr in func_attrs: for attr in func_attrs:
if attr['name'] in task_attr_names: mapped_attr = mapped_attributes.get(attr["name"])
supported_attrs[func_label].update({attr["name"] : attr}) if mapped_attr in task_attr_names:
supported_attrs[func_label].update({ attr["name"]: task_attributes[mapped_label][mapped_attr] })
if self.kind == LambdaType.DETECTOR: if self.kind == LambdaType.DETECTOR:
payload.update({ payload.update({
"image": self._get_image(db_task, data["frame"], quality) "image": self._get_image(db_task, data["frame"], quality)
@ -259,29 +274,43 @@ class LambdaFunction:
return db_attr_type == "text" or \ return db_attr_type == "text" or \
(db_attr_type in ["select", "radio"] and len(value.split(" ")) == 1) (db_attr_type in ["select", "radio"] and len(value.split(" ")) == 1)
elif func_attr_type == "select": elif func_attr_type == "select":
return db_attr["input_type"] in ["radio", "text"] return db_attr_type in ["radio", "text"]
elif func_attr_type == "radio": elif func_attr_type == "radio":
return db_attr["input_type"] in ["select", "text"] return db_attr_type in ["select", "text"]
elif func_attr_type == "checkbox": elif func_attr_type == "checkbox":
return value in ["true", "false"] return value in ["true", "false"]
else: else:
return False return False
if self.kind == LambdaType.DETECTOR: if self.kind == LambdaType.DETECTOR:
for item in response: for item in response:
if item['label'] in mapping: item_label = item['label']
attributes = deepcopy(item.get("attributes", []))
item["attributes"] = [] if item_label not in mapping:
for attr in attributes: continue
db_attr = supported_attrs.get(item['label'], {}).get(attr["name"])
func_attr = [func_attr for func_attr in self.func_attributes.get(item['label'], []) if func_attr['name'] == attr["name"]] attributes = deepcopy(item.get("attributes", []))
# Skip current attribute if it was not declared as supportd in function config item["attributes"] = []
if not func_attr: mapped_attributes = attr_mapping[item_label]
continue
if attr["name"] in supported_attrs.get(item['label'], {}) and check_attr_value(attr["value"], func_attr[0], db_attr): for attr in attributes:
item["attributes"].append(attr) if attr['name'] not in mapped_attributes:
item['label'] = mapping[item['label']] continue
response_filtered.append(item)
response = response_filtered func_attr = [func_attr for func_attr in self.func_attributes.get(item_label, []) if func_attr['name'] == attr["name"]]
# Skip current attribute if it was not declared as supported in function config
if not func_attr:
continue
db_attr = supported_attrs.get(item_label, {}).get(attr["name"])
if check_attr_value(attr["value"], func_attr[0], db_attr):
attr["name"] = mapped_attributes[attr['name']]
item["attributes"].append(attr)
item['label'] = mapping[item['label']]
response_filtered.append(item)
response = response_filtered
return response return response
def _get_image(self, db_task, frame, quality): def _get_image(self, db_task, frame, quality):
@ -444,7 +473,7 @@ class LambdaJob:
for frame in range(db_task.data.size): for frame in range(db_task.data.size):
annotations = function.invoke(db_task, data={ annotations = function.invoke(db_task, data={
"frame": frame, "quality": quality, "mapping": mapping, "frame": frame, "quality": quality, "mapping": mapping,
"threshold": threshold}) "threshold": threshold })
progress = (frame + 1) / db_task.data.size progress = (frame + 1) / db_task.data.size
if not LambdaJob._update_progress(progress): if not LambdaJob._update_progress(progress):
break break

Loading…
Cancel
Save