You've already forked DataMate
feat(annotation): 添加标注任务算子编排前端页面和测试算子
## 功能概述 为标注任务通用算子编排功能添加完整的前端界面,包括任务创建、列表管理、详情查看等功能,并提供测试算子用于功能验证。 ## 改动内容 ### 前端功能 #### 1. 算子编排页面 - 新增两步创建流程: - 第一步:基本信息(数据集选择、任务名称等) - 第二步:算子编排(选择算子、配置参数、预览 pipeline) - 核心文件: - frontend/src/pages/DataAnnotation/OperatorCreate/CreateTask.tsx - frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useOperatorOperations.ts - frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useDragOperators.ts - frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useCreateStepTwo.tsx #### 2. UI 组件 - 算子库(OperatorLibrary):显示可用算子,支持分类筛选 - 编排区(OperatorOrchestration):拖拽排序算子 - 参数面板(OperatorConfig):配置算子参数 - Pipeline 预览(PipelinePreview):预览算子链 - 核心文件:frontend/src/pages/DataAnnotation/OperatorCreate/components/ #### 3. 任务列表管理 - 在数据标注首页同一 Tab 中添加任务列表 - 支持状态筛选(pending/running/completed/failed/stopped) - 支持关键词搜索 - 支持轮询刷新 - 支持停止任务 - 支持下载结果 - 核心文件:frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskList.tsx #### 4. 任务详情抽屉 - 点击任务名打开详情抽屉 - 显示任务基本信息(名称、状态、进度、时间等) - 显示 pipeline 配置(算子链和参数) - 显示错误信息(如果失败) - 显示产物路径和下载按钮 - 核心文件:frontend/src/pages/DataAnnotation/Home/components/AutoAnnotationTaskDetailDrawer.tsx #### 5. API 集成 - 封装自动标注任务相关接口: - list:获取任务列表 - create:创建任务 - detail:获取任务详情 - delete:删除任务 - stop:停止任务 - download:下载结果 - 核心文件:frontend/src/pages/DataAnnotation/annotation.api.ts #### 6. 路由配置 - 新增路由:/data/annotation/create-auto-task - 集成到数据标注首页 - 核心文件: - frontend/src/routes/routes.ts - frontend/src/pages/DataAnnotation/Home/DataAnnotation.tsx #### 7. 算子模型增强 - 新增 runtime 字段用于标注算子筛选 - 核心文件:frontend/src/pages/OperatorMarket/operator.model.ts ### 后端功能 #### 1. 测试算子(test_annotation_marker) - 功能:在图片上绘制测试标记并输出 JSON 标注 - 用途:测试标注功能是否正常工作 - 实现文件: - runtime/ops/annotation/test_annotation_marker/process.py - runtime/ops/annotation/test_annotation_marker/metadata.yml - runtime/ops/annotation/test_annotation_marker/__init__.py #### 2. 算子注册 - 将测试算子注册到 annotation ops 包 - 添加到运行时白名单 - 核心文件: - runtime/ops/annotation/__init__.py - runtime/python-executor/datamate/auto_annotation_worker.py #### 3. 数据库初始化 - 添加测试算子到数据库 - 添加算子分类关联 - 核心文件:scripts/db/data-operator-init.sql ### 问题修复 #### 1. outputDir 默认值覆盖问题 - 问题:前端设置空字符串默认值导致 worker 无法注入真实输出目录 - 解决:过滤掉空/null 的 outputDir,确保 worker 能注入真实输出目录 - 修改位置:frontend/src/pages/DataAnnotation/OperatorCreate/hooks/useOperatorOperations.ts #### 2. targetClasses 默认值类型问题 - 问题:YOLO 算子 metadata 中 targetClasses 默认值是字符串 '[]' 而不是列表 - 解决:改为列表 [] - 修改位置:runtime/ops/annotation/image_object_detection_bounding_box/metadata.yml ## 关键特性 ### 用户体验 - 统一的算子编排界面(与数据清洗保持一致) - 直观的拖拽操作 - 实时的 pipeline 预览 - 完整的任务管理功能 ### 功能完整性 - 任务创建:两步流程,清晰明了 - 任务管理:列表展示、状态筛选、搜索 - 任务操作:停止、下载 - 任务详情:完整的信息展示 ### 可测试性 - 提供测试算子用于功能验证 - 支持快速测试标注流程 ## 验证结果 - ESLint 检查:✅ 通过 - 前端构建:✅ 通过(10.91s) - 功能测试:✅ 所有功能正常 ## 部署说明 1. 执行数据库初始化脚本(如果是新环境) 2. 重启前端服务 3. 重启后端服务(如果修改了 worker 白名单) ## 使用说明 1. 进入数据标注页面 2. 点击创建自动标注任务 3. 选择数据集和文件 4. 从算子库拖拽算子到编排区 5. 配置算子参数 6. 预览 pipeline 7. 提交任务 8. 在任务列表中查看进度 9. 点击任务名查看详情 10. 下载标注结果 ## 相关文件 - 前端页面:frontend/src/pages/DataAnnotation/OperatorCreate/ - 任务管理:frontend/src/pages/DataAnnotation/Home/components/ - API 集成:frontend/src/pages/DataAnnotation/annotation.api.ts - 测试算子:runtime/ops/annotation/test_annotation_marker/ - 数据库脚本:scripts/db/data-operator-init.sql
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
"""Annotation-related operators (e.g. YOLO detection)."""
|
||||
|
||||
from . import image_object_detection_bounding_box
|
||||
from . import test_annotation_marker
|
||||
|
||||
__all__ = [
|
||||
"image_object_detection_bounding_box",
|
||||
"test_annotation_marker",
|
||||
]
|
||||
|
||||
@@ -40,7 +40,7 @@ settings:
|
||||
name: '目标类别'
|
||||
description: 'COCO 类别 ID 列表;为空表示全部类别。'
|
||||
type: 'input'
|
||||
defaultVal: '[]'
|
||||
defaultVal: []
|
||||
outputDir:
|
||||
name: '输出目录'
|
||||
description: '算子输出目录(由运行时注入)。'
|
||||
|
||||
12
runtime/ops/annotation/test_annotation_marker/__init__.py
Normal file
12
runtime/ops/annotation/test_annotation_marker/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Test annotation marker operator package."""
|
||||
|
||||
from datamate.core.base_op import OPERATORS
|
||||
|
||||
from .process import test_annotation_marker
|
||||
|
||||
OPERATORS.register_module(
|
||||
module_name="test_annotation_marker",
|
||||
module_path="ops.annotation.test_annotation_marker.process",
|
||||
)
|
||||
|
||||
__all__ = ["test_annotation_marker"]
|
||||
37
runtime/ops/annotation/test_annotation_marker/metadata.yml
Normal file
37
runtime/ops/annotation/test_annotation_marker/metadata.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
name: '测试标注标记'
|
||||
name_en: 'Test Annotation Marker'
|
||||
description: '系统测试算子:在图片上添加测试标记并输出对应标注文件。'
|
||||
description_en: 'System testing operator: draw test marker on images and emit annotation json.'
|
||||
language: 'python'
|
||||
vendor: 'datamate'
|
||||
raw_id: 'test_annotation_marker'
|
||||
version: '1.0.0'
|
||||
types:
|
||||
- 'annotation'
|
||||
modal: 'image'
|
||||
inputs: 'image'
|
||||
outputs: 'image'
|
||||
settings:
|
||||
markerText:
|
||||
name: '标记文本'
|
||||
description: '绘制在图片上的测试标记文本。'
|
||||
type: 'input'
|
||||
defaultVal: 'TEST_ANNOTATION'
|
||||
markerColor:
|
||||
name: '标记颜色'
|
||||
description: '标记颜色(OpenCV BGR,格式如 0,255,0)。'
|
||||
type: 'input'
|
||||
defaultVal: '0,255,0'
|
||||
markerThickness:
|
||||
name: '线宽'
|
||||
description: '标记框线宽。'
|
||||
type: 'inputNumber'
|
||||
defaultVal: 2
|
||||
min: 1
|
||||
max: 10
|
||||
step: 1
|
||||
outputDir:
|
||||
name: '输出目录'
|
||||
description: '算子输出目录(由运行时注入)。'
|
||||
type: 'input'
|
||||
defaultVal: ''
|
||||
122
runtime/ops/annotation/test_annotation_marker/process.py
Normal file
122
runtime/ops/annotation/test_annotation_marker/process.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
import cv2
|
||||
from loguru import logger
|
||||
|
||||
from datamate.core.base_op import Mapper
|
||||
|
||||
|
||||
class test_annotation_marker(Mapper):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(test_annotation_marker, self).__init__(*args, **kwargs)
|
||||
self._marker_text = str(kwargs.get("markerText", "TEST_ANNOTATION"))
|
||||
self._marker_color = self._parse_color(kwargs.get("markerColor", "0,255,0"))
|
||||
self._marker_thickness = int(kwargs.get("markerThickness", 2) or 2)
|
||||
self._output_dir = kwargs.get("outputDir")
|
||||
|
||||
@staticmethod
|
||||
def _parse_color(value: Any) -> Tuple[int, int, int]:
|
||||
if isinstance(value, (list, tuple)) and len(value) >= 3:
|
||||
try:
|
||||
bgr = tuple(int(max(min(float(item), 255), 0)) for item in value[:3])
|
||||
return bgr # type: ignore[return-value]
|
||||
except Exception:
|
||||
return 0, 255, 0
|
||||
|
||||
if isinstance(value, str):
|
||||
parts = [part.strip() for part in value.split(",")]
|
||||
if len(parts) >= 3:
|
||||
try:
|
||||
bgr = tuple(int(max(min(float(item), 255), 0)) for item in parts[:3])
|
||||
return bgr # type: ignore[return-value]
|
||||
except Exception:
|
||||
return 0, 255, 0
|
||||
|
||||
return 0, 255, 0
|
||||
|
||||
def execute(self, sample: Dict[str, Any]) -> Dict[str, Any]:
|
||||
image_path = sample.get(self.image_key) or sample.get("image")
|
||||
if not image_path or not os.path.exists(image_path):
|
||||
logger.warning("test_annotation_marker: image not found: {}", image_path)
|
||||
return sample
|
||||
|
||||
image = cv2.imread(image_path)
|
||||
if image is None:
|
||||
logger.warning("test_annotation_marker: failed to read image: {}", image_path)
|
||||
return sample
|
||||
|
||||
image_height, image_width = image.shape[:2]
|
||||
|
||||
margin = max(min(image_width, image_height) // 20, 10)
|
||||
x1, y1 = margin, margin
|
||||
x2, y2 = image_width - margin, image_height - margin
|
||||
|
||||
cv2.rectangle(
|
||||
image,
|
||||
(x1, y1),
|
||||
(x2, y2),
|
||||
self._marker_color,
|
||||
self._marker_thickness,
|
||||
)
|
||||
|
||||
cv2.putText(
|
||||
image,
|
||||
self._marker_text,
|
||||
(x1, max(y1 - 10, 20)),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.8,
|
||||
self._marker_color,
|
||||
max(self._marker_thickness, 1),
|
||||
cv2.LINE_AA,
|
||||
)
|
||||
|
||||
if self._output_dir and os.path.exists(self._output_dir):
|
||||
output_dir = self._output_dir
|
||||
else:
|
||||
output_dir = os.path.dirname(image_path)
|
||||
|
||||
images_dir = os.path.join(output_dir, "images")
|
||||
annotations_dir = os.path.join(output_dir, "annotations")
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
os.makedirs(annotations_dir, exist_ok=True)
|
||||
|
||||
base_name = os.path.basename(image_path)
|
||||
name_without_ext = os.path.splitext(base_name)[0]
|
||||
|
||||
output_image_path = os.path.join(images_dir, base_name)
|
||||
output_json_path = os.path.join(annotations_dir, f"{name_without_ext}.json")
|
||||
|
||||
cv2.imwrite(output_image_path, image)
|
||||
|
||||
annotations = {
|
||||
"image": base_name,
|
||||
"width": image_width,
|
||||
"height": image_height,
|
||||
"marker": {
|
||||
"text": self._marker_text,
|
||||
"color_bgr": list(self._marker_color),
|
||||
"thickness": self._marker_thickness,
|
||||
},
|
||||
"detections": [
|
||||
{
|
||||
"label": self._marker_text,
|
||||
"class_id": -1,
|
||||
"confidence": 1.0,
|
||||
"bbox_xyxy": [x1, y1, x2, y2],
|
||||
"bbox_xywh": [x1, y1, x2 - x1, y2 - y1],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
with open(output_json_path, "w", encoding="utf-8") as file:
|
||||
json.dump(annotations, file, indent=2, ensure_ascii=False)
|
||||
|
||||
sample["output_image"] = output_image_path
|
||||
sample["annotations_file"] = output_json_path
|
||||
sample["annotations"] = annotations
|
||||
sample["detection_count"] = 1
|
||||
return sample
|
||||
@@ -122,7 +122,7 @@ DEFAULT_OUTPUT_ROOT = os.getenv(
|
||||
|
||||
DEFAULT_OPERATOR_WHITELIST = os.getenv(
|
||||
"AUTO_ANNOTATION_OPERATOR_WHITELIST",
|
||||
"ImageObjectDetectionBoundingBox",
|
||||
"ImageObjectDetectionBoundingBox,test_annotation_marker",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user