ARTICLE DETAIL

资讯详情

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

车辆类型自动识别系统毕设实战:从环境搭建到推理部署全链路

车辆类型自动识别系统毕设实战:从环境搭建到推理部署全链路 简介这份资源是一套基于Python的车辆类型自动识别系统完整项目面向计算机视觉方向的本科生及需要完成毕业设计的同学可用于交通监控、停车场管理等场景下的车辆分类需求。项目以Python为开发语言结合OpenCV与TensorFlow、Keras框架采用卷积神经网络实现轿车、SUV、卡车、摩托车等车型的自动识别涵盖数据采集、标注、预处理到模型训练与系统设计的完整流程。压缩包共278个文件约25.54MB包含151张jpg与14张jpeg图像样本、7个py源码文件、11个html与18个css前端页面资源以及xml标注、db数据库、mat数据文件等兼顾算法实现与可视化界面。目前已有117人学习下载。读者可获得一套可直接参考的毕设级项目结构理解CNN车型分类的落地思路并借助前端页面与数据文件快速复现和二次开发。1. 车辆类型自动识别系统从一张车辆照片到结构化数据的完整链路很多同学做毕设时第一反应是“找个开源模型跑一下就行”结果卡在环境配置、数据集标注、模型调参上最后交上去的东西连自己都说不清。车辆类型自动识别系统本质上是一条从图像输入到类别输出的流水线输入是一张车辆图片或一段视频帧输出是轿车、SUV、卡车、巴士等具体类别标签。它解决的是人工分类效率低、主观性强的问题适合安防卡口、停车场管理、交通流量统计等场景。如果你正在找一份能跑通、能讲清原理、还能在答辩时经得起追问的毕设方案这个方向值得认真做。下面我从环境搭建、数据准备、模型选型到推理部署把整条链路拆开讲。2. 环境搭建与依赖安装把 Python 环境配到能跑通第一行推理代码2.1 为什么建议用 Python 3.8 而不是最新版车辆类型识别依赖的深度学习框架对 Python 版本有硬性要求。PyTorch 和 TensorFlow 在 3.8 上的兼容性最稳很多预训练模型和工具链也是按这个版本测试的。如果你用 3.11 或 3.12可能会遇到 numpy 版本冲突、opencv 编译失败等问题。常见做法是装 Miniconda 或 Anaconda建一个独立环境避免污染系统 Python。# 创建名为 vehicle_cls 的虚拟环境指定 Python 3.8 conda create -n vehicle_cls python3.8 -y conda activate vehicle_cls # 安装 PyTorch以 CUDA 11.8 为例没有 GPU 就用 cpu 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 OpenCV、numpy、matplotlib、pillow pip install opencv-python numpy matplotlib pillow # 安装 scikit-learn 用于后续评估指标 pip install scikit-learn逻辑说明先建虚拟环境是为了隔离依赖避免和系统里其他 Python 项目打架。PyTorch 的安装源要选对否则会默认装 CPU 版本训练速度差几十倍。OpenCV 用来读图和做预处理numpy 负责数组运算matplotlib 用来画训练曲线。参数说明-n vehicle_cls是环境名可以改成你喜欢的python3.8必须写不写会装最新版--index-url后面跟的是 PyTorch 官方源国内如果慢可以换清华源但要注意版本对应。提示装完以后用python -c import torch; print(torch.cuda.is_available())验证 GPU 是否可用。返回 False 就检查显卡驱动和 CUDA 版本。2.2 验证环境是否装好的最小测试脚本装完依赖别急着跑训练先写个最小脚本确认 OpenCV 和 PyTorch 都能正常调用。import cv2 import torch import numpy as np # 打印版本信息 print(OpenCV:, cv2.__version__) print(PyTorch:, torch.__version__) print(CUDA available:, torch.cuda.is_available()) # 生成一张纯色图模拟车辆图片输入 img np.zeros((224, 224, 3), dtypenp.uint8) img[:] (128, 128, 128) # 灰色 # 用 OpenCV 做一次缩放和归一化 img_resized cv2.resize(img, (224, 224)) img_tensor torch.from_numpy(img_resized).permute(2, 0, 1).float() / 255.0 print(Tensor shape:, img_tensor.shape)逻辑说明这段代码不涉及模型只验证基础库能不能用。cv2.resize确认 OpenCV 正常torch.from_numpy确认 PyTorch 能接 numpy 数组permute是把 HWC 格式转成 CHW这是 PyTorch 的标准输入格式。参数说明(224, 224)是常见输入尺寸后面模型选型会再调permute(2, 0, 1)里的 2、0、1 分别对应原来的通道、高、宽除以 255.0 是归一化到 0 到 1 之间。3. 数据集准备与标注把原始车辆图片变成模型能吃的格式3.1 车辆类型数据集从哪来、怎么选毕设用的数据集一般有三个来源公开数据集、自己爬取、实验室已有数据。公开数据集里车辆类型识别常用的是 Stanford Cars、CompCars、BIT-Vehicle。Stanford Cars 有 196 类但很多是车型细分对“轿车/SUV/卡车/巴士”这种粗分类来说太细了。BIT-Vehicle 有 9850 张图分巴士、微面、货车、轿车、SUV 等比较适合毕设。如果找不到合适的可以用爬虫从公开图片站抓但要注意版权和标注工作量。常见做法是先确定你要分几类一般 4 到 6 类就够了比如轿车、SUV、卡车、巴士、面包车。类别太多标注成本高模型也难训。每类至少 500 张最好 1000 张以上否则容易过拟合。3.2 用 LabelImg 标注并转成 YOLO 格式如果你做的是检测加分类需要标出车辆位置和类别。LabelImg 是个常用工具装好以后打开图片目录框选车辆选类别保存成 XML。但 YOLO 需要的是 txt 格式每行是类别编号 x_center y_center width height坐标要归一化到 0 到 1。import os import xml.etree.ElementTree as ET # 类别映射根据你的数据集改 classes [car, suv, truck, bus, van] def convert_annotation(xml_path, output_txt_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() with open(output_txt_path, w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.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 f.write(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n) # 示例遍历目录批量转换 for xml_file in os.listdir(annotations): if xml_file.endswith(.xml): xml_path os.path.join(annotations, xml_file) txt_path os.path.join(labels, xml_file.replace(.xml, .txt)) # 图片宽高需要从对应图片读取这里假设已知 convert_annotation(xml_path, txt_path, 640, 480)逻辑说明XML 里存的是左上角和右下角坐标YOLO 要的是中心点加宽高而且都要除以图片宽高做归一化。类别名要映射成数字编号顺序必须和训练时的类别列表一致否则模型学出来的类别会错位。参数说明classes列表顺序就是类别编号0 对应 car1 对应 suv以此类推img_w和img_h必须和实际图片尺寸一致不能随便填:.6f是保留六位小数精度够用。注意标注时框要贴紧车辆边缘不要留太多背景否则模型会学到无关特征。同一张图里如果有多个车辆每个都要标。3.3 数据集划分与 DataLoader 配置标注完以后要分成训练集、验证集、测试集常见比例是 7:2:1 或 8:1:1。用 PyTorch 的 Dataset 和 DataLoader 来加载可以顺便做数据增强。import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import os class VehicleDataset(Dataset): def __init__(self, img_dir, label_file, transformNone): self.img_dir img_dir self.transform transform self.samples [] with open(label_file, r) as f: for line in f: img_name, cls_id line.strip().split() self.samples.append((img_name, int(cls_id))) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_name, cls_id self.samples[idx] img_path os.path.join(self.img_dir, img_name) img Image.open(img_path).convert(RGB) if self.transform: img self.transform(img) return img, cls_id # 定义训练和验证的预处理 train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset VehicleDataset(images/train, labels/train.txt, train_transform) val_dataset VehicleDataset(images/val, labels/val.txt, val_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4)逻辑说明Dataset 负责按索引读一张图和对应标签DataLoader 负责批量组装和多线程加载。训练集用了随机水平翻转和随机旋转是为了增加数据多样性防止过拟合。验证集不做增强只做缩放和归一化保证评估结果稳定。参数说明batch_size32根据显存调显存小就改 16 或 8num_workers4是加载数据的线程数Windows 下如果报错就改成 0Normalize的均值和标准差是 ImageNet 的统计值用预训练模型时必须保持一致。4. 模型选型与训练从 ResNet 到 YOLOv8 的取舍4.1 分类任务用 ResNet50 还是轻量级 MobileNet如果只做车辆类型分类不要求标出位置ResNet50 是个稳妥选择。它在 ImageNet 上预训练过特征提取能力强微调几十个 epoch 就能到不错的精度。但 ResNet50 参数量 25M 左右推理速度在 CPU 上偏慢。如果毕设要求实时性或者部署在边缘设备上MobileNetV3 更合适参数量只有几 M精度掉得不多。常见做法是先用 ResNet50 跑一个 baseline看验证集准确率能不能到 90% 以上。如果达不到再检查数据质量和标注。如果达到了但速度不满足再换 MobileNetV3 或 ShuffleNet 对比。import torch.nn as nn from torchvision import models def build_resnet50(num_classes5): model models.resnet50(pretrainedTrue) # 冻结前面的层只训练最后的全连接 for param in model.parameters(): param.requires_grad False # 替换最后的分类层 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model def build_mobilenet_v3(num_classes5): model models.mobilenet_v3_large(pretrainedTrue) for param in model.parameters(): param.requires_grad False in_features model.classifier[3].in_features model.classifier[3] nn.Linear(in_features, num_classes) return model # 实例化 resnet_model build_resnet50(num_classes5) mobilenet_model build_mobilenet_v3(num_classes5) print(ResNet50 params:, sum(p.numel() for p in resnet_model.parameters())) print(MobileNetV3 params:, sum(p.numel() for p in mobilenet_model.parameters()))逻辑说明pretrainedTrue会加载 ImageNet 预训练权重这是迁移学习的关键。冻结前面的层是为了不让预训练特征被破坏只训练新加的分类层。等分类层收敛后可以解冻部分层做微调精度还能再涨一点。参数说明num_classes5对应你的类别数必须和数据集一致model.fc是 ResNet 的全连接层model.classifier[3]是 MobileNetV3 的分类层不同模型名字不一样改之前先打印模型结构看一眼。4.2 训练循环与关键超参数设置训练循环里要关注学习率、优化器、损失函数和早停。学习率太大会震荡太小收敛慢。常见做法是分类层用 1e-3微调时用 1e-4。import torch.optim as optim from torch.optim.lr_scheduler import StepLR device torch.device(cuda if torch.cuda.is_available() else cpu) model build_resnet50(num_classes5).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.fc.parameters(), lr1e-3) scheduler StepLR(optimizer, step_size10, gamma0.1) best_acc 0.0 patience 5 no_improve 0 for epoch in range(50): model.train() running_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total scheduler.step() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) no_improve 0 else: no_improve 1 if no_improve patience: print(Early stopping) break逻辑说明每个 epoch 先训练再验证验证时用model.eval()关掉 dropout 和 batchnorm 的训练模式。保存验证集上最好的模型而不是最后一个 epoch 的模型避免过拟合。早停是防止验证集准确率不再提升时继续浪费时间。参数说明lr1e-3是初始学习率StepLR每 10 个 epoch 乘 0.1patience5表示连续 5 个 epoch 没提升就停batch_size在 DataLoader 里已经设了这里不用重复。提示如果验证集准确率一直上不去先检查标签有没有错再检查学习率是不是太大。把训练集准确率也打印出来如果训练集很高但验证集很低就是过拟合加数据增强或加 dropout。4.3 用 YOLOv8 做检测加分类的替代方案如果毕设要求同时输出车辆位置和类型YOLOv8 更合适。它把检测和分类合在一个网络里推理速度快部署也方便。用 ultralytics 库几行代码就能训。from ultralytics import YOLO # 加载预训练的 YOLOv8n 模型 model YOLO(yolov8n.pt) # 训练数据配置文件 data.yaml 里写清楚路径和类别 model.train( datadata.yaml, epochs100, imgsz640, batch16, lr00.01, patience20, device0 ) # 验证 metrics model.val() print(mAP50:, metrics.box.map50)逻辑说明YOLOv8 把检测框和类别一起输出不需要单独的分类网络。data.yaml里要写训练集、验证集路径和类别名。训练完以后model.val()会自动算 mAP 等指标。参数说明yolov8n.pt是 nano 版本速度最快精度最低可以换yolov8s.pt或yolov8m.ptimgsz640是输入尺寸batch16根据显存调device0表示用第一块 GPU没有 GPU 就写cpu。5. 避坑与排查车辆类型识别项目里最容易翻车的五个地方5.1 现象训练 loss 不下降准确率一直在 20% 左右原因最常见的是标签和图片没对上。比如图片名是001.jpg标签文件里写的是002.jpg或者类别编号从 1 开始而不是 0。另一个原因是学习率太大梯度直接炸了。解决先打印前 10 个样本的图片名和标签确认一一对应。再把学习率降到 1e-4 试一个 epoch看 loss 有没有变化。如果还是不动检查数据预处理里的归一化是不是把像素值搞成了全 0 或全 1。5.2 现象验证集准确率比训练集低很多差距超过 20%原因过拟合。训练集样本太少或者模型太复杂。车辆类型识别里如果每类只有两三百张图ResNet50 很容易过拟合。解决加数据增强比如随机裁剪、颜色抖动、随机擦除。或者换小模型MobileNetV3 的参数量只有 ResNet50 的十分之一过拟合风险小很多。还可以加 dropout 和权重衰减。5.3 现象推理时图片预处理和训练时不一致结果全错原因训练时用了归一化推理时忘了做或者训练时 resize 到 224推理时用了原图尺寸。OpenCV 读进来是 BGRPIL 读进来是 RGB用混了颜色通道就反了。解决把预处理写成一个函数训练和推理都调同一个。用 PIL 读图就统一用 PIL用 OpenCV 就统一用 OpenCV。归一化的均值和标准差要一模一样。5.4 现象GPU 显存不够报 CUDA out of memory原因batch_size 太大或者图片尺寸太大。YOLOv8 训练时如果 imgsz 设成 1280显存占用会翻好几倍。解决先把 batch_size 减半再把 imgsz 降到 640 或 416。如果还不够用梯度累积小 batch 多跑几次再更新。PyTorch 里可以用torch.cuda.empty_cache()清一下缓存但治标不治本。5.5 现象模型在测试集上表现好但实际拍的照片识别不准原因数据集和实际场景分布不一致。公开数据集里的车大多是白天、晴天、正面角度实际场景可能有夜间、雨天、遮挡、倾斜。解决在数据集里补充实际场景的图片哪怕每类只加几十张也能明显提升泛化。推理时加一些预处理比如直方图均衡化、自适应亮度调整。如果条件允许做在线数据增强让模型见到更多变化。6. 推理部署与效果验证把模型跑成能用的接口6.1 用 Flask 搭一个最小可用的识别接口训练完的模型要能对外提供服务最简单的方式是用 Flask 包一层 HTTP 接口。上传图片返回类别和置信度。from flask import Flask, request, jsonify from PIL import Image import torch import torchvision.transforms as transforms import io app Flask(__name__) device torch.device(cuda if torch.cuda.is_available() else cpu) model build_resnet50(num_classes5) model.load_state_dict(torch.load(best_model.pth, map_locationdevice)) model.to(device) model.eval() classes [car, suv, truck, bus, van] transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) app.route(/predict, methods[POST]) def predict(): file request.files[image] img Image.open(io.BytesIO(file.read())).convert(RGB) img_tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(img_tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, 1) return jsonify({ class: classes[pred.item()], confidence: round(conf.item(), 4) }) if __name__ __main__: app.run(host0.0.0.0, port5000)逻辑说明Flask 接收上传的图片用和训练时一样的预处理转成 tensor送进模型推理。softmax把输出转成概率取最大的那个作为预测类别。unsqueeze(0)是加一个 batch 维度因为模型要求输入是四维。参数说明map_locationdevice保证在 CPU 上也能加载 GPU 训的模型host0.0.0.0让局域网内其他机器也能访问port5000可以改。6.2 用混淆矩阵和置信度分布验证模型真实水平准确率只是一个数字要看模型在哪些类别上容易错。用 sklearn 画混淆矩阵能看出是轿车和 SUV 混了还是卡车和巴士混了。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt # 收集验证集所有预测和真实标签 all_preds [] all_labels [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, xticklabelsclasses, yticklabelsclasses) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png) print(classification_report(all_labels, all_preds, target_namesclasses))逻辑说明混淆矩阵对角线是正确分类非对角线是错分。如果轿车和 SUV 之间错分很多说明这两个类特征太接近需要加更多区分性强的样本。classification_report会输出每个类的精确率、召回率和 F1。参数说明fmtd表示显示整数target_names要和类别列表顺序一致保存图片用savefig不要用show否则在服务器上会卡住。6.3 一个容易被忽略的技巧用 TTA 提升推理稳定性测试时增强TTA是在推理时对同一张图做多种变换把结果平均。比如原图、水平翻转、不同裁剪各跑一次取平均概率。这样能提升 1 到 3 个百分点的准确率而且不需要重新训练。def predict_with_tta(model, img, transform, device): model.eval() probs_list [] # 原图 probs_list.append(torch.softmax(model(transform(img).unsqueeze(0).to(device)), 1)) # 水平翻转 img_flip img.transpose(Image.FLIP_LEFT_RIGHT) probs_list.append(torch.softmax(model(transform(img_flip).unsqueeze(0).to(device)), 1)) # 平均 avg_probs torch.mean(torch.stack(probs_list), dim0) conf, pred torch.max(avg_probs, 1) return pred.item(), conf.item()逻辑说明TTA 的核心是模型对同一张图的不同视角应该给出一致的预测平均以后能抵消一些随机误差。水平翻转是最常用的变换因为车辆左右对称翻转后类别不变。参数说明transpose(Image.FLIP_LEFT_RIGHT)是 PIL 的翻转方法torch.stack把多个概率张量堆起来mean求平均如果显存够可以再加一个中心裁剪。我自己的习惯是每次训完模型先看混淆矩阵再看错分样本的原始图片最后决定是补数据还是调模型。这个流程比盲目调参有效得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表