LabelImg源码架构剖析:Python与Qt的完美结合
LabelImg作为基于Python和Qt的图形化图像标注工具,采用清晰的MVC架构模式,将数据模型、用户界面和控制逻辑有效分离。项目整体架构分为用户界面层、业务逻辑层、数据处理层和文件I/O层四个主要层次,包含主界面模块、图形绘制模块、数据模型模块、文件I/O模块和工具类模块等核心组件,展现了优秀的模块化设计和可扩展性。
项目整体架构与模块划分
LabelImg作为一个基于Python和Qt的图形化图像标注工具,其架构设计体现了清晰的模块化思想。整个项目采用MVC(Model-View-Controller)架构模式,将数据模型、用户界面和控制逻辑进行了有效分离,确保了代码的可维护性和扩展性。
核心架构层次
LabelImg的架构可以分为四个主要层次:
主要模块功能划分
1. 主界面模块 (MainWindow)
作为应用程序的核心控制器,MainWindow类继承自QMainWindow和WindowMixin,负责协调各个模块之间的交互。其主要职责包括:
- 应用程序初始化:加载设置、国际化字符串包、预定义类别
- 界面布局管理:创建和管理停靠窗口、工具栏、菜单栏
- 事件处理:处理键盘快捷键、鼠标事件、文件操作
- 状态管理:维护当前图像索引、标注状态、脏标志等
2. 图形绘制模块 (Canvas)
Canvas类是图像标注的核心组件,负责所有的图形绘制和交互操作:
| 功能类别 | 具体功能 | 实现方法 |
|---|---|---|
| 形状绘制 | 矩形框创建、编辑、移动 | mousePressEvent, mouseMoveEvent |
| 顶点操作 | 添加、删除、移动顶点 | add_point, pop_point, move_vertex_by |
| 选择操作 | 形状选择、取消选择 | select_shape, de_select_shape |
| 可视化 | 绘制形状、顶点、标签 | paintEvent, draw_vertex |
3. 数据模型模块
Shape类 - 标注形状模型
class Shape:
def __init__(self, label=None, line_color=None, difficult=False, paint_label=False):
self.label = label
self.points = []
self.fill = False
self.selected = False
self.difficult = difficult
self.paint_label = paint_label
self.line_color = line_color
# ... 其他属性和方法
Shape类封装了标注形状的所有属性和行为,包括:
- 几何信息:顶点坐标、边界矩形
- 样式属性:线条颜色、填充颜色、标签显示
- 状态标志:选中状态、困难样本标志
- 操作方法:移动、复制、序列化
LabelFile类 - 标注文件管理
负责不同格式标注文件的读写操作,支持三种主流格式:
| 格式类型 | 文件扩展名 | 适用场景 | 特点 |
|---|---|---|---|
| PascalVOC | .xml | 目标检测 | 详细的元数据信息 |
| YOLO | .txt | 实时检测 | 归一化坐标,简洁格式 |
| CreateML | .json | Apple生态系统 | JSON格式,兼容Core ML |
4. 文件I/O模块
项目采用工厂模式实现多格式支持,每个格式都有对应的读写器:
5. 工具类模块 (libs目录)
libs目录包含了所有的工具类和辅助模块:
- utils.py:通用工具函数,如图标生成、颜色处理、自然排序
- settings.py:应用程序设置管理,使用pickle持久化配置
- constants.py:常量定义,统一管理字符串常量
- stringBundle.py:国际化支持,多语言字符串管理
- colorDialog.py:颜色选择对话框封装
- zoomWidget.py:缩放控制组件
- lightWidget.py:亮度调节组件
6. 对话框模块
- LabelDialog:标签输入和选择对话框,支持历史记录和预定义类别
- ColorDialog:颜色选择对话框,提供颜色预设和自定义功能
模块间协作关系
各个模块通过明确的责任边界和清晰的接口进行协作:
- 用户交互流程:用户操作 → MainWindow事件处理 → Canvas图形更新 → LabelFile数据保存
- 数据流:图像加载 → 形状创建/编辑 → 格式转换 → 文件保存
- 配置管理:Settings统一管理应用程序状态,确保用户体验一致性
这种模块化架构使得LabelImg具有良好的可扩展性,开发者可以轻松地:
- 添加新的标注格式支持
- 扩展图形绘制功能
- 定制用户界面布局
- 集成新的机器学习框架格式
GUI界面设计与Qt框架集成
LabelImg作为一款专业的图像标注工具,其GUI界面设计采用了Python与Qt框架的深度集成方案。通过精心设计的组件架构和信号槽机制,实现了高效的用户交互体验。
Qt框架核心组件架构
LabelImg基于PyQt5/PyQt4构建,采用了经典的MVC(Model-View-Controller)设计模式。主界面由多个可停靠窗口组件构成,形成了灵活的布局结构。
界面布局与组件设计
LabelImg的主界面采用经典的IDE式布局,包含以下核心组件:
| 组件类型 | 组件名称 | 功能描述 | 实现类 |
|---|---|---|---|
| 中央画布 | Canvas | 图像显示与标注区域 | Canvas(QWidget) |
| 标签面板 | Label Dock | 显示和管理标注标签 | QDockWidget |
| 文件列表 | File Dock | 图像文件浏览 | QDockWidget |
| 工具栏 | ToolBar | 常用操作按钮 | ToolBar |
| 状态栏 | StatusBar | 显示坐标和状态信息 | QStatusBar |
信号槽机制与事件处理
LabelImg充分利用了Qt的信号槽机制来实现组件间的通信。Canvas组件定义了多个自定义信号:
class Canvas(QWidget):
zoomRequest = pyqtSignal(int)
lightRequest = pyqtSignal(int)
scrollRequest = pyqtSignal(int, int)
newShape = pyqtSignal()
selectionChanged = pyqtSignal(bool)
shapeMoved = pyqtSignal()
drawingPolygon = pyqtSignal(bool)
这些信号与主窗口的槽函数连接,实现了实时的用户交互反馈:
自定义Widget组件开发
LabelImg开发了多个自定义Qt组件来满足特定的功能需求:
Canvas组件 - 核心绘图区域
class Canvas(QWidget):
def __init__(self, *args, **kwargs):
super(Canvas, self).__init__(*args, **kwargs)
self.mode = self.EDIT # 编辑模式
self.shapes = [] # 存储所有形状
self.current = None # 当前正在绘制的形状
self.selected_shape = None # 选中的形状
def paintEvent(self, event):
"""重绘事件,绘制图像和所有标注形状"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制背景图像
painter.drawPixmap(0, 0, self.pixmap)
# 绘制所有形状
for shape in self.shapes:
if self.isVisible(shape):
shape.paint(painter)
LabelDialog组件 - 标签输入对话框
class LabelDialog(QDialog):
def __init__(self, text="Enter object label", parent=None, list_item=None):
super(LabelDialog, self).__init__(parent)
self.list_item = list_item if list_item else []
self.initUI(text)
def initUI(self, text):
"""初始化对话框UI"""
self.setWindowTitle("Label Dialog")
layout = QVBoxLayout()
self.edit = QLineEdit()
self.edit.setText(text)
self.edit.selectAll()
layout.addWidget(self.edit)
# 添加历史标签列表
if self.list_item:
self.list_widget = QListWidget()
for item in self.list_item:
self.list_widget.addItem(item)
layout.addWidget(self.list_widget)
# 按钮布局
button_layout = QHBoxLayout()
ok_button = QPushButton("OK")
cancel_button = QPushButton("Cancel")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
多语言支持与国际化
LabelImg通过StringBundle类实现了多语言支持,使用Qt的国际化机制:
class StringBundle:
def __init__(self, create_key, locale_str):
self.create_key = create_key
self.locale_str = locale_str
self.strings = {}
self.__load_bundle()
def get_string(self, string_id):
"""根据ID获取本地化字符串"""
return self.strings.get(string_id, string_id)
样式与主题定制
通过Qt的样式表机制,LabelImg实现了界面样式的灵活定制:
# 设置应用程序样式
app.setStyle(QStyleFactory.create('Fusion'))
# 自定义样式表
app.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QDockWidget {
titlebar-normal-icon: url(:/icons/dock-normal);
titlebar-close-icon: url(:/icons/dock-close);
}
""")
响应式布局与自适应设计
LabelImg的界面采用了响应式设计,能够适应不同屏幕尺寸和分辨率:
def resizeEvent(self, event):
"""窗口大小改变事件处理"""
super(MainWindow, self).resizeEvent(event)
# 调整画布大小
if self.canvas and not self.canvas.pixmap().isNull():
self.adjust_scale()
这种基于Qt框架的GUI设计不仅提供了丰富的用户交互功能,还确保了应用程序的跨平台兼容性和高性能表现。通过精心设计的组件架构和信号槽机制,LabelImg实现了图像标注工作流程的流畅体验。
核心标注算法实现原理
LabelImg的核心标注算法基于Qt框架的事件驱动机制和几何图形处理,实现了高效、精确的图像标注功能。该算法通过Canvas组件管理图形绘制、Shape类处理几何形状、以及鼠标事件处理机制,构成了完整的标注系统。
几何形状管理:Shape类的核心设计
Shape类是标注系统的核心数据结构,负责管理标注框的几何属性和绘制逻辑:
class Shape(object):
P_SQUARE, P_ROUND = range(2)
MOVE_VERTEX, NEAR_VERTEX = range(2)
def __init__(self, label=None, line_color=None, difficult=False, paint_label=False):
self.label = label
self.points = [] # 存储四个顶点坐标
self.fill = False
self.selected = False
self.difficult = difficult
self.paint_label = paint_label
self._closed = False
Shape类采用四顶点矩形表示法,每个标注框由四个QPointF对象组成,支持以下核心操作:
| 方法 | 功能描述 | 参数说明 |
|---|---|---|
add_point(point) | 添加顶点 | point: QPointF坐标 |
nearest_vertex(point, epsilon) | 查找最近顶点 | epsilon: 搜索半径 |
contains_point(point) | 判断点是否在形状内 | 基于QPainterPath |
move_by(offset) | 整体移动形状 | offset: QPointF偏移量 |
move_vertex_by(i, offset) | 移动单个顶点 | i: 顶点索引 |
标注绘制状态机与事件处理
Canvas组件实现了复杂的状态机机制来处理用户交互:
鼠标事件处理算法
Canvas的鼠标事件处理采用分层处理策略:
1. 鼠标移动事件处理流程
def mouseMoveEvent(self, ev):
pos = self.transform_pos(ev.pos()) # 坐标转换
if self.drawing(): # 绘制模式
self.handle_drawing_mode(pos)
elif Qt.RightButton & ev.buttons(): # 右键拖动
self.handle_right_drag(pos)
elif Qt.LeftButton & ev.buttons(): # 左键操作
self.handle_left_drag(pos)
else: # 悬停状态
self.handle_hover(pos)
2. 绘制模式核心算法
绘制矩形标注框的算法实现:
def handle_drawing(self, pos):
if not self.current: # 开始新标注
self.current = Shape()
self.current.add_point(pos)
self.line[0] = pos
elif len(self.current) == 1: # 确定第二个点
self.current.add_point(pos)
if self.draw_square: # 正方形模式
self.calculate_square_points(pos)
else: # 矩形模式
self.line[1] = pos
elif len(self.current) >= 2: # 完成绘制
self.finalize_shape()
正方形模式的计算算法:
def calculate_square_points(self, pos):
init_pos = self.current[0]
min_x = init_pos.x()
min_y = init_pos.y()
# 计算最小边长确保正方形
min_size = min(abs(pos.x() - min_x), abs(pos.y() - min_y))
direction_x = -1 if pos.x() - min_x < 0 else 1
direction_y = -1 if pos.y() - min_y < 0 else 1
# 计算对角点坐标
end_point = QPointF(min_x + direction_x * min_size,
min_y + direction_y * min_size)
self.line[1] = end_point
顶点精确定位与吸附算法
LabelImg实现了精确的顶点定位机制,确保用户能够准确选择和操作标注框:
def nearest_vertex(self, point, epsilon):
"""查找距离给定点最近的顶点"""
index = None
min_distance = float('inf')
for i, vertex in enumerate(self.points):
dist = distance(vertex - point) # 计算欧几里得距离
if dist <= epsilon and dist < min_distance:
index = i
min_distance = dist
return index # 返回最近顶点的索引
距离计算采用欧几里得距离公式: $$distance = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$$
边界约束与坐标裁剪
为确保标注框不超出图像范围,算法实现了边界约束机制:
def out_of_pixmap(self, p):
"""判断点是否超出图像边界"""
size = self.pixmap.size()
return p.x() < 0 or p.y() < 0 or p.x() > size.width() or p.y() > size.height()
def snap_point_to_canvas(self, x, y):
"""将点吸附到画布边界"""
size = self.pixmap.size()
clipped_x = min(max(0, x), size.width())
clipped_y = min(max(0, y), size.height())
return QPointF(clipped_x, clipped_y)
实时坐标显示与反馈
标注过程中实时显示几何信息:
# 绘制过程中显示宽度和高度
current_width = abs(self.current[0].x() - pos.x())
current_height = abs(self.current[0].y() - pos.y())
self.parent().window().label_coordinates.setText(
'Width: %d, Height: %d / X: %d; Y: %d' %
(current_width, current_height, pos.x(), pos.y()))
多格式标注输出支持
标注算法支持多种输出格式的坐标转换:
| 格式类型 | 坐标表示 | 转换方法 |
|---|---|---|
| Pascal VOC | 绝对坐标 (xmin, ymin, xmax, ymax) | 直接存储 |
| YOLO | 相对坐标 (x_center, y_center, width, height) | 归一化处理 |
| CreateML | JSON格式的归一化坐标 | 比例转换 |
YOLO格式转换算法示例:
def bnd_box_to_yolo_line(self, box, class_list=[]):
"""将边界框转换为YOLO格式"""
x_min, y_min, x_max, y_max = box
class_index = class_list.index(self.label) if self.label in class_list else 0
# 计算中心点和宽高(归一化)
x_center = (x_min + x_max) / 2 / self.img_size[0]
y_center = (y_min + y_max) / 2 / self.img_size[1]
width = (x_max - x_min) / self.img_size[0]
height = (y_max - y_min) / self.img_size[1]
return "%d %.6f %.6f %.6f %.6f" % (class_index, x_center, y_center, width, height)
性能优化策略
标注算法采用了多种性能优化措施:
- 增量重绘:只重绘发生变化的部分区域
- 空间索引:使用边界框进行快速碰撞检测
- 延迟计算:只在需要时进行几何计算
- 内存管理:及时释放不再使用的图形对象
这种算法设计使得LabelImg能够高效处理大型图像数据集,同时保持流畅的用户交互体验。通过精确的几何计算和智能的事件处理,为用户提供了专业级的图像标注工具。
插件化扩展与自定义开发
LabelImg作为一个成熟的图像标注工具,虽然官方文档中并未明确提及插件系统,但其架构设计天然支持扩展和自定义开发。通过分析源码结构,我们可以发现多个可扩展的接口和设计模式,为开发者提供了丰富的自定义能力。
多格式支持架构
LabelImg的核心扩展性体现在其多格式支持架构上。项目采用了策略模式(Strategy Pattern)来处理不同的标注文件格式,这使得添加新的文件格式变得非常简单。
class LabelFileFormat(Enum):
PASCAL_VOC = 1
YOLO = 2
CREATE_ML = 3
class LabelFile(object):
def save_pascal_voc_format(self, filename, shapes, image_path, image_data,
line_color=None, fill_color=None, database_src=None):
# PascalVOC格式保存实现
writer = PascalVocWriter(...)
writer.save(target_file=filename)
def save_yolo_format(self, filename, shapes, image_path, image_data, class_list,
line_color=None, fill_color=None, database_src=None):
# YOLO格式保存实现
writer = YOLOWriter(...)
writer.save(target_file=filename, class_list=class_list)
def save_create_ml_format(self, filename, shapes, image_path, image_data, class_list,
line_color=None, fill_color=None, database_src=None):
# CreateML格式保存实现
writer = CreateMLWriter(...)
writer.write()
这种设计模式的优势在于:
- 开闭原则:对扩展开放,对修改关闭
- 模块化设计:每种格式的处理逻辑独立封装
- 易于维护:格式之间的变更互不影响
自定义格式开发指南
要添加新的标注格式,开发者需要遵循以下步骤:
1. 创建格式读写器
首先创建一个新的IO类,继承或实现标准的读写接口:
# 自定义格式写入器示例
class CustomFormatWriter:
def __init__(self, folder_name, filename, img_size, shapes, output_file, **kwargs):
self.folder_name = folder_name
self.filename = filename
self.img_size = img_size
self.shapes = shapes
self.output_file = output_file
self.verified = kwargs.get('verified', False)
def write(self):
# 实现自定义格式的写入逻辑
with open(self.output_file, 'w') as f:
# 自定义格式序列化
custom_data = self._serialize_shapes()
f.write(custom_data)
def _serialize_shapes(self):
# 形状数据序列化逻辑
serialized = []
for shape in self.shapes:
serialized.append({
'label': shape['label'],
'points': shape['points'],
'difficult': shape.get('difficult', False)
})
return json.dumps(serialized, indent=2)
# 自定义格式读取器
class CustomFormatReader:
def __init__(self, file_path, image=None, **kwargs):
self.file_path = file_path
self.image = image
self.shapes = []
def parse_custom_format(self):
with open(self.file_path, 'r') as f:
data = json.load(f)
for item in data:
self.shapes.append({
'label': item['label'],
'points': item['points'],
'difficult': item.get('difficult', False)
})
return self.shapes
2. 注册新格式到系统
在LabelFile类中添加对新格式的支持:
# 在LabelFile类中添加新格式支持方法
def save_custom_format(self, filename, shapes, image_path, image_data, class_list,
line_color=None, fill_color=None, database_src=None):
img_folder_name = os.path.basename(os.path.dirname(image_path))
img_file_name = os.path.basename(image_path)
image = QImage()
image.load(image_path)
image_shape = [image.height(), image.width(),
1 if image.isGrayscale() else 3]
writer = CustomFormatWriter(img_folder_name, img_file_name,
image_shape, shapes, filename,
local_img_path=image_path)
writer.verified = self.verified
writer.write()
3. 更新格式枚举和UI
# 扩展LabelFileFormat枚举
class LabelFileFormat(Enum):
PASCAL_VOC = 1
YOLO = 2
CREATE_ML = 3
CUSTOM_FORMAT = 4 # 新增自定义格式
# 在UI中添加快捷切换
def get_format_meta(format):
if format == LabelFileFormat.PASCAL_VOC:
return '&PascalVOC', 'format_voc'
elif format == LabelFileFormat.YOLO:
return '&YOLO', 'format_yolo'
elif format == LabelFileFormat.CREATE_ML:
return '&CreateML', 'format_createml'
elif format == LabelFileFormat.CUSTOM_FORMAT:
return '&CustomFormat', 'format_custom' # 新增UI选项
工具类扩展机制
LabelImg的utils模块提供了丰富的工具函数,这些函数本身也是可扩展的:
# 自定义工具函数示例
def custom_image_processor(image_path, processing_config):
"""
自定义图像预处理函数
"""
image = QImage(image_path)
# 应用自定义处理配置
if processing_config.get('normalize', False):
image = normalize_image(image)
if processing_config.get('augment', False):
image = augment_image(image)
return image
def normalize_image(image):
"""图像归一化处理"""
# 实现具体的归一化逻辑
return image
def augment_image(image):
"""数据增强处理"""
# 实现数据增强逻辑
return image
画布行为自定义
Canvas类提供了丰富的可重写方法,允许开发者自定义标注行为:
class CustomCanvas(Canvas):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.custom_tools_enabled = False
def enable_custom_tools(self, enable=True):
"""启用自定义标注工具"""
self.custom_tools_enabled = enable
def mousePressEvent(self, ev):
if self.custom_tools_enabled and ev.button() == Qt.RightButton:
# 自定义右键行为
self.handle_custom_right_click(ev.pos())
else:
super().mousePressEvent(ev)
def handle_custom_right_click(self, position):
"""处理自定义右键点击"""
# 实现自定义右键菜单或功能
self.show_custom_context_menu(position)
def show_custom_context_menu(self, position):
"""显示自定义上下文菜单"""
menu = QMenu(self)
# 添加自定义菜单项
action1 = menu.addAction("自定义操作1")
action2 = menu.addAction("自定义操作2")
action1.triggered.connect(self.custom_action_1)
action2.triggered.connect(self.custom_action_2)
menu.exec_(self.mapToGlobal(position))
设置系统扩展
Settings类提供了配置管理的基础设施,可以轻松扩展:
# 自定义设置项扩展
CUSTOM_SETTING_1 = 'custom_setting_1'
CUSTOM_SETTING_2 = 'custom_setting_2'
class ExtendedSettings(Settings):
def __init__(self):
super().__init__()
# 添加自定义默认值
self.defaults.update({
CUSTOM_SETTING_1: 'default_value_1',
CUSTOM_SETTING_2: 100
})
def get_custom_setting(self, key, default=None):
"""获取自定义设置"""
return self.get(key, default)
def set_custom_setting(self, key, value):
"""设置自定义配置"""
self[key] = value
插件化架构设计模式
虽然LabelImg没有正式的插件系统,但我们可以借鉴其架构设计来实现插件化:
实际扩展案例:COCO格式支持
以下是一个完整的COCO格式扩展示例:
import json
from datetime import datetime
from enum import Enum
class CocoWriter:
def __init__(self, output_path):
self.output_path = output_path
self.coco_data = {
"info": {
"year": datetime.now().year,
"version": "1.0",
"description": "COCO dataset generated by LabelImg",
"contributor": "LabelImg User",
"url": "",
"date_created": datetime.now().isoformat()
},
"licenses": [{
"id": 1,
"name": "Unknown License",
"url": ""
}],
"images": [],
"annotations": [],
"categories": []
}
self.image_id = 1
self.annotation_id = 1
self.category_map = {}
def add_image(self, image_info, shapes):
image_entry = {
"id": self.image_id,
"width": image_info["width"],
"height": image_info["height"],
"file_name": image_info["file_name"],
"license": 1,
"flickr_url": "",
"coco_url": "",
"date_captured": datetime.now().isoformat()
}
self.coco_data["images"].append(image_entry)
for shape in shapes:
self.add_annotation(shape, self.image_id)
self.image_id += 1
def add_annotation(self, shape, image_id):
points = shape['points']
x_min = min(p[0] for p in points)
y_min = min(p[1] for p in points)
x_max = max(p[0] for p in points)
y_max = max(p[1] for p in points)
width = x_max - x_min
height = y_max - y_min
category_id = self.get_category_id(shape['label'])
annotation = {
"id": self.annotation_id,
"image_id": image_id,
"category_id": category_id,
"segmentation": [],
"area": width * height,
"bbox": [x_min, y_min, width, height],
"iscrowd": 0
}
self.coco_data["annotations"].append(annotation)
self.annotation_id += 1
def get_category_id(self, category_name):
if category_name not in self.category_map:
category_id = len(self.category_map) + 1
self.category_map[category_name] = category_id
self.coco_data["categories"].append({
"id": category_id,
"name": category_name,
"supercategory": "none"
})
return self.category_map[category_name]
def save(self):
with open(self.output_path, 'w', encoding='utf-8') as f:
json.dump(self.coco_data, f, indent=2, ensure_ascii=False)
扩展开发最佳实践
- 保持向后兼容:扩展时不要破坏现有功能
- 模块化设计:将新功能封装为独立模块
- 配置驱动:通过配置文件管理扩展选项
- 错误处理:为扩展功能添加适当的异常处理
- 文档完善:为自定义扩展提供详细的使用文档
通过以上架构分析和代码示例,我们可以看到LabelImg虽然表面简单,但其内部设计为扩展和自定义开发提供了丰富的可能性。开发者可以根据具体需求,灵活地扩展文件格式、标注工具、导出功能等,打造符合特定工作流程的图像标注解决方案。
总结
LabelImg通过精心设计的架构展现了Python与Qt框架的深度集成能力。其核心优势在于清晰的模块化划分、灵活的扩展机制和专业的标注算法实现。从MVC架构到多格式支持,从GUI设计到插件化扩展,项目为开发者提供了丰富的自定义可能性。无论是添加新的标注格式、扩展图形功能还是定制用户界面,LabelImg的架构都展现了出色的适应性和可维护性,为图像标注工具的开发提供了优秀的设计范例。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



