简介:本资源是一套基于Python、PyCharm与OpenCV的车牌识别完整项目源码,面向具备一定Python基础、希望入门计算机视觉与深度学习实战的开发者,可用于交通监控、智能停车等场景的学习与二次开发。压缩包共69个文件,约35.89MB,包含21个py脚本、23个pyc编译文件、7个h5模型权重、7张jpg与3个gif示例图、2个xml配置、1个ttf字体及1个mp4演示视频,覆盖从图像预处理、车牌定位、文本分割到字符识别的完整流程。项目内含模型配置、字体资源与检测脚本,便于直接运行与调试。目前已有876人学习下载。通过该资源,读者可掌握灰度化、直方图均衡化、边缘检测、轮廓筛选等预处理方法,理解连通组件分析与CNN字符识别思路,并借助PyCharm的项目结构高效组织代码、调试模型,为后续数据集标注、模型优化与实时性能调优提供可复用的实践参考。
1. 从一张糊到看不清的抓拍说起:这套车牌识别源码到底能跑出什么
地下车库出口的摄像头抓拍,十张里有三张车牌是斜的、两张过曝、一张被前车挡住半边。拿这种图去跑识别,能出结果才怪。我手上这套基于 PyCharm + Python + OpenCV 的车牌识别项目,解决的正是这种"非理想图像"下的车牌定位与字符分割问题——它不依赖深度学习模型,纯靠 OpenCV 的形态学、边缘检测和轮廓筛选把车牌抠出来,再走字符切分和模板匹配。整套代码在 PyCharm 社区版里直接打开就能跑,不需要 GPU,不需要训练权重,适合刚学完 OpenCV 基础、想找一个完整图像处理链路练手的 Python 入门者,也适合需要快速搭一个车牌定位原型、后续再替换识别模块的工程师。它最大的价值不是识别率有多高,而是把"图像预处理 → 车牌定位 → 字符分割 → 字符识别"这条链路用可调试的代码摊开给你看,每一步都能单独停下来看中间结果。
2. 环境搭建与工程结构:PyCharm 里怎么把 OpenCV 跑通
2.1 为什么选 PyCharm 社区版而不是别的
这套代码对 IDE 没有硬性要求,但 PyCharm 社区版有个实际好处:它的项目解释器管理足够直观,装 OpenCV 和 NumPy 不容易把环境搞乱。很多人用系统 Python 直接 pip install,结果 cv2 和 numpy 版本打架,报出cv2.error: OpenCV(4.4.0) ... pip-req-build这种一看就头大的错误。我的习惯是每个图像处理项目单独建虚拟环境,PyCharm 新建项目时勾选"New environment using Virtualenv",解释器路径指向项目目录下的 venv,这样后面装什么包都不会污染全局。
工程结构上,这套源码通常长这样:
license_plate_recognition/ ├── main.py # 主流程入口 ├── plate_locate.py # 车牌定位 ├── plate_split.py # 字符分割 ├── plate_recog.py # 字符识别 ├── utils.py # 图像显示、保存等工具函数 ├── templates/ # 字符模板图(0-9、A-Z、省份简称) └── images/ # 测试用车辆图片如果你拿到的包结构略有不同,不影响,核心是找到那个main.py或者类似的入口文件。先别急着改代码,把环境跑通再说。
2.2 安装依赖:一行命令背后的版本坑
在 PyCharm 的 Terminal 里执行:
pip install opencv-python numpy matplotlib这里有个血泪经验:opencv-python不要盲目装最新版。有些老代码用了cv2.findContours的旧返回值(两个返回值),而 OpenCV 4.x 返回三个值,直接报ValueError: not enough values to unpack。如果你拿到的源码是几年前写的,先确认它适配的 OpenCV 版本。稳妥做法是装 4.5.x 系列:
pip install opencv-python==4.5.5.64 pip install numpy==1.21.6装完在 PyCharm 里跑一句验证:
import cv2 import numpy as np print(cv2.__version__) print(np.__version__)如果报ModuleNotFoundError: No module named 'cv2',九成是 PyCharm 当前项目解释器没选对。去 File → Settings → Project → Python Interpreter,确认选的是你刚建虚拟环境那个解释器,而不是系统默认的。这个坑我见过太多次,新手往往在 cmd 里 pip install 成功了,但 PyCharm 用的却是另一个解释器,两边对不上。
2.3 第一次运行:先跑通再谈调参
把测试图片放进images/目录,运行main.py。如果一切正常,你会看到弹出窗口依次显示原图、灰度图、边缘检测结果、形态学处理后的车牌候选区域,最后框出车牌并输出识别字符。如果窗口一闪而过,在cv2.imshow后面加cv2.waitKey(0),这是 OpenCV 的 GUI 事件循环,不加的话窗口来不及显示就关了。
第一次跑不建议直接上自己的图,先用源码自带的测试图确认链路通畅。跑通之后,再换自己的图,观察哪一步开始出问题——是定位框歪了,还是字符切分切多了,还是模板匹配对不上。这套代码的调试友好之处就在于每一步都有中间输出,你可以逐段注释掉后面的流程,只看定位结果。
3. 车牌定位的完整链路:从灰度图到候选矩形
3.1 预处理:高斯模糊、Sobel 与形态学闭运算的配合
车牌定位的核心思路是利用车牌区域的纹理特征——字符和背景之间有密集的垂直边缘。所以第一步不是找颜色,而是找边缘。
import cv2 import numpy as np def locate_plate(image_path): # 读取原图 img = cv2.imread(image_path) # 转灰度,减少计算量 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,抑制噪声,核大小(5,5)是经验值 blur = cv2.GaussianBlur(gray, (5, 5), 0) # Sobel算子求x方向梯度,车牌字符垂直边缘明显 sobel_x = cv2.Sobel(blur, cv2.CV_16S, 1, 0, ksize=3) # 转回uint8并取绝对值 abs_x = cv2.convertScaleAbs(sobel_x) # 二值化,阈值可调,150是常用起点 _, binary = cv2.threshold(abs_x, 150, 255, cv2.THRESH_BINARY) # 形态学闭运算,把断裂的字符边缘连成块 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return img, closed这段代码里每个参数都有讲究。高斯核(5,5)太小去不掉噪点,太大把边缘也糊没了。Sobel 的ksize=3是最小核,对细边缘敏感;如果你处理的图分辨率很高,可以试ksize=5。二值化阈值 150 不是固定的,光照强的图可能要提到 180,暗图要降到 100 左右。形态学闭运算的核(17,5)是横向长条,因为车牌字符是横向排列的,这个核能把同一行的字符边缘连成一个矩形块,同时不会把上下无关区域粘进来。如果你发现定位框把整个车头都框进去了,先把核改小,比如(13,5)。
3.2 轮廓筛选:用长宽比和面积把车牌挑出来
闭运算之后,图上会出现若干白色块,车牌只是其中之一。接下来用findContours找轮廓,再按几何特征筛选。
# 找轮廓,RETR_EXTERNAL只取外轮廓 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: # 计算外接矩形 x, y, w, h = cv2.boundingRect(cnt) # 长宽比,中国蓝牌大约3:1到4:1 ratio = w / float(h) # 面积过滤,太小的噪点不要 area = w * h if 2.5 < ratio < 5.0 and area > 1000: candidates.append((x, y, w, h)) # 按面积从大到小排序,取最大的几个作为候选 candidates.sort(key=lambda b: b[2] * b[3], reverse=True) return img, candidates长宽比范围2.5 < ratio < 5.0是覆盖蓝牌和新能源绿牌的常见比例。面积阈值 1000 是针对 720p 以上图片的经验值,如果你的图很小,这个值要降。RETR_EXTERNAL只取最外层轮廓,避免字符内部的孔洞干扰。筛选出来的候选框按面积排序,通常车牌是面积最大的那个,但也不绝对——如果图里有广告牌、路标,可能被误选。所以后面还要加颜色验证。
3.3 颜色验证:HSV 空间里把蓝色车牌捞出来
几何筛选之后,对每个候选框做颜色判断,进一步排除误检。
def verify_by_color(img, box): x, y, w, h = box roi = img[y:y+h, x:x+w] hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 蓝色车牌HSV范围,OpenCV中H是0-180 lower_blue = np.array([100, 80, 80]) upper_blue = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) # 蓝色像素占比超过30%认为是车牌 blue_ratio = cv2.countNonZero(mask) / float(w * h) return blue_ratio > 0.3HSV 的 H 通道在 OpenCV 里是 0 到 180,不是 0 到 360,这个容易搞混。蓝色范围[100, 80, 80]到[124, 255, 255]覆盖了大多数蓝牌。饱和度下限 80 是为了排除灰白色的干扰。占比阈值 0.3 是保守值,实际车牌区域蓝色占比通常超过 50%,但考虑到反光和污渍,放宽到 0.3 更稳。如果是新能源绿牌,把 H 范围改成[35, 80, 80]到[85, 255, 255]。
4. 字符分割与模板匹配:把车牌拆成单个字符
4.1 垂直投影法切分字符
定位到车牌后,裁剪出来,做二值化,然后用垂直投影找字符之间的间隙。
def split_characters(plate_img): # 转灰度并二值化,OTSU自动阈值 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影:统计每列白色像素个数 projection = np.sum(binary, axis=0) / 255 # 找字符边界,投影值从低到高再降低算一个字符 chars = [] in_char = False start = 0 for i, val in enumerate(projection): if val > 2 and not in_char: in_char = True start = i elif val <= 2 and in_char: in_char = False # 宽度过滤,太窄的当噪点 if i - start > 5: chars.append(binary[:, start:i]) return charsTHRESH_OTSU会自动算阈值,比手动指定省事,但对光照不均的图可能翻车。投影阈值val > 2表示这一列至少有两个白色像素才算字符区域,这个值可以根据车牌图大小调整。宽度过滤i - start > 5是去掉那些一两个像素宽的噪点列。切分出来的字符顺序是从左到右,但要注意中文车牌第一个字符是省份简称,宽度通常比后面的字母数字大,模板匹配时要单独处理。
4.2 模板匹配:归一化后的字符比对
字符识别用的是最朴素的模板匹配,把切分出的字符缩放到统一尺寸,和 templates 目录下的模板逐个比对。
def recognize_char(char_img, templates): # 统一缩放到20x40 char_resized = cv2.resize(char_img, (20, 40)) best_score = -1 best_label = '?' for label, tmpl in templates.items(): # 模板匹配,TM_CCOEFF_NORMED返回-1到1 score = cv2.matchTemplate(char_resized, tmpl, cv2.TM_CCOEFF_NORMED) score = score[0][0] if score > best_score: best_score = score best_label = label return best_label, best_scoreTM_CCOEFF_NORMED是归一化相关系数匹配,对亮度变化不敏感,适合这种场景。模板图要提前准备好,每个字符一张,尺寸和char_resized一致。匹配分数低于 0.5 的基本可以认为是误识别,实际使用中可以把低分结果标记出来人工复核。这套模板匹配的识别率在清晰车牌上能到 80% 左右,模糊或倾斜的会明显下降——这是方法本身的边界,不是代码 bug。
4.3 把整条链路串起来
主流程就是把上面的函数按顺序调用:
def main(image_path): img, closed = locate_plate(image_path) _, candidates = find_candidates(closed) for box in candidates: if verify_by_color(img, box): x, y, w, h = box plate = img[y:y+h, x:x+w] chars = split_characters(plate) result = '' for ch in chars: label, score = recognize_char(ch, templates) result += label print('识别结果:', result) cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.imshow('result', img) cv2.waitKey(0)跑通之后你会发现,定位和分割的中间结果比最终识别结果更有调试价值。识别错了,先看分割对不对;分割不对,回头看定位框准不准;定位不准,回去调形态学核和阈值。这条链路每一环都可控,这也是纯 OpenCV 方案相比深度学习黑匣子的优势。
5. 避坑与排查:那些让代码跑不起来的常见问题
5.1 cv2.error 版本不兼容
现象:运行时报cv2.error: OpenCV(4.4.0) ... pip-req-build或者findContours返回值解包失败。
原因:OpenCV 4.x 的findContours返回三个值(contours, hierarchy, _),而旧代码按两个值解包;或者 opencv-python 和 numpy 版本不匹配。
解决:确认代码适配的 OpenCV 版本,统一降级到 4.5.x,同时把 numpy 锁在 1.21.x。用pip list看实际安装版本,不要只看 requirements.txt。
5.2 定位框把整个车头框进去
现象:画出的矩形远大于车牌,甚至覆盖半个车身。
原因:形态学闭运算的核太大,把车身其他边缘也连成块了;或者二值化阈值太低,背景噪声太多。
解决:先把闭运算核从(17,5)降到(13,5)甚至(11,5),再把二值化阈值提高 20 到 30。如果还不行,在轮廓筛选里把长宽比范围收窄,比如3.0 < ratio < 4.5。
5.3 字符切分多切或少切
现象:识别结果里多出无意义的字符,或者两个字符粘在一起。
原因:垂直投影的阈值val > 2不适合当前图片的字符粗细;或者二值化后字符有断裂。
解决:把投影阈值改成动态的,比如取投影最大值的 10% 作为阈值。字符断裂的话,在二值化后加一次形态学闭运算,核用(3,3)小核,把断笔连上。
5.4 PyCharm 里 import cv2 报红但能运行
现象:代码编辑器里import cv2下面有红色波浪线,但运行没问题。
原因:PyCharm 的索引没更新,或者当前解释器选错了。
解决:File → Invalidate Caches → Restart,然后确认 Project Interpreter 指向 venv 里的 python。如果还报红,在 Settings → Project Structure 里把源码目录标记为 Sources。
5.5 换自己的图后完全没结果
现象:测试图能跑,换成自己的图后一个候选框都没有。
原因:图片分辨率、光照、车牌颜色和测试图差异太大,所有阈值都不适用。
解决:先把图缩放到和测试图相近的尺寸,再逐步放宽筛选条件——面积阈值降到 500,长宽比范围扩到2.0 < ratio < 6.0,颜色占比阈值降到 0.2。观察中间输出,看是在哪一步丢的。
6. 进阶调优:把识别率从能用推到好用
模板匹配的天花板就在那里,想再往上走,有几个方向可以试。第一个是字符归一化做得更细:不要统一缩放到 20x40,而是先做透视校正,把倾斜的车牌拉正,再按字符实际宽高比缩放。透视校正用cv2.getPerspectiveTransform和cv2.warpPerspective,需要手动或自动找到车牌的四个角点。自动找角点可以用cv2.approxPolyDP对车牌轮廓做多边形逼近,取四个顶点的版本。
第二个方向是模板库的扩充。同一字符准备多张不同字体、不同粗细的模板,匹配时取最高分。比如"京"字,黑体和宋体各一张,能覆盖更多实际场景。模板图不用多,每个字符三到五张就够,关键是覆盖你实际会遇到的字体风格。
第三个方向是后处理校验。中国车牌有固定格式:第一个是省份简称,第二个是字母,后面五位是字母数字混合。识别完之后用正则校验一遍,不符合格式的字符用次高分候选替换。这一步能把识别率再拉几个百分点,而且实现成本很低。
import re def post_process(result): # 中国车牌正则:省份简称+字母+5位字母数字 pattern = r'^[\u4e00-\u9fa5][A-Z][A-Z0-9]{5}$' if re.match(pattern, result): return result # 不符合格式的,尝试常见混淆替换 confusion = {'0': 'O', 'O': '0', '1': 'I', 'I': '1', '8': 'B', 'B': '8'} for i, ch in enumerate(result): if ch in confusion: candidate = result[:i] + confusion[ch] + result[i+1:] if re.match(pattern, candidate): return candidate return result这个后处理逻辑不复杂,但效果立竿见影。我一般会在识别完每个字符后保留前三个候选,后处理时按格式约束做组合搜索,比单纯替换更稳。
最后一个技巧是关于调试的:把每一步的中间结果都存成图片,用cv2.imwrite写到 debug 目录,文件名带上步骤序号。这样跑完一遍,你有一整套可视化流程,哪一步出问题一目了然。比在 PyCharm 里打断点看变量快得多,尤其是处理批量图片的时候。
从那以后我每次拿到新的图像处理项目,都强制先跑一遍中间结果可视化,确认每一环的输出符合预期再往下走。这套车牌识别代码的价值不在于它多完美,而在于它把每一步都摊开了,让你能看见问题出在哪。希望帮到你。
本文还有配套的精品资源,点击获取