资讯详情

资讯详情

别急着上深度学习:Python+OpenCV传统流水线跑通车牌识别

简介围绕Python与OpenCV实现车牌自动识别的实战资料包面向想入门计算机视觉、做课程设计或毕业设计的开发者聚焦图像预处理、车牌定位、字符分割到模板匹配识别的完整链路帮助读者把零散算法串成可运行项目。压缩包共2000个文件、约12.37MB其中1999张jpg涵盖车辆原图与分割后的字符匹配模板含鄂、豫等车牌字母样本1个py文件为识别主程序源码数据和代码配套可直接复现实验并调整参数。目前已有3109人学习下载。读者可据此理解灰度化、二值化、滤波去噪、边缘检测、轮廓查找与形态学操作的组合方式掌握垂直投影、滑动窗口等分割思路并借助模板库完成字符比对对智能交通、停车场管理等场景的车辆追踪与违章查询也有参考价值。1. 别急着上深度学习PythonOpenCV 这套传统流水线先把车牌识别跑通前几天有人丢给我一套车牌识别的源码说是 PythonOpenCV 写的带测试图片和一套字符匹配模板问能不能直接改成小工具用。我原本以为又是调个现成库的两百行 Demo翻进去发现它是把整条链路老老实实手写了一遍Sobel 边缘检测、形态学闭运算、轮廓筛选定位、垂直投影切字符、最后拿字符模板逐个比对打分每一行参数都摊在明面上看得懂也改得动。反直觉的地方在于很多人默认车牌识别必须上 YOLO 加 CRNN可换成一张正脸、光照正常的抓拍图这套纯传统方法出结果并不慢而且它出错时你知道该去调哪个数不是对着一个黑匣子干瞪眼。适合想搞懂图像处理链路、手里正好有一份课程设计或内部小工具要交差的开发者。2. 项目骨架与运行链路从 main.py 到字符模板五个阶段怎么串拿到一个源码包我习惯先不跑先把目录捋一遍。因为这类项目最容易出的问题不是代码写错而是你把测试图丢错目录或者模板图的命名规则跟识别代码对不上跑起来一路 None排查半天怀疑人生。2.1 目录结构与模块职责这套东西的分层很朴素一共五个 py 文件按「定位 → 分割 → 识别」切开另外单独抽了个 config 放参数这个做法值得保留路径职责改动频率main.py入口串起定位、分割、识别三步低config.py所有阈值、核尺寸、比例区间高调参基本都在这plate_locate.py灰度化、边缘检测、形态学、轮廓筛选中char_segment.py车牌矫正、二值化、垂直投影切字符中char_recognize.py模板加载、归一化、matchTemplate 打分中img/测试图片蓝牌、黄牌、斜拍各放几张随意chars/字符模板一个字符一张小图文件名即标签增删时注意chars/这个目录是整套源码的核心资产。命名一般就是0.png到9.png、A.png到Z.png省级简称单独放中文文件名的图。识别代码读模板时直接用os.path.splitext(filename)[0]当标签所以文件名写错一个字那个字符就永远匹配不上而且不会有任何报错只会在结果里安静地错一位。注意模板图必须是二值图或灰度图尺寸要和config.py里的CHAR_SIZE完全一致。我见过有人在里面混进几张彩色截图matchTemplate因为通道数不匹配直接抛异常报错信息还指向 resize 那一行很容易带偏。2.2 主流程一行调用背后的三段式main.py写得越薄越好所有脏活都推到子模块里# main.py import cv2 from plate_locate import locate_plate from char_segment import segment_chars from char_recognize import recognize_chars def run(image_path, save_debugTrue): img cv2.imread(image_path) if img is None: # imread 读不到文件不报错只返回 None这里必须自己拦 raise FileNotFoundError(f图片读不到检查路径: {image_path}) # 第一步定位。返回裁好的车牌小图以及若干中间图用于排查 plate_img, debug_imgs locate_plate(img, debugsave_debug) if plate_img is None: print(没定位到车牌先去看 debug 目录里的闭运算结果图) return None # 第二步分割。返回从左到右排好的字符小图列表 char_imgs segment_chars(plate_img) if len(char_imgs) ! 7: # 普通蓝牌是 7 位数量不对说明投影切歪了但先别中断让后面打分兜底 print(f切出 {len(char_imgs)} 个字符预期 7 个) # 第三步识别。逐个字符和模板比对取分数最高的标签 text, scores recognize_chars(char_imgs) avg sum(scores) / max(len(scores), 1) print(f识别结果: {text} 平均置信度: {avg:.3f}) return text if __name__ __main__: run(img/test1.jpg)这段代码有三处设计意图值得说。第一locate_plate返回None时不抛异常而是打印提示因为定位失败在实拍图里太常见了抛异常会让批量跑图直接断掉。第二字符数量不等于 7 时只警告不中断后面靠模板匹配的分数判断可信度——切多了通常会有几个字符得低分一眼就能看出来。第三debug开关把中间图落盘这是这类项目里最有用的一个习惯定位不准的时候你没法靠 print 看出问题。参数上image_path建议先用项目自带的img/图片跑通再换自己的图。换成手机拍的图时分辨率往往在 3000px 以上Sobel 和形态学的核尺寸是按小图定的直接跑容易把整块车身连成一个巨型连通域这个在第 5 章会细说。2.3 环境准备与第一次运行依赖只有两个别装一堆用不上的# 建议开虚拟环境OpenCV 的版本差异在这类项目里非常致命 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install opencv-python numpy -i https://pypi.tuna.tsinghua.edu.cn/simple装完先确认版本再跑图python -c import cv2; print(cv2.__version__) # 4.x 和 3.x 的 findContours 返回值不一样 python main.pycv2.findContours在 OpenCV 3 里返回三个值(image, contours, hierarchy)4.x 只返回两个(contours, hierarchy)。这份源码是照 4.x 写的如果你环境里是 3.x会直接报ValueError: too many values to unpack。解决办法要么升 OpenCV要么把解包改成_, contours, _ cv2.findContours(...)别去改后面逻辑。还有一个经典坑是中文路径。cv2.imread对非 ASCII 路径的支持一直不稳定在部分平台上会静默返回None。图片目录如果带中文用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)替代两行代码的事能省掉半小时的自我怀疑。跑通之后你应该能在终端看到类似识别结果: 京A12345的输出。看不到也别急着改代码先确认img/里那张图是不是正脸、车牌在画面里占比够不够大——这套方法对「车牌占画面高度不到 5%」的远景图基本无能为力这是原理决定的不是参数没调好。3. 车牌定位灰度、Sobel 与形态学闭运算的参数调整定位是整个流程里最脆的一环也是调参花时间最多的地方。原理不复杂车牌区域有大量密集的竖直边缘字符笔画和边框把竖直方向的梯度强化出来再用闭运算把散落的边缘连成一个矩形块最后按宽高比和面积把矩形块筛出来。3.1 预处理四步灰度、高斯、Sobel、OTSU 二值化# plate_locate.py import cv2 import numpy as np from config import * def preprocess(img): # 1. 灰度化颜色信息在这里没用纹理才是车牌的特征 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 高斯模糊5x5 是经验值核越大去噪越狠但也越容易把细笔画糊掉 blur cv2.GaussianBlur(gray, (5, 5), 0) # 3. Sobel 只求 x 方向梯度。dx1, dy0 表示突出竖直边缘 # ksize3 是最小可用奇数核车牌字符间距小核大了会粘连 sobel cv2.Sobel(blur, cv2.CV_8U, 1, 0, ksizeSOBEL_KSIZE) # 4. OTSU 自动找阈值。对光照不均的图比固定阈值 127 稳得多 _, binary cv2.threshold(sobel, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) return gray, binary逐段说。灰度化不可省因为Sobel只能吃单通道彩色图上做梯度会把颜色跳变也算成边缘反而添乱。高斯模糊的(5,5)是一个折中原图分辨率在 1000px 以内时够用如果换 3000px 的手机图这个核相对太小、去噪等于没做可以按比例放大到(7,7)或者先把图缩到 1000px 宽再处理。Sobel的ddepth参数写成cv2.CV_8U是很多人会翻车的地方。梯度值本身可能为负用 8 位无符号会直接把负值截断成 0导致一半的边缘消失。稳妥写法是先用cv2.CV_16S求梯度再cv2.convertScaleAbs取绝对值转回 8 位。这份源码为了简洁用了CV_8U在多数正脸图上够用但遇到逆光或者深色车身就容易丢边缘想稳就换成下面这两行sobel_x cv2.Sobel(blur, cv2.CV_16S, 1, 0, ksize3) sobel cv2.convertScaleAbs(sobel_x) # 取绝对值并归一化到 0-255OTSU 的优势在于不用手填阈值代价是它假设图像直方图是双峰的。一张整块车身占满画面的图双峰可能落在车身和背景上车牌那块区域就被二值化成全黑或全白直接失联。3.2 形态学闭运算把散落的边缘连成一个块边缘检测出来的结果是一堆断断续续的白线直接找轮廓会得到几百个碎片。闭运算先膨胀后腐蚀的作用是把挨得近的白点连起来同时大致保持原来的形状def morph(binary): # 矩形核宽度远大于高度因为车牌是横长条字符之间是横向间隙 kernel cv2.getStructuringElement(cv2.MORPH_RECT, MORPH_KERNEL) # 默认 (17, 5) # 闭运算 先膨胀后腐蚀填补字符之间的横向空隙 closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 腐蚀一轮去掉小噪点再膨胀两轮把车牌主体补回来 closed cv2.erode(closed, kernel, iterations1) closed cv2.dilate(closed, kernel, iterations2) return closed(17, 5)这个核的形状是整个定位环节里最灵的一个参数值得单独理解。宽度 17 是因为字符之间的间隙在预处理后的图里通常是 8 到 15 像素核得比间隙宽才能连上高度 5 是因为字符的上下边缘本身就有梯度纵向不需要连太多核高了反而会把车牌和上下保险杠粘成一片。如果你拿到的图分辨率翻倍这个核也得跟着翻否则字符间隙比核还宽闭运算做完还是一片散沙findContours返回几百个候选框最后靠打分选出来的大概率是保险杠。我的做法是在config.py里不写死像素值改成按图宽的比例算# 核宽取图宽的 1.8%核高取核宽的 0.3 倍换分辨率不用重调 kw max(9, int(img.shape[1] * 0.018)) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (kw, max(3, kw // 3)))后面那次「腐蚀一轮再膨胀两轮」也有讲究。腐蚀是为了干掉零零散散的小白块比如车标、螺丝、反光点膨胀两轮是为了把闭运算后仍然偏小的车牌块补回到接近真实尺寸。膨胀多了会把旁边的东西吸进来做出来的框比真实车牌大一圈裁出来的图里带一圈车身分割阶段投影就会多出两段噪声。3.3 轮廓筛选宽高比、面积、角度三把尺子形态学处理完之后图上剩下十几个到几十个白色连通块接下来就是按几何先验筛def find_candidates(closed): # OpenCV 4.x 返回两个值3.x 是三个这里按 4.x 写 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if h 0: continue ratio w / float(h) area w * h # 国内蓝牌 440x140宽高比约 3.14留出透视变形余量取 2.2 到 4.2 if PLATE_RATIO_MIN ratio PLATE_RATIO_MAX and \ PLATE_AREA_MIN area PLATE_AREA_MAX: candidates.append((x, y, w, h)) # 打分宽高比越接近 3.14 排得越靠前这是最省事的排序依据 candidates.sort(keylambda b: abs(b[2] / b[3] - 3.14)) return candidates三个筛选条件的取值逻辑宽高比2.2 ~ 4.2覆盖了正拍到轻微倾斜的情况超过 4.2 基本是车身腰线或者窗户低于 2.2 一般是竖直部件面积上下限要结合你的实际图片分辨率定参数不合适时最典型的症状是候选列表为空代码走进if not candidates分支直接返回None宽高比排序则是兜底多个候选都能过筛时优先信最像车牌的那个。RETR_EXTERNAL这个模式也别随手改。它只取最外层轮廓能避免车牌内部字符各自成框导致列表爆炸。改成RETR_TREE会得到带层级的完整轮廓树对定位这步来说是纯粹的负担。提示定位这一步调不通的时候别在代码里加 print直接把closed、binary两张中间图cv2.imwrite到本地打开看。九成的定位问题看一眼闭运算结果图就知道是核小了还是阈值歪了。4. 字符分割与模板匹配垂直投影切分与归一化比对定位拿到的是整块车牌接下来要把它拆成 7 个单字符再逐个认。这一步的精度直接决定最终结果因为分割错一位后面匹配得再准也是白搭。4.1 车牌矫正与二值化先统一成黑底白字不同底色车牌的二值化方向是反的。蓝底白字要反相二值化THRESH_BINARY_INV黄底黑字则要正相。项目里如果只有蓝牌样本这块通常写死成反相换黄牌立刻全错def to_binary(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 蓝底白字白字在灰度图里更亮INV 之后变成黑底白字方便投影统计 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 小核开运算去掉边框残留和铆钉 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return binary开运算先腐蚀后膨胀在这里是清理细节的车牌四周常见的金属边框、铆钉、反光条在二值图上会变成贴着边缘的白色条带投影的时候会被误判成字符。3x3 的核足够削掉一两个像素宽的细线又不会伤到字符笔画。要判断底色最稳的是在 HSV 空间看色相分布蓝牌的 H 值集中在 100 到 124 之间黄牌在 20 到 34 之间。判断完再决定用THRESH_BINARY还是THRESH_BINARY_INV多写十行代码能省掉后面一堆莫名其妙的识别错误。4.2 垂直投影切字符按列统计白点一块车牌二值化之后字符是白色的竖条字符之间是黑色的间隙。把每一列的白点数量加起来画成一条曲线字符位置是波峰间隙位置接近零切开就是字符def segment_chars(plate_img): binary to_binary(plate_img) h, w binary.shape # 每一列的白点个数得到长度为 w 的一维数组 col_white (binary 0).sum(axis0) # 阈值取高度的 5%低于这个认为进入了字符间隙 thresh h * 0.05 segments, in_char, start [], False, 0 for i, v in enumerate(col_white): if v thresh and not in_char: in_char, start True, i # 进入一个字符 elif v thresh and in_char: in_char False if i - start 5: # 宽度小于 5px 的当噪点丢掉 segments.append((start, i)) # 去掉车牌两边边框和中间那个圆点分隔符 # 做法按宽度从大到小排取前 7 个再按原位置排回来 segments sorted(segments, keylambda s: s[1] - s[0], reverseTrue)[:7] segments.sort(keylambda s: s[0]) char_imgs [] for s, e in segments: # 上下各留 2px 余量避免切掉字符顶部 char_imgs.append(binary[2:h - 2, s:e]) return char_imgsthresh h * 0.05里的 0.05 是这套算法的调节参数。调小了字符间隙里零星的白点也会被算进去相邻字符粘成一段最后只切出 4 个块调大了笔画细的字符比如数字 1整列白点不够阈值直接被跳过。实拍图里因为光照不均同一个车牌的左右两端亮度可能差很多固定比例阈值在暗的那一侧容易失效可以考虑对投影数组做一次归一化或者按左右两半分别算阈值。「取宽度前 7 名」这个兜底策略有其局限。如果省份简称那个中文字符笔画密集、宽度偏大它会挤掉真正该入选的字符如果车牌右端带一圈装饰框那个框的宽度往往排第一。更稳的做法是先用投影找出所有段再看段与段之间的间距是否接近等距用等距这条约束把 7 个字符挑出来。这份源码用的是简单版本够用但不鲁棒二次开发时这是第一个该动手改的地方。4.3 模板匹配识别归一化是前提尺寸必须严格对齐cv2.matchTemplate要求模板和待匹配图同尺寸模板更小时会在图上滑窗所以第一步必须把所有字符图缩放到和模板一致的尺寸def load_templates(template_dir): templates {} for fn in os.listdir(template_dir): if not fn.lower().endswith((.png, .jpg)): continue label os.path.splitext(fn)[0] # 文件名即标签 img cv2.imread(os.path.join(template_dir, fn), 0) # 模板统一缩放到 CHAR_SIZE这一步做了才能和待识别图对齐 img cv2.resize(img, CHAR_SIZE) _, img cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) templates[label] img return templates def recognize_chars(char_imgs, template_dirchars): templates load_templates(template_dir) text, scores [], [] for ch in char_imgs: # 待识别字符也缩放到同样尺寸否则 matchTemplate 的分数没有可比性 ch_norm cv2.resize(ch, CHAR_SIZE) best_label, best_score , -1.0 for label, tpl in templates.items(): # TM_CCOEFF_NORMED 输出 -1 到 1越接近 1 越相似 score cv2.matchTemplate(ch_norm, tpl, cv2.TM_CCOEFF_NORMED)[0][0] if score best_score: best_label, best_score label, score text.append(best_label) scores.append(best_score) return .join(text), scoresCHAR_SIZE一般设成(20, 40)宽 20 高 40保持 1:2 的比例接近国内车牌字符的实际宽高比。这个值一旦改了chars/里所有模板都要跟着重新生成否则两边尺寸不一致matchTemplate会因为模板比图大而抛出断言错误。TM_CCOEFF_NORMED的选择理由它对整体亮度偏移不敏感字符图偏亮偏暗都能给出可比的分数TM_SQDIFF是差平方值越小越像很容易在写代码时忘记取反导致识别结果永远是分数最高的那个错误字符。分数本身要利用起来。正常情况下正确字符的得分在 0.6 以上错误匹配往往低于 0.5。我一般会在返回结果时把低于 0.55 的字符标出来# 分数低于阈值说明这块可能是噪声或者模板缺失输出时加个标记方便人工复核 low_conf [i for i, s in enumerate(scores) if s 0.55] if low_conf: print(f第 {low_conf} 位置信度偏低建议人工核对)这一步在批量处理里价值很大——你不需要它百分百准只需要它告诉你哪几张需要人看工作量就从「全看」降到「看几十分之一」。5. 避坑与排查颜色、光照、模板尺寸这三类翻车现场这套流程我在不同图上反复跑过失败案例高度集中在几个固定位置。下面按「现象 → 原因 → 解决」列出来都是只要看一眼中间图就能定位的问题。5.1 定位阶段边缘检测与形态学的问题现象一候选框列表为空函数直接返回None。原因是形态学核的尺寸和图片分辨率不匹配。手机拍的图宽度 3000px 时字符间隙可能有 40px 宽而核宽还是写死的 17闭运算做完字符边缘依然是散的没有任何一个连通块的宽高比落进2.2 ~ 4.2。解决方式是按图宽动态算核尺寸或者干脆在预处理开头把长边缩到 1000px 以内让后面的固定参数重新生效。我一般选后者一行 resize 换回全套参数可用性价比最高。现象二定位到的框比车牌大一圈裁出来的图里带车身。原因是膨胀次数太多。闭运算之后那次dilate(closed, kernel, iterations2)用的是和闭运算同一个核宽 17 的核膨胀两轮横向能向外扩 34px正好把车牌两侧的车身吸进来。解决方式是把膨胀单独用一个更小的核比如(5, 5)或者把 iterations 降到 1然后靠后面轮廓的boundingRect自然贴合。判断标准很简单裁出来的图上下边缘应该紧贴车牌边框多出一条车身就是膨胀过头了。现象三同一张图白天拍能定位傍晚拍就定位到车灯上。原因是 OTSU 阈值在低照度图上发生了漂移。傍晚照片整体偏暗直方图双峰的位置全变了OTSU 算出来的阈值把车灯的高光区域判成了前景那一块又亮又方宽高比和面积都可能落进筛选区间。解决方式是在 OTSU 之前先做一次直方图均衡化cv2.equalizeHist把灰度分布拉开或者干脆放弃全局阈值改用cv2.adaptiveThreshold做局部自适应。代价是自适应阈值会产生更多噪点后面的开运算核也得跟着调大。5.2 分割与识别阶段投影断裂与模板不匹配现象四切出 8 个或 9 个字符块结果整体错位。原因是车牌中间那个圆点分隔符或者固定螺丝被当成了字符。它宽度小但白点密度够在投影上形成一个独立波峰。segment_chars里「按宽度取前 7」的逻辑会把它和真正的字符一起排序宽的那个圆点挤掉一个窄字符通常是数字 1最后位置全乱。解决方式有两条一是投影前先通过连通域面积过滤掉小连通块面积小于字符平均面积 20% 的直接抹掉二是在排序前按位置做一次等距校验7 个字符的中心点间距应该高度接近间距异常的那个段就是分隔符。现象五所有字符都识别成同一个字母置信度还都在 0.7 以上。这个最迷惑人。根本原因是模板图本身有问题chars/里混进了一张全白或接近全白的模板图可能是某次截图的残留TM_CCOEFF_NORMED对空白图会给出异常高的相关性。解决方式是加载模板时加一道检查统计每张模板的前景像素占比低于 5% 或高于 95% 的直接跳过并打印警告# 加载模板时做一次健全性检查空白图和满屏图都会让匹配结果失真 ratio (img 0).sum() / img.size if ratio 0.05 or ratio 0.95: print(f模板 {fn} 前景占比 {ratio:.2f}疑似异常跳过) continue同一类问题还有尺寸不一致模板是 20x40但某张你自己加进去的模板是 30x60resize那步会把它强行拉成 20x40字形被压扁识别自然全错。加一行尺寸断言就能拦住。6. 二次开发把模板匹配抽成可替换接口以及我怎么验证改动模板匹配的上限很明显字符字体一变、车牌有污损或者有明显透视分数就掉得厉害。想往上走最省事的路径不是重写整个项目而是把识别这一步抽成接口让模板匹配和别的识别器能互换。先定义一个极薄的抽象层# recognizer.py class BaseRecognizer: def predict(self, char_imgs): 输入字符小图列表返回 (字符串, 每位的置信度列表) raise NotImplementedError class TemplateRecognizer(BaseRecognizer): def __init__(self, template_dirchars): self.templates load_templates(template_dir) def predict(self, char_imgs): return recognize_chars(char_imgs, self.templates)有了这层main.py里只依赖BaseRecognizer换成调用外部 OCR 或者自己训的小分类器都不需要动定位和分割代码。字符分类器其实很好训——65 个类别10 数字 24 字母 31 省份简称每类几百张样本一个小 CNN 在 CPU 上推理几毫秒就能出结果比模板匹配稳得多class CNNRecognizer(BaseRecognizer): def __init__(self, model_path): self.net cv2.dnn.readNetFromONNX(model_path) # 不额外引框架OpenCV 自带 dnn 就够 def predict(self, char_imgs): out_text, out_scores [], [] for ch in char_imgs: blob cv2.dnn.blobFromImage(cv2.resize(ch, (20, 40)), 1/255.0) self.net.setInput(blob) prob self.net.forward()[0] # softmax 输出长度 65 idx int(prob.argmax()) out_text.append(IDX2CHAR[idx]) out_scores.append(float(prob[idx])) return .join(out_text), out_scores两种方案怎么选我按场景分维度模板匹配小 CNNONNX依赖只要 OpenCV只要 OpenCVdnn 模块训练成本零加图即加类每类几百张样本半天能跑完抗污损、抗形变弱字体一变就掉分明显更好可解释性高分数一眼看穿低只能看 softmax 概率适合场景固定摄像头、固定字体多来源图片、光照差异大改造之后怎么验证改动没把原来能跑的图跑坏我的习惯是留一组回归图。把 20 张不同光照、不同角度的测试图放进img/regression/文件名里直接写正确答案比如京A12345_01.jpg再写个二十行的脚本批量跑、比对文件名前缀、输出准确率# 批量回归文件名前半段就是标答跑完直接看准确率变化 ok 0 for fn in os.listdir(img/regression): truth fn.split(_)[0] pred run(os.path.join(img/regression, fn), save_debugFalse) if pred truth: ok 1 print(f准确率 {ok}/{len(os.listdir(img/regression))})有了这个基线你换识别器、调投影阈值、加直方图均衡每一步都能立刻看到是涨了还是跌了而不是凭感觉觉得「好像变准了」。早些年我改这类项目从来不留回归集参数调完在自己手上那三张图上跑通了就提交换一批图全崩回头连是哪次改动引入的都查不出来那种没有后悔药的感觉一次就够了。从那以后我每次动定位或分割参数都强制先把中间图落盘看一眼再跑一遍回归集对比数字两次都对得上才继续往下写。希望这套流程帮到你——源码包里的东西本身就是个不错的起点剩下的就看你把它往哪个方向推了。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →