☰
基于OpenCV与轻量CNN的轮胎字符识别实战:从定位到分类全流程
2026/10/6 2:55:04 网站建设 项目流程

简介:这份资源面向高校机器学习课程学习者与期末大作业、课程设计需求者,聚焦轮胎字符识别这一典型图像分类任务,提供从数据到模型推理的完整实现方案。压缩包共156个文件,约333.12MB,其中19个Python源码文件承载核心算法与训练逻辑,63个png与27个jpg构成图像数据及识别结果样例,另含pdmodel、pdiparams、pdparams等模型权重与配置,以及txt、md说明文档和ttf字体文件,目录结构清晰,便于按模块查阅。代码注释详尽,新手也能看懂,下载后简单部署即可运行,适合作为课程设计或期末大作业直接使用。系统功能完善、界面美观、操作简单,具有较高的实际应用价值。目前已有298人学习关注,可作为轮胎字符识别方向的入门实践与高分项目参考。

1. 轮胎字符识别到底在识别什么:从一条产线质检需求说起

轮胎侧壁那圈凸起的字符,包含规格、生产日期、批次号、模具号,是追溯质量问题的唯一身份标识。硫化完成后字符被压进橡胶,对比度极低、曲面反光、字间距还不均匀,人工读一条胎要好几秒,夜班疲劳时误读率直线上升。机器学习作业里选这个题目,本质是让你用一套完整的图像分类或检测流程,把「读胎侧字符」这件事跑通。它适合正在做课程设计、想找一个非玩具数据集练手、又希望方案能讲清楚工业落地逻辑的人。核心链路不复杂:采集图像、定位字符区域、切分单字、训练分类器、输出结构化结果。真正难的是数据质量与预处理,而不是模型本身。下面按我实际做过的顺序拆开讲,每一步都给可复现的命令和参数。

2. 数据准备与字符区域定位:从原始胎侧图到可训练样本

2.1 轮胎字符数据集长什么样,为什么不能直接丢进 CNN

公开的轮胎字符数据通常以两种形态出现:一种是整张胎侧图加标注框,另一种是已经切好的单字符小图。课程作业里常见的是后者,按 0-9、A-Z 分文件夹存放,每个类别几十到几百张不等。直接把这些小图丢进卷积网络能跑出不错的准确率,但答辩时老师一问「字符怎么从整胎里找到的」就露馅。我的做法是保留两级结构:先用整图做字符区域检测,再用检测框裁出单字送分类。这样整条链路完整,也方便解释工业现场为什么需要先定位再识别。

数据量方面,如果每个字符类别低于 80 张,建议做离线增强再训练。增强不是随便旋转,轮胎字符有明确方向,旋转超过 ±10 度就不符合实际。我一般用亮度扰动、对比度拉伸、轻微平移和加高斯噪声这四种,把每类扩到 300 张左右。

2.2 用 OpenCV 做字符区域粗定位的完整脚本

工业现场光照相对可控,所以传统形态学方法在粗定位阶段仍然好用,而且比直接上深度学习检测省算力。下面这段脚本做的是:灰度化、CLAHE 增强、自适应二值化、形态学闭运算连字、轮廓筛选。

import cv2 import numpy as np def locate_char_region(img_path): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # CLAHE 限制对比度自适应直方图均衡,clipLimit 控制增强强度 clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) # 自适应二值化,blockSize 必须是奇数,C 是常数偏移 binary = cv2.adaptiveThreshold( enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize=35, C=10 ) # 闭运算把断开的笔画连起来,核大小按字符实际像素高度调 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) area = w * h # 过滤过小噪点和过大背景,阈值按图像分辨率等比换算 if 200 < area < 20000 and 0.2 < w / h < 5.0: boxes.append((x, y, w, h)) # 按 x 坐标排序,保证字符顺序从左到右 boxes.sort(key=lambda b: b[0]) return img, boxes if __name__ == "__main__": img, boxes = locate_char_region("tire_side_01.jpg") for i, (x, y, w, h) in enumerate(boxes): cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite(f"crop_{i:02d}.png", img[y:y+h, x:x+w]) cv2.imwrite("located_result.png", img)

逻辑说明:CLAHE 解决胎侧字符与背景对比度低的问题,clipLimit设 3.0 是经验值,再高会把橡胶纹理也放大成噪点。blockSize=35对应字符笔画宽度大约 10 到 15 像素的场景,如果你的图分辨率翻倍,这个值也要跟着翻。闭运算的核宽度 17 是为了把相邻字符之间的间隙保留、同时把同一字符断开的笔画连上,核太宽会把两个字粘成一个框。轮廓面积上下限和宽高比是过滤非字符区域的关键,这两个参数必须拿几张典型图先统计再定,不要照抄。

2.3 裁切与标签对齐:避免训练集里混进错误样本

定位框裁出来之后,文件名和标签的对应关系要严格。我习惯用「类别_序号.png」命名,比如A_001.png、7_023.png。如果原始数据里已经有标签文件,写一个校验脚本统计每个类别的数量,数量低于阈值的类别单独列出来做补充采集或增强。这一步不做,训练时某个类别只有十几张,模型会直接偏向多数类,准确率看着高但混淆矩阵很难看。

提示:裁切时向外扩 3 到 5 个像素,保留字符边缘,否则二值化后笔画末端容易被切掉,分类器学到的特征不完整。

3. 分类模型选型与训练:小样本下怎么把准确率做稳

3.1 为什么轮胎字符分类用轻量 CNN 比迁移学习更合适

轮胎字符类别数通常在 30 到 40 之间(数字加字母),单字图像是灰度、尺寸小、结构固定。这种任务用 ResNet50 这类大模型属于杀鸡用牛刀,训练慢、容易过拟合,而且作业环境往往没有 GPU。我的选择是自建一个 4 层卷积加 2 层全连接的轻量网络,参数量控制在 50 万以内,CPU 上也能在几分钟内跑完一个 epoch。如果非要用迁移学习,MobileNetV3 是上限,再大就不划算了。

数据划分按 7:2:1 分训练、验证、测试。注意同一个轮胎上的字符要分到同一集合,否则同一张胎的字符同时出现在训练和测试里,准确率会虚高。这个细节很多人忽略,答辩时被问到会很难解释。

3.2 PyTorch 训练脚本与关键参数设置

import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms # 输入统一到 32x32 灰度图,与工业相机常用 ROI 尺寸对齐 train_tf = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((32, 32)), transforms.RandomAffine(degrees=8, translate=(0.05, 0.05)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) val_tf = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) train_ds = datasets.ImageFolder("dataset/train", transform=train_tf) val_ds = datasets.ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=2) class TireCharNet(nn.Module): def __init__(self, num_classes): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128, 128), nn.ReLU(), nn.Dropout(0.4), nn.Linear(128, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = TireCharNet(num_classes=len(train_ds.classes)).to(device) # 交叉熵配合 Adam,学习率 1e-3 是这类小网络的稳妥起点 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=15, gamma=0.5) best_acc = 0.0 for epoch in range(60): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) pred = model(imgs).argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) acc = correct / total if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "best_tire_char.pth") print(f"epoch {epoch+1}, val_acc {acc:.4f}")

逻辑说明:RandomAffine的旋转角度限制在 8 度,是因为轮胎字符在实际成像中不会有大角度倾斜,增强过头反而引入不存在的样本。Dropout(0.4)放在全连接前,小样本下这个比例能明显压住过拟合。学习率用 StepLR 每 15 个 epoch 减半,比固定学习率更容易在后期收敛到更优点。weight_decay=1e-4是轻量正则,配合 Dropout 一起用。保存策略按验证集最优保存,不要用最后一个 epoch 的权重,小样本训练后期波动很常见。

3.3 训练过程要看哪些曲线,异常时怎么调

必须同时看训练损失、验证损失、验证准确率三条线。训练损失降、验证损失先降后升,是过拟合,加 Dropout 或加数据。两条损失都降但准确率上不去,检查标签有没有错位,尤其是文件名和类别文件夹对应关系。验证准确率震荡幅度超过 5 个百分点,把 batch size 调小到 32,或者把学习率降到 5e-4。这些判断不需要复杂工具,每个 epoch 打印一次就够。

4. 推理部署与结果输出:把单字识别串成完整读码流程

4.1 从单字分类到整串字符的拼接逻辑

单字分类器输出的是每个裁切块的类别和置信度,真正要交付的是「一整条胎侧字符」。拼接时按定位阶段的 x 坐标排序,把置信度低于阈值的块标记为待复核,不要直接丢弃,因为工业场景里漏读比误读更麻烦。我一般设置信度阈值 0.85,低于这个值的字符在结果里用问号占位,同时输出原始裁切图供人工确认。

import torch from PIL import Image from torchvision import transforms def decode_tire_string(model, crop_paths, class_names, device, conf_thr=0.85): tf = transforms.Compose([ transforms.Grayscale(1), transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]) ]) model.eval() result = [] with torch.no_grad(): for p in crop_paths: img = tf(Image.open(p)).unsqueeze(0).to(device) prob = torch.softmax(model(img), dim=1) conf, idx = prob.max(dim=1) if conf.item() >= conf_thr: result.append(class_names[idx.item()]) else: result.append("?") return "".join(result)

逻辑说明:conf_thr是业务参数,不是模型参数。产线要求零漏读时把阈值降到 0.7,代价是误读增多需要人工复核;要求高准确率时提到 0.9,代价是更多字符被标问号。这个值要根据实际质检标准来定,没有通用最优解。class_names的顺序必须和训练时ImageFolder的类别顺序一致,否则输出全是乱的,这个坑我踩过。

4.2 批量推理脚本与结果落盘格式

作业交付通常要求能对测试集批量跑出结果。写一个脚本遍历测试目录,输出 CSV,每行包含图片名、识别结果、平均置信度、低置信字符位置。这样老师或评审一眼能看出你的方案不只是跑通,还考虑了结果可追溯。

import os import csv import torch def batch_inference(model, test_dir, class_names, device, out_csv="result.csv"): rows = [] for fname in sorted(os.listdir(test_dir)): if not fname.lower().endswith((".png", ".jpg", ".jpeg")): continue # 假设单张测试图已经过定位裁切,或在此处调用定位函数 crop_paths = [os.path.join(test_dir, fname)] text = decode_tire_string(model, crop_paths, class_names, device) rows.append([fname, text]) with open(out_csv, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["filename", "predicted_text"]) writer.writerows(rows) return rows

逻辑说明:真实场景里一张胎侧图会裁出多个字符块,这里为了脚本简洁按单块演示,实际使用时把crop_paths换成定位阶段输出的列表即可。CSV 用 utf-8 编码,避免中文路径或特殊字符出错。输出文件名固定,方便后续脚本读取做准确率统计。

5. 避坑与排查:轮胎字符识别作业里最容易翻车的 5 个点

5.1 准确率虚高但实际读码全错

现象:验证集准确率 98%,但拿新图跑出来字符顺序完全乱。原因:训练时ImageFolder按文件夹名排序生成类别索引,推理时自己手写的class_names顺序不一致。解决:训练完立刻把train_ds.classes保存成 json,推理脚本读同一个文件,不要手动维护两份列表。

5.2 二值化后字符笔画断裂或粘连

现象:定位框把两个字粘成一个,或者一个字被切成两半。原因:adaptiveThreshold的blockSize和闭运算核尺寸不匹配当前图像分辨率。解决:先统计字符实际像素高度,blockSize设为笔画宽度的 2 到 3 倍,闭运算核宽度设为字符间距的 1.5 倍左右,拿 5 张典型图调参后再批量跑。

5.3 训练损失不下降,准确率卡在随机水平

现象:loss 一直在 3.5 附近,准确率等于类别数的倒数。原因:输入归一化参数和实际数据分布不匹配,或者标签全是同一个类。解决:打印一个 batch 的像素均值和方差,确认在 0 到 1 之间;再统计每个类别的样本数,如果某个类占 90% 以上,先做重采样或增强。

5.4 推理速度慢到无法接受

现象:单张图推理超过 2 秒。原因:模型没切到 eval 模式,Dropout 和 BatchNorm 还在训练状态;或者每次推理都重新加载模型。解决:推理前调model.eval(),模型只加载一次,用torch.no_grad()包住前向。这三步做完,CPU 上单字推理通常在 10 毫秒以内。

5.5 文档说明和代码对不上

现象:文档里写的参数和脚本里实际值不一致,评审按文档复现失败。原因:调参后只改了代码没同步文档。解决:把关键参数抽到一个config.yaml里,代码和文档都引用同一份配置,改一处全生效。这个习惯在课程作业里看起来多余,但答辩时能省很多解释成本。

6. 把作业做成可复现项目的几个进阶习惯

如果你想让这份作业在评分之外还有留存价值,我建议做三件事。第一,把定位、训练、推理拆成三个独立可执行的脚本,用一个run.sh串起来,别人拿到之后改一个数据路径就能跑通。第二,在 README 里写清楚数据来源、每类样本数、训练轮数、最终测试集准确率和混淆矩阵,不要只写「准确率很高」。第三,留一个error_cases文件夹,把分错的样本截图放进去,附一句你判断的原因。评审看到这个,会认为你真的理解模型边界,而不是碰巧调出一个好看的数字。

验证方法上,除了看整体准确率,一定要单独统计容易混的类别对,比如 0 和 O、1 和 I、8 和 B。轮胎字符里这几组混淆率通常最高,如果它们之间的误判超过 5%,说明模型学到的特征还不够细,可以考虑在输入里保留更多边缘信息,或者把输入尺寸从 32 提到 48。这个调整会带来训练时间增加,但混淆矩阵的改善通常值得。

我自己的习惯是每次改完参数先跑一个 10 epoch 的小实验,看验证损失趋势,趋势对了再跑完整训练。这样一轮调参从两小时压到二十分钟,省下来的时间用来检查数据标签。血泪经验是,标签错误带来的准确率损失,比任何超参数都大,而且最难发现。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询