
简介这份资源面向高校学生与机器学习初学者提供一套完整的轮胎字符识别课程设计实现方案可用于期末大作业、课程设计或自学练手。项目以Python语言编写包含源代码、文档说明与配套数据集代码注释详尽新手也能看懂下载后简单部署即可运行具备功能完善、界面美观、操作便捷的特点。压缩包共156个文件约333.12MB其中19个py源码文件承载核心识别逻辑63个png与27个jpg图片构成训练与测试样本另有6组pdmodel、pdiparams等模型权重文件、8个txt说明及2个md文档便于理解项目结构与推理流程。目前已有298人学习关注。通过该资源读者可掌握字符识别从数据准备、模型训练到推理部署的完整链路并参考现成目录组织与排错思路快速完成一份高分作业。1. 轮胎字符识别到底在识别什么从一条产线质检需求说起轮胎侧壁那圈凸起的字符包含规格、生产日期、DOT 码、模具号是追溯和质量管控的唯一身份信息。传统做法是人工拿手电筒照着看一条胎 3 到 5 秒夜班还容易看串行。机器学习作业里做轮胎字符识别本质就是把「检测字符区域 识别字符内容」这两步串成一条可复现的流水线用开源数据集和预训练模型在单卡上跑通。它适合两类人一是课程设计需要交完整源码和文档说明的学生二是产线视觉工程师想把 OCR 方案快速验证一遍。核心难点不在模型多深而在轮胎字符是凸起浮雕光照一斜就出现高光断裂和阴影粘连直接套通用 OCR 会翻车。下面按「数据怎么造 → 检测怎么训 → 识别怎么调 → 坑在哪」推一遍代码和参数都能直接抄。2. 数据准备与标注轮胎字符数据集怎么造才不返工2.1 先想清楚标注粒度检测框和识别文本要分开存轮胎字符识别是典型的「检测 识别」两阶段任务标注也必须分两层。检测层标字符行或单个字符的外接矩形识别层标矩形内的文本内容。常见做法是检测标整行比如195/65R15一整行一个框识别再按行切分这样标注量少、对齐简单。如果一上来就标单字符轮胎上字符间距不均匀框会互相压后期训练 NMS 阈值很难调。我一般用 LabelImg 或 PPOCRLabel 标检测框导出 YOLO 格式的txt每行class x_center y_center w h坐标归一化到 0~1。识别文本单独存一个rec_gt.txt格式是图片路径\t文本内容。两份文件用同一套文件名索引避免对不上。# 目录结构建议检测和识别数据物理隔离 dataset/ ├── det/ │ ├── images/ # 原图 │ ├── labels/ # YOLO 格式 txt │ └── train.txt # 图片绝对路径列表 ├── rec/ │ ├── crop_images/ # 从检测框裁出的字符行小图 │ └── rec_gt.txt # 路径 制表符 文本 └── char_dict.txt # 字符集一行一个字符逻辑说明检测和识别分开存是为了让两个模型独立迭代。检测框调好了识别数据可以随时重新裁不用动检测标注。char_dict.txt必须和识别标签里出现的字符完全一致多一个少一个都会导致训练时索引越界。参数说明YOLO 格式的class从 0 开始编号轮胎字符识别通常只有一类text所以全是 0。归一化坐标用x_center/image_w不要用像素值否则换分辨率就废。2.2 数据增强要针对浮雕字符做别用通用增强轮胎字符最大的干扰是光照。通用增强里的随机裁剪、翻转对字符识别有害字符翻转后语义变了但随机亮度、对比度、高斯噪声、运动模糊这四样必须开。尤其是运动模糊模拟产线轮胎旋转时的拖影能显著提升实拍鲁棒性。import cv2 import numpy as np import random def augment_tire_char(img): # 只做光照和模糊类增强不做几何翻转 if random.random() 0.5: alpha random.uniform(0.6, 1.4) # 对比度 beta random.uniform(-40, 40) # 亮度偏移 img cv2.convertScaleAbs(img, alphaalpha, betabeta) if random.random() 0.3: ksize random.choice([3, 5]) img cv2.GaussianBlur(img, (ksize, ksize), 0) if random.random() 0.3: # 模拟旋转拖影只沿水平方向 kernel np.zeros((1, 15), np.float32) kernel[0, :] 1.0 / 15 img cv2.filter2D(img, -1, kernel) if random.random() 0.3: noise np.random.normal(0, 8, img.shape).astype(np.uint8) img cv2.add(img, noise) return img逻辑说明convertScaleAbs同时调对比度和亮度模拟不同角度打光。水平方向卷积核模拟轮胎转动拖影因为产线上轮胎是绕轴转的拖影方向基本水平。高斯噪声模拟高 ISO 下的噪点。参数说明alpha范围 0.6~1.4 是血泪经验超过这个范围字符边缘会糊到无法辨认。beta正负 40 是灰度偏移上限再大暗部细节全丢。模糊核只用 3 和 57 以上字符笔画就断了。2.3 数据集划分和字符集统计的硬规矩划分比例按 8:1:1 走但必须按轮胎品牌或拍摄批次分层抽样。如果随机分同一批次的相似图片可能全进训练集验证集指标虚高实拍直接崩。字符集统计要在划分前做把所有识别文本拆成单字符去重生成char_dict.txt第一行留一个空白符给 CTC 的 blank。项目建议值说明训练集比例80%按批次分层验证集比例10%用于早停和调参测试集比例10%只在最后跑一次单字符最小样本数≥ 50低于此值的字符要补数据图片最短边≥ 640低于此值字符笔画会丢提示字符集里如果出现O和0、I和1混标识别准确率永远上不去。标注阶段就要定死规则轮胎行业一般0带斜杠、O不带按实际字体来。3. 检测模型训练YOLO 在轮胎字符上的参数怎么设3.1 为什么选 YOLO 而不是通用文本检测通用文本检测如 DBNet、EAST是为自然场景设计的对长文本行友好但轮胎字符是短行、多行密集排列字符行之间间距很小。YOLO 作为通用目标检测把每个字符行当独立目标框回归更直接小目标召回率反而更高。而且 YOLO 生态成熟改配置文件就能跑适合作业场景快速出结果。常见做法是用 YOLOv5 或 YOLOv8 的 nano/small 版本输入 640×640单类检测。轮胎字符行高度通常占图片 1/10 到 1/20属于中小目标anchor 要重新聚类。# tire_det.yaml 关键配置 path: ./dataset/det train: train.txt val: val.txt nc: 1 names: [text] # 训练超参 img_size: 640 batch_size: 16 epochs: 200 lr0: 0.01 lrf: 0.01 anchors: - [10,13, 16,30, 33,23] - [30,61, 62,45, 59,119] - [116,90, 156,198, 373,326]逻辑说明nc: 1表示只检测字符行一类。anchors三组分别对应小、中、大目标第一组最小的是 10×13 像素对应远距离拍摄的字符行。如果自己的数据字符行普遍更小要用 k-means 重新聚类命令是python utils/autoanchor.py --data tire_det.yaml。参数说明lr0初始学习率 0.01 是 YOLO 默认值轮胎字符数据量通常几千张这个值偏大建议降到 0.005 并开余弦退火。batch_size16 是 8G 显存的安全值显存够可以上 32。epochs200 配合早停patience30避免过拟合。3.2 训练命令和关键监控指标python train.py \ --data tire_det.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --patience 30 \ --lr0 0.005 \ --cos-lr \ --name tire_det_run1逻辑说明--weights yolov5s.pt用预训练权重轮胎字符数据量不大从头训收敛慢且容易过拟合。--cos-lr开余弦退火后期学习率自动降精度更稳。--patience 30表示验证集 30 轮不提升就停。参数说明--img 640是输入分辨率如果字符行在原图里小于 20 像素要提到 960 或 1280但显存翻倍。--name指定输出目录方便对比多次实验。训练时重点看三个指标mAP0.5到 0.9 以上算可用precision低于 0.8 说明误检多recall低于 0.85 说明漏检多。漏检比误检更致命因为漏掉的字符行直接导致识别缺失。如果 recall 上不去优先检查标注框是否把字符行裁得太紧留 2~3 像素边距。3.3 检测后处理NMS 阈值和框合并轮胎字符行可能被检测成多个重叠框需要 NMS 去重。但字符行之间本身间距小NMS 阈值设高了会把相邻行误删。我一般把iou_thres设 0.45比默认 0.5 略低宁可多留几个框后面识别阶段再过滤。def merge_boxes(boxes, texts, iou_thres0.45): # boxes: Nx4, texts: N keep [] order boxes[:, 4].argsort()[::-1] # 按置信度降序 while order.size 0: i order[0] keep.append(i) xx1 np.maximum(boxes[i, 0], boxes[order[1:], 0]) yy1 np.maximum(boxes[i, 1], boxes[order[1:], 1]) xx2 np.minimum(boxes[i, 2], boxes[order[1:], 2]) yy2 np.minimum(boxes[i, 3], boxes[order[1:], 3]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) iou w * h / ( (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1]) (boxes[order[1:], 2] - boxes[order[1:], 0]) * (boxes[order[1:], 3] - boxes[order[1:], 1]) - w * h ) inds np.where(iou iou_thres)[0] order order[inds 1] return keep逻辑说明标准 NMS 实现按置信度从高到低遍历删掉与当前框 IoU 超阈值的框。iou_thres0.45是轮胎字符场景的平衡点。参数说明如果发现相邻字符行被误删把iou_thres降到 0.35如果同一行出现多个重复框升到 0.5。这个值没有万能解要在验证集上试。4. 字符识别模型CRNN 训练与 CTC 解码的实操细节4.1 为什么用 CRNN 而不是 Transformer OCR轮胎字符是固定字体、固定排列的短文本不需要 Transformer 那种大感受野和语言建模能力。CRNNCNN BiLSTM CTC参数量小、训练快、对短文本精度足够单卡几小时就能收敛。Transformer OCR 在长文本和复杂版面上有优势但轮胎字符场景属于杀鸡用牛刀还容易过拟合。CRNN 的结构CNN 提特征把高度压到 1宽度保留BiLSTM 做序列建模CTC 做对齐解码。输入统一缩放到 32×320高×宽宽度按比例缩放不足补白。import torch import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes, hidden_size256): super().__init__() # CNN: 输入 1x32x320 - 输出 512x1x40 self.cnn nn.Sequential( nn.Conv2d(1, 64, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.ReLU(), nn.Conv2d(256, 256, 3, 1, 1), nn.ReLU(), nn.MaxPool2d((2,1), (2,1)), nn.Conv2d(256, 512, 3, 1, 1), nn.ReLU(), nn.Conv2d(512, 512, 3, 1, 1), nn.ReLU(), nn.MaxPool2d((2,1), (2,1)), ) self.rnn nn.LSTM(512, hidden_size, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(hidden_size * 2, num_classes) def forward(self, x): # x: B x 1 x 32 x 320 conv self.cnn(x) # B x 512 x 1 x 40 conv conv.squeeze(2) # B x 512 x 40 conv conv.permute(0, 2, 1) # B x 40 x 512 rnn_out, _ self.rnn(conv) # B x 40 x 512 out self.fc(rnn_out) # B x 40 x num_classes return out逻辑说明CNN 最后两层池化用(2,1)只在高度方向下采样宽度保留这样序列长度不会太短。BiLSTM 输出每个时间步的字符概率CTC 负责把重复字符和 blank 去掉。参数说明num_classes等于字符集大小加 1blank。hidden_size256是常用值数据量小可以降到 128。输入高度固定 32宽度 320 对应约 20 个字符轮胎字符行一般不超过 20 个字符够用。4.2 CTC 损失和解码blank 和重复字符的处理CTC 的核心是允许输出序列和标签长度不一致通过 blank 分隔重复字符。比如真实文本AABCTC 可能输出A_A_B或AAAB解码时合并重复并去 blank 得到AAB。import torch.nn.functional as F def ctc_loss(pred, targets, target_lengths): # pred: B x T x C, 需要转成 T x B x C pred pred.permute(1, 0, 2) T, B, C pred.shape input_lengths torch.full((B,), T, dtypetorch.long) pred_log F.log_softmax(pred, dim2) loss F.ctc_loss(pred_log, targets, input_lengths, target_lengths, blank0, reductionmean) return loss def ctc_decode(pred): # 贪心解码取每步最大概率去重复去 blank pred_idx pred.argmax(dim2) # B x T results [] for seq in pred_idx: out [] prev -1 for idx in seq: idx idx.item() if idx ! prev and idx ! 0: # 0 是 blank out.append(idx) prev idx results.append(out) return results逻辑说明ctc_loss要求输入是T x B x C所以先 permute。blank0约定第 0 类是 blank字符集从 1 开始编号。贪心解码简单够用轮胎字符没有语言歧义不需要 beam search。参数说明input_lengths全是 T因为 CRNN 输出序列长度固定。如果用了变长输入要按实际宽度算。reductionmean对 batch 求平均batch 内长度差异大时可以改sum再手动除。4.3 训练配置和精度验证python train_rec.py \ --train_data ./dataset/rec/rec_gt.txt \ --char_dict ./dataset/char_dict.txt \ --img_h 32 \ --img_w 320 \ --batch 64 \ --lr 0.001 \ --epochs 100 \ --val_interval 5逻辑说明识别模型比检测模型收敛快100 轮足够。--val_interval 5每 5 轮验证一次看准确率曲线。参数说明--batch 64识别模型小显存占用低batch 可以大。--lr 0.001配合 Adam 优化器比 SGD 收敛稳。--img_w 320如果字符行更长按比例放大到 480 或 640但序列长度增加CTC 收敛变慢。验证时看两个指标字符准确率逐字符对比和整行准确率整行完全正确。轮胎追溯场景整行准确率更重要因为错一个字符整条码就废了。整行准确率到 95% 以上算可用低于 90% 要回去查标注质量。5. 避坑与排查轮胎字符识别最常见的 5 个翻车点5.1 高光断裂导致字符被切成两半现象检测框把一行字符拆成两个框识别出来文本中间缺字符。原因轮胎凸起字符在斜射光下笔画顶部出现高光二值化或特征图上表现为断裂。YOLO 的框回归把断裂的两段当两个目标。解决训练数据里加入高光增强用cv2.inRange模拟局部过曝检测后处理加一步「水平方向相邻框合并」如果两个框垂直中心差小于框高的 30%且水平间距小于框宽的 50%就合并成一个框再送识别。5.2 字符集里混入不可见字符现象训练 loss 正常下降但验证时整行准确率始终卡在 80% 上不去。原因标注文本里混入了空格、制表符或全角字符char_dict.txt里没有对应项CTC 解码时这些位置输出 blank导致字符错位。解决标注完成后跑一遍清洗脚本把所有文本转半角、去首尾空格、过滤非打印字符。char_dict.txt生成后用set(所有文本字符) - set(char_dict)检查差集必须为空。5.3 检测框太紧导致识别输入缺笔画现象检测 mAP 很高但识别准确率低裁出来的小图字符边缘被切。原因标注时框贴字符外沿训练出的检测框也紧裁剪时没有留边距字符顶部或底部笔画被切掉。解决标注时框向外扩 2~3 像素推理裁剪时按框的 1.1 倍扩边再送识别。这个改动通常能提升 3~5 个点整行准确率。5.4 学习率过大导致 CTC 输出全 blank现象训练前几轮 loss 不降反升解码结果全是空。原因CTC 在初期容易陷入全 blank 的局部最优学习率大时更明显。解决识别模型初始学习率降到 0.0005并加 5 轮 warmup。如果已经出现全 blank加载检测模型预训练权重重新训或者把 blank 类的初始 bias 设成负值抑制初期 blank 概率。5.5 验证集和测试集分布不一致现象验证集整行准确率 96%换一批实拍图掉到 70%。原因验证集和训练集来自同一批次光照、角度、轮胎品牌都相似模型没学到泛化特征。解决划分数据时按拍摄批次分层验证集里必须包含至少一个训练集没见过的批次。如果数据量允许留一个完整品牌作为测试集专门验证跨品牌泛化。6. 进阶技巧用合成数据把冷门字符的召回拉起来真实轮胎数据里某些字符比如Z、Q、W出现频率极低模型对它们几乎没学到。补数据靠实拍成本太高我一般用合成数据补。思路是拿真实背景图用 PIL 把字符渲染上去加光照和模糊增强生成带标注的合成样本。from PIL import Image, ImageDraw, ImageFont import random def synth_char_sample(bg_path, text, font_path, char_dict): bg Image.open(bg_path).convert(L) draw ImageDraw.Draw(bg) font ImageFont.truetype(font_path, random.randint(28, 42)) # 随机位置避开边缘 x random.randint(20, bg.width - 200) y random.randint(20, bg.height - 60) draw.text((x, y), text, fillrandom.randint(180, 255), fontfont) # 记录框用于检测标注 bbox draw.textbbox((x, y), text, fontfont) return bg, bbox, text逻辑说明背景用真实轮胎图保证纹理和噪声分布一致字符用系统字体渲染颜色在 180~255 之间模拟凸起高光。textbbox拿到渲染后的实际框直接转 YOLO 格式。参数说明字体大小 28~42 对应原图里字符行的实际高度要先量几张真实图确定范围。颜色下限 180 是防止字符太暗和背景混在一起。合成数据比例控制在总数据的 20% 以内太多会引入字体偏差。合成数据只补检测和识别的冷门字符不要全用合成数据训练否则模型学到的是渲染字体特征实拍字体一换就崩。我一般把合成样本按 1:4 混进真实数据训练时对合成样本降采样权重。验证合成数据有没有用看冷门字符的召回率变化。如果某个字符原来召回 60%混入合成后到 85% 以上说明有效如果整体准确率掉了说明合成比例过高或字体差异太大。这套方案从数据标注到检测识别跑通单卡一天内能出第一版结果。轮胎字符识别的门槛不在模型在数据质量和后处理细节。我踩过最深的坑是标注框太紧改扩边后整行准确率直接涨了 4 个点这个习惯一直保留到现在。希望帮到你。本文还有配套的精品资源点击获取