共计 3284 个字符,预计需要花费 9 分钟才能阅读完成。
背景痛点
CCPD 数据集是车牌识别领域常用的公开数据集,但它的原始标注格式与常见的 YOLO、PASCAL VOC 等格式存在显著差异。CCPD 的标注信息存储在以图片名命名的.txt 文件中,包含车牌四点坐标、倾斜角度、车牌号码等信息。而 YOLO 格式需要的是归一化的中心点坐标和宽高,PASCAL VOC 则需要 XML 格式的标注。手动转换这些格式不仅耗时,还容易出错,特别是当数据量达到数万张时,几乎不可行。

技术方案
解析 CCPD 标注文件结构
CCPD 的标注文件内容通常如下所示:
车牌号码 四点坐标 (x1,y1,x2,y2,x3,y3,x4,y4) 倾斜角度 其他信息
我们需要解析这些信息,并提取出车牌区域的边界框。
坐标系转换算法
YOLO 格式需要将四点坐标转换为归一化的中心点坐标和宽高。步骤如下:
- 从四点坐标中找出最小和最大的 x、y 值,确定边界框
- 计算中心点坐标:
(x_min + x_max)/2和(y_min + y_max)/2 - 计算宽度和高度:
x_max - x_min和y_max - y_min - 归一化:将上述值除以图像宽度和高度
多格式输出支持
我们将实现一个转换器类,支持输出 YOLO 和 PASCAL VOC 格式。YOLO 格式简单,适合快速训练;PASCAL VOC 格式更详细,适合需要更多信息的场景。
代码实现
以下是 Python 实现的核心代码:
import os
import xml.etree.ElementTree as ET
from typing import List, Tuple
class CCPDAnnotationConverter:
"""CCPD 标注文件转换器"""
def __init__(self, img_width: int, img_height: int):
self.img_width = img_width
self.img_height = img_height
def parse_ccpd_line(self, line: str) -> dict:
"""解析 CCPD 单行标注"""
parts = line.strip().split()
if len(parts) < 9:
raise ValueError("Invalid CCPD annotation format")
plate_number = parts[0]
points = list(map(float, parts[1:9]))
angle = float(parts[9])
return {
'plate_number': plate_number,
'points': points,
'angle': angle
}
def convert_to_yolo(self, points: List[float]) -> Tuple[float, float, float, float]:
"""将四点坐标转换为 YOLO 格式"""
x_coords = points[0::2]
y_coords = points[1::2]
x_min, x_max = min(x_coords), max(x_coords)
y_min, y_max = min(y_coords), max(y_coords)
# 计算中心点和宽高
x_center = (x_min + x_max) / 2 / self.img_width
y_center = (y_min + y_max) / 2 / self.img_height
width = (x_max - x_min) / self.img_width
height = (y_max - y_min) / self.img_height
return x_center, y_center, width, height
def convert_to_voc(self, points: List[float], plate_number: str) -> ET.Element:
"""转换为 PASCAL VOC 格式"""
annotation = ET.Element('annotation')
# 添加图像信息
size = ET.SubElement(annotation, 'size')
ET.SubElement(size, 'width').text = str(self.img_width)
ET.SubElement(size, 'height').text = str(self.img_height)
ET.SubElement(size, 'depth').text = '3'
# 添加目标信息
obj = ET.SubElement(annotation, 'object')
ET.SubElement(obj, 'name').text = 'license_plate'
ET.SubElement(obj, 'pose').text = 'Unspecified'
ET.SubElement(obj, 'truncated').text = '0'
ET.SubElement(obj, 'difficult').text = '0'
bndbox = ET.SubElement(obj, 'bndbox')
x_coords = points[0::2]
y_coords = points[1::2]
ET.SubElement(bndbox, 'xmin').text = str(int(min(x_coords)))
ET.SubElement(bndbox, 'ymin').text = str(int(min(y_coords)))
ET.SubElement(bndbox, 'xmax').text = str(int(max(x_coords)))
ET.SubElement(bndbox, 'ymax').text = str(int(max(y_coords)))
# 添加车牌号码
ET.SubElement(obj, 'plate_number').text = plate_number
return annotation
# 使用示例
if __name__ == '__main__':
converter = CCPDAnnotationConverter(720, 1160) # CCPD 典型图像尺寸
# 示例 CCPD 标注行
ccpd_line = "皖 AD12345 123.4 567.8 234.5 678.9 345.6 789.0 456.7 890.1 15 其他信息"
# 解析
parsed = converter.parse_ccpd_line(ccpd_line)
# 转换为 YOLO 格式
yolo_format = converter.convert_to_yolo(parsed['points'])
print(f"YOLO 格式: {yolo_format}")
# 转换为 VOC 格式
voc_format = converter.convert_to_voc(parsed['points'], parsed['plate_number'])
print(f"VOC 格式: {ET.tostring(voc_format, encoding='unicode')}")
避坑指南
处理倾斜车牌的坐标转换
CCPD 中的车牌经常有倾斜角度,直接取最小 / 最大 x,y 值得到的矩形框可能会包含过多背景。解决方案:
- 使用四点坐标计算最小外接矩形
- 考虑旋转角度调整边界框
多线程批处理的资源竞争
当处理大量文件时,使用多线程可以加速,但要注意:
- 避免多个线程同时写入同一个文件
- 使用队列管理任务分配
- 控制线程数量,避免内存不足
验证集划分时的数据泄漏
确保同一个车牌的不同角度图像不会同时出现在训练集和验证集中:
- 按车牌号码分组后再划分
- 使用分层抽样保证分布均衡
扩展思考
- 如何调整代码以支持其他车牌数据集(如 AOLP、PKU 等)?不同数据集的标注格式差异如何处理?
- YOLO 格式和 PASCAL VOC 格式在训练目标检测模型时,会有哪些性能差异?
- 对于极端倾斜(如大于 45 度)的车牌,当前的边界框表示方式是否仍然有效?如何改进?
总结
本文详细介绍了如何将 CCPD 数据集转换为常用标注格式的全过程。通过面向对象的设计,我们实现了一个灵活的转换工具,可以轻松扩展到其他格式和其他数据集。在实际应用中,还需要考虑数据增强、模型兼容性等问题,但有了标准化的标注文件,这些后续工作会变得容易很多。
正文完
发表至: 未分类
近一天内
