如何利用VIA的JSON标注文件进行数据可视化?Python实战教程

在计算机视觉和机器学习项目中,数据标注是构建高质量模型的第一步。当你花费数小时甚至数天,使用VGG Image Annotator (VIA) 在图像上精心标记了成千上万个关键点、矩形框或多边形后,那些辛勤劳动的成果最终都凝结在一个.json文件里。然而,这个文件本身只是一串冰冷的代码,如何让它“开口说话”,直观地展示你的标注质量、分布规律,甚至发现潜在的标注错误?这正是数据可视化大显身手的舞台。

对于数据科学家和算法工程师而言,将VIA的JSON标注文件转化为直观的图表,远不止是“画个图”那么简单。它是一个至关重要的数据验证和探索环节。通过Python,我们可以深度解析JSON结构,提取坐标信息,并利用Matplotlib、OpenCV等库,将抽象的坐标数据还原为覆盖在原始图像上的可视化标注。这个过程不仅能帮你快速复查标注一致性,还能为后续的数据增强、样本均衡分析提供直观依据。本文将带你从零开始,手把手实现一套高效、可复用的VIA标注可视化流程,让你真正“看见”你的数据。

1. 深度解析VIA JSON文件结构

在动手写代码之前,我们必须像侦探一样,彻底搞清楚VIA输出的JSON文件里到底藏了些什么。很多人在解析时遇到的第一个坑,就是被其看似复杂的嵌套结构吓退,或者错误地理解了数据的组织方式。

一个典型的VIA标注JSON文件,其核心是一个以图像文件名(或唯一ID)为键的字典。每个键对应的值,则包含了该图像的所有元信息和标注区域信息。我们来看一个比原始资料更复杂的例子,它可能包含多种形状:

{
  "image1.jpg": {
    "filename": "image1.jpg",
    "size": 204800,
    "regions": [
      {
        "shape_attributes": {
          "name": "point",
          "cx": 100,
          "cy": 150
        },
        "region_attributes": {
          "class": "person",
          "visibility": "clear"
        }
      },
      {
        "shape_attributes": {
          "name": "rect",
          "x": 50,
          "y": 60,
          "width": 120,
          "height": 80
        },
        "region_attributes": {
          "class": "car"
        }
      },
      {
        "shape_attributes": {
          "name": "polygon",
          "all_points_x": [200, 220, 240, 230],
          "all_points_y": [300, 320, 310, 290]
        },
        "region_attributes": {}
      }
    ],
    "file_attributes": {
      "source": "camera_trap",
      "weather": "sunny"
    }
  }
}

注意:VIA允许用户自定义region_attributesfile_attributes,这意味着你的JSON结构可能会根据标注任务的不同而有所变化。一个健壮的解析器必须能灵活处理这些可选字段。

从上面的结构可以看出,关键信息位于regions列表中。每个region对象包含两个主要部分:

  • shape_attributes: 定义了标注的几何形状和具体参数(如点的坐标、矩形的左上角及宽高、多边形的点序列)。
  • region_attributes: 以字典形式存储用户为该区域定义的属性(如物体类别、状态等),这是一个非常灵活的字段,可能为空,也可能包含多个键值对。

理解这个结构后,我们就可以用Python的json库轻松将其加载到内存中。但更重要的是,我们需要设计一个清晰的数据模型来承载这些信息,而不是每次都去操作原始的、深度嵌套的字典。这能极大提升后续代码的可读性和可维护性。

2. 构建Python数据模型与解析器

直接操作原始的、深度嵌套的字典和列表不仅代码冗长,而且容易出错。更好的做法是构建一个轻量级的面向对象模型,将JSON数据映射为Python对象。这样做的好处是代码意图清晰,属性访问方便,并且易于扩展。

首先,我们定义几个数据类来代表不同的标注形状。这里使用Python的dataclass装饰器,它能自动生成__init__等方法,非常简洁。

from dataclasses import dataclass
from typing import List, Dict, Any, Optional

@dataclass
class Point:
    """表示一个点标注"""
    cx: int
    cy: int

@dataclass
class Rectangle:
    """表示一个矩形标注"""
    x: int  # 左上角x坐标
    y: int  # 左上角y坐标
    width: int
    height: int

@dataclass
class Polygon:
    """表示一个多边形标注"""
    all_points_x: List[int]
    all_points_y: List[int]

# 使用Union类型表示可能是任何一种形状
Shape = Point | Rectangle | Polygon

接下来,我们定义核心的AnnotationRegionVIAImageAnnotation类。

@dataclass
class AnnotationRegion:
    """表示图像中的一个标注区域"""
    shape: Shape  # 几何形状
    attributes: Dict[str, Any]  # 区域属性,如类别标签

@dataclass
class VIAImageAnnotation:
    """表示一张图像的所有标注信息"""
    file_id: str  # JSON中的键,通常是文件名
    filename: str
    size: int
    regions: List[AnnotationRegion]
    file_attributes: Dict[str, Any]  # 文件级属性

有了数据模型,我们就可以编写解析函数,将原始的JSON字典转化为这些对象的集合。这个解析器的核心是一个shape_factory函数,它根据shape_attributes中的name字段,创建对应的Shape对象。

import json

def parse_via_json(json_path: str) -> Dict[str, VIAImageAnnotation]:
    """
    解析VIA JSON文件,返回一个以file_id为键的字典。
    """
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    annotations = {}
    for file_id, img_info in data.items():
        regions = []
        for region in img_info.get('regions', []):
            shape_attr = region['shape_attributes']
            shape_name = shape_attr['name']

            shape_obj = None
            if shape_name == 'point':
                shape_obj = Point(cx=shape_attr['cx'], cy=shape_attr['cy'])
            elif shape_name == 'rect':
                shape_obj = Rectangle(
                    x=shape_attr['x'],
                    y=shape_attr['y'],
                    width=shape_attr['width'],
                    height=shape_attr['height']
                )
            elif shape_name == 'polygon':
                shape_obj = Polygon(
                    all_points_x=shape_attr['all_points_x'],
                    all_points_y=shape_attr['all_points_y']
                )
            else:
                # 处理其他形状或跳过
                continue

            region_obj = AnnotationRegion(
                shape=shape_obj,
                attributes=region.get('region_attributes', {})
            )
            regions.append(region_obj)

        img_annotation = VIAImageAnnotation(
            file_id=file_id,
            filename=img_info['filename'],
            size=img_info['size'],
            regions=regions,
            file_attributes=img_info.get('file_attributes', {})
        )
        annotations[file_id] = img_annotation

    return annotations

提示:在实际项目中,你可能会遇到VIA旧版本或其他标注工具导出的不同格式。一个好的实践是在解析函数开始时添加一个版本或格式校验,并做好异常处理,让脚本更加鲁棒。

通过这种方式,我们成功将杂乱的JSON数据转换成了结构清晰、类型明确的Python对象。接下来,我们就可以基于这些对象进行各种分析和可视化了。

3. 使用Matplotlib实现基础可视化

将标注数据在图像上绘制出来,是最直接、最有效的质检方式。Matplotlib是Python生态中最著名的绘图库,虽然其初衷是绘制科学图表,但其imshow和丰富的绘图API也使其成为图像标注可视化的得力工具。

假设我们已经用上节的解析器加载了标注数据,并有一张对应的图片image_path。基础的可视化步骤如下:

  1. 读取图像:使用OpenCV或PIL读取图片。注意OpenCV默认以BGR通道顺序读取,而Matplotlib显示需要RGB顺序。
  2. 创建画布:使用Matplotlib创建图形和坐标轴。
  3. 显示图像:使用ax.imshow()显示背景图。
  4. 遍历并绘制标注:根据每个AnnotationRegionshape类型,调用不同的绘图函数。
  5. 美化与显示:添加标题、关闭坐标轴、调整布局等。

下面是一个集成的可视化函数示例:

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from PIL import Image
import numpy as np

def visualize_annotations(image_path: str, annotation: VIAImageAnnotation, save_path: Optional[str] = None):
    """
    可视化单张图像的标注结果。
    
    Args:
        image_path: 原始图像路径。
        annotation: VIAImageAnnotation对象,包含该图所有标注。
        save_path: 可选,如果提供,则将可视化结果保存至此路径。
    """
    # 1. 读取图像
    img = np.array(Image.open(image_path))
    
    # 2. 创建画布
    fig, ax = plt.subplots(1, figsize=(12, 8))
    ax.axis('off')  # 关闭坐标轴
    ax.set_title(f"Visualization: {annotation.filename}", fontsize=14)
    
    # 3. 显示背景图
    ax.imshow(img)
    
    # 4. 遍历并绘制所有标注区域
    colors = plt.cm.tab10(np.linspace(0, 1, len(annotation.regions)))  # 为不同区域分配不同颜色
    for idx, region in enumerate(annotation.regions):
        shape = region.shape
        attr = region.attributes
        color = colors[idx % len(colors)]
        
        # 根据形状类型绘图
        if isinstance(shape, Point):
            # 绘制点,用一个圆圈表示
            circle = patches.Circle((shape.cx, shape.cy), radius=5, linewidth=2, 
                                    edgecolor=color, facecolor=color, alpha=0.6)
            ax.add_patch(circle)
            # 可选:在点旁边添加标签(如类别)
            label = attr.get('class', str(idx))
            ax.text(shape.cx + 7, shape.cy - 7, label, color='white', 
                    fontsize=9, bbox=dict(boxstyle="round,pad=0.3", facecolor=color, alpha=0.8))
            
        elif isinstance(shape, Rectangle):
            # 绘制矩形框
            rect = patches.Rectangle((shape.x, shape.y), shape.width, shape.height, 
                                     linewidth=2, edgecolor=color, facecolor='none')
            ax.add_patch(rect)
            # 在框的左上角添加标签
            label = attr.get('class', str(idx))
            ax.text(shape.x, shape.y - 5, label, color=color, 
                    fontsize=10, weight='bold', 
                    bbox=dict(boxstyle="round,pad=0.3", facecolor='white', alpha=0.7))
            
        elif isinstance(shape, Polygon):
            # 绘制多边形,需要将点列表组合成 (N, 2) 的数组
            points = list(zip(shape.all_points_x, shape.all_points_y))
            polygon = patches.Polygon(points, linewidth=2, 
                                      edgecolor=color, facecolor=color, alpha=0.3)
            ax.add_patch(polygon)
            # 在多边形中心添加标签
            if points:
                center_x = np.mean(shape.all_points_x)
                center_y = np.mean(shape.all_points_y)
                label = attr.get('class', str(idx))
                ax.text(center_x, center_y, label, color='white', 
                        fontsize=9, ha='center', va='center',
                        bbox=dict(boxstyle="round,pad=0.3", facecolor=color, alpha=0.8))
    
    plt.tight_layout()
    
    if save_path:
        plt.savefig(save_path, dpi=150, bbox_inches='tight')
        print(f"可视化结果已保存至: {save_path}")
    else:
        plt.show()
    plt.close(fig)  # 关闭图形,释放内存

这个函数提供了基础的可视化能力。但在实际项目中,你可能会遇到图像尺寸巨大、标注密集的情况,直接显示可能会卡顿或不清晰。一个技巧是,在imshow之前,可以先按比例缩小图像和所有坐标进行计算和显示,而在保存高清图时,再使用原图尺寸和坐标。

4. 高级分析与统计图表绘制

可视化不仅仅是把框画在图上。对于项目管理者或想要深入理解数据分布的人来说,基于标注数据的统计图表能提供更高维度的洞察。我们可以利用解析好的数据,轻松生成以下几类分析图表:

4.1 标注类别分布图 这是最常用的分析之一,可以让你一眼看出数据集中各类别的样本是否均衡。

import pandas as pd
from collections import Counter

def plot_class_distribution(annotations: Dict[str, VIAImageAnnotation]):
    """
    绘制数据集中所有标注类别的分布柱状图。
    """
    all_classes = []
    for ann in annotations.values():
        for region in ann.regions:
            # 假设类别信息存储在region.attributes的‘class’键中
            class_name = region.attributes.get('class', 'unknown')
            all_classes.append(class_name)
    
    class_counts = Counter(all_classes)
    
    # 转换为DataFrame便于排序和绘图
    df = pd.DataFrame.from_dict(class_counts, orient='index', columns=['count']).reset_index()
    df.columns = ['class', 'count']
    df = df.sort_values('count', ascending=False)
    
    fig, ax = plt.subplots(figsize=(10, 6))
    bars = ax.bar(df['class'], df['count'], color=plt.cm.Set3(np.arange(len(df))))
    ax.set_xlabel('Object Class')
    ax.set_ylabel('Annotation Count')
    ax.set_title('Distribution of Annotation Classes Across Dataset')
    ax.tick_params(axis='x', rotation=45)  # 旋转x轴标签防止重叠
    
    # 在柱子上方添加数量标签
    for bar in bars:
        height = bar.get_height()
        ax.text(bar.get_x() + bar.get_width()/2., height + 0.5,
                f'{int(height)}', ha='center', va='bottom', fontsize=9)
    
    plt.tight_layout()
    plt.show()

4.2 标注区域尺寸分布 对于目标检测任务,了解标注框的尺寸分布(宽度、高度、面积)至关重要,这直接影响着你如何设计模型的锚框(Anchor)尺寸。

def plot_bbox_size_distribution(annotations: Dict[str, VIAImageAnnotation], img_width: int, img_height: int):
    """
    分析并绘制矩形标注框的尺寸分布(归一化到图像尺寸)。
    """
    widths, heights, areas = [], [], []
    for ann in annotations.values():
        for region in ann.regions:
            if isinstance(region.shape, Rectangle):
                rect = region.shape
                # 计算归一化尺寸(相对于图像尺寸的比例)
                norm_width = rect.width / img_width
                norm_height = rect.height / img_height
                norm_area = norm_width * norm_height
                
                widths.append(norm_width)
                heights.append(norm_height)
                areas.append(norm_area)
    
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    
    axes[0].hist(widths, bins=50, edgecolor='black', alpha=0.7)
    axes[0].set_xlabel('Normalized Width')
    axes[0].set_ylabel('Frequency')
    axes[0].set_title('Bounding Box Width Distribution')
    
    axes[1].hist(heights, bins=50, edgecolor='black', alpha=0.7)
    axes[1].set_xlabel('Normalized Height')
    axes[1].set_title('Bounding Box Height Distribution')
    
    axes[2].hist(areas, bins=50, edgecolor='black', alpha=0.7)
    axes[2].set_xlabel('Normalized Area')
    axes[2].set_title('Bounding Box Area Distribution')
    
    plt.tight_layout()
    plt.show()

4.3 每张图像标注数量分布 这个分析可以帮助你发现那些标注异常密集或异常稀疏的图像,可能指向需要重点检查的样本。

def plot_annotations_per_image(annotations: Dict[str, VIAImageAnnotation]):
    """
    绘制每张图像中标注数量的分布直方图。
    """
    counts_per_image = [len(ann.regions) for ann in annotations.values()]
    
    fig, ax = plt.subplots(figsize=(10, 5))
    ax.hist(counts_per_image, bins=range(0, max(counts_per_image)+2, 1), 
            edgecolor='black', alpha=0.7, align='left')
    ax.set_xlabel('Number of Annotations per Image')
    ax.set_ylabel('Number of Images')
    ax.set_title('Distribution of Annotation Counts Across Images')
    ax.set_xticks(range(0, max(counts_per_image)+1, max(1, max(counts_per_image)//10)))
    plt.grid(axis='y', alpha=0.3)
    plt.tight_layout()
    plt.show()
    
    # 输出一些统计信息
    print(f"总图像数: {len(counts_per_image)}")
    print(f"总标注数: {sum(counts_per_image)}")
    print(f"平均每图标注数: {np.mean(counts_per_image):.2f}")
    print(f"标注数中位数: {np.median(counts_per_image)}")
    print(f"标注数标准差: {np.std(counts_per_image):.2f}")

将这些统计图表与之前的单图可视化结合,你就能从宏观到微观,全面把控数据标注的质量和特征。例如,你可能会发现“汽车”类别的标注框普遍宽高比接近1.8,而“行人”的标注框则更为瘦高,这些信息对于模型训练前的数据预处理和增强策略选择极具价值。

5. 工程化实践:构建可复用的可视化流水线

当项目从探索阶段进入生产阶段,我们需要的不再是零散的脚本,而是一个可靠、可配置、可扩展的可视化流水线。这一章,我们将把前面所有的模块组装起来,并加入日志、配置管理和批处理能力。

5.1 项目结构设计 一个清晰的项目结构是工程化的第一步。建议按如下方式组织你的代码:

via_visualization_pipeline/
├── config.yaml            # 配置文件,存储路径、颜色方案等
├── requirements.txt       # 项目依赖
├── src/
│   ├── __init__.py
│   ├── data_models.py    # 存放Point, Rectangle, VIAImageAnnotation等数据类
│   ├── json_parser.py    # 存放解析JSON的代码
│   ├── visualizers.py    # 存放单图可视化、统计绘图等函数
│   └── pipeline.py       # 主流水线,协调所有模块
└── run_pipeline.py       # 主运行脚本

5.2 使用配置文件 使用YAML或JSON配置文件来管理路径和参数,避免将硬编码散落在脚本中。例如,config.yaml

paths:
  json_annotation: "/path/to/your/project/annotations.json"
  image_root_dir: "/path/to/your/project/images/"
  output_dir: "./visualization_output"

visualization:
  single_image:
    dpi: 150
    figsize: [12, 8]
    point_radius: 5
    bbox_linewidth: 2
    polygon_alpha: 0.3
  statistics:
    class_dist_figsize: [10, 6]
    bbox_hist_bins: 50

logging:
  level: "INFO"
  file: "./pipeline.log"

然后在pipeline.py中读取配置:

import yaml
import logging
from pathlib import Path

def setup_pipeline(config_path: str):
    with open(config_path, 'r') as f:
        config = yaml.safe_load(f)
    
    # 设置日志
    logging.basicConfig(
        level=getattr(logging, config['logging']['level']),
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler(config['logging']['file']),
            logging.StreamHandler()
        ]
    )
    logger = logging.getLogger(__name__)
    
    # 创建输出目录
    output_dir = Path(config['paths']['output_dir'])
    output_dir.mkdir(parents=True, exist_ok=True)
    
    return config, logger

5.3 批处理与报告生成 主流水线应该能够遍历整个数据集,为每张图片生成可视化结果,并汇总生成一份HTML或PDF格式的分析报告。

from src.json_parser import parse_via_json
from src.visualizers import visualize_annotations, plot_class_distribution, plot_annotations_per_image
import pandas as pd
from jinja2 import Template

def run_batch_visualization(config, logger):
    """运行批处理可视化流水线"""
    # 1. 解析标注
    logger.info(f"开始解析标注文件: {config['paths']['json_annotation']}")
    all_annotations = parse_via_json(config['paths']['json_annotation'])
    logger.info(f"解析完成,共 {len(all_annotations)} 张图像。")
    
    # 2. 为每张图像生成可视化
    image_root = Path(config['paths']['image_root_dir'])
    output_dir = Path(config['paths']['output_dir']) / 'single_images'
    output_dir.mkdir(exist_ok=True)
    
    success_count = 0
    error_list = []
    
    for file_id, annotation in all_annotations.items():
        image_path = image_root / annotation.filename
        if not image_path.exists():
            logger.warning(f"图像文件不存在,跳过: {image_path}")
            error_list.append({'file_id': file_id, 'error': 'Image file not found'})
            continue
        
        save_path = output_dir / f"{Path(annotation.filename).stem}_annotated.png"
        try:
            visualize_annotations(
                str(image_path),
                annotation,
                save_path=str(save_path),
                config=config['visualization']['single_image']  # 传入配置参数
            )
            success_count += 1
        except Exception as e:
            logger.error(f"处理图像 {annotation.filename} 时出错: {e}")
            error_list.append({'file_id': file_id, 'error': str(e)})
    
    logger.info(f"单图可视化完成。成功: {success_count}, 失败: {len(error_list)}")
    
    # 3. 生成统计图表
    stats_dir = Path(config['paths']['output_dir']) / 'statistics'
    stats_dir.mkdir(exist_ok=True)
    
    # 绘制并保存类别分布图
    class_dist_fig = plot_class_distribution(all_annotations, save=False)
    class_dist_fig.savefig(stats_dir / 'class_distribution.png', dpi=150, bbox_inches='tight')
    plt.close(class_dist_fig)
    
    # 绘制并保存每图标注数分布
    count_fig = plot_annotations_per_image(all_annotations, save=False)
    count_fig.savefig(stats_dir / 'annotations_per_image.png', dpi=150, bbox_inches='tight')
    plt.close(count_fig)
    
    # 4. 生成摘要报告 (CSV格式示例)
    summary_data = []
    for file_id, ann in all_annotations.items():
        summary_data.append({
            'file_id': file_id,
            'filename': ann.filename,
            'num_regions': len(ann.regions),
            'classes': ', '.join(set(r.attributes.get('class', 'unknown') for r in ann.regions))
        })
    df_summary = pd.DataFrame(summary_data)
    df_summary.to_csv(stats_dir / 'annotation_summary.csv', index=False)
    
    # 5. 生成简单的HTML报告
    generate_html_report(config, df_summary, success_count, len(error_list), stats_dir)
    
    logger.info("可视化流水线执行完毕。")

def generate_html_report(config, df_summary, success_count, error_count, stats_dir):
    """生成一个简单的HTML报告页面"""
    html_template = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>VIA标注可视化分析报告</title>
        <style>
            body { font-family: sans-serif; margin: 40px; }
            .summary { background-color: #f4f4f4; padding: 20px; border-radius: 5px; }
            table { border-collapse: collapse; width: 100%; margin-top: 20px; }
            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
            th { background-color: #4CAF50; color: white; }
            img { max-width: 600px; margin: 10px; border: 1px solid #ccc; }
        </style>
    </head>
    <body>
        <h1>VIA标注数据集可视化分析报告</h1>
        <div class="summary">
            <h2>执行摘要</h2>
            <p><strong>标注文件:</strong> {{ config.paths.json_annotation }}</p>
            <p><strong>总图像数:</strong> {{ df_summary.shape[0] }}</p>
            <p><strong>成功可视化图像数:</strong> {{ success_count }}</p>
            <p><strong>失败图像数:</strong> {{ error_count }}</p>
            <p><strong>生成时间:</strong> {{ timestamp }}</p>
        </div>
        
        <h2>统计图表</h2>
        <div>
            <h3>标注类别分布</h3>
            <img src="statistics/class_distribution.png" alt="Class Distribution">
            
            <h3>每图像标注数量分布</h3>
            <img src="statistics/annotations_per_image.png" alt="Annotations per Image">
        </div>
        
        <h2>数据摘要 (前20行)</h2>
        {{ df_summary_head|safe }}
        
        <p><em>详细数据请查看: statistics/annotation_summary.csv</em></p>
    </body>
    </html>
    """
    
    from datetime import datetime
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    template = Template(html_template)
    html_content = template.render(
        config=config,
        df_summary=df_summary,
        df_summary_head=df_summary.head(20).to_html(index=False),
        success_count=success_count,
        error_count=error_count,
        timestamp=timestamp
    )
    
    report_path = Path(config['paths']['output_dir']) / 'visualization_report.html'
    with open(report_path, 'w', encoding='utf-8') as f:
        f.write(html_content)
    
    print(f"HTML报告已生成: {report_path}")

最后,创建一个简单的入口脚本run_pipeline.py

#!/usr/bin/env python3
"""
VIA标注可视化流水线主入口。
"""
import sys
from src.pipeline import setup_pipeline, run_batch_visualization

if __name__ == "__main__":
    config_path = "config.yaml" if len(sys.argv) < 2 else sys.argv[1]
    
    try:
        config, logger = setup_pipeline(config_path)
        logger.info("启动VIA标注可视化流水线...")
        run_batch_visualization(config, logger)
        logger.info("流水线执行成功结束。")
    except FileNotFoundError as e:
        print(f"错误:配置文件或资源未找到 - {e}")
        sys.exit(1)
    except Exception as e:
        print(f"流水线执行过程中发生未预期错误: {e}")
        sys.exit(1)

通过这样一套工程化的流水线,你只需配置好config.yaml,运行一个命令,就能自动完成整个数据集的解析、可视化、分析和报告生成。这不仅能节省大量重复劳动,也使得标注质量审查和数据分析过程变得标准化、可追溯。在实际团队协作中,这份自动生成的报告可以成为每次标注迭代后的标准交付物之一,让所有成员对数据状态一目了然。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐