ARTICLE DETAIL

资讯详情

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

轮胎字符识别实战:高度图预处理与推理模型全链路解析

轮胎字符识别实战:高度图预处理与推理模型全链路解析 简介这份资源面向计算机、电子信息工程、数学等专业的大学生服务于课程设计、期末大作业与毕业设计场景核心是2023年机器学习轮胎字符识别任务的完整实现方案。包内共157个文件以63个png与27个jpg图像样本、19个Python脚本、9个pyc编译文件为主另含txt说明、Paddle推理模型文件pdmodel、pdiparams、pdparams及字体、配置等辅助资源压缩包约333.11MB。处理流程覆盖原始数据提取高度数据、高度图转化、裁切、图像修复、展平、规格化、去噪、直方图均衡化与直方图裁切等环节代码参数化设计、注释清晰并附运行结果便于对照调试。目前已有268人学习下载。读者可据此获得一套可直接复现的字符识别工程范例理解从数据预处理到模型推理的完整链路并借助现成脚本与文档快速迁移到同类识别任务中。1. 轮胎字符识别作业包从高度图到识别结果这份源码到底能不能跑轮胎胎侧的字符识别是工业质检里一个典型的“小目标 低对比度 曲面畸变”场景。字符直接印在黑色橡胶上光照稍偏就糊成一片用普通 OCR 直接怼识别率基本靠玄学。这份 2023 机器学习作业包走的是一条更工程化的路子先把轮胎表面高度数据提取出来转成高度图再做裁切、修复、展平、规格化、去噪、直方图均衡化和裁切最后送进识别模型输出 Result_5.jpg、Result_6.jpg、Result_12.jpg 这类带框的结果图。它适合计算机、电子信息、数学方向的同学做课程设计或期末大作业也适合想摸一遍“图像预处理 字符识别”完整链路的从业者。包里带了 inference.pdiparams 系列推理模型文件和 Cache.cach 缓存说明作者是跑通了推理才上传的不是只丢一堆训练脚本让你自己猜。2. 高度图预处理链路八个步骤为什么一个都不能省2.1 从原始高度数据到高度图先搞清楚数据长什么样轮胎字符识别的第一步不是识别是把“高度”变成“图”。原始数据提取出来的是离散的高度值常见做法是按扫描行或扫描列排成一个二维矩阵矩阵里每个元素代表该点相对基准面的高度。这个矩阵直接看就是一堆数字必须映射成灰度图才能做后续图像处理。映射逻辑很简单高度值归一化到 0 到 255低处暗、高处亮字符凸起或凹陷的区域就会在高度图上形成明暗对比。我一般会先确认三件事高度数据的行列数是否和扫描物理尺寸对得上、有没有无效值比如传感器丢点产生的 NaN 或极大值、高度范围是否被异常点拉爆。如果直接归一化而不处理异常值整张高度图会灰成一片字符区域根本看不出来。下面这段是常见的高度数据转高度图的写法参数按你实际的数据范围改。import numpy as np import cv2 # raw_height: 原始高度矩阵shape 为 (H, W) # invalid_mask: 无效点掩码True 表示该点无效 raw_height np.load(height_data.npy) invalid_mask np.isnan(raw_height) | (np.abs(raw_height) 1e4) # 用有效点的分位数做归一化避免异常值拉爆对比度 valid raw_height[~invalid_mask] h_min, h_max np.percentile(valid, 1), np.percentile(valid, 99) norm np.clip((raw_height - h_min) / (h_max - h_min 1e-6), 0, 1) height_img (norm * 255).astype(np.uint8) # 无效点填成中灰后续修复步骤会处理 height_img[invalid_mask] 128 cv2.imwrite(height_map.png, height_img)这段代码的关键在归一化策略。用 1% 和 99% 分位数而不是最小最大值是为了让个别飞点不参与拉伸。invalid_mask把 NaN 和超过 1e4 的高度标出来填成 128 中灰避免后续修复时被当成真实边缘。如果你拿到的数据没有无效点这一步可以简化但分位数归一化建议保留。2.2 裁切、修复、展平把曲面上的字符“掰直”高度图出来之后整张图里字符只占一小块周围全是背景。裁切就是先把字符所在的感兴趣区域抠出来减少后续计算量也避免背景噪声干扰。裁切边界一般按高度图的梯度能量来定字符区域梯度密集背景区域平缓。常见做法是算 Sobel 幅值做列方向和行方向的投影取投影超过阈值的连续区间作为裁切框。修复针对的是高度图里的孔洞和断裂。轮胎表面有花纹、有磨损高度数据在某些位置会缺失字符笔画可能断成几截。修复的常见做法是形态学闭运算加中值滤波先闭运算把细小断裂连上再用中值滤波去掉孤立的噪点。注意闭运算的核不能太大否则相邻字符会粘在一起后面展平时会出问题。展平是这条链路里最容易被低估的一步。轮胎胎侧是曲面高度图上的字符是弯的直接送识别模型字符形状和训练集里的直立字符对不上识别率会掉得很厉害。展平的思路是先估计曲面的弯曲方向常见做法是对字符区域做多项式拟合得到一条基准曲线然后按这条曲线做重映射把弯曲的字符拉直。参数上多项式阶数一般取 2 到 3阶数太高会过拟合噪声阶数太低拉不直。# 裁切按梯度能量投影取字符区域 gray cv2.imread(height_map.png, 0) sobel_x cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize3) col_energy np.sum(np.abs(sobel_x), axis0) thresh col_energy.mean() col_energy.std() cols np.where(col_energy thresh)[0] x1, x2 cols.min(), cols.max() roi gray[:, max(0, x1-10):min(gray.shape[1], x210)] # 修复闭运算连断裂中值滤波去孤立点 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) closed cv2.morphologyEx(roi, cv2.MORPH_CLOSE, kernel) repaired cv2.medianBlur(closed, 3) # 展平按行拟合基准曲线后重映射 h, w repaired.shape ys, xs np.where(repaired 0) coeff np.polyfit(xs, ys, 2) # 二次拟合弯曲趋势 curve np.polyval(coeff, np.arange(w)) map_x np.tile(np.arange(w), (h, 1)).astype(np.float32) map_y (np.arange(h)[:, None] - curve[None, :] h/2).astype(np.float32) flattened cv2.remap(repaired, map_x, map_y, cv2.INTER_LINEAR)裁切部分用列方向梯度能量投影阈值取均值和标准差之和这是比较稳的经验值。修复的核用 3×3 椭圆闭运算只连小断裂中值滤波窗口也是 3再大就会把细笔画抹掉。展平用二次多项式拟合弯曲趋势map_y把每一列按曲线偏移重映射相当于把弯的字符“掰直”。如果你的轮胎曲面弯曲方向是竖直的把 x 和 y 对调即可。2.3 规格化、去噪、直方图均衡化与裁切让字符对比度拉满展平之后字符虽然直了但对比度可能还不够。规格化是把图像尺寸统一到模型输入尺寸常见做法是等比缩放加 padding不要直接拉伸拉伸会改变字符宽高比识别模型对宽高比敏感。去噪在高度图上主要是去掉高频的传感器噪声高斯滤波是最常用的sigma 取 0.8 到 1.2 之间比较合适太大字符边缘会糊。直方图均衡化是这条链路里提升对比度最明显的一步。高度图经过归一化后字符区域和背景的灰度差可能只有几十个灰度级均衡化能把动态范围拉开。但直接对全图做均衡化背景噪声也会被放大所以常见做法是先做直方图裁切把极端灰度级截掉再做均衡化。裁切比例一般取 0.5% 到 1%把最暗和最亮的少量像素排除在外。# 规格化等比缩放到模型输入尺寸短边对齐后 padding target (32, 100) # 高, 宽 scale min(target[0]/flattened.shape[0], target[1]/flattened.shape[1]) new_size (int(flattened.shape[1]*scale), int(flattened.shape[0]*scale)) resized cv2.resize(flattened, new_size, interpolationcv2.INTER_AREA) canvas np.full(target, 128, dtypenp.uint8) y_off (target[0] - new_size[1]) // 2 x_off (target[1] - new_size[0]) // 2 canvas[y_off:y_offnew_size[1], x_off:x_offnew_size[0]] resized # 去噪高斯滤波sigma 控制平滑程度 denoised cv2.GaussianBlur(canvas, (3, 3), 1.0) # 直方图裁切 均衡化 clip_limit 0.01 # 裁掉 1% 的极端像素 hist cv2.calcHist([denoised], [0], None, [256], [0, 256]).flatten() total hist.sum() low np.searchsorted(np.cumsum(hist), total * clip_limit) high np.searchsorted(np.cumsum(hist), total * (1 - clip_limit)) clipped np.clip(denoised, low, high) equalized cv2.equalizeHist(((clipped - low) / (high - low 1e-6) * 255).astype(np.uint8))规格化用等比缩放加 padding短边对齐后居中填 128这样不会改变字符形状。去噪的高斯 sigma 取 1.0是高度图比较通用的值。直方图裁切先算累积直方图找到 1% 和 99% 对应的灰度级截断后再均衡化这样背景噪声不会被过度放大。最后再做一次裁切把均衡化后仍然过暗或过亮的边缘去掉送进识别模型的就是一张对比度拉满、尺寸统一的字符图。3. 推理模型与结果输出inference.pdiparams 怎么用、结果图怎么看3.1 推理文件结构Cache.cach 和 pdiparams 各管什么包里出现的Cache.cach、inference.pdiparams、inference.pdiparams.info是典型的推理部署文件组合。inference.pdiparams存的是模型权重inference.pdiparams.info存的是权重的元信息比如每个参数块的名称、形状、偏移量。Cache.cach一般是推理框架的缓存文件用来加速模型加载删掉通常不影响推理但首次加载会慢一点。Result_5.jpg、Result_6.jpg、Result_12.jpg 是推理后的可视化结果图上应该画了字符检测框和识别结果。用这套文件做推理常见做法是加载模型后把预处理好的字符图送进去得到字符序列或分类结果。如果你拿到的包里有推理脚本直接跑就行如果没有需要按推理框架的 API 写加载逻辑。注意inference.pdiparams.info不要单独删它和inference.pdiparams是配对的缺了元信息权重加载会报形状不匹配。3.2 跑通推理从单张图到批量结果推理脚本的核心就三步加载模型、预处理、后处理。预处理就是第 2 章那条链路后处理是把模型输出转成字符。下面是一个常见的推理调用框架参数按你实际用的框架改。import cv2 import numpy as np # 加载推理模型以常见推理 API 为例按实际框架替换 # model load_model(inference.pdiparams, inference.pdiparams.info) def preprocess(img_path): img cv2.imread(img_path, 0) # 这里接第 2 章的裁切、修复、展平、规格化、去噪、均衡化 # 返回 shape 为 (1, 32, 100) 的 float32 张量 return processed[None, ...].astype(np.float32) / 255.0 def postprocess(logits, charset): # logits: (T, C) 或 (C,)按 CTC 或分类解码 indices np.argmax(logits, axis-1) text .join(charset[i] for i in indices if i ! 0) return text charset 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ for name in [Result_5.jpg, Result_6.jpg, Result_12.jpg]: tensor preprocess(name) # logits model.run(tensor) # print(name, postprocess(logits, charset))预处理返回的张量形状要和模型输入对齐常见字符识别模型输入是 32×100 或 32×320通道为 1。后处理如果是 CTC 解码注意 blank 标签的索引通常是 0解码时要跳过。批量跑的时候建议先把所有图预处理完再统一推理比一张一张跑快很多。结果图 Result_5.jpg 这类文件重点看框有没有把字符框全、识别结果和图上字符是否对得上如果框偏了回去检查裁切和展平的参数。3.3 参数化编程怎么改三个最常调的参数这份代码的特点是参数化编程参数改起来方便。最常调的三个参数是裁切阈值、展平多项式阶数、直方图裁切比例。裁切阈值调大裁切框变小可能漏掉字符边缘调小裁切框变大背景噪声进来。展平阶数调高弯曲拉得更狠但容易过拟合噪声调低拉不直。直方图裁切比例调大对比度更强但字符笔画可能被截断调小对比度提升不明显。我一般会先用默认参数跑一遍看结果图里哪些字符识别错了再针对性调。如果错误集中在边缘字符优先调裁切阈值如果错误集中在弯曲严重的区域优先调展平阶数如果整体对比度不够优先调直方图裁切比例。每次只调一个参数调完看结果图对比不要一次改好几个否则出了问题不知道是哪个参数导致的。4. 避坑与排查这份作业包跑不起来时先看这几条4.1 现象推理时报权重形状不匹配原因inference.pdiparams和inference.pdiparams.info版本对不上或者推理框架版本和模型导出时的版本不一致。常见情况是只拷贝了 pdiparams 文件info 文件丢了或者 info 文件被其他模型的覆盖了。解决确认两个文件是同一套不要混用不同模型的 info。如果框架版本不一致看推理框架的版本兼容说明常见做法是降级或升级框架到模型导出时的版本。实在不行用 Netron 打开 pdiparams 看权重形状和模型定义对一下。4.2 现象结果图里字符框偏移或框不全原因裁切阈值设得不对或者展平后的重映射把字符位置带偏了。高度图里字符区域梯度能量分布不均匀时固定阈值容易切偏。解决把裁切阈值改成自适应按梯度能量的分位数来定比如取 85% 分位数作为阈值。展平部分检查多项式拟合的曲线是否合理可以把拟合曲线画在图上目视确认。如果曲线明显偏离字符中心线降低多项式阶数。4.3 现象识别结果全是同一个字符或空原因预处理后的图像对比度太低模型输入几乎是一张灰图或者后处理解码时 blank 标签索引搞错了把所有输出都跳过了。解决先把预处理后的图存下来看一眼确认字符是否清晰可辨。如果图是灰的检查直方图均衡化那一步裁切比例是不是设得太小。后处理部分打印原始 logits 的 argmax 结果看是不是全 0如果是检查 blank 索引和 charset 的对应关系。4.4 现象Cache.cach 加载报错或推理速度没变化原因Cache.cach 是框架的缓存文件和硬件、框架版本绑定。换机器或换框架版本后缓存失效加载会报错或直接忽略。解决直接删掉 Cache.cach让框架重新生成。删掉不影响推理结果只是首次加载慢一点。如果删掉后还报错说明缓存文件不是问题根源回去检查模型文件本身。4.5 现象批量跑结果图时中间某张报错中断原因某张高度图数据异常比如全 NaN、尺寸和其他图不一致、或者裁切后区域为空。批量脚本没有做异常捕获一张出错整个中断。解决在预处理和推理外面包 try-except把出错的文件名和异常信息打出来跳过继续跑。同时检查出错图的原始高度数据看是不是传感器丢点或扫描范围不对。常见做法是在预处理开头加尺寸校验尺寸不对的直接 resize 到统一尺寸再处理。5. 进阶技巧把识别结果反标回原图并做批量验证跑通单张推理之后下一步是把识别结果反标回原始高度图或原始轮胎图像上方便目视验证。常见做法是记录裁切和展平时用的变换矩阵推理得到字符框后用逆变换把框映射回原图坐标。如果展平用的是cv2.remap逆映射需要保存map_x和map_y然后用cv2.invertAffineTransform或手动插值反算。这一步容易翻车的地方是坐标偏移padding 的偏移量要在反算时减掉。批量验证我一般会写一个对比脚本把识别结果和文件名里的标签如果有做比对统计准确率同时把识别错误的图单独存到一个文件夹方便集中看。下面是一个批量验证的框架。import os import cv2 import numpy as np def batch_verify(img_dir, label_file, charset): with open(label_file, r, encodingutf-8) as f: labels dict(line.strip().split() for line in f) wrong [] total, correct 0, 0 for name in os.listdir(img_dir): if not name.lower().endswith((.jpg, .png)): continue img_path os.path.join(img_dir, name) try: tensor preprocess(img_path) # logits model.run(tensor) pred postprocess(logits, charset) except Exception as e: print(fskip {name}: {e}) continue gt labels.get(name, ) total 1 if pred gt: correct 1 else: wrong.append((name, gt, pred)) cv2.imwrite(fwrong/{name}, cv2.imread(img_path)) print(facc: {correct}/{total} {correct/max(total,1):.4f}) for item in wrong[:20]: print(item) batch_verify(results, labels.txt, 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ)这个脚本的关键是异常捕获和错误样本留存。preprocess和postprocess接第 3 章的推理链路labels.txt每行是文件名和真实标签空格分隔。跑完看准确率如果低于预期先看 wrong 文件夹里的图确认是预处理问题还是模型问题。错误样本按文件名排序能看出是不是集中在某几个字符或某个区域。从那以后我每次拿到这类作业包都强制先跑一遍单张推理把预处理中间结果存图看一眼再跑批量验证。中间结果图比任何日志都直观字符有没有拉直、对比度够不够、框有没有偏一眼就能看出来。希望帮到你。本文还有配套的精品资源点击获取
返回列表