diff --git "a/docs/annotation/format/pascalVoc/annotation-xml\346\240\274\345\274\217.jpg" "b/docs/annotation/format/pascalVoc/annotation-xml\346\240\274\345\274\217.jpg"
new file mode 100644
index 0000000..c2aff4f
Binary files /dev/null and "b/docs/annotation/format/pascalVoc/annotation-xml\346\240\274\345\274\217.jpg" differ
diff --git a/docs/annotation/format/pascalVoc/pascalVoc.md b/docs/annotation/format/pascalVoc/pascalVoc.md
new file mode 100644
index 0000000..475d02b
--- /dev/null
+++ b/docs/annotation/format/pascalVoc/pascalVoc.md
@@ -0,0 +1,36 @@
+# 导出 VOC2012 Detection 类型
+
+PASCAL VOC 为图像识别和分类提供了一整套标准化的数据集,VOC2012 Detection 预测图片中每一个目标的 bounding box(位置) and label(类别)。
+
+## 格式说明
+
+- 仅`矩形`可导出 VOC2012 Detection。
+
+- Annotations: 一张图片导出一个 xml 文件,图片中的每一个标注框的信息需要一个 `` 包裹,无标注框则无`` 标签。
+
+- 无拉框,则对应标签包裹内容为空。
+
+标签格式:
+
+ | 标签名称 | 包裹值
+ ------------------------------------
+ | 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 | 最大纵坐标
diff --git a/src/ProjectPlatform/ProjectList/ExportData/index.tsx b/src/ProjectPlatform/ProjectList/ExportData/index.tsx
index 5ee2085..fcefcb1 100644
--- a/src/ProjectPlatform/ProjectList/ExportData/index.tsx
+++ b/src/ProjectPlatform/ProjectList/ExportData/index.tsx
@@ -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;
@@ -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';
@@ -201,22 +220,33 @@ const ExportData = (props: IProps) => {
-
+
{isTransfer2Coco ? (
'COCO'
) : (
COCO
)}
-
+
{isTransfer2Yolo ? (
'YOLO'
) : (
YOLO
)}
- {t('StandardFormat')}
-
+
+
+ {isTransfer2PascalVoc ? (
+ 'VOC2012 Detection'
+ ) : (
+ VOC2012 Detection
+ )}
+
+
+
+ {t('StandardFormat')}
+
+
{isTransfer2ACE20k ? (
'Mask'
) : (
diff --git a/src/copyApp.tsx b/src/copyApp.tsx
new file mode 100644
index 0000000..51d03b8
--- /dev/null
+++ b/src/copyApp.tsx
@@ -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 = (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 (
+
+
+
+
+
+ );
+};
+
+const mapStateToProps = (state: AppState) => ({
+ toolInstance: state.annotation.toolInstance,
+});
+
+export default connect(mapStateToProps, null, null, { context: LabelBeeContext })(App);
diff --git a/src/i18n.ts b/src/i18n.ts
index e9896f6..9fbae6d 100644
--- a/src/i18n.ts
+++ b/src/i18n.ts
@@ -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',
@@ -187,6 +188,7 @@ const resources = {
ExportSuccess: '导出成功',
ExportCOCOLimitMsg: '仅限拉框、多边形工具可以实现 coco 数据的转换',
ExportYOLOLimitMsg: '仅限拉框工具可以实现 yolo 数据的转换',
+ ExportVOCLimitMsg: '仅限拉框工具可以实现VOC2012 Detection 数据的转换',
ExportMaskLimitMsg: '仅限多边形工具可以实现 Mask 数据的转换',
SelectedExportPath: '选择导出的路径',
MultiSelect: '多选',
diff --git a/src/utils/CreateDoc.ts b/src/utils/CreateDoc.ts
new file mode 100644
index 0000000..bd2fe5e
--- /dev/null
+++ b/src/utils/CreateDoc.ts
@@ -0,0 +1,27 @@
+/**
+ * 创建dom节点
+ */
+export default class CreateDoc {
+ public tagName: string = '';
+ public children: any = [];
+ constructor(props: any) {
+ this.tagName = props.tagName;
+ const children = props.children.map((item: any) => {
+ if (typeof item === 'object') {
+ item = new CreateDoc(item);
+ }
+ return item;
+ });
+ this.children = children;
+ }
+
+ public render() {
+ const el = document.createElement(this.tagName);
+ const children = this.children || [];
+ children.forEach((v: any) => {
+ const childEl = v instanceof CreateDoc ? v.render() : document.createTextNode(v);
+ el.appendChild(childEl);
+ });
+ return el;
+ }
+}
diff --git a/src/utils/DataTransfer.ts b/src/utils/DataTransfer.ts
index e9d438e..86efb98 100644
--- a/src/utils/DataTransfer.ts
+++ b/src/utils/DataTransfer.ts
@@ -7,6 +7,7 @@ import { IFileInfo, IStepInfo } from '@/store';
import { getBaseName, jsonParser } from './tool/common';
import { DrawUtils } from '@labelbee/lb-annotation';
import ColorCheatSheet from '@/assets/color.json';
+import CreateDoc from '@/utils/CreateDoc';
// 获取 color cheat sheet 内的颜色
export const getRgbFromColorCheatSheet = (index: number) => {
@@ -316,6 +317,198 @@ export default class DataTransfer {
return { categories, idString };
}
+ /**
+ * 格式化xml代码
+ *
+ * @param xmlStr
+ */
+ public static formateXml(xmlStr: any) {
+ var text = xmlStr;
+
+ //使用replace去空格
+ text =
+ '\n' +
+ text
+ .replace(/(<\w+)(\s.*?>)/g, function ($0: any, name: any, props: any) {
+ return name + ' ' + props.replace(/\s+(\w+=)/g, ' $1');
+ })
+ .replace(/>\s*?\n<');
+
+ //调整格式 以压栈方式递归调整缩进
+ var rgx = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/gm;
+ var nodeStack: any = [];
+ var output = text.replace(
+ rgx,
+ function (
+ $0: any,
+ all: any,
+ name: any,
+ isBegin: any,
+ isCloseFull1: any,
+ isCloseFull2: any,
+ isFull1: any,
+ isFull2: any,
+ ) {
+ var isClosed =
+ isCloseFull1 == '/' || isCloseFull2 == '/' || isFull1 == '/' || isFull2 == '/';
+ var prefix = '';
+ if (isBegin == '!') {
+ //!开头
+ prefix = DataTransfer.setPrefix(nodeStack.length);
+ } else {
+ if (isBegin != '/') {
+ ///开头
+ prefix = DataTransfer.setPrefix(nodeStack.length);
+ if (!isClosed) {
+ //非关闭标签
+ nodeStack.push(name);
+ }
+ } else {
+ nodeStack.pop(); //弹栈
+ prefix = DataTransfer.setPrefix(nodeStack.length);
+ }
+ }
+ var ret = '\n' + prefix + all;
+ return ret;
+ },
+ );
+ var outputText = output.substring(1);
+ return outputText;
+ }
+
+ /**
+ * 计算头函数,用来缩进
+ *
+ * @param prefixIndex
+ */
+ public static setPrefix(prefixIndex: any) {
+ var result = '';
+ var span = ' '; //缩进长度
+ var output = [];
+ for (var i = 0; i < prefixIndex; ++i) {
+ output.push(span);
+ }
+ result = output.join('');
+ return result;
+ }
+
+ /**
+ * 将 sensebee 格式转换为 pascal voc 格式
+ *
+ * @param file
+ */
+ public static transferDefault2Voc(file: any) {
+ const result = jsonParser(file?.result);
+ const folder = file.url.replace(file?.fileName, ''); // 文件夹
+
+ let labelObj = {
+ tagName: 'annotation',
+ children: [
+ {
+ tagName: 'folder', // 图片所处文件夹
+ children: [folder],
+ },
+ {
+ tagName: 'filename', // 图片名
+ children: [file?.fileName],
+ },
+ {
+ tagName: 'path',
+ children: [file?.url],
+ },
+ {
+ tagName: 'source', // 图片来源相关信息
+ children: [
+ {
+ tagName: 'database',
+ children: [],
+ },
+ ],
+ },
+ {
+ tagName: 'size', // 图片尺寸
+ children: [
+ {
+ tagName: 'width',
+ children: [result?.width],
+ },
+ {
+ tagName: 'height',
+ children: [result?.height],
+ },
+ {
+ tagName: 'depth',
+ children: [3],
+ },
+ ],
+ },
+ {
+ tagName: 'segmented', // 是否有分割
+ children: [0],
+ },
+ ],
+ };
+
+ if (result?.step_1?.result?.length > 0) {
+ result?.step_1.result.forEach((v: any) => {
+ const xmin = (v.x - v.width / 2).toFixed(6); // 最小横坐标
+ const ymin = (v.x - v.height / 2).toFixed(6); // 最小纵坐标
+ const xmax = (v.x + v.width / 2).toFixed(6); // 最小横坐标
+ const ymax = (v.x + v.height / 2).toFixed(6); // 最大纵坐标
+ const object = {
+ tagName: 'object', // 包含物体
+ children: [
+ {
+ tagName: 'name', // 物体类别
+ children: [],
+ },
+ {
+ tagName: 'pose', // 物体姿态
+ children: ['Unspecified'],
+ },
+ {
+ tagName: 'truncated', // 物体是否被遮挡
+ children: [0],
+ },
+ {
+ tagName: 'difficult', // 是否为难识别的物体
+ children: [0],
+ },
+ {
+ tagName: 'bndbox',
+ children: [
+ {
+ tagName: 'xmin',
+ children: [xmin],
+ },
+ {
+ tagName: 'ymin',
+ children: [ymin],
+ },
+ {
+ tagName: 'xmax',
+ children: [xmax],
+ },
+ {
+ tagName: 'ymax',
+ children: [ymax],
+ },
+ ],
+ },
+ ],
+ };
+ labelObj.children.push(object);
+ });
+ }
+ const doc = new CreateDoc(labelObj);
+ const xmlSerializer = new XMLSerializer();
+ let setupSerial = xmlSerializer.serializeToString(doc.render());
+ const reg = new RegExp(' xmlns="http://www.w3.org/1999/xhtml"', 'g');
+ setupSerial = setupSerial.replace(reg, '');
+ setupSerial = this.formateXml(setupSerial);
+ return setupSerial;
+ }
+
/**
* 将 sensebee 格式转换为 yolo 格式
* 仅限工具: 拉框