未验证 提交 796991a1 编写于 作者: B Boris Sekachev 提交者: GitHub

Added intelligent function when paste labels to another task (#4161)

* Added intelligent paste labels function, added notification when remove labels from raw editor

* Adjusted raw tab behaviour

* Fixed issue with selection

* Updated version and changelog, removed previous implementation

* Removed outdated comment

* Additional checks on the server

* Added check for default boolean attr

* Updated version

* Conditionally show lost labels/attributes

* Remove labels only when create
Co-authored-by: NNikita Manovich <nikita.manovich@intel.com>
上级 c77d9564
......@@ -53,10 +53,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Order in an annotation file(<https://github.com/openvinotoolkit/cvat/pull/4087>)
- Fixed task data upload progressbar (<https://github.com/openvinotoolkit/cvat/pull/4134>)
- Email in org invitations is case sensitive (<https://github.com/openvinotoolkit/cvat/pull/4153>)
- Added intelligent function when paste labels to another task (<https://github.com/openvinotoolkit/cvat/pull/4161>)
- Uncaught TypeError: this.el.node.getScreenCTM() is null in Firefox (<https://github.com/openvinotoolkit/cvat/pull/4175>)
- Bug: canvas is busy when start playing, start resizing a shape and do not release the mouse cursor (<https://github.com/openvinotoolkit/cvat/pull/4151>)
- Fixed tus upload error over https (<https://github.com/openvinotoolkit/cvat/pull/4154>)
### Security
- Updated ELK to 6.8.22 which uses log4j 2.17.0 (<https://github.com/openvinotoolkit/cvat/pull/4052>)
......
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -1434,7 +1434,6 @@
},
},
/**
* After task has been created value can be appended only.
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Task
......
{
"name": "cvat-ui",
"version": "1.33.1",
"version": "1.33.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.33.1",
"version": "1.33.2",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",
......
{
"name": "cvat-ui",
"version": "1.33.1",
"version": "1.33.2",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -64,11 +64,11 @@ export function validateParsedLabel(label: Label): void {
);
}
if (typeof label.color !== 'string') {
if (label.color && typeof label.color !== 'string') {
throw new Error(`Label "${label.name}". Label color must be a string. Got ${typeof label.color}`);
}
if (!label.color.match(/^#[0-9a-fA-F]{6}$|^$/)) {
if (label.color && !label.color.match(/^#[0-9a-fA-F]{6}$|^$/)) {
throw new Error(
`Label "${label.name}". ` +
`Type of label color must be only a valid color string. Got value ${label.color}`,
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -205,13 +205,20 @@ export default class LabelForm extends React.Component<Props> {
);
}
private renderBooleanValueInput(fieldInstance: any, attr: Attribute | null): JSX.Element {
private renderBooleanValueInput(fieldInstance: any): JSX.Element {
const { key } = fieldInstance;
const value = attr ? attr.values[0] : 'false';
return (
<CVATTooltip title='Specify a default value'>
<Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
<Form.Item
rules={[
{
required: true,
message: 'Please, specify a default value',
}]}
name={[key, 'values']}
fieldKey={[fieldInstance.fieldKey, 'values']}
>
<Select className='cvat-attribute-values-input'>
<Select.Option value='false'>False</Select.Option>
<Select.Option value='true'>True</Select.Option>
......@@ -354,7 +361,7 @@ export default class LabelForm extends React.Component<Props> {
if ([AttributeType.SELECT, AttributeType.RADIO].includes(type)) {
element = this.renderAttributeValuesInput(fieldInstance, attr);
} else if (type === AttributeType.CHECKBOX) {
element = this.renderBooleanValueInput(fieldInstance, attr);
element = this.renderBooleanValueInput(fieldInstance);
} else if (type === AttributeType.NUMBER) {
element = this.renderNumberRangeInput(fieldInstance, attr);
} else {
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import Tabs from 'antd/lib/tabs';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import ModalConfirm from 'antd/lib/modal/confirm';
import copy from 'copy-to-clipboard';
import {
CopyOutlined, EditOutlined, BuildOutlined, ExclamationCircleOutlined,
EditOutlined, BuildOutlined, ExclamationCircleOutlined,
} from '@ant-design/icons';
import CVATTooltip from 'components/common/cvat-tooltip';
import RawViewer from './raw-viewer';
import ConstructorViewer from './constructor-viewer';
import ConstructorCreator from './constructor-creator';
......@@ -207,32 +204,6 @@ export default class LabelsEditor extends React.PureComponent<LabelsEditorProps,
defaultActiveKey='2'
type='card'
tabBarStyle={{ marginBottom: '0px' }}
tabBarExtraContent={(
<CVATTooltip title='Copied to clipboard!' trigger='click'>
<Button
type='link'
icon={<CopyOutlined />}
onClick={(): void => {
copy(
JSON.stringify(
savedLabels.concat(unsavedLabels).map((label): any => ({
...label,
id: undefined,
attributes: label.attributes.map((attribute): any => ({
...attribute,
id: undefined,
})),
})),
null,
4,
),
);
}}
>
Copy
</Button>
</CVATTooltip>
)}
>
<Tabs.TabPane
tab={(
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -7,7 +7,10 @@ import { Row, Col } from 'antd/lib/grid';
import Input from 'antd/lib/input';
import Button from 'antd/lib/button';
import Form, { FormInstance, RuleObject } from 'antd/lib/form';
import Tag from 'antd/lib/tag';
import Modal from 'antd/lib/modal';
import { Store } from 'antd/lib/form/interface';
import Paragraph from 'antd/lib/typography/Paragraph';
import CVATTooltip from 'components/common/cvat-tooltip';
import {
......@@ -44,6 +47,21 @@ interface Props {
onSubmit: (labels: Label[]) => void;
}
function convertLabels(labels: Label[]): Label[] {
return labels.map(
(label: any): Label => ({
...label,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map(
(attribute: any): Attribute => ({
...attribute,
id: attribute.id < 0 ? undefined : attribute.id,
}),
),
}),
);
}
export default class RawViewer extends React.PureComponent<Props> {
private formRef: RefObject<FormInstance>;
......@@ -52,49 +70,122 @@ export default class RawViewer extends React.PureComponent<Props> {
this.formRef = React.createRef<FormInstance>();
}
public componentDidUpdate(prevProps: Props): void {
const { labels } = this.props;
if (JSON.stringify(prevProps.labels) !== JSON.stringify(labels) && this.formRef.current) {
const convertedLabels = convertLabels(labels);
const textLabels = JSON.stringify(convertedLabels, null, 2);
this.formRef.current.setFieldsValue({ labels: textLabels });
}
}
private handleSubmit = (values: Store): void => {
const { onSubmit } = this.props;
const { onSubmit, labels } = this.props;
const parsed = JSON.parse(values.labels);
const labelIDs: number[] = [];
const attrIDs: number[] = [];
for (const label of parsed) {
label.id = label.id || idGenerator();
if (label.id >= 0) {
labelIDs.push(label.id);
}
for (const attr of label.attributes) {
attr.id = attr.id || idGenerator();
if (attr.id >= 0) {
attrIDs.push(attr.id);
}
}
}
onSubmit(parsed);
const deletedLabels = labels.filter((_label: Label) => _label.id >= 0 && !labelIDs.includes(_label.id));
const deletedAttributes = labels
.reduce((acc: Attribute[], _label) => [...acc, ..._label.attributes], [])
.filter((_attr: Attribute) => _attr.id >= 0 && !attrIDs.includes(_attr.id));
if (deletedLabels.length || deletedAttributes.length) {
Modal.confirm({
title: 'You are going to remove existing labels/attributes',
content: (
<>
{deletedLabels.length ? (
<Paragraph>
Following labels are going to be removed:
<div>
{deletedLabels
.map((_label: Label) => <Tag color={_label.color}>{_label.name}</Tag>)}
</div>
</Paragraph>
) : null}
{deletedAttributes.length ? (
<Paragraph>
Following attributes are going to be removed:
<div>
{deletedAttributes.map((_attr: Attribute) => <Tag>{_attr.name}</Tag>)}
</div>
</Paragraph>
) : null}
<Paragraph type='danger'>All related annotations will be destroyed. Continue?</Paragraph>
</>
),
okText: 'Delete existing data',
okButtonProps: {
danger: true,
},
onOk: () => {
onSubmit(parsed);
},
closable: true,
});
} else {
onSubmit(parsed);
}
};
public render(): JSX.Element {
const { labels } = this.props;
const convertedLabels = labels.map(
(label: any): Label => ({
...label,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map(
(attribute: any): Attribute => ({
...attribute,
id: attribute.id < 0 ? undefined : attribute.id,
}),
),
}),
);
const convertedLabels = convertLabels(labels);
const textLabels = JSON.stringify(convertedLabels, null, 2);
return (
<Form layout='vertical' onFinish={this.handleSubmit} ref={this.formRef}>
<Form.Item name='labels' initialValue={textLabels} rules={[{ validator: validateLabels }]}>
<Input.TextArea rows={5} className='cvat-raw-labels-viewer' />
<Input.TextArea
onPaste={(e: React.ClipboardEvent) => {
const data = e.clipboardData.getData('text');
const element = window.document.getElementsByClassName('cvat-raw-labels-viewer')[0] as HTMLTextAreaElement;
if (element && this.formRef.current) {
const { selectionStart, selectionEnd } = element;
// remove all "id": <number>,
let replaced = data.replace(/[\s]*"id":[\s]?[-{0-9}]+[,]?/g, '');
if (replaced !== data) {
// remove all carriage characters (textarea value does not contain them)
replaced = replaced.replace(/\r/g, '');
const value = this.formRef.current.getFieldValue('labels');
const updatedValue = value
.substr(0, selectionStart) + replaced + value.substr(selectionEnd);
this.formRef.current.setFieldsValue({ labels: updatedValue });
setTimeout(() => {
element.setSelectionRange(selectionEnd, selectionEnd);
});
e.preventDefault();
}
}
}}
rows={5}
className='cvat-raw-labels-viewer'
/>
</Form.Item>
<Row justify='start' align='middle'>
<Col>
<CVATTooltip title='Save labels and return'>
<CVATTooltip title='Save labels'>
<Button style={{ width: '150px' }} type='primary' htmlType='submit'>
Done
</Button>
</CVATTooltip>
</Col>
<Col offset={1}>
<CVATTooltip title='Do not save the label and return'>
<CVATTooltip title='Reset all changes'>
<Button
type='primary'
danger
......
......@@ -439,11 +439,15 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
label_colors = list()
for label in labels:
attributes = label.pop('attributespec_set')
if label.get('id', None):
del label['id']
if not label.get('color', None):
label['color'] = get_label_color(label['name'], label_colors)
label_colors.append(label['color'])
db_label = models.Label.objects.create(task=db_task, **label)
for attr in attributes:
if attr.get('id', None):
del attr['id']
models.AttributeSpec.objects.create(label=db_label, **attr)
task_path = db_task.get_task_dirname()
......@@ -606,12 +610,16 @@ class ProjectSerializer(serializers.ModelSerializer):
db_project = models.Project.objects.create(**validated_data)
label_colors = list()
for label in labels:
if label.get('id', None):
del label['id']
attributes = label.pop('attributespec_set')
if not label.get('color', None):
label['color'] = get_label_color(label['name'], label_colors)
label_colors.append(label['color'])
db_label = models.Label.objects.create(project=db_project, **label)
for attr in attributes:
if attr.get('id', None):
del attr['id']
models.AttributeSpec.objects.create(label=db_label, **attr)
project_path = db_project.get_project_dirname()
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册