简介:本资源是一份基于Python与OpenCV实现的高分课程设计项目——车牌识别系统源码,面向计算机、人工智能及电子信息类专业本科生,用于数字图像处理课程设计、期末大作业与CV实战入门。项目完整覆盖车牌定位、字符分割与识别三大核心流程,融合颜色分割、边缘检测、形态学操作及SVM字符分类等典型算法,代码经严格调试,下载解压后可直接运行。压缩包共18个文件(4.73MB),含5个核心Python脚本(main.py、chuli.py、img_recognition.py等)、8张测试图像(jpg/png格式)、2个SVM模型文件(svm.dat、svmchinese.dat)、1份PPTX项目汇报材料、1份README说明文档及基础配置文件,结构清晰、模块分工明确,便于理解算法流程与工程组织逻辑。目前已有74人学习下载,适合初学者掌握OpenCV图像预处理与传统机器学习在OCR任务中的落地实践。
1. 这不是“跑通就行”的课程设计:一个97分车牌识别项目,为什么能直接进毕设答辩PPT?
你下载过几十个“OpenCV车牌识别”压缩包,解压后main.py一运行就报错ModuleNotFoundError: No module named 'cv2',或者svm.dat找不到路径,又或者02.jpg里根本没车牌——最后只能把README.md复制粘贴进Word交差。但这个项目不一样:它不是Demo,是实打实拿97分的课程设计成品,压缩包里连.gitignore和车牌识别.pptx都在,chuli.py做预处理、img_recognition.py负责字符识别、main.py是总控入口,svm.dat和svmchinese.dat是训练好的双模型(英文+中文字符),连dhm.jpg这种带模糊+反光+倾斜的真实街拍图都进了测试集。它解决的不是“能不能识别”,而是“怎么让老师一眼看出你真调过参数、真画过轮廓、真改过阈值”——适合计算机/智能科学与技术/电子信息类学生直接用于课程设计、期末大作业、甚至毕设开题验证。不靠玄学调参,靠的是每一步都有对应图像输出(locate.png是定位结果,hy.png是二值化中间态),所有脚本都带中文注释,且全部基于 OpenCV 4.x + Python 3.8 实测通过,不是“理论上可行”的玩具代码。
2. 从一张模糊街拍照到车牌坐标:车牌定位模块的三层过滤逻辑
车牌定位不是简单套个cv2.Canny()就完事。这个项目用的是颜色空间转换 + 形态学增强 + 区域筛选三步闭环,比单纯边缘检测鲁棒得多。核心逻辑在chuli.py和img_function.py中,我们拆开看真实执行链。
2.1 颜色空间降维:为什么不用RGB而选HSV+YUV双通道?
车牌在自然光下易受光照干扰,RGB通道对亮度敏感,而HSV中H(色调)对颜色稳定性高,YUV中U/V通道对蓝底白字/黄底黑字车牌有天然区分度。项目实际采用HSV阈值粗筛 + YUV二次校验:
# chuli.py 第42行起 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) # 蓝牌阈值(国内主流) lower_blue = np.array([100, 43, 46]) upper_blue = np.array([124, 255, 255]) mask_hsv = cv2.inRange(hsv, lower_blue, upper_blue) # YUV中U通道强化蓝/黄车牌(U≈85-130为蓝牌,U≈130-180为黄牌) _, mask_u = cv2.threshold(yuv[:,:,1], 85, 255, cv2.THRESH_BINARY) mask_combined = cv2.bitwise_and(mask_hsv, mask_u)提示:
mask_combined不是最终结果,而是后续形态学操作的输入。这里没用cv2.findContours()直接找轮廓,因为单张图里常有多个蓝色区域(广告牌、衣服),必须叠加几何约束。
2.2 形态学“清洗”:开运算去噪 + 闭运算补边,参数怎么定?
原始掩膜图噪声多、边缘断裂。项目用cv2.morphologyEx()做两次操作,但结构元素尺寸不是拍脑袋定的:
# img_function.py 第67行 kernel_open = np.ones((3, 9), np.uint8) # 宽 > 高:横向去细条噪声(如栅栏、电线) mask_clean = cv2.morphologyEx(mask_combined, cv2.MORPH_OPEN, kernel_open) kernel_close = np.ones((5, 15), np.uint8) # 更大更宽:连接断裂的车牌边缘 mask_final = cv2.morphologyEx(mask_clean, cv2.MORPH_CLOSE, kernel_close)关键点:kernel_open的(3,9)是根据常见车牌宽高比(约3:1)反推的——车牌字符高度约3像素单位,宽度约9像素单位,这样开运算能精准切掉干扰线而不伤车牌主体;闭运算(5,15)则覆盖车牌整体长宽,确保“京A·12345”这种带点的车牌不被断开。
2.3 轮廓筛选:四条规则卡死误检,不是“面积最大就选它”
cv2.findContours()后得到上百个轮廓,项目用硬规则筛出唯一候选:
# main.py 第89行起 contours, _ = cv2.findContours(mask_final, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) aspect_ratio = w / float(h) # 车牌宽高比 2.5~3.5 area = w * h if (2.5 <= aspect_ratio <= 3.5 and 1500 < area < 15000 and # 排除小噪点和整图误检 w > 50 and h > 15 and # 字符最小可识别尺寸 cv2.contourArea(cnt) / area > 0.6): # 轮廓填充率 >60%,排除空心框 candidates.append((x, y, w, h))注意最后一项cv2.contourArea(cnt) / area > 0.6:这是防“矩形框套住整辆车”的关键。真实车牌轮廓接近实心矩形,而车体轮廓面积虽大但内部空洞多,比值远低于0.6。这步让ganzou5.png(车头特写含多块金属反光)也能准确定位。
3. 字符分割不靠投影法硬切:自适应垂直分割 + 黑白跳变检测
传统投影法在字符粘连(如“川A·12345”中“·”与数字粘连)、模糊、低对比度时极易切错。本项目在img_recognition.py中实现了一种基于边缘密度+跳变阈值的自适应分割,对035.png(夜间拍摄带拖影)效果显著。
3.1 二值化不是一刀切:Otsu + 局部均值双保险
01.png(强光直射)和chebai.jpg(背光阴影)需要不同策略:
# img_recognition.py 第22行 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 先全局Otsu _, binary_global = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 再对局部暗区做自适应阈值(窗口11×11,C=2) binary_adapt = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 取并集:确保暗区字符不丢失 binary = cv2.bitwise_or(binary_global, binary_adapt)注意:
cv2.bitwise_or不是简单叠加,而是“任一为白则为白”,避免Otsu在暗区漏字、自适应在亮区过曝。
3.2 垂直分割核心:统计列像素跳变次数,动态找字符间隙
不依赖固定宽度切分,而是扫描每一列,计算黑白跳变频次(即边缘密度):
# img_recognition.py 第105行 def vertical_split(binary_img): h, w = binary_img.shape jump_counts = np.zeros(w) # 每列跳变次数 for col in range(w): col_data = binary_img[:, col] # 统计0→1和1→0跳变总数 jumps = 0 for i in range(1, h): if col_data[i] != col_data[i-1]: jumps += 1 jump_counts[col] = jumps # 找跳变最少的连续区域(字符间隙) gaps = [] for i in range(1, w-1): if jump_counts[i] < 3 and jump_counts[i-1] < 3 and jump_counts[i+1] < 3: gaps.append(i) # 合并相邻gap,取中点为分割线 if not gaps: return [] split_lines = [gaps[0]] for g in gaps[1:]: if g - split_lines[-1] > 5: # 间隔>5像素才认为是新间隙 split_lines.append(g) return split_lines这段代码对dhm.jpg(车牌轻微倾斜+部分字符模糊)依然有效,因为跳变少的列一定是字符之间的空白,而非字符内部(字符内部跳变密集)。分割线位置由split_lines动态生成,后续再用cv2.boundingRect()提取每个字符ROI。
3.3 字符归一化:不是简单resize,而是保持宽高比+边缘填充
分割后字符大小不一,直接resize会扭曲字体。项目用img_math.py中的normalize_char():
# img_math.py 第33行 def normalize_char(char_img, target_size=(40, 60)): h, w = char_img.shape scale = min(target_size[0]/w, target_size[1]/h) # 等比缩放 new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(char_img, (new_w, new_h)) # 计算填充量(居中放置) pad_w = (target_size[0] - new_w) // 2 pad_h = (target_size[1] - new_h) // 2 normalized = cv2.copyMakeBorder(resized, pad_h, pad_h, pad_w, pad_w, cv2.BORDER_CONSTANT, value=0) return normalizedtarget_size=(40,60)是SVM训练时的输入尺寸,必须严格一致。cv2.copyMakeBorder用黑色填充(value=0),确保字符在40×60画布中居中,避免resize导致的形变影响SVM分类精度。
4. 字符识别双模型:SVM英文+中文字符分类器,不是模板匹配凑数
很多“车牌识别”项目用cv2.matchTemplate()匹配0-9+A-Z模板,遇到手写体、磨损、反光就崩。本项目用两个独立训练的SVM模型:svm.dat(英文+数字)和svmchinese.dat(汉字),特征提取用HOG(方向梯度直方图),比LBP更抗光照变化。
4.1 HOG特征提取:参数为什么设为(8,8)+(2,2)?
img_recognition.py中extract_hog_features()函数:
# img_recognition.py 第158行 def extract_hog_features(img): # winSize=(64,128)是HOG标准窗口,但字符图仅40×60,故缩小 winSize = (40, 60) blockSize = (8, 8) # 块大小:8×8像素,兼顾局部纹理和计算量 blockStride = (4, 4) # 块步长:4像素,重叠率50%,提升鲁棒性 cellSize = (2, 2) # 单元格:2×2像素,足够捕获笔画方向 nbins = 9 # 方向bin数:0-180度分9份,覆盖所有笔画角度 hog = cv2.HOGDescriptor(winSize, blockSize, blockStride, cellSize, nbins) features = hog.compute(img) return features.flatten()cellSize=(2,2)是关键:字符笔画宽度常为1-3像素,2×2单元格能精确捕捉“横”“竖”“撇”的方向梯度;若设为(4,4),则细笔画会被平滑掉。blockSize=(8,8)对应4个cell,形成局部纹理描述子,既不过拟合也不欠表达。
4.2 SVM模型加载与预测:如何避免“找不到svm.dat”报错?
main.py中模型加载逻辑:
# main.py 第32行 def load_svm_model(model_path): try: svm = cv2.ml.SVM_load(model_path) print(f"✅ 加载模型成功: {model_path}") return svm except cv2.error as e: print(f"❌ 加载失败 {model_path}: {e}") print("请确认文件存在且路径正确,推荐将项目解压到无中文、无空格路径") exit(1) except Exception as e: print(f"⚠️ 未知错误: {e}") exit(1) svm_en = load_svm_model("svm.dat") svm_zh = load_svm_model("svmchinese.dat")注意:
cv2.ml.SVM_load()在 OpenCV 4.5+ 才稳定支持,若用 OpenCV 3.x 会报AttributeError: module 'cv2.ml' has no attribute 'SVM_load'。解决方案见第5章避坑。
4.3 中英文混合识别逻辑:先判汉字再切分,不是逐字猜
国内车牌格式为“省份简称+字母+5位编码”,如“粤B·12345”。项目先用svm_zh识别第一个字符(必为汉字),再用svm_en识别后续字符:
# main.py 第205行 chars = [] # 第1位必为汉字 char1 = recognize_char(char_imgs[0], svm_zh, is_chinese=True) chars.append(char1) # 第2位开始为英文/数字 for i in range(1, len(char_imgs)): char = recognize_char(char_imgs[i], svm_en, is_chinese=False) chars.append(char) result = "".join(chars) print(f"识别结果: {result}") # 输出"粤B12345"recognize_char()内部对汉字模型做了额外预处理:将字符图旋转-5°~+5°生成3张图,取SVM置信度最高者,解决hy.png(车牌轻微旋转)的识别问题。
5. 避坑指南:97分作业背后踩过的5个真实坑,现在告诉你怎么绕开
这个项目能拿高分,不是因为没bug,而是把常见翻车点全预埋了修复方案。以下是调试过程中记录的5个高频问题,现象、原因、解法全部实测有效。
5.1 现象:ImportError: DLL load failed while importing cv2
原因:Windows下Python与OpenCV版本不匹配,常见于pip install opencv-python装了CPU版,但系统缺MSVC运行库或CUDA驱动冲突。
解决:
- 卸载所有opencv相关包:
pip uninstall opencv-python opencv-contrib-python opencv-python-headless - 清理
site-packages残留文件夹(尤其cv2开头的) - 用conda安装(更稳定):
conda install -c conda-forge opencv=4.8.1 - 若必须pip,指定版本:
pip install opencv-python==4.8.1.78(此版本经项目实测无DLL冲突)
5.2 现象:locate.png显示车牌框错位,或根本没框
原因:chuli.py中HSV阈值针对蓝牌优化,但测试图ganzou5.png是黄牌(甘肃牌照),lower_blue/upper_blue不适用。
解决:
- 手动修改
chuli.py第45行,添加黄牌阈值分支:# 黄牌U通道范围更宽 if is_yellow_plate: _, mask_u = cv2.threshold(yuv[:,:,1], 130, 255, cv2.THRESH_BINARY) - 或更稳妥:运行前用
cv2.imshow("HSV", hsv)查看目标车牌在HSV空间的H值,动态调整lower_blue。
5.3 现象:main.py报错cv2.error: OpenCV(4.8.1) ... error: (-215:Assertion failed) !_src.empty() in function 'cv::cvtColor'
原因:cv2.imread()读取图片失败(路径含中文、文件损坏、扩展名大小写错误如.JPG),返回None,后续cv2.cvtColor(None, ...)崩溃。
解决:
- 在
main.py读图后加校验:img = cv2.imread(args.image) if img is None: print(f"❌ 图片读取失败,请检查路径: {args.image}") print("提示:路径勿含中文、空格,文件是否存在?扩展名是否为.jpg/.png?") exit(1) - 项目自带图均用小写扩展名(
01.png,02.jpg),解压后勿重命名。
5.4 现象:字符识别结果全是“0”或乱码,svm.dat加载后预测全错
原因:SVM模型训练时用的HOG参数(如cellSize)与识别时提取特征的参数不一致,特征向量维度对不上。
解决:
- 确认
img_recognition.py中extract_hog_features()的cellSize、blockSize与训练脚本(项目未提供,但模型文件内嵌参数)一致。本项目固定为(2,2)和(8,8)。 - 若自行训练,导出模型时保存参数:
# 训练脚本中 import pickle with open('hog_params.pkl', 'wb') as f: pickle.dump({'cellSize': (2,2), 'blockSize': (8,8)}, f)
5.5 现象:车牌识别.pptx打不开,提示“文件损坏”
原因:Windows资源管理器默认隐藏扩展名,用户可能将车牌识别.pptx.zip误认为车牌识别.pptx,直接双击导致Office用zip程序打开。
解决:
- 解压后进入文件夹,找到真实
车牌识别.pptx(大小约1.2MB,非几KB的小文件) - 若仍打不开,用WPS Office或PowerPoint 2016+打开(兼容性更好)
- 项目PPT含完整流程图、关键代码截图、效果对比图,建议答辩前用“幻灯片放映→排练计时”预演一遍。
6. 进阶技巧:用img_function.py里的draw_contour_debug()做实时调试,比print()高效10倍
课程设计最耗时间的不是写代码,而是定位哪一步出错了。比如035.png识别结果是“粤B1234”少一位,你得知道是分割丢了“5”,还是SVM把“5”认成“3”。项目在img_function.py里藏了一个神级调试函数draw_contour_debug(),它能把每一步中间图自动保存+叠加文字标注,比疯狂cv2.imshow()省事得多。
6.1 三步启用调试模式:改一行代码,生成12张过程图
默认main.py不保存中间图,开启只需改一处:
# main.py 第12行 DEBUG_MODE = False # 改为 True运行后,同目录生成debug/文件夹,内含:
| 文件名 | 内容 | 用途 |
|---|---|---|
01_original.jpg | 原图 | 对照基准 |
02_hsv_mask.png | HSV掩膜图 | 检查颜色分割是否准确 |
03_morphology.png | 形态学处理后 | 看噪声是否清除干净 |
04_contours.png | 所有候选轮廓 | 快速判断筛选规则是否过严 |
05_plate_roi.png | 裁剪出的车牌图 | 验证定位精度 |
06_binary_plate.png | 二值化车牌 | 检查字符是否连通 |
07_vertical_gaps.png | 垂直分割线 | 看字符间隙识别是否正确 |
08_char_00.png~08_char_06.png | 分割出的每个字符 | 逐个检查形变/缺失 |
09_normalized_00.png | 归一化字符 | 确认尺寸是否达标 |
10_hog_feature_00.png | HOG可视化图 | 验证特征提取有效性(需额外库) |
11_svm_result.txt | SVM原始输出 | 查看各字符置信度 |
12_final_result.png | 最终结果图 | 含识别文本+红框 |
注意:
08_char_*.png和09_normalized_*.png是关键。若08_char_05.png是空图,说明分割算法在第6个字符处失败;若09_normalized_05.png字符扭曲,则问题在归一化逻辑。
6.2 用cv2.putText()在图上标参数,告别记不住的magic number
draw_contour_debug()不只存图,还在每张图右下角用cv2.putText()标出当前步骤参数:
# img_function.py 第210行 def draw_contour_debug(img, step_name, params=None): h, w = img.shape[:2] # 右下角写步骤名 cv2.putText(img, f"{step_name}", (w-150, h-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) # 若有参数,换行写 if params: text = " | ".join([f"{k}={v}" for k,v in params.items()]) cv2.putText(img, text, (w-150, h-25), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255,0,0), 1) return img例如03_morphology.png右下角显示:morphology | kernel_open=(3,9) | kernel_close=(5,15)。这样你调参时改了kernel_open,立刻能在图上看到效果,不用翻代码找变量名。
6.3 一个血泪经验:每次改chuli.py前,先备份locate.png作基线
我在调试ganzou5.png时,曾把kernel_close从(5,15)改成(7,17),结果locate.png里车牌框变大,把车灯也框进去了。当时没备份,花了2小时才还原。现在我的习惯是:
- 运行一次原版
main.py -i ganzou5.png,保存debug/04_contours.png为baseline_ganzou5.png - 修改代码后,再运行,对比新旧图差异
- 用
cv2.absdiff()自动计算差异像素数:
差异<1000说明改动安全,>5000就得回滚。base = cv2.imread("baseline_ganzou5.png", 0) curr = cv2.imread("debug/04_contours.png", 0) diff = cv2.absdiff(base, curr) print(f"轮廓差异像素: {np.sum(diff > 30)}") # 阈值30,忽略微小抖动
从那以后我每次改chuli.py或img_recognition.py,都强制走一遍这个备份-对比流程。不是怕错,是怕花时间在已知问题上兜圈子。希望帮到你。
本文还有配套的精品资源,点击获取