2026/9/27 23:04:03

车牌识别系统从模型到Web:YOLOv8+Django全栈实践

车牌识别系统从模型到Web:YOLOv8+Django全栈实践 简介一套基于Python、Django与MySQL 8.0构建的车牌识别系统面向毕业设计、课程作业及智能交通应用场景。系统采用YOLO深度学习算法完成车牌目标检测并结合OCR技术提取车牌号码同时支持新能源、普通车及黄牌货车的类别识别。管理员侧涵盖账号管理、用户信息管理和识别统计用户侧提供单张与批量识别、结果查看及个人资料维护功能划分清晰适合作为完整项目参考或二次开发基础。资源压缩包约42.44MB共479个文件包括76个Python源码、105个编译后的pyc文件、YOLO配置与权重yaml、pt、pth、前端页面html、css、js、图片及字体文件、数据库脚本sql及环境配置说明dockerfile、sh等目录结构完整便于按模块学习或部署运行。已有45人浏览学习适合需要搭建完整车牌识别Web系统、撰写毕业论文或完善课程设计的学生与开发者使用。1. 车牌识别系统从模型到 Web这条全栈路线为什么值得照着做把车牌识别做成一个能交出去的毕业设计最难的往往不是模型训练而是让模型在一个 Django 工程里稳定地跑起来。绝大多数开源项目只给了检测训练代码等你接手时才发现模型加载到哪个位置、上传接口怎么返回坐标和车牌号、识别记录怎么保存、前端怎么把蓝色框画到原图上这些衔接问题全得自己补。这篇笔记把这条路线完整走一遍从深度学习车牌识别的检测加字符识别两级选型到模型接入 Django 做 REST 接口和 WebSocket 进度推送再到论文、开题和 PPT 的呈现要点。适合正在做毕设的学生也适合给停车场或园区道闸加一个车牌识别模块的开发者。2. 先跑通识别用 PyTorch 把车牌检测和字符识别做出来2.1 两级结构为什么比“一张图直接出车牌号”更稳不少新人上来就找端到端模型想输入一张图片直接输出“京A12345”省去检测和识别两套流程。真这么做会发现模型需要对整张图的背景、车灯、行人和车牌同时建模复杂场景下定位稍微偏一点后面字符全错而且你怎么查都查不出是哪里出了问题。所以我更推荐检测识别两级结构先用目标检测网络把车牌区域框出来再把裁剪后的车牌小图交给字符识别网络。这样每一级都能单独调参、单独替换识别错了能判断是框没框准还是字符看错了。早期很多车牌识别论文走的是传统路线用边缘检测加形态学找车牌再用模板匹配读字符。这套思路在光线均匀、角度正的场景下效果还可以一旦碰上夜间逆光、车身反光、车牌有泥污效果就很不稳定。深度学习 CNN 加数据增强之后同样一张夜间图片的鲁棒性会好很多。Matlab 做车牌识别的老代码网上还能找到但环境装起来费劲出图效果和答辩说服力都不如 PyTorch 方案强。2.2 用 YOLOv8 训练车牌检测器数据准备和训练命令检测部分我一般直接用 YOLOv8它是当前最容易上手的开源检测框架自己不用手写 NMS也不用管 anchor 分配。训练前先准备数据集公开的 CCPD 车牌数据集是主流选择里面包含停车场、道路、不同光照下的车牌照直接用来做毕设数据量是够的。能再补充几十张自己手机拍的校园停车场照片更好可以让模型适应你答辩现场要演示的环境。数据集目录建议整理成 YOLO 需要的格式images 和 labels 分开标签文件里每一行是“类别、中心点x、中心点y、宽、高”坐标全部归一化datasets/ plate/ images/ train/ 0001.jpg 0002.jpg val/ 0010.jpg labels/ train/ 0001.txt 0002.txt val/ 0010.txt plate.yamlplate.yaml 里只需要写三行关键内容训练路径、验证路径、类别名。类别只有一个叫 plate。写好后用一条命令启动训练pip install ultralytics yolo taskdetect modetrain modelyolov8n.pt datadatasets/plate/plate.yaml epochs100 imgsz640 batch16 device0参数里 yolov8n.pt 是轻量权重显存占用小训练一轮快适合在单卡 GPU 上完成毕设实验。imgsz640 指输入图片缩放边长CCPD 原图普遍较大640 能保证速度和精度的平衡。batch16 根据显存调整6GB 显存降到 8 也能跑。device0 是使用第一块 GPU没有 GPU 就写 devicecpu但训练时间会成倍拉长。2.3 用 LPRNet 做字符识别字符集、CTC 解码和推理车牌区域裁出来后下一步是把小图转成车牌号字符串。这里我用 LPRNet它是专为车牌字符识别设计的轻量网络最大的特点是训练时不需要把字符一个个分割开直接用 CTC Loss 对齐整串字符。对毕设来说省去了字符分割这个最容易出错的环节。训练好之后模型输出的是一个“序列长度 × 字符类别数”的概率矩阵需要做一步 CTC 贪心解码才能得到最终文本。中国车牌字符集由三部分组成31 个省份简称、24 个英文字母去掉 I 和 O避免和数字 1、0 混淆、10 个数字。整合起来是 65 类推理时再加一个 CTC 的 blank 空白类。下面是一段可直接运行的解码逻辑import torch CHARS 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新 \ ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 def greedy_decode(probs: torch.Tensor) - str: # probs shape: [batch, seq_len, num_classes] pred probs.argmax(dim2).squeeze(0).tolist() chars [] last -1 for idx in pred: if idx ! last and idx len(CHARS): chars.append(CHARS[idx]) last idx return .join(chars)这段代码做的事情很简单沿序列方向逐个取概率最大的类别下标然后合并相邻的重复字符。注意示例里 blank 类别下标假定为 len(CHARS)所以只在 idx len(CHARS) 时才输出字符。推理前要把裁剪出来的车牌图缩放到高 24、宽 94转 RGB 或灰度都可以归一化后直接 forward 一次解码得到的字符串就是最终车牌号。3. 把模型接进 Django上传、推理、返回 JSON 的一站式接口设计3.1 Django 工程目录怎么摆给模型单独开一个 app很多人拿到训练好的权重第一个动作就是往 Django 的 views.py 里 import torch然后一股脑把所有逻辑写进去。短期看能跑后面要加 WebSocket、要加历史记录、要加前端画框时代码会越改越乱。我一般会把 Django 工程按职责拆成多个 app模型相关的代码单独放不跟页面逻辑混在一起。plate_project/ manage.py config/ # 项目配置、路由、wsgi detection/ # 车牌检测封装 YOLO 模型 ocr/ # 车牌字符识别封装 LPRNet web/ # 上传接口、识别记录、前端模板 static/ templates/detection 和 ocr 两个 app 里只放模型的加载和推理函数不写视图。web 这个 app 负责处理 HTTP 请求把 detection 和 ocr 当普通 Python 模块来 import。这样后续哪怕把模型换成 ONNX 或者接到一台独立的推理服务器Django 的业务代码都不用动。对于毕业论文这种规模的项目这种结构已经足够清晰答辩老师问起系统设计也能讲得明白。3.2 用 DRF 写上传接口预处理、推理、返回 JSON接口层我用 Django REST Framework它对文件上传和 JSON 返回处理得比原生 Django 更顺手。视图接收 multipart 格式的图片文件图像数据先在内存里解码成 numpy 数组再传给检测和识别模块。这样避免把临时图片写到磁盘再读一遍接口响应能快几十毫秒对演示场景也算个小优化。# web/views.py import cv2 import numpy as np from rest_framework.views import APIView from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.response import Response from detection.yolo import get_plate_boxes from ocr.lprnet import recognize_crop class PlateRecognitionView(APIView): parser_classes [MultiPartParser, FormParser] def post(self, request): file request.FILES.get(image) if not file: return Response({error: image is required}, status400) np_arr np.frombuffer(file.read(), np.uint8) img cv2.imdecode(np_arr, cv2.IMREAD_COLOR) boxes get_plate_boxes(img) items [] for box in boxes: x1, y1, x2, y2 box[:4] crop img[y1:y2, x1:x2] plate, conf recognize_crop(crop) items.append({ box: [x1, y1, x2, y2], plate: plate, conf: round(conf, 3) }) return Response({count: len(items), items: items})parser_classes 决定接口能解析哪种请求体这里只开 form 和 multipart避免不必要的 JSON body 解析。imdecode 的参数 IMREAD_COLOR 强制转成 BGR 三通道因为 YOLO 和 LPRNet 的预处理都默认输入三通道。box 里返回的坐标是原图坐标系前端画框时可以直接用。置信度 conf 保留小数点后三位前端可以根据这个值过滤掉低于 0.5 的误检框。3.3 识别进度实时可见用 Django Channels 做 WebSocket 推送一张复杂图片从上传到识别完成可能要两三秒如果前端只是转圈干等用户很容易以为接口挂了。网上常搜的“python django websocket 实现后台有数据前端推送”在车牌识别系统里正好有个合适的落点识别过程分成检测、字符识别、保存记录三个阶段把状态实时推到前端体验会好很多。Django 里实现 WebSocket 通常走 Channels 这个库它把 WebSocket 请求映射到 AsyncWebsocketConsumer 类。# web/consumers.py import asyncio import json from channels.generic.websocket import AsyncWebsocketConsumer class ProgressConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept() async def receive(self, text_data): msg json.loads(text_data) if msg.get(action) start: stages [检测车牌, 识别字符, 保存记录] for stage in stages: await self.send(text_datajson.dumps({ stage: stage, status: running })) await asyncio.sleep(0.3) await self.send(text_datajson.dumps({ stage: done, status: finished }))这段代码只是演示推送节奏真正工程里不会让 WebSocket 直接跑模型推理因为异步消息循环里不能放同步阻塞任务。更可靠的做法是前端收到“检测车牌”后再单独发 POST 请求做识别WebSocket 只负责过程中的状态通知。路由配置记得在 config/routing.py 里加上 websocket 的 URL 映射前端用 new WebSocket(ws://127.0.0.1:8000/ws/progress/) 连接。4. 从接口到可用系统识别记录、前端绘制和部署4.1 用 Django 的 ORM 管理识别历史保存、查询、删除毕设系统不能只做实时识别通常还要有一个历史记录页面能看每天识别了多少车。这块直接落在 Django 的 ORM 上比手写 SQL 省事。我给上传接口加一个辅助函数识别成功把车牌号、置信度、原图路径一起落库。模型定义好之后迁移命令 python manage.py makemigrations 和 python manage.py migrate 按顺序跑一遍即可。# web/models.py from django.db import models class PlateRecord(models.Model): plate_no models.CharField(max_length16, help_text识别的车牌号) confidence models.FloatField(default0.0, help_text置信度) image models.ImageField(upload_toplates/%Y%m/, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at]查询历史记录时用 PlateRecord.objects.all()过滤低置信度的识别结果可以写 filter(confidence__lt0.6)。删除操作也很简单先 count 看影响多少行再调 delete()low_conf_qs PlateRecord.objects.filter(confidence__lt0.6) print(low_conf_qs.count()) low_conf_qs.delete()这里的 delete() 是硬删除数据直接从数据库里没了。毕设系统一般不需要软删除但答辩时老师可能会问“误删了怎么办”提前想好这一层答“生产环境可加 is_deleted 字段做软删除”会显得考虑得更全面。4.2 前端页面上传照片后把车牌框叠在图上前端没必要上 Vue 或者 React对单个上传页来说原生 HTML 加少量 JavaScript 足够。核心交互是用户选图 → POST 给后端 → 拿回车牌框坐标 → 在原图上画矩形并显示车牌文字。画框用 canvas把图片绘制到 canvas 上再用 strokeRect 精确复刻识别结果里的坐标。input typefile idplateInput acceptimage/* canvas idboxCanvas/canvasconst canvas document.getElementById(boxCanvas); const ctx canvas.getContext(2d); image.onload () { canvas.width image.width; canvas.height image.height; ctx.drawImage(image, 0, 0); data.items.forEach(item { const [x1, y1, x2, y2] item.box; ctx.strokeStyle #00ff00; ctx.lineWidth 4; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); }); };这里最容易被忽略的是 canvas 的宽高必须和图片原始像素尺寸一致。如果先给 img 标签设了 CSS 缩放再用显屏尺寸去画框坐标会整体偏移。要么以原始尺寸为准要么在画框前统一乘上缩放比例两种做法二选一混着用必出乱框。4.3 部署到 Linux 服务器gunicorn 和 nginx 怎么配合毕设答辩现场通常不在自己电脑上把系统部署到一台 Linux 服务器会更稳妥也符合“项目实战”的定位。Django 自带的 runserver 只适合开发正式跑我要用 gunicorn它能用多 worker 承载并发请求。模型推理比较耗时worker 数不能开太多否则内存和显存容易被打满。pip install gunicorn gunicorn config.wsgi:application --workers 2 --threads 4 --timeout 120workers2 是因为每个 worker 都会把 PyTorch 模型加载到内存开太多 8GB 内存的服务器会直接卡死。threads4 让每个 worker 能同时处理轻量请求图片推理的耗时主要在模型 forward 上多线程对 CPU 推理也有一定帮助。timeout120 是给模型推理留足余量默认 30 秒会频繁杀掉慢请求。gunicorn 直接监听本地端口前面由 nginx 负责接收外部 HTTP 请求同时托管收集好的静态文件。部署前先执行 python manage.py collectstatic把零散的 CSS、JS、上传图片统一收集到 static 目录nginx 再用一个 server 块把 /static/ 路径指过去。这样关掉 DEBUG 之后前端页面样式也能正常加载。5. 车牌识别系统的常见坑从训练不收敛到答辩被问倒5.1 新能源绿牌被当成蓝牌类别不平衡让检测器学偏现象训练集以蓝色燃油车牌为主上线后遇到绿色新能源车牌要么检测框直接丢失要么框的位置偏大把半个车头都圈进去后续字符识别全乱。原因CCPD 数据集里蓝牌样本占绝对多数绿牌样本太少检测模型对蓝色方块特征过拟合。解决从“训练不多类别就多做数据增强”这条原则出发给绿牌图做随机亮度调整和 HSV 扰动同时收集几十张真实绿牌照片放进训练集重新跑一轮 fine tune。5.2 模型加载一次把 4GB 显存吃光每个请求都新建模型现象接口第一次调用很正常第二次开始变慢第三次直接 OOMGPU 显存一路涨到满。原因推理代码写在了视图内部每一次请求都会重新实例化一次 torch 模型前一个模型的显存还没释放后一个就挤上来了。解决把模型加载放到模块层面整个进程启动时只加载一次再加上一个全局锁避免多个请求同时进入模型 forward。代码上就是 detection/yolo.py 模块里加一行 plate_model load_model()视图只负责调用。5.3 中文车牌识别乱码成“”字符表和解码器没对齐现象字母数字全对省份简称大部分是错的或者输出一串不存在的字符。原因训练代码里字符表顺序和推理代码里 CHARS 常量的顺序不一致贪心解码时拿到的类别下标对应了错误的字符。解决字符表只在单独一个 constants.py 里定义一份训练和推理都从这个文件 import不要在两处代码里各抄一遍。顺手在解码前打印一下模型输出的类别分布能快速验证字符表顺序对不对。5.4 Django 关掉 DEBUG 之后图片 404静态文件托管没配现象开发时上传图片和前端 CSS 都正常把 DEBUG 改成 False 后所有静态资源全部打不开。原因Django 在 DEBUGTrue 时会自动处理静态文件路由DEBUGFalse 后这个机制被关闭必须由外部服务器或中间件接管。解决settings.py 里配好 STATIC_ROOT部署前执行 collectstatic再用 nginx 托管 static 目录本地最快速验证方案是用 WhiteNoise 库把静态文件交给 Django 自己的 WSGI 进程处理。5.5 答辩 PPT 里写着 99% 准确率老师一追问就翻车现象PPT 放一个大大的识别准确率 99%老师问测试集多少张、模型对夜间图预测 FPS 多少、误检样本长什么样现场答不上来。原因所谓准确率大概率是在训练集上统计的或者只统计了识别成功的一部分评估口径不严谨。解决别在 PPT 里写单一准确率改成一组可复现指标mAP0.5、FPS、字符识别 CER放 5 张夜间或遮挡的失败案例主动说明失败原因和后续改进思路。真实数值也许没那么漂亮但可信度比 99% 高得多。6. 答辩季的进阶验证与演示技巧6.1 加一套可复现的评估命令mAP 和 CER 比一个准确率硬验证检测模型不必手写评估脚本Ultralytics 训练完自带验证命令跑一遍就能输出 mAP50、mAP50-95 和推理速度yolo taskdetect modeval modelruns/detect/train/weights/best.pt datadatasets/plate/plate.yaml字符识别部分建议按字符错误率 CER 评估抽 500 张车牌图统计识别结果里每个字符的错误比例再把错误样本按“汉字错、字母错、数字错、多字少字”分成四类做成一张统计表放进论文。这套数据在答辩时比一句“准确率很高”有价值得多老师问任何细节你都能说出具体数字和评估方法。6.2 PPT 现场演示的兜底方案录屏优先live Demo 次之现场演示最大风险是网络或服务器临时出问题。我的习惯是给 PPT 放一段提前录好的识别演示视频从上传图片到框出车牌、显示置信度全程录屏时长控制在 40 秒内。视频演示稳定不会因为现场断网翻车。实在要 live Demo建议用本地虚拟机提前把 gunicorn 启动好上传用静态预置好的测试图片不要现场现拍照片。我做过这类项目的另一条教训是所有权重文件、数据划分表、评估输出全部和项目代码放在同一个目录结构里答辩前完整重启一遍服务并重跑一次回放脚本确保整个流程可复现而不是打一次通宵补丁。希望帮到你。本文还有配套的精品资源点击获取