简介:面向计算机视觉与图像处理开发者的车牌识别实战资源,基于OpenCV与百度API构建,覆盖静态图片、网络图片、实时截图与摄像头视频流等多种识别场景,适合需要快速实现车牌检测、定位、对比与检索功能的算法学习者、毕设学生及工程人员。资源包共139个文件,以Python源码、图片样本、模型文件及Docker配置为主,压缩包约73.86MB;其中50张JPG、32张PNG、19张JPEG等多样图片可作为测试集,18个PY脚本覆盖主要逻辑,另有YAML/DAT模型与1个SH脚本便于环境部署,整体目录结构清晰。已有1608人学习使用。内容涵盖车牌搜索识别、车牌对比识别、车牌数据库认证、图文搜索、数据库检索,以及本地文件、网络地址、实时截图、摄像头拍照识别等完整流程,并引入hyperlpr方案提升识别率,支持图片自适应窗口大小,可帮助读者快速搭建一套可扩展、可验证的车牌识别原型系统。
1. 车牌识别不是玄学:这份Python OpenCV工程能直接改着用
车牌识别这类需求,常被等同于要训练深度学习模型,实际上对停车场、园区门禁这类相对固定的场景,用Python和OpenCV配合传统SVM,完全能把识别链路跑通。这份资源的结构很有意思:既有svm.dat和svmchinese.dat两个训练好的模型,也保留了调用百度API的兜底通道,还覆盖了摄像头抓拍、网络图片识别、数据库认证这些贴近真实业务的功能分支。对正在做毕设、写课程设计、或者要给公司Demo快速验证识别效果的人来说,它最大的价值不是识别精度有多夸张,而是把“从图片到车牌字符串”的完整路径都铺好了,拆开改改就能用。
2. 环境与模型文件:先把OpenCV、依赖和Docker跑通
2.1 文件清单:每个文件在工程里承担什么角色
在动手改代码之前,得先搞清楚这份资源里的文件分别干嘛。我从工程角度看,下面这几个文件是整个项目能跑起来的关键:
| 文件 | 角色 |
|---|---|
| svm.dat | 英文和数字字符的SVM分类模型,OpenCV的ml模块加载 |
| svmchinese.dat | 中文字符(省份简称)的SVM分类模型 |
| Dockerfile | 容器化运行环境的构建脚本 |
| .env.docker | 环境变量模板,通常存放API Key、模型路径等配置 |
| duibi.gif / log.gif | 项目演示动图,用来快速查看对比效果和运行日志 |
| IMG_1762.jpeg / IMG_1872.jpeg | 自带测试图片,用于验证识别流程 |
这两个SVM模型是整个识别链路的重点。svm.dat处理的是车牌第二位到第七位的字母和数字,svmchinese.dat只负责第一位的省份汉字。之所以拆成两个模型,是因为汉字字形结构复杂,类别多,跟英文数字混在一起训练反而会把分类边界搞乱。实际项目里,我一般会直接沿用这两个模型,除非你的识别目标地理区域有明显变化,否则不需要重训。
2.2 Python环境安装:pip一条命令和容易忽略的系统依赖
先装OpenCV。常见做法是用pip安装预编译版本,Windows下不需要自己编译,省掉接近半小时的CMake折腾。项目本身没有强制指定Python版本,建议用3.8到3.10之间的64位解释器,这个区间跟OpenCV的预编译wheel配合最顺。
# 基础依赖,numpy和requests是识别和API调用必需的 pip install opencv-python numpy requests如果你用的是VSCode,装完之后要确认右下角解释器已经切到了同一个虚拟环境,否则import cv2会提示ModuleNotFoundError。PyCharm则建议直接在Settings里新建一个虚拟环境再装,别用全局解释器,不然项目依赖一多就乱了。
Linux下有个高频翻车点:OpenCV导入时报“libGL.so.1: cannot open shared object file”。原因是OpenCV的GUI模块依赖libGL,而精简版系统镜像里通常没有。解决办法是先装系统库再pip install:
sudo apt update sudo apt install -y libgl1 libglib2.0-0 pip install opencv-python numpy requests这一步在Docker基础镜像里尤其容易出问题,后面会专门说。装完后可以跑一句python -c "import cv2; print(cv2.__version__)"确认能看到版本号,看到版本号就说明OpenCV安装这块过了。
2.3 Docker部署:.env.docker和Dockerfile配合起来的运行方式
工程里自带Dockerfile和.env.docker,说明它本身就是按容器运行设计的。容器方式对服务器部署或者毕设答辩演示都方便,不用在评审机器上折腾Python环境。常见做法是把.env.docker复制成.env,然后按模板填上自己的配置项:
cp .env.docker .env docker build -t plate-ocr . docker run --rm -it --env-file .env plate-ocr python main.py --image ./IMG_1762.jpeg这里的--env-file会把.env里的API Key等变量注入容器,让代码用os.getenv读取。main.py这个入口文件名在原工程里不一定叫这个,项目自带的是Dockerfile和模型文件,入口脚本可以由你自己命名,构建时把Dockerfile里的CMD改成你的入口文件就行。首次构建时间会长一些,因为要拉基础镜像和编译依赖,后面再构建会走缓存,快很多。
3. 识别主流程拆解:从目标定位到字符分类
3.1 车牌定位:Sobel边缘检测加形态学闭运算
车牌识别第一步不是识别字符,而是先定位车牌在哪。对着一个复杂背景的停车场照片,直接跑字符识别是浪费算力,所以要先做区域筛选。这份工程里的定位思路延续了OpenCV车牌识别的经典做法:先做边缘检测,再用形态学闭运算把密集的竖向纹理连成一个整体候选块,最后按宽高比过滤。
import cv2 import numpy as np def locate_plate(img): # 高斯模糊降噪,核大小取5x5够用,太大会糊掉边缘 blurred = cv2.GaussianBlur(img, (5, 5), 0) gray = cv2.cvtColor(blurred, cv2.COLOR_BGR2GRAY) # Sobel纵向边缘检测:车牌字符竖向纹理密集,纵向梯度响应明显 sobel = cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize=3) abs_sobel = cv2.convertScaleAbs(sobel) # OTSU自适应二值化,阈值不用手工调 _, binary = cv2.threshold(abs_sobel, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 闭运算:先膨胀后腐蚀,把字符边缘连成块 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for c in contours: x, y, w, h = cv2.boundingRect(c) # 标准车牌宽高比接近3到4,这个范围可以覆盖蓝牌和新能源绿牌 if 2.5 < w / h < 6.0 and w > 80 and h > 20: candidates.append((x, y, w, h)) return candidates这段代码有两个要点。Sobel的ksize取3是经验值,太大会把噪声也当成边缘;闭运算的核用(17,5),横向尺寸远大于纵向,就是为了把相邻字符的竖向边缘横向连接起来。最后筛选条件的w > 80是假设输入图不是缩略图,如果摄像头采集的是1280x720以上的分辨率,这个阈值可以适当放大。我一般会先打印出所有轮廓的宽高比分布,再根据实际场景微调这两个数值。
3.2 字符分割与SVM分类:两个模型各管各的一段
定位出车牌区域之后,要做的第一件事是把车牌上的字符一个个切开。最常见的做法是用垂直投影,也就是统计每一列上的黑色像素数量。字符之间的间隔会形成明显的波谷,根据波谷位置就能切出单个字符。
def char_split(plate_roi): gray = cv2.cvtColor(plate_roi, cv2.COLOR_BGR2GRAY) # 车牌区域光线复杂,直接二值化容易把字符和底色糊在一起, # 先做一次直方图均衡化让字符边缘更干净 gray = cv2.equalizeHist(gray) _, th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) height, width = th.shape col_sum = np.sum(th, axis=0) # 每列黑色像素总数 chars = [] in_char = False start = 0 for i in range(width): if col_sum[i] > 0: if not in_char: start = i in_char = True else: if in_char: # 宽度小于3的片段当成噪声丢掉 if i - start > 3: chars.append((start, i)) in_char = False return chars这里的equalizeHist是我后加的,实际效果很明显,逆光环境下直接二值化往往会把字符和底色粘在一起,均衡化之后字符边缘更干净。字符切完之后,每个字符统一缩放到20x20大小,再提取HOG特征送给SVM分类。HOG特征可以直接用skimage的接口生成,避免自己手写梯度直方图:
from skimage.feature import hog def compute_hog(roi): # orientations=9表示方向分为9个bin,pixels_per_cell取4x4适合20x20的小图 return hog(roi, orientations=9, pixels_per_cell=(4, 4), cells_per_block=(2, 2), visualize=False) svm_eng = cv2.ml.SVM_load('svm.dat') # 字母和数字 svm_ch = cv2.ml.SVM_load('svmchinese.dat') # 中文省份字 def predict_char(roi, idx): resized = cv2.resize(roi, (20, 20)) hog_feat = compute_hog(resized).reshape(1, -1) # 第一位汉字走svmchinese,后面六位走svm_eng model = svm_ch if idx == 0 else svm_eng _, result = model.predict(hog_feat) return int(result[0][0])SVM的输出是一个整数标签,你需要把标签映射回字符。这个映射表在训练模型时就已经固定了,用的时候必须跟训练时的顺序一致。svm.dat和svmchinese.dat分别是两个独立的分类器,千万不要为了省事让所有字符都走同一个模型——中文省份简称和英文数字的特征空间差异很大,混用会让结果完全不可读。
3.3 百度API兜底:本地模型识别不了的场景交给云端
就算SVM调得再好,碰到严重倾斜、低光照、或者车牌上带泥点的情况,本地识别仍然可能失败。工程里保留了百度API的调用通道,正确的用法是做成兜底而不是主流程。
import requests import base64 def baidu_plate(image_path): # 1. 用API Key和Secret Key换AccessToken token_url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=你的AK&client_secret=你的SK' token_resp = requests.post(token_url) access_token = token_resp.json()['access_token'] # 2. 图片转base64交给识别接口 with open(image_path, 'rb') as f: img_base64 = base64.b64encode(f.read()) api_url = f'https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate?access_token={access_token}' r = requests.post(api_url, data={'image': img_base64}) return r.json().get('words_result', {}).get('number', '')这个兜底逻辑的关键是要设置调用条件。我习惯在本地识别结果为空或者置信度低于某个阈值时才发起网络请求,避免每张图片都去刷API。同时AK、SK不要硬编码在代码里,放到.env文件里用环境变量读取,不然代码一提交,密钥也跟着泄露出去了。
4. 六大功能模块怎么落地:文件、URL、摄像头与数据库的四个姿势
4.1 文件图片识别与自适应窗口:别让显示窗口卡掉你的体验
单张图片识别是整个工程的基础功能。工程里提到的“图片自适应窗口大小”,对应OpenCV的窗口标志参数。默认的cv2.imshow不指定窗口属性时,窗口无法缩放,小图放大看不清,大图超屏显示不全。正确的做法是先声明窗口类型,再设置尺寸。
import cv2 def show_plate_result(img_path): img = cv2.imread(img_path) plate_text = full_recognize(img) cv2.putText(img, plate_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) # WINDOW_NORMAL允许用户手动拖拽缩放,WINDOW_AUTOSIZE则强制按原尺寸 cv2.namedWindow('plate_result', cv2.WINDOW_NORMAL) cv2.resizeWindow('plate_result', 800, 600) cv2.imshow('plate_result', img) cv2.waitKey(0) cv2.destroyAllWindows()WINDOW_NORMAL和WINDOW_AUTOSIZE的区别很直接,前者窗口大小可以由用户调整,后者窗口会跟着图像尺寸走。实际演示的时候,我用WINDOW_NORMAL再配合resizeWindow,把窗口固定在一个合理尺寸,体验会好很多。
4.2 网络图片地址识别:带上User-Agent,少踩防盗链的坑
输入不再是一个本地文件路径,而是一个URL。实现思路是先下载图片再识别,但直接requests.get经常拿不到图,很多图床会校验请求头,没有浏览器标识就直接拒绝。下面是工程里适配网络图片的完整流程:
import requests import numpy as np import cv2 def recognize_from_url(url): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } resp = requests.get(url, headers=headers, timeout=5) resp.raise_for_status() # 字节流转numpy数组,再用imdecode解码,省掉临时文件 img_arr = np.frombuffer(resp.content, dtype=np.uint8) img = cv2.imdecode(img_arr, cv2.IMREAD_COLOR) return full_recognize(img)这里用imdecode而不是先写成临时文件再imread,一是减少磁盘IO,二是不怕临时文件清理不干净。timeout参数必须设,不然遇到一个响应很慢的图床,整个线程会卡住。我还会额外检查resp.status_code,碰到403就说明被防盗链拦了,这时要再加Referer头。
4.3 实时截图与摄像头拍照识别:分辨率与帧率之间的取舍
摄像头识别面临的最大问题是速度和画质之间的矛盾。如果摄像头分辨率拉到1920x1080,识别一帧可能要几百毫秒,画面就会非常卡。工程里的做法是把画面降下来,并且跳帧处理:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) frame_id = 0 while True: ret, frame = cap.read() if not ret: break if frame_id % 3 == 0: # 每隔两帧识别一次,减少CPU压力 plate_text = full_recognize(frame) if plate_text: cv2.putText(frame, plate_text, (30, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 255), 2) frame_id += 1 cv2.imshow('camera_plate', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()640x480对车牌识别来说是下限,再低字符会糊掉。跳帧的间隔取决于你的CPU,如果识别一帧需要200毫秒,那么每隔两帧识别一次,画面还能维持在20帧以上,交互体验跟得上。这里的实时截图识别,也可以理解为对屏幕某个区域截图后识别,方式是用PyAutoGUI或PIL的ImageGrab截取屏幕再转成OpenCV的BGR格式,流程和网络图片识别里imdecode那一步是相通的。
4.4 车牌对比、数据库认证与图文搜索:把识别结果串成业务
资源描述里提到的对比识别、车牌数据库认证、图文搜索,本质上都是同一件事:把“识别车牌”这个动作的输出,喂给不同的业务逻辑。对比识别最简单,两张图片分别识别,判断字符串是否相等。数据库认证则要用SQLite或者MySQL存一份已知车牌表,识别后去查。
import sqlite3 conn = sqlite3.connect('plates.db') conn.execute('''CREATE TABLE IF NOT EXISTS plates( plate TEXT PRIMARY KEY, image_path TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP )''') def verify_plate(image_path, allowed_plates): plate = full_recognize(image_path) return plate, plate in allowed_plates def search_images_by_plate(target_plate): # 图文搜索:按车牌查关联图片 cursor = conn.execute( 'SELECT image_path, create_time FROM plates WHERE plate = ?', (target_plate,) ) return cursor.fetchall() def compare_two_images(path_a, path_b): plate_a = full_recognize(path_a) plate_b = full_recognize(path_b) return bool(plate_a) and plate_a == plate_b数据库里车牌字段一定要建索引,数据量上万后没有索引的查询会很慢。还有一个容易被忽略的细节:识别错误是不可避免的,数据库认证系统里一旦把错误车牌写进库里,后续所有查询都会被污染。所以我一般会在写入前加一个人工确认步骤,或者设定一个置信度阈值,低于阈值的识别结果不算数。
5. 避坑记录:四个最容易翻车的地方与现场解法
5.1 中文识别变成乱码:映射表错位和模型加载失败
现象:英文数字都能识别出来,但车牌第一位汉字经常变成“?”,或者干脆是几个完全没有关联的字符。
原因:第一,svmchinese.dat没有正确加载,代码里找不到模型文件时OpenCV不会直接报错,predict会返回一个无效值;第二,训练时定义的标签映射表跟你代码里的字符列表顺序不一致,标签0可能代表“京”,你的代码却把它当成“沪”。
解决:加载模型前先打印文件路径和文件大小,确认资源被正确读取;字符映射表统一由训练脚本导出成JSON,代码里只加载这份JSON,不要手写字符顺序。
import json with open('chinese_map.json', 'r') as f: chinese_map = json.load(f) label = int(svm_ch.predict(hog_feat)[0][0]) plate_char = chinese_map.get(str(label), '?')这样即使标签顺序变了,代码也只是查到空值,而不会识别成另一个字。
5.2 Linux和Docker容器里报libGL错误
现象:代码在Windows上正常,部署到Linux服务器后import cv2直接抛异常,提示“libGL.so.1: cannot open shared object file”。
原因:很多精简版系统镜像和Python基础镜像不带OpenCV的GUI依赖库。
解决:装系统依赖是最快路径。Dockerfile里加一行:
RUN apt-get update && apt-get install -y libgl1 libglib2.0-0如果机器上还缺其他动态库,用ldd查看opencv的so文件缺失项,缺什么补什么,不要盲目去源码编译OpenCV,除非你明确需要CUDA加速。源码编译一次至少半小时起步,而且依赖问题未必能解决。
5.3 摄像头实时识别明显卡顿
现象:画面能显示,但识别结果要隔好几秒才更新,整个流程像是慢放。
原因:摄像头分辨率太高,或者每一帧都跑完整的定位、分割、SVM识别,再加上百度API的网络请求,单帧耗时被拉高。
解决:把分辨率降到640x480,并用跳帧策略。网络请求放到独立线程,识别结果用队列回传,避免阻塞主循环。设置一个置信度阈值,本地识别结果够好就不走API。
5.4 Docker容器里调用cv2.imshow弹不出窗口
现象:程序不报错,但画面窗口永远不会出现,日志输出的waitKey一直阻塞。
原因:容器默认没有显示环境,DISPLAY变量为空,imshow无法创建GUI窗口。
解决:如果只是做后台识别,把cv2.imshow和waitKey去掉,把结果图用cv2.imwrite写到宿主机挂载目录。确实需要看画面时,在宿主机运行容器时挂载X11 socket:
docker run -it --rm \ -e DISPLAY=$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ plate-ocr python main.py --camera5.5 网络图片识别结果为空
现象:本地图片识别正常,换成URL传进来的图片就返回空。
原因:requests请求被防盗链拒绝,返回的是HTML错误页而不是图片;或者图片格式是webp,OpenCV的imdecode无法直接解码。
解决:请求头里加User-Agent,必要时再加Referer。下载后检查resp.headers.get('Content-Type'),以image开头才继续。webp图片可以先用PIL转成PNG再交给OpenCV,避免直接解码失败。
6. 进阶:把HyperLPR接进来,识别率还能再往上顶
6.1 HyperLPR比SVM强在哪
如果你觉得SVM的表现在复杂场景下不够用,工程摘要里提到的HyperLPR就是最直接的升级路径。HyperLPR用卷积网络做端到端识别,和传统SVM方案最大的区别在于它的定位方式更鲁棒:对倾斜车牌、模糊图像和光照不均的容忍度明显更高,字符识别用的是CNN分类器,中文车牌的整体识别率比单独SVM高出一截。代价是模型体积和计算量都上去了,GPU之下运行流畅,纯CPU就要注意帧率控制。
6.2 集成示例:替换识别调用,保留业务代码
HyperLPR的Python接口非常简单,不需要像SVM那样手动分割字符。改造的做法是新增一个识别后端,并在业务层保留原来的full_recognize接口不变:
from hyperlpr import HyperLPR_PlateRecogntion def hyper_recognize(image): # HyperLPR返回列表,每个元素的第一项就是车牌字符串 results = HyperLPR_PlateRecogntion(image) return results[0][0] if results else ''集成时要注意HyperLPR的依赖链里有TensorFlow、Keras和OpenCV,版本之间需要互相兼容。常见做法是单独建一个venv环境,避免跟原有的OpenCV配置冲突。另外HyperLPR对输入图片尺寸有要求,过小的图会直接返回空结果,实际项目中我会先把图进行缩放预处理再接进来。如果是工业级场景,直接使用车牌识别一体机是更省心的选择,一体机把抓拍、补光、识别全部做成硬件方案,但那就不是这份Python工程要解决的问题了。
我之前做停车场道闸项目时就在这里翻过车:本地SVM识别率90%出头,接了HyperLPR后到了97%左右,但CPU占用也上去了。从那以后,我每次接车牌识别都会强制走一遍本地识别、兜底API、跳帧控制这三层流程,再决定要不要上HyperLPR或者专用一体机。希望帮到你。
本文还有配套的精品资源,点击获取