车牌检测与识别这个方向,几乎是每个做计算机视觉的人都会碰一遍的"入门实战题"。原因不复杂:它有明确的目标框、有清晰的字符语义、有落地的业务场景,同时难度又刚好卡在"纯分类太简单、通用检测太难"的中间地带。我这里要聊的这套系统,用的是 YOLOv8 做车牌定位,配合字符识别模块完成整串号码还原,外层套一个 PyQt5 桌面界面,附带完整数据集、训练代码和推理源码。整套流程从数据采集一直跑到可以双击 exe 出结果,中间踩过的坑不算少,我把能复现的部分全部整理出来了。适合刚学完 YOLOv8 基础、想找一个完整项目练手的朋友,也适合已经会训模型、但卡在"怎么把模型变成能用的软件"这一步的同学。下面不按教科书顺序讲,就按我实际开发的推进顺序来。
1. 项目整体架构与技术选型背后的取舍
动手之前先想清楚一件事:这套系统的边界在哪。车牌检测识别看起来是一个任务,实际拆开至少包含三个子问题——车牌在哪里(定位)、车牌是斜的还是正的(矫正)、车牌上的字符是什么(识别)。很多新手一上来就想训一个端到端的大模型直接输出字符串,结果发现标注成本翻了几倍,精度还上不去。我最终选的是"检测 + 矫正 + 识别"三段式流水线,每一段都独立可调、独立可替换,出问题的时候能快速定位是哪一环拖了后腿。
1.1 为什么拆成两段而不是端到端
端到端方案听着诱人,但车牌场景有几个现实约束让它不太划算。第一是标注成本:要让模型直接输出字符序列,你需要做序列标注或者字符级检测框标注,一张图的工作量是单纯框一个车牌的三到五倍,而我手里几千张图,逐字符标完基本没时间做别的了。第二是可解释性:两段式跑出来的中间结果是可视的,检测框歪了能立刻看出来,识别错了也能追溯是分割切错还是分类分错,端到端只有一个黑盒输出,调优全靠猜。
第三是可维护性。车牌规格是会变的,颜色、位数、字符集都可能调整。两段式里,检测部分只管"这是不是车牌",识别部分只管"上面是什么字",换识别模型不影响检测,换检测模型也不影响识别。这种解耦在真实项目里价值很高,尤其是当你想把 YOLOv8 换成别的检测器做对比实验时,几乎零成本。
不过两段式也有代价:中间环节多了,误差会累积。检测框如果偏了或者漏了,后面识别再准也白搭。所以我在设计时把检测的召回率放在第一位,宁可多框几个候选,也不能漏掉真车牌,后面用简单的面积、长宽比过滤把误检剔除掉。
1.2 YOLOv8 三个尺寸模型在实际场景的对比
YOLOv8 官方提供了 n、s、m、l、x 五个尺寸,我实际只在 n、s、m 三个里选,因为车牌检测本身属于"目标明确、特征强烈"的任务,用大模型属于浪费。我做了三组对照实验,测试环境是 GTX 1660Ti 6G,输入 640×640,数据集约五千张:
| 模型 | 参数量 | 单帧推理耗时(GPU) | mAP@0.5 | 模型体积 |
|---|---|---|---|---|
| YOLOv8n | 3.2M | 约 6ms | 0.972 | 约 6MB |
| YOLOv8s | 11.2M | 约 11ms | 0.981 | 约 22MB |
| YOLOv8m | 25.9M | 约 24ms | 0.983 | 约 50MB |
数据摆在这就很清楚了:s 相比 n 提升了约 0.9 个点,代价是体积涨了三倍多、速度慢了一倍;m 相比 s 几乎没提升,纯属浪费。所以如果你的部署环境是普通显卡或者 CPU,直接上 n;如果对精度有执念、且显存够,用 s。我最终交付版本默认用 n,因为要在低端设备上也能跑起来,同时留了 s 的权重让用户自己切换。
这里顺带提一句轻量化的事。网上经常能看到"仅 5MB 的目标检测模型"这种说法,其实 YOLOv8n 本身就差不多这个量级,真正省体积的地方不在于换更小的架构,而在于导出格式。用 ONNX 导出并做 FP16 量化,体积能再砍一半,速度还有提升,后面部署章节会细讲。
1.3 系统整体的数据流设计
把整条链路画清楚,对后面写代码帮助极大。我的设计是:输入源(图片/视频/摄像头)→ 帧预处理(缩放、归一化)→ YOLOv8 检测车牌框 → 框裁剪并按置信度排序 → 透视矫正 → 字符识别 → 后处理规则校验 → 结果渲染回界面。
每一步都有明确的输入输出契约,我甚至把中间结果都做了可视化开关,调试的时候能看到"框在哪、矫正后长啥样、识别出的原始字符串是什么"。这个习惯是从前辈那学的,看起来多写了几十行代码,实际上省下的调试时间远不止这些。尤其是字符识别死活不准的时候,你把矫正后的图调出来一看,发现是透视变换参数写反了,五分钟就解决了。
2. 数据集制作:从采集到标注的完整链路
模型精度上限由数据决定,这句话在车牌任务里体现得特别明显。我见过太多人拿网上随便下的几百张图就开训,训完发现模型只认白天、只认正面、只认蓝牌,稍微换个角度或者晚上就全瞎。所以这一章我把数据从哪来、怎么标、怎么增强讲透。
2.1 车牌框标注的规范与常见错误
标注工具我用的是 labelImg,格式选 YOLO txt。类别只有一类,叫 plate,不要自作聪明去区分蓝牌绿牌黄牌,颜色信息交给后面的识别模块去判断更好。标注时有几个细节必须抠:
框的边界要贴合车牌的实际边缘,包括车牌的外框边框。我见过有人只框住字符区域,把上下白边排除在外,这样训出来的模型在裁剪阶段会切掉部分字符,识别直接崩。正确的做法是框住车牌整个矩形区域,让边框完整包含。
倾斜车牌怎么办?不要试图用旋转框(OBB),YOLOv8 虽然支持 OBB 任务,但会增加标注复杂度和训练难度。我的做法还是用水平外接矩形,把倾斜车牌整个罩住,然后在识别前做透视矫正。这样标注快、训练稳,矫正环节用 OpenCV 十几行代码就能搞定。
还有一个高频错误是同一张图里车牌被重复标注,或者框的大小差异极大。前者会让模型学到矛盾的监督信号,后者会让回归分支震荡。我的经验是,标注完用脚本跑一遍检查,统计一下所有框的长宽比分布,正常车牌长宽比大概在 2.5 到 4.5 之间,超出这个范围的框基本都有问题,人工复核一遍能删掉不少脏数据。
2.2 数据增强参数的实操选择
YOLOv8 自带的数据增强很丰富,但默认参数是给通用目标检测调的,直接用在车牌上有几个坑。默认的 mosaic 概率是 1.0,也就是每张图都做四图拼接,这对车牌有风险——拼接后车牌可能被切到边缘甚至截断,模型会学到半截车牌的特征。
我的配置是把 mosaic 概率降到 0.5,并且开启 close_mosaic,在训练最后 10 个 epoch 关掉 mosaic,让模型在真实分布上收敛一把。degrees 旋转我设成 5 度,因为实际场景里车牌倾斜主要靠透视矫正处理,不需要模型去硬扛大角度旋转。translate 设 0.1,scale 设 0.5,这两个是安全范围内的温和增强。
色彩方面,hsv_h 设 0.015,hsv_s 设 0.7,hsv_v 设 0.4。为什么饱和度和亮度放得比色相大?因为车牌的关键信息在结构而不在颜色,你可以把蓝牌染成偏青色、把亮度压暗模拟夜间,但只要形状和字符还在,模型就该认出来。反过来,如果色相变化太大,绿牌被调成蓝牌的样子,反而会引入错误的监督信号。
提示:夜间和逆光样本一定要真实采集,别指望靠亮度增强"造"出来。我试过纯靠增强模拟夜间,模型在真实夜间图上召回率掉了将近二十个点,后来补了七八百张真实夜间图,问题立刻缓解。
2.3 字符识别数据集怎么切分
检测部分用 YOLO 格式就够了,识别部分要单独准备。我用的是 CRNN + CTC 的方案,标注格式是"图片路径 + 空格 + 车牌字符串",每行一条。这里的核心难点在于中文字符。
蓝牌第一位是省份简称,共 31 个(含港澳学警等特殊前缀,按你的场景裁剪),后面是字母和数字。绿牌是八位。这些都要在字典里定义好,字典顺序必须和训练时一致,否则推理出来的索引对应错字符,结果全乱。
切分字符的训练图我直接用检测框裁剪出来的原图,不额外做精细的字符分割。因为 CTC 的好处就是不需要对齐,模型自己学会在序列里找字符边界。这样做的好处是省事,坏处是字间粘连或者有污渍时会识别成重复字符,需要用后处理去重。
数量上,每个字符至少保证出现两百次以上,稀有省份简称(比如某些字母开头的特殊车牌)如果样本太少,我建议直接在字典里合并或者排除,别硬撑。样本不均衡带来的长尾问题在字符分类里非常致命,一个只出现十次的字符,模型基本学不会。
3. YOLOv8 训练全流程实战
数据和结构都定了,接下来就是训练。这部分我把它拆成环境、参数、监控三块,每块都给出可以直接抄的配置和踩过的坑。
3.1 环境搭建与版本踩坑
环境这块,我强烈建议单独建一个 conda 环境,不要和系统 Python 混用。基础组合是 Python 3.8 到 3.10 之间挑一个,我个人用 3.9,兼容性最好。PyTorch 版本要和 CUDA 匹配,如果你用 1660Ti,CUDA 11.8 加 torch 2.0 以上是稳妥的搭配。
安装 ultralytics 直接 pip 装最新版即可,但要注意它会顺带装一个特定版本的 opencv 和其他依赖,如果系统里已经有旧版本,可能出现 numpy 版本冲突导致 import 报错。我的解决办法是先装 torch,确认 torch.cuda.is_available() 是 True,再装 ultralytics,这样能把依赖顺序理清楚。
PyQt5 是另一个重灾区。PyQt5 和 PyQt5-tools 版本必须对应,我见过因为版本错位导致 designer 打不开的情况。稳妥做法是 pip install PyQt5==5.15.9 PyQt5-tools==5.15.9.3.3,版本锁死,别追新。另外在部分机器上,PyQt5 界面会因为没有正确加载 OpenGL 而显示异常,这个后面排查章节细说。
3.2 训练参数配置与显存估算
我用的训练命令大致是这样:
yolo task=detect mode=train model=yolov8n.pt data=plate.yaml epochs=100 imgsz=640 batch=16 workers=4 device=0其中 batch 是关键。1660Ti 只有 6G 显存,batch=16 加 imgsz=640 基本能跑满,如果报显存不足就把 batch 降到 8,同时把 workers 也降一点,因为 DataLoader 开太多进程也会吃内存。别一上来就 batch=32,除非你是 12G 以上的卡。
学习率我不怎么动,YOLOv8 默认用的是带 warmup 的自适应策略,初学率 0.01 配合余弦退火,对大多数数据集都够用。优化器默认 SGD,如果你想收敛快一点可以换 AdamW,但要注意改优化器时学习率也要相应调小一个量级。
如果你想做迁移学习并且冻住一部分主干,可以用 freeze 参数。freeze=10 会冻结前 10 层,适合数据量小、想快速出结果的场景。但车牌任务和 COCO 差异不小,我建议要么不冻,要么只冻前 5 层,冻太多反而限制模型适应新任务。
关于训练轮数,我的经验是:五千张图、单类别检测,70 到 100 个 epoch 足够,一般到 80 轮左右指标就平了。盯着 results.csv 里的 mAP50-95,如果连续 15 轮没有提升就可以考虑停了,YOLOv8 自带的 patience 参数设成 20 就能自动早停。
3.3 损失曲线怎么看、什么时候停
训练完第一件事是画损失曲线。YOLOv8 训练结束会在 runs 目录下生成 results.png,包含 box_loss、cls_loss、dfl_loss 三条损失和对应的验证指标。怎么看这几条线,是有讲究的。
如果训练损失一直下降但验证损失开始抬头,这是典型的过拟合,说明模型在背训练集,这时候要么加数据、要么加增强、要么提前停。如果训练损失震荡很厉害,通常是学习率太大或者 batch 太小,可以调小学习率或者开启梯度累积。如果三条损失都平得很早、指标上不去,那多半是数据有问题,比如标注错误或者类别太单一,这时候该回头看数据而不是死磕参数。
我还会额外画一张 mAP 曲线。box_loss 降只能说明框回归得准了,但检测不只看框,还看分类。真正决定模型能不能用的是 mAP50-95 这条线,尤其是 0.5 到 0.95 这个严格区间的表现,如果这条线只有 0.5 左右,说明模型对框的定位还不够精细,实际使用中裁剪出来的车牌会有偏移。我一般要求 0.5:0.95 至少到 0.8 以上才敢拿去用。
4. 车牌矫正与字符识别实现
检测框出来后,直接扔给识别网络其实效果一般,因为车牌可能是倾斜的、有透视畸变的。这一步矫正做得好,识别准确率能拉高一大截。矫正完再做识别,是整个流水线里技术含量最高的部分。
4.1 透视矫正的坐标计算
矫正的目标是把任意四边形的车牌变成标准矩形。思路是找到车牌的四个角点,然后做透视变换。角点怎么找?简单的方法是直接用检测框的四个角,但检测框是水平矩形,对倾斜车牌不准。好一点的方法是在检测框内做一次边缘检测或者颜色分割,找到车牌区域的轮廓,再用 cv2.minAreaRect 或者 approxPolyDP 取出四角。
我用的是折中方案:先用检测框裁剪出大致区域,转灰度做一遍 Sobel 边缘检测,再找最大轮廓,用 approxPolyDP 拟合四边形。如果拟合出来的四个点长宽比在合理范围内就用它做变换,否则退化回用检测框四角。这样既有鲁棒性,又不会因为边缘检测失败而完全没法用。
透视变换的映射关系是这样的:把拟合出来的四个点按"左上、右上、右下、左下"的顺序排好,映射到目标矩形的四个角。目标矩形的高度我固定设成 48 像素,宽度按车牌原始长宽比乘以 48 得到,一般蓝牌是 440×140,比值约 3.14,所以宽度大约 150。这个尺寸不要乱设,要和后面识别网络的输入尺寸对齐,否则又得多做一次缩放。
def four_point_transform(image, pts): rect = order_points(pts) (tl, tr, br, bl) = rect width = max(int(np.linalg.norm(br - bl)), int(np.linalg.norm(tr - tl))) height = max(int(np.linalg.norm(tr - br)), int(np.linalg.norm(tl - bl))) dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype="float32") M = cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(image, M, (width, height))这段 order_points 的顺序一定要保证是顺时针且从左上开始,如果顺序错了,变换出来的图会翻转或者扭曲,识别必然出错。我在这个函数上栽过一次,调了半天识别精度,最后发现是点的排序逻辑写反了。
4.2 CRNN 识别网络的结构与训练要点
识别网络我用的是经典的 CRNN:CNN 提特征 + RNN 建模序列 + CTC 解码头。为什么不用纯 CNN 直接分类?因为纯 CNN 需要先精确定位每个字符的位置再逐个分类,遇到字符粘连或者边框有干扰就直接崩。CRNN 的 CTC 机制天然允许对齐模糊,鲁棒性好很多。
具体结构上,CNN 部分我用了五层卷积,每层后接池化和 ReLU,输出一个高度为 1、宽度为 W/4 左右的特征图。RNN 部分用两层双向 LSTM,隐藏维度 256。最后接一个全连接层映射到字符集大小加一(多出来的是 CTC 的 blank)。输入尺寸我固定成 32×160,所以前面矫正时算出来的宽度要统一缩放。
训练的核心参数是学习率和批次。CRNN 对学习率比较敏感,我一开始用 0.01 直接发散,后来降到 0.001 才稳定。batch 用 32 到 64 都可以,看显存。损失函数用 CTCLoss,注意设置 zero_infinity=True,否则遇到某些异常样本会报 inf。
训练数据上,我强烈建议加入一些真实的低质量样本——模糊的、有反光的、部分遮挡的。这些样本可以让模型学会从残缺信息里推断,代价是训练前期损失降得慢一些,但最终泛化能力明显更强。当时我加了两百多张夜间反光样本,训练集准确率从 99% 掉到 97%,一开始还挺慌,但测试集上反而涨了三个点。
4.3 后处理规则与车牌字典设计
识别网络输出的原始字符串是没法直接用的,必须做后处理校验。这里有几条规则我总结下来:
第一,长度校验。蓝牌固定 7 位,绿牌 8 位,如果识别出来长度不对,说明有丢字或者多字,需要做修正。常见的修正是用 CTC 的置信度做过滤,把置信度低于阈值(我设 0.6)的字符去掉再补位。
第二,字典约束。第一位必须是省份简称,如果识别出个数字,说明是错的,可以从候选中选置信度最高的合法省份。第二到第七位是字母数字组合,但要注意字母 I、O 容易和数字 1、0 混淆,我在后处理里会把车牌中出现的 I、O 强制替换成 1、0,因为车牌规范本来就不使用这两个字母。
第三,去重规则。CTC 在字符粘连时容易输出重复字符,比如 "京A12345" 变成 "京A112345",这时候简单的连续去重不一定对,因为真实的 "京A11" 这种也可能存在。我的经验是只在长度超限时才做去重,而且要去掉的是置信度较低的那个重复字符。
字典的构建要注意,省份简称要按固定顺序排列,字母数字也是。我的字典顺序是"省份简称 + 数字 + 大写字母 + 特殊字符",索引从 0 开始,最后一个索引留给 CTC blank。这个顺序在训练和推理时必须严格一致,换一次就要重新训一次,否则识别全乱。
5. PyQt5 界面开发与系统集成
模型能跑了,接下来要把它包装成一个能用的软件。这部分很多做算法的同学会忽略,觉得界面不重要,但实际上界面决定了你的项目能不能被别人使用,也决定了演示时的观感。
5.1 界面布局与交互逻辑设计
我的界面分三块布局:左侧是控制区,包含模型加载、选择图片、选择视频、打开摄像头、开始识别这几个按钮;中间是主显示区,展示原图和识别结果;右侧是结果列表,记录每一次识别的车牌号和置信度,支持导出 CSV。
用 QMainWindow + QDockWidget 的布局比纯 QWidget 灵活,因为可以拖动面板,也能适应不同分辨率。这里有个坑:网上搜"PyQt5 适配分辨率",大部分方案是加缩放因子或者动态计算尺寸,我实测下来最稳的还是设置固定的最小尺寸,然后让布局管理器自己撑开,不要手动去写死的坐标。
字体和控件样式我会统一设置一遍 QSS,用一套深色主题,因为深度学习工具类软件深色看起来更专业,也更护眼。控件大小上,按钮高度设 36 到 40 像素,用户点起来舒服,太小了容易点错。
5.2 多线程推理避免界面卡死
这是新手最容易犯的错:直接把推理代码写在按钮的点击回调里。单张图片还好,一旦处理视频或者摄像头,主线程被推理阻塞,界面直接假死,连关闭按钮都点不动。
正确做法是用 QThread 或者 QThreadPool 把推理放到工作线程。我的实现是定义一个 Worker 类,继承 QObject,里面封装推理逻辑,再用 moveToThread 挪到子线程。线程和主线程之间通过信号槽通信,把识别结果发回主线程更新界面。
class InferWorker(QObject): finished = pyqtSignal(list) progress = pyqtSignal(int) def run(self, source): results = [] for idx, frame in enumerate(source): boxes = model_detect(frame) texts = recognize(frame, boxes) results.append((boxes, texts)) self.progress.emit(idx) self.finished.emit(results)记得在关闭窗口时把线程正确终止,否则可能出现程序退不干净、后台进程残留的情况。我一般会在 closeEvent 里发一个信号让 worker 停止循环,然后 wait 一下再退出。
摄像头这块还有个细节:OpenCV 的 VideoCapture 读取和 PyQt5 的显示必须解耦,否则帧率会掉得很厉害。我的做法是单独开一个采集线程,每读到一帧就 emit 出来显示,主线程只负责画,不负责读,这样界面流畅度提升明显。
5.3 打包与分发
最后一步是打包成 exe。用 PyInstaller 一条命令就能打包,但有几个坑必须提前避。第一,YOLOv8 的权重文件要作为附加数据打包进去,否则用户拿到 exe 找不到模型。第二,torch 体积巨大,打出来动辄几个 G,如果只是演示,可以考虑用 ONNX Runtime 推理,体积能小很多,速度也不差。
打包命令大致是:
pyinstaller -F -w --add-data "best.pt;." --add-data "chars.txt;." main.py-F 是单文件模式,-w 是去掉控制台窗口。单文件模式启动会慢一点,因为它要先解压到临时目录,如果在意启动速度可以用 -D 目录模式。打包后务必在一台干净的机器上跑一遍,因为开发机上装了一堆库,很多问题在开发机上根本暴露不出来。
6. 常见问题排查与独家避坑指南
这一章是我实际开发中踩过的坑,整理成速查的形式,遇到问题直接对号入座。
6.1 环境与依赖类问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| import cv2 报 numpy 版本冲突 | numpy 版本过高或过低 | 卸载重装 numpy 到 1.24 左右 |
| PyQt5 界面空白或黑屏 | OpenGL 未正确加载 | 设置显卡驱动为最新,或改用软件渲染 |
| torch.cuda.is_available() 为 False | CUDA 与 torch 版本不匹配 | 按官网对应表重装 torch |
| 训练时报显存不足 | batch 或输入尺寸过大 | batch 减半,imgsz 降到 512 |
| PyQt5 install 特别慢 | 网络或 pip 源问题 | 换国内镜像源,加超时参数 |
关于 PyQt5 界面显示异常的问题,我遇到过好几次,表现是窗口能打开但是控件不显示,或者整个窗口是黑的。排查下来基本都和环境里的图形渲染配置有关。稳妥的排查顺序是:先确认显卡驱动是新的,再确认 PyQt5 版本没有混用,最后试试在代码里设置软件渲染的环境变量。这个问题在远程桌面场景下尤其容易触发,如果你是在服务器上开发,建议直接用本地环境跑界面。
6.2 精度类问题定位思路
识别不准的时候,别急着改模型,先按顺序排查。第一步看检测框,是不是框歪了或者框偏了。第二步看矫正后的图,是不是扭曲了或者被裁掉一部分。第三步看识别网络的输入,是不是尺寸和训练时对不上。第四步才是模型本身的问题。
我遇到过一次识别率莫名其妙掉了三十个点,排查一圈发现是矫正后的图片宽度没做统一缩放,导致 CRNN 的输入尺寸和训练时不一致。这种问题在代码里藏得很深,只有在流程中间把图 dump 出来对比才能发现。
字符混淆也是常见问题,尤其是字母 D 和数字 0、字母 B 和数字 8、字母 S 和数字 5。这类混淆靠模型很难完全解决,最有效的办法是后处理字典约束,结合车牌规范做强制修正。比如国内普通车牌不出现字母 I 和 O,那就直接把它们替换掉;再比如某一位根据位置规则只能是数字,那就从候选中挑数字。
6.3 性能优化与部署经验
GPU 上跑得飞快不代表 CPU 上能跑。如果你的部署环境是纯 CPU,一定要提前做性能测试。我实测过,YOLOv8n 在普通 CPU 上单帧要 80 到 150 毫秒,勉强能跑 8 到 10 帧,如果做视频实时识别会掉帧。这时候要么降输入分辨率,要么用 ONNX Runtime 加量化加速。
ONNX 导出是性价比最高的优化手段,导出后配 ONNX Runtime 推理,CPU 上速度能提升两到三倍,模型体积也小一半。导出时注意 opset 版本要选支持你模型结构的,太老会报不支持算子。量化用动态量化就够,静态量化需要校准数据集,麻烦一点但效果更好。
如果要做多路视频同时识别,建议开多个推理进程而不是多线程,因为 Python 的 GIL 会限制多线程的并行效果。每个进程加载一份模型,各跑各的,这样才能真正吃满多核 CPU。
注意:模型文件在打包或部署时一定要校验一遍 MD5,我就遇到过因为传输损坏导致模型加载报错,排查了大半天才发现是文件本身坏了。
最后分享一个我在实际项目里养成的小习惯:每次训练完,把权重、配置文件、训练日志、当时的 git commit 一起归档到一个带日期的文件夹里。刚开始觉得麻烦,直到有一次换了台机器要复现半年前的实验结果,翻出归档五分钟就跑起来了,而旁边的同事还在群里问"当时那个版本的数据集是哪份"。这个习惯对个人项目尤其重要,因为没人帮你维护版本记录。至于后续还能怎么扩展,我目前在做的是把检测模型换成更轻的骨干,同时加上车牌颜色分类分支,一个模型同时出框和颜色,省掉一次额外的判断逻辑,等效果稳定了再来聊这块。