首页 / 资讯中心 / 文章详情

基于OpenCV与轻量CNN的轮胎字符识别实战:从定位到分类全流程

基于OpenCV与轻量CNN的轮胎字符识别实战:从定位到分类全流程 ★ FEATURED ARTICLE
简介这份资源面向高校机器学习课程学习者与期末大作业、课程设计需求者聚焦轮胎字符识别这一典型图像分类任务提供从数据到模型推理的完整实现方案。压缩包共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(clipLimit3.0, tileGridSize(8, 8)) enhanced clahe.apply(gray) # 自适应二值化blockSize 必须是奇数C 是常数偏移 binary cv2.adaptiveThreshold( enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize35, C10 ) # 闭运算把断开的笔画连起来核大小按字符实际像素高度调 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(keylambda 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(fcrop_{i:02d}.png, img[y:yh, x:xw]) cv2.imwrite(located_result.png, img)逻辑说明CLAHE 解决胎侧字符与背景对比度低的问题clipLimit设 3.0 是经验值再高会把橡胶纹理也放大成噪点。blockSize35对应字符笔画宽度大约 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_channels1), transforms.Resize((32, 32)), transforms.RandomAffine(degrees8, translate(0.05, 0.05)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) val_tf transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) train_ds datasets.ImageFolder(dataset/train, transformtrain_tf) val_ds datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers2) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers2) class TireCharNet(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 128, 3, padding1), 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_classeslen(train_ds.classes)).to(device) # 交叉熵配合 Adam学习率 1e-3 是这类小网络的稳妥起点 criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size15, gamma0.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(dim1) 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(fepoch {epoch1}, val_acc {acc:.4f})逻辑说明RandomAffine的旋转角度限制在 8 度是因为轮胎字符在实际成像中不会有大角度倾斜增强过头反而引入不存在的样本。Dropout(0.4)放在全连接前小样本下这个比例能明显压住过拟合。学习率用 StepLR 每 15 个 epoch 减半比固定学习率更容易在后期收敛到更优点。weight_decay1e-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_thr0.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), dim1) conf, idx prob.max(dim1) 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_csvresult.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, encodingutf-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 的小实验看验证损失趋势趋势对了再跑完整训练。这样一轮调参从两小时压到二十分钟省下来的时间用来检查数据标签。血泪经验是标签错误带来的准确率损失比任何超参数都大而且最难发现。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站