feat(annotation): 支持图像标注项目并添加内置标注模板

- 扩展标注编辑器支持 TEXT/IMAGE 数据类型
- 添加三个内置图像标注模板:目标检测、语义分割(掩码)、语义分割(多边形)
- 实现内置标注模板的数据库初始化功能
- 集成标注配置验证和模板管理服务
- 更新项目不支持提示信息以反映新的数据类型支持
This commit is contained in:
2026-01-25 18:35:07 +08:00
parent c5ace0c4cc
commit 250a13ff70
4 changed files with 166 additions and 3 deletions

View File

@@ -621,7 +621,7 @@ export default function LabelStudioTextEditor() {
<Card style={{ maxWidth: 640 }}>
<Typography.Title level={4}></Typography.Title>
<Typography.Paragraph type="secondary">
{project.unsupportedReason || "当前仅支持文本(TEXT项目的内嵌编辑器。"}
{project.unsupportedReason || "当前仅支持 TEXT/IMAGE 项目的内嵌编辑器。"}
</Typography.Paragraph>
<div className="flex justify-end gap-2">
<Button onClick={() => navigate("/data/annotation")}></Button>

View File

@@ -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}")

View File

@@ -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

View File

@@ -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 = """<View>
<Image name=\"image\" value=\"$image\"/>
<RectangleLabels name=\"label\" toName=\"image\">
<Label value=\"Airplane\" background=\"green\"/>
<Label value=\"Car\" background=\"blue\"/>
</RectangleLabels>
</View>"""
SEMANTIC_SEGMENTATION_MASK_LABEL_CONFIG = """<View>
<Image name=\"image\" value=\"$image\" zoom=\"true\"/>
<BrushLabels name=\"tag\" toName=\"image\">
<Label value=\"Airplane\" background=\"rgba(255, 0, 0, 0.7)\"/>
<Label value=\"Car\" background=\"rgba(0, 0, 255, 0.7)\"/>
</BrushLabels>
</View>"""
SEMANTIC_SEGMENTATION_POLYGON_LABEL_CONFIG = """<View>
<Header value=\"选择标签并点击图像开始\"/>
<Image name=\"image\" value=\"$image\" zoom=\"true\"/>
<PolygonLabels name=\"label\" toName=\"image\" strokeWidth=\"3\" pointSize=\"small\" opacity=\"0.9\">
<Label value=\"Airplane\" background=\"red\"/>
<Label value=\"Car\" background=\"blue\"/>
</PolygonLabels>
</View>"""
@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