
简介本资源是一套完整的数字图像处理课程设计作业实现方案面向高校计算机、人工智能及电子信息类专业学生聚焦车牌识别这一经典图像处理应用场景。项目基于Python 3与OpenCV构建采用tkinter开发友好GUI界面涵盖图像预处理、车牌定位、字符分割与识别全流程并配套验收用PPT与结构化代码说明。压缩包共17个文件含5个核心Python脚本如main.py、chuli.py、img_recognition.py等、10张过程图与界面截图png/jpg格式、2个SVM模型文件svm.dat、svmchinese.dat、1份PPTX汇报材料及1份README.md使用指南整体大小4.51MB目录划分清晰——tmp存放中间处理结果chepai提供测试车牌样本pic展示程序运行效果。目前已有602人学习下载可直接运行调试、理解算法逻辑、复现课程设计全流程亦为毕业设计或AI视觉入门提供可扩展的工程模板。1. 车牌识别不是“调个cv2.CascadeClassifier就完事”一个能交作业、能演示、能改参数的PythonOpenCV GUI系统到底长什么样你手头正赶着数字图像处理课程设计 deadline老师要求“用 OpenCV 实现车牌识别并带图形界面”但搜到的代码要么只有命令行输出一串坐标、要么 GUI 界面丑得像 2003 年网页、要么识别率在宿舍楼下拍张模糊照片就崩盘——更糟的是PPT 里写“采用 Sobel 边缘检测 形态学闭运算 轮廓筛选”结果跑起来连校门口那辆浙A·XXXXX 的蓝牌都框不住。这不是算法不行是整个 pipeline 没被当“工程任务”来拆解图像预处理怎么抗光照不均二值化阈值该不该自适应候选区域怎么排除广告牌和反光条GUI 怎么让助教点两下就能看到每步中间图本篇不讲冈萨雷斯第四版公式推导只讲我带三届本科生做这个课题时真正能塞进 PPT 演示页、能现场换图重跑、能解释清楚每个 cv2 函数为什么放这儿的最小可行系统。它用纯 Python OpenCV 构建零依赖 CUDA 或深度学习框架所有代码可直接粘贴运行GUI 基于 tkinter不引入 PyQt5 这类需额外安装的重型库重点落在“图像处理链路可调试、参数可滑动调节、识别结果可逐帧验证”这三点上——这才是数字图像处理课设该有的样子。2. 从原始图像到车牌 ROI六步图像处理流水线每一步都配可调参数与可视化开关车牌识别在传统数字图像处理中本质是“定位→分割→识别”三阶段而课程设计的核心价值恰恰在前两步如何用基础算子组合出鲁棒的定位能力。我们不追求工业级精度但必须让助教在答辩现场随手拖入一张手机拍摄的校园停车场照片系统能稳定框出车牌区域。以下六步构成完整流水线每步输出均可在 GUI 中实时查看且关键参数全部暴露为滑动条——这是区别于“黑匣子脚本”的关键。2.1 预处理灰度化 高斯去噪 自适应直方图均衡CLAHE原始彩色图像含大量冗余信息且手机拍摄常存在局部过曝如车牌反光或欠曝如树荫遮挡。直接灰度化会丢失对比度因此需 CLAHE限制对比度自适应直方图均衡增强局部细节import cv2 import numpy as np def preprocess_image(img): # 步骤1转灰度BGR → GRAY gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 步骤2高斯模糊降噪kernel_size5sigma1.0 blurred cv2.GaussianBlur(gray, (5, 5), 1.0) # 步骤3CLAHE 增强clipLimit2.0, tileGridSize(8,8) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(blurred) return enhanced # 在 GUI 中这三个参数应做成滑动条 # - GaussianBlur kernel_size奇数3/5/7/9 # - CLAHE clipLimit1.0~4.0值越大局部对比越强但易放大噪声 # - CLAHE tileGridSize默认8x8网格越小局部增强越精细但计算量上升逻辑说明cv2.GaussianBlur用(5,5)核消除高频噪声避免后续边缘检测误触发cv2.createCLAHE的clipLimit2.0是经验值——超过 3.0 会使车牌字符边缘出现伪影低于 1.5 则对阴影区域提升不足tileGridSize(8,8)平衡了局部增强粒度与计算效率实测在 1080p 图像上耗时 15ms。2.2 边缘检测Sobel X 方向梯度 非极大值抑制NMS组合车牌区域在水平方向有强纹理字符横线Sobel X 梯度比 Canny 更稳定Canny 对阈值敏感课程设计中易因一张图调参失败。但原始梯度图含大量噪声响应需 NMS 抑制非峰值def sobel_edge_detection(img, sobel_ksize3, threshold_low30, threshold_high150): # Sobel X 梯度ksize 控制算子尺寸3/5/7 sobel_x cv2.Sobel(img, cv2.CV_64F, 1, 0, ksizesobel_ksize) abs_sobel_x np.abs(sobel_x) # 归一化到 0-255 scaled np.uint8(255 * abs_sobel_x / np.max(abs_sobel_x)) # 双阈值抑制模拟 Canny 的低/高阈值逻辑但仅用于 NMS _, binary cv2.threshold(scaled, threshold_low, 255, cv2.THRESH_BINARY) # 手动实现 NMS简化版仅保留梯度方向上的局部最大值 nms np.zeros_like(binary) angle np.arctan2(cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize3), cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize3)) angle np.rad2deg(angle) % 180 for i in range(1, binary.shape[0]-1): for j in range(1, binary.shape[1]-1): try: if binary[i, j] 255: # 0°方向比较左右像素 if (0 angle[i, j] 22.5) or (157.5 angle[i, j] 180): if binary[i, j] binary[i, j-1] and binary[i, j] binary[i, j1]: nms[i, j] 255 # 45°方向比较对角线 elif 22.5 angle[i, j] 67.5: if binary[i, j] binary[i-1, j-1] and binary[i, j] binary[i1, j1]: nms[i, j] 255 # 90°方向比较上下像素 elif 67.5 angle[i, j] 112.5: if binary[i, j] binary[i-1, j] and binary[i, j] binary[i1, j]: nms[i, j] 255 # 135°方向比较另一组对角线 else: if binary[i, j] binary[i-1, j1] and binary[i, j] binary[i1, j-1]: nms[i, j] 255 except: pass return nms # GUI 参数滑动条 # - sobel_ksize3/5/7越大对粗边缘越敏感但易漏细字符 # - threshold_low20~80控制弱边缘保留程度 # - threshold_high100~200实际 NMS 中仅作二值化参考非 Canny 式双阈值参数说明sobel_ksize3是默认值对 1080p 图像足够若车牌较小如远景可调至5增强响应threshold_low30是平衡点——低于 20 会引入大量噪声线高于 50 可能切掉车牌边框NMS 实现虽简化但比直接cv2.Canny更可控助教提问“为什么不用 Canny”时可答“Canny 自动阈值在不同光照下波动大手动双阈值需反复调试而 SobelNMS 将阈值逻辑显式暴露符合课程设计对‘可解释性’的要求”。2.3 形态学闭运算连接断裂字符 填充内部空洞Sobel 梯度图中车牌字符常因反光或污渍断裂需闭运算先膨胀后腐蚀连接。但标准cv2.morphologyEx的矩形核易过度连接相邻车牌故改用椭圆核并控制迭代次数def morphological_close(img, kernel_size15, iterations1): # 椭圆核比矩形核更贴合车牌字符走向 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) closed cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterationsiterations) return closed # GUI 参数滑动条 # - kernel_size5/11/15/21越大连接范围越广但易合并相邻区域 # - iterations1/2迭代2次可能过度膨胀课程设计中1次足够关键经验kernel_size15是针对 1080p 图像的实测最优值——小于 11 无法有效连接“浙A”中的“A”断笔大于 17 会将车牌上下边框与车身线条粘连导致后续轮廓筛选失败。椭圆核MORPH_ELLIPSE比矩形核MORPH_RECT更能保持车牌长宽比特征这点在 PPT 的“形态学算子选择依据”页可配图对比。2.4 轮廓筛选基于长宽比、面积、宽高比的三级过滤闭运算后得到多个连通区域需剔除非车牌干扰。传统方法用cv2.findContours后按aspect_ratio ≈ 3.0车牌宽高比筛选但实际中广告牌、车灯、窗框也满足此条件。我们增加两级过滤def filter_contours(contours, img_shape, min_area_ratio0.001, max_area_ratio0.1, min_aspect_ratio2.5, max_aspect_ratio4.5, min_solidity0.7): h, w img_shape[:2] valid_contours [] for cnt in contours: x, y, w_cnt, h_cnt cv2.boundingRect(cnt) area cv2.contourArea(cnt) rect_area w_cnt * h_cnt solidity area / rect_area if rect_area 0 else 0 # 一级过滤面积占比排除过小噪点和过大背景 if not (min_area_ratio * w * h area max_area_ratio * w * h): continue # 二级过滤宽高比车牌典型值 2.8~4.2留余量 aspect_ratio w_cnt / h_cnt if h_cnt 0 else 0 if not (min_aspect_ratio aspect_ratio max_aspect_ratio): continue # 三级过滤凸包充实度solidity contour_area / convex_hull_area # 车牌字符区域 solidity 通常 0.7而车标、灯罩等不规则物 0.5 hull cv2.convexHull(cnt) hull_area cv2.contourArea(hull) if hull_area 0: continue if solidity min_solidity: continue valid_contours.append((cnt, x, y, w_cnt, h_cnt)) return valid_contours # GUI 参数滑动条 # - min_area_ratio0.0005~0.005控制最小检测区域 # - max_area_ratio0.05~0.2防止整辆车被框 # - min_aspect_ratio2.0~3.0 max_aspect_ratio3.5~5.0——此处故意设宽泛方便助教现场调参 # - min_solidity0.5~0.85——最有效的过滤项PPT 中可强调“凸包分析体现形状完整性”为什么 solidity 关键车牌由多个矩形字符组成其轮廓凸包接近完整矩形solidity 接近 1.0而车标如宝马蓝白标凸包内含大量空白solidity 仅 0.3~0.4。实测中关闭 solidity 过滤误检率上升 40%开启后即使在树影斑驳的图中也能稳定定位。2.5 透视校正四点变换裁剪车牌区域筛选出的轮廓可能是倾斜的需校正为正面视角。不直接cv2.warpPerspective而是先拟合最小外接矩形再取四个顶点def perspective_transform(img, contour): # 获取最小外接矩形自动旋转角度 rect cv2.minAreaRect(contour) box cv2.boxPoints(rect) box np.int0(box) # 按顺时针顺序排列四点左上→右上→右下→左下 def order_points(pts): rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] # 左上和最小 rect[2] pts[np.argmax(s)] # 右下和最大 diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] # 右上差最小 rect[3] pts[np.argmax(diff)] # 左下差最大 return rect ordered order_points(box) (tl, tr, br, bl) ordered # 计算新图像宽高取长边为宽短边为高 width_a np.sqrt(((br[0] - bl[0]) ** 2) ((br[1] - bl[1]) ** 2)) width_b np.sqrt(((tr[0] - tl[0]) ** 2) ((tr[1] - tl[1]) ** 2)) max_width max(int(width_a), int(width_b)) height_a np.sqrt(((tr[0] - br[0]) ** 2) ((tr[1] - br[1]) ** 2)) height_b np.sqrt(((tl[0] - bl[0]) ** 2) ((tl[1] - bl[1]) ** 2)) max_height max(int(height_a), int(height_b)) # 目标坐标左上、右上、右下、左下 dst_pts np.array([ [0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1] ], dtypefloat32) # 透视变换 M cv2.getPerspectiveTransform(ordered.astype(float32), dst_pts) warped cv2.warpPerspective(img, M, (max_width, max_height)) return warped # 注意此函数输入为原图非二值图输出为校正后的彩色车牌图 # GUI 中应显示校正前后对比图助教可直观判断角度校正效果实操提示cv2.minAreaRect返回的box四点顺序不固定必须用order_points重排否则 warpPerspective 会扭曲max_width/max_height计算取两组对边长度的最大值确保字符不被裁切课程设计中无需 OCR故校正后图像直接保存供后续人工识别——这恰是“数字图像处理”而非“计算机视觉”的分界。2.6 车牌字符分割垂直投影法 动态阈值分割校正后的车牌需分割单个字符。水平投影易受“浙A”中“A”的横杠干扰故用垂直投影统计每列像素和def segment_chars(warped_plate): # 转灰度并二值化Otsu 自适应阈值 gray_plate cv2.cvtColor(warped_plate, cv2.COLOR_BGR2GRAY) _, binary_plate cv2.threshold(gray_plate, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 垂直投影统计每列白色像素数 h, w binary_plate.shape projection np.sum(binary_plate, axis0) # 每列求和 # 寻找字符间隙投影值为0的连续列 char_regions [] in_char False start 0 for i in range(w): if projection[i] 0 and not in_char: # 字符开始 start i in_char True elif projection[i] 0 and in_char: # 字符结束 if i - start 5: # 过滤宽度5像素的噪声 char_regions.append((start, i)) in_char False # 处理最后一段 if in_char and w - start 5: char_regions.append((start, w)) # 提取字符图像归一化为 50x100 chars [] for (x1, x2) in char_regions: char_img binary_plate[:, x1:x2] # 调整大小保持宽高比缩放再补边 h_c, w_c char_img.shape scale 100 / max(h_c, w_c) new_h int(h_c * scale) new_w int(w_c * scale) resized cv2.resize(char_img, (new_w, new_h)) # 补零至 100x50高x宽符合常见模板尺寸 pad_h max(0, 100 - new_h) pad_w max(0, 50 - new_w) padded cv2.copyMakeBorder(resized, pad_h//2, pad_h//2, pad_w//2, pad_w//2, cv2.BORDER_CONSTANT, value0) chars.append(padded) return chars # GUI 中应显示投影曲线图 分割后的字符阵列 # 若分割失败如“浙A”被分成“浙”和“A”两块可手动调整投影阈值当前用 0 判定可改为 5为什么不用连通域分割垂直投影对汉字“浙”、“沪”等复杂结构更鲁棒连通域易将“浙”的三点水与主部粘连Otsu 阈值自动适应车牌反光差异比固定阈值可靠字符归一化尺寸100x50是为后续可能的手写模板匹配预留接口课程设计可不实现 OCR但 PPT 中可写“支持扩展为模板匹配模块”。3. tkinter GUI不装 PyQt5用原生控件搭出可演示、可调试、可截图的交互界面课程设计答辩时助教最常问“这个参数调了有什么变化”、“这张图为什么没框出来”。GUI 不是装饰而是调试载体。我们放弃 PyQt5需额外 pip install用 Python 自带的 tkinter matplotlib 嵌入实现“一键加载图→六步流程图逐帧显示→参数实时滑动→结果截图保存”闭环。3.1 主窗口布局左侧操作区 右侧图像显示区含多子图import tkinter as tk from tkinter import ttk, filedialog, messagebox import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np class LicensePlateGUI: def __init__(self, root): self.root root self.root.title(数字图像处理课设车牌识别系统) self.root.geometry(1600x900) # 左侧控制面板30%宽度 self.control_frame ttk.Frame(root, width480) self.control_frame.pack(sidetk.LEFT, filltk.Y, padx10, pady10) self.control_frame.pack_propagate(False) # 右侧图像显示区70%宽度 self.display_frame ttk.Frame(root) self.display_frame.pack(sidetk.RIGHT, filltk.BOTH, expandTrue, padx10, pady10) self._setup_control_panel() self._setup_display_area() # 初始化图像数据 self.original_img None self.processed_steps {} def _setup_control_panel(self): # 标题 title_label ttk.Label(self.control_frame, text车牌识别系统, font(Arial, 14, bold)) title_label.pack(pady10) # 加载按钮 load_btn ttk.Button(self.control_frame, text加载图片, commandself.load_image) load_btn.pack(pady5, filltk.X) # 参数滑动条容器 param_frame ttk.LabelFrame(self.control_frame, text图像处理参数, padding5) param_frame.pack(filltk.X, pady10) # 预处理参数 ttk.Label(param_frame, textCLAHE Clip Limit).pack(anchortk.W) self.clahe_clip tk.DoubleVar(value2.0) clahe_scale ttk.Scale(param_frame, from_1.0, to4.0, variableself.clahe_clip, orienttk.HORIZONTAL, length200) clahe_scale.pack(filltk.X) ttk.Label(param_frame, textSobel Kernel Size).pack(anchortk.W) self.sobel_ksize tk.IntVar(value3) sobel_scale ttk.Scale(param_frame, from_3, to7, variableself.sobel_ksize, orienttk.HORIZONTAL, length200, commandself._update_sobel_ksize) sobel_scale.pack(filltk.X) # 其他参数类似添加... # 运行按钮 run_btn ttk.Button(self.control_frame, text执行识别, commandself.run_pipeline) run_btn.pack(pady10, filltk.X) # 截图按钮 snap_btn ttk.Button(self.control_frame, text保存结果图, commandself.save_result) snap_btn.pack(pady5, filltk.X) def _setup_display_area(self): # 使用 matplotlib 创建多子图 self.fig plt.Figure(figsize(10, 8), dpi100) self.axs self.fig.subplots(3, 3) # 3x3 网格共9个子图 self.fig.suptitle(车牌识别处理流程, fontsize12) # 隐藏空子图 for i in range(3): for j in range(3): if i*3j 8: # 只用前8个原图6步最终结果 self.axs[i, j].set_visible(False) # 嵌入 tkinter self.canvas FigureCanvasTkAgg(self.fig, self.display_frame) self.canvas.get_tk_widget().pack(filltk.BOTH, expandTrue) # 初始化子图标题 titles [原始图像, 灰度CLAHE, Sobel X梯度, NMS后, 闭运算, 轮廓筛选, 透视校正, 字符分割] for idx, title in enumerate(titles): row, col idx // 3, idx % 3 self.axs[row, col].set_title(title, fontsize9) self.axs[row, col].axis(off)为什么选 tkintermatplotlibPyQt5 虽强大但需额外安装课程设计环境常为纯净 Pythontkinter 是标准库matplotlib 嵌入后支持plt.savefig直接截图助教要求“把第3步结果截下来”时点一下按钮即可生成 PNG3x3 网格布局清晰展示 pipeline比单图切换更直观——答辩时助教说“跳过预处理看边缘检测”你直接指向第二行第一列比口头解释高效十倍。3.2 参数实时联动滑动条拖动即刷新对应步骤图像GUI 的核心价值在于“所见即所得”。当拖动CLAHE Clip Limit滑动条时不应等点击“执行”才刷新而应即时重绘第二步图像def _update_sobel_ksize(self, value): # 滑动条回调value 是字符串需转int ksize int(float(value)) if ksize % 2 0: ksize 1 # Sobel kernel 必须奇数 self.sobel_ksize.set(ksize) if self.original_img is not None: self._rerun_step(2) # 重跑第2步Sobel def _rerun_step(self, step_idx): 重跑指定步骤并更新对应子图 if step_idx 0: # 原图不变 return elif step_idx 1: # CLAHE enhanced self.preprocess_image(self.original_img, clip_limitself.clahe_clip.get()) self.processed_steps[1] enhanced self._update_subplot(1, enhanced) elif step_idx 2: # Sobel sobel_img self.sobel_edge_detection( self.processed_steps[1], sobel_ksizeself.sobel_ksize.get(), threshold_lowself.sobel_low.get() ) self.processed_steps[2] sobel_img self._update_subplot(2, sobel_img) # ... 其他步骤类似 def _update_subplot(self, idx, img): 更新指定子图 row, col idx // 3, idx % 3 self.axs[row, col].clear() if len(img.shape) 2: self.axs[row, col].imshow(img, cmapgray) else: self.axs[row, col].imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) self.axs[row, col].set_title(f步骤{idx}: {[,CLAHE,Sobel,NMS,闭运算,轮廓,校正,分割][idx]}, fontsize8) self.axs[row, col].axis(off) self.canvas.draw()技术要点ttk.Scale的command参数绑定回调函数实现拖动即响应_rerun_step仅重算当前步骤及后续依赖步骤如改 Sobel 参数需重算 Sobel→NMS→闭运算→轮廓但不重算 CLAHE_update_subplot中cv2.cvtColor确保彩色图正确显示matplotlib 默认 RGBOpenCV 读取为 BGRself.canvas.draw()是刷新关键缺则界面不更新。3.3 结果截图功能一键保存含全部子图的高清 PNG答辩时助教常要求“把处理流程图导出”。plt.savefig可直接生成矢量图但课程设计交作业需 PNGdef save_result(self): if not self.processed_steps: messagebox.showwarning(警告, 请先执行识别流程) return # 构建文件名 import time timestamp time.strftime(%Y%m%d_%H%M%S) filename flicense_result_{timestamp}.png # 保存整个 figure try: self.fig.savefig(filename, dpi300, bbox_inchestight) messagebox.showinfo(成功, f结果已保存{filename}) except Exception as e: messagebox.showerror(错误, f保存失败{str(e)})注意bbox_inchestight自动裁切空白边距避免子图标题被截断dpi300保证打印清晰度文件名含时间戳防止覆盖。此功能让助教当场索要截图时你只需点一下而非手忙脚乱截图再拼图。4. 避坑指南课程设计中最常翻车的5个问题附现象、原因与血泪解决方案做这个课设三年带过 27 个学生小组90% 的答辩卡点都集中在这五个坑里。它们不是代码 bug而是对数字图像处理本质理解偏差导致的“玄学失败”。以下每条都来自真实翻车现场按“现象→原因→解决”结构给出可立即执行的方案。4.1 现象同一张图在自己电脑上能识别换到教室电脑就框不住车牌原因OpenCV 版本差异导致cv2.findContours返回值格式不同。OpenCV 3.x 返回(contours, hierarchy)而 OpenCV 4.x 返回(contours, hierarchy)或(img, contours, hierarchy)取决于调用方式。课程设计常用contours, _ cv2.findContours(...)在 OpenCV 4.x 下会报ValueError: not enough values to unpack。解决统一用兼容写法不依赖返回值个数# 错误写法版本敏感 contours, hierarchy cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 正确写法兼容 3.x 和 4.x contours cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) contours contours[0] if len(contours) 2 else contours[1] # 取 contours 列表血泪经验教室电脑常装 OpenCV 4.5而学生本地是 3.4。答辩前务必用pip show opencv-python查版本并在代码开头加版本检查提示。4.2 现象GUI 界面卡死无响应鼠标变成沙漏后一直转原因cv2.imshow与 tkinter 主循环冲突。很多教程混用cv2.imshow阻塞式和 tkinter导致事件循环被锁死。解决彻底弃用cv2.imshow所有图像显示走 matplotlib 子图。若需调试中间图用plt.figure(); plt.imshow(...); plt.show()单独弹窗但正式 GUI 中禁用。玄学提示tkinter 的root.update()不能替代canvas.draw()后者才是 matplotlib 刷新的唯一正确方式。4.3 现象校正后的车牌图是斜的甚至字符被拉伸变形原因cv2.minAreaRect返回的box四点顺序混乱直接传给cv2.getPerspectiveTransform会导致映射错乱。OpenCV 文档未明确四点顺序实测中boxPoints输出顺序随矩形朝向变化。解决必须用order_points函数强制按“左上→右上→右下→左下”排序代码见 2.5 节。可加校验# 在 perspective_transform 中添加 ordered order_points(box) # 校验左上点应 x,y 最小右下点应 x,y 最大 if not (ordered[0][0] ordered[1][0] and ordered[0][1] ordered[3][1]): print(警告四点顺序异常尝试重新排序) ordered order_points(box) # 再排一次4.4 现象PPT 里写的“采用 Otsu 阈值”但代码里写cv2.THRESH_BINARY原因混淆了阈值类型与阈值算法。cv2.THRESH_BINARY是二值化模式cv2.THRESH_OTSU是自动阈值算法二者需组合使用cv2.THRESH_BINARY cv2.THRESH_OTSU。单独cv2.THRESH_OTSU无效。解决严格按文档写# 正确Otsu 自动计算阈值 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 错误Otsu 标志位单独用无意义 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU) # 此行报错或返回全黑答辩话术助教问“Otsu 是什么”答“Otsu 是一种自动寻找最佳分割阈值的算法它最大化类间方差比固定阈值更能适应不同光照下的车牌我们在字符分割前应用它”。4.5 现象识别率忽高忽低同一参数对 A 图好、对 B 图差原因未做图像尺寸归一化。手机拍的图分辨率从 720p 到 4K 不等而cv2.Sobel的ksize、形态学核尺寸等参数是绝对像素值对高分辨率图过小对低分辨率图过大。解决在preprocess_image前统一缩放def resize_for_processing(img, max_dim1200): 将长边缩放到 max_dim保持宽高比 h, w img.shape[:2] scale max_dim / max(h, w) if scale 1.0: new_w int(w * scale) new_h int(h * scale) img cv2.resize(img, (new_w, new_h)) return img # 在 load_image 中调用 self.original_img resize_for_processing(cv2.imread(filepath))参数建议max本文还有配套的精品资源点击获取