首页 / 资讯中心 / 文章详情

Python+OpenCV车牌识别系统源码:定位、矫正、分割、识别全流程解析

Python+OpenCV车牌识别系统源码:定位、矫正、分割、识别全流程解析 ★ FEATURED ARTICLE
简介基于Python与OpenCV的国内车牌识别系统源码配有图形化操作界面tkinter面向正在准备毕业设计、课程设计或需要项目实战的深度学习CV/模式识别学习者。压缩包共16个文件9个py脚本覆盖GUI主程序、模型训练、视频抽帧与图像转视频等核心流程2个h5权重文件提供预训练模型另有演示视频、GIF运行截图及使用说明文档整体大小约26.7MB。资源内置完整车牌识别流程包括基于CNN与UNet的检测/分割模型可直接运行体验也可逐行阅读代码学习车牌定位、字符分割与识别的主流实现思路。资源编排清晰从数据预处理、模型训练到GUI封装均有覆盖通过自带演示视频和GIF可快速预览效果结合使用说明降低上手门槛。目前已有1086人学习下载适合需要快速搭建可演示项目、或作为毕设基座进行二次开发的同学。1. 基于PythonOpenCV的车牌识别系统从图片到车牌号的完整闭环停车场道闸、小区门禁、工地出入口几乎每个场景都在用摄像头识别车牌但你手里不一定有一份能看懂、能改、能本地跑的方案。拿商业SDK做对接一个授权就是一笔钱而且接口逻辑是个黑匣子——出问题只能找供应商。基于PythonOpenCV的国内车牌识别系统源码搭配GUI界面是另一个思路自己掌握从定位、矫正、分割到识别的全部流程离线运行代码透明。它解决的是“在自己的电脑上快速跑通一套车牌识别流程”这件事适合做毕业设计改造、本地私有化工具以及入门计算机视觉的练手项目。下面就从原理拆到实现把这条链路完整过一遍。2. 车牌识别全流程拆解定位、矫正、分割、识别四个环节的原理与参数任何一个传统车牌识别系统内核都是四段式流水线定位车牌位置、矫正透视变形、分割单个字符、识别字符内容。这四步环环相扣前面任何一环出错后面就全错。这也是为什么很多新手直接去换识别算法准确率却上不来——瓶颈往往根本不在最后一步。2.1 车牌定位为什么用HSV色域而不是直接用RGB车牌定位是整个系统最不稳定的环节。一个画面里可能有车前脸、车窗反光、路牌、护栏、车身的蓝色贴纸都会干扰定位。最常见也最可靠的传统做法是在HSV颜色空间里用颜色范围框出候选区域再用轮廓形状做二次筛选。为什么不直接用RGB因为RGB三个通道对光照几乎是线性敏感。同一个蓝色车牌在阴天和正午日光下B通道值能差出100多而阈值是写死的一换环境就抓不到车牌。HSV把色相H、饱和度S、明度V拆开H描述的是颜色本质受光照影响比RGB小得多。所以用inRange筛色相再配S、V下限滤掉暗色干扰是传统视觉里最稳妥的做法。import cv2 import numpy as np img cv2.imread(car.jpg) # 读取原始图像 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 转到HSV色域 # 蓝色车牌经验阈值H100~124S和V下限都取90 lower_blue np.array([100, 90, 90]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算把车牌字符的黑色挖空补上让车牌连成一块 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 提取轮廓后续按宽高比筛选 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)这段代码做了三件事第一彩色图转HSV第二inRange生成二值掩码第三闭运算把字符造成的黑色空洞补掉。H阈值是核心S和V下限设得越低能容忍的环境越多但误检也越多。经验法则是H偏移±2S下限取80~100V下限取80~120再按你采集环境的亮度微调。拿到轮廓后不能直接当车牌要按车牌的宽高比和面积过滤。蓝牌标准宽高比约440:140透视拉伸后落在2:1到4:1之间面积占整图比例约0.3%到2%。这两个条件能过滤掉大部分像色块但不属于车牌的干扰。对候选轮廓执行minAreaRect得到带角度的最小外接矩形进入矫正环节。传统蓝底白字只是国内最常见的一种新能源绿牌、大车黄牌也在路上出现。按颜色定位时绿牌的H区间约50~80黄牌的H区间约15~35S和V下限相应调低一点。如果你的识别范围只需要蓝牌单独保留蓝色通道的阈值就够了如果场景是大车停车场黄牌检测就得把H区间加上。车牌类型H区间S下限V下限典型场景蓝牌100~1249090小型汽车绿牌新能源50~809080新能源车黄牌15~358080大型货车/客车2.2 透视矫正让倾斜的车牌回到正视图车牌在画面里不一定正对摄像头可能偏左、偏右、仰拍、俯拍字符呈现平行四边形状态。矫正目标是把候选区域变成宽高稳定的矩形再统一缩放到标准尺寸。rect cv2.minAreaRect(plate_contour) box cv2.boxPoints(rect) box np.int0(box) width int(rect[1][0]) height int(rect[1][1]) # 矩形方向修正竖着的矩形交换宽高 if width height: width, height height, width box np.roll(box, 1, axis0) src_pts box.astype(float32) dst_pts np.array([ [0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1] ], dtypefloat32) # 由四个点对求透视变换矩阵再映射到标准矩形 M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(img, M, (width, height))有一个新人容易踩的细节minAreaRect返回的角度范围是-90到0度矩形宽高不一定和视觉上的横竖一致。摄像头斜着拍时返回的宽高可能反了所以先判断宽高再交换。np.roll把坐标点整体挪一位是为了让src_pts和dst_pts四个角一一对应顺序错了矫正出来就是镜像翻转。矫正后的warped要统一resize到440x140这个尺寸是标准车牌宽高比后续分割和二值化的参数都基于这个尺寸调参时能省掉很多麻烦。2.3 字符分割投影法与几个边界条件矫正后的车牌图包含7个字符1个汉字、1个字母、5个字母或数字。字符宽度各不相同直接按固定位置切分一定会错位。常用做法是二值化后做列投影统计每列白色像素总数。字符笔画区域会有明显的波峰字符间隙接近0低谷点就是天然分界。gray cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) # 自适应阈值应对光照不均 binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, # 窗口大小必须是奇数 10 # 减去均值后的补偿值 ) col_sum binary.sum(axis0) # 扫描字符段 char_regions [] in_char False start 0 min_width int(col_sum.shape[0] / 18) for i, val in enumerate(col_sum): if val 0 and not in_char: start i in_char True elif val 0 and in_char: end i if end - start min_width: char_regions.append((start, end)) in_char False if in_char: char_regions.append((start, len(col_sum)))投影法的效果高度依赖二值化稳定性。adaptiveThreshold窗口一般取15到25窗口太小会在笔画内部打出黑色空洞窗口太大又接近全局阈值失去自适应意义。min_width取全宽的1/18大约24像素比这更窄的段大概率是铆钉或边框噪声直接丢弃。分割完成后从右往左数7个段截取字符区域。为什么要从右往左因为车牌最右侧往往有边缘干扰左边多出来的噪声段不要右边按字符边界数是最稳定的。保留首个字符宽度较大的特征识别时才知道第一个是汉字不会拿字母模板去匹配汉字区。2.4 字符识别模板匹配的局限与字符库构建思路传统识别做法是字符模板匹配。准备31个省份简称汉字、24个英文字母、10个数字每个字符和模板逐一匹配取最高相似度作为结果。相似度可以用像素绝对值差也可以用matchTemplate的归一化相关系数。一个现实问题汉字笔画多、字体差异大、形近字多“贵”和“青”在低分辨率下容易认错字母“O”和数字“0”在某些车牌字体下几乎一样。所以很多源码包实际采用双轨策略汉字部分比较字符的轮廓特征比如笔画密度、HOG特征向量距离字母数字部分用模板匹配。模板怎么来可靠做法是用PIL按统一大小渲染字符保存成模板。不要用网上随便下载的车牌字符图片当模板因为拍摄场景字体光照都不一样模板与实际数据差异越大识别越差。模板清晰度、字符居中、背景与前景色固定、统一保存为44x44灰度图这四点缺一不可。提示传统模板匹配在规整场景下够用但遇到扭曲、模糊、夜间噪点会明显退化。如果你的场景复杂建议直接看第六章的CNN字符识别迁移思路模板匹配只当兜底方案。3. 从源码读透核心模块的代码组织与改写入口源码包拿到手第一件事不是急着跑demo先看代码分几个文件、入口在哪、每个模块做什么。常见的工程结构如下plate_recognition/ ├── app.py # GUI主程序入口 ├── recognizer.py # 识别核心逻辑 ├── templates/ # 字符模板目录 │ ├── hanzi/ # 省份简称汉字模板 │ └── chars/ # 字母和数字模板 ├── images/ # 测试图片 └── requirements.txt # 依赖清单这个结构的好处是界面与算法分离app.py只负责界面交互不懂车牌识别recognizer.py只负责图像处理不关心按钮事件。以后想接REST API、命令行工具直接调recognizer就行界面层可以整个丢掉。3.1 识别核心的类设计与调用顺序核心类通常把四段流程拆成独立方法run方法描述完整调用链class PlateRecognizer: def __init__(self, template_dirtemplates/): self.hanzi_templates self.load_templates(template_dir hanzi) self.char_templates self.load_templates(template_dir chars) def detect_plate(self, image): # 返回候选车牌区域轮廓 ... def warp_plate(self, image, contour): # 返回矫正后的矩形车牌图 ... def segment_chars(self, warped): # 返回字符图列表 ... def recognize_chars(self, char_imgs): # 返回识别结果字符串 ... def run(self, image): plate_contour self.detect_plate(image) warped self.warp_plate(image, plate_contour) char_imgs self.segment_chars(warped) result self.recognize_chars(char_imgs) return result改源码时只要保证每个方法输入输出类型不变内部实现可以随意替换。比如detect_plate换成YOLO检测车牌不影响warp_plate和segment_charsrecognize_chars换成CNN分类器前面的分割逻辑也不用动。拆成独立方法的工程价值就在这里。load_templates是很容易被忽略的函数。常见做法是遍历模板目录按文件名首字符建立映射文件名叫“京.png”读完放进字典key为“京”。这个操作要用sorted排序保证Windows和Linux下读取顺序一致否则不同机器上识别结果可能因为模板顺序抖动。依赖清单也值得看一眼。requirements.txt大致包含numpy、opencv-python、PyQt5个别还带Pillow。安装时最容易翻车的是opencv-python和opencv-contrib-python混装两个包同时存在时会导致cv2模块符号冲突启动就报错。装之前先检查环境里面有没有旧包pip uninstall opencv-python opencv-contrib-python -y pip install opencv-python numpy PyQt53.2 字符分割的细节实现边框裁剪与宽度过滤分割代码单独看矫正出来的图像里有车牌边缘边框和字符在二值化后经常连成一片投影分割时把边框当成了一个字符。解决方法是先裁边再动态计算最小宽度。def segment_chars(self, warped): h, w warped.shape[:2] # 裁掉车牌边框区域避免边框干扰投影 crop warped[int(h*0.15):int(h*0.85), int(w*0.05):int(w*0.95)] gray cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) # 自适应二值化 binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 21, 10 ) col_sum np.sum(binary, axis0) # 最小字符宽度约等于全图宽度的1/18 min_width int(col_sum.shape[0] / 18) ...最小宽度的选取需要单独说。全图宽度440像素时1/18大约是24像素正好是英文字符和数字的典型宽度下限。数值太小会引入噪声段太大会把数字“1”这种窄字符丢掉。不同相机分辨率不同但矫正后统一缩放到440宽1/18这个比例在各种环境下都适用这也是上一步统一尺寸的回报。3.3 识别准确率的三个可调旋钮HSV阈值、二值化窗口、模板数量识别结果的瓶颈很少在识别算法本身往往在前级三个参数上。HSV阈值收紧漏检率上升放宽误检率上升。真实场景要区分白天和夜间两套阈值按图像整体亮度自动切换这比一套阈值打天下更适合落地。图像整体亮度可以直接取灰度图的全局均值低于某个门限切到夜间参数。adaptiveThreshold窗口窗口小适应细节窗口大适应均匀光照。窗口一般取21到25。遇到光影复杂的图像可先计算灰度图方差方差大就用更大窗口相当于用图像统计量自动选参。模板数量传统源码包模板往往只有一两套匹配结果在形近字符上很不稳定。能自己补就往模板库加多套数字、字母模板尤其是0、O、1、I这些易混字符模板越多匹配结果越不容易在边缘处翻转。三个参数调完一定要拿一批没参与开发调试的图片验证不要用同一张图调参又验准。用同一批数据调参最后报告的准确率偏乐观换到真实场景立刻露馅。注意调整参数后建议把每版参数写进配置文件的注释里注明调整原因和验证场景。我之前因为偷懒不写注释回退参数只能靠记忆来猜浪费了很多时间。4. GUI界面集成PyQt5主流程、摄像头接入与识别线程设计带GUI的源码包界面多数用PyQt5或Tkinter做的。PyQt5控件丰富、布局灵活摄像头实时画面用QLabel显示图片选择用QFileDialog按钮绑定槽函数Tkinter的好处是标准库自带但界面相对简陋。下面以PyQt5为例拆解。4.1 主窗口布局与图片识别逻辑主窗口可以分出两个区域上半部分图像显示区下半部分操作按钮和识别结果文本框。整体流程很直接最容易顺手就错的地方是显示图像时忘了从BGR转到RGB。from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton, QFileDialog from PyQt5.QtGui import QPixmap, QImage import cv2 class MainWindow(QMainWindow): def __init__(self): super().__init__() self.recognizer PlateRecognizer() # 布局代码这里省略实际就是标签加按钮 def open_image(self): path, _ QFileDialog.getOpenFileName( self, 选择图片, images/, Image Files (*.jpg *.png *.bmp)) if not path: return img cv2.imread(path) plate_text, confidence self.recognizer.run(img) # BGR转RGB否则画面颜色和实际不一致 rgb_img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) height, width, channel rgb_img.shape bytes_per_line 3 * width qimage QImage(rgb_img.data, width, height, bytes_per_line, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(qimage)) self.result_label.setText(f识别结果{plate_text})这个回调函数是图片识别的完整交互。注意QImage构造时传的是rgb_img.data转换后要保证bytes_per_line是3*width。如果显示出来红蓝通道颠倒、画面变成蓝脸车大概率就是漏了cvtColor那行。图片打开后如果原图尺寸很大建议先缩放到QLabel能容纳的尺寸再显示否则大图会直接压垮窗口布局。4.2 摄像头实时识别QThread与信号槽设计实时识别性能要求是单帧处理要快界面不能卡。如果你直接在一个定时器里调用识别函数摄像头每帧都会阻塞主线程窗口拖动都卡顿看起来就像程序假死。正确做法是把识别逻辑放到QThread子线程通过信号把结果传回主线程更新界面。from PyQt5.QtCore import QThread, pyqtSignal class CaptureThread(QThread): frame_ready pyqtSignal(object) # 原始帧信号用于预览 result_ready pyqtSignal(str) # 识别结果信号 def __init__(self, recognizer): super().__init__() self.recognizer recognizer self.device_index 0 # 摄像头设备索引 def run(self): cap cv2.VideoCapture(self.device_index) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while not self.isInterruptionRequested(): ok, frame cap.read() if not ok: continue self.frame_ready.emit(frame) plate_text, confidence self.recognizer.run(frame) if confidence 0.6: self.result_ready.emit(plate_text) # 节流降低识别频率避免线程积压 self.msleep(200)这个线程里有两个关键点。第一是设备索引不能写死笔记本自带摄像头和多台USB摄像头并存时索引会变。界面上加一个下拉框让用户选索引比在代码里改数字更实用。第二是识别无法追上帧率时要节流识别一帧耗时200毫秒循环里加msleep(200)降低积压否则线程消息队列堆满界面收到信号会越来越滞后。信号回传到主线程要用pyqtSignal绑定到界面槽函数def on_result(self, plate_text): self.result_label.setText(plate_text) self.save_result(plate_text)这条链是PyQt5的标准玩法子线程只负责采集和识别主线程只负责刷新UI。跨线程直接修改界面控件是PyQt5的禁忌不通过信号槽会导致随机崩溃这个坑踩一次就长了记性。4.3 识别记录保存CSV落地和编码坑识别结果不能只停在屏幕上停车场场景要记录时间、车牌、置信度方便事后回查。常见做法是追加写入CSV。import csv from datetime import datetime def save_result(plate_text, confidence): row [ datetime.now().strftime(%Y-%m-%d %H:%M:%S), plate_text, f{confidence:.2f} ] with open(records.csv, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow(row)编码是重灾区CSV不指定utf-8-sigExcel打开中文车牌大概率乱码。utf-8-sig会写入BOM标记Excel能正确识别。另一个细节是置信度低于门限时干脆不入库。我见过有人把低于0.5的结果全存进去一周后整理数据发现里面混了几十条错误车牌筛选起来比写代码还痛苦。提示实际现场如果有多路摄像头建议在CSV里加一列摄像头编号后续按编号和日期检索数据会方便很多。多进一出的停车场没有编号的数据基本没法用。5. 避坑指南车牌识别实战中的5个翻车现场这章不聊高深算法全是实际调试中容易卡住你的地方。每一条都按“现象、原因、解决”三个角度拆开你可以直接对照自己的代码定位问题。如果还没遇到过这些坑说明你可能还没把系统放到真实场景里跑过建议先把第二章的流程在自己机器上完整过一遍再回来对照这些边界情况。5.1 矫正出的车牌是镜像翻转的现象矫正之后的图像看起来像从反面看的车牌文字左右颠倒。原因minAreaRect的boxPoints返回的四个点在不同OpenCV版本里排列顺序不一致。src_pts和dst_pts顶点对应错位透视变换就会把图像翻面。解决不要用box自带顺序自己按几何位置重新排序。四个点先按y从小到大排再按x从小到大排强制固定顺序。这样无论OpenCV返回什么顺序结果都一致。代码里加一个sort_vertices函数返回值固定为左上、右上、右下、左下。5.2 阴天识别时蓝牌区域在HSV下碎成几块现象阴天光线弱蓝牌的V通道值整体偏低蓝色区域在HSV下不连续findContours只能找出一堆小块最终因面积不达标全部被过滤定位失败。原因HSV的V下限设置偏高阴天直接把车牌区域的一部分排除掉了。闭运算核不够大碎片没能连成片。解决把V下限从120降到80同时把闭运算核从5x5加大到7x7。阈值越低测到的蓝区域越完整但误检也多要配合轮廓面积过滤来压制。另一个做法是单独准备一套夜间/阴天参数在代码里做成双阈值模式白天夜间自动切换。5.3 数字“1”和字母“L”识别结果来回跳现象同一辆车连续两帧识别第一帧返回京A1F234第二帧返回京ALF234字符1和L来回变。原因模板匹配对形近字符区分力不足归一化后字符分辨率不高1和L的模板相似度高匹配分数差距极小。解决针对这一对易混字符加结构先验。统计字符上半部分是否存在明显水平线段有水平线段的偏向L没有的偏向1。这不是玄学是字符的几何特征。类似处理还适用于字母O和数字0、字母I和数字1这种先验逻辑比分值阈值可靠得多。5.4 画面里同时出现两辆车时识别顺序随机现象画面同时停着前后两辆蓝车识别出的车牌一会儿是前车一会儿是后车不稳定。原因传统HSV定位会把所有蓝色区域都找出来多个候选轮廓时代码取了第一个或最大面积的那个没有考虑车辆重叠时两个轮廓合并成一个大区域的情况合并后的轮廓宽高比仍然在2:1到4:1之间被误当成一个标准车牌。解决检测阶段对候选框做非极大值抑制NMS。计算候选框两两交并比高于阈值就只保留置信度更高的框。OpenCV没有现成的NMS函数用于轮廓提取可以自己按交并比实现def nms_filter(boxes, threshold0.4): keep [] boxes sorted(boxes, keylambda b: b[1], reverseTrue) while boxes: best boxes.pop(0) keep.append(best) boxes [b for b in boxes if iou(best[0], b[0]) threshold] return keep这样多个重叠候选只会留下一个分数最高的识别结果稳定在一个目标上。5.5 加了GUI之后界面一识别就卡死现象点按钮识别单张图片没事一旦接摄像头实时识别界面开始卡顿点哪里都没反应过一会儿弹出未响应提示。原因识别函数跑在GUI主线程。摄像头每帧都触发识别阻塞了Qt事件循环界面无法重绘看起来像卡死。解决把识别逻辑拆到QThread子线程识别完通过信号槽把结果抛回主线程主线程只负责更新QLabel和文本框。这是PyQt5的标准线程模型和算法本身无关但很多人第一次用PyQt5都栽在这。无论是哪条问题调试时把中间阶段的图都保存下来是最高效的手法HSV掩码一张、矫正后一张、二值化一张、分割边界一张。有了这四张图失败在哪一环几乎一眼就能看出来不需要盲猜。很多你以为玄学的问题最终发现其实是某个环节的图和你预期的不一样。6. 进阶验证与优化方向把识别准确率变成可量化的数字功能跑通只是第一步能不能上真实场景要看统计准确率。我会立刻写一个批量验证脚本用真实图片目录做整体评估。import os import cv2 import csv test_dir test_images results [] correct 0 total 0 for filename in sorted(os.listdir(test_dir)): if not filename.endswith(.jpg): continue # 文件名按“真实车牌号_序号.jpg”命名 true_plate filename.split(_)[0] img cv2.imread(os.path.join(test_dir, filename)) plate_text, conf recognizer.run(img) total 1 correct (plate_text true_plate) results.append([filename, true_plate, plate_text, plate_text true_plate]) with open(eval_result.csv, w, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([图片, 真实车牌, 识别结果, 是否一致]) writer.writerows(results) print(f准确率{correct / total:.2%})准确率低于预期不要盲目调参先把错误样本按定位失败、分割失败、识别失败分开统计。做法是把中间过程图各保存一份看看错的那一批卡在哪个环节。定位错的单独放一个目录分割错的在矫正图上画边界识别错的打印出候选字符和模板相似度。四步链路里前面环节的错误会往后级传导所以从链路最前端开始排查效率最高。往深度学习方向升级最常见的路径是保留OpenCV的定位与分割只把recognize_chars从模板匹配换成CNN分类器。用PyTorch或Keras做一个小型卷积分类器并不难难的是样本。不要自己渲染字符当训练集要从真实拍摄的车牌图里把分割好的字符攒起来把省份、光照、清晰度都覆盖到起步每个字符类别有几百张真实样本识别准确率就能明显超过模板匹配。样本是上限模型只是逼近这个上限的手段。这个项目我反复做过几版最大的体会是车牌识别系统的瓶颈很少在算法本身而是数据。干净的数据集、合适的阈值、充分有代表性的测试图片比频繁更换模型更有效。还有一条血泪经验——每版改动都留一份带时间戳的备份不然调坏了想回头后悔药可不好找。话说到这希望这个方向能帮你在自己的环境里跑通一套完整的车牌识别系统。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站