ARTICLE DETAIL

资讯详情

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

轮胎字符识别实战:OpenCV预处理与随机森林分类

轮胎字符识别实战:OpenCV预处理与随机森林分类 简介一套面向机器学习期末设计与轮胎字符自动识别场景的完整项目资源包含源代码、训练模型与使用说明适合机器学习课程学生、毕业设计者以及工业视觉入门开发者参考。项目围绕轮胎表面的字符检测与识别展开利用EAST、DB算法完成文本区域检测以CRNN网络实现字符识别并给出了模型在弯曲胎面、花样字体等干扰下的效果分析与改进方向包括高度特征局限、7与/易混淆、长句识别不佳等问题。资源包共157个文件约332.75MB主要包含py源代码、png/jpg检测识别结果图、pdmodel/pdiparams等模型权重文件、txt/log运行记录以及README等说明文档结构清晰可直接对照学习。目前已有165人学习下载通过源码、模型与实验分析可以清晰了解轮胎字符识别从检测、识别到稳定性评估的完整流程并避开作者遇到的常见坑点。1. 轮胎字符识别为什么期末作业选它难在哪轮胎侧壁上的那串字符看着像普通的打印体实际拍下来比车牌识别还要麻烦胎面是曲面字符跟着弧度变形橡胶表面有花纹和颗粒纹理二值化之后全是噪声再加上拍摄角度、反光和油污同一个字符在不同照片里可能长得完全不像。机器学习期末作业选这个题目本质上是把“图像处理 特征工程 分类模型”一整条链路都考了一遍比单纯在 MNIST 上跑个 KNN 有区分度得多。这个项目的交付物很明确源代码、训练好的模型、使用说明三者缺一不可。它面对的读者和评分者通常是两类人——老师看的是技术路线是否完整、参数是否讲得清同学看的是“我自己拍的照片能不能跑出结果”。所以这篇文章会按一条可复现的路线走先讲为什么用传统机器学习而不是直接上深度网络再给完整的预处理与分割流程然后训练分类器最后把使用说明的结构和验证脚本的设计讲清楚。适合正在做期末项目、手头有轮胎照片但不知道怎么下手的人。2. 方案选型检测加分类还是整行识别期末作业怎么选2.1 标题里的“机器学习”大概率指什么很多期末作业标题写“基于机器学习”实际上默认排除了深度学习端到端方案。这不算限制反而是一个务实的选择轮胎字符识别这种任务字符集小数字加少量字母、背景相对固定、单个字符结构清晰传统机器学习完全够用。常见做法是把它拆成三个子任务定位字符区域、切出单个字符、对每个字符做分类。三个子任务分别用图像处理和分类模型解决每一步都能在答辩时讲清楚“为什么这么做”这是评分最看重的东西。轮胎字符和 MNIST 手写数字最大的区别在于分布。MNIST 的字符基本居中、背景干净而轮胎照片里的字符往往在弧面上、有强反光、还有轮胎品牌 logo 干扰。所以预处理环节的工作量远大于模型环节。如果你发现自己的代码里预处理占了 60% 以上的行数这是正常的说明你理解了这类任务的本质。反过来如果有人上来就调一个 ResNet 跑迁移学习虽然在真实项目里合理但在期末作业里反而暴露两个问题一是难以解释特征是怎么提取的二是对硬件和调参经验要求高翻车概率大。2.2 为什么不用现成 OCR轮胎字符的差异现成 OCR 引擎比如 Tesseract对印刷体、扫描件效果不错但对轮胎侧壁这种场景往往水土不服。原因是 Tesseract 的训练数据以文档为主它默认字符是在一条水平基线上排列的。轮胎字符有弧度、有倾斜同一个字符可能被橡胶纹理分割成几段Tesseract 的整形算法会把它们错误合并或拆散。试过的人会发现一个典型现象Tesseract 识别“DOT”这种字母串偶尔能对但识别“3512”这种数字串时经常把“1”认成“l小写L”“8”和“3”互相混淆。这是因为轮胎字符本身就不是标准印刷体它的笔画边缘是橡胶硫化出来的有圆角、有毛刺不符合 OCR 引擎对“干净笔画”的假设。自己训练分类器的好处是你可以针对轮胎字符的笔画形态提取特征比如把字符归一化到 32×32 之后统计水平和垂直投影这些特征比 OCR 引擎内部的形状描述子更贴合你的数据。2.3 技术路线对比三种做法的取舍方案实现成本期末场景适配度主要风险传统 ML预处理 分割 分类器中低高链路完整、答辩好讲分割粘连字符需要调参CNN 端到端检测 识别高中容易陷入训练不稳定数据量不够、GPU 依赖现成 OCR 引擎低低缺乏“机器学习”过程轮胎场景识别率差我在实际做的时候选择的是第一行OpenCV 做预处理和字符分割HOG 随机森林做字符分类。理由很直接——第一期末作业的数据量通常只有几百张图随机森林在这个规模下比 SVM 更稳不用花太多时间调核函数参数第二随机森林能输出特征重要性答辩的时候你可以直接说“HOG 里哪几个 block 对数字‘0’和‘8’的区分贡献最大”这种细节非常加分。如果你对传统机器学习中的数据处理流程比较熟可以直接跳到第 3 章的实现部分如果不熟先把这一章的选型逻辑记住分割比分类难预处理比模型重要。3. 图像预处理与字符分割从轮胎照片到干净字符的完整流程3.1 数据从哪来自拍、合成与公开字符集轮胎字符识别的数据来源有三条路。第一是自拍把轮胎放在窗边自然光下手机横屏拍摄每张照片可以包含 4 到 6 个字符拍 100 张左右就能通过滑动裁切得到几千个单字符样本。第二是合成找一张干净背景的轮胎照片把字符用不同字体的渲染图通过仿射变换贴上去这样可以控制弧度和光照方向用来补足自拍里缺少的字符组合。第三是直接使用公开的英文字符集做预训练再用手里的轮胎照片微调适合字符形态与标准字体接近的情况。我一般会把自拍和合成按 7:3 混合。合成数据不需要多它的作用不是增加数量而是补充“稀有字符”——比如轮胎规格里的“R”和“Z”自拍数据里可能只出现两三次分类器对它们的特征学习会很不充分。合成时注意一点字符的弧度要能对应真实轮胎的曲率半径否则模型学到的变形规律在真实照片上不成立。这是很多人忽略的地方直接用平面字体贴图会导致训练集和测试集分布不一致跑出来的准确率虚高一上真照片就崩。3.2 预处理流水线灰度、去噪、二值化、倾斜校正预处理的目标只有一个把轮胎照片变成“黑底白字、字符尽量分离”的干净图像。下面是完整代码基于 OpenCV。import cv2 import numpy as np def preprocess_tire_image(img_path, debugFalse): # 读入并缩放轮胎照片通常太大统一到宽 800 img cv2.imread(img_path) h, w img.shape[:2] scale 800.0 / w img cv2.resize(img, (800, int(h * scale))) # 转灰度做高斯模糊模糊半径别太大3x3 足够 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (3, 3), 0) # 自适应阈值轮胎表面光照不均匀全局阈值一定会翻车 binary cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize35, # 邻域大小轮胎纹理粗用小了噪声全出来 C8 # 阈值偏移C 越大越不容易把橡胶纹理二值化成前景 ) # 形态学开运算去掉孤立的颗粒噪声保持字符主体 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) if debug: cv2.imwrite(debug_binary.png, cleaned) return cleaned, img这个流程里有三个参数值得单独说明。blockSize是自适应阈值的邻域窗口轮胎侧壁的纹理周期大约在 10 到 20 个像素窗口设到 35 可以保证邻域内有足够多的背景像素参与比较这样字符笔画不会被误判为背景。C是阈值偏移量它的作用是整体压低判定阈值让橡胶本身的浅纹理不满足“比邻域均值高 8”的条件从而被滤掉。kernel尺寸决定了开运算去除的最小噪声粒度设成 2×2 是因为轮胎字符的笔画宽度一般在 3 像素以上2×2 不会伤到字符本体但能去掉大部分颗粒噪声。倾斜校正放在二值化之后做。先用cv2.findContours拿到所有字符连通域的外接矩形再用cv2.minAreaRect求最小外接矩形的角度最后做仿射变换。注意不要对整张图直接做 Hough 直线检测来求倾斜角轮胎侧壁的纹理本身会产生大量伪直线角度会测偏。按字符连通域求角度会更稳def deskew(binary, img): contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) angles [] for cnt in contours: area cv2.contourArea(cnt) if area 50: # 面积过小的连通域是噪声跳过 continue rect cv2.minAreaRect(cnt) angle rect[2] # OpenCV 的 angle 范围是 [-90, 0)统一到 [-45, 45] if angle -45: angle 90 angle angles.append(angle) if not angles: return img # 取中位数而不是均值避免个别大块干扰 median_angle np.median(angles) h, w binary.shape[:2] M cv2.getRotationMatrix2D((w / 2, h / 2), median_angle, 1.0) return cv2.warpAffine(binary, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE)用中位数而不是均值是血泪经验。轮胎上可能有一个很大的品牌 logo 连通域它的外接矩形角度和字符区域差 20 度以上如果取均值整个图的校正角度都会被带偏中位数能保证大部分字符的倾斜角决定最终旋转角度单个异常连通域影响不了结果。旋转后的图像用BORDER_REPLICATE填充边缘不要用默认的黑色填充否则旋转产生的黑边在后续分割时会变成一个巨大的连通域把字符区域的投影统计搞乱。3.3 字符分割投影法、连通域法、滑动窗口的配合分割是轮胎字符识别里最频繁翻车的环节。字符间距不均匀、笔画断裂、弧面导致字符边缘弯曲这些都会让分割算法出错。我通常把两种方法叠起来用先用垂直投影粗切再用连通域细校。def segment_chars(binary, min_w8, min_h12): # 垂直投影统计每列的前景像素数 col_sum binary.sum(axis0) / 255 # 找“连续有内容的列区间” in_char False ranges [] start 0 for i, val in enumerate(col_sum): if val 2 and not in_char: # 列前景像素 2 视为字符开始 in_char True start i elif val 2 and in_char: in_char False ranges.append((start, i)) # 过滤过窄的区间可能是噪声列 ranges [(s, e) for (s, e) in ranges if (e - s) min_w] chars [] for (s, e) in ranges: char_img binary[:, s:e] # 行投影去掉字符上下的残留噪声 row_sum char_img.sum(axis1) / 255 rows [y for y, v in enumerate(row_sum) if v 1] if rows: char_img char_img[min(rows):max(rows) 1, :] # 统一缩放到 28x28和 MNIST 保持一致的习惯 if char_img.size 0: char_img cv2.resize(char_img, (28, 28), interpolationcv2.INTER_AREA) chars.append(char_img) return chars投影法的问题在于如果两个字符粘连成了一片垂直投影看不出它们之间的间隙会直接分成一个宽块。这时要用连通域分析兜底。拿一个分割失败的宽块做cv2.findContours如果里面能提取出两个宽度接近我们预设字符宽度的连通域就把它们当作两个字符如果是一个大连通域多半是“8”或“B”这种本身就闭合的字符保持原样。滑动窗口在这套流程里是个容易被忽略但很好用的工具。轮胎字符不是严格等间距的某些品牌会在字符之间加一个圆点分隔符用固定窗口滑过去会把这些点切成噪声。我的做法是先按投影法粗切再用小窗口16 像素宽在字符间隙处二次扫描判断间隙里是否存在宽度小于 4 像素的小块如果有就忽略。这样分隔符不会干扰字符序列代码量也不大。4. 特征提取与模型训练HOG 加随机森林把分类器跑起来4.1 特征选择HOG、像素特征和投影特征怎么组合字符图像归一化到 28×28 之后就能直接拉平当特征但纯像素特征对笔画粗细和轻微位移太敏感。比如轮胎橡胶在硫化时字符边缘会有 1 到 2 像素的圆角变化纯像素特征会把这种变化当成重要信号导致同一个字符“0”在不同照片里被分到不同类别。我一般用 HOG梯度方向直方图做主特征再加两组统计特征做补充。HOG 的核心参数是 cell 大小和 block 大小。对 28×28 的字符图合理的设置是 cell4×4、block2×2、方向数 bin9。这样每个 cell 有 9 维直方图每个 block 有 4 个 cell 共 36 维滑动步长 4 像素最后特征维度不高随机森林训练非常快。HOG 对边缘方向的统计天然抗光照变化因为梯度方向不受整体亮度影响这正好对应轮胎反光导致的局部过曝。补充的两组特征是水平和垂直投影的归一化向量再加一个字符的宽高比和像素密度。这组特征一共才 2828258 维但效果非常实在宽高比可以直接把“I”“1”和“0”区分开投影特征能把断裂的“5”和完整的“5”拉到相近的特征空间。组合方式很简单HOG 特征和投影特征拼成一个一维数组不需要做 PCA 降维随机森林对冗余特征不太敏感强行降维反而可能在期末答辩时被追问“每个主成分的含义”。4.2 训练数据构造滑动窗口裁切与类别均衡把预处理好的图片喂给模型之前得先构造训练数据集。这里有一个常见的错误做法直接把每张轮胎照片里的字符手工截出来整张图放进一个文件夹就完事。问题是不同照片里同一个字符的归一化大小不一致、位置有偏移模型学到的特征里混入了“位置”信息测试时换了一张构图不同的照片准确率就掉下来。我的做法是把分割得到的字符图先做一次“居中校正”计算字符像素的质心然后平移整个图让质心落在 28×28 的中心格点上。这一步很简单但收益很大相当于把训练分布里的位置方差去掉了。之后再统一除以 255 归一化像素值HOG 特征和统计特征按前文方式提取。类别均衡也需要主动处理。轮胎规格字符串中数字“1”“3”出现频率远高于“0”“8”而“Z”“R”这种字母更稀有。如果直接训练随机森林会倾向于把稀有字符预测成高频字符。我用imblearn.over_sampling.RandomOverSampler做少量过采样但只对小类别生效不是无脑复制到高频类别数量。过采样倍率设成 2 到 3 倍就够太多会让决策边界过拟合到重复样本上。4.3 训练脚本随机森林加网格搜索from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV, train_test_split import joblib # X: 形状为 (n_samples, n_features) 的特征矩阵 # y: 对应的字符标签全部转成字符串类型 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 随机森林的网格搜索范围 param_grid { n_estimators: [100, 200, 300], max_depth: [10, 20, None], min_samples_leaf: [1, 2, 4], class_weight: [balanced, None], } rf RandomForestClassifier( n_jobs-1, random_state42, criterionentropy # 熵增益比基尼对字符这种多类别更稳定 ) grid GridSearchCV( rf, param_grid, scoringaccuracy, cv5, verbose1 ) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(best cv score:, grid.best_score_) test_acc grid.score(X_test, y_test) print(test acc:, test_acc) # 保存模型同时把特征提取配置一起保存 joblib.dump({ model: grid.best_estimator_, feature_config: { hog_cell: (4, 4), hog_block: (2, 2), hog_bins: 9, with_projection: True, }, classes: sorted(set(y)), }, tire_char_model.joblib)网格搜索的参数范围是根据实际经验收窄过的。min_samples_leaf从 1 开始搜但通常最优值会落在 2 或 4因为轮胎字符的噪声会让叶子节点过细class_weight设成balanced和不设都试试如果你的数据做过过采样balanced的增益不明显这时就不要重复加权。训练结束后我会额外打印每个类别的召回率而不是只看总体准确率——总体准确率可能被高频字符拉高到 98%但“Z”的召回率只有 40%这个信息在答辩时比一个漂亮的总分更有说服力。特征提取和模型训练如果分开写要注意特征提取的代码必须和推理时完全一致。我遇到过这样的情况训练脚本里对字符图做了cv2.resize(28, 28)但推理脚本里换成了cv2.resize(32, 32)HOG 特征维度对不上模型直接报维度错误。所以上面对feature_config做了序列化保存推理时从模型文件里读配置再重建特征提取器这是开发效率最高的做法。5. 避坑记录预处理、分割与训练集不平衡的排查清单5.1 自适应阈值把字符滤没了现象二值化之后字符区域变成一片黑和背景无法区分分割出来的字符全是空白块。原因blockSize设得太小比如 11 或 15自适应阈值的邻域范围小于字符笔画之间的间隔导致邻域均值被字符本身拉高笔画像素的灰度低于“邻域均值 C”被判成背景。解决把blockSize加大到 35 到 45 之间同时检查C的值。C 太小比如 2橡胶纹理也会被判成前景C 太大比如 15细笔画字符会被吞掉。调试方法很朴素跑完预处理后把每张中间结果写盘肉眼扫一遍不要用统计指标代替人工检查。5.2 字符“8”被分割成“0”和“1”现象投影法切割宽字符时把“8”的左侧和右侧看成两个字符或者把“B”的竖笔画和圆弧拆开。原因轮胎字符笔画宽度不均匀某些字体里“8”的左上侧笔画较细垂直投影在那一列出现接近 0 的低谷触发了切分条件。这是投影法的固有缺陷它只统计像素数量不检查切分位置是否落在字符结构的凹口。解决在投影法切出的相邻区间间距过小时比如两个区间相隔不到 2 像素合并它们再用连通域判断合并后的宽度是否接近单个字符的正常宽度。也可以给分割器加一个最小字符宽度约束比该约束窄的区间直接合并到相邻区间。这个阈值一般取平均字符宽度的 0.6 倍最稳。5.3 训练集不平衡导致稀有字符识别率极低现象总体识别准确率 96%但单独看“Z”和“Q”的召回率只有 30% 左右图片里一旦出现这些稀有字符整串字符就识别错。原因轮胎规格字符中数字占了八成以上字母出现频率很低随机森林学到的节点分裂策略偏向高频类别。在样本极少的情况下模型甚至会把含有“Z”特征的字符直接分给“2”或“7”。解决两层手段并用。第一在数据侧做稀有字符的合成扩充用第 3 章提到的合成流程补足“Z”“Q”“R”等字母第二在模型侧设置class_weightbalanced让损失函数自动提高稀有类别的权重。两个手段一起用比单靠模型侧调权重更稳因为纯权重法在样本量只有十几张时会导致模型对重复样本过拟合。5.4 模型加载时报错或推理结果与训练时不一致现象模型文件换了一台电脑推理报“特征维度不匹配”错误或者不报错但准确率骤降。原因模型文件只保存了树结构没有保存特征提取参数。换了机器或换了 OpenCV 版本字符归一化尺寸、HOG 参数在推理脚本里被重新写了一遍和训练时不一致。解决像第 4 章代码那样把特征提取配置序列化进模型文件。加载模型时先读配置再实例化特征提取器而不是在推理脚本里手写一遍参数。还有一个容易被忽略的点OpenCV 的cv2.resize在不同版本里默认插值算法不一样尤其INTER_AREA的缩小时行为在不同版本有微妙差异尽量在requirements.txt里固定 OpenCV 的版本号。5.5 推理时整串字符输出错位现象单字符分类准确率很高但整串字符输出总是漏掉最后一个或重复第一个字符。原因分割阶段的“过滤噪声”逻辑太激进。比如字符区开头有一个很小的圆点分隔符被投影法当成一个字符切出来分类器把它错分成一个数字整串识别结果就多了一位或者最后一个字符的边缘被形态学开运算腐蚀掉了投影统计该列前景像素数低于阈值于是漏切。解决在分割结果上做一个后处理——检查切出的字符序列宽度是否符合轮胎字符串的字符数规律比如 DOT 码通常 12 到 13 位规格码 6 到 8 位。如果切出的数量超出预期优先怀疑分隔符和噪声如果少于预期优先怀疑边缘字符被腐蚀。把“数量校验”写进推理流程里能挡住大多数分割错误。6. 使用说明与验证脚本让你的源代码和模型可复现期末作业的“使用说明”不是随便写几个步骤就行它承担的功能是让另一个人只花五分钟就能把你的代码跑通。我会固定成套组织requirements.txt、目录结构说明、训练脚本、推理脚本、验证脚本外加一份 README。README 里只写三类内容环境怎么装、训练怎么跑、单张图片怎么预测。不多写不写设计理念。验证脚本是使用说明里最容易被忽略但最加分的部分。建议写一个evaluate.py读取一张标注好的轮胎图片调用预处理、分割、分类整条链路输出预测结果和置信度并把分割得到的字符图像按顺序拼接预览。这个脚本的意义在于它能一次性暴露链路中的问题预处理改了参数但分割没跟上、模型文件加载路径错误、分类器输入维度不匹配全部会在运行时报出来。如果只用 README 描述流程这些问题只有真正跑推理的人才会发现。如果要加一个对期末作业有实际价值的进阶技巧我的建议是把置信度阈值加进推理脚本。随机森林可以输出每个字符类别的概率分布当最高概率低于 0.85 时脚本不吐结果而是输出“识别失败建议重新拍摄或调整光照”。这个逻辑在真实使用时非常有用——它让使用者知道问题出在拍摄环节而不是模型环节也避免了模型硬输出一个错误答案误导后续处理。加上这个功能只需要十几行代码但答辩时讲“我设计了置信度拒绝机制”远比“我的模型准确率 96%”更有含金量。我自己的教训是第一次做这个项目时把全部时间都花在调分类器上最后发现识别瓶颈根本不在模型而在分割。后来把所有踩过的参数记录进 README 的“常见问题”段落比如“分割后字符粘连调什么、过曝后二值化调什么”整个项目才算真正闭环。这个习惯保持到了现在。如果你想在这个方向上拿到好结果先把预处理和分割的中间结果全部可视化出来确认每一步没问题再碰模型。前 80% 的时间花在前半段流程上后 20% 的时间会让模型训练变得异常顺利。希望帮到你。本文还有配套的精品资源点击获取
返回列表