尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

YOLOv8+OCR+海康摄像头车牌识别实战:从环境搭建到踩坑排查

YOLOv8+OCR+海康摄像头车牌识别实战:从环境搭建到踩坑排查 简介面向智能交通与安防监控领域的开发者这份车牌识别实战项目将YOLOv8目标检测车牌定位、Tesseract OCR字符识别与海康摄像头视频流采集三者串联覆盖从模型训练、数据集标注到实时推理的完整链路。压缩包共61个文件以Python训练与测试脚本、Rust客户端源码、YOLOv8多版本权重文件、标注数据集及演示视频为主同时含环境配置、启动脚本与说明文档整体约831.95MB目录结构清晰便于按模块检索学习。目前已有424人学习下载。通过研读源码与配套图文说明可掌握YOLOv8在车牌场景下的训练调优方法、Tesseract OCR识别集成技巧以及海康摄像头RTSP流接入与前后端联调排错思路结合示例截图与演示视频还能直观比对不同光照、倾斜角度下车牌的识别效果适合图像识别、智能交通等方向的项目参考与二次开发。1. 一套车牌识别项目为什么卡在取流和识别两端停车场入口的栏杆机旁边通常挂着一台海康摄像头和一块补光板。这个基于 YOLOv8 Tesseract-OCR 海康摄像头的车牌识别项目要做的就是把摄像头画面变成一行车牌字符串先让 YOLOv8 在视频帧里框出车牌区域再把车牌图块交给 Tesseract-OCR 读出字符。这套组合常见于园区出入口、商超车库和不少毕业设计里它不需要专用一体机设备一台普通 PC 加现成的网络摄像头就能跑。网上这类项目源码包里模型文件通常是现成的难的是把摄像头参数、环境依赖、数据路径都对上。真正值得投入精力的地方不是“模型有多新”而是取流、检测、OCR 三段链路各自有一堆参数和边界要处理任何一个环节没接好最终结果都会翻车。2. 先搭环境再谈识别海康 RTSP 取流与 YOLOv8、Tesseract 的最小组合这一章的目标很直接把取流、检测、OCR 三个环节的最小可运行环境各自跑通。先拿海康摄像头的视频流开刀因为拿不到画面后面训练和识别都无从谈起。常见做法是直接拉海康网络摄像头的 RTSP 地址海康设备的 RTSP 地址格式有固定的套路。2.1 海康摄像头 RTSP 地址先让视频流跑起来再谈识别海康设备的 RTSP 地址格式基本如下rtsp://admin:password192.168.1.64:554/Streaming/Channels/101说明admin 是设备 Web 登录用户名password 对应密码IP 是摄像头在局域网里的地址554 是默认 RTSP 端口。最后一段“101”是通道编号第一位 1 表示通道 1后两位 01 表示主码流、02 表示子码流。主码流分辨率高适合识别子码流分辨率低适合预览。车牌识别尽量用主码流CPU 扛不住时再考虑子码流这个取舍在第5章会细说。拿到地址后用 ffprobe 验证能不能通ffprobe -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101 -show_format -hide_bannerffprobe 是 FFmpeg 套件里的探针工具Ubuntu 上安装 ffmpeg 后自带。参数 -rtsp_transport tcp 强制走 TCP 传输比 UDP 丢包少-show_format 会打印流格式信息能看到 duration 和 format_name 就说明取流成功。这一步的目的是把摄像头、网线、账号密码的问题在进入代码前先排掉省得后面查问题时分不清是摄像头还是代码的问题。提示如果密码中包含 、:、/ 等字符这些符号会被解析成 URL 分隔符建议在设备上把密码改成纯字母数字绕开转义问题。2.2 Ubuntu 20.04 搭建 CPU 版 YOLOv8 环境从预训练权重到跑通第一张图YOLOv8 是目前做车牌检测比较省事的方案训练和推理接口都统一在 Ultralytics 包里。手头没有独立显卡时也能跑只是速度慢但环境搭建和调参流程完全一样。常见做法是用 conda 新建虚拟环境避免和系统 Python 打架conda create -n plate python3.10 -y conda activate plate pip install ultralyticsCPU 环境不需要额外装 CUDA直接 pip 即可。如果不想下载带 CUDA 的 torch可以先按 PyTorch 官网的 CPU 索引装 torch再装 ultralytics体积小很多。装完用仓库自带的预训练权重跑一张图验证环境yolov8n.pt 是最小的权重文件第一次运行会自动下载yolo detect predict modelyolov8n.pt sourcetest_plate.jpg如果 test_plate.jpg 里有车牌命令行会打印检测结果并输出一张画了框的图。这一步的意义是确认 ultralytics 包、权重下载、推理链路都正常。输出目录默认是 runs/detect/predict打开里面的标注图能框出车牌就算环境通。很多检索资料会强调“yolov8 环境配置”要装一堆依赖其实 ultralytics 已经把这些依赖声明在包里了。真正要看的是模型尺寸的选择YOLOv8 有 n/s/m/l/x 五个档位CPU 上推理建议先用 nGTX 1660 Ti 这类入门卡可以用 s 甚至 m。它的网络结构图里 C2f 是主干的基本块PAN-FPN 负责多尺度融合这些决定了远处小车牌能不能被召回了解结构图有助于后面决定要不要换输入分辨率。2.3 Tesseract-OCR 安装与语言包只装包不装语言包是最常见的坑Tesseract 是开源 OCR 引擎车牌识别里负责把 YOLOv8 框出来的图像块转成文本。装它很简单但很多人只装了引擎没装中文语言包到识别中国车牌时就只能吐乱码。Ubuntu 20.04 上直接 apt 安装顺便把中文简体语言包装上sudo apt update sudo apt install -y tesseract-ocr tesseract-ocr-chi-sim tesseract --version tesseract --list-langs--version 确认引擎版本--list-langs 应该能看到 eng 和 chi_sim。中国车牌里省份简称是汉字后面的编号是字母加数字所以 eng 和 chi_sim 两个语言包都要装后面在 Python 里调用时指定 langchi_simeng 就可以同时处理两类字符。这一步走通后摄像头画面能进来、检测模型能跑、OCR 引擎能调用第2章的目标就算完成。3. 数据标注与训练用 labelme 处理车牌数据集并跑通 YOLOv8 训练检测模型的准确率不是靠“换个更强的主干”提上去的而是靠数据。车牌目标小、长宽比固定、字符密集标注习惯和训练参数直接决定最终框得准不准。这一章按数据标注、格式转换、训练参数三步走先把手里的原始图片处理成 YOLOv8 能直接消费的数据集。3.1 用 labelme 标注车牌数据多边形标注与 YOLO 格式转换脚本车牌检测需要标注的是“车牌框”而不是“车框”。一张图里可能同时有车脸、进气格栅、车灯但 YOLOv8 要学的只是车牌这一个类别。所以 labelme 里统一建一个名为 plate 的标签用矩形或多边形把车牌完整框住。labelme 安装只需一行pip install labelme labelme启动后打开图片目录用“Create Rectangle”或“Create Polygon”描车牌区域保存后每个图像会生成同名 .json 文件。矩形比较简单多边形能贴合倾斜车牌但 YOLOv8 训练时标签都是水平框多边形最终还是会转成外接矩形。车牌在画面里一般不会严重旋转直接使用矩形效率更高。labelme 生成的 json 里坐标是图像像素坐标YOLO 训练格式要求的是归一化后的 x_center, y_center, width, height需要写个脚本转换import os import json def labelme_to_yolo(json_path, out_dir, class_id0): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] yolo_lines [] for shape in data[shapes]: if shape[label] ! plate: continue xs [p[0] for p in shape[points]] ys [p[1] for p in shape[points]] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h w (x_max - x_min) / img_w h (y_max - y_min) / img_h yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_txt os.path.join(out_dir, os.path.basename(json_path).replace(.json, .txt)) with open(out_txt, w, encodingutf-8) as f: f.write(\n.join(yolo_lines))脚本逻辑是读取 json 里每个 shape 的 points取所有点的横纵坐标极值得到外接框再除以图像宽高做归一化。class_id 填 0对应 data.yaml 里的第一个类别。注意 labelme 保存的 points 可能是小数坐标但宽高一定和原图一致转换时不要用错字段。转换完找个样本用文本编辑器打开 .txt检查坐标值是否都小于 1、大于 0会有一些边界像素误差正常。3.2 处理数据集用于 YOLOv8 训练目录结构、data.yaml 与 train/val 划分要把数据集处理成 YOLOv8 能直接训练的结构必须按官方约定的目录摆放否则 train 时会直接报找不到标签datasets/plate/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 └── labels/ ├── train/ # 与训练图片同名的 .txt └── val/ # 与验证图片同名的 .txt图片和标签文件名必须一一对应。划分数据集时常见做法是随机按 8:2 拆分注意别把同一场景的连续帧全留在 train 里否则验证集里全是“见过的角度”mAP 虚高。data.yaml 内容如下path: /home/user/datasets/plate train: images/train val: images/val names: 0: platepath 写绝对路径最稳。train 和 val 是相对 path 的目录名不要在前面加斜杠写错。names 只列一个类这就是一个二分类检测任务。划分脚本可以直接用 sklearn 的 train_test_split但要注意同步移动图片和同名标签import os import random import shutil image_dir raw_images train_dir datasets/plate/images/train val_dir datasets/plate/images/val images os.listdir(image_dir) random.shuffle(images) split int(len(images) * 0.8) for name in images[:split]: shutil.copy(os.path.join(image_dir, name), train_dir) label os.path.splitext(name)[0] .txt if os.path.exists(os.path.join(raw_labels, label)): shutil.copy(os.path.join(raw_labels, label), datasets/plate/labels/train)split 的索引决定了 80% 进 train剩下 20% 进 val。跑完数一遍两个目录里的文件数量确认图片和标签数量一致漏了一个文件后面训练时排查很费时间。3.3 YOLOv8 训练参数里真正要调的五个值epochs、imgsz、batch、lr0、patience训练命令写在下面参数含义单独解释这是“yolov8 模型训练参数含义”搜索里最常被问的部分yolo detect train \ dataplate.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch8 \ lr00.01 \ patience20参数含义车牌场景建议epochs训练轮数100小数据集轮数太少欠拟合imgsz输入图像尺寸640车牌是小目标可用 768 提高召回batch每批样本数按显存调CPU 训练建议 8 以下lr0初始学习率0.01后续会自动余弦退火patience早停轮数20防止过拟合后还在白跑yolov8s.pt 是 COCO 上预训练过的 s 版权重用它初始化会比从零训练收敛快很多。没有 GPU 时 CPU 训练几百张图可能要按小时算epochs 可以先给 50 跑通流程再看 results.csv 里的验证 mAP 决定要不要加。训练结束后runs/detect/train 目录里会生成 weights/best.pt 和 loss 曲线图。想自己画损失函数曲线的话直接读 results.csv 里的 train/box_loss、val/box_loss 两列画折线图就行val loss 不再下降就该停patience 就是自动化这个判断。把 model 换成 yolov8n.pt 会更省资源但框的定位精度通常会低一点对车牌这种高度结构化的目标s 版是性价比比较高的选择。4. 检测框之后车牌图像预处理与 Tesseract-OCR 识别参数YOLOv8 输出的是“画面里哪个位置有车牌”OCR 要解决的是“这块图像上写的什么字”。直接拿原始裁块去识别效果通常很差因为摄像头画面里的车牌存在反光、倾斜、暗光、字符粘连。常见做法是先做图像预处理再限制 OCR 的字符范围和版面模式。4.1 从检测框裁图到 OCR 预处理灰度、二值化、放大三件套识别一张车牌图块最常见且有效的一套预处理是转灰度、放大三倍、Otsu 二值化。为什么放大Tesseract 对高度低于 20 像素的小字符识别率很低车牌裁块在 1920x1080 画面里可能只有 200x50 像素放大到 600x150 后字符高度超过 60识别率明显不一样。import cv2 def preprocess_plate(crop_bgr): gray cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY) gray cv2.resize(gray, None, fx3, fy3, interpolationcv2.INTER_CUBIC) th cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)[1] return thcvtColor 把三通道彩图转成灰度resize 用 fx3、fy3 表示宽高各放大三倍INTER_CUBIC 是三次插值对边缘的保持比 INTER_LINEAR 稍好threshold 里的 THRESH_BINARY THRESH_OTSU 让算法自动算阈值避免固定阈值在光照变化时失效。这样处理完的图是黑底白字或白底黑字字符和背景被拉得很开。如果二值化后字符断成碎块识别时经常会少读一位可以在二值化后做一次闭运算kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) th cv2.morphologyEx(th, cv2.MORPH_CLOSE, kernel)MORPH_CLOSE 先膨胀再腐蚀能把被二值化打断的笔画重新连上。车牌字符间距固定、笔画相对简单这个操作不太会把两个不同字符粘在一起。4.2 Tesseract 字符白名单与 PSM 模式把识别范围锁死在车牌字符集Tesseract 默认识别“任何语言里的任何字符”这在车牌场景是灾难。中文车牌实际只包含三类内容省份简称汉字、大写英文字母、数字且为了易读性普通车牌编号里不出现字母 I 和 O新能源车牌也遵循这个规则。把这些约束写进白名单就能把“0 看成 O、1 看成 I”的幻觉压到最低。import pytesseract from pytesseract import Output config ( r--psm 7 r-c tessedit_char_whitelist0123456789ABCDEFGHJKLMNPQRSTUVWXYZ r京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 ) result pytesseract.image_to_string( th_img, langchi_simeng, configconfig, output_typeOutput.DICT ) text result[text].strip().replace( , )config 里的 psm 7 表示“按单行文本识别”正好适配车牌一行字符-c tessedit_char_whitelist 把可识别字符限定进白名单。字母表里特意去掉了 I 和 O同时保留汉字省份简称这里的汉字列表覆盖了所有省级简称和“使领”字样。lang 指定 chi_simeng让 Tesseract 同时用中文和英文字符集但实际输出只允许白名单里的字符。如果识别结果里出现多余空格或换行最后的 replace( , ) 会把它清掉。需要更深一层调试时可以用 Output.DICT 拿到每个字符的置信度把低置信度的帧丢掉或进入多帧投票。不同 Tesseract 版本对中文白名单的支持有差异实测不生效时用后置正则过滤兜底别在配置上死磕太久。4.3 识别结果过滤与多帧投票单帧识别容易错连续帧拍板才靠谱单帧识别面对拖影、逆光、遮挡时错误难免常见做法是对连续多帧结果做投票让“出现次数最多的字符串”胜出。设计原理是真实车牌字符在每一帧都一样OCR 随机错误却不会每帧都错在同一个位置。from collections import Counter def vote_plate_texts(result_list): valid [] for text in result_list: text text.strip().replace( , ) # 蓝牌常见为7位汉字字母数字新能源牌为8位需要时单独放开 if len(text) 7 and text[0] in 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领: valid.append(text) if not valid: return return Counter(valid).most_common(1)[0][0]注意普通蓝牌是 7 位新能源车牌是 8 位投票过滤时要同时兼容两种长度否则新能源牌会被误杀。投票之前先过滤格式蓝牌是“汉字字母数字”共 7 位长度不符直接不参与投票汉字首字不在省份简称列表里也直接丢弃。这样能把“识别成功但格式明显错误”的结果提前挡住。连续 3 到 5 帧里出现两次相同字符串基本可以认为就是真实车牌如果一直投不出结果就把这段视频帧存下来留到第6章讲的 badcase 复盘里处理。5. 车牌识别项目必踩的 5 个坑从海康摄像头到模型推理全链路排查不管环境装得多顺项目真正难的是现场运行。这一章集中列我从取流、训练、识别三端碰到过的高频问题每一条都按现象、原因、解决来写可以作为排查清单直接对着看避免在同一个坑里反复折腾。5.1 摄像头分辨率设置保存不了主码流和子码流要分开设现象用浏览器登录海康摄像头 Web 管理页去改分辨率点完保存后页面显示成功再刷新又跳回原值或者换成代码里用 cap.set(cv2.CAP_PROP_FRAME_WIDTH) 设置分辨率cap.read() 读回来的画面尺寸完全没变。这种情况通常伴随主码流和子码流显示的分辨率不一致现场经常误以为设备坏了。原因海康多数机型的主码流和子码流是独立配置两者分辨率组合有限制比如主码流设为 1080P 时子码流不能超过某一档另外老版本浏览器插件兼容问题也会让设置没有真正提交到设备端而不是参数本身不对。解决进“视频/音频”设置里分别配置主码流和子码流主码流用 1920x1080子码流用 704x576 这类低档位代码里不要依赖 cap.set 改分辨率改为按设备端配好的参数直接取流。识别算力吃紧时切换到 102 子码流通道而不是试图在代码里压低分辨率。5.2 RTSP 断流导致 OpenCV 读流卡死加心跳重连而不是崩溃退出现象程序跑十几分钟后 cap.read() 开始一直返回 False或者整个进程卡在 read 调用上不退出按 CtrlC 都没反应只能杀掉进程重启。重启后能恢复一段时间过一会儿又复现。原因摄像头重启、网络抖动、路由器老化都会让 RTSP 会话断开。OpenCV 底层 FFmpeg 在 TCP 模式下重连逻辑比较弱断流后 read 会一直阻塞占住线程后面的检测识别全被堵死。解决给读取循环加超时和重连机制。常见做法是先 grab 再 retrieve对连续失败计数超过阈值就重建 VideoCapture。import cv2 import time def read_frame_with_retry(rtsp_url, timeout_sec5): cap cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) cap.set(cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, timeout_sec * 1000) cap.set(cv2.CAP_PROP_READ_TIMEOUT_MSEC, timeout_sec * 1000) fail_cnt 0 while True: ok, frame cap.read() if ok: fail_cnt 0 yield frame else: fail_cnt 1 cap.release() time.sleep(2) cap cv2.VideoCapture(rtsp_url, cv2.CAP_FFMPEG) if fail_cnt 5: raise RuntimeError(rtsp reconnected too many times)代码用生成器封装读取循环每次失败先 release 再重新打开fail_cnt 超过 5 就主动抛异常让上层重启服务而不是死循环。关键是把 read 放在独立线程或进程里避免主线程被阻死处理不过来时丢帧也比卡死强。5.3 夜间识别率暴跌补光、宽动态与模型侧一起调现象白天单帧识别率 90% 以上太阳落山后开始漏检测检测框有时整个消失识别结果里数字常被读错。有人把问题归结为“海康威视监控摄像头夜晚不灵敏”其实大部分是光照策略不对。原因夜间画面整体偏暗车牌反光板把字符打得过亮形成光晕YOLOv8 在训练数据里没见过这种对比度特征算出的置信度自然下降Tesseract 同样对高光下的二值化结果过敏。解决硬件侧优先开摄像头宽动态WDR把明暗对比拉平补光灯保持常亮而不是只在触发时亮避免亮度突变影响连续帧。模型侧在训练集里加入夜间增强样本用亮度抖动、高斯噪声、运动模糊做数据增强让模型见过黑暗版本的车牌。推荐做法白天数据跑一遍再用同一批图做 HSV 亮度下调生成一份夜间伪样本和真实夜间图混着训。5.4 把“0”识别成“O”、“1”识别成“I”不是玄学是白名单和预处理没到位现象OCR 输出的字符串里经常出现 O、I 这类字母但正规车牌序列里根本不出现结果被后端当成错误车牌丢掉。单独跑单帧的时候偶尔对连续跑就频繁冒出来。原因Tesseract 的默认字典会输出英文字母而“0/O”“1/I”在低分辨率下字形高度接近很多人没意识到车牌字符集本来就不含 I 和 O也没有在调用时做字符集限制。解决把 I 和 O 从白名单里删掉就是从根上堵住。代码见 4.2 节白名单里只保留数字和 H、J、M、N 这类易混但车牌允许的字母。另外检查二值化结果字符太细或断笔时调大放大倍数从 2 倍加到 4 倍必要时加一次闭运算。如果“京”这类汉字被识别成别的字多半是 chi_sim 语言包版本太老或裁块太窄换新版 traineddata 再试。5.5 CPU 推理太慢帧率个位数的三种自救方式现象用 yolov8n 在 CPU 上跑 1080p 主码流检测加 OCR 一帧要半秒多处理速度追不上视频帧率视频播放画面和人看到的不一致。原因CPU 浮点算力有限1920x1080 全尺寸输入对 YOLOv8 的卷积负担过重每次都把所有帧都做 OCR也把有限算力浪费在没有变化的画面上。解决第一取流时切子码流用 640x480 甚至更低分辨率做检测车牌在画面里仍然足够大第二隔帧检测每 3 帧检测一次中间帧直接沿用上一帧的检测框第三把 YOLOv8 导出成 ONNX 后用 OpenCV DNN 或 ONNXRuntime 推理通常比 ultralytics 的 Python 推理路径快一截代价是后处理要自己写。如果想让车牌识别真正追上实时最直接的路是把推理放到板卡 NPU 上RK3588 这类设备上部署 YOLOv8 的流程已经比较成熟属于进阶优化方向而不是在 CPU 上死磕参数。6. 用 badcase 复盘把识别正确率抬上去验证脚本与闭环训练等到检测、识别都跑通接下来不是急着上线而是先验证。我的习惯是拿一段没参与训练的真实视频让程序把每一帧的检测框、OCR 结果、原始裁块全部落盘晚上统一翻一遍误识别样本。一个最小的落盘脚本大概长这样循环读取视频帧YOLOv8 检测到车牌就裁块预处理后交给 Tesseract把 result 和原图一起写到 badcase 目录文件名带时间戳方便回看。跑完一晚上第二天打开目录就能看到问题集中在“夜间反光”“倾斜车牌”“字符粘连”哪一类。这些 badcase 图不能看一眼就删而是回贴进训练集重新标注。把识别错误或漏检的帧用 labelme 重新标框混入原数据集再跑一次第3章的训练命令这是最可靠的正确率提升手段。这样迭代两到三轮后模型对现场光线、角度的适配会明显好转。源码包里如果已经带了训练好的权重和推理脚本我建议先不要急着改网络结构或加注意力机制比如“协调注意力机制”这类改进属于数据已经足够干净、baseline 打满之后才值得考虑的手段。先看 badcase 的分布再决定是加数据、调预处理还是换大模型成本低得多。我现在拿到这类车牌识别项目源码包第一件事就是按第2章的环境清单过一遍再拿自己的视频跑一遍并统计错误集中在哪等你把 badcase 复盘做成一圈闭环这套系统的可信度就真正立住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表