ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从数据标注到模型部署:构建车辆检测与车型识别系统

从数据标注到模型部署:构建车辆检测与车型识别系统 简介YOLO车辆检测数据集与车型识别系统是一套面向人工智能开发者的完整实践方案覆盖车辆位置检测和型号分类两大任务。整套资源采用LabelImg标注部分图片基于SSD-MobileNet预训练模型训练车辆检测器并借助InceptionV4调优实现车型分类最后将检测结果裁剪后送入分类模型串联成可运行的Web演示系统。包内共有576个文件包括Python源码、配置文件、模型定义、训练数据、Dockerfile以及示例图片等其中300个py文件覆盖数据读取、模型训练和推理部署流程42个config方便调整网络参数整体压缩包大小约158.84MB。已有2626人学习/下载。对于希望上手目标检测与图像分类联合应用的读者可以直接借助已有标注数据和模型配置复现实验也能基于模型和部署脚本搭建车辆识别Web服务是一份难得的端到端学习资源。1. 从数据集到识别系统YOLO车辆检测的整体链路这个压缩包名字里带 YOLO但拆开看并不是一个纯 YOLO 端到端项目而是用 SSD-MobileNet 做车辆位置检测、用 InceptionV4 做型号分类的串行识别系统。数据集里只有部分图片用 LabelImg 做了 Bounding Box 标注训练时先通过检测模型定位车辆再把裁剪出的子图送进型号分类模型最后封装成可运行的 Web 演示。对正在做车辆检测、车位管理、车型统计的开发者来说这套链路最值得借鉴的地方不是单点模型有多强而是「检测 分类」两级模型如何衔接裁剪边界怎么定、置信度阈值怎么调、两个模型各自该用什么配置文件。下面按数据标注、检测训练、分类调优、串行部署的顺序拆解每一步都会给出可直接对照的命令和参数。2. 用 LabelImg 制作车辆检测数据集标注规范与格式转换车辆检测模型需要的是「目标在哪」的标注也就是每个车辆实例的包围盒。LabelImg 是最常用的图形化标注工具同时支持 PASCAL VOC 的 XML 格式和 YOLO 的 TXT 格式输出。这个项目里的标注做法建议先用 VOC 格式保存原始标注再统一转换成 YOLO 格式这样后续调整类别顺序、划分训练验证集都不需要重新打开标注工具。即使你后来改用 YOLOv8 训练自己的数据集这套标注流程和转换逻辑也完全通用。2.1 标注步骤与类别定义打开 LabelImg 后先通过Change Save Dir指定输出目录再用Create RectBox绘制车辆外框。绘制时注意车辆被遮挡超过三分之一就跳过后视镜、车标等突出部件不必精确贴合框的上边贴近车顶、下边贴近车轮下沿。类别建议只定义一个vehicle降低检测难度如果后续需要区分轿车和卡车再增加类别也不迟。标注完成后每张图片对应一个同名 XML 文件里面记录图片尺寸、路径、目标类别和位置。下面是一个典型的标注片段annotation filename0001.jpg/filename size width1920/width height1080/height depth3/depth /size object namevehicle/name bndbox xmin350/xmin ymin280/ymin xmax720/xmax ymax610/ymax /bndbox /object /annotationbndbox里的四个值是绝对像素坐标训练时需要归一化到 01 区间。name字段必须与之后在label_map.pbtxt中定义的类别名完全一致否则训练器会报 label 找不到的错误。除了 XML 本身还有几个标注属性值得用表格固化下来方便多人协作时保持一致。标注属性推荐值说明类别名vehicle单类别检测时统一用小写车辆遮挡 1/3 可标注超过则跳过避免引入误检边界贴合车轮下沿、车顶不要包含过多背景图片分辨率原始分辨率不压缩保留小目标细节存储格式VOC XML便于转换其他格式标注工具本身没有自动校验功能所以标注完一批图片后随机抽 10 张用 OpenCV 画框检查一次能省掉训练时排查错误的大量时间。2.2 VOC 转 YOLO 格式脚本训练 SSD 时最终要的是每个目标一行class_id x_center y_center width height的 YOLO 格式文件。我习惯用下面的脚本统一转换import os import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, out_dir, classesNone): tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size).find(width).text) img_h int(root.find(size).find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if classes and name not in classes: continue cls_id classes.index(name) if classes else 0 box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt), w) as f: f.write(\n.join(lines)) # 示例组装类别列表 classes [vehicle] for xml in os.listdir(annotations): if xml.endswith(.xml): convert_voc_to_yolo(os.path.join(annotations, xml), yolo_labels, classes)这段代码的核心是坐标归一化把左上角和右下角坐标换算成相对图片宽高的中心点与宽高比例。classes参数决定类别索引如果之后扩成多类别请保持与 TFRecord 的label_map顺序一致。转换完成后建议随机抽 5 张图用 OpenCV 的rectangle函数把归一化坐标还原成像素框叠加到原图上确认没有出现框错位、中心点漂移的情况。2.3 数据集划分与目录组织训练需要有清晰的目录层次。我一般用images、labels、trainval.txt三个目录组织其中trainval.txt每行是图片路径。划分比例按 8:1:1 切成训练、验证、测试并用random.shuffle打乱顺序。目录/文件作用建议数量images/原始图片全部labels/对应 YOLO 格式的 txt与 images 一一对应train.txt训练集图片路径列表80%val.txt验证集图片路径列表10%test.txt测试集图片路径列表10%车辆检测中一个常见错误是同一场景的相邻帧被分到训练和验证两个集合导致验证指标虚高。正确做法是先按视频片段或场景分组再整体划分。比如用监控视频抽帧得到的图片要把前 10 分钟的画面归入训练、后 2 分钟归入验证而不是随机抽帧。这样测试才能在没见过的场景下评估模型的真实泛化能力。3. SSD-MobileNet 车辆检测模型训练配置文件与损失分析有了标注数据下一步训练检测模型。项目里提供了多个配置文件包括ssd_mobilenet_v1_coco.config、ssd_mobilenet_v2_coco.config、ssdlite_mobilenet_v1_coco.config、ssdlite_mobilenet_v2_coco.config、ssd_inception_v2_coco.config等。对车辆这类刚体目标我优先选ssd_mobilenet_v1_coco.config因为 MobileNet V1 特征提取速度快COCO 预训练权重里已经包含车辆类别的底层特征如果部署端对体积有要求再降级到ssdlite_mobilenet_v2_coco.config。3.1 配置文件关键参数解读以ssd_mobilenet_v1_coco.config为例需要修改的关键项集中在model.ssd、train_config、train_input_reader三个区块model { ssd { num_classes: 1 box_coder { faster_rcnn_box_coder { y_scale: 10.0 x_scale: 10.0 height_scale: 5.0 width_scale: 5.0 } } image_resizer { fixed_shape_resizer { height: 300 width: 300 } } } } train_config { batch_size: 24 fine_tune_checkpoint: PATH_TO_BE_CONFIGURED/model.ckpt fine_tune_checkpoint_type: detection initial_learning_rate: 0.004 num_steps: 20000 } train_input_reader { label_map_path: PATH_TO_BE_CONFIGURED/label_map.pbtxt input_path: PATH_TO_BE_CONFIGURED/trainval.record }num_classes改成实际类别数这里是 1。fine_tune_checkpoint指向 COCO 预训练模型fine_tune_checkpoint_type: detection表示只加载检测子网络权重。initial_learning_rate从 0.004 开始如果 loss 振荡明显就降到 0.002。num_steps先设 20000训练到一半看 TensorBoard 曲线再决定是否加量。image_resizer默认缩放到 300x300如果你的车辆图片普遍较大可以改成 512x512 提高小目标召回率但训练时间会增加约一倍。不同配置文件的取舍可以按下面的表格快速判断配置文件特征提取器推理速度精度适用场景ssd_mobilenet_v1_cocoMobileNet V1快中等通用车辆检测平衡性好ssdlite_mobilenet_v2_cocoMobileNet V2-Lite最快中等偏下边缘设备、实时要求高ssd_mobilenet_v2_cocoMobileNet V2中等中等对精度和速度要求均衡ssd_inception_v2_cocoInception V2慢较高对精度要求高、算力充足ssd_mobilenet_v1_focal_loss_petsMobileNet V1快中等正负样本极不平衡时试验3.2 生成 TFRecord 并与开始训练TensorFlow Object Detection API 需要把图片和标注打包成 TFRecord。数据集里没有现成 TFRecord 时需要把 YOLO 格式的 txt 解析成tf.train.Example。核心逻辑是读取图片二进制解析标注文件构造tf.train.Feature字典包含image/encoded、image/source_id、image/object/bbox/xmin等字段。注意检测框坐标在 TFRecord 中必须是归一化后的ymin, xmin, ymax, xmax顺序和 YOLO 的x_center, y_center, w, h不同写脚本时要小心转换。训练命令如下# 从 models/research 目录执行 PIPELINE_CONFIG_PATH/path/to/ssd_mobilenet_v1_coco.config MODEL_DIR/path/to/train_output python object_detection/model_main.py \ --pipeline_config_path${PIPELINE_CONFIG_PATH} \ --model_dir${MODEL_DIR} \ --num_train_steps20000 \ --alsologtostderr训练过程中打开 TensorBoard 观察 losstensorboard --logdir${MODEL_DIR}MODEL_DIR存放检查点文件每 2000 步保存一次。如果训练中断重新执行相同命令会默认从最近的 checkpoint 恢复不用担心进度丢失。另外batch_size需要根据显存调整24 在 11G 显存上没问题换成 8G 卡就降到 8同时学习率也相应减半否则梯度更新步长过大会导致 loss 发散。3.3 损失函数与常见异常SSD 的总 loss 由分类损失和回归损失加权组成。正常收敛时总 loss 在前 5000 步从 58 快速下降到 1 以下之后缓慢逼近 0.30.5。如果localization_loss不降先检查box_coder的 scale 参数是否被改动过默认的 10/10/5/5 对 300x300 输入是经验值乱改会导致坐标回归难以收敛。如果classification_loss直接变成 NaN通常是学习率过大或标注框出现宽度为 0 的目标需要在生成 TFRecord 时过滤掉异常框。另一个容易忽略的点配置文件里的num_epochs和num_steps是二选一的设了num_steps就不要设num_epochs否则训练步数会被覆盖。还有预训练模型的 checkpoint 路径必须准确fine_tune_checkpoint_type: detection只加载检测塔的参数如果是classification则会忽略锚框相关参数加载不完整。4. InceptionV4 型号分类从裁剪子图到分类调优检测模型输出的是车辆位置框接下来要把框内的子图裁剪出来输送给型号分类模型。这里选 InceptionV4 是合理的它的 Inception 模块能在同样计算量下提取更丰富的多尺度特征对车辆这种类内差异大但类间又相似的场景比 VGG 系收敛更快。训练分类模型时输入不是原始大图而是检测框裁剪出来的子图因此分类模型学到的特征是局部的、去除背景干扰的。4.1 从检测结果生成分类样本在训练分类模型之前先从训练集图片中按标注框裁剪子图。裁剪时注意检测框往往比实际车身略大子图会包含一部分背景这在训练时相当于随机裁剪能提升鲁棒性。但不要直接把原图 resize 成 299x299可以先按比例留出 5% 的边距再裁剪。import cv2 def crop_vehicle(image_path, box): img cv2.imread(image_path) h, w img.shape[:2] x1, y1, x2, y2 box # 外扩 5% 防止切到车身边缘 dx int((x2 - x1) * 0.05) dy int((y2 - y1) * 0.05) x1 max(0, x1 - dx) y1 max(0, y1 - dy) x2 min(w, x2 dx) y2 min(h, y2 dy) crop img[y1:y2, x1:x2] crop cv2.resize(crop, (299, 299)) return cropbox是检测输出的像素坐标外扩 5% 是在补偿检测框与真值的偏差。实际使用时也可以在数据增强阶段随机缩放但先在裁剪阶段固定外扩能让后续的数据分布更稳定。裁剪出的子图按型号分类存放每张子图打上所属型号码然后按与检测数据集相同的 8:1:1 比例划分。4.2 迁移学习与微调InceptionV4 不要从头训练直接加载 ImageNet 权重做迁移学习。模型最后一层全连接输出改为你的车型类别数。由于检测框已经过滤掉大量背景分类模型的输入类内差异相对集中微调时可以把所有 BatchNorm 层解冻让底层继续适应车辆图像的色调分布。训练时使用 Adam 优化器初始学习率 0.0001每 5 个 epoch 衰减一次衰减因子 0.9。如果类别不均衡用加权交叉熵损失权重设置为各类别样本数的倒数。下面是一段基于 Keras 的微调示意from tensorflow.keras.applications import InceptionV4 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D from tensorflow.keras.models import Model base_model InceptionV4(weightsimagenet, include_topFalse, input_shape(299, 299, 3)) x base_model.output x GlobalAveragePooling2D()(x) predictions Dense(NUM_CLASSES, activationsoftmax)(x) model Model(inputsbase_model.input, outputspredictions) # 先冻结所有层只训练顶部分类器 for layer in base_model.layers: layer.trainable False model.compile(optimizeradam, losscategorical_crossentropy) model.fit(train_generator, epochs10) # 再解冻后 50 层降低学习率继续微调 for layer in base_model.layers[-50:]: layer.trainable True model.compile(optimizeradam, losscategorical_crossentropy) # 学习率需要另外设置 model.fit(train_generator, epochs20)注意第二个compile时学习率要显式设置成 0.00001否则 Adam 默认参数会被重置导致前面的微调失效。如果过拟合在train_generator里设置rotation_range15、width_shift_range0.1、height_shift_range0.1水平翻转按 0.5 概率开启。如果欠拟合则先减少数据增强强度再看分类 loss。数据增强参数可以按下面的表配置增强项参数值说明rotation_range15轻微旋转防止角度过拟合width_shift_range0.1水平平移增强位置鲁棒性height_shift_range0.1垂直平移horizontal_flipTrue车辆左右对称可翻转zoom_range0.1模拟检测框大小变化brightness_range(0.8, 1.2)应对不同光照条件4.3 分类模型的评估与故障排查分类模型常用 top-1 和 top-5 准确率。车辆型号识别任务中top-1 能做到 90% 以上就说明检测和分类的衔接没问题。我在实际项目里遇到过卡车和公交车轮廓相近导致混淆的情况后来在训练数据里增加尾部和侧面视角的样本才压下去。如果分类 loss 下降但准确率不动检查是不是某些类别样本太少用classification_report看每类的 precision/recall不要只看整体准确率。5. 串行推理与 Web 演示验证模型边界与提速技巧把检测和分类串起来之后需要一个统一的推理入口。常见的做法是先用检测模型对整张图做一次前向得到 N 个车辆框再用非极大值抑制去掉冗余框最后把每个框裁剪出来依次送入分类模型。这里有一个容易忽略的细节检测框的置信度阈值要设置得比单独使用检测模型时更高比如从 0.5 提到 0.6因为一个错误的框送入分类模型后分类器会硬性给出一个车型结果形成二次错误。from flask import Flask, request, jsonify import cv2 import numpy as np def infer(image): det_boxes detect_vehicles(image) # 返回 [x1, y1, x2, y2, score] det_boxes nms(det_boxes, threshold0.5) results [] for box in det_boxes: crop crop_vehicle(image, box[:4]) cls_id, prob classify_vehicle(crop) results.append({box: box[:4], type: class_names[cls_id], score: float(prob)}) return results app Flask(__name__) app.route(/predict, methods[POST]) def predict(): file request.files[image] img cv2.imdecode(np.frombuffer(file.read(), np.uint8), cv2.IMREAD_COLOR) return jsonify(infer(img))这段代码把检测和分类封装在infer内便于单元测试。验证时用一小批带真值的图片统计 mAP 或逐类的 precision/recall不要只看单张效果。我一般额外检查两种失败样本一是检测框把两辆车框在同一个框里二是分类模型对倒车镜视角的输出概率分布过于平坦。这两种情况都意味着模型边界需要调整比如前者需要调低 NMS 的 IoU 阈值后者需要补充样本。提速方面检测模型和分类模型可以分别导出为 TensorRT engine。以常见做法为例TensorRT 的 FP16 模式能让 SSD-MobileNet 的推理时间从 35ms 压到 12ms 左右InceptionV4 从 45ms 压到 20ms整体单帧耗时控制在 50ms 以内。实际操作时先用onnx-tensorrt工具转换再用pycuda做会话管理。还要注意如果检测模型输出大量低分框要先把 NMS 阈值调高否则裁剪和分类的操作会让延迟成倍上升。最后在 Web 前端的响应区域显示处理耗时能快速定位是检测阶段慢还是分类阶段慢。本文还有配套的精品资源点击获取
返回列表