From 250a13ff70d52a7dcbd330caf60b385c75fcb4f4 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sun, 25 Jan 2026 18:35:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(annotation):=20=E6=94=AF=E6=8C=81=E5=9B=BE?= =?UTF-8?q?=E5=83=8F=E6=A0=87=E6=B3=A8=E9=A1=B9=E7=9B=AE=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=86=85=E7=BD=AE=E6=A0=87=E6=B3=A8=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展标注编辑器支持 TEXT/IMAGE 数据类型 - 添加三个内置图像标注模板:目标检测、语义分割(掩码)、语义分割(多边形) - 实现内置标注模板的数据库初始化功能 - 集成标注配置验证和模板管理服务 - 更新项目不支持提示信息以反映新的数据类型支持 --- .../Annotate/LabelStudioTextEditor.tsx | 2 +- runtime/datamate-python/app/main.py | 11 +- .../app/module/annotation/interface/editor.py | 2 +- .../annotation/service/builtin_templates.py | 154 ++++++++++++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 runtime/datamate-python/app/module/annotation/service/builtin_templates.py diff --git a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx index 2bbdd9f..0196dfa 100644 --- a/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx +++ b/frontend/src/pages/DataAnnotation/Annotate/LabelStudioTextEditor.tsx @@ -621,7 +621,7 @@ export default function LabelStudioTextEditor() { 暂不支持该数据类型 - {project.unsupportedReason || "当前仅支持文本(TEXT)项目的内嵌编辑器。"} + {project.unsupportedReason || "当前仅支持 TEXT/IMAGE 项目的内嵌编辑器。"}
diff --git a/runtime/datamate-python/app/main.py b/runtime/datamate-python/app/main.py index efab3aa..5333f74 100644 --- a/runtime/datamate-python/app/main.py +++ b/runtime/datamate-python/app/main.py @@ -9,6 +9,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from .core.config import settings from .core.logging import setup_logging, get_logger from .db.session import AsyncSessionLocal +from .module.annotation.service.builtin_templates import ensure_builtin_annotation_templates from .exception import ( starlette_http_exception_handler, fastapi_http_exception_handler, @@ -31,7 +32,15 @@ async def lifespan(app: FastAPI): try: async with AsyncSessionLocal() as session: await session.execute(text("SELECT 1")) - logger.info(f"Database: mysql+aiomysql://{'*' * len(settings.mysql_user)}:{'*' * len(settings.mysql_password)}@{settings.mysql_host}:{settings.mysql_port}/{settings.mysql_database}") + logger.info(f"Database: mysql+aiomysql://{'*' * len(settings.mysql_user)}:{'*' * len(settings.mysql_password)}@{settings.mysql_host}:{settings.mysql_port}/{settings.mysql_database}") + try: + inserted = await ensure_builtin_annotation_templates(session) + if inserted > 0: + logger.info(f"内置标注模板初始化完成,新增 {inserted} 条") + else: + logger.info("内置标注模板已存在,跳过初始化") + except Exception as e: + logger.error(f"内置标注模板初始化失败: {e}") except Exception as e: logger.error(f"Database connection validation failed: {e}") logger.debug(f"Connection details: {settings.database_url}") diff --git a/runtime/datamate-python/app/module/annotation/interface/editor.py b/runtime/datamate-python/app/module/annotation/interface/editor.py index 97d5a43..316ce6b 100644 --- a/runtime/datamate-python/app/module/annotation/interface/editor.py +++ b/runtime/datamate-python/app/module/annotation/interface/editor.py @@ -4,7 +4,7 @@ Label Studio Editor(前端嵌入式)接口 说明: - 不依赖 Label Studio Server;仅复用其“编辑器”前端库 - DataMate 负责提供 tasks/annotations 数据与保存能力 -- 当前为 TEXT POC:只支持 dataset_type=TEXT 的项目 +- 当前支持 dataset_type=TEXT/IMAGE 的项目 """ from __future__ import annotations diff --git a/runtime/datamate-python/app/module/annotation/service/builtin_templates.py b/runtime/datamate-python/app/module/annotation/service/builtin_templates.py new file mode 100644 index 0000000..3c3b9e0 --- /dev/null +++ b/runtime/datamate-python/app/module/annotation/service/builtin_templates.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import List + +from sqlalchemy import select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.logging import get_logger +from app.db.models.annotation_management import AnnotationTemplate +from app.module.annotation.utils.config_validator import LabelStudioConfigValidator + +logger = get_logger(__name__) + +DATA_TYPE_IMAGE = "image" +CATEGORY_COMPUTER_VISION = "computer-vision" +STYLE_HORIZONTAL = "horizontal" +VERSION_DEFAULT = "1.0.0" + +OBJECT_DETECTION_LABEL_CONFIG = """ + + + +""" + +SEMANTIC_SEGMENTATION_MASK_LABEL_CONFIG = """ + + + +""" + +SEMANTIC_SEGMENTATION_POLYGON_LABEL_CONFIG = """ +
+ + + +""" + + +@dataclass(frozen=True) +class BuiltInTemplateDefinition: + id: str + name: str + description: str + data_type: str + labeling_type: str + label_config: str + style: str + category: str + version: str + + +BUILT_IN_TEMPLATES: List[BuiltInTemplateDefinition] = [ + BuiltInTemplateDefinition( + id="tpl-object-detection-001", + name="目标检测(边界框)", + description=( + "在目标周围绘制边界框,适用于自动驾驶、交通监控、安防监控、零售分析等场景。" + "关联模型:YOLO、R-CNN、SSD" + ), + data_type=DATA_TYPE_IMAGE, + labeling_type="object-detection", + label_config=OBJECT_DETECTION_LABEL_CONFIG, + style=STYLE_HORIZONTAL, + category=CATEGORY_COMPUTER_VISION, + version=VERSION_DEFAULT, + ), + BuiltInTemplateDefinition( + id="tpl-semantic-segmentation-mask-001", + name="语义分割(掩码)", + description=( + "使用画笔工具在目标周围绘制掩码,适用于自动驾驶、医学图像分析、卫星图像分析等场景。" + "关联模型:U-Net、DeepLab、Mask R-CNN" + ), + data_type=DATA_TYPE_IMAGE, + labeling_type="semantic-segmentation-mask", + label_config=SEMANTIC_SEGMENTATION_MASK_LABEL_CONFIG, + style=STYLE_HORIZONTAL, + category=CATEGORY_COMPUTER_VISION, + version=VERSION_DEFAULT, + ), + BuiltInTemplateDefinition( + id="tpl-semantic-segmentation-polygon-001", + name="语义分割(多边形)", + description=( + "在目标周围绘制多边形,适用于自动驾驶、医学图像、卫星图像、精准农业等场景。" + "关联模型:DeepLab、PSPNet、U-Net" + ), + data_type=DATA_TYPE_IMAGE, + labeling_type="semantic-segmentation-polygon", + label_config=SEMANTIC_SEGMENTATION_POLYGON_LABEL_CONFIG, + style=STYLE_HORIZONTAL, + category=CATEGORY_COMPUTER_VISION, + version=VERSION_DEFAULT, + ), +] + +assert len({template.id for template in BUILT_IN_TEMPLATES}) == len(BUILT_IN_TEMPLATES), ( + "内置模板ID不能重复" +) + + +async def ensure_builtin_annotation_templates(db: AsyncSession) -> int: + inserted = 0 + + for template in BUILT_IN_TEMPLATES: + label_config = template.label_config.strip() + assert label_config, f"内置模板 {template.id} 的 label_config 不能为空" + + valid, error = LabelStudioConfigValidator.validate_xml(label_config) + if not valid: + raise ValueError(f"内置模板 {template.id} 的 label_config 无效: {error}") + + result = await db.execute( + select(AnnotationTemplate).where(AnnotationTemplate.id == template.id) + ) + existing = result.scalar_one_or_none() + if existing: + continue + + record = AnnotationTemplate( + id=template.id, + name=template.name, + description=template.description, + data_type=template.data_type, + labeling_type=template.labeling_type, + configuration=None, + label_config=label_config, + style=template.style, + category=template.category, + built_in=True, + version=template.version, + created_at=datetime.now(), + ) + db.add(record) + + try: + await db.commit() + inserted += 1 + except IntegrityError: + await db.rollback() + logger.warning(f"内置模板已存在,跳过插入: {template.id}") + except Exception: + await db.rollback() + logger.exception(f"写入内置模板失败: {template.id}") + raise + + return inserted