ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

轮胎字符识别实战:从数据标注到CNN分类的完整链路

轮胎字符识别实战:从数据标注到CNN分类的完整链路 简介这份资源是面向计算机、通信、人工智能、自动化等专业学生与教师的机器学习期末大作业完整方案聚焦轮胎字符识别这一典型图像分类任务适合作为课程设计、毕业设计或进阶练手项目。包内共156个文件以19个Python源码、63张png与27张jpg样本及结果图、6组pdmodel与pdiparams推理模型文件为主另含ttf字体、md说明、yml配置与日志等辅助内容压缩包约333MB目录结构清晰便于按模块查阅。项目已通过调试测试答辩评审分达98分读者可据此掌握数据预处理、模型训练、推理部署与结果可视化的完整链路并参考使用说明快速复现实验。目前已有130人学习下载基础较好的学习者还可在此基础上修改调整实现不同字符识别功能。1. 轮胎字符识别到底难在哪一个期末作业级别的真实落地场景轮胎侧壁的字符识别是工业质检里一个典型的“看起来简单、做起来翻车”的任务。字符是凸起的、反光的、弧面分布的光照一变同一批轮胎的成像差异能大到让模型怀疑人生。很多同学做机器学习期末作业时选这个题目往往是因为它听起来够“工业”、够“完整”但真正动手才发现数据集要自己标、字符要自己切、模型要自己调最后还要写一份能让人看懂的使用说明。这个项目的核心价值就在于它把“轮胎字符识别”从一句口号拆成了一条可复现的链路——数据采集与标注、字符区域定位、单字符分割、分类模型训练、推理脚本封装最后配上一份能让别人跑起来的使用说明。适合谁适合正在找机器学习期末作业选题的本科生、想练手完整CV项目的新手以及需要快速搭一个字符识别baseline的工程师。它不追求SOTA追求的是“你能在一台普通笔记本上跑通并且知道每一步为什么这么做”。2. 从轮胎照片到字符样本数据准备与标注的完整链路2.1 为什么轮胎字符识别不能直接套用通用OCR通用OCR模型比如Tesseract、PaddleOCR的默认模型在文档场景下表现很好但直接拿来识别轮胎侧壁字符召回率会掉得很厉害。原因有三个第一轮胎字符是凸起橡胶边缘对比度低二值化后字符经常断连第二轮胎侧壁是弧面靠近边缘的字符存在透视形变第三工业现场的光照不均匀同一张图里有的字符过曝、有的欠曝。所以这个项目的技术路线不是“端到端OCR”而是“先定位字符区域再分割单字符最后做分类”。这样做的好处是每一步都可解释、可调试坏处是流程长、中间环节容易出错。我一般会建议如果你的期末作业时间有限优先保证“定位分割”这两个环节的鲁棒性分类模型反而可以用轻量级的CNN快速搞定。2.2 数据采集与标注用LabelImg还是自己写脚本轮胎字符识别的数据集没有现成的公开大规模数据必须自己采集。常见做法是用手机或工业相机在轮胎侧壁拍200500张照片覆盖不同光照、不同角度、不同磨损程度。标注工具用LabelImg就够了标注格式选Pascal VOCXML因为后续转YOLO格式或裁剪字符都用得上。标注时只框“字符区域”不要框单个字符——单字符分割交给后续的图像处理步骤。这里有个血泪经验标注框要尽量贴紧字符边缘但不要切掉字符的凸起部分否则分割时容易把字符切断。# 目录结构建议 tire_dataset/ ├── images/ # 原始照片 ├── annotations/ # LabelImg生成的XML ├── train.txt # 训练集文件列表 └── val.txt # 验证集文件列表标注完成后用脚本把XML里的字符区域裁剪出来保存为单独的图片。裁剪时向外扩510个像素给后续分割留余量。import xml.etree.ElementTree as ET import cv2 import os def crop_char_region(xml_path, img_path, save_dir, padding8): tree ET.parse(xml_path) root tree.getroot() img cv2.imread(img_path) h, w img.shape[:2] for obj in root.findall(object): bbox obj.find(bndbox) xmin max(0, int(bbox.find(xmin).text) - padding) ymin max(0, int(bbox.find(ymin).text) - padding) xmax min(w, int(bbox.find(xmax).text) padding) ymax min(h, int(bbox.find(ymax).text) padding) crop img[ymin:ymax, xmin:xmax] # 用原图名坐标命名避免冲突 name os.path.basename(img_path).split(.)[0] cv2.imwrite(f{save_dir}/{name}_{xmin}_{ymin}.jpg, crop)这段代码的逻辑很直接解析VOC格式的XML读取每个标注框向外扩padding像素后裁剪保存。padding参数建议设812太小会切掉字符边缘太大会引入过多背景。保存的文件名带坐标方便后续追溯。注意如果一张图里有多个字符区域这个脚本会分别裁剪后续分割步骤再对每个crop单独处理。2.3 字符区域预处理灰度化、去噪、二值化怎么选参数裁剪出来的字符区域还是RGB图需要先转灰度再做去噪和二值化。这里参数选择很关键直接决定后续分割能不能用。灰度化用cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)就行。去噪我一般用高斯滤波kernel size设(3,3)或(5,5)太大把字符边缘也模糊了。二值化用自适应阈值cv2.adaptiveThreshold比全局阈值稳定得多尤其是在光照不均的情况下。import cv2 def preprocess_char_region(img_path): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波去噪kernel不宜过大 blur cv2.GaussianBlur(gray, (3, 3), 0) # 自适应阈值blockSize11, C2 binary cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2 ) return binaryblockSize决定局部阈值的计算窗口设11表示每个像素参考周围11x11区域C是常数从均值里减去设2能去掉一些噪点。如果二值化后字符断连严重把blockSize调到15或21试试如果噪点太多把C调到4或5。这一步没有万能参数必须拿几张典型图试。3. 字符分割与分类模型把凸起橡胶上的字一个个抠出来3.1 基于轮廓的字符分割为什么投影法在轮胎上不好用常见的字符分割方法是垂直投影法统计每一列的白色像素数找波谷切分。但轮胎字符是凸起的二值化后字符之间经常有粘连投影法的波谷不明显切出来的字符要么粘在一起、要么被切碎。更稳的做法是基于轮廓用cv2.findContours找到所有连通区域然后按面积和宽高比过滤掉噪点和非字符区域。import cv2 import numpy as np def segment_characters(binary_img, min_area50, max_area5000): contours, _ cv2.findContours( binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) char_boxes [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) area w * h aspect_ratio w / float(h) # 过滤面积太小是噪点太大可能是整块区域 if area min_area or area max_area: continue # 字符宽高比一般在0.21.5之间 if aspect_ratio 0.15 or aspect_ratio 2.0: continue char_boxes.append((x, y, w, h)) # 按x坐标从左到右排序 char_boxes.sort(keylambda b: b[0]) return char_boxesmin_area和max_area要根据你的图片分辨率调。假设字符区域裁剪后宽度在200400像素单个字符面积大概在2002000之间那min_area设50、max_area设5000是合理的。宽高比过滤能去掉细长的噪声和过宽的粘连块。排序是为了让后续分类结果按字符顺序输出。如果分割后字符数量不对先可视化看一下二值图大概率是二值化参数需要调。3.2 分类模型选型CNN还是SVM参数怎么定分割出来的单字符图片尺寸归一化到32x32或28x28然后送分类模型。轮胎字符一般包括数字0-9和字母A-Z有些还有特殊符号类别数在36左右。模型选型上我一般会推荐一个小型CNN因为它在小数据集上比SVM稳定而且训练脚本好写。结构不用复杂两个卷积块两个全连接层就够了。import torch import torch.nn as nn class TireCharCNN(nn.Module): def __init__(self, num_classes36): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 16, 3, padding1), # 输入单通道灰度图 nn.ReLU(), nn.MaxPool2d(2), # 32x32 - 16x16 nn.Conv2d(16, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 16x16 - 8x8 ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(32 * 8 * 8, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, num_classes) ) def forward(self, x): x self.features(x) return self.classifier(x)输入是1x32x32的灰度图两个卷积块后特征图变成32x8x8展平后接128维全连接最后输出36类。Dropout设0.3防止过拟合。训练时用Adam学习率1e-3batch size 32跑50个epoch基本能收敛。如果验证集准确率卡在80%上不去优先检查分割质量——很多时候不是模型不行是分割出来的字符图里有太多背景或断笔。3.3 训练脚本与数据增强小样本下的必要操作轮胎字符数据集通常不大每个类别可能只有几十到几百张。不做数据增强模型很容易过拟合。我一般会加随机旋转±10度、随机平移±2像素、随机亮度调整。注意不要加水平翻转因为字符翻转后就不是原来的字了。from torchvision import transforms train_transform transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((32, 32)), transforms.RandomRotation(10), # 模拟拍摄角度偏差 transforms.RandomAffine(0, translate(0.1, 0.1)), # 模拟定位偏差 transforms.ColorJitter(brightness0.3), # 模拟光照变化 transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ])RandomRotation(10)对应轮胎弧面带来的轻微旋转RandomAffine的平移参数模拟分割时的定位误差ColorJitter的亮度变化模拟工业现场光照波动。归一化用0.5均值和0.5标准差把像素值压到[-1,1]训练更稳。验证集和测试集只做Resize和Normalize不做增强。4. 推理脚本与使用说明让别人也能跑起来的关键细节4.1 推理流程封装从一张轮胎照片到字符输出推理脚本要把前面的步骤串起来读图→定位字符区域→预处理→分割→分类→输出字符串。定位字符区域这一步如果训练了YOLO或SSD检测器就用检测器如果没有就用简单的轮廓筛选假设轮胎字符区域在图片中央且面积较大。为了简化这个项目可以先用轮廓面积筛选定位后续再升级检测器。def recognize_tire_chars(img_path, model, char_boxes, devicecpu): img cv2.imread(img_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (3, 3), 0) binary cv2.adaptiveThreshold( blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2 ) boxes segment_characters(binary) results [] for (x, y, w, h) in boxes: char_img gray[y:yh, x:xw] char_img cv2.resize(char_img, (32, 32)) tensor torch.tensor(char_img, dtypetorch.float32).unsqueeze(0).unsqueeze(0) / 255.0 tensor (tensor - 0.5) / 0.5 tensor tensor.to(device) with torch.no_grad(): output model(tensor) pred output.argmax(dim1).item() results.append(pred) return results这段代码把预处理、分割、分类串在一起。注意分类时的归一化要和训练时一致先除以255再减0.5除0.5。如果推理结果乱序检查segment_characters里的排序是否按x坐标。如果某个字符识别错误先看分割出来的小图是不是清晰可辨——人眼都看不清的模型更没戏。4.2 使用说明怎么写让助教和同学都能复现使用说明不是流水账要按“环境→数据→训练→推理”的顺序写清楚。环境部分列出Python版本、PyTorch版本、OpenCV版本给一条pip安装命令。数据部分说明目录结构和标注格式。训练部分给一条命令并说明关键参数在哪里改。推理部分给一条命令并说明输出格式。# 环境安装 pip install torch torchvision opencv-python numpy # 训练 python train.py --data_dir ./tire_dataset --epochs 50 --batch_size 32 --lr 0.001 # 推理 python inference.py --image ./test.jpg --model ./best_model.pth使用说明里要特别标注--data_dir指向你的数据集根目录里面要有images和annotations两个子目录--epochs根据数据集大小调小数据集50够了--lr如果loss震荡就降到5e-4。推理脚本的--image支持单张图片输出是识别到的字符序列。如果报错“CUDA out of memory”把batch_size降到16或8。4.3 模型保存与加载state_dict还是整个模型保存模型时我一般只保存state_dict不保存整个模型对象。因为整个模型对象依赖代码里的类定义换台机器或改了类名就加载不了。state_dict只存参数加载时先实例化模型再load_state_dict。# 保存 torch.save(model.state_dict(), best_model.pth) # 加载 model TireCharCNN(num_classes36) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval()map_locationcpu很重要否则在GPU上训练的模型在CPU机器上加载会报错。model.eval()切换到推理模式关掉Dropout和BatchNorm的训练行为。如果加载时报“Missing key(s)”说明保存和加载的模型结构不一致检查num_classes是否对得上。5. 避坑与排查轮胎字符识别项目里最容易翻车的5个地方5.1 二值化后字符断连分割出来全是碎片现象自适应阈值处理后字符笔画中间出现断裂轮廓分割出来一堆小碎片分类模型完全没法用。原因轮胎字符是凸起橡胶反光不均匀局部阈值在暗区把字符也当成背景了。解决先把blockSize从11调到21或31让阈值参考更大区域如果还不行改用Otsu全局阈值形态学闭运算闭运算kernel用(3,3)或(5,5)把断连补上。5.2 分割出来的字符顺序乱了输出字符串对不上现象识别结果里字符都对但顺序是乱的比如“ABC”输出成“BAC”。原因findContours返回的轮廓顺序不保证从左到右必须自己按x坐标排序。解决在segment_characters里加char_boxes.sort(keylambda b: b[0])。如果字符有换行还要先按y坐标分组再组内按x排序。5.3 训练准确率很高但推理时一塌糊涂现象验证集准确率95%但拿新照片推理识别率不到50%。原因训练集和推理时的预处理不一致最常见的是归一化参数不同或者训练时用了数据增强但推理时忘了对应的Resize。解决把预处理封装成一个函数训练和推理都调用同一个函数。检查归一化训练用Normalize((0.5,),(0.5,))推理也必须一样。5.4 模型加载报错“RuntimeError: Error(s) in loading state_dict”现象load_state_dict时报key不匹配要么多了module.前缀要么少了某些层。原因保存时用了DataParallel或DistributedDataParallelstate_dict的key会带module.前缀或者加载时模型结构改了。解决如果是module.前缀问题加载时用model.load_state_dict({k.replace(module.,): v for k,v in state_dict.items()})。如果是结构不一致打印两边的key对比缺哪层补哪层。5.5 使用说明里没写依赖版本别人跑不起来现象同学按你的说明安装报了一堆版本冲突最后放弃。原因使用说明只写了pip install torch opencv-python没写版本号不同版本的API有差异。解决在使用说明里固定版本比如torch2.0.1、opencv-python4.8.0.74。如果用了torchvision的transforms也要固定版本。最好附一个requirements.txt别人一条pip install -r requirements.txt搞定。6. 把识别率再往上推一截一个可验证的调优技巧如果你已经把基础流程跑通识别率卡在85%左右上不去我建议你试一个技巧对分割出来的每个字符图做一次“去背景”再送分类。具体做法是在二值化之后用轮廓的最小外接矩形把字符抠出来然后把矩形外的像素全部置为背景色白色或黑色取决于你的二值化方向。这样分类模型看到的输入更干净尤其是对于那些分割时带入了较多背景的字符效果提升明显。def remove_background(binary_img, char_box): x, y, w, h char_box roi binary_img[y:yh, x:xw] # 找到ROI里最大的轮廓只保留它 contours, _ cv2.findContours(roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return roi max_cnt max(contours, keycv2.contourArea) mask np.zeros_like(roi) cv2.drawContours(mask, [max_cnt], -1, 255, -1) # 背景置为0黑色字符保留 result cv2.bitwise_and(roi, mask) return result这个函数的逻辑是在字符ROI里找最大轮廓生成掩码只保留掩码内的像素。这样即使分割时框大了一点背景也会被去掉。验证方法很简单拿20张之前识别错误的图对比去背景前后的分类置信度。我实测下来对于背景复杂的样本置信度能从0.6提升到0.85以上。注意如果字符本身断成多个轮廓这个函数会只保留最大的那块反而丢字。所以用之前先确认你的二值化质量——字符是连通的才用这招。调参时还有一个习惯每次只改一个参数改完在验证集上跑一遍记录准确率。不要一次改三个参数否则你根本不知道是哪个起了作用。我一般会建一个简单的实验记录表列清楚日期、改动内容、验证集准确率。这个习惯在期末作业答辩时特别有用老师问“你为什么这么调”你能直接翻记录。希望帮到你。本文还有配套的精品资源点击获取
返回列表