Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support rectTool to export VOC2012 Detection format data #39

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions docs/annotation/format/pascalVoc/pascalVoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 导出 VOC2012 Detection 类型

PASCAL VOC 为图像识别和分类提供了一整套标准化的数据集,VOC2012 Detection 预测图片中每一个目标的 bounding box(位置) and label(类别)。

## 格式说明

- 仅`矩形`可导出 VOC2012 Detection。

- Annotations: 一张图片导出一个 xml 文件,图片中的每一个标注框的信息需要一个 `<object> </object>` 包裹,无标注框则无`<object/>` 标签。

- 无拉框,则对应标签包裹内容为空。

标签格式:

| 标签名称 | 包裹值
------------------------------------
| folder | 图片所处文件夹
| filename | 图片名
| path | 图片路径
| source | 图片来源相关信息
| database | 导出属性映射文件内容
| size | 图片尺寸
| width | 宽
| height | 高
| depth | 管道
| segmented | 是否有分割
| object | 包含物体
| name | 物体类别
| pose | 物体姿态(默认 Unspecified)
| truncated | 物体是否被遮挡(默认 0)
| difficult | 是否为难识别的物体(默认 0)
| bndbox | 物体bound box
| xmin | 最小横坐标
| ymin | 最小纵坐标
| xmax | 最大横坐标
| ymax | 最大纵坐标
38 changes: 34 additions & 4 deletions src/ProjectPlatform/ProjectList/ExportData/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ const ExportData = (props: IProps) => {
*/
const isTransfer2Yolo = projectInfo?.toolName === EToolName.Rect;

/**
* 判断当前是否允许被转换成 VOC2012 Detection 格式
*/
const isTransfer2PascalVoc = projectInfo?.toolName === EToolName.Rect;

/**
* 是否允许被转换的成 Mask
*/
const isTransfer2ACE20k = projectInfo?.toolName === EToolName.Polygon;

const onOk = () => {
if (!ipcRenderer || !projectInfo) {
return;
Expand Down Expand Up @@ -101,7 +107,20 @@ const ExportData = (props: IProps) => {
);

break;
case 'voc':
name = `${projectInfo.name}-voc`;

fileList.forEach((file, i) => {
const doc = DataTransfer.transferDefault2Voc(file);
electron.ipcRenderer.send(
EIpcEvent.SaveFile,
doc,
values.path,
'utf8',
`${file.fileName}_voc.xml`,
);
});
break;
case 'Mask':
name = `${projectInfo.name}-Mask`;
suffix = 'png';
Expand Down Expand Up @@ -201,22 +220,33 @@ const ExportData = (props: IProps) => {
<Form labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} layout='horizontal' form={form}>
<Form.Item label={t('ExportFormat')} name='format' initialValue='default'>
<Radio.Group>
<Radio.Button value='coco' disabled={!isTransfer2Coco}>
<Radio.Button value='coco' disabled={!isTransfer2Coco} style={{ margin: 4 }}>
{isTransfer2Coco ? (
'COCO'
) : (
<Popover content={t('ExportCOCOLimitMsg')}>COCO</Popover>
)}
</Radio.Button>
<Radio.Button value='yolo' disabled={!isTransfer2Yolo}>
<Radio.Button value='yolo' disabled={!isTransfer2Yolo} style={{ margin: 4 }}>
{isTransfer2Yolo ? (
'YOLO'
) : (
<Popover content={t('ExportYOLOLimitMsg')}>YOLO</Popover>
)}
</Radio.Button>
<Radio.Button value='default'>{t('StandardFormat')}</Radio.Button>
<Radio.Button value='Mask' disabled={!isTransfer2ACE20k}>

<Radio.Button value='voc' disabled={!isTransfer2PascalVoc} style={{ margin: 4 }}>
{isTransfer2PascalVoc ? (
'VOC2012 Detection'
) : (
<Popover content={t('ExportVOCLimitMsg')}>VOC2012 Detection</Popover>
)}
</Radio.Button>

<Radio.Button value='default' style={{ margin: 4 }}>
{t('StandardFormat')}
</Radio.Button>
<Radio.Button value='Mask' disabled={!isTransfer2ACE20k} style={{ margin: 4 }}>
{isTransfer2ACE20k ? (
'Mask'
) : (
Expand Down
252 changes: 252 additions & 0 deletions src/copyApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import MainView from '@/views/MainView';
import { IPointCloudBox, i18n } from '@labelbee/lb-utils';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { store } from '.';
import { LabelBeeContext } from '@/store/ctx';
import { AppState } from './store';
import { ANNOTATION_ACTIONS } from './store/Actions';
import {
InitAnnotationState,
InitTaskData,
loadImgList,
UpdateInjectFunc,
} from './store/annotation/actionCreators';
import { AfterCommonLoaded, LoadFileAndFileData } from './store/annotation/reducer';
import { ToolInstance } from './store/annotation/types';
import {
GetFileData,
IFileItem,
LoadFileList,
OnPageChange,
OnSave,
OnStepChange,
OnSubmit,
} from './types/data';
import { Header, RenderFooter, Sider, DrawLayerSlot } from './types/main';
import { IStepInfo } from './types/step';
import { ConfigProvider } from 'antd/es';
import zhCN from 'antd/es/locale/zh_CN';
import enUS from 'antd/es/locale/en_US';
import { EPointCloudName } from '@labelbee/lb-annotation';

interface IAnnotationStyle {
strokeColor: string;
fillColor: string;
textColor: string;
toolColor: any;
}

export interface IPreDataProcessParams {
// 标注类型:暂时只支持点云
tool: EPointCloudName.PointCloud | string;
// 更新数据
dataList: IPointCloudBox[];
// 更新数据的具体动作
action: 'preDataProcess' | 'viewUpdateBox';
// 当前步骤的config
stepConfig: IStepInfo['config'];
}

export interface AppProps {
exportData?: (data: any[]) => void;
goBack?: () => void;
imgList?: IFileItem[];
config: string;
stepList: IStepInfo[];
step: number;
onSubmit?: OnSubmit;
onSave?: OnSave;
onPageChange?: OnPageChange;
onStepChange?: OnStepChange;
getFileData?: GetFileData;
pageSize: number;
loadFileList?: LoadFileList;
headerName?: string;
initialIndex?: number;
className?: string;
toolInstance: ToolInstance;
header?: Header;
footer?: RenderFooter;
sider?: Sider;
style?: {
layout?: { [key: string]: any };
header?: { [key: string]: any };
sider?: { [key: string]: any };
footer?: { [key: string]: any };
};
setToolInstance?: (tool: ToolInstance) => void;
mode?: 'light' | 'dark'; // 临时需求应用于 toolFooter 的操作
showTips?: boolean; // 是否展示 tips
tips?: string; // Tips 具体内容
defaultLang: 'en' | 'cn'; // 国际化设置
leftSider?: () => React.ReactNode | React.ReactNode;

// data Correction
skipBeforePageTurning?: (pageTurning: Function) => void;
beforeRotate?: () => boolean;

drawLayerSlot?: DrawLayerSlot;

// 标注信息扩展的功能
dataInjectionAtCreation: (annotationData: any) => {};
// 渲染增强
renderEnhance: {
staticRender?: (canvas: HTMLCanvasElement, data: any, style: IAnnotationStyle) => void;
selectedRender?: (canvas: HTMLCanvasElement, data: any, style: IAnnotationStyle) => void;
creatingRender?: (canvas: HTMLCanvasElement, data: any, style: IAnnotationStyle) => void;
};
customRenderStyle?: (data: any) => IAnnotationStyle;

checkMode?: boolean;
intelligentFit?: boolean;
enableColorPicker?: boolean;
highlightAttribute?: string;
onLoad?: ({ toolInstance }: { toolInstance: ToolInstance }) => void;
preDataProcess?: (params: IPreDataProcessParams) => IPointCloudBox[];
auditContext?: any;
}

const App: React.FC<AppProps> = (props) => {
const [_, forceRender] = useState(0);
const {
imgList,
step = 1,
stepList,
onSubmit,
onSave,
onPageChange,
onStepChange,
initialIndex = 0,
toolInstance,
setToolInstance,
getFileData,
pageSize = 10,
loadFileList,
defaultLang = 'cn',
skipBeforePageTurning,
beforeRotate,
checkMode = false,
intelligentFit = true,
highlightAttribute = '',
preDataProcess,
imgIndex
} = props;

useEffect(() => {
store.dispatch(
InitTaskData({
onSubmit,
stepList,
step,
getFileData,
pageSize,
loadFileList,
onSave,
onPageChange,
onStepChange,
skipBeforePageTurning,
beforeRotate,
checkMode,
highlightAttribute,
preDataProcess,
}),
);

initImgList();
// 初始化国际化语言
i18n.changeLanguage(defaultLang);

const i18nLanguageChangedFunc = () => {
forceRender((v) => v + 1);
};

i18n.on('languageChanged', i18nLanguageChangedFunc);
return () => {
i18n.off('languageChanged', i18nLanguageChangedFunc);

// Init all annotation state after unmounting
InitAnnotationState(store.dispatch);
};
}, []);

useEffect(() => {
store.dispatch(
UpdateInjectFunc({
onSubmit,
stepList,
getFileData,
pageSize,
loadFileList,
onSave,
onPageChange,
onStepChange,
beforeRotate,
highlightAttribute,
preDataProcess,
}),
);

i18n.changeLanguage(defaultLang);
}, [
onSubmit,
stepList,
getFileData,
pageSize,
loadFileList,
onSave,
onPageChange,
onStepChange,
defaultLang,
beforeRotate,
highlightAttribute,
preDataProcess,
]);

useEffect(() => {
setToolInstance?.(toolInstance);
}, [toolInstance]);

useEffect(()=>{
store.dispatch({
type: ANNOTATION_ACTIONS.UPDATE_IMG_INDEX,
payload: {
imgIndex,
},
});
store.dispatch(LoadFileAndFileData(imgIndex));
},[imgIndex])

// 初始化imgList 优先以loadFileList方式加载数据
const initImgList = () => {
if (loadFileList) {
loadImgList(store.dispatch, store.getState, initialIndex, true).then((isSuccess) => {
if (isSuccess) {
store.dispatch(LoadFileAndFileData(initialIndex));
}
});
} else if (imgList && imgList.length > 0) {
store.dispatch({
type: ANNOTATION_ACTIONS.UPDATE_IMG_LIST,
payload: {
imgList,
},
});
store.dispatch(LoadFileAndFileData(initialIndex));
}
};

return (
<div>
<ConfigProvider locale={i18n.language === 'en' ? enUS : zhCN}>
<MainView {...props} intelligentFit={intelligentFit} checkMode={checkMode} />
</ConfigProvider>
</div>
);
};

const mapStateToProps = (state: AppState) => ({
toolInstance: state.annotation.toolInstance,
});

export default connect(mapStateToProps, null, null, { context: LabelBeeContext })(App);
2 changes: 2 additions & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const resources = {
ExportSuccess: 'Export successfully',
ExportCOCOLimitMsg: 'Only rectTool and polygonTool can realize the conversion of coco data',
ExportYOLOLimitMsg: 'Only rectTool can realize the conversion of yolo data',
ExportVOCLimitMsg: 'Only rectTool can realize the conversion of VOC2012 Detection data',
ExportMaskLimitMsg: 'Only polygonTool can realize the conversion of Mask',
SelectedExportPath: 'Choose Export path',
MultiSelect: 'multi-select',
Expand Down Expand Up @@ -187,6 +188,7 @@ const resources = {
ExportSuccess: '导出成功',
ExportCOCOLimitMsg: '仅限拉框、多边形工具可以实现 coco 数据的转换',
ExportYOLOLimitMsg: '仅限拉框工具可以实现 yolo 数据的转换',
ExportVOCLimitMsg: '仅限拉框工具可以实现VOC2012 Detection 数据的转换',
ExportMaskLimitMsg: '仅限多边形工具可以实现 Mask 数据的转换',
SelectedExportPath: '选择导出的路径',
MultiSelect: '多选',
Expand Down
Loading