简介面向机器学习课程设计与期末大作业的轮胎字符识别完整项目提供可直接运行的Python源码、配套文档说明与训练数据覆盖从轮胎图像预处理、字符定位到识别的全流程。项目包含模型推理与参数文件、大量测试图片及多种识别结果样例代码注释细致关键步骤均有解释新手也能对照理解便于答辩讲解。压缩包共156个文件以Python源码、图像素材、模型参数文件及说明文档为主整体约333MB资源结构清晰下载后简单配置即可部署运行。已有298人学习下载适合课程设计、期末大作业或入门OCR场景的快速参考。下载后可获得完整数据集、带注释代码、预训练模型与识别结果截图文档说明进一步梳理实现思路可直接复用于类似字符识别任务具有较高的实际应用价值。1. 轮胎字符识别是什么一道让通用OCR翻车的机器学习作业轮胎字符识别就是把轮胎侧壁上的那串字母数字读出来。它看着和车牌识别差不多都是“拍一张图读一串字符”但只要亲手做过一次你就会发现通用OCR在轮胎面前基本靠不住。因为轮胎侧壁不是平面字符印在弧面橡胶上有凸字、有凹字还有防滑纹和胎毛在字符间穿行光照稍微偏一点同一个字符就能拍出完全不同的长相。这道题的难点不在识别模型而在怎么把一张脏兮兮的轮胎照片变成干净、可训练的字符图像。这门作业通常的价值点在于它是一个完整的“数据→预处理→检测→识别→评估”闭环。你得先在图像里找到字符带再把字符一个个切出来最后用机器学习分类器把它们认出来。整个过程任何一步掉链子最后的识别率都上不去。这篇文章适合三类人正在做机器学习课程设计的学生、想练手计算机视觉项目的新手、以及被轮胎厂或汽修店项目问到“能不能做个自动识别”的工程师。我按自己实际做过的流程把数据组织、图像预处理、模型选型和调参踩坑一次讲透。2. 轮胎字符识别的数据准备采集、标注与两类预处理2.1 先看数据再定方案轮胎字符图长什么样拿到题目先别急着写代码。把数据文件夹打开挑十张有代表性的图看一下。轮胎字符图像和普通OCR数据最大的区别在于字符写在一个圆柱面上。这就导致同样的字符在图像中间和图像边缘的尺度、倾斜角度都不同轮胎转动一点弧面反光位置就变了字符可能是凸起的模压字也可能是激光雕刻的凹陷字二者的明暗关系完全相反。我一般会先给数据分三类看一是光照是否均匀二是字符是否在一条水平带上三是字符有没有断裂或粘连。如果字符带明显弯曲就需要做极坐标展开或分段校正如果只是轻微倾斜做单框透视校正就够了。作业数据通常比真实产线数据干净但干净的图反而容易让人忽略弧面校正这一步测试时换一组手机拍的图就原形毕露。数据准备阶段一定要把原始图、标注文件、划分后的训练验证集分开存。我的目录结构是这样的你可以直接照着建tire_ocr/ ├── raw_images/ # 原始拍摄图一张都不要动 ├── annotations/ # 标注文件JSON或XML均可 ├── crop_char/ # 预处理后切出的单字符图 ├── train/ # 划分后的训练集 ├── val/ # 验证集 └── test/ # 测试集这里有个容易犯的错直接把原始图放进训练集预处理过程散落在不同的Notebook里。作业要提交源代码和文档评审大概率会重跑你的代码。预处理脚本化、数据集划分固定是最值得花半小时做的事。2.2 第一类预处理把倾斜字符“摆正”轮胎字符识别的预处理和车牌识别有个根本差异车牌是平面字符做一次透视变换就足够轮胎字符则是分布在弯曲面上。如果你的样本里字符带整体偏斜不超过15度最省事的做法是直接检测整体字符区域的边缘然后用仿射变换把字符带拉平。如果字符在整条轮胎侧壁上呈明显弧度就需要换思路。常见做法是先把图像转成灰度再做边缘检测找轮胎的外轮廓利用轮廓拟合成椭圆然后做极坐标展开。我一般先试仿射校正因为极坐标展开会把字符边缘拉出锯齿后续分割容易把笔画切断。实际做下来对课程作业的数据量仿射校正加局部透视已经能拿到很稳定的结果。2.3 第二类预处理凸字和凹字的二值化方向是反的这一步是轮胎字符识别的核心坑点。凸字的笔画在侧壁上是凸起的受光后形成一个高光区和阴影区凹字正好相反凹陷区域亮度低、边缘有高光。如果你统一用“暗背景、亮字符”的二值化逻辑凹字会变成背景凸字可能被高光劈成两半。我的处理办法是先做一次自适应阈值同时得到正反两张二值图然后分别计算两图中的连通域数量字符带区域里连通域数量多的那张通常就是正确方向。更稳定的做法是直接用形态学顶帽变换它能同时强化明暗交界区域让凸字和凹字都呈现为边缘特征。顶帽变换的核大小建议取字符笔画宽度的3到5倍比如字符笔画大约5个像素就取15到25像素的核。2.4 标注和数据集划分小数据更要留验证集轮胎字符数据通常不大几十张到两三百张。按6:2:2划分训练、验证、测试。这里强调一个原则同一张原始图上切出的字符不能同时出现在训练集和验证集里。轮胎图像里同一行字符来自同一个光照环境模型很容易从背景纹理而不是字符形状上“作弊”导致验证集虚高。import os import random import shutil from glob import glob random.seed(42) char_images glob(crop_char/*.png) random.shuffle(char_images) n_train int(len(char_images) * 0.6) n_val int(len(char_images) * 0.2) for idx, path in enumerate(char_images): img_name os.path.basename(path) # 根据文件名前缀判断来自哪张原图 src_id img_name.split(_)[0] # 同一个原图的字符必须进同一个集合 if idx n_train: shutil.copy(path, ftrain/{src_id}_{img_name}) elif idx n_train n_val: shutil.copy(path, fval/{src_id}_{img_name}) else: shutil.copy(path, ftest/{src_id}_{img_name})这段代码的关键在src_id它来自文件名最前面一段表示这个字符是从哪张原图切出来的。如果只按打乱顺序硬切同一个原图的字符会被拆到训练和验证两个集合中验证分数就失去了参考意义。作业项目通常没有海量数据这种按原图分组的划分方式比纯随机划分更接近真实评估结果。3. 用 OpenCVHOG/CNN 复现轮胎字符识别核心代码与选型依据3.1 识别方案怎么选从轮廓分割到分类器轮胎字符识别在课程作业里一般不建议一上来就端到端深度学习。原因是字符本体小标注成本高而且作业评审通常看重“你能解释每一步在做什么”。传统的“分割特征分类”路线每一步都可以单独验证某个环节出错也容易定位。我一般推荐的组合是OpenCV做字符定位和分割HOG特征加SVM做分类。如果数据量超过300张且包含多种轮胎规格可以把SVM换成一层简单的CNN见招拆招的效果会好一些。注意轮胎字符数据集很容易包含形近字比如“0”和“O”、“8”和“B”传统方法在image里靠形状特征区分如果特征设计得不好就会在这个地方丢分。3.2 字符定位与分割从原图到干净的单字符给定一张轮胎侧壁图像第一步是定位字符带。处理方法import cv2 import numpy as np def locate_char_region(img_gray): # 顶帽变换增强字符与背景的交界 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (21, 21)) tophat cv2.morphologyEx(img_gray, cv2.MORPH_TOPHAT, kernel) # 自适应阈值blockSize取奇数 thresh cv2.adaptiveThreshold( tophat, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize31, # 窗口越大对光照变化越鲁棒 C10 # 常数值越小越容易把噪声也框进来 ) # 形态学闭运算把字符连成行 close_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (25, 5)) closed cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel) # 按面积过滤找到字符带 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h, w img_gray.shape region None for c in contours: x, y, cw, ch cv2.boundingRect(c) if cw w * 0.3 and ch h * 0.05: if region is None or cw * ch region[2] * region[3]: region (x, y, cw, ch) # 选面积最大的横条 return region这段代码的逻辑是分层递进的。先用顶帽变换把凸字和凹字统一转成边缘特征再用自适应阈值提取二值图。blockSize31对轮胎这种大面积渐变的背景很合适如果图像分辨率偏高超过2000像素宽建议改成51C10是我试下来对轻度脏污容忍度较高的取值取值太大会把字符内部掏空太小会把橡胶纹理当成字符。形态学闭运算用25x5的矩形核是因为字符在水平方向排列紧密、在垂直方向有间隔宽而扁的核能把同一行的字符“焊”成一块却不会把上下两行连起来。最后只保留宽度超过全图30%、高度超过5%的连通域这个阈值能有效排除胎肩的大块黑色区域和侧壁上的装饰纹理。拿到字符带后再对tophat结果在字符带局部做一次二值化和连通域提取就能切出单个字符def split_into_chars(img_gray, region): x, y, w, h region roi img_gray[y:yh, x:xw] # 字符带的局部二值化阈值要重新算不能用全图参数 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) tophat cv2.morphologyEx(roi, cv2.MORPH_TOPHAT, kernel) _, binary cv2.threshold(tophat, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 去除细条干扰字符间常见的防滑纹 clean cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) # 连通域切分 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(clean) chars [] for i in range(1, num_labels): cx, cy, cw, ch, area stats[i] if area 30: # 去噪点 continue if ch h * 0.3: # 去横纹 continue if cw / ch 1.5: # 可能是被粘连的字符 或者 1、I 的误判 # 常见做法优先保留之后在识别环节用宽度约束修正 pass chars.append((cx, cy, cw, ch)) chars.sort(keylambda b: b[0]) return chars这里有几个参数值得细看。area 30是去除孤立噪点对600像素高的图像适用分辨率高就成比例放大ch h*0.3用来剔除字符带上下的装饰线轮胎侧壁在字符带附近常有很细的圆环纹理不滤掉会多出一堆假字符。cw/ch 1.5代表宽高比偏大的连通域可能是两个字符粘连成一个框也可能是数字“1”或字母“I”本身不要在这里直接丢弃留到识别阶段用宽度信息二次判断。切出来的字符图需要统一尺寸。SVM和CNN都要求固定输入我用cv2.resize把每个字符统一到32x32并做零均值标准化。这里要提醒一点不要直接拉伸字符的宽高比会被破坏。先补边到正方形再缩放形近字比如“0”和“9”才不容易混淆。3.3 字符分类HOGSVM 的最小可运行实现字符分割做完后识别就是标准的特征加分类。HOG对边缘方向敏感、对光照变化相对鲁棒很适合轮胎字符这种笔画简单、结构清晰的场景。from skimage.feature import hog from sklearn.svm import SVC from sklearn.model_selection import cross_val_score def extract_hog(img_32x32): # 每个cell 8x8block 2x2方向9个 features hog( img_32x32, orientations9, pixels_per_cell(8, 8), cells_per_block(2, 2), block_normL2-Hys, visualizeFalse ) return features X_train, y_train [], [] for char_img, label in train_data: X_train.append(extract_hog(char_img)) y_train.append(label) clf SVC(C12.0, kernelrbf, gammascale, class_weightbalanced) scores cross_val_score(clf, X_train, y_train, cv3) print(CV accuracy:, scores.mean())参数逻辑如下orientations9是HOG的惯例取值表示梯度方向分成9个区间pixels_per_cell(8,8)对32x32图像是刚好合适的粒度每字符分4x4共16个cell特征维度为16*4*9576。block_normL2-Hys是对梯度直方图做L2归一化能压住部分光照过曝的影响。SVM的C12.0是个经验值在普通光照数据上C从1到15都有不错表现但如果字符边缘断笔严重C要调低到5以下给模型更大容错空间。值得指出的是直接用SVM对HOG特征分类在五六十类字符26个字母10个数字可能的符号上准确率通常在92%到97%。如果验证集准确率低于90%大概率不是分类器的错而是字符分割阶段出了问题比如同一个字符被切成了两半。我用一个简单的自检方法把每个字符框的宽度画在直方图里如果出现大量半宽字符说明分割断了如果出现大量两倍宽字符说明有粘连。4. 轮胎字符识别调参三个直接影响精度的旋钮4.1 自适应阈值的 blockSize 与 C先看字符笔画宽度轮胎字符识别的第一个精度瓶颈是二值化因为后续分割完全依赖二值图。自适应阈值的表现主要被两个参数控制blockSize和C。blockSize决定计算局部阈值时用的邻域大小。字符笔画粗、图像分辨率高就把blockSize调大字符密集、彼此间距小就调小。一个可复现的经验值是让邻域覆盖至少3个字符宽度。比如字符宽度约40像素blockSize取127到151之间比较合适过大如251会导致字符密集区域整体变黑。我排查这个问题时最直接的观察方法是保存二值图人眼扫一遍——如果字符内出现大面积空洞就是邻域太大把字符并进了背景如果字符边缘出现大量毛刺就是邻域太小跟着纹理走了。C是阈值偏移量。C越大像素被判为前景的要求越高字符越细甚至消失C越小字符越粗且容易和相邻字符连上。轮胎字符有凸字和凹字两种同一张图里二者厚度不同我一般取C8~12。如果一套数据里既有模压凸字又有喷墨凹字建议用OTSU全局阈值做兜底因为自适应阈值在这类混合数据上的表现不稳定# 常见做法自适应阈值失败时尝试 OTSU 再比较字符连通域数量 val, otsu_bin cv2.threshold(tophat, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)4.2 形态学核大小开运算去横纹闭运算焊字符形态学核的形状和大小是第二个旋钮。轮胎字符周围最典型的干扰物是细长防滑纹它和字符笔画呈垂直方向。如果防滑纹是横向的用cv2.getStructuringElement(cv2.MORPH_RECT, (1, 5))做开运算就能把它断开——竖直方向的长核会把横向细线切断。反之如果干扰纹是纵向的就用(5, 1)的横向核。闭运算核的大小直接影响字符粘连程度。核太窄同一行的字符连不起来核太宽上下相邻行的字符也会被焊上后面分割就惨了。验证方法很直接用闭运算后的连通域数量作为指标理想结果是连通域数量和字符个数基本一致误差在正负一个以内。这个验证逻辑建议写进处理流程里每换一张图都打印一次统计值。4.3 字符最小面积与宽高比过严则漏字过宽则连字我在split_into_chars里设置的两个过滤条件看似简单却是整个识别精度最容易被忽视的地方。面积阈值定得太严会把磨损较重的字符滤掉——轮胎字符里最常磨损的是生产日期那几位太松侧壁装饰纹理和橡胶颗粒会被当成字符分类器被迫多学一类垃圾。宽高比阈值对字母“1”、数字“1”尤其敏感。“1”的宽高比通常只有0.2到0.35而“M”“W”可能达到0.9以上。如果为过滤噪声把宽高比阈值设到0.5等于把“1”全部丢了。我的做法是单独维护一个窄字符白名单只允许宽高比小于0.4的连通域出现在目标区域同时校验它的面积是否落在字符面积分布的合理区间内。5. 轮胎字符识别避坑清单四个代价最高的踩坑经验5.1 现象验证集准确率96%测试集只有62%这个落差几乎每个做过图像作业的人都会遇到一次。原因是数据划分时没有按原图分组同一张原图上切出来的字符同时出现在训练和验证集里。模型记住的是这张原图的“成像环境”不是字符形状一旦换一组光照不同的图立马现原形。解决方法是按原始图ID分组再做划分并且测试集单独放在一个目录里训练过程中一次都不要打开看。5.2 现象0 和 O、8 和 B 总是互相认错轮胎字符里“0”和“O”在图像上几乎一模一样。模压凸字在强光下圆环内部会产生阴影这个阴影让数字0看起来就是字母O。我踩过这个坑后不再指望分类器自己区分直接在识别后处理里加上轮胎语义约束DOT码和规格位出现的“O”统一按数字0处理品牌信息里的“0”按字母O处理。这是典型的“靠知识纠错”而不是“靠模型硬扛”。5.3 现象字符之间有个别字符始终切不出来排查后发现问题出在字符带里的一个特殊符号上比如轮胎规格里的“/”或“R”。它们笔画细、面积小在形态学开运算后直接消失。解决方法是把面积阈值和宽度阈值分开设置不直接用面积一刀切同时保留原始二值图做对照如果某一行字符数量明显少于预期就降低阈值重新分割一次。5.4 现象训练集是凸字测试时遇到凹字准确率断崖下跌轮胎侧壁字符有凸有凹这是这个项目和普通OCR最大的不同。凸字和凹字在二值化后明暗相反HOG特征完全对不上。解决思路有两个一是把顶帽变换作为统一前置处理让两类字符都变成边缘特征二是在数据增强阶段加入反色变换。def augment_invert(img): # 随机反转让模型不依赖字符明暗方向 if np.random.rand() 0.5: return 255 - img return img这个增强操作简单但非常有效特别是在同时包含凸字和凹字的轮胎数据上比旋转、平移这些常规增强带来的提升更明显。因为轮胎字符识别最大的领域差异不在字形而在光线和字符材质的交互方式。想再进一步可以加入gamma变换模拟不同光照强度。5.5 现象SVM训练时间暴涨字符数据不均衡轮胎字符里数字多、字母少某些品牌字符只有几个样本。class_weightbalanced能缓解这个问题但量级差距太大时也可直接对少数类做复制增强。注意复制增强的位置要放在HOG特征提取之后不能在图像阶段硬复制否则相似的图会同时进入训练和验证。复制的位置要异常小心我吃过亏用一份增强后的复制图做测试结果虚高到让人怀疑人生。6. 进阶把作业方案升级成端到端识别管线到这里你已经拿到了一个“分割HOGSVM”可运行的轮胎字符识别方案。如果想把分数从“完成作业”提到“可展示项目”下一步是把它升级成端到端的目标检测管线用 YOLOv8 直接定位并识别轮胎侧壁字符。YOLOv8 在字符检测任务上的优势是效率高但它的识别的本质是“检测分类”无论如何都逃不脱“样本量”问题。真实产线的数据质量参差不齐摄像头角度固定光照随机直接把作业方案搬进产线之前我会在文档里明确标注“当前方案适用于受控光照环境”这是负责人的做法。import torch from ultralytics import YOLO model YOLO(yolov8n.pt) model.train( datatire_ocr.yaml, epochs80, imgsz640, batch16, lr00.005, augmentTrue, plotsTrue )这里的关键参数yolov8n是最小的模型用于验证标注是否正确auditTrue在训练结束后会输出一批可视化的预测框你必须肉眼确认框是否对齐字符边缘而不是只看mAP。YOLO训练之前要准备tire_ocr.yaml指定训练集、验证集路径和类别名称列表。这样一个端到端方案的好处是干掉了字符分割环节避免“一个字符框错后面全错”的错误传导。坏处是需要更多标注数据并且模型成了一块“黑匣子”在作业答辩里如果被问“这个字符为什么识别错了”你只能解释到特征层面。所以我通常把两种方案都写进文档传统方法作为可解释的主方案YOLO作为精度提升的扩展方案这正好也对应了完整的作业评分体系。最后说说我个人的习惯每次跑完一个流程会把关键中间结果图存一份按“原图→二值图→分割结果→识别结果”四联图保存。排查问题时先打开这套图按顺序看比直接在模型参数里找原因快得多。轮胎字符识别项目看起来小而简单实际上每一步的坑都很深按前面的路子走一遍希望能帮到你。本文还有配套的精品资源点击获取