简介:一份面向计算机、通信、人工智能、自动化等相关专业师生与从业者的机器学习期末大作业项目,基于机器学习完成轮胎字符识别,配套完整源码、预训练模型和使用说明,适合作为课程设计、期末大作业或毕业设计参考,也适合小白入门与进阶深造。资源共156个文件、333.12MB,含90张图片样本与结果图、19个Python代码文件、PaddlePaddle框架的模型参数与网络结构文件(pdmodel、pdparams、pdiparams等),以及md/txt说明文档,结构清晰便于按代码、模型、数据分类查阅。代码均已调试测试,可稳定运行,项目答辩评审分达98分,学习借鉴价值较高;说明文档覆盖环境配置、运行步骤与常见调整思路,降低了复现门槛。目前已有130人学习浏览,基础较强的读者可在原方案基础上修改扩展,实现更多样化的字符识别功能。
1. 轮胎字符识别这个期末作业,为什么值得认真做一遍
如果你正在机器学习期末作业选题,在“手写字符识别”和“轮胎字符识别”之间纠结,我建议选后者。一样是字符识别,但轮胎侧壁上的DOT码、规格号是曲面、凸起、带纹理和磨损的真实工业场景,比MNIST一类数据集更能体现完整的机器学习工程链路:采集、标注、检测、识别、部署。期末项目最常见的失分点不是模型不够新,而是数据乱、代码散、跑不出结果。这套“源码+模型+使用说明”的组合,价值不在于某个文件能跑通,而在于它把机器学习从调库变成了能交付的字符识别系统。下面按我实际做过的方案,把每一步讲清楚。
2. 从轮胎照片到标注数据:先解决拍得清和标得准
轮胎字符识别和车牌识别的最大区别在“字符载体”。车牌是平面印刷,字符对比度稳定;轮胎侧壁是弧面,字符常常是模具压出来的凸起或凹槽,在侧面光照下会产生阴影,磨损后笔画还会断开。如果先想模型、再想数据,期末作业大概率会卡在“模型挺好但识别不出来”。我做这个项目的顺序永远是:先拍真实胎壁照片,再做标注,再谈模型。
2.1 轮胎字符的常见形态与拍摄要点
先认识要识别的东西。轮胎字符一般有两类:一类是规格信息,例如195/65R15 91V,字符大小相对均匀,多在同一行;一类是DOT生产编号,由字母、数字和“·”组成,可能跨两行排列。此外还有品牌名、花纹代号。期末作业通常只做规格和DOT区域,避免类别膨胀。
拍摄要点按优先级排:
- 焦距贴紧字符所在平面,不要拍整个轮胎,否则字符在画面里不到30像素,检测模型很难学到有效特征。
- 尽量让字符行与相机成像平面平行。轮胎是弧面,从正上方垂直拍,中间清晰两边虚是正常的,但角度不要超过30度,否则透视变形太大。
- 光照避免正对胎壁的强反光。轮胎字符识别最怕“黑橡胶上的白色高光”,这会吃掉笔画。用柔光从侧面45度打光,让凸起字符形成阴影,反而更容易识别。
- 每张图只保留一类目标区域。如果一张图里既有规格又有DOT,建议先裁切成两个子图,避免检测模型混淆。
对于期末作业,200张真实照片是底线。不要想着全用公开数据集,轮胎字符识别没有特别统一的开源数据集,而且每批轮胎的字体、压痕深浅都不一样。我的经验是:先拍100张,跑一版检测模型,看哪些照片漏检,再补拍对应场景,数据质量会明显提升。
2.2 标注工具与格式转换:Labelme到YOLO格式
标注我推荐用Labelme。单张轮胎图里框出每一行字符,标签不要用字符本身,而是用text_area一类区域名。原因是识别交给下游CRNN,检测只负责把文字的横向排布切出来。如果用单字符框,标注工作量会大很多,而且曲面变形会让单字框重叠。
下面这段脚本把Labelme的JSON转成YOLO格式的txt。只处理矩形框,Labelme里用rectangle,输出为类别ID和归一化中心坐标。
import json import glob import os def labelme_to_yolo(json_path, out_dir, class_id=0): os.makedirs(out_dir, exist_ok=True) with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w, img_h = data["imageWidth"], data["imageHeight"] lines = [] for shape in data["shapes"]: if shape["shape_type"] != "rectangle": continue (x1, y1), (x2, y2) = shape["points"] # 处理画框方向:Labelme允许从右下角向左上角拖拽 x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h # 过滤掉面积过小的标签,比如只有几个像素的噪点 if bw < 0.01 or bh < 0.01: continue lines.append(f"{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") base = os.path.basename(json_path).replace(".json", ".txt") with open(os.path.join(out_dir, base), "w", encoding="utf-8") as f: f.write("\n".join(lines)) if __name__ == "__main__": for jp in glob.glob("data/labelme/*.json"): labelme_to_yolo(jp, "data/labels")逻辑说明:脚本把Labelme的矩形框坐标从像素值归一化到0~1之间,方便YOLO直接读取。class_id=0表示当前项目只检测一个类别,后续如果要同时检测规格和DOT,就把类别ID改成按名称映射。注意脚本会自动把两点坐标排序,避免从右向左画框导致负宽度。面积过滤参数0.01是按归一化后的宽高,约等于在1080p图像里过滤掉10像素以下的小框,实际使用可根据你的图像分辨率调整。
2.3 用合成数据给模型补课:字体渲染与背景增广
真实胎壁照片只有200张,对YOLO来说勉强能跑,但CRNN识别模型很容易因为字符样本太少而欠拟合。常见做法是用字体渲染生成合成字符图,再叠加真实轮胎背景纹理。轮胎字符接近工程字体,比如DIN 1451,可以用系统里接近的TTF字体模拟。
from PIL import Image, ImageDraw, ImageFont, ImageFilter import numpy as np import random def render_tire_text(text, font_path, img_size=(64, 320)): img = Image.new("L", img_size, random.randint(40, 70)) draw = ImageDraw.Draw(img) font = ImageFont.truetype(font_path, random.randint(28, 36)) # 让字符带一点纵向拉伸,模拟轮胎弧面 x = random.randint(5, 25) y = random.randint(10, 20) draw.text((x, y), text, font=font, fill=random.randint(180, 230)) # 随机高斯模糊,模拟橡胶颗粒感 if random.random() < 0.3: img = img.filter(ImageFilter.GaussianBlur(radius=1)) # 加一点椒盐噪声 arr = np.array(img) noise = np.random.choice([0, 255], size=arr.shape, p=[0.98, 0.02]) arr = np.clip(arr.astype(int) + noise.astype(int), 0, 255).astype(np.uint8) return arr逻辑说明:这段脚本生成灰度字符图,背景灰度随机在40~70模拟橡胶暗色,字符填充在180~230模拟凸起高光。字体大小、横向起始位置、模糊概率都是随机参数,它们直接影响合成样本的多样性。建议把字符集限定在规格和DOT里出现的字符:数字0~9、大写字母B D E H J L M N O P R S T U V W X Y Z、以及“/”“·”“-”。生成后用这些合成图和真实标注图混合训练,真实图占比不要低于30%,否则模型会依赖合成背景导致真实场景掉点。
3. 模型选型与训练:YOLO检测 + CRNN识别的两段式方案
模型部分我直接说结论:检测用YOLOv8,识别用轻量CRNN+CTC。这个组合是“yolo字符识别”方向最常见的落地套路,兼顾检测精度、训练成本和期末作业的可解释性。
3.1 为什么选两段式而不是端到端OCR
有些同学会用PaddleOCR直接识别轮胎字符,省事,但期末答辩大概率会被问住。通用OCR是为印刷体、阅读顺序整齐的文档设计的,遇到轮胎这种无词典、字符紧挨、无空格分割、曲面畸变严重的场景,识别准确率很容易掉到80%以下。更关键的是,通用OCR单字框不稳定,没法解释为什么出错。
两段式的好处在于责任分离:
- 检测段负责回答“字符在哪一行”,输出的是
text_area框,允许框内有少量背景。 - 识别段负责回答“这一行字符是什么”,输入是检测框裁剪图,输出定长或变长的字符序列。
如果最终错误,可以精确定位是检测框歪了还是识别模型把字符看错。期末答辩时,这种排查思路比“我调了PaddleOCR参数”更有说服力。而且模型都是小参数量,CPU也能做推理,不需要给老师现场演示时开GPU。
3.2 检测模型:YOLOv8的配置与训练命令
YOLOv8的紫外线安装一行命令,这里直接给训练配置。我会把数据配置单独写成YAML文件,避免改代码。
# tire_data.yaml path: ./data train: images/train val: images/val nc: 1 names: 0: text_area训练命令:
yolo detect train \ data=tire_data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/tire_det \ name=yolo_tire参数说明:yolov8n.pt是官方预训练的nano权重,适合期末作业的数据规模;如果显存充足、图像细节更多,换成yolov8s.pt会稳一点。imgsz=640是训练分辨率,轮胎字符如果原图里字符高度不足20像素,建议先用imgsz=960跑一版,再和640对比,不要盲目加大,否则显存直接翻倍。patience=20是20个epoch内验证集没提升就早停,既省时间又防止过拟合。这里我建议用lr0=0.01而不是默认的0.02,因为轮胎数据集背景相似度高,学习率太大很容易在训练初期震荡。
3.3 识别模型:CRNN+CTC的训练脚本和参数解释
识别模型我用常见的CRNN结构:卷积层提特征,双向LSTM建模序列,最后接CTC损失。为了期末作业不需要从零实现CTC,PyTorch自带torch.nn.CTCLoss。关键的训练循环片段如下。
import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class CRNN(nn.Module): def __init__(self, num_classes=32, hidden_size=64): super().__init__() # 省略卷积和RNN细节,重点看参数配置 self.cnn = nn.Sequential( nn.Conv2d(1, 16, 3, padding=1), nn.BatchNorm2d(16), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d((2, 2)), nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d((2, 2)), nn.Conv2d(64, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), # 这里把特征压成序列 ) self.rnn = nn.LSTM(input_size=64, hidden_size=hidden_size, bidirectional=True, batch_first=True) self.fc = nn.Linear(hidden_size * 2, num_classes) def forward(self, x): x = self.cnn(x) # 输出 (B, C, H, W) b, c, h, w = x.shape x = x.squeeze(2).permute(0, 2, 1) # 对每个时间步 out, _ = self.rnn(x) return self.fc(out) # 训练超参 num_epochs = 40 batch_size = 16 lr = 3e-4 num_classes = len("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ/-.")逻辑说明:CRNN输入是灰度字符行图片,输出是每个时间步的类别概率,类别数由字符表大小决定。这里用-代替空格,CTC的blank保留给num_classes-1。训练时注意把图片统一缩放到高度32、宽度不定,并把同一批次的图片按宽度排序后做pad_sequence,这样能减少无效计算。识别模型的学习率我习惯给3e-4,配合AdamW,比SGD稳定。CRNN训练不会很慢,40个epoch在单张消费级GPU或 MPS 上大约十几分钟到半小时,CPU也能接受但会慢不少。
4. 源码组织与使用说明:从训练到推理的完整跑通
期末作业的源码光有模型不行,老师最看重“照着使用说明能跑通”。仓库结构不要复杂到像工程框架,也不要只有两个裸脚本。
4.1 一个能直接交作业的目录结构
├── data/ │ ├── images/train/ │ ├── images/val/ │ ├── labels/train/ │ ├── labels/val/ │ └── tire_data.yaml ├── src/ │ ├── detect.py # YOLO检测推理 │ ├── crnn_train.py # 识别模型训练 │ ├── crnn_infer.py # 识别模型推理 │ └── utils.py # 字符表、解码函数 ├── models/ │ └── best.pt # 检测权重 ├── weights/ │ └── crnn.pt # 识别权重 ├── requirements.txt └── README.md逻辑说明:data目录直接放YOLO需要的数据集结构,src放三个Python脚本,models放检测权重,weights放识别权重,避免混在一起。.pt文件是PyTorch的权重格式,期末答辩时最好附一张检测可视化图和一张识别结果表格,帮助老师快速理解系统工作流程。requirements.txt里写清ultralytics、opencv-python、torch、numpy的版本范围,不要用>=无限定,否则环境装不上会浪费时间。
4.2 推理脚本:从图片到字符结果
推理脚本要能直接调用,输入一张轮胎图,输出字符序列。代码里我会把检测和识别串起来。
import cv2 import torch from ultralytics import YOLO from crnn_infer import CRNNInfer def tire_ocr_pipeline(image_path, det_weights="models/best.pt", crnn_weights="weights/crnn.pt"): det_model = YOLO(det_weights) crnn = CRNNInfer(crnn_weights) img = cv2.imread(image_path) results = det_model(img, conf=0.5, iou=0.45, verbose=False)[0] lines = [] for box in results.boxes.data.cpu().numpy(): x1, y1, x2, y2 = box[:4].astype(int) crop = img[y1:y2, x1:x2] if crop.size == 0: continue text = crnn.run(crop) lines.append((x1, y1, text)) lines.sort(key=lambda t: (t[1] // 20, t[0])) # 按行粗略排序 return "\n".join([t[2] for t in lines]) if __name__ == "__main__": print(tire_ocr_pipeline("test_tire.jpg"))逻辑说明:conf=0.5是检测置信度阈值,如果发现漏检,降到0.3观察是否包含更多候选框;iou=0.45是NMS参数,目标框比较紧密时增加到0.5。排序逻辑按Y坐标粗略分块,再用X坐标排序,适用于轮胎字符行接近水平的情况。如果照片有明显倾斜,排序会失效,这时需要先做透视矫正,这个在最后一章展开。crnn.run(crop)内部会把裁剪图缩放、转张量、过模型,再用贪心解码把连续重复字符合并。
4.3 README与使用说明该怎么写
README不是应付评阅,它是你自己两星期后重新跑代码的后悔药。我会固定四个部分:环境安装、数据准备、训练命令、推理命令。训练命令和推理命令直接复用上面贴的代码块,不要截图贴图,文字可复制才实用。使用说明里还要写一句“如果检测框跑偏,优先检查标注框是否包含字符上下白边;如果识别字符重复,优先检查CTC输入图片是否过宽导致序列长度差距大”。这两条是从实际踩坑里总结的,老师看到会认为你真的调试过,而不是只调通了默认参数。
5. 期末项目避坑指南:五个最影响成绩的问题
不管源码多干净,期末项目翻车总在几个固定点上。这里按现象、原因、解决的顺序写,基本覆盖了我身边同学踩过的坑,也包括教学运行环境上的坑。
5.1 检测框漂移和漏检:数据问题还是模型问题
现象:训练了100个epoch,验证集AP也有90%,但换一批没见过的轮胎照片,检测框经常偏左或漏掉整行字符。
原因:最常见是标注框紧贴字符外轮廓,没有包含字符上下的一小段橡胶背景。轮胎字符有凸起和阴影,模型在学习时把阴影也当成了目标部分,导致实际推理时响应位置偏移。另一种原因是训练集里没有“空轮胎壁”的负样本,模型不擅长判断“这里没有文字”。
解决:用Labelme重新检查200张图的标注框,统一向上和向下拓宽约8~10像素。然后从轮胎上没字符的区域截一堆负样本图,放大到imgsz尺寸,放入images/train,在标注txt里放一个空文件占位。YOLO对负样本的处理就是没有标签即可,它会学会背景的响应。
5.2 识别结果多字少字:CTC解码和标签对齐的坑
现象:检测框没偏,裁剪图肉眼看得很清楚,但识别输出不是多一个重复字符,就是少一个数字。
原因:CTC的blank机制要求相同字符连续出现时只能保留一个。如果模型把“O”和“0”都输出,并且它们在序列里相邻,贪心解码会把两个相同字符合并成一个。另一个原因是裁剪图高度和宽度比例差距太大,CRNN中池化后序列长度小于字符数,导致序列塞不下所有字符。
解决:先把CRNN输入图像的高度统一为32,宽度按比例缩放,不要做拉伸。然后检查卷积池化后特征图的宽度是否大于字符串最大长度,一般要求序列长度至少是字符数的2倍。如果还不行,在解码时用beam search替代贪心,torch.nn.CTCLoss解码用torch.argmax只是最快方式,不是最稳方式。
5.3 显存不够和训练中断:参数调小后模型不收敛
现象:老师在机房演示,显存只有4GB,batch_size从16改成4,结果训练loss从一开始就震荡,测试集识别全错。
原因:batch太小导致BatchNorm的统计量不稳定,学习率又没有跟着调。YOLO和CRNN里的BatchNorm对batch size敏感,小于8时收敛显著变慢。
解决:不要只改batch size,同时把学习率按比例降低,比如从lr0=0.01降到0.0025。另外开启AMP混合精度训练,YOLO的命令加amp=True,CRNN里用torch.cuda.amp.GradScaler,这能把显存占用压缩一半。如果一个GPU跑不动imgsz=640,降到480,不要用256,否则检测特征太粗糙。
5.4 验证集精度虚高:随机划分泄漏的典型表现
现象:训练集准确率99%,验证集准确率98%,但拿到现场采集的新轮胎照片就掉到85%,而且怎么加数据都提不回来。
原因:很多同学会把同一批轮胎的多张不同角度照片随机按80/20划分,模型在训练时已经看过同一物理轮胎的字符,验证时再看到只是换个角度,等于作弊。字符识别项目里数据泄漏的典型表现就是“验证集虚高”。
解决:按轮胎ID划分数据集。先把每张图像的文件名归到具体轮胎编号,比如tire_01_01.jpg,再按轮胎编号整体划分训练、验证、测试集。一个原则:同一轮胎的所有图只能出现在同一个集合里,一张都不能跨集合。
5.5 模型文件太大:交作业时怎么压缩和打包
现象:论文提交系统限制附件20MB,而YOLO检测权重加CRNN权重合计可能超过40MB,压缩包传不上去。
原因:PyTorch权重默认存了训练时的优化器状态、损失函数状态、锚框等额外字典,这些对推理没有用。YOLO的.pt文件也同时包含权重和配置。
解决:导出ONNX格式作为部署权重,或者只保存state_dict。YOLO可以用yolo export model=best.pt format=onnx dynamic=True,CRNN权重用torch.save(model.state_dict(), "crnn.onnx")或者直接保存onnx。推理脚本里把YOLO模型参数改成onnx文件路径,CRNN改成onnx。这样文件普遍能压到十几MB,而且测评时不需要担心PyTorch版本对不上。代码里注意ONNX的动态输入要保留最大序列维度,不然输入宽度变化时导出模型报错。
6. 让识别更稳的验证与增广技巧:置信度、透视矫正和字符错误率
6.1 用字符错误率代替准确率做验收
期末作业容易只看单张准确率,但轮胎字符识别真正该用的指标是字符错误率。准确率只看整行是否完全一致,DOT码错一位就是整行错,会把识别模型的中期状态误判为失败。我写过一个简单函数,按长度归一化的编辑距离算CER。
def cer(pred, truth): m, n = len(pred) + 1, len(truth) + 1 dp = [[0] * n for _ in range(m)] for i in range(m): dp[i][0] = i for j in range(n): dp[0][j] = j for i in range(1, m): for j in range(1, n): if pred[i-1] == truth[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 return dp[-1][-1] / m逻辑说明:CER越低越好,0.05代表平均每100个字符错5个。建议在测试集上把检测框裁剪结果和人工标注字符逐一计算CER,再把所有行的CER平均。如果整体CER在0.1以上,优先检查透视矫正而不是继续换模型。
6.2 推理前做透视矫正能救回三个点
轮胎是弧面,检测框裁出来的字符行可能左右高度不一致。常见做法是检测框拿到后,用边缘点做四点透视变换,把字符行拉平。我的习惯是先用YOLO检测出字符行的左上、右上、左下、右下四个边界点,再用OpenCV的getPerspectiveTransform变换到固定宽度。这里不展开完整代码,关键是矫正后CRNN的输入宽度更规整,序列对齐会明显变好。这个技巧放在推理脚本里,只影响预处理,不会增加训练复杂度。
6.3 一个每轮保存模型并回滚的脚本习惯
训练CRNN或YOLO时,不要只留最后一个epoch的权重。我习惯每个epoch都保存一次model_epoch{}.pt,并在验证集上记录CER。如果第30个epoch时验证集CER最低,但第35个epoch过拟合又涨上去了,直接回滚到第30个epoch的权重。很多同学在答辩前问“为什么training loss还降但测试结果变差”,答案就是过拟合后没有保存最优中间权重。这个习惯治标也治本。
我做这个项目留下的最深教训是:轮胎字符识别最难的不是模型,是让每一张脏照片、暗光照下的裁剪图都稳定输出。后来所有新数据我都先跑一遍完整推理,用CER和可视化结果一起看,眼睛确认哪一行错了,再去查是检测还是识别。这个方法帮我把模型从只认识“干净图”改成能抵抗真实轮胎表面。希望这个完整方案能帮你把这门机器学习期末作业做得既快又稳,更能在答辩时讲清楚每个决定背后的理由。
本文还有配套的精品资源,点击获取