简介:目标检测是计算机视觉的核心任务之一,而训练高质量检测模型离不开规范的数据集与标注格式。VOC格式以XML描述目标边界框,YOLO格式采用归一化坐标实现高效加载,JSON(COCO风格)则通过结构化字段适配主流检测框架。理解这三种标注格式的转换原理,能够有效避免数据读取错误、坐标越界等常见工程问题。本文围绕电动车识别数据集,梳理从标注格式解析、格式转换到YOLOv8微调的全流程,并探讨数据增强与模型评估要点,为实际场景中的电动自行车检测项目提供可复用的技术参考。
电动车识别数据集,别只盯着1306张,这三个格式才是真正的坑
做目标检测这么多年,我经常看到有人下载数据集后,卡在“文件打不开”“标签读不出来”这种最基础的问题上。上周我正好在做一个电动车进电梯的识别项目,顺手把这个通用电动车识别数据集完整跑了一遍,包括三种标签格式的解析、转换、训练全流程。今天抽时间把整个过程整理出来,希望对刚接触目标检测或者准备拿电动车检测练手的人有点帮助。
这个数据集包含1306张电动车实景图,每张图片都配了三种格式的标签文件:VOC格式的XML、YOLO格式的TXT,以及JSON格式的标注文件。通俗点说,同一批图片给你准备了三种“答案”,你可以根据自己用的训练框架直接选对应格式,不用再费劲做格式转换。它适合的人群很明确:想快速跑通YOLO训练流程的初学者、需要在电动车检测项目里做预训练模型微调的人,以及想研究三种标注格式之间转换关系的研究者。
1. 数据集整体设计与格式解析
1.1 数据集目录结构说明
拿到压缩包解压之后,第一件事不是急着训练,而是先搞清楚目录结构。这个数据集的目录组织比较标准,大致是这样的:
电动车识别数据集_通用1306张/ ├── images/ # 全部图片文件(JPG格式) ├── annotations/ │ ├── xml/ # VOC格式标注,每个xml对应一张图片 │ ├── yolo/ # YOLO格式标注,每个txt对应一张图片 │ └── json/ # JSON格式标注,可能是合并的单个文件 ├── classes.txt # 类别列表 ├── train.txt # 训练集文件列表(YOLO格式) ├── val.txt # 验证集文件列表 └── README.md我建议你解压后先打开README文件,看看官方是否已经划分了训练集和验证集。如果里面已经提供了train.txt和val.txt,那么省事很多;如果只有图片和标注,你需要自己划分。另外,我习惯把压缩包解压后先跑一个脚本统计一下图片数量和标注数量是否一一对应,后文会详细讲怎么排查这种基础问题。
1.2 三种标注格式分别是什么
很多初学者对“VOC”“YOLO”“JSON”这三个词是懵的,其实它们只是同一批标注信息的不同“记录方式”,就像同一个地址可以写成“某某省某某市某某街道”,也可以写成经纬度坐标,表达的内容是一样的,但不同的系统(或者说模型训练框架)要求不同的输入格式。
VOC格式是Pascal VOC比赛带火的标准,核心是XML文件,文件里记录了图片文件名、路径、图片尺寸、标注框的左上角坐标(xmin, ymin)和右下角坐标(xmax, ymax),还有对象类别名称。这种格式人类可读性最好,用文本编辑器打开一目了然。
YOLO格式是Ultralytics系列框架默认使用的格式,每个标注框用一行表示,包含“类别ID 中心点x 中心点y 宽 高”五个数字,而且所有数值都是相对图片宽高的归一化值。这种格式非常简洁,模型在训练加载时不需要再做复杂的坐标变换,所以YOLO系列框架都选择它作为默认标注格式。
JSON格式在这份数据集里通常是COCO风格的标注文件。COCO数据集的标注结构是把所有信息都塞进一个JSON对象里,包含“images”数组、“annotations”数组和“categories”数组,每张图片的标注框通过image_id、category_id、bbox(左上角x、y、宽、高)来关联。这种格式适合mmdetection、Detectron2等框架直接读取。
2. 三种格式的深入拆解
2.1 VOC XML格式逐字段解读
打开一个XML标注文件,你会发现结构是这样的:
<annotation> <folder>images</folder> <filename>ev_001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>electric_bicycle</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>356</xmin> <ymin>540</ymin> <xmax>864</xmax> <ymax>1021</ymax> </bndbox> </object> </annotation>这里面最关键的就是<size>标签下的宽高信息和<object>标签下的<bndbox>坐标信息。为什么图片尺寸那么重要?因为后续要转换到YOLO格式时,必须用标注坐标除以图片宽高得到归一化坐标。如果图片尺寸填错了,转换出来的归一化坐标就容易超出0到1的范围,训练时就会出现警告甚至直接过滤掉标注框。
另外留意<truncated>和<difficult>字段。truncated表示目标是否被图片边缘截断,difficult表示这个目标是否难以辨认。在通用电动车检测数据集里,这些字段一般设为0,但如果你后续要对VOC格式的数据做数据增强(比如随机裁剪)时,这些字段能帮你判断哪些标注框可能在增强后失效,属于进阶用法了。
2.2 YOLO TXT格式的归一化坐标逻辑
在同一份数据集里,对应的YOLO标注文件内容大概是这样的:
0 0.3177 0.7231 0.2646 0.4454 0 0.6125 0.5312 0.1833 0.3567每一行代表一个目标。第一个数字是类别ID,classes.txt里如果写的是electric_bicycle,那么类别ID为0的就是它。后面四个数字分别是归一化中心点x坐标、中心点y坐标、归一化宽度、归一化高度。
归一化计算的方法如下:
x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height width = (xmax - xmin) / image_width height = (ymax - ymin) / image_height这里要注意一个经典坑点:VOC的坐标是像素整数,而YOLO要求的是0到1之间的浮点数。如果图片尺寸比较大、目标比较小,转换后的宽度和高度可能非常接近0;如果某个框恰好超出图片边界,转换后x_center可能大于1或者小于0,这种情况训练时会出问题。我在后文会专门讲怎么处理越界坐标。
2.3 JSON标注的结构设计
这份数据集的JSON标注文件是COCO风格的话,你会看到类似这样的结构:
{ "images": [ { "id": 1, "file_name": "ev_001.jpg", "width": 1920, "height": 1080 } ], "annotations": [ { "id": 1, "image_id": 1, "category_id": 1, "bbox": [356, 540, 508, 481], "area": 244348, "segmentation": [] } ], "categories": [ { "id": 1, "name": "electric_bicycle" } ] }注意COCO风格的bbox是[x左上角, y左上角, width宽度, height高度],这和VOC的[xmin, ymin, xmax, ymax]不同。如果你在写数据转换脚本时搞混了这两个坐标系,会导致标注框整体偏移或者宽高错误。
另外segmentation字段在用于检测时可以留空数组,但如果做实例分割任务就需要补充多边形坐标。对于“电动车识别检测数据集”来说,这个字段一般是空的,说明该数据集主要用于目标检测,不太适合直接做分割训练。
3. 实操:三种格式的转换与训练准备
3.1 XML转YOLO格式的标准脚本
虽然你下载的数据集已经同时提供了XML、TXT、JSON三种格式,但实际项目中你常会遇到“我只拿到了VOC标注,但我想用YOLO训练”的情况。所以掌握XML转YOLO的方法仍然很有必要。
下面这个脚本是我常用的,直接复制就能用:
import os import xml.etree.ElementTree as ET def xml_to_yolo(xml_file, class_list, output_dir): tree = ET.parse(xml_file) root = tree.getroot() # 读取图片尺寸 size = root.find('size') img_width = int(size.find('width').text) img_height = int(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_list: continue class_id = class_list.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 归一化 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height # 裁剪到0-1范围,避免越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) width = min(max(width, 0.0), 1.0) height = min(max(height, 0.0), 1.0) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") # 输出txt文件 base_name = os.path.basename(xml_file).replace('.xml', '.txt') with open(os.path.join(output_dir, base_name), 'w') as f: f.write('\n'.join(lines)) # 使用示例 class_list = ['electric_bicycle'] xml_dir = 'annotations/xml' yolo_dir = 'annotations/yolo' os.makedirs(yolo_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith('.xml'): xml_to_yolo(os.path.join(xml_dir, xml_file), class_list, yolo_dir)这段脚本里我加了一个细节:将归一化坐标裁剪到0到1之间。经过这一步处理后,即使原标注有略微越界的框,也不会导致YOLO加载时报错。不过要注意,如果越界严重(比如宽度都变成负数了),裁剪也没用,需要重新检查原始标注。
3.2 JSON转YOLO格式的关键点
COCO JSON转YOLO稍微复杂一些,因为需要根据每张图片的id关联到对应的标注框:
import json import os def coco_json_to_yolo(json_path, output_dir): with open(json_path, 'r') as f: data = json.load(f) # 建立id到类别名称的映射 categories = {} for cat in data['categories']: categories[cat['id']] = cat['name'] # 建立image_id到文件名和尺寸的映射 images = {} for img in data['images']: images[img['id']] = img # 包含file_name, width, height # 每个图片一个txt annotations_by_image = {} for ann in data['annotations']: image_id = ann['image_id'] if image_id not in annotations_by_image: annotations_by_image[image_id] = [] annotations_by_image[image_id].append(ann) for image_id, anns in annotations_by_image.items(): img_info = images[image_id] img_width = img_info['width'] img_height = img_info['height'] lines = [] for ann in anns: category_id = ann['category_id'] class_id = category_id - 1 # COCO的category_id通常从1开始 bbox = ann['bbox'] # [x, y, width, height] x_center = (bbox[0] + bbox[2] / 2.0) / img_width y_center = (bbox[1] + bbox[3] / 2.0) / img_height width = bbox[2] / img_width height = bbox[3] / img_height lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") base_name = os.path.splitext(img_info['file_name'])[0] + '.txt' with open(os.path.join(output_dir, base_name), 'w') as f: f.write('\n'.join(lines))这里有一个关键问题:COCO的category_id一般是全局唯一的,通常从1开始编号,而YOLO的类别ID要求从0开始连续编号。如果你的categories数组顺序和实际id不一致,直接做category_id - 1就会出现错位。稳妥做法是先根据categories数组的排列顺序建立一个新旧id映射关系,再做转换。
3.3 数据集目录组织与训练文件生成
拿到这份数据集后,无论你想用YOLOv5还是YOLOv8来训练,都需要把数据组织成框架要求的结构。用YOLOv8举例,官方推荐的目录结构是:
datasets/ ├── electric_bicycle/ │ ├── images/ │ │ ├── train/ │ │ └── val/ │ ├── labels/ │ │ ├── train/ │ │ └── val/ │ └── data.yamldata.yaml文件内容很简单:
train: datasets/electric_bicycle/images/train val: datasets/electric_bicycle/images/val nc: 1 names: ['electric_bicycle']如果你下载的数据集已经提供了train.txt和val.txt,那么可以通过脚本将对应的图片和标注复制到正确的目录下。如果没有提供,建议按8:2或9:1的比例自行划分。划分时我习惯用随机种子固定随机数,方便复现实验。
这里提供一个简单可靠的划分脚本:
import os import shutil import random random.seed(42) img_dir = 'images' label_dir = 'annotations/yolo' train_ratio = 0.8 all_images = [f for f in os.listdir(img_dir) if f.endswith('.jpg')] random.shuffle(all_images) split_idx = int(len(all_images) * train_ratio) train_images = all_images[:split_idx] val_images = all_images[split_idx:] os.makedirs('datasets/electric_bicycle/images/train', exist_ok=True) os.makedirs('datasets/electric_bicycle/images/val', exist_ok=True) os.makedirs('datasets/electric_bicycle/labels/train', exist_ok=True) os.makedirs('datasets/electric_bicycle/labels/val', exist_ok=True) for img in train_images: shutil.copy(f'{img_dir}/{img}', f'datasets/electric_bicycle/images/train/{img}') label = img.replace('.jpg', '.txt') if os.path.exists(f'{label_dir}/{label}'): shutil.copy(f'{label_dir}/{label}', f'datasets/electric_bicycle/labels/train/{label}') for img in val_images: shutil.copy(f'{img_dir}/{img}', f'datasets/electric_bicycle/images/val/{img}') label = img.replace('.jpg', '.txt') if os.path.exists(f'{label_dir}/{label}'): shutil.copy(f'{label_dir}/{label}', f'datasets/electric_bicycle/labels/val/{label}')注意这里有个隐患:如果某张图片对应的TXT标注文件不存在,代码就直接跳过复制,但图片还是被复制过去了。训练时YOLO框架会提示“没有标签文件”,虽然不影响训练(会当作背景样本处理),但如果数量太多会浪费时间。所以在划分前最好先做一次完整性检查。
4. 常见问题与排查技巧实录
4.1 标注文件缺失或数量不匹配
我在实际使用中遇到过几次这种情况:解压后统计,图片有1306张,XML文件确实也是1306个,但TXT文件只有1298个。这种细微差异不会立刻报错,但训练过程中会不断有警告。
排查方法很简单,用脚本对比文件名集合:
import os img_files = set(f[:-4] for f in os.listdir('images') if f.endswith('.jpg')) txt_files = set(f[:-4] for f in os.listdir('annotations/yolo') if f.endswith('.txt')) missing_txt = img_files - txt_files print(f'缺失TXT标注的图片数量: {len(missing_txt)}') for name in list(missing_txt)[:10]: print(name)查到缺失清单后,可以从XML标注重新生成TXT,或者直接从数据集中删掉这些图片。我一般倾向于从原XML重新生成,保证数据量最大化。
4.2 坐标越界与归一化值异常
YOLO训练时如果出现大量“WARNING: 检测到标注框坐标超出图像边界”的日志,大概率是XML中的标注框坐标超出了图片的实际尺寸范围。这种情况常见于标注工具边缘手误,或者图片在采集后经过缩放但没有同步更新标注坐标。
处理方法有两个方向:一是批量修正,将越界的坐标裁剪到图片边界内;二是直接过滤掉那些“极端异常”的标注框。我个人建议先做裁剪,因为电动车在图片边缘很常见,随便删框会丢失有效样本。
对于归一化之后数值不在0到1范围内的情况,YOLO训练框架(尤其是YOLOv8)在strict模式下可能会直接忽略该框,导致实际参与训练的样本比预期少。你可以在训练日志里看一下每个epoch的实例数,如果明显低于标注总数,就要检查是不是有大量越界框被忽略了。
4.3 类别ID与类别名称不匹配
另一个高频问题是:数据集里有多个类别的时候,XML里写的类别名是“electric_bicycle”,但你的训练配置里names列表写的是['bicycle'],或者写成了['electric_bicycle', 'motorcycle']。类别名称不一致会导致训练阶段读取标注时找不到对应的类别ID,框架一般会报错或者自动跳过这些框。
建议不管用哪个框架,都先检查一下数据集的classes.txt内容,再和你要用到YAML配置对齐。以下命令可以快速查看YOLO标注中出现了哪些类别ID,以及每个类别的框数量:
import os from collections import Counter label_dir = 'annotations/yolo' counter = Counter() for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f), 'r') as fh: for line in fh.readlines(): if line.strip(): class_id = int(line.split()[0]) counter[class_id] += 1 print(counter)如果数据集是单类别电动车,那么输出应该是Counter({0: 具体数量}),如果出现了其他ID,说明标注文件里有不属于当前数据集的类别,需要排查。
4.4 训练时样本分布不均衡
拿到1306张图片后,如果笼统地跑训练而不看标注框的分布情况,很容易忽略一个事实:不同图片里的电动车数量差异很大,有的图只有1辆,有的图可能停了一排车有6、7辆。这种分布不均会让模型在检测密集场景时表现较差。
我建议你在训练前先统计一下每张图片的平均目标数量和最大目标数量,对数据构成有个基本认识。统计方法非常简单,就是逐行读取TXT文件的行数。如果发现密集样本占比太少,训练时可以考虑对密集样本做过采样,或者调整loss函数中的正负样本权重。
再有一点:这个数据集虽然标注了电动车,但看图片内容会发现有室内停车场的场景、马路边的场景、小区门口的场景。如果你的目标是“楼道内检测电动车”,那么模型在训练时见过的背景类型越多,误检率相对会越高。条件允许的话,建议用这份通用数据集做预训练,再用你自己场景下的小批量图片做微调,效果会好很多。
5. 应用场景与基于YOLO的模型微调流程
5.1 电动车识别的主要应用场景
电动车目标检测在真实落地场景中很有价值。我接触过的项目大致可以分成三类。
第一类是小区的电动车进电梯识别。这个场景需要检测摄像头画面中是否有电动车,配合梯控系统阻止电动车入梯或是发出告警。这种场景比较吃“形变鲁棒性”,因为电动车进电梯的视角变化很大,有时是侧面推进来,有时是骑跨式进入,有时被遮挡了一部分,模型的定位精度和召回率都要平衡。
第二类是道路或园区的违停检测,通过识别电动车是否停放在禁停区域来产生告警。这种场景更看重“高召回”和“低误报”,因为漏报容易造成管理漏洞,而误报太多会干扰管理员的注意力。
第三类是电动车牌照识别的前置步骤。严格来说车牌识别是一个单独的任务,通常会用单独的OCR模型来处理,但在整个识别流程中,第一步是先定位到电动车区域,才能进一步去提取牌照区域。很多做“两轮车车牌识别”的团队会在车牌识别网络之前加一个电动车检测器,这样既减少定位范围,也提高车牌识别准确率。
这份通用数据集在这些场景里可以当作基础训练数据。不过要注意,通用数据集标注的类别名称是electric_bicycle,但不同场景里可能还需要区分电动自行车和电动摩托车,这属于类别细粒度扩展的问题,需要额外补充数据。
5.2 YOLOv8快速微调流程
如果你决定用YOLOv8来跑这份数据,流程非常清爽。先安装ultralytics包:
pip install ultralytics然后把数据集目录组织成前面说的结构,再写一个data.yaml,最后用一行命令启动训练:
yolo detect train data=datasets/electric_bicycle/data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16这里的model=yolov8n.pt表示加载YOLOv8n的预训练权重,n是nano版本,参数量最小、训练最快,适合快速验证数据质量。如果你的算力允许,可以换成s或m版本,检测精度会更高。训练完成后,在runs/detect/train目录下会看到weights/best.pt和weights/last.pt两个权重文件,一般用best.pt来做推理。
推理也很简单:
yolo detect predict model=runs/detect/train/weights/best.pt source=test_images/Ultralytics框架会在输出目录里生成带标注框的图片,你可以肉眼判断模型效果。我习惯把训练集和验证集各抽10张图片单独放一个目录,训练完直接跑一遍,快速看效果,比光盯着mAP数字更有直观感受。
5.3 模型评估的几个关键指标
很多人在跑完训练后只看mAP50和mAP50-95两个数字,确实这两个指标是主流,但它们不能告诉你全部情况。
mAP50指的是IoU阈值为0.5时的平均精度,衡量的是“框大方向准不准”;mAP50-95则是从0.5到0.95每隔0.05算一次再取平均,更严格,对框的定位精度更敏感。如果你发现mAP50挺高但mAP50-95偏低,说明框的位置不够精准,可能需要提高输入分辨率或者调整anchor。
除了mAP,还要看Precision和Recall。Precision衡量的是预测出来的框有多少是正确的,Recall衡量的是真实标注中有多少被找回来了。在电动车检测场景里,我一般会优先保证Recall,因为漏检的代价往往比误检更高——漏了一辆车进电梯,可能就是一次安全事故。
还有一个容易忽略的指标是F1-Confidence曲线。模型输出每个框都带置信度,你设定置信度阈值不同,Precision和Recall会跟着变化。默认0.25的阈值对某些场景不一定合适,建议跑完训练后画一下P-R曲线,找到Precision和Recall平衡点对应的置信度阈值,再在部署时用这个阈值。
6. 数据增强与模型鲁棒性提升
6.1 通用增强策略
训练目标检测模型,光靠数据集的1306张图是远远不够的。YOLOv8内置了Mosaic、随机翻转、色彩抖动等增强策略,对电动车这种目标来说非常有用。Mosaic增强会把4张图拼成一张,相当于一次训练看到更多上下文,对小目标检测尤其有帮助。
但Mosaic也不是万能的。在“电动车进电梯”这类场景里,电动车往往占画面比例很大,拼图之后目标会被缩小,反而增加了学习难度。我的做法是:如果目标主要是近距离大目标,就把mosaic的概率调低一些,或者输入图片的imgsz增大,保持目标在图像中的实际大小不会缩得太厉害。
6.2 场景针对性的数据增强
电动车检测有个显著特点:电动车的色彩和背景颜色可能非常接近。比如灰色电动车停在灰色墙边,模型很容易把电动车和背景混淆。针对这种情况,增加“色彩扰动”增强(HSV调整)会有些帮助,让模型看到更多饱和度、明度变化下的电动车,降低对特定颜色组合的依赖。
另外,电动车的外形在不同角度下差异很大,从正前方看是电瓶加把手,从侧面看是车架和轮子。如果原始数据集里图片视角不够多样,训练的模型在某个角度的检测效果会很差。你可以通过旋转增强、缩放增强来模拟不同视角,但要注意旋转角度不能太大,否则电动车特征会失真。
6.3 使用预训练模型加速收敛
我强烈建议不要从随机权重开始训练,哪怕你的场景和COCO数据集差异很大。预训练权重里学到的低级特征(边缘、纹理、角点)在电动车检测中依然有效,能让你用更少的epoch达到相同的精度。
使用YOLOv8的官方预训练权重,从0开始训练100个epoch达到的效果,可能比加载COCO权重后训练50个epoch的效果还要差一些。原因很简单:电动车虽然不在COCO的80类里,但COCO里有自行车、摩托车,这些类别的底层特征和电动车有很多可迁移的地方。
这份“电动车识别检测数据集(通用)1306张”在尺度上算是中小型数据集,加载预训练权重基本是必选项。除非你已经积累了足够多的电动车场景数据,否则不建议挑战从零训练这条路。
7. 我的几点实操心得
最后分享几个我在这几天实操中整理出的心得,不一定写在官方文档里,但对提升使用体验很有帮助。
第一,拿到任何数据集,第一件事先跑一次“标注完整性检查”,确认图片数量和标注文件数量一致。第二,三种格式虽然内容等价,但转换脚本一定要自己验证一遍,尤其是坐标单位——VOC是像素,COCO的bbox也是像素但顺序是[x,y,w,h],YOLO是归一化值,搞混了就是灾难。第三,电动车检测的“通用数据集”和“场景化数据集”之间的差距,需要在微调阶段补,不能指望一份公开数据集解决所有场景。
还有一个很实用的小技巧:如果你后续要把这个模型部署到边缘设备(比如NVIDIA Jetson、瑞芯微RK3588等),训练时就不要总是盯着yolov8x这种大模型跑精度。建议直接用小模型n或s版本做数据探索和标注质量验证,等流程完全跑通之后,再换大模型做最终训练和蒸馏,省时省力。另一个经验是,数据集中偶发会混入模糊的、严重遮挡的样本,训练前看一下标注框宽高分布,把宽或高小于5%图片尺寸的异常框过滤掉,能有效减少训练时的无效计算。
最后说一句关于标注格式转换的忠告:即便数据集已经提供了三种格式,你在自己项目里也经常会遇到需要二次转换的情况,例如从YOLO转成VOC给别人协作标注,或者把VOC转成JSON给mmdetection用。所以花点时间把本文里的脚本读懂、跑通,不要只是复制粘贴以后换一套框架就不会了。这个数据集质量整体算不错的,标注框也比较规整,直接拿来做训练完全够用。如果你正好在做电动车检测,用它起步绝对是一个靠谱的选择。
本文还有配套的精品资源,点击获取