You've already forked DataMate
feat: 完善数据标注导出格式兼容性验证
- 后端:添加 YOLO 格式对 TEXT 数据集的限制验证 - 后端:统一 COCO/YOLO 兼容性校验规则(仅允许图像类或目标检测类数据集) - 后端:修复 datasetType 字段传递,在任务列表响应中补充 dataset_type - 前端:在导出对话框中禁用 TEXT 数据集的 COCO/YOLO 选项 - 前端:添加 datasetType 和 labelingType 字段传递 - 前端:对齐前后端 COCO/YOLO 兼容性规则 - 前端:优化提示文案,明确说明格式适用范围 修改文件: - runtime/datamate-python/app/module/annotation/service/export.py - runtime/datamate-python/app/module/annotation/service/mapping.py - runtime/datamate-python/app/module/annotation/schema/mapping.py - frontend/src/pages/DataAnnotation/Home/ExportAnnotationDialog.tsx - frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx - frontend/src/pages/DataAnnotation/annotation.const.tsx
This commit is contained in:
@@ -378,6 +378,8 @@ export default function DataAnnotation() {
|
|||||||
open={!!exportTask}
|
open={!!exportTask}
|
||||||
projectId={exportTask?.id || ""}
|
projectId={exportTask?.id || ""}
|
||||||
projectName={exportTask?.name || ""}
|
projectName={exportTask?.name || ""}
|
||||||
|
datasetType={exportTask?.datasetType}
|
||||||
|
labelingType={exportTask?.labelingType}
|
||||||
onClose={() => setExportTask(null)}
|
onClose={() => setExportTask(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
message,
|
message,
|
||||||
Alert,
|
Alert,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { FileTextOutlined, CheckCircleOutlined } from "@ant-design/icons";
|
import { FileTextOutlined, CheckCircleOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
getExportStatsUsingGet,
|
getExportStatsUsingGet,
|
||||||
downloadAnnotationsUsingGet,
|
downloadAnnotationsUsingGet,
|
||||||
@@ -22,6 +22,8 @@ interface ExportAnnotationDialogProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
datasetType?: string;
|
||||||
|
labelingType?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +59,8 @@ export default function ExportAnnotationDialog({
|
|||||||
open,
|
open,
|
||||||
projectId,
|
projectId,
|
||||||
projectName,
|
projectName,
|
||||||
|
datasetType,
|
||||||
|
labelingType,
|
||||||
onClose,
|
onClose,
|
||||||
}: ExportAnnotationDialogProps) {
|
}: ExportAnnotationDialogProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -67,6 +71,15 @@ export default function ExportAnnotationDialog({
|
|||||||
annotatedFiles: number;
|
annotatedFiles: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
const normalizedDatasetType = datasetType?.toUpperCase().replace(/-/g, "_") || "";
|
||||||
|
const normalizedLabelingType = labelingType?.toUpperCase().replace(/-/g, "_") || "";
|
||||||
|
const hasDatasetType = Boolean(normalizedDatasetType);
|
||||||
|
const isTextDataset = normalizedDatasetType === "TEXT";
|
||||||
|
const isDetectionCompatible =
|
||||||
|
normalizedDatasetType === "IMAGE" ||
|
||||||
|
normalizedDatasetType === "OBJECT_DETECTION" ||
|
||||||
|
normalizedLabelingType === "OBJECT_DETECTION";
|
||||||
|
|
||||||
// 加载导出统计信息
|
// 加载导出统计信息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && projectId) {
|
if (open && projectId) {
|
||||||
@@ -176,20 +189,46 @@ export default function ExportAnnotationDialog({
|
|||||||
rules={[{ required: true, message: "请选择导出格式" }]}
|
rules={[{ required: true, message: "请选择导出格式" }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={FORMAT_OPTIONS.map((opt) => ({
|
options={FORMAT_OPTIONS.map((opt) => {
|
||||||
label: (
|
const isDetectionFormat = opt.value === "coco" || opt.value === "yolo";
|
||||||
<div className="py-1">
|
const isDisabledForText = isDetectionFormat && isTextDataset;
|
||||||
<div className="font-medium">{opt.label}</div>
|
const isDisabledForIncompatibleType =
|
||||||
<div className="text-xs text-gray-400">{opt.description}</div>
|
isDetectionFormat && hasDatasetType && !isTextDataset && !isDetectionCompatible;
|
||||||
</div>
|
const isDisabled = isDisabledForText || isDisabledForIncompatibleType;
|
||||||
),
|
return {
|
||||||
value: opt.value,
|
label: (
|
||||||
simpleLabel: opt.label,
|
<div className="py-1 relative">
|
||||||
}))}
|
<div className={`font-medium ${isDisabled ? "text-gray-400" : ""}`}>{opt.label}</div>
|
||||||
|
<div className={`text-xs ${isDisabled ? "text-gray-300" : "text-gray-400"}`}>
|
||||||
|
{opt.description}
|
||||||
|
{isDisabled && (
|
||||||
|
<span className="block mt-1 text-orange-500">
|
||||||
|
<InfoCircleOutlined className="mr-1" />
|
||||||
|
仅适用于图像类或目标检测类数据集
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: opt.value,
|
||||||
|
simpleLabel: opt.label,
|
||||||
|
disabled: isDisabled,
|
||||||
|
};
|
||||||
|
})}
|
||||||
optionLabelProp="simpleLabel"
|
optionLabelProp="simpleLabel"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{(isTextDataset || (hasDatasetType && !isDetectionCompatible)) && (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
message="导出格式兼容性提示"
|
||||||
|
description="COCO 和 YOLO 格式仅适用于图像类或目标检测类数据集,当前项目建议使用 JSON、JSON Lines 或 CSV 格式导出。"
|
||||||
|
showIcon
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item name="onlyAnnotated" valuePropName="checked">
|
<Form.Item name="onlyAnnotated" valuePropName="checked">
|
||||||
<Checkbox>仅导出已标注数据</Checkbox>
|
<Checkbox>仅导出已标注数据</Checkbox>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type AnnotationTaskPayload = {
|
|||||||
datasetId?: string;
|
datasetId?: string;
|
||||||
datasetName?: string;
|
datasetName?: string;
|
||||||
dataset_name?: string;
|
dataset_name?: string;
|
||||||
|
datasetType?: string;
|
||||||
|
dataset_type?: string;
|
||||||
labelingType?: string;
|
labelingType?: string;
|
||||||
labeling_type?: string;
|
labeling_type?: string;
|
||||||
template?: {
|
template?: {
|
||||||
@@ -54,6 +56,7 @@ export type AnnotationTaskListItem = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
datasetId?: string;
|
datasetId?: string;
|
||||||
datasetName?: string;
|
datasetName?: string;
|
||||||
|
datasetType?: string;
|
||||||
labelingType?: string;
|
labelingType?: string;
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
annotatedCount?: number;
|
annotatedCount?: number;
|
||||||
@@ -97,6 +100,7 @@ export function mapAnnotationTask(task: AnnotationTaskPayload): AnnotationTaskLi
|
|||||||
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
|
const labelingProjId = task?.labelingProjId || task?.labelingProjectId || task?.projId || task?.labeling_project_id || "";
|
||||||
const segmentationEnabled = task?.segmentationEnabled ?? task?.segmentation_enabled ?? false;
|
const segmentationEnabled = task?.segmentationEnabled ?? task?.segmentation_enabled ?? false;
|
||||||
const inProgressCount = task?.inProgressCount ?? task?.in_progress_count ?? 0;
|
const inProgressCount = task?.inProgressCount ?? task?.in_progress_count ?? 0;
|
||||||
|
const datasetType = task?.datasetType || task?.dataset_type;
|
||||||
const labelingType =
|
const labelingType =
|
||||||
task?.labelingType ||
|
task?.labelingType ||
|
||||||
task?.labeling_type ||
|
task?.labeling_type ||
|
||||||
@@ -119,6 +123,7 @@ export function mapAnnotationTask(task: AnnotationTaskPayload): AnnotationTaskLi
|
|||||||
projId: labelingProjId,
|
projId: labelingProjId,
|
||||||
segmentationEnabled,
|
segmentationEnabled,
|
||||||
inProgressCount,
|
inProgressCount,
|
||||||
|
datasetType,
|
||||||
labelingType,
|
labelingType,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
description: task.description || "",
|
description: task.description || "",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class DatasetMappingResponse(BaseModel):
|
|||||||
id: str = Field(..., description="映射UUID")
|
id: str = Field(..., description="映射UUID")
|
||||||
dataset_id: str = Field(..., alias="datasetId", description="源数据集ID")
|
dataset_id: str = Field(..., alias="datasetId", description="源数据集ID")
|
||||||
dataset_name: Optional[str] = Field(None, alias="datasetName", description="数据集名称")
|
dataset_name: Optional[str] = Field(None, alias="datasetName", description="数据集名称")
|
||||||
|
dataset_type: Optional[str] = Field(None, alias="datasetType", description="数据集类型")
|
||||||
labeling_project_id: str = Field(..., alias="labelingProjectId", description="标注项目ID")
|
labeling_project_id: str = Field(..., alias="labelingProjectId", description="标注项目ID")
|
||||||
name: Optional[str] = Field(None, description="标注项目名称")
|
name: Optional[str] = Field(None, description="标注项目名称")
|
||||||
description: Optional[str] = Field(None, description="标注项目描述")
|
description: Optional[str] = Field(None, description="标注项目描述")
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ DATASET_TYPE_IMAGE = "IMAGE"
|
|||||||
DATASET_TYPE_OBJECT_DETECTION = "OBJECT_DETECTION"
|
DATASET_TYPE_OBJECT_DETECTION = "OBJECT_DETECTION"
|
||||||
LABELING_TYPE_CONFIG_KEY = "labeling_type"
|
LABELING_TYPE_CONFIG_KEY = "labeling_type"
|
||||||
LABELING_TYPE_OBJECT_DETECTION = "OBJECT_DETECTION"
|
LABELING_TYPE_OBJECT_DETECTION = "OBJECT_DETECTION"
|
||||||
COCO_COMPATIBLE_DATASET_TYPES = {
|
DETECTION_COMPATIBLE_DATASET_TYPES = {
|
||||||
DATASET_TYPE_IMAGE,
|
DATASET_TYPE_IMAGE,
|
||||||
DATASET_TYPE_OBJECT_DETECTION,
|
DATASET_TYPE_OBJECT_DETECTION,
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ class AnnotationExportService:
|
|||||||
project: LabelingProject,
|
project: LabelingProject,
|
||||||
format_type: ExportFormat,
|
format_type: ExportFormat,
|
||||||
) -> None:
|
) -> None:
|
||||||
if format_type != ExportFormat.COCO:
|
if format_type not in (ExportFormat.COCO, ExportFormat.YOLO):
|
||||||
return
|
return
|
||||||
|
|
||||||
dataset_type = self._normalize_type_value(
|
dataset_type = self._normalize_type_value(
|
||||||
@@ -249,11 +249,11 @@ class AnnotationExportService:
|
|||||||
if dataset_type == DATASET_TYPE_TEXT:
|
if dataset_type == DATASET_TYPE_TEXT:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="导出格式 COCO 不支持文本类数据集(TEXT),请改用 JSON/JSONL/CSV 格式",
|
detail=f"导出格式 {format_type.value.upper()} 不支持文本类数据集(TEXT),请改用 JSON/JSONL/CSV 格式",
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dataset_type in COCO_COMPATIBLE_DATASET_TYPES
|
dataset_type in DETECTION_COMPATIBLE_DATASET_TYPES
|
||||||
or labeling_type == LABELING_TYPE_OBJECT_DETECTION
|
or labeling_type == LABELING_TYPE_OBJECT_DETECTION
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
@@ -261,7 +261,7 @@ class AnnotationExportService:
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=(
|
detail=(
|
||||||
"导出格式 COCO 仅适用于图像类或目标检测类数据集,"
|
f"导出格式 {format_type.value.upper()} 仅适用于图像类或目标检测类数据集,"
|
||||||
f"当前数据集类型: {dataset_type or 'UNKNOWN'},"
|
f"当前数据集类型: {dataset_type or 'UNKNOWN'},"
|
||||||
f"标注类型: {labeling_type or 'UNKNOWN'}"
|
f"标注类型: {labeling_type or 'UNKNOWN'}"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ class DatasetMappingService:
|
|||||||
"""Build base query with dataset name joined"""
|
"""Build base query with dataset name joined"""
|
||||||
return select(
|
return select(
|
||||||
LabelingProject,
|
LabelingProject,
|
||||||
Dataset.name.label('dataset_name')
|
Dataset.name.label('dataset_name'),
|
||||||
|
Dataset.dataset_type.label('dataset_type'),
|
||||||
).outerjoin(
|
).outerjoin(
|
||||||
Dataset,
|
Dataset,
|
||||||
LabelingProject.dataset_id == Dataset.id
|
LabelingProject.dataset_id == Dataset.id
|
||||||
@@ -98,6 +99,7 @@ class DatasetMappingService:
|
|||||||
"""
|
"""
|
||||||
mapping = row[0] # LabelingProject object
|
mapping = row[0] # LabelingProject object
|
||||||
dataset_name = row[1] # dataset_name from join
|
dataset_name = row[1] # dataset_name from join
|
||||||
|
dataset_type = row[2] # dataset_type from join
|
||||||
|
|
||||||
# Get template_id from mapping
|
# Get template_id from mapping
|
||||||
template_id = getattr(mapping, 'template_id', None)
|
template_id = getattr(mapping, 'template_id', None)
|
||||||
@@ -134,6 +136,7 @@ class DatasetMappingService:
|
|||||||
"id": mapping.id,
|
"id": mapping.id,
|
||||||
"dataset_id": mapping.dataset_id,
|
"dataset_id": mapping.dataset_id,
|
||||||
"dataset_name": dataset_name,
|
"dataset_name": dataset_name,
|
||||||
|
"dataset_type": dataset_type,
|
||||||
"labeling_project_id": mapping.labeling_project_id,
|
"labeling_project_id": mapping.labeling_project_id,
|
||||||
"name": mapping.name,
|
"name": mapping.name,
|
||||||
"description": description,
|
"description": description,
|
||||||
@@ -166,12 +169,15 @@ class DatasetMappingService:
|
|||||||
"""
|
"""
|
||||||
# Fetch dataset name
|
# Fetch dataset name
|
||||||
dataset_name = None
|
dataset_name = None
|
||||||
|
dataset_type = None
|
||||||
dataset_id = getattr(mapping, 'dataset_id', None)
|
dataset_id = getattr(mapping, 'dataset_id', None)
|
||||||
if dataset_id:
|
if dataset_id:
|
||||||
dataset_result = await self.db.execute(
|
dataset_result = await self.db.execute(
|
||||||
select(Dataset.name).where(Dataset.id == dataset_id)
|
select(Dataset.name, Dataset.dataset_type).where(Dataset.id == dataset_id)
|
||||||
)
|
)
|
||||||
dataset_name = dataset_result.scalar_one_or_none()
|
dataset_row = dataset_result.one_or_none()
|
||||||
|
if dataset_row:
|
||||||
|
dataset_name, dataset_type = dataset_row
|
||||||
|
|
||||||
# Get template_id from mapping
|
# Get template_id from mapping
|
||||||
template_id = getattr(mapping, 'template_id', None)
|
template_id = getattr(mapping, 'template_id', None)
|
||||||
@@ -211,6 +217,7 @@ class DatasetMappingService:
|
|||||||
"id": mapping.id,
|
"id": mapping.id,
|
||||||
"dataset_id": dataset_id,
|
"dataset_id": dataset_id,
|
||||||
"dataset_name": dataset_name,
|
"dataset_name": dataset_name,
|
||||||
|
"dataset_type": dataset_type,
|
||||||
"labeling_project_id": mapping.labeling_project_id,
|
"labeling_project_id": mapping.labeling_project_id,
|
||||||
"name": mapping.name,
|
"name": mapping.name,
|
||||||
"description": description,
|
"description": description,
|
||||||
|
|||||||
Reference in New Issue
Block a user