
简介面向计算机视觉与图像处理开发者的车牌识别实战资源基于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 installsudo 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, ksize3) 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, axis0) # 每列黑色像素总数 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): # orientations9表示方向分为9个binpixels_per_cell取4x4适合20x20的小图 return hog(roi, orientations9, pixels_per_cell(4, 4), cells_per_block(2, 2), visualizeFalse) 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_typeclient_credentialsclient_id你的AKclient_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 fhttps://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, headersheaders, timeout5) resp.raise_for_status() # 字节流转numpy数组再用imdecode解码省掉临时文件 img_arr np.frombuffer(resp.content, dtypenp.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 socketdocker run -it --rm \ -e DISPLAY$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ plate-ocr python main.py --camera5.5 网络图片识别结果为空现象本地图片识别正常换成URL传进来的图片就返回空。原因requests请求被防盗链拒绝返回的是HTML错误页而不是图片或者图片格式是webpOpenCV的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或者专用一体机。希望帮到你。本文还有配套的精品资源点击获取