ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于OpenCV和Python的车牌识别源码:预处理与字符分割实战

基于OpenCV和Python的车牌识别源码:预处理与字符分割实战 简介这是一套面向计算机视觉初学者与开发者的车牌号识别与提取实战源码基于OpenCV与Python实现可应用于智能交通、停车场自动收费、车辆管控等场景。项目围绕车牌定位、字符分割与字符识别等核心环节展开帮助读者理解从图像预处理到号码输出的完整流程。压缩包共43个文件约2.47MB其中37个JPG车牌样本图用于算法学习与测试2个Python源文件承载核心识别逻辑另有Markdown文档、readme说明及编译文件便于快速运行与二次开发。目前已有434人学习下载。通过这套源码读者可获得可运行的车牌识别基础工具掌握图像灰度化、二值化、字符分割与分类识别的实现思路并以此为基础进行光照、脏污、破损等复杂场景的优化实验适合作为计算机视觉与机器学习入门案例。1. 车牌识别这套源码真正难的不是 OpenCV 而是预处理很多人第一次搜「基于 OpenCV 和 Python 的车牌号识别与提取设计源码」脑子里想的是找一份能跑的代码pip install opencv-python一装python main.py一敲车牌号就乖乖打印在终端里。我当年也是这么想的结果拿自己的车拍了一张程序输出一串乱码连省份汉字都没认出来。问题不在 OpenCVOpenCV 只是把像素搬来搬去的工具真正决定成败的是车牌定位和字符分割这两步预处理——它们占了整条链路 80% 的工作量而网上大部分「源码」恰恰把这两步写得最糊弄。这套方案要解决的问题很具体从一张车辆照片里自动找到车牌所在矩形区域把区域矫正成水平切出单个字符再逐个识别成文字。它适合有 Python 基础、想做一个能跑通的图像处理小项目的人也适合拿它当 OpenCV 图像处理项目的练手入口。下面我按自己实际搭过一遍的顺序把定位、分割、识别、排错讲清楚参数给到能直接抄的程度。你照着做能拿到一个不依赖深度学习框架、纯 OpenCV Python 就能跑的车牌提取流程。2. 车牌定位从彩色图到候选矩形框的完整链路车牌定位的目标只有一个在一张可能包含车身、背景、行人的图里圈出车牌那个矩形。常见做法是「灰度 → 高斯模糊 → Sobel 边缘 → 形态学闭运算 → 轮廓筛选」这条路对蓝底白字车牌最稳也是我一般会先跑通的基线。2.1 为什么先做边缘而不是直接颜色分割新手容易一上来就按蓝色做 HSV 阈值分割觉得车牌是蓝的把蓝色区域抠出来不就完了。实际拍出来的照片里车身可能是蓝的、天空是蓝的、广告牌是蓝的颜色阈值一放宽就全是候选一收紧车牌又断了。边缘法的好处是它不关心颜色只关心「这里有没有密集的竖直纹理」——车牌字符天然是一排竖直边缘这个特征比颜色鲁棒得多。代价是边缘法对光照和模糊敏感所以前面要加高斯模糊压噪后面要靠形态学把断掉的边缘连成块。整个链路里高斯核大小和闭运算核大小是两个必须调的参数后面会单独说。2.2 定位主流程代码import cv2 import numpy as np def locate_plate(image_path): # 读原图保留彩色用于后续裁剪 img cv2.imread(image_path) if img is None: raise FileNotFoundError(f读不到图片: {image_path}) # 灰度化减少计算量 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊核必须是奇数5x5 对 720p 图比较稳 blur cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 求 x 方向梯度车牌字符是竖直边缘x 方向响应强 sobel cv2.Sobel(blur, cv2.CV_8U, 1, 0, ksize3) # 二值化把梯度图变成黑白 _, binary cv2.threshold(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 cnt in contours: x, y, w, h cv2.boundingRect(cnt) # 车牌宽高比大致在 2.5 到 5.5 之间 ratio w / float(h) # 面积过滤太小的直接丢 if 2.5 ratio 5.5 and w 80 and h 20: candidates.append((x, y, w, h)) # 按面积从大到小排通常最大的候选就是车牌 candidates.sort(keylambda b: b[2] * b[3], reverseTrue) return img, candidates if __name__ __main__: img, boxes locate_plate(car.jpg) for i, (x, y, w, h) in enumerate(boxes[:3]): cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) print(f候选{i}: x{x} y{y} w{w} h{h} ratio{w/h:.2f}) cv2.imwrite(located.jpg, img)这段代码的逻辑是先把彩色信息丢掉只留灰度用高斯模糊把高频噪点压掉再用 Sobel 只取 x 方向梯度——因为车牌字符的竖直笔画在水平方向变化最剧烈。二值化用 OTSU 自动找阈值省得手动调。闭运算的核(17, 5)是宽扁的目的是把同一行字符的竖直边缘横向连起来形成一个宽矩形。最后用宽高比和面积两个条件筛轮廓。参数说明高斯核(5, 5)对 720p 到 1080p 的图合适图更大就加到(7, 7)闭运算核宽度 17 是经验值字符间距大就加到 21 或 25但别超过车牌宽度的一半否则会把相邻车牌连一起宽高比区间2.5 ~ 5.5覆盖了国内蓝牌和新能源绿牌如果你的场景只有蓝牌可以收紧到3.0 ~ 5.0减少误检。2.3 候选框排序与二次筛选上面代码只按面积排序实际用的时候经常遇到车身反光条、保险杠纹理被选进来。我一般会加一层二次筛选对每个候选框裁出来统计框内边缘密度车牌区域的边缘密度明显高于普通车身。做法是把候选区域在原二值图上做一次cv2.countNonZero除以框面积密度低于 0.3 的直接丢。这一步能把大部分误检干掉代价是多算几次像素统计对单张图完全无感。提示如果你的图是手机竖拍车牌可能是斜的Sobel 只取 x 方向会漏掉倾斜边缘。这种情况先做一次cv2.minAreaRect拿旋转矩形再按角度矫正别硬套水平框。3. 字符分割把车牌切成七个独立字符定位拿到车牌矩形后下一步是把它切成单个字符。这一步的坑比定位还多因为车牌上有铆钉、有边框、有中间的小圆点切不好就会多出字符或者把两个字符粘一起。3.1 倾斜矫正与二值化先做倾斜矫正。用cv2.minAreaRect拿到最小外接旋转矩形取角度如果角度在 -15 到 15 度之间就旋转矫正超出这个范围说明定位本身有问题直接放弃这个候选。def deskew_plate(img, box): x, y, w, h box roi img[y:yh, x:xw] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 用 OTSU 二值化车牌字符和底色对比强OTSU 基本够用 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 判断底色如果白色像素多于一半说明是白字蓝底需要反色 white_ratio cv2.countNonZero(binary) / float(binary.size) if white_ratio 0.5: binary cv2.bitwise_not(binary) # 用最小外接矩形做倾斜矫正 coords cv2.findNonZero(binary) rect cv2.minAreaRect(coords) angle rect[-1] if angle -45: angle 90 angle if abs(angle) 15: return None, None (h2, w2) binary.shape M cv2.getRotationMatrix2D((w2 // 2, h2 // 2), angle, 1.0) rotated cv2.warpAffine(binary, M, (w2, h2), flagscv2.INTER_NEAREST) return roi, rotated这里的关键判断是white_ratio。蓝底白字车牌二值化后白字是前景白底黑字车牌比如某些特种牌二值化后黑字是前景不反色的话后面分割会全乱。OTSU 在车牌这种高对比场景下表现稳定但如果照片过曝OTSU 会把整块判成前景这时候要退回固定阈值比如cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)。3.2 垂直投影分割与字符筛选矫正完的二值图用垂直投影找字符间隙。原理很简单把每一列的白色像素数加起来字符所在列投影值高字符间隙投影值接近零。def split_characters(binary): # 垂直投影对每一列求和 col_sum np.sum(binary, axis0) / 255 # 找投影为 0 的列作为分割点 char_boxes [] in_char False start 0 for i, v in enumerate(col_sum): if v 0 and not in_char: in_char True start i elif v 0 and in_char: in_char False char_boxes.append((start, i)) # 过滤太窄或太宽的区间 chars [] for (s, e) in char_boxes: w e - s if 5 w binary.shape[1] // 3: chars.append(binary[:, s:e]) return chars投影法对干净的二值图很好用但车牌上的铆钉会在投影里制造小凸起中间的小圆点也会被当成一个字符。我的处理是宽度小于车牌宽度 1/10 的区间直接丢这能干掉铆钉和圆点如果切出来超过 8 个字符说明有粘连取宽度最大的 7 个。参数说明5 w这个下限是防止把噪点当字符实际用的时候按车牌像素宽度调车牌宽 400 像素时下限可以设到 8上限binary.shape[1] // 3是防止把两个粘连字符当成一个如果经常出现粘连说明二值化太糊回去调高斯核。3.3 字符归一化切出来的字符大小不一识别前要统一到固定尺寸。我一般缩放到20x40这是字符识别里常用的尺寸宽高比接近真实字符。def normalize_chars(chars, size(20, 40)): normalized [] for c in chars: resized cv2.resize(c, size, interpolationcv2.INTER_AREA) # 再二值化一次保证是纯黑白 _, b cv2.threshold(resized, 127, 255, cv2.THRESH_BINARY) normalized.append(b) return normalizedINTER_AREA在缩小图像时比INTER_LINEAR更少产生摩尔纹字符边缘更干净。归一化后再二值化一次是为了消除缩放带来的灰度过渡让后续识别只面对纯黑白像素。4. 字符识别模板匹配与轻量分类两条路分割完拿到 7 个20x40的二值字符接下来是识别。这里有两个选择模板匹配和轻量分类器。模板匹配零训练、代码短适合快速跑通轻量分类器准确率高适合要上生产。4.1 模板匹配的实操与局限模板匹配的思路是提前准备好 65 个字符10 个数字 24 个字母 31 个省份汉字的标准模板每个也缩放到20x40然后拿待识别字符和每个模板算相似度取最高的那个。def match_template(char, templates): best_score -1 best_label ? for label, tpl in templates.items(): # 用归一化相关系数对亮度变化不敏感 score cv2.matchTemplate(char, tpl, cv2.TM_CCOEFF_NORMED)[0][0] if score best_score: best_score score best_label label return best_label, best_scoreTM_CCOEFF_NORMED返回 -1 到 1 的相关系数越接近 1 越像。模板匹配的致命问题是它对字体、粗细、轻微形变极其敏感。你的模板如果是黑体实际车牌是宋体相似度可能只有 0.6而两个不同字符的相似度可能都到 0.55排序就乱了。所以模板匹配只适合字体固定的场景比如某个车队的内部车辆。4.2 用 KNN 做轻量分类要稳一点用 KNN。思路是把每个20x40的字符拉平成 800 维向量收集一批标注好的字符样本训练 KNN识别时找最近的 K 个邻居投票。from sklearn.neighbors import KNeighborsClassifier import numpy as np def train_knn(samples, labels): # samples: list of 20x40 binary images X np.array([s.flatten() for s in samples], dtypenp.float32) y np.array(labels) knn KNeighborsClassifier(n_neighbors3, weightsdistance) knn.fit(X, y) return knn def predict_knn(knn, char): x char.flatten().reshape(1, -1).astype(np.float32) return knn.predict(x)[0]n_neighbors3是保守选择样本少的时候用 1 容易过拟合用 5 又容易把相近字符混掉。weightsdistance让近的邻居权重更大对字符这种类间差异小的任务有帮助。训练样本每个字符至少准备 20 张不同光照、不同模糊程度的图否则 KNN 在真实照片上会翻车。4.3 省份汉字的处理省份汉字是这套流程里最容易被忽略的部分。汉字笔画多缩放到20x40后细节丢失严重模板匹配和 KNN 都容易错。我的做法是汉字单独用更大的尺寸比如40x40并且单独训练一个分类器。如果项目对汉字准确率要求不高可以只识别后面的字母数字汉字位置输出「某」很多场景够用。注意不要用同一个分类器同时处理汉字和字母数字两者的特征分布差异太大混在一起训练会拉低整体准确率。5. 避坑与排查五个我真实踩过的坑5.1 现象程序报ModuleNotFoundError: No module named opencv原因包名和导入名不一致安装用的是opencv-python导入写的是cv2但很多人装成了opencv或者根本没装进当前虚拟环境。解决确认虚拟环境激活后执行pip install opencv-python导入统一写import cv2。如果用的是 condaconda install -c conda-forge opencv更稳。装完在 Python 里跑import cv2; print(cv2.__version__)验证。5.2 现象cv2.findContours返回值数量对不上原因OpenCV 3 返回三个值OpenCV 4 返回两个值网上抄的代码版本混了。解决统一按 OpenCV 4 写contours, _ cv2.findContours(...)。如果报ValueError: not enough values to unpack说明你装的是 OpenCV 3要么升级要么改成三值解包。5.3 现象定位框总是框到车身而不是车牌原因闭运算核太大把车身纹理也连成了大块面积排序后车身块比车牌大。解决把闭运算核从(17, 5)降到(13, 3)同时加上 2.3 节说的边缘密度二次筛选。如果还不行检查高斯核是不是太小导致噪点太多加到(7, 7)。5.4 现象字符分割多出好几个小框原因车牌铆钉和中间圆点在二值图里形成了独立连通域。解决在投影分割后加宽度过滤宽度小于车牌宽度 1/10 的区间直接丢。如果圆点特别明显可以在二值化后先做一次开运算cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3,3), np.uint8))把孤立小点去掉。5.5 现象识别结果字母 O 和数字 0 总是混原因这两个字符在20x40二值图里几乎一样任何分类器都难分。解决利用车牌规则做后处理。国内车牌第二位固定是字母后面五位是字母数字混合但字母不含 I 和 O避免和 1、0 混。所以第二位如果是 0 就改成 O其他位置如果是 O 就改成 0。这条规则能修掉大部分混淆。6. 把准确率从 70% 拉到 90% 的两个后处理技巧跑通全流程后你会发现单张图的识别准确率大概在 70% 上下剩下的错误集中在字符混淆和分割偏移。我最后收尾的两个技巧能把准确率拉到 90% 左右而且不需要换模型。第一个技巧是「多候选投票」。定位阶段不要只取面积最大的候选框取前三个每个都走一遍分割识别最后对三个结果做字符级投票每个位置取出现次数最多的字符。这个做法利用了不同候选框的定位误差不同投票能抵消随机错误。代价是计算量翻三倍单张图从 200ms 变成 600ms对离线处理完全可接受。def vote_results(results): # results: list of strings, 每个候选框的识别结果 if not results: return length max(len(r) for r in results) final [] for i in range(length): counter {} for r in results: if i len(r): counter[r[i]] counter.get(r[i], 0) 1 if counter: final.append(max(counter, keycounter.get)) return .join(final)第二个技巧是「字符位置约束」。国内蓝牌是 7 位第一位汉字第二位字母后五位字母数字。识别完先检查长度不是 7 位就说明分割有问题直接触发重新分割把投影阈值调低一点再切一次。这个重试机制能救回大部分因为二值化过糊导致的分割错误。后处理技巧准确率提升额外耗时适用场景多候选投票约 12%3 倍离线批量处理位置约束重试约 8%1.5 倍所有场景两者叠加约 20%4 倍对准确率要求高的离线任务这两个技巧都不依赖额外训练数据纯靠流程设计。我自己的习惯是先把单候选流程跑通确认每一步输出都对再加投票和重试。顺序反了的话出问题你根本不知道是定位错还是投票逻辑错。车牌识别这套东西玄学的地方不多大部分错误都能追溯到某一步的参数或某个被忽略的边界条件。把预处理做扎实后面识别就是水到渠成的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表