diff --git a/packages/components/src/Annotator/components/AttributeEditor/index.tsx b/packages/components/src/Annotator/components/AttributeEditor/index.tsx index 2edafac..60e0a21 100644 --- a/packages/components/src/Annotator/components/AttributeEditor/index.tsx +++ b/packages/components/src/Annotator/components/AttributeEditor/index.tsx @@ -1,12 +1,14 @@ +import { CloseOutlined } from '@ant-design/icons'; import { Button, Card, message } from 'antd'; -import { useImmer } from 'use-immer'; -import { FloatWrapper } from '../FloatWrapper'; -import { memo, useEffect } from 'react'; import { useLocale } from 'dds-utils/locale'; +import { memo, useEffect } from 'react'; +import { useImmer } from 'use-immer'; + import { IAttributeValue, IEditingAttribute } from '../../type'; -import './index.less'; import AttributesForm from '../AttributesForm'; -import { CloseOutlined } from '@ant-design/icons'; +import { FloatWrapper } from '../FloatWrapper'; + +import './index.less'; interface IProps { data: IEditingAttribute; diff --git a/packages/components/src/Annotator/components/AttributesForm/index.tsx b/packages/components/src/Annotator/components/AttributesForm/index.tsx index 2dbd10a..fb5c366 100644 --- a/packages/components/src/Annotator/components/AttributesForm/index.tsx +++ b/packages/components/src/Annotator/components/AttributesForm/index.tsx @@ -1,11 +1,13 @@ -import React, { memo } from 'react'; -import classNames from 'classnames'; import { Button, Checkbox, Form, Input, Radio, Tooltip } from 'antd'; -import { EActionType, IAttribute, IAttributeValue } from '../../type'; +import classNames from 'classnames'; +import { useLocale } from 'dds-utils/locale'; import { isEqual } from 'lodash'; -import './index.less'; +import React, { memo } from 'react'; + import { ReactComponent as Attribute } from '../../assets/attribute.svg'; -import { useLocale } from 'dds-utils/locale'; +import { EActionType, IAttribute, IAttributeValue } from '../../type'; + +import './index.less'; export interface IProps { isDarkTheme?: boolean; diff --git a/packages/components/src/Annotator/components/CategoryCreator/index.tsx b/packages/components/src/Annotator/components/CategoryCreator/index.tsx index d2bbbc0..b776e4c 100644 --- a/packages/components/src/Annotator/components/CategoryCreator/index.tsx +++ b/packages/components/src/Annotator/components/CategoryCreator/index.tsx @@ -1,7 +1,8 @@ -import { useLocale } from 'dds-utils/locale'; import { PlusOutlined } from '@ant-design/icons'; import { Button, Divider, Input, InputRef, Space } from 'antd'; +import { useLocale } from 'dds-utils/locale'; import { memo, useRef, useState } from 'react'; + interface IProps { onAdd: (value: string) => void; } diff --git a/packages/components/src/Annotator/components/Classification/index.tsx b/packages/components/src/Annotator/components/Classification/index.tsx index f227137..b00f7ca 100644 --- a/packages/components/src/Annotator/components/Classification/index.tsx +++ b/packages/components/src/Annotator/components/Classification/index.tsx @@ -1,19 +1,21 @@ -import React, { memo, useMemo, useState } from 'react'; +import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; import { Button, Tabs, Tooltip } from 'antd'; import classNames from 'classnames'; import { useLocale } from 'dds-utils/locale'; +import { isEqual } from 'lodash'; +import React, { memo, useMemo, useState } from 'react'; +import { Updater } from 'use-immer'; + import { Category, DrawData, IAttributeValue, IEditingAttribute, } from '../../type'; -import { isEqual } from 'lodash'; -import './index.less'; -import { Updater } from 'use-immer'; -import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; import AttributesForm from '../AttributesForm'; +import './index.less'; + export interface IProps { className?: string; supportEdit?: boolean; diff --git a/packages/components/src/Annotator/components/DisplaySettings/index.tsx b/packages/components/src/Annotator/components/DisplaySettings/index.tsx index aa1f4f6..c9dab1f 100644 --- a/packages/components/src/Annotator/components/DisplaySettings/index.tsx +++ b/packages/components/src/Annotator/components/DisplaySettings/index.tsx @@ -1,14 +1,16 @@ -import { Button, Popover, Slider, Tooltip } from 'antd'; import Icon from '@ant-design/icons'; +import { Button, Popover, Slider, Tooltip } from 'antd'; import { useLocale } from 'dds-utils/locale'; -import { ReactComponent as SettingIcon } from '../../assets/settings-sliders.svg'; -import { ReactComponent as DisplayReset } from '../../assets/displayReset.svg'; import { memo, useMemo } from 'react'; + +import { ReactComponent as DisplayReset } from '../../assets/displayReset.svg'; +import { ReactComponent as SettingIcon } from '../../assets/settings-sliders.svg'; import { DEFAULT_IMG_DISPLAY_OPTIONS, IAnnotsDisplayOptions, IImageDisplayOptions, } from '../../type'; + import './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/EditorStatus/index.tsx b/packages/components/src/Annotator/components/EditorStatus/index.tsx index 6c9d428..d7ebb4f 100644 --- a/packages/components/src/Annotator/components/EditorStatus/index.tsx +++ b/packages/components/src/Annotator/components/EditorStatus/index.tsx @@ -1,9 +1,11 @@ -import { memo } from 'react'; import classNames from 'classnames'; import { useLocale } from 'dds-utils/locale'; -import { EditorMode } from '../../type'; +import { memo } from 'react'; + import { ReactComponent as LabelIcon } from '../../assets/label.svg'; import { ReactComponent as ReviewIcon } from '../../assets/review.svg'; +import { EditorMode } from '../../type'; + import './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/ImageView/index.tsx b/packages/components/src/Annotator/components/ImageView/index.tsx index 02abb90..ea688bc 100644 --- a/packages/components/src/Annotator/components/ImageView/index.tsx +++ b/packages/components/src/Annotator/components/ImageView/index.tsx @@ -1,7 +1,9 @@ -import { Button, Spin } from 'antd'; import { ReloadOutlined } from '@ant-design/icons'; -import ImgBroken from '../../assets/img-broken.svg'; +import { Button, Spin } from 'antd'; import React, { useState } from 'react'; + +import ImgBroken from '../../assets/img-broken.svg'; + import './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/LabelSelector/index.tsx b/packages/components/src/Annotator/components/LabelSelector/index.tsx index 8cebe23..e1afe36 100644 --- a/packages/components/src/Annotator/components/LabelSelector/index.tsx +++ b/packages/components/src/Annotator/components/LabelSelector/index.tsx @@ -1,14 +1,16 @@ import { Select } from 'antd'; import { useLocale } from 'dds-utils/locale'; import { memo, useMemo } from 'react'; -import { Category, DrawData } from '../../type'; -import CategoryCreator from '../CategoryCreator'; + import { EBasicToolItem, EBasicToolTypeMap, LABEL_TOOL_MAP, OBJECT_ICON, } from '../../constants'; +import { Category, DrawData } from '../../type'; +import CategoryCreator from '../CategoryCreator'; + import './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/ModelSelectModal/index.less b/packages/components/src/Annotator/components/ModelSelectModal/index.less index 76dba70..b51c889 100644 --- a/packages/components/src/Annotator/components/ModelSelectModal/index.less +++ b/packages/components/src/Annotator/components/ModelSelectModal/index.less @@ -1,18 +1,20 @@ .dds-annotator-model-selector-modal { display: flex; - gap: 30px; + align-items: center; + justify-content: space-between; &-option { position: relative; display: flex; flex-direction: column; align-items: center; - justify-content: space-between; + gap: 10px; padding-top: 30px; - padding-block-end: 20px; - margin-block: 25px; + padding-bottom: 20px; + padding-inline: 5px; + margin-block: 10px; width: 220px; - height: 180px; + height: 200px; border: 0.5px solid #d6d6d6; &-icon { @@ -23,15 +25,19 @@ } &-name { + text-align: center; color: #000; font-size: 18px; font-weight: 500; user-select: none; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 90%; } &-description { text-align: center; - width: 80%; color: rgba(0, 0, 0, 0.4); font-size: 12px; font-weight: 400; @@ -47,14 +53,13 @@ } &:hover { - border: 2px solid #165cff; background: rgba(185, 206, 255, 0.11); box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25); } } &-option-hightlight { - border: 2px solid #165cff5f; + border: 1px solid #165cff5f; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.25); background: rgba(185, 206, 255, 0.3); } diff --git a/packages/components/src/Annotator/components/ModelSelectModal/index.tsx b/packages/components/src/Annotator/components/ModelSelectModal/index.tsx index 8c240a0..5141f45 100644 --- a/packages/components/src/Annotator/components/ModelSelectModal/index.tsx +++ b/packages/components/src/Annotator/components/ModelSelectModal/index.tsx @@ -1,19 +1,16 @@ -import { - EBasicToolItem, - EnumModelType, - MODEL_INTRO_MAP, - TOOL_MODELS_MAP, -} from '../../constants'; import Icon from '@ant-design/icons'; import { Modal, Tag } from 'antd'; +import classNames from 'classnames'; +import { useLocale } from 'dds-utils'; import { memo, useMemo } from 'react'; + +import { EnumModelType, MODEL_INTRO_MAP } from '../../constants'; + import './index.less'; -import { useLocale } from 'dds-utils'; -import classNames from 'classnames'; interface IProps { - selectedTool: EBasicToolItem; AIAnnotation: boolean; + modelOptions: EnumModelType[]; selectedModel?: EnumModelType; onSelectModel: (type: EnumModelType) => void; onCloseModal: () => void; @@ -21,37 +18,41 @@ interface IProps { const ModelSelectModal: React.FC = memo( ({ - selectedTool, AIAnnotation, + modelOptions, selectedModel, onSelectModel, onCloseModal, }) => { const { localeText } = useLocale(); + const modalWidth = + modelOptions.length * 220 + (modelOptions.length + 1) * 20; + const autoOpen = useMemo(() => { if ( AIAnnotation && - TOOL_MODELS_MAP[selectedTool] && - TOOL_MODELS_MAP[selectedTool]!.length > 1 && + modelOptions && + modelOptions.length > 1 && !selectedModel ) { return true; } return false; - }, [AIAnnotation, selectedTool, selectedModel]); + }, [AIAnnotation, modelOptions, selectedModel]); return (
- {TOOL_MODELS_MAP[selectedTool]?.map((model, index) => { + {modelOptions.map((model, index) => { const intro = MODEL_INTRO_MAP[model]; if (!intro) return <>; return ( @@ -71,7 +72,7 @@ const ModelSelectModal: React.FC = memo( component={intro.icon} />
- {intro.name} + {localeText(intro.name)}
{localeText(intro.description)} diff --git a/packages/components/src/Annotator/components/ModelSelector/index.tsx b/packages/components/src/Annotator/components/ModelSelector/index.tsx index 759c87f..881ca0e 100644 --- a/packages/components/src/Annotator/components/ModelSelector/index.tsx +++ b/packages/components/src/Annotator/components/ModelSelector/index.tsx @@ -1,32 +1,37 @@ +import Icon from '@ant-design/icons'; import { Select } from 'antd'; import { useLocale } from 'dds-utils/locale'; import { memo } from 'react'; -import { DrawData } from '../../type'; + import { + EBasicToolItem, + EBasicToolTypeMap, EnumModelType, - EObjectType, MODEL_INTRO_MAP, OBJECT_AI_ICON, } from '../../constants'; + import './index.less'; -import Icon from '@ant-design/icons'; interface IProps { - drawData: DrawData; + selectedTool: EBasicToolItem; + selectedModel?: EnumModelType; modelOptions: EnumModelType[]; onSelectModel: (type: EnumModelType) => void; } const ModelSelector: React.FC = memo( - ({ drawData, modelOptions, onSelectModel }) => { + ({ selectedTool, selectedModel, modelOptions, onSelectModel }) => { const { localeText } = useLocale(); + const objectType = EBasicToolTypeMap[selectedTool]; + return (
diff --git a/packages/components/src/Annotator/components/ObjectList/index.tsx b/packages/components/src/Annotator/components/ObjectList/index.tsx index e6bd0f7..78adf80 100644 --- a/packages/components/src/Annotator/components/ObjectList/index.tsx +++ b/packages/components/src/Annotator/components/ObjectList/index.tsx @@ -1,3 +1,15 @@ +import Icon, { + DeleteOutlined, + EyeInvisibleOutlined, + EyeOutlined, +} from '@ant-design/icons'; +import { useKeyPress } from 'ahooks'; +import { Button, Collapse, List, Tabs, Tooltip } from 'antd'; +import classNames from 'classnames'; +import { useWindowResize } from 'dds-hooks'; +import { useLocale } from 'dds-utils/locale'; +import { isEqual } from 'lodash'; +import VirtualList, { ListRef } from 'rc-virtual-list'; import React, { memo, useCallback, @@ -6,32 +18,22 @@ import React, { useRef, useState, } from 'react'; -import { Button, Collapse, List, Tabs, Tooltip } from 'antd'; -import { OBJECT_ICON } from '../../constants'; -import { ReactComponent as DownArrorIcon } from '../../assets/downArror.svg'; -import { ReactComponent as Palette } from '../../assets/palette.svg'; +import { Updater } from 'use-immer'; + import { ReactComponent as Attribute } from '../../assets/attribute.svg'; +import { ReactComponent as DownArrorIcon } from '../../assets/downArror.svg'; import { ReactComponent as Layer } from '../../assets/layer.svg'; -import classNames from 'classnames'; -import Icon, { - DeleteOutlined, - EyeInvisibleOutlined, - EyeOutlined, -} from '@ant-design/icons'; +import { ReactComponent as Palette } from '../../assets/palette.svg'; +import { OBJECT_ICON } from '../../constants'; +import { EDITOR_SHORTCUTS, EShortcuts } from '../../constants/shortcuts'; import { Category, DrawData, IAnnotationObject, IAnnotsDisplayOptions, } from '../../type'; -import { useKeyPress } from 'ahooks'; -import { EDITOR_SHORTCUTS, EShortcuts } from '../../constants/shortcuts'; -import { useLocale } from 'dds-utils/locale'; -import VirtualList, { ListRef } from 'rc-virtual-list'; -import { useWindowResize } from 'dds-hooks'; -import { isEqual } from 'lodash'; + import './index.less'; -import { Updater } from 'use-immer'; export interface IProps { objects: IAnnotationObject[]; diff --git a/packages/components/src/Annotator/components/PointItem/index.tsx b/packages/components/src/Annotator/components/PointItem/index.tsx index c6ed230..8ded57f 100644 --- a/packages/components/src/Annotator/components/PointItem/index.tsx +++ b/packages/components/src/Annotator/components/PointItem/index.tsx @@ -1,6 +1,8 @@ -import { KEYPOINTS_VISIBLE_TYPE } from '../../constants'; -import { useLocale } from 'dds-utils/locale'; import { Select } from 'antd'; +import { useLocale } from 'dds-utils/locale'; + +import { KEYPOINTS_VISIBLE_TYPE } from '../../constants'; + import styles from './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/PointsEditModal/index.tsx b/packages/components/src/Annotator/components/PointsEditModal/index.tsx index dd78595..33265fa 100644 --- a/packages/components/src/Annotator/components/PointsEditModal/index.tsx +++ b/packages/components/src/Annotator/components/PointsEditModal/index.tsx @@ -1,18 +1,20 @@ +import { DownCircleOutlined, UpCircleOutlined } from '@ant-design/icons'; import { Card } from 'antd'; import classNames from 'classnames'; -import { FloatWrapper } from '../FloatWrapper'; -import { memo, useMemo, useState } from 'react'; import { useLocale } from 'dds-utils/locale'; -import { EditState, EditorMode, IAnnotationObject } from '../../type'; +import { memo, useMemo, useState } from 'react'; +import { Updater } from 'use-immer'; + import { EElementType, EObjectType, KEYPOINTS_VISIBLE_TYPE, } from '../../constants'; -import './index.less'; +import { EditState, EditorMode, IAnnotationObject } from '../../type'; +import { FloatWrapper } from '../FloatWrapper'; import PointItem from '../PointItem'; -import { DownCircleOutlined, UpCircleOutlined } from '@ant-design/icons'; -import { Updater } from 'use-immer'; + +import './index.less'; interface IProps { mode: EditorMode; diff --git a/packages/components/src/Annotator/components/PopoverMenu/index.tsx b/packages/components/src/Annotator/components/PopoverMenu/index.tsx index ba5f463..1376a81 100644 --- a/packages/components/src/Annotator/components/PopoverMenu/index.tsx +++ b/packages/components/src/Annotator/components/PopoverMenu/index.tsx @@ -1,4 +1,5 @@ import { FloatWrapper } from '../FloatWrapper'; + import './index.less'; interface IPopoverMenu { diff --git a/packages/components/src/Annotator/components/SegConfirmModal/index.tsx b/packages/components/src/Annotator/components/SegConfirmModal/index.tsx index e39492e..63dbb44 100644 --- a/packages/components/src/Annotator/components/SegConfirmModal/index.tsx +++ b/packages/components/src/Annotator/components/SegConfirmModal/index.tsx @@ -1,12 +1,14 @@ +import { useKeyPress } from 'ahooks'; import { Button, Card } from 'antd'; import classNames from 'classnames'; -import { FloatWrapper } from '../FloatWrapper'; +import { useLocale } from 'dds-utils/locale'; import { memo, useMemo } from 'react'; -import { useKeyPress } from 'ahooks'; + +import { EObjectType } from '../../constants'; import { EDITOR_SHORTCUTS, EShortcuts } from '../../constants/shortcuts'; -import { useLocale } from 'dds-utils/locale'; import { EditorMode, IAnnotationObject } from '../../type'; -import { EObjectType } from '../../constants'; +import { FloatWrapper } from '../FloatWrapper'; + import './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/ShortcutsInfo/index.tsx b/packages/components/src/Annotator/components/ShortcutsInfo/index.tsx index e5ae72b..dfff441 100644 --- a/packages/components/src/Annotator/components/ShortcutsInfo/index.tsx +++ b/packages/components/src/Annotator/components/ShortcutsInfo/index.tsx @@ -1,7 +1,10 @@ -import { Dropdown, Menu, MenuProps, Tooltip } from 'antd'; -import { ReactComponent as KeyboardIcon } from '../../assets/keyboard-down.svg'; import Icon from '@ant-design/icons'; +import { Dropdown, Menu, MenuProps, Tooltip } from 'antd'; +import classNames from 'classnames'; +import { useLocale } from 'dds-utils/locale'; import { memo, useMemo } from 'react'; + +import { ReactComponent as KeyboardIcon } from '../../assets/keyboard-down.svg'; import { convertAliasToSymbol, EDITOR_SHORTCUTS, @@ -9,11 +12,10 @@ import { EShortcutType, TShortcutItem, } from '../../constants/shortcuts'; -import { useLocale } from 'dds-utils/locale'; -import './index.less'; -import classNames from 'classnames'; import { EditorMode } from '../../type'; +import './index.less'; + interface IProps { mode: EditorMode; // viewOnly: boolean; diff --git a/packages/components/src/Annotator/components/SliderToolBar/index.tsx b/packages/components/src/Annotator/components/SliderToolBar/index.tsx index 81340c3..ac6916b 100644 --- a/packages/components/src/Annotator/components/SliderToolBar/index.tsx +++ b/packages/components/src/Annotator/components/SliderToolBar/index.tsx @@ -1,6 +1,12 @@ -import { Button, Popover } from 'antd'; import Icon, { ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons'; +import { useKeyPress } from 'ahooks'; +import { Button, Popover } from 'antd'; import classNames from 'classnames'; +import { useLocale } from 'dds-utils/locale'; +import { memo, useMemo } from 'react'; + +import { ReactComponent as DragToolIcon } from '../../assets/drag.svg'; +import { ReactComponent as ZoomResize } from '../../assets/zoomResize.svg'; import { EBasicToolItem, EObjectType, @@ -14,17 +20,13 @@ import { TOOL_MODELS_MAP, EnumModelType, } from '../../constants'; -import { ReactComponent as DragToolIcon } from '../../assets/drag.svg'; -import { useKeyPress } from 'ahooks'; import { EDITOR_SHORTCUTS, EShortcuts, TShortcutItem, } from '../../constants/shortcuts'; -import { memo, useMemo } from 'react'; import { getIconFromShortcut } from '../ShortcutsInfo'; -import { useLocale } from 'dds-utils/locale'; -import { ReactComponent as ZoomResize } from '../../assets/zoomResize.svg'; + import './index.less'; type TToolItem = { diff --git a/packages/components/src/Annotator/components/SmartAnnotationControl/index.tsx b/packages/components/src/Annotator/components/SmartAnnotationControl/index.tsx index b22572a..9175867 100644 --- a/packages/components/src/Annotator/components/SmartAnnotationControl/index.tsx +++ b/packages/components/src/Annotator/components/SmartAnnotationControl/index.tsx @@ -1,3 +1,14 @@ +import { CloseOutlined } from '@ant-design/icons'; +import Icon from '@ant-design/icons/lib/components/Icon'; +import { Button, Card, Select, Slider, Space } from 'antd'; +import classNames from 'classnames'; +import { useLocale } from 'dds-utils/locale'; +import { useMemo, memo, useState } from 'react'; +import { useImmer } from 'use-immer'; + +import { ReactComponent as DragToolIcon } from '../../assets/drag.svg'; +import { ReactComponent as MouseLeftIcon } from '../../assets/mouse-left.svg'; +import { ReactComponent as MouseRightIcon } from '../../assets/mouse-right.svg'; import { OBJECT_ICON, EBasicToolItem, @@ -8,19 +19,10 @@ import { EToolType, EnumModelType, } from '../../constants'; -import { CloseOutlined } from '@ant-design/icons'; -import Icon from '@ant-design/icons/lib/components/Icon'; -import { Button, Card, Select, Slider, Space } from 'antd'; -import classNames from 'classnames'; -import { useMemo, memo, useState } from 'react'; -import { FloatWrapper } from '../FloatWrapper'; -import { useLocale } from 'dds-utils/locale'; import { OnAiAnnotationFunc } from '../../hooks/useActions'; -import { useImmer } from 'use-immer'; -import { ReactComponent as DragToolIcon } from '../../assets/drag.svg'; -import { ReactComponent as MouseLeftIcon } from '../../assets/mouse-left.svg'; -import { ReactComponent as MouseRightIcon } from '../../assets/mouse-right.svg'; import { Category } from '../../type'; +import { FloatWrapper } from '../FloatWrapper'; + import './index.less'; interface IProps { @@ -97,7 +99,12 @@ const SmartAnnotationControl: React.FC = memo( icon: OBJECT_ICON[EObjectType.Skeleton], }, [EBasicToolItem.Mask]: { - name: localeText('DDSAnnotator.smart.mask.name'), + name: + selectedModel === EnumModelType.SegmentByMask + ? localeText('DDSAnnotator.smart.isg.name') + : selectedModel === EnumModelType.SegmentEverything + ? localeText('DDSAnnotator.smart.sam.name') + : localeText('DDSAnnotator.smart.ivp.name'), icon: OBJECT_ICON[EObjectType.Mask], }, }; @@ -143,12 +150,18 @@ const SmartAnnotationControl: React.FC = memo( const isVisible = useMemo(() => { if (!AIAnnotation || selectedTool === EBasicToolItem.Drag) return false; - if ( - (selectedTool === EBasicToolItem.Mask && - selectedSubTool !== ESubToolItem.AutoSegmentEverything) || - selectedTool === EBasicToolItem.Polygon - ) + if (selectedTool === EBasicToolItem.Mask) { + if (selectedModel === EnumModelType.SegmentEverything) { + return selectedSubTool === ESubToolItem.AutoSegmentEverything; + } else if (selectedModel === EnumModelType.SegmentByMask) { + return false; + } else if (selectedModel === EnumModelType.IVP) { + return isBatchEditing; + } return false; + } + + if (selectedTool === EBasicToolItem.Polygon) return false; if (selectedTool === EBasicToolItem.Rectangle) { if (selectedModel === EnumModelType.Detection) { @@ -312,26 +325,28 @@ const SmartAnnotationControl: React.FC = memo(
))} - {selectedTool === EBasicToolItem.Rectangle && - selectedModel === EnumModelType.IVP && ( -
-
- {localeText('DDSAnnotator.smart.tip')}: - {localeText('DDSAnnotator.smart.tip.visualPrompt')} -
-
- - -
+ {((selectedTool === EBasicToolItem.Rectangle && + selectedModel === EnumModelType.IVP) || + (selectedTool === EBasicToolItem.Mask && + selectedModel === EnumModelType.IVP)) && ( +
+
+ {localeText('DDSAnnotator.smart.tip')}: + {localeText('DDSAnnotator.smart.tip.visualPrompt')}
- )} +
+ + +
+
+ )} {selectedTool === EBasicToolItem.Skeleton && (isBatchEditing ? ( <> @@ -409,6 +424,7 @@ const SmartAnnotationControl: React.FC = memo( ))} {selectedTool === EBasicToolItem.Mask && + selectedModel === EnumModelType.SegmentEverything && selectedSubTool === ESubToolItem.AutoSegmentEverything && ( <>
= memo( {toolOptions.basicTools.map((item) => ToolItemBtn(item))} {isAIAnnotationActive && ( <> - {toolOptions.basicTools.length > 0 && ( -
- )} + {toolOptions.basicTools.length > 0 && + toolOptions.smartTools.length > 0 && ( +
+ )} {toolOptions.smartTools.map((item) => ToolItemBtn(item))} )} diff --git a/packages/components/src/Annotator/components/TopPagination/index.tsx b/packages/components/src/Annotator/components/TopPagination/index.tsx index 3038f87..fbc5c45 100644 --- a/packages/components/src/Annotator/components/TopPagination/index.tsx +++ b/packages/components/src/Annotator/components/TopPagination/index.tsx @@ -1,11 +1,13 @@ -import { Button, Tooltip } from 'antd'; import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { useKeyPress } from 'ahooks'; +import { Button, Tooltip } from 'antd'; import classNames from 'classnames'; -import { AnnoItem } from '../../type'; +import { useLocale } from 'dds-utils/locale'; import { memo, useState } from 'react'; -import { useKeyPress } from 'ahooks'; + import { EDITOR_SHORTCUTS, EShortcuts } from '../../constants/shortcuts'; -import { useLocale } from 'dds-utils/locale'; +import { AnnoItem } from '../../type'; + import './index.less'; interface IProps { diff --git a/packages/components/src/Annotator/components/TopTools/index.tsx b/packages/components/src/Annotator/components/TopTools/index.tsx index 59d8323..2439612 100644 --- a/packages/components/src/Annotator/components/TopTools/index.tsx +++ b/packages/components/src/Annotator/components/TopTools/index.tsx @@ -1,6 +1,7 @@ -import React from 'react'; -import classNames from 'classnames'; import { Tooltip } from 'antd'; +import classNames from 'classnames'; +import React from 'react'; + import './index.less'; export interface ITopToolItem { diff --git a/packages/components/src/Annotator/constants/index.ts b/packages/components/src/Annotator/constants/index.ts index 73436b0..ea8e250 100644 --- a/packages/components/src/Annotator/constants/index.ts +++ b/packages/components/src/Annotator/constants/index.ts @@ -1,18 +1,18 @@ -import { ReactComponent as RectIcon } from '../assets/rectangle.svg'; -import { ReactComponent as RectAiIcon } from '../assets/rectangle-ai.svg'; -import { ReactComponent as PolygonIcon } from '../assets/polygon.svg'; -import { ReactComponent as PolygonAiIcon } from '../assets/polygon-ai.svg'; -import { ReactComponent as SkeletonIcon } from '../assets/skeleton.svg'; -import { ReactComponent as SkeletonAiIcon } from '../assets/skeleton-ai.svg'; -import { ReactComponent as MaskIcon } from '../assets/mask.svg'; -import { ReactComponent as MaskAiIcon } from '../assets/mask-ai.svg'; -import { ReactComponent as MagicIcon } from '../assets/magic.svg'; import { ReactComponent as CustomIcon } from '../assets/custom.svg'; -import { ReactComponent as UndoIcon } from '../assets/undo.svg'; +import { ReactComponent as DeleteAllIcon } from '../assets/delete_all.svg'; +import { ReactComponent as MagicIcon } from '../assets/magic.svg'; +import { ReactComponent as MaskAiIcon } from '../assets/mask-ai.svg'; +import { ReactComponent as MaskIcon } from '../assets/mask.svg'; +import { ReactComponent as PolygonAiIcon } from '../assets/polygon-ai.svg'; +import { ReactComponent as PolygonIcon } from '../assets/polygon.svg'; +import { ReactComponent as RectAiIcon } from '../assets/rectangle-ai.svg'; +import { ReactComponent as RectIcon } from '../assets/rectangle.svg'; import { ReactComponent as RedoIcon } from '../assets/redo.svg'; import { ReactComponent as RepeatIcon } from '../assets/repeat.svg'; -import { ReactComponent as DeleteAllIcon } from '../assets/delete_all.svg'; +import { ReactComponent as SkeletonAiIcon } from '../assets/skeleton-ai.svg'; +import { ReactComponent as SkeletonIcon } from '../assets/skeleton.svg'; import { ReactComponent as TextPromptIcon } from '../assets/text-prompt.svg'; +import { ReactComponent as UndoIcon } from '../assets/undo.svg'; import { ReactComponent as VisualPromptIcon } from '../assets/visual-prompt.svg'; export enum DisplayOption { @@ -59,6 +59,7 @@ export enum EObjectType { Mask = 'Mask', Matting = 'Matting', Point = 'Point', + Polyline = 'Polyline', } export enum EElementType { @@ -122,7 +123,11 @@ export const TOOL_MODELS_MAP: Record = { [EBasicToolItem.Drag]: [], [EBasicToolItem.Rectangle]: [EnumModelType.Detection, EnumModelType.IVP], [EBasicToolItem.Polygon]: [EnumModelType.SegmentByPolygon], - [EBasicToolItem.Mask]: [EnumModelType.SegmentByMask], + [EBasicToolItem.Mask]: [ + EnumModelType.SegmentEverything, + EnumModelType.SegmentByMask, + EnumModelType.IVP, + ], [EBasicToolItem.Skeleton]: [EnumModelType.Pose], }; @@ -138,17 +143,29 @@ export const MODEL_INTRO_MAP: Partial< > > = { [EnumModelType.Detection]: { - name: 'Grounding-DINO', + name: 'DDSAnnotator.smart.gdino.name', icon: TextPromptIcon, description: 'DDSAnnotator.smart.gdino.desc', hightlight: false, }, [EnumModelType.IVP]: { - name: 'iVP', + name: 'DDSAnnotator.smart.ivp.name', icon: VisualPromptIcon, description: 'DDSAnnotator.smart.ivp.desc', hightlight: true, }, + [EnumModelType.SegmentEverything]: { + name: 'DDSAnnotator.smart.sam.name', + icon: VisualPromptIcon, + description: 'DDSAnnotator.smart.sam.desc', + hightlight: false, + }, + [EnumModelType.SegmentByMask]: { + name: 'DDSAnnotator.smart.isg.name', + icon: VisualPromptIcon, + description: 'DDSAnnotator.smart.isg.desc', + hightlight: false, + }, }; export const LABEL_TOOL_MAP = { @@ -168,11 +185,14 @@ export const OBJECT_ICON: Record< [EObjectType.Mask]: MaskIcon, [EObjectType.Matting]: MaskIcon, [EObjectType.Point]: CustomIcon, + [EObjectType.Polyline]: CustomIcon, [EObjectType.Custom]: CustomIcon, [EObjectType.Classification]: CustomIcon, }; -export const OBJECT_AI_ICON = { +export const OBJECT_AI_ICON: Partial< + Record>> +> = { [EObjectType.Rectangle]: RectAiIcon, [EObjectType.Skeleton]: SkeletonAiIcon, [EObjectType.Polygon]: PolygonAiIcon, diff --git a/packages/components/src/Annotator/constants/render.ts b/packages/components/src/Annotator/constants/render.ts index 3a98beb..bc4a7e8 100644 --- a/packages/components/src/Annotator/constants/render.ts +++ b/packages/components/src/Annotator/constants/render.ts @@ -1,3 +1,5 @@ +import { LineType } from '../type'; + export const ANNO_FILL_ALPHA = { DEFAULT: 0, CREATING: 0, @@ -42,3 +44,54 @@ export const PROMPT_FILL_COLOR = { POSITIVE: 'rgba(1, 128, 0, 0.6)', NEGATIVE: 'rgba(255, 3, 0, 0.6)', }; + +export const LINE_STYLE_MAP: Record< + LineType, + { + lineDash: number[]; + thickness: number; + } +> = { + [LineType.Solid]: { + lineDash: [], + thickness: 2, + }, + [LineType.DoubleSolid]: { + lineDash: [], + thickness: 4, + }, + [LineType.LCurbside]: { + lineDash: [], + thickness: 2, + }, + [LineType.RCurbside]: { + lineDash: [], + thickness: 2, + }, + [LineType.Unknown]: { + lineDash: [], + thickness: 2, + }, + [LineType.Dashed]: { + lineDash: [4, 4], + thickness: 2, + }, + [LineType.DoubleDashed]: { + lineDash: [4, 4], + thickness: 4, + }, + [LineType.LDashedRSolid]: { + lineDash: [4, 8, 4, 8, 16, 4], + thickness: 4, + }, + [LineType.LSolidRDashed]: { + lineDash: [4, 8, 16, 4], + thickness: 4, + }, +}; + +export const LINE_COLOR = { + Yellow: '#d97945', + White: '#de1760', + Other: '#72af44', +}; diff --git a/packages/components/src/Annotator/editor.tsx b/packages/components/src/Annotator/editor.tsx index b78dd8e..f9a4daf 100755 --- a/packages/components/src/Annotator/editor.tsx +++ b/packages/components/src/Annotator/editor.tsx @@ -1,16 +1,37 @@ -import React, { useEffect, useMemo, useRef } from 'react'; import { Dropdown, Modal } from 'antd'; -import { EBasicToolItem } from './constants'; +import classNames from 'classnames'; +import { cloneDeep } from 'lodash'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Updater, useImmer } from 'use-immer'; -import useLabels from './hooks/useLabels'; -import useActions from './hooks/useActions'; + +import AttributeEditor from './components/AttributeEditor'; +import ClassificationPanel from './components/Classification'; +import { ImageView } from './components/ImageView'; +import ModelSelectModal from './components/ModelSelectModal'; import { ObjectList } from './components/ObjectList'; +import PointsEditModal from './components/PointsEditModal'; +import SegConfirmModal from './components/SegConfirmModal'; +import SliderToolBar from './components/SliderToolBar'; import SmartAnnotationControl from './components/SmartAnnotationControl'; import { TopPagination } from './components/TopPagination'; +import { DisplayOption, EBasicToolItem, TOOL_MODELS_MAP } from './constants'; +import useActions from './hooks/useActions'; +import useAttributes from './hooks/useAttributes'; +import useCanvasContainer from './hooks/useCanvasContainer'; +import useCanvasRender from './hooks/useCanvasRender'; +import useColor from './hooks/useColor'; +import useDataEffect from './hooks/useDataEffect'; import useHistory from './hooks/useHistory'; +import useLabels from './hooks/useLabels'; +import useMouseCursor from './hooks/useMouseCursor'; +import useMouseEvents from './hooks/useMouseEvents'; import useObjects from './hooks/useObjects'; -import useCanvasContainer from './hooks/useCanvasContainer'; -import { cloneDeep } from 'lodash'; +import useShortcuts from './hooks/useShortcuts'; +import useSubTools from './hooks/useSubtools'; +import useToolActions from './hooks/useToolActions'; +import useTopTools from './hooks/useTopTools'; +import useTranslate from './hooks/useTranslate'; +import { useToolInstances } from './tools/base'; import { BaseObject, Category, @@ -22,27 +43,8 @@ import { EditorMode, DrawObject, } from './type'; -import useMouseCursor from './hooks/useMouseCursor'; -import useShortcuts from './hooks/useShortcuts'; -import useToolActions from './hooks/useToolActions'; -import useMouseEvents from './hooks/useMouseEvents'; -import useCanvasRender from './hooks/useCanvasRender'; -import useDataEffect from './hooks/useDataEffect'; -import useSubTools from './hooks/useSubtools'; -import { useToolInstances } from './tools/base'; -import useColor from './hooks/useColor'; -import { ImageView } from './components/ImageView'; -import useTranslate from './hooks/useTranslate'; -import ClassificationPanel from './components/Classification'; -import AttributeEditor from './components/AttributeEditor'; -import SegConfirmModal from './components/SegConfirmModal'; -import useAttributes from './hooks/useAttributes'; -import SliderToolBar from './components/SliderToolBar'; -import useTopTools from './hooks/useTopTools'; + import './index.less'; -import classNames from 'classnames'; -import ModelSelectModal from './components/ModelSelectModal'; -import PointsEditModal from './components/PointsEditModal'; export interface EditProps { isOldMode?: boolean; // is old dataset design mode @@ -66,6 +68,7 @@ export interface EditProps { layoutOptions?: { wrapHeight?: string; hideRightList?: boolean; + hideMainToolBar?: boolean; hideTopBar?: boolean; hideTopBarActions?: boolean; hideUndoRedoActions?: boolean; @@ -75,6 +78,7 @@ export interface EditProps { left: number; }; }; + displayOptionsResult?: { [key in DisplayOption]?: boolean }; manualMode?: boolean; forceColorByObject?: boolean; limitActiveObject?: boolean; @@ -114,6 +118,7 @@ const Edit: React.FC = (props) => { titleElements, actionElements, layoutOptions, + displayOptionsResult, manualMode, forceColorByObject, limitActiveObject, @@ -198,6 +203,7 @@ const Edit: React.FC = (props) => { undo, redo, clearHistory, + flagSaved, hadChangeRecord, updateHistory, setDrawDataWithHistory, @@ -283,11 +289,11 @@ const Edit: React.FC = (props) => { clientSize, imagePos, containerMouse, - updateAllObject, hadChangeRecord, getAnnotColor, categories, translateObject, + flagSaved, onCancel, onSave, onCommit, @@ -366,6 +372,7 @@ const Edit: React.FC = (props) => { onAiAnnotation, getAnnotColor, categories, + displayOptionsResult, }); const { updateRender, renderPopoverMenu } = useCanvasRender({ @@ -538,23 +545,25 @@ const Edit: React.FC = (props) => { className="editor-container" style={{ top: layoutOptions?.hideTopBar ? '0' : '' }} > - + {!layoutOptions?.hideMainToolBar && ( + + )}
{currImageItem && ( = (props) => { = (props) => { onCancelBatchEdit={onAbortBatchObjects} /> setDrawData((s) => { diff --git a/packages/components/src/Annotator/hooks/useActions.tsx b/packages/components/src/Annotator/hooks/useActions.tsx index 1179796..f190c1c 100644 --- a/packages/components/src/Annotator/hooks/useActions.tsx +++ b/packages/components/src/Annotator/hooks/useActions.tsx @@ -1,19 +1,11 @@ -import { - getVisibleAreaForImage, - translateBoundingBoxToRect, - translatePointsToPointObjs, - translatePointZoom, - translateRectToAbsBbox, - getCanvasPoint, - getNaturalPoint, - translateRectToBoundingBox, - translatePointObjsToPointAttrs, - convertFrameObjectsIntoFramesObjects, - translateRectZoom, - translateAbsBBoxToRect, -} from '../utils/compute'; +import { useModel } from '@umijs/max'; +import { CursorState } from 'ahooks/lib/useMouse'; import { Modal, message } from 'antd'; +import { ModalStaticFunctions } from 'antd/es/modal/confirm'; +import { useLocale } from 'dds-utils/locale'; +import { useCallback } from 'react'; import { Updater } from 'use-immer'; + import { BODY_TEMPLATE, EBasicToolItem, @@ -22,14 +14,8 @@ import { EObjectType, ESubToolItem, } from '../constants'; -import { - getImageBase64, - isBase64, - isBlobUrl, - isHttpsUrl, -} from '../utils/base64'; -import { useLocale } from 'dds-utils/locale'; -import { useModel } from '@umijs/max'; +import { NsApiAnnotator, fetchModelResults } from '../sevices'; +import { rleToCanvas } from '../tools/useMask'; import { DrawData, AnnoItem, @@ -40,16 +26,24 @@ import { EObjectStatus, Category, VideoFramesData, + EPromptType, + ReqPromptItem, + IMask, } from '../type'; -import { objectToRle, rleToCanvas } from '../tools/useMask'; -import { CursorState } from 'ahooks/lib/useMouse'; -import { ModalStaticFunctions } from 'antd/es/modal/confirm'; -import { useCallback } from 'react'; +import { getImageBase64, getServerAddressableUrl } from '../utils/base64'; import { - NsApiAnnotator, - fetchModelResults, - getOssUrlByBlobUrl, -} from '../sevices'; + getVisibleAreaForImage, + translateBoundingBoxToRect, + translatePointsToPointObjs, + translatePointZoom, + translateRectToAbsBbox, + getCanvasPoint, + getNaturalPoint, + translateRectToBoundingBox, + translatePointObjsToPointAttrs, + translateRectZoom, + translateAbsBBoxToRect, +} from '../utils/compute'; interface IProps { mode: EditorMode; @@ -65,17 +59,29 @@ interface IProps { clientSize: ISize; containerMouse: CursorState; imagePos: React.MutableRefObject; - updateAllObject: (objectList: IAnnotationObject[]) => void; hadChangeRecord: boolean; getAnnotColor: (category: string, forceColorByCategory?: boolean) => string; categories: Category[]; translateObject?: (object: any) => any; + flagSaved?: () => void; onCancel?: () => void; onSave?: (id: string, labels: any[]) => Promise; onCommit?: (id: string, labels: any[]) => Promise; - onReviewModify?: (id: string, labels: any[]) => Promise; - onReviewAccept?: (id: string, labels: any[]) => Promise; - onReviewReject?: (id: string, labels: any[]) => Promise; + onReviewModify?: ( + id: string, + labels: any[], + frameIssues?: Record, + ) => Promise; + onReviewAccept?: ( + id: string, + labels: any[], + frameIssues?: Record, + ) => Promise; + onReviewReject?: ( + id: string, + labels: any[], + frameIssues?: Record, + ) => Promise; classificationOptions?: Category[]; } @@ -114,11 +120,11 @@ const useActions = ({ clientSize, imagePos, containerMouse, - updateAllObject, hadChangeRecord, categories, getAnnotColor, translateObject, + flagSaved, onCancel, onSave, onCommit, @@ -135,13 +141,15 @@ const useActions = ({ s.isRequiring = requiring; }); - const requestAiDetection = async (source: string, aiLabels: string) => { + const requestAiDetection = async (aiLabels: string) => { + if (!currImageItem) return; + try { setLoading(true); - const result = await fetchModelResults( + const { result } = await fetchModelResults( EnumModelType.Detection, { - image: source, + image: await getImageBase64(currImageItem.url), text: aiLabels, }, ); @@ -189,17 +197,7 @@ const useActions = ({ } }; - const convertPromptFormat = ( - prompt: PromptItem[], - ): { - type: string; - isPositive: boolean; - point?: number[]; - rect?: number[]; - stroke?: number[]; - radius?: number; - polygons?: number[][]; - }[] => { + const convertPromptFormat = (prompt: PromptItem[]): ReqPromptItem[] => { const newPromptArr = prompt.map((item) => { const { type, isPositive, point, rect, stroke, radius, polygons } = item; @@ -275,10 +273,10 @@ const useActions = ({ }; const requestIvpDetection = async ( - base64Img: string, + drawData: DrawData, promptsQueue?: PromptItem[], ) => { - if (!promptsQueue || !currImageItem) return; + if (!currImageItem || !promptsQueue) return; if (promptsQueue.every((prompt) => !prompt.isPositive)) { message.error(localeText('DDSAnnotator.smart.msg.positivePrompt')); @@ -290,25 +288,21 @@ const useActions = ({ try { setLoading(true); - - let url = base64Img; - if (isHttpsUrl(currImageItem.url)) { - url = currImageItem.url; - } else if (isBlobUrl(currImageItem.url)) { - url = await getOssUrlByBlobUrl( - currImageItem.fileName || 'image', - currImageItem.url, - ); - } - const reqParams = { - promptImage: url, - inferImage: url, prompts: convertPromptFormat(promptsQueue || []), labelTypes: ['bbox'], }; + if (drawData.prompt.sessionId) { + Object.assign(reqParams, { sessionId: drawData.prompt.sessionId }); + } else { + const url = await getServerAddressableUrl(currImageItem.url); + Object.assign(reqParams, { + promptImage: url, + inferImage: url, + }); + } - const result = await fetchModelResults( + const { result, sessionId } = await fetchModelResults( EnumModelType.IVP, reqParams, ); @@ -353,6 +347,91 @@ const useActions = ({ s.creatingObject = { ...s.objectList[s.activeObjectIndex] }; } s.prompt.promptsQueue = promptsQueue; + s.prompt.sessionId = sessionId; + s.prompt.creatingPrompt = undefined; + }); + message.success(localeText('DDSAnnotator.smart.msg.success')); + } + } catch (error: any) { + message.error(localeText('DDSAnnotator.smart.msg.error')); + setDrawDataWithHistory((s) => { + s.prompt.creatingPrompt = undefined; + }); + } finally { + setLoading(false); + } + }; + + const requestIvpMask = async ( + drawData: DrawData, + promptsQueue?: PromptItem[], + ) => { + if (!currImageItem || !promptsQueue) return; + + if (promptsQueue.every((prompt) => !prompt.isPositive)) { + message.error(localeText('DDSAnnotator.smart.msg.positivePrompt')); + setDrawDataWithHistory((s) => { + s.prompt.creatingPrompt = undefined; + }); + return; + } + + try { + setLoading(true); + const reqParams = { + prompts: convertPromptFormat(promptsQueue || []), + labelTypes: ['mask'], + }; + if (drawData.prompt.sessionId) { + Object.assign(reqParams, { sessionId: drawData.prompt.sessionId }); + } else { + const url = await getServerAddressableUrl(currImageItem.url); + Object.assign(reqParams, { + promptImage: url, + inferImage: url, + }); + } + + const { result, sessionId } = await fetchModelResults( + EnumModelType.IVP, + reqParams, + ); + + if (result) { + // Display mask in different color + setEditState((s) => { + s.annotsDisplayOptions.colorByCategory = false; + }); + + const { objects } = result; + const newObjects: IAnnotationObject[] = objects + .filter((item) => !!item.mask) + .map((item) => { + const color = getAnnotColor(editState.latestLabelId); + const maskRleStr = item.mask?.counts || ''; + return { + type: EObjectType.Mask, + hidden: false, + labelId: editState.latestLabelId, + maskRle: maskRleStr, + maskCanvasElement: rleToCanvas(maskRleStr, naturalSize, color), + status: EObjectStatus.Checked, + conf: item.score, + color: getAnnotColor(editState.latestLabelId, true), + }; + }); + + setDrawDataWithHistory((s) => { + s.isBatchEditing = true; + const commitedObjects = s.objectList.filter( + (obj) => obj.status === EObjectStatus.Commited, + ); + s.objectList = [...commitedObjects, ...newObjects]; + if (s.creatingObject && s.objectList[s.activeObjectIndex]) { + s.creatingObject = { ...s.objectList[s.activeObjectIndex] }; + } + s.prompt.promptsQueue = promptsQueue; + s.prompt.sessionId = sessionId; s.prompt.creatingPrompt = undefined; }); message.success(localeText('DDSAnnotator.smart.msg.success')); @@ -399,15 +478,14 @@ const useActions = ({ const requestAiSegmentByPolygon = async ( drawData: DrawData, - source: string, promptsQueue?: PromptItem[], ) => { - if (!promptsQueue) return; + if (!currImageItem || !promptsQueue) return; const reqParams = { image: editState.imageCacheIdForPolygon ? `image_id://${editState.imageCacheIdForPolygon}` - : source, + : await getImageBase64(currImageItem.url), density: drawData.pointResolution, area: getCurrVisibleBbox(), prompts: convertPromptFormat(promptsQueue || []), @@ -419,10 +497,11 @@ const useActions = ({ try { setLoading(true); - const result = await fetchModelResults( - EnumModelType.SegmentByPolygon, - reqParams, - ); + const { result } = + await fetchModelResults( + EnumModelType.SegmentByPolygon, + reqParams, + ); if (result) { const { image, polygons, sessionId } = result; @@ -485,64 +564,50 @@ const useActions = ({ const requestAiSegmentByMask = async ( drawData: DrawData, - source: string, promptsQueue?: PromptItem[], ) => { - if (!promptsQueue) return; - const currMask = - drawData.creatingObject?.maskCanvasElement || - drawData.creatingObject?.tempMaskSteps - ? objectToRle( - clientSize, - naturalSize, - drawData.creatingObject?.tempMaskSteps || [], - drawData.creatingObject?.maskCanvasElement, - ) - : []; + if (!promptsQueue || !currImageItem) return; const reqParams: NsApiAnnotator.FetchAIMaskSegmentReq = { - maskRle: currMask || [], - maskId: drawData.prompt.sessionId || '', - prompt: convertPromptFormat(promptsQueue || []), - area: getCurrVisibleBbox(), + prompts: convertPromptFormat(promptsQueue || []), }; - - if (editState.imageCacheId) { - Object.assign(reqParams, { imageId: editState.imageCacheId }); + if (drawData.prompt.sessionId) { + Object.assign(reqParams, { sessionId: drawData.prompt.sessionId }); } else { - Object.assign(reqParams, { image: source }); + Object.assign(reqParams, { + image: await getServerAddressableUrl(currImageItem.url), + }); } try { setLoading(true); - const result = await fetchModelResults( - EnumModelType.SegmentByMask, - reqParams, - ); + const { result, sessionId } = + await fetchModelResults( + EnumModelType.SegmentByMask, + reqParams, + ); if (result) { - const { maskId, maskRle, imageId } = result; + const { mask } = result; const color = drawData.creatingObject?.color || getAnnotColor(editState.latestLabelId); + const maskRleStr = mask.counts || ''; const creatingObj = { type: EObjectType.Mask, hidden: false, labelId: editState.latestLabelId, currIndex: -1, - maskCanvasElement: rleToCanvas(maskRle, naturalSize, color), - maskRle, + maskCanvasElement: rleToCanvas(maskRleStr, naturalSize, color), + maskRle: maskRleStr, status: EObjectStatus.Checked, color, }; setDrawDataWithHistory((s) => { s.creatingObject = creatingObj; s.prompt.promptsQueue = promptsQueue; - s.prompt.sessionId = maskId; + s.prompt.sessionId = sessionId; s.prompt.creatingPrompt = undefined; }); - setEditState((s) => { - s.imageCacheId = imageId; - }); message.success(localeText('DDSAnnotator.smart.msg.success')); } } catch (error: any) { @@ -557,13 +622,14 @@ const useActions = ({ const requestAiPoseEstimation = async ( drawData: DrawData, - source: string, aiLabels: string, ) => { + if (!currImageItem) return; + // TODO: Integrate custom templates const { lines, pointNames, pointColors } = BODY_TEMPLATE; const reqParams = { - image: source, + image: await getImageBase64(currImageItem.url), targets: aiLabels, template: { lines, @@ -612,7 +678,7 @@ const useActions = ({ try { setLoading(true); - const result = await fetchModelResults( + const { result } = await fetchModelResults( EnumModelType.Pose, reqParams, ); @@ -676,11 +742,9 @@ const useActions = ({ } }; - const requestEdgeStitchingForMask = async ( - drawData: DrawData, - source: string, - ) => { + const requestEdgeStitchingForMask = async (drawData: DrawData) => { if ( + !currImageItem || !drawData.prompt.creatingPrompt?.stroke || !drawData.prompt.creatingPrompt?.radius ) @@ -702,13 +766,10 @@ const useActions = ({ return; } - const rleList = maskObjects.map((item) => { - const maskRle = - objectToRle(clientSize, naturalSize, [], item.maskCanvasElement) || []; - const categoryName = - categories.find((c) => c.id === item.labelId)?.name || ''; - return { maskRle, categoryName }; - }); + const masks: IMask[] = maskObjects.map((item) => ({ + counts: item.maskRle || '', + size: [naturalSize.height, naturalSize.width], + })); const points = stroke.reduce((acc: number[], point: IPoint) => { const { x, y } = point; @@ -717,39 +778,37 @@ const useActions = ({ }, []); const reqParams: NsApiAnnotator.FetchEdgeStitchingReq = { - rleList, - stroke: points, - radius, + masks, + prompts: [ + { + type: EPromptType.Stroke, + stroke: points, + radius, + }, + ], }; - - if (editState.imageCacheId) { - Object.assign(reqParams, { imageId: editState.imageCacheId }); + if (drawData.prompt.sessionId) { + Object.assign(reqParams, { sessionId: drawData.prompt.sessionId }); } else { - Object.assign(reqParams, { image: source }); + Object.assign(reqParams, { + image: await getServerAddressableUrl(currImageItem.url), + }); } - Object.assign(reqParams, { image: source }); - try { setLoading(true); - const result = await fetchModelResults( - EnumModelType.MaskEdgeStitching, - reqParams, - ); - if (result && result.rleList?.length > 0) { - const maskObjects = result.rleList.map((item) => { - const labelId = - categories.find((c) => c.name === item.categoryName)?.id || ''; - const color = getAnnotColor(labelId); + const { result, sessionId } = + await fetchModelResults( + EnumModelType.MaskEdgeStitching, + reqParams, + ); + if (result && result.masks?.length > 0) { + const newMaskObjects = maskObjects.map((item, index) => { + const maskRleStr = result.masks?.[index]?.counts || ''; return { - type: EObjectType.Mask, - hidden: false, - labelId: labelId, - maskRle: item.maskRle, - maskCanvasElement: rleToCanvas(item.maskRle, naturalSize, color), - conf: 1, - status: EObjectStatus.Commited, - color, + ...item, + maskRle: maskRleStr, + maskCanvasElement: rleToCanvas(maskRleStr, naturalSize, item.color), }; }); @@ -758,54 +817,55 @@ const useActions = ({ (obj) => obj.type !== EObjectType.Mask, ); - const updatedObjects = [...leftObjs, ...maskObjects]; - updateAllObject(updatedObjects); + setDrawDataWithHistory((s) => { + s.objectList = [...leftObjs, ...newMaskObjects]; + s.prompt.creatingPrompt = undefined; + s.prompt.sessionId = sessionId; + }); message.success(localeText('DDSAnnotator.smart.msg.success')); } } catch (error: any) { message.error(localeText('DDSAnnotator.smart.msg.error')); - } finally { - setLoading(false); - setDrawData((s) => { + setDrawDataWithHistory((s) => { s.prompt.creatingPrompt = undefined; }); + } finally { + setLoading(false); } }; const requestSegmentEverything = async ( - source: string, params?: NsApiAnnotator.SegmentEverythingParams, ) => { + if (!currImageItem) return; + const reqParams: NsApiAnnotator.FetchSegmentEverythingReq = { + image: await getServerAddressableUrl(currImageItem.url), ...params, }; - if (editState.imageCacheId) { - Object.assign(reqParams, { imageId: editState.imageCacheId }); - } else { - Object.assign(reqParams, { image: source }); - } - try { setLoading(true); - const result = await fetchModelResults( - EnumModelType.SegmentEverything, - reqParams, - ); - if (result && result.rleList?.length > 0) { + const { result } = + await fetchModelResults( + EnumModelType.SegmentEverything, + reqParams, + ); + if (result && result.masks?.length > 0) { // change to display different color setEditState((s) => { s.annotsDisplayOptions.colorByCategory = false; }); - const maskObjects: IAnnotationObject[] = result.rleList.map((item) => { + const maskObjects: IAnnotationObject[] = result.masks.map((item) => { const color = getAnnotColor(editState.latestLabelId); + const maskRleStr = item?.counts || ''; return { type: EObjectType.Mask, hidden: false, labelId: editState.latestLabelId, - maskRle: item.maskRle, - maskCanvasElement: rleToCanvas(item.maskRle, naturalSize, color), + maskRle: maskRleStr, + maskCanvasElement: rleToCanvas(maskRleStr, naturalSize, color), conf: 1, status: EObjectStatus.Checked, color, @@ -840,7 +900,8 @@ const useActions = ({ !aiLabels && (drawData.selectedTool === EBasicToolItem.Skeleton || (drawData.selectedTool === EBasicToolItem.Rectangle && - drawData.selectedModel === EnumModelType.Detection)) + drawData.selectedModel[drawData.selectedTool] === + EnumModelType.Detection)) ) { message.warning(localeText('DDSAnnotator.smart.msg.labelRequired')); return; @@ -850,46 +911,43 @@ const useActions = ({ localeText('DDSAnnotator.smart.msg.loading'), 100000, ); - let imgSrc = `${currImageItem?.url}`; - - try { - setIsRequiring(true); - if (!isBase64(imgSrc)) { - imgSrc = await getImageBase64(imgSrc); - } - } catch (e: any) { - message.error('ImageToBase64 Error:', e); - } - try { setIsRequiring(true); const aiType = type || EBasicToolTypeMap[drawData.selectedTool]; switch (aiType) { case EObjectType.Rectangle: { - if (drawData.selectedModel === EnumModelType.Detection) { - await requestAiDetection(imgSrc, aiLabels || ''); + if ( + drawData.selectedModel[drawData.selectedTool] === + EnumModelType.Detection + ) { + await requestAiDetection(aiLabels || ''); } else { - await requestIvpDetection(imgSrc, promptsQueue); + await requestIvpDetection(drawData, promptsQueue); } break; } case EObjectType.Skeleton: { - await requestAiPoseEstimation(drawData, imgSrc, aiLabels || ''); + await requestAiPoseEstimation(drawData, aiLabels || ''); break; } case EObjectType.Polygon: { - await requestAiSegmentByPolygon(drawData, imgSrc, promptsQueue); + await requestAiSegmentByPolygon(drawData, promptsQueue); break; } case EObjectType.Mask: { - if (drawData.selectedSubTool === ESubToolItem.AutoEdgeStitching) { - await requestEdgeStitchingForMask(drawData, imgSrc); - } else if ( - drawData.selectedSubTool === ESubToolItem.AutoSegmentEverything - ) { - await requestSegmentEverything(imgSrc, segmentEverythingParams); + const model = drawData.selectedModel[drawData.selectedTool]; + if (model === EnumModelType.SegmentEverything) { + if (drawData.selectedSubTool === ESubToolItem.AutoEdgeStitching) { + await requestEdgeStitchingForMask(drawData); + } else if ( + drawData.selectedSubTool === ESubToolItem.AutoSegmentEverything + ) { + await requestSegmentEverything(segmentEverythingParams); + } + } else if (model === EnumModelType.IVP) { + await requestIvpMask(drawData, promptsQueue); } else { - await requestAiSegmentByMask(drawData, imgSrc, promptsQueue); + await requestAiSegmentByMask(drawData, promptsQueue); } break; } @@ -911,15 +969,10 @@ const useActions = ({ ); const translateDrawData = useCallback( - (drawData: DrawData): [string, any[]] => { + (drawData: DrawData): [string, any[], any] => { let objectList = []; if (framesData) { - objectList = convertFrameObjectsIntoFramesObjects( - drawData.objectList, - framesData.objects, - framesData.list.length, - framesData.activeIndex, - ).map((objs) => { + objectList = framesData.objects.map((objs) => { const availObjs: any = {}; objs.forEach((obj, frameIndex) => { if (obj && !obj.frameEmpty) { @@ -950,6 +1003,7 @@ const useActions = ({ }), ...objectList, ], + framesData ? { [framesData.activeIndex]: {} } : undefined, ]; }, [currImageItem, translateObject, framesData], @@ -1019,6 +1073,7 @@ const useActions = ({ setIsRequiring(true); try { await onSave(id, labels); + flagSaved?.(); } catch (error) { console.error(error); } diff --git a/packages/components/src/Annotator/hooks/useAttributes.ts b/packages/components/src/Annotator/hooks/useAttributes.ts index fa15f12..ecb04a1 100644 --- a/packages/components/src/Annotator/hooks/useAttributes.ts +++ b/packages/components/src/Annotator/hooks/useAttributes.ts @@ -1,11 +1,12 @@ import { useCallback } from 'react'; +import { Updater } from 'use-immer'; + import { Category, DrawData, IAnnotationObject, IAttributeValue, } from '../type'; -import { Updater } from 'use-immer'; interface IProps { setDrawDataWithHistory: Updater; diff --git a/packages/components/src/Annotator/hooks/useCanvasContainer.tsx b/packages/components/src/Annotator/hooks/useCanvasContainer.tsx index 92c7daa..9b1f771 100644 --- a/packages/components/src/Annotator/hooks/useCanvasContainer.tsx +++ b/packages/components/src/Annotator/hooks/useCanvasContainer.tsx @@ -1,3 +1,5 @@ +import { useEventListener, useMouse, useSize } from 'ahooks'; +import { fixedFloatNum } from 'dds-utils/digit'; import React, { useCallback, useEffect, @@ -5,9 +7,8 @@ import React, { useRef, useState, } from 'react'; -import { useEventListener, useMouse, useSize } from 'ahooks'; import { useImmer } from 'use-immer'; -import { isInCanvas, zoomImgSize } from '../utils/compute'; + import { MIN_SCALE, MAX_SCALE, @@ -17,8 +18,8 @@ import { EObjectType, EBasicToolItem, } from '../constants'; -import { fixedFloatNum } from 'dds-utils/digit'; import { DrawData } from '../type'; +import { isInCanvas, zoomImgSize } from '../utils/compute'; interface IProps { isRequiring: boolean; diff --git a/packages/components/src/Annotator/hooks/useCanvasRender.tsx b/packages/components/src/Annotator/hooks/useCanvasRender.tsx index 18fee88..fafd697 100644 --- a/packages/components/src/Annotator/hooks/useCanvasRender.tsx +++ b/packages/components/src/Annotator/hooks/useCanvasRender.tsx @@ -1,26 +1,13 @@ -import React from 'react'; import { CursorState } from 'ahooks/lib/useMouse'; -import { - DrawData, - EditState, - IAnnotationObject, - ICreatingObject, -} from '../type'; -import { translateAnnotCoord } from '../utils/compute'; +import React from 'react'; + +import PopoverMenu from '../components/PopoverMenu'; import { EBasicToolItem, EElementType, EnumModelType, EObjectType, } from '../constants'; -import { - addFilter, - clearCanvas, - drawImage, - removeFilter, - resizeSmoothCanvas, - setCanvasGlobalAlpha, -} from '../utils/draw'; import { ANNO_FILL_ALPHA, ANNO_FILL_COLOR, @@ -29,8 +16,22 @@ import { ANNO_STROKE_COLOR, } from '../constants/render'; import { ToolInstanceHookReturn } from '../tools/base'; +import { + DrawData, + EditState, + IAnnotationObject, + ICreatingObject, +} from '../type'; import { hexToRgba } from '../utils/color'; -import PopoverMenu from '../components/PopoverMenu'; +import { translateAnnotCoord } from '../utils/compute'; +import { + addFilter, + clearCanvas, + drawImage, + removeFilter, + resizeSmoothCanvas, + setCanvasGlobalAlpha, +} from '../utils/draw'; interface IProps { visible: boolean; @@ -43,6 +44,7 @@ interface IProps { activeCanvasRef: React.RefObject; imgRef: React.RefObject; objectHooksMap: Record; + videoLoading?: boolean; } const useCanvasRender = ({ @@ -56,6 +58,7 @@ const useCanvasRender = ({ activeCanvasRef, imgRef, objectHooksMap, + videoLoading, }: IProps) => { // ================================================================================================================= // Render @@ -159,7 +162,8 @@ const useCanvasRender = ({ }); } else if ( theDrawData.selectedTool === EBasicToolItem.Rectangle && - theDrawData.selectedModel === EnumModelType.IVP + theDrawData.selectedModel[theDrawData.selectedTool] === + EnumModelType.IVP ) { objectHooksMap[EObjectType.Rectangle].renderPrompt({ prompt, @@ -242,6 +246,12 @@ const useCanvasRender = ({ ) return; + // video load maybe use long time, should clearCanvas first + if (videoLoading) { + clearCanvas(canvasRef.current); + return; + } + resizeSmoothCanvas(canvasRef.current, { width: containerMouse.elementW, height: containerMouse.elementH, diff --git a/packages/components/src/Annotator/hooks/useColor.ts b/packages/components/src/Annotator/hooks/useColor.ts index a6cdcc0..b6d1a90 100644 --- a/packages/components/src/Annotator/hooks/useColor.ts +++ b/packages/components/src/Annotator/hooks/useColor.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { getCategoryColors, hsvToRgb, rgbArrayToHex } from '../utils/color'; + import { Category, EditState } from '../type'; +import { getCategoryColors, hsvToRgb, rgbArrayToHex } from '../utils/color'; interface IProps { categories: Category[]; diff --git a/packages/components/src/Annotator/hooks/useDataEffect.ts b/packages/components/src/Annotator/hooks/useDataEffect.ts index 389ed8f..79b1c4a 100644 --- a/packages/components/src/Annotator/hooks/useDataEffect.ts +++ b/packages/components/src/Annotator/hooks/useDataEffect.ts @@ -1,5 +1,7 @@ -import { useCallback, useEffect } from 'react'; import { cloneDeep } from 'lodash'; +import { useCallback, useEffect } from 'react'; +import { Updater } from 'use-immer'; + import { BaseObject, Category, @@ -12,7 +14,7 @@ import { VideoFramesData, } from '../type'; import { scaleDrawData, scaleFramesObjects } from '../utils/compute'; -import { Updater } from 'use-immer'; + import usePreviousState from './usePreviousState'; interface IProps { @@ -135,15 +137,15 @@ const useDataEffect = ({ const resetDataWithImageData = useCallback( ( - imageData: AnnoItem, - visible: boolean, + imageData?: AnnoItem, + visible: boolean = false, clearHistoryQueue: boolean = true, ) => { setAnnotations([]); resetDrawData(); resetEditData(); if (clearHistoryQueue) clearHistory(); - if (visible) { + if (visible && imageData) { applyImageAnnots(imageData); } }, diff --git a/packages/components/src/Annotator/hooks/useHistory.ts b/packages/components/src/Annotator/hooks/useHistory.ts index 6d7c85f..ba52618 100644 --- a/packages/components/src/Annotator/hooks/useHistory.ts +++ b/packages/components/src/Annotator/hooks/useHistory.ts @@ -1,8 +1,13 @@ -import { useCallback, useState } from 'react'; -import { DraftFunction, Updater, useImmer } from 'use-immer'; import { cloneDeep, isEqual } from 'lodash'; -import { scaleDrawData, scaleFramesObjects } from '../utils/compute'; +import { useCallback, useRef, useState } from 'react'; +import { DraftFunction, Updater, useImmer } from 'use-immer'; + import { BaseObject, DrawData, VideoFramesData } from '../type'; +import { + convertFrameObjectsIntoFramesObjects, + scaleDrawData, + scaleFramesObjects, +} from '../utils/compute'; export interface HistoryItem { drawData: DrawData; @@ -29,10 +34,15 @@ const useHistory = ({ framesData, setFramesData, }: IProps) => { + const hadChangeRecordRef = useRef(false); const [historyQueue, setHistoryQueue] = useImmer([]); const [currentIndex, setCurrIndex] = useState(0); const maxCacheSize = 20; + const flagSaved = () => { + hadChangeRecordRef.current = false; + }; + const autoSave = (item: HistoryItem) => { if (onAutoSave) { const annotations = item.drawData.objectList.map( @@ -44,16 +54,18 @@ const useHistory = ({ const updateCurrentRecord = useCallback( (record: HistoryItem) => { - if (record.framesData) { - setFramesData?.({ - ...record.framesData, - objects: scaleFramesObjects( + // video + setFramesData?.((s) => { + if (record.framesData) { + s.activeIndex = record.framesData?.activeIndex; + s.objects = scaleFramesObjects( record.framesData.objects, record.clientSize, clientSize, - ), - }); - } + ); + } + }); + const updateDrawData = scaleDrawData( record.drawData, record.clientSize, @@ -70,7 +82,12 @@ const useHistory = ({ */ const undo = useCallback(() => { if (currentIndex > 0) { - setCurrIndex((prevIndex) => prevIndex - 1); + setCurrIndex((prevIndex) => { + hadChangeRecordRef.current = Boolean( + historyQueue.length > 1 && prevIndex - 1 !== 0, + ); + return prevIndex - 1; + }); updateCurrentRecord(historyQueue[currentIndex - 1]); } }, [currentIndex, historyQueue, updateCurrentRecord]); @@ -80,7 +97,12 @@ const useHistory = ({ */ const redo = useCallback(() => { if (currentIndex < historyQueue.length - 1) { - setCurrIndex((prevIndex) => prevIndex + 1); + setCurrIndex((prevIndex) => { + hadChangeRecordRef.current = Boolean( + historyQueue.length > 1 && prevIndex + 1 !== 0, + ); + return prevIndex + 1; + }); updateCurrentRecord(historyQueue[currentIndex + 1]); } }, [currentIndex, historyQueue, updateCurrentRecord]); @@ -92,11 +114,11 @@ const useHistory = ({ drawData: DrawData, theframesData?: VideoFramesData, ) => { - const item = { + const item = cloneDeep({ drawData, clientSize, framesData: theframesData || framesData, - }; + }); setHistoryQueue((queue) => { if (queue[currentIndex] && isEqual(item, queue[currentIndex])) { return queue; @@ -108,13 +130,40 @@ const useHistory = ({ // fix to change image current render return queue; } - // console.log('>>> updata history', item.drawData, framesData); + + if ( + !theframesData && + item.framesData && + setFramesData && + item.drawData.objectList.length && + !isEqual( + item.drawData.objectList, + queue[currentIndex]?.drawData?.objectList, + ) + ) { + // video && not update framesData && single frame objectList changed + // sync frame objectlist change to every frame + item.framesData.objects = convertFrameObjectsIntoFramesObjects( + item.drawData.objectList, + item.framesData.objects, + item.framesData.list.length, + item.framesData.activeIndex, + naturalSize, + ); + setFramesData((s) => { + s.objects = cloneDeep(item.framesData!.objects); + }); + } queue.splice(currentIndex + 1); queue.push(item); if (queue.length > maxCacheSize) { queue.shift(); } setCurrIndex(queue.length - 1); + + hadChangeRecordRef.current = Boolean( + queue.length > 1 && queue.length - 1 !== 0, + ); }); autoSave(item); }; @@ -143,7 +192,8 @@ const useHistory = ({ redo, clearHistory, setDrawDataWithHistory, - hadChangeRecord: historyQueue.length > 1 && currentIndex !== 0, + flagSaved, + hadChangeRecord: hadChangeRecordRef.current, }; }; diff --git a/packages/components/src/Annotator/hooks/useLabels.ts b/packages/components/src/Annotator/hooks/useLabels.ts index a44a89f..3613b2b 100644 --- a/packages/components/src/Annotator/hooks/useLabels.ts +++ b/packages/components/src/Annotator/hooks/useLabels.ts @@ -1,12 +1,7 @@ +import { cloneDeep } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Updater } from 'use-immer'; -import { - Category, - DrawData, - EditState, - EditorMode, - IAnnotationObject, -} from '../type'; + import { EBasicToolItem, EBasicToolTypeMap, @@ -15,7 +10,13 @@ import { KEYPOINTS_VISIBLE_TYPE, LABEL_TOOL_MAP, } from '../constants'; -import { cloneDeep } from 'lodash'; +import { + Category, + DrawData, + EditState, + EditorMode, + IAnnotationObject, +} from '../type'; interface IProps { isOldMode?: boolean; diff --git a/packages/components/src/Annotator/hooks/useMouseCursor.ts b/packages/components/src/Annotator/hooks/useMouseCursor.ts index 6a4057f..87bb952 100644 --- a/packages/components/src/Annotator/hooks/useMouseCursor.ts +++ b/packages/components/src/Annotator/hooks/useMouseCursor.ts @@ -1,7 +1,8 @@ import { useCallback, useEffect } from 'react'; + +import { EBasicToolItem } from '../constants'; import { DrawData, EditState } from '../type'; import { Direction } from '../utils/compute'; -import { EBasicToolItem } from '../constants'; interface IProps { topCanvas: HTMLCanvasElement | null; diff --git a/packages/components/src/Annotator/hooks/useMouseEvents.tsx b/packages/components/src/Annotator/hooks/useMouseEvents.tsx index 458301e..19a230d 100644 --- a/packages/components/src/Annotator/hooks/useMouseEvents.tsx +++ b/packages/components/src/Annotator/hooks/useMouseEvents.tsx @@ -1,5 +1,17 @@ -import { useCallback, useRef, useState } from 'react'; +import { useEventListener, useRafInterval } from 'ahooks'; import { CursorState } from 'ahooks/lib/useMouse'; +import { fixedFloatNum } from 'dds-utils/digit'; +import { useCallback, useRef, useState } from 'react'; +import { Updater } from 'use-immer'; + +import { + EBasicToolItem, + EBasicToolTypeMap, + EElementType, + EnumModelType, + EObjectType, +} from '../constants'; +import { ToolInstanceHookReturn } from '../tools/base'; import { Category, DrawData, @@ -15,17 +27,6 @@ import { judgeFocusOnObject, judgeFocusOnPointAllObject, } from '../utils/compute'; -import { - EBasicToolItem, - EBasicToolTypeMap, - EElementType, - EnumModelType, - EObjectType, -} from '../constants'; -import { Updater } from 'use-immer'; -import { ToolInstanceHookReturn } from '../tools/base'; -import { useEventListener, useRafInterval } from 'ahooks'; -import { fixedFloatNum } from 'dds-utils/digit'; interface IProps { visible: boolean; @@ -382,7 +383,8 @@ const useMouseEvents = ({ // 2. Create object if ( drawData.selectedTool !== EBasicToolItem.Drag && - (!drawData.isBatchEditing || drawData.selectedModel === EnumModelType.IVP) + (!drawData.isBatchEditing || + drawData.selectedModel[drawData.selectedTool] === EnumModelType.IVP) ) { setDrawData((s) => { s.editingAttribute = undefined; diff --git a/packages/components/src/Annotator/hooks/useObjects.ts b/packages/components/src/Annotator/hooks/useObjects.ts index 17af253..8622edc 100644 --- a/packages/components/src/Annotator/hooks/useObjects.ts +++ b/packages/components/src/Annotator/hooks/useObjects.ts @@ -1,5 +1,8 @@ -import { EElementType, EObjectType } from '../constants'; +import { cloneDeep } from 'lodash'; +import { useCallback, useMemo } from 'react'; import { Updater } from 'use-immer'; + +import { EElementType, EObjectType } from '../constants'; import { BaseObject, DrawData, @@ -11,8 +14,6 @@ import { EObjectStatus, VideoFramesData, } from '../type'; -import { useCallback, useMemo } from 'react'; -import { cloneDeep } from 'lodash'; interface IProps { mode: EditorMode; @@ -153,7 +154,7 @@ const useObjects = ({ newDrawData.editingAttribute = undefined; setDrawData(newDrawData); updateHistory(cloneDeep(newDrawData), cloneDeep(newFramesData)); - }, [mode]); + }, [mode, framesData, drawData]); const updateObject = (object: IAnnotationObject, index: number) => { if (mode !== EditorMode.Edit || !drawData.objectList[index]) return; diff --git a/packages/components/src/Annotator/hooks/useShortcuts.ts b/packages/components/src/Annotator/hooks/useShortcuts.ts index b40ce9a..3f2ee24 100644 --- a/packages/components/src/Annotator/hooks/useShortcuts.ts +++ b/packages/components/src/Annotator/hooks/useShortcuts.ts @@ -1,5 +1,6 @@ -import { Updater } from 'use-immer'; import { useKeyPress } from 'ahooks'; +import { Updater } from 'use-immer'; + import { EObjectType } from '../constants'; import { EDITOR_SHORTCUTS, EShortcuts } from '../constants/shortcuts'; import { @@ -86,7 +87,7 @@ const useShortcuts = ({ EDITOR_SHORTCUTS[EShortcuts.PanImage].shortcut, (event: KeyboardEvent) => { if (!visible) return; - event.preventDefault(); + // event.preventDefault(); if (event.type === 'keydown' && !isMousePress) { setEditState((s) => { s.allowMove = true; diff --git a/packages/components/src/Annotator/hooks/useSubtools.tsx b/packages/components/src/Annotator/hooks/useSubtools.tsx index cb5a92b..c28cc87 100644 --- a/packages/components/src/Annotator/hooks/useSubtools.tsx +++ b/packages/components/src/Annotator/hooks/useSubtools.tsx @@ -1,26 +1,27 @@ -import { TShortcutItem } from '../constants/shortcuts'; +import Icon from '@ant-design/icons'; +import { Slider } from 'antd'; import { useLocale } from 'dds-utils/locale'; +import { useMemo } from 'react'; + +import { ReactComponent as AddPromptIcon } from '../assets/add-prompt.svg'; +import { ReactComponent as BrushAddIcon } from '../assets/brush-add.svg'; +import { ReactComponent as BrushEraseIcon } from '../assets/brush-erase.svg'; +import { ReactComponent as EdgeStitchIcon } from '../assets/edge-stitch.svg'; +import { ReactComponent as MagicBoxIcon } from '../assets/magic-box.svg'; +import { ReactComponent as StrokeIcon } from '../assets/magic-brush.svg'; +import { ReactComponent as ClickIcon } from '../assets/magic-click.svg'; +import { ReactComponent as PenAddIcon } from '../assets/pen-add.svg'; +import { ReactComponent as PenEraseIcon } from '../assets/pen-erase.svg'; +import { ReactComponent as RemovePromptIcon } from '../assets/remove-prompt.svg'; +import { ReactComponent as SegmentEverythingIcon } from '../assets/segment-everything.svg'; import { EBasicToolItem, EnumModelType, EObjectType, ESubToolItem, } from '../constants'; -import Icon from '@ant-design/icons'; -import { ReactComponent as PenAddIcon } from '../assets/pen-add.svg'; -import { ReactComponent as PenEraseIcon } from '../assets/pen-erase.svg'; -import { ReactComponent as BrushAddIcon } from '../assets/brush-add.svg'; -import { ReactComponent as BrushEraseIcon } from '../assets/brush-erase.svg'; -import { ReactComponent as MagicBoxIcon } from '../assets/magic-box.svg'; -import { ReactComponent as ClickIcon } from '../assets/magic-click.svg'; -import { ReactComponent as EdgeStitchIcon } from '../assets/edge-stitch.svg'; -import { ReactComponent as SegmentEverythingIcon } from '../assets/segment-everything.svg'; -import { ReactComponent as StrokeIcon } from '../assets/magic-brush.svg'; -import { ReactComponent as AddPromptIcon } from '../assets/add-prompt.svg'; -import { ReactComponent as RemovePromptIcon } from '../assets/remove-prompt.svg'; -import { useMemo } from 'react'; +import { TShortcutItem } from '../constants/shortcuts'; import { DrawData } from '../type'; -import { Slider } from 'antd'; export type TToolItem = { key: T; @@ -96,7 +97,7 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { [isManualAvailable, drawData.creatingObject], ); - const smartMaskTools: TToolItem[] = useMemo(() => { + const isgTools: TToolItem[] = useMemo(() => { return [ { key: ESubToolItem.AutoSegmentByBox, @@ -117,24 +118,8 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { icon: , available: true, }, - { - key: ESubToolItem.AutoEdgeStitching, - name: localeText('DDSAnnotator.subtoolbar.mask.edgeStitch'), - icon: , - available: true, - withSize: true, - }, - { - key: ESubToolItem.AutoSegmentEverything, - name: localeText('DDSAnnotator.subtoolbar.mask.sam'), - icon: , - available: isSegEverythingAvailable, - description: isSegEverythingAvailable - ? localeText('DDSAnnotator.subtoolbar.mask.sam.desc') - : localeText('DDSAnnotator.subtoolbar.mask.sam.notAllow'), - }, ]; - }, [isSegEverythingAvailable]); + }, []); const smartPolygonTools: TToolItem[] = useMemo(() => { return [ @@ -180,6 +165,27 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { ]; }, []); + const samTools: TToolItem[] = useMemo(() => { + return [ + { + key: ESubToolItem.AutoSegmentEverything, + name: localeText('DDSAnnotator.subtoolbar.mask.sam'), + icon: , + available: isSegEverythingAvailable, + description: isSegEverythingAvailable + ? localeText('DDSAnnotator.subtoolbar.mask.sam.desc') + : localeText('DDSAnnotator.subtoolbar.mask.sam.notAllow'), + }, + { + key: ESubToolItem.AutoEdgeStitching, + name: localeText('DDSAnnotator.subtoolbar.mask.edgeStitch'), + icon: , + available: true, + withSize: true, + }, + ]; + }, [isSegEverythingAvailable]); + const showSubTools = useMemo(() => { if (drawData.selectedTool === EBasicToolItem.Mask) return true; @@ -192,7 +198,7 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { if ( drawData.selectedTool === EBasicToolItem.Rectangle && drawData.AIAnnotation && - drawData.selectedModel === EnumModelType.IVP + drawData.selectedModel[drawData.selectedTool] === EnumModelType.IVP ) return true; @@ -217,9 +223,33 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { drawData.selectedTool === EBasicToolItem.Mask || drawData.creatingObject?.type === EObjectType.Mask ) { + if (!drawData.AIAnnotation) { + return { + basicTools: basicMaskTools, + smartTools: [], + }; + } + + const currModel = drawData.selectedModel[drawData.selectedTool]; + if (currModel === EnumModelType.IVP) { + return { + basicTools: [], + smartTools: ivpTools, + }; + } else if (currModel === EnumModelType.SegmentByMask) { + return { + basicTools: [], + smartTools: isgTools, + }; + } else if (currModel === EnumModelType.SegmentEverything) { + return { + basicTools: [], + smartTools: samTools, + }; + } return { basicTools: basicMaskTools, - smartTools: smartMaskTools, + smartTools: [], }; } else if ( drawData.selectedTool === EBasicToolItem.Polygon || @@ -249,7 +279,7 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { } else if ( drawData.selectedTool === EBasicToolItem.Rectangle && drawData.AIAnnotation && - drawData.selectedModel === EnumModelType.IVP + drawData.selectedModel[drawData.selectedTool] === EnumModelType.IVP ) { return { basicTools: [], @@ -265,9 +295,10 @@ const useSubTools = ({ drawData, onChangePointResolution }: IProps) => { drawData.creatingObject, drawData.AIAnnotation, drawData.selectedModel, - basicMaskTools, - smartMaskTools, smartPolygonTools, + basicMaskTools, + isgTools, + samTools, ivpTools, drawData.pointResolution, ]); diff --git a/packages/components/src/Annotator/hooks/useToolActions.ts b/packages/components/src/Annotator/hooks/useToolActions.ts index a53914e..087f456 100644 --- a/packages/components/src/Annotator/hooks/useToolActions.ts +++ b/packages/components/src/Annotator/hooks/useToolActions.ts @@ -1,12 +1,16 @@ -import { useCallback } from 'react'; -import { Updater } from 'use-immer'; import { Modal, message } from 'antd'; +import { useLocale } from 'dds-utils/locale'; +import { cloneDeep } from 'lodash'; +import { useCallback, useEffect } from 'react'; +import { Updater } from 'use-immer'; + import { EBasicToolItem, EnumModelType, EObjectType, ESubToolItem, } from '../constants'; +import { objectToRle, rleToCanvas } from '../tools/useMask'; import { DrawData, EditState, @@ -16,9 +20,7 @@ import { IImageDisplayOptions, IAnnotsDisplayOptions, } from '../type'; -import { objectToRle, rleToCanvas } from '../tools/useMask'; -import { useLocale } from 'dds-utils/locale'; -import { cloneDeep } from 'lodash'; + import { OnAiAnnotationFunc } from './useActions'; interface IProps { @@ -244,26 +246,16 @@ const useToolActions = ({ const selectTool = useCallback( (tool: EBasicToolItem) => { + console.log(drawData.selectedTool, drawData.AIAnnotation, tool); if ( mode !== EditorMode.Edit || - (tool === drawData.selectedTool && drawData.AIAnnotation) || + (tool === drawData.selectedTool && !drawData.AIAnnotation) || drawData.isBatchEditing ) return; + setDrawData((s) => { s.selectedTool = tool; - if (tool === EBasicToolItem.Mask) { - s.selectedSubTool = s.AIAnnotation - ? ESubToolItem.AutoSegmentByBox - : ESubToolItem.PenAdd; - } else if (tool === EBasicToolItem.Polygon) { - s.selectedSubTool = ESubToolItem.AutoSegmentByBox; - } else if ( - tool === EBasicToolItem.Rectangle && - s.selectedModel === EnumModelType.IVP - ) { - s.selectedSubTool = ESubToolItem.PositiveVisualPrompt; - } s.AIAnnotation = false; s.activeObjectIndex = -1; s.creatingObject = undefined; @@ -275,19 +267,22 @@ const useToolActions = ({ mode, drawData.selectedTool, drawData.isBatchEditing, - drawData.selectedModel, + drawData.AIAnnotation, ], ); const selectSubTool = useCallback( (tool: ESubToolItem) => { + if (mode !== EditorMode.Edit || tool === drawData.selectedSubTool) return; + if ( - mode !== EditorMode.Edit || - tool === drawData.selectedSubTool || - (drawData.selectedTool === EBasicToolItem.Mask && - drawData.isBatchEditing) - ) + drawData.selectedTool === EBasicToolItem.Mask && + drawData.selectedModel[drawData.selectedTool] === + EnumModelType.SegmentEverything && + drawData.isBatchEditing + ) { return; + } setDrawData((s) => { s.selectedSubTool = tool; @@ -485,17 +480,51 @@ const useToolActions = ({ updateAllObject(newObjectList); }, [drawData.objectList, getAnnotColor]); - const onSelectModel = useCallback((type: EnumModelType) => { + const onSelectModel = useCallback((modelKey: EnumModelType) => { setDrawData((s) => { - s.selectedModel = type; - if (type === EnumModelType.IVP) { - s.selectedSubTool = ESubToolItem.PositiveVisualPrompt; + s.selectedModel[s.selectedTool] = modelKey; + }); + }, []); + + useEffect(() => { + setDrawData((s) => { + if (s.AIAnnotation) { + const model = s.selectedModel[s.selectedTool]; + switch (s.selectedTool) { + case EBasicToolItem.Rectangle: + if (model === EnumModelType.IVP) { + s.selectedSubTool = ESubToolItem.PositiveVisualPrompt; + } else { + s.selectedSubTool = ESubToolItem.PenAdd; // TODO + } + break; + case EBasicToolItem.Mask: + if (model === EnumModelType.IVP) { + s.selectedSubTool = ESubToolItem.PositiveVisualPrompt; + } else if (model === EnumModelType.SegmentEverything) { + const isAvailbale = + (s.objectList.length === 0 && !s.creatingObject) || + s.isBatchEditing; + s.selectedSubTool = isAvailbale + ? ESubToolItem.AutoSegmentEverything + : ESubToolItem.PenAdd; + } else { + s.selectedSubTool = ESubToolItem.AutoSegmentByBox; + } + break; + case EBasicToolItem.Polygon: + s.selectedSubTool = ESubToolItem.AutoSegmentByBox; + break; + case EBasicToolItem.Skeleton: + case EBasicToolItem.Drag: + s.selectedSubTool = ESubToolItem.PenAdd; + break; + } } else { - // TODO s.selectedSubTool = ESubToolItem.PenAdd; } }); - }, []); + }, [drawData.selectedTool, drawData.AIAnnotation, drawData.selectedModel]); return { onChangeObjectLabel, diff --git a/packages/components/src/Annotator/hooks/useTopTools.tsx b/packages/components/src/Annotator/hooks/useTopTools.tsx index e907a86..c5d4803 100644 --- a/packages/components/src/Annotator/hooks/useTopTools.tsx +++ b/packages/components/src/Annotator/hooks/useTopTools.tsx @@ -1,8 +1,17 @@ -import { useMemo } from 'react'; -import { Button, Tooltip } from 'antd'; import Icon, { ArrowLeftOutlined } from '@ant-design/icons'; -import { ReactComponent as LogoIcon } from '../assets/logo.svg'; +import { Button, Tooltip } from 'antd'; +import { useLocale } from 'dds-utils/locale'; +import { useMemo } from 'react'; + import { ReactComponent as DocsIcon } from '../assets/docs.svg'; +import { ReactComponent as LogoIcon } from '../assets/logo.svg'; +import DisplaySettings from '../components/DisplaySettings'; +import EditorStatus from '../components/EditorStatus'; +import LabelSelector from '../components/LabelSelector'; +import ModelSelector from '../components/ModelSelector'; +import { ShortcutsInfo } from '../components/ShortcutsInfo'; +import SubToolBar from '../components/SubToolBar'; +import TopTools from '../components/TopTools'; import { EBasicToolItem, EnumModelType, @@ -17,14 +26,7 @@ import { IAnnotsDisplayOptions, Category, } from '../type'; -import { useLocale } from 'dds-utils/locale'; -import DisplaySettings from '../components/DisplaySettings'; -import { ShortcutsInfo } from '../components/ShortcutsInfo'; -import EditorStatus from '../components/EditorStatus'; -import TopTools from '../components/TopTools'; -import LabelSelector from '../components/LabelSelector'; -import ModelSelector from '../components/ModelSelector'; -import SubToolBar from '../components/SubToolBar'; + import { TSubtoolOptions } from './useSubtools'; interface IProps { @@ -221,7 +223,8 @@ const useTopTools = ({ actions.push({ customElement: ( diff --git a/packages/components/src/Annotator/hooks/useTranslate.ts b/packages/components/src/Annotator/hooks/useTranslate.ts index caecb9f..1e4f7e4 100644 --- a/packages/components/src/Annotator/hooks/useTranslate.ts +++ b/packages/components/src/Annotator/hooks/useTranslate.ts @@ -1,9 +1,19 @@ +import { cloneDeep } from 'lodash'; + import { BODY_TEMPLATE, ELabelType, EObjectType, KEYPOINTS_VISIBLE_TYPE, } from '../constants'; +import { rleToCanvas } from '../tools/useMask'; +import { + IAnnotationObject, + EObjectStatus, + DrawObject, + Category, + BaseObject, +} from '../type'; import { getObjectType, translateBoundingBoxToRect, @@ -20,16 +30,9 @@ import { newTranslatePointObjsToPointAttrs, getCanvasPoint, getNaturalPoint, + translateUVtoPolylinePoints, + convertLaneLineColorToHex, } from '../utils/compute'; -import { - IAnnotationObject, - EObjectStatus, - DrawObject, - Category, - BaseObject, -} from '../type'; -import { rleToCanvas } from '../tools/useMask'; -import { cloneDeep } from 'lodash'; interface IProps { isOldMode?: boolean; @@ -65,6 +68,9 @@ const useTranslate = ({ mask, alpha, point, + polyline: polylineUv, + lineColor, + lineType, } = annotation; const color = getAnnotColor(categoryId || ''); @@ -118,10 +124,11 @@ const useTranslate = ({ Object.assign(newObj, { polygon }); } - if (mask && mask.length) { + if (mask) { + const maskRleStr = mask.counts || ''; Object.assign(newObj, { - maskRle: mask, - maskCanvasElement: rleToCanvas(mask, naturalSize, color), + maskRle: maskRleStr, + maskCanvasElement: rleToCanvas(maskRleStr, naturalSize, color), }); } @@ -144,6 +151,19 @@ const useTranslate = ({ }); } + if (polylineUv && lineType && lineColor) { + const line = translateUVtoPolylinePoints(polylineUv).map((point) => + getCanvasPoint([point.x, point.y], naturalSize, clientSize), + ); + const polyline: IElement = { + group: [line], + visible: true, + lineType, + color: convertLaneLineColorToHex(lineColor), + }; + Object.assign(newObj, { polyline }); + } + newObj.type = getObjectType(newObj); return newObj; }; @@ -188,7 +208,10 @@ const useTranslate = ({ } if (maskRle) { Object.assign(annoObj, { - mask: maskRle, + mask: { + counts: maskRle, + size: [naturalSize.height, naturalSize.width], + }, }); } if (point) { @@ -282,9 +305,10 @@ const useTranslate = ({ break; } case ELabelType.Mask: { + const maskRleStr = labelValue.counts || ''; Object.assign(newObj, { - maskRle: labelValue, - maskCanvasElement: rleToCanvas(labelValue, naturalSize, color), + maskRle: maskRleStr, + maskCanvasElement: rleToCanvas(maskRleStr, naturalSize, color), type: EObjectType.Mask, }); break; @@ -378,7 +402,10 @@ const useTranslate = ({ } case ELabelType.Mask: { if (maskRle) { - annoObj.labelValue = maskRle; + annoObj.labelValue = { + counts: maskRle, + size: [naturalSize.height, naturalSize.width], + }; } break; } diff --git a/packages/components/src/Annotator/preview.tsx b/packages/components/src/Annotator/preview.tsx index e26e049..d85f48c 100755 --- a/packages/components/src/Annotator/preview.tsx +++ b/packages/components/src/Annotator/preview.tsx @@ -1,8 +1,3 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { DisplayOption, EElementType, MAX_SCALE, MIN_SCALE } from './constants'; -import { useImmer } from 'use-immer'; -import TopTools from './components/TopTools'; -import PopoverMenu from './components/PopoverMenu'; import { CloseOutlined, LeftOutlined, @@ -10,10 +5,30 @@ import { ZoomInOutlined, ZoomOutOutlined, } from '@ant-design/icons'; +import { useKeyPress } from 'ahooks'; +import { message } from 'antd'; +import classNames from 'classnames'; +import { cloneDeep, isEmpty } from 'lodash'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useImmer } from 'use-immer'; + +import { ReactComponent as DoubleRightIcon } from './assets/doubleRight.svg'; +import { ReactComponent as DownloadIcon } from './assets/download.svg'; +import { ImageView } from './components/ImageView'; +import PopoverMenu from './components/PopoverMenu'; +import TopTools from './components/TopTools'; +import { DisplayOption, EElementType, MAX_SCALE, MIN_SCALE } from './constants'; +import { EDITOR_SHORTCUTS, EShortcuts } from './constants/shortcuts'; +import useCanvasContainer from './hooks/useCanvasContainer'; +import useCanvasRender from './hooks/useCanvasRender'; +import useColor from './hooks/useColor'; +import useDataEffect from './hooks/useDataEffect'; import useHistory from './hooks/useHistory'; +import useMouseCursor from './hooks/useMouseCursor'; +import useMouseEvents from './hooks/useMouseEvents'; import useObjects from './hooks/useObjects'; -import useCanvasContainer from './hooks/useCanvasContainer'; -import { cloneDeep, isEmpty } from 'lodash'; +import useTranslate from './hooks/useTranslate'; +import { useToolInstances } from './tools/base'; import { BaseObject, Category, @@ -25,21 +40,8 @@ import { EditState, EditorMode, } from './type'; -import useColor from './hooks/useColor'; -import useMouseCursor from './hooks/useMouseCursor'; -import useMouseEvents from './hooks/useMouseEvents'; -import useCanvasRender from './hooks/useCanvasRender'; -import useDataEffect from './hooks/useDataEffect'; -import { useToolInstances } from './tools/base'; -import classNames from 'classnames'; -import { ReactComponent as DoubleRightIcon } from './assets/doubleRight.svg'; -import { ReactComponent as DownloadIcon } from './assets/download.svg'; -import { useKeyPress } from 'ahooks'; -import { EDITOR_SHORTCUTS, EShortcuts } from './constants/shortcuts'; -import { message } from 'antd'; -import { ImageView } from './components/ImageView'; + import './index.less'; -import useTranslate from './hooks/useTranslate'; export interface PreviewProps { isOldMode?: boolean; // is old dataset design mode diff --git a/packages/components/src/Annotator/sevices/index.ts b/packages/components/src/Annotator/sevices/index.ts index 4a32855..540164e 100644 --- a/packages/components/src/Annotator/sevices/index.ts +++ b/packages/components/src/Annotator/sevices/index.ts @@ -2,7 +2,9 @@ import { request } from '@umijs/max'; import { Modal } from 'antd'; import { globalLocaleText } from 'dds-utils/locale'; + import { EnumModelType, EnumTaskStatus } from '../constants'; +import { IMask, ReqPromptItem } from '../type'; export namespace NsApiAnnotator { export type ModelParam = @@ -45,58 +47,31 @@ export namespace NsApiAnnotator { } export interface FetchIVPReq { - promptImage: string; - inferImage: string; - prompts: { - type: string; // 'rect' | 'point' - isPositive: boolean; - rect?: number[]; // [xmin, ymin, xmax, ymax]; - point?: number[]; // [x, y] - }[]; + promptImage?: string; + inferImage?: string; labelTypes: string[]; // ["bbox", "mask"] + prompts: ReqPromptItem[]; + sessionId?: string; } export interface FetchAIPolygonSegmentReq { image: string; // image_id:// | base64:// | http:// | https:// density: number; // (0, 1) default 0.2 area: number[]; // [xmin, ymin, xmax, ymax]; - prompts: { - type: string; // 'rect' | 'point' | 'stroke' | 'modify'; - isPositive: boolean; // - rect?: number[]; // [xmin, ymin, xmax, ymax]; - point?: number[]; // [x, y] - stroke?: number[]; // [x1, y1, x2, y2, ...]; - radius?: number; // brush size while using stroke prompt - polygons?: number[][]; // [[x1, y1, x2, y2, ...], [xn, yn, xn+1, yn+1, ...], ....]; - }[]; + prompts: ReqPromptItem[]; sessionId?: string; } export interface FetchAIMaskSegmentReq { image?: string; // required when first request - imageId?: string; - maskId: string; - maskRle: number[]; - prompt: { - type: string; // 'rect' | 'point' | 'stroke'; - isPositive: boolean; - point?: number[]; // [x, y] - rect?: number[]; // [xmin, ymin, xmax, ymax]; - stroke?: number[]; // [x1, y1, x2, y2]; - radius?: number; - }[]; - area: number[]; // [xmin, ymin, xmax, ymax]; + sessionId?: string; + prompts: ReqPromptItem[]; } export interface FetchEdgeStitchingReq { - image?: string; // base64 - imageId?: string; - rleList: { - maskRle: number[]; - categoryName: string; - }[]; - stroke: number[]; // [x1, y1, x2, y2]; - radius: number; + image?: string; + masks: IMask[]; + prompts: ReqPromptItem[]; } export interface SegmentEverythingParams { @@ -107,7 +82,6 @@ export namespace NsApiAnnotator { export interface FetchSegmentEverythingReq extends SegmentEverythingParams { image?: string; - imageId?: string; } export interface FetchAIPoseEstimationReq { @@ -138,7 +112,8 @@ export namespace NsApiAnnotator { export interface FetchIVPRsp { objects: Array<{ bbox?: number[]; - mask?: number[]; + mask?: IMask; + maskUrl?: string; score: number; }>; } @@ -150,15 +125,10 @@ export namespace NsApiAnnotator { } export interface FetchAIMaskSegmentRsp { - maskRle: number[]; // rle - maskId: string; - imageId: string; + mask: IMask; } export interface FetchEdgeStitchingRsp { - rleList: { - maskRle: number[]; - categoryName: string; - }[]; + masks: IMask[]; } export interface FetchAIPoseEstimationRsp { objects: Array<{ @@ -170,9 +140,7 @@ export namespace NsApiAnnotator { } export interface FetchSegmentEverythingRsp { - rleList: { - maskRle: number[]; - }[]; + masks: IMask[]; } export interface fetchTaskUuid { @@ -183,20 +151,9 @@ export namespace NsApiAnnotator { error: string; status: EnumTaskStatus; uuid: string; + sessionId: string; result: ModelResult; } - export interface FetchUploadSignatureRsp { - downloadUrl: string; - uploadUrl: string; - } - - export interface FetchBetchUploadSignatureRsp { - fileUrls: { - fileName: string; - downloadUrl: string; - uploadUrl: string; - }[]; - } } async function fetchTaskUuid( @@ -231,19 +188,6 @@ function fetchTaskResults( ); } -function fetchMaskTaskResults( - taskUuid: string, - options?: { [key: string]: any }, -) { - return request>( - `${process.env.MODEL_API_PATH}/mask_task_statuses/${taskUuid}`, - { - method: 'GET', - ...(options || {}), - }, - ); -} - export async function pollTaskResults( type: EnumModelType, taskUuid: string, @@ -253,21 +197,14 @@ export async function pollTaskResults( let attempts = 0; while (attempts < maxAttempts) { - const fetchTaskResultsRequest = [ - EnumModelType.SegmentByMask, - EnumModelType.MaskEdgeStitching, - EnumModelType.SegmentEverything, - ].includes(type) - ? fetchMaskTaskResults - : fetchTaskResults; - const results = await fetchTaskResultsRequest(taskUuid); + const rsp = await fetchTaskResults(taskUuid); - if (results.status === EnumTaskStatus.Success) { - return results.result; + if (rsp.status === EnumTaskStatus.Success) { + return rsp; } - if (results.status === EnumTaskStatus.Failed) { - throw new Error(results.error); + if (rsp.status === EnumTaskStatus.Failed) { + throw new Error(rsp.error); } await new Promise((resolve) => { @@ -282,7 +219,7 @@ export async function pollTaskResults( export async function fetchModelResults( type: EnumModelType, params: NsApiAnnotator.ModelParam, -) { +): Promise> { try { const { taskUuid } = await fetchTaskUuid(type, params); const result = await pollTaskResults(type, taskUuid); @@ -297,91 +234,7 @@ export async function fetchModelResults( okText: globalLocaleText('DDSAnnotator.smart.rateLimit.okText'), onOk: () => {}, }); - } else { - throw new Error(error.message); - } - } -} - -export async function fetchUploadSignature( - params: { - fileName: string; - }, - options?: { [key: string]: any }, -) { - return request( - `${process.env.MODEL_API_PATH}/upload_signature`, - { - method: 'POST', - data: { - ...params, - }, - ...(options || {}), - }, - ); -} - -export async function fetchBatchUploadSignature( - params: { - fileNames: string[]; - }, - options?: { [key: string]: any }, -) { - return request( - `${process.env.MODEL_API_PATH}/batch_upload_signatures`, - { - method: 'POST', - data: { - ...params, - }, - ...(options || {}), - }, - ); -} - -export const putFile = async ( - uploadUrl: string, - file?: File, - contentType?: string, -): Promise => { - return new Promise((resolve, reject) => { - if (!file) reject(null); - fetch(uploadUrl, { - method: 'PUT', - headers: { - 'Content-Type': contentType || '', - }, - body: file, - }) - .then((response) => { - if (response.status === 200) { - resolve(response); - } else { - console.error('Upload file error: ', uploadUrl, response); - reject(null); - } - }) - .catch((error) => { - console.error('Upload file error: ', uploadUrl, error); - reject(null); - }); - }); -}; - -export async function getOssUrlByBlobUrl( - fileName: string, - blobUrl: string, -): Promise { - try { - const { downloadUrl, uploadUrl } = await fetchUploadSignature({ fileName }); - const response = await fetch(blobUrl); - if (!response.ok) { - throw new Error('Failed to fetch file'); } - const blobData = await response.blob(); - await putFile(uploadUrl, blobData as File); - return downloadUrl; - } catch (error: any) { - throw new Error('Failed to get oss url', error.message); + throw new Error(error.message); } } diff --git a/packages/components/src/Annotator/tools/base.ts b/packages/components/src/Annotator/tools/base.ts index a5a28bb..21ace95 100644 --- a/packages/components/src/Annotator/tools/base.ts +++ b/packages/components/src/Annotator/tools/base.ts @@ -1,6 +1,18 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import { drawRect } from '../utils/draw'; +import { CursorState } from 'ahooks/lib/useMouse'; +import { Updater } from 'use-immer'; + import { DisplayOption, EElementType, EObjectType } from '../constants'; +import { OnAiAnnotationFunc } from '../hooks/useActions'; +import { + Category, + DrawData, + EditState, + EObjectStatus, + IAnnotationObject, + ICreatingObject, + IPrompt, +} from '../type'; import { Direction, getAnchorFixRectPoint, @@ -14,24 +26,15 @@ import { resizeRect, setRectBetweenPixels, } from '../utils/compute'; -import { - Category, - DrawData, - EditState, - EObjectStatus, - IAnnotationObject, - ICreatingObject, - IPrompt, -} from '../type'; -import { CursorState } from 'ahooks/lib/useMouse'; -import { Updater } from 'use-immer'; -import { OnAiAnnotationFunc } from '../hooks/useActions'; -import useRectangle from './useRectangle'; -import usePolygon from './usePolygon'; -import useSkeleton from './useSkeleton'; +import { drawRect } from '../utils/draw'; + import useMask from './useMask'; import useMatting from './useMatting'; import usePoint from './usePoint'; +import usePolygon from './usePolygon'; +import usePolyline from './usePolyline'; +import useRectangle from './useRectangle'; +import useSkeleton from './useSkeleton'; export type RenderStyles = { strokeColor: string; @@ -151,6 +154,7 @@ export const useToolInstances = (props: ToolInstanceHookProps) => { const maskHooks = useMask(props); const mattingHooks = useMatting(props); const pointHooks = usePoint(props); + const polylineHooks = usePolyline(props); const objectHooksMap: Record = { [EObjectType.Rectangle]: rectangleHooks, @@ -159,7 +163,9 @@ export const useToolInstances = (props: ToolInstanceHookProps) => { [EObjectType.Mask]: maskHooks, [EObjectType.Matting]: mattingHooks, [EObjectType.Point]: pointHooks, + [EObjectType.Polyline]: polylineHooks, [EObjectType.Custom]: rectangleHooks, // todo + [EObjectType.Classification]: rectangleHooks, // todo }; return { diff --git a/packages/components/src/Annotator/tools/useMask.ts b/packages/components/src/Annotator/tools/useMask.ts index 9957e3f..d55ed1d 100644 --- a/packages/components/src/Annotator/tools/useMask.ts +++ b/packages/components/src/Annotator/tools/useMask.ts @@ -1,32 +1,13 @@ -import { - clearCanvas, - drawBooleanBrush, - drawBooleanPolygon, - drawCircleWithFill, - drawImage, - drawLine, - drawPath, - drawQuadraticPath, - drawRect, - shadeEverythingButRect, -} from '../utils/draw'; -import { EObjectType, ESubToolItem } from '../constants'; -import { - getRectFromPoints, - isInCanvas, - isPointOnPoint, - translatePointCoord, - translatePointZoom, - translatePolygonCoord, - translateRectCoord, -} from '../utils/compute'; -import { ToolInstanceHook, ToolHooksFunc, getPromptBoolean } from './base'; +import { cloneDeep } from 'lodash'; + +import { EnumModelType, EObjectType, ESubToolItem } from '../constants'; import { ANNO_FILL_COLOR, ANNO_MASK_ALPHA, ANNO_STROKE_ALPHA, ANNO_STROKE_COLOR, PROMPT_FILL_COLOR, + PROMPT_STROKE_COLOR, } from '../constants/render'; import { EPromptType, @@ -35,51 +16,140 @@ import { PromptItem, } from '../type'; import { hexToRgbArray, hexToRgba } from '../utils/color'; -import { cloneDeep } from 'lodash'; +import { + getRectFromPoints, + isInCanvas, + isPointOnPoint, + translatePointCoord, + translatePointZoom, + translatePolygonCoord, + translateRectCoord, +} from '../utils/compute'; +import { + clearCanvas, + drawBooleanBrush, + drawBooleanPolygon, + drawCircleWithFill, + drawImage, + drawLine, + drawPath, + drawQuadraticPath, + drawRect, + shadeEverythingButRect, +} from '../utils/draw'; + +import { ToolInstanceHook, ToolHooksFunc, getPromptBoolean } from './base'; + +export const encodeRleString = (rleArr: number[]): string => { + const m = rleArr.length; + let p = 0; + let x = 0; + let more = true; + const s = Array(m * 6); + + for (let i = 0; i < m; i++) { + x = rleArr[i]; + if (i > 2) { + x -= rleArr[i - 2]; + } + more = true; + + while (more) { + let c = x & 0x1f; + x >>= 5; + more = !!((x !== -1 && c & 0x10) || (x !== 0 && !(c & 0x10))); + if (more) { + c |= 0x20; + } + c += 48; + s[p] = String.fromCharCode(c); + p += 1; + } + } + return s.join(''); +}; + +export const decodeRleString = (s: string): number[] => { + let p = 0; + const cnts = []; + + while (p < s.length && s[p]) { + let x = 0; + let k = 0; + let more = 1; + + while (more) { + const c = s.charCodeAt(p) - 48; + x |= (c & 0x1f) << (5 * k); + more = c & 0x20; + p += 1; + k += 1; + + if (!more && c & 0x10) { + x |= -1 << (5 * k); + } + } + + if (cnts.length > 2) { + x += cnts[cnts.length - 2]; + } + cnts.push(x); + } + return cnts; +}; /** * only [0,1] array with rle decode * example: - * [2,3,8,1,....] to [0,0,1,1,1,0,0,0,1,0,....] + * [2,3,4,1,....] to [0,0,1,1,1,0,0,0,0,1....] */ -const decodeRle = (arr: number[], length: number) => { - const result = new Array(length).fill(0); - for (let i = 0; i < arr.length; i += 2) { - const spliceLen = Math.min(arr[i + 1], length - arr[i]); - for (let j = 0; j < spliceLen; j++) { - result[arr[i] + j] = 1; +export const decodeRleArray = (rle: number[], length: number) => { + let mask = new Array(length); + let pixel = 0; + let bit = 0; + + for (let i = 0; i < rle.length; i++) { + const count = rle[i]; + for (let j = 0; j < count; j++) { + mask[pixel] = bit; + pixel++; } + bit = 1 - bit; } - return result; + return mask; }; /** * only [0,1] array with rle encode * example: - * [0,0,1,1,1,0,0,0,1,0,....] to [2,3,8,1,....] + * [0,0,1,1,1,0,0,0,0,1....] to [2,3,4,1,....] */ -const encodeRle = (arr: number[]) => { - const result = []; - let curLen = 0; - let len = arr.length; - for (let i = 0; i < len; i++) { - const value = arr[i]; - if (curLen !== 0) { - if (value === 1) { - curLen++; - } else { - result.push(curLen); - curLen = 0; - } - } else if (value === 1) { - result.push(i); - curLen = 1; +export const encodeRleArray = (mask: number[]) => { + const rle: number[] = []; + let count = 0; + let prevBit = mask[0]; + + for (let i = 0; i < mask.length; i++) { + const bit = mask[i]; + if (bit === prevBit) { + count++; + } else { + rle.push(count); + count = 1; + prevBit = bit; } } - if (curLen !== 0) { - result.push(curLen); - } - return result; + rle.push(count); + + return rle; +}; + +const decodeRle = (rleStr: string, length: number) => { + return decodeRleArray(decodeRleString(rleStr), length); +}; + +const encodeRle = (mask: number[]) => { + return encodeRleString(encodeRleArray(mask)); }; export const renderMaskSteps = ( @@ -342,15 +412,10 @@ export const objectToRle = ( maskAplha; } - // @thi.ng/rle-pack encode - // const arr = encode(maskData.data, maskData.data.length); - // return maskPixelCount > 0 ? Array.from(arr) : []; - - // console.log('>>>> output', encodeRle(arr)); - return maskPixelCount > 0 ? encodeRle(arr) : []; + return maskPixelCount > 0 ? encodeRle(arr) : ''; }; -export const rleToCanvas = (rle: number[], size: ISize, color: string) => { +export const rleToCanvas = (rle: string, size: ISize, color: string) => { const { width, height } = size; const canvas = document.createElement('canvas'); @@ -364,18 +429,6 @@ export const rleToCanvas = (rle: number[], size: ISize, color: string) => { const newdata = ctx.createImageData(width, height); const rgb = hexToRgbArray(color); - // @thi.ng/rle-pack decode - // const decoded = decode(rle as unknown as Uint8Array); - // newdata.data.set(decoded, 0); - // for (let i = newdata.data.length / 4; i--; ) { - // if (newdata.data[i * 4 + 3] > 0) { - // newdata.data[i * 4] = rgb[0]; - // newdata.data[i * 4 + 1] = rgb[1]; - // newdata.data[i * 4 + 2] = rgb[2]; - // newdata.data[i * 4 + 3] = 255; - // } - // } - // custom rle decode const maskArr = decodeRle(rle, Math.ceil(width) * Math.ceil(height)); for (let i = newdata.data.length / 4; i--; ) { @@ -466,123 +519,229 @@ const useMask: ToolInstanceHook = ({ }; const renderPrompt: ToolHooksFunc.RenderPrompt = ({ prompt }) => { + const model = drawData.selectedModel[drawData.selectedTool]; + // draw creating prompt if (prompt.creatingPrompt) { - const strokeColor = ANNO_STROKE_COLOR.CREATING; - const fillColor = ANNO_FILL_COLOR.CREATING; - switch (prompt.creatingPrompt.type) { - case EPromptType.Rect: { - const { startPoint } = prompt.creatingPrompt; - const rect = getRectFromPoints( - startPoint!, - { - x: contentMouse.elementX, - y: contentMouse.elementY, - }, - { - width: contentMouse.elementW, - height: contentMouse.elementH, - }, - ); - const canvasCoordRect = translateRectCoord(rect, { - x: -imagePos.current.x, - y: -imagePos.current.y, - }); - drawRect( - activeCanvasRef.current, - canvasCoordRect, - strokeColor, - 2, - [0], - fillColor, - ); - break; - } - case EPromptType.Point: { - if (!prompt.creatingPrompt.point) break; - const canvasCoordPoint = translatePointCoord( - prompt.creatingPrompt.point, - { + if (model === EnumModelType.IVP) { + const strokeColor = prompt.creatingPrompt.isPositive + ? PROMPT_STROKE_COLOR.POSITIVE + : PROMPT_STROKE_COLOR.NEGATIVE; + const fillColor = prompt.creatingPrompt.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE; + + switch (prompt.creatingPrompt.type) { + case EPromptType.Rect: { + const { startPoint } = prompt.creatingPrompt; + const rect = getRectFromPoints( + startPoint!, + { + x: contentMouse.elementX, + y: contentMouse.elementY, + }, + { + width: contentMouse.elementW, + height: contentMouse.elementH, + }, + ); + const canvasCoordRect = translateRectCoord(rect, { x: -imagePos.current.x, y: -imagePos.current.y, - }, - ); - drawCircleWithFill( - activeCanvasRef.current!, - canvasCoordPoint, - 4, - prompt.creatingPrompt.isPositive - ? PROMPT_FILL_COLOR.POSITIVE - : PROMPT_FILL_COLOR.NEGATIVE, - 2, - '#fff', - ); + }); + drawRect( + activeCanvasRef.current, + canvasCoordRect, + strokeColor, + 2, + [0], + fillColor, + ); + break; + } + case EPromptType.Point: { + if (!prompt.creatingPrompt.point) break; + const canvasCoordPoint = translatePointCoord( + prompt.creatingPrompt.point, + { + x: -imagePos.current.x, + y: -imagePos.current.y, + }, + ); + drawCircleWithFill( + activeCanvasRef.current!, + canvasCoordPoint, + 4, + prompt.creatingPrompt.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE, + 2, + '#fff', + ); + } + default: + break; } - case EPromptType.EdgeStitch: - case EPromptType.Stroke: { - if (!prompt.creatingPrompt.stroke || !prompt.creatingPrompt.radius) + } else { + const strokeColor = ANNO_STROKE_COLOR.CREATING; + const fillColor = ANNO_FILL_COLOR.CREATING; + switch (prompt.creatingPrompt.type) { + case EPromptType.Rect: { + const { startPoint } = prompt.creatingPrompt; + const rect = getRectFromPoints( + startPoint!, + { + x: contentMouse.elementX, + y: contentMouse.elementY, + }, + { + width: contentMouse.elementW, + height: contentMouse.elementH, + }, + ); + const canvasCoordRect = translateRectCoord(rect, { + x: -imagePos.current.x, + y: -imagePos.current.y, + }); + drawRect( + activeCanvasRef.current, + canvasCoordRect, + strokeColor, + 2, + [0], + fillColor, + ); + break; + } + case EPromptType.Point: { + if (!prompt.creatingPrompt.point) break; + const canvasCoordPoint = translatePointCoord( + prompt.creatingPrompt.point, + { + x: -imagePos.current.x, + y: -imagePos.current.y, + }, + ); + drawCircleWithFill( + activeCanvasRef.current!, + canvasCoordPoint, + 4, + prompt.creatingPrompt.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE, + 2, + '#fff', + ); + } + case EPromptType.EdgeStitch: + case EPromptType.Stroke: { + if (!prompt.creatingPrompt.stroke || !prompt.creatingPrompt.radius) + break; + const canvasCoordStroke = translatePolygonCoord( + prompt.creatingPrompt.stroke, + { + x: -imagePos.current.x, + y: -imagePos.current.y, + }, + ); + const radius = + (prompt.creatingPrompt.radius * clientSize.width) / + naturalSize.width; + const color = + prompt.creatingPrompt.type === EPromptType.EdgeStitch + ? hexToRgba(strokeColor, ANNO_MASK_ALPHA.CREATING) + : prompt.creatingPrompt.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE; + drawQuadraticPath( + activeCanvasRef.current!, + canvasCoordStroke, + color, + radius, + ); break; - const canvasCoordStroke = translatePolygonCoord( - prompt.creatingPrompt.stroke, + } + default: + break; + } + + // draw active area while loading ai annotations + if (editState.isRequiring && prompt.activeRectWhileLoading) { + const canvasCoordRect = translateRectCoord( + prompt.activeRectWhileLoading, { x: -imagePos.current.x, y: -imagePos.current.y, }, ); - const radius = - (prompt.creatingPrompt.radius * clientSize.width) / - naturalSize.width; - const color = - prompt.creatingPrompt.type === EPromptType.EdgeStitch - ? hexToRgba(strokeColor, ANNO_MASK_ALPHA.CREATING) - : prompt.creatingPrompt.isPositive - ? PROMPT_FILL_COLOR.POSITIVE - : PROMPT_FILL_COLOR.NEGATIVE; - drawQuadraticPath( - activeCanvasRef.current!, - canvasCoordStroke, - color, - radius, - ); - break; + shadeEverythingButRect(activeCanvasRef.current!, canvasCoordRect); } - default: - break; - } - - // draw active area while loading ai annotations - if (editState.isRequiring && prompt.activeRectWhileLoading) { - const canvasCoordRect = translateRectCoord( - prompt.activeRectWhileLoading, - { - x: -imagePos.current.x, - y: -imagePos.current.y, - }, - ); - shadeEverythingButRect(activeCanvasRef.current!, canvasCoordRect); } } // draw existing prompts if (prompt.promptsQueue) { - prompt.promptsQueue.forEach((item) => { - if (item.type === EPromptType.Point) { - const canvasCoordPoint = translatePointCoord(item.point!, { - x: -imagePos.current.x, - y: -imagePos.current.y, - }); - drawCircleWithFill( - activeCanvasRef.current!, - canvasCoordPoint, - 4, - item.isPositive - ? PROMPT_FILL_COLOR.POSITIVE - : PROMPT_FILL_COLOR.NEGATIVE, - 2, - '#fff', - ); - } - }); + if (model === EnumModelType.IVP) { + prompt.promptsQueue.forEach((item) => { + switch (item.type) { + case EPromptType.Rect: { + const canvasCoordRect = translateRectCoord(item.rect!, { + x: -imagePos.current.x, + y: -imagePos.current.y, + }); + drawRect( + activeCanvasRef.current, + canvasCoordRect, + item.isPositive + ? PROMPT_STROKE_COLOR.POSITIVE + : PROMPT_STROKE_COLOR.NEGATIVE, + 2, + [0], + item.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE, + ); + break; + } + case EPromptType.Point: { + const canvasCoordPoint = translatePointCoord(item.point!, { + x: -imagePos.current.x, + y: -imagePos.current.y, + }); + drawCircleWithFill( + activeCanvasRef.current!, + canvasCoordPoint, + 4, + item.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE, + 2, + '#fff', + ); + break; + } + } + }); + } else { + prompt.promptsQueue.forEach((item) => { + if (item.type === EPromptType.Point) { + const canvasCoordPoint = translatePointCoord(item.point!, { + x: -imagePos.current.x, + y: -imagePos.current.y, + }); + drawCircleWithFill( + activeCanvasRef.current!, + canvasCoordPoint, + 4, + item.isPositive + ? PROMPT_FILL_COLOR.POSITIVE + : PROMPT_FILL_COLOR.NEGATIVE, + 2, + '#fff', + ); + } + }); + } } }; @@ -740,6 +899,15 @@ const useMask: ToolInstanceHook = ({ isPositive: true, }; break; + case ESubToolItem.PositiveVisualPrompt: + case ESubToolItem.NegativeVisualPrompt: + s.prompt.creatingPrompt = { + type: EPromptType.Rect, + startPoint: point, + point, + isPositive: + s.selectedSubTool !== ESubToolItem.NegativeVisualPrompt, + }; default: break; } @@ -812,6 +980,7 @@ const useMask: ToolInstanceHook = ({ x: contentMouse.elementX, y: contentMouse.elementY, }; + const model = drawData.selectedModel[drawData.selectedTool]; switch (drawData.selectedSubTool) { case ESubToolItem.BrushAdd: case ESubToolItem.BrushErase: @@ -912,6 +1081,38 @@ const useMask: ToolInstanceHook = ({ onAiAnnotation?.({ type: EObjectType.Mask, drawData }); break; } + case ESubToolItem.PositiveVisualPrompt: + case ESubToolItem.NegativeVisualPrompt: { + if ( + model !== EnumModelType.IVP || + !drawData.prompt.creatingPrompt?.startPoint + ) + break; + const { startPoint } = drawData.prompt.creatingPrompt; + if (mouse.x === startPoint.x || mouse.y === startPoint.y) { + setDrawData((s) => (s.prompt.creatingPrompt = undefined)); + break; + } else { + const rect = getRectFromPoints(startPoint, mouse, { + width: contentMouse.elementW, + height: contentMouse.elementH, + }); + const promptItem: PromptItem = { + type: EPromptType.Rect, + isPositive: drawData.prompt.creatingPrompt.isPositive, + rect, + }; + const promptsQueue = [ + ...(drawData.prompt.promptsQueue || []), + promptItem, + ]; + onAiAnnotation?.({ + type: EObjectType.Mask, + drawData, + promptsQueue, + }); + } + } } }; diff --git a/packages/components/src/Annotator/tools/useMatting.ts b/packages/components/src/Annotator/tools/useMatting.ts index 88a3516..d5055a1 100644 --- a/packages/components/src/Annotator/tools/useMatting.ts +++ b/packages/components/src/Annotator/tools/useMatting.ts @@ -1,4 +1,5 @@ import { clearCanvas, drawImage, drawRectWithFill } from '../utils/draw'; + import { ToolInstanceHook, ToolHooksFunc } from './base'; const useMatting: ToolInstanceHook = ({ diff --git a/packages/components/src/Annotator/tools/usePoint.ts b/packages/components/src/Annotator/tools/usePoint.ts index af131a8..ff64d6a 100644 --- a/packages/components/src/Annotator/tools/usePoint.ts +++ b/packages/components/src/Annotator/tools/usePoint.ts @@ -1,4 +1,5 @@ import { drawCircleWithFill } from '../utils/draw'; + import { ToolInstanceHook, ToolHooksFunc } from './base'; const usePoint: ToolInstanceHook = ({ canvasRef }) => { diff --git a/packages/components/src/Annotator/tools/usePolygon.ts b/packages/components/src/Annotator/tools/usePolygon.ts index bcc9a12..8bb0ae3 100644 --- a/packages/components/src/Annotator/tools/usePolygon.ts +++ b/packages/components/src/Annotator/tools/usePolygon.ts @@ -1,12 +1,15 @@ -import { - drawCircleWithFill, - drawLine, - drawPolygonWithFill, - drawQuadraticPath, - drawRect, - shadeEverythingButRect, -} from '../utils/draw'; +import { cloneDeep } from 'lodash'; + import { EElementType, EObjectType, ESubToolItem } from '../constants'; +import { + ANNO_FILL_ALPHA, + ANNO_FILL_COLOR, + ANNO_STROKE_ALPHA, + ANNO_STROKE_COLOR, + PROMPT_FILL_COLOR, +} from '../constants/render'; +import { EPromptType, PromptItem } from '../type'; +import { hexToRgba } from '../utils/color'; import { getClosestPointOnLineSegment, getLinesFromPolygon, @@ -20,22 +23,21 @@ import { translatePolygonCoord, translateRectCoord, } from '../utils/compute'; +import { + drawCircleWithFill, + drawLine, + drawPolygonWithFill, + drawQuadraticPath, + drawRect, + shadeEverythingButRect, +} from '../utils/draw'; + import { ToolInstanceHook, ToolHooksFunc, editBaseElementWhenMouseDown, getPromptBoolean, } from './base'; -import { hexToRgba } from '../utils/color'; -import { - ANNO_FILL_ALPHA, - ANNO_FILL_COLOR, - ANNO_STROKE_ALPHA, - ANNO_STROKE_COLOR, - PROMPT_FILL_COLOR, -} from '../constants/render'; -import { cloneDeep } from 'lodash'; -import { EPromptType, PromptItem } from '../type'; const usePolygon: ToolInstanceHook = ({ editState, diff --git a/packages/components/src/Annotator/tools/usePolyline.ts b/packages/components/src/Annotator/tools/usePolyline.ts new file mode 100644 index 0000000..4be8182 --- /dev/null +++ b/packages/components/src/Annotator/tools/usePolyline.ts @@ -0,0 +1,87 @@ +import { ANNO_STROKE_ALPHA } from '../constants/render'; +import { LineType } from '../type'; +import { hexToRgba } from '../utils/color'; +import { drawPolylineByType } from '../utils/draw'; + +import { ToolInstanceHook, ToolHooksFunc } from './base'; + +const usePolyline: ToolInstanceHook = ({ canvasRef }) => { + const renderObject: ToolHooksFunc.RenderObject = ({ + object, + color, + isFocus, + }) => { + const { polyline } = object; + if (polyline && polyline.visible && polyline.lineType) { + const lineType: LineType = polyline.lineType as unknown as LineType; + + const baseColor = polyline.color || color; + const strokeColor = isFocus + ? hexToRgba(baseColor, ANNO_STROKE_ALPHA.FOCUS) + : hexToRgba(baseColor, ANNO_STROKE_ALPHA.DEFAULT); + + polyline?.group.forEach((anchors) => { + drawPolylineByType(canvasRef.current, anchors, strokeColor, lineType); + }); + + // todo render point & text + } + }; + + const renderCreatingObject: ToolHooksFunc.RenderCreatingObject = () => { + // todo + }; + + const renderEditingObject: ToolHooksFunc.RenderEditingObject = () => { + // to do + }; + + const renderPrompt: ToolHooksFunc.RenderPrompt = () => { + // nothing in rect + }; + + const startEditingWhenMouseDown: ToolHooksFunc.StartEditingWhenMouseDown = + () => { + return false; + }; + + const startCreatingWhenMouseDown: ToolHooksFunc.StartCreatingWhenMouseDown = + () => { + return false; + }; + + const updateEditingWhenMouseMove: ToolHooksFunc.UpdateEditingWhenMouseMove = + () => { + return false; + }; + + const updateCreatingWhenMouseMove: ToolHooksFunc.UpdateCreatingWhenMouseMove = + () => { + return false; + }; + + const finishEditingWhenMouseUp: ToolHooksFunc.FinishEditingWhenMouseUp = + () => { + return false; + }; + + const finishCreatingWhenMouseUp: ToolHooksFunc.FinishCreatingWhenMouseUp = + () => { + return false; + }; + + return { + renderObject, + renderCreatingObject, + renderEditingObject, + renderPrompt, + startEditingWhenMouseDown, + startCreatingWhenMouseDown, + updateEditingWhenMouseMove, + updateCreatingWhenMouseMove, + finishEditingWhenMouseUp, + finishCreatingWhenMouseUp, + }; +}; + +export default usePolyline; diff --git a/packages/components/src/Annotator/tools/useRectangle.ts b/packages/components/src/Annotator/tools/useRectangle.ts index 270ce43..d0579f5 100644 --- a/packages/components/src/Annotator/tools/useRectangle.ts +++ b/packages/components/src/Annotator/tools/useRectangle.ts @@ -1,15 +1,23 @@ -import { - drawCircleWithFill, - drawRect, - drawText, - shadeEverythingButRect, -} from '../utils/draw'; import { EnumModelType, EObjectType, ESubToolItem } from '../constants'; +import { + ANNO_FILL_ALPHA, + PROMPT_FILL_COLOR, + PROMPT_STROKE_COLOR, +} from '../constants/render'; +import { EObjectStatus, EPromptType, PromptItem } from '../type'; +import { hexToRgba } from '../utils/color'; import { getRectFromPoints, translatePointCoord, translateRectCoord, } from '../utils/compute'; +import { + drawCircleWithFill, + drawRect, + drawText, + shadeEverythingButRect, +} from '../utils/draw'; + import { ToolInstanceHook, ToolHooksFunc, @@ -17,13 +25,6 @@ import { editBaseElementWhenMouseDown, updateEditingRectWhenMouseMove, } from './base'; -import { EObjectStatus, EPromptType, PromptItem } from '../type'; -import { hexToRgba } from '../utils/color'; -import { - ANNO_FILL_ALPHA, - PROMPT_FILL_COLOR, - PROMPT_STROKE_COLOR, -} from '../constants/render'; const useRectangle: ToolInstanceHook = ({ contentMouse, @@ -54,17 +55,14 @@ const useRectangle: ToolInstanceHook = ({ let strokeColor = styles.strokeColor; let fillColor = styles.fillColor; let thickness = styles.thickness; + const model = drawData.selectedModel[drawData.selectedTool]; if (drawData.isBatchEditing) { if ( object.status === EObjectStatus.Unchecked && - (!editState.isCtrlPressed || - drawData.selectedModel === EnumModelType.IVP) + (!editState.isCtrlPressed || model === EnumModelType.IVP) ) return; - if ( - editState.isCtrlPressed && - drawData.selectedModel === EnumModelType.Detection - ) { + if (editState.isCtrlPressed && model === EnumModelType.Detection) { if (object.status !== EObjectStatus.Unchecked) { strokeColor = hexToRgba(color, 0.8); strokeDash = [2]; @@ -92,15 +90,15 @@ const useRectangle: ToolInstanceHook = ({ categories.find((c) => c.id === object.labelId)?.name || ''; const label = object?.conf && object.conf > 0 && object.conf < 1 - ? `${labelName}(${object.conf.toFixed(3)})` + ? `${labelName} (${object.conf.toFixed(3)})` : labelName; drawText( canvasRef.current!, label || '', 13, - { x: rect.x + 2, y: rect.y + 2 }, + { x: rect.x + 6, y: rect.y + 6 }, color, - false, + true, 'left', ); } @@ -292,7 +290,8 @@ const useRectangle: ToolInstanceHook = ({ const startCreatingWhenMouseDown: ToolHooksFunc.StartCreatingWhenMouseDown = ({ point, basic }) => { setDrawData((s) => { - if (s.AIAnnotation && s.selectedModel === EnumModelType.IVP) { + const model = s.selectedModel[s.selectedTool]; + if (s.AIAnnotation && model === EnumModelType.IVP) { s.prompt.creatingPrompt = { type: EPromptType.Rect, startPoint: point, @@ -352,7 +351,7 @@ const useRectangle: ToolInstanceHook = ({ }; if ( drawData.AIAnnotation && - drawData.selectedModel === EnumModelType.IVP && + drawData.selectedModel[drawData.selectedTool] === EnumModelType.IVP && drawData.prompt.creatingPrompt?.startPoint ) { const { startPoint } = drawData.prompt.creatingPrompt; @@ -379,14 +378,10 @@ const useRectangle: ToolInstanceHook = ({ // }); // return true; } else { - const rect = getRectFromPoints( - drawData.prompt.creatingPrompt.startPoint as IPoint, - mouse, - { - width: contentMouse.elementW, - height: contentMouse.elementH, - }, - ); + const rect = getRectFromPoints(startPoint, mouse, { + width: contentMouse.elementW, + height: contentMouse.elementH, + }); const promptItem: PromptItem = { type: EPromptType.Rect, isPositive: drawData.prompt.creatingPrompt.isPositive, diff --git a/packages/components/src/Annotator/tools/useSkeleton.ts b/packages/components/src/Annotator/tools/useSkeleton.ts index e1cf86b..2a586e2 100644 --- a/packages/components/src/Annotator/tools/useSkeleton.ts +++ b/packages/components/src/Annotator/tools/useSkeleton.ts @@ -1,10 +1,10 @@ -import { drawCircleWithFill, drawLine, drawRect } from '../utils/draw'; import { BODY_TEMPLATE, EElementType, EObjectType, KEYPOINTS_VISIBLE_TYPE, } from '../constants'; +import { EObjectStatus } from '../type'; import { getKeypointsFromRect, getRectFromPoints, @@ -12,6 +12,8 @@ import { translatePointsToPointObjs, translateRectCoord, } from '../utils/compute'; +import { drawCircleWithFill, drawLine, drawRect } from '../utils/draw'; + import { ToolInstanceHook, ToolHooksFunc, @@ -20,7 +22,6 @@ import { updateEditingRectWhenMouseMove, RenderStyles, } from './base'; -import { EObjectStatus } from '../type'; const renderKeypoints = ( canvas: HTMLCanvasElement, diff --git a/packages/components/src/Annotator/type.ts b/packages/components/src/Annotator/type.ts index a66dfaa..7bae8ed 100644 --- a/packages/components/src/Annotator/type.ts +++ b/packages/components/src/Annotator/type.ts @@ -24,6 +24,11 @@ export interface IAttribute { export type IAttributeValue = string | number | number[] | null; +export interface IMask { + counts: string; // mask rle string + size: [number, number]; // [height, width] +} + export interface Category { id: string; name: string; @@ -58,9 +63,13 @@ export interface BaseObject { /** Keypoint connection. [start point index, end point index, ...] */ lines?: number[]; /** mask */ - mask?: number[]; + mask?: IMask; /** point */ point?: number[]; + /** polyline */ + polyline?: [number[], number[]]; // [[x1, x2, x3, ...], [y1, y2, y3, ...]] + lineColor?: string; + lineType?: string; } export interface DrawObject extends BaseObject { @@ -104,7 +113,8 @@ export interface IAnnotationObject { lines: number[]; }; point?: IElement; - maskRle?: number[]; + polyline?: IElement; + maskRle?: string; maskCanvasElement?: any; alpha?: string; alphaImageElement?: any; @@ -142,7 +152,7 @@ export enum EPromptType { Modify = 'modify', } -export type PromptItem = { +export interface PromptItem { type: EPromptType; isPositive: boolean; /** Rect */ @@ -155,7 +165,17 @@ export type PromptItem = { radius?: number; /** Modify */ polygons?: number[][]; -}; +} + +export interface ReqPromptItem { + type: string; + isPositive?: boolean; + point?: number[]; + rect?: number[]; + stroke?: number[]; + radius?: number; + polygons?: number[][]; +} export interface IPrompt { creatingPrompt?: PromptItem; @@ -181,7 +201,7 @@ export interface DrawData { selectedTool: EToolType; selectedSubTool: ESubToolItem; AIAnnotation: boolean; - selectedModel?: EnumModelType; + selectedModel: Record; brushSize: number; pointResolution: number; @@ -257,7 +277,13 @@ export const DEFAULT_DRAW_DATA: DrawData = { /** Selected tool */ selectedTool: EBasicToolItem.Drag, selectedSubTool: ESubToolItem.PenAdd, - selectedModel: undefined, + selectedModel: { + [EBasicToolItem.Drag]: undefined, + [EBasicToolItem.Rectangle]: undefined, + [EBasicToolItem.Mask]: undefined, + [EBasicToolItem.Skeleton]: EnumModelType.Pose, + [EBasicToolItem.Polygon]: EnumModelType.SegmentByPolygon, + }, AIAnnotation: false, /** drawed */ @@ -306,3 +332,15 @@ export const DEFAULT_EDIT_STATE: EditState = { imageDisplayOptions: DEFAULT_IMG_DISPLAY_OPTIONS, annotsDisplayOptions: DEFAULT_ANNOTS_DISPLAY_OPTIONS, }; + +export enum LineType { + Solid = 'solid', + Dashed = 'dash', + DoubleSolid = 'double_solid', + DoubleDashed = 'double_dash', + LDashedRSolid = 'left_dash-right_solid', + LSolidRDashed = 'left_solid-right_dash', + LCurbside = 'left_curbside', + RCurbside = 'right_curbside', + Unknown = 'none', +} diff --git a/packages/components/src/Annotator/utils/base64.ts b/packages/components/src/Annotator/utils/base64.ts index 22a3320..f40334c 100644 --- a/packages/components/src/Annotator/utils/base64.ts +++ b/packages/components/src/Annotator/utils/base64.ts @@ -47,6 +47,13 @@ export const isHttpsUrl = (str: string) => { return httpsRegex.test(str); }; +export const getServerAddressableUrl = async (url: string) => { + if (isBlobUrl(url)) { + return await getImageBase64(url); + } + return url; +}; + export const getImgBase64ByBlob = (blobUrl: Blob) => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); diff --git a/packages/components/src/Annotator/utils/color.ts b/packages/components/src/Annotator/utils/color.ts index 3f696b1..eb0adde 100644 --- a/packages/components/src/Annotator/utils/color.ts +++ b/packages/components/src/Annotator/utils/color.ts @@ -43,12 +43,7 @@ export const hexToRgba = (hex: string, opacity = 1) => { )},${op})`; }; -/** - * Generate a color list based on the number of categories. - * max random 1000 - * @param count - * @returns - */ +/** Generate a color list based on the number of categories. */ export const createColorList = (count: number) => { const colors = [ '#FFFF00', @@ -80,7 +75,7 @@ export const createColorList = (count: number) => { .padStart(2, '0')}${rgb[2] .toString(16) .padStart(2, '0')}`.toUpperCase(); - if (count > 1000 || !colors.includes(hexColor)) { + if (!colors.includes(hexColor)) { colors.push(hexColor); } } @@ -92,10 +87,10 @@ export const getCategoryColors = (list: string[]) => { if (!list.length) return {}; const sortList = [...list]; - const colors = createColorList(sortList.length) ; + const colors = createColorList(sortList.length); const result: Record = {}; sortList.forEach((item, index) => { - result[item] = colors[index] || '#fff'; + result[item] = colors[index]; }); return result; }; diff --git a/packages/components/src/Annotator/utils/compute.ts b/packages/components/src/Annotator/utils/compute.ts index 1493390..48d5eca 100644 --- a/packages/components/src/Annotator/utils/compute.ts +++ b/packages/components/src/Annotator/utils/compute.ts @@ -1,12 +1,16 @@ +import { CursorState } from 'ahooks/lib/useMouse'; +import { cloneDeep, isEqual, isNumber } from 'lodash'; + import { EElementType, EObjectType, KEYPOINTS_VISIBLE_TYPE, } from '../constants'; +import { LINE_COLOR } from '../constants/render'; +import { rleToCanvas } from '../tools/useMask'; import { DrawData, IAnnotationObject, PromptItem } from '../type'; -import { CursorState } from 'ahooks/lib/useMouse'; + import { rgbArrayToRgba, rgbaToRgbArray } from './color'; -import { cloneDeep, isEqual, isNumber } from 'lodash'; /** * Calculate the scaled width and height. @@ -1210,6 +1214,9 @@ export const getObjectType = (obj: IAnnotationObject): EObjectType => { if (obj.rect && isValidRect(obj.rect)) { return EObjectType.Rectangle; } + if (obj.polyline) { + return EObjectType.Polyline; + } return EObjectType.Custom; }; @@ -1416,7 +1423,7 @@ export const translateAnnotCoord = ( annoObj: IAnnotationObject, newCoordOrigin: IPoint, ): IAnnotationObject => { - const { rect, polygon, keypoints, point } = annoObj; + const { rect, polygon, keypoints, point, polyline } = annoObj; const newAnnoObj = { ...annoObj }; if (rect) { @@ -1456,6 +1463,16 @@ export const translateAnnotCoord = ( }; } + if (polyline) { + const newGroup = polyline.group.map((lineItem) => { + return translatePolygonCoord(lineItem, newCoordOrigin); + }); + newAnnoObj.polyline = { + ...polyline, + group: newGroup, + }; + } + return newAnnoObj; }; @@ -1497,6 +1514,14 @@ export const scaleObject = ( const newPoint = translatePointZoom(newObj.point, preSize, curSize); newObj.point = { ...newObj.point, ...newPoint }; } + if (newObj.polyline) { + const newGroups = newObj.polyline.group.map((polyline) => { + return polyline.map((point) => { + return translatePointZoom(point, preSize, curSize); + }); + }); + newObj.polyline = { ...newObj.polyline, group: newGroups }; + } return newObj; }; const scalePromptItem = ( @@ -1656,6 +1681,7 @@ export const convertFrameObjectsIntoFramesObjects = ( framesObjects: IAnnotationObject[][], frameCount: number, activeIndex: number, + naturalSize: ISize, ) => { const tempObjects = [...framesObjects]; currFrameObjects.forEach((item, objectIdx) => { @@ -1665,11 +1691,25 @@ export const convertFrameObjectsIntoFramesObjects = ( if (frameIdx === activeIndex) { return item; } + const frameEmpty = obj?.frameEmpty || Boolean(!obj); let resultObject = obj; - if (frameIdx > activeIndex) { - // frame change to after active frame - resultObject = isEqual(obj, objectframes[activeIndex]) ? item : obj; + if (frameIdx > activeIndex && isEqual(obj, objectframes[activeIndex])) { + // [active frame, later changed frame] -> same change + resultObject = item; + } else if ( + !frameEmpty && + item.type === EObjectType.Mask && + resultObject.maskRle && + item.labelId !== obj.labelId + ) { + // mask label changed => re compute maskCanvas + resultObject.maskCanvasElement = rleToCanvas( + resultObject.maskRle, + naturalSize, + item.color, + ); } + return { ...resultObject, type: item.type, @@ -1686,6 +1726,23 @@ export const convertFrameObjectsIntoFramesObjects = ( return tempObjects; }; +export const updateDrawDataFrameCreateObject = (newDrawData: DrawData) => { + // TODO: creating object should reset alone + if ( + newDrawData.activeObjectIndex >= 0 && + newDrawData.objectList?.[newDrawData.activeObjectIndex] && + !newDrawData.objectList?.[newDrawData.activeObjectIndex].frameEmpty + ) { + newDrawData.creatingObject = { + ...newDrawData.creatingObject, + ...newDrawData.objectList?.[newDrawData.activeObjectIndex], + }; + return; + } + newDrawData.prompt = {}; + newDrawData.creatingObject = undefined; +}; + export const getVisibleAreaForImage = ( imagePos: IPoint, clientSize: ISize, @@ -1779,3 +1836,33 @@ export const getMaskInfoByCanvas = ( bbox, }; }; + +export const translateUVtoPolylinePoints = ( + uv: [number[], number[]], +): IPolyline => { + if (!uv || uv.length < 2 || uv[0]?.length !== uv[1]?.length) { + return []; + } + + const [xCoords, yCoords] = uv; + const polyline: IPolyline = []; + + for (let i = 0; i < xCoords.length; i++) { + polyline.push({ + x: xCoords[i], + y: yCoords[i], + }); + } + + return polyline; +}; + +export const convertLaneLineColorToHex = (lineColor: string): string => { + if (lineColor === 'yellow') { + return LINE_COLOR.Yellow; + } else if (lineColor === 'white') { + return LINE_COLOR.White; + } else { + return LINE_COLOR.Other; + } +}; diff --git a/packages/components/src/Annotator/utils/draw.ts b/packages/components/src/Annotator/utils/draw.ts index b680d23..813429d 100644 --- a/packages/components/src/Annotator/utils/draw.ts +++ b/packages/components/src/Annotator/utils/draw.ts @@ -1,3 +1,6 @@ +import { LINE_STYLE_MAP } from '../constants/render'; +import { LineType } from '../type'; + import { hexToRgba } from './color'; function deg2rad(angleDeg: number) { @@ -268,6 +271,118 @@ export function drawPolygon( ctx.restore(); } +// function calculateOffsetPoints( +// anchors: IPoint[], +// offset: IPoint, +// ): [IPoint[], IPoint[]] { +// const calculateOffset = (point: IPoint, angle: number, side: number) => ({ +// x: point.x + side * offset.x * Math.cos(angle), +// y: point.y + side * offset.y * Math.sin(angle), +// }); + +// let leftOffsetPoints: IPoint[] = []; +// let rightOffsetPoints: IPoint[] = []; + +// for (let i = 0; i < anchors.length - 1; i++) { +// const angle = +// Math.atan2( +// anchors[i + 1].y - anchors[i].y, +// anchors[i + 1].x - anchors[i].x, +// ) + +// Math.PI / 2; +// leftOffsetPoints.push(calculateOffset(anchors[i], angle, 1)); +// rightOffsetPoints.push(calculateOffset(anchors[i], angle, -1)); +// } + +// // 处理最后一个点 +// const lastAngle = +// Math.atan2( +// anchors[anchors.length - 1].y - anchors[anchors.length - 2].y, +// anchors[anchors.length - 1].x - anchors[anchors.length - 2].x, +// ) + +// Math.PI / 2; +// leftOffsetPoints.push( +// calculateOffset(anchors[anchors.length - 1], lastAngle, 1), +// ); +// rightOffsetPoints.push( +// calculateOffset(anchors[anchors.length - 1], lastAngle, -1), +// ); + +// return [leftOffsetPoints, rightOffsetPoints]; +// } + +export function drawPolyline( + canvas: HTMLCanvasElement | null, + offset: IPoint = { x: 0, y: 0 }, + anchors: IPoint[], + color = '#fff', + thickness = 1, + lineDash?: number[], +): void { + if (!canvas) return; + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + ctx.save(); + + ctx.strokeStyle = color; + ctx.lineWidth = thickness; + if (lineDash) { + ctx.setLineDash(lineDash); + } + + ctx.beginPath(); + const { x: offsetX, y: offsetY } = offset; + ctx.moveTo(anchors[0].x + offsetX, anchors[0].y + offsetY); + for (let i = 1; i < anchors.length; i++) { + ctx.lineTo(anchors[i].x + offsetX, anchors[i].y + offsetX); + } + ctx.stroke(); + ctx.restore(); +} + +// function drawOffsetDoubleLine( +// canvas: HTMLCanvasElement | null, +// offset: IPoint = { x: 0, y: 0 }, +// anchors: IPoint[], +// color: string = '#fff', +// thickness: number = 1, +// lineDash: [number[], number[]] = [[], []], +// ): void { +// if (!canvas || anchors.length < 2) return; +// const [leftOffsetPoints, rightOffsetPoints] = calculateOffsetPoints( +// anchors, +// offset, +// ); +// const [leftLineDash, rightLineDash] = lineDash; + +// drawPolyline( +// canvas, +// { x: 0, y: 0 }, +// leftOffsetPoints, +// color, +// thickness, +// leftLineDash, +// ); +// drawPolyline( +// canvas, +// { x: 0, y: 0 }, +// rightOffsetPoints, +// color, +// thickness, +// rightLineDash, +// ); +// } + +export function drawPolylineByType( + canvas: HTMLCanvasElement | null, + anchors: IPoint[], + color: string, + lineType: LineType, +): void { + if (!LINE_STYLE_MAP[lineType]) return; + const { lineDash, thickness } = LINE_STYLE_MAP[lineType]; + drawPolyline(canvas, { x: 0, y: 0 }, anchors, color, thickness, lineDash); +} + export function drawPolygonWithFill( canvas: HTMLCanvasElement | null, anchors: IPoint[], diff --git a/packages/components/src/Annotator/view.tsx b/packages/components/src/Annotator/view.tsx index 9942556..5761a50 100755 --- a/packages/components/src/Annotator/view.tsx +++ b/packages/components/src/Annotator/view.tsx @@ -1,9 +1,18 @@ +import { CursorState } from 'ahooks/lib/useMouse'; +import { cloneDeep } from 'lodash'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { DisplayOption } from './constants'; import { useImmer } from 'use-immer'; -import { cloneDeep } from 'lodash'; + +import { ImageView } from './components/ImageView'; +import { DisplayOption } from './constants'; +import useCanvasRender from './hooks/useCanvasRender'; +import useColor from './hooks/useColor'; +import useDataEffect from './hooks/useDataEffect'; import useHistory from './hooks/useHistory'; +import useMouseCursor from './hooks/useMouseCursor'; import useObjects from './hooks/useObjects'; +import useTranslate from './hooks/useTranslate'; +import { useToolInstances } from './tools/base'; import { BaseObject, Category, @@ -15,16 +24,9 @@ import { AnnoItem, DrawObject, } from './type'; -import useColor from './hooks/useColor'; -import useMouseCursor from './hooks/useMouseCursor'; -import useCanvasRender from './hooks/useCanvasRender'; -import useDataEffect from './hooks/useDataEffect'; -import { useToolInstances } from './tools/base'; import { zoomImgSize } from './utils/compute'; -import { CursorState } from 'ahooks/lib/useMouse'; -import { ImageView } from './components/ImageView'; + import './index.less'; -import useTranslate from './hooks/useTranslate'; export interface ViewProps { isOldMode?: boolean; // is old dataset design mode diff --git a/packages/components/src/QuickLabel/components/ImageFilter/index.tsx b/packages/components/src/QuickLabel/components/ImageFilter/index.tsx index dd3231e..813b8bc 100644 --- a/packages/components/src/QuickLabel/components/ImageFilter/index.tsx +++ b/packages/components/src/QuickLabel/components/ImageFilter/index.tsx @@ -1,7 +1,9 @@ import { ClearOutlined } from '@ant-design/icons'; import { Button, Select } from 'antd'; -import { Category } from '@/Annotator/type'; import { globalLocaleText } from 'dds-utils/locale'; + +import { Category } from '@/Annotator/type'; + import './index.less'; interface IProps { diff --git a/packages/components/src/QuickLabel/components/ImageList/index.tsx b/packages/components/src/QuickLabel/components/ImageList/index.tsx index 9424f53..979dc0c 100644 --- a/packages/components/src/QuickLabel/components/ImageList/index.tsx +++ b/packages/components/src/QuickLabel/components/ImageList/index.tsx @@ -1,6 +1,8 @@ import VirtualList from 'rc-virtual-list'; import { useCallback, useEffect, useState } from 'react'; + import { QsAnnotatorFile } from '../../type'; + import './index.less'; interface IProps { diff --git a/packages/components/src/QuickLabel/components/QuickstartModal/index.tsx b/packages/components/src/QuickLabel/components/QuickstartModal/index.tsx index 011a4bc..32e0808 100644 --- a/packages/components/src/QuickLabel/components/QuickstartModal/index.tsx +++ b/packages/components/src/QuickLabel/components/QuickstartModal/index.tsx @@ -1,9 +1,10 @@ -import { Alert, Button, Modal, UploadFile as AntdUploadFile } from 'antd'; -import Upload, { UploadFile } from 'dds-components/Upload'; import { UploadOutlined } from '@ant-design/icons'; +import { Alert, Button, Modal, UploadFile as AntdUploadFile } from 'antd'; import { UploadChangeParam } from 'antd/es/upload'; -import UploadPreAnno from 'dds-components/UploadPreAnno'; +import { UploadPreAnno } from 'dds-components'; +import Upload, { UploadFile } from 'dds-components/Upload'; import { globalLocaleText } from 'dds-utils/locale'; + import './index.less'; const MAX_COUNT = 1000; diff --git a/packages/components/src/QuickLabel/hooks/useQuickLabelModel.ts b/packages/components/src/QuickLabel/hooks/useQuickLabelModel.ts index 30467da..5bd57d6 100644 --- a/packages/components/src/QuickLabel/hooks/useQuickLabelModel.ts +++ b/packages/components/src/QuickLabel/hooks/useQuickLabelModel.ts @@ -1,13 +1,3 @@ -import { useCallback, useMemo, useState } from 'react'; -import { Updater, useImmer } from 'use-immer'; -import { genFileNameByTimestamp, saveObejctToJsonFile } from 'dds-utils/file'; -import { - convertToCocoDateset, - convertCocoDatasetToAnnotStates, - validateCocoData, -} from '../utils/adapter'; -import { COCO, QsAnnotatorFile } from '../type'; -import { Category } from 'dds-components/Annotator'; import { history } from '@umijs/max'; import { message, @@ -15,9 +5,20 @@ import { UploadFile as AntdUploadFile, UploadProps, } from 'antd'; -import { globalLocaleText } from 'dds-utils/locale'; -import { UploadFile } from 'dds-components/Upload'; import { UploadChangeParam } from 'antd/es/upload'; +import { Category } from 'dds-components/Annotator'; +import { UploadFile } from 'dds-components/Upload'; +import { genFileNameByTimestamp, saveObejctToJsonFile } from 'dds-utils/file'; +import { globalLocaleText } from 'dds-utils/locale'; +import { useCallback, useMemo, useState } from 'react'; +import { Updater, useImmer } from 'use-immer'; + +import { COCO, QsAnnotatorFile } from '../type'; +import { + convertToCocoDateset, + convertCocoDatasetToAnnotStates, + validateCocoData, +} from '../utils/adapter'; const INIT_PRE_ANNOT = { info: {}, diff --git a/packages/components/src/QuickLabel/index.tsx b/packages/components/src/QuickLabel/index.tsx index 56e8d1b..b365e91 100644 --- a/packages/components/src/QuickLabel/index.tsx +++ b/packages/components/src/QuickLabel/index.tsx @@ -1,18 +1,20 @@ -import React, { useEffect } from 'react'; +import { SettingOutlined } from '@ant-design/icons'; import { history } from '@umijs/max'; +import { useKeyPress } from 'ahooks'; +import { Button } from 'antd'; import { AnnotateEditor, BaseObject, EditorMode, } from 'dds-components/Annotator'; -import { Button } from 'antd'; -import { SettingOutlined } from '@ant-design/icons'; -import { useKeyPress } from 'ahooks'; +import { globalLocaleText } from 'dds-utils/locale'; +import React, { useEffect } from 'react'; + +import ImageFilter from './components/ImageFilter'; import { ImageList } from './components/ImageList'; import QuickstartModal from './components/QuickstartModal'; -import ImageFilter from './components/ImageFilter'; import { QuickLabelModel } from './hooks/useQuickLabelModel'; -import { globalLocaleText } from 'dds-utils/locale'; + import './index.less'; const QuickLabel: React.FC = (props) => { diff --git a/packages/components/src/QuickLabel/type.ts b/packages/components/src/QuickLabel/type.ts index 3fe359e..2122ac7 100644 --- a/packages/components/src/QuickLabel/type.ts +++ b/packages/components/src/QuickLabel/type.ts @@ -1,5 +1,5 @@ -import { UploadFile } from 'dds-components/Upload'; import { BaseObject } from 'dds-components/Annotator'; +import { UploadFile } from 'dds-components/Upload'; export interface QsAnnotatorFile extends UploadFile { urlFullRes: string; diff --git a/packages/components/src/QuickLabel/utils/adapter.ts b/packages/components/src/QuickLabel/utils/adapter.ts index a5044dd..26cea6f 100644 --- a/packages/components/src/QuickLabel/utils/adapter.ts +++ b/packages/components/src/QuickLabel/utils/adapter.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { Category } from 'dds-components/Annotator'; +import { BODY_TEMPLATE } from 'dds-components/Annotator/constants'; import { rleToCanvas } from 'dds-components/Annotator/tools/useMask'; import { calculatePolygonArea, @@ -8,10 +9,12 @@ import { translateBoundingBoxToRect, translateRectToBoundingBox, } from 'dds-components/Annotator/utils/compute'; -import { idConverter } from './idConverter'; import { getImageDimensions } from 'dds-utils/file'; + import { COCO, QsAnnotatorFile } from '../type'; +import { idConverter } from './idConverter'; + interface IAnnotatorStates { info: COCO.Info; categories: Category[]; @@ -22,28 +25,6 @@ const IMPORT_CATEGORYID_PRIFIX = 'user_import_category'; const IMPORT_IMAGE_PRIFIX = 'user_import_image'; const IMPORT_ANNOT_PRIFIX = 'user_import_annot'; -export const ddsRleToCocoRle = (ddsRle: number[], imageSize: ISize) => { - const { width, height } = imageSize; - const counts: number[] = []; - - let pos: number = 0; - - for (let i = 0; i < Math.floor(ddsRle.length / 2); i++) { - counts.push(ddsRle[2 * i] - pos); - counts.push(ddsRle[2 * i + 1]); - pos = ddsRle[2 * i] + ddsRle[2 * i + 1]; - } - - if (pos < width * height) { - counts.push(width * height - pos); - } - - return { - size: [imageSize.height, imageSize.width], - counts: counts, - }; -}; - export const convertToCocoDateset = async ({ info, images, @@ -85,6 +66,14 @@ export const convertToCocoDateset = async ({ images, ); + // const allAnnots = images.reduce((prev: BaseObject[], curr) => { + // return prev.concat(curr.objects) + // }, []); + + // const { + // getIntItemId: getIntAnnotId + // } = idConverter(IMPORT_ANNOT_PRIFIX, allAnnots); + for (const image of images) { const imageId = getIntImageId(image.id); @@ -142,18 +131,35 @@ export const convertToCocoDateset = async ({ return sum + area; }, 0); + // const points: number[] = segmentation.flat(); + // const pointObjs: IPoint[] = []; + // for (let i = 0; i < points.length; i += 2) { + // const [x, y] = points.slice(i, i + 2); + // pointObjs.push({ x, y }); + // } + // const { x, y, width, height } = getLimitRectFromPoints(pointObjs); + // const bbox = [x, y, width, height]; + Object.assign(newAnnotation, { segmentation, area }); } - if (annotation.mask && annotation.mask.length > 0) { - const ddsRle = annotation.mask; - const canvas = rleToCanvas(ddsRle, imageSize, '#fff'); - const segmentation = ddsRleToCocoRle(ddsRle, imageSize); + if (annotation.mask) { + const canvas = rleToCanvas( + annotation.mask.counts || '', + imageSize, + '#fff', + ); + const segmentation = annotation.mask; if (canvas) { const { area } = getMaskInfoByCanvas(canvas); + // const { x, y, width, height } = translateBoundingBoxToRect(bbox, { + // width: 1, + // height: 1, + // }); Object.assign(newAnnotation, { segmentation, area, + // bbox: [x, y, width, height], }); } else { Object.assign(newAnnotation, { segmentation }); @@ -172,6 +178,14 @@ export const convertToCocoDateset = async ({ keypoints, num_keypoints, }); + + // const targetCategory = cocoDataset.categories.find( + // (item) => item.name === categoryName, + // ); + // if (targetCategory) { + // targetCategory.skeleton = convertToVerticesArray(lines!); + // targetCategory.keypoints = pointNames; + // } } cocoDataset.annotations.push(newAnnotation); @@ -251,7 +265,10 @@ export const convertCocoDatasetToAnnotStates = ( id: cocoAnnotId, image_id: cocoImageId, category_id: cocoCategoryId, - bbox: cocoBbox, + bbox, + segmentation, + keypoints, + num_keypoints, } = cocoAnnot; const cocoImageData = cocoImageMap.get(cocoImageId); @@ -259,18 +276,58 @@ export const convertCocoDatasetToAnnotStates = ( if (cocoImageData && targetImageData) { const { width: imgWidth, height: imgHeight } = cocoImageData; - const [x, y, width, height] = cocoBbox!; const newObject = { id: getStringAnnotID(cocoAnnotId), categoryId: getStringCategoryID(cocoCategoryId!), categoryName: cocoCategories?.find( (item) => item.id === cocoCategoryId, )?.name, - boundingBox: translateRectToBoundingBox( - { x, y, width, height }, - { width: imgWidth, height: imgHeight }, - ), }; + if (bbox) { + const [x, y, width, height] = bbox; + Object.assign(newObject, { + boundingBox: translateRectToBoundingBox( + { x, y, width, height }, + { width: imgWidth, height: imgHeight }, + ), + }); + } + if (segmentation) { + if (Array.isArray(segmentation)) { + // polygon type + Object.assign(newObject, { + segmentation: (segmentation as number[][]) + ?.map((group) => group.map((pos) => pos.toString())?.join(',')) + .join('/'), + }); + } else if (typeof segmentation.counts === 'string') { + // rle type + Object.assign(newObject, { + mask: segmentation, + }); + } + } + + // TODO: adapt to more keypoints type + if (keypoints && num_keypoints === BODY_TEMPLATE.pointNames.length) { + const points: number[] = []; + for (let i = 0; i * 3 < keypoints.length; i++) { + points.push( + keypoints[i * 3], + keypoints[i * 3 + 1], + 0, + 0, + keypoints[i * 3 + 2], + 1, + ); + } + Object.assign(newObject, { + points, + lines: BODY_TEMPLATE.lines, + pointColors: BODY_TEMPLATE.pointColors, + pointNames: BODY_TEMPLATE.pointNames, + }); + } if (!targetImageData.objects) { targetImageData.objects = []; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 1c7aead..e5df7c4 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -9,3 +9,4 @@ export { default as MobileAlert } from './MobileAlert'; export { default as DynamicPagination } from './DynamicPagination'; export { default as QuickLabel } from './QuickLabel'; export { AnnotateEditor, AnnotatePreview, AnnotateView } from './Annotator'; +export { default as UploadPreAnno } from './UploadPreAnno'; \ No newline at end of file diff --git a/packages/components/src/locales/en-US.ts b/packages/components/src/locales/en-US.ts index 4eefca8..4a644b5 100644 --- a/packages/components/src/locales/en-US.ts +++ b/packages/components/src/locales/en-US.ts @@ -150,20 +150,27 @@ export default { 'DDSAnnotator.anno.mask.translateToRleError': 'Error converting Mask format.', /** DDSAnnotator.smart */ + 'DDSAnnotator.smart.modelSelectModal.title': 'Enable AI Annotate', 'DDSAnnotator.smart.infoModal.title': 'Experience Intelligent Annotate', 'DDSAnnotator.smart.infoModal.content': 'Sorry, this feature is not available in the local version of DeepDataSpace currently. Please visit the official website for more information. You can contact us (deepdataspace_dm@idea.edu.cn) for a priority experience of intelligent annotate.', 'DDSAnnotator.smart.infoModal.action': 'Visit Our Website', 'DDSAnnotator.smart.detection.name': 'Intelligent Object Detection', 'DDSAnnotator.smart.detection.input': 'Select or enter categories', - 'DDSAnnotator.smart.ivp.name': 'Interactive Visual Prompt (iVP)', 'DDSAnnotator.smart.segmentation.name': 'Intelligent Segmentation (Polygon)', 'DDSAnnotator.smart.pose.name': 'Intelligent Pose Estimation', 'DDSAnnotator.smart.mask.name': 'Intelligent Panoramic Segmentation', 'DDSAnnotator.smart.pose.input': 'Select template', 'DDSAnnotator.smart.pose.apply': 'Apply Results', - 'DDSAnnotator.smart.ivp.desc': 'Detect the objects with visual prompt', + 'DDSAnnotator.smart.ivp.name': 'iVP', + 'DDSAnnotator.smart.ivp.desc': + 'Detect the objects with visual prompt (For dense scenes)', + 'DDSAnnotator.smart.gdino.name': 'Grounding-Dino', 'DDSAnnotator.smart.gdino.desc': 'Detect the objects with text prompt', + 'DDSAnnotator.smart.isg.name': 'Interactive Segment', + 'DDSAnnotator.smart.sam.name': 'SAM', + 'DDSAnnotator.smart.sam.desc': 'Segment everything with SAM model', + 'DDSAnnotator.smart.isg.desc': 'Segment objects by interactive actions', 'DDSAnnotator.smart.annotate': 'Auto-Annotate', 'DDSAnnotator.smart.retry': 'Retry', 'DDSAnnotator.smart.modelTyle': 'Model Type', @@ -225,19 +232,19 @@ export default { 'DDSAnnotator.video.track.setting': 'Tracking settings', 'DDSAnnotator.video.frame': 'Frames', 'DDSAnnotator.video.track.backward': 'Backward inference frames', + 'DDSAnnotator.video.limit.play': 'Please wait for all frames to be loaded.', /** dds-upload */ 'dds-upload.title': 'Drag or Click to upload your data', 'dds-upload.limit.type.image': 'Image files (.jpg/.jpeg/.png) are supported.', - 'dds-upload.limit.type.video': - 'Video files (.mp4/.mov & duration < 60s) are supported.', + 'dds-upload.limit.type.video': 'Video files (.mp4/.mov) are supported.', 'dds-upload.upload': 'Add', 'dds-upload.tip.successLoad': 'Had added {count} files', 'dds-upload.tip.fileCountLimitMsg': 'File count should not exceed {count}.', 'dds-upload.videoFrame.title': 'Adjust Frame Count', 'dds-upload.videoFrame.tip': 'Attn', 'dds-upload.videoFrame.tip.content': - 'Choose how many frames you want to annotate. A high frequency will create more, similarframes. A low one will create less frames but more varied imagery.', + 'Choose how many frames you want to annotate(<= 300). A high frequency will create more, similarframes. A low one will create less frames but more varied imagery.', 'dds-upload.videoFrame.adjust': 'Frame rate adjustment range', 'dds-upload.videoFrame.fps': 'frames per second', 'dds-upload.videoFrame.matchNative': 'Match native frame rate', @@ -249,7 +256,7 @@ export default { /** dds-upload-pre-anno */ 'dds-upload-pre-anno': 'Upload Pre-annotate Data', 'dds-upload-pre-anno.tip': - 'Only annotations in DDS format are supported. File size should not exceed {maxSize} MB.', + 'Only annotations in COCO format are supported. File size should not exceed {maxSize} MB.', /** QuickLabel */ 'quicklabel.formModal.attn': 'Attn', diff --git a/packages/components/src/locales/zh-CN.ts b/packages/components/src/locales/zh-CN.ts index acd6b65..0e7a11a 100644 --- a/packages/components/src/locales/zh-CN.ts +++ b/packages/components/src/locales/zh-CN.ts @@ -137,12 +137,12 @@ export default { 'DDSAnnotator.anno.mask.translateToRleError': '转换 Mask 格式错误', /** Annotator.smart */ + 'DDSAnnotator.smart.modelSelectModal.title': '开启智能标注', 'DDSAnnotator.smart.infoModal.title': '体验智能标注', 'DDSAnnotator.smart.infoModal.content': '抱歉, DeepDataSpace的本地版本暂时不支持智能标注功能, 您可以前往官网了解更多信息或联系我们(deepdataspace_dm@idea.edu.cn)获取智能标注的体验通道。', 'DDSAnnotator.smart.infoModal.action': '前往官网', 'DDSAnnotator.smart.detection.name': '智能目标检测', - 'DDSAnnotator.smart.ivp.name': '交互式视觉提示 (iVP)', 'DDSAnnotator.smart.segmentation.name': '智能图像分割(多边形)', 'DDSAnnotator.smart.pose.name': '智能姿态估计', 'DDSAnnotator.smart.mask.name': '智能全景分割', @@ -152,8 +152,14 @@ export default { 'DDSAnnotator.smart.detection.input': '选择或输入类别', 'DDSAnnotator.smart.pose.input': '选择模版', 'DDSAnnotator.smart.pose.apply': '保留当前结果', - 'DDSAnnotator.smart.ivp.desc': '根据视觉提示检测任意目标', + 'DDSAnnotator.smart.ivp.name': '交互式视觉提示 (iVP)', + 'DDSAnnotator.smart.ivp.desc': '适用于密集场景', + 'DDSAnnotator.smart.gdino.name': '文本提示检测一切', 'DDSAnnotator.smart.gdino.desc': '输入任意描述词检测目标', + 'DDSAnnotator.smart.sam.name': '分割一切 (SAM)', + 'DDSAnnotator.smart.sam.desc': '使用 SAM 模型分割全图所有目标', + 'DDSAnnotator.smart.isg.name': '交互式分割', + 'DDSAnnotator.smart.isg.desc': '使用多种交互式操作进行实例分割', 'DDSAnnotator.smart.minArea': '最小分割面积', 'DDSAnnotator.smart.iouThres': 'IoU阈值', 'DDSAnnotator.smart.segmentation.tipsInitial': @@ -205,18 +211,19 @@ export default { 'DDSAnnotator.video.track.setting': '推理设置', 'DDSAnnotator.video.frame': '帧', 'DDSAnnotator.video.track.backward': '向后推理帧数', + 'DDSAnnotator.video.limit.play': '请等待所有帧加载完毕', /** dds-upload */ 'dds-upload.title': '将文件拖动到这里或点击进行上传', 'dds-upload.limit.type.image': '图片格式支持: .jpg/.jpeg/.png', - 'dds-upload.limit.type.video': '视频格式支持: .mp4/.mov、时长 <= 60s', + 'dds-upload.limit.type.video': '视频格式支持: .mp4/.mov', 'dds-upload.upload': '添加', 'dds-upload.tip.successLoad': '成功加载{count}个文件', 'dds-upload.tip.fileCountLimitMsg': '文件数量不能超过{count}', 'dds-upload.videoFrame.title': '调整帧率', 'dds-upload.videoFrame.tip': '注意', 'dds-upload.videoFrame.tip.content': - '选择您想要标注的帧数。高帧率将创建更多相似的帧。低帧率将创建较少的帧,但图像更多样化。', + '选择您想要标注的帧数(<= 300)。高帧率将创建更多相似的帧。低帧率将创建较少的帧,但图像更多样化。', 'dds-upload.videoFrame.adjust': '帧数调整范围', 'dds-upload.videoFrame.fps': '帧/秒', 'dds-upload.videoFrame.matchNative': '与原始帧率匹配', @@ -228,7 +235,7 @@ export default { /** dds-upload-pre-anno */ 'dds-upload-pre-anno': '上传预标注数据', 'dds-upload-pre-anno.tip': - '目前仅支持DDS格式的标注。文件大小不得超过{maxSize} MB。', + '目前仅支持COCO格式的标注。文件大小不得超过{maxSize} MB。', /** QuickLabel */ 'quicklabel.formModal.attn': '注意',