简介:这是一套面向智能交通与安防监控方向开发者的车牌识别实战资源,将YOLOv8目标检测、Tesseract OCR字符识别与海康摄像头接入流程整合为可运行项目。资源共61个文件,其中包含6个Python训练与推理脚本、数据集标注文件、YOLO权重配置、服务端代码、2段测试视频、GIF演示图及说明文档,压缩包大小约831.95MB,目录结构清晰,便于按模块学习。目前已有424人学习下载。读者可通过源码与配套说明了解车牌定位、字符识别、RTSP视频流接入、模型训练与调优的完整链路,并结合实际摄像头完成从数据准备到实时识别的落地实践与调试优化。资源还提供了数据库初始化脚本与部署服务配置,适合有一定深度学习基础并希望将模型与硬件设备结合的中高级开发者。
1. 车牌识别项目实战:YOLOv8+Tesseract-OCR+海康摄像头,这套组合能做什么
把一套车牌识别从“能演示”做成“能上线”,最常卡住的不是检测模型,而是识别那最后一步。这个项目用 YOLOv8 负责在画面里找到车牌,用 Tesseract-OCR 负责把车牌上的字符读出来,前面再接一个海康摄像头做实时取流,正好凑成一条不用手写一堆图像处理算法、又能自己掌控每一步的实战管线。它适合毕业设计、园区出入口改造、停车场道闸 Demo;如果你正想找一套带项目源码的 YOLOv8 实战来抄,这篇文章就把骨架、代码、参数和坑一起讲透。
2. 先拆系统再写代码:一张车牌从摄像头到文本要经过哪五步
2.1 一条流水线:抓拍、检测、定位、裁剪、识别
车牌识别不是“一个模型全干”,而是五个环节串起来的流水线。海康摄像头负责把物理世界的画面变成视频流;拉帧程序从 RTSP 流里取出一帧;YOLOv8 在这一帧上画出车牌框;程序按框把车牌区域裁剪成小图;最后 Tesseract-OCR 才在这张小图上读字符。很多人一上来就调 OCR 的参数,结果识别率上不去,原因往往在检测框太松、裁剪图太糊,前面两步已经把后面的路堵死了。
| 环节 | 输入 | 输出 | 承担者 |
|---|---|---|---|
| 取流 | 摄像头 RTSP 流 | 单帧 BGR 图像 | OpenCV / FFmpeg |
| 检测 | 单帧图像 | 车牌框坐标 | YOLOv8 |
| 裁剪 | 原图 + 坐标 | 车牌小图 | Python 切片 |
| 预处理 | 车牌小图 | 二值图 / 放大图 | OpenCV |
| 识别 | 预处理图 | 字符串 | Tesseract-OCR |
我习惯把这套流程做成一个独立脚本,而不是堆在 Jupyter 里。原因很简单:项目源码后期要接到道闸、数据库、消息队列上,流程被封装成函数,后面每一步改动都只动一个函数,不会牵一发动全身。
2.2 为什么是 YOLOv8:传统方法也能检测,但鲁棒性差一截
车牌检测传统做法是灰度化、边缘检测、形态学闭运算、找外接矩形,再用宽高比过滤。这套方法在固定机位、固定光线的停车场里能跑,但车灯一开、雨天反光、车牌倾斜,边缘检测就碎成一片。YOLOv8 是目标检测模型,它学习的是“车牌在画面里长什么样”的整体特征,灯影、泥点、反光对它的干扰远小于对边缘算法的干扰。
选型号时看你的机器。YOLOv8n 只有 3MB 左右,CPU 上跑一帧能做到几十毫秒;YOLOv8s 精度更高,但 GTX 1660 Ti 这种显卡跑起来大概 20 到 30 帧,CPU 就吃力了。我的建议是:先拿 YOLOv8n 把流程跑通,把识别率瓶颈找出来,再决定要不要换大模型。这一步很关键,因为车牌识别的最终准确率往往卡在 OCR 而不是检测,换大模型可能白花钱。
值得一提的是,YOLOv8 的 Python 接口非常省事:model(frame)直接返回检测结果,坐标、置信度、类别都在里面。对不熟深度学习的读者来说,它像是一个高级工具函数,你不用关心网络结构也能用。但要理解它的输出格式:结果是 xyxy 归一化坐标、置信度、类别三个数组,后面裁剪和过滤全靠这三个数组。
2.3 Tesseract-OCR 的边界:它不是为车牌而生,但用对姿势就够
Tesseract-OCR 原本是给扫描文档设计的,擅长白底黑字、横向排版的印刷体。车牌长什么样?蓝底白字、黄底黑字、绿色渐变底黑字,还有汉字和字母数字混排。直接拿原图喂给 Tesseract,十个里面能认错八个。所以这个项目里 OCR 不是主角,预处理才是。
你需要接受一个事实:Tesseract 对“图”很挑剔。它喜欢高对比度、字符清晰、笔画粗细均匀、没有倾斜的图。而车牌恰好是一个强约束场景——字符集有限、位置固定、字体统一。这就给了我们两条路:一是把图预处理到 Tesseract 的舒适区,二是用白名单把可识别字符锁死。两条路都走,识别率能到 90% 以上。
有人问为什么不直接用 PaddleOCR。PaddleOCR 的中文识别能力确实强,但模型体积大、依赖多、推理速度慢,在 RK3588、树莓派这类边缘设备上部署成本高。Tesseract 是 C++ 写的,pip 一行装上,离线可用,配合白名单在车牌这种固定场景里够用。如果哪天你发现 Tesseract 怎么调都压不住错误率,再考虑换 PaddleOCR 也不迟。
2.4 海康摄像头:能抓 RTSP 流就别碰 SDK
海康威视摄像头买回来,最常见的接入方式有两种:SDK 和 RTSP。SDK 功能最全,能发现设备、调云台、抓回放,但要在 Windows/Linux 上装它的库,还要处理设备序列号、端口映射、回调函数一堆东西,跨平台时尤其痛苦。而且 SDK 一旦升级,接口变动就能让你重写一遍代码。
RTSP 是标准协议,OpenCV 的VideoCapture直接吃 RTSP 地址,等于把摄像头当成一个网络视频源。海康摄像头的 RTSP 地址会直接相中主码流和子码流,这是后面调试必用的。主码流分辨率高、码率大,适合识别;子码流分辨率低、帧率高,适合实时预览和快速检测。实战里我通常并行拉两路:子码流做持续检测,检测到车牌后切到主码流那一帧做识别。这个方案能省不少 CPU。
海康网页后台能直接看到主码流和子码流的配置,改分辨率、帧率、码率都在“配置 > 音视频 > 视频编码”里。别小看这一节,很多新手卡在第一行取流代码,就是因为不知道 RTSP 地址里要带上摄像头的登录用户名和密码,也不知道地址末尾的 101/102 代表哪条流。
3. 把环境一次装顺:Ubuntu 20.04 上的 YOLOv8、Tesseract、海康 RTSP 取流
3.1 Ubuntu 20.04 搭建 YOLOv8 环境:CPU 版和 GPU 版两条命令路径
先装 Python 虚拟环境,再装 PyTorch,最后装 ultralytics。网上很多教程把三步混成一条命令,出了问题根本不知道是哪一步的锅。我习惯分开执行。
# 创建虚拟环境,Python 3.8 以上都行 python3 -m venv venv source venv/bin/activate # CPU 版 PyTorch pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # GPU 版的话先装驱动,再装对应 CUDA 的 PyTorch # 这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 装上 YOLOv8 本体 pip install ultralytics opencv-python注意,CPU 版和 GPU 版二选一,不要两个都装。装完验证一下:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"这段输出的后半段,CPU 机器是 False,NVIDIA 显卡机器是 True。如果装的是 CPU 版却想跑 GPU,torch.cuda.is_available()会一直是 False,后面的训练速度会慢到怀疑人生。我见过有人在 GTX 1660 Ti 上跑了几小时训练,最后发现 torch 是 CPU 版,这种血泪教训尽量避免。
新机器上我还会顺手装numpy和pandas,后面处理数据集和统计识别率要用。注意,ultralytics 会自动依赖一部分 numpy,但版本可能冲突,装完先跑一次import ultralytics,报错再按提示调版本。
3.2 安装 Tesseract-OCR:别漏掉中文语言包
Tesseract 在 Ubuntu 上直接 apt 安装,包名别写错。很多教程只让你装tesseract-ocr,结果识别中文车牌时全变成乱码,因为没有装中文语言包。
sudo apt update sudo apt install -y tesseract-ocr tesseract-ocr-chi-sim tesseract-ocr-eng pip install pytesseracttesseract-ocr-chi-sim是简体中文语言包,tesseract-ocr-eng是英文。车牌里有省份汉字,比如“京”“沪”“粤”,这些字属于中文语言包;字母和数字属于英文语言包。只装英文包,汉字全部读不出来;只装中文包,字母数字又容易误判。
装完跑一下版本检查和语言列表:
tesseract --list-langs输出里应该同时出现eng和chi_sim。如果只有eng,说明中文包没装上,重新检查包名。pytesseract 是 Python 调用 Tesseract 的桥,它不包含 Tesseract 本体,所以两个都要装。
3.3 海康摄像头设置 RTSP 地址:先用手里的播放器验证两条码流
海康网络摄像头的 RTSP 地址有固定格式,不同固件略有差异,最通用的是这一种:
rtsp://用户名:密码@摄像头IP:554/Streaming/Channels/101其中 101 是主码流,102 是子码流。部分新固件还支持 ISAPI 路径:
rtsp://用户名:密码@摄像头IP:554/ISAPI/Streaming/Channels/101我不建议直接跳进代码,先在终端用 ffprobe 验证地址通不通:
ffprobe -rtsp_transport tcp -v error -show_entries format=duration -i "rtsp://admin:你的密码@192.168.1.64:554/Streaming/Channels/101"能输出 duration 就说明链路通。-rtsp_transport tcp这一步特别重要,默认 UDP 传输在跨网段、弱网环境会花屏、断流,TCP 传输稳定得多。你把这个参数记下来,后面 OpenCV 里也要对应设置。
我用 ffprobe 验证有两层目的:一是确认账号密码、IP 没写错,二是确认 101 主码流和 102 子码流分别指向哪路画面。有些摄像头 101 是 1080P,102 是 D1 分辨率,分辨率设置不对会直接影响检测效果。
3.4 第一次跑通 YOLOv8:用一张抓拍图验证检测坐标
环境装完,先别接摄像头,用一张现成的车辆图片验证模型能跑。Ultralytics 提供了预训练权重,代码里直接YOLO("yolov8n.pt")会自动下载(首次自动下载需要网络,后续离线可用)。但预训练模型的类别里有 80 种物体,包括“car”和“truck”,它并不专门检测车牌。所以这一步只是验证环境,真正检测车牌要用你自己训练或项目源码里自带的权重。
from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model("car.jpg", conf=0.5, save=True) boxes = results[0].boxes for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls = int(box.cls[0]) conf = float(box.conf[0]) print(x1, y1, x2, y2, cls, conf)这段代码做两件事:跑一次推理并保存标注图,打印所有检测框的坐标、类别索引和置信度。xyxy是原图像素坐标,可以直接用来裁剪;cls对照 COCO 类别名,2 是 car,7 是 truck。如果你发现打印出来的cls里有车辆但没车牌框,说明预训练模型不产出车牌类别,这正是后面要训练自定义检测模型的原因。这一步跑通,环境就没什么大问题了。
4. 核心代码实战:海康拉帧、YOLOv8 检测、车牌预处理到 Tesseract 识别的完整管线
4.1 海康 RTSP 拉帧:写一个不会卡死的取流线程
cv2.VideoCapture(rtsp_url)直接在主循环里read(),最简单的写法也是最容易翻车的写法:网络一跳帧,read()就阻塞,程序像死了一样。我一般用生产者-消费者模式,一个线程持续拉帧放进队列,主线程从队列取最新的帧。
import cv2 import threading import queue import time class RTSPCapture: def __init__(self, url, buffer_size=2): self.url = url self.cap = None self.q = queue.Queue(maxsize=buffer_size) self.running = False self.thread = None def _open(self): # 二次尝试打开 RTSP,用 TCP 传输减少花屏和断流 self.cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG) self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 2) def _reader(self): while self.running: if self.cap is None: self._open() ok, frame = self.cap.read() if not ok: self.cap.release() self.cap = None time.sleep(1) continue # 队列满就把最旧的一帧丢掉,保证读到的尽量是新帧 if self.q.full(): try: self.q.get_nowait() except queue.Empty: pass self.q.put(frame) def start(self): self.running = True self.thread = threading.Thread(target=self._reader, daemon=True) self.thread.start() def get_frame(self): try: return self.q.get_nowait() except queue.Empty: return None这段代码把底层read()阻塞的问题隔离在线程里,主程序随时能拿到最新一帧。buffer_size=2是因为实时识别不需要缓存一堆历史帧,反而会越积越卡;丢旧帧的逻辑保证了延迟可控。CAP_FFMPEG让 OpenCV 走 FFmpeg 后端,对 RTSP 兼容性比默认后端好。
注意,海康摄像头主动断开、网络抖动、密码错误都会让read()返回False。上面的_reader里做了释放重连,但如果你在独立脚本里测试,可以先把断流重连去掉,等确认链路稳定后再加回,这样排错更容易。
4.2 YOLOv8 检测车牌并把目标裁出来
拿到帧之后,把它喂给 YOLO 模型。如果你的项目源码里有训练好的车牌权重,直接加载那个 best.pt;如果还没有,先用自己的数据训练一个,或者用开源的预训练车牌模型。下面的代码假设你的模型只有一个类别,就是“plate”。
from ultralytics import YOLO plate_model = YOLO("best.pt") def detect_plate(frame): results = plate_model(frame, conf=0.5, iou=0.45, verbose=False) boxes = results[0].boxes if boxes is None or len(boxes) == 0: return None # 取置信度最高的那个框 idx = int(boxes.conf.argmax()) x1, y1, x2, y2 = boxes.xyxy[idx].tolist() conf = float(boxes.conf[idx]) x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) # 防止坐标越界 h, w = frame.shape[:2] x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) if x2 - x1 < 10 or y2 - y1 < 10: return None crop = frame[y1:y2, x1:x2] return crop, (x1, y1, x2, y2), confconf=0.5的意思是置信度低于 0.5 的框直接丢弃。调这个参数要理解背后的取舍:调高会减少误检,但也会放过一些被遮挡、模糊的车牌;调低会召回更多车牌,但会把车尾、反光板当成车牌。我习惯在白天先跑 0.5,晚上降到 0.35,后面避坑章节细说。
裁出来的crop就是后面预处理的输入。有个细节容易被忽略:boxes.xyxy是浮点数,一定要先int()再切片,直接拿浮点坐标切片 OpenCV 会报错或者裁出奇怪的结果。这个坑我踩过不止一次。
4.3 车牌预处理:灰度、反色、放大和形态学,一步都不能少
这是整个项目里最像“玄学”的部分。有人拿原图直接 OCR 识别率低,做完预处理就上去了,中间的每个步骤都有明确目的,但组合起来的效果却很难一眼看穿。我把常用的一套流程拆成代码:
import cv2 import numpy as np def preprocess_plate(crop): # 统一放大到宽度 440,高度按原比例 h, w = crop.shape[:2] target_w = 440 target_h = int(h * target_w / w) crop = cv2.resize(crop, (target_w, target_h), interpolation=cv2.INTER_CUBIC) # 转 HSV,提取蓝色区域作为车牌底 hsv = cv2.cvtColor(crop, cv2.COLOR_BGR2HSV) blue_mask = cv2.inRange(hsv, (100, 100, 50), (130, 255, 255)) # 形态学:闭运算把蓝色底上的白色字符裂纹填起来 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) blue_mask = cv2.morphologyEx(blue_mask, cv2.MORPH_CLOSE, kernel) # 蓝色区域为白底,字符自然变成黑字 return blue_mask这一步的思路是:车牌本身是蓝底白字,如果用 HSV 把蓝色提取出来,得到的二值图里蓝色底变成白色,白色字符变成黑色,正好是 Tesseract 最喜欢的“白底黑字”。放大到宽度 440 是因为 Tesseract 在小图上识别率极差,字符至少要占几十个像素才算清晰。
HSV 阈值(100, 100, 50)到(130, 255, 255)是针对蓝牌的。如果你是黄牌(教练车、大型车),阈值要换成黄色范围;新能源绿牌要换成绿色范围。方法不变,改范围就行。我见过有人把彩色车牌直接灰度化后反色,出来的图噪声大得没法看,问题就出在没有做颜色约束,背景里的蓝天、蓝车全被当成车牌底色了。
4.4 Tesseract 识别:PSM、白名单和语言包的组合
预处理之后,调用 Tesseract 本身只有一行,但参数配置决定成败:
import pytesseract def ocr_plate(binary_img): config = ( "--psm 7 " "-c tessedit_char_whitelist=" "京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领" "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) text = pytesseract.image_to_string(binary_img, lang="eng+chi_sim", config=config) return text.strip()--psm 7告诉 Tesseract:这一整张图就是一行文字,不用费劲去做版面分析。这是车牌识别里最重要的一个参数,默认的 PSM 3 会把一行车牌当成多块文本,输出乱序或加空格。
tessedit_char_whitelist是字符白名单。车牌上只会出现这些内容:31 个省份汉字、24 个大写字母(I 和 O 在车牌里不用,但白名单里留着字母序号问题不大)、10 个数字。锁死白名单后,Tesseract 不会输出任何不在名单里的字符,错别字概率大幅下降。
lang="eng+chi_sim"同时启用英文和中文语言包。注意,如果白名单里有汉字但 lang 里没加chi_sim,汉字就会全部识别失败。运行时会报警告说某些语言数据缺失,那就是语言包装错了。
白名单这一招还有副作用:如果图像里混入噪声,Tesseract 也会强行从白名单里选字符,所以不是白名单越长越好。实际工程里我会针对省份做进一步约束,比如项目只在北京用,白名单汉字就只剩“京”,识别率能再提一截。
4.5 串起来跑:一条主循环代码
把上面几个函数拼起来,就是一个最小的可运行程序:
while True: frame = capture.get_frame() if frame is None: continue result = detect_plate(frame) if result is None: continue crop, box, conf = result binary = preprocess_plate(crop) plate_text = ocr_plate(binary) if plate_text: print(plate_text, conf) if cv2.waitKey(1) & 0xFF == ord("q"): break主循环逻辑不复杂:拿帧、检测、裁剪、预处理、识别、打印。实际项目里会在print的位置接上报过滤逻辑,比如同一辆车连续 5 帧识别结果一致才输出,防止闪烁造成重复记录。
这里有一个性能取舍:检测和 OCR 都是阻塞操作,加起来大约几十毫秒到几百毫秒。对道闸场景来说,车辆是低速通过的,每秒处理 5 帧足够;对高速卡口来说,这个方案就不合适了,要换硬件加速和更高帧率的抓拍。项目源码里一般会在这一层加“检测到车牌才做 OCR”的开关,避免每帧都跑 OCR 浪费 CPU。这个开关看着不起眼,却能让整个程序的 CPU 占用掉一半以上。
5. 车牌识别避坑指南:夜晚不灵敏、分辨率保存失败、断流和 OCR 乱码
5.1 海康威视摄像头夜晚不灵敏:车灯过曝、反光让 OCR 全乱
现象:白天识别正常,到了晚上漏检变多,检测到了车牌也读错,0 和 8 乱跳。车辆开近时车灯把车牌照成一片白。
原因:监控摄像头默认的自动曝光和自动增益会为了照顾整体画面亮度,把车灯区域压暗或者把车牌区域过曝。车牌本身是反光材质,近光灯一照,字符和底色的对比度瞬间被抹平。这属于前端成像问题,靠后端算法很难完全救回来。
解决:分三路走。第一路是摄像头端,在海康网页后台开启宽动态(WDR),并把曝光模式改成区域曝光,优先保证画面中间偏下区域的亮度;第二路是在拉帧后加一个 CLAHE 对比度增强,让字符边缘重新立起来;第三路是检测参数上把conf从 0.5 降到 0.35,因为夜晚车牌画面模糊,模型输出的置信度天然偏低,阈值太高等于直接放弃夜间识别。
# CLAHE 在预处理前的增强,只针对夜间的低对比度帧 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) enhanced = clahe.apply(gray)我一般只对夜间帧做增强,白天做了反而会放大噪点。判断白天黑夜可以用平均亮度简单阈值,也可以用摄像头时间戳,哪种顺手用哪种。
5.2 海康摄像头分辨率设置保存不了:主码流和子码流在打架
现象:在网页后台把分辨率从 1080P 改成 4MP,点“保存”提示成功,刷新后还是 1080P;或者在主码流里改了 4MP,子码流分辨率跟着乱跳。
原因:海康摄像头的编码模板里,主码流和子码流是两套独立配置,但某些型号对“主码流分辨率不能低于子码流”有限制。如果你先把子码流设置成 1080P,再想给主码流设 4MP 是没问题的;反过来主码流只想设 720P,但子码流已经被设成 1080P,保存就会失败,因为你主码流比子码流还低。
解决:按这个顺序操作:先在“配置 > 音视频 > 视频编码”里把子码流分辨率调到最低(比如 CIF),保存;再改主码流到目标分辨率,保存;最后把子码流改回来。改完后在本地用 ffprobe 拉一下 RTSP,看Stream: Video: ... 1920x1080这类输出,确认实际生效的是不是你设的分辨率。
这个坑特别隐蔽,因为网页端报错并不明显,只是悄悄“保存失败”。项目接入时我习惯先记录一版主码流和子码流的分辨率基线,后续排查画面模糊、识别率下降的问题,直接对比 RTSP 实际输出和期望配置,能省掉半天调试时间。
5.3 RTSP 断流卡死:换 TCP 传输并加重连
现象:程序刚启动能识别,跑半小时或一晚上后画面卡住,日志停在同一帧,CPU 占用却很高。重启程序又能恢复。
原因:摄像头默认 UDP 传输 RTSP,弱网下丢包严重,OpenCV 的read()在等待下一帧时会一直阻塞,线程里没有超时机制,整个取流线程就挂住了。还有些情况是摄像头做了空闲连接自动断开,比如没有客户端拉流 30 秒后主动关闭编码器。
解决:双管齐下。第一,所有取流的地方显式指定 TCP:cv2.VideoCapture(url, cv2.CAP_FFMPEG)之后用cap.set(cv2.CAP_PROP_RTSP_TRANSPORT, cv2.CAP_PROP_RTSP_TRANSPORT_TCP)(或者在上面的自定义类里改成 FFmpeg 选项)。第二,read()返回False时释放当前 cap 并重新建立连接,重连之间 sleep 1 秒,防止高频重试把摄像头打挂。
重连还有一个容易被忽视的细节:摄像头 IP 由 DHCP 分配时,断电后 IP 可能变掉,程序重连还是老地址。项目要稳定运行,摄像头 IP 务必在设备端固定,并且在代码里把 IP 做成配置文件而不是写死。
5.4 Tesseract 把 0 读成 O、1 读成 I:白名单之外还有三个细节
现象:识别结果里数字和字母混淆,比如京A0B123读成京AOB123,1读成I。白名单已经限制了字符集,但混读仍然存在。
原因:白名单只是把输出限制在集合内,Tesseract 对这个集合里的相似字形仍然会犹豫。0 和 O 在车牌字体里确实像,1 和 I 在无衬线字体里也接近。另一个常见原因是没有放大图像,字符太小,细节特征丢失。
解决:三个细节叠加。一是预处理时把车牌图宽度放大到 440 以上,字符高度至少 40 像素;二是对二值图再做一次“去孤立噪点”,用 OpenCV 的连通域分析,把面积小于 20 像素的连通域涂掉,防止噪点在白名单里“猜”出一个字符;三是后处理环节做规则修正,比如车牌第二位是字母的常识(中国大陆车牌第 2 位是省份字母,实际是英文字母),如果识别结果第 2 位是数字 0 或 1,就替换成 O 或 I。这类规则看着粗暴,但在固定场景里非常有效。
def fix_plate_rule(text): if len(text) < 2: return text text = list(text) if text[1] == "0": text[1] = "O" elif text[1] == "1": text[1] = "I" return "".join(text)注意,规则只在你确认业务场景时才能加。全国通用、自由通行场景下,第二位也有可能是数字?不对,按现行大陆车牌标准,第二位确实是字母,但这个规则最好做成可配置项,别写死在核心代码里。
5.5 模型不认黄牌和新能源车牌:自己标数据、转格式、重新训练
现象:预训练模型或者项目自带的权重在蓝牌上表现很好,遇到黄牌(教练车、大型车)、绿牌(新能源)直接漏检。车头是全的,车牌就在正中间,模型就是没框出来。
原因:检测数据里蓝牌占绝对多数,模型学到的“车牌”特征实际上偏向蓝色车牌。黄色车牌和绿色车牌的纹理、颜色、对比度分布完全不同,模型把它们归为背景了。这是典型的数据集分布问题,不是模型结构问题。
解决:收集你自己的场景数据,用 Labelme 标注,再转成 YOLO 格式重新训练。Labelme 标出来的是多边形或多矩形,YOLO 需要的是归一化的中心坐标和宽高,两者格式不一样,要写个转换脚本。
# 数据集目录结构,YOLO 训练默认按这个组织 datasets/plate/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml# data.yaml path: datasets/plate train: images/train val: images/val names: 0: plate训练命令用 ultralytics 一条跑完:
yolo detect train model=yolov8n.pt data=datasets/plate/data.yaml epochs=100 imgsz=640 batch=16这里提醒几个训练参数的含义:epochs=100决定训练轮数,数据量大可以减到 80,数据少反而要加正则;imgsz=640是输入尺寸,车牌目标不算小,640 够用,不需要 1280,否则训练和推理都会明显变慢;batch=16在 1660 Ti 这类 6GB 显存卡上偏大,爆显存就减到 8。
训练完成后看runs/detect/train/results.png,重点看val_box_mAP50这条曲线,mAP50 到 0.95 以上基本就够用。标数据建议只标车牌本体,不要标进“车头”区域,否则模型学到的边界是错的,裁剪出来的图会带一圈无关背景,直接影响 OCR。我在第一次标数据时就犯过这个错,裁剪图带了半块保险杠,Tesseract 把保险杠上的纹理也当成字符了。
6. 让它从“能跑”变成“能用”:准确率统计与三个必调参数
6.1 先算字符级准确率:100 张图里有多少字符读对了
很多项目验收只看“检测到车牌没有”,但车牌识别真正要考核的是“字符对不对”。我习惯准备 100 张不同光线、不同角度的实拍图,手工标注正确车牌,再跑一遍识别,用 Levenshtein 距离算字符级准确率。
from rapidfuzz import distance def char_accuracy(gt, pred): if len(gt) == 0: return 1.0 if len(pred) == 0 else 0.0 return 1 - distance.Levenshtein.distance(gt, pred) / len(gt)Levenshtein 距离把“插入、删除、替换”都算一次编辑,1 - 编辑距离/真实长度就是字符正确率。一辆真实车牌京A12345被识别成京A1234,长度 7 编辑距离 1,准确率约 0.857。这个指标比“整串完全正确率”更敏感,能告诉你错在哪一位。如果你统计结果是第 1 位汉字错得多,说明 charset 训练不足或语言包缺失;第 2 位字母错得多,重点查 0/O、1/I 混读;末位数字错得多,通常是图像模糊或闪烁。
6.2 上线前只动这三个参数:conf、psm、预处理尺寸
这三个参数是整个项目里投入产出比最高的调优点。conf管漏检和误检的平衡,白天 0.5、夜间 0.35,按时间段切换比固定值强;psm固定用 7,不要尝试 3 或 6,多行排版逻辑在车牌场景只会帮倒忙;预处理放大宽度 440 是一个甜点位,太小字符糊,太大 Tesseract 处理变慢且会引入边缘锯齿。
| 参数 | 推荐值 | 调高/调低的效果 |
|---|---|---|
| conf | 0.35~0.5 | 调低召回多、误检多;调高干净但漏检 |
| --psm | 7 | 固定单行文本,禁止自动版面分析 |
| 放大宽度 | 440 | 过小识别率骤降,过大耗时上涨 |
我自己第一次把这套方案推上线时,最深刻的教训就是“先调参、别重构”。当时想让识别率更高,花了两周改预处理算法,最后发现最明显的提升来自把放大宽度从 320 提到 440,顺便把夜间 conf 调到 0.35。从那以后,接手任何识别项目,我都先跑参数实验,再考虑换算法。这三板斧如果你也想省点时间,建议直接按表格里的起点试,然后围绕各自的 20% 范围再微调。希望帮到你。
本文还有配套的精品资源,点击获取