
简介这套基于深度学习的车辆特征分析系统资源包面向Python初学者与车辆识别方向学习者可用于课程设计、毕业设计或算法入门解决车辆品牌、类型、颜色及车牌的自动识别问题。包内提供完整的Python源码、训练脚本、前端展示页面以及训练好的模型权重可通过上传图片直接体验识别流程。资源共1826个文件压缩包约925MB其中包含1561张jpg车辆图片构成的数据集另有py/pyc代码文件、pth模型参数文件、html/js/css前端交互文件、docx说明文档及字体图标等辅助资源目录结构清晰便于按需取用。目前已有27人学习适合作为学习深度学习视觉任务的完整参考能帮助读者理解图像数据收集、模型训练、推理展示与界面封装的全流程也可在此基础上扩展车牌识别等应用。1. 这个标题真正要解决的是让机器“看懂”每一辆车项目标题把“车辆”写成了“车俩”但要做的事其实很明确用 Python 和深度学习把监控视频或抓拍图片里的每一辆车自动识别出颜色、车型、品牌、车牌号这些特征组成一条结构化的车辆档案。这个方向在停车场月卡识别、安防平台按“红色 SUV”检索、交警卡口的车流分类统计里都是刚需而且相比传统的图像处理方案深度学习在复杂光线、多角度、遮挡场景下确实更稳。我做这类系统的主要诉求很简单摄像头拍到的车进了系统就该变成一行文字记录而不是一张无法检索的图片。这个标题指向的方案本质上就是一套“检测 分类 车牌识别”的串联流水线全程用 Python 落地。适合谁适合手上已经有一批车辆图像数据、想从零搭一套可用系统的工程师也适合需要评估“这个方向到底值不值得做”的团队。接下来我按自己实际做过的方案拆开讲从选型到避坑每一层都可以照着复现。2. 车辆特征分析系统先拆解特征维度再定模型架构2.1 车辆特征分析到底在分析什么从颜色、车型、品牌到车牌做系统之前必须先想清楚“特征”两个字指什么。在我的项目里车辆特征通常拆成四个维度车身颜色、车辆类型、品牌型号、车牌号码。这四个维度的技术难度完全不一样不能混在一个模型里处理。车身颜色是全局特征但受光照影响极大同一辆车在正午和路灯下拍出来可能是两种颜色。车辆类型轿车、SUV、卡车、巴士相对简单类间差异大一个轻量分类网络就能拿很高的准确率。品牌型号是最难的部分比如说区分“宝马 3 系”和“宝马 5 系”属于细粒度识别类间差异极小必须用更强的骨干网络。车牌号则是另一个技术栈它对单字符的精度要求极高而且不同省份的车牌格式不同新能源车牌还多一位。在实际系统里我会把车牌单独拆出去因为它是“身份标识”而不是“外观特征”识别逻辑也完全不一样。很多刚入门的同学把颜色、车型、车牌放在同一个模型里输出结果训练时 loss 互相干扰车牌识别精度上不去。正确的做法是先跑一个目标检测模型把所有车辆框出来然后对每个框并行做颜色分类、车型分类和车牌识别三条支路互不干扰这也在工程上方便单独迭代——车牌不准就只换车牌模型不会动到颜色分类。这里还有一个容易忽略的点车头车尾的特征分布差异。车头有进气格栅、车标、大灯车尾只有尾灯和文字标牌同一个品牌的车头和车尾在视觉上差别很大。如果你的数据源是卡口抓拍通常只有车头或者只有车尾那训练出来的模型在另一视角下会明显掉点。我在做方案设计时第一件事就是确认数据来源的视角分布这决定了后面所有标注工作的方向。2.2 三层模型架构目标检测、细粒度分类、车牌识别的选型逻辑明确了特征维度之后模型选型就顺理成章了。我采用的是“检测 分类 车牌识别”三层架构每层各选一个主力模型用部署代价和精度表现来做取舍。第一层是目标检测负责找出画面里的所有车辆。这个环节我首选 YOLO 系模型目前常用的是 YOLOv8因为它端到端训练、部署生态成熟CPU 和 GPU 都能跑。YOLO 输出的每个检测框附带置信度和类别标签这里类别只做“车辆”和“背景”不做细分类因为细分类交给第二层专门处理。这样检测模型的负担小小目标召回率也能保住。如果画面里车辆密集比如高速收费站排队可以换 YOLOv8 的更大尺度版本但推理耗时相应增加。第二层是特征分类包括颜色、车型、品牌。颜色和车型用 ResNet 或 EfficientNet 这类图像分类网络就够了输入是检测框裁剪出来的车辆图片。品牌细粒度识别需要更高分辨率和更强骨干我一般用 ResNet101 或者 EfficientNet-B4在 ImageNet 预训练权重上做微调比自己从零训练收敛快得多效果也好得多。这一层的输出是多个属性的独立概率分布注意是“独立多标签”而不是“单标签”因为一辆车同时有颜色、车型、品牌三个属性不能做成互斥的多分类。第三层是车牌识别。这个环节很多项目直接用目标检测加 OCR 两段式但更高效的做法是用 LPRNet 这类端到端车牌识别网络它把定位和字符识别合在一起支持不定长车牌。考虑到实际部署环境车牌识别对算力的要求要严格控制否则整个链路延迟会被它拖垮。下表是我在项目中常用的选型方案直接拿来参考任务推荐模型输入尺寸单张推理耗时GPU备注车辆检测YOLOv8s640×640约 5ms密集场景可换 YOLOv8m颜色/车型分类ResNet50224×224约 2ms轻量且精度够用品牌细粒度识别ResNet101224×224约 4ms类间差异小时再上车牌识别LPRNet94×24约 1ms端到端不定长识别这个架构看起来直接但落地时每一步都有隐藏的麻烦。比如 YOLOv8 检测框如果裁得太紧把车牌区域切掉了一半后续车牌识别模型必然翻车分类模型的输入如果是检测框那么检测框的定位精度直接决定了分类输入的质量。所以整个系统不是四个独立模型拼在一起而是需要把前后级联的接口设计好数据维度对齐好才算真正“搭起来”。3. 准备车辆数据集从采集到标注的完整流程3.1 数据集来源与目录规划把分类数据集和检测数据集分开放模型架构定好之后最大的工作量落到数据上。车辆特征分析系统对数据质量极其敏感我见过太多项目模型结构抄得一模一样只因数据集不同效果天差地别。先说公开数据集车检方向常用的是 UA-DETRAC、Cityscapes 这类自动驾驶数据集品牌细粒度识别可以用 CompCars 或 PKU-VD。但它们各有局限UA-DETRAC 偏车辆检测没有品牌标注CompCars 有品牌和车型标注但图片大多是网图和监控视角的实拍图差异很大。所以我的建议是公开数据集只用来做预训练和验证流程最终的系统训练还是得基于自己的场景数据。自采数据集的第一步是目录规划。我习惯把检测和分类的数据分开管理因为它们的标注格式完全不一样。检测数据需要边界框坐标分类数据只需要文件夹名称代表类别。一个规整的数据目录结构大概是这样的vehicle_data/ ├── detection/ │ ├── images/ # 检测用原始图片 │ │ ├── train/ │ │ └── val/ │ ├── labels/ # YOLO 格式标注和 images 一一对应 │ │ ├── train/ │ │ └── val/ │ └── classes.yaml # 类别配置文件 ├── color_cls/ │ ├── train/ │ │ ├── black/ │ │ ├── white/ │ │ └── red/ │ └── val/ │ ├── black/ │ ├── white/ │ └── red/ ├── brand_cls/ │ ├── train/ │ │ ├── audi/ │ │ ├── bmw/ │ │ └── toyota/ │ └── val/ │ ├── audi/ │ ├── bmw/ │ └── toyota/ └── plate_rec/ ├── train/ # 车牌图片按字符序列命名 └── val/这里有几个关键决策。检测数据用 YOLO 格式而不是 COCO 格式因为 YOLO 格式每个图片一个 txt训练时读取效率高也不用维护庞大的 json 文件。分类数据的文件夹直接按类别名称命名用 PyTorch 的ImageFolder可以一行代码加载。车牌识别数据不用文件夹分类而是按文件名命名字符序列因为 LPRNet 的标注是变长字符串。数据量参考我做过的最小可行方案检测 5000 张、颜色分类每类 2000 张、品牌每类 1500 张、车牌 3000 张。这个规模足以支撑一个内部演示系统但离生产级别还差得远——生产级至少翻五倍。数据采集渠道主要是自建摄像头、公共监控回放、合作单位的脱敏数据不要直接爬取网图因为版权和场景偏差都是大坑。3.2 把 COCO 标注转成 YOLO 格式转换脚本与四个边界坑拿到原始标注后第一步通常是格式转换。很多已有数据是 COCO 格式即一个总的 json 文件存放所有图片的标注信息。要把数据喂给 YOLOv8必须转成每张图片一个 txt 的格式。我在这里写过一个转换脚本踩过几个边界坑直接分享出来import json import os from PIL import Image def coco_to_yolo(json_path, img_dir, out_dir, img_width, img_height): 将 COCO 格式标注转换为 YOLO 格式 txt json_path: COCO 标注文件 img_dir: 图片目录 out_dir: 输出 txt 目录 img_width, img_height: 统一缩放的尺寸训练时用 with open(json_path, r) as f: coco json.load(f) # 建立 image_id 到 标注列表 的映射 img_id_to_info {img[id]: img for img in coco[images]} anns_by_img {} for ann in coco[annotations]: img_id ann[image_id] anns_by_img.setdefault(img_id, []).append(ann) os.makedirs(out_dir, exist_okTrue) for img_id, anns in anns_by_img.items(): img_info img_id_to_info[img_id] # 坑1: 原图尺寸可能和训练尺寸不一致必须归一化到实际宽高 # 这里 img_info[width] 是原图宽不要写死用 json_path 里的全局宽 orig_w img_info[width] orig_h img_info[height] txt_path os.path.join(out_dir, img_info[file_name].replace(.jpg, .txt)) with open(txt_path, w) as out: for ann in anns: # COCO 的 bbox 是 [x, y, width, height] x, y, w, h ann[bbox] # 坑2: 坐标必须归一化到 0-1否则 YOLOv8 训练直接报错 x_center (x w / 2) / orig_w y_center (y h / 2) / orig_h w_norm w / orig_w h_norm h / orig_h # 坑3: 坐标裁剪有些标注超出图片边界会导致训练 loss 异常跳变 x_center min(0.999, max(0.001, x_center)) y_center min(0.999, max(0.001, y_center)) w_norm min(0.999, max(0.001, w_norm)) h_norm min(0.999, max(0.001, h_norm)) # 坑4: 类别 id 从 0 开始COCO 原始 category_id 可能不连续 # 需要自己维护一个映射表不要直接用 category_id cat_id category_map.get(ann[category_id], 0) out.write(f{cat_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(转换完成输出目录, out_dir)这段脚本的逻辑就是把 COCO 的绝对坐标框转为相对坐标的 YOLO 格式。参数说明img_width和img_height我留着没在计算里用因为每个原图尺寸不同必须从img_info里读取真实尺寸。如果所有输入图片统一缩放后再标注那才用全局尺寸但那样会丢失原始精度我倾向于保留原图尺寸训练YOLOv8 内部会自动缩放。四个边界坑值得记一下一是全局宽高和单图宽高混用导致归一化错位二是坐标没有裁剪超出边界的框让模型收敛变慢三是类别 ID 没有重映射COCO 数据集里 category_id 往往从 1 开始且不连续而 YOLO 要求从 0 开始连续编号四是训练尺寸和原图尺寸不一致时输入标签错误。这些坑我都在实际项目里踩过花了半天排查 loss 不收敛最后发现是标注坐标归一化算错了。4. 训练车辆特征模型YOLO 主线加分类支线4.1 训练车辆检测模型YOLOv8 训练脚本与五个关键参数数据准备好后先跑检测模型。YOLOv8 的好处是命令行工具和 Python API 都齐全数据配置用 yaml 文件搞定。先写数据配置文件再写训练脚本整个流程不用自己写网络结构。# vehicle_det.yaml # YOLOv8 数据配置文件 path: /data/vehicle_data/detection train: images/train val: images/val test: images/val # 类别名 names: 0: vehicle# train_yolo.py from ultralytics import YOLO # 使用 yolov8s.pt 预训练权重比从零开始收敛快很多 model YOLO(yolov8s.pt) results model.train( datavehicle_det.yaml, epochs100, batch16, imgsz640, workers8, lr00.01, patience15, # 连续 15 轮验证集没提升就提前停止 save_period10, # 每 10 轮存一次权重防止训练中断白跑 device0, # 单 GPU 训练 )参数说明epochs100是起步值车辆检测是相对简单的任务50 轮左右基本收敛100 轮留余量配合 early stopping。batch16取决于显存大小如果显存报错就降到 8。imgsz640是训练输入尺寸如果监控画面里车辆很小建议提高到 960但训练时间会变长。lr00.01主要用于迁移学习阶段如果从零训练要降到 0.001。训练过程中要盯两个指标train/loss和val/box_loss。如果验证集 loss 持续上升而训练集 loss 下降那就是过拟合增加数据增强的mosaic比例或者加 Dropout。如果两个 loss 都在高位徘徊优先检查标注格式和类别配置不要盲目调参。YOLOv8 训练完成后会输出best.pt用这个权重做后续推理和导出。4.2 用 ResNet 微调车型颜色分类冻结骨干层与学习率设置检测模型把车辆框出来之后每个框要送去分类。分类模型我强烈建议微调预训练权重而不是从零训练因为车辆外观的底层特征边缘、纹理、形状和 ImageNet 学习到的通用特征高度重合微调可以大幅减少需要的训练数据量。import torch import torch.nn as nn from torchvision import models, transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder # 数据增强颜色抖动对车辆颜色分类非常重要 transform_train transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 加载数据 train_dataset ImageFolder(/data/vehicle_data/color_cls/train, transformtransform_train) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) # 微调 ResNet50 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) # 冻结前几层前 3 个 stage 不更新只微调最后 1 个 stage 和 FC 层 ct 0 for name, param in model.named_parameters(): ct 1 if ct 0: # 这里改成按需冻结 param.requires_grad False # 更实用的冻结方式冻结除 layer4 和 fc 之外的所有层 for name, param in model.named_parameters(): if not name.startswith(layer4) and not name.startswith(fc): param.requires_grad False # 替换最后的分类头为车辆颜色类别数 num_classes len(train_dataset.classes) model.fc nn.Linear(model.fc.in_features, num_classes) # 用 AdamW学习率给到 1e-4比默认的 1e-3 稳妥 optimizer torch.optim.AdamW( [p for p in model.parameters() if p.requires_grad], lr1e-4, weight_decay1e-4 ) criterion nn.CrossEntropyLoss() # 训练循环 for epoch in range(30): model.train() total_loss 0.0 for images, labels in train_loader: images, labels images.cuda(), labels.cuda() outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() avg_loss total_loss / len(train_loader) print(fEpoch {epoch1}/30, Loss: {avg_loss:.4f})逻辑说明冻结骨干层的原因是小数据集下微调全模型容易过拟合只更新最后几层相当于让模型在已有特征表示上做轻量适配。ColorJitter的亮度和饱和度扰动专门针对车辆颜色受光照影响的问题这是我在实际项目里验证过最有效的增强手段。学习率设为1e-4而不是1e-3因为冻结后可学习的参数量少太激进的学习率会把预训练学到的特征冲掉。参数说明batch_size64在常见单卡上没问题如果显存不足降到 32。30个 epoch 对微调来说足够我见过最快的在第 15 轮就收敛。关键参数是requires_grad的冻结逻辑——这段代码里我自己注释了两种方式实际用第二种只解锁layer4和fc。如果做品牌细粒度识别建议改用 ResNet101 并把layer3也解锁因为品牌差异比颜色差异微弱得多需要更多层参与微调。5. 车辆特征分析系统避坑指南从标注到部署的 5 个血泪经验5.1 视角偏置训练集全是车头测试集全是车尾现象模型在验证集上测试准确率 96%但到了现场实际跑识别率掉到 70% 左右。我早期做的第一个版本就是这样拿卡口的车头数据训练到停车场落地后发现大量车辆只有车尾视角品牌分类几乎全错。原因车辆检测和品牌分类模型都只学到了车头特征比如进气格栅、车标位置、大灯轮廓而车尾只有尾灯、文字标牌、后备箱线条特征分布完全不同。测试集里恰好也是车头数据所以指标虚高。解决数据准备阶段强制按视角分区。训练集里车头、车尾、侧面各占三分之一更严格的做法是检测模型和分类模型独立训练检测模型用全视角数据分类模型按视角分别训练两个分支推理时先判断视角再路由到对应分类网络。这个改动之后现场识别率从 70% 提升到 91%。5.2 颜色分类受光照影响夜间车灯反光把黑色车识别成白色现象夜间场景下黑色车辆的识别结果经常变成深蓝色白色车被路灯的暖黄色光照成浅黄色系统输出的颜色档案和人工标签对不上。原因分类网络学到的颜色特征很大程度来自整体亮度分布而夜间图像的亮度主要来自路灯和车灯反射不是车身本身的颜色。色温变化让 RGB 分布整体偏移模型把偏亮的暗色车判成了浅色。解决训练数据里混入不同时段的图像至少包含白天、黄昏、夜间三组。另外在数据增强中加入色温扰动和亮度扰动ColorJitter的brightness参数调到 0.3 以上。如果夜间样本仍然不够可以在预处理阶段先做白平衡矫正再送分类网络。5.3 检测框贴太紧车牌区域被切掉导致识别率骤降现象车牌识别模型的单字符准确率很高但整牌识别率一直上不去。排查后发现上游 YOLOv8 检测框输出的是紧贴车身的矩形车牌位于车身最下方有一部分被框边缘裁掉了。原因标注车辆检测框时标注员习惯把框贴紧车身边缘导致车牌被“切出去”了一部分后级的车牌识别模块输入图像不完整。解决调整标注规范——车辆检测框四周预留 3%~5% 的边距确保车牌和车灯完整落在框内。如果标注已完成没有重标的预算可以在检测框输出后做“外扩处理”把每个框按长宽各扩大 5% 再裁图送后续模块。这个操作用代码实现也就是几行 OpenCV 的事但对车牌识别的提升立竿见影。5.4 品牌分类滑向黑匣子数据不平衡比模型结构更致命现象品牌分类结果里销量大的车型比如大众、丰田准确率极高但稀有车型比如高性能车、小众进口车几乎全被误判成常见品牌。原因这是一个典型的长尾分布问题。数据集中保有量大的品牌占据了 70% 以上的样本模型在训练时学到的先验概率严重偏向高频类别这是个“黑匣子”问题——表面看整体准确率 90%分解到每个类别长尾类别的召回率可能只有 20%。解决先做统计把每个品牌类别的样本数画出来小于 500 张的类别不参与训练单独归入“其他”类。想要真正识别这些稀有车型就得针对性补数据或者用数据合成做扩充。训练层面用 Focal Loss 替代 CrossEntropyLoss降低高置信度样本的权重让模型更关注难分的稀有类别。我实际测试下来Focal Loss 在这个场景里能把长尾类别召回率提升 12 个百分点。5.5 推理速度与并发没有 GPU 的服务器别硬上 YOLOv8x现象系统开发完成申请到的部署服务器只有 CPUYOLOv8x 跑一张图耗时 2.8 秒并发 5 路视频时 CPU 直接打满系统彻底失去实时性。原因模型选型时只考虑了精度定了过大的网络结构。YOLOv8x 的参数量是 s 的 5 倍在 CPU 上推理速度完全不可接受。深度学习模型的部署落地必须从第一天就考虑算力约束。解决部署环境确定后再定模型体积。CPU 服务器用 YOLOv8n 或 YOLOv8s精度下降但速度能到 200ms 之内。再配合 ONNX 导出和 OpenVINO 推理把整体延迟压到 100ms 左右。如果必须上大模型就把推理框架换成 TensorRT仅限 NVIDIA GPU并用 FP16 精度我的实测效果是在不损失明显精度的情况下推理速度提升约 3 倍。6. 把系统跑起来模型导出到服务化部署的进阶技巧6.1 模型加速与接口封装ONNX 导出加 FastAPI 服务训练好的 PyTorch 模型不能直接用于生产环境一方面推理框架对 PyTorch 模型的支持有限另一方面模型的启动和预热时间太长。我习惯先导出 ONNX 格式再通过 ONNX Runtime 或 OpenVINO 做推理加速。导出命令很简单import torch from ultralytics import YOLO # 加载训练好的模型并导出为 ONNX model YOLO(best.pt) model.export(formatonnx, imgsz640, halfTrue) # halfTrue 导出 FP16 # 分类模型导出 import torch from torchvision import models clf_model models.resnet50() clf_model.fc torch.nn.Linear(clf_model.fc.in_features, 8) checkpoint torch.load(best_cls.pth, map_locationcpu) clf_model.load_state_dict(checkpoint[model]) clf_model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(clf_model, dummy_input, color_cls.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})参数说明halfTrue只在 GPU 推理时有用如果最终部署在 CPU 上就不要导 FP16 模型因为很多 CPU 不支持 FP16 加速反而转回 FP32 更稳。dynamic_axes设置动态 batch这样接口层可以灵活传入多张图片。服务化封装我通常用 FastAPI代码量小、性能足够。接口接收图片返回车辆特征结构化 JSONfrom fastapi import FastAPI, UploadFile import onnxruntime as ort import numpy as np import cv2 app FastAPI() # 初始化 ONNX Runtime 会话 det_session ort.InferenceSession(best.onnx) clf_session ort.InferenceSession(color_cls.onnx) app.post(/vehicle/analyze) async def analyze(file: UploadFile): # 读取并预处理图片 img_bytes await file.read() img cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) # 检测车辆 det_input preprocess_det(img) # 缩放、归一化、转 NCHW det_outputs det_session.run(None, {images: det_input}) # 对每个检测框做分类 cars [] for box in det_outputs[0]: crop img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] color clf_session.run(None, {input: preprocess_cls(crop)}) cars.append({ bbox: box[:4].tolist(), confidence: float(box[4]), color: color_label[int(np.argmax(color[0]))] }) return {vehicle_count: len(cars), vehicles: cars}这个接口能直接跑起来给前端或业务系统调用。注意部署时要做三件事设置并发上限防止 OOM、增加超时控制、把主要耗时环节做异步化。不然同一时间涌入多路视频时服务会无响应这是我踩过的真实教训。6.2 系统验收方法拿一段真实监控视频做端到端回放模型部署完不等于系统做完。我的习惯是拿一段 10 分钟的真实监控视频做端到端验收而不是单张图片测试。记录三个维度每辆车的检出数是否正确、车牌识别准确率、颜色和品牌分类是否符合人工判断。具体做法是先把这段视频抽帧用系统跑一遍生成标注结果再把结果画回视频上逐帧人工核对。验收时特别注意跨天时段。上午 10 点和晚上 10 点的同一批车识别结果差异可能很大。我在项目里吃过这个亏——白天测完觉得万无一失结果第二天领导晚上去看现场识别率肉眼可见地下降。后来我在验收报告里强制要求分时段打表白天的准确率和夜间的准确率分开统计达不到标准不验收。最后说一个我个人的教训做这类系统永远不要只看整体准确率一定要按“颜色/车型/品牌/车牌”四个维度分别统计。品牌准确率 90% 而车牌准确率只有 60%这不叫系统达标只能算半成品。宁可每项都是 80 分也不要单项 99 分、另一项 30 分——因为现场用户只会用最短的那块板子评价你。现在我做验收的第一件事就是先把各特征维度拆开跑一次指标再决定要不要继续调。希望帮到你少走我走过的弯路。本文还有配套的精品资源点击获取